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

fix(MediaMTX): fix some connection issues #1979

Merged
2 changes: 1 addition & 1 deletion src/components/panels/WebcamPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<v-card-text v-if="webcams.length" class="px-0 py-0 content d-inline-block">
<v-row>
<v-col class="pb-0" style="position: relative">
<webcam-wrapper :webcam="currentCam" />
<webcam-wrapper :webcam="currentCam" :page="currentPage" />
</v-col>
</v-row>
</v-card-text>
Expand Down
9 changes: 7 additions & 2 deletions src/components/webcams/WebcamWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
<v-container v-if="webcams" fluid class="pb-4">
<v-row dense>
<v-col v-for="gridWebcam in webcams" :key="gridWebcam.name" cols="6">
<webcam-wrapper-item :webcam="gridWebcam" :printer-url="printerUrl" :show-fps="showFps" />
<webcam-wrapper-item
:webcam="gridWebcam"
:printer-url="printerUrl"
:show-fps="showFps"
:page="page" />
</v-col>
</v-row>
</v-container>
</template>
<template v-else>
<webcam-wrapper-item :webcam="webcam" :printer-url="printerUrl" :show-fps="showFps" />
<webcam-wrapper-item :webcam="webcam" :printer-url="printerUrl" :show-fps="showFps" :page="page" />
</template>
</div>
</template>
Expand All @@ -31,6 +35,7 @@ export default class WebcamWrapper extends Mixins(BaseMixin) {
@Prop({ type: Object, required: true }) webcam!: GuiWebcamStateWebcam
@Prop({ type: Boolean, default: true }) showFps!: Boolean
@Prop({ type: String, default: null }) printerUrl!: string | null
@Prop({ type: String, default: null }) page!: string | null

get webcams(): GuiWebcamStateWebcam[] {
return this.$store.getters['gui/webcams/getWebcams']
Expand Down
3 changes: 2 additions & 1 deletion src/components/webcams/WebcamWrapperItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<janus-streamer-async :cam-settings="webcam" :printer-url="printerUrl" />
</template>
<template v-else-if="service === 'webrtc-mediamtx'">
<webrtc-media-m-t-x-async :cam-settings="webcam" :printer-url="printerUrl" />
<webrtc-media-m-t-x-async :cam-settings="webcam" :printer-url="printerUrl" :page="page" />
</template>
<template v-else-if="service === 'webrtc-go2rtc'">
<webrtc-go2rtc-async :cam-settings="webcam" :printer-url="printerUrl" />
Expand Down Expand Up @@ -61,6 +61,7 @@ export default class WebcamWrapperItem extends Mixins(BaseMixin) {
@Prop({ type: Object, required: true }) webcam!: GuiWebcamStateWebcam
@Prop({ type: Boolean, default: true }) showFps!: Boolean
@Prop({ default: null }) printerUrl!: string | null
@Prop({ type: String, default: null }) page!: string | null

get service() {
return this.webcam?.service ?? 'unknown'
Expand Down
146 changes: 68 additions & 78 deletions src/components/webcams/streamers/WebrtcMediaMTX.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface OfferData {
export default class WebrtcMediaMTX extends Mixins(BaseMixin, WebcamMixin) {
@Prop({ required: true }) readonly camSettings!: GuiWebcamStateWebcam
@Prop({ default: null }) readonly printerUrl!: string | null
@Prop({ type: String, default: null }) readonly page!: string | null
@Ref() declare video: HTMLVideoElement

pc: RTCPeerConnection | null = null
Expand All @@ -48,10 +49,6 @@ export default class WebrtcMediaMTX extends Mixins(BaseMixin, WebcamMixin) {
}
RESTART_PAUSE = 2000

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()
Expand All @@ -73,16 +70,9 @@ export default class WebrtcMediaMTX extends Mixins(BaseMixin, WebcamMixin) {
get url() {
let baseUrl = this.camSettings.stream_url
if (!baseUrl.endsWith('/')) baseUrl += '/'
baseUrl += 'whep'

try {
baseUrl = new URL('whep', baseUrl).toString()

return this.convertUrl(baseUrl, this.printerUrl)
} catch (e) {
this.log('invalid baseURL', baseUrl)

return null
}
return this.convertUrl(baseUrl, this.printerUrl)
}

// stop and restart the video if the url changes
Expand All @@ -93,11 +83,13 @@ export default class WebrtcMediaMTX extends Mixins(BaseMixin, WebcamMixin) {
}

get expanded(): boolean {
if (this.page === 'page') return true

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

// start or stop the video when the expand state changes
@Watch('expanded')
// start or stop the video when the expanded state changes
@Watch('expanded', { immediate: true })
expandChanged(newExpanded: boolean): void {
if (!newExpanded) {
this.terminate()
Expand Down Expand Up @@ -198,7 +190,7 @@ export default class WebrtcMediaMTX extends Mixins(BaseMixin, WebcamMixin) {
return frag
}

start() {
async start() {
// stop if url is not valid
if (this.url === null) {
this.log('invalid url')
Expand All @@ -209,14 +201,13 @@ export default class WebrtcMediaMTX extends Mixins(BaseMixin, WebcamMixin) {

this.log('requesting ICE servers from ' + this.url)

fetch(this.url, {
method: 'OPTIONS',
})
.then((res) => this.onIceServers(res))
.catch((err) => {
this.log('error: ' + err)
this.scheduleRestart()
})
try {
const res = await fetch(this.url, { method: 'OPTIONS' })
this.onIceServers(res)
} catch (err) {
this.log('error: ' + err)
this.scheduleRestart()
}
}

onIceServers(res: Response) {
Expand Down Expand Up @@ -246,40 +237,37 @@ export default class WebrtcMediaMTX extends Mixins(BaseMixin, WebcamMixin) {
}

// eslint-disable-next-line no-undef
onLocalOffer(offer: RTCSessionDescriptionInit) {
this.offerData = this.parseOffer(offer.sdp ?? '')
this.pc?.setLocalDescription(offer)

fetch(this.url ?? '', {
method: 'POST',
headers: {
'Content-Type': 'application/sdp',
},
body: offer.sdp,
})
.then((res) => {
if (res.status !== 201) throw new Error('bad status code')
this.eTag = res.headers.get('ETag')
const location = res.headers.get('Location') ?? ''
this.sessionUuid = location?.substring(location.lastIndexOf('/') + 1) ?? null
async onLocalOffer(offer: RTCSessionDescriptionInit) {
try {
const res = await fetch(this.url ?? '', {
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: offer.sdp,
})

// fallback for MediaMTX v1.0.x with broken ETag header
if (res.headers.has('E-Tag')) this.eTag = res.headers.get('E-Tag')
if (res.status !== 201) new Error('bad status code')

return res.text()
})
.then((sdp) => {
this.onRemoteAnswer(
new RTCSessionDescription({
type: 'answer',
sdp,
})
)
})
.catch((err) => {
this.log(err)
this.scheduleRestart()
})
this.offerData = this.parseOffer(offer.sdp ?? '')
this.pc?.setLocalDescription(offer)

this.eTag = res.headers.get('ETag')
const location = res.headers.get('Location') ?? ''
this.sessionUuid = location?.substring(location.lastIndexOf('/') + 1) ?? null

// fallback for MediaMTX v1.0.x with broken ETag header
if (res.headers.has('E-Tag')) this.eTag = res.headers.get('E-Tag')

const sdp = await res.text()
this.onRemoteAnswer(
new RTCSessionDescription({
type: 'answer',
sdp,
})
)
} catch (err: any) {
this.log(err?.message ?? err ?? 'unknown error')
this.scheduleRestart()
}
}

onRemoteAnswer(answer: RTCSessionDescription) {
Expand Down Expand Up @@ -319,7 +307,7 @@ export default class WebrtcMediaMTX extends Mixins(BaseMixin, WebcamMixin) {
}
}

sendLocalCandidates(candidates: RTCIceCandidate[]) {
async sendLocalCandidates(candidates: RTCIceCandidate[]) {
if (this.sessionUuid === null) {
this.log('Session-UUID is null')
this.scheduleRestart()
Expand All @@ -328,29 +316,31 @@ export default class WebrtcMediaMTX extends Mixins(BaseMixin, WebcamMixin) {
}

const url = (this.url ?? '') + '/' + this.sessionUuid
fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/trickle-ice-sdpfrag',
'If-Match': this.eTag,
// eslint-disable-next-line no-undef
} as HeadersInit,
body: this.generateSdpFragment(this.offerData, candidates),
})
.then((res) => {
switch (res.status) {
case 204:
break
case 404:
throw new Error('stream not found')
default:
throw new Error(`bad status code ${res.status}`)
}
try {
const res = await fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/trickle-ice-sdpfrag',
'If-Match': this.eTag,
// eslint-disable-next-line no-undef
} as HeadersInit,
body: this.generateSdpFragment(this.offerData, candidates),
})
.catch((err) => {
this.log(err)

if (res.status === 204) return

if (res.status === 404) {
this.log('stream not found')
this.scheduleRestart()
})
return
}

this.log(`bad status code ${res.status}`)
this.scheduleRestart()
} catch (err: any) {
this.log(err)
this.scheduleRestart()
}
}

terminate() {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Webcam.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div>
<v-row>
<v-col col-12>
<webcam-panel current-page="page"></webcam-panel>
<webcam-panel current-page="page" />
</v-col>
</v-row>
</div>
Expand Down
Loading