Çevrimdışı yayın özellikli PWA

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

Yayınlanma tarihi: 5 Temmuz 2021

Progresif web uygulamaları, daha önce yerel uygulamalara ayrılmış 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ışı akışlı medya deneyimidir. Ancak bu durum gerçekten benzersiz bir sorun yaratır: Medya dosyaları çok büyük olabilir. Bu nedenle,

  • Büyük boyutlu video dosyalarını nasıl indirip saklayabilirim?
  • Ve bunu kullanıcıya nasıl sunarım?

Bu makalede, işlevsel veya sunumla ilgili çerçeveler kullanmadan çevrimdışı akışlı medya deneyimi sunma konusunda pratik örnekler veren Kino adlı demo PWA'mızdan bahsederek bu soruların yanıtlarını ele alacağız. Aşağıdaki örnekler, bu özellikleri sağlamak için çoğu durumda mevcut Medya Çerçeveleri'nden birini kullanmanız gerektiğinden temel olarak eğitim amaçlıdır.

Kendi PWA'nızı geliştirmeniz için iyi bir iş gerekçeniz yoksa çevrimdışı akış özelliği olan bir PWA oluşturmak zor olabilir. Bu makalede, kullanıcılara yüksek kaliteli bir çevrimdışı medya deneyimi sunmak için kullanılan API'ler ve teknikler hakkında bilgi verilmektedir.

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

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

Aşağıda, bir hizmet çalışanı içinde Cache API'yi kullanmayla ilgili 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 çalışsa da Cache API'nin büyük dosyalarla kullanımını pratik hale getirmeyen çeşitli sınırlamaları vardır.

Örneğin, Cache API:

  • İndirme işlemlerini kolayca duraklatıp devam ettirmenize olanak tanır.
  • İndirme işlemlerinin ilerleme durumunu takip etmenizi sağlar.
  • HTTP aralık isteklerine uygun şekilde yanıt verme olanağı sunma

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

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

Artık Fetch API ile veri parçalarını okuyabildiğinize göre bunları saklamanız da gerekir. Medya dosyanızla ilişkili bir dizi meta veri vardır. Örneğin: ad, açıklama, çalışma süresi, kategori vb.

Yalnızca tek bir medya dosyası depolamıyorsunuz, yapılandırılmış bir nesne depoluyorsunuz ve medya dosyası yalnızca bu nesnenin özelliklerinden biri.

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

Fetch API'yi kullanarak medya dosyalarını 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 inceleyebilirsiniz.

  • Tamamlanmamış indirme işlemlerini duraklatıp devam ettirebilme
  • Veri parçalarını veritabanında depolamak için özel bir arabellek.

Bu özelliklerin nasıl uygulandığını göstermeden önce, Fetch API'yi kullanarak dosyaları nasıl indirebileceğinizle ilgili kısa bir özet yapacağı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() simgesinin döngü içinde olduğunu fark ettiniz mi? Bu şekilde, ağdan geldikçe okunabilir bir akıştan veri parçaları alırsınız. Bu özelliğin ne kadar faydalı olduğunu düşünün: Verilerinizin tamamı ağdan gelmeden bile işlemeye başlayabilirsiniz.

İndirmelere devam etme

İndirme işlemi duraklatıldığında veya kesintiye uğradığında, gelen veri parçaları IndexedDB veritabanında güvenli bir şekilde saklanır. Ardından, uygulamanızda indirmeye devam etme düğmesi gösterebilirsiniz. Kino demo PWA sunucusu HTTP aralığı isteklerini desteklediğinden indirmeye devam etmek 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 bir IndexedDB veritabanına yazma süreci teoride basittir. Bu değerler zaten IndexedDB'de doğrudan depolanabilen ArrayBuffer örnekleridir. Bu nedenle, uygun ş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şlemlerinize kıyasla çok daha yavaş olduğunu fark edebilirsiniz. Bu durum, IndexedDB yazma işlemlerinin yavaş olmasından değil, bir ağdan aldığımız her veri parçası için yeni bir işlem oluşturarak çok fazla işlem yükü eklememizden kaynaklanmaktadır.

İ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 aracı yazma arabelleği uygulayarak yapıyoruz.

Ağdan gelen veri parçalarını önce arabelleğimize ekleriz. Gelen veriler sığmazsa arabelleğin tamamını veritabanına boşaltırız ve verilerin geri kalanını eklemeden önce arabelleği temizleriz. Sonuç olarak, IndexedDB yazma işlemlerimiz daha az sıklıkta gerçekleşiyor ve bu da yazma performansında önemli bir iyileşmeye yol açıyor.

Çevrimdışı depolama alanından medya dosyası sunma

İndirilmiş bir medya dosyanız olduğunda, hizmet çalışanınızın dosyayı ağdan getirmek yerine IndexedDB'den sunmasını istersiniz.

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

Peki getVideoResponse() içinde ne yapmanız gerekiyor?

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

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

  • Tüm verilerini bellekte tutmayan bir nesneye ihtiyacımız var. Bu nedenle, büyük olasılıkla ReadableStream öğesini seçmek isteyeceğiz.

Ayrıca, büyük dosyalarla çalıştığımız ve tarayıcıların yalnızca o anda ihtiyaç duydukları dosya bölümünü istemesine izin vermek istediğimiz için HTTP aralık istekleri için temel destek 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 nasıl akış oluşturduğumuzu öğrenmek için Kino demo PWA service worker kaynak kodunu inceleyebilirsiniz.

Dikkat edilmesi gereken diğer noktalar

Artık temel engelleri aştığınıza göre video uygulamanıza bazı faydalı özellikler eklemeye başlayabilirsiniz. Kino demo PWA'sında bulabileceğiniz özelliklere birkaç örnek:

  • 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.
  • Altyazılar ve poster resimleri gibi medya dosyalarıyla ilişkili diğer öğelerin, eski Cache API kullanılarak önbelleğe alınması.
  • Uygulamada video akışlarının (DASH, HLS) indirilmesi desteklenir. Akış manifestleri genellikle farklı bit hızlarına sahip birden fazla kaynak bildirdiğinden, manifest dosyasını dönüştürmeniz ve çevrimdışı görüntüleme için depolamadan önce yalnızca bir medya sürümünü indirmeniz gerekir.

Sıradaki içerikte Ses ve video önceden yükleme ile hızlı oynatma hakkında bilgi edineceksiniz.