Szybkie odtwarzanie z wstępnym wczytywaniem dźwięku i obrazu

Jak przyspieszyć odtwarzanie multimediów przez aktywne wstępne wczytywanie zasobów.

François Beaufort
François Beaufort

Szybsze rozpoczynanie odtwarzania oznacza, że więcej osób ogląda Twój film lub słucha audio. To znany fakt. W tym artykule omówię Za pomocą tych technik możesz przyspieszyć odtwarzanie dźwięku i wideo, aktywnie wstępne ładowanie zasobów w zależności od przypadku użycia.

Autorzy: Copyright Blender Foundation | www.blender.org

Omówię 3 metody wstępnego wczytywania plików multimedialnych, zaczynając od profesjonalistów i wady.

Wspaniale... Ale...
Atrybut wstępnego wczytywania filmu Prosta w użyciu w przypadku unikalnego pliku hostowanego na serwerze WWW. Przeglądarki mogą całkowicie zignorować ten atrybut.
Pobieranie zasobów rozpoczyna się po całkowitym wczytaniu dokumentu HTML przeanalizowano.
Rozszerzenia Media Source Extensions (MSE) ignorują atrybut preload w elementach multimedialnych, ponieważ to aplikacja jest odpowiedzialna za dostarczania multimediów do MSE.
Wstępne wczytywanie linków Wymusza na przeglądarce wysłanie żądania dotyczącego zasobu wideo bez blokowania zdarzenie onload dokumentu. Żądania zakresu HTTP są niezgodne.
Zgodny z MSE i segmentami plików. Powinna być używana tylko w przypadku małych plików multimedialnych (poniżej 5 MB) podczas pobierania pełnych zasobów.
Buforowanie ręczne Pełna kontrola Za obsługę złożonych błędów odpowiada właściciel witryny.

Atrybut wstępnego wczytywania filmu

Jeśli źródłem wideo jest unikalny plik przechowywany na serwerze WWW, warto użyj atrybutu wideo preload, aby przekazać przeglądarce wskazówkę, jak to zrobić. dużo informacji lub treści do wstępnego wczytania. Oznacza to, że rozszerzenia źródeł multimediów (MSE) jest niezgodny z tabelą preload.

Pobieranie zasobów rozpocznie się dopiero wtedy, gdy początkowy dokument HTML zostanie w pełni wczytane i przeanalizowane (np. uruchomienie zdarzenia DOMContentLoaded) podczas gdy bardzo różne zdarzenie load będzie wywoływane, gdy zasób został pobrany.

Jeśli atrybut preload ma wartość metadata, oznacza to, że użytkownik nie powinien potrzebować wideo, ale pobieranie jego metadanych (wymiary, śledzenie lista, czas trwania itd.) jest pożądane. Pamiętaj, że od Chrome 64, wartość domyślna preload to metadata. (Było auto wcześniej).

<video id="video" preload="metadata" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

Ustawienie atrybutu preload na auto oznacza, że przeglądarka może buforować dane. dostatecznie dużo danych, by odtwarzanie całkowite było możliwe bez konieczności zatrzymywania dalsze buforowanie.

<video id="video" preload="auto" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

Jest jednak kilka zastrzeżeń. To tylko wskazówka, że przeglądarka może całkowicie zignoruj atrybut preload. W momencie pisania tych zasad możemy zapoznać się z tymi zasadami zastosowane w Chrome:

  • Gdy włączone jest Oszczędzanie danych, Chrome wymusza wartość preload na none
  • W Androidzie 4.3 Chrome wymusza wartość preload na none ze względu na Androida Błąd.
  • W przypadku połączenia komórkowego (2G, 3G i 4G) Chrome wymusza wartość preload na metadata

Wskazówki

Jeśli witryna zawiera wiele zasobów wideo w tej samej domenie, zalecamy ustawienie wartości preload na metadata lub zdefiniowanie poster i ustaw preload na none. Dzięki temu unikniesz klikania maksymalną liczbę połączeń HTTP z tą samą domeną (6 zgodnie z specyfikacji HTTP 1.1), które mogą powodować opóźnienia ładowania zasobów. Pamiętaj, że może to też popraw szybkość stron, jeśli filmy nie są główną częścią usługi.

Jak omówiliśmy w innych artykułach, wstępne wczytywanie linków to deklaratywne pobieranie, pozwala wymusić na przeglądarce wysłanie żądania do zasobu bez blokowanie zdarzenia load i podczas pobierania strony. Zasoby wczytywane przez <link rel="preload"> są przechowywane lokalnie w przeglądarce i są działają bezwzględnie, dopóki nie zostaną wyraźnie przywoływane w DOM, JavaScript lub CSS.

Wstępne wczytywanie różni się od pobierania z wyprzedzeniem, ponieważ koncentruje się na bieżącej nawigacji pobiera zasoby o priorytecie na podstawie ich typu (skrypt, styl, czcionka wideo, audio itp.). Powinien być używany do ogrzania pamięci podręcznej przeglądarki na potrzeby bieżącego sesji.

Wstępnie wczytuj cały film

Oto jak wczytać wstępnie pełny film na stronie, JavaScript prosi o pobranie treści wideo. Jest on odczytywany z pamięci podręcznej jako zasób mogły już zostać zapisane w pamięci podręcznej przeglądarki. Jeśli żądanie wstępnego wczytywania nie zostało nie zostało zakończone, odbywa się zwykłe pobieranie sieciowe.

<link rel="preload" as="video" href="https://cdn.com/small-file.mp4">

<video id="video" controls></video>

<script>
  // Later on, after some condition has been met, set video source to the
  // preloaded video URL.
  video.src = 'https://cdn.com/small-file.mp4';
  video.play().then(() => {
    // If preloaded video URL was already cached, playback started immediately.
  });
</script>

Wstępnie załadowany zasób zostanie wykorzystany przez element wideo w W tym przykładzie wartość linku wstępnego wczytywania as to video. Jeśli to dźwięk element, będzie to as="audio".

Wstępnie wczytuj pierwszy segment

Poniższy przykład pokazuje, jak wstępnie wczytać pierwszy segment filmu za pomocą parametru <link rel="preload"> i używać go z rozszerzeniami źródła multimediów. Jeśli nie znasz usługi, przy użyciu interfejsu MSE JavaScript API, zapoznaj się z podstawowymi informacjami o MSE.

Dla uproszczenia załóżmy, że cały film został podzielony na mniejsze pliki, takie jak file_1.webm, file_2.webm, file_3.webm itp.

<link rel="preload" as="fetch" href="https://cdn.com/file_1.webm">

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // If video is preloaded already, fetch will return immediately a response
    // from the browser cache (memory cache). Otherwise, it will perform a
    // regular network fetch.
    fetch('https://cdn.com/file_1.webm')
    .then(response => response.arrayBuffer())
    .then(data => {
      // Append the data into the new sourceBuffer.
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch file_2.webm when user starts playing video.
    })
    .catch(error => {
      // TODO: Show "Video is not available" message to user.
    });
  }
</script>

Pomoc

Możesz wykryć obsługę różnych typów as w witrynie <link rel=preload> za pomocą poniżej:

function preloadFullVideoSupported() {
  const link = document.createElement('link');
  link.as = 'video';
  return (link.as === 'video');
}

function preloadFirstSegmentSupported() {
  const link = document.createElement('link');
  link.as = 'fetch';
  return (link.as === 'fetch');
}

Buforowanie ręczne

Zanim przejdziemy do omówienia interfejsu Cache API i mechanizmów Service Worker jak ręcznie buforować film za pomocą MSE. W przykładzie poniżej zakładamy, że Twoja witryna serwer obsługuje HTTP Range ale działa to podobnie w przypadku plików. segmentów niestandardowych. Pamiętaj, że niektóre biblioteki oprogramowania pośredniczącego, takie jak Google Shaka Player, JW Player i Video.js stworzonych po to, aby sprostać tym potrzebom.

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // Fetch beginning of the video by setting the Range HTTP request header.
    fetch('file.webm', { headers: { range: 'bytes=0-567139' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      sourceBuffer.appendBuffer(data);
      sourceBuffer.addEventListener('updateend', updateEnd, { once: true });
    });
  }

  function updateEnd() {
    // Video is now ready to play!
    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);

    // Fetch the next segment of video when user starts playing the video.
    video.addEventListener('playing', fetchNextSegment, { once: true });
  }

  function fetchNextSegment() {
    fetch('file.webm', { headers: { range: 'bytes=567140-1196488' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      const sourceBuffer = mediaSource.sourceBuffers[0];
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch further segment and append it.
    });
  }
</script>

Uwagi

Masz teraz kontrolę nad całym procesem buforowania multimediów, więc zalecamy sprawdzić poziom baterii w urządzeniu, tryb oszczędzania danych preferencji użytkownika informacje o sieci podczas wstępnego wczytywania.

Znajomość baterii

Weź pod uwagę poziom naładowania baterii urządzeń, zanim pomyślą na temat wstępnego wczytywania filmu. Pozwoli to wydłużyć czas pracy na baterii, gdy poziom naładowania baterii jest niski.

Wyłącz wstępne wczytywanie lub przynajmniej wczytuj filmy w niższej rozdzielczości, gdy w urządzeniu kończy się bateria.

if ('getBattery' in navigator) {
  navigator.getBattery()
  .then(battery => {
    // If battery is charging or battery level is high enough
    if (battery.charging || battery.level > 0.15) {
      // TODO: Preload the first segment of a video.
    }
  });
}

Wykrywanie funkcji „Oszczędzanie danych”

Użyj nagłówka żądania wskazówek dla klienta Save-Data, aby przyspieszyć i łatwiej realizować działania użytkownikom, którzy wyrazili zgodę na „oszczędność danych” w swoim trybie przeglądarki. Dzięki zidentyfikowaniu tego nagłówka żądania aplikacja może dostosowywać i są zoptymalizowane pod kątem wrażeń użytkowników z ograniczonym kosztem i wydajnością użytkowników.

Więcej informacji znajdziesz w artykule o przesyłaniu szybkich i lekkich aplikacji z opcją oszczędzania danych.

Inteligentne ładowanie na podstawie informacji o sieci

Warto sprawdzić navigator.connection.type przed rozpoczęciem wstępnego wczytywania. Kiedy ma wartość cellular, możesz zapobiec wstępnemu ładowaniu i zalecić użytkownikom jego operator sieci komórkowej może pobierać opłaty za przepustowość i dopiero wtedy automatycznego odtwarzania treści zapisanych w pamięci podręcznej.

if ('connection' in navigator) {
  if (navigator.connection.type == 'cellular') {
    // TODO: Prompt user before preloading video
  } else {
    // TODO: Preload the first segment of a video.
  }
}

Zapoznaj się z przykładem informacji o sieci, aby dowiedzieć się, jak zareagować na sieć jego zmiany.

Wstępnie zapisuj wiele pierwszych segmentów w pamięci podręcznej

A co, jeśli chcę spekulować wcześniej ładować treści multimedialne bez wiedząc, na jaki media użytkownik ostatecznie wybierze? Jeśli użytkownik korzysta z która zawiera 10 filmów, prawdopodobnie mamy wystarczająco dużo pamięci, aby pobrać jeden segmentacji z każdego pliku, ale na pewno nie powinniśmy tworzyć 10 ukrytych <video> oraz 10 obiektów MediaSource i zacznij dodawać te dane.

W dwuczęściowym przykładzie poniżej pokazujemy, jak wstępnie zapisać wiele pierwszych segmentów w pamięci podręcznej zaawansowany i łatwy w obsłudze interfejs Cache API. Zauważ, że coś podobnego którą można osiągnąć za pomocą IndexedDB. Nie używamy jeszcze mechanizmów Service Worker, interfejs Cache API jest dostępny również z poziomu obiektu window.

Pobierz i buforuj

const videoFileUrls = [
  'bat_video_file_1.webm',
  'cow_video_file_1.webm',
  'dog_video_file_1.webm',
  'fox_video_file_1.webm',
];

// Let's create a video pre-cache and store all first segments of videos inside.
window.caches.open('video-pre-cache')
.then(cache => Promise.all(videoFileUrls.map(videoFileUrl => fetchAndCache(videoFileUrl, cache))));

function fetchAndCache(videoFileUrl, cache) {
  // Check first if video is in the cache.
  return cache.match(videoFileUrl)
  .then(cacheResponse => {
    // Let's return cached response if video is already in the cache.
    if (cacheResponse) {
      return cacheResponse;
    }
    // Otherwise, fetch the video from the network.
    return fetch(videoFileUrl)
    .then(networkResponse => {
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, networkResponse.clone());
      return networkResponse;
    });
  });
}

Gdybym używać żądań HTTP Range, trzeba było ręcznie utworzyć ponownie obiekt Response, ponieważ interfejs Cache API nie obsługuje jeszcze odpowiedzi Range. Bądź pamiętaj, że wywołanie metody networkResponse.arrayBuffer() powoduje pobranie całej treści odpowiedzi możesz wysłać do pamięci mechanizmu renderowania, dlatego warto użyć funkcji małe zakresy.

Część powyższego przykładu została zmodyfikowana, aby zapisać zakres HTTP. żądania do pamięci podręcznej filmu.

    ...
    return fetch(videoFileUrl, { headers: { range: 'bytes=0-567139' } })
    .then(networkResponse => networkResponse.arrayBuffer())
    .then(data => {
      const response = new Response(data);
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, response.clone());
      return response;
    });

Odtwórz film

Gdy użytkownik kliknie przycisk odtwarzania, zostanie pobrany pierwszy segment filmu. dostępne w interfejsie Cache API, dzięki czemu odtwarzanie rozpocznie się natychmiast, jeśli będzie to możliwe. W przeciwnym razie po prostu pobierzemy go z sieci. Pamiętaj, że przeglądarki a użytkownicy mogą wyczyścić pamięć podręczną.

Jak widzieliśmy wcześniej, używamy MSE, by przekazać ten pierwszy segment filmu do filmu. .

function onPlayButtonClick(videoFileUrl) {
  video.load(); // Used to be able to play video later.

  window.caches.open('video-pre-cache')
  .then(cache => fetchAndCache(videoFileUrl, cache)) // Defined above.
  .then(response => response.arrayBuffer())
  .then(data => {
    const mediaSource = new MediaSource();
    video.src = URL.createObjectURL(mediaSource);
    mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

    function sourceOpen() {
      URL.revokeObjectURL(video.src);

      const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
      sourceBuffer.appendBuffer(data);

      video.play().then(() => {
        // TODO: Fetch the rest of the video when user starts playing video.
      });
    }
  });
}

Tworzenie odpowiedzi zakresu za pomocą skryptu service worker

A co, jeśli pobierzesz cały plik wideo i zapiszesz go interfejs API Cache? Gdy przeglądarka wysyła żądanie HTTP Range, na pewno nie chcesz umieścić w pamięci mechanizmu renderowania cały film, ponieważ obsługują jeszcze Range odpowiedzi.

Pokażę więc, jak przechwycić te żądania i zwrócić dostosowane Range ze skryptu service worker.

addEventListener('fetch', event => {
  event.respondWith(loadFromCacheOrFetch(event.request));
});

function loadFromCacheOrFetch(request) {
  // Search through all available caches for this request.
  return caches.match(request)
  .then(response => {

    // Fetch from network if it's not already in the cache.
    if (!response) {
      return fetch(request);
      // Note that we may want to add the response to the cache and return
      // network response in parallel as well.
    }

    // Browser sends a HTTP Range request. Let's provide one reconstructed
    // manually from the cache.
    if (request.headers.has('range')) {
      return response.blob()
      .then(data => {

        // Get start position from Range request header.
        const pos = Number(/^bytes\=(\d+)\-/g.exec(request.headers.get('range'))[1]);
        const options = {
          status: 206,
          statusText: 'Partial Content',
          headers: response.headers
        }
        const slicedResponse = new Response(data.slice(pos), options);
        slicedResponse.setHeaders('Content-Range': 'bytes ' + pos + '-' +
            (data.size - 1) + '/' + data.size);
        slicedResponse.setHeaders('X-From-Cache': 'true');

        return slicedResponse;
      });
    }

    return response;
  }
}

Należy zauważyć, że do odtworzenia tego wycinka służy mi usługa response.blob() ponieważ daje mi to po prostu nick do pliku, response.arrayBuffer() umieszcza cały plik w pamięci mechanizmu renderowania.

Mój niestandardowy nagłówek HTTP X-From-Cache może być używany do sprawdzania, czy to żądanie z pamięci podręcznej lub z sieci. Może go używać gracz, taki jak ShakaPlayer, aby zignorować czas odpowiedzi jako wskaźnik oraz ich prędkość sieci.

Przyjrzyj się oficjalnej aplikacji Sample Media, a w szczególności pliku ranged-response.js, aby uzyskać kompletne rozwiązanie do obsługi protokołu Range. żądań.