@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.
- package/bin/LiveMediaSession.d.ts.map +1 -1
- package/bin/LiveMediaSession.js +8 -0
- package/bin/LiveMediaSession.js.map +1 -1
- package/bin/LiveMediaSessionCoordinator.d.ts +15 -0
- package/bin/LiveMediaSessionCoordinator.d.ts.map +1 -1
- package/bin/LiveMediaSessionCoordinator.js +25 -0
- package/bin/LiveMediaSessionCoordinator.js.map +1 -1
- package/bin/MediaPlayerSynchronizer.d.ts +6 -4
- package/bin/MediaPlayerSynchronizer.d.ts.map +1 -1
- package/bin/MediaPlayerSynchronizer.js +41 -12
- package/bin/MediaPlayerSynchronizer.js.map +1 -1
- package/bin/MediaSessionExtensions.d.ts +17 -4
- package/bin/MediaSessionExtensions.d.ts.map +1 -1
- package/bin/MediaSessionExtensions.js +4 -0
- package/bin/MediaSessionExtensions.js.map +1 -1
- package/bin/internals/GroupCoordinatorState.d.ts +15 -1
- package/bin/internals/GroupCoordinatorState.d.ts.map +1 -1
- package/bin/internals/GroupCoordinatorState.js +123 -57
- package/bin/internals/GroupCoordinatorState.js.map +1 -1
- package/bin/internals/GroupPlaybackPosition.d.ts +3 -1
- package/bin/internals/GroupPlaybackPosition.d.ts.map +1 -1
- package/bin/internals/GroupPlaybackPosition.js +7 -3
- package/bin/internals/GroupPlaybackPosition.js.map +1 -1
- package/bin/internals/GroupPlaybackRate.d.ts +39 -0
- package/bin/internals/GroupPlaybackRate.d.ts.map +1 -0
- package/bin/internals/GroupPlaybackRate.js +62 -0
- package/bin/internals/GroupPlaybackRate.js.map +1 -0
- package/bin/internals/GroupPlaybackTrack.d.ts +2 -2
- package/bin/internals/GroupPlaybackTrack.d.ts.map +1 -1
- package/bin/internals/GroupPlaybackTrack.js +3 -2
- package/bin/internals/GroupPlaybackTrack.js.map +1 -1
- package/bin/internals/GroupPlaybackTrackData.d.ts +3 -3
- package/bin/internals/GroupPlaybackTrackData.d.ts.map +1 -1
- package/bin/internals/GroupPlaybackTrackData.js +12 -11
- package/bin/internals/GroupPlaybackTrackData.js.map +1 -1
- package/bin/internals/GroupTransportState.d.ts +6 -3
- package/bin/internals/GroupTransportState.d.ts.map +1 -1
- package/bin/internals/GroupTransportState.js +16 -11
- package/bin/internals/GroupTransportState.js.map +1 -1
- package/bin/internals/consts.d.ts +8 -1
- package/bin/internals/consts.d.ts.map +1 -1
- package/bin/internals/consts.js +8 -1
- package/bin/internals/consts.js.map +1 -1
- package/bin/internals/index.d.ts +1 -0
- package/bin/internals/index.d.ts.map +1 -1
- package/bin/internals/index.js +1 -0
- package/bin/internals/index.js.map +1 -1
- package/bin/internals/interfaces.d.ts +10 -0
- package/bin/internals/interfaces.d.ts.map +1 -0
- package/bin/internals/interfaces.js +3 -0
- package/bin/internals/interfaces.js.map +1 -0
- package/package.json +3 -3
- package/src/LiveMediaSession.ts +12 -0
- package/src/LiveMediaSessionCoordinator.ts +49 -0
- package/src/MediaPlayerSynchronizer.ts +55 -15
- package/src/MediaSessionExtensions.ts +18 -2
- package/src/internals/GroupCoordinatorState.ts +196 -79
- package/src/internals/GroupPlaybackPosition.ts +9 -2
- package/src/internals/GroupPlaybackRate.ts +90 -0
- package/src/internals/GroupPlaybackTrack.ts +5 -4
- package/src/internals/GroupPlaybackTrackData.ts +14 -13
- package/src/internals/GroupTransportState.ts +21 -12
- package/src/internals/consts.ts +14 -2
- package/src/internals/index.ts +1 -0
- 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.
|
|
233
|
+
this._blockUnexpectedPlayerEvents &&
|
|
234
|
+
this._expectedPlaybackRate !==
|
|
235
|
+
this._player.playbackRate &&
|
|
233
236
|
!this._mediaSession.coordinator.isSuspended
|
|
234
237
|
) {
|
|
235
|
-
this.
|
|
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
|
-
.
|
|
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`,
|
|
405
|
-
* `mediaSession.coordinator.canSetTrackData
|
|
406
|
-
* grained control over the local clients policies,
|
|
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
|
-
*
|
|
186
|
+
* Reason an action is ignored by the local client, undefined if not ignored.
|
|
173
187
|
*/
|
|
174
|
-
|
|
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 {
|
|
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(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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.
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 +
|
|
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
|
}
|