From 14cbfaf47e14a86c6fa859d854d7cc11e8cfee6c Mon Sep 17 00:00:00 2001 From: Mirlan Date: Mon, 21 Sep 2020 14:17:52 +0600 Subject: [PATCH] feat(#454): added video quality selector (#155) * feat(#454): added video quality selector * refactor(#454): pr comments fix Co-authored-by: mirlan.maksitaliev --- src/features/MatchPage/index.tsx | 12 +++- .../components/ProgressBar/stories.tsx | 6 +- .../components/Settings/hooks.tsx | 27 +++++++++ .../components/Settings/index.tsx | 44 +++++++++++++++ .../components/Settings/styled.tsx | 53 ++++++++++++++++++ .../MultiSourcePlayer/helpers/index.tsx | 24 +++++--- .../MultiSourcePlayer/hooks/index.tsx | 56 +++++++++++++------ .../hooks/usePlayingState.tsx | 53 ++++++++++++------ .../hooks/useProgressChangeHandler.tsx | 17 +++--- .../hooks/useVideoQuality.tsx | 27 +++++++++ .../MultiSourcePlayer/hooks/useVideos.tsx | 41 ++++++++++---- src/features/MultiSourcePlayer/index.tsx | 9 +++ src/features/MultiSourcePlayer/types.tsx | 4 +- src/features/StreamPlayer/styled.tsx | 3 +- 14 files changed, 308 insertions(+), 68 deletions(-) create mode 100644 src/features/MultiSourcePlayer/components/Settings/hooks.tsx create mode 100644 src/features/MultiSourcePlayer/components/Settings/index.tsx create mode 100644 src/features/MultiSourcePlayer/components/Settings/styled.tsx create mode 100644 src/features/MultiSourcePlayer/hooks/useVideoQuality.tsx diff --git a/src/features/MatchPage/index.tsx b/src/features/MatchPage/index.tsx index 3ec24104..13564817 100644 --- a/src/features/MatchPage/index.tsx +++ b/src/features/MatchPage/index.tsx @@ -1,5 +1,7 @@ import React from 'react' +import isEmpty from 'lodash/isEmpty' + import { StreamPlayer } from 'features/StreamPlayer' import { MatchStatuses } from 'features/HeaderFilters' import { MultiSourcePlayer } from 'features/MultiSourcePlayer' @@ -15,12 +17,18 @@ export const MatchPage = () => { const { url, videos } = useVideoData(profile?.stream_status) const { onPlayerProgressChange, onPlayingChange } = usePlayerProgressReporter() + const isLiveMatch = profile?.stream_status === MatchStatuses.Live + const isFinishedMatch = ( + profile?.stream_status === MatchStatuses.Finished + && !isEmpty(videos) + ) + return ( { - profile?.stream_status === MatchStatuses.Live && ( + isLiveMatch && ( { ) } { - profile?.stream_status === MatchStatuses.Finished && ( + isFinishedMatch && ( void, + selectedQuality: string, + videoQualities: Array, +} + +export const useSettings = ({ onSelect }: Props) => { + const { + close, + isOpen, + open, + } = useToggle() + + const onItemClick = (quality: string) => { + onSelect(quality) + close() + } + + return { + close, + isOpen, + onItemClick, + open, + } +} diff --git a/src/features/MultiSourcePlayer/components/Settings/index.tsx b/src/features/MultiSourcePlayer/components/Settings/index.tsx new file mode 100644 index 00000000..2e021ff6 --- /dev/null +++ b/src/features/MultiSourcePlayer/components/Settings/index.tsx @@ -0,0 +1,44 @@ +import React, { Fragment } from 'react' + +import map from 'lodash/map' + +import { SettingsButton } from 'features/StreamPlayer/styled' +import { OutsideClick } from 'features/OutsideClick' + +import type { Props } from './hooks' +import { useSettings } from './hooks' +import { QualitiesList, QualityItem } from './styled' + +export const Settings = (props: Props) => { + const { selectedQuality, videoQualities } = props + const { + close, + isOpen, + onItemClick, + open, + } = useSettings(props) + return ( + + + { + isOpen && ( + + + { + map(videoQualities, (quality) => ( + onItemClick(quality)} + > + {quality} + + )) + } + + + ) + } + + ) +} diff --git a/src/features/MultiSourcePlayer/components/Settings/styled.tsx b/src/features/MultiSourcePlayer/components/Settings/styled.tsx new file mode 100644 index 00000000..b9d5d166 --- /dev/null +++ b/src/features/MultiSourcePlayer/components/Settings/styled.tsx @@ -0,0 +1,53 @@ +import styled, { css } from 'styled-components/macro' + +export const QualitiesList = styled.ul` + position: absolute; + z-index: 1; + bottom: calc(100% + 14px); + right: 24px; + width: 52px; + list-style: none; + border-radius: 2px; + background-color: rgba(0, 0, 0, 0.5); + overflow: hidden; +` + +type QualityItemProps = { + active: boolean, +} + +const activeIcon = css` + :before { + position: absolute; + top: 45%; + transform: rotate(-45deg); + content: ''; + left: 8px; + width: 5px; + height: 3px; + border-left: 1px solid #fff; + border-bottom: 1px solid #fff; + } +` + +export const QualityItem = styled.li` + width: 100%; + padding: 5px 8px; + text-align: right; + font-style: normal; + font-weight: normal; + font-family: Montserrat; + font-size: 10px; + line-height: 12px; + letter-spacing: 0.01em; + text-transform: uppercase; + color: #fff; + cursor: pointer; + position: relative; + + :hover, :focus { + background-color: rgba(255, 255, 255, 0.1); + } + + ${({ active }) => (active ? activeIcon : '')} +` diff --git a/src/features/MultiSourcePlayer/helpers/index.tsx b/src/features/MultiSourcePlayer/helpers/index.tsx index 638dfc48..f1b24072 100644 --- a/src/features/MultiSourcePlayer/helpers/index.tsx +++ b/src/features/MultiSourcePlayer/helpers/index.tsx @@ -2,21 +2,31 @@ import { RefObject } from 'react' import findIndex from 'lodash/findIndex' -import type { Chapters, Chapter } from '../types' +import type { Chapters } from '../types' -export const preparePlayer = (videoRef: RefObject, { url }: Chapter) => { +type Args = { + resume?: boolean, + url: string, + videoRef: RefObject, +} + +export const preparePlayer = ({ + resume = false, + url, + videoRef, +}: Args) => { const video = videoRef?.current if (!video) return + + const wasAtTime = video.currentTime // eslint-disable-next-line no-param-reassign video.src = url + if (resume) { + video.currentTime = wasAtTime + } video.load() } -export const startPlayer = (videoRef: RefObject, chapter: Chapter) => { - preparePlayer(videoRef, chapter) - videoRef.current?.play() -} - export const findChapterByProgress = (chapters: Chapters, progress: number) => ( findIndex(chapters, ({ endMs, startMs }) => ( startMs / 1000 <= progress && progress <= endMs / 1000 diff --git a/src/features/MultiSourcePlayer/hooks/index.tsx b/src/features/MultiSourcePlayer/hooks/index.tsx index c9a67bc4..34be611c 100644 --- a/src/features/MultiSourcePlayer/hooks/index.tsx +++ b/src/features/MultiSourcePlayer/hooks/index.tsx @@ -1,12 +1,12 @@ import type { MouseEvent } from 'react' import { + useCallback, useEffect, useState, useRef, } from 'react' import size from 'lodash/size' -import isEmpty from 'lodash/isEmpty' import type { Videos } from 'requests' @@ -16,13 +16,10 @@ import { useFullscreen } from 'features/StreamPlayer/hooks/useFullscreen' 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' -import { - preparePlayer, - startPlayer, -} from '../helpers' export type Props = { onError?: () => void, @@ -43,31 +40,50 @@ export const useMultiSourcePlayer = ({ const [loadedProgress, setLoadedProgress] = useState(0) const [playedProgress, setPlayedProgress] = useState(0) const { + continuePlaying, + firstTimeStart, playing, - ready, + startPlaying, + stopPlaying, togglePlaying, } = usePlayingState(videoRef) + + const { + selectedQuality, + setSelectedQuality, + videoQualities, + } = useVideoQuality(videos) + const { chapters } = useVideos(videos) const duration = useDuration(chapters) const onProgressChange = useProgressChangeHandler({ activeChapterIndex, chapters, + continuePlaying, duration, - playing, + selectedQuality, setPlayedProgress, videoRef, }) + const getCurrentChapterUrl = useCallback((quality: string = selectedQuality) => ( + chapters[activeChapterIndex.current].urls[quality] + ), [selectedQuality, chapters]) + + const onQualitySelect = (quality: string) => { + setSelectedQuality(quality) + continuePlaying(getCurrentChapterUrl(quality), true) + } + const playNextChapter = () => { activeChapterIndex.current += 1 const isLastChapterPlayed = activeChapterIndex.current === size(chapters) if (isLastChapterPlayed) { activeChapterIndex.current = 0 - preparePlayer(videoRef, chapters[activeChapterIndex.current]) - togglePlaying() + stopPlaying(getCurrentChapterUrl()) } else { - startPlayer(videoRef, chapters[activeChapterIndex.current]) + continuePlaying(getCurrentChapterUrl()) } } @@ -92,11 +108,15 @@ export const useMultiSourcePlayer = ({ } useEffect(() => { - if (isEmpty(chapters)) return - - startPlayer(videoRef, chapters[activeChapterIndex.current]) - togglePlaying() - }, [chapters, togglePlaying]) + const url = getCurrentChapterUrl() + if (url && firstTimeStart) { + startPlaying(url) + } + }, [ + firstTimeStart, + getCurrentChapterUrl, + startPlaying, + ]) useLoadedEvent({ onChange: onLoadedChange, @@ -125,13 +145,10 @@ export const useMultiSourcePlayer = ({ }, [playing, onPlayingChange]) useEffect(() => { - if (!ready) return - const progressSeconds = playedProgress / 1000 const { period } = chapters[activeChapterIndex.current] onProgressChangeCallback(progressSeconds, Number(period)) }, [ - ready, playedProgress, chapters, onProgressChangeCallback, @@ -143,9 +160,12 @@ export const useMultiSourcePlayer = ({ loadedProgress, onPlayerClick, onProgressChange, + onQualitySelect, playedProgress, playing, + selectedQuality, togglePlaying, + videoQualities, videoRef, wrapperRef, ...useFullscreen(wrapperRef), diff --git a/src/features/MultiSourcePlayer/hooks/usePlayingState.tsx b/src/features/MultiSourcePlayer/hooks/usePlayingState.tsx index f079b126..0b677755 100644 --- a/src/features/MultiSourcePlayer/hooks/usePlayingState.tsx +++ b/src/features/MultiSourcePlayer/hooks/usePlayingState.tsx @@ -1,18 +1,16 @@ import type { RefObject } from 'react' import { useState, useCallback } from 'react' -import once from 'lodash/once' - -import { useEventListener } from 'hooks' +import { preparePlayer } from '../helpers' export const usePlayingState = (videoRef: RefObject) => { + const [firstTimeStart, setFirstTimeStart] = useState(true) const [playing, setPlaying] = useState(false) - const [ready, setReady] = useState(false) const togglePlaying = useCallback(() => { setPlaying((isPlaying) => { const video = videoRef.current - if (!ready || !video) return false + if (!video || firstTimeStart) return false const nextIsPlaying = !isPlaying if (nextIsPlaying) { @@ -22,21 +20,44 @@ export const usePlayingState = (videoRef: RefObject) => { } return nextIsPlaying }) - }, [videoRef, ready]) - - const onReady = useCallback(once(() => { - setReady(true) - }), []) + }, [firstTimeStart, videoRef]) + + const stopPlaying = useCallback((nextUrl: string = '') => { + preparePlayer({ url: nextUrl, videoRef }) + setPlaying(false) + }, [videoRef]) + + const startPlaying = useCallback((url: string) => { + preparePlayer({ url, videoRef }) + videoRef.current?.play() + setFirstTimeStart(false) + setPlaying(true) + }, [videoRef]) + + const continuePlaying = useCallback(( + url: string, + rememberTime: boolean = false, + ) => { + preparePlayer({ + resume: rememberTime, + url, + videoRef, + }) - useEventListener({ - callback: onReady, - event: 'canplay', - ref: videoRef, - }) + setPlaying((isPlaying) => { + if (isPlaying) { + videoRef.current?.play() + } + return isPlaying + }) + }, [videoRef]) return { + continuePlaying, + firstTimeStart, playing, - ready, + startPlaying, + stopPlaying, togglePlaying, } } diff --git a/src/features/MultiSourcePlayer/hooks/useProgressChangeHandler.tsx b/src/features/MultiSourcePlayer/hooks/useProgressChangeHandler.tsx index 2208a46c..3af8f94f 100644 --- a/src/features/MultiSourcePlayer/hooks/useProgressChangeHandler.tsx +++ b/src/features/MultiSourcePlayer/hooks/useProgressChangeHandler.tsx @@ -8,15 +8,14 @@ import throttle from 'lodash/throttle' import type { Chapters } from '../types' import { findChapterByProgress, - preparePlayer, - startPlayer, } from '../helpers' type Args = { activeChapterIndex: MutableRefObject, chapters: Chapters, + continuePlaying: (url: string) => void, duration: number, - playing: boolean, + selectedQuality: string, setPlayedProgress: (value: number) => void, videoRef: RefObject, } @@ -24,8 +23,9 @@ type Args = { export const useProgressChangeHandler = ({ activeChapterIndex, chapters, + continuePlaying, duration, - playing, + selectedQuality, setPlayedProgress, videoRef, }: Args) => { @@ -49,10 +49,7 @@ export const useProgressChangeHandler = ({ ) // если ползунок остановили на другой главе if (isProgressOnDifferentChapter) { - // если проигрывали видео то продолжаем проигрывание, - // если нет то только загружаем видео - const continueOrPreparePlayer = playing ? startPlayer : preparePlayer - continueOrPreparePlayer(videoRef, chapter) + continuePlaying(chapter.urls[selectedQuality]) // eslint-disable-next-line no-param-reassign activeChapterIndex.current = chapterIndex } @@ -63,13 +60,13 @@ export const useProgressChangeHandler = ({ const chapterProgressSec = (progressMs - chapter.startMs) / 1000 setPlayerProgress(chapterProgressSec) }, [ - playing, + selectedQuality, chapters, duration, + continuePlaying, setPlayedProgress, setPlayerProgress, activeChapterIndex, - videoRef, ]) return onProgressChange diff --git a/src/features/MultiSourcePlayer/hooks/useVideoQuality.tsx b/src/features/MultiSourcePlayer/hooks/useVideoQuality.tsx new file mode 100644 index 00000000..d17339bd --- /dev/null +++ b/src/features/MultiSourcePlayer/hooks/useVideoQuality.tsx @@ -0,0 +1,27 @@ +import { useState } from 'react' + +import map from 'lodash/map' +import uniq from 'lodash/uniq' +import orderBy from 'lodash/orderBy' + +import type { Videos } from 'requests' + +const getVideoQualities = (videos: Videos) => { + const qualities = uniq(map(videos, 'quality')) + return orderBy( + qualities, + Number, + 'desc', + ) +} + +export const useVideoQuality = (videos: Videos) => { + const videoQualities = getVideoQualities(videos) + const [selectedQuality, setSelectedQuality] = useState(videoQualities[0]) + + return { + selectedQuality, + setSelectedQuality, + videoQualities, + } +} diff --git a/src/features/MultiSourcePlayer/hooks/useVideos.tsx b/src/features/MultiSourcePlayer/hooks/useVideos.tsx index a59d08d5..6399f4cf 100644 --- a/src/features/MultiSourcePlayer/hooks/useVideos.tsx +++ b/src/features/MultiSourcePlayer/hooks/useVideos.tsx @@ -3,35 +3,56 @@ import { useMemo } from 'react' import map from 'lodash/map' import last from 'lodash/last' import uniq from 'lodash/uniq' -import maxBy from 'lodash/maxBy' import filter from 'lodash/filter' import reduce from 'lodash/reduce' import concat from 'lodash/concat' import orderBy from 'lodash/orderBy' +import isEmpty from 'lodash/isEmpty' import type { Videos } from 'requests' -import type { Chapters } from '../types' +import type { Chapters, Urls } from '../types' const getUniquePeriods = (videos: Videos) => uniq(map(videos, 'period')) -const getHighestQualityVideo = (videos: Videos, period: number) => { +type Video = { + duration: number, + period: number, + urls: Urls, +} + +const getVideoByPeriod = (videos: Videos, period: number) => { const videosWithSamePeriod = filter(videos, { period }) - return maxBy(videosWithSamePeriod, ({ quality }) => Number(quality)) + if (isEmpty(videosWithSamePeriod)) return null + + const urls = reduce( + videosWithSamePeriod, + (acc: Urls, video) => ({ + ...acc, + [video.quality]: video.url, + }), + {}, + ) + const [video] = videosWithSamePeriod + return { + duration: video.duration, + period: video.period, + urls, + } } -const getHighQualityVideos = (videos: Videos, periods: Array) => ( +const getVideoByPeriods = (videos: Videos, periods: Array) => ( reduce( periods, - (acc: Videos, period) => { - const video = getHighestQualityVideo(videos, period) + (acc: Array