@scrypted/prebuffer-mixin 0.1.145 → 0.1.149

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