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"
},
"dependencies": {
"@reach/combobox": "^0.10.4",
"date-fns": "^2.14.0",
"history": "^4.10.1",
"hls.js": "^0.14.15",
@ -22,7 +21,6 @@
"react": "^16.13.1",
"react-datepicker": "^3.1.3",
"react-dom": "^16.13.1",
"react-player": "^2.6.0",
"react-responsive": "^8.1.0",
"react-router": "^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 { useEventListener } from 'hooks'
import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen'
import { useVolume } from 'features/VideoPlayer/hooks/useVolume'
import { useProgressChangeHandler } from './useProgressChangeHandler'
import { useLoadedEvent, usePlayedEvent } from './useProgressEvents'
import { usePlayingState } from './usePlayingState'
import { useVideoQuality } from './useVideoQuality'
import { useDuration } from './useDuration'
import { useVolume } from './useVolume'
import { useVideos } from './useVideos'
export type Props = {
@ -30,24 +28,24 @@ export type Props = {
}
export const useMultiSourcePlayer = ({
resumeFrom,
onError = () => {},
videos,
onError,
onPlayingChange,
onProgressChange: onProgressChangeCallback,
resumeFrom,
videos,
}: Props) => {
const activeChapterIndex = useRef(resumeFrom.half)
const wrapperRef = useRef<HTMLDivElement>(null)
const videoRef = useRef<HTMLVideoElement>(null)
const [seek, setSeek] = useState(resumeFrom.second)
const [loadedProgress, setLoadedProgress] = useState(0)
const [playedProgress, setPlayedProgress] = useState(0)
const {
continuePlaying,
onReady,
playing,
startPlaying,
stopPlaying,
togglePlaying,
} = usePlayingState(videoRef)
} = usePlayingState()
const {
selectedQuality,
@ -58,14 +56,17 @@ export const useMultiSourcePlayer = ({
const { chapters } = useVideos(videos)
const duration = useDuration(chapters)
const handleError = useCallback(() => {
stopPlaying()
onError?.()
}, [onError, stopPlaying])
const onProgressChange = useProgressChangeHandler({
activeChapterIndex,
chapters,
continuePlaying,
duration,
selectedQuality,
setPlayedProgress,
videoRef,
setSeek,
})
const getActiveChapterUrl = useCallback((quality: string = selectedQuality) => (
@ -77,9 +78,8 @@ export const useMultiSourcePlayer = ({
), [chapters])
const onQualitySelect = (quality: string) => {
const from = videoRef.current?.currentTime
setSeek(videoRef.current?.currentTime || 0)
setSelectedQuality(quality)
continuePlaying(getActiveChapterUrl(quality), from)
}
const playNextChapter = () => {
@ -87,9 +87,7 @@ export const useMultiSourcePlayer = ({
const isLastChapterPlayed = activeChapterIndex.current === size(chapters)
if (isLastChapterPlayed) {
activeChapterIndex.current = 0
stopPlaying(getActiveChapterUrl())
} else {
continuePlaying(getActiveChapterUrl())
stopPlaying()
}
}
@ -99,52 +97,16 @@ export const useMultiSourcePlayer = ({
}
}
const onLoadedChange = (loadedMs: number) => {
const onLoadedProgress = (loadedMs: number) => {
const chapterStart = getActiveChapterStart()
setLoadedProgress(chapterStart + loadedMs)
}
const onPlayedChange = (playedMs: number) => {
const onPlayedProgress = (playedMs: number) => {
const chapterStart = getActiveChapterStart()
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(() => {
onPlayingChange(playing)
}, [playing, onPlayingChange])
@ -160,20 +122,27 @@ export const useMultiSourcePlayer = ({
])
return {
activeSrc: getActiveChapterUrl(),
chapters,
duration,
loadedProgress,
onError: handleError,
onLoadedProgress,
onPlayedProgress,
onPlayerClick,
onProgressChange,
onQualitySelect,
onReady,
playNextChapter,
playedProgress,
playing,
seek,
selectedQuality,
startPlaying,
togglePlaying,
videoQualities,
videoRef,
wrapperRef,
...useFullscreen(wrapperRef),
...useVolume(videoRef),
...useFullscreen(),
...useVolume(),
}
}

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

@ -1,43 +1,24 @@
import type { MutableRefObject, RefObject } from 'react'
import {
useCallback,
} from 'react'
import throttle from 'lodash/throttle'
import type { MutableRefObject } from 'react'
import { useCallback } from 'react'
import type { Chapters } from '../types'
import {
findChapterByProgress,
} from '../helpers'
import { findChapterByProgress } from '../helpers'
type Args = {
activeChapterIndex: MutableRefObject<number>,
chapters: Chapters,
continuePlaying: (url: string) => void,
duration: number,
selectedQuality: string,
setPlayedProgress: (value: number) => void,
videoRef: RefObject<HTMLVideoElement>,
setSeek: (value: number) => void,
}
export const useProgressChangeHandler = ({
activeChapterIndex,
chapters,
continuePlaying,
duration,
selectedQuality,
setPlayedProgress,
videoRef,
setSeek,
}: Args) => {
const setPlayerProgress = useCallback(
throttle((value: number) => {
const video = videoRef.current
if (!video) return
video.currentTime = value
}, 100),
[],
)
const onProgressChange = useCallback((progress: number) => {
// значение новой позиции ползунка в миллисекундах
const progressMs = progress * duration
@ -49,7 +30,6 @@ export const useProgressChangeHandler = ({
)
// если ползунок остановили на другой главе
if (isProgressOnDifferentChapter) {
continuePlaying(chapter.urls[selectedQuality])
// eslint-disable-next-line no-param-reassign
activeChapterIndex.current = chapterIndex
}
@ -58,14 +38,12 @@ export const useProgressChangeHandler = ({
// отнимаем начало главы на котором остановились от общего прогресса
// чтобы получить прогресс текущей главы
const chapterProgressSec = (progressMs - chapter.startMs) / 1000
setPlayerProgress(chapterProgressSec)
setSeek(chapterProgressSec)
}, [
selectedQuality,
chapters,
duration,
continuePlaying,
setPlayedProgress,
setPlayerProgress,
setSeek,
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,
Fullscreen,
} from 'features/StreamPlayer/styled'
import { VideoPlayer } from 'features/VideoPlayer'
import { ProgressBar } from './components/ProgressBar'
import { Settings } from './components/Settings'
import type { Props } from './hooks'
import { useMultiSourcePlayer } from './hooks'
import { Video } from './styled'
export const MultiSourcePlayer = (props: Props) => {
const {
activeSrc,
chapters,
duration,
isFullscreen,
loadedProgress,
muted,
onError,
onFullscreenClick,
onLoadedProgress,
onPlayedProgress,
onPlayerClick,
onProgressChange,
onQualitySelect,
onReady,
onVolumeChange,
onVolumeClick,
playedProgress,
playing,
playNextChapter,
seek,
selectedQuality,
togglePlaying,
videoQualities,
videoRef,
volume,
volumeInPercent,
wrapperRef,
} = useMultiSourcePlayer(props)
@ -42,9 +50,18 @@ export const MultiSourcePlayer = (props: Props) => {
playing={playing}
onClick={onPlayerClick}
>
<Video
<VideoPlayer
src={activeSrc}
playing={playing}
muted={muted}
volume={volume}
ref={videoRef}
seek={seek}
onLoadedProgress={onLoadedProgress}
onPlayedProgress={onPlayedProgress}
onEnded={playNextChapter}
onError={onError}
onReady={onReady}
/>
<Controls>
<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 = {
file: {
forceHLS: true,
hlsOptions: {
liveSyncDuration: 10,
maxBufferLength: 10,
maxBufferSize: 0,
},
},
export const streamConfig: Partial<Hls.Config> = {
liveSyncDuration: 10,
maxBufferLength: 10,
maxBufferSize: 0,
}
export const progressCallbackInterval = 100

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

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

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

@ -1,7 +1,8 @@
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;
top: 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,
onVolumeClick,
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