Adds WebRTC support to video widget (#1386)

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
pull/1430/head
Dan Cunningham 2022-06-23 14:17:37 -07:00 committed by GitHub
parent 182aada13d
commit 9305080c7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 155 additions and 3 deletions

View File

@ -1,9 +1,15 @@
import { pi, pt, pb } from '../helpers.js'
import { pi, pt, pb, pd } from '../helpers.js'
export default () => [
pi('item', 'Item', 'Item containing the address of the video'),
pt('url', 'URL', 'URL to show (if item if not specified)'),
pt('type', 'Type', 'Content Type of the video, for example <em>video/mp4</em> (optional)'),
pb('hideControls', 'Hide Controls', 'Hide the control buttons of the video'),
pb('startManually', 'Start manually', 'Does not start playing the video automatically')
pb('startManually', 'Start Manually', 'Does not start playing the video automatically'),
pt('playerType', 'Player Type', 'Select the player type (optional), defualts to Video.js').o([
{ value: 'videojs', label: 'Video.js (Dash, HLS, Others)' },
{ value: 'webrtc', label: 'WebRTC' }
], true, false).a(),
pt('stunServer', 'Stun Server', 'WebRTC stun server (optional), defaults to \'stun:stun.l.google.com:19302\'').a(),
pd('candidatesTimeout', 'ICE candidates timeout', 'WebRTC ICE candidates discovery timeout length in milliseconds (optional), defaults to \'2000\', \'0\' to disable').a()
]

View File

@ -0,0 +1,137 @@
<template>
<video
ref="videoPlayer"
:autoplay="this.startManually ? false : true"
:controls="this.hideControls ? false : true"
playsinline
style="max-width: 100%">
Sorry, your browser doesn't support embedded videos.
</video>
</template>
<script>
import foregroundService from '../widget-foreground-service'
export default {
mixins: [foregroundService],
name: 'OhVideoWebRTC',
props: {
src: { type: String },
stunServer: { type: String },
candidatesTimeout: { type: Number },
startManually: { type: Boolean },
hideControls: { type: Boolean }
},
data () {
return {
webrtc: null
}
},
watch: {
src (value) {
this.startStream()
}
},
methods: {
stopStream () {
console.debug('WebRTC Closing Connection')
if (this.webrtc) {
this.webrtc.isClosed = true
this.webrtc.close()
// see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/close
this.webrtc = null
}
},
startStream () {
if (!this.inForeground || !this.src) {
return
}
this.stopStream()
const self = this
const webrtc = new RTCPeerConnection({
iceServers: [
{
urls: [self.stunServer || 'stun:stun.l.google.com:19302']
}
],
sdpSemantics: 'unified-plan'
})
webrtc.isClosed = false
webrtc.ontrack = function (event) {
console.debug(event.streams.length + ' track is delivered')
self.$refs.videoPlayer.srcObject = event.streams[0]
}
webrtc.addTransceiver('video', { direction: 'sendrecv' })
webrtc.addTransceiver('audio', { direction: 'sendrecv' })
webrtc.onnegotiationneeded = function handleNegotiationNeeded () {
// WebRTC lifecycle to create a live stream
webrtc.createOffer()
.then(offer => webrtc.setLocalDescription(offer))
.then(() => waitForCandidates(self.candidatesTimeout))
.then(() => sendOffer())
.then(answer => webrtc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: answer })))
.catch(e => console.warn(e))
// waits x amount of time for ICE candidates before resolving
function waitForCandidates (timeout = 2000) {
return new Promise((resolve, reject) => {
let timer = null
if (timeout > 0) {
timer = setTimeout(() => {
resolve()
}, timeout)
}
// ICE is complicated
// see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/canTrickleIceCandidates
webrtc.addEventListener('icegatheringstatechange', (e) => {
if (e.target.iceGatheringState === 'complete') {
resolve()
if (timer) {
clearTimeout(timer)
}
}
})
})
}
// Sends our SDP offer to the remote server, expects a SDP answer back
function sendOffer () {
console.debug('Offer: ', webrtc.localDescription.sdp)
return new Promise((resolve, reject) => {
fetch(self.src, {
method: 'POST',
body: new URLSearchParams({
data: btoa(webrtc.localDescription.sdp)
})
})
.then(response => response.text())
.then(data => {
const answer = atob(data)
console.debug('Answer: ', answer)
resolve(answer)
}).catch(e => reject(e))
})
}
}
// creates a channel needed by nest(?) cameras, we also use this to restart streams if closed
const webrtcSendChannel = webrtc.createDataChannel(
'dataSendChannel'
)
webrtcSendChannel.onclose = _event => {
console.debug(`${webrtcSendChannel.label} has closed`)
// if we did not close this, restart the stream
if (!webrtc.isClosed) {
console.warn(`${webrtcSendChannel.label} closed prematurely, restarting`)
self.startStream()
}
}
this.webrtc = webrtc
},
startForegroundActivity () {
this.startStream()
},
stopForegroundActivity () {
this.stopStream()
}
}
}
</script>

View File

@ -1,6 +1,14 @@
<template>
<div class="player">
<oh-video-webrtc
v-if="config.playerType === 'webrtc'"
:src="src"
:stunServer="config.stunServer"
:candidatesTimeout="config.candidatesTimeout"
:startManually="config.startManually"
:hideControls="config.hideControls" />
<oh-video-videojs
v-else
:src="src"
:type="config.type"
:config="config.videoOptions"
@ -17,7 +25,8 @@ export default {
mixins: [mixin],
widget: OhVideoDefinition,
components: {
'oh-video-videojs': () => import(/* webpackChunkName: "oh-video-videojs" */ './oh-video-videojs.vue')
'oh-video-videojs': () => import(/* webpackChunkName: "oh-video-videojs" */ './oh-video-videojs.vue'),
'oh-video-webrtc': () => import(/* webpackChunkName: "oh-video-webrtc" */ './oh-video-webrtc.vue')
},
data () {
return {