오프라인 스트리밍 기능이 있는 PWA

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

프로그레시브 웹 앱은 기존에 네이티브 전용 앱에 사용했던 많은 기능을 제공합니다. 배포할 수 있습니다 Kubernetes와 관련하여 가장 눈에 띄는 기능 중 하나는 PWA는 오프라인 환경입니다.

오프라인 스트리밍 미디어 경험은 더욱 좋겠죠. 사용자에게 제공할 수 있는 개선사항을 몇 가지 방법으로 소개합니다. 하지만 이는 정말 고유한 문제를 야기합니다. 미디어 파일은 매우 클 수 있습니다. 그래서 다음과 같은 질문을 할 수 있습니다.

  • 대용량 동영상 파일을 다운로드하고 저장하려면 어떻게 해야 하나요?
  • 사용자에게 제공하려면 어떻게 해야 하나요?

이 도움말에서는 이러한 질문에 대한 답변에 대해 논의하며 Google에서 빌드한 Kino 데모 PWA를 참조하여 오프라인 스트리밍 미디어 환경을 구현하는 방법의 예 모델을 학습시키는 작업도 반복해야 합니다 다음 예는 주로 교육 목적으로 사용됩니다. 대부분의 경우 기존 미디어 프레임워크 중 하나를 사용하여 이러한 기능을 제공해야 합니다.

자체 개발, PWA 빌드에 관한 좋은 비즈니스 사례가 없는 경우 오프라인 스트리밍에는 어려움이 있습니다. 이 도움말에서는 다음 내용에 대해 알아봅니다. 사용자에게 고품질 오프라인 미디어를 제공하는 데 사용되는 API와 기법 경험해 볼 수 있습니다

대용량 미디어 파일 다운로드 및 저장

프로그레시브 웹 앱은 일반적으로 편리한 Cache API를 사용하여 오프라인 환경을 제공하는 데 필요한 애셋(문서, 이미지 등을 사용할 수 있습니다.

다음은 서비스 워커 내에서 Cache API를 사용하는 기본적인 예입니다.

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',
      ]);
    })
  );
});

위의 예는 기술적으로 작동하지만 Cache API를 사용하면 몇 가지 사항이 있습니다. 한계가 있습니다.

예를 들어 Cache API는 다음 작업을 실행하지 않습니다.

  • 다운로드를 쉽게 일시중지하고 재개할 수 있습니다.
  • 다운로드 진행 상황을 추적할 수 있음
  • HTTP 범위 요청에 올바르게 응답하는 방법을 제공합니다.

이러한 모든 문제는 모든 동영상 애플리케이션에 매우 심각한 제한사항입니다. 더 적합할 수 있는 다른 옵션을 검토해 보겠습니다.

요즘은 Fetch API는 브라우저 간 원격 액세스를 비동기식으로 할 수 있습니다. 이 사용 사례에서는 대용량 동영상 파일에 스트림으로 액세스하고 HTTP 범위 요청을 사용하여 점진적으로 청크로 저장합니다.

이제 Fetch API를 사용하여 데이터 청크를 읽을 수 있으므로 저장할 수 있습니다 내 미디어와 연결된 메타데이터가 많이 있을 확률이 높습니다. 파일(예: 이름, 설명, 런타임 길이, 카테고리 등)

하나의 미디어 파일만 저장하는 것이 아니라 구조화된 객체를 저장하는 것입니다. 미디어 파일은 그 속성 중 하나일 뿐입니다.

이 경우 IndexedDB API는 미디어 데이터 및 메타데이터입니다. 방대한 양의 바이너리 데이터를 쉽게 담을 수 있고 는 매우 빠른 데이터 조회를 수행할 수 있는 색인을 제공합니다.

Fetch API를 사용하여 미디어 파일 다운로드

데모 PWA의 Fetch API를 중심으로 몇 가지 흥미로운 기능을 빌드했습니다. 이 버전의 이름은 Kino입니다. 소스 코드는 공개되어 있으므로 자유롭게 검토하실 수 있습니다.

  • 완료되지 않은 다운로드를 일시중지했다가 재개하는 기능
  • 데이터베이스에 데이터 청크를 저장하기 위한 커스텀 버퍼.

이러한 기능을 구현하는 방법을 살펴보기 전에 먼저 Fetch API를 사용하여 파일을 다운로드하는 방법을 간략히 요약

/**
 * 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()가 루프에 있는 것을 알 수 있나요? 이 방법으로 청크를 수신할 수 있습니다. 읽을 수 있는 스트림으로부터 입수해야 할 수 있습니다. 어떻게 데이터가 모두 도착하기 전에 처리를 시작할 수 있다는 점이 유용합니다. 네트워크로부터 전달됩니다.

다운로드 재개

다운로드가 일시중지되거나 중단되면 도착한 데이터 청크는 IndexedDB 데이터베이스에 안전하게 저장해야 합니다. 그런 다음 다운로드를 재개할 수 있습니다 Kino 데모 PWA 서버 HTTP 범위 요청을 지원하는 방법은 다소 간단합니다.

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용 커스텀 쓰기 버퍼

종이에 dataChunk 값을 IndexedDB 데이터베이스에 쓰는 프로세스 간단합니다. 해당 값은 이미 저장 가능한 인스턴스 ArrayBuffer개입니다. IndexedDB에 직접 저장되므로 적절한 형태의 객체를 만들면 됩니다. 저장합니다.

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 = () => { ... }

이 접근 방식이 효과가 있지만 IndexedDB가 다운로드보다 훨씬 느립니다. 이는 IndexedDB가 속도가 느리기 때문입니다. 새 트랜잭션을 생성하여 트랜잭션 오버헤드가 새로운 트랜잭션이 생성됩니다.

다운로드한 청크는 다소 작을 수 있으며 급격히 빠르게 이어지고 있습니다. IndexedDB 쓰기 속도를 제한해야 합니다. Kino 데모 PWA에서는 중개 쓰기 버퍼를 구현하여 이를 수행합니다.

데이터 청크가 네트워크에서 도착하면 먼저 버퍼에 추가합니다. 만약 들어오는 데이터가 맞지 않을 경우 전체 버퍼를 데이터베이스로 플러시하고 데이터를 지울 수 있습니다. 그 결과 IndexedDB는 쓰기 빈도가 줄어 쓰기가 크게 향상됩니다. 확인할 수 있습니다

오프라인 저장소에서 미디어 파일 제공

미디어 파일을 다운로드했으면 서비스 워커가 네트워크에서 파일을 가져오는 대신 IndexedDB에서 제공합니다.

/**
 * 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()에서 무엇을 해야 할까요?

  • event.respondWith() 메서드는 Response 객체를 매개변수로 예상합니다.

  • Response() 생성자를 통해 반환되는 객체에는 여러 가지가 있음을 알 수 있습니다. Response 객체(Blob, BufferSource, ReadableStream

  • 메모리에 모든 데이터를 보관하지 않는 객체가 필요하므로 ReadableStream를 선택하는 것이 좋습니다.

또한 대용량 파일을 다루기 때문에 브라우저에서 현재 필요한 파일의 부분만 요청하는 대신 HTTP 범위 요청에 대한 몇 가지 기본 지원

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

Kino 데모 PWA 서비스 워커 소스 코드를 확인하여 IndexedDB에서 파일 데이터를 읽고 가상 머신을 만드는 것입니다.

기타 고려사항

주요 장애물을 해결했다면 이제 기능을 동영상 애플리케이션에 추가하는 것입니다. 몇 가지 예시를 통해 Kino 데모 PWA에 있는 기능입니다.

  • 사용자가 미디어를 제어할 수 있는 Media Session API 통합 전용 하드웨어 미디어 키를 사용하여 또는 미디어 알림에서 재생 팝업이 표시됩니다.
  • 자막 등 미디어 파일과 연결된 기타 저작물 캐싱 포스터 이미지를 생성할 수 있습니다.
  • 앱 내에서 동영상 스트림 (DASH, HLS) 다운로드를 지원합니다. 스트림 매니페스트는 일반적으로 다양한 비트 전송률의 여러 소스를 선언하므로 매니페스트 파일을 변환하고 하나의 미디어 버전만 다운로드한 후 오프라인 보기

다음에는 오디오 및 동영상 미리 로드를 통한 빠른 재생에 관해 알아보겠습니다.