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/18161pull/21833/head
							parent
							
								
									b590b1d263
								
							
						
					
					
						commit
						3d72b9e227
					
				|  | @ -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,34 +94,37 @@ 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}> | ||||
|             <div className='mx_AudioPlayer_primaryContainer'> | ||||
|                 <PlayPauseButton | ||||
|                     playback={this.props.playback} | ||||
|                     playbackPhase={this.state.playbackPhase} | ||||
|                     tabIndex={-1} // prevent tabbing into the button
 | ||||
|                     ref={this.playPauseRef} | ||||
|                 /> | ||||
|                 <div className='mx_AudioPlayer_mediaInfo'> | ||||
|                     <span className='mx_AudioPlayer_mediaName'> | ||||
|                         { this.props.mediaName || _t("Unnamed audio") } | ||||
|                     </span> | ||||
|                     <div className='mx_AudioPlayer_byline'> | ||||
|                         <DurationClock playback={this.props.playback} /> | ||||
|                           { /* easiest way to introduce a gap between the components */ } | ||||
|                         { this.renderFileSize() } | ||||
|         return <> | ||||
|             <div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}> | ||||
|                 <div className='mx_AudioPlayer_primaryContainer'> | ||||
|                     <PlayPauseButton | ||||
|                         playback={this.props.playback} | ||||
|                         playbackPhase={this.state.playbackPhase} | ||||
|                         tabIndex={-1} // prevent tabbing into the button
 | ||||
|                         ref={this.playPauseRef} | ||||
|                     /> | ||||
|                     <div className='mx_AudioPlayer_mediaInfo'> | ||||
|                         <span className='mx_AudioPlayer_mediaName'> | ||||
|                             { this.props.mediaName || _t("Unnamed audio") } | ||||
|                         </span> | ||||
|                         <div className='mx_AudioPlayer_byline'> | ||||
|                             <DurationClock playback={this.props.playback} /> | ||||
|                               { /* easiest way to introduce a gap between the components */ } | ||||
|                             { this.renderFileSize() } | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div className='mx_AudioPlayer_seek'> | ||||
|                     <SeekBar | ||||
|                         playback={this.props.playback} | ||||
|                         tabIndex={-1} // prevent tabbing into the bar
 | ||||
|                         playbackPhase={this.state.playbackPhase} | ||||
|                         ref={this.seekRef} | ||||
|                     /> | ||||
|                     <PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} /> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div className='mx_AudioPlayer_seek'> | ||||
|                 <SeekBar | ||||
|                     playback={this.props.playback} | ||||
|                     tabIndex={-1} // prevent tabbing into the bar
 | ||||
|                     playbackPhase={this.state.playbackPhase} | ||||
|                     ref={this.seekRef} | ||||
|                 /> | ||||
|                 <PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} /> | ||||
|             </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}> | ||||
|             <PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} /> | ||||
|             <PlaybackClock playback={this.props.playback} /> | ||||
|             { this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> } | ||||
|         </div>; | ||||
|         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> | ||||
|             { 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,18 +135,23 @@ 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 => { | ||||
|                 // This error handler is largely for Safari as well, which doesn't support Opus/Ogg
 | ||||
|                 // very well.
 | ||||
|                 console.error("Error decoding recording: ", e); | ||||
|                 console.warn("Trying to re-encode to WAV instead..."); | ||||
|                 try { | ||||
|                     // This error handler is largely for Safari as well, which doesn't support Opus/Ogg
 | ||||
|                     // very well.
 | ||||
|                     console.error("Error decoding recording: ", e); | ||||
|                     console.warn("Trying to re-encode to WAV instead..."); | ||||
| 
 | ||||
|                 const wav = await decodeOgg(this.buf); | ||||
|                     const wav = await decodeOgg(this.buf); | ||||
| 
 | ||||
|                 // noinspection ES6MissingAwait - not needed when using callbacks
 | ||||
|                 this.context.decodeAudioData(wav, b => resolve(b), e => { | ||||
|                     console.error("Still failed to decode recording: ", e); | ||||
|                     // noinspection ES6MissingAwait - not needed when using callbacks
 | ||||
|                     this.context.decodeAudioData(wav, b => resolve(b), e => { | ||||
|                         console.error("Still failed to decode recording: ", e); | ||||
|                         reject(e); | ||||
|                     }); | ||||
|                 } catch (e) { | ||||
|                     console.error("Caught decoding error:", e); | ||||
|                     reject(e); | ||||
|                 }); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston