1/**
2 * Detect if the browser supports the standard fullscreen API on the given
3 * element:
4 * https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
5 */
6const supportsFullscreenAPI = (element: HTMLMediaElement): boolean =>
7  'requestFullscreen' in element;
8
9/**
10 * Detect if the browser supports the non-standard webkit fullscreen API on the
11 * given element (looking at you, Safari).
12 */
13const supportsWebkitFullscreenAPI = (element: HTMLMediaElement): boolean =>
14  'webkitEnterFullScreen' in element;
15
16/**
17 * Detect if the browser supports the non-standard ms fullscreen API on the
18 * given element (looking at you, IE11).
19 */
20const supportsMsFullscreenAPI = (element: HTMLMediaElement): boolean =>
21  'msRequestFullscreen' in element;
22
23/**
24 * Detect if the browser supports the `webkitFullscreenChange` event. This is
25 * a non-standard event added to Safari on macOS by Apple:
26 * https://developer.apple.com/documentation/webkitjs/document/1631998-onwebkitfullscreenchange
27 */
28const supportsWebkitFullscreenChangeEvent = (): boolean =>
29  supportsEvent('video', 'webkitfullscreenchange');
30
31/**
32 * A helper that adds an event listener to an element. The key value-add over
33 * the native addEventListener is that it returns a function that will remove
34 * the event listener. This allows the setup and teardown logic for a listener
35 * to be easily colocated.
36 */
37function addEventListener(
38  element: Document | HTMLElement,
39  eventName: string,
40  listener: EventListenerOrEventListenerObject
41): () => any {
42  element.addEventListener(eventName, listener);
43  return () => element.removeEventListener(eventName, listener);
44}
45
46/**
47 * Detect if the browser supports an event on a particular element type.
48 */
49const supportsEvent = (elementName: string, eventName: string): boolean => {
50  // Detect if the browser supports the event by attempting to add a handler
51  // attribute for that event to the provided element. If the event is supported
52  // then the browser will accept the attribute and report the type of the
53  // attribute as "function". See: https://stackoverflow.com/a/4562426/2747759
54  const element = document.createElement(elementName);
55  element.setAttribute('on' + eventName, 'return;');
56  return typeof element['on' + eventName] === 'function';
57};
58
59/**
60 * Switches a video element into fullscreen.
61 */
62export async function requestFullscreen(element: HTMLMediaElement): Promise<void> {
63  if (supportsFullscreenAPI(element)) {
64    return element.requestFullscreen();
65  } else if (supportsWebkitFullscreenAPI(element)) {
66    // This API is synchronous so no need to return the result
67    element['webkitEnterFullScreen']?.();
68  } else if (supportsMsFullscreenAPI(element)) {
69    // This API is synchronous so no need to return the result
70    element['msRequestFullscreen']?.();
71  } else {
72    throw new Error('Fullscreen not supported');
73  }
74}
75
76/**
77 * Switches a video element out of fullscreen.
78 */
79
80export async function exitFullscreen(element: HTMLMediaElement): Promise<void> {
81  if (supportsFullscreenAPI(element)) {
82    return document.exitFullscreen();
83  } else if (supportsWebkitFullscreenAPI(element)) {
84    // This API is synchronous so no need to return the result
85    element['webkitExitFullScreen']?.();
86  } else if (supportsMsFullscreenAPI(element)) {
87    // This API is synchronous so no need to return the result
88    document['msExitFullscreen']?.();
89  } else {
90    throw new Error('Fullscreen not supported');
91  }
92}
93
94/**
95 * Listens for fullscreen change events on a video element. The provided
96 * callback will be called with `true` when the video is switched into
97 * fullscreen and `false` when the video is switched out of fullscreen.
98 */
99export function addFullscreenListener(
100  element: HTMLVideoElement,
101  callback: (isFullscreen: boolean) => void
102): () => any {
103  if (supportsFullscreenAPI(element)) {
104    // Used by browsers that support the official spec
105    return addEventListener(element, 'fullscreenchange', (event) =>
106      callback(document.fullscreenElement === event.target)
107    );
108  } else if (supportsWebkitFullscreenAPI(element) && supportsWebkitFullscreenChangeEvent()) {
109    // Used by Safari on macOS
110    return addEventListener(element, 'webkitfullscreenchange', (event) =>
111      callback(document['webkitFullscreenElement'] === event.target)
112    );
113  } else if (supportsWebkitFullscreenAPI(element)) {
114    // Used by Safari on iOS
115    const removeBeginListener = addEventListener(element, 'webkitbeginfullscreen', () =>
116      callback(true)
117    );
118    const removeEndListener = addEventListener(element, 'webkitendfullscreen', () =>
119      callback(false)
120    );
121    return () => {
122      removeBeginListener();
123      removeEndListener();
124    };
125  } else if (supportsMsFullscreenAPI(element)) {
126    // Used by IE11
127    return addEventListener(document, 'MSFullscreenChange', (event) =>
128      callback(document['msFullscreenElement'] === event.target)
129    );
130  } else {
131    return () => {};
132  }
133}
134