From 2dd1a8627f6b7d0b559c1f6c6fadb980caf58b26 Mon Sep 17 00:00:00 2001 From: Alex X Date: Fri, 13 Oct 2023 22:38:34 +0300 Subject: [PATCH] Add media param support --- custom_components/webrtc/www/video-rtc.js | 131 ++++++++++++------ custom_components/webrtc/www/webrtc-camera.js | 25 ++-- 2 files changed, 97 insertions(+), 59 deletions(-) diff --git a/custom_components/webrtc/www/video-rtc.js b/custom_components/webrtc/www/video-rtc.js index a259962..3b13a23 100644 --- a/custom_components/webrtc/www/video-rtc.js +++ b/custom_components/webrtc/www/video-rtc.js @@ -7,6 +7,7 @@ * - ECMAScript 2017 (ES8) = ES6 + async * - RTCPeerConnection for Safari iOS 11.0+ * - IntersectionObserver for Safari iOS 12.2+ + * - ManagedMediaSource for Safari 17+ * * Doesn't support: * - MediaSource for Safari iOS @@ -37,6 +38,12 @@ export class VideoRTC extends HTMLElement { */ this.mode = 'webrtc,mse,hls,mjpeg'; + /** + * [Config] Requested medias (video, audio, microphone). + * @type {string} + */ + this.media = 'video,audio'; + /** * [config] Run stream when not displayed on the screen. Default `false`. * @type {boolean} @@ -128,8 +135,8 @@ export class VideoRTC extends HTMLElement { this.ondata = null; /** - * [internal] Handlers list for receiving JSON from WebSocket - * @type {Object.}} + * [internal] Handlers list for receiving JSON from WebSocket. + * @type {Object.} */ this.onmessage = null; } @@ -174,11 +181,16 @@ export class VideoRTC extends HTMLElement { if (this.ws) this.ws.send(JSON.stringify(value)); } - codecs(type) { - const test = type === 'mse' - ? codec => MediaSource.isTypeSupported(`video/mp4; codecs="${codec}"`) - : codec => this.video.canPlayType(`video/mp4; codecs="${codec}"`); - return this.CODECS.filter(test).join(); + /** @param {Function} isSupported */ + codecs(isSupported) { + return this.CODECS.filter(this.isRequested) + .filter(codec => isSupported(`video/mp4; codecs="${codec}"`)).join(); + } + + isRequested(codec) { + return codec.indexOf('vc1') > 0 + ? this.media.indexOf('video') >= 0 + : this.media.indexOf('audio') >= 0; } /** @@ -303,6 +315,9 @@ export class VideoRTC extends HTMLElement { this.pcState = WebSocket.CLOSED; if (this.pc) { + this.pc.getSenders().forEach(sender => { + if (sender.track) sender.track.stop(); + }); this.pc.close(); this.pc = null; } @@ -334,7 +349,7 @@ export class VideoRTC extends HTMLElement { const modes = []; - if (this.mode.indexOf('mse') >= 0 && 'MediaSource' in window) { // iPhone + if (this.mode.indexOf('mse') >= 0 && ('MediaSource' in window || 'ManagedMediaSource' in window)) { modes.push('mse'); this.onmse(); } else if (this.mode.indexOf('hls') >= 0 && this.video.canPlayType('application/vnd.apple.mpegurl')) { @@ -345,7 +360,7 @@ export class VideoRTC extends HTMLElement { this.onmp4(); } - if (this.mode.indexOf('webrtc') >= 0 && 'RTCPeerConnection' in window) { // macOS Desktop app + if (this.mode.indexOf('webrtc') >= 0 && 'RTCPeerConnection' in window) { modes.push('webrtc'); this.onwebrtc(); } @@ -387,14 +402,30 @@ export class VideoRTC extends HTMLElement { } onmse() { - const ms = new MediaSource(); - ms.addEventListener('sourceopen', () => { - URL.revokeObjectURL(this.video.src); - this.send({type: 'mse', value: this.codecs('mse')}); - }, {once: true}); + /** @type {MediaSource} */ + let ms; + + if ('ManagedMediaSource' in window) { + const MediaSource = window.ManagedMediaSource; + + ms = new MediaSource(); + ms.addEventListener('sourceopen', () => { + this.send({type: 'mse', value: this.codecs(MediaSource.isTypeSupported)}); + }, {once: true}); + + this.video.disableRemotePlayback = true; + this.video.srcObject = ms; + } else { + ms = new MediaSource(); + ms.addEventListener('sourceopen', () => { + URL.revokeObjectURL(this.video.src); + this.send({type: 'mse', value: this.codecs(MediaSource.isTypeSupported)}); + }, {once: true}); + + this.video.src = URL.createObjectURL(ms); + this.video.srcObject = null; + } - this.video.src = URL.createObjectURL(ms); - this.video.srcObject = null; this.play(); this.mseCodecs = ''; @@ -451,10 +482,6 @@ export class VideoRTC extends HTMLElement { onwebrtc() { const pc = new RTCPeerConnection(this.pcConfig); - /** @type {HTMLVideoElement} */ - const video2 = document.createElement('video'); - video2.addEventListener('loadeddata', ev => this.onpcvideo(ev), {once: true}); - pc.addEventListener('icecandidate', ev => { if (ev.candidate && this.mode.indexOf('webrtc/tcp') >= 0 && ev.candidate.protocol === 'udp') return; @@ -462,19 +489,6 @@ export class VideoRTC extends HTMLElement { this.send({type: 'webrtc/candidate', value: candidate}); }); - pc.addEventListener('track', ev => { - // when stream already init - if (video2.srcObject !== null) return; - - // when audio track not exist in Chrome - if (ev.streams.length === 0) return; - - // when audio track not exist in Firefox - if (ev.streams[0].id[0] === '{') return; - - video2.srcObject = ev.streams[0]; - }); - pc.addEventListener('connectionstatechange', () => { if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') { pc.close(); // stop next events @@ -506,14 +520,15 @@ export class VideoRTC extends HTMLElement { } }; - // Safari doesn't support "offerToReceiveVideo" - pc.addTransceiver('video', {direction: 'recvonly'}); - pc.addTransceiver('audio', {direction: 'recvonly'}); + this.createStream(pc).then(async stream => { + /** @type {HTMLVideoElement} */ + const video2 = document.createElement('video'); + video2.addEventListener('loadeddata', () => this.onpcvideo(video2), {once: true}); + video2.srcObject = stream; - pc.createOffer().then(offer => { - pc.setLocalDescription(offer).then(() => { - this.send({type: 'webrtc/offer', value: offer.sdp}); - }); + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + this.send({type: 'webrtc/offer', value: offer.sdp}); }); this.pcState = WebSocket.CONNECTING; @@ -521,13 +536,37 @@ export class VideoRTC extends HTMLElement { } /** - * @param ev {Event} + * @param pc {RTCPeerConnection} + * @returns {Promise} */ - onpcvideo(ev) { + async createStream(pc) { + try { + if (this.media.indexOf('microphone') >= 0) { + const media = await navigator.mediaDevices.getUserMedia({audio: true}); + media.getTracks().forEach(track => { + pc.addTransceiver(track, {direction: 'sendonly'}); + }); + } + } catch (e) { + console.warn(e); + } + + for (const kind of ['video', 'audio']) { + if (this.media.indexOf(kind) >= 0) { + pc.addTransceiver(kind, {direction: 'recvonly'}); + } + } + + const tracks = pc.getReceivers().map(receiver => receiver.track); + return new MediaStream(tracks); + } + + /** + * @param video2 {HTMLVideoElement} + */ + onpcvideo(video2) { if (!this.pc) return; - /** @type {HTMLVideoElement} */ - const video2 = ev.target; const state = this.pc.connectionState; // Firefox doesn't support pc.connectionState @@ -586,7 +625,7 @@ export class VideoRTC extends HTMLElement { this.play(); }; - this.send({type: 'hls', value: this.codecs('hls')}); + this.send({type: 'hls', value: this.codecs(this.video.canPlayType)}); } onmp4() { @@ -618,7 +657,7 @@ export class VideoRTC extends HTMLElement { video2.src = 'data:video/mp4;base64,' + VideoRTC.btoa(data); }; - this.send({type: 'mp4', value: this.codecs('mp4')}); + this.send({type: 'mp4', value: this.codecs(this.video.canPlayType)}); } static btoa(buffer) { diff --git a/custom_components/webrtc/www/webrtc-camera.js b/custom_components/webrtc/www/webrtc-camera.js index 36f0fbf..df1ace2 100644 --- a/custom_components/webrtc/www/webrtc-camera.js +++ b/custom_components/webrtc/www/webrtc-camera.js @@ -15,27 +15,19 @@ class WebRTCCamera extends VideoRTC { if (config.intersection === 0) this.visibilityThreshold = 0; else this.visibilityThreshold = config.intersection || 0.75; - /** @type {string} configMode */ - this.configMode = config.mode - ? config.mode - : config.mse === false - ? 'webrtc' - : config.webrtc === false - ? 'mse' - : this.mode; - /** * @type {{ * url: string, * entity: string, * mode: string, - * server: string, + * media: string, * * streams: Array<{ * name: string, * url: string, * entity: string, - * mode: string + * mode: string, + * media: string, * }>, * * title: string, @@ -45,6 +37,8 @@ class WebRTCCamera extends VideoRTC { * ui: boolean, * style: string, * + * server: string, + * * mse: boolean, * webrtc: boolean, * @@ -65,7 +59,10 @@ class WebRTCCamera extends VideoRTC { * shortcuts:Array<{ name:string, icon:string }>, * }} config */ - this.config = Object.assign({}, config); + this.config = Object.assign({ + mode: config.mse === false ? 'webrtc' : config.webrtc === false ? 'mse' : this.mode, + media: this.media, + }, config); if (!this.config.streams) { this.config.streams = [{url: config.url, entity: config.entity}]; @@ -107,10 +104,12 @@ class WebRTCCamera extends VideoRTC { /** @param reload {boolean} */ nextStream(reload) { this.streamID = (this.streamID + 1) % this.config.streams.length; + const stream = this.config.streams[this.streamID]; this.config.url = stream.url; this.config.entity = stream.entity; - this.mode = stream.mode || this.configMode; + this.mode = stream.mode || this.config.mode; + this.media = stream.media || this.config.media; if (reload) { this.ondisconnect();