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 kareler ve kodlanmamış video veya ses parçaları gibi bağımsız video akışı bileşenleriyle çalışmasına izin vermez. 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 bulunan medya bileşenlerini kullanma olanağı sunarak bu verimsizliği ortadan kaldırır. Özellikle:

  • Video ve ses kod çözücüleri
  • 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. Dolayısıyla, WebCodecs'te çoğu sınıf çerçeveleri tüketir veya üretir. Video kodlayıcılar, kareleri kodlanmış parçalara dönüştürür. Video kod çözücüler tam tersini yapar.

Ayrıca VideoFrame, CanvasImageSource türüne sahip ve CanvasImageSource kabul eden bir constructor içererek diğer Web API'leriyle iyi çalışır. Bu nedenle, drawImage() ve texImage2D() 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ığında yapmaz. Ancak çerçeve ve parça geri çağırmaları genellikle saniyede birden çok kez çağrılabildiğinden, ana iş parçacığını karmaşık hale getirebilir ve web sitesini daha az duyarlı hale getirebilir. 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 kullanışlı değilse VideoFrame'ün kendisi aktarılabilir ve çalışanlar arasında taşınabilir.

WebCodecs'in kullanımı

Kodlama

Bir Canvas veya ImageBitmap'ten ağ veya depolama alanına 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.

  • Tuval, resim bit eşlemi 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 öğesine iki JavaScript nesnesi verilmesi gerekir:

  • Kodlanmış parçaları ve hataları işlemek için iki işleve sahip ilk sözlük. 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 değerini bildirir. Yapılandırmanın desteklenip desteklenmediğini önceden kontrol etmek ve sözünü beklemek için yapılandırmayı içeren VideoEncoder.isConfigSupported() statik 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öntemiyle 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() yöntemini çağırabilir ve isteğin gönderilmesini bekleyebilirsiniz.

await encoder.flush();

Kod çözme

Ağdan veya depolama alanından bir Canvas&#39;a ya da ImageBitmap&#39;e giden yol.
Ağdan veya depolama alanından Canvas ya da ImageBitmap öğesine giden yol.

VideoDecoder kurulumu VideoEncoder için yapılanlara benzer: Kod çözücü oluşturulduğunda iki işlev iletilir ve codec parametreleri configure() öğesine verilir.

Codec parametre 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ı)
  • aşağıdakilerden biri olabilir:
    • Parçanın kodu önceki parçalardan bağımsız olarak çözülebiliyorsa key
    • 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 sıra kodu çözülmüş yeni bir karenin sayfada nasıl gösterilebileceğini göstermeye geldi. Kod çözücü çıkışı geri çağırmasının (handleFrame()) hızlı bir şekilde dönmesini sağlamak 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çeve tuvale çiziliyor.

Bir çerçeveye artık ihtiyaç duyulmadığında, çöp toplayıcı ona ulaşmadan temel belleği serbest bırakmak için close() çağrısı yapı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, tuvaldeki animasyon karelerinin nasıl olduğu gösterilmektedir:

  • MediaStreamTrackProcessor tarafından 25 fps'de ReadableStream içine ç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 mu var? İlgili GitHub deposunda özellik sorunu oluşturun veya mevcut bir soruna düşüncelerinizi ekleyin.

Uygulamayla ilgili sorunları bildirme

Chrome&#39;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.

Denise Jans'dan Unsplash'teki hero resim.