@scrypted/prebuffer-mixin 0.1.150 → 0.1.154

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 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})})();
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.createParserRebroadcaster=async function(e,t,i){return await g(Object.assign({},i,{connect:(i,r)=>(e.on(t,i),()=>{r()})}))},t.createRebroadcaster=g,t.handleRebroadcasterClient=v,t.parseAudioCodec=f,t.parseResolution=p,t.parseVideoCodec=h,t.startParserSession=async function(e,t){const{console:i}=t;let r,a=!0;const u=new s.EventEmitter;let m,g,v,y,S;u.on("error",(e=>i.error("rebroadcast error",e)));const b=new Promise(((e,t)=>{y=e,S=t}));function P(){var e;a&&(u.emit("killed"),u.emit("error",new Error("killed"))),a=!1,null==D||D.kill(),null==D||D.kill("SIGKILL"),null===(e=S)||void 0===e||e(new Error("ffmpeg was killed before connecting to the rebroadcast session")),clearTimeout(r)}const M=e.inputArguments.slice();r=setTimeout(P,3e4);const w=e=>{let r;function o(){i.error("timeout waiting for data, killing parser session",e),P()}function n(){t.timeout&&(clearTimeout(r),r=setTimeout(o,t.timeout))}return u.once("killed",(()=>clearTimeout(r))),n(),{resetActivityTimer:n}},x=["pipe","pipe","pipe"];let O=3;for(const e of Object.keys(t.parsers)){const r=t.parsers[e];if(r.parseDatagram){const t=d.default.createSocket("udp4"),i=await(0,n.bindZero)(t),o=d.default.createSocket("udp4");await(0,n.bind)(o,i.port+1),u.once("killed",(()=>{t.close(),o.close()})),M.push(...r.outputArguments,i.url.replace("udp://","rtp://"));const{resetActivityTimer:s}=w(e);(async()=>{for await(const a of r.parseDatagram(t,parseInt(null===(i=v)||void 0===i?void 0:i[2]),parseInt(null===(o=v)||void 0===o?void 0:o[3]))){var i,o,n;null===(n=y)||void 0===n||n(void 0),u.emit(e,a),s()}})(),(async()=>{for await(const a of r.parseDatagram(o,parseInt(null===(t=v)||void 0===t?void 0:t[2]),parseInt(null===(i=v)||void 0===i?void 0:i[3]),"rtcp")){var t,i,n;null===(n=y)||void 0===n||n(void 0),u.emit(e,a),s()}})()}else if(r.tcpProtocol){const t=await(0,n.listenZeroSingleClient)(),o=new URL(r.tcpProtocol);o.port=t.port.toString(),M.push(...r.outputArguments,o.toString());const{resetActivityTimer:s}=w(e);(async()=>{const o=await t.clientPromise;try{for await(const t of r.parse(o,parseInt(null===(n=v)||void 0===n?void 0:n[2]),parseInt(null===(a=v)||void 0===a?void 0:a[3]))){var n,a,c;null===(c=y)||void 0===c||c(void 0),u.emit(e,t),s()}}catch(e){i.error("rebroadcast parse error",e),P()}})()}else M.push(...r.outputArguments,"pipe:"+O++),x.push("pipe")}M.push("-sdp_file","pipe:"+O++),x.push("pipe"),M.unshift("-hide_banner"),(0,c.safePrintFFmpegArguments)(i,M);const D=o.default.spawn(await l.getFFmpegPath(),M,{stdio:x});(0,c.ffmpegLogInitialOutput)(i,D,void 0,null==t?void 0:t.storage),D.on("exit",P);const I=new Promise((e=>{const t=[];D.stdio[O-1].on("data",(i=>{t.push(i),e(t)}))}));let C=0;return Object.keys(t.parsers).forEach((async e=>{const r=t.parsers[e];if(!r.parse||r.tcpProtocol)return;const o=D.stdio[3+C];C++;try{const{resetActivityTimer:t}=w(e);for await(const i of r.parse(o,parseInt(null===(n=v)||void 0===n?void 0:n[2]),parseInt(null===(s=v)||void 0===s?void 0:s[3]))){var n,s,a;null===(a=y)||void 0===a||a(void 0),u.emit(e,i),t()}}catch(e){i.error("rebroadcast parse error",e),P()}})),f(D).then((e=>m=e)),h(D).then((e=>g=e)),p(D).then((e=>v=e)),await b,y=void 0,S=void 0,clearTimeout(r),{sdp:I,inputAudioCodec:m,inputVideoCodec:g,inputVideoResolution:v,isActive:()=>a,kill:P,mediaStreamOptions:e.mediaStreamOptions||{id:void 0,name:void 0},on(e,t){return u.on(e,t),this},once(e,t){return u.once(e,t),this},removeListener(e,t){return u.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")}async function g(e){let t,i=0;const o=()=>{null!=e&&e.idle&&(clearTimeout(t),t=setTimeout((()=>{0!==i?o():e.idle.callback()}),e.idle.timeout))};o();const s=(0,r.createServer)((t=>{v(t,e),t.once("close",(()=>{o(),i--})),o(),i++})),a=await(0,n.listenZero)(s);return{server:s,port:a,url:`tcp://127.0.0.1:${a}`,get clients(){return i}}}async function v(e,t){const i=await e;let r=!0;const o=()=>{i.removeAllListeners(),i.destroy();const e=n;n=void 0,null==e||e()};let n=null==t?void 0:t.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",(()=>{o()})),i.on("error",(e=>{var i;return null==t||null===(i=t.console)||void 0===i?void 0:i.log("client stream ended")}))}},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]}}))}))},961:(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(961);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,t.createRtspParser=function(){let e;return{container:"rtsp",tcpProtocol:"rtsp://127.0.0.1/"+(0,o.randomBytes)(8).toString("hex"),inputArguments:["-rtsp_transport","tcp"],outputArguments:["-rtsp_transport","tcp","-vcodec","copy","-acodec","copy","-f","rtsp"],findSyncFrame:n,sdp:new Promise((t=>e=t)),async*parse(t,i,r){const o=new RtspServer(t);await o.handleSetup(),e(o.sdp);for await(const{type:e,rtcp:t,header:n,packet:s}of o.handleRecord())yield{chunks:[n,s],type:e,width:i,height:r}}}};var r=i(961),o=i(113);function n(e){return e}class RtspServer{constructor(e,t){this.duplex=e,this.sdp=t,this.session=(0,o.randomBytes)(4).toString("hex")}async handleSetup(){let e=[];for(;;){let t=await(0,r.readLine)(this.duplex);if(t=t.trim(),t)e.push(t);else{if(!await this.headers(e))break;e=[]}}}async handlePlayback(){return this.handleSetup()}async*handleRecord(){for(;;){const e=await(0,r.readLength)(this.duplex,4),t=e.readUInt16BE(2),i=await(0,r.readLength)(this.duplex,t),o=e.readUInt8(1);yield{type:o<2?"video":"audio",rtcp:o%2==1,header:e,packet:i}}}send(e,t){const i=Buffer.alloc(4);i.writeUInt8(36,0),i.writeUInt8(t,1),i.writeUInt16BE(e.length,2),this.duplex.write(i),this.duplex.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={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)}async announce(e,t){const i=parseInt(t["content-length"]),o=await(0,r.readLength)(this.duplex,i);this.sdp=o.toString();const n={};n.Session=this.session,this.respond(200,"OK",t,n)}async record(e,t){const i={};i.Session=this.session,this.respond(200,"OK",t,i)}async 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&&"record"!==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.duplex.write(n),o&&this.duplex.write(o)}}t.RtspServer=RtspServer},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.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",e.RTCAVSignalingOfferSetup="x-scrypted/x-rtc-av-signalling-offer-setup",e.SchemePrefix="x-scrypted/x-scrypted-scheme-",e.MediaObject="x-scrypted/x-scrypted-media-object"}(l||(t.ScryptedMimeTypes=l={}))},568:(e,t,i)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ffmpegLogInitialOutput=function(e,t,i,r){var s,a;const c=!!o.default.env.SCRYPTED_FFMPEG_NOISY||!(null==r||!r.getItem("SCRYPTED_FFMPEG_NOISY"));function d(e){const r=o=>{const s=o.toString();for(const e of n)if(-1!==s.indexOf(e))return;if(!(c||i||-1===s.indexOf("frame=")&&-1===s.indexOf("size=")))return e(s),e("video/audio detected, discarding further input"),t.stdout.removeListener("data",r),void t.stderr.removeListener("data",r);e(s)};return r}null===(s=t.stdout)||void 0===s||s.on("data",d(e.log)),null===(a=t.stderr)||void 0===a||a.on("data",d(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(" "))};var r,o=(r=i(282))&&r.__esModule?r:{default:r};const n=["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")},282:e=>{"use strict";e.exports=require("process")}},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",x="Other Audio",O=["aac","mp3","mp2","opus"],D="-fflags +genpts",I=[P,w,x],C=["mpegts","mp4","rtsp"];class PrebufferSession{constructor(e,t,i,r){p(this,"prebuffers",{mp4:[],mpegts:[],rtsp:[]}),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,this.lastDetectedAudioCodecKey="lastDetectedAudioCodec-"+this.streamId}clearPrebuffers(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtsp=[]}ensurePrebufferSession(){this.parserSessionPromise||this.mixin.released||(this.console.log(this.streamName,"prebuffer session started"),this.parserSessionPromise=this.startPrebufferSession(),this.parserSessionPromise.catch((()=>this.parserSessionPromise=void 0)))}getAudioConfig(){let e=this.storage.getItem(this.audioConfigurationKey)||"";I.find((t=>e.startsWith(t)))||(e="");const t=-1!==e.indexOf(P),i=-1!==e.indexOf(w),r=-1!==e.indexOf(x);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:D,choices:[D,"-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.rtsp=[];const l=parseInt(this.storage.getItem(y))||v;let p;try{p=(await this.mixinDevice.getVideoStreamOptions()).find((e=>e.id===this.streamId))}catch(e){}const f=null===(null===(e=p)||void 0===e?void 0:e.audio),g=null===(i=p)||void 0===i||null===(r=i.audio)||void 0===r?void 0:r.codec,{isUsingDefaultAudioConfig:S,aacAudio:b,compatibleAudio:P,reencodeAudio:M}=this.getAudioConfig();let w=this.storage.getItem(this.lastDetectedAudioCodecKey)||void 0;"null"===w&&(w=null);let x=!1;f||g||!S||void 0!==w||(this.console.warn("Camera did not report an audio codec, muting the audio stream and probing the codec."),x=!0),!f&&g&&void 0!==w&&w!==g&&this.console.warn("Audio codec plugin reported vs detected mismatch",g,w);const I=void 0===w?null==g?void 0:g.toLowerCase():null===(o=w)||void 0===o?void 0:o.toLowerCase(),A=!O.includes(I);x||!1===(null===(n=p)||void 0===n?void 0:n.userConfigurable)||f||A&&(S&&h.a(`${this.mixin.name} is using the ${I} 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:",I));const k=await this.mixinDevice.getVideoStream(p),T=await m.convertMediaObjectToBuffer(k,t.ScryptedMimeTypes.FFmpegInput),_=JSON.parse(T.toString()),E=["-bsf:a","aac_adtstoasc"],R=[];let L;this.audioDisabled=!1;const B=null===w;let j=!1;var F;!x&&S&&A&&(!1===(null===(F=p)||void 0===F?void 0:F.userConfigurable)?this.console.log("camera reports it is not user configurable. transcoding due to incompatible codec",I):this.console.log("camera audio transcoding due to incompatible codec. configure the camera to use a compatible codec if possible."),j=!0);if(f||x)L=["-an"],this.audioDisabled=!0;else if(M||j)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(b||B)L=["-acodec","copy"],L.push(...E);else if(P)L=["-acodec","copy"],L.push(...R);else{L=["-acodec","copy"];const e="aac"===I?E:R;L.push(...e)}const H=["-vcodec","copy"],V={console:this.console,timeout:6e4,parsers:{mp4:(0,a.createFragmentedMp4Parser)({vcodec:H,acodec:L})}};if("RTSP"===this.storage.getItem(this.rebroadcastModeKey)){const e=(0,u.createRtspParser)();this.sdp=e.sdp,V.parsers.rtsp=e}else V.parsers.mpegts=(0,a.createMpegTsParser)({vcodec:H,acodec:L});this.parsers=V.parsers;const N=this.storage.getItem(this.ffmpegInputArgumentsKey)||D;_.inputArguments.unshift(...N.split(" ")),this.storage.removeItem(this.lastDetectedAudioCodecKey);const U=await(0,s.startParserSession)(_,V);if(U.inputAudioCodec?O.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.storage.setItem(this.lastDetectedAudioCodecKey,U.inputAudioCodec||"null"),"h264"!==U.inputVideoCodec&&this.console.error("Video codec is not h264. If there are errors, try changing your camera's encoder output."),x)return this.console.warn("Audio probe complete, ending rebroadcast session and restarting with detected codecs."),U.kill(),this.startPrebufferSession();if(this.parserSession=U,null!==(d=_.mediaStreamOptions)&&void 0!==d&&d.refreshAt){let e,i=_.mediaStreamOptions;const r=async()=>{if(!U.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(_),U.once("killed",(()=>clearTimeout(e)))}U.once("killed",(()=>{this.parserSessionPromise=void 0,this.parserSession===U&&(this.parserSession=void 0)}));for(const e of C){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,t){this.printActiveClients(),this.stopInactive&&(this.activeClients||this.inactivityTimeout&&!t||(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)?"rtsp":"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 t=>{const i=this.prebuffers[t];let o,a;if("rtsp"===t){this.sdp.then((e=>console.log(e)));const e=await(0,d.listenZeroSingleClient)();o=e.clientPromise.then((async e=>{let t=await this.sdp;const i=new u.RtspServer(e,t);return await i.handlePlayback(),e})),a=e.url.replace("tcp://","rtsp://")}else{const e=await(0,d.listenZeroSingleClient)();o=e.clientPromise,a=`tcp://127.0.0.1:${e.port}`}return(0,s.handleRebroadcasterClient)(o,{console:this.console,connect:(o,s)=>{this.activeClients++,this.printActiveClients();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(t,c),r.removeListener("killed",d)};r.on(t,c),r.once("killed",d);for(const e of i)e.time<a-n||c(e.chunk);return()=>{this.activeClients--,this.inactivityCheck(r,!1!==(null==e?void 0:e.refresh)),d()}}}),a})(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;for(const o of e){var r;(null!==(r=this.sessions.get(o.id))&&void 0!==r&&r.parserSession||t.includes(o))&&(o.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.150",
3
+ "version": "0.1.154",
4
4
  "description": "Rebroadcast and Prebuffer for VideoCameras.",
5
5
  "author": "Scrypted",
6
6
  "license": "Apache-2.0",
package/src/main.ts CHANGED
@@ -3,21 +3,18 @@ import { MixinProvider, ScryptedDeviceType, ScryptedInterface, MediaObject, Vide
3
3
  import sdk from '@scrypted/sdk';
4
4
  import { once } from 'events';
5
5
  import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
6
- import { createRebroadcaster, ParserOptions, ParserSession, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
7
- import { createMpegTsParser, createFragmentedMp4Parser, StreamChunk, StreamParser, createRtpParser } from '@scrypted/common/src/stream-parser';
6
+ import { handleRebroadcasterClient, ParserOptions, ParserSession, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
7
+ import { createMpegTsParser, createFragmentedMp4Parser, StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
8
8
  import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
9
- import dgram from 'dgram';
10
9
  import { listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
11
- import { RtspServer } from './rtsp-server';
10
+ import { createRtspParser, RtspServer } from './rtsp-server';
11
+ import { Duplex } from 'stream';
12
12
 
13
13
  const { mediaManager, log, systemManager, deviceManager } = sdk;
14
14
 
15
15
  const defaultPrebufferDuration = 10000;
16
16
  const PREBUFFER_DURATION_MS = 'prebufferDuration';
17
17
  const SEND_KEYFRAME = 'sendKeyframe';
18
- const AUDIO_CONFIGURATION_KEY_PREFIX = 'audioConfiguration-';
19
- const FFMPEG_INPUT_ARGUMENTS_KEY_PREFIX = 'ffmpegInputArguments-';
20
- const REBROADCAST_MODE_KEY_PREFIX = 'rebroadcastMode-';
21
18
  const DEFAULT_AUDIO = 'Default';
22
19
  const AAC_AUDIO = 'AAC or No Audio';
23
20
  const AAC_AUDIO_DESCRIPTION = `${AAC_AUDIO} (Copy)`;
@@ -42,12 +39,11 @@ interface PrebufferStreamChunk {
42
39
  interface Prebuffers {
43
40
  mp4: PrebufferStreamChunk[];
44
41
  mpegts: PrebufferStreamChunk[];
45
- rtpvideo: PrebufferStreamChunk[];
46
- rtpaudio: PrebufferStreamChunk[];
42
+ rtsp: PrebufferStreamChunk[];
47
43
  }
48
44
 
49
- type PrebufferParsers = 'mpegts' | 'mp4' | 'rtpvideo' | 'rtpaudio';
50
- const PrebufferParserValues: PrebufferParsers[] = ['mpegts', 'mp4', 'rtpvideo', 'rtpaudio'];
45
+ type PrebufferParsers = 'mpegts' | 'mp4' | 'rtsp';
46
+ const PrebufferParserValues: PrebufferParsers[] = ['mpegts', 'mp4', 'rtsp'];
51
47
 
52
48
  class PrebufferSession {
53
49
 
@@ -56,15 +52,13 @@ class PrebufferSession {
56
52
  prebuffers: Prebuffers = {
57
53
  mp4: [],
58
54
  mpegts: [],
59
- rtpvideo: [],
60
- rtpaudio: [],
55
+ rtsp: [],
61
56
  };
62
57
  parsers: { [container: string]: StreamParser };
58
+ sdp: Promise<string>;
63
59
 
64
60
  detectedIdrInterval = 0;
65
61
  prevIdr = 0;
66
- detectedAudioCodec: string;
67
- detectedVideoCodec: string;
68
62
  audioDisabled = false;
69
63
 
70
64
  mixinDevice: VideoCamera;
@@ -75,22 +69,23 @@ class PrebufferSession {
75
69
  inactivityTimeout: NodeJS.Timeout;
76
70
  audioConfigurationKey: string;
77
71
  ffmpegInputArgumentsKey: string;
72
+ lastDetectedAudioCodecKey: string;
78
73
  rebroadcastModeKey: string;
79
74
 
80
75
  constructor(public mixin: PrebufferMixin, public streamName: string, public streamId: string, public stopInactive: boolean) {
81
76
  this.storage = mixin.storage;
82
77
  this.console = mixin.console;
83
78
  this.mixinDevice = mixin.mixinDevice;
84
- this.audioConfigurationKey = AUDIO_CONFIGURATION_KEY_PREFIX + this.streamId;
85
- this.ffmpegInputArgumentsKey = FFMPEG_INPUT_ARGUMENTS_KEY_PREFIX + this.streamId;
86
- this.rebroadcastModeKey = REBROADCAST_MODE_KEY_PREFIX + this.streamId;
79
+ this.audioConfigurationKey = 'audioConfiguration-' + this.streamId;
80
+ this.ffmpegInputArgumentsKey = 'ffmpegInputArguments-' + this.streamId;
81
+ this.rebroadcastModeKey = 'rebroadcastMode-' + this.streamId;
82
+ this.lastDetectedAudioCodecKey = 'lastDetectedAudioCodec-' + this.streamId;
87
83
  }
88
84
 
89
85
  clearPrebuffers() {
90
86
  this.prebuffers.mp4 = [];
91
87
  this.prebuffers.mpegts = [];
92
- this.prebuffers.rtpaudio = [];
93
- this.prebuffers.rtpvideo = [];
88
+ this.prebuffers.rtsp = [];
94
89
  }
95
90
 
96
91
  ensurePrebufferSession() {
@@ -230,8 +225,7 @@ class PrebufferSession {
230
225
  async startPrebufferSession() {
231
226
  this.prebuffers.mp4 = [];
232
227
  this.prebuffers.mpegts = [];
233
- this.prebuffers.rtpvideo = [];
234
- this.prebuffers.rtpaudio = [];
228
+ this.prebuffers.rtsp = [];
235
229
  const prebufferDurationMs = parseInt(this.storage.getItem(PREBUFFER_DURATION_MS)) || defaultPrebufferDuration;
236
230
 
237
231
  let mso: MediaStreamOptions;
@@ -251,23 +245,27 @@ class PrebufferSession {
251
245
 
252
246
  const { isUsingDefaultAudioConfig, aacAudio, compatibleAudio, reencodeAudio } = this.getAudioConfig();
253
247
 
248
+ let detectedAudioCodec = this.storage.getItem(this.lastDetectedAudioCodecKey) || undefined;
249
+ if (detectedAudioCodec === 'null')
250
+ detectedAudioCodec = null;
251
+
254
252
  let probingAudioCodec = false;
255
- if (!audioSoftMuted && !advertisedAudioCodec && isUsingDefaultAudioConfig && this.detectedAudioCodec === undefined) {
253
+ if (!audioSoftMuted && !advertisedAudioCodec && isUsingDefaultAudioConfig && detectedAudioCodec === undefined) {
256
254
  this.console.warn('Camera did not report an audio codec, muting the audio stream and probing the codec.');
257
255
  probingAudioCodec = true;
258
256
  }
259
257
 
260
258
  // complain to the user about the codec if necessary. upstream may send a audio
261
259
  // stream but report none exists (to request muting).
262
- if (!audioSoftMuted && advertisedAudioCodec && this.detectedAudioCodec !== undefined
263
- && this.detectedAudioCodec !== advertisedAudioCodec) {
264
- this.console.warn('Audio codec plugin reported vs detected mismatch', advertisedAudioCodec, this.detectedAudioCodec);
260
+ if (!audioSoftMuted && advertisedAudioCodec && detectedAudioCodec !== undefined
261
+ && detectedAudioCodec !== advertisedAudioCodec) {
262
+ this.console.warn('Audio codec plugin reported vs detected mismatch', advertisedAudioCodec, detectedAudioCodec);
265
263
  }
266
264
 
267
265
  // the assumed audio codec is the detected codec first and the reported codec otherwise.
268
- const assumedAudioCodec = this.detectedAudioCodec === undefined
266
+ const assumedAudioCodec = detectedAudioCodec === undefined
269
267
  ? advertisedAudioCodec?.toLowerCase()
270
- : this.detectedAudioCodec?.toLowerCase();
268
+ : detectedAudioCodec?.toLowerCase();
271
269
 
272
270
  // after probing the audio codec is complete, alert the user with appropriate instructions.
273
271
  // assume the codec is user configurable unless the camera explictly reports otherwise.
@@ -280,14 +278,14 @@ class PrebufferSession {
280
278
  }
281
279
  this.console.warn('Configure your camera to output AAC, MP3, MP2, or Opus audio. Suboptimal audio codec in use:', assumedAudioCodec);
282
280
  }
283
- else if (!audioSoftMuted && isUsingDefaultAudioConfig && advertisedAudioCodec === undefined && this.detectedAudioCodec !== undefined) {
281
+ else if (!audioSoftMuted && isUsingDefaultAudioConfig && advertisedAudioCodec === undefined && detectedAudioCodec !== undefined) {
284
282
  // handling compatible codecs that were unspecified...
285
- if (this.detectedAudioCodec === 'aac') {
286
- log.a(`${this.mixin.name} did not report a codec and ${this.detectedAudioCodec} was found during probe. Select '${AAC_AUDIO}' in the camera stream's Rebroadcast settings to suppress this alert.`);
287
- }
288
- else {
289
- log.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.`);
290
- }
283
+ // if (detectedAudioCodec === 'aac') {
284
+ // log.a(`${this.mixin.name} did not report a codec and ${detectedAudioCodec} was found during probe. Select '${AAC_AUDIO}' in the camera stream's Rebroadcast settings to suppress this alert and improve startup time.`);
285
+ // }
286
+ // else {
287
+ // log.a(`${this.mixin.name} did not report a codec and ${detectedAudioCodec} was found during probe. Select '${COMPATIBLE_AUDIO}' in the camera stream's Rebroadcast settings to suppress this alert and improve startup time.`);
288
+ // }
291
289
  }
292
290
  }
293
291
 
@@ -306,7 +304,7 @@ class PrebufferSession {
306
304
  this.audioDisabled = false;
307
305
  let acodec: string[];
308
306
 
309
- const detectedNoAudio = this.detectedAudioCodec === null;
307
+ const detectedNoAudio = detectedAudioCodec === null;
310
308
 
311
309
  // if the camera reports audio is incompatible and the user can't do anything about it
312
310
  // enable transcoding by default. however, still allow the user to change the settings
@@ -320,7 +318,7 @@ class PrebufferSession {
320
318
  mustTranscode = true;
321
319
  }
322
320
 
323
- if (audioSoftMuted || probingAudioCodec || detectedNoAudio) {
321
+ if (audioSoftMuted || probingAudioCodec) {
324
322
  // no audio? explicitly disable it.
325
323
  acodec = ['-an'];
326
324
  this.audioDisabled = true;
@@ -338,9 +336,11 @@ class PrebufferSession {
338
336
  '-flags', '+global_header',
339
337
  ];
340
338
  }
341
- else if (aacAudio) {
339
+ else if (aacAudio || detectedNoAudio) {
342
340
  // NOTE: If there is no audio track, the aac filters will still work fine without complaints
343
341
  // from ffmpeg. This is why AAC and No Audio can be grouped into a single setting.
342
+ // This is preferred, because failure and recovery is preferable to
343
+ // permanently muting camera audio due to erroneous detection.
344
344
  acodec = [
345
345
  '-acodec',
346
346
  'copy',
@@ -389,8 +389,9 @@ class PrebufferSession {
389
389
  });
390
390
  }
391
391
  else {
392
- rbo.parsers.rtpvideo = createRtpParser('-an', '-vcodec', 'copy');
393
- rbo.parsers.rtpaudio = createRtpParser('-vn', '-acodec', 'copy');
392
+ const parser = createRtspParser();
393
+ this.sdp = parser.sdp;
394
+ rbo.parsers.rtsp = parser;
394
395
  }
395
396
 
396
397
  this.parsers = rbo.parsers;
@@ -399,6 +400,10 @@ class PrebufferSession {
399
400
  const extraInputArguments = this.storage.getItem(this.ffmpegInputArgumentsKey) || DEFAULT_FFMPEG_INPUT_ARGUMENTS;
400
401
  ffmpegInput.inputArguments.unshift(...extraInputArguments.split(' '));
401
402
 
403
+ // before launching the parser session, clear out the last detected codec.
404
+ // an erroneous cached codec could cause ffmpeg to fail to start.
405
+ this.storage.removeItem(this.lastDetectedAudioCodecKey);
406
+
402
407
  const session = await startParserSession(ffmpegInput, rbo);
403
408
 
404
409
  if (!session.inputAudioCodec) {
@@ -412,8 +417,7 @@ class PrebufferSession {
412
417
  }
413
418
 
414
419
  // set/update the detected codec, set it to null if no audio was found.
415
- this.detectedAudioCodec = session.inputAudioCodec || null;
416
- this.detectedVideoCodec = session.inputVideoCodec || null;
420
+ this.storage.setItem(this.lastDetectedAudioCodecKey, session.inputAudioCodec || 'null');
417
421
 
418
422
  if (session.inputVideoCodec !== 'h264') {
419
423
  this.console.error(`Video codec is not h264. If there are errors, try changing your camera's encoder output.`);
@@ -498,13 +502,19 @@ class PrebufferSession {
498
502
  this.console.log(this.streamName, 'active rebroadcast clients:', this.activeClients);
499
503
  }
500
504
 
501
- inactivityCheck(session: ParserSession<PrebufferParsers>) {
505
+ inactivityCheck(session: ParserSession<PrebufferParsers>, refresh: boolean) {
502
506
  this.printActiveClients();
503
507
  if (!this.stopInactive)
504
508
  return;
505
509
  if (this.activeClients)
506
510
  return;
507
511
 
512
+ // by default, clients disconnecting will reset the inactivity timeout.
513
+ // but in some cases, like optimistic prebuffer stream snapshots (google sdm)
514
+ // we do not want that behavior.
515
+ if (this.inactivityTimeout && !refresh)
516
+ return;
517
+
508
518
  clearTimeout(this.inactivityTimeout)
509
519
  this.inactivityTimeout = setTimeout(() => {
510
520
  if (this.activeClients)
@@ -514,7 +524,7 @@ class PrebufferSession {
514
524
  }, 30000);
515
525
  }
516
526
 
517
- async getVideoStream(options?: MediaStreamOptions): Promise<MediaObject> {
527
+ async getVideoStream(options?: RequestMediaStreamOptions): Promise<MediaObject> {
518
528
  this.ensurePrebufferSession();
519
529
 
520
530
  const session = await this.parserSessionPromise;
@@ -527,117 +537,32 @@ class PrebufferSession {
527
537
  const createContainerServer = async (container: PrebufferParsers) => {
528
538
  const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
529
539
 
530
- if (this.parsers[container].parseDatagram) {
531
- let sdp = Buffer.concat(await session.sdp).toString();
532
-
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');
557
- }
558
- }
559
-
560
- const wv = safeWriteData((packet, rtcp) => server.sendVideo(packet, rtcp));
561
- const wa = safeWriteData((packet, rtcp) => server.sendAudio(packet, rtcp));
562
- const cleanup = () => {
563
- session.removeListener('rtpvideo', wv);
564
- session.removeListener('rtpaudio', wa);
565
- session.removeListener('killed', cleanup);
566
- }
567
-
568
- session.once('killed', cleanup);
569
-
570
- const sdpClient = await listenZeroSingleClient();
571
- sdpClient.clientPromise.then(async (c) => {
572
- this.activeClients++;
573
- this.printActiveClients();
574
-
575
- c.once('close', () => {
576
- this.activeClients--;
577
- this.inactivityCheck(session);
578
- cleanup();
579
- });
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
- }
540
+ let socketPromise: Promise<Duplex>;
541
+ let containerUrl: string;
542
+
543
+ if (container === 'rtsp') {
544
+ this.sdp.then(sdp => console.log(sdp));
545
+ const client = await listenZeroSingleClient();
546
+ socketPromise = client.clientPromise.then(async (socket) => {
547
+ let sdp = await this.sdp;
548
+ const server = new RtspServer(socket, sdp);
549
+ await server.handlePlayback();
550
+ return socket;
622
551
  })
623
- .catch(e => {
624
- this.console.error('failure client', e)
625
- cleanup()
626
- });
627
-
628
- session.on('rtpvideo', wv)
629
- session.on('rtpaudio', wa);
630
- const url = sdpClient.url.replace('tcp://', 'rtsp://');
631
- return url;
552
+ containerUrl = client.url.replace('tcp://', 'rtsp://');
553
+ }
554
+ else {
555
+ const client = await listenZeroSingleClient();
556
+ socketPromise = client.clientPromise;
557
+ containerUrl = `tcp://127.0.0.1:${client.port}`
632
558
  }
633
559
 
634
- const { server, port } = await createRebroadcaster({
560
+ handleRebroadcasterClient(socketPromise, {
635
561
  console: this.console,
636
562
  connect: (writeData, destroy) => {
637
563
  this.activeClients++;
638
564
  this.printActiveClients();
639
565
 
640
- server.close();
641
566
  const now = Date.now();
642
567
 
643
568
  const safeWriteData = (chunk: StreamChunk) => {
@@ -677,19 +602,17 @@ class PrebufferSession {
677
602
 
678
603
  return () => {
679
604
  this.activeClients--;
680
- this.inactivityCheck(session);
605
+ this.inactivityCheck(session, options?.refresh !== false);
681
606
  cleanup();
682
607
  };
683
608
  }
684
609
  })
685
610
 
686
- setTimeout(() => server.close(), 30000);
687
-
688
- return `tcp://127.0.0.1:${port}`;
611
+ return containerUrl;
689
612
  }
690
613
 
691
614
  const rtspMode = this.storage.getItem(this.rebroadcastModeKey) === 'RTSP';
692
- const defaultContainer = rtspMode ? 'rtpvideo' : 'mpegts';
615
+ const defaultContainer = rtspMode ? 'rtsp' : 'mpegts';
693
616
 
694
617
  const container: PrebufferParsers = this.parsers[options?.container] ? options?.container as PrebufferParsers : defaultContainer;
695
618
 
@@ -954,18 +877,11 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
954
877
 
955
878
  const prebuffer = parseInt(this.storage.getItem(PREBUFFER_DURATION_MS)) || defaultPrebufferDuration;
956
879
 
957
- if (!enabledStreams) {
958
- ret.push({
959
- id: 'default',
960
- name: 'Default',
961
- prebuffer,
962
- });
963
- }
964
- else {
965
- for (const enabledStream of enabledStreams) {
966
- enabledStream.prebuffer = prebuffer;
967
- }
880
+ for (const mso of ret) {
881
+ if (this.sessions.get(mso.id)?.parserSession || enabledStreams.includes(mso))
882
+ mso.prebuffer = prebuffer;
968
883
  }
884
+
969
885
  return ret;
970
886
  }
971
887
 
@@ -1,11 +1,53 @@
1
- import { readLine } from '../../../common/src/read-length';
2
- import net from 'net';
3
- import { Duplex, Readable } from 'stream';
1
+ import { readLength, readLine } from '../../../common/src/read-stream';
2
+ import { Duplex } from 'stream';
4
3
  import { randomBytes } from 'crypto';
4
+ import { StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
5
5
 
6
+ interface Headers {
7
+ [header: string]: string
8
+ }
9
+
10
+ function findSyncFrame(streamChunks: StreamChunk[]): StreamChunk[] {
11
+ return streamChunks;
12
+ }
6
13
 
7
- interface Headers{
8
- [header: string]: string
14
+ export interface RtspStreamParser extends StreamParser {
15
+ sdp: Promise<string>;
16
+ }
17
+
18
+ export function createRtspParser(): RtspStreamParser {
19
+ let resolve: any;
20
+
21
+ return {
22
+ container: 'rtsp',
23
+ tcpProtocol: 'rtsp://127.0.0.1/' + randomBytes(8).toString('hex'),
24
+ inputArguments: [
25
+ '-rtsp_transport',
26
+ 'tcp',
27
+ ],
28
+ outputArguments: [
29
+ '-rtsp_transport',
30
+ 'tcp',
31
+ '-vcodec', 'copy',
32
+ '-acodec', 'copy',
33
+ '-f', 'rtsp',
34
+ ],
35
+ findSyncFrame,
36
+ sdp: new Promise<string>(r => resolve = r),
37
+ async *parse(duplex, width, height) {
38
+ const server = new RtspServer(duplex);
39
+ await server.handleSetup();
40
+ resolve(server.sdp);
41
+ for await (const { type, rtcp, header, packet } of server.handleRecord()) {
42
+ yield {
43
+ chunks: [header, packet],
44
+ type,
45
+ width,
46
+ height,
47
+ }
48
+ }
49
+ }
50
+ }
9
51
  }
10
52
 
11
53
  function parseHeaders(headers: string[]): Headers {
@@ -23,27 +65,47 @@ function parseHeaders(headers: string[]): Headers {
23
65
 
24
66
  export class RtspServer {
25
67
  session: string;
26
- constructor(public socket: Duplex, public sdp: string, public playing: (server: RtspServer) => void) {
68
+ constructor(public duplex: Duplex, public sdp?: string) {
27
69
  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);
70
+ }
71
+
72
+ async handleSetup() {
73
+ let currentHeaders: string[] = [];
74
+ while (true) {
75
+ let line = await readLine(this.duplex);
76
+ line = line.trim();
77
+ if (!line) {
78
+ if (!await this.headers(currentHeaders))
79
+ break;
80
+ currentHeaders = [];
81
+ continue;
44
82
  }
83
+ currentHeaders.push(line);
45
84
  }
46
- catch (e) {
85
+
86
+ }
87
+
88
+ async handlePlayback() {
89
+ return this.handleSetup();
90
+ }
91
+
92
+ async *handleRecord(): AsyncGenerator<{
93
+ type: 'audio' | 'video',
94
+ rtcp: boolean,
95
+ header: Buffer,
96
+ packet: Buffer,
97
+ }> {
98
+ while (true) {
99
+ const header = await readLength(this.duplex, 4);
100
+ const length = header.readUInt16BE(2);
101
+ const packet = await readLength(this.duplex, length);
102
+ const id = header.readUInt8(1);
103
+ yield {
104
+ type: id < 2 ? 'video' : 'audio',
105
+ rtcp: id % 2 === 1,
106
+ header,
107
+ packet,
108
+ }
47
109
  }
48
110
  }
49
111
 
@@ -53,8 +115,8 @@ export class RtspServer {
53
115
  header.writeUInt8(channel, 1);
54
116
  header.writeUInt16BE(rtp.length, 2);
55
117
 
56
- this.socket.write(header);
57
- this.socket.write(Buffer.from(rtp));
118
+ this.duplex.write(header);
119
+ this.duplex.write(Buffer.from(rtp));
58
120
  }
59
121
 
60
122
  sendVideo(packet: Buffer, rtcp: boolean) {
@@ -67,7 +129,6 @@ export class RtspServer {
67
129
 
68
130
  options(url: string, requestHeaders: Headers) {
69
131
  const headers: Headers = {};
70
- headers['CSeq'] = requestHeaders['cseq'];
71
132
  headers['Public'] = 'DESCRIBE, OPTIONS, PAUSE, PLAY, SETUP, TEARDOWN, ANNOUNCE, RECORD';
72
133
 
73
134
  this.respond(200, 'OK', requestHeaders, headers);
@@ -93,8 +154,22 @@ export class RtspServer {
93
154
  headers['Range'] = 'npt=now-';
94
155
  headers['Session'] = this.session;
95
156
  this.respond(200, 'OK', requestHeaders, headers);
157
+ }
158
+
159
+ async announce(url: string, requestHeaders: Headers) {
160
+ const contentLength = parseInt(requestHeaders['content-length']);
161
+ const sdpBuffer = await readLength(this.duplex, contentLength);
162
+ this.sdp = sdpBuffer.toString();
163
+ const headers: Headers = {};
164
+ headers['Session'] = this.session;
165
+
166
+ this.respond(200, 'OK', requestHeaders, headers);
167
+ }
96
168
 
97
- this.playing(this);
169
+ async record(url: string, requestHeaders: Headers) {
170
+ const headers: Headers = {};
171
+ headers['Session'] = this.session;
172
+ this.respond(200, 'OK', requestHeaders, headers);
98
173
  }
99
174
 
100
175
  async headers(headers: string[]) {
@@ -107,7 +182,7 @@ export class RtspServer {
107
182
  }
108
183
 
109
184
  await this[method](url, requestHeaders);
110
- return method !== 'play';
185
+ return method !== 'play' && method !== 'record';
111
186
  }
112
187
 
113
188
  respond(code: number, message: string, requestHeaders: Headers, headers: Headers, buffer?: Buffer) {
@@ -120,8 +195,8 @@ export class RtspServer {
120
195
  response += `${key}: ${value}\r\n`;
121
196
  }
122
197
  response += '\r\n';
123
- this.socket.write(response);
198
+ this.duplex.write(response);
124
199
  if (buffer)
125
- this.socket.write(buffer);
200
+ this.duplex.write(buffer);
126
201
  }
127
202
  }