Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions core/frontend/src/components/video-manager/VideoDevice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
mdi-circle
</v-icon>
</template>
<span v-if="!are_video_streams_available">
<span v-if="device.blocked">
Video source is blocked
</span>
<span v-else-if="!are_video_streams_available">
No streams added to this video source
</span>
<span v-else-if="has_healthy_streams">
Expand Down Expand Up @@ -43,12 +46,21 @@
</v-btn>
<v-btn
class="my-1"
:disabled="!is_redirect_source && (are_video_streams_available || updating_streams)"
:disabled="device.blocked || (!is_redirect_source && (are_video_streams_available || updating_streams))"
@click="openStreamCreationDialog"
>
<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"
Comment on lines +55 to +62
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.

/>
</div>
</div>
<div>
Expand Down Expand Up @@ -104,6 +116,7 @@
import Vue, { PropType } from 'vue'

import SpinningLogo from '@/components/common/SpinningLogo.vue'
import settings from '@/libs/settings'
import video from '@/store/video'
import {
CreatedStream, Device, StreamStatus,
Expand Down Expand Up @@ -160,7 +173,13 @@ export default Vue.extend({
(stream) => stream.video_and_stream.stream_information?.extended_configuration?.disable_thumbnails === true,
)
},
is_pirate_mode(): boolean {
return settings.is_pirate_mode
},
status_color(): string {
if (this.device.blocked) {
return 'warning'
}
if (!this.are_video_streams_available) {
return 'grey'
}
Expand All @@ -178,6 +197,14 @@ export default Vue.extend({
this.show_stream_creation_dialog = true
},

async toggleBlocked(): Promise<void> {
if (this.device.blocked) {
await video.unblockSource(this.device.source)
} else {
await video.blockSource(this.device.source)
}
},

async createNewStream(stream: CreatedStream): Promise<void> {
await video.createStream(stream)
},
Expand Down
36 changes: 36 additions & 0 deletions core/frontend/src/store/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,42 @@ class VideoStore extends VuexModule {
}
}

@Action
async blockSource(source: string): Promise<void> {
await back_axios({
method: 'post',
url: `${this.API_URL}/block_source`,
timeout: 10000,
params: { source_string: source },
})
Comment on lines +249 to +255
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).

.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 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 resetSettings(): Promise<void> {
await back_axios({
Expand Down
1 change: 1 addition & 0 deletions core/frontend/src/types/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export interface Device {
source: string
formats: Format[]
controls: Control[]
blocked?: boolean
}

export enum VideoCaptureType {
Expand Down
Loading