Çevrimdışı yayın özellikli PWA

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

Progresif Web Uygulamaları, daha önce yerel uygulamalar için ayrılmış birçok özelliği web'e taşır. PWA'larla ilişkili en belirgin özelliklerden biri çevrimdışı deneyimdir.

Daha da iyisi, kullanıcılarınıza birkaç farklı yolla sunabileceğiniz bir geliştirme olan çevrimdışı medya akışı deneyimi. Ancak bu, gerçekten benzersiz bir sorun oluşturur. Medya dosyaları çok büyük olabilir. Bu nedenle şöyle soruyor olabilirsiniz:

  • Büyük bir video dosyasını nasıl indirir ve saklarım?
  • Bunu kullanıcıya nasıl sunabilirim?

Bu makalede bu soruların yanıtlarını ele alacağız. Ayrıca Kino demo PWA'ya atıfta bulunacağız. Bu demoda, herhangi bir işlevsel veya sunusal çerçeve kullanmadan çevrimdışı medya akışı deneyimini nasıl uygulayabileceğinize dair pratik örnekler sunuluyor. 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.

Kendiniz için iyi bir iş gerekçeniz yoksa çevrimdışı akış içeren bir PWA oluşturmanın zorlukları vardır. Bu makalede, kullanıcılara yüksek kaliteli bir çevrimdışı medya deneyimi sunmak 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ı, çevrimdışı deneyimi sunmak için gerekli olan belgeleri (belgeler, stil sayfaları, resimler vb.) hem indirmek hem de depolamak için genellikle kullanışlı Cache API'yi kullanır.

Bir hizmet çalışanı içinde Cache API'yi kullanmaya dair temel bir örneği burada bulabilirsiniz:

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 yaraysa da Cache API'nin kullanılması, büyük dosyalarla ilgili kullanımı 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 izlemenize olanak tanır
  • HTTP aralığı isteklerine doğru şekilde yanıt verme yöntemi sunun.

Bu sorunların hepsi, herhangi bir video uygulaması için oldukça ciddi sınırlamalardır. Sizin için 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 örneğimizde, büyük video dosyalarına akış olarak erişmenize ve bunları bir HTTP aralığı isteği ile parçalar 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şkilendirilmiş ad, açıklama, çalışma zamanı uzunluğu, kategori gibi pek çok meta veri vardır.

Yalnızca tek bir medya dosyası değil, yapılandırılmış bir nesne de depoluyorsunuz ve medya dosyası, bu dosyanın özelliklerinden yalnızca biri.

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

PWA demomuz üzerinde Fetch API'si ile ilgili birkaç ilginç özellik geliştirdik. Bu özelliği Kino olarak adlandırdık. Kaynak kod herkese açıktır. Kodu inceleyebilirsiniz.

  • Tamamlanmamış indirme işlemlerini 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, ilk olarak dosyaları indirmek için Fetch API'yi nasıl kullanabileceğinize dair 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() adlı cihazın döngüye girdiğini fark ettiniz mi? Okunabilir bir akıştan ağdan gelen veri parçalarını bu şekilde alırsınız. Bunun ne kadar yararlı olduğunu düşünün: Verilerinizi daha tümü ağdan gelmeden işlemeye başlayabilirsiniz.

İndirmeler devam ettiriliyor

Bir indirme 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ızdaki bir 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 biraz 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, IndexedDB veritabanına dataChunk değerleri yazma işlemi basittir. Bu değerler zaten ArrayBuffer örnekleridir ve bunlar doğrudan IndexedDB'de depolanabilir. Bu nedenle, uygun şekilde bir nesne oluşturup bunu 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ğildir. Bunun nedeni, 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 akış tarafından sırayla hızlı bir şekilde yayınlanabilir. IndexedDB yazma hızını sınırlamanız gerekir. Kino demosu PWA'da bunu bir aracı yazma arabelleği uygulayarak yapıyoruz.

Veri parçaları ağdan geldikçe ilk olarak tamponumuza eklenir. Gelen veriler uygun değilse tam arabelleği veritabanına temizler ve kalan verileri eklemeden önce temizleriz. Sonuç olarak IndexedDB yazma işlemlerimiz daha seyrek olabiliyor ve bu da yazma performansında önemli bir artış sağlıyor.

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

Bir medya dosyası indirildikten 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);

Peki getVideoResponse() bölgesinde ne yapmanız gerekiyor?

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

  • Response() oluşturucusu, bir Response nesnesini örneklendirmek için kullanabileceğimiz birkaç nesne türü olduğunu bize bildirir: Blob, BufferSource, ReadableStream vb.

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

Ayrıca, büyük dosyalarla işlem yaptığımız ve tarayıcıların dosyanın şu anda yalnızca ihtiyaç duydukları bölümünü istemesine izin vermek istediğimizden, HTTP aralığı istekleri için bazı temel desteği 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'deki 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 koduna göz atabilirsiniz.

Dikkat edilmesi gereken diğer noktalar

Önünüzdeki başlıca engelleri ortadan kaldırarak artık video uygulamanıza güzel özellikler eklemeye başlayabilirsiniz. Kino demosu PWA'da bulabileceğiniz bazı özellikler şunlardır:

  • Kullanıcılarınızın, özel donanım medya anahtarları veya medya bildirim pop-up'larını kullanarak medya oynatmayı kontrol etmesine olanak tanıyan Media Session API entegrasyonu.
  • Eski iyi 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ışı (DASH, HLS) indirme desteği. 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 saklamadan önce yalnızca bir medya sürümünü indirmeniz gerekir.

Sıradaki videoda Ses ve video önceden yükleyerek hızlı oynatma hakkında bilgi edineceksiniz.