diff --git a/src/components/mini-widgets/MiniVideoRecorder.vue b/src/components/mini-widgets/MiniVideoRecorder.vue index 178028382..4acbf0e93 100644 --- a/src/components/mini-widgets/MiniVideoRecorder.vue +++ b/src/components/mini-widgets/MiniVideoRecorder.vue @@ -244,7 +244,7 @@ watch(externalStreams, () => { // Retrieve stream from the saved stream name, otherwise choose the first available stream as a fallback const savedStream = savedStreamName ? availableStreams.value.find((s) => s.name === savedStreamName) : undefined - if (savedStream !== undefined && savedStream !== selectedStream.value && selectedStream.value === undefined) { + if (savedStream !== undefined && savedStream.id !== selectedStream.value?.id && selectedStream.value === undefined) { console.debug!('[WebRTC] trying to set stream...') updateCurrentStream(savedStream) } diff --git a/src/components/widgets/VideoPlayer.vue b/src/components/widgets/VideoPlayer.vue index ce21e6835..debbeab86 100644 --- a/src/components/widgets/VideoPlayer.vue +++ b/src/components/widgets/VideoPlayer.vue @@ -35,6 +35,17 @@ hide-details return-object /> + Saved stream name: "{{ widget.options.streamName }}" Signaller Status: {{ signallerStatus }} Stream Status: {{ streamStatus }} @@ -66,6 +77,30 @@ import type { Stream } from '@/libs/webrtc/signalling_protocol' import { useMainVehicleStore } from '@/stores/mainVehicle' import type { Widget } from '@/types/widgets' +// FIXME: Why can't I just import a property from another component? +// import isValidHostAddress from '@/views/ConfigurationGeneralView.vue' +const isValidHostAddress = (value: string): boolean | string => { + if (value.length >= 255) { + return 'Address is too long' + } + + // Regexes from https://stackoverflow.com/a/106223/3850957 + const ipRegex = new RegExp( + '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' + ) + const hostnameRegex = new RegExp( + '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$' + ) + + if (ipRegex.test(value) || hostnameRegex.test(value)) { + return true + } + + return 'Invalid host address. Should be an IP address or a hostname' +} + +const mainVehicleStore = useMainVehicleStore() + const { rtcConfiguration, webRTCSignallingURI } = useMainVehicleStore() console.debug('[WebRTC] Using webrtc-adapter for', adapter.browserDetails) @@ -79,10 +114,15 @@ const props = defineProps<{ const widget = toRefs(props).widget +const selectedICEIPsField = ref() +const selectedICEIPs = ref() const selectedStream = ref() const videoElement = ref() const webRTCManager = new WebRTCManager(webRTCSignallingURI.val, rtcConfiguration) -const { availableStreams, mediaStream, signallerStatus, streamStatus } = webRTCManager.startStream(selectedStream) +const { availableStreams, mediaStream, signallerStatus, streamStatus } = webRTCManager.startStream( + selectedStream, + selectedICEIPs +) onBeforeMount(() => { // Set initial widget options if they don't exist @@ -119,6 +159,15 @@ watch(mediaStream, async (newStream, oldStream) => { }) }) +watch(selectedICEIPsField, async (oldAddr, newAddr) => { + if (!newAddr || isValidHostAddress(newAddr)) { + return + } + + selectedICEIPs.value = selectedICEIPsField.value +}) + + watch(selectedStream, () => (widget.value.options.streamName = selectedStream.value?.name)) watch(availableStreams, () => { @@ -133,7 +182,7 @@ watch(availableStreams, () => { ? availableStreams.value.find((s) => s.name === savedStreamName) : availableStreams.value.first() - if (savedStream !== undefined && savedStream !== selectedStream.value) { + if (savedStream !== undefined && savedStream.id !== selectedStream.value?.id) { console.debug!('[WebRTC] trying to set stream...') selectedStream.value = savedStream } diff --git a/src/composables/webRTC.ts b/src/composables/webRTC.ts index 893a8e219..aea1e5e63 100644 --- a/src/composables/webRTC.ts +++ b/src/composables/webRTC.ts @@ -41,6 +41,7 @@ export class WebRTCManager { private streamName: string | undefined private session: Session | undefined private rtcConfiguration: RTCConfiguration + private selectedICEIPs: string | undefined private hasEnded = false private signaller: Signaller @@ -51,6 +52,7 @@ export class WebRTCManager { * * @param {Connection.URI} webRTCSignallingURI * @param {RTCConfiguration} rtcConfiguration + * @param {string[]} selectedICEIPs */ constructor(webRTCSignallingURI: Connection.URI, rtcConfiguration: RTCConfiguration) { console.debug('[WebRTC] Trying to connect to signalling server.') @@ -78,9 +80,13 @@ export class WebRTCManager { /** * * @param { Ref } selectedStream - Stream to receive stream from + * @param { Ref } selectedICEIPs * @returns { startStreamReturn } */ - public startStream(selectedStream: Ref): startStreamReturn { + public startStream( + selectedStream: Ref, + selectedICEIPs: Ref + ): startStreamReturn { watch(selectedStream, (newStream, oldStream) => { if (newStream?.id === oldStream?.id) { return @@ -97,6 +103,28 @@ export class WebRTCManager { } }) + watch(selectedICEIPs, (newIp, oldIp) => { + if (newIp === oldIp) { + return + } + + const msg = `Selected IP changed from "${oldIp}" to "${newIp}".` + console.debug('[WebRTC] ' + msg) + + this.selectedICEIPs = newIp + + if (this.streamName !== undefined) { + this.stopSession(msg) + } + + if (this.streamName !== undefined) { + this.startSession() + } + }) + + // FIXME: I want to assign this.selectedICEIPs when startStream is first called + // this.selectedICEIPs = selectedICEIPs.value + return { availableStreams: this.availableStreams, mediaStream: this.mediaStream, @@ -294,6 +322,11 @@ export class WebRTCManager { * @param {string} receivedSessionId */ private onSessionIdReceived(stream: Stream, producerId: string, receivedSessionId: string): void { + const selectedICEIPs = [] + if (this.selectedICEIPs) { + selectedICEIPs.push(this.selectedICEIPs) + } + // Create a new Session with the received Session ID this.session = new Session( receivedSessionId, @@ -301,6 +334,7 @@ export class WebRTCManager { stream, this.signaller, this.rtcConfiguration, + selectedICEIPs, (event: RTCTrackEvent): void => this.onTrackAdded(event), (_sessionId, reason) => this.onSessionClosed(reason) ) diff --git a/src/libs/webrtc/session.ts b/src/libs/webrtc/session.ts index 15b19c364..7b94fd727 100644 --- a/src/libs/webrtc/session.ts +++ b/src/libs/webrtc/session.ts @@ -16,6 +16,7 @@ export class Session { private ended: boolean private signaller: Signaller private peerConnection: RTCPeerConnection + private selectedICEIPs: string[] public rtcConfiguration: RTCConfiguration public onTrackAdded?: OnTrackAddedCallback public onClose?: OnCloseCallback @@ -27,6 +28,7 @@ export class Session { * @param {Stream} stream - The Stream instance for which this Session will be created with, given by the signalling server * @param {Signaller} signaller - The Signaller instance for this Session to use * @param {RTCConfiguration} rtcConfiguration - Configuration for the RTC connection, such as Turn and Stun servers + * @param {string[]} selectedICEIPs - A whitelist for ICE IP addresses, ignored if empty * @param {OnTrackAddedCallback} onTrackAdded - An optional callback for when a track is added to this session * @param {OnCloseCallback} onClose - An optional callback for when this session closes */ @@ -36,6 +38,7 @@ export class Session { stream: Stream, signaller: Signaller, rtcConfiguration: RTCConfiguration, + selectedICEIPs: string[] = [], onTrackAdded?: OnTrackAddedCallback, onClose?: OnCloseCallback ) { @@ -48,6 +51,7 @@ export class Session { this.signaller = signaller this.rtcConfiguration = rtcConfiguration this.ended = false + this.selectedICEIPs = selectedICEIPs this.peerConnection = this.createRTCPeerConnection(rtcConfiguration) @@ -165,6 +169,12 @@ export class Session { * @param {RTCIceCandidateInit} candidate - The ICE candidate received from the signalling server */ public onIncomingICE(candidate: RTCIceCandidateInit): void { + // Ignores unwanted routes, useful, for example, to prevent WebRTC to chose the wrong route, like when the OS default is WiFi but you want to receive the video via tether because of reliability + if (candidate.candidate && !this.selectedICEIPs.some((address) => candidate.candidate!.includes(address))) { + console.debug(`[WebRTC] [Session] ICE candidate ignored: ${JSON.stringify(candidate, null, 4)}`) + return + } + this.peerConnection .addIceCandidate(candidate) .then(() => console.debug(`[WebRTC] [Session] ICE candidate added: ${JSON.stringify(candidate, null, 4)}`))