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