Skip to content

Commit

Permalink
feat: allow change sources on the fly, refactor VideoSourceProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
ambar committed Jan 27, 2022
1 parent 74f9400 commit 044f220
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 116 deletions.
1 change: 1 addition & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const NavLinks = () => {
<br />
<Link to="/mp4?loop">/mp4?loop</Link>
<br />
<Link to="/mp4?hls">/mp4?hls</Link>
</li>
<li>
<Link to="/mp4-mse">/mp4-mse</Link>
Expand Down
2 changes: 1 addition & 1 deletion example/src/HLSPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react'
import {logEvent} from './utils'
import useQuery from './utils/useQuery'

const sources = {
export const sources = {
// 注意,这里手动提供了 auto 品质的 source,因此会无视 useAutoQuality 的配置
auto: {
format: 'm3u8',
Expand Down
2 changes: 2 additions & 0 deletions example/src/MP4Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, {useState} from 'react'
import Logo from './Logo'
import {logEvent} from './utils'
import useQuery from './utils/useQuery'
import {sources as hlsSources} from './HLSPage'

const duration = 182

Expand Down Expand Up @@ -61,6 +62,7 @@ const App = () => {
<>
<Player
{...props}
sources={'hls' in query ? hlsSources : props.sources}
localeConfig={{
'zh-Hans': {
'quality-ld': {
Expand Down
4 changes: 1 addition & 3 deletions packages/griffith/src/components/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -866,10 +866,8 @@ const Player: React.FC<PlayerProps> = ({
<InternalMessageContext.Consumer>
{({emitEvent, subscribeAction}) => (
<VideoSourceProvider
// TODO:改名 emitEvent
onEvent={emitEvent as any}
emitEvent={emitEvent}
sources={sources}
id={id}
defaultQuality={defaultQuality}
useAutoQuality={useAutoQuality}
defaultPlaybackRate={defaultPlaybackRate}
Expand Down
1 change: 0 additions & 1 deletion packages/griffith/src/contexts/VideoSourceContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react'
import {PlaySource, Quality, PlaybackRate} from '../types'

export type VideoSourceContextValue = {
dataKey?: string
currentSrc: string
format: string
sources: PlaySource[]
Expand Down
195 changes: 84 additions & 111 deletions packages/griffith/src/contexts/VideoSourceProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,43 @@
import React from 'react'
import {
PlaySourceMap,
FormattedPlaySource,
PlaybackRate,
RealQuality,
} from '../types'
import VideoSourceContext, {VideoSourceContextValue} from './VideoSourceContext'
import React, {useEffect, useMemo, useState} from 'react'
import {PlaySourceMap, PlaybackRate, RealQuality} from '../types'
import VideoSourceContext from './VideoSourceContext'
import {getQualities, getSources} from './parsePlaylist'
import {EVENTS} from 'griffith-message'
import {ua} from 'griffith-utils'
import useHandler from '../hooks/useHandler'
import useChanged from '../hooks/useChanged'
import {InternalMessageContextValue} from './MessageContext'
import usePrevious from '../hooks/usePrevious'

const {isMobile} = ua

const getQuery = (url: string, key: string) => {
const [, value] = new RegExp(`\\b${key}=([^&]+)`).exec(url) || []
return value
}

type VideoSourceProviderProps = {
onEvent: (name: EVENTS, data?: unknown) => void
emitEvent: InternalMessageContextValue['emitEvent']
sources: PlaySourceMap
id: string
defaultQuality?: RealQuality
useAutoQuality?: boolean
playbackRates: PlaybackRate[]
defaultPlaybackRate?: PlaybackRate
}

type VideoSourceProviderState = Partial<VideoSourceContextValue> & {
expiration: number
}

export default class VideoSourceProvider extends React.Component<
VideoSourceProviderProps,
VideoSourceProviderState
> {
state = {
qualities: [],
currentQuality: undefined,
format: undefined,
sources: [] as FormattedPlaySource[],
expiration: 0,
dataKey: undefined,
currentPlaybackRate: undefined,
}

static getDerivedStateFromProps = (
{
sources: videoSources,
id,
defaultQuality,
useAutoQuality,
defaultPlaybackRate,
}: VideoSourceProviderProps,
state: VideoSourceProviderState
): VideoSourceProviderState | null => {
if (!videoSources) return null
const {format, play_url} = Object.values(videoSources)[0]!
const expiration = getQuery(play_url, 'expiration')
const dataKey = `${id}-${expiration}` // expiration 和 id 组合可以唯一标识一次请求的数据

if (dataKey == state.dataKey) return null

const qualities = getQualities(videoSources, isMobile)

const sources = getSources(qualities, videoSources)
const VideoSourceProvider: React.FC<VideoSourceProviderProps> = ({
sources: sourceMap,
useAutoQuality,
emitEvent,
playbackRates,
defaultPlaybackRate,
defaultQuality,
children,
}) => {
const lastSourceMap = useChanged(sourceMap)
const {qualities, sources, format} = useMemo(() => {
// 其实视频源应当是必需参数
if (!lastSourceMap) {
return {qualities: [], sources: []}
}
const {format} = Object.values(lastSourceMap)[0]!
const qualities = getQualities(lastSourceMap, isMobile)
const sources = getSources(qualities, lastSourceMap)

// 目前只有直播流实现了手动拼接 auto 清晰度的功能
if (
Expand All @@ -76,83 +49,83 @@ export default class VideoSourceProvider extends React.Component<
qualities.unshift('auto')
}

const defaultCurrentQuality = defaultQuality || qualities[0]
const currentQuality = state.currentQuality || defaultCurrentQuality
const currentPlaybackRate = state.currentPlaybackRate || defaultPlaybackRate
return {
currentQuality,
currentPlaybackRate,
qualities,
sources,
format,
expiration: Number(expiration),
dataKey,
}
}
return {qualities, sources, format}
}, [useAutoQuality, lastSourceMap])

setCurrentQuality = (quality: RealQuality) => {
const prevQuality = this.state.currentQuality
if (prevQuality !== quality) {
this.setState({currentQuality: quality})
this.props.onEvent(EVENTS.QUALITY_CHANGE, {
prevQuality,
const [currentQuality, setCurrentQualityRaw] = useState(
defaultQuality || qualities[0]
)
const [playbackRate, setPlaybackRate] = useState(defaultPlaybackRate)

const setCurrentQuality = useHandler((quality: RealQuality) => {
if (currentQuality !== quality) {
setCurrentQualityRaw(quality)
emitEvent(EVENTS.QUALITY_CHANGE, {
prevQuality: currentQuality,
quality,
})
}
}
})

// 当 sources 改变重置 `currentQuality`
const prevQualities = usePrevious(qualities)
useEffect(() => {
if (prevQualities && prevQualities !== qualities) {
setCurrentQuality((defaultQuality || qualities[0]) as RealQuality)
}
}, [prevQualities, qualities, defaultQuality, setCurrentQuality])

setCurrentPlaybackRate = (rate: PlaybackRate) => {
const prevRate = this.state.currentPlaybackRate
if (prevRate !== rate) {
this.setState({currentPlaybackRate: rate})
this.props.onEvent(EVENTS.PLAYBACK_RATE_CHANGE, {
prevRate,
const setCurrentPlaybackRate = useHandler((rate: PlaybackRate) => {
if (playbackRate !== rate) {
setPlaybackRate(rate)
emitEvent(EVENTS.PLAYBACK_RATE_CHANGE, {
prevRate: playbackRate!,
rate,
})
}
}
})

/**
* 获得当前 src, 根据当前清晰度返回对应的 src
*/
getCurrentSrc = () => {
const {currentQuality, sources} = this.state
// 根据当前清晰度返回对应的 src
const currentSrc = useMemo(() => {
if (sources.length === 0) {
return
}

const source =
sources.find((item) => item.quality === currentQuality) || sources[0]
return source.source
}
}, [currentQuality, sources])

render() {
const {
const contextValue = useMemo(
() => ({
qualities,
playbackRates,
format: format!,
sources,
currentQuality,
currentPlaybackRate: playbackRate!,
currentSrc: currentSrc!,
setCurrentQuality,
setCurrentPlaybackRate,
}),
[
playbackRate,
currentQuality,
currentSrc,
format,
dataKey,
playbackRates,
qualities,
setCurrentPlaybackRate,
setCurrentQuality,
sources,
currentPlaybackRate,
} = this.state
const {playbackRates} = this.props
return (
<VideoSourceContext.Provider
value={{
dataKey,
qualities,
playbackRates,
format: format!,
sources,
currentQuality: currentQuality!,
currentPlaybackRate: currentPlaybackRate!,
currentSrc: this.getCurrentSrc()!,
setCurrentQuality: this.setCurrentQuality,
setCurrentPlaybackRate: this.setCurrentPlaybackRate,
}}
>
{this.props.children}
</VideoSourceContext.Provider>
)
}
]
)

return (
<VideoSourceContext.Provider value={contextValue}>
{children}
</VideoSourceContext.Provider>
)
}

export default VideoSourceProvider
17 changes: 17 additions & 0 deletions packages/griffith/src/hooks/useChanged.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {useRef} from 'react'

/**
* Get last changed value (useful for object comparison)
*/
const useChanged = <T extends any>(value: T) => {
const ref = useRef<T>(value)
if (
value !== ref.current &&
JSON.stringify(value) !== JSON.stringify(ref.current)
) {
ref.current = value
}
return ref.current
}

export default useChanged

0 comments on commit 044f220

Please sign in to comment.