diff --git a/plugins/homekit/package-lock.json b/plugins/homekit/package-lock.json index 7dc745d5c..980fbd738 100644 --- a/plugins/homekit/package-lock.json +++ b/plugins/homekit/package-lock.json @@ -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", diff --git a/plugins/homekit/package.json b/plugins/homekit/package.json index 029a7413b..0ffcf8597 100644 --- a/plugins/homekit/package.json +++ b/plugins/homekit/package.json @@ -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", diff --git a/plugins/homekit/src/hap-utils.ts b/plugins/homekit/src/hap-utils.ts index 039c367ca..2f4af477d 100644 --- a/plugins/homekit/src/hap-utils.ts +++ b/plugins/homekit/src/hap-utils.ts @@ -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) { const server: EventedHTTPServer = accessory._server.httpServer; server.on('connection-opened', connection => { connection.on('authenticated', () => { console.log('HomeKit Connection', connection.remoteAddress); + seenConnections.add(connection.remoteAddress); }); }); } diff --git a/plugins/homekit/src/main.ts b/plugins/homekit/src/main.ts index 14add0df0..caeb6be32 100644 --- a/plugins/homekit/src/main.ts +++ b/plugins/homekit/src/main.ts @@ -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(); bridge = new Bridge('Scrypted', getHAPUUID(this.storage)); snapshotThrottles = new Map(); standalones = new Map(); @@ -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); diff --git a/plugins/homekit/src/types/camera/camera-recording.ts b/plugins/homekit/src/types/camera/camera-recording.ts index 278f08aa4..744d02aff 100644 --- a/plugins/homekit/src/types/camera/camera-recording.ts +++ b/plugins/homekit/src/types/camera/camera-recording.ts @@ -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); } diff --git a/plugins/homekit/src/types/camera/camera-streaming.ts b/plugins/homekit/src/types/camera/camera-streaming.ts index c595d2cff..4f02f5d47 100644 --- a/plugins/homekit/src/types/camera/camera-streaming.ts +++ b/plugins/homekit/src/types/camera/camera-streaming.ts @@ -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, });