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 restoringkeep-around/af30b88d367751c9e05a735e4a0467a96238ef47
parent
b61efae298
commit
9cc373a8d2
@ -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, |
|
||||||
} |
|
||||||
} |
|
||||||
@ -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%; |
|
||||||
` |
|
||||||
@ -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, |
||||||
|
} |
||||||
|
} |
||||||
@ -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, |
||||||
|
} |
||||||
|
} |
||||||
@ -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…
Reference in new issue