mirror of
https://github.com/owncast/owncast.git
synced 2024-11-22 12:49:37 +03:00
Allow time jumps to compenstate for large latency
This commit is contained in:
parent
9fd32a9301
commit
179e7ab7ca
1 changed files with 110 additions and 22 deletions
|
@ -4,10 +4,23 @@ It will try to slowly adjust the playback rate to enable the player to get
|
|||
further into the future, with the goal of being as close to the live edge as
|
||||
possible, without causing any buffering events.
|
||||
|
||||
It will:
|
||||
How does latency occur?
|
||||
Two pieces are at play. The first being the server. The larger each segment is
|
||||
that is being generated by Owncast, the larger gap you are going to be from
|
||||
live when you begin playback.
|
||||
|
||||
Second is your media player.
|
||||
The player tries to play every segment as it comes in.
|
||||
However, your computer is not always 100% in playing things in real time, and
|
||||
there are natural stutters in playback So if one frame is delayed in playback
|
||||
you may not see it visually, but now you're one frame behind. Eventually this
|
||||
can build up and you can be many seconds behind.
|
||||
|
||||
How to help with this? The Owncast Latency Compensator will:
|
||||
- Determine the start (max) and end (min) latency values.
|
||||
- Keep an eye on download speed and stop compensating if it drops too low.
|
||||
- Limit the playback speedup so it doesn't sound weird by jumping rates.
|
||||
- Force jump to the live edge once compensation begins.
|
||||
- Dynamically calculate the speedup rate based on network speed.
|
||||
- Pause the compensation if buffering events occur.
|
||||
- Completely give up on all compensation if too many buffering events occur.
|
||||
|
@ -18,24 +31,32 @@ const MIN_BUFFER_DURATION = 500; // Min duration a buffer event must last to be
|
|||
const MAX_SPEEDUP_RATE = 1.08; // The playback rate when compensating for latency.
|
||||
const MAX_SPEEDUP_RAMP = 0.02; // The max amount we will increase the playback rate at once.
|
||||
const TIMEOUT_DURATION = 30 * 1000; // The amount of time we stop handling latency after certain events.
|
||||
const CHECK_TIMER_INTERVAL = 5 * 1000; // How often we check if we should be compensating for latency.
|
||||
const CHECK_TIMER_INTERVAL = 3 * 1000; // How often we check if we should be compensating for latency.
|
||||
const BUFFERING_AMNESTY_DURATION = 3 * 1000 * 60; // How often until a buffering event expires.
|
||||
const REQUIRED_BANDWIDTH_RATIO = 2.0; // The player:bitrate ratio required to enable compensating for latency.
|
||||
const HIGHEST_LATENCY_SEGMENT_LENGTH_MULTIPLIER = 2.5; // Segment length * this value is when we start compensating.
|
||||
const LOWEST_LATENCY_SEGMENT_LENGTH_MULTIPLIER = 1.7; // Segment length * this value is when we stop compensating.
|
||||
const MIN_LATENCY = 4 * 1000; // The lowest we'll continue compensation to be running at.
|
||||
const MAX_LATENCY = 8 * 1000; // The highest we'll allow a target latency to be before we start compensating.
|
||||
const HIGHEST_LATENCY_SEGMENT_LENGTH_MULTIPLIER = 2.6; // Segment length * this value is when we start compensating.
|
||||
const LOWEST_LATENCY_SEGMENT_LENGTH_MULTIPLIER = 1.8; // Segment length * this value is when we stop compensating.
|
||||
const MIN_LATENCY = 4 * 1000; // The absolute lowest we'll continue compensation to be running at.
|
||||
const MAX_LATENCY = 15 * 1000; // The absolute highest we'll allow a target latency to be before we start compensating.
|
||||
const MAX_JUMP_LATENCY = 7 * 1000; // How much behind the max latency we need to be behind before we allow a jump.
|
||||
const MAX_JUMP_FREQUENCY = 20 * 1000; // How often we'll allow a time jump.
|
||||
const STARTUP_WAIT_TIME = 10 * 1000; // The amount of time after we start up that we'll allow monitoring to occur.
|
||||
|
||||
class LatencyCompensator {
|
||||
constructor(player) {
|
||||
this.player = player;
|
||||
this.enabled = false;
|
||||
this.running = false;
|
||||
this.inTimeout = false;
|
||||
this.jumpingToLiveIgnoreBuffer = false;
|
||||
this.performedInitialLiveJump = false;
|
||||
this.timeoutTimer = 0;
|
||||
this.checkTimer = 0;
|
||||
this.bufferingCounter = 0;
|
||||
this.bufferingTimer = 0;
|
||||
this.playbackRate = 1.0;
|
||||
this.lastJumpOccurred = null;
|
||||
this.startupTime = new Date();
|
||||
this.player.on('playing', this.handlePlaying.bind(this));
|
||||
this.player.on('error', this.handleError.bind(this));
|
||||
this.player.on('waiting', this.handleBuffering.bind(this));
|
||||
|
@ -46,6 +67,15 @@ class LatencyCompensator {
|
|||
|
||||
// This is run on a timer to check if we should be compensating for latency.
|
||||
check() {
|
||||
if (new Date().getTime() - this.startupTime.getTime() < STARTUP_WAIT_TIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're paused then do nothing.
|
||||
if (this.player.paused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inTimeout) {
|
||||
console.log('in timeout...');
|
||||
return;
|
||||
|
@ -66,6 +96,7 @@ class LatencyCompensator {
|
|||
// Check the player buffers to make sure there's enough playable content
|
||||
// that we can safely play.
|
||||
if (tech.vhs.stats.buffered.length === 0) {
|
||||
console.log('timeout due to zero buffers');
|
||||
this.timeout();
|
||||
}
|
||||
|
||||
|
@ -126,11 +157,7 @@ class LatencyCompensator {
|
|||
1.0
|
||||
);
|
||||
|
||||
if (latency < minLatencyThreshold * 1.2) {
|
||||
// If nearing the end of our latency compensation window, then ramp down
|
||||
// the speed.
|
||||
proposedPlaybackRate = proposedPlaybackRate - 0.01;
|
||||
} else if (proposedPlaybackRate > this.playbackRate + MAX_SPEEDUP_RAMP) {
|
||||
if (proposedPlaybackRate > this.playbackRate + MAX_SPEEDUP_RAMP) {
|
||||
// If this proposed speed is substantially faster than the current rate,
|
||||
// then allow us to ramp up by using a slower value for now.
|
||||
proposedPlaybackRate = this.playbackRate + MAX_SPEEDUP_RAMP;
|
||||
|
@ -141,8 +168,28 @@ class LatencyCompensator {
|
|||
Math.round(proposedPlaybackRate * Math.pow(10, 3)) / Math.pow(10, 3);
|
||||
|
||||
if (latency > maxLatencyThreshold) {
|
||||
// If the current latency exceeds the max jump amount then
|
||||
// force jump into the future, skipping all the video in between.
|
||||
if (
|
||||
this.shouldJumpToLive() &&
|
||||
latency > maxLatencyThreshold + MAX_JUMP_LATENCY
|
||||
) {
|
||||
const seekPosition =
|
||||
this.player.currentTime() + segment.duration * 2.4;
|
||||
console.log(
|
||||
'latency',
|
||||
latency / 1000,
|
||||
'jumping to live from ',
|
||||
this.player.currentTime(),
|
||||
' to ',
|
||||
seekPosition
|
||||
);
|
||||
this.jump(seekPosition);
|
||||
}
|
||||
|
||||
// Otherwise start the playback rate adjustment.
|
||||
this.start(proposedPlaybackRate);
|
||||
} else if (latency < minLatencyThreshold) {
|
||||
} else if (latency <= minLatencyThreshold) {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
|
@ -152,10 +199,7 @@ class LatencyCompensator {
|
|||
'min',
|
||||
minLatencyThreshold / 1000,
|
||||
'max',
|
||||
maxLatencyThreshold / 1000
|
||||
);
|
||||
|
||||
console.log(
|
||||
maxLatencyThreshold / 1000,
|
||||
'playback rate',
|
||||
this.playbackRate,
|
||||
'enabled:',
|
||||
|
@ -164,14 +208,38 @@ class LatencyCompensator {
|
|||
this.running,
|
||||
'timeout: ',
|
||||
this.inTimeout,
|
||||
'buffer count:',
|
||||
'buffers: ',
|
||||
this.bufferingCounter
|
||||
);
|
||||
} catch (err) {
|
||||
// console.warn(err);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
shouldJumpToLive() {
|
||||
const now = new Date().getTime();
|
||||
const delta = now - this.lastJumpOccurred;
|
||||
return delta > MAX_JUMP_FREQUENCY;
|
||||
}
|
||||
|
||||
jump(seekPosition) {
|
||||
this.jumpingToLiveIgnoreBuffer = true;
|
||||
this.lastJumpOccurred = new Date();
|
||||
|
||||
console.log(
|
||||
'current time',
|
||||
this.player.currentTime(),
|
||||
'seeking to',
|
||||
seekPosition
|
||||
);
|
||||
this.player.currentTime(seekPosition);
|
||||
|
||||
setTimeout(() => {
|
||||
this.jumpingToLiveIgnoreBuffer = false;
|
||||
this.performedInitialLiveJump = true;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
setPlaybackRate(rate) {
|
||||
this.playbackRate = rate;
|
||||
this.player.playbackRate(rate);
|
||||
|
@ -187,8 +255,9 @@ class LatencyCompensator {
|
|||
}
|
||||
|
||||
stop() {
|
||||
if (this.running) {
|
||||
console.log('stopping latency compensator...');
|
||||
|
||||
}
|
||||
this.running = false;
|
||||
this.setPlaybackRate(1.0);
|
||||
}
|
||||
|
@ -212,6 +281,18 @@ class LatencyCompensator {
|
|||
}
|
||||
|
||||
timeout() {
|
||||
if (this.inTimeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.performedInitialLiveJump) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.jumpingToLiveIgnoreBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.inTimeout = true;
|
||||
this.stop();
|
||||
|
||||
|
@ -257,7 +338,7 @@ class LatencyCompensator {
|
|||
this.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('timeout due to buffering');
|
||||
this.timeout();
|
||||
|
||||
// Allow us to forget about old buffering events if enough time goes by.
|
||||
|
@ -273,7 +354,14 @@ class LatencyCompensator {
|
|||
return;
|
||||
}
|
||||
|
||||
this.timeout();
|
||||
if (!this.performedInitialLiveJump) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.jumpingToLiveIgnoreBuffer) {
|
||||
this.jumpingToLiveIgnoreBuffer = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.bufferingTimer = setTimeout(() => {
|
||||
this.countBufferingEvent();
|
||||
|
|
Loading…
Reference in a new issue