@microsoft/live-share-media 1.3.0 → 1.4.0

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