From e360ede5cb23da3b704bf25b4810fc6cd441dd40 Mon Sep 17 00:00:00 2001 From: Brett Jia Date: Sat, 22 Apr 2023 19:54:15 -0400 Subject: [PATCH] rebroadcast: prebuffer on charging battery (#751) * rework battery prebuffer to take into account charger interface * rename handler * do not restart exited stream on low battery * tweak battery prebuffer state + periodically poll battery prebuffer state --- plugins/prebuffer-mixin/src/main.ts | 82 +++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/plugins/prebuffer-mixin/src/main.ts b/plugins/prebuffer-mixin/src/main.ts index 72a54e025..cfa80bf92 100644 --- a/plugins/prebuffer-mixin/src/main.ts +++ b/plugins/prebuffer-mixin/src/main.ts @@ -10,7 +10,7 @@ import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils'; import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin"; import { sleep } from '@scrypted/common/src/sleep'; import { createFragmentedMp4Parser, createMpegTsParser, StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser'; -import sdk, { BufferConverter, DeviceProvider, DeviceState, EventListenerRegister, FFmpegInput, H264Info, MediaObject, MediaStreamDestination, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from '@scrypted/sdk'; +import sdk, { BufferConverter, ChargeState, DeviceBase, DeviceProvider, DeviceState, EventListenerRegister, FFmpegInput, H264Info, MediaObject, MediaStreamDestination, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from '@scrypted/sdk'; import { StorageSettings } from '@scrypted/sdk/storage-settings'; import crypto from 'crypto'; import { once } from 'events'; @@ -105,7 +105,10 @@ class PrebufferSession { rtspServerPath: string; rtspServerMutedPath: string; - constructor(public mixin: PrebufferMixin, public advertisedMediaStreamOptions: ResponseMediaStreamOptions, public stopInactive: boolean) { + batteryListener: EventListenerRegister; + chargerListener: EventListenerRegister; + + constructor(public mixin: PrebufferMixin, public advertisedMediaStreamOptions: ResponseMediaStreamOptions, public enabled: boolean, public forceBatteryPrebuffer: boolean) { this.storage = mixin.storage; this.console = mixin.console; this.mixinDevice = mixin.mixinDevice; @@ -129,6 +132,12 @@ class PrebufferSession { this.rtspServerMutedPath = crypto.randomBytes(8).toString('hex'); this.storage.setItem(rtspServerMutedPathKey, this.rtspServerMutedPath); } + + this.handleChargingBatteryEvents(); + } + + get stopInactive() { + return !this.enabled || this.shouldDisableBatteryPrebuffer(); } get canPrebuffer() { @@ -206,6 +215,14 @@ class PrebufferSession { parserSession.kill(new Error('rebroadcast disabled')); this.clearPrebuffers(); }); + if (this.batteryListener) { + this.batteryListener.removeListener(); + this.batteryListener = null; + } + if (this.chargerListener) { + this.chargerListener.removeListener(); + this.chargerListener = null; + } } ensurePrebufferSession() { @@ -934,6 +951,46 @@ class PrebufferSession { }, 10000); } + handleChargingBatteryEvents() { + if (!this.mixin.interfaces.includes(ScryptedInterface.Charger) || + !this.mixin.interfaces.includes(ScryptedInterface.Battery)) { + return; + } + + const checkDisablePrebuffer = async () => { + if (this.stopInactive) { + this.console.log(this.streamName, 'low battery or not charging, prebuffering and rebroadcasting will only work on demand') + if (!this.activeClients && this.parserSessionPromise) { + this.console.log(this.streamName, 'terminating rebroadcast due to low battery or not charging') + const session = await this.parserSessionPromise; + session.kill(new Error('low battery or not charging')); + } + } else { + this.ensurePrebufferSession(); + } + } + + const id = this.mixin.id; + if (!this.batteryListener) { + this.batteryListener = systemManager.listenDevice(id, ScryptedInterface.Battery, () => checkDisablePrebuffer()); + } + if (!this.chargerListener) { + this.chargerListener = systemManager.listenDevice(id, ScryptedInterface.Charger, () => checkDisablePrebuffer()); + } + } + + shouldDisableBatteryPrebuffer(): boolean | null { + if (!this.mixin.interfaces.includes(ScryptedInterface.Battery)) { + return null; + } + if (this.forceBatteryPrebuffer) { + return false; + } + const lowBattery = this.mixin.batteryLevel == null || this.mixin.batteryLevel < 20; + const hasCharger = this.mixin.interfaces.includes(ScryptedInterface.Charger); + return !hasCharger || lowBattery || this.mixin.chargeState !== ChargeState.Charging; + } + async handleRebroadcasterClient(options: { findSyncFrame: boolean, isActiveClient: boolean, @@ -1497,8 +1554,6 @@ class PrebufferMixin extends SettingsMixinDeviceBase implements Vid } } - const isBatteryPowered = this.mixinDeviceInterfaces.includes(ScryptedInterface.Battery); - if (!enabledIds.length) this.online = true; @@ -1524,22 +1579,27 @@ class PrebufferMixin extends SettingsMixinDeviceBase implements Vid } const name = mso?.name; const enabled = enabledIds.includes(id); - const stopInactive = (isBatteryPowered && !mso.allowBatteryPrebuffer) || !enabled; - session = new PrebufferSession(this, mso, stopInactive); + session = new PrebufferSession(this, mso, enabled, mso.allowBatteryPrebuffer); this.sessions.set(id, session); - if (isBatteryPowered && !mso.allowBatteryPrebuffer) { - this.console.log('camera is battery powered, prebuffering and rebroadcasting will only work on demand.'); + if (!enabled) { + this.console.log('stream', name, 'is not enabled and will be rebroadcast on demand.'); continue; } - if (!enabled) { - this.console.log('stream', name, 'will be rebroadcast on demand.'); - continue; + if (session.shouldDisableBatteryPrebuffer()) { + this.console.log('camera is battery powered and either not charging or on low battery, prebuffering and rebroadcasting will only work on demand.'); } (async () => { while (this.sessions.get(id) === session && !this.released) { + if (session.shouldDisableBatteryPrebuffer()) { + // since battery devices could be eligible for prebuffer, check periodically + // in the event the battery device becomes eligible again + await new Promise(resolve => setTimeout(resolve, 60000)); + continue; + } + session.ensurePrebufferSession(); let wasActive = false; try {