@microsoft/live-share-media 0.1.2 → 0.2.2
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/.vscode/settings.json +2 -0
- package/LICENSE +87 -0
- package/README.md +118 -92
- package/bin/EphemeralMediaSession.d.ts +51 -4
- package/bin/EphemeralMediaSession.d.ts.map +1 -1
- package/bin/EphemeralMediaSession.js +91 -44
- package/bin/EphemeralMediaSession.js.map +1 -1
- package/bin/EphemeralMediaSessionCoordinator.d.ts +173 -6
- package/bin/EphemeralMediaSessionCoordinator.d.ts.map +1 -1
- package/bin/EphemeralMediaSessionCoordinator.js +170 -23
- package/bin/EphemeralMediaSessionCoordinator.js.map +1 -1
- package/bin/EphemeralMediaSessionCoordinatorSuspension.d.ts +1 -1
- package/bin/EphemeralMediaSessionCoordinatorSuspension.js +1 -1
- package/bin/IMediaPlayer.d.ts +1 -1
- package/bin/IMediaPlayer.js +1 -1
- package/bin/MediaPlayerSynchronizer.d.ts +95 -6
- package/bin/MediaPlayerSynchronizer.d.ts.map +1 -1
- package/bin/MediaPlayerSynchronizer.js +93 -31
- package/bin/MediaPlayerSynchronizer.js.map +1 -1
- package/bin/MediaSessionActionThrottler.d.ts +1 -1
- package/bin/MediaSessionActionThrottler.js +1 -1
- package/bin/MediaSessionExtensions.d.ts +1 -1
- package/bin/MediaSessionExtensions.js +1 -1
- package/bin/RepeatedActionThrottler.d.ts +1 -1
- package/bin/RepeatedActionThrottler.js +1 -1
- package/bin/VolumeLimiter.d.ts +2 -2
- package/bin/VolumeLimiter.js +2 -2
- package/bin/index.d.ts +1 -1
- package/bin/index.js +1 -1
- package/bin/internals/GroupCoordinatorState.d.ts +22 -1
- package/bin/internals/GroupCoordinatorState.d.ts.map +1 -1
- package/bin/internals/GroupCoordinatorState.js +10 -4
- package/bin/internals/GroupCoordinatorState.js.map +1 -1
- package/bin/internals/GroupPlaybackPosition.d.ts +8 -1
- package/bin/internals/GroupPlaybackPosition.d.ts.map +1 -1
- package/bin/internals/GroupPlaybackPosition.js +5 -2
- package/bin/internals/GroupPlaybackPosition.js.map +1 -1
- package/bin/internals/GroupPlaybackTrack.d.ts +13 -1
- package/bin/internals/GroupPlaybackTrack.d.ts.map +1 -1
- package/bin/internals/GroupPlaybackTrack.js +8 -2
- package/bin/internals/GroupPlaybackTrack.js.map +1 -1
- package/bin/internals/GroupPlaybackTrackData.d.ts +13 -1
- package/bin/internals/GroupPlaybackTrackData.d.ts.map +1 -1
- package/bin/internals/GroupPlaybackTrackData.js +8 -2
- package/bin/internals/GroupPlaybackTrackData.js.map +1 -1
- package/bin/internals/GroupTransportState.d.ts +13 -1
- package/bin/internals/GroupTransportState.d.ts.map +1 -1
- package/bin/internals/GroupTransportState.js +8 -2
- package/bin/internals/GroupTransportState.js.map +1 -1
- package/bin/internals/consts.d.ts +2 -1
- package/bin/internals/consts.d.ts.map +1 -1
- package/bin/internals/consts.js +5 -1
- package/bin/internals/consts.js.map +1 -1
- package/bin/internals/index.d.ts +1 -1
- package/bin/internals/index.js +1 -1
- package/package.json +14 -5
- package/src/EphemeralMediaSession.ts +95 -48
- package/src/EphemeralMediaSessionCoordinator.ts +190 -26
- package/src/EphemeralMediaSessionCoordinatorSuspension.ts +1 -1
- package/src/IMediaPlayer.ts +1 -1
- package/src/MediaPlayerSynchronizer.ts +101 -33
- package/src/MediaSessionActionThrottler.ts +1 -1
- package/src/MediaSessionExtensions.ts +1 -1
- package/src/RepeatedActionThrottler.ts +1 -1
- package/src/VolumeLimiter.ts +2 -2
- package/src/index.ts +1 -1
- package/src/internals/GroupCoordinatorState.ts +39 -18
- package/src/internals/GroupPlaybackPosition.ts +16 -10
- package/src/internals/GroupPlaybackTrack.ts +22 -9
- package/src/internals/GroupPlaybackTrackData.ts +20 -7
- package/src/internals/GroupTransportState.ts +20 -7
- package/src/internals/consts.ts +7 -3
- package/src/internals/index.ts +1 -1
- package/src/test/GroupPlaybackPosition.spec.ts +1 -1
- package/src/test/GroupPlaybackTrack.spec.ts +1 -1
- package/src/test/SharedMediaSession.spec.ts +1 -1
- package/src/test/SharedMediaSessionCoordinator.spec.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
-
* Licensed under the
|
|
3
|
+
* Licensed under the Microsoft Live Share SDK License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { EphemeralTelemetryLogger, IEvent, TimeInterval } from '@microsoft/live-share';
|
|
@@ -11,11 +11,23 @@ import { VolumeLimiter } from './VolumeLimiter';
|
|
|
11
11
|
import { IMediaPlayer } from './IMediaPlayer';
|
|
12
12
|
import { TelemetryEvents } from './internals';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Event data returned by `MediaPlayerSynchronizer` object.
|
|
16
|
+
*/
|
|
14
17
|
export interface IMediaPlayerSynchronizerEvent extends IEvent {
|
|
18
|
+
/**
|
|
19
|
+
* Event details.
|
|
20
|
+
*/
|
|
15
21
|
details: ExtendedMediaSessionActionDetails;
|
|
16
22
|
}
|
|
17
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Events supported by `MediaPlayerSynchronizer` object.
|
|
26
|
+
*/
|
|
18
27
|
export enum MediaPlayerSynchronizerEvents {
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
*/
|
|
19
31
|
coordinatorstatechange = 'coordinatorstatechange',
|
|
20
32
|
groupaction = 'groupaction',
|
|
21
33
|
useraction = 'useraction'
|
|
@@ -40,7 +52,6 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
40
52
|
private _volumeLimiter: VolumeLimiter;
|
|
41
53
|
private _onEnd?: () => void;
|
|
42
54
|
private _onPlayerEvent: EventListener;
|
|
43
|
-
private _catchupFastSeekPeriod: TimeInterval = new TimeInterval(0);
|
|
44
55
|
private _seekSuspension?: MediaSessionCoordinatorSuspension;
|
|
45
56
|
private _viewOnly = false;
|
|
46
57
|
private _expectedPlaybackState: ExtendedMediaSessionPlaybackState = 'none';
|
|
@@ -61,7 +72,7 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
61
72
|
this._onEnd = onEnd;
|
|
62
73
|
|
|
63
74
|
// Listen for player state requests
|
|
64
|
-
mediaSession.
|
|
75
|
+
mediaSession.setRequestPlayerStateHandler(() => {
|
|
65
76
|
// Find playback state
|
|
66
77
|
let playbackState: ExtendedMediaSessionPlaybackState;
|
|
67
78
|
const src = this._player.currentSrc || this._player.src;
|
|
@@ -87,7 +98,7 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
87
98
|
};
|
|
88
99
|
|
|
89
100
|
return state;
|
|
90
|
-
}
|
|
101
|
+
});
|
|
91
102
|
|
|
92
103
|
// Listen for player events
|
|
93
104
|
this._onPlayerEvent = (evt: Event) => {
|
|
@@ -104,7 +115,7 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
104
115
|
// Handle case for YouTube player where user can pause/play video by clicking on it.
|
|
105
116
|
// - Videos don't always start at 0.0 seconds.
|
|
106
117
|
if (this._expectedPlaybackState != 'playing') {
|
|
107
|
-
if (
|
|
118
|
+
if (this._mediaSession.coordinator.canPlayPause && this._player.currentTime < 1.0) {
|
|
108
119
|
this._logger.sendTelemetryEvent(TelemetryEvents.MediaPlayerSynchronizer.UserTappedVideoToPlay);
|
|
109
120
|
this.play();
|
|
110
121
|
}
|
|
@@ -185,22 +196,30 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
185
196
|
this._logger.sendTelemetryEvent(TelemetryEvents.MediaPlayerSynchronizer.SynchronizationStarted);
|
|
186
197
|
}
|
|
187
198
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
public set catchupFastSeekPeriod(value: number) {
|
|
193
|
-
this._catchupFastSeekPeriod.seconds = value;
|
|
194
|
-
}
|
|
195
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Media player being synchronized.
|
|
201
|
+
*/
|
|
196
202
|
public get player(): IMediaPlayer {
|
|
197
203
|
return this._player;
|
|
198
204
|
}
|
|
199
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Synchronizers media session.
|
|
208
|
+
*/
|
|
200
209
|
public get mediaSession(): EphemeralMediaSession {
|
|
201
210
|
return this._mediaSession;
|
|
202
211
|
}
|
|
203
212
|
|
|
213
|
+
/**
|
|
214
|
+
* If true the client is in a view only mode.
|
|
215
|
+
*
|
|
216
|
+
* @remarks
|
|
217
|
+
* Toggling this value to true results in `mediaSession.coordinator.canPlayPause`,
|
|
218
|
+
* `mediaSession.coordinator.canSeek`, `mediaSession.coordinator.canSetTrack`, and
|
|
219
|
+
* `mediaSession.coordinator.canSetTrackData` all being set to false. For more fine
|
|
220
|
+
* grained control over the local clients policies, call the `mediaSession.coordinator`
|
|
221
|
+
* directly.
|
|
222
|
+
*/
|
|
204
223
|
public get viewOnly(): boolean {
|
|
205
224
|
return this._viewOnly;
|
|
206
225
|
}
|
|
@@ -209,22 +228,40 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
209
228
|
this._viewOnly = value;
|
|
210
229
|
this.mediaSession.coordinator.canPlayPause = !value;
|
|
211
230
|
this.mediaSession.coordinator.canSeek = !value;
|
|
231
|
+
this.mediaSession.coordinator.canSetTrack = !value;
|
|
232
|
+
this.mediaSession.coordinator.canSetTrackData = !value;
|
|
212
233
|
}
|
|
213
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Volume limiter used to temporarily reduce the videos volume when someone speaks in a meeting.
|
|
237
|
+
*/
|
|
214
238
|
public get volumeLimiter(): VolumeLimiter {
|
|
215
239
|
return this._volumeLimiter;
|
|
216
240
|
}
|
|
217
241
|
|
|
218
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Registers a new event listener.
|
|
244
|
+
* @param event Name of the event to add.
|
|
245
|
+
* @param listener Function to call when the event is triggered.
|
|
246
|
+
*/
|
|
247
|
+
public addEventListener(event: MediaPlayerSynchronizerEvents, listener: (evt: IMediaPlayerSynchronizerEvent) => void): this {
|
|
219
248
|
this.on(event, listener);
|
|
220
249
|
return this;
|
|
221
250
|
}
|
|
222
251
|
|
|
223
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Un-registers an existing event listener.
|
|
254
|
+
* @param event Name of the event to remove.
|
|
255
|
+
* @param listener Function that was registered in call to `addEventListener()`.
|
|
256
|
+
*/
|
|
257
|
+
public removeEventListener(event: MediaPlayerSynchronizerEvents, listener: (evt: IMediaPlayerSynchronizerEvent) => void): this {
|
|
224
258
|
this.off(event, listener);
|
|
225
259
|
return this;
|
|
226
260
|
}
|
|
227
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Ends synchronization of the current media player.
|
|
264
|
+
*/
|
|
228
265
|
public end(): void {
|
|
229
266
|
if (this._onEnd) {
|
|
230
267
|
try {
|
|
@@ -246,6 +283,15 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
246
283
|
}
|
|
247
284
|
}
|
|
248
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Begin a local seek operation.
|
|
288
|
+
*
|
|
289
|
+
* @remarks
|
|
290
|
+
* UI can call this when a user grabs a timeline scrubber and starts scrubbing the video to a
|
|
291
|
+
* new playback position. The synchronizer will being a new suspension which temporarily
|
|
292
|
+
* disconnects the client for the rest of the group for synchronization purposes. Calling
|
|
293
|
+
* `endSeek()` will end the suspension and seek the group to the users final seek position.
|
|
294
|
+
*/
|
|
249
295
|
public beginSeek(): void {
|
|
250
296
|
if (this._seekSuspension) {
|
|
251
297
|
throw new Error(`MediaPlayerSynchronizer: cannot begin seek. A seek is already in progress.`);
|
|
@@ -255,6 +301,10 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
255
301
|
this._seekSuspension = this._mediaSession.coordinator.beginSuspension();
|
|
256
302
|
}
|
|
257
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Ends a seek operation that was started by calling `beginSeek()`.
|
|
306
|
+
* @param seekTo Playback position in seconds to seek the group to.
|
|
307
|
+
*/
|
|
258
308
|
public endSeek(seekTo: number): void {
|
|
259
309
|
if (!this._seekSuspension) {
|
|
260
310
|
throw new Error(`MediaPlayerSynchronizer: cannot end seek. No seek is in progress.`);
|
|
@@ -274,6 +324,13 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
274
324
|
this.dispatchUserAction({action: 'seekto', seekTime: seekTo});
|
|
275
325
|
}
|
|
276
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Tells the group to begin playing the current video.
|
|
329
|
+
*
|
|
330
|
+
* @remarks
|
|
331
|
+
* For proper operation apps should avoid calling `mediaSession.coordinator.play()` directly
|
|
332
|
+
* and instead use the synchronizers `play()` method.
|
|
333
|
+
*/
|
|
277
334
|
public play(): void {
|
|
278
335
|
this._logger.sendTelemetryEvent(TelemetryEvents.MediaPlayerSynchronizer.PlayCalled);
|
|
279
336
|
this._expectedPlaybackState = 'playing';
|
|
@@ -282,6 +339,13 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
282
339
|
this.dispatchUserAction({action: 'play'});
|
|
283
340
|
}
|
|
284
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Tells the group to pause the current video.
|
|
344
|
+
*
|
|
345
|
+
* @remarks
|
|
346
|
+
* For proper operation apps should avoid calling `mediaSession.coordinator.pause()` directly
|
|
347
|
+
* and instead use the synchronizers `pause()` method.
|
|
348
|
+
*/
|
|
285
349
|
public pause(): void {
|
|
286
350
|
this._logger.sendTelemetryEvent(TelemetryEvents.MediaPlayerSynchronizer.PauseCalled);
|
|
287
351
|
this._expectedPlaybackState = 'paused';
|
|
@@ -290,6 +354,13 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
290
354
|
this.dispatchUserAction({action: 'pause'});
|
|
291
355
|
}
|
|
292
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Tells the group to seek the current video to a new playback position.
|
|
359
|
+
*
|
|
360
|
+
* @remarks
|
|
361
|
+
* For proper operation apps should avoid calling `mediaSession.coordinator.seekTo()` directly
|
|
362
|
+
* and instead use the synchronizers `seekTo()` method.
|
|
363
|
+
*/
|
|
293
364
|
public seekTo(time: number): void {
|
|
294
365
|
// Always seek player to new time.
|
|
295
366
|
// - This resolves an issue where the timeline scrubber can temporarily snap back to the original
|
|
@@ -302,6 +373,13 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
302
373
|
this.dispatchUserAction({action: 'seekto', seekTime: time});
|
|
303
374
|
}
|
|
304
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Tells the group to change to a new track.
|
|
378
|
+
*
|
|
379
|
+
* @remarks
|
|
380
|
+
* For proper operation apps should avoid calling `mediaSession.coordinator.setTrack()` directly
|
|
381
|
+
* and instead use the synchronizers `setTrack()` method.
|
|
382
|
+
*/
|
|
305
383
|
public setTrack(track: ExtendedMediaMetadata, waitPoints?: CoordinationWaitPoint[]): void {
|
|
306
384
|
this._logger.sendTelemetryEvent(TelemetryEvents.MediaPlayerSynchronizer.SetTrackCalled);
|
|
307
385
|
this._mediaSession.coordinator.setTrack(track, waitPoints);
|
|
@@ -309,6 +387,13 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
309
387
|
this.dispatchUserAction({action: 'settrack', metadata: track});
|
|
310
388
|
}
|
|
311
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Updates the current tracks data object.
|
|
392
|
+
*
|
|
393
|
+
* @remarks
|
|
394
|
+
* For proper operation apps should avoid calling `mediaSession.coordinator.setTrackData()` directly
|
|
395
|
+
* and instead use the synchronizers `setTrackData()` method.
|
|
396
|
+
*/
|
|
312
397
|
public setTrackData(data: object|null): void {
|
|
313
398
|
this._logger.sendTelemetryEvent(TelemetryEvents.MediaPlayerSynchronizer.SetTrackDataCalled);
|
|
314
399
|
this._trackData = data;
|
|
@@ -334,23 +419,6 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
334
419
|
}
|
|
335
420
|
|
|
336
421
|
private async catchupPlayer(time: number): Promise<void> {
|
|
337
|
-
|
|
338
|
-
// - more then 2 seconds behind.
|
|
339
|
-
// - fast seek hasn't been disabled.
|
|
340
|
-
// - the current playback rate is 1.0.
|
|
341
|
-
if (time >= (this._player.currentTime + 2.0) && this.catchupFastSeekPeriod > 0 && this._player.playbackRate == 1.0) {
|
|
342
|
-
// Fast seek to new position
|
|
343
|
-
const suspension = await this._mediaSession.coordinator.beginSuspension();
|
|
344
|
-
const seekPeriod = Math.min(time - this._player.currentTime, this.catchupFastSeekPeriod);
|
|
345
|
-
const startTime = time - seekPeriod;
|
|
346
|
-
this._player.currentTime = startTime;
|
|
347
|
-
this._player.playbackRate = 4.0;
|
|
348
|
-
setTimeout(async () => {
|
|
349
|
-
this._player.playbackRate = 1.0;
|
|
350
|
-
await suspension.end();
|
|
351
|
-
}, (seekPeriod * 0.25) * 1000)
|
|
352
|
-
} else {
|
|
353
|
-
this._player.currentTime = time;
|
|
354
|
-
}
|
|
422
|
+
this._player.currentTime = time;
|
|
355
423
|
}
|
|
356
424
|
}
|
package/src/VolumeLimiter.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
-
* Licensed under the
|
|
3
|
+
* Licensed under the Microsoft Live Share SDK License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { TimeInterval } from '@microsoft/live-share';
|
|
@@ -33,7 +33,7 @@ export class VolumeLimiter {
|
|
|
33
33
|
* Time, in seconds, that the volume should be lowered.
|
|
34
34
|
*
|
|
35
35
|
* @remarks
|
|
36
|
-
* Applications should periodically call `lowerVolume()` to keep
|
|
36
|
+
* Applications should periodically call `lowerVolume()` to keep the volume lowered.
|
|
37
37
|
*/
|
|
38
38
|
public get duration(): number {
|
|
39
39
|
return this._duration.seconds;
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
-
* Licensed under the
|
|
3
|
+
* Licensed under the Microsoft Live Share SDK License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { IEvent, IEphemeralEvent, EphemeralEvent, TimeInterval, IRuntimeSignaler, EphemeralTelemetryLogger } from '@microsoft/live-share';
|
|
@@ -14,6 +14,9 @@ import { GroupTransportStateEvents } from './GroupTransportState';
|
|
|
14
14
|
import { GroupPlaybackTrackData, PlaybackTrackDataEvents, IPlaybackTrackData } from './GroupPlaybackTrackData';
|
|
15
15
|
import { TelemetryEvents } from './consts';
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* @hidden
|
|
19
|
+
*/
|
|
17
20
|
export interface IPositionUpdateEvent extends IEphemeralEvent {
|
|
18
21
|
track: IPlaybackTrack;
|
|
19
22
|
trackData: IPlaybackTrackData;
|
|
@@ -23,35 +26,53 @@ export interface IPositionUpdateEvent extends IEphemeralEvent {
|
|
|
23
26
|
waitPoint?: CoordinationWaitPoint;
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @hidden
|
|
31
|
+
*/
|
|
26
32
|
export interface ITransportCommandEvent extends IEphemeralEvent {
|
|
27
33
|
track: IPlaybackTrack;
|
|
28
34
|
position: number;
|
|
29
35
|
}
|
|
30
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @hidden
|
|
39
|
+
*/
|
|
31
40
|
export interface ISetTrackEvent extends IEphemeralEvent {
|
|
32
41
|
metadata: ExtendedMediaMetadata|null;
|
|
33
42
|
waitPoints: CoordinationWaitPoint[];
|
|
34
43
|
}
|
|
35
44
|
|
|
45
|
+
/**
|
|
46
|
+
* @hidden
|
|
47
|
+
*/
|
|
36
48
|
export interface ISetTrackDataEvent extends IEphemeralEvent {
|
|
37
49
|
data: object|null;
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
|
|
52
|
+
/**
|
|
53
|
+
* @hidden
|
|
54
|
+
*/
|
|
55
|
+
export interface ITriggerActionEvent extends IEvent {
|
|
41
56
|
details: ExtendedMediaSessionActionDetails;
|
|
42
57
|
}
|
|
43
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @hidden
|
|
61
|
+
*/
|
|
44
62
|
export enum GroupCoordinatorStateEvents {
|
|
45
63
|
newwaitpoint = 'newwaitpoint',
|
|
46
64
|
triggeraction = 'triggeraction'
|
|
47
65
|
}
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
/**
|
|
68
|
+
* @hidden
|
|
69
|
+
*/
|
|
70
|
+
export class GroupCoordinatorState extends EventEmitter {
|
|
71
|
+
private readonly _runtime: IRuntimeSignaler;
|
|
51
72
|
private readonly _logger: EphemeralTelemetryLogger;
|
|
52
73
|
private readonly _maxPlaybackDrift: TimeInterval;
|
|
53
74
|
private _getMediaPlayerState: () => IMediaPlayerState;
|
|
54
|
-
|
|
75
|
+
|
|
55
76
|
// Shared group state
|
|
56
77
|
private _playbackTrack: GroupPlaybackTrack;
|
|
57
78
|
private _playbackTrackData: GroupPlaybackTrackData;
|
|
@@ -65,7 +86,7 @@ export class GroupCoordinatorState extends EventEmitter {
|
|
|
65
86
|
// "Soft Suspension" tracking
|
|
66
87
|
// - Soft suspensions temporarily disconnect the local media session when transitioning between
|
|
67
88
|
// paused, playing, and none states. This is to give the local client time to finish any inprogress
|
|
68
|
-
// operations like sending a transport command or ending a suspension.
|
|
89
|
+
// operations like sending a transport command or ending a suspension.
|
|
69
90
|
private _lastStateChange: ExtendedMediaSessionPlaybackState = 'none';
|
|
70
91
|
private _lastStateChangeTime: number = 0;
|
|
71
92
|
|
|
@@ -161,13 +182,13 @@ export class GroupCoordinatorState extends EventEmitter {
|
|
|
161
182
|
|
|
162
183
|
public beginSuspension(waitPoint?: CoordinationWaitPoint): void {
|
|
163
184
|
this._suspensionCnt++;
|
|
164
|
-
|
|
185
|
+
|
|
165
186
|
if (waitPoint) {
|
|
166
187
|
// Save most recent wait point
|
|
167
188
|
this._waitPoint = waitPoint;
|
|
168
189
|
|
|
169
190
|
// Add to track state if dynamic wait point
|
|
170
|
-
this.playbackTrack.addWaitPoint(waitPoint);
|
|
191
|
+
this.playbackTrack.addWaitPoint(waitPoint);
|
|
171
192
|
}
|
|
172
193
|
}
|
|
173
194
|
|
|
@@ -204,7 +225,7 @@ export class GroupCoordinatorState extends EventEmitter {
|
|
|
204
225
|
playbackState: this._waitPoint ? 'waiting' : playbackState,
|
|
205
226
|
position: positionState?.position || 0.0,
|
|
206
227
|
waitPoint: this._waitPoint
|
|
207
|
-
};
|
|
228
|
+
};
|
|
208
229
|
}
|
|
209
230
|
}
|
|
210
231
|
|
|
@@ -251,7 +272,7 @@ export class GroupCoordinatorState extends EventEmitter {
|
|
|
251
272
|
case 'pause':
|
|
252
273
|
playbackState = 'paused';
|
|
253
274
|
break;
|
|
254
|
-
}
|
|
275
|
+
}
|
|
255
276
|
|
|
256
277
|
// Try to update playback state
|
|
257
278
|
const newState: ITransportState = {
|
|
@@ -312,7 +333,7 @@ export class GroupCoordinatorState extends EventEmitter {
|
|
|
312
333
|
// Begin a "soft suspension" on select state changes
|
|
313
334
|
// - This is needed because the catchup logic can try to sink the client with
|
|
314
335
|
// the rest of the group while the local player is trying to to seek to a
|
|
315
|
-
// new position.
|
|
336
|
+
// new position.
|
|
316
337
|
switch (event.playbackState) {
|
|
317
338
|
case 'none':
|
|
318
339
|
case 'paused':
|
|
@@ -349,7 +370,7 @@ export class GroupCoordinatorState extends EventEmitter {
|
|
|
349
370
|
if (!this.isWaiting && softSuspensionDelta >= 1000) {
|
|
350
371
|
let { metadata, trackData, positionState, playbackState } = this._getMediaPlayerState();
|
|
351
372
|
this._logger.sendTelemetryEvent(TelemetryEvents.GroupCoordinator.CheckingForSyncIssues);
|
|
352
|
-
|
|
373
|
+
|
|
353
374
|
// Check for track change
|
|
354
375
|
if (!this.playbackTrack.compare(metadata)) {
|
|
355
376
|
this._logger.sendTelemetryEvent(TelemetryEvents.GroupCoordinator.TrackOutOfSync);
|
|
@@ -359,7 +380,7 @@ export class GroupCoordinatorState extends EventEmitter {
|
|
|
359
380
|
|
|
360
381
|
// Once local playback has ended there's no need to further sync the client. In fact
|
|
361
382
|
// doing so could cause the player to loop.
|
|
362
|
-
// - The playbackPosition.trackEnded check is to catch late joiners and prevents syncing if everyone
|
|
383
|
+
// - The playbackPosition.trackEnded check is to catch late joiners and prevents syncing if everyone
|
|
363
384
|
// else has finished the video.
|
|
364
385
|
// - Should another user seek or press play after the video has ended that will cause
|
|
365
386
|
// a transport action to trigger which will take the player out of the ended state.
|
|
@@ -374,18 +395,18 @@ export class GroupCoordinatorState extends EventEmitter {
|
|
|
374
395
|
// - Target can return -1 in cases where there is no position tracking data
|
|
375
396
|
const target = this.playbackPosition.targetPosition;
|
|
376
397
|
if (target >= 0.0 && Math.abs(target - position) >= this._maxPlaybackDrift.seconds) {
|
|
377
|
-
this._logger.sendTelemetryEvent(TelemetryEvents.GroupCoordinator.PositionOutOfSync, null, {
|
|
378
|
-
current: position,
|
|
379
|
-
target: target
|
|
398
|
+
this._logger.sendTelemetryEvent(TelemetryEvents.GroupCoordinator.PositionOutOfSync, null, {
|
|
399
|
+
current: position,
|
|
400
|
+
target: target
|
|
380
401
|
});
|
|
381
402
|
await this.emitTriggerAction({action: 'catchup', seekTime: target});
|
|
382
403
|
}
|
|
383
404
|
|
|
384
405
|
// Sync transport state
|
|
385
406
|
if (this.transportState.playbackState != playbackState) {
|
|
386
|
-
this._logger.sendTelemetryEvent(TelemetryEvents.GroupCoordinator.TransportOutOfSync, null, {
|
|
407
|
+
this._logger.sendTelemetryEvent(TelemetryEvents.GroupCoordinator.TransportOutOfSync, null, {
|
|
387
408
|
current: playbackState,
|
|
388
|
-
target: this.transportState.playbackState
|
|
409
|
+
target: this.transportState.playbackState
|
|
389
410
|
});
|
|
390
411
|
switch (this.transportState.playbackState) {
|
|
391
412
|
case 'playing':
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
-
* Licensed under the
|
|
3
|
+
* Licensed under the Microsoft Live Share SDK License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { EphemeralEvent, IRuntimeSignaler, TimeInterval } from '@microsoft/live-share';
|
|
@@ -8,7 +8,10 @@ import { GroupTransportState } from './GroupTransportState';
|
|
|
8
8
|
import { GroupPlaybackTrackEvents } from './GroupPlaybackTrack';
|
|
9
9
|
import { CoordinationWaitPoint, ExtendedMediaSessionPlaybackState } from '../MediaSessionExtensions';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
*Per client position
|
|
13
|
+
* @hidden
|
|
14
|
+
*/
|
|
12
15
|
export interface ICurrentPlaybackPosition {
|
|
13
16
|
playbackState: ExtendedMediaSessionPlaybackState;
|
|
14
17
|
waitPoint?: CoordinationWaitPoint
|
|
@@ -18,13 +21,16 @@ export interface ICurrentPlaybackPosition {
|
|
|
18
21
|
clientId: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
/**
|
|
25
|
+
* @hidden
|
|
26
|
+
*/
|
|
21
27
|
export class GroupPlaybackPosition {
|
|
22
28
|
private _transportState: GroupTransportState;
|
|
23
29
|
private _runtime: IRuntimeSignaler;
|
|
24
30
|
private _updateInterval: TimeInterval;
|
|
25
31
|
private _positions: Map<string, ICurrentPlaybackPosition>;
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
|
|
28
34
|
constructor(transportState: GroupTransportState, runtime: IRuntimeSignaler, updateInterval: TimeInterval) {
|
|
29
35
|
this._transportState = transportState;
|
|
30
36
|
this._runtime = runtime;
|
|
@@ -67,14 +73,14 @@ export class GroupPlaybackPosition {
|
|
|
67
73
|
case 'ended':
|
|
68
74
|
// not playing.
|
|
69
75
|
break;
|
|
70
|
-
default:
|
|
76
|
+
default:
|
|
71
77
|
playing++;
|
|
72
78
|
break;
|
|
73
79
|
}
|
|
74
80
|
});
|
|
75
81
|
|
|
76
82
|
return playing == 0;
|
|
77
|
-
}
|
|
83
|
+
}
|
|
78
84
|
|
|
79
85
|
/**
|
|
80
86
|
* Returns the number of clients we're waiting for before we can stop waiting.
|
|
@@ -93,13 +99,13 @@ export class GroupPlaybackPosition {
|
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
return cnt;
|
|
96
|
-
}
|
|
102
|
+
}
|
|
97
103
|
|
|
98
104
|
/**
|
|
99
105
|
* Returns the max playback position relative to the start position.
|
|
100
|
-
*
|
|
106
|
+
*
|
|
101
107
|
* @remarks
|
|
102
|
-
* This is called when calculating the current seekTo position.
|
|
108
|
+
* This is called when calculating the current seekTo position.
|
|
103
109
|
*/
|
|
104
110
|
public get maxPosition(): number {
|
|
105
111
|
if (this._transportState.playbackState == 'playing') {
|
|
@@ -151,7 +157,7 @@ export class GroupPlaybackPosition {
|
|
|
151
157
|
if (position.duration != undefined) {
|
|
152
158
|
this.mediaDuration = position.duration;
|
|
153
159
|
}
|
|
154
|
-
|
|
160
|
+
|
|
155
161
|
// Save last position
|
|
156
162
|
if (this._positions.has(position.clientId)) {
|
|
157
163
|
// Only update if newer position
|
|
@@ -166,7 +172,7 @@ export class GroupPlaybackPosition {
|
|
|
166
172
|
|
|
167
173
|
private getMostProgressedPosition(): number {
|
|
168
174
|
// Compute the max possible position for the current transport state.
|
|
169
|
-
// - This is needed to properly handle seeking backwards in time. Some playback heads may
|
|
175
|
+
// - This is needed to properly handle seeking backwards in time. Some playback heads may
|
|
170
176
|
// not have performed their seek yet and will therefore be ahead of the local player.
|
|
171
177
|
const maxPosition = this.maxPosition;
|
|
172
178
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
-
* Licensed under the
|
|
3
|
+
* Licensed under the Microsoft Live Share SDK License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { IEvent } from '@microsoft/live-share';
|
|
@@ -8,22 +8,35 @@ import EventEmitter from 'events';
|
|
|
8
8
|
import { IMediaPlayerState } from '../EphemeralMediaSessionCoordinator';
|
|
9
9
|
import { CoordinationWaitPoint, ExtendedMediaMetadata } from '../MediaSessionExtensions';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* @hidden
|
|
13
|
+
*/
|
|
14
|
+
export interface IPlaybackTrack {
|
|
12
15
|
metadata: ExtendedMediaMetadata|null;
|
|
13
16
|
waitPoints: CoordinationWaitPoint[];
|
|
14
17
|
timestamp: number;
|
|
15
18
|
clientId: string;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @hidden
|
|
24
|
+
*/
|
|
25
|
+
export enum GroupPlaybackTrackEvents {
|
|
19
26
|
trackChange = 'trackChange'
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
/**
|
|
30
|
+
* @hidden
|
|
31
|
+
*/
|
|
32
|
+
export interface IPlaybackTrackChangeEvent extends IEvent {
|
|
23
33
|
metadata: ExtendedMediaMetadata|null;
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
/**
|
|
37
|
+
* @hidden
|
|
38
|
+
*/
|
|
39
|
+
export class GroupPlaybackTrack extends EventEmitter {
|
|
27
40
|
private readonly _getMediaPlayerState: () => IMediaPlayerState;
|
|
28
41
|
private _current: IPlaybackTrack;
|
|
29
42
|
|
|
@@ -95,9 +108,9 @@ export class GroupPlaybackTrack extends EventEmitter {
|
|
|
95
108
|
if (track.timestamp == current.timestamp && track.clientId == current.clientId) {
|
|
96
109
|
return false;
|
|
97
110
|
}
|
|
98
|
-
}
|
|
111
|
+
}
|
|
99
112
|
|
|
100
|
-
// Ignore state changes that are older
|
|
113
|
+
// Ignore state changes that are older
|
|
101
114
|
if (track.timestamp < current.timestamp) {
|
|
102
115
|
return false;
|
|
103
116
|
}
|
|
@@ -112,8 +125,8 @@ export class GroupPlaybackTrack extends EventEmitter {
|
|
|
112
125
|
this._current = track;
|
|
113
126
|
|
|
114
127
|
// Notify listeners
|
|
115
|
-
this.emit(GroupPlaybackTrackEvents.trackChange, { name: GroupPlaybackTrackEvents.trackChange, metadata: track.metadata });
|
|
116
|
-
|
|
128
|
+
this.emit(GroupPlaybackTrackEvents.trackChange, { name: GroupPlaybackTrackEvents.trackChange, metadata: track.metadata });
|
|
129
|
+
|
|
117
130
|
return true;
|
|
118
131
|
}
|
|
119
132
|
|