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

Commit

Permalink
Implement voice broadcast playback buffering
Browse files Browse the repository at this point in the history
  • Loading branch information
weeman1337 committed Oct 17, 2022
1 parent 631720b commit 67f58e2
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 22 deletions.
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

0 comments on commit 67f58e2

Please sign in to comment.