diff --git a/controllers/status.go b/controllers/status.go index 9b90525bc..98871f612 100644 --- a/controllers/status.go +++ b/controllers/status.go @@ -3,6 +3,7 @@ package controllers import ( "encoding/json" "net/http" + "time" "github.com/owncast/owncast/core" "github.com/owncast/owncast/router/middleware" @@ -17,6 +18,7 @@ func GetStatus(w http.ResponseWriter, r *http.Request) { response := webStatusResponse{ Online: status.Online, ViewerCount: status.ViewerCount, + ServerTime: time.Now(), LastConnectTime: status.LastConnectTime, LastDisconnectTime: status.LastDisconnectTime, VersionNumber: status.VersionNumber, @@ -32,9 +34,9 @@ func GetStatus(w http.ResponseWriter, r *http.Request) { } type webStatusResponse struct { - Online bool `json:"online"` - ViewerCount int `json:"viewerCount"` - + Online bool `json:"online"` + ViewerCount int `json:"viewerCount"` + ServerTime time.Time `json:"serverTime"` LastConnectTime *utils.NullTime `json:"lastConnectTime"` LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"` diff --git a/webroot/js/app.js b/webroot/js/app.js index 673816d56..671847b40 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -319,8 +319,12 @@ export default class App extends Component { lastConnectTime, streamTitle, lastDisconnectTime, + serverTime, } = status; + const clockSkew = new Date(serverTime).getTime() - Date.now(); + this.player.setClockSkew(clockSkew); + this.setState({ viewerCount, lastConnectTime, diff --git a/webroot/js/components/latencyCompensator.js b/webroot/js/components/latencyCompensator.js index a2d3de06d..f6dec50b8 100644 --- a/webroot/js/components/latencyCompensator.js +++ b/webroot/js/components/latencyCompensator.js @@ -58,6 +58,7 @@ class LatencyCompensator { this.playbackRate = 1.0; this.lastJumpOccurred = null; this.startupTime = new Date(); + this.clockSkewMs = 0; this.player.on('playing', this.handlePlaying.bind(this)); this.player.on('error', this.handleError.bind(this)); @@ -68,6 +69,15 @@ class LatencyCompensator { 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. check() { // 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 now = new Date().getTime(); + const now = new Date().getTime() + this.clockSkewMs; const latency = now - segmentTime; // Since the calculation of latency is based on clock times, it's possible @@ -190,12 +200,13 @@ class LatencyCompensator { latency > maxLatencyThreshold + MAX_JUMP_LATENCY ) { const jumpAmount = latency / 1000 - segment.duration * 3; - console.log('jump amount', jumpAmount); const seekPosition = this.player.currentTime() + jumpAmount; console.log( 'latency', latency / 1000, - 'jumping to live from ', + 'jumping', + jumpAmount, + 'to live from ', this.player.currentTime(), ' to ', seekPosition @@ -253,9 +264,9 @@ class LatencyCompensator { this.enabled, 'running: ', this.running, - 'timeout: ', - this.inTimeout, - 'buffers: ', + 'skew: ', + this.clockSkewMs, + 'rebuffer events: ', this.bufferingCounter ); } catch (err) { diff --git a/webroot/js/components/player.js b/webroot/js/components/player.js index 7376b1a7d..a34d46659 100644 --- a/webroot/js/components/player.js +++ b/webroot/js/components/player.js @@ -57,6 +57,8 @@ class OwncastPlayer { this.hasStartedPlayback = false; this.latencyCompensatorEnabled = false; + this.clockSkewMs = 0; + // bind all the things because safari this.startPlayer = this.startPlayer.bind(this); this.handleReady = this.handleReady.bind(this); @@ -92,6 +94,18 @@ class OwncastPlayer { 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) { const { onReady, onPlaying, onEnded, onError } = callbacks; @@ -116,6 +130,7 @@ class OwncastPlayer { setupPlaybackMetrics() { this.playbackMetrics = new PlaybackMetrics(this.vjsPlayer, videojs); + this.playbackMetrics.setClockSkew(this.clockSkewMs); } setupLatencyCompensator() { @@ -139,6 +154,7 @@ class OwncastPlayer { startLatencyCompensator() { this.latencyCompensator = new LatencyCompensator(this.vjsPlayer); + this.playbackMetrics.setClockSkew(this.clockSkewMs); this.latencyCompensator.enable(); this.latencyCompensatorEnabled = true; this.setLatencyCompensatorItemTitle('disable minimized latency'); diff --git a/webroot/js/metrics/playback.js b/webroot/js/metrics/playback.js index 2bd45c6ae..d059f94f0 100644 --- a/webroot/js/metrics/playback.js +++ b/webroot/js/metrics/playback.js @@ -7,6 +7,7 @@ class PlaybackMetrics { this.player = player; this.supportsDetailedMetrics = false; this.hasPerformedInitialVariantChange = false; + this.clockSkewMs = 0; this.segmentDownloadTime = []; this.bandwidthTracking = []; @@ -59,6 +60,12 @@ class PlaybackMetrics { }, METRICS_SEND_INTERVAL); } + // Keep our client clock in sync with the server clock to determine + // accurate latency calculations. + setClockSkew(skewMs) { + this.clockSkewMs = skewMs; + } + videoJSReady() { const tech = this.player.tech({ IWillNotUseThisInPlugins: true }); this.supportsDetailedMetrics = !!tech; @@ -173,7 +180,7 @@ class PlaybackMetrics { } const segmentTime = segment.dateTimeObject.getTime(); - const now = new Date().getTime(); + const now = new Date().getTime() + this.clockSkewMs; const latency = now - segmentTime; // Throw away values that seem invalid. @@ -237,7 +244,7 @@ class PlaybackMetrics { }; try { - fetch(URL_PLAYBACK_METRICS, options); + await fetch(URL_PLAYBACK_METRICS, options); } catch (e) { console.error(e); } @@ -249,9 +256,7 @@ export default PlaybackMetrics; function getCurrentlyPlayingSegment(tech, old_segment = null) { var target_media = tech.vhs.playlists.media(); var snapshot_time = tech.currentTime(); - var segment; - var segment_time; // Iterate trough available segments and get first within which snapshot_time is 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. - if (segment) { - segment_time = Math.max( - 0, - snapshot_time - (segment.end - segment.duration) - ); - // Because early segments don't have end property - } else { + if (!segment) { segment = target_media.segments[0]; segment_time = 0; }