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ışmak için birçok yol 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 sunar. Bu API'ler, belirli üst düzey görevleri çözerken web programcılarının bir video akışının kareler ve kodlanmış video veya sesin ayrıştırılmamış parçaları gibi tek tek bileşenleriyle çalışmasına izin vermez. Geliştiriciler, bu temel bileşenlere düşük düzeyde erişmek için video ve ses codec'lerini tarayıcıya getirmek üzere WebAssembly'yi kullanıyor. Ancak modern tarayıcılar zaten çeşitli codec'lerle birlikte geldiğinden (genellikle donanım tarafından hızlandırılır), bunları WebAssembly olarak yeniden paketlemek insan ve bilgisayar kaynaklarının israfı gibi görünüyor.

WebCodecs API, programcılara tarayıcıda zaten bulunan medya bileşenlerini kullanma olanağı vererek 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, video düzenleyiciler, video konferans, video akışı gibi medya içeriklerinin işlenme şekli üzerinde tam kontrol gerektiren web uygulamaları için kullanışlıdır.

Video işleme iş akışı

Kareler, video işlemenin temelini oluşturur. Bu nedenle, WebCodecs'teki ç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 ise tam tersini yapar.

Ayrıca, VideoFrame CanvasImageSource olarak diğer Web API'leriyle de uyumlu çalışır ve CanvasImageSource kabul eden bir constructor'a sahiptir. Bu nedenle, drawImage() ve texImage2D() gibi işlevlerde kullanılabilir. Ayrıca, tuval, bit eşlem, video öğeleri ve diğer video karelerinden de oluşturulabilir.

WebCodecs API, WebCodecs'i medya akışı parçalarına bağlayan Insertable Streams API'deki sınıflarla birlikte iyi çalışır.

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

WebCodecs ve web işçileri

WebCodecs API, tasarım gereği tüm ağır işlemleri eşzamansız olarak ve ana ileti dizisi dışında gerçekleştirir. Ancak kare ve parça geri çağırmaları genellikle saniyede birden çok kez çağrılabildiğinden ana iş parçacığını karıştırabilir ve bu nedenle web sitesinin daha az yanıt vermesine neden olabilir. Bu nedenle, tek tek karelerin ve kodlanmış parçaların işlenmesini bir web worker'a taşımak tercih edilir.

Bu konuda yardımcı olmak için ReadableStream, bir medya parçasından gelen tüm kareleri otomatik olarak çalışana aktarmanın kolay bir yolunu sunar. Örneğin, MediaStreamTrackProcessor, web kamerasından gelen bir medya akışı parçası için ReadableStream elde etmek üzere kullanılabilir. Ardından akış, karelerin tek tek okunup VideoEncoder içine sıraya alındığı bir web worker'a aktarılır.

HTMLCanvasElement.transferControlToOffscreen ile oluşturma işlemi ana iş parçacığı dışında da yapılabilir. Ancak üst düzey araçların tümü kullanışsız çıkarsa VideoFrame aktarılabilir ve çalışanlar arasında taşınabilir.

WebCodecs'in kullanılma şekli

Kodlama

Canvas veya ImageBitmap'ten ağa ya da depolamaya giden yol
Canvas veya ImageBitmap cihazından 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 öğesinden kare çekmek için MediaStreamTrackProcessor aracını kullanma

    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 gösteriminden bir ç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 nesneleri olarak kodlanabilir.

Kodlamadan önce VideoEncoder öğesine iki JavaScript nesnesi verilmesi gerekir:

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

Yapılandırma tarayıcı tarafından desteklenmiyorsa configure() yöntemi NotSupportedError hatası verir. Yapılandırmanın desteklenip desteklenmediğini önceden kontrol etmek için yapılandırmayla birlikte VideoEncoder.isConfigSupported() statik yöntemini çağırmanız ve sözünün yerine getirilmesini beklemeniz ö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 geri döner. Bu sayede, aynı anda birden fazla karenin kodlama için sıraya alınmasına olanak tanınır. encodeQueueSize ise önceki kodlamaların tamamlanması için sırada bekleyen istek 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 ediyorsa hemen bir istisna oluşturularak ya da codec uygulamada karşılaşılan sorunlar için error() geri çağırma işlevi çağrılarak bildirilir. Kodlama başarılı bir şekilde tamamlanırsa output() geri çağırma işlevi, bağımsız değişken olarak yeni bir kodlanmış parça ile çağrılır. Burada dikkat edilmesi gereken bir diğer önemli nokta, close() çağrılarak çerçevelere artık ihtiyaç duyulmadığının bildirilmesi 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 çıkan 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 çoklar.

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() işlevini çağırabilir ve bu işlevin tamamlanmasını bekleyebilirsiniz.

await encoder.flush();

Kod çözme

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

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

Codec parametreleri, codec'ten codec'e farklılık gösterir. Örneğin, H.264 codec, Annex B biçiminde (encoderConfig.avc = { format: "annexb" }) kodlanmadığı sürece AVCC'nin ikili blob'unu gerektirebilir.

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. Parça oluşturmak için gerekenler:

  • 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):
    • key Parça, önceki parçalardan bağımsız olarak kod çözülebilirse
    • delta Parça yalnızca bir veya daha fazla önceki parça kod çözüldükten sonra kod çözülebilirse

Ayrıca, kodlayıcı tarafından yayınlanan tüm parçalar olduğu gibi kod çözücü için hazırdır. Hata raporlama ve kodlayıcının yöntemlerinin eşzamansız 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 de yeni çözülmüş bir karenin sayfada nasıl gösterilebileceğini açıklayacağız. Kod çözücü çıkışı geri aramasının (handleFrame()) hızlı bir şekilde döndüğünden emin olmanız daha iyi olur. Aşağıdaki örnekte, yalnızca oluşturulmaya hazır karelerin sırasına bir kare eklenir. Oluşturma işlemi ayrı olarak gerçekleşir ve iki adımdan oluşur:

  1. Çerçevenin gösterilmesi için doğru zaman bekleniyor.
  2. Çerçeveyi tuval üzerine çizme.

Bir çerçeveye artık ihtiyaç duyulmadığında, çöp toplayıcıya ulaşmadan önce temel belleği 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ştiriciler için İpuçları

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

WebCodecs&#39;te hata ayıklama için medya panelinin ekran görüntüsü
WebCodecs'i hata ayıklamak için Chrome Geliştirici Araçları'ndaki Medya paneli.

Demo

Demoda, tuvaldeki animasyon karelerinin nasıl:

  • MediaStreamTrackProcessor tarafından ReadableStream içine 25 fps hızında kaydedildi
  • bir web işçisine aktarılır.
  • H.264 video biçiminde kodlanmış olmalıdır.
  • tekrar video kareleri dizisine dönüştürülür.
  • 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ılabildiğ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'nin beklentilerinizi karşılamayan bir özelliği var mı? Ya da fikrinizi uygulamak için eksik yöntemler veya özellikler mi var? Güvenlik modeliyle ilgili sorunuz veya yorumunuz mu var? İlgili GitHub deposunda bir spesifikasyon sorunu bildirin veya düşüncelerinizi mevcut bir soruna ekleyin.

Uygulamayla ilgili sorun bildirme

Chrome'un uygulamasında bir hata mı buldunuz? Yoksa uygulama, spesifikasyondan farklı mı? new.crbug.com adresinden hata bildirin. Mümkün olduğunca fazla ayrıntı eklediğinizden, hatayı yeniden oluşturmak için basit talimatlar verdiğinizden ve Bileşenler kutusuna Blink>Media>WebCodecs girdiğinizden emin olun.

API'ye desteğinizi gösterme

WebCodecs API'yi kullanmayı planlıyor musunuz? Herkese açık desteğiniz, Chrome ekibinin özellikleri önceliklendirmesine yardımcı olur ve diğer tarayıcı satıcılarına bu özelliklerin desteklenmesinin 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 nerede, nasıl kullandığınızı bize bildirin.

Lokomotif resim, Denise Jans tarafından Unsplash'te.