Skip to content

frontend: video-manager: Add block list support for MCM sources#3846

Open
joaoantoniocardoso wants to merge 1 commit intobluerobotics:masterfrom
joaoantoniocardoso:master-add-block-list-mcm-sources
Open

frontend: video-manager: Add block list support for MCM sources#3846
joaoantoniocardoso wants to merge 1 commit intobluerobotics:masterfrom
joaoantoniocardoso:master-add-block-list-mcm-sources

Conversation

@joaoantoniocardoso
Copy link
Member

@joaoantoniocardoso joaoantoniocardoso commented Mar 23, 2026

master counterpart of the 1.4 PR: #3837

This branch includes the commits from the thumbnail rework and MCM Idle master PRs so the UI and types stay consistent and CI can pass.

Suggested merge order: merge the thumbnail and Idle PRs to master first, then rebase this branch onto master so this PR collapses to a single commit (the block-list change) before final merge.

Related master PRs:

Block-list commit: 33e42773 (equivalent to the rebased 1.4 head).

Summary by Sourcery

Add video source block list support and enhanced thumbnail controls to the video manager UI and store.

New Features:

  • Allow blocking and unblocking video sources from the video manager, including visual status and controls.
  • Expose per-stream options to disable lazy mode, mavlink, thumbnails, and Zenoh in the stream creation and editing UI.
  • Provide user-facing controls to fetch single or continuous thumbnails per device, with idle/disabled states and debug overlay in pirate mode.

Enhancements:

  • Refine device and stream status handling to distinguish running, idle, and stopped states and update corresponding UI text and colors.
  • Centralize thumbnail fetch scheduling in a shared, window-scoped state, tracking round-trip latency and preventing overlapping requests.
  • Improve thumbnail placeholder and layout styling and ensure blob URLs are cleaned up when thumbnails are updated or components destroyed.

Summary by Sourcery

Add support in the video manager to block and unblock video sources and reflect their blocked status in the UI and device model.

New Features:

  • Introduce Vuex actions to block and unblock video sources via backend API and refresh devices/streams afterward.
  • Expose a block/unblock control for each video device in the video manager (pirate mode only) and prevent adding streams to blocked sources.
  • Extend the video device type to track whether a source is blocked and display corresponding status text and color in the UI.

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 23, 2026

Reviewer's Guide

Adds block-list support for video sources in the video manager by wiring new backend endpoints into the Vuex video store, exposing a pirate-mode-only block toggle in the VideoDevice UI, and extending the Device type with an optional blocked flag that affects status rendering and stream creation controls.

Sequence diagram for blocking and unblocking a video source

sequenceDiagram
  actor User
  participant VideoDevice as VideoDevice_component
  participant VideoStore as Vuex_VideoStore
  participant Backend as Backend_API

  User->>VideoDevice: Toggle Block source switch
  activate VideoDevice
  alt Device currently not blocked
    VideoDevice->>VideoStore: blockSource(device.source)
    activate VideoStore
    VideoStore->>Backend: POST /block_source
    Backend-->>VideoStore: 200 OK or error
    alt Success
      VideoStore->>VideoStore: fetchDevices()
      VideoStore->>Backend: GET /devices
      Backend-->>VideoStore: Devices_with_blocked_flag
      VideoStore->>VideoStore: fetchStreams()
      VideoStore->>Backend: GET /streams
      Backend-->>VideoStore: Streams
    else Failure
      VideoStore->>VideoStore: notifier.pushError(VIDEO_SOURCE_BLOCK_FAIL)
    end
    deactivate VideoStore
  else Device currently blocked
    VideoDevice->>VideoStore: unblockSource(device.source)
    activate VideoStore
    VideoStore->>Backend: POST /unblock_source
    Backend-->>VideoStore: 200 OK or error
    alt Success
      VideoStore->>VideoStore: fetchDevices()
      VideoStore->>Backend: GET /devices
      Backend-->>VideoStore: Devices_with_blocked_flag
      VideoStore->>VideoStore: fetchStreams()
      VideoStore->>Backend: GET /streams
      Backend-->>VideoStore: Streams
    else Failure
      VideoStore->>VideoStore: notifier.pushError(VIDEO_SOURCE_UNBLOCK_FAIL)
    end
    deactivate VideoStore
  end
  VideoStore-->>VideoDevice: Updated devices and streams in store
  VideoDevice-->>User: Updated status text, color and Add stream button state
  deactivate VideoDevice
Loading

Updated class diagram for video store and device block flag

classDiagram
  class VideoStore {
    +string API_URL
    +Promise~void~ blockSource(source)
    +Promise~void~ unblockSource(source)
    +Promise~void~ fetchDevices()
    +Promise~void~ fetchStreams()
  }

  class Device {
    +string source
    +Format[] formats
    +Control[] controls
    +boolean blocked
  }

  class VideoDeviceComponent {
    +Device device
    +boolean are_video_streams_available
    +boolean updating_streams
    +boolean is_redirect_source
    +boolean is_pirate_mode
    +string status_color
    +void openStreamCreationDialog()
    +Promise~void~ toggleBlocked()
    +Promise~void~ createNewStream(stream)
  }

  class Settings {
    +boolean is_pirate_mode
  }

  VideoDeviceComponent --> Device : uses
  VideoDeviceComponent --> VideoStore : dispatches_actions
  VideoDeviceComponent --> Settings : reads
  VideoStore --> Device : populates_list_of
  class Format
  class Control
  VideoStore --> Format : uses
  VideoStore --> Control : uses
Loading

File-Level Changes

Change Details Files
Wire block/unblock source actions in the video Vuex store to backend endpoints and refresh device/stream state after changes.
  • Add blockSource and unblockSource Vuex actions that POST to /block_source and /unblock_source with source_string params.
  • On successful block/unblock, refetch devices and streams to synchronize frontend state with backend.
  • On failure, surface notifier errors with specific VIDEO_SOURCE_BLOCK_FAIL/VIDEO_SOURCE_UNBLOCK_FAIL codes.
core/frontend/src/store/video.ts
Expose a pirate-mode-only UI control to block/unblock devices and propagate blocked state into device status and actions.
  • Display a 'Video source is blocked' message when a device is blocked, ahead of other stream-availability messages.
  • Disable the Add stream button when the device is blocked to prevent creating streams for blocked sources.
  • Add a pirate-mode-only v-switch bound to device.blocked that calls a new toggleBlocked method.
  • Update status_color to show a warning color when the device is blocked, and implement toggleBlocked to call the new Vuex actions.
  • Inject settings to query is_pirate_mode for conditional rendering of the block control.
core/frontend/src/components/video-manager/VideoDevice.vue
Extend the Device type to carry block-list state from the backend.
  • Add an optional blocked boolean field to the Device interface so UI can react to backend block-list info.
core/frontend/src/types/video.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Integrate the block list API from mavlink-camera-manager PR bluerobotics#561.
Pirate-mode users can toggle source blocking via a switch; all users
see the blocked state reflected in the status indicator and tooltip.
@joaoantoniocardoso joaoantoniocardoso force-pushed the master-add-block-list-mcm-sources branch from b7698d0 to f693aec Compare March 24, 2026 18:04
@joaoantoniocardoso joaoantoniocardoso marked this pull request as ready for review March 24, 2026 18:11
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In the blockSource and unblockSource actions you're mixing async/await with .then/.catch; consider rewriting them to use a single try/await/catch flow for readability and to make it clearer what is awaited.
  • blockSource/unblockSource call fetchDevices() and fetchStreams() sequentially after every change; if both are needed every time, you could await Promise.all([this.fetchDevices(), this.fetchStreams()]) to avoid unnecessary latency and make the intent explicit.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the `blockSource` and `unblockSource` actions you're mixing `async/await` with `.then/.catch`; consider rewriting them to use a single `try/await/catch` flow for readability and to make it clearer what is awaited.
- `blockSource`/`unblockSource` call `fetchDevices()` and `fetchStreams()` sequentially after every change; if both are needed every time, you could `await Promise.all([this.fetchDevices(), this.fetchStreams()])` to avoid unnecessary latency and make the intent explicit.

## Individual Comments

### Comment 1
<location path="core/frontend/src/store/video.ts" line_range="248-264" />
<code_context>
   }

+  @Action
+  async blockSource(source: string): Promise<void> {
+    await back_axios({
+      method: 'post',
+      url: `${this.API_URL}/block_source`,
+      timeout: 10000,
+      params: { source_string: source },
+    })
+      .then(() => {
+        this.fetchDevices()
+        this.fetchStreams()
+      })
+      .catch((error) => {
+        const message = `Could not block video source: ${error.message}.`
+        notifier.pushError('VIDEO_SOURCE_BLOCK_FAIL', message)
</code_context>
<issue_to_address>
**suggestion:** Avoid mixing async/await with .then/.catch and consider awaiting the subsequent fetches.

Given this is already an async function using await, prefer a try/catch instead of .then/.catch for consistency and readability. That also lets you `await Promise.all([this.fetchDevices(), this.fetchStreams()])` so `blockSource` only resolves once the device and stream state are updated for callers.

```suggestion
  @Action
  async blockSource(source: string): Promise<void> {
    try {
      await back_axios({
        method: 'post',
        url: `${this.API_URL}/block_source`,
        timeout: 10000,
        params: { source_string: source },
      })

      await Promise.all([this.fetchDevices(), this.fetchStreams()])
    } catch (error: any) {
      const message = `Could not block video source: ${error.message}.`
      notifier.pushError('VIDEO_SOURCE_BLOCK_FAIL', message)
    }
  }
```
</issue_to_address>

### Comment 2
<location path="core/frontend/src/store/video.ts" line_range="266-282" />
<code_context>
+  }
+
+  @Action
+  async unblockSource(source: string): Promise<void> {
+    await back_axios({
+      method: 'post',
+      url: `${this.API_URL}/unblock_source`,
+      timeout: 10000,
+      params: { source_string: source },
+    })
+      .then(() => {
+        this.fetchDevices()
+        this.fetchStreams()
+      })
+      .catch((error) => {
+        const message = `Could not unblock video source: ${error.message}.`
+        notifier.pushError('VIDEO_SOURCE_UNBLOCK_FAIL', message)
</code_context>
<issue_to_address>
**suggestion:** Mirror the blockSource refactor here for consistency and clearer control flow.

Please apply the same structure you use in `blockSource` (e.g., try/catch with awaited follow-up fetches) so these two actions stay in sync and avoid subtle behavioral differences between block and unblock over time.

```suggestion
  @Action
  async unblockSource(source: string): Promise<void> {
    try {
      await back_axios({
        method: 'post',
        url: `${this.API_URL}/unblock_source`,
        timeout: 10000,
        params: { source_string: source },
      })

      await this.fetchDevices()
      await this.fetchStreams()
    } catch (error: any) {
      const message = `Could not unblock video source: ${error.message}.`
      notifier.pushError('VIDEO_SOURCE_UNBLOCK_FAIL', message)
    }
  }
```
</issue_to_address>

### Comment 3
<location path="core/frontend/src/components/video-manager/VideoDevice.vue" line_range="55-62" />
<code_context>
             <v-icon>mdi-plus</v-icon>
             Add stream
           </v-btn>
+          <v-switch
+            v-if="is_pirate_mode"
+            :input-value="device.blocked"
+            dense
+            hide-details
+            class="mt-2"
+            label="Block source"
+            @change="toggleBlocked"
+          />
         </div>
</code_context>
<issue_to_address>
**suggestion:** Consider handling transient toggle state or disabling the switch while the block/unblock request is in flight.

As written, users can toggle the switch multiple times while `toggleBlocked` is still waiting on the backend, which can trigger overlapping block/unblock calls and leave the UI out of sync. A simple `blocking`/`unblocking` flag wired to `:loading`/`:disabled` on the v-switch (or reusing `updating_streams` if it fits) would prevent this and make the interaction more reliable.

Suggested implementation:

```
          <v-switch
            v-if="is_pirate_mode"
            :input-value="device.blocked"
            :disabled="updating_streams"
            :loading="updating_streams"
            dense
            hide-details
            class="mt-2"
            label="Block source"
            @change="toggleBlocked"
          />

```

To fully address the race condition, ensure that `toggleBlocked` manages `updating_streams` (or a dedicated flag like `blocking_source`) while the block/unblock request is in flight, for example:

1. Set `this.updating_streams = true` before issuing the backend request.
2. Use a `try/finally` block so that `this.updating_streams` is reset to `false` regardless of success/failure.
3. Optionally, handle failures by reverting the local `device.blocked` state or re-fetching device data so the UI stays in sync with the backend.

If `updating_streams` is not appropriate for this purpose, introduce a new `blocking_source` data property and bind `:disabled` / `:loading` to that instead, updating it in `toggleBlocked` with the same pattern.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +248 to +264
@Action
async blockSource(source: string): Promise<void> {
await back_axios({
method: 'post',
url: `${this.API_URL}/block_source`,
timeout: 10000,
params: { source_string: source },
})
.then(() => {
this.fetchDevices()
this.fetchStreams()
})
.catch((error) => {
const message = `Could not block video source: ${error.message}.`
notifier.pushError('VIDEO_SOURCE_BLOCK_FAIL', message)
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Avoid mixing async/await with .then/.catch and consider awaiting the subsequent fetches.

Given this is already an async function using await, prefer a try/catch instead of .then/.catch for consistency and readability. That also lets you await Promise.all([this.fetchDevices(), this.fetchStreams()]) so blockSource only resolves once the device and stream state are updated for callers.

Suggested change
@Action
async blockSource(source: string): Promise<void> {
await back_axios({
method: 'post',
url: `${this.API_URL}/block_source`,
timeout: 10000,
params: { source_string: source },
})
.then(() => {
this.fetchDevices()
this.fetchStreams()
})
.catch((error) => {
const message = `Could not block video source: ${error.message}.`
notifier.pushError('VIDEO_SOURCE_BLOCK_FAIL', message)
})
}
@Action
async blockSource(source: string): Promise<void> {
try {
await back_axios({
method: 'post',
url: `${this.API_URL}/block_source`,
timeout: 10000,
params: { source_string: source },
})
await Promise.all([this.fetchDevices(), this.fetchStreams()])
} catch (error: any) {
const message = `Could not block video source: ${error.message}.`
notifier.pushError('VIDEO_SOURCE_BLOCK_FAIL', message)
}
}

Comment on lines +266 to +282
@Action
async unblockSource(source: string): Promise<void> {
await back_axios({
method: 'post',
url: `${this.API_URL}/unblock_source`,
timeout: 10000,
params: { source_string: source },
})
.then(() => {
this.fetchDevices()
this.fetchStreams()
})
.catch((error) => {
const message = `Could not unblock video source: ${error.message}.`
notifier.pushError('VIDEO_SOURCE_UNBLOCK_FAIL', message)
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Mirror the blockSource refactor here for consistency and clearer control flow.

Please apply the same structure you use in blockSource (e.g., try/catch with awaited follow-up fetches) so these two actions stay in sync and avoid subtle behavioral differences between block and unblock over time.

Suggested change
@Action
async unblockSource(source: string): Promise<void> {
await back_axios({
method: 'post',
url: `${this.API_URL}/unblock_source`,
timeout: 10000,
params: { source_string: source },
})
.then(() => {
this.fetchDevices()
this.fetchStreams()
})
.catch((error) => {
const message = `Could not unblock video source: ${error.message}.`
notifier.pushError('VIDEO_SOURCE_UNBLOCK_FAIL', message)
})
}
@Action
async unblockSource(source: string): Promise<void> {
try {
await back_axios({
method: 'post',
url: `${this.API_URL}/unblock_source`,
timeout: 10000,
params: { source_string: source },
})
await this.fetchDevices()
await this.fetchStreams()
} catch (error: any) {
const message = `Could not unblock video source: ${error.message}.`
notifier.pushError('VIDEO_SOURCE_UNBLOCK_FAIL', message)
}
}

Comment on lines +55 to +62
<v-switch
v-if="is_pirate_mode"
:input-value="device.blocked"
dense
hide-details
class="mt-2"
label="Block source"
@change="toggleBlocked"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider handling transient toggle state or disabling the switch while the block/unblock request is in flight.

As written, users can toggle the switch multiple times while toggleBlocked is still waiting on the backend, which can trigger overlapping block/unblock calls and leave the UI out of sync. A simple blocking/unblocking flag wired to :loading/:disabled on the v-switch (or reusing updating_streams if it fits) would prevent this and make the interaction more reliable.

Suggested implementation:

          <v-switch
            v-if="is_pirate_mode"
            :input-value="device.blocked"
            :disabled="updating_streams"
            :loading="updating_streams"
            dense
            hide-details
            class="mt-2"
            label="Block source"
            @change="toggleBlocked"
          />

To fully address the race condition, ensure that toggleBlocked manages updating_streams (or a dedicated flag like blocking_source) while the block/unblock request is in flight, for example:

  1. Set this.updating_streams = true before issuing the backend request.
  2. Use a try/finally block so that this.updating_streams is reset to false regardless of success/failure.
  3. Optionally, handle failures by reverting the local device.blocked state or re-fetching device data so the UI stays in sync with the backend.

If updating_streams is not appropriate for this purpose, introduce a new blocking_source data property and bind :disabled / :loading to that instead, updating it in toggleBlocked with the same pattern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant