diff --git a/package-lock.json b/package-lock.json index ad47aead..c08bac41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6692,6 +6692,16 @@ "eslint-visitor-keys": "^2.0.0" } }, + "@videojs/vhs-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.4.tgz", + "integrity": "sha512-hui4zOj2I1kLzDgf8QDVxD3IzrwjS/43KiS8IHQO0OeeSsb4pB/lgNt1NG7Dv0wMQfCccUpMVLGcK618s890Yg==", + "requires": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -10553,8 +10563,7 @@ "dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", - "dev": true + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" }, "domain-browser": { "version": "1.2.0", @@ -12943,7 +12952,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "dev": true, "requires": { "min-document": "^2.19.0", "process": "^0.11.10" @@ -16744,6 +16752,16 @@ "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", "dev": true }, + "m3u8-parser": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.0.tgz", + "integrity": "sha512-48l/OwRyjBm+QhNNigEEcRcgbRvnUjL7rxs597HmW9QSNbyNvt+RcZ9T/d9vxi9A9z7EZrB1POtZYhdRlwYQkQ==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.0", + "global": "^4.4.0" + } + }, "magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -17122,7 +17140,6 @@ "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", - "dev": true, "requires": { "dom-walk": "^0.1.0" } @@ -25118,6 +25135,13 @@ "integrity": "sha512-AH6x5pYq4vwQvfRDWH+vfOePfPIYQ00nCEB7dJRU1e0n9+9HMRyvI63FlDvtFT2AvXVRsXvUt7DNMEToyJLpSA==", "requires": { "workbox-core": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "workbox-broadcast-update": { @@ -25126,6 +25150,13 @@ "integrity": "sha512-HTyTWkqXvHRuqY73XrwvXPud/FN6x3ROzkfFPsRjtw/kGZuZkPzfeH531qdUGfhtwjmtO/ZzXcWErqVzJNdXaA==", "requires": { "workbox-core": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "workbox-build": { @@ -25198,6 +25229,11 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" } } }, @@ -25207,6 +25243,13 @@ "integrity": "sha512-0bfvMZs0Of1S5cdswfQK0BXt6ulU5kVD4lwer2CeI+03czHprXR3V4Y8lPTooamn7eHP8Iywi5QjyAMjw0qauA==", "requires": { "workbox-core": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "workbox-core": { @@ -25220,6 +25263,13 @@ "integrity": "sha512-oDO/5iC65h2Eq7jctAv858W2+CeRW5e0jZBMNRXpzp0ZPvuT6GblUiHnAsC5W5lANs1QS9atVOm4ifrBiYY7AQ==", "requires": { "workbox-core": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "workbox-google-analytics": { @@ -25231,6 +25281,13 @@ "workbox-core": "^5.1.4", "workbox-routing": "^5.1.4", "workbox-strategies": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "workbox-navigation-preload": { @@ -25239,6 +25296,13 @@ "integrity": "sha512-Wf03osvK0wTflAfKXba//QmWC5BIaIZARU03JIhAEO2wSB2BDROWI8Q/zmianf54kdV7e1eLaIEZhth4K4MyfQ==", "requires": { "workbox-core": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "workbox-precaching": { @@ -25247,6 +25311,13 @@ "integrity": "sha512-gCIFrBXmVQLFwvAzuGLCmkUYGVhBb7D1k/IL7pUJUO5xacjLcFUaLnnsoVepBGAiKw34HU1y/YuqvTKim9qAZA==", "requires": { "workbox-core": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "workbox-range-requests": { @@ -25255,6 +25326,13 @@ "integrity": "sha512-1HSujLjgTeoxHrMR2muDW2dKdxqCGMc1KbeyGcmjZZAizJTFwu7CWLDmLv6O1ceWYrhfuLFJO+umYMddk2XMhw==", "requires": { "workbox-core": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "workbox-routing": { @@ -25263,6 +25341,13 @@ "integrity": "sha512-8ljknRfqE1vEQtnMtzfksL+UXO822jJlHTIR7+BtJuxQ17+WPZfsHqvk1ynR/v0EHik4x2+826Hkwpgh4GKDCw==", "requires": { "workbox-core": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "workbox-strategies": { @@ -25272,6 +25357,13 @@ "requires": { "workbox-core": "^5.1.4", "workbox-routing": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "workbox-streams": { @@ -25281,6 +25373,13 @@ "requires": { "workbox-core": "^5.1.4", "workbox-routing": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "workbox-sw": { @@ -25307,6 +25406,13 @@ "integrity": "sha512-vXQtgTeMCUq/4pBWMfQX8Ee7N2wVC4Q7XYFqLnfbXJ2hqew/cU1uMTD2KqGEgEpE4/30luxIxgE+LkIa8glBYw==", "requires": { "workbox-core": "^5.1.4" + }, + "dependencies": { + "workbox-core": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-5.1.4.tgz", + "integrity": "sha512-+4iRQan/1D8I81nR2L5vcbaaFskZC2CL17TLbvWVzQ4qiF/ytOGF6XeV54pVxAvKUtkLANhk8TyIUMtiMw2oDg==" + } } }, "worker-farm": { diff --git a/package.json b/package.json index b37ab067..59822392 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "history": "^4.10.1", "hls.js": "^0.14.15", "lodash": "^4.17.15", + "m3u8-parser": "^4.7.0", "oidc-client": "^1.11.5", "react": "^17.0.2", "react-datepicker": "^3.1.3", @@ -32,7 +33,9 @@ "react-scripts": "^4.0.3", "react-window": "^1.8.6", "screenfull": "^5.0.2", - "styled-components": "^5.3.3" + "styled-components": "^5.3.3", + "workbox-core": "^5.1.4", + "workbox-precaching": "^5.1.4" }, "devDependencies": { "@commitlint/cli": "^14.1.0", diff --git a/src/config/userAgent.tsx b/src/config/userAgent.tsx index b5576ab7..b0405c07 100644 --- a/src/config/userAgent.tsx +++ b/src/config/userAgent.tsx @@ -1,5 +1,3 @@ -import includes from 'lodash/includes' +export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) -export const isIphone = includes(window.navigator.userAgent, 'iPhone') - -export const isMobileDevice = includes(window.navigator.userAgent, 'Android') || isIphone +export const isMobileDevice = /iPhone|Android/.test(navigator.userAgent) diff --git a/src/features/StreamPlayer/components/ProgressBar/index.tsx b/src/features/StreamPlayer/components/ProgressBar/index.tsx index 1260e4e8..49ccdfc7 100644 --- a/src/features/StreamPlayer/components/ProgressBar/index.tsx +++ b/src/features/StreamPlayer/components/ProgressBar/index.tsx @@ -24,8 +24,8 @@ export const ProgressBar = ({ playedProgress, }: Props) => { const progressBarRef = useSlider({ onChange: onPlayedProgressChange }) - const loadedFraction = loadedProgress * 100 / duration - const playedFraction = playedProgress * 100 / duration + const loadedFraction = Math.min(loadedProgress * 100 / duration, 100) + const playedFraction = Math.min(playedProgress * 100 / duration, 100) return ( diff --git a/src/features/StreamPlayer/config.tsx b/src/features/StreamPlayer/config.tsx index 28494424..72bec30c 100644 --- a/src/features/StreamPlayer/config.tsx +++ b/src/features/StreamPlayer/config.tsx @@ -1,19 +1,13 @@ import Hls from 'hls.js' import { readToken } from 'helpers/token' -import { isIphone } from 'config/userAgent' export const streamConfig: Partial = { liveSyncDuration: 30, maxBufferLength: 30, xhrSetup: (xhr, urlString) => { - if (isIphone) { - // eslint-disable-next-line no-param-reassign - xhr.withCredentials = true - } else { - const url = new URL(urlString) - url.searchParams.set('access_token', readToken() || '') - xhr.open('GET', url.toString()) - } + const url = new URL(urlString) + url.searchParams.set('access_token', readToken() || '') + xhr.open('GET', url.toString()) }, } diff --git a/src/features/StreamPlayer/hooks/index.tsx b/src/features/StreamPlayer/hooks/index.tsx index 86c5afbc..42fae2cb 100644 --- a/src/features/StreamPlayer/hooks/index.tsx +++ b/src/features/StreamPlayer/hooks/index.tsx @@ -15,6 +15,8 @@ import { useObjectState } from 'hooks' import type { MatchInfo } from 'requests/getMatchInfo' +import { isIOS } from 'config/userAgent' + import { useHlsPlayer } from './useHlsPlayer' import { useFullscreen } from './useFullscreen' import { useVideoQuality } from './useVideoQuality' @@ -107,13 +109,14 @@ export const useVideoPlayer = ({ const onLoadedProgress = (loadedMs: number) => { setPlayerState({ loadedProgress: loadedMs }) } + const onPlayedProgress = (playedMs: number) => { setPlayerState({ playedProgress: playedMs }) progressChangeCallback(playedMs / 1000) } const backToLive = useCallback(() => { - const liveProgressMs = duration - 10000 + const liveProgressMs = Math.max(duration - 10000, 0) setPlayerState({ playedProgress: liveProgressMs, seek: liveProgressMs / 1000 }) }, [duration, setPlayerState]) @@ -130,6 +133,18 @@ export const useVideoPlayer = ({ setPlayerState, ]) + useEffect(() => { + if (!navigator.serviceWorker || !isIOS) return undefined + const listener = (event: MessageEvent) => { + setPlayerState({ duration: toMilliSeconds(event.data.duration) }) + } + + navigator.serviceWorker.addEventListener('message', listener) + return () => { + navigator.serviceWorker.removeEventListener('message', listener) + } + }, [setPlayerState]) + return { backToLive, duration, diff --git a/src/features/StreamPlayer/hooks/useFullscreen.tsx b/src/features/StreamPlayer/hooks/useFullscreen.tsx index f17542e8..ffbfb4a5 100644 --- a/src/features/StreamPlayer/hooks/useFullscreen.tsx +++ b/src/features/StreamPlayer/hooks/useFullscreen.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react' import screenfull from 'screenfull' -import { isIphone } from 'config/userAgent' +import { isIOS } from 'config/userAgent' import { useToggle } from 'hooks' @@ -62,7 +62,7 @@ export const useFullscreen = () => { } const onFullscreenClick = () => { - if (isIphone) { + if (isIOS) { toggleIOSFullscreen() } else { toggleFullscreen() diff --git a/src/features/StreamPlayer/hooks/useHlsPlayer.tsx b/src/features/StreamPlayer/hooks/useHlsPlayer.tsx index 054056c6..1463d086 100644 --- a/src/features/StreamPlayer/hooks/useHlsPlayer.tsx +++ b/src/features/StreamPlayer/hooks/useHlsPlayer.tsx @@ -11,23 +11,26 @@ import { streamConfig } from '../config' export const useHlsPlayer = (src: string, resumeFrom?: number) => { const hls = useMemo(() => { + if (!Hls.isSupported()) return null + const newStreamConfig = { ...streamConfig } if (isNumber(resumeFrom)) { newStreamConfig.startPosition = resumeFrom } return new Hls(newStreamConfig) }, [resumeFrom]) + const videoRef = useRef(null) useEffect(() => { const video = videoRef.current - if (!video) return + if (!video || !hls) return hls.loadSource(src) hls.attachMedia(video) }, [src, hls]) - useEffect(() => () => hls.destroy(), [hls]) + useEffect(() => () => hls?.destroy(), [hls]) return { hls, diff --git a/src/features/StreamPlayer/hooks/useVideoQuality.tsx b/src/features/StreamPlayer/hooks/useVideoQuality.tsx index 27878d5f..c8617513 100644 --- a/src/features/StreamPlayer/hooks/useVideoQuality.tsx +++ b/src/features/StreamPlayer/hooks/useVideoQuality.tsx @@ -46,7 +46,7 @@ const getVideoQualities = (levels: Array) => { return uniqBy([...sorted, autoQuality], 'label') } -export const useVideoQuality = (hls: Hls) => { +export const useVideoQuality = (hls: Hls | null) => { const [videoQualities, setVideoQualities] = useState([autoQuality]) const [selectedQuality, setSelectedQuality] = useLocalStore({ defaultValue: autoQuality.label, @@ -55,6 +55,8 @@ export const useVideoQuality = (hls: Hls) => { }) const onQualitySelect = useCallback((label: string) => { + if (!hls) return + const quality = find(videoQualities, { label }) if (!quality || quality.level === hls.currentLevel) return // eslint-disable-next-line no-param-reassign @@ -67,6 +69,8 @@ export const useVideoQuality = (hls: Hls) => { ]) useEffect(() => { + if (!hls) return undefined + const listener = () => { const qualities = getVideoQualities(hls.levels) const quality = find(qualities, { label: selectedQuality }) || autoQuality diff --git a/src/features/StreamPlayer/index.tsx b/src/features/StreamPlayer/index.tsx index d4d2fdc2..29633ba3 100644 --- a/src/features/StreamPlayer/index.tsx +++ b/src/features/StreamPlayer/index.tsx @@ -79,13 +79,11 @@ export const StreamPlayer = (props: Props) => { onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} > - { - !ready && ( - - - - ) - } + {!ready && ( + + + + )} { onError={onError} crossOrigin='use-credentials' /> - - { - ready && ( - - - {REWIND_SECONDS} - - - - {REWIND_SECONDS} - - - ) - } + {ready && ( + + + {REWIND_SECONDS} + + + + {REWIND_SECONDS} + + + )} @@ -142,9 +137,7 @@ export const StreamPlayer = (props: Props) => { onChange={onVolumeChange} onClick={onVolumeClick} /> - - {secondsToHms(playedProgress / 1000)} - + {secondsToHms(playedProgress / 1000)} {REWIND_SECONDS} {REWIND_SECONDS} @@ -156,7 +149,6 @@ export const StreamPlayer = (props: Props) => { ) } - { + const parser = new Parser() + parser.push(manifest) + parser.end() + const parsedManifest: Manifest = parser.manifest + + const totalDuration = reduce( + parsedManifest?.segments, + (sum: number, { duration }) => sum + duration, + 0, + ) + return totalDuration +} diff --git a/src/index.tsx b/src/index.tsx index 7f94bd65..42976fea 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,8 @@ import { } from 'react' import ReactDOM from 'react-dom' +import { isIOS } from 'config/userAgent' + import * as serviceWorker from './serviceWorker' export const App = process.env.REACT_APP_TYPE === 'auth-service' @@ -20,7 +22,21 @@ ReactDOM.render( document.getElementById('root'), ) -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://bit.ly/CRA-PWA -serviceWorker.unregister() +if (isIOS) { + serviceWorker.register({ + onUpdate: (registration) => { + const waitingServiceWorker = registration.waiting + if (waitingServiceWorker) { + waitingServiceWorker.addEventListener('statechange', (event) => { + // @ts-expect-error + if (event.target?.state === 'activated') { + window.location.reload() + } + }) + waitingServiceWorker.postMessage({ type: 'SKIP_WAITING' }) + } + }, + }) +} else { + serviceWorker.unregister() +} diff --git a/src/service-worker.ts b/src/service-worker.ts new file mode 100644 index 00000000..703a8c8d --- /dev/null +++ b/src/service-worker.ts @@ -0,0 +1,51 @@ +/// +/* eslint-disable no-restricted-globals */ + +// This service worker can be customized! +// See https://developers.google.com/web/tools/workbox/modules +// for the list of available Workbox modules, or add any other +// code you'd like. +// You can also remove this file if you'd prefer not to use a +// service worker, and the Workbox build step will be skipped. + +import { clientsClaim } from 'workbox-core' +import { precacheAndRoute } from 'workbox-precaching' + +import { manifestParser } from './helpers/parseHlsResponse' + +declare const self: ServiceWorkerGlobalScope + +clientsClaim() + +// Precache all of the assets generated by your build process. +// Their URLs are injected into the manifest variable below. +// This variable must be present somewhere in your service worker file, +// even if you decide not to use precaching. See https://cra.link/PWA +precacheAndRoute(self.__WB_MANIFEST) + +// This allows the web app to trigger skipWaiting via +// registration.waiting.postMessage({type: 'SKIP_WAITING'}) +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting() + } +}) +const sendDuration = async (clientId: string, response:Response) => { + const client = await self.clients.get(clientId) + if (!client) return + const text = await response.text() + const totalDuration = manifestParser(text) + client.postMessage({ duration: totalDuration }) +} +// Any other custom service worker logic can go here. +self.addEventListener('fetch', async (event) => { + const regex = /m3u8/g + if (regex.test(event.request.url)) { + const getPlaylists = async () => { + const response = await fetch(event.request) + sendDuration(event.clientId, response.clone()) + return response + } + event.respondWith(getPlaylists()) + } +}) diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts index 5656739b..3fd94404 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorker.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ // This optional code is used to register a service worker. // register() is not called by default. @@ -9,16 +8,14 @@ // resources are updated in the background. // To learn more about the benefits of this model and instructions on how to -// opt-in, read https://bit.ly/CRA-PWA - +// opt-in, read https://cra.link/PWA +/* eslint-disable no-console */ const isLocalhost = Boolean( - window.location.hostname === 'localhost' || + window.location.hostname === 'localhost' // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || + || window.location.hostname === '[::1]' // 127.0.0.0/8 are considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) + || window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/), ) type Config = { @@ -27,16 +24,13 @@ type Config = { } export function register(config?: Config) { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + if ('serviceWorker' in navigator) { // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL( - process.env.PUBLIC_URL, - window.location.href - ) + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href) if (publicUrl.origin !== window.location.origin) { // Our service worker won't work if PUBLIC_URL is on a different origin // from what our page is served on. This might happen if a CDN is used to - // serve assets see https://github.com/facebook/create-react-app/issues/2374 + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 return } @@ -51,8 +45,8 @@ export function register(config?: Config) { // service worker/PWA documentation. navigator.serviceWorker.ready.then(() => { console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://bit.ly/CRA-PWA' + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://cra.link/PWA', ) }) } else { @@ -62,11 +56,11 @@ export function register(config?: Config) { }) } } - +/* eslint no-param-reassign: "error" */ function registerValidSW(swUrl: string, config?: Config) { navigator.serviceWorker .register(swUrl) - .then(registration => { + .then((registration) => { registration.onupdatefound = () => { const installingWorker = registration.installing if (installingWorker == null) { @@ -79,8 +73,8 @@ function registerValidSW(swUrl: string, config?: Config) { // but the previous service worker will still serve the older // content until all client tabs are closed. console.log( - 'New content is available and will be used when all ' + - 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://cra.link/PWA.', ) // Execute callback @@ -102,7 +96,7 @@ function registerValidSW(swUrl: string, config?: Config) { } } }) - .catch(error => { + .catch((error) => { console.error('Error during service worker registration:', error) }) } @@ -110,17 +104,17 @@ function registerValidSW(swUrl: string, config?: Config) { function checkValidServiceWorker(swUrl: string, config?: Config) { // Check if the service worker can be found. If it can't reload the page. fetch(swUrl, { - headers: { 'Service-Worker': 'script' } + headers: { 'Service-Worker': 'script' }, }) - .then(response => { + .then((response) => { // Ensure service worker exists, and that we really are getting a JS file. const contentType = response.headers.get('content-type') if ( - response.status === 404 || - (contentType != null && contentType.indexOf('javascript') === -1) + response.status === 404 + || (contentType != null && contentType.indexOf('javascript') === -1) ) { // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then(registration => { + navigator.serviceWorker.ready.then((registration) => { registration.unregister().then(() => { window.location.reload() }) @@ -131,19 +125,17 @@ function checkValidServiceWorker(swUrl: string, config?: Config) { } }) .catch(() => { - console.log( - 'No internet connection found. App is running in offline mode.' - ) + console.log('No internet connection found. App is running in offline mode.') }) } export function unregister() { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready - .then(registration => { + .then((registration) => { registration.unregister() }) - .catch(error => { + .catch((error) => { console.error(error.message) }) } diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 00000000..5d86129e --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1,70 @@ +declare module 'm3u8-parser' { + export const Parser = any + + export interface Manifest { + allowCache: boolean, + custom: {}, + dateTimeObject: Date, + dateTimeString: string, + discontinuitySequence: number, + discontinuityStarts: [number], + endList: boolean, + mediaGroups: { + AUDIO: { + 'GROUP-ID': { + NAME: { + autoselect: boolean, + characteristics: string, + default: boolean, + forced: boolean, + instreamId: string, + language: string, + uri: string, + }, + }, + }, + 'CLOSED-CAPTIONS': {}, + SUBTITLES: {}, + VIDEO: {}, + }, + mediaSequence: number, + playlistType: string, + playlists: [ + { + Manifest, + attributes: {}, + } + ], + segments: [ + { + attributes: {}, + byterange: { + length: number, + offset: number, + }, + 'cue-in': string, + 'cue-out': string, + 'cue-out-cont': string, + custom: {}, + discontinuity: number, + duration: number, + key: { + iv: string, + method: string, + uri: string, + }, + map: { + byterange: { + length: number, + offset: number, + }, + uri: string, + }, + timeline: number, + uri: string, + } + ], + targetDuration: number, + totalDuration: number, + } +} diff --git a/tsconfig.json b/tsconfig.json index 9021894b..58b76a6f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,10 +19,7 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "noUnusedLocals": true, "noFallthroughCasesInSwitch": true }, - "include": [ - "src" - ] + "include": ["src"] }