diff --git a/core/frontend/src/components/video-manager/VideoControlsDialog.vue b/core/frontend/src/components/video-manager/VideoControlsDialog.vue index 3bcaa8560b..b18ab9b51f 100644 --- a/core/frontend/src/components/video-manager/VideoControlsDialog.vue +++ b/core/frontend/src/components/video-manager/VideoControlsDialog.vue @@ -15,9 +15,9 @@ > @@ -126,6 +126,10 @@ export default Vue.extend({ type: Boolean, default: true, }, + thumbnailDisabled: { + type: Boolean, + default: false, + }, }, data() { return { diff --git a/core/frontend/src/components/video-manager/VideoStreamCreationDialog.vue b/core/frontend/src/components/video-manager/VideoStreamCreationDialog.vue index d5a382d466..ac034b4450 100644 --- a/core/frontend/src/components/video-manager/VideoStreamCreationDialog.vue +++ b/core/frontend/src/components/video-manager/VideoStreamCreationDialog.vue @@ -116,10 +116,22 @@ v-model="is_thermal" label="Thermal camera" /> + + + @@ -206,7 +218,10 @@ export default Vue.extend({ interval: undefined, endpoints: [''], thermal: false, + disable_lazy: false, disable_mavlink: false, + disable_thumbnails: false, + disable_zenoh: false, } }, }, @@ -224,7 +239,10 @@ export default Vue.extend({ selected_interval: this.stream.interval, stream_endpoints: this.stream.endpoints, is_thermal: this.stream.thermal, + is_disable_lazy: this.stream.disable_lazy, is_disable_mavlink: this.stream.disable_mavlink, + is_disable_thumbnails: this.stream.disable_thumbnails, + is_disable_zenoh: this.stream.disable_zenoh, settings, } }, @@ -259,7 +277,10 @@ export default Vue.extend({ }, extended_configuration: { thermal: this.is_thermal, + disable_lazy: this.is_disable_lazy, disable_mavlink: this.is_disable_mavlink, + disable_thumbnails: this.is_disable_thumbnails, + disable_zenoh: this.is_disable_zenoh, }, }, } @@ -311,10 +332,36 @@ export default Vue.extend({ return this.stream_name.replace(/[^a-z0-9]/gi, '_').toLowerCase() }, }, + watch: { + show(newVal: boolean) { + if (newVal) { + this.resetFormFromStream() + } + }, + }, mounted() { this.set_default_configurations() }, methods: { + resetFormFromStream(): void { + const format_match = this.device.formats + .find((format) => format.encode === this.stream.encode) + const size_match = format_match?.sizes + .find((size) => size.width === this.stream.dimensions?.width + && size.height === this.stream.dimensions?.height) + + this.stream_name = this.stream.name + this.selected_encode = this.stream.encode + this.selected_size = size_match || null + this.selected_interval = this.stream.interval + this.stream_endpoints = this.stream.endpoints + this.is_thermal = this.stream.thermal + this.is_disable_lazy = this.stream.disable_lazy + this.is_disable_mavlink = this.stream.disable_mavlink + this.is_disable_thumbnails = this.stream.disable_thumbnails + this.is_disable_zenoh = this.stream.disable_zenoh + this.set_default_configurations() + }, validate_required_field(input: string | null): (true | string) { if (!this.format_required) { return true diff --git a/core/frontend/src/components/video-manager/VideoThumbnail.vue b/core/frontend/src/components/video-manager/VideoThumbnail.vue index d823104e32..72bbe68331 100644 --- a/core/frontend/src/components/video-manager/VideoThumbnail.vue +++ b/core/frontend/src/components/video-manager/VideoThumbnail.vue @@ -1,38 +1,115 @@ + + diff --git a/core/frontend/src/store/video.ts b/core/frontend/src/store/video.ts index 376bf7715e..345c05489a 100644 --- a/core/frontend/src/store/video.ts +++ b/core/frontend/src/store/video.ts @@ -16,10 +16,28 @@ import back_axios, { isBackendOffline } from '@/utils/api' export interface Thumbnail { source: string | undefined status: number | undefined + roundtripMs: number | undefined } const notifier = new Notifier(video_manager_service) +interface ThumbnailFetchState { + task: OneMoreTime + sources: Set + busy: Set + inProgress: boolean +} + +const THUMBNAIL_STATE_KEY = '__blueos_video_thumbnail_state__' +const thumbnailState: ThumbnailFetchState = (window as any)[THUMBNAIL_STATE_KEY] ??= { + task: new OneMoreTime({ delay: 1000, autostart: false }), + sources: new Set(), + busy: new Set(), + inProgress: false, +} +;(window as any)[THUMBNAIL_STATE_KEY] = thumbnailState +thumbnailState.task.setDelay(1000) + @Module({ dynamic: true, store, @@ -43,14 +61,6 @@ class VideoStore extends VuexModule { thumbnails: Map = new Map() - private sources_to_request_thumbnail: Set = new Set() - - private busy_sources: Set = new Set() - - fetchThumbnailsTask = new OneMoreTime( - { delay: 1000, autostart: false }, - ) - @Mutation setUpdatingStreams(updating: boolean): void { this.updating_streams = updating @@ -174,17 +184,21 @@ class VideoStore extends VuexModule { @Action async fetchThumbnails(): Promise { + if (thumbnailState.inProgress) return + thumbnailState.inProgress = true + const target_height = 150 const quality = 75 const requests: Promise[] = [] - this.sources_to_request_thumbnail.forEach(async (source: string) => { - if (this.busy_sources.has(source)) { + thumbnailState.sources.forEach(async (source: string) => { + if (thumbnailState.busy.has(source)) { return } - this.busy_sources.add(source) + thumbnailState.busy.add(source) + const requestStart = Date.now() const request = back_axios({ method: 'get', url: `${this.API_URL}/thumbnail?source=${source}&quality=${quality}&target_height=${target_height}`, @@ -193,29 +207,42 @@ class VideoStore extends VuexModule { }) .then((response) => { if (response.status === 200) { + const roundtripMs = Date.now() - requestStart const old_thumbnail_source = this.thumbnails.get(source)?.source if (old_thumbnail_source !== undefined) { URL.revokeObjectURL(old_thumbnail_source) } - this.thumbnails.set(source, { source: URL.createObjectURL(response.data), status: response.status }) + this.thumbnails.set(source, { + source: URL.createObjectURL(response.data), status: response.status, roundtripMs, + }) } }) .catch((error) => { + const roundtripMs = Date.now() - requestStart if (error?.response?.status === StatusCodes.SERVICE_UNAVAILABLE) { - this.thumbnails.set(source, { source: undefined, status: error.response.status }) + const existing = this.thumbnails.get(source) + if (existing?.source) { + this.thumbnails.set(source, { source: existing.source, status: error.response.status, roundtripMs }) + } else { + this.thumbnails.set(source, { source: undefined, status: error.response.status, roundtripMs }) + } } else { this.thumbnails.delete(source) } }) .finally(() => { - this.busy_sources.delete(source) + thumbnailState.busy.delete(source) }) requests.push(request) }) - await Promise.allSettled(requests) + try { + await Promise.allSettled(requests) + } finally { + thumbnailState.inProgress = false + } } @Action @@ -236,26 +263,25 @@ class VideoStore extends VuexModule { }) } + // eslint-disable-next-line class-methods-use-this @Action startGetThumbnailForDevice(source: string): void { - if (this.sources_to_request_thumbnail.size > 0) { - this.fetchThumbnailsTask.resume() - } else { - this.fetchThumbnailsTask.start() + thumbnailState.sources.add(source) + const task = thumbnailState.task as any + if (task.isPaused) { + thumbnailState.task.resume() + } else if (!task.isRunning && !task.timeoutId) { + thumbnailState.task.start() } - this.sources_to_request_thumbnail.add(source) } + // eslint-disable-next-line class-methods-use-this @Action stopGetThumbnailForDevice(source: string): void { - const old_thumbnail_source = this.thumbnails.get(source)?.source - if (old_thumbnail_source !== undefined) { - URL.revokeObjectURL(old_thumbnail_source) - } - this.sources_to_request_thumbnail.delete(source) + thumbnailState.sources.delete(source) - if (this.sources_to_request_thumbnail.size === 0) { - this.fetchThumbnailsTask.stop() + if (thumbnailState.sources.size === 0) { + thumbnailState.task.stop() } } } @@ -264,6 +290,6 @@ export { VideoStore } const video: VideoStore = getModule(VideoStore) -video.fetchThumbnailsTask.setAction(video.fetchThumbnails) +thumbnailState.task.setAction(video.fetchThumbnails) export default video