From 22cfa26dae67204dfe10c69d8370034313cb5b7f Mon Sep 17 00:00:00 2001 From: Anton Lantukh Date: Mon, 3 Apr 2023 11:41:18 +0200 Subject: [PATCH] fix(analytics): fix watch duration by filtering seek events - clear timeout - renaming --- docs/features/video-analytics.md | 4 +- public/jwpltx.js | 84 +++++++++++++++++--------------- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/docs/features/video-analytics.md b/docs/features/video-analytics.md index 140e40064..6b8b57f00 100644 --- a/docs/features/video-analytics.md +++ b/docs/features/video-analytics.md @@ -44,6 +44,8 @@ The event trigger implementation for the ott web app can be found at [jwpltx.js] Note that `navigator.sendBeacon()` is used to call the endpoints. The browser will not do CORS checks on this operation. It furthermore minimizes performance impact as the browser doesn't wait for the response of the server. +It also lets us to use `beforeunload` event in order to send remaining data to analytics server when closing the window during watching in progress. + ## Analytics ID A special data parameter is the Analytics ID (`aid`). It determines to which JW Player account & property the events belong. Each property has its unique analytics ID and is provided by a JW PLayer Solution Engineer or Account manager. @@ -181,7 +183,7 @@ Any decimal values are truncated to an integer. ### Live Streams -For live streams, this quantile distribution is ignored and time ticks are sent at pre-set intervals. We recommend 20 seconds. The following values are sent: +For live streams, this quantile distribution is ignored and time ticks are sent at pre-set intervals of 20 seconds. The following values are sent: - pw (Quantile watched): -1 - q (Quantiles): 0 diff --git a/public/jwpltx.js b/public/jwpltx.js index eb4f71e06..ce9077051 100644 --- a/public/jwpltx.js +++ b/public/jwpltx.js @@ -19,21 +19,40 @@ window.jwpltx = window.jwpltx || {}; let timeWatched = 0; // Last progress of the video stored let lastVp = 0; - // Last quantile reached - let lastQ; + // Next quantile to send t event + let nextQuantile; // Seeking state, gets reset after 1 sec of the latest seeked event - let isSeeking = null; - // Timeout for seeking state + let isSeeking = false; + // Interval for Live Streams seeking state let liveInterval = null; + // Timeout for seeking state + let seekingTimeout = null; - // How many quantiles have we passed (whether we need to send new t event or not) - function getLastVODQuantile(progress, duration, numberOfQuantiles) { - return Math.floor(progress / (duration / numberOfQuantiles)); + // Here we convert seconds watched to quantiles, the units accepted by the analytics service (res is 0 - 128) + function getCurrentProgressQuantile(progress, duration) { + return Math.floor(MAX_DURATION_IN_QUANTILES * (progress / duration)); } - // Here we convert seconds watched to the unites accepted by analytics service (res is 0 - 128) - function getProgressWatched(progress, duration) { - return Math.floor(MAX_DURATION_IN_QUANTILES * (progress / duration)); + // We convert `q` metric to quantiles (0 - 128) to define next breakpoint for `t` event + function getNextTriggerQuantile(progress, duration) { + return (Math.ceil((progress / duration) * uri.q) * MAX_DURATION_IN_QUANTILES) / uri.q; + } + + // Here we convert seconds watched to quantiles, the units accepted by the analytics service (res is 0 - 128) + function sendRemainingData() { + if (uri.pw === -1) { + clearLiveInterval(); + } else { + const pw = getCurrentProgressQuantile(lastVp, uri.vd); + uri.pw = pw; + } + + if (timeWatched) { + uri.ti = Math.floor(timeWatched); + timeWatched = 0; + + sendData('t'); + } } // We set interval for sending t events for live streams where we can't get progress info @@ -71,20 +90,21 @@ window.jwpltx = window.jwpltx || {}; // Process seek event o.seek = function (offset, duration) { isSeeking = true; + clearTimeout(seekingTimeout); // Clear interval in case of a live stream not to update time watched while seeking if (uri.pw === -1) { clearLiveInterval(); } else { // We need to rewrite progress of the video when seeking to have a valid ti param lastVp = offset; - lastQ = getLastVODQuantile(offset, duration, uri.q); + nextQuantile = getNextTriggerQuantile(offset, duration); } }; // Process seeked event o.seeked = function () { // There is currently a 1 sec debounce surrounding this event in order to logically group multiple `seeked` events - window.setTimeout(() => { + seekingTimeout = setTimeout(() => { isSeeking = false; // Set new timeout when seeked event reached for live events if (uri.pw === -1 && !liveInterval) { @@ -94,21 +114,16 @@ window.jwpltx = window.jwpltx || {}; }, 1000); }; - // When player is disconnected from the page -> we send remove event and update analytics with recent playback changes + // When player is disconnected from the page -> send the rest of the data and cancel possible intervals o.remove = function () { - if (uri.pw === -1) { - clearLiveInterval(); - } else { - const pw = getProgressWatched(lastVp, uri.vd); - uri.pw = pw; - } - - uri.ti = Math.floor(timeWatched); - timeWatched = 0; - - sendData('t'); + sendRemainingData(); }; + // Send the rest of the data and cancel possible intervals in case a web page is closed while watching + window.addEventListener('beforeunload', () => { + sendRemainingData(); + }); + // Process a time tick event o.time = function (vp, vd) { if (isSeeking) { @@ -155,27 +170,27 @@ window.jwpltx = window.jwpltx || {}; uri.pw = 0; // Initialize latest quantile to compare further quantiles with - lastQ = getLastVODQuantile(vp, vd, uri.q); + nextQuantile = getNextTriggerQuantile(vp, vd); // Initial values to compare watched progress lastVp = vp; sendData('s'); // monitor ticks for entering new quantile } else { - const pw = getProgressWatched(vp, vd); - const passedQ = getLastVODQuantile(vp, vd, uri.q); + const pw = getCurrentProgressQuantile(vp, vd); + const quantile = getNextTriggerQuantile(vp, vd); // Total time watched since last t event. timeWatched = timeWatched + (vp - lastVp); lastVp = vp; - if (passedQ > lastQ) { - uri.ti = Math.floor(timeWatched); + if (pw >= nextQuantile) { + uri.ti = Math.round(timeWatched); uri.pw = pw; sendData('t'); - lastQ = passedQ; + nextQuantile = quantile; timeWatched = 0; } } @@ -233,13 +248,4 @@ window.jwpltx = window.jwpltx || {}; console.log(url + str); } } - - // Send the rest of the data and cancel possible intervals in case a web page is closed while watching - window.addEventListener('beforeunload', () => { - clearLiveInterval(); - if (timeWatched) { - uri.ti = Math.floor(timeWatched); - sendData('t'); - } - }); })(window.jwpltx);