@@ -14,10 +14,24 @@ See the License for the specific language governing permissions and
14
14
limitations under the License.
15
15
*/
16
16
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" ;
18
26
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter" ;
19
27
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" ;
20
33
import { IDestroyable } from "../../utils/IDestroyable" ;
34
+ import { VoiceBroadcastChunkEventType } from ".." ;
21
35
22
36
export enum VoiceBroadcastPlaybackState {
23
37
Paused ,
@@ -26,51 +40,216 @@ export enum VoiceBroadcastPlaybackState {
26
40
}
27
41
28
42
export enum VoiceBroadcastPlaybackEvent {
43
+ LengthChanged = "length_changed" ,
29
44
StateChanged = "state_changed" ,
30
45
}
31
46
32
47
interface EventMap {
48
+ [ VoiceBroadcastPlaybackEvent . LengthChanged ] : ( length : number ) => void ;
33
49
[ VoiceBroadcastPlaybackEvent . StateChanged ] : ( state : VoiceBroadcastPlaybackState ) => void ;
34
50
}
35
51
36
52
export class VoiceBroadcastPlayback
37
53
extends TypedEventEmitter < VoiceBroadcastPlaybackEvent , EventMap >
38
54
implements IDestroyable {
39
55
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 ;
40
61
41
62
public constructor (
42
63
public readonly infoEvent : MatrixEvent ,
64
+ private client : MatrixClient ,
43
65
) {
44
66
super ( ) ;
67
+ this . setUpRelations ( ) ;
45
68
}
46
69
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
+
48
175
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 ;
49
184
}
50
185
51
- public stop ( ) {
186
+ public stop ( ) : void {
52
187
this . setState ( VoiceBroadcastPlaybackState . Stopped ) ;
188
+
189
+ if ( this . currentlyPlaying ) {
190
+ this . currentlyPlaying . stop ( ) ;
191
+ }
53
192
}
54
193
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 ( ) {
56
215
if ( this . state === VoiceBroadcastPlaybackState . Stopped ) {
57
- this . setState ( VoiceBroadcastPlaybackState . Playing ) ;
216
+ await this . start ( ) ;
58
217
return ;
59
218
}
60
219
61
- this . setState ( VoiceBroadcastPlaybackState . Stopped ) ;
220
+ if ( this . state === VoiceBroadcastPlaybackState . Paused ) {
221
+ this . resume ( ) ;
222
+ return ;
223
+ }
224
+
225
+ this . pause ( ) ;
62
226
}
63
227
64
228
public getState ( ) : VoiceBroadcastPlaybackState {
65
229
return this . state ;
66
230
}
67
231
68
232
private setState ( state : VoiceBroadcastPlaybackState ) : void {
233
+ if ( this . state === state ) {
234
+ return ;
235
+ }
236
+
69
237
this . state = state ;
70
238
this . emit ( VoiceBroadcastPlaybackEvent . StateChanged , state ) ;
71
239
}
72
240
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 ) ;
74
252
this . removeAllListeners ( ) ;
253
+ this . destroyQueue ( ) ;
75
254
}
76
255
}
0 commit comments