diff --git a/.eslintrc.js b/.eslintrc.js
index d15cd9a..5285fee 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -24,6 +24,7 @@ module.exports = {
//
) => event.altKey}}/>
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
+ "@typescript-eslint/no-unsafe-assignment": "off",
},
overrides: [
{
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 447fd05..341f627 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,19 @@
+# 0.8.3 (Beta)
+
+## New Features
+
+* Add option to keep player controls hidden.
+* Make `BaseThemeOverlay` more configurable:
+ * Add option to hide buttons (audio, full screen, track options)
+ * Add option to hide the top bar of player controls
+ * Add option to render a companion component above the bottom bar
+ of player controls
+* Add option to display cues on the seek bar
+
+## Fixes
+
+* Fix rounding of duration.
+
# 0.7.3 (Beta)
## Fixes
diff --git a/app/index.html b/app/index.html
new file mode 100644
index 0000000..5d17cb8
--- /dev/null
+++ b/app/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
PRESTOplay React Components
+
+
+
+
+
+
+
diff --git a/app/src/index.html b/app/src/index.html
deleted file mode 100644
index 0a12e85..0000000
--- a/app/src/index.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
PRESTOplay React Components
-
-
-
-
-
-
-
diff --git a/package.json b/package.json
index 6950647..d900286 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,8 @@
"test:watch": "jest --watch",
"lint": "npx eslint .",
"storybook": "storybook dev --port 6006",
- "build-storybook": "storybook build --output-dir ./dist/storybook"
+ "build-storybook": "storybook build --output-dir ./dist/storybook",
+ "prepack": "npm run build"
},
"license": "Apache-2.0",
"devDependencies": {
diff --git a/scripts/rollup.config.app.mjs b/scripts/rollup.config.app.mjs
index 5f745e8..3a71287 100644
--- a/scripts/rollup.config.app.mjs
+++ b/scripts/rollup.config.app.mjs
@@ -49,7 +49,7 @@ const options = [
open: true,
verbose: true,
contentBase: ["", "app", "app/src"],
- host: "0.0.0.0",
+ host: "localhost",
port: "3000",
}),
livereload({
diff --git a/src/Player.ts b/src/Player.ts
index 02e7d72..d445a2b 100644
--- a/src/Player.ts
+++ b/src/Player.ts
@@ -13,6 +13,7 @@ import {
TrackType,
} from './Track'
import { defaultTrackLabel, defaultTrackSorter, TrackLabeler, TrackLabelerOptions, TrackSorter } from './TrackLabeler'
+import { Cue, Disposer } from './types'
/**
* The player initializer is a function that receives the presto play instance
@@ -182,6 +183,10 @@ export interface UIEvents {
* Unset, Idle, and Error states.
*/
enabled: boolean
+ /**
+ * Event triggered when timeline/seek bar cues change.
+ */
+ cuesChanged: Cue[]
}
/**
@@ -201,153 +206,120 @@ const isEnabledState = (state: State): boolean => {
export class Player {
/**
* The player instance
- * @private
*/
- private pp_: clpp.Player | null = null
+ protected pp_: clpp.Player | null = null
/**
* We maintain a queue of actions that will be posted towards the player
* instance once it is initialized
- *
- * @private
*/
private _actionQueue_: Action[] = []
/**
* Function that resolves the player initialization
- *
- * @private
*/
private _actionQueueResolved?: () => void
/**
* A promise that resolves once the player is initialized
- * @private
*/
private readonly _actionQueuePromise: Promise
/**
* The player initializer
- * @private
*/
private readonly _initializer?: PlayerInitializer
-
/**
* Internal state that indicates that the "controls" are visible
- *
- * @private
*/
private _controlsVisible = false
/**
* Internal controls that indicate that the slide in menu is visible
- * @private
*/
private _slideInMenuVisible = false
/**
* The currently playing video track
- *
- * @private
*/
private _playingVideoTrack: Track | undefined
-
/**
* The currently selected video track
- *
- * @private
*/
private _videoTrack: Track = getUnavailableTrack('video')
/**
* The currently selected audio track
- * @private
*/
private _audioTrack: Track = getUnavailableTrack('audio')
/**
* The currently selected text track
- *
- * @private
*/
private _textTrack: Track = getUnavailableTrack('text')
-
/**
* All available video tracks
- *
- * @private
*/
private _videoTracks: Track[] = []
/**
* All available audio tracks
- *
- * @private
*/
private _audioTracks: Track[] = []
-
/**
* All available text tracks
- *
- * @private
*/
private _textTracks: Track[] = []
-
/**
* The track sorter
- *
- * @private
*/
private _trackSorter: TrackSorter = defaultTrackSorter
-
/**
* The track labeler
- *
- * @private
*/
private _trackLabeler: TrackLabeler = defaultTrackLabel
-
/**
* The track labeler options
- * @private
*/
private _trackLabelerOptions?: TrackLabelerOptions
-
/**
* The event emitter for UI related events
- *
- * @private
*/
private readonly _eventEmitter = new EventEmitter()
-
/**
* This is true while we are waiting for a user initiated seek even to
* complete
- *
- * @private
*/
private _isUserSeeking = false
-
/**
* The target of the last user initiated seek event. We use this in case
* there were more seek events while we were waiting for the last event
* to complete
- *
- * @private
*/
private _userSeekingTarget = -1
-
/**
* Proxy the playback rate
- * @private
*/
private _rate = 1
-
/**
* The current player configuration
- *
- * @private
*/
private _config: clpp.PlayerConfiguration | null = null
-
/**
* Indicate that the config was loaded
- * @private
*/
private _configLoaded = false
-
+ /**
+ * UI control visibility manager
+ */
private _controls = new Controls()
+ /**
+ * Disposers of listeners on the PRESTOplay player instance
+ */
+ private _prestoDisposers: Disposer[] = []
+ /**
+ * The last playback state
+ */
+ private _lastPlaybackState: State = State.Unset
+ /**
+ * If true state changes to "ended" state should be ignored
+ */
+ private _ignoreStateEnded = false
+ /**
+ * Timeline cues
+ */
+ private _cues: Cue[] = []
constructor(initializer?: PlayerInitializer) {
this._initializer = initializer
@@ -375,7 +347,40 @@ export class Player {
this.pp_ = new clpp.Player(element, baseConfig)
- const handlePlayerTracksChanged = (type?: TrackType) => {
+ this.attachPrestoListeners_(this.pp_)
+
+ if (this._initializer) {
+ this._initializer(this.pp_)
+ }
+ for (let i = 0; i < this._actionQueue_.length; i++) {
+ await this._actionQueue_[i]()
+ }
+ this._actionQueue_ = []
+ if (this._actionQueueResolved) {
+ this._actionQueueResolved()
+ }
+ }
+
+ /**
+ * Set timeline cues
+ */
+ setCues(cues: Cue[]) {
+ this._cues = cues
+ this.emitUIEvent('cuesChanged', cues)
+ }
+
+ /**
+ * Get timeline cues
+ */
+ getCues(): Cue[] {
+ return this._cues
+ }
+
+ /**
+ * Attach listeners to PRESTOplay events
+ */
+ protected attachPrestoListeners_(player: clpp.Player) {
+ const createTrackChangeHandler = (type?: TrackType) => {
return () => {
const trackManager = this.trackManager
if (!trackManager) {return}
@@ -395,12 +400,16 @@ export class Player {
}
}
- this.pp_.on(clpp.events.TRACKS_ADDED, handlePlayerTracksChanged())
- this.pp_.on(clpp.events.AUDIO_TRACK_CHANGED, handlePlayerTracksChanged('audio'))
- this.pp_.on(clpp.events.VIDEO_TRACK_CHANGED, handlePlayerTracksChanged('video'))
- this.pp_.on(clpp.events.TEXT_TRACK_CHANGED, handlePlayerTracksChanged('text'))
+ const onTracksAdded = createTrackChangeHandler()
+ const onAudioTrackChanged = createTrackChangeHandler('audio')
+ const onVideoTrackChanged = createTrackChangeHandler('video')
+ const onTextTrackChanged = createTrackChangeHandler('text')
+ player.on(clpp.events.TRACKS_ADDED, onTracksAdded)
+ player.on(clpp.events.AUDIO_TRACK_CHANGED, onAudioTrackChanged)
+ player.on(clpp.events.VIDEO_TRACK_CHANGED, onVideoTrackChanged)
+ player.on(clpp.events.TEXT_TRACK_CHANGED, onTextTrackChanged)
- this.pp_.on(clpp.events.STATE_CHANGED, (event: any) => {
+ const onStateChanged = (event: any) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const e = event
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
@@ -408,16 +417,21 @@ export class Player {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const previousState = toState(e.detail.previousState)
+ if (this._ignoreStateEnded && currentState === State.Ended) {
+ return
+ }
+
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,
})
- if (isEnabledState(currentState) !== isEnabledState(previousState)) {
+ if (
+ isEnabledState(currentState) !== isEnabledState(previousState)
+ || isEnabledState(currentState) !== isEnabledState(this._lastPlaybackState)
+ ) {
this.emitUIEvent('enabled', isEnabledState(currentState))
}
@@ -426,39 +440,47 @@ export class Player {
} else {
this._controls.unpin()
}
- })
- this.pp_.on('timeupdate', () => {
- const position = this.pp_?.getPosition()
+ this._lastPlaybackState = currentState
+ }
+ player.on(clpp.events.STATE_CHANGED, onStateChanged)
+
+ const onTimeupdate = () => {
+ const position = player.getPosition()
if (position != null) {
this.emitUIEvent('position', position)
}
- })
+ }
+ player.on('timeupdate', onTimeupdate)
- this.pp_.on('ratechange', () => {
- const ppRate = this.pp_?.getPlaybackRate()
+ this._rate = player.getPlaybackRate()
+ const onRateChange = () => {
+ const ppRate = player.getPlaybackRate()
if (ppRate != null && this.state !== State.Buffering) {
this._rate = ppRate
this.emitUIEvent('ratechange', this.rate)
}
- })
+ }
+ player.on('ratechange', onRateChange)
- this.pp_.on('durationchange', () => {
- const duration = this.pp_?.getDuration()
+ const onDurationChange = () => {
+ const duration = player.getDuration()
if (duration != null) {
this.emitUIEvent('durationchange', duration)
}
- })
+ }
+ player.on('durationchange', onDurationChange)
- this.pp_.on('volumechange', () => {
+ const onVolumeChange = () => {
this.emitUIEvent('volumechange', {
volume: this.volume,
muted: this.muted,
})
- })
+ }
+ player.on('volumechange', onVolumeChange)
- this.pp_.on(clpp.events.BITRATE_CHANGED, (e: any) => {
+ const onBitrateChange = (e: any) => {
const tm = this.trackManager
if (tm) {
@@ -466,27 +488,74 @@ export class Player {
} else {
this.playingVideoTrack = undefined
}
- handlePlayerTracksChanged('video')
+ }
+ player.on(clpp.events.BITRATE_CHANGED, onBitrateChange)
+
+ this._prestoDisposers.push(() => {
+ player.off(clpp.events.TRACKS_ADDED, onTracksAdded)
+ player.off(clpp.events.AUDIO_TRACK_CHANGED, onAudioTrackChanged)
+ player.off(clpp.events.VIDEO_TRACK_CHANGED, onVideoTrackChanged)
+ player.off(clpp.events.TEXT_TRACK_CHANGED, onTextTrackChanged)
+ player.off(clpp.events.STATE_CHANGED, onStateChanged)
+ player.off('timeupdate', onTimeupdate)
+ player.off('ratechange', onRateChange)
+ player.off('durationchange', onDurationChange)
+ player.off('volumechange', onVolumeChange)
+ player.off(clpp.events.BITRATE_CHANGED, onBitrateChange)
})
+ }
- this._rate = this.pp_.getPlaybackRate()
+ protected refreshPrestoState_ (player: clpp.Player) {
+ const state = toState(player.getState())
- if (this._initializer) {
- this._initializer(this.pp_)
- }
- for (let i = 0; i < this._actionQueue_.length; i++) {
- await this._actionQueue_[i]()
+ if (isEnabledState(state)) {
+ this.emitUIEvent('enabled', true)
}
- this._actionQueue_ = []
- if (this._actionQueueResolved) {
- this._actionQueueResolved()
+
+ this.emitUIEvent('statechanged', {
+ currentState: state,
+ previousState: this._lastPlaybackState,
+ timeSinceLastStateChangeMS: 10,
+ })
+
+ const position = player.getPosition() ?? 0
+ this.emitUIEvent('position', position)
+
+ const duration = player.getDuration()
+ if (duration != null) {
+ this.emitUIEvent('durationchange', duration)
}
+
+ this.emitUIEvent('volumechange', {
+ volume: this.volume,
+ muted: this.muted,
+ })
+ }
+
+ /**
+ * Remove listeners to PRESTOplay events
+ */
+ protected removePrestoListeners_ () {
+ this._prestoDisposers.forEach(dispose => dispose())
+ this._prestoDisposers = []
}
get trackManager() {
return this.pp_?.getTrackManager() ?? null
}
+ /**
+ * Configure the player to ignore the "ended" state change. This is useful
+ * for situations where RESTOplay goes to the "ended" state prematurely, which
+ * sometimes happens (e.g. state changes to ended, but the video still plays
+ * another 800 ms or so and timeupdates are triggered).
+ * Not sure if this is a bug in PRESTOplay or a problem with the asset, but
+ * it happens.
+ */
+ set ignoreStateEnded(value: boolean) {
+ this._ignoreStateEnded = value
+ }
+
/**
* Seek to the given position. Calling this function has no effect unless the
* presto instance is already initialized.
@@ -638,6 +707,7 @@ export class Player {
}
private async reset_() {
+ this.removePrestoListeners_()
if (this.pp_) {
await this.pp_.release()
}
@@ -889,9 +959,9 @@ export class Player {
const isSameTrack = (a?: Track, b?: Track): boolean => {
if (a && b) {
return a.type === b.type &&
- a.ppTrack === b.ppTrack &&
- a.selected === b.selected &&
- a.id === b.id
+ a.ppTrack === b.ppTrack &&
+ a.selected === b.selected &&
+ a.id === b.id
}
return !a && !b
diff --git a/src/components/BaseThemeOverlay.tsx b/src/components/BaseThemeOverlay.tsx
index e1265cf..0894ab7 100644
--- a/src/components/BaseThemeOverlay.tsx
+++ b/src/components/BaseThemeOverlay.tsx
@@ -1,5 +1,7 @@
import React from 'react'
+import { ControlsVisibilityMode } from '../services/controls'
+
import { BufferingIndicator } from './BufferingIndicator'
import { CurrentTime } from './CurrentTime'
import { Duration } from './Duration'
@@ -63,6 +65,38 @@ export interface BaseThemeOverlayProps extends BaseComponentProps {
* seekbar, 'none' hides the seekbar.
*/
seekBar?: 'enabled' | 'disabled' | 'none'
+ /**
+ * Visibility mode of UI controls.
+ */
+ controlsVisibility?: ControlsVisibilityMode
+ /**
+ * If true, audio controls are displayed. Defaults to true.
+ */
+ hasAudioControls?: boolean
+ /**
+ * If true, a fullscreen button is displayed. Defaults to true.
+ */
+ hasFullScreenButton?: boolean
+ /**
+ * If true, track controls are displayed. Defaults to true.
+ */
+ hasTrackControls?: boolean
+ /**
+ * If true, the top controls bar is displayed. Defaults to true.
+ */
+ hasTopControlsBar?: boolean
+ /**
+ * Render a custom bottom companion component.
+ */
+ renderBottomCompanion?: () => (JSX.Element | null)
+ /**
+ * If true, seek bar cues are shown. Default: true.
+ */
+ showSeekBarCues?: boolean
+ /**
+ * Class name for the seek bar slider component.
+ */
+ seekBarSliderClassName?: string
}
/**
@@ -70,6 +104,11 @@ export interface BaseThemeOverlayProps extends BaseComponentProps {
*/
export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => {
const selectionOptions = props.menuSelectionOptions ?? DEFAULT_SELECTION_OPTIONS
+ const hasAudioControls = props.hasAudioControls ?? true
+ const hasTrackControls = props.hasTrackControls ?? true
+ const hasFullScreenButton = props.hasFullScreenButton ?? true
+ const hasTopControlsBar = props.hasTopControlsBar ?? true
+ const showSeekBarCues = props.showSeekBarCues ?? true
const renderOptionsMenu = () => {
if (selectionOptions.length === 0) {return}
@@ -77,23 +116,19 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => {
}
const renderTopBar = () => {
- if (selectionOptions.length === 0) {return}
+ if (!hasTopControlsBar || selectionOptions.length === 0) {return}
return (
-
+
-
-
-
+ {hasTrackControls ? (
+
+
+
+ ) : null}
)
}
- // Some component rendering depends on configuration, and
- // we wrap the rendering code into helpers
- const renderFullscreenButton = () => {
- return
- }
-
const renderPosterImage = () => {
if (!props.posterUrl) {return}
return
@@ -106,7 +141,7 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => {
return (
-
+
{/* Top bar */}
@@ -119,6 +154,8 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => {
+ {props.renderBottomCompanion?.()}
+
{/* Bottom bar */}
@@ -134,6 +171,8 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => {
adjustWithKeyboard={true}
enableThumbnailSlider={false}
enabled={(props.seekBar ?? 'enabled') === 'enabled'}
+ showCues={showSeekBarCues}
+ sliderClassName={props.seekBarSliderClassName}
/>}
@@ -143,11 +182,11 @@ export const BaseThemeOverlay = (props: BaseThemeOverlayProps) => {
-
+ {hasAudioControls ? : null}
-
- {renderFullscreenButton()}
+ {hasAudioControls ? : null}
+ {hasFullScreenButton ? : null}
diff --git a/src/components/Duration.tsx b/src/components/Duration.tsx
index 8cc2aa2..c61b12f 100644
--- a/src/components/Duration.tsx
+++ b/src/components/Duration.tsx
@@ -15,7 +15,7 @@ const toString = (duration: number) => {
if (duration === Infinity) {
return 'Live'
}
- return timeToString(duration, getMinimalFormat(duration))
+ return timeToString(duration, getMinimalFormat(duration), 0.2)
}
export interface DurationProps extends BaseComponentProps {
diff --git a/src/components/MuteButton.tsx b/src/components/MuteButton.tsx
index 9d5a463..3746154 100644
--- a/src/components/MuteButton.tsx
+++ b/src/components/MuteButton.tsx
@@ -36,7 +36,7 @@ export const MuteButton = (props: MuteButtonProps) => {
})
function toggle(e: React.MouseEvent) {
- if (e.defaultPrevented) {return}
+ if (e.defaultPrevented || !presto) {return}
presto.setMuted(!presto.isMuted())
}
diff --git a/src/components/PlayerControls.tsx b/src/components/PlayerControls.tsx
index adb23d3..7479f27 100644
--- a/src/components/PlayerControls.tsx
+++ b/src/components/PlayerControls.tsx
@@ -45,7 +45,7 @@ export interface PlayerControlsProps extends BaseComponentProps {
export const PlayerControls = (props: PlayerControlsProps) => {
const [lastFocusIndex, setLastFocusIndex] = useState(-1)
const { player } = useContext(PrestoContext)
- const controlsVisible = useControlsVisible()
+ const controlsVisible = useControlsVisible() && props.mode !== 'never'
useEffect(() => {
if (props.hideDelay) {
diff --git a/src/components/PlayerSurface.tsx b/src/components/PlayerSurface.tsx
index 6ce87e3..f5de044 100644
--- a/src/components/PlayerSurface.tsx
+++ b/src/components/PlayerSurface.tsx
@@ -51,8 +51,10 @@ export interface PlayerProps extends BaseComponentProps {
}
const getContext = (nullableContext: Partial
) => {
- return Object.values(nullableContext)
- .every(value => value != null) ? nullableContext as PrestoContextType : null
+ if (!nullableContext.playerSurface || !nullableContext.player) {
+ return null
+ }
+ return nullableContext as PrestoContextType
}
/**
diff --git a/src/components/SeekBar.tsx b/src/components/SeekBar.tsx
index a72336f..872d82f 100644
--- a/src/components/SeekBar.tsx
+++ b/src/components/SeekBar.tsx
@@ -1,8 +1,10 @@
import React, { ForwardedRef, forwardRef, useContext, useState } from 'react'
import { PrestoContext } from '../context/PrestoContext'
+import { useCues } from '../hooks/hooks'
import { usePrestoEnabledState, usePrestoUiEvent } from '../react'
+import { SeekBarCues } from './SeekBarCues'
import { Slider } from './Slider'
import { Thumbnail } from './Thumbnail'
@@ -16,6 +18,14 @@ export interface SeekBarProps extends BaseComponentProps {
keyboardSeekBackward?: number
notFocusable?: boolean
enabled?: boolean
+ /**
+ * If cues should be shown on the timeline. Default: true.
+ */
+ showCues?: boolean
+ /**
+ * Class name passed to sub-component.
+ */
+ sliderClassName?: string
}
const useEnabled = (enabled: boolean) => {
@@ -32,14 +42,18 @@ const useEnabled = (enabled: boolean) => {
export const SeekBar = forwardRef((props: SeekBarProps, ref: ForwardedRef) => {
const { player } = useContext(PrestoContext)
const [progress, setProgress] = useState(0)
+ const [duration, setDuration] = useState(0)
const [hoverPosition, setHoverPosition] = useState(-1)
const [hoverValue, setHoverValue] = useState(0)
const [thumbWidth, setThumbWidth] = useState(0)
const enabled = useEnabled(props.enabled ?? true)
+ const cues = useCues()
+ const showCues = props.showCues ?? true
function updateFromPlayer(position?: number): number {
const range = player.seekRange
const rangeDuration = range.end - range.start
+ setDuration(rangeDuration)
position = position || player.position
const positionInRange = position - range.start
const progress = Math.min(100, Math.max(0, 100.0 * (positionInRange / rangeDuration)))
@@ -113,26 +127,33 @@ export const SeekBar = forwardRef((props: SeekBarProps, ref: ForwardedRef
-
+
+
+
{renderThumbnailSlider()}
+ {showCues && cues.length > 0 ? (
+
+
+
+ ): null}
)
})
diff --git a/src/components/SeekBarCues.tsx b/src/components/SeekBarCues.tsx
new file mode 100644
index 0000000..84cd3c5
--- /dev/null
+++ b/src/components/SeekBarCues.tsx
@@ -0,0 +1,30 @@
+import React from 'react'
+
+import { Cue } from '../types'
+
+type Props = {
+ cues: Cue[]
+ duration: number
+}
+
+/**
+ * Timeline cues
+ */
+export const SeekBarCues = (props: Props) => {
+ const cues = props.cues
+ return (
+
+
+ {cues.map((cue) => {
+ const left = (cue.startTime / props.duration) * 100
+ return (
+
+ )
+ })}
+
+
+ )
+}
diff --git a/src/components/Slider.tsx b/src/components/Slider.tsx
index 468387b..0ff9ec5 100644
--- a/src/components/Slider.tsx
+++ b/src/components/Slider.tsx
@@ -79,8 +79,6 @@ const getPositionFromMouseEvent = (e: PressEvent, container: HTMLDivElement | nu
const width = rect.width
let x: number
if (e.type === 'touchmove' || e.type === 'touchstart' || e.type === 'touchend') {
- // Eslint false alarm
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const touchEvent = e as React.TouchEvent
x = touchEvent.changedTouches[touchEvent.changedTouches.length - 1].pageX - rect.left
} else {
@@ -310,11 +308,13 @@ export const Slider = forwardRef((props: SliderProps, ref) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
+ const className = `pp-ui-slider ${interacting ? 'pp-ui-slider-interacting' : ''} `
+ + `${props.disabled ? 'pp-ui-disabled' : 'pp-ui-enabled'} ${props.className || ''}`
+
return (
{
>
+ className="pp-ui-slider-range">
Promise
+ onClick?: () => any | Promise
}
const isVisibleState = (state: State) => {
diff --git a/src/components/Thumbnail.tsx b/src/components/Thumbnail.tsx
index 503804a..23a234d 100644
--- a/src/components/Thumbnail.tsx
+++ b/src/components/Thumbnail.tsx
@@ -28,6 +28,7 @@ export interface ThumbnailProps extends BaseComponentProps {
*/
const usePlugin = () => {
const { presto } = useContext(PrestoContext)
+ if (!presto) {return null}
return presto.getPlugin(clpp.thumbnails.ThumbnailsPlugin.Id) as clpp.thumbnails.ThumbnailsPlugin | null
}
diff --git a/src/components/VuMeter.tsx b/src/components/VuMeter.tsx
index ebeb777..883ee1c 100644
--- a/src/components/VuMeter.tsx
+++ b/src/components/VuMeter.tsx
@@ -11,23 +11,49 @@ export type Props = BaseComponentProps & {
height: number
}
+
+const mount = (service: VolumeMeterService, canvas: HTMLCanvasElement, config: VuMeterConfig) => {
+ service.configure(canvas, config)
+ service.mount()
+}
+
/**
* Volume Unit Meter
*/
export const VuMeter = (props: Props) => {
- const ctx = useContext(PrestoContext)
- const serviceRef = useRef(new VolumeMeterService(ctx.presto))
+ const { presto } = useContext(PrestoContext)
+ const canvasRef = useRef(null)
+ const serviceRef = useRef(null)
+ const config = props.config ?? {}
+
+ useEffect(() => {
+ if (presto) {
+ serviceRef.current = new VolumeMeterService(presto)
+ if (canvasRef.current) {
+ mount(serviceRef.current, canvasRef.current, config)
+ }
+ }
+ }, [presto])
+
const onRef = useCallback((canvas: HTMLCanvasElement) => {
- serviceRef.current.configure(canvas, props.config ?? {})
- serviceRef.current.mount()
+ canvasRef.current = canvas
+ if (serviceRef.current) {
+ mount(serviceRef.current, canvas, config)
+ }
}, [])
useEffect(() => {
return () => {
- serviceRef.current.unmount()
+ if (serviceRef.current) {
+ serviceRef.current.unmount()
+ serviceRef.current = null
+ canvasRef.current = null
+ }
}
}, [])
- return