Extract PlaybackInterface (#9526)

This commit is contained in:
Michael Weimann 2022-11-02 09:46:42 +01:00 committed by GitHub
parent 1e65dcd0aa
commit 9096bd82d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 182 additions and 13 deletions

View file

@ -32,6 +32,13 @@ export enum PlaybackState {
Playing = "playing", // active progress through timeline
}
export interface PlaybackInterface {
readonly liveData: SimpleObservable<number[]>;
readonly timeSeconds: number;
readonly durationSeconds: number;
skipTo(timeSeconds: number): Promise<void>;
}
export const PLAYBACK_WAVEFORM_SAMPLES = 39;
const THUMBNAIL_WAVEFORM_SAMPLES = 100; // arbitrary: [30,120]
export const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
@ -45,7 +52,7 @@ function makePlaybackWaveform(input: number[]): number[] {
return arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
}
export class Playback extends EventEmitter implements IDestroyable {
export class Playback extends EventEmitter implements IDestroyable, PlaybackInterface {
/**
* Stable waveform for representing a thumbnail of the media. Values are
* guaranteed to be between zero and one, inclusive.
@ -111,6 +118,18 @@ export class Playback extends EventEmitter implements IDestroyable {
return this.currentState === PlaybackState.Playing;
}
public get liveData(): SimpleObservable<number[]> {
return this.clock.liveData;
}
public get timeSeconds(): number {
return this.clock.timeSeconds;
}
public get durationSeconds(): number {
return this.clock.durationSeconds;
}
public emit(event: PlaybackState, ...args: any[]): boolean {
this.state = event;
super.emit(event, ...args);

View file

@ -23,6 +23,7 @@ import { _t } from "../../../languageHandler";
import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock";
import AudioPlayerBase from "./AudioPlayerBase";
import { PlaybackState } from "../../../audio/Playback";
export default class AudioPlayer extends AudioPlayerBase {
protected renderFileSize(): string {
@ -61,7 +62,7 @@ export default class AudioPlayer extends AudioPlayerBase {
<SeekBar
playback={this.props.playback}
tabIndex={-1} // prevent tabbing into the bar
playbackPhase={this.state.playbackPhase}
disabled={this.state.playbackPhase === PlaybackState.Decoding}
ref={this.seekRef}
/>
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />

View file

@ -21,6 +21,7 @@ import PlaybackClock from "./PlaybackClock";
import AudioPlayerBase, { IProps as IAudioPlayerBaseProps } from "./AudioPlayerBase";
import SeekBar from "./SeekBar";
import PlaybackWaveform from "./PlaybackWaveform";
import { PlaybackState } from "../../../audio/Playback";
export enum PlaybackLayout {
/**
@ -56,7 +57,7 @@ export default class RecordingPlayback extends AudioPlayerBase<IProps> {
<SeekBar
playback={this.props.playback}
tabIndex={0} // allow keyboard users to fall into the seek bar
playbackPhase={this.state.playbackPhase}
disabled={this.state.playbackPhase === PlaybackState.Decoding}
ref={this.seekRef}
/>
</div>

View file

@ -16,20 +16,20 @@ limitations under the License.
import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
import { Playback, PlaybackState } from "../../../audio/Playback";
import { PlaybackInterface } from "../../../audio/Playback";
import { MarkedExecution } from "../../../utils/MarkedExecution";
import { percentageOf } from "../../../utils/numbers";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
playback: PlaybackInterface;
// Tab index for the underlying component. Useful if the seek bar is in a managed state.
// Defaults to zero.
tabIndex?: number;
playbackPhase: PlaybackState;
disabled?: boolean;
}
interface IState {
@ -52,6 +52,7 @@ export default class SeekBar extends React.PureComponent<IProps, IState> {
public static defaultProps = {
tabIndex: 0,
disabled: false,
};
constructor(props: IProps) {
@ -62,26 +63,26 @@ export default class SeekBar extends React.PureComponent<IProps, IState> {
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.clockInfo.liveData.onUpdate(() => this.animationFrameFn.mark());
this.props.playback.liveData.onUpdate(() => this.animationFrameFn.mark());
}
private doUpdate() {
this.setState({
percentage: percentageOf(
this.props.playback.clockInfo.timeSeconds,
this.props.playback.timeSeconds,
0,
this.props.playback.clockInfo.durationSeconds),
this.props.playback.durationSeconds),
});
}
public left() {
// noinspection JSIgnoredPromiseFromCall
this.props.playback.skipTo(this.props.playback.clockInfo.timeSeconds - ARROW_SKIP_SECONDS);
this.props.playback.skipTo(this.props.playback.timeSeconds - ARROW_SKIP_SECONDS);
}
public right() {
// noinspection JSIgnoredPromiseFromCall
this.props.playback.skipTo(this.props.playback.clockInfo.timeSeconds + ARROW_SKIP_SECONDS);
this.props.playback.skipTo(this.props.playback.timeSeconds + ARROW_SKIP_SECONDS);
}
private onChange = (ev: ChangeEvent<HTMLInputElement>) => {
@ -89,7 +90,7 @@ export default class SeekBar extends React.PureComponent<IProps, IState> {
// change the value on the component. We can use this as a reliable "skip to X" function.
//
// noinspection JSIgnoredPromiseFromCall
this.props.playback.skipTo(Number(ev.target.value) * this.props.playback.clockInfo.durationSeconds);
this.props.playback.skipTo(Number(ev.target.value) * this.props.playback.durationSeconds);
};
public render(): ReactNode {
@ -105,7 +106,7 @@ export default class SeekBar extends React.PureComponent<IProps, IState> {
value={this.state.percentage}
step={0.001}
style={{ '--fillTo': this.state.percentage } as ISeekCSS}
disabled={this.props.playbackPhase === PlaybackState.Decoding}
disabled={this.props.disabled}
/>;
}
}