Skip to content

Commit

Permalink
video: Use abstracted names for video selection
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaellehmkuhl authored and ArturoManzoli committed Aug 15, 2024
1 parent 94c8aba commit 144c896
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 44 deletions.
61 changes: 39 additions & 22 deletions src/components/mini-widgets/MiniVideoRecorder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const props = defineProps<{
const miniWidget = toRefs(props).miniWidget
const nameSelectedStream = ref<string | undefined>()
const { namesAvailableStreams } = storeToRefs(videoStore)
const { namessAvailableAbstractedStreams: namesAvailableStreams } = storeToRefs(videoStore)
const recorderWidget = ref()
const { isOutside } = useMouseInElement(recorderWidget)
const isVideoLibraryDialogOpen = ref(false)
Expand All @@ -130,6 +130,16 @@ const mediaStream = ref<MediaStream | undefined>()
const isProcessingVideo = ref(false)
const numberOfVideosOnDB = ref(0)
const externalStreamId = computed(() => {
return nameSelectedStream.value ? videoStore.externalStreamId(nameSelectedStream.value) : undefined
})
watch(
() => videoStore.streamsCorrespondency,
() => (mediaStream.value = undefined),
{ deep: true }
)
onMounted(async () => {
await fetchNumebrOfTempVideos()
})
Expand All @@ -138,14 +148,14 @@ onBeforeMount(async () => {
// Set initial widget options if they don't exist
if (Object.keys(miniWidget.value.options).length === 0) {
miniWidget.value.options = {
streamName: undefined as string | undefined,
internalStreamName: undefined as string | undefined,
}
}
nameSelectedStream.value = miniWidget.value.options.streamName
nameSelectedStream.value = miniWidget.value.options.internalStreamName
})
watch(nameSelectedStream, () => {
miniWidget.value.options.streamName = nameSelectedStream.value
miniWidget.value.options.internalStreamName = nameSelectedStream.value
mediaStream.value = undefined
})
Expand Down Expand Up @@ -180,8 +190,8 @@ function assertStreamIsSelectedAndAvailable(
const toggleRecording = async (): Promise<void> => {
if (isRecording.value) {
if (nameSelectedStream.value !== undefined) {
videoStore.stopRecording(nameSelectedStream.value)
if (externalStreamId.value !== undefined) {
videoStore.stopRecording(externalStreamId.value)
}
return
}
Expand All @@ -197,23 +207,27 @@ const toggleRecording = async (): Promise<void> => {
}
const startRecording = (): void => {
if (nameSelectedStream.value && !videoStore.getStreamData(nameSelectedStream.value)?.connected) {
if (externalStreamId.value === undefined) {
showDialog({ title: 'Cannot start recording.', message: 'No stream selected.', variant: 'error' })
return
}
if (!videoStore.getStreamData(externalStreamId.value)?.connected) {
showDialog({ title: 'Cannot start recording.', message: 'Stream is not connected.', variant: 'error' })
return
}
assertStreamIsSelectedAndAvailable(nameSelectedStream.value)
videoStore.startRecording(nameSelectedStream.value)
videoStore.startRecording(externalStreamId.value)
widgetStore.miniWidgetManagerVars(miniWidget.value.hash).configMenuOpen = false
}
const isRecording = computed(() => {
if (nameSelectedStream.value === undefined) return false
return videoStore.isRecording(nameSelectedStream.value)
if (externalStreamId.value === undefined) return false
return videoStore.isRecording(externalStreamId.value)
})
const timePassedString = computed(() => {
if (nameSelectedStream.value === undefined) return '00:00:00'
const timeRecordingStart = videoStore.getStreamData(nameSelectedStream.value)?.timeRecordingStart
if (externalStreamId.value === undefined) return '00:00:00'
const timeRecordingStart = videoStore.getStreamData(externalStreamId.value)?.timeRecordingStart
if (timeRecordingStart === undefined) return '00:00:00'
const duration = intervalToDuration({ start: timeRecordingStart, end: timeNow.value })
Expand All @@ -223,8 +237,8 @@ const timePassedString = computed(() => {
return `${durationHours}:${durationMinutes}:${durationSeconds}`
})
const updateCurrentStream = async (streamName: string | undefined): Promise<void> => {
assertStreamIsSelectedAndAvailable(streamName)
const updateCurrentStream = async (internalStreamName: string | undefined): Promise<void> => {
assertStreamIsSelectedAndAvailable(internalStreamName)
mediaStream.value = undefined
isLoadingStream.value = true
Expand All @@ -244,17 +258,17 @@ const updateCurrentStream = async (streamName: string | undefined): Promise<void
return
}
miniWidget.value.options.streamName = streamName
miniWidget.value.options.internalStreamName = internalStreamName
}
let streamConnectionRoutine: ReturnType<typeof setInterval> | undefined = undefined
if (widgetStore.isRealMiniWidget(miniWidget.value)) {
streamConnectionRoutine = setInterval(() => {
// If the video recording widget is cold booted, assign the first stream to it
if (miniWidget.value.options.streamName === undefined && !namesAvailableStreams.value.isEmpty()) {
miniWidget.value.options.streamName = namesAvailableStreams.value[0]
nameSelectedStream.value = miniWidget.value.options.streamName
if (miniWidget.value.options.internalStreamName === undefined && !namesAvailableStreams.value.isEmpty()) {
miniWidget.value.options.internalStreamName = namesAvailableStreams.value[0]
nameSelectedStream.value = miniWidget.value.options.internalStreamName
// If there are multiple streams available, warn user that we chose one automatically and they should change if wanted
if (namesAvailableStreams.value.length > 1) {
Expand All @@ -277,10 +291,13 @@ if (widgetStore.isRealMiniWidget(miniWidget.value)) {
}
}
const updatedMediaStream = videoStore.getMediaStream(miniWidget.value.options.streamName)
// If the widget is not connected to the MediaStream, try to connect it
if (!isEqual(updatedMediaStream, mediaStream.value)) {
mediaStream.value = updatedMediaStream
// If the stream name is defined, try to connect the widget to the MediaStream
if (externalStreamId.value !== undefined) {
const updatedMediaStream = videoStore.getMediaStream(miniWidget.value.options.internalStreamName)
// If the widget is not connected to the MediaStream, try to connect it
if (!isEqual(updatedMediaStream, mediaStream.value)) {
mediaStream.value = updatedMediaStream
}
}
}, 1000)
}
Expand Down
52 changes: 32 additions & 20 deletions src/components/widgets/VideoPlayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
hide-details
return-object
/>
<v-banner-text>Saved stream name: "{{ widget.options.streamName }}"</v-banner-text>
<v-banner-text>Saved stream name: "{{ widget.options.internalStreamName }}"</v-banner-text>
<v-switch
v-model="widget.options.flipHorizontally"
class="my-1"
Expand Down Expand Up @@ -113,7 +113,7 @@ const { showDialog } = useInteractionDialog()
const videoStore = useVideoStore()
const widgetStore = useWidgetManagerStore()
const { namesAvailableStreams } = storeToRefs(videoStore)
const { namessAvailableAbstractedStreams: namesAvailableStreams } = storeToRefs(videoStore)
const props = defineProps<{
/**
Expand All @@ -136,17 +136,27 @@ onBeforeMount(() => {
flipHorizontally: false,
flipVertically: false,
rotationAngle: 0,
streamName: undefined as string | undefined,
internalStreamName: undefined as string | undefined,
}
widget.value.options = Object.assign({}, defaultOptions, widget.value.options)
nameSelectedStream.value = widget.value.options.streamName
nameSelectedStream.value = widget.value.options.internalStreamName
})
const externalStreamId = computed(() => {
return nameSelectedStream.value ? videoStore.externalStreamId(nameSelectedStream.value) : undefined
})
watch(
() => videoStore.streamsCorrespondency,
() => (mediaStream.value = undefined),
{ deep: true }
)
const streamConnectionRoutine = setInterval(() => {
// If the video player widget is cold booted, assign the first stream to it
if (widget.value.options.streamName === undefined && !namesAvailableStreams.value.isEmpty()) {
widget.value.options.streamName = namesAvailableStreams.value[0]
nameSelectedStream.value = widget.value.options.streamName
if (widget.value.options.internalStreamName === undefined && !namesAvailableStreams.value.isEmpty()) {
widget.value.options.internalStreamName = namesAvailableStreams.value[0]
nameSelectedStream.value = widget.value.options.internalStreamName
// If there are multiple streams available, warn user that we chose one automatically and he should change if wanted
if (namesAvailableStreams.value.length > 1) {
Expand All @@ -156,21 +166,23 @@ const streamConnectionRoutine = setInterval(() => {
}
}
const updatedMediaStream = videoStore.getMediaStream(widget.value.options.streamName)
// If the widget is not connected to the MediaStream, try to connect it
if (!isEqual(updatedMediaStream, mediaStream.value)) {
mediaStream.value = updatedMediaStream
}
if (externalStreamId.value !== undefined) {
const updatedMediaStream = videoStore.getMediaStream(externalStreamId.value)
// If the widget is not connected to the MediaStream, try to connect it
if (!isEqual(updatedMediaStream, mediaStream.value)) {
mediaStream.value = updatedMediaStream
}
const updatedStreamState = videoStore.getStreamData(widget.value.options.streamName)?.connected ?? false
if (updatedStreamState !== streamConnected.value) {
streamConnected.value = updatedStreamState
const updatedStreamState = videoStore.getStreamData(externalStreamId.value)?.connected ?? false
if (updatedStreamState !== streamConnected.value) {
streamConnected.value = updatedStreamState
}
}
}, 1000)
onBeforeUnmount(() => clearInterval(streamConnectionRoutine))
watch(nameSelectedStream, () => {
widget.value.options.streamName = nameSelectedStream.value
widget.value.options.internalStreamName = nameSelectedStream.value
mediaStream.value = undefined
})
Expand Down Expand Up @@ -203,19 +215,19 @@ const transformStyle = computed(() => {
})
const serverStatus = computed(() => {
if (nameSelectedStream.value === undefined) return 'Unknown.'
return videoStore.getStreamData(nameSelectedStream.value)?.webRtcManager.signallerStatus ?? 'Unknown.'
if (externalStreamId.value === undefined) return 'Unknown.'
return videoStore.getStreamData(externalStreamId.value)?.webRtcManager.signallerStatus ?? 'Unknown.'
})
const streamStatus = computed(() => {
if (nameSelectedStream.value === undefined) return 'Unknown.'
if (externalStreamId.value === undefined) return 'Unknown.'
const availableSources = videoStore.availableIceIps
if (!availableSources.isEmpty() && !availableSources.find((ip) => videoStore.allowedIceIps.includes(ip))) {
return `Stream is coming from IPs [${availableSources.join(', ')}], which are not in the list of allowed sources
[${videoStore.allowedIceIps.join(', ')}].\\n Please check your configuration.`
}
return videoStore.getStreamData(nameSelectedStream.value)?.webRtcManager.streamStatus ?? 'Unknown.'
return videoStore.getStreamData(externalStreamId.value)?.webRtcManager.streamStatus ?? 'Unknown.'
})
</script>

Expand Down
34 changes: 34 additions & 0 deletions src/stores/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
type VideoProcessingDetails,
getBlobExtensionContainer,
VideoExtensionContainer,
VideoStreamCorrespondency,
} from '@/types/video'

import { useAlertStore } from './alert'
Expand All @@ -42,6 +43,7 @@ export const useVideoStore = defineStore('video', () => {
const { globalAddress, rtcConfiguration, webRTCSignallingURI } = useMainVehicleStore()
console.debug('[WebRTC] Using webrtc-adapter for', adapter.browserDetails)

const streamsCorrespondency = useBlueOsStorage<VideoStreamCorrespondency[]>('cockpit-streams-correspondency', [])
const allowedIceIps = useBlueOsStorage<string[]>('cockpit-allowed-stream-ips', [])
const enableAutoIceIpFetch = useBlueOsStorage('cockpit-enable-auto-ice-ip-fetch', true)
const allowedIceProtocols = useBlueOsStorage<string[]>('cockpit-allowed-stream-protocols', [])
Expand All @@ -56,6 +58,35 @@ export const useVideoStore = defineStore('video', () => {

const namesAvailableStreams = computed(() => mainWebRTCManager.availableStreams.value.map((stream) => stream.name))

const namessAvailableAbstractedStreams = computed(() => {
return streamsCorrespondency.value.map((stream) => stream.name)
})

const externalStreamId = (internalName: string): string | undefined => {
const corr = streamsCorrespondency.value.find((stream) => stream.name === internalName)
return corr ? corr.externalId : undefined
}

const initializeStreamsCorrespondency = (): void => {
if (streamsCorrespondency.value.length >= namesAvailableStreams.value.length) return

// If there are more external streams available than the ones in the correspondency, add the extra ones
const newCorrespondency: VideoStreamCorrespondency[] = []
let i = 1
namesAvailableStreams.value.forEach((streamName) => {
newCorrespondency.push({
name: `Stream ${i}`,
externalId: streamName,
})
i++
})
streamsCorrespondency.value = newCorrespondency
}

watch(namesAvailableStreams, () => {
initializeStreamsCorrespondency()
})

// If the allowed ICE IPs are updated, all the streams should be reconnected
watch([allowedIceIps, allowedIceProtocols], () => {
Object.keys(activeStreams.value).forEach((streamName) => (activeStreams.value[streamName] = undefined))
Expand Down Expand Up @@ -859,6 +890,9 @@ export const useVideoStore = defineStore('video', () => {
namesAvailableStreams,
videoStoringDB,
tempVideoChunksDB,
streamsCorrespondency,
namessAvailableAbstractedStreams,
externalStreamId,
discardProcessedFilesFromVideoDB,
discardUnprocessedFilesFromVideoDB,
downloadFilesFromVideoDB,
Expand Down
5 changes: 5 additions & 0 deletions src/types/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,8 @@ export type WebRTCStatsEvent = {
}
}
}

export type VideoStreamCorrespondency = {
name: string
externalId: string
}
54 changes: 52 additions & 2 deletions src/views/ConfigurationVideoView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,57 @@
<template #help-icon> </template>
<template #title>Video configuration</template>
<template #content>
<div class="flex-col h-full ml-[1vw] max-w-[500px] max-h-[85vh] overflow-y-auto pr-3">
<div class="flex-col h-full ml-[1vw] max-w-[540px] max-h-[85vh] overflow-y-auto pr-3">
<ExpansiblePanel no-top-divider :is-expanded="!interfaceStore.isOnPhoneScreen">
<template #title>Streams mapping</template>
<template #info>
Here you can map your external video streams to internal names. This allows you to easily switch between
different video sources in Cockpit, without having to reconfigure every widget that uses the video stream.
The widgets will be connected to the internal names, and the external video stream will be mapped to the
internal name, so if you need to change the external one, you only need to do it here.
</template>
<template #content>
<div class="flex justify-center flex-col w-[90%] ml-2 mb-8 mt-2">
<v-data-table
:items="videoStore.streamsCorrespondency"
items-per-page="10"
class="elevation-1 bg-transparent rounded-lg"
theme="dark"
:style="interfaceStore.globalGlassMenuStyles"
>
<template #headers>
<tr>
<th class="text-center"><p class="text-[16px] font-bold">Internal name</p></th>
<th class="text-center"><p class="text-[16px] font-bold">External name</p></th>
</tr>
</template>
<template #item="{ item }">
<tr>
<td>
<div class="flex items-center justify-center rounded-xl mx-3">
{{ item.name }}
</div>
</td>
<td>
<div class="flex items-center justify-center rounded-xl mx-3">
<v-select
v-model="item.externalId"
:items="videoStore.namesAvailableStreams"
hide-details
class="mb-2"
density="compact"
variant="plain"
theme="dark"
/>
</div>
</td>
</tr>
</template>
<template #bottom></template>
</v-data-table>
</div>
</template>
</ExpansiblePanel>
<ExpansiblePanel no-top-divider :is-expanded="!interfaceStore.isOnPhoneScreen">
<template #title>Allowed WebRTC remote IP Addresses</template>
<template #info>
Expand Down Expand Up @@ -35,7 +85,7 @@
</div>
</template>
</ExpansiblePanel>
<ExpansiblePanel no-top-divider :is-expanded="!interfaceStore.isOnPhoneScreen">
<ExpansiblePanel :is-expanded="!interfaceStore.isOnPhoneScreen">
<template #title>Allowed WebRTC protocols:</template>
<template #info>
<li>
Expand Down

0 comments on commit 144c896

Please sign in to comment.