Skip to content

Commit

Permalink
DE-7019: Enrich InterstitalPlayer API (#41)
Browse files Browse the repository at this point in the history
* Fix play/pause button in HLS-I Player

* Option to hide top controls bar

* Fix CSS of seek bar cues

* Add option for top companion component

* Option to change player controls during interstitial

* HLS-I Options to listen to events

* Upgrade to beta 2
  • Loading branch information
fingerartur authored Apr 9, 2024
1 parent 787cf43 commit 9544639
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 31 deletions.
2 changes: 1 addition & 1 deletion app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function setQueryParam(key: string, value: string) {
export function App() {
// We track the configuration here to make sure we can dynamically change it
const [assetId, setAssetId] = useState<number>(Number(getQueryVariable('asset') || 0))
const [pageId, setPageId] = useState<Page>(getQueryVariable('page') ?? 'basic')
const [pageId, setPageId] = useState<Page>(getQueryVariable('page') ?? 'interstitial')
const [asset, setAsset] = useState<Asset|undefined>(TestAssets[assetId])
const [autoload, setAutoload] = useState<boolean>(false)
const [navVisible, setNavVisible] = useState<boolean>(false)
Expand Down
55 changes: 54 additions & 1 deletion app/src/InterstitialPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ export const InterstitialPage = () => {
<InterstitialPlayer
asset={{
source: {
// url: 'http://localhost:3000/vod-fixed.m3u8',
// url: 'http://localhost:3000/vod-fixed.m3u8',
url: 'http://localhost:3000/vod-preroll.m3u8',
// url: 'https://9457688946fc45ac9a3b526e93b06bf7.us-west-2.alpha.mediatailor.aws.a2z.com/v1/master/5d22c610440c419b9290f9233dc99fe61adb77ab/mt-dev-vod/index.m3u8?aws.insertionMode=GUIDED',
type: clpp.Type.HLS,
},
}}
Expand All @@ -34,10 +35,62 @@ export const InterstitialPage = () => {
// 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}
hasTopControlsBar={false}
interstitialOptions={{
// Start resolving X-ASSET-LIST 15 seconds or less before
// the cue is scheduled
resolutionOffsetSec: 15,
interstitialAssetConverter: (asset: clpp.interstitial.PlayerItem) => {
asset.config.htmlcue = {
enableResizeObserver: false,
}
return asset
},
}}
renderTopCompanion={(isFullScreen) => {
if (!isFullScreen) {return null}
return <div className="in-logo-container"><img className="in-logo" src="./logo.png"/></div>
}}
interstitialControls={{
pause: true,
seekButtons: false,
time: false,
fullScreen: true,
audio: false,
}}
onIntermissionEnded={() => {
console.info('EEEEvent: intermission-ended playback or primary or preroll started')
}}
onHlsiPlayerReady={hp => {
hp.on('cues-changed', (event) => {
const cues = hp.getCues()
console.info('EEEEvent: cues-changed', event.detail, 'cues via api call', cues)
})

hp.on('interstitial-started', (event) => {
console.info('EEEEvent: interstitial-started', event.detail)
})

hp.on('interstitial-item-started', (event) => {
// There are multiple items in one interstitial
console.info('EEEEvent: interstitial-item-started', event.detail)
})

hp.on('interstitial-ended', (event) => {
console.info('EEEEvent: interstitial-ended', event.detail)
})

hp.on('primary-started', (event) => {
console.info('EEEEvent: primary-started', event.detail)
})

hp.on('playback-started', (event) => {
console.info('EEEEvent: playback-started (primary or preroll)', event.detail)
})

hp.on('primary-ended', (event) => {
console.info('EEEEvent: primary-ended', event.detail)
})
}}
// onPlayerChanged={p => {
// // @ts-ignore
Expand Down
15 changes: 14 additions & 1 deletion app/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,18 @@ nav button:hover {

.in-video-container {
width: 800px;
height: 580px;
height: 400px;
}

.in-logo-container {
width: 100%;
display: flex;
justify-content: end;
padding: 10px;
position: relative;
top: 10px;
}

.in-logo {
width: 200px;
}
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@babel/preset-env": "^7.22.4",
"@babel/preset-react": "^7.22.3",
"@babel/preset-typescript": "^7.21.5",
"@castlabs/prestoplay": "^6.11.1-beta.1",
"@castlabs/prestoplay": "^6.11.1-beta.2",
"@finga/eslint-config": "^1.2.1",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-image": "^3.0.2",
Expand Down
2 changes: 1 addition & 1 deletion src/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ export class Player {
/**
* Indicate that the config was loaded
*/
private _configLoaded = false
protected _configLoaded = false
/**
* UI control visibility manager
*/
Expand Down
43 changes: 34 additions & 9 deletions src/components/BaseThemeOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BufferingIndicator } from './BufferingIndicator'
import { CurrentTime } from './CurrentTime'
import { Duration } from './Duration'
import { ForSize } from './ForSize'
import { FullscreenButton } from './FullscreenButton'
import { FullscreenButton, useIsPlayerFullScreen } from './FullscreenButton'
import { HorizontalBar } from './HorizontalBar'
import { Label } from './Label'
import {
Expand Down Expand Up @@ -81,14 +81,26 @@ export interface BaseThemeOverlayProps extends BaseComponentProps {
* If true, track controls are displayed. Defaults to true.
*/
hasTrackControls?: boolean
/**
* If true, the play/pause button is displayed. Defaults to true.
*/
hasPauseButton?: boolean
/**
* If true, the top controls bar is displayed. Defaults to true.
*/
hasTopControlsBar?: boolean
/**
* If true, the time is displayed. Defaults to true.
*/
hasTime?: boolean
/**
* Render a custom bottom companion component.
*/
renderBottomCompanion?: () => (JSX.Element | null)
renderBottomCompanion?: (isFullScreen: boolean) => (JSX.Element | null)
/**
* Render a custom top companion component.
*/
renderTopCompanion?: (isFullScreen: boolean) => (JSX.Element | null)
/**
* If true, seek bar cues are shown. Default: true.
*/
Expand All @@ -109,6 +121,10 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => {
const hasFullScreenButton = props.hasFullScreenButton ?? true
const hasTopControlsBar = props.hasTopControlsBar ?? true
const showSeekBarCues = props.showSeekBarCues ?? true
const hasPauseButton = props.hasPauseButton ?? true
const hasTime = props.hasTime ?? true

const isFullScreen = useIsPlayerFullScreen()

const renderOptionsMenu = () => {
if (selectionOptions.length === 0) {return}
Expand Down Expand Up @@ -139,13 +155,20 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => {
return <StartButton onClick={typeof props.startButton === 'object' ? props.startButton.onClick : undefined}/>
}

const topCompanion = props.renderTopCompanion?.(isFullScreen)

return (
<div data-testid="pp-ui-basic-theme" className={'pp-ui pp-ui-overlay pp-ui-basic-theme'} style={props.style}>
<PlayerControls mode={props.controlsVisibility}>
<VerticalBar className={'pp-ui-spacer'}>

{/* Top bar */}
{renderTopBar()}
{topCompanion ? (
<div className="pp-ui pp-ui-row pp-ui-top-bar">
{topCompanion}
</div>
): null}

<Spacer/>

Expand All @@ -154,12 +177,12 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => {
<Thumbnail moveRelativeToParent={true}/>
</HorizontalBar>

{props.renderBottomCompanion?.()}
{props.renderBottomCompanion?.(isFullScreen)}

{/* Bottom bar */}
<HorizontalBar className="pp-ui-flex-space-between">
<div className="pp-ui-row pp-ui-margin-horizontal-sm">
<PlayPauseButton resetRate={true}/>
{hasPauseButton ? <PlayPauseButton resetRate={true}/> : null}
<ForSize size="small">
<SeekButton seconds={props.seekBackward ?? -10}/>
<SeekButton seconds={props.seekForward ?? 10}/>
Expand All @@ -176,11 +199,13 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => {
/>}

<div className="pp-ui-row pp-ui-margin-horizontal-sm">
<ForSize size="small">
<CurrentTime />
<Label label={'/'}/>
<Duration />
</ForSize>
{hasTime ? (
<ForSize size="small">
<CurrentTime />
<Label label={'/'}/>
<Duration />
</ForSize>
): null}

{hasAudioControls ? <MuteButton/>: null}

Expand Down
11 changes: 10 additions & 1 deletion src/components/FullscreenButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const getVideoChild = (element: HTMLElement) => {
* This hook will return whether the element of its descendant video
* element is currently in fullscreen mode.
*/
const useIsFullscreen = (playerSurface: HTMLElement | null) => {
export const useIsFullscreen = (playerSurface: HTMLElement | null) => {
const [is, setIs] = useState(fullscreen.isInFullscreen())

const listener = () => {
Expand All @@ -100,6 +100,15 @@ const useIsFullscreen = (playerSurface: HTMLElement | null) => {
return is
}

/**
* This hooks returns true if the player is in fullscreen mode, false otherwise.
*/
export const useIsPlayerFullScreen = () => {
const { playerSurface } = useContext(PrestoContext)
const isFullscreen = useIsFullscreen(playerSurface)
return isFullscreen
}

/**
* Fullscreen button.
* A button that brings the player into fullscreen mode.
Expand Down
25 changes: 23 additions & 2 deletions src/interstitial/InterstitialPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ControlsVisibilityMode } from '../services/controls'
import { InterstitialOverlay } from './components/OverlayHlsi'
import { PlayerSurfaceHlsi } from './components/PlayerSurfaceHlsi'
import { PlayerHlsi } from './PlayerHlsi'
import { HlsInterstitial } from './types'
import { HlsInterstitial, InterstitialControls } from './types'

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand Down Expand Up @@ -41,6 +41,10 @@ export type InterstitialPlayerProps = {
* Callback called when one loop cycle ended (if loop is `true`)
*/
onLoopEnded?: () => any
/**
* Callback called when intermission ended
*/
onIntermissionEnded?: () => any
/**
* Interstitial label text renderer. Default: `Interstitial ${podOrder} of ${podCount}`
*/
Expand Down Expand Up @@ -74,6 +78,10 @@ export type InterstitialPlayerProps = {
* If true, track controls are displayed. Defaults to false.
*/
hasTrackControls?: boolean
/**
* If true, the top controls bar is displayed. Defaults to true.
*/
hasTopControlsBar?: boolean
/**
* Callback called when the player of multi-controller changes.
*/
Expand All @@ -94,6 +102,18 @@ export type InterstitialPlayerProps = {
* If true, the player will ignore all state changes to state "ended".
*/
patchIgnoreStateEnded?: boolean
/**
* Render a custom top companion component.
*/
renderTopCompanion?: (isFullScreen: boolean) => (JSX.Element | null)
/**
* Player controls to shown during interstitial playback.
*/
interstitialControls?: InterstitialControls
/**
* Callback to get the instance of the HLS interstitial player
*/
onHlsiPlayerReady?: (player: clpp.interstitial.Player) => void
}

/**
Expand All @@ -103,7 +123,7 @@ export type InterstitialPlayerProps = {
* intermission in between.
*/
export const InterstitialPlayer = React.memo((props: InterstitialPlayerProps) => {
const playerRef = useRef(new PlayerHlsi())
const playerRef = useRef(new PlayerHlsi(props.onHlsiPlayerReady))

useEffect(() => {
if (props.patchIgnoreStateEnded) {
Expand Down Expand Up @@ -161,6 +181,7 @@ export const InterstitialPlayer = React.memo((props: InterstitialPlayerProps) =>
await load()
}}
onIntermissionEnded={async () => {
props.onIntermissionEnded?.()
await playerRef.current.unpause()
}}
/>
Expand Down
9 changes: 9 additions & 0 deletions src/interstitial/PlayerHlsi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ export class PlayerHlsi extends Player {
private _isPlayingInterstitial = false
private _disposers: Disposer[] = []

constructor(
private _onReady?: (p: clpp.interstitial.Player) => void,
) {
super()
}

/**
* Initialize the HLS Interstitial player
*/
Expand All @@ -53,6 +59,7 @@ export class PlayerHlsi extends Player {

this._options = options
this._ip = new clpp.interstitial.Player(options)
this._onReady?.(this._ip)

this.on('interstitial-item-started', (event) => {
this.emitUIEvent('hlsInterstitial', {
Expand Down Expand Up @@ -98,6 +105,8 @@ export class PlayerHlsi extends Player {
async loadHlsi(config?: clpp.PlayerConfiguration) {
if (!this._ip || !config) {return}
await this._ip.loadPaused(config)
// To enabled play/pause button
this._configLoaded = true
}

/**
Expand Down
Loading

0 comments on commit 9544639

Please sign in to comment.