Skip to content

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

Open
joaoantoniocardoso wants to merge 1 commit intobluerobotics:1.4from
joaoantoniocardoso:1.4-add-block-list-mcm-sources
Open

frontend: video-manager: Add block list support for MCM sources#3837
joaoantoniocardoso wants to merge 1 commit intobluerobotics:1.4from
joaoantoniocardoso:1.4-add-block-list-mcm-sources

Conversation

@joaoantoniocardoso
Copy link
Member

@joaoantoniocardoso joaoantoniocardoso commented Mar 19, 2026

Cherry-pick of 3662db48 to 1.4.

Adds block list support for MCM sources in the video manager frontend.

Summary by Sourcery

Add block list support and related UI updates for video sources in the video manager frontend.

New Features:

  • Introduce ability to block and unblock video sources from the video manager when pirate mode is enabled.

Enhancements:

  • Indicate blocked status and updated stream health using refined status colors and tooltip messages in the video device view.
  • Adjust video thumbnail registration and disabled state based on stream health and per-stream thumbnail configuration.
  • Extend the video device model to include an optional blocked flag and wire it through the frontend store to reflect backend state.

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 19, 2026

Reviewer's Guide

Adds UI, state handling, and store actions to support blocking/unblocking MCM video sources, updating status indicators, controls, and thumbnails behavior in the video manager frontend.

Sequence diagram for blocking and unblocking a video source in the video manager

sequenceDiagram
  actor User
  participant VideoDeviceComponent
  participant VideoStore
  participant BackendAPI

  User->>VideoDeviceComponent: Toggle block source switch
  VideoDeviceComponent->>VideoDeviceComponent: toggleBlocked()
  alt Device is currently unblocked
    VideoDeviceComponent->>VideoStore: blockSource(device.source)
    VideoStore->>BackendAPI: POST API_URL/block_source?source_string=source
    alt Block request succeeds
      BackendAPI-->>VideoStore: 200 OK
      VideoStore->>VideoStore: fetchDevices()
      VideoStore->>VideoStore: fetchStreams()
      VideoStore-->>VideoDeviceComponent: Updated devices and streams state
      VideoDeviceComponent->>VideoDeviceComponent: Recompute status_color, has_healthy_streams, thumbnails_disabled
    else Block request fails
      BackendAPI-->>VideoStore: Error
      VideoStore->>VideoStore: notifier.pushError(VIDEO_SOURCE_BLOCK_FAIL)
    end
  else Device is currently blocked
    VideoDeviceComponent->>VideoStore: unblockSource(device.source)
    VideoStore->>BackendAPI: POST API_URL/unblock_source?source_string=source
    alt Unblock request succeeds
      BackendAPI-->>VideoStore: 200 OK
      VideoStore->>VideoStore: fetchDevices()
      VideoStore->>VideoStore: fetchStreams()
      VideoStore-->>VideoDeviceComponent: Updated devices and streams state
      VideoDeviceComponent->>VideoDeviceComponent: Recompute status_color, has_healthy_streams, thumbnails_disabled
    else Unblock request fails
      BackendAPI-->>VideoStore: Error
      VideoStore->>VideoStore: notifier.pushError(VIDEO_SOURCE_UNBLOCK_FAIL)
    end
  end

  User->>VideoDeviceComponent: View device card
  VideoDeviceComponent->>VideoDeviceComponent: Render tooltip, Add stream button, thumbnail based on device.blocked and streams
Loading

Class diagram for updated video manager blocking support

classDiagram
  class Device {
    string source
    Format[] formats
    Control[] controls
    bool blocked
  }

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

  class VideoDeviceComponent {
    +Device device
    +bool show_controls_dialog
    +bool show_stream_creation_dialog
    +bool are_video_streams_available

    +has_configs() bool
    +has_healthy_streams() bool
    +thumbnails_disabled() bool
    +is_pirate_mode() bool
    +status_color() string

    +openControlsDialog() void
    +openStreamCreationDialog() void
    +toggleBlocked() Promise~void~
    +createNewStream(stream CreatedStream) Promise~void~
  }

  class Settings {
    bool is_pirate_mode
  }

  class VideoThumbnail {
    +string source
    +int width
    +bool register
    +bool disabled
  }

  VideoDeviceComponent --> Device : uses
  VideoDeviceComponent --> VideoStore : dispatches_actions
  VideoDeviceComponent --> Settings : reads
  VideoDeviceComponent --> VideoThumbnail : renders
  VideoStore --> Device : fetches
Loading

File-Level Changes

Change Details Files
Add block/unblock support and status indication for video sources in the VideoDevice component.
  • Replace inline stream status color logic with a computed status_color that accounts for blocked devices, no streams, and healthy/unhealthy states.
  • Display a blocked message when device.blocked is true, adjust other status texts, and gate the Add stream button when the device is blocked.
  • Switch from has_running_streams to has_healthy_streams, treating any non-stopped stream as active for UI messaging and thumbnail registration.
  • Introduce a pirate-mode-only v-switch bound to device.blocked to toggle blocking via a new toggleBlocked method.
  • Update video-thumbnail and video-controls-dialog bindings to respect has_healthy_streams and a new thumbnails_disabled flag that disables thumbnail registration when any stream has disable_thumbnails set.
core/frontend/src/components/video-manager/VideoDevice.vue
Introduce store actions for blocking and unblocking video sources via the backend API.
  • Add blockSource and unblockSource Vuex actions that POST to /block_source and /unblock_source with source_string, respectively.
  • On successful block/unblock responses, refresh devices and streams by calling fetchDevices and fetchStreams.
  • On errors, emit user-facing notifications with specific VIDEO_SOURCE_BLOCK_FAIL or VIDEO_SOURCE_UNBLOCK_FAIL keys.
core/frontend/src/store/video.ts
Extend the Device type to carry block-list state.
  • Add an optional blocked boolean field to the Device interface so UI components can reflect block-list status.
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 1.4-add-block-list-mcm-sources branch from 9d98fe9 to 188055b Compare March 19, 2026 19:24
@joaoantoniocardoso joaoantoniocardoso marked this pull request as ready for review March 19, 2026 21:29
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 2 issues, and left some high level feedback:

  • In the blockSource and unblockSource actions, you're mixing async/await with .then/.catch; consider refactoring to a single try/catch with await and explicitly awaiting fetchDevices()/fetchStreams() (e.g., via await Promise.all([...])) so errors propagate cleanly and the UI refresh is deterministic.
  • The toggleBlocked method does not provide any loading/disabled state for the v-switch, so rapid toggling could trigger overlapping blockSource/unblockSource requests; consider tracking a per-device busy state to disable the switch while an update is in flight.
  • The thumbnails_disabled computed currently disables thumbnails if any stream has disable_thumbnails === true; if the intent is to only disable when all associated streams request it, consider adjusting the predicate (e.g., using .every) to better match the desired behavior.
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 refactoring to a single `try/catch` with `await` and explicitly awaiting `fetchDevices()`/`fetchStreams()` (e.g., via `await Promise.all([...])`) so errors propagate cleanly and the UI refresh is deterministic.
- The `toggleBlocked` method does not provide any loading/disabled state for the `v-switch`, so rapid toggling could trigger overlapping `blockSource`/`unblockSource` requests; consider tracking a per-device busy state to disable the switch while an update is in flight.
- The `thumbnails_disabled` computed currently disables thumbnails if any stream has `disable_thumbnails === true`; if the intent is to only disable when all associated streams request it, consider adjusting the predicate (e.g., using `.every`) to better match the desired behavior.

## Individual Comments

### Comment 1
<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 (bug_risk):** Consider handling in-flight toggle state to avoid repeated block/unblock requests and confusing UI feedback.

The `v-switch` stays enabled and `toggleBlocked` doesn’t prevent overlapping requests, so rapid toggling can fire multiple `blockSource`/`unblockSource` calls while `device.blocked` is still stale from props. This can lead to a racy final state and UI lag. Track a local `blockingInProgress` flag to temporarily disable the switch (or ignore further changes) while the request is in flight, and only re-enable it once the updated state is loaded.

Suggested implementation:

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

```

Because I can only see the template portion of this component, a few more changes are required in the `<script>` section of `VideoDevice.vue`:

1. **Track in-flight state in local data**  
   In the component’s `data()` function, add the `blockingInProgress` flag:
   ```js
   data () {
     return {
       // ...existing state
       blockingInProgress: false,
     };
   }
   ```

2. **Update `toggleBlocked` to guard against overlapping requests**  
   Locate the existing `toggleBlocked` method and update it roughly as follows (adjust to match your current implementation and APIs):
   ```js
   methods: {
     async toggleBlocked (nextValue) {
       // Prevent overlapping requests
       if (this.blockingInProgress) {
         return;
       }

       this.blockingInProgress = true;
       try {
         if (nextValue) {
           // user turned the switch ON → block source
           await this.blockSource(this.device.id); // or whatever call you are currently using
         } else {
           // user turned the switch OFF → unblock source
           await this.unblockSource(this.device.id);
         }

         // Option A: if you manage the state locally, update it here:
         // this.device.blocked = nextValue;
         //
         // Option B (recommended if `device` comes from Vuex/parent props):
         // rely on store/parent to refresh `device.blocked` after the API call.
       } finally {
         this.blockingInProgress = false;
       }
     },
   }
   ```

3. **Ensure state refresh after the API call**  
   If `device.blocked` is driven by Vuex or a parent component, make sure the block/unblock actions trigger a refresh of the device data so the prop reflects the final state. If you already have such a refresh (e.g. reloading the device list after the call), no extra work is needed.

These changes ensure that rapid toggling does not send overlapping requests and the `v-switch` is visually disabled while a block/unblock operation is in flight, matching your review comment.
</issue_to_address>

### Comment 2
<location path="core/frontend/src/store/video.ts" line_range="222-228" />
<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()
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid mixing `async/await` with `.then/.catch` and ensure `fetchDevices/Streams` errors are handled.

In `blockSource`/`unblockSource` you `await back_axios(...)` and then chain `.then/.catch`. That means:
- Errors from `fetchDevices()` / `fetchStreams()` aren’t caught, since their promises aren’t awaited.
- Mixing `async/await` with `.then/.catch` makes the flow harder to follow.

Use a single `try/catch` with `await` for all async steps, e.g.:

```ts
@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) {
    const message = `Could not block video source: ${error.message}.`
    notifier.pushError('VIDEO_SOURCE_BLOCK_FAIL', message)
  }
}
```

Same pattern for `unblockSource` so all async work shares one clear error path.

Suggested implementation:

```typescript
  @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)
    }
  }

```

You should apply the same pattern to `unblockSource`:

1. Wrap its body in a `try/catch`.
2. `await` the `back_axios` call to the `/unblock_source` endpoint.
3. `await Promise.all([this.fetchDevices(), this.fetchStreams()])` after the request.
4. In `catch`, construct an appropriate error message and push it via `notifier.pushError` (likely with a key such as `VIDEO_SOURCE_UNBLOCK_FAIL` to match your existing conventions).
</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 +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 (bug_risk): Consider handling in-flight toggle state to avoid repeated block/unblock requests and confusing UI feedback.

The v-switch stays enabled and toggleBlocked doesn’t prevent overlapping requests, so rapid toggling can fire multiple blockSource/unblockSource calls while device.blocked is still stale from props. This can lead to a racy final state and UI lag. Track a local blockingInProgress flag to temporarily disable the switch (or ignore further changes) while the request is in flight, and only re-enable it once the updated state is loaded.

Suggested implementation:

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

Because I can only see the template portion of this component, a few more changes are required in the <script> section of VideoDevice.vue:

  1. Track in-flight state in local data
    In the component’s data() function, add the blockingInProgress flag:

    data () {
      return {
        // ...existing state
        blockingInProgress: false,
      };
    }
  2. Update toggleBlocked to guard against overlapping requests
    Locate the existing toggleBlocked method and update it roughly as follows (adjust to match your current implementation and APIs):

    methods: {
      async toggleBlocked (nextValue) {
        // Prevent overlapping requests
        if (this.blockingInProgress) {
          return;
        }
    
        this.blockingInProgress = true;
        try {
          if (nextValue) {
            // user turned the switch ON → block source
            await this.blockSource(this.device.id); // or whatever call you are currently using
          } else {
            // user turned the switch OFF → unblock source
            await this.unblockSource(this.device.id);
          }
    
          // Option A: if you manage the state locally, update it here:
          // this.device.blocked = nextValue;
          //
          // Option B (recommended if `device` comes from Vuex/parent props):
          // rely on store/parent to refresh `device.blocked` after the API call.
        } finally {
          this.blockingInProgress = false;
        }
      },
    }
  3. Ensure state refresh after the API call
    If device.blocked is driven by Vuex or a parent component, make sure the block/unblock actions trigger a refresh of the device data so the prop reflects the final state. If you already have such a refresh (e.g. reloading the device list after the call), no extra work is needed.

These changes ensure that rapid toggling does not send overlapping requests and the v-switch is visually disabled while a block/unblock operation is in flight, matching your review comment.

Comment on lines +222 to +228
async blockSource(source: string): Promise<void> {
await back_axios({
method: 'post',
url: `${this.API_URL}/block_source`,
timeout: 10000,
params: { source_string: source },
})
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): Avoid mixing async/await with .then/.catch and ensure fetchDevices/Streams errors are handled.

In blockSource/unblockSource you await back_axios(...) and then chain .then/.catch. That means:

  • Errors from fetchDevices() / fetchStreams() aren’t caught, since their promises aren’t awaited.
  • Mixing async/await with .then/.catch makes the flow harder to follow.

Use a single try/catch with await for all async steps, e.g.:

@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) {
    const message = `Could not block video source: ${error.message}.`
    notifier.pushError('VIDEO_SOURCE_BLOCK_FAIL', message)
  }
}

Same pattern for unblockSource so all async work shares one clear error path.

Suggested implementation:

  @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)
    }
  }

You should apply the same pattern to unblockSource:

  1. Wrap its body in a try/catch.
  2. await the back_axios call to the /unblock_source endpoint.
  3. await Promise.all([this.fetchDevices(), this.fetchStreams()]) after the request.
  4. In catch, construct an appropriate error message and push it via notifier.pushError (likely with a key such as VIDEO_SOURCE_UNBLOCK_FAIL to match your existing conventions).

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.

2 participants