@scrypted/prebuffer-mixin 0.1.44 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1,2 @@
1
- !function(e,t){for(var r in t)e[r]=t[r]}(window,function(e){var t={};function r(o){if(t[o])return t[o].exports;var n=t[o]={i:o,l:!1,exports:{}};return e[o].call(n.exports,n,n.exports,r),n.l=!0,n.exports}return r.m=e,r.c=t,r.d=function(e,t,o){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)r.d(o,n,function(t){return e[t]}.bind(null,n));return o},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=6)}([function(e,t,r){"use strict";var o=r(7);try{o=Object.assign(o,{log:deviceManager.getDeviceLogger(void 0),deviceManager:deviceManager,endpointManager:endpointManager,mediaManager:mediaManager,systemManager:systemManager,pluginHostAPI:pluginHostAPI})}catch(e){console.error("sdk initialization error, import @scrypted/sdk/types instead",e)}e.exports=o,e.exports.default=o},function(e,t){e.exports=require("net")},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.listenZeroCluster=async function(e){for(;;){const t=1e4+Math.round(3e4*Math.random());e.listen(t);try{return await(0,o.once)(e,"listening"),e.address().port}catch(e){}}};var o=r(3)},function(e,t){e.exports=require("events")},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=r(11);Object.keys(o).forEach((function(e){"default"!==e&&"__esModule"!==e&&(e in t&&t[e]===o[e]||Object.defineProperty(t,e,{enumerable:!0,get:function(){return o[e]}}))}))},function(e,t){e.exports=require("child_process")},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var o,n=function(e){if(e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=l();if(t&&t.has(e))return t.get(e);var r={},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n)){var i=o?Object.getOwnPropertyDescriptor(e,n):null;i&&(i.get||i.set)?Object.defineProperty(r,n,i):r[n]=e[n]}r.default=e,t&&t.set(e,r);return r}(r(0)),i=r(1),s=r(2),a=(o=r(3))&&o.__esModule?o:{default:o},c=r(9),d=r(10),u=r(4),p=r(12);function l(){if("function"!=typeof WeakMap)return null;var e=new WeakMap;return l=function(){return e},e}function m(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:f,log:g}=n.default;class h extends c.SettingsMixinDeviceBase{constructor(e,t,r,o){super(e,r,{providerNativeId:o,mixinDeviceInterfaces:t,group:"Rebroadcast and Prebuffer Settings",groupKey:"prebuffer"}),m(this,"prebufferMpegTs",[]),m(this,"prebufferFmp4",[]),m(this,"events",new a.default),m(this,"released",!1),m(this,"detectedIdrInterval",0),m(this,"detectedVcodec",""),m(this,"detectedAcodec",""),m(this,"prevIdr",0),console.log(this.name+" prebuffer session starting in 10 seconds"),setTimeout(()=>this.ensurePrebufferSession(),1e4)}async getMixinSettings(){const e=[];return e.push({title:"Prebuffer Duration",description:"Duration of the prebuffer in milliseconds.",type:"number",key:"prebufferDuration",value:this.storage.getItem("prebufferDuration")||15e3.toString()},{title:"Start at Previous Keyframe",description:"Start live streams from the previous key frame. Improves startup time.",type:"boolean",key:"sendKeyframe",value:("true"===this.storage.getItem("sendKeyframe")).toString()},{title:"Reencode Audio",description:"Reencode the audio (necessary if camera outputs PCM).",type:"boolean",key:"reencodeAudio",value:("true"===this.storage.getItem("reencodeAudio")).toString()},{group:"Media Information",title:"Detected Video Codec",readonly:!0,key:"detectedVcodec",value:this.detectedVcodec.toString()||"none"},{group:"Media Information",title:"Detected Audio Codec",readonly:!0,key:"detectedAcodec",value:this.detectedAcodec.toString()||"none"},{group:"Media Information",title:"Detected Keyframe Interval",description:"Currently detected keyframe interval. This value may vary based on the stream behavior.",readonly:!0,key:"detectedIdr",value:this.detectedIdrInterval.toString()||"none"}),e}async putMixinSetting(e,t){var r;this.storage.setItem(e,t.toString()),null===(r=this.prebufferSession)||void 0===r||r.then(e=>e.kill())}ensurePrebufferSession(){this.prebufferSession||this.released||(console.log(this.name+" prebuffer session started"),this.prebufferSession=this.startPrebufferSession())}async startPrebufferSession(){this.prebufferMpegTs=[];const e=parseInt(this.storage.getItem("prebufferDuration"))||15e3,t=JSON.parse((await f.convertMediaObjectToBuffer(await this.mixinDevice.getVideoStream(),n.ScryptedMimeTypes.FFmpegInput)).toString()),r="true"===this.storage.getItem("reencodeAudio");let o;o=(await(0,u.probeVideoCamera)(this.mixinDevice)).noAudio?["-an"]:r?[]:["-acodec","copy"];const a=["-vcodec","copy"],c=async t=>{l.close();const r=(0,p.parseFragmentedMP4)(t);for await(const t of r){const r=Date.now();for(this.ftyp?this.moov?("mdat"===t.type&&(this.prevIdr&&(this.detectedIdrInterval=r-this.prevIdr),this.prevIdr=r),this.prebufferFmp4.push({atom:t,time:r})):this.moov=t:this.ftyp=t;this.prebufferFmp4.length&&this.prebufferFmp4[0].time<r-e;)this.prebufferFmp4.shift();this.events.emit("atom",t)}},l=(0,i.createServer)(e=>{c(e).catch(e=>console.log(this.name,"fragmented mp4 session ended",e))}),m=await(0,s.listenZeroCluster)(l),h=["-f","mp4",...o,...a,"-movflags","frag_keyframe+empty_moov+default_base_moof","tcp://127.0.0.1:"+m],v=await(0,d.startRebroadcastSession)(t,{additionalOutputs:h,vcodec:a,acodec:o});if(this.detectedAcodec=v.inputAudioCodec||"",this.detectedVcodec=v.inputVideoCodec||"",this.detectedAcodec){if("aac"!==this.detectedAcodec){var S;console.error(this.name,"Detected audio codec was not AAC."),-1===(null===(S=this.name)||void 0===S?void 0:S.indexOf("pcm"))||r||g.a(this.name+" is using PCM audio. You will need to enable Reencode Audio in Rebroadcast Settings for this stream.")}}else console.warn(this.name,"no audio detected.");return"h264"!==this.detectedVcodec&&console.error(this.name+" video codec is not h264. If there are errors, try changing your camera's encoder output."),v.events.on("killed",()=>{l.close(),this.prebufferSession=void 0}),v.events.on("data",t=>{const r=Date.now();for(this.prebufferMpegTs.push({time:r,buffer:t});this.prebufferMpegTs.length&&this.prebufferMpegTs[0].time<r-e;)this.prebufferMpegTs.shift();this.events.emit("mpegts-data",t)}),v}async getVideoStream(e){var t;this.ensurePrebufferSession();const r=await this.prebufferSession;if(null!=e&&e.id&&e.id!==(null===(t=r.ffmpegInput.mediaStreamOptions)||void 0===t?void 0:t.id))return console.log(this.name,"rebroadcast session cant be used here",e),this.mixinDevice.getVideoStream(e);const o="true"===this.storage.getItem("sendKeyframe");if(!(null!=e&&e.prebuffer||o)){return f.createFFmpegMediaObject(r.ffmpegInput)}console.log(this.name,"prebuffer request started");const n=new i.Server(t=>{n.close();const r=(null==e?void 0:e.prebuffer)||(o?1.5*Math.max(4e3,this.detectedIdrInterval||4e3):0),i=Date.now();let s;if("mp4"===(null==e?void 0:e.container)){const e=e=>{t.write(Buffer.concat([e.header,e.data]))};this.ftyp&&e(this.ftyp),this.moov&&e(this.moov);const o=Date.now();let n=!0;for(const t of this.prebufferFmp4)t.time<o-r||n&&"moof"!==t.atom.type||(n=!1,e(t.atom));this.events.on("atom",e),s=()=>{console.log(this.name,"prebuffer request ended"),this.events.removeListener("atom",e),this.events.removeListener("killed",s),t.removeAllListeners(),t.destroy()}}else{const e=e=>{t.write(e)};for(const t of this.prebufferMpegTs)t.time<i-r||e(t.buffer);this.events.on("mpegts-data",e),s=()=>{console.log(this.name,"prebuffer request ended"),this.events.removeListener("mpegts-data",e),this.events.removeListener("killed",s),t.removeAllListeners(),t.destroy()}}this.events.once("killed",s),t.once("end",s),t.once("close",s),t.once("error",s)});setTimeout(()=>n.close(),3e4);const a=await(0,s.listenZeroCluster)(n),c=r.ffmpegInput.mediaStreamOptions?Object.assign({},r.ffmpegInput.mediaStreamOptions):void 0;if(c&&c.audio){"true"===this.storage.getItem("reencodeAudio")&&(c.audio={codec:"aac"})}const d={inputArguments:["-f","mp4"===(null==e?void 0:e.container)?"mp4":"mpegts","-i","tcp://127.0.0.1:"+a],mediaStreamOptions:c};console.log(this.name,"prebuffer ffmpeg input",d.inputArguments[3]);return f.createFFmpegMediaObject(d)}async getVideoStreamOptions(){const e=await this.mixinDevice.getVideoStreamOptions()||[];let t=e[0];return t||(t={},e.push(t)),t.prebuffer=parseInt(this.storage.getItem("prebufferDuration"))||15e3,e}release(){var e;console.log(this.name,"prebuffer releasing if started"),this.released=!0,null===(e=this.prebufferSession)||void 0===e||e.then(e=>{console.log(this.name,"prebuffer released"),e.kill()})}}class v extends n.ScryptedDeviceBase{async canMixin(e,t){return t.includes(n.ScryptedInterface.VideoCamera)?[n.ScryptedInterface.VideoCamera,n.ScryptedInterface.Settings]:null}async getMixin(e,t,r){return new h(e,t,r,this.nativeId)}async releaseMixin(e,t){t.release()}}var S=new v;t.default=S},function(e,t,r){"use strict";const o=r(8);class n{constructor(e){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())}}class i{constructor(e,t,r,o){this.mixinDevice=e,this.mixinDevice=e,this.mixinDeviceInterfaces=t,this._deviceState=r,this.mixinProviderNativeId=o}get storage(){return this._storage||(this._storage=deviceManager.getMixinStorage(this.id,this.mixinProviderNativeId)),this._storage}_lazyLoadDeviceState(){}release(){}}!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(o.ScryptedInterfaceProperty))Object.defineProperty(n.prototype,r,{set:t(r),get:e(r)}),Object.defineProperty(i.prototype,r,{set:t(r),get:e(r)})}();const s={ScryptedDeviceBase:n,MixinDeviceBase:i};Object.assign(s,o),e.exports=s,e.exports.default=s},function(e,t,r){"use strict";const o={};e.exports=o,e.exports.default=o,e.exports.ScryptedDeviceType={Builtin:"Builtin",Camera:"Camera",Fan:"Fan",Light:"Light",Switch:"Switch",Outlet:"Outlet",Sensor:"Sensor",Scene:"Scene",Program:"Program",Automation:"Automation",Vacuum:"Vacuum",Notifier:"Notifier",Thermostat:"Thermostat",Lock:"Lock",PasswordControl:"PasswordControl",Display:"Display",Speaker:"Speaker",Event:"Event",Entry:"Entry",Garage:"Garage",DeviceProvider:"DeviceProvider",DataSource:"DataSource",API:"API",Doorbell:"Doorbell",Irrigation:"Irrigation",Valve:"Valve",Unknown:"Unknown"},e.exports.TemperatureUnit={C:"C",F:"F"},e.exports.ThermostatMode={Off:"Off",Cool:"Cool",Heat:"Heat",HeatCool:"HeatCool",Auto:"Auto",FanOnly:"FanOnly",Purifier:"Purifier",Eco:"Eco",Dry:"Dry",On:"On"},e.exports.LockState={Locked:"Locked",Unlocked:"Unlocked",Jammed:"Jammed"},e.exports.MediaPlayerState={Idle:"Idle",Playing:"Playing",Paused:"Paused",Buffering:"Buffering"},e.exports.ScryptedInterface={ScryptedDevice:"ScryptedDevice",OnOff:"OnOff",Brightness:"Brightness",ColorSettingTemperature:"ColorSettingTemperature",ColorSettingRgb:"ColorSettingRgb",ColorSettingHsv:"ColorSettingHsv",Notifier:"Notifier",StartStop:"StartStop",Pause:"Pause",Dock:"Dock",TemperatureSetting:"TemperatureSetting",Thermometer:"Thermometer",HumiditySensor:"HumiditySensor",Camera:"Camera",VideoCamera:"VideoCamera",Intercom:"Intercom",Lock:"Lock",PasswordStore:"PasswordStore",Authenticator:"Authenticator",Scene:"Scene",Entry:"Entry",EntrySensor:"EntrySensor",DeviceProvider:"DeviceProvider",Battery:"Battery",Refresh:"Refresh",MediaPlayer:"MediaPlayer",Online:"Online",SoftwareUpdate:"SoftwareUpdate",BufferConverter:"BufferConverter",Settings:"Settings",BinarySensor:"BinarySensor",IntrusionSensor:"IntrusionSensor",PowerSensor:"PowerSensor",AudioSensor:"AudioSensor",MotionSensor:"MotionSensor",OccupancySensor:"OccupancySensor",FloodSensor:"FloodSensor",UltravioletSensor:"UltravioletSensor",LuminanceSensor:"LuminanceSensor",PositionSensor:"PositionSensor",MediaSource:"MediaSource",MessagingEndpoint:"MessagingEndpoint",OauthClient:"OauthClient",MixinProvider:"MixinProvider",HttpRequestHandler:"HttpRequestHandler",EngineIOHandler:"EngineIOHandler",PushHandler:"PushHandler",Program:"Program",Javascript:"Javascript"},e.exports.ScryptedInterfaceDescriptors={ScryptedDevice:{name:"ScryptedDevice",properties:["id","interfaces","mixins","info","name","providedInterfaces","providedName","providedRoom","providedType","providerId","room","type"],methods:["listen","setName","setRoom","setType"]},OnOff:{name:"OnOff",properties:["on"],methods:["turnOff","turnOn"]},Brightness:{name:"Brightness",properties:["brightness"],methods:["setBrightness"]},ColorSettingTemperature:{name:"ColorSettingTemperature",properties:["colorTemperature"],methods:["getTemperatureMaxK","getTemperatureMinK","setColorTemperature"]},ColorSettingRgb:{name:"ColorSettingRgb",properties:["rgb"],methods:["setRgb"]},ColorSettingHsv:{name:"ColorSettingHsv",properties:["hsv"],methods:["setHsv"]},Notifier:{name:"Notifier",properties:[],methods:["sendNotification"]},StartStop:{name:"StartStop",properties:["running"],methods:["start","stop"]},Pause:{name:"Pause",properties:["paused"],methods:["pause","resume"]},Dock:{name:"Dock",properties:["docked"],methods:["dock"]},TemperatureSetting:{name:"TemperatureSetting",properties:["thermostatAvailableModes","thermostatMode","thermostatSetpoint","thermostatSetpointHigh","thermostatSetpointLow"],methods:["setThermostatMode","setThermostatSetpoint","setThermostatSetpointHigh","setThermostatSetpointLow"]},Thermometer:{name:"Thermometer",properties:["temperature","temperatureUnit"],methods:[]},HumiditySensor:{name:"HumiditySensor",properties:["humidity"],methods:[]},Camera:{name:"Camera",properties:[],methods:["takePicture"]},VideoCamera:{name:"VideoCamera",properties:[],methods:["getVideoStream","getVideoStreamOptions"]},Intercom:{name:"Intercom",properties:[],methods:["startIntercom","stopIntercom"]},Lock:{name:"Lock",properties:["lockState"],methods:["lock","unlock"]},PasswordStore:{name:"PasswordStore",properties:[],methods:["addPassword","getPasswords","removePassword"]},Authenticator:{name:"Authenticator",properties:[],methods:["checkPassword"]},Scene:{name:"Scene",properties:[],methods:["activate","deactivate","isReversible"]},Entry:{name:"Entry",properties:[],methods:["closeEntry","openEntry"]},EntrySensor:{name:"EntrySensor",properties:["entryOpen"],methods:[]},DeviceProvider:{name:"DeviceProvider",properties:[],methods:["discoverDevices","getDevice"]},Battery:{name:"Battery",properties:["batteryLevel"],methods:[]},Refresh:{name:"Refresh",properties:[],methods:["getRefreshFrequency","refresh"]},MediaPlayer:{name:"MediaPlayer",properties:[],methods:["getMediaStatus","load","seek","skipNext","skipPrevious"]},Online:{name:"Online",properties:["online"],methods:[]},SoftwareUpdate:{name:"SoftwareUpdate",properties:["updateAvailable"],methods:["checkForUpdate","installUpdate"]},BufferConverter:{name:"BufferConverter",properties:["fromMimeType","toMimeType"],methods:["convert"]},Settings:{name:"Settings",properties:[],methods:["getSettings","putSetting"]},BinarySensor:{name:"BinarySensor",properties:["binaryState"],methods:[]},IntrusionSensor:{name:"IntrusionSensor",properties:["intrusionDetected"],methods:[]},PowerSensor:{name:"PowerSensor",properties:["powerDetected"],methods:[]},AudioSensor:{name:"AudioSensor",properties:["audioDetected"],methods:[]},MotionSensor:{name:"MotionSensor",properties:["motionDetected"],methods:[]},OccupancySensor:{name:"OccupancySensor",properties:["occupied"],methods:[]},FloodSensor:{name:"FloodSensor",properties:["flooded"],methods:[]},UltravioletSensor:{name:"UltravioletSensor",properties:["ultraviolet"],methods:[]},LuminanceSensor:{name:"LuminanceSensor",properties:["luminance"],methods:[]},PositionSensor:{name:"PositionSensor",properties:["position"],methods:[]},MediaSource:{name:"MediaSource",properties:[],methods:["getMedia"]},MessagingEndpoint:{name:"MessagingEndpoint",properties:[],methods:[]},OauthClient:{name:"OauthClient",properties:[],methods:["getOauthUrl","onOauthCallback"]},MixinProvider:{name:"MixinProvider",properties:[],methods:["canMixin","getMixin","releaseMixin"]},HttpRequestHandler:{name:"HttpRequestHandler",properties:[],methods:["onRequest"]},EngineIOHandler:{name:"EngineIOHandler",properties:[],methods:["onConnection"]},PushHandler:{name:"PushHandler",properties:[],methods:["onPush"]},Program:{name:"Program",properties:[],methods:["run"]},Javascript:{name:"Javascript",properties:[],methods:["eval"]}},e.exports.ScryptedInterfaceProperty={},Object.values(e.exports.ScryptedInterfaceDescriptors).map(e=>e.properties).flat().forEach(t=>e.exports.ScryptedInterfaceProperty[t]=t),e.exports.ScryptedMimeTypes={AcceptUrlParameter:"accept-url",Url:"text/x-uri",InsecureLocalUrl:"text/x-insecure-local-uri",LocalUrl:"text/x-local-uri",PushEndpoint:"text/x-push-endpoint",FFmpegInput:"x-scrypted/x-ffmpeg-input",RTCAVOffer:"x-scrypted/x-rtc-av-offer",RTCAVAnswer:"x-scrypted/x-rtc-av-answer"}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SettingsMixinDeviceBase=void 0;var o=function(e){if(e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=n();if(t&&t.has(e))return t.get(e);var r={},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var i in e)if(Object.prototype.hasOwnProperty.call(e,i)){var s=o?Object.getOwnPropertyDescriptor(e,i):null;s&&(s.get||s.set)?Object.defineProperty(r,i,s):r[i]=e[i]}r.default=e,t&&t.set(e,r);return r}(r(0));function n(){if("function"!=typeof WeakMap)return null;var e=new WeakMap;return n=function(){return e},e}const{deviceManager:i}=o.default;class s extends o.MixinDeviceBase{constructor(e,t,r){super(e,r.mixinDeviceInterfaces,t,r.providerNativeId),this.settingsGroup=r.group,this.settingsGroupKey=r.groupKey}async getSettings(){const e=(this.mixinDeviceInterfaces.includes(o.ScryptedInterface.Settings)?await this.mixinDevice.getSettings():[])||[],t=await this.getMixinSettings();for(const e of t)e.group=e.group||this.settingsGroup,e.key=this.settingsGroupKey+":"+e.key;return e.push(...t),e}async putSetting(e,t){var r;const n=this.settingsGroupKey+":";if(null==e||!e.startsWith(n))return this.mixinDevice.putSetting(e,t);await this.putMixinSetting(e.substring(n.length),t),null===(r=i.onMixinEvent)||void 0===r||r.call(i,this.id,this.mixinProviderNativeId,o.ScryptedInterface.Settings,null)}}t.SettingsMixinDeviceBase=s},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.parseResolution=p,t.parseVideoCodec=m,t.parseAudioCodec=f,t.startRebroadcastSession=async function(e,t){return new Promise(async r=>{let a,d=0,l=!0;const g=new s.EventEmitter;let h,v,S;function y(){l&&g.emit("killed"),l=!1,null==w||w.kill(),null==O||O.close(),null==P||P.close()}function b(){t.timeout&&(clearTimeout(a),a=setTimeout(y,t.timeout))}b();const P=(0,o.createServer)(e=>{d++,console.log("rebroadcast client",d),clearTimeout(a);const t=t=>{e.write(t)},r=()=>{e.removeAllListeners(),g.removeListener("data",t),d--,0===d&&b(),e.destroy()};g.on("data",t),e.on("end",r),e.on("close",r),e.on("error",r)}),M=await(0,i.listenZeroCluster)(P),O=(0,o.createServer)(t=>{O.close(),(async()=>{let e=[],r=0;for(;;){const o=t.read();if(!o){await(0,s.once)(t,"readable");continue}if(e.push(o),r+=o.length,r<188)continue;const n=Buffer.concat(e);if(71!=n[0])throw new Error("Invalid sync byte in mpeg-ts packet. Terminating stream.");const i=n.length%188,a=n.slice(0,n.length-i),c=n.slice(n.length-i);e=[c],r=c.length,g.emit("data",a)}})().catch(e=>{console.error("rebroadcast source ended",e),y()}),r({inputAudioCodec:h,inputVideoCodec:v,inputVideoResolution:S,events:g,resetActivityTimer:b,isActive:()=>l,kill:y,server:P,cp:w,ffmpegInput:{inputArguments:["-f","mpegts","-i","tcp://127.0.0.1:"+M],mediaStreamOptions:e.mediaStreamOptions}})}),I=await(0,i.listenZeroCluster)(O),x=e.inputArguments.slice();x.push(...t.additionalOutputs||[],"-f",t.outputFormat||"mpegts",...t.vcodec||[],...t.acodec||[],"tcp://127.0.0.1:"+I),console.log(x);const w=n.default.spawn(await u.getFFmpegPath(),x,{});(0,c.ffmpegLogInitialOutput)(console,w),w.on("exit",y),f(w).then(e=>h=e),m(w).then(e=>v=e),p(w).then(e=>S=e)})};var o=r(1),n=d(r(5)),i=r(2),s=r(3),a=d(r(0)),c=r(4);function d(e){return e&&e.__esModule?e:{default:e}}const{mediaManager:u}=a.default;async function p(e){return new Promise(t=>{const r=o=>{const n=o.toString(),i=/(([0-9]{2,5})x([0-9]{2,5}))/.exec(n);i&&(e.stdout.removeListener("data",r),e.stderr.removeListener("data",r),t(i))};e.stdout.on("data",r),e.stderr.on("data",r)})}async function l(e,t){return new Promise(r=>{const o=n=>{const i=n.toString(),s=i.indexOf(t+": ");if(-1!==s){const n=i.substring(s+t.length+1).trim();let a=n.indexOf(" ");const c=n.indexOf(",");-1!==a&&c<a&&(a=c),-1!==a&&(e.stdout.removeListener("data",o),e.stderr.removeListener("data",o),r(n.substring(0,a)))}};e.stdout.on("data",o),e.stderr.on("data",o)})}async function m(e){return l(e,"Video")}async function f(e){return l(e,"Audio")}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ffmpegLogInitialOutput=function(e,t,r){var n,i;function s(e){const n=i=>{const s=i.toString();for(const e of o)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",n),void t.stderr.removeListener("data",n);e(s)};return n}null===(n=t.stdout)||void 0===n||n.on("data",s(e.log)),null===(i=t.stderr)||void 0===i||i.on("data",s(e.error))},t.probeVideoCamera=async function(e){let t;try{t=await e.getVideoStreamOptions()||[]}catch(e){}const r=t&&t.length&&null===t[0].audio;return{options:t,noAudio:r}};const o=["decode_slice_header error","no frame!","non-existing PPS"]},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.parseFragmentedMP4=p,t.startFFMPegFragmetedMP4Session=async function(e,t,r){return new Promise(async s=>{const a=(0,o.createServer)(e=>{a.close(),s({socket:e,cp:m,generator:p(e)})}),d=await(0,i.listenZeroCluster)(a),l=e.inputArguments.slice();l.push("-f","mp4"),l.push(...r),l.push(...t),l.push("-movflags","frag_keyframe+empty_moov+default_base_moof","tcp://127.0.0.1:"+d),console.log(l);const m=n.default.spawn(await u.getFFmpegPath(),l,{});(0,c.ffmpegLogInitialOutput)(console,m)})};var o=r(1),n=d(r(5)),i=r(2),s=r(13),a=d(r(0)),c=r(4);function d(e){return e&&e.__esModule?e:{default:e}}const{mediaManager:u}=a.default;async function*p(e){for(;;){const t=await(0,s.readLength)(e,8),r=t.readInt32BE(0)-8,o=t.slice(4).toString(),n=await(0,s.readLength)(e,r);yield{header:t,length:r,type:o,data:n}}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.readLength=async function(e,t){if(!t)return Buffer.alloc(0);{const r=e.read(t);if(r)return r}return new Promise((r,o)=>{const n=()=>{const o=e.read(t);o&&(s(),r(o))},i=()=>{s(),o(new Error(`stream ended during read for minimum ${t} bytes`))},s=()=>{e.removeListener("readable",n),e.removeListener("end",i)};e.on("readable",n),e.on("end",i)})}}]));
1
+ !function(e,t){for(var r in t)e[r]=t[r]}(window,function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=5)}([function(e,t,r){"use strict";var n=r(6);try{n=Object.assign(n,{log:deviceManager.getDeviceLogger(void 0),deviceManager:deviceManager,endpointManager:endpointManager,mediaManager:mediaManager,systemManager:systemManager,pluginHostAPI:pluginHostAPI})}catch(e){console.error("sdk initialization error, import @scrypted/sdk/types instead",e)}e.exports=n,e.exports.default=n},function(e,t){e.exports=require("events")},function(e,t){e.exports=require("net")},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.listenZeroCluster=async function(e){for(;;){const t=1e4+Math.round(3e4*Math.random());e.listen(t);try{return await(0,n.once)(e,"listening"),e.address().port}catch(e){}}};var n=r(1)},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=r(11);Object.keys(n).forEach((function(e){"default"!==e&&"__esModule"!==e&&(e in t&&t[e]===n[e]||Object.defineProperty(t,e,{enumerable:!0,get:function(){return n[e]}}))}))},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n,o=function(e){if(e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=f();if(t&&t.has(e))return t.get(e);var r={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var o in e)if(Object.prototype.hasOwnProperty.call(e,o)){var i=n?Object.getOwnPropertyDescriptor(e,o):null;i&&(i.get||i.set)?Object.defineProperty(r,o,i):r[o]=e[o]}r.default=e,t&&t.set(e,r);return r}(r(0)),i=r(2),s=r(3),a=(n=r(1))&&n.__esModule?n:{default:n},c=r(8),d=r(9),u=r(4),p=r(12),l=r(14);function f(){if("function"!=typeof WeakMap)return null;var e=new WeakMap;return f=function(){return e},e}function m(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:g,log:h,systemManager:v,deviceManager:y}=o.default;class S extends c.SettingsMixinDeviceBase{constructor(e,t,r,n){super(e,r,{providerNativeId:n,mixinDeviceInterfaces:t,group:"Rebroadcast and Prebuffer Settings",groupKey:"prebuffer"}),m(this,"prebuffers",{mp4:[],mpegts:[]}),m(this,"events",new a.default),m(this,"released",!1),m(this,"detectedIdrInterval",0),m(this,"prevIdr",0),m(this,"unexpectedPCM",!1),console.log(this.name+" prebuffer session starting in 10 seconds"),setTimeout(()=>this.ensurePrebufferSession(),1e4)}async getMixinSettings(){var e,t,r,n,o,i,s;const a=[];return a.push({title:"Prebuffer Duration",description:"Duration of the prebuffer in milliseconds.",type:"number",key:"prebufferDuration",value:this.storage.getItem("prebufferDuration")||15e3.toString()},{title:"Start at Previous Keyframe",description:"Start live streams from the previous key frame. Improves startup time.",type:"boolean",key:"sendKeyframe",value:("false"!==this.storage.getItem("sendKeyframe")).toString()},{title:"Reencode Audio",description:"Reencode the audio (necessary if camera outputs PCM).",type:"boolean",key:"reencodeAudio",value:("true"===this.storage.getItem("reencodeAudio")).toString()},{group:"Media Information",title:"Detected Resolution",readonly:!0,key:"detectedAcodec",value:""+((null===(e=this.session)||void 0===e||null===(t=e.inputVideoResolution)||void 0===t?void 0:t[0])||"unknown")},{group:"Media Information",title:"Detected Video/Audio Codecs",readonly:!0,key:"detectedVcodec",value:((null===(r=this.session)||void 0===r||null===(n=r.inputVideoCodec)||void 0===n?void 0:n.toString())||"unknown")+"/"+((null===(o=this.session)||void 0===o||null===(i=o.inputAudioCodec)||void 0===i?void 0:i.toString())||"unknown")},{group:"Media Information",title:"Detected Keyframe Interval",description:"Currently detected keyframe interval. This value may vary based on the stream behavior.",readonly:!0,key:"detectedIdr",value:(null===(s=this.detectedIdrInterval)||void 0===s?void 0:s.toString())||"none"}),a}async putMixinSetting(e,t){var r;this.storage.setItem(e,t.toString()),null===(r=this.prebufferSession)||void 0===r||r.then(e=>e.kill())}ensurePrebufferSession(){this.prebufferSession||this.released||(console.log(this.name+" prebuffer session started"),this.prebufferSession=this.startPrebufferSession())}async startPrebufferSession(){this.prebuffers.mp4=[],this.prebuffers.mpegts=[];const e=parseInt(this.storage.getItem("prebufferDuration"))||15e3,t=JSON.parse((await g.convertMediaObjectToBuffer(await this.mixinDevice.getVideoStream(),o.ScryptedMimeTypes.FFmpegInput)).toString()),r="true"===this.storage.getItem("reencodeAudio")||this.unexpectedPCM;let n;n=(await(0,u.probeVideoCamera)(this.mixinDevice)).noAudio?["-an"]:r?[]:["-acodec","copy"];const i=["-vcodec","copy"],s=await(0,d.startRebroadcastSession)(t,{parsers:{mp4:(0,p.createFragmentedMp4Parser)({vcodec:i,acodec:n}),mpegts:(0,p.createMpegTsParser)({vcodec:i,acodec:n})}});this.session=s,this.session.inputAudioCodec?"aac"!==this.session.inputAudioCodec&&(console.error(this.name,"Detected audio codec was not AAC."),s.inputAudioCodec&&-1!==s.inputAudioCodec.indexOf("pcm")&&!r&&(h.a(this.name+" is using PCM audio and will be reencoded. Enable Reencode Audio in Rebroadcast Settings to disable this alert."),this.unexpectedPCM=!0)):console.warn(this.name,"no audio detected."),"h264"!==this.session.inputVideoCodec&&console.error(this.name+" video codec is not h264. If there are errors, try changing your camera's encoder output."),s.events.on("killed",()=>{this.prebufferSession=void 0});for(const t of["mpegts","mp4"]){const r=t+"-data",n=this.prebuffers[t];s.events.on(r,t=>{const o=Date.now();for("mdat"===t.type&&(this.prevIdr&&(this.detectedIdrInterval=o-this.prevIdr),this.prevIdr=o),n.push({time:o,chunk:t});n.length&&n[0].time<o-e;)n.shift();this.events.emit(r,t)})}return s}async getVideoStream(e){var t;this.ensurePrebufferSession();const r=await this.prebufferSession;if(null!=e&&e.id&&e.id!==(null===(t=r.ffmpegInputs.mpegts.mediaStreamOptions)||void 0===t?void 0:t.id))return console.log(this.name,"rebroadcast session cant be used here",e),this.mixinDevice.getVideoStream(e);const n="false"!==this.storage.getItem("sendKeyframe");if(!(null!=e&&e.prebuffer||n)){return g.createFFmpegMediaObject(r.ffmpegInputs.mpegts)}console.log(this.name,"prebuffer request started");const o=(null==e?void 0:e.container)||"mpegts",a=o+"-data",c=this.prebuffers[o],d=new i.Server(t=>{d.close();const r=(null==e?void 0:e.prebuffer)||(n?1.5*Math.max(4e3,this.detectedIdrInterval||4e3):0),o=Date.now();let i,s=!0;const u=e=>{s&&(s=!1,e.startStream&&t.write(e.startStream)),t.write(e.chunk)};for(const e of c)e.time<o-r||u(e.chunk);this.events.on(a,u),i=()=>{console.log(this.name,"prebuffer request ended"),this.events.removeListener(a,u),this.events.removeListener("killed",i),t.removeAllListeners(),t.destroy()},this.events.once("killed",i),t.once("end",i),t.once("close",i),t.once("error",i)});setTimeout(()=>d.close(),3e4);const u=await(0,s.listenZeroCluster)(d),p=r.ffmpegInputs[o].mediaStreamOptions?Object.assign({},r.ffmpegInputs[o].mediaStreamOptions):void 0;if(p&&p.audio){"true"===this.storage.getItem("reencodeAudio")&&(p.audio={codec:"aac"})}const l={inputArguments:["-f",o,"-i","tcp://127.0.0.1:"+u],mediaStreamOptions:p};console.log(this.name,"prebuffer ffmpeg input",l.inputArguments[3]);return g.createFFmpegMediaObject(l)}async getVideoStreamOptions(){const e=await this.mixinDevice.getVideoStreamOptions()||[];let t=e[0];return t||(t={},e.push(t)),t.prebuffer=parseInt(this.storage.getItem("prebufferDuration"))||15e3,e}release(){var e;console.log(this.name,"prebuffer releasing if started"),this.released=!0,null===(e=this.prebufferSession)||void 0===e||e.then(e=>{console.log(this.name,"prebuffer released"),e.kill()})}}class b extends l.AutoenableMixinProvider{constructor(e){super(e);for(const e of Object.keys(v.getSystemState())){var t;const r=v.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(()=>y.requestRestart(),r)}async canMixin(e,t){return t.includes(o.ScryptedInterface.VideoCamera)?[o.ScryptedInterface.VideoCamera,o.ScryptedInterface.Settings]:null}async getMixin(e,t,r){return this.setHasEnabledMixin(r.id),new S(e,t,r,this.nativeId)}async releaseMixin(e,t){t.release()}}var M=new b;t.default=M},function(e,t,r){"use strict";const n=r(7);class o{constructor(e){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())}}class i{constructor(e,t,r,n){this.mixinDevice=e,this.mixinDevice=e,this.mixinDeviceInterfaces=t,this._deviceState=r,this.mixinProviderNativeId=n}get storage(){return this._storage||(this._storage=deviceManager.getMixinStorage(this.id,this.mixinProviderNativeId)),this._storage}_lazyLoadDeviceState(){}release(){}}!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(o.prototype,r,{set:t(r),get:e(r)}),Object.defineProperty(i.prototype,r,{set:t(r),get:e(r)})}();const s={ScryptedDeviceBase:o,MixinDeviceBase:i};Object.assign(s,n),e.exports=s,e.exports.default=s},function(e,t,r){"use strict";const n={};e.exports=n,e.exports.default=n,e.exports.ScryptedDeviceType={Builtin:"Builtin",Camera:"Camera",Fan:"Fan",Light:"Light",Switch:"Switch",Outlet:"Outlet",Sensor:"Sensor",Scene:"Scene",Program:"Program",Automation:"Automation",Vacuum:"Vacuum",Notifier:"Notifier",Thermostat:"Thermostat",Lock:"Lock",PasswordControl:"PasswordControl",Display:"Display",Speaker:"Speaker",Event:"Event",Entry:"Entry",Garage:"Garage",DeviceProvider:"DeviceProvider",DataSource:"DataSource",API:"API",Doorbell:"Doorbell",Irrigation:"Irrigation",Valve:"Valve",Unknown:"Unknown"},e.exports.TemperatureUnit={C:"C",F:"F"},e.exports.ThermostatMode={Off:"Off",Cool:"Cool",Heat:"Heat",HeatCool:"HeatCool",Auto:"Auto",FanOnly:"FanOnly",Purifier:"Purifier",Eco:"Eco",Dry:"Dry",On:"On"},e.exports.LockState={Locked:"Locked",Unlocked:"Unlocked",Jammed:"Jammed"},e.exports.MediaPlayerState={Idle:"Idle",Playing:"Playing",Paused:"Paused",Buffering:"Buffering"},e.exports.ScryptedInterface={ScryptedDevice:"ScryptedDevice",OnOff:"OnOff",Brightness:"Brightness",ColorSettingTemperature:"ColorSettingTemperature",ColorSettingRgb:"ColorSettingRgb",ColorSettingHsv:"ColorSettingHsv",Notifier:"Notifier",StartStop:"StartStop",Pause:"Pause",Dock:"Dock",TemperatureSetting:"TemperatureSetting",Thermometer:"Thermometer",HumiditySensor:"HumiditySensor",Camera:"Camera",VideoCamera:"VideoCamera",Intercom:"Intercom",Lock:"Lock",PasswordStore:"PasswordStore",Authenticator:"Authenticator",Scene:"Scene",Entry:"Entry",EntrySensor:"EntrySensor",DeviceProvider:"DeviceProvider",Battery:"Battery",Refresh:"Refresh",MediaPlayer:"MediaPlayer",Online:"Online",SoftwareUpdate:"SoftwareUpdate",BufferConverter:"BufferConverter",Settings:"Settings",BinarySensor:"BinarySensor",IntrusionSensor:"IntrusionSensor",PowerSensor:"PowerSensor",AudioSensor:"AudioSensor",MotionSensor:"MotionSensor",OccupancySensor:"OccupancySensor",FloodSensor:"FloodSensor",UltravioletSensor:"UltravioletSensor",LuminanceSensor:"LuminanceSensor",PositionSensor:"PositionSensor",MediaSource:"MediaSource",MessagingEndpoint:"MessagingEndpoint",OauthClient:"OauthClient",MixinProvider:"MixinProvider",HttpRequestHandler:"HttpRequestHandler",EngineIOHandler:"EngineIOHandler",PushHandler:"PushHandler",Program:"Program",Scriptable:"Scriptable"},e.exports.ScryptedInterfaceDescriptors={ScryptedDevice:{name:"ScryptedDevice",properties:["id","interfaces","mixins","info","name","providedInterfaces","providedName","providedRoom","providedType","providerId","room","type"],methods:["listen","setName","setRoom","setType"]},OnOff:{name:"OnOff",properties:["on"],methods:["turnOff","turnOn"]},Brightness:{name:"Brightness",properties:["brightness"],methods:["setBrightness"]},ColorSettingTemperature:{name:"ColorSettingTemperature",properties:["colorTemperature"],methods:["getTemperatureMaxK","getTemperatureMinK","setColorTemperature"]},ColorSettingRgb:{name:"ColorSettingRgb",properties:["rgb"],methods:["setRgb"]},ColorSettingHsv:{name:"ColorSettingHsv",properties:["hsv"],methods:["setHsv"]},Notifier:{name:"Notifier",properties:[],methods:["sendNotification"]},StartStop:{name:"StartStop",properties:["running"],methods:["start","stop"]},Pause:{name:"Pause",properties:["paused"],methods:["pause","resume"]},Dock:{name:"Dock",properties:["docked"],methods:["dock"]},TemperatureSetting:{name:"TemperatureSetting",properties:["thermostatAvailableModes","thermostatMode","thermostatSetpoint","thermostatSetpointHigh","thermostatSetpointLow"],methods:["setThermostatMode","setThermostatSetpoint","setThermostatSetpointHigh","setThermostatSetpointLow"]},Thermometer:{name:"Thermometer",properties:["temperature","temperatureUnit"],methods:[]},HumiditySensor:{name:"HumiditySensor",properties:["humidity"],methods:[]},Camera:{name:"Camera",properties:[],methods:["takePicture"]},VideoCamera:{name:"VideoCamera",properties:[],methods:["getVideoStream","getVideoStreamOptions"]},Intercom:{name:"Intercom",properties:[],methods:["startIntercom","stopIntercom"]},Lock:{name:"Lock",properties:["lockState"],methods:["lock","unlock"]},PasswordStore:{name:"PasswordStore",properties:[],methods:["addPassword","getPasswords","removePassword"]},Authenticator:{name:"Authenticator",properties:[],methods:["checkPassword"]},Scene:{name:"Scene",properties:[],methods:["activate","deactivate","isReversible"]},Entry:{name:"Entry",properties:[],methods:["closeEntry","openEntry"]},EntrySensor:{name:"EntrySensor",properties:["entryOpen"],methods:[]},DeviceProvider:{name:"DeviceProvider",properties:[],methods:["discoverDevices","getDevice"]},Battery:{name:"Battery",properties:["batteryLevel"],methods:[]},Refresh:{name:"Refresh",properties:[],methods:["getRefreshFrequency","refresh"]},MediaPlayer:{name:"MediaPlayer",properties:[],methods:["getMediaStatus","load","seek","skipNext","skipPrevious"]},Online:{name:"Online",properties:["online"],methods:[]},SoftwareUpdate:{name:"SoftwareUpdate",properties:["updateAvailable"],methods:["checkForUpdate","installUpdate"]},BufferConverter:{name:"BufferConverter",properties:["fromMimeType","toMimeType"],methods:["convert"]},Settings:{name:"Settings",properties:[],methods:["getSettings","putSetting"]},BinarySensor:{name:"BinarySensor",properties:["binaryState"],methods:[]},IntrusionSensor:{name:"IntrusionSensor",properties:["intrusionDetected"],methods:[]},PowerSensor:{name:"PowerSensor",properties:["powerDetected"],methods:[]},AudioSensor:{name:"AudioSensor",properties:["audioDetected"],methods:[]},MotionSensor:{name:"MotionSensor",properties:["motionDetected"],methods:[]},OccupancySensor:{name:"OccupancySensor",properties:["occupied"],methods:[]},FloodSensor:{name:"FloodSensor",properties:["flooded"],methods:[]},UltravioletSensor:{name:"UltravioletSensor",properties:["ultraviolet"],methods:[]},LuminanceSensor:{name:"LuminanceSensor",properties:["luminance"],methods:[]},PositionSensor:{name:"PositionSensor",properties:["position"],methods:[]},MediaSource:{name:"MediaSource",properties:[],methods:["getMedia"]},MessagingEndpoint:{name:"MessagingEndpoint",properties:[],methods:[]},OauthClient:{name:"OauthClient",properties:[],methods:["getOauthUrl","onOauthCallback"]},MixinProvider:{name:"MixinProvider",properties:[],methods:["canMixin","getMixin","releaseMixin"]},HttpRequestHandler:{name:"HttpRequestHandler",properties:[],methods:["onRequest"]},EngineIOHandler:{name:"EngineIOHandler",properties:[],methods:["onConnection"]},PushHandler:{name:"PushHandler",properties:[],methods:["onPush"]},Program:{name:"Program",properties:[],methods:["run"]},Scriptable:{name:"Scriptable",properties:[],methods:["saveScript","loadScripts","eval"]}},e.exports.ScryptedInterfaceProperty={},Object.values(e.exports.ScryptedInterfaceDescriptors).map(e=>e.properties).flat().forEach(t=>e.exports.ScryptedInterfaceProperty[t]=t),e.exports.ScryptedMimeTypes={AcceptUrlParameter:"accept-url",Url:"text/x-uri",InsecureLocalUrl:"text/x-insecure-local-uri",LocalUrl:"text/x-local-uri",PushEndpoint:"text/x-push-endpoint",FFmpegInput:"x-scrypted/x-ffmpeg-input",RTCAVOffer:"x-scrypted/x-rtc-av-offer",RTCAVAnswer:"x-scrypted/x-rtc-av-answer"}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SettingsMixinDeviceBase=void 0;var n=function(e){if(e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=o();if(t&&t.has(e))return t.get(e);var r={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var i in e)if(Object.prototype.hasOwnProperty.call(e,i)){var s=n?Object.getOwnPropertyDescriptor(e,i):null;s&&(s.get||s.set)?Object.defineProperty(r,i,s):r[i]=e[i]}r.default=e,t&&t.set(e,r);return r}(r(0));function o(){if("function"!=typeof WeakMap)return null;var e=new WeakMap;return o=function(){return e},e}const{deviceManager:i}=n.default;class s extends n.MixinDeviceBase{constructor(e,t,r){super(e,r.mixinDeviceInterfaces,t,r.providerNativeId),this.settingsGroup=r.group,this.settingsGroupKey=r.groupKey}async getSettings(){const e=(this.mixinDeviceInterfaces.includes(n.ScryptedInterface.Settings)?await this.mixinDevice.getSettings():[])||[],t=await this.getMixinSettings();for(const e of t)e.group=e.group||this.settingsGroup,e.key=this.settingsGroupKey+":"+e.key;return e.push(...t),e}async putSetting(e,t){var r;const o=this.settingsGroupKey+":";if(null==e||!e.startsWith(o))return this.mixinDevice.putSetting(e,t);await this.putMixinSetting(e.substring(o.length),t),null===(r=i.onMixinEvent)||void 0===r||r.call(i,this.id,this.mixinProviderNativeId,n.ScryptedInterface.Settings,null)}}t.SettingsMixinDeviceBase=s},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.parseResolution=p,t.parseVideoCodec=f,t.parseAudioCodec=m,t.startRebroadcastSession=async function(e,t){let r,a=0,d=!0;const l=new s.EventEmitter;let g,h,v;function y(){d&&l.emit("killed"),d=!1,null==I||I.kill();for(const e of P)null==e||e.close()}function S(){t.timeout&&(clearTimeout(r),r=setTimeout(y,t.timeout))}S();const b={},M=e.inputArguments.slice(),P=[];let x;const O=new Promise(e=>x=e);for(const o of Object.keys(t.parsers)){const s=t.parsers[o],c=o+"-data",d=(0,n.createServer)(e=>{a++,console.log("rebroadcast client",a),clearTimeout(r);let t=!0;const n=r=>{t&&(t=!1,r.startStream&&e.write(r.startStream)),e.write(r.chunk)},o=()=>{e.removeAllListeners(),l.removeListener(c,n),a--,0===a&&S(),e.destroy()};l.on(c,n),e.on("end",o),e.on("close",o),e.on("error",o)});P.push(d);const u=await(0,i.listenZeroCluster)(d);b[o]={inputArguments:["-f",o,"-i","tcp://127.0.0.1:"+u],mediaStreamOptions:e.mediaStreamOptions};const p=(0,n.createServer)(async e=>{p.close(),x(e);try{const t=o+"-data";for await(const r of s.parse(e))l.emit(t,r)}catch(e){console.error("rebroadcast parse error",e),y()}});P.push(p);const f=await(0,i.listenZeroCluster)(p);M.push(...s.outputArguments,"tcp://127.0.0.1:"+f)}console.log(M);const I=o.default.spawn(await u.getFFmpegPath(),M);return(0,c.ffmpegLogInitialOutput)(console,I),I.on("exit",y),m(I).then(e=>g=e),f(I).then(e=>h=e),p(I).then(e=>v=e),await O,{inputAudioCodec:g,inputVideoCodec:h,inputVideoResolution:v,events:l,resetActivityTimer:S,isActive:()=>d,kill:y,servers:P,cp:I,ffmpegInputs:b}};var n=r(2),o=d(r(10)),i=r(3),s=r(1),a=d(r(0)),c=r(4);function d(e){return e&&e.__esModule?e:{default:e}}const{mediaManager:u}=a.default;async function p(e){return new Promise(t=>{const r=n=>{const o=n.toString(),i=/(([0-9]{2,5})x([0-9]{2,5}))/.exec(o);i&&(e.stdout.removeListener("data",r),e.stderr.removeListener("data",r),t(i))};e.stdout.on("data",r),e.stderr.on("data",r)})}async function l(e,t){return new Promise(r=>{const n=o=>{const i=o.toString(),s=i.indexOf(t+": ");if(-1!==s){const o=i.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",n),e.stderr.removeListener("data",n),r(o.substring(0,a)))}};e.stdout.on("data",n),e.stderr.on("data",n)})}async function f(e){return l(e,"Video")}async function m(e){return l(e,"Audio")}},function(e,t){e.exports=require("child_process")},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ffmpegLogInitialOutput=function(e,t,r){var o,i;function s(e){const o=i=>{const s=i.toString();for(const e of n)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===(i=t.stderr)||void 0===i||i.on("data",s(e.error)),t.on("exit",()=>e.log("ffmpeg exited"))},t.probeVideoCamera=async function(e){let t;try{t=await e.getVideoStreamOptions()||[]}catch(e){}const r=t&&t.length&&null===t[0].audio;return{options:t,noAudio:r}};const n=["decode_slice_header error","no frame!","non-existing PPS"]},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.createMpegTsParser=function(e){return{container:"mpegts",outputArguments:["-f","mpegts",...(null==e?void 0:e.vcodec)||[],...(null==e?void 0:e.acodec)||[]],async*parse(e){let t=[],r=0;for(;;){const o=e.read();if(!o){await(0,n.once)(e,"readable");continue}if(t.push(o),r+=o.length,r<188)continue;const i=Buffer.concat(t);if(71!=i[0])throw new Error("Invalid sync byte in mpeg-ts packet. Terminating stream.");const s=i.length%188,a=i.slice(0,i.length-s),c=i.slice(i.length-s);t=[c],r=c.length,yield{chunk:a}}}}},t.parseFragmentedMP4=i,t.createFragmentedMp4Parser=function(e){return{container:"mp4",outputArguments:["-f","mp4",...(null==e?void 0:e.vcodec)||[],...(null==e?void 0:e.acodec)||[],"-movflags","frag_keyframe+empty_moov+default_base_moof"],async*parse(e){const t=i(e);let r,n,o;for await(const e of t)r?n||(n=e):r=e,yield{startStream:o,chunk:Buffer.concat([e.header,e.data]),type:e.type},r&&n&&!o&&(o=Buffer.concat([r.header,r.data,n.header,n.data]))}}};var n=r(1),o=r(13);async function*i(e){for(;;){const t=await(0,o.readLength)(e,8),r=t.readInt32BE(0)-8,n=t.slice(4).toString(),i=await(0,o.readLength)(e,r);yield{header:t,length:r,type:n,data:i}}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.readLength=async function(e,t){if(!t)return Buffer.alloc(0);{const r=e.read(t);if(r)return r}return new Promise((r,n)=>{const o=()=>{const n=e.read(t);n&&(s(),r(n))},i=()=>{s(),n(new Error(`stream ended during read for minimum ${t} bytes`))},s=()=>{e.removeListener("readable",o),e.removeListener("end",i)};e.on("readable",o),e.on("end",i)})}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.AutoenableMixinProvider=void 0;var n=function(e){if(e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=o();if(t&&t.has(e))return t.get(e);var r={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var i in e)if(Object.prototype.hasOwnProperty.call(e,i)){var s=n?Object.getOwnPropertyDescriptor(e,i):null;s&&(s.get||s.set)?Object.defineProperty(r,i,s):r[i]=e[i]}r.default=e,t&&t.set(e,r);return r}(r(0));function o(){if("function"!=typeof WeakMap)return null;var e=new WeakMap;return o=function(){return e},e}const{systemManager:i}=n.default;class s extends n.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=i.getComponent("plugins"),i.listen(async(e,t,r)=>{t.eventInterface!==n.ScryptedInterface.ScryptedDevice||t.property||this.maybeEnableMixin(e)});for(const e of Object.keys(i.getSystemState())){const t=i.getDeviceById(e);this.maybeEnableMixin(t)}}async maybeEnableMixin(e){var t;if(!e||null!==(t=e.mixins)&&void 0!==t&&t.includes(this.id))return;if(this.hasEnabledMixin[e.id])return;if(!await this.canMixin(e.type,e.interfaces))return;this.log.i("auto enabling mixin for "+e.name);const r=e.mixins||[];r.push(this.id);const n=await this.pluginsComponent;await n.setMixins(e.id,r)}setHasEnabledMixin(e){this.hasEnabledMixin[e]||(this.hasEnabledMixin[e]=!0,this.storage.setItem("hasEnabledMixin",JSON.stringify(this.hasEnabledMixin)))}}t.AutoenableMixinProvider=s}]));
2
+ //# sourceMappingURL=main.nodejs.js.map
package/dist/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrypted/prebuffer-mixin",
3
- "version": "0.1.44",
3
+ "version": "0.1.48",
4
4
  "description": "Rebroadcast and Prebuffer for VideoCameras.",
5
5
  "author": "Scrypted",
6
6
  "license": "Apache-2.0",
package/src/main.ts CHANGED
@@ -1,15 +1,16 @@
1
1
 
2
2
  import { MixinProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, MediaObject, VideoCamera, MediaStreamOptions, Settings, Setting, ScryptedMimeTypes, FFMpegInput } from '@scrypted/sdk';
3
3
  import sdk from '@scrypted/sdk';
4
- import { createServer, Server, Socket } from 'net';
4
+ import { Server } from 'net';
5
5
  import { listenZeroCluster } from '@scrypted/common/src/listen-cluster';
6
6
  import EventEmitter from 'events';
7
7
  import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
8
8
  import { FFMpegRebroadcastSession, startRebroadcastSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
9
9
  import { probeVideoCamera } from '@scrypted/common/src/media-helpers';
10
- import { MP4Atom, parseFragmentedMP4 } from '@scrypted/common/src/ffmpeg-mp4-parser-session';
10
+ import { createMpegTsParser, createFragmentedMp4Parser, MP4Atom, StreamChunk } from '@scrypted/common/src/stream-parser';
11
+ import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
11
12
 
12
- const { mediaManager, log } = sdk;
13
+ const { mediaManager, log, systemManager, deviceManager } = sdk;
13
14
 
14
15
  const defaultPrebufferDuration = 15000;
15
16
  const PREBUFFER_DURATION_MS = 'prebufferDuration';
@@ -17,29 +18,25 @@ const SEND_KEYFRAME = 'sendKeyframe';
17
18
  const REENCODE_AUDIO = 'reencodeAudio';
18
19
  const REENCODE_VIDEO = 'reencodeVideo';
19
20
 
20
- interface PrebufferMpegTs {
21
- buffer: Buffer;
21
+ interface PrebufferStreamChunk {
22
+ chunk: StreamChunk;
22
23
  time: number;
23
24
  }
24
25
 
25
- interface PrebufferFmp4 {
26
- atom: MP4Atom;
27
- time: number;
28
- }
29
-
30
-
31
26
  class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements VideoCamera, Settings {
32
27
  prebufferSession: Promise<FFMpegRebroadcastSession>;
33
- prebufferMpegTs: PrebufferMpegTs[] = [];
34
- prebufferFmp4: PrebufferFmp4[] = [];
28
+ prebuffers = {
29
+ mp4: [],
30
+ mpegts: [],
31
+ };
35
32
  events = new EventEmitter();
36
33
  released = false;
37
34
  ftyp: MP4Atom;
38
35
  moov: MP4Atom;
36
+ session: FFMpegRebroadcastSession;
39
37
  detectedIdrInterval = 0;
40
- detectedVcodec = '';
41
- detectedAcodec = '';
42
38
  prevIdr = 0;
39
+ unexpectedPCM = false;
43
40
 
44
41
  constructor(mixinDevice: VideoCamera & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string) {
45
42
  super(mixinDevice, mixinDeviceState, {
@@ -70,7 +67,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
70
67
  description: 'Start live streams from the previous key frame. Improves startup time.',
71
68
  type: 'boolean',
72
69
  key: SEND_KEYFRAME,
73
- value: (this.storage.getItem(SEND_KEYFRAME) === 'true').toString(),
70
+ value: (this.storage.getItem(SEND_KEYFRAME) !== 'false').toString(),
74
71
  },
75
72
  {
76
73
  title: 'Reencode Audio',
@@ -81,17 +78,17 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
81
78
  },
82
79
  {
83
80
  group: 'Media Information',
84
- title: 'Detected Video Codec',
81
+ title: 'Detected Resolution',
85
82
  readonly: true,
86
- key: 'detectedVcodec',
87
- value: this.detectedVcodec.toString() || 'none',
83
+ key: 'detectedAcodec',
84
+ value: `${this.session?.inputVideoResolution?.[0] || "unknown"}`
88
85
  },
89
86
  {
90
87
  group: 'Media Information',
91
- title: 'Detected Audio Codec',
88
+ title: 'Detected Video/Audio Codecs',
92
89
  readonly: true,
93
- key: 'detectedAcodec',
94
- value: this.detectedAcodec.toString() || 'none',
90
+ key: 'detectedVcodec',
91
+ value: (this.session?.inputVideoCodec?.toString() || 'unknown') + '/' + (this.session?.inputAudioCodec?.toString() || 'unknown'),
95
92
  },
96
93
  {
97
94
  group: 'Media Information',
@@ -99,7 +96,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
99
96
  description: "Currently detected keyframe interval. This value may vary based on the stream behavior.",
100
97
  readonly: true,
101
98
  key: 'detectedIdr',
102
- value: this.detectedIdrInterval.toString() || 'none',
99
+ value: this.detectedIdrInterval?.toString() || 'none',
103
100
  },
104
101
  );
105
102
  return settings;
@@ -118,11 +115,12 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
118
115
  }
119
116
 
120
117
  async startPrebufferSession() {
121
- this.prebufferMpegTs = [];
118
+ this.prebuffers.mp4 = [];
119
+ this.prebuffers.mpegts = [];
122
120
  const prebufferDurationMs = parseInt(this.storage.getItem(PREBUFFER_DURATION_MS)) || defaultPrebufferDuration;
123
121
  const ffmpegInput = JSON.parse((await mediaManager.convertMediaObjectToBuffer(await this.mixinDevice.getVideoStream(), ScryptedMimeTypes.FFmpegInput)).toString()) as FFMpegInput;
124
122
 
125
- const reencodeAudio = this.storage.getItem(REENCODE_AUDIO) === 'true';
123
+ const reencodeAudio = this.storage.getItem(REENCODE_AUDIO) === 'true' || this.unexpectedPCM;
126
124
 
127
125
  const probe = await probeVideoCamera(this.mixinDevice);
128
126
 
@@ -143,91 +141,66 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
143
141
  'copy',
144
142
  ];
145
143
 
146
- const fragmentClientHandler = async (socket: Socket) => {
147
- fmp4OutputServer.close();
148
- const parser = parseFragmentedMP4(socket);
149
- for await (const atom of parser) {
150
- const now = Date.now();
151
- if (!this.ftyp) {
152
- this.ftyp = atom;
153
- }
154
- else if (!this.moov) {
155
- this.moov = atom;
156
- }
157
- else {
158
- if (atom.type === 'mdat') {
159
- if (this.prevIdr)
160
- this.detectedIdrInterval = now - this.prevIdr;
161
- this.prevIdr = now;
162
- }
163
-
164
- this.prebufferFmp4.push({
165
- atom,
166
- time: now,
167
- });
168
- }
169
-
170
- while (this.prebufferFmp4.length && this.prebufferFmp4[0].time < now - prebufferDurationMs) {
171
- this.prebufferFmp4.shift();
172
- }
173
-
174
- this.events.emit('atom', atom);
175
- }
176
- }
177
-
178
- const fmp4OutputServer = createServer(socket => {
179
- fragmentClientHandler(socket).catch(e => console.log(this.name, 'fragmented mp4 session ended', e));
180
- });
181
- const fmp4Port = await listenZeroCluster(fmp4OutputServer);
182
-
183
- const additionalOutputs = [
184
- '-f', 'mp4',
185
- ...acodec,
186
- ...vcodec,
187
- '-movflags', 'frag_keyframe+empty_moov+default_base_moof',
188
- `tcp://127.0.0.1:${fmp4Port}`
189
- ];
190
-
191
144
  const session = await startRebroadcastSession(ffmpegInput, {
192
- additionalOutputs,
193
- vcodec,
194
- acodec,
145
+ parsers: {
146
+ mp4: createFragmentedMp4Parser({
147
+ vcodec,
148
+ acodec,
149
+ }),
150
+ mpegts: createMpegTsParser({
151
+ vcodec,
152
+ acodec,
153
+ }),
154
+ }
195
155
  });
196
156
 
197
- this.detectedAcodec = session.inputAudioCodec || '';
198
- this.detectedVcodec = session.inputVideoCodec || '';
157
+ this.session = session;
199
158
 
200
- if (!this.detectedAcodec) {
159
+ if (!this.session.inputAudioCodec) {
201
160
  console.warn(this.name, 'no audio detected.');
202
161
  }
203
- else if (this.detectedAcodec !== 'aac') {
162
+ else if (this.session.inputAudioCodec !== 'aac') {
204
163
  console.error(this.name, 'Detected audio codec was not AAC.');
205
- if (this.name?.indexOf('pcm') !== -1 && !reencodeAudio) {
206
- log.a(`${this.name} is using PCM audio. You will need to enable Reencode Audio in Rebroadcast Settings for this stream.`);
164
+ if (session.inputAudioCodec && session.inputAudioCodec.indexOf('pcm') !== -1 && !reencodeAudio) {
165
+ log.a(`${this.name} is using PCM audio and will be reencoded. Enable Reencode Audio in Rebroadcast Settings to disable this alert.`);
166
+ this.unexpectedPCM = true;
207
167
  }
208
168
  }
209
169
 
210
- if (this.detectedVcodec !== 'h264') {
170
+ if (this.session.inputVideoCodec !== 'h264') {
211
171
  console.error(`${this.name} video codec is not h264. If there are errors, try changing your camera's encoder output.`);
212
172
  }
213
173
 
214
174
  session.events.on('killed', () => {
215
- fmp4OutputServer.close();
216
175
  this.prebufferSession = undefined;
217
176
  });
218
- session.events.on('data', (data: Buffer) => {
219
- const now = Date.now();
220
- this.prebufferMpegTs.push({
221
- time: now,
222
- buffer: data,
223
- });
224
177
 
225
- while (this.prebufferMpegTs.length && this.prebufferMpegTs[0].time < now - prebufferDurationMs) {
226
- this.prebufferMpegTs.shift();
227
- }
178
+ for (const container of ['mpegts', 'mp4']) {
179
+ const eventName = container + '-data';
180
+ const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
181
+
182
+ session.events.on(eventName, (chunk: StreamChunk) => {
183
+ const now = Date.now();
184
+
185
+ if (chunk.type === 'mdat') {
186
+ if (this.prevIdr)
187
+ this.detectedIdrInterval = now - this.prevIdr;
188
+ this.prevIdr = now;
189
+ }
190
+
191
+ prebufferContainer.push({
192
+ time: now,
193
+ chunk,
194
+ });
195
+
196
+ while (prebufferContainer.length && prebufferContainer[0].time < now - prebufferDurationMs) {
197
+ prebufferContainer.shift();
198
+ }
199
+
200
+ this.events.emit(eventName, chunk);
201
+ });
202
+ }
228
203
 
229
- this.events.emit('mpegts-data', data);
230
- });
231
204
  return session;
232
205
  }
233
206
 
@@ -237,19 +210,23 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
237
210
  const session = await this.prebufferSession;
238
211
 
239
212
  // if a specific stream is requested, and it's not what we're streaming, just fall through to source.
240
- if (options?.id && options.id !== session.ffmpegInput.mediaStreamOptions?.id) {
213
+ if (options?.id && options.id !== session.ffmpegInputs['mpegts'].mediaStreamOptions?.id) {
241
214
  console.log(this.name, 'rebroadcast session cant be used here', options);
242
215
  return this.mixinDevice.getVideoStream(options);
243
216
  }
244
217
 
245
- const sendKeyframe = this.storage.getItem(SEND_KEYFRAME) === 'true';
218
+ const sendKeyframe = this.storage.getItem(SEND_KEYFRAME) !== 'false';
246
219
  if (!options?.prebuffer && !sendKeyframe) {
247
- const mo = mediaManager.createFFmpegMediaObject(session.ffmpegInput);
220
+ const mo = mediaManager.createFFmpegMediaObject(session.ffmpegInputs['mpegts']);
248
221
  return mo;
249
222
  }
250
223
 
251
224
  console.log(this.name, 'prebuffer request started');
252
225
 
226
+ const container = options?.container || 'mpegts';
227
+ const eventName = container + '-data';
228
+ const prebufferContainer: PrebufferStreamChunk[] = this.prebuffers[container];
229
+
253
230
  const server = new Server(socket => {
254
231
  server.close();
255
232
  const requestedPrebuffer = options?.prebuffer || (sendKeyframe ? Math.max(4000, (this.detectedIdrInterval || 4000)) * 1.5 : 0);
@@ -258,58 +235,31 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
258
235
 
259
236
  let cleanup: () => void;
260
237
 
261
- if (options?.container === 'mp4') {
262
- const writeAtom = (atom: MP4Atom) => {
263
- socket.write(Buffer.concat([atom.header, atom.data]));
264
- };
265
-
266
- if (this.ftyp) {
267
- writeAtom(this.ftyp);
268
- }
269
- if (this.moov) {
270
- writeAtom(this.moov);
271
- }
272
- const now = Date.now();
273
- let needMoof = true;
274
- for (const prebuffer of this.prebufferFmp4) {
275
- if (prebuffer.time < now - requestedPrebuffer)
276
- continue;
277
- if (needMoof && prebuffer.atom.type !== 'moof')
278
- continue;
279
- needMoof = false;
280
- // console.log('writing prebuffer atom', prebuffer.atom);
281
- writeAtom(prebuffer.atom);
238
+ let first = true;
239
+ const writeData = (data: StreamChunk) => {
240
+ if (first) {
241
+ first = false;
242
+ if (data.startStream) {
243
+ socket.write(data.startStream)
244
+ }
282
245
  }
246
+ socket.write(data.chunk);
247
+ };
283
248
 
284
- this.events.on('atom', writeAtom);
285
- cleanup = () => {
286
- console.log(this.name, 'prebuffer request ended');
287
- this.events.removeListener('atom', writeAtom);
288
- this.events.removeListener('killed', cleanup);
289
- socket.removeAllListeners();
290
- socket.destroy();
291
- }
249
+ for (const prebuffer of prebufferContainer) {
250
+ if (prebuffer.time < now - requestedPrebuffer)
251
+ continue;
292
252
 
253
+ writeData(prebuffer.chunk);
293
254
  }
294
- else {
295
- const writeData = (data: Buffer) => {
296
- socket.write(data);
297
- };
298
-
299
- for (const prebuffer of this.prebufferMpegTs) {
300
- if (prebuffer.time < now - requestedPrebuffer)
301
- continue;
302
- writeData(prebuffer.buffer);
303
- }
304
255
 
305
- this.events.on('mpegts-data', writeData);
306
- cleanup = () => {
307
- console.log(this.name, 'prebuffer request ended');
308
- this.events.removeListener('mpegts-data', writeData);
309
- this.events.removeListener('killed', cleanup);
310
- socket.removeAllListeners();
311
- socket.destroy();
312
- }
256
+ this.events.on(eventName, writeData);
257
+ cleanup = () => {
258
+ console.log(this.name, 'prebuffer request ended');
259
+ this.events.removeListener(eventName, writeData);
260
+ this.events.removeListener('killed', cleanup);
261
+ socket.removeAllListeners();
262
+ socket.destroy();
313
263
  }
314
264
 
315
265
  this.events.once('killed', cleanup);
@@ -322,8 +272,8 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
322
272
 
323
273
  const port = await listenZeroCluster(server);
324
274
 
325
- const mediaStreamOptions = session.ffmpegInput.mediaStreamOptions
326
- ? Object.assign({}, session.ffmpegInput.mediaStreamOptions)
275
+ const mediaStreamOptions = session.ffmpegInputs[container].mediaStreamOptions
276
+ ? Object.assign({}, session.ffmpegInputs[container].mediaStreamOptions)
327
277
  : undefined;
328
278
 
329
279
  if (mediaStreamOptions && mediaStreamOptions.audio) {
@@ -336,7 +286,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
336
286
 
337
287
  const ffmpegInput: FFMpegInput = {
338
288
  inputArguments: [
339
- '-f', options?.container === 'mp4' ? 'mp4' : 'mpegts',
289
+ '-f', container,
340
290
  '-i', `tcp://127.0.0.1:${port}`,
341
291
  ],
342
292
  mediaStreamOptions,
@@ -368,7 +318,34 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
368
318
  }
369
319
  }
370
320
 
371
- class PrebufferProvider extends ScryptedDeviceBase implements MixinProvider {
321
+ function millisUntilMidnight() {
322
+ var midnight = new Date();
323
+ midnight.setHours(24);
324
+ midnight.setMinutes(0);
325
+ midnight.setSeconds(0);
326
+ midnight.setMilliseconds(0);
327
+ return (midnight.getTime() - new Date().getTime());
328
+ }
329
+
330
+ class PrebufferProvider extends AutoenableMixinProvider implements MixinProvider {
331
+ constructor(nativeId?: string) {
332
+ super(nativeId);
333
+
334
+ // trigger the prebuffer.
335
+ for (const id of Object.keys(systemManager.getSystemState())) {
336
+ const device = systemManager.getDeviceById<VideoCamera>(id);
337
+ if (!device.mixins?.includes(this.id))
338
+ continue;
339
+ device.getVideoStreamOptions();
340
+ }
341
+
342
+ // schedule restarts at 2am
343
+ const midnight = millisUntilMidnight();
344
+ const twoAM = midnight + 2 * 60 * 60 * 1000;
345
+ this.log.i(`Rebroadcaster scheduled for restart at 2AM: ${Math.round(twoAM / 1000 / 60)} minutes`)
346
+ setTimeout(() => deviceManager.requestRestart(), twoAM);
347
+ }
348
+
372
349
  async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
373
350
  if (!interfaces.includes(ScryptedInterface.VideoCamera))
374
351
  return null;
@@ -376,6 +353,7 @@ class PrebufferProvider extends ScryptedDeviceBase implements MixinProvider {
376
353
  }
377
354
 
378
355
  async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }) {
356
+ this.setHasEnabledMixin(mixinDeviceState.id);
379
357
  return new PrebufferMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId);
380
358
  }
381
359
  async releaseMixin(id: string, mixinDevice: any) {
@@ -1,80 +0,0 @@
1
- // Copyright 2013 The Chromium Authors. All rights reserved.
2
- // Use of this source code is governed by a BSD-style license that can be
3
- // found in the LICENSE file.
4
-
5
- syntax = "proto2";
6
-
7
- option optimize_for = LITE_RUNTIME;
8
-
9
- package extensions.api.cast_channel;
10
-
11
- message CastMessage {
12
- // Always pass a version of the protocol for future compatibility
13
- // requirements.
14
- enum ProtocolVersion {
15
- CASTV2_1_0 = 0;
16
- }
17
- required ProtocolVersion protocol_version = 1;
18
-
19
- // source and destination ids identify the origin and destination of the
20
- // message. They are used to route messages between endpoints that share a
21
- // device-to-device channel.
22
- //
23
- // For messages between applications:
24
- // - The sender application id is a unique identifier generated on behalf of
25
- // the sender application.
26
- // - The receiver id is always the the session id for the application.
27
- //
28
- // For messages to or from the sender or receiver platform, the special ids
29
- // 'sender-0' and 'receiver-0' can be used.
30
- //
31
- // For messages intended for all endpoints using a given channel, the
32
- // wildcard destination_id '*' can be used.
33
- required string source_id = 2;
34
- required string destination_id = 3;
35
-
36
- // This is the core multiplexing key. All messages are sent on a namespace
37
- // and endpoints sharing a channel listen on one or more namespaces. The
38
- // namespace defines the protocol and semantics of the message.
39
- required string namespace = 4;
40
-
41
- // Encoding and payload info follows.
42
-
43
- // What type of data do we have in this message.
44
- enum PayloadType {
45
- STRING = 0;
46
- BINARY = 1;
47
- }
48
- required PayloadType payload_type = 5;
49
-
50
- // Depending on payload_type, exactly one of the following optional fields
51
- // will always be set.
52
- optional string payload_utf8 = 6;
53
- optional bytes payload_binary = 7;
54
- }
55
-
56
- // Messages for authentication protocol between a sender and a receiver.
57
- message AuthChallenge {
58
- }
59
-
60
- message AuthResponse {
61
- required bytes signature = 1;
62
- required bytes client_auth_certificate = 2;
63
- repeated bytes client_ca = 3;
64
- }
65
-
66
- message AuthError {
67
- enum ErrorType {
68
- INTERNAL_ERROR = 0;
69
- NO_TLS = 1; // The underlying connection is not TLS
70
- }
71
- required ErrorType error_type = 1;
72
- }
73
-
74
- message DeviceAuthMessage {
75
- // Request fields
76
- optional AuthChallenge challenge = 1;
77
- // Response fields
78
- optional AuthResponse response = 2;
79
- optional AuthError error = 3;
80
- }