@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
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* Licensed under the Microsoft Live Share SDK License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { IEvent,
|
|
6
|
+
import { IEvent, ILiveEvent, LiveEvent, TimeInterval, IRuntimeSignaler, LiveTelemetryLogger } from '@microsoft/live-share';
|
|
7
7
|
import EventEmitter from 'events';
|
|
8
8
|
import { ExtendedMediaMetadata, CoordinationWaitPoint, ExtendedMediaSessionPlaybackState, ExtendedMediaSessionActionDetails } from '../MediaSessionExtensions';
|
|
9
9
|
import { GroupPlaybackTrack, GroupPlaybackTrackEvents, IPlaybackTrack } from './GroupPlaybackTrack';
|
|
10
10
|
import { GroupTransportState, ITransportState } from './GroupTransportState';
|
|
11
11
|
import { GroupPlaybackPosition, ICurrentPlaybackPosition } from './GroupPlaybackPosition';
|
|
12
|
-
import { IMediaPlayerState } from '../
|
|
12
|
+
import { IMediaPlayerState } from '../LiveMediaSessionCoordinator';
|
|
13
13
|
import { GroupTransportStateEvents } from './GroupTransportState';
|
|
14
14
|
import { GroupPlaybackTrackData, PlaybackTrackDataEvents, IPlaybackTrackData } from './GroupPlaybackTrackData';
|
|
15
15
|
import { TelemetryEvents } from './consts';
|
|
@@ -17,7 +17,7 @@ import { TelemetryEvents } from './consts';
|
|
|
17
17
|
/**
|
|
18
18
|
* @hidden
|
|
19
19
|
*/
|
|
20
|
-
export interface IPositionUpdateEvent extends
|
|
20
|
+
export interface IPositionUpdateEvent extends ILiveEvent {
|
|
21
21
|
track: IPlaybackTrack;
|
|
22
22
|
trackData: IPlaybackTrackData;
|
|
23
23
|
transport: ITransportState;
|
|
@@ -29,7 +29,7 @@ export interface IPositionUpdateEvent extends IEphemeralEvent {
|
|
|
29
29
|
/**
|
|
30
30
|
* @hidden
|
|
31
31
|
*/
|
|
32
|
-
export interface ITransportCommandEvent extends
|
|
32
|
+
export interface ITransportCommandEvent extends ILiveEvent {
|
|
33
33
|
track: IPlaybackTrack;
|
|
34
34
|
position: number;
|
|
35
35
|
}
|
|
@@ -37,7 +37,7 @@ export interface ITransportCommandEvent extends IEphemeralEvent {
|
|
|
37
37
|
/**
|
|
38
38
|
* @hidden
|
|
39
39
|
*/
|
|
40
|
-
export interface ISetTrackEvent extends
|
|
40
|
+
export interface ISetTrackEvent extends ILiveEvent {
|
|
41
41
|
metadata: ExtendedMediaMetadata|null;
|
|
42
42
|
waitPoints: CoordinationWaitPoint[];
|
|
43
43
|
}
|
|
@@ -45,7 +45,7 @@ export interface ISetTrackEvent extends IEphemeralEvent {
|
|
|
45
45
|
/**
|
|
46
46
|
* @hidden
|
|
47
47
|
*/
|
|
48
|
-
export interface ISetTrackDataEvent extends
|
|
48
|
+
export interface ISetTrackDataEvent extends ILiveEvent {
|
|
49
49
|
data: object|null;
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -69,7 +69,7 @@ export enum GroupCoordinatorStateEvents {
|
|
|
69
69
|
*/
|
|
70
70
|
export class GroupCoordinatorState extends EventEmitter {
|
|
71
71
|
private readonly _runtime: IRuntimeSignaler;
|
|
72
|
-
private readonly _logger:
|
|
72
|
+
private readonly _logger: LiveTelemetryLogger;
|
|
73
73
|
private readonly _maxPlaybackDrift: TimeInterval;
|
|
74
74
|
private _getMediaPlayerState: () => IMediaPlayerState;
|
|
75
75
|
|
|
@@ -93,7 +93,7 @@ export enum GroupCoordinatorStateEvents {
|
|
|
93
93
|
constructor (runtime: IRuntimeSignaler, maxPlaybackDrift: TimeInterval, positionUpdateInterval: TimeInterval, getMediaPlayerState: () => IMediaPlayerState) {
|
|
94
94
|
super();
|
|
95
95
|
this._runtime = runtime;
|
|
96
|
-
this._logger = new
|
|
96
|
+
this._logger = new LiveTelemetryLogger(runtime);
|
|
97
97
|
this._maxPlaybackDrift = maxPlaybackDrift;
|
|
98
98
|
this._playbackTrack = new GroupPlaybackTrack(getMediaPlayerState);
|
|
99
99
|
this._playbackTrackData = new GroupPlaybackTrackData(this._playbackTrack);
|
|
@@ -240,7 +240,7 @@ export enum GroupCoordinatorStateEvents {
|
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
if (updated) {
|
|
243
|
-
this._logger.sendTelemetryEvent(TelemetryEvents.SessionCoordinator.RemoteSetTrackReceived, null, {correlationId:
|
|
243
|
+
this._logger.sendTelemetryEvent(TelemetryEvents.SessionCoordinator.RemoteSetTrackReceived, null, {correlationId: LiveTelemetryLogger.formatCorrelationId(event.clientId, event.timestamp)});
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
@@ -254,7 +254,7 @@ export enum GroupCoordinatorStateEvents {
|
|
|
254
254
|
});
|
|
255
255
|
|
|
256
256
|
if (updated) {
|
|
257
|
-
this._logger.sendTelemetryEvent(TelemetryEvents.SessionCoordinator.RemoteSetTrackReceived, null, {correlationId:
|
|
257
|
+
this._logger.sendTelemetryEvent(TelemetryEvents.SessionCoordinator.RemoteSetTrackReceived, null, {correlationId: LiveTelemetryLogger.formatCorrelationId(event.clientId, event.timestamp)});
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
@@ -284,7 +284,7 @@ export enum GroupCoordinatorStateEvents {
|
|
|
284
284
|
const updated = this.transportState.updateState(newState);
|
|
285
285
|
|
|
286
286
|
if (updated) {
|
|
287
|
-
const correlationId =
|
|
287
|
+
const correlationId = LiveTelemetryLogger.formatCorrelationId(event.clientId, event.timestamp);
|
|
288
288
|
switch (event.name) {
|
|
289
289
|
case 'play':
|
|
290
290
|
this._logger.sendTelemetryEvent(TelemetryEvents.SessionCoordinator.RemotePlayReceived, null, {correlationId: correlationId});
|
|
@@ -341,7 +341,7 @@ export enum GroupCoordinatorStateEvents {
|
|
|
341
341
|
if (event.playbackState != this._lastStateChange) {
|
|
342
342
|
this._logger.sendTelemetryEvent(TelemetryEvents.GroupCoordinator.BeginSoftSuspension, null, {playbackState: event.playbackState});
|
|
343
343
|
this._lastStateChange = event.playbackState;
|
|
344
|
-
this._lastStateChangeTime =
|
|
344
|
+
this._lastStateChangeTime = LiveEvent.getTimestamp();
|
|
345
345
|
}
|
|
346
346
|
break;
|
|
347
347
|
}
|
|
@@ -366,7 +366,7 @@ export enum GroupCoordinatorStateEvents {
|
|
|
366
366
|
|
|
367
367
|
public async syncLocalMediaSession(): Promise<void> {
|
|
368
368
|
// Skip further syncs if we're waiting or in a "soft suspension".
|
|
369
|
-
const softSuspensionDelta =
|
|
369
|
+
const softSuspensionDelta = LiveEvent.getTimestamp() - this._lastStateChangeTime;
|
|
370
370
|
if (!this.isWaiting && softSuspensionDelta >= 1000) {
|
|
371
371
|
let { metadata, trackData, positionState, playbackState } = this._getMediaPlayerState();
|
|
372
372
|
this._logger.sendTelemetryEvent(TelemetryEvents.GroupCoordinator.CheckingForSyncIssues);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Licensed under the Microsoft Live Share SDK License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { LiveEvent, IRuntimeSignaler, TimeInterval } from '@microsoft/live-share';
|
|
7
7
|
import { GroupTransportState } from './GroupTransportState';
|
|
8
8
|
import { GroupPlaybackTrackEvents } from './GroupPlaybackTrack';
|
|
9
9
|
import { CoordinationWaitPoint, ExtendedMediaSessionPlaybackState } from '../MediaSessionExtensions';
|
|
@@ -109,7 +109,7 @@ export class GroupPlaybackPosition {
|
|
|
109
109
|
*/
|
|
110
110
|
public get maxPosition(): number {
|
|
111
111
|
if (this._transportState.playbackState == 'playing') {
|
|
112
|
-
const now =
|
|
112
|
+
const now = LiveEvent.getTimestamp();
|
|
113
113
|
const projected = this._transportState.startPosition + ((now - this._transportState.timestamp) / 1000);
|
|
114
114
|
return this.limitProjectedPosition(projected);
|
|
115
115
|
} else {
|
|
@@ -136,7 +136,7 @@ export class GroupPlaybackPosition {
|
|
|
136
136
|
* @param callbackFn Function applied to each position,
|
|
137
137
|
*/
|
|
138
138
|
public forEach(callbackFn: (position: ICurrentPlaybackPosition, projectedPosition: number) => void): void {
|
|
139
|
-
const now =
|
|
139
|
+
const now = LiveEvent.getTimestamp();
|
|
140
140
|
const ignoreBefore = now - (this._updateInterval.milliseconds * 2);
|
|
141
141
|
const shouldProject = !this._transportState.track.metadata?.liveStream;
|
|
142
142
|
this._positions.forEach((value, key) => {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { IEvent } from '@microsoft/live-share';
|
|
7
7
|
import EventEmitter from 'events';
|
|
8
|
-
import { IMediaPlayerState } from '../
|
|
8
|
+
import { IMediaPlayerState } from '../LiveMediaSessionCoordinator';
|
|
9
9
|
import { CoordinationWaitPoint, ExtendedMediaMetadata } from '../MediaSessionExtensions';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Licensed under the Microsoft Live Share SDK License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { IEvent,
|
|
6
|
+
import { IEvent, LiveEvent } from '@microsoft/live-share';
|
|
7
7
|
import EventEmitter from 'events';
|
|
8
|
-
import { IMediaPlayerState } from '../
|
|
8
|
+
import { IMediaPlayerState } from '../LiveMediaSessionCoordinator';
|
|
9
9
|
import { ExtendedMediaSessionPlaybackState, ExtendedMediaSessionAction } from '../MediaSessionExtensions';
|
|
10
10
|
import { GroupPlaybackTrack, GroupPlaybackTrackEvents } from './GroupPlaybackTrack';
|
|
11
11
|
|
|
@@ -115,7 +115,7 @@ export interface ITransportStateChangeEvent extends IEvent {
|
|
|
115
115
|
if (originalState.playbackState == state.playbackState && playerState != 'ended') {
|
|
116
116
|
this.emit(GroupTransportStateEvents.transportStateChange, { type: GroupTransportStateEvents.transportStateChange, action: 'seekto', seekTime: state.startPosition });
|
|
117
117
|
} else if (state.playbackState == 'playing') {
|
|
118
|
-
const now =
|
|
118
|
+
const now = LiveEvent.getTimestamp();
|
|
119
119
|
const projectedPosition = state.startPosition + ((now - state.timestamp) / 1000);
|
|
120
120
|
this.emit(GroupTransportStateEvents.transportStateChange, { type: GroupTransportStateEvents.transportStateChange, action: 'play', seekTime: projectedPosition});
|
|
121
121
|
} else {
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
import 'mocha';
|
|
7
7
|
import { strict as assert } from 'assert';
|
|
8
8
|
import { CollaborationSpace, CollaborationSpaceEvents, Deferred } from '@microsoft/teams-collaboration';
|
|
9
|
-
import { LastMediaPositionState } from '../
|
|
9
|
+
import { LastMediaPositionState } from '../LiveMediaSessionCoordinator';
|
|
10
10
|
import { ExtendedMediaMetadata, ExtendedMediaSessionActionDetails } from '../MediaSessionExtensions';
|
|
11
|
-
import { SharedMediaSession } from '../
|
|
11
|
+
import { SharedMediaSession } from '../LiveMediaSession';
|
|
12
12
|
|
|
13
13
|
export class TestMediaSession extends SharedMediaSession {
|
|
14
14
|
private done = new Deferred<ExtendedMediaSessionActionDetails>();
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { strict as assert } from "assert";
|
|
2
|
+
import { IMediaPlayer } from "../IMediaPlayer"
|
|
3
|
+
import { LimitLevelType, VolumeManager } from "../VolumeManager"
|
|
4
|
+
import { Deferred } from '@microsoft/live-share/src/test/Deferred';
|
|
5
|
+
|
|
6
|
+
describe('VolumeManager', () => {
|
|
7
|
+
it('should ramp down volume', async () => {
|
|
8
|
+
const testAwait = new Deferred();
|
|
9
|
+
const player = new TestMediaPlayer();
|
|
10
|
+
player.volume = 1.0
|
|
11
|
+
const volumeManager = new VolumeManager(player);
|
|
12
|
+
|
|
13
|
+
assert(player.volume == 1.0);
|
|
14
|
+
volumeManager.startLimiting();
|
|
15
|
+
setTimeout(() => {
|
|
16
|
+
// check volume at halfway point
|
|
17
|
+
assert(player.volume > 0.4);
|
|
18
|
+
assert(player.volume < 0.6);
|
|
19
|
+
}, volumeManager.volumeChangeDuration * 1000 / 2);
|
|
20
|
+
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
// check volume at end with 20ms of leeway
|
|
23
|
+
assert(player.volume == volumeManager.limitLevel);
|
|
24
|
+
testAwait.resolve();
|
|
25
|
+
}, volumeManager.volumeChangeDuration * 1000 + 20);
|
|
26
|
+
|
|
27
|
+
await testAwait.promise;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should ramp up volume', async () => {
|
|
31
|
+
const testAwait = new Deferred();
|
|
32
|
+
const player = new TestMediaPlayer();
|
|
33
|
+
player.volume = 1.0
|
|
34
|
+
const volumeManager = new VolumeManager(player);
|
|
35
|
+
|
|
36
|
+
// limit at start
|
|
37
|
+
volumeManager.startLimiting();
|
|
38
|
+
|
|
39
|
+
// when limited all the way, test ramp up
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
assert(player.volume == volumeManager.limitLevel);
|
|
42
|
+
volumeManager.stopLimiting();
|
|
43
|
+
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
// check volume at halfway point
|
|
46
|
+
assert(player.volume > 0.4);
|
|
47
|
+
assert(player.volume < 0.6);
|
|
48
|
+
}, volumeManager.volumeChangeDuration * 1000 / 2);
|
|
49
|
+
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
// check volume at end with 20ms of leeway
|
|
52
|
+
assert(player.volume == volumeManager.volume);
|
|
53
|
+
testAwait.resolve();
|
|
54
|
+
}, volumeManager.volumeChangeDuration * 1000 + 20);
|
|
55
|
+
|
|
56
|
+
}, volumeManager.volumeChangeDuration * 1000 + 20);
|
|
57
|
+
|
|
58
|
+
await testAwait.promise;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should ramp down halfway, then ramp up', async () => {
|
|
62
|
+
const testAwait = new Deferred();
|
|
63
|
+
const player = new TestMediaPlayer();
|
|
64
|
+
player.volume = 1.0
|
|
65
|
+
|
|
66
|
+
const volumeManager = new VolumeManager(player);
|
|
67
|
+
|
|
68
|
+
volumeManager.startLimiting();
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
// check volume at halfway point, begin ramping other direction
|
|
71
|
+
assert(player.volume > 0.4);
|
|
72
|
+
assert(player.volume < 0.6);
|
|
73
|
+
volumeManager.stopLimiting();
|
|
74
|
+
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
// check volume at end with 20ms of leeway
|
|
77
|
+
assert(player.volume == volumeManager.volume);
|
|
78
|
+
testAwait.resolve();
|
|
79
|
+
}, volumeManager.volumeChangeDuration * 1000 + 20);
|
|
80
|
+
|
|
81
|
+
}, volumeManager.volumeChangeDuration * 1000 / 2);
|
|
82
|
+
|
|
83
|
+
await testAwait.promise;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('ramp using selected volume', async () => {
|
|
87
|
+
const testAwait = new Deferred();
|
|
88
|
+
const player = new TestMediaPlayer();
|
|
89
|
+
player.volume = 1.0
|
|
90
|
+
|
|
91
|
+
const volumeManager = new VolumeManager(player);
|
|
92
|
+
|
|
93
|
+
volumeManager.volume = 0.3;
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
// check volume at halfway point
|
|
96
|
+
assert(player.volume > 0.6);
|
|
97
|
+
assert(player.volume < 0.7);
|
|
98
|
+
}, volumeManager.volumeChangeDuration * 1000 / 2);
|
|
99
|
+
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
// check volume at end with 20ms of leeway
|
|
102
|
+
assert(player.volume == volumeManager.volume);
|
|
103
|
+
testAwait.resolve();
|
|
104
|
+
}, volumeManager.volumeChangeDuration * 1000 + 20);
|
|
105
|
+
|
|
106
|
+
await testAwait.promise;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('enable limit, change selected volume, then disable limit', async () => {
|
|
110
|
+
const testAwait = new Deferred();
|
|
111
|
+
const player = new TestMediaPlayer();
|
|
112
|
+
player.volume = 1.0
|
|
113
|
+
const volumeManager = new VolumeManager(player);
|
|
114
|
+
|
|
115
|
+
volumeManager.startLimiting();
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
// check volume at halfway point, begin ramping other direction with lower selected volume
|
|
118
|
+
assert(player.volume > 0.4);
|
|
119
|
+
assert(player.volume < 0.6);
|
|
120
|
+
|
|
121
|
+
volumeManager.volume = 0.7;
|
|
122
|
+
volumeManager.stopLimiting();
|
|
123
|
+
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
// check volume at end with 20ms of leeway
|
|
126
|
+
assert(player.volume === volumeManager.volume);
|
|
127
|
+
testAwait.resolve();
|
|
128
|
+
}, volumeManager.volumeChangeDuration * 1000 + 20);
|
|
129
|
+
|
|
130
|
+
}, volumeManager.volumeChangeDuration * 1000 / 2);
|
|
131
|
+
|
|
132
|
+
await testAwait.promise;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('ramp down with selected volume to 0.3, then up to 0.8', async () => {
|
|
136
|
+
const testAwait = new Deferred();
|
|
137
|
+
const player = new TestMediaPlayer();
|
|
138
|
+
player.volume = 1.0
|
|
139
|
+
|
|
140
|
+
const volumeManager = new VolumeManager(player);
|
|
141
|
+
volumeManager.volume = 0.3;
|
|
142
|
+
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
// check volume at halfway point
|
|
145
|
+
assert(player.volume > 0.6);
|
|
146
|
+
assert(player.volume < 0.7);
|
|
147
|
+
volumeManager.volume = 0.8;
|
|
148
|
+
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
// check volume at end with 20ms of leeway
|
|
151
|
+
assert(player.volume == volumeManager.volume);
|
|
152
|
+
testAwait.resolve();
|
|
153
|
+
}, volumeManager.volumeChangeDuration * 1000 + 20);
|
|
154
|
+
}, volumeManager.volumeChangeDuration * 1000 / 2);
|
|
155
|
+
|
|
156
|
+
await testAwait.promise;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('test 50% limiting', async () => {
|
|
160
|
+
const testAwait = new Deferred();
|
|
161
|
+
const player = new TestMediaPlayer();
|
|
162
|
+
player.volume = 1.0
|
|
163
|
+
|
|
164
|
+
const volumeManager = new VolumeManager(player);
|
|
165
|
+
volumeManager.limitLevelType = LimitLevelType.percentage;
|
|
166
|
+
volumeManager.limitLevel = 0.5;
|
|
167
|
+
|
|
168
|
+
volumeManager.startLimiting();
|
|
169
|
+
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
// check volume at halfway point
|
|
172
|
+
assert(player.volume > 0.7);
|
|
173
|
+
assert(player.volume < 0.8);
|
|
174
|
+
}, volumeManager.volumeChangeDuration * 1000 / 2);
|
|
175
|
+
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
// check volume at end with 20ms of leeway
|
|
178
|
+
assert(player.volume == volumeManager.limitLevel * volumeManager.volume);
|
|
179
|
+
testAwait.resolve();
|
|
180
|
+
}, volumeManager.volumeChangeDuration * 1000 + 20);
|
|
181
|
+
|
|
182
|
+
await testAwait.promise;
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// test implementation
|
|
187
|
+
class TestMediaPlayer implements IMediaPlayer {
|
|
188
|
+
currentSrc: string;
|
|
189
|
+
currentTime: number;
|
|
190
|
+
duration: number;
|
|
191
|
+
ended: boolean;
|
|
192
|
+
muted: boolean;
|
|
193
|
+
paused: boolean;
|
|
194
|
+
playbackRate: number;
|
|
195
|
+
src: string;
|
|
196
|
+
volume: number;
|
|
197
|
+
load(): void {
|
|
198
|
+
throw new Error("Method not implemented.");
|
|
199
|
+
}
|
|
200
|
+
pause(): void {
|
|
201
|
+
throw new Error("Method not implemented.");
|
|
202
|
+
}
|
|
203
|
+
play(): Promise<void> {
|
|
204
|
+
throw new Error("Method not implemented.");
|
|
205
|
+
}
|
|
206
|
+
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined): void {
|
|
207
|
+
throw new Error("Method not implemented.");
|
|
208
|
+
}
|
|
209
|
+
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions | undefined): void {
|
|
210
|
+
throw new Error("Method not implemented.");
|
|
211
|
+
}
|
|
212
|
+
}
|
package/src/VolumeLimiter.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
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 LevelType { fixed, percentage }
|
|
10
|
-
|
|
11
|
-
export class VolumeLimiter {
|
|
12
|
-
private readonly _player: IMediaPlayer;
|
|
13
|
-
|
|
14
|
-
// Settings
|
|
15
|
-
private readonly _duration = new TimeInterval(2000);
|
|
16
|
-
private readonly _rampDownDuration = new TimeInterval(500);
|
|
17
|
-
private readonly _rampUpDuration = new TimeInterval(500);
|
|
18
|
-
private _level: number = 0.1
|
|
19
|
-
private _levelType: LevelType = LevelType.fixed;
|
|
20
|
-
|
|
21
|
-
// Running state
|
|
22
|
-
private _running = false;
|
|
23
|
-
private _endTime = 0;
|
|
24
|
-
private _startTime = 0;
|
|
25
|
-
private _startVolume = 0;
|
|
26
|
-
private _inRamp = false;
|
|
27
|
-
|
|
28
|
-
constructor(player: IMediaPlayer) {
|
|
29
|
-
this._player = player;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Time, in seconds, that the volume should be lowered.
|
|
34
|
-
*
|
|
35
|
-
* @remarks
|
|
36
|
-
* Applications should periodically call `lowerVolume()` to keep the volume lowered.
|
|
37
|
-
*/
|
|
38
|
-
public get duration(): number {
|
|
39
|
-
return this._duration.seconds;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
public set duration(value: number) {
|
|
43
|
-
this._duration.seconds = Math.abs(value);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Target level to lower volume to.
|
|
48
|
-
*
|
|
49
|
-
* @remarks
|
|
50
|
-
* Expressed as a value between 0.0 and 1.0. The value is applied based upon the configured
|
|
51
|
-
* `levelType`. The default value is 0.2.
|
|
52
|
-
*
|
|
53
|
-
* For a level type of `LevelType.fixed` the value is the exact level the volume will be
|
|
54
|
-
* lowered to. The default value of 0.2 would cause the volume to be lowered to 0.2.
|
|
55
|
-
*
|
|
56
|
-
* For a level type of `LevelType.percentage` the value is the percentage by which the volume
|
|
57
|
-
* level should be lowered to. The default value of 0.2 would cause the volume to be lowered
|
|
58
|
-
* to 20% of its starting value.
|
|
59
|
-
*/
|
|
60
|
-
public get level(): number {
|
|
61
|
-
return this._level;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
public set level(value: number) {
|
|
65
|
-
if (value < 0 || value > 1.0) {
|
|
66
|
-
throw new Error(`VolumeLimiter: cannot set level to ${value}. Level must be between 0.0 and 1.0.`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
this._level = value;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* The type of level represented by the `level` property.
|
|
74
|
-
*/
|
|
75
|
-
public get levelType(): LevelType {
|
|
76
|
-
return this._levelType;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
public set levelType(value: LevelType) {
|
|
80
|
-
this._levelType = value;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Amount of time, in seconds, it should take to ramp the volume down to the desired level.
|
|
85
|
-
*/
|
|
86
|
-
public get rampDownDuration(): number {
|
|
87
|
-
return this._rampDownDuration.seconds;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
public set rampDownDuration(value: number) {
|
|
91
|
-
this._rampDownDuration.seconds = Math.abs(value);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Amount of time, in seconds, it should take to ramp the volume back up to the original level.
|
|
96
|
-
*/
|
|
97
|
-
public get rampUpDuration(): number {
|
|
98
|
-
return this._rampUpDuration.seconds;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
public set rampUpDuration(value: number) {
|
|
102
|
-
this._rampUpDuration.seconds = Math.abs(value);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Temporarily lowers an audio sources volume.
|
|
107
|
-
*/
|
|
108
|
-
public lowerVolume(): void {
|
|
109
|
-
this._endTime = new Date().getTime() + this._duration.milliseconds;
|
|
110
|
-
if (!this._running) {
|
|
111
|
-
this.startLimiter();
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private startLimiter(): void {
|
|
116
|
-
const adjustVolume = () => {
|
|
117
|
-
const now = new Date().getTime();
|
|
118
|
-
if (now < this._endTime) {
|
|
119
|
-
// Make no adjustment if muted
|
|
120
|
-
if (!this._player.muted) {
|
|
121
|
-
// Compute adjusted volume level
|
|
122
|
-
let volume: number;
|
|
123
|
-
const targetVolume = this.computeTargetVolume();
|
|
124
|
-
const rampRange = this._startVolume - targetVolume;
|
|
125
|
-
const rampUpStart = this._endTime - this._rampUpDuration.milliseconds;
|
|
126
|
-
const rampDownEnd = this._startTime + this._rampDownDuration.milliseconds;
|
|
127
|
-
if (now > rampUpStart) {
|
|
128
|
-
// We're about to end so ramping up
|
|
129
|
-
const progress = (now - rampUpStart) / this._rampUpDuration.milliseconds;
|
|
130
|
-
volume = targetVolume + (rampRange * progress);
|
|
131
|
-
this._player.volume = volume;
|
|
132
|
-
this._inRamp = true;
|
|
133
|
-
} else if (now < rampDownEnd) {
|
|
134
|
-
// We just begun so ramping down
|
|
135
|
-
const remaining = (rampDownEnd - now) / this._rampUpDuration.milliseconds;
|
|
136
|
-
volume = targetVolume + (rampRange * remaining);
|
|
137
|
-
this._player.volume = volume;
|
|
138
|
-
this._inRamp = true;
|
|
139
|
-
} else if (this._inRamp) {
|
|
140
|
-
// We're at the end of a ramp
|
|
141
|
-
volume = targetVolume;
|
|
142
|
-
this._player.volume = volume;
|
|
143
|
-
this._inRamp = false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Schedule next animation frame
|
|
148
|
-
this.scheduleAnimationFrame(adjustVolume);
|
|
149
|
-
} else {
|
|
150
|
-
// Reset volume to original level
|
|
151
|
-
this._player.volume = this._startVolume;
|
|
152
|
-
this._running = false;
|
|
153
|
-
this._endTime = 0;
|
|
154
|
-
this._startTime = 0;
|
|
155
|
-
this._startVolume = 0;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
this._running = true;
|
|
160
|
-
this._startTime = new Date().getTime();
|
|
161
|
-
this._startVolume = this._player.volume;
|
|
162
|
-
this.scheduleAnimationFrame(adjustVolume)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private scheduleAnimationFrame(callback: FrameRequestCallback): void {
|
|
166
|
-
if (requestAnimationFrame) {
|
|
167
|
-
requestAnimationFrame(callback);
|
|
168
|
-
} else {
|
|
169
|
-
setTimeout(callback, 20);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private computeTargetVolume(): number {
|
|
174
|
-
switch (this._levelType) {
|
|
175
|
-
case LevelType.percentage:
|
|
176
|
-
return this._startVolume * this._level;
|
|
177
|
-
case LevelType.fixed:
|
|
178
|
-
default:
|
|
179
|
-
return this._level;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|