WebCodecs로 동영상 처리

동영상 스트림 구성요소 조작

Eugene Zemtsov
Eugene Zemtsov
François Beaufort
François Beaufort

최신 웹 기술은 동영상을 사용할 수 있는 다양한 방법을 제공합니다. Media Stream API Media Recording API, Media Source API, WebRTC API가 합쳐짐 다양한 도구가 포함되어 있습니다. 이러한 API는 특정한 상위 수준의 작업을 해결할 때 웹이 프로그래머는 프레임과 같은 동영상 스트림의 개별 구성 요소를 인코딩한 동영상 또는 오디오의 다중화되지 않은 청크입니다 이러한 기본 구성 요소에 대한 하위 수준 액세스를 얻기 위해 개발자는 WebAssembly를 사용하여 브라우저로 동영상 및 오디오 코덱을 가져옵니다. 하지만 주어진 최신 브라우저는 이미 다양한 코덱 (종종 코덱 없음)과 함께 리패키징하였으나, WebAssembly는 인적 및 컴퓨터 자원을 활용합니다.

WebCodecs API는 이러한 비효율성을 제거합니다. 미디어 구성 요소를 사용하는 방법을 프로그래머에게 제공하여 있습니다. 특히 다음에 주의해야 합니다.

  • 동영상 및 오디오 디코더
  • 동영상 및 오디오 인코더
  • 원본 동영상 프레임
  • 이미지 디코더

WebCodecs API는 동영상 편집기, 화상 회의, 화상 회의 등 미디어 콘텐츠가 처리되는 방식 사용할 수 있습니다.

동영상 처리 워크플로

프레임은 동영상 처리의 핵심입니다. 따라서 WebCodecs에서는 대부분의 클래스가 생성하는 데 사용됩니다 동영상 인코더는 프레임을 인코딩해서 처리할 수 있습니다. 동영상 디코더는 이와 반대로 작동합니다.

또한 VideoFrameCanvasImageSource가 되고 CanvasImageSource를 허용하는 생성자를 보유함으로써 다른 웹 API와 원활하게 작동합니다. 따라서 drawImage()texImage2D()과 같은 함수에서 사용할 수 있습니다. 또한 캔버스, 비트맵, 동영상 요소 및 기타 동영상 프레임으로부터 구성할 수 있습니다.

WebCodecs API는 Insertable Streams API의 클래스와 함께 원활하게 작동합니다. 이는 WebCodecs를 미디어 스트림 트랙에 연결합니다.

  • MediaStreamTrackProcessor는 미디어 트랙을 개별 프레임으로 분할합니다.
  • MediaStreamTrackGenerator는 프레임 스트림에서 미디어 트랙을 만듭니다.

WebCodecs 및 웹 작업자

설계상 WebCodecs API는 기본 스레드 외부에서 모든 어려운 작업을 비동기식으로 처리합니다. 그러나 프레임 및 청크 콜백은 종종 초당 여러 번 호출될 수 있으므로 기본 스레드가 어수선해져서 웹사이트의 응답성이 떨어질 수 있습니다. 따라서 개별 프레임 및 인코딩된 청크 처리를 있습니다

이를 위해 ReadableStream을 미디어로부터 들어오는 모든 프레임을 자동으로 전송하는 편리한 방법을 제공합니다. 작업자에 전달합니다 예를 들어 MediaStreamTrackProcessor를 사용하면 ReadableStream: 웹 카메라에서 들어오는 미디어 스트림 트랙 그 이후에는 스트림이 웹 작업자로 전송되어 프레임을 하나씩 읽고 큐에 추가합니다. VideoEncoder로 변환합니다.

HTMLCanvasElement.transferControlToOffscreen를 사용하면 기본 스레드 외부에서 렌더링도 실행할 수 있습니다. 그러나 모든 고급 도구들이 VideoFrame 자체는 양도되며 더 쉽게 제어할 수 있습니다

WebCodecs 실행

인코딩

<ph type="x-smartling-placeholder">
</ph> 캔버스 또는 ImageBitmap에서 네트워크 또는 스토리지로의 경로
Canvas 또는 ImageBitmap에서 네트워크 또는 스토리지로의 경로

모든 것은 VideoFrame로 시작합니다. 동영상 프레임을 구성하는 방법에는 세 가지가 있습니다.

  • 캔버스, 이미지 비트맵, 동영상 요소와 같은 이미지 소스에서 가져옵니다.

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • MediaStreamTrackProcessor를 사용하여 MediaStreamTrack에서 프레임 가져오기

    const stream = await navigator.mediaDevices.getUserMedia({});
    const track = stream.getTracks()[0];
    
    const trackProcessor = new MediaStreamTrackProcessor(track);
    
    const reader = trackProcessor.readable.getReader();
    while (true) {
      const result = await reader.read();
      if (result.done) break;
      const frameFromCamera = result.value;
    }
    
  • BufferSource의 바이너리 픽셀 표현으로 프레임을 만듭니다.

    const pixelSize = 4;
    const init = {
      timestamp: 0,
      codedWidth: 320,
      codedHeight: 200,
      format: "RGBA",
    };
    const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize);
    for (let x = 0; x < init.codedWidth; x++) {
      for (let y = 0; y < init.codedHeight; y++) {
        const offset = (y * init.codedWidth + x) * pixelSize;
        data[offset] = 0x7f;      // Red
        data[offset + 1] = 0xff;  // Green
        data[offset + 2] = 0xd4;  // Blue
        data[offset + 3] = 0x0ff; // Alpha
      }
    }
    const frame = new VideoFrame(data, init);
    

출처에 관계없이 프레임은 인코더-디코더 VideoEncoder가 있는 EncodedVideoChunk 객체

인코딩하기 전에 VideoEncoder에 두 개의 JavaScript 객체를 제공해야 합니다.

  • 인코딩된 청크 및 오류가 발생했습니다. 이 함수는 개발자가 정의하며 이후에 변경할 수 없습니다. VideoEncoder 생성자에 전달됩니다.
  • 출력을 위한 매개변수가 포함된 인코더 구성 객체 있습니다. 나중에 configure()를 호출하여 이러한 매개변수를 변경할 수 있습니다.

구성이 설정되지 않으면 configure() 메서드에서 NotSupportedError이 발생합니다. 브라우저에서 지원되지 않습니다. 정적 메서드를 호출하는 것이 좋습니다. VideoEncoder.isConfigSupported(): 구성이 지원되는지 확인하고 프로미스를 기다립니다.

const init = {
  output: handleChunk,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  width: 640,
  height: 480,
  bitrate: 2_000_000, // 2 Mbps
  framerate: 30,
};

const { supported } = await VideoEncoder.isConfigSupported(config);
if (supported) {
  const encoder = new VideoEncoder(init);
  encoder.configure(config);
} else {
  // Try another config.
}

인코더가 설정되고 나면 encode() 메서드를 통해 프레임을 수락할 준비가 됩니다. configure()encode()는 둘 다 실제 작업을 완료해야 합니다. 이를 통해 여러 프레임이 encodeQueueSize는 큐에서 대기 중인 요청 수를 보여줍니다. 완료되어야 합니다. 인수가 메서드 호출 순서가 API 계약을 위반하거나 error() 코덱 구현 시 발생한 문제에 대한 콜백을 제공합니다. 인코딩이 성공적으로 완료되면 output() 콜백은 인수로 새롭게 인코딩된 청크를 사용하여 호출됩니다. 여기서 또 다른 중요한 점은 프레임이 렌더링되지 않은 상태이면 close()를 호출하여 더 오래 필요합니다.

let frameCounter = 0;

const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor(track);

const reader = trackProcessor.readable.getReader();
while (true) {
  const result = await reader.read();
  if (result.done) break;

  const frame = result.value;
  if (encoder.encodeQueueSize > 2) {
    // Too many frames in flight, encoder is overwhelmed
    // let's drop this frame.
    frame.close();
  } else {
    frameCounter++;
    const keyFrame = frameCounter % 150 == 0;
    encoder.encode(frame, { keyFrame });
    frame.close();
  }
}

마지막으로, 입력 시퀀스를 처리하는 함수를 작성하여 인코딩 코드를 인코더에서 나올 때 청크로 변환합니다. 일반적으로 이 기능은 네트워크를 통해 데이터 청크를 전송하거나 미디어로 다중화합니다. kube-APIserver로 전송합니다

function handleChunk(chunk, metadata) {
  if (metadata.decoderConfig) {
    // Decoder needs to be configured (or reconfigured) with new parameters
    // when metadata has a new decoderConfig.
    // Usually it happens in the beginning or when the encoder has a new
    // codec specific binary configuration. (VideoDecoderConfig.description).
    fetch("/upload_extra_data", {
      method: "POST",
      headers: { "Content-Type": "application/octet-stream" },
      body: metadata.decoderConfig.description,
    });
  }

  // actual bytes of encoded data
  const chunkData = new Uint8Array(chunk.byteLength);
  chunk.copyTo(chunkData);

  fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream" },
    body: chunkData,
  });
}

언젠가 대기 중인 모든 인코딩 요청에 완료되면 flush()를 호출하고 프로미스를 기다릴 수 있습니다.

await encoder.flush();

디코딩

<ph type="x-smartling-placeholder">
</ph> 네트워크 또는 스토리지에서 캔버스 또는 ImageBitmap까지의 경로입니다.
네트워크 또는 스토리지에서 Canvas 또는 ImageBitmap까지의 경로입니다.

VideoDecoder를 설정하는 것은 VideoEncoder: 디코더가 생성되면 두 함수가 전달되고 코덱은 매개변수는 configure()에 제공됩니다.

코덱 매개변수 집합은 코덱마다 다릅니다. 예: H.264 코덱 바이너리 blob이 필요할 수 있음 단, 부록 B 형식 (encoderConfig.avc = { format: "annexb" })으로 인코딩되지 않는 한

const init = {
  output: handleFrame,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  codedWidth: 640,
  codedHeight: 480,
};

const { supported } = await VideoDecoder.isConfigSupported(config);
if (supported) {
  const decoder = new VideoDecoder(init);
  decoder.configure(config);
} else {
  // Try another config.
}

디코더가 초기화되면 디코더에 EncodedVideoChunk 객체를 제공할 수 있습니다. 청크를 만들려면 다음이 필요합니다.

  • 인코딩된 동영상 데이터의 BufferSource
  • 마이크로초 단위의 청크 시작 타임스탬프 (청크에서 첫 번째로 인코딩된 프레임의 미디어 시간)
  • 청크의 유형(다음 중 하나) <ph type="x-smartling-placeholder">
      </ph>
    • 청크가 이전 청크와 독립적으로 디코딩될 수 있는 경우 key
    • delta: 하나 이상의 이전 청크가 디코딩된 후에만 청크를 디코딩할 수 있는 경우

또한 인코더에서 내보낸 모든 청크는 있는 그대로 디코더에 준비됩니다. 오류 보고 및 비동기식 특성에 대해 앞서 설명한 모든 사항 인코더의 메서드가 디코더에도 동일하게 적용됩니다.

const responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
  const chunk = new EncodedVideoChunk({
    timestamp: responses[i].timestamp,
    type: responses[i].key ? "key" : "delta",
    data: new Uint8Array(responses[i].body),
  });
  decoder.decode(chunk);
}
await decoder.flush();

이제 새로 디코딩된 프레임이 페이지에 표시되는 방법을 보여드리겠습니다. 그것은 디코더 출력 콜백 (handleFrame())이 빠르게 회복할 수 있습니다. 아래 예에서는 렌더링할 준비가 되었습니다. 렌더링은 별도로 발생하며 다음 두 단계로 구성됩니다.

  1. 프레임을 표시할 적절한 시간을 기다리는 중입니다.
  2. 캔버스에 프레임을 그립니다.

프레임이 더 이상 필요하지 않으면 close()를 호출하여 기본 메모리를 해제합니다. 이렇게 하면 가비지 컬렉터가 얻기 전에 발생하는 웹 애플리케이션에서 사용하는 메모리의 양을 나타냅니다.

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let pendingFrames = [];
let underflow = true;
let baseTime = 0;

function handleFrame(frame) {
  pendingFrames.push(frame);
  if (underflow) setTimeout(renderFrame, 0);
}

function calculateTimeUntilNextFrame(timestamp) {
  if (baseTime == 0) baseTime = performance.now();
  let mediaTime = performance.now() - baseTime;
  return Math.max(0, timestamp / 1000 - mediaTime);
}

async function renderFrame() {
  underflow = pendingFrames.length == 0;
  if (underflow) return;

  const frame = pendingFrames.shift();

  // Based on the frame's timestamp calculate how much of real time waiting
  // is needed before showing the next frame.
  const timeUntilNextFrame = calculateTimeUntilNextFrame(frame.timestamp);
  await new Promise((r) => {
    setTimeout(r, timeUntilNextFrame);
  });
  ctx.drawImage(frame, 0, 0);
  frame.close();

  // Immediately schedule rendering of the next frame
  setTimeout(renderFrame, 0);
}

개발자 팁

미디어 패널 사용 를 사용하여 미디어 로그를 확인하고 WebCodecs를 디버그할 수 있습니다.

<ph type="x-smartling-placeholder">
</ph> WebCodecs 디버깅을 위한 Media 패널 스크린샷
WebCodecs 디버깅을 위한 Chrome DevTools의 미디어 패널

데모

아래 데모는 캔버스의 애니메이션 프레임이 어떻게 작동하는지 보여줍니다.

  • 25fps로 촬영된 동영상을 MediaStreamTrackProcessorReadableStream에 촬영
  • 웹 작업자에게 전송
  • H.264 동영상 형식으로 인코딩
  • 다시 디코딩하여
  • transferControlToOffscreen()를 사용하여 두 번째 캔버스에서 렌더링됩니다.

기타 데모

다른 데모도 확인해 보세요.

WebCodecs API 사용

특성 감지

WebCodecs 지원 여부를 확인하려면 다음 단계를 따르세요.

if ('VideoEncoder' in window) {
  // WebCodecs API is supported.
}

WebCodecs API는 보안 컨텍스트에서만 사용할 수 있으며, 따라서 self.isSecureContext가 false인 경우 감지가 실패합니다.

의견

Chrome팀은 WebCodecs API 사용 경험에 대해 의견을 듣고자 합니다.

API 설계에 대해 알려주세요.

API에서 예상대로 작동하지 않는 부분이 있나요? 또는 아이디어를 구현하는 데 필요한 메서드나 속성이 빠졌는가? 질문이나 의견이 있으면 알려주세요. 다음에서 사양 문제를 신고하세요. 해당 GitHub 저장소를 확인하거나 기존 문제에 대한 생각을 전달할 수 있습니다

구현 문제 신고

Chrome 구현에서 버그를 발견하셨나요? 아니면 구현이 어떻게 해야 할까요? new.crbug.com에서 버그를 신고합니다. 가능한 한 많은 세부정보를 포함해 주세요. 구성요소 상자에 Blink>Media>WebCodecs를 입력합니다. Glitch는 쉽고 빠른 재현을 공유하는 데 효과적입니다.

API 지원 표시

WebCodecs API를 사용할 계획이신가요? 여러분의 공적 후원은 Chrome팀이 기능의 우선순위를 정하고 다른 브라우저 공급업체에 그들을 지원하는 일입니다

media-dev@chromium.org로 이메일을 보내거나 트윗을 보냅니다. 해시태그를 사용하여 @ChromiumDev로 변경 #WebCodecs 어디서 어떻게 사용하는지 Google에 알려주세요.

히어로 이미지 제공 데니스 얀스 Unsplash를 참고하세요.