Skip to content

Commit

Permalink
feat: add WebRTC (MediaMTX / rtsp-simple-server) webcam mode (#1318)
Browse files Browse the repository at this point in the history
Co-authored-by: Stefan Dej <meteyou@gmail.com>
  • Loading branch information
slynn1324 and meteyou authored Jun 18, 2023
1 parent cff7b32 commit 8682dd7
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/components/TheSettingsMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
ref="settingsScroll"
:class="'settings-tabs ' + (isMobile ? '' : 'height500')"
:options="{ overflowBehavior: { x: 'hidden' } }">
<component :is="'settings-' + activeTab + '-tab'" @scrollToTop="scrollToTop"></component>
<component :is="'settings-' + activeTab + '-tab'" @scrollToTop="scrollToTop" />
</overlay-scrollbars>
</v-col>
</v-row>
Expand Down
1 change: 1 addition & 0 deletions src/components/settings/SettingsWebcamsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ export default class SettingsWebcamsTab 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-mediamtx', text: this.$t('Settings.WebcamsTab.WebrtcMediaMTX') },
{ value: 'hlsstream', text: this.$t('Settings.WebcamsTab.Hlsstream') },
{ value: 'jmuxer-stream', text: this.$t('Settings.WebcamsTab.JMuxerStream') },
{ value: 'webrtc-janus', text: this.$t('Settings.WebcamsTab.WebrtcJanus') },
Expand Down
6 changes: 5 additions & 1 deletion src/components/webcams/WebcamWrapperItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
<template v-else-if="service === 'webrtc-janus'">
<janus-streamer :cam-settings="webcam" />
</template>
<template v-else-if="service === 'webrtc-mediamtx'">
<webrtc-media-m-t-x :cam-settings="webcam" />
</template>
<template v-else>
<p class="text-center py-3 font-italic">{{ $t('Panels.WebcamPanel.UnknownWebcamService') }}</p>
</template>
Expand All @@ -40,12 +43,13 @@ import { GuiWebcamStateWebcam } from '@/store/gui/webcams/types'
components: {
Hlsstreamer: () => import('@/components/webcams/Hlsstreamer.vue'),
Ipstreamer: () => import('@/components/webcams/Ipstreamer.vue'),
JanusStreamer: () => import('@/components/webcams/JanusStreamer.vue'),
JMuxerStream: () => import('@/components/webcams/JMuxerStream.vue'),
Mjpegstreamer: () => import('@/components/webcams/Mjpegstreamer.vue'),
MjpegstreamerAdaptive: () => import('@/components/webcams/MjpegstreamerAdaptive.vue'),
Uv4lMjpeg: () => import('@/components/webcams/Uv4lMjpeg.vue'),
WebrtcCameraStreamer: () => import('@/components/webcams/WebrtcCameraStreamer.vue'),
JanusStreamer: () => import('@/components/webcams/JanusStreamer.vue'),
WebrtcMediaMTX: () => import('@/components/webcams/WebrtcMediaMTX.vue'),
},
})
export default class WebcamWrapperItem extends Mixins(BaseMixin) {
Expand Down
237 changes: 237 additions & 0 deletions src/components/webcams/WebrtcMediaMTX.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
<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'
@Component
export default class WebrtcRTSPSimpleServer extends Mixins(BaseMixin) {
@Prop({ required: true }) camSettings: any
@Ref()
declare video: HTMLVideoElement
// webrtc player vars
private terminated: boolean = false
private ws: WebSocket | null = null
private pc: RTCPeerConnection | null = null
private restartTimeoutTimer: any = null
private status: string = 'connecting'
// 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.restartTimeoutTimer) {
clearTimeout(this.restartTimeoutTimer)
}
}
get webcamStyle() {
let transforms = ''
if ('flipX' in this.camSettings && this.camSettings.flipX) transforms += ' scaleX(-1)'
if ('flipX' in this.camSettings && this.camSettings.flipY) transforms += ' scaleY(-1)'
if (transforms.trimStart().length) return { transform: transforms.trimStart() }
return ''
}
get url() {
if (this.camSettings.urlStream.startsWith('http')) {
return this.camSettings.urlStream.replace('http', 'ws') + 'ws'
}
return this.camSettings.urlStream
}
// 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()
}
// webrtc player methods
// adapated from sample player in https://github.com/mrlt8/docker-wyze-bridge
start() {
// unterminate we're starting again.
this.terminated = false
// clear any potentially open restart timeout
if (this.restartTimeoutTimer) {
clearTimeout(this.restartTimeoutTimer)
this.restartTimeoutTimer = null
}
window.console.log('[webcam-rtspsimpleserver] web socket connecting')
// test if the url is valid
try {
const url = new URL(this.url)
// break if url protocol is not ws
if (!url.protocol.startsWith('ws')) {
console.log('[webcam-rtspsimpleserver] invalid URL (no ws protocol)')
return
}
} catch (err) {
console.log('[webcam-rtspsimpleserver] invalid URL')
return
}
// open websocket connection
this.ws = new WebSocket(this.url)
this.ws.onerror = (event) => {
window.console.log('[webcam-rtspsimpleserver] websocket error', event)
this.ws?.close()
this.ws = null
}
this.ws.onclose = (event) => {
console.log('[webcam-rtspsimpleserver] websocket closed', event)
this.ws = null
this.scheduleRestart()
}
this.ws.onmessage = (msg: MessageEvent) => this.webRtcOnIceServers(msg)
}
terminate() {
this.terminated = true
try {
this.video.pause()
} catch (err) {
// ignore -- make sure we close down the sockets anyway
}
this.ws?.close()
this.pc?.close()
}
webRtcOnIceServers(msg: MessageEvent) {
if (this.ws === null) return
const iceServers = JSON.parse(msg.data)
this.pc = new RTCPeerConnection({
iceServers,
})
this.ws.onmessage = (msg: MessageEvent) => this.webRtcOnRemoteDescription(msg)
this.pc.onicecandidate = (evt: RTCPeerConnectionIceEvent) => this.webRtcOnIceCandidate(evt)
this.pc.oniceconnectionstatechange = () => {
if (this.pc === null) return
window.console.log('[webcam-rtspsimpleserver] peer connection state:', this.pc.iceConnectionState)
this.status = (this.pc?.iceConnectionState ?? '').toString()
if (['failed', 'disconnected'].includes(this.status)) {
this.scheduleRestart()
}
}
this.pc.ontrack = (evt: RTCTrackEvent) => {
window.console.log('[webcam-rtspsimpleserver] new track ' + evt.track.kind)
this.video.srcObject = evt.streams[0]
}
const direction = 'sendrecv'
this.pc.addTransceiver('video', { direction })
this.pc.addTransceiver('audio', { direction })
this.pc.createOffer().then((desc: any) => {
if (this.pc === null || this.ws === null) return
this.pc.setLocalDescription(desc)
window.console.log('[webcam-rtspsimpleserver] sending offer')
this.ws.send(JSON.stringify(desc))
})
}
webRtcOnRemoteDescription(msg: any) {
if (this.pc === null || this.ws === null) return
this.pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.data)))
this.ws.onmessage = (msg: any) => this.webRtcOnRemoteCandidate(msg)
}
webRtcOnIceCandidate(evt: RTCPeerConnectionIceEvent) {
if (this.ws === null) return
if (evt.candidate?.candidate !== '') {
this.ws.send(JSON.stringify(evt.candidate))
}
}
webRtcOnRemoteCandidate(msg: any) {
if (this.pc === null) return
this.pc.addIceCandidate(JSON.parse(msg.data))
}
scheduleRestart() {
this.ws?.close()
this.ws = null
this.pc?.close()
this.pc = null
if (this.terminated) return
this.restartTimeoutTimer = setTimeout(() => {
this.start()
}, 2000)
}
}
</script>

<style scoped>
.webcamImage {
width: 100%;
}
._webcam_webrtc_output {
aspect-ratio: calc(3 / 2);
}
video {
width: 100%;
}
</style>
3 changes: 2 additions & 1 deletion src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1079,7 +1079,8 @@
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "vertikal",
"Webcams": "Webcams",
"WebrtcCameraStreamer": "WebRTC (camera-streamer)"
"WebrtcCameraStreamer": "WebRTC (camera-streamer)",
"WebrtcMediaMTX": "WebRTC (MediaMTX / rtsp-simple-server)"
}
},
"ScrewsTiltAdjust": {
Expand Down
3 changes: 2 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1094,7 +1094,8 @@
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "vertically",
"Webcams": "Webcams",
"WebrtcCameraStreamer": "WebRTC (camera-streamer)"
"WebrtcCameraStreamer": "WebRTC (camera-streamer)",
"WebrtcMediaMTX": "WebRTC (MediaMTX / rtsp-simple-server)"
}
},
"ScrewsTiltAdjust": {
Expand Down

0 comments on commit 8682dd7

Please sign in to comment.