Ott 1997 ios live btn

keep-around/401f9622901491e44aff8564418e9f565b5f439a
Дектерев Андрей 4 years ago committed by Макситалиев Мирлан
parent 023687a6c0
commit 401f962290
  1. 114
      package-lock.json
  2. 5
      package.json
  3. 6
      src/config/userAgent.tsx
  4. 4
      src/features/StreamPlayer/components/ProgressBar/index.tsx
  5. 6
      src/features/StreamPlayer/config.tsx
  6. 17
      src/features/StreamPlayer/hooks/index.tsx
  7. 4
      src/features/StreamPlayer/hooks/useFullscreen.tsx
  8. 7
      src/features/StreamPlayer/hooks/useHlsPlayer.tsx
  9. 6
      src/features/StreamPlayer/hooks/useVideoQuality.tsx
  10. 18
      src/features/StreamPlayer/index.tsx
  11. 16
      src/helpers/parseHlsResponse/index.ts
  12. 24
      src/index.tsx
  13. 51
      src/service-worker.ts
  14. 54
      src/serviceWorker.ts
  15. 70
      src/types/index.d.ts
  16. 5
      tsconfig.json

114
package-lock.json generated

@ -6692,6 +6692,16 @@
"eslint-visitor-keys": "^2.0.0" "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": { "@webassemblyjs/ast": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
@ -10553,8 +10563,7 @@
"dom-walk": { "dom-walk": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
"dev": true
}, },
"domain-browser": { "domain-browser": {
"version": "1.2.0", "version": "1.2.0",
@ -12943,7 +12952,6 @@
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"dev": true,
"requires": { "requires": {
"min-document": "^2.19.0", "min-document": "^2.19.0",
"process": "^0.11.10" "process": "^0.11.10"
@ -16744,6 +16752,16 @@
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
"dev": true "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": { "magic-string": {
"version": "0.25.7", "version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
@ -17122,7 +17140,6 @@
"version": "2.19.0", "version": "2.19.0",
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
"integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=",
"dev": true,
"requires": { "requires": {
"dom-walk": "^0.1.0" "dom-walk": "^0.1.0"
} }
@ -25118,6 +25135,13 @@
"integrity": "sha512-AH6x5pYq4vwQvfRDWH+vfOePfPIYQ00nCEB7dJRU1e0n9+9HMRyvI63FlDvtFT2AvXVRsXvUt7DNMEToyJLpSA==", "integrity": "sha512-AH6x5pYq4vwQvfRDWH+vfOePfPIYQ00nCEB7dJRU1e0n9+9HMRyvI63FlDvtFT2AvXVRsXvUt7DNMEToyJLpSA==",
"requires": { "requires": {
"workbox-core": "^5.1.4" "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": { "workbox-broadcast-update": {
@ -25126,6 +25150,13 @@
"integrity": "sha512-HTyTWkqXvHRuqY73XrwvXPud/FN6x3ROzkfFPsRjtw/kGZuZkPzfeH531qdUGfhtwjmtO/ZzXcWErqVzJNdXaA==", "integrity": "sha512-HTyTWkqXvHRuqY73XrwvXPud/FN6x3ROzkfFPsRjtw/kGZuZkPzfeH531qdUGfhtwjmtO/ZzXcWErqVzJNdXaA==",
"requires": { "requires": {
"workbox-core": "^5.1.4" "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": { "workbox-build": {
@ -25198,6 +25229,11 @@
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" "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==", "integrity": "sha512-0bfvMZs0Of1S5cdswfQK0BXt6ulU5kVD4lwer2CeI+03czHprXR3V4Y8lPTooamn7eHP8Iywi5QjyAMjw0qauA==",
"requires": { "requires": {
"workbox-core": "^5.1.4" "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": { "workbox-core": {
@ -25220,6 +25263,13 @@
"integrity": "sha512-oDO/5iC65h2Eq7jctAv858W2+CeRW5e0jZBMNRXpzp0ZPvuT6GblUiHnAsC5W5lANs1QS9atVOm4ifrBiYY7AQ==", "integrity": "sha512-oDO/5iC65h2Eq7jctAv858W2+CeRW5e0jZBMNRXpzp0ZPvuT6GblUiHnAsC5W5lANs1QS9atVOm4ifrBiYY7AQ==",
"requires": { "requires": {
"workbox-core": "^5.1.4" "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": { "workbox-google-analytics": {
@ -25231,6 +25281,13 @@
"workbox-core": "^5.1.4", "workbox-core": "^5.1.4",
"workbox-routing": "^5.1.4", "workbox-routing": "^5.1.4",
"workbox-strategies": "^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": { "workbox-navigation-preload": {
@ -25239,6 +25296,13 @@
"integrity": "sha512-Wf03osvK0wTflAfKXba//QmWC5BIaIZARU03JIhAEO2wSB2BDROWI8Q/zmianf54kdV7e1eLaIEZhth4K4MyfQ==", "integrity": "sha512-Wf03osvK0wTflAfKXba//QmWC5BIaIZARU03JIhAEO2wSB2BDROWI8Q/zmianf54kdV7e1eLaIEZhth4K4MyfQ==",
"requires": { "requires": {
"workbox-core": "^5.1.4" "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": { "workbox-precaching": {
@ -25247,6 +25311,13 @@
"integrity": "sha512-gCIFrBXmVQLFwvAzuGLCmkUYGVhBb7D1k/IL7pUJUO5xacjLcFUaLnnsoVepBGAiKw34HU1y/YuqvTKim9qAZA==", "integrity": "sha512-gCIFrBXmVQLFwvAzuGLCmkUYGVhBb7D1k/IL7pUJUO5xacjLcFUaLnnsoVepBGAiKw34HU1y/YuqvTKim9qAZA==",
"requires": { "requires": {
"workbox-core": "^5.1.4" "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": { "workbox-range-requests": {
@ -25255,6 +25326,13 @@
"integrity": "sha512-1HSujLjgTeoxHrMR2muDW2dKdxqCGMc1KbeyGcmjZZAizJTFwu7CWLDmLv6O1ceWYrhfuLFJO+umYMddk2XMhw==", "integrity": "sha512-1HSujLjgTeoxHrMR2muDW2dKdxqCGMc1KbeyGcmjZZAizJTFwu7CWLDmLv6O1ceWYrhfuLFJO+umYMddk2XMhw==",
"requires": { "requires": {
"workbox-core": "^5.1.4" "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": { "workbox-routing": {
@ -25263,6 +25341,13 @@
"integrity": "sha512-8ljknRfqE1vEQtnMtzfksL+UXO822jJlHTIR7+BtJuxQ17+WPZfsHqvk1ynR/v0EHik4x2+826Hkwpgh4GKDCw==", "integrity": "sha512-8ljknRfqE1vEQtnMtzfksL+UXO822jJlHTIR7+BtJuxQ17+WPZfsHqvk1ynR/v0EHik4x2+826Hkwpgh4GKDCw==",
"requires": { "requires": {
"workbox-core": "^5.1.4" "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": { "workbox-strategies": {
@ -25272,6 +25357,13 @@
"requires": { "requires": {
"workbox-core": "^5.1.4", "workbox-core": "^5.1.4",
"workbox-routing": "^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": { "workbox-streams": {
@ -25281,6 +25373,13 @@
"requires": { "requires": {
"workbox-core": "^5.1.4", "workbox-core": "^5.1.4",
"workbox-routing": "^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": { "workbox-sw": {
@ -25307,6 +25406,13 @@
"integrity": "sha512-vXQtgTeMCUq/4pBWMfQX8Ee7N2wVC4Q7XYFqLnfbXJ2hqew/cU1uMTD2KqGEgEpE4/30luxIxgE+LkIa8glBYw==", "integrity": "sha512-vXQtgTeMCUq/4pBWMfQX8Ee7N2wVC4Q7XYFqLnfbXJ2hqew/cU1uMTD2KqGEgEpE4/30luxIxgE+LkIa8glBYw==",
"requires": { "requires": {
"workbox-core": "^5.1.4" "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": { "worker-farm": {

@ -23,6 +23,7 @@
"history": "^4.10.1", "history": "^4.10.1",
"hls.js": "^0.14.15", "hls.js": "^0.14.15",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"m3u8-parser": "^4.7.0",
"oidc-client": "^1.11.5", "oidc-client": "^1.11.5",
"react": "^17.0.2", "react": "^17.0.2",
"react-datepicker": "^3.1.3", "react-datepicker": "^3.1.3",
@ -32,7 +33,9 @@
"react-scripts": "^4.0.3", "react-scripts": "^4.0.3",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"screenfull": "^5.0.2", "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": { "devDependencies": {
"@commitlint/cli": "^14.1.0", "@commitlint/cli": "^14.1.0",

@ -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 = /iPhone|Android/.test(navigator.userAgent)
export const isMobileDevice = includes(window.navigator.userAgent, 'Android') || isIphone

@ -24,8 +24,8 @@ export const ProgressBar = ({
playedProgress, playedProgress,
}: Props) => { }: Props) => {
const progressBarRef = useSlider({ onChange: onPlayedProgressChange }) const progressBarRef = useSlider({ onChange: onPlayedProgressChange })
const loadedFraction = loadedProgress * 100 / duration const loadedFraction = Math.min(loadedProgress * 100 / duration, 100)
const playedFraction = playedProgress * 100 / duration const playedFraction = Math.min(playedProgress * 100 / duration, 100)
return ( return (
<ProgressBarList ref={progressBarRef}> <ProgressBarList ref={progressBarRef}>

@ -1,19 +1,13 @@
import Hls from 'hls.js' import Hls from 'hls.js'
import { readToken } from 'helpers/token' import { readToken } from 'helpers/token'
import { isIphone } from 'config/userAgent'
export const streamConfig: Partial<Hls.Config> = { export const streamConfig: Partial<Hls.Config> = {
liveSyncDuration: 30, liveSyncDuration: 30,
maxBufferLength: 30, maxBufferLength: 30,
xhrSetup: (xhr, urlString) => { xhrSetup: (xhr, urlString) => {
if (isIphone) {
// eslint-disable-next-line no-param-reassign
xhr.withCredentials = true
} else {
const url = new URL(urlString) const url = new URL(urlString)
url.searchParams.set('access_token', readToken() || '') url.searchParams.set('access_token', readToken() || '')
xhr.open('GET', url.toString()) xhr.open('GET', url.toString())
}
}, },
} }

@ -15,6 +15,8 @@ import { useObjectState } from 'hooks'
import type { MatchInfo } from 'requests/getMatchInfo' import type { MatchInfo } from 'requests/getMatchInfo'
import { isIOS } from 'config/userAgent'
import { useHlsPlayer } from './useHlsPlayer' import { useHlsPlayer } from './useHlsPlayer'
import { useFullscreen } from './useFullscreen' import { useFullscreen } from './useFullscreen'
import { useVideoQuality } from './useVideoQuality' import { useVideoQuality } from './useVideoQuality'
@ -107,13 +109,14 @@ export const useVideoPlayer = ({
const onLoadedProgress = (loadedMs: number) => { const onLoadedProgress = (loadedMs: number) => {
setPlayerState({ loadedProgress: loadedMs }) setPlayerState({ loadedProgress: loadedMs })
} }
const onPlayedProgress = (playedMs: number) => { const onPlayedProgress = (playedMs: number) => {
setPlayerState({ playedProgress: playedMs }) setPlayerState({ playedProgress: playedMs })
progressChangeCallback(playedMs / 1000) progressChangeCallback(playedMs / 1000)
} }
const backToLive = useCallback(() => { const backToLive = useCallback(() => {
const liveProgressMs = duration - 10000 const liveProgressMs = Math.max(duration - 10000, 0)
setPlayerState({ playedProgress: liveProgressMs, seek: liveProgressMs / 1000 }) setPlayerState({ playedProgress: liveProgressMs, seek: liveProgressMs / 1000 })
}, [duration, setPlayerState]) }, [duration, setPlayerState])
@ -130,6 +133,18 @@ export const useVideoPlayer = ({
setPlayerState, 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 { return {
backToLive, backToLive,
duration, duration,

@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import screenfull from 'screenfull' import screenfull from 'screenfull'
import { isIphone } from 'config/userAgent' import { isIOS } from 'config/userAgent'
import { useToggle } from 'hooks' import { useToggle } from 'hooks'
@ -62,7 +62,7 @@ export const useFullscreen = () => {
} }
const onFullscreenClick = () => { const onFullscreenClick = () => {
if (isIphone) { if (isIOS) {
toggleIOSFullscreen() toggleIOSFullscreen()
} else { } else {
toggleFullscreen() toggleFullscreen()

@ -11,23 +11,26 @@ import { streamConfig } from '../config'
export const useHlsPlayer = (src: string, resumeFrom?: number) => { export const useHlsPlayer = (src: string, resumeFrom?: number) => {
const hls = useMemo(() => { const hls = useMemo(() => {
if (!Hls.isSupported()) return null
const newStreamConfig = { ...streamConfig } const newStreamConfig = { ...streamConfig }
if (isNumber(resumeFrom)) { if (isNumber(resumeFrom)) {
newStreamConfig.startPosition = resumeFrom newStreamConfig.startPosition = resumeFrom
} }
return new Hls(newStreamConfig) return new Hls(newStreamConfig)
}, [resumeFrom]) }, [resumeFrom])
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => { useEffect(() => {
const video = videoRef.current const video = videoRef.current
if (!video) return if (!video || !hls) return
hls.loadSource(src) hls.loadSource(src)
hls.attachMedia(video) hls.attachMedia(video)
}, [src, hls]) }, [src, hls])
useEffect(() => () => hls.destroy(), [hls]) useEffect(() => () => hls?.destroy(), [hls])
return { return {
hls, hls,

@ -46,7 +46,7 @@ const getVideoQualities = (levels: Array<Hls.Level>) => {
return uniqBy([...sorted, autoQuality], 'label') return uniqBy([...sorted, autoQuality], 'label')
} }
export const useVideoQuality = (hls: Hls) => { export const useVideoQuality = (hls: Hls | null) => {
const [videoQualities, setVideoQualities] = useState([autoQuality]) const [videoQualities, setVideoQualities] = useState([autoQuality])
const [selectedQuality, setSelectedQuality] = useLocalStore({ const [selectedQuality, setSelectedQuality] = useLocalStore({
defaultValue: autoQuality.label, defaultValue: autoQuality.label,
@ -55,6 +55,8 @@ export const useVideoQuality = (hls: Hls) => {
}) })
const onQualitySelect = useCallback((label: string) => { const onQualitySelect = useCallback((label: string) => {
if (!hls) return
const quality = find(videoQualities, { label }) const quality = find(videoQualities, { label })
if (!quality || quality.level === hls.currentLevel) return if (!quality || quality.level === hls.currentLevel) return
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
@ -67,6 +69,8 @@ export const useVideoQuality = (hls: Hls) => {
]) ])
useEffect(() => { useEffect(() => {
if (!hls) return undefined
const listener = () => { const listener = () => {
const qualities = getVideoQualities(hls.levels) const qualities = getVideoQualities(hls.levels)
const quality = find(qualities, { label: selectedQuality }) || autoQuality const quality = find(qualities, { label: selectedQuality }) || autoQuality

@ -79,13 +79,11 @@ export const StreamPlayer = (props: Props) => {
onTouchStart={onTouchStart} onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd} onTouchEnd={onTouchEnd}
> >
{ {!ready && (
!ready && (
<LoaderWrapper> <LoaderWrapper>
<Loader color='#515151' /> <Loader color='#515151' />
</LoaderWrapper> </LoaderWrapper>
) )}
}
<VideoPlayer <VideoPlayer
width='100%' width='100%'
height='100%' height='100%'
@ -104,9 +102,7 @@ export const StreamPlayer = (props: Props) => {
onError={onError} onError={onError}
crossOrigin='use-credentials' crossOrigin='use-credentials'
/> />
{ready && (
{
ready && (
<CenterControls playing={playing}> <CenterControls playing={playing}>
<Backward size='lg' onClick={rewindBackward}> <Backward size='lg' onClick={rewindBackward}>
{REWIND_SECONDS} {REWIND_SECONDS}
@ -121,8 +117,7 @@ export const StreamPlayer = (props: Props) => {
{REWIND_SECONDS} {REWIND_SECONDS}
</Forward> </Forward>
</CenterControls> </CenterControls>
) )}
}
<Controls visible={controlsVisible}> <Controls visible={controlsVisible}>
<ControlsRow> <ControlsRow>
@ -142,9 +137,7 @@ export const StreamPlayer = (props: Props) => {
onChange={onVolumeChange} onChange={onVolumeChange}
onClick={onVolumeClick} onClick={onVolumeClick}
/> />
<PlaybackTime> <PlaybackTime>{secondsToHms(playedProgress / 1000)}</PlaybackTime>
{secondsToHms(playedProgress / 1000)}
</PlaybackTime>
<Backward onClick={rewindBackward}>{REWIND_SECONDS}</Backward> <Backward onClick={rewindBackward}>{REWIND_SECONDS}</Backward>
<Forward onClick={rewindForward}>{REWIND_SECONDS}</Forward> <Forward onClick={rewindForward}>{REWIND_SECONDS}</Forward>
</ControlsGroup> </ControlsGroup>
@ -156,7 +149,6 @@ export const StreamPlayer = (props: Props) => {
</LiveBtn> </LiveBtn>
) )
} }
<Fullscreen <Fullscreen
onClick={onFullscreenClick} onClick={onFullscreenClick}
isFullscreen={isFullscreen} isFullscreen={isFullscreen}

@ -0,0 +1,16 @@
import { Parser, Manifest } from 'm3u8-parser'
import reduce from 'lodash/reduce'
export const manifestParser = (manifest: string) => {
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
}

@ -5,6 +5,8 @@ import {
} from 'react' } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { isIOS } from 'config/userAgent'
import * as serviceWorker from './serviceWorker' import * as serviceWorker from './serviceWorker'
export const App = process.env.REACT_APP_TYPE === 'auth-service' export const App = process.env.REACT_APP_TYPE === 'auth-service'
@ -20,7 +22,21 @@ ReactDOM.render(
document.getElementById('root'), document.getElementById('root'),
) )
// If you want your app to work offline and load faster, you can change if (isIOS) {
// unregister() to register() below. Note this comes with some pitfalls. serviceWorker.register({
// Learn more about service workers: https://bit.ly/CRA-PWA onUpdate: (registration) => {
serviceWorker.unregister() 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()
}

@ -0,0 +1,51 @@
/// <reference lib="webworker" />
/* 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())
}
})

@ -1,4 +1,3 @@
/* eslint-disable */
// This optional code is used to register a service worker. // This optional code is used to register a service worker.
// register() is not called by default. // register() is not called by default.
@ -9,16 +8,14 @@
// resources are updated in the background. // resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to // 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( const isLocalhost = Boolean(
window.location.hostname === 'localhost' || window.location.hostname === 'localhost'
// [::1] is the IPv6 localhost address. // [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' || || window.location.hostname === '[::1]'
// 127.0.0.0/8 are considered localhost for IPv4. // 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match( || window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/),
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
) )
type Config = { type Config = {
@ -27,16 +24,13 @@ type Config = {
} }
export function register(config?: 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. // The URL constructor is available in all browsers that support SW.
const publicUrl = new URL( const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
process.env.PUBLIC_URL,
window.location.href
)
if (publicUrl.origin !== window.location.origin) { if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different 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 // 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 return
} }
@ -51,8 +45,8 @@ export function register(config?: Config) {
// service worker/PWA documentation. // service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log(
'This web app is being served cache-first by a service ' + 'This web app is being served cache-first by a service '
'worker. To learn more, visit https://bit.ly/CRA-PWA' + 'worker. To learn more, visit https://cra.link/PWA',
) )
}) })
} else { } else {
@ -62,11 +56,11 @@ export function register(config?: Config) {
}) })
} }
} }
/* eslint no-param-reassign: "error" */
function registerValidSW(swUrl: string, config?: Config) { function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker navigator.serviceWorker
.register(swUrl) .register(swUrl)
.then(registration => { .then((registration) => {
registration.onupdatefound = () => { registration.onupdatefound = () => {
const installingWorker = registration.installing const installingWorker = registration.installing
if (installingWorker == null) { if (installingWorker == null) {
@ -79,8 +73,8 @@ function registerValidSW(swUrl: string, config?: Config) {
// but the previous service worker will still serve the older // but the previous service worker will still serve the older
// content until all client tabs are closed. // content until all client tabs are closed.
console.log( console.log(
'New content is available and will be used when all ' + 'New content is available and will be used when all '
'tabs for this page are closed. See https://bit.ly/CRA-PWA.' + 'tabs for this page are closed. See https://cra.link/PWA.',
) )
// Execute callback // Execute callback
@ -102,7 +96,7 @@ function registerValidSW(swUrl: string, config?: Config) {
} }
} }
}) })
.catch(error => { .catch((error) => {
console.error('Error during service worker registration:', 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) { function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page. // Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, { 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. // Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type') const contentType = response.headers.get('content-type')
if ( if (
response.status === 404 || response.status === 404
(contentType != null && contentType.indexOf('javascript') === -1) || (contentType != null && contentType.indexOf('javascript') === -1)
) { ) {
// No service worker found. Probably a different app. Reload the page. // No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => { registration.unregister().then(() => {
window.location.reload() window.location.reload()
}) })
@ -131,19 +125,17 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
} }
}) })
.catch(() => { .catch(() => {
console.log( console.log('No internet connection found. App is running in offline mode.')
'No internet connection found. App is running in offline mode.'
)
}) })
} }
export function unregister() { export function unregister() {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready navigator.serviceWorker.ready
.then(registration => { .then((registration) => {
registration.unregister() registration.unregister()
}) })
.catch(error => { .catch((error) => {
console.error(error.message) console.error(error.message)
}) })
} }

@ -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,
}
}

@ -19,10 +19,7 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"noUnusedLocals": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": [ "include": ["src"]
"src"
]
} }

Loading…
Cancel
Save