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();

View file

@ -1,6 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<VoiceBroadcastPlaybackControl /> should render state 0 as expected 1`] = `
exports[`<VoiceBroadcastPlaybackControl /> should render state buffering as expected 1`] = `
<div>
<div
aria-label="pause voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>
`;
exports[`<VoiceBroadcastPlaybackControl /> should render state pause as expected 1`] = `
<div>
<div
aria-label="resume voice broadcast"
@ -15,7 +30,7 @@ exports[`<VoiceBroadcastPlaybackControl /> should render state 0 as expected 1`]
</div>
`;
exports[`<VoiceBroadcastPlaybackControl /> should render state 1 as expected 1`] = `
exports[`<VoiceBroadcastPlaybackControl /> should render state playing as expected 1`] = `
<div>
<div
aria-label="pause voice broadcast"
@ -30,7 +45,7 @@ exports[`<VoiceBroadcastPlaybackControl /> should render state 1 as expected 1`]
</div>
`;
exports[`<VoiceBroadcastPlaybackControl /> should render state 2 as expected 1`] = `
exports[`<VoiceBroadcastPlaybackControl /> should render state stopped as expected 1`] = `
<div>
<div
aria-label="play voice broadcast"
@ -44,18 +59,3 @@ exports[`<VoiceBroadcastPlaybackControl /> should render state 2 as expected 1`]
</div>
</div>
`;
exports[`<VoiceBroadcastPlaybackControl /> should render state 3 as expected 1`] = `
<div>
<div
aria-label="pause voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>
`;

View file

@ -28,7 +28,7 @@ import {
VoiceBroadcastPlaybackEvent,
VoiceBroadcastPlaybackState,
} from "../../../../src/voice-broadcast";
import { stubClient } from "../../../test-utils";
import { filterConsole, stubClient } from "../../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils";
import dis from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
@ -53,6 +53,11 @@ describe("VoiceBroadcastPlaybackBody", () => {
let playback: VoiceBroadcastPlayback;
let renderResult: RenderResult;
filterConsole(
// expected for some tests
"voice broadcast chunk event to skip to not found",
);
beforeAll(() => {
client = stubClient();
mocked(client.relations).mockClear();
@ -214,6 +219,17 @@ describe("VoiceBroadcastPlaybackBody", () => {
});
});
describe("when rendering an error broadcast", () => {
beforeEach(() => {
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Error);
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
});
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
});
describe.each([
[VoiceBroadcastPlaybackState.Paused, "not-live"],
[VoiceBroadcastPlaybackState.Playing, "live"],

View file

@ -1,235 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VoiceBroadcastPlaybackBody when rendering a 0/not-live broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastBody"
>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
My room
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room_wrapper"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
</div>
<div
aria-label="Change input device"
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
</span>
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
</div>
</div>
<div
class="mx_VoiceBroadcastBody_controls"
>
<div
aria-label="30s backward"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_24"
/>
</div>
<div
aria-label="resume voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
<div
aria-label="30s forward"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_24"
/>
</div>
</div>
<input
class="mx_SeekBar"
max="1"
min="0"
step="0.001"
style="--fillTo: 0;"
tabindex="0"
type="range"
value="0"
/>
<div
class="mx_VoiceBroadcastBody_timerow"
>
<span
class="mx_Clock"
>
00:00
</span>
<span
class="mx_Clock"
>
-23:42
</span>
</div>
</div>
</div>
`;
exports[`VoiceBroadcastPlaybackBody when rendering a 1/live broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastBody"
>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
My room
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room_wrapper"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
</div>
<div
aria-label="Change input device"
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
</span>
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
</div>
<div
class="mx_LiveBadge"
>
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
</div>
<div
class="mx_VoiceBroadcastBody_controls"
>
<div
aria-label="30s backward"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_24"
/>
</div>
<div
aria-label="pause voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
<div
aria-label="30s forward"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_24"
/>
</div>
</div>
<input
class="mx_SeekBar"
max="1"
min="0"
step="0.001"
style="--fillTo: 0;"
tabindex="0"
type="range"
value="0"
/>
<div
class="mx_VoiceBroadcastBody_timerow"
>
<span
class="mx_Clock"
>
00:00
</span>
<span
class="mx_Clock"
>
-23:42
</span>
</div>
</div>
</div>
`;
exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast should render as expected 1`] = `
<div>
<div
@ -357,6 +127,117 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
</div>
`;
exports[`VoiceBroadcastPlaybackBody when rendering a pause/not-live broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastBody"
>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
My room
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room_wrapper"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
</div>
<div
aria-label="Change input device"
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
</span>
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
</div>
</div>
<div
class="mx_VoiceBroadcastBody_controls"
>
<div
aria-label="30s backward"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_24"
/>
</div>
<div
aria-label="resume voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-play"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
<div
aria-label="30s forward"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_24"
/>
</div>
</div>
<input
class="mx_SeekBar"
max="1"
min="0"
step="0.001"
style="--fillTo: 0;"
tabindex="0"
type="range"
value="0"
/>
<div
class="mx_VoiceBroadcastBody_timerow"
>
<span
class="mx_Clock"
>
00:00
</span>
<span
class="mx_Clock"
>
-23:42
</span>
</div>
</div>
</div>
`;
exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast in pip mode should render as expected 1`] = `
<div>
<div
@ -591,6 +472,125 @@ exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast should re
</div>
`;
exports[`VoiceBroadcastPlaybackBody when rendering a playing/live broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastBody"
>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
My room
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room_wrapper"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
</div>
<div
aria-label="Change input device"
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
</span>
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
</div>
<div
class="mx_LiveBadge"
>
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
</div>
<div
class="mx_VoiceBroadcastBody_controls"
>
<div
aria-label="30s backward"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_24"
/>
</div>
<div
aria-label="pause voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
<div
aria-label="30s forward"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary_content"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_24"
/>
</div>
</div>
<input
class="mx_SeekBar"
max="1"
min="0"
step="0.001"
style="--fillTo: 0;"
tabindex="0"
type="range"
value="0"
/>
<div
class="mx_VoiceBroadcastBody_timerow"
>
<span
class="mx_Clock"
>
00:00
</span>
<span
class="mx_Clock"
>
-23:42
</span>
</div>
</div>
</div>
`;
exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast should render as expected 1`] = `
<div>
<div
@ -681,3 +681,64 @@ exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast should re
</div>
</div>
`;
exports[`VoiceBroadcastPlaybackBody when rendering an error broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastBody"
>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
My room
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room_wrapper"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
</div>
<div
aria-label="Change input device"
class="mx_AccessibleButton mx_VoiceBroadcastHeader_line"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
</span>
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
</div>
</div>
<div
class="mx_VoiceBroadcastRecordingConnectionError"
>
<div
class="mx_Icon mx_Icon_16"
/>
Unable to play this voice broadcast
</div>
</div>
</div>
`;

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a { state: 0, liveness: 'not-live' }/%s broadcast should render as expected 1`] = `
exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a { state: 'pause', liveness: 'not-live' }/%s broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small"
@ -76,7 +76,7 @@ exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a { state: 0, livene
</div>
`;
exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a { state: 1, liveness: 'live' }/%s broadcast should render as expected 1`] = `
exports[`<VoiceBroadcastSmallPlaybackBody /> when rendering a { state: 'playing', liveness: 'live' }/%s broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip mx_VoiceBroadcastBody--small"

View file

@ -186,7 +186,11 @@ describe("VoiceBroadcastPlayback", () => {
});
};
filterConsole("Starting load of AsyncWrapper for modal");
filterConsole(
"Starting load of AsyncWrapper for modal",
// expected for some tests
"Unable to load broadcast playback",
);
beforeEach(() => {
client = stubClient();
@ -283,6 +287,35 @@ describe("VoiceBroadcastPlayback", () => {
expect(playback.durationSeconds).toEqual(6.5);
});
describe("and starting a playback with a broken chunk", () => {
beforeEach(async () => {
mocked(chunk2Playback.prepare).mockRejectedValue("Error decoding chunk");
await playback.start();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Error);
it("start() should keep it in the error state)", async () => {
await playback.start();
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error);
});
it("stop() should keep it in the error state)", () => {
playback.stop();
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error);
});
it("toggle() should keep it in the error state)", async () => {
await playback.toggle();
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error);
});
it("pause() should keep it in the error state)", () => {
playback.pause();
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Error);
});
});
describe("and an event with the same transaction Id occurs", () => {
beforeEach(() => {
room.addLiveEvents([chunk2BEvent]);