Ott 493 player refactoring (#192)

* Ott 493 player refactoring part 1 (#190)

* chore(#493): turned off react prop types lint rule

* refactor(#493): replaced react-player with own video wrapper compoonent

* chore(#493): removed react-player and reach combobox

* Revert "chore(#493): turned off react prop types lint rule"

This reverts commit e449f859fa900aa24fafe2e28acf218852f7b1ad.

* fix(#493): fix props type

* Ott 493 player refactoring part 2 (#191)

* refactor(#493): used VideoPlayer in MultiSource player

* fix(#493): fixed selected quality restoring
keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Mirlan 5 years ago committed by GitHub
parent b61efae298
commit 9cc373a8d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      package.json
  2. 85
      src/features/MultiSourcePlayer/hooks/index.tsx
  3. 68
      src/features/MultiSourcePlayer/hooks/usePlayingState.tsx
  4. 36
      src/features/MultiSourcePlayer/hooks/useProgressChangeHandler.tsx
  5. 68
      src/features/MultiSourcePlayer/hooks/useProgressEvents.tsx
  6. 47
      src/features/MultiSourcePlayer/hooks/useVolume.tsx
  7. 21
      src/features/MultiSourcePlayer/index.tsx
  8. 9
      src/features/MultiSourcePlayer/styled.tsx
  9. 17
      src/features/StreamPlayer/config.tsx
  10. 82
      src/features/StreamPlayer/hooks/index.tsx
  11. 12
      src/features/StreamPlayer/hooks/useFullscreen.tsx
  12. 26
      src/features/StreamPlayer/hooks/useHlsPlayer.tsx
  13. 46
      src/features/StreamPlayer/hooks/useVideoQuality.tsx
  14. 28
      src/features/StreamPlayer/index.tsx
  15. 5
      src/features/StreamPlayer/styled.tsx
  16. 114
      src/features/VideoPlayer/hooks/index.tsx
  17. 48
      src/features/VideoPlayer/hooks/useProgressChange.tsx
  18. 1
      src/features/VideoPlayer/hooks/useVolume.tsx
  19. 40
      src/features/VideoPlayer/index.tsx
  20. 9
      src/features/VideoPlayer/styled.tsx

@ -13,7 +13,6 @@
"build-storybook": "build-storybook -s public" "build-storybook": "build-storybook -s public"
}, },
"dependencies": { "dependencies": {
"@reach/combobox": "^0.10.4",
"date-fns": "^2.14.0", "date-fns": "^2.14.0",
"history": "^4.10.1", "history": "^4.10.1",
"hls.js": "^0.14.15", "hls.js": "^0.14.15",
@ -22,7 +21,6 @@
"react": "^16.13.1", "react": "^16.13.1",
"react-datepicker": "^3.1.3", "react-datepicker": "^3.1.3",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-player": "^2.6.0",
"react-responsive": "^8.1.0", "react-responsive": "^8.1.0",
"react-router": "^5.2.0", "react-router": "^5.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",

@ -10,15 +10,13 @@ import size from 'lodash/size'
import type { LastPlayPosition, Videos } from 'requests' import type { LastPlayPosition, Videos } from 'requests'
import { useEventListener } from 'hooks'
import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen' import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen'
import { useVolume } from 'features/VideoPlayer/hooks/useVolume'
import { useProgressChangeHandler } from './useProgressChangeHandler' import { useProgressChangeHandler } from './useProgressChangeHandler'
import { useLoadedEvent, usePlayedEvent } from './useProgressEvents'
import { usePlayingState } from './usePlayingState' import { usePlayingState } from './usePlayingState'
import { useVideoQuality } from './useVideoQuality' import { useVideoQuality } from './useVideoQuality'
import { useDuration } from './useDuration' import { useDuration } from './useDuration'
import { useVolume } from './useVolume'
import { useVideos } from './useVideos' import { useVideos } from './useVideos'
export type Props = { export type Props = {
@ -30,24 +28,24 @@ export type Props = {
} }
export const useMultiSourcePlayer = ({ export const useMultiSourcePlayer = ({
resumeFrom, onError,
onError = () => {},
videos,
onPlayingChange, onPlayingChange,
onProgressChange: onProgressChangeCallback, onProgressChange: onProgressChangeCallback,
resumeFrom,
videos,
}: Props) => { }: Props) => {
const activeChapterIndex = useRef(resumeFrom.half) const activeChapterIndex = useRef(resumeFrom.half)
const wrapperRef = useRef<HTMLDivElement>(null)
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const [seek, setSeek] = useState(resumeFrom.second)
const [loadedProgress, setLoadedProgress] = useState(0) const [loadedProgress, setLoadedProgress] = useState(0)
const [playedProgress, setPlayedProgress] = useState(0) const [playedProgress, setPlayedProgress] = useState(0)
const { const {
continuePlaying, onReady,
playing, playing,
startPlaying, startPlaying,
stopPlaying, stopPlaying,
togglePlaying, togglePlaying,
} = usePlayingState(videoRef) } = usePlayingState()
const { const {
selectedQuality, selectedQuality,
@ -58,14 +56,17 @@ export const useMultiSourcePlayer = ({
const { chapters } = useVideos(videos) const { chapters } = useVideos(videos)
const duration = useDuration(chapters) const duration = useDuration(chapters)
const handleError = useCallback(() => {
stopPlaying()
onError?.()
}, [onError, stopPlaying])
const onProgressChange = useProgressChangeHandler({ const onProgressChange = useProgressChangeHandler({
activeChapterIndex, activeChapterIndex,
chapters, chapters,
continuePlaying,
duration, duration,
selectedQuality,
setPlayedProgress, setPlayedProgress,
videoRef, setSeek,
}) })
const getActiveChapterUrl = useCallback((quality: string = selectedQuality) => ( const getActiveChapterUrl = useCallback((quality: string = selectedQuality) => (
@ -77,9 +78,8 @@ export const useMultiSourcePlayer = ({
), [chapters]) ), [chapters])
const onQualitySelect = (quality: string) => { const onQualitySelect = (quality: string) => {
const from = videoRef.current?.currentTime setSeek(videoRef.current?.currentTime || 0)
setSelectedQuality(quality) setSelectedQuality(quality)
continuePlaying(getActiveChapterUrl(quality), from)
} }
const playNextChapter = () => { const playNextChapter = () => {
@ -87,9 +87,7 @@ export const useMultiSourcePlayer = ({
const isLastChapterPlayed = activeChapterIndex.current === size(chapters) const isLastChapterPlayed = activeChapterIndex.current === size(chapters)
if (isLastChapterPlayed) { if (isLastChapterPlayed) {
activeChapterIndex.current = 0 activeChapterIndex.current = 0
stopPlaying(getActiveChapterUrl()) stopPlaying()
} else {
continuePlaying(getActiveChapterUrl())
} }
} }
@ -99,52 +97,16 @@ export const useMultiSourcePlayer = ({
} }
} }
const onLoadedChange = (loadedMs: number) => { const onLoadedProgress = (loadedMs: number) => {
const chapterStart = getActiveChapterStart() const chapterStart = getActiveChapterStart()
setLoadedProgress(chapterStart + loadedMs) setLoadedProgress(chapterStart + loadedMs)
} }
const onPlayedChange = (playedMs: number) => { const onPlayedProgress = (playedMs: number) => {
const chapterStart = getActiveChapterStart() const chapterStart = getActiveChapterStart()
setPlayedProgress(chapterStart + playedMs) setPlayedProgress(chapterStart + playedMs)
} }
useEffect(() => {
const url = getActiveChapterUrl()
const chapterStartMs = getActiveChapterStart()
const from = resumeFrom.second - (chapterStartMs / 1000)
if (url) {
startPlaying(url, from)
}
}, [
resumeFrom,
getActiveChapterUrl,
getActiveChapterStart,
startPlaying,
])
useLoadedEvent({
onChange: onLoadedChange,
videoRef,
})
usePlayedEvent({
onChange: onPlayedChange,
videoRef,
})
useEventListener({
callback: playNextChapter,
event: 'ended',
target: videoRef,
})
useEventListener({
callback: onError,
event: 'error',
target: videoRef,
})
useEffect(() => { useEffect(() => {
onPlayingChange(playing) onPlayingChange(playing)
}, [playing, onPlayingChange]) }, [playing, onPlayingChange])
@ -160,20 +122,27 @@ export const useMultiSourcePlayer = ({
]) ])
return { return {
activeSrc: getActiveChapterUrl(),
chapters, chapters,
duration, duration,
loadedProgress, loadedProgress,
onError: handleError,
onLoadedProgress,
onPlayedProgress,
onPlayerClick, onPlayerClick,
onProgressChange, onProgressChange,
onQualitySelect, onQualitySelect,
onReady,
playNextChapter,
playedProgress, playedProgress,
playing, playing,
seek,
selectedQuality, selectedQuality,
startPlaying,
togglePlaying, togglePlaying,
videoQualities, videoQualities,
videoRef, videoRef,
wrapperRef, ...useFullscreen(),
...useFullscreen(wrapperRef), ...useVolume(),
...useVolume(videoRef),
} }
} }

@ -1,64 +1,32 @@
import type { RefObject } from 'react'
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
import { preparePlayer } from '../helpers' export const usePlayingState = () => {
const [ready, setReady] = useState(false)
export const usePlayingState = (videoRef: RefObject<HTMLVideoElement>) => {
const [playing, setPlaying] = useState(false) const [playing, setPlaying] = useState(false)
const togglePlaying = useCallback(() => { const onReady = useCallback(() => {
setPlaying((isPlaying) => { if (ready) return
const video = videoRef.current
if (!video) return false setReady(true)
setPlaying(true)
}, [ready])
const nextIsPlaying = !isPlaying const togglePlaying = useCallback(() => {
if (nextIsPlaying) { setPlaying((isPlaying) => (ready ? !isPlaying : false))
video.play() }, [ready])
} else {
video.pause()
}
return nextIsPlaying
})
}, [videoRef])
const stopPlaying = useCallback((nextUrl: string = '') => { const stopPlaying = useCallback(() => {
preparePlayer({ url: nextUrl, videoRef })
setPlaying(false) setPlaying(false)
}, [videoRef]) }, [])
const startPlaying = useCallback((url: string, from?: number) => { const startPlaying = useCallback(() => {
preparePlayer({ if (ready) {
from,
url,
videoRef,
})
// автовоспроизведение со звуком иногда может не сработать
// https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#new-behaviors
videoRef.current?.play().then(() => {
setPlaying(true) setPlaying(true)
}) }
}, [videoRef]) }, [ready])
const continuePlaying = useCallback((
url: string,
from?: number,
) => {
preparePlayer({
from,
url,
videoRef,
})
setPlaying((isPlaying) => {
if (isPlaying) {
videoRef.current?.play()
}
return isPlaying
})
}, [videoRef])
return { return {
continuePlaying, onReady,
playing, playing,
startPlaying, startPlaying,
stopPlaying, stopPlaying,

@ -1,43 +1,24 @@
import type { MutableRefObject, RefObject } from 'react' import type { MutableRefObject } from 'react'
import { import { useCallback } from 'react'
useCallback,
} from 'react'
import throttle from 'lodash/throttle'
import type { Chapters } from '../types' import type { Chapters } from '../types'
import { import { findChapterByProgress } from '../helpers'
findChapterByProgress,
} from '../helpers'
type Args = { type Args = {
activeChapterIndex: MutableRefObject<number>, activeChapterIndex: MutableRefObject<number>,
chapters: Chapters, chapters: Chapters,
continuePlaying: (url: string) => void,
duration: number, duration: number,
selectedQuality: string,
setPlayedProgress: (value: number) => void, setPlayedProgress: (value: number) => void,
videoRef: RefObject<HTMLVideoElement>, setSeek: (value: number) => void,
} }
export const useProgressChangeHandler = ({ export const useProgressChangeHandler = ({
activeChapterIndex, activeChapterIndex,
chapters, chapters,
continuePlaying,
duration, duration,
selectedQuality,
setPlayedProgress, setPlayedProgress,
videoRef, setSeek,
}: Args) => { }: Args) => {
const setPlayerProgress = useCallback(
throttle((value: number) => {
const video = videoRef.current
if (!video) return
video.currentTime = value
}, 100),
[],
)
const onProgressChange = useCallback((progress: number) => { const onProgressChange = useCallback((progress: number) => {
// значение новой позиции ползунка в миллисекундах // значение новой позиции ползунка в миллисекундах
const progressMs = progress * duration const progressMs = progress * duration
@ -49,7 +30,6 @@ export const useProgressChangeHandler = ({
) )
// если ползунок остановили на другой главе // если ползунок остановили на другой главе
if (isProgressOnDifferentChapter) { if (isProgressOnDifferentChapter) {
continuePlaying(chapter.urls[selectedQuality])
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
activeChapterIndex.current = chapterIndex activeChapterIndex.current = chapterIndex
} }
@ -58,14 +38,12 @@ export const useProgressChangeHandler = ({
// отнимаем начало главы на котором остановились от общего прогресса // отнимаем начало главы на котором остановились от общего прогресса
// чтобы получить прогресс текущей главы // чтобы получить прогресс текущей главы
const chapterProgressSec = (progressMs - chapter.startMs) / 1000 const chapterProgressSec = (progressMs - chapter.startMs) / 1000
setPlayerProgress(chapterProgressSec) setSeek(chapterProgressSec)
}, [ }, [
selectedQuality,
chapters, chapters,
duration, duration,
continuePlaying,
setPlayedProgress, setPlayedProgress,
setPlayerProgress, setSeek,
activeChapterIndex, activeChapterIndex,
]) ])

@ -1,68 +0,0 @@
import type { RefObject } from 'react'
import size from 'lodash/size'
import { useEventListener } from 'hooks'
/**
* Возвращает значение отрезка буфера которое ближе
* к проигрываемому времени
*/
const getLoadedSeconds = (video: HTMLVideoElement | null) => {
const bufferLength = size(video?.buffered)
if (!video || bufferLength === 0) return 0
for (let i = 0; i < bufferLength; i++) {
const bufferPosition = bufferLength - 1 - i
if (video.buffered.start(bufferPosition) < video.currentTime) {
return video.buffered.end(bufferPosition)
}
}
return 0
}
type Args = {
onChange: (millis: number) => void,
videoRef: RefObject<HTMLVideoElement>,
}
/**
* Подписывается на прогресс буферизации и вызывает колбек
* со значением прогресса в милисекундах
*/
export const useLoadedEvent = ({
onChange,
videoRef,
}: Args) => {
const listenLoadedProgress = () => {
const loadedSeconds = getLoadedSeconds(videoRef.current)
onChange(loadedSeconds * 1000)
}
useEventListener({
callback: listenLoadedProgress,
event: 'progress',
target: videoRef,
})
}
/**
* Подписывается на прогресс проигывания и вызывает колбек
* со значением прогресса в милисекундах
*/
export const usePlayedEvent = ({
onChange,
videoRef,
}: Args) => {
const listenPlayedProgresses = () => {
const video = videoRef.current
if (!video) return
onChange(video.currentTime * 1000)
}
useEventListener({
callback: listenPlayedProgresses,
event: 'timeupdate',
target: videoRef,
})
}

@ -1,47 +0,0 @@
import type { RefObject } from 'react'
import { useEffect, useRef } from 'react'
import isNumber from 'lodash/isNumber'
import { useLocalStore } from 'hooks'
const defaultVolume = 1
const useVolumeState = (videoRef: RefObject<HTMLVideoElement>) => {
const [volume, setVolume] = useLocalStore({
defaultValue: defaultVolume,
key: 'player_volume',
validator: isNumber,
})
useEffect(() => {
if (videoRef.current) {
// eslint-disable-next-line no-param-reassign
videoRef.current.volume = volume
}
}, [volume, videoRef])
return [volume, setVolume] as const
}
export const useVolume = (videoRef: RefObject<HTMLVideoElement>) => {
const prevVolumeRef = useRef(defaultVolume)
const [volume, setVolume] = useVolumeState(videoRef)
const onVolumeChange = (value: number) => {
setVolume(value)
}
const onVolumeClick = () => {
setVolume(volume === 0 ? prevVolumeRef.current : 0)
prevVolumeRef.current = volume || defaultVolume
}
return {
muted: volume === 0,
onVolumeChange,
onVolumeClick,
volume,
volumeInPercent: volume * 100,
}
}

@ -7,32 +7,40 @@ import {
PlayStop, PlayStop,
Fullscreen, Fullscreen,
} from 'features/StreamPlayer/styled' } from 'features/StreamPlayer/styled'
import { VideoPlayer } from 'features/VideoPlayer'
import { ProgressBar } from './components/ProgressBar' import { ProgressBar } from './components/ProgressBar'
import { Settings } from './components/Settings' import { Settings } from './components/Settings'
import type { Props } from './hooks' import type { Props } from './hooks'
import { useMultiSourcePlayer } from './hooks' import { useMultiSourcePlayer } from './hooks'
import { Video } from './styled'
export const MultiSourcePlayer = (props: Props) => { export const MultiSourcePlayer = (props: Props) => {
const { const {
activeSrc,
chapters, chapters,
duration, duration,
isFullscreen, isFullscreen,
loadedProgress, loadedProgress,
muted, muted,
onError,
onFullscreenClick, onFullscreenClick,
onLoadedProgress,
onPlayedProgress,
onPlayerClick, onPlayerClick,
onProgressChange, onProgressChange,
onQualitySelect, onQualitySelect,
onReady,
onVolumeChange, onVolumeChange,
onVolumeClick, onVolumeClick,
playedProgress, playedProgress,
playing, playing,
playNextChapter,
seek,
selectedQuality, selectedQuality,
togglePlaying, togglePlaying,
videoQualities, videoQualities,
videoRef, videoRef,
volume,
volumeInPercent, volumeInPercent,
wrapperRef, wrapperRef,
} = useMultiSourcePlayer(props) } = useMultiSourcePlayer(props)
@ -42,9 +50,18 @@ export const MultiSourcePlayer = (props: Props) => {
playing={playing} playing={playing}
onClick={onPlayerClick} onClick={onPlayerClick}
> >
<Video <VideoPlayer
src={activeSrc}
playing={playing}
muted={muted} muted={muted}
volume={volume}
ref={videoRef} ref={videoRef}
seek={seek}
onLoadedProgress={onLoadedProgress}
onPlayedProgress={onPlayedProgress}
onEnded={playNextChapter}
onError={onError}
onReady={onReady}
/> />
<Controls> <Controls>
<PlayStop onClick={togglePlaying} playing={playing} /> <PlayStop onClick={togglePlaying} playing={playing} />

@ -1,9 +0,0 @@
import styled from 'styled-components/macro'
export const Video = styled.video`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
`

@ -1,14 +1,7 @@
import type { Config } from 'react-player' import Hls from 'hls.js'
export const streamConfig: Config = { export const streamConfig: Partial<Hls.Config> = {
file: { liveSyncDuration: 10,
forceHLS: true, maxBufferLength: 10,
hlsOptions: { maxBufferSize: 0,
liveSyncDuration: 10,
maxBufferLength: 10,
maxBufferSize: 0,
},
},
} }
export const progressCallbackInterval = 100

@ -1,24 +1,13 @@
import type { MouseEvent } from 'react' import type { MouseEvent } from 'react'
import { import { useCallback, useState } from 'react'
useCallback,
useState,
useRef,
} from 'react'
import ReactPlayer from 'react-player'
import once from 'lodash/once' import once from 'lodash/once'
import throttle from 'lodash/throttle'
import { useVolume } from 'features/VideoPlayer/hooks/useVolume'
import { useHlsPlayer } from './useHlsPlayer'
import { useVideoQuality } from './useVideoQuality' import { useVideoQuality } from './useVideoQuality'
import { useFullscreen } from './useFullscreen' import { useFullscreen } from './useFullscreen'
import { useVolume } from './useVolume'
type ProgressState = {
loaded: number,
loadedSeconds: number,
played: number,
playedSeconds: number,
}
export type Props = { export type Props = {
onPlayingChange: (playing: boolean) => void, onPlayingChange: (playing: boolean) => void,
@ -33,25 +22,21 @@ export const useVideoPlayer = ({
onPlayingChange, onPlayingChange,
onProgressChange: progressChangeCallback, onProgressChange: progressChangeCallback,
resumeFrom, resumeFrom,
url,
}: Props) => { }: Props) => {
const { hls, videoRef } = useHlsPlayer(url)
const [ready, setReady] = useState(false) const [ready, setReady] = useState(false)
const [playing, setPlaying] = useState(false) const [playing, setPlaying] = useState(false)
const [duration, setDuration] = useState(0) const [duration, setDuration] = useState(0)
const [seek, setSeek] = useState(resumeFrom)
const [loadedProgress, setLoadedProgress] = useState(0) const [loadedProgress, setLoadedProgress] = useState(0)
const [playedProgress, setPlayedProgress] = useState(toMilliSeconds(resumeFrom)) const [playedProgress, setPlayedProgress] = useState(toMilliSeconds(resumeFrom))
const wrapperRef = useRef<HTMLDivElement>(null)
const playerRef = useRef<ReactPlayer>(null)
const startPlaying = useCallback(once(() => { const startPlaying = useCallback(once(() => {
setReady(true) setReady(true)
// автовоспроизведение со звуком иногда может не сработать setPlaying(true)
// https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#new-behaviors onPlayingChange(true)
const video = playerRef.current?.getInternalPlayer() as HTMLVideoElement | undefined
video?.play().then(() => {
setPlaying(true)
onPlayingChange(true)
})
}), []) }), [])
const togglePlaying = () => { const togglePlaying = () => {
@ -61,19 +46,23 @@ export const useVideoPlayer = ({
} }
} }
const onReady = useCallback(() => {
if (ready) return
setReady(true)
setPlaying(true)
}, [ready])
const onError = useCallback(() => {
setPlaying(false)
}, [])
const onPlayerClick = (e: MouseEvent<HTMLDivElement>) => { const onPlayerClick = (e: MouseEvent<HTMLDivElement>) => {
if (e.target === playerRef.current?.getInternalPlayer()) { if (e.target === videoRef.current) {
togglePlaying() togglePlaying()
} }
} }
const setPlayerProgress = useCallback(
throttle((value: number) => {
playerRef.current?.seekTo(value, 'fraction')
}, 100),
[],
)
const onDuration = (durationSeconds: number) => { const onDuration = (durationSeconds: number) => {
setDuration(toMilliSeconds(durationSeconds)) setDuration(toMilliSeconds(durationSeconds))
} }
@ -81,39 +70,38 @@ export const useVideoPlayer = ({
const onProgressChange = useCallback((progress: number) => { const onProgressChange = useCallback((progress: number) => {
const progressMs = progress * duration const progressMs = progress * duration
setPlayedProgress(progressMs) setPlayedProgress(progressMs)
setPlayerProgress(progress) setSeek(progressMs / 1000)
}, [ }, [
duration, duration,
setSeek,
setPlayedProgress, setPlayedProgress,
setPlayerProgress,
]) ])
const setProgress = ({ const onLoadedProgress = setLoadedProgress
loadedSeconds, const onPlayedProgress = (playedMs: number) => {
playedSeconds, setPlayedProgress(playedMs)
}: ProgressState) => { progressChangeCallback(playedMs / 1000)
setLoadedProgress(toMilliSeconds(loadedSeconds))
setPlayedProgress(toMilliSeconds(playedSeconds))
progressChangeCallback(playedSeconds)
} }
return { return {
duration, duration,
loadedProgress, loadedProgress,
onDuration, onDuration,
onError,
onLoadedProgress,
onPlayedProgress,
onPlayerClick, onPlayerClick,
onProgressChange, onProgressChange,
onReady,
playedProgress, playedProgress,
playerRef,
playing, playing,
setProgress, seek,
setReady, setReady,
startPlaying, startPlaying,
togglePlaying, togglePlaying,
wrapperRef, videoRef,
...useFullscreen(wrapperRef), ...useFullscreen(),
...useVolume(), ...useVolume(),
...useVideoQuality(playerRef), ...useVideoQuality(hls),
} }
} }

@ -1,10 +1,10 @@
import type { RefObject } from 'react' import { useEffect, useRef } from 'react'
import { useEffect } from 'react'
import screenfull from 'screenfull' import screenfull from 'screenfull'
import { useToggle } from 'hooks' import { useToggle } from 'hooks'
export const useFullscreen = (wrapperRef: RefObject<HTMLDivElement>) => { export const useFullscreen = () => {
const wrapperRef = useRef<HTMLDivElement>(null)
const { const {
isOpen: isFullscreen, isOpen: isFullscreen,
setIsOpen: setFullscreen, setIsOpen: setFullscreen,
@ -30,5 +30,9 @@ export const useFullscreen = (wrapperRef: RefObject<HTMLDivElement>) => {
} }
} }
return { isFullscreen, onFullscreenClick } return {
isFullscreen,
onFullscreenClick,
wrapperRef,
}
} }

@ -0,0 +1,26 @@
import {
useEffect,
useMemo,
useRef,
} from 'react'
import Hls from 'hls.js'
import { streamConfig } from '../config'
export const useHlsPlayer = (src: string) => {
const hls = useMemo(() => new Hls(streamConfig), [])
const videoRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
const video = videoRef.current
if (!video) return
hls.loadSource(src)
hls.attachMedia(video)
}, [src, hls])
return {
hls,
videoRef,
}
}

@ -1,10 +1,8 @@
import type { RefObject } from 'react'
import { import {
useMemo, useState,
useEffect, useEffect,
useCallback, useCallback,
} from 'react' } from 'react'
import ReactPlayer from 'react-player'
import Hls from 'hls.js' import Hls from 'hls.js'
@ -22,10 +20,10 @@ const autoQuality = {
level: -1, level: -1,
} }
const getVideoQualities = (hls: Hls | null) => { const getVideoQualities = (levels: Array<Hls.Level>) => {
if (isEmpty(hls?.levels)) return [] if (isEmpty(levels)) return []
const qualities = map(hls?.levels, (level, i) => ({ const qualities = map(levels, (level, i) => ({
label: String(level.height), label: String(level.height),
level: i, level: i,
})) }))
@ -37,9 +35,8 @@ const getVideoQualities = (hls: Hls | null) => {
return uniqBy([...sorted, autoQuality], 'label') return uniqBy([...sorted, autoQuality], 'label')
} }
export const useVideoQuality = (ref: RefObject<ReactPlayer>) => { export const useVideoQuality = (hls: Hls) => {
const hls = ref.current?.getInternalPlayer('hls') as Hls | null const [videoQualities, setVideoQualities] = useState([autoQuality])
const videoQualities = useMemo(() => getVideoQualities(hls), [hls])
const [selectedQuality, setSelectedQuality] = useLocalStore({ const [selectedQuality, setSelectedQuality] = useLocalStore({
defaultValue: autoQuality.label, defaultValue: autoQuality.label,
key: 'player_quality', key: 'player_quality',
@ -47,30 +44,35 @@ export const useVideoQuality = (ref: RefObject<ReactPlayer>) => {
}) })
const onQualitySelect = useCallback((label: string) => { const onQualitySelect = useCallback((label: string) => {
const item = find(videoQualities, { label }) const quality = find(videoQualities, { label })
if (hls && item) { if (!quality || quality.level === hls.currentLevel) return
hls.currentLevel = item.level // eslint-disable-next-line no-param-reassign
setSelectedQuality(item.label) hls.currentLevel = quality.level
} setSelectedQuality(quality.label)
}, [ }, [
setSelectedQuality, setSelectedQuality,
hls,
videoQualities, videoQualities,
hls,
]) ])
useEffect(() => { useEffect(() => {
if (!hls) return const listener = () => {
const qualities = getVideoQualities(hls.levels)
const quality = find(videoQualities, { label: selectedQuality }) || autoQuality const quality = find(qualities, { label: selectedQuality }) || autoQuality
if (quality.level === hls.currentLevel) return // eslint-disable-next-line no-param-reassign
hls.currentLevel = quality.level
setSelectedQuality(quality.label)
setVideoQualities(qualities)
}
hls.on(Hls.Events.MANIFEST_PARSED, listener)
hls.currentLevel = quality.level return () => {
setSelectedQuality(quality.label) hls.off(Hls.Events.MANIFEST_PARSED, listener)
}
}, [ }, [
selectedQuality, selectedQuality,
setSelectedQuality, setSelectedQuality,
videoQualities,
hls, hls,
]) ])

@ -2,14 +2,13 @@ import React from 'react'
import { Settings } from 'features/MultiSourcePlayer/components/Settings' import { Settings } from 'features/MultiSourcePlayer/components/Settings'
import { streamConfig, progressCallbackInterval } from './config'
import type { Props } from './hooks' import type { Props } from './hooks'
import { useVideoPlayer } from './hooks' import { useVideoPlayer } from './hooks'
import { ProgressBar } from './components/ProgressBar' import { ProgressBar } from './components/ProgressBar'
import { VolumeBar } from './components/VolumeBar' import { VolumeBar } from './components/VolumeBar'
import { import {
PlayerWrapper, PlayerWrapper,
ReactPlayer, VideoPlayer,
Controls, Controls,
PlayStop, PlayStop,
Fullscreen, Fullscreen,
@ -23,46 +22,49 @@ export const StreamPlayer = (props: Props) => {
loadedProgress, loadedProgress,
muted, muted,
onDuration, onDuration,
onError,
onFullscreenClick, onFullscreenClick,
onLoadedProgress,
onPlayedProgress,
onPlayerClick, onPlayerClick,
onProgressChange, onProgressChange,
onQualitySelect, onQualitySelect,
onVolumeChange, onVolumeChange,
onVolumeClick, onVolumeClick,
playedProgress, playedProgress,
playerRef,
playing, playing,
seek,
selectedQuality, selectedQuality,
setProgress,
startPlaying, startPlaying,
togglePlaying, togglePlaying,
videoQualities, videoQualities,
videoRef,
volume, volume,
volumeInPercent,
wrapperRef, wrapperRef,
} = useVideoPlayer(props) } = useVideoPlayer(props)
const volumeInPercent = volume * 100
return ( return (
<PlayerWrapper <PlayerWrapper
ref={wrapperRef} ref={wrapperRef}
playing={playing} playing={playing}
onClick={onPlayerClick} onClick={onPlayerClick}
> >
<ReactPlayer <VideoPlayer
width='100%' width='100%'
height='100%' height='100%'
url={url} src={url}
ref={playerRef} ref={videoRef}
playing={playing} playing={playing}
volume={volume} volume={volume}
muted={muted} muted={muted}
config={streamConfig} seek={seek}
progressInterval={progressCallbackInterval} onLoadedProgress={onLoadedProgress}
onProgress={setProgress} onPlayedProgress={onPlayedProgress}
onDuration={onDuration} onDurationChange={onDuration}
onEnded={togglePlaying} onEnded={togglePlaying}
onReady={startPlaying} onReady={startPlaying}
onError={onError}
/> />
<Controls> <Controls>
<PlayStop onClick={togglePlaying} playing={playing} /> <PlayStop onClick={togglePlaying} playing={playing} />

@ -1,7 +1,8 @@
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import ReactPlayerBase from 'react-player'
export const ReactPlayer = styled(ReactPlayerBase)` import { VideoPlayer as VideoPlayerBase } from 'features/VideoPlayer'
export const VideoPlayer = styled(VideoPlayerBase)`
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;

@ -0,0 +1,114 @@
import type { ForwardRefRenderFunction, SyntheticEvent } from 'react'
import {
useEffect,
useState,
useRef,
} from 'react'
import isUndefined from 'lodash/isUndefined'
import isNumber from 'lodash/isNumber'
import { useProgressChange } from './useProgressChange'
type Ref = Parameters<ForwardRefRenderFunction<HTMLVideoElement>>[1]
export type Props = {
className?: string,
height?: string,
muted?: boolean,
onDurationChange?: (durationMs: number) => void,
onEnded?: (e: SyntheticEvent<HTMLVideoElement>) => void,
onError?: (e?: SyntheticEvent<HTMLVideoElement>) => void,
onLoadedProgress?: (loadedMs: number) => void,
onPlayedProgress?: (playedMs: number) => void,
onReady?: () => void,
playing?: boolean,
ref?: Ref,
seek?: number | null,
src: string,
volume?: number,
width?: string,
}
const useVideoRef = (ref?: Ref) => {
const videoRef = useRef<HTMLVideoElement>(null)
if (ref && typeof ref === 'object') return ref
return videoRef
}
export const useVideoPlayer = ({
onDurationChange,
onError,
onLoadedProgress,
onPlayedProgress,
onReady,
playing,
ref,
seek = null,
src,
volume,
}: Props) => {
const [ready, setReady] = useState(false)
const videoRef = useVideoRef(ref)
const handleReady = () => {
setReady(true)
onReady?.()
}
const handleDurationChange = () => {
onDurationChange?.(videoRef.current?.duration || 0)
}
useEffect(() => {
const video = videoRef.current
if (!video) return
video.src = src
video.load()
}, [src, videoRef])
useEffect(() => {
const video = videoRef.current
if (video && isNumber(seek)) {
video.currentTime = seek
}
}, [seek, videoRef])
useEffect(() => {
const video = videoRef.current
if (!video?.src || !ready || isUndefined(playing)) return
if (playing) {
// автовоспроизведение со звуком иногда может не сработать
// https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#new-behaviors
video.play().catch(onError)
} else {
video.pause()
}
}, [
ready,
playing,
src,
onError,
videoRef,
])
useEffect(() => {
const video = videoRef.current
if (video) {
video.volume = volume ?? 1
}
}, [volume, videoRef])
return {
handleDurationChange,
handleReady,
videoRef,
...useProgressChange({
onLoadedProgress,
onPlayedProgress,
videoRef,
}),
}
}

@ -0,0 +1,48 @@
import { RefObject } from 'react'
import size from 'lodash/size'
/**
* Возвращает значение отрезка буфера которое ближе
* к проигрываемому времени
*/
const getLoadedSeconds = (video: HTMLVideoElement | null) => {
const bufferLength = size(video?.buffered)
if (!video || bufferLength === 0) return 0
for (let i = 0; i < bufferLength; i++) {
const bufferPosition = bufferLength - 1 - i
if (video.buffered.start(bufferPosition) < video.currentTime) {
return video.buffered.end(bufferPosition)
}
}
return 0
}
type Args = {
onLoadedProgress?: (millis: number) => void,
onPlayedProgress?: (millis: number) => void,
videoRef: RefObject<HTMLVideoElement>,
}
export const useProgressChange = ({
onLoadedProgress,
onPlayedProgress,
videoRef,
}: Args) => {
const handleLoadedChange = () => {
const loadedSeconds = getLoadedSeconds(videoRef.current)
onLoadedProgress?.(loadedSeconds * 1000)
}
const handlePlayedChange = () => {
const video = videoRef.current
if (!video) return
onPlayedProgress?.(video.currentTime * 1000)
}
return {
handleLoadedChange,
handlePlayedChange,
}
}

@ -28,5 +28,6 @@ export const useVolume = () => {
onVolumeChange, onVolumeChange,
onVolumeClick, onVolumeClick,
volume, volume,
volumeInPercent: volume * 100,
} }
} }

@ -0,0 +1,40 @@
import React, { forwardRef } from 'react'
import type { Props } from './hooks'
import { useVideoPlayer } from './hooks'
import { Video } from './styled'
export const VideoPlayer = forwardRef<HTMLVideoElement, Props>((props: Props, ref) => {
const {
className,
height,
onEnded,
onError,
src,
volume,
width,
} = props
const {
handleDurationChange,
handleLoadedChange,
handlePlayedChange,
handleReady,
videoRef,
} = useVideoPlayer({ ...props, ref })
return (
<Video
className={className}
width={width}
height={height}
ref={videoRef}
src={src}
muted={volume === 0}
onCanPlay={handleReady}
onTimeUpdate={handlePlayedChange}
onProgress={handleLoadedChange}
onEnded={onEnded}
onDurationChange={handleDurationChange}
onError={onError}
/>
)
})

@ -0,0 +1,9 @@
import styled from 'styled-components/macro'
export const Video = styled.video`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
`
Loading…
Cancel
Save