Skip to content

Commit

Permalink
Merge pull request #24 from CeeblueTV/ENG-762
Browse files Browse the repository at this point in the history
Revise ABRLinear to support bitrateConstraint and have a configurable recoverySteps
  • Loading branch information
MathieuPOUX authored Feb 6, 2025
2 parents e3d1099 + 728a5eb commit 5500d73
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 64 deletions.
52 changes: 27 additions & 25 deletions src/Streamer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,47 +143,50 @@ export class Streamer extends EventEmitter {
}

/**
* Video bitrate configured by the server
* A null value is returned if the video bitrate is undefined
* NOTE: Use {@link connectionInfos} to get the current precise audio or video bitrate
* Video bitrate configured by the server,
* can be undefined on start or when there is no controllable connector
* @note Use {@link connectionInfos} to get the current precise audio or video bitrate
*/
get videoBitrate(): number {
get videoBitrate(): number | undefined {
return this._videoBitrate;
}

/**
* Configure the video bitrate on the server side,
* Configure the video bitrate from the server,
* possible only if your {@link Streamer} instance is built with a controllable connector
* Set undefined to remove this configuration
*/
set videoBitrate(value: number) {
set videoBitrate(value: number | undefined) {
if (value === this._videoBitrate) {
return;
}
if (!this._controller) {
throw Error('Cannot set videoBitrate without start a controllable session');
}
this._videoBitrateFixed = value != null;
if (!this._videoBitrateFixed) {
if (value == null) {
this._videoBitrateFixed = false;
return;
}
if (value !== this._videoBitrate) {
// send a video bitrate command to controller only if different of current value otherwise it creates
// an infinite loop with onVideoBitrate command
this._controller.setVideoBitrate(value);
}
this._videoBitrateFixed = true;
this._videoBitrate = value;
this._controller.setVideoBitrate(value);
}

/**
* Video bitrate constraint configured by the server
* NOTE: Use {@link connectionInfos} to get the current precise audio or video bitrate
* Video bitrate constraint configured by the server,
* can be undefined on start or when there is no controllable connector
* @note Use {@link connectionInfos} to get the current precise audio or video bitrate
*/
get videoBitrateConstraint(): number {
get videoBitrateConstraint(): number | undefined {
return this._videoBitrateConstraint;
}

private _connector?: IConnector;
private _controller?: IController;
private _mediaReport?: MediaReport;
private _videoBitrate: number;
private _videoBitrateConstraint: number;
private _videoBitrateFixed?: boolean;
private _videoBitrate?: number;
private _videoBitrateConstraint?: number;
private _videoBitrateFixed: boolean;
private _rtpProps?: RTPProps;
/**
* Constructs a new Streamer instance, optionally with a custom connector
Expand All @@ -192,8 +195,7 @@ export class Streamer extends EventEmitter {
*/
constructor(private Connector?: { new (connectParams: Connect.Params, stream: MediaStream): IConnector }) {
super();
this._videoBitrate = 0;
this._videoBitrateConstraint = 0;
this._videoBitrateFixed = false;
}

/**
Expand Down Expand Up @@ -310,8 +312,8 @@ export class Streamer extends EventEmitter {
connector.close();
this._controller = undefined;
this._mediaReport = undefined;
this._videoBitrate = 0;
this._videoBitrateConstraint = 0;
this._videoBitrate = undefined;
this._videoBitrateConstraint = undefined;
this._rtpProps = undefined;
// User event (always in last)
this.onStop(error);
Expand All @@ -321,9 +323,9 @@ export class Streamer extends EventEmitter {
if (!this._controller || this._videoBitrateFixed) {
return;
}
const videoBitrate =
abr.compute(this._videoBitrate, this.videoBitrateConstraint, this.mediaReport) ?? this._videoBitrate;
const videoBitrate = abr.compute(this._videoBitrate, this.videoBitrateConstraint, this.mediaReport);
if (videoBitrate !== this._videoBitrate) {
this._videoBitrate = videoBitrate;
this._controller.setVideoBitrate(videoBitrate);
}
}
Expand Down
92 changes: 74 additions & 18 deletions src/abr/ABRAbstract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ export type ABRParams = {
* @defaultValue 3000000
*/
maximum?: number;
/**
* The `recoverySteps` parameter defines the step size used to gradually restore the bitrate
* towards the ideal bandwidth, aiming to approach the {@link ABRParams.maximum} limit.
*
* Initially set to the configured value (default: 2), this factor determines the number of steps
* taken to adapt the bitrate based on network conditions :
* - If network congestion is detected, the step count increases to avoid overshooting.
* - Once the network stabilizes, the step count decreases, returning gradually to its initial value.
*
* In essence, `recoverySteps` controls the initial speed at which the system recovers a high transfer rate.
*
* @warning Only used by ABRLinear
* @defaultValue 2
*/
recoverySteps?: number;
/**
* The `appreciationDuration` parameter defines the duration (in milliseconds) for recognizing a stable network
* condition. By default it is set to 4 seconds, which is longer than the typical GOP unit, usually set to 2 seconds.
*
* @warning Only used by ABRLinear.
* @defaultValue 4000
*/
appreciationDuration?: number;
};

/**
Expand Down Expand Up @@ -95,6 +118,34 @@ export abstract class ABRAbstract extends EventEmitter implements ABRParams {
return this._bitrateConstraint;
}

/**
* Get {@link ABRParams.recoverySteps}
*/
get recoverySteps(): number {
return this._recoverySteps;
}

/**
* Set {@link ABRParams.recoverySteps}
*/
set recoverySteps(value: number) {
this._recoverySteps = Math.max(1, value);
}

/**
* Get {@link ABRParams.appreciationDuration}
*/
get appreciationDuration(): number {
return this._appreciationDuration;
}

/**
* Set {@link ABRParams.appreciationDuration}
*/
set appreciationDuration(value: number) {
this._appreciationDuration = value;
}

/**
* Get the current bitrate
*/
Expand Down Expand Up @@ -122,6 +173,8 @@ export abstract class ABRAbstract extends EventEmitter implements ABRParams {
private _startup: number;
private _maximum: number;
private _minimum: number;
private _recoverySteps: number;
private _appreciationDuration: number;
private _stream?: MediaStream;
/**
* Build the ABR implementation, call {@link compute} to use it
Expand All @@ -134,14 +187,19 @@ export abstract class ABRAbstract extends EventEmitter implements ABRParams {
{
startup: 2000000, // Default 2Mbps
maximum: 3000000, // Default 3Mbps
minimum: 200000 // Default 200Kbps
minimum: 200000, // Default 200Kbps
recoverySteps: 2, // Default 2
appreciationDuration: 4000 // Default 4000
},
params
);
this._startup = init.startup;
this._minimum = init.minimum;
this._maximum = init.maximum;
this._appreciationDuration = init.appreciationDuration;
this._stream = stream;
this._recoverySteps = 0;
this.recoverySteps = init.recoverySteps;
}

/**
Expand All @@ -150,39 +208,37 @@ export abstract class ABRAbstract extends EventEmitter implements ABRParams {
* @param bitrate the current bitrate
* @param bitrateConstraint the current bitrate constraint
* @param mediaReport the media report structure received from the server
* @returns the wanted bitrate or undefined to not change the current bitrate
* @returns the wanted bitrate
*/
compute(bitrate: number | undefined, bitrateConstraint?: number, mediaReport?: MediaReport): number | undefined {
if (bitrate == null) {
return (this._bitrate = bitrate);
} // disabled (reset _bitrate)
const firstTime = this._bitrate == null;
compute(bitrate: number | undefined, bitrateConstraint?: number, mediaReport?: MediaReport): number {
// compute required bitrate
const newBitrate = firstTime
? this.startup
: Math.max(
this.minimum,
Math.min(this._computeBitrate(bitrate, bitrateConstraint, mediaReport), this.maximum)
);
// assign current bitrate
this._bitrate = bitrate;
this._bitrateConstraint = bitrateConstraint;
const newBitrate =
bitrate == null
? this.startup
: Math.max(
this.minimum,
Math.min(this._computeBitrate(bitrate, bitrateConstraint, mediaReport), this.maximum)
);
// log changes
if (firstTime) {
if (bitrate == null) {
this.log(`Set startup bitrate to ${newBitrate}`).info();
} else if (newBitrate > bitrate) {
this.log(`Increase bitrate ${bitrate} => ${newBitrate}`).info();
} else if (newBitrate < bitrate) {
this.log(`Decrease bitrate ${bitrate} => ${newBitrate}`).info();
}
// assign current bitrate
this._bitrate = bitrate;
this._bitrateConstraint = bitrateConstraint;
return newBitrate;
}

/**
* Reset the ABR algorithm to its initial state
*/
reset() {
this._bitrate = this._bitrateConstraint = undefined;
this._bitrate = undefined;
this._bitrateConstraint = undefined;
}

/**
Expand Down
51 changes: 31 additions & 20 deletions src/abr/ABRLinear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import { MediaReport } from '../connectors/IController';
import { Util } from '@ceeblue/web-utils';
import { ABRAbstract, ABRParams } from './ABRAbstract';

const STABLE_TIMEOUT = 11; // > 10 sec = maximum window between 2 key frames
class Vars {
stableTime: number = 0;
stableBitrate: number = 0;
attempts: number = 1;
lastLoss: number = Number.POSITIVE_INFINITY;
constructor(public recoverySteps: number) {}
}

/**
Expand All @@ -27,56 +26,68 @@ export class ABRLinear extends ABRAbstract {
*/
constructor(params: ABRParams, stream?: MediaStream) {
super(params, stream);
this._vars = new Vars();
// Set initial recoverySteps to the value configured minus 1
// to cancel the first incrementation (see ++vars.recoverySteps )
this._vars = new Vars(this.recoverySteps - 1);
}

/**
* @override
*/
reset() {
super.reset();
this._vars = new Vars();
this._vars = new Vars(this.recoverySteps - 1);
}

/**
* Compute ideal bitrate regarding current bitrate, bitrateConstraint and loss infos in mediaReport
* @override{@inheritDoc ABRAbstract._computeBitrate}
*/
protected _computeBitrate(bitrate: number, bitrateConstraint?: number, mediaReport?: MediaReport): number {
/*this.log({
bitrate,
lost:mediaReport && mediaReport.stats && mediaReport.stats.loss_perc,
attempts: this._attempts,
stableTime: this._stableTime,
stableVideoBitrate: this._stableVideoBitrate
}).info();*/
// this.log({
// bitrate,
// bitrateConstraint,
// lost: mediaReport && mediaReport.stats && mediaReport.stats.loss_perc,
// recoverySteps: this._vars.recoverySteps,
// stableTime: this._vars.stableTime,
// stableBitrate: this._vars.stableBitrate
// }).info();

const stats = mediaReport && mediaReport.stats;
const vars = this._vars;
if (stats && stats.loss_perc) {

if (bitrateConstraint && bitrate > bitrateConstraint) {
// BitrateConstraint reached!
// Decrease bitrate to listen server advisement
vars.stableTime = 0;
bitrate = bitrateConstraint;
} else if (stats && stats.loss_perc) {
// Loss few packets, decrease bitrate!
if (stats.loss_perc >= vars.lastLoss) {
// lost => decrease bitrate
// - decrease recoveryFactor
if (vars.stableTime) {
++vars.attempts;
}
vars.stableTime = 0;
bitrate = Math.round((1 - stats.loss_perc / 100) * bitrate);
}
vars.stableTime = 0;
vars.lastLoss = stats.loss_perc;
} else {
// Network OK => Search stability close to videoBitrateMax
vars.lastLoss = Number.POSITIVE_INFINITY;
// Search stability close to videoBitrateMax
const now = Util.time();
if (now >= vars.stableTime) {
if (vars.stableTime) {
// Stable => try to increase
this._updateVideoConstraints(bitrate);
bitrate += Math.ceil((this.maximum - vars.stableBitrate) / vars.attempts);
bitrate += Math.ceil((this.maximum - vars.stableBitrate) / vars.recoverySteps);
// restore recovery steps to its initial value
vars.recoverySteps = Math.max(vars.recoverySteps - 1, this.recoverySteps);
} else {
// After a congestion (or the first time)
// => store stable bitrate
// => increase recoverySteps
vars.stableBitrate = bitrate;
++vars.recoverySteps;
}
vars.stableTime = now + STABLE_TIMEOUT;
vars.stableTime = now + this.appreciationDuration;
}
}
return bitrate;
Expand Down
2 changes: 1 addition & 1 deletion src/connectors/SIPConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export abstract class SIPConnector extends EventEmitter implements IConnector {
const target = <unknown>ev.target;
if (target) {
const connectionState = (target as Record<string, unknown>)?.['connectionState'];
this.log(`Peer connection state: ${connectionState}`).debug();
this.log(`Peer connection state: ${connectionState}`).info();
switch (connectionState) {
case 'connected':
case 'connecting':
Expand Down
6 changes: 6 additions & 0 deletions src/connectors/WSController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ export class WSController extends SIPConnector implements IController {
}
case 'on_media_receive': {
this._reportReceivedTimestamp = Util.time();
if (ev.stats.loss_perc && !ev.stats.loss_num) {
// Fix an abnormal loss_perc=100% whereas loss_num=0
// happen general on start
// WIP => would be fixed on server side
ev.stats.loss_perc = 0;
}
this.onMediaReport(ev);
break;
}
Expand Down

0 comments on commit 5500d73

Please sign in to comment.