diff --git a/app/src/App.tsx b/app/src/App.tsx index e13ff5b..139b5af 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -8,6 +8,7 @@ import { Asset, TestAssets } from './Asset' import { BasicOverlayPage } from './BasicOverlayPage' import { ComponentsOverviewPage } from './ComponentsOverviewPage' import { CustomControlsPage } from './CustomControlsPage' +import { InterstitialPage } from './InterstitialPage' import { YoutubeControlsPage } from './YoutubeControlsPage' // load app styles @@ -17,7 +18,7 @@ import '@castlabs/prestoplay/clpp.styles.css' // load the theme import '../../src/themes/pp-ui-base-theme.css' -type Page = 'basic' | 'custom' | 'components' | 'youtube' +type Page = 'basic' | 'custom' | 'components' | 'youtube' | 'interstitial' function getQueryVariable(variable: string) { const searchParams = new URLSearchParams(window.location.search) @@ -60,6 +61,8 @@ export function App() { return } else if (pageId === 'youtube') { return + } else if (pageId === 'interstitial') { + return } return
Unknown Page!
}, [pageId, asset, autoload]) @@ -94,6 +97,7 @@ export function App() { +
diff --git a/app/src/InterstitialPage.tsx b/app/src/InterstitialPage.tsx new file mode 100644 index 0000000..1848f0e --- /dev/null +++ b/app/src/InterstitialPage.tsx @@ -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 ( +
+
+
+ +
+ {mounted ? ( +
+ { + // // @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) =>
{seconds}
} + // loop={false} + // onEnded={() => {}} + // onLoopEnded={() => {}} + /> +
+ ): null} +
+
+ ) +} diff --git a/app/src/styles.css b/app/src/styles.css index 0955f0e..d3805c9 100644 --- a/app/src/styles.css +++ b/app/src/styles.css @@ -110,3 +110,18 @@ nav button:hover { background-color: #b4b4b4; } +.in-page { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.in-container { + width: 1000px; +} + +.in-video-container { + width: 800px; + height: 580px; +} diff --git a/package-lock.json b/package-lock.json index 576d565..a7fead5 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.6.0", + "@castlabs/prestoplay": "^6.11.1-beta.1", "@finga/eslint-config": "^1.2.1", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-image": "^3.0.2", @@ -2347,12 +2347,13 @@ "license": "MIT" }, "node_modules/@castlabs/prestoplay": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@castlabs/prestoplay/-/prestoplay-6.6.0.tgz", - "integrity": "sha512-R6XjgzzKxtrwQoEUBNVFGSMmEZO0QgB0WmhBie59Y8RpcVUZg5FPsz1TPWwTdggu9QVj+cD2s6RB5SeiFNu7iQ==", + "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==", "dev": true, "peerDependencies": { "@broadpeak/smartlib": "4.5.1-328cb1e", + "@broadpeak/smartlib-all-compatibility": "4.5.1-328cb1e", "@broadpeak/smartlib-analytics": "4.5.1-328cb1e", "mux.js": "^5.14.1", "youboralib": "6.8.49" @@ -2361,6 +2362,9 @@ "@broadpeak/smartlib": { "optional": true }, + "@broadpeak/smartlib-all-compatibility": { + "optional": true + }, "@broadpeak/smartlib-analytics": { "optional": true }, @@ -23312,9 +23316,9 @@ "dev": true }, "@castlabs/prestoplay": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@castlabs/prestoplay/-/prestoplay-6.6.0.tgz", - "integrity": "sha512-R6XjgzzKxtrwQoEUBNVFGSMmEZO0QgB0WmhBie59Y8RpcVUZg5FPsz1TPWwTdggu9QVj+cD2s6RB5SeiFNu7iQ==", + "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==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index d900286..6b7ee89 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.6.0", + "@castlabs/prestoplay": "^6.11.1-beta.1", "@finga/eslint-config": "^1.2.1", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-image": "^3.0.2", diff --git a/src/index.ts b/src/index.ts index e49d26a..c2143fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,4 +40,6 @@ export * from './components/VolumeBar' export * from './components/PlayPauseIndicator' export * from './components/VuMeter' +export * from './interstitial/InterstitialPlayer' + export * from './context/PrestoContext' diff --git a/src/interstitial/InterstitialPlayer.tsx b/src/interstitial/InterstitialPlayer.tsx new file mode 100644 index 0000000..b366166 --- /dev/null +++ b/src/interstitial/InterstitialPlayer.tsx @@ -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 + /** + * 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 ( +
+ + { + await load() + }} + onLoopEnded={async () => { + props.onLoopEnded?.() + await playerRef.current.reset() + await load() + }} + onIntermissionEnded={async () => { + await playerRef.current.unpause() + }} + /> + +
+ ) +}) + +InterstitialPlayer.displayName = 'InterstitialPlayer' diff --git a/src/interstitial/PlayerHlsi.ts b/src/interstitial/PlayerHlsi.ts new file mode 100644 index 0000000..79949bc --- /dev/null +++ b/src/interstitial/PlayerHlsi.ts @@ -0,0 +1,183 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { clpp } from '@castlabs/prestoplay' + +import { EventListener, EventType } from '../EventEmitter' +import { Player, UIEvents } from '../Player' +import { Disposer } from '../types' + +import { HlsInterstitial } from './types' + + +export interface UIEventHlsi extends UIEvents { + /** + * Triggered when an HLS interstitial starts playing + */ + hlsInterstitial: (HlsInterstitial | null) + /** + * Triggered when primary content playback ended + */ + ended: undefined + /** + * Triggered when the player instance changes (e.g. when switching between + * primary and interstitial player(s)) + */ + playerChanged: clpp.Player +} + + +/** + * Player of HLS Interstitial streams + */ +export class PlayerHlsi extends Player { + /** + * The HLS interstitial player instance + */ + private _ip: clpp.interstitial.Player | null = null + private _options: clpp.interstitial.Options | null = null + private _interstitialCues: clpp.interstitial.UiCue[] = [] + /** + * True if interstitial asset content currently playing + * (as opposed to primary content) + */ + private _isPlayingInterstitial = false + private _disposers: Disposer[] = [] + + /** + * Initialize the HLS Interstitial player + */ + initHlsi(options: clpp.interstitial.Options) { + if (this._ip) {return} + + this.refreshCues_() + + this._options = options + this._ip = new clpp.interstitial.Player(options) + + this.on('interstitial-item-started', (event) => { + this.emitUIEvent('hlsInterstitial', { + podOrder: event.detail.podOrder as number, + podCount: event.detail.podCount as number, + }) + this._isPlayingInterstitial = true + }) + + this.on('interstitial-ended', () => { + this.emitUIEvent('hlsInterstitial', null) + this._isPlayingInterstitial = false + }) + + this.on('cues-changed', () => { + this.refreshCues_() + }) + + this.on('item-changed', () => { + const item = this._ip?.getCurrentItem() ?? null + if (!item || !item.player) { + console.error('Interstitial: item-changed event without item/player') + return + } + + const player = item.player + this.pp_ = player + + this.removePrestoListeners_() + this.refreshPrestoState_(player) + this.attachPrestoListeners_(player) + this.emitUIEvent('playerChanged', player) + }) + + this.on('primary-ended', () => { + this.emitUIEvent('ended', undefined) + }) + } + + /** + * Load an asset paused. + */ + async loadHlsi(config?: clpp.PlayerConfiguration) { + if (!this._ip || !config) {return} + await this._ip.loadPaused(config) + } + + /** + * Unpause the currently loaded (paused) asset. + */ + async unpause() { + if (!this._ip) {return} + await this._ip.unpause() + } + + /** + * Reset this player to a completely fresh state (same as newly constructed). + */ + async reset() { + await this._ip?.destroy() + this._ip = null + await this.release() + if (this._options) { + this.initHlsi(this._options) + } + } + + /** + * Destroy the player + */ + async destroy() { + this._disposers.forEach((dispose) => dispose()) + this._disposers = [] + await this._ip?.destroy() + this._ip = null + await this.release() + } + + /** + * Get HLS Interstitial cues. + */ + getInterstitialCues() { + return this._interstitialCues + } + + emitUIEvent>(type: K, data: UIEventHlsi[K]): void { + super.emitUIEvent(type as any, data as any) + } + + offUIEvent>(type: K, listener: EventListener): void { + super.offUIEvent(type as any, listener as any) + } + + onUIEvent>(type: K, listener: EventListener): void { + super.onUIEvent(type as any, listener as any) + } + + private refreshCues_ () { + if (this._isPlayingInterstitial) { + this.setCues([]) + return + } + + this._interstitialCues = (this._ip?.getCues() ?? []) + const cues = this._interstitialCues.map((cue) => { + return { + id: cue.cueId, + startTime: cue.startTime, + endTime: cue.endTime, + } + }) + .filter(cue => cue.startTime > 2) // remove interstitial preroll cue(s) + this.setCues(cues) + } + + /** + * Attach an event listener to the interstitial player + */ + private on(event: string, listener: EventListener) { + if (!this._ip) {return} + // @ts-ignore + this._ip.on(event, listener) + this._disposers.push(() => { + // @ts-ignore + this._ip.off(event, listener) + }) + } +} diff --git a/src/interstitial/components/Countdown.tsx b/src/interstitial/components/Countdown.tsx new file mode 100644 index 0000000..6a7a330 --- /dev/null +++ b/src/interstitial/components/Countdown.tsx @@ -0,0 +1,43 @@ +import React, { useEffect, useRef, useState } from 'react' + +type Props = { + seconds: number + render?: (seconds: number) =>(JSX.Element | null) + onDone?: () => any +} + +/** + * Countdown component + */ +const CountDown = React.memo((props: Props) => { + const [count, setCount] = useState(props.seconds) + const countRef = useRef(count) + + useEffect(() => { + const interval = setInterval(() => { + countRef.current -= 1 + if (countRef.current === 0) { + clearInterval(interval) + props.onDone?.() + } else { + setCount((c) => c - 1) + } + }, 1000) + + return () => clearInterval(interval) + }, []) + + if (props.render) { + return props.render(count) + } + + return ( +
+
{count}
+
+ ) +}) + +CountDown.displayName = 'CountDown' + +export { CountDown } diff --git a/src/interstitial/components/OverlayHlsi.tsx b/src/interstitial/components/OverlayHlsi.tsx new file mode 100644 index 0000000..ae2322e --- /dev/null +++ b/src/interstitial/components/OverlayHlsi.tsx @@ -0,0 +1,172 @@ + +import React, { useCallback, useState } from 'react' + +import { BaseThemeOverlay } from '../../components/BaseThemeOverlay' +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 { CountDown } from './Countdown' + + +export type Props = { + /** + * 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 + /** + * Callback called when the intermission ended. + */ + onIntermissionEnded?: () => any + /** + * Callback called when start button was clicked + */ + onStartClick?: () => 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 + /** + * Custom class name for the player container. + */ + className?: string + /** + * Custom style for the player container. + */ + style?: React.CSSProperties +} + +/** + * UI overlay for HLS interstitial player. + */ +export const InterstitialOverlay = React.memo((props: Props) => { + const [hadInteraction, setHadInteraction] = useState(false) + const [intermission, setIntermission] = useState(true) + const interstitial = useHlsInterstitial() + const loop = props.loop ?? true + const seekStep = props.seekStep ?? 10 + + const endIntermission = useCallback(() => { + setIntermission(false) + props.onIntermissionEnded?.() + }, [props.onIntermissionEnded]) + + const onStartClick = useCallback(() => { + setHadInteraction(true) + props.onStartClick?.() + }, [props.onStartClick]) + + usePrestoUiEventHlsi('ended', () => { + if (loop) { + props.onLoopEnded?.() + setIntermission(true) + } else { + props.onEnded?.() + } + }) + + /** + * Render info about the currently playing HLS interstitial if there is one. + */ + const renderInterstitialInfo = () => { + if (!interstitial) { + return null + } + + let content = null + if (props.renderInterstitialLabel) { + content = props.renderInterstitialLabel(interstitial) + } else { + let text = null + if (props.interstitialLabel) { + text = props.interstitialLabel(interstitial) + } else { + text = `Interstitial ${interstitial.podOrder} of ${interstitial.podCount}` + } + content =
{text}
+ } + + return ( + + {content} + + ) + } + + if (!hadInteraction) { + return + } + + if (intermission) { + const duration = props.intermissionDuration ?? 3 + return + } + + return +}) + +InterstitialOverlay.displayName = 'InterstitialOverlay' diff --git a/src/interstitial/components/PlayerSurfaceHlsi.tsx b/src/interstitial/components/PlayerSurfaceHlsi.tsx new file mode 100644 index 0000000..07a5ca5 --- /dev/null +++ b/src/interstitial/components/PlayerSurfaceHlsi.tsx @@ -0,0 +1,118 @@ +import { clpp } from '@castlabs/prestoplay' +import React, { useCallback, useEffect, useRef, useState } from 'react' + +import { BaseComponentProps } from '../../components/types' +import { PrestoContext, PrestoContextType } from '../../context/PrestoContext' +import { PlayerHlsi } from '../PlayerHlsi' + +export interface PlayerProps extends BaseComponentProps { + /** + * The player instance + */ + player: PlayerHlsi + children?: React.ReactNode + /** + * Options of HLS Interstitial Player + */ + interstitialOptions?: Omit +} + +const getContext = (nullableContext: Partial) => { + if (!nullableContext.playerSurface || !nullableContext.player) { + return null + } + + return nullableContext as PrestoContextType +} + +/** + * Player Surface for HLS Interstitial Player + */ +export const PlayerSurfaceHlsi = (props: PlayerProps) => { + const [nullableContext, setPrestoContext ] = useState>({ + playerSurface: undefined, + player: props.player, + presto: undefined, + }) + const containerRef = useRef(null) + + const onAnchorRef = useCallback((anchor: HTMLDivElement) => { + if (!anchor) {return} + props.player.initHlsi({ ...(props.interstitialOptions ?? {}), anchorEl: anchor }) + }, []) + + useEffect(() => { + const setPresto = (player: clpp.Player) => { + setPrestoContext(context => ({ + ...context, + presto: player, + })) + } + props.player.onUIEvent('playerChanged', setPresto) + + return () => { + props.player.offUIEvent('playerChanged', setPresto) + props.player.release() + .catch(err => console.error('Failed to release the player', err)) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const mouseMove = () => { + if (!props.player.controlsVisible && !props.player.slideInMenuVisible) { + props.player.surfaceInteraction() + } + } + + const mouseClick = (e: React.MouseEvent) => { + if (!e.defaultPrevented) { + if (props.player.slideInMenuVisible) { + props.player.slideInMenuVisible = false + e.preventDefault() + } else if (props.player.controlsVisible) { + props.player.controlsVisible = false + e.preventDefault() + } else { + props.player.surfaceInteraction() + e.preventDefault() + } + } + } + + const handleContainerRef = (element: HTMLDivElement|null) => { + containerRef.current = element + + if (element && !nullableContext.playerSurface) { + setPrestoContext(context => ({ + ...context, + playerSurface: element, + })) + } + } + + const context = getContext(nullableContext) + + return ( +
+
+ {context && + +
+ {props.children} +
+
+ } +
+ ) +} diff --git a/src/interstitial/hooks.ts b/src/interstitial/hooks.ts new file mode 100644 index 0000000..d871c52 --- /dev/null +++ b/src/interstitial/hooks.ts @@ -0,0 +1,35 @@ +import { useContext, useEffect, useState } from 'react' + +import { PrestoContext } from '../context/PrestoContext' +import { EventListener, EventType } from '../EventEmitter' + +import { PlayerHlsi, UIEventHlsi } from './PlayerHlsi' +import { HlsInterstitial } from './types' + +/** + * Helper hook to listen to UI related events from the player + */ +export function usePrestoUiEventHlsi>( + eventName: E, handler: EventListener, dependencies?: unknown[], +) { + const player = useContext(PrestoContext).player as PlayerHlsi + dependencies = dependencies || [] + + useEffect(() => { + player.onUIEvent(eventName, handler) + return () => { + player.offUIEvent(eventName, handler) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [eventName, ...dependencies]) +} + + +/** + * @returns Metadata about the currently playing HLS interstitial. + */ +export const useHlsInterstitial = () => { + const [interstitial, setInterstitial] = useState(null) + usePrestoUiEventHlsi('hlsInterstitial', setInterstitial) + return interstitial +} diff --git a/src/interstitial/types.ts b/src/interstitial/types.ts new file mode 100644 index 0000000..931cfbe --- /dev/null +++ b/src/interstitial/types.ts @@ -0,0 +1,10 @@ +export type HlsInterstitial = { + /** + * Ordering number of a HLS interstitial asset inside its pod. (starts from 1) + */ + podOrder: number + /** + * Number of assets in one HLS interstitial (pod). + */ + podCount: number +} diff --git a/src/themes/pp-ui-base-theme.css b/src/themes/pp-ui-base-theme.css index 716adde..725ac08 100644 --- a/src/themes/pp-ui-base-theme.css +++ b/src/themes/pp-ui-base-theme.css @@ -856,3 +856,36 @@ display: none; } } + + +/** Styles for interstitial HLS player */ +.pp-ui-hlsi-video-anchor { + width: 100%; + height: 100%; +} + +.pp-ui-hlsi-companion-label { + margin-left: 18px; + margin-bottom: 8px; + color: white; + text-shadow: black 1px 1px 5px; +} + +.pp-ui-hlsi-player { + width: 100%; + height: 100%; +} + +.pp-ui-countdown { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + background: black; +} + +.pp-ui-countdown-count { + font-size: 200px; +} +/** END: Styles for interstitial HLS player */