@microsoft/live-share-media 1.3.1 → 1.4.0

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 (65) hide show
  1. package/bin/LiveMediaSession.d.ts.map +1 -1
  2. package/bin/LiveMediaSession.js +8 -0
  3. package/bin/LiveMediaSession.js.map +1 -1
  4. package/bin/LiveMediaSessionCoordinator.d.ts +15 -0
  5. package/bin/LiveMediaSessionCoordinator.d.ts.map +1 -1
  6. package/bin/LiveMediaSessionCoordinator.js +25 -0
  7. package/bin/LiveMediaSessionCoordinator.js.map +1 -1
  8. package/bin/MediaPlayerSynchronizer.d.ts +6 -4
  9. package/bin/MediaPlayerSynchronizer.d.ts.map +1 -1
  10. package/bin/MediaPlayerSynchronizer.js +41 -12
  11. package/bin/MediaPlayerSynchronizer.js.map +1 -1
  12. package/bin/MediaSessionExtensions.d.ts +17 -4
  13. package/bin/MediaSessionExtensions.d.ts.map +1 -1
  14. package/bin/MediaSessionExtensions.js +4 -0
  15. package/bin/MediaSessionExtensions.js.map +1 -1
  16. package/bin/internals/GroupCoordinatorState.d.ts +15 -1
  17. package/bin/internals/GroupCoordinatorState.d.ts.map +1 -1
  18. package/bin/internals/GroupCoordinatorState.js +123 -57
  19. package/bin/internals/GroupCoordinatorState.js.map +1 -1
  20. package/bin/internals/GroupPlaybackPosition.d.ts +3 -1
  21. package/bin/internals/GroupPlaybackPosition.d.ts.map +1 -1
  22. package/bin/internals/GroupPlaybackPosition.js +7 -3
  23. package/bin/internals/GroupPlaybackPosition.js.map +1 -1
  24. package/bin/internals/GroupPlaybackRate.d.ts +39 -0
  25. package/bin/internals/GroupPlaybackRate.d.ts.map +1 -0
  26. package/bin/internals/GroupPlaybackRate.js +62 -0
  27. package/bin/internals/GroupPlaybackRate.js.map +1 -0
  28. package/bin/internals/GroupPlaybackTrack.d.ts +2 -2
  29. package/bin/internals/GroupPlaybackTrack.d.ts.map +1 -1
  30. package/bin/internals/GroupPlaybackTrack.js +3 -2
  31. package/bin/internals/GroupPlaybackTrack.js.map +1 -1
  32. package/bin/internals/GroupPlaybackTrackData.d.ts +3 -3
  33. package/bin/internals/GroupPlaybackTrackData.d.ts.map +1 -1
  34. package/bin/internals/GroupPlaybackTrackData.js +12 -11
  35. package/bin/internals/GroupPlaybackTrackData.js.map +1 -1
  36. package/bin/internals/GroupTransportState.d.ts +6 -3
  37. package/bin/internals/GroupTransportState.d.ts.map +1 -1
  38. package/bin/internals/GroupTransportState.js +16 -11
  39. package/bin/internals/GroupTransportState.js.map +1 -1
  40. package/bin/internals/consts.d.ts +8 -1
  41. package/bin/internals/consts.d.ts.map +1 -1
  42. package/bin/internals/consts.js +8 -1
  43. package/bin/internals/consts.js.map +1 -1
  44. package/bin/internals/index.d.ts +1 -0
  45. package/bin/internals/index.d.ts.map +1 -1
  46. package/bin/internals/index.js +1 -0
  47. package/bin/internals/index.js.map +1 -1
  48. package/bin/internals/interfaces.d.ts +10 -0
  49. package/bin/internals/interfaces.d.ts.map +1 -0
  50. package/bin/internals/interfaces.js +3 -0
  51. package/bin/internals/interfaces.js.map +1 -0
  52. package/package.json +3 -3
  53. package/src/LiveMediaSession.ts +12 -0
  54. package/src/LiveMediaSessionCoordinator.ts +49 -0
  55. package/src/MediaPlayerSynchronizer.ts +55 -15
  56. package/src/MediaSessionExtensions.ts +18 -2
  57. package/src/internals/GroupCoordinatorState.ts +196 -79
  58. package/src/internals/GroupPlaybackPosition.ts +9 -2
  59. package/src/internals/GroupPlaybackRate.ts +90 -0
  60. package/src/internals/GroupPlaybackTrack.ts +5 -4
  61. package/src/internals/GroupPlaybackTrackData.ts +14 -13
  62. package/src/internals/GroupTransportState.ts +21 -12
  63. package/src/internals/consts.ts +14 -2
  64. package/src/internals/index.ts +1 -0
  65. package/src/internals/interfaces.ts +10 -0
@@ -20,7 +20,7 @@ import {
20
20
  import { VolumeManager } from "./VolumeManager";
21
21
  import { LiveMediaSession } from "./LiveMediaSession";
22
22
  import { IMediaPlayer } from "./IMediaPlayer";
23
- import { TelemetryEvents } from "./internals";
23
+ import { ITriggerActionEvent, TelemetryEvents } from "./internals";
24
24
  import { waitUntilConnected } from "@microsoft/live-share/bin/internals";
25
25
 
26
26
  /**
@@ -64,6 +64,7 @@ export class MediaPlayerSynchronizer extends EventEmitter {
64
64
  "pause",
65
65
  "seekto",
66
66
  "settrack",
67
+ "ratechange",
67
68
  "datachange",
68
69
  "catchup",
69
70
  "wait",
@@ -89,6 +90,7 @@ export class MediaPlayerSynchronizer extends EventEmitter {
89
90
  private _viewOnly = false;
90
91
  private _blockUnexpectedPlayerEvents = true;
91
92
  private _expectedPlaybackState: ExtendedMediaSessionPlaybackState = "none";
93
+ private _expectedPlaybackRate: number = 1.0;
92
94
  private _metadata: ExtendedMediaMetadata | null = null;
93
95
  private _trackData: object | null = null;
94
96
 
@@ -142,7 +144,7 @@ export class MediaPlayerSynchronizer extends EventEmitter {
142
144
  playbackState: playbackState,
143
145
  positionState: {
144
146
  position: this._player.currentTime,
145
- playbackRate: this._player.playbackRate,
147
+ playbackRate: this._player.playbackRate ?? 1,
146
148
  duration: this._player.duration,
147
149
  },
148
150
  trackData: this._trackData,
@@ -227,22 +229,19 @@ export class MediaPlayerSynchronizer extends EventEmitter {
227
229
  }
228
230
  break;
229
231
  case "ratechange":
230
- // Block rate changes unless suspended.
231
232
  if (
232
- this._player.playbackRate != 1.0 &&
233
+ this._blockUnexpectedPlayerEvents &&
234
+ this._expectedPlaybackRate !==
235
+ this._player.playbackRate &&
233
236
  !this._mediaSession.coordinator.isSuspended
234
237
  ) {
235
- this._logger.sendTelemetryEvent(
236
- TelemetryEvents.MediaPlayerSynchronizer
237
- .PlaybackRateChangeBlocked
238
- );
239
- this._player.playbackRate = 1.0;
238
+ this._player.playbackRate = this._expectedPlaybackRate;
240
239
  }
241
240
  break;
242
241
  case "blocked":
243
242
  this._logger.sendTelemetryEvent(
244
243
  TelemetryEvents.MediaPlayerSynchronizer
245
- .PlaybackRateChangeBlocked
244
+ .PlayerBlockedOperation
246
245
  );
247
246
  break;
248
247
  }
@@ -254,6 +253,13 @@ export class MediaPlayerSynchronizer extends EventEmitter {
254
253
  (evt) => this.emit(evt.type, evt)
255
254
  );
256
255
 
256
+ this._mediaSession.coordinator.on(
257
+ MediaSessionCoordinatorEvents.triggeractionignored,
258
+ (evt: ITriggerActionEvent) => {
259
+ this.dispatchGroupAction(evt.details, undefined);
260
+ }
261
+ );
262
+
257
263
  // Register media session actions
258
264
  for (const action of MediaPlayerSynchronizer.SESSION_ACTIONS) {
259
265
  this._mediaSession.setActionHandler(
@@ -344,6 +350,22 @@ export class MediaPlayerSynchronizer extends EventEmitter {
344
350
  .DataChangeAction
345
351
  );
346
352
  break;
353
+ case "ratechange":
354
+ if (!details.playbackRate) {
355
+ break;
356
+ }
357
+
358
+ this._logger.sendTelemetryEvent(
359
+ TelemetryEvents.MediaPlayerSynchronizer
360
+ .RateChangeAction,
361
+ null,
362
+ { playbackRate: details.playbackRate }
363
+ );
364
+ this._expectedPlaybackRate =
365
+ details.playbackRate;
366
+ this._player.playbackRate =
367
+ details.playbackRate;
368
+ break;
347
369
  case "catchup":
348
370
  if (typeof details.seekTime == "number") {
349
371
  this._logger.sendTelemetryEvent(
@@ -401,10 +423,10 @@ export class MediaPlayerSynchronizer extends EventEmitter {
401
423
  *
402
424
  * @remarks
403
425
  * Toggling this value to true results in `mediaSession.coordinator.canPlayPause`,
404
- * `mediaSession.coordinator.canSeek`, `mediaSession.coordinator.canSetTrack`, and
405
- * `mediaSession.coordinator.canSetTrackData` all being set to false. For more fine
406
- * grained control over the local clients policies, call the `mediaSession.coordinator`
407
- * directly.
426
+ * `mediaSession.coordinator.canSeek`, `mediaSession.coordinator.canSetTrack`,
427
+ * `mediaSession.coordinator.canSetTrackData`, and `mediaSession.coordinator.canSetPlaybackRate`
428
+ * all being set to false. For more fine grained control over the local clients policies,
429
+ * call the `mediaSession.coordinator` directly.
408
430
  */
409
431
  public get viewOnly(): boolean {
410
432
  return this._viewOnly;
@@ -416,6 +438,7 @@ export class MediaPlayerSynchronizer extends EventEmitter {
416
438
  this.mediaSession.coordinator.canSeek = !value;
417
439
  this.mediaSession.coordinator.canSetTrack = !value;
418
440
  this.mediaSession.coordinator.canSetTrackData = !value;
441
+ this.mediaSession.coordinator.canSetPlaybackRate = !value;
419
442
  }
420
443
 
421
444
  /**
@@ -635,6 +658,24 @@ export class MediaPlayerSynchronizer extends EventEmitter {
635
658
  });
636
659
  }
637
660
 
661
+ public async setPlaybackRate(playbackRate: number): Promise<void> {
662
+ this._logger.sendTelemetryEvent(
663
+ TelemetryEvents.MediaPlayerSynchronizer.RateChangeCalled,
664
+ null,
665
+ { playbackRate }
666
+ );
667
+
668
+ await this._mediaSession.coordinator.setPlaybackRate(playbackRate);
669
+
670
+ this.dispatchUserAction({
671
+ action: "ratechange",
672
+ source: "user",
673
+ clientId: await waitUntilConnected(this._runtime),
674
+ local: true,
675
+ playbackRate,
676
+ });
677
+ }
678
+
638
679
  /**
639
680
  * Tells the group to change to a new track.
640
681
  *
@@ -687,7 +728,6 @@ export class MediaPlayerSynchronizer extends EventEmitter {
687
728
  });
688
729
  }
689
730
 
690
- // TODO: what is delay useful for? we do not use, delete?
691
731
  private dispatchGroupAction(
692
732
  details: ExtendedMediaSessionActionDetails,
693
733
  error: Error | undefined
@@ -59,6 +59,10 @@ export enum MediaSessionCoordinatorEvents {
59
59
  * Event emitted when an action was triggered.
60
60
  */
61
61
  triggeraction = "triggeraction",
62
+ /**
63
+ * Event emitted when an action would have been triggered, but a suspension is active for the local client.
64
+ */
65
+ triggeractionignored = "triggeractionignored",
62
66
  }
63
67
 
64
68
  /**
@@ -71,6 +75,7 @@ export enum MediaSessionCoordinatorEvents {
71
75
  export type ExtendedMediaSessionAction =
72
76
  | MediaSessionAction
73
77
  | "settrack"
78
+ | "ratechange"
74
79
  | "wait"
75
80
  | "catchup"
76
81
  | "datachange"
@@ -116,6 +121,15 @@ export interface ExtendedMediaMetadata extends MediaMetadata {
116
121
  liveStream?: boolean;
117
122
  }
118
123
 
124
+ /**
125
+ * The reasons why an action is ignored.
126
+ *
127
+ * @remarks
128
+ * Is `localusersuspended` when the action updated the group state, but ignored locally due to the local client being suspended.
129
+
130
+ */
131
+ export type ExtendedMediaSessionActionIgnore = "localusersuspended";
132
+
119
133
  /**
120
134
  * Details for emitted actions from `LiveMediaSession`.
121
135
  *
@@ -169,9 +183,11 @@ export interface ExtendedMediaSessionActionDetails {
169
183
  */
170
184
  data?: object | null;
171
185
  /**
172
- * Action type that was blocked.
186
+ * Reason an action is ignored by the local client, undefined if not ignored.
173
187
  */
174
- blocked?: ExtendedMediaSessionAction | null;
188
+ ignoreReason?: ExtendedMediaSessionActionIgnore | null;
189
+
190
+ playbackRate?: number | null;
175
191
  }
176
192
 
177
193
  /**
@@ -23,8 +23,13 @@ import {
23
23
  GroupPlaybackTrack,
24
24
  GroupPlaybackTrackEvents,
25
25
  IPlaybackTrack,
26
+ IPlaybackTrackChangeEvent,
26
27
  } from "./GroupPlaybackTrack";
27
- import { GroupTransportState, ITransportState } from "./GroupTransportState";
28
+ import {
29
+ GroupTransportState,
30
+ ITransportState,
31
+ ITransportStateChangeEvent,
32
+ } from "./GroupTransportState";
28
33
  import {
29
34
  GroupPlaybackPosition,
30
35
  ICurrentPlaybackPosition,
@@ -35,9 +40,16 @@ import {
35
40
  GroupPlaybackTrackData,
36
41
  PlaybackTrackDataEvents,
37
42
  IPlaybackTrackData,
43
+ IPlaybackTrackDataChangeEvent,
38
44
  } from "./GroupPlaybackTrackData";
39
45
  import { TelemetryEvents } from "./consts";
40
46
  import { waitUntilConnected } from "@microsoft/live-share/bin/internals";
47
+ import {
48
+ GroupPlaybackRate,
49
+ IPlaybackRate,
50
+ IPlaybackRateChangeEvent,
51
+ PlaybackRateEvents,
52
+ } from "./GroupPlaybackRate";
41
53
 
42
54
  /**
43
55
  * @hidden
@@ -46,6 +58,7 @@ export interface IPositionUpdateEvent {
46
58
  track: IPlaybackTrack;
47
59
  trackData: IPlaybackTrackData;
48
60
  transport: ITransportState;
61
+ playbackRate: IPlaybackRate;
49
62
  playbackState: ExtendedMediaSessionPlaybackState;
50
63
  position: number;
51
64
  waitPoint?: CoordinationWaitPoint;
@@ -59,6 +72,13 @@ export interface ITransportCommandEvent {
59
72
  position: number;
60
73
  }
61
74
 
75
+ /**
76
+ * @hidden
77
+ */
78
+ export interface IRateChangeCommandEvent {
79
+ playbackRate: number;
80
+ }
81
+
62
82
  /**
63
83
  * @hidden
64
84
  */
@@ -87,6 +107,7 @@ export interface ITriggerActionEvent extends IEvent {
87
107
  export enum GroupCoordinatorStateEvents {
88
108
  newwaitpoint = "newwaitpoint",
89
109
  triggeraction = "triggeraction",
110
+ triggeractionignored = "triggeractionignored",
90
111
  }
91
112
 
92
113
  /**
@@ -104,6 +125,7 @@ export class GroupCoordinatorState extends EventEmitter {
104
125
  private _playbackTrackData: GroupPlaybackTrackData;
105
126
  private _transportState: GroupTransportState;
106
127
  private _playbackPosition: GroupPlaybackPosition;
128
+ private _playbackRate: GroupPlaybackRate;
107
129
 
108
130
  // Suspension tracking
109
131
  private _suspensionCnt: number = 0;
@@ -132,13 +154,16 @@ export class GroupCoordinatorState extends EventEmitter {
132
154
  this._playbackTrackData = new GroupPlaybackTrackData(
133
155
  this._playbackTrack
134
156
  );
157
+ this._playbackRate = new GroupPlaybackRate();
135
158
  this._transportState = new GroupTransportState(
136
159
  this._playbackTrack,
160
+ this._playbackRate,
137
161
  getMediaPlayerState,
138
162
  this._liveRuntime
139
163
  );
140
164
  this._playbackPosition = new GroupPlaybackPosition(
141
165
  this._transportState,
166
+ this._playbackRate,
142
167
  this._runtime,
143
168
  this._liveRuntime,
144
169
  positionUpdateInterval
@@ -146,104 +171,122 @@ export class GroupCoordinatorState extends EventEmitter {
146
171
  this._getMediaPlayerState = getMediaPlayerState;
147
172
 
148
173
  // Listen track related events
149
- this._playbackTrack.on(GroupPlaybackTrackEvents.trackChange, (evt) => {
150
- if (!this.isSuspended) {
151
- this._logger.sendTelemetryEvent(
152
- TelemetryEvents.GroupCoordinator.TrackChanged
153
- );
154
- this.emitSetTrack(evt.clientId, evt.metadata!, evt.source);
155
- } else {
156
- this._logger.sendTelemetryEvent(
157
- TelemetryEvents.GroupCoordinator.TrackChangeDelayed
158
- );
174
+ this._playbackTrack.on(
175
+ GroupPlaybackTrackEvents.trackChange,
176
+ (evt: IPlaybackTrackChangeEvent) => {
177
+ if (!this.isSuspended) {
178
+ this._logger.sendTelemetryEvent(
179
+ TelemetryEvents.GroupCoordinator.TrackChanged
180
+ );
181
+ this.emitSetTrack(evt.clientId, evt.metadata!, evt.source);
182
+ } else {
183
+ this._logger.sendTelemetryEvent(
184
+ TelemetryEvents.GroupCoordinator.TrackChangeDelayed
185
+ );
186
+ }
159
187
  }
160
- });
188
+ );
161
189
 
162
190
  // Listen for track data changes
163
191
  this._playbackTrackData.on(
164
192
  PlaybackTrackDataEvents.dataChange,
165
- async (evt) => {
193
+ async (evt: IPlaybackTrackDataChangeEvent) => {
166
194
  if (!this.isSuspended && !this.isWaiting) {
167
195
  this._logger.sendTelemetryEvent(
168
196
  TelemetryEvents.GroupCoordinator.TrackDataChanged
169
197
  );
170
- const clientId = await waitUntilConnected(this._runtime);
171
- this.emitTriggerAction({
172
- action: "datachange",
173
- source: evt.source,
174
- data: evt.data,
175
- clientId: evt.clientId,
176
- local: evt.clientId === clientId,
177
- });
178
198
  } else {
179
199
  this._logger.sendTelemetryEvent(
180
200
  TelemetryEvents.GroupCoordinator.TrackDataChangeDelayed
181
201
  );
182
202
  }
203
+ const clientId = await waitUntilConnected(this._runtime);
204
+ this.emitTriggerActionOrIgnored({
205
+ action: "datachange",
206
+ source: evt.source,
207
+ data: evt.data,
208
+ clientId: evt.clientId,
209
+ local: evt.clientId === clientId,
210
+ });
183
211
  }
184
212
  );
185
213
 
186
- // Listen to transport related events
187
- this._transportState.on(
188
- GroupTransportStateEvents.transportStateChange,
189
- async (evt) => {
214
+ this._playbackRate.on(
215
+ PlaybackRateEvents.rateChange,
216
+ async (evt: IPlaybackRateChangeEvent) => {
190
217
  if (!this.isSuspended && !this.isWaiting) {
191
218
  this._logger.sendTelemetryEvent(
192
- TelemetryEvents.GroupCoordinator.TransportStateChanged,
193
- null,
194
- {
195
- action: evt.action,
196
- seekTime: evt.seekTime,
197
- }
219
+ TelemetryEvents.GroupCoordinator.PlaybackRateChanged
198
220
  );
199
-
200
- const localClientId = await waitUntilConnected(
201
- this._runtime
202
- );
203
- const local = evt.clientId === localClientId;
204
- // Trigger action
205
- switch (evt.action) {
206
- case "play":
207
- this.emitTriggerAction({
208
- action: "play",
209
- source: evt.source,
210
- clientId: evt.clientId,
211
- local,
212
- seekTime: evt.seekTime,
213
- });
214
- break;
215
-
216
- case "pause":
217
- this.emitTriggerAction({
218
- action: "pause",
219
- source: evt.source,
220
- clientId: evt.clientId,
221
- local,
222
- seekTime: evt.seekTime,
223
- });
224
- break;
225
-
226
- case "seekto":
227
- this.emitTriggerAction({
228
- action: "seekto",
229
- source: evt.source,
230
- clientId: evt.clientId,
231
- local,
232
- seekTime: evt.seekTime,
233
- });
234
- break;
235
- }
236
221
  } else {
237
222
  this._logger.sendTelemetryEvent(
238
223
  TelemetryEvents.GroupCoordinator
239
- .TransportStateChangeDelayed,
240
- null,
241
- {
242
- action: evt.action,
243
- seekTime: evt.seekTime,
244
- }
224
+ .PlaybackRateChangedDelayed
245
225
  );
246
226
  }
227
+
228
+ const localClientId = await waitUntilConnected(this._runtime);
229
+ const local = evt.clientId === localClientId;
230
+ this.emitTriggerActionOrIgnored({
231
+ action: "ratechange",
232
+ source: evt.source,
233
+ clientId: evt.clientId,
234
+ local,
235
+ playbackRate: evt.playbackRate,
236
+ });
237
+ }
238
+ );
239
+
240
+ // Listen to transport related events
241
+ this._transportState.on(
242
+ GroupTransportStateEvents.transportStateChange,
243
+ async (evt: ITransportStateChangeEvent) => {
244
+ const tag =
245
+ this.isSuspended || this.isWaiting
246
+ ? TelemetryEvents.GroupCoordinator
247
+ .TransportStateChangeDelayed
248
+ : TelemetryEvents.GroupCoordinator
249
+ .TransportStateChanged;
250
+
251
+ this._logger.sendTelemetryEvent(tag, null, {
252
+ action: evt.action,
253
+ seekTime: evt.seekTime,
254
+ });
255
+
256
+ const localClientId = await waitUntilConnected(this._runtime);
257
+ const local = evt.clientId === localClientId;
258
+ // Trigger action
259
+ switch (evt.action) {
260
+ case "play":
261
+ this.emitTriggerActionOrIgnored({
262
+ action: "play",
263
+ source: evt.source,
264
+ clientId: evt.clientId,
265
+ local,
266
+ seekTime: evt.seekTime,
267
+ });
268
+ break;
269
+
270
+ case "pause":
271
+ this.emitTriggerActionOrIgnored({
272
+ action: "pause",
273
+ source: evt.source,
274
+ clientId: evt.clientId,
275
+ local,
276
+ seekTime: evt.seekTime,
277
+ });
278
+ break;
279
+
280
+ case "seekto":
281
+ this.emitTriggerActionOrIgnored({
282
+ action: "seekto",
283
+ source: evt.source,
284
+ clientId: evt.clientId,
285
+ local,
286
+ seekTime: evt.seekTime,
287
+ });
288
+ break;
289
+ }
247
290
  }
248
291
  );
249
292
  }
@@ -264,6 +307,10 @@ export class GroupCoordinatorState extends EventEmitter {
264
307
  return this._playbackPosition;
265
308
  }
266
309
 
310
+ public get playbackRate(): GroupPlaybackRate {
311
+ return this._playbackRate;
312
+ }
313
+
267
314
  public get waitingAt(): CoordinationWaitPoint | undefined {
268
315
  return this._waitPoint;
269
316
  }
@@ -312,6 +359,7 @@ export class GroupCoordinatorState extends EventEmitter {
312
359
  track: this.playbackTrack.current,
313
360
  trackData: this.playbackTrackData.current,
314
361
  transport: this.transportState.current,
362
+ playbackRate: this.playbackRate.current,
315
363
  playbackState: "suspended",
316
364
  position: positionState?.position || 0.0,
317
365
  waitPoint: this._waitPoint,
@@ -322,6 +370,7 @@ export class GroupCoordinatorState extends EventEmitter {
322
370
  track: this.playbackTrack.current,
323
371
  trackData: this.playbackTrackData.current,
324
372
  transport: this.transportState.current,
373
+ playbackRate: this.playbackRate.current,
325
374
  playbackState: this._waitPoint ? "waiting" : playbackState,
326
375
  position: positionState?.position || 0.0,
327
376
  waitPoint: this._waitPoint,
@@ -388,6 +437,32 @@ export class GroupCoordinatorState extends EventEmitter {
388
437
  }
389
438
  }
390
439
 
440
+ public handleRateChangeCommand(
441
+ event: ILiveEvent<IRateChangeCommandEvent>,
442
+ local: boolean
443
+ ): void {
444
+ const newState: IPlaybackRate = {
445
+ playbackRate: event.data.playbackRate,
446
+ clientId: event.clientId,
447
+ timestamp: event.timestamp,
448
+ };
449
+ const updated = this.playbackRate.updatePlaybackRate(newState, "user");
450
+
451
+ if (updated) {
452
+ const correlationId = LiveTelemetryLogger.formatCorrelationId(
453
+ event.clientId,
454
+ event.timestamp
455
+ );
456
+ this._logger.sendTelemetryEvent(
457
+ TelemetryEvents.SessionCoordinator.RemoteRateChangeReceived,
458
+ null,
459
+ {
460
+ correlationId: correlationId,
461
+ }
462
+ );
463
+ }
464
+ }
465
+
391
466
  public handleTransportCommand(
392
467
  event: ILiveEvent<ITransportCommandEvent>,
393
468
  local: boolean
@@ -579,7 +654,11 @@ export class GroupCoordinatorState extends EventEmitter {
579
654
  if (
580
655
  target >= 0.0 &&
581
656
  Math.abs(target - position) >=
582
- this._maxPlaybackDrift.seconds
657
+ Math.max(
658
+ this._maxPlaybackDrift.seconds,
659
+ this._maxPlaybackDrift.seconds *
660
+ this._playbackRate.rate
661
+ )
583
662
  ) {
584
663
  this._logger.sendTelemetryEvent(
585
664
  TelemetryEvents.GroupCoordinator.PositionOutOfSync,
@@ -589,7 +668,7 @@ export class GroupCoordinatorState extends EventEmitter {
589
668
  target: target,
590
669
  }
591
670
  );
592
- await this.emitTriggerAction({
671
+ this.emitTriggerAction({
593
672
  action: "catchup",
594
673
  source: "system",
595
674
  clientId: await waitUntilConnected(this._runtime),
@@ -613,7 +692,7 @@ export class GroupCoordinatorState extends EventEmitter {
613
692
  localClientId === this.transportState.current.clientId;
614
693
  switch (this.transportState.playbackState) {
615
694
  case "playing":
616
- await this.emitTriggerAction({
695
+ this.emitTriggerAction({
617
696
  action: "play",
618
697
  source: "system",
619
698
  clientId: this.transportState.current.clientId,
@@ -621,7 +700,7 @@ export class GroupCoordinatorState extends EventEmitter {
621
700
  });
622
701
  break;
623
702
  case "paused":
624
- await this.emitTriggerAction({
703
+ this.emitTriggerAction({
625
704
  action: "pause",
626
705
  source: "system",
627
706
  clientId: this.transportState.current.clientId,
@@ -642,7 +721,7 @@ export class GroupCoordinatorState extends EventEmitter {
642
721
  const local =
643
722
  localClientId ===
644
723
  this.playbackTrackData.current.clientId;
645
- await this.emitTriggerAction({
724
+ this.emitTriggerAction({
646
725
  action: "datachange",
647
726
  source: "system",
648
727
  clientId: this.playbackTrackData.current.clientId,
@@ -650,6 +729,23 @@ export class GroupCoordinatorState extends EventEmitter {
650
729
  data: this.playbackTrackData.data,
651
730
  });
652
731
  }
732
+
733
+ // Sync playback rate
734
+ if (this.playbackRate.rate != positionState?.playbackRate) {
735
+ this._logger.sendTelemetryEvent(
736
+ TelemetryEvents.GroupCoordinator.PlaybackRateOutOfSync
737
+ );
738
+ const local =
739
+ localClientId ===
740
+ this.playbackTrackData.current.clientId;
741
+ this.emitTriggerAction({
742
+ action: "ratechange",
743
+ source: "system",
744
+ clientId: this.playbackTrackData.current.clientId,
745
+ local,
746
+ playbackRate: this.playbackRate.rate,
747
+ });
748
+ }
653
749
  }
654
750
  }
655
751
  }
@@ -673,6 +769,16 @@ export class GroupCoordinatorState extends EventEmitter {
673
769
  });
674
770
  }
675
771
 
772
+ private emitTriggerActionOrIgnored(
773
+ details: ExtendedMediaSessionActionDetails
774
+ ): void {
775
+ if (this.isSuspended || this.isWaiting) {
776
+ return this.emitTriggerActionIgnored(details);
777
+ } else {
778
+ return this.emitTriggerAction(details);
779
+ }
780
+ }
781
+
676
782
  private emitTriggerAction(
677
783
  details: ExtendedMediaSessionActionDetails
678
784
  ): void {
@@ -682,4 +788,15 @@ export class GroupCoordinatorState extends EventEmitter {
682
788
  };
683
789
  this.emit(GroupCoordinatorStateEvents.triggeraction, evt);
684
790
  }
791
+
792
+ private emitTriggerActionIgnored(
793
+ details: ExtendedMediaSessionActionDetails
794
+ ): void {
795
+ details.ignoreReason = "localusersuspended";
796
+ const evt: ITriggerActionEvent = {
797
+ name: GroupCoordinatorStateEvents.triggeractionignored,
798
+ details: details,
799
+ };
800
+ this.emit(GroupCoordinatorStateEvents.triggeractionignored, evt);
801
+ }
685
802
  }
@@ -14,6 +14,7 @@ import {
14
14
  CoordinationWaitPoint,
15
15
  ExtendedMediaSessionPlaybackState,
16
16
  } from "../MediaSessionExtensions";
17
+ import { GroupPlaybackRate } from "./GroupPlaybackRate";
17
18
 
18
19
  /**
19
20
  * Per client position
@@ -33,6 +34,7 @@ export interface ICurrentPlaybackPosition {
33
34
  */
34
35
  export class GroupPlaybackPosition {
35
36
  private _transportState: GroupTransportState;
37
+ private _playbackRate: GroupPlaybackRate;
36
38
  private _runtime: IRuntimeSignaler;
37
39
  private _liveRuntime: LiveShareRuntime;
38
40
  private _updateInterval: TimeInterval;
@@ -40,11 +42,13 @@ export class GroupPlaybackPosition {
40
42
 
41
43
  constructor(
42
44
  transportState: GroupTransportState,
45
+ playbackRate: GroupPlaybackRate,
43
46
  runtime: IRuntimeSignaler,
44
47
  liveRuntime: LiveShareRuntime,
45
48
  updateInterval: TimeInterval
46
49
  ) {
47
50
  this._transportState = transportState;
51
+ this._playbackRate = playbackRate;
48
52
  this._runtime = runtime;
49
53
  this._liveRuntime = liveRuntime;
50
54
  this._updateInterval = updateInterval;
@@ -135,7 +139,8 @@ export class GroupPlaybackPosition {
135
139
  const now = this._liveRuntime.getTimestamp();
136
140
  const projected =
137
141
  this._transportState.startPosition +
138
- (now - this._transportState.timestamp) / 1000;
142
+ ((now - this._transportState.timestamp) / 1000) *
143
+ this._playbackRate.rate;
139
144
  return this.limitProjectedPosition(projected);
140
145
  } else {
141
146
  return this._transportState.startPosition;
@@ -175,7 +180,9 @@ export class GroupPlaybackPosition {
175
180
  // - This computation does not take into account future wait points.
176
181
  const projected =
177
182
  position.playbackState == "playing" && shouldProject
178
- ? position.position + (now - position.timestamp) / 1000
183
+ ? position.position +
184
+ ((now - position.timestamp) / 1000) *
185
+ this._playbackRate.rate
179
186
  : position.position;
180
187
  callbackFn(position, this.limitProjectedPosition(projected));
181
188
  }