@scrypted/prebuffer-mixin 0.1.146 → 0.1.150

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.
@@ -1,2 +1,2 @@
1
- (()=>{var e={454:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.AutoenableMixinProvider=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var i=o(t);if(i&&i.has(e))return i.get(e);var r={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in e)if("default"!==s&&Object.prototype.hasOwnProperty.call(e,s)){var a=n?Object.getOwnPropertyDescriptor(e,s):null;a&&(a.get||a.set)?Object.defineProperty(r,s,a):r[s]=e[s]}r.default=e,i&&i.set(e,r);return r}(i(510));function o(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,i=new WeakMap;return(o=function(e){return e?i:t})(e)}const{systemManager:n}=r.default,s="v4";class AutoenableMixinProvider extends r.ScryptedDeviceBase{constructor(e){var t,i,o;super(e),o={},(i="hasEnabledMixin")in(t=this)?Object.defineProperty(t,i,{value:o,enumerable:!0,configurable:!0,writable:!0}):t[i]=o;try{this.hasEnabledMixin=JSON.parse(this.storage.getItem("hasEnabledMixin"))}catch(e){this.hasEnabledMixin={}}this.pluginsComponent=n.getComponent("plugins"),n.listen((async(e,t,i)=>{t.eventInterface!==r.ScryptedInterface.ScryptedDevice||t.property||this.maybeEnableMixin(e)}));for(const e of Object.keys(n.getSystemState())){const t=n.getDeviceById(e);this.maybeEnableMixin(t)}}async shouldEnableMixin(e){return!0}async maybeEnableMixin(e){var t;if(!e||null!==(t=e.mixins)&&void 0!==t&&t.includes(this.id))return;if(this.hasEnabledMixin[e.id]===s)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 i=(e.mixins||[]).slice();i.push(this.id);const r=await this.pluginsComponent;await r.setMixins(e.id,i),this.setHasEnabledMixin(e.id)}setHasEnabledMixin(e){this.hasEnabledMixin[e]!==s&&(this.hasEnabledMixin[e]=s,this.storage.setItem("hasEnabledMixin",JSON.stringify(this.hasEnabledMixin)))}}t.AutoenableMixinProvider=AutoenableMixinProvider},201:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.createRebroadcaster=async function(e){let t=0;const i=(0,r.createServer)((i=>{let r=!0;const o=()=>{i.removeAllListeners(),i.destroy();const e=n;n=void 0,null==e||e()};let n=null==e?void 0:e.connect((e=>{r&&(r=!1,e.startStream&&i.write(e.startStream));for(const t of e.chunks)i.write(t);return i.writableLength}),o);i.once("close",(()=>{t--,o()})),i.on("error",(t=>{var i;return null==e||null===(i=e.console)||void 0===i?void 0:i.log("client stream ended")})),t++})),o=await(0,n.listenZero)(i);return{server:i,port:o,get clients(){return t}}},t.parseAudioCodec=f,t.parseResolution=p,t.parseVideoCodec=h,t.startParserSession=async function(e,t){const{console:i}=t;let r,a,u=!0;const m=new s.EventEmitter;let g,v,y,S,b;m.on("error",(e=>i.error("rebroadcast error",e)));const M=new Promise(((e,t)=>{S=e,b=t}));function P(){var e;u&&(m.emit("killed"),m.emit("error",new Error("killed"))),u=!1,null==I||I.kill(),null==I||I.kill("SIGKILL"),null===(e=b)||void 0===e||e(new Error("ffmpeg was killed before connecting to the rebroadcast session")),clearTimeout(r),clearTimeout(a)}function w(){i.error("timeout waiting for data, killing parser session"),P()}function x(){t.timeout&&(clearTimeout(r),r=setTimeout(w,t.timeout))}x();const D=e.inputArguments.slice();a=setTimeout(P,3e4);const O=["pipe","pipe","pipe"];let C=3;for(const e of Object.keys(t.parsers)){const i=t.parsers[e];if(i.parseDatagram){const t=c.default.createSocket("udp4"),r=await(0,n.bindZero)(t);D.push(...i.outputArguments,r.url),(async()=>{for await(const s of i.parseDatagram(t,parseInt(null===(r=y)||void 0===r?void 0:r[2]),parseInt(null===(o=y)||void 0===o?void 0:o[3]))){var r,o,n;null===(n=S)||void 0===n||n(void 0),m.emit(e,s),x()}})()}else D.push(...i.outputArguments,"pipe:"+C++),O.push("pipe")}D.push("-sdp_file","pipe:"+C++),O.push("pipe"),D.unshift("-hide_banner"),(0,d.safePrintFFmpegArguments)(i,D);const I=o.default.spawn(await l.getFFmpegPath(),D,{stdio:O});(0,d.ffmpegLogInitialOutput)(i,I),I.on("exit",P);const A=new Promise((e=>{const t=[];I.stdio[C-1].on("data",(i=>{t.push(i),e(t)}))}));let k=0;return Object.keys(t.parsers).forEach((async e=>{const r=t.parsers[e];if(!r.parse)return;const o=I.stdio[3+k];k++;try{for await(const t of r.parse(o,parseInt(null===(n=y)||void 0===n?void 0:n[2]),parseInt(null===(s=y)||void 0===s?void 0:s[3]))){var n,s,a;null===(a=S)||void 0===a||a(void 0),m.emit(e,t),x()}}catch(e){i.error("rebroadcast parse error",e),P()}})),f(I).then((e=>g=e)),h(I).then((e=>v=e)),p(I).then((e=>y=e)),await M,S=void 0,b=void 0,clearTimeout(a),{sdp:A,inputAudioCodec:g,inputVideoCodec:v,inputVideoResolution:y,resetActivityTimer:x,isActive:()=>u,kill:P,mediaStreamOptions:e.mediaStreamOptions||{id:void 0,name:void 0},on(e,t){return m.on(e,t),this},once(e,t){return m.once(e,t),this},removeListener(e,t){return m.removeListener(e,t),this}}};var r=i(808),o=u(i(81)),n=i(769),s=i(361),a=u(i(510)),d=i(833),c=u(i(891));function u(e){return e&&e.__esModule?e:{default:e}}const{mediaManager:l}=a.default;async function p(e){return new Promise((t=>{const i=r=>{const o=r.toString(),n=/(([0-9]{2,5})x([0-9]{2,5}))/.exec(o);n&&(e.stdout.removeListener("data",i),e.stderr.removeListener("data",i),t(n))};e.stdout.on("data",i),e.stderr.on("data",i)}))}async function m(e,t){return new Promise((i=>{const r=o=>{const n=o.toString(),s=n.indexOf(`${t}: `);if(-1!==s){const o=n.substring(s+t.length+1).trim();let a=o.indexOf(" ");const d=o.indexOf(",");-1!==a&&d<a&&(a=d),-1!==a&&(e.stdout.removeListener("data",r),e.stderr.removeListener("data",r),i(o.substring(0,a)))}};e.stdout.on("data",r),e.stderr.on("data",r)}))}async function h(e){return m(e,"Video")}async function f(e){return m(e,"Audio")}},769:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.bindZero=async function(e){e.bind(0),await(0,n.once)(e,"listening");const{port:t}=e.address();return{port:t,url:`udp://127.0.0.1:${t}`}},t.listenZero=s,t.listenZeroSingleClient=async function(){const e=new o.default.Server,t=await s(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{url:`tcp://127.0.0.1:${t}`,port:t,clientPromise:i}};var r,o=(r=i(808))&&r.__esModule?r:{default:r},n=i(361);async function s(e){return e.listen(0),await(0,n.once)(e,"listening"),e.address().port}},833:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=i(568);Object.keys(r).forEach((function(e){"default"!==e&&"__esModule"!==e&&(e in t&&t[e]===r[e]||Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}}))}))},701:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.readLength=async function(e,t){if(!t)return Buffer.alloc(0);{const i=e.read(t);if(i)return i}return new Promise(((i,r)=>{const o=()=>{const r=e.read(t);r&&(s(),i(r))},n=()=>{s(),r(new Error(`stream ended during read for minimum ${t} bytes`))},s=()=>{e.removeListener("readable",o),e.removeListener("end",n)};e.on("readable",o),e.on("end",n)}))}},567:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SettingsMixinDeviceBase=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var i=o(t);if(i&&i.has(e))return i.get(e);var r={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in e)if("default"!==s&&Object.prototype.hasOwnProperty.call(e,s)){var a=n?Object.getOwnPropertyDescriptor(e,s):null;a&&(a.get||a.set)?Object.defineProperty(r,s,a):r[s]=e[s]}r.default=e,i&&i.set(e,r);return r}(i(510));function o(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,i=new WeakMap;return(o=function(e){return e?i:t})(e)}const{deviceManager:n}=r.default;class SettingsMixinDeviceBase extends r.MixinDeviceBase{constructor(e,t,i){super(e,i.mixinDeviceInterfaces,t,i.providerNativeId,i.mixinStorageSuffix),this.settingsGroup=i.group,this.settingsGroupKey=i.groupKey,process.nextTick((()=>n.onMixinEvent(this.id,this,r.ScryptedInterface.Settings,null)))}async getSettings(){const e=this.mixinDeviceInterfaces.includes(r.ScryptedInterface.Settings)?this.mixinDevice.getSettings():void 0,t=this.getMixinSettings(),i=[];try{const t=await e||[];i.push(...t)}catch(e){const t=this.name,r=`${t} Extension settings failed to load.`;this.console.error(r,e),i.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:r,readonly:!0})}try{const e=await t||[];for(const t of e)t.group=t.group||this.settingsGroup,t.key=this.settingsGroupKey+":"+t.key;i.push(...e)}catch(e){const t=n.getDeviceState(this.mixinProviderNativeId).name,r=`${t} Extension settings failed to load.`;this.console.error(r,e),i.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:r,readonly:!0})}return i}async putSetting(e,t){const i=this.settingsGroupKey+":";if(null==e||!e.startsWith(i))return this.mixinDevice.putSetting(e,t);await this.putMixinSetting(e.substring(i.length),t),n.onMixinEvent(this.id,this,r.ScryptedInterface.Settings,null)}release(){n.onMixinEvent(this.id,this,r.ScryptedInterface.Settings,null)}}t.SettingsMixinDeviceBase=SettingsMixinDeviceBase},129:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.PIXEL_FORMAT_YUV420P=t.PIXEL_FORMAT_RGB24=void 0,t.createDgramParser=a,t.createFragmentedMp4Parser=function(e){return{container:"mp4",outputArguments:[...(null==e?void 0:e.vcodec)||[],...(null==e?void 0:e.acodec)||[],"-movflags","frag_keyframe+empty_moov+default_base_moof","-f","mp4"],async*parse(e){const t=d(e);let i,r,o;for await(const e of t)i?r||(r=e):i=e,yield{startStream:o,chunks:[e.header,e.data],type:e.type},i&&r&&!o&&(o=Buffer.concat([i.header,i.data,r.header,r.data]))},findSyncFrame:n}},t.createMpegTsParser=function(e){return{container:"mpegts",outputArguments:[...(null==e?void 0:e.vcodec)||[],...(null==e?void 0:e.acodec)||[],"-f","mpegts"],parse:s(188,(e=>{if(71!=e[0])throw new Error("Invalid sync byte in mpeg-ts packet. Terminating stream.")})),findSyncFrame(e){for(let t=0;t<e.length;t++){const i=e[t];for(let r=0;r<i.chunks.length;r++){const o=i.chunks[r];let n=0;for(;n+188<o.length;){const i=o.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}}},t.createPCMParser=function(){return{container:"s16le",outputArguments:["-vn","-acodec","pcm_s16le","-f","s16le"],parse:s(512),findSyncFrame:n}},t.createRawVideoParser=function(e){var t;const i=(null===(t=e)||void 0===t?void 0:t.pixelFormat)||c;let r;e=e||{};const{size:s,everyNFrames:a}=e;s&&(r=`scale=${s.width}:${s.height}`);a&&a>1&&(r?r+=",":r="",r+=`select=not(mod(n\\,${a}))`);return{container:"rawvideo",outputArguments:[...r?["-vf",r]:[],"-an","-vcodec","rawvideo","-pix_fmt",i.name,"-f","rawvideo"],async*parse(e,t,r){if(!t||!r)throw new Error("error parsing rawvideo, unknown width and height");t=(null==s?void 0:s.width)||t,r=(null==s?void 0:s.height)||r;const n=i.computeLength(t,r);for(;;){const i=await(0,o.readLength)(e,n);yield{chunks:[i],width:t,height:r}}},findSyncFrame:n}},t.createRtpParser=function(...e){return{container:"sdp",outputArguments:[...e,"-f","rtp"],parseDatagram:a(),findSyncFrame:n}},t.parseFragmentedMP4=d;var r=i(361),o=i(701);function n(e){return e}function s(e,t){return async function*(i){let o=[],n=0;for(;;){const s=i.read();if(!s){await(0,r.once)(i,"readable");continue}if(o.push(s),n+=s.length,n<e)continue;const a=Buffer.concat(o);null==t||t(a);const d=a.length%e,c=a.slice(0,a.length-d),u=a.slice(a.length-d);o=[u],n=u.length,yield{chunks:[c]}}}}function a(){return async function*(e){for(;;){const[t]=await(0,r.once)(e,"message");yield{chunks:[t]}}}}async function*d(e){for(;;){const t=await(0,o.readLength)(e,8),i=t.readInt32BE(0)-8,r=t.slice(4).toString(),n=await(0,o.readLength)(e,i);yield{header:t,length:i,type:r,data:n}}}const c={name:"yuv420p",computeLength:(e,t)=>e*t*1.5};t.PIXEL_FORMAT_YUV420P=c;t.PIXEL_FORMAT_RGB24={name:"rgb24",computeLength:(e,t)=>e*t*3}},510:(e,t,i)=>{"use strict";var r=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]},o=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,o(i(393),t);const n=i(393);class ScryptedDeviceBase 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=ScryptedDeviceBase;class MixinDeviceBase extends n.DeviceBase{constructor(e,t,i,r,o){super(),this.mixinDevice=e,this.mixinDeviceInterfaces=t,this.mixinProviderNativeId=r,this._mixinStorageSuffix=o,this._listeners=new Set,this._deviceState=i}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=MixinDeviceBase,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(ScryptedDeviceBase.prototype,i,{set:t(i),get:e(i)}),Object.defineProperty(MixinDeviceBase.prototype,i,{set:t(i),get:e(i)})}();let s={};try{s=Object.assign(s,{log:deviceManager.getDeviceLogger(void 0),deviceManager,endpointManager,mediaManager,systemManager,pluginHostAPI})}catch(e){console.error("sdk initialization error, import @scrypted/sdk/types instead",e)}t.default=s},393:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ThermostatMode=t.TemperatureUnit=t.ScryptedMimeTypes=t.ScryptedInterfaceProperty=t.ScryptedInterfaceDescriptors=t.ScryptedInterface=t.ScryptedDeviceType=t.SCRYPTED_MEDIA_SCHEME=t.MediaPlayerState=t.LockState=t.HumidityMode=t.FanMode=t.DeviceBase=void 0;let i;t.DeviceBase=class DeviceBase{},t.ScryptedInterfaceProperty=i,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"}(i||(t.ScryptedInterfaceProperty=i={}));let r,o,n,s,a,d,c,u,l;t.ScryptedInterfaceDescriptors={ScryptedDevice:{name:"ScryptedDevice",methods:["listen","probe","setName","setRoom","setType"],properties:["id","info","interfaces","mixins","name","providedInterfaces","providedName","providedRoom","providedType","providerId","room","type"]},ScryptedPlugin:{name:"ScryptedPlugin",methods:["getPluginJson"],properties:[]},OnOff:{name:"OnOff",methods:["turnOff","turnOn"],properties:["on"]},Brightness:{name:"Brightness",methods:["setBrightness"],properties:["brightness"]},ColorSettingTemperature:{name:"ColorSettingTemperature",methods:["getTemperatureMaxK","getTemperatureMinK","setColorTemperature"],properties:["colorTemperature"]},ColorSettingRgb:{name:"ColorSettingRgb",methods:["setRgb"],properties:["rgb"]},ColorSettingHsv:{name:"ColorSettingHsv",methods:["setHsv"],properties:["hsv"]},Notifier:{name:"Notifier",methods:["sendNotification"],properties:[]},StartStop:{name:"StartStop",methods:["start","stop"],properties:["running"]},Pause:{name:"Pause",methods:["pause","resume"],properties:["paused"]},Dock:{name:"Dock",methods:["dock"],properties:["docked"]},TemperatureSetting:{name:"TemperatureSetting",methods:["setThermostatMode","setThermostatSetpoint","setThermostatSetpointHigh","setThermostatSetpointLow"],properties:["thermostatActiveMode","thermostatAvailableModes","thermostatMode","thermostatSetpoint","thermostatSetpointHigh","thermostatSetpointLow"]},Thermometer:{name:"Thermometer",methods:["setTemperatureUnit"],properties:["temperature","temperatureUnit"]},HumiditySensor:{name:"HumiditySensor",methods:[],properties:["humidity"]},Camera:{name:"Camera",methods:["getPictureOptions","takePicture"],properties:[]},VideoCamera:{name:"VideoCamera",methods:["getVideoStream","getVideoStreamOptions"],properties:[]},VideoCameraConfiguration:{name:"VideoCameraConfiguration",methods:["setVideoStreamOptions"],properties:[]},Intercom:{name:"Intercom",methods:["startIntercom","stopIntercom"],properties:[]},Lock:{name:"Lock",methods:["lock","unlock"],properties:["lockState"]},PasswordStore:{name:"PasswordStore",methods:["addPassword","getPasswords","removePassword"],properties:[]},Authenticator:{name:"Authenticator",methods:["checkPassword"],properties:[]},Scene:{name:"Scene",methods:["activate","deactivate","isReversible"],properties:[]},Entry:{name:"Entry",methods:["closeEntry","openEntry"],properties:[]},EntrySensor:{name:"EntrySensor",methods:[],properties:["entryOpen"]},DeviceProvider:{name:"DeviceProvider",methods:["getDevice"],properties:[]},DeviceDiscovery:{name:"DeviceDiscovery",methods:["discoverDevices"],properties:[]},DeviceCreator:{name:"DeviceCreator",methods:["createDevice","getCreateDeviceSettings"],properties:[]},Battery:{name:"Battery",methods:[],properties:["batteryLevel"]},Refresh:{name:"Refresh",methods:["getRefreshFrequency","refresh"],properties:[]},MediaPlayer:{name:"MediaPlayer",methods:["getMediaStatus","load","seek","skipNext","skipPrevious"],properties:[]},Online:{name:"Online",methods:[],properties:["online"]},SoftwareUpdate:{name:"SoftwareUpdate",methods:["checkForUpdate","installUpdate"],properties:["updateAvailable"]},BufferConverter:{name:"BufferConverter",methods:["convert"],properties:["fromMimeType","toMimeType"]},Settings:{name:"Settings",methods:["getSettings","putSetting"],properties:[]},BinarySensor:{name:"BinarySensor",methods:[],properties:["binaryState"]},IntrusionSensor:{name:"IntrusionSensor",methods:[],properties:["intrusionDetected"]},PowerSensor:{name:"PowerSensor",methods:[],properties:["powerDetected"]},AudioSensor:{name:"AudioSensor",methods:[],properties:["audioDetected"]},MotionSensor:{name:"MotionSensor",methods:[],properties:["motionDetected"]},AmbientLightSensor:{name:"AmbientLightSensor",methods:[],properties:["ambientLight"]},OccupancySensor:{name:"OccupancySensor",methods:[],properties:["occupied"]},FloodSensor:{name:"FloodSensor",methods:[],properties:["flooded"]},UltravioletSensor:{name:"UltravioletSensor",methods:[],properties:["ultraviolet"]},LuminanceSensor:{name:"LuminanceSensor",methods:[],properties:["luminance"]},PositionSensor:{name:"PositionSensor",methods:[],properties:["position"]},Readme:{name:"Readme",methods:["getReadmeMarkdown"],properties:[]},OauthClient:{name:"OauthClient",methods:["getOauthUrl","onOauthCallback"],properties:[]},MixinProvider:{name:"MixinProvider",methods:["canMixin","getMixin","releaseMixin"],properties:[]},HttpRequestHandler:{name:"HttpRequestHandler",methods:["onRequest"],properties:[]},EngineIOHandler:{name:"EngineIOHandler",methods:["onConnection"],properties:[]},PushHandler:{name:"PushHandler",methods:["onPush"],properties:[]},Program:{name:"Program",methods:["run"],properties:[]},Scriptable:{name:"Scriptable",methods:["eval","loadScripts","saveScript"],properties:[]},ObjectDetector:{name:"ObjectDetector",methods:["getDetectionInput","getObjectTypes"],properties:[]},ObjectDetection:{name:"ObjectDetection",methods:["detectObjects","getDetectionModel"],properties:[]},HumiditySetting:{name:"HumiditySetting",methods:["setHumidity"],properties:["humiditySetting"]},Fan:{name:"Fan",methods:["setFan"],properties:["fan"]}},t.ScryptedDeviceType=r,function(e){e.Builtin="Builtin",e.Camera="Camera",e.Fan="Fan",e.Light="Light",e.Switch="Switch",e.Outlet="Outlet",e.Sensor="Sensor",e.Scene="Scene",e.Program="Program",e.Automation="Automation",e.Vacuum="Vacuum",e.Notifier="Notifier",e.Thermostat="Thermostat",e.Lock="Lock",e.PasswordControl="PasswordControl",e.Display="Display",e.Speaker="Speaker",e.Event="Event",e.Entry="Entry",e.Garage="Garage",e.DeviceProvider="DeviceProvider",e.DataSource="DataSource",e.API="API",e.Doorbell="Doorbell",e.Irrigation="Irrigation",e.Valve="Valve",e.Person="Person",e.Unknown="Unknown"}(r||(t.ScryptedDeviceType=r={})),t.HumidityMode=o,function(e){e.Humidify="Humidify",e.Dehumidify="Dehumidify",e.Auto="Auto",e.Off="Off"}(o||(t.HumidityMode=o={})),t.FanMode=n,function(e){e.Auto="Auto",e.Manual="Manual"}(n||(t.FanMode=n={})),t.TemperatureUnit=s,function(e){e.C="C",e.F="F"}(s||(t.TemperatureUnit=s={})),t.ThermostatMode=a,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"}(a||(t.ThermostatMode=a={})),t.LockState=d,function(e){e.Locked="Locked",e.Unlocked="Unlocked",e.Jammed="Jammed"}(d||(t.LockState=d={})),t.MediaPlayerState=c,function(e){e.Idle="Idle",e.Playing="Playing",e.Paused="Paused",e.Buffering="Buffering"}(c||(t.MediaPlayerState=c={})),t.ScryptedInterface=u,function(e){e.ScryptedDevice="ScryptedDevice",e.ScryptedPlugin="ScryptedPlugin",e.OnOff="OnOff",e.Brightness="Brightness",e.ColorSettingTemperature="ColorSettingTemperature",e.ColorSettingRgb="ColorSettingRgb",e.ColorSettingHsv="ColorSettingHsv",e.Notifier="Notifier",e.StartStop="StartStop",e.Pause="Pause",e.Dock="Dock",e.TemperatureSetting="TemperatureSetting",e.Thermometer="Thermometer",e.HumiditySensor="HumiditySensor",e.Camera="Camera",e.VideoCamera="VideoCamera",e.VideoCameraConfiguration="VideoCameraConfiguration",e.Intercom="Intercom",e.Lock="Lock",e.PasswordStore="PasswordStore",e.Authenticator="Authenticator",e.Scene="Scene",e.Entry="Entry",e.EntrySensor="EntrySensor",e.DeviceProvider="DeviceProvider",e.DeviceDiscovery="DeviceDiscovery",e.DeviceCreator="DeviceCreator",e.Battery="Battery",e.Refresh="Refresh",e.MediaPlayer="MediaPlayer",e.Online="Online",e.SoftwareUpdate="SoftwareUpdate",e.BufferConverter="BufferConverter",e.Settings="Settings",e.BinarySensor="BinarySensor",e.IntrusionSensor="IntrusionSensor",e.PowerSensor="PowerSensor",e.AudioSensor="AudioSensor",e.MotionSensor="MotionSensor",e.AmbientLightSensor="AmbientLightSensor",e.OccupancySensor="OccupancySensor",e.FloodSensor="FloodSensor",e.UltravioletSensor="UltravioletSensor",e.LuminanceSensor="LuminanceSensor",e.PositionSensor="PositionSensor",e.Readme="Readme",e.OauthClient="OauthClient",e.MixinProvider="MixinProvider",e.HttpRequestHandler="HttpRequestHandler",e.EngineIOHandler="EngineIOHandler",e.PushHandler="PushHandler",e.Program="Program",e.Scriptable="Scriptable",e.ObjectDetector="ObjectDetector",e.ObjectDetection="ObjectDetection",e.HumiditySetting="HumiditySetting",e.Fan="Fan"}(u||(t.ScryptedInterface=u={})),t.ScryptedMimeTypes=l,function(e){e.AcceptUrlParameter="accept-url",e.Url="text/x-uri",e.InsecureLocalUrl="text/x-insecure-local-uri",e.LocalUrl="text/x-local-uri",e.PushEndpoint="text/x-push-endpoint",e.MediaStreamUrl="text/x-media-url",e.FFmpegInput="x-scrypted/x-ffmpeg-input",e.RTCAVSignalingPrefix="x-scrypted-rtc-signaling-",e.RTCAVOffer="x-scrypted/x-rtc-av-offer",e.RTCAVAnswer="x-scrypted/x-rtc-av-answer"}(l||(t.ScryptedMimeTypes=l={}));t.SCRYPTED_MEDIA_SCHEME="scryped-media://"},568:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ffmpegLogInitialOutput=function(e,t,r){var o,n;function s(e){const o=n=>{const s=n.toString();for(const e of i)if(-1!==s.indexOf(e))return;if(!r&&(-1!==s.indexOf("frame=")||-1!==s.indexOf("size=")))return e(s),e("video/audio detected, discarding further input"),t.stdout.removeListener("data",o),void t.stderr.removeListener("data",o);e(s)};return o}null===(o=t.stdout)||void 0===o||o.on("data",s(e.log)),null===(n=t.stderr)||void 0===n||n.on("data",s(e.error)),t.on("exit",(()=>e.log("ffmpeg exited")))},t.safePrintFFmpegArguments=function(e,t){const i=[];for(const e of t)try{const t=new URL(e);i.push(`${t.protocol}[REDACTED]`)}catch(t){i.push(e)}e.log(i.join(" "))};const i=["decode_slice_header error","no frame!","non-existing PPS"]},81:e=>{"use strict";e.exports=require("child_process")},891:e=>{"use strict";e.exports=require("dgram")},361:e=>{"use strict";e.exports=require("events")},808:e=>{"use strict";e.exports=require("net")}},t={};function i(r){var o=t[r];if(void 0!==o)return o.exports;var n=t[r]={exports:{}};return e[r](n,n.exports,i),n.exports}var r={};(()=>{"use strict";var e=r;Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0;var t,o=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var i=p(t);if(i&&i.has(e))return i.get(e);var r={},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var n in e)if("default"!==n&&Object.prototype.hasOwnProperty.call(e,n)){var s=o?Object.getOwnPropertyDescriptor(e,n):null;s&&(s.get||s.set)?Object.defineProperty(r,n,s):r[n]=e[n]}r.default=e,i&&i.set(e,r);return r}(i(510)),n=i(361),s=i(567),a=i(201),d=i(129),c=i(454),u=(t=i(891))&&t.__esModule?t:{default:t},l=i(769);function p(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,i=new WeakMap;return(p=function(e){return e?i:t})(e)}function m(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}const{mediaManager:h,log:f,systemManager:g,deviceManager:v}=o.default,y=1e4,S="prebufferDuration",b="sendKeyframe",M="Default",P="AAC or No Audio",w=`${P} (Copy)`,x="Compatible Audio",D="Other Audio",O="PCM or G.711 Audio",C=`${O} (Copy, Unstable)`,I=["aac","mp3","mp2","opus"],A="-fflags +genpts",k=[P,x,D],T=["mpegts","mp4","s16le","rtpvideo","rtpaudio"];class PrebufferSession{constructor(e,t,i,r){m(this,"prebuffers",{mp4:[],mpegts:[],s16le:[],rtpvideo:[],rtpaudio:[]}),m(this,"detectedIdrInterval",0),m(this,"prevIdr",0),m(this,"audioDisabled",!1),m(this,"activeClients",0),this.mixin=e,this.streamName=t,this.streamId=i,this.stopInactive=r,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}clearPrebuffers(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.s16le=[],this.prebuffers.rtpaudio=[],this.prebuffers.rtpvideo=[]}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)||"";k.find((t=>e.startsWith(t)))||(e="");const t=-1!==e.indexOf(P),i=-1!==e.indexOf(x),r=-1!==e.indexOf(D),o=-1!==e.indexOf(O);return{isUsingDefaultAudioConfig:!(t||i||r||o),aacAudio:t,pcmAudio:o,compatibleAudio:i,reencodeAudio:r}}async getMixinSettings(){const e=[],t=this.parserSession;let i=0,r=0;for(const e of this.prebuffers.mp4){r=r||e.time;for(const t of e.chunk.chunks)i+=t.byteLength}const o=Date.now()-r,n=Math.round(i/o*8),s=this.streamName?`Rebroadcast: ${this.streamName}`:"Rebroadcast";var a,d,c;(e.push({title:"Audio Codec Transcoding",group:s,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)||M,choices:[M,w,"Compatible Audio (Copy)","Other Audio (Transcode)",C]},{title:"FFmpeg Input Arguments Prefix",group:s,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:A,choices:[A,"-use_wallclock_as_timestamps 1","-v verbose"],combobox:!0},{title:"Rebroadcast Mode",group:s,description:"THIS FEATURE IS IN TESTING. DO NOT CHANGE THIS FROM MPEG-TS. The stream format to use when rebroadcasting. RTP will increase startup time but may resolve PCM audio issues.",placeholder:"MPEG-TS",choices:["MPEG-TS","RTP"],key:this.rebroadcastModeKey,value:this.storage.getItem(this.rebroadcastModeKey)||"MPEG-TS"}),t)?e.push({key:"detectedResolution",group:s,title:"Detected Resolution and Bitrate",readonly:!0,value:`${(null==t||null===(a=t.inputVideoResolution)||void 0===a?void 0:a[0])||"unknown"} @ ${n||"unknown"} Kb/s`,description:"Configuring your camera to 1920x1080, 2000Kb/S, Variable Bit Rate, is recommended."},{key:"detectedCodec",group:s,title:"Detected Video/Audio Codecs",readonly:!0,value:((null==t||null===(d=t.inputVideoCodec)||void 0===d?void 0:d.toString())||"unknown")+"/"+((null==t||null===(c=t.inputAudioCodec)||void 0===c?void 0:c.toString())||"unknown"),description:"Configuring your camera to H264 video and AAC/MP3/MP2/Opus audio is recommended."},{key:"detectedKeyframe",group:s,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).toString()||"none"}):e.push({title:"Status",group:s,key:"status",description:"Rebroadcast is currently idle and will be started automatically on demand.",value:"Idle",readonly:!0});return e}async startPrebufferSession(){var e,t,i,r,n,s,c,u;this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.s16le=[],this.prebuffers.rtpvideo=[],this.prebuffers.rtpaudio=[];const l=parseInt(this.storage.getItem(S))||y;let p;try{p=(await this.mixinDevice.getVideoStreamOptions()).find((e=>e.id===this.streamId))}catch(e){}const m=null===(null===(e=p)||void 0===e||null===(t=e.audio)||void 0===t?void 0:t.codec),g=null===(i=p)||void 0===i||null===(r=i.audio)||void 0===r?void 0:r.codec,{isUsingDefaultAudioConfig:v,aacAudio:b,compatibleAudio:M,reencodeAudio:w,pcmAudio:x}=this.getAudioConfig();let D=!1;m||g||!v||void 0!==this.detectedAudioCodec||(this.console.warn("Camera did not report an audio codec, muting the audio stream and probing the codec."),D=!0),!m&&g&&void 0!==this.detectedAudioCodec&&this.detectedAudioCodec!==g&&this.console.warn("Audio codec plugin reported vs detected mismatch",g,this.detectedAudioCodec);const O=void 0===this.detectedAudioCodec?null==g?void 0:g.toLowerCase():null===(n=this.detectedAudioCodec)||void 0===n?void 0:n.toLowerCase(),C=!I.includes(O);D||!1===(null===(s=p)||void 0===s?void 0:s.userConfigurable)||m||(C?(v&&f.a(`${this.mixin.name} is using the ${O} audio codec and has had its audio disabled. Select 'Disable Audio' or '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:",O)):!m&&v&&void 0===g&&void 0!==this.detectedAudioCodec&&("aac"===this.detectedAudioCodec?f.a(`${this.mixin.name} did not report a codec and ${this.detectedAudioCodec} was found during probe. Select '${P}' in the camera stream's Rebroadcast settings to suppress this alert.`):f.a(`${this.mixin.name} did not report a codec and ${this.detectedAudioCodec} was found during probe. Select 'Compatible Audio' in the camera stream's Rebroadcast settings to suppress this alert.`)));const k=await this.mixinDevice.getVideoStream(p),_=await h.convertMediaObjectToBuffer(k,o.ScryptedMimeTypes.FFmpegInput),E=JSON.parse(_.toString()),L=["-bsf:a","aac_adtstoasc"],R=[];let j;this.audioDisabled=!1;const B=null===this.detectedAudioCodec,H=v&&C;if(H&&this.console.log("camera reports it is not user configurable. transcoding due to incompatible codec",O),m||D||B)j=["-an"],this.audioDisabled=!0;else if(x||H)j=["-an"];else if(w||g&&!I.includes(g))j=["-bsf:a","aac_adtstoasc","-ar","8k","-b:a","100k","-bufsize","400k","-ac","1","-acodec","libfdk_aac","-profile:a","aac_low","-flags","+global_header"];else if(b)j=["-acodec","copy"],j.push(...L);else if(M)j=["-acodec","copy"],j.push(...R);else{j=["-acodec","copy"];const e="aac"===O?L:R;j.push(...e)}const F=["-vcodec","copy"],V={console:this.console,timeout:6e4,parsers:{mp4:(0,d.createFragmentedMp4Parser)({vcodec:F,acodec:j})}};"RTP"===this.storage.getItem(this.rebroadcastModeKey)?(V.parsers.rtpvideo=(0,d.createRtpParser)("-an","-vcodec","copy"),V.parsers.rtpaudio=(0,d.createRtpParser)("-vn","-acodec","copy")):V.parsers.mpegts=(0,d.createMpegTsParser)({vcodec:F,acodec:j}),!x||m||B||(V.parsers.s16le=(0,d.createPCMParser)()),this.parsers=V.parsers;const N=this.storage.getItem(this.ffmpegInputArgumentsKey)||A;E.inputArguments.unshift(...N.split(" "));const U=await(0,a.startParserSession)(E,V);if(U.inputAudioCodec?I.includes(null===(c=U.inputAudioCodec)||void 0===c?void 0:c.toLowerCase())?this.console.log("Detected audio codec is mp4/mpegts compatible.",U.inputAudioCodec):this.console.log("Detected audio codec is not mp4/mpegts compatible.",U.inputAudioCodec):this.console.log("No audio stream detected."),this.detectedAudioCodec=U.inputAudioCodec||null,this.detectedVideoCodec=U.inputVideoCodec||null,"h264"!==U.inputVideoCodec&&this.console.error("Video codec is not h264. If there are errors, try changing your camera's encoder output."),D)return this.console.warn("Audio probe complete, ending rebroadcast session and restarting with detected codecs."),U.kill(),this.startPrebufferSession();if(this.parserSession=U,null!==(u=E.mediaStreamOptions)&&void 0!==u&&u.refreshAt){let e,t=E.mediaStreamOptions;const i=async()=>{if(!U.isActive)return;const e=await this.mixinDevice.getVideoStream(t),i=await h.convertMediaObjectToBuffer(e,o.ScryptedMimeTypes.FFmpegInput),n=JSON.parse(i.toString());t=n.mediaStreamOptions,r(n)},r=t=>{const r=t.mediaStreamOptions.refreshAt-Date.now()-3e4;this.console.log("refreshing media stream in",r),e=setTimeout(i,r)};r(E),U.once("killed",(()=>clearTimeout(e)))}U.once("killed",(()=>{this.parserSessionPromise=void 0,this.parserSession===U&&(this.parserSession=void 0)}));for(const e of T){var K;if(null!==(K=this.parsers[e])&&void 0!==K&&K.parseDatagram)continue;let t=0;U.on(e,(i=>{const r=this.prebuffers[e],o=Date.now();for("mdat"===i.type&&(this.prevIdr&&(this.detectedIdrInterval=o-this.prevIdr),this.prevIdr=o),r.push({time:o,chunk:i});r.length&&r[0].time<o-l;)r.shift(),t++;t>1e3&&(this.prebuffers[e]=r.slice(),t=0)}))}return U}printActiveClients(){this.console.log(this.streamName,"active rebroadcast clients:",this.activeClients)}inactivityCheck(e){this.printActiveClients(),this.stopInactive&&(this.activeClients||(clearTimeout(this.inactivityTimeout),this.inactivityTimeout=setTimeout((()=>{this.activeClients||(this.console.log(this.streamName,"terminating rebroadcast due to inactivity"),e.kill())}),3e4)))}async getVideoStream(e){var t,i;this.ensurePrebufferSession();const r=await this.parserSessionPromise,o="false"!==this.storage.getItem(b),n=(null==e?void 0:e.prebuffer)||(o?1.5*Math.max(4e3,this.detectedIdrInterval||4e3):0);this.console.log(this.streamName,"prebuffer request started");const s=async e=>{const t=this.prebuffers[e];if(this.parsers[e].parseDatagram){let e=Buffer.concat(await r.sdp).toString();const t=Math.round(4e4*Math.random()+1e4),i=Math.round(4e4*Math.random()+1e4);e=e.replace("m=audio 0","m=audio "+t),e=e.replace("m=video 0","m=video "+i);const o=u.default.createSocket("udp4");o.bind();const n=(e,t)=>{for(const i of e.chunks)o.send(i,t)},s=e=>n(e,i),a=e=>n(e,t),d=()=>{o.close(),r.removeListener("rtpvideo",s),r.removeListener("rtpaudio",a),r.removeListener("killed",d)};r.once("killed",d);const c=await(0,l.listenZeroSingleClient)();return c.clientPromise.then((async t=>{this.activeClients++,this.printActiveClients(),t.once("close",(()=>{this.activeClients--,this.inactivityCheck(r),d()})),t.write(e),t.end()})).catch(d),r.on("rtpvideo",s),r.on("rtpaudio",a),c.url}const{server:i,port:o}=await(0,a.createRebroadcaster)({console:this.console,connect:(o,s)=>{this.activeClients++,this.printActiveClients(),i.close();const a=Date.now(),d=e=>{o(e)>1e8&&(this.console.log("more than 100MB has been buffered, did downstream die? killing connection.",this.streamName),c())},c=()=>{s(),this.console.log(this.streamName,"prebuffer request ended"),r.removeListener(e,d),r.removeListener("killed",c)};r.on(e,d),r.once("killed",c);for(const e of t)e.time<a-n||d(e.chunk);return()=>{this.activeClients--,this.inactivityCheck(r),c()}}});return setTimeout((()=>i.close()),3e4),`tcp://127.0.0.1:${o}`},d="RTP"===this.storage.getItem(this.rebroadcastModeKey)?"rtpvideo":"mpegts",c=this.parsers[null==e?void 0:e.container]?null==e?void 0:e.container:d,p=Object.assign({},r.mediaStreamOptions);p.prebuffer=n;const{pcmAudio:m,reencodeAudio:f}=this.getAudioConfig();this.audioDisabled?p.audio=null:p.audio=f?{codec:"aac",encoder:"libfdk_aac",profile:"aac_low"}:{codec:null==r?void 0:r.inputAudioCodec},p.video&&null!==(t=r.inputVideoResolution)&&void 0!==t&&t[2]&&null!==(i=r.inputVideoResolution)&&void 0!==i&&i[3]&&Object.assign(p.video,{width:parseInt(r.inputVideoResolution[2]),height:parseInt(r.inputVideoResolution[3])});const g=Date.now();let v=0;const y=this.prebuffers[c];for(const e of y)if(!(e.time<g-n))for(const t of e.chunk.chunks)v+=t.length;const S=Math.max(5e5,v).toString(),M=await s(c),P={url:M,container:c,inputArguments:["-analyzeduration","0","-probesize",S,"-f",this.parsers[c].container,"-i",M],mediaStreamOptions:p};m&&P.inputArguments.push("-analyzeduration","0","-probesize",S,"-f","s16le","-i",await s("s16le"));return h.createFFmpegMediaObject(P)}}class PrebufferMixin extends s.SettingsMixinDeviceBase{constructor(e,t,i,r){super(e,i,{providerNativeId:r,mixinDeviceInterfaces:t,group:"Prebuffer Settings",groupKey:"prebuffer"}),m(this,"released",!1),m(this,"sessions",new Map),this.delayStart()}delayStart(){this.console.log("prebuffer sessions starting in 5 seconds"),setTimeout((()=>this.ensurePrebufferSessions()),5e3)}async getVideoStream(e){await this.ensurePrebufferSessions();const t=null==e?void 0:e.id;let i=this.sessions.get(t);return!i||null!=e&&e.directMediaStream?this.mixinDevice.getVideoStream(e):(i.ensurePrebufferSession(),await i.parserSessionPromise,i=this.sessions.get(t),i?i.getVideoStream(e):this.mixinDevice.getVideoStream(e))}async ensurePrebufferSessions(){const e=await this.mixinDevice.getVideoStreamOptions(),t=this.getEnabledMediaStreamOptions(e),i=t?t.map((e=>e.id)):[void 0],r=(null==e?void 0:e.map((e=>e.id)))||[void 0];if("true"!==this.storage.getItem("warnedCloud")){(null==e?void 0:e.find((e=>"cloud"===e.source)))&&(this.storage.setItem("warnedCloud","true"),f.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 s=this.mixinDeviceInterfaces.includes(o.ScryptedInterface.Battery);let a=0;const d=r.length;for(const t of r){let r=this.sessions.get(t);if(!r){var c;const o=null==e?void 0:e.find((e=>e.id===t));null!=o&&o.prebuffer&&f.a(`Prebuffer is already available on ${this.name}. If this is a grouped device, disable the Rebroadcast extension.`);const u=null==o?void 0:o.name,l=!i.includes(t);if(r=new PrebufferSession(this,u,t,s||l),this.sessions.set(t,r),t===(null==e||null===(c=e[0])||void 0===c?void 0:c.id)&&this.sessions.set(void 0,r),s){this.console.log("camera is battery powered, prebuffering and rebroadcasting will only work on demand.");continue}if(l){this.console.log("stream",u,"will be rebroadcast on demand.");continue}(async()=>{for(;this.sessions.get(t)===r&&!this.released;){r.ensurePrebufferSession();try{const e=await r.parserSessionPromise;a++,this.online=a==d,await(0,n.once)(e,"killed"),this.console.error("prebuffer session ended")}catch(e){this.console.error("prebuffer session ended with error",e)}finally{a--,this.online=a==d}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)")})()}}v.onMixinEvent(this.id,this.mixinProviderNativeId,o.ScryptedInterface.Settings,void 0)}async getMixinSettings(){const e=[];try{const t=await this.mixinDevice.getVideoStreamOptions(),i=this.getEnabledMediaStreamOptions(t);(null==t?void 0:t.length)>0&&e.push({title:"Prebuffered Streams",description:"The streams to prebuffer. Enable only as necessary to reduce traffic.",key:"enabledStreams",value:i.map((e=>e.name||"")),choices:t.map((e=>e.name)),multiple:!0})}catch(e){throw this.console.error("error in getVideoStreamOptions",e),e}e.push({title:"Prebuffer Duration",description:"Duration of the prebuffer in milliseconds.",type:"number",key:S,value:this.storage.getItem(S)||y.toString()},{title:"Start at Previous Keyframe",description:"Start live streams from the previous key frame. Improves startup time.",type:"boolean",key:b,value:("false"!==this.storage.getItem(b)).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()){var r;null==e||null===(r=e.parserSessionPromise)||void 0===r||r.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(S))||y;if(t)for(const e of t)e.prebuffer=i;else e.push({id:"default",name:"Default",prebuffer:i});return e}release(){this.console.log("prebuffer releasing if started"),this.released=!0;for(const t of this.sessions.values()){var e;t&&(t.clearPrebuffers(),null===(e=t.parserSessionPromise)||void 0===e||e.then((e=>{this.console.log("prebuffer released"),e.kill(),t.clearPrebuffers()})))}}}class PrebufferProvider extends c.AutoenableMixinProvider{constructor(e){super(e);for(const e of Object.keys(g.getSystemState())){var t;const i=g.getDeviceById(e);null!==(t=i.mixins)&&void 0!==t&&t.includes(this.id)&&i.getVideoStreamOptions()}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((()=>v.requestRestart()),i)}async canMixin(e,t){return t.includes(o.ScryptedInterface.VideoCamera)?[o.ScryptedInterface.VideoCamera,o.ScryptedInterface.Settings,o.ScryptedInterface.Online]:null}async getMixin(e,t,i){return this.setHasEnabledMixin(i.id),new PrebufferMixin(e,t,i,this.nativeId)}async releaseMixin(e,t){t.online=!0,t.release()}}var _=new PrebufferProvider;e.default=_})();var o=exports="undefined"==typeof exports?{}:exports;for(var n in r)o[n]=r[n];r.__esModule&&Object.defineProperty(o,"__esModule",{value:!0})})();
1
+ (()=>{var e={454:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.AutoenableMixinProvider=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var i=o(t);if(i&&i.has(e))return i.get(e);var r={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in e)if("default"!==s&&Object.prototype.hasOwnProperty.call(e,s)){var a=n?Object.getOwnPropertyDescriptor(e,s):null;a&&(a.get||a.set)?Object.defineProperty(r,s,a):r[s]=e[s]}r.default=e,i&&i.set(e,r);return r}(i(510));function o(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,i=new WeakMap;return(o=function(e){return e?i:t})(e)}const{systemManager:n}=r.default,s="v4";class AutoenableMixinProvider extends r.ScryptedDeviceBase{constructor(e){var t,i,o;super(e),o={},(i="hasEnabledMixin")in(t=this)?Object.defineProperty(t,i,{value:o,enumerable:!0,configurable:!0,writable:!0}):t[i]=o;try{this.hasEnabledMixin=JSON.parse(this.storage.getItem("hasEnabledMixin"))}catch(e){this.hasEnabledMixin={}}this.pluginsComponent=n.getComponent("plugins"),n.listen((async(e,t,i)=>{t.eventInterface!==r.ScryptedInterface.ScryptedDevice||t.property||this.maybeEnableMixin(e)}));for(const e of Object.keys(n.getSystemState())){const t=n.getDeviceById(e);this.maybeEnableMixin(t)}}async shouldEnableMixin(e){return!0}async maybeEnableMixin(e){var t;if(!e||null!==(t=e.mixins)&&void 0!==t&&t.includes(this.id))return;if(this.hasEnabledMixin[e.id]===s)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 i=(e.mixins||[]).slice();i.push(this.id);const r=await this.pluginsComponent;await r.setMixins(e.id,i),this.setHasEnabledMixin(e.id)}setHasEnabledMixin(e){this.hasEnabledMixin[e]!==s&&(this.hasEnabledMixin[e]=s,this.storage.setItem("hasEnabledMixin",JSON.stringify(this.hasEnabledMixin)))}}t.AutoenableMixinProvider=AutoenableMixinProvider},201:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.createRebroadcaster=async function(e){let t=0;const i=(0,r.createServer)((i=>{let r=!0;const o=()=>{i.removeAllListeners(),i.destroy();const e=n;n=void 0,null==e||e()};let n=null==e?void 0:e.connect((e=>{r&&(r=!1,e.startStream&&i.write(e.startStream));for(const t of e.chunks)i.write(t);return i.writableLength}),o);i.once("close",(()=>{t--,o()})),i.on("error",(t=>{var i;return null==e||null===(i=e.console)||void 0===i?void 0:i.log("client stream ended")})),t++})),o=await(0,n.listenZero)(i);return{server:i,port:o,get clients(){return t}}},t.parseAudioCodec=f,t.parseResolution=p,t.parseVideoCodec=h,t.startParserSession=async function(e,t){const{console:i}=t;let r,a,u=!0;const m=new s.EventEmitter;let g,v,y,S,b;m.on("error",(e=>i.error("rebroadcast error",e)));const P=new Promise(((e,t)=>{S=e,b=t}));function M(){var e;u&&(m.emit("killed"),m.emit("error",new Error("killed"))),u=!1,null==C||C.kill(),null==C||C.kill("SIGKILL"),null===(e=b)||void 0===e||e(new Error("ffmpeg was killed before connecting to the rebroadcast session")),clearTimeout(r),clearTimeout(a)}function w(){i.error("timeout waiting for data, killing parser session"),M()}function D(){t.timeout&&(clearTimeout(r),r=setTimeout(w,t.timeout))}D();const O=e.inputArguments.slice();a=setTimeout(M,3e4);const x=["pipe","pipe","pipe"];let I=3;for(const e of Object.keys(t.parsers)){const i=t.parsers[e];if(i.parseDatagram){const t=d.default.createSocket("udp4"),r=await(0,n.bindZero)(t),o=d.default.createSocket("udp4");await(0,n.bind)(o,r.port+1),m.once("killed",(()=>{t.close(),o.close()})),O.push(...i.outputArguments,r.url.replace("udp://","rtp://")),(async()=>{for await(const s of i.parseDatagram(t,parseInt(null===(r=y)||void 0===r?void 0:r[2]),parseInt(null===(o=y)||void 0===o?void 0:o[3]))){var r,o,n;null===(n=S)||void 0===n||n(void 0),m.emit(e,s),D()}})(),(async()=>{for await(const s of i.parseDatagram(o,parseInt(null===(t=y)||void 0===t?void 0:t[2]),parseInt(null===(r=y)||void 0===r?void 0:r[3]),"rtcp")){var t,r,n;null===(n=S)||void 0===n||n(void 0),m.emit(e,s),D()}})()}else O.push(...i.outputArguments,"pipe:"+I++),x.push("pipe")}O.push("-sdp_file","pipe:"+I++),x.push("pipe"),O.unshift("-hide_banner"),(0,c.safePrintFFmpegArguments)(i,O);const C=o.default.spawn(await l.getFFmpegPath(),O,{stdio:x});(0,c.ffmpegLogInitialOutput)(i,C),C.on("exit",M);const A=new Promise((e=>{const t=[];C.stdio[I-1].on("data",(i=>{t.push(i),e(t)}))}));let k=0;return Object.keys(t.parsers).forEach((async e=>{const r=t.parsers[e];if(!r.parse)return;const o=C.stdio[3+k];k++;try{for await(const t of r.parse(o,parseInt(null===(n=y)||void 0===n?void 0:n[2]),parseInt(null===(s=y)||void 0===s?void 0:s[3]))){var n,s,a;null===(a=S)||void 0===a||a(void 0),m.emit(e,t),D()}}catch(e){i.error("rebroadcast parse error",e),M()}})),f(C).then((e=>g=e)),h(C).then((e=>v=e)),p(C).then((e=>y=e)),await P,S=void 0,b=void 0,clearTimeout(a),{sdp:A,inputAudioCodec:g,inputVideoCodec:v,inputVideoResolution:y,resetActivityTimer:D,isActive:()=>u,kill:M,mediaStreamOptions:e.mediaStreamOptions||{id:void 0,name:void 0},on(e,t){return m.on(e,t),this},once(e,t){return m.once(e,t),this},removeListener(e,t){return m.removeListener(e,t),this}}};var r=i(808),o=u(i(81)),n=i(769),s=i(361),a=u(i(510)),c=i(833),d=u(i(891));function u(e){return e&&e.__esModule?e:{default:e}}const{mediaManager:l}=a.default;async function p(e){return new Promise((t=>{const i=r=>{const o=r.toString(),n=/(([0-9]{2,5})x([0-9]{2,5}))/.exec(o);n&&(e.stdout.removeListener("data",i),e.stderr.removeListener("data",i),t(n))};e.stdout.on("data",i),e.stderr.on("data",i)}))}async function m(e,t){return new Promise((i=>{const r=o=>{const n=o.toString(),s=n.indexOf(`${t}: `);if(-1!==s){const o=n.substring(s+t.length+1).trim();let a=o.indexOf(" ");const c=o.indexOf(",");-1!==a&&c<a&&(a=c),-1!==a&&(e.stdout.removeListener("data",r),e.stderr.removeListener("data",r),i(o.substring(0,a)))}};e.stdout.on("data",r),e.stderr.on("data",r)}))}async function h(e){return m(e,"Video")}async function f(e){return m(e,"Audio")}},769:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.bind=async function(e,t){return e.bind(t),await(0,n.once)(e,"listening"),{port:t,url:`udp://127.0.0.1:${t}`}},t.bindZero=async function(e){e.bind(0),await(0,n.once)(e,"listening");const{port:t}=e.address();return{port:t,url:`udp://127.0.0.1:${t}`}},t.listenZero=s,t.listenZeroSingleClient=async function(){const e=new o.default.Server,t=await s(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{url:`tcp://127.0.0.1:${t}`,port:t,clientPromise:i}};var r,o=(r=i(808))&&r.__esModule?r:{default:r},n=i(361);async function s(e){return e.listen(0),await(0,n.once)(e,"listening"),e.address().port}},833:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=i(568);Object.keys(r).forEach((function(e){"default"!==e&&"__esModule"!==e&&(e in t&&t[e]===r[e]||Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}}))}))},701:(e,t)=>{"use strict";async function i(e,t){if(!t)return Buffer.alloc(0);{const i=e.read(t);if(i)return i}return new Promise(((i,r)=>{const o=()=>{const r=e.read(t);r&&(s(),i(r))},n=()=>{s(),r(new Error(`stream ended during read for minimum ${t} bytes`))},s=()=>{e.removeListener("readable",o),e.removeListener("end",n)};e.on("readable",o),e.on("end",n)}))}Object.defineProperty(t,"__esModule",{value:!0}),t.readLength=i,t.readLine=async function(e){return o(e,r)},t.readUntil=o;const r="\n".charCodeAt(0);async function o(e,t){const r=[];let o=0;for(;;){const n=await i(e,1);if(!n)throw new Error("end of stream");if(n[0]===t)break;r[o++]=n[0]}return Buffer.from(r).toString()}},567:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SettingsMixinDeviceBase=void 0;var r=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var i=o(t);if(i&&i.has(e))return i.get(e);var r={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in e)if("default"!==s&&Object.prototype.hasOwnProperty.call(e,s)){var a=n?Object.getOwnPropertyDescriptor(e,s):null;a&&(a.get||a.set)?Object.defineProperty(r,s,a):r[s]=e[s]}r.default=e,i&&i.set(e,r);return r}(i(510));function o(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,i=new WeakMap;return(o=function(e){return e?i:t})(e)}const{deviceManager:n}=r.default;class SettingsMixinDeviceBase extends r.MixinDeviceBase{constructor(e,t,i){super(e,i.mixinDeviceInterfaces,t,i.providerNativeId,i.mixinStorageSuffix),this.settingsGroup=i.group,this.settingsGroupKey=i.groupKey,process.nextTick((()=>n.onMixinEvent(this.id,this,r.ScryptedInterface.Settings,null)))}async getSettings(){const e=this.mixinDeviceInterfaces.includes(r.ScryptedInterface.Settings)?this.mixinDevice.getSettings():void 0,t=this.getMixinSettings(),i=[];try{const t=await e||[];i.push(...t)}catch(e){const t=this.name,r=`${t} Extension settings failed to load.`;this.console.error(r,e),i.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:r,readonly:!0})}try{const e=await t||[];for(const t of e)t.group=t.group||this.settingsGroup,t.key=this.settingsGroupKey+":"+t.key;i.push(...e)}catch(e){const t=n.getDeviceState(this.mixinProviderNativeId).name,r=`${t} Extension settings failed to load.`;this.console.error(r,e),i.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:r,readonly:!0})}return i}async putSetting(e,t){const i=this.settingsGroupKey+":";if(null==e||!e.startsWith(i))return this.mixinDevice.putSetting(e,t);await this.putMixinSetting(e.substring(i.length),t),n.onMixinEvent(this.id,this,r.ScryptedInterface.Settings,null)}release(){n.onMixinEvent(this.id,this,r.ScryptedInterface.Settings,null)}}t.SettingsMixinDeviceBase=SettingsMixinDeviceBase},129:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.PIXEL_FORMAT_YUV420P=t.PIXEL_FORMAT_RGB24=void 0,t.createDgramParser=a,t.createFragmentedMp4Parser=function(e){return{container:"mp4",outputArguments:[...(null==e?void 0:e.vcodec)||[],...(null==e?void 0:e.acodec)||[],"-movflags","frag_keyframe+empty_moov+default_base_moof","-f","mp4"],async*parse(e){const t=c(e);let i,r,o;for await(const e of t)i?r||(r=e):i=e,yield{startStream:o,chunks:[e.header,e.data],type:e.type},i&&r&&!o&&(o=Buffer.concat([i.header,i.data,r.header,r.data]))},findSyncFrame:n}},t.createMpegTsParser=function(e){return{container:"mpegts",outputArguments:[...(null==e?void 0:e.vcodec)||[],...(null==e?void 0:e.acodec)||[],"-f","mpegts"],parse:s(188,(e=>{if(71!=e[0])throw new Error("Invalid sync byte in mpeg-ts packet. Terminating stream.")})),findSyncFrame(e){for(let t=0;t<e.length;t++){const i=e[t];for(let r=0;r<i.chunks.length;r++){const o=i.chunks[r];let n=0;for(;n+188<o.length;){const i=o.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}}},t.createRawVideoParser=function(e){var t;const i=(null===(t=e)||void 0===t?void 0:t.pixelFormat)||d;let r;e=e||{};const{size:s,everyNFrames:a}=e;s&&(r=`scale=${s.width}:${s.height}`);a&&a>1&&(r?r+=",":r="",r+=`select=not(mod(n\\,${a}))`);return{container:"rawvideo",outputArguments:[...r?["-vf",r]:[],"-an","-vcodec","rawvideo","-pix_fmt",i.name,"-f","rawvideo"],async*parse(e,t,r){if(!t||!r)throw new Error("error parsing rawvideo, unknown width and height");t=(null==s?void 0:s.width)||t,r=(null==s?void 0:s.height)||r;const n=i.computeLength(t,r);for(;;){const i=await(0,o.readLength)(e,n);yield{chunks:[i],width:t,height:r}}},findSyncFrame:n}},t.createRtpParser=function(...e){return{container:"rtsp",inputArguments:["-v","verbose","-rtsp_transport","tcp"],outputArguments:[...e,"-f","rtp"],parseDatagram:a(),findSyncFrame:n}},t.parseFragmentedMP4=c;var r=i(361),o=i(701);function n(e){return e}function s(e,t){return async function*(i){let o=[],n=0;for(;;){const s=i.read();if(!s){await(0,r.once)(i,"readable");continue}if(o.push(s),n+=s.length,n<e)continue;const a=Buffer.concat(o);null==t||t(a);const c=a.length%e,d=a.slice(0,a.length-c),u=a.slice(a.length-c);o=[u],n=u.length,yield{chunks:[d]}}}}function a(){return async function*(e,t,i,o){for(;;){const[t]=await(0,r.once)(e,"message");yield{chunks:[t],type:o}}}}async function*c(e){for(;;){const t=await(0,o.readLength)(e,8),i=t.readInt32BE(0)-8,r=t.slice(4).toString(),n=await(0,o.readLength)(e,i);yield{header:t,length:i,type:r,data:n}}}const d={name:"yuv420p",computeLength:(e,t)=>e*t*1.5};t.PIXEL_FORMAT_YUV420P=d;t.PIXEL_FORMAT_RGB24={name:"rgb24",computeLength:(e,t)=>e*t*3}},168:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.RtspServer=void 0;var r=i(701),o=i(113);t.RtspServer=class RtspServer{constructor(e,t,i){this.socket=e,this.sdp=t,this.playing=i,this.session=(0,o.randomBytes)(4).toString("hex"),this.loop()}async loop(){try{let e=[];for(;;){let t=await(0,r.readLine)(this.socket);if(t=t.trim(),t)e.push(t);else{if(!await this.headers(e))break;e=[]}}}catch(e){}}send(e,t){const i=Buffer.alloc(4);i.writeUInt8(36,0),i.writeUInt8(t,1),i.writeUInt16BE(e.length,2),this.socket.write(i),this.socket.write(Buffer.from(e))}sendVideo(e,t){this.send(e,t?1:0)}sendAudio(e,t){this.send(e,t?3:2)}options(e,t){const i={};i.CSeq=t.cseq,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={};i.Transport=t.transport,i.Session=this.session,this.respond(200,"OK",t,i)}play(e,t){const i={};i["RTP-Info"]=`url=${e}/trackID=0;seq=0;rtptime=0,url=${e}/trackID=1;seq=0;rtptime=0`,i.Range="npt=now-",i.Session=this.session,this.respond(200,"OK",t,i),this.playing(this)}async headers(e){let[t,i]=e[0].split(" ",2);t=t.toLowerCase();const r=function(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}(e);if(this[t])return await this[t](i,r),"play"!==t;this.respond(400,"Bad Request",r,{})}respond(e,t,i,r,o){let n=`RTSP/1.0 ${e} ${t}\r\n`;i.cseq&&(r.CSeq=i.cseq),o&&(r["Content-Length"]=o.length.toString());for(const[e,t]of Object.entries(r))n+=`${e}: ${t}\r\n`;n+="\r\n",this.socket.write(n),o&&this.socket.write(o)}}},510:(e,t,i)=>{"use strict";var r=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]},o=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,o(i(393),t);const n=i(393);class ScryptedDeviceBase 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=ScryptedDeviceBase;class MixinDeviceBase extends n.DeviceBase{constructor(e,t,i,r,o){super(),this.mixinDevice=e,this.mixinDeviceInterfaces=t,this.mixinProviderNativeId=r,this._mixinStorageSuffix=o,this._listeners=new Set,this._deviceState=i}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=MixinDeviceBase,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(ScryptedDeviceBase.prototype,i,{set:t(i),get:e(i)}),Object.defineProperty(MixinDeviceBase.prototype,i,{set:t(i),get:e(i)})}();let s={};try{s=Object.assign(s,{log:deviceManager.getDeviceLogger(void 0),deviceManager,endpointManager,mediaManager,systemManager,pluginHostAPI})}catch(e){console.error("sdk initialization error, import @scrypted/sdk/types instead",e)}t.default=s},393:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ThermostatMode=t.TemperatureUnit=t.ScryptedMimeTypes=t.ScryptedInterfaceProperty=t.ScryptedInterfaceDescriptors=t.ScryptedInterface=t.ScryptedDeviceType=t.SCRYPTED_MEDIA_SCHEME=t.MediaPlayerState=t.LockState=t.HumidityMode=t.FanMode=t.DeviceBase=void 0;let i;t.DeviceBase=class DeviceBase{},t.ScryptedInterfaceProperty=i,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"}(i||(t.ScryptedInterfaceProperty=i={}));let r,o,n,s,a,c,d,u,l;t.ScryptedInterfaceDescriptors={ScryptedDevice:{name:"ScryptedDevice",methods:["listen","probe","setName","setRoom","setType"],properties:["id","info","interfaces","mixins","name","providedInterfaces","providedName","providedRoom","providedType","providerId","room","type"]},ScryptedPlugin:{name:"ScryptedPlugin",methods:["getPluginJson"],properties:[]},OnOff:{name:"OnOff",methods:["turnOff","turnOn"],properties:["on"]},Brightness:{name:"Brightness",methods:["setBrightness"],properties:["brightness"]},ColorSettingTemperature:{name:"ColorSettingTemperature",methods:["getTemperatureMaxK","getTemperatureMinK","setColorTemperature"],properties:["colorTemperature"]},ColorSettingRgb:{name:"ColorSettingRgb",methods:["setRgb"],properties:["rgb"]},ColorSettingHsv:{name:"ColorSettingHsv",methods:["setHsv"],properties:["hsv"]},Notifier:{name:"Notifier",methods:["sendNotification"],properties:[]},StartStop:{name:"StartStop",methods:["start","stop"],properties:["running"]},Pause:{name:"Pause",methods:["pause","resume"],properties:["paused"]},Dock:{name:"Dock",methods:["dock"],properties:["docked"]},TemperatureSetting:{name:"TemperatureSetting",methods:["setThermostatMode","setThermostatSetpoint","setThermostatSetpointHigh","setThermostatSetpointLow"],properties:["thermostatActiveMode","thermostatAvailableModes","thermostatMode","thermostatSetpoint","thermostatSetpointHigh","thermostatSetpointLow"]},Thermometer:{name:"Thermometer",methods:["setTemperatureUnit"],properties:["temperature","temperatureUnit"]},HumiditySensor:{name:"HumiditySensor",methods:[],properties:["humidity"]},Camera:{name:"Camera",methods:["getPictureOptions","takePicture"],properties:[]},VideoCamera:{name:"VideoCamera",methods:["getVideoStream","getVideoStreamOptions"],properties:[]},VideoCameraConfiguration:{name:"VideoCameraConfiguration",methods:["setVideoStreamOptions"],properties:[]},Intercom:{name:"Intercom",methods:["startIntercom","stopIntercom"],properties:[]},Lock:{name:"Lock",methods:["lock","unlock"],properties:["lockState"]},PasswordStore:{name:"PasswordStore",methods:["addPassword","getPasswords","removePassword"],properties:[]},Authenticator:{name:"Authenticator",methods:["checkPassword"],properties:[]},Scene:{name:"Scene",methods:["activate","deactivate","isReversible"],properties:[]},Entry:{name:"Entry",methods:["closeEntry","openEntry"],properties:[]},EntrySensor:{name:"EntrySensor",methods:[],properties:["entryOpen"]},DeviceProvider:{name:"DeviceProvider",methods:["getDevice"],properties:[]},DeviceDiscovery:{name:"DeviceDiscovery",methods:["discoverDevices"],properties:[]},DeviceCreator:{name:"DeviceCreator",methods:["createDevice","getCreateDeviceSettings"],properties:[]},Battery:{name:"Battery",methods:[],properties:["batteryLevel"]},Refresh:{name:"Refresh",methods:["getRefreshFrequency","refresh"],properties:[]},MediaPlayer:{name:"MediaPlayer",methods:["getMediaStatus","load","seek","skipNext","skipPrevious"],properties:[]},Online:{name:"Online",methods:[],properties:["online"]},SoftwareUpdate:{name:"SoftwareUpdate",methods:["checkForUpdate","installUpdate"],properties:["updateAvailable"]},BufferConverter:{name:"BufferConverter",methods:["convert"],properties:["fromMimeType","toMimeType"]},Settings:{name:"Settings",methods:["getSettings","putSetting"],properties:[]},BinarySensor:{name:"BinarySensor",methods:[],properties:["binaryState"]},IntrusionSensor:{name:"IntrusionSensor",methods:[],properties:["intrusionDetected"]},PowerSensor:{name:"PowerSensor",methods:[],properties:["powerDetected"]},AudioSensor:{name:"AudioSensor",methods:[],properties:["audioDetected"]},MotionSensor:{name:"MotionSensor",methods:[],properties:["motionDetected"]},AmbientLightSensor:{name:"AmbientLightSensor",methods:[],properties:["ambientLight"]},OccupancySensor:{name:"OccupancySensor",methods:[],properties:["occupied"]},FloodSensor:{name:"FloodSensor",methods:[],properties:["flooded"]},UltravioletSensor:{name:"UltravioletSensor",methods:[],properties:["ultraviolet"]},LuminanceSensor:{name:"LuminanceSensor",methods:[],properties:["luminance"]},PositionSensor:{name:"PositionSensor",methods:[],properties:["position"]},Readme:{name:"Readme",methods:["getReadmeMarkdown"],properties:[]},OauthClient:{name:"OauthClient",methods:["getOauthUrl","onOauthCallback"],properties:[]},MixinProvider:{name:"MixinProvider",methods:["canMixin","getMixin","releaseMixin"],properties:[]},HttpRequestHandler:{name:"HttpRequestHandler",methods:["onRequest"],properties:[]},EngineIOHandler:{name:"EngineIOHandler",methods:["onConnection"],properties:[]},PushHandler:{name:"PushHandler",methods:["onPush"],properties:[]},Program:{name:"Program",methods:["run"],properties:[]},Scriptable:{name:"Scriptable",methods:["eval","loadScripts","saveScript"],properties:[]},ObjectDetector:{name:"ObjectDetector",methods:["getDetectionInput","getObjectTypes"],properties:[]},ObjectDetection:{name:"ObjectDetection",methods:["detectObjects","getDetectionModel"],properties:[]},HumiditySetting:{name:"HumiditySetting",methods:["setHumidity"],properties:["humiditySetting"]},Fan:{name:"Fan",methods:["setFan"],properties:["fan"]}},t.ScryptedDeviceType=r,function(e){e.Builtin="Builtin",e.Camera="Camera",e.Fan="Fan",e.Light="Light",e.Switch="Switch",e.Outlet="Outlet",e.Sensor="Sensor",e.Scene="Scene",e.Program="Program",e.Automation="Automation",e.Vacuum="Vacuum",e.Notifier="Notifier",e.Thermostat="Thermostat",e.Lock="Lock",e.PasswordControl="PasswordControl",e.Display="Display",e.Speaker="Speaker",e.Event="Event",e.Entry="Entry",e.Garage="Garage",e.DeviceProvider="DeviceProvider",e.DataSource="DataSource",e.API="API",e.Doorbell="Doorbell",e.Irrigation="Irrigation",e.Valve="Valve",e.Person="Person",e.Unknown="Unknown"}(r||(t.ScryptedDeviceType=r={})),t.HumidityMode=o,function(e){e.Humidify="Humidify",e.Dehumidify="Dehumidify",e.Auto="Auto",e.Off="Off"}(o||(t.HumidityMode=o={})),t.FanMode=n,function(e){e.Auto="Auto",e.Manual="Manual"}(n||(t.FanMode=n={})),t.TemperatureUnit=s,function(e){e.C="C",e.F="F"}(s||(t.TemperatureUnit=s={})),t.ThermostatMode=a,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"}(a||(t.ThermostatMode=a={})),t.LockState=c,function(e){e.Locked="Locked",e.Unlocked="Unlocked",e.Jammed="Jammed"}(c||(t.LockState=c={})),t.MediaPlayerState=d,function(e){e.Idle="Idle",e.Playing="Playing",e.Paused="Paused",e.Buffering="Buffering"}(d||(t.MediaPlayerState=d={})),t.ScryptedInterface=u,function(e){e.ScryptedDevice="ScryptedDevice",e.ScryptedPlugin="ScryptedPlugin",e.OnOff="OnOff",e.Brightness="Brightness",e.ColorSettingTemperature="ColorSettingTemperature",e.ColorSettingRgb="ColorSettingRgb",e.ColorSettingHsv="ColorSettingHsv",e.Notifier="Notifier",e.StartStop="StartStop",e.Pause="Pause",e.Dock="Dock",e.TemperatureSetting="TemperatureSetting",e.Thermometer="Thermometer",e.HumiditySensor="HumiditySensor",e.Camera="Camera",e.VideoCamera="VideoCamera",e.VideoCameraConfiguration="VideoCameraConfiguration",e.Intercom="Intercom",e.Lock="Lock",e.PasswordStore="PasswordStore",e.Authenticator="Authenticator",e.Scene="Scene",e.Entry="Entry",e.EntrySensor="EntrySensor",e.DeviceProvider="DeviceProvider",e.DeviceDiscovery="DeviceDiscovery",e.DeviceCreator="DeviceCreator",e.Battery="Battery",e.Refresh="Refresh",e.MediaPlayer="MediaPlayer",e.Online="Online",e.SoftwareUpdate="SoftwareUpdate",e.BufferConverter="BufferConverter",e.Settings="Settings",e.BinarySensor="BinarySensor",e.IntrusionSensor="IntrusionSensor",e.PowerSensor="PowerSensor",e.AudioSensor="AudioSensor",e.MotionSensor="MotionSensor",e.AmbientLightSensor="AmbientLightSensor",e.OccupancySensor="OccupancySensor",e.FloodSensor="FloodSensor",e.UltravioletSensor="UltravioletSensor",e.LuminanceSensor="LuminanceSensor",e.PositionSensor="PositionSensor",e.Readme="Readme",e.OauthClient="OauthClient",e.MixinProvider="MixinProvider",e.HttpRequestHandler="HttpRequestHandler",e.EngineIOHandler="EngineIOHandler",e.PushHandler="PushHandler",e.Program="Program",e.Scriptable="Scriptable",e.ObjectDetector="ObjectDetector",e.ObjectDetection="ObjectDetection",e.HumiditySetting="HumiditySetting",e.Fan="Fan"}(u||(t.ScryptedInterface=u={})),t.ScryptedMimeTypes=l,function(e){e.AcceptUrlParameter="accept-url",e.Url="text/x-uri",e.InsecureLocalUrl="text/x-insecure-local-uri",e.LocalUrl="text/x-local-uri",e.PushEndpoint="text/x-push-endpoint",e.MediaStreamUrl="text/x-media-url",e.FFmpegInput="x-scrypted/x-ffmpeg-input",e.RTCAVSignalingPrefix="x-scrypted-rtc-signaling-",e.RTCAVOffer="x-scrypted/x-rtc-av-offer",e.RTCAVAnswer="x-scrypted/x-rtc-av-answer"}(l||(t.ScryptedMimeTypes=l={}));t.SCRYPTED_MEDIA_SCHEME="scryped-media://"},568:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ffmpegLogInitialOutput=function(e,t,r){var o,n;function s(e){const o=n=>{const s=n.toString();for(const e of i)if(-1!==s.indexOf(e))return;if(!r&&(-1!==s.indexOf("frame=")||-1!==s.indexOf("size=")))return e(s),e("video/audio detected, discarding further input"),t.stdout.removeListener("data",o),void t.stderr.removeListener("data",o);e(s)};return o}null===(o=t.stdout)||void 0===o||o.on("data",s(e.log)),null===(n=t.stderr)||void 0===n||n.on("data",s(e.error)),t.on("exit",(()=>e.log("ffmpeg exited")))},t.safePrintFFmpegArguments=function(e,t){const i=[];for(const e of t)try{const t=new URL(e);i.push(`${t.protocol}[REDACTED]`)}catch(t){i.push(e)}e.log(i.join(" "))};const i=["decode_slice_header error","no frame!","non-existing PPS"]},81:e=>{"use strict";e.exports=require("child_process")},113:e=>{"use strict";e.exports=require("crypto")},891:e=>{"use strict";e.exports=require("dgram")},361:e=>{"use strict";e.exports=require("events")},808:e=>{"use strict";e.exports=require("net")}},t={};function i(r){var o=t[r];if(void 0!==o)return o.exports;var n=t[r]={exports:{}};return e[r](n,n.exports,i),n.exports}var r={};(()=>{"use strict";var e=r;Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0;var t=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var i=l(t);if(i&&i.has(e))return i.get(e);var r={},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var n in e)if("default"!==n&&Object.prototype.hasOwnProperty.call(e,n)){var s=o?Object.getOwnPropertyDescriptor(e,n):null;s&&(s.get||s.set)?Object.defineProperty(r,n,s):r[n]=e[n]}r.default=e,i&&i.set(e,r);return r}(i(510)),o=i(361),n=i(567),s=i(201),a=i(129),c=i(454),d=i(769),u=i(168);function l(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,i=new WeakMap;return(l=function(e){return e?i:t})(e)}function p(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}const{mediaManager:m,log:h,systemManager:f,deviceManager:g}=t.default,v=1e4,y="prebufferDuration",S="sendKeyframe",b="Default",P="AAC or No Audio",M=`${P} (Copy)`,w="Compatible Audio",D="Other Audio",O=["aac","mp3","mp2","opus"],x="-fflags +genpts",I=[P,w,D],C=["mpegts","mp4","rtpvideo","rtpaudio"];class PrebufferSession{constructor(e,t,i,r){p(this,"prebuffers",{mp4:[],mpegts:[],rtpvideo:[],rtpaudio:[]}),p(this,"detectedIdrInterval",0),p(this,"prevIdr",0),p(this,"audioDisabled",!1),p(this,"activeClients",0),this.mixin=e,this.streamName=t,this.streamId=i,this.stopInactive=r,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}clearPrebuffers(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtpaudio=[],this.prebuffers.rtpvideo=[]}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)||"";I.find((t=>e.startsWith(t)))||(e="");const t=-1!==e.indexOf(P),i=-1!==e.indexOf(w),r=-1!==e.indexOf(D);return{isUsingDefaultAudioConfig:!(t||i||r),aacAudio:t,compatibleAudio:i,reencodeAudio:r}}async getMixinSettings(){const e=[],t=this.parserSession;let i=0,r=0;for(const e of this.prebuffers.mp4){r=r||e.time;for(const t of e.chunk.chunks)i+=t.byteLength}const o=Date.now()-r,n=Math.round(i/o*8),s=this.streamName?`Rebroadcast: ${this.streamName}`:"Rebroadcast";var a,c,d;(e.push({title:"Audio Codec Transcoding",group:s,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)||b,choices:[b,M,"Compatible Audio (Copy)","Other Audio (Transcode)"]},{title:"FFmpeg Input Arguments Prefix",group:s,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:x,choices:[x,"-use_wallclock_as_timestamps 1","-v verbose"],combobox:!0},{title:"Rebroadcast Mode",group:s,description:"THIS FEATURE IS IN TESTING. DO NOT CHANGE THIS FROM MPEG-TS. The stream format to use when rebroadcasting.",placeholder:"MPEG-TS",choices:["MPEG-TS","RTSP"],key:this.rebroadcastModeKey,value:this.storage.getItem(this.rebroadcastModeKey)||"MPEG-TS"}),t)?e.push({key:"detectedResolution",group:s,title:"Detected Resolution and Bitrate",readonly:!0,value:`${(null==t||null===(a=t.inputVideoResolution)||void 0===a?void 0:a[0])||"unknown"} @ ${n||"unknown"} Kb/s`,description:"Configuring your camera to 1920x1080, 2000Kb/S, Variable Bit Rate, is recommended."},{key:"detectedCodec",group:s,title:"Detected Video/Audio Codecs",readonly:!0,value:((null==t||null===(c=t.inputVideoCodec)||void 0===c?void 0:c.toString())||"unknown")+"/"+((null==t||null===(d=t.inputAudioCodec)||void 0===d?void 0:d.toString())||"unknown"),description:"Configuring your camera to H264 video and AAC/MP3/MP2/Opus audio is recommended."},{key:"detectedKeyframe",group:s,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).toString()||"none"}):e.push({title:"Status",group:s,key:"status",description:"Rebroadcast is currently idle and will be started automatically on demand.",value:"Idle",readonly:!0});return e}async startPrebufferSession(){var e,i,r,o,n,c,d;this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtpvideo=[],this.prebuffers.rtpaudio=[];const u=parseInt(this.storage.getItem(y))||v;let l;try{l=(await this.mixinDevice.getVideoStreamOptions()).find((e=>e.id===this.streamId))}catch(e){}const p=null===(null===(e=l)||void 0===e?void 0:e.audio),f=null===(i=l)||void 0===i||null===(r=i.audio)||void 0===r?void 0:r.codec,{isUsingDefaultAudioConfig:g,aacAudio:S,compatibleAudio:b,reencodeAudio:M}=this.getAudioConfig();let w=!1;p||f||!g||void 0!==this.detectedAudioCodec||(this.console.warn("Camera did not report an audio codec, muting the audio stream and probing the codec."),w=!0),!p&&f&&void 0!==this.detectedAudioCodec&&this.detectedAudioCodec!==f&&this.console.warn("Audio codec plugin reported vs detected mismatch",f,this.detectedAudioCodec);const D=void 0===this.detectedAudioCodec?null==f?void 0:f.toLowerCase():null===(o=this.detectedAudioCodec)||void 0===o?void 0:o.toLowerCase(),I=!O.includes(D);w||!1===(null===(n=l)||void 0===n?void 0:n.userConfigurable)||p||(I?(g&&h.a(`${this.mixin.name} is using the ${D} 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:",D)):!p&&g&&void 0===f&&void 0!==this.detectedAudioCodec&&("aac"===this.detectedAudioCodec?h.a(`${this.mixin.name} did not report a codec and ${this.detectedAudioCodec} was found during probe. Select '${P}' in the camera stream's Rebroadcast settings to suppress this alert.`):h.a(`${this.mixin.name} did not report a codec and ${this.detectedAudioCodec} was found during probe. Select 'Compatible Audio' in the camera stream's Rebroadcast settings to suppress this alert.`)));const A=await this.mixinDevice.getVideoStream(l),k=await m.convertMediaObjectToBuffer(A,t.ScryptedMimeTypes.FFmpegInput),T=JSON.parse(k.toString()),_=["-bsf:a","aac_adtstoasc"],E=[];let L;this.audioDisabled=!1;const R=null===this.detectedAudioCodec;let B=!1;var j;!w&&g&&I&&(!1===(null===(j=l)||void 0===j?void 0:j.userConfigurable)?this.console.log("camera reports it is not user configurable. transcoding due to incompatible codec",D):this.console.log("camera audio transcoding due to incompatible codec. configure the camera to use a compatible codec if possible."),B=!0);if(p||w||R)L=["-an"],this.audioDisabled=!0;else if(M||B)L=["-bsf:a","aac_adtstoasc","-acodec","libfdk_aac","-ar","8k","-b:a","100k","-bufsize","400k","-ac","1","-profile:a","aac_low","-flags","+global_header"];else if(S)L=["-acodec","copy"],L.push(..._);else if(b)L=["-acodec","copy"],L.push(...E);else{L=["-acodec","copy"];const e="aac"===D?_:E;L.push(...e)}const H=["-vcodec","copy"],V={console:this.console,timeout:6e4,parsers:{mp4:(0,a.createFragmentedMp4Parser)({vcodec:H,acodec:L})}};"RTSP"===this.storage.getItem(this.rebroadcastModeKey)?(V.parsers.rtpvideo=(0,a.createRtpParser)("-an","-vcodec","copy"),V.parsers.rtpaudio=(0,a.createRtpParser)("-vn","-acodec","copy")):V.parsers.mpegts=(0,a.createMpegTsParser)({vcodec:H,acodec:L}),this.parsers=V.parsers;const F=this.storage.getItem(this.ffmpegInputArgumentsKey)||x;T.inputArguments.unshift(...F.split(" "));const N=await(0,s.startParserSession)(T,V);if(N.inputAudioCodec?O.includes(null===(c=N.inputAudioCodec)||void 0===c?void 0:c.toLowerCase())?this.console.log("Detected audio codec is mp4/mpegts compatible.",N.inputAudioCodec):this.console.log("Detected audio codec is not mp4/mpegts compatible.",N.inputAudioCodec):this.console.log("No audio stream detected."),this.detectedAudioCodec=N.inputAudioCodec||null,this.detectedVideoCodec=N.inputVideoCodec||null,"h264"!==N.inputVideoCodec&&this.console.error("Video codec is not h264. If there are errors, try changing your camera's encoder output."),w)return this.console.warn("Audio probe complete, ending rebroadcast session and restarting with detected codecs."),N.kill(),this.startPrebufferSession();if(this.parserSession=N,null!==(d=T.mediaStreamOptions)&&void 0!==d&&d.refreshAt){let e,i=T.mediaStreamOptions;const r=async()=>{if(!N.isActive)return;const e=await this.mixinDevice.getVideoStream(i),r=await m.convertMediaObjectToBuffer(e,t.ScryptedMimeTypes.FFmpegInput),n=JSON.parse(r.toString());i=n.mediaStreamOptions,o(n)},o=t=>{const i=t.mediaStreamOptions.refreshAt-Date.now()-3e4;this.console.log("refreshing media stream in",i),e=setTimeout(r,i)};o(T),N.once("killed",(()=>clearTimeout(e)))}N.once("killed",(()=>{this.parserSessionPromise=void 0,this.parserSession===N&&(this.parserSession=void 0)}));for(const e of C){let t=0;N.on(e,(i=>{const r=this.prebuffers[e],o=Date.now();for("mdat"===i.type&&(this.prevIdr&&(this.detectedIdrInterval=o-this.prevIdr),this.prevIdr=o),r.push({time:o,chunk:i});r.length&&r[0].time<o-u;)r.shift(),t++;t>1e3&&(this.prebuffers[e]=r.slice(),t=0)}))}return N}printActiveClients(){this.console.log(this.streamName,"active rebroadcast clients:",this.activeClients)}inactivityCheck(e){this.printActiveClients(),this.stopInactive&&(this.activeClients||(clearTimeout(this.inactivityTimeout),this.inactivityTimeout=setTimeout((()=>{this.activeClients||(this.console.log(this.streamName,"terminating rebroadcast due to inactivity"),e.kill())}),3e4)))}async getVideoStream(e){var t,i;this.ensurePrebufferSession();const r=await this.parserSessionPromise,o="false"!==this.storage.getItem(S),n=(null==e?void 0:e.prebuffer)||(o?1.5*Math.max(4e3,this.detectedIdrInterval||4e3):0);this.console.log(this.streamName,"prebuffer request started");const a="RTSP"===this.storage.getItem(this.rebroadcastModeKey)?"rtpvideo":"mpegts",c=this.parsers[null==e?void 0:e.container]?null==e?void 0:e.container:a,l=Object.assign({},r.mediaStreamOptions);l.prebuffer=n;const{reencodeAudio:p}=this.getAudioConfig();this.audioDisabled?l.audio=null:l.audio=p?{codec:"aac",encoder:"libfdk_aac",profile:"aac_low"}:{codec:null==r?void 0:r.inputAudioCodec},l.video&&null!==(t=r.inputVideoResolution)&&void 0!==t&&t[2]&&null!==(i=r.inputVideoResolution)&&void 0!==i&&i[3]&&Object.assign(l.video,{width:parseInt(r.inputVideoResolution[2]),height:parseInt(r.inputVideoResolution[3])});const h=Date.now();let f=0;const g=this.prebuffers[c];for(const e of g)if(!(e.time<h-n))for(const t of e.chunk.chunks)f+=t.length;const v=Math.max(5e5,f).toString(),y=await(async e=>{const t=this.prebuffers[e];if(this.parsers[e].parseDatagram){let e=Buffer.concat(await r.sdp).toString();const t=e.split("\n").map((e=>e.trim())).filter((e=>"c=IN IP4 127.0.0.1"!==e));let i=0;const o=e=>{const r=t.findIndex((t=>t.startsWith(`m=${e}`)));-1!==r&&(t.splice(r+1,0,"a=recvonly",`a=control:trackID=${i}`),i++)};o("video"),o("audio"),e=t.join("\r\n"),e=e.replace(/m=audio .*? /,"m=audio 0 "),e=e.replace(/m=video .*? /,"m=video 0 "),e=e.replace("t=0 0","c=IN IP4 127.0.0.1\r\nt=0 0\r\na=recvonly\r\na=control:*\r\na=range:npt=now-"),this.console.log(e);let s,a=!1;const c=e=>t=>{a&&e(t.chunks[0],"rtcp"===t.type)},l=c(((e,t)=>s.sendVideo(e,t))),p=c(((e,t)=>s.sendAudio(e,t))),m=()=>{r.removeListener("rtpvideo",l),r.removeListener("rtpaudio",p),r.removeListener("killed",m)};r.once("killed",m);const h=await(0,d.listenZeroSingleClient)();h.clientPromise.then((async t=>{this.activeClients++,this.printActiveClients(),t.once("close",(()=>{this.activeClients--,this.inactivityCheck(r),m()})),s=await new Promise((i=>new u.RtspServer(t,e,(e=>i(e))))),a=!0;let i=0,o=0;const c=Date.now();for(;i<this.prebuffers.rtpvideo.length&&this.prebuffers.rtpvideo[i].time<c-n;)i++;for(;o<this.prebuffers.rtpaudio.length&&this.prebuffers.rtpaudio[o].time<c-n;)o++;const d=e=>{l(e),i++},h=e=>{p(e),o++};for(;i<this.prebuffers.rtpvideo.length||o<this.prebuffers.rtpaudio.length;){const e=this.prebuffers.rtpvideo[i],t=this.prebuffers.rtpaudio[o];t?e?t.time<e.time?h(t.chunk):d(e.chunk):h(t.chunk):d(e.chunk)}})).catch((e=>{this.console.error("failure client",e),m()})),r.on("rtpvideo",l),r.on("rtpaudio",p);return h.url.replace("tcp://","rtsp://")}const{server:i,port:o}=await(0,s.createRebroadcaster)({console:this.console,connect:(o,s)=>{this.activeClients++,this.printActiveClients(),i.close();const a=Date.now(),c=e=>{o(e)>1e8&&(this.console.log("more than 100MB has been buffered, did downstream die? killing connection.",this.streamName),d())},d=()=>{s(),this.console.log(this.streamName,"prebuffer request ended"),r.removeListener(e,c),r.removeListener("killed",d)};r.on(e,c),r.once("killed",d);for(const e of t)e.time<a-n||c(e.chunk);return()=>{this.activeClients--,this.inactivityCheck(r),d()}}});return setTimeout((()=>i.close()),3e4),`tcp://127.0.0.1:${o}`})(c),b={url:y,container:c,inputArguments:["-analyzeduration","0","-probesize",v,...this.parsers[c].inputArguments||[],"-f",this.parsers[c].container,"-i",y],mediaStreamOptions:l};return m.createFFmpegMediaObject(b)}}class PrebufferMixin extends n.SettingsMixinDeviceBase{constructor(e,t,i,r){super(e,i,{providerNativeId:r,mixinDeviceInterfaces:t,group:"Prebuffer Settings",groupKey:"prebuffer"}),p(this,"released",!1),p(this,"sessions",new Map),this.delayStart()}delayStart(){this.console.log("prebuffer sessions starting in 5 seconds"),setTimeout((()=>this.ensurePrebufferSessions()),5e3)}async getVideoStream(e){await this.ensurePrebufferSessions();const t=null==e?void 0:e.id;let i=this.sessions.get(t);return!i||null!=e&&e.directMediaStream?this.mixinDevice.getVideoStream(e):(i.ensurePrebufferSession(),await i.parserSessionPromise,i=this.sessions.get(t),i?i.getVideoStream(e):this.mixinDevice.getVideoStream(e))}async ensurePrebufferSessions(){const e=await this.mixinDevice.getVideoStreamOptions(),i=this.getEnabledMediaStreamOptions(e),r=i?i.map((e=>e.id)):[void 0],n=(null==e?void 0:e.map((e=>e.id)))||[void 0];if("true"!==this.storage.getItem("warnedCloud")){(null==e?void 0:e.find((e=>"cloud"===e.source)))&&(this.storage.setItem("warnedCloud","true"),h.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 s=this.mixinDeviceInterfaces.includes(t.ScryptedInterface.Battery);let a=0;const c=n.length;for(const t of n){let i=this.sessions.get(t);if(!i){var d;const n=null==e?void 0:e.find((e=>e.id===t));null!=n&&n.prebuffer&&h.a(`Prebuffer is already available on ${this.name}. If this is a grouped device, disable the Rebroadcast extension.`);const u=null==n?void 0:n.name,l=!r.includes(t);if(i=new PrebufferSession(this,u,t,s||l),this.sessions.set(t,i),t===(null==e||null===(d=e[0])||void 0===d?void 0:d.id)&&this.sessions.set(void 0,i),s){this.console.log("camera is battery powered, prebuffering and rebroadcasting will only work on demand.");continue}if(l){this.console.log("stream",u,"will be rebroadcast on demand.");continue}(async()=>{for(;this.sessions.get(t)===i&&!this.released;){i.ensurePrebufferSession();try{const e=await i.parserSessionPromise;a++,this.online=a==c,await(0,o.once)(e,"killed"),this.console.error("prebuffer session ended")}catch(e){this.console.error("prebuffer session ended with error",e)}finally{a--,this.online=a==c}this.console.log("restarting prebuffer session in 5 seconds"),await new Promise((e=>setTimeout(e,5e3)))}this.console.log("exiting prebuffer session (released or restarted with new configuration)")})()}}g.onMixinEvent(this.id,this.mixinProviderNativeId,t.ScryptedInterface.Settings,void 0)}async getMixinSettings(){const e=[];try{const t=await this.mixinDevice.getVideoStreamOptions(),i=this.getEnabledMediaStreamOptions(t);(null==t?void 0:t.length)>0&&e.push({title:"Prebuffered Streams",description:"The streams to prebuffer. Enable only as necessary to reduce traffic.",key:"enabledStreams",value:i.map((e=>e.name||"")),choices:t.map((e=>e.name)),multiple:!0})}catch(e){throw this.console.error("error in getVideoStreamOptions",e),e}e.push({title:"Prebuffer Duration",description:"Duration of the prebuffer in milliseconds.",type:"number",key:y,value:this.storage.getItem(y)||v.toString()},{title:"Start at Previous Keyframe",description:"Start live streams from the previous key frame. Improves startup time.",type:"boolean",key:S,value:("false"!==this.storage.getItem(S)).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()){var r;null==e||null===(r=e.parserSessionPromise)||void 0===r||r.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(y))||v;if(t)for(const e of t)e.prebuffer=i;else e.push({id:"default",name:"Default",prebuffer:i});return e}release(){this.console.log("prebuffer releasing if started"),this.released=!0;for(const t of this.sessions.values()){var e;t&&(t.clearPrebuffers(),null===(e=t.parserSessionPromise)||void 0===e||e.then((e=>{this.console.log("prebuffer released"),e.kill(),t.clearPrebuffers()})))}}}class PrebufferProvider extends c.AutoenableMixinProvider{constructor(e){super(e);for(const e of Object.keys(f.getSystemState())){var t;const i=f.getDeviceById(e);null!==(t=i.mixins)&&void 0!==t&&t.includes(this.id)&&i.getVideoStreamOptions()}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((()=>g.requestRestart()),i)}async canMixin(e,i){return i.includes(t.ScryptedInterface.VideoCamera)?[t.ScryptedInterface.VideoCamera,t.ScryptedInterface.Settings,t.ScryptedInterface.Online]:null}async getMixin(e,t,i){return this.setHasEnabledMixin(i.id),new PrebufferMixin(e,t,i,this.nativeId)}async releaseMixin(e,t){t.online=!0,t.release()}}var A=new PrebufferProvider;e.default=A})();var o=exports="undefined"==typeof exports?{}:exports;for(var n in r)o[n]=r[n];r.__esModule&&Object.defineProperty(o,"__esModule",{value:!0})})();
2
2
  //# sourceMappingURL=main.nodejs.js.map
package/dist/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrypted/prebuffer-mixin",
3
- "version": "0.1.146",
3
+ "version": "0.1.150",
4
4
  "description": "Rebroadcast and Prebuffer for VideoCameras.",
5
5
  "author": "Scrypted",
6
6
  "license": "Apache-2.0",
package/src/main.ts CHANGED
@@ -4,10 +4,11 @@ import sdk from '@scrypted/sdk';
4
4
  import { once } from 'events';
5
5
  import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
6
6
  import { createRebroadcaster, ParserOptions, ParserSession, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
7
- import { createMpegTsParser, createFragmentedMp4Parser, StreamChunk, createPCMParser, StreamParser, createRtpParser } from '@scrypted/common/src/stream-parser';
7
+ import { createMpegTsParser, createFragmentedMp4Parser, StreamChunk, StreamParser, createRtpParser } from '@scrypted/common/src/stream-parser';
8
8
  import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
9
9
  import dgram from 'dgram';
10
10
  import { listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
11
+ import { RtspServer } from './rtsp-server';
11
12
 
12
13
  const { mediaManager, log, systemManager, deviceManager } = sdk;
13
14
 
@@ -24,8 +25,6 @@ const COMPATIBLE_AUDIO = 'Compatible Audio'
24
25
  const COMPATIBLE_AUDIO_DESCRIPTION = `${COMPATIBLE_AUDIO} (Copy)`;
25
26
  const TRANSCODE_AUDIO = 'Other Audio';
26
27
  const TRANSCODE_AUDIO_DESCRIPTION = `${TRANSCODE_AUDIO} (Transcode)`;
27
- const PCM_AUDIO = 'PCM or G.711 Audio';
28
- const PCM_AUDIO_DESCRIPTION = `${PCM_AUDIO} (Copy, Unstable)`;
29
28
  const COMPATIBLE_AUDIO_CODECS = ['aac', 'mp3', 'mp2', 'opus'];
30
29
  const DEFAULT_FFMPEG_INPUT_ARGUMENTS = '-fflags +genpts';
31
30
 
@@ -33,7 +32,6 @@ const VALID_AUDIO_CONFIGS = [
33
32
  AAC_AUDIO,
34
33
  COMPATIBLE_AUDIO,
35
34
  TRANSCODE_AUDIO,
36
- // PCM_AUDIO,
37
35
  ];
38
36
 
39
37
  interface PrebufferStreamChunk {
@@ -44,13 +42,12 @@ interface PrebufferStreamChunk {
44
42
  interface Prebuffers {
45
43
  mp4: PrebufferStreamChunk[];
46
44
  mpegts: PrebufferStreamChunk[];
47
- s16le: PrebufferStreamChunk[];
48
45
  rtpvideo: PrebufferStreamChunk[];
49
46
  rtpaudio: PrebufferStreamChunk[];
50
47
  }
51
48
 
52
- type PrebufferParsers = "mpegts" | "mp4" | "s16le" | "rtpvideo" | "rtpaudio";
53
- const PrebufferParserValues: PrebufferParsers[] = ['mpegts', 'mp4', 's16le', 'rtpvideo', 'rtpaudio'];
49
+ type PrebufferParsers = 'mpegts' | 'mp4' | 'rtpvideo' | 'rtpaudio';
50
+ const PrebufferParserValues: PrebufferParsers[] = ['mpegts', 'mp4', 'rtpvideo', 'rtpaudio'];
54
51
 
55
52
  class PrebufferSession {
56
53
 
@@ -59,7 +56,6 @@ class PrebufferSession {
59
56
  prebuffers: Prebuffers = {
60
57
  mp4: [],
61
58
  mpegts: [],
62
- s16le: [],
63
59
  rtpvideo: [],
64
60
  rtpaudio: [],
65
61
  };
@@ -93,7 +89,6 @@ class PrebufferSession {
93
89
  clearPrebuffers() {
94
90
  this.prebuffers.mp4 = [];
95
91
  this.prebuffers.mpegts = [];
96
- this.prebuffers.s16le = [];
97
92
  this.prebuffers.rtpaudio = [];
98
93
  this.prebuffers.rtpvideo = [];
99
94
  }
@@ -111,7 +106,6 @@ class PrebufferSession {
111
106
  aacAudio: boolean,
112
107
  compatibleAudio: boolean,
113
108
  reencodeAudio: boolean,
114
- pcmAudio: boolean,
115
109
  } {
116
110
  let audioConfig = this.storage.getItem(this.audioConfigurationKey) || '';
117
111
  if (!VALID_AUDIO_CONFIGS.find(config => audioConfig.startsWith(config)))
@@ -120,12 +114,9 @@ class PrebufferSession {
120
114
  const compatibleAudio = audioConfig.indexOf(COMPATIBLE_AUDIO) !== -1;
121
115
  // reencode audio will be used if explicitly set.
122
116
  const reencodeAudio = audioConfig.indexOf(TRANSCODE_AUDIO) !== -1;
123
- // pcm audio only used when explicitly set.
124
- const pcmAudio = audioConfig.indexOf(PCM_AUDIO) !== -1;
125
117
  return {
126
- isUsingDefaultAudioConfig: !(aacAudio || compatibleAudio || reencodeAudio || pcmAudio),
118
+ isUsingDefaultAudioConfig: !(aacAudio || compatibleAudio || reencodeAudio),
127
119
  aacAudio,
128
- pcmAudio,
129
120
  compatibleAudio,
130
121
  reencodeAudio,
131
122
  }
@@ -162,7 +153,6 @@ class PrebufferSession {
162
153
  AAC_AUDIO_DESCRIPTION,
163
154
  COMPATIBLE_AUDIO_DESCRIPTION,
164
155
  TRANSCODE_AUDIO_DESCRIPTION,
165
- PCM_AUDIO_DESCRIPTION,
166
156
  ],
167
157
  },
168
158
  {
@@ -182,11 +172,11 @@ class PrebufferSession {
182
172
  {
183
173
  title: 'Rebroadcast Mode',
184
174
  group,
185
- description: 'THIS FEATURE IS IN TESTING. DO NOT CHANGE THIS FROM MPEG-TS. The stream format to use when rebroadcasting. RTP will increase startup time but may resolve PCM audio issues.',
175
+ description: 'THIS FEATURE IS IN TESTING. DO NOT CHANGE THIS FROM MPEG-TS. The stream format to use when rebroadcasting.',
186
176
  placeholder: 'MPEG-TS',
187
177
  choices: [
188
178
  'MPEG-TS',
189
- 'RTP',
179
+ 'RTSP',
190
180
  ],
191
181
  key: this.rebroadcastModeKey,
192
182
  value: this.storage.getItem(this.rebroadcastModeKey) || 'MPEG-TS',
@@ -240,7 +230,6 @@ class PrebufferSession {
240
230
  async startPrebufferSession() {
241
231
  this.prebuffers.mp4 = [];
242
232
  this.prebuffers.mpegts = [];
243
- this.prebuffers.s16le = [];
244
233
  this.prebuffers.rtpvideo = [];
245
234
  this.prebuffers.rtpaudio = [];
246
235
  const prebufferDurationMs = parseInt(this.storage.getItem(PREBUFFER_DURATION_MS)) || defaultPrebufferDuration;
@@ -257,10 +246,10 @@ class PrebufferSession {
257
246
  // to determine the codec without causing a parse failure.
258
247
  // camera may explicity request that its audio stream be muted via a null.
259
248
  // respect that setting.
260
- const audioSoftMuted = mso?.audio?.codec === null;
249
+ const audioSoftMuted = mso?.audio === null;
261
250
  const advertisedAudioCodec = mso?.audio?.codec;
262
251
 
263
- const { isUsingDefaultAudioConfig, aacAudio, compatibleAudio, reencodeAudio, pcmAudio } = this.getAudioConfig();
252
+ const { isUsingDefaultAudioConfig, aacAudio, compatibleAudio, reencodeAudio } = this.getAudioConfig();
264
253
 
265
254
  let probingAudioCodec = false;
266
255
  if (!audioSoftMuted && !advertisedAudioCodec && isUsingDefaultAudioConfig && this.detectedAudioCodec === undefined) {
@@ -287,7 +276,7 @@ class PrebufferSession {
287
276
  if (audioIncompatible) {
288
277
  // show an alert that rebroadcast needs an explicit setting by the user.
289
278
  if (isUsingDefaultAudioConfig) {
290
- log.a(`${this.mixin.name} is using the ${assumedAudioCodec} audio codec and has had its audio disabled. Select 'Disable Audio' or 'Transcode Audio' in the camera stream's Rebroadcast settings to suppress this alert.`);
279
+ log.a(`${this.mixin.name} is using the ${assumedAudioCodec} 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.`);
291
280
  }
292
281
  this.console.warn('Configure your camera to output AAC, MP3, MP2, or Opus audio. Suboptimal audio codec in use:', assumedAudioCodec);
293
282
  }
@@ -322,26 +311,28 @@ class PrebufferSession {
322
311
  // if the camera reports audio is incompatible and the user can't do anything about it
323
312
  // enable transcoding by default. however, still allow the user to change the settings
324
313
  // in case something changed.
325
- const mustTranscode = isUsingDefaultAudioConfig && audioIncompatible;
326
- if (mustTranscode)
327
- this.console.log('camera reports it is not user configurable. transcoding due to incompatible codec', assumedAudioCodec);
314
+ let mustTranscode = false;
315
+ if (!probingAudioCodec && isUsingDefaultAudioConfig && audioIncompatible) {
316
+ if (mso?.userConfigurable === false)
317
+ this.console.log('camera reports it is not user configurable. transcoding due to incompatible codec', assumedAudioCodec);
318
+ else
319
+ this.console.log('camera audio transcoding due to incompatible codec. configure the camera to use a compatible codec if possible.');
320
+ mustTranscode = true;
321
+ }
328
322
 
329
323
  if (audioSoftMuted || probingAudioCodec || detectedNoAudio) {
330
324
  // no audio? explicitly disable it.
331
325
  acodec = ['-an'];
332
326
  this.audioDisabled = true;
333
327
  }
334
- else if (pcmAudio || mustTranscode) {
335
- acodec = ['-an'];
336
- }
337
- else if (reencodeAudio || (advertisedAudioCodec && !COMPATIBLE_AUDIO_CODECS.includes(advertisedAudioCodec))) {
328
+ else if (reencodeAudio || mustTranscode) {
338
329
  acodec = [
339
330
  '-bsf:a', 'aac_adtstoasc',
331
+ '-acodec', 'libfdk_aac',
340
332
  '-ar', `8k`,
341
333
  '-b:a', `100k`,
342
334
  '-bufsize', '400k',
343
335
  '-ac', `1`,
344
- '-acodec', 'libfdk_aac',
345
336
  // can we change this to aac_eld somehow? mpegts does not support aac eld (AOT-39).
346
337
  '-profile:a', 'aac_low',
347
338
  '-flags', '+global_header',
@@ -390,8 +381,8 @@ class PrebufferSession {
390
381
  },
391
382
  };
392
383
 
393
- const rtpMode = this.storage.getItem(this.rebroadcastModeKey) === 'RTP';
394
- if (!rtpMode) {
384
+ const rtspMode = this.storage.getItem(this.rebroadcastModeKey) === 'RTSP';
385
+ if (!rtspMode) {
395
386
  rbo.parsers.mpegts = createMpegTsParser({
396
387
  vcodec,
397
388
  acodec,
@@ -402,13 +393,6 @@ class PrebufferSession {
402
393
  rbo.parsers.rtpaudio = createRtpParser('-vn', '-acodec', 'copy');
403
394
  }
404
395
 
405
- // if pcm prebuffer is requested, create the the parser. don't do it if
406
- // the camera wants to mute the audio though, or no audio was detected
407
- // in a prior attempt.
408
- if (pcmAudio && !audioSoftMuted && !detectedNoAudio) {
409
- rbo.parsers.s16le = createPCMParser();
410
- }
411
-
412
396
  this.parsers = rbo.parsers;
413
397
 
414
398
  // create missing pts from dts so mpegts and mp4 muxing does not fail
@@ -475,11 +459,7 @@ class PrebufferSession {
475
459
  this.parserSession = undefined;
476
460
  });
477
461
 
478
- // s16le will be a no-op if there's no pcm, no harm.
479
462
  for (const container of PrebufferParserValues) {
480
- if (this.parsers[container]?.parseDatagram)
481
- continue;
482
-
483
463
  let shifts = 0;
484
464
 
485
465
  session.on(container, (chunk: StreamChunk) => {
@@ -549,24 +529,37 @@ class PrebufferSession {
549
529
 
550
530
  if (this.parsers[container].parseDatagram) {
551
531
  let sdp = Buffer.concat(await session.sdp).toString();
552
- const audioPort = Math.round(Math.random() * 40000 + 10000);
553
- const videoPort = Math.round(Math.random() * 40000 + 10000);
554
- sdp = sdp.replace('m=audio 0', 'm=audio ' + audioPort);
555
- sdp = sdp.replace('m=video 0', 'm=video ' + videoPort);
556
-
557
- const d = dgram.createSocket('udp4');
558
- d.bind();
559
532
 
560
- const safeWriteData = (chunk: StreamChunk, port: number) => {
561
- for (const c of chunk.chunks) {
562
- d.send(c, port);
533
+ const sdpLines = sdp.split('\n').map(sdp => sdp.trim()).filter(sdp => sdp !== 'c=IN IP4 127.0.0.1');
534
+ let id = 0;
535
+ const addTrack = (type: string) => {
536
+ const index = sdpLines.findIndex(sdp => sdp.startsWith(`m=${type}`));
537
+ if (index !== -1) {
538
+ sdpLines.splice(index + 1, 0, 'a=recvonly', `a=control:trackID=${id}`);
539
+ id++;
540
+ }
541
+ }
542
+ addTrack('video');
543
+ addTrack('audio');
544
+ sdp = sdpLines.join('\r\n');
545
+ sdp = sdp.replace(/m=audio .*? /, 'm=audio 0 ')
546
+ sdp = sdp.replace(/m=video .*? /, 'm=video 0 ')
547
+ sdp = sdp.replace('t=0 0', 'c=IN IP4 127.0.0.1\r\nt=0 0\r\na=recvonly\r\na=control:*\r\na=range:npt=now-');
548
+ this.console.log(sdp);
549
+
550
+ let playing = false;
551
+ let server: RtspServer;
552
+ const safeWriteData = (send: (packet: Buffer, rtcp: boolean) => void) => {
553
+ return (chunk: StreamChunk) => {
554
+ if (!playing)
555
+ return;
556
+ send(chunk.chunks[0], chunk.type === 'rtcp');
563
557
  }
564
558
  }
565
559
 
566
- const wv = (chunk: StreamChunk) => safeWriteData(chunk, videoPort);
567
- const wa = (chunk: StreamChunk) => safeWriteData(chunk, audioPort);
560
+ const wv = safeWriteData((packet, rtcp) => server.sendVideo(packet, rtcp));
561
+ const wa = safeWriteData((packet, rtcp) => server.sendAudio(packet, rtcp));
568
562
  const cleanup = () => {
569
- d.close();
570
563
  session.removeListener('rtpvideo', wv);
571
564
  session.removeListener('rtpaudio', wa);
572
565
  session.removeListener('killed', cleanup);
@@ -584,26 +577,58 @@ class PrebufferSession {
584
577
  this.inactivityCheck(session);
585
578
  cleanup();
586
579
  });
587
- c.write(sdp);
588
- c.end();
589
-
590
- // await new Promise(resolve => setTimeout(resolve, 500));
591
- // for (const prebuffer of this.prebuffers.rtpvideo) {
592
- // if (prebuffer.time < now - requestedPrebuffer)
593
- // continue;
594
- // safeWriteData(prebuffer.chunk, videoPort);
595
- // }
596
- // for (const prebuffer of this.prebuffers.rtpaudio) {
597
- // if (prebuffer.time < now - requestedPrebuffer)
598
- // continue;
599
- // safeWriteData(prebuffer.chunk, audioPort);
600
- // }
580
+
581
+ server = await new Promise(resolve => new RtspServer(c, sdp, server => resolve(server)));
582
+ playing = true;
583
+
584
+ let pvi = 0;
585
+ let pai = 0;
586
+ const now = Date.now();
587
+ while (pvi < this.prebuffers.rtpvideo.length && this.prebuffers.rtpvideo[pvi].time < now - requestedPrebuffer) {
588
+ pvi++;
589
+ }
590
+ while (pai < this.prebuffers.rtpaudio.length && this.prebuffers.rtpaudio[pai].time < now - requestedPrebuffer) {
591
+ pai++;
592
+ }
593
+
594
+ const wpv = (chunk: StreamChunk) => {
595
+ wv(chunk);
596
+ pvi++;
597
+ }
598
+ const wpa = (chunk: StreamChunk) => {
599
+ wa(chunk);
600
+ pai++;
601
+ }
602
+
603
+ while (pvi < this.prebuffers.rtpvideo.length || pai < this.prebuffers.rtpaudio.length) {
604
+ const pv = this.prebuffers.rtpvideo[pvi];
605
+ const pa = this.prebuffers.rtpaudio[pai];
606
+
607
+ if (!pa) {
608
+ wpv(pv.chunk)
609
+ continue;
610
+ }
611
+
612
+ if (!pv) {
613
+ wpa(pa.chunk);
614
+ continue;
615
+ }
616
+
617
+ if (pa.time < pv.time)
618
+ wpa(pa.chunk);
619
+ else
620
+ wpv(pv.chunk);
621
+ }
601
622
  })
602
- .catch(cleanup);
623
+ .catch(e => {
624
+ this.console.error('failure client', e)
625
+ cleanup()
626
+ });
603
627
 
604
628
  session.on('rtpvideo', wv)
605
629
  session.on('rtpaudio', wa);
606
- return sdpClient.url;
630
+ const url = sdpClient.url.replace('tcp://', 'rtsp://');
631
+ return url;
607
632
  }
608
633
 
609
634
  const { server, port } = await createRebroadcaster({
@@ -663,8 +688,8 @@ class PrebufferSession {
663
688
  return `tcp://127.0.0.1:${port}`;
664
689
  }
665
690
 
666
- const rtpMode = this.storage.getItem(this.rebroadcastModeKey) === 'RTP';
667
- const defaultContainer = rtpMode ? 'rtpvideo' : 'mpegts';
691
+ const rtspMode = this.storage.getItem(this.rebroadcastModeKey) === 'RTSP';
692
+ const defaultContainer = rtspMode ? 'rtpvideo' : 'mpegts';
668
693
 
669
694
  const container: PrebufferParsers = this.parsers[options?.container] ? options?.container as PrebufferParsers : defaultContainer;
670
695
 
@@ -672,7 +697,7 @@ class PrebufferSession {
672
697
 
673
698
  mediaStreamOptions.prebuffer = requestedPrebuffer;
674
699
 
675
- const { pcmAudio, reencodeAudio } = this.getAudioConfig();
700
+ const { reencodeAudio } = this.getAudioConfig();
676
701
 
677
702
  if (this.audioDisabled) {
678
703
  mediaStreamOptions.audio = null;
@@ -716,20 +741,13 @@ class PrebufferSession {
716
741
  container,
717
742
  inputArguments: [
718
743
  '-analyzeduration', '0', '-probesize', length,
744
+ ...(this.parsers[container].inputArguments || []),
719
745
  '-f', this.parsers[container].container,
720
746
  '-i', url,
721
747
  ],
722
748
  mediaStreamOptions,
723
749
  }
724
750
 
725
- if (pcmAudio) {
726
- ffmpegInput.inputArguments.push(
727
- '-analyzeduration', '0', '-probesize', length,
728
- '-f', 's16le',
729
- '-i', await createContainerServer('s16le'),
730
- )
731
- }
732
-
733
751
  const mo = mediaManager.createFFmpegMediaObject(ffmpegInput);
734
752
  return mo;
735
753
  }
@@ -0,0 +1,127 @@
1
+ import { readLine } from '../../../common/src/read-length';
2
+ import net from 'net';
3
+ import { Duplex, Readable } from 'stream';
4
+ import { randomBytes } from 'crypto';
5
+
6
+
7
+ interface Headers{
8
+ [header: string]: string
9
+ }
10
+
11
+ function parseHeaders(headers: string[]): Headers {
12
+ const ret = {};
13
+ for (const header of headers.slice(1)) {
14
+ const index = header.indexOf(':');
15
+ let value = '';
16
+ if (index !== -1)
17
+ value = header.substring(index + 1).trim();
18
+ const key = header.substring(0, index).toLowerCase();
19
+ ret[key] = value;
20
+ }
21
+ return ret;
22
+ }
23
+
24
+ export class RtspServer {
25
+ session: string;
26
+ constructor(public socket: Duplex, public sdp: string, public playing: (server: RtspServer) => void) {
27
+ this.session = randomBytes(4).toString('hex');
28
+ this.loop();
29
+ }
30
+
31
+ async loop() {
32
+ try {
33
+ let currentHeaders: string[] = [];
34
+ while (true) {
35
+ let line = await readLine(this.socket);
36
+ line = line.trim();
37
+ if (!line) {
38
+ if (!await this.headers(currentHeaders))
39
+ break;
40
+ currentHeaders = [];
41
+ continue;
42
+ }
43
+ currentHeaders.push(line);
44
+ }
45
+ }
46
+ catch (e) {
47
+ }
48
+ }
49
+
50
+ send(rtp: Buffer, channel: number) {
51
+ const header = Buffer.alloc(4);
52
+ header.writeUInt8(36, 0);
53
+ header.writeUInt8(channel, 1);
54
+ header.writeUInt16BE(rtp.length, 2);
55
+
56
+ this.socket.write(header);
57
+ this.socket.write(Buffer.from(rtp));
58
+ }
59
+
60
+ sendVideo(packet: Buffer, rtcp: boolean) {
61
+ this.send(packet, rtcp ? 1 : 0);
62
+ }
63
+
64
+ sendAudio(packet: Buffer, rtcp: boolean) {
65
+ this.send(packet, rtcp ? 3 : 2);
66
+ }
67
+
68
+ options(url: string, requestHeaders: Headers) {
69
+ const headers: Headers = {};
70
+ headers['CSeq'] = requestHeaders['cseq'];
71
+ headers['Public'] = 'DESCRIBE, OPTIONS, PAUSE, PLAY, SETUP, TEARDOWN, ANNOUNCE, RECORD';
72
+
73
+ this.respond(200, 'OK', requestHeaders, headers);
74
+ }
75
+
76
+ describe(url: string, requestHeaders: Headers) {
77
+ const headers: Headers = {};
78
+ headers['Content-Base'] = url;
79
+ headers['Content-Type'] = 'application/sdp';
80
+ this.respond(200, 'OK', requestHeaders, headers, Buffer.from(this.sdp))
81
+ }
82
+
83
+ setup(url: string, requestHeaders: Headers) {
84
+ const headers: Headers = {};
85
+ headers['Transport'] = requestHeaders['transport'];
86
+ headers['Session'] = this.session;
87
+ this.respond(200, 'OK', requestHeaders, headers)
88
+ }
89
+
90
+ play(url: string, requestHeaders: Headers) {
91
+ const headers: Headers = {};
92
+ headers['RTP-Info'] = `url=${url}/trackID=0;seq=0;rtptime=0,url=${url}/trackID=1;seq=0;rtptime=0`;
93
+ headers['Range'] = 'npt=now-';
94
+ headers['Session'] = this.session;
95
+ this.respond(200, 'OK', requestHeaders, headers);
96
+
97
+ this.playing(this);
98
+ }
99
+
100
+ async headers(headers: string[]) {
101
+ let [method, url] = headers[0].split(' ', 2);
102
+ method = method.toLowerCase();
103
+ const requestHeaders = parseHeaders(headers);
104
+ if (!this[method]) {
105
+ this.respond(400, 'Bad Request', requestHeaders, {});
106
+ return;
107
+ }
108
+
109
+ await this[method](url, requestHeaders);
110
+ return method !== 'play';
111
+ }
112
+
113
+ respond(code: number, message: string, requestHeaders: Headers, headers: Headers, buffer?: Buffer) {
114
+ let response = `RTSP/1.0 ${code} ${message}\r\n`;
115
+ if (requestHeaders['cseq'])
116
+ headers['CSeq'] = requestHeaders['cseq'];
117
+ if (buffer)
118
+ headers['Content-Length'] = buffer.length.toString();
119
+ for (const [key, value] of Object.entries(headers)) {
120
+ response += `${key}: ${value}\r\n`;
121
+ }
122
+ response += '\r\n';
123
+ this.socket.write(response);
124
+ if (buffer)
125
+ this.socket.write(buffer);
126
+ }
127
+ }