@microsoft/live-share-media 1.0.0-preview.1 → 1.0.0-preview.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/bin/IMediaPlayer.d.ts.map +1 -1
- package/bin/LiveMediaSession.d.ts +7 -7
- package/bin/LiveMediaSession.d.ts.map +1 -1
- package/bin/LiveMediaSession.js +28 -17
- package/bin/LiveMediaSession.js.map +1 -1
- package/bin/LiveMediaSessionCoordinator.d.ts +2 -2
- package/bin/LiveMediaSessionCoordinator.d.ts.map +1 -1
- package/bin/LiveMediaSessionCoordinator.js +34 -22
- package/bin/LiveMediaSessionCoordinator.js.map +1 -1
- package/bin/LiveMediaSessionCoordinatorSuspension.d.ts +1 -1
- package/bin/LiveMediaSessionCoordinatorSuspension.d.ts.map +1 -1
- package/bin/LiveMediaSessionCoordinatorSuspension.js.map +1 -1
- package/bin/MediaPlayerSynchronizer.d.ts +7 -7
- package/bin/MediaPlayerSynchronizer.d.ts.map +1 -1
- package/bin/MediaPlayerSynchronizer.js +127 -72
- package/bin/MediaPlayerSynchronizer.js.map +1 -1
- package/bin/MediaSessionActionThrottler.d.ts +1 -1
- package/bin/MediaSessionActionThrottler.d.ts.map +1 -1
- package/bin/MediaSessionActionThrottler.js.map +1 -1
- package/bin/MediaSessionExtensions.d.ts.map +1 -1
- package/bin/RepeatedActionThrottler.d.ts +2 -2
- package/bin/RepeatedActionThrottler.d.ts.map +1 -1
- package/bin/RepeatedActionThrottler.js +9 -3
- package/bin/RepeatedActionThrottler.js.map +1 -1
- package/bin/VolumeManager.d.ts +1 -1
- package/bin/VolumeManager.d.ts.map +1 -1
- package/bin/VolumeManager.js +4 -2
- package/bin/VolumeManager.js.map +1 -1
- package/bin/index.d.ts +9 -9
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js.map +1 -1
- package/bin/internals/GroupCoordinatorState.d.ts +8 -8
- package/bin/internals/GroupCoordinatorState.d.ts.map +1 -1
- package/bin/internals/GroupCoordinatorState.js +91 -53
- package/bin/internals/GroupCoordinatorState.js.map +1 -1
- package/bin/internals/GroupPlaybackPosition.d.ts +3 -3
- package/bin/internals/GroupPlaybackPosition.d.ts.map +1 -1
- package/bin/internals/GroupPlaybackPosition.js +22 -13
- package/bin/internals/GroupPlaybackPosition.js.map +1 -1
- package/bin/internals/GroupPlaybackTrack.d.ts +4 -4
- package/bin/internals/GroupPlaybackTrack.d.ts.map +1 -1
- package/bin/internals/GroupPlaybackTrack.js +14 -4
- package/bin/internals/GroupPlaybackTrack.js.map +1 -1
- package/bin/internals/GroupPlaybackTrackData.d.ts +3 -3
- package/bin/internals/GroupPlaybackTrackData.d.ts.map +1 -1
- package/bin/internals/GroupPlaybackTrackData.js +8 -4
- package/bin/internals/GroupPlaybackTrackData.js.map +1 -1
- package/bin/internals/GroupTransportState.d.ts +5 -5
- package/bin/internals/GroupTransportState.d.ts.map +1 -1
- package/bin/internals/GroupTransportState.js +27 -12
- package/bin/internals/GroupTransportState.js.map +1 -1
- package/bin/internals/consts.d.ts.map +1 -1
- package/bin/internals/consts.js +54 -54
- package/bin/internals/consts.js.map +1 -1
- package/bin/internals/index.d.ts +6 -6
- package/package.json +3 -3
- package/src/IMediaPlayer.ts +10 -2
- package/src/LiveMediaSession.ts +133 -71
- package/src/LiveMediaSessionCoordinator.ts +302 -142
- package/src/LiveMediaSessionCoordinatorSuspension.ts +13 -5
- package/src/MediaPlayerSynchronizer.ts +329 -149
- package/src/MediaSessionActionThrottler.ts +5 -3
- package/src/MediaSessionExtensions.ts +15 -5
- package/src/RepeatedActionThrottler.ts +27 -9
- package/src/VolumeManager.ts +24 -12
- package/src/index.ts +9 -10
- package/src/internals/GroupCoordinatorState.ts +274 -114
- package/src/internals/GroupPlaybackPosition.ts +61 -27
- package/src/internals/GroupPlaybackTrack.ts +51 -22
- package/src/internals/GroupPlaybackTrackData.ts +24 -16
- package/src/internals/GroupTransportState.ts +53 -23
- package/src/internals/consts.ts +73 -55
- package/src/internals/index.ts +6 -6
- package/src/test/GroupPlaybackPosition.spec.ts +371 -111
- package/src/test/GroupPlaybackTrack.spec.ts +91 -35
- package/src/test/SharedMediaSession.spec.ts +64 -28
- package/src/test/SharedMediaSessionCoordinator.spec.ts +86 -46
- package/src/test/VolumeManager.spec.ts +38 -31
- package/tsconfig.json +1 -0
- package/bin/EphemeralMediaSession.d.ts +0 -102
- package/bin/EphemeralMediaSession.d.ts.map +0 -1
- package/bin/EphemeralMediaSession.js +0 -226
- package/bin/EphemeralMediaSession.js.map +0 -1
- package/bin/EphemeralMediaSessionCoordinator.d.ts +0 -228
- package/bin/EphemeralMediaSessionCoordinator.d.ts.map +0 -1
- package/bin/EphemeralMediaSessionCoordinator.js +0 -396
- package/bin/EphemeralMediaSessionCoordinator.js.map +0 -1
- package/bin/EphemeralMediaSessionCoordinatorSuspension.d.ts +0 -20
- package/bin/EphemeralMediaSessionCoordinatorSuspension.d.ts.map +0 -1
- package/bin/EphemeralMediaSessionCoordinatorSuspension.js +0 -28
- package/bin/EphemeralMediaSessionCoordinatorSuspension.js.map +0 -1
- package/bin/VolumeLimiter.d.ts +0 -70
- package/bin/VolumeLimiter.d.ts.map +0 -1
- package/bin/VolumeLimiter.js +0 -170
- package/bin/VolumeLimiter.js.map +0 -1
|
@@ -2,14 +2,21 @@
|
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Licensed under the Microsoft Live Share SDK License.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
import { LiveTelemetryLogger, IEvent } from "@microsoft/live-share";
|
|
6
|
+
import EventEmitter from "events";
|
|
7
|
+
import {
|
|
8
|
+
ExtendedMediaSessionAction,
|
|
9
|
+
ExtendedMediaSessionPlaybackState,
|
|
10
|
+
ExtendedMediaMetadata,
|
|
11
|
+
CoordinationWaitPoint,
|
|
12
|
+
ExtendedMediaSessionActionDetails,
|
|
13
|
+
MediaSessionCoordinatorEvents,
|
|
14
|
+
MediaSessionCoordinatorSuspension,
|
|
15
|
+
} from "./MediaSessionExtensions";
|
|
16
|
+
import { VolumeManager } from "./VolumeManager";
|
|
17
|
+
import { LiveMediaSession } from "./LiveMediaSession";
|
|
18
|
+
import { IMediaPlayer } from "./IMediaPlayer";
|
|
19
|
+
import { TelemetryEvents } from "./internals";
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
22
|
* Event data returned by `MediaPlayerSynchronizer` object.
|
|
@@ -30,25 +37,41 @@ export interface IMediaPlayerSynchronizerEvent extends IEvent {
|
|
|
30
37
|
*/
|
|
31
38
|
export enum MediaPlayerSynchronizerEvents {
|
|
32
39
|
/**
|
|
33
|
-
*
|
|
40
|
+
*
|
|
34
41
|
*/
|
|
35
|
-
coordinatorstatechange =
|
|
36
|
-
groupaction =
|
|
37
|
-
useraction =
|
|
42
|
+
coordinatorstatechange = "coordinatorstatechange",
|
|
43
|
+
groupaction = "groupaction",
|
|
44
|
+
useraction = "useraction",
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
/**
|
|
41
48
|
* Synchronizes a local HTML Media Element with a group of remote HTML Media Elements.
|
|
42
|
-
*
|
|
49
|
+
*
|
|
43
50
|
* @remarks
|
|
44
51
|
* All of an apps transport control commands should be routed through the synchronizer. If the
|
|
45
|
-
* app is not currently joined to the group media session, the commands will be applied directly
|
|
52
|
+
* app is not currently joined to the group media session, the commands will be applied directly
|
|
46
53
|
* to the local player. When the group session is joined the commands will be broadcast to the
|
|
47
|
-
* group in addition to being applied to the local player.
|
|
54
|
+
* group in addition to being applied to the local player.
|
|
48
55
|
*/
|
|
49
56
|
export class MediaPlayerSynchronizer extends EventEmitter {
|
|
50
|
-
private static SESSION_ACTIONS: ExtendedMediaSessionAction[] = [
|
|
51
|
-
|
|
57
|
+
private static SESSION_ACTIONS: ExtendedMediaSessionAction[] = [
|
|
58
|
+
"play",
|
|
59
|
+
"pause",
|
|
60
|
+
"seekto",
|
|
61
|
+
"settrack",
|
|
62
|
+
"datachange",
|
|
63
|
+
"catchup",
|
|
64
|
+
"wait",
|
|
65
|
+
];
|
|
66
|
+
private static PLAYER_EVENTS: string[] = [
|
|
67
|
+
"playing",
|
|
68
|
+
"pause",
|
|
69
|
+
"ratechange",
|
|
70
|
+
"timeupdate",
|
|
71
|
+
"ended",
|
|
72
|
+
"loadedmetadata",
|
|
73
|
+
"blocked",
|
|
74
|
+
];
|
|
52
75
|
|
|
53
76
|
private _logger: LiveTelemetryLogger;
|
|
54
77
|
private _player: IMediaPlayer;
|
|
@@ -58,8 +81,8 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
58
81
|
private _onPlayerEvent: EventListener;
|
|
59
82
|
private _seekSuspension?: MediaSessionCoordinatorSuspension;
|
|
60
83
|
private _viewOnly = false;
|
|
61
|
-
private _expectedPlaybackState: ExtendedMediaSessionPlaybackState =
|
|
62
|
-
private _trackData: object|null = null;
|
|
84
|
+
private _expectedPlaybackState: ExtendedMediaSessionPlaybackState = "none";
|
|
85
|
+
private _trackData: object | null = null;
|
|
63
86
|
|
|
64
87
|
/**
|
|
65
88
|
* Creates a new `MediaElementSynchronizer` instance.
|
|
@@ -67,7 +90,11 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
67
90
|
* @param mediaSession Group MediaSession object being used.
|
|
68
91
|
* @param onEnd Optional. Function to call when synchronizers `end()` method is called.
|
|
69
92
|
*/
|
|
70
|
-
constructor(
|
|
93
|
+
constructor(
|
|
94
|
+
player: IMediaPlayer,
|
|
95
|
+
mediaSession: LiveMediaSession,
|
|
96
|
+
onEnd: () => void
|
|
97
|
+
) {
|
|
71
98
|
super();
|
|
72
99
|
this._player = player;
|
|
73
100
|
this._mediaSession = mediaSession;
|
|
@@ -81,13 +108,16 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
81
108
|
let playbackState: ExtendedMediaSessionPlaybackState;
|
|
82
109
|
const src = this._player.currentSrc || this._player.src;
|
|
83
110
|
if (this._player.ended) {
|
|
84
|
-
playbackState =
|
|
85
|
-
} else if (
|
|
86
|
-
|
|
111
|
+
playbackState = "ended";
|
|
112
|
+
} else if (
|
|
113
|
+
!src ||
|
|
114
|
+
(this._player.paused && this._player.currentTime < 1.0)
|
|
115
|
+
) {
|
|
116
|
+
playbackState = "none";
|
|
87
117
|
} else if (this._player.paused) {
|
|
88
|
-
playbackState =
|
|
118
|
+
playbackState = "paused";
|
|
89
119
|
} else {
|
|
90
|
-
playbackState =
|
|
120
|
+
playbackState = "playing";
|
|
91
121
|
}
|
|
92
122
|
|
|
93
123
|
const state = {
|
|
@@ -96,9 +126,9 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
96
126
|
positionState: {
|
|
97
127
|
position: this._player.currentTime,
|
|
98
128
|
playbackRate: this._player.playbackRate,
|
|
99
|
-
duration: this._player.duration
|
|
129
|
+
duration: this._player.duration,
|
|
100
130
|
},
|
|
101
|
-
trackData: this._trackData
|
|
131
|
+
trackData: this._trackData,
|
|
102
132
|
};
|
|
103
133
|
|
|
104
134
|
return state;
|
|
@@ -106,118 +136,201 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
106
136
|
|
|
107
137
|
// Listen for player events
|
|
108
138
|
this._onPlayerEvent = (evt: Event) => {
|
|
109
|
-
if (evt.type !=
|
|
110
|
-
this._logger.sendTelemetryEvent(
|
|
139
|
+
if (evt.type != "timeupdate") {
|
|
140
|
+
this._logger.sendTelemetryEvent(
|
|
141
|
+
TelemetryEvents.MediaPlayerSynchronizer.PlayerEvent,
|
|
142
|
+
null,
|
|
143
|
+
{
|
|
144
|
+
type: evt.type,
|
|
145
|
+
}
|
|
146
|
+
);
|
|
111
147
|
}
|
|
112
148
|
|
|
113
149
|
switch (evt.type) {
|
|
114
|
-
case
|
|
150
|
+
case "loadedmetadata":
|
|
151
|
+
// eslint-disable-next-line no-case-declarations
|
|
115
152
|
const src = this._player.currentSrc || this._player.src;
|
|
116
153
|
this._trackData = null;
|
|
117
154
|
break;
|
|
118
|
-
case
|
|
155
|
+
case "playing":
|
|
119
156
|
// Handle case for YouTube player where user can pause/play video by clicking on it.
|
|
120
157
|
// - Videos don't always start at 0.0 seconds.
|
|
121
|
-
if (this._expectedPlaybackState !=
|
|
122
|
-
if (
|
|
123
|
-
this.
|
|
158
|
+
if (this._expectedPlaybackState != "playing") {
|
|
159
|
+
if (
|
|
160
|
+
this._mediaSession.coordinator.canPlayPause &&
|
|
161
|
+
this._player.currentTime < 1.0
|
|
162
|
+
) {
|
|
163
|
+
this._logger.sendTelemetryEvent(
|
|
164
|
+
TelemetryEvents.MediaPlayerSynchronizer
|
|
165
|
+
.UserTappedVideoToPlay
|
|
166
|
+
);
|
|
124
167
|
this.play();
|
|
125
168
|
}
|
|
126
169
|
}
|
|
127
|
-
// block play if player state is playing when expected synced state is paused.
|
|
170
|
+
// block play if player state is playing when expected synced state is paused and coordinator is not suspended.
|
|
128
171
|
// needed because cannot tell if its a user initiated event, so disallow play
|
|
129
|
-
if (
|
|
172
|
+
if (
|
|
173
|
+
this._expectedPlaybackState === "paused" &&
|
|
174
|
+
!this._mediaSession.coordinator.isSuspended
|
|
175
|
+
) {
|
|
130
176
|
this._player.pause();
|
|
131
177
|
}
|
|
132
178
|
|
|
133
|
-
// block play if player state is playing when expected synced state is none.
|
|
179
|
+
// block play if player state is playing when expected synced state is none and coordinator is not suspended.
|
|
134
180
|
// needed because user who is not in control should not be able to start, so disallow play
|
|
135
|
-
if (
|
|
181
|
+
if (
|
|
182
|
+
this._expectedPlaybackState === "none" &&
|
|
183
|
+
!this._mediaSession.coordinator.isSuspended
|
|
184
|
+
) {
|
|
136
185
|
this._player.pause();
|
|
137
186
|
}
|
|
138
187
|
break;
|
|
139
|
-
case
|
|
140
|
-
// block pause if player state is paused when expected synced state is playing
|
|
188
|
+
case "pause":
|
|
189
|
+
// block pause if player state is paused when expected synced state is playing and coordinator is not suspended.
|
|
141
190
|
// needed because cannot tell if its a user initiated event, so disallow pause
|
|
142
|
-
if (
|
|
191
|
+
if (
|
|
192
|
+
this._expectedPlaybackState === "playing" &&
|
|
193
|
+
!this._mediaSession.coordinator.isSuspended
|
|
194
|
+
) {
|
|
143
195
|
this._player.play();
|
|
144
196
|
}
|
|
145
197
|
break;
|
|
146
|
-
case
|
|
198
|
+
case "ratechange":
|
|
147
199
|
// Block rate changes unless suspended.
|
|
148
|
-
if (
|
|
149
|
-
this.
|
|
200
|
+
if (
|
|
201
|
+
this._player.playbackRate != 1.0 &&
|
|
202
|
+
!this._mediaSession.coordinator.isSuspended
|
|
203
|
+
) {
|
|
204
|
+
this._logger.sendTelemetryEvent(
|
|
205
|
+
TelemetryEvents.MediaPlayerSynchronizer
|
|
206
|
+
.PlaybackRateChangeBlocked
|
|
207
|
+
);
|
|
150
208
|
this._player.playbackRate = 1.0;
|
|
151
209
|
}
|
|
152
210
|
break;
|
|
153
|
-
case
|
|
154
|
-
this._logger.sendTelemetryEvent(
|
|
211
|
+
case "blocked":
|
|
212
|
+
this._logger.sendTelemetryEvent(
|
|
213
|
+
TelemetryEvents.MediaPlayerSynchronizer
|
|
214
|
+
.PlaybackRateChangeBlocked
|
|
215
|
+
);
|
|
155
216
|
break;
|
|
156
217
|
}
|
|
157
218
|
};
|
|
158
219
|
|
|
159
220
|
// Register for coordinator state changes
|
|
160
|
-
this._mediaSession.coordinator.on(
|
|
221
|
+
this._mediaSession.coordinator.on(
|
|
222
|
+
MediaSessionCoordinatorEvents.coordinatorstatechange,
|
|
223
|
+
(evt) => this.emit(evt.type, evt)
|
|
224
|
+
);
|
|
161
225
|
|
|
162
226
|
// Register media session actions
|
|
163
227
|
for (const action of MediaPlayerSynchronizer.SESSION_ACTIONS) {
|
|
164
|
-
this._mediaSession.setActionHandler(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.
|
|
172
|
-
if (
|
|
173
|
-
this._logger.sendTelemetryEvent(
|
|
174
|
-
|
|
228
|
+
this._mediaSession.setActionHandler(
|
|
229
|
+
action,
|
|
230
|
+
async (details: ExtendedMediaSessionActionDetails) => {
|
|
231
|
+
let error: Error | undefined;
|
|
232
|
+
try {
|
|
233
|
+
switch (details.action) {
|
|
234
|
+
case "play":
|
|
235
|
+
this._expectedPlaybackState = "playing";
|
|
236
|
+
if (this._player.paused) {
|
|
237
|
+
this._logger.sendTelemetryEvent(
|
|
238
|
+
TelemetryEvents.MediaPlayerSynchronizer
|
|
239
|
+
.PlayAction
|
|
240
|
+
);
|
|
241
|
+
if (
|
|
242
|
+
typeof details.seekTime == "number" &&
|
|
243
|
+
this._player.currentTime < 1.0
|
|
244
|
+
) {
|
|
245
|
+
this._logger.sendTelemetryEvent(
|
|
246
|
+
TelemetryEvents
|
|
247
|
+
.MediaPlayerSynchronizer
|
|
248
|
+
.SeekingPlayerToStartPosition,
|
|
249
|
+
null,
|
|
250
|
+
{ position: details.seekTime }
|
|
251
|
+
);
|
|
252
|
+
this._player.currentTime =
|
|
253
|
+
details.seekTime!;
|
|
254
|
+
}
|
|
255
|
+
// Reference: https://developer.mozilla.org/docs/Web/API/HTMLMediaElement/play#exceptions
|
|
256
|
+
await this._player.play();
|
|
175
257
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
258
|
+
break;
|
|
259
|
+
case "pause":
|
|
260
|
+
this._logger.sendTelemetryEvent(
|
|
261
|
+
TelemetryEvents.MediaPlayerSynchronizer
|
|
262
|
+
.PauseAction
|
|
263
|
+
);
|
|
264
|
+
this._expectedPlaybackState = "paused";
|
|
265
|
+
if (
|
|
266
|
+
typeof details.seekTime == "number" &&
|
|
267
|
+
this._player.currentTime < 1.0
|
|
268
|
+
) {
|
|
269
|
+
this._logger.sendTelemetryEvent(
|
|
270
|
+
TelemetryEvents.MediaPlayerSynchronizer
|
|
271
|
+
.SeekingPlayerToStartPosition,
|
|
272
|
+
null,
|
|
273
|
+
{ position: details.seekTime }
|
|
274
|
+
);
|
|
275
|
+
this._player.currentTime =
|
|
276
|
+
details.seekTime!;
|
|
277
|
+
}
|
|
278
|
+
this._player.pause();
|
|
279
|
+
break;
|
|
280
|
+
case "seekto":
|
|
281
|
+
if (typeof details.seekTime == "number") {
|
|
282
|
+
this._logger.sendTelemetryEvent(
|
|
283
|
+
TelemetryEvents.MediaPlayerSynchronizer
|
|
284
|
+
.SeekToAction,
|
|
285
|
+
null,
|
|
286
|
+
{ position: details.seekTime }
|
|
287
|
+
);
|
|
288
|
+
this._player.currentTime =
|
|
289
|
+
details.seekTime!;
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
case "settrack":
|
|
293
|
+
this._logger.sendTelemetryEvent(
|
|
294
|
+
TelemetryEvents.MediaPlayerSynchronizer
|
|
295
|
+
.SetTrackAction
|
|
296
|
+
);
|
|
297
|
+
this._expectedPlaybackState = "none";
|
|
298
|
+
this._player.src =
|
|
299
|
+
details.metadata!.trackIdentifier;
|
|
300
|
+
this._player.load();
|
|
301
|
+
break;
|
|
302
|
+
case "datachange":
|
|
303
|
+
this._logger.sendTelemetryEvent(
|
|
304
|
+
TelemetryEvents.MediaPlayerSynchronizer
|
|
305
|
+
.DataChangeAction
|
|
306
|
+
);
|
|
307
|
+
break;
|
|
308
|
+
case "catchup":
|
|
309
|
+
if (typeof details.seekTime == "number") {
|
|
310
|
+
this._logger.sendTelemetryEvent(
|
|
311
|
+
TelemetryEvents.MediaPlayerSynchronizer
|
|
312
|
+
.CatchupAction,
|
|
313
|
+
null,
|
|
314
|
+
{ position: details.seekTime }
|
|
315
|
+
);
|
|
316
|
+
this.catchupPlayer(details.seekTime!);
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
} catch (err: any) {
|
|
321
|
+
if (err instanceof Error) {
|
|
322
|
+
error = err;
|
|
323
|
+
} else {
|
|
324
|
+
error = new Error(
|
|
325
|
+
err?.message ??
|
|
326
|
+
"An unknown error occurred after processing the group session action."
|
|
327
|
+
);
|
|
328
|
+
}
|
|
216
329
|
}
|
|
217
|
-
}
|
|
218
330
|
|
|
219
|
-
|
|
220
|
-
|
|
331
|
+
this.dispatchGroupAction(details, false, error);
|
|
332
|
+
}
|
|
333
|
+
);
|
|
221
334
|
}
|
|
222
335
|
|
|
223
336
|
// Subscribe to player events
|
|
@@ -225,7 +338,9 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
225
338
|
this._player.addEventListener(event, this._onPlayerEvent);
|
|
226
339
|
}
|
|
227
340
|
|
|
228
|
-
this._logger.sendTelemetryEvent(
|
|
341
|
+
this._logger.sendTelemetryEvent(
|
|
342
|
+
TelemetryEvents.MediaPlayerSynchronizer.SynchronizationStarted
|
|
343
|
+
);
|
|
229
344
|
}
|
|
230
345
|
|
|
231
346
|
/**
|
|
@@ -244,7 +359,7 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
244
359
|
|
|
245
360
|
/**
|
|
246
361
|
* If true the client is in a view only mode.
|
|
247
|
-
*
|
|
362
|
+
*
|
|
248
363
|
* @remarks
|
|
249
364
|
* Toggling this value to true results in `mediaSession.coordinator.canPlayPause`,
|
|
250
365
|
* `mediaSession.coordinator.canSeek`, `mediaSession.coordinator.canSetTrack`, and
|
|
@@ -276,7 +391,10 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
276
391
|
* @param event Name of the event to add.
|
|
277
392
|
* @param listener Function to call when the event is triggered.
|
|
278
393
|
*/
|
|
279
|
-
public addEventListener(
|
|
394
|
+
public addEventListener(
|
|
395
|
+
event: MediaPlayerSynchronizerEvents,
|
|
396
|
+
listener: (evt: IMediaPlayerSynchronizerEvent) => void
|
|
397
|
+
): this {
|
|
280
398
|
this.on(event, listener);
|
|
281
399
|
return this;
|
|
282
400
|
}
|
|
@@ -286,7 +404,10 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
286
404
|
* @param event Name of the event to remove.
|
|
287
405
|
* @param listener Function that was registered in call to `addEventListener()`.
|
|
288
406
|
*/
|
|
289
|
-
public removeEventListener(
|
|
407
|
+
public removeEventListener(
|
|
408
|
+
event: MediaPlayerSynchronizerEvents,
|
|
409
|
+
listener: (evt: IMediaPlayerSynchronizerEvent) => void
|
|
410
|
+
): this {
|
|
290
411
|
this.off(event, listener);
|
|
291
412
|
return this;
|
|
292
413
|
}
|
|
@@ -304,7 +425,10 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
304
425
|
|
|
305
426
|
// Subscribe to player events
|
|
306
427
|
for (const event of MediaPlayerSynchronizer.PLAYER_EVENTS) {
|
|
307
|
-
this._player.removeEventListener(
|
|
428
|
+
this._player.removeEventListener(
|
|
429
|
+
event,
|
|
430
|
+
this._onPlayerEvent
|
|
431
|
+
);
|
|
308
432
|
}
|
|
309
433
|
|
|
310
434
|
// Notify parent
|
|
@@ -317,19 +441,23 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
317
441
|
|
|
318
442
|
/**
|
|
319
443
|
* Begin a local seek operation.
|
|
320
|
-
*
|
|
444
|
+
*
|
|
321
445
|
* @remarks
|
|
322
|
-
* UI can call this when a user grabs a timeline scrubber and starts scrubbing the video to a
|
|
446
|
+
* UI can call this when a user grabs a timeline scrubber and starts scrubbing the video to a
|
|
323
447
|
* new playback position. The synchronizer will being a new suspension which temporarily
|
|
324
448
|
* disconnects the client for the rest of the group for synchronization purposes. Calling
|
|
325
449
|
* `endSeek()` will end the suspension and seek the group to the users final seek position.
|
|
326
450
|
*/
|
|
327
451
|
public beginSeek(): void {
|
|
328
452
|
if (this._seekSuspension) {
|
|
329
|
-
throw new Error(
|
|
453
|
+
throw new Error(
|
|
454
|
+
`MediaPlayerSynchronizer: cannot begin seek. A seek is already in progress.`
|
|
455
|
+
);
|
|
330
456
|
}
|
|
331
457
|
|
|
332
|
-
this._logger.sendTelemetryEvent(
|
|
458
|
+
this._logger.sendTelemetryEvent(
|
|
459
|
+
TelemetryEvents.MediaPlayerSynchronizer.BeginSeekCalled
|
|
460
|
+
);
|
|
333
461
|
this._seekSuspension = this._mediaSession.coordinator.beginSuspension();
|
|
334
462
|
}
|
|
335
463
|
|
|
@@ -339,7 +467,9 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
339
467
|
*/
|
|
340
468
|
public endSeek(seekTo: number): void {
|
|
341
469
|
if (!this._seekSuspension) {
|
|
342
|
-
throw new Error(
|
|
470
|
+
throw new Error(
|
|
471
|
+
`MediaPlayerSynchronizer: cannot end seek. No seek is in progress.`
|
|
472
|
+
);
|
|
343
473
|
}
|
|
344
474
|
|
|
345
475
|
// Seek player to new time immediately.
|
|
@@ -350,48 +480,58 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
350
480
|
const suspension = this._seekSuspension;
|
|
351
481
|
this._seekSuspension = undefined;
|
|
352
482
|
|
|
353
|
-
this._logger.sendTelemetryEvent(
|
|
483
|
+
this._logger.sendTelemetryEvent(
|
|
484
|
+
TelemetryEvents.MediaPlayerSynchronizer.EndSeekCalled,
|
|
485
|
+
null,
|
|
486
|
+
{
|
|
487
|
+
position: seekTo,
|
|
488
|
+
}
|
|
489
|
+
);
|
|
354
490
|
suspension.end(seekTo);
|
|
355
491
|
|
|
356
|
-
this.dispatchUserAction({action:
|
|
492
|
+
this.dispatchUserAction({ action: "seekto", seekTime: seekTo });
|
|
357
493
|
}
|
|
358
494
|
|
|
359
495
|
/**
|
|
360
496
|
* Tells the group to begin playing the current video.
|
|
361
|
-
*
|
|
497
|
+
*
|
|
362
498
|
* @remarks
|
|
363
|
-
* For proper operation apps should avoid calling `mediaSession.coordinator.play()` directly
|
|
364
|
-
* and instead use the synchronizers `play()` method.
|
|
499
|
+
* For proper operation apps should avoid calling `mediaSession.coordinator.play()` directly
|
|
500
|
+
* and instead use the synchronizers `play()` method.
|
|
365
501
|
*/
|
|
366
502
|
public play(): void {
|
|
367
|
-
this._logger.sendTelemetryEvent(
|
|
368
|
-
|
|
503
|
+
this._logger.sendTelemetryEvent(
|
|
504
|
+
TelemetryEvents.MediaPlayerSynchronizer.PlayCalled
|
|
505
|
+
);
|
|
506
|
+
this._expectedPlaybackState = "playing";
|
|
369
507
|
this._mediaSession.coordinator.play();
|
|
370
508
|
|
|
371
|
-
this.dispatchUserAction({action:
|
|
509
|
+
this.dispatchUserAction({ action: "play" });
|
|
372
510
|
}
|
|
373
511
|
|
|
374
512
|
/**
|
|
375
513
|
* Tells the group to pause the current video.
|
|
376
|
-
*
|
|
514
|
+
*
|
|
377
515
|
* @remarks
|
|
378
|
-
* For proper operation apps should avoid calling `mediaSession.coordinator.pause()` directly
|
|
379
|
-
* and instead use the synchronizers `pause()` method.
|
|
516
|
+
* For proper operation apps should avoid calling `mediaSession.coordinator.pause()` directly
|
|
517
|
+
* and instead use the synchronizers `pause()` method.
|
|
380
518
|
*/
|
|
381
519
|
public pause(): void {
|
|
382
|
-
this._logger.sendTelemetryEvent(
|
|
383
|
-
|
|
520
|
+
this._logger.sendTelemetryEvent(
|
|
521
|
+
TelemetryEvents.MediaPlayerSynchronizer.PauseCalled
|
|
522
|
+
);
|
|
523
|
+
this._expectedPlaybackState = "paused";
|
|
384
524
|
this._mediaSession.coordinator.pause();
|
|
385
525
|
|
|
386
|
-
this.dispatchUserAction({action:
|
|
526
|
+
this.dispatchUserAction({ action: "pause" });
|
|
387
527
|
}
|
|
388
528
|
|
|
389
529
|
/**
|
|
390
530
|
* Tells the group to seek the current video to a new playback position.
|
|
391
|
-
*
|
|
531
|
+
*
|
|
392
532
|
* @remarks
|
|
393
|
-
* For proper operation apps should avoid calling `mediaSession.coordinator.seekTo()` directly
|
|
394
|
-
* and instead use the synchronizers `seekTo()` method.
|
|
533
|
+
* For proper operation apps should avoid calling `mediaSession.coordinator.seekTo()` directly
|
|
534
|
+
* and instead use the synchronizers `seekTo()` method.
|
|
395
535
|
*/
|
|
396
536
|
public seekTo(time: number): void {
|
|
397
537
|
// Always seek player to new time.
|
|
@@ -399,58 +539,98 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
399
539
|
// location.
|
|
400
540
|
this._player.currentTime = time;
|
|
401
541
|
|
|
402
|
-
this._logger.sendTelemetryEvent(
|
|
542
|
+
this._logger.sendTelemetryEvent(
|
|
543
|
+
TelemetryEvents.MediaPlayerSynchronizer.SeekToCalled,
|
|
544
|
+
null,
|
|
545
|
+
{ position: time }
|
|
546
|
+
);
|
|
403
547
|
this._mediaSession.coordinator.seekTo(time);
|
|
404
548
|
|
|
405
|
-
this.dispatchUserAction({action:
|
|
549
|
+
this.dispatchUserAction({ action: "seekto", seekTime: time });
|
|
406
550
|
}
|
|
407
551
|
|
|
408
552
|
/**
|
|
409
553
|
* Tells the group to change to a new track.
|
|
410
|
-
*
|
|
554
|
+
*
|
|
411
555
|
* @remarks
|
|
412
|
-
* For proper operation apps should avoid calling `mediaSession.coordinator.setTrack()` directly
|
|
413
|
-
* and instead use the synchronizers `setTrack()` method.
|
|
556
|
+
* For proper operation apps should avoid calling `mediaSession.coordinator.setTrack()` directly
|
|
557
|
+
* and instead use the synchronizers `setTrack()` method.
|
|
414
558
|
*/
|
|
415
|
-
public setTrack(
|
|
416
|
-
|
|
559
|
+
public setTrack(
|
|
560
|
+
track: ExtendedMediaMetadata,
|
|
561
|
+
waitPoints?: CoordinationWaitPoint[]
|
|
562
|
+
): void {
|
|
563
|
+
this._logger.sendTelemetryEvent(
|
|
564
|
+
TelemetryEvents.MediaPlayerSynchronizer.SetTrackCalled
|
|
565
|
+
);
|
|
417
566
|
this._mediaSession.coordinator.setTrack(track, waitPoints);
|
|
418
567
|
|
|
419
|
-
this.dispatchUserAction({action:
|
|
568
|
+
this.dispatchUserAction({ action: "settrack", metadata: track });
|
|
420
569
|
}
|
|
421
570
|
|
|
422
571
|
/**
|
|
423
572
|
* Updates the current tracks data object.
|
|
424
|
-
*
|
|
573
|
+
*
|
|
425
574
|
* @remarks
|
|
426
|
-
* For proper operation apps should avoid calling `mediaSession.coordinator.setTrackData()` directly
|
|
427
|
-
* and instead use the synchronizers `setTrackData()` method.
|
|
575
|
+
* For proper operation apps should avoid calling `mediaSession.coordinator.setTrackData()` directly
|
|
576
|
+
* and instead use the synchronizers `setTrackData()` method.
|
|
428
577
|
*/
|
|
429
|
-
public setTrackData(data: object|null): void {
|
|
430
|
-
this._logger.sendTelemetryEvent(
|
|
578
|
+
public setTrackData(data: object | null): void {
|
|
579
|
+
this._logger.sendTelemetryEvent(
|
|
580
|
+
TelemetryEvents.MediaPlayerSynchronizer.SetTrackDataCalled
|
|
581
|
+
);
|
|
431
582
|
this._trackData = data;
|
|
432
583
|
this._mediaSession.coordinator.setTrackData(data);
|
|
433
584
|
|
|
434
|
-
this.dispatchUserAction({action:
|
|
585
|
+
this.dispatchUserAction({ action: "datachange", data: data });
|
|
435
586
|
}
|
|
436
587
|
|
|
437
|
-
private dispatchGroupAction(
|
|
588
|
+
private dispatchGroupAction(
|
|
589
|
+
details: ExtendedMediaSessionActionDetails,
|
|
590
|
+
delay = false,
|
|
591
|
+
error: Error | undefined
|
|
592
|
+
): void {
|
|
438
593
|
if (delay) {
|
|
439
|
-
setTimeout(
|
|
594
|
+
setTimeout(
|
|
595
|
+
() =>
|
|
596
|
+
this.emit(MediaPlayerSynchronizerEvents.groupaction, {
|
|
597
|
+
type: MediaPlayerSynchronizerEvents.groupaction,
|
|
598
|
+
details: details,
|
|
599
|
+
playerError: error,
|
|
600
|
+
}),
|
|
601
|
+
50
|
|
602
|
+
);
|
|
440
603
|
} else {
|
|
441
|
-
this.emit(MediaPlayerSynchronizerEvents.groupaction, {
|
|
604
|
+
this.emit(MediaPlayerSynchronizerEvents.groupaction, {
|
|
605
|
+
type: MediaPlayerSynchronizerEvents.groupaction,
|
|
606
|
+
details: details,
|
|
607
|
+
error,
|
|
608
|
+
});
|
|
442
609
|
}
|
|
443
610
|
}
|
|
444
611
|
|
|
445
|
-
private dispatchUserAction(
|
|
612
|
+
private dispatchUserAction(
|
|
613
|
+
details: ExtendedMediaSessionActionDetails,
|
|
614
|
+
delay = false
|
|
615
|
+
): void {
|
|
446
616
|
if (delay) {
|
|
447
|
-
setTimeout(
|
|
617
|
+
setTimeout(
|
|
618
|
+
() =>
|
|
619
|
+
this.emit(MediaPlayerSynchronizerEvents.useraction, {
|
|
620
|
+
type: MediaPlayerSynchronizerEvents.useraction,
|
|
621
|
+
details: details,
|
|
622
|
+
}),
|
|
623
|
+
50
|
|
624
|
+
);
|
|
448
625
|
} else {
|
|
449
|
-
this.emit(MediaPlayerSynchronizerEvents.useraction, {
|
|
626
|
+
this.emit(MediaPlayerSynchronizerEvents.useraction, {
|
|
627
|
+
type: MediaPlayerSynchronizerEvents.useraction,
|
|
628
|
+
details: details,
|
|
629
|
+
});
|
|
450
630
|
}
|
|
451
631
|
}
|
|
452
632
|
|
|
453
633
|
private async catchupPlayer(time: number): Promise<void> {
|
|
454
634
|
this._player.currentTime = time;
|
|
455
635
|
}
|
|
456
|
-
}
|
|
636
|
+
}
|