Skip to content

Commit

Permalink
maxPlaybackDrift and positionUpdateInterval scaling by audience s…
Browse files Browse the repository at this point in the history
…ize (#796)
  • Loading branch information
ryanbliss authored Sep 24, 2024
1 parent 01a704e commit e80c59b
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 32 deletions.
106 changes: 96 additions & 10 deletions packages/live-share-media/src/LiveMediaSessionCoordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
TrackMetadataNotSetError,
ActionBlockedError,
} from "./internals/errors.js";

import { PriorityTimeInterval } from "./internals/PriorityTimeInterval.js";
import { LiveMediaSessionCoordinatorSuspension } from "./LiveMediaSessionCoordinatorSuspension.js";
import { TypedEventEmitter } from "@fluid-internal/client-utils";
import { IEvent } from "@fluidframework/core-interfaces";
Expand Down Expand Up @@ -97,8 +97,27 @@ export class LiveMediaSessionCoordinator extends TypedEventEmitter<ILiveMediaSes
private readonly _liveRuntime: LiveShareRuntime;
private readonly _logger: LiveTelemetryLogger;
private readonly _getPlayerState: () => IMediaPlayerState;
private _positionUpdateInterval = new TimeInterval(2000);
private _maxPlaybackDrift = new TimeInterval(1000);
private _positionUpdateInterval = new PriorityTimeInterval(
2000,
// Scale by function
() => {
const audience = this._liveRuntime.audience;
if (!audience) return 1;
// As the audience size gets bigger, we relax the update interval, since more variance is expected
const count = audience.getMembers().size;
return 1 + count / 5;
}
);
private _maxPlaybackDrift = new PriorityTimeInterval(
1500, // Scale by function
() => {
const audience = this._liveRuntime.audience;
if (!audience) return 1;
// As the audience size gets bigger, we relax the playback drift, since more variance is expected
const count = audience.getMembers().size;
return 1 + count / 25;
}
);
private _lastWaitPoint?: CoordinationWaitPoint;
private initializeState: LiveDataObjectInitializeState =
LiveDataObjectInitializeState.needed;
Expand Down Expand Up @@ -222,13 +241,29 @@ export class LiveMediaSessionCoordinator extends TypedEventEmitter<ILiveMediaSes
}

/**
* Max amount of playback drift allowed in seconds.
* Controls whether or not to scale {@link maxPlaybackDrift} by the number of users in the session.
*
* @remarks
* Should the local clients playback position lag by more than the specified value, the
* coordinator will trigger a `catchup` action.
* As more clients join a session, it is more likely that some users will fall behind the presenter.
* It is also less likely that users will notice drift as more clients are added.
* It is usually helpful to be more lenient as more clients are added so everyone can watch all the content.
* Default value is true.
*/
public get shouldScaleMaxPlaybackDrift(): boolean {
return this._maxPlaybackDrift.shouldPrioritize;
}

public set shouldScaleMaxPlaybackDrift(value: boolean) {
this._maxPlaybackDrift.shouldPrioritize = value;
}

/**
* Max amount of playback drift allowed in seconds.
* This will scale automatically according to the number of participants in the session when {@link shouldScaleMaxPlaybackDrift} is true.
*
* Defaults to a value of `1` second.
* @remarks
* Should the local clients playback position lag by more than the specified value, the coordinator will trigger a `catchup` action.
* Default value is `1.5` seconds.
*/
public get maxPlaybackDrift(): number {
return this._maxPlaybackDrift.seconds;
Expand All @@ -239,11 +274,30 @@ export class LiveMediaSessionCoordinator extends TypedEventEmitter<ILiveMediaSes
}

/**
* Frequency with which position updates are broadcast to the rest of the group in
* seconds.
* Controls whether or not to compute {@link positionUpdateInterval} based on whether user has initiated a playback command.
* Default value is true.
*
* @remarks
* Defaults to a value of `2` seconds.
* When true, users that are simply watching but not controlling playback will report their positions less frequently.
* When a user has sent a transport command, they will begin sending position updates more frequently.
* Most Live Share apps have a concept of a primary presenter, and don't allow anyone to pause/play for the group.
* By ensuring only one/few users broadcast updates frequently, the server load is reduced considerably.
*/
public get shouldScalePositionUpdateInterval(): boolean {
return this._positionUpdateInterval.shouldPrioritize;
}

public set shouldScalePositionUpdateInterval(value: boolean) {
this._positionUpdateInterval.shouldPrioritize = value;
}

/**
* Frequency with which position updates are broadcast to the rest of the group in seconds.
* When {@link shouldScalePositionUpdateInterval} is set to `true`, the value set is the minimum value.
* The value returned is the actual value used to send updates by the local user, which may be larger than the value set.
*
* @remarks
* Defaults to a minimum value of `2` seconds.
*/
public get positionUpdateInterval(): number {
return this._positionUpdateInterval.seconds;
Expand Down Expand Up @@ -293,6 +347,10 @@ export class LiveMediaSessionCoordinator extends TypedEventEmitter<ILiveMediaSes
track: this._groupState.playbackTrack.current,
position: position,
});
if (this.canSendPositionUpdates) {
// User has taken initiative to send event, give priority for sending position updates
this._positionUpdateInterval.hasPriority = true;
}
}

/**
Expand Down Expand Up @@ -335,6 +393,10 @@ export class LiveMediaSessionCoordinator extends TypedEventEmitter<ILiveMediaSes
track: this._groupState.playbackTrack.current,
position: position,
});
if (this.canSendPositionUpdates) {
// User has taken initiative to send event, give priority for sending position updates
this._positionUpdateInterval.hasPriority = true;
}
}

/**
Expand Down Expand Up @@ -375,12 +437,24 @@ export class LiveMediaSessionCoordinator extends TypedEventEmitter<ILiveMediaSes
track: this._groupState.playbackTrack.current,
position: time,
});
if (this.canSendPositionUpdates) {
// User has taken initiative to send event, give priority for sending position updates
this._positionUpdateInterval.hasPriority = true;
}
} catch (err) {
await this._groupState!.syncLocalMediaSession();
throw err;
}
}

/**
* Sets the playback rate for everyone in the session.
*
* @param playbackRate the playback rate to set.
*
* @throws
* Throws an exception if the session/coordinator hasn't been initialized or {@link canSetPlaybackRate} is false.
*/
public async setPlaybackRate(playbackRate: number): Promise<void> {
LiveDataObjectNotInitializedError.assert(
"LiveMediaSessionCoordinator:setPlaybackRate",
Expand All @@ -400,6 +474,10 @@ export class LiveMediaSessionCoordinator extends TypedEventEmitter<ILiveMediaSes
{ playbackRate }
);
await this._rateChangeEvent!.sendEvent({ playbackRate });
if (this.canSendPositionUpdates) {
// User has taken initiative to send event, give priority for sending position updates
this._positionUpdateInterval.hasPriority = true;
}
}

/**
Expand Down Expand Up @@ -436,6 +514,10 @@ export class LiveMediaSessionCoordinator extends TypedEventEmitter<ILiveMediaSes
metadata: metadata,
waitPoints: waitPoints || [],
});
if (this.canSendPositionUpdates) {
// User has taken initiative to send event, give priority for sending position updates
this._positionUpdateInterval.hasPriority = true;
}
}

/**
Expand Down Expand Up @@ -470,6 +552,10 @@ export class LiveMediaSessionCoordinator extends TypedEventEmitter<ILiveMediaSes
await this._setTrackDataEvent!.sendEvent({
data: data,
});
if (this.canSendPositionUpdates) {
// User has taken initiative to send event, give priority for sending position updates
this._positionUpdateInterval.hasPriority = true;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
} from "./GroupPlaybackRate.js";
import { TypedEventEmitter } from "@fluid-internal/client-utils";
import { IGenericTypedEvents } from "./interfaces.js";
import { PriorityTimeInterval } from "./PriorityTimeInterval.js";

/**
* @hidden
Expand Down Expand Up @@ -145,7 +146,7 @@ export class GroupCoordinatorState extends TypedEventEmitter<IGenericTypedEvents
runtime: IRuntimeSignaler,
liveRuntime: LiveShareRuntime,
maxPlaybackDrift: TimeInterval,
positionUpdateInterval: TimeInterval,
positionUpdateInterval: PriorityTimeInterval,
getMediaPlayerState: () => IMediaPlayerState
) {
super();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Licensed under the Microsoft Live Share SDK License.
*/

import { TimeInterval } from "@microsoft/live-share";
import {
IRuntimeSignaler,
LiveShareRuntime,
Expand All @@ -15,6 +14,7 @@ import {
ExtendedMediaSessionPlaybackState,
} from "../MediaSessionExtensions.js";
import { GroupPlaybackRate } from "./GroupPlaybackRate.js";
import { PriorityTimeInterval } from "./PriorityTimeInterval.js";

/**
* Per client position
Expand All @@ -37,15 +37,15 @@ export class GroupPlaybackPosition {
private _playbackRate: GroupPlaybackRate;
private _runtime: IRuntimeSignaler;
private _liveRuntime: LiveShareRuntime;
private _updateInterval: TimeInterval;
private _updateInterval: PriorityTimeInterval;
private _positions: Map<string, ICurrentPlaybackPosition>;

constructor(
transportState: GroupTransportState,
playbackRate: GroupPlaybackRate,
runtime: IRuntimeSignaler,
liveRuntime: LiveShareRuntime,
updateInterval: TimeInterval
updateInterval: PriorityTimeInterval
) {
this._transportState = transportState;
this._playbackRate = playbackRate;
Expand Down Expand Up @@ -171,7 +171,7 @@ export class GroupPlaybackPosition {
) => void
): void {
const now = this._liveRuntime.getTimestamp();
const ignoreBefore = now - this._updateInterval.milliseconds * 2;
const ignoreBefore = now - this._updateInterval.maxMilliseconds * 2;
const shouldProject = !this._transportState.track.metadata?.liveStream;
this._positions.forEach((position, _) => {
// Ignore any old updates
Expand Down
76 changes: 76 additions & 0 deletions packages/live-share-media/src/internals/PriorityTimeInterval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { TimeInterval } from "@microsoft/live-share";

function getScaledPriorityTimeMs(
minMilliseconds: number,
hasPriority: boolean,
shouldPrioritize: boolean,
scaleBy: number
): number {
if (!shouldPrioritize) return minMilliseconds;
if (hasPriority) return minMilliseconds;
return minMilliseconds * scaleBy;
}

/**
* @hidden
* Time interval that can scale based on a scaling function.
*/
export class PriorityTimeInterval extends TimeInterval {
/**
* If true, local user has priority and will have the lowest possible millesecond value.
*/
public hasPriority: boolean;
/**
* If true, milliseconds will be scaled when {@link hasPriority} is false.
*/
public shouldPrioritize: boolean;
/**
* Function to get the scale ratio.
*/
private getScaleBy: () => number;
/**
* Time interval that can scale based on a scaling function.
*
* @param defaultMilliseconds the default minimum milliseconds
* @param getScaleByFn a function to scale the milliseconds by when {@link hasPriority} is false and {@link shouldPrioritize} is true.
* @param defaultHasPriority the default {@link hasPriority} value
* @param defaultShouldPrioritize the default {@link shouldPrioritize} value
*/
constructor(
defaultMilliseconds: number,
getScaleByFn: () => number,
defaultHasPriority: boolean = false,
defaultShouldPrioritize: boolean = true
) {
super(defaultMilliseconds);
this.hasPriority = defaultHasPriority;
this.shouldPrioritize = defaultShouldPrioritize;
this.getScaleBy = getScaleByFn;
}

public get milliseconds(): number {
return getScaledPriorityTimeMs(
this._milliseconds,
this.hasPriority,
this.shouldPrioritize,
this.getScaleBy()
);
}

public set milliseconds(value: number) {
this._milliseconds = value;
}

public get minMilliseconds(): number {
return this._milliseconds;
}

public get maxMilliseconds(): number {
return getScaledPriorityTimeMs(
this._milliseconds,
false,
this.shouldPrioritize,
this.getScaleBy()
);
}
}
Loading

0 comments on commit e80c59b

Please sign in to comment.