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

Dale Curtis
Dale Curtis

Giriş

Medya Kaynağı Uzantıları (MSE), HTML5 <audio> ve <video> öğeleri için genişletilmiş arabelleğe alma ve oynatma kontrolü sağlar. Başlangıçta HTTP (DASH) tabanlı video oynatıcıları kolaylaştırmak için geliştirilmiş olsa da aşağıda, özellikle aralıksız oynatma için ses için nasıl kullanılabileceklerini inceleyeceğiz.

Muhtemelen şarkıların farklı parçalarda sorunsuz bir şekilde aktığı bir müzik albümü dinlemişsinizdir. Şu anda başka bir parçayı bile dinliyor olabilirsiniz. Sanatçılar bu aralıksız çalma deneyimlerini hem sanatsal bir tercih hem de sesin kesintisiz bir akış halinde yazıldığı vinil plak ve CD'lerden oluşan bir eser olarak yaratır. MP3 ve AAC gibi modern ses codec'lerinin çalışma şekli nedeniyle bu sorunsuz işitsel deneyim maalesef günümüzde çoğu zaman kaybediliyor.

Nedenin ayrıntılarına aşağıda gireceğiz ama şimdilik bir tanıtımla başlayalım. Aşağıda, beş ayrı MP3 dosyasına kesilip MSE kullanılarak yeniden derlenen mükemmel Sintel'in ilk otuz saniyesini görebilirsiniz. Kırmızı çizgiler, her MP3'ün oluşturulması (kodlama) sırasında ortaya çıkan boşlukları belirtir. Bu noktalarda arıza sesi duyarsınız.

Tanıtım

Hay aksi! Bu çok iyi bir deneyim değil. Daha iyisini yapabiliriz. Biraz daha çalışarak, yukarıdaki demoda yer alan MP3 dosyalarının aynısını kullanarak bu can sıkıcı boşlukları gidermek için MSE'yi kullanabiliriz. Bir sonraki demodaki yeşil çizgiler dosyaların nerede birleştirildiğini ve boşlukların kaldırıldığını gösterir. Bu özellik, Chrome 38 ve sonraki sürümlerde sorunsuz bir şekilde oynatılır.

Tanıtım

Boşluksuz içerik oluşturmanın çeşitli yolları vardır. Bu demonun amacı doğrultusunda, normal bir kullanıcının dosya üzerinde durabileceği dosya türlerine odaklanacağız. Burada, her bir dosya, kendisinden önceki veya sonraki ses segmentlerine bakılmaksızın ayrı ayrı kodlanmıştır.

Temel Kurulum

İlk olarak, bir MediaSource örneğine geri dönüp temel ayarları ele alalım. Medya Kaynağı Uzantıları, adından da anlaşılacağı gibi yalnızca mevcut medya öğelerinin uzantılarıdır. Aşağıda, tıpkı standart URL ayarladığınız gibi, MediaSource örneğimizi temsil eden bir ses öğesinin kaynak özelliğine bir Object URL atıyoruz.

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 bir miktar başlatma işlemi gerçekleştirir ve sonunda bir sourceopen etkinliği tetikler. Bu noktada bir SourceBuffer oluşturabiliriz. Yukarıdaki örnekte, MP3 segmentlerimizi ayrıştırabilen ve kodunu çözebilen bir audio/mpeg oluşturacağız. Çeşitli başka türler de mevcut.

Anormal Dalga Biçimleri

Birazdan koda geri döneceğiz, ancak şimdi az önce eklediğimiz dosyayı, özellikle de sonuna daha yakından inceleyelim. Aşağıda, sintel_0.mp3 parçasından her iki kanalda ortalaması alınan son 3.000 örneğin grafiği verilmiştir. Kırmızı çizgideki her piksel, [-1.0, 1.0] aralığında bir kayan nokta örneğidir.

mp3 boşluğu

O sıfır (sessiz) örneklerin nedeni ne? Bunun nedeni, kodlama sırasında eklenen sıkıştırma yapılarıdır. Neredeyse her kodlayıcı, bir tür dolgu sunar. Bu durumda LAME, dosyanın sonuna tam olarak 576 dolgu örneği ekler.

Sondaki dolguya ek olarak, her dosyanın başına dolgu da eklendi. Parçanın ilerisine baktığımızda sintel_1.mp3 önde 576 başka dolgu örneği olduğunu görürüz. Dolgu miktarı kodlayıcıya ve içeriğe göre değişir ancak her bir dosyaya dahil edilen metadata değerine göre tam değerleri biliyoruz.

mp3 boşluk sonu

Her dosyanın başındaki ve sonundaki sessiz bölümler, önceki demoda segmentler arasındaki aksaklıklara neden olur. Kesintisiz oynatma için bu sessizlik bölümlerini kaldırmamız gerekir. Neyse ki MediaSource ile bu işlemi kolayca yapabilirsiniz. Aşağıda, bu sessizliği kaldırmak için pencere ekleme ve zaman damgası ofseti kullanılacak şekilde onAudioLoaded() yöntemimizi değiştireceğiz.

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

Kusursuz Dalga Formu

Ekleme pencerelerimizi uyguladıktan sonra dalga formuna bir kez daha göz atarak parlak yeni kodumuzun ne başardığını görelim. Aşağıda, sintel_0.mp3 sonunda sessiz bölümün (kırmızı) ve sintel_1.mp3 başındaki sessiz bölümün (mavi) kaldırıldığını görebilirsiniz. Bu sayede segmentler arasında sorunsuz bir geçiş sağlanır.

mp3 orta

Sonuç

Böylece, beş segmenti de kusursuz bir şekilde tek bir parçada birleştirdik ve böylece demomuzun sonuna geldik. Ayrılmadan önce, onAudioLoaded() yöntemimizin container'ları veya codec'leri dikkate almadığını fark etmiş olabilirsiniz. Bu da, container veya Codec türünden bağımsız olarak tüm bu tekniklerin çalışacağı anlamına gelir. Aşağıda, MP3 yerine, DASH için hazır parçalı MP4 orijinal demosunu yeniden oynatabilirsiniz.

Tanıtım

Boşluksuz içerik oluşturma ve meta veri ayrıştırma ile ilgili daha ayrıntılı bilgi edinmek için aşağıdaki eklere göz atabilirsiniz. Bu demoyu destekleyen kodu daha ayrıntılı bir şekilde incelemek için gapless.js adresini de keşfedebilirsiniz.

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

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

Boşluksuz içerikler oluşturmak zor olabilir. Aşağıda, bu demoda kullanılan Sintel medyasının nasıl oluşturulduğunu inceleyeceğiz. Başlamak için Sintel'in kayıpsız FLAC film müziğinin bir kopyasına ihtiyacınız vardır. İleride yayınlanacak olması için SHA1 aşağıda verilmiştir. Araçlar için FFmpeg, MP4Box, LAME ve afconvert özellikli bir OSX kurulumu gerekir.

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

Öncelikle 1-Snow_Fight.flac parçasının ilk 31, 5 saniyesini ayıracağız. Ayrıca, oynatma bittikten sonra tıklamaları önlemek için 28.saniyeden itibaren 2,5 saniyelik bir sönme eklemek istiyoruz. Aşağıdaki FFmpeg komut satırını kullanarak tüm bunları gerçekleştirebilir ve sonuçları sintel.flac dosyasına koyabiliriz.

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

Daha sonra dosyayı her biri 6, 5 saniyelik 5 wave dosyasına ayıracağız.Hemen her kodlayıcı bu dosyanın beslemesini desteklediğinden wave'in kullanımı en kolay olan dosyadır. Aynı şekilde, bu işlemi FFmpeg ile hassas bir şekilde yapabiliriz. Sonrasında şöyle olur: sintel_0.wav, sintel_1.wav, sintel_2.wav, sintel_3.wav ve sintel_4.wav.

    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, boşluksuz içerik oluşturmak için çeşitli seçeneklere sahiptir. İçeriğin kontrolü sizdeyse segmentlerin arasında dolgu olmasını önlemek için tüm dosyaları toplu kodlamayla --nogap kullanabilirsiniz. Ancak bu demonun amacı doğrultusunda bu dolguyu kullanmak istiyoruz. Bu nedenle wave dosyaları için standart yüksek kaliteli bir VBR kodlaması 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 tüm yapmanız gereken budur. Şimdi, parçalara ayrılmış MP4 dosyalarının nasıl oluşturulduğunu ele alalım. iTunes için uzmanlaşmış medya oluşturma konusunda Apple'ın talimatlarını uygulayacağız. Aşağıda, önerilen parametreleri kullanarak wave dosyalarını MP4 kapsayıcısında AAC olarak kodlamadan önce talimatlara uygun olarak 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

Şu anda, MediaSource ile kullanılmadan önce uygun şekilde parçalamamız gereken birkaç M4A dosyamız var. Amaçlarımız için bir saniyelik parça boyutunu kullanacağız. MP4Box, parçalara ayrılmış her MP4'ü, silinebilecek bir MPEG-DASH manifesti (sintel_#_dash.mpd) ile birlikte sintel_#_dashinit.mp4 olarak yazar.

    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 aralıksız çalma için gereken doğru meta verilere sahip, parçalara ayrılmış MP4 ve MP3 dosyalarınız var. Meta verilerin nasıl göründüğüyle ilgili daha fazla bilgi için Ek B'ye bakın.

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

Boşluksuz içerik oluşturmada olduğu gibi, depolama için standart bir yöntem olmadığından boşluksuz meta verileri ayrıştırmak da yanıltıcı olabilir. Aşağıda, en yaygın iki kodlayıcının (LAME ve iTunes) boşluksuz meta verilerini nasıl depoladığını ele alacağız. Bazı yardımcı yöntemler oluşturarak ve yukarıda kullanılan ParseGaplessData() için bir özet 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.

Ayrıştırılması ve açıklanması en kolay yöntem olduğu için ilk olarak Apple'ın iTunes meta veri biçimini ele alacağız. iTunes (ve afconvert) MP3 ve M4A dosyaları için ASCII'de aşağıdaki gibi kısa bir bölüm yazın:

    iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00

Bu anahtar, MP3 kapsayıcısı içinde bir ID3 etiketinin içine ve MP4 kapsayıcısı içindeki bir meta veri atomunun içine yazılır. Bu amaçlar doğrultusunda ilk 0000000 jetonunu yok sayabiliriz. Sonraki üç jeton ön dolgu, son dolgu ve toplam dolgu olmayan örnek sayısıdır. Bunların her birini sesin örnek hızına bölerek her birinin süresini buluruz.

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

Öte yandan, çoğu açık kaynaklı MP3 kodlayıcı, boşluksuz meta verileri sessiz bir MPEG çerçevesinin içine yerleştirilmiş özel bir Xing başlığı içinde depolar (sessizdir, Xing başlığını anlamayan kod çözücüler ise sessiz bir şekilde çalar). Ne yazık ki bu etiket her zaman mevcut değil ve birçok isteğe bağlı alan içeriyor. Bu demonun amacı doğrultusunda, medya üzerinde kontrol sahibiyiz. Ancak uygulamada boşluksuz meta verilerin gerçekten kullanılabilir olup olmadığını bilmek için bazı ek hassasiyet kontrolleri yapılması gerekir.

İlk olarak toplam örnek sayısını ayrıştıracağız. Basit olması için bunu Xing başlığından okuyacağız, ancak normal MPEG ses başlığından oluşturulabilir. Xing üst bilgileri, Xing veya Info etiketiyle işaretlenebilir. Bu etiketten tam olarak 4 bayt sonra dosyadaki toplam kare sayısını temsil eden 32 bit bulunur. Bu değerin kare başına örnek sayısıyla çarpılması, dosyadaki toplam örnekleri elde etmemizi sağlar.

    // 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.

Artık toplam örnek sayısına sahip olduğumuza göre dolgu örneklerinin sayısını okumaya geçebiliriz. Kodlayıcınıza bağlı olarak bu, Xing başlığına yerleştirilmiş bir LAME veya Lavf etiketi altında yazılabilir. Bu başlıktan sonra tam olarak 17 bayt olmak üzere sırasıyla ön ve son dolguyu 12 bit olarak 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
      };
    }

Bununla birlikte, boşluksuz içeriklerin büyük çoğunluğunu ayrıştırmak için eksiksiz bir işlevimiz vardır. Yine de uç durumlar oldukça fazladır. Bu nedenle, üretimde benzer bir kod kullanmadan önce dikkatli olmanız önerilir.

Ek C: Çöp Toplama Hakkında

SourceBuffer örneklerine ait bellek; içerik türüne, platforma özel sınırlara ve mevcut oynatma konumuna göre etkin olarak atık toplanır. Chrome'da bellek, önceden oynatılan arabelleklerden geri alınır. Ancak bellek kullanımı platforma özel sınırları aşarsa bellek, oynatılmayan arabelleklerden kaldırılır.

Geri alınan bellek nedeniyle oynatma, zaman çizelgesinde bir boşluğa ulaştığında, boşluk yeterince küçükse boşluk oluşabilir veya boşluk çok büyükse tamamen duraklayabilir. İkisi de mükemmel bir kullanıcı deneyimi sunmaz. Bu nedenle, tek seferde ç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öntemi kullanılarak kaldırılabilir. Bu yöntem, saniye cinsinden bir [start, end] aralığı alır. appendBuffer()'a benzer şekilde, her remove() tamamlandığında bir updateend etkinliği tetikler. Diğer kaldırma veya ekleme işlemleri etkinlik tetiklenene kadar yapılmamalıdır.

Masaüstü Chrome'da, bellekte aynı anda yaklaşık 12 megabayt ses içeriği ve 150 megabayt video içeriği saklayabilirsiniz. Tarayıcılarda veya platformlarda bu değerleri temel almamalısınız. Örneğin, bu değerler kesinlikle mobil cihazları temsil etmez.

Atık toplama yalnızca SourceBuffers öğesine eklenen verileri etkiler. JavaScript değişkenlerinde ne kadar veriyi arabelleğe alabileceğinizle ilgili herhangi bir sınır yoktur. Ayrıca, gerekirse aynı verileri aynı konuma yeniden ekleyebilirsiniz.

Geri bildirim