diff --git a/cypress/e2e/audio-player/audio-player.spec.ts b/cypress/e2e/audio-player/audio-player.spec.ts index 9443e063115..41f7f1a8568 100644 --- a/cypress/e2e/audio-player/audio-player.spec.ts +++ b/cypress/e2e/audio-player/audio-player.spec.ts @@ -204,8 +204,9 @@ describe("Audio player", () => { // Assert that the counter is zero before clicking the play button cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist"); - // Find and click "Play" button - cy.findByRole("button", { name: "Play" }).click(); + // Find and click "Play" button, the wait is to make the test less flaky + cy.findByRole("button", { name: "Play" }).should("exist"); + cy.wait(500).findByRole("button", { name: "Play" }).click(); // Assert that "Pause" button can be found cy.findByRole("button", { name: "Pause" }).should("exist"); @@ -339,8 +340,9 @@ describe("Audio player", () => { // Assert that the counter is zero before clicking the play button cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist"); - // Find and click "Play" button - cy.findByRole("button", { name: "Play" }).click(); + // Find and click "Play" button, the wait is to make the test less flaky + cy.findByRole("button", { name: "Play" }).should("exist"); + cy.wait(500).findByRole("button", { name: "Play" }).click(); // Assert that "Pause" button can be found cy.findByRole("button", { name: "Pause" }).should("exist"); @@ -349,7 +351,7 @@ describe("Audio player", () => { cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist"); // Assert that "Play" button can be found - cy.findByRole("button", { name: "Play" }).should("exist"); + cy.findByRole("button", { name: "Play" }).should("exist").should("not.have.attr", "disabled"); }); }) .realHover() diff --git a/src/BlurhashEncoder.ts b/src/BlurhashEncoder.ts index 01f84421b6a..89ed2b56e54 100644 --- a/src/BlurhashEncoder.ts +++ b/src/BlurhashEncoder.ts @@ -14,15 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { defer, IDeferred } from "matrix-js-sdk/src/utils"; - // @ts-ignore - `.ts` is needed here to make TS happy -import BlurhashWorker from "./workers/blurhash.worker.ts"; - -interface IBlurhashWorkerResponse { - seq: number; - blurhash: string; -} +import BlurhashWorker, { Request, Response } from "./workers/blurhash.worker.ts"; +import { WorkerManager } from "./WorkerManager"; export class BlurhashEncoder { private static internalInstance = new BlurhashEncoder(); @@ -31,29 +25,9 @@ export class BlurhashEncoder { return BlurhashEncoder.internalInstance; } - private readonly worker: Worker; - private seq = 0; - private pendingDeferredMap = new Map>(); - - public constructor() { - this.worker = new BlurhashWorker(); - this.worker.onmessage = this.onMessage; - } - - private onMessage = (ev: MessageEvent): void => { - const { seq, blurhash } = ev.data; - const deferred = this.pendingDeferredMap.get(seq); - if (deferred) { - this.pendingDeferredMap.delete(seq); - deferred.resolve(blurhash); - } - }; + private readonly worker = new WorkerManager(BlurhashWorker); public getBlurhash(imageData: ImageData): Promise { - const seq = this.seq++; - const deferred = defer(); - this.pendingDeferredMap.set(seq, deferred); - this.worker.postMessage({ seq, imageData }); - return deferred.promise; + return this.worker.call({ imageData }).then((resp) => resp.blurhash); } } diff --git a/src/WorkerManager.ts b/src/WorkerManager.ts new file mode 100644 index 00000000000..5dcb56c6109 --- /dev/null +++ b/src/WorkerManager.ts @@ -0,0 +1,46 @@ +/* +Copyright 2022 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 { defer, IDeferred } from "matrix-js-sdk/src/utils"; + +import { WorkerPayload } from "./workers/worker"; + +export class WorkerManager { + private readonly worker: Worker; + private seq = 0; + private pendingDeferredMap = new Map>(); + + public constructor(WorkerConstructor: { new (): Worker }) { + this.worker = new WorkerConstructor(); + this.worker.onmessage = this.onMessage; + } + + private onMessage = (ev: MessageEvent): void => { + const deferred = this.pendingDeferredMap.get(ev.data.seq); + if (deferred) { + this.pendingDeferredMap.delete(ev.data.seq); + deferred.resolve(ev.data); + } + }; + + public call(request: Request): Promise { + const seq = this.seq++; + const deferred = defer(); + this.pendingDeferredMap.set(seq, deferred); + this.worker.postMessage({ seq, ...request }); + return deferred.promise; + } +} diff --git a/src/audio/ManagedPlayback.ts b/src/audio/ManagedPlayback.ts index c33d032b688..6ecedd6766a 100644 --- a/src/audio/ManagedPlayback.ts +++ b/src/audio/ManagedPlayback.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DEFAULT_WAVEFORM, Playback } from "./Playback"; +import { Playback } from "./Playback"; import { PlaybackManager } from "./PlaybackManager"; +import { DEFAULT_WAVEFORM } from "./consts"; /** * A managed playback is a Playback instance that is guided by a PlaybackManager. diff --git a/src/audio/Playback.ts b/src/audio/Playback.ts index e1ab1a1c593..43cbf904c0e 100644 --- a/src/audio/Playback.ts +++ b/src/audio/Playback.ts @@ -17,13 +17,18 @@ limitations under the License. import EventEmitter from "events"; import { SimpleObservable } from "matrix-widget-api"; import { logger } from "matrix-js-sdk/src/logger"; +import { defer } from "matrix-js-sdk/src/utils"; +// @ts-ignore - `.ts` is needed here to make TS happy +import PlaybackWorker, { Request, Response } from "../workers/playback.worker.ts"; import { UPDATE_EVENT } from "../stores/AsyncStore"; -import { arrayFastResample, arrayRescale, arraySeed, arraySmoothingResample } from "../utils/arrays"; +import { arrayFastResample } from "../utils/arrays"; import { IDestroyable } from "../utils/IDestroyable"; import { PlaybackClock } from "./PlaybackClock"; import { createAudioContext, decodeOgg } from "./compat"; import { clamp } from "../utils/numbers"; +import { WorkerManager } from "../WorkerManager"; +import { DEFAULT_WAVEFORM, PLAYBACK_WAVEFORM_SAMPLES } from "./consts"; export enum PlaybackState { Decoding = "decoding", @@ -32,25 +37,7 @@ export enum PlaybackState { Playing = "playing", // active progress through timeline } -export interface PlaybackInterface { - readonly liveData: SimpleObservable; - readonly timeSeconds: number; - readonly durationSeconds: number; - skipTo(timeSeconds: number): Promise; -} - -export const PLAYBACK_WAVEFORM_SAMPLES = 39; const THUMBNAIL_WAVEFORM_SAMPLES = 100; // arbitrary: [30,120] -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". - const noiseWaveform = input.map((v) => Math.abs(v)); - - // Then, we'll resample the waveform using a smoothing approach so we can keep the same rough shape. - // We also rescale the waveform to be 0-1 so we end up with a clamped waveform to rely upon. - return arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1); -} export interface PlaybackInterface { readonly currentState: PlaybackState; @@ -68,14 +55,15 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte public readonly thumbnailWaveform: number[]; private readonly context: AudioContext; - private source: AudioBufferSourceNode | MediaElementAudioSourceNode; + private source?: AudioBufferSourceNode | MediaElementAudioSourceNode; private state = PlaybackState.Decoding; - private audioBuf: AudioBuffer; - private element: HTMLAudioElement; + private audioBuf?: AudioBuffer; + private element?: HTMLAudioElement; private resampledWaveform: number[]; private waveformObservable = new SimpleObservable(); private readonly clock: PlaybackClock; private readonly fileSize: number; + private readonly worker = new WorkerManager(PlaybackWorker); /** * Creates a new playback instance from a buffer. @@ -178,12 +166,11 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte // 5mb logger.log("Audio file too large: processing through