Skip to content

Commit

Permalink
fix(analytics): fix watch duration by filtering seek events
Browse files Browse the repository at this point in the history
- add 'sw' event
- fix seeking issues resulting in invalid watch duration
- change ti generation logic to match the docs
- change live ti generation logic
  • Loading branch information
AntonLantukh committed Mar 30, 2023
1 parent 6d9afa8 commit 048c497
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 76 deletions.
25 changes: 13 additions & 12 deletions docs/features/video-analytics.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Video Analytics

By understanding how viewers consume content across OTT Apps, customers can create informed monetization and engagement strategies.
By understanding how viewers consume content across OTT Apps, customers can create informed monetization and engagement strategies.

Video analytics also allows JW Player to calculate recommendations and trending videos, which are critical in monetizing videos.

Expand All @@ -22,7 +22,7 @@ https://ihe.jwpltx.com/v1/clienta/ping.gif?e=<eventid>&<event-metrics-as-params-

The data is available to customers in three ways:

- [Dashboard](https://support.jwplayer.com/articles/jw-player-analytics-reference)
- [Dashboard](https://support.jwplayer.com/articles/jw-player-analytics-reference)
- [Reporting API](https://developer.jwplayer.com/jwplayer/docs/analytics-getting-started)
- Play Sessions Data Export which provides aggregated play session data and more detailed metrics in s3 buckets

Expand All @@ -36,6 +36,7 @@ The app sends the following events (param `e`) to JW platform:
| Impression | i | clienta | Yes | Fired after the first frame of an ad creative has been rendered. |
| Play | s | jwplayer6 | Yes | Fired after the first frame of a video has been rendered. |
| Quantile | t | jwplayer6 | Yes | Fired after the video plays past the threshold of a quantile. |
| Seeked | vs | jwplayer6 | Yes | Fired after the video is seeked |

## Event JS Script

Expand All @@ -53,13 +54,13 @@ For the OTT Web App the Analytics ID is stored in [`config.json`](/public/config

Each event comes with a set of data sent through the query parameters of the JWPlayer OTT Analytics endpoints:

- Session - Timestamp and unique keys
- App - The app of the viewer
- Session - Timestamp and unique keys
- App - The app of the viewer
- Device - The device of the viewer
- Media - The media that is being watched
- Quantile - How much the user watched a particular movie

Theee metrics are described below.
Theee metrics are described below.

### Session Metrics

Expand Down Expand Up @@ -106,8 +107,8 @@ Theee metrics are described below.
| Ping Spec Version | psv | No | SemVer | 1.0.0 | Version of the JW Ping Specification referenced. |
| External Player Version | epv | No | \- | 1.2.3 | Version number of external video player (not a JW player or SDK) |

<sup>1</sup> Allowed values are: `0` = web, `1` = Android, `2`= iOS, `3` = Roku, `4` = tvOS, `5` = Chromecast Receiver, `6` = FireOS. </br>
<sup>2</sup> This field was intended to identify the JW Player SDK. However, OTT reports use it to determine the device platform. We, therefore, keep using this field to indicate the OS platform for pragmatic reasons. As a result, it overlaps with field `oos`
<sup>1</sup> Allowed values are: `0` = web, `1` = Android, `2`= iOS, `3` = Roku, `4` = tvOS, `5` = Chromecast Receiver, `6` = FireOS. </br>
<sup>2</sup> This field was intended to identify the JW Player SDK. However, OTT reports use it to determine the device platform. We, therefore, keep using this field to indicate the OS platform for pragmatic reasons. As a result, it overlaps with field `oos`

### Media Metrics

Expand All @@ -121,22 +122,22 @@ Theee metrics are described below.

### Quantile Metrics

Quantiles are to track how much of a video is watched. It's fired after the video plays past the threshold of a quantile. The number of quantiles in a video depends on video length.
Quantiles are to track how much of a video is watched. It's fired after the video plays past the threshold of a quantile. The number of quantiles in a video depends on video length.

There are 3 related parameters:
There are 3 related parameters:

- Video Duration `vd`
- Video Quantiles `q`
- Quantile Watched `pw`

The table belows defines the relations and when the Quantile (`t`) event ping should be fired.

Any decimal values are truncated to an integer.
Any decimal values are truncated to an integer.

#### Example

- a video is 72 seconds long (`vd=72`)
- hence it will be split into 8 quantiles (`q=8`). Each quantile will be 72/8=9 seconds.
- a video is 72 seconds long (`vd=72`)
- hence it will be split into 8 quantiles (`q=8`). Each quantile will be 72/8=9 seconds.
- the first quantile event should fire when the 9th second of the video has been crossed. Quantile watched would have value 16 (`pw=16`).
- the second time this event should fire when the 18th second is crossed. Quantile watched its value would be 32 (`pw=32`)

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"@testing-library/react-hooks": "^8.0.1",
"@types/dompurify": "^2.3.4",
"@types/ini": "^1.3.31",
"@types/jwplayer": "^8.2.7",
"@types/jwplayer": "^8.2.13",
"@types/lodash.merge": "^4.6.6",
"@types/luxon": "^3.0.2",
"@types/marked": "^4.0.7",
Expand Down Expand Up @@ -142,4 +142,4 @@
"flat": "^5.0.1",
"json5": "^2.2.2"
}
}
}
167 changes: 126 additions & 41 deletions public/jwpltx.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,3 @@
/***
Javascript library for sending OTT analytics to JW Player.
Include in your head and include the following listeners:
jwplayer().on("ready",function(evt) {
jwpltx.ready(
CONFIG.analyticsToken, // Analytics token
window.location.hostname, // Domain name
getParam("fed"), // ID of referring feed
item.mediaid, // ID of media playing
item.title // Title of media playing
);
});
jwplayer().on("time",function(evt) {
jwpltx.time(evt.currentTime,evt.duration);
});
jwplayer().on("complete",function(evt) {
jwpltx.complete();
});
jwplayer().on("adImpression",function(evt) {
jwpltx.adImpression();
});
***/

window.jwpltx = window.jwpltx || {};

(function (o) {
Expand All @@ -37,10 +10,44 @@ window.jwpltx = window.jwpltx || {};
oosv: '5',
sdk: '0',
};

// There are anywhere between 1 to 128 quantiles in any given video, 128 is max for every video
const MAX_DURATION_IN_QUANTILES = 128;
// query params instance.
let uri;
// Current time for live streams.
let current;
// Time watched after last t event was sent
let timeWatched = 0;
// Last progress of the video stored
let lastVp = 0;
// Last quantile reached
let lastQ;
// Seeking state, gets reset after 1 sec of the latest seeked event
let isSeeking = null;
// Timeout for seeking state
let liveInterval = 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 the unites accepted by analytics service (res is 0 - 128)
function getProgressWatched(progress, duration) {
return Math.floor(MAX_DURATION_IN_QUANTILES * (progress / duration));
}

// We set interval for sending t events for live streams where we can't get progress info
function setLiveInterval() {
liveInterval = setInterval(() => {
timeWatched += 1;
}, 1000);
}

// We clear interval after complete or when seeking
function clearLiveInterval() {
clearInterval(liveInterval);
liveInterval = null;
}

// Process a player ready event
o.ready = function (aid, bun, fed, id, t) {
Expand All @@ -61,23 +68,68 @@ window.jwpltx = window.jwpltx || {};
sendData('i');
};

// Process seek event
o.seek = function (offset, duration) {
isSeeking = true;
// 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);
}
};

// 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(() => {
isSeeking = false;
// Set new timeout when seeked event reached for live events
if (uri.pw === -1 && !liveInterval) {
setLiveInterval();
}
sendData('vs');
}, 1000);
};

// When player is disconnected from the page -> we send remove event and update analytics with recent playback changes
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');
};

// Process a time tick event
o.time = function (vp, vd) {
if (isSeeking) {
return;
}

// 0 or negative vd means live stream
if (vd < 1) {
// Initial tick means play() event
if (!uri.pw) {
uri.vd = 0;
uri.q = 0;
uri.pw = -1;
uri.ti = 20;
current = vp;
sendData('s');

sendData('s');
setLiveInterval();
// monitor ticks for 20s elapsed
} else {
if (vp - current > 20) {
current = vp;
if (timeWatched > 19) {
uri.ti = timeWatched;
timeWatched = 0;
sendData('t');
}
}
Expand All @@ -98,27 +150,51 @@ window.jwpltx = window.jwpltx || {};
} else {
uri.q = 32;
}
uri.ti = Math.round(uri.vd / uri.q);

uri.ti = 0;
uri.pw = 0;
sendData('s');

// Initialize latest quantile to compare further quantiles with
lastQ = getLastVODQuantile(vp, vd, uri.q);
// Initial values to compare watched progress
lastVp = vp;

sendData('s');
// monitor ticks for entering new quantile
} else {
let pw = (Math.floor(vp / uri.ti) * 128) / uri.q;
if (pw != uri.pw) {
const pw = getProgressWatched(vp, vd);
const passedQ = getLastVODQuantile(vp, vd, uri.q);

// Total time watched since last t event.
timeWatched = timeWatched + (vp - lastVp);
lastVp = vp;

if (passedQ > lastQ) {
uri.ti = Math.floor(timeWatched);
uri.pw = pw;

sendData('t');

lastQ = passedQ;
timeWatched = 0;
}
}
}
};

// Process a video complete events
o.complete = function () {
if (uri.pw != 128) {
uri.pw = 128;
sendData('t');
// Clear intervals for live streams
if (uri.pw === -1) {
clearLiveInterval();
} else {
uri.pw = MAX_DURATION_IN_QUANTILES;
}

uri.ti = Math.floor(timeWatched);
timeWatched = 0;

sendData('t');
};

// Helper function to generate IDs
Expand Down Expand Up @@ -157,4 +233,13 @@ 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);
Loading

0 comments on commit 048c497

Please sign in to comment.