diff --git a/CHANGELOG.md b/CHANGELOG.md index a3bde36..87731c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,10 @@ (or `PrestoContext`), in which case it is not necessary to pass the `player` argument anymore. * `HoverContainer` no longer accepts props `listenToHover` and `notTrackFullWidth`. +## New Features + +* VU Meter component. + ## Fixes * Fixes to `BaseThemeOverlay`: diff --git a/app/src/Asset.ts b/app/src/Asset.ts index 895a8a2..be83792 100644 --- a/app/src/Asset.ts +++ b/app/src/Asset.ts @@ -1,10 +1,10 @@ -import type { clpp } from '@castlabs/prestoplay' +import { clpp } from '@castlabs/prestoplay' export interface Asset { /** * The player load configuration */ - config: clpp.LoadConfig + config: clpp.PlayerConfiguration /** * Optional asset title */ @@ -40,7 +40,7 @@ export const TestAssets: Asset[] = [ config: { source: { url: 'https://content.players.castlabs.com/demos/drm-agent/manifest.mpd', - type: 'application/dash+xml', + type: clpp.Type.DASH, drmProtected: true, }, autoplay: true, @@ -72,7 +72,7 @@ export const TestAssets: Asset[] = [ config: { source: { url: 'https://content.players.castlabs.com/demos/clear-segmented/master.m3u8', - type: 'application/x-mpegurl', + type: clpp.Type.HLS, drmProtected: false, }, autoplay: false, diff --git a/app/src/YoutubeControlsPage.tsx b/app/src/YoutubeControlsPage.tsx index 9991348..893e733 100644 --- a/app/src/YoutubeControlsPage.tsx +++ b/app/src/YoutubeControlsPage.tsx @@ -75,7 +75,7 @@ const Ui = (props: PropsUi) => { const playerEnabled = usePrestoEnabledState(player) return ( - +
diff --git a/docs/vu_meter_avg.drawio b/docs/vu_meter_avg.drawio new file mode 100644 index 0000000..854ff69 --- /dev/null +++ b/docs/vu_meter_avg.drawio @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index f242c2a..e95b51d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@babel/preset-env": "^7.22.4", "@babel/preset-react": "^7.22.3", "@babel/preset-typescript": "^7.21.5", - "@castlabs/prestoplay": "^6.2.7", + "@castlabs/prestoplay": "^6.6.0", "@finga/eslint-config": "^1.2.1", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-image": "^3.0.2", @@ -74,7 +74,7 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "@castlabs/prestoplay": "~6.2.0", + "@castlabs/prestoplay": "^6.6.0", "react": "^18.0.0", "react-dom": "^18.0.0" } @@ -2297,14 +2297,23 @@ "license": "MIT" }, "node_modules/@castlabs/prestoplay": { - "version": "6.2.7", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@castlabs/prestoplay/-/prestoplay-6.6.0.tgz", + "integrity": "sha512-R6XjgzzKxtrwQoEUBNVFGSMmEZO0QgB0WmhBie59Y8RpcVUZg5FPsz1TPWwTdggu9QVj+cD2s6RB5SeiFNu7iQ==", "dev": true, - "license": "See https://castlabs.com/legal/", "peerDependencies": { + "@broadpeak/smartlib": "4.5.1-328cb1e", + "@broadpeak/smartlib-analytics": "4.5.1-328cb1e", "mux.js": "^5.14.1", - "youboralib": "^6.8.12" + "youboralib": "6.8.49" }, "peerDependenciesMeta": { + "@broadpeak/smartlib": { + "optional": true + }, + "@broadpeak/smartlib-analytics": { + "optional": true + }, "mux.js": { "optional": true }, @@ -19762,7 +19771,9 @@ "dev": true }, "@castlabs/prestoplay": { - "version": "6.2.7", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@castlabs/prestoplay/-/prestoplay-6.6.0.tgz", + "integrity": "sha512-R6XjgzzKxtrwQoEUBNVFGSMmEZO0QgB0WmhBie59Y8RpcVUZg5FPsz1TPWwTdggu9QVj+cD2s6RB5SeiFNu7iQ==", "dev": true, "requires": {} }, diff --git a/package.json b/package.json index 828cb2d..b265969 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@babel/preset-env": "^7.22.4", "@babel/preset-react": "^7.22.3", "@babel/preset-typescript": "^7.21.5", - "@castlabs/prestoplay": "^6.2.7", + "@castlabs/prestoplay": "^6.6.0", "@finga/eslint-config": "^1.2.1", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-image": "^3.0.2", @@ -89,7 +89,7 @@ "typescript": "^4.8.4" }, "peerDependencies": { - "@castlabs/prestoplay": "~6.2.0", + "@castlabs/prestoplay": "^6.6.0", "react": "^18.0.0", "react-dom": "^18.0.0" }, diff --git a/src/Player.ts b/src/Player.ts index 59c3327..02e7d72 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -29,6 +29,11 @@ export type PlayerInitializer = (presto: clpp.Player) => void */ type Action = () => Promise +type Range = { + start: number + end: number +} + /** * Player states */ @@ -334,7 +339,7 @@ export class Player { * * @private */ - private _config: clpp.LoadConfig | null = null + private _config: clpp.PlayerConfiguration | null = null /** * Indicate that the config was loaded @@ -395,15 +400,20 @@ export class Player { this.pp_.on(clpp.events.VIDEO_TRACK_CHANGED, handlePlayerTracksChanged('video')) this.pp_.on(clpp.events.TEXT_TRACK_CHANGED, handlePlayerTracksChanged('text')) - this.pp_.on(clpp.events.STATE_CHANGED, (event: clpp.Event) => { - const e = event as clpp.StateChangeEvent + this.pp_.on(clpp.events.STATE_CHANGED, (event: any) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const e = event + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const currentState = toState(e.detail.currentState) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const previousState = toState(e.detail.previousState) - + this.emitUIEvent('statechanged', { currentState, previousState, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment reason: e.detail.reason, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment timeSinceLastStateChangeMS: e.detail.timeSinceLastStateChangeMS, }) @@ -448,9 +458,9 @@ export class Player { }) }) - this.pp_.on(clpp.events.BITRATE_CHANGED, (e: clpp.Event) => { + this.pp_.on(clpp.events.BITRATE_CHANGED, (e: any) => { const tm = this.trackManager - + if (tm) { this.playingVideoTrack = fromPrestoTrack(tm, e.detail.rendition as clpp.Rendition, 'video') } else { @@ -530,12 +540,12 @@ export class Player { return this.pp_ ? this.pp_.isLive() : false } - get seekRange(): { start: number; end: number } { - return this.pp_ ? this.pp_.getSeekRange() : { start: 0, end: 0 } + get seekRange(): Range { + return this.pp_ ? this.pp_.getSeekRange() as Range : { start: 0, end: 0 } } get volume(): number { - return this.pp_ ? this.pp_.getVolume() : 1 + return this.pp_ ? this.pp_.getVolume() ?? 0 : 1 } set volume(volume: number) { @@ -548,7 +558,7 @@ export class Player { } get muted() { - return this.pp_ ? this.pp_.isMuted() : false + return this.pp_ ? this.pp_.isMuted() ?? false : false } set muted(muted: boolean) { @@ -596,7 +606,7 @@ export class Player { } } - load(config?: clpp.LoadConfig, autoload = false) { + load(config?: clpp.PlayerConfiguration, autoload = false) { if (config) { this._config = config return this.action(async () => { @@ -705,9 +715,8 @@ export class Player { async presto(): Promise { await this._actionQueuePromise - // @ts-ignore I am not sure if this.pp_ is - // always defined here or if it can possibly be null... - return this.pp_ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + return (this.pp_ as clpp.Player) } /** @@ -788,7 +797,6 @@ export class Player { set videoTracks(value: Track[]) { value = value.filter(v => { - // @ts-ignore type conflicet between Track and Rendition return v.ppTrack && v.ppTrack.height && v.ppTrack.width }) if (value.length > 0 && !value.find(t => t.id === 'abr')) { @@ -828,20 +836,14 @@ export class Player { } else { track.selected = true if (track.type === 'audio') { - // @ts-ignore Rendition is not assignable to Track - // there is a type conflict tm.setAudioTrack(track.ppTrack) this.audioTrack = getActiveTrack(tm, 'audio') this.audioTracks = getTracks(tm, 'audio') } else if (track.type === 'text') { - // @ts-ignore Rendition is not assignable to Track - // there is a type conflict tm.setTextTrack(track.ppTrack) this.textTrack = getActiveTrack(tm, 'text') this.textTracks = getTracks(tm, 'text') } else if (track.type === 'video') { - // @ts-ignore Track is not assignable to Rendition - // there is a miss match between the types tm.setVideoRendition(track.ppTrack, true) this.videoTrack = getActiveTrack(tm, 'video') this.videoTracks = getTracks(tm, 'video') diff --git a/src/components/MuteButton.tsx b/src/components/MuteButton.tsx index b32d4e9..9d5a463 100644 --- a/src/components/MuteButton.tsx +++ b/src/components/MuteButton.tsx @@ -29,10 +29,10 @@ export const MuteButton = (props: MuteButtonProps) => { const enabled = usePrestoEnabledState() usePrestoCoreEvent('volumechange', (e, presto) => { - setMuted(presto.isMuted()) + setMuted(presto.isMuted() ?? false) }) usePrestoCoreEvent('loadedmetadata', (e, presto) => { - setMuted(presto.isMuted()) + setMuted(presto.isMuted() ?? false) }) function toggle(e: React.MouseEvent) { diff --git a/src/components/PlayerSurface.tsx b/src/components/PlayerSurface.tsx index 8a4228f..3590b7f 100644 --- a/src/components/PlayerSurface.tsx +++ b/src/components/PlayerSurface.tsx @@ -25,7 +25,7 @@ export interface PlayerProps extends BaseComponentProps { * The PRESTOplay player configuration to load and play a video * https://demo.castlabs.com/#/docs?q=clpp#PlayerConfiguration */ - config?: clpp.LoadConfig + config?: clpp.PlayerConfiguration /** * The PRESTOplay player configuration to initialize the player * https://demo.castlabs.com/#/docs?q=clpp#PlayerConfiguration diff --git a/src/components/Thumbnail.tsx b/src/components/Thumbnail.tsx index 15e9556..503804a 100644 --- a/src/components/Thumbnail.tsx +++ b/src/components/Thumbnail.tsx @@ -28,7 +28,7 @@ export interface ThumbnailProps extends BaseComponentProps { */ const usePlugin = () => { const { presto } = useContext(PrestoContext) - return presto.getPlugin(clpp.thumbnails.ThumbnailsPlugin.Id) as clpp.ThumbnailsPlugin | null + return presto.getPlugin(clpp.thumbnails.ThumbnailsPlugin.Id) as clpp.thumbnails.ThumbnailsPlugin | null } const DEFAULT_STYLE = { height: 130 } diff --git a/src/components/VolumeBar.tsx b/src/components/VolumeBar.tsx index da32aca..ceafbe2 100644 --- a/src/components/VolumeBar.tsx +++ b/src/components/VolumeBar.tsx @@ -50,7 +50,9 @@ export const VolumeBar = (props: VolumeBarProps) => { const presto = await player.presto() const current = presto.isMuted() ? 0 : presto.getVolume() let targetPosition = current - + + if (targetPosition == null || current == null) {return} + if (e.key === 'ArrowLeft') { targetPosition = Math.max(0, current + (-0.1)) e.preventDefault() diff --git a/src/components/VuMeter.tsx b/src/components/VuMeter.tsx new file mode 100644 index 0000000..ebeb777 --- /dev/null +++ b/src/components/VuMeter.tsx @@ -0,0 +1,33 @@ +import React, { useCallback, useContext, useEffect, useRef } from 'react' + +import { PrestoContext } from '../context/PrestoContext' +import { VolumeMeterService, VuMeterConfig } from '../services/volumeMeterService' + +import type { BaseComponentProps } from './types' + +export type Props = BaseComponentProps & { + config?: VuMeterConfig + width: number + height: number +} + +/** + * Volume Unit Meter + */ +export const VuMeter = (props: Props) => { + const ctx = useContext(PrestoContext) + const serviceRef = useRef(new VolumeMeterService(ctx.presto)) + + const onRef = useCallback((canvas: HTMLCanvasElement) => { + serviceRef.current.configure(canvas, props.config ?? {}) + serviceRef.current.mount() + }, []) + + useEffect(() => { + return () => { + serviceRef.current.unmount() + } + }, []) + + return +} diff --git a/src/index.ts b/src/index.ts index 517354b..e49d26a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,5 +38,6 @@ export * from './components/TrackSelectionList' export * from './components/VerticalBar' export * from './components/VolumeBar' export * from './components/PlayPauseIndicator' +export * from './components/VuMeter' export * from './context/PrestoContext' diff --git a/src/react.ts b/src/react.ts index 9c43dc8..7e32519 100644 --- a/src/react.ts +++ b/src/react.ts @@ -6,7 +6,7 @@ import { PrestoContext } from './context/PrestoContext' import { EventListener, EventType } from './EventEmitter' import { Player, State, UIEvents } from './Player' -export type ClppEventHandler = (e: clpp.Event, presto: clpp.Player) => void +type ClppEventHandler = (event: Record, presto: clpp.Player) => void /** * This is a React hook that can be used to listen to presto play events. @@ -28,7 +28,7 @@ export function usePrestoCoreEvent( const presto = useContext(PrestoContext).presto ?? presto_ useEffect(() => { - const handleEvent = (event: clpp.Event) => { + const handleEvent = (event: Record) => { handler(event, presto) } diff --git a/src/services/volumeMeterService.ts b/src/services/volumeMeterService.ts new file mode 100644 index 0000000..7333899 --- /dev/null +++ b/src/services/volumeMeterService.ts @@ -0,0 +1,339 @@ +import { clpp } from '@castlabs/prestoplay' + +type GradientColorStop = { + stop: number + color: string +} + +type AnalyzerContext = { + analyser: AnalyserNode + dataArray: Uint8Array +} + +export type ConfigInternal = { + fontSize: number + fontColor: string + fontFamily: string + backgroundColor: string + barGradient: GradientColorStop[] + barWidth?: number + barLeftOffset?: number + barSpacing: number + smoothingTimeConstant: number + frequencyBinCount: number + levelLabel: (level: number) => string + minDecibels: number + maxDecibels: number +} + +export type VuMeterConfig = Partial + +const DEFAULT_CONFIG: ConfigInternal = { + fontSize: 12, + fontColor: 'rgba(255, 255, 255, 0.7)', + fontFamily: 'sans-serif', + backgroundColor: 'rgba(0,0,0,1)', + barGradient: [ + { stop: 0, color: 'green' }, + { stop: 0.8, color: 'yellow' }, + { stop: 1, color: 'red' }, + ], + barSpacing: 4, + smoothingTimeConstant: 0.4, + frequencyBinCount: 1, + levelLabel: (level => `${level} dB`), + minDecibels: -100, + maxDecibels: -30, +} + +const dimensions = (element: HTMLCanvasElement) => { + return { + width: element.width, + height: element.height, + } +} + +/** + * Volume Meter Service. + * It can analyze volume and animate the results on a canvas. + */ +export class VolumeMeterService { + private animationRequestID: number | null = null + private attachedVideoElement: HTMLMediaElement | null = null + private audioCtx: AudioContext | null = null + private audioSource: AudioNode | null = null + private canvas: HTMLCanvasElement | null = null + private config: ConfigInternal = DEFAULT_CONFIG + private disposers: (() => void)[] = [] + private enabled = false + private mediaElementToSourceNodeMap: Map = new Map() + private log = new clpp.log.Logger('clpp.services.VuMeter') + + constructor (private player: clpp.Player) {} + + /** + * Configure. + */ + configure (canvas: HTMLCanvasElement, config?: VuMeterConfig) { + this.canvas = canvas + this.config = { + ...DEFAULT_CONFIG, + ...config, + } + + this.log.info('VU meter configured', config) + } + + /** + * Mount the VU meter to the canvas element. + */ + mount () { + if (this.canvas) { + this.drawEmpty(this.canvas) + this.log.info('VU meter mounted') + } + + // Handle start / re-start of content playback + const onPlaying = () => { + this.attachToMainVideo() + } + this.player.on('playing', onPlaying) + this.disposers.push(() => this.player.off('playing', onPlaying)) + + // Handle time update (if mounted after playback started) + const onTimeupdate = () => { + this.attachToMainVideo() + } + this.player.on('timeupdate', onTimeupdate) + this.disposers.push(() => this.player.off('timeupdate', onTimeupdate)) + + // Handle pause + const onPaused = () => { + this.disable() + } + this.player.on('paused', onPaused) + this.disposers.push(() => this.player.off('paused', onPaused)) + + // Handle end + const onEnded = () => { + this.disable() + } + this.player.on('ended', onEnded) + this.disposers.push(() => this.player.off('ended', onEnded)) + } + + /** + * Un-Mount the VU meter from the canvas element. + */ + unmount () { + this.disable() + if (this.canvas) { + this.clear(this.canvas) + this.canvas = null + } + this.disposers.forEach(dispose => dispose()) + this.disposers = [] + this.log.info('VU meter un-mounted') + } + + /** + * Enable the VU meter - start measuring the audio volume. + */ + enable () { + if (this.enabled || !this.attachedVideoElement || !this.canvas) { + return + } + + this.log.info('VU meter enabled') + + this.enabled = true + + const audioContext = this.createAudioContext() + this.audioSource = this.getAudioSource(audioContext, this.attachedVideoElement) + const splitter = audioContext.createChannelSplitter(2) + this.audioSource.connect(splitter) + + const leftAudioCtx = this.createAnalyserContext(audioContext, splitter, 0) + const rightAudioCtx = this.createAnalyserContext(audioContext, splitter, 1) + + const paintRecursively = () => { + this.animationRequestID = requestAnimationFrame(() => { + if (!this.canvas) {return} + this.draw(this.canvas, leftAudioCtx, rightAudioCtx) + paintRecursively() + }) + } + + paintRecursively() + } + + /** + * Disable the VU meter - stop measuring the audio volume. + */ + disable () { + if (!this.enabled) { + return + } + + if (this.animationRequestID != null) { + cancelAnimationFrame(this.animationRequestID) + this.animationRequestID = null + } + if (this.canvas) { + this.drawEmpty(this.canvas) + } + + this.enabled = false + } + + /** + * Attach to volume of the main video element. + */ + private attachToMainVideo () { + const media = this.player.getSurface()?.getMedia() + + if (media === this.attachedVideoElement) { + if (!this.enabled) { + this.enable() + } + return + } + + this.disable() + + if (!media) { + this.log.warn('Failed to get Main video element.') + return + } + + this.setVideoElement(media) + this.enable() + this.log.info('Attached to main video.') + } + + private setVideoElement (element: HTMLMediaElement) { + this.log.info(`VU meter set video element ID: ${element.id},` + + ` classes: ${element.className}`) + + this.attachedVideoElement = element + } + + private getAudioSource(audioContext: AudioContext, element: HTMLMediaElement): AudioNode { + let audioSource = this.mediaElementToSourceNodeMap.get(element) + if (audioSource) { + return audioSource + } + audioSource = audioContext.createMediaElementSource(element) + // `createMediaElementSource` disconnected audio from output, + // so connect it back + audioSource.connect(audioContext.destination) + this.mediaElementToSourceNodeMap.set(element, audioSource) + return audioSource + } + + private clear (canvas: HTMLCanvasElement) { + if (!canvas) {return} + const canvasCtx = canvas.getContext('2d') + if (!canvasCtx) {return} + + const { width, height } = dimensions(canvas) + canvasCtx.clearRect(0, 0, width, height) + } + + private drawEmpty (canvas: HTMLCanvasElement) { + const canvasCtx = canvas.getContext('2d') + if (!canvasCtx) { + return + } + + const { width, height } = dimensions(canvas) + canvasCtx.clearRect(0, 0, width, height) + canvasCtx.fillStyle = this.config.backgroundColor + canvasCtx.fillRect(0, 0, width, height) + + // Render numbering + canvasCtx.fillStyle = this.config.fontColor + canvasCtx.font = `${this.config.fontSize}px ${this.config.fontFamily}` + const steps = Math.round(( + this.config.maxDecibels - this.config.minDecibels) / 10) + const startStep = Math.round(- this.config.maxDecibels / 10) + const heightStep = height / steps + for (let i = 1; i < steps; i++) { + const label = this.config.levelLabel(-10 * (i + startStep)) + canvasCtx.fillText(label, 3, i * heightStep) + } + } + + private draw (canvas: HTMLCanvasElement, leftAudioCtx: AnalyzerContext, rightAudioCtx: AnalyzerContext) { + this.drawEmpty(canvas) + + const canvasCtx = canvas.getContext('2d') + if (!canvasCtx) {return} + + const { width, height } = dimensions(canvas) + const leftBarHeight = this.computeBarHeight(leftAudioCtx, height) + const rightBarHeight = this.computeBarHeight(rightAudioCtx, height) + const barWidth = this.config.barWidth || (width / 6) + const barLeftOffset = this.config.barLeftOffset || (3.4 * barWidth) + const barSpacing = this.config.barSpacing + + const gradient = canvasCtx.createLinearGradient(0, height, 0, 0) + this.config.barGradient.forEach(({ stop, color }) => { + gradient.addColorStop(stop, color) + }) + + // Render volume bar + canvasCtx.fillStyle = gradient + canvasCtx.fillRect(barLeftOffset, height - leftBarHeight, + barWidth, leftBarHeight) + canvasCtx.fillRect(barLeftOffset + barWidth + barSpacing, + height - rightBarHeight, barWidth, rightBarHeight) + } + + private computeBarHeight (analyzerCtx: AnalyzerContext, maxHeight: number): number { + const { analyser, dataArray } = analyzerCtx + const MAX_VALUE = 255 + + analyser.getByteFrequencyData(dataArray) + + const groupCount = this.config.frequencyBinCount + const groupSize = dataArray.length / groupCount + + // Calculate the averages + const averages: number[] = [] + for (let i = 0; i < groupCount; i++) { + const start = i * groupSize + const end = (i + 1) * groupSize + const array = dataArray.slice(start, end) + const average = array.reduce((a, b) => a + b, 0) / groupSize + averages.push(average) + } + const average = Math.max(...averages) + const averagePercent = average / MAX_VALUE + + const barHeight = maxHeight * averagePercent + return barHeight + } + + private createAnalyserContext (audioContext: AudioContext, audioNode: AudioNode, channel: number): AnalyzerContext { + const analyser = audioContext.createAnalyser() + analyser.smoothingTimeConstant = this.config.smoothingTimeConstant + analyser.fftSize = 256 + const bufferLength = analyser.frequencyBinCount + const dataArray = new Uint8Array(bufferLength) + audioNode.connect(analyser, channel) + analyser.minDecibels = this.config.minDecibels + analyser.maxDecibels = this.config.maxDecibels + return { analyser, dataArray } + } + + private createAudioContext(): AudioContext { + if (!this.audioCtx) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.audioCtx = new (window.AudioContext || window.webkitAudioContext)() + } + return this.audioCtx + } +} diff --git a/story/stories/components/VuMeter.mdx b/story/stories/components/VuMeter.mdx new file mode 100644 index 0000000..caf1a14 --- /dev/null +++ b/story/stories/components/VuMeter.mdx @@ -0,0 +1,15 @@ +import { Canvas, Meta, Primary, Controls, Story, ArgTypes } from '@storybook/blocks'; +import * as VuMeterStories from './VuMeter.stories'; + + + + + +# VU Meter + +Component that shows the current levels of volume (left and right channels separately). + +### Props + + + diff --git a/story/stories/components/VuMeter.stories.tsx b/story/stories/components/VuMeter.stories.tsx new file mode 100644 index 0000000..761ec57 --- /dev/null +++ b/story/stories/components/VuMeter.stories.tsx @@ -0,0 +1,81 @@ +import React, { useMemo, useState } from 'react' +import '@castlabs/prestoplay/clpp.styles.css' +import '../../../src/themes/pp-ui-base-theme.css' + +import { clpp } from '@castlabs/prestoplay' +import '@castlabs/prestoplay/cl.mse' +import '@castlabs/prestoplay/cl.dash' +import '@castlabs/prestoplay/cl.hls' +import '@castlabs/prestoplay/cl.htmlcue' +import '@castlabs/prestoplay/cl.ttml' +import '@castlabs/prestoplay/cl.vtt' + +import { BaseThemeOverlay, Player, PlayerSurface, PrestoContext, PrestoContextType, VuMeter } from '../../../src' +import { TEST_ASSETS } from '../Asset' + +import type { Meta, StoryObj } from '@storybook/react' + + +const Component = () => { + const [context, setContext] = useState(null) + + const player = useMemo(() => { + return new Player(pp => { + pp.use(clpp.dash.DashComponent) + pp.use(clpp.hls.HlsComponent) + pp.use(clpp.htmlcue.HtmlCueComponent) + pp.use(clpp.ttml.TtmlComponent) + pp.use(clpp.vtt.VttComponent) + }) + }, []) + + const playerConfig = { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + source: TEST_ASSETS[0].config.source ?? '', + } + + return ( +
+ + + + {context && ( + + + + )} +
+ ) +} + +const meta: Meta = { + title: 'components/VU Meter', + component: Component, + parameters: { + docs: { + source: { + code: ` +import { VuMeter } from '@castlabs/prestoplay-react-components' + +return ( + +) + `, + }, + }, + }, +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = {} diff --git a/types/presto.d.ts b/types/presto.d.ts deleted file mode 100644 index 2982e59..0000000 --- a/types/presto.d.ts +++ /dev/null @@ -1,205 +0,0 @@ -/** - * Taken from @see {@link https://demo.castlabs.com/#/docs?q=clpp.Player} - */ -declare module '@castlabs/prestoplay' { - export namespace clpp { - type Event = { - detail: Record - } - - export type StateChangeEvent = { - detail: { - currentState: number - previousState: number - reason: clpp.events.BufferingReasons - timeSinceLastStateChangeMS: number - } - } - - type EventCallback = (payload: Event) => void - - interface IClass { - new (): unknown - } - - export class Player { - constructor(element: HTMLElement|string, config?: PlayerConfiguration) - - static State: { - IDLE: 0 - PREPARING: 1 - BUFFERING: 2 - PLAYING: 3 - PAUSED: 4 - ENDED: 5 - ERROR: 6 - UNSET: 7 - } - - destroy(): Promise - /** - * Duration in seconds or -1 if the duration is unknown. - */ - getDuration(): number - getPlaybackRate(): number - getPlugin(id: string): PlayerPlugin | null - getPosition(): number - /** - * Get the range of time (in seconds) that seeking is allowed. - * If the player has not loaded content, this will return a range from 0 to 0. - */ - getSeekRange(): { start: number; end: number } - getState(): number - getTrackManager(): TrackManager | null - /** - * The volume as a value between 0 and 1. - */ - getVolume(): number - isLive(): boolean - isMuted(): boolean - isPaused(): boolean - load(config: LoadConfig): Promise - off(event: string, callback: EventCallback): void - one(event: string, callback: EventCallback): void - on(event: string, callback: EventCallback): void - pause(): Promise - play(): Promise - release(): Promise - seek(time: number): Promise - setMuted(is: boolean): void - /** - * Set speed of the playback, where 1 means "normal" speed. - */ - setPlaybackRate(rate: number): void - /** - * The volume as a value between 0 and 1. - */ - setVolume(volume: number): void - use(component: IClass): void - } - - /** - * @see {@link https://demo.castlabs.com/#/docs?q=clpp.TrackManager} - */ - export class TrackManager { - getAudioTrack(): Track | null - getAudioTracks(): Track[] - getTextTrack(): Track | null - getTextTracks(): Track[] - getVideoRendition(): Rendition | null - getVideoTrack(): Track | null - getVideoTracks(): Track[] - isAbrEnabled(): boolean - setAudioTrack(track: Track | null): void - setTextTrack(track: Track | null): void - setVideoRendition(rendition: Rendition, clearBuffer: boolean): void - setVideoTrack(track: Track | null): void - } - - /** - * @see {@link https://demo.castlabs.com/#/docs?q=clpp.Rendition} - */ - export type Rendition = { - bandwidth: number - height: number - id: string - originalId: string | null - track: Track - width: number - } - - /** - * @see {@link https://demo.castlabs.com/#/docs?q=clpp.Track} - */ - export type Track = { - id: string - type: string - renditions: Rendition[] - } - - export class PlayerPlugin {} - - export type Source = { - url: string - type?: string - drmProtected: boolean - name: string - isLive: boolean - videoMimeType: string - audioMimeType: string - } - - /** - * @see {@link https://demo.castlabs.com/#/docs?q=clpp#PlayerConfiguration} - */ - export type PlayerConfiguration = Record & { - autoplay?: boolean - muted?: boolean - drm?: { - env: string - customData?: Record - } - } - - export type SourceLike = string | clpp.Source | string[] | clpp.Source[] - - export type LoadConfig = SourceLike | clpp.PlayerConfiguration & { source: SourceLike } - - export namespace events { - export const BITRATE_CHANGED = 'bitratechanged' - export const TRACKS_ADDED = 'tracksadded' - export const AUDIO_TRACK_CHANGED = 'audiotrackchanged' - export const VIDEO_TRACK_CHANGED = 'videotrackchanged' - export const TEXT_TRACK_CHANGED = 'texttrackchanged' - export const STATE_CHANGED = 'statechanged' - - export enum BufferingReasons { - SEEKING, - NO_DATA, - } - } - - export namespace Track { - export enum Type { - AUDIO = 'audio', - TEXT = 'text', - VIDEO = 'video', - } - } - - export namespace thumbnails { - export class ThumbnailsPlugin { - static Id: string - } - } - - export namespace vtt { - export class VttComponent {} - } - - export namespace ttml { - export class TtmlComponent {} - } - - export namespace htmlcue { - export class HtmlCueComponent {} - } - - export namespace hls { - export class HlsComponent {} - } - - export namespace dash { - export class DashComponent {} - } - - export class ThumbnailsPlugin { - get(position: number): Promise - } - - export class Thumbnail { - element(): HTMLElement - load(): Promise - } - } -}