WebCodec ile video işleme

Video akışı bileşenlerini değiştirme

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

Modern web teknolojileri, videolarla çalışmanın birçok yolunu sunar. Media Stream API, Media Recording API, Media Source API ve WebRTC API, video akışlarını kaydetmek, aktarmak ve oynatmak için zengin bir araç seti oluşturur. Bu API'ler, belirli üst düzey görevleri çözerken web programcılarının video akışının tek tek bileşenleriyle (ör. kareler ve kodlanmış video veya sesin birleştirilmemiş parçaları) çalışmasını engellemez. Geliştiriciler, bu temel bileşenlere düşük düzeyde erişim elde etmek için video ve ses codec'lerini tarayıcıya getirmek üzere WebAssembly'i kullanıyordu. Ancak modern tarayıcıların zaten çeşitli codec'lerle (genellikle donanım tarafından hızlandırılır) birlikte gönderildiği göz önüne alındığında, bunları WebAssembly olarak yeniden paketlemek insan ve bilgisayar kaynaklarının israf edilmesi gibi görünüyor.

WebCodecs API, programcılara tarayıcıda zaten mevcut olan medya bileşenlerini kullanmanın bir yolunu sunarak bu verimsizliği ortadan kaldırır. Özellikle:

  • Video ve ses kod çözücüler
  • Video ve ses kodlayıcılar
  • İşlenmemiş video kareleri
  • Resim kod çözücüler

WebCodecs API, medya içeriğinin işlenme şekli üzerinde tam kontrol gerektiren web uygulamaları (ör. video düzenleyiciler, video konferansı, video aktarımı vb.) için kullanışlıdır.

Video işleme iş akışı

Çerçeveler, video işleme sürecinin merkezinde yer alır. Bu nedenle, WebCodecs'teki çoğu sınıf kare tüketir veya üretir. Video kodlayıcılar, kareleri kodlanmış parçalara dönüştürür. Video kod çözücüler ise tam tersini yapar.

Ayrıca VideoFrame, CanvasImageSource türüne sahip ve CanvasImageSource kabul eden bir yapıcı içererek diğer Web API'leriyle iyi çalışır. Bu nedenle drawImage() vetexImage2D() gibi işlevlerde kullanılabilir. Ayrıca kanvaslar, bitmap'ler, video öğeleri ve diğer video çerçevelerinden de oluşturulabilir.

WebCodecs API, WebCodecs'i medya akış kanallarına bağlayan Insertable Streams API sınıflarıyla birlikte iyi çalışır.

  • MediaStreamTrackProcessor, medya parçalarını ayrı karelere ayırır.
  • MediaStreamTrackGenerator, bir kare akışından medya parçası oluşturur.

WebCodecs ve web işçileri

WebCodecs API, tüm ağır işleri tasarım gereği asenkron olarak ve ana iş parçacığından bağımsız olarak yapar. Ancak çerçeve ve parça geri çağırma işlevleri genellikle saniyede birden çok kez çağrılabildiğinden ana iş parçacığı tıkanabilir ve bu da web sitesinin daha az duyarlı olmasına neden olabilir. Bu nedenle, tek tek karelerin ve kodlanmış parçaların işlenmesi bir web çalışanına taşınmalıdır.

Bu konuda yardımcı olmak için ReadableStream, bir medya kanalından gelen tüm kareleri işleyiciye otomatik olarak aktarmanın uygun bir yolunu sağlar. Örneğin, web kamerasından gelen bir medya akışı parçası için MediaStreamTrackProcessor, ReadableStream elde etmek amacıyla kullanılabilir. Ardından akış, karelerin tek tek okunup VideoEncoder olarak sıraya alındığı bir web işleyicisine aktarılır.

HTMLCanvasElement.transferControlToOffscreen ile oluşturma işlemi bile ana iş parçacığının dışında yapılabilir. Ancak tüm üst düzey araçlar uygun değilse VideoFrame'ün kendisi aktarılabilir ve çalışanlar arasında taşınabilir.

WebCodecs'in kullanımı

Kodlama

Bir kanvastan veya ImageBitmap'ten ağa ya da depolamaya giden yol
Canvas veya ImageBitmap'ten ağa ya da depolama alanına giden yol

Her şey bir VideoFrame ile başlar. Video kareleri oluşturmanın üç yolu vardır.

  • Kanvas, resim bitmap'i veya video öğesi gibi bir resim kaynağından.

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • MediaStreamTrack kaynağından kare almak için MediaStreamTrackProcessor'ü kullanın.

    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 içinde ikili piksel temsilinden çerçeve oluşturma

    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);
    

Kaynakları ne olursa olsun kareler, VideoEncoder ile EncodedVideoChunk nesnelerine kodlanabilir.

Kodlamadan önce VideoEncoder'e iki JavaScript nesnesi verilmelidir:

  • Kodlanmış parçaları ve hataları işlemek için sözlüğü iki işlevle başlatın. Bu işlevler geliştirici tarafından tanımlanır ve VideoEncoder yapıcısına iletildikten sonra değiştirilemez.
  • Çıkış video akışıyla ilgili parametreleri içeren kodlayıcı yapılandırma nesnesi. Bu parametreleri daha sonra configure()'yi çağırarak değiştirebilirsiniz.

Yapılandırma tarayıcı tarafından desteklenmiyorsa configure() yöntemi NotSupportedError hatası atar. Yapılandırmanın desteklenip desteklenmediğini önceden kontrol etmek ve promise'ini beklemek için yapılandırmayı içeren statik VideoEncoder.isConfigSupported() yöntemini çağırmanız önerilir.

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.
}

Kodlayıcı ayarlandıktan sonra encode() yöntemi aracılığıyla kareleri kabul etmeye hazırdır. Hem configure() hem de encode(), gerçek çalışmanın tamamlanmasını beklemeden hemen döndürülür. Bu sayede, aynı anda birden fazla karenin kodlama için sıraya alınmasına izin verilir. encodeQueueSize ise önceki kodlamaların tamamlanması için sırada bekleyen isteklerin sayısını gösterir. Hatalar, bağımsız değişkenler veya yöntem çağrılarının sırası API sözleşmesini ihlal ettiğinde hemen bir istisna atılarak ya da codec uygulamasında karşılaşılan sorunlar için error() geri çağırma işlevi çağrılarak bildirilir. Kodlama işlemi başarıyla tamamlanırsa output() geri çağırma işlevi, bağımsız değişken olarak yeni kodlanmış bir parçayla çağrılır. Buradaki bir diğer önemli ayrıntı da, artık ihtiyaç duyulmayan çerçevelerin close() çağrılarak kaldırılması gerektiğidir.

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();
  }
}

Son olarak, kodlayıcıdan çıktıkları sırada kodlanmış video parçalarını işleyen bir işlev yazarak kodlama kodunu tamamlamanın zamanı geldi. Bu işlev genellikle veri parçalarını ağ üzerinden gönderir veya depolama için bir medya kapsayıcısında birleştirir.

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,
  });
}

Bir noktada bekleyen tüm kodlama isteklerinin tamamlandığından emin olmanız gerekirse flush()'ü arayabilir ve sözünü bekleyebilirsiniz.

await encoder.flush();

Kod çözme

Ağdan veya depolama alanından bir tuvale veya ImageBitmap&#39;e giden yol.
Ağ veya depolama alanından Canvas veya ImageBitmap'e giden yol.

VideoDecoder oluşturma işlemi, VideoEncoder için yapılana benzer: Kod çözücü oluşturulurken iki işlev iletilir ve codec parametreleri configure()'ye verilir.

Codec parametreleri grubu codec'e göre değişir. Örneğin, H.264 codec'i, ek B biçiminde (encoderConfig.avc = { format: "annexb" }) kodlanmadığı sürece AVCC ikili blob'una ihtiyaç duyabilir.

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.
}

Kod çözücü başlatıldıktan sonra EncodedVideoChunk nesneleriyle beslemeye başlayabilirsiniz. Bir parça oluşturmak için şunlara ihtiyacınız vardır:

  • Kodlanmış video verilerinin BufferSource
  • Parçanın başlangıç zaman damgası (mikrosaniye cinsinden) (parçadaki ilk kodlanmış karenin medya zamanı)
  • Parçanın türü. Şunlardan biri olabilir:
    • key (parçanın önceki parçalardan bağımsız olarak kodu çözülebiliyorsa)
    • delta (parçanın kodu yalnızca bir veya daha fazla önceki parçanın kodu çözüldükten sonra çözülebiliyorsa)

Ayrıca kodlayıcı tarafından yayınlanan tüm parçalar olduğu gibi kod çözücüye hazırdır. Hata raporlama ve kodlayıcı yöntemlerinin asenkron yapısı hakkında yukarıda söylenenlerin tümü kod çözücüler için de geçerlidir.

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();

Şimdi, yeni kod çözülmüş bir çerçevenin sayfada nasıl gösterilebileceğini göstermenin zamanı geldi. Kod çözücü çıkışı geri çağırma işlevinin (handleFrame()) hızlı bir şekilde döndürüldüğünden emin olmak daha iyidir. Aşağıdaki örnekte, yalnızca oluşturmaya hazır karelerin sırasına bir kare eklenmektedir. Oluşturma işlemi ayrı olarak gerçekleşir ve iki adımdan oluşur:

  1. Çerçeveyi göstermek için doğru zamanı bekleme.
  2. Çerçeveyi tuvale çizme.

Bir çerçeveye artık ihtiyaç duyulmadığında, temel belleği çöp toplayıcıdan önce serbest bırakmak için close() işlevini çağırın. Bu, web uygulaması tarafından kullanılan ortalama bellek miktarını azaltır.

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);
}

Geliştirici İpuçları

Medya günlüklerini görüntülemek ve WebCodec'lerde hata ayıklama yapmak için Chrome Geliştirici Araçları'ndaki Medya Paneli'ni kullanın.

WebCodecs&#39;de hata ayıklama için Medya Paneli&#39;nin ekran görüntüsü
WebCodecs'de hata ayıklama için Chrome Geliştirici Araçları'ndaki Medya Paneli.

Demo

Aşağıdaki demoda, kanvastaki animasyon karelerinin nasıl olduğu gösterilmektedir:

  • MediaStreamTrackProcessor tarafından 25 fps'de ReadableStream cihazına çekilen
  • bir web işleyicisine aktarılır.
  • H.264 video biçiminde kodlanmış olmalıdır.
  • yeniden kodlanarak bir video karesi dizisi haline getirilir.
  • ve transferControlToOffscreen() kullanılarak ikinci tuvalde oluşturulur.

Diğer demolar

Diğer demolarımıza da göz atın:

WebCodecs API'yi kullanma

Özellik algılama

WebCodecs desteğini kontrol etmek için:

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

WebCodecs API'nin yalnızca güvenli bağlamlarda kullanılabileceğini unutmayın. Bu nedenle, self.isSecureContext yanlışsa algılama başarısız olur.

Geri bildirim

Chrome ekibi, WebCodecs API ile ilgili deneyimlerinizi öğrenmek istiyor.

API tasarımı hakkında bilgi verin

API ile ilgili olarak beklediğiniz gibi çalışmayan bir şey var mı? Yoksa fikrinizi uygulamak için ihtiyaç duyduğunuz yöntemler veya özellikler eksik mi? Güvenlik modeliyle ilgili bir sorunuz veya yorumunuz var mı? İlgili GitHub deposunda özellik sorunu oluşturun veya mevcut bir soruna düşüncelerinizi ekleyin.

Uygulamayla ilgili sorunları bildirme

Chrome'un uygulamasında bir hata mı buldunuz? Yoksa uygulama, spesifikasyondan farklı mı? new.crbug.com adresinden hata kaydı oluşturun. Mümkün olduğunca fazla ayrıntı ekleyin, sorunu yeniden oluşturmayla ilgili basit talimatlar verin ve Bileşenler kutusuna Blink>Media>WebCodecs yazın. Glitch, hızlı ve kolay yeniden oluşturma işlemlerini paylaşmak için idealdir.

API'yi destekleme

WebCodecs API'yi kullanmayı planlıyor musunuz? Herkese açık desteğiniz, Chrome ekibinin özelliklere öncelik vermesine yardımcı olur ve diğer tarayıcı tedarikçi firmalarına bu özellikleri desteklemenin ne kadar önemli olduğunu gösterir.

media-dev@chromium.org adresine e-posta gönderin veya #WebCodecs hashtag'ini kullanarak @ChromiumDev adresine tweet gönderin ve bu özelliği nerede ve nasıl kullandığınızı bize bildirin.

Unsplash'taki Denise Jans tarafından oluşturulan lokomotif resim.