search check home clock-o tag tags chevron-left chevron-right chevron-up chevron-down twitter facebook github rss comment comments terminal code

[JS] Jak zsynchronizować wideo między dwiema zakładkami przeglądarki?

[JS] Jak zsynchronizować wideo między dwiema zakładkami przeglądarki?

Jakiś czas temu zastanawiałem się czy można synchronizować stan aplikacji między dwiema otwartymi zakładkami przeglądarki? W pewnych sytuacjach byłaby to bardzo przydatna rzecz. Jak się okazało, jest taka możliwość, a sposób w jaki można synchronizować stany nieco mnie zaskoczył.

Aby dokonać synchronizacji należy wykorzystać localStorage API i system zdarzeń jakie to API udostępnia. W tym artykule, opiszę sposób synchronizacji odtwarzania pliku wideo między dwiema otwartymi zakładkami przeglądarki i oprócz HTML5 localStorage API wykorzystamy HTML5 Page Visibility API oraz HTML5 Video API.

Czym jest localStorage?

localStorage można określić jako podręczną pamięć przeglądarki, która jest stale dostępna i nieulotna. Z której można korzystać za pomocą JavaScript.

Kod video w HTML

Pierwszą rzeczą, którą musimy zrobi jest wstawienie kodu filmu wideo na stronę. Minimalna ilość kodu HTML wymaganego do wstawienia filmu wideo na stronę wygląda następująco:

1
2
3
<video autobuffer autoplay loop controls>
  <source src="test-video.mp4" type="video/mp4">
</video>

Film wideo umieszczony za pomocą powyższego kodu będzie się charakteryzował następującymi rzeczami:

  • Będzie automatycznie buforowany,
  • Będzie puszczony w pętli,
  • Będzie automatycznie odtwarzany jak tylko zostanie załadowany,
  • Pojawią się kontrolki do obsługi wideo.

Obsługa zdarzeń wideo za pomocą JS

Samo wstawienie pliku wideo na stronę nie kończy naszej pracy. Teraz należy zaprogramować obsługę zdarzeń wideo za pomocą kodu JavaScript.

Element <video> posiada swoje własne API javascriptowe (podobnie jak SVG, audio i wiele innych), dzięki czemu jesteśmy w stanie dowolnie manipulować elementem w zależnie od potrzeb. To API udostępnia również zdarzenia emitowane przez ten element. Przykładem zdarzeń emitowanych przez element <video> są na przykład zdarzenia: play, pause czy stop. Wykorzystamy, wymienione przed chwilą, w naszym kodzie:

1
2
3
4
5
6
7
8
9
10
(function () {
  'use strict';
 
  var video = document.querySelector('video');
 
  // obsługa zdarzenia emitowanego przez HTML5 Video API
  video.addEventListener('pause', function (event) {
    window.localStorage.setItem('video-time', event.target.currentTime);
  });
})();

Powyższy kod ma tylko jedno zadanie, zapisać czas w którym spauzowano odtwarzanie filmu wideo w podręcznej pamięci przeglądarki za pomocą localStorage API.

Wykrywanie zmian w pamięci podręcznej przeglądarki

Tak jak wspomniałem wcześniej, localStorage ma swoje API, które udostępnia dostęp do zdarzeń emitowanych przez to API. Wykorzystując go jesteśmy w stanie wykryć czy w pamięci przeglądarki nastąpiły jakieś zmiany:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(function () {
  'use strict';
 
  var video = document.querySelector('video');
 
  // obsługa zdarzenia emitowanego przez HTML5 Video API
  video.addEventListener('pause', function (event) {
    window.localStorage.setItem('video-time', event.target.currentTime);
  });
 
  // obsługa zdarzenia emitowanego przez HTML5 localStorage API
  window.addEventListener('storage', function (event) {
    if (event.key === 'video-time') {
      video.currentTime = event.newValue;
    }
  });
})();

Do wcześniejszego kodu JS dodałem obsługę zdarzenia zmiany stanu pamięci podręcznej przeglądarki. Jeśli zdarzenie jest emitowane przez zmianę wartości w pożądanym miejscu - klucz o wartości video-time, to film wideo umieszczony na stronie zmieni czasu od którego ma zacząć odtwarzanie wideo.

Tym samym mamy już obsługę dwóch zdarzeń. Taki krótki zapis pozwoli nam na osiągnięcie zamierzonego efektu, tj. synchronizacji czasu odtwarzanego wideo między zakładkami.

Pauzowanie wideo, gdy strona jest niewidoczna - HTML5 Page Visibility API

Powyższy kod ulepszymy za pomocą dodania obsługi przełączenia stanu widoczności strony. Co to oznacza? Oznacza to, że dodamy obsługę pauzowania filmu wideo w momencie gdy użytkownik przełączy zakładki. W momencie spauzowania wideo, zostanie zaktualizowana wartość klucza video-time, a w kolejnej zakładce przeglądarki (w której powinna być otwarta ta sama strona), rozpocznie się odtwarzanie filmu wideo od miejsca w którym wideo zostało spauzowane na poprzedniej stronie. Wydaje się to skomplikowane, ale jest banalnie proste do osiągnięcia:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
(function () {
  'use strict';
 
  var video = document.querySelector('video');
  var isPageHidden;
  var visibilityChangeEventName;
 
  video.addEventListener('pause', function (event) {
    window.localStorage.setItem('video-time', event.target.currentTime);
  });
 
  window.addEventListener('storage', function (event) {
    if (event.key === 'video-time') {
      video.currentTime = event.newValue;
    }
  });
 
  // sprawdzanie wersji zdarzenia obsługiwanego przez przeglądarkę
  // różne przeglądarki mogą różnie nazwać to samo zdarzenie
  if (typeof document.hidden !== 'undefined') {
    isPageHidden = 'hidden';
    visibilityChangeEventName = 'visibilitychange';
  } else if (typeof document.mozHidden !== 'undefined') {
    isPageHidden = 'mozHidden';
    visibilityChangeEventName = 'mozvisibilitychange';
  } else if (typeof document.msHidden !== 'undefined') {
    isPageHidden = 'msHidden';
    visibilityChangeEventName = 'msvisibilitychange';
  } else if (typeof document.webkitHidden !== 'undefined') {
    isPageHidden = 'webkitHidden';
    visibilityChangeEventName = 'webkitvisibilitychange';
  }
 
  // funkcja obsługująca zdarzenie zmiany stanu widoczności strony
  function handleVisibilityChange() {
    // jeśli strona nie jest widoczna
    if (document[isPageHidden]) {
      video.pause();
    }
    // jeśli strona jest widoczna 
    else {
      video.play();
    }
  }
 
  // jeśli przeglądarka obsługuje HTML5 Page Visibility API
  if (typeof document[isPageHidden] !== 'undefined') {
    // dodanie obsługi zdarzenia zmiany widoczności strony
    document.addEventListener(visibilityChangeEventName, handleVisibilityChange);
  }
})();

W powyższej aktualizacji kodu obsługujemy zdarzenie przełączenia strony internetowej przez użytkownika. Dzięki sprawdzeniu czy strona jest aktualnie oglądania przez użytkownika możemy zadecydować czy uruchamiamy odtwarzanie wideo czy też pauzujemy wideo i zapisujemy stan odtwarzania do pamięci podręcznej przeglądarki.

Podmiana ikony strony internetowej pokazującej stan odtwarzania wideo

Jako bonus, dodamy podmianę ikonę strony, która się pojawia obok nazwy strony w tytule zakładki. Dodatkowo, będziemy zmieniac tytuł strony aktualizując go o aktualny czas odtwarzania filmu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
(function () {
  'use strict';
 
  var video = document.querySelector('video');
  // znajdź link do ikony strony
  var favicon = document.querySelector('link[rel="shortcut icon"]');
  // tablica dostępnych ikon strony
  var icons = {
      pause: 'img/pause.png',
      play: 'img/play.png'
  };
  var isPageHidden;
  var visibilityChangeEventName;
 
  input.addEventListener('keyup', function () {
      window.localStorage.setItem('input-test', input.value);
  });
 
  // zmień ikonę gdy film będzie odtwarzany
  video.addEventListener('play', function () {
      favicon.href = icons.play;
  });
 
  // zmień ikonę gdy film będzie spauzowany
  video.addEventListener('pause', function (event) {
      window.localStorage.setItem('video-time', event.target.currentTime);
      favicon.href = icons.pause;
  });
 
  window.addEventListener('storage', function (event) {
      if (event.key === 'video-time') {
          video.currentTime = event.newValue;
      }
  });
 
  if (typeof document.hidden !== 'undefined') {
    isPageHidden = 'hidden';
    visibilityChangeEventName = 'visibilitychange';
  } else if (typeof document.mozHidden !== 'undefined') {
    isPageHidden = 'mozHidden';
    visibilityChangeEventName = 'mozvisibilitychange';
  } else if (typeof document.msHidden !== 'undefined') {
    isPageHidden = 'msHidden';
    visibilityChangeEventName = 'msvisibilitychange';
  } else if (typeof document.webkitHidden !== 'undefined') {
    isPageHidden = 'webkitHidden';
    visibilityChangeEventName = 'webkitvisibilitychange';
  }
 
  function handleVisibilityChange() {
    // gdy storna jest niewidoczna ustaw ikone pauzy
    if (document[isPageHidden]) {
      video.pause();
      favicon.href = icons.pause;
    }
    // gdy strona jest widoczna ustaw ikonę odtwarzania
    else {
      video.play();
      favicon.href = icons.play;
    }
  }
 
  if (typeof document[isPageHidden] !== 'undefined') {
    document.addEventListener(visibilityChangeEventName, handleVisibilityChange);
  }
 
  // aktualizowanie tytułu strony o aktualny czas odtwarzania przy każdej zmianie czasu filmu
  video.addEventListener('timeupdate', function () {
    document.title = Math.floor(video.currentTime) + ' sekund(y)';
  }, false);
})();

Jak widać na powyższym przykładzie, podmiana ikony jest prosta i ogranicza się do odnalezienia elementu będącego linkiem do ikony strony/aplikacji internetowej a następnie podmiany wartości źródła ikony. Dodatkowo przy każdej zmianie aktualnego czasu odtwarzania filmu następuje aktualizacja tytułu strony. W tytule strony pokazuje się aktualny czas odtwarzania filmu.

Podsumowanie

Mam nadzieję, że udało mi się Ciebie zainteresować przedstawionym rozwiązaniem. Czasami tego typu rozwiązanie można spotkać na stronach, które udostępniają wideo do obejrzenia, ale najpierw trzeba obejrzeć reklamy. Za pomocą tego sposobu są w stanie wykryć czy użytkownik ogląda stronę z reklamą czy też nie.

Obsługę zdarzeń localStorage można również do wielu innych rzeczy, między innymi do synchronizacji stanu aplikacji internetowych. Czasem się zdarzy że użytkownicy tworzą w jednej zakładce nowe treści na stronę a w drugiej mają podgląd. Dzięki zdarzeniom emitowanym przez localStorage API można synchronizować podgląd na żywo.

Demo - otwórz 2x tą samą stronę

  • >Jak się okazało, jest taka możliwość, a sposób w jaki można synchronizować stany nieco mnie zaskoczył.
    Też czytujesz PonyFoo czy gdzie indziej się na to nadziałeś? 😉 Prawdę mówiąc gdy zobaczyłem to po raz pierwszy na oczy faktycznie przeżyłem szok – a później był taki moment: „Ale czemu sam na to nie wpadłem?” 😀 No bo to w sumie logiczne, jakby tak mocniej nad tym się pogłowić…

    Niemniej, skoro już wspomniałem PonyFoo, autor tej strony przygotował nawet proste API do „nasłuchiwania” poszczególnych kluczy ze storage – https://github.com/bevacqua/local-storage#lsonkey-fn (szkoda, że oparte na CommonJS, a nie UMD… no ale nie można mieć wszystkiego, prawda? ;))

    Co do ikonki – ciekawie to też rozwiązało YT, które zamiast podmieniać ikonkę, dostawia do adresu znaczek Unicode odtwarzania (przynajmniej na lisku dostawia, na Chrome tego nie widzę). Natomiast w Chrome jest inny, fajny eksperyment: na tabach wyświetlana jest ikonka głośnika, dzięki której można wyciszyć całą kartę. document.soundEnabled API? IMO brzmi dobrze 😉

  • Rzeczywiście, z tamtej strony się dowiedziałem o tym. Lecz tak naprawdę coś w tym kierunku już kombinowałem w wakacje, ale nie zdałem sobie sprawy z tego że tam też są eventy.
    Co do ikonki głośnika w Chrome, to jest to już feature przeglądarki niż API HTML5.

  • Ja za to próbowałem się bawić Service Workerem, żeby zrobić coś podobnego… więc zaliczyłem niezłego facepalma po tym artykule 😉

    >jest to już feature przeglądarki niż API HTML5
    Owszem, ale API by z tego wyszło fajne IMO. Skoro przeportowali do HTML5 połowę syfu z IE6, to głośniczek z Chrome dużej różnicy nie zrobi 😉

  • Co masz na myśli pisząc: „połowę syfu z IE6”?

  • Np Drag API, designMode czy choćby DOM0 (chociaż to może niekoniecznie z IE6, ale z tych czasów).

  • Chciałem kiedyś wykorzystać podobny mechanizm, ale zabrakło mi chęci, by się w to wszystko wgryźć. A wygląda na to, że to wcale nie jest skomplikowane. Dzięki, przyda się 🙂