Ses ve video önceden yükleme özelliğiyle hızlı oynatma

Kaynakları etkin bir şekilde önceden yükleyerek medya oynatmanızı hızlandırma

Faruk Mercan
François Beaufort

Oynatma işleminin daha hızlı başlaması, videonuzu veya sesinizi daha fazla kişinin izlemesi anlamına gelir. Bu bilinen bir gerçektir. Bu makalede, kullanım alanınıza bağlı olarak kaynakları etkin bir şekilde önceden yükleyerek ses ve video oynatmanızı hızlandırmak için kullanabileceğiniz teknikleri inceleyeceğiz.

Katkıda bulunanlar: telif hakkı Blender Foundation | www.blender.org .

Medya dosyalarını önceden yüklemenin üç yöntemini açıklayacağım. Bunlar, bunların artıları ve eksilerinden başlıyor.

Harika... Ama...
Video önceden yükleme özelliği Web sunucusunda barındırılan benzersiz bir dosya için kullanımı kolaydır. Tarayıcılar bu özelliği tamamen yok sayabilir.
Kaynak getirme, HTML belgesi tamamen yüklendiğinde ve ayrıştırıldığında başlar.
Uygulama, MSE'ye medya sağlamaktan sorumlu olduğundan Medya Kaynağı Uzantıları (MSE), medya öğelerinde preload özelliğini yoksayar.
Bağlantı önceden yükleme Tarayıcıyı, dokümanın onload etkinliğini engellemeden video kaynağı isteğinde bulunmaya zorlar. HTTP Aralığı istekleri uyumlu değildir.
MSE ve dosya segmentleriyle uyumludur. Tüm kaynaklar getirilirken yalnızca küçük medya dosyaları (<5 MB) için kullanılmalıdır.
Manuel arabelleğe alma Tam denetim Karmaşık hataları ele alma, web sitesinin sorumluluğundadır.

Video önceden yükleme özelliği

Video kaynağı, web sunucusunda barındırılan benzersiz bir dosyaysa tarayıcıya ne kadar bilgi veya içeriğin önceden yükleneceği konusunda ipucu vermek için video preload özelliğini kullanabilirsiniz. Bu, Medya Kaynağı Uzantıları (MSE) öğesinin preload ile uyumlu olmadığı anlamına gelir.

Kaynak getirme, yalnızca ilk HTML belgesi tamamen yüklendiğinde ve ayrıştırıldığında başlar (ör. DOMContentLoaded etkinliği tetiklendiğinde), kaynak gerçekten getirildiğinde ise çok farklı load etkinliği tetiklenir.

preload özelliğinin metadata olarak ayarlanması, kullanıcının videoya ihtiyaç duymasının beklenmediğini ancak meta verilerin (boyutlar, parça listesi, süre vb.) alınmasının istendiğini belirtir. Chrome 64'ten itibaren preload için varsayılan değerin metadata olduğunu unutmayın. (Daha önce auto idi).

<video id="video" preload="metadata" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

preload özelliğinin auto olarak ayarlanması, tarayıcının daha fazla arabelleğe alma işlemi için durdurma gerektirmeden oynatmanın tamamlanmasını sağlayacak kadar veriyi önbelleğe alabileceğini belirtir.

<video id="video" preload="auto" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

Yine de dikkat edilmesi gereken bazı noktalar var. Bu sadece bir ipucu olduğundan tarayıcı preload özelliğini tamamen yok sayabilir. Bu metnin yazıldığı sırada Chrome'da uygulanan bazı kurallar aşağıda belirtilmiştir:

  • Veri Tasarrufu etkinleştirildiğinde Chrome preload değerini none yapmaya zorlar.
  • Android 4.3'te Chrome, bir Android Hatası nedeniyle preload değerini none olmaya zorlar.
  • Hücresel bağlantıda (2G, 3G ve 4G) Chrome, preload değerini metadata yapmaya zorlar.

İpuçları

Web siteniz aynı alanda çok sayıda video kaynağı içeriyorsa preload değerini metadata olarak ayarlamanızı veya poster özelliğini tanımlayıp preload değerini none olarak ayarlamanızı öneririz. Bu şekilde, aynı alan için maksimum sayıda HTTP bağlantısına (HTTP 1.1 spesifikasyonuna göre 6) ulaşmaktan kaçınmış olursunuz. Bu da kaynakların yüklenmesini geciktirebilir. Videolar temel kullanıcı deneyiminizin bir parçası değilse bu işlemin sayfa hızını da artırabileceğini unutmayın.

Diğer makalelerde bahsedildiği gibi, link preload (bağlantı önceden yükleme) özelliği, tarayıcının load etkinliğini engellemeden ve sayfa indirilirken bir kaynak için istekte bulunmasını zorunlu kılmanıza olanak tanıyan bildirim temelli bir getirme işlemidir. <link rel="preload"> aracılığıyla yüklenen kaynaklar tarayıcıda yerel olarak depolanır ve DOM, JavaScript veya CSS'de açıkça başvuruda bulunulana kadar etkili bir şekilde etkisizdir.

Önceden yükleme, geçerli gezinmeye odaklanması ve kaynakları türlerine (komut dosyası, stil, yazı tipi, video, ses vb.) göre öncelikli olarak getirmesi açısından önceden getirme işleminden farklıdır. Tarayıcı önbelleğini geçerli oturumlar için ısıtmak üzere kullanılmalıdır.

Tam videoyu önceden yükle

JavaScript'iniz video içeriği getirmeyi istediğinde, kaynak tarayıcı tarafından önbelleğe alınmış olabileceğinden video önbellekten okunur. Bunun için web sitenizdeki tam uzunluktaki bir videoyu nasıl önceden yükleyeceğinizi buradan öğrenebilirsiniz. Önceden yükleme isteği henüz tamamlanmamışsa normal bir ağ getirme işlemi gerçekleşir.

<link rel="preload" as="video" href="https://cdn.com/small-file.mp4">

<video id="video" controls></video>

<script>
  // Later on, after some condition has been met, set video source to the
  // preloaded video URL.
  video.src = 'https://cdn.com/small-file.mp4';
  video.play().then(() => {
    // If preloaded video URL was already cached, playback started immediately.
  });
</script>

Önceden yüklenmiş kaynak örnekteki bir video öğesi tarafından kullanılacağından as önceden yükleme bağlantısı değeri video olur. Bu bir ses öğesi olsaydı as="audio" olurdu.

İlk segmenti önceden yükle

Aşağıdaki örnekte, bir videonun ilk segmentinin <link rel="preload"> ile nasıl önceden yükleneceği ve bunun Medya Kaynağı Uzantıları ile nasıl kullanılacağı gösterilmektedir. MSE JavaScript API hakkında bilginiz yoksa MSE ile ilgili temel bilgiler bölümünü inceleyin.

Kolaylık olması açısından, videonun tamamının file_1.webm, file_2.webm, file_3.webm gibi daha küçük dosyalara bölündüğünü varsayalım.

<link rel="preload" as="fetch" href="https://cdn.com/file_1.webm">

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // If video is preloaded already, fetch will return immediately a response
    // from the browser cache (memory cache). Otherwise, it will perform a
    // regular network fetch.
    fetch('https://cdn.com/file_1.webm')
    .then(response => response.arrayBuffer())
    .then(data => {
      // Append the data into the new sourceBuffer.
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch file_2.webm when user starts playing video.
    })
    .catch(error => {
      // TODO: Show "Video is not available" message to user.
    });
  }
</script>

Destek

Aşağıdaki snippet'leri kullanarak <link rel=preload> için çeşitli as türlerinin desteklendiğini tespit edebilirsiniz:

function preloadFullVideoSupported() {
  const link = document.createElement('link');
  link.as = 'video';
  return (link.as === 'video');
}

function preloadFirstSegmentSupported() {
  const link = document.createElement('link');
  link.as = 'fetch';
  return (link.as === 'fetch');
}

Manuel arabelleğe alma

Cache API'yi ve hizmet çalışanlarını incelemeden önce, bir videonun MSE ile manuel olarak nasıl arabelleğe alınacağına bakalım. Aşağıdaki örnekte, web sunucunuzun HTTP Range isteklerini desteklediği varsayılmıştır, ancak bu durum dosya segmentleriyle oldukça benzerdir. Google Shaka Oynatıcısı, JW Player ve Video.js gibi bazı ara katman yazılımı kitaplıklarının bunu sizin yerinize yönetecek şekilde oluşturulduğunu unutmayın.

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // Fetch beginning of the video by setting the Range HTTP request header.
    fetch('file.webm', { headers: { range: 'bytes=0-567139' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      sourceBuffer.appendBuffer(data);
      sourceBuffer.addEventListener('updateend', updateEnd, { once: true });
    });
  }

  function updateEnd() {
    // Video is now ready to play!
    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);

    // Fetch the next segment of video when user starts playing the video.
    video.addEventListener('playing', fetchNextSegment, { once: true });
  }

  function fetchNextSegment() {
    fetch('file.webm', { headers: { range: 'bytes=567140-1196488' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      const sourceBuffer = mediaSource.sourceBuffers[0];
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch further segment and append it.
    });
  }
</script>

Dikkat edilmesi gereken noktalar

Artık medya arabelleğe alma deneyiminin tamamının kontrolü sizde olduğundan, önceden yüklemeyi düşünürken cihazın pil düzeyini, "Veri Tasarrufu Modu" kullanıcı tercihini ve ağ bilgilerini göz önünde bulundurmanızı öneririm.

Pil farkındalığı

Bir videoyu önceden yüklemeyi düşünmeden önce, kullanıcıların cihazlarının pil düzeyini göz önünde bulundurun. Bu, güç seviyesi düşük olduğunda pil ömrünü korur.

Cihazın pili azaldığında önceden yüklemeyi devre dışı bırakın veya en azından daha düşük çözünürlüklü bir videoyu önceden yükleyin.

if ('getBattery' in navigator) {
  navigator.getBattery()
  .then(battery => {
    // If battery is charging or battery level is high enough
    if (battery.charging || battery.level > 0.15) {
      // TODO: Preload the first segment of a video.
    }
  });
}

"Veri Tasarrufu"nu algılama

Tarayıcılarında "veri tasarrufu" modunu etkinleştirmiş kullanıcılara hızlı ve basit uygulamalar sunmak için Save-Data istemci ipucu istek başlığını kullanın. Uygulamanız, bu istek başlığını tanımlayarak maliyet ve performans kısıtlamalı kullanıcılara optimize edilmiş bir kullanıcı deneyimi sunabilir ve bu deneyimi sunabilir.

Daha fazla bilgi için Save-Data ile Hızlı ve Hafif Uygulamalar Sunma bölümüne bakın.

Ağ bilgilerine dayalı akıllı yükleme

Önceden yükleme işleminden önce navigator.connection.type başlıklı makaleyi incelemenizi öneririz. cellular olarak ayarlandığında önceden yüklemeyi engelleyebilir, kullanıcılara mobil ağ operatörlerinin bant genişliği için ücret alıyor olabileceği konusunda öneride bulunabilir ve yalnızca önceden önbelleğe alınan içeriği otomatik olarak oynatmayı başlatabilirsiniz.

if ('connection' in navigator) {
  if (navigator.connection.type == 'cellular') {
    // TODO: Prompt user before preloading video
  } else {
    // TODO: Preload the first segment of a video.
  }
}

Ağ değişikliklerine nasıl tepki vereceğinizi öğrenmek için de Ağ Bilgileri örneğine göz atın.

Birden çok ilk segmenti önceden önbelleğe al

Kullanıcının en sonunda hangi medyayı seçeceğini bilmeden bazı medya içeriklerini tahmine dayalı olarak önceden yüklemek istersem ne olur? Kullanıcı 10 video içeren bir web sayfasındaysa muhtemelen her birinden bir segment dosyası getirmek için yeterli belleğimiz vardır, ancak 10 gizli <video> öğesi ve 10 MediaSource nesnesi oluşturup bu verileri beslemeye kesinlikle başlamamamız gerekir.

Aşağıdaki iki bölümden oluşan örnekte, güçlü ve kullanımı kolay Cache API'yi kullanarak videonun birden çok ilk segmentini nasıl önceden önbelleğe alacağınız gösterilmektedir. Benzer bir işlemin IndexedDB ile de elde edilebileceğini unutmayın. Cache API'ye window nesnesinden de erişilebildiği için henüz Service Worker'ları kullanmıyoruz.

Getir ve önbelleğe al

const videoFileUrls = [
  'bat_video_file_1.webm',
  'cow_video_file_1.webm',
  'dog_video_file_1.webm',
  'fox_video_file_1.webm',
];

// Let's create a video pre-cache and store all first segments of videos inside.
window.caches.open('video-pre-cache')
.then(cache => Promise.all(videoFileUrls.map(videoFileUrl => fetchAndCache(videoFileUrl, cache))));

function fetchAndCache(videoFileUrl, cache) {
  // Check first if video is in the cache.
  return cache.match(videoFileUrl)
  .then(cacheResponse => {
    // Let's return cached response if video is already in the cache.
    if (cacheResponse) {
      return cacheResponse;
    }
    // Otherwise, fetch the video from the network.
    return fetch(videoFileUrl)
    .then(networkResponse => {
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, networkResponse.clone());
      return networkResponse;
    });
  });
}

HTTP Range isteklerini kullanacak olsaydım Cache API henüz Range yanıtlarını desteklemediğinden bir Response nesnesini manuel olarak yeniden oluşturmam gerekecekti. networkResponse.arrayBuffer() çağrısının, yanıtın tüm içeriğini bir defada oluşturucu belleğine getirdiğini unutmayın. Bu nedenle, küçük aralıklar kullanmak isteyebilirsiniz.

Referans olması açısından, yukarıdaki örneğin bir kısmını HTTP Aralığı isteklerini video önbelleğine kaydedecek şekilde değiştirdim.

    ...
    return fetch(videoFileUrl, { headers: { range: 'bytes=0-567139' } })
    .then(networkResponse => networkResponse.arrayBuffer())
    .then(data => {
      const response = new Response(data);
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, response.clone());
      return response;
    });

Videoyu oynat

Kullanıcılar oynat düğmesini tıkladığında Cache API'de bulunan ilk video segmentini getiririz. Böylece oynatma kullanılabiliyorsa hemen başlar. Aksi takdirde, verileri ağdan alırız. Tarayıcıların ve kullanıcıların Önbelleği temizlemeye karar verebileceğini unutmayın.

Daha önce görüldüğü gibi, videonun bu ilk segmentini video öğesine aktarmak için MSE'yi kullanırız.

function onPlayButtonClick(videoFileUrl) {
  video.load(); // Used to be able to play video later.

  window.caches.open('video-pre-cache')
  .then(cache => fetchAndCache(videoFileUrl, cache)) // Defined above.
  .then(response => response.arrayBuffer())
  .then(data => {
    const mediaSource = new MediaSource();
    video.src = URL.createObjectURL(mediaSource);
    mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

    function sourceOpen() {
      URL.revokeObjectURL(video.src);

      const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
      sourceBuffer.appendBuffer(data);

      video.play().then(() => {
        // TODO: Fetch the rest of the video when user starts playing video.
      });
    }
  });
}

Hizmet çalışanı ile aralık yanıtları oluşturma

Peki bir video dosyasının tamamını getirip Cache API'ye kaydettiyseniz ne olur? Tarayıcı HTTP Range isteği gönderdiğinde, Cache API henüz Range yanıtlarını desteklemediğinden videonun tamamını oluşturucu belleğine aktarmak kesinlikle istemezsiniz.

Şimdi bu isteklere nasıl müdahale edeceğimi ve bir Service Worker'dan özelleştirilmiş Range yanıtı döndüreceğimi göstereyim.

addEventListener('fetch', event => {
  event.respondWith(loadFromCacheOrFetch(event.request));
});

function loadFromCacheOrFetch(request) {
  // Search through all available caches for this request.
  return caches.match(request)
  .then(response => {

    // Fetch from network if it's not already in the cache.
    if (!response) {
      return fetch(request);
      // Note that we may want to add the response to the cache and return
      // network response in parallel as well.
    }

    // Browser sends a HTTP Range request. Let's provide one reconstructed
    // manually from the cache.
    if (request.headers.has('range')) {
      return response.blob()
      .then(data => {

        // Get start position from Range request header.
        const pos = Number(/^bytes\=(\d+)\-/g.exec(request.headers.get('range'))[1]);
        const options = {
          status: 206,
          statusText: 'Partial Content',
          headers: response.headers
        }
        const slicedResponse = new Response(data.slice(pos), options);
        slicedResponse.setHeaders('Content-Range': 'bytes ' + pos + '-' +
            (data.size - 1) + '/' + data.size);
        slicedResponse.setHeaders('X-From-Cache': 'true');

        return slicedResponse;
      });
    }

    return response;
  }
}

Bu dilimlenmiş yanıtı yeniden oluşturmak için response.blob() kullandığımı unutmama gerek. Bu şekilde dosya için bir herkese açık kullanıcı adı verirken response.arrayBuffer() dosyanın tamamını oluşturucu belleğine getiriyor.

Özel X-From-Cache HTTP üst bilgim, bu isteğin önbellekten mi yoksa ağdan mı geldiğini öğrenmek için kullanılabilir. ShakaPlayer gibi oyuncular tarafından ağ hızının göstergesi olarak yanıt süresini yoksaymak için kullanılabilir.

Range isteklerinin nasıl ele alınacağına dair eksiksiz bir çözüm için resmi Sample Media App'e, özellikle de ranged-response.js dosyasına göz atın.