-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DE-6967: HLS Interstitial Player Component
- Loading branch information
1 parent
93ac093
commit 6ae0d04
Showing
14 changed files
with
862 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { clpp } from '@castlabs/prestoplay' | ||
import React, { useState } from 'react' | ||
|
||
import { InterstitialPlayer } from '../../src' | ||
|
||
/** | ||
* A page featuring the HLS interstitial player. | ||
*/ | ||
export const InterstitialPage = () => { | ||
const [mounted, setMounted] = useState(true) | ||
|
||
const toggleMounted = () => { | ||
setMounted(m => !m) | ||
} | ||
|
||
return ( | ||
<main className="in-page"> | ||
<div className="in-container"> | ||
<div> | ||
<button onClick={toggleMounted}>Toggle mounted</button> | ||
</div> | ||
{mounted ? ( | ||
<div className="in-video-container"> | ||
<InterstitialPlayer | ||
asset={{ | ||
source: { | ||
// url: 'http://localhost:3000/vod-fixed.m3u8', | ||
url: 'http://localhost:3000/vod-preroll.m3u8', | ||
type: clpp.Type.HLS, | ||
}, | ||
}} | ||
// Possibly it's something wrong with the AIP stream http://localhost:3000/vod-preroll.m3u8 | ||
// but unfortunately what happens is that we get state "Ended" and then the video | ||
// continues playing for another cca 800ms. This would obviously cause a glitch | ||
// in the UI so configure the player to ignore all ended states changes | ||
patchIgnoreStateEnded={true} | ||
interstitialOptions={{ | ||
// Start resolving X-ASSET-LIST 15 seconds or less before | ||
// the cue is scheduled | ||
resolutionOffsetSec: 15, | ||
}} | ||
// onPlayerChanged={p => { | ||
// // @ts-ignore | ||
// window.player = p | ||
// }} | ||
// showInterstitialMarkers={false} | ||
// seekStep={2} | ||
// controlsVisibility='never' | ||
// intermissionDuration={5} | ||
// interstitialLabel={(i) => `Ad ${i.podOrder} of ${i.podCount}`} | ||
// renderInterstitialLabel={(i) => null} | ||
// renderIntermission={(seconds) => <div>{seconds}</div>} | ||
// loop={false} | ||
// onEnded={() => {}} | ||
// onLoopEnded={() => {}} | ||
/> | ||
</div> | ||
): null} | ||
</div> | ||
</main> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import { clpp } from '@castlabs/prestoplay' | ||
import '@castlabs/prestoplay/cl.mse' | ||
import '@castlabs/prestoplay/cl.hls' | ||
import React, { useEffect, useRef } from 'react' | ||
|
||
import { ControlsVisibilityMode } from '../services/controls' | ||
|
||
import { InterstitialOverlay } from './components/OverlayHlsi' | ||
import { PlayerSurfaceHlsi } from './components/PlayerSurfaceHlsi' | ||
import { PlayerHlsi } from './PlayerHlsi' | ||
import { HlsInterstitial } from './types' | ||
|
||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
clpp.install(clpp.hls.HlsComponent) | ||
|
||
export type InterstitialPlayerProps = { | ||
/** | ||
* HLS interstitial Asset | ||
*/ | ||
asset: clpp.PlayerConfiguration | ||
/** | ||
* If the asset should be played back in a loop, one cycle of the loop | ||
* consists of a few seconds of intermission and then the asset is played. | ||
* Default: true. | ||
*/ | ||
loop?: boolean | ||
/** | ||
* Intermission duration in seconds, defaults to 3. | ||
*/ | ||
intermissionDuration?: number | null | ||
/** | ||
* Intermission element renderer. Default: Countdown | ||
*/ | ||
renderIntermission?: (seconds: number) => (JSX.Element | null) | ||
/** | ||
* Callback called when playback ended (if loop is `false`) | ||
*/ | ||
onEnded?: () => any | ||
/** | ||
* Callback called when one loop cycle ended (if loop is `true`) | ||
*/ | ||
onLoopEnded?: () => any | ||
/** | ||
* Interstitial label text renderer. Default: `Interstitial ${podOrder} of ${podCount}` | ||
*/ | ||
interstitialLabel?: (i: HlsInterstitial) => string | ||
/** | ||
* Interstitial label component renderer. | ||
*/ | ||
renderInterstitialLabel?: (i: HlsInterstitial) => (JSX.Element | null) | ||
/** | ||
* Visibility mode of UI controls. Default: 'always-visible' | ||
*/ | ||
controlsVisibility?: ControlsVisibilityMode | ||
/** | ||
* Seek step in seconds for seek buttons. A value of 0 will hide the buttons. | ||
* Default: 10. | ||
*/ | ||
seekStep?: number | ||
/** | ||
* If true interstitial markers should be shown on the timeline. Default: true. | ||
*/ | ||
showInterstitialMarkers?: boolean | ||
/** | ||
* If true, a fullscreen button is displayed. Defaults to true. | ||
*/ | ||
hasFullScreenButton?: boolean | ||
/** | ||
* If true, audio controls are displayed. Defaults to false. | ||
*/ | ||
hasAudioControls?: boolean | ||
/** | ||
* If true, track controls are displayed. Defaults to false. | ||
*/ | ||
hasTrackControls?: boolean | ||
/** | ||
* Callback called when the player of multi-controller changes. | ||
*/ | ||
onPlayerChanged?: (p: clpp.Player) => any | ||
/** | ||
* Options for clpp.interstitial.Player | ||
*/ | ||
interstitialOptions?: Omit<clpp.interstitial.Options, 'anchorEl'> | ||
/** | ||
* Custom class name for the player container. | ||
*/ | ||
className?: string | ||
/** | ||
* Custom style for the player container. | ||
*/ | ||
style?: React.CSSProperties | ||
/** | ||
* If true, the player will ignore all state changes to state "ended". | ||
*/ | ||
patchIgnoreStateEnded?: boolean | ||
} | ||
|
||
/** | ||
* A dedicated component for playback of HLS streams with interstitials. | ||
* | ||
* By default the stream is played in an infinite loop with a countdown | ||
* intermission in between. | ||
*/ | ||
export const InterstitialPlayer = React.memo((props: InterstitialPlayerProps) => { | ||
const playerRef = useRef(new PlayerHlsi()) | ||
|
||
useEffect(() => { | ||
if (props.patchIgnoreStateEnded) { | ||
playerRef.current.ignoreStateEnded = true | ||
} | ||
}, [props.patchIgnoreStateEnded]) | ||
|
||
const load = async () => { | ||
try { | ||
await playerRef.current.loadHlsi(props.asset) | ||
} catch (e) { | ||
console.error('Interstitial player Failed to load asset', e) | ||
} | ||
} | ||
|
||
useEffect(() => { | ||
if (props.onPlayerChanged) { | ||
playerRef.current.onUIEvent('playerChanged', props.onPlayerChanged) | ||
} | ||
|
||
return () => { | ||
if (props.onPlayerChanged) { | ||
playerRef.current.offUIEvent('playerChanged', props.onPlayerChanged) | ||
} | ||
} | ||
}, []) | ||
|
||
useEffect(() => { | ||
return () => { | ||
playerRef.current.destroy().catch(e => { | ||
console.error('Failed to destroy Interstitial player', e) | ||
}) | ||
} | ||
}, []) | ||
|
||
let className = 'pp-ui-hlsi-player' | ||
if (props.className) { | ||
className += ` ${props.className}` | ||
} | ||
|
||
return ( | ||
<div className={className} style={props.style}> | ||
<PlayerSurfaceHlsi | ||
player={playerRef.current} | ||
interstitialOptions={props.interstitialOptions} | ||
> | ||
<InterstitialOverlay | ||
{...props} | ||
onStartClick={async () => { | ||
await load() | ||
}} | ||
onLoopEnded={async () => { | ||
props.onLoopEnded?.() | ||
await playerRef.current.reset() | ||
await load() | ||
}} | ||
onIntermissionEnded={async () => { | ||
await playerRef.current.unpause() | ||
}} | ||
/> | ||
</PlayerSurfaceHlsi> | ||
</div> | ||
) | ||
}) | ||
|
||
InterstitialPlayer.displayName = 'InterstitialPlayer' |
Oops, something went wrong.