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

Implement voice broadcast playback buffering #9435

Merged
merged 3 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import {
PlaybackControlButton,
VoiceBroadcastHeader,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybackState,
} from "../..";
import Spinner from "../../../components/views/elements/Spinner";
import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback";

interface VoiceBroadcastPlaybackBodyProps {
Expand All @@ -38,6 +40,10 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
playbackState,
} = useVoiceBroadcastPlayback(playback);

const control = playbackState === VoiceBroadcastPlaybackState.Buffering
? <Spinner />
: <PlaybackControlButton onClick={toggle} state={playbackState} />;

return (
<div className="mx_VoiceBroadcastPlaybackBody">
<VoiceBroadcastHeader
Expand All @@ -47,10 +53,7 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
showBroadcast={true}
/>
<div className="mx_VoiceBroadcastPlaybackBody_controls">
<PlaybackControlButton
onClick={toggle}
state={playbackState}
/>
{ control }
</div>
</div>
);
Expand Down
42 changes: 31 additions & 11 deletions src/voice-broadcast/models/VoiceBroadcastPlayback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export enum VoiceBroadcastPlaybackState {
Paused,
Playing,
Stopped,
Buffering,
}

export enum VoiceBroadcastPlaybackEvent {
Expand Down Expand Up @@ -91,7 +92,7 @@ export class VoiceBroadcastPlayback
this.chunkRelationHelper.emitCurrent();
}

private addChunkEvent(event: MatrixEvent): boolean {
private addChunkEvent = async (event: MatrixEvent): Promise<boolean> => {
const eventId = event.getId();

if (!eventId
Expand All @@ -102,8 +103,17 @@ export class VoiceBroadcastPlayback
}

this.chunkEvents.set(eventId, event);

if (this.getState() !== VoiceBroadcastPlaybackState.Stopped) {
await this.enqueueChunk(event);
}

if (this.getState() === VoiceBroadcastPlaybackState.Buffering) {
await this.start();
}

return true;
}
};

private addInfoEvent = (event: MatrixEvent): void => {
if (this.lastInfoEvent && this.lastInfoEvent.getTs() >= event.getTs()) {
Expand Down Expand Up @@ -149,20 +159,30 @@ export class VoiceBroadcastPlayback
playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, state));
}

private onPlaybackStateChange(playback: Playback, newState: PlaybackState) {
private async onPlaybackStateChange(playback: Playback, newState: PlaybackState) {
if (newState !== PlaybackState.Stopped) {
return;
}

const next = this.queue[this.queue.indexOf(playback) + 1];
await this.playNext(playback);
}

private async playNext(current: Playback): Promise<void> {
const next = this.queue[this.queue.indexOf(current) + 1];

if (next) {
this.setState(VoiceBroadcastPlaybackState.Playing);
this.currentlyPlaying = next;
next.play();
await next.play();
return;
}

this.setState(VoiceBroadcastPlaybackState.Stopped);
if (this.getInfoState() === VoiceBroadcastInfoState.Stopped) {
this.setState(VoiceBroadcastPlaybackState.Stopped);
} else {
// No more chunks available, although the broadcast is not finished → enter buffering state.
this.setState(VoiceBroadcastPlaybackState.Buffering);
}
}

public async start(): Promise<void> {
Expand All @@ -174,14 +194,14 @@ export class VoiceBroadcastPlayback
? 0 // start at the beginning for an ended voice broadcast
: this.queue.length - 1; // start at the current chunk for an ongoing voice broadcast

if (this.queue.length === 0 || !this.queue[toPlayIndex]) {
this.setState(VoiceBroadcastPlaybackState.Stopped);
if (this.queue[toPlayIndex]) {
this.setState(VoiceBroadcastPlaybackState.Playing);
this.currentlyPlaying = this.queue[toPlayIndex];
await this.currentlyPlaying.play();
return;
}

this.setState(VoiceBroadcastPlaybackState.Playing);
this.currentlyPlaying = this.queue[toPlayIndex];
await this.currentlyPlaying.play();
this.setState(VoiceBroadcastPlaybackState.Buffering);
}

public get length(): number {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import React from "react";
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { render, RenderResult } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";

import {
VoiceBroadcastInfoEventType,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybackBody,
VoiceBroadcastPlaybackState,
} from "../../../../src/voice-broadcast";
import { mkEvent, stubClient } from "../../../test-utils";

Expand All @@ -40,6 +42,7 @@ describe("VoiceBroadcastPlaybackBody", () => {
let client: MatrixClient;
let infoEvent: MatrixEvent;
let playback: VoiceBroadcastPlayback;
let renderResult: RenderResult;

beforeAll(() => {
client = stubClient();
Expand All @@ -50,12 +53,18 @@ describe("VoiceBroadcastPlaybackBody", () => {
room: roomId,
user: userId,
});
});

beforeEach(() => {
playback = new VoiceBroadcastPlayback(infoEvent, client);
jest.spyOn(playback, "toggle");
jest.spyOn(playback, "getState");
});

describe("when rendering a broadcast", () => {
let renderResult: RenderResult;
describe("when rendering a buffering voice broadcast", () => {
beforeEach(() => {
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Buffering);
});

beforeEach(() => {
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
Expand All @@ -64,6 +73,16 @@ describe("VoiceBroadcastPlaybackBody", () => {
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
});

describe("when rendering a broadcast", () => {
beforeEach(() => {
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
});

it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});

describe("and clicking the play button", () => {
beforeEach(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,78 @@ exports[`VoiceBroadcastPlaybackBody when rendering a broadcast should render as
</div>
</div>
`;

exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastPlaybackBody"
>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
My room
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
@user:example.com
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
Voice broadcast
</div>
</div>
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
Live
</div>
</div>
<div
class="mx_VoiceBroadcastPlaybackBody_controls"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading..."
class="mx_Spinner_icon"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
</div>
</div>
`;
Loading