more analytics + fixes (#1476)
more analytics + refactor - passes player with time to start (until we move this api to watchman) - supports livestream metrics for buffering - fixes bug with buffering over 10 second period - less head calls by moving to videojs-events * review fixes
This commit is contained in:
parent
be7193382c
commit
1d61d80009
5 changed files with 72 additions and 35 deletions
|
@ -63,7 +63,7 @@ type Analytics = {
|
||||||
tagFollowEvent: (string, boolean, ?string) => void,
|
tagFollowEvent: (string, boolean, ?string) => void,
|
||||||
playerLoadedEvent: (string, ?boolean) => void,
|
playerLoadedEvent: (string, ?boolean) => void,
|
||||||
playerVideoStartedEvent: (?boolean) => void,
|
playerVideoStartedEvent: (?boolean) => void,
|
||||||
videoStartEvent: (?string, number, string, ?number, string, any, ?number) => void,
|
videoStartEvent: (?string, number, string, ?number, string, any, ?number, boolean) => void,
|
||||||
videoIsPlaying: (boolean, any) => void,
|
videoIsPlaying: (boolean, any) => void,
|
||||||
videoBufferEvent: (
|
videoBufferEvent: (
|
||||||
StreamClaim,
|
StreamClaim,
|
||||||
|
@ -75,6 +75,7 @@ type Analytics = {
|
||||||
userId: string,
|
userId: string,
|
||||||
playerPoweredBy: string,
|
playerPoweredBy: string,
|
||||||
readyState: number,
|
readyState: number,
|
||||||
|
isLivestream: boolean,
|
||||||
}
|
}
|
||||||
) => Promise<any>,
|
) => Promise<any>,
|
||||||
adsFetchedEvent: () => void,
|
adsFetchedEvent: () => void,
|
||||||
|
@ -133,7 +134,7 @@ function getDeviceType() {
|
||||||
// variables initialized for watchman
|
// variables initialized for watchman
|
||||||
let amountOfBufferEvents = 0;
|
let amountOfBufferEvents = 0;
|
||||||
let amountOfBufferTimeInMS = 0;
|
let amountOfBufferTimeInMS = 0;
|
||||||
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond;
|
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond, isLivestream;
|
||||||
let lastSentTime;
|
let lastSentTime;
|
||||||
|
|
||||||
// calculate data for backend, send them, and reset buffer data for next interval
|
// calculate data for backend, send them, and reset buffer data for next interval
|
||||||
|
@ -150,20 +151,27 @@ async function sendAndResetWatchmanData() {
|
||||||
lastSentTime = new Date();
|
lastSentTime = new Date();
|
||||||
|
|
||||||
let protocol;
|
let protocol;
|
||||||
if (videoType === 'application/x-mpegURL') {
|
if (videoType === 'application/x-mpegURL' && !isLivestream) {
|
||||||
protocol = 'hls';
|
protocol = 'hls';
|
||||||
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
|
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth;
|
bitrateAsBitsPerSecond = videoPlayer.tech(true).vhs?.playlists?.media?.().attributes?.BANDWIDTH;
|
||||||
|
} else if (isLivestream) {
|
||||||
|
protocol = 'lvs';
|
||||||
|
// $FlowFixMe
|
||||||
|
bitrateAsBitsPerSecond = videoPlayer.tech(true).vhs?.playlists?.media?.().attributes?.BANDWIDTH;
|
||||||
} else {
|
} else {
|
||||||
protocol = 'stb';
|
protocol = 'stb';
|
||||||
}
|
}
|
||||||
|
|
||||||
// current position in video in MS
|
// current position in video in MS
|
||||||
const positionInVideo = videoPlayer && Math.round(videoPlayer.currentTime()) * 1000;
|
const positionInVideo = isLivestream ? 0 : videoPlayer && Math.round(videoPlayer.currentTime()) * 1000;
|
||||||
|
|
||||||
// get the duration marking the time in the video for relative position calculation
|
// get the duration marking the time in the video for relative position calculation
|
||||||
const totalDurationInSeconds = videoPlayer && Math.round(videoPlayer.duration());
|
const totalDurationInSeconds = isLivestream ? 0 : videoPlayer && Math.round(videoPlayer.duration());
|
||||||
|
|
||||||
|
// temp: if buffering over the interval, the duration doesn't reset since we don't get an event
|
||||||
|
if (amountOfBufferTimeInMS > timeSinceLastIntervalSend) amountOfBufferTimeInMS = timeSinceLastIntervalSend;
|
||||||
|
|
||||||
// build object for watchman backend
|
// build object for watchman backend
|
||||||
const objectToSend = {
|
const objectToSend = {
|
||||||
|
@ -175,8 +183,8 @@ async function sendAndResetWatchmanData() {
|
||||||
protocol,
|
protocol,
|
||||||
player: playerPoweredBy,
|
player: playerPoweredBy,
|
||||||
user_id: userId.toString(),
|
user_id: userId.toString(),
|
||||||
position: Math.round(positionInVideo),
|
position: isLivestream ? 0 : Math.round(positionInVideo),
|
||||||
rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
|
rel_position: isLivestream ? 0 : Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
|
||||||
bitrate: bitrateAsBitsPerSecond,
|
bitrate: bitrateAsBitsPerSecond,
|
||||||
bandwidth: undefined,
|
bandwidth: undefined,
|
||||||
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
|
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
|
||||||
|
@ -267,16 +275,26 @@ const analytics: Analytics = {
|
||||||
startWatchmanIntervalIfNotRunning();
|
startWatchmanIntervalIfNotRunning();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
|
videoStartEvent: (
|
||||||
|
claimId,
|
||||||
|
timeToStartVideo,
|
||||||
|
poweredBy,
|
||||||
|
passedUserId,
|
||||||
|
canonicalUrl,
|
||||||
|
passedPlayer,
|
||||||
|
videoBitrate,
|
||||||
|
isLivestreamClaim
|
||||||
|
) => {
|
||||||
// populate values for watchman when video starts
|
// populate values for watchman when video starts
|
||||||
userId = passedUserId;
|
userId = passedUserId;
|
||||||
claimUrl = canonicalUrl;
|
claimUrl = canonicalUrl;
|
||||||
playerPoweredBy = poweredBy;
|
playerPoweredBy = poweredBy;
|
||||||
|
isLivestream = isLivestreamClaim;
|
||||||
|
|
||||||
videoType = passedPlayer.currentSource().type;
|
videoType = passedPlayer.currentSource().type;
|
||||||
videoPlayer = passedPlayer;
|
videoPlayer = passedPlayer;
|
||||||
bitrateAsBitsPerSecond = videoBitrate;
|
bitrateAsBitsPerSecond = videoBitrate;
|
||||||
sendPromMetric('time_to_start', timeToStartVideo);
|
!isLivestreamClaim && sendPromMetric('time_to_start', timeToStartVideo, playerPoweredBy);
|
||||||
},
|
},
|
||||||
error: (message) => {
|
error: (message) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
@ -469,10 +487,10 @@ function sendGaEvent(event: string, params?: { [string]: string | number }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendPromMetric(name: string, value?: number) {
|
function sendPromMetric(name: string, value?: number, player: string) {
|
||||||
if (IS_WEB) {
|
if (IS_WEB) {
|
||||||
let url = new URL(SDK_API_PATH + '/metric/ui');
|
let url = new URL(SDK_API_PATH + '/metric/ui');
|
||||||
const params = { name: name, value: value ? value.toString() : '' };
|
const params = { name: name, value: value ? value.toString() : '', player: player };
|
||||||
url.search = new URLSearchParams(params).toString();
|
url.search = new URLSearchParams(params).toString();
|
||||||
return fetch(url, { method: 'post' }).catch(function (error) {});
|
return fetch(url, { method: 'post' }).catch(function (error) {});
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ const VideoJsEvents = ({
|
||||||
embedded,
|
embedded,
|
||||||
uri,
|
uri,
|
||||||
doAnalyticsView,
|
doAnalyticsView,
|
||||||
|
doAnalyticsBuffer,
|
||||||
claimRewards,
|
claimRewards,
|
||||||
playerServerRef,
|
playerServerRef,
|
||||||
isLivestreamClaim,
|
isLivestreamClaim,
|
||||||
|
@ -38,16 +39,28 @@ const VideoJsEvents = ({
|
||||||
clearPosition: (string) => void,
|
clearPosition: (string) => void,
|
||||||
uri: string,
|
uri: string,
|
||||||
doAnalyticsView: (string, number) => any,
|
doAnalyticsView: (string, number) => any,
|
||||||
|
doAnalyticsBuffer: (string, any) => void,
|
||||||
claimRewards: () => void,
|
claimRewards: () => void,
|
||||||
playerServerRef: any,
|
playerServerRef: any,
|
||||||
isLivestreamClaim: boolean,
|
isLivestreamClaim: boolean,
|
||||||
}) => {
|
}) => {
|
||||||
|
function doTrackingBuffered(e: Event, data: any) {
|
||||||
|
const playerPoweredBy = isLivestreamClaim ? 'lvs' : playerServerRef.current;
|
||||||
|
|
||||||
|
data.playPoweredBy = playerPoweredBy;
|
||||||
|
data.isLivestream = isLivestreamClaim;
|
||||||
|
// $FlowFixMe
|
||||||
|
data.bitrateAsBitsPerSecond = this.tech(true).vhs?.playlists?.media?.().attributes?.BANDWIDTH;
|
||||||
|
doAnalyticsBuffer(uri, data);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Analytics functionality that is run on first video start
|
* Analytics functionality that is run on first video start
|
||||||
* @param e - event from videojs (from the plugin?)
|
* @param e - event from videojs (from the plugin?)
|
||||||
* @param data - only has secondsToLoad property
|
* @param data - only has secondsToLoad property
|
||||||
*/
|
*/
|
||||||
function doTrackingFirstPlay(e: Event, data: any) {
|
function doTrackingFirstPlay(e: Event, data: any) {
|
||||||
|
const playerPoweredBy = isLivestreamClaim ? 'lvs' : playerServerRef.current;
|
||||||
|
|
||||||
// how long until the video starts
|
// how long until the video starts
|
||||||
let timeToStartVideo = data.secondsToLoad;
|
let timeToStartVideo = data.secondsToLoad;
|
||||||
|
|
||||||
|
@ -63,10 +76,6 @@ const VideoJsEvents = ({
|
||||||
bitrateAsBitsPerSecond = Math.round(contentInBits / durationInSeconds);
|
bitrateAsBitsPerSecond = Math.round(contentInBits / durationInSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// figure out what server the video is served from and then run start analytic event
|
|
||||||
// server string such as 'eu-p6'
|
|
||||||
const playerPoweredBy = playerServerRef.current;
|
|
||||||
|
|
||||||
// populates data for watchman, sends prom and matomo event
|
// populates data for watchman, sends prom and matomo event
|
||||||
analytics.videoStartEvent(
|
analytics.videoStartEvent(
|
||||||
claimId,
|
claimId,
|
||||||
|
@ -75,7 +84,21 @@ const VideoJsEvents = ({
|
||||||
userId,
|
userId,
|
||||||
uri,
|
uri,
|
||||||
this, // pass the player
|
this, // pass the player
|
||||||
bitrateAsBitsPerSecond
|
bitrateAsBitsPerSecond,
|
||||||
|
isLivestreamClaim
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// populates data for watchman, sends prom and matomo event
|
||||||
|
analytics.videoStartEvent(
|
||||||
|
claimId,
|
||||||
|
0,
|
||||||
|
playerPoweredBy,
|
||||||
|
userId,
|
||||||
|
uri,
|
||||||
|
this, // pass the player
|
||||||
|
// $FlowFixMe
|
||||||
|
this.tech(true).vhs?.playlists?.media?.().attributes?.BANDWIDTH,
|
||||||
|
isLivestreamClaim
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,6 +214,9 @@ const VideoJsEvents = ({
|
||||||
player.on('error', onError);
|
player.on('error', onError);
|
||||||
// custom tracking plugin, event used for watchman data, and marking view/getting rewards
|
// custom tracking plugin, event used for watchman data, and marking view/getting rewards
|
||||||
player.on('tracking:firstplay', doTrackingFirstPlay);
|
player.on('tracking:firstplay', doTrackingFirstPlay);
|
||||||
|
// used for tracking buffering for watchman
|
||||||
|
player.on('tracking:buffered', doTrackingBuffered);
|
||||||
|
|
||||||
// hide forcing control bar show
|
// hide forcing control bar show
|
||||||
player.on('canplaythrough', function () {
|
player.on('canplaythrough', function () {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
|
|
|
@ -94,6 +94,7 @@ type Props = {
|
||||||
toggleVideoTheaterMode: () => void,
|
toggleVideoTheaterMode: () => void,
|
||||||
claimRewards: () => void,
|
claimRewards: () => void,
|
||||||
doAnalyticsView: (string, number) => void,
|
doAnalyticsView: (string, number) => void,
|
||||||
|
doAnalyticsBuffer: (string, any) => void,
|
||||||
uri: string,
|
uri: string,
|
||||||
claimValues: any,
|
claimValues: any,
|
||||||
clearPosition: (string) => void,
|
clearPosition: (string) => void,
|
||||||
|
@ -151,6 +152,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
toggleVideoTheaterMode,
|
toggleVideoTheaterMode,
|
||||||
claimValues,
|
claimValues,
|
||||||
doAnalyticsView,
|
doAnalyticsView,
|
||||||
|
doAnalyticsBuffer,
|
||||||
claimRewards,
|
claimRewards,
|
||||||
uri,
|
uri,
|
||||||
clearPosition,
|
clearPosition,
|
||||||
|
@ -203,6 +205,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
claimId,
|
claimId,
|
||||||
embedded,
|
embedded,
|
||||||
doAnalyticsView,
|
doAnalyticsView,
|
||||||
|
doAnalyticsBuffer,
|
||||||
claimRewards,
|
claimRewards,
|
||||||
uri,
|
uri,
|
||||||
playerServerRef,
|
playerServerRef,
|
||||||
|
|
|
@ -204,16 +204,6 @@ function VideoViewer(props: Props) {
|
||||||
};
|
};
|
||||||
}, [embedded, videoPlaybackRate]);
|
}, [embedded, videoPlaybackRate]);
|
||||||
|
|
||||||
// TODO: analytics functionality
|
|
||||||
function doTrackingBuffered(e: Event, data: any) {
|
|
||||||
if (!isLivestreamClaim) {
|
|
||||||
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
|
|
||||||
data.playerPoweredBy = response.headers.get('x-powered-by');
|
|
||||||
doAnalyticsBuffer(uri, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const doPlay = useCallback(
|
const doPlay = useCallback(
|
||||||
(playUri) => {
|
(playUri) => {
|
||||||
setDoNavigate(false);
|
setDoNavigate(false);
|
||||||
|
@ -409,9 +399,6 @@ function VideoViewer(props: Props) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// used for tracking buffering for watchman
|
|
||||||
player.on('tracking:buffered', doTrackingBuffered);
|
|
||||||
|
|
||||||
player.on('ended', () => setEnded(true));
|
player.on('ended', () => setEnded(true));
|
||||||
player.on('play', onPlay);
|
player.on('play', onPlay);
|
||||||
player.on('pause', (event) => onPause(event, player));
|
player.on('pause', (event) => onPause(event, player));
|
||||||
|
@ -519,6 +506,7 @@ function VideoViewer(props: Props) {
|
||||||
embedded={embedded}
|
embedded={embedded}
|
||||||
claimValues={claim.value}
|
claimValues={claim.value}
|
||||||
doAnalyticsView={doAnalyticsView}
|
doAnalyticsView={doAnalyticsView}
|
||||||
|
doAnalyticsBuffer={doAnalyticsBuffer}
|
||||||
claimRewards={claimRewards}
|
claimRewards={claimRewards}
|
||||||
uri={uri}
|
uri={uri}
|
||||||
clearPosition={clearPosition}
|
clearPosition={clearPosition}
|
||||||
|
|
|
@ -480,22 +480,24 @@ export function doAnalyticsView(uri, timeToStart) {
|
||||||
|
|
||||||
export function doAnalyticsBuffer(uri, bufferData) {
|
export function doAnalyticsBuffer(uri, bufferData) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
|
const isLivestream = bufferData.isLivestream;
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const claim = selectClaimForUri(state, uri);
|
const claim = selectClaimForUri(state, uri);
|
||||||
const user = selectUser(state);
|
const user = selectUser(state);
|
||||||
const {
|
const {
|
||||||
value: { video, audio, source },
|
value: { video, audio, source },
|
||||||
} = claim;
|
} = claim;
|
||||||
const timeAtBuffer = parseInt(bufferData.currentTime * 1000);
|
const timeAtBuffer = isLivestream ? 0 : parseInt(bufferData.currentTime * 1000);
|
||||||
const bufferDuration = parseInt(bufferData.secondsToLoad * 1000);
|
const bufferDuration = parseInt(bufferData.secondsToLoad * 1000);
|
||||||
const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration);
|
const fileDurationInSeconds = isLivestream ? 0 : (video && video.duration) || (audio && audio.duration);
|
||||||
const fileSize = source.size; // size in bytes
|
const fileSize = isLivestream ? 0 : source.size; // size in bytes
|
||||||
const fileSizeInBits = fileSize * 8;
|
const fileSizeInBits = isLivestream ? '0' : fileSize * 8;
|
||||||
const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds);
|
const bitRate = isLivestream ? bufferData.bitrateAsBitsPerSecond : parseInt(fileSizeInBits / fileDurationInSeconds);
|
||||||
const userId = user && user.id.toString();
|
const userId = user && user.id.toString();
|
||||||
// if there's a logged in user, send buffer event data to watchman
|
// if there's a logged in user, send buffer event data to watchman
|
||||||
if (userId) {
|
if (userId) {
|
||||||
analytics.videoBufferEvent(claim, {
|
analytics.videoBufferEvent(claim, {
|
||||||
|
isLivestream,
|
||||||
timeAtBuffer,
|
timeAtBuffer,
|
||||||
bufferDuration,
|
bufferDuration,
|
||||||
bitRate,
|
bitRate,
|
||||||
|
|
Loading…
Reference in a new issue