@scrypted/prebuffer-mixin 0.1.219 → 0.1.222
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 +84 -108
- package/src/rfc4571.ts +3 -3
- package/src/stream-settings.ts +157 -0
package/dist/main.nodejs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(()=>{var e={747:(e,t,i)=>{"use strict";Object.defineProperty(t,"rh",{enumerable:!0,get:function(){return s.default}}),Object.defineProperty(t,"Nu",{enumerable:!0,get:function(){return n.default}});var r=a(i(945)),s=a(i(816)),n=a(i(243)),o=a(i(828));function a(e){return e&&e.__esModule?e:{default:e}}const c=[s.default,n.default,o.default];s.default,n.default,o.default},816:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,s=(r=i(945))&&r.__esModule?r:{default:r},n=i(761);const o=["realm"],a=o,c={type:"Basic",parseWWWAuthenticateRest:function(e){return(0,n.parseHTTPHeadersQuotedKeyValueSet)(e,a,o)},buildWWWAuthenticateRest:function(e){return(0,n.buildHTTPHeadersQuotedKeyValueSet)(e,a,o)},parseAuthorizationRest:function(e){if(!e)throw new s.default("E_EMPTY_AUTH");const{username:t,password:i}=c.decodeHash(e);return{hash:e,username:t,password:i}},buildAuthorizationRest:function({hash:e,username:t,password:i}){if(t&&i)return c.computeHash({username:t,password:i});if(!e)throw new s.default("E_NO_HASH");return e},computeHash:function({username:e,password:t}){return Buffer.from(e+":"+t).toString("base64")},decodeHash:function(e){const[t,...i]=Buffer.from(e,"base64").toString().split(":");return{username:t,password:i.join(":")}}};var d=c;t.default=d},828:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,s=(r=i(945))&&r.__esModule?r:{default:r},n=i(761);const o=["invalid_request","invalid_token","insufficient_scope"],a=["realm","scope","error","error_description"];var c={type:"Bearer",parseWWWAuthenticateRest:function(e){return(0,n.parseHTTPHeadersQuotedKeyValueSet)(e,a,[])},buildWWWAuthenticateRest:function(e){if(e.error&&-1===o.indexOf(e.error))throw new s.default("E_INVALID_ERROR",e.error,o);return(0,n.buildHTTPHeadersQuotedKeyValueSet)(e,a,[])},parseAuthorizationRest:function(e){if(!e)throw new s.default("E_EMPTY_AUTH");return{hash:e}},buildAuthorizationRest:function({hash:e}){if(!e)throw new s.default("E_NO_HASH");return e}};t.default=c},243:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,s=i(761),n=(r=i(113))&&r.__esModule?r:{default:r};const o=["realm","nonce"],a=[...o,"domain","opaque","stale","algorithm","qop"],c=["username","realm","nonce","uri","response"],d=[...c,"algorithm","cnonce","opaque","qop","nc"];function u(e,t){const i=n.default.createHash(e);return i.update(t),i.digest("hex")}var l={type:"Digest",parseWWWAuthenticateRest:function(e){return(0,s.parseHTTPHeadersQuotedKeyValueSet)(e,a,o)},buildWWWAuthenticateRest:function(e){return(0,s.buildHTTPHeadersQuotedKeyValueSet)(e,a,o)},parseAuthorizationRest:function(e){return(0,s.parseHTTPHeadersQuotedKeyValueSet)(e,d,c)},buildAuthorizationRest:function(e){return(0,s.buildHTTPHeadersQuotedKeyValueSet)(e,d,c)},computeHash:function(e){const t=e.ha1||u(e.algorithm,[e.username,e.realm,e.password].join(":")),i=u(e.algorithm,[e.method,e.uri].join(":"));return u(e.algorithm,[t,e.nonce,e.nc,e.cnonce,e.qop,i].join(":"))}};t.default=l},761:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.parseHTTPHeadersQuotedKeyValueSet=function(e,t,i=[]){const r=e.trim().match(n);if(!r)throw new s.default("E_MALFORMED_QUOTEDKEYVALUE",e);const a=r.map(((e,t)=>{const i=e.split("=");if(2!==i.length)throw new s.default("E_MALFORMED_QUOTEDKEYVALUE",t,e,i.length);return i})).reduce((function(e,[i,r],n){if(-1===t.indexOf(i))throw new s.default("E_UNAUTHORIZED_KEY",n,i);return e[i]=r.replace(/^"(.+(?="$))"$/,"$1"),e}),{});return o(i,a),a},t.buildHTTPHeadersQuotedKeyValueSet=function(e,t,i=[]){return o(i,e),t.reduce((function(t,i){return e[i]?t+(t?", ":"")+i+'="'+e[i]+'"':t}),"")};var r,s=(r=i(945))&&r.__esModule?r:{default:r};const n=/\w+=(".*?"|[^",]+)(?=,|$)/g;function o(e,t){e.forEach((e=>{if(void 0===t[e])throw new s.default("E_REQUIRED_KEY",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:()=>l,MediaPlayerState:()=>p,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:[]},Microphone:{name:"Microphone",methods:["getAudioStream"],properties:[]},Display:{name:"Display",methods:["startDisplay","stopDisplay"],properties:[]},VideoCamera:{name:"VideoCamera",methods:["getVideoStream","getVideoStreamOptions"],properties:[]},VideoRecorder:{name:"VideoRecorder",methods:["getRecordingStream","getRecordingStreamOptions","getRecordingStreamThumbnail"],properties:[]},VideoClips:{name:"VideoClips",methods:["getVideoClip","getVideoClipThumbnail","getVideoClips","removeVideoClips"],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,l,p,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.SmartDisplay="SmartDisplay",e.Speaker="Speaker",e.SmartSpeaker="SmartSpeaker",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"}(l||(l={})),function(e){e.Idle="Idle",e.Playing="Playing",e.Paused="Paused",e.Buffering="Buffering"}(p||(p={})),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.Microphone="Microphone",e.Display="Display",e.VideoCamera="VideoCamera",e.VideoRecorder="VideoRecorder",e.VideoClips="VideoClips",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.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={}))},113:e=>{"use strict";e.exports=require("crypto")},945:(e,t,i)=>{"use strict";i.r(t),i.d(t,{default:()=>m});const r=require("os");function s(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 n(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function o(e){var t="function"==typeof Map?new Map:void 0;return o=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 a(e,arguments,u(this).constructor)}return r.prototype=Object.create(e.prototype,{constructor:{value:r,enumerable:!1,writable:!0,configurable:!0}}),d(r,e)},o(e)}function a(e,t,i){return a=c()?Reflect.construct:function(e,t,i){var r=[null];r.push.apply(r,t);var s=new(Function.bind.apply(e,r));return i&&d(s,i.prototype),s},a.apply(null,arguments)}function c(){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 d(e,t){return d=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},d(e,t)}function u(e){return u=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},u(e)}let l=function(e){function t(e,i,...r){var s,o,a;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=[]),o=this,(s=!(a=u(t).call(this,i))||"object"!=typeof a&&"function"!=typeof a?n(o):a).code=i||"E_UNEXPECTED",s.params=r,s.wrappedErrors=e,s.name=s.toString(),Error.captureStackTrace&&Error.captureStackTrace(n(s),s.constructor),s}var i,o,a;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&&d(e,t)}(t,e),i=t,(o=[{key:"toString",value:function(){return(this.wrappedErrors.length?this.wrappedErrors[this.wrappedErrors.length-1].stack+r.EOL:"")+this.constructor.name+": "+this.code+" ("+this.params.join(", ")+")"}}])&&s(i.prototype,o),a&&s(i,a),t}(o(Error));function p(e){return e instanceof l||e.constructor&&e.constructor.name&&e.constructor.name.endsWith("Error")&&"string"==typeof e.code&&h(e.code)&&e.params&&e.params instanceof Array}function h(e){return/^([A-Z0-9_]+)$/.test(e)}l.wrap=function(e,t,...i){let r=null;const s=h(e.message),n=(e.wrappedErrors||[]).concat(e);return t||(t=s?e.message:"E_UNEXPECTED"),e.message&&!s&&i.push(e.message),r=new l(n,t,...i),r},l.cast=function(e,...t){return p(e)?e:l.wrap.apply(l,[e].concat(t))},l.bump=function(e,...t){return p(e)?l.wrap.apply(l,[e,e.code].concat(e.params)):l.wrap.apply(l,[e].concat(t))};const m=l}},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:()=>we});var e=i(510),t=i.n(e);const{deviceManager:s}=t();class n 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 o=require("net");var a=i.n(o);const c=require("child_process");var d=i.n(c);const u=require("events"),l=require("dgram");var p=i.n(l);async function h(e,t){e.bind(t),await(0,u.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,u.once)(e,"listening"),{port:t,url:`udp://127.0.0.1:${t}`}}async function g(){const e=new(a().Server),t=await async function(e){return e.listen(0),await(0,u.once)(e,"listening"),e.address().port}(e),i=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)}))}));return{server:e,url:`tcp://127.0.0.1:${t}`,port:t,clientPromise:i}}const y=require("process");var v=i.n(y);const S=["decode_slice_header error","no frame!","non-existing PPS"];function b(e){e&&(e.stdin.write("q\n"),setTimeout((()=>{e.kill(),setTimeout((()=>{e.kill("SIGKILL")}),2e3)}),2e3))}function w(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 P(e,t){const i=[];let r=!1;for(const e of t){try{if(r){const t=new URL(e);i.push(`${t.protocol}[REDACTED]`)}else i.push(e)}catch(t){i.push(e)}r="-i"===e}e.log(i.join(" "))}const{mediaManager:I}=t();async function M(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 x(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 C(e,t){const{console:i}=t;let r,s=!0;const n=new u.EventEmitter;let o,a,c,l,h;n.on("error",(e=>i.error("rebroadcast error",e)));const y=new Promise(((e,t)=>{l=e,h=t}));let v;const S=new Promise((e=>{v=e}));function C(){s&&(n.emit("killed"),n.emit("error",new Error("killed"))),s=!1,clearTimeout(r),v(),h?.(new Error("ffmpeg was killed before connecting to the rebroadcast session")),b(E)}const O=e.inputArguments.slice();r=setTimeout(C,3e4);let T=!1;const D=e=>{if(!s)throw e(),new Error("parser session was killed killed before ffmpeg connected");n.on("killed",e)},A=["pipe","pipe","pipe"];let k=3;for(const e of Object.keys(t.parsers)){const r=t.parsers[e];if(r.parseDatagram){T=!0;const i=p().createSocket("udp4"),s=await m(i),o=p().createSocket("udp4");await f(o,s.port+1),D((()=>{i.close(),o.close()})),O.push(...r.outputArguments,s.url.replace("udp://","rtp://"));const{resetActivityTimer:a}=x(e,C,n,t?.timeout);(async()=>{for await(const t of r.parseDatagram(i,parseInt(c?.[2]),parseInt(c?.[3])))l?.(void 0),n.emit(e,t),a()})(),(async()=>{for await(const t of r.parseDatagram(o,parseInt(c?.[2]),parseInt(c?.[3]),"rtcp"))l?.(void 0),n.emit(e,t),a()})()}else if(r.tcpProtocol){const s=await g(),o=new URL(r.tcpProtocol);o.port=s.port.toString(),O.push(...r.outputArguments,o.toString());const{resetActivityTimer:a}=x(e,C,n,t?.timeout);(async()=>{const t=await s.clientPromise;try{D((()=>t.destroy()));for await(const i of r.parse(t,parseInt(c?.[2]),parseInt(c?.[3])))l?.(void 0),n.emit(e,i),a()}catch(e){i.error("rebroadcast parse error",e),C()}})()}else O.push(...r.outputArguments,"pipe:"+k++),A.push("pipe")}T&&(O.push("-sdp_file","pipe:"+k++),A.push("pipe")),O.unshift("-hide_banner"),P(i,O);const E=d().spawn(await I.getFFmpegPath(),O,{stdio:A});let R;w(i,E,void 0,t?.storage),E.on("exit",C),R=T?new Promise((e=>{const t=[];E.stdio[k-1].on("data",(i=>{t.push(i),e(t)}))})):Promise.resolve([]);let _=0;return Object.keys(t.parsers).forEach((async e=>{const r=t.parsers[e];if(!r.parse||r.tcpProtocol)return;const s=E.stdio[3+_];_++;try{const{resetActivityTimer:i}=x(e,C,n,t?.timeout);for await(const t of r.parse(s,parseInt(c?.[2]),parseInt(c?.[3])))l?.(void 0),n.emit(e,t),i()}catch(e){i.error("rebroadcast parse error",e),C()}})),async function(e){return M(e,"Audio")}(E).then((e=>o=e)),async function(e){return M(e,"Video")}(E).then((e=>a=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)}))}(E).then((e=>c=e)),await y,l=void 0,h=void 0,clearTimeout(r),{sdp:R,inputAudioCodec:o,inputVideoCodec:a,inputVideoResolution:c,get isActive(){return s},kill:C,killed:S,mediaStreamOptions:e.mediaStreamOptions||{id:void 0,name:void 0},emit(e,t){return n.emit(e,t),this},on(e,t){return n.on(e,t),this},once(e,t){return n.once(e,t),this},removeListener(e,t){return n.removeListener(e,t),this}}}async function O(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 T(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 A(e){return async function(e,t){const i=[];let r=0;for(;;){const s=await T(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 k(e){return e}async function*E(e){for(;;){const t=await T(e,8),i=t.readInt32BE(0)-8,r=t.slice(4).toString(),s=await T(e,i);yield{header:t,length:i,type:r,data:s}}}async function*R(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:_}=t(),B="v4";class V extends e.ScryptedDeviceBase{hasEnabledMixin={};unshiftMixin=!1;constructor(t){super(t);try{this.hasEnabledMixin=JSON.parse(this.storage.getItem("hasEnabledMixin"))}catch(e){this.hasEnabledMixin={}}this.pluginsComponent=_.getComponent("plugins"),_.listen((async(t,i,r)=>{i.eventInterface!==e.ScryptedInterface.ScryptedDevice||i.property||this.maybeEnableMixin(t)}));for(const e of Object.keys(_.getSystemState())){const t=_.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]===B)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();this.unshiftMixin?t.unshift(this.id):t.push(this.id);const i=await this.pluginsComponent;await i.setMixins(e.id,t),this.setHasEnabledMixin(e.id)}setHasEnabledMixin(e){this.hasEnabledMixin[e]!==B&&(this.hasEnabledMixin[e]=B,this.storage.setItem("hasEnabledMixin",JSON.stringify(this.hasEnabledMixin)))}}const{mediaManager:L}=t();function H(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")}function U(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("stream");var K=i(113),N=i.n(K);const $=require("tls");var q=i.n($),F=i(747);class W extends Error{constructor(){super("Operation Timed Out")}}function G(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 z 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 outgoing message\n",r),this.console?.log(),i&&this.client.write(i)}async readMessage(){const e=await async function(e){let t=[];for(;;){let i=await A(e);if(i=i.trim(),!i)return t;t.push(i)}}(this.client);return this.console?.log("rtsp incoming message\n",e.join("\n")),this.console?.log(),e}}{cseq=0;rfc4571=new j.PassThrough;needKeepAlive=!1;constructor(e,t){super(t),this.url=e;const i=new URL(e),r=parseInt(i.port)||554;e.startsWith("rtsps")?this.client=q().connect({rejectUnauthorized:!1,port:r,host:i.hostname}):this.client=a().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 handleDataPayload(e){if(36!==e[0])throw new Error("RTSP Client expected frame magic but received: "+e.toString());const t=e.readUInt16BE(2),i=await T(this.client,t);this.rfc4571.push(e),this.rfc4571.push(i)}async readDataPayload(){const e=await T(this.client,4);return this.handleDataPayload(e)}async readLoop(){try{for(;;)this.needKeepAlive&&(this.needKeepAlive=!1,await this.getParameter()),await this.readDataPayload()}catch(e){throw this.client.destroy(e),this.rfc4571.destroy(e),e}}async readMessage(){for(;;){const e=await T(this.client,4);if("RTSP"===e.toString()){this.client.unshift(e);const t=await super.readMessage();return this.console?.log("rtsp incoming message\n",t.join("\n")),this.console?.log(),t}await this.handleDataPayload(e)}}async request(e,t,i,r,s){this.writeRequest(e,t,i,r);const n=this.requestTimeout?await(o=this.requestTimeout,a=this.readMessage(),new Promise(((e,t)=>{setTimeout((()=>t(new W)),o),a.then(e),a.catch(t)}))):await this.readMessage();var o,a;const c=n[0],d=G(n);if(!c.includes("200")&&!d["www-authenticate"])throw new Error(c);const u=d["www-authenticate"];if(u){if(s)throw new Error("auth failed");const n=new URL(this.url);if(u.includes("Basic")){const e=F.rh.computeHash(n);this.authorization=`Basic ${e}`}else{const t=F.Nu.parseWWWAuthenticateRest(u),i=decodeURIComponent(n.username),r=decodeURIComponent(n.password),s=N().createHash("md5").update(`${i}:${t.realm}:${r}`).digest("hex"),o=N().createHash("md5").update(`${e}:${n.pathname}`).digest("hex"),a=N().createHash("md5").update(`${s}:${t.nonce}:${o}`).digest("hex"),c={username:i,realm:t.realm,nonce:t.nonce,uri:n.pathname,algorithm:"MD5",response:a},d=Object.entries(c).map((([e,t])=>{return`${e}=${t&&(i=t,`"${i.replace(/"/g,'\\"')}"`)}`;var i})).join(", ");this.authorization=`Digest ${d}`}return this.request(e,t,i,r,!0)}const l=parseInt(d["content-length"]);return l?{headers:d,body:await T(this.client,l)}:{headers:d,body:void 0}}async options(){return this.request("OPTIONS",{})}async getParameter(){return this.request("GET_PARAMETER")}async describe(e){return this.request("DESCRIBE",{...e||{},Accept:"application/sdp"})}async setup(e,t,i){const r={Transport:`RTP/AVP/${i?"UDP":"TCP"};unicast;${i?"client_port":"interleaved"}=${e}-${e+1}`},s=await this.request("SETUP",r,t);if(s.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}(s.headers.session);let t=parseInt(e.timeout);if(t){let e=setInterval((()=>this.needKeepAlive=!0),1e3*(t-5));this.client.once("close",(()=>clearInterval(e)))}this.session=s.headers.session.split(";")[0]}return s}async play(e="0.000"){const t={Range:`npt=${e}-`};return this.request("PLAY",t)}async pause(){return this.request("PAUSE")}async teardown(){try{return await this.request("TEARDOWN")}finally{this.client.destroy()}}}class J{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,K.randomBytes)(4).toString("hex"),t&&(t=t.trim())}async handleSetup(){let e=[];for(;;){let t=await A(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 T(this.client,4);if(36!==e[0])throw new Error("RTSP Server expected frame magic but received: "+e.toString());const t=e.readUInt16BE(2),i=await T(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=U(this.sdp,"audio"),n=U(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=U(this.sdp,"audio"),s=U(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 T(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=G(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:Q}=t();class Y{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)){let r=Object.assign({},i);r.onGet&&(r=Object.assign(r,await r.onGet())),r.hide||await(this.options?.hide?.[t]?.())||(r.key=t,r.value=this.getItem(t),e.push(r),delete r.onPut,delete r.onGet,delete r.mapPut,delete r.mapGet)}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=function(e,t){const{defaultValue:i}=t,r=t.multiple?"array":t.type;if("boolean"===r)return"true"===e||"false"!==e&&(i||!1);if("number"===r)return parseFloat(e)||i||0;if("integer"===r)return parseInt(e)||i||0;if("array"===r)try{return JSON.parse(e)}catch(e){return[]}if("device"===r)return Q.getDeviceById(e);if(e&&t.json)try{return JSON.parse(e)}catch(e){return i}return e||i}(this.device.storage.getItem(e),t);return t.mapGet?t.mapGet(i):i}}const{mediaManager:X}=t();async function Z(e,t,i,r,s,n){let o=!0;const a=new j.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]);let u;const l=new Promise((e=>{u=e})),p=()=>{o&&(a.emit("killed"),a.emit("error",new Error("killed"))),o=!1,u(),t.destroy()};let h,m;t.on("close",p),t.on("error",p),(async()=>{const{resetActivityTimer:e}=x("rtsp",p,a,n?.timeout);for(;;){let i,r;s?(i=await T(t,4),r=i.readUInt16BE(2)):(i=await T(t,2),r=i.readUInt16BE(0));const n=await T(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 l={chunks:[i,n],type:u};a.emit("rtsp",l),e()}})().finally(p);const f=U(i,"audio"),g=U(i,"video");if(f){const e=f.section.toLowerCase();e.includes("mpeg4")?h="aac":e.includes("pcm")&&(h="pcm")}return g&&g.section.toLowerCase().includes("h264")&&(m="h264"),{sdp:Promise.resolve([Buffer.from(i)]),inputAudioCodec:h,inputVideoCodec:m,inputVideoResolution:void 0,get isActive(){return o},kill:p,killed:l,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:ee,log:te,systemManager:ie,deviceManager:re}=t(),se=1e4,ne="prebufferDuration",oe="sendKeyframe",ae="Default",ce="AAC or No Audio",de=`${ce} (Copy)`,ue="Compatible Audio",le="Other Audio",pe=["aac","mp3","mp2","opus"],he="-fflags +genpts",me="Scrypted",fe="FFmpeg",ge="Default",ye=[ce,ue,le],ve=["mpegts","mp4","rtsp"];class Se{prebuffers={mp4:[],mpegts:[],rtsp:[]};detectedIdrInterval=0;prevIdr=0;audioDisabled=!1;activeClients=0;needBitrateReset=!1;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.maxBitrateKey="maxBitrate-"+this.streamId,this.rtspServerPath=this.storage.getItem(r),this.rtspServerPath||(this.rtspServerPath=N().randomBytes(8).toString("hex"),this.storage.setItem(r,this.rtspServerPath))}get maxBitrate(){let e=parseInt(this.storage.getItem(this.maxBitrateKey));return e||(e=this.advertisedMediaStreamOptions?.video?.maxBitrate,this.storage.setItem(this.maxBitrateKey,e?.toString())),e||void 0}async resetBitrate(){this.console.log("Resetting bitrate after adaptive streaming session",this.maxBitrate),this.needBitrateReset=!1,this.mixinDevice.setVideoStreamOptions({id:this.streamId,video:{bitrate:this.maxBitrate}})}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)||"";ye.find((t=>e.startsWith(t)))||(e="");const t=-1!==e.indexOf(ce),i=-1!==e.indexOf(ue),r=-1!==e.indexOf(le);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 fe;const r=e&&"scrypted"===i?.tool?me:fe,s=this.storage.getItem(this.rtspParserKey);return s&&s!==ge?s===me?me:s===fe?fe:r:r}getRebroadcastMode(){let e=this.storage.getItem(this.rebroadcastModeKey)||"Default",t="MPEG-TS";"scrypted"===this.advertisedMediaStreamOptions?.tool&&this.advertisedMediaStreamOptions?.container?.startsWith("rtsp")&&(t="RTSP"),"Default"===e&&(e=t);const i=e?.startsWith("RTSP");return{defaultMode:t,rtspMode:e?.startsWith("RTSP"),muxingMp4:!i||e?.includes("MP4")}}async getMixinSettings(){const t=[],i=this.parserSession;let r=0,s=0;const{muxingMp4:n,rtspMode:o,defaultMode:a}=this.getRebroadcastMode();for(const e of n?this.prebuffers.mp4:this.prebuffers.rtsp){s=s||e.time;for(const t of e.chunk.chunks)r+=t.byteLength}const c=Date.now()-s,d=Math.round(r/c*8),u=this.streamName?`Rebroadcast: ${this.streamName}`:"Rebroadcast";t.push({title:"Audio Codec Transcoding",group:u,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)||ae,choices:[ae,de,"Compatible Audio (Copy)","Other Audio (Transcode)"]},{title:"Rebroadcast Container",group:u,description:`Experimental: The container format to use when rebroadcasting. MPEG-TS is stable. RTSP is lower latency. The default mode for this camera is ${a}.`,placeholder:"MPEG-TS",choices:[ge,"MPEG-TS","RTSP"],key:this.rebroadcastModeKey,value:this.storage.getItem(this.rebroadcastModeKey)||ge});const l=()=>{t.push({title:"FFmpeg Input Arguments Prefix",group:u,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:he,choices:[he,"-use_wallclock_as_timestamps 1","-v verbose"],combobox:!0})};if(this.canUseRtspParser(n,this.advertisedMediaStreamOptions)&&o&&"rtsp"===this.advertisedMediaStreamOptions?.container){const e=this.getParser(o,n,this.advertisedMediaStreamOptions);t.push({key:this.rtspParserKey,group:u,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 "${e}" for this camera.`,value:this.storage.getItem(this.rtspParserKey)||ge,choices:[ge,fe,me]}),e!==me&&l()}else l();return i?t.push({key:"detectedResolution",group:u,title:"Detected Resolution and Bitrate",readonly:!0,value:`${i?.inputVideoResolution?.[0]||"unknown"} @ ${d||"unknown"} Kb/s`,description:"Configuring your camera to 1920x1080, 2000Kb/S, Variable Bit Rate, is recommended."},{key:"detectedCodec",group:u,title:"Detected Video/Audio Codecs",readonly:!0,value:(i?.inputVideoCodec?.toString()||"unknown")+"/"+(i?.inputAudioCodec?.toString()||"unknown"),description:"Configuring your camera to H264 video and AAC/MP3/MP2/Opus audio is recommended."},{key:"detectedKeyframe",group:u,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"}):t.push({title:"Status",group:u,key:"status",description:"Rebroadcast is currently idle and will be started automatically on demand.",value:"Idle",readonly:!0}),o&&t.push({group:u,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}`}),this.mixin.mixinDeviceInterfaces.includes(e.ScryptedInterface.VideoCameraConfiguration)&&t.push({group:u,key:this.maxBitrateKey,title:"Max Bitrate",description:"This camera supports Adaptive Bitrate. Set the maximum bitrate to be allowed while using adaptive bitrate streaming. This will also serve as the default bitrate.",type:"number",value:this.maxBitrate?.toString()}),t}async startPrebufferSession(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtsp=[];const t=parseInt(this.storage.getItem(ne))||se;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:n,aacAudio:o,compatibleAudio:c,reencodeAudio:l}=this.getAudioConfig(),{rtspMode:p,muxingMp4:h}=this.getRebroadcastMode();let m=this.storage.getItem(this.lastDetectedAudioCodecKey)||void 0;"null"===m&&(m=null);let f=!1;h&&!r&&!s&&n&&void 0===m&&(this.console.warn("Camera did not report an audio codec, muting the audio stream and probing the codec."),f=!0);const g=void 0===m?s?.toLowerCase():m?.toLowerCase(),y=!pe.includes(g);!h||f||!1===i?.userConfigurable||r||y&&(n&&te.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 I;this.audioDisabled=!1;const M=null===m;let O=!1;if(!f&&n&&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."),O=!0),r||f)I=["-an"],this.audioDisabled=!0;else if(l||O)I=["-bsf:a","aac_adtstoasc","-acodec","aac","-ar","32k","-b:a","32k","-ac","1","-profile:a","aac_low","-flags","+global_header"];else if(o||M)I=["-acodec","copy"],I.push(...v);else if(c)I=["-acodec","copy"],I.push(...S);else{I=["-acodec","copy"];const e="aac"===g?v:S;I.push(...e)}const T=["-vcodec","copy"],D={console:this.console,timeout:6e4,parsers:{}};if(this.parsers=D.parsers,this.console.log("rebroadcast mode:",p?"rtsp":"mpegts"),p){const e=function(e){let t;return{container:"rtsp",tcpProtocol:"rtsp://127.0.0.1/"+(0,K.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 J(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:T,acodec:n?["-acodec","copy"]:I});this.sdp=e.sdp,D.parsers.rtsp=e}else D.parsers.mpegts={container:"mpegts",outputArguments:[...(A={vcodec:T,acodec:I})?.vcodec||[],...A?.acodec||[],"-f","mpegts"],parse:(_=188,B=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,u.once)(e,"readable");continue}if(t.push(r),i+=r.length,i<_)continue;const s=Buffer.concat(t);B?.(s);const n=s.length%_,o=s.slice(0,s.length-n),a=s.slice(s.length-n);t=[a],i=a.length,yield{chunks:[o]}}}),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 A,_,B;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=E(e);yield*R(t)},findSyncFrame:k}}({vcodec:T,acodec:I}));const V=await this.mixinDevice.getVideoStream(i),H="x-scrypted/x-rfc4571"===V.mimeType;let j,N;this.storage.removeItem(this.lastDetectedAudioCodecKey);let $=!1;if(p&&H){$=!0,this.console.log("bypassing ffmpeg: using scrypted rfc4571 parser");const e=await ee.convertMediaObjectToJSON(V,"x-scrypted/x-rfc4571"),{url:t,sdp:i,mediaStreamOptions:r}=e;j=await Z(this.console,function(e){const t=new URL(e);if(!t.protocol.startsWith("tcp"))throw new Error("rfc4751 url must be tcp");return a().connect(parseInt(t.port),t.hostname)}(t),i,r,!1,D),this.sdp=j.sdp.then((e=>Buffer.concat(e).toString()))}else{const t=await ee.convertMediaObjectToBuffer(V,e.ScryptedMimeTypes.FFmpegInput),i=JSON.parse(t.toString());N=i.mediaStreamOptions;if(this.getParser(p,h,i.mediaStreamOptions)===me){$=!0,this.console.log("bypassing ffmpeg: using scrypted rtsp/rfc4571 parser");const e=new z(i.url,this.console);try{e.requestTimeout=1e4,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:U(e,"audio",t)?.trackId,video:U(e,"video",t)?.trackId}}(t);let o=0;r||(await e.setup(o,s),o+=2),await e.setup(o,n),await e.play(),j=await Z(this.console,e.rfc4571,t,i.mediaStreamOptions,!0,D);const a=j.kill.bind(j);let c=!1;if(j.kill=async()=>{c||(c=!0,e.teardown().finally(a)),await async function(e){await new Promise((t=>setTimeout(t,e)))}(500),e.client.destroy(),a()},!j.isActive)throw new Error("parser was killed before rtsp client started");e.readLoop().finally((()=>j.kill()))}catch(t){throw e.client.destroy(),t}}else{const e=this.storage.getItem(this.ffmpegInputArgumentsKey)||he;i.inputArguments.unshift(...e.split(" ")),j=await C(i,D)}}$&&h&&this.getVideoStream({id:this.streamId,refresh:!1}).then((async t=>{const i=this.storage.getItem(this.ffmpegInputArgumentsKey)||he,r=await ee.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"),P(r,s);const n=d().spawn(await L.getFFmpegPath(),s,{stdio:["pipe","pipe","pipe","pipe"]});return w(r,n),{cp:n,generator:E(n.stdio[3])}}(r.inputArguments,I,T,this.console),n=()=>{b(s.cp),j.kill(),s.generator.throw(new Error("killed"))};if(!j.isActive)return void n();j.killed.finally(n);const{resetActivityTimer:o}=x("mp4",n,j,D.timeout);for await(const e of R(s.generator))o(),j.emit("mp4",e)})).catch((()=>{})),!r&&s&&void 0!==j.inputAudioCodec&&j.inputAudioCodec!==s&&this.console.warn("Audio codec plugin reported vs detected mismatch",s,m);const q=i?.video?.codec;if(q&&void 0!==j.inputVideoCodec&&j.inputVideoCodec!==q&&this.console.warn("Video codec plugin reported vs detected mismatch",q,j.inputVideoCodec),j.inputAudioCodec?pe.includes(j.inputAudioCodec?.toLowerCase())?this.console.log("Detected audio codec is mp4/mpegts compatible.",j.inputAudioCodec):this.console.log("Detected audio codec is not mp4/mpegts compatible.",j.inputAudioCodec):this.console.log("No audio stream detected."),this.storage.setItem(this.lastDetectedAudioCodecKey,j.inputAudioCodec||"null"),"h264"!==j.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."),j.kill(),this.startPrebufferSession();if(this.parserSession=j,re.onMixinEvent(this.mixin.id,this.mixin.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0),N?.refreshAt){let t,i=N;const r=async()=>{if(!j.isActive)return;const t=await this.mixinDevice.getVideoStream(i),r=await ee.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),j.killed.finally((()=>clearTimeout(t)))}j.killed.finally((()=>{clearTimeout(this.inactivityTimeout),this.parserSessionPromise=void 0,this.parserSession===j&&(this.parserSession=void 0)}));for(const i of ve){let r=0;j.on(i,(s=>{const n=this.prebuffers[i],o=Date.now(),a=()=>{if(this.prevIdr){const t="number"!=typeof this.detectedIdrInterval;this.detectedIdrInterval=o-this.prevIdr,t&&re.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 j}printActiveClients(){this.console.log(this.streamName,"active rebroadcast clients:",this.activeClients)}inactivityCheck(t,i){this.activeClients||(this.needBitrateReset&&this.mixin.mixinDeviceInterfaces.includes(e.ScryptedInterface.VideoCameraConfiguration)&&this.resetBitrate(),this.stopInactive&&(this.inactivityTimeout&&!i||(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"),t.kill())}),3e4))))}async handleRebroadcasterClient(e){const{isActiveClient:t,container:i,session:r,socketPromise:s,requestedPrebuffer:n}=e;O(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=()=>{r.removeListener(i,a),r.removeListener("killed",c),s()};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.printActiveClients()),this.inactivityCheck(r,t),c()}}})}async getVideoStream(e){if(!1===e?.refresh&&!this.parserSessionPromise)throw new Error("Stream is currently unavailable and will not be started for this request. RequestMediaStreamOptions.refresh === false");this.ensurePrebufferSession();const t=await this.parserSessionPromise,i="false"!==this.storage.getItem(oe);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||(a.video={}),a.video.codec=t.inputVideoCodec,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 l=0;const p=this.prebuffers[o];for(const e of p)if(!(e.time<u-r))for(const t of e.chunk.chunks)l+=t.length;const h=Math.max(5e5,l).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=H(t);const i=new J(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 ee.createFFmpegMediaObject(f)}}class be extends n{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){if(e?.directMediaStream)return this.mixinDevice.getVideoStream(e);await this.ensurePrebufferSessions();const t=e?.id,i=this.sessions.get(t);return 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"),te.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 n=this.mixinDeviceInterfaces.includes(e.ScryptedInterface.Battery);let o=0;for(const e of s){let i=this.sessions.get(e);if(!i){const s=t?.find((t=>t.id===e));s?.prebuffer&&te.a(`Prebuffer is already available on ${this.name}. If this is a grouped device, disable the Rebroadcast extension.`);const a=s?.name,c=r.includes(e);if(i=new Se(this,s,n||!c),this.sessions.set(e,i),n){this.console.log("camera is battery powered, prebuffering and rebroadcasting will only work on demand.");continue}if(!c){this.console.log("stream",a,"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;o++,e=!0,this.online=!!o,await t.killed,this.console.error("prebuffer session ended")}catch(e){this.console.error("prebuffer session ended with error",e)}finally{e&&o--,e=!1,this.online=!!o}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)")})()}}if(!this.sessions.has(void 0)){const e=this.storage.getItem("defaultStream");let i=this.sessions.get(t?.find((t=>t.name===e))?.id);i||(i=this.sessions.get(t?.find((e=>e.id===r[0]))?.id)),i||(i=this.sessions.get(t?.find((e=>e.id===s?.[0]))?.id)),i?(this.sessions.set(void 0,i),this.console.log("Default Stream:",i.advertisedMediaStreamOptions.id,i.advertisedMediaStreamOptions.name)):this.console.warn("Unable to find Default Stream?")}re.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:"Default Stream",description:"The default stream to use when not specified.",key:"defaultStream",value:this.storage.getItem("defaultStream")||i?.[0]?.name||t[0].name,choices:t.map((e=>e.name))},{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:ne,value:this.storage.getItem(ne)||se.toString()},{title:"Start at Previous Keyframe",description:"Start live streams from the previous key frame. Improves startup time.",type:"boolean",key:oe,value:("false"!==this.storage.getItem(oe)).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(ne))||se;for(const r of e)(this.sessions.get(r.id)?.parserSession||t.includes(r))&&(r.prebuffer=i);return e}setVideoStreamOptions(e){const t=this.sessions.get(e.id);if(t&&e?.video?.bitrate){t.needBitrateReset=!0;const i=t.maxBitrate;i&&e?.video?.bitrate>i&&(this.console.log("clamping max bitrate request",e.video.bitrate,i),e.video.bitrate=i)}return this.mixinDevice.setVideoStreamOptions(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 we=new class extends V{storageSettings=new Y(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(ie.getSystemState())){const t=ie.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((()=>re.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(a().Server)((async e=>{let t;const i=new J(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,t=e,t.ensurePrebufferSession(),await t.parserSessionPromise,i.sdp=await t.sdp,!0}return!1}));this.console.log("RTSP Rebroadcast connection started."),i.console=this.console;try{await i.handlePlayback();const r=await t.parserSessionPromise,s=1.5*Math.max(4e3,t.detectedIdrInterval||4e3);t.handleRebroadcasterClient({isActiveClient:!0,container:"rtsp",session:r,socketPromise:Promise.resolve(e),requestedPrebuffer:s}),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:c}=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:l}=await g(),p={url:l,inputArguments:["-rtsp_transport","tcp","-i",l.replace("tcp","rtsp")]};return u.then((async e=>{const t=new J(e,n);await t.handlePlayback();const i=a().connect(parseInt(d.port),d.hostname);for(e.on("close",(()=>{i.destroy()})),i.on("close",(()=>{e.destroy()}));;){const r=(await T(i,2)).readInt16BE(0),s=await T(i,r),n=127&s[1];if(o.has(n))t.sendAudio(s,!1);else{if(!c.has(n))throw e.destroy(),i.destroy(),new Error("unknown payload type "+n);t.sendVideo(s,!1)}}})),Buffer.from(JSON.stringify(p))}async canMixin(t,i){if(!i.includes(e.ScryptedInterface.VideoCamera))return null;const r=[e.ScryptedInterface.VideoCamera,e.ScryptedInterface.Settings,e.ScryptedInterface.Online];return i.includes(e.ScryptedInterface.VideoCameraConfiguration)&&r.push(e.ScryptedInterface.VideoCameraConfiguration),r}async getMixin(e,t,i){this.setHasEnabledMixin(i.id);const r=new be(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})})();
|
|
1
|
+
(()=>{var e={747:(e,t,i)=>{"use strict";Object.defineProperty(t,"rh",{enumerable:!0,get:function(){return s.default}}),Object.defineProperty(t,"Nu",{enumerable:!0,get:function(){return n.default}});var r=a(i(945)),s=a(i(816)),n=a(i(243)),o=a(i(828));function a(e){return e&&e.__esModule?e:{default:e}}const c=[s.default,n.default,o.default];s.default,n.default,o.default},816:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,s=(r=i(945))&&r.__esModule?r:{default:r},n=i(761);const o=["realm"],a=o,c={type:"Basic",parseWWWAuthenticateRest:function(e){return(0,n.parseHTTPHeadersQuotedKeyValueSet)(e,a,o)},buildWWWAuthenticateRest:function(e){return(0,n.buildHTTPHeadersQuotedKeyValueSet)(e,a,o)},parseAuthorizationRest:function(e){if(!e)throw new s.default("E_EMPTY_AUTH");const{username:t,password:i}=c.decodeHash(e);return{hash:e,username:t,password:i}},buildAuthorizationRest:function({hash:e,username:t,password:i}){if(t&&i)return c.computeHash({username:t,password:i});if(!e)throw new s.default("E_NO_HASH");return e},computeHash:function({username:e,password:t}){return Buffer.from(e+":"+t).toString("base64")},decodeHash:function(e){const[t,...i]=Buffer.from(e,"base64").toString().split(":");return{username:t,password:i.join(":")}}};var d=c;t.default=d},828:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,s=(r=i(945))&&r.__esModule?r:{default:r},n=i(761);const o=["invalid_request","invalid_token","insufficient_scope"],a=["realm","scope","error","error_description"];var c={type:"Bearer",parseWWWAuthenticateRest:function(e){return(0,n.parseHTTPHeadersQuotedKeyValueSet)(e,a,[])},buildWWWAuthenticateRest:function(e){if(e.error&&-1===o.indexOf(e.error))throw new s.default("E_INVALID_ERROR",e.error,o);return(0,n.buildHTTPHeadersQuotedKeyValueSet)(e,a,[])},parseAuthorizationRest:function(e){if(!e)throw new s.default("E_EMPTY_AUTH");return{hash:e}},buildAuthorizationRest:function({hash:e}){if(!e)throw new s.default("E_NO_HASH");return e}};t.default=c},243:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,s=i(761),n=(r=i(113))&&r.__esModule?r:{default:r};const o=["realm","nonce"],a=[...o,"domain","opaque","stale","algorithm","qop"],c=["username","realm","nonce","uri","response"],d=[...c,"algorithm","cnonce","opaque","qop","nc"];function u(e,t){const i=n.default.createHash(e);return i.update(t),i.digest("hex")}var l={type:"Digest",parseWWWAuthenticateRest:function(e){return(0,s.parseHTTPHeadersQuotedKeyValueSet)(e,a,o)},buildWWWAuthenticateRest:function(e){return(0,s.buildHTTPHeadersQuotedKeyValueSet)(e,a,o)},parseAuthorizationRest:function(e){return(0,s.parseHTTPHeadersQuotedKeyValueSet)(e,d,c)},buildAuthorizationRest:function(e){return(0,s.buildHTTPHeadersQuotedKeyValueSet)(e,d,c)},computeHash:function(e){const t=e.ha1||u(e.algorithm,[e.username,e.realm,e.password].join(":")),i=u(e.algorithm,[e.method,e.uri].join(":"));return u(e.algorithm,[t,e.nonce,e.nc,e.cnonce,e.qop,i].join(":"))}};t.default=l},761:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.parseHTTPHeadersQuotedKeyValueSet=function(e,t,i=[]){const r=e.trim().match(n);if(!r)throw new s.default("E_MALFORMED_QUOTEDKEYVALUE",e);const a=r.map(((e,t)=>{const i=e.split("=");if(2!==i.length)throw new s.default("E_MALFORMED_QUOTEDKEYVALUE",t,e,i.length);return i})).reduce((function(e,[i,r],n){if(-1===t.indexOf(i))throw new s.default("E_UNAUTHORIZED_KEY",n,i);return e[i]=r.replace(/^"(.+(?="$))"$/,"$1"),e}),{});return o(i,a),a},t.buildHTTPHeadersQuotedKeyValueSet=function(e,t,i=[]){return o(i,e),t.reduce((function(t,i){return e[i]?t+(t?", ":"")+i+'="'+e[i]+'"':t}),"")};var r,s=(r=i(945))&&r.__esModule?r:{default:r};const n=/\w+=(".*?"|[^",]+)(?=,|$)/g;function o(e,t){e.forEach((e=>{if(void 0===t[e])throw new s.default("E_REQUIRED_KEY",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}async createMediaObject(e,t){return mediaManager.createMediaObject(e,t,{sourceId:this.id})}getMediaObjectConsole(e){return"string"!=typeof e.sourceId?this.console:deviceManager.getMixinConsole(e.sourceId,this.nativeId)}_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}async createMediaObject(e,t){return mediaManager.createMediaObject(e,t,{sourceId:this.id})}getMediaObjectConsole(e){return"string"!=typeof e.sourceId?this.console:deviceManager.getMixinConsole(e.sourceId,this.mixinProviderNativeId)}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:()=>l,MediaPlayerState:()=>p,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.pluginId="pluginId",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","pluginId","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:[]},Microphone:{name:"Microphone",methods:["getAudioStream"],properties:[]},Display:{name:"Display",methods:["startDisplay","stopDisplay"],properties:[]},VideoCamera:{name:"VideoCamera",methods:["getVideoStream","getVideoStreamOptions"],properties:[]},VideoRecorder:{name:"VideoRecorder",methods:["getRecordingStream","getRecordingStreamOptions","getRecordingStreamThumbnail"],properties:[]},VideoClips:{name:"VideoClips",methods:["getVideoClip","getVideoClipThumbnail","getVideoClips","removeVideoClips"],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,l,p,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.SmartDisplay="SmartDisplay",e.Speaker="Speaker",e.SmartSpeaker="SmartSpeaker",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"}(l||(l={})),function(e){e.Idle="Idle",e.Playing="Playing",e.Paused="Paused",e.Buffering="Buffering"}(p||(p={})),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.Microphone="Microphone",e.Display="Display",e.VideoCamera="VideoCamera",e.VideoRecorder="VideoRecorder",e.VideoClips="VideoClips",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.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.RTCSignalingChannel="x-scrypted/x-scrypted-rtc-signaling-channel",e.SchemePrefix="x-scrypted/x-scrypted-scheme-",e.MediaObject="x-scrypted/x-scrypted-media-object"}(m||(m={}))},113:e=>{"use strict";e.exports=require("crypto")},945:(e,t,i)=>{"use strict";i.r(t),i.d(t,{default:()=>m});const r=require("os");function s(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 n(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function o(e){var t="function"==typeof Map?new Map:void 0;return o=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 a(e,arguments,u(this).constructor)}return r.prototype=Object.create(e.prototype,{constructor:{value:r,enumerable:!1,writable:!0,configurable:!0}}),d(r,e)},o(e)}function a(e,t,i){return a=c()?Reflect.construct:function(e,t,i){var r=[null];r.push.apply(r,t);var s=new(Function.bind.apply(e,r));return i&&d(s,i.prototype),s},a.apply(null,arguments)}function c(){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 d(e,t){return d=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},d(e,t)}function u(e){return u=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},u(e)}let l=function(e){function t(e,i,...r){var s,o,a;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=[]),o=this,(s=!(a=u(t).call(this,i))||"object"!=typeof a&&"function"!=typeof a?n(o):a).code=i||"E_UNEXPECTED",s.params=r,s.wrappedErrors=e,s.name=s.toString(),Error.captureStackTrace&&Error.captureStackTrace(n(s),s.constructor),s}var i,o,a;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&&d(e,t)}(t,e),i=t,(o=[{key:"toString",value:function(){return(this.wrappedErrors.length?this.wrappedErrors[this.wrappedErrors.length-1].stack+r.EOL:"")+this.constructor.name+": "+this.code+" ("+this.params.join(", ")+")"}}])&&s(i.prototype,o),a&&s(i,a),t}(o(Error));function p(e){return e instanceof l||e.constructor&&e.constructor.name&&e.constructor.name.endsWith("Error")&&"string"==typeof e.code&&h(e.code)&&e.params&&e.params instanceof Array}function h(e){return/^([A-Z0-9_]+)$/.test(e)}l.wrap=function(e,t,...i){let r=null;const s=h(e.message),n=(e.wrappedErrors||[]).concat(e);return t||(t=s?e.message:"E_UNEXPECTED"),e.message&&!s&&i.push(e.message),r=new l(n,t,...i),r},l.cast=function(e,...t){return p(e)?e:l.wrap.apply(l,[e].concat(t))},l.bump=function(e,...t){return p(e)?l.wrap.apply(l,[e,e.code].concat(e.params)):l.wrap.apply(l,[e].concat(t))};const m=l}},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:()=>xe});var e=i(510),t=i.n(e);const{systemManager:s}=t(),n="v4";class o extends e.ScryptedDeviceBase{hasEnabledMixin={};unshiftMixin=!1;constructor(t){super(t);try{this.hasEnabledMixin=JSON.parse(this.storage.getItem("hasEnabledMixin"))}catch(e){this.hasEnabledMixin={}}this.pluginsComponent=s.getComponent("plugins"),s.listen((async(t,i,r)=>{i.eventInterface!==e.ScryptedInterface.ScryptedDevice||i.property||this.maybeEnableMixin(t)}));for(const e of Object.keys(s.getSystemState())){const t=s.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]===n)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();this.unshiftMixin?t.unshift(this.id):t.push(this.id);const i=await this.pluginsComponent;await i.setMixins(e.id,t),this.setHasEnabledMixin(e.id)}setHasEnabledMixin(e){this.hasEnabledMixin[e]!==n&&(this.hasEnabledMixin[e]=n,this.storage.setItem("hasEnabledMixin",JSON.stringify(this.hasEnabledMixin)))}}const a=require("child_process");var c=i.n(a);const d=require("process");var u=i.n(d);const l=["decode_slice_header error","no frame!","non-existing PPS"];function p(e){if(e){try{e.stdin.write("q\n")}catch(e){}setTimeout((()=>{e.kill(),setTimeout((()=>{e.kill("SIGKILL")}),2e3)}),2e3)}}function h(e,t,i,r){const s=!!u().env.SCRYPTED_FFMPEG_NOISY||!!r?.getItem("SCRYPTED_FFMPEG_NOISY");function n(e){const r=n=>{const o=n.toString();for(const e of l)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 m(e,t){const i=[];let r=!1;for(const e of t){try{if(r){const t=new URL(e);i.push(`${t.protocol}[REDACTED]`)}else i.push(e)}catch(t){i.push(e)}r="-i"===e}e.log(i.join(" "))}const f=require("events");async function g(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 y="\n".charCodeAt(0);async function S(e){return async function(e,t){const i=[];let r=0;for(;;){const s=await g(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,y)}const{mediaManager:v}=t();async function*b(e){for(;;){const t=await g(e,8),i=t.readInt32BE(0)-8,r=t.slice(4).toString(),s=await g(e,i);yield{header:t,length:i,type:r,data:s}}}const w=["-movflags","frag_keyframe+empty_moov+default_base_moof+skip_sidx+skip_trailer","-f","mp4"];const P=require("net");var I=i.n(P);const M=require("dgram");var x=i.n(M);async function C(e,t){e.bind(t),await(0,f.once)(e,"listening");const i=e.address().port;return{port:i,url:`udp://127.0.0.1:${i}`}}async function T(e){return C(e,0)}async function O(e,t){return e.bind(t),await(0,f.once)(e,"listening"),{port:t,url:`udp://127.0.0.1:${t}`}}async function R(){const e=new(I().Server),t=await async function(e){return e.listen(0),await(0,f.once)(e,"listening"),e.address().port}(e),i=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)}))}));return{server:e,url:`tcp://127.0.0.1:${t}`,port:t,clientPromise:i}}const{mediaManager:D}=t();async function A(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 k(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 E(e,t){const{console:i}=t;let r,s=!0;const n=new f.EventEmitter;let o,a,d,u,l;n.on("error",(e=>i.error("rebroadcast error",e)));const g=new Promise(((e,t)=>{u=e,l=t}));let y;const S=new Promise((e=>{y=e}));function v(){s&&(n.emit("killed"),n.emit("error",new Error("killed"))),s=!1,clearTimeout(r),y(),l?.(new Error("ffmpeg was killed before connecting to the rebroadcast session")),p(C)}const b=e.inputArguments.slice();r=setTimeout(v,3e4);let w=!1;const P=e=>{if(!s)throw e(),new Error("parser session was killed killed before ffmpeg connected");n.on("killed",e)},I=["pipe","pipe","pipe"];let M=3;for(const e of Object.keys(t.parsers)){const r=t.parsers[e];if(r.parseDatagram){w=!0;const i=x().createSocket("udp4"),s=await T(i),o=x().createSocket("udp4");await O(o,s.port+1),P((()=>{i.close(),o.close()})),b.push(...r.outputArguments,s.url.replace("udp://","rtp://"));const{resetActivityTimer:a}=k(e,v,n,t?.timeout);(async()=>{for await(const t of r.parseDatagram(i,parseInt(d?.[2]),parseInt(d?.[3])))u?.(void 0),n.emit(e,t),a()})(),(async()=>{for await(const t of r.parseDatagram(o,parseInt(d?.[2]),parseInt(d?.[3]),"rtcp"))u?.(void 0),n.emit(e,t),a()})()}else if(r.tcpProtocol){const s=await R(),o=new URL(r.tcpProtocol);o.port=s.port.toString(),b.push(...r.outputArguments,o.toString());const{resetActivityTimer:a}=k(e,v,n,t?.timeout);(async()=>{const t=await s.clientPromise;try{P((()=>t.destroy()));for await(const i of r.parse(t,parseInt(d?.[2]),parseInt(d?.[3])))u?.(void 0),n.emit(e,i),a()}catch(e){i.error("rebroadcast parse error",e),v()}})()}else b.push(...r.outputArguments,"pipe:"+M++),I.push("pipe")}w&&(b.push("-sdp_file","pipe:"+M++),I.push("pipe")),b.unshift("-hide_banner"),m(i,b);const C=c().spawn(await D.getFFmpegPath(),b,{stdio:I});let E;h(i,C,void 0,t?.storage),C.on("exit",v),E=w?new Promise((e=>{const t=[];C.stdio[M-1].on("data",(i=>{t.push(i),e(t)}))})):Promise.resolve([]);let _=0;return Object.keys(t.parsers).forEach((async e=>{const r=t.parsers[e];if(!r.parse||r.tcpProtocol)return;const s=C.stdio[3+_];_++;try{const{resetActivityTimer:i}=k(e,v,n,t?.timeout);for await(const t of r.parse(s,parseInt(d?.[2]),parseInt(d?.[3])))u?.(void 0),n.emit(e,t),i()}catch(e){i.error("rebroadcast parse error",e),v()}})),async function(e){return A(e,"Audio")}(C).then((e=>o=e)),async function(e){return A(e,"Video")}(C).then((e=>a=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)}))}(C).then((e=>d=e)),await g,u=void 0,l=void 0,clearTimeout(r),{sdp:E,inputAudioCodec:o,inputVideoCodec:a,inputVideoResolution:d,get isActive(){return s},kill:v,killed:S,mediaStreamOptions:e.mediaStreamOptions||{id:void 0,name:void 0},emit(e,t){return n.emit(e,t),this},on(e,t){return n.on(e,t),this},once(e,t){return n.once(e,t),this},removeListener(e,t){return n.removeListener(e,t),this}}}async function _(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")))}const V=require("stream");var B=i(113),L=i.n(B);function H(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")}function j(e,t,i=["recvonly","sendrecv"]){const r=function(e){return("\n"+e).split("\nm=")}(e).filter((e=>e.startsWith(t)));for(const e of r){const t=()=>{const t=e.split("\n").map((e=>e.trim())),i="a=control:",r=t.find((e=>e.startsWith(i)));return{section:"m="+e,trackId:r.substring(i.length)}};for(const r of i)if(e.includes(`a=${r}`))return t();if(i.includes("recvonly")&&!e.includes("sendonly")&&!e.includes("inactive"))return t()}}function U(e){const t=new Set;return(e=>{for(const i of e||[])t.add(parseInt(i))})(e.split(" ").slice(3)),t}const N="a=control:";function K(e){const t=e.find((e=>e.startsWith(N)))?.substring(N.length);return{...(i=e[0],{type:i.split(" ")[0].substring(2),payloadTypes:U(i)}),lines:e,contents:e.join("\r\n"),control:t};var i}const $=require("tls");var q=i.n($),F=i(747);class W extends Error{constructor(){super("Operation Timed Out")}}function G(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 z 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 outgoing message\n",r),this.console?.log(),i&&this.client.write(i)}async readMessage(){const e=await async function(e){let t=[];for(;;){let i=await S(e);if(i=i.trim(),!i)return t;t.push(i)}}(this.client);return this.console?.log("rtsp incoming message\n",e.join("\n")),this.console?.log(),e}}{cseq=0;rfc4571=new V.PassThrough;needKeepAlive=!1;constructor(e,t){super(t),this.url=e;const i=new URL(e),r=parseInt(i.port)||554;e.startsWith("rtsps")?this.client=q().connect({rejectUnauthorized:!1,port:r,host:i.hostname}):this.client=I().connect(r,i.hostname)}writeRequest(e,t,i,r){t=t||{};let s=this.url;i&&(i.includes("rtsp://")||i.includes("rtsps://")?s=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 handleDataPayload(e){if(36!==e[0])throw new Error("RTSP Client expected frame magic but received: "+e.toString());const t=e.readUInt16BE(2),i=await g(this.client,t);this.rfc4571.push(e),this.rfc4571.push(i)}async readDataPayload(){const e=await g(this.client,4);return this.handleDataPayload(e)}async readLoop(){try{for(;;)this.needKeepAlive&&(this.needKeepAlive=!1,await this.getParameter()),await this.readDataPayload()}catch(e){throw this.client.destroy(e),this.rfc4571.destroy(e),e}}async readMessage(){for(;;){const e=await g(this.client,4);if("RTSP"===e.toString()){this.client.unshift(e);const t=await super.readMessage();return this.console?.log("rtsp incoming message\n",t.join("\n")),this.console?.log(),t}await this.handleDataPayload(e)}}async request(e,t,i,r,s){this.writeRequest(e,t,i,r);const n=this.requestTimeout?await(o=this.requestTimeout,a=this.readMessage(),new Promise(((e,t)=>{setTimeout((()=>t(new W)),o),a.then(e),a.catch(t)}))):await this.readMessage();var o,a;const c=n[0],d=G(n);if(!c.includes("200")&&!d["www-authenticate"])throw new Error(c);const u=d["www-authenticate"];if(u){if(s)throw new Error("auth failed");const n=new URL(this.url);if(u.includes("Basic")){const e=F.rh.computeHash(n);this.authorization=`Basic ${e}`}else{const t=F.Nu.parseWWWAuthenticateRest(u),i=decodeURIComponent(n.username),r=decodeURIComponent(n.password),s=L().createHash("md5").update(`${i}:${t.realm}:${r}`).digest("hex"),o=L().createHash("md5").update(`${e}:${n.pathname}`).digest("hex"),a=L().createHash("md5").update(`${s}:${t.nonce}:${o}`).digest("hex"),c={username:i,realm:t.realm,nonce:t.nonce,uri:n.pathname,algorithm:"MD5",response:a},d=Object.entries(c).map((([e,t])=>{return`${e}=${t&&(i=t,`"${i.replace(/"/g,'\\"')}"`)}`;var i})).join(", ");this.authorization=`Digest ${d}`}return this.request(e,t,i,r,!0)}const l=parseInt(d["content-length"]);return l?{headers:d,body:await g(this.client,l)}:{headers:d,body:void 0}}async options(){return this.request("OPTIONS",{})}async getParameter(){return this.request("GET_PARAMETER")}async describe(e){return this.request("DESCRIBE",{...e||{},Accept:"application/sdp"})}async setup(e,t,i){const r={Transport:`RTP/AVP/${i?"UDP":"TCP"};unicast;${i?"client_port":"interleaved"}=${e}-${e+1}`},s=await this.request("SETUP",r,t);if(s.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}(s.headers.session);let t=parseInt(e.timeout);if(t){let e=setInterval((()=>this.needKeepAlive=!0),1e3*(t-5));this.client.once("close",(()=>clearInterval(e)))}this.session=s.headers.session.split(";")[0]}return s}async play(e="0.000"){const t={Range:`npt=${e}-`};return this.request("PLAY",t)}async pause(){return this.request("PAUSE")}async teardown(){try{return await this.request("TEARDOWN")}finally{this.client.destroy()}}}class J{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,B.randomBytes)(4).toString("hex"),t&&(t=t.trim())}async handleSetup(){let e=[];for(;;){let t=await S(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 g(this.client,4);if(36!==e[0])throw new Error("RTSP Server expected frame magic but received: "+e.toString());const t=e.readUInt16BE(2),i=await g(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=j(this.sdp,"audio"),n=j(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=j(this.sdp,"audio"),s=j(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 g(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=G(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),this.console?.log("response body",s.toString()))}}const{systemManager:Q}=t();class Y{values={};hasValue={};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)}),Object.defineProperty(this.hasValue,e,{get:()=>null!=this.device.storage.getItem(e)})}}async getSettings(){const e=await(this.options?.onGet?.()),t=[];for(const[i,r]of Object.entries(this.settings)){let s=Object.assign({},r);e?.[i]&&(s=Object.assign(s,e[i])),s.onGet&&(s=Object.assign(s,await s.onGet())),s.hide||await(this.options?.hide?.[i]?.())||(s.key=i,s.value=this.getItemInternal(i,s),t.push(s),delete s.onPut,delete s.onGet,delete s.mapPut,delete s.mapGet)}return t}async putSetting(t,i){const r=this.settings[t];let s;r&&(s=this.getItemInternal(t,r)),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)}getItemInternal(e,t){if(!t)return this.device.storage.getItem(e);const i=function(e,t){const{defaultValue:i}=t,r=t.multiple?"array":t.type;if("boolean"===r)return"true"===e||"false"!==e&&(i||!1);if("number"===r)return parseFloat(e)||i||0;if("integer"===r)return parseInt(e)||i||0;if("array"===r)try{return JSON.parse(e)}catch(e){return i||[]}if("device"===r)return Q.getDeviceById(e);if(e&&t.json)try{return JSON.parse(e)}catch(e){return i}return e||i}(this.device.storage.getItem(e),t);return t.mapGet?t.mapGet(i):i}getItem(e){return this.getItemInternal(e,this.settings[e])}}const{deviceManager:X}=t();class Z extends e.MixinDeviceBase{constructor(t){super(t),this.settingsGroup=t.group,this.settingsGroupKey=t.groupKey,process.nextTick((()=>X.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=X.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),X.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)}async release(){await X.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)}}function ee(e){return e}async function*te(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{mediaManager:ie}=t();async function re(e,t,i,r,s,n){let o=!0;const a=new V.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]);let u;const l=new Promise((e=>{u=e})),p=()=>{o&&(a.emit("killed"),a.emit("error",new Error("killed"))),o=!1,u(),t.destroy()};let h,m;t.on("close",p),t.on("error",p),(async()=>{const{resetActivityTimer:e}=k("rtsp",p,a,n?.timeout);for(;;){let i,r;s?(i=await g(t,4),r=i.readUInt16BE(2)):(i=await g(t,2),r=i.readUInt16BE(0));const n=await g(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 l={chunks:[i,n],type:u};a.emit("rtsp",l),e()}})().finally(p);const f=j(i,"audio"),y=j(i,"video");if(f){const e=f.section.toLowerCase();e.includes("mpeg4")?h="aac":e.includes("pcm")&&(h="pcm")}return y&&y.section.toLowerCase().includes("h264")&&(m="h264"),{sdp:Promise.resolve([Buffer.from(i)]),inputAudioCodec:h,inputVideoCodec:m,inputVideoResolution:void 0,get isActive(){return o},kill:p,killed:l,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}}}function se(e){if(!e)return;const t=e.find((e=>"cloud"!==e.source));return t?[t]:[]}function ne(e,t){if(t)return e.hasValue.enabledStreams?t.filter((t=>e.values.enabledStreams.includes(t.name))):se(t)}function oe(e){const t={defaultStream:{title:"Local Stream",description:"The media stream to use when streaming on your local network. This is the default stream. Recommended resolution: 1920x1080 to 4K.",hide:!0,prefersPrebuffer:!0,preferredResolution:8294400},remoteStream:{title:"Remote/Mid Stream",description:"The media stream to use when streaming from outside your local network. Selecting a low birate stream is recommended. Recommended resolution: 1270x720.",hide:!0,prefersPrebuffer:!1,preferredResolution:921600},lowResolutionStream:{title:"Low Resolution Stream",description:"The media stream to use for low resolution output, such as Apple Watch and Video Analysis. Recommended resolution: 480x360.",hide:!0,prefersPrebuffer:!1,preferredResolution:153600},recordingStream:{title:"Local Recording Stream",description:"The media stream to use when recording to local storage such as an NVR.",hide:!0,prefersPrebuffer:!0,preferredResolution:8294400},remoteRecordingStream:{title:"Remote Recording Stream",description:"The media stream to use when recording to cloud storage such as HomeKit Secure Video clips in iCloud.",hide:!0,prefersPrebuffer:!0,preferredResolution:921600}},i=new Y(e,{enabledStreams:{title:"Prebuffered Streams",description:"Prebuffering maintains an active connection to the stream and improves load times. Prebuffer also retains the recent video for capturing motion events with HomeKit Secure video. Enabling Prebuffer is not recommended on Cloud cameras.",multiple:!0,hide:!0},...t});function r(e,t){const r=ne(i,t);return function(e,t){if(!e)return;let i,r;for(const s of e){const e=Math.abs(s.video?.width*s.video?.height-t);(!i||e<r)&&(i=s,r=e,Number.isNaN(r)&&(r=Number.MAX_SAFE_INTEGER))}return i}(e.prefersPrebuffer&&r?.length>0?r:t,e.preferredResolution)}function s(e,t){const i=t.map((e=>e.name));return{defaultValue:r(e,t)?.name,choices:i,hide:!1}}return i.options={onGet:async()=>{try{const i=await e.mixinDevice.getVideoStreamOptions();if(i?.length>1){const e=i.map((e=>e.name));return{enabledStreams:{defaultValue:se(i)?.map((e=>e.name)),choices:e,hide:!1},defaultStream:s(t.defaultStream,i),remoteStream:s(t.remoteStream,i),lowResolutionStream:s(t.lowResolutionStream,i),recordingStream:s(t.recordingStream,i),remoteRecordingStream:s(t.remoteRecordingStream,i)}}}catch(t){e.console.error("error retrieving getVideoStreamOptions",t)}return{}}},{getDefaultStream:e=>r(t.defaultStream,e),getRemoteStream:e=>r(t.remoteStream,e),getLowResolutionStream:e=>r(t.lowResolutionStream,e),getRecordingStream:e=>r(t.recordingStream,e),getRemoteRecordingStream:e=>r(t.remoteRecordingStream,e),storageSettings:i}}const{mediaManager:ae,log:ce,systemManager:de,deviceManager:ue}=t(),le="Default",pe="AAC or No Audio",he=`${pe} (Copy)`,me="Compatible Audio",fe="Other Audio",ge=["aac","mp3","mp2","opus"],ye="-fflags +genpts",Se="Scrypted",ve="FFmpeg",be="Default",we=[pe,me,fe],Pe=["mpegts","mp4","rtsp"];class Ie{prebuffers={mp4:[],mpegts:[],rtsp:[]};detectedIdrInterval=0;prevIdr=0;audioDisabled=!1;activeClients=0;needBitrateReset=!1;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.maxBitrateKey="maxBitrate-"+this.streamId,this.rtspServerPath=this.storage.getItem(r),this.rtspServerPath||(this.rtspServerPath=L().randomBytes(8).toString("hex"),this.storage.setItem(r,this.rtspServerPath))}get maxBitrate(){let e=parseInt(this.storage.getItem(this.maxBitrateKey));return e||(e=this.advertisedMediaStreamOptions?.video?.maxBitrate,this.storage.setItem(this.maxBitrateKey,e?.toString())),e||void 0}async resetBitrate(){this.console.log("Resetting bitrate after adaptive streaming session",this.maxBitrate),this.needBitrateReset=!1,this.mixinDevice.setVideoStreamOptions({id:this.streamId,video:{bitrate:this.maxBitrate}})}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)||"";we.find((t=>e.startsWith(t)))||(e="");const t=-1!==e.indexOf(pe),i=-1!==e.indexOf(me),r=-1!==e.indexOf(fe);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 ve;const r=e&&"scrypted"===i?.tool?Se:ve,s=this.storage.getItem(this.rtspParserKey);return s&&s!==be?s===Se?Se:s===ve?ve:r:r}getRebroadcastMode(){let e=this.storage.getItem(this.rebroadcastModeKey)||"Default",t="MPEG-TS";"scrypted"===this.advertisedMediaStreamOptions?.tool&&this.advertisedMediaStreamOptions?.container?.startsWith("rtsp")&&(t="RTSP"),"Default"===e&&(e=t);const i=e?.startsWith("RTSP");return{defaultMode:t,rtspMode:e?.startsWith("RTSP"),muxingMp4:!i||e?.includes("MP4")}}async getMixinSettings(){const t=[],i=this.parserSession;let r=0,s=0;const{muxingMp4:n,rtspMode:o,defaultMode:a}=this.getRebroadcastMode();for(const e of n?this.prebuffers.mp4:this.prebuffers.rtsp){s=s||e.time;for(const t of e.chunk.chunks)r+=t.byteLength}const c=Date.now()-s,d=Math.round(r/c*8),u=this.streamName?`Rebroadcast: ${this.streamName}`:"Rebroadcast";t.push({title:"Audio Codec Transcoding",group:u,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)||le,choices:[le,he,"Compatible Audio (Copy)","Other Audio (Transcode)"]},{title:"Rebroadcast Container",group:u,description:`Experimental: The container format to use when rebroadcasting. MPEG-TS is stable. RTSP is lower latency. The default mode for this camera is ${a}.`,placeholder:"MPEG-TS",choices:[be,"MPEG-TS","RTSP"],key:this.rebroadcastModeKey,value:this.storage.getItem(this.rebroadcastModeKey)||be});const l=()=>{t.push({title:"FFmpeg Input Arguments Prefix",group:u,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:ye,choices:[ye,"-use_wallclock_as_timestamps 1","-v verbose"],combobox:!0})};if(this.canUseRtspParser(n,this.advertisedMediaStreamOptions)&&o&&"rtsp"===this.advertisedMediaStreamOptions?.container){const e=this.getParser(o,n,this.advertisedMediaStreamOptions);t.push({key:this.rtspParserKey,group:u,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 "${e}" for this camera.`,value:this.storage.getItem(this.rtspParserKey)||be,choices:[be,ve,Se]}),e!==Se&&l()}else l();return i?t.push({key:"detectedResolution",group:u,title:"Detected Resolution and Bitrate",readonly:!0,value:`${i?.inputVideoResolution?.[0]||"unknown"} @ ${d||"unknown"} Kb/s`,description:"Configuring your camera to 1920x1080, 2000Kb/S, Variable Bit Rate, is recommended."},{key:"detectedCodec",group:u,title:"Detected Video/Audio Codecs",readonly:!0,value:(i?.inputVideoCodec?.toString()||"unknown")+"/"+(i?.inputAudioCodec?.toString()||"unknown"),description:"Configuring your camera to H264 video and AAC/MP3/MP2/Opus audio is recommended."},{key:"detectedKeyframe",group:u,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"}):t.push({title:"Status",group:u,key:"status",description:"Rebroadcast is currently idle and will be started automatically on demand.",value:"Idle",readonly:!0}),o&&t.push({group:u,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}`}),this.mixin.mixinDeviceInterfaces.includes(e.ScryptedInterface.VideoCameraConfiguration)&&t.push({group:u,key:this.maxBitrateKey,title:"Max Bitrate",description:"This camera supports Adaptive Bitrate. Set the maximum bitrate to be allowed while using adaptive bitrate streaming. This will also serve as the default bitrate.",type:"number",value:this.maxBitrate?.toString()}),t}async startPrebufferSession(){let t;this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtsp=[];try{t=(await this.mixinDevice.getVideoStreamOptions()).find((e=>e.id===this.streamId))}catch(e){}const i=null===t?.audio,r=t?.audio?.codec,{isUsingDefaultAudioConfig:s,aacAudio:n,compatibleAudio:o,reencodeAudio:a}=this.getAudioConfig(),{rtspMode:d,muxingMp4:u}=this.getRebroadcastMode();let l=this.storage.getItem(this.lastDetectedAudioCodecKey)||void 0;"null"===l&&(l=null);let g=!1;u&&!i&&!r&&s&&void 0===l&&(this.console.warn("Camera did not report an audio codec, muting the audio stream and probing the codec."),g=!0);const y=void 0===l?r?.toLowerCase():l?.toLowerCase(),S=!ge.includes(y);!u||g||!1===t?.userConfigurable||i||S&&(s&&ce.a(`${this.mixin.name} is using the ${y} 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:",y));const P=["-bsf:a","aac_adtstoasc"],M=[];let x;this.audioDisabled=!1;const C=null===l;let T=!1;if(!g&&s&&S&&(!1===t?.userConfigurable?this.console.log("camera reports it is not user configurable. transcoding due to incompatible codec",y):this.console.log("camera audio transcoding due to incompatible codec. configure the camera to use a compatible codec if possible."),T=!0),i||g)x=["-an"],this.audioDisabled=!0;else if(a||T)x=["-bsf:a","aac_adtstoasc","-acodec","aac","-ar","32k","-b:a","32k","-ac","1","-profile:a","aac_low","-flags","+global_header"];else if(n||C)x=["-acodec","copy"],x.push(...P);else if(o)x=["-acodec","copy"],x.push(...M);else{x=["-acodec","copy"];const e="aac"===y?P:M;x.push(...e)}const O=["-vcodec","copy"],R={console:this.console,timeout:6e4,parsers:{}};if(this.parsers=R.parsers,this.console.log("rebroadcast mode:",d?"rtsp":"mpegts"),d){const e=function(e){let t;return{container:"rtsp",tcpProtocol:"rtsp://127.0.0.1/"+(0,B.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 J(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:s?["-acodec","copy"]:x});this.sdp=e.sdp,R.parsers.rtsp=e}else R.parsers.mpegts={container:"mpegts",outputArguments:[...(D={vcodec:O,acodec:x})?.vcodec||[],...D?.acodec||[],"-f","mpegts"],parse:(A=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,f.once)(e,"readable");continue}if(t.push(r),i+=r.length,i<A)continue;const s=Buffer.concat(t);_?.(s);const n=s.length%A,o=s.slice(0,s.length-n),a=s.slice(s.length-n);t=[a],i=a.length,yield{chunks:[o]}}}),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 D,A,_;u&&(R.parsers.mp4=function(e){return{container:"mp4",outputArguments:[...e?.vcodec||[],...e?.acodec||[],...w],async*parse(e){const t=b(e);yield*te(t)},findSyncFrame:ee}}({vcodec:O,acodec:x}));const V=await this.mixinDevice.getVideoStream(t),L="x-scrypted/x-rfc4571"===V.mimeType;let H,j;this.storage.removeItem(this.lastDetectedAudioCodecKey);let U=!1;if(d&&L){U=!0,this.console.log("bypassing ffmpeg: using scrypted rfc4571 parser");const e=await ae.convertMediaObjectToJSON(V,"x-scrypted/x-rfc4571"),{url:t,sdp:i,mediaStreamOptions:r}=e;H=await re(this.console,function(e){const t=new URL(e);if(!t.protocol.startsWith("tcp"))throw new Error("rfc4751 url must be tcp");return I().connect(parseInt(t.port),t.hostname)}(t),i,r,!1,R),this.sdp=H.sdp.then((e=>Buffer.concat(e).toString()))}else{const t=await ae.convertMediaObjectToBuffer(V,e.ScryptedMimeTypes.FFmpegInput),r=JSON.parse(t.toString());j=r.mediaStreamOptions;if(this.getParser(d,u,r.mediaStreamOptions)===Se){U=!0,this.console.log("bypassing ffmpeg: using scrypted rtsp/rfc4571 parser");const e=new z(r.url,this.console);try{e.requestTimeout=1e4,await e.options();let t=(await e.describe()).body.toString().trim();this.console.log("sdp",t);const s=function(e){const t=e.split("\n").map((e=>e.trim())),i=[],r=[];let s;for(const e of t)e.startsWith("m=")&&(s&&r.push(s),s=[]),s?s.push(e):i.push(e);return s&&r.push(s),{header:{lines:i,contents:i.join("\r\n")},msections:r.map(K)}}(t),n=s.msections.find((e=>"audio"===e.type)),o=s.msections.find((e=>"video"===e.type));s.msections=s.msections.filter((e=>e===n||e===o)),t=[...s.header.lines,...s.msections.map((e=>e.lines)).flat()].join("\r\n"),this.sdp=Promise.resolve(t);let a=0;i||(n?(await e.setup(a,n.control),a+=2):this.console.warn("sdp did not contain audio track and audio was not reported as missing.")),await e.setup(a,o.control),await e.play(),H=await re(this.console,e.rfc4571,t,r.mediaStreamOptions,!0,R);const c=H.kill.bind(H);let d=!1;if(H.kill=async()=>{d||(d=!0,e.teardown().finally(c)),await async function(e){await new Promise((t=>setTimeout(t,e)))}(500),e.client.destroy(),c()},!H.isActive)throw new Error("parser was killed before rtsp client started");e.readLoop().finally((()=>H.kill()))}catch(t){throw e.client.destroy(),t}}else{const e=this.storage.getItem(this.ffmpegInputArgumentsKey)||ye;r.inputArguments.unshift(...e.split(" ")),H=await E(r,R)}}U&&u&&this.getVideoStream({id:this.streamId,refresh:!1}).then((async t=>{const i=this.storage.getItem(this.ffmpegInputArgumentsKey)||ye,r=await ae.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,...w,"pipe:3"),s.unshift("-hide_banner"),m(r,s);const n=c().spawn(await v.getFFmpegPath(),s,{stdio:["pipe","pipe","pipe","pipe"]});return h(r,n),{cp:n,generator:b(n.stdio[3])}}(r.inputArguments,x,O,this.console),n=()=>{p(s.cp),H.kill(),s.generator.throw(new Error("killed"))};if(!H.isActive)return void n();H.killed.finally(n);const{resetActivityTimer:o}=k("mp4",n,H,R.timeout);for await(const e of te(s.generator))o(),H.emit("mp4",e)})).catch((()=>{})),!i&&r&&void 0!==H.inputAudioCodec&&H.inputAudioCodec!==r&&this.console.warn("Audio codec plugin reported vs detected mismatch",r,l);const N=t?.video?.codec;if(N&&void 0!==H.inputVideoCodec&&H.inputVideoCodec!==N&&this.console.warn("Video codec plugin reported vs detected mismatch",N,H.inputVideoCodec),H.inputAudioCodec?ge.includes(H.inputAudioCodec?.toLowerCase())?this.console.log("Detected audio codec is mp4/mpegts compatible.",H.inputAudioCodec):this.console.log("Detected audio codec is not mp4/mpegts compatible.",H.inputAudioCodec):this.console.log("No audio stream detected."),this.storage.setItem(this.lastDetectedAudioCodecKey,H.inputAudioCodec||"null"),"h264"!==H.inputVideoCodec&&this.console.error("Video codec is not h264. If there are errors, try changing your camera's encoder output."),g)return this.console.warn("Audio probe complete, ending rebroadcast session and restarting with detected codecs."),H.kill(),this.startPrebufferSession();if(this.parserSession=H,ue.onMixinEvent(this.mixin.id,this.mixin.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0),j?.refreshAt){let t,i=j;const r=async()=>{if(!H.isActive)return;const t=await this.mixinDevice.getVideoStream(i),r=await ae.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),H.killed.finally((()=>clearTimeout(t)))}H.killed.finally((()=>{clearTimeout(this.inactivityTimeout),this.parserSessionPromise=void 0,this.parserSession===H&&(this.parserSession=void 0)}));for(const t of Pe){let i=0;H.on(t,(r=>{const s=this.prebuffers[t],n=Date.now(),o=()=>{if(this.prevIdr){const t="number"!=typeof this.detectedIdrInterval;this.detectedIdrInterval=n-this.prevIdr,t&&ue.onMixinEvent(this.mixin.id,this.mixin.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0)}this.prevIdr=n};if("mdat"===r.type&&o(),"rtp-video"===r.type){const e=31&r.chunks[1].readUInt8(12),t=r.chunks[1].readUInt8(13),i=31&t,s=128&t;(28!==e&&29!==e||5!==i||128!=s)&&5!=e||o()}for(s.push({time:n,chunk:r});s.length&&s[0].time<n-1e4;)s.shift(),i++;i>1e3&&(this.prebuffers[t]=s.slice(),i=0)}))}return H}printActiveClients(){this.console.log(this.streamName,"active rebroadcast clients:",this.activeClients)}inactivityCheck(t,i){this.activeClients||(this.needBitrateReset&&this.mixin.mixinDeviceInterfaces.includes(e.ScryptedInterface.VideoCameraConfiguration)&&this.resetBitrate(),this.stopInactive&&(this.inactivityTimeout&&!i||(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"),t.kill())}),3e4))))}async handleRebroadcasterClient(e){const{isActiveClient:t,container:i,session:r,socketPromise:s,requestedPrebuffer:n}=e;n&&this.console.log("sending prebuffer",n),_(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=()=>{r.removeListener(i,a),r.removeListener("killed",c),s()};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.printActiveClients()),this.inactivityCheck(r,t),c()}}})}async getVideoStream(t){if(!1===t?.refresh&&!this.parserSessionPromise)throw new Error("Stream is currently unavailable and will not be started for this request. RequestMediaStreamOptions.refresh === false");this.ensurePrebufferSession();const i=await this.parserSessionPromise;let r=t?.prebuffer;null==r&&(r=1.5*Math.max(4e3,this.detectedIdrInterval||4e3));const{rtspMode:s}=this.getRebroadcastMode(),n=s?"rtsp":"mpegts";let o=this.parsers[t?.container]?t?.container:n;t?.prebuffer&&"mp4"!==o&&"mp4"===t?.container&&(r+=1.5*(this.detectedIdrInterval||4e3));const a=Object.assign({},i.mediaStreamOptions);a.sdp=(await i.sdp)?.toString(),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=i?.mediaStreamOptions?.audio?.codec===i?.inputAudioCodec?i?.mediaStreamOptions?.audio:{codec:i?.inputAudioCodec}),a.video||(a.video={}),a.video.codec=i.inputVideoCodec,i.inputVideoResolution?.[2]&&i.inputVideoResolution?.[3]&&Object.assign(a.video,{width:parseInt(i.inputVideoResolution[2]),height:parseInt(i.inputVideoResolution[3])});const u=Date.now();let l=0;const p=this.prebuffers[o];for(const e of p)if(!(e.time<u-r))for(const t of e.chunk.chunks)l+=t.length;const h=Math.max(5e5,l).toString(),m=await(async e=>{let s,n;if("rtsp"===e){const e=await R();s=e.clientPromise.then((async e=>{let t=await this.sdp;t=H(t);const i=new J(e,t);return await i.handlePlayback(),e})),n=e.url.replace("tcp://","rtsp://")}else{const e=await R();s=e.clientPromise,n=`tcp://127.0.0.1:${e.port}`}const o=!1!==t?.refresh;return this.handleRebroadcasterClient({isActiveClient:o,container:e,requestedPrebuffer:r,socketPromise:s,session:i}),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 this.mixin.createMediaObject(f,e.ScryptedMimeTypes.FFmpegInput)}}class Me extends Z{released=!1;sessions=new Map;streamSettings=oe(this);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){if(e?.directMediaStream)return this.mixinDevice.getVideoStream(e);await this.ensurePrebufferSessions();let t=e?.id;if(!t&&e?.destination){const i=await this.mixinDevice.getVideoStreamOptions();let r;switch(e.destination){case"remote":r=this.streamSettings.getRemoteStream(i);break;case"low-resolution":r=this.streamSettings.getLowResolutionStream(i);break;case"local-recorder":r=this.streamSettings.getRecordingStream(i);break;case"remote-recorder":r=this.streamSettings.getRemoteRecordingStream(i);break;default:r=this.streamSettings.getDefaultStream(i)}t=r?.id,t&&this.console.log("Selected Stream",{destination:e.destination,id:r.id,name:r.name})}const i=this.sessions.get(t);return i?i.getVideoStream(e):this.mixinDevice.getVideoStream(e)}async ensurePrebufferSessions(){const t=await this.mixinDevice.getVideoStreamOptions(),i=this.getPrebufferedStreams(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"),ce.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 n=this.mixinDeviceInterfaces.includes(e.ScryptedInterface.Battery);let o=0;for(const e of s){let i=this.sessions.get(e);if(!i){const s=t?.find((t=>t.id===e));s?.prebuffer&&ce.a(`Prebuffer is already available on ${this.name}. If this is a grouped device, disable the Rebroadcast extension.`);const a=s?.name,c=r.includes(e);if(i=new Ie(this,s,n||!c),this.sessions.set(e,i),n){this.console.log("camera is battery powered, prebuffering and rebroadcasting will only work on demand.");continue}if(!c){this.console.log("stream",a,"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;o++,e=!0,this.online=!!o,await t.killed,this.console.error("prebuffer session ended")}catch(e){this.console.error("prebuffer session ended with error",e)}finally{e&&o--,e=!1,this.online=!!o}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)")})()}}if(!this.sessions.has(void 0)){const e=this.streamSettings.storageSettings.values.defaultStream;let i=this.sessions.get(t?.find((t=>t.name===e))?.id);i||(i=this.sessions.get(t?.find((e=>e.id===r[0]))?.id)),i||(i=this.sessions.get(t?.find((e=>e.id===s?.[0]))?.id)),i?(this.sessions.set(void 0,i),this.console.log("Default Stream:",i.advertisedMediaStreamOptions.id,i.advertisedMediaStreamOptions.name)):this.console.warn("Unable to find Default Stream?")}ue.onMixinEvent(this.id,this.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0)}async getMixinSettings(){const e=[];e.push(...await this.streamSettings.storageSettings.getSettings());for(const t of new Set([...this.sessions.values()]))if(t)try{e.push(...await t.getMixinSettings())}catch(e){this.console.error("error in prebuffer session getMixinSettings",e)}return e}async putMixinSetting(e,t){const i=this.sessions;this.sessions=new Map,this.streamSettings.storageSettings.settings[e]?await this.streamSettings.storageSettings.putSetting(e,t):this.storage.setItem(e,t?.toString());for(const e of i.values())e?.parserSessionPromise?.then((e=>e.kill()));this.ensurePrebufferSessions()}getPrebufferedStreams(e){return ne(this.streamSettings.storageSettings,e)}async getVideoStreamOptions(){const e=await this.mixinDevice.getVideoStreamOptions()||[];let t=this.getPrebufferedStreams(e);for(const i of e)(this.sessions.get(i.id)?.parserSession||t.includes(i))&&(i.prebuffer=1e4);return e}setVideoStreamOptions(e){const t=this.sessions.get(e.id);if(t&&e?.video?.bitrate){t.needBitrateReset=!0;const i=t.maxBitrate;i&&e?.video?.bitrate>i&&(this.console.log("clamping max bitrate request",e.video.bitrate,i),e.video.bitrate=i)}return this.mixinDevice.setVideoStreamOptions(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 xe=new class extends o{storageSettings=new Y(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(de.getSystemState())){const t=de.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((()=>ue.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(I().Server)((async e=>{let t;const i=new J(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,t=e,t.ensurePrebufferSession(),await t.parserSessionPromise,i.sdp=await t.sdp,!0}return!1}));this.console.log("RTSP Rebroadcast connection started."),i.console=this.console;try{await i.handlePlayback();const r=await t.parserSessionPromise,s=1.5*Math.max(4e3,t.detectedIdrInterval||4e3);t.handleRebroadcasterClient({isActiveClient:!0,container:"rtsp",session:r,socketPromise:Promise.resolve(e),requestedPrebuffer:s}),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),c=new URL(s);if(!c.protocol.startsWith("tcp"))throw new Error("rfc4751 url must be tcp");const{clientPromise:d,url:u}=await R(),l={url:u,inputArguments:["-rtsp_transport","tcp","-i",u.replace("tcp","rtsp")]};return d.then((async e=>{const t=new J(e,n);await t.handlePlayback();const i=I().connect(parseInt(c.port),c.hostname);for(e.on("close",(()=>{i.destroy()})),i.on("close",(()=>{e.destroy()}));;){const r=(await g(i,2)).readInt16BE(0),s=await g(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){if(!i.includes(e.ScryptedInterface.VideoCamera))return null;const r=[e.ScryptedInterface.VideoCamera,e.ScryptedInterface.Settings,e.ScryptedInterface.Online];return i.includes(e.ScryptedInterface.VideoCameraConfiguration)&&r.push(e.ScryptedInterface.VideoCameraConfiguration),r}async getMixin(e,t,i){this.setHasEnabledMixin(i.id);const r=new Me(this,{mixinDevice:e,mixinDeviceState:i,mixinProviderNativeId:this.nativeId,mixinDeviceInterfaces:t,group:"Stream Selection",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
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
|
|
2
|
-
import { MixinProvider, ScryptedDeviceType, ScryptedInterface, MediaObject, VideoCamera, MediaStreamOptions, Settings, Setting, ScryptedMimeTypes, FFMpegInput, RequestMediaStreamOptions, BufferConverter, ResponseMediaStreamOptions, VideoCameraConfiguration } from '@scrypted/sdk';
|
|
3
|
-
import sdk from '@scrypted/sdk';
|
|
4
|
-
import { once } from 'events';
|
|
5
|
-
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
|
|
6
|
-
import { handleRebroadcasterClient, ParserOptions, ParserSession, setupActivityTimer, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
|
|
7
|
-
import { createMpegTsParser, createFragmentedMp4Parser, StreamChunk, StreamParser, MP4Atom, parseMp4StreamChunks } from '@scrypted/common/src/stream-parser';
|
|
8
2
|
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
|
9
3
|
import { startFFMPegFragmentedMP4Session } from '@scrypted/common/src/ffmpeg-mp4-parser-session';
|
|
4
|
+
import { handleRebroadcasterClient, ParserOptions, ParserSession, setupActivityTimer, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
|
|
10
5
|
import { closeQuiet, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
|
11
|
-
import {
|
|
12
|
-
import { createRtspParser, RtspClient, RtspServer } from '@scrypted/common/src/rtsp-server';
|
|
13
|
-
import { Duplex } from 'stream';
|
|
14
|
-
import net from 'net';
|
|
6
|
+
import { safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
|
|
15
7
|
import { readLength } from '@scrypted/common/src/read-stream';
|
|
8
|
+
import { createRtspParser, RtspClient, RtspServer } from '@scrypted/common/src/rtsp-server';
|
|
9
|
+
import { addTrackControls, parsePayloadTypes, findTracksByType, parseSdp } from '@scrypted/common/src/sdp-utils';
|
|
16
10
|
import { StorageSettings } from '@scrypted/common/src/settings';
|
|
17
|
-
import {
|
|
18
|
-
import { connectRFC4571Parser, startRFC4571Parser } from './rfc4571';
|
|
11
|
+
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
|
|
19
12
|
import { sleep } from '@scrypted/common/src/sleep';
|
|
13
|
+
import { createFragmentedMp4Parser, createMpegTsParser, parseMp4StreamChunks, StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
|
|
14
|
+
import sdk, { BufferConverter, FFMpegInput, MediaObject, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from '@scrypted/sdk';
|
|
20
15
|
import crypto from 'crypto';
|
|
21
|
-
import
|
|
16
|
+
import net from 'net';
|
|
17
|
+
import { Duplex } from 'stream';
|
|
18
|
+
import { connectRFC4571Parser, startRFC4571Parser } from './rfc4571';
|
|
19
|
+
import { createStreamSettings, getPrebufferedStreams } from './stream-settings';
|
|
22
20
|
|
|
23
21
|
const { mediaManager, log, systemManager, deviceManager } = sdk;
|
|
24
22
|
|
|
25
|
-
const
|
|
26
|
-
const PREBUFFER_DURATION_MS = 'prebufferDuration';
|
|
27
|
-
const SEND_KEYFRAME = 'sendKeyframe';
|
|
23
|
+
const prebufferDurationMs = 10000;
|
|
28
24
|
const DEFAULT_AUDIO = 'Default';
|
|
29
25
|
const AAC_AUDIO = 'AAC or No Audio';
|
|
30
26
|
const AAC_AUDIO_DESCRIPTION = `${AAC_AUDIO} (Copy)`;
|
|
@@ -390,7 +386,6 @@ class PrebufferSession {
|
|
|
390
386
|
this.prebuffers.mp4 = [];
|
|
391
387
|
this.prebuffers.mpegts = [];
|
|
392
388
|
this.prebuffers.rtsp = [];
|
|
393
|
-
const prebufferDurationMs = parseInt(this.storage.getItem(PREBUFFER_DURATION_MS)) || defaultPrebufferDuration;
|
|
394
389
|
|
|
395
390
|
let mso: ResponseMediaStreamOptions;
|
|
396
391
|
try {
|
|
@@ -613,16 +608,29 @@ class PrebufferSession {
|
|
|
613
608
|
rtspClient.requestTimeout = 10000;
|
|
614
609
|
await rtspClient.options();
|
|
615
610
|
const sdpResponse = await rtspClient.describe();
|
|
616
|
-
|
|
611
|
+
let sdp = sdpResponse.body.toString().trim();
|
|
617
612
|
this.console.log('sdp', sdp);
|
|
613
|
+
|
|
614
|
+
const parsedSdp = parseSdp(sdp);
|
|
615
|
+
const audioSection = parsedSdp.msections.find(msection => msection.type === 'audio');
|
|
616
|
+
const videoSection = parsedSdp.msections.find(msection => msection.type === 'video');
|
|
617
|
+
|
|
618
|
+
// sdp may contain multiple audio/video sections. take only the first.
|
|
619
|
+
parsedSdp.msections = parsedSdp.msections.filter(msection => msection === audioSection || msection === videoSection);
|
|
620
|
+
sdp = [...parsedSdp.header.lines, ...parsedSdp.msections.map(msection => msection.lines).flat()].join('\r\n');
|
|
621
|
+
|
|
618
622
|
this.sdp = Promise.resolve(sdp);
|
|
619
|
-
const { audio, video } = parseTrackIds(sdp);
|
|
620
623
|
let channel = 0;
|
|
621
624
|
if (!audioSoftMuted) {
|
|
622
|
-
|
|
623
|
-
|
|
625
|
+
if (audioSection) {
|
|
626
|
+
await rtspClient.setup(channel, audioSection.control);
|
|
627
|
+
channel += 2;
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
this.console.warn('sdp did not contain audio track and audio was not reported as missing.');
|
|
631
|
+
}
|
|
624
632
|
}
|
|
625
|
-
await rtspClient.setup(channel,
|
|
633
|
+
await rtspClient.setup(channel, videoSection.control);
|
|
626
634
|
await rtspClient.play();
|
|
627
635
|
session = await startRFC4571Parser(this.console, rtspClient.rfc4571, sdp, ffmpegInput.mediaStreamOptions, true, rbo);
|
|
628
636
|
const sessionKill = session.kill.bind(session);
|
|
@@ -859,6 +867,8 @@ class PrebufferSession {
|
|
|
859
867
|
requestedPrebuffer: number,
|
|
860
868
|
}) {
|
|
861
869
|
const { isActiveClient, container, session, socketPromise, requestedPrebuffer } = options;
|
|
870
|
+
if (requestedPrebuffer)
|
|
871
|
+
this.console.log('sending prebuffer', requestedPrebuffer);
|
|
862
872
|
|
|
863
873
|
handleRebroadcasterClient(socketPromise, {
|
|
864
874
|
// console: this.console,
|
|
@@ -928,16 +938,10 @@ class PrebufferSession {
|
|
|
928
938
|
|
|
929
939
|
const session = await this.parserSessionPromise;
|
|
930
940
|
|
|
931
|
-
const sendKeyframe = this.storage.getItem(SEND_KEYFRAME) !== 'false';
|
|
932
941
|
let requestedPrebuffer = options?.prebuffer;
|
|
933
942
|
if (requestedPrebuffer == null) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
requestedPrebuffer = Math.max(4000, (this.detectedIdrInterval || 4000)) * 1.5;
|
|
937
|
-
}
|
|
938
|
-
else {
|
|
939
|
-
requestedPrebuffer = 0;
|
|
940
|
-
}
|
|
943
|
+
// get into the general area of finding a sync frame.
|
|
944
|
+
requestedPrebuffer = Math.max(4000, (this.detectedIdrInterval || 4000)) * 1.5;
|
|
941
945
|
}
|
|
942
946
|
|
|
943
947
|
const { rtspMode } = this.getRebroadcastMode();
|
|
@@ -988,6 +992,7 @@ class PrebufferSession {
|
|
|
988
992
|
}
|
|
989
993
|
|
|
990
994
|
const mediaStreamOptions: ResponseMediaStreamOptions = Object.assign({}, session.mediaStreamOptions);
|
|
995
|
+
mediaStreamOptions.sdp = (await session.sdp)?.toString();
|
|
991
996
|
|
|
992
997
|
mediaStreamOptions.prebuffer = requestedPrebuffer;
|
|
993
998
|
|
|
@@ -1059,8 +1064,7 @@ class PrebufferSession {
|
|
|
1059
1064
|
mediaStreamOptions,
|
|
1060
1065
|
}
|
|
1061
1066
|
|
|
1062
|
-
|
|
1063
|
-
return mo;
|
|
1067
|
+
return this.mixin.createMediaObject(ffmpegInput, ScryptedMimeTypes.FFmpegInput);
|
|
1064
1068
|
}
|
|
1065
1069
|
}
|
|
1066
1070
|
|
|
@@ -1068,6 +1072,8 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
|
|
|
1068
1072
|
released = false;
|
|
1069
1073
|
sessions = new Map<string, PrebufferSession>();
|
|
1070
1074
|
|
|
1075
|
+
streamSettings = createStreamSettings(this);
|
|
1076
|
+
|
|
1071
1077
|
constructor(public plugin: PrebufferProvider, options: SettingsMixinDeviceOptions<VideoCamera & VideoCameraConfiguration>) {
|
|
1072
1078
|
super(options);
|
|
1073
1079
|
|
|
@@ -1086,7 +1092,38 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
|
|
|
1086
1092
|
|
|
1087
1093
|
await this.ensurePrebufferSessions();
|
|
1088
1094
|
|
|
1089
|
-
|
|
1095
|
+
let id = options?.id;
|
|
1096
|
+
if (!id && options?.destination) {
|
|
1097
|
+
const msos = await this.mixinDevice.getVideoStreamOptions();
|
|
1098
|
+
let selected: ResponseMediaStreamOptions;
|
|
1099
|
+
switch (options.destination) {
|
|
1100
|
+
case 'remote':
|
|
1101
|
+
selected = this.streamSettings.getRemoteStream(msos);
|
|
1102
|
+
break;
|
|
1103
|
+
case 'low-resolution':
|
|
1104
|
+
selected = this.streamSettings.getLowResolutionStream(msos);
|
|
1105
|
+
break;
|
|
1106
|
+
case 'local-recorder':
|
|
1107
|
+
selected = this.streamSettings.getRecordingStream(msos);
|
|
1108
|
+
break;
|
|
1109
|
+
case 'remote-recorder':
|
|
1110
|
+
selected = this.streamSettings.getRemoteRecordingStream(msos);
|
|
1111
|
+
break;
|
|
1112
|
+
default:
|
|
1113
|
+
selected = this.streamSettings.getDefaultStream(msos);
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
id = selected?.id;
|
|
1118
|
+
if (id) {
|
|
1119
|
+
this.console.log('Selected Stream', {
|
|
1120
|
+
destination: options.destination,
|
|
1121
|
+
id: selected.id,
|
|
1122
|
+
name: selected.name,
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1090
1127
|
const session = this.sessions.get(id);
|
|
1091
1128
|
if (!session)
|
|
1092
1129
|
return this.mixinDevice.getVideoStream(options);
|
|
@@ -1095,7 +1132,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
|
|
|
1095
1132
|
|
|
1096
1133
|
async ensurePrebufferSessions() {
|
|
1097
1134
|
const msos = await this.mixinDevice.getVideoStreamOptions();
|
|
1098
|
-
const enabled = this.
|
|
1135
|
+
const enabled = this.getPrebufferedStreams(msos);
|
|
1099
1136
|
const enabledIds = enabled ? enabled.map(mso => mso.id) : [undefined];
|
|
1100
1137
|
const ids = msos?.map(mso => mso.id) || [undefined];
|
|
1101
1138
|
|
|
@@ -1163,7 +1200,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
|
|
|
1163
1200
|
}
|
|
1164
1201
|
|
|
1165
1202
|
if (!this.sessions.has(undefined)) {
|
|
1166
|
-
const defaultStreamName = this.
|
|
1203
|
+
const defaultStreamName = this.streamSettings.storageSettings.values.defaultStream;
|
|
1167
1204
|
let defaultSession = this.sessions.get(msos?.find(mso => mso.name === defaultStreamName)?.id);
|
|
1168
1205
|
if (!defaultSession)
|
|
1169
1206
|
defaultSession = this.sessions.get(msos?.find(mso => mso.id === enabledIds[0])?.id);
|
|
@@ -1185,52 +1222,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
|
|
|
1185
1222
|
async getMixinSettings(): Promise<Setting[]> {
|
|
1186
1223
|
const settings: Setting[] = [];
|
|
1187
1224
|
|
|
1188
|
-
|
|
1189
|
-
const msos = await this.mixinDevice.getVideoStreamOptions();
|
|
1190
|
-
const enabledStreams = this.getEnabledMediaStreamOptions(msos);
|
|
1191
|
-
if (msos?.length > 0) {
|
|
1192
|
-
settings.push(
|
|
1193
|
-
{
|
|
1194
|
-
title: 'Default Stream',
|
|
1195
|
-
description: 'The default stream to use when not specified.',
|
|
1196
|
-
key: 'defaultStream',
|
|
1197
|
-
value: this.storage.getItem('defaultStream') || enabledStreams?.[0]?.name || msos[0].name,
|
|
1198
|
-
choices: msos.map(mso => mso.name),
|
|
1199
|
-
},
|
|
1200
|
-
{
|
|
1201
|
-
title: 'Prebuffered Streams',
|
|
1202
|
-
description: 'The streams to prebuffer. Enable only as necessary to reduce traffic.',
|
|
1203
|
-
key: 'enabledStreams',
|
|
1204
|
-
value: enabledStreams.map(mso => mso.name || ''),
|
|
1205
|
-
choices: msos.map(mso => mso.name),
|
|
1206
|
-
multiple: true,
|
|
1207
|
-
},
|
|
1208
|
-
)
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
catch (e) {
|
|
1212
|
-
this.console.error('error in getVideoStreamOptions', e);
|
|
1213
|
-
throw e;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
settings.push(
|
|
1218
|
-
{
|
|
1219
|
-
title: 'Prebuffer Duration',
|
|
1220
|
-
description: 'Duration of the prebuffer in milliseconds.',
|
|
1221
|
-
type: 'number',
|
|
1222
|
-
key: PREBUFFER_DURATION_MS,
|
|
1223
|
-
value: this.storage.getItem(PREBUFFER_DURATION_MS) || defaultPrebufferDuration.toString(),
|
|
1224
|
-
},
|
|
1225
|
-
{
|
|
1226
|
-
title: 'Start at Previous Keyframe',
|
|
1227
|
-
description: 'Start live streams from the previous key frame. Improves startup time.',
|
|
1228
|
-
type: 'boolean',
|
|
1229
|
-
key: SEND_KEYFRAME,
|
|
1230
|
-
value: (this.storage.getItem(SEND_KEYFRAME) !== 'false').toString(),
|
|
1231
|
-
},
|
|
1232
|
-
);
|
|
1233
|
-
|
|
1225
|
+
settings.push(...await this.streamSettings.storageSettings.getSettings());
|
|
1234
1226
|
|
|
1235
1227
|
for (const session of new Set([...this.sessions.values()])) {
|
|
1236
1228
|
if (!session)
|
|
@@ -1240,53 +1232,37 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera & VideoCameraCo
|
|
|
1240
1232
|
}
|
|
1241
1233
|
catch (e) {
|
|
1242
1234
|
this.console.error('error in prebuffer session getMixinSettings', e);
|
|
1243
|
-
throw e;
|
|
1244
1235
|
}
|
|
1245
1236
|
}
|
|
1246
1237
|
|
|
1247
1238
|
return settings;
|
|
1248
1239
|
}
|
|
1249
1240
|
|
|
1250
|
-
async putMixinSetting(key: string, value:
|
|
1241
|
+
async putMixinSetting(key: string, value: SettingValue): Promise<void> {
|
|
1251
1242
|
const sessions = this.sessions;
|
|
1252
1243
|
this.sessions = new Map();
|
|
1253
|
-
if (key
|
|
1254
|
-
this.
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
}
|
|
1244
|
+
if (this.streamSettings.storageSettings.settings[key])
|
|
1245
|
+
await this.streamSettings.storageSettings.putSetting(key, value);
|
|
1246
|
+
else
|
|
1247
|
+
this.storage.setItem(key, value?.toString());
|
|
1248
|
+
|
|
1259
1249
|
for (const session of sessions.values()) {
|
|
1260
1250
|
session?.parserSessionPromise?.then(session => session.kill());
|
|
1261
1251
|
}
|
|
1262
1252
|
this.ensurePrebufferSessions();
|
|
1263
1253
|
}
|
|
1264
1254
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
return;
|
|
1268
|
-
|
|
1269
|
-
try {
|
|
1270
|
-
const parsed: any[] = JSON.parse(this.storage.getItem('enabledStreams'));
|
|
1271
|
-
const filtered = msos.filter(mso => parsed.includes(mso.name));
|
|
1272
|
-
return filtered;
|
|
1273
|
-
}
|
|
1274
|
-
catch (e) {
|
|
1275
|
-
}
|
|
1276
|
-
// do not enable rebroadcast on cloud streams by default.
|
|
1277
|
-
const firstNonCloudStream = msos.find(mso => mso.source !== 'cloud');
|
|
1278
|
-
return firstNonCloudStream ? [firstNonCloudStream] : [];
|
|
1255
|
+
getPrebufferedStreams(msos?: ResponseMediaStreamOptions[]) {
|
|
1256
|
+
return getPrebufferedStreams(this.streamSettings.storageSettings, msos);
|
|
1279
1257
|
}
|
|
1280
1258
|
|
|
1281
1259
|
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
|
|
1282
1260
|
const ret: ResponseMediaStreamOptions[] = await this.mixinDevice.getVideoStreamOptions() || [];
|
|
1283
|
-
let enabledStreams = this.
|
|
1284
|
-
|
|
1285
|
-
const prebuffer = parseInt(this.storage.getItem(PREBUFFER_DURATION_MS)) || defaultPrebufferDuration;
|
|
1261
|
+
let enabledStreams = this.getPrebufferedStreams(ret);
|
|
1286
1262
|
|
|
1287
1263
|
for (const mso of ret) {
|
|
1288
1264
|
if (this.sessions.get(mso.id)?.parserSession || enabledStreams.includes(mso))
|
|
1289
|
-
mso.prebuffer =
|
|
1265
|
+
mso.prebuffer = prebufferDurationMs;
|
|
1290
1266
|
}
|
|
1291
1267
|
|
|
1292
1268
|
return ret;
|
|
@@ -1500,7 +1476,7 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
|
|
|
1500
1476
|
mixinDeviceState,
|
|
1501
1477
|
mixinProviderNativeId: this.nativeId,
|
|
1502
1478
|
mixinDeviceInterfaces,
|
|
1503
|
-
group: "
|
|
1479
|
+
group: "Stream Selection",
|
|
1504
1480
|
groupKey: "prebuffer",
|
|
1505
1481
|
});
|
|
1506
1482
|
this.currentMixins.set(mixinDeviceState.id, ret);
|
package/src/rfc4571.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { EventEmitter, Readable } from "stream";
|
|
|
5
5
|
import net from 'net';
|
|
6
6
|
import { StreamChunk } from "@scrypted/common/src/stream-parser";
|
|
7
7
|
import { parseHeaders, readMessage, RTSP_FRAME_MAGIC } from "@scrypted/common/src/rtsp-server";
|
|
8
|
-
import {
|
|
8
|
+
import { findTrackByType } from "@scrypted/common/src/sdp-utils";
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
const { mediaManager } = sdk;
|
|
@@ -95,8 +95,8 @@ export async function startRFC4571Parser(console: Console, socket: Readable, sdp
|
|
|
95
95
|
let inputAudioCodec: string;
|
|
96
96
|
let inputVideoCodec: string;
|
|
97
97
|
// todo: multiple codecs may be offered, default is the first one in the sdp.
|
|
98
|
-
const audio =
|
|
99
|
-
const video =
|
|
98
|
+
const audio = findTrackByType(sdp, 'audio');
|
|
99
|
+
const video = findTrackByType(sdp, 'video');
|
|
100
100
|
if (audio) {
|
|
101
101
|
const lc = audio.section.toLowerCase();
|
|
102
102
|
if (lc.includes('mpeg4'))
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { StorageSetting, StorageSettings, StorageSettingsDict } from "@scrypted/common/src/settings";
|
|
2
|
+
import { MixinDeviceBase, ResponseMediaStreamOptions, VideoCamera } from "@scrypted/sdk";
|
|
3
|
+
|
|
4
|
+
export function getDefaultPrebufferedStreams(msos: ResponseMediaStreamOptions[]) {
|
|
5
|
+
if (!msos)
|
|
6
|
+
return;
|
|
7
|
+
|
|
8
|
+
// do not enable rebroadcast on cloud streams by default.
|
|
9
|
+
const firstNonCloudStream = msos.find(mso => mso.source !== 'cloud');
|
|
10
|
+
return firstNonCloudStream ? [firstNonCloudStream] : [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getPrebufferedStreams(storageSettings: StorageSettings<'enabledStreams'>, msos: ResponseMediaStreamOptions[]) {
|
|
14
|
+
if (!msos)
|
|
15
|
+
return;
|
|
16
|
+
|
|
17
|
+
if (!storageSettings.hasValue.enabledStreams)
|
|
18
|
+
return getDefaultPrebufferedStreams(msos);
|
|
19
|
+
|
|
20
|
+
return msos.filter(mso => storageSettings.values.enabledStreams.includes(mso.name));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type StreamStorageSetting = StorageSetting & {
|
|
24
|
+
prefersPrebuffer: boolean,
|
|
25
|
+
preferredResolution: number,
|
|
26
|
+
};
|
|
27
|
+
export type StreamStorageSettingsDict<T extends string> = { [key in T]: StreamStorageSetting };
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
function getStreamTypes<T extends string>(storageSettings: StreamStorageSettingsDict<T>) {
|
|
31
|
+
return storageSettings;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pickBestStream(msos: ResponseMediaStreamOptions[], resolution: number) {
|
|
35
|
+
if (!msos)
|
|
36
|
+
return;
|
|
37
|
+
|
|
38
|
+
let best: ResponseMediaStreamOptions;
|
|
39
|
+
let bestScore: number;
|
|
40
|
+
for (const mso of msos) {
|
|
41
|
+
const score = Math.abs(mso.video?.width * mso.video?.height - resolution);
|
|
42
|
+
if (!best || score < bestScore) {
|
|
43
|
+
best = mso;
|
|
44
|
+
bestScore = score;
|
|
45
|
+
if (Number.isNaN(bestScore))
|
|
46
|
+
bestScore = Number.MAX_SAFE_INTEGER;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return best;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
|
54
|
+
const streamTypes = getStreamTypes({
|
|
55
|
+
defaultStream: {
|
|
56
|
+
title: 'Local Stream',
|
|
57
|
+
description: 'The media stream to use when streaming on your local network. This is the default stream. Recommended resolution: 1920x1080 to 4K.',
|
|
58
|
+
hide: true,
|
|
59
|
+
prefersPrebuffer: true,
|
|
60
|
+
preferredResolution: 3840 * 2160,
|
|
61
|
+
},
|
|
62
|
+
remoteStream: {
|
|
63
|
+
title: 'Remote/Mid Stream',
|
|
64
|
+
description: 'The media stream to use when streaming from outside your local network. Selecting a low birate stream is recommended. Recommended resolution: 1270x720.',
|
|
65
|
+
hide: true,
|
|
66
|
+
prefersPrebuffer: false,
|
|
67
|
+
preferredResolution: 1280 * 720,
|
|
68
|
+
},
|
|
69
|
+
lowResolutionStream: {
|
|
70
|
+
title: 'Low Resolution Stream',
|
|
71
|
+
description: 'The media stream to use for low resolution output, such as Apple Watch and Video Analysis. Recommended resolution: 480x360.',
|
|
72
|
+
hide: true,
|
|
73
|
+
prefersPrebuffer: false,
|
|
74
|
+
preferredResolution: 480 * 320,
|
|
75
|
+
},
|
|
76
|
+
recordingStream: {
|
|
77
|
+
title: 'Local Recording Stream',
|
|
78
|
+
description: 'The media stream to use when recording to local storage such as an NVR.',
|
|
79
|
+
hide: true,
|
|
80
|
+
prefersPrebuffer: true,
|
|
81
|
+
preferredResolution: 3840 * 2160,
|
|
82
|
+
},
|
|
83
|
+
remoteRecordingStream: {
|
|
84
|
+
title: 'Remote Recording Stream',
|
|
85
|
+
description: 'The media stream to use when recording to cloud storage such as HomeKit Secure Video clips in iCloud.',
|
|
86
|
+
hide: true,
|
|
87
|
+
prefersPrebuffer: true,
|
|
88
|
+
preferredResolution: 1280 * 720,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const storageSettings = new StorageSettings(device, {
|
|
93
|
+
enabledStreams: {
|
|
94
|
+
title: 'Prebuffered Streams',
|
|
95
|
+
description: 'Prebuffering maintains an active connection to the stream and improves load times. Prebuffer also retains the recent video for capturing motion events with HomeKit Secure video. Enabling Prebuffer is not recommended on Cloud cameras.',
|
|
96
|
+
multiple: true,
|
|
97
|
+
hide: true,
|
|
98
|
+
},
|
|
99
|
+
...streamTypes,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function getMediaStream(v: StreamStorageSetting, msos: ResponseMediaStreamOptions[]) {
|
|
103
|
+
const enabledStreams = getPrebufferedStreams(storageSettings, msos);
|
|
104
|
+
const prebufferPreferenceStreams = v.prefersPrebuffer && enabledStreams?.length > 0 ? enabledStreams : msos;
|
|
105
|
+
return pickBestStream(prebufferPreferenceStreams, v.preferredResolution);
|
|
106
|
+
};
|
|
107
|
+
function createStreamOptions(v: StreamStorageSetting, msos: ResponseMediaStreamOptions[]) {
|
|
108
|
+
const choices = msos.map(mso => mso.name);
|
|
109
|
+
|
|
110
|
+
const streamOptions = {
|
|
111
|
+
defaultValue: getMediaStream(v, msos)?.name,
|
|
112
|
+
choices,
|
|
113
|
+
hide: false,
|
|
114
|
+
};
|
|
115
|
+
return streamOptions;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
storageSettings.options = {
|
|
119
|
+
onGet: async () => {
|
|
120
|
+
try {
|
|
121
|
+
const msos = await device.mixinDevice.getVideoStreamOptions();
|
|
122
|
+
|
|
123
|
+
if (msos?.length > 1) {
|
|
124
|
+
const choices = msos.map(mso => mso.name);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
enabledStreams: {
|
|
128
|
+
defaultValue: getDefaultPrebufferedStreams(msos)?.map(mso => mso.name),
|
|
129
|
+
choices,
|
|
130
|
+
hide: false,
|
|
131
|
+
},
|
|
132
|
+
defaultStream: createStreamOptions(streamTypes.defaultStream, msos),
|
|
133
|
+
remoteStream: createStreamOptions(streamTypes.remoteStream, msos),
|
|
134
|
+
lowResolutionStream: createStreamOptions(streamTypes.lowResolutionStream, msos),
|
|
135
|
+
recordingStream: createStreamOptions(streamTypes.recordingStream, msos),
|
|
136
|
+
remoteRecordingStream: createStreamOptions(streamTypes.remoteRecordingStream, msos),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
device.console.error('error retrieving getVideoStreamOptions', e);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
getDefaultStream: (msos: ResponseMediaStreamOptions[]) => getMediaStream(streamTypes.defaultStream, msos),
|
|
151
|
+
getRemoteStream: (msos: ResponseMediaStreamOptions[]) => getMediaStream(streamTypes.remoteStream, msos),
|
|
152
|
+
getLowResolutionStream: (msos: ResponseMediaStreamOptions[]) => getMediaStream(streamTypes.lowResolutionStream, msos),
|
|
153
|
+
getRecordingStream: (msos: ResponseMediaStreamOptions[]) => getMediaStream(streamTypes.recordingStream, msos),
|
|
154
|
+
getRemoteRecordingStream: (msos: ResponseMediaStreamOptions[]) => getMediaStream(streamTypes.remoteRecordingStream, msos),
|
|
155
|
+
storageSettings,
|
|
156
|
+
};
|
|
157
|
+
}
|