Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ private IBluetooth? BluetoothService { get; set; }</Pre>
<div class="row form-inline g-3">
<div class="col-12">
<Button Text="@Localizer["VideoDeviceRequestText"]" Icon="fa-solid fa-photo-film" OnClick="OnRequestDevice"></Button>
<Button Text="@Localizer["VideoDeviceOpenText"]" Icon="fa-solid fa-play" OnClick="OnOpenVideo" class="ms-2"></Button>
<Button Text="@Localizer["VideoDeviceCloseText"]" Icon="fa-solid fa-stop" OnClick="OnCloseVideo" class="ms-2"></Button>
<Button Text="@Localizer["VideoDeviceCaptureText"]" Icon="fa-solid fa-camera" OnClick="OnCapture" class="ms-2"></Button>
<Button Text="@Localizer["VideoDeviceFlipText"]" Icon="fa-solid fa-camera-rotate" OnClick="OnFlip" class="ms-2"></Button>
<Button Text="@Localizer["VideoDeviceOpenText"]" Icon="fa-solid fa-play" OnClick="OnOpenVideo" IsDisabled="_isOpen" class="ms-2"></Button>
<Button Text="@Localizer["VideoDeviceCloseText"]" Icon="fa-solid fa-stop" OnClick="OnCloseVideo" IsDisabled="!_isOpen" class="ms-2"></Button>
<Button Text="@Localizer["VideoDeviceCaptureText"]" Icon="fa-solid fa-camera" OnClick="OnCapture" IsDisabled="!_isOpen" class="ms-2"></Button>
</div>
<div class="col-12">
<Select Items="@_items" @bind-Value="_deviceId" DisplayText="Devices" ShowLabel="true"></Select>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ public partial class VideoDevices

private string? _previewUrl;

private bool _isOpen = false;

private async Task OnRequestDevice()
{
var devices = await VideoDeviceService.GetDevices();
if (devices != null)
{
_devices.Clear();
_devices.AddRange(devices);
_items = [.. _devices.Select(i => new SelectedItem(i.DeviceId, i.Label))];
}
Expand All @@ -40,7 +43,7 @@ private async Task OnOpenVideo()
DeviceId = _deviceId,
VideoSelector = ".bb-video"
};
await VideoDeviceService.Open(constraints);
_isOpen = await VideoDeviceService.Open(constraints);
}
}

Expand All @@ -54,9 +57,4 @@ private async Task OnCapture()
{
_previewUrl = await VideoDeviceService.GetPreviewUrl();
}

private async Task OnFlip()
{
await VideoDeviceService.Flip();
}
}
10 changes: 2 additions & 8 deletions src/BootstrapBlazor/Services/MediaDevices/DefaultMediaDevices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ private async Task<JSModule> LoadModule()
return await module.InvokeAsync<List<MediaDeviceInfo>?>("enumerateDevices");
}

public async Task Open(MediaTrackConstraints constraints)
public async Task<bool> Open(MediaTrackConstraints constraints)
{
var module = await LoadModule();
await module.InvokeVoidAsync("open", constraints);
return await module.InvokeAsync<bool>("open", constraints);
}

public async Task Close(string? videoSelector)
Expand All @@ -44,10 +44,4 @@ public async Task Capture()
var module = await LoadModule();
return await module.InvokeAsync<string?>("getPreviewUrl");
}

public async Task Flip()
{
var module = await LoadModule();
await module.InvokeAsync<string?>("flip");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class DefaultVideoDevice(IMediaDevices deviceService) : IVideoDevice
return ret;
}

public Task Open(MediaTrackConstraints constraints)
public Task<bool> Open(MediaTrackConstraints constraints)
{
return deviceService.Open(constraints);
}
Expand All @@ -41,9 +41,4 @@ public Task Capture()
{
return deviceService.GetPreviewUrl();
}

public Task Flip()
{
return deviceService.Flip();
}
}
8 changes: 1 addition & 7 deletions src/BootstrapBlazor/Services/MediaDevices/IMediaDevices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public interface IMediaDevices
/// </summary>
/// <param name="constraints"></param>
/// <returns></returns>
Task Open(MediaTrackConstraints constraints);
Task<bool> Open(MediaTrackConstraints constraints);

/// <summary>
/// The close() method of the MediaDevices interface stops capturing media from the specified device and closes the MediaStream object.
Expand All @@ -41,10 +41,4 @@ public interface IMediaDevices
/// </summary>
/// <returns></returns>
Task<string?> GetPreviewUrl();

/// <summary>
/// Flip the video device.
/// </summary>
/// <returns></returns>
Task Flip();
}
8 changes: 1 addition & 7 deletions src/BootstrapBlazor/Services/MediaDevices/IVideoDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public interface IVideoDevice
/// </summary>
/// <param name="constraints"></param>
/// <returns></returns>
Task Open(MediaTrackConstraints constraints);
Task<bool> Open(MediaTrackConstraints constraints);

/// <summary>
/// Close the video device with the specified selector.
Expand Down Expand Up @@ -53,10 +53,4 @@ public interface IVideoDevice
/// </summary>
/// <returns></returns>
Task<string?> GetPreviewUrl();

/// <summary>
/// Flip the video device.
/// </summary>
/// <returns></returns>
Task Flip();
}
48 changes: 16 additions & 32 deletions src/BootstrapBlazor/wwwroot/modules/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,25 @@ export async function open(options) {
if (height) {
constrains.video.height = { ideal: height };
}
const stream = await navigator.mediaDevices.getUserMedia(constrains);
const media = registerBootstrapBlazorModule("MediaDevices");
media.stream = stream;

if (videoSelector) {
const video = document.querySelector(videoSelector);
if (video) {
video.srcObject = stream;
let ret = false;
try {
const stream = await navigator.mediaDevices.getUserMedia(constrains);
const media = registerBootstrapBlazorModule("MediaDevices");
media.stream = stream;

if (videoSelector) {
const video = document.querySelector(videoSelector);
if (video) {
video.srcObject = stream;
}
}
ret = true;
}
catch (err) {
console.error("Error accessing media devices.", err);
}
return ret;
}

export async function close(videoSelector) {
Expand Down Expand Up @@ -75,31 +84,6 @@ export async function getPreviewUrl() {
return url;
}

export async function flip() {
const media = registerBootstrapBlazorModule("MediaDevices");
const { stream } = media;
if (stream && stream.active) {
const tracks = stream.getVideoTracks();
if (tracks) {
const track = tracks[0];
const constraints = track.getSettings();
const { facingMode } = constraints;
if (facingMode === void 0) {
console.log('facingMode is not supported');
return;
}

if (facingMode === "user" || facingMode.exact === "user" || facingMode.ideal === "user") {
constraints.facingMode = { ideal: "environment" }
}
else {
constraints.facingMode = { ideal: "user" }
}
await track.applyConstraints(constraints);
}
}
}

const closeStream = stream => {
if (stream) {
const tracks = stream.getTracks();
Expand Down
6 changes: 4 additions & 2 deletions test/UnitTest/Services/VideoDeviceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public async Task GetDevices_Ok()
public async Task Open_Ok()
{
Context.JSInterop.Setup<string?>("getPreviewUrl").SetResult("blob:https://test-preview");
Context.JSInterop.Setup<bool>("open", _ => true).SetResult(true);

var service = Context.Services.GetRequiredService<IVideoDevice>();
var options = new MediaTrackConstraints()
Expand All @@ -36,7 +37,9 @@ public async Task Open_Ok()
Width = 480,
VideoSelector = ".bb-video"
};
await service.Open(options);
var open = await service.Open(options);
Assert.True(open);

await service.Close(".bb-video");

Assert.Equal("test-device-id", options.DeviceId);
Expand All @@ -46,7 +49,6 @@ public async Task Open_Ok()
Assert.Equal(".bb-video", options.VideoSelector);

await service.Capture();
await service.Flip();
var url = await service.GetPreviewUrl();
Assert.Equal("blob:https://test-preview", url);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Add test case for open method failure

Add a test for when JS navigator.mediaDevices.getUserMedia fails, verifying that the C# Open method returns false on interop failure.

Suggested implementation:

        Assert.Equal("blob:https://test-preview", url);
    }

    [Fact]
    public async Task Open_ReturnsFalse_WhenUserMediaFails()
    {
        // Arrange
        var jsRuntime = new FailingMockJSRuntime();
        var service = new VideoDeviceService(jsRuntime);

        // Act
        bool result = await service.Open(".bb-video");

        // Assert
        Assert.False(result);
    }

    // Failing mock that simulates navigator.mediaDevices.getUserMedia failure.
    private class FailingMockJSRuntime : IJSRuntime
    {
        public Task<TValue> InvokeAsync<TValue>(string identifier, object[] args)
        {
            return Task.FromException<TValue>(new Exception("navigator.mediaDevices.getUserMedia error"));
        }

        public Task<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args)
        {
            return Task.FromException<TValue>(new Exception("navigator.mediaDevices.getUserMedia error"));
        }
    }

Make sure that:

  1. The test framework (xUnit) and required namespaces (e.g., using System.Threading, using System.Threading.Tasks, using Microsoft.JSInterop, and using Xunit) are referenced at the top of the file.
  2. The VideoDeviceService class has a constructor that accepts an IJSRuntime, and its Open method internally catches the JS interop failure and returns false.
  3. Adjust the details if your codebase implements JS interop differently.

Expand Down