Medya Kaynağı Uzantıları

François Beaufort
François Beaufort
Joe Medley
Joe Medley

Media Source Extensions (MSE), ses veya video segmentlerinden oynatma için akış oluşturmanıza olanak tanıyan bir JavaScript API'sidir. Bu makalede ele alınmasa da sitenize aşağıdaki gibi işlemler yapan videolar yerleştirmek istiyorsanız MSE'yi anlamanız gerekir:

  • Uyarlanabilir akış (cihaz özelliklerine ve ağ koşullarına uyum sağlamanın başka bir yolu)
  • Reklam ekleme gibi uyarlanabilir birleştirme
  • Zaman kaydırma
  • Performans ve indirme boyutunu kontrol etme
Temel MSE veri akışı
Şekil 1: Temel MSE veri akışı

MSE'yi bir zincir olarak düşünebilirsiniz. Şekilde gösterildiği gibi, indirilen dosya ile medya öğeleri arasında birkaç katman vardır.

  • Medyayı oynatmak için bir <audio> veya <video> öğesi.
  • Medya öğesini beslemek için SourceBuffer içeren bir MediaSource örneği.
  • Bir Response nesnesinde medya verilerini almak için fetch() veya XHR çağrısı.
  • MediaSource.SourceBuffer beslemek için Response.arrayBuffer() çağrısı.

Pratikte zincir şu şekilde görünür:

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

Şimdiye kadarki açıklamaları anlayabildiyseniz okumayı bırakabilirsiniz. Daha ayrıntılı bir açıklama istiyorsanız lütfen okumaya devam edin. Temel bir MSE örneği oluşturarak bu zinciri adım adım açıklayacağım. Derleme adımlarının her biri, bir önceki adıma kod ekler.

Anlaşılırlık hakkında bir not

Bu makalede, web sayfasında medya oynatma hakkında bilmeniz gereken her şey açıklanacak mı? Hayır, yalnızca başka yerlerde bulabileceğiniz daha karmaşık kodları anlamanıza yardımcı olmak için tasarlanmıştır. Anlaşılırlık için bu dokümanda birçok şey basitleştirilmiş ve hariç tutulmuştur. Google'ın Shaka Player gibi bir kitaplık kullanmanızı da önerdiğimiz için bu durumdan sorunsuzca kurtulabileceğimizi düşünüyoruz. Nerede kasıtlı olarak basitleştirdiğimi belirteceğim.

Bu lisansın kapsamında olmayanlar

Belirli bir sırayla belirtmeyeceğim birkaç konuyu aşağıda bulabilirsiniz.

  • Oynatma kontrolleri. HTML5 <audio> ve <video> öğeleri sayesinde bunları ücretsiz olarak ediniyoruz.
  • Hata işleme.

Üretim ortamlarında kullanım için

MSE ile ilgili API'lerin üretimde kullanılmasıyla ilgili olarak önerdiğimiz bazı noktalar şunlardır:

  • Bu API'lere çağrıda bulunmadan önce tüm hata etkinliklerini veya API istisnalarını ele alın ve HTMLMediaElement.readyState ile MediaSource.readyState değerlerini kontrol edin. Bu değerler, ilişkili etkinlikler yayınlanmadan önce değişebilir.
  • SourceBuffer'ın mode, timestampOffset, appendWindowStart, appendWindowEnd özelliklerini güncellemeden veya SourceBuffer'da appendBuffer() ya da remove()'ı çağırmadan önce SourceBuffer.updating boole değerini kontrol ederek önceki appendBuffer() ve remove() çağrılarının devam etmediğinden emin olun.
  • MediaSource'unuza eklenen tüm SourceBuffer örnekleri için MediaSource.endOfStream() çağırmadan veya MediaSource.duration'i güncellemeden önce updating değerlerinin hiçbirinin doğru olmadığından emin olun.
  • MediaSource.readyState değeri ended ise appendBuffer() ve remove() gibi çağrılar veya SourceBuffer.mode ya da SourceBuffer.timestampOffset ayarlaması bu değerin open değerine geçiş yapmasına neden olur. Bu nedenle, birden fazla sourceopen etkinliğini yönetmeye hazır olmalısınız.
  • HTMLMediaElement error etkinlikleri işlenirken, özellikle test ortamlarında yeniden oluşturulması zor olan hatalar için MediaError.message içeriği, hatanın temel nedenini belirlemek amacıyla yararlı olabilir.

Bir MediaSource örneğini medya öğesine ekleme

Bugünlerde web geliştirmede yapılan pek çok şeyde olduğu gibi, önce özellik algılama ile başlıyorsunuz. Ardından bir medya öğesi (<audio> veya <video> öğesi) alın. Son olarak MediaSource örneği oluşturun. Bu değer bir URL'ye dönüştürülür ve medya öğesinin kaynak özelliğine iletilir.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  // Is the MediaSource instance ready?
} else {
  console.log('The Media Source Extensions API is not supported.');
}
Blob olarak bir kaynak özellik
Şekil 1: Blob olarak kaynak özelliği

Bir MediaSource nesnesinin src özelliğine iletilebileceği biraz tuhaf görünebilir. Bunlar genellikle dizedir ancak blob da olabilirler. Yerleşik medyası olan bir sayfayı inceler ve medya öğesini incelerseniz ne demek istediğimi anlarsınız.

MediaSource örneği hazır mı?

URL.createObjectURL() eşzamanlı olsa da eki eşzamansız olarak işler. Bu durum, MediaSource örneğiyle herhangi bir işlem yapmadan önce kısa bir gecikmeye neden olur. Neyse ki bunu test etmenin yolları var. En basit yöntem, readyState adlı bir MediaSource mülkü kullanmaktır. readyState mülkü, bir MediaSource örneği ile medya öğesi arasındaki ilişkiyi tanımlar. Aşağıdaki değerlerden biri olabilir:

  • closed: MediaSource örneği bir medya öğesine eklenmemiş.
  • open: MediaSource örneği bir medya öğesine ekli ve veri almaya hazır veya veri alıyor.
  • ended: MediaSource örneği bir medya öğesine eklenmiştir ve tüm verileri bu öğeye iletilmiştir.

Bu seçenekleri doğrudan sorgulamak performansı olumsuz yönde etkileyebilir. Neyse ki MediaSource, readyState değiştiğinde (özellikle sourceopen, sourceclosed, sourceended) de etkinlik tetikler. Oluşturduğum örnekte, videoyu ne zaman getirip arabelleğe alacağımı belirtmek için sourceopen etkinliğini kullanacağım.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  <strong>mediaSource.addEventListener('sourceopen', sourceOpen);</strong>
} else {
  console.log("The Media Source Extensions API is not supported.")
}

<strong>function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  // Create a SourceBuffer and get the media file.
}</strong>

revokeObjectURL() işlevini de çağırdığımı fark edin. Bunun erken olduğunun farkındayım ancak medya öğesinin src özelliği bir MediaSource örneğine bağlandıktan sonra bunu istediğim zaman yapabilirim. Bu yöntem çağrıldığında hiçbir nesne kaldırılmaz. Platformun uygun bir zamanda çöp toplama işlemini gerçekleştirmesine izin verir. Bu nedenle, hemen çağırıyorum.

SourceBuffer Oluşturma

Şimdi sıra, medya kaynakları ile medya öğeleri arasındaki verileri kapatma işini yapan nesne olan SourceBuffer'ı oluşturmaya geldi. SourceBuffer, yüklediğiniz medya dosyasının türüne özel olmalıdır.

Uygulamada bunu, addSourceBuffer() işlevini uygun değerle çağırarak yapabilirsiniz. Aşağıdaki örnekte mime türü dizesinin bir mime türü ve iki codec içerdiğine dikkat edin. Bu, bir video dosyası için mime dizesidir ancak dosyanın video ve ses bölümleri için ayrı codec'ler kullanır.

MSE spesifikasyonunun 1. sürümü, kullanıcı aracılarının hem mime türü hem de codec gerektirip gerektirmeyeceği konusunda farklı olmasına olanak tanır. Bazı kullanıcı aracıları, mime türüne izin verir ancak codec'e izin vermez. Bazı kullanıcı aracıları (ör. Chrome), kendi codec'lerini tanımlamayan mime türleri için codec gerektirir. Tüm bunları ayırt etmek yerine her ikisini de eklemek daha iyidir.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  <strong>
    var mime = 'video/webm; codecs="opus, vp09.00.10.08"'; // e.target refers to
    the mediaSource instance. // Store it in a variable so it can be used in a
    closure. var mediaSource = e.target; var sourceBuffer =
    mediaSource.addSourceBuffer(mime); // Fetch and process the video.
  </strong>;
}

Medya dosyasını alma

MSE örnekleri için internette arama yaparsanız XHR kullanarak medya dosyalarını alan birçok olduğunu görürsünüz. Daha da ileri gitmek için Fetch API'yi ve döndürdüğü Promise'i kullanacağım. Bunu Safari'de yapmaya çalışıyorsanız fetch() polyfill olmadan çalışmaz.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  <strong>
    fetch(videoUrl) .then(function(response){' '}
    {
      // Process the response object.
    }
    );
  </strong>;
}

Üretim kalitesindeki bir oynatıcı, aynı dosyanın farklı tarayıcıları desteklemek için birden fazla sürümde olmasını sağlayabilir. Ses ve video için ayrı dosyalar kullanarak sesin dil ayarlarına göre seçilmesine izin verebilir.

Gerçek dünyadaki kodda, farklı cihaz özelliklerine ve ağ koşullarına uyum sağlayabilmesi için medya dosyalarının farklı çözünürlüklerde birden fazla kopyası bulunur. Bu tür bir uygulama, aralık istekleri veya segmentler kullanarak videoları parçalar halinde yükleyip oynatabilir. Bu, medya oynatılırken ağ koşullarına uyum sağlamaya olanak tanır. DASH veya HLS terimlerini duymuş olabilirsiniz. Bu iki yöntem, bu işlemi gerçekleştirmenin yollarından biridir. Bu girişte bu konunun kapsamlı olarak açıklanması girişin kapsamı dışındadır.

Yanıt nesnesini işleme

Kod neredeyse tamamlanmış görünüyor ancak medya oynatılmıyor. Medya verilerini Response nesnesinden SourceBuffer öğesine almamız gerekiyor.

Yanıt nesnesinden MediaSource örneğine veri aktarmanın tipik yolu, yanıt nesnesinden bir ArrayBuffer almak ve bunu SourceBuffer'ye aktarmaktır. response.arrayBuffer() öğesini çağırarak başlayın. Bu çağrı, tampon için bir söz döndürür. Kodumda bu promise'i, SourceBuffer'a eklediğim ikinci bir then() yan tümcesine ilettim.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      <strong>return response.arrayBuffer();</strong>
    })
    <strong>.then(function(arrayBuffer) {
      sourceBuffer.appendBuffer(arrayBuffer);
    });</strong>
}

endOfStream() işlevini çağırın.

Tüm ArrayBuffers eklendikten ve başka medya verisi beklenmedikten sonra MediaSource.endOfStream() işlevini çağırın. Bu işlemle MediaSource.readyState, ended olarak değişir ve sourceended etkinliği tetiklenir.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {
      <strong>sourceBuffer.addEventListener('updateend', function(e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });</strong>
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

Nihai sürüm

Kod örneğinin tamamını aşağıda bulabilirsiniz. Medya Kaynağı Uzantıları hakkında bilgi edindiğinizi umuyoruz.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

Geri bildirim