Handle broadcast chunk errors (#9970)

* Use strings for broadcast playback states

* Handle broadcast decode errors
This commit is contained in:
Michael Weimann 2023-01-24 11:20:26 +01:00 committed by GitHub
parent 60edb85a1a
commit 533b250bb6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 483 additions and 275 deletions

View file

@ -659,6 +659,7 @@
"%(senderName)s ended a <a>voice broadcast</a>": "%(senderName)s ended a <a>voice broadcast</a>",
"You ended a voice broadcast": "You ended a voice broadcast",
"%(senderName)s ended a voice broadcast": "%(senderName)s ended a voice broadcast",
"Unable to play this voice broadcast": "Unable to play this voice broadcast",
"Stop live broadcasting?": "Stop live broadcasting?",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.",
"Yes, stop broadcast": "Yes, stop broadcast",

View file

@ -0,0 +1,32 @@
/*
Copyright 2023 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 React from "react";
import { Icon as WarningIcon } from "../../../../res/img/element-icons/warning.svg";
interface Props {
message: string;
}
export const VoiceBroadcastError: React.FC<Props> = ({ message }) => {
return (
<div className="mx_VoiceBroadcastRecordingConnectionError">
<WarningIcon className="mx_Icon mx_Icon_16" />
{message}
</div>
);
};

View file

@ -18,6 +18,7 @@ import React, { ReactElement } from "react";
import classNames from "classnames";
import {
VoiceBroadcastError,
VoiceBroadcastHeader,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybackControl,
@ -67,6 +68,24 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
["mx_VoiceBroadcastBody--pip"]: pip,
});
const content =
playbackState === VoiceBroadcastPlaybackState.Error ? (
<VoiceBroadcastError message={playback.errorMessage} />
) : (
<>
<div className="mx_VoiceBroadcastBody_controls">
{seekBackwardButton}
<VoiceBroadcastPlaybackControl state={playbackState} onClick={toggle} />
{seekForwardButton}
</div>
<SeekBar playback={playback} />
<div className="mx_VoiceBroadcastBody_timerow">
<Clock seconds={times.position} />
<Clock seconds={-times.timeLeft} />
</div>
</>
);
return (
<div className={classes}>
<VoiceBroadcastHeader
@ -77,16 +96,7 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
showBroadcast={playbackState !== VoiceBroadcastPlaybackState.Buffering}
showBuffering={playbackState === VoiceBroadcastPlaybackState.Buffering}
/>
<div className="mx_VoiceBroadcastBody_controls">
{seekBackwardButton}
<VoiceBroadcastPlaybackControl state={playbackState} onClick={toggle} />
{seekForwardButton}
</div>
<SeekBar playback={playback} />
<div className="mx_VoiceBroadcastBody_timerow">
<Clock seconds={times.position} />
<Clock seconds={-times.timeLeft} />
</div>
{content}
</div>
);
};

View file

@ -27,6 +27,7 @@ export * from "./audio/VoiceBroadcastRecorder";
export * from "./components/VoiceBroadcastBody";
export * from "./components/atoms/LiveBadge";
export * from "./components/atoms/VoiceBroadcastControl";
export * from "./components/atoms/VoiceBroadcastError";
export * from "./components/atoms/VoiceBroadcastHeader";
export * from "./components/atoms/VoiceBroadcastPlaybackControl";
export * from "./components/atoms/VoiceBroadcastRecordingConnectionError";

View file

@ -43,12 +43,14 @@ import {
import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents";
import { determineVoiceBroadcastLiveness } from "../utils/determineVoiceBroadcastLiveness";
import { _t } from "../../languageHandler";
export enum VoiceBroadcastPlaybackState {
Paused,
Playing,
Stopped,
Buffering,
Paused = "pause",
Playing = "playing",
Stopped = "stopped",
Buffering = "buffering",
Error = "error",
}
export enum VoiceBroadcastPlaybackEvent {
@ -205,12 +207,24 @@ export class VoiceBroadcastPlayback
}
};
private async tryLoadPlayback(chunkEvent: MatrixEvent): Promise<void> {
try {
return await this.loadPlayback(chunkEvent);
} catch (err) {
logger.warn("Unable to load broadcast playback", {
message: err.message,
broadcastId: this.infoEvent.getId(),
chunkId: chunkEvent.getId(),
});
this.setError();
}
}
private async loadPlayback(chunkEvent: MatrixEvent): Promise<void> {
const eventId = chunkEvent.getId();
if (!eventId) {
logger.warn("got voice broadcast chunk event without ID", this.infoEvent, chunkEvent);
return;
throw new Error("Broadcast chunk event without Id occurred");
}
const helper = new MediaEventHelper(chunkEvent);
@ -311,16 +325,28 @@ export class VoiceBroadcastPlayback
private async playEvent(event: MatrixEvent): Promise<void> {
this.setState(VoiceBroadcastPlaybackState.Playing);
this.currentlyPlaying = event;
const playback = await this.getOrLoadPlaybackForEvent(event);
const playback = await this.tryGetOrLoadPlaybackForEvent(event);
playback?.play();
}
private async tryGetOrLoadPlaybackForEvent(event: MatrixEvent): Promise<Playback | undefined> {
try {
return await this.getOrLoadPlaybackForEvent(event);
} catch (err) {
logger.warn("Unable to load broadcast playback", {
message: err.message,
broadcastId: this.infoEvent.getId(),
chunkId: event.getId(),
});
this.setError();
}
}
private async getOrLoadPlaybackForEvent(event: MatrixEvent): Promise<Playback | undefined> {
const eventId = event.getId();
if (!eventId) {
logger.warn("event without id occurred");
return;
throw new Error("Broadcast chunk event without Id occurred");
}
if (!this.playbacks.has(eventId)) {
@ -330,13 +356,12 @@ export class VoiceBroadcastPlayback
const playback = this.playbacks.get(eventId);
if (!playback) {
// logging error, because this should not happen
logger.warn("unable to find playback for event", event);
throw new Error(`Unable to find playback for event ${event.getId()}`);
}
// try to load the playback for the next event for a smooth(er) playback
const nextEvent = this.chunkEvents.getNext(event);
if (nextEvent) this.loadPlayback(nextEvent);
if (nextEvent) this.tryLoadPlayback(nextEvent);
return playback;
}
@ -405,8 +430,8 @@ export class VoiceBroadcastPlayback
}
const currentPlayback = this.getCurrentPlayback();
const skipToPlayback = await this.tryGetOrLoadPlaybackForEvent(event);
const currentPlaybackEvent = this.currentlyPlaying;
const skipToPlayback = await this.getOrLoadPlaybackForEvent(event);
if (!skipToPlayback) {
logger.warn("voice broadcast chunk to skip to not found", event);
@ -464,6 +489,9 @@ export class VoiceBroadcastPlayback
}
public stop(): void {
// error is a final state
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
this.setState(VoiceBroadcastPlaybackState.Stopped);
this.getCurrentPlayback()?.stop();
this.currentlyPlaying = null;
@ -471,6 +499,9 @@ export class VoiceBroadcastPlayback
}
public pause(): void {
// error is a final state
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
// stopped voice broadcasts cannot be paused
if (this.getState() === VoiceBroadcastPlaybackState.Stopped) return;
@ -479,6 +510,9 @@ export class VoiceBroadcastPlayback
}
public resume(): void {
// error is a final state
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
if (!this.currentlyPlaying) {
// no playback to resume, start from the beginning
this.start();
@ -496,6 +530,9 @@ export class VoiceBroadcastPlayback
* paused playing
*/
public async toggle(): Promise<void> {
// error is a final state
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
if (this.state === VoiceBroadcastPlaybackState.Stopped) {
await this.start();
return;
@ -514,6 +551,9 @@ export class VoiceBroadcastPlayback
}
private setState(state: VoiceBroadcastPlaybackState): void {
// error is a final state
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
if (this.state === state) {
return;
}
@ -522,6 +562,16 @@ export class VoiceBroadcastPlayback
this.emit(VoiceBroadcastPlaybackEvent.StateChanged, state, this);
}
/**
* Set error state. Stop current playback, if any.
*/
private setError(): void {
this.setState(VoiceBroadcastPlaybackState.Error);
this.getCurrentPlayback()?.stop();
this.currentlyPlaying = null;
this.setPosition(0);
}
public getInfoState(): VoiceBroadcastInfoState {
return this.infoState;
}
@ -536,6 +586,10 @@ export class VoiceBroadcastPlayback
this.setLiveness(determineVoiceBroadcastLiveness(this.infoState));
}
public get errorMessage(): string {
return this.getState() === VoiceBroadcastPlaybackState.Error ? _t("Unable to play this voice broadcast") : "";
}
public destroy(): void {
this.chunkRelationHelper.destroy();
this.infoRelationHelper.destroy();