Ses İçin Medya Kaynağı Uzantıları

Dale Curtis
Dale Curtis

Giriş

Media Source Extensions (MSE), HTML5 <audio> ve <video> öğeleri için genişletilmiş arabelleğe alma ve oynatma kontrolü sağlar. Başlangıçta HTTP üzerinden Dinamik Adaptif Akış (DASH) tabanlı video oynatıcıları kolaylaştırmak için geliştirilmiş olsalar da aşağıda, ses için (özellikle aralıksız oynatma için) nasıl kullanılabileceklerini göreceğiz.

Şarkıların parçalar arasında sorunsuz bir şekilde aktığı bir müzik albümü dinlemiş olabilirsiniz. Hatta şu anda bile böyle bir albüm dinliyor olabilirsiniz. Sanatçılar, bu aralıksız oynatma deneyimlerini hem sanatsal bir seçim hem de sesin tek bir kesintisiz akış olarak yazıldığı plakların ve CD'lerin bir sonucu olarak oluşturur. Maalesef MP3 ve AAC gibi modern ses codec'lerinin çalışma şekli nedeniyle bu kesintisiz işitsel deneyim günümüzde genellikle kaybolmaktadır.

Nedenini aşağıda ayrıntılı olarak açıklayacağız ancak şimdilik bir gösterimle başlayalım. Aşağıda, mükemmel bir film olan Sintel'in ilk otuz saniyesi beş ayrı MP3 dosyası hâlinde ve MSE kullanılarak yeniden birleştirilmiş olarak gösterilmektedir. Kırmızı çizgiler, her MP3'ün oluşturulması (kodlanması) sırasında oluşan boşlukları gösterir. Bu noktalarda aksaklıklar duyarsınız.

Demo

İğrenç! Bu deneyimin daha iyi olabileceğini biliyoruz. Biraz daha fazla çalışarak, yukarıdaki demoda kullanılan MP3 dosyalarının aynısını kullanarak bu can sıkıcı boşlukları kaldırmak için MSE'yi kullanabiliriz. Sonraki demoda, dosyaların birleştirildiği ve boşlukların kaldırıldığı yerler yeşil çizgilerle gösterilmektedir. Chrome 38 ve sonraki sürümlerde bu işlem sorunsuz bir şekilde oynatılır.

Demo

Boşluksuz içerik oluşturmanın çeşitli yolları vardır. Bu demoda, normal bir kullanıcının sahip olabileceği dosya türlerine odaklanacağız. Her dosyanın, öncesindeki veya sonrasındaki ses segmentleri dikkate alınmadan ayrı ayrı kodlandığı durumlarda.

Temel Kurulum

Öncelikle geri dönüp MediaSource örneğinin temel kurulumunu ele alalım. Adından da anlaşılacağı gibi Media Source Extensions, mevcut medya öğelerinin yalnızca uzantılarıdır. Aşağıda, MediaSource örneğimizi temsil eden bir Object URL değerini, ses öğesinin kaynak özelliğine atıyoruz. Bu işlem, standart bir URL ayarlamaya benzer.

var audio = document.createElement('audio');
var mediaSource = new MediaSource();
var SEGMENTS = 5;

mediaSource.addEventListener('sourceopen', function () {
  var sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');

  function onAudioLoaded(data, index) {
    // Append the ArrayBuffer data into our new SourceBuffer.
    sourceBuffer.appendBuffer(data);
  }

  // Retrieve an audio segment via XHR.  For simplicity, we're retrieving the
  // entire segment at once, but we could also retrieve it in chunks and append
  // each chunk separately.  MSE will take care of assembling the pieces.
  GET('sintel/sintel_0.mp3', function (data) {
    onAudioLoaded(data, 0);
  });
});

audio.src = URL.createObjectURL(mediaSource);

MediaSource nesnesi bağlandıktan sonra bazı başlatma işlemleri gerçekleştirir ve sonunda bir sourceopen etkinliği tetikler. Bu noktada bir SourceBuffer oluşturabiliriz. Yukarıdaki örnekte, MP3 segmentlerimizi ayrıştırıp kodunu çözebilen bir audio/mpeg oluşturuyoruz. Kullanabileceğiniz diğer türler de mevcuttur.

Anormal Dalga Biçimleri

Birazdan koda geri döneceğiz ancak şimdi yeni eklediğimiz dosyaya, özellikle de sonuna daha yakından bakalım. Aşağıda, sintel_0.mp3 parçasından alınan ve her iki kanalda ortalaması alınmış son 3.000 örneğin grafiği yer almaktadır. Kırmızı çizgi üzerindeki her piksel, [-1.0, 1.0] aralığında bir kayan nokta örneğidir.

mp3 gap

What's with all that those zero (silent) samples!? Bu sorunlar aslında kodlama sırasında ortaya çıkan sıkıştırma yapaylıklarından kaynaklanır. Neredeyse her kodlayıcı bir tür dolgu kullanır. Bu durumda LAME, dosyanın sonuna tam olarak 576 dolgu örneği ekledi.

Her dosyanın sonuna eklenen dolgunun yanı sıra başına da dolgu eklenmişti. sintel_1.mp3Parçaya göz atarsak ön tarafta 576 örnek daha dolgu olduğunu görürüz. Dolgu miktarı kodlayıcıya ve içeriğe göre değişir ancak her dosyada yer alan metadata temelinde tam değerleri biliriz.

mp3 gap end

Önceki demoda segmentler arasındaki aksaklıklara neden olan, her dosyanın başındaki ve sonundaki sessizlik bölümleridir. Boşluksuz oynatma için bu sessiz bölümleri kaldırmamız gerekir. Neyse ki bu işlem MediaSource ile kolayca yapılabilir. Aşağıda, bu sessizliği kaldırmak için onAudioLoaded() yöntemimizi değiştirerek ekleme penceresi ve zaman damgası kaydırması kullanacağız.

Örnek Kod

function onAudioLoaded(data, index) {
  // Parsing gapless metadata is unfortunately non trivial and a bit messy, so
  // we'll glaze over it here; see the appendix for details.
  // ParseGaplessData() will return a dictionary with two elements:
  //
  //    audioDuration: Duration in seconds of all non-padding audio.
  //    frontPaddingDuration: Duration in seconds of the front padding.
  //
  var gaplessMetadata = ParseGaplessData(data);

  // Each appended segment must be appended relative to the next.  To avoid any
  // overlaps, we'll use the end timestamp of the last append as the starting
  // point for our next append or zero if we haven't appended anything yet.
  var appendTime = index > 0 ? sourceBuffer.buffered.end(0) : 0;

  // Simply put, an append window allows you to trim off audio (or video) frames
  // which fall outside of a specified time range.  Here, we'll use the end of
  // our last append as the start of our append window and the end of the real
  // audio data for this segment as the end of our append window.
  sourceBuffer.appendWindowStart = appendTime;
  sourceBuffer.appendWindowEnd = appendTime + gaplessMetadata.audioDuration;

  // The timestampOffset field essentially tells MediaSource where in the media
  // timeline the data given to appendBuffer() should be placed.  I.e., if the
  // timestampOffset is 1 second, the appended data will start 1 second into
  // playback.
  //
  // MediaSource requires that the media timeline starts from time zero, so we
  // need to ensure that the data left after filtering by the append window
  // starts at time zero.  We'll do this by shifting all of the padding we want
  // to discard before our append time (and thus, before our append window).
  sourceBuffer.timestampOffset =
    appendTime - gaplessMetadata.frontPaddingDuration;

  // When appendBuffer() completes, it will fire an updateend event signaling
  // that it's okay to append another segment of media.  Here, we'll chain the
  // append for the next segment to the completion of our current append.
  if (index == 0) {
    sourceBuffer.addEventListener('updateend', function () {
      if (++index < SEGMENTS) {
        GET('sintel/sintel_' + index + '.mp3', function (data) {
          onAudioLoaded(data, index);
        });
      } else {
        // We've loaded all available segments, so tell MediaSource there are no
        // more buffers which will be appended.
        mediaSource.endOfStream();
        URL.revokeObjectURL(audio.src);
      }
    });
  }

  // appendBuffer() will now use the timestamp offset and append window settings
  // to filter and timestamp the data we're appending.
  //
  // Note: While this demo uses very little memory, more complex use cases need
  // to be careful about memory usage or garbage collection may remove ranges of
  // media in unexpected places.
  sourceBuffer.appendBuffer(data);
}

Sorunsuz Dalga Biçimi

Ekleme pencerelerimizi uyguladıktan sonra dalga biçimine tekrar göz atarak yepyeni kodumuzun neler başardığını görelim. Aşağıda, sintel_0.mp3 (kırmızıyla gösterilen) sonundaki sessiz bölümün ve sintel_1.mp3 (maviyle gösterilen) başındaki sessiz bölümün kaldırıldığını görebilirsiniz. Böylece, segmentler arasında sorunsuz bir geçiş sağlanır.

mp3 mid

Sonuç

Böylece beş segmenti sorunsuz bir şekilde tek bir videoda birleştirdik ve demonun sonuna geldik. Devam etmeden önce, onAudioLoaded() yöntemimizin kapsayıcıları veya codec'leri dikkate almadığını fark etmiş olabilirsiniz. Bu nedenle, tüm bu teknikler kapsayıcı veya codec türünden bağımsız olarak çalışır. Aşağıda, MP3 yerine orijinal demo DASH'e hazır parçalanmış MP4'ü yeniden oynatabilirsiniz.

Demo

Daha fazla bilgi edinmek isterseniz kesintisiz içerik oluşturma ve meta veri ayrıştırma hakkında daha ayrıntılı bilgi için aşağıdaki ekleri inceleyin. Bu demoyu destekleyen koda daha yakından bakmak için gapless.js bölümünü de inceleyebilirsiniz.

Okuduğunuz için teşekkür ederiz.

Ek A: Boşluksuz İçerik Oluşturma

Boşluksuz içerik oluşturmak zor olabilir. Aşağıda, bu demoda kullanılan Sintel medyası oluşturma sürecini adım adım açıklayacağız. Başlamak için Sintel'in kayıpsız FLAC film müziğinin bir kopyasına ihtiyacınız olacak. SHA1, ileride kullanmak üzere aşağıda verilmiştir. Araçlar için FFmpeg, MP4Box, LAME ve afconvert ile bir OSX yüklemesi gerekir.

    unzip Jan_Morgenstern-Sintel-FLAC.zip
    sha1sum 1-Snow_Fight.flac
    # 0535ca207ccba70d538f7324916a3f1a3d550194  1-Snow_Fight.flac

İlk olarak, 1-Snow_Fight.flac parçasının ilk 31,5 saniyesini ayıracağız. Ayrıca, oynatma tamamlandıktan sonra tıklama olmaması için 28.saniyede başlayıp 2,5 saniye süren bir solma efekti eklemek istiyoruz. Aşağıdaki FFmpeg komut satırını kullanarak tüm bunları yapabilir ve sonuçları sintel.flac içine yerleştirebiliriz.

    ffmpeg -i 1-Snow_Fight.flac -t 31.5 -af "afade=t=out:st=28:d=2.5" sintel.flac

Ardından, dosyayı her biri 6,5 saniye uzunluğunda 5 wave dosyasına böleceğiz.Neredeyse her kodlayıcı, wave dosyalarının alınmasını desteklediği için bu dosya türünü kullanmak en kolay yöntemdir. Bunu FFmpeg ile de yapabiliriz. Bu durumda sintel_0.wav, sintel_1.wav, sintel_2.wav, sintel_3.wav ve sintel_4.wav dosyalarımız olur.

    ffmpeg -i sintel.flac -acodec pcm_f32le -map 0 -f segment \
           -segment_list out.list -segment_time 6.5 sintel_%d.wav

Şimdi MP3 dosyalarını oluşturalım. LAME, aralıksız içerik oluşturmak için çeşitli seçenekler sunar. İçeriği kontrol ediyorsanız segmentler arasında hiç dolgu olmaması için tüm dosyaların toplu kodlamasıyla --nogap kullanmayı düşünebilirsiniz. Ancak bu demo için dolguya ihtiyacımız var. Bu nedenle, dalga dosyalarının standart ve yüksek kaliteli VBR kodlamasını kullanacağız.

    lame -V=2 sintel_0.wav sintel_0.mp3
    lame -V=2 sintel_1.wav sintel_1.mp3
    lame -V=2 sintel_2.wav sintel_2.mp3
    lame -V=2 sintel_3.wav sintel_3.mp3
    lame -V=2 sintel_4.wav sintel_4.mp3

MP3 dosyalarını oluşturmak için gereken tek şey budur. Şimdi de parçalanmış MP4 dosyalarının oluşturulmasını ele alalım. iTunes için mastering edilmiş medya oluşturma konusunda Apple'ın talimatlarına uyarız. Aşağıda, dalga dosyalarını önerilen parametreleri kullanarak MP4 kapsayıcısında AAC olarak kodlamadan önce talimatlara uygun şekilde ara CAF dosyalarına dönüştüreceğiz.

    afconvert sintel_0.wav sintel_0_intermediate.caf -d 0 -f caff \
              --soundcheck-generate
    afconvert sintel_1.wav sintel_1_intermediate.caf -d 0 -f caff \
              --soundcheck-generate
    afconvert sintel_2.wav sintel_2_intermediate.caf -d 0 -f caff \
              --soundcheck-generate
    afconvert sintel_3.wav sintel_3_intermediate.caf -d 0 -f caff \
              --soundcheck-generate
    afconvert sintel_4.wav sintel_4_intermediate.caf -d 0 -f caff \
              --soundcheck-generate
    afconvert sintel_0_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
              -b 256000 -q 127 -s 2 sintel_0.m4a
    afconvert sintel_1_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
              -b 256000 -q 127 -s 2 sintel_1.m4a
    afconvert sintel_2_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
              -b 256000 -q 127 -s 2 sintel_2.m4a
    afconvert sintel_3_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
              -b 256000 -q 127 -s 2 sintel_3.m4a
    afconvert sintel_4_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
              -b 256000 -q 127 -s 2 sintel_4.m4a

Artık MediaSource ile kullanılabilmeleri için uygun şekilde parçalara ayırmamız gereken birkaç M4A dosyamız var. Bu örnekte, bir saniyelik bir parça boyutu kullanacağız. MP4Box, her parçalanmış MP4'ü sintel_#_dashinit.mp4 olarak ve bir MPEG-DASH manifesti (sintel_#_dash.mpd) ile birlikte yazar. Bu manifest atılabilir.

    MP4Box -dash 1000 sintel_0.m4a && mv sintel_0_dashinit.mp4 sintel_0.mp4
    MP4Box -dash 1000 sintel_1.m4a && mv sintel_1_dashinit.mp4 sintel_1.mp4
    MP4Box -dash 1000 sintel_2.m4a && mv sintel_2_dashinit.mp4 sintel_2.mp4
    MP4Box -dash 1000 sintel_3.m4a && mv sintel_3_dashinit.mp4 sintel_3.mp4
    MP4Box -dash 1000 sintel_4.m4a && mv sintel_4_dashinit.mp4 sintel_4.mp4
    rm sintel_{0,1,2,3,4}_dash.mpd

İşte bu kadar. Artık kesintisiz oynatma için gerekli doğru meta verileri içeren parçalanmış MP4 ve MP3 dosyalarımız var. Bu meta verilerin nasıl göründüğü hakkında daha fazla bilgi için Ek B'ye bakın.

Ek B: Boşluksuz Meta Verileri Ayrıştırma

Boşluksuz içerik oluşturmak gibi, boşluksuz meta verileri ayrıştırmak da depolama için standart bir yöntem olmadığından zor olabilir. Aşağıda, en yaygın iki kodlayıcı olan LAME ve iTunes'un aralıksız meta verilerini nasıl depoladığını ele alacağız. Bazı yardımcı yöntemler ve yukarıda kullanılan ParseGaplessData() için bir taslak oluşturarak başlayalım.

    // Since most MP3 encoders store the gapless metadata in binary, we'll need a
    // method for turning bytes into integers.  Note: This doesn't work for values
    // larger than 2^30 since we'll overflow the signed integer type when shifting.
    function ReadInt(buffer) {
      var result = buffer.charCodeAt(0);
      for (var i = 1; i < buffer.length; ++i) {
        result <<= 8;
        result += buffer.charCodeAt(i);
      }
      return result;
    }

    function ParseGaplessData(arrayBuffer) {
      // Gapless data is generally within the first 512 bytes, so limit parsing.
      var byteStr = new TextDecoder().decode(arrayBuffer.slice(0, 512));

      var frontPadding = 0, endPadding = 0, realSamples = 0;

      // ... we'll fill this in as we go below.

En kolay ayrıştırılıp açıklanabilen format olduğu için ilk olarak Apple'ın iTunes meta veri biçimini ele alacağız. iTunes (ve afconvert), MP3 ve M4A dosyalarına ASCII olarak kısa bir bölüm yazar:

    iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00

Bu, MP3 kapsayıcısındaki bir ID3 etiketi ve MP4 kapsayıcısındaki bir meta veri atomu içine yazılır. Bu örnekte, ilk 0000000 jetonunu göz ardı edebiliriz. Sonraki üç jeton, ön dolgu, son dolgu ve toplam dolgu dışı örnek sayısıdır. Bunların her birini sesin örnekleme hızına böldüğümüzde her birinin süresini elde ederiz.

// iTunes encodes the gapless data as hex strings like so:
//
//    'iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00'
//    'iTunSMPB[ 26 bytes ]####### frontpad  endpad    real samples'
//
// The approach here elides the complexity of actually parsing MP4 atoms. It
// may not work for all files without some tweaks.
var iTunesDataIndex = byteStr.indexOf('iTunSMPB');
if (iTunesDataIndex != -1) {
  var frontPaddingIndex = iTunesDataIndex + 34;
  frontPadding = parseInt(byteStr.substr(frontPaddingIndex, 8), 16);

  var endPaddingIndex = frontPaddingIndex + 9;
  endPadding = parseInt(byteStr.substr(endPaddingIndex, 8), 16);

  var sampleCountIndex = endPaddingIndex + 9;
  realSamples = parseInt(byteStr.substr(sampleCountIndex, 16), 16);
}

Diğer taraftan, çoğu açık kaynaklı MP3 kodlayıcı, boşluksuz meta verileri sessiz bir MPEG çerçevesinin içine yerleştirilmiş özel bir Xing üstbilgisinde saklar (sessiz olduğundan, Xing üstbilgisini anlamayan kod çözücüler yalnızca sessizlik oynatır). Maalesef bu etiket her zaman mevcut değildir ve bir dizi isteğe bağlı alanı vardır. Bu demo amacıyla medyayı kontrol ediyoruz ancak uygulamada, boşluksuz meta verilerin ne zaman kullanılabileceğini öğrenmek için bazı ek hassasiyet kontrolleri yapılması gerekir.

Öncelikle toplam örnek sayısını ayrıştıracağız. Basitlik açısından bunu Xing başlığından okuyacağız ancak normal MPEG ses başlığından da oluşturulabilir. Çapraz geçiş üstbilgileri Xing veya Info etiketiyle işaretlenebilir. Bu etiketten tam olarak 4 bayt sonra, dosyadaki toplam kare sayısını temsil eden 32 bitlik bir değer bulunur. Bu değeri kare başına örnek sayısıyla çarptığımızda dosyadaki toplam örnek sayısını elde ederiz.

    // Xing padding is encoded as 24bits within the header.  Note: This code will
    // only work for Layer3 Version 1 and Layer2 MP3 files with XING frame counts
    // and gapless information.  See the following document for more details:
    // http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header
    var xingDataIndex = byteStr.indexOf('Xing');
    if (xingDataIndex == -1) xingDataIndex = byteStr.indexOf('Info');
    if (xingDataIndex != -1) {
      // See section 2.3.1 in the link above for the specifics on parsing the Xing
      // frame count.
      var frameCountIndex = xingDataIndex + 8;
      var frameCount = ReadInt(byteStr.substr(frameCountIndex, 4));

      // For Layer3 Version 1 and Layer2 there are 1152 samples per frame.  See
      // section 2.1.5 in the link above for more details.
      var paddedSamples = frameCount * 1152;

      // ... we'll cover this below.

Toplam örnek sayısını bildiğimize göre artık dolgu örneklerinin sayısını okumaya geçebiliriz. Kodlayıcınıza bağlı olarak bu, Xing üstbilgisinde yer alan bir LAME veya Lavf etiketi altında yazılabilir. Bu başlığın tam olarak 17 bayt sonrasında, sırasıyla 12 bitlik ön ve son dolguyu temsil eden 3 bayt bulunur.

        xingDataIndex = byteStr.indexOf('LAME');
        if (xingDataIndex == -1) xingDataIndex = byteStr.indexOf('Lavf');
        if (xingDataIndex != -1) {
          // See http://gabriel.mp3-tech.org/mp3infotag.html#delays for details of
          // how this information is encoded and parsed.
          var gaplessDataIndex = xingDataIndex + 21;
          var gaplessBits = ReadInt(byteStr.substr(gaplessDataIndex, 3));

          // Upper 12 bits are the front padding, lower are the end padding.
          frontPadding = gaplessBits >> 12;
          endPadding = gaplessBits & 0xFFF;
        }

        realSamples = paddedSamples - (frontPadding + endPadding);
      }

      return {
        audioDuration: realSamples * SECONDS_PER_SAMPLE,
        frontPaddingDuration: frontPadding * SECONDS_PER_SAMPLE
      };
    }

Bu sayede, boşluksuz içeriklerin büyük çoğunluğunu ayrıştırmak için eksiksiz bir işlevimiz var. Ancak kesinlikle uç durumlar da vardır. Bu nedenle, benzer kodu üretimde kullanmadan önce dikkatli olmanız önerilir.

Ek C: Çöp Toplama Hakkında

SourceBuffer örneklerine ait bellek, içerik türü, platforma özgü sınırlar ve mevcut oynatma konumuna göre aktif olarak çöp toplanır. Chrome'da bellek öncelikle oynatılmış arabelleklerden geri alınır. Ancak bellek kullanımı platforma özgü sınırları aşarsa oynatılmamış arabelleklerden bellek kaldırılır.

Oynatma, geri kazanılan bellek nedeniyle zaman çizelgesinde bir boşluğa ulaştığında boşluk yeterince küçükse aksaklık yaşanabilir, boşluk çok büyükse oynatma tamamen durabilir. Bu durum, iyi bir kullanıcı deneyimi sunmanızı da engeller. Bu nedenle, aynı anda çok fazla veri eklemekten kaçınmak ve artık gerekli olmayan aralıkları medya zaman çizelgesinden manuel olarak kaldırmak önemlidir.

Aralıklar, her SourceBuffer üzerinde remove() yöntemiyle kaldırılabilir. Bu işlem, [start, end] aralığını saniye cinsinden alır. appendBuffer() parametresine benzer şekilde, her remove() tamamlandığında bir kez updateend etkinliği tetikler. Etkinlik tetiklenene kadar başka kaldırma veya ekleme işlemleri yapılmamalıdır.

Masaüstü Chrome'da, yaklaşık 12 megabayt ses içeriğini ve 150 megabayt video içeriğini aynı anda bellekte tutabilirsiniz. Tarayıcılar veya platformlar arasında bu değerlere güvenmemelisiniz. Örneğin, bu değerler kesinlikle mobil cihazları temsil etmez.

Atık toplama yalnızca SourceBuffers'ya eklenen verileri etkiler. JavaScript değişkenlerinde arabelleğe alınabilecek veri miktarıyla ilgili bir sınır yoktur. Gerekirse aynı verileri aynı konuma yeniden ekleyebilirsiniz.

Geri bildirim