Trabalhando com Câmera no Blazor WASM

Usar a câmera no Blazor WASM é uma forma prática de levar suas aplicações além do CRUD, combinando o C# no navegador com as APIs nativas do dispositivo. Neste artigo, você vai abrir a câmera, exibir o vídeo em tempo real, tirar uma foto e salvar a imagem em disco, tudo com JavaScript puro e interop do Blazor.

Blazor WebAssembly e PWAs formam uma dupla poderosa para o desenvolvimento web moderno. Por isso, vale explorar recursos que antes pareciam exclusivos de aplicativos nativos, como a câmera.

Por que usar a câmera no Blazor WASM

Muitas aplicações precisam capturar imagens, como documentos, fotos de perfil ou registros de entrega. Assim, oferecer a câmera no Blazor WASM evita que o usuário saia do app para tirar a foto em outro lugar.

O Blazor WebAssembly executa o .NET diretamente no navegador. No entanto, o acesso a dispositivos como câmera e microfone continua sendo feito pelas APIs do próprio navegador, expostas em JavaScript.

Em outras palavras, a câmera no Blazor WASM depende de uma ponte entre o C# e o JavaScript. Essa ponte se chama JavaScript interop, e o Blazor oferece suporte nativo a ela.

Quando o app também é um PWA, a câmera no Blazor WASM fica ainda mais interessante. Afinal, o usuário instala a aplicação no celular e usa a câmera traseira como em um app de loja.

Criando o projeto Blazor WASM para a câmera

Para começar, crie um novo projeto do tipo blazorwasm, que será a base para interagir com a câmera. Antes disso, confirme que o SDK do .NET está instalado na sua máquina.

Os exemplos usam o template do .NET 8, no qual a página inicial se chama Pages/Home.razor. Então, execute os comandos abaixo no terminal:

dotnet new blazorwasm -o BlzCam
cd BlzCam

O primeiro comando cria o projeto na pasta BlzCam. Em seguida, o segundo comando entra nessa pasta para que você possa editar e executar a aplicação.

Funções JavaScript

Neste exemplo, você vai usar JavaScript puro, sem plugins nem bibliotecas adicionais. De fato, o JavaScript é necessário aqui porque a API de câmera é fornecida pelo navegador.

Inclusive, esse cenário é uma ótima oportunidade para criar uma biblioteca que encapsula a câmera no Blazor WASM. Dessa forma, outros projetos reutilizam a mesma integração.

Crie um arquivo chamado camera.js dentro da pasta wwwroot/js. Normalmente essa pasta não vem criada no template, então fique à vontade para criá-la.

O arquivo terá quatro funções: iniciar a câmera, parar a câmera, tirar a foto e salvar a foto em disco. Por enquanto, cada uma apenas escreve uma mensagem no console para testes:

window.startCamera = () => {
    console.log('Starting camera');
}

window.stopCamera = () => {
    console.log('Stopping camera');
}

window.takePhoto = () => {
    console.log('Taking photo');
}

window.savePhoto = () => {
    console.log('Saving photo');
}

Mais adiante você vai substituir esses métodos pela implementação real. Por fim, referencie o script no arquivo wwwroot/index.html, logo abaixo da referência ao script do Blazor WebAssembly:

<script src="js/camera.js"></script>

A documentação sobre a localização do JavaScript no Blazor recomenda carregar scripts externos no fim do <body>. Além disso, ela desaconselha colocar scripts no <head>, porque o interop pode falhar se o código depender do Blazor.

Invocando o JavaScript com IJSRuntime

A próxima missão é chamar essas funções JavaScript a partir do Blazor, e isso é relativamente simples. O exemplo usa a página Pages/Home.razor, mas você pode aplicar o mesmo código em qualquer página.

O primeiro passo é injetar o IJSRuntime, que o próprio framework registra no container de dependências:

@inject IJSRuntime JsRuntime

Com a injeção feita, você pode usar JsRuntime.InvokeAsync<T> para funções que retornam valores. Por outro lado, o JsRuntime.InvokeVoidAsync atende funções que não retornam nada.

Segundo a documentação de interop do Blazor, o identificador da função é relativo ao escopo global window. Por isso, basta informar o nome startCamera, sem nenhum registro prévio.

Agora crie quatro métodos em C# que correspondem às funções JavaScript:

@code
{
    public async Task StartCamera()
    {
        await JsRuntime.InvokeVoidAsync("startCamera");
    }

    public async Task StopCamera()
    {
        await JsRuntime.InvokeVoidAsync("stopCamera");
    }

    public async Task TakePhoto()
    {
        await JsRuntime.InvokeVoidAsync("takePhoto");
    }

    public async Task SavePhoto()
    {
        await JsRuntime.InvokeVoidAsync("savePhoto");
    }
}

Note que os métodos são praticamente iguais, já que as funções JavaScript também são simples até aqui. Em seguida, crie quatro botões, cada um chamando um dos métodos:

<button @onclick="StartCamera">Start Camera</button>
<button @onclick="StopCamera">Stop Camera</button>
<button @onclick="TakePhoto">Take Photo</button>
<button @onclick="SavePhoto">Save Photo</button>

Neste momento, você já pode executar a aplicação e acompanhar os cliques no console do Developer Tools (F12 ou CTRL+SHIFT+I):

dotnet watch run

Abrindo a câmera no Blazor WASM

O primeiro desafio real é abrir a câmera no Blazor WASM. Aqui, alguns detalhes merecem atenção, como as permissões e a forma como o navegador inicializa o dispositivo.

Na prática, o objetivo é exibir um espelho da câmera na tela. Assim, o usuário vê o que está sendo captado e só então tira a foto, que nada mais é do que capturar um frame do vídeo.

O elemento video do HTML

Para exibir o conteúdo da câmera em tempo real, o melhor componente é o próprio video do HTML. Ele reproduz arquivos em formatos como MP4, mas também recebe dados por stream, que é justamente o caso aqui.

Adicione o elemento de vídeo na página e atribua a ele o id blazor-video:

<video autoplay id="blazor-video"></video>

Deixe o atributo autoplay ativo. Caso contrário, a imagem da câmera ficará pausada até que o usuário clique em play.

O objeto navigator e o getUserMedia

Os navegadores disponibilizam diversas APIs para tarefas como geolocalização, área de transferência e credenciais. Todas elas ficam dentro de um objeto raiz chamado navigator.

A API que interessa aqui é a mediaDevices, que fornece acesso às mídias conectadas ao dispositivo. Dentro dela, o método getUserMedia solicita o acesso à câmera:

navigator.mediaDevices.getUserMedia();

Conforme a referência do getUserMedia no MDN, o método retorna uma Promise que entrega um objeto MediaStream quando a mídia fica disponível. Ou seja, o retorno não é imediato.

Essa demora acontece principalmente por causa da inicialização do dispositivo, que varia conforme a máquina do usuário. Além disso, no primeiro acesso, o navegador pergunta se o usuário permite o uso da câmera.

É comum ficar vários segundos esperando sem perceber o aviso de permissão, que aparece perto da barra de endereço. Portanto, oriente o usuário na interface quando a câmera demorar a abrir.

Restrições do vídeo

Outro ponto importante é definir as restrições iniciais do vídeo, como largura, altura e qual câmera usar. Para isso, você monta um objeto video com as propriedades facingMode, width e height:

const constraints = {
    video: {
        facingMode: 'environment',
        width: {
            max: 1920,
            ideal: 1024
        },
        height: {
            max: 1080,
            ideal: 768
        }
    }
};

A propriedade facingMode indica a câmera desejada. O valor environment pede a câmera voltada para o ambiente, ou seja, a traseira do celular, enquanto user pede a câmera frontal.

Nas dimensões, max define o limite e ideal define o valor que o navegador tenta alcançar. Segundo o MDN, o ideal aceita alternativas, enquanto exact torna o valor obrigatório.

Exibindo a imagem da câmera

Com as restrições prontas, use o .then() para aguardar a Promise do getUserMedia. Quando ela for resolvida, você recebe o objeto stream, que representa o vídeo capturado.

Em seguida, obtenha a referência ao elemento video com document.getElementById e atribua o stream à propriedade srcObject. Dessa forma, o código final do método startCamera fica assim:

window.startCamera = () => {
    navigator
        .mediaDevices
        .getUserMedia({
            video: {
                facingMode: 'environment',
                width: {
                    max: 1920,
                    ideal: 1024
                },
                height: {
                    max: 1080,
                    ideal: 768
                }
            }
        })
        .then((stream) => {
            // Liga o stream da câmera ao elemento de vídeo
            document.getElementById('blazor-video').srcObject = stream;
        })
        .catch((error) => {
            // Permissão negada, câmera ausente ou dispositivo ocupado
            console.error('Unable to start camera', error);
        });
}

Teste o código e clique no botão Start Camera. Então, a imagem da câmera no Blazor WASM aparece na tela, logo depois que você autoriza o acesso.

Permissões e contexto seguro da câmera no Blazor WASM

O MDN informa que o getUserMedia só funciona em contextos seguros, como páginas servidas por HTTPS ou por localhost. Por isso, a câmera no Blazor WASM funciona no ambiente de desenvolvimento, mas exige HTTPS quando a aplicação vai para produção.

Fora de um contexto seguro, o navigator.mediaDevices fica indefinido. Nesse caso, a chamada falha com um TypeError antes mesmo de pedir permissão.

O bloco .catch() adicionado ao startCamera captura os erros mais comuns documentados pelo MDN:

  • NotAllowedError: o usuário negou a permissão ou uma política do navegador bloqueou o acesso.
  • NotFoundError: nenhum dispositivo atende ao tipo de mídia solicitado.
  • NotReadableError: o hardware falhou, por exemplo, porque outro programa já usa a câmera.
  • OverconstrainedError: nenhuma câmera consegue atender às restrições informadas.

Além disso, o usuário pode simplesmente ignorar o aviso de permissão. Nesse cenário, a Promise pode não ser resolvida nem rejeitada, então a interface precisa lidar com a espera.

Fechando a câmera no Blazor WASM

O próximo passo é fechar a câmera no Blazor WASM quando o usuário não quiser mais mantê-la aberta. Esse processo é simples, mas exige um cuidado: um stream pode conter várias faixas, como vídeo e áudio.

Portanto, você precisa parar todas as faixas em execução no elemento de vídeo. Primeiro, obtenha a referência ao elemento:

const videoPreview = document.getElementById('blazor-video');

Como o código vai manipular o conteúdo do elemento, um srcObject vazio causaria erro. Então, valide esse ponto antes de continuar:

if (videoPreview.srcObject) {
    // Só segue quando existe um stream ativo
}

Com a validação feita, obtenha o stream atribuído ao elemento e percorra as faixas com getTracks e forEach. Para cada faixa, chame track.stop():

window.stopCamera = () => {
    const videoPreview = document.getElementById('blazor-video');

    if (videoPreview.srcObject) {
        const stream = videoPreview.srcObject;
        stream
            .getTracks()
            .forEach((track) => track.stop());
    }
}

Ao parar as faixas, o navegador libera o dispositivo e apaga o indicador de câmera em uso. Isso protege a privacidade de quem usa a câmera no Blazor WASM e poupa bateria.

Tirando a foto com a câmera no Blazor WASM

Tirar uma foto com a câmera no Blazor WASM nada mais é do que obter um frame do vídeo. Afinal, um vídeo é apenas uma sequência de imagens.

Aqui, a ideia é capturar o que está sendo exibido e usar esse frame como foto. Embora seja possível fazer isso sem nenhum outro elemento, o canvas traz vantagens claras.

O canvas é uma área em branco onde você desenha elementos 2D e 3D e manipula imagens. Com ele, o usuário visualiza a foto antes de salvar, e você ainda pode aplicar filtros.

De fato, tudo o que estiver no canvas será salvo, incluindo efeitos, rabiscos ou qualquer outra modificação. Então, adicione o elemento logo abaixo do video:

<canvas id="blazor-canvas"></canvas>

Referências e tamanho do canvas

Como a imagem vai passar do vídeo para o canvas, você precisa da referência aos dois elementos:

const videoPreview = document.getElementById('blazor-video');
const canvas = document.getElementById('blazor-canvas');

Com as referências em mãos, defina as dimensões do canvas. O exemplo usa as mesmas dimensões reais do vídeo, mas você pode usar valores maiores ou menores:

canvas.width = videoPreview.videoWidth;
canvas.height = videoPreview.videoHeight;

Caso queira gerar miniaturas, basta reduzir a largura e a altura do canvas proporcionalmente.

Contexto 2D e o frame

O canvas trabalha com diferentes contextos de desenho, por isso você precisa garantir o uso do contexto 2D. Para isso, crie uma constante com o retorno de getContext('2d'):

const context = canvas.getContext('2d');

Agora use o método drawImage do contexto para desenhar no canvas. Como o vídeo já está rodando no elemento videoPreview, basta passá-lo como origem:

context.drawImage(videoPreview, 0, 0);

Os dois zeros representam as coordenadas X e Y onde o desenho começa. Se quiser aplicar um filtro, faça isso logo depois desta linha.

O código completo do método takePhoto fica assim:

window.takePhoto = () => {
    const videoPreview = document.getElementById('blazor-video');
    const canvas = document.getElementById('blazor-canvas');

    // Usa as dimensões reais do vídeo
    canvas.width = videoPreview.videoWidth;
    canvas.height = videoPreview.videoHeight;
    const context = canvas.getContext('2d');

    // Copia o frame atual do vídeo para o canvas
    context.drawImage(videoPreview, 0, 0);
}

Salvando a foto da câmera no Blazor WASM

A última tarefa é salvar a foto da câmera no Blazor WASM, o que significa fazer o download da imagem. Existem várias formas de implementar isso, e aqui você verá uma das mais simples.

O primeiro passo é obter a referência ao canvas, já que é nele que está o conteúdo a ser baixado:

const canvas = document.getElementById('blazor-canvas');

Convertendo o canvas para imagem

O canvas possui um método integrado chamado toDataURL, que converte o conteúdo para uma URL de dados em formatos de imagem. Por exemplo, a linha abaixo gera um PNG:

canvas.toDataURL('image/png');

Segundo a referência do toDataURL no MDN, o PNG é o formato padrão. Além disso, formatos como image/jpeg e image/webp aceitam um segundo parâmetro de qualidade, entre 0 e 1.

Exibir ou baixar

O resultado do toDataURL está pronto para ser exibido no navegador. Esse detalhe importa, porque o navegador usa o tipo MIME para decidir o que fazer com o conteúdo.

Com o tipo image/png, o navegador interpreta os dados como imagem e a exibe na tela, em vez de baixá-la. Embora o usuário pudesse salvar a imagem manualmente, a experiência fica pior.

Para resolver, troque o tipo MIME para image/octet-stream com um simples replace. Assim, o navegador entende que deve baixar o conteúdo em vez de exibi-lo:

canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream');

Download e nome do arquivo

Com o tipo MIME ajustado, você poderia usar window.location para abrir a imagem, mas essa abordagem costuma gerar problemas. Por isso, a solução mais estável é criar um elemento a e clicar nele via JavaScript.

O atributo download do elemento a indica ao navegador que o link deve ser baixado. Para finalizar, a função savePhoto recebe o nome do arquivo como parâmetro:

window.savePhoto = (fileName) => {
    const canvas = document.getElementById('blazor-canvas');
    const link = document.createElement('a');

    // Define o nome do arquivo e o conteúdo do download
    link.setAttribute('download', fileName);
    link.setAttribute('href', canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream'));
    link.click();
}

Do lado do Blazor, gere um nome curto e passe esse valor para a função JavaScript. O exemplo usa os oito primeiros caracteres de um Guid:

public async Task SavePhoto()
{
    var fileName = Guid.NewGuid().ToString("N")[..8];
    await JsRuntime.InvokeVoidAsync("savePhoto", $"{fileName}.png");
}

Com isso, cada clique em Save Photo baixa um PNG com nome único. Dessa forma, o fluxo completo da câmera no Blazor WASM fica pronto.

Boas práticas para a câmera no Blazor WASM

O exemplo funciona, mas alguns cuidados deixam a câmera no Blazor WASM pronta para produção. Primeiro, considere componentizar o vídeo, o canvas e os botões em um único componente Razor reutilizável.

Em seguida, evite poluir o escopo global com funções soltas no window. A documentação do Blazor recomenda isolar o código em módulos JavaScript, carregados com import por meio do IJSObjectReference.

Também é uma boa prática chamar stopCamera quando o usuário sai da página. Assim, a câmera não continua ligada em segundo plano depois da navegação.

Outro ponto é o tamanho das imagens. O MDN alerta que o toDataURL guarda a imagem inteira em uma string na memória, então o toBlob com URL.createObjectURL é uma alternativa melhor para fotos grandes.

Por fim, se você pretende transformar o app em PWA, veja como criar um PWA com Blazor WASM. Para entender melhor os modelos de execução, leia também uma visão geral do Blazor.

Código fonte e próximos passos

O código completo deste exemplo está disponível no repositório blazor-camera-sample no GitHub. Então, deixe uma estrela no projeto e acompanhe o perfil para ver novos exemplos.

Se quiser se aprofundar em Progressive Web Apps, o balta.io promoveu a Jornada PWA. Ela ensina a criar PWAs, uma solução moderna, fácil de manter e com alcance em diversas lojas de aplicativos.

Banner de divulgação da Jornada PWA do balta.io, sobre criação de Progressive Web Apps com recursos como a câmera no Blazor WASM
A Jornada PWA aprofunda a criação de Progressive Web Apps com Blazor.

Participar da Jornada PWA

Conclusão

Trabalhar com a câmera no Blazor WASM é relativamente simples quando você entende o papel de cada parte. O Blazor cuida da interface e do C#, enquanto o JavaScript acessa as APIs nativas do navegador.

Neste artigo, você abriu a câmera com getUserMedia, exibiu o vídeo, fechou as faixas, capturou o frame no canvas e baixou a foto. Além disso, viu como tratar permissões, contexto seguro e erros comuns.

Com esse interop, o Blazor deixa de ser apenas uma ferramenta de formulários. Portanto, componentize essa solução e reutilize a câmera no Blazor WASM em todos os projetos que precisarem capturar imagens.

FAQ

Preciso de HTTPS para usar a câmera no Blazor WASM?

Sim, em produção. O getUserMedia só funciona em contextos seguros, então localhost atende ao desenvolvimento, mas o site publicado precisa de HTTPS.

Como escolher entre a câmera frontal e a traseira?

Use a propriedade facingMode nas restrições do vídeo. O valor user pede a câmera frontal, e environment pede a traseira.

Dá para acessar a câmera sem JavaScript no Blazor?

Não diretamente. A API de câmera pertence ao navegador e é exposta em JavaScript, por isso o Blazor usa o interop com IJSRuntime.

Por que a câmera continua ligada depois que saio da página?

Isso acontece quando as faixas do stream não são encerradas. Chame track.stop() em todas as faixas ao sair da página ou ao fechar a câmera.

Posso salvar a foto em JPEG em vez de PNG?

Sim. Passe image/jpeg para o toDataURL e, se quiser, informe a qualidade entre 0 e 1 no segundo parâmetro.