@scrypted/prebuffer-mixin 0.1.168 → 0.1.171
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +3 -2
- package/src/main.ts +48 -37
- package/src/rfc4571.ts +30 -3
package/dist/main.nodejs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(()=>{var e={510:function(e,t,i){"use strict";var s=this&&this.__createBinding||(Object.create?function(e,t,i,s){void 0===s&&(s=i),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[i]}})}:function(e,t,i,s){void 0===s&&(s=i),e[s]=t[i]}),r=this&&this.__exportStar||function(e,t){for(var i in e)"default"===i||Object.prototype.hasOwnProperty.call(t,i)||s(t,e,i)};Object.defineProperty(t,"__esModule",{value:!0}),t.MixinDeviceBase=t.ScryptedDeviceBase=void 0,r(i(632),t);const o=i(632);class n extends o.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=n;class a extends o.DeviceBase{constructor(e,t,i,s,r){super(),this.mixinDevice=e,this.mixinDeviceInterfaces=t,this.mixinProviderNativeId=s,this._mixinStorageSuffix=r,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=a,function(){function e(e){return function(){return this._lazyLoadDeviceState(),this._deviceState[e]}}function t(e){return function(t){this._lazyLoadDeviceState(),this._deviceState[e]=t}}for(var i of Object.values(o.ScryptedInterfaceProperty))Object.defineProperty(n.prototype,i,{set:t(i),get:e(i)}),Object.defineProperty(a.prototype,i,{set:t(i),get:e(i)})}();let c={};try{c=Object.assign(c,{log:deviceManager.getDeviceLogger(void 0),deviceManager,endpointManager,mediaManager,systemManager,pluginHostAPI})}catch(e){console.error("sdk initialization error, import @scrypted/types or use @scrypted/web-sdk instead",e)}t.default=c},632:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ScryptedMimeTypes=t.ScryptedInterface=t.MediaPlayerState=t.LockState=t.ThermostatMode=t.TemperatureUnit=t.FanMode=t.HumidityMode=t.ScryptedDeviceType=t.ScryptedInterfaceDescriptors=t.ScryptedInterfaceProperty=t.DeviceBase=void 0;t.DeviceBase=class{},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"}(t.ScryptedInterfaceProperty||(t.ScryptedInterfaceProperty={})),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"]},RTCSignalingChannel:{name:"RTCSignalingChannel",methods:["startRTCSignalingSession"],properties:[]}},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"}(t.ScryptedDeviceType||(t.ScryptedDeviceType={})),function(e){e.Humidify="Humidify",e.Dehumidify="Dehumidify",e.Auto="Auto",e.Off="Off"}(t.HumidityMode||(t.HumidityMode={})),function(e){e.Auto="Auto",e.Manual="Manual"}(t.FanMode||(t.FanMode={})),function(e){e.C="C",e.F="F"}(t.TemperatureUnit||(t.TemperatureUnit={})),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"}(t.ThermostatMode||(t.ThermostatMode={})),function(e){e.Locked="Locked",e.Unlocked="Unlocked",e.Jammed="Jammed"}(t.LockState||(t.LockState={})),function(e){e.Idle="Idle",e.Playing="Playing",e.Paused="Paused",e.Buffering="Buffering"}(t.MediaPlayerState||(t.MediaPlayerState={})),function(e){e.ScryptedDevice="ScryptedDevice",e.ScryptedPlugin="ScryptedPlugin",e.OnOff="OnOff",e.Brightness="Brightness",e.ColorSettingTemperature="ColorSettingTemperature",e.ColorSettingRgb="ColorSettingRgb",e.ColorSettingHsv="ColorSettingHsv",e.Notifier="Notifier",e.StartStop="StartStop",e.Pause="Pause",e.Dock="Dock",e.TemperatureSetting="TemperatureSetting",e.Thermometer="Thermometer",e.HumiditySensor="HumiditySensor",e.Camera="Camera",e.VideoCamera="VideoCamera",e.VideoCameraConfiguration="VideoCameraConfiguration",e.Intercom="Intercom",e.Lock="Lock",e.PasswordStore="PasswordStore",e.Authenticator="Authenticator",e.Scene="Scene",e.Entry="Entry",e.EntrySensor="EntrySensor",e.DeviceProvider="DeviceProvider",e.DeviceDiscovery="DeviceDiscovery",e.DeviceCreator="DeviceCreator",e.Battery="Battery",e.Refresh="Refresh",e.MediaPlayer="MediaPlayer",e.Online="Online",e.SoftwareUpdate="SoftwareUpdate",e.BufferConverter="BufferConverter",e.Settings="Settings",e.BinarySensor="BinarySensor",e.IntrusionSensor="IntrusionSensor",e.PowerSensor="PowerSensor",e.AudioSensor="AudioSensor",e.MotionSensor="MotionSensor",e.AmbientLightSensor="AmbientLightSensor",e.OccupancySensor="OccupancySensor",e.FloodSensor="FloodSensor",e.UltravioletSensor="UltravioletSensor",e.LuminanceSensor="LuminanceSensor",e.PositionSensor="PositionSensor",e.Readme="Readme",e.OauthClient="OauthClient",e.MixinProvider="MixinProvider",e.HttpRequestHandler="HttpRequestHandler",e.EngineIOHandler="EngineIOHandler",e.PushHandler="PushHandler",e.Program="Program",e.Scriptable="Scriptable",e.ObjectDetector="ObjectDetector",e.ObjectDetection="ObjectDetection",e.HumiditySetting="HumiditySetting",e.Fan="Fan",e.RTCSignalingChannel="RTCSignalingChannel"}(t.ScryptedInterface||(t.ScryptedInterface={})),function(e){e.AcceptUrlParameter="accept-url",e.Url="text/x-uri",e.InsecureLocalUrl="text/x-insecure-local-uri",e.LocalUrl="text/x-local-uri",e.PushEndpoint="text/x-push-endpoint",e.MediaStreamUrl="text/x-media-url",e.FFmpegInput="x-scrypted/x-ffmpeg-input",e.RTCAVSignalingPrefix="x-scrypted-rtc-signaling-",e.RTCAVOffer="x-scrypted/x-rtc-av-offer",e.RTCAVAnswer="x-scrypted/x-rtc-av-answer",e.RTCAVSignalingOfferSetup="x-scrypted/x-rtc-av-signalling-offer-setup",e.SchemePrefix="x-scrypted/x-scrypted-scheme-",e.MediaObject="x-scrypted/x-scrypted-media-object"}(t.ScryptedMimeTypes||(t.ScryptedMimeTypes={}))}},t={};function i(s){var r=t[s];if(void 0!==r)return r.exports;var o=t[s]={exports:{}};return e[s].call(o.exports,o,o.exports,i),o.exports}i.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var s in t)i.o(t,s)&&!i.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),i.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var s={};(()=>{"use strict";i.r(s),i.d(s,{default:()=>re});var e=i(510),t=i.n(e);const r=require("events"),{deviceManager:o}=t();class n extends e.MixinDeviceBase{constructor(t,i,s){super(t,s.mixinDeviceInterfaces,i,s.providerNativeId,s.mixinStorageSuffix),this.settingsGroup=s.group,this.settingsGroupKey=s.groupKey,process.nextTick((()=>o.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)))}async getSettings(){const t=this.mixinDeviceInterfaces.includes(e.ScryptedInterface.Settings)?this.mixinDevice.getSettings():void 0,i=this.getMixinSettings(),s=[];try{const e=await t||[];s.push(...e)}catch(e){const t=this.name,i=`${t} Extension settings failed to load.`;this.console.error(i,e),s.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:i,readonly:!0})}try{const e=await i||[];for(const t of e)t.group=t.group||this.settingsGroup,t.key=this.settingsGroupKey+":"+t.key;s.push(...e)}catch(e){const t=o.getDeviceState(this.mixinProviderNativeId).name,i=`${t} Extension settings failed to load.`;this.console.error(i,e),s.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:i,readonly:!0})}return s}async putSetting(t,i){const s=this.settingsGroupKey+":";if(!t?.startsWith(s))return this.mixinDevice.putSetting(t,i);await this.putMixinSetting(t.substring(s.length),i),o.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)}release(){o.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)}}const a=require("net");var c=i.n(a);const d=require("child_process");var u=i.n(d);const p=require("dgram");var l=i.n(p);async function m(e,t){e.bind(t),await(0,r.once)(e,"listening");const i=e.address().port;return{port:i,url:`udp://127.0.0.1:${i}`}}async function h(e){return m(e,0)}async function f(e,t){return e.bind(t),await(0,r.once)(e,"listening"),{port:t,url:`udp://127.0.0.1:${t}`}}async function g(){const e=new(c().Server),t=await async function(e){return e.listen(0),await(0,r.once)(e,"listening"),e.address().port}(e);return{url:`tcp://127.0.0.1:${t}`,port:t,clientPromise:new Promise(((t,i)=>{const s=setTimeout((()=>{i(new Error("timeout waiting for client"))}),3e4);e.on("connection",(i=>{e.close(),clearTimeout(s),t(i)}))}))}}const y=require("process");var S=i.n(y);const v=["decode_slice_header error","no frame!","non-existing PPS"];const{mediaManager:b}=t();async function w(e,t){return new Promise((i=>{const s=r=>{const o=r.toString(),n=o.indexOf(`${t}: `);if(-1!==n){const r=o.substring(n+t.length+1).trim();let a=r.indexOf(" ");const c=r.indexOf(",");-1!==a&&c<a&&(a=c),-1!==a&&(e.stdout.removeListener("data",s),e.stderr.removeListener("data",s),i(r.substring(0,a)))}};e.stdout.on("data",s),e.stderr.on("data",s)}))}async function P(e,t){const{console:i}=t;let s,o=!0;const n=new r.EventEmitter;let a,c,d,p,m;n.on("error",(e=>i.error("rebroadcast error",e)));const y=new Promise(((e,t)=>{p=e,m=t}));function P(){o&&(n.emit("killed"),n.emit("error",new Error("killed"))),o=!1,O?.kill(),setTimeout((()=>O?.kill("SIGKILL")),1e3),m?.(new Error("ffmpeg was killed before connecting to the rebroadcast session")),clearTimeout(s)}const M=e.inputArguments.slice();s=setTimeout(P,3e4);const I=e=>{let s;function r(){i.error("timeout waiting for data, killing parser session",e),P()}function o(){t.timeout&&(clearTimeout(s),s=setTimeout(r,t.timeout))}return n.once("killed",(()=>clearTimeout(s))),o(),{resetActivityTimer:o}};let C=!1;const x=["pipe","pipe","pipe"];let D=3;for(const e of Object.keys(t.parsers)){const s=t.parsers[e];if(s.parseDatagram){C=!0;const t=l().createSocket("udp4"),i=await h(t),r=l().createSocket("udp4");await f(r,i.port+1),n.once("killed",(()=>{t.close(),r.close()})),M.push(...s.outputArguments,i.url.replace("udp://","rtp://"));const{resetActivityTimer:o}=I(e);(async()=>{for await(const i of s.parseDatagram(t,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),n.emit(e,i),o()})(),(async()=>{for await(const t of s.parseDatagram(r,parseInt(d?.[2]),parseInt(d?.[3]),"rtcp"))p?.(void 0),n.emit(e,t),o()})()}else if(s.tcpProtocol){const t=await g(),r=new URL(s.tcpProtocol);r.port=t.port.toString(),M.push(...s.outputArguments,r.toString());const{resetActivityTimer:o}=I(e);(async()=>{const r=await t.clientPromise;try{for await(const t of s.parse(r,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),n.emit(e,t),o()}catch(e){i.error("rebroadcast parse error",e),P()}})()}else M.push(...s.outputArguments,"pipe:"+D++),x.push("pipe")}C&&(M.push("-sdp_file","pipe:"+D++),x.push("pipe")),M.unshift("-hide_banner"),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(" "))}(i,M);const O=u().spawn(await b.getFFmpegPath(),M,{stdio:x});let A;!function(e,t,i,s){const r=!!S().env.SCRYPTED_FFMPEG_NOISY||!!s?.getItem("SCRYPTED_FFMPEG_NOISY");function o(e){const s=o=>{const n=o.toString();for(const e of v)if(-1!==n.indexOf(e))return;if(!(r||i||-1===n.indexOf("frame=")&&-1===n.indexOf("size=")))return e(n),e("video/audio detected, discarding further input"),t.stdout.removeListener("data",s),void t.stderr.removeListener("data",s);e(n)};return s}t.stdout?.on("data",o(e.log)),t.stderr?.on("data",o(e.error)),t.on("exit",(()=>e.log("ffmpeg exited")))}(i,O,void 0,t?.storage),O.on("exit",P),A=C?new Promise((e=>{const t=[];O.stdio[D-1].on("data",(i=>{t.push(i),e(t)}))})):Promise.resolve([]);let T=0;return Object.keys(t.parsers).forEach((async e=>{const s=t.parsers[e];if(!s.parse||s.tcpProtocol)return;const r=O.stdio[3+T];T++;try{const{resetActivityTimer:t}=I(e);for await(const i of s.parse(r,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),n.emit(e,i),t()}catch(e){i.error("rebroadcast parse error",e),P()}})),async function(e){return w(e,"Audio")}(O).then((e=>a=e)),async function(e){return w(e,"Video")}(O).then((e=>c=e)),async function(e){return new Promise((t=>{const i=s=>{const r=s.toString(),o=/(([0-9]{2,5})x([0-9]{2,5}))/.exec(r);o&&(e.stdout.removeListener("data",i),e.stderr.removeListener("data",i),t(o))};e.stdout.on("data",i),e.stderr.on("data",i)}))}(O).then((e=>d=e)),await y,p=void 0,m=void 0,clearTimeout(s),{sdp:A,inputAudioCodec:a,inputVideoCodec:c,inputVideoResolution:d,isActive:()=>o,kill:P,mediaStreamOptions:e.mediaStreamOptions||{id:void 0,name:void 0},on(e,t){return n.on(e,t),this},once(e,t){return n.once(e,t),this},removeListener(e,t){return n.removeListener(e,t),this}}}async function M(e,t){const i=await e;let s=!0;const r=()=>{i.removeAllListeners(),i.destroy();const e=o;o=void 0,e?.()};let o=t?.connect((e=>{s&&(s=!1,e.startStream&&i.write(e.startStream));for(const t of e.chunks)i.write(t);return i.writableLength}),r);i.once("close",(()=>{r()})),i.on("error",(e=>t?.console?.log("client stream ended")))}async function I(e,t){if(!t)return Buffer.alloc(0);{const i=e.read(t);if(i)return i}return new Promise(((i,s)=>{const r=()=>{const s=e.read(t);s&&(n(),i(s))},o=()=>{n(),s(new Error(`stream ended during read for minimum ${t} bytes`))},n=()=>{e.removeListener("readable",r),e.removeListener("end",o)};e.on("readable",r),e.on("end",o)}))}const C="\n".charCodeAt(0);async function x(e){return async function(e,t){const i=[];let s=0;for(;;){const r=await I(e,1);if(!r)throw new Error("end of stream");if(r[0]===t)break;i[s++]=r[0]}return Buffer.from(i).toString()}(e,C)}function D(e){return e}function O(e){return{container:"mp4",outputArguments:[...e?.vcodec||[],...e?.acodec||[],"-movflags","frag_keyframe+empty_moov+default_base_moof","-f","mp4"],async*parse(e){const t=async function*(e){for(;;){const t=await I(e,8),i=t.readInt32BE(0)-8,s=t.slice(4).toString(),r=await I(e,i);yield{header:t,length:i,type:s,data:r}}}(e);let i,s,r;for await(const e of t)i?s||(s=e):i=e,yield{startStream:r,chunks:[e.header,e.data],type:e.type},i&&s&&!r&&(r=Buffer.concat([i.header,i.data,s.header,s.data]))},findSyncFrame:D}}const{systemManager:A}=t(),T="v4";class k extends e.ScryptedDeviceBase{hasEnabledMixin={};constructor(t){super(t);try{this.hasEnabledMixin=JSON.parse(this.storage.getItem("hasEnabledMixin"))}catch(e){this.hasEnabledMixin={}}this.pluginsComponent=A.getComponent("plugins"),A.listen((async(t,i,s)=>{i.eventInterface!==e.ScryptedInterface.ScryptedDevice||i.property||this.maybeEnableMixin(t)}));for(const e of Object.keys(A.getSystemState())){const t=A.getDeviceById(e);this.maybeEnableMixin(t)}}async shouldEnableMixin(e){return!0}async maybeEnableMixin(e){if(!e||e.mixins?.includes(this.id))return;if(this.hasEnabledMixin[e.id]===T)return;if(!await this.canMixin(e.type,e.interfaces))return;if(!await this.shouldEnableMixin(e))return;this.log.i("auto enabling mixin for "+e.name);const t=(e.mixins||[]).slice();t.push(this.id);const i=await this.pluginsComponent;await i.setMixins(e.id,t),this.setHasEnabledMixin(e.id)}setHasEnabledMixin(e){this.hasEnabledMixin[e]!==T&&(this.hasEnabledMixin[e]=T,this.storage.setItem("hasEnabledMixin",JSON.stringify(this.hasEnabledMixin)))}}const E=require("crypto");function R(e){return e}function _(e){const t={};for(const i of e.slice(1)){const e=i.indexOf(":");let s="";-1!==e&&(s=i.substring(e+1).trim());t[i.substring(0,e).toLowerCase()]=s}return t}class L extends class{write(e,t,i){let s=`${e}\r\n`;i&&(t["Content-Length"]=i.length.toString());for(const[e,i]of Object.entries(t))s+=`${e}: ${i}\r\n`;s+="\r\n",this.client.write(s),i&&this.client.write(i)}async readMessage(){let e=[];for(;;){let t=await x(this.client);if(t=t.trim(),!t)return e;e.push(t)}}}{cseq=0;constructor(e){super(),this.url=e;const t=new URL(e);this.client=c().connect(parseInt(t.port)||554,t.hostname)}async request(e,t,i,s){t=t||{};const r=`${e} ${this.url}${i||""} RTSP/1.0`;t.CSeq=(this.cseq++).toString(),this.write(r,t,s);const o=_(await this.readMessage()),n=parseInt(o["content-length"]);return n?{headers:o,body:await I(this.client,n)}:{headers:o,body:void 0}}async options(){return this.request("OPTIONS")}async describe(){return this.request("DESCRIBE",{Accept:"application/sdp"})}async setup(e,t){const i={Transport:`RTP/AVP/TCP;unicast;interleaved=${e}-${e+1}`};this.session&&(i.Session=this.session);const s=await this.request("SETUP",i,t);return s.headers.session&&(this.session=s.headers.session),s}async play(){const e={Range:"npt=0.000-"};return this.session&&(e.Session=this.session),await this.request("PLAY",e),this.client}async teardown(){try{return await this.request("TEARDOWN")}finally{this.client.destroy()}}}class B{videoChannel=0;audioChannel=2;udpPorts={video:0,audio:0};constructor(e,t,i){this.client=e,this.sdp=t,this.udp=i,this.session=(0,E.randomBytes)(4).toString("hex"),t&&(t=t.trim())}async handleSetup(){let e=[];for(;;){let t=await x(this.client);if(t=t.trim(),t)e.push(t);else{if(!await this.headers(e))break;e=[]}}}async handlePlayback(){return this.handleSetup()}async handleTeardown(){return this.handleSetup()}async*handleRecord(){for(;;){const e=await I(this.client,4),t=e.readUInt16BE(2),i=await I(this.client,t),s=e.readUInt8(1);yield{type:s-s%2===this.videoChannel?"video":"audio",rtcp:s%2==1,header:e,packet:i}}}send(e,t){const i=Buffer.alloc(4);i.writeUInt8(36,0),i.writeUInt8(t,1),i.writeUInt16BE(e.length,2),this.client.write(i),this.client.write(Buffer.from(e))}sendUdp(e,t,i){this.udp.send(t,i?e+1:e,"127.0.0.1")}sendVideo(e,t){this.udp&&this.udpPorts.video?this.sendUdp(this.udpPorts.video,e,t):this.send(e,t?this.videoChannel+1:this.videoChannel)}sendAudio(e,t){this.udp&&this.udpPorts.audio?this.sendUdp(this.udpPorts.audio,e,t):this.send(e,t?this.audioChannel+1:this.audioChannel)}options(e,t){const i={Public:"DESCRIBE, OPTIONS, PAUSE, PLAY, SETUP, TEARDOWN, ANNOUNCE, RECORD"};this.respond(200,"OK",t,i)}describe(e,t){const i={};i["Content-Base"]=e,i["Content-Type"]="application/sdp",this.respond(200,"OK",t,i,Buffer.from(this.sdp))}setup(e,t){const i={},s=t.transport;if(i.Transport=t.transport,i.Session=this.session,s.includes("UDP")){const t=s.match(/.*?client_port=([0-9]+)-([0-9]+)/),[i,r,o]=t;e.includes("audio")?this.udpPorts.audio=parseInt(r):this.udpPorts.video=parseInt(r)}else if(s.includes("TCP")){const t=s.match(/.*?interleaved=([0-9]+)-([0-9]+)/);if(t){const i=parseInt(t[1]);parseInt(t[2]);e.includes("audio")?this.audioChannel=i:this.videoChannel=i}}this.respond(200,"OK",t,i)}play(e,t){const i={};i["RTP-Info"]=`url=${e}/trackID=0;seq=0;rtptime=0,url=${e}/trackID=1;seq=0;rtptime=0`,i.Range="npt=now-",i.Session=this.session,this.respond(200,"OK",t,i)}async announce(e,t){const i=parseInt(t["content-length"]),s=await I(this.client,i);this.sdp=s.toString();const r={};r.Session=this.session,this.respond(200,"OK",t,r)}async record(e,t){const i={};i.Session=this.session,this.respond(200,"OK",t,i)}async teardown(e,t){const i={};i.Session=this.session,this.respond(200,"OK",t,i)}async headers(e){this.console?.log("request headers",e.join("\n"));let[t,i]=e[0].split(" ",2);t=t.toLowerCase();const s=_(e);if(this[t])return await this[t](i,s),"play"!==t&&"record"!==t&&"teardown"!==t;this.respond(400,"Bad Request",s,{})}respond(e,t,i,s,r){let o=`RTSP/1.0 ${e} ${t}\r\n`;i.cseq&&(s.CSeq=i.cseq),r&&(s["Content-Length"]=r.length.toString());for(const[e,t]of Object.entries(s))o+=`${e}: ${t}\r\n`;this.console?.log("response headers",o),o+="\r\n",this.client.write(o),r&&this.client.write(r)}}const H=require("stream"),{mediaManager:N}=t();async function V(e,t,i,s){let r=!0;const o=new H.EventEmitter,n=parseInt(t.match(/m=audio.* ([0-9]+)/)?.[1]),a=parseInt(t.match(/m=video.* ([0-9]+)/)?.[1]),c=()=>{r&&(o.emit("killed"),o.emit("error",new Error("killed"))),r=!1,e.destroy()};return e.on("close",c),e.on("error",c),(async()=>{for(;;){let t,i;s?(t=await I(e,4),i=t.readInt16BE(2)):(t=await I(e,2),i=t.readInt16BE(0));const r=await I(e,i);if(!s){const e=127&r[1],i=Buffer.alloc(2);i[0]=36,e===n?i[1]=0:e===a&&(i[1]=2),t=Buffer.concat([i,t])}const c={chunks:[t,r]};o.emit("rtsp",c)}})().finally(c),{sdp:Promise.resolve([Buffer.from(t)]),inputAudioCodec:i.audio.codec,inputVideoCodec:i.video.codec,inputVideoResolution:void 0,isActive:()=>r,kill:c,mediaStreamOptions:i,on(e,t){return o.on(e,t),this},once(e,t){return o.once(e,t),this},removeListener(e,t){return o.removeListener(e,t),this}}}const{mediaManager:U,log:F,systemManager:j,deviceManager:K}=t(),q=1e4,$="prebufferDuration",G="sendKeyframe",J="Default",z="AAC or No Audio",W=`${z} (Copy)`,Y="Compatible Audio",Q="Other Audio",X=["aac","mp3","mp2","opus"],Z="-fflags +genpts",ee=[z,Y,Q],te=["mpegts","mp4","rtsp"];class ie{prebuffers={mp4:[],mpegts:[],rtsp:[]};detectedIdrInterval=0;prevIdr=0;audioDisabled=!1;activeClients=0;constructor(e,t,i,s){this.mixin=e,this.streamName=t,this.streamId=i,this.stopInactive=s,this.storage=e.storage,this.console=e.console,this.mixinDevice=e.mixinDevice,this.audioConfigurationKey="audioConfiguration-"+this.streamId,this.ffmpegInputArgumentsKey="ffmpegInputArguments-"+this.streamId,this.rebroadcastModeKey="rebroadcastMode-"+this.streamId,this.lastDetectedAudioCodecKey="lastDetectedAudioCodec-"+this.streamId}clearPrebuffers(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtsp=[]}ensurePrebufferSession(){this.parserSessionPromise||this.mixin.released||(this.console.log(this.streamName,"prebuffer session started"),this.parserSessionPromise=this.startPrebufferSession(),this.parserSessionPromise.catch((()=>this.parserSessionPromise=void 0)))}getAudioConfig(){let e=this.storage.getItem(this.audioConfigurationKey)||"";ee.find((t=>e.startsWith(t)))||(e="");const t=-1!==e.indexOf(z),i=-1!==e.indexOf(Y),s=-1!==e.indexOf(Q);return{isUsingDefaultAudioConfig:!(t||i||s),aacAudio:t,compatibleAudio:i,reencodeAudio:s}}async getMixinSettings(){const e=[],t=this.parserSession;let i=0,s=0;const{mp4Mode:r}=this.getRebroadcastMode();for(const e of r?this.prebuffers.mp4:this.prebuffers.rtsp){s=s||e.time;for(const t of e.chunk.chunks)i+=t.byteLength}const o=Date.now()-s,n=Math.round(i/o*8),a=this.streamName?`Rebroadcast: ${this.streamName}`:"Rebroadcast";return e.push({title:"Audio Codec Transcoding",group:a,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)||J,choices:[J,W,"Compatible Audio (Copy)","Other Audio (Transcode)"]},{title:"FFmpeg Input Arguments Prefix",group:a,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:Z,choices:[Z,"-use_wallclock_as_timestamps 1","-v verbose"],combobox:!0},{title:"Rebroadcast Mode",group:a,description:"THIS FEATURE IS IN TESTING. DO NOT CHANGE THIS FROM MPEG-TS. The stream format to use when rebroadcasting.",placeholder:"MPEG-TS",choices:["MPEG-TS","RTSP","RTSP+MP4"],key:this.rebroadcastModeKey,value:this.storage.getItem(this.rebroadcastModeKey)||"MPEG-TS"}),t?e.push({key:"detectedResolution",group:a,title:"Detected Resolution and Bitrate",readonly:!0,value:`${t?.inputVideoResolution?.[0]||"unknown"} @ ${n||"unknown"} Kb/s`,description:"Configuring your camera to 1920x1080, 2000Kb/S, Variable Bit Rate, is recommended."},{key:"detectedCodec",group:a,title:"Detected Video/Audio Codecs",readonly:!0,value:(t?.inputVideoCodec?.toString()||"unknown")+"/"+(t?.inputAudioCodec?.toString()||"unknown"),description:"Configuring your camera to H264 video and AAC/MP3/MP2/Opus audio is recommended."},{key:"detectedKeyframe",group:a,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:a,key:"status",description:"Rebroadcast is currently idle and will be started automatically on demand.",value:"Idle",readonly:!0}),e}getRebroadcastMode(){const e=this.storage.getItem(this.rebroadcastModeKey),t=e?.startsWith("RTSP");return{rtspMode:e?.startsWith("RTSP"),mp4Mode:!t||e?.includes("MP4")}}async startPrebufferSession(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtsp=[];const t=parseInt(this.storage.getItem($))||q;let i;try{i=(await this.mixinDevice.getVideoStreamOptions()).find((e=>e.id===this.streamId))}catch(e){}const s=null===i?.audio,o=i?.audio?.codec,{isUsingDefaultAudioConfig:n,aacAudio:a,compatibleAudio:d,reencodeAudio:u}=this.getAudioConfig();let p=this.storage.getItem(this.lastDetectedAudioCodecKey)||void 0;"null"===p&&(p=null);let l=!1;s||o||!n||void 0!==p||(this.console.warn("Camera did not report an audio codec, muting the audio stream and probing the codec."),l=!0),!s&&o&&void 0!==p&&p!==o&&this.console.warn("Audio codec plugin reported vs detected mismatch",o,p);const m=void 0===p?o?.toLowerCase():p?.toLowerCase(),{rtspMode:h,mp4Mode:f}=this.getRebroadcastMode(),g=!h||f,y=!X.includes(m);!g||l||!1===i?.userConfigurable||s||y&&(n&&F.a(`${this.mixin.name} is using the ${m} 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:",m));const S=["-bsf:a","aac_adtstoasc"],v=[];let b;this.audioDisabled=!1;const w=null===p;let M=!1;if(!l&&n&&y&&(!1===i?.userConfigurable?this.console.log("camera reports it is not user configurable. transcoding due to incompatible codec",m):this.console.log("camera audio transcoding due to incompatible codec. configure the camera to use a compatible codec if possible."),M=!0),s||l)b=["-an"],this.audioDisabled=!0;else if(u||M)b=["-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(a||w)b=["-acodec","copy"],b.push(...S);else if(d)b=["-acodec","copy"],b.push(...v);else{b=["-acodec","copy"];const e="aac"===m?S:v;b.push(...e)}const I=["-vcodec","copy"],C={console:this.console,timeout:6e4,parsers:{}};if(this.parsers=C.parsers,this.console.log("rebroadcast mode:",h?"rtsp":"mpegts"),h){const e=function(){let e;return{container:"rtsp",tcpProtocol:"rtsp://127.0.0.1/"+(0,E.randomBytes)(8).toString("hex"),inputArguments:["-rtsp_transport","tcp"],outputArguments:["-rtsp_transport","tcp","-vcodec","copy","-acodec","copy","-f","rtsp"],findSyncFrame:R,sdp:new Promise((t=>e=t)),async*parse(t,i,s){const r=new B(t);await r.handleSetup(),e(r.sdp);for await(const{type:e,rtcp:t,header:o,packet:n}of r.handleRecord())yield{chunks:[o,n],type:e,width:i,height:s}}}}();this.sdp=e.sdp,C.parsers.rtsp=e}else C.parsers.mpegts={container:"mpegts",outputArguments:[...(x={vcodec:I,acodec:b})?.vcodec||[],...x?.acodec||[],"-f","mpegts"],parse:(D=188,A=e=>{if(71!=e[0])throw new Error("Invalid sync byte in mpeg-ts packet. Terminating stream.")},async function*(e){let t=[],i=0;for(;;){const s=e.read();if(!s){await(0,r.once)(e,"readable");continue}if(t.push(s),i+=s.length,i<D)continue;const o=Buffer.concat(t);A?.(o);const n=o.length%D,a=o.slice(0,o.length-n),c=o.slice(o.length-n);t=[c],i=c.length,yield{chunks:[a]}}}),findSyncFrame(e){for(let t=0;t<e.length;t++){const i=e[t];for(let s=0;s<i.chunks.length;s++){const r=i.chunks[s];let o=0;for(;o+188<r.length;){const i=r.subarray(o,o+188);if(256==((31&i[1])<<8|i[2])&&32&i[3]&&i[4]>0&&64&i[5])return e.slice(t);o+=188}}}return e}};var x,D,A;f&&(C.parsers.mp4=O({vcodec:I,acodec:b}));const T=await this.mixinDevice.getVideoStream(i),k="x-scrypted/x-rfc4571"===T.mimeType;let _,H;this.storage.removeItem(this.lastDetectedAudioCodecKey);const N=h&&!f;if(N&&k){const e=await U.convertMediaObjectToJSON(T,"x-scrypted/x-rfc4571"),{url:t,sdp:i,mediaStreamOptions:s}=e;_=await V(function(e){const t=new URL(e);if(!t.protocol.startsWith("tcp"))throw new Error("rfc4751 url must be tcp");return c().connect(parseInt(t.port),t.hostname)}(t),i,s),this.sdp=_.sdp.then((e=>Buffer.concat(e).toString()))}else{const t=await U.convertMediaObjectToBuffer(T,e.ScryptedMimeTypes.FFmpegInput),i=JSON.parse(t.toString());if(H=i.mediaStreamOptions,N&&"rtsp"===i.mediaStreamOptions?.container&&"scrypted"===i.mediaStreamOptions?.tool){const e=new L(i.url);await e.options();const t=(await e.describe()).body.toString().trim();this.sdp=Promise.resolve(t),await e.setup(0,"/audio"),await e.setup(2,"/video");const s=await e.play();_=await V(s,t,i.mediaStreamOptions,!0)}else{const e=this.storage.getItem(this.ffmpegInputArgumentsKey)||Z;i.inputArguments.unshift(...e.split(" ")),_=await P(i,C)}}if(_.inputAudioCodec?X.includes(_.inputAudioCodec?.toLowerCase())?this.console.log("Detected audio codec is mp4/mpegts compatible.",_.inputAudioCodec):this.console.log("Detected audio codec is not mp4/mpegts compatible.",_.inputAudioCodec):this.console.log("No audio stream detected."),this.storage.setItem(this.lastDetectedAudioCodecKey,_.inputAudioCodec||"null"),"h264"!==_.inputVideoCodec&&this.console.error("Video codec is not h264. If there are errors, try changing your camera's encoder output."),l)return this.console.warn("Audio probe complete, ending rebroadcast session and restarting with detected codecs."),_.kill(),this.startPrebufferSession();if(this.parserSession=_,H?.refreshAt){let t,i=H;const s=async()=>{if(!_.isActive)return;const t=await this.mixinDevice.getVideoStream(i),s=await U.convertMediaObjectToBuffer(t,e.ScryptedMimeTypes.FFmpegInput),o=JSON.parse(s.toString());i=o.mediaStreamOptions,r(i)},r=e=>{const i=e.refreshAt-Date.now()-3e4;this.console.log("refreshing media stream in",i),t=setTimeout(s,i)};r(i),_.once("killed",(()=>clearTimeout(t)))}_.once("killed",(()=>{this.parserSessionPromise=void 0,this.parserSession===_&&(this.parserSession=void 0)}));for(const e of te){let i=0;_.on(e,(s=>{const r=this.prebuffers[e],o=Date.now();for("mdat"===s.type&&(this.prevIdr&&(this.detectedIdrInterval=o-this.prevIdr),this.prevIdr=o),r.push({time:o,chunk:s});r.length&&r[0].time<o-t;)r.shift(),i++;i>1e3&&(this.prebuffers[e]=r.slice(),i=0)}))}return _}printActiveClients(){this.console.log(this.streamName,"active rebroadcast clients:",this.activeClients)}inactivityCheck(e,t){this.printActiveClients(),this.stopInactive&&(this.activeClients||this.inactivityTimeout&&!t||(clearTimeout(this.inactivityTimeout),this.inactivityTimeout=setTimeout((()=>{this.activeClients||(this.console.log(this.streamName,"terminating rebroadcast due to inactivity"),e.kill())}),3e4)))}async getVideoStream(e){this.ensurePrebufferSession();const t=await this.parserSessionPromise,i="false"!==this.storage.getItem(G),s=e?.prebuffer||(i?1.5*Math.max(4e3,this.detectedIdrInterval||4e3):0);this.console.log(this.streamName,"client request started");const{rtspMode:r,mp4Mode:o}=this.getRebroadcastMode(),n=r?"rtsp":"mpegts",a=this.parsers[e?.container]?e?.container:n,c=Object.assign({},t.mediaStreamOptions);c.prebuffer=s;const{reencodeAudio:d}=this.getAudioConfig();let u=!1;r&&"rtsp"===a?u=!0:this.audioDisabled?c.audio=null:d?c.audio={codec:"aac",encoder:"libfdk_aac",profile:"aac_low"}:u=!0,u&&(c.audio=t?.mediaStreamOptions?.audio?.codec===t?.inputAudioCodec?t?.mediaStreamOptions?.audio:{codec:t?.inputAudioCodec}),c.video&&t.inputVideoResolution?.[2]&&t.inputVideoResolution?.[3]&&Object.assign(c.video,{width:parseInt(t.inputVideoResolution[2]),height:parseInt(t.inputVideoResolution[3])});const p=Date.now();let l=0;const m=this.prebuffers[a];for(const e of m)if(!(e.time<p-s))for(const t of e.chunk.chunks)l+=t.length;const h=Math.max(5e5,l).toString(),f=await(async i=>{const r=this.prebuffers[i];let o,n;if("rtsp"===i){this.sdp.then((e=>console.log(e)));const e=await g();o=e.clientPromise.then((async e=>{let t=await this.sdp;const i=new B(e,t);return await i.handlePlayback(),e})),n=e.url.replace("tcp://","rtsp://")}else{const e=await g();o=e.clientPromise,n=`tcp://127.0.0.1:${e.port}`}return M(o,{console:this.console,connect:(o,n)=>{this.activeClients++,this.printActiveClients();const a=Date.now(),c=e=>{o(e)>1e8&&(this.console.log("more than 100MB has been buffered, did downstream die? killing connection.",this.streamName),d())},d=()=>{n(),this.console.log(this.streamName,"client request ended"),t.removeListener(i,c),t.removeListener("killed",d)};t.on(i,c),t.once("killed",d);for(const e of r)e.time<a-s||c(e.chunk);return()=>{this.activeClients--,this.inactivityCheck(t,!1!==e?.refresh),d()}}}),n})(a),y={url:f,container:a,inputArguments:["-analyzeduration","0","-probesize",h,...this.parsers[a].inputArguments||[],"-f",this.parsers[a].container,"-i",f],mediaStreamOptions:c};return U.createFFmpegMediaObject(y)}}class se extends n{released=!1;sessions=new Map;constructor(e,t,i,s){super(e,i,{providerNativeId:s,mixinDeviceInterfaces:t,group:"Prebuffer Settings",groupKey:"prebuffer"}),this.delayStart()}delayStart(){this.console.log("prebuffer sessions starting in 5 seconds"),setTimeout((()=>this.ensurePrebufferSessions()),5e3)}async getVideoStream(e){await this.ensurePrebufferSessions();const t=e?.id;let i=this.sessions.get(t);return!i||e?.directMediaStream?this.mixinDevice.getVideoStream(e):(i.ensurePrebufferSession(),await i.parserSessionPromise,i=this.sessions.get(t),i?i.getVideoStream(e):this.mixinDevice.getVideoStream(e))}async ensurePrebufferSessions(){const t=await this.mixinDevice.getVideoStreamOptions(),i=this.getEnabledMediaStreamOptions(t),s=i?i.map((e=>e.id)):[void 0],o=t?.map((e=>e.id))||[void 0];if("true"!==this.storage.getItem("warnedCloud")){const e=t?.find((e=>"cloud"===e.source));e&&(this.storage.setItem("warnedCloud","true"),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 n=this.mixinDeviceInterfaces.includes(e.ScryptedInterface.Battery);let a=0;const c=o.length;for(const e of o){let i=this.sessions.get(e);if(!i){const o=t?.find((t=>t.id===e));o?.prebuffer&&F.a(`Prebuffer is already available on ${this.name}. If this is a grouped device, disable the Rebroadcast extension.`);const d=o?.name,u=!s.includes(e);if(i=new ie(this,d,e,n||u),this.sessions.set(e,i),e===t?.[0]?.id&&this.sessions.set(void 0,i),n){this.console.log("camera is battery powered, prebuffering and rebroadcasting will only work on demand.");continue}if(u){this.console.log("stream",d,"will be rebroadcast on demand.");continue}(async()=>{for(;this.sessions.get(e)===i&&!this.released;){i.ensurePrebufferSession();try{const e=await i.parserSessionPromise;a++,this.online=a==c,await(0,r.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)")})()}}K.onMixinEvent(this.id,this.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0)}async getMixinSettings(){const e=[];try{const t=await this.mixinDevice.getVideoStreamOptions(),i=this.getEnabledMediaStreamOptions(t);t?.length>0&&e.push({title:"Prebuffered Streams",description:"The streams to prebuffer. Enable only as necessary to reduce traffic.",key:"enabledStreams",value:i.map((e=>e.name||"")),choices:t.map((e=>e.name)),multiple:!0})}catch(e){throw this.console.error("error in getVideoStreamOptions",e),e}e.push({title:"Prebuffer Duration",description:"Duration of the prebuffer in milliseconds.",type:"number",key:$,value:this.storage.getItem($)||q.toString()},{title:"Start at Previous Keyframe",description:"Start live streams from the previous key frame. Improves startup time.",type:"boolean",key:G,value:("false"!==this.storage.getItem(G)).toString()});for(const t of new Set([...this.sessions.values()]))if(t)try{e.push(...await t.getMixinSettings())}catch(e){throw this.console.error("error in prebuffer session getMixinSettings",e),e}return e}async putMixinSetting(e,t){const i=this.sessions;this.sessions=new Map,"enabledStreams"===e?this.storage.setItem(e,JSON.stringify(t)):this.storage.setItem(e,t.toString());for(const e of i.values())e?.parserSessionPromise?.then((e=>e.kill()));this.ensurePrebufferSessions()}getEnabledMediaStreamOptions(e){if(!e)return;try{const t=JSON.parse(this.storage.getItem("enabledStreams"));return e.filter((e=>t.includes(e.name)))}catch(e){}const t=e.find((e=>"cloud"!==e.source));return t?[t]:[]}async getVideoStreamOptions(){const e=await this.mixinDevice.getVideoStreamOptions()||[];let t=this.getEnabledMediaStreamOptions(e);const i=parseInt(this.storage.getItem($))||q;for(const s of e)(this.sessions.get(s.id)?.parserSession||t.includes(s))&&(s.prebuffer=i);return e}release(){this.console.log("prebuffer session releasing if started"),this.released=!0;for(const e of this.sessions.values())e&&(e.clearPrebuffers(),e.parserSessionPromise?.then((t=>{this.console.log("prebuffer session released"),t.kill(),e.clearPrebuffers()})))}}const re=new class extends k{constructor(t){super(t),this.fromMimeType="x-scrypted/x-rfc4571",this.toMimeType=e.ScryptedMimeTypes.FFmpegInput;for(const e of Object.keys(j.getSystemState())){const t=j.getDeviceById(e);t.mixins?.includes(this.id)&&t.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((()=>K.requestRestart()),i)}async convert(e,t,i){const s=JSON.parse(e.toString()),{url:r,sdp:o}=s,n=o,a=new Set,d=new Set,u=(e,t)=>{for(const i of t||[])e.add(parseInt(i))},p=n.match(/m=audio.*/)?.[0];u(a,p?.split(" ").slice(3));const l=o.match(/m=video.*/)?.[0];u(d,l?.split(" ").slice(3));const m=new URL(r);if(!m.protocol.startsWith("tcp"))throw new Error("rfc4751 url must be tcp");const{clientPromise:h,url:f}=await g(),y={url:f,inputArguments:["-rtsp_transport","tcp","-max_delay","1000000","-i",f.replace("tcp","rtsp")]};return h.then((async e=>{const t=new B(e,o);await t.handlePlayback();const i=c().connect(parseInt(m.port),m.hostname);for(e.on("close",(()=>{i.destroy()})),i.on("close",(()=>{e.destroy()}));;){const s=(await I(i,2)).readInt16BE(0),r=await I(i,s),o=127&r[1];if(a.has(o))t.sendAudio(r,!1);else{if(!d.has(o))throw e.destroy(),i.destroy(),new Error("unknown payload type "+o);t.sendVideo(r,!1)}}})),Buffer.from(JSON.stringify(y))}async canMixin(t,i){return i.includes(e.ScryptedInterface.VideoCamera)?[e.ScryptedInterface.VideoCamera,e.ScryptedInterface.Settings,e.ScryptedInterface.Online]:null}async getMixin(e,t,i){return this.setHasEnabledMixin(i.id),new se(e,t,i,this.nativeId)}async releaseMixin(e,t){t.online=!0,t.release()}}})();var r=exports="undefined"==typeof exports?{}:exports;for(var o in s)r[o]=s[o];s.__esModule&&Object.defineProperty(r,"__esModule",{value:!0})})();
|
|
1
|
+
(()=>{var e={510:function(e,t,i){"use strict";var s=this&&this.__createBinding||(Object.create?function(e,t,i,s){void 0===s&&(s=i),Object.defineProperty(e,s,{enumerable:!0,get:function(){return t[i]}})}:function(e,t,i,s){void 0===s&&(s=i),e[s]=t[i]}),r=this&&this.__exportStar||function(e,t){for(var i in e)"default"===i||Object.prototype.hasOwnProperty.call(t,i)||s(t,e,i)};Object.defineProperty(t,"__esModule",{value:!0}),t.MixinDeviceBase=t.ScryptedDeviceBase=void 0,r(i(268),t);const o=i(268);class n extends o.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=n;class a extends o.DeviceBase{constructor(e,t,i,s,r){super(),this.mixinDevice=e,this.mixinDeviceInterfaces=t,this.mixinProviderNativeId=s,this._mixinStorageSuffix=r,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=a,function(){function e(e){return function(){return this._lazyLoadDeviceState(),this._deviceState[e]}}function t(e){return function(t){this._lazyLoadDeviceState(),this._deviceState[e]=t}}for(var i of Object.values(o.ScryptedInterfaceProperty))Object.defineProperty(n.prototype,i,{set:t(i),get:e(i)}),Object.defineProperty(a.prototype,i,{set:t(i),get:e(i)})}();let c={};try{c=Object.assign(c,{log:deviceManager.getDeviceLogger(void 0),deviceManager,endpointManager,mediaManager,systemManager,pluginHostAPI})}catch(e){console.error("sdk initialization error, import @scrypted/types or use @scrypted/web-sdk instead",e)}t.default=c},268:(e,t,i)=>{"use strict";i.r(t),i.d(t,{DeviceBase:()=>s,ScryptedInterfaceProperty:()=>r,ScryptedInterfaceDescriptors:()=>o,ScryptedDeviceType:()=>n,HumidityMode:()=>a,FanMode:()=>c,TemperatureUnit:()=>d,ThermostatMode:()=>u,LockState:()=>p,MediaPlayerState:()=>l,ScryptedInterface:()=>m,ScryptedMimeTypes:()=>h});class s{}let r;!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"}(r||(r={}));const o={ScryptedDevice:{name:"ScryptedDevice",methods:["listen","probe","setName","setRoom","setType"],properties:["id","info","interfaces","mixins","name","providedInterfaces","providedName","providedRoom","providedType","providerId","room","type"]},ScryptedPlugin:{name:"ScryptedPlugin",methods:["getPluginJson"],properties:[]},OnOff:{name:"OnOff",methods:["turnOff","turnOn"],properties:["on"]},Brightness:{name:"Brightness",methods:["setBrightness"],properties:["brightness"]},ColorSettingTemperature:{name:"ColorSettingTemperature",methods:["getTemperatureMaxK","getTemperatureMinK","setColorTemperature"],properties:["colorTemperature"]},ColorSettingRgb:{name:"ColorSettingRgb",methods:["setRgb"],properties:["rgb"]},ColorSettingHsv:{name:"ColorSettingHsv",methods:["setHsv"],properties:["hsv"]},Notifier:{name:"Notifier",methods:["sendNotification"],properties:[]},StartStop:{name:"StartStop",methods:["start","stop"],properties:["running"]},Pause:{name:"Pause",methods:["pause","resume"],properties:["paused"]},Dock:{name:"Dock",methods:["dock"],properties:["docked"]},TemperatureSetting:{name:"TemperatureSetting",methods:["setThermostatMode","setThermostatSetpoint","setThermostatSetpointHigh","setThermostatSetpointLow"],properties:["thermostatActiveMode","thermostatAvailableModes","thermostatMode","thermostatSetpoint","thermostatSetpointHigh","thermostatSetpointLow"]},Thermometer:{name:"Thermometer",methods:["setTemperatureUnit"],properties:["temperature","temperatureUnit"]},HumiditySensor:{name:"HumiditySensor",methods:[],properties:["humidity"]},Camera:{name:"Camera",methods:["getPictureOptions","takePicture"],properties:[]},VideoCamera:{name:"VideoCamera",methods:["getVideoStream","getVideoStreamOptions"],properties:[]},VideoCameraConfiguration:{name:"VideoCameraConfiguration",methods:["setVideoStreamOptions"],properties:[]},Intercom:{name:"Intercom",methods:["startIntercom","stopIntercom"],properties:[]},Lock:{name:"Lock",methods:["lock","unlock"],properties:["lockState"]},PasswordStore:{name:"PasswordStore",methods:["addPassword","getPasswords","removePassword"],properties:[]},Authenticator:{name:"Authenticator",methods:["checkPassword"],properties:[]},Scene:{name:"Scene",methods:["activate","deactivate","isReversible"],properties:[]},Entry:{name:"Entry",methods:["closeEntry","openEntry"],properties:[]},EntrySensor:{name:"EntrySensor",methods:[],properties:["entryOpen"]},DeviceProvider:{name:"DeviceProvider",methods:["getDevice"],properties:[]},DeviceDiscovery:{name:"DeviceDiscovery",methods:["discoverDevices"],properties:[]},DeviceCreator:{name:"DeviceCreator",methods:["createDevice","getCreateDeviceSettings"],properties:[]},Battery:{name:"Battery",methods:[],properties:["batteryLevel"]},Refresh:{name:"Refresh",methods:["getRefreshFrequency","refresh"],properties:[]},MediaPlayer:{name:"MediaPlayer",methods:["getMediaStatus","load","seek","skipNext","skipPrevious"],properties:[]},Online:{name:"Online",methods:[],properties:["online"]},SoftwareUpdate:{name:"SoftwareUpdate",methods:["checkForUpdate","installUpdate"],properties:["updateAvailable"]},BufferConverter:{name:"BufferConverter",methods:["convert"],properties:["fromMimeType","toMimeType"]},Settings:{name:"Settings",methods:["getSettings","putSetting"],properties:[]},BinarySensor:{name:"BinarySensor",methods:[],properties:["binaryState"]},IntrusionSensor:{name:"IntrusionSensor",methods:[],properties:["intrusionDetected"]},PowerSensor:{name:"PowerSensor",methods:[],properties:["powerDetected"]},AudioSensor:{name:"AudioSensor",methods:[],properties:["audioDetected"]},MotionSensor:{name:"MotionSensor",methods:[],properties:["motionDetected"]},AmbientLightSensor:{name:"AmbientLightSensor",methods:[],properties:["ambientLight"]},OccupancySensor:{name:"OccupancySensor",methods:[],properties:["occupied"]},FloodSensor:{name:"FloodSensor",methods:[],properties:["flooded"]},UltravioletSensor:{name:"UltravioletSensor",methods:[],properties:["ultraviolet"]},LuminanceSensor:{name:"LuminanceSensor",methods:[],properties:["luminance"]},PositionSensor:{name:"PositionSensor",methods:[],properties:["position"]},Readme:{name:"Readme",methods:["getReadmeMarkdown"],properties:[]},OauthClient:{name:"OauthClient",methods:["getOauthUrl","onOauthCallback"],properties:[]},MixinProvider:{name:"MixinProvider",methods:["canMixin","getMixin","releaseMixin"],properties:[]},HttpRequestHandler:{name:"HttpRequestHandler",methods:["onRequest"],properties:[]},EngineIOHandler:{name:"EngineIOHandler",methods:["onConnection"],properties:[]},PushHandler:{name:"PushHandler",methods:["onPush"],properties:[]},Program:{name:"Program",methods:["run"],properties:[]},Scriptable:{name:"Scriptable",methods:["eval","loadScripts","saveScript"],properties:[]},ObjectDetector:{name:"ObjectDetector",methods:["getDetectionInput","getObjectTypes"],properties:[]},ObjectDetection:{name:"ObjectDetection",methods:["detectObjects","getDetectionModel"],properties:[]},HumiditySetting:{name:"HumiditySetting",methods:["setHumidity"],properties:["humiditySetting"]},Fan:{name:"Fan",methods:["setFan"],properties:["fan"]},RTCSignalingChannel:{name:"RTCSignalingChannel",methods:["startRTCSignalingSession"],properties:[]},RTCSignalingClient:{name:"RTCSignalingClient",methods:["createRTCSignalingSession"],properties:[]}};let n,a,c,d,u,p,l,m,h;!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"}(n||(n={})),function(e){e.Humidify="Humidify",e.Dehumidify="Dehumidify",e.Auto="Auto",e.Off="Off"}(a||(a={})),function(e){e.Auto="Auto",e.Manual="Manual"}(c||(c={})),function(e){e.C="C",e.F="F"}(d||(d={})),function(e){e.Off="Off",e.Cool="Cool",e.Heat="Heat",e.HeatCool="HeatCool",e.Auto="Auto",e.FanOnly="FanOnly",e.Purifier="Purifier",e.Eco="Eco",e.Dry="Dry",e.On="On"}(u||(u={})),function(e){e.Locked="Locked",e.Unlocked="Unlocked",e.Jammed="Jammed"}(p||(p={})),function(e){e.Idle="Idle",e.Playing="Playing",e.Paused="Paused",e.Buffering="Buffering"}(l||(l={})),function(e){e.ScryptedDevice="ScryptedDevice",e.ScryptedPlugin="ScryptedPlugin",e.OnOff="OnOff",e.Brightness="Brightness",e.ColorSettingTemperature="ColorSettingTemperature",e.ColorSettingRgb="ColorSettingRgb",e.ColorSettingHsv="ColorSettingHsv",e.Notifier="Notifier",e.StartStop="StartStop",e.Pause="Pause",e.Dock="Dock",e.TemperatureSetting="TemperatureSetting",e.Thermometer="Thermometer",e.HumiditySensor="HumiditySensor",e.Camera="Camera",e.VideoCamera="VideoCamera",e.VideoCameraConfiguration="VideoCameraConfiguration",e.Intercom="Intercom",e.Lock="Lock",e.PasswordStore="PasswordStore",e.Authenticator="Authenticator",e.Scene="Scene",e.Entry="Entry",e.EntrySensor="EntrySensor",e.DeviceProvider="DeviceProvider",e.DeviceDiscovery="DeviceDiscovery",e.DeviceCreator="DeviceCreator",e.Battery="Battery",e.Refresh="Refresh",e.MediaPlayer="MediaPlayer",e.Online="Online",e.SoftwareUpdate="SoftwareUpdate",e.BufferConverter="BufferConverter",e.Settings="Settings",e.BinarySensor="BinarySensor",e.IntrusionSensor="IntrusionSensor",e.PowerSensor="PowerSensor",e.AudioSensor="AudioSensor",e.MotionSensor="MotionSensor",e.AmbientLightSensor="AmbientLightSensor",e.OccupancySensor="OccupancySensor",e.FloodSensor="FloodSensor",e.UltravioletSensor="UltravioletSensor",e.LuminanceSensor="LuminanceSensor",e.PositionSensor="PositionSensor",e.Readme="Readme",e.OauthClient="OauthClient",e.MixinProvider="MixinProvider",e.HttpRequestHandler="HttpRequestHandler",e.EngineIOHandler="EngineIOHandler",e.PushHandler="PushHandler",e.Program="Program",e.Scriptable="Scriptable",e.ObjectDetector="ObjectDetector",e.ObjectDetection="ObjectDetection",e.HumiditySetting="HumiditySetting",e.Fan="Fan",e.RTCSignalingChannel="RTCSignalingChannel",e.RTCSignalingClient="RTCSignalingClient"}(m||(m={})),function(e){e.AcceptUrlParameter="accept-url",e.Url="text/x-uri",e.InsecureLocalUrl="text/x-insecure-local-uri",e.LocalUrl="text/x-local-uri",e.PushEndpoint="text/x-push-endpoint",e.MediaStreamUrl="text/x-media-url",e.FFmpegInput="x-scrypted/x-ffmpeg-input",e.RTCAVSignalingPrefix="x-scrypted-rtc-signaling-",e.SchemePrefix="x-scrypted/x-scrypted-scheme-",e.MediaObject="x-scrypted/x-scrypted-media-object"}(h||(h={}))}},t={};function i(s){var r=t[s];if(void 0!==r)return r.exports;var o=t[s]={exports:{}};return e[s].call(o.exports,o,o.exports,i),o.exports}i.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var s in t)i.o(t,s)&&!i.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),i.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var s={};(()=>{"use strict";i.r(s),i.d(s,{default:()=>oe});var e=i(510),t=i.n(e);const r=require("events"),{deviceManager:o}=t();class n extends e.MixinDeviceBase{constructor(t,i,s){super(t,s.mixinDeviceInterfaces,i,s.providerNativeId,s.mixinStorageSuffix),this.settingsGroup=s.group,this.settingsGroupKey=s.groupKey,process.nextTick((()=>o.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)))}async getSettings(){const t=this.mixinDeviceInterfaces.includes(e.ScryptedInterface.Settings)?this.mixinDevice.getSettings():void 0,i=this.getMixinSettings(),s=[];try{const e=await t||[];s.push(...e)}catch(e){const t=this.name,i=`${t} Extension settings failed to load.`;this.console.error(i,e),s.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:i,readonly:!0})}try{const e=await i||[];for(const t of e)t.group=t.group||this.settingsGroup,t.key=this.settingsGroupKey+":"+t.key;s.push(...e)}catch(e){const t=o.getDeviceState(this.mixinProviderNativeId).name,i=`${t} Extension settings failed to load.`;this.console.error(i,e),s.push({key:Math.random().toString(),title:t,value:"Settings Error",group:"Errors",description:i,readonly:!0})}return s}async putSetting(t,i){const s=this.settingsGroupKey+":";if(!t?.startsWith(s))return this.mixinDevice.putSetting(t,i);await this.putMixinSetting(t.substring(s.length),i),o.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)}async release(){await o.onMixinEvent(this.id,this,e.ScryptedInterface.Settings,null)}}const a=require("net");var c=i.n(a);const d=require("child_process");var u=i.n(d);const p=require("dgram");var l=i.n(p);async function m(e,t){e.bind(t),await(0,r.once)(e,"listening");const i=e.address().port;return{port:i,url:`udp://127.0.0.1:${i}`}}async function h(e){return m(e,0)}async function f(e,t){return e.bind(t),await(0,r.once)(e,"listening"),{port:t,url:`udp://127.0.0.1:${t}`}}async function g(){const e=new(c().Server),t=await async function(e){return e.listen(0),await(0,r.once)(e,"listening"),e.address().port}(e);return{url:`tcp://127.0.0.1:${t}`,port:t,clientPromise:new Promise(((t,i)=>{const s=setTimeout((()=>{i(new Error("timeout waiting for client"))}),3e4);e.on("connection",(i=>{e.close(),clearTimeout(s),t(i)}))}))}}const y=require("process");var S=i.n(y);const v=["decode_slice_header error","no frame!","non-existing PPS"];const{mediaManager:b}=t();async function w(e,t){return new Promise((i=>{const s=r=>{const o=r.toString(),n=o.indexOf(`${t}: `);if(-1!==n){const r=o.substring(n+t.length+1).trim();let a=r.indexOf(" ");const c=r.indexOf(",");-1!==a&&c<a&&(a=c),-1!==a&&(e.stdout.removeListener("data",s),e.stderr.removeListener("data",s),i(r.substring(0,a)))}};e.stdout.on("data",s),e.stderr.on("data",s)}))}async function P(e,t){const{console:i}=t;let s,o=!0;const n=new r.EventEmitter;let a,c,d,p,m;n.on("error",(e=>i.error("rebroadcast error",e)));const y=new Promise(((e,t)=>{p=e,m=t}));function P(){o&&(n.emit("killed"),n.emit("error",new Error("killed"))),o=!1,O?.kill(),setTimeout((()=>O?.kill("SIGKILL")),1e3),m?.(new Error("ffmpeg was killed before connecting to the rebroadcast session")),clearTimeout(s)}const M=e.inputArguments.slice();s=setTimeout(P,3e4);const I=e=>{let s;function r(){i.error("timeout waiting for data, killing parser session",e),P()}function o(){t.timeout&&(clearTimeout(s),s=setTimeout(r,t.timeout))}return n.once("killed",(()=>clearTimeout(s))),o(),{resetActivityTimer:o}};let C=!1;const x=["pipe","pipe","pipe"];let D=3;for(const e of Object.keys(t.parsers)){const s=t.parsers[e];if(s.parseDatagram){C=!0;const t=l().createSocket("udp4"),i=await h(t),r=l().createSocket("udp4");await f(r,i.port+1),n.once("killed",(()=>{t.close(),r.close()})),M.push(...s.outputArguments,i.url.replace("udp://","rtp://"));const{resetActivityTimer:o}=I(e);(async()=>{for await(const i of s.parseDatagram(t,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),n.emit(e,i),o()})(),(async()=>{for await(const t of s.parseDatagram(r,parseInt(d?.[2]),parseInt(d?.[3]),"rtcp"))p?.(void 0),n.emit(e,t),o()})()}else if(s.tcpProtocol){const t=await g(),r=new URL(s.tcpProtocol);r.port=t.port.toString(),M.push(...s.outputArguments,r.toString());const{resetActivityTimer:o}=I(e);(async()=>{const r=await t.clientPromise;try{for await(const t of s.parse(r,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),n.emit(e,t),o()}catch(e){i.error("rebroadcast parse error",e),P()}})()}else M.push(...s.outputArguments,"pipe:"+D++),x.push("pipe")}C&&(M.push("-sdp_file","pipe:"+D++),x.push("pipe")),M.unshift("-hide_banner"),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(" "))}(i,M);const O=u().spawn(await b.getFFmpegPath(),M,{stdio:x});let T;!function(e,t,i,s){const r=!!S().env.SCRYPTED_FFMPEG_NOISY||!!s?.getItem("SCRYPTED_FFMPEG_NOISY");function o(e){const s=o=>{const n=o.toString();for(const e of v)if(-1!==n.indexOf(e))return;if(!(r||i||-1===n.indexOf("frame=")&&-1===n.indexOf("size=")))return e(n),e("video/audio detected, discarding further input"),t.stdout.removeListener("data",s),void t.stderr.removeListener("data",s);e(n)};return s}t.stdout?.on("data",o(e.log)),t.stderr?.on("data",o(e.error)),t.on("exit",(()=>e.log("ffmpeg exited")))}(i,O,void 0,t?.storage),O.on("exit",P),T=C?new Promise((e=>{const t=[];O.stdio[D-1].on("data",(i=>{t.push(i),e(t)}))})):Promise.resolve([]);let A=0;return Object.keys(t.parsers).forEach((async e=>{const s=t.parsers[e];if(!s.parse||s.tcpProtocol)return;const r=O.stdio[3+A];A++;try{const{resetActivityTimer:t}=I(e);for await(const i of s.parse(r,parseInt(d?.[2]),parseInt(d?.[3])))p?.(void 0),n.emit(e,i),t()}catch(e){i.error("rebroadcast parse error",e),P()}})),async function(e){return w(e,"Audio")}(O).then((e=>a=e)),async function(e){return w(e,"Video")}(O).then((e=>c=e)),async function(e){return new Promise((t=>{const i=s=>{const r=s.toString(),o=/(([0-9]{2,5})x([0-9]{2,5}))/.exec(r);o&&(e.stdout.removeListener("data",i),e.stderr.removeListener("data",i),t(o))};e.stdout.on("data",i),e.stderr.on("data",i)}))}(O).then((e=>d=e)),await y,p=void 0,m=void 0,clearTimeout(s),{sdp:T,inputAudioCodec:a,inputVideoCodec:c,inputVideoResolution:d,isActive:()=>o,kill:P,mediaStreamOptions:e.mediaStreamOptions||{id:void 0,name:void 0},on(e,t){return n.on(e,t),this},once(e,t){return n.once(e,t),this},removeListener(e,t){return n.removeListener(e,t),this}}}async function M(e,t){const i=await e;let s=!0;const r=()=>{i.removeAllListeners(),i.destroy();const e=o;o=void 0,e?.()};let o=t?.connect((e=>{s&&(s=!1,e.startStream&&i.write(e.startStream));for(const t of e.chunks)i.write(t);return i.writableLength}),r);i.once("close",(()=>{r()})),i.on("error",(e=>t?.console?.log("client stream ended")))}async function I(e,t){if(!t)return Buffer.alloc(0);{const i=e.read(t);if(i)return i}return new Promise(((i,s)=>{const r=()=>{const s=e.read(t);s&&(n(),i(s))},o=()=>{n(),s(new Error(`stream ended during read for minimum ${t} bytes`))},n=()=>{e.removeListener("readable",r),e.removeListener("end",o)};e.on("readable",r),e.on("end",o)}))}const C="\n".charCodeAt(0);async function x(e){return async function(e,t){const i=[];let s=0;for(;;){const r=await I(e,1);if(!r)throw new Error("end of stream");if(r[0]===t)break;i[s++]=r[0]}return Buffer.from(i).toString()}(e,C)}function D(e){return e}function O(e){return{container:"mp4",outputArguments:[...e?.vcodec||[],...e?.acodec||[],"-movflags","frag_keyframe+empty_moov+default_base_moof","-f","mp4"],async*parse(e){const t=async function*(e){for(;;){const t=await I(e,8),i=t.readInt32BE(0)-8,s=t.slice(4).toString(),r=await I(e,i);yield{header:t,length:i,type:s,data:r}}}(e);let i,s,r;for await(const e of t)i?s||(s=e):i=e,yield{startStream:r,chunks:[e.header,e.data],type:e.type},i&&s&&!r&&(r=Buffer.concat([i.header,i.data,s.header,s.data]))},findSyncFrame:D}}const{systemManager:T}=t(),A="v4";class k extends e.ScryptedDeviceBase{hasEnabledMixin={};constructor(t){super(t);try{this.hasEnabledMixin=JSON.parse(this.storage.getItem("hasEnabledMixin"))}catch(e){this.hasEnabledMixin={}}this.pluginsComponent=T.getComponent("plugins"),T.listen((async(t,i,s)=>{i.eventInterface!==e.ScryptedInterface.ScryptedDevice||i.property||this.maybeEnableMixin(t)}));for(const e of Object.keys(T.getSystemState())){const t=T.getDeviceById(e);this.maybeEnableMixin(t)}}async shouldEnableMixin(e){return!0}async maybeEnableMixin(e){if(!e||e.mixins?.includes(this.id))return;if(this.hasEnabledMixin[e.id]===A)return;if(!await this.canMixin(e.type,e.interfaces))return;if(!await this.shouldEnableMixin(e))return;this.log.i("auto enabling mixin for "+e.name);const t=(e.mixins||[]).slice();t.push(this.id);const i=await this.pluginsComponent;await i.setMixins(e.id,t),this.setHasEnabledMixin(e.id)}setHasEnabledMixin(e){this.hasEnabledMixin[e]!==A&&(this.hasEnabledMixin[e]=A,this.storage.setItem("hasEnabledMixin",JSON.stringify(this.hasEnabledMixin)))}}function E(e){if(!e)return;return e.split("\n").map((e=>e.trim())).find((e=>e.startsWith("a=control:")))?.split("a=control:")?.[1]}const R=require("crypto");function L(e){return e}function _(e){const t={};for(const i of e.slice(1)){const e=i.indexOf(":");let s="";-1!==e&&(s=i.substring(e+1).trim());t[i.substring(0,e).toLowerCase()]=s}return t}class B extends class{write(e,t,i){let s=`${e}\r\n`;i&&(t["Content-Length"]=i.length.toString());for(const[e,i]of Object.entries(t))s+=`${e}: ${i}\r\n`;s+="\r\n",this.client.write(s),i&&this.client.write(i)}async readMessage(){let e=[];for(;;){let t=await x(this.client);if(t=t.trim(),!t)return e;e.push(t)}}}{cseq=0;constructor(e){super(),this.url=e;const t=new URL(e);this.client=c().connect(parseInt(t.port)||554,t.hostname)}async request(e,t,i,s){let r;t=t||{},r=i?new URL(i,this.url).toString():this.url;const o=`${e} ${r} RTSP/1.0`;t.CSeq=(this.cseq++).toString(),this.write(o,t,s);const n=_(await this.readMessage()),a=parseInt(n["content-length"]);return a?{headers:n,body:await I(this.client,a)}:{headers:n,body:void 0}}async options(){return this.request("OPTIONS")}async describe(){return this.request("DESCRIBE",{Accept:"application/sdp"})}async setup(e,t){const i={Transport:`RTP/AVP/TCP;unicast;interleaved=${e}-${e+1}`};this.session&&(i.Session=this.session);const s=await this.request("SETUP",i,t);return s.headers.session&&(this.session=s.headers.session),s}async play(){const e={Range:"npt=0.000-"};return this.session&&(e.Session=this.session),await this.request("PLAY",e),this.client}async teardown(){try{return await this.request("TEARDOWN")}finally{this.client.destroy()}}}class N{videoChannel=0;audioChannel=2;udpPorts={video:0,audio:0};constructor(e,t,i){this.client=e,this.sdp=t,this.udp=i,this.session=(0,R.randomBytes)(4).toString("hex"),t&&(t=t.trim())}async handleSetup(){let e=[];for(;;){let t=await x(this.client);if(t=t.trim(),t)e.push(t);else{if(!await this.headers(e))break;e=[]}}}async handlePlayback(){return this.handleSetup()}async handleTeardown(){return this.handleSetup()}async*handleRecord(){for(;;){const e=await I(this.client,4),t=e.readUInt16BE(2),i=await I(this.client,t),s=e.readUInt8(1);yield{type:s-s%2===this.videoChannel?"video":"audio",rtcp:s%2==1,header:e,packet:i}}}send(e,t){const i=Buffer.alloc(4);i.writeUInt8(36,0),i.writeUInt8(t,1),i.writeUInt16BE(e.length,2),this.client.write(i),this.client.write(Buffer.from(e))}sendUdp(e,t,i){this.udp.send(t,i?e+1:e,"127.0.0.1")}sendVideo(e,t){this.udp&&this.udpPorts.video?this.sendUdp(this.udpPorts.video,e,t):this.send(e,t?this.videoChannel+1:this.videoChannel)}sendAudio(e,t){this.udp&&this.udpPorts.audio?this.sendUdp(this.udpPorts.audio,e,t):this.send(e,t?this.audioChannel+1:this.audioChannel)}options(e,t){const i={Public:"DESCRIBE, OPTIONS, PAUSE, PLAY, SETUP, TEARDOWN, ANNOUNCE, RECORD"};this.respond(200,"OK",t,i)}describe(e,t){const i={};i["Content-Base"]=e,i["Content-Type"]="application/sdp",this.respond(200,"OK",t,i,Buffer.from(this.sdp))}setup(e,t){const i={},s=t.transport;if(i.Transport=t.transport,i.Session=this.session,s.includes("UDP")){const t=s.match(/.*?client_port=([0-9]+)-([0-9]+)/),[i,r,o]=t;e.includes("audio")?this.udpPorts.audio=parseInt(r):this.udpPorts.video=parseInt(r)}else if(s.includes("TCP")){const t=s.match(/.*?interleaved=([0-9]+)-([0-9]+)/);if(t){const i=parseInt(t[1]);parseInt(t[2]);e.includes("audio")?this.audioChannel=i:this.videoChannel=i}}this.respond(200,"OK",t,i)}play(e,t){const i={};i["RTP-Info"]=`url=${e}/trackID=0;seq=0;rtptime=0,url=${e}/trackID=1;seq=0;rtptime=0`,i.Range="npt=now-",i.Session=this.session,this.respond(200,"OK",t,i)}async announce(e,t){const i=parseInt(t["content-length"]),s=await I(this.client,i);this.sdp=s.toString();const r={};r.Session=this.session,this.respond(200,"OK",t,r)}async record(e,t){const i={};i.Session=this.session,this.respond(200,"OK",t,i)}async teardown(e,t){const i={};i.Session=this.session,this.respond(200,"OK",t,i)}async headers(e){this.console?.log("request headers",e.join("\n"));let[t,i]=e[0].split(" ",2);t=t.toLowerCase();const s=_(e);if(this[t])return await this[t](i,s),"play"!==t&&"record"!==t&&"teardown"!==t;this.respond(400,"Bad Request",s,{})}respond(e,t,i,s,r){let o=`RTSP/1.0 ${e} ${t}\r\n`;i.cseq&&(s.CSeq=i.cseq),r&&(s["Content-Length"]=r.length.toString());for(const[e,t]of Object.entries(s))o+=`${e}: ${t}\r\n`;this.console?.log("response headers",o),o+="\r\n",this.client.write(o),r&&this.client.write(r)}}const H=require("stream"),{mediaManager:V}=t();async function U(e,t,i,s,r){let o=!0;const n=new H.EventEmitter,a=parseInt(t.match(/m=audio.* ([0-9]+)/)?.[1]),c=parseInt(t.match(/m=video.* ([0-9]+)/)?.[1]),d=()=>{o&&(n.emit("killed"),n.emit("error",new Error("killed"))),o=!1,e.destroy()};e.on("close",d),e.on("error",d);return(async()=>{const{resetActivityTimer:t}=(e=>{let t;function i(){console.error("timeout waiting for data, killing parser session",e),d()}function s(){r.timeout&&(clearTimeout(t),t=setTimeout(i,r.timeout))}return n.once("killed",(()=>clearTimeout(t))),s(),{resetActivityTimer:s}})("rtsp");for(;;){let i,r;s?(i=await I(e,4),r=i.readUInt16BE(2)):(i=await I(e,2),r=i.readUInt16BE(0));const o=await I(e,r);if(!s){const e=127&o[1],t=Buffer.alloc(2);t[0]=36,e===a?t[1]=0:e===c&&(t[1]=2),i=Buffer.concat([t,i])}const d={chunks:[i,o]};n.emit("rtsp",d),t()}})().finally(d),{sdp:Promise.resolve([Buffer.from(t)]),inputAudioCodec:i.audio.codec,inputVideoCodec:i.video.codec,inputVideoResolution:void 0,isActive:()=>o,kill:d,mediaStreamOptions:i,on(e,t){return n.on(e,t),this},once(e,t){return n.once(e,t),this},removeListener(e,t){return n.removeListener(e,t),this}}}const{mediaManager:F,log:j,systemManager:K,deviceManager:q}=t(),$=1e4,G="prebufferDuration",J="sendKeyframe",W="Default",z="AAC or No Audio",Y=`${z} (Copy)`,Q="Compatible Audio",X="Other Audio",Z=["aac","mp3","mp2","opus"],ee="-fflags +genpts",te=[z,Q,X],ie=["mpegts","mp4","rtsp"];class se{prebuffers={mp4:[],mpegts:[],rtsp:[]};detectedIdrInterval=0;prevIdr=0;audioDisabled=!1;activeClients=0;constructor(e,t,i,s){this.mixin=e,this.streamName=t,this.streamId=i,this.stopInactive=s,this.storage=e.storage,this.console=e.console,this.mixinDevice=e.mixinDevice,this.audioConfigurationKey="audioConfiguration-"+this.streamId,this.ffmpegInputArgumentsKey="ffmpegInputArguments-"+this.streamId,this.rebroadcastModeKey="rebroadcastMode-"+this.streamId,this.lastDetectedAudioCodecKey="lastDetectedAudioCodec-"+this.streamId}clearPrebuffers(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtsp=[]}ensurePrebufferSession(){this.parserSessionPromise||this.mixin.released||(this.console.log(this.streamName,"prebuffer session started"),this.parserSessionPromise=this.startPrebufferSession(),this.parserSessionPromise.catch((()=>this.parserSessionPromise=void 0)))}getAudioConfig(){let e=this.storage.getItem(this.audioConfigurationKey)||"";te.find((t=>e.startsWith(t)))||(e="");const t=-1!==e.indexOf(z),i=-1!==e.indexOf(Q),s=-1!==e.indexOf(X);return{isUsingDefaultAudioConfig:!(t||i||s),aacAudio:t,compatibleAudio:i,reencodeAudio:s}}async getMixinSettings(){const e=[],t=this.parserSession;let i=0,s=0;const{mp4Mode:r}=this.getRebroadcastMode();for(const e of r?this.prebuffers.mp4:this.prebuffers.rtsp){s=s||e.time;for(const t of e.chunk.chunks)i+=t.byteLength}const o=Date.now()-s,n=Math.round(i/o*8),a=this.streamName?`Rebroadcast: ${this.streamName}`:"Rebroadcast";return e.push({title:"Audio Codec Transcoding",group:a,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)||W,choices:[W,Y,"Compatible Audio (Copy)","Other Audio (Transcode)"]},{title:"FFmpeg Input Arguments Prefix",group:a,description:"Optional/Advanced: Additional input arguments to pass to the ffmpeg command. These will be placed before the input arguments.",key:this.ffmpegInputArgumentsKey,value:this.storage.getItem(this.ffmpegInputArgumentsKey),placeholder:ee,choices:[ee,"-use_wallclock_as_timestamps 1","-v verbose"],combobox:!0},{title:"Rebroadcast Mode",group:a,description:"THIS FEATURE IS IN TESTING. DO NOT CHANGE THIS FROM MPEG-TS. The stream format to use when rebroadcasting.",placeholder:"MPEG-TS",choices:["MPEG-TS","RTSP","RTSP+MP4"],key:this.rebroadcastModeKey,value:this.storage.getItem(this.rebroadcastModeKey)||"MPEG-TS"}),t?e.push({key:"detectedResolution",group:a,title:"Detected Resolution and Bitrate",readonly:!0,value:`${t?.inputVideoResolution?.[0]||"unknown"} @ ${n||"unknown"} Kb/s`,description:"Configuring your camera to 1920x1080, 2000Kb/S, Variable Bit Rate, is recommended."},{key:"detectedCodec",group:a,title:"Detected Video/Audio Codecs",readonly:!0,value:(t?.inputVideoCodec?.toString()||"unknown")+"/"+(t?.inputAudioCodec?.toString()||"unknown"),description:"Configuring your camera to H264 video and AAC/MP3/MP2/Opus audio is recommended."},{key:"detectedKeyframe",group:a,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:a,key:"status",description:"Rebroadcast is currently idle and will be started automatically on demand.",value:"Idle",readonly:!0}),e}getRebroadcastMode(){const e=this.storage.getItem(this.rebroadcastModeKey),t=e?.startsWith("RTSP");return{rtspMode:e?.startsWith("RTSP"),mp4Mode:!t||e?.includes("MP4")}}async startPrebufferSession(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[],this.prebuffers.rtsp=[];const t=parseInt(this.storage.getItem(G))||$;let i;try{i=(await this.mixinDevice.getVideoStreamOptions()).find((e=>e.id===this.streamId))}catch(e){}const s=null===i?.audio,o=i?.audio?.codec,{isUsingDefaultAudioConfig:n,aacAudio:a,compatibleAudio:d,reencodeAudio:u}=this.getAudioConfig();let p=this.storage.getItem(this.lastDetectedAudioCodecKey)||void 0;"null"===p&&(p=null);let l=!1;s||o||!n||void 0!==p||(this.console.warn("Camera did not report an audio codec, muting the audio stream and probing the codec."),l=!0),!s&&o&&void 0!==p&&p!==o&&this.console.warn("Audio codec plugin reported vs detected mismatch",o,p);const m=void 0===p?o?.toLowerCase():p?.toLowerCase(),{rtspMode:h,mp4Mode:f}=this.getRebroadcastMode(),g=!h||f,y=!Z.includes(m);!g||l||!1===i?.userConfigurable||s||y&&(n&&j.a(`${this.mixin.name} is using the ${m} 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:",m));const S=["-bsf:a","aac_adtstoasc"],v=[];let b;this.audioDisabled=!1;const w=null===p;let M=!1;if(!l&&n&&y&&(!1===i?.userConfigurable?this.console.log("camera reports it is not user configurable. transcoding due to incompatible codec",m):this.console.log("camera audio transcoding due to incompatible codec. configure the camera to use a compatible codec if possible."),M=!0),s||l)b=["-an"],this.audioDisabled=!0;else if(u||M)b=["-bsf:a","aac_adtstoasc","-acodec","aac","-ar","8k","-b:a","100k","-bufsize","400k","-ac","1","-profile:a","aac_low","-flags","+global_header"];else if(a||w)b=["-acodec","copy"],b.push(...S);else if(d)b=["-acodec","copy"],b.push(...v);else{b=["-acodec","copy"];const e="aac"===m?S:v;b.push(...e)}const I=["-vcodec","copy"],C={console:this.console,timeout:6e4,parsers:{}};if(this.parsers=C.parsers,this.console.log("rebroadcast mode:",h?"rtsp":"mpegts"),h){const e=function(){let e;return{container:"rtsp",tcpProtocol:"rtsp://127.0.0.1/"+(0,R.randomBytes)(8).toString("hex"),inputArguments:["-rtsp_transport","tcp"],outputArguments:["-rtsp_transport","tcp","-vcodec","copy","-acodec","copy","-f","rtsp"],findSyncFrame:L,sdp:new Promise((t=>e=t)),async*parse(t,i,s){const r=new N(t);await r.handleSetup(),e(r.sdp);for await(const{type:e,rtcp:t,header:o,packet:n}of r.handleRecord())yield{chunks:[o,n],type:e,width:i,height:s}}}}();this.sdp=e.sdp,C.parsers.rtsp=e}else C.parsers.mpegts={container:"mpegts",outputArguments:[...(x={vcodec:I,acodec:b})?.vcodec||[],...x?.acodec||[],"-f","mpegts"],parse:(D=188,T=e=>{if(71!=e[0])throw new Error("Invalid sync byte in mpeg-ts packet. Terminating stream.")},async function*(e){let t=[],i=0;for(;;){const s=e.read();if(!s){await(0,r.once)(e,"readable");continue}if(t.push(s),i+=s.length,i<D)continue;const o=Buffer.concat(t);T?.(o);const n=o.length%D,a=o.slice(0,o.length-n),c=o.slice(o.length-n);t=[c],i=c.length,yield{chunks:[a]}}}),findSyncFrame(e){for(let t=0;t<e.length;t++){const i=e[t];for(let s=0;s<i.chunks.length;s++){const r=i.chunks[s];let o=0;for(;o+188<r.length;){const i=r.subarray(o,o+188);if(256==((31&i[1])<<8|i[2])&&32&i[3]&&i[4]>0&&64&i[5])return e.slice(t);o+=188}}}return e}};var x,D,T;f&&(C.parsers.mp4=O({vcodec:I,acodec:b}));const A=await this.mixinDevice.getVideoStream(i),k="x-scrypted/x-rfc4571"===A.mimeType;let _,H;this.storage.removeItem(this.lastDetectedAudioCodecKey);const V=h&&!f;if(V&&k){const e=await F.convertMediaObjectToJSON(A,"x-scrypted/x-rfc4571"),{url:t,sdp:i,mediaStreamOptions:s}=e;_=await U(function(e){const t=new URL(e);if(!t.protocol.startsWith("tcp"))throw new Error("rfc4751 url must be tcp");return c().connect(parseInt(t.port),t.hostname)}(t),i,s,!1,C),this.sdp=_.sdp.then((e=>Buffer.concat(e).toString()))}else{const t=await F.convertMediaObjectToBuffer(A,e.ScryptedMimeTypes.FFmpegInput),i=JSON.parse(t.toString());if(H=i.mediaStreamOptions,V&&"rtsp"===i.mediaStreamOptions?.container&&"scrypted"===i.mediaStreamOptions?.tool){const e=new B(i.url);await e.options();const t=(await e.describe()).body.toString().trim();this.sdp=Promise.resolve(t);const{audio:s,video:r}=function(e){const t=e.split("m="),i=t.find((e=>e.startsWith("audio"))),s=t.find((e=>e.startsWith("video")));return{audio:E(i),video:E(s)}}(t);await e.setup(0,s),await e.setup(2,r);const o=await e.play();_=await U(o,t,i.mediaStreamOptions,!0,C)}else{const e=this.storage.getItem(this.ffmpegInputArgumentsKey)||ee;i.inputArguments.unshift(...e.split(" ")),_=await P(i,C)}}if(_.inputAudioCodec?Z.includes(_.inputAudioCodec?.toLowerCase())?this.console.log("Detected audio codec is mp4/mpegts compatible.",_.inputAudioCodec):this.console.log("Detected audio codec is not mp4/mpegts compatible.",_.inputAudioCodec):this.console.log("No audio stream detected."),this.storage.setItem(this.lastDetectedAudioCodecKey,_.inputAudioCodec||"null"),"h264"!==_.inputVideoCodec&&this.console.error("Video codec is not h264. If there are errors, try changing your camera's encoder output."),l)return this.console.warn("Audio probe complete, ending rebroadcast session and restarting with detected codecs."),_.kill(),this.startPrebufferSession();if(this.parserSession=_,H?.refreshAt){let t,i=H;const s=async()=>{if(!_.isActive)return;const t=await this.mixinDevice.getVideoStream(i),s=await F.convertMediaObjectToBuffer(t,e.ScryptedMimeTypes.FFmpegInput),o=JSON.parse(s.toString());i=o.mediaStreamOptions,r(i)},r=e=>{const i=e.refreshAt-Date.now()-3e4;this.console.log("refreshing media stream in",i),t=setTimeout(s,i)};r(i),_.once("killed",(()=>clearTimeout(t)))}_.once("killed",(()=>{this.parserSessionPromise=void 0,this.parserSession===_&&(this.parserSession=void 0)}));for(const e of ie){let i=0;_.on(e,(s=>{const r=this.prebuffers[e],o=Date.now();for("mdat"===s.type&&(this.prevIdr&&(this.detectedIdrInterval=o-this.prevIdr),this.prevIdr=o),r.push({time:o,chunk:s});r.length&&r[0].time<o-t;)r.shift(),i++;i>1e3&&(this.prebuffers[e]=r.slice(),i=0)}))}return _}printActiveClients(){this.console.log(this.streamName,"active rebroadcast clients:",this.activeClients)}inactivityCheck(e,t){this.printActiveClients(),this.stopInactive&&(this.activeClients||this.inactivityTimeout&&!t||(clearTimeout(this.inactivityTimeout),this.inactivityTimeout=setTimeout((()=>{this.activeClients||(this.console.log(this.streamName,"terminating rebroadcast due to inactivity"),e.kill())}),3e4)))}async getVideoStream(e){this.ensurePrebufferSession();const t=await this.parserSessionPromise,i="false"!==this.storage.getItem(J);let s=e?.prebuffer;null==s&&(s=i?1.5*Math.max(4e3,this.detectedIdrInterval||4e3):0),this.console.log(this.streamName,"client request started");const{rtspMode:r}=this.getRebroadcastMode(),o=r?"rtsp":"mpegts",n=this.parsers[e?.container]?e?.container:o;e?.prebuffer&&"mp4"!==n&&"mp4"===e?.container&&(s+=4e3);const a=Object.assign({},t.mediaStreamOptions);a.prebuffer=s;const{reencodeAudio:c}=this.getAudioConfig();let d=!1;r&&"rtsp"===n?d=!0:this.audioDisabled?a.audio=null:c?a.audio={codec:"aac",encoder:"aac",profile:"aac_low"}:d=!0,d&&(a.audio=t?.mediaStreamOptions?.audio?.codec===t?.inputAudioCodec?t?.mediaStreamOptions?.audio:{codec:t?.inputAudioCodec}),a.video&&t.inputVideoResolution?.[2]&&t.inputVideoResolution?.[3]&&Object.assign(a.video,{width:parseInt(t.inputVideoResolution[2]),height:parseInt(t.inputVideoResolution[3])});const u=Date.now();let p=0;const l=this.prebuffers[n];for(const e of l)if(!(e.time<u-s))for(const t of e.chunk.chunks)p+=t.length;const m=Math.max(5e5,p).toString(),h=await(async i=>{const r=this.prebuffers[i];let o,n;if("rtsp"===i){this.sdp.then((e=>console.log(e)));const e=await g();o=e.clientPromise.then((async e=>{let t=await this.sdp;const i=new N(e,t);return await i.handlePlayback(),e})),n=e.url.replace("tcp://","rtsp://")}else{const e=await g();o=e.clientPromise,n=`tcp://127.0.0.1:${e.port}`}return M(o,{console:this.console,connect:(o,n)=>{this.activeClients++,this.printActiveClients();const a=Date.now(),c=e=>{o(e)>1e8&&(this.console.log("more than 100MB has been buffered, did downstream die? killing connection.",this.streamName),d())},d=()=>{n(),this.console.log(this.streamName,"client request ended"),t.removeListener(i,c),t.removeListener("killed",d)};t.on(i,c),t.once("killed",d);for(const e of r)e.time<a-s||c(e.chunk);return()=>{this.activeClients--,this.inactivityCheck(t,!1!==e?.refresh),d()}}}),n})(n),f={url:h,container:n,inputArguments:["-analyzeduration","0","-probesize",m,...this.parsers[n].inputArguments||[],"-f",this.parsers[n].container,"-i",h],mediaStreamOptions:a};return F.createFFmpegMediaObject(f)}}class re extends n{released=!1;sessions=new Map;constructor(e,t,i,s){super(e,i,{providerNativeId:s,mixinDeviceInterfaces:t,group:"Prebuffer Settings",groupKey:"prebuffer"}),this.delayStart()}delayStart(){this.console.log("prebuffer sessions starting in 5 seconds"),setTimeout((()=>this.ensurePrebufferSessions()),5e3)}async getVideoStream(e){await this.ensurePrebufferSessions();const t=e?.id;let i=this.sessions.get(t);return!i||e?.directMediaStream?this.mixinDevice.getVideoStream(e):(i.ensurePrebufferSession(),await i.parserSessionPromise,i=this.sessions.get(t),i?i.getVideoStream(e):this.mixinDevice.getVideoStream(e))}async ensurePrebufferSessions(){const t=await this.mixinDevice.getVideoStreamOptions(),i=this.getEnabledMediaStreamOptions(t),s=i?i.map((e=>e.id)):[void 0],o=t?.map((e=>e.id))||[void 0];if("true"!==this.storage.getItem("warnedCloud")){const e=t?.find((e=>"cloud"===e.source));e&&(this.storage.setItem("warnedCloud","true"),j.a(`${this.name} is a cloud camera. Prebuffering maintains a persistent stream and will not enabled by default. You must enable the Prebuffer stream manually.`))}const n=this.mixinDeviceInterfaces.includes(e.ScryptedInterface.Battery);let a=0;const c=s.length;for(const e of o){let i=this.sessions.get(e);if(!i){const o=t?.find((t=>t.id===e));o?.prebuffer&&j.a(`Prebuffer is already available on ${this.name}. If this is a grouped device, disable the Rebroadcast extension.`);const d=o?.name,u=!s.includes(e);if(i=new se(this,d,e,n||u),this.sessions.set(e,i),e===t?.[0]?.id&&this.sessions.set(void 0,i),n){this.console.log("camera is battery powered, prebuffering and rebroadcasting will only work on demand.");continue}if(u){this.console.log("stream",d,"will be rebroadcast on demand.");continue}(async()=>{for(;this.sessions.get(e)===i&&!this.released;){i.ensurePrebufferSession();let e=!1;try{const t=await i.parserSessionPromise;a++,e=!0,this.online=a==c,await(0,r.once)(t,"killed"),this.console.error("prebuffer session ended")}catch(e){this.console.error("prebuffer session ended with error",e)}finally{e&&a--,e=!1,this.online=a==c}this.console.log("restarting prebuffer session in 5 seconds"),await new Promise((e=>setTimeout(e,5e3)))}this.console.log("exiting prebuffer session (released or restarted with new configuration)")})()}}q.onMixinEvent(this.id,this.mixinProviderNativeId,e.ScryptedInterface.Settings,void 0)}async getMixinSettings(){const e=[];try{const t=await this.mixinDevice.getVideoStreamOptions(),i=this.getEnabledMediaStreamOptions(t);t?.length>0&&e.push({title:"Prebuffered Streams",description:"The streams to prebuffer. Enable only as necessary to reduce traffic.",key:"enabledStreams",value:i.map((e=>e.name||"")),choices:t.map((e=>e.name)),multiple:!0})}catch(e){throw this.console.error("error in getVideoStreamOptions",e),e}e.push({title:"Prebuffer Duration",description:"Duration of the prebuffer in milliseconds.",type:"number",key:G,value:this.storage.getItem(G)||$.toString()},{title:"Start at Previous Keyframe",description:"Start live streams from the previous key frame. Improves startup time.",type:"boolean",key:J,value:("false"!==this.storage.getItem(J)).toString()});for(const t of new Set([...this.sessions.values()]))if(t)try{e.push(...await t.getMixinSettings())}catch(e){throw this.console.error("error in prebuffer session getMixinSettings",e),e}return e}async putMixinSetting(e,t){const i=this.sessions;this.sessions=new Map,"enabledStreams"===e?this.storage.setItem(e,JSON.stringify(t)):this.storage.setItem(e,t.toString());for(const e of i.values())e?.parserSessionPromise?.then((e=>e.kill()));this.ensurePrebufferSessions()}getEnabledMediaStreamOptions(e){if(!e)return;try{const t=JSON.parse(this.storage.getItem("enabledStreams"));return e.filter((e=>t.includes(e.name)))}catch(e){}const t=e.find((e=>"cloud"!==e.source));return t?[t]:[]}async getVideoStreamOptions(){const e=await this.mixinDevice.getVideoStreamOptions()||[];let t=this.getEnabledMediaStreamOptions(e);const i=parseInt(this.storage.getItem(G))||$;for(const s of e)(this.sessions.get(s.id)?.parserSession||t.includes(s))&&(s.prebuffer=i);return e}async release(){this.console.log("prebuffer session releasing if started"),this.released=!0;for(const e of this.sessions.values())e&&(e.clearPrebuffers(),e.parserSessionPromise?.then((t=>{this.console.log("prebuffer session released"),t.kill(),e.clearPrebuffers()})))}}const oe=new class extends k{constructor(t){super(t),this.fromMimeType="x-scrypted/x-rfc4571",this.toMimeType=e.ScryptedMimeTypes.FFmpegInput;for(const e of Object.keys(K.getSystemState())){const t=K.getDeviceById(e);t.mixins?.includes(this.id)&&t.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((()=>q.requestRestart()),i)}async convert(e,t,i){const s=JSON.parse(e.toString()),{url:r,sdp:o}=s,{audioPayloadTypes:n,videoPayloadTypes:a}=function(e){const t=new Set,i=new Set,s=(e,t)=>{for(const i of t||[])e.add(parseInt(i))},r=e.match(/m=audio.*/)?.[0];s(t,r?.split(" ").slice(3));const o=e.match(/m=video.*/)?.[0];return s(i,o?.split(" ").slice(3)),{audioPayloadTypes:t,videoPayloadTypes:i}}(o),d=new URL(r);if(!d.protocol.startsWith("tcp"))throw new Error("rfc4751 url must be tcp");const{clientPromise:u,url:p}=await g(),l={url:p,inputArguments:["-rtsp_transport","tcp","-i",p.replace("tcp","rtsp")]};return u.then((async e=>{const t=new N(e,o);await t.handlePlayback();const i=c().connect(parseInt(d.port),d.hostname);for(e.on("close",(()=>{i.destroy()})),i.on("close",(()=>{e.destroy()}));;){const s=(await I(i,2)).readInt16BE(0),r=await I(i,s),o=127&r[1];if(n.has(o))t.sendAudio(r,!1);else{if(!a.has(o))throw e.destroy(),i.destroy(),new Error("unknown payload type "+o);t.sendVideo(r,!1)}}})),Buffer.from(JSON.stringify(l))}async canMixin(t,i){return i.includes(e.ScryptedInterface.VideoCamera)?[e.ScryptedInterface.VideoCamera,e.ScryptedInterface.Settings,e.ScryptedInterface.Online]:null}async getMixin(e,t,i){return this.setHasEnabledMixin(i.id),new re(e,t,i,this.nativeId)}async releaseMixin(e,t){t.online=!0,t.release()}}})();var r=exports="undefined"==typeof exports?{}:exports;for(var o in s)r[o]=s[o];s.__esModule&&Object.defineProperty(r,"__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.
|
|
3
|
+
"version": "0.1.171",
|
|
4
4
|
"description": "Rebroadcast and Prebuffer for VideoCameras.",
|
|
5
5
|
"author": "Scrypted",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"scrypted",
|
|
20
|
-
"plugin"
|
|
20
|
+
"plugin",
|
|
21
|
+
"rebroadcast"
|
|
21
22
|
],
|
|
22
23
|
"scrypted": {
|
|
23
24
|
"name": "Rebroadcast Plugin",
|
package/src/main.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { MixinProvider, ScryptedDeviceType, ScryptedInterface, MediaObject, VideoCamera, MediaStreamOptions, Settings, Setting, ScryptedMimeTypes, FFMpegInput, RequestMediaStreamOptions, BufferConverter, ResponseMediaStreamOptions } from '@scrypted/sdk';
|
|
3
3
|
import sdk from '@scrypted/sdk';
|
|
4
4
|
import { once } from 'events';
|
|
5
|
-
import { SettingsMixinDeviceBase } from "
|
|
5
|
+
import { SettingsMixinDeviceBase } from "@scrypted/common/src/settings-mixin";
|
|
6
6
|
import { handleRebroadcasterClient, ParserOptions, ParserSession, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
|
|
7
7
|
import { createMpegTsParser, createFragmentedMp4Parser, StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
|
|
8
8
|
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
|
9
9
|
import { listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
|
10
|
-
import {
|
|
10
|
+
import { parsePayloadTypes, parseTrackIds } from '@scrypted/common/src/sdp-utils';
|
|
11
|
+
import { createRtspParser, RtspClient, RtspServer } from '@scrypted/common/src/rtsp-server';
|
|
11
12
|
import { Duplex } from 'stream';
|
|
12
13
|
import net from 'net';
|
|
13
14
|
import { readLength } from '@scrypted/common/src/read-stream';
|
|
@@ -340,7 +341,8 @@ class PrebufferSession {
|
|
|
340
341
|
else if (reencodeAudio || mustTranscode) {
|
|
341
342
|
acodec = [
|
|
342
343
|
'-bsf:a', 'aac_adtstoasc',
|
|
343
|
-
'-acodec', 'libfdk_aac',
|
|
344
|
+
// '-acodec', 'libfdk_aac',
|
|
345
|
+
'-acodec', 'aac',
|
|
344
346
|
'-ar', `8k`,
|
|
345
347
|
'-b:a', `100k`,
|
|
346
348
|
'-bufsize', '400k',
|
|
@@ -428,7 +430,7 @@ class PrebufferSession {
|
|
|
428
430
|
const json = await mediaManager.convertMediaObjectToJSON<any>(mo, 'x-scrypted/x-rfc4571');
|
|
429
431
|
const { url, sdp, mediaStreamOptions } = json;
|
|
430
432
|
|
|
431
|
-
session = await startRFC4571Parser(connectRFC4571Parser(url), sdp, mediaStreamOptions);
|
|
433
|
+
session = await startRFC4571Parser(connectRFC4571Parser(url), sdp, mediaStreamOptions, false, rbo);
|
|
432
434
|
this.sdp = session.sdp.then(buffers => Buffer.concat(buffers).toString());
|
|
433
435
|
}
|
|
434
436
|
else {
|
|
@@ -444,10 +446,12 @@ class PrebufferSession {
|
|
|
444
446
|
const sdpResponse = await rtspClient.describe();
|
|
445
447
|
const sdp = sdpResponse.body.toString().trim();
|
|
446
448
|
this.sdp = Promise.resolve(sdp);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
+
const { audio, video } = parseTrackIds(sdp);
|
|
450
|
+
// handle no audio?
|
|
451
|
+
await rtspClient.setup(0, audio);
|
|
452
|
+
await rtspClient.setup(2, video);
|
|
449
453
|
const socket = await rtspClient.play();
|
|
450
|
-
session = await startRFC4571Parser(socket, sdp, ffmpegInput.mediaStreamOptions, true);
|
|
454
|
+
session = await startRFC4571Parser(socket, sdp, ffmpegInput.mediaStreamOptions, true, rbo);
|
|
451
455
|
}
|
|
452
456
|
else {
|
|
453
457
|
// create missing pts from dts so mpegts and mp4 muxing does not fail
|
|
@@ -581,7 +585,15 @@ class PrebufferSession {
|
|
|
581
585
|
const session = await this.parserSessionPromise;
|
|
582
586
|
|
|
583
587
|
const sendKeyframe = this.storage.getItem(SEND_KEYFRAME) !== 'false';
|
|
584
|
-
|
|
588
|
+
let requestedPrebuffer = options?.prebuffer;
|
|
589
|
+
if (requestedPrebuffer == null) {
|
|
590
|
+
if (sendKeyframe) {
|
|
591
|
+
requestedPrebuffer = Math.max(4000, (this.detectedIdrInterval || 4000)) * 1.5;
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
requestedPrebuffer = 0;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
585
597
|
|
|
586
598
|
this.console.log(this.streamName, 'client request started');
|
|
587
599
|
|
|
@@ -663,11 +675,18 @@ class PrebufferSession {
|
|
|
663
675
|
return containerUrl;
|
|
664
676
|
}
|
|
665
677
|
|
|
666
|
-
const { rtspMode
|
|
678
|
+
const { rtspMode } = this.getRebroadcastMode();
|
|
667
679
|
const defaultContainer = rtspMode ? 'rtsp' : 'mpegts';
|
|
668
680
|
|
|
669
681
|
const container: PrebufferParsers = this.parsers[options?.container] ? options?.container as PrebufferParsers : defaultContainer;
|
|
670
682
|
|
|
683
|
+
// If a mp4 prebuffer was explicitly requested, but an mp4 prebuffer is not available (rtsp mode),
|
|
684
|
+
// rewind a little bit earlier to gaurantee a full segment of that length
|
|
685
|
+
// is sent.
|
|
686
|
+
if (options?.prebuffer && container !== 'mp4' && options?.container === 'mp4') {
|
|
687
|
+
requestedPrebuffer += 4000;
|
|
688
|
+
}
|
|
689
|
+
|
|
671
690
|
const mediaStreamOptions: MediaStreamOptions = Object.assign({}, session.mediaStreamOptions);
|
|
672
691
|
|
|
673
692
|
mediaStreamOptions.prebuffer = requestedPrebuffer;
|
|
@@ -682,7 +701,7 @@ class PrebufferSession {
|
|
|
682
701
|
else if (reencodeAudio) {
|
|
683
702
|
mediaStreamOptions.audio = {
|
|
684
703
|
codec: 'aac',
|
|
685
|
-
encoder: '
|
|
704
|
+
encoder: 'aac',
|
|
686
705
|
profile: 'aac_low',
|
|
687
706
|
}
|
|
688
707
|
}
|
|
@@ -696,16 +715,16 @@ class PrebufferSession {
|
|
|
696
715
|
}
|
|
697
716
|
|
|
698
717
|
if (codecCopy) {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
}
|
|
718
|
+
// reported codecs may be wrong/cached/etc, so before blindly copying the audio codec info,
|
|
719
|
+
// verify what was found.
|
|
720
|
+
if (session?.mediaStreamOptions?.audio?.codec === session?.inputAudioCodec) {
|
|
721
|
+
mediaStreamOptions.audio = session?.mediaStreamOptions?.audio;
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
mediaStreamOptions.audio = {
|
|
725
|
+
codec: session?.inputAudioCodec,
|
|
708
726
|
}
|
|
727
|
+
}
|
|
709
728
|
}
|
|
710
729
|
|
|
711
730
|
if (mediaStreamOptions.video && session.inputVideoResolution?.[2] && session.inputVideoResolution?.[3]) {
|
|
@@ -799,7 +818,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
|
|
799
818
|
const isBatteryPowered = this.mixinDeviceInterfaces.includes(ScryptedInterface.Battery);
|
|
800
819
|
|
|
801
820
|
let active = 0;
|
|
802
|
-
const total =
|
|
821
|
+
const total = enabledIds.length;
|
|
803
822
|
for (const id of ids) {
|
|
804
823
|
let session = this.sessions.get(id);
|
|
805
824
|
if (!session) {
|
|
@@ -828,9 +847,11 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
|
|
828
847
|
(async () => {
|
|
829
848
|
while (this.sessions.get(id) === session && !this.released) {
|
|
830
849
|
session.ensurePrebufferSession();
|
|
850
|
+
let wasActive = false;
|
|
831
851
|
try {
|
|
832
852
|
const ps = await session.parserSessionPromise;
|
|
833
853
|
active++;
|
|
854
|
+
wasActive = true;
|
|
834
855
|
this.online = active == total;
|
|
835
856
|
await once(ps, 'killed');
|
|
836
857
|
this.console.error('prebuffer session ended');
|
|
@@ -839,7 +860,9 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
|
|
839
860
|
this.console.error('prebuffer session ended with error', e);
|
|
840
861
|
}
|
|
841
862
|
finally {
|
|
842
|
-
|
|
863
|
+
if (wasActive)
|
|
864
|
+
active--;
|
|
865
|
+
wasActive = false;
|
|
843
866
|
this.online = active == total;
|
|
844
867
|
}
|
|
845
868
|
this.console.log('restarting prebuffer session in 5 seconds');
|
|
@@ -955,7 +978,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
|
|
955
978
|
return ret;
|
|
956
979
|
}
|
|
957
980
|
|
|
958
|
-
release() {
|
|
981
|
+
async release() {
|
|
959
982
|
this.console.log('prebuffer session releasing if started');
|
|
960
983
|
this.released = true;
|
|
961
984
|
for (const session of this.sessions.values()) {
|
|
@@ -1006,18 +1029,7 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
|
|
|
1006
1029
|
const json = JSON.parse(data.toString());
|
|
1007
1030
|
const { url, sdp } = json;
|
|
1008
1031
|
|
|
1009
|
-
const
|
|
1010
|
-
const audioPt = new Set<number>();
|
|
1011
|
-
const videoPt = new Set<number>();
|
|
1012
|
-
const addPts = (set: Set<number>, pts: string[]) => {
|
|
1013
|
-
for (const pt of pts || []) {
|
|
1014
|
-
set.add(parseInt(pt));
|
|
1015
|
-
}
|
|
1016
|
-
};
|
|
1017
|
-
const audioPts = sdpString.match(/m=audio.*/)?.[0];
|
|
1018
|
-
addPts(audioPt, audioPts?.split(' ').slice(3));
|
|
1019
|
-
const videoPts = (sdp as string).match(/m=video.*/)?.[0];
|
|
1020
|
-
addPts(videoPt, videoPts?.split(' ').slice(3));
|
|
1032
|
+
const { audioPayloadTypes, videoPayloadTypes } = parsePayloadTypes(sdp);
|
|
1021
1033
|
|
|
1022
1034
|
const u = new URL(url);
|
|
1023
1035
|
if (!u.protocol.startsWith('tcp'))
|
|
@@ -1027,7 +1039,6 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
|
|
|
1027
1039
|
url: clientUrl,
|
|
1028
1040
|
inputArguments: [
|
|
1029
1041
|
"-rtsp_transport", "tcp",
|
|
1030
|
-
"-max_delay", "1000000",
|
|
1031
1042
|
'-i', clientUrl.replace('tcp', 'rtsp'),
|
|
1032
1043
|
]
|
|
1033
1044
|
};
|
|
@@ -1050,10 +1061,10 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
|
|
|
1050
1061
|
const length = header.readInt16BE(0);
|
|
1051
1062
|
const data = await readLength(socket, length);
|
|
1052
1063
|
const pt = data[1] & 0x7f;
|
|
1053
|
-
if (
|
|
1064
|
+
if (audioPayloadTypes.has(pt)) {
|
|
1054
1065
|
rtsp.sendAudio(data, false);
|
|
1055
1066
|
}
|
|
1056
|
-
else if (
|
|
1067
|
+
else if (videoPayloadTypes.has(pt)) {
|
|
1057
1068
|
rtsp.sendVideo(data, false);
|
|
1058
1069
|
}
|
|
1059
1070
|
else {
|
package/src/rfc4571.ts
CHANGED
|
@@ -19,7 +19,7 @@ export function connectRFC4571Parser(url: string) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
export async function startRFC4571Parser(socket: net.Socket, sdp: string, mediaStreamOptions: MediaStreamOptions, hasRstpPrefix?: boolean): Promise<ParserSession<"rtsp">> {
|
|
22
|
+
export async function startRFC4571Parser(socket: net.Socket, sdp: string, mediaStreamOptions: MediaStreamOptions, hasRstpPrefix?: boolean, options?: ParserOptions<"rtsp">): Promise<ParserSession<"rtsp">> {
|
|
23
23
|
let isActive = true;
|
|
24
24
|
const events = new EventEmitter();
|
|
25
25
|
|
|
@@ -38,17 +38,43 @@ export async function startRFC4571Parser(socket: net.Socket, sdp: string, mediaS
|
|
|
38
38
|
socket.on('close', kill);
|
|
39
39
|
socket.on('error', kill);
|
|
40
40
|
|
|
41
|
+
const setupActivityTimer = (container: string) => {
|
|
42
|
+
let dataTimeout: NodeJS.Timeout;
|
|
43
|
+
|
|
44
|
+
function dataKill() {
|
|
45
|
+
console.error('timeout waiting for data, killing parser session', container);
|
|
46
|
+
kill();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resetActivityTimer() {
|
|
50
|
+
if (!options.timeout)
|
|
51
|
+
return;
|
|
52
|
+
clearTimeout(dataTimeout);
|
|
53
|
+
dataTimeout = setTimeout(dataKill, options.timeout);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
events.once('killed', () => clearTimeout(dataTimeout));
|
|
57
|
+
|
|
58
|
+
resetActivityTimer();
|
|
59
|
+
return {
|
|
60
|
+
resetActivityTimer,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
41
65
|
(async () => {
|
|
66
|
+
const { resetActivityTimer } = setupActivityTimer('rtsp');
|
|
67
|
+
|
|
42
68
|
while (true) {
|
|
43
69
|
let header: Buffer;
|
|
44
70
|
let length: number;
|
|
45
71
|
if (hasRstpPrefix) {
|
|
46
72
|
header = await readLength(socket, 4);
|
|
47
|
-
length = header.
|
|
73
|
+
length = header.readUInt16BE(2);
|
|
48
74
|
}
|
|
49
75
|
else {
|
|
50
76
|
header = await readLength(socket, 2);
|
|
51
|
-
length = header.
|
|
77
|
+
length = header.readUInt16BE(0);
|
|
52
78
|
}
|
|
53
79
|
const data = await readLength(socket, length);
|
|
54
80
|
|
|
@@ -69,6 +95,7 @@ export async function startRFC4571Parser(socket: net.Socket, sdp: string, mediaS
|
|
|
69
95
|
chunks: [header, data],
|
|
70
96
|
}
|
|
71
97
|
events.emit('rtsp', chunk);
|
|
98
|
+
resetActivityTimer();
|
|
72
99
|
}
|
|
73
100
|
})()
|
|
74
101
|
.finally(kill);
|