Como usar o MediaRecorder como MediaSource

8

Como um exercício para aprender o WebRTC, estou tentando mostrar a webcam local e lado a lado com uma reprodução atrasada da webcam. Para conseguir isso, estou tentando passar blobs gravados para um BufferSource e usar o MediaSource correspondente como fonte para um elemento de vídeo.

// the ondataavailable callback for the MediaRecorder
async function handleDataAvailable(event) {
  // console.log("handleDataAvailable", event);
  if (event.data && event.data.size > 0) {
    recordedBlobs.push(event.data);
  }

  if (recordedBlobs.length > 5) {
    if (recordedBlobs.length === 5)
      console.log("buffered enough for delayed playback");
    if (!updatingBuffer) {
      updatingBuffer = true;
      const bufferedBlob = recordedBlobs.shift();
      const bufferedAsArrayBuffer = await bufferedBlob.arrayBuffer();
      if (!sourceBuffer.updating) {
        console.log("appending to buffer");
        sourceBuffer.appendBuffer(bufferedAsArrayBuffer);
      } else {
        console.warn("Buffer still updating... ");
        recordedBlobs.unshift(bufferedBlob);
      }
    }
  }
}
// connecting the media source to the video element
recordedVideo.src = null;
recordedVideo.srcObject = null;
recordedVideo.src = window.URL.createObjectURL(mediaSource);
recordedVideo.controls = true;
try {
  await recordedVideo.play();
} catch (e) {
  console.error(`Play failed: ${e}`);
}

Todo o código: https://jsfiddle.net/43rm7258/1/

Quando executo isso no Chromium 78, recebo um NotSupportedError: Failed to load because no supported source was found.do playelemento do elemento de vídeo.

Não tenho idéia do que estou fazendo de errado ou como proceder neste momento.

Trata-se de algo semelhante, mas não me ajuda: o MediaSource interrompe aleatoriamente o vídeo

Este exemplo foi o ponto de partida: https://webrtc.github.io/samples/src/content/getusermedia/record/

André
fonte

Respostas:

9

Em suma

É fácil fazê-lo funcionar no Firefox e Chrome: você só precisa adicionar um codec de áudio à sua lista de codecs! video/webm;codecs=opus,vp8

Conseguir que ele funcione no Safari é significativamente mais complicado. MediaRecorder é um recurso "experimental" que deve ser ativado manualmente nas opções do desenvolvedor. Uma vez ativado, o Safari não possui um isTypeSupportedmétodo, então você precisa lidar com isso. Por fim, não importa o que você solicite do MediaRecorder, o Safari sempre fornecerá um arquivo MP4 - que não pode ser transmitido da maneira que o WEBM pode. Isso significa que você precisa executar transmuxing em JavaScript para converter o formato do contêiner de vídeo rapidamente

O Android deve funcionar se o Chrome funcionar

O iOS não suporta Extensões de Origem de Mídia, portanto, SourceBuffernão está definido no iOS e toda a solução não funcionará

Correio Original

Observando o JSFiddle que você postou, uma solução rápida antes de começarmos:

  • Você faz referência a uma variável errorMsgElementque nunca é definida. Você deve adicionar um <div>à página com um ID apropriado e criar uma const errorMsgElement = document.querySelector(...)linha para capturá-lo

Agora, algo a ser observado ao trabalhar com o Media Source Extensions e o MediaRecorder é que o suporte será muito diferente por navegador. Mesmo que essa seja uma parte "padronizada" da especificação do HTML5, ela não é muito consistente entre as plataformas. Na minha experiência, conseguir que o MediaRecorder funcione no Firefox não exige muito esforço, fazê-lo funcionar no Chrome é um pouco mais difícil, fazê-lo funcionar no Safari é quase impossível, e fazê-lo funcionar no iOS não é literalmente algo que você pode fazer.

Analisei e depurei isso por navegador e registrei minhas etapas, para que você possa entender algumas das ferramentas disponíveis ao depurar problemas de mídia

Raposa de fogo

Quando fiz o check-out do seu JSFiddle no Firefox, vi o seguinte erro no console:

NotSupportedError: Uma faixa de áudio não pode ser gravada: video / webm; codecs = vp8 indica um codec não suportado

Lembro que o VP8 / VP9 foi um grande impulso do Google e, como tal, pode não funcionar no Firefox, então tentei fazer um pequeno ajuste no seu código. Eu removi o , options)parâmetro da sua chamada para new MediaRecorder(). Isso indica ao navegador para usar o codec que desejar, portanto, você provavelmente obterá uma saída diferente em cada navegador (mas deve pelo menos funcionar em todos os navegadores)

Como funcionou no Firefox, verifiquei o Chrome.

cromada

Desta vez, recebi um novo erro:

(índice): 409 Não detectado (em promessa) DOMException: falha ao executar 'appendBuffer' em 'SourceBuffer': este SourceBuffer foi removido da fonte de mídia pai. em MediaRecorder.handleDataAvailable ( https://fiddle.jshell.net/43rm7258/1/show/:409:22 )

Então, fui para o chrome: // media-internals / no meu navegador e vi o seguinte:

O opus do codec de fluxo de áudio não corresponde aos codecs do SourceBuffer.

No seu código, você está especificando um codec de vídeo (VP9 ou VP8), mas não um codec de áudio; portanto, o MediaRecorder permite que o navegador escolha qualquer codec de áudio que desejar. Parece que no MediaRecorder do Chrome, por padrão, escolhe "opus" como codec de áudio, mas o SourceBuffer do Chrome, por padrão, escolhe outra coisa. Isso foi trivialmente corrigido. Atualizei suas duas linhas que definem o seguinte options.mimeType:

  • options = { mimeType: "video/webm;codecs=opus, vp9" };
  • options = { mimeType: "video/webm;codecs=opus, vp8" };

Como você usa o mesmo optionsobjeto para declarar o MediaRecorder e o SourceBuffer, adicionar o codec de áudio à lista significa que o SourceBuffer agora é declarado com um codec de áudio válido e o vídeo é reproduzido

Por uma boa medida, testei o novo código (com um codec de áudio) no Firefox. Isso funcionou! Então, somos 2 a 2 apenas adicionando o codec de áudio à optionslista (e deixando-o nos parâmetros para declarar o MediaRecorder)

Parece que o VP8 e o opus funcionam no Firefox, mas não são os padrões (embora, ao contrário do Chrome, o padrão para MediaRecorder e SourceBuffer sejam os mesmos, e é por isso que a remoção do optionsparâmetro funcionou totalmente)

Safári

Dessa vez, ocorreu um erro que talvez não consiga solucionar:

Rejeição de promessa não tratada: ReferenceError: Não é possível encontrar a variável: MediaRecorder

A primeira coisa que fiz foi o Google "Safari MediaRecorder", que apareceu neste artigo . Eu pensei em tentar, então dei uma olhada. Com certeza:

Propriedades do Safari

Cliquei para ativar o MediaRecorder e recebi o seguinte no console:

Rejeição de promessa não tratada: TypeError: MediaRecorder.isTypeSupported não é uma função. (Em 'MediaRecorder.isTypeSupported (options.mimeType)', 'MediaRecorder.isTypeSupported' está indefinido)

Portanto, o Safari não tem o isTypeSupportedmétodo. Não se preocupe, apenas diremos "se esse método não existir, assuma o Safari e defina o tipo de acordo"

  if (MediaRecorder.isTypeSupported) {
    options = { mimeType: "video/webm;codecs=vp9" };
    if (!MediaRecorder.isTypeSupported(options.mimeType)) {
      console.error(`${options.mimeType} is not Supported`);
      errorMsgElement.innerHTML = `${options.mimeType} is not Supported`;
      options = { mimeType: "video/webm;codecs=vp8" };
      if (!MediaRecorder.isTypeSupported(options.mimeType)) {
        console.error(`${options.mimeType} is not Supported`);
        errorMsgElement.innerHTML = `${options.mimeType} is not Supported`;
        options = { mimeType: "video/webm" };
        if (!MediaRecorder.isTypeSupported(options.mimeType)) {
          console.error(`${options.mimeType} is not Supported`);
          errorMsgElement.innerHTML = `${options.mimeType} is not Supported`;
          options = { mimeType: "" };
        }
      }
    }
  } else {
    options = { mimeType: "" };
  }

Agora eu só precisava encontrar um mimeType compatível com o Safari. Alguma pesquisa leve sugere que o H.264 é suportado, então tentei:

options = { mimeType: "video/webm;codecs=h264" };

Isso me deu êxito MediaRecorder started, mas falhou na linha addSourceBuffercom o novo erro:

NotSupportedError: A operação não é suportada.

Vou continuar a tentar diagnosticar como fazer isso funcionar no Safari, mas por enquanto eu pelo menos lidei com o Firefox e o Chrome

Atualização 1

Continuei trabalhando no Safari. Infelizmente, o Safari não possui as ferramentas do Chrome e do Firefox para se aprofundar nas mídias internas, por isso há muitas suposições envolvidas.

Eu já havia descoberto que estávamos recebendo um erro "A operação não é suportada" ao tentar ligar addSourceBuffer. Então, criei uma página única para tentar chamar apenas esse método em diferentes circunstâncias:

  • Talvez adicione um buffer de origem antes de playser chamado no vídeo
  • Talvez adicione um buffer de origem antes que a fonte de mídia seja anexada a um elemento de vídeo
  • Talvez adicione um buffer de origem com codecs diferentes
  • etc

Eu descobri que o problema ainda era o codec e que a mensagem de erro sobre a "operação" não ser permitida era um pouco enganadora. Foram os parâmetros que não foram permitidos. O simples fornecimento de "h264" funcionou para o MediaRecorder, mas o SourceBuffer precisava que eu passasse os parâmetros do codec .

Uma das primeiras coisas que eu tentei estava indo para a página de amostra MDN e copiar os codecs que eles usaram lá: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'. Isso deu o mesmo erro "operação não permitida". Cavando o significado desses parâmetros de codec (como o que diabos 42E01Eainda significa ?). Enquanto eu gostaria de ter uma resposta melhor, enquanto pesquisava no Google, me deparei com este post do StackOverflow que mencionava o uso 'video/mp4; codecs="avc1.64000d,mp4a.40.2"'no Safari. Eu tentei e os erros do console sumiram!

Embora os erros do console tenham desaparecido agora, ainda não estou vendo nenhum vídeo. Portanto, ainda há trabalho a fazer.

Atualização 2

Uma investigação mais aprofundada no Depurador no Safari (colocando vários pontos de interrupção e inspecionando variáveis ​​em cada etapa do processo) descobriu que isso handleDataAvailablenunca estava sendo chamado no Safari. Parece que no Firefox e Chrome mediaRecorder.start(100)seguirá adequadamente as especificações e chamará a ondatavailablecada 100 milissegundos, mas o Safari ignora o parâmetro e armazena tudo em um Blob enorme. A chamada mediaRecorder.stop()manual provocou ondataavailablea chamada com tudo o que havia sido gravado até aquele momento

Tentei usar setIntervalpara chamar a mediaRecorder.requestData()cada 100 milissegundos, mas requestDatanão foi definido no Safari (assim como isTypeSupportednão foi definido). Isso me deixou meio confuso.

Em seguida, tentei limpar todo o objeto MediaRecorder e criar um novo a cada 100 milissegundos, mas isso gerou um erro na linha await bufferedBlob.arrayBuffer(). Eu ainda estou investigando por que aquele falhou

Atualização 3

Uma coisa que lembro sobre o formato MP4 é que o átomo "moov" é necessário para reproduzir qualquer conteúdo. É por isso que você não pode baixar a metade do meio de um arquivo MP4 e reproduzi-lo. Você precisa baixar o arquivo INTEIRO. Então, eu me perguntava se o fato de ter selecionado o MP4 era o motivo de eu não estar recebendo atualizações regulares.

Tentei mudar video/mp4para alguns valores diferentes e obtive resultados variados:

  • video/webm - A operação não é suportada
  • video/x-m4v- Me comportei como MP4, só consegui dados quando .stop()foi chamado
  • video/3gpp - Comportou-se como MP4
  • video/flv - A operação não é suportada
  • video/mpeg - Comportou-se como MP4

Tudo se comportando como MP4 me levou a inspecionar os dados que realmente estavam sendo transmitidos handleDataAvailable. Foi quando eu notei isso:

insira a descrição da imagem aqui

Não importa o que eu selecionei para o formato de vídeo, o Safari estava sempre me dando um MP4!

De repente, lembrei-me por que o Safari era um pesadelo e por que eu o havia classificado mentalmente como "quase impossível". Para unir vários MP4s seria necessário um transmuxer JavaScript

Foi quando me lembrei, foi exatamente o que eu havia feito antes . Trabalhei com o MediaRecorder e o SourceBuffer há pouco mais de um ano para tentar criar um player RTMP JavaScript. Depois que o player terminou, eu queria adicionar suporte ao DVR (procurando voltar para as partes do vídeo que já haviam sido transmitidas), o que fiz usando o MediaRecorder e mantendo um buffer de anel na memória dos blobs de vídeo de 1 segundo. No Safari, passei esses blobs de vídeo através do transmuxer que eu havia codificado para convertê-los de MP4 para ISO-BMFF, para concatená-los juntos.

Gostaria de poder compartilhar o código com você, mas tudo pertence ao meu antigo empregador - então, neste momento, a solução foi perdida para mim. Sei que alguém teve o problema de compilar o FFMPEG para JavaScript usando o emscripten, para que você possa tirar proveito disso.

stevendesu
fonte
Uau! Muito obrigado por isso! Estou realmente impressionado com o esforço que você coloca nisso. Vou revisar suas descobertas e ver se consigo fazer as coisas funcionarem.
André
1
Com seus ponteiros, eu realmente o tenho trabalhando no Chrome, eu estava perto da solução, mas não sabia como continuar com ela. Obrigado por explicar os passos que você tomou para encontrá-lo. Obrigado novamente, sua resposta foi realmente útil e eu aprendi muito.
André