Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 533b250

Browse files
authored
Handle broadcast chunk errors (#9970)
* Use strings for broadcast playback states * Handle broadcast decode errors
1 parent 60edb85 commit 533b250

File tree

10 files changed

+312
-104
lines changed

10 files changed

+312
-104
lines changed

src/i18n/strings/en_EN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@
659659
"%(senderName)s ended a <a>voice broadcast</a>": "%(senderName)s ended a <a>voice broadcast</a>",
660660
"You ended a voice broadcast": "You ended a voice broadcast",
661661
"%(senderName)s ended a voice broadcast": "%(senderName)s ended a voice broadcast",
662+
"Unable to play this voice broadcast": "Unable to play this voice broadcast",
662663
"Stop live broadcasting?": "Stop live broadcasting?",
663664
"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.",
664665
"Yes, stop broadcast": "Yes, stop broadcast",
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from "react";
18+
19+
import { Icon as WarningIcon } from "../../../../res/img/element-icons/warning.svg";
20+
21+
interface Props {
22+
message: string;
23+
}
24+
25+
export const VoiceBroadcastError: React.FC<Props> = ({ message }) => {
26+
return (
27+
<div className="mx_VoiceBroadcastRecordingConnectionError">
28+
<WarningIcon className="mx_Icon mx_Icon_16" />
29+
{message}
30+
</div>
31+
);
32+
};

src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import React, { ReactElement } from "react";
1818
import classNames from "classnames";
1919

2020
import {
21+
VoiceBroadcastError,
2122
VoiceBroadcastHeader,
2223
VoiceBroadcastPlayback,
2324
VoiceBroadcastPlaybackControl,
@@ -67,6 +68,24 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
6768
["mx_VoiceBroadcastBody--pip"]: pip,
6869
});
6970

71+
const content =
72+
playbackState === VoiceBroadcastPlaybackState.Error ? (
73+
<VoiceBroadcastError message={playback.errorMessage} />
74+
) : (
75+
<>
76+
<div className="mx_VoiceBroadcastBody_controls">
77+
{seekBackwardButton}
78+
<VoiceBroadcastPlaybackControl state={playbackState} onClick={toggle} />
79+
{seekForwardButton}
80+
</div>
81+
<SeekBar playback={playback} />
82+
<div className="mx_VoiceBroadcastBody_timerow">
83+
<Clock seconds={times.position} />
84+
<Clock seconds={-times.timeLeft} />
85+
</div>
86+
</>
87+
);
88+
7089
return (
7190
<div className={classes}>
7291
<VoiceBroadcastHeader
@@ -77,16 +96,7 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
7796
showBroadcast={playbackState !== VoiceBroadcastPlaybackState.Buffering}
7897
showBuffering={playbackState === VoiceBroadcastPlaybackState.Buffering}
7998
/>
80-
<div className="mx_VoiceBroadcastBody_controls">
81-
{seekBackwardButton}
82-
<VoiceBroadcastPlaybackControl state={playbackState} onClick={toggle} />
83-
{seekForwardButton}
84-
</div>
85-
<SeekBar playback={playback} />
86-
<div className="mx_VoiceBroadcastBody_timerow">
87-
<Clock seconds={times.position} />
88-
<Clock seconds={-times.timeLeft} />
89-
</div>
99+
{content}
90100
</div>
91101
);
92102
};

src/voice-broadcast/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export * from "./audio/VoiceBroadcastRecorder";
2727
export * from "./components/VoiceBroadcastBody";
2828
export * from "./components/atoms/LiveBadge";
2929
export * from "./components/atoms/VoiceBroadcastControl";
30+
export * from "./components/atoms/VoiceBroadcastError";
3031
export * from "./components/atoms/VoiceBroadcastHeader";
3132
export * from "./components/atoms/VoiceBroadcastPlaybackControl";
3233
export * from "./components/atoms/VoiceBroadcastRecordingConnectionError";

src/voice-broadcast/models/VoiceBroadcastPlayback.ts

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ import {
4343
import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
4444
import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents";
4545
import { determineVoiceBroadcastLiveness } from "../utils/determineVoiceBroadcastLiveness";
46+
import { _t } from "../../languageHandler";
4647

4748
export enum VoiceBroadcastPlaybackState {
48-
Paused,
49-
Playing,
50-
Stopped,
51-
Buffering,
49+
Paused = "pause",
50+
Playing = "playing",
51+
Stopped = "stopped",
52+
Buffering = "buffering",
53+
Error = "error",
5254
}
5355

5456
export enum VoiceBroadcastPlaybackEvent {
@@ -205,12 +207,24 @@ export class VoiceBroadcastPlayback
205207
}
206208
};
207209

210+
private async tryLoadPlayback(chunkEvent: MatrixEvent): Promise<void> {
211+
try {
212+
return await this.loadPlayback(chunkEvent);
213+
} catch (err) {
214+
logger.warn("Unable to load broadcast playback", {
215+
message: err.message,
216+
broadcastId: this.infoEvent.getId(),
217+
chunkId: chunkEvent.getId(),
218+
});
219+
this.setError();
220+
}
221+
}
222+
208223
private async loadPlayback(chunkEvent: MatrixEvent): Promise<void> {
209224
const eventId = chunkEvent.getId();
210225

211226
if (!eventId) {
212-
logger.warn("got voice broadcast chunk event without ID", this.infoEvent, chunkEvent);
213-
return;
227+
throw new Error("Broadcast chunk event without Id occurred");
214228
}
215229

216230
const helper = new MediaEventHelper(chunkEvent);
@@ -311,16 +325,28 @@ export class VoiceBroadcastPlayback
311325
private async playEvent(event: MatrixEvent): Promise<void> {
312326
this.setState(VoiceBroadcastPlaybackState.Playing);
313327
this.currentlyPlaying = event;
314-
const playback = await this.getOrLoadPlaybackForEvent(event);
328+
const playback = await this.tryGetOrLoadPlaybackForEvent(event);
315329
playback?.play();
316330
}
317331

332+
private async tryGetOrLoadPlaybackForEvent(event: MatrixEvent): Promise<Playback | undefined> {
333+
try {
334+
return await this.getOrLoadPlaybackForEvent(event);
335+
} catch (err) {
336+
logger.warn("Unable to load broadcast playback", {
337+
message: err.message,
338+
broadcastId: this.infoEvent.getId(),
339+
chunkId: event.getId(),
340+
});
341+
this.setError();
342+
}
343+
}
344+
318345
private async getOrLoadPlaybackForEvent(event: MatrixEvent): Promise<Playback | undefined> {
319346
const eventId = event.getId();
320347

321348
if (!eventId) {
322-
logger.warn("event without id occurred");
323-
return;
349+
throw new Error("Broadcast chunk event without Id occurred");
324350
}
325351

326352
if (!this.playbacks.has(eventId)) {
@@ -330,13 +356,12 @@ export class VoiceBroadcastPlayback
330356
const playback = this.playbacks.get(eventId);
331357

332358
if (!playback) {
333-
// logging error, because this should not happen
334-
logger.warn("unable to find playback for event", event);
359+
throw new Error(`Unable to find playback for event ${event.getId()}`);
335360
}
336361

337362
// try to load the playback for the next event for a smooth(er) playback
338363
const nextEvent = this.chunkEvents.getNext(event);
339-
if (nextEvent) this.loadPlayback(nextEvent);
364+
if (nextEvent) this.tryLoadPlayback(nextEvent);
340365

341366
return playback;
342367
}
@@ -405,8 +430,8 @@ export class VoiceBroadcastPlayback
405430
}
406431

407432
const currentPlayback = this.getCurrentPlayback();
433+
const skipToPlayback = await this.tryGetOrLoadPlaybackForEvent(event);
408434
const currentPlaybackEvent = this.currentlyPlaying;
409-
const skipToPlayback = await this.getOrLoadPlaybackForEvent(event);
410435

411436
if (!skipToPlayback) {
412437
logger.warn("voice broadcast chunk to skip to not found", event);
@@ -464,13 +489,19 @@ export class VoiceBroadcastPlayback
464489
}
465490

466491
public stop(): void {
492+
// error is a final state
493+
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
494+
467495
this.setState(VoiceBroadcastPlaybackState.Stopped);
468496
this.getCurrentPlayback()?.stop();
469497
this.currentlyPlaying = null;
470498
this.setPosition(0);
471499
}
472500

473501
public pause(): void {
502+
// error is a final state
503+
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
504+
474505
// stopped voice broadcasts cannot be paused
475506
if (this.getState() === VoiceBroadcastPlaybackState.Stopped) return;
476507

@@ -479,6 +510,9 @@ export class VoiceBroadcastPlayback
479510
}
480511

481512
public resume(): void {
513+
// error is a final state
514+
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
515+
482516
if (!this.currentlyPlaying) {
483517
// no playback to resume, start from the beginning
484518
this.start();
@@ -496,6 +530,9 @@ export class VoiceBroadcastPlayback
496530
* paused → playing
497531
*/
498532
public async toggle(): Promise<void> {
533+
// error is a final state
534+
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
535+
499536
if (this.state === VoiceBroadcastPlaybackState.Stopped) {
500537
await this.start();
501538
return;
@@ -514,6 +551,9 @@ export class VoiceBroadcastPlayback
514551
}
515552

516553
private setState(state: VoiceBroadcastPlaybackState): void {
554+
// error is a final state
555+
if (this.getState() === VoiceBroadcastPlaybackState.Error) return;
556+
517557
if (this.state === state) {
518558
return;
519559
}
@@ -522,6 +562,16 @@ export class VoiceBroadcastPlayback
522562
this.emit(VoiceBroadcastPlaybackEvent.StateChanged, state, this);
523563
}
524564

565+
/**
566+
* Set error state. Stop current playback, if any.
567+
*/
568+
private setError(): void {
569+
this.setState(VoiceBroadcastPlaybackState.Error);
570+
this.getCurrentPlayback()?.stop();
571+
this.currentlyPlaying = null;
572+
this.setPosition(0);
573+
}
574+
525575
public getInfoState(): VoiceBroadcastInfoState {
526576
return this.infoState;
527577
}
@@ -536,6 +586,10 @@ export class VoiceBroadcastPlayback
536586
this.setLiveness(determineVoiceBroadcastLiveness(this.infoState));
537587
}
538588

589+
public get errorMessage(): string {
590+
return this.getState() === VoiceBroadcastPlaybackState.Error ? _t("Unable to play this voice broadcast") : "";
591+
}
592+
539593
public destroy(): void {
540594
this.chunkRelationHelper.destroy();
541595
this.infoRelationHelper.destroy();

test/voice-broadcast/components/atoms/__snapshots__/VoiceBroadcastPlaybackControl-test.tsx.snap

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`<VoiceBroadcastPlaybackControl /> should render state 0 as expected 1`] = `
3+
exports[`<VoiceBroadcastPlaybackControl /> should render state buffering as expected 1`] = `
44
<div>
55
<div
6-
aria-label="resume voice broadcast"
7-
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
6+
aria-label="pause voice broadcast"
7+
class="mx_AccessibleButton mx_VoiceBroadcastControl"
88
role="button"
99
tabindex="0"
1010
>
@@ -15,11 +15,11 @@ exports[`<VoiceBroadcastPlaybackControl /> should render state 0 as expected 1`]
1515
</div>
1616
`;
1717

18-
exports[`<VoiceBroadcastPlaybackControl /> should render state 1 as expected 1`] = `
18+
exports[`<VoiceBroadcastPlaybackControl /> should render state pause as expected 1`] = `
1919
<div>
2020
<div
21-
aria-label="pause voice broadcast"
22-
class="mx_AccessibleButton mx_VoiceBroadcastControl"
21+
aria-label="resume voice broadcast"
22+
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
2323
role="button"
2424
tabindex="0"
2525
>
@@ -30,11 +30,11 @@ exports[`<VoiceBroadcastPlaybackControl /> should render state 1 as expected 1`]
3030
</div>
3131
`;
3232

33-
exports[`<VoiceBroadcastPlaybackControl /> should render state 2 as expected 1`] = `
33+
exports[`<VoiceBroadcastPlaybackControl /> should render state playing as expected 1`] = `
3434
<div>
3535
<div
36-
aria-label="play voice broadcast"
37-
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
36+
aria-label="pause voice broadcast"
37+
class="mx_AccessibleButton mx_VoiceBroadcastControl"
3838
role="button"
3939
tabindex="0"
4040
>
@@ -45,11 +45,11 @@ exports[`<VoiceBroadcastPlaybackControl /> should render state 2 as expected 1`]
4545
</div>
4646
`;
4747

48-
exports[`<VoiceBroadcastPlaybackControl /> should render state 3 as expected 1`] = `
48+
exports[`<VoiceBroadcastPlaybackControl /> should render state stopped as expected 1`] = `
4949
<div>
5050
<div
51-
aria-label="pause voice broadcast"
52-
class="mx_AccessibleButton mx_VoiceBroadcastControl"
51+
aria-label="play voice broadcast"
52+
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
5353
role="button"
5454
tabindex="0"
5555
>

test/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
VoiceBroadcastPlaybackEvent,
2929
VoiceBroadcastPlaybackState,
3030
} from "../../../../src/voice-broadcast";
31-
import { stubClient } from "../../../test-utils";
31+
import { filterConsole, stubClient } from "../../../test-utils";
3232
import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils";
3333
import dis from "../../../../src/dispatcher/dispatcher";
3434
import { Action } from "../../../../src/dispatcher/actions";
@@ -53,6 +53,11 @@ describe("VoiceBroadcastPlaybackBody", () => {
5353
let playback: VoiceBroadcastPlayback;
5454
let renderResult: RenderResult;
5555

56+
filterConsole(
57+
// expected for some tests
58+
"voice broadcast chunk event to skip to not found",
59+
);
60+
5661
beforeAll(() => {
5762
client = stubClient();
5863
mocked(client.relations).mockClear();
@@ -214,6 +219,17 @@ describe("VoiceBroadcastPlaybackBody", () => {
214219
});
215220
});
216221

222+
describe("when rendering an error broadcast", () => {
223+
beforeEach(() => {
224+
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Error);
225+
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
226+
});
227+
228+
it("should render as expected", () => {
229+
expect(renderResult.container).toMatchSnapshot();
230+
});
231+
});
232+
217233
describe.each([
218234
[VoiceBroadcastPlaybackState.Paused, "not-live"],
219235
[VoiceBroadcastPlaybackState.Playing, "live"],

0 commit comments

Comments
 (0)