From 9544639c52e3658bbdc566364c2c5d501b70262b Mon Sep 17 00:00:00 2001 From: Artur Finger Date: Tue, 9 Apr 2024 13:35:34 +0300 Subject: [PATCH] DE-7019: Enrich InterstitalPlayer API (#41) * 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 --- app/src/App.tsx | 2 +- app/src/InterstitialPage.tsx | 55 ++++++++++++++++++++- app/src/styles.css | 15 +++++- package-lock.json | 16 +++--- package.json | 2 +- src/Player.ts | 2 +- src/components/BaseThemeOverlay.tsx | 43 ++++++++++++---- src/components/FullscreenButton.tsx | 11 ++++- src/interstitial/InterstitialPlayer.tsx | 25 +++++++++- src/interstitial/PlayerHlsi.ts | 9 ++++ src/interstitial/components/OverlayHlsi.tsx | 38 ++++++++++++-- src/interstitial/types.ts | 11 +++++ src/themes/pp-ui-base-theme.css | 4 +- 13 files changed, 202 insertions(+), 31 deletions(-) diff --git a/app/src/App.tsx b/app/src/App.tsx index 139b5af..c8c122f 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -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(getQueryVariable('asset') || 0)) - const [pageId, setPageId] = useState(getQueryVariable('page') ?? 'basic') + const [pageId, setPageId] = useState(getQueryVariable('page') ?? 'interstitial') const [asset, setAsset] = useState(TestAssets[assetId]) const [autoload, setAutoload] = useState(false) const [navVisible, setNavVisible] = useState(false) diff --git a/app/src/InterstitialPage.tsx b/app/src/InterstitialPage.tsx index 1848f0e..5efc2b8 100644 --- a/app/src/InterstitialPage.tsx +++ b/app/src/InterstitialPage.tsx @@ -24,8 +24,9 @@ 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
+ }} + 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 diff --git a/app/src/styles.css b/app/src/styles.css index d3805c9..9ebb74e 100644 --- a/app/src/styles.css +++ b/app/src/styles.css @@ -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; } diff --git a/package-lock.json b/package-lock.json index a7fead5..247033c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,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", @@ -75,7 +75,7 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "@castlabs/prestoplay": "^6.6.0", + "@castlabs/prestoplay": "^6.11.1-beta.1", "react": "^18.0.0", "react-dom": "^18.0.0" } @@ -2347,9 +2347,9 @@ "license": "MIT" }, "node_modules/@castlabs/prestoplay": { - "version": "6.11.1-beta.1", - "resolved": "https://registry.npmjs.org/@castlabs/prestoplay/-/prestoplay-6.11.1-beta.1.tgz", - "integrity": "sha512-QUxzyxKaK1KEaC/fPKxFyn5BEqYHU5vXl/DngakgydHr3xHbjTCCL/e0uZ3f2RMyu8KmOiK18EjCjoFon+IhMQ==", + "version": "6.11.1-beta.2", + "resolved": "https://registry.npmjs.org/@castlabs/prestoplay/-/prestoplay-6.11.1-beta.2.tgz", + "integrity": "sha512-9UvrrtIkhaSAfX3eBokWyLHxrix5vTpWC6Gi8MxVvKuEegqVLKJCR65bXZIICMZAjkO2TUA2Z7uM9cTo7ZAxZg==", "dev": true, "peerDependencies": { "@broadpeak/smartlib": "4.5.1-328cb1e", @@ -23316,9 +23316,9 @@ "dev": true }, "@castlabs/prestoplay": { - "version": "6.11.1-beta.1", - "resolved": "https://registry.npmjs.org/@castlabs/prestoplay/-/prestoplay-6.11.1-beta.1.tgz", - "integrity": "sha512-QUxzyxKaK1KEaC/fPKxFyn5BEqYHU5vXl/DngakgydHr3xHbjTCCL/e0uZ3f2RMyu8KmOiK18EjCjoFon+IhMQ==", + "version": "6.11.1-beta.2", + "resolved": "https://registry.npmjs.org/@castlabs/prestoplay/-/prestoplay-6.11.1-beta.2.tgz", + "integrity": "sha512-9UvrrtIkhaSAfX3eBokWyLHxrix5vTpWC6Gi8MxVvKuEegqVLKJCR65bXZIICMZAjkO2TUA2Z7uM9cTo7ZAxZg==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index d94f03e..b9990c8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Player.ts b/src/Player.ts index b32a01e..68c9538 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -299,7 +299,7 @@ export class Player { /** * Indicate that the config was loaded */ - private _configLoaded = false + protected _configLoaded = false /** * UI control visibility manager */ diff --git a/src/components/BaseThemeOverlay.tsx b/src/components/BaseThemeOverlay.tsx index 0894ab7..fd1c870 100644 --- a/src/components/BaseThemeOverlay.tsx +++ b/src/components/BaseThemeOverlay.tsx @@ -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 { @@ -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. */ @@ -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} @@ -139,6 +155,8 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => { return } + const topCompanion = props.renderTopCompanion?.(isFullScreen) + return (
@@ -146,6 +164,11 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => { {/* Top bar */} {renderTopBar()} + {topCompanion ? ( +
+ {topCompanion} +
+ ): null} @@ -154,12 +177,12 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => { - {props.renderBottomCompanion?.()} + {props.renderBottomCompanion?.(isFullScreen)} {/* Bottom bar */}
- + {hasPauseButton ? : null} @@ -176,11 +199,13 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => { />}
- - - + {hasTime ? ( + + + + ): null} {hasAudioControls ? : null} diff --git a/src/components/FullscreenButton.tsx b/src/components/FullscreenButton.tsx index 6f4dcfe..f5ce6d8 100644 --- a/src/components/FullscreenButton.tsx +++ b/src/components/FullscreenButton.tsx @@ -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 = () => { @@ -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. diff --git a/src/interstitial/InterstitialPlayer.tsx b/src/interstitial/InterstitialPlayer.tsx index b366166..b6c925c 100644 --- a/src/interstitial/InterstitialPlayer.tsx +++ b/src/interstitial/InterstitialPlayer.tsx @@ -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 @@ -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}` */ @@ -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. */ @@ -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 } /** @@ -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) { @@ -161,6 +181,7 @@ export const InterstitialPlayer = React.memo((props: InterstitialPlayerProps) => await load() }} onIntermissionEnded={async () => { + props.onIntermissionEnded?.() await playerRef.current.unpause() }} /> diff --git a/src/interstitial/PlayerHlsi.ts b/src/interstitial/PlayerHlsi.ts index 79949bc..e7da401 100644 --- a/src/interstitial/PlayerHlsi.ts +++ b/src/interstitial/PlayerHlsi.ts @@ -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 */ @@ -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', { @@ -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 } /** diff --git a/src/interstitial/components/OverlayHlsi.tsx b/src/interstitial/components/OverlayHlsi.tsx index ae2322e..3b55e31 100644 --- a/src/interstitial/components/OverlayHlsi.tsx +++ b/src/interstitial/components/OverlayHlsi.tsx @@ -6,7 +6,7 @@ import { HorizontalBar } from '../../components/HorizontalBar' import { StartButton } from '../../components/StartButton' import { ControlsVisibilityMode } from '../../services/controls' import { useHlsInterstitial, usePrestoUiEventHlsi } from '../hooks' -import { HlsInterstitial } from '../types' +import { HlsInterstitial, InterstitialControls } from '../types' import { CountDown } from './Countdown' @@ -75,6 +75,14 @@ export type Props = { * If true, track controls are displayed. Defaults to false. */ hasTrackControls?: boolean + /** + * If true, the top controls bar is displayed. Defaults to true. + */ + hasTopControlsBar?: boolean + /** + * Render a custom top companion component. + */ + renderTopCompanion?: (isFullScreen: boolean) => (JSX.Element | null) /** * Custom class name for the player container. */ @@ -83,6 +91,10 @@ export type Props = { * Custom style for the player container. */ style?: React.CSSProperties + /** + * Player controls to shown during interstitial playback. + */ + interstitialControls?: InterstitialControls } /** @@ -93,7 +105,21 @@ export const InterstitialOverlay = React.memo((props: Props) => { const [intermission, setIntermission] = useState(true) const interstitial = useHlsInterstitial() const loop = props.loop ?? true - const seekStep = props.seekStep ?? 10 + + let seekStep = props.seekStep ?? 10 + let hasFullScreenButton = props.hasFullScreenButton ?? true + let hasAudioControls = props.hasAudioControls ?? false + let hasTime = true + let hasPauseButton = true + if (props.interstitialControls && interstitial) { + if (props.interstitialControls.seekButtons === false) { + seekStep = 0 + } + hasFullScreenButton = props.interstitialControls.fullScreen + hasAudioControls = props.interstitialControls.audio + hasTime = props.interstitialControls.time + hasPauseButton = props.interstitialControls.pause + } const endIntermission = useCallback(() => { setIntermission(false) @@ -159,13 +185,17 @@ export const InterstitialOverlay = React.memo((props: Props) => { startButton={false} seekForward={seekStep} seekBackward={-seekStep} - hasAudioControls={props.hasAudioControls ?? false} - hasFullScreenButton={props.hasFullScreenButton ?? true} + hasAudioControls={hasAudioControls} + hasFullScreenButton={hasFullScreenButton} hasTrackControls={props.hasTrackControls ?? false} controlsVisibility={props.controlsVisibility ?? 'always-visible'} seekBarSliderClassName={interstitial ? 'pp-ui-color-gold' : undefined} + hasPauseButton={hasPauseButton} + hasTime={hasTime} showSeekBarCues={props.showInterstitialMarkers} renderBottomCompanion={renderInterstitialInfo} + hasTopControlsBar={props.hasTopControlsBar ?? true} + renderTopCompanion={props.renderTopCompanion} /> }) diff --git a/src/interstitial/types.ts b/src/interstitial/types.ts index 931cfbe..170228d 100644 --- a/src/interstitial/types.ts +++ b/src/interstitial/types.ts @@ -8,3 +8,14 @@ export type HlsInterstitial = { */ podCount: number } + +/** + * Player Controls visible during playback of HLS interstitial assets. + */ +export type InterstitialControls = { + audio: boolean + fullScreen: boolean + pause: boolean + seekButtons: boolean + time: boolean +} diff --git a/src/themes/pp-ui-base-theme.css b/src/themes/pp-ui-base-theme.css index 725ac08..f21b693 100644 --- a/src/themes/pp-ui-base-theme.css +++ b/src/themes/pp-ui-base-theme.css @@ -550,9 +550,9 @@ .pp-ui-seekbar-cue { background-color: rgba(7, 7, 7, 0.3); - height: 130%; + height: 100%; position: absolute; - top: 16px; + top: 14px; }