Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(webcam): add support for go2rtc webrtc #1651

Merged
merged 11 commits into from
Dec 10, 2023
1 change: 1 addition & 0 deletions src/components/settings/Webcams/WebcamForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export default class WebcamForm extends Mixins(BaseMixin, WebcamMixin) {
{ value: 'uv4l-mjpeg', text: this.$t('Settings.WebcamsTab.Uv4lMjpeg') },
{ value: 'ipstream', text: this.$t('Settings.WebcamsTab.Ipstream') },
{ value: 'webrtc-camerastreamer', text: this.$t('Settings.WebcamsTab.WebrtcCameraStreamer') },
{ value: 'webrtc-go2rtc', text: this.$t('Settings.WebcamsTab.WebrtcGo2rtc') },
{ value: 'webrtc-mediamtx', text: this.$t('Settings.WebcamsTab.WebrtcMediaMTX') },
{ value: 'hlsstream', text: this.$t('Settings.WebcamsTab.Hlsstream') },
{ value: 'jmuxer-stream', text: this.$t('Settings.WebcamsTab.JMuxerStream') },
Expand Down
4 changes: 4 additions & 0 deletions src/components/webcams/WebcamWrapperItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
<template v-else-if="service === 'webrtc-mediamtx'">
<webrtc-media-m-t-x-async :cam-settings="webcam" :printer-url="printerUrl" />
</template>
<template v-else-if="service === 'webrtc-go2rtc'">
<webrtc-go2rtc-async :cam-settings="webcam" :printer-url="printerUrl" />
</template>
<template v-else>
<p class="text-center py-3 font-italic">{{ $t('Panels.WebcamPanel.UnknownWebcamService') }}</p>
</template>
Expand All @@ -51,6 +54,7 @@ import { DynamicCamLoader } from '@/components/webcams/streamers/DynamicCamLoade
Uv4lMjpegAsync: DynamicCamLoader('Uv4lMjpeg'),
WebrtcCameraStreamerAsync: DynamicCamLoader('WebrtcCameraStreamer'),
WebrtcMediaMTXAsync: DynamicCamLoader('WebrtcMediaMTX'),
WebrtcGo2rtcAsync: DynamicCamLoader('WebrtcGo2rtc'),
},
})
export default class WebcamWrapperItem extends Mixins(BaseMixin) {
Expand Down
3 changes: 3 additions & 0 deletions src/components/webcams/streamers/DynamicCamLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type StreamerTypes =
| 'Uv4lMjpeg'
| 'WebrtcCameraStreamer'
| 'WebrtcMediaMTX'
| 'WebrtcGo2rtc'

function getDynamicCamImport(componentName: StreamerTypes) {
// split each webcam streamer into its own chunk
Expand All @@ -32,6 +33,8 @@ function getDynamicCamImport(componentName: StreamerTypes) {
return () => import('@/components/webcams/streamers/WebrtcCameraStreamer.vue')
case 'WebrtcMediaMTX':
return () => import('@/components/webcams/streamers/WebrtcMediaMTX.vue')
case 'WebrtcGo2rtc':
return () => import('@/components/webcams/streamers/WebrtcGo2rtc.vue')
}
}

Expand Down
216 changes: 216 additions & 0 deletions src/components/webcams/streamers/WebrtcGo2rtc.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<template>
<div>
<video
v-show="status === 'connected'"
ref="video"
:style="webcamStyle"
class="webcamImage"
autoplay
playsinline
muted />
<v-row v-if="status !== 'connected'">
<v-col class="_webcam_webrtc_output text-center d-flex flex-column justify-center align-center">
<v-progress-circular v-if="status === 'connecting'" indeterminate color="primary" class="mb-3" />
<span class="mt-3">{{ status }}</span>
</v-col>
</v-row>
</div>
</template>

<script lang="ts">
import { Component, Mixins, Prop, Ref, Watch } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { GuiWebcamStateWebcam } from '@/store/gui/webcams/types'
import WebcamMixin from '@/components/mixins/webcam'

@Component
export default class WebrtcGo2rtc extends Mixins(BaseMixin, WebcamMixin) {
@Prop({ required: true }) readonly camSettings!: GuiWebcamStateWebcam
@Prop({ default: null }) readonly printerUrl!: string | null
@Ref() declare video: HTMLVideoElement

pc: RTCPeerConnection | null = null
ws: WebSocket | null = null
restartPause = 2000
restartTimeout: any = null
status: string = 'connecting'

mounted() {
this.start()
}

// stop the video and close the streams if the component is going to be destroyed so we don't leave hanging streams
beforeDestroy() {
this.terminate()

// clear any potentially open restart timeout
if (this.restartTimeout) clearTimeout(this.restartTimeout)
}

get webcamStyle() {
return {
transform: this.generateTransform(
this.camSettings.flip_horizontal ?? false,
this.camSettings.flip_vertical ?? false,
this.camSettings.rotation ?? 0
),
}
}

get url() {
const urlSearch = new URL(this.camSettings.stream_url).search.toString()
meteyou marked this conversation as resolved.
Show resolved Hide resolved
const url = new URL('api/ws' + urlSearch, this.camSettings.stream_url)
url.searchParams.set('media', 'video+audio')
// change protocol to ws
url.protocol = this.$store.state.socket.protocol + ':'

// output a warning, if no src is set in the url
if (!url.searchParams.has('src')) {
this.log('no src set in url')
}

return this.convertUrl(url.toString(), this.printerUrl)
}

// stop and restart the video if the url changes
@Watch('url')
changedUrl() {
this.terminate()
this.start()
}

get expanded(): boolean {
return this.$store.getters['gui/getPanelExpand']('webcam-panel', this.viewport) ?? false
}

// start or stop the video when the expand state changes
@Watch('expanded', { immediate: true })
expandChanged(newExpanded: boolean): void {
if (!newExpanded) {
this.terminate()
return
}

this.start()
}

log(msg: string, obj?: any) {
if (obj) {
window.console.log(`[WebRTC go2rtc] ${msg}`, obj)
return
}

window.console.log(`[WebRTC go2rtc] ${msg}`)
}

// webrtc player methods
// adapted from https://github.com/AlexxIT/go2rtc/blob/master/www/webrtc.html

start() {
if (!this.video) {
this.scheduleRestart()
return
}

this.log('connecting to ' + this.url)
this.status = 'connecting'

this.pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
})

let localTracks: MediaStreamTrack[] = []
const kinds = ['video', 'audio']
kinds.forEach((kind: string) => {
const track = this.pc?.addTransceiver(kind, { direction: 'recvonly' }).receiver.track
if (track) localTracks.push(track)
})
this.video.srcObject = new MediaStream(localTracks)

this.ws = new WebSocket(this.url)
this.ws.addEventListener('open', () => this.onWebSocketOpen())
this.ws.addEventListener('message', (ev) => this.onWebSocketMessage(ev))
this.ws.addEventListener('close', (ev) => this.onWebSocketClose(ev))
}

onWebSocketOpen() {
this.log('open')
this.status = 'connected'

if (this.restartTimeout !== null) {
clearTimeout(this.restartTimeout)
this.restartTimeout = null
}

this.pc?.addEventListener('icecandidate', (ev) => {
if (!ev.candidate) return
const msg = { type: 'webrtc/candidate', value: ev.candidate.candidate }
this.ws?.send(JSON.stringify(msg))
})

this.pc
?.createOffer()
.then((offer) => this.pc?.setLocalDescription(offer))
.then(() => {
const msg = { type: 'webrtc/offer', value: this.pc?.localDescription?.sdp }
this.ws?.send(JSON.stringify(msg))
})
}

onWebSocketMessage(ev: MessageEvent) {
const msg = JSON.parse(ev.data)

if (msg.type === 'webrtc/candidate') {
this.pc?.addIceCandidate({ candidate: msg.value, sdpMid: '0' })
} else if (msg.type === 'webrtc/answer') {
this.pc?.setRemoteDescription({ type: 'answer', sdp: msg.value })
}
}

onWebSocketClose(ev: CloseEvent) {
this.log('close')
this.status = 'disconnected'

if (!ev.wasClean) this.scheduleRestart()
}

terminate() {
this.log('terminating')

if (this.pc !== null) {
this.pc.close()
this.pc = null
}

if (this.ws !== null) {
this.ws.close()
this.ws = null
}
}

scheduleRestart() {
if (this.restartTimeout !== null) return

this.terminate()

this.restartTimeout = window.setTimeout(() => {
this.restartTimeout = null
this.start()
}, this.restartPause)
}
}
</script>

<style scoped>
.webcamImage {
width: 100%;
}

._webcam_webrtc_output {
aspect-ratio: calc(3 / 2);
}

video {
width: 100%;
}
</style>
1 change: 1 addition & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,7 @@
"Vertically": "vertically",
"Webcams": "Webcams",
"WebrtcCameraStreamer": "WebRTC (camera-streamer)",
"WebrtcGo2rtc": "WebRTC (go2rtc)",
"WebrtcJanus": "WebRTC (janus-gateway)",
"WebrtcMediaMTX": "WebRTC (MediaMTX)"
}
Expand Down
Loading