Return and pass around clock skew to be used in latency calculations.
Closes #1920
This commit is contained in:
@@ -3,6 +3,7 @@ package controllers
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
"github.com/owncast/owncast/router/middleware"
|
"github.com/owncast/owncast/router/middleware"
|
||||||
@@ -17,6 +18,7 @@ func GetStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
response := webStatusResponse{
|
response := webStatusResponse{
|
||||||
Online: status.Online,
|
Online: status.Online,
|
||||||
ViewerCount: status.ViewerCount,
|
ViewerCount: status.ViewerCount,
|
||||||
|
ServerTime: time.Now(),
|
||||||
LastConnectTime: status.LastConnectTime,
|
LastConnectTime: status.LastConnectTime,
|
||||||
LastDisconnectTime: status.LastDisconnectTime,
|
LastDisconnectTime: status.LastDisconnectTime,
|
||||||
VersionNumber: status.VersionNumber,
|
VersionNumber: status.VersionNumber,
|
||||||
@@ -32,9 +34,9 @@ func GetStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type webStatusResponse struct {
|
type webStatusResponse struct {
|
||||||
Online bool `json:"online"`
|
Online bool `json:"online"`
|
||||||
ViewerCount int `json:"viewerCount"`
|
ViewerCount int `json:"viewerCount"`
|
||||||
|
ServerTime time.Time `json:"serverTime"`
|
||||||
LastConnectTime *utils.NullTime `json:"lastConnectTime"`
|
LastConnectTime *utils.NullTime `json:"lastConnectTime"`
|
||||||
LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"`
|
LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"`
|
||||||
|
|
||||||
|
|||||||
@@ -319,8 +319,12 @@ export default class App extends Component {
|
|||||||
lastConnectTime,
|
lastConnectTime,
|
||||||
streamTitle,
|
streamTitle,
|
||||||
lastDisconnectTime,
|
lastDisconnectTime,
|
||||||
|
serverTime,
|
||||||
} = status;
|
} = status;
|
||||||
|
|
||||||
|
const clockSkew = new Date(serverTime).getTime() - Date.now();
|
||||||
|
this.player.setClockSkew(clockSkew);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
viewerCount,
|
viewerCount,
|
||||||
lastConnectTime,
|
lastConnectTime,
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class LatencyCompensator {
|
|||||||
this.playbackRate = 1.0;
|
this.playbackRate = 1.0;
|
||||||
this.lastJumpOccurred = null;
|
this.lastJumpOccurred = null;
|
||||||
this.startupTime = new Date();
|
this.startupTime = new Date();
|
||||||
|
this.clockSkewMs = 0;
|
||||||
|
|
||||||
this.player.on('playing', this.handlePlaying.bind(this));
|
this.player.on('playing', this.handlePlaying.bind(this));
|
||||||
this.player.on('error', this.handleError.bind(this));
|
this.player.on('error', this.handleError.bind(this));
|
||||||
@@ -68,6 +69,15 @@ class LatencyCompensator {
|
|||||||
this.player.on('canplay', this.handlePlaying.bind(this));
|
this.player.on('canplay', this.handlePlaying.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To keep our client clock in sync with the server clock to determine
|
||||||
|
// accurate latency the clock skew should be set here to be used in
|
||||||
|
// the calculation. Otherwise if somebody's client clock is significantly
|
||||||
|
// off it will have a very incorrect latency determination and make bad
|
||||||
|
// decisions.
|
||||||
|
setClockSkew(skewMs) {
|
||||||
|
this.clockSkewMs = skewMs;
|
||||||
|
}
|
||||||
|
|
||||||
// This is run on a timer to check if we should be compensating for latency.
|
// This is run on a timer to check if we should be compensating for latency.
|
||||||
check() {
|
check() {
|
||||||
// We have an arbitrary delay at startup to allow the player to run
|
// We have an arbitrary delay at startup to allow the player to run
|
||||||
@@ -167,7 +177,7 @@ class LatencyCompensator {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const segmentTime = segment.dateTimeObject.getTime();
|
const segmentTime = segment.dateTimeObject.getTime();
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime() + this.clockSkewMs;
|
||||||
const latency = now - segmentTime;
|
const latency = now - segmentTime;
|
||||||
|
|
||||||
// Since the calculation of latency is based on clock times, it's possible
|
// Since the calculation of latency is based on clock times, it's possible
|
||||||
@@ -190,12 +200,13 @@ class LatencyCompensator {
|
|||||||
latency > maxLatencyThreshold + MAX_JUMP_LATENCY
|
latency > maxLatencyThreshold + MAX_JUMP_LATENCY
|
||||||
) {
|
) {
|
||||||
const jumpAmount = latency / 1000 - segment.duration * 3;
|
const jumpAmount = latency / 1000 - segment.duration * 3;
|
||||||
console.log('jump amount', jumpAmount);
|
|
||||||
const seekPosition = this.player.currentTime() + jumpAmount;
|
const seekPosition = this.player.currentTime() + jumpAmount;
|
||||||
console.log(
|
console.log(
|
||||||
'latency',
|
'latency',
|
||||||
latency / 1000,
|
latency / 1000,
|
||||||
'jumping to live from ',
|
'jumping',
|
||||||
|
jumpAmount,
|
||||||
|
'to live from ',
|
||||||
this.player.currentTime(),
|
this.player.currentTime(),
|
||||||
' to ',
|
' to ',
|
||||||
seekPosition
|
seekPosition
|
||||||
@@ -253,9 +264,9 @@ class LatencyCompensator {
|
|||||||
this.enabled,
|
this.enabled,
|
||||||
'running: ',
|
'running: ',
|
||||||
this.running,
|
this.running,
|
||||||
'timeout: ',
|
'skew: ',
|
||||||
this.inTimeout,
|
this.clockSkewMs,
|
||||||
'buffers: ',
|
'rebuffer events: ',
|
||||||
this.bufferingCounter
|
this.bufferingCounter
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ class OwncastPlayer {
|
|||||||
this.hasStartedPlayback = false;
|
this.hasStartedPlayback = false;
|
||||||
this.latencyCompensatorEnabled = false;
|
this.latencyCompensatorEnabled = false;
|
||||||
|
|
||||||
|
this.clockSkewMs = 0;
|
||||||
|
|
||||||
// bind all the things because safari
|
// bind all the things because safari
|
||||||
this.startPlayer = this.startPlayer.bind(this);
|
this.startPlayer = this.startPlayer.bind(this);
|
||||||
this.handleReady = this.handleReady.bind(this);
|
this.handleReady = this.handleReady.bind(this);
|
||||||
@@ -92,6 +94,18 @@ class OwncastPlayer {
|
|||||||
this.vjsPlayer.ready(this.handleReady);
|
this.vjsPlayer.ready(this.handleReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setClockSkew(skewMs) {
|
||||||
|
this.clockSkewMs = skewMs;
|
||||||
|
|
||||||
|
if (this.playbackMetrics) {
|
||||||
|
this.playbackMetrics.setClockSkew(skewMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.latencyCompensator) {
|
||||||
|
this.latencyCompensator.setClockSkew(skewMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setupPlayerCallbacks(callbacks) {
|
setupPlayerCallbacks(callbacks) {
|
||||||
const { onReady, onPlaying, onEnded, onError } = callbacks;
|
const { onReady, onPlaying, onEnded, onError } = callbacks;
|
||||||
|
|
||||||
@@ -116,6 +130,7 @@ class OwncastPlayer {
|
|||||||
|
|
||||||
setupPlaybackMetrics() {
|
setupPlaybackMetrics() {
|
||||||
this.playbackMetrics = new PlaybackMetrics(this.vjsPlayer, videojs);
|
this.playbackMetrics = new PlaybackMetrics(this.vjsPlayer, videojs);
|
||||||
|
this.playbackMetrics.setClockSkew(this.clockSkewMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupLatencyCompensator() {
|
setupLatencyCompensator() {
|
||||||
@@ -139,6 +154,7 @@ class OwncastPlayer {
|
|||||||
|
|
||||||
startLatencyCompensator() {
|
startLatencyCompensator() {
|
||||||
this.latencyCompensator = new LatencyCompensator(this.vjsPlayer);
|
this.latencyCompensator = new LatencyCompensator(this.vjsPlayer);
|
||||||
|
this.playbackMetrics.setClockSkew(this.clockSkewMs);
|
||||||
this.latencyCompensator.enable();
|
this.latencyCompensator.enable();
|
||||||
this.latencyCompensatorEnabled = true;
|
this.latencyCompensatorEnabled = true;
|
||||||
this.setLatencyCompensatorItemTitle('disable minimized latency');
|
this.setLatencyCompensatorItemTitle('disable minimized latency');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class PlaybackMetrics {
|
|||||||
this.player = player;
|
this.player = player;
|
||||||
this.supportsDetailedMetrics = false;
|
this.supportsDetailedMetrics = false;
|
||||||
this.hasPerformedInitialVariantChange = false;
|
this.hasPerformedInitialVariantChange = false;
|
||||||
|
this.clockSkewMs = 0;
|
||||||
|
|
||||||
this.segmentDownloadTime = [];
|
this.segmentDownloadTime = [];
|
||||||
this.bandwidthTracking = [];
|
this.bandwidthTracking = [];
|
||||||
@@ -59,6 +60,12 @@ class PlaybackMetrics {
|
|||||||
}, METRICS_SEND_INTERVAL);
|
}, METRICS_SEND_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep our client clock in sync with the server clock to determine
|
||||||
|
// accurate latency calculations.
|
||||||
|
setClockSkew(skewMs) {
|
||||||
|
this.clockSkewMs = skewMs;
|
||||||
|
}
|
||||||
|
|
||||||
videoJSReady() {
|
videoJSReady() {
|
||||||
const tech = this.player.tech({ IWillNotUseThisInPlugins: true });
|
const tech = this.player.tech({ IWillNotUseThisInPlugins: true });
|
||||||
this.supportsDetailedMetrics = !!tech;
|
this.supportsDetailedMetrics = !!tech;
|
||||||
@@ -173,7 +180,7 @@ class PlaybackMetrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const segmentTime = segment.dateTimeObject.getTime();
|
const segmentTime = segment.dateTimeObject.getTime();
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime() + this.clockSkewMs;
|
||||||
const latency = now - segmentTime;
|
const latency = now - segmentTime;
|
||||||
|
|
||||||
// Throw away values that seem invalid.
|
// Throw away values that seem invalid.
|
||||||
@@ -237,7 +244,7 @@ class PlaybackMetrics {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fetch(URL_PLAYBACK_METRICS, options);
|
await fetch(URL_PLAYBACK_METRICS, options);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
@@ -249,9 +256,7 @@ export default PlaybackMetrics;
|
|||||||
function getCurrentlyPlayingSegment(tech, old_segment = null) {
|
function getCurrentlyPlayingSegment(tech, old_segment = null) {
|
||||||
var target_media = tech.vhs.playlists.media();
|
var target_media = tech.vhs.playlists.media();
|
||||||
var snapshot_time = tech.currentTime();
|
var snapshot_time = tech.currentTime();
|
||||||
|
|
||||||
var segment;
|
var segment;
|
||||||
var segment_time;
|
|
||||||
|
|
||||||
// Iterate trough available segments and get first within which snapshot_time is
|
// Iterate trough available segments and get first within which snapshot_time is
|
||||||
for (var i = 0, l = target_media.segments.length; i < l; i++) {
|
for (var i = 0, l = target_media.segments.length; i < l; i++) {
|
||||||
@@ -263,13 +268,7 @@ function getCurrentlyPlayingSegment(tech, old_segment = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Null segment_time in case it's lower then 0.
|
// Null segment_time in case it's lower then 0.
|
||||||
if (segment) {
|
if (!segment) {
|
||||||
segment_time = Math.max(
|
|
||||||
0,
|
|
||||||
snapshot_time - (segment.end - segment.duration)
|
|
||||||
);
|
|
||||||
// Because early segments don't have end property
|
|
||||||
} else {
|
|
||||||
segment = target_media.segments[0];
|
segment = target_media.segments[0];
|
||||||
segment_time = 0;
|
segment_time = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user