Prevent voice message from displaying spurious errors (#30736)

* fix: avoid to render `AudioPlayerViewModel` when `MAudioBody` is inherited

* fix: avoid `Playback.prepare` to fail when called twice

* fix: add `decoding` to playback type

* refactor: fix circular deps

* refactor: extract `MockedPlayback` from `AudioPlayerViewModel`

* test: add `MAudioBody` basic test

* test: add tests for `MVoiceMessageBody`

* fix: lint
This commit is contained in:
Florian Duros
2025-09-12 10:24:51 +02:00
committed by GitHub
parent 7fc0cb242c
commit b6710d19c0
9 changed files with 219 additions and 54 deletions

View File

@@ -74,7 +74,11 @@ export default class RecordingPlayback extends AudioPlayerBase<IProps> {
}
return (
<div className="mx_MediaBody mx_VoiceMessagePrimaryContainer" onKeyDown={this.onKeyDown}>
<div
className="mx_MediaBody mx_VoiceMessagePrimaryContainer"
onKeyDown={this.onKeyDown}
data-testid="recording-playback"
>
<PlayPauseButton
playback={this.props.playback}
playbackPhase={this.state.playbackPhase}

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import React, { type JSX, useEffect, useMemo } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { type IContent } from "matrix-js-sdk/src/matrix";
import { type MediaEventContent } from "matrix-js-sdk/src/types";
@@ -17,8 +17,6 @@ import { _t } from "../../../languageHandler";
import MFileBody from "./MFileBody";
import { type IBodyProps } from "./IBodyProps";
import { PlaybackManager } from "../../../audio/PlaybackManager";
import { isVoiceMessage } from "../../../utils/EventUtils";
import { PlaybackQueue } from "../../../audio/PlaybackQueue";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import MediaProcessingError from "./shared/MediaProcessingError";
import { AudioPlayerViewModel } from "../../../viewmodels/audio/AudioPlayerViewModel";
@@ -27,7 +25,6 @@ import { AudioPlayerView } from "../../../shared-components/audio/AudioPlayerVie
interface IState {
error?: boolean;
playback?: Playback;
audioPlayerVm?: AudioPlayerViewModel;
}
export default class MAudioBody extends React.PureComponent<IBodyProps, IState> {
@@ -38,7 +35,6 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
public async componentDidMount(): Promise<void> {
let buffer: ArrayBuffer;
try {
try {
const blob = await this.props.mediaEventHelper!.sourceBlob.value;
@@ -63,18 +59,16 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
// We should have a buffer to work with now: let's set it up
const playback = PlaybackManager.instance.createPlaybackInstance(buffer, waveform);
playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent);
this.setState({ playback, audioPlayerVm: new AudioPlayerViewModel({ playback, mediaName: content.body }) });
this.setState({ playback });
if (isVoiceMessage(this.props.mxEvent)) {
PlaybackQueue.forRoom(this.props.mxEvent.getRoomId()!).unsortedEnqueue(this.props.mxEvent, playback);
}
// Note: the components later on will handle preparing the Playback class for us.
this.onMount(playback);
// Note: the components later on will handle preparing the Playback class for us
}
protected onMount(playback: Playback): void {}
public componentWillUnmount(): void {
this.state.playback?.destroy();
this.state.audioPlayerVm?.dispose();
}
protected get showFileBody(): boolean {
@@ -116,9 +110,35 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
// At this point we should have a playable state
return (
<span className="mx_MAudioBody">
{this.state.audioPlayerVm && <AudioPlayerView vm={this.state.audioPlayerVm} />}
<AudioPlayer playback={this.state.playback} mediaName={this.props.mxEvent.getContent().body} />
{this.showFileBody && <MFileBody {...this.props} showGenericPlaceholder={false} />}
</span>
);
}
}
interface AudioPlayerProps {
/**
* The playback instance to control audio playback.
*/
playback: Playback;
/**
* The name of the media being played
*/
mediaName: string;
}
/**
* AudioPlayer component that initializes the AudioPlayerViewModel and renders the AudioPlayerView.
*/
function AudioPlayer({ playback, mediaName }: AudioPlayerProps): JSX.Element {
const vm = useMemo(() => new AudioPlayerViewModel({ playback, mediaName }), [playback, mediaName]);
useEffect(() => {
return () => {
vm.dispose();
};
}, [vm]);
return <AudioPlayerView vm={vm} />;
}

View File

@@ -14,8 +14,17 @@ import RecordingPlayback from "../audio_messages/RecordingPlayback";
import MAudioBody from "./MAudioBody";
import MFileBody from "./MFileBody";
import MediaProcessingError from "./shared/MediaProcessingError";
import { isVoiceMessage } from "../../../utils/EventUtils";
import { PlaybackQueue } from "../../../audio/PlaybackQueue";
import { type Playback } from "../../../audio/Playback";
export default class MVoiceMessageBody extends MAudioBody {
protected onMount(playback: Playback): void {
if (isVoiceMessage(this.props.mxEvent)) {
PlaybackQueue.forRoom(this.props.mxEvent.getRoomId()!).unsortedEnqueue(this.props.mxEvent, playback);
}
}
// A voice message is an audio file but rendered in a special way.
public render(): React.ReactNode {
if (this.state.error) {