@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.
Files changed (54) hide show
  1. package/.vscode/launch.json +29 -0
  2. package/.vscode/settings.json +2 -0
  3. package/README.md +6 -5
  4. package/bin/LiveMediaSession.d.ts +95 -0
  5. package/bin/LiveMediaSession.d.ts.map +1 -0
  6. package/bin/LiveMediaSession.js +217 -0
  7. package/bin/LiveMediaSession.js.map +1 -0
  8. package/bin/LiveMediaSessionCoordinator.d.ts +228 -0
  9. package/bin/LiveMediaSessionCoordinator.d.ts.map +1 -0
  10. package/bin/LiveMediaSessionCoordinator.js +396 -0
  11. package/bin/LiveMediaSessionCoordinator.js.map +1 -0
  12. package/bin/LiveMediaSessionCoordinatorSuspension.d.ts +20 -0
  13. package/bin/LiveMediaSessionCoordinatorSuspension.d.ts.map +1 -0
  14. package/bin/LiveMediaSessionCoordinatorSuspension.js +28 -0
  15. package/bin/LiveMediaSessionCoordinatorSuspension.js.map +1 -0
  16. package/bin/MediaPlayerSynchronizer.d.ts +6 -6
  17. package/bin/MediaPlayerSynchronizer.d.ts.map +1 -1
  18. package/bin/MediaPlayerSynchronizer.js +12 -7
  19. package/bin/MediaPlayerSynchronizer.js.map +1 -1
  20. package/bin/VolumeManager.d.ts +84 -0
  21. package/bin/VolumeManager.d.ts.map +1 -0
  22. package/bin/VolumeManager.js +173 -0
  23. package/bin/VolumeManager.js.map +1 -0
  24. package/bin/index.d.ts +4 -4
  25. package/bin/index.d.ts.map +1 -1
  26. package/bin/index.js +4 -4
  27. package/bin/index.js.map +1 -1
  28. package/bin/internals/GroupCoordinatorState.d.ts +6 -6
  29. package/bin/internals/GroupCoordinatorState.d.ts.map +1 -1
  30. package/bin/internals/GroupCoordinatorState.js +6 -6
  31. package/bin/internals/GroupCoordinatorState.js.map +1 -1
  32. package/bin/internals/GroupPlaybackPosition.d.ts.map +1 -1
  33. package/bin/internals/GroupPlaybackPosition.js +2 -2
  34. package/bin/internals/GroupPlaybackPosition.js.map +1 -1
  35. package/bin/internals/GroupPlaybackTrack.d.ts +1 -1
  36. package/bin/internals/GroupPlaybackTrack.d.ts.map +1 -1
  37. package/bin/internals/GroupTransportState.d.ts +1 -1
  38. package/bin/internals/GroupTransportState.d.ts.map +1 -1
  39. package/bin/internals/GroupTransportState.js +1 -1
  40. package/bin/internals/GroupTransportState.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/{EphemeralMediaSession.ts → LiveMediaSession.ts} +13 -23
  43. package/src/{EphemeralMediaSessionCoordinator.ts → LiveMediaSessionCoordinator.ts} +41 -41
  44. package/src/{EphemeralMediaSessionCoordinatorSuspension.ts → LiveMediaSessionCoordinatorSuspension.ts} +1 -1
  45. package/src/MediaPlayerSynchronizer.ts +20 -14
  46. package/src/VolumeManager.ts +185 -0
  47. package/src/index.ts +4 -4
  48. package/src/internals/GroupCoordinatorState.ts +13 -13
  49. package/src/internals/GroupPlaybackPosition.ts +3 -3
  50. package/src/internals/GroupPlaybackTrack.ts +1 -1
  51. package/src/internals/GroupTransportState.ts +3 -3
  52. package/src/test/SharedMediaSession.spec.ts +2 -2
  53. package/src/test/VolumeManager.spec.ts +212 -0
  54. package/src/VolumeLimiter.ts +0 -182
@@ -4,23 +4,23 @@
4
4
  */
5
5
 
6
6
  import { DataObject, DataObjectFactory } from '@fluidframework/aqueduct';
7
- import { EphemeralTelemetryLogger, UserMeetingRole } from '@microsoft/live-share';
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 { EphemeralMediaSessionCoordinator, IMediaPlayerState } from './EphemeralMediaSessionCoordinator';
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
- * Ephemeral fluid object that synchronizes media playback across multiple clients.
17
+ * Live fluid object that synchronizes media playback across multiple clients.
18
18
  */
19
- export class EphemeralMediaSession extends DataObject {
19
+ export class LiveMediaSession extends DataObject {
20
20
  private _actionThrottler: MediaSessionActionThrottler = new RepeatedActionThrottler();
21
- private _logger?: EphemeralTelemetryLogger;
21
+ private _logger?: LiveTelemetryLogger;
22
22
  private _requestPlayerStateHandler?: () => IMediaPlayerState;
23
- private _coordinator?: EphemeralMediaSessionCoordinator;
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
- EphemeralMediaSession.TypeName,
41
- EphemeralMediaSession,
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 `EphemeralMediaCoordinator` is fairly aggressive at wanting to keep the local media player
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(): EphemeralMediaSessionCoordinator {
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(): EphemeralTelemetryLogger {
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 EphemeralTelemetryLogger(this.runtime);
169
+ this. _logger = new LiveTelemetryLogger(this.runtime);
180
170
 
181
171
  // Create coordinator and listen for triggered actions
182
- this._coordinator = new EphemeralMediaSessionCoordinator(this.runtime, () => this.getCurrentPlayerState());
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 { EphemeralEventScope, EphemeralTelemetryLogger, EphemeralEventTarget, IEphemeralEvent, IRuntimeSignaler, TimeInterval, UserMeetingRole } from '@microsoft/live-share';
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 { EphemeralMediaSessionCoordinatorSuspension } from './EphemeralMediaSessionCoordinatorSuspension';
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 `EphemeralMediaSessionCoordinator` tracks the playback & position state of all other
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 EphemeralMediaSessionCoordinator extends EventEmitter {
45
+ export class LiveMediaSessionCoordinator extends EventEmitter {
46
46
  private readonly _runtime: IRuntimeSignaler;
47
- private readonly _logger: EphemeralTelemetryLogger;
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?: EphemeralEventTarget<ITransportCommandEvent>;
56
- private _pauseEvent?: EphemeralEventTarget<ITransportCommandEvent>;
57
- private _seekToEvent?: EphemeralEventTarget<ITransportCommandEvent>;
58
- private _setTrackEvent?: EphemeralEventTarget<ISetTrackEvent>;
59
- private _setTrackDataEvent?: EphemeralEventTarget<ISetTrackDataEvent>;
60
- private _positionUpdateEvent?: EphemeralEventTarget<IPositionUpdateEvent>;
61
- private _joinedEvent?: EphemeralEventTarget<IEphemeralEvent>;
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 EphemeralTelemetryLogger(runtime);
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(`EphemeralMediaSessionCoordinator.play() called before initialize() called.`);
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(`EphemeralMediaSessionCoordinator.play() called before MediaSession.metadata assigned.`);
184
+ throw new Error(`LiveMediaSessionCoordinator.play() called before MediaSession.metadata assigned.`);
185
185
  }
186
186
 
187
187
  if (!this.canPlayPause) {
188
- throw new Error(`EphemeralMediaSessionCoordinator.play() operation blocked.`);
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(`EphemeralMediaSessionCoordinator.pause() called before initialize() called.`);
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(`EphemeralMediaSessionCoordinator.pause() called before MediaSession.metadata assigned.`);
215
+ throw new Error(`LiveMediaSessionCoordinator.pause() called before MediaSession.metadata assigned.`);
216
216
  }
217
217
 
218
218
  if (!this.canPlayPause) {
219
- throw new Error(`EphemeralMediaSessionCoordinator.pause() operation blocked.`);
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(`EphemeralMediaSessionCoordinator.seekTo() called before initialize() called.`);
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(`EphemeralMediaSessionCoordinator.seekTo() called before MediaSession.metadata assigned.`);
247
+ throw new Error(`LiveMediaSessionCoordinator.seekTo() called before MediaSession.metadata assigned.`);
248
248
  }
249
249
 
250
250
  if (!this.canSeek) {
251
- throw new Error(`EphemeralMediaSessionCoordinator.seekTo() operation blocked.`);
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(`EphemeralMediaSessionCoordinator.setTrack() called before initialize() called.`);
273
+ throw new Error(`LiveMediaSessionCoordinator.setTrack() called before initialize() called.`);
274
274
  }
275
275
 
276
276
  if (!this.canSetTrack) {
277
- throw new Error(`EphemeralMediaSessionCoordinator.setTrack() operation blocked.`);
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(`EphemeralMediaSessionCoordinator.setTrackData() called before initialize() called.`);
301
+ throw new Error(`LiveMediaSessionCoordinator.setTrackData() called before initialize() called.`);
302
302
  }
303
303
 
304
304
  if (!this.canSetTrackData) {
305
- throw new Error(`EphemeralMediaSessionCoordinator.setTrackData() operation blocked.`);
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(`EphemeralMediaSessionCoordinator.beginSuspension() called before initialize() called.`);
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(`EphemeralMediaSessionCoordinator.beginSuspension() called before MediaSession.metadata assigned.`);
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 EphemeralMediaSessionCoordinatorSuspension(waitPoint, time => {
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(`EphemeralMediaSessionCoordinator.initialize() already initialized.`);
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(`EphemeralMediaSessionCoordinator.sendPositionUpdate() called before initialize() called.`);
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 EphemeralEventScope(this._runtime, acceptTransportChangesFrom);
420
- const unrestrictedScope = new EphemeralEventScope(this._runtime);
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 EphemeralEventTarget<ISetTrackEvent>(scope, 'setTrack', (event, local) => this._groupState!.handleSetTrack(event, local));
428
- this._setTrackDataEvent = new EphemeralEventTarget<ISetTrackDataEvent>(scope, 'setTrackData', (event, local) => this._groupState!.handleSetTrackData(event, local));
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 EphemeralEventTarget(scope, 'play', (event, local) => this._groupState!.handleTransportCommand(event, local));
432
- this._pauseEvent = new EphemeralEventTarget(scope, 'pause', (event, local) => this._groupState!.handleTransportCommand(event, local));
433
- this._seekToEvent = new EphemeralEventTarget(scope, 'seekTo', (event, local) => this._groupState!.handleTransportCommand(event, local));
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 EphemeralEventTarget(unrestrictedScope, 'positionUpdate', (event, local) => this._groupState!.handlePositionUpdate(event, local));
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 EphemeralEventTarget(unrestrictedScope, 'joined', (evt, local) => {
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: EphemeralTelemetryLogger.formatCorrelationId(evt.clientId, evt.timestamp)});
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 EphemeralMediaSessionCoordinatorSuspension implements MediaSessionCoordinatorSuspension {
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 { EphemeralTelemetryLogger, IEvent, TimeInterval } from '@microsoft/live-share';
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 { EphemeralMediaSession } from './EphemeralMediaSession';
10
- import { VolumeLimiter } from './VolumeLimiter';
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: EphemeralTelemetryLogger;
53
+ private _logger: LiveTelemetryLogger;
54
54
  private _player: IMediaPlayer;
55
- private _mediaSession: EphemeralMediaSession;
56
- private _volumeLimiter: VolumeLimiter;
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: EphemeralMediaSession, onEnd: () => void) {
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._volumeLimiter = new VolumeLimiter(player);
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 for YouTube because cannot tell if its a user initiated event, so disallow play (except when starting)
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 for YouTube because cannot tell if its a user initiated event, so disallow pause
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(): EphemeralMediaSession {
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 volumeLimiter(): VolumeLimiter {
265
- return this._volumeLimiter;
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 './EphemeralMediaSession';
8
- export * from './EphemeralMediaSessionCoordinator';
9
- export * from './EphemeralMediaSessionCoordinatorSuspension';
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 './VolumeLimiter';
15
+ export * from './VolumeManager';