Handle broadcast chunk errors (#9970)
* Use strings for broadcast playback states * Handle broadcast decode errors
This commit is contained in:
parent
60edb85a1a
commit
533b250bb6
10 changed files with 483 additions and 275 deletions
|
@ -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",
|
||||
|
|
32
src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx
Normal file
32
src/voice-broadcast/components/atoms/VoiceBroadcastError.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue