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: adds WebRTC (go2rtc) support #1245

Merged
merged 11 commits into from
Nov 26, 2023
4 changes: 4 additions & 0 deletions docs/features/cameras.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ The current supported types are:
- A highly bandwidth-efficient stream type.
- **IMPORTANT:** Currently only available on Raspberry devices.

- **WebRTC (go2rtc)**
- Loads a webrtc stream from go2rtc.
- Example stream URL: `http(s)://your.domain/webrtc/stream.html?src=trident&mode=webrtc`

- **IP Camera**
- Experimental option replacing the `<img>` tag with a `<video>` tag.
- Use only if your provided URL supports native HTML5 video tags.
Expand Down
3 changes: 2 additions & 1 deletion src/components/settings/cameras/CameraConfigDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@
{ text: $t('app.setting.camera_type_options.hlsstream'), value: 'hlsstream' },
{ text: $t('app.setting.camera_type_options.webrtc_camera_streamer'), value: 'webrtc-camerastreamer' },
{ text: $t('app.setting.camera_type_options.video'), value: 'ipstream' },
{ text: $t('app.setting.camera_type_options.iframe'), value: 'iframe' }
{ text: $t('app.setting.camera_type_options.iframe'), value: 'iframe' },
{ text: $t('app.setting.camera_type_options.webrtc_gortc'), value: 'webrtc-go2rtc' }
]"
item-value="value"
item-text="text"
Expand Down
2 changes: 1 addition & 1 deletion src/components/widgets/camera/CameraItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default class CameraItem extends Vue {

get cameraComponent () {
if (this.camera.service) {
const componentName = `${this.$filters.startCase(this.camera.service).replace(' ', '')}Camera`
const componentName = `${this.$filters.startCase(this.camera.service).replace(/ /g, '')}Camera`

if (componentName in CameraComponents) {
return CameraComponents[componentName]
Expand Down
124 changes: 124 additions & 0 deletions src/components/widgets/camera/services/WebrtcGo2RtcCamera.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<template>
<video
ref="streamingElement"
autoplay
playsinline
controls
muted
:style="cameraStyle"
:crossorigin="crossorigin"
/>
</template>

<script lang="ts">
import { Component, Ref, Mixins } from 'vue-property-decorator'
import CameraMixin from '@/mixins/camera'
import consola from 'consola'

@Component({})
export default class WebrtcGo2RtcCamera extends Mixins(CameraMixin) {
@Ref('streamingElement')
readonly cameraVideo!: HTMLVideoElement

pc: RTCPeerConnection | null = null
ws: WebSocket | null = null

// webrtc player methods
// adapted from https://github.com/AlexxIT/go2rtc/blob/master/www/webrtc.html
// also adapted from https://github.com/mainsail-crew/mainsail/pull/1651

get socketUrl () {
const url = this.buildAbsoluteUrl(this.camera.urlStream || '')
const socketUrl = new URL('api/ws' + url.search, url)

socketUrl.searchParams.set('media', 'video+audio')
socketUrl.protocol = socketUrl.protocol === 'https:'
? 'wss:'
: 'ws:'

return socketUrl
}

startPlayback () {
this.pc?.close()
this.ws?.close()

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

const localTracks = ['video', 'audio']
.map(kind => {
const init: RTCRtpTransceiverInit = {
direction: 'recvonly'
}

return this.pc?.addTransceiver(kind, init).receiver.track
})
.filter((track): track is MediaStreamTrack => track != null)

this.cameraVideo.srcObject = new MediaStream(localTracks)

this.ws = new WebSocket(this.socketUrl)
this.ws.addEventListener('open', this.onWebSocketOpen)
this.ws.addEventListener('message', this.onWebSocketMessage)
}

onWebSocketOpen () {
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) as {
type: 'webrtc/candidate' | 'webrtc/answer' | 'error',
value: string
}

switch (msg.type) {
case 'webrtc/candidate':
this.pc?.addIceCandidate({ candidate: msg.value, sdpMid: '0' })
break

case 'webrtc/answer':
this.pc?.setRemoteDescription({ type: 'answer', sdp: msg.value })
break

case 'error':
consola.error(`[WebrtcGo2RtcCamera] ${msg.value}`)
break
}
}

stopPlayback () {
this.pc?.close()
this.pc = null
this.ws?.close()
this.ws = null
this.cameraVideo.src = ''
}
}
</script>
1 change: 1 addition & 0 deletions src/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ app:
webrtc_camera_streamer: WebRTC (camera-streamer)
video: IP Camera
iframe: HTTP page
webrtc_gortc: WebRTC (go2rtc)
camera_rotate_options:
'90': 90°
'180': 180°
Expand Down
2 changes: 1 addition & 1 deletion src/store/cameras/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface CameraConfig extends CameraConfigWithoutId {
id: string;
}

export type CameraService = 'mjpegstreamer' | 'mjpegstreamer-adaptive' | 'ipstream' | 'iframe' | 'hlsstream' | 'webrtc-camerastreamer' | 'device'
export type CameraService = 'mjpegstreamer' | 'mjpegstreamer-adaptive' | 'ipstream' | 'iframe' | 'hlsstream' | 'webrtc-camerastreamer' | 'device' | 'webrtc-go2rtc'

export interface LegacyCamerasState {
activeCamera: string;
Expand Down