homekit: fix qr code accessory race condition. add slow connection setting to add all home hubs.

This commit is contained in:
Koushik Dutta
2022-05-24 09:51:22 -07:00
parent c2f7b0d965
commit 4c843c39fc
6 changed files with 66 additions and 26 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/homekit",
"version": "1.1.9",
"version": "1.1.12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/homekit",
"version": "1.1.9",
"version": "1.1.12",
"dependencies": {
"@koush/qrcode-terminal": "^0.12.0",
"@koush/werift-src": "file:../../external/werift",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/homekit",
"version": "1.1.9",
"version": "1.1.12",
"description": "HomeKit Plugin for Scrypted",
"scripts": {
"prepublishOnly": "NODE_ENV=production scrypted-webpack",

View File

@@ -111,11 +111,12 @@ export function createHAPUsernameStorageSettingsDict(): StorageSettingsDict<'mac
}
}
export function logConnections(console: Console, accessory: any) {
export function logConnections(console: Console, accessory: any, seenConnections: Set<string>) {
const server: EventedHTTPServer = accessory._server.httpServer;
server.on('connection-opened', connection => {
connection.on('authenticated', () => {
console.log('HomeKit Connection', connection.remoteAddress);
seenConnections.add(connection.remoteAddress);
});
});
}

View File

@@ -1,21 +1,19 @@
import qrcode from '@koush/qrcode-terminal';
import { StorageSettings } from '@scrypted/common/src/settings';
import { SettingsMixinDeviceOptions } from '@scrypted/common/src/settings-mixin';
import { sleep } from '@scrypted/common/src/sleep';
import sdk, { DeviceProvider, MixinProvider, Online, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceProperty, Setting, Settings } from '@scrypted/sdk';
import crypto from 'crypto';
import packageJson from "../package.json";
import { maybeAddBatteryService } from './battery';
import { CameraMixin, canCameraMixin } from './camera-mixin';
import { SnapshotThrottle, supportedTypes } from './common';
import { Category, Accessory, Bridge, Categories, Characteristic, ControllerStorage, EventedHTTPServer, MDNSAdvertiser, PublishInfo, Service } from './hap';
import { Accessory, Bridge, Categories, Characteristic, ControllerStorage, MDNSAdvertiser, PublishInfo, Service } from './hap';
import { createHAPUsernameStorageSettingsDict, getAddresses, getHAPUUID, getRandomPort as createRandomPort, initializeHapStorage, logConnections, typeToCategory } from './hap-utils';
import { HomekitMixin, HOMEKIT_MIXIN } from './homekit-mixin';
import { randomPinCode } from './pincode';
import './types';
import { VIDEO_CLIPS_NATIVE_ID } from './types/camera/camera-recording-files';
import { VideoClipsMixinProvider } from './video-clips-provider';
import crypto from 'crypto';
import { access } from 'fs';
const { systemManager, deviceManager } = sdk;
@@ -23,6 +21,7 @@ initializeHapStorage();
const includeToken = 4;
export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider, Settings, DeviceProvider {
seenConnections = new Set<string>();
bridge = new Bridge('Scrypted', getHAPUUID(this.storage));
snapshotThrottles = new Map<string, SnapshotThrottle>();
standalones = new Map<string, Accessory>();
@@ -91,10 +90,23 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
choices: [MDNSAdvertiser.BONJOUR, MDNSAdvertiser.CIAO],
defaultValue: MDNSAdvertiser.CIAO,
},
slowConnections: {
group: 'Network',
title: 'Slow Mode Addresses',
description: 'The addressesses of Home Hubs and iOS clients that will always be served remote/medium streams.',
type: 'string',
multiple: true,
combobox: true,
onGet: async () => {
return {
choices: [...this.seenConnections],
}
}
},
lastKnownHomeHub: {
hide: true,
description: 'The last home hub to request a recording. Internally used to determine if a streaming request is coming from remote wifi.',
}
},
});
constructor() {
@@ -222,7 +234,7 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
this.publishAccessory(accessory, storageSettings.values.mac, standaloneCategory);
if (!hasPublished) {
hasPublished = true;
logConnections(mixinConsole, accessory);
logConnections(mixinConsole, accessory, this.seenConnections);
qrcode.generate(accessory.setupURI(), { small: true }, (code: string) => {
mixinConsole.log('Pairing QR Code:')
@@ -256,7 +268,7 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
updateDeviceAdvertisement();
if (!published)
mixinConsole.warn('Device is in accessory mode and was offline during HomeKit startup. Device will not be started until it comes back online. Disable accessory mode if this is in error.');
// throttle this in case the device comes back online very quickly.
device.listen(ScryptedInterface.Online, () => {
const isOnline = !device.interfaces.includes(ScryptedInterface.Online) || device.online;
@@ -294,7 +306,7 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
};
this.bridge.publish(publishInfo, true);
logConnections(this.console, this.bridge);
logConnections(this.console, this.bridge, this.seenConnections);
qrcode.generate(this.bridge.setupURI(), { small: true }, (code: string) => {
this.console.log('Pairing QR Code:')
@@ -378,8 +390,12 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
ret = new HomekitMixin(options);
}
const accessory = this.standalones.get(mixinDeviceState.id);
ret.storageSettings.settings.qrCode.onPut = () => {
const accessory = this.standalones.get(mixinDeviceState.id);
if (!accessory) {
ret.console.error('Accessory not found. Try reloading the HomeKit plugin?');
return;
}
if (!accessory._setupID) {
ret.console.warn('This accessory is currently unpublished since it is offline. The accessory will be published now to generate the QR Code and allow pairing. The device may not respond to commands.');
const standaloneCategory = typeToCategory(ret.type);

View File

@@ -9,7 +9,7 @@ import fs from 'fs';
import mkdirp from 'mkdirp';
import net from 'net';
import { Duplex, Readable, Writable } from 'stream';
import { } from '../../common';
import { } from '../../common';
import { DataStreamConnection, AudioRecordingCodecType, AudioRecordingSamplerateValues, CameraRecordingConfiguration } from '../../hap';
import { getCameraRecordingFiles, HksvVideoClip, VIDEO_CLIPS_NATIVE_ID } from './camera-recording-files';
import { checkCompatibleCodec, FORCE_OPUS, transcodingDebugModeWarning } from './camera-utils';
@@ -122,14 +122,19 @@ export async function* handleFragmentsRequests(connection: DataStreamConnection,
const isDefinitelyNotAAC = !audioCodec || audioCodec.toLowerCase().indexOf('aac') === -1;
const transcodingDebugMode = storage.getItem('transcodingDebugMode') === 'true';
const transcodeRecording = !!ffmpegInput.h264EncoderArguments?.length;
const incompatibleStream = noAudio || transcodeRecording || isDefinitelyNotAAC;
const needsFFmpeg = transcodingDebugMode
|| !ffmpegInput.url.startsWith('tcp://')
|| !!ffmpegInput.h264EncoderArguments?.length
|| !!ffmpegInput.h264FilterArguments?.length
|| ffmpegInput.container !== 'mp4'
|| noAudio;
if (transcodingDebugMode)
transcodingDebugModeWarning();
let session: FFmpegFragmentedMP4Session & { socket?: Duplex };
if (ffmpegInput.container === 'mp4' && ffmpegInput.url.startsWith('tcp://') && !incompatibleStream) {
if (!needsFFmpeg) {
console.log('prebuffer is tcp/mp4/h264/aac compatible. using direct tcp.');
const socketUrl = new URL(ffmpegInput.url);
const socket = net.connect(parseInt(socketUrl.port), socketUrl.hostname);
@@ -218,6 +223,10 @@ export async function* handleFragmentsRequests(connection: DataStreamConnection,
];
}
if (ffmpegInput.h264FilterArguments?.length) {
videoArgs.push(...ffmpegInput.h264FilterArguments);
}
console.log(`motion recording starting`);
session = await startFFMPegFragmentedMP4Session(inputArguments, audioArgs, videoArgs, console);
}

View File

@@ -194,26 +194,40 @@ export function createCameraStreamingDelegate(device: ScryptedDevice & VideoCame
session.startRequest = request as StartStreamRequest;
const isHomeHub = homekitPlugin.storageSettings.values.lastKnownHomeHub?.includes(session.prepareRequest.targetAddress);
// ios is seemingly forcing all connections through the home hub on ios 15.5. this is test code to force low bandwidth.
// remote wifi connections request the same audio packet time as local wifi connections.
// so there's no way to differentiate between remote and local wifi. with low bandwidth forcing off,
// it will always select the local stream. with it on, it always selects the remote stream.
if (isHomeHub)
console.log('Streaming request is coming from the active HomeHub. Medium resolution stream will be selected in case this is a remote wifi connection or a wireless HomeHub. Using Accessory Mode is recommended.');
let forceSlowConnection = false;
try {
for (const address of homekitPlugin.storageSettings.values.slowConnections) {
if (address.includes(session.prepareRequest.targetAddress))
forceSlowConnection = true;
}
}
catch (e) {
}
if (forceSlowConnection) {
console.log('Streaming request is coming from a device in the slow mode connection list. Medium resolution stream will be selected.');
}
else {
// ios is seemingly forcing all connections through the home hub on ios 15.5. this is test code to force low bandwidth.
// remote wifi connections request the same audio packet time as local wifi connections.
// so there's no way to differentiate between remote and local wifi. with low bandwidth forcing off,
// it will always select the local stream. with it on, it always selects the remote stream.
forceSlowConnection = homekitPlugin.storageSettings.values.slowConnections?.includes(session.prepareRequest.targetAddress);
if (forceSlowConnection)
console.log('Streaming request is coming from the active HomeHub. Medium resolution stream will be selected in case this is a remote wifi connection or a wireless HomeHub. Using Accessory Mode is recommended if not already in use.');
}
const {
destination,
dynamicBitrate,
isLowBandwidth,
isWatch,
} = await getStreamingConfiguration(device, isHomeHub, storage, request)
} = await getStreamingConfiguration(device, forceSlowConnection, storage, request)
const hasHomeHub = !!homekitPlugin.storageSettings.values.lastKnownHomeHub;
const waitRtcp = isHomeHub || isLowBandwidth || !hasHomeHub;
const waitRtcp = forceSlowConnection || isLowBandwidth || !hasHomeHub;
if (waitRtcp) {
console.log('Will wait for initial RTCP packet.', {
isHomeHub,
isHomeHub: forceSlowConnection,
isLowBandwidth,
hasHomeHub,
});