diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 3444c2a3d0..1f0b0f25f4 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -23,6 +23,7 @@ import AudioPlayer from "../audio_messages/AudioPlayer"; import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import MFileBody from "./MFileBody"; import { IBodyProps } from "./IBodyProps"; +import { PlaybackManager } from "../../../voice/PlaybackManager"; interface IState { error?: Error; @@ -62,7 +63,7 @@ export default class MAudioBody extends React.PureComponent const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024); // We should have a buffer to work with now: let's set it up - const playback = new Playback(buffer, waveform); + const playback = PlaybackManager.instance.createPlaybackInstance(buffer, waveform); playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent); this.setState({ playback }); diff --git a/src/voice/ManagedPlayback.ts b/src/voice/ManagedPlayback.ts new file mode 100644 index 0000000000..bff6ce7088 --- /dev/null +++ b/src/voice/ManagedPlayback.ts @@ -0,0 +1,37 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { DEFAULT_WAVEFORM, Playback } from "./Playback"; +import { PlaybackManager } from "./PlaybackManager"; + +/** + * A managed playback is a Playback instance that is guided by a PlaybackManager. + */ +export class ManagedPlayback extends Playback { + public constructor(private manager: PlaybackManager, buf: ArrayBuffer, seedWaveform = DEFAULT_WAVEFORM) { + super(buf, seedWaveform); + } + + public async play(): Promise { + this.manager.playOnly(this); + return super.play(); + } + + public destroy() { + this.manager.destroyPlaybackInstance(this); + super.destroy(); + } +} diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index 1a1ee54466..df0bf593fa 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -32,7 +32,7 @@ export enum PlaybackState { export const PLAYBACK_WAVEFORM_SAMPLES = 39; const THUMBNAIL_WAVEFORM_SAMPLES = 100; // arbitrary: [30,120] -const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); +export const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); function makePlaybackWaveform(input: number[]): number[] { // First, convert negative amplitudes to positive so we don't detect zero as "noisy". diff --git a/src/voice/PlaybackClock.ts b/src/voice/PlaybackClock.ts index e3f41930de..4f3d4e6dbb 100644 --- a/src/voice/PlaybackClock.ts +++ b/src/voice/PlaybackClock.ts @@ -132,6 +132,10 @@ export class PlaybackClock implements IDestroyable { public flagStop() { this.stopped = true; + + // Reset the clock time now so that the update going out will trigger components + // to check their seek/position information (alongside the clock). + this.clipStart = this.context.currentTime; } public syncTo(contextTime: number, clipTime: number) { diff --git a/src/voice/PlaybackManager.ts b/src/voice/PlaybackManager.ts new file mode 100644 index 0000000000..58fa61df56 --- /dev/null +++ b/src/voice/PlaybackManager.ts @@ -0,0 +1,54 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { DEFAULT_WAVEFORM, Playback } from "./Playback"; +import { ManagedPlayback } from "./ManagedPlayback"; + +/** + * Handles management of playback instances to ensure certain functionality, like + * one playback operating at any one time. + */ +export class PlaybackManager { + private static internalInstance: PlaybackManager; + + private instances: ManagedPlayback[] = []; + + public static get instance(): PlaybackManager { + if (!PlaybackManager.internalInstance) { + PlaybackManager.internalInstance = new PlaybackManager(); + } + return PlaybackManager.internalInstance; + } + + /** + * Stops all other playback instances. If no playback is provided, all instances + * are stopped. + * @param playback Optional. The playback to leave untouched. + */ + public playOnly(playback?: Playback) { + this.instances.filter(p => p !== playback).forEach(p => p.stop()); + } + + public destroyPlaybackInstance(playback: ManagedPlayback) { + this.instances = this.instances.filter(p => p !== playback); + } + + public createPlaybackInstance(buf: ArrayBuffer, waveform = DEFAULT_WAVEFORM): Playback { + const instance = new ManagedPlayback(this, buf, waveform); + this.instances.push(instance); + return instance; + } +}