From a759d61ba0a8c5f3479f2ec8a4d89050d660354a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 20 Jul 2021 23:28:07 -0600 Subject: [PATCH 1/2] Play only one audio file at a time Fixes https://github.com/vector-im/element-web/issues/17439 --- src/components/views/messages/MAudioBody.tsx | 3 +- src/voice/ManagedPlayback.ts | 37 ++++++++++++++ src/voice/Playback.ts | 2 +- src/voice/PlaybackClock.ts | 4 ++ src/voice/PlaybackManager.ts | 54 ++++++++++++++++++++ 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/voice/ManagedPlayback.ts create mode 100644 src/voice/PlaybackManager.ts diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 3444c2a3d0..f4783796cc 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.createInstance(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..6281a8a59e --- /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.destroyInstance(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..573b78f234 --- /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 destroyInstance(playback: ManagedPlayback) { + this.instances = this.instances.filter(p => p !== playback); + } + + public createInstance(buf: ArrayBuffer, waveform = DEFAULT_WAVEFORM): Playback { + const instance = new ManagedPlayback(this, buf, waveform); + this.instances.push(instance); + return instance; + } +} From 45f9f3e13ec763bdd89746666f6ddc206e6b7fd1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 20 Jul 2021 23:34:27 -0600 Subject: [PATCH 2/2] Name functions better --- src/components/views/messages/MAudioBody.tsx | 2 +- src/voice/ManagedPlayback.ts | 2 +- src/voice/PlaybackManager.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index f4783796cc..1f0b0f25f4 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -63,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 = PlaybackManager.instance.createInstance(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 index 6281a8a59e..bff6ce7088 100644 --- a/src/voice/ManagedPlayback.ts +++ b/src/voice/ManagedPlayback.ts @@ -31,7 +31,7 @@ export class ManagedPlayback extends Playback { } public destroy() { - this.manager.destroyInstance(this); + this.manager.destroyPlaybackInstance(this); super.destroy(); } } diff --git a/src/voice/PlaybackManager.ts b/src/voice/PlaybackManager.ts index 573b78f234..58fa61df56 100644 --- a/src/voice/PlaybackManager.ts +++ b/src/voice/PlaybackManager.ts @@ -42,11 +42,11 @@ export class PlaybackManager { this.instances.filter(p => p !== playback).forEach(p => p.stop()); } - public destroyInstance(playback: ManagedPlayback) { + public destroyPlaybackInstance(playback: ManagedPlayback) { this.instances = this.instances.filter(p => p !== playback); } - public createInstance(buf: ArrayBuffer, waveform = DEFAULT_WAVEFORM): Playback { + public createPlaybackInstance(buf: ArrayBuffer, waveform = DEFAULT_WAVEFORM): Playback { const instance = new ManagedPlayback(this, buf, waveform); this.instances.push(instance); return instance;