@@ -14,10 +14,24 @@ See the License for the specific language governing permissions and
1414limitations under the License.
1515*/
1616
17- import { MatrixEvent } from "matrix-js-sdk/src/matrix" ;
17+ import {
18+ EventType ,
19+ MatrixClient ,
20+ MatrixEvent ,
21+ MatrixEventEvent ,
22+ MsgType ,
23+ RelationType ,
24+ } from "matrix-js-sdk/src/matrix" ;
25+ import { Relations , RelationsEvent } from "matrix-js-sdk/src/models/relations" ;
1826import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter" ;
1927
28+ import { Playback , PlaybackState } from "../../audio/Playback" ;
29+ import { PlaybackManager } from "../../audio/PlaybackManager" ;
30+ import { getReferenceRelationsForEvent } from "../../events" ;
31+ import { UPDATE_EVENT } from "../../stores/AsyncStore" ;
32+ import { MediaEventHelper } from "../../utils/MediaEventHelper" ;
2033import { IDestroyable } from "../../utils/IDestroyable" ;
34+ import { VoiceBroadcastChunkEventType } from ".." ;
2135
2236export enum VoiceBroadcastPlaybackState {
2337 Paused ,
@@ -26,51 +40,216 @@ export enum VoiceBroadcastPlaybackState {
2640}
2741
2842export enum VoiceBroadcastPlaybackEvent {
43+ LengthChanged = "length_changed" ,
2944 StateChanged = "state_changed" ,
3045}
3146
3247interface EventMap {
48+ [ VoiceBroadcastPlaybackEvent . LengthChanged ] : ( length : number ) => void ;
3349 [ VoiceBroadcastPlaybackEvent . StateChanged ] : ( state : VoiceBroadcastPlaybackState ) => void ;
3450}
3551
3652export class VoiceBroadcastPlayback
3753 extends TypedEventEmitter < VoiceBroadcastPlaybackEvent , EventMap >
3854 implements IDestroyable {
3955 private state = VoiceBroadcastPlaybackState . Stopped ;
56+ private chunkEvents = new Map < string , MatrixEvent > ( ) ;
57+ /** Holds the playback qeue with a 1-based index (sequence number) */
58+ private queue : Playback [ ] = [ ] ;
59+ private currentlyPlaying : Playback ;
60+ private relations : Relations ;
4061
4162 public constructor (
4263 public readonly infoEvent : MatrixEvent ,
64+ private client : MatrixClient ,
4365 ) {
4466 super ( ) ;
67+ this . setUpRelations ( ) ;
4568 }
4669
47- public start ( ) {
70+ private addChunkEvent ( event : MatrixEvent ) : boolean {
71+ const eventId = event . getId ( ) ;
72+
73+ if ( ! eventId
74+ || eventId . startsWith ( "~!" ) // don't add local events
75+ || event . getContent ( ) ?. msgtype !== MsgType . Audio // don't add non-audio event
76+ || this . chunkEvents . has ( eventId ) ) {
77+ return false ;
78+ }
79+
80+ this . chunkEvents . set ( eventId , event ) ;
81+ return true ;
82+ }
83+
84+ private setUpRelations ( ) : void {
85+ const relations = getReferenceRelationsForEvent ( this . infoEvent , EventType . RoomMessage , this . client ) ;
86+
87+ if ( ! relations ) {
88+ // No related events, yet. Set up relation watcher.
89+ this . infoEvent . on ( MatrixEventEvent . RelationsCreated , this . onRelationsCreated ) ;
90+ return ;
91+ }
92+
93+ this . relations = relations ;
94+ relations . getRelations ( ) ?. forEach ( e => this . addChunkEvent ( e ) ) ;
95+ relations . on ( RelationsEvent . Add , this . onRelationsEventAdd ) ;
96+
97+ if ( this . chunkEvents . size > 0 ) {
98+ this . emitLengthChanged ( ) ;
99+ }
100+ }
101+
102+ private onRelationsEventAdd = ( event : MatrixEvent ) => {
103+ if ( this . addChunkEvent ( event ) ) {
104+ this . emitLengthChanged ( ) ;
105+ }
106+ } ;
107+
108+ private emitLengthChanged ( ) : void {
109+ this . emit ( VoiceBroadcastPlaybackEvent . LengthChanged , this . chunkEvents . size ) ;
110+ }
111+
112+ private onRelationsCreated = ( relationType : string ) => {
113+ if ( relationType !== RelationType . Reference ) {
114+ return ;
115+ }
116+
117+ this . infoEvent . off ( MatrixEventEvent . RelationsCreated , this . onRelationsCreated ) ;
118+ this . setUpRelations ( ) ;
119+ } ;
120+
121+ private async loadChunks ( ) : Promise < void > {
122+ const relations = getReferenceRelationsForEvent ( this . infoEvent , EventType . RoomMessage , this . client ) ;
123+ const chunkEvents = relations ?. getRelations ( ) ;
124+
125+ if ( ! chunkEvents ) {
126+ return ;
127+ }
128+
129+ for ( const chunkEvent of chunkEvents ) {
130+ await this . enqueueChunk ( chunkEvent ) ;
131+ }
132+ }
133+
134+ private async enqueueChunk ( chunkEvent : MatrixEvent ) {
135+ const sequenceNumber = parseInt ( chunkEvent . getContent ( ) ?. [ VoiceBroadcastChunkEventType ] ?. sequence , 10 ) ;
136+ if ( isNaN ( sequenceNumber ) ) return ;
137+
138+ const helper = new MediaEventHelper ( chunkEvent ) ;
139+ const blob = await helper . sourceBlob . value ;
140+ const buffer = await blob . arrayBuffer ( ) ;
141+ const playback = PlaybackManager . instance . createPlaybackInstance ( buffer ) ;
142+ await playback . prepare ( ) ;
143+ playback . clockInfo . populatePlaceholdersFrom ( chunkEvent ) ;
144+ this . queue [ sequenceNumber ] = playback ;
145+ playback . on ( UPDATE_EVENT , ( state ) => this . onPlaybackStateChange ( playback , state ) ) ;
146+ }
147+
148+ private onPlaybackStateChange ( playback : Playback , newState : PlaybackState ) {
149+ if ( newState !== PlaybackState . Stopped ) {
150+ return ;
151+ }
152+
153+ const next = this . queue [ this . queue . indexOf ( playback ) + 1 ] ;
154+
155+ if ( next ) {
156+ this . currentlyPlaying = next ;
157+ next . play ( ) ;
158+ return ;
159+ }
160+
161+ this . setState ( VoiceBroadcastPlaybackState . Stopped ) ;
162+ }
163+
164+ public async start ( ) : Promise < void > {
165+ if ( this . queue . length === 0 ) {
166+ await this . loadChunks ( ) ;
167+ }
168+
169+ if ( this . queue . length === 0 || ! this . queue [ 1 ] ) {
170+ // set to stopped fi the queue is empty of the first chunk (sequence number: 1-based index) is missing
171+ this . setState ( VoiceBroadcastPlaybackState . Stopped ) ;
172+ return ;
173+ }
174+
48175 this . setState ( VoiceBroadcastPlaybackState . Playing ) ;
176+ // index of the first schunk is the first sequence number
177+ const first = this . queue [ 1 ] ;
178+ this . currentlyPlaying = first ;
179+ await first . play ( ) ;
180+ }
181+
182+ public get length ( ) : number {
183+ return this . chunkEvents . size ;
49184 }
50185
51- public stop ( ) {
186+ public stop ( ) : void {
52187 this . setState ( VoiceBroadcastPlaybackState . Stopped ) ;
188+
189+ if ( this . currentlyPlaying ) {
190+ this . currentlyPlaying . stop ( ) ;
191+ }
53192 }
54193
55- public toggle ( ) {
194+ public pause ( ) : void {
195+ if ( ! this . currentlyPlaying ) return ;
196+
197+ this . setState ( VoiceBroadcastPlaybackState . Paused ) ;
198+ this . currentlyPlaying . pause ( ) ;
199+ }
200+
201+ public resume ( ) : void {
202+ if ( ! this . currentlyPlaying ) return ;
203+
204+ this . setState ( VoiceBroadcastPlaybackState . Playing ) ;
205+ this . currentlyPlaying . play ( ) ;
206+ }
207+
208+ /**
209+ * Toggles the playback:
210+ * stopped → playing
211+ * playing → paused
212+ * paused → playing
213+ */
214+ public async toggle ( ) {
56215 if ( this . state === VoiceBroadcastPlaybackState . Stopped ) {
57- this . setState ( VoiceBroadcastPlaybackState . Playing ) ;
216+ await this . start ( ) ;
58217 return ;
59218 }
60219
61- this . setState ( VoiceBroadcastPlaybackState . Stopped ) ;
220+ if ( this . state === VoiceBroadcastPlaybackState . Paused ) {
221+ this . resume ( ) ;
222+ return ;
223+ }
224+
225+ this . pause ( ) ;
62226 }
63227
64228 public getState ( ) : VoiceBroadcastPlaybackState {
65229 return this . state ;
66230 }
67231
68232 private setState ( state : VoiceBroadcastPlaybackState ) : void {
233+ if ( this . state === state ) {
234+ return ;
235+ }
236+
69237 this . state = state ;
70238 this . emit ( VoiceBroadcastPlaybackEvent . StateChanged , state ) ;
71239 }
72240
73- destroy ( ) : void {
241+ private destroyQueue ( ) : void {
242+ this . queue . forEach ( p => p . destroy ( ) ) ;
243+ this . queue = [ ] ;
244+ }
245+
246+ public destroy ( ) : void {
247+ if ( this . relations ) {
248+ this . relations . off ( RelationsEvent . Add , this . onRelationsEventAdd ) ;
249+ }
250+
251+ this . infoEvent . off ( MatrixEventEvent . RelationsCreated , this . onRelationsCreated ) ;
74252 this . removeAllListeners ( ) ;
253+ this . destroyQueue ( ) ;
75254 }
76255}
0 commit comments