Adds WebRTC support to video widget (#1386)
Signed-off-by: Dan Cunningham <dan@digitaldan.com>pull/1430/head
parent
182aada13d
commit
9305080c7b
|
@ -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()
|
||||
]
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue