@microsoft/live-share-media 1.3.0 → 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.
Files changed (39) hide show
  1. package/.nycrc.json +10 -10
  2. package/LICENSE +90 -90
  3. package/README.md +135 -135
  4. package/bin/LiveMediaSession.d.ts.map +1 -1
  5. package/bin/LiveMediaSession.js +1 -1
  6. package/bin/LiveMediaSession.js.map +1 -1
  7. package/bin/LiveMediaSessionCoordinator.d.ts +3 -2
  8. package/bin/LiveMediaSessionCoordinator.d.ts.map +1 -1
  9. package/bin/LiveMediaSessionCoordinator.js +27 -13
  10. package/bin/LiveMediaSessionCoordinator.js.map +1 -1
  11. package/bin/MediaSessionExtensions.d.ts +0 -4
  12. package/bin/MediaSessionExtensions.d.ts.map +1 -1
  13. package/bin/MediaSessionExtensions.js.map +1 -1
  14. package/bin/internals/consts.d.ts +2 -2
  15. package/bin/internals/consts.d.ts.map +1 -1
  16. package/bin/internals/consts.js +2 -2
  17. package/bin/internals/consts.js.map +1 -1
  18. package/package.json +45 -45
  19. package/src/IMediaPlayer.ts +29 -29
  20. package/src/LiveMediaSession.ts +337 -336
  21. package/src/LiveMediaSessionCoordinator.ts +711 -678
  22. package/src/LiveMediaSessionCoordinatorSuspension.ts +39 -39
  23. package/src/MediaPlayerSynchronizer.ts +714 -714
  24. package/src/MediaSessionActionThrottler.ts +19 -19
  25. package/src/MediaSessionExtensions.ts +182 -187
  26. package/src/RepeatedActionThrottler.ts +76 -76
  27. package/src/VolumeManager.ts +200 -200
  28. package/src/index.ts +14 -14
  29. package/src/internals/GroupCoordinatorState.ts +685 -685
  30. package/src/internals/GroupPlaybackPosition.ts +228 -228
  31. package/src/internals/GroupPlaybackTrack.ts +182 -182
  32. package/src/internals/GroupPlaybackTrackData.ts +104 -104
  33. package/src/internals/GroupTransportState.ts +170 -170
  34. package/src/internals/consts.ts +103 -102
  35. package/src/internals/errors.ts +42 -42
  36. package/src/internals/index.ts +12 -12
  37. package/tsconfig.build.json +8 -8
  38. package/tsconfig.json +76 -76
  39. package/typedoc.json +10 -10
@@ -1,678 +1,711 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation. All rights reserved.
3
- * Licensed under the Microsoft Live Share SDK License.
4
- */
5
-
6
- import {
7
- LiveEventScope,
8
- LiveTelemetryLogger,
9
- LiveEventTarget,
10
- IRuntimeSignaler,
11
- TimeInterval,
12
- UserMeetingRole,
13
- LiveShareRuntime,
14
- LiveDataObjectInitializeState,
15
- LiveDataObjectInitializeNotNeededError,
16
- LiveDataObjectNotInitializedError,
17
- } from "@microsoft/live-share";
18
- import {
19
- CoordinationWaitPoint,
20
- ExtendedMediaMetadata,
21
- ExtendedMediaSessionPlaybackState,
22
- MediaSessionCoordinatorSuspension,
23
- } from "./MediaSessionExtensions";
24
- import {
25
- TelemetryEvents,
26
- ITransportCommandEvent,
27
- ISetTrackEvent,
28
- IPositionUpdateEvent,
29
- GroupCoordinatorState,
30
- GroupCoordinatorStateEvents,
31
- ISetTrackDataEvent,
32
- TrackMetadataNotSetError,
33
- ActionBlockedError,
34
- } from "./internals";
35
- import { LiveMediaSessionCoordinatorSuspension } from "./LiveMediaSessionCoordinatorSuspension";
36
- import EventEmitter from "events";
37
- import { isErrorLike } from "@microsoft/live-share/bin/internals";
38
-
39
- /**
40
- * Most recent state of the media session.
41
- */
42
- export interface IMediaPlayerState {
43
- /**
44
- * Metadata for the sessions current track.
45
- */
46
- metadata: ExtendedMediaMetadata | null;
47
-
48
- /**
49
- * Optional track data object being synchronized.
50
- *
51
- * @remarks
52
- * This can be used to sync things like pitch, roll, and yaw when watching 360 videos together.
53
- */
54
- trackData: object | null;
55
-
56
- /**
57
- * Sessions current playback state.
58
- */
59
- playbackState: ExtendedMediaSessionPlaybackState;
60
-
61
- /**
62
- * Sessions current position state if known.
63
- */
64
- positionState?: MediaPositionState;
65
- }
66
-
67
- /**
68
- * The `LiveMediaSessionCoordinator` tracks the playback & position state of all other
69
- * clients being synchronized with. It is responsible for keeping the local media player
70
- * in sync with the group.
71
- */
72
- export class LiveMediaSessionCoordinator extends EventEmitter {
73
- private readonly _runtime: IRuntimeSignaler;
74
- private readonly _liveRuntime: LiveShareRuntime;
75
- private readonly _logger: LiveTelemetryLogger;
76
- private readonly _getPlayerState: () => IMediaPlayerState;
77
- private _positionUpdateInterval = new TimeInterval(2000);
78
- private _maxPlaybackDrift = new TimeInterval(1000);
79
- private _lastWaitPoint?: CoordinationWaitPoint;
80
- private initializeState: LiveDataObjectInitializeState =
81
- LiveDataObjectInitializeState.needed;
82
-
83
- // Broadcast events
84
- private _playEvent?: LiveEventTarget<ITransportCommandEvent>;
85
- private _pauseEvent?: LiveEventTarget<ITransportCommandEvent>;
86
- private _seekToEvent?: LiveEventTarget<ITransportCommandEvent>;
87
- private _setTrackEvent?: LiveEventTarget<ISetTrackEvent>;
88
- private _setTrackDataEvent?: LiveEventTarget<ISetTrackDataEvent>;
89
- private _positionUpdateEvent?: LiveEventTarget<IPositionUpdateEvent>;
90
- private _joinedEvent?: LiveEventTarget<undefined>;
91
-
92
- // Distributed state
93
- private _groupState?: GroupCoordinatorState;
94
-
95
- /**
96
- * @hidden
97
- * Applications shouldn't directly create new coordinator instances.
98
- */
99
- constructor(
100
- runtime: IRuntimeSignaler,
101
- liveRuntime: LiveShareRuntime,
102
- getPlayerState: () => IMediaPlayerState
103
- ) {
104
- super();
105
- this._runtime = runtime;
106
- this._liveRuntime = liveRuntime;
107
- this._logger = new LiveTelemetryLogger(runtime, liveRuntime);
108
- this._getPlayerState = getPlayerState;
109
- }
110
-
111
- /**
112
- * Controls whether or not the local client is allowed to instruct the group to play or pause.
113
- *
114
- * @remarks
115
- * This flag largely meant to influence decisions made by the coordinator and can be used by
116
- * the UI to determine what controls should be shown to the user. It does not provide any
117
- * security in itself.
118
- *
119
- * If your app is running in a semi-trusted environment where only some clients are allowed
120
- * to play/pause media, you should use "role based verification" to enforce those policies.
121
- */
122
- public canPlayPause: boolean = true;
123
-
124
- /**
125
- * Controls whether or not the local client is allowed to seek the group to a new playback
126
- * position.
127
- *
128
- * @remarks
129
- * This flag largely meant to influence decisions made by the coordinator and can be used by
130
- * the UI to determine what controls should be shown to the user. It does not provide any
131
- * security in itself.
132
- *
133
- * If your app is running in a semi-trusted environment where only some clients are allowed
134
- * to change the playback position, you should use "role based verification" to enforce those policies.
135
- */
136
- public canSeek: boolean = true;
137
-
138
- /**
139
- * Controls whether or not the local client is allowed to change tracks.
140
- *
141
- * @remarks
142
- * This flag largely meant to influence decisions made by the coordinator and can be used by
143
- * the UI to determine what controls should be shown to the user. It does not provide any
144
- * security in itself.
145
- *
146
- * If your app is running in a semi-trusted environment where only some clients are allowed
147
- * to change tracks, you should use "role based verification" to enforce those policies.
148
- */
149
- public canSetTrack: boolean = true;
150
-
151
- /**
152
- * Controls whether or not the local client is allowed to change the tracks custom data object.
153
- *
154
- * @remarks
155
- * This flag largely meant to influence decisions made by the coordinator and can be used by
156
- * the UI to determine what controls should be shown to the user. It does not provide any
157
- * security in itself.
158
- *
159
- * If your app is running in a semi-trusted environment where only some clients are allowed
160
- * to change the tracks data object, you should use "role based verification" to enforce those
161
- * policies.
162
- */
163
- public canSetTrackData: boolean = true;
164
-
165
- /**
166
- * Controls whether or not the local client is allowed to send position updates to the group.
167
- *
168
- * @remarks
169
- * This flag largely meant to limit the number of signals sent to the group for performance reasons.
170
- * It does not provide any security in itself.
171
- */
172
- public canSendPositionUpdates: boolean = true;
173
-
174
- /**
175
- * Returns true if the local client is in a suspended state.
176
- */
177
- public get isSuspended(): boolean {
178
- return this._groupState ? this._groupState.isSuspended : false;
179
- }
180
-
181
- /**
182
- * Max amount of playback drift allowed in seconds.
183
- *
184
- * @remarks
185
- * Should the local clients playback position lag by more than the specified value, the
186
- * coordinator will trigger a `catchup` action.
187
- *
188
- * Defaults to a value of `1` second.
189
- */
190
- public get maxPlaybackDrift(): number {
191
- return this._maxPlaybackDrift.seconds;
192
- }
193
-
194
- public set maxPlaybackDrift(value: number) {
195
- this._maxPlaybackDrift.seconds = value;
196
- }
197
-
198
- /**
199
- * Frequency with which position updates are broadcast to the rest of the group in
200
- * seconds.
201
- *
202
- * @remarks
203
- * Defaults to a value of `2` seconds.
204
- */
205
- public get positionUpdateInterval(): number {
206
- return this._positionUpdateInterval.seconds;
207
- }
208
-
209
- public set positionUpdateInterval(value: number) {
210
- this._positionUpdateInterval.seconds = value;
211
- }
212
-
213
- /**
214
- * Instructs the group to play the current track.
215
- *
216
- * @remarks
217
- * Throws an exception if the session/coordinator hasn't been initialized, no track has been
218
- * loaded, or `canPlayPause` is false.
219
- *
220
- * @returns a void promise that resolves once complete, throws if user does not have proper roles
221
- */
222
- public async play(): Promise<void> {
223
- LiveDataObjectNotInitializedError.assert(
224
- "LiveMediaSessionCoordinator:play",
225
- "play",
226
- this.initializeState
227
- );
228
- TrackMetadataNotSetError.assert(
229
- !!this._groupState?.playbackTrack.current.metadata,
230
- "LiveMediaSessionCoordinator:play",
231
- "play"
232
- );
233
- ActionBlockedError.assert(
234
- this.canPlayPause,
235
- "LiveMediaSessionCoordinator:play",
236
- "play",
237
- "canPlayPause"
238
- );
239
-
240
- // Get projected position
241
- const position = this.getPlayerPosition();
242
-
243
- // Send transport command
244
- this._logger.sendTelemetryEvent(
245
- TelemetryEvents.SessionCoordinator.PlayCalled,
246
- null,
247
- { position: position }
248
- );
249
- await this._playEvent!.sendEvent({
250
- track: this._groupState.playbackTrack.current,
251
- position: position,
252
- });
253
- }
254
-
255
- /**
256
- * Instructs the group to pause the current track.
257
- *
258
- * @remarks
259
- * Throws an exception if the session/coordinator hasn't been initialized, no track has been
260
- * loaded, or `canPlayPause` is false.
261
- *
262
- * @returns a void promise that resolves once complete, throws if user does not have proper roles
263
- */
264
- public async pause(): Promise<void> {
265
- LiveDataObjectNotInitializedError.assert(
266
- "LiveMediaSessionCoordinator:pause",
267
- "pause",
268
- this.initializeState
269
- );
270
- TrackMetadataNotSetError.assert(
271
- !!this._groupState?.playbackTrack.current.metadata,
272
- "LiveMediaSessionCoordinator:pause",
273
- "pause"
274
- );
275
- ActionBlockedError.assert(
276
- this.canPlayPause,
277
- "LiveMediaSessionCoordinator:pause",
278
- "pause",
279
- "canPlayPause"
280
- );
281
-
282
- // Get projected position
283
- const position = this.getPlayerPosition();
284
-
285
- // Send transport command
286
- this._logger.sendTelemetryEvent(
287
- TelemetryEvents.SessionCoordinator.PauseCalled,
288
- null,
289
- { position: position }
290
- );
291
- await this._pauseEvent!.sendEvent({
292
- track: this._groupState.playbackTrack.current,
293
- position: position,
294
- });
295
- }
296
-
297
- /**
298
- * Instructs the group to seek to a new position within the current track.
299
- *
300
- * @remarks
301
- * Throws an exception if the session/coordinator hasn't been initialized, no track has been
302
- * loaded, or `canSeek` is false.
303
- * @param time Playback position in seconds to seek to.
304
- * @returns a void promise that resolves once complete, throws if user does not have proper roles
305
- */
306
- public async seekTo(time: number): Promise<void> {
307
- LiveDataObjectNotInitializedError.assert(
308
- "LiveMediaSessionCoordinator:seekTo",
309
- "seekTo",
310
- this.initializeState
311
- );
312
- TrackMetadataNotSetError.assert(
313
- !!this._groupState?.playbackTrack.current.metadata,
314
- "LiveMediaSessionCoordinator:seekTo",
315
- "seekTo"
316
- );
317
- ActionBlockedError.assert(
318
- this.canSeek,
319
- "LiveMediaSessionCoordinator:seekTo",
320
- "seekTo",
321
- "canSeek"
322
- );
323
-
324
- // Send transport command
325
- this._logger.sendTelemetryEvent(
326
- TelemetryEvents.SessionCoordinator.SeekToCalled,
327
- null,
328
- { position: time }
329
- );
330
- try {
331
- await this._seekToEvent!.sendEvent({
332
- track: this._groupState.playbackTrack.current,
333
- position: time,
334
- });
335
- } catch (err) {
336
- await this._groupState!.syncLocalMediaSession();
337
- throw err;
338
- }
339
- }
340
-
341
- /**
342
- * Instructs the group to load a new track.
343
- *
344
- * @remarks
345
- * Throws an exception if the session/coordinator hasn't been initialized or `canSetTrack` is
346
- * false.
347
- * @param metadata The track to load or `null` to indicate that the end of the track is reached.
348
- * @param waitPoints Optional. List of static wait points to configure for the track. Dynamic wait points can be added via the `beginSuspension()` call.
349
- * @returns a void promise that resolves once complete, throws if user does not have proper roles
350
- */
351
- public async setTrack(
352
- metadata: ExtendedMediaMetadata | null,
353
- waitPoints?: CoordinationWaitPoint[]
354
- ): Promise<void> {
355
- LiveDataObjectNotInitializedError.assert(
356
- "LiveMediaSessionCoordinator:setTrack",
357
- "setTrack",
358
- this.initializeState
359
- );
360
- ActionBlockedError.assert(
361
- this.canSetTrack,
362
- "LiveMediaSessionCoordinator:setTrack",
363
- "setTrack",
364
- "canSetTrack"
365
- );
366
-
367
- // Send transport command
368
- this._logger.sendTelemetryEvent(
369
- TelemetryEvents.SessionCoordinator.SetTrackCalled
370
- );
371
- await this._setTrackEvent!.sendEvent({
372
- metadata: metadata,
373
- waitPoints: waitPoints || [],
374
- });
375
- }
376
-
377
- /**
378
- * Updates the track data object for the current track.
379
- *
380
- * @remarks
381
- * The track data object can be used by applications to synchronize things like pitch, roll,
382
- * and yaw of a 360 video. This data object will be reset to null anytime the track changes.
383
- *
384
- * Throws an exception if the session/coordinator hasn't been initialized or `canSetTrackData` is
385
- * false.
386
- * @param data New data object to sync with the group. This value will be synchronized using a last writer wins strategy.
387
- * @returns a void promise that resolves once complete, throws if user does not have proper roles
388
- */
389
- public async setTrackData(data: object | null): Promise<void> {
390
- LiveDataObjectNotInitializedError.assert(
391
- "LiveMediaSessionCoordinator:setTrackData",
392
- "setTrackData",
393
- this.initializeState
394
- );
395
- ActionBlockedError.assert(
396
- this.canSetTrackData,
397
- "LiveMediaSessionCoordinator:setTrackData",
398
- "setTrackData",
399
- "canSetTrackData"
400
- );
401
-
402
- // Send transport command
403
- this._logger.sendTelemetryEvent(
404
- TelemetryEvents.SessionCoordinator.SetTrackDataCalled
405
- );
406
- await this._setTrackDataEvent!.sendEvent({
407
- data: data,
408
- });
409
- }
410
-
411
- /**
412
- * Begins a new local suspension.
413
- *
414
- * @remarks
415
- * Suspension temporarily suspend the clients local synchronization with the group. This can
416
- * be useful for displaying ads to users or temporarily disconnecting from the session while
417
- * the user seeks the video using a timeline scrubber.
418
- *
419
- * Multiple simultaneous suspensions are allowed and when the last suspension ends the local
420
- * client will be immediately re-synchronized with the group.
421
- *
422
- * A "Dynamic Wait Point" can be specified when `beginSuspension()` is called and the wait
423
- * point will be broadcast to all other clients in the group. Those clients will then
424
- * automatically enter a suspension state once they reach the positions specified by the
425
- * wait point. Clients that are passed the wait point will immediately suspend.
426
- *
427
- * Any wait point based suspension (dynamic or static) will result in all clients remaining
428
- * in a suspension state until the list client ends their suspension. This behavior can be
429
- * conditionally bypassed by settings the wait points `maxClients` value.
430
- *
431
- * Throws an exception if the session/coordinator hasn't been initialized.
432
- * @param waitPoint Optional. Dynamic wait point to broadcast to all of the clients.
433
- * @returns The suspension object. Call `end()` on the returned suspension to end the suspension.
434
- */
435
- public beginSuspension(
436
- waitPoint?: CoordinationWaitPoint
437
- ): MediaSessionCoordinatorSuspension {
438
- LiveDataObjectNotInitializedError.assert(
439
- "LiveMediaSessionCoordinator:beginSuspension",
440
- "beginSuspension",
441
- this.initializeState
442
- );
443
- TrackMetadataNotSetError.assert(
444
- !!this._groupState?.playbackTrack.current.metadata,
445
- "LiveMediaSessionCoordinator:beginSuspension",
446
- "beginSuspension"
447
- );
448
-
449
- // Tell group state that suspension is started
450
- if (waitPoint) {
451
- this._logger.sendTelemetryEvent(
452
- TelemetryEvents.SessionCoordinator.BeginSuspensionAndWait,
453
- null,
454
- {
455
- position: waitPoint.position,
456
- maxClients: waitPoint.maxClients,
457
- }
458
- );
459
- this._lastWaitPoint = waitPoint;
460
- this._groupState.beginSuspension(waitPoint);
461
- } else {
462
- this._logger.sendTelemetryEvent(
463
- TelemetryEvents.SessionCoordinator.BeginSuspension
464
- );
465
- this._groupState.beginSuspension();
466
- }
467
-
468
- // Return new suspension object
469
- return new LiveMediaSessionCoordinatorSuspension(waitPoint, (time) => {
470
- if (waitPoint) {
471
- this._logger.sendTelemetryEvent(
472
- TelemetryEvents.SessionCoordinator.EndSuspensionAndWait,
473
- null,
474
- {
475
- position: waitPoint.position,
476
- maxClients: waitPoint.maxClients,
477
- }
478
- );
479
- } else {
480
- this._logger.sendTelemetryEvent(
481
- TelemetryEvents.SessionCoordinator.EndSuspension
482
- );
483
- }
484
- this._groupState!.endSuspension(time == undefined);
485
- if (
486
- time != undefined &&
487
- !this._groupState?.isSuspended &&
488
- !this._groupState?.isWaiting
489
- ) {
490
- // Seek to new position
491
- this.seekTo(Math.max(time, 0.0));
492
- }
493
- });
494
- }
495
-
496
- /**
497
- * @hidden
498
- * Called by MediaSession to verify the coordinator has been initialized.
499
- */
500
- public get isInitialized(): boolean {
501
- return this.initializeState === LiveDataObjectInitializeState.succeeded;
502
- }
503
-
504
- /**
505
- * @hidden
506
- * Called by MediaSession to start coordinator.
507
- */
508
- public async initialize(
509
- acceptTransportChangesFrom?: UserMeetingRole[]
510
- ): Promise<void> {
511
- LiveDataObjectInitializeNotNeededError.assert(
512
- "LiveMediaSessionCoordinator:initialize",
513
- this.initializeState
514
- );
515
- this.initializeState = LiveDataObjectInitializeState.pending;
516
-
517
- try {
518
- // Create children
519
- await this.createChildren(acceptTransportChangesFrom);
520
- } catch (error: unknown) {
521
- this.initializeState = LiveDataObjectInitializeState.needed;
522
- throw error;
523
- }
524
- this.initializeState = LiveDataObjectInitializeState.succeeded;
525
- }
526
-
527
- /**
528
- * @hidden
529
- * Called by the MediaSession to detect if a wait point has been hit.
530
- */
531
- public findNextWaitPoint(): CoordinationWaitPoint | null {
532
- return (
533
- this._groupState?.playbackTrack.findNextWaitPoint(
534
- this._lastWaitPoint
535
- ) || null
536
- );
537
- }
538
-
539
- /**
540
- * @hidden
541
- * Called by MediaSession to trigger the sending of a position update.
542
- */
543
- public sendPositionUpdate(state: IMediaPlayerState): void {
544
- LiveDataObjectNotInitializedError.assert(
545
- "LiveMediaSessionCoordinator:sendPositionUpdate",
546
- "sendPositionUpdate",
547
- this.initializeState
548
- );
549
-
550
- if (this.canSendPositionUpdates) {
551
- // Send position update event
552
- const evt = this._groupState!.createPositionUpdateEvent(state);
553
- this._positionUpdateEvent?.sendEvent(evt).catch((err) => {
554
- this._logger.sendErrorEvent(
555
- TelemetryEvents.SessionCoordinator.PositionUpdateEventError,
556
- err
557
- );
558
- });
559
- } else {
560
- // make sure position is synced to position of clients who can send position updates
561
- this._groupState!.syncLocalMediaSession();
562
- }
563
- }
564
-
565
- protected async createChildren(
566
- acceptTransportChangesFrom?: UserMeetingRole[]
567
- ): Promise<void> {
568
- // Create event scopes
569
- const scope = new LiveEventScope(
570
- this._runtime,
571
- this._liveRuntime,
572
- acceptTransportChangesFrom
573
- );
574
- const unrestrictedScope = new LiveEventScope(
575
- this._runtime,
576
- this._liveRuntime
577
- );
578
-
579
- // Initialize internal coordinator state
580
- this._groupState = new GroupCoordinatorState(
581
- this._runtime,
582
- this._liveRuntime,
583
- this._maxPlaybackDrift,
584
- this._positionUpdateInterval,
585
- this._getPlayerState
586
- );
587
- this._groupState.on(GroupCoordinatorStateEvents.triggeraction, (evt) =>
588
- this.emit(evt.name, evt)
589
- );
590
-
591
- // Listen for track changes
592
- this._setTrackEvent = new LiveEventTarget<ISetTrackEvent>(
593
- scope,
594
- "setTrack",
595
- (event, local) => this._groupState!.handleSetTrack(event, local)
596
- );
597
- this._setTrackDataEvent = new LiveEventTarget<ISetTrackDataEvent>(
598
- scope,
599
- "setTrackData",
600
- (event, local) => this._groupState!.handleSetTrackData(event, local)
601
- );
602
-
603
- // Listen for transport commands
604
- this._playEvent = new LiveEventTarget(scope, "play", (event, local) =>
605
- this._groupState!.handleTransportCommand(event, local)
606
- );
607
- this._pauseEvent = new LiveEventTarget(scope, "pause", (event, local) =>
608
- this._groupState!.handleTransportCommand(event, local)
609
- );
610
- this._seekToEvent = new LiveEventTarget(
611
- scope,
612
- "seekTo",
613
- (event, local) =>
614
- this._groupState!.handleTransportCommand(event, local)
615
- );
616
-
617
- // Listen for position updates
618
- this._positionUpdateEvent = new LiveEventTarget(
619
- unrestrictedScope,
620
- "positionUpdate",
621
- (event, local) =>
622
- this._groupState!.handlePositionUpdate(event, local)
623
- );
624
-
625
- // Listen for joined event
626
- this._joinedEvent = new LiveEventTarget(
627
- unrestrictedScope,
628
- "joined",
629
- (evt, local) => {
630
- // Immediately send a position update
631
- this._logger.sendTelemetryEvent(
632
- TelemetryEvents.SessionCoordinator.RemoteJoinReceived,
633
- null,
634
- {
635
- correlationId: LiveTelemetryLogger.formatCorrelationId(
636
- evt.clientId,
637
- evt.timestamp
638
- ),
639
- }
640
- );
641
- try {
642
- const state = this._getPlayerState();
643
- this.sendPositionUpdate(state);
644
- } catch (err: any) {
645
- // if player is not setup yet, local client might have also just joined and can't send its position.
646
- const playerNotSetup =
647
- isErrorLike(err) &&
648
- err.message.includes(
649
- "LiveMediaSession:getCurrentPlayerState"
650
- );
651
- if (!playerNotSetup) {
652
- throw err;
653
- }
654
- }
655
- }
656
- );
657
- // Send initial joined event
658
- this._joinedEvent?.sendEvent(undefined).catch((err) => {
659
- this._logger.sendErrorEvent(
660
- TelemetryEvents.SessionCoordinator.SendJoinedEventError,
661
- err
662
- );
663
- });
664
- }
665
-
666
- private getPlayerPosition(): number {
667
- const { positionState, playbackState } = this._getPlayerState();
668
- switch (playbackState) {
669
- case "none":
670
- case "ended":
671
- return 0.0;
672
- default:
673
- return positionState && positionState.position != undefined
674
- ? positionState.position
675
- : 0.0;
676
- }
677
- }
678
- }
1
+ /**
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the Microsoft Live Share SDK License.
4
+ */
5
+
6
+ import {
7
+ LiveEventScope,
8
+ LiveTelemetryLogger,
9
+ LiveEventTarget,
10
+ IRuntimeSignaler,
11
+ TimeInterval,
12
+ UserMeetingRole,
13
+ LiveShareRuntime,
14
+ LiveDataObjectInitializeState,
15
+ LiveDataObjectInitializeNotNeededError,
16
+ LiveDataObjectNotInitializedError,
17
+ LiveObjectSynchronizer,
18
+ ILiveEvent,
19
+ } from "@microsoft/live-share";
20
+ import {
21
+ CoordinationWaitPoint,
22
+ ExtendedMediaMetadata,
23
+ ExtendedMediaSessionPlaybackState,
24
+ MediaSessionCoordinatorSuspension,
25
+ } from "./MediaSessionExtensions";
26
+ import {
27
+ TelemetryEvents,
28
+ ITransportCommandEvent,
29
+ ISetTrackEvent,
30
+ IPositionUpdateEvent,
31
+ GroupCoordinatorState,
32
+ GroupCoordinatorStateEvents,
33
+ ISetTrackDataEvent,
34
+ TrackMetadataNotSetError,
35
+ ActionBlockedError,
36
+ } from "./internals";
37
+ import { LiveMediaSessionCoordinatorSuspension } from "./LiveMediaSessionCoordinatorSuspension";
38
+ import EventEmitter from "events";
39
+ import { isErrorLike } from "@microsoft/live-share/bin/internals";
40
+
41
+ /**
42
+ * Most recent state of the media session.
43
+ */
44
+ export interface IMediaPlayerState {
45
+ /**
46
+ * Metadata for the sessions current track.
47
+ */
48
+ metadata: ExtendedMediaMetadata | null;
49
+
50
+ /**
51
+ * Optional track data object being synchronized.
52
+ *
53
+ * @remarks
54
+ * This can be used to sync things like pitch, roll, and yaw when watching 360 videos together.
55
+ */
56
+ trackData: object | null;
57
+
58
+ /**
59
+ * Sessions current playback state.
60
+ */
61
+ playbackState: ExtendedMediaSessionPlaybackState;
62
+
63
+ /**
64
+ * Sessions current position state if known.
65
+ */
66
+ positionState?: MediaPositionState;
67
+ }
68
+
69
+ /**
70
+ * The `LiveMediaSessionCoordinator` tracks the playback & position state of all other
71
+ * clients being synchronized with. It is responsible for keeping the local media player
72
+ * in sync with the group.
73
+ */
74
+ export class LiveMediaSessionCoordinator extends EventEmitter {
75
+ private readonly _id: string;
76
+ private readonly _runtime: IRuntimeSignaler;
77
+ private readonly _liveRuntime: LiveShareRuntime;
78
+ private readonly _logger: LiveTelemetryLogger;
79
+ private readonly _getPlayerState: () => IMediaPlayerState;
80
+ private _positionUpdateInterval = new TimeInterval(2000);
81
+ private _maxPlaybackDrift = new TimeInterval(1000);
82
+ private _lastWaitPoint?: CoordinationWaitPoint;
83
+ private initializeState: LiveDataObjectInitializeState =
84
+ LiveDataObjectInitializeState.needed;
85
+
86
+ // Broadcast events
87
+ private _playEvent?: LiveEventTarget<ITransportCommandEvent>;
88
+ private _pauseEvent?: LiveEventTarget<ITransportCommandEvent>;
89
+ private _seekToEvent?: LiveEventTarget<ITransportCommandEvent>;
90
+ private _setTrackEvent?: LiveEventTarget<ISetTrackEvent>;
91
+ private _setTrackDataEvent?: LiveEventTarget<ISetTrackDataEvent>;
92
+ private _positionUpdateEvent?: LiveEventTarget<IPositionUpdateEvent>;
93
+
94
+ // Synchronizer does not play a significant role in LiveMediaSession beyond being able to
95
+ // use "connect" events to replace old "joined" events when sending position data to new clients.
96
+ private _synchronizer?: LiveObjectSynchronizer<undefined>;
97
+
98
+ // Distributed state
99
+ private _groupState?: GroupCoordinatorState;
100
+
101
+ /**
102
+ * @hidden
103
+ * Applications shouldn't directly create new coordinator instances.
104
+ */
105
+ constructor(
106
+ id: string,
107
+ runtime: IRuntimeSignaler,
108
+ liveRuntime: LiveShareRuntime,
109
+ getPlayerState: () => IMediaPlayerState
110
+ ) {
111
+ super();
112
+ this._id = id;
113
+ this._runtime = runtime;
114
+ this._liveRuntime = liveRuntime;
115
+ this._logger = new LiveTelemetryLogger(runtime, liveRuntime);
116
+ this._getPlayerState = getPlayerState;
117
+ }
118
+
119
+ /**
120
+ * Controls whether or not the local client is allowed to instruct the group to play or pause.
121
+ *
122
+ * @remarks
123
+ * This flag largely meant to influence decisions made by the coordinator and can be used by
124
+ * the UI to determine what controls should be shown to the user. It does not provide any
125
+ * security in itself.
126
+ *
127
+ * If your app is running in a semi-trusted environment where only some clients are allowed
128
+ * to play/pause media, you should use "role based verification" to enforce those policies.
129
+ */
130
+ public canPlayPause: boolean = true;
131
+
132
+ /**
133
+ * Controls whether or not the local client is allowed to seek the group to a new playback
134
+ * position.
135
+ *
136
+ * @remarks
137
+ * This flag largely meant to influence decisions made by the coordinator and can be used by
138
+ * the UI to determine what controls should be shown to the user. It does not provide any
139
+ * security in itself.
140
+ *
141
+ * If your app is running in a semi-trusted environment where only some clients are allowed
142
+ * to change the playback position, you should use "role based verification" to enforce those policies.
143
+ */
144
+ public canSeek: boolean = true;
145
+
146
+ /**
147
+ * Controls whether or not the local client is allowed to change tracks.
148
+ *
149
+ * @remarks
150
+ * This flag largely meant to influence decisions made by the coordinator and can be used by
151
+ * the UI to determine what controls should be shown to the user. It does not provide any
152
+ * security in itself.
153
+ *
154
+ * If your app is running in a semi-trusted environment where only some clients are allowed
155
+ * to change tracks, you should use "role based verification" to enforce those policies.
156
+ */
157
+ public canSetTrack: boolean = true;
158
+
159
+ /**
160
+ * Controls whether or not the local client is allowed to change the tracks custom data object.
161
+ *
162
+ * @remarks
163
+ * This flag largely meant to influence decisions made by the coordinator and can be used by
164
+ * the UI to determine what controls should be shown to the user. It does not provide any
165
+ * security in itself.
166
+ *
167
+ * If your app is running in a semi-trusted environment where only some clients are allowed
168
+ * to change the tracks data object, you should use "role based verification" to enforce those
169
+ * policies.
170
+ */
171
+ public canSetTrackData: boolean = true;
172
+
173
+ /**
174
+ * Controls whether or not the local client is allowed to send position updates to the group.
175
+ *
176
+ * @remarks
177
+ * This flag largely meant to limit the number of signals sent to the group for performance reasons.
178
+ * It does not provide any security in itself.
179
+ */
180
+ public canSendPositionUpdates: boolean = true;
181
+
182
+ /**
183
+ * Returns true if the local client is in a suspended state.
184
+ */
185
+ public get isSuspended(): boolean {
186
+ return this._groupState ? this._groupState.isSuspended : false;
187
+ }
188
+
189
+ /**
190
+ * Max amount of playback drift allowed in seconds.
191
+ *
192
+ * @remarks
193
+ * Should the local clients playback position lag by more than the specified value, the
194
+ * coordinator will trigger a `catchup` action.
195
+ *
196
+ * Defaults to a value of `1` second.
197
+ */
198
+ public get maxPlaybackDrift(): number {
199
+ return this._maxPlaybackDrift.seconds;
200
+ }
201
+
202
+ public set maxPlaybackDrift(value: number) {
203
+ this._maxPlaybackDrift.seconds = value;
204
+ }
205
+
206
+ /**
207
+ * Frequency with which position updates are broadcast to the rest of the group in
208
+ * seconds.
209
+ *
210
+ * @remarks
211
+ * Defaults to a value of `2` seconds.
212
+ */
213
+ public get positionUpdateInterval(): number {
214
+ return this._positionUpdateInterval.seconds;
215
+ }
216
+
217
+ public set positionUpdateInterval(value: number) {
218
+ this._positionUpdateInterval.seconds = value;
219
+ }
220
+
221
+ /**
222
+ * Instructs the group to play the current track.
223
+ *
224
+ * @remarks
225
+ * Throws an exception if the session/coordinator hasn't been initialized, no track has been
226
+ * loaded, or `canPlayPause` is false.
227
+ *
228
+ * @returns a void promise that resolves once complete, throws if user does not have proper roles
229
+ */
230
+ public async play(): Promise<void> {
231
+ LiveDataObjectNotInitializedError.assert(
232
+ "LiveMediaSessionCoordinator:play",
233
+ "play",
234
+ this.initializeState
235
+ );
236
+ TrackMetadataNotSetError.assert(
237
+ !!this._groupState?.playbackTrack.current.metadata,
238
+ "LiveMediaSessionCoordinator:play",
239
+ "play"
240
+ );
241
+ ActionBlockedError.assert(
242
+ this.canPlayPause,
243
+ "LiveMediaSessionCoordinator:play",
244
+ "play",
245
+ "canPlayPause"
246
+ );
247
+
248
+ // Get projected position
249
+ const position = this.getPlayerPosition();
250
+
251
+ // Send transport command
252
+ this._logger.sendTelemetryEvent(
253
+ TelemetryEvents.SessionCoordinator.PlayCalled,
254
+ null,
255
+ { position: position }
256
+ );
257
+ await this._playEvent!.sendEvent({
258
+ track: this._groupState.playbackTrack.current,
259
+ position: position,
260
+ });
261
+ }
262
+
263
+ /**
264
+ * Instructs the group to pause the current track.
265
+ *
266
+ * @remarks
267
+ * Throws an exception if the session/coordinator hasn't been initialized, no track has been
268
+ * loaded, or `canPlayPause` is false.
269
+ *
270
+ * @returns a void promise that resolves once complete, throws if user does not have proper roles
271
+ */
272
+ public async pause(): Promise<void> {
273
+ LiveDataObjectNotInitializedError.assert(
274
+ "LiveMediaSessionCoordinator:pause",
275
+ "pause",
276
+ this.initializeState
277
+ );
278
+ TrackMetadataNotSetError.assert(
279
+ !!this._groupState?.playbackTrack.current.metadata,
280
+ "LiveMediaSessionCoordinator:pause",
281
+ "pause"
282
+ );
283
+ ActionBlockedError.assert(
284
+ this.canPlayPause,
285
+ "LiveMediaSessionCoordinator:pause",
286
+ "pause",
287
+ "canPlayPause"
288
+ );
289
+
290
+ // Get projected position
291
+ const position = this.getPlayerPosition();
292
+
293
+ // Send transport command
294
+ this._logger.sendTelemetryEvent(
295
+ TelemetryEvents.SessionCoordinator.PauseCalled,
296
+ null,
297
+ { position: position }
298
+ );
299
+ await this._pauseEvent!.sendEvent({
300
+ track: this._groupState.playbackTrack.current,
301
+ position: position,
302
+ });
303
+ }
304
+
305
+ /**
306
+ * Instructs the group to seek to a new position within the current track.
307
+ *
308
+ * @remarks
309
+ * Throws an exception if the session/coordinator hasn't been initialized, no track has been
310
+ * loaded, or `canSeek` is false.
311
+ * @param time Playback position in seconds to seek to.
312
+ * @returns a void promise that resolves once complete, throws if user does not have proper roles
313
+ */
314
+ public async seekTo(time: number): Promise<void> {
315
+ LiveDataObjectNotInitializedError.assert(
316
+ "LiveMediaSessionCoordinator:seekTo",
317
+ "seekTo",
318
+ this.initializeState
319
+ );
320
+ TrackMetadataNotSetError.assert(
321
+ !!this._groupState?.playbackTrack.current.metadata,
322
+ "LiveMediaSessionCoordinator:seekTo",
323
+ "seekTo"
324
+ );
325
+ ActionBlockedError.assert(
326
+ this.canSeek,
327
+ "LiveMediaSessionCoordinator:seekTo",
328
+ "seekTo",
329
+ "canSeek"
330
+ );
331
+
332
+ // Send transport command
333
+ this._logger.sendTelemetryEvent(
334
+ TelemetryEvents.SessionCoordinator.SeekToCalled,
335
+ null,
336
+ { position: time }
337
+ );
338
+ try {
339
+ await this._seekToEvent!.sendEvent({
340
+ track: this._groupState.playbackTrack.current,
341
+ position: time,
342
+ });
343
+ } catch (err) {
344
+ await this._groupState!.syncLocalMediaSession();
345
+ throw err;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Instructs the group to load a new track.
351
+ *
352
+ * @remarks
353
+ * Throws an exception if the session/coordinator hasn't been initialized or `canSetTrack` is
354
+ * false.
355
+ * @param metadata The track to load or `null` to indicate that the end of the track is reached.
356
+ * @param waitPoints Optional. List of static wait points to configure for the track. Dynamic wait points can be added via the `beginSuspension()` call.
357
+ * @returns a void promise that resolves once complete, throws if user does not have proper roles
358
+ */
359
+ public async setTrack(
360
+ metadata: ExtendedMediaMetadata | null,
361
+ waitPoints?: CoordinationWaitPoint[]
362
+ ): Promise<void> {
363
+ LiveDataObjectNotInitializedError.assert(
364
+ "LiveMediaSessionCoordinator:setTrack",
365
+ "setTrack",
366
+ this.initializeState
367
+ );
368
+ ActionBlockedError.assert(
369
+ this.canSetTrack,
370
+ "LiveMediaSessionCoordinator:setTrack",
371
+ "setTrack",
372
+ "canSetTrack"
373
+ );
374
+
375
+ // Send transport command
376
+ this._logger.sendTelemetryEvent(
377
+ TelemetryEvents.SessionCoordinator.SetTrackCalled
378
+ );
379
+ await this._setTrackEvent!.sendEvent({
380
+ metadata: metadata,
381
+ waitPoints: waitPoints || [],
382
+ });
383
+ }
384
+
385
+ /**
386
+ * Updates the track data object for the current track.
387
+ *
388
+ * @remarks
389
+ * The track data object can be used by applications to synchronize things like pitch, roll,
390
+ * and yaw of a 360 video. This data object will be reset to null anytime the track changes.
391
+ *
392
+ * Throws an exception if the session/coordinator hasn't been initialized or `canSetTrackData` is
393
+ * false.
394
+ * @param data New data object to sync with the group. This value will be synchronized using a last writer wins strategy.
395
+ * @returns a void promise that resolves once complete, throws if user does not have proper roles
396
+ */
397
+ public async setTrackData(data: object | null): Promise<void> {
398
+ LiveDataObjectNotInitializedError.assert(
399
+ "LiveMediaSessionCoordinator:setTrackData",
400
+ "setTrackData",
401
+ this.initializeState
402
+ );
403
+ ActionBlockedError.assert(
404
+ this.canSetTrackData,
405
+ "LiveMediaSessionCoordinator:setTrackData",
406
+ "setTrackData",
407
+ "canSetTrackData"
408
+ );
409
+
410
+ // Send transport command
411
+ this._logger.sendTelemetryEvent(
412
+ TelemetryEvents.SessionCoordinator.SetTrackDataCalled
413
+ );
414
+ await this._setTrackDataEvent!.sendEvent({
415
+ data: data,
416
+ });
417
+ }
418
+
419
+ /**
420
+ * Begins a new local suspension.
421
+ *
422
+ * @remarks
423
+ * Suspension temporarily suspend the clients local synchronization with the group. This can
424
+ * be useful for displaying ads to users or temporarily disconnecting from the session while
425
+ * the user seeks the video using a timeline scrubber.
426
+ *
427
+ * Multiple simultaneous suspensions are allowed and when the last suspension ends the local
428
+ * client will be immediately re-synchronized with the group.
429
+ *
430
+ * A "Dynamic Wait Point" can be specified when `beginSuspension()` is called and the wait
431
+ * point will be broadcast to all other clients in the group. Those clients will then
432
+ * automatically enter a suspension state once they reach the positions specified by the
433
+ * wait point. Clients that are passed the wait point will immediately suspend.
434
+ *
435
+ * Any wait point based suspension (dynamic or static) will result in all clients remaining
436
+ * in a suspension state until the list client ends their suspension. This behavior can be
437
+ * conditionally bypassed by settings the wait points `maxClients` value.
438
+ *
439
+ * Throws an exception if the session/coordinator hasn't been initialized.
440
+ * @param waitPoint Optional. Dynamic wait point to broadcast to all of the clients.
441
+ * @returns The suspension object. Call `end()` on the returned suspension to end the suspension.
442
+ */
443
+ public beginSuspension(
444
+ waitPoint?: CoordinationWaitPoint
445
+ ): MediaSessionCoordinatorSuspension {
446
+ LiveDataObjectNotInitializedError.assert(
447
+ "LiveMediaSessionCoordinator:beginSuspension",
448
+ "beginSuspension",
449
+ this.initializeState
450
+ );
451
+ TrackMetadataNotSetError.assert(
452
+ !!this._groupState?.playbackTrack.current.metadata,
453
+ "LiveMediaSessionCoordinator:beginSuspension",
454
+ "beginSuspension"
455
+ );
456
+
457
+ // Tell group state that suspension is started
458
+ if (waitPoint) {
459
+ this._logger.sendTelemetryEvent(
460
+ TelemetryEvents.SessionCoordinator.BeginSuspensionAndWait,
461
+ null,
462
+ {
463
+ position: waitPoint.position,
464
+ maxClients: waitPoint.maxClients,
465
+ }
466
+ );
467
+ this._lastWaitPoint = waitPoint;
468
+ this._groupState.beginSuspension(waitPoint);
469
+ } else {
470
+ this._logger.sendTelemetryEvent(
471
+ TelemetryEvents.SessionCoordinator.BeginSuspension
472
+ );
473
+ this._groupState.beginSuspension();
474
+ }
475
+
476
+ // Return new suspension object
477
+ return new LiveMediaSessionCoordinatorSuspension(waitPoint, (time) => {
478
+ if (waitPoint) {
479
+ this._logger.sendTelemetryEvent(
480
+ TelemetryEvents.SessionCoordinator.EndSuspensionAndWait,
481
+ null,
482
+ {
483
+ position: waitPoint.position,
484
+ maxClients: waitPoint.maxClients,
485
+ }
486
+ );
487
+ } else {
488
+ this._logger.sendTelemetryEvent(
489
+ TelemetryEvents.SessionCoordinator.EndSuspension
490
+ );
491
+ }
492
+ this._groupState!.endSuspension(time == undefined);
493
+ if (
494
+ time != undefined &&
495
+ !this._groupState?.isSuspended &&
496
+ !this._groupState?.isWaiting
497
+ ) {
498
+ // Seek to new position
499
+ this.seekTo(Math.max(time, 0.0));
500
+ }
501
+ });
502
+ }
503
+
504
+ /**
505
+ * @hidden
506
+ * Called by MediaSession to verify the coordinator has been initialized.
507
+ */
508
+ public get isInitialized(): boolean {
509
+ return this.initializeState === LiveDataObjectInitializeState.succeeded;
510
+ }
511
+
512
+ /**
513
+ * @hidden
514
+ * Called by MediaSession to start coordinator.
515
+ */
516
+ public async initialize(
517
+ acceptTransportChangesFrom?: UserMeetingRole[]
518
+ ): Promise<void> {
519
+ LiveDataObjectInitializeNotNeededError.assert(
520
+ "LiveMediaSessionCoordinator:initialize",
521
+ this.initializeState
522
+ );
523
+ this.initializeState = LiveDataObjectInitializeState.pending;
524
+
525
+ try {
526
+ // Create children
527
+ await this.createChildren(acceptTransportChangesFrom);
528
+ } catch (error: unknown) {
529
+ this.initializeState = LiveDataObjectInitializeState.needed;
530
+ throw error;
531
+ }
532
+ this.initializeState = LiveDataObjectInitializeState.succeeded;
533
+
534
+ try {
535
+ // start needs to happen after setting initializedState to "succeeded"
536
+ await this._synchronizer?.start(
537
+ undefined,
538
+ async (evt, sender, local) => false,
539
+ async (connecting) => true,
540
+ false,
541
+ false
542
+ );
543
+ } catch (error: unknown) {
544
+ // not a fatal error for LiveMediaSession, only used for "connect" event to send new clients position updates.
545
+ this._logger.sendErrorEvent(
546
+ TelemetryEvents.SessionCoordinator
547
+ .LiveObjectSynchronizerStartError,
548
+ error
549
+ );
550
+ }
551
+ }
552
+
553
+ /**
554
+ * @hidden
555
+ * Called by the MediaSession to detect if a wait point has been hit.
556
+ */
557
+ public findNextWaitPoint(): CoordinationWaitPoint | null {
558
+ return (
559
+ this._groupState?.playbackTrack.findNextWaitPoint(
560
+ this._lastWaitPoint
561
+ ) || null
562
+ );
563
+ }
564
+
565
+ /**
566
+ * @hidden
567
+ * Called by MediaSession to trigger the sending of a position update.
568
+ */
569
+ public sendPositionUpdate(state: IMediaPlayerState): void {
570
+ LiveDataObjectNotInitializedError.assert(
571
+ "LiveMediaSessionCoordinator:sendPositionUpdate",
572
+ "sendPositionUpdate",
573
+ this.initializeState
574
+ );
575
+
576
+ if (this.canSendPositionUpdates) {
577
+ // Send position update event
578
+ const evt = this._groupState!.createPositionUpdateEvent(state);
579
+ this._positionUpdateEvent?.sendEvent(evt).catch((err) => {
580
+ this._logger.sendErrorEvent(
581
+ TelemetryEvents.SessionCoordinator.PositionUpdateEventError,
582
+ err
583
+ );
584
+ });
585
+ } else if (this.isSuspended) {
586
+ // send a local only position update event that was not sent as a signal, and use to handle local
587
+ // position update for clients that have canSendPositionUpdates==false, but are suspending
588
+ const evt = this._groupState!.createPositionUpdateEvent(state);
589
+ this._positionUpdateEvent?.sendLocalEvent(evt).catch((err) => {
590
+ this._logger.sendErrorEvent(
591
+ TelemetryEvents.SessionCoordinator.PositionUpdateEventError,
592
+ err
593
+ );
594
+ });
595
+ } else {
596
+ // make sure position is synced to position of clients who can send position updates
597
+ this._groupState!.syncLocalMediaSession();
598
+ }
599
+ }
600
+
601
+ protected async createChildren(
602
+ acceptTransportChangesFrom?: UserMeetingRole[]
603
+ ): Promise<void> {
604
+ // Create event scopes
605
+ const scope = new LiveEventScope(
606
+ this._runtime,
607
+ this._liveRuntime,
608
+ acceptTransportChangesFrom
609
+ );
610
+ const unrestrictedScope = new LiveEventScope(
611
+ this._runtime,
612
+ this._liveRuntime
613
+ );
614
+
615
+ // Initialize internal coordinator state
616
+ this._groupState = new GroupCoordinatorState(
617
+ this._runtime,
618
+ this._liveRuntime,
619
+ this._maxPlaybackDrift,
620
+ this._positionUpdateInterval,
621
+ this._getPlayerState
622
+ );
623
+ this._groupState.on(GroupCoordinatorStateEvents.triggeraction, (evt) =>
624
+ this.emit(evt.name, evt)
625
+ );
626
+
627
+ // Listen for track changes
628
+ this._setTrackEvent = new LiveEventTarget<ISetTrackEvent>(
629
+ scope,
630
+ "setTrack",
631
+ (event, local) => this._groupState!.handleSetTrack(event, local)
632
+ );
633
+ this._setTrackDataEvent = new LiveEventTarget<ISetTrackDataEvent>(
634
+ scope,
635
+ "setTrackData",
636
+ (event, local) => this._groupState!.handleSetTrackData(event, local)
637
+ );
638
+
639
+ // Listen for transport commands
640
+ this._playEvent = new LiveEventTarget(scope, "play", (event, local) =>
641
+ this._groupState!.handleTransportCommand(event, local)
642
+ );
643
+ this._pauseEvent = new LiveEventTarget(scope, "pause", (event, local) =>
644
+ this._groupState!.handleTransportCommand(event, local)
645
+ );
646
+ this._seekToEvent = new LiveEventTarget(
647
+ scope,
648
+ "seekTo",
649
+ (event, local) =>
650
+ this._groupState!.handleTransportCommand(event, local)
651
+ );
652
+
653
+ // Listen for position updates
654
+ this._positionUpdateEvent = new LiveEventTarget(
655
+ unrestrictedScope,
656
+ "positionUpdate",
657
+ (event, local) =>
658
+ this._groupState!.handlePositionUpdate(event, local)
659
+ );
660
+
661
+ this._synchronizer = new LiveObjectSynchronizer<undefined>(
662
+ this._id,
663
+ this._runtime,
664
+ this._liveRuntime
665
+ );
666
+
667
+ this._synchronizer.onJoinedListener = (
668
+ clientId: string,
669
+ timestamp: number
670
+ ) => {
671
+ this._logger.sendTelemetryEvent(
672
+ TelemetryEvents.SessionCoordinator.RemoteConnectReceived,
673
+ null,
674
+ {
675
+ correlationId: LiveTelemetryLogger.formatCorrelationId(
676
+ clientId,
677
+ timestamp
678
+ ),
679
+ }
680
+ );
681
+ // Immediately send a position update
682
+ try {
683
+ const state = this._getPlayerState();
684
+ this.sendPositionUpdate(state);
685
+ } catch (err: any) {
686
+ // if player is not setup yet, local client might have also just joined and can't send its position.
687
+ const playerNotSetup =
688
+ isErrorLike(err) &&
689
+ err.message.includes(
690
+ "LiveMediaSession:getCurrentPlayerState"
691
+ );
692
+ if (!playerNotSetup) {
693
+ throw err;
694
+ }
695
+ }
696
+ };
697
+ }
698
+
699
+ private getPlayerPosition(): number {
700
+ const { positionState, playbackState } = this._getPlayerState();
701
+ switch (playbackState) {
702
+ case "none":
703
+ case "ended":
704
+ return 0.0;
705
+ default:
706
+ return positionState && positionState.position != undefined
707
+ ? positionState.position
708
+ : 0.0;
709
+ }
710
+ }
711
+ }