@microsoft/live-share-media 1.0.0-preview.9 → 1.0.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 (89) hide show
  1. package/LICENSE +64 -61
  2. package/README.md +7 -13
  3. package/bin/IMediaPlayer.d.ts +20 -20
  4. package/bin/IMediaPlayer.js +6 -6
  5. package/bin/LiveMediaSession.d.ts +90 -94
  6. package/bin/LiveMediaSession.d.ts.map +1 -1
  7. package/bin/LiveMediaSession.js +228 -231
  8. package/bin/LiveMediaSession.js.map +1 -1
  9. package/bin/LiveMediaSessionCoordinator.d.ts +238 -227
  10. package/bin/LiveMediaSessionCoordinator.d.ts.map +1 -1
  11. package/bin/LiveMediaSessionCoordinator.js +476 -407
  12. package/bin/LiveMediaSessionCoordinator.js.map +1 -1
  13. package/bin/LiveMediaSessionCoordinatorSuspension.d.ts +19 -19
  14. package/bin/LiveMediaSessionCoordinatorSuspension.js +27 -27
  15. package/bin/MediaPlayerSynchronizer.d.ts +174 -163
  16. package/bin/MediaPlayerSynchronizer.d.ts.map +1 -1
  17. package/bin/MediaPlayerSynchronizer.js +503 -468
  18. package/bin/MediaPlayerSynchronizer.js.map +1 -1
  19. package/bin/MediaSessionActionThrottler.d.ts +11 -11
  20. package/bin/MediaSessionActionThrottler.js +13 -13
  21. package/bin/MediaSessionExtensions.d.ts +34 -34
  22. package/bin/MediaSessionExtensions.d.ts.map +1 -1
  23. package/bin/MediaSessionExtensions.js +12 -12
  24. package/bin/RepeatedActionThrottler.d.ts +19 -19
  25. package/bin/RepeatedActionThrottler.js +62 -62
  26. package/bin/RepeatedActionThrottler.js.map +1 -1
  27. package/bin/VolumeManager.d.ts +83 -83
  28. package/bin/VolumeManager.d.ts.map +1 -1
  29. package/bin/VolumeManager.js +174 -174
  30. package/bin/VolumeManager.js.map +1 -1
  31. package/bin/index.d.ts +13 -13
  32. package/bin/index.js +29 -29
  33. package/bin/internals/GroupCoordinatorState.d.ts +93 -92
  34. package/bin/internals/GroupCoordinatorState.d.ts.map +1 -1
  35. package/bin/internals/GroupCoordinatorState.js +407 -404
  36. package/bin/internals/GroupCoordinatorState.js.map +1 -1
  37. package/bin/internals/GroupPlaybackPosition.d.ts +67 -66
  38. package/bin/internals/GroupPlaybackPosition.d.ts.map +1 -1
  39. package/bin/internals/GroupPlaybackPosition.js +165 -166
  40. package/bin/internals/GroupPlaybackPosition.js.map +1 -1
  41. package/bin/internals/GroupPlaybackTrack.d.ts +45 -45
  42. package/bin/internals/GroupPlaybackTrack.d.ts.map +1 -1
  43. package/bin/internals/GroupPlaybackTrack.js +121 -121
  44. package/bin/internals/GroupPlaybackTrackData.d.ts +39 -39
  45. package/bin/internals/GroupPlaybackTrackData.d.ts.map +1 -1
  46. package/bin/internals/GroupPlaybackTrackData.js +69 -69
  47. package/bin/internals/GroupTransportState.d.ts +49 -48
  48. package/bin/internals/GroupTransportState.d.ts.map +1 -1
  49. package/bin/internals/GroupTransportState.js +111 -111
  50. package/bin/internals/GroupTransportState.js.map +1 -1
  51. package/bin/internals/consts.d.ts +75 -72
  52. package/bin/internals/consts.d.ts.map +1 -1
  53. package/bin/internals/consts.js +84 -81
  54. package/bin/internals/consts.js.map +1 -1
  55. package/bin/internals/index.d.ts +10 -10
  56. package/bin/internals/index.js +26 -26
  57. package/package.json +16 -9
  58. package/src/LiveMediaSession.ts +24 -20
  59. package/src/LiveMediaSessionCoordinator.ts +101 -29
  60. package/src/MediaPlayerSynchronizer.ts +47 -15
  61. package/src/VolumeManager.ts +4 -1
  62. package/src/internals/GroupCoordinatorState.ts +50 -35
  63. package/src/internals/GroupPlaybackPosition.ts +8 -7
  64. package/src/internals/GroupPlaybackTrack.ts +2 -2
  65. package/src/internals/GroupPlaybackTrackData.ts +2 -2
  66. package/src/internals/GroupTransportState.ts +11 -4
  67. package/src/internals/consts.ts +4 -0
  68. package/typedoc.json +10 -0
  69. package/bin/EphemeralMediaSession.d.ts +0 -102
  70. package/bin/EphemeralMediaSession.d.ts.map +0 -1
  71. package/bin/EphemeralMediaSession.js +0 -226
  72. package/bin/EphemeralMediaSession.js.map +0 -1
  73. package/bin/EphemeralMediaSessionCoordinator.d.ts +0 -228
  74. package/bin/EphemeralMediaSessionCoordinator.d.ts.map +0 -1
  75. package/bin/EphemeralMediaSessionCoordinator.js +0 -396
  76. package/bin/EphemeralMediaSessionCoordinator.js.map +0 -1
  77. package/bin/EphemeralMediaSessionCoordinatorSuspension.d.ts +0 -20
  78. package/bin/EphemeralMediaSessionCoordinatorSuspension.d.ts.map +0 -1
  79. package/bin/EphemeralMediaSessionCoordinatorSuspension.js +0 -28
  80. package/bin/EphemeralMediaSessionCoordinatorSuspension.js.map +0 -1
  81. package/bin/VolumeLimiter.d.ts +0 -70
  82. package/bin/VolumeLimiter.d.ts.map +0 -1
  83. package/bin/VolumeLimiter.js +0 -170
  84. package/bin/VolumeLimiter.js.map +0 -1
  85. package/src/test/GroupPlaybackPosition.spec.ts +0 -600
  86. package/src/test/GroupPlaybackTrack.spec.ts +0 -122
  87. package/src/test/SharedMediaSession.spec.ts +0 -149
  88. package/src/test/SharedMediaSessionCoordinator.spec.ts +0 -271
  89. package/src/test/VolumeManager.spec.ts +0 -219
@@ -1,469 +1,504 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.MediaPlayerSynchronizer = exports.MediaPlayerSynchronizerEvents = void 0;
16
- const events_1 = __importDefault(require("events"));
17
- const MediaSessionExtensions_1 = require("./MediaSessionExtensions");
18
- const VolumeManager_1 = require("./VolumeManager");
19
- const internals_1 = require("./internals");
20
- /**
21
- * Events supported by `MediaPlayerSynchronizer` object.
22
- */
23
- var MediaPlayerSynchronizerEvents;
24
- (function (MediaPlayerSynchronizerEvents) {
25
- /**
26
- *
27
- */
28
- MediaPlayerSynchronizerEvents["coordinatorstatechange"] = "coordinatorstatechange";
29
- MediaPlayerSynchronizerEvents["groupaction"] = "groupaction";
30
- MediaPlayerSynchronizerEvents["useraction"] = "useraction";
31
- })(MediaPlayerSynchronizerEvents = exports.MediaPlayerSynchronizerEvents || (exports.MediaPlayerSynchronizerEvents = {}));
32
- /**
33
- * Synchronizes a local HTML Media Element with a group of remote HTML Media Elements.
34
- *
35
- * @remarks
36
- * All of an apps transport control commands should be routed through the synchronizer. If the
37
- * app is not currently joined to the group media session, the commands will be applied directly
38
- * to the local player. When the group session is joined the commands will be broadcast to the
39
- * group in addition to being applied to the local player.
40
- */
41
- class MediaPlayerSynchronizer extends events_1.default {
42
- /**
43
- * Creates a new `MediaElementSynchronizer` instance.
44
- * @param player Media player element. This can be an HTML Media Element or any player that looks like an HTML Media Element.
45
- * @param mediaSession Group MediaSession object being used.
46
- * @param onEnd Optional. Function to call when synchronizers `end()` method is called.
47
- */
48
- constructor(player, mediaSession, onEnd) {
49
- super();
50
- this._viewOnly = false;
51
- this._expectedPlaybackState = "none";
52
- this._trackData = null;
53
- this._player = player;
54
- this._mediaSession = mediaSession;
55
- this._logger = mediaSession.logger;
56
- this._volumeManager = new VolumeManager_1.VolumeManager(player);
57
- this._onEnd = onEnd;
58
- // Listen for player state requests
59
- mediaSession.setRequestPlayerStateHandler(() => {
60
- // Find playback state
61
- let playbackState;
62
- const src = this._player.currentSrc || this._player.src;
63
- if (this._player.ended) {
64
- playbackState = "ended";
65
- }
66
- else if (!src ||
67
- (this._player.paused && this._player.currentTime < 1.0)) {
68
- playbackState = "none";
69
- }
70
- else if (this._player.paused) {
71
- playbackState = "paused";
72
- }
73
- else {
74
- playbackState = "playing";
75
- }
76
- const state = {
77
- metadata: { trackIdentifier: src },
78
- playbackState: playbackState,
79
- positionState: {
80
- position: this._player.currentTime,
81
- playbackRate: this._player.playbackRate,
82
- duration: this._player.duration,
83
- },
84
- trackData: this._trackData,
85
- };
86
- return state;
87
- });
88
- // Listen for player events
89
- this._onPlayerEvent = (evt) => {
90
- if (evt.type != "timeupdate") {
91
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.PlayerEvent, null, {
92
- type: evt.type,
93
- });
94
- }
95
- switch (evt.type) {
96
- case "loadedmetadata":
97
- // eslint-disable-next-line no-case-declarations
98
- const src = this._player.currentSrc || this._player.src;
99
- this._trackData = null;
100
- break;
101
- case "playing":
102
- // Handle case for YouTube player where user can pause/play video by clicking on it.
103
- // - Videos don't always start at 0.0 seconds.
104
- if (this._expectedPlaybackState != "playing") {
105
- if (this._mediaSession.coordinator.canPlayPause &&
106
- this._player.currentTime < 1.0) {
107
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
108
- .UserTappedVideoToPlay);
109
- this.play();
110
- }
111
- }
112
- // block play if player state is playing when expected synced state is paused and coordinator is not suspended.
113
- // needed because cannot tell if its a user initiated event, so disallow play
114
- if (this._expectedPlaybackState === "paused" &&
115
- !this._mediaSession.coordinator.isSuspended) {
116
- this._player.pause();
117
- }
118
- // block play if player state is playing when expected synced state is none and coordinator is not suspended.
119
- // needed because user who is not in control should not be able to start, so disallow play
120
- if (this._expectedPlaybackState === "none" &&
121
- !this._mediaSession.coordinator.isSuspended) {
122
- this._player.pause();
123
- }
124
- break;
125
- case "pause":
126
- // block pause if player state is paused when expected synced state is playing and coordinator is not suspended.
127
- // needed because cannot tell if its a user initiated event, so disallow pause
128
- if (this._expectedPlaybackState === "playing" &&
129
- !this._mediaSession.coordinator.isSuspended) {
130
- this._player.play();
131
- }
132
- break;
133
- case "ratechange":
134
- // Block rate changes unless suspended.
135
- if (this._player.playbackRate != 1.0 &&
136
- !this._mediaSession.coordinator.isSuspended) {
137
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
138
- .PlaybackRateChangeBlocked);
139
- this._player.playbackRate = 1.0;
140
- }
141
- break;
142
- case "blocked":
143
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
144
- .PlaybackRateChangeBlocked);
145
- break;
146
- }
147
- };
148
- // Register for coordinator state changes
149
- this._mediaSession.coordinator.on(MediaSessionExtensions_1.MediaSessionCoordinatorEvents.coordinatorstatechange, (evt) => this.emit(evt.type, evt));
150
- // Register media session actions
151
- for (const action of MediaPlayerSynchronizer.SESSION_ACTIONS) {
152
- this._mediaSession.setActionHandler(action, (details) => __awaiter(this, void 0, void 0, function* () {
153
- var _a;
154
- let error;
155
- try {
156
- switch (details.action) {
157
- case "play":
158
- this._expectedPlaybackState = "playing";
159
- if (this._player.paused) {
160
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
161
- .PlayAction);
162
- if (typeof details.seekTime == "number" &&
163
- this._player.currentTime < 1.0) {
164
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents
165
- .MediaPlayerSynchronizer
166
- .SeekingPlayerToStartPosition, null, { position: details.seekTime });
167
- this._player.currentTime =
168
- details.seekTime;
169
- }
170
- // Reference: https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/play#exceptions
171
- yield this._player.play();
172
- }
173
- break;
174
- case "pause":
175
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
176
- .PauseAction);
177
- this._expectedPlaybackState = "paused";
178
- if (typeof details.seekTime == "number" &&
179
- this._player.currentTime < 1.0) {
180
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
181
- .SeekingPlayerToStartPosition, null, { position: details.seekTime });
182
- this._player.currentTime =
183
- details.seekTime;
184
- }
185
- this._player.pause();
186
- break;
187
- case "seekto":
188
- if (typeof details.seekTime == "number") {
189
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
190
- .SeekToAction, null, { position: details.seekTime });
191
- this._player.currentTime =
192
- details.seekTime;
193
- }
194
- break;
195
- case "settrack":
196
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
197
- .SetTrackAction);
198
- this._expectedPlaybackState = "none";
199
- this._player.src =
200
- details.metadata.trackIdentifier;
201
- this._player.load();
202
- break;
203
- case "datachange":
204
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
205
- .DataChangeAction);
206
- break;
207
- case "catchup":
208
- if (typeof details.seekTime == "number") {
209
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
210
- .CatchupAction, null, { position: details.seekTime });
211
- this.catchupPlayer(details.seekTime);
212
- }
213
- break;
214
- }
215
- }
216
- catch (err) {
217
- if (err instanceof Error) {
218
- error = err;
219
- }
220
- else {
221
- error = new Error((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : "An unknown error occurred after processing the group session action.");
222
- }
223
- }
224
- this.dispatchGroupAction(details, false, error);
225
- }));
226
- }
227
- // Subscribe to player events
228
- for (const event of MediaPlayerSynchronizer.PLAYER_EVENTS) {
229
- this._player.addEventListener(event, this._onPlayerEvent);
230
- }
231
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.SynchronizationStarted);
232
- }
233
- /**
234
- * Media player being synchronized.
235
- */
236
- get player() {
237
- return this._player;
238
- }
239
- /**
240
- * Synchronizers media session.
241
- */
242
- get mediaSession() {
243
- return this._mediaSession;
244
- }
245
- /**
246
- * If true the client is in a view only mode.
247
- *
248
- * @remarks
249
- * Toggling this value to true results in `mediaSession.coordinator.canPlayPause`,
250
- * `mediaSession.coordinator.canSeek`, `mediaSession.coordinator.canSetTrack`, and
251
- * `mediaSession.coordinator.canSetTrackData` all being set to false. For more fine
252
- * grained control over the local clients policies, call the `mediaSession.coordinator`
253
- * directly.
254
- */
255
- get viewOnly() {
256
- return this._viewOnly;
257
- }
258
- set viewOnly(value) {
259
- this._viewOnly = value;
260
- this.mediaSession.coordinator.canPlayPause = !value;
261
- this.mediaSession.coordinator.canSeek = !value;
262
- this.mediaSession.coordinator.canSetTrack = !value;
263
- this.mediaSession.coordinator.canSetTrackData = !value;
264
- }
265
- /**
266
- * Volume limiter used to temporarily reduce the videos volume when someone speaks in a meeting.
267
- */
268
- get volumeManager() {
269
- return this._volumeManager;
270
- }
271
- /**
272
- * Registers a new event listener.
273
- * @param event Name of the event to add.
274
- * @param listener Function to call when the event is triggered.
275
- */
276
- addEventListener(event, listener) {
277
- this.on(event, listener);
278
- return this;
279
- }
280
- /**
281
- * Un-registers an existing event listener.
282
- * @param event Name of the event to remove.
283
- * @param listener Function that was registered in call to `addEventListener()`.
284
- */
285
- removeEventListener(event, listener) {
286
- this.off(event, listener);
287
- return this;
288
- }
289
- /**
290
- * Ends synchronization of the current media player.
291
- */
292
- end() {
293
- if (this._onEnd) {
294
- try {
295
- // Clear media session actions
296
- for (const action of MediaPlayerSynchronizer.SESSION_ACTIONS) {
297
- this._mediaSession.setActionHandler(action, null);
298
- }
299
- // Subscribe to player events
300
- for (const event of MediaPlayerSynchronizer.PLAYER_EVENTS) {
301
- this._player.removeEventListener(event, this._onPlayerEvent);
302
- }
303
- // Notify parent
304
- this._onEnd();
305
- }
306
- finally {
307
- this._onEnd = undefined;
308
- }
309
- }
310
- }
311
- /**
312
- * Begin a local seek operation.
313
- *
314
- * @remarks
315
- * UI can call this when a user grabs a timeline scrubber and starts scrubbing the video to a
316
- * new playback position. The synchronizer will being a new suspension which temporarily
317
- * disconnects the client for the rest of the group for synchronization purposes. Calling
318
- * `endSeek()` will end the suspension and seek the group to the users final seek position.
319
- */
320
- beginSeek() {
321
- if (this._seekSuspension) {
322
- throw new Error(`MediaPlayerSynchronizer: cannot begin seek. A seek is already in progress.`);
323
- }
324
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.BeginSeekCalled);
325
- this._seekSuspension = this._mediaSession.coordinator.beginSuspension();
326
- }
327
- /**
328
- * Ends a seek operation that was started by calling `beginSeek()`.
329
- * @param seekTo Playback position in seconds to seek the group to.
330
- */
331
- endSeek(seekTo) {
332
- if (!this._seekSuspension) {
333
- throw new Error(`MediaPlayerSynchronizer: cannot end seek. No seek is in progress.`);
334
- }
335
- // Seek player to new time immediately.
336
- // - This resolves an issue where the timeline scrubber can temporarily snap back to the original
337
- // location.
338
- this._player.currentTime = seekTo;
339
- const suspension = this._seekSuspension;
340
- this._seekSuspension = undefined;
341
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.EndSeekCalled, null, {
342
- position: seekTo,
343
- });
344
- suspension.end(seekTo);
345
- this.dispatchUserAction({ action: "seekto", seekTime: seekTo });
346
- }
347
- /**
348
- * Tells the group to begin playing the current video.
349
- *
350
- * @remarks
351
- * For proper operation apps should avoid calling `mediaSession.coordinator.play()` directly
352
- * and instead use the synchronizers `play()` method.
353
- */
354
- play() {
355
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.PlayCalled);
356
- this._expectedPlaybackState = "playing";
357
- this._mediaSession.coordinator.play();
358
- this.dispatchUserAction({ action: "play" });
359
- }
360
- /**
361
- * Tells the group to pause the current video.
362
- *
363
- * @remarks
364
- * For proper operation apps should avoid calling `mediaSession.coordinator.pause()` directly
365
- * and instead use the synchronizers `pause()` method.
366
- */
367
- pause() {
368
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.PauseCalled);
369
- this._expectedPlaybackState = "paused";
370
- this._mediaSession.coordinator.pause();
371
- this.dispatchUserAction({ action: "pause" });
372
- }
373
- /**
374
- * Tells the group to seek the current video to a new playback position.
375
- *
376
- * @remarks
377
- * For proper operation apps should avoid calling `mediaSession.coordinator.seekTo()` directly
378
- * and instead use the synchronizers `seekTo()` method.
379
- */
380
- seekTo(time) {
381
- // Always seek player to new time.
382
- // - This resolves an issue where the timeline scrubber can temporarily snap back to the original
383
- // location.
384
- this._player.currentTime = time;
385
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.SeekToCalled, null, { position: time });
386
- this._mediaSession.coordinator.seekTo(time);
387
- this.dispatchUserAction({ action: "seekto", seekTime: time });
388
- }
389
- /**
390
- * Tells the group to change to a new track.
391
- *
392
- * @remarks
393
- * For proper operation apps should avoid calling `mediaSession.coordinator.setTrack()` directly
394
- * and instead use the synchronizers `setTrack()` method.
395
- */
396
- setTrack(track, waitPoints) {
397
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.SetTrackCalled);
398
- this._mediaSession.coordinator.setTrack(track, waitPoints);
399
- this.dispatchUserAction({ action: "settrack", metadata: track });
400
- }
401
- /**
402
- * Updates the current tracks data object.
403
- *
404
- * @remarks
405
- * For proper operation apps should avoid calling `mediaSession.coordinator.setTrackData()` directly
406
- * and instead use the synchronizers `setTrackData()` method.
407
- */
408
- setTrackData(data) {
409
- this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.SetTrackDataCalled);
410
- this._trackData = data;
411
- this._mediaSession.coordinator.setTrackData(data);
412
- this.dispatchUserAction({ action: "datachange", data: data });
413
- }
414
- dispatchGroupAction(details, delay = false, error) {
415
- if (delay) {
416
- setTimeout(() => this.emit(MediaPlayerSynchronizerEvents.groupaction, {
417
- type: MediaPlayerSynchronizerEvents.groupaction,
418
- details: details,
419
- playerError: error,
420
- }), 50);
421
- }
422
- else {
423
- this.emit(MediaPlayerSynchronizerEvents.groupaction, {
424
- type: MediaPlayerSynchronizerEvents.groupaction,
425
- details: details,
426
- error,
427
- });
428
- }
429
- }
430
- dispatchUserAction(details, delay = false) {
431
- if (delay) {
432
- setTimeout(() => this.emit(MediaPlayerSynchronizerEvents.useraction, {
433
- type: MediaPlayerSynchronizerEvents.useraction,
434
- details: details,
435
- }), 50);
436
- }
437
- else {
438
- this.emit(MediaPlayerSynchronizerEvents.useraction, {
439
- type: MediaPlayerSynchronizerEvents.useraction,
440
- details: details,
441
- });
442
- }
443
- }
444
- catchupPlayer(time) {
445
- return __awaiter(this, void 0, void 0, function* () {
446
- this._player.currentTime = time;
447
- });
448
- }
449
- }
450
- exports.MediaPlayerSynchronizer = MediaPlayerSynchronizer;
451
- MediaPlayerSynchronizer.SESSION_ACTIONS = [
452
- "play",
453
- "pause",
454
- "seekto",
455
- "settrack",
456
- "datachange",
457
- "catchup",
458
- "wait",
459
- ];
460
- MediaPlayerSynchronizer.PLAYER_EVENTS = [
461
- "playing",
462
- "pause",
463
- "ratechange",
464
- "timeupdate",
465
- "ended",
466
- "loadedmetadata",
467
- "blocked",
468
- ];
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.MediaPlayerSynchronizer = exports.MediaPlayerSynchronizerEvents = void 0;
16
+ const events_1 = __importDefault(require("events"));
17
+ const MediaSessionExtensions_1 = require("./MediaSessionExtensions");
18
+ const VolumeManager_1 = require("./VolumeManager");
19
+ const internals_1 = require("./internals");
20
+ /**
21
+ * Events supported by `MediaPlayerSynchronizer` object.
22
+ */
23
+ var MediaPlayerSynchronizerEvents;
24
+ (function (MediaPlayerSynchronizerEvents) {
25
+ /**
26
+ *
27
+ */
28
+ MediaPlayerSynchronizerEvents["coordinatorstatechange"] = "coordinatorstatechange";
29
+ MediaPlayerSynchronizerEvents["groupaction"] = "groupaction";
30
+ MediaPlayerSynchronizerEvents["useraction"] = "useraction";
31
+ })(MediaPlayerSynchronizerEvents = exports.MediaPlayerSynchronizerEvents || (exports.MediaPlayerSynchronizerEvents = {}));
32
+ /**
33
+ * Synchronizes a local HTML Media Element with a group of remote HTML Media Elements.
34
+ *
35
+ * @remarks
36
+ * All of an apps transport control commands should be routed through the synchronizer. If the
37
+ * app is not currently joined to the group media session, the commands will be applied directly
38
+ * to the local player. When the group session is joined the commands will be broadcast to the
39
+ * group in addition to being applied to the local player.
40
+ */
41
+ class MediaPlayerSynchronizer extends events_1.default {
42
+ /**
43
+ * Creates a new `MediaElementSynchronizer` instance.
44
+ * @param player Media player element. This can be an HTML Media Element or any player that looks like an HTML Media Element.
45
+ * @param mediaSession Group MediaSession object being used.
46
+ * @param onEnd Optional. Function to call when synchronizers `end()` method is called.
47
+ */
48
+ constructor(player, mediaSession, onEnd) {
49
+ super();
50
+ this._viewOnly = false;
51
+ this._expectedPlaybackState = "none";
52
+ this._metadata = null;
53
+ this._trackData = null;
54
+ this._player = player;
55
+ this._mediaSession = mediaSession;
56
+ this._logger = mediaSession.logger;
57
+ this._volumeManager = new VolumeManager_1.VolumeManager(player);
58
+ this._onEnd = onEnd;
59
+ // Listen for player state requests
60
+ mediaSession.setRequestPlayerStateHandler(() => {
61
+ // Find playback state
62
+ let playbackState;
63
+ const src = this._player.currentSrc || this._player.src;
64
+ if (this._player.ended) {
65
+ playbackState = "ended";
66
+ }
67
+ else if (!src ||
68
+ (this._player.paused && this._player.currentTime < 1.0)) {
69
+ playbackState = "none";
70
+ }
71
+ else if (this._player.paused) {
72
+ playbackState = "paused";
73
+ }
74
+ else {
75
+ playbackState = "playing";
76
+ }
77
+ // Metadata equality check in `GroupPlaybackTrack` would return false if only providing src.
78
+ // Use full metadata if this._metadata has same src
79
+ const metadata = this._metadata && this._metadata.trackIdentifier === src
80
+ ? this._metadata
81
+ : { trackIdentifier: src };
82
+ const state = {
83
+ metadata: metadata,
84
+ playbackState: playbackState,
85
+ positionState: {
86
+ position: this._player.currentTime,
87
+ playbackRate: this._player.playbackRate,
88
+ duration: this._player.duration,
89
+ },
90
+ trackData: this._trackData,
91
+ };
92
+ return state;
93
+ });
94
+ // Listen for player events
95
+ this._onPlayerEvent = (evt) => {
96
+ if (evt.type != "timeupdate") {
97
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.PlayerEvent, null, {
98
+ type: evt.type,
99
+ });
100
+ }
101
+ switch (evt.type) {
102
+ case "loadedmetadata":
103
+ // eslint-disable-next-line no-case-declarations
104
+ const src = this._player.currentSrc || this._player.src;
105
+ this._trackData = null;
106
+ break;
107
+ case "playing":
108
+ // Handle case for YouTube player where user can pause/play video by clicking on it.
109
+ // - Videos don't always start at 0.0 seconds.
110
+ if (this._expectedPlaybackState != "playing") {
111
+ if (this._mediaSession.coordinator.canPlayPause &&
112
+ this._player.currentTime < 1.0) {
113
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
114
+ .UserTappedVideoToPlay);
115
+ this.play().catch((err) => {
116
+ this._logger.sendErrorEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
117
+ .UserTappedVideoToPlayError, err);
118
+ });
119
+ }
120
+ }
121
+ // block play if player state is playing when expected synced state is paused and coordinator is not suspended.
122
+ // needed because cannot tell if its a user initiated event, so disallow play
123
+ if (this._expectedPlaybackState === "paused" &&
124
+ !this._mediaSession.coordinator.isSuspended) {
125
+ this._player.pause();
126
+ }
127
+ // block play if player state is playing when expected synced state is none and coordinator is not suspended.
128
+ // needed because user who is not in control should not be able to start, so disallow play
129
+ if (this._expectedPlaybackState === "none" &&
130
+ !this._mediaSession.coordinator.isSuspended) {
131
+ this._player.pause();
132
+ }
133
+ break;
134
+ case "pause":
135
+ // block pause if player state is paused when expected synced state is playing and coordinator is not suspended.
136
+ // needed because cannot tell if its a user initiated event, so disallow pause
137
+ if (this._expectedPlaybackState === "playing" &&
138
+ !this._mediaSession.coordinator.isSuspended) {
139
+ this._player.play();
140
+ }
141
+ break;
142
+ case "ratechange":
143
+ // Block rate changes unless suspended.
144
+ if (this._player.playbackRate != 1.0 &&
145
+ !this._mediaSession.coordinator.isSuspended) {
146
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
147
+ .PlaybackRateChangeBlocked);
148
+ this._player.playbackRate = 1.0;
149
+ }
150
+ break;
151
+ case "blocked":
152
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
153
+ .PlaybackRateChangeBlocked);
154
+ break;
155
+ }
156
+ };
157
+ // Register for coordinator state changes
158
+ this._mediaSession.coordinator.on(MediaSessionExtensions_1.MediaSessionCoordinatorEvents.coordinatorstatechange, (evt) => this.emit(evt.type, evt));
159
+ // Register media session actions
160
+ for (const action of MediaPlayerSynchronizer.SESSION_ACTIONS) {
161
+ this._mediaSession.setActionHandler(action, (details) => __awaiter(this, void 0, void 0, function* () {
162
+ var _a, _b, _c;
163
+ let error;
164
+ try {
165
+ switch (details.action) {
166
+ case "play":
167
+ this._expectedPlaybackState = "playing";
168
+ if (this._player.paused) {
169
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
170
+ .PlayAction);
171
+ if (typeof details.seekTime == "number" &&
172
+ this._player.currentTime < 1.0) {
173
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents
174
+ .MediaPlayerSynchronizer
175
+ .SeekingPlayerToStartPosition, null, { position: details.seekTime });
176
+ this._player.currentTime =
177
+ details.seekTime;
178
+ }
179
+ // Reference: https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/play#exceptions
180
+ yield this._player.play();
181
+ }
182
+ break;
183
+ case "pause":
184
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
185
+ .PauseAction);
186
+ this._expectedPlaybackState = "paused";
187
+ if (typeof details.seekTime == "number" &&
188
+ this._player.currentTime < 1.0) {
189
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
190
+ .SeekingPlayerToStartPosition, null, { position: details.seekTime });
191
+ this._player.currentTime =
192
+ details.seekTime;
193
+ }
194
+ this._player.pause();
195
+ break;
196
+ case "seekto":
197
+ if (typeof details.seekTime == "number") {
198
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
199
+ .SeekToAction, null, { position: details.seekTime });
200
+ this._player.currentTime =
201
+ details.seekTime;
202
+ }
203
+ break;
204
+ case "settrack":
205
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
206
+ .SetTrackAction);
207
+ this._expectedPlaybackState = "none";
208
+ this._metadata = (_a = details.metadata) !== null && _a !== void 0 ? _a : null;
209
+ if (this._player.src ===
210
+ ((_b = details.metadata) === null || _b === void 0 ? void 0 : _b.trackIdentifier)) {
211
+ // Don't set duplicate src and load again.
212
+ break;
213
+ }
214
+ this._player.src =
215
+ details.metadata.trackIdentifier;
216
+ this._player.load();
217
+ break;
218
+ case "datachange":
219
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
220
+ .DataChangeAction);
221
+ break;
222
+ case "catchup":
223
+ if (typeof details.seekTime == "number") {
224
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer
225
+ .CatchupAction, null, { position: details.seekTime });
226
+ this.catchupPlayer(details.seekTime);
227
+ }
228
+ break;
229
+ }
230
+ }
231
+ catch (err) {
232
+ if (err instanceof Error) {
233
+ error = err;
234
+ }
235
+ else {
236
+ error = new Error((_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : "An unknown error occurred after processing the group session action.");
237
+ }
238
+ }
239
+ this.dispatchGroupAction(details, false, error);
240
+ }));
241
+ }
242
+ // Subscribe to player events
243
+ for (const event of MediaPlayerSynchronizer.PLAYER_EVENTS) {
244
+ this._player.addEventListener(event, this._onPlayerEvent);
245
+ }
246
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.SynchronizationStarted);
247
+ }
248
+ /**
249
+ * Media player being synchronized.
250
+ */
251
+ get player() {
252
+ return this._player;
253
+ }
254
+ /**
255
+ * Synchronizers media session.
256
+ */
257
+ get mediaSession() {
258
+ return this._mediaSession;
259
+ }
260
+ /**
261
+ * If true the client is in a view only mode.
262
+ *
263
+ * @remarks
264
+ * Toggling this value to true results in `mediaSession.coordinator.canPlayPause`,
265
+ * `mediaSession.coordinator.canSeek`, `mediaSession.coordinator.canSetTrack`, and
266
+ * `mediaSession.coordinator.canSetTrackData` all being set to false. For more fine
267
+ * grained control over the local clients policies, call the `mediaSession.coordinator`
268
+ * directly.
269
+ */
270
+ get viewOnly() {
271
+ return this._viewOnly;
272
+ }
273
+ set viewOnly(value) {
274
+ this._viewOnly = value;
275
+ this.mediaSession.coordinator.canPlayPause = !value;
276
+ this.mediaSession.coordinator.canSeek = !value;
277
+ this.mediaSession.coordinator.canSetTrack = !value;
278
+ this.mediaSession.coordinator.canSetTrackData = !value;
279
+ }
280
+ /**
281
+ * Volume limiter used to temporarily reduce the videos volume when someone speaks in a meeting.
282
+ */
283
+ get volumeManager() {
284
+ return this._volumeManager;
285
+ }
286
+ /**
287
+ * Registers a new event listener.
288
+ * @param event Name of the event to add.
289
+ * @param listener Function to call when the event is triggered.
290
+ */
291
+ addEventListener(event, listener) {
292
+ this.on(event, listener);
293
+ return this;
294
+ }
295
+ /**
296
+ * Un-registers an existing event listener.
297
+ * @param event Name of the event to remove.
298
+ * @param listener Function that was registered in call to `addEventListener()`.
299
+ */
300
+ removeEventListener(event, listener) {
301
+ this.off(event, listener);
302
+ return this;
303
+ }
304
+ /**
305
+ * Ends synchronization of the current media player.
306
+ */
307
+ end() {
308
+ if (this._onEnd) {
309
+ try {
310
+ // Clear media session actions
311
+ for (const action of MediaPlayerSynchronizer.SESSION_ACTIONS) {
312
+ this._mediaSession.setActionHandler(action, null);
313
+ }
314
+ // Subscribe to player events
315
+ for (const event of MediaPlayerSynchronizer.PLAYER_EVENTS) {
316
+ this._player.removeEventListener(event, this._onPlayerEvent);
317
+ }
318
+ // Notify parent
319
+ this._onEnd();
320
+ }
321
+ finally {
322
+ this._onEnd = undefined;
323
+ }
324
+ }
325
+ }
326
+ /**
327
+ * Begin a local seek operation.
328
+ *
329
+ * @remarks
330
+ * UI can call this when a user grabs a timeline scrubber and starts scrubbing the video to a
331
+ * new playback position. The synchronizer will being a new suspension which temporarily
332
+ * disconnects the client for the rest of the group for synchronization purposes. Calling
333
+ * `endSeek()` will end the suspension and seek the group to the users final seek position.
334
+ */
335
+ beginSeek() {
336
+ if (this._seekSuspension) {
337
+ throw new Error(`MediaPlayerSynchronizer: cannot begin seek. A seek is already in progress.`);
338
+ }
339
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.BeginSeekCalled);
340
+ this._seekSuspension = this._mediaSession.coordinator.beginSuspension();
341
+ }
342
+ /**
343
+ * Ends a seek operation that was started by calling `beginSeek()`.
344
+ * @param seekTo Playback position in seconds to seek the group to.
345
+ */
346
+ endSeek(seekTo) {
347
+ if (!this._seekSuspension) {
348
+ throw new Error(`MediaPlayerSynchronizer: cannot end seek. No seek is in progress.`);
349
+ }
350
+ // Seek player to new time immediately.
351
+ // - This resolves an issue where the timeline scrubber can temporarily snap back to the original
352
+ // location.
353
+ this._player.currentTime = seekTo;
354
+ const suspension = this._seekSuspension;
355
+ this._seekSuspension = undefined;
356
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.EndSeekCalled, null, {
357
+ position: seekTo,
358
+ });
359
+ suspension.end(seekTo);
360
+ this.dispatchUserAction({ action: "seekto", seekTime: seekTo });
361
+ }
362
+ /**
363
+ * Tells the group to begin playing the current video.
364
+ *
365
+ * @remarks
366
+ * For proper operation apps should avoid calling `mediaSession.coordinator.play()` directly
367
+ * and instead use the synchronizers `play()` method.
368
+ *
369
+ * @returns a void promise that resolves once complete, throws if user does not have proper roles
370
+ */
371
+ play() {
372
+ return __awaiter(this, void 0, void 0, function* () {
373
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.PlayCalled);
374
+ this._expectedPlaybackState = "playing";
375
+ yield this._mediaSession.coordinator.play();
376
+ this.dispatchUserAction({ action: "play" });
377
+ });
378
+ }
379
+ /**
380
+ * Tells the group to pause the current video.
381
+ *
382
+ * @remarks
383
+ * For proper operation apps should avoid calling `mediaSession.coordinator.pause()` directly
384
+ * and instead use the synchronizers `pause()` method.
385
+ *
386
+ * @returns a void promise that resolves once complete, throws if user does not have proper roles
387
+ */
388
+ pause() {
389
+ return __awaiter(this, void 0, void 0, function* () {
390
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.PauseCalled);
391
+ this._expectedPlaybackState = "paused";
392
+ yield this._mediaSession.coordinator.pause();
393
+ this.dispatchUserAction({ action: "pause" });
394
+ });
395
+ }
396
+ /**
397
+ * Tells the group to seek the current video to a new playback position.
398
+ *
399
+ * @remarks
400
+ * For proper operation apps should avoid calling `mediaSession.coordinator.seekTo()` directly
401
+ * and instead use the synchronizers `seekTo()` method.
402
+ *
403
+ * @returns a void promise that resolves once complete, throws if user does not have proper roles
404
+ */
405
+ seekTo(time) {
406
+ return __awaiter(this, void 0, void 0, function* () {
407
+ // Always seek player to new time.
408
+ // - This resolves an issue where the timeline scrubber can temporarily snap back to the original
409
+ // location.
410
+ this._player.currentTime = time;
411
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.SeekToCalled, null, { position: time });
412
+ yield this._mediaSession.coordinator.seekTo(time);
413
+ this.dispatchUserAction({ action: "seekto", seekTime: time });
414
+ });
415
+ }
416
+ /**
417
+ * Tells the group to change to a new track.
418
+ *
419
+ * @remarks
420
+ * For proper operation apps should avoid calling `mediaSession.coordinator.setTrack()` directly
421
+ * and instead use the synchronizers `setTrack()` method.
422
+ *
423
+ * @returns a void promise that resolves once complete, throws if user does not have proper roles
424
+ */
425
+ setTrack(track, waitPoints) {
426
+ return __awaiter(this, void 0, void 0, function* () {
427
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.SetTrackCalled);
428
+ yield this._mediaSession.coordinator.setTrack(track, waitPoints);
429
+ this.dispatchUserAction({ action: "settrack", metadata: track });
430
+ });
431
+ }
432
+ /**
433
+ * Updates the current tracks data object.
434
+ *
435
+ * @remarks
436
+ * For proper operation apps should avoid calling `mediaSession.coordinator.setTrackData()` directly
437
+ * and instead use the synchronizers `setTrackData()` method.
438
+ *
439
+ * @returns a void promise that resolves once complete, throws if user does not have proper roles
440
+ */
441
+ setTrackData(data) {
442
+ return __awaiter(this, void 0, void 0, function* () {
443
+ this._logger.sendTelemetryEvent(internals_1.TelemetryEvents.MediaPlayerSynchronizer.SetTrackDataCalled);
444
+ this._trackData = data;
445
+ yield this._mediaSession.coordinator.setTrackData(data);
446
+ this.dispatchUserAction({ action: "datachange", data: data });
447
+ });
448
+ }
449
+ dispatchGroupAction(details, delay = false, error) {
450
+ if (delay) {
451
+ setTimeout(() => this.emit(MediaPlayerSynchronizerEvents.groupaction, {
452
+ type: MediaPlayerSynchronizerEvents.groupaction,
453
+ details: details,
454
+ playerError: error,
455
+ }), 50);
456
+ }
457
+ else {
458
+ this.emit(MediaPlayerSynchronizerEvents.groupaction, {
459
+ type: MediaPlayerSynchronizerEvents.groupaction,
460
+ details: details,
461
+ error,
462
+ });
463
+ }
464
+ }
465
+ dispatchUserAction(details, delay = false) {
466
+ if (delay) {
467
+ setTimeout(() => this.emit(MediaPlayerSynchronizerEvents.useraction, {
468
+ type: MediaPlayerSynchronizerEvents.useraction,
469
+ details: details,
470
+ }), 50);
471
+ }
472
+ else {
473
+ this.emit(MediaPlayerSynchronizerEvents.useraction, {
474
+ type: MediaPlayerSynchronizerEvents.useraction,
475
+ details: details,
476
+ });
477
+ }
478
+ }
479
+ catchupPlayer(time) {
480
+ return __awaiter(this, void 0, void 0, function* () {
481
+ this._player.currentTime = time;
482
+ });
483
+ }
484
+ }
485
+ MediaPlayerSynchronizer.SESSION_ACTIONS = [
486
+ "play",
487
+ "pause",
488
+ "seekto",
489
+ "settrack",
490
+ "datachange",
491
+ "catchup",
492
+ "wait",
493
+ ];
494
+ MediaPlayerSynchronizer.PLAYER_EVENTS = [
495
+ "playing",
496
+ "pause",
497
+ "ratechange",
498
+ "timeupdate",
499
+ "ended",
500
+ "loadedmetadata",
501
+ "blocked",
502
+ ];
503
+ exports.MediaPlayerSynchronizer = MediaPlayerSynchronizer;
469
504
  //# sourceMappingURL=MediaPlayerSynchronizer.js.map