diff --git a/src/BootstrapBlazor.Server/Components/Samples/VideoDevices.razor b/src/BootstrapBlazor.Server/Components/Samples/VideoDevices.razor new file mode 100644 index 00000000000..790b0ffac35 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/VideoDevices.razor @@ -0,0 +1,34 @@ +@page "/video-device" +@inject IStringLocalizer Localizer + +

@Localizer["VideoDeviceTitle"]

+ +

@Localizer["VideoDeviceIntro"]

+ +
[Inject, NotNull]
+private IBluetooth? BluetoothService { get; set; }
+ + +
+
+ + + + + +
+
+ +
+
+ + + + @if (!string.IsNullOrEmpty(_previewUrl)) + { + + } +
+ diff --git a/src/BootstrapBlazor.Server/Components/Samples/VideoDevices.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/VideoDevices.razor.cs new file mode 100644 index 00000000000..fcb4abdd807 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/VideoDevices.razor.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Server.Components.Samples; + +/// +/// VideoDevice Component +/// +public partial class VideoDevices +{ + [Inject, NotNull] + private IVideoDevice? VideoDeviceService { get; set; } + + private readonly List _devices = []; + + private List _items = []; + + private string? _deviceId; + + private string? _previewUrl; + + private async Task OnRequestDevice() + { + var devices = await VideoDeviceService.GetDevices(); + if (devices != null) + { + _devices.AddRange(devices); + _items = [.. _devices.Select(i => new SelectedItem(i.DeviceId, i.Label))]; + } + } + + private async Task OnOpenVideo() + { + if (!string.IsNullOrEmpty(_deviceId)) + { + var constraints = new MediaTrackConstraints + { + DeviceId = _deviceId, + VideoSelector = ".bb-video" + }; + await VideoDeviceService.Open(constraints); + } + } + + private async Task OnCloseVideo() + { + _previewUrl = ""; + await VideoDeviceService.Close(".bb-video"); + } + + private async Task OnCapture() + { + _previewUrl = await VideoDeviceService.GetPreviewUrl(); + } + + private async Task OnFlip() + { + await VideoDeviceService.Flip(); + } +} diff --git a/src/BootstrapBlazor.Server/Components/Samples/VideoDevices.razor.css b/src/BootstrapBlazor.Server/Components/Samples/VideoDevices.razor.css new file mode 100644 index 00000000000..386d7ebb984 --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Samples/VideoDevices.razor.css @@ -0,0 +1,16 @@ +.bb-video { + min-height: 240px; + height: auto; + width: auto; + transform: scaleX(-1); + margin-top: 1rem; + display: block; +} + +.bb-image { + border: 1px solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + transform: scaleX(-1); + margin-top: 1rem; + display: block; +} diff --git a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs index 4f7c676f2bd..f293b1a6dd3 100644 --- a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs +++ b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs @@ -1549,6 +1549,12 @@ void AddServices(DemoMenuItem item) Url = "title" }, new() + { + IsNew = true, + Text = Localizer["VideoDevices"], + Url = "video-device" + }, + new() { Text = Localizer["WebSerial"], Url = "web-serial" diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index e8e0184e4ba..3a496e0dbae 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -4920,7 +4920,8 @@ "UniverSheet": "UniverSheet", "ShieldBadge": "ShieldBadge", "OtpInput": "OtpInput", - "TotpService": "ITotpService" + "TotpService": "ITotpService", + "VideoDevices": "IVideoDevice" }, "BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": { "TablesHeaderTitle": "Header grouping function", @@ -7116,5 +7117,16 @@ "UrlDesc": "Use your iPhone's camera or the QR code scanning function to scan the QR code below, then open the Safari browser and open the current address", "WiFiDesc": "Use your iPhone's camera or the QR code scanner to scan the QR code below to automatically join the WiFi network.", "EmailDesc": "Use your iPhone's camera or the QR code scanner to scan the QR code below, then open the Email app to send an email." + }, + "BootstrapBlazor.Server.Components.Samples.VideoDevices": { + "VideoDeviceTitle": "IVideoDevice", + "VideoDeviceIntro": "Get video equipment operation capabilities through this service", + "BaseUsageTitle": "Basic usage", + "BaseUsageIntro": "Perform different operations by calling different API methods", + "VideoDeviceRequestText": "List", + "VideoDeviceOpenText": "Open", + "VideoDeviceCloseText": "Close", + "VideoDeviceCaptureText": "Capture", + "VideoDeviceFlipText": "Flip" } } diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index ede716295d7..d8dbccfe1fb 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -4920,7 +4920,8 @@ "UniverSheet": "表格组件 UniverSheet", "ShieldBadge": "徽章组件 ShieldBadge", "OtpInput": "验证码输入框 OtpInput", - "TotpService": "时间密码验证服务 ITotpService" + "TotpService": "时间密码验证服务 ITotpService", + "VideoDevices": "视频设备服务 IVideoDevice" }, "BootstrapBlazor.Server.Components.Samples.Table.TablesHeader": { "TablesHeaderTitle": "表头分组功能", @@ -7116,5 +7117,16 @@ "UrlDesc": "使用 iPhone 手机相机或者扫描二维码功能扫描下方二维码后,打开 Safari 浏览器并且打开当前地址", "WiFiDesc": "使用 iPhone 手机相机或者扫描二维码功能扫描下方二维码后,自动加入 WiFi 网络", "EmailDesc": "使用 iPhone 手机相机或者扫描二维码功能扫描下方二维码后,打开 Email 应用发送邮件" + }, + "BootstrapBlazor.Server.Components.Samples.VideoDevices": { + "VideoDeviceTitle": "IVideoDevice 视频设备服务", + "VideoDeviceIntro": "通过此服务获得视频设备操作能力", + "BaseUsageTitle": "基本用法", + "BaseUsageIntro": "通过调用不同的 api 方法进行不同操作", + "VideoDeviceRequestText": "枚举设备", + "VideoDeviceOpenText": "打开设备", + "VideoDeviceCloseText": "关闭设备", + "VideoDeviceCaptureText": "截图", + "VideoDeviceFlipText": "翻转镜头" } } diff --git a/src/BootstrapBlazor.Server/docs.json b/src/BootstrapBlazor.Server/docs.json index c86e547fe12..c59c0d20386 100644 --- a/src/BootstrapBlazor.Server/docs.json +++ b/src/BootstrapBlazor.Server/docs.json @@ -230,7 +230,8 @@ "univer-sheet": "UniverSheets", "shield-badge": "ShieldBadges", "opt-input": "OtpInputs", - "otp-service": "OtpServices" + "otp-service": "OtpServices", + "video-device": "VideoDevices" }, "video": { "table": "BV1ap4y1x7Qn?p=1", diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index 1a7f7ef8660..0e6eb0e364b 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@  - 9.6.1-beta02 + 9.6.1-beta01 diff --git a/src/BootstrapBlazor/Components/Camera/Camera.razor.js b/src/BootstrapBlazor/Components/Camera/Camera.razor.js index 37329c3a68f..1c610695f8e 100644 --- a/src/BootstrapBlazor/Components/Camera/Camera.razor.js +++ b/src/BootstrapBlazor/Components/Camera/Camera.razor.js @@ -40,6 +40,9 @@ const play = (camera, option = {}) => { ...option } navigator.mediaDevices.getUserMedia(constrains).then(stream => { + const supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); + console.log(supportedConstraints); + camera.video = { deviceId: option.video.deviceId }; camera.video.element = camera.el.querySelector('video') camera.video.element.srcObject = stream @@ -136,22 +139,46 @@ export function download(id, fileName) { createEl.remove(); } -export function resize(id, width, height) { +export async function resize(id, width, height) { const camera = Data.get(id) if (camera === null || camera.video === void 0) { return } - const constrains = { - video: { - deviceId: camera.video.deviceId, - width: { ideal: width }, - height: { ideal: height } - } + const constraints = { + facingMode: "environment", + width: { ideal: width }, + height: { ideal: height } } + await camera.video.track.applyConstraints(constraints); +} - stopDevice(camera) - play(camera, constrains) + +export async function flip(id) { + const camera = Data.get(id) + if (camera === null || camera.video === void 0) { + return + } + + const { track } = camera.video; + if (track) { + const constraints = track.getConstraints(); + if (constraints.facingMode === void 0) { + constraints.facingMode = { exact: "environment" } + } + const { exact } = constraints.facingMode; + if (exact === void 0) { + constraints.facingMode = { ideal: "environment" } + } + else if (exact === "user") { + constraints.facingMode = { ideal: "environment" } + } + else { + constraints.facingMode = { ideal: "user" } + } + console.log(constraints); + await track.applyConstraints(constraints); + } } export function dispose(id) { diff --git a/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs b/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs index ab81e7c0203..0d482e74430 100644 --- a/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs +++ b/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs @@ -86,6 +86,8 @@ public static IServiceCollection AddBootstrapBlazor(this IServiceCollection serv services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/BootstrapBlazor/Services/MediaDevices/DefaultMediaDevices.cs b/src/BootstrapBlazor/Services/MediaDevices/DefaultMediaDevices.cs new file mode 100644 index 00000000000..d7292f54682 --- /dev/null +++ b/src/BootstrapBlazor/Services/MediaDevices/DefaultMediaDevices.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +class DefaultMediaDevices(IJSRuntime jsRuntime) : IMediaDevices +{ + private JSModule? _module = null; + + private async Task LoadModule() + { + _module ??= await jsRuntime.LoadModuleByName("media"); + return _module; + } + + public async Task?> EnumerateDevices() + { + var module = await LoadModule(); + return await module.InvokeAsync?>("enumerateDevices"); + } + + public async Task Open(MediaTrackConstraints constraints) + { + var module = await LoadModule(); + await module.InvokeVoidAsync("open", constraints); + } + + public async Task Close(string? videoSelector) + { + var module = await LoadModule(); + await module.InvokeVoidAsync("close", videoSelector); + } + + public async Task Capture() + { + var module = await LoadModule(); + await module.InvokeVoidAsync("capture"); + } + + public async Task GetPreviewUrl() + { + var module = await LoadModule(); + return await module.InvokeAsync("getPreviewUrl"); + } + + public async Task Flip() + { + var module = await LoadModule(); + await module.InvokeAsync("flip"); + } +} diff --git a/src/BootstrapBlazor/Services/MediaDevices/DefaultVideoDevice.cs b/src/BootstrapBlazor/Services/MediaDevices/DefaultVideoDevice.cs new file mode 100644 index 00000000000..afd44842007 --- /dev/null +++ b/src/BootstrapBlazor/Services/MediaDevices/DefaultVideoDevice.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +class DefaultVideoDevice(IMediaDevices deviceService) : IVideoDevice +{ + /// + /// + /// + /// + public async Task?> GetDevices() + { + var ret = new List(); + var devices = await deviceService.EnumerateDevices(); + if (devices != null) + { + ret.AddRange(devices.Where(d => d.Kind == "videoinput")); + } + return ret; + } + + public Task Open(MediaTrackConstraints constraints) + { + return deviceService.Open(constraints); + } + + public Task Close(string? videoSelector) + { + return deviceService.Close(videoSelector); + } + + public Task Capture() + { + return deviceService.Capture(); + } + + public Task GetPreviewUrl() + { + return deviceService.GetPreviewUrl(); + } + + public Task Flip() + { + return deviceService.Flip(); + } +} diff --git a/src/BootstrapBlazor/Services/MediaDevices/IMediaDeviceInfo.cs b/src/BootstrapBlazor/Services/MediaDevices/IMediaDeviceInfo.cs new file mode 100644 index 00000000000..71c57b71a37 --- /dev/null +++ b/src/BootstrapBlazor/Services/MediaDevices/IMediaDeviceInfo.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +/// +/// The MediaDeviceInfo interface of the Media Capture and Streams API contains information that describes a single media input or output device. +/// +public interface IMediaDeviceInfo +{ + /// + /// The deviceId read-only property of the MediaDeviceInfo interface returns a string that is an identifier for the represented device and is persisted across sessions. + /// + public string DeviceId { get; } + + /// + /// The groupId read-only property of the MediaDeviceInfo interface returns a string that is a group identifier. + /// + public string GroupId { get; } + + /// + /// The kind read-only property of the MediaDeviceInfo interface returns an enumerated value, that is either "videoinput", "audioinput" or "audiooutput". + /// + public string Kind { get; } + + /// + /// The label read-only property of the MediaDeviceInfo interface returns a string describing this device (for example "External USB Webcam"). + /// + public string Label { get; } +} diff --git a/src/BootstrapBlazor/Services/MediaDevices/IMediaDevices.cs b/src/BootstrapBlazor/Services/MediaDevices/IMediaDevices.cs new file mode 100644 index 00000000000..ea4988a9940 --- /dev/null +++ b/src/BootstrapBlazor/Services/MediaDevices/IMediaDevices.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +/// +/// The MediaDevices interface of the Media Capture and Streams API provides access to connected media input devices like cameras and microphones, as well as screen sharing. In essence, it lets you obtain access to any hardware source of media data. +/// +public interface IMediaDevices +{ + /// + /// An array of MediaDeviceInfo objects. Each object in the array describes one of the available media input and output devices. + /// + /// + Task?> EnumerateDevices(); + + /// + /// The open() method of the MediaDevices interface creates a new MediaStream object and starts capturing media from the specified device. + /// + /// + /// + Task Open(MediaTrackConstraints constraints); + + /// + /// The close() method of the MediaDevices interface stops capturing media from the specified device and closes the MediaStream object. + /// + /// + /// + Task Close(string? videoSelector); + + /// + /// The capture() method of the MediaDevices interface captures a still image from the specified video stream and saves it to the specified location. + /// + /// + Task Capture(); + + /// + /// Gets the preview URL of the captured image. + /// + /// + Task GetPreviewUrl(); + + /// + /// Flip the video device. + /// + /// + Task Flip(); +} diff --git a/src/BootstrapBlazor/Services/MediaDevices/IVideoDevice.cs b/src/BootstrapBlazor/Services/MediaDevices/IVideoDevice.cs new file mode 100644 index 00000000000..7adffb16911 --- /dev/null +++ b/src/BootstrapBlazor/Services/MediaDevices/IVideoDevice.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +/// +/// Video Media Device Interface +/// +public interface IVideoDevice +{ + /// + /// Gets the list of video devices. + /// + /// + Task?> GetDevices(); + + /// + /// Opens the video device with the specified constraints. + /// + /// + /// + Task Open(MediaTrackConstraints constraints); + + /// + /// Close the video device with the specified selector. + /// + /// + /// + Task Close(string? videoSelector); + + /// + /// Capture a still image from the video stream. + /// + /// + Task Capture(); + + ///// + ///// Preview a still image from the video stream. + ///// + ///// + //Task Preview(); + + ///// + ///// Gets the stream of the captured image. + ///// + ///// + //Task GetPreviewImage(); + + /// + /// Gets the preview URL of the captured image. + /// + /// + Task GetPreviewUrl(); + + /// + /// Flip the video device. + /// + /// + Task Flip(); +} diff --git a/src/BootstrapBlazor/Services/MediaDevices/MediaDeviceInfo.cs b/src/BootstrapBlazor/Services/MediaDevices/MediaDeviceInfo.cs new file mode 100644 index 00000000000..12ab88a2cfb --- /dev/null +++ b/src/BootstrapBlazor/Services/MediaDevices/MediaDeviceInfo.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +/// +/// +/// +public class MediaDeviceInfo : IMediaDeviceInfo +{ + /// + /// + /// + public string DeviceId { get; set; } = ""; + + /// + /// + /// + public string GroupId { get; set; } = ""; + + /// + /// + /// + public string Kind { get; set; } = ""; + + /// + /// + /// + public string Label { get; set; } = ""; +} diff --git a/src/BootstrapBlazor/Services/MediaDevices/MediaTrackConstraints.cs b/src/BootstrapBlazor/Services/MediaDevices/MediaTrackConstraints.cs new file mode 100644 index 00000000000..9e5c7b052e2 --- /dev/null +++ b/src/BootstrapBlazor/Services/MediaDevices/MediaTrackConstraints.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +/// +/// The MediaTrackConstraints interface of the Media Capture and Streams API is used to specify constraints on the media tracks that are requested from a media device. It is used in conjunction with the getUserMedia() method to specify the desired properties of the media tracks, such as resolution, frame rate, and aspect ratio. +/// +public class MediaTrackConstraints +{ + /// + /// + /// + public string DeviceId { get; set; } = ""; + + /// + /// + /// + public string? VideoSelector { get; set; } + + /// + /// + /// + public int? Width { get; set; } + + /// + /// + /// + public int? Height { get; set; } + + /// + /// + /// + public string? FacingMode { get; set; } +} diff --git a/src/BootstrapBlazor/wwwroot/modules/media.js b/src/BootstrapBlazor/wwwroot/modules/media.js new file mode 100644 index 00000000000..442bebfe87b --- /dev/null +++ b/src/BootstrapBlazor/wwwroot/modules/media.js @@ -0,0 +1,111 @@ +import { registerBootstrapBlazorModule } from "./utility.js" + +export async function enumerateDevices() { + let ret = null; + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || !navigator.mediaDevices.enumerateDevices) { + console.log("enumerateDevices() not supported."); + } + else { + await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); + const devices = await navigator.mediaDevices.enumerateDevices(); + ret = devices; + } + return ret; +} + +export async function open(options) { + const constrains = { + video: { + deviceId: options.deviceId ? { exact: options.deviceId } : null, + facingMode: { ideal: options.facingMode || "environment" } + }, + audio: false + } + + const { videoSelector, width, height } = options; + if (width) { + constrains.video.width = { ideal: width }; + } + 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; + } + } +} + +export async function close(videoSelector) { + if (videoSelector) { + const video = document.querySelector(videoSelector); + if (video) { + video.pause(); + const stream = video.srcObject; + closeStream(stream); + video.srcObject = null; + } + } + const media = registerBootstrapBlazorModule("MediaDevices"); + const { stream } = media; + if (stream && stream.active) { + closeStream(stream); + } + media.stream = null; +} + +export async function getPreviewUrl() { + let url = null; + const media = registerBootstrapBlazorModule("MediaDevices"); + const { stream } = media; + if (stream && stream.active) { + const tracks = stream.getVideoTracks(); + if (tracks) { + const track = tracks[0]; + const capture = new ImageCapture(track); + const blob = await capture.takePhoto(); + url = URL.createObjectURL(blob); + } + } + 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(); + + tracks.forEach(track => { + track.stop(); + }); + } +} diff --git a/test/UnitTest/Services/VideoDeviceTest.cs b/test/UnitTest/Services/VideoDeviceTest.cs new file mode 100644 index 00000000000..3446801668d --- /dev/null +++ b/test/UnitTest/Services/VideoDeviceTest.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace UnitTest.Services; + +public class VideoDeviceTest : BootstrapBlazorTestBase +{ + [Fact] + public async Task GetDevices_Ok() + { + Context.JSInterop.Setup>("enumerateDevices").SetResult([ + new() { DeviceId = "test-device-id", GroupId = "test-groupd-id", Kind = "videoinput", Label="test-video" } + ]); + var service = Context.Services.GetRequiredService(); + var devices = await service.GetDevices(); + Assert.NotNull(devices); + Assert.Equal("test-device-id", devices[0].DeviceId); + Assert.Equal("test-groupd-id", devices[0].GroupId); + Assert.Equal("videoinput", devices[0].Kind); + Assert.Equal("test-video", devices[0].Label); + } + + [Fact] + public async Task Open_Ok() + { + Context.JSInterop.Setup("getPreviewUrl").SetResult("blob:https://test-preview"); + + var service = Context.Services.GetRequiredService(); + var options = new MediaTrackConstraints() + { + DeviceId = "test-device-id", + FacingMode = "user", + Height = 640, + Width = 480, + VideoSelector = ".bb-video" + }; + await service.Open(options); + await service.Close(".bb-video"); + + Assert.Equal("test-device-id", options.DeviceId); + Assert.Equal("user", options.FacingMode); + Assert.Equal(640, options.Height); + Assert.Equal(480, options.Width); + Assert.Equal(".bb-video", options.VideoSelector); + + await service.Capture(); + await service.Flip(); + var url = await service.GetPreviewUrl(); + Assert.Equal("blob:https://test-preview", url); + } +}