@scrypted/prebuffer-mixin 0.1.166 → 0.1.170
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 +52 -41
- package/src/rfc4571.ts +28 -1
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"]}},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"}(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 x=!1;const C=["pipe","pipe","pipe"];let D=3;for(const e of Object.keys(t.parsers)){const s=t.parsers[e];if(s.parseDatagram){x=!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++),C.push("pipe")}x&&(M.push("-sdp_file","pipe:"+D++),C.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:C});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=x?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 x="\n".charCodeAt(0);async function C(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,x)}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 C(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 C(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"],x={console:this.console,timeout:6e4,parsers:{}};if(this.parsers=x.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,x.parsers.rtsp=e}else x.parsers.mpegts={container:"mpegts",outputArguments:[...(C={vcodec:I,acodec:b})?.vcodec||[],...C?.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 C,D,A;f&&(x.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&&k&&!f;if(N){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,x)}}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,"prebuffer 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,"prebuffer 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 releasing if started"),this.released=!0;for(const e of this.sessions.values())e&&(e.clearPrebuffers(),e.parserSessionPromise?.then((t=>{this.console.log("prebuffer 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:[]}};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"}(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.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"}(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:()=>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,D?.kill(),setTimeout((()=>D?.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 O=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:"+O++),x.push("pipe")}C&&(M.push("-sdp_file","pipe:"+O++),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 D=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,D,void 0,t?.storage),D.on("exit",P),A=C?new Promise((e=>{const t=[];D.stdio[O-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=D.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")}(D).then((e=>a=e)),async function(e){return w(e,"Video")}(D).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)}))}(D).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 O(e){return e}function D(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:O}}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 L(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 _ 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=L(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=L(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 N=require("stream"),{mediaManager:V}=t();async function H(e,t,i,s,r){let o=!0;const n=new N.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.readInt16BE(2)):(i=await I(e,2),r=i.readInt16BE(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: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","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:(O=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<O)continue;const o=Buffer.concat(t);A?.(o);const n=o.length%O,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,O,A;f&&(C.parsers.mp4=D({vcodec:I,acodec:b}));const T=await this.mixinDevice.getVideoStream(i),k="x-scrypted/x-rfc4571"===T.mimeType;let L,N;this.storage.removeItem(this.lastDetectedAudioCodecKey);const V=h&&!f;if(V&&k){const e=await U.convertMediaObjectToJSON(T,"x-scrypted/x-rfc4571"),{url:t,sdp:i,mediaStreamOptions:s}=e;L=await H(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=L.sdp.then((e=>Buffer.concat(e).toString()))}else{const t=await U.convertMediaObjectToBuffer(T,e.ScryptedMimeTypes.FFmpegInput),i=JSON.parse(t.toString());if(N=i.mediaStreamOptions,V&&"rtsp"===i.mediaStreamOptions?.container&&"scrypted"===i.mediaStreamOptions?.tool){const e=new _(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();L=await H(s,t,i.mediaStreamOptions,!0,C)}else{const e=this.storage.getItem(this.ffmpegInputArgumentsKey)||Z;i.inputArguments.unshift(...e.split(" ")),L=await P(i,C)}}if(L.inputAudioCodec?X.includes(L.inputAudioCodec?.toLowerCase())?this.console.log("Detected audio codec is mp4/mpegts compatible.",L.inputAudioCodec):this.console.log("Detected audio codec is not mp4/mpegts compatible.",L.inputAudioCodec):this.console.log("No audio stream detected."),this.storage.setItem(this.lastDetectedAudioCodecKey,L.inputAudioCodec||"null"),"h264"!==L.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."),L.kill(),this.startPrebufferSession();if(this.parserSession=L,N?.refreshAt){let t,i=N;const s=async()=>{if(!L.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),L.once("killed",(()=>clearTimeout(t)))}L.once("killed",(()=>{this.parserSessionPromise=void 0,this.parserSession===L&&(this.parserSession=void 0)}));for(const e of te){let i=0;L.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 L}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);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 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})(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 U.createFFmpegMediaObject(f)}}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=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&&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();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)")})()}}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,{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 B(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 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})})();
|
|
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.170",
|
|
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 } 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',
|
|
@@ -421,14 +423,14 @@ class PrebufferSession {
|
|
|
421
423
|
// before launching the parser session, clear out the last detected codec.
|
|
422
424
|
// an erroneous cached codec could cause ffmpeg to fail to start.
|
|
423
425
|
this.storage.removeItem(this.lastDetectedAudioCodecKey);
|
|
424
|
-
const canUseScryptedParser = rtspMode &&
|
|
426
|
+
const canUseScryptedParser = rtspMode && !mp4Mode;
|
|
425
427
|
|
|
426
|
-
if (canUseScryptedParser) {
|
|
428
|
+
if (canUseScryptedParser && isRfc4571) {
|
|
427
429
|
|
|
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 {
|
|
@@ -436,7 +438,9 @@ class PrebufferSession {
|
|
|
436
438
|
const ffmpegInput = JSON.parse(moBuffer.toString()) as FFMpegInput;
|
|
437
439
|
sessionMso = ffmpegInput.mediaStreamOptions;
|
|
438
440
|
|
|
439
|
-
if (canUseScryptedParser
|
|
441
|
+
if (canUseScryptedParser
|
|
442
|
+
&& ffmpegInput.mediaStreamOptions?.container === 'rtsp'
|
|
443
|
+
&& ffmpegInput.mediaStreamOptions?.tool === 'scrypted') {
|
|
440
444
|
const rtspClient = new RtspClient(ffmpegInput.url);
|
|
441
445
|
await rtspClient.options();
|
|
442
446
|
const sdpResponse = await rtspClient.describe();
|
|
@@ -445,7 +449,7 @@ class PrebufferSession {
|
|
|
445
449
|
await rtspClient.setup(0, '/audio');
|
|
446
450
|
await rtspClient.setup(2, '/video');
|
|
447
451
|
const socket = await rtspClient.play();
|
|
448
|
-
session = await startRFC4571Parser(socket, sdp, ffmpegInput.mediaStreamOptions, true);
|
|
452
|
+
session = await startRFC4571Parser(socket, sdp, ffmpegInput.mediaStreamOptions, true, rbo);
|
|
449
453
|
}
|
|
450
454
|
else {
|
|
451
455
|
// create missing pts from dts so mpegts and mp4 muxing does not fail
|
|
@@ -579,9 +583,17 @@ class PrebufferSession {
|
|
|
579
583
|
const session = await this.parserSessionPromise;
|
|
580
584
|
|
|
581
585
|
const sendKeyframe = this.storage.getItem(SEND_KEYFRAME) !== 'false';
|
|
582
|
-
|
|
586
|
+
let requestedPrebuffer = options?.prebuffer;
|
|
587
|
+
if (requestedPrebuffer == null) {
|
|
588
|
+
if (sendKeyframe) {
|
|
589
|
+
requestedPrebuffer = Math.max(4000, (this.detectedIdrInterval || 4000)) * 1.5;
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
requestedPrebuffer = 0;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
583
595
|
|
|
584
|
-
this.console.log(this.streamName, '
|
|
596
|
+
this.console.log(this.streamName, 'client request started');
|
|
585
597
|
|
|
586
598
|
const createContainerServer = async (container: PrebufferParsers) => {
|
|
587
599
|
const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
|
|
@@ -625,7 +637,7 @@ class PrebufferSession {
|
|
|
625
637
|
|
|
626
638
|
const cleanup = () => {
|
|
627
639
|
destroy();
|
|
628
|
-
this.console.log(this.streamName, '
|
|
640
|
+
this.console.log(this.streamName, 'client request ended');
|
|
629
641
|
session.removeListener(container, safeWriteData);
|
|
630
642
|
session.removeListener('killed', cleanup);
|
|
631
643
|
}
|
|
@@ -661,11 +673,18 @@ class PrebufferSession {
|
|
|
661
673
|
return containerUrl;
|
|
662
674
|
}
|
|
663
675
|
|
|
664
|
-
const { rtspMode
|
|
676
|
+
const { rtspMode } = this.getRebroadcastMode();
|
|
665
677
|
const defaultContainer = rtspMode ? 'rtsp' : 'mpegts';
|
|
666
678
|
|
|
667
679
|
const container: PrebufferParsers = this.parsers[options?.container] ? options?.container as PrebufferParsers : defaultContainer;
|
|
668
680
|
|
|
681
|
+
// If a mp4 prebuffer was explicitly requested, but an mp4 prebuffer is not available (rtsp mode),
|
|
682
|
+
// rewind a little bit earlier to gaurantee a full segment of that length
|
|
683
|
+
// is sent.
|
|
684
|
+
if (options?.prebuffer && container !== 'mp4' && options?.container === 'mp4') {
|
|
685
|
+
requestedPrebuffer += 4000;
|
|
686
|
+
}
|
|
687
|
+
|
|
669
688
|
const mediaStreamOptions: MediaStreamOptions = Object.assign({}, session.mediaStreamOptions);
|
|
670
689
|
|
|
671
690
|
mediaStreamOptions.prebuffer = requestedPrebuffer;
|
|
@@ -680,7 +699,7 @@ class PrebufferSession {
|
|
|
680
699
|
else if (reencodeAudio) {
|
|
681
700
|
mediaStreamOptions.audio = {
|
|
682
701
|
codec: 'aac',
|
|
683
|
-
encoder: '
|
|
702
|
+
encoder: 'aac',
|
|
684
703
|
profile: 'aac_low',
|
|
685
704
|
}
|
|
686
705
|
}
|
|
@@ -694,16 +713,16 @@ class PrebufferSession {
|
|
|
694
713
|
}
|
|
695
714
|
|
|
696
715
|
if (codecCopy) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
716
|
+
// reported codecs may be wrong/cached/etc, so before blindly copying the audio codec info,
|
|
717
|
+
// verify what was found.
|
|
718
|
+
if (session?.mediaStreamOptions?.audio?.codec === session?.inputAudioCodec) {
|
|
719
|
+
mediaStreamOptions.audio = session?.mediaStreamOptions?.audio;
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
mediaStreamOptions.audio = {
|
|
723
|
+
codec: session?.inputAudioCodec,
|
|
706
724
|
}
|
|
725
|
+
}
|
|
707
726
|
}
|
|
708
727
|
|
|
709
728
|
if (mediaStreamOptions.video && session.inputVideoResolution?.[2] && session.inputVideoResolution?.[3]) {
|
|
@@ -797,7 +816,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
|
|
797
816
|
const isBatteryPowered = this.mixinDeviceInterfaces.includes(ScryptedInterface.Battery);
|
|
798
817
|
|
|
799
818
|
let active = 0;
|
|
800
|
-
const total =
|
|
819
|
+
const total = enabledIds.length;
|
|
801
820
|
for (const id of ids) {
|
|
802
821
|
let session = this.sessions.get(id);
|
|
803
822
|
if (!session) {
|
|
@@ -826,9 +845,11 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
|
|
826
845
|
(async () => {
|
|
827
846
|
while (this.sessions.get(id) === session && !this.released) {
|
|
828
847
|
session.ensurePrebufferSession();
|
|
848
|
+
let wasActive = false;
|
|
829
849
|
try {
|
|
830
850
|
const ps = await session.parserSessionPromise;
|
|
831
851
|
active++;
|
|
852
|
+
wasActive = true;
|
|
832
853
|
this.online = active == total;
|
|
833
854
|
await once(ps, 'killed');
|
|
834
855
|
this.console.error('prebuffer session ended');
|
|
@@ -837,7 +858,9 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
|
|
837
858
|
this.console.error('prebuffer session ended with error', e);
|
|
838
859
|
}
|
|
839
860
|
finally {
|
|
840
|
-
|
|
861
|
+
if (wasActive)
|
|
862
|
+
active--;
|
|
863
|
+
wasActive = false;
|
|
841
864
|
this.online = active == total;
|
|
842
865
|
}
|
|
843
866
|
this.console.log('restarting prebuffer session in 5 seconds');
|
|
@@ -954,14 +977,14 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
|
|
954
977
|
}
|
|
955
978
|
|
|
956
979
|
release() {
|
|
957
|
-
this.console.log('prebuffer releasing if started');
|
|
980
|
+
this.console.log('prebuffer session releasing if started');
|
|
958
981
|
this.released = true;
|
|
959
982
|
for (const session of this.sessions.values()) {
|
|
960
983
|
if (!session)
|
|
961
984
|
continue;
|
|
962
985
|
session.clearPrebuffers();
|
|
963
986
|
session.parserSessionPromise?.then(parserSession => {
|
|
964
|
-
this.console.log('prebuffer released');
|
|
987
|
+
this.console.log('prebuffer session released');
|
|
965
988
|
parserSession.kill();
|
|
966
989
|
session.clearPrebuffers();
|
|
967
990
|
});
|
|
@@ -1004,18 +1027,7 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
|
|
|
1004
1027
|
const json = JSON.parse(data.toString());
|
|
1005
1028
|
const { url, sdp } = json;
|
|
1006
1029
|
|
|
1007
|
-
const
|
|
1008
|
-
const audioPt = new Set<number>();
|
|
1009
|
-
const videoPt = new Set<number>();
|
|
1010
|
-
const addPts = (set: Set<number>, pts: string[]) => {
|
|
1011
|
-
for (const pt of pts || []) {
|
|
1012
|
-
set.add(parseInt(pt));
|
|
1013
|
-
}
|
|
1014
|
-
};
|
|
1015
|
-
const audioPts = sdpString.match(/m=audio.*/)?.[0];
|
|
1016
|
-
addPts(audioPt, audioPts?.split(' ').slice(3));
|
|
1017
|
-
const videoPts = (sdp as string).match(/m=video.*/)?.[0];
|
|
1018
|
-
addPts(videoPt, videoPts?.split(' ').slice(3));
|
|
1030
|
+
const { audioPayloadTypes, videoPayloadTypes } = parsePayloadTypes(sdp);
|
|
1019
1031
|
|
|
1020
1032
|
const u = new URL(url);
|
|
1021
1033
|
if (!u.protocol.startsWith('tcp'))
|
|
@@ -1025,7 +1037,6 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
|
|
|
1025
1037
|
url: clientUrl,
|
|
1026
1038
|
inputArguments: [
|
|
1027
1039
|
"-rtsp_transport", "tcp",
|
|
1028
|
-
"-max_delay", "1000000",
|
|
1029
1040
|
'-i', clientUrl.replace('tcp', 'rtsp'),
|
|
1030
1041
|
]
|
|
1031
1042
|
};
|
|
@@ -1048,10 +1059,10 @@ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider
|
|
|
1048
1059
|
const length = header.readInt16BE(0);
|
|
1049
1060
|
const data = await readLength(socket, length);
|
|
1050
1061
|
const pt = data[1] & 0x7f;
|
|
1051
|
-
if (
|
|
1062
|
+
if (audioPayloadTypes.has(pt)) {
|
|
1052
1063
|
rtsp.sendAudio(data, false);
|
|
1053
1064
|
}
|
|
1054
|
-
else if (
|
|
1065
|
+
else if (videoPayloadTypes.has(pt)) {
|
|
1055
1066
|
rtsp.sendVideo(data, false);
|
|
1056
1067
|
}
|
|
1057
1068
|
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,7 +38,33 @@ 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;
|
|
@@ -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);
|