Render error state for audio components
Fixes https://github.com/vector-im/element-web/issues/17148 Both `AudioPlayer` and `RecordingPlayback` got a fragment added to their DOM structure, to incorporate the rarely seen error message below it. Additionally, a missing try/catch was added to the `Playback` class to handle uncaught decoding issues. The similarity of the components is tracked in https://github.com/vector-im/element-web/issues/18161
This commit is contained in:
parent
b590b1d263
commit
3d72b9e227
4 changed files with 63 additions and 44 deletions
|
@ -36,6 +36,7 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
playbackPhase: PlaybackState;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.audio_messages.AudioPlayer")
|
||||
|
@ -55,8 +56,10 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
|
|||
|
||||
// Don't wait for the promise to complete - it will emit a progress update when it
|
||||
// is done, and it's not meant to take long anyhow.
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.props.playback.prepare();
|
||||
this.props.playback.prepare().catch(e => {
|
||||
console.error("Error processing audio file:", e);
|
||||
this.setState({ error: true });
|
||||
});
|
||||
}
|
||||
|
||||
private onPlaybackUpdate = (ev: PlaybackState) => {
|
||||
|
@ -91,7 +94,8 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
|
|||
public render(): ReactNode {
|
||||
// tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
|
||||
// events for accessibility
|
||||
return <div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}>
|
||||
return <>
|
||||
<div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}>
|
||||
<div className='mx_AudioPlayer_primaryContainer'>
|
||||
<PlayPauseButton
|
||||
playback={this.props.playback}
|
||||
|
@ -119,6 +123,8 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
|
|||
/>
|
||||
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />
|
||||
</div>
|
||||
</div>;
|
||||
</div>
|
||||
{ this.state.error && <div className="text-warning">{ _t("Error downloading audio") }</div> }
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import PlaybackClock from "./PlaybackClock";
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { TileShape } from "../rooms/EventTile";
|
||||
import PlaybackWaveform from "./PlaybackWaveform";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
interface IProps {
|
||||
// Playback instance to render. Cannot change during component lifecycle: create
|
||||
|
@ -33,6 +34,7 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
playbackPhase: PlaybackState;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.audio_messages.RecordingPlayback")
|
||||
|
@ -49,8 +51,10 @@ export default class RecordingPlayback extends React.PureComponent<IProps, IStat
|
|||
|
||||
// Don't wait for the promise to complete - it will emit a progress update when it
|
||||
// is done, and it's not meant to take long anyhow.
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.props.playback.prepare();
|
||||
this.props.playback.prepare().catch(e => {
|
||||
console.error("Error processing audio file:", e);
|
||||
this.setState({ error: true });
|
||||
});
|
||||
}
|
||||
|
||||
private get isWaveformable(): boolean {
|
||||
|
@ -65,10 +69,13 @@ export default class RecordingPlayback extends React.PureComponent<IProps, IStat
|
|||
|
||||
public render(): ReactNode {
|
||||
const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : '';
|
||||
return <div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
|
||||
return <>
|
||||
<div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
|
||||
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
|
||||
<PlaybackClock playback={this.props.playback} />
|
||||
{ this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> }
|
||||
</div>;
|
||||
</div>
|
||||
{ this.state.error && <div className="text-warning">{ _t("Error downloading audio") }</div> }
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2601,6 +2601,7 @@
|
|||
"Use email to optionally be discoverable by existing contacts.": "Use email to optionally be discoverable by existing contacts.",
|
||||
"Sign in with SSO": "Sign in with SSO",
|
||||
"Unnamed audio": "Unnamed audio",
|
||||
"Error downloading audio": "Error downloading audio",
|
||||
"Pause": "Pause",
|
||||
"Play": "Play",
|
||||
"Couldn't load page": "Couldn't load page",
|
||||
|
|
|
@ -135,6 +135,7 @@ export class Playback extends EventEmitter implements IDestroyable {
|
|||
// Safari compat: promise API not supported on this function
|
||||
this.audioBuf = await new Promise((resolve, reject) => {
|
||||
this.context.decodeAudioData(this.buf, b => resolve(b), async e => {
|
||||
try {
|
||||
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
|
||||
// very well.
|
||||
console.error("Error decoding recording: ", e);
|
||||
|
@ -147,6 +148,10 @@ export class Playback extends EventEmitter implements IDestroyable {
|
|||
console.error("Still failed to decode recording: ", e);
|
||||
reject(e);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Caught decoding error:", e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue