I’m developing a web application that allows users to switch between the front and back cameras on their device. While the front camera works fine, I’m encountering an issue where the back camera only shows a black screen.
camera.js
// wwwroot/camera.js
let currentStream = null;
async function openCamera(facingMode) {
if (currentStream) {
currentStream.getTracks().forEach(track => track.stop());
}
try {
const constraints = {
video: {
facingMode: facingMode
}
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
currentStream = stream;
document.getElementById('videoFeed').srcObject = stream;
console.log(`Camera opened with facing mode: ${facingMode}`);
} catch (err) {
console.error('Error accessing camera: ', err);
}
}
function stopCamera() {
if (currentStream) {
currentStream.getTracks().forEach(track => track.stop());
currentStream = null;
}
}
async function startVideo(src) {
try {
const permission = await checkCameraPermission();
if (permission === 'prompt' || permission === 'granted') {
navigator.getUserMedia(
{ video: true, audio: false },
function (localMediaStream) {
let video = document.getElementById(src);
video.srcObject = localMediaStream;
video.onloadedmetadata = function (e) {
video.play();
};
},
function (err) {
console.error('Error accessing camera:', err);
throw err; // Propagate the error
}
);
} else {
console.error('Camera permission denied.');
}
} catch (error) {
console.error('Error starting video:', error);
}
}
function getFrame(src, dest, dotnetHelper) {
let video = document.getElementById(src);
let canvas = document.getElementById(dest);
// Check if the video and canvas elements exist before drawing the image
if (video && canvas) {
canvas.getContext('2d').drawImage(video, 0, 0, 150, 150);
// Resize the image on the canvas
let resizedCanvas = document.createElement('canvas');
resizedCanvas.width = 200; // Set desired width
resizedCanvas.height = 200; // Set desired height
let ctx = resizedCanvas.getContext('2d');
ctx.drawImage(canvas, 0, 0, resizedCanvas.width, resizedCanvas.height);
// Convert the resized image to base64 JPEG format
let dataUrl = resizedCanvas.toDataURL("image/jpeg");
// Invoke the .NET method with the resized image data
dotnetHelper.invokeMethodAsync('ProcessImage', dataUrl);
} else {
console.error('Video or canvas element not found.');
}
}
function stopVideo(src) {
let video = document.getElementById(src);
// Check if the video element exists before stopping
if (video) {
if ('srcObject' in video) {
let tracks = video.srcObject.getTracks();
tracks.forEach(track => track.stop());
video.srcObject = null;
} else {
video.src = '';
}
} else {
console.error('Video element with ID ' + src + ' not found.');
}
}
function closeCamera() {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({ video: true })
.then(function (stream) {
// Stop all tracks
let tracks = stream.getTracks();
tracks.forEach(track => track.stop());
})
.catch(function (error) {
console.error('Error closing the camera: ', error);
});
} else {
console.error('getUserMedia is not supported on this browser.');
}
}
Razorpage
@using MudBlazor
@inject IJSRuntime JSR
@inject ISnackbar Snackbar
<div>
<MudDialog Style="overflow: hidden;">
<DialogContent>
<MudCard Class="pa-2">
<MudCardContent>
<div style="display: flex; justify-content: center; align-items: center;">
<video id="videoFeed" width="600" height="300"></video>
</div>
<canvas class="d-none" id="currentFrame" width="150" height="150"></canvas>
<div style="display: flex; justify-content: center; margin-top: 16px;">
<MudIconButton Icon="@Icons.Material.Filled.CameraAlt" Color="Color.Primary" Size="Size.Large" OnClick="@Save" />
<MudButton OnClick="ToggleCamera" Color="Color.Primary">@buttonText</MudButton>
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Cancel" Size="Size.Large" OnClick="@Cancel" />
</div>
</MudCardContent>
</MudCard>
</DialogContent>
</MudDialog>
</div>
@code {
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
[Parameter] public string ImageUri { get; set; }
private bool isFrontCamera = true;
private string buttonText = "Open Back Camera";
private DotNetObjectReference<CameraDialog> oCounter;
private string frameUri;
private bool nestedVisible = false;
private bool isVideoStarted = false;
protected override async Task OnInitializedAsync()
{
try
{
await JSR.InvokeVoidAsync("startVideo", "videoFeed");
isVideoStarted = true;
}
catch (Exception ex)
{
Snackbar.Add("Camera access is denied. Please allow access to the camera.", Severity.Error);
}
}
private async Task ToggleCamera()
{
if (isFrontCamera)
{
await OpenCamera("environment");
buttonText = "Open Front Camera";
}
else
{
await OpenCamera("user");
buttonText = "Open Back Camera";
}
isFrontCamera = !isFrontCamera;
}
private async Task OpenCamera(string facingMode)
{
await JSR.InvokeVoidAsync("openCamera", facingMode);
}
private async Task Save()
{
if (!isVideoStarted)
{
Snackbar.Add("Camera access is denied. Please allow access to the camera.", Severity.Error);
MudDialog.Cancel();
return;
}
if (oCounter == null)
oCounter = DotNetObjectReference.Create(this);
try
{
await JSR.InvokeAsync<string>("getFrame", "videoFeed", "currentFrame", oCounter);
await JSR.InvokeVoidAsync("stopVideo", "videoFeed");
isVideoStarted = false;
MudDialog.Close(DialogResult.Ok(frameUri));
}
catch (Exception ex)
{
if (ex.Message.Contains("Cannot read properties of null (reading 'getTracks')"))
{
Snackbar.Add("Camera access is denied. Please give camera permission in the browser settings.", Severity.Error);
MudDialog.Cancel();
}
else
{
Snackbar.Add($"An error occurred while capturing the image. Please try again. Error details: {ex.Message}", Severity.Error);
MudDialog.Cancel();
}
}
}
[JSInvokable]
public async Task ProcessImage(string imageString)
{
frameUri = imageString;
StateHasChanged();
var parameters = new DialogParameters { { "ImageUri", frameUri } };
var options = new DialogOptions { FullWidth = true };
}
private async Task Cancel()
{
if (isVideoStarted)
{
try
{
await JSR.InvokeVoidAsync("stopVideo", "videoFeed");
isVideoStarted = false;
MudDialog.Cancel();
}
catch (Exception ex)
{
if (ex.Message.Contains("Cannot read properties of null (reading 'getTracks')"))
{
MudDialog.Cancel();
}
else
{
MudDialog.Cancel();
}
}
}
}
}
What I Have Tried I tried adding a delay between stopping the current stream and starting the new one. I added console logs to debug the flow, but still facing issues. Expected Behavior The back camera should activate immediately after clicking the "Rotate Camera" button without showing a black screen.
Actual Behavior The back camera either shows a black screen or takes multiple clicks to activate. I’m testing this on multiple devices, and the issue persists across different browsers and devices. Camera permissions are granted properly. Any Help or Suggestions? I’d appreciate any help or suggestions to resolve this issue. Thank you!
2
Answers
Test Code
site.css
camera.js
index.html
Camera.razor
CameraDialog.cs
UPDATE
Test in iPhone 13 with ios17
Test Result