@microsoft/live-share-media 1.2.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nycrc.json +10 -10
- package/LICENSE +90 -90
- package/README.md +135 -135
- package/bin/LiveMediaSession.d.ts.map +1 -1
- package/bin/LiveMediaSession.js +4 -6
- package/bin/LiveMediaSession.js.map +1 -1
- package/bin/LiveMediaSessionCoordinator.d.ts +4 -3
- package/bin/LiveMediaSessionCoordinator.d.ts.map +1 -1
- package/bin/LiveMediaSessionCoordinator.js +56 -69
- package/bin/LiveMediaSessionCoordinator.js.map +1 -1
- package/bin/MediaPlayerSynchronizer.d.ts +12 -0
- package/bin/MediaPlayerSynchronizer.d.ts.map +1 -1
- package/bin/MediaPlayerSynchronizer.js +26 -6
- package/bin/MediaPlayerSynchronizer.js.map +1 -1
- package/bin/MediaSessionExtensions.d.ts +0 -4
- package/bin/MediaSessionExtensions.d.ts.map +1 -1
- package/bin/MediaSessionExtensions.js.map +1 -1
- package/bin/internals/GroupCoordinatorState.d.ts.map +1 -1
- package/bin/internals/GroupCoordinatorState.js +1 -1
- package/bin/internals/GroupCoordinatorState.js.map +1 -1
- package/bin/internals/consts.d.ts +2 -2
- package/bin/internals/consts.d.ts.map +1 -1
- package/bin/internals/consts.js +2 -2
- package/bin/internals/consts.js.map +1 -1
- package/bin/internals/errors.d.ts +17 -0
- package/bin/internals/errors.d.ts.map +1 -0
- package/bin/internals/errors.js +34 -0
- package/bin/internals/errors.js.map +1 -0
- package/bin/internals/index.d.ts +1 -0
- package/bin/internals/index.d.ts.map +1 -1
- package/bin/internals/index.js +1 -0
- package/bin/internals/index.js.map +1 -1
- package/package.json +45 -45
- package/src/IMediaPlayer.ts +29 -29
- package/src/LiveMediaSession.ts +337 -334
- package/src/LiveMediaSessionCoordinator.ts +711 -671
- package/src/LiveMediaSessionCoordinatorSuspension.ts +39 -39
- package/src/MediaPlayerSynchronizer.ts +714 -690
- package/src/MediaSessionActionThrottler.ts +19 -19
- package/src/MediaSessionExtensions.ts +182 -187
- package/src/RepeatedActionThrottler.ts +76 -76
- package/src/VolumeManager.ts +200 -200
- package/src/index.ts +14 -14
- package/src/internals/GroupCoordinatorState.ts +685 -683
- package/src/internals/GroupPlaybackPosition.ts +228 -228
- package/src/internals/GroupPlaybackTrack.ts +182 -182
- package/src/internals/GroupPlaybackTrackData.ts +104 -104
- package/src/internals/GroupTransportState.ts +170 -170
- package/src/internals/consts.ts +103 -102
- package/src/internals/errors.ts +42 -0
- package/src/internals/index.ts +12 -11
- package/tsconfig.build.json +8 -8
- package/tsconfig.json +76 -76
- package/typedoc.json +10 -10
|
@@ -1,683 +1,685 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
-
* Licensed under the Microsoft Live Share SDK License.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
ILiveEvent,
|
|
8
|
-
TimeInterval,
|
|
9
|
-
IRuntimeSignaler,
|
|
10
|
-
LiveTelemetryLogger,
|
|
11
|
-
LiveShareRuntime,
|
|
12
|
-
IEvent,
|
|
13
|
-
} from "@microsoft/live-share";
|
|
14
|
-
import EventEmitter from "events";
|
|
15
|
-
import {
|
|
16
|
-
ExtendedMediaMetadata,
|
|
17
|
-
CoordinationWaitPoint,
|
|
18
|
-
ExtendedMediaSessionPlaybackState,
|
|
19
|
-
ExtendedMediaSessionActionDetails,
|
|
20
|
-
ExtendedMediaSessionActionSource,
|
|
21
|
-
} from "../MediaSessionExtensions";
|
|
22
|
-
import {
|
|
23
|
-
GroupPlaybackTrack,
|
|
24
|
-
GroupPlaybackTrackEvents,
|
|
25
|
-
IPlaybackTrack,
|
|
26
|
-
} from "./GroupPlaybackTrack";
|
|
27
|
-
import { GroupTransportState, ITransportState } from "./GroupTransportState";
|
|
28
|
-
import {
|
|
29
|
-
GroupPlaybackPosition,
|
|
30
|
-
ICurrentPlaybackPosition,
|
|
31
|
-
} from "./GroupPlaybackPosition";
|
|
32
|
-
import { IMediaPlayerState } from "../LiveMediaSessionCoordinator";
|
|
33
|
-
import { GroupTransportStateEvents } from "./GroupTransportState";
|
|
34
|
-
import {
|
|
35
|
-
GroupPlaybackTrackData,
|
|
36
|
-
PlaybackTrackDataEvents,
|
|
37
|
-
IPlaybackTrackData,
|
|
38
|
-
} from "./GroupPlaybackTrackData";
|
|
39
|
-
import { TelemetryEvents } from "./consts";
|
|
40
|
-
import { waitUntilConnected } from "@microsoft/live-share/bin/internals";
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @hidden
|
|
44
|
-
*/
|
|
45
|
-
export interface IPositionUpdateEvent {
|
|
46
|
-
track: IPlaybackTrack;
|
|
47
|
-
trackData: IPlaybackTrackData;
|
|
48
|
-
transport: ITransportState;
|
|
49
|
-
playbackState: ExtendedMediaSessionPlaybackState;
|
|
50
|
-
position: number;
|
|
51
|
-
waitPoint?: CoordinationWaitPoint;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* @hidden
|
|
56
|
-
*/
|
|
57
|
-
export interface ITransportCommandEvent {
|
|
58
|
-
track: IPlaybackTrack;
|
|
59
|
-
position: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* @hidden
|
|
64
|
-
*/
|
|
65
|
-
export interface ISetTrackEvent {
|
|
66
|
-
metadata: ExtendedMediaMetadata | null;
|
|
67
|
-
waitPoints: CoordinationWaitPoint[];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* @hidden
|
|
72
|
-
*/
|
|
73
|
-
export interface ISetTrackDataEvent {
|
|
74
|
-
data: object | null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @hidden
|
|
79
|
-
*/
|
|
80
|
-
export interface ITriggerActionEvent extends IEvent {
|
|
81
|
-
details: ExtendedMediaSessionActionDetails;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* @hidden
|
|
86
|
-
*/
|
|
87
|
-
export enum GroupCoordinatorStateEvents {
|
|
88
|
-
newwaitpoint = "newwaitpoint",
|
|
89
|
-
triggeraction = "triggeraction",
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* @hidden
|
|
94
|
-
*/
|
|
95
|
-
export class GroupCoordinatorState extends EventEmitter {
|
|
96
|
-
private readonly _runtime: IRuntimeSignaler;
|
|
97
|
-
private readonly _liveRuntime: LiveShareRuntime;
|
|
98
|
-
private readonly _logger: LiveTelemetryLogger;
|
|
99
|
-
private readonly _maxPlaybackDrift: TimeInterval;
|
|
100
|
-
private _getMediaPlayerState: () => IMediaPlayerState;
|
|
101
|
-
|
|
102
|
-
// Shared group state
|
|
103
|
-
private _playbackTrack: GroupPlaybackTrack;
|
|
104
|
-
private _playbackTrackData: GroupPlaybackTrackData;
|
|
105
|
-
private _transportState: GroupTransportState;
|
|
106
|
-
private _playbackPosition: GroupPlaybackPosition;
|
|
107
|
-
|
|
108
|
-
// Suspension tracking
|
|
109
|
-
private _suspensionCnt: number = 0;
|
|
110
|
-
private _waitPoint?: CoordinationWaitPoint;
|
|
111
|
-
|
|
112
|
-
// "Soft Suspension" tracking
|
|
113
|
-
// - Soft suspensions temporarily disconnect the local media session when transitioning between
|
|
114
|
-
// paused, playing, and none states. This is to give the local client time to finish any inprogress
|
|
115
|
-
// operations like sending a transport command or ending a suspension.
|
|
116
|
-
private _lastStateChange: ExtendedMediaSessionPlaybackState = "none";
|
|
117
|
-
private _lastStateChangeTime: number = 0;
|
|
118
|
-
|
|
119
|
-
constructor(
|
|
120
|
-
runtime: IRuntimeSignaler,
|
|
121
|
-
liveRuntime: LiveShareRuntime,
|
|
122
|
-
maxPlaybackDrift: TimeInterval,
|
|
123
|
-
positionUpdateInterval: TimeInterval,
|
|
124
|
-
getMediaPlayerState: () => IMediaPlayerState
|
|
125
|
-
) {
|
|
126
|
-
super();
|
|
127
|
-
this._runtime = runtime;
|
|
128
|
-
this._liveRuntime = liveRuntime;
|
|
129
|
-
this._logger = new LiveTelemetryLogger(runtime, liveRuntime);
|
|
130
|
-
this._maxPlaybackDrift = maxPlaybackDrift;
|
|
131
|
-
this._playbackTrack = new GroupPlaybackTrack(getMediaPlayerState);
|
|
132
|
-
this._playbackTrackData = new GroupPlaybackTrackData(
|
|
133
|
-
this._playbackTrack
|
|
134
|
-
);
|
|
135
|
-
this._transportState = new GroupTransportState(
|
|
136
|
-
this._playbackTrack,
|
|
137
|
-
getMediaPlayerState,
|
|
138
|
-
this._liveRuntime
|
|
139
|
-
);
|
|
140
|
-
this._playbackPosition = new GroupPlaybackPosition(
|
|
141
|
-
this._transportState,
|
|
142
|
-
this._runtime,
|
|
143
|
-
this._liveRuntime,
|
|
144
|
-
positionUpdateInterval
|
|
145
|
-
);
|
|
146
|
-
this._getMediaPlayerState = getMediaPlayerState;
|
|
147
|
-
|
|
148
|
-
// Listen track related events
|
|
149
|
-
this._playbackTrack.on(GroupPlaybackTrackEvents.trackChange, (evt) => {
|
|
150
|
-
if (!this.isSuspended) {
|
|
151
|
-
this._logger.sendTelemetryEvent(
|
|
152
|
-
TelemetryEvents.GroupCoordinator.TrackChanged
|
|
153
|
-
);
|
|
154
|
-
this.emitSetTrack(evt.clientId, evt.metadata!, evt.source);
|
|
155
|
-
} else {
|
|
156
|
-
this._logger.sendTelemetryEvent(
|
|
157
|
-
TelemetryEvents.GroupCoordinator.TrackChangeDelayed
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// Listen for track data changes
|
|
163
|
-
this._playbackTrackData.on(
|
|
164
|
-
PlaybackTrackDataEvents.dataChange,
|
|
165
|
-
async (evt) => {
|
|
166
|
-
if (!this.isSuspended && !this.isWaiting) {
|
|
167
|
-
this._logger.sendTelemetryEvent(
|
|
168
|
-
TelemetryEvents.GroupCoordinator.TrackDataChanged
|
|
169
|
-
);
|
|
170
|
-
const clientId = await waitUntilConnected(this._runtime);
|
|
171
|
-
this.emitTriggerAction({
|
|
172
|
-
action: "datachange",
|
|
173
|
-
source: evt.source,
|
|
174
|
-
data: evt.data,
|
|
175
|
-
clientId: evt.clientId,
|
|
176
|
-
local: evt.clientId === clientId,
|
|
177
|
-
});
|
|
178
|
-
} else {
|
|
179
|
-
this._logger.sendTelemetryEvent(
|
|
180
|
-
TelemetryEvents.GroupCoordinator.TrackDataChangeDelayed
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// Listen to transport related events
|
|
187
|
-
this._transportState.on(
|
|
188
|
-
GroupTransportStateEvents.transportStateChange,
|
|
189
|
-
async (evt) => {
|
|
190
|
-
if (!this.isSuspended && !this.isWaiting) {
|
|
191
|
-
this._logger.sendTelemetryEvent(
|
|
192
|
-
TelemetryEvents.GroupCoordinator.TransportStateChanged,
|
|
193
|
-
null,
|
|
194
|
-
{
|
|
195
|
-
action: evt.action,
|
|
196
|
-
seekTime: evt.seekTime,
|
|
197
|
-
}
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
const localClientId = await waitUntilConnected(
|
|
201
|
-
this._runtime
|
|
202
|
-
);
|
|
203
|
-
const local = evt.clientId === localClientId;
|
|
204
|
-
// Trigger action
|
|
205
|
-
switch (evt.action) {
|
|
206
|
-
case "play":
|
|
207
|
-
this.emitTriggerAction({
|
|
208
|
-
action: "play",
|
|
209
|
-
source: evt.source,
|
|
210
|
-
clientId: evt.clientId,
|
|
211
|
-
local,
|
|
212
|
-
seekTime: evt.seekTime,
|
|
213
|
-
});
|
|
214
|
-
break;
|
|
215
|
-
|
|
216
|
-
case "pause":
|
|
217
|
-
this.emitTriggerAction({
|
|
218
|
-
action: "pause",
|
|
219
|
-
source: evt.source,
|
|
220
|
-
clientId: evt.clientId,
|
|
221
|
-
local,
|
|
222
|
-
seekTime: evt.seekTime,
|
|
223
|
-
});
|
|
224
|
-
break;
|
|
225
|
-
|
|
226
|
-
case "seekto":
|
|
227
|
-
this.emitTriggerAction({
|
|
228
|
-
action: "seekto",
|
|
229
|
-
source: evt.source,
|
|
230
|
-
clientId: evt.clientId,
|
|
231
|
-
local,
|
|
232
|
-
seekTime: evt.seekTime,
|
|
233
|
-
});
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
this._logger.sendTelemetryEvent(
|
|
238
|
-
TelemetryEvents.GroupCoordinator
|
|
239
|
-
.TransportStateChangeDelayed,
|
|
240
|
-
null,
|
|
241
|
-
{
|
|
242
|
-
action: evt.action,
|
|
243
|
-
seekTime: evt.seekTime,
|
|
244
|
-
}
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
public get playbackTrack(): GroupPlaybackTrack {
|
|
252
|
-
return this._playbackTrack;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
public get playbackTrackData(): GroupPlaybackTrackData {
|
|
256
|
-
return this._playbackTrackData;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
public get transportState(): GroupTransportState {
|
|
260
|
-
return this._transportState;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
public get playbackPosition(): GroupPlaybackPosition {
|
|
264
|
-
return this._playbackPosition;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
public get waitingAt(): CoordinationWaitPoint | undefined {
|
|
268
|
-
return this._waitPoint;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
public get isSuspended(): boolean {
|
|
272
|
-
return this._suspensionCnt > 0;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
public get isWaiting(): boolean {
|
|
276
|
-
return !!this._waitPoint;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
public beginSuspension(waitPoint?: CoordinationWaitPoint): void {
|
|
280
|
-
this._suspensionCnt++;
|
|
281
|
-
|
|
282
|
-
if (waitPoint) {
|
|
283
|
-
// Save most recent wait point
|
|
284
|
-
this._waitPoint = waitPoint;
|
|
285
|
-
|
|
286
|
-
// Add to track state if dynamic wait point
|
|
287
|
-
this.playbackTrack.addWaitPoint(waitPoint);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
public endSuspension(syncState: boolean): void {
|
|
292
|
-
if (this._suspensionCnt == 0) {
|
|
293
|
-
throw new Error(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
//
|
|
472
|
-
if
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
case "
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
this.
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
this.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
// -
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
this.
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the Microsoft Live Share SDK License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ILiveEvent,
|
|
8
|
+
TimeInterval,
|
|
9
|
+
IRuntimeSignaler,
|
|
10
|
+
LiveTelemetryLogger,
|
|
11
|
+
LiveShareRuntime,
|
|
12
|
+
IEvent,
|
|
13
|
+
} from "@microsoft/live-share";
|
|
14
|
+
import EventEmitter from "events";
|
|
15
|
+
import {
|
|
16
|
+
ExtendedMediaMetadata,
|
|
17
|
+
CoordinationWaitPoint,
|
|
18
|
+
ExtendedMediaSessionPlaybackState,
|
|
19
|
+
ExtendedMediaSessionActionDetails,
|
|
20
|
+
ExtendedMediaSessionActionSource,
|
|
21
|
+
} from "../MediaSessionExtensions";
|
|
22
|
+
import {
|
|
23
|
+
GroupPlaybackTrack,
|
|
24
|
+
GroupPlaybackTrackEvents,
|
|
25
|
+
IPlaybackTrack,
|
|
26
|
+
} from "./GroupPlaybackTrack";
|
|
27
|
+
import { GroupTransportState, ITransportState } from "./GroupTransportState";
|
|
28
|
+
import {
|
|
29
|
+
GroupPlaybackPosition,
|
|
30
|
+
ICurrentPlaybackPosition,
|
|
31
|
+
} from "./GroupPlaybackPosition";
|
|
32
|
+
import { IMediaPlayerState } from "../LiveMediaSessionCoordinator";
|
|
33
|
+
import { GroupTransportStateEvents } from "./GroupTransportState";
|
|
34
|
+
import {
|
|
35
|
+
GroupPlaybackTrackData,
|
|
36
|
+
PlaybackTrackDataEvents,
|
|
37
|
+
IPlaybackTrackData,
|
|
38
|
+
} from "./GroupPlaybackTrackData";
|
|
39
|
+
import { TelemetryEvents } from "./consts";
|
|
40
|
+
import { waitUntilConnected } from "@microsoft/live-share/bin/internals";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @hidden
|
|
44
|
+
*/
|
|
45
|
+
export interface IPositionUpdateEvent {
|
|
46
|
+
track: IPlaybackTrack;
|
|
47
|
+
trackData: IPlaybackTrackData;
|
|
48
|
+
transport: ITransportState;
|
|
49
|
+
playbackState: ExtendedMediaSessionPlaybackState;
|
|
50
|
+
position: number;
|
|
51
|
+
waitPoint?: CoordinationWaitPoint;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @hidden
|
|
56
|
+
*/
|
|
57
|
+
export interface ITransportCommandEvent {
|
|
58
|
+
track: IPlaybackTrack;
|
|
59
|
+
position: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @hidden
|
|
64
|
+
*/
|
|
65
|
+
export interface ISetTrackEvent {
|
|
66
|
+
metadata: ExtendedMediaMetadata | null;
|
|
67
|
+
waitPoints: CoordinationWaitPoint[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @hidden
|
|
72
|
+
*/
|
|
73
|
+
export interface ISetTrackDataEvent {
|
|
74
|
+
data: object | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @hidden
|
|
79
|
+
*/
|
|
80
|
+
export interface ITriggerActionEvent extends IEvent {
|
|
81
|
+
details: ExtendedMediaSessionActionDetails;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @hidden
|
|
86
|
+
*/
|
|
87
|
+
export enum GroupCoordinatorStateEvents {
|
|
88
|
+
newwaitpoint = "newwaitpoint",
|
|
89
|
+
triggeraction = "triggeraction",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @hidden
|
|
94
|
+
*/
|
|
95
|
+
export class GroupCoordinatorState extends EventEmitter {
|
|
96
|
+
private readonly _runtime: IRuntimeSignaler;
|
|
97
|
+
private readonly _liveRuntime: LiveShareRuntime;
|
|
98
|
+
private readonly _logger: LiveTelemetryLogger;
|
|
99
|
+
private readonly _maxPlaybackDrift: TimeInterval;
|
|
100
|
+
private _getMediaPlayerState: () => IMediaPlayerState;
|
|
101
|
+
|
|
102
|
+
// Shared group state
|
|
103
|
+
private _playbackTrack: GroupPlaybackTrack;
|
|
104
|
+
private _playbackTrackData: GroupPlaybackTrackData;
|
|
105
|
+
private _transportState: GroupTransportState;
|
|
106
|
+
private _playbackPosition: GroupPlaybackPosition;
|
|
107
|
+
|
|
108
|
+
// Suspension tracking
|
|
109
|
+
private _suspensionCnt: number = 0;
|
|
110
|
+
private _waitPoint?: CoordinationWaitPoint;
|
|
111
|
+
|
|
112
|
+
// "Soft Suspension" tracking
|
|
113
|
+
// - Soft suspensions temporarily disconnect the local media session when transitioning between
|
|
114
|
+
// paused, playing, and none states. This is to give the local client time to finish any inprogress
|
|
115
|
+
// operations like sending a transport command or ending a suspension.
|
|
116
|
+
private _lastStateChange: ExtendedMediaSessionPlaybackState = "none";
|
|
117
|
+
private _lastStateChangeTime: number = 0;
|
|
118
|
+
|
|
119
|
+
constructor(
|
|
120
|
+
runtime: IRuntimeSignaler,
|
|
121
|
+
liveRuntime: LiveShareRuntime,
|
|
122
|
+
maxPlaybackDrift: TimeInterval,
|
|
123
|
+
positionUpdateInterval: TimeInterval,
|
|
124
|
+
getMediaPlayerState: () => IMediaPlayerState
|
|
125
|
+
) {
|
|
126
|
+
super();
|
|
127
|
+
this._runtime = runtime;
|
|
128
|
+
this._liveRuntime = liveRuntime;
|
|
129
|
+
this._logger = new LiveTelemetryLogger(runtime, liveRuntime);
|
|
130
|
+
this._maxPlaybackDrift = maxPlaybackDrift;
|
|
131
|
+
this._playbackTrack = new GroupPlaybackTrack(getMediaPlayerState);
|
|
132
|
+
this._playbackTrackData = new GroupPlaybackTrackData(
|
|
133
|
+
this._playbackTrack
|
|
134
|
+
);
|
|
135
|
+
this._transportState = new GroupTransportState(
|
|
136
|
+
this._playbackTrack,
|
|
137
|
+
getMediaPlayerState,
|
|
138
|
+
this._liveRuntime
|
|
139
|
+
);
|
|
140
|
+
this._playbackPosition = new GroupPlaybackPosition(
|
|
141
|
+
this._transportState,
|
|
142
|
+
this._runtime,
|
|
143
|
+
this._liveRuntime,
|
|
144
|
+
positionUpdateInterval
|
|
145
|
+
);
|
|
146
|
+
this._getMediaPlayerState = getMediaPlayerState;
|
|
147
|
+
|
|
148
|
+
// Listen track related events
|
|
149
|
+
this._playbackTrack.on(GroupPlaybackTrackEvents.trackChange, (evt) => {
|
|
150
|
+
if (!this.isSuspended) {
|
|
151
|
+
this._logger.sendTelemetryEvent(
|
|
152
|
+
TelemetryEvents.GroupCoordinator.TrackChanged
|
|
153
|
+
);
|
|
154
|
+
this.emitSetTrack(evt.clientId, evt.metadata!, evt.source);
|
|
155
|
+
} else {
|
|
156
|
+
this._logger.sendTelemetryEvent(
|
|
157
|
+
TelemetryEvents.GroupCoordinator.TrackChangeDelayed
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Listen for track data changes
|
|
163
|
+
this._playbackTrackData.on(
|
|
164
|
+
PlaybackTrackDataEvents.dataChange,
|
|
165
|
+
async (evt) => {
|
|
166
|
+
if (!this.isSuspended && !this.isWaiting) {
|
|
167
|
+
this._logger.sendTelemetryEvent(
|
|
168
|
+
TelemetryEvents.GroupCoordinator.TrackDataChanged
|
|
169
|
+
);
|
|
170
|
+
const clientId = await waitUntilConnected(this._runtime);
|
|
171
|
+
this.emitTriggerAction({
|
|
172
|
+
action: "datachange",
|
|
173
|
+
source: evt.source,
|
|
174
|
+
data: evt.data,
|
|
175
|
+
clientId: evt.clientId,
|
|
176
|
+
local: evt.clientId === clientId,
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
this._logger.sendTelemetryEvent(
|
|
180
|
+
TelemetryEvents.GroupCoordinator.TrackDataChangeDelayed
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Listen to transport related events
|
|
187
|
+
this._transportState.on(
|
|
188
|
+
GroupTransportStateEvents.transportStateChange,
|
|
189
|
+
async (evt) => {
|
|
190
|
+
if (!this.isSuspended && !this.isWaiting) {
|
|
191
|
+
this._logger.sendTelemetryEvent(
|
|
192
|
+
TelemetryEvents.GroupCoordinator.TransportStateChanged,
|
|
193
|
+
null,
|
|
194
|
+
{
|
|
195
|
+
action: evt.action,
|
|
196
|
+
seekTime: evt.seekTime,
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const localClientId = await waitUntilConnected(
|
|
201
|
+
this._runtime
|
|
202
|
+
);
|
|
203
|
+
const local = evt.clientId === localClientId;
|
|
204
|
+
// Trigger action
|
|
205
|
+
switch (evt.action) {
|
|
206
|
+
case "play":
|
|
207
|
+
this.emitTriggerAction({
|
|
208
|
+
action: "play",
|
|
209
|
+
source: evt.source,
|
|
210
|
+
clientId: evt.clientId,
|
|
211
|
+
local,
|
|
212
|
+
seekTime: evt.seekTime,
|
|
213
|
+
});
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case "pause":
|
|
217
|
+
this.emitTriggerAction({
|
|
218
|
+
action: "pause",
|
|
219
|
+
source: evt.source,
|
|
220
|
+
clientId: evt.clientId,
|
|
221
|
+
local,
|
|
222
|
+
seekTime: evt.seekTime,
|
|
223
|
+
});
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
case "seekto":
|
|
227
|
+
this.emitTriggerAction({
|
|
228
|
+
action: "seekto",
|
|
229
|
+
source: evt.source,
|
|
230
|
+
clientId: evt.clientId,
|
|
231
|
+
local,
|
|
232
|
+
seekTime: evt.seekTime,
|
|
233
|
+
});
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
this._logger.sendTelemetryEvent(
|
|
238
|
+
TelemetryEvents.GroupCoordinator
|
|
239
|
+
.TransportStateChangeDelayed,
|
|
240
|
+
null,
|
|
241
|
+
{
|
|
242
|
+
action: evt.action,
|
|
243
|
+
seekTime: evt.seekTime,
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
public get playbackTrack(): GroupPlaybackTrack {
|
|
252
|
+
return this._playbackTrack;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
public get playbackTrackData(): GroupPlaybackTrackData {
|
|
256
|
+
return this._playbackTrackData;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public get transportState(): GroupTransportState {
|
|
260
|
+
return this._transportState;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
public get playbackPosition(): GroupPlaybackPosition {
|
|
264
|
+
return this._playbackPosition;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public get waitingAt(): CoordinationWaitPoint | undefined {
|
|
268
|
+
return this._waitPoint;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
public get isSuspended(): boolean {
|
|
272
|
+
return this._suspensionCnt > 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public get isWaiting(): boolean {
|
|
276
|
+
return !!this._waitPoint;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public beginSuspension(waitPoint?: CoordinationWaitPoint): void {
|
|
280
|
+
this._suspensionCnt++;
|
|
281
|
+
|
|
282
|
+
if (waitPoint) {
|
|
283
|
+
// Save most recent wait point
|
|
284
|
+
this._waitPoint = waitPoint;
|
|
285
|
+
|
|
286
|
+
// Add to track state if dynamic wait point
|
|
287
|
+
this.playbackTrack.addWaitPoint(waitPoint);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public endSuspension(syncState: boolean): void {
|
|
292
|
+
if (this._suspensionCnt == 0) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`GroupCoordinatorState:endSuspension - cannot end suspension when there are no active suspensions. The expected value is > 0 but the actual count is ${this._suspensionCnt}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this._suspensionCnt--;
|
|
299
|
+
if (this._suspensionCnt == 0 && syncState) {
|
|
300
|
+
// Sync media session to group
|
|
301
|
+
this.syncLocalMediaSession();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
public createPositionUpdateEvent(
|
|
306
|
+
state: IMediaPlayerState
|
|
307
|
+
): IPositionUpdateEvent {
|
|
308
|
+
const { positionState, playbackState } = state;
|
|
309
|
+
if (this.isSuspended) {
|
|
310
|
+
// Report suspension state
|
|
311
|
+
return {
|
|
312
|
+
track: this.playbackTrack.current,
|
|
313
|
+
trackData: this.playbackTrackData.current,
|
|
314
|
+
transport: this.transportState.current,
|
|
315
|
+
playbackState: "suspended",
|
|
316
|
+
position: positionState?.position || 0.0,
|
|
317
|
+
waitPoint: this._waitPoint,
|
|
318
|
+
};
|
|
319
|
+
} else {
|
|
320
|
+
// Report current position
|
|
321
|
+
return {
|
|
322
|
+
track: this.playbackTrack.current,
|
|
323
|
+
trackData: this.playbackTrackData.current,
|
|
324
|
+
transport: this.transportState.current,
|
|
325
|
+
playbackState: this._waitPoint ? "waiting" : playbackState,
|
|
326
|
+
position: positionState?.position || 0.0,
|
|
327
|
+
waitPoint: this._waitPoint,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
public handleSetTrack(
|
|
333
|
+
event: ILiveEvent<ISetTrackEvent>,
|
|
334
|
+
local: boolean
|
|
335
|
+
): void {
|
|
336
|
+
// Update shared track
|
|
337
|
+
// - Will trigger 'trackChange' event to update media session
|
|
338
|
+
const updated = this.playbackTrack.updateTrack(
|
|
339
|
+
{
|
|
340
|
+
metadata: event.data.metadata,
|
|
341
|
+
waitPoints: event.data.waitPoints,
|
|
342
|
+
timestamp: event.timestamp,
|
|
343
|
+
clientId: event.clientId || "",
|
|
344
|
+
},
|
|
345
|
+
"user"
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (updated) {
|
|
349
|
+
this._logger.sendTelemetryEvent(
|
|
350
|
+
TelemetryEvents.SessionCoordinator.RemoteSetTrackReceived,
|
|
351
|
+
null,
|
|
352
|
+
{
|
|
353
|
+
correlationId: LiveTelemetryLogger.formatCorrelationId(
|
|
354
|
+
event.clientId,
|
|
355
|
+
event.timestamp
|
|
356
|
+
),
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
public handleSetTrackData(
|
|
363
|
+
event: ILiveEvent<ISetTrackDataEvent>,
|
|
364
|
+
local: boolean
|
|
365
|
+
): void {
|
|
366
|
+
// Update shared track data
|
|
367
|
+
// - Will trigger 'dataChange' event to update media session
|
|
368
|
+
const updated = this.playbackTrackData.updateData(
|
|
369
|
+
{
|
|
370
|
+
data: event.data.data,
|
|
371
|
+
timestamp: event.timestamp,
|
|
372
|
+
clientId: event.clientId || "",
|
|
373
|
+
},
|
|
374
|
+
"user"
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
if (updated) {
|
|
378
|
+
this._logger.sendTelemetryEvent(
|
|
379
|
+
TelemetryEvents.SessionCoordinator.RemoteSetTrackReceived,
|
|
380
|
+
null,
|
|
381
|
+
{
|
|
382
|
+
correlationId: LiveTelemetryLogger.formatCorrelationId(
|
|
383
|
+
event.clientId,
|
|
384
|
+
event.timestamp
|
|
385
|
+
),
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
public handleTransportCommand(
|
|
392
|
+
event: ILiveEvent<ITransportCommandEvent>,
|
|
393
|
+
local: boolean
|
|
394
|
+
): void {
|
|
395
|
+
// Ensure change is for current track
|
|
396
|
+
// - Will trigger a 'trackChange' event if newer track.
|
|
397
|
+
this.playbackTrack.updateTrack(event.data.track, "system");
|
|
398
|
+
if (this.playbackTrack.compare(event.data.track.metadata)) {
|
|
399
|
+
// Update playback state
|
|
400
|
+
let playbackState = this.transportState.playbackState;
|
|
401
|
+
switch (event.name) {
|
|
402
|
+
case "play":
|
|
403
|
+
playbackState = "playing";
|
|
404
|
+
break;
|
|
405
|
+
case "pause":
|
|
406
|
+
playbackState = "paused";
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Try to update playback state
|
|
411
|
+
const newState: ITransportState = {
|
|
412
|
+
playbackState: playbackState,
|
|
413
|
+
startPosition: event.data.position,
|
|
414
|
+
timestamp: event.timestamp,
|
|
415
|
+
clientId: event.clientId || "",
|
|
416
|
+
};
|
|
417
|
+
const updated = this.transportState.updateState(newState, "user");
|
|
418
|
+
|
|
419
|
+
if (updated) {
|
|
420
|
+
const correlationId = LiveTelemetryLogger.formatCorrelationId(
|
|
421
|
+
event.clientId,
|
|
422
|
+
event.timestamp
|
|
423
|
+
);
|
|
424
|
+
switch (event.name) {
|
|
425
|
+
case "play":
|
|
426
|
+
this._logger.sendTelemetryEvent(
|
|
427
|
+
TelemetryEvents.SessionCoordinator
|
|
428
|
+
.RemotePlayReceived,
|
|
429
|
+
null,
|
|
430
|
+
{
|
|
431
|
+
correlationId: correlationId,
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
break;
|
|
435
|
+
case "pause":
|
|
436
|
+
this._logger.sendTelemetryEvent(
|
|
437
|
+
TelemetryEvents.SessionCoordinator
|
|
438
|
+
.RemotePauseReceived,
|
|
439
|
+
null,
|
|
440
|
+
{
|
|
441
|
+
correlationId: correlationId,
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
break;
|
|
445
|
+
case "seekTo":
|
|
446
|
+
this._logger.sendTelemetryEvent(
|
|
447
|
+
TelemetryEvents.SessionCoordinator
|
|
448
|
+
.RemoteSeekToReceived,
|
|
449
|
+
null,
|
|
450
|
+
{
|
|
451
|
+
correlationId: correlationId,
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
public handlePositionUpdate(
|
|
461
|
+
event: ILiveEvent<IPositionUpdateEvent>,
|
|
462
|
+
local: boolean
|
|
463
|
+
): void {
|
|
464
|
+
// Ensure change is for current track
|
|
465
|
+
// - Will trigger a 'trackChange' event if newer track.
|
|
466
|
+
this.playbackTrack.updateTrack(event.data.track, "system");
|
|
467
|
+
if (this.playbackTrack.compare(event.data.track.metadata)) {
|
|
468
|
+
// Ensure we have the latest track data
|
|
469
|
+
this.playbackTrackData.updateData(event.data.trackData, "system");
|
|
470
|
+
|
|
471
|
+
// Update transport state if needed
|
|
472
|
+
// - Ignore transport state changes if the client has ended as this will cause the local
|
|
473
|
+
// player to start playback even though the video may have ended for everyone.
|
|
474
|
+
if (event.data.playbackState != "ended") {
|
|
475
|
+
this.transportState.updateState(event.data.transport, "system");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Ensure change is for current transport state
|
|
479
|
+
if (this.transportState.compare(event.data.transport)) {
|
|
480
|
+
// Update playback position
|
|
481
|
+
const position: ICurrentPlaybackPosition = {
|
|
482
|
+
playbackState: event.data.playbackState,
|
|
483
|
+
waitPoint: event.data.waitPoint,
|
|
484
|
+
position: event.data.position,
|
|
485
|
+
timestamp: event.timestamp,
|
|
486
|
+
clientId: event.clientId || "",
|
|
487
|
+
};
|
|
488
|
+
this.playbackPosition.UpdatePlaybackPosition(position);
|
|
489
|
+
|
|
490
|
+
// Ensure local media session is in sync with group
|
|
491
|
+
if (local && !this.isSuspended) {
|
|
492
|
+
// Begin a "soft suspension" on select state changes
|
|
493
|
+
// - This is needed because the catchup logic can try to sync the client with
|
|
494
|
+
// the rest of the group while the local player is trying to to seek to a
|
|
495
|
+
// new position.
|
|
496
|
+
switch (event.data.playbackState) {
|
|
497
|
+
case "none":
|
|
498
|
+
case "paused":
|
|
499
|
+
case "playing":
|
|
500
|
+
if (
|
|
501
|
+
event.data.playbackState !=
|
|
502
|
+
this._lastStateChange
|
|
503
|
+
) {
|
|
504
|
+
this._logger.sendTelemetryEvent(
|
|
505
|
+
TelemetryEvents.GroupCoordinator
|
|
506
|
+
.BeginSoftSuspension,
|
|
507
|
+
null,
|
|
508
|
+
{ playbackState: event.data.playbackState }
|
|
509
|
+
);
|
|
510
|
+
this._lastStateChange =
|
|
511
|
+
event.data.playbackState;
|
|
512
|
+
this._lastStateChangeTime =
|
|
513
|
+
this._liveRuntime.getTimestamp();
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Are we waiting?
|
|
519
|
+
if (this.isWaiting) {
|
|
520
|
+
// Check for end of waiting
|
|
521
|
+
if (this.playbackPosition.clientsWaiting == 0) {
|
|
522
|
+
this._waitPoint = undefined;
|
|
523
|
+
|
|
524
|
+
// Start syncing again
|
|
525
|
+
this.syncLocalMediaSession();
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
// Sync client with group
|
|
529
|
+
this.syncLocalMediaSession();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
public async syncLocalMediaSession(): Promise<void> {
|
|
537
|
+
// Skip further syncs if we're waiting or in a "soft suspension".
|
|
538
|
+
const softSuspensionDelta =
|
|
539
|
+
this._liveRuntime.getTimestamp() - this._lastStateChangeTime;
|
|
540
|
+
if (!this.isWaiting && softSuspensionDelta >= 1000) {
|
|
541
|
+
let { metadata, trackData, positionState, playbackState } =
|
|
542
|
+
this._getMediaPlayerState();
|
|
543
|
+
this._logger.sendTelemetryEvent(
|
|
544
|
+
TelemetryEvents.GroupCoordinator.CheckingForSyncIssues
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
// Check for track change
|
|
548
|
+
if (!this.playbackTrack.compare(metadata)) {
|
|
549
|
+
this._logger.sendTelemetryEvent(
|
|
550
|
+
TelemetryEvents.GroupCoordinator.TrackOutOfSync
|
|
551
|
+
);
|
|
552
|
+
this.emitSetTrack(
|
|
553
|
+
this.playbackTrack.current.clientId,
|
|
554
|
+
this.playbackTrack.metadata!,
|
|
555
|
+
"system"
|
|
556
|
+
);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Once local playback has ended there's no need to further sync the client. In fact
|
|
561
|
+
// doing so could cause the player to loop.
|
|
562
|
+
// - The playbackPosition.trackEnded check is to catch late joiners and prevents syncing if everyone
|
|
563
|
+
// else has finished the video.
|
|
564
|
+
// - Should another user seek or press play after the video has ended that will cause
|
|
565
|
+
// a transport action to trigger which will take the player out of the ended state.
|
|
566
|
+
if (
|
|
567
|
+
playbackState != "ended" &&
|
|
568
|
+
!this._playbackPosition.trackEnded
|
|
569
|
+
) {
|
|
570
|
+
// Ensure we have a position
|
|
571
|
+
let position = positionState?.position;
|
|
572
|
+
if (position == undefined) {
|
|
573
|
+
position = 0.0;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Check for catchup
|
|
577
|
+
// - Target can return -1 in cases where there is no position tracking data
|
|
578
|
+
const target = this.playbackPosition.targetPosition;
|
|
579
|
+
if (
|
|
580
|
+
target >= 0.0 &&
|
|
581
|
+
Math.abs(target - position) >=
|
|
582
|
+
this._maxPlaybackDrift.seconds
|
|
583
|
+
) {
|
|
584
|
+
this._logger.sendTelemetryEvent(
|
|
585
|
+
TelemetryEvents.GroupCoordinator.PositionOutOfSync,
|
|
586
|
+
null,
|
|
587
|
+
{
|
|
588
|
+
current: position,
|
|
589
|
+
target: target,
|
|
590
|
+
}
|
|
591
|
+
);
|
|
592
|
+
await this.emitTriggerAction({
|
|
593
|
+
action: "catchup",
|
|
594
|
+
source: "system",
|
|
595
|
+
clientId: await waitUntilConnected(this._runtime),
|
|
596
|
+
local: true,
|
|
597
|
+
seekTime: target,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const localClientId = await waitUntilConnected(this._runtime);
|
|
602
|
+
// Sync transport state
|
|
603
|
+
if (this.transportState.playbackState != playbackState) {
|
|
604
|
+
this._logger.sendTelemetryEvent(
|
|
605
|
+
TelemetryEvents.GroupCoordinator.TransportOutOfSync,
|
|
606
|
+
null,
|
|
607
|
+
{
|
|
608
|
+
current: playbackState,
|
|
609
|
+
target: this.transportState.playbackState,
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
const local =
|
|
613
|
+
localClientId === this.transportState.current.clientId;
|
|
614
|
+
switch (this.transportState.playbackState) {
|
|
615
|
+
case "playing":
|
|
616
|
+
await this.emitTriggerAction({
|
|
617
|
+
action: "play",
|
|
618
|
+
source: "system",
|
|
619
|
+
clientId: this.transportState.current.clientId,
|
|
620
|
+
local,
|
|
621
|
+
});
|
|
622
|
+
break;
|
|
623
|
+
case "paused":
|
|
624
|
+
await this.emitTriggerAction({
|
|
625
|
+
action: "pause",
|
|
626
|
+
source: "system",
|
|
627
|
+
clientId: this.transportState.current.clientId,
|
|
628
|
+
local,
|
|
629
|
+
});
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Sync track data
|
|
635
|
+
if (
|
|
636
|
+
JSON.stringify(this.playbackTrackData.data) !=
|
|
637
|
+
JSON.stringify(trackData)
|
|
638
|
+
) {
|
|
639
|
+
this._logger.sendTelemetryEvent(
|
|
640
|
+
TelemetryEvents.GroupCoordinator.TrackDataOutOfSync
|
|
641
|
+
);
|
|
642
|
+
const local =
|
|
643
|
+
localClientId ===
|
|
644
|
+
this.playbackTrackData.current.clientId;
|
|
645
|
+
await this.emitTriggerAction({
|
|
646
|
+
action: "datachange",
|
|
647
|
+
source: "system",
|
|
648
|
+
clientId: this.playbackTrackData.current.clientId,
|
|
649
|
+
local,
|
|
650
|
+
data: this.playbackTrackData.data,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private async emitSetTrack(
|
|
658
|
+
clientId: string,
|
|
659
|
+
metadata: ExtendedMediaMetadata,
|
|
660
|
+
source: ExtendedMediaSessionActionSource
|
|
661
|
+
): Promise<void> {
|
|
662
|
+
// Reset tracking states
|
|
663
|
+
this._waitPoint = undefined;
|
|
664
|
+
|
|
665
|
+
const localClientId = await waitUntilConnected(this._runtime);
|
|
666
|
+
// Trigger settrack action
|
|
667
|
+
this.emitTriggerAction({
|
|
668
|
+
action: "settrack",
|
|
669
|
+
source,
|
|
670
|
+
clientId,
|
|
671
|
+
local: localClientId === clientId,
|
|
672
|
+
metadata,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private emitTriggerAction(
|
|
677
|
+
details: ExtendedMediaSessionActionDetails
|
|
678
|
+
): void {
|
|
679
|
+
const evt: ITriggerActionEvent = {
|
|
680
|
+
name: GroupCoordinatorStateEvents.triggeraction,
|
|
681
|
+
details: details,
|
|
682
|
+
};
|
|
683
|
+
this.emit(GroupCoordinatorStateEvents.triggeraction, evt);
|
|
684
|
+
}
|
|
685
|
+
}
|