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