동영상 스트림 구성요소 조작
최신 웹 기술은 동영상을 사용할 수 있는 다양한 방법을 제공합니다. Media Stream API Media Recording API, Media Source API, WebRTC API가 합쳐짐 다양한 도구가 포함되어 있습니다. 이러한 API는 특정한 상위 수준의 작업을 해결할 때 웹이 프로그래머는 프레임과 같은 동영상 스트림의 개별 구성 요소를 인코딩한 동영상 또는 오디오의 다중화되지 않은 청크입니다 이러한 기본 구성 요소에 대한 하위 수준 액세스를 얻기 위해 개발자는 WebAssembly를 사용하여 브라우저로 동영상 및 오디오 코덱을 가져옵니다. 하지만 주어진 최신 브라우저는 이미 다양한 코덱 (종종 코덱 없음)과 함께 리패키징하였으나, WebAssembly는 인적 및 컴퓨터 자원을 활용합니다.
WebCodecs API는 이러한 비효율성을 제거합니다. 미디어 구성 요소를 사용하는 방법을 프로그래머에게 제공하여 있습니다. 특히 다음에 주의해야 합니다.
- 동영상 및 오디오 디코더
- 동영상 및 오디오 인코더
- 원본 동영상 프레임
- 이미지 디코더
WebCodecs API는 동영상 편집기, 화상 회의, 화상 회의 등 미디어 콘텐츠가 처리되는 방식 사용할 수 있습니다.
동영상 처리 워크플로
프레임은 동영상 처리의 핵심입니다. 따라서 WebCodecs에서는 대부분의 클래스가 생성하는 데 사용됩니다 동영상 인코더는 프레임을 인코딩해서 처리할 수 있습니다. 동영상 디코더는 이와 반대로 작동합니다.
또한 VideoFrame
는 CanvasImageSource
가 되고 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">![캔버스 또는 ImageBitmap에서 네트워크 또는 스토리지로의 경로](https://developer.chrome.google.cn/static/docs/web-platform/best-practices/webcodecs/image/the-path-a-canvas-an-im-79c5fdb232597.png?authuser=19&hl=ko)
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">![네트워크 또는 스토리지에서 캔버스 또는 ImageBitmap까지의 경로입니다.](https://developer.chrome.google.cn/static/docs/web-platform/best-practices/webcodecs/image/the-path-the-network-st-fde08124cbe74.png?authuser=19&hl=ko)
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()
)이
빠르게 회복할 수 있습니다. 아래 예에서는
렌더링할 준비가 되었습니다.
렌더링은 별도로 발생하며 다음 두 단계로 구성됩니다.
- 프레임을 표시할 적절한 시간을 기다리는 중입니다.
- 캔버스에 프레임을 그립니다.
프레임이 더 이상 필요하지 않으면 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">![WebCodecs 디버깅을 위한 Media 패널 스크린샷](https://developer.chrome.google.cn/static/docs/web-platform/best-practices/webcodecs/image/screenshot-the-media-pan-c6b0366039008.png?authuser=19&hl=ko)
데모
아래 데모는 캔버스의 애니메이션 프레임이 어떻게 작동하는지 보여줍니다.
- 25fps로 촬영된 동영상을
MediaStreamTrackProcessor
로ReadableStream
에 촬영 - 웹 작업자에게 전송
- 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에 알려주세요.