@microsoft/live-share-media 0.5.0 → 1.0.0-preview.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.
- package/.vscode/launch.json +29 -0
- package/.vscode/settings.json +2 -0
- package/README.md +6 -5
- package/bin/LiveMediaSession.d.ts +95 -0
- package/bin/LiveMediaSession.d.ts.map +1 -0
- package/bin/LiveMediaSession.js +217 -0
- package/bin/LiveMediaSession.js.map +1 -0
- package/bin/LiveMediaSessionCoordinator.d.ts +228 -0
- package/bin/LiveMediaSessionCoordinator.d.ts.map +1 -0
- package/bin/LiveMediaSessionCoordinator.js +396 -0
- package/bin/LiveMediaSessionCoordinator.js.map +1 -0
- package/bin/LiveMediaSessionCoordinatorSuspension.d.ts +20 -0
- package/bin/LiveMediaSessionCoordinatorSuspension.d.ts.map +1 -0
- package/bin/LiveMediaSessionCoordinatorSuspension.js +28 -0
- package/bin/LiveMediaSessionCoordinatorSuspension.js.map +1 -0
- package/bin/MediaPlayerSynchronizer.d.ts +6 -6
- package/bin/MediaPlayerSynchronizer.d.ts.map +1 -1
- package/bin/MediaPlayerSynchronizer.js +12 -7
- package/bin/MediaPlayerSynchronizer.js.map +1 -1
- package/bin/VolumeManager.d.ts +84 -0
- package/bin/VolumeManager.d.ts.map +1 -0
- package/bin/VolumeManager.js +173 -0
- package/bin/VolumeManager.js.map +1 -0
- package/bin/index.d.ts +4 -4
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +4 -4
- package/bin/index.js.map +1 -1
- package/bin/internals/GroupCoordinatorState.d.ts +6 -6
- package/bin/internals/GroupCoordinatorState.d.ts.map +1 -1
- package/bin/internals/GroupCoordinatorState.js +6 -6
- package/bin/internals/GroupCoordinatorState.js.map +1 -1
- package/bin/internals/GroupPlaybackPosition.d.ts.map +1 -1
- package/bin/internals/GroupPlaybackPosition.js +2 -2
- package/bin/internals/GroupPlaybackPosition.js.map +1 -1
- package/bin/internals/GroupPlaybackTrack.d.ts +1 -1
- package/bin/internals/GroupPlaybackTrack.d.ts.map +1 -1
- package/bin/internals/GroupTransportState.d.ts +1 -1
- package/bin/internals/GroupTransportState.d.ts.map +1 -1
- package/bin/internals/GroupTransportState.js +1 -1
- package/bin/internals/GroupTransportState.js.map +1 -1
- package/package.json +2 -2
- package/src/{EphemeralMediaSession.ts → LiveMediaSession.ts} +13 -23
- package/src/{EphemeralMediaSessionCoordinator.ts → LiveMediaSessionCoordinator.ts} +41 -41
- package/src/{EphemeralMediaSessionCoordinatorSuspension.ts → LiveMediaSessionCoordinatorSuspension.ts} +1 -1
- package/src/MediaPlayerSynchronizer.ts +20 -14
- package/src/VolumeManager.ts +185 -0
- package/src/index.ts +4 -4
- package/src/internals/GroupCoordinatorState.ts +13 -13
- package/src/internals/GroupPlaybackPosition.ts +3 -3
- package/src/internals/GroupPlaybackTrack.ts +1 -1
- package/src/internals/GroupTransportState.ts +3 -3
- package/src/test/SharedMediaSession.spec.ts +2 -2
- package/src/test/VolumeManager.spec.ts +212 -0
- package/src/VolumeLimiter.ts +0 -182
|
@@ -4,23 +4,23 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { DataObject, DataObjectFactory } from '@fluidframework/aqueduct';
|
|
7
|
-
import {
|
|
7
|
+
import { LiveTelemetryLogger, UserMeetingRole } from '@microsoft/live-share';
|
|
8
8
|
import { MediaPlayerSynchronizer } from './MediaPlayerSynchronizer';
|
|
9
9
|
import { ITriggerActionEvent, TelemetryEvents } from './internals';
|
|
10
10
|
import { MediaSessionCoordinatorEvents, ExtendedMediaSessionAction, ExtendedMediaSessionActionDetails } from './MediaSessionExtensions';
|
|
11
|
-
import {
|
|
11
|
+
import { LiveMediaSessionCoordinator, IMediaPlayerState } from './LiveMediaSessionCoordinator';
|
|
12
12
|
import { MediaSessionActionThrottler } from './MediaSessionActionThrottler';
|
|
13
13
|
import { RepeatedActionThrottler } from './RepeatedActionThrottler';
|
|
14
14
|
import { IMediaPlayer } from './IMediaPlayer';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Live fluid object that synchronizes media playback across multiple clients.
|
|
18
18
|
*/
|
|
19
|
-
export class
|
|
19
|
+
export class LiveMediaSession extends DataObject {
|
|
20
20
|
private _actionThrottler: MediaSessionActionThrottler = new RepeatedActionThrottler();
|
|
21
|
-
private _logger?:
|
|
21
|
+
private _logger?: LiveTelemetryLogger;
|
|
22
22
|
private _requestPlayerStateHandler?: () => IMediaPlayerState;
|
|
23
|
-
private _coordinator?:
|
|
23
|
+
private _coordinator?: LiveMediaSessionCoordinator;
|
|
24
24
|
private readonly _actionHandlers: Map<string, MediaSessionActionHandler> = new Map();
|
|
25
25
|
private _synchronizing?: MediaPlayerSynchronizer;
|
|
26
26
|
|
|
@@ -37,8 +37,8 @@ import { IMediaPlayer } from './IMediaPlayer';
|
|
|
37
37
|
* The objects fluid type factory.
|
|
38
38
|
*/
|
|
39
39
|
public static readonly factory = new DataObjectFactory(
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
LiveMediaSession.TypeName,
|
|
41
|
+
LiveMediaSession,
|
|
42
42
|
[],
|
|
43
43
|
{}
|
|
44
44
|
);
|
|
@@ -48,7 +48,7 @@ import { IMediaPlayer } from './IMediaPlayer';
|
|
|
48
48
|
* local sync behavior.
|
|
49
49
|
*
|
|
50
50
|
* @remarks
|
|
51
|
-
* The `
|
|
51
|
+
* The `LiveMediaCoordinator` is fairly aggressive at wanting to keep the local media player
|
|
52
52
|
* in sync with the rest of the group. This aggressiveness can result in the coordinator sending
|
|
53
53
|
* new sync actions before the local player has finished responding to the previous sync action.
|
|
54
54
|
* The `ActionThrottler` gives apps fine grain control over how aggressive they want sync to be.
|
|
@@ -82,7 +82,7 @@ import { IMediaPlayer } from './IMediaPlayer';
|
|
|
82
82
|
/**
|
|
83
83
|
* The group coordinator for the session.
|
|
84
84
|
*/
|
|
85
|
-
public get coordinator():
|
|
85
|
+
public get coordinator(): LiveMediaSessionCoordinator {
|
|
86
86
|
return this._coordinator!;
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -92,7 +92,7 @@ import { IMediaPlayer } from './IMediaPlayer';
|
|
|
92
92
|
* @remarks
|
|
93
93
|
* This is used by the `MediaPlayerSynchronizer` to log events.
|
|
94
94
|
*/
|
|
95
|
-
public get logger():
|
|
95
|
+
public get logger(): LiveTelemetryLogger {
|
|
96
96
|
return this._logger!;
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -105,16 +105,6 @@ import { IMediaPlayer } from './IMediaPlayer';
|
|
|
105
105
|
return this.coordinator.initialize(acceptTransportChangesFrom);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
/**
|
|
109
|
-
* @deprecated initialize should be used instead
|
|
110
|
-
* initialize the object.
|
|
111
|
-
* @param acceptTransportChangesFrom Optional. List of roles allowed to group transport
|
|
112
|
-
* operations like play/pause/seek/setTrack.
|
|
113
|
-
*/
|
|
114
|
-
public start(acceptTransportChangesFrom?: UserMeetingRole[]): Promise<void> {
|
|
115
|
-
return this.initialize(acceptTransportChangesFrom)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
108
|
/**
|
|
119
109
|
* Registers an action handler with the session.
|
|
120
110
|
* @param action Name of the action to register a handler for.
|
|
@@ -176,10 +166,10 @@ import { IMediaPlayer } from './IMediaPlayer';
|
|
|
176
166
|
}
|
|
177
167
|
|
|
178
168
|
protected async hasInitialized(): Promise<void> {
|
|
179
|
-
this. _logger = new
|
|
169
|
+
this. _logger = new LiveTelemetryLogger(this.runtime);
|
|
180
170
|
|
|
181
171
|
// Create coordinator and listen for triggered actions
|
|
182
|
-
this._coordinator = new
|
|
172
|
+
this._coordinator = new LiveMediaSessionCoordinator(this.runtime, () => this.getCurrentPlayerState());
|
|
183
173
|
this._coordinator.on(MediaSessionCoordinatorEvents.triggeraction, (event: ITriggerActionEvent) => {
|
|
184
174
|
// Pre-process actions
|
|
185
175
|
const details = event.details;
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Licensed under the Microsoft Live Share SDK License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { LiveEventScope, LiveTelemetryLogger, LiveEventTarget, ILiveEvent, IRuntimeSignaler, TimeInterval, UserMeetingRole } from '@microsoft/live-share';
|
|
7
7
|
import { CoordinationWaitPoint, ExtendedMediaSessionActionDetails, ExtendedMediaMetadata, ExtendedMediaSessionPlaybackState, MediaSessionCoordinatorEvents, MediaSessionCoordinatorState, MediaSessionCoordinatorSuspension } from './MediaSessionExtensions';
|
|
8
8
|
import { TelemetryEvents, ITransportCommandEvent, ISetTrackEvent, IPositionUpdateEvent, GroupCoordinatorState, GroupCoordinatorStateEvents, ITriggerActionEvent, ISetTrackDataEvent } from './internals';
|
|
9
|
-
import {
|
|
9
|
+
import { LiveMediaSessionCoordinatorSuspension } from './LiveMediaSessionCoordinatorSuspension';
|
|
10
10
|
import EventEmitter from "events";
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -38,13 +38,13 @@ export interface IMediaPlayerState {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* The `
|
|
41
|
+
* The `LiveMediaSessionCoordinator` tracks the playback & position state of all other
|
|
42
42
|
* clients being synchronized with. It is responsible for keeping the local media player
|
|
43
43
|
* in sync with the group.
|
|
44
44
|
*/
|
|
45
|
-
export class
|
|
45
|
+
export class LiveMediaSessionCoordinator extends EventEmitter {
|
|
46
46
|
private readonly _runtime: IRuntimeSignaler;
|
|
47
|
-
private readonly _logger:
|
|
47
|
+
private readonly _logger: LiveTelemetryLogger;
|
|
48
48
|
private readonly _getPlayerState: () => IMediaPlayerState;
|
|
49
49
|
private _positionUpdateInterval = new TimeInterval(2000);
|
|
50
50
|
private _maxPlaybackDrift = new TimeInterval(1000);
|
|
@@ -52,13 +52,13 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
52
52
|
private _hasInitialized = false;
|
|
53
53
|
|
|
54
54
|
// Broadcast events
|
|
55
|
-
private _playEvent?:
|
|
56
|
-
private _pauseEvent?:
|
|
57
|
-
private _seekToEvent?:
|
|
58
|
-
private _setTrackEvent?:
|
|
59
|
-
private _setTrackDataEvent?:
|
|
60
|
-
private _positionUpdateEvent?:
|
|
61
|
-
private _joinedEvent?:
|
|
55
|
+
private _playEvent?: LiveEventTarget<ITransportCommandEvent>;
|
|
56
|
+
private _pauseEvent?: LiveEventTarget<ITransportCommandEvent>;
|
|
57
|
+
private _seekToEvent?: LiveEventTarget<ITransportCommandEvent>;
|
|
58
|
+
private _setTrackEvent?: LiveEventTarget<ISetTrackEvent>;
|
|
59
|
+
private _setTrackDataEvent?: LiveEventTarget<ISetTrackDataEvent>;
|
|
60
|
+
private _positionUpdateEvent?: LiveEventTarget<IPositionUpdateEvent>;
|
|
61
|
+
private _joinedEvent?: LiveEventTarget<ILiveEvent>;
|
|
62
62
|
|
|
63
63
|
// Distributed state
|
|
64
64
|
private _groupState?: GroupCoordinatorState;
|
|
@@ -70,7 +70,7 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
70
70
|
constructor(runtime: IRuntimeSignaler, getPlayerState: () => IMediaPlayerState) {
|
|
71
71
|
super();
|
|
72
72
|
this._runtime = runtime;
|
|
73
|
-
this._logger = new
|
|
73
|
+
this._logger = new LiveTelemetryLogger(runtime);
|
|
74
74
|
this._getPlayerState = getPlayerState;
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -177,15 +177,15 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
177
177
|
*/
|
|
178
178
|
public play(): void {
|
|
179
179
|
if (!this._hasInitialized) {
|
|
180
|
-
throw new Error(`
|
|
180
|
+
throw new Error(`LiveMediaSessionCoordinator.play() called before initialize() called.`);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
if (!this._groupState?.playbackTrack.current.metadata) {
|
|
184
|
-
throw new Error(`
|
|
184
|
+
throw new Error(`LiveMediaSessionCoordinator.play() called before MediaSession.metadata assigned.`);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
if (!this.canPlayPause) {
|
|
188
|
-
throw new Error(`
|
|
188
|
+
throw new Error(`LiveMediaSessionCoordinator.play() operation blocked.`);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
// Get projected position
|
|
@@ -208,15 +208,15 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
208
208
|
*/
|
|
209
209
|
public pause(): void {
|
|
210
210
|
if (!this._hasInitialized) {
|
|
211
|
-
throw new Error(`
|
|
211
|
+
throw new Error(`LiveMediaSessionCoordinator.pause() called before initialize() called.`);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
if (!this._groupState?.playbackTrack.current.metadata) {
|
|
215
|
-
throw new Error(`
|
|
215
|
+
throw new Error(`LiveMediaSessionCoordinator.pause() called before MediaSession.metadata assigned.`);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
if (!this.canPlayPause) {
|
|
219
|
-
throw new Error(`
|
|
219
|
+
throw new Error(`LiveMediaSessionCoordinator.pause() operation blocked.`);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// Get projected position
|
|
@@ -240,15 +240,15 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
240
240
|
*/
|
|
241
241
|
public seekTo(time: number): void {
|
|
242
242
|
if (!this._hasInitialized) {
|
|
243
|
-
throw new Error(`
|
|
243
|
+
throw new Error(`LiveMediaSessionCoordinator.seekTo() called before initialize() called.`);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
if (!this._groupState?.playbackTrack.current.metadata) {
|
|
247
|
-
throw new Error(`
|
|
247
|
+
throw new Error(`LiveMediaSessionCoordinator.seekTo() called before MediaSession.metadata assigned.`);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
if (!this.canSeek) {
|
|
251
|
-
throw new Error(`
|
|
251
|
+
throw new Error(`LiveMediaSessionCoordinator.seekTo() operation blocked.`);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
// Send transport command
|
|
@@ -270,11 +270,11 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
270
270
|
*/
|
|
271
271
|
public setTrack(metadata: ExtendedMediaMetadata|null, waitPoints?: CoordinationWaitPoint[]): void {
|
|
272
272
|
if (!this._hasInitialized) {
|
|
273
|
-
throw new Error(`
|
|
273
|
+
throw new Error(`LiveMediaSessionCoordinator.setTrack() called before initialize() called.`);
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
if (!this.canSetTrack) {
|
|
277
|
-
throw new Error(`
|
|
277
|
+
throw new Error(`LiveMediaSessionCoordinator.setTrack() operation blocked.`);
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
// Send transport command
|
|
@@ -298,11 +298,11 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
298
298
|
*/
|
|
299
299
|
public setTrackData(data: object|null): void {
|
|
300
300
|
if (!this._hasInitialized) {
|
|
301
|
-
throw new Error(`
|
|
301
|
+
throw new Error(`LiveMediaSessionCoordinator.setTrackData() called before initialize() called.`);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
if (!this.canSetTrackData) {
|
|
305
|
-
throw new Error(`
|
|
305
|
+
throw new Error(`LiveMediaSessionCoordinator.setTrackData() operation blocked.`);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
// Send transport command
|
|
@@ -338,11 +338,11 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
338
338
|
*/
|
|
339
339
|
public beginSuspension(waitPoint?: CoordinationWaitPoint): MediaSessionCoordinatorSuspension {
|
|
340
340
|
if (!this._hasInitialized) {
|
|
341
|
-
throw new Error(`
|
|
341
|
+
throw new Error(`LiveMediaSessionCoordinator.beginSuspension() called before initialize() called.`);
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
if (!this._groupState?.playbackTrack.current.metadata) {
|
|
345
|
-
throw new Error(`
|
|
345
|
+
throw new Error(`LiveMediaSessionCoordinator.beginSuspension() called before MediaSession.metadata assigned.`);
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
// Tell group state that suspension is started
|
|
@@ -356,7 +356,7 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
// Return new suspension object
|
|
359
|
-
return new
|
|
359
|
+
return new LiveMediaSessionCoordinatorSuspension(waitPoint, time => {
|
|
360
360
|
if (waitPoint) {
|
|
361
361
|
this._logger.sendTelemetryEvent(TelemetryEvents.SessionCoordinator.EndSuspensionAndWait, null, {position: waitPoint.position, maxClients: waitPoint.maxClients});
|
|
362
362
|
} else {
|
|
@@ -384,7 +384,7 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
384
384
|
*/
|
|
385
385
|
public async initialize(acceptTransportChangesFrom?: UserMeetingRole[]): Promise<void> {
|
|
386
386
|
if (this._hasInitialized) {
|
|
387
|
-
throw new Error(`
|
|
387
|
+
throw new Error(`LiveMediaSessionCoordinator.initialize() already initialized.`);
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
// Create children
|
|
@@ -406,7 +406,7 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
406
406
|
*/
|
|
407
407
|
public sendPositionUpdate(state: IMediaPlayerState): void {
|
|
408
408
|
if (!this._hasInitialized) {
|
|
409
|
-
throw new Error(`
|
|
409
|
+
throw new Error(`LiveMediaSessionCoordinator.sendPositionUpdate() called before initialize() called.`);
|
|
410
410
|
}
|
|
411
411
|
|
|
412
412
|
// Send position update event
|
|
@@ -416,29 +416,29 @@ export class EphemeralMediaSessionCoordinator extends EventEmitter {
|
|
|
416
416
|
|
|
417
417
|
protected async createChildren(acceptTransportChangesFrom?: UserMeetingRole[]): Promise<void> {
|
|
418
418
|
// Create event scopes
|
|
419
|
-
const scope = new
|
|
420
|
-
const unrestrictedScope = new
|
|
419
|
+
const scope = new LiveEventScope(this._runtime, acceptTransportChangesFrom);
|
|
420
|
+
const unrestrictedScope = new LiveEventScope(this._runtime);
|
|
421
421
|
|
|
422
422
|
// Initialize internal coordinator state
|
|
423
423
|
this._groupState = new GroupCoordinatorState(this._runtime, this._maxPlaybackDrift, this._positionUpdateInterval, this._getPlayerState);
|
|
424
424
|
this._groupState.on(GroupCoordinatorStateEvents.triggeraction, evt => this.emit(evt.name, evt));
|
|
425
425
|
|
|
426
426
|
// Listen for track changes
|
|
427
|
-
this._setTrackEvent = new
|
|
428
|
-
this._setTrackDataEvent = new
|
|
427
|
+
this._setTrackEvent = new LiveEventTarget<ISetTrackEvent>(scope, 'setTrack', (event, local) => this._groupState!.handleSetTrack(event, local));
|
|
428
|
+
this._setTrackDataEvent = new LiveEventTarget<ISetTrackDataEvent>(scope, 'setTrackData', (event, local) => this._groupState!.handleSetTrackData(event, local));
|
|
429
429
|
|
|
430
430
|
// Listen for transport commands
|
|
431
|
-
this._playEvent = new
|
|
432
|
-
this._pauseEvent = new
|
|
433
|
-
this._seekToEvent = new
|
|
431
|
+
this._playEvent = new LiveEventTarget(scope, 'play', (event, local) => this._groupState!.handleTransportCommand(event, local));
|
|
432
|
+
this._pauseEvent = new LiveEventTarget(scope, 'pause', (event, local) => this._groupState!.handleTransportCommand(event, local));
|
|
433
|
+
this._seekToEvent = new LiveEventTarget(scope, 'seekTo', (event, local) => this._groupState!.handleTransportCommand(event, local));
|
|
434
434
|
|
|
435
435
|
// Listen for position updates
|
|
436
|
-
this._positionUpdateEvent = new
|
|
436
|
+
this._positionUpdateEvent = new LiveEventTarget(unrestrictedScope, 'positionUpdate', (event, local) => this._groupState!.handlePositionUpdate(event, local));
|
|
437
437
|
|
|
438
438
|
// Listen for joined event
|
|
439
|
-
this._joinedEvent = new
|
|
439
|
+
this._joinedEvent = new LiveEventTarget(unrestrictedScope, 'joined', (evt, local) => {
|
|
440
440
|
// Immediately send a position update
|
|
441
|
-
this._logger.sendTelemetryEvent(TelemetryEvents.SessionCoordinator.RemoteJoinReceived, null, {correlationId:
|
|
441
|
+
this._logger.sendTelemetryEvent(TelemetryEvents.SessionCoordinator.RemoteJoinReceived, null, {correlationId: LiveTelemetryLogger.formatCorrelationId(evt.clientId, evt.timestamp)});
|
|
442
442
|
const state = this._getPlayerState();
|
|
443
443
|
const update = this._groupState!.createPositionUpdateEvent(state);
|
|
444
444
|
this._positionUpdateEvent?.sendEvent(update);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { MediaSessionCoordinatorSuspension, CoordinationWaitPoint } from './MediaSessionExtensions';
|
|
7
7
|
|
|
8
|
-
export class
|
|
8
|
+
export class LiveMediaSessionCoordinatorSuspension implements MediaSessionCoordinatorSuspension {
|
|
9
9
|
private _waitPoint?: CoordinationWaitPoint;
|
|
10
10
|
private _onEnd: (seekTo?: number) => void;
|
|
11
11
|
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Licensed under the Microsoft Live Share SDK License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { LiveTelemetryLogger, IEvent, TimeInterval } from '@microsoft/live-share';
|
|
7
7
|
import EventEmitter from 'events';
|
|
8
8
|
import { ExtendedMediaSessionAction, ExtendedMediaSessionPlaybackState, ExtendedMediaMetadata, CoordinationWaitPoint, ExtendedMediaSessionActionDetails, MediaSessionCoordinatorEvents, MediaSessionCoordinatorSuspension } from './MediaSessionExtensions';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { VolumeManager } from './VolumeManager';
|
|
10
|
+
import { LiveMediaSession } from './LiveMediaSession';
|
|
11
11
|
import { IMediaPlayer } from './IMediaPlayer';
|
|
12
12
|
import { TelemetryEvents } from './internals';
|
|
13
13
|
|
|
@@ -50,10 +50,10 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
50
50
|
private static SESSION_ACTIONS: ExtendedMediaSessionAction[] = ['play', 'pause', 'seekto', 'settrack', 'datachange', 'catchup', 'wait'];
|
|
51
51
|
private static PLAYER_EVENTS: string[] = ['playing', 'pause', 'ratechange', 'timeupdate', 'ended', 'loadedmetadata', 'blocked'];
|
|
52
52
|
|
|
53
|
-
private _logger:
|
|
53
|
+
private _logger: LiveTelemetryLogger;
|
|
54
54
|
private _player: IMediaPlayer;
|
|
55
|
-
private _mediaSession:
|
|
56
|
-
private
|
|
55
|
+
private _mediaSession: LiveMediaSession;
|
|
56
|
+
private _volumeManager: VolumeManager;
|
|
57
57
|
private _onEnd?: () => void;
|
|
58
58
|
private _onPlayerEvent: EventListener;
|
|
59
59
|
private _seekSuspension?: MediaSessionCoordinatorSuspension;
|
|
@@ -67,12 +67,12 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
67
67
|
* @param mediaSession Group MediaSession object being used.
|
|
68
68
|
* @param onEnd Optional. Function to call when synchronizers `end()` method is called.
|
|
69
69
|
*/
|
|
70
|
-
constructor(player: IMediaPlayer, mediaSession:
|
|
70
|
+
constructor(player: IMediaPlayer, mediaSession: LiveMediaSession, onEnd: () => void) {
|
|
71
71
|
super();
|
|
72
72
|
this._player = player;
|
|
73
73
|
this._mediaSession = mediaSession;
|
|
74
74
|
this._logger = mediaSession.logger;
|
|
75
|
-
this.
|
|
75
|
+
this._volumeManager = new VolumeManager(player);
|
|
76
76
|
this._onEnd = onEnd;
|
|
77
77
|
|
|
78
78
|
// Listen for player state requests
|
|
@@ -124,15 +124,21 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
124
124
|
this.play();
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
// block play if player state is playing when expected synced state is paused
|
|
128
|
-
// needed
|
|
127
|
+
// block play if player state is playing when expected synced state is paused.
|
|
128
|
+
// needed because cannot tell if its a user initiated event, so disallow play
|
|
129
129
|
if (this._expectedPlaybackState === 'paused') {
|
|
130
130
|
this._player.pause();
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
// block play if player state is playing when expected synced state is none.
|
|
134
|
+
// needed because user who is not in control should not be able to start, so disallow play
|
|
135
|
+
if (this._expectedPlaybackState === 'none') {
|
|
136
|
+
this._player.pause();
|
|
137
|
+
}
|
|
132
138
|
break;
|
|
133
139
|
case 'pause':
|
|
134
140
|
// block pause if player state is paused when expected synced state is playing
|
|
135
|
-
// needed
|
|
141
|
+
// needed because cannot tell if its a user initiated event, so disallow pause
|
|
136
142
|
if (this._expectedPlaybackState === 'playing') {
|
|
137
143
|
this._player.play();
|
|
138
144
|
}
|
|
@@ -232,7 +238,7 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
232
238
|
/**
|
|
233
239
|
* Synchronizers media session.
|
|
234
240
|
*/
|
|
235
|
-
public get mediaSession():
|
|
241
|
+
public get mediaSession(): LiveMediaSession {
|
|
236
242
|
return this._mediaSession;
|
|
237
243
|
}
|
|
238
244
|
|
|
@@ -261,8 +267,8 @@ export class MediaPlayerSynchronizer extends EventEmitter {
|
|
|
261
267
|
/**
|
|
262
268
|
* Volume limiter used to temporarily reduce the videos volume when someone speaks in a meeting.
|
|
263
269
|
*/
|
|
264
|
-
public get
|
|
265
|
-
return this.
|
|
270
|
+
public get volumeManager(): VolumeManager {
|
|
271
|
+
return this._volumeManager;
|
|
266
272
|
}
|
|
267
273
|
|
|
268
274
|
/**
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the Microsoft Live Share SDK License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TimeInterval } from '@microsoft/live-share';
|
|
7
|
+
import { IMediaPlayer } from './IMediaPlayer';
|
|
8
|
+
|
|
9
|
+
export enum LimitLevelType { fixed, percentage }
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Smooth audio level changes when volume is modified, or if volume limiting has started/ended.
|
|
13
|
+
*/
|
|
14
|
+
export class VolumeManager {
|
|
15
|
+
private readonly _player: IMediaPlayer;
|
|
16
|
+
private readonly _volumeChangeDuration = new TimeInterval(500);
|
|
17
|
+
|
|
18
|
+
// defaults to player volume
|
|
19
|
+
private _volume = 0.0;
|
|
20
|
+
private _isLimiting = false;
|
|
21
|
+
private _limitLevel = 0.1
|
|
22
|
+
private _limitLevelType: LimitLevelType = LimitLevelType.fixed;
|
|
23
|
+
private _startTime = 0;
|
|
24
|
+
private _startVolume = 0;
|
|
25
|
+
private _running = false;
|
|
26
|
+
|
|
27
|
+
constructor(player: IMediaPlayer) {
|
|
28
|
+
this._player = player;
|
|
29
|
+
this._volume = this._player.volume;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The selected volume.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Expressed as a value between 0.0 and 1.0. The default value is 1.0.
|
|
37
|
+
* Can be used for things like volume sliders.
|
|
38
|
+
*
|
|
39
|
+
* Does not return the current volume if limiting is enabled, but the desired volume.
|
|
40
|
+
*/
|
|
41
|
+
public get volume(): number {
|
|
42
|
+
return this._volume;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public set volume(value: number) {
|
|
46
|
+
if (value < 0 || value > 1.0) {
|
|
47
|
+
throw new Error(`VolumeManager: cannot set volume to ${value}. Level must be between 0.0 and 1.0.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this._volume = value;
|
|
51
|
+
this.startAdjusting();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Target level to lower volume to.
|
|
56
|
+
*
|
|
57
|
+
* @remarks
|
|
58
|
+
* Expressed as a value between 0.0 and 1.0. The value is applied based upon the configured
|
|
59
|
+
* `levelType`. The default value is 0.1.
|
|
60
|
+
*
|
|
61
|
+
* For a level type of `LevelType.fixed` the value is the exact level the volume will be
|
|
62
|
+
* lowered to. The default value of 0.1 would cause the volume to be lowered to 0.1.
|
|
63
|
+
*
|
|
64
|
+
* For a level type of `LevelType.percentage` the value is the percentage by which the volume
|
|
65
|
+
* level should be lowered to. The default value of 0.1 would cause the volume to be lowered
|
|
66
|
+
* to 10% of its starting value.
|
|
67
|
+
*/
|
|
68
|
+
public get limitLevel(): number {
|
|
69
|
+
return this._limitLevel;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public set limitLevel(value: number) {
|
|
73
|
+
if (value < 0 || value > 1.0) {
|
|
74
|
+
throw new Error(`VolumeManager: cannot set level to ${value}. Level must be between 0.0 and 1.0.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this._limitLevel = value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The type of level represented by the `limitLevel` property.
|
|
82
|
+
*/
|
|
83
|
+
public get limitLevelType(): LimitLevelType {
|
|
84
|
+
return this._limitLevelType;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public set limitLevelType(value: LimitLevelType) {
|
|
88
|
+
this._limitLevelType = value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Amount of time, in seconds, it should take to change the volume up or down to the desired level.
|
|
93
|
+
*
|
|
94
|
+
* @remarks
|
|
95
|
+
* Default `volumeChangeDuration` is 0.5 seconds.
|
|
96
|
+
*/
|
|
97
|
+
public get volumeChangeDuration(): number {
|
|
98
|
+
return this._volumeChangeDuration.seconds;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public set volumeChangeDuration(value: number) {
|
|
102
|
+
this._volumeChangeDuration.seconds = Math.abs(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether or not volume limiting is currently enabled.
|
|
107
|
+
*/
|
|
108
|
+
public get isLimiting(): boolean {
|
|
109
|
+
return this._isLimiting;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Limits volume based on `limitLevel` and `limitLevelType` properties.
|
|
114
|
+
* @see `limitLevel`
|
|
115
|
+
* @see `limitLevelType`
|
|
116
|
+
*/
|
|
117
|
+
public startLimiting(): void {
|
|
118
|
+
this._isLimiting = true;
|
|
119
|
+
this.startAdjusting();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Disables volume limit.
|
|
124
|
+
*/
|
|
125
|
+
public stopLimiting(): void {
|
|
126
|
+
this._isLimiting = false;
|
|
127
|
+
this.startAdjusting();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private startAdjusting() {
|
|
131
|
+
const adjustVolume = () => {
|
|
132
|
+
// Schedule next animation frame if volume change not finished
|
|
133
|
+
if (this.millisSinceVolumeChangeStart() <= this._volumeChangeDuration.milliseconds) {
|
|
134
|
+
this._player.volume = this.computeInterpolatedVolume();
|
|
135
|
+
this.scheduleAnimationFrame(adjustVolume);
|
|
136
|
+
} else {
|
|
137
|
+
this._player.volume = this.computeTargetVolume();
|
|
138
|
+
this._running = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this._startTime = new Date().getTime();
|
|
143
|
+
this._startVolume = this._player.volume;
|
|
144
|
+
if (!this._running) {
|
|
145
|
+
this._running = true
|
|
146
|
+
this.scheduleAnimationFrame(adjustVolume);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private scheduleAnimationFrame(callback: FrameRequestCallback): void {
|
|
151
|
+
if (typeof requestAnimationFrame == "function") {
|
|
152
|
+
requestAnimationFrame(callback);
|
|
153
|
+
} else {
|
|
154
|
+
setTimeout(callback, 20);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private computeInterpolatedVolume(): number {
|
|
159
|
+
const volumeChangeMillis = this._volumeChangeDuration.milliseconds
|
|
160
|
+
const volumeDifference = this.computeTargetVolume() - this._startVolume
|
|
161
|
+
const adjustmentFromStart = volumeDifference / volumeChangeMillis * this.millisSinceVolumeChangeStart()
|
|
162
|
+
return this._startVolume + adjustmentFromStart;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private computeTargetVolume(): number {
|
|
166
|
+
if (this._limitLevelType == LimitLevelType.percentage) {
|
|
167
|
+
if (this._isLimiting) {
|
|
168
|
+
return this._volume * this._limitLevel;
|
|
169
|
+
} else {
|
|
170
|
+
return this._volume;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
if (this._isLimiting && this._volume > this._limitLevel) {
|
|
174
|
+
return this._limitLevel;
|
|
175
|
+
} else {
|
|
176
|
+
return this._volume;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private millisSinceVolumeChangeStart(): number {
|
|
182
|
+
const now = new Date().getTime();
|
|
183
|
+
return now - this._startTime;
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
export * from './
|
|
8
|
-
export * from './
|
|
9
|
-
export * from './
|
|
7
|
+
export * from './LiveMediaSession';
|
|
8
|
+
export * from './LiveMediaSessionCoordinator';
|
|
9
|
+
export * from './LiveMediaSessionCoordinatorSuspension';
|
|
10
10
|
export * from './IMediaPlayer';
|
|
11
11
|
export * from './MediaPlayerSynchronizer';
|
|
12
12
|
export * from './MediaSessionActionThrottler';
|
|
13
13
|
export * from './MediaSessionExtensions';
|
|
14
14
|
export * from './RepeatedActionThrottler';
|
|
15
|
-
export * from './
|
|
15
|
+
export * from './VolumeManager';
|