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,
|
||||
playerLoadedEvent: (string, ?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,
|
||||
videoBufferEvent: (
|
||||
StreamClaim,
|
||||
|
@ -75,6 +75,7 @@ type Analytics = {
|
|||
userId: string,
|
||||
playerPoweredBy: string,
|
||||
readyState: number,
|
||||
isLivestream: boolean,
|
||||
}
|
||||
) => Promise<any>,
|
||||
adsFetchedEvent: () => void,
|
||||
|
@ -133,7 +134,7 @@ function getDeviceType() {
|
|||
// variables initialized for watchman
|
||||
let amountOfBufferEvents = 0;
|
||||
let amountOfBufferTimeInMS = 0;
|
||||
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond;
|
||||
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond, isLivestream;
|
||||
let lastSentTime;
|
||||
|
||||
// calculate data for backend, send them, and reset buffer data for next interval
|
||||
|
@ -150,20 +151,27 @@ async function sendAndResetWatchmanData() {
|
|||
lastSentTime = new Date();
|
||||
|
||||
let protocol;
|
||||
if (videoType === 'application/x-mpegURL') {
|
||||
if (videoType === 'application/x-mpegURL' && !isLivestream) {
|
||||
protocol = 'hls';
|
||||
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
|
||||
// $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 {
|
||||
protocol = 'stb';
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
const objectToSend = {
|
||||
|
@ -175,8 +183,8 @@ async function sendAndResetWatchmanData() {
|
|||
protocol,
|
||||
player: playerPoweredBy,
|
||||
user_id: userId.toString(),
|
||||
position: Math.round(positionInVideo),
|
||||
rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
|
||||
position: isLivestream ? 0 : Math.round(positionInVideo),
|
||||
rel_position: isLivestream ? 0 : Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
|
||||
bitrate: bitrateAsBitsPerSecond,
|
||||
bandwidth: undefined,
|
||||
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
|
||||
|
@ -267,16 +275,26 @@ const analytics: Analytics = {
|
|||
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
|
||||
userId = passedUserId;
|
||||
claimUrl = canonicalUrl;
|
||||
playerPoweredBy = poweredBy;
|
||||
isLivestream = isLivestreamClaim;
|
||||
|
||||
videoType = passedPlayer.currentSource().type;
|
||||
videoPlayer = passedPlayer;
|
||||
bitrateAsBitsPerSecond = videoBitrate;
|
||||
sendPromMetric('time_to_start', timeToStartVideo);
|
||||
!isLivestreamClaim && sendPromMetric('time_to_start', timeToStartVideo, playerPoweredBy);
|
||||
},
|
||||
error: (message) => {
|
||||
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) {
|
||||
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();
|
||||
return fetch(url, { method: 'post' }).catch(function (error) {});
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ const VideoJsEvents = ({
|
|||
embedded,
|
||||
uri,
|
||||
doAnalyticsView,
|
||||
doAnalyticsBuffer,
|
||||
claimRewards,
|
||||
playerServerRef,
|
||||
isLivestreamClaim,
|
||||
|
@ -38,16 +39,28 @@ const VideoJsEvents = ({
|
|||
clearPosition: (string) => void,
|
||||
uri: string,
|
||||
doAnalyticsView: (string, number) => any,
|
||||
doAnalyticsBuffer: (string, any) => void,
|
||||
claimRewards: () => void,
|
||||
playerServerRef: any,
|
||||
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
|
||||
* @param e - event from videojs (from the plugin?)
|
||||
* @param data - only has secondsToLoad property
|
||||
*/
|
||||
function doTrackingFirstPlay(e: Event, data: any) {
|
||||
const playerPoweredBy = isLivestreamClaim ? 'lvs' : playerServerRef.current;
|
||||
|
||||
// how long until the video starts
|
||||
let timeToStartVideo = data.secondsToLoad;
|
||||
|
||||
|
@ -63,10 +76,6 @@ const VideoJsEvents = ({
|
|||
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
|
||||
analytics.videoStartEvent(
|
||||
claimId,
|
||||
|
@ -75,7 +84,21 @@ const VideoJsEvents = ({
|
|||
userId,
|
||||
uri,
|
||||
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);
|
||||
// custom tracking plugin, event used for watchman data, and marking view/getting rewards
|
||||
player.on('tracking:firstplay', doTrackingFirstPlay);
|
||||
// used for tracking buffering for watchman
|
||||
player.on('tracking:buffered', doTrackingBuffered);
|
||||
|
||||
// hide forcing control bar show
|
||||
player.on('canplaythrough', function () {
|
||||
setTimeout(function () {
|
||||
|
|
|
@ -94,6 +94,7 @@ type Props = {
|
|||
toggleVideoTheaterMode: () => void,
|
||||
claimRewards: () => void,
|
||||
doAnalyticsView: (string, number) => void,
|
||||
doAnalyticsBuffer: (string, any) => void,
|
||||
uri: string,
|
||||
claimValues: any,
|
||||
clearPosition: (string) => void,
|
||||
|
@ -151,6 +152,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
toggleVideoTheaterMode,
|
||||
claimValues,
|
||||
doAnalyticsView,
|
||||
doAnalyticsBuffer,
|
||||
claimRewards,
|
||||
uri,
|
||||
clearPosition,
|
||||
|
@ -203,6 +205,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
claimId,
|
||||
embedded,
|
||||
doAnalyticsView,
|
||||
doAnalyticsBuffer,
|
||||
claimRewards,
|
||||
uri,
|
||||
playerServerRef,
|
||||
|
|
|
@ -204,16 +204,6 @@ function VideoViewer(props: Props) {
|
|||
};
|
||||
}, [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(
|
||||
(playUri) => {
|
||||
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('play', onPlay);
|
||||
player.on('pause', (event) => onPause(event, player));
|
||||
|
@ -519,6 +506,7 @@ function VideoViewer(props: Props) {
|
|||
embedded={embedded}
|
||||
claimValues={claim.value}
|
||||
doAnalyticsView={doAnalyticsView}
|
||||
doAnalyticsBuffer={doAnalyticsBuffer}
|
||||
claimRewards={claimRewards}
|
||||
uri={uri}
|
||||
clearPosition={clearPosition}
|
||||
|
|
|
@ -480,22 +480,24 @@ export function doAnalyticsView(uri, timeToStart) {
|
|||
|
||||
export function doAnalyticsBuffer(uri, bufferData) {
|
||||
return (dispatch, getState) => {
|
||||
const isLivestream = bufferData.isLivestream;
|
||||
const state = getState();
|
||||
const claim = selectClaimForUri(state, uri);
|
||||
const user = selectUser(state);
|
||||
const {
|
||||
value: { video, audio, source },
|
||||
} = claim;
|
||||
const timeAtBuffer = parseInt(bufferData.currentTime * 1000);
|
||||
const timeAtBuffer = isLivestream ? 0 : parseInt(bufferData.currentTime * 1000);
|
||||
const bufferDuration = parseInt(bufferData.secondsToLoad * 1000);
|
||||
const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration);
|
||||
const fileSize = source.size; // size in bytes
|
||||
const fileSizeInBits = fileSize * 8;
|
||||
const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds);
|
||||
const fileDurationInSeconds = isLivestream ? 0 : (video && video.duration) || (audio && audio.duration);
|
||||
const fileSize = isLivestream ? 0 : source.size; // size in bytes
|
||||
const fileSizeInBits = isLivestream ? '0' : fileSize * 8;
|
||||
const bitRate = isLivestream ? bufferData.bitrateAsBitsPerSecond : parseInt(fileSizeInBits / fileDurationInSeconds);
|
||||
const userId = user && user.id.toString();
|
||||
// if there's a logged in user, send buffer event data to watchman
|
||||
if (userId) {
|
||||
analytics.videoBufferEvent(claim, {
|
||||
isLivestream,
|
||||
timeAtBuffer,
|
||||
bufferDuration,
|
||||
bitRate,
|
||||
|
|
Loading…
Reference in a new issue