Skip to content

Commit

Permalink
Add media param support
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexxIT committed Oct 13, 2023
1 parent cfb361b commit 2dd1a86
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 59 deletions.
131 changes: 85 additions & 46 deletions custom_components/webrtc/www/video-rtc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -128,8 +135,8 @@ export class VideoRTC extends HTMLElement {
this.ondata = null;

/**
* [internal] Handlers list for receiving JSON from WebSocket
* @type {Object.<string,Function>}}
* [internal] Handlers list for receiving JSON from WebSocket.
* @type {Object.<string,Function>}
*/
this.onmessage = null;
}
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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')) {
Expand All @@ -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();
}
Expand Down Expand Up @@ -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 = '';
Expand Down Expand Up @@ -451,30 +482,13 @@ 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;

const candidate = ev.candidate ? ev.candidate.toJSON().candidate : '';
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
Expand Down Expand Up @@ -506,28 +520,53 @@ 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;
this.pc = pc;
}

/**
* @param ev {Event}
* @param pc {RTCPeerConnection}
* @returns {Promise<MediaStream>}
*/
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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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) {
Expand Down
25 changes: 12 additions & 13 deletions custom_components/webrtc/www/webrtc-camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -45,6 +37,8 @@ class WebRTCCamera extends VideoRTC {
* ui: boolean,
* style: string,
*
* server: string,
*
* mse: boolean,
* webrtc: boolean,
*
Expand All @@ -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}];
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 2dd1a86

Please sign in to comment.