Çevrimdışı yayın özellikli PWA

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

Progresif web uygulamaları, daha önce yerel uygulamalara özel olan birçok özelliği web'e getirir. PWA'larla ilişkili en belirgin özelliklerden biri çevrimdışı deneyimdir.

Daha da iyisi, kullanıcılarınıza birkaç farklı şekilde sunabileceğiniz bir geliştirme olan çevrimdışı medya akış deneyimidir. Ancak bu, gerçekten benzersiz bir soruna yol açar: Medya dosyaları çok büyük olabilir. Bu nedenle şu soruları sorabilirsiniz:

  • Büyük boyutlu video dosyalarını nasıl indirip depolayabilirim?
  • Bunu kullanıcıya nasıl sunabilirim?

Bu makalede, bu soruların yanıtlarını tartışırken, herhangi bir işlevsel veya sunum çerçevesi kullanmadan çevrimdışı medya aktarma deneyimini nasıl uygulayabileceğinize dair pratik örnekler sunan Kino demo PWA'sından da bahsedeceğiz. Aşağıdaki örnekler, bu özellikleri sağlamak için çoğu durumda mevcut medya çerçevelerinden birini kullanmanız gerektiğinden, esas olarak eğitim amaçlıdır.

Kendinizi geliştirmek için iyi bir işletme örneğiniz yoksa çevrimdışı akış içeren bir PWA oluşturmanın zorlukları vardır. Bu makalede, kullanıcılara yüksek kaliteli çevrimdışı medya deneyimi sunmak için kullanılan API'ler ve teknikler hakkında bilgi verilmektedir.

Büyük boyutlu bir medya dosyasını indirme ve depolama

Progresif web uygulamaları, çevrimdışı deneyim sunmak için gereken öğeleri (ör. dokümanlar, stil sayfaları, resimler) hem indirmek hem de depolamak üzere genellikle kullanışlı Cache API'yi kullanır.

Aşağıda, bir Hizmet Çalışanı'nda Cache API'nin kullanımına dair temel bir örnek verilmiştir:

const cacheStorageName = 'v1';

this.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheStorageName).then(function(cache) {
      return cache.addAll([
        'index.html',
        'style.css',
        'scripts.js',

        // Don't do this.
        'very-large-video.mp4',
      ]);
    })
  );
});

Yukarıdaki örnek teknik olarak işe yarasa da Cache API'nin kullanımı, büyük dosyalarda kullanımını pratik olmayan birkaç sınırlamaya sahiptir.

Örneğin, Cache API:

  • İndirme işlemlerini kolayca duraklatmanıza ve devam ettirmenize olanak tanır
  • İndirme işlemlerinin ilerleme durumunu izleyebilirsiniz
  • HTTP aralık isteklerine uygun şekilde yanıt verme

Bu sorunların tümü, video uygulamaları için oldukça ciddi sınırlamalardır. Daha uygun olabilecek diğer seçenekleri inceleyelim.

Günümüzde Getirme API'si, uzak dosyalara eşzamansız olarak erişmenin tarayıcılar arası bir yoludur. Kullanım alanımızda, büyük video dosyalarına akış olarak erişmenize ve HTTP aralığı isteği kullanarak bunları artımlı olarak parçalar halinde depolamanıza olanak tanır.

Fetch API ile veri parçalarını okuyabildiğinize göre bunları depolamanız da gerekir. Muhtemelen medya dosyanızla ilişkilendirilmiş ad, açıklama, çalışma zamanı uzunluğu, kategori gibi pek çok meta veri vardır.

Yalnızca bir medya dosyası değil, yapılandırılmış bir nesne depoluyorsunuz. Medya dosyası, bu nesnenin özelliklerinden yalnızca biridir.

Bu durumda IndexedDB API, hem medya verilerini hem de meta verileri depolamak için mükemmel bir çözüm sunar. Büyük miktarda ikili veriyi kolayca saklayabilir ve çok hızlı veri aramaları yapmanızı sağlayan dizinler sunar.

Medya dosyalarını Fetch API'yi kullanarak indirme

Kino adını verdiğimiz demo PWA'mızda Fetch API ile ilgili birkaç ilginç özellik geliştirdik. Kaynak kodu herkese açık olduğundan dilediğiniz zaman inceleyebilirsiniz.

  • Tamamlanmamış indirme işlemlerini duraklatma ve devam ettirme olanağı.
  • Veritabanında veri parçalarını depolamak için özel bir arabellek.

Bu özelliklerin nasıl uygulandığını göstermeden önce, dosyaları indirmek için Fetch API'yi nasıl kullanabileceğinize dair kısa bir özet sunacağız.

/**
 * Downloads a single file.
 *
 * @param {string} url URL of the file to be downloaded.
 */
async function downloadFile(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  do {
    const { done, dataChunk } = await reader.read();
    // Store the `dataChunk` to IndexedDB.
  } while (!done);
}

await reader.read() adlı cihazın döngüye girdiğini fark ettiniz mi? Bu şekilde, okunabilir bir akıştan ağdan gelen veri parçalarını alırsınız. Bunun ne kadar yararlı olduğunu düşünün: Verilerinizin tamamı ağdan gelmeden önce bile işlemeye başlayabilirsiniz.

İndirmeler devam ettiriliyor

Bir indirme duraklatıldığında veya kesintiye uğradığında, indirilen veri parçaları güvenli bir şekilde IndexedDB veritabanında saklanır. Ardından, uygulamanızda indirme işlemini devam ettirmek için bir düğme gösterebilirsiniz. Kino demo PWA sunucusu HTTP aralık isteklerini desteklediğinden, indirme işlemini devam ettirmek oldukça kolaydır:

async downloadFile() {
  // this.currentFileMeta contains data from IndexedDB.
  const { bytesDownloaded, url, downloadUrl } = this.currentFileMeta;
  const fetchOpts = {};

  // If we already have some data downloaded,
  // request everything from that position on.
  if (bytesDownloaded) {
    fetchOpts.headers = {
      Range: `bytes=${bytesDownloaded}-`,
    };
  }

  const response = await fetch(downloadUrl, fetchOpts);
  const reader = response.body.getReader();

  let dataChunk;
  do {
    dataChunk = await reader.read();
    if (!dataChunk.done) this.buffer.add(dataChunk.value);
  } while (!dataChunk.done && !this.paused);
}

IndexedDB için özel yazma arabelleği

dataChunk değerlerini IndexedDB veritabanına yazma işlemi kağıt üzerinde basittir. Bu değerler zaten doğrudan IndexedDB'de depolanabilen ArrayBuffer örnekleridir. Bu nedenle, uygun bir şekle sahip bir nesne oluşturup depolayabiliriz.

const dataItem = {
  url: fileUrl,
  rangeStart: dataStartByte,
  rangeEnd: dataEndByte,
  data: dataChunk,
}

// Name of the store that will hold your data.
const storeName = 'fileChunksStorage'

// `db` is an instance of `IDBDatabase`.
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const putRequest = store.put(data);

putRequest.onsuccess = () => { ... }

Bu yaklaşım işe yarasa da IndexedDB yazma işlemlerinizin, indirme işleminizden önemli ölçüde daha yavaş olduğunu fark edebilirsiniz. Bunun nedeni, IndexedDB yazma işlemlerinin yavaş olması değil, ağdan aldığımız her veri parçası için yeni bir işlem oluşturarak çok fazla işlemsel yükü eklememizdir.

İndirilen parçalar oldukça küçük olabilir ve akış tarafından hızlı bir şekilde yayınlanabilir. IndexedDB yazma hızını sınırlamanız gerekir. Kino demo PWA'sında bunu bir aracı yazma arabelleği uygulayarak yapıyoruz.

Ağdan veri parçaları geldikçe bunları önce arabelleğimize ekleriz. Gelen veriler sığmazsa tamponun tamamını veritabanına gönderip verilerin geri kalanını eklemeden önce tamponu temizleriz. Sonuç olarak, IndexedDB yazma işlemlerinin sıklığı azalır ve yazma performansı önemli ölçüde artar.

Çevrimdışı depolama alanından medya dosyası yayınlama

İndirilen medya dosyasını, hizmet çalışanınızın ağdan almak yerine IndexedDB'den sunmasını istiyorsanız bunu yapabilirsiniz.

/**
 * The main service worker fetch handler.
 *
 * @param {FetchEvent} event Fetch event.
 */
const fetchHandler = async (event) => {
  const getResponse = async () => {
    // Omitted Cache API code used to serve static assets.

    const videoResponse = await getVideoResponse(event);
    if (videoResponse) return videoResponse;

    // Fallback to network.
    return fetch(event.request);
  };
  event.respondWith(getResponse());
};
self.addEventListener('fetch', fetchHandler);

getVideoResponse()'te ne yapmanız gerekiyor?

  • event.respondWith() yöntemi, parametre olarak bir Response nesnesi bekler.

  • Response() kurucusu, Response nesnesi oluşturmak için kullanabileceğimiz çeşitli nesne türleri olduğunu belirtir: Blob, BufferSource, ReadableStream ve daha fazlası.

  • Tüm verilerini bellekte tutmayan bir nesneye ihtiyacımız var. Bu nedenle, muhtemelen ReadableStream seçeneğini tercih ederiz.

Ayrıca, büyük dosyalarla uğraştığımız ve tarayıcıların yalnızca dosyanın şu anda ihtiyaç duydukları bölümünü istemelerine izin vermek istediğimiz için HTTP aralık istekleri için bazı temel destekleri uygulamamız gerekiyordu.

/**
 * Respond to a request to fetch offline video file and construct a response
 * stream.
 *
 * Includes support for `Range` requests.
 *
 * @param {Request} request  Request object.
 * @param {Object}  fileMeta File meta object.
 *
 * @returns {Response} Response object.
 */
const getVideoResponse = (request, fileMeta) => {
  const rangeRequest = request.headers.get('range') || '';
  const byteRanges = rangeRequest.match(/bytes=(?<from>[0-9]+)?-(?<to>[0-9]+)?/);

  // Using the optional chaining here to access properties of
  // possibly nullish objects.
  const rangeFrom = Number(byteRanges?.groups?.from || 0);
  const rangeTo = Number(byteRanges?.groups?.to || fileMeta.bytesTotal - 1);

  // Omitting implementation for brevity.
  const streamSource = {
     pull(controller) {
       // Read file data here and call `controller.enqueue`
       // with every retrieved chunk, then `controller.close`
       // once all data is read.
     }
  }
  const stream = new ReadableStream(streamSource);

  // Make sure to set proper headers when supporting range requests.
  const responseOpts = {
    status: rangeRequest ? 206 : 200,
    statusText: rangeRequest ? 'Partial Content' : 'OK',
    headers: {
      'Accept-Ranges': 'bytes',
      'Content-Length': rangeTo - rangeFrom + 1,
    },
  };
  if (rangeRequest) {
    responseOpts.headers['Content-Range'] = `bytes ${rangeFrom}-${rangeTo}/${fileMeta.bytesTotal}`;
  }
  const response = new Response(stream, responseOpts);
  return response;

IndexedDB'den dosya verilerini nasıl okuduğumuzu ve gerçek bir uygulamada akış oluşturma işlemini nasıl yaptığımızı öğrenmek için Kino demo PWA hizmet çalışanı kaynak koduna göz atabilirsiniz.

Dikkat edilmesi gereken diğer noktalar

Önünüzdeki ana engelleri ortadan kaldırarak artık video uygulamanıza güzel özellikler eklemeye başlayabilirsiniz. Kino demo PWA'sında bulabileceğiniz özelliklere dair birkaç örnek aşağıda verilmiştir:

  • Kullanıcılarınızın özel donanım medya tuşlarını veya medya bildirimi pop-up'larını kullanarak medya oynatmayı kontrol etmesine olanak tanıyan Media Session API entegrasyonu.
  • Eski Cache API'yi kullanarak altyazılar ve poster resimleri gibi medya dosyalarıyla ilişkili diğer öğelerin önbelleğe alınması.
  • Uygulama içinde video akışları (DASH, HLS) indirme desteği. Akış manifestleri genellikle farklı bit hızlarına sahip birden fazla kaynak tanımladığından, manifest dosyasını dönüştürmeniz ve çevrimdışı izleme için depolamadan önce yalnızca bir medya sürümü indirmeniz gerekir.

Sıradaki içerik: Ses ve video ön yüklemeyle hızlı oynatma hakkında bilgi edinin.