Start playback for ongoing broadcast with the last chunk (#9434)
This commit is contained in:
parent
1b74782854
commit
631720b21b
2 changed files with 140 additions and 101 deletions
|
@ -56,7 +56,6 @@ export class VoiceBroadcastPlayback
|
||||||
private state = VoiceBroadcastPlaybackState.Stopped;
|
private state = VoiceBroadcastPlaybackState.Stopped;
|
||||||
private infoState: VoiceBroadcastInfoState;
|
private infoState: VoiceBroadcastInfoState;
|
||||||
private chunkEvents = new Map<string, MatrixEvent>();
|
private chunkEvents = new Map<string, MatrixEvent>();
|
||||||
/** Holds the playback queue with a 1-based index (sequence number) */
|
|
||||||
private queue: Playback[] = [];
|
private queue: Playback[] = [];
|
||||||
private currentlyPlaying: Playback;
|
private currentlyPlaying: Playback;
|
||||||
private lastInfoEvent: MatrixEvent;
|
private lastInfoEvent: MatrixEvent;
|
||||||
|
@ -138,7 +137,7 @@ export class VoiceBroadcastPlayback
|
||||||
|
|
||||||
private async enqueueChunk(chunkEvent: MatrixEvent) {
|
private async enqueueChunk(chunkEvent: MatrixEvent) {
|
||||||
const sequenceNumber = parseInt(chunkEvent.getContent()?.[VoiceBroadcastChunkEventType]?.sequence, 10);
|
const sequenceNumber = parseInt(chunkEvent.getContent()?.[VoiceBroadcastChunkEventType]?.sequence, 10);
|
||||||
if (isNaN(sequenceNumber)) return;
|
if (isNaN(sequenceNumber) || sequenceNumber < 1) return;
|
||||||
|
|
||||||
const helper = new MediaEventHelper(chunkEvent);
|
const helper = new MediaEventHelper(chunkEvent);
|
||||||
const blob = await helper.sourceBlob.value;
|
const blob = await helper.sourceBlob.value;
|
||||||
|
@ -146,7 +145,7 @@ export class VoiceBroadcastPlayback
|
||||||
const playback = PlaybackManager.instance.createPlaybackInstance(buffer);
|
const playback = PlaybackManager.instance.createPlaybackInstance(buffer);
|
||||||
await playback.prepare();
|
await playback.prepare();
|
||||||
playback.clockInfo.populatePlaceholdersFrom(chunkEvent);
|
playback.clockInfo.populatePlaceholdersFrom(chunkEvent);
|
||||||
this.queue[sequenceNumber] = playback;
|
this.queue[sequenceNumber - 1] = playback; // -1 because the sequence number starts at 1
|
||||||
playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, state));
|
playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,17 +170,18 @@ export class VoiceBroadcastPlayback
|
||||||
await this.loadChunks();
|
await this.loadChunks();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.queue.length === 0 || !this.queue[1]) {
|
const toPlayIndex = this.getInfoState() === VoiceBroadcastInfoState.Stopped
|
||||||
// set to stopped fi the queue is empty of the first chunk (sequence number: 1-based index) is missing
|
? 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);
|
this.setState(VoiceBroadcastPlaybackState.Stopped);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState(VoiceBroadcastPlaybackState.Playing);
|
this.setState(VoiceBroadcastPlaybackState.Playing);
|
||||||
// index of the first chunk is the first sequence number
|
this.currentlyPlaying = this.queue[toPlayIndex];
|
||||||
const first = this.queue[1];
|
await this.currentlyPlaying.play();
|
||||||
this.currentlyPlaying = first;
|
|
||||||
await first.play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get length(): number {
|
public get length(): number {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { MediaEventHelper } from "../../../src/utils/MediaEventHelper";
|
||||||
import {
|
import {
|
||||||
VoiceBroadcastChunkEventType,
|
VoiceBroadcastChunkEventType,
|
||||||
VoiceBroadcastInfoEventType,
|
VoiceBroadcastInfoEventType,
|
||||||
|
VoiceBroadcastInfoState,
|
||||||
VoiceBroadcastPlayback,
|
VoiceBroadcastPlayback,
|
||||||
VoiceBroadcastPlaybackEvent,
|
VoiceBroadcastPlaybackEvent,
|
||||||
VoiceBroadcastPlaybackState,
|
VoiceBroadcastPlaybackState,
|
||||||
|
@ -100,15 +101,33 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
const mkInfoEvent = (state: VoiceBroadcastInfoState) => {
|
||||||
client = stubClient();
|
return mkEvent({
|
||||||
infoEvent = mkEvent({
|
|
||||||
event: true,
|
event: true,
|
||||||
type: VoiceBroadcastInfoEventType,
|
type: VoiceBroadcastInfoEventType,
|
||||||
user: userId,
|
user: userId,
|
||||||
room: roomId,
|
room: roomId,
|
||||||
content: {},
|
content: {
|
||||||
|
state,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mkPlayback = () => {
|
||||||
|
const playback = new VoiceBroadcastPlayback(infoEvent, client);
|
||||||
|
jest.spyOn(playback, "removeAllListeners");
|
||||||
|
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
|
||||||
|
return playback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setUpChunkEvents = (chunkEvents: MatrixEvent[]) => {
|
||||||
|
const relations = new Relations(RelationType.Reference, EventType.RoomMessage, client);
|
||||||
|
jest.spyOn(relations, "getRelations").mockReturnValue(chunkEvents);
|
||||||
|
mocked(getReferenceRelationsForEvent).mockReturnValue(relations);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
client = stubClient();
|
||||||
|
|
||||||
// crap event to test 0 as first sequence number
|
// crap event to test 0 as first sequence number
|
||||||
chunk0Event = mkChunkEvent(0);
|
chunk0Event = mkChunkEvent(0);
|
||||||
|
@ -139,128 +158,148 @@ describe("VoiceBroadcastPlayback", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
onStateChanged = jest.fn();
|
onStateChanged = jest.fn();
|
||||||
|
|
||||||
playback = new VoiceBroadcastPlayback(infoEvent, client);
|
|
||||||
jest.spyOn(playback, "removeAllListeners");
|
|
||||||
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when there is only a 0 sequence event", () => {
|
describe("when there is a running voice broadcast with some chunks", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const relations = new Relations(RelationType.Reference, EventType.RoomMessage, client);
|
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Running);
|
||||||
jest.spyOn(relations, "getRelations").mockReturnValue([chunk0Event]);
|
playback = mkPlayback();
|
||||||
mocked(getReferenceRelationsForEvent).mockReturnValue(relations);
|
setUpChunkEvents([chunk2Event, chunk0Event, chunk1Event]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when calling start", () => {
|
describe("and calling start", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await playback.start();
|
await playback.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
it("should play the last chunk", () => {
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when there are some chunks", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const relations = new Relations(RelationType.Reference, EventType.RoomMessage, client);
|
|
||||||
jest.spyOn(relations, "getRelations").mockReturnValue([chunk2Event, chunk1Event]);
|
|
||||||
mocked(getReferenceRelationsForEvent).mockReturnValue(relations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should expose the info event", () => {
|
|
||||||
expect(playback.infoEvent).toBe(infoEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be in state Stopped", () => {
|
|
||||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when calling start", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await playback.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
|
||||||
|
|
||||||
it("should play the chunks", () => {
|
|
||||||
// assert that the first chunk is being played
|
// assert that the first chunk is being played
|
||||||
expect(chunk1Playback.play).toHaveBeenCalled();
|
|
||||||
expect(chunk2Playback.play).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
// simulate end of first chunk
|
|
||||||
chunk1Playback.emit(PlaybackState.Stopped);
|
|
||||||
|
|
||||||
// assert that the second chunk is being played
|
|
||||||
expect(chunk2Playback.play).toHaveBeenCalled();
|
expect(chunk2Playback.play).toHaveBeenCalled();
|
||||||
|
expect(chunk1Playback.play).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// simulate end of second chunk
|
describe("when there is a stopped voice broadcast", () => {
|
||||||
chunk2Playback.emit(PlaybackState.Stopped);
|
beforeEach(() => {
|
||||||
|
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
|
||||||
|
playback = mkPlayback();
|
||||||
|
});
|
||||||
|
|
||||||
// assert that the entire playback is now in stopped state
|
describe("and there is only a 0 sequence event", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setUpChunkEvents([chunk0Event]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and calling start", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await playback.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and there are some chunks", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setUpChunkEvents([chunk2Event, chunk0Event, chunk1Event]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should expose the info event", () => {
|
||||||
|
expect(playback.infoEvent).toBe(infoEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be in state Stopped", () => {
|
||||||
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
|
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and calling pause", () => {
|
describe("and calling start", () => {
|
||||||
beforeEach(() => {
|
|
||||||
playback.pause();
|
|
||||||
});
|
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
|
||||||
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when calling toggle for the first time", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await playback.toggle();
|
|
||||||
});
|
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
|
||||||
|
|
||||||
describe("and calling toggle a second time", () => {
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await playback.toggle();
|
await playback.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||||
|
|
||||||
describe("and calling toggle a third time", () => {
|
it("should play the chunks beginning with the first one", () => {
|
||||||
beforeEach(async () => {
|
// assert that the first chunk is being played
|
||||||
await playback.toggle();
|
expect(chunk1Playback.play).toHaveBeenCalled();
|
||||||
|
expect(chunk2Playback.play).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// simulate end of first chunk
|
||||||
|
chunk1Playback.emit(PlaybackState.Stopped);
|
||||||
|
|
||||||
|
// assert that the second chunk is being played
|
||||||
|
expect(chunk2Playback.play).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// simulate end of second chunk
|
||||||
|
chunk2Playback.emit(PlaybackState.Stopped);
|
||||||
|
|
||||||
|
// assert that the entire playback is now in stopped state
|
||||||
|
expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and calling pause", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
playback.pause();
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
||||||
|
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("when calling stop", () => {
|
describe("and calling toggle for the first time", () => {
|
||||||
beforeEach(() => {
|
|
||||||
playback.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
|
||||||
|
|
||||||
describe("and calling toggle", () => {
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mocked(onStateChanged).mockReset();
|
|
||||||
await playback.toggle();
|
await playback.toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||||
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when calling destroy", () => {
|
describe("and calling toggle a second time", () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
playback.destroy();
|
await playback.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused);
|
||||||
|
|
||||||
|
describe("and calling toggle a third time", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await playback.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call removeAllListeners", () => {
|
describe("and calling stop", () => {
|
||||||
expect(playback.removeAllListeners).toHaveBeenCalled();
|
beforeEach(() => {
|
||||||
|
playback.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped);
|
||||||
|
|
||||||
|
describe("and calling toggle", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mocked(onStateChanged).mockReset();
|
||||||
|
await playback.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
|
||||||
|
itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Playing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and calling destroy", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
playback.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call removeAllListeners", () => {
|
||||||
|
expect(playback.removeAllListeners).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue