Çevrimdışı yayın özellikli PWA

Deren Herman
Derek Herman
Jaroslav Polakoviç
Jaroslav Polakovič

Progresif Web Uygulamaları, daha önce web'deki yerel uygulamalar için ayrılmış birçok özellik getiriyor. PWA'larla ilişkili en belirgin özelliklerden biri çevrimdışı deneyimdir.

Çevrimdışı medya akışı deneyimi daha da iyi olabilir. Bu, kullanıcılarınıza birkaç farklı yolla sunabileceğiniz bir geliştirmedir. Ancak bu durum gerçekten benzersiz bir sorun oluşturur; medya dosyaları çok büyük olabilir. Şöyle soruyor olabilirsiniz:

  • Büyük bir video dosyasını nasıl indirip depolayabilirim?
  • Ve bunu kullanıcıya nasıl sunabilirim?

Bu makalede bu soruların yanıtlarını ele alacağız. Ayrıca, herhangi bir işlevsel veya sunumsel çerçeve kullanmadan çevrimdışı medya akışı deneyimini nasıl uygulayabileceğinize dair pratik örnekler sağlayan, geliştirdiğimiz Kino demo PWA'ya atıfta bulunacağız. Aşağıdaki örnekler çoğunlukla eğitim amaçlıdır çünkü çoğu durumda bu özellikleri sağlamak için muhtemelen mevcut Medya Çerçevelerinden birini kullanmanız gerekir.

Kendi iş senaryonuzu geliştirmek için iyi bir gerekçeniz yoksa çevrimdışı akışlı bir PWA geliştirmenin bazı zorlukları vardır. Bu makalede, kullanıcılara yüksek kaliteli bir çevrimdışı medya deneyimi sağlamak için kullanılan API'ler ve teknikler hakkında bilgi edineceksiniz.

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

Progresif Web Uygulamaları (PWA), genellikle çevrimdışı deneyimi sağlamak için gereken öğeleri (dokümanlar, stil sayfaları, resimler vb.) hem indirmek hem de depolamak amacıyla kullanışlı Cache API'sini kullanır.

Aşağıda, Cache API'nin bir Hizmet Çalışanı içinde kullanımına ilişkin 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 yarıyor olsa da, Cache API'nin büyük dosyalarla kullanımını zorlaştıran bazı sınırlamaları vardır.

Örneğin, Cache API şunları yapmaz:

  • İndirme işlemlerini kolayca duraklatmanıza ve devam ettirmenize olanak tanır
  • İndirmelerin ilerleme durumunu takip etmenize olanak tanır
  • HTTP aralığı isteklerine düzgün şekilde yanıt vermenin yolunu sunma

Bu sorunların hepsi, video uygulamaları için oldukça ciddi kısıtlamalardı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 yol sunuyor. Kullanım örneğimizde, büyük video dosyalarına akış olarak erişmenize ve bunları bir HTTP aralığı isteği kullanarak parça halinde aşamalı olarak depolamanıza olanak tanır.

Artık Getirme API'si ile veri parçalarını okuyabildiğinize göre bunları da depolamanız gerekir. Muhtemelen medya dosyanızla ilişkili ad, açıklama, çalışma zamanı uzunluğu, kategori gibi bir dizi meta veri bulunmaktadır.

Yalnızca tek bir medya dosyasını değil, yapılandırılmış bir nesneyi depolarsınız ve medya dosyası bunun özelliklerinden yalnızca biridir.

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

Getirme API'sini kullanarak medya dosyalarını indirme

Kino adını verdiğimiz demo PWA'daki Fetch API'si çerçevesinde bazı ilginç özellikler geliştirdik. Kaynak kod herkese açıktır, bu nedenle kodu inceleyebilirsiniz.

  • Tamamlanmamış indirmeleri duraklatma ve devam ettirme olanağı.
  • Veri parçalarını veritabanında depolamak için özel bir arabellek.

Bu özelliklerin nasıl uygulandığını göstermeden önce, dosya indirmek için Fetch API'yi nasıl kullanabileceğinizin özetini kısaca göstereceğiz.

/**
 * 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() öğesinin döngüde olduğunu fark ettiniz mi? Bu şekilde, ağdan gelen okunabilir bir akıştan veri parçaları alırsınız. Bunun ne kadar yararlı olduğunu düşünün: Tüm veriler ağdan ulaşmadan verilerinizi işlemeye başlayabilirsiniz.

İndirme işlemleri devam ettiriliyor

Bir indirme işlemi duraklatıldığında veya kesintiye uğratıldığında, gelen veri parçaları bir IndexedDB veritabanında güvenli bir şekilde saklanır. Daha sonra, uygulamanızda indirme işlemini devam ettirmek için bir düğme görüntüleyebilirsiniz. Kino demo PWA sunucusu, HTTP aralığı isteklerini desteklediğinden indirme işlemini devam ettirmek oldukça basittir:

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

Kağıt üzerinde, dataChunk değerlerini IndexedDB veritabanına yazma işlemi basittir. Bu değerler zaten doğrudan IndexedDB'de depolanabilen ArrayBuffer örnekleridir. Bu yüzden, uygun şekilde 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şleminden çok daha yavaş olduğunu muhtemelen keşfedeceksiniz. Bunun nedeni IndexedDB yazmalarının yavaş olması değil, bir ağdan aldığımız her veri parçası için yeni bir işlem oluşturarak çok sayıda işlem ek yükü eklememizdir.

İndirilen parçalar oldukça küçük olabilir ve arka arkaya akış tarafından yayınlanabilir. IndexedDB yazma hızını sınırlandırmanız gerekir. Kino demo PWA'da bu işlemi aracı yazma arabelleği uygulayarak yaparız.

Veri parçaları ağdan geldiğinde, ilk olarak bunları arabelleğimize ekleriz. Gelen veriler sığmazsa tam arabelleği veritabanına boşaltır ve kalan verileri eklemeden önce temizleriz. Sonuç olarak IndexedDB yazma işlemlerimiz daha az sıklıkta gerçekleşerek yazma performansı önemli ölçüde iyileşir.

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

Bir medya dosyası indirdikten sonra muhtemelen 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);

getVideoResponse() ürününde ne yapmanız gerekiyor?

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

  • Response() oluşturucu, bir Response nesnesini örneklendirmek için kullanabileceğimiz çeşitli nesne türleri olduğunu bize bildirir: Blob, BufferSource, ReadableStream ve daha fazlası.

  • Tüm verilerini bellekte barındırmayan bir nesneye ihtiyacımız vardır. Bu nedenle, muhtemelen ReadableStream öğesini seçmek isteriz.

Ayrıca, büyük dosyalarla uğraştığımız ve tarayıcıların, dosyanın yalnızca şu anda ihtiyaç duydukları bölümünü istemelerine olanak tanımak istediğimizden, HTTP aralığı istekleri için bazı temel destek sağlamamı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'deki dosya verilerini nasıl okuduğumuzu ve gerçek bir uygulamada nasıl akış oluşturduğumuzu öğrenmek için Kino demo PWA hizmet çalışanı kaynak koduna göz atabilirsiniz.

Dikkat edilmesi gereken diğer noktalar

Önünüze çıkan ana engellerden kurtulduktan sonra, artık video uygulamanıza bulunması güzel bazı özellikler eklemeye başlayabilirsiniz. Kino demo PWA'da bulabileceğiniz özelliklere birkaç örnek:

  • Media Session API entegrasyonu, kullanıcılarınızın özel donanım medya anahtarları kullanarak veya medya bildirimi pop-up'larından medya oynatmayı kontrol etmesine olanak tanır.
  • Altyazılar ve poster resimleri gibi medya dosyalarıyla ilişkili diğer öğelerin, eski ve iyi bir Cache API kullanılarak önbelleğe alınması.
  • Uygulama içinden video akışı (DASH, HLS) indirme desteği. Akış manifestleri genellikle birden fazla farklı bit hızı kaynağı 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ü indirmeniz gerekir.

Şimdi ise Ses ve video önceden yükleme ile hızlı oynatma hakkında bilgi edineceksiniz.