@scrypted/prebuffer-mixin 0.1.196 → 0.1.197
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/main.ts +178 -66
package/dist/main.nodejs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(()=>{var e={510:function(e,t,i){"use strict";var r=this&&this.__createBinding||(Object.create?function(e,t,i,r){void 0===r&&(r=i),Object.defineProperty(e,r,{enumerable:!0,get:function(){return t[i]}})}:function(e,t,i,r){void 0===r&&(r=i),e[r]=t[i]}),n=this&&this.__exportStar||function(e,t){for(var i in e)"default"===i||Object.prototype.hasOwnProperty.call(t,i)||r(t,e,i)};Object.defineProperty(t,"__esModule",{value:!0}),t.MixinDeviceBase=t.ScryptedDeviceBase=void 0,n(i(268),t);const s=i(268);class o extends s.DeviceBase{constructor(e){super(),this.nativeId=e}get storage(){return this._storage||(this._storage=deviceManager.getDeviceStorage(this.nativeId)),this._storage}get log(){return this._log||(this._log=deviceManager.getDeviceLogger(this.nativeId)),this._log}get console(){return this._console||(this._console=deviceManager.getDeviceConsole(this.nativeId)),this._console}_lazyLoadDeviceState(){this._deviceState||(this.nativeId?this._deviceState=deviceManager.getDeviceState(this.nativeId):this._deviceState=deviceManager.getDeviceState())}onDeviceEvent(e,t){return deviceManager.onDeviceEvent(this.nativeId,e,t)}}t.ScryptedDeviceBase=o;class a extends s.DeviceBase{constructor(e){super(),this._listeners=new Set,this.mixinDevice=e.mixinDevice,this.mixinDeviceInterfaces=e.mixinDeviceInterfaces,this.mixinStorageSuffix=e.mixinStorageSuffix,this._deviceState=e.mixinDeviceState,this.mixinProviderNativeId=e.mixinProviderNativeId}get storage(){if(!this._storage){const e=this.mixinStorageSuffix,t=this.id+(e?":"+e:"");this._storage=deviceManager.getMixinStorage(t,this.mixinProviderNativeId)}return this._storage}get console(){return this._console||(deviceManager.getMixinConsole?this._console=deviceManager.getMixinConsole(this.id,this.mixinProviderNativeId):this._console=deviceManager.getDeviceConsole(this.mixinProviderNativeId)),this._console}onDeviceEvent(e,t){return deviceManager.onMixinEvent(this.id,this,e,t)}_lazyLoadDeviceState(){}manageListener(e){this._listeners.add(e)}release(){for(const e of this._listeners)e.removeListener()}}t.MixinDeviceBase=a,function(){function e(e){return function(){return this._lazyLoadDeviceState(),this._deviceState[e]}}function t(e){return function(t){this._lazyLoadDeviceState(),this._deviceState[e]=t}}for(var i of Object.values(s.ScryptedInterfaceProperty))Object.defineProperty(o.prototype,i,{set:t(i),get:e(i)}),Object.defineProperty(a.prototype,i,{set:t(i),get:e(i)})}();let c={};try{c=Object.assign(c,{log:deviceManager.getDeviceLogger(void 0),deviceManager,endpointManager,mediaManager,systemManager,pluginHostAPI})}catch(e){console.error("sdk initialization error, import @scrypted/types or use @scrypted/web-sdk instead",e)}t.default=c},268:(e,t,i)=>{"use strict";i.r(t),i.d(t,{DeviceBase:()=>r,ScryptedInterfaceProperty:()=>n,ScryptedInterfaceDescriptors:()=>s,ScryptedDeviceType:()=>o,HumidityMode:()=>a,FanMode:()=>c,TemperatureUnit:()=>d,ThermostatMode:()=>u,LockState:()=>p,MediaPlayerState:()=>l,ScryptedInterface:()=>h,ScryptedMimeTypes:()=>m});class r{}let n;!function(e){e.id="id",e.info="info",e.interfaces="interfaces",e.mixins="mixins",e.name="name",e.providedInterfaces="providedInterfaces",e.providedName="providedName",e.providedRoom="providedRoom",e.providedType="providedType",e.providerId="providerId",e.room="room",e.type="type",e.on="on",e.brightness="brightness",e.colorTemperature="colorTemperature",e.rgb="rgb",e.hsv="hsv",e.running="running",e.paused="paused",e.docked="docked",e.thermostatActiveMode="thermostatActiveMode",e.thermostatAvailableModes="thermostatAvailableModes",e.thermostatMode="thermostatMode",e.thermostatSetpoint="thermostatSetpoint",e.thermostatSetpointHigh="thermostatSetpointHigh",e.thermostatSetpointLow="thermostatSetpointLow",e.temperature="temperature",e.temperatureUnit="temperatureUnit",e.humidity="humidity",e.lockState="lockState",e.entryOpen="entryOpen",e.batteryLevel="batteryLevel",e.online="online",e.updateAvailable="updateAvailable",e.fromMimeType="fromMimeType",e.toMimeType="toMimeType",e.binaryState="binaryState",e.intrusionDetected="intrusionDetected",e.powerDetected="powerDetected",e.audioDetected="audioDetected",e.motionDetected="motionDetected",e.ambientLight="ambientLight",e.occupied="occupied",e.flooded="flooded",e.ultraviolet="ultraviolet",e.luminance="luminance",e.position="position",e.humiditySetting="humiditySetting",e.fan="fan"}(n||(n={}));const s={ScryptedDevice:{name:"ScryptedDevice",methods:["listen","probe","setName","setRoom","setType"],properties:["id","info","interfaces","mixins","name","providedInterfaces","providedName","providedRoom","providedType","providerId","room","type"]},ScryptedPlugin:{name:"ScryptedPlugin",methods:["getPluginJson"],properties:[]},OnOff:{name:"OnOff",methods:["turnOff","turnOn"],properties:["on"]},Brightness:{name:"Brightness",methods:["setBrightness"],properties:["brightness"]},ColorSettingTemperature:{name:"ColorSettingTemperature",methods:["getTemperatureMaxK","getTemperatureMinK","setColorTemperature"],properties:["colorTemperature"]},ColorSettingRgb:{name:"ColorSettingRgb",methods:["setRgb"],properties:["rgb"]},ColorSettingHsv:{name:"ColorSettingHsv",methods:["setHsv"],properties:["hsv"]},Notifier:{name:"Notifier",methods:["sendNotification"],properties:[]},StartStop:{name:"StartStop",methods:["start","stop"],properties:["running"]},Pause:{name:"Pause",methods:["pause","resume"],properties:["paused"]},Dock:{name:"Dock",methods:["dock"],properties:["docked"]},TemperatureSetting:{name:"TemperatureSetting",methods:["setThermostatMode","setThermostatSetpoint","setThermostatSetpointHigh","setThermostatSetpointLow"],properties:["thermostatActiveMode","thermostatAvailableModes","thermostatMode","thermostatSetpoint","thermostatSetpointHigh","thermostatSetpointLow"]},Thermometer:{name:"Thermometer",methods:["setTemperatureUnit"],properties:["temperature","temperatureUnit"]},HumiditySensor:{name:"HumiditySensor",methods:[],properties:["humidity"]},Camera:{name:"Camera",methods:["getPictureOptions","takePicture"],properties:[]},VideoCamera:{name:"VideoCamera",methods:["getVideoStream","getVideoStreamOptions"],properties:[]},VideoCameraConfiguration:{name:"VideoCameraConfiguration",methods:["setVideoStreamOptions"],properties:[]},Intercom:{name:"Intercom",methods:["startIntercom","stopIntercom"],properties:[]},Lock:{name:"Lock",methods:["lock","unlock"],properties:["lockState"]},PasswordStore:{name:"PasswordStore",methods:["addPassword","getPasswords","removePassword"],properties:[]},Authenticator:{name:"Authenticator",methods:["checkPassword"],properties:[]},Scene:{name:"Scene",methods:["activate","deactivate","isReversible"],properties:[]},Entry:{name:"Entry",methods:["closeEntry","openEntry"],properties:[]},EntrySensor:{name:"EntrySensor",methods:[],properties:["entryOpen"]},DeviceProvider:{name:"DeviceProvider",methods:["getDevice"],properties:[]},DeviceDiscovery:{name:"DeviceDiscovery",methods:["discoverDevices"],properties:[]},DeviceCreator:{name:"DeviceCreator",methods:["createDevice","getCreateDeviceSettings"],properties:[]},Battery:{name:"Battery",methods:[],properties:["batteryLevel"]},Refresh:{name:"Refresh",methods:["getRefreshFrequency","refresh"],properties:[]},MediaPlayer:{name:"MediaPlayer",methods:["getMediaStatus","load","seek","skipNext","skipPrevious"],properties:[]},Online:{name:"Online",methods:[],properties:["online"]},SoftwareUpdate:{name:"SoftwareUpdate",methods:["checkForUpdate","installUpdate"],properties:["updateAvailable"]},BufferConverter:{name:"BufferConverter",methods:["convert"],properties:["fromMimeType","toMimeType"]},Settings:{name:"Settings",methods:["getSettings","putSetting"],properties:[]},BinarySensor:{name:"BinarySensor",methods:[],properties:["binaryState"]},IntrusionSensor:{name:"IntrusionSensor",methods:[],properties:["intrusionDetected"]},PowerSensor:{name:"PowerSensor",methods:[],properties:["powerDetected"]},AudioSensor:{name:"AudioSensor",methods:[],properties:["audioDetected"]},MotionSensor:{name:"MotionSensor",methods:[],properties:["motionDetected"]},AmbientLightSensor:{name:"AmbientLightSensor",methods:[],properties:["ambientLight"]},OccupancySensor:{name:"OccupancySensor",methods:[],properties:["occupied"]},FloodSensor:{name:"FloodSensor",methods:[],properties:["flooded"]},UltravioletSensor:{name:"UltravioletSensor",methods:[],properties:["ultraviolet"]},LuminanceSensor:{name:"LuminanceSensor",methods:[],properties:["luminance"]},PositionSensor:{name:"PositionSensor",methods:[],properties:["position"]},Readme:{name:"Readme",methods:["getReadmeMarkdown"],properties:[]},OauthClient:{name:"OauthClient",methods:["getOauthUrl","onOauthCallback"],properties:[]},MixinProvider:{name:"MixinProvider",methods:["canMixin","getMixin","releaseMixin"],properties:[]},HttpRequestHandler:{name:"HttpRequestHandler",methods:["onRequest"],properties:[]},EngineIOHandler:{name:"EngineIOHandler",methods:["onConnection"],properties:[]},PushHandler:{name:"PushHandler",methods:["onPush"],properties:[]},Program:{name:"Program",methods:["run"],properties:[]},Scriptable:{name:"Scriptable",methods:["eval","loadScripts","saveScript"],properties:[]},ObjectDetector:{name:"ObjectDetector",methods:["getDetectionInput","getObjectTypes"],properties:[]},ObjectDetection:{name:"ObjectDetection",methods:["detectObjects","getDetectionModel"],properties:[]},HumiditySetting:{name:"HumiditySetting",methods:["setHumidity"],properties:["humiditySetting"]},Fan:{name:"Fan",methods:["setFan"],properties:["fan"]},RTCSignalingChannel:{name:"RTCSignalingChannel",methods:["startRTCSignalingSession"],properties:[]},RTCSignalingClient:{name:"RTCSignalingClient",methods:["createRTCSignalingSession"],properties:[]}};let o,a,c,d,u,p,l,h,m;!function(e){e.Builtin="Builtin",e.Camera="Camera",e.Fan="Fan",e.Light="Light",e.Switch="Switch",e.Outlet="Outlet",e.Sensor="Sensor",e.Scene="Scene",e.Program="Program",e.Automation="Automation",e.Vacuum="Vacuum",e.Notifier="Notifier",e.Thermostat="Thermostat",e.Lock="Lock",e.PasswordControl="PasswordControl",e.Display="Display",e.Speaker="Speaker",e.Event="Event",e.Entry="Entry",e.Garage="Garage",e.DeviceProvider="DeviceProvider",e.DataSource="DataSource",e.API="API",e.Doorbell="Doorbell",e.Irrigation="Irrigation",e.Valve="Valve",e.Person="Person",e.Unknown="Unknown"}(o||(o={})),function(e){e.Humidify="Humidify",e.Dehumidify="Dehumidify",e.Auto="Auto",e.Off="Off"}(a||(a={})),function(e){e.Auto="Auto",e.Manual="Manual"}(c||(c={})),function(e){e.C="C",e.F="F"}(d||(d={})),function(e){e.Off="Off",e.Cool="Cool",e.Heat="Heat",e.HeatCool="HeatCool",e.Auto="Auto",e.FanOnly="FanOnly",e.Purifier="Purifier",e.Eco="Eco",e.Dry="Dry",e.On="On"}(u||(u={})),function(e){e.Locked="Locked",e.Unlocked="Unlocked",e.Jammed="Jammed"}(p||(p={})),function(e){e.Idle="Idle",e.Playing="Playing",e.Paused="Paused",e.Buffering="Buffering"}(l||(l={})),function(e){e.ScryptedDevice="ScryptedDevice",e.ScryptedPlugin="ScryptedPlugin",e.OnOff="OnOff",e.Brightness="Brightness",e.ColorSettingTemperature="ColorSettingTemperature",e.ColorSettingRgb="ColorSettingRgb",e.ColorSettingHsv="ColorSettingHsv",e.Notifier="Notifier",e.StartStop="StartStop",e.Pause="Pause",e.Dock="Dock",e.TemperatureSetting="TemperatureSetting",e.Thermometer="Thermometer",e.HumiditySensor="HumiditySensor",e.Camera="Camera",e.VideoCamera="VideoCamera",e.VideoCameraConfiguration="VideoCameraConfiguration",e.Intercom="Intercom",e.Lock="Lock",e.PasswordStore="PasswordStore",e.Authenticator="Authenticator",e.Scene="Scene",e.Entry="Entry",e.EntrySensor="EntrySensor",e.DeviceProvider="DeviceProvider",e.DeviceDiscovery="DeviceDiscovery",e.DeviceCreator="DeviceCreator",e.Battery="Battery",e.Refresh="Refresh",e.MediaPlayer="MediaPlayer",e.Online="Online",e.SoftwareUpdate="SoftwareUpdate",e.BufferConverter="BufferConverter",e.Settings="Settings",e.BinarySensor="BinarySensor",e.IntrusionSensor="IntrusionSensor",e.PowerSensor="PowerSensor",e.AudioSensor="AudioSensor",e.MotionSensor="MotionSensor",e.AmbientLightSensor="AmbientLightSensor",e.OccupancySensor="OccupancySensor",e.FloodSensor="FloodSensor",e.UltravioletSensor="UltravioletSensor",e.LuminanceSensor="LuminanceSensor",e.PositionSensor="PositionSensor",e.Readme="Readme",e.OauthClient="OauthClient",e.MixinProvider="MixinProvider",e.HttpRequestHandler="HttpRequestHandler",e.EngineIOHandler="EngineIOHandler",e.PushHandler="PushHandler",e.Program="Program",e.Scriptable="Scriptable",e.ObjectDetector="ObjectDetector",e.ObjectDetection="ObjectDetection",e.HumiditySetting="HumiditySetting",e.Fan="Fan",e.RTCSignalingChannel="RTCSignalingChannel",e.RTCSignalingClient="RTCSignalingClient"}(h||(h={})),function(e){e.AcceptUrlParameter="accept-url",e.Url="text/x-uri",e.InsecureLocalUrl="text/x-insecure-local-uri",e.LocalUrl="text/x-local-uri",e.PushEndpoint="text/x-push-endpoint",e.MediaStreamUrl="text/x-media-url",e.FFmpegInput="x-scrypted/x-ffmpeg-input",e.RTCAVSignalingPrefix="x-scrypted-rtc-signaling-",e.SchemePrefix="x-scrypted/x-scrypted-scheme-",e.MediaObject="x-scrypted/x-scrypted-media-object"}(m||(m={}))}},t={};function i(r){var n=t[r];if(void 0!==n)return n.exports;var s=t[r]={exports:{}};return e[r].call(s.exports,s,s.exports,i),s.exports}i.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var r in t)i.o(t,r)&&!i.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),i.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r={};(()=>{"use strict";i.r(r),i.d(r,{default:()=>je});var e=i(510),t=i.n(e);const n=require("events"),{deviceManager:s}=t();class o extends e.MixinDeviceBase{constructor(t){super(t),this.settingsGroup=t.group,this.settingsGroupKey=t.groupKey,process.nextTick((()=>s.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)))}async getSettings(){const t=this.mixinDeviceInterfaces.includes(e.ScryptedInterface.Settings)?this.mixinDevice.getSettings():void 0,i=this.getMixinSettings(),r=[];try{const e=await t||[];r.push(...e)}catch(e){const t=this.name,i=`${t} Extension settings failed to load.`;this.console.error(i,e),r.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:i,readonly:!0})}try{const e=await i||[];for(const t of e)t.group=t.group||this.settingsGroup,t.key=this.settingsGroupKey+":"+t.key;r.push(...e)}catch(e){const t=s.getDeviceState(this.mixinProviderNativeId).name,i=`${t} Extension settings failed to load.`;this.console.error(i,e),r.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:i,readonly:!0})}return r}async putSetting(t,i){const r=this.settingsGroupKey+":";if(!t?.startsWith(r))return this.mixinDevice.putSetting(t,i);await this.putMixinSetting(t.substring(r.length),i),s.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)}async release(){await s.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)}}const a=require("net");var c=i.n(a);const d=require("child_process");var u=i.n(d);const p=require("dgram");var l=i.n(p);async function h(e,t){e.bind(t),await(0,n.once)(e,"listening");const i=e.address().port;return{port:i,url:`udp://127.0.0.1:${i}`}}async function m(e){return h(e,0)}async function f(e,t){return e.bind(t),await(0,n.once)(e,"listening"),{port:t,url:`udp://127.0.0.1:${t}`}}async function g(){const e=new(c().Server),t=await async function(e){return e.listen(0),await(0,n.once)(e,"listening"),e.address().port}(e);return{url:`tcp://127.0.0.1:${t}`,port:t,clientPromise:new Promise(((t,i)=>{const r=setTimeout((()=>{i(new Error("timeout waiting for client"))}),3e4);e.on("connection",(i=>{e.close(),clearTimeout(r),t(i)}))}))}}const y=require("process");var v=i.n(y);const S=["decode_slice_header error","no frame!","non-existing PPS"];function b(e,t,i,r){const n=!!v().env.SCRYPTED_FFMPEG_NOISY||!!r?.getItem("SCRYPTED_FFMPEG_NOISY");function s(e){const r=s=>{const o=s.toString();for(const e of S)if(-1!==o.indexOf(e))return;if(!(n||i||-1===o.indexOf("frame=")&&-1===o.indexOf("size=")))return e(o),e("video/audio detected, discarding further input"),t.stdout.removeListener("data",r),void t.stderr.removeListener("data",r);e(o)};return r}t.stdout?.on("data",s(e.log)),t.stderr?.on("data",s(e.error)),t.on("exit",(()=>e.log("ffmpeg exited")))}function w(e,t){const i=[];for(const e of t)try{const t=new URL(e);i.push(`${t.protocol}[REDACTED]`)}catch(t){i.push(e)}e.log(i.join(" "))}const{mediaManager:P}=t();async function I(e,t){return new Promise((i=>{const r=n=>{const s=n.toString(),o=s.indexOf(`${t}: `);if(-1!==o){const n=s.substring(o+t.length+1).trim();let a=n.indexOf(" ");const c=n.indexOf(",");-1!==a&&c<a&&(a=c),-1!==a&&(e.stdout.removeListener("data",r),e.stderr.removeListener("data",r),i(n.substring(0,a)))}};e.stdout.on("data",r),e.stderr.on("data",r)}))}function M(e,t,i,r){let n;function s(){console.error("timeout waiting for data, killing parser session",e),t()}function o(){r&&(clearTimeout(n),n=setTimeout(s,r))}return i.once("killed",(()=>clearTimeout(n))),o(),{resetActivityTimer:o}}async function C(e,t){const{console:i}=t;let r,s=!0;const o=new n.EventEmitter;let a,c,d,p,h;o.on("error",(e=>i.error("rebroadcast error",e)));const y=new Promise(((e,t)=>{p=e,h=t}));function v(){s&&(o.emit("killed"),o.emit("error",new Error("killed"))),s=!1,D?.kill(),setTimeout((()=>D?.kill("SIGKILL")),1e3),h?.(new Error("ffmpeg was killed before connecting to the rebroadcast session")),clearTimeout(r)}const S=e.inputArguments.slice();r=setTimeout(v,3e4);let C=!1;const x=["pipe","pipe","pipe"];let O=3;for(const e of Object.keys(t.parsers)){const r=t.parsers[e];if(r.parseDatagram){C=!0;const i=l().createSocket("udp4"),n=await m(i),s=l().createSocket("udp4");await f(s,n.port+1),o.once("killed",(()=>{i.close(),s.close()})),S.push(...r.outputArguments,n.url.replace("udp://","rtp://"));const{resetActivityTimer:a}=M(e,v,o,t?.timeout);(async()=>{for await(const t of r.parseDatagram(i,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),o.emit(e,t),a()})(),(async()=>{for await(const t of r.parseDatagram(s,parseInt(d?.[2]),parseInt(d?.[3]),"rtcp"))p?.(void 0),o.emit(e,t),a()})()}else if(r.tcpProtocol){const n=await g(),s=new URL(r.tcpProtocol);s.port=n.port.toString(),S.push(...r.outputArguments,s.toString());const{resetActivityTimer:a}=M(e,v,o,t?.timeout);(async()=>{const t=await n.clientPromise;try{for await(const i of r.parse(t,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),o.emit(e,i),a()}catch(e){i.error("rebroadcast parse error",e),v()}})()}else S.push(...r.outputArguments,"pipe:"+O++),x.push("pipe")}C&&(S.push("-sdp_file","pipe:"+O++),x.push("pipe")),S.unshift("-hide_banner"),w(i,S);const D=u().spawn(await P.getFFmpegPath(),S,{stdio:x});let T;b(i,D,void 0,t?.storage),D.on("exit",v),T=C?new Promise((e=>{const t=[];D.stdio[O-1].on("data",(i=>{t.push(i),e(t)}))})):Promise.resolve([]);let A=0;return Object.keys(t.parsers).forEach((async e=>{const r=t.parsers[e];if(!r.parse||r.tcpProtocol)return;const n=D.stdio[3+A];A++;try{const{resetActivityTimer:i}=M(e,v,o,t?.timeout);for await(const t of r.parse(n,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),o.emit(e,t),i()}catch(e){i.error("rebroadcast parse error",e),v()}})),async function(e){return I(e,"Audio")}(D).then((e=>a=e)),async function(e){return I(e,"Video")}(D).then((e=>c=e)),async function(e){return new Promise((t=>{const i=r=>{const n=r.toString(),s=/(([0-9]{2,5})x([0-9]{2,5}))/.exec(n);s&&(e.stdout.removeListener("data",i),e.stderr.removeListener("data",i),t(s))};e.stdout.on("data",i),e.stderr.on("data",i)}))}(D).then((e=>d=e)),await y,p=void 0,h=void 0,clearTimeout(r),{sdp:T,inputAudioCodec:a,inputVideoCodec:c,inputVideoResolution:d,isActive:()=>s,kill:v,mediaStreamOptions:e.mediaStreamOptions||{id:void 0,name:void 0},emit(e,t){return o.emit(e,t),this},on(e,t){return o.on(e,t),this},once(e,t){return o.once(e,t),this},removeListener(e,t){return o.removeListener(e,t),this}}}async function x(e,t){const i=await e;let r=!0;const n=()=>{i.removeAllListeners(),i.destroy();const e=s;s=void 0,e?.()};let s=t?.connect((e=>{r&&(r=!1,e.startStream&&i.write(e.startStream));for(const t of e.chunks)i.write(t);return i.writableLength}),n);i.once("close",(()=>{n()})),i.on("error",(e=>t?.console?.log("client stream ended")))}async function O(e,t){if(!t)return Buffer.alloc(0);{const i=e.read(t);if(i)return i}return new Promise(((i,r)=>{const n=()=>{const r=e.read(t);r&&(o(),i(r))},s=()=>{o(),r(new Error(`stream ended during read for minimum ${t} bytes`))},o=()=>{e.removeListener("readable",n),e.removeListener("end",s)};e.on("readable",n),e.on("end",s)}))}const D="\n".charCodeAt(0);async function T(e){return async function(e,t){const i=[];let r=0;for(;;){const n=await O(e,1);if(!n)throw new Error("end of stream");if(n[0]===t)break;i[r++]=n[0]}return Buffer.from(i).toString()}(e,D)}function A(e){return e}async function*k(e){for(;;){const t=await O(e,8),i=t.readInt32BE(0)-8,r=t.slice(4).toString(),n=await O(e,i);yield{header:t,length:i,type:r,data:n}}}async function*E(e){let t,i,r;for await(const n of e)t?i||(i=n):t=n,yield{startStream:r,chunks:[n.header,n.data],type:n.type},t&&i&&!r&&(r=Buffer.concat([t.header,t.data,i.header,i.data]))}const{systemManager:R}=t(),_="v4";class L extends e.ScryptedDeviceBase{hasEnabledMixin={};constructor(t){super(t);try{this.hasEnabledMixin=JSON.parse(this.storage.getItem("hasEnabledMixin"))}catch(e){this.hasEnabledMixin={}}this.pluginsComponent=R.getComponent("plugins"),R.listen((async(t,i,r)=>{i.eventInterface!==e.ScryptedInterface.ScryptedDevice||i.property||this.maybeEnableMixin(t)}));for(const e of Object.keys(R.getSystemState())){const t=R.getDeviceById(e);this.maybeEnableMixin(t)}}async shouldEnableMixin(e){return!0}async maybeEnableMixin(e){if(!e||e.mixins?.includes(this.id))return;if(this.hasEnabledMixin[e.id]===_)return;if(!await this.canMixin(e.type,e.interfaces))return;if(!await this.shouldEnableMixin(e))return;this.log.i("auto enabling mixin for "+e.name);const t=(e.mixins||[]).slice();t.push(this.id);const i=await this.pluginsComponent;await i.setMixins(e.id,t),this.setHasEnabledMixin(e.id)}setHasEnabledMixin(e){this.hasEnabledMixin[e]!==_&&(this.hasEnabledMixin[e]=_,this.storage.setItem("hasEnabledMixin",JSON.stringify(this.hasEnabledMixin)))}}const{mediaManager:U}=t();function B(e,t,i=["recvonly","sendrecv"]){const r=e.split("m=").filter((e=>e.startsWith(t)));for(const e of r){const t=()=>{const t=e.split("\n").map((e=>e.trim())).find((e=>e.startsWith("a=control:")));return{section:"m="+e,trackId:t?.split("a=control:")?.[1]}};for(const r of i)if(e.includes(`a=${r}`))return t();if(i.includes("recvonly")&&!e.includes("sendonly")&&!e.includes("inactive"))return t()}}const j=require("crypto");var H=i.n(j);const $=require("tls");var F=i.n($);const N=require("os");function V(e,t){for(var i=0;i<t.length;i++){var r=t[i];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function K(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function q(e){var t="function"==typeof Map?new Map:void 0;return q=function(e){if(null===e||(i=e,-1===Function.toString.call(i).indexOf("[native code]")))return e;var i;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,r)}function r(){return W(e,arguments,J(this).constructor)}return r.prototype=Object.create(e.prototype,{constructor:{value:r,enumerable:!1,writable:!0,configurable:!0}}),z(r,e)},q(e)}function W(e,t,i){return W=G()?Reflect.construct:function(e,t,i){var r=[null];r.push.apply(r,t);var n=new(Function.bind.apply(e,r));return i&&z(n,i.prototype),n},W.apply(null,arguments)}function G(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}function z(e,t){return z=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},z(e,t)}function J(e){return J=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},J(e)}let Y=function(e){function t(e,i,...r){var n,s,o;return function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t),e instanceof Array||(r=(void 0===i?[]:[i]).concat(r),i=e,e=[]),s=this,(n=!(o=J(t).call(this,i))||"object"!=typeof o&&"function"!=typeof o?K(s):o).code=i||"E_UNEXPECTED",n.params=r,n.wrappedErrors=e,n.name=n.toString(),Error.captureStackTrace&&Error.captureStackTrace(K(n),n.constructor),n}var i,r,n;return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&z(e,t)}(t,e),i=t,(r=[{key:"toString",value:function(){return(this.wrappedErrors.length?this.wrappedErrors[this.wrappedErrors.length-1].stack+N.EOL:"")+this.constructor.name+": "+this.code+" ("+this.params.join(", ")+")"}}])&&V(i.prototype,r),n&&V(i,n),t}(q(Error));function Q(e){return e instanceof Y||e.constructor&&e.constructor.name&&e.constructor.name.endsWith("Error")&&"string"==typeof e.code&&X(e.code)&&e.params&&e.params instanceof Array}function X(e){return/^([A-Z0-9_]+)$/.test(e)}Y.wrap=function(e,t,...i){let r=null;const n=X(e.message),s=(e.wrappedErrors||[]).concat(e);return t||(t=n?e.message:"E_UNEXPECTED"),e.message&&!n&&i.push(e.message),r=new Y(s,t,...i),r},Y.cast=function(e,...t){return Q(e)?e:Y.wrap.apply(Y,[e].concat(t))},Y.bump=function(e,...t){return Q(e)?Y.wrap.apply(Y,[e,e.code].concat(e.params)):Y.wrap.apply(Y,[e].concat(t))};const Z=Y,ee=/\w+=(".*?"|[^",]+)(?=,|$)/g;function te(e,t,i=[]){const r=e.trim().match(ee);if(!r)throw new Z("E_MALFORMED_QUOTEDKEYVALUE",e);const n=r.map(((e,t)=>{const i=e.split("=");if(2!==i.length)throw new Z("E_MALFORMED_QUOTEDKEYVALUE",t,e,i.length);return i})).reduce((function(e,[i,r],n){if(-1===t.indexOf(i))throw new Z("E_UNAUTHORIZED_KEY",n,i);return e[i]=r.replace(/^"(.+(?="$))"$/,"$1"),e}),{});return re(i,n),n}function ie(e,t,i=[]){return re(i,e),t.reduce((function(t,i){return e[i]?t+(t?", ":"")+i+'="'+e[i]+'"':t}),"")}function re(e,t){e.forEach((e=>{if(void 0===t[e])throw new Z("E_REQUIRED_KEY",e)}))}const ne=["realm","nonce"],se=[...ne,"domain","opaque","stale","algorithm","qop"],oe=["username","realm","nonce","uri","response"],ae=[...oe,"algorithm","cnonce","opaque","qop","nc"];function ce(e,t){const i=H().createHash(e);return i.update(t),i.digest("hex")}const de={type:"Digest",parseWWWAuthenticateRest:function(e){return te(e,se,ne)},buildWWWAuthenticateRest:function(e){return ie(e,se,ne)},parseAuthorizationRest:function(e){return te(e,ae,oe)},buildAuthorizationRest:function(e){return ie(e,ae,oe)},computeHash:function(e){const t=e.ha1||ce(e.algorithm,[e.username,e.realm,e.password].join(":")),i=ce(e.algorithm,[e.method,e.uri].join(":"));return ce(e.algorithm,[t,e.nonce,e.nc,e.cnonce,e.qop,i].join(":"))}};async function ue(e){let t=[];for(;;){let i=await T(e);if(i=i.trim(),!i)return t;t.push(i)}}function pe(e){const t={};for(const i of e.slice(1)){const e=i.indexOf(":");let r="";-1!==e&&(r=i.substring(e+1).trim());t[i.substring(0,e).toLowerCase()]=r}return t}class le extends class{constructor(e){this.console=e}write(e,t,i){let r=`${e}\r\n`;i&&(t["Content-Length"]=i.length.toString());for(const[e,i]of Object.entries(t))r+=`${e}: ${i}\r\n`;r+="\r\n",this.client.write(r),this.console?.log("rtsp outgpoing message\n",r),this.console?.log(),i&&this.client.write(i)}async readMessage(){const e=await ue(this.client);return this.console?.log("rtsp incoming message\n",e.join("\n")),this.console?.log(),e}}{cseq=0;constructor(e,t){super(t),this.url=e;const i=new URL(e),r=parseInt(i.port)||554;e.startsWith("rtsps")?this.client=F().connect({rejectUnauthorized:!1,port:r,host:i.hostname}):this.client=c().connect(r,i.hostname)}writeRequest(e,t,i,r){t=t||{};let n=this.url;i&&(n+="/"+i);const s=new URL(n);s.username="",s.password="",n=s.toString();const o=`${e} ${n} RTSP/1.0`,a=this.cseq++;t.CSeq=a.toString(),this.authorization&&(t.Authorization=this.authorization),this.session&&(t.Session=this.session),this.write(o,t,r)}async request(e,t,i,r,n){this.writeRequest(e,t,i,r);const s=await this.readMessage(),o=s[0],a=pe(s);if(!o.includes("200")&&!a["www-authenticate"])throw new Error(o);if(a["www-authenticate"]){if(n)throw new Error("auth failed");const s=new URL(this.url),o=de.parseWWWAuthenticateRest(a["www-authenticate"]),c=H().createHash("md5").update(`${s.username}:${o.realm}:${s.password}`).digest("hex"),d=H().createHash("md5").update(`${e}:${s.pathname}`).digest("hex"),u=H().createHash("md5").update(`${c}:${o.nonce}:${d}`).digest("hex"),p={username:s.username,realm:o.realm,nonce:o.nonce,uri:s.pathname,algorithm:"MD5",response:u},l=Object.entries(p).map((([e,t])=>{return`${e}=${t&&(i=t,`"${i.replace(/"/g,'\\"')}"`)}`;var i})).join(", ");return this.authorization=`Digest ${l}`,this.request(e,t,i,r,!0)}const c=parseInt(a["content-length"]);return c?{headers:a,body:await O(this.client,c)}:{headers:a,body:void 0}}async options(){return this.request("OPTIONS",{})}async writeGetParameter(){return this.writeRequest("GET_PARAMETER",{})}async describe(e){return this.request("DESCRIBE",{...e||{},Accept:"application/sdp"})}async setup(e,t){const i={Transport:`RTP/AVP/TCP;unicast;interleaved=${e}-${e+1}`},r=await this.request("SETUP",i,t);if(r.headers.session){const e=function(e){const t={};for(const i of e.split(";")){const[e,r]=i.split("=",2);t[e]=r}return t}(r.headers.session);let t=parseInt(e.timeout);if(t){let e=setInterval((()=>this.writeGetParameter()),1e3*(t-5));this.client.once("close",(()=>clearInterval(e)))}this.session=r.headers.session.split(";")[0]}return r}async play(){return await this.request("PLAY",{Range:"npt=0.000-"}),this.client}async teardown(){try{return await this.request("TEARDOWN")}finally{this.client.destroy()}}}class he{udpPorts={video:0,audio:0};constructor(e,t,i){this.client=e,this.sdp=t,this.udp=i,this.session=(0,j.randomBytes)(4).toString("hex"),t&&(t=t.trim())}async handleSetup(){let e=[];for(;;){let t=await T(this.client);if(t=t.trim(),t)e.push(t);else{if(!await this.headers(e))break;e=[]}}}async handlePlayback(){return this.handleSetup()}async handleTeardown(){return this.handleSetup()}async*handleRecord(){for(;;){const e=await O(this.client,4),t=e.readUInt16BE(2),i=await O(this.client,t),r=e.readUInt8(1);yield{type:r-r%2===this.videoChannel?"video":"audio",rtcp:r%2==1,header:e,packet:i}}}send(e,t){const i=Buffer.alloc(4);i.writeUInt8(36,0),i.writeUInt8(t,1),i.writeUInt16BE(e.length,2),this.client.write(i),this.client.write(Buffer.from(e))}sendUdp(e,t,i){this.udp.send(t,i?e+1:e,"127.0.0.1")}sendVideo(e,t){if(this.udp&&this.udpPorts.video)this.sendUdp(this.udpPorts.video,e,t);else{if(null==this.videoChannel)throw new Error("rtsp videoChannel not set up");this.send(e,t?this.videoChannel+1:this.videoChannel)}}sendAudio(e,t){if(this.udp&&this.udpPorts.audio)this.sendUdp(this.udpPorts.audio,e,t);else{if(null==this.audioChannel)throw new Error("rtsp audioChannel not set up");this.send(e,t?this.audioChannel+1:this.audioChannel)}}options(e,t){const i={Public:"DESCRIBE, OPTIONS, PAUSE, PLAY, SETUP, TEARDOWN, ANNOUNCE, RECORD"};this.respond(200,"OK",t,i)}describe(e,t){const i={};i["Content-Base"]=e,i["Content-Type"]="application/sdp",this.respond(200,"OK",t,i,Buffer.from(this.sdp))}setup(e,t){const i={},r=t.transport;i.Transport=t.transport,i.Session=this.session;let n=B(this.sdp,"audio"),s=B(this.sdp,"video");if(r.includes("UDP")){const t=r.match(/.*?client_port=([0-9]+)-([0-9]+)/),[i,o,a]=t;n&&e.includes(n.trackId)?this.udpPorts.audio=parseInt(o):s&&e.includes(s.trackId)?this.udpPorts.video=parseInt(o):this.console?.warn("unknown track id",e)}else if(r.includes("TCP")){const t=r.match(/.*?interleaved=([0-9]+)-([0-9]+)/);if(t){const i=parseInt(t[1]);parseInt(t[2]);n&&e.includes(n.trackId)?this.audioChannel=i:s&&e.includes(s.trackId)?this.videoChannel=i:this.console?.warn("unknown track id",e)}}this.respond(200,"OK",t,i)}play(e,t){const i={};let r=B(this.sdp,"audio"),n=B(this.sdp,"video"),s="";r&&(s=`url=${e}/trackID=${r.trackId};seq=0;rtptime=0`),r&&n&&(s+=","),n&&(s+=`url=${e}/trackID=${n.trackId};seq=0;rtptime=0`),i["RTP-Info"]=s,i.Range="npt=now-",i.Session=this.session,this.respond(200,"OK",t,i)}async announce(e,t){const i=parseInt(t["content-length"]),r=await O(this.client,i);this.sdp=r.toString();const n={};n.Session=this.session,this.respond(200,"OK",t,n)}async record(e,t){const i={};i.Session=this.session,this.respond(200,"OK",t,i)}async teardown(e,t){const i={};i.Session=this.session,this.respond(200,"OK",t,i)}async headers(e){this.console?.log("request headers",e.join("\n"));let[t,i]=e[0].split(" ",2);t=t.toLowerCase();const r=pe(e);if(this[t])return await this[t](i,r),"play"!==t&&"record"!==t&&"teardown"!==t;this.respond(400,"Bad Request",r,{})}respond(e,t,i,r,n){let s=`RTSP/1.0 ${e} ${t}\r\n`;i.cseq&&(r.CSeq=i.cseq),n&&(r["Content-Length"]=n.length.toString());for(const[e,t]of Object.entries(r))s+=`${e}: ${t}\r\n`;this.console?.log("response headers",s),s+="\r\n",this.client.write(s),n&&this.client.write(n)}}const me=require("stream"),{mediaManager:fe}=t();async function ge(e,t,i,r,n,s){let o=!0;const a=new me.EventEmitter;a.on("error",(t=>e.error("rebroadcast error",t)));const c=parseInt(i.match(/m=audio.* ([0-9]+)/)?.[1]),d=parseInt(i.match(/m=video.* ([0-9]+)/)?.[1]),u=()=>{o&&(a.emit("killed"),a.emit("error",new Error("killed"))),o=!1,t.destroy()};let p,l;t.on("close",u),t.on("error",u),(async()=>{const{resetActivityTimer:e}=M("rtsp",u,a,s?.timeout);for(;;){let i,r;if(n){if(i=await O(t,4),"RTSP"===i.toString()){const e=pe(await ue(t)),i=parseInt(e["content-length"]);i&&await O(t,i);continue}r=i.readUInt16BE(2)}else i=await O(t,2),r=i.readUInt16BE(0);const s=await O(t,r),o=127&s[1];if(!n){const e=Buffer.alloc(2);e[0]=36,o===c?e[1]=0:o===d&&(e[1]=2),i=Buffer.concat([e,i])}let u;o===c?u="rtp-audio":o===d&&(u="rtp-video");const p={chunks:[i,s],type:u};a.emit("rtsp",p),e()}})().finally(u);const h=B(i,"audio"),m=B(i,"video");if(h){const e=h.section.toLowerCase();e.includes("mpeg4")?p="aac":e.includes("pcm")&&(p="pcm")}return m&&m.section.toLowerCase().includes("h264")&&(l="h264"),{sdp:Promise.resolve([Buffer.from(i)]),inputAudioCodec:p,inputVideoCodec:l,inputVideoResolution:void 0,isActive:()=>o,kill:u,mediaStreamOptions:r,emit(e,t){return a.emit(e,t),this},on(e,t){return a.on(e,t),this},once(e,t){return a.once(e,t),this},removeListener(e,t){return a.removeListener(e,t),this}}}const{mediaManager:ye,log:ve,systemManager:Se,deviceManager:be}=t(),we=1e4,Pe="prebufferDuration",Ie="sendKeyframe",Me="Default",Ce="AAC or No Audio",xe=`${Ce} (Copy)`,Oe="Compatible Audio",De="Other Audio",Te=["aac","mp3","mp2","opus"],Ae="-fflags +genpts",ke="Scrypted",Ee="FFmpeg",Re="Default",_e=[Ce,Oe,De],Le=["mpegts","mp4","rtsp"];class Ue{prebuffers={mp4:[],mpegts:[],rtsp:[]};detectedIdrInterval=0;prevIdr=0;audioDisabled=!1;activeClients=0;constructor(e,t,i){this.mixin=e,this.advertisedMediaStreamOptions=t,this.stopInactive=i,this.storage=e.storage,this.console=e.console,this.mixinDevice=e.mixinDevice,this.audioConfigurationKey="audioConfiguration-"+this.streamId,this.ffmpegInputArgumentsKey="ffmpegInputArguments-"+this.streamId,this.rebroadcastModeKey="rebroadcastMode-"+this.streamId,this.lastDetectedAudioCodecKey="lastDetectedAudioCodec-"+this.streamId,this.rtspParserKey="rtspParser-"+this.streamId}get streamId(){return this.advertisedMediaStreamOptions.id}get streamName(){return this.advertisedMediaStreamOptions.name}clearPrebuffers(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtsp=[]}ensurePrebufferSession(){this.parserSessionPromise||this.mixin.released||(this.console.log(this.streamName,"prebuffer session started"),this.parserSessionPromise=this.startPrebufferSession(),this.parserSessionPromise.catch((()=>this.parserSessionPromise=void 0)))}getAudioConfig(){let e=this.storage.getItem(this.audioConfigurationKey)||"";_e.find((t=>e.startsWith(t)))||(e="");const t=-1!==e.indexOf(Ce),i=-1!==e.indexOf(Oe),r=-1!==e.indexOf(De);return{isUsingDefaultAudioConfig:!(t||i||r),aacAudio:t,compatibleAudio:i,reencodeAudio:r}}canUseRtspParser(e,t){if(e)return!1;if("rtsp"!==t?.container)return!1;const{isUsingDefaultAudioConfig:i,compatibleAudio:r,aacAudio:n}=this.getAudioConfig();return i||r||n}getParser(e,t,i){if(!this.canUseRtspParser(t,i))return Ee;const r=e&&"scrypted"===i?.tool?ke:Ee,n=this.storage.getItem(this.rtspParserKey);return n&&n!==Re?n===ke?ke:n===Ee?Ee:r:r}getRebroadcastMode(){const e=this.storage.getItem(this.rebroadcastModeKey),t=e?.startsWith("RTSP");return{rtspMode:e?.startsWith("RTSP"),muxingMp4:!t||e?.includes("MP4")}}async getMixinSettings(){const e=[],t=this.parserSession;let i=0,r=0;const{muxingMp4:n,rtspMode:s}=this.getRebroadcastMode();for(const e of n?this.prebuffers.mp4:this.prebuffers.rtsp){r=r||e.time;for(const t of e.chunk.chunks)i+=t.byteLength}const o=Date.now()-r,a=Math.round(i/o*8),c=this.streamName?`Rebroadcast: ${this.streamName}`:"Rebroadcast";e.push({title:"Audio Codec Transcoding",group:c,description:"Configuring your camera to output AAC, MP3, MP2, or Opus is recommended. PCM/G711 cameras should set this to Transcode.",type:"string",key:this.audioConfigurationKey,value:this.storage.getItem(this.audioConfigurationKey)||Me,choices:[Me,xe,"Compatible Audio (Copy)","Other Audio (Transcode)"]},{title:"Rebroadcast Container",group:c,description:'Experimental: The container format to use when rebroadcasting. MPEG-TS is stable. RTSP is lower latency. The default is "MPEG-TS" for this camera.',placeholder:"MPEG-TS",choices:[Re,"MPEG-TS","RTSP"],key:this.rebroadcastModeKey,value:this.storage.getItem(this.rebroadcastModeKey)||Re});const d=()=>{e.push({title:"FFmpeg Input Arguments Prefix",group:c,description:"Optional/Advanced: Additional input arguments to pass to the ffmpeg command. These will be placed before the input arguments.",key:this.ffmpegInputArgumentsKey,value:this.storage.getItem(this.ffmpegInputArgumentsKey),placeholder:Ae,choices:[Ae,"-use_wallclock_as_timestamps 1","-v verbose"],combobox:!0})};if(this.canUseRtspParser(n,this.advertisedMediaStreamOptions)&&s&&"rtsp"===this.advertisedMediaStreamOptions?.container){const t=this.getParser(s,n,this.advertisedMediaStreamOptions);e.push({key:this.rtspParserKey,group:c,title:"RTSP Parser",description:`Experimental: The RTSP Parser used to read the stream. FFmpeg is stable. The Scrypted parser is lower latency. The Scrypted Parser is only available when the Audo Codec is not Transcoding and the Rebroadcast Container is RTSP. The default is "${t}" for this camera.`,value:this.storage.getItem(this.rtspParserKey)||Re,choices:[Re,Ee,ke]}),t!==ke&&d()}else d();return t?e.push({key:"detectedResolution",group:c,title:"Detected Resolution and Bitrate",readonly:!0,value:`${t?.inputVideoResolution?.[0]||"unknown"} @ ${a||"unknown"} Kb/s`,description:"Configuring your camera to 1920x1080, 2000Kb/S, Variable Bit Rate, is recommended."},{key:"detectedCodec",group:c,title:"Detected Video/Audio Codecs",readonly:!0,value:(t?.inputVideoCodec?.toString()||"unknown")+"/"+(t?.inputAudioCodec?.toString()||"unknown"),description:"Configuring your camera to H264 video and AAC/MP3/MP2/Opus audio is recommended."},{key:"detectedKeyframe",group:c,title:"Detected Keyframe Interval",description:"Configuring your camera to 4 seconds is recommended (IDR aka Frame Interval = FPS * 4 seconds).",readonly:!0,value:(this.detectedIdrInterval||0)/1e3||"unknown"}):e.push({title:"Status",group:c,key:"status",description:"Rebroadcast is currently idle and will be started automatically on demand.",value:"Idle",readonly:!0}),e}async startPrebufferSession(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtsp=[];const t=parseInt(this.storage.getItem(Pe))||we;let i;try{i=(await this.mixinDevice.getVideoStreamOptions()).find((e=>e.id===this.streamId))}catch(e){}const r=null===i?.audio,s=i?.audio?.codec,{isUsingDefaultAudioConfig:o,aacAudio:a,compatibleAudio:d,reencodeAudio:p}=this.getAudioConfig(),{rtspMode:l,muxingMp4:h}=this.getRebroadcastMode();let m=this.storage.getItem(this.lastDetectedAudioCodecKey)||void 0;"null"===m&&(m=null);let f=!1;h&&!r&&!s&&o&&void 0===m&&(this.console.warn("Camera did not report an audio codec, muting the audio stream and probing the codec."),f=!0),!r&&s&&void 0!==m&&m!==s&&this.console.warn("Audio codec plugin reported vs detected mismatch",s,m);const g=void 0===m?s?.toLowerCase():m?.toLowerCase(),y=!Te.includes(g);!h||f||!1===i?.userConfigurable||r||y&&(o&&ve.a(`${this.mixin.name} is using the ${g} audio codec. Configuring your Camera to use AAC, MP3, MP2, or Opus audio is recommended. If this is not possible, Select 'Transcode Audio' in the camera stream's Rebroadcast settings to suppress this alert.`),this.console.warn("Configure your camera to output AAC, MP3, MP2, or Opus audio. Suboptimal audio codec in use:",g));const v=["-bsf:a","aac_adtstoasc"],S=[];let P;this.audioDisabled=!1;const I=null===m;let x=!1;if(!f&&o&&y&&(!1===i?.userConfigurable?this.console.log("camera reports it is not user configurable. transcoding due to incompatible codec",g):this.console.log("camera audio transcoding due to incompatible codec. configure the camera to use a compatible codec if possible."),x=!0),r||f)P=["-an"],this.audioDisabled=!0;else if(p||x)P=["-bsf:a","aac_adtstoasc","-acodec","aac","-ar","32k","-b:a","32k","-ac","1","-profile:a","aac_low","-flags","+global_header"];else if(a||I)P=["-acodec","copy"],P.push(...v);else if(d)P=["-acodec","copy"],P.push(...S);else{P=["-acodec","copy"];const e="aac"===g?v:S;P.push(...e)}const O=["-vcodec","copy"],D={console:this.console,timeout:6e4,parsers:{}};if(this.parsers=D.parsers,this.console.log("rebroadcast mode:",l?"rtsp":"mpegts"),l){const e=function(e){let t;return{container:"rtsp",tcpProtocol:"rtsp://127.0.0.1/"+(0,j.randomBytes)(8).toString("hex"),inputArguments:["-rtsp_transport","tcp"],outputArguments:["-rtsp_transport","tcp",...e?.vcodec||[],...e?.acodec||[],"-f","rtsp"],findSyncFrame(e){for(let t=0;t<e.length;t++){const i=e[t];if("rtp-video"===i.type){const r=31&i.chunks[1].readUInt8(12),n=i.chunks[1].readUInt8(13),s=31&n,o=128&n;if((28===r||29===r)&&5===s&&128==o||5==r)return e.slice(t)}}return e},sdp:new Promise((e=>t=e)),async*parse(e,i,r){const n=new he(e);await n.handleSetup(),t(n.sdp);for await(const{type:e,rtcp:t,header:s,packet:o}of n.handleRecord())yield{chunks:[s,o],type:`${t?"rtcp":"rtp"}-${e}`,width:i,height:r}}}}({vcodec:O,acodec:P});this.sdp=e.sdp,D.parsers.rtsp=e}else D.parsers.mpegts={container:"mpegts",outputArguments:[...(T={vcodec:O,acodec:P})?.vcodec||[],...T?.acodec||[],"-f","mpegts"],parse:(R=188,_=e=>{if(71!=e[0])throw new Error("Invalid sync byte in mpeg-ts packet. Terminating stream.")},async function*(e){let t=[],i=0;for(;;){const r=e.read();if(!r){await(0,n.once)(e,"readable");continue}if(t.push(r),i+=r.length,i<R)continue;const s=Buffer.concat(t);_?.(s);const o=s.length%R,a=s.slice(0,s.length-o),c=s.slice(s.length-o);t=[c],i=c.length,yield{chunks:[a]}}}),findSyncFrame(e){for(let t=0;t<e.length;t++){const i=e[t];for(let r=0;r<i.chunks.length;r++){const n=i.chunks[r];let s=0;for(;s+188<n.length;){const i=n.subarray(s,s+188);if(256==((31&i[1])<<8|i[2])&&32&i[3]&&i[4]>0&&64&i[5])return e.slice(t);s+=188}}}return e}};var T,R,_;h&&(D.parsers.mp4=function(e){return{container:"mp4",outputArguments:[...e?.vcodec||[],...e?.acodec||[],"-movflags","frag_keyframe+empty_moov+default_base_moof","-f","mp4"],async*parse(e){const t=k(e);yield*E(t)},findSyncFrame:A}}({vcodec:O,acodec:P}));const L=await this.mixinDevice.getVideoStream(i),H="x-scrypted/x-rfc4571"===L.mimeType;let $,F;this.storage.removeItem(this.lastDetectedAudioCodecKey);let N=!1;if(l&&H){N=!0,this.console.log("bypassing ffmpeg: using scrypted rfc4571 parser");const e=await ye.convertMediaObjectToJSON(L,"x-scrypted/x-rfc4571"),{url:t,sdp:i,mediaStreamOptions:r}=e;$=await ge(this.console,function(e){const t=new URL(e);if(!t.protocol.startsWith("tcp"))throw new Error("rfc4751 url must be tcp");return c().connect(parseInt(t.port),t.hostname)}(t),i,r,!1,D),this.sdp=$.sdp.then((e=>Buffer.concat(e).toString()))}else{const t=await ye.convertMediaObjectToBuffer(L,e.ScryptedMimeTypes.FFmpegInput),i=JSON.parse(t.toString());F=i.mediaStreamOptions;if(this.getParser(l,h,i.mediaStreamOptions)===ke){N=!0,this.console.log("bypassing ffmpeg: using scrypted rtsp/rfc4571 parser");const e=new le(i.url,this.console);await e.options();const t=(await e.describe()).body.toString().trim();this.console.log("sdp",t),this.sdp=Promise.resolve(t);const{audio:n,video:s}=function(e,t=["recvonly","sendrecv"]){return{audio:B(e,"audio",t)?.trackId,video:B(e,"video",t)?.trackId}}(t);let o=0;r||(await e.setup(o,n),o+=2),await e.setup(o,s);const a=await e.play();$=await ge(this.console,a,t,i.mediaStreamOptions,!0,D);const c=$.kill.bind($);$.kill=async()=>{e.teardown().finally(c),await async function(e){await new Promise((t=>setTimeout(t,e)))}(500),c()}}else{const e=this.storage.getItem(this.ffmpegInputArgumentsKey)||Ae;i.inputArguments.unshift(...e.split(" ")),$=await C(i,D)}}if(N&&h&&this.getVideoStream({id:this.streamId,refresh:!1}).then((async t=>{const i=this.storage.getItem(this.ffmpegInputArgumentsKey)||Ae,r=await ye.convertMediaObjectToJSON(t,e.ScryptedMimeTypes.FFmpegInput);r.inputArguments.unshift(...i.split(" "));const n=await async function(e,t,i,r){const n=e.slice();n.push(...i,...t,"-movflags","frag_keyframe+empty_moov+default_base_moof","-f","mp4","pipe:3"),n.unshift("-hide_banner"),w(r,n);const s=u().spawn(await U.getFFmpegPath(),n,{stdio:["pipe","pipe","pipe","pipe"]});return b(r,s),{cp:s,generator:k(s.stdio[3])}}(r.inputArguments,P,O,this.console),s=()=>{n.cp.kill("SIGKILL"),$.kill(),n.generator.throw(new Error("killed"))};if(!$.isActive)return void s();$.once("killed",s);const{resetActivityTimer:o}=M("mp4",s,$,D.timeout);for await(const e of E(n.generator))o(),$.emit("mp4",e)})).catch((()=>{})),$.inputAudioCodec?Te.includes($.inputAudioCodec?.toLowerCase())?this.console.log("Detected audio codec is mp4/mpegts compatible.",$.inputAudioCodec):this.console.log("Detected audio codec is not mp4/mpegts compatible.",$.inputAudioCodec):this.console.log("No audio stream detected."),this.storage.setItem(this.lastDetectedAudioCodecKey,$.inputAudioCodec||"null"),"h264"!==$.inputVideoCodec&&this.console.error("Video codec is not h264. If there are errors, try changing your camera's encoder output."),f)return this.console.warn("Audio probe complete, ending rebroadcast session and restarting with detected codecs."),$.kill(),this.startPrebufferSession();if(this.parserSession=$,be.onMixinEvent(this.mixin.id,this.mixin.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0),F?.refreshAt){let t,i=F;const r=async()=>{if(!$.isActive)return;const t=await this.mixinDevice.getVideoStream(i),r=await ye.convertMediaObjectToBuffer(t,e.ScryptedMimeTypes.FFmpegInput),s=JSON.parse(r.toString());i=s.mediaStreamOptions,n(i)},n=e=>{const i=e.refreshAt-Date.now()-3e4;this.console.log("refreshing media stream in",i),t=setTimeout(r,i)};n(i),$.once("killed",(()=>clearTimeout(t)))}$.once("killed",(()=>{clearTimeout(this.inactivityTimeout),this.parserSessionPromise=void 0,this.parserSession===$&&(this.parserSession=void 0)}));for(const i of Le){let r=0;$.on(i,(n=>{const s=this.prebuffers[i],o=Date.now(),a=()=>{if(this.prevIdr){const t=!this.detectedIdrInterval;this.detectedIdrInterval=o-this.prevIdr,t&&be.onMixinEvent(this.mixin.id,this.mixin.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0)}this.prevIdr=o};if("mdat"===n.type&&a(),"rtp-video"===n.type){const e=31&n.chunks[1].readUInt8(12),t=n.chunks[1].readUInt8(13),i=31&t,r=128&t;(28!==e&&29!==e||5!==i||128!=r)&&5!=e||a()}for(s.push({time:o,chunk:n});s.length&&s[0].time<o-t;)s.shift(),r++;r>1e3&&(this.prebuffers[i]=s.slice(),r=0)}))}return $}printActiveClients(){this.console.log(this.streamName,"active rebroadcast clients:",this.activeClients)}inactivityCheck(e){this.printActiveClients(),this.activeClients||(this.stopInactive?(clearTimeout(this.inactivityTimeout),this.inactivityTimeout=setTimeout((()=>{this.inactivityTimeout=void 0,this.activeClients?this.console.log("inactivity timeout found active clients."):(this.console.log(this.streamName,"terminating rebroadcast due to inactivity"),e.kill())}),3e4)):0===this.activeClients&&this.console.log("stopInactive false"))}async getVideoStream(e){this.ensurePrebufferSession();const t=await this.parserSessionPromise,i="false"!==this.storage.getItem(Ie);let r=e?.prebuffer;null==r&&(r=i?1.5*Math.max(4e3,this.detectedIdrInterval||4e3):0);const{rtspMode:n}=this.getRebroadcastMode(),s=n?"rtsp":"mpegts";let o=this.parsers[e?.container]?e?.container:s;e?.prebuffer&&"mp4"!==o&&"mp4"===e?.container&&(r+=1.5*(this.detectedIdrInterval||4e3));const a=Object.assign({},t.mediaStreamOptions);a.prebuffer=r;const{reencodeAudio:c}=this.getAudioConfig();let d=!1;this.audioDisabled?a.audio=null:c?a.audio={codec:"aac",encoder:"aac",profile:"aac_low"}:d=!0,d&&(a.audio=t?.mediaStreamOptions?.audio?.codec===t?.inputAudioCodec?t?.mediaStreamOptions?.audio:{codec:t?.inputAudioCodec}),a.video&&t.inputVideoResolution?.[2]&&t.inputVideoResolution?.[3]&&Object.assign(a.video,{width:parseInt(t.inputVideoResolution[2]),height:parseInt(t.inputVideoResolution[3])});const u=Date.now();let p=0;const l=this.prebuffers[o];for(const e of l)if(!(e.time<u-r))for(const t of e.chunk.chunks)p+=t.length;const h=Math.max(5e5,p).toString(),m=await(async i=>{const n=this.prebuffers[i];let s,o;if("rtsp"===i){const e=await g();s=e.clientPromise.then((async e=>{let t=await this.sdp;t=function(e){let t=e.split("\n").map((e=>e.trim()));t=t.filter((e=>!e.includes("a=control:")));const i=t.findIndex((e=>e.startsWith("m=video")));-1!==i&&t.splice(i+1,0,"a=control:trackID=video");const r=t.findIndex((e=>e.startsWith("m=audio")));return-1!==r&&t.splice(r+1,0,"a=control:trackID=audio"),t.join("\r\n")}(t);const i=new he(e,t);return await i.handlePlayback(),e})),o=e.url.replace("tcp://","rtsp://")}else{const e=await g();s=e.clientPromise,o=`tcp://127.0.0.1:${e.port}`}const a=!1!==e?.refresh;return x(s,{connect:(e,s)=>{a&&(this.activeClients++,this.printActiveClients());const o=Date.now(),c=t=>{e(t)>1e8&&(this.console.log("more than 100MB has been buffered, did downstream die? killing connection.",this.streamName),d())},d=()=>{s(),t.removeListener(i,c),t.removeListener("killed",d)};t.on(i,c),t.once("killed",d);for(const e of n)e.time<o-r||c(e.chunk);return()=>{a&&(this.activeClients--,this.inactivityCheck(t)),d()}}}),o})(o),f={url:m,container:o,inputArguments:["-analyzeduration","0","-probesize",h,...this.parsers[o].inputArguments||[],"-f",this.parsers[o].container,"-i",m],mediaStreamOptions:a};return ye.createFFmpegMediaObject(f)}}class Be extends o{released=!1;sessions=new Map;constructor(e){super(e),this.delayStart()}delayStart(){this.console.log("prebuffer sessions starting in 5 seconds"),setTimeout((()=>this.ensurePrebufferSessions()),5e3)}async getVideoStream(e){await this.ensurePrebufferSessions();const t=e?.id;let i=this.sessions.get(t);return!i||e?.directMediaStream?this.mixinDevice.getVideoStream(e):(i.ensurePrebufferSession(),await i.parserSessionPromise,i=this.sessions.get(t),i?i.getVideoStream(e):this.mixinDevice.getVideoStream(e))}async ensurePrebufferSessions(){const t=await this.mixinDevice.getVideoStreamOptions(),i=this.getEnabledMediaStreamOptions(t),r=i?i.map((e=>e.id)):[void 0],s=t?.map((e=>e.id))||[void 0];if("true"!==this.storage.getItem("warnedCloud")){const e=t?.find((e=>"cloud"===e.source));e&&(this.storage.setItem("warnedCloud","true"),ve.a(`${this.name} is a cloud camera. Prebuffering maintains a persistent stream and will not enabled by default. You must enable the Prebuffer stream manually.`))}const o=this.mixinDeviceInterfaces.includes(e.ScryptedInterface.Battery);let a=0;const c=r.length;for(const e of s){let i=this.sessions.get(e);if(!i){const s=t?.find((t=>t.id===e));s?.prebuffer&&ve.a(`Prebuffer is already available on ${this.name}. If this is a grouped device, disable the Rebroadcast extension.`);const d=s?.name,u=!r.includes(e);if(i=new Ue(this,s,o||u),this.sessions.set(e,i),e===t?.[0]?.id&&this.sessions.set(void 0,i),o){this.console.log("camera is battery powered, prebuffering and rebroadcasting will only work on demand.");continue}if(u){this.console.log("stream",d,"will be rebroadcast on demand.");continue}(async()=>{for(;this.sessions.get(e)===i&&!this.released;){i.ensurePrebufferSession();let e=!1;try{const t=await i.parserSessionPromise;a++,e=!0,this.online=a==c,await(0,n.once)(t,"killed"),this.console.error("prebuffer session ended")}catch(e){this.console.error("prebuffer session ended with error",e)}finally{e&&a--,e=!1,this.online=a==c}this.console.log("restarting prebuffer session in 5 seconds"),await new Promise((e=>setTimeout(e,5e3)))}this.console.log("exiting prebuffer session (released or restarted with new configuration)")})()}}be.onMixinEvent(this.id,this.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0)}async getMixinSettings(){const e=[];try{const t=await this.mixinDevice.getVideoStreamOptions(),i=this.getEnabledMediaStreamOptions(t);t?.length>0&&e.push({title:"Prebuffered Streams",description:"The streams to prebuffer. Enable only as necessary to reduce traffic.",key:"enabledStreams",value:i.map((e=>e.name||"")),choices:t.map((e=>e.name)),multiple:!0})}catch(e){throw this.console.error("error in getVideoStreamOptions",e),e}e.push({title:"Prebuffer Duration",description:"Duration of the prebuffer in milliseconds.",type:"number",key:Pe,value:this.storage.getItem(Pe)||we.toString()},{title:"Start at Previous Keyframe",description:"Start live streams from the previous key frame. Improves startup time.",type:"boolean",key:Ie,value:("false"!==this.storage.getItem(Ie)).toString()});for(const t of new Set([...this.sessions.values()]))if(t)try{e.push(...await t.getMixinSettings())}catch(e){throw this.console.error("error in prebuffer session getMixinSettings",e),e}return e}async putMixinSetting(e,t){const i=this.sessions;this.sessions=new Map,"enabledStreams"===e?this.storage.setItem(e,JSON.stringify(t)):this.storage.setItem(e,t.toString());for(const e of i.values())e?.parserSessionPromise?.then((e=>e.kill()));this.ensurePrebufferSessions()}getEnabledMediaStreamOptions(e){if(!e)return;try{const t=JSON.parse(this.storage.getItem("enabledStreams"));return e.filter((e=>t.includes(e.name)))}catch(e){}const t=e.find((e=>"cloud"!==e.source));return t?[t]:[]}async getVideoStreamOptions(){const e=await this.mixinDevice.getVideoStreamOptions()||[];let t=this.getEnabledMediaStreamOptions(e);const i=parseInt(this.storage.getItem(Pe))||we;for(const r of e)(this.sessions.get(r.id)?.parserSession||t.includes(r))&&(r.prebuffer=i);return e}async release(){this.console.log("prebuffer session releasing if started"),this.released=!0;for(const e of this.sessions.values())e&&(e.clearPrebuffers(),e.parserSessionPromise?.then((t=>{this.console.log("prebuffer session released"),t.kill(),e.clearPrebuffers()})))}}const je=new class extends L{constructor(t){super(t),this.fromMimeType="x-scrypted/x-rfc4571",this.toMimeType=e.ScryptedMimeTypes.FFmpegInput,process.nextTick((()=>{for(const e of Object.keys(Se.getSystemState())){const t=Se.getDeviceById(e);if(t.mixins?.includes(this.id))try{t.getVideoStreamOptions()}catch(e){this.console.error("error triggering prebuffer",t.name,e)}}}));const i=function(){var e=new Date;return e.setHours(24),e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0),e.getTime()-(new Date).getTime()}()+72e5;this.log.i(`Rebroadcaster scheduled for restart at 2AM: ${Math.round(i/1e3/60)} minutes`),setTimeout((()=>be.requestRestart()),i)}async convert(e,t,i){const r=JSON.parse(e.toString()),{url:n,sdp:s}=r,{audioPayloadTypes:o,videoPayloadTypes:a}=function(e){const t=new Set,i=new Set,r=(e,t)=>{for(const i of t||[])e.add(parseInt(i))},n=e.match(/m=audio.*/)?.[0];r(t,n?.split(" ").slice(3));const s=e.match(/m=video.*/)?.[0];return r(i,s?.split(" ").slice(3)),{audioPayloadTypes:t,videoPayloadTypes:i}}(s),d=new URL(n);if(!d.protocol.startsWith("tcp"))throw new Error("rfc4751 url must be tcp");const{clientPromise:u,url:p}=await g(),l={url:p,inputArguments:["-rtsp_transport","tcp","-i",p.replace("tcp","rtsp")]};return u.then((async e=>{const t=new he(e,s);await t.handlePlayback();const i=c().connect(parseInt(d.port),d.hostname);for(e.on("close",(()=>{i.destroy()})),i.on("close",(()=>{e.destroy()}));;){const r=(await O(i,2)).readInt16BE(0),n=await O(i,r),s=127&n[1];if(o.has(s))t.sendAudio(n,!1);else{if(!a.has(s))throw e.destroy(),i.destroy(),new Error("unknown payload type "+s);t.sendVideo(n,!1)}}})),Buffer.from(JSON.stringify(l))}async canMixin(t,i){return i.includes(e.ScryptedInterface.VideoCamera)?[e.ScryptedInterface.VideoCamera,e.ScryptedInterface.Settings,e.ScryptedInterface.Online]:null}async getMixin(e,t,i){return this.setHasEnabledMixin(i.id),new Be({mixinDevice:e,mixinDeviceState:i,mixinProviderNativeId:this.nativeId,mixinDeviceInterfaces:t,group:"Prebuffer Settings",groupKey:"prebuffer"})}async releaseMixin(e,t){t.online=!0,t.release()}}})();var n=exports="undefined"==typeof exports?{}:exports;for(var s in r)n[s]=r[s];r.__esModule&&Object.defineProperty(n,"__esModule",{value:!0})})();
|
|
1
|
+
(()=>{var e={510:function(e,t,i){"use strict";var r=this&&this.__createBinding||(Object.create?function(e,t,i,r){void 0===r&&(r=i),Object.defineProperty(e,r,{enumerable:!0,get:function(){return t[i]}})}:function(e,t,i,r){void 0===r&&(r=i),e[r]=t[i]}),s=this&&this.__exportStar||function(e,t){for(var i in e)"default"===i||Object.prototype.hasOwnProperty.call(t,i)||r(t,e,i)};Object.defineProperty(t,"__esModule",{value:!0}),t.MixinDeviceBase=t.ScryptedDeviceBase=void 0,s(i(268),t);const n=i(268);class o extends n.DeviceBase{constructor(e){super(),this.nativeId=e}get storage(){return this._storage||(this._storage=deviceManager.getDeviceStorage(this.nativeId)),this._storage}get log(){return this._log||(this._log=deviceManager.getDeviceLogger(this.nativeId)),this._log}get console(){return this._console||(this._console=deviceManager.getDeviceConsole(this.nativeId)),this._console}_lazyLoadDeviceState(){this._deviceState||(this.nativeId?this._deviceState=deviceManager.getDeviceState(this.nativeId):this._deviceState=deviceManager.getDeviceState())}onDeviceEvent(e,t){return deviceManager.onDeviceEvent(this.nativeId,e,t)}}t.ScryptedDeviceBase=o;class a extends n.DeviceBase{constructor(e){super(),this._listeners=new Set,this.mixinDevice=e.mixinDevice,this.mixinDeviceInterfaces=e.mixinDeviceInterfaces,this.mixinStorageSuffix=e.mixinStorageSuffix,this._deviceState=e.mixinDeviceState,this.mixinProviderNativeId=e.mixinProviderNativeId}get storage(){if(!this._storage){const e=this.mixinStorageSuffix,t=this.id+(e?":"+e:"");this._storage=deviceManager.getMixinStorage(t,this.mixinProviderNativeId)}return this._storage}get console(){return this._console||(deviceManager.getMixinConsole?this._console=deviceManager.getMixinConsole(this.id,this.mixinProviderNativeId):this._console=deviceManager.getDeviceConsole(this.mixinProviderNativeId)),this._console}onDeviceEvent(e,t){return deviceManager.onMixinEvent(this.id,this,e,t)}_lazyLoadDeviceState(){}manageListener(e){this._listeners.add(e)}release(){for(const e of this._listeners)e.removeListener()}}t.MixinDeviceBase=a,function(){function e(e){return function(){return this._lazyLoadDeviceState(),this._deviceState[e]}}function t(e){return function(t){this._lazyLoadDeviceState(),this._deviceState[e]=t}}for(var i of Object.values(n.ScryptedInterfaceProperty))Object.defineProperty(o.prototype,i,{set:t(i),get:e(i)}),Object.defineProperty(a.prototype,i,{set:t(i),get:e(i)})}();let c={};try{c=Object.assign(c,{log:deviceManager.getDeviceLogger(void 0),deviceManager,endpointManager,mediaManager,systemManager,pluginHostAPI})}catch(e){console.error("sdk initialization error, import @scrypted/types or use @scrypted/web-sdk instead",e)}t.default=c},268:(e,t,i)=>{"use strict";i.r(t),i.d(t,{DeviceBase:()=>r,ScryptedInterfaceProperty:()=>s,ScryptedInterfaceDescriptors:()=>n,ScryptedDeviceType:()=>o,HumidityMode:()=>a,FanMode:()=>c,TemperatureUnit:()=>d,ThermostatMode:()=>u,LockState:()=>p,MediaPlayerState:()=>l,ScryptedInterface:()=>h,ScryptedMimeTypes:()=>m});class r{}let s;!function(e){e.id="id",e.info="info",e.interfaces="interfaces",e.mixins="mixins",e.name="name",e.providedInterfaces="providedInterfaces",e.providedName="providedName",e.providedRoom="providedRoom",e.providedType="providedType",e.providerId="providerId",e.room="room",e.type="type",e.on="on",e.brightness="brightness",e.colorTemperature="colorTemperature",e.rgb="rgb",e.hsv="hsv",e.running="running",e.paused="paused",e.docked="docked",e.thermostatActiveMode="thermostatActiveMode",e.thermostatAvailableModes="thermostatAvailableModes",e.thermostatMode="thermostatMode",e.thermostatSetpoint="thermostatSetpoint",e.thermostatSetpointHigh="thermostatSetpointHigh",e.thermostatSetpointLow="thermostatSetpointLow",e.temperature="temperature",e.temperatureUnit="temperatureUnit",e.humidity="humidity",e.lockState="lockState",e.entryOpen="entryOpen",e.batteryLevel="batteryLevel",e.online="online",e.updateAvailable="updateAvailable",e.fromMimeType="fromMimeType",e.toMimeType="toMimeType",e.binaryState="binaryState",e.intrusionDetected="intrusionDetected",e.powerDetected="powerDetected",e.audioDetected="audioDetected",e.motionDetected="motionDetected",e.ambientLight="ambientLight",e.occupied="occupied",e.flooded="flooded",e.ultraviolet="ultraviolet",e.luminance="luminance",e.position="position",e.humiditySetting="humiditySetting",e.fan="fan"}(s||(s={}));const n={ScryptedDevice:{name:"ScryptedDevice",methods:["listen","probe","setName","setRoom","setType"],properties:["id","info","interfaces","mixins","name","providedInterfaces","providedName","providedRoom","providedType","providerId","room","type"]},ScryptedPlugin:{name:"ScryptedPlugin",methods:["getPluginJson"],properties:[]},OnOff:{name:"OnOff",methods:["turnOff","turnOn"],properties:["on"]},Brightness:{name:"Brightness",methods:["setBrightness"],properties:["brightness"]},ColorSettingTemperature:{name:"ColorSettingTemperature",methods:["getTemperatureMaxK","getTemperatureMinK","setColorTemperature"],properties:["colorTemperature"]},ColorSettingRgb:{name:"ColorSettingRgb",methods:["setRgb"],properties:["rgb"]},ColorSettingHsv:{name:"ColorSettingHsv",methods:["setHsv"],properties:["hsv"]},Notifier:{name:"Notifier",methods:["sendNotification"],properties:[]},StartStop:{name:"StartStop",methods:["start","stop"],properties:["running"]},Pause:{name:"Pause",methods:["pause","resume"],properties:["paused"]},Dock:{name:"Dock",methods:["dock"],properties:["docked"]},TemperatureSetting:{name:"TemperatureSetting",methods:["setThermostatMode","setThermostatSetpoint","setThermostatSetpointHigh","setThermostatSetpointLow"],properties:["thermostatActiveMode","thermostatAvailableModes","thermostatMode","thermostatSetpoint","thermostatSetpointHigh","thermostatSetpointLow"]},Thermometer:{name:"Thermometer",methods:["setTemperatureUnit"],properties:["temperature","temperatureUnit"]},HumiditySensor:{name:"HumiditySensor",methods:[],properties:["humidity"]},Camera:{name:"Camera",methods:["getPictureOptions","takePicture"],properties:[]},VideoCamera:{name:"VideoCamera",methods:["getVideoStream","getVideoStreamOptions"],properties:[]},VideoCameraConfiguration:{name:"VideoCameraConfiguration",methods:["setVideoStreamOptions"],properties:[]},Intercom:{name:"Intercom",methods:["startIntercom","stopIntercom"],properties:[]},Lock:{name:"Lock",methods:["lock","unlock"],properties:["lockState"]},PasswordStore:{name:"PasswordStore",methods:["addPassword","getPasswords","removePassword"],properties:[]},Authenticator:{name:"Authenticator",methods:["checkPassword"],properties:[]},Scene:{name:"Scene",methods:["activate","deactivate","isReversible"],properties:[]},Entry:{name:"Entry",methods:["closeEntry","openEntry"],properties:[]},EntrySensor:{name:"EntrySensor",methods:[],properties:["entryOpen"]},DeviceProvider:{name:"DeviceProvider",methods:["getDevice"],properties:[]},DeviceDiscovery:{name:"DeviceDiscovery",methods:["discoverDevices"],properties:[]},DeviceCreator:{name:"DeviceCreator",methods:["createDevice","getCreateDeviceSettings"],properties:[]},Battery:{name:"Battery",methods:[],properties:["batteryLevel"]},Refresh:{name:"Refresh",methods:["getRefreshFrequency","refresh"],properties:[]},MediaPlayer:{name:"MediaPlayer",methods:["getMediaStatus","load","seek","skipNext","skipPrevious"],properties:[]},Online:{name:"Online",methods:[],properties:["online"]},SoftwareUpdate:{name:"SoftwareUpdate",methods:["checkForUpdate","installUpdate"],properties:["updateAvailable"]},BufferConverter:{name:"BufferConverter",methods:["convert"],properties:["fromMimeType","toMimeType"]},Settings:{name:"Settings",methods:["getSettings","putSetting"],properties:[]},BinarySensor:{name:"BinarySensor",methods:[],properties:["binaryState"]},IntrusionSensor:{name:"IntrusionSensor",methods:[],properties:["intrusionDetected"]},PowerSensor:{name:"PowerSensor",methods:[],properties:["powerDetected"]},AudioSensor:{name:"AudioSensor",methods:[],properties:["audioDetected"]},MotionSensor:{name:"MotionSensor",methods:[],properties:["motionDetected"]},AmbientLightSensor:{name:"AmbientLightSensor",methods:[],properties:["ambientLight"]},OccupancySensor:{name:"OccupancySensor",methods:[],properties:["occupied"]},FloodSensor:{name:"FloodSensor",methods:[],properties:["flooded"]},UltravioletSensor:{name:"UltravioletSensor",methods:[],properties:["ultraviolet"]},LuminanceSensor:{name:"LuminanceSensor",methods:[],properties:["luminance"]},PositionSensor:{name:"PositionSensor",methods:[],properties:["position"]},Readme:{name:"Readme",methods:["getReadmeMarkdown"],properties:[]},OauthClient:{name:"OauthClient",methods:["getOauthUrl","onOauthCallback"],properties:[]},MixinProvider:{name:"MixinProvider",methods:["canMixin","getMixin","releaseMixin"],properties:[]},HttpRequestHandler:{name:"HttpRequestHandler",methods:["onRequest"],properties:[]},EngineIOHandler:{name:"EngineIOHandler",methods:["onConnection"],properties:[]},PushHandler:{name:"PushHandler",methods:["onPush"],properties:[]},Program:{name:"Program",methods:["run"],properties:[]},Scriptable:{name:"Scriptable",methods:["eval","loadScripts","saveScript"],properties:[]},ObjectDetector:{name:"ObjectDetector",methods:["getDetectionInput","getObjectTypes"],properties:[]},ObjectDetection:{name:"ObjectDetection",methods:["detectObjects","getDetectionModel"],properties:[]},HumiditySetting:{name:"HumiditySetting",methods:["setHumidity"],properties:["humiditySetting"]},Fan:{name:"Fan",methods:["setFan"],properties:["fan"]},RTCSignalingChannel:{name:"RTCSignalingChannel",methods:["startRTCSignalingSession"],properties:[]},RTCSignalingClient:{name:"RTCSignalingClient",methods:["createRTCSignalingSession"],properties:[]}};let o,a,c,d,u,p,l,h,m;!function(e){e.Builtin="Builtin",e.Camera="Camera",e.Fan="Fan",e.Light="Light",e.Switch="Switch",e.Outlet="Outlet",e.Sensor="Sensor",e.Scene="Scene",e.Program="Program",e.Automation="Automation",e.Vacuum="Vacuum",e.Notifier="Notifier",e.Thermostat="Thermostat",e.Lock="Lock",e.PasswordControl="PasswordControl",e.Display="Display",e.Speaker="Speaker",e.Event="Event",e.Entry="Entry",e.Garage="Garage",e.DeviceProvider="DeviceProvider",e.DataSource="DataSource",e.API="API",e.Doorbell="Doorbell",e.Irrigation="Irrigation",e.Valve="Valve",e.Person="Person",e.Unknown="Unknown"}(o||(o={})),function(e){e.Humidify="Humidify",e.Dehumidify="Dehumidify",e.Auto="Auto",e.Off="Off"}(a||(a={})),function(e){e.Auto="Auto",e.Manual="Manual"}(c||(c={})),function(e){e.C="C",e.F="F"}(d||(d={})),function(e){e.Off="Off",e.Cool="Cool",e.Heat="Heat",e.HeatCool="HeatCool",e.Auto="Auto",e.FanOnly="FanOnly",e.Purifier="Purifier",e.Eco="Eco",e.Dry="Dry",e.On="On"}(u||(u={})),function(e){e.Locked="Locked",e.Unlocked="Unlocked",e.Jammed="Jammed"}(p||(p={})),function(e){e.Idle="Idle",e.Playing="Playing",e.Paused="Paused",e.Buffering="Buffering"}(l||(l={})),function(e){e.ScryptedDevice="ScryptedDevice",e.ScryptedPlugin="ScryptedPlugin",e.OnOff="OnOff",e.Brightness="Brightness",e.ColorSettingTemperature="ColorSettingTemperature",e.ColorSettingRgb="ColorSettingRgb",e.ColorSettingHsv="ColorSettingHsv",e.Notifier="Notifier",e.StartStop="StartStop",e.Pause="Pause",e.Dock="Dock",e.TemperatureSetting="TemperatureSetting",e.Thermometer="Thermometer",e.HumiditySensor="HumiditySensor",e.Camera="Camera",e.VideoCamera="VideoCamera",e.VideoCameraConfiguration="VideoCameraConfiguration",e.Intercom="Intercom",e.Lock="Lock",e.PasswordStore="PasswordStore",e.Authenticator="Authenticator",e.Scene="Scene",e.Entry="Entry",e.EntrySensor="EntrySensor",e.DeviceProvider="DeviceProvider",e.DeviceDiscovery="DeviceDiscovery",e.DeviceCreator="DeviceCreator",e.Battery="Battery",e.Refresh="Refresh",e.MediaPlayer="MediaPlayer",e.Online="Online",e.SoftwareUpdate="SoftwareUpdate",e.BufferConverter="BufferConverter",e.Settings="Settings",e.BinarySensor="BinarySensor",e.IntrusionSensor="IntrusionSensor",e.PowerSensor="PowerSensor",e.AudioSensor="AudioSensor",e.MotionSensor="MotionSensor",e.AmbientLightSensor="AmbientLightSensor",e.OccupancySensor="OccupancySensor",e.FloodSensor="FloodSensor",e.UltravioletSensor="UltravioletSensor",e.LuminanceSensor="LuminanceSensor",e.PositionSensor="PositionSensor",e.Readme="Readme",e.OauthClient="OauthClient",e.MixinProvider="MixinProvider",e.HttpRequestHandler="HttpRequestHandler",e.EngineIOHandler="EngineIOHandler",e.PushHandler="PushHandler",e.Program="Program",e.Scriptable="Scriptable",e.ObjectDetector="ObjectDetector",e.ObjectDetection="ObjectDetection",e.HumiditySetting="HumiditySetting",e.Fan="Fan",e.RTCSignalingChannel="RTCSignalingChannel",e.RTCSignalingClient="RTCSignalingClient"}(h||(h={})),function(e){e.AcceptUrlParameter="accept-url",e.Url="text/x-uri",e.InsecureLocalUrl="text/x-insecure-local-uri",e.LocalUrl="text/x-local-uri",e.PushEndpoint="text/x-push-endpoint",e.MediaStreamUrl="text/x-media-url",e.FFmpegInput="x-scrypted/x-ffmpeg-input",e.RTCAVSignalingPrefix="x-scrypted-rtc-signaling-",e.SchemePrefix="x-scrypted/x-scrypted-scheme-",e.MediaObject="x-scrypted/x-scrypted-media-object"}(m||(m={}))}},t={};function i(r){var s=t[r];if(void 0!==s)return s.exports;var n=t[r]={exports:{}};return e[r].call(n.exports,n,n.exports,i),n.exports}i.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var r in t)i.o(t,r)&&!i.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),i.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r={};(()=>{"use strict";i.r(r),i.d(r,{default:()=>$e});var e=i(510),t=i.n(e);const s=require("events"),{deviceManager:n}=t();class o extends e.MixinDeviceBase{constructor(t){super(t),this.settingsGroup=t.group,this.settingsGroupKey=t.groupKey,process.nextTick((()=>n.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)))}async getSettings(){const t=this.mixinDeviceInterfaces.includes(e.ScryptedInterface.Settings)?this.mixinDevice.getSettings():void 0,i=this.getMixinSettings(),r=[];try{const e=await t||[];r.push(...e)}catch(e){const t=this.name,i=`${t} Extension settings failed to load.`;this.console.error(i,e),r.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:i,readonly:!0})}try{const e=await i||[];for(const t of e)t.group=t.group||this.settingsGroup,t.key=this.settingsGroupKey+":"+t.key;r.push(...e)}catch(e){const t=n.getDeviceState(this.mixinProviderNativeId).name,i=`${t} Extension settings failed to load.`;this.console.error(i,e),r.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:i,readonly:!0})}return r}async putSetting(t,i){const r=this.settingsGroupKey+":";if(!t?.startsWith(r))return this.mixinDevice.putSetting(t,i);await this.putMixinSetting(t.substring(r.length),i),n.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)}async release(){await n.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)}}const a=require("net");var c=i.n(a);const d=require("child_process");var u=i.n(d);const p=require("dgram");var l=i.n(p);async function h(e,t){e.bind(t),await(0,s.once)(e,"listening");const i=e.address().port;return{port:i,url:`udp://127.0.0.1:${i}`}}async function m(e){return h(e,0)}async function f(e,t){return e.bind(t),await(0,s.once)(e,"listening"),{port:t,url:`udp://127.0.0.1:${t}`}}async function g(){const e=new(c().Server),t=await async function(e){return e.listen(0),await(0,s.once)(e,"listening"),e.address().port}(e);return{url:`tcp://127.0.0.1:${t}`,port:t,clientPromise:new Promise(((t,i)=>{const r=setTimeout((()=>{i(new Error("timeout waiting for client"))}),3e4);e.on("connection",(i=>{e.close(),clearTimeout(r),t(i)}))}))}}const y=require("process");var v=i.n(y);const S=["decode_slice_header error","no frame!","non-existing PPS"];function b(e,t,i,r){const s=!!v().env.SCRYPTED_FFMPEG_NOISY||!!r?.getItem("SCRYPTED_FFMPEG_NOISY");function n(e){const r=n=>{const o=n.toString();for(const e of S)if(-1!==o.indexOf(e))return;if(!(s||i||-1===o.indexOf("frame=")&&-1===o.indexOf("size=")))return e(o),e("video/audio detected, discarding further input"),t.stdout.removeListener("data",r),void t.stderr.removeListener("data",r);e(o)};return r}t.stdout?.on("data",n(e.log)),t.stderr?.on("data",n(e.error)),t.on("exit",(()=>e.log("ffmpeg exited")))}function w(e,t){const i=[];for(const e of t)try{const t=new URL(e);i.push(`${t.protocol}[REDACTED]`)}catch(t){i.push(e)}e.log(i.join(" "))}const{mediaManager:P}=t();async function I(e,t){return new Promise((i=>{const r=s=>{const n=s.toString(),o=n.indexOf(`${t}: `);if(-1!==o){const s=n.substring(o+t.length+1).trim();let a=s.indexOf(" ");const c=s.indexOf(",");-1!==a&&c<a&&(a=c),-1!==a&&(e.stdout.removeListener("data",r),e.stderr.removeListener("data",r),i(s.substring(0,a)))}};e.stdout.on("data",r),e.stderr.on("data",r)}))}function M(e,t,i,r){let s;function n(){console.error("timeout waiting for data, killing parser session",e),t()}function o(){r&&(clearTimeout(s),s=setTimeout(n,r))}return i.once("killed",(()=>clearTimeout(s))),o(),{resetActivityTimer:o}}async function x(e,t){const{console:i}=t;let r,n=!0;const o=new s.EventEmitter;let a,c,d,p,h;o.on("error",(e=>i.error("rebroadcast error",e)));const y=new Promise(((e,t)=>{p=e,h=t}));function v(){n&&(o.emit("killed"),o.emit("error",new Error("killed"))),n=!1,D?.kill(),setTimeout((()=>D?.kill("SIGKILL")),1e3),h?.(new Error("ffmpeg was killed before connecting to the rebroadcast session")),clearTimeout(r)}const S=e.inputArguments.slice();r=setTimeout(v,3e4);let x=!1;const C=["pipe","pipe","pipe"];let O=3;for(const e of Object.keys(t.parsers)){const r=t.parsers[e];if(r.parseDatagram){x=!0;const i=l().createSocket("udp4"),s=await m(i),n=l().createSocket("udp4");await f(n,s.port+1),o.once("killed",(()=>{i.close(),n.close()})),S.push(...r.outputArguments,s.url.replace("udp://","rtp://"));const{resetActivityTimer:a}=M(e,v,o,t?.timeout);(async()=>{for await(const t of r.parseDatagram(i,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),o.emit(e,t),a()})(),(async()=>{for await(const t of r.parseDatagram(n,parseInt(d?.[2]),parseInt(d?.[3]),"rtcp"))p?.(void 0),o.emit(e,t),a()})()}else if(r.tcpProtocol){const s=await g(),n=new URL(r.tcpProtocol);n.port=s.port.toString(),S.push(...r.outputArguments,n.toString());const{resetActivityTimer:a}=M(e,v,o,t?.timeout);(async()=>{const t=await s.clientPromise;try{for await(const i of r.parse(t,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),o.emit(e,i),a()}catch(e){i.error("rebroadcast parse error",e),v()}})()}else S.push(...r.outputArguments,"pipe:"+O++),C.push("pipe")}x&&(S.push("-sdp_file","pipe:"+O++),C.push("pipe")),S.unshift("-hide_banner"),w(i,S);const D=u().spawn(await P.getFFmpegPath(),S,{stdio:C});let k;b(i,D,void 0,t?.storage),D.on("exit",v),k=x?new Promise((e=>{const t=[];D.stdio[O-1].on("data",(i=>{t.push(i),e(t)}))})):Promise.resolve([]);let T=0;return Object.keys(t.parsers).forEach((async e=>{const r=t.parsers[e];if(!r.parse||r.tcpProtocol)return;const s=D.stdio[3+T];T++;try{const{resetActivityTimer:i}=M(e,v,o,t?.timeout);for await(const t of r.parse(s,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),o.emit(e,t),i()}catch(e){i.error("rebroadcast parse error",e),v()}})),async function(e){return I(e,"Audio")}(D).then((e=>a=e)),async function(e){return I(e,"Video")}(D).then((e=>c=e)),async function(e){return new Promise((t=>{const i=r=>{const s=r.toString(),n=/(([0-9]{2,5})x([0-9]{2,5}))/.exec(s);n&&(e.stdout.removeListener("data",i),e.stderr.removeListener("data",i),t(n))};e.stdout.on("data",i),e.stderr.on("data",i)}))}(D).then((e=>d=e)),await y,p=void 0,h=void 0,clearTimeout(r),{sdp:k,inputAudioCodec:a,inputVideoCodec:c,inputVideoResolution:d,isActive:()=>n,kill:v,mediaStreamOptions:e.mediaStreamOptions||{id:void 0,name:void 0},emit(e,t){return o.emit(e,t),this},on(e,t){return o.on(e,t),this},once(e,t){return o.once(e,t),this},removeListener(e,t){return o.removeListener(e,t),this}}}async function C(e,t){const i=await e;let r=!0;const s=()=>{i.removeAllListeners(),i.destroy();const e=n;n=void 0,e?.()};let n=t?.connect((e=>{r&&(r=!1,e.startStream&&i.write(e.startStream));for(const t of e.chunks)i.write(t);return i.writableLength}),s);i.once("close",(()=>{s()})),i.on("error",(e=>t?.console?.log("client stream ended")))}async function O(e,t){if(!t)return Buffer.alloc(0);{const i=e.read(t);if(i)return i}return new Promise(((i,r)=>{const s=()=>{const r=e.read(t);r&&(o(),i(r))},n=()=>{o(),r(new Error(`stream ended during read for minimum ${t} bytes`))},o=()=>{e.removeListener("readable",s),e.removeListener("end",n)};e.on("readable",s),e.on("end",n)}))}const D="\n".charCodeAt(0);async function k(e){return async function(e,t){const i=[];let r=0;for(;;){const s=await O(e,1);if(!s)throw new Error("end of stream");if(s[0]===t)break;i[r++]=s[0]}return Buffer.from(i).toString()}(e,D)}function T(e){return e}async function*A(e){for(;;){const t=await O(e,8),i=t.readInt32BE(0)-8,r=t.slice(4).toString(),s=await O(e,i);yield{header:t,length:i,type:r,data:s}}}async function*E(e){let t,i,r;for await(const s of e)t?i||(i=s):t=s,yield{startStream:r,chunks:[s.header,s.data],type:s.type},t&&i&&!r&&(r=Buffer.concat([t.header,t.data,i.header,i.data]))}const{systemManager:R}=t(),_="v4";class L extends e.ScryptedDeviceBase{hasEnabledMixin={};constructor(t){super(t);try{this.hasEnabledMixin=JSON.parse(this.storage.getItem("hasEnabledMixin"))}catch(e){this.hasEnabledMixin={}}this.pluginsComponent=R.getComponent("plugins"),R.listen((async(t,i,r)=>{i.eventInterface!==e.ScryptedInterface.ScryptedDevice||i.property||this.maybeEnableMixin(t)}));for(const e of Object.keys(R.getSystemState())){const t=R.getDeviceById(e);this.maybeEnableMixin(t)}}async shouldEnableMixin(e){return!0}async maybeEnableMixin(e){if(!e||e.mixins?.includes(this.id))return;if(this.hasEnabledMixin[e.id]===_)return;if(!await this.canMixin(e.type,e.interfaces))return;if(!await this.shouldEnableMixin(e))return;this.log.i("auto enabling mixin for "+e.name);const t=(e.mixins||[]).slice();t.push(this.id);const i=await this.pluginsComponent;await i.setMixins(e.id,t),this.setHasEnabledMixin(e.id)}setHasEnabledMixin(e){this.hasEnabledMixin[e]!==_&&(this.hasEnabledMixin[e]=_,this.storage.setItem("hasEnabledMixin",JSON.stringify(this.hasEnabledMixin)))}}const{mediaManager:U}=t();function B(e,t,i=["recvonly","sendrecv"]){const r=e.split("m=").filter((e=>e.startsWith(t)));for(const e of r){const t=()=>{const t=e.split("\n").map((e=>e.trim())).find((e=>e.startsWith("a=control:")));return{section:"m="+e,trackId:t?.split("a=control:")?.[1]}};for(const r of i)if(e.includes(`a=${r}`))return t();if(i.includes("recvonly")&&!e.includes("sendonly")&&!e.includes("inactive"))return t()}}const j=require("crypto");var N=i.n(j);const $=require("tls");var F=i.n($);const H=require("os");function V(e,t){for(var i=0;i<t.length;i++){var r=t[i];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function q(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function K(e){var t="function"==typeof Map?new Map:void 0;return K=function(e){if(null===e||(i=e,-1===Function.toString.call(i).indexOf("[native code]")))return e;var i;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==t){if(t.has(e))return t.get(e);t.set(e,r)}function r(){return W(e,arguments,z(this).constructor)}return r.prototype=Object.create(e.prototype,{constructor:{value:r,enumerable:!1,writable:!0,configurable:!0}}),J(r,e)},K(e)}function W(e,t,i){return W=G()?Reflect.construct:function(e,t,i){var r=[null];r.push.apply(r,t);var s=new(Function.bind.apply(e,r));return i&&J(s,i.prototype),s},W.apply(null,arguments)}function G(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}function J(e,t){return J=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},J(e,t)}function z(e){return z=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},z(e)}let Y=function(e){function t(e,i,...r){var s,n,o;return function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t),e instanceof Array||(r=(void 0===i?[]:[i]).concat(r),i=e,e=[]),n=this,(s=!(o=z(t).call(this,i))||"object"!=typeof o&&"function"!=typeof o?q(n):o).code=i||"E_UNEXPECTED",s.params=r,s.wrappedErrors=e,s.name=s.toString(),Error.captureStackTrace&&Error.captureStackTrace(q(s),s.constructor),s}var i,r,s;return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&J(e,t)}(t,e),i=t,(r=[{key:"toString",value:function(){return(this.wrappedErrors.length?this.wrappedErrors[this.wrappedErrors.length-1].stack+H.EOL:"")+this.constructor.name+": "+this.code+" ("+this.params.join(", ")+")"}}])&&V(i.prototype,r),s&&V(i,s),t}(K(Error));function Q(e){return e instanceof Y||e.constructor&&e.constructor.name&&e.constructor.name.endsWith("Error")&&"string"==typeof e.code&&X(e.code)&&e.params&&e.params instanceof Array}function X(e){return/^([A-Z0-9_]+)$/.test(e)}Y.wrap=function(e,t,...i){let r=null;const s=X(e.message),n=(e.wrappedErrors||[]).concat(e);return t||(t=s?e.message:"E_UNEXPECTED"),e.message&&!s&&i.push(e.message),r=new Y(n,t,...i),r},Y.cast=function(e,...t){return Q(e)?e:Y.wrap.apply(Y,[e].concat(t))},Y.bump=function(e,...t){return Q(e)?Y.wrap.apply(Y,[e,e.code].concat(e.params)):Y.wrap.apply(Y,[e].concat(t))};const Z=Y,ee=/\w+=(".*?"|[^",]+)(?=,|$)/g;function te(e,t,i=[]){const r=e.trim().match(ee);if(!r)throw new Z("E_MALFORMED_QUOTEDKEYVALUE",e);const s=r.map(((e,t)=>{const i=e.split("=");if(2!==i.length)throw new Z("E_MALFORMED_QUOTEDKEYVALUE",t,e,i.length);return i})).reduce((function(e,[i,r],s){if(-1===t.indexOf(i))throw new Z("E_UNAUTHORIZED_KEY",s,i);return e[i]=r.replace(/^"(.+(?="$))"$/,"$1"),e}),{});return re(i,s),s}function ie(e,t,i=[]){return re(i,e),t.reduce((function(t,i){return e[i]?t+(t?", ":"")+i+'="'+e[i]+'"':t}),"")}function re(e,t){e.forEach((e=>{if(void 0===t[e])throw new Z("E_REQUIRED_KEY",e)}))}const se=["realm","nonce"],ne=[...se,"domain","opaque","stale","algorithm","qop"],oe=["username","realm","nonce","uri","response"],ae=[...oe,"algorithm","cnonce","opaque","qop","nc"];function ce(e,t){const i=N().createHash(e);return i.update(t),i.digest("hex")}const de={type:"Digest",parseWWWAuthenticateRest:function(e){return te(e,ne,se)},buildWWWAuthenticateRest:function(e){return ie(e,ne,se)},parseAuthorizationRest:function(e){return te(e,ae,oe)},buildAuthorizationRest:function(e){return ie(e,ae,oe)},computeHash:function(e){const t=e.ha1||ce(e.algorithm,[e.username,e.realm,e.password].join(":")),i=ce(e.algorithm,[e.method,e.uri].join(":"));return ce(e.algorithm,[t,e.nonce,e.nc,e.cnonce,e.qop,i].join(":"))}};async function ue(e){let t=[];for(;;){let i=await k(e);if(i=i.trim(),!i)return t;t.push(i)}}function pe(e){const t={};for(const i of e.slice(1)){const e=i.indexOf(":");let r="";-1!==e&&(r=i.substring(e+1).trim());t[i.substring(0,e).toLowerCase()]=r}return t}class le extends class{constructor(e){this.console=e}write(e,t,i){let r=`${e}\r\n`;i&&(t["Content-Length"]=i.length.toString());for(const[e,i]of Object.entries(t))r+=`${e}: ${i}\r\n`;r+="\r\n",this.client.write(r),this.console?.log("rtsp outgpoing message\n",r),this.console?.log(),i&&this.client.write(i)}async readMessage(){const e=await ue(this.client);return this.console?.log("rtsp incoming message\n",e.join("\n")),this.console?.log(),e}}{cseq=0;constructor(e,t){super(t),this.url=e;const i=new URL(e),r=parseInt(i.port)||554;e.startsWith("rtsps")?this.client=F().connect({rejectUnauthorized:!1,port:r,host:i.hostname}):this.client=c().connect(r,i.hostname)}writeRequest(e,t,i,r){t=t||{};let s=this.url;i&&(s+="/"+i);const n=new URL(s);n.username="",n.password="",s=n.toString();const o=`${e} ${s} RTSP/1.0`,a=this.cseq++;t.CSeq=a.toString(),this.authorization&&(t.Authorization=this.authorization),this.session&&(t.Session=this.session),this.write(o,t,r)}async request(e,t,i,r,s){this.writeRequest(e,t,i,r);const n=await this.readMessage(),o=n[0],a=pe(n);if(!o.includes("200")&&!a["www-authenticate"])throw new Error(o);if(a["www-authenticate"]){if(s)throw new Error("auth failed");const n=new URL(this.url),o=de.parseWWWAuthenticateRest(a["www-authenticate"]),c=N().createHash("md5").update(`${n.username}:${o.realm}:${n.password}`).digest("hex"),d=N().createHash("md5").update(`${e}:${n.pathname}`).digest("hex"),u=N().createHash("md5").update(`${c}:${o.nonce}:${d}`).digest("hex"),p={username:n.username,realm:o.realm,nonce:o.nonce,uri:n.pathname,algorithm:"MD5",response:u},l=Object.entries(p).map((([e,t])=>{return`${e}=${t&&(i=t,`"${i.replace(/"/g,'\\"')}"`)}`;var i})).join(", ");return this.authorization=`Digest ${l}`,this.request(e,t,i,r,!0)}const c=parseInt(a["content-length"]);return c?{headers:a,body:await O(this.client,c)}:{headers:a,body:void 0}}async options(){return this.request("OPTIONS",{})}async writeGetParameter(){return this.writeRequest("GET_PARAMETER",{})}async describe(e){return this.request("DESCRIBE",{...e||{},Accept:"application/sdp"})}async setup(e,t){const i={Transport:`RTP/AVP/TCP;unicast;interleaved=${e}-${e+1}`},r=await this.request("SETUP",i,t);if(r.headers.session){const e=function(e){const t={};for(const i of e.split(";")){const[e,r]=i.split("=",2);t[e]=r}return t}(r.headers.session);let t=parseInt(e.timeout);if(t){let e=setInterval((()=>this.writeGetParameter()),1e3*(t-5));this.client.once("close",(()=>clearInterval(e)))}this.session=r.headers.session.split(";")[0]}return r}async play(){return await this.request("PLAY",{Range:"npt=0.000-"}),this.client}async teardown(){try{return await this.request("TEARDOWN")}finally{this.client.destroy()}}}class he{udpPorts={video:0,audio:0};constructor(e,t,i,r){this.client=e,this.sdp=t,this.udp=i,this.checkRequest=r,this.session=(0,j.randomBytes)(4).toString("hex"),t&&(t=t.trim())}async handleSetup(){let e=[];for(;;){let t=await k(this.client);if(t=t.trim(),t)e.push(t);else{if(!await this.headers(e))break;e=[]}}}async handlePlayback(){return this.handleSetup()}async handleTeardown(){return this.handleSetup()}async*handleRecord(){for(;;){const e=await O(this.client,4),t=e.readUInt16BE(2),i=await O(this.client,t),r=e.readUInt8(1);yield{type:r-r%2===this.videoChannel?"video":"audio",rtcp:r%2==1,header:e,packet:i}}}send(e,t){const i=Buffer.alloc(4);i.writeUInt8(36,0),i.writeUInt8(t,1),i.writeUInt16BE(e.length,2),this.client.write(i),this.client.write(Buffer.from(e))}sendUdp(e,t,i){this.udp.send(t,i?e+1:e,"127.0.0.1")}sendVideo(e,t){if(this.udp&&this.udpPorts.video)this.sendUdp(this.udpPorts.video,e,t);else{if(null==this.videoChannel)throw new Error("rtsp videoChannel not set up");this.send(e,t?this.videoChannel+1:this.videoChannel)}}sendAudio(e,t){if(this.udp&&this.udpPorts.audio)this.sendUdp(this.udpPorts.audio,e,t);else{if(null==this.audioChannel)throw new Error("rtsp audioChannel not set up");this.send(e,t?this.audioChannel+1:this.audioChannel)}}options(e,t){const i={Public:"DESCRIBE, OPTIONS, PAUSE, PLAY, SETUP, TEARDOWN, ANNOUNCE, RECORD"};this.respond(200,"OK",t,i)}describe(e,t){const i={};i["Content-Base"]=e,i["Content-Type"]="application/sdp",this.respond(200,"OK",t,i,Buffer.from(this.sdp))}setup(e,t){const i={},r=t.transport;i.Transport=r,i.Session=this.session;let s=B(this.sdp,"audio"),n=B(this.sdp,"video");if(r.includes("UDP")){if(!this.udp)return void this.respond(461,"Unsupported Transport",t,{});const i=r.match(/.*?client_port=([0-9]+)-([0-9]+)/),[o,a,c]=i;s&&e.includes(s.trackId)?this.udpPorts.audio=parseInt(a):n&&e.includes(n.trackId)?this.udpPorts.video=parseInt(a):this.console?.warn("unknown track id",e)}else if(r.includes("TCP")){const t=r.match(/.*?interleaved=([0-9]+)-([0-9]+)/);if(t){const i=parseInt(t[1]);parseInt(t[2]);s&&e.includes(s.trackId)?this.audioChannel=i:n&&e.includes(n.trackId)?this.videoChannel=i:this.console?.warn("unknown track id",e)}}this.respond(200,"OK",t,i)}play(e,t){const i={};let r=B(this.sdp,"audio"),s=B(this.sdp,"video"),n="";r&&(n=`url=${e}/trackID=${r.trackId};seq=0;rtptime=0`),r&&s&&(n+=","),s&&(n+=`url=${e}/trackID=${s.trackId};seq=0;rtptime=0`),i["RTP-Info"]=n,i.Range="npt=now-",i.Session=this.session,this.respond(200,"OK",t,i)}async announce(e,t){const i=parseInt(t["content-length"]),r=await O(this.client,i);this.sdp=r.toString();const s={};s.Session=this.session,this.respond(200,"OK",t,s)}async record(e,t){const i={};i.Session=this.session,this.respond(200,"OK",t,i)}async teardown(e,t){const i={};i.Session=this.session,this.respond(200,"OK",t,i)}async headers(e){this.console?.log("request headers",e.join("\n"));let[t,i]=e[0].split(" ",2);t=t.toLowerCase();const r=pe(e);if(this.checkRequest){let s;try{s=await this.checkRequest(t,i,r,e)}catch(e){this.console?.error("error checking request",e)}if(!s)throw this.respond(400,"Bad Request",r,{}),this.client.destroy(),new Error("check request failed")}if(this[t])return await this[t](i,r),"play"!==t&&"record"!==t&&"teardown"!==t;this.respond(400,"Bad Request",r,{})}respond(e,t,i,r,s){let n=`RTSP/1.0 ${e} ${t}\r\n`;i.cseq&&(r.CSeq=i.cseq),s&&(r["Content-Length"]=s.length.toString());for(const[e,t]of Object.entries(r))n+=`${e}: ${t}\r\n`;this.console?.log("response headers",n),n+="\r\n",this.client.write(n),s&&this.client.write(s)}}const{systemManager:me}=t();class fe{values={};constructor(e,t){this.device=e,this.settings=t;for(const e of Object.keys(t)){const i=t[e],r=()=>this.getItem(e);let s;s="clippath"!==i.type?r:()=>{try{return JSON.parse(r())}catch(e){}},Object.defineProperty(this.values,e,{get:s,set:t=>this.putSetting(e,t)})}}async getSettings(){const e=[];for(const[t,i]of Object.entries(this.settings)){const r=Object.assign({},i);r.hide||(r.key=t,r.value=this.getItem(t),e.push(r),delete r.onPut,delete r.mapPut)}return e}async putSetting(t,i){const r=this.settings[t];let s;r&&(s=this.getItem(t)),r?.noStore||(r.mapPut&&(i=r.mapPut(s,i)),"object"==typeof i?this.device.storage.setItem(t,JSON.stringify(i)):this.device.storage.setItem(t,i?.toString())),r?.onPut?.(s,i),this.device.onDeviceEvent(e.ScryptedInterface.Settings,void 0)}getItem(e){const t=this.settings[e];if(!t)return this.device.storage.getItem(e);const i=t.multiple?"array":t.type;return function(e,t,i){if("boolean"===t)return"true"===e||"false"!==e&&(i||!1);if("number"===t)return parseFloat(e)||i||0;if("integer"===t)return parseInt(e)||i||0;if("array"===t)try{return JSON.parse(e)}catch(e){return[]}return"device"===t?me.getDeviceById(e):e||i}(this.device.storage.getItem(e),i,t.defaultValue)}}const ge=require("stream"),{mediaManager:ye}=t();async function ve(e,t,i,r,s,n){let o=!0;const a=new ge.EventEmitter;a.on("error",(t=>e.error("rebroadcast error",t)));const c=parseInt(i.match(/m=audio.* ([0-9]+)/)?.[1]),d=parseInt(i.match(/m=video.* ([0-9]+)/)?.[1]),u=()=>{o&&(a.emit("killed"),a.emit("error",new Error("killed"))),o=!1,t.destroy()};let p,l;t.on("close",u),t.on("error",u),(async()=>{const{resetActivityTimer:e}=M("rtsp",u,a,n?.timeout);for(;;){let i,r;if(s){if(i=await O(t,4),"RTSP"===i.toString()){const e=pe(await ue(t)),i=parseInt(e["content-length"]);i&&await O(t,i);continue}r=i.readUInt16BE(2)}else i=await O(t,2),r=i.readUInt16BE(0);const n=await O(t,r),o=127&n[1];if(!s){const e=Buffer.alloc(2);e[0]=36,o===c?e[1]=0:o===d&&(e[1]=2),i=Buffer.concat([e,i])}let u;o===c?u="rtp-audio":o===d&&(u="rtp-video");const p={chunks:[i,n],type:u};a.emit("rtsp",p),e()}})().finally(u);const h=B(i,"audio"),m=B(i,"video");if(h){const e=h.section.toLowerCase();e.includes("mpeg4")?p="aac":e.includes("pcm")&&(p="pcm")}return m&&m.section.toLowerCase().includes("h264")&&(l="h264"),{sdp:Promise.resolve([Buffer.from(i)]),inputAudioCodec:p,inputVideoCodec:l,inputVideoResolution:void 0,isActive:()=>o,kill:u,mediaStreamOptions:r,emit(e,t){return a.emit(e,t),this},on(e,t){return a.on(e,t),this},once(e,t){return a.once(e,t),this},removeListener(e,t){return a.removeListener(e,t),this}}}const{mediaManager:Se,log:be,systemManager:we,deviceManager:Pe}=t(),Ie=1e4,Me="prebufferDuration",xe="sendKeyframe",Ce="Default",Oe="AAC or No Audio",De=`${Oe} (Copy)`,ke="Compatible Audio",Te="Other Audio",Ae=["aac","mp3","mp2","opus"],Ee="-fflags +genpts",Re="Scrypted",_e="FFmpeg",Le="Default",Ue=[Oe,ke,Te],Be=["mpegts","mp4","rtsp"];class je{prebuffers={mp4:[],mpegts:[],rtsp:[]};detectedIdrInterval=0;prevIdr=0;audioDisabled=!1;activeClients=0;constructor(e,t,i){this.mixin=e,this.advertisedMediaStreamOptions=t,this.stopInactive=i,this.storage=e.storage,this.console=e.console,this.mixinDevice=e.mixinDevice,this.audioConfigurationKey="audioConfiguration-"+this.streamId,this.ffmpegInputArgumentsKey="ffmpegInputArguments-"+this.streamId,this.rebroadcastModeKey="rebroadcastMode-"+this.streamId,this.lastDetectedAudioCodecKey="lastDetectedAudioCodec-"+this.streamId,this.rtspParserKey="rtspParser-"+this.streamId;const r="rtspServerPathKey-"+this.streamId;this.rtspServerPath=this.storage.getItem(r),this.rtspServerPath||(this.rtspServerPath=N().randomBytes(8).toString("hex"),this.storage.setItem(r,this.rtspServerPath))}get streamId(){return this.advertisedMediaStreamOptions.id}get streamName(){return this.advertisedMediaStreamOptions.name}clearPrebuffers(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtsp=[]}ensurePrebufferSession(){this.parserSessionPromise||this.mixin.released||(this.console.log(this.streamName,"prebuffer session started"),this.parserSessionPromise=this.startPrebufferSession(),this.parserSessionPromise.catch((()=>this.parserSessionPromise=void 0)))}getAudioConfig(){let e=this.storage.getItem(this.audioConfigurationKey)||"";Ue.find((t=>e.startsWith(t)))||(e="");const t=-1!==e.indexOf(Oe),i=-1!==e.indexOf(ke),r=-1!==e.indexOf(Te);return{isUsingDefaultAudioConfig:!(t||i||r),aacAudio:t,compatibleAudio:i,reencodeAudio:r}}canUseRtspParser(e,t){if(e)return!1;if("rtsp"!==t?.container)return!1;const{isUsingDefaultAudioConfig:i,compatibleAudio:r,aacAudio:s}=this.getAudioConfig();return i||r||s}getParser(e,t,i){if(!this.canUseRtspParser(t,i))return _e;const r=e&&"scrypted"===i?.tool?Re:_e,s=this.storage.getItem(this.rtspParserKey);return s&&s!==Le?s===Re?Re:s===_e?_e:r:r}getRebroadcastMode(){const e=this.storage.getItem(this.rebroadcastModeKey),t=e?.startsWith("RTSP");return{rtspMode:e?.startsWith("RTSP"),muxingMp4:!t||e?.includes("MP4")}}async getMixinSettings(){const e=[],t=this.parserSession;let i=0,r=0;const{muxingMp4:s,rtspMode:n}=this.getRebroadcastMode();for(const e of s?this.prebuffers.mp4:this.prebuffers.rtsp){r=r||e.time;for(const t of e.chunk.chunks)i+=t.byteLength}const o=Date.now()-r,a=Math.round(i/o*8),c=this.streamName?`Rebroadcast: ${this.streamName}`:"Rebroadcast";e.push({title:"Audio Codec Transcoding",group:c,description:"Configuring your camera to output AAC, MP3, MP2, or Opus is recommended. PCM/G711 cameras should set this to Transcode.",type:"string",key:this.audioConfigurationKey,value:this.storage.getItem(this.audioConfigurationKey)||Ce,choices:[Ce,De,"Compatible Audio (Copy)","Other Audio (Transcode)"]},{title:"Rebroadcast Container",group:c,description:'Experimental: The container format to use when rebroadcasting. MPEG-TS is stable. RTSP is lower latency. The default is "MPEG-TS" for this camera.',placeholder:"MPEG-TS",choices:[Le,"MPEG-TS","RTSP"],key:this.rebroadcastModeKey,value:this.storage.getItem(this.rebroadcastModeKey)||Le});const d=()=>{e.push({title:"FFmpeg Input Arguments Prefix",group:c,description:"Optional/Advanced: Additional input arguments to pass to the ffmpeg command. These will be placed before the input arguments.",key:this.ffmpegInputArgumentsKey,value:this.storage.getItem(this.ffmpegInputArgumentsKey),placeholder:Ee,choices:[Ee,"-use_wallclock_as_timestamps 1","-v verbose"],combobox:!0})};if(this.canUseRtspParser(s,this.advertisedMediaStreamOptions)&&n&&"rtsp"===this.advertisedMediaStreamOptions?.container){const t=this.getParser(n,s,this.advertisedMediaStreamOptions);e.push({key:this.rtspParserKey,group:c,title:"RTSP Parser",description:`Experimental: The RTSP Parser used to read the stream. FFmpeg is stable. The Scrypted parser is lower latency. The Scrypted Parser is only available when the Audo Codec is not Transcoding and the Rebroadcast Container is RTSP. The default is "${t}" for this camera.`,value:this.storage.getItem(this.rtspParserKey)||Le,choices:[Le,_e,Re]}),t!==Re&&d()}else d();return t?e.push({key:"detectedResolution",group:c,title:"Detected Resolution and Bitrate",readonly:!0,value:`${t?.inputVideoResolution?.[0]||"unknown"} @ ${a||"unknown"} Kb/s`,description:"Configuring your camera to 1920x1080, 2000Kb/S, Variable Bit Rate, is recommended."},{key:"detectedCodec",group:c,title:"Detected Video/Audio Codecs",readonly:!0,value:(t?.inputVideoCodec?.toString()||"unknown")+"/"+(t?.inputAudioCodec?.toString()||"unknown"),description:"Configuring your camera to H264 video and AAC/MP3/MP2/Opus audio is recommended."},{key:"detectedKeyframe",group:c,title:"Detected Keyframe Interval",description:"Configuring your camera to 4 seconds is recommended (IDR aka Frame Interval = FPS * 4 seconds).",readonly:!0,value:(this.detectedIdrInterval||0)/1e3||"unknown"}):e.push({title:"Status",group:c,key:"status",description:"Rebroadcast is currently idle and will be started automatically on demand.",value:"Idle",readonly:!0}),n&&e.push({group:c,key:"rtspRebroadcastUrl",title:"RTSP Rebroadcast Url",description:"The RTSP URL of the rebroadcast stream. Substitute localhost as appropriate.",readonly:!0,value:`rtsp://localhost:${this.mixin.plugin.storageSettings.values.rebroadcastPort}/${this.rtspServerPath}`}),e}async startPrebufferSession(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtsp=[];const t=parseInt(this.storage.getItem(Me))||Ie;let i;try{i=(await this.mixinDevice.getVideoStreamOptions()).find((e=>e.id===this.streamId))}catch(e){}const r=null===i?.audio,n=i?.audio?.codec,{isUsingDefaultAudioConfig:o,aacAudio:a,compatibleAudio:d,reencodeAudio:p}=this.getAudioConfig(),{rtspMode:l,muxingMp4:h}=this.getRebroadcastMode();let m=this.storage.getItem(this.lastDetectedAudioCodecKey)||void 0;"null"===m&&(m=null);let f=!1;h&&!r&&!n&&o&&void 0===m&&(this.console.warn("Camera did not report an audio codec, muting the audio stream and probing the codec."),f=!0),!r&&n&&void 0!==m&&m!==n&&this.console.warn("Audio codec plugin reported vs detected mismatch",n,m);const g=void 0===m?n?.toLowerCase():m?.toLowerCase(),y=!Ae.includes(g);!h||f||!1===i?.userConfigurable||r||y&&(o&&be.a(`${this.mixin.name} is using the ${g} audio codec. Configuring your Camera to use AAC, MP3, MP2, or Opus audio is recommended. If this is not possible, Select 'Transcode Audio' in the camera stream's Rebroadcast settings to suppress this alert.`),this.console.warn("Configure your camera to output AAC, MP3, MP2, or Opus audio. Suboptimal audio codec in use:",g));const v=["-bsf:a","aac_adtstoasc"],S=[];let P;this.audioDisabled=!1;const I=null===m;let C=!1;if(!f&&o&&y&&(!1===i?.userConfigurable?this.console.log("camera reports it is not user configurable. transcoding due to incompatible codec",g):this.console.log("camera audio transcoding due to incompatible codec. configure the camera to use a compatible codec if possible."),C=!0),r||f)P=["-an"],this.audioDisabled=!0;else if(p||C)P=["-bsf:a","aac_adtstoasc","-acodec","aac","-ar","32k","-b:a","32k","-ac","1","-profile:a","aac_low","-flags","+global_header"];else if(a||I)P=["-acodec","copy"],P.push(...v);else if(d)P=["-acodec","copy"],P.push(...S);else{P=["-acodec","copy"];const e="aac"===g?v:S;P.push(...e)}const O=["-vcodec","copy"],D={console:this.console,timeout:6e4,parsers:{}};if(this.parsers=D.parsers,this.console.log("rebroadcast mode:",l?"rtsp":"mpegts"),l){const e=function(e){let t;return{container:"rtsp",tcpProtocol:"rtsp://127.0.0.1/"+(0,j.randomBytes)(8).toString("hex"),inputArguments:["-rtsp_transport","tcp"],outputArguments:["-rtsp_transport","tcp",...e?.vcodec||[],...e?.acodec||[],"-f","rtsp"],findSyncFrame(e){for(let t=0;t<e.length;t++){const i=e[t];if("rtp-video"===i.type){const r=31&i.chunks[1].readUInt8(12),s=i.chunks[1].readUInt8(13),n=31&s,o=128&s;if((28===r||29===r)&&5===n&&128==o||5==r)return e.slice(t)}}return e},sdp:new Promise((e=>t=e)),async*parse(e,i,r){const s=new he(e);await s.handleSetup(),t(s.sdp);for await(const{type:e,rtcp:t,header:n,packet:o}of s.handleRecord())yield{chunks:[n,o],type:`${t?"rtcp":"rtp"}-${e}`,width:i,height:r}}}}({vcodec:O,acodec:P});this.sdp=e.sdp,D.parsers.rtsp=e}else D.parsers.mpegts={container:"mpegts",outputArguments:[...(k={vcodec:O,acodec:P})?.vcodec||[],...k?.acodec||[],"-f","mpegts"],parse:(R=188,_=e=>{if(71!=e[0])throw new Error("Invalid sync byte in mpeg-ts packet. Terminating stream.")},async function*(e){let t=[],i=0;for(;;){const r=e.read();if(!r){await(0,s.once)(e,"readable");continue}if(t.push(r),i+=r.length,i<R)continue;const n=Buffer.concat(t);_?.(n);const o=n.length%R,a=n.slice(0,n.length-o),c=n.slice(n.length-o);t=[c],i=c.length,yield{chunks:[a]}}}),findSyncFrame(e){for(let t=0;t<e.length;t++){const i=e[t];for(let r=0;r<i.chunks.length;r++){const s=i.chunks[r];let n=0;for(;n+188<s.length;){const i=s.subarray(n,n+188);if(256==((31&i[1])<<8|i[2])&&32&i[3]&&i[4]>0&&64&i[5])return e.slice(t);n+=188}}}return e}};var k,R,_;h&&(D.parsers.mp4=function(e){return{container:"mp4",outputArguments:[...e?.vcodec||[],...e?.acodec||[],"-movflags","frag_keyframe+empty_moov+default_base_moof","-f","mp4"],async*parse(e){const t=A(e);yield*E(t)},findSyncFrame:T}}({vcodec:O,acodec:P}));const L=await this.mixinDevice.getVideoStream(i),N="x-scrypted/x-rfc4571"===L.mimeType;let $,F;this.storage.removeItem(this.lastDetectedAudioCodecKey);let H=!1;if(l&&N){H=!0,this.console.log("bypassing ffmpeg: using scrypted rfc4571 parser");const e=await Se.convertMediaObjectToJSON(L,"x-scrypted/x-rfc4571"),{url:t,sdp:i,mediaStreamOptions:r}=e;$=await ve(this.console,function(e){const t=new URL(e);if(!t.protocol.startsWith("tcp"))throw new Error("rfc4751 url must be tcp");return c().connect(parseInt(t.port),t.hostname)}(t),i,r,!1,D),this.sdp=$.sdp.then((e=>Buffer.concat(e).toString()))}else{const t=await Se.convertMediaObjectToBuffer(L,e.ScryptedMimeTypes.FFmpegInput),i=JSON.parse(t.toString());F=i.mediaStreamOptions;if(this.getParser(l,h,i.mediaStreamOptions)===Re){H=!0,this.console.log("bypassing ffmpeg: using scrypted rtsp/rfc4571 parser");const e=new le(i.url,this.console);await e.options();const t=(await e.describe()).body.toString().trim();this.console.log("sdp",t),this.sdp=Promise.resolve(t);const{audio:s,video:n}=function(e,t=["recvonly","sendrecv"]){return{audio:B(e,"audio",t)?.trackId,video:B(e,"video",t)?.trackId}}(t);let o=0;r||(await e.setup(o,s),o+=2),await e.setup(o,n);const a=await e.play();$=await ve(this.console,a,t,i.mediaStreamOptions,!0,D);const c=$.kill.bind($);$.kill=async()=>{e.teardown().finally(c),await async function(e){await new Promise((t=>setTimeout(t,e)))}(500),c()}}else{const e=this.storage.getItem(this.ffmpegInputArgumentsKey)||Ee;i.inputArguments.unshift(...e.split(" ")),$=await x(i,D)}}if(H&&h&&this.getVideoStream({id:this.streamId,refresh:!1}).then((async t=>{const i=this.storage.getItem(this.ffmpegInputArgumentsKey)||Ee,r=await Se.convertMediaObjectToJSON(t,e.ScryptedMimeTypes.FFmpegInput);r.inputArguments.unshift(...i.split(" "));const s=await async function(e,t,i,r){const s=e.slice();s.push(...i,...t,"-movflags","frag_keyframe+empty_moov+default_base_moof","-f","mp4","pipe:3"),s.unshift("-hide_banner"),w(r,s);const n=u().spawn(await U.getFFmpegPath(),s,{stdio:["pipe","pipe","pipe","pipe"]});return b(r,n),{cp:n,generator:A(n.stdio[3])}}(r.inputArguments,P,O,this.console),n=()=>{s.cp.kill("SIGKILL"),$.kill(),s.generator.throw(new Error("killed"))};if(!$.isActive)return void n();$.once("killed",n);const{resetActivityTimer:o}=M("mp4",n,$,D.timeout);for await(const e of E(s.generator))o(),$.emit("mp4",e)})).catch((()=>{})),$.inputAudioCodec?Ae.includes($.inputAudioCodec?.toLowerCase())?this.console.log("Detected audio codec is mp4/mpegts compatible.",$.inputAudioCodec):this.console.log("Detected audio codec is not mp4/mpegts compatible.",$.inputAudioCodec):this.console.log("No audio stream detected."),this.storage.setItem(this.lastDetectedAudioCodecKey,$.inputAudioCodec||"null"),"h264"!==$.inputVideoCodec&&this.console.error("Video codec is not h264. If there are errors, try changing your camera's encoder output."),f)return this.console.warn("Audio probe complete, ending rebroadcast session and restarting with detected codecs."),$.kill(),this.startPrebufferSession();if(this.parserSession=$,Pe.onMixinEvent(this.mixin.id,this.mixin.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0),F?.refreshAt){let t,i=F;const r=async()=>{if(!$.isActive)return;const t=await this.mixinDevice.getVideoStream(i),r=await Se.convertMediaObjectToBuffer(t,e.ScryptedMimeTypes.FFmpegInput),n=JSON.parse(r.toString());i=n.mediaStreamOptions,s(i)},s=e=>{const i=e.refreshAt-Date.now()-3e4;this.console.log("refreshing media stream in",i),t=setTimeout(r,i)};s(i),$.once("killed",(()=>clearTimeout(t)))}$.once("killed",(()=>{clearTimeout(this.inactivityTimeout),this.parserSessionPromise=void 0,this.parserSession===$&&(this.parserSession=void 0)}));for(const i of Be){let r=0;$.on(i,(s=>{const n=this.prebuffers[i],o=Date.now(),a=()=>{if(this.prevIdr){const t=!this.detectedIdrInterval;this.detectedIdrInterval=o-this.prevIdr,t&&Pe.onMixinEvent(this.mixin.id,this.mixin.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0)}this.prevIdr=o};if("mdat"===s.type&&a(),"rtp-video"===s.type){const e=31&s.chunks[1].readUInt8(12),t=s.chunks[1].readUInt8(13),i=31&t,r=128&t;(28!==e&&29!==e||5!==i||128!=r)&&5!=e||a()}for(n.push({time:o,chunk:s});n.length&&n[0].time<o-t;)n.shift(),r++;r>1e3&&(this.prebuffers[i]=n.slice(),r=0)}))}return $}printActiveClients(){this.console.log(this.streamName,"active rebroadcast clients:",this.activeClients)}inactivityCheck(e){this.printActiveClients(),this.activeClients||(this.stopInactive?(clearTimeout(this.inactivityTimeout),this.inactivityTimeout=setTimeout((()=>{this.inactivityTimeout=void 0,this.activeClients?this.console.log("inactivity timeout found active clients."):(this.console.log(this.streamName,"terminating rebroadcast due to inactivity"),e.kill())}),3e4)):0===this.activeClients&&this.console.log("stopInactive false"))}async handleRebroadcasterClient(e){const{isActiveClient:t,container:i,session:r,socketPromise:s,requestedPrebuffer:n}=e;C(s,{connect:(e,s)=>{t&&(this.activeClients++,this.printActiveClients());const o=Date.now(),a=t=>{e(t)>1e8&&(this.console.log("more than 100MB has been buffered, did downstream die? killing connection.",this.streamName),c())},c=()=>{s(),r.removeListener(i,a),r.removeListener("killed",c)};r.on(i,a),r.once("killed",c);const d=this.prebuffers[i];for(const e of d)e.time<o-n||a(e.chunk);return()=>{t&&(this.activeClients--,this.inactivityCheck(r)),c()}}})}async getVideoStream(e){this.ensurePrebufferSession();const t=await this.parserSessionPromise,i="false"!==this.storage.getItem(xe);let r=e?.prebuffer;null==r&&(r=i?1.5*Math.max(4e3,this.detectedIdrInterval||4e3):0);const{rtspMode:s}=this.getRebroadcastMode(),n=s?"rtsp":"mpegts";let o=this.parsers[e?.container]?e?.container:n;e?.prebuffer&&"mp4"!==o&&"mp4"===e?.container&&(r+=1.5*(this.detectedIdrInterval||4e3));const a=Object.assign({},t.mediaStreamOptions);a.prebuffer=r;const{reencodeAudio:c}=this.getAudioConfig();let d=!1;this.audioDisabled?a.audio=null:c?a.audio={codec:"aac",encoder:"aac",profile:"aac_low"}:d=!0,d&&(a.audio=t?.mediaStreamOptions?.audio?.codec===t?.inputAudioCodec?t?.mediaStreamOptions?.audio:{codec:t?.inputAudioCodec}),a.video&&t.inputVideoResolution?.[2]&&t.inputVideoResolution?.[3]&&Object.assign(a.video,{width:parseInt(t.inputVideoResolution[2]),height:parseInt(t.inputVideoResolution[3])});const u=Date.now();let p=0;const l=this.prebuffers[o];for(const e of l)if(!(e.time<u-r))for(const t of e.chunk.chunks)p+=t.length;const h=Math.max(5e5,p).toString(),m=await(async i=>{let s,n;if("rtsp"===i){const e=await g();s=e.clientPromise.then((async e=>{let t=await this.sdp;t=function(e){let t=e.split("\n").map((e=>e.trim()));t=t.filter((e=>!e.includes("a=control:")));const i=t.findIndex((e=>e.startsWith("m=video")));-1!==i&&t.splice(i+1,0,"a=control:trackID=video");const r=t.findIndex((e=>e.startsWith("m=audio")));return-1!==r&&t.splice(r+1,0,"a=control:trackID=audio"),t.join("\r\n")}(t);const i=new he(e,t);return await i.handlePlayback(),e})),n=e.url.replace("tcp://","rtsp://")}else{const e=await g();s=e.clientPromise,n=`tcp://127.0.0.1:${e.port}`}const o=!1!==e?.refresh;return this.handleRebroadcasterClient({isActiveClient:o,container:i,requestedPrebuffer:r,socketPromise:s,session:t}),n})(o),f={url:m,container:o,inputArguments:["-analyzeduration","0","-probesize",h,...this.parsers[o].inputArguments||[],"-f",this.parsers[o].container,"-i",m],mediaStreamOptions:a};return Se.createFFmpegMediaObject(f)}}class Ne extends o{released=!1;sessions=new Map;constructor(e,t){super(t),this.plugin=e,this.delayStart()}delayStart(){this.console.log("prebuffer sessions starting in 5 seconds"),setTimeout((()=>this.ensurePrebufferSessions()),5e3)}async getVideoStream(e){await this.ensurePrebufferSessions();const t=e?.id;let i=this.sessions.get(t);return!i||e?.directMediaStream?this.mixinDevice.getVideoStream(e):(i.ensurePrebufferSession(),await i.parserSessionPromise,i=this.sessions.get(t),i?i.getVideoStream(e):this.mixinDevice.getVideoStream(e))}async ensurePrebufferSessions(){const t=await this.mixinDevice.getVideoStreamOptions(),i=this.getEnabledMediaStreamOptions(t),r=i?i.map((e=>e.id)):[void 0],n=t?.map((e=>e.id))||[void 0];if("true"!==this.storage.getItem("warnedCloud")){const e=t?.find((e=>"cloud"===e.source));e&&(this.storage.setItem("warnedCloud","true"),be.a(`${this.name} is a cloud camera. Prebuffering maintains a persistent stream and will not enabled by default. You must enable the Prebuffer stream manually.`))}const o=this.mixinDeviceInterfaces.includes(e.ScryptedInterface.Battery);let a=0;r.length;for(const e of n){let i=this.sessions.get(e);if(!i){const n=t?.find((t=>t.id===e));n?.prebuffer&&be.a(`Prebuffer is already available on ${this.name}. If this is a grouped device, disable the Rebroadcast extension.`);const c=n?.name,d=!r.includes(e);if(i=new je(this,n,o||d),this.sessions.set(e,i),e===t?.[0]?.id&&this.sessions.set(void 0,i),o){this.console.log("camera is battery powered, prebuffering and rebroadcasting will only work on demand.");continue}if(d){this.console.log("stream",c,"will be rebroadcast on demand.");continue}(async()=>{for(;this.sessions.get(e)===i&&!this.released;){i.ensurePrebufferSession();let e=!1;try{const t=await i.parserSessionPromise;a++,e=!0,this.online=!!a,await(0,s.once)(t,"killed"),this.console.error("prebuffer session ended")}catch(e){this.console.error("prebuffer session ended with error",e)}finally{e&&a--,e=!1,this.online=!!a}this.console.log("restarting prebuffer session in 5 seconds"),await new Promise((e=>setTimeout(e,5e3)))}this.console.log("exiting prebuffer session (released or restarted with new configuration)")})()}}Pe.onMixinEvent(this.id,this.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0)}async getMixinSettings(){const e=[];try{const t=await this.mixinDevice.getVideoStreamOptions(),i=this.getEnabledMediaStreamOptions(t);t?.length>0&&e.push({title:"Prebuffered Streams",description:"The streams to prebuffer. Enable only as necessary to reduce traffic.",key:"enabledStreams",value:i.map((e=>e.name||"")),choices:t.map((e=>e.name)),multiple:!0})}catch(e){throw this.console.error("error in getVideoStreamOptions",e),e}e.push({title:"Prebuffer Duration",description:"Duration of the prebuffer in milliseconds.",type:"number",key:Me,value:this.storage.getItem(Me)||Ie.toString()},{title:"Start at Previous Keyframe",description:"Start live streams from the previous key frame. Improves startup time.",type:"boolean",key:xe,value:("false"!==this.storage.getItem(xe)).toString()});for(const t of new Set([...this.sessions.values()]))if(t)try{e.push(...await t.getMixinSettings())}catch(e){throw this.console.error("error in prebuffer session getMixinSettings",e),e}return e}async putMixinSetting(e,t){const i=this.sessions;this.sessions=new Map,"enabledStreams"===e?this.storage.setItem(e,JSON.stringify(t)):this.storage.setItem(e,t.toString());for(const e of i.values())e?.parserSessionPromise?.then((e=>e.kill()));this.ensurePrebufferSessions()}getEnabledMediaStreamOptions(e){if(!e)return;try{const t=JSON.parse(this.storage.getItem("enabledStreams"));return e.filter((e=>t.includes(e.name)))}catch(e){}const t=e.find((e=>"cloud"!==e.source));return t?[t]:[]}async getVideoStreamOptions(){const e=await this.mixinDevice.getVideoStreamOptions()||[];let t=this.getEnabledMediaStreamOptions(e);const i=parseInt(this.storage.getItem(Me))||Ie;for(const r of e)(this.sessions.get(r.id)?.parserSession||t.includes(r))&&(r.prebuffer=i);return e}async release(){this.console.log("prebuffer session releasing if started"),this.released=!0;for(const e of this.sessions.values())e&&(e.clearPrebuffers(),e.parserSessionPromise?.then((t=>{this.console.log("prebuffer session released"),t.kill(),e.clearPrebuffers()})))}}const $e=new class extends L{storageSettings=new fe(this,{rebroadcastPort:{title:"Rebroadcast Port",description:"The port of the RTSP server that will rebroadcast your streams.",type:"number"}});currentMixins=new Map;constructor(t){super(t),this.fromMimeType="x-scrypted/x-rfc4571",this.toMimeType=e.ScryptedMimeTypes.FFmpegInput,process.nextTick((()=>{for(const e of Object.keys(we.getSystemState())){const t=we.getDeviceById(e);if(t.mixins?.includes(this.id))try{t.getVideoStreamOptions()}catch(e){this.console.error("error triggering prebuffer",t.name,e)}}}));const i=function(){var e=new Date;return e.setHours(24),e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0),e.getTime()-(new Date).getTime()}()+72e5;this.log.i(`Rebroadcaster scheduled for restart at 2AM: ${Math.round(i/1e3/60)} minutes`),setTimeout((()=>Pe.requestRestart()),i),this.startRtspServer()}startRtspServer(){!async function(e){if(e)try{await new Promise((t=>e.close(t)))}catch(e){}}(this.rtspServer),this.rtspServer=new(c().Server)((async e=>{let t;const i=new he(e,void 0,void 0,(async(e,r,s,n)=>{i.checkRequest=void 0;const o=new URL(r);for(const e of this.currentMixins.keys()){const r=this.currentMixins.get(e);for(const e of r.sessions.values())if(o.pathname.endsWith(e.rtspServerPath))return i.console=e.console,i.sdp=await e.sdp,t=e,!0}return!1}));this.console.log("RTSP Rebroadcast connection started."),i.console=this.console;try{await i.handlePlayback(),t.ensurePrebufferSession();const r=await t.parserSessionPromise;t.handleRebroadcasterClient({isActiveClient:!0,container:"rtsp",session:r,socketPromise:Promise.resolve(e),requestedPrebuffer:0}),await i.handleTeardown()}catch(t){e.destroy()}this.console.log("RTSP Rebroadcast connection finished.")})),this.storageSettings.values.rebroadcastPort||(this.storageSettings.values.rebroadcastPort=Math.round(1e4*Math.random()+3e4)),this.rtspServer.listen(this.storageSettings.values.rebroadcastPort)}async convert(e,t,i){const r=JSON.parse(e.toString()),{url:s,sdp:n}=r,{audioPayloadTypes:o,videoPayloadTypes:a}=function(e){const t=new Set,i=new Set,r=(e,t)=>{for(const i of t||[])e.add(parseInt(i))},s=e.match(/m=audio.*/)?.[0];r(t,s?.split(" ").slice(3));const n=e.match(/m=video.*/)?.[0];return r(i,n?.split(" ").slice(3)),{audioPayloadTypes:t,videoPayloadTypes:i}}(n),d=new URL(s);if(!d.protocol.startsWith("tcp"))throw new Error("rfc4751 url must be tcp");const{clientPromise:u,url:p}=await g(),l={url:p,inputArguments:["-rtsp_transport","tcp","-i",p.replace("tcp","rtsp")]};return u.then((async e=>{const t=new he(e,n);await t.handlePlayback();const i=c().connect(parseInt(d.port),d.hostname);for(e.on("close",(()=>{i.destroy()})),i.on("close",(()=>{e.destroy()}));;){const r=(await O(i,2)).readInt16BE(0),s=await O(i,r),n=127&s[1];if(o.has(n))t.sendAudio(s,!1);else{if(!a.has(n))throw e.destroy(),i.destroy(),new Error("unknown payload type "+n);t.sendVideo(s,!1)}}})),Buffer.from(JSON.stringify(l))}async canMixin(t,i){return i.includes(e.ScryptedInterface.VideoCamera)?[e.ScryptedInterface.VideoCamera,e.ScryptedInterface.Settings,e.ScryptedInterface.Online]:null}async getMixin(e,t,i){this.setHasEnabledMixin(i.id);const r=new Ne(this,{mixinDevice:e,mixinDeviceState:i,mixinProviderNativeId:this.nativeId,mixinDeviceInterfaces:t,group:"Prebuffer Settings",groupKey:"prebuffer"});return this.currentMixins.set(i.id,r),r}async releaseMixin(e,t){t.online=!0,await t.release(),this.currentMixins.delete(e)}}})();var s=exports="undefined"==typeof exports?{}:exports;for(var n in r)s[n]=r[n];r.__esModule&&Object.defineProperty(s,"__esModule",{value:!0})})();
|
|
2
2
|
//# sourceMappingURL=main.nodejs.js.map
|
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -7,15 +7,17 @@ import { handleRebroadcasterClient, ParserOptions, ParserSession, setupActivityT
|
|
|
7
7
|
import { createMpegTsParser, createFragmentedMp4Parser, StreamChunk, StreamParser, MP4Atom, parseMp4StreamChunks } from '@scrypted/common/src/stream-parser';
|
|
8
8
|
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
|
9
9
|
import { startFFMPegFragmentedMP4Session } from '@scrypted/common/src/ffmpeg-mp4-parser-session';
|
|
10
|
-
import { listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
|
10
|
+
import { closeQuiet, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
|
11
11
|
import { parsePayloadTypes, parseTrackIds } from '@scrypted/common/src/sdp-utils';
|
|
12
12
|
import { createRtspParser, RtspClient, RtspServer } from '@scrypted/common/src/rtsp-server';
|
|
13
13
|
import { Duplex } from 'stream';
|
|
14
14
|
import net from 'net';
|
|
15
15
|
import { readLength } from '@scrypted/common/src/read-stream';
|
|
16
|
+
import { StorageSettings } from '@scrypted/common/src/settings';
|
|
16
17
|
import { addTrackControls } from '@scrypted/common/src/sdp-utils';
|
|
17
18
|
import { connectRFC4571Parser, startRFC4571Parser } from './rfc4571';
|
|
18
19
|
import { sleep } from '@scrypted/common/src/sleep';
|
|
20
|
+
import crypto from 'crypto';
|
|
19
21
|
|
|
20
22
|
const { mediaManager, log, systemManager, deviceManager } = sdk;
|
|
21
23
|
|
|
@@ -83,6 +85,7 @@ class PrebufferSession {
|
|
|
83
85
|
lastDetectedAudioCodecKey: string;
|
|
84
86
|
rebroadcastModeKey: string;
|
|
85
87
|
rtspParserKey: string;
|
|
88
|
+
rtspServerPath: string;
|
|
86
89
|
|
|
87
90
|
constructor(public mixin: PrebufferMixin, public advertisedMediaStreamOptions: MediaStreamOptions, public stopInactive: boolean) {
|
|
88
91
|
this.storage = mixin.storage;
|
|
@@ -93,6 +96,13 @@ class PrebufferSession {
|
|
|
93
96
|
this.rebroadcastModeKey = 'rebroadcastMode-' + this.streamId;
|
|
94
97
|
this.lastDetectedAudioCodecKey = 'lastDetectedAudioCodec-' + this.streamId;
|
|
95
98
|
this.rtspParserKey = 'rtspParser-' + this.streamId;
|
|
99
|
+
const rtspServerPathKey = 'rtspServerPathKey-' + this.streamId;
|
|
100
|
+
|
|
101
|
+
this.rtspServerPath = this.storage.getItem(rtspServerPathKey);
|
|
102
|
+
if (!this.rtspServerPath) {
|
|
103
|
+
this.rtspServerPath = crypto.randomBytes(8).toString('hex');
|
|
104
|
+
this.storage.setItem(rtspServerPathKey, this.rtspServerPath);
|
|
105
|
+
}
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
get streamId() {
|
|
@@ -318,6 +328,17 @@ class PrebufferSession {
|
|
|
318
328
|
)
|
|
319
329
|
}
|
|
320
330
|
|
|
331
|
+
if (rtspMode) {
|
|
332
|
+
settings.push({
|
|
333
|
+
group,
|
|
334
|
+
key: 'rtspRebroadcastUrl',
|
|
335
|
+
title: 'RTSP Rebroadcast Url',
|
|
336
|
+
description: 'The RTSP URL of the rebroadcast stream. Substitute localhost as appropriate.',
|
|
337
|
+
readonly: true,
|
|
338
|
+
value: `rtsp://localhost:${this.mixin.plugin.storageSettings.values.rebroadcastPort}/${this.rtspServerPath}`,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
321
342
|
return settings;
|
|
322
343
|
}
|
|
323
344
|
|
|
@@ -755,6 +776,77 @@ class PrebufferSession {
|
|
|
755
776
|
}, 30000);
|
|
756
777
|
}
|
|
757
778
|
|
|
779
|
+
async handleRebroadcasterClient(options: {
|
|
780
|
+
isActiveClient: boolean,
|
|
781
|
+
container: PrebufferParsers,
|
|
782
|
+
session: ParserSession<PrebufferParsers>,
|
|
783
|
+
socketPromise: Promise<Duplex>,
|
|
784
|
+
requestedPrebuffer: number,
|
|
785
|
+
}) {
|
|
786
|
+
const { isActiveClient, container, session, socketPromise, requestedPrebuffer } = options;
|
|
787
|
+
|
|
788
|
+
handleRebroadcasterClient(socketPromise, {
|
|
789
|
+
// console: this.console,
|
|
790
|
+
connect: (writeData, destroy) => {
|
|
791
|
+
if (isActiveClient) {
|
|
792
|
+
this.activeClients++;
|
|
793
|
+
this.printActiveClients();
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
// this.console.log('passive client request started');
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const now = Date.now();
|
|
800
|
+
|
|
801
|
+
const safeWriteData = (chunk: StreamChunk) => {
|
|
802
|
+
const buffered = writeData(chunk);
|
|
803
|
+
if (buffered > 100000000) {
|
|
804
|
+
this.console.log('more than 100MB has been buffered, did downstream die? killing connection.', this.streamName);
|
|
805
|
+
cleanup();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const cleanup = () => {
|
|
810
|
+
destroy();
|
|
811
|
+
session.removeListener(container, safeWriteData);
|
|
812
|
+
session.removeListener('killed', cleanup);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
session.on(container, safeWriteData);
|
|
816
|
+
session.once('killed', cleanup);
|
|
817
|
+
|
|
818
|
+
const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
|
|
819
|
+
if (true) {
|
|
820
|
+
for (const prebuffer of prebufferContainer) {
|
|
821
|
+
if (prebuffer.time < now - requestedPrebuffer)
|
|
822
|
+
continue;
|
|
823
|
+
|
|
824
|
+
safeWriteData(prebuffer.chunk);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
// for some reason this doesn't work as well as simply guessing and dumping.
|
|
829
|
+
const parser = this.parsers[container];
|
|
830
|
+
const availablePrebuffers = parser.findSyncFrame(prebufferContainer.filter(pb => pb.time >= now - requestedPrebuffer).map(pb => pb.chunk));
|
|
831
|
+
for (const prebuffer of availablePrebuffers) {
|
|
832
|
+
safeWriteData(prebuffer);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return () => {
|
|
837
|
+
if (isActiveClient) {
|
|
838
|
+
this.activeClients--;
|
|
839
|
+
this.inactivityCheck(session);
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
// this.console.log('passive client request ended');
|
|
843
|
+
}
|
|
844
|
+
cleanup();
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
})
|
|
848
|
+
}
|
|
849
|
+
|
|
758
850
|
async getVideoStream(options?: RequestMediaStreamOptions): Promise<MediaObject> {
|
|
759
851
|
this.ensurePrebufferSession();
|
|
760
852
|
|
|
@@ -784,7 +876,6 @@ class PrebufferSession {
|
|
|
784
876
|
}
|
|
785
877
|
|
|
786
878
|
const createContainerServer = async (container: PrebufferParsers) => {
|
|
787
|
-
const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
|
|
788
879
|
|
|
789
880
|
let socketPromise: Promise<Duplex>;
|
|
790
881
|
let containerUrl: string;
|
|
@@ -809,65 +900,13 @@ class PrebufferSession {
|
|
|
809
900
|
|
|
810
901
|
const isActiveClient = options?.refresh !== false;
|
|
811
902
|
|
|
812
|
-
handleRebroadcasterClient(
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
else {
|
|
820
|
-
// this.console.log('passive client request started');
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const now = Date.now();
|
|
824
|
-
|
|
825
|
-
const safeWriteData = (chunk: StreamChunk) => {
|
|
826
|
-
const buffered = writeData(chunk);
|
|
827
|
-
if (buffered > 100000000) {
|
|
828
|
-
this.console.log('more than 100MB has been buffered, did downstream die? killing connection.', this.streamName);
|
|
829
|
-
cleanup();
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const cleanup = () => {
|
|
834
|
-
destroy();
|
|
835
|
-
session.removeListener(container, safeWriteData);
|
|
836
|
-
session.removeListener('killed', cleanup);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
session.on(container, safeWriteData);
|
|
840
|
-
session.once('killed', cleanup);
|
|
841
|
-
|
|
842
|
-
if (true) {
|
|
843
|
-
for (const prebuffer of prebufferContainer) {
|
|
844
|
-
if (prebuffer.time < now - requestedPrebuffer)
|
|
845
|
-
continue;
|
|
846
|
-
|
|
847
|
-
safeWriteData(prebuffer.chunk);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
else {
|
|
851
|
-
// for some reason this doesn't work as well as simply guessing and dumping.
|
|
852
|
-
const parser = this.parsers[container];
|
|
853
|
-
const availablePrebuffers = parser.findSyncFrame(prebufferContainer.filter(pb => pb.time >= now - requestedPrebuffer).map(pb => pb.chunk));
|
|
854
|
-
for (const prebuffer of availablePrebuffers) {
|
|
855
|
-
safeWriteData(prebuffer);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
return () => {
|
|
860
|
-
if (isActiveClient) {
|
|
861
|
-
this.activeClients--;
|
|
862
|
-
this.inactivityCheck(session);
|
|
863
|
-
}
|
|
864
|
-
else {
|
|
865
|
-
// this.console.log('passive client request ended');
|
|
866
|
-
}
|
|
867
|
-
cleanup();
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
})
|
|
903
|
+
this.handleRebroadcasterClient({
|
|
904
|
+
isActiveClient,
|
|
905
|
+
container,
|
|
906
|
+
requestedPrebuffer,
|
|
907
|
+
socketPromise,
|
|
908
|
+
session,
|
|
909
|
+
});
|
|
871
910
|
|
|
872
911
|
return containerUrl;
|
|
873
912
|
}
|
|
@@ -948,7 +987,8 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
|
|
948
987
|
released = false;
|
|
949
988
|
sessions = new Map<string, PrebufferSession>();
|
|
950
989
|
|
|
951
|
-
|
|
990
|
+
|
|
991
|
+
constructor(public plugin: PrebufferProvider, options: SettingsMixinDeviceOptions<VideoCamera>) {
|
|
952
992
|
super(options);
|
|
953
993
|
|
|
954
994
|
this.delayStart();
|
|
@@ -1026,7 +1066,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
|
|
1026
1066
|
const ps = await session.parserSessionPromise;
|
|
1027
1067
|
active++;
|
|
1028
1068
|
wasActive = true;
|
|
1029
|
-
this.online = active
|
|
1069
|
+
this.online = !!active;
|
|
1030
1070
|
await once(ps, 'killed');
|
|
1031
1071
|
this.console.error('prebuffer session ended');
|
|
1032
1072
|
}
|
|
@@ -1037,7 +1077,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
|
|
1037
1077
|
if (wasActive)
|
|
1038
1078
|
active--;
|
|
1039
1079
|
wasActive = false;
|
|
1040
|
-
this.online = active
|
|
1080
|
+
this.online = !!active;
|
|
1041
1081
|
}
|
|
1042
1082
|
this.console.log('restarting prebuffer session in 5 seconds');
|
|
1043
1083
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
@@ -1178,6 +1218,16 @@ function millisUntilMidnight() {
|
|
|
1178
1218
|
}
|
|
1179
1219
|
|
|
1180
1220
|
class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider, BufferConverter {
|
|
1221
|
+
storageSettings = new StorageSettings(this, {
|
|
1222
|
+
rebroadcastPort: {
|
|
1223
|
+
title: 'Rebroadcast Port',
|
|
1224
|
+
description: 'The port of the RTSP server that will rebroadcast your streams.',
|
|
1225
|
+
type: 'number',
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
rtspServer: net.Server;
|
|
1229
|
+
currentMixins = new Map<string, PrebufferMixin>();
|
|
1230
|
+
|
|
1181
1231
|
constructor(nativeId?: string) {
|
|
1182
1232
|
super(nativeId);
|
|
1183
1233
|
|
|
@@ -1205,6 +1255,64 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
|
|
|
1205
1255
|
const twoAM = midnight + 2 * 60 * 60 * 1000;
|
|
1206
1256
|
this.log.i(`Rebroadcaster scheduled for restart at 2AM: ${Math.round(twoAM / 1000 / 60)} minutes`)
|
|
1207
1257
|
setTimeout(() => deviceManager.requestRestart(), twoAM);
|
|
1258
|
+
|
|
1259
|
+
this.startRtspServer();
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
startRtspServer() {
|
|
1263
|
+
closeQuiet(this.rtspServer);
|
|
1264
|
+
|
|
1265
|
+
this.rtspServer = new net.Server(async (client) => {
|
|
1266
|
+
let prebufferSession: PrebufferSession;
|
|
1267
|
+
|
|
1268
|
+
const server = new RtspServer(client, undefined, undefined, async (method, url, headers, rawMessage) => {
|
|
1269
|
+
server.checkRequest = undefined;
|
|
1270
|
+
|
|
1271
|
+
const u = new URL(url);
|
|
1272
|
+
|
|
1273
|
+
for (const id of this.currentMixins.keys()) {
|
|
1274
|
+
const mixin = this.currentMixins.get(id);
|
|
1275
|
+
for (const session of mixin.sessions.values()) {
|
|
1276
|
+
if (u.pathname.endsWith(session.rtspServerPath)) {
|
|
1277
|
+
server.console = session.console;
|
|
1278
|
+
server.sdp = await session.sdp;
|
|
1279
|
+
prebufferSession = session;
|
|
1280
|
+
return true;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
return false;
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
this.console.log('RTSP Rebroadcast connection started.')
|
|
1289
|
+
server.console = this.console;
|
|
1290
|
+
|
|
1291
|
+
try {
|
|
1292
|
+
await server.handlePlayback();
|
|
1293
|
+
prebufferSession.ensurePrebufferSession();
|
|
1294
|
+
const session = await prebufferSession.parserSessionPromise;
|
|
1295
|
+
|
|
1296
|
+
prebufferSession.handleRebroadcasterClient({
|
|
1297
|
+
isActiveClient: true,
|
|
1298
|
+
container: 'rtsp',
|
|
1299
|
+
session,
|
|
1300
|
+
socketPromise: Promise.resolve(client),
|
|
1301
|
+
requestedPrebuffer: 0,
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
await server.handleTeardown();
|
|
1305
|
+
}
|
|
1306
|
+
catch (e) {
|
|
1307
|
+
client.destroy();
|
|
1308
|
+
}
|
|
1309
|
+
this.console.log('RTSP Rebroadcast connection finished.')
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
if (!this.storageSettings.values.rebroadcastPort)
|
|
1313
|
+
this.storageSettings.values.rebroadcastPort = Math.round(Math.random() * 10000 + 30000);
|
|
1314
|
+
|
|
1315
|
+
this.rtspServer.listen(this.storageSettings.values.rebroadcastPort);
|
|
1208
1316
|
}
|
|
1209
1317
|
|
|
1210
1318
|
async convert(data: Buffer, fromMimeType: string, toMimeType: string): Promise<Buffer> {
|
|
@@ -1268,7 +1376,7 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
|
|
|
1268
1376
|
|
|
1269
1377
|
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }) {
|
|
1270
1378
|
this.setHasEnabledMixin(mixinDeviceState.id);
|
|
1271
|
-
|
|
1379
|
+
const ret = new PrebufferMixin(this, {
|
|
1272
1380
|
mixinDevice,
|
|
1273
1381
|
mixinDeviceState,
|
|
1274
1382
|
mixinProviderNativeId: this.nativeId,
|
|
@@ -1276,10 +1384,14 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
|
|
|
1276
1384
|
group: "Prebuffer Settings",
|
|
1277
1385
|
groupKey: "prebuffer",
|
|
1278
1386
|
});
|
|
1387
|
+
this.currentMixins.set(mixinDeviceState.id, ret);
|
|
1388
|
+
return ret;
|
|
1279
1389
|
}
|
|
1390
|
+
|
|
1280
1391
|
async releaseMixin(id: string, mixinDevice: any) {
|
|
1281
1392
|
mixinDevice.online = true;
|
|
1282
|
-
mixinDevice.release();
|
|
1393
|
+
await mixinDevice.release();
|
|
1394
|
+
this.currentMixins.delete(id);
|
|
1283
1395
|
}
|
|
1284
1396
|
}
|
|
1285
1397
|
|