Skip to content

Commit

Permalink
feat(analytics): implement OTT video analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristiaanScheermeijer committed Jun 17, 2021
1 parent de1f4f7 commit 90bbbaa
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 1 deletion.
1 change: 1 addition & 0 deletions .commitlintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
'search',
'watchhistory',
'favorites',
'analytics',
],
],
},
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ The allowed scopes are:
- search
- watchhistory
- favorites
- analytics

### Subject

Expand Down
1 change: 1 addition & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@
}
</script>
<script type="module" src="/dist/index.js"></script>
<script type="module" src="/jwpltx.js"></script>
</body>
</html>
176 changes: 176 additions & 0 deletions public/jwpltx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/***
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) {
// Hostname for sending analytics
const URL = "https://ihe.jwpltx.com/v1/jwplayer6/ping.gif?";
// Query parameters for sending analytics
const URI = {
"pss": "1",
"oos": "Web",
"oosv": "5",
"sdk": "0"
};
// query params instance.
let uri;
// Current time for live streams.
let current;



// Process a player ready event
o.ready = function(aid, bun, fed, id, t) {
uri = JSON.parse(JSON.stringify(URI));
uri.aid = aid;
uri.bun = bun;
uri.fed = fed;
uri.id = id;
uri.t = t;

uri.emi = generateId(12);
uri.pli = generateId(12);
sendData("e");
};



// Process an ad impression event
o.adImpression = function() {
sendData("i");
};



// Process a time tick event
o.time = function(vp, vd) {
// 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");

// monitor ticks for 20s elapsed
} else {
if (vp - current > 20) {
current = vp;
sendData("t");
}
}

// positive vd means VOD stream
} else {
// Initial tick means play() event
if (!uri.vd) {
uri.vd = Math.round(vd);
if (vd < 30) {
uri.q = 1;
} else if (vd < 60) {
uri.q = 4;
} else if (vd < 180) {
uri.q = 8;
} else if (vd < 300) {
uri.q = 16;
} else {
uri.q = 32;
}
uri.ti = Math.round(uri.vd / uri.q);
uri.pw = 0;
sendData("s");

// monitor ticks for entering new quantile
} else {
let pw = Math.floor(vp / uri.ti) * 128 / uri.q;
if (pw != uri.pw) {
uri.pw = pw;
sendData("t");
}
}
}
};



// Process a video complete events
o.complete = function() {
if (uri.pw != 128) {
uri.pw = 128;
sendData("t");
}
};



// Helper function to generate IDs
function generateId(len) {
let arr = new Uint8Array((len || 40) / 2);
window.crypto.getRandomValues(arr);
return Array.from(arr, dec2hex).join('');
};

function dec2hex(dec) {
return dec.toString(16).padStart(2, "0");
};



// Serialize and send data to JW Player
function sendData(type) {
uri.e = type;
uri.sa = Date.now();
// Serialize data
let str = "";
for (let key in uri) {
if (uri[key] !== null) {
str == "" ? null : str += "&";
str += key + "=" + encodeURIComponent(uri[key]);
}
}
// Ads are sent to special bucket
let url = URL;
if (uri.e == "i") {
url = URL.replace("jwplayer6", "clienta");
}
// Send data if analytics token is set
if (uri.aid) {
navigator.sendBeacon(url + str);
} else {
console.log(url + str);
}
};



})(window.jwpltx);
5 changes: 4 additions & 1 deletion src/containers/Cinema/Cinema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useWatchHistoryListener } from '../../hooks/useWatchHistoryListener';
import { watchHistoryStore, useWatchHistory } from '../../stores/WatchHistoryStore';
import { ConfigContext } from '../../providers/ConfigProvider';
import { addScript } from '../../utils/dom';
import useOttAnalytics from '../../hooks/useOttAnalytics';

import styles from './Cinema.module.scss';

Expand All @@ -24,6 +25,7 @@ const Cinema: React.FC<Props> = ({ item, onPlay, onPause, onComplete, isTrailer
const file = item.sources?.[0]?.file;
const scriptUrl = `https://content.jwplatform.com/libraries/${config.player}.js`;
const enableWatchHistory = config.options.enableContinueWatching && !isTrailer;
const setPlayer = useOttAnalytics(item);

const getProgress = (): VideoProgress | null => {
const player = window.jwplayer && (window.jwplayer('cinema') as jwplayer.JWPlayer);
Expand All @@ -46,6 +48,7 @@ const Cinema: React.FC<Props> = ({ item, onPlay, onPause, onComplete, isTrailer
let applyWatchHistory = !!watchHistory && enableWatchHistory;

player.setup({ file, image: item.image, title: item.title, autostart: 'viewable' });
setPlayer(player);
player.on('play', () => onPlay && onPlay());
player.on('pause', () => onPause && onPause());
player.on('beforePlay', () => {
Expand All @@ -62,7 +65,7 @@ const Cinema: React.FC<Props> = ({ item, onPlay, onPause, onComplete, isTrailer
getPlayer() ? loadVideo() : addScript(scriptUrl, loadVideo);
setInitialized(true);
}
}, [item, onPlay, onPause, onComplete, config.player, file, scriptUrl, initialized, enableWatchHistory]);
}, [item, onPlay, onPause, onComplete, config.player, file, scriptUrl, initialized, enableWatchHistory, setPlayer]);

return <div className={styles.Cinema} id="cinema" />;
};
Expand Down
55 changes: 55 additions & 0 deletions src/hooks/useOttAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useContext, useEffect, useState } from 'react';

import { ConfigContext } from '../providers/ConfigProvider';
import type { PlaylistItem } from '../../types/playlist';

const useOttAnalytics = (item?: PlaylistItem) => {
const config = useContext(ConfigContext);
const [player, setPlayer] = useState<jwplayer.JWPlayer>();

useEffect(() => {
if (!window.jwpltx || !config.analyticsToken || !player || !item) {
return;
}

player.on('ready', () => {
if (!config.analyticsToken) return;

window.jwpltx.ready(
config.analyticsToken,
window.location.hostname,
item.feedid,
item.mediaid,
item.title
);
});

player.on('ready', () => {
if (!config.analyticsToken) return;

window.jwpltx.ready(
config.analyticsToken,
window.location.hostname,
item.feedid,
item.mediaid,
item.title
);
});

player.on('complete', () => {
window.jwpltx.complete();
});

player.on('time', ({ position, duration }) => {
window.jwpltx.time(position, duration);
});

player.on('adImpression', () => {
window.jwpltx.adImpression();
});
}, [player]);

return setPlayer;
};

export default useOttAnalytics;
1 change: 1 addition & 0 deletions types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ interface Window {
configLocation: configLocation;
configId: string;
jwplayer: (id: string) => unknown;
jwpltx: Jwpltx;
}
6 changes: 6 additions & 0 deletions types/jwpltx.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
interface Jwpltx {
ready: (analyticsId: string, hostname: string, feedid: string, mediaid: string, title: string) => void;
adImpression: () => void;
time: (position: number, duration: number) => void;
complete: () => void;
}

0 comments on commit 90bbbaa

Please sign in to comment.