WebCodec ile video işleme

Video akışı bileşenleri değiştiriliyor.

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

Modern web teknolojileri, videoyla çalışmak için geniş kapsamlı yöntemler sunar. Media Stream API, Media Record API, Media Source API ve WebRTC API video akışlarını kaydetmek, aktarmak ve oynatmak için zengin bir araç seti sağlar. Bu API'ler bazı üst düzey görevleri çözerken web programlarının bir video akışının kareler ve kodlanmış video veya ses parçalarından oluşan muğlak olmayan bileşenleri gibi bağımsız bileşenleriyle çalışmasına izin vermez. Geliştiriciler bu temel bileşenlere alt düzey erişim elde etmek için video ve ses codec'lerini tarayıcıya getirmek için WebAssembly hizmetini kullanmaktadır. Ancak modern tarayıcıların halihazırda çeşitli codec'lerle (genellikle donanım tarafından hızlandırılarak) birlikte gönderilmesi, WebAssembly'nin yeniden paketlenmesinin insan ve bilgisayar kaynakları israfı gibi görünmesidir.

WebCodecs API, programcılara zaten tarayıcıda bulunan medya bileşenlerini kullanma olanağı sunarak bu verimsizliği ortadan kaldırır. Özellikle:

  • Video ve ses kod çözücüler
  • Video ve ses kodlayıcıları
  • Ham video kareleri
  • Görüntü kod çözücüleri

WebCodecs API; video düzenleyiciler, video konferans, video akışı vb. gibi medya içeriğinin işlenme şekli üzerinde tam kontrol gerektiren web uygulamaları için yararlıdır.

Video işleme iş akışı

Kareler, video işlemenin en önemli parçasıdır. Bu nedenle, WebCodecs'de çoğu sınıf kare tüketir veya oluşturur. Video kodlayıcılar, kareleri kodlanmış parçalara dönüştürür. Video kod çözücüler ise bunun tersini yapar.

Ayrıca VideoFrame, CanvasImageSource olması ve CanvasImageSource kabul eden bir kurucu olması nedeniyle diğer Web API'leriyle uyumlu şekilde çalışır. Bu nedenle drawImage() ve texImage2D() gibi işlevlerde kullanılabilir. Ayrıca tuvallerden, bit eşlemlerden, video öğelerinden ve diğer video karelerinden de oluşturulabilir.

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

  • MediaStreamTrackProcessor, medya kanallarını ayrı ayrı karelere ayırıyor.
  • MediaStreamTrackGenerator, bir kare akışından medya kanalı oluşturur.

WebCodec'ler ve web çalışanları

WebCodecs API, tasarımı gereği tüm ağır işleri eşzamansız olarak ve ana iş parçacığı dışında gerçekleştirir. Ancak çerçeve ve parça geri çağırma işlevleri genellikle saniyede birden çok kez çağrılabildiğinden ana iş parçacığını karıştırabilir ve bu nedenle web sitesinin daha az duyarlı olmasına neden olabilir. Bu nedenle, bağımsız çerçevelerin ve kodlanmış parçaların işlenmesini bir web çalışanına taşımak tercih edilir.

ReadableStream, bir medya kanalından gelen tüm kareleri çalışana otomatik olarak aktarmak için rahat bir yol sunuyor. Örneğin, web kamerasından gelen bir medya akışı kanalı için ReadableStream elde etmek üzere MediaStreamTrackProcessor kullanılabilir. Ardından akış bir web çalışanına aktarılır. Burada çerçeveler tek tek okunur ve VideoEncoder için sıraya alınır.

HTMLCanvasElement.transferControlToOffscreen ile oluşturma işlemi ana ileti dizisi dışında da yapılabilir. Ancak üst düzey araçların hiçbiri sorun yaratırsa VideoFrame ürününün kendisi aktarılabilir ve çalışanlar arasında taşınabilir.

WebCodecs'in işleyiş şekli

Kodlama

Canvas veya ImageBitmap'ten ağa veya depolama alanına giden yol
Canvas veya ImageBitmap noktasından ağa veya depolama alanına giden yol

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

  • Kanvas, 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 });
    
  • Bir MediaStreamTrack öğesinden kare çekmek için MediaStreamTrackProcessor öğesini 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;
    }
    
  • BufferSourceda ikili piksel gösteriminden bir çerçeve oluşturun.

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

Nereden geldiğine bakılmaksızın kareler bir 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şlevli sözlüğü başlatın. Bu işlevler geliştirici tarafından tanımlanır ve VideoEncoder oluşturucuya geçirildikten sonra değiştirilemez.
  • Çıkış video akışı için parametreleri içeren kodlayıcı yapılandırma nesnesi. Bu parametreleri daha sonra configure() yöntemini çağırarak değiştirebilirsiniz.

Tarayıcı, yapılandırmayı desteklemiyorsa configure() yöntemi NotSupportedError hatasını verir. Yapılandırmanın desteklenip desteklenmediğini önceden kontrol etmek için yapılandırma ile birlikte statik yöntemi VideoEncoder.isConfigSupported() çağırmanız ve sözünü 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 kare kabul etmeye hazır hâle gelir. Hem configure() hem de encode(), asıl çalışmanın tamamlanmasını beklemeden hemen geri döner. Bu işlev, birkaç karenin kodlama için aynı anda sıraya girmesine olanak tanır. encodeQueueSize ise önceki kodlamaların tamamlanması için sırada bekleyen istek sayısını gösterir. Bağımsız değişkenler veya yöntem çağrılarının sırası API sözleşmesini ihlal ediyorsa ya da codec uygulamasında karşılaşılan sorunlar için error() geri çağırması çağrılıp hatalar hemen bir istisna gönderilerek bildirilir. Kodlama başarıyla tamamlanırsa output() geri çağırma, bağımsız değişken olarak yeni bir kodlanmış yığınla çağrılır. Buradaki bir diğer önemli ayrıntı, çerçevelerin artık ihtiyaç kalmadığında close() çağrısı yapılarak 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 bitirme zamanı geldi. Genellikle bu işlev, veri parçalarını ağ üzerinden gönderir veya bunları depolama amacıyla bir medya kapsayıcısında mux oluşturur.

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

Herhangi bir noktada bekleyen tüm kodlama isteklerinin tamamlandığından emin olmanız gerekirse flush() yöntemini çağırabilir ve sözünü bekleyebilirsiniz.

await encoder.flush();

Kod çözülüyor

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

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

Codec parametreleri grubu codec'ten codec'e farklılık gösterir. Örneğin H.264 codec'i, Ek B biçiminde (encoderConfig.avc = { format: "annexb" }) olarak kodlanmadığı sürece AVCC'nin 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. Parça oluşturmak için aşağıdakilere ihtiyacınız vardır:

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

Ayrıca kodlayıcı tarafından yayınlanan tüm parçalar, kod çözücü için olduğu gibi hazırdır. Yukarıda hata raporlama ve kodlayıcının yöntemlerinin eşzamansız doğasıyla ilgili söylenenlerin hepsi, kod çözücüler için de eşit derecede 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 şifresi yeni çözülmüş bir karenin sayfada nasıl gösterilebileceğini gösterme zamanı geldi. Kod çözücü çıkışı geri çağırma işlevinin (handleFrame()) hızlı bir şekilde geri döndüğünden emin olmak daha iyi olur. Aşağıdaki örnekte, yalnızca oluşturulmaya hazır olan kareler sırasına bir kare eklenir. Oluşturma ayrı olarak gerçekleşir ve iki adımdan oluşur:

  1. Karenin gösterilmesi için doğru zaman bekleniyor.
  2. Çerçeve kanvasa çiziliyor.

Bir çerçeveye artık ihtiyaç duyulmadığında, çöp toplayıcı kendisine ulaşmadan önce temel belleği serbest bırakmak için close() yöntemini çağırın. Bu işlem, web uygulamasının kullandığı 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ştiricilere Yönelik İpuçları

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

WebCodecs hatalarını ayıklamaya ilişkin Media Panel ekran görüntüsü
WebCodecs hatalarını ayıklamak için Chrome Geliştirici Araçları'ndaki Medya Paneli.

Demo

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

  • 25 fps'de MediaStreamTrackProcessor tarafından ReadableStream ile yakalandı
  • bir web çalışanına aktarıldı
  • H.264 video biçimine kodlanmış
  • kod tekrar bir video karesi dizisine dönüştürülür
  • ve transferControlToOffscreen() ile ikinci kanvasta oluşturuldu.

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, bu nedenle self.isSecureContext değeri yanlış olduğunda algılamanın başarısız olacağını unutmayın.

Geri bildirim

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

Bize API tasarımı hakkında bilgi verin

API'de beklediğiniz gibi çalışmayan bir durum mu var? Yoksa fikrinizi uygulamak için gereken eksik yöntemler veya özellikler mi var? Güvenlik modeliyle ilgili bir sorunuz veya yorumunuz mu var? İlgili GitHub deposuna teknik özellik sorunu kaydedin veya mevcut bir soruna düşüncelerinizi ekleyin.

Uygulamayla ilgili bir sorunu bildirin

Chrome'un uygulamasında bir hata buldunuz mu? Yoksa uygulama, spesifikasyondan farklı mı? new.crbug.com adresinden hata bildiriminde bulunun. Mümkün olduğunca çok ayrıntı eklediğinizden ve basit yeniden oluşturma talimatlarını eklediğinizden emin olun ve Bileşenler kutusuna Blink>Media>WebCodecs yazın. Glitch, hızlı ve kolay yeniden oluşturmalar paylaşmak için idealdir.

API'ye desteği gösterin

WebCodecs API'yi kullanmayı planlıyor musunuz? Herkese açık desteğiniz, Chrome ekibinin özellikleri öncelik sırasına koymasına yardımcı olur ve diğer tarayıcı satıcılarına onları desteklemenin ne kadar kritik olduğunu gösterir.

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

Denise Jans'ın Unsplash'teki hero resmi.