Skip to content

Commit

Permalink
feat(webcam): add support for go2rtc webrtc (mainsail-crew#1651)
Browse files Browse the repository at this point in the history
  • Loading branch information
meteyou authored and 4rnoP committed Dec 13, 2023
1 parent e0a6692 commit 6fc531a
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 0 deletions.
31 changes: 31 additions & 0 deletions src/components/settings/Webcams/WebcamForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@
:label="$t('Settings.WebcamsTab.HideFps')" />
</v-col>
</v-row>
<v-row v-if="hasAudioOption">
<v-col class="pt-1 pb-3">
<v-checkbox
v-model="enableAudio"
class="mt-1"
hide-details
:label="$t('Settings.WebcamsTab.EnableAudio')" />
</v-col>
</v-row>
<v-row>
<v-col class="pt-1 pb-3">
<div class="v-label v-label--active theme--dark text-subtitle-1">
Expand Down Expand Up @@ -231,6 +240,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 Expand Up @@ -263,6 +273,10 @@ export default class WebcamForm extends Mixins(BaseMixin, WebcamMixin) {
return ['mjpegstreamer', 'mjpegstreamer-adaptive'].includes(this.webcam.service)
}
get hasAudioOption() {
return ['webrtc-go2rtc'].includes(this.webcam.service)
}
get hideFps() {
return this.webcam.extra_data?.hideFps ?? false
}
Expand All @@ -280,6 +294,23 @@ export default class WebcamForm extends Mixins(BaseMixin, WebcamMixin) {
this.webcam.extra_data.hideFps = newVal
}
get enableAudio() {
return this.webcam.extra_data?.enableAudio ?? false
}
set enableAudio(newVal) {
if (!('extra_data' in this.webcam)) {
this.webcam.extra_data = {
enableAudio: newVal,
}
return
}
// @ts-ignore
this.webcam.extra_data.enableAudio = newVal
}
mounted() {
this.oldWebcamName = this.webcam.name
}
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
247 changes: 247 additions & 0 deletions src/components/webcams/streamers/WebrtcGo2rtc.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
<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() {
let urlSearch = ''
let url = new URL(location.href)
try {
urlSearch = new URL(this.camSettings.stream_url).search.toString()
url = new URL('api/ws' + urlSearch, this.camSettings.stream_url)
} catch (e) {
this.log('invalid url', this.camSettings.stream_url)
}
// create media types array
const media = ['video']
if (this.enableAudio) media.push('audio')
url.searchParams.set('media', media.join('+'))
// 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)
}
get enableAudio() {
return this.camSettings.extra_data?.enableAudio ?? false
}
// stop and restart the video if the url changes
@Watch('url')
changedUrl() {
this.terminate()
this.start()
}
// stop and restart the video if enableAudio changes
@Watch('enableAudio')
changedEnableAudio() {
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')
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?.addEventListener('connectionstatechange', () => {
this.status = (this.pc?.connectionState ?? '').toString()
this.log('connection state changed', this.status)
if (['failed', 'disconnected'].includes(this.status)) {
this.scheduleRestart()
}
})
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/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,7 @@
"CreateWebcam": "Erstelle Webcam",
"EditCrowsnestConf": "crowsnest.conf bearbeiten",
"EditWebcam": "Webcam bearbeiten",
"EnableAudio": "Ton einschalten",
"FlipWebcam": "Webcam-Bild spiegeln:",
"HideFps": "FPS-Anzeige verstecken",
"Hlsstream": "HLS-Stream",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,7 @@
"CreateWebcam": "Create Webcam",
"EditCrowsnestConf": "Edit crowsnest.conf",
"EditWebcam": "Edit Webcam",
"EnableAudio": "Enable audio",
"FlipWebcam": "Flip webcam image:",
"HideFps": "Hide FPS counter",
"Hlsstream": "HLS Stream",
Expand Down Expand Up @@ -1194,6 +1195,7 @@
"Vertically": "vertically",
"Webcams": "Webcams",
"WebrtcCameraStreamer": "WebRTC (camera-streamer)",
"WebrtcGo2rtc": "WebRTC (go2rtc)",
"WebrtcJanus": "WebRTC (janus-gateway)",
"WebrtcMediaMTX": "WebRTC (MediaMTX)"
}
Expand Down
1 change: 1 addition & 0 deletions src/store/gui/webcams/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface GuiWebcamStateWebcam {
rotation: number
aspect_ratio?: string
extra_data?: {
enableAudio?: boolean
hideFps?: boolean
}
source?: 'config' | 'database'
Expand Down

0 comments on commit 6fc531a

Please sign in to comment.