2021-06-29 03:51:04 +02:00
|
|
|
// Created by xander on 6/21/2021
|
2021-07-01 04:12:21 +02:00
|
|
|
import videojs from 'video.js';
|
Fill in remaining Recsys fields
## Issue
6366 Recsys Evaluation Telemetry
The recommended list from lighthouse is obtained from `makeSelectRecommendedContentForUri`. This list is further tweaked by the GUI (e.g. move autoplay next item to top, remove blocked content, etc.). Recsys wants the final recommendation list and the clicked index (in exact order), so we need pass these info to the videojs recsys plugin somehow. Also, Recsys wants a recommendation list ID as well as the parent (referrer) ID, we so need to track the clicks and navigation.
## General Approach
- It seems easiest to just spew back the final (displayed) list and all the required info to Redux, and the recsys plugin (or anyone else in the future) can grab it.
- Try to touch few files as possible. The dirty work should all reside in `<RecommendedContent>` only.
## Changes
- `ClaimPreview`: add optional parameters to store an ID of the container that it is in (for this case, it is `ClaimList`) as well as the index within the container.
- When clicked, we store the container ID in the navigation history `state` object.
- For general cases, anyone can check this state from `history.location.state` to know which container referred/navigated to the current page. For the recsys use case, we can use this as the `parentUUID`.
- `ClaimList`: just relay `onClick` and set IDs.
- `RecommendedContent`: now handles the uuid generation (for both parent and child) and stores the data in Redux.
2021-07-28 14:07:49 +02:00
|
|
|
|
2021-10-17 10:36:14 +02:00
|
|
|
import RecSys from 'extras/recsys/recsys';
|
2021-06-29 03:51:04 +02:00
|
|
|
const VERSION = '0.0.1';
|
|
|
|
|
|
|
|
/* RecSys */
|
2021-09-03 00:39:40 +02:00
|
|
|
const PlayerEvent = {
|
2021-06-29 03:51:04 +02:00
|
|
|
event: {
|
2021-09-03 00:39:40 +02:00
|
|
|
start: 0, // event types
|
2021-06-29 03:51:04 +02:00
|
|
|
stop: 1,
|
|
|
|
scrub: 2,
|
|
|
|
speed: 3,
|
2022-02-05 00:38:42 +01:00
|
|
|
ended: 4,
|
2021-06-29 03:51:04 +02:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2021-09-03 00:39:40 +02:00
|
|
|
function newRecsysPlayerEvent(eventType, offset, arg) {
|
2021-06-29 03:51:04 +02:00
|
|
|
if (arg) {
|
|
|
|
return {
|
|
|
|
event: eventType,
|
|
|
|
offset: offset,
|
|
|
|
arg: arg,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
event: eventType,
|
|
|
|
offset: offset,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const defaults = {
|
|
|
|
videoId: null,
|
|
|
|
userId: 0,
|
|
|
|
debug: false,
|
2021-09-03 00:39:40 +02:00
|
|
|
embedded: false,
|
2021-06-29 03:51:04 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const Component = videojs.getComponent('Component');
|
|
|
|
const registerPlugin = videojs.registerPlugin || videojs.plugin;
|
|
|
|
|
|
|
|
class RecsysPlugin extends Component {
|
|
|
|
constructor(player, options) {
|
|
|
|
super(player, options);
|
|
|
|
|
|
|
|
if (options.debug) {
|
2021-09-03 00:39:40 +02:00
|
|
|
this.log(`Created recsys plugin for: videoId:${options.videoId}`);
|
2021-06-29 03:51:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// To help with debugging, we'll add a global vjs object with the video js player
|
|
|
|
window.vjs = player;
|
|
|
|
|
|
|
|
this.player = player;
|
|
|
|
this.lastTimeUpdate = null;
|
|
|
|
this.currentTimeUpdate = null;
|
2021-07-19 23:19:53 +02:00
|
|
|
this.inPause = false;
|
2022-05-10 12:25:09 +02:00
|
|
|
this.watchedDuration = { total: 0, lastTimestamp: -1 };
|
2021-06-29 03:51:04 +02:00
|
|
|
|
|
|
|
// Plugin event listeners
|
|
|
|
player.on('playing', (event) => this.onPlay(event));
|
|
|
|
player.on('pause', (event) => this.onPause(event));
|
|
|
|
player.on('ended', (event) => this.onEnded(event));
|
|
|
|
player.on('ratechange', (event) => this.onRateChange(event));
|
|
|
|
player.on('timeupdate', (event) => this.onTimeUpdate(event));
|
|
|
|
player.on('seeked', (event) => this.onSeeked(event));
|
|
|
|
|
|
|
|
// Event trigger to send recsys event
|
|
|
|
player.on('dispose', (event) => this.onDispose(event));
|
|
|
|
}
|
|
|
|
|
|
|
|
onPlay(event) {
|
2021-09-03 00:39:40 +02:00
|
|
|
const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.start, this.player.currentTime());
|
2021-06-29 03:51:04 +02:00
|
|
|
this.log('onPlay', recsysEvent);
|
2021-09-03 00:39:40 +02:00
|
|
|
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent, this.options_.embedded);
|
2021-07-19 23:19:53 +02:00
|
|
|
|
|
|
|
this.inPause = false;
|
|
|
|
this.lastTimeUpdate = recsysEvent.offset;
|
2021-06-29 03:51:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
onPause(event) {
|
2021-09-03 00:39:40 +02:00
|
|
|
const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.stop, this.player.currentTime());
|
2021-06-29 03:51:04 +02:00
|
|
|
this.log('onPause', recsysEvent);
|
2021-09-03 00:39:40 +02:00
|
|
|
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
2021-07-19 23:19:53 +02:00
|
|
|
|
|
|
|
this.inPause = true;
|
2021-06-29 03:51:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
onEnded(event) {
|
2022-02-05 00:38:42 +01:00
|
|
|
const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.ended, this.player.currentTime());
|
2021-06-29 03:51:04 +02:00
|
|
|
this.log('onEnded', recsysEvent);
|
2021-09-03 00:39:40 +02:00
|
|
|
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
2021-06-29 03:51:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
onRateChange(event) {
|
2021-09-03 00:39:40 +02:00
|
|
|
const recsysEvent = newRecsysPlayerEvent(
|
|
|
|
PlayerEvent.event.speed,
|
|
|
|
this.player.currentTime(),
|
|
|
|
this.player.playbackRate()
|
|
|
|
);
|
2021-06-29 03:51:04 +02:00
|
|
|
this.log('onRateChange', recsysEvent);
|
2021-09-03 00:39:40 +02:00
|
|
|
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
2021-06-29 03:51:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
onTimeUpdate(event) {
|
2021-07-19 23:19:53 +02:00
|
|
|
const nextCurrentTime = this.player.currentTime();
|
|
|
|
|
|
|
|
if (!this.inPause && Math.abs(this.lastTimeUpdate - nextCurrentTime) < 0.5) {
|
|
|
|
// Don't update lastTimeUpdate if we are in a pause segment.
|
|
|
|
//
|
|
|
|
// However, if we aren't in a pause and the time jumped
|
|
|
|
// the onTimeUpdate event probably fired before the pause and seek.
|
|
|
|
// Don't update in that case, either.
|
|
|
|
this.lastTimeUpdate = this.currentTimeUpdate;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.currentTimeUpdate = nextCurrentTime;
|
2022-05-10 12:25:09 +02:00
|
|
|
|
|
|
|
// --- Log watch duration (PR 1317) ---
|
|
|
|
// - Total playing time includes scrubbed durations, i.e. can technically be
|
|
|
|
// greater than length of the video if scrubbed back to re-watch parts.
|
|
|
|
// - Factors out playback speed, i.e. if the user watches two minutes of
|
|
|
|
// video at 2x, it's recorded as two minutes, not the browsers clock time
|
|
|
|
// of one minute.
|
|
|
|
// - Excludes jumping frames, e.g. jumping incrementally from start to end
|
|
|
|
// while paused will not contribute to the total despite user having
|
|
|
|
// "watched" the static frames.
|
|
|
|
const curTimeSec = nextCurrentTime.toFixed(0);
|
|
|
|
if (this.watchedDuration.lastTimestamp !== curTimeSec && !this.inPause) {
|
|
|
|
this.watchedDuration.total += 1;
|
|
|
|
this.watchedDuration.lastTimestamp = curTimeSec;
|
|
|
|
}
|
2021-06-29 03:51:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
onSeeked(event) {
|
2021-07-19 23:19:53 +02:00
|
|
|
const curTime = this.player.currentTime();
|
|
|
|
|
|
|
|
// There are three patterns for seeking:
|
|
|
|
//
|
|
|
|
// Assuming the video is playing,
|
|
|
|
//
|
|
|
|
// 1. Dragging the player head emits: onPause -> onSeeked -> onSeeked -> ... -> onPlay
|
|
|
|
// 2. Key press left right emits: onSeeked -> onPlay
|
|
|
|
// 3. Clicking a position emits: onPause -> onSeeked -> onPlay
|
|
|
|
//
|
|
|
|
// If the video is NOT playing,
|
|
|
|
//
|
|
|
|
// 1. Dragging the player head emits: onSeeked
|
|
|
|
// 2. Key press left right emits: onSeeked
|
|
|
|
// 3. Clicking a position emits: onSeeked
|
|
|
|
const fromTime = this.lastTimeUpdate;
|
|
|
|
|
|
|
|
if (fromTime !== curTime) {
|
|
|
|
// This removes duplicates that aren't useful.
|
2021-09-03 00:39:40 +02:00
|
|
|
const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.scrub, fromTime, curTime);
|
2021-07-19 23:19:53 +02:00
|
|
|
this.log('onSeeked', recsysEvent);
|
2021-09-03 00:39:40 +02:00
|
|
|
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
2021-07-19 23:19:53 +02:00
|
|
|
}
|
2021-06-29 03:51:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
onDispose(event) {
|
2022-01-28 22:07:57 +01:00
|
|
|
// Some browsers don't send onEnded event when closing player/browser, force it here
|
|
|
|
const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.stop, this.player.currentTime());
|
|
|
|
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
2022-05-10 12:25:09 +02:00
|
|
|
RecSys.onPlayerDispose(this.options_.videoId, this.options_.embedded, this.watchedDuration.total);
|
2021-06-29 03:51:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
log(...args) {
|
|
|
|
if (this.options_.debug) {
|
2021-09-03 00:39:40 +02:00
|
|
|
console.log(`Recsys Player Debug:`, JSON.stringify(args));
|
2021-06-29 03:51:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
videojs.registerComponent('recsys', RecsysPlugin);
|
|
|
|
|
|
|
|
const onPlayerReady = (player, options) => {
|
|
|
|
player.recsys = new RecsysPlugin(player, options);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize the plugin.
|
|
|
|
*
|
|
|
|
* @function plugin
|
|
|
|
* @param {Object} [options={}]
|
|
|
|
*/
|
Fill in remaining Recsys fields
## Issue
6366 Recsys Evaluation Telemetry
The recommended list from lighthouse is obtained from `makeSelectRecommendedContentForUri`. This list is further tweaked by the GUI (e.g. move autoplay next item to top, remove blocked content, etc.). Recsys wants the final recommendation list and the clicked index (in exact order), so we need pass these info to the videojs recsys plugin somehow. Also, Recsys wants a recommendation list ID as well as the parent (referrer) ID, we so need to track the clicks and navigation.
## General Approach
- It seems easiest to just spew back the final (displayed) list and all the required info to Redux, and the recsys plugin (or anyone else in the future) can grab it.
- Try to touch few files as possible. The dirty work should all reside in `<RecommendedContent>` only.
## Changes
- `ClaimPreview`: add optional parameters to store an ID of the container that it is in (for this case, it is `ClaimList`) as well as the index within the container.
- When clicked, we store the container ID in the navigation history `state` object.
- For general cases, anyone can check this state from `history.location.state` to know which container referred/navigated to the current page. For the recsys use case, we can use this as the `parentUUID`.
- `ClaimList`: just relay `onClick` and set IDs.
- `RecommendedContent`: now handles the uuid generation (for both parent and child) and stores the data in Redux.
2021-07-28 14:07:49 +02:00
|
|
|
const plugin = function (options) {
|
2021-06-29 03:51:04 +02:00
|
|
|
this.ready(() => {
|
|
|
|
onPlayerReady(this, videojs.mergeOptions(defaults, options));
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
plugin.VERSION = VERSION;
|
|
|
|
|
|
|
|
registerPlugin('recsys', plugin);
|
|
|
|
|
|
|
|
export default plugin;
|