diff --git a/package.json b/package.json index 2ff2d1ed4..53f833e1e 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "electron-updater": "^4.1.2", "express": "^4.17.1", "if-env": "^1.0.4", - "keytar": "^4.4.1" + "keytar": "^4.4.1", + "videojs-event-tracking": "^1.0.1" }, "devDependencies": { "@babel/core": "^7.0.0", diff --git a/ui/analytics.js b/ui/analytics.js index 242767d9f..c7b100d62 100644 --- a/ui/analytics.js +++ b/ui/analytics.js @@ -28,6 +28,8 @@ type Analytics = { apiLogView: (string, string, string, ?number, ?() => void) => Promise, apiLogPublish: (ChannelClaim | StreamClaim) => void, tagFollowEvent: (string, boolean, string) => void, + videoStartEvent: (string, number) => void, + videoBufferEvent: (string, number) => void, emailProvidedEvent: () => void, emailVerifiedEvent: () => void, rewardEligibleEvent: () => void, @@ -127,6 +129,12 @@ const analytics: Analytics = { Lbryio.call('feedback', 'search', { query, vote }); } }, + videoStartEvent: (claimId, duration) => { + sendGaEvent('Media', 'StartDelay', claimId, duration); + }, + videoBufferEvent: (claimId, currentTime) => { + sendGaEvent('Media', 'BufferTimestamp', claimId, currentTime); + }, tagFollowEvent: (tag, following, location) => { sendGaEvent(following ? 'Tag-Follow' : 'Tag-Unfollow', tag); }, @@ -154,13 +162,14 @@ const analytics: Analytics = { }, }; -function sendGaEvent(category, action, label) { +function sendGaEvent(category, action, label, value) { if (analyticsEnabled && isProduction) { ReactGA.event( { category, action, ...(label ? { label } : {}), + ...(value ? { value } : {}), }, [SECOND_TRACKER_NAME] ); diff --git a/ui/component/viewers/videoViewer/index.js b/ui/component/viewers/videoViewer/index.js index 54c91c73e..78a38982b 100644 --- a/ui/component/viewers/videoViewer/index.js +++ b/ui/component/viewers/videoViewer/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { makeSelectFileInfoForUri, makeSelectThumbnailForUri } from 'lbry-redux'; +import { makeSelectClaimForUri, makeSelectFileInfoForUri, makeSelectThumbnailForUri } from 'lbry-redux'; import { doChangeVolume, doChangeMute } from 'redux/actions/app'; import { selectVolume, selectMute } from 'redux/selectors/app'; import { savePosition, doSetPlayingUri } from 'redux/actions/content'; @@ -12,6 +12,7 @@ const select = (state, props) => ({ muted: selectMute(state), hasFileInfo: Boolean(makeSelectFileInfoForUri(props.uri)(state)), thumbnail: makeSelectThumbnailForUri(props.uri)(state), + claim: makeSelectClaimForUri(props.uri)(state), }); const perform = dispatch => ({ diff --git a/ui/component/viewers/videoViewer/view.jsx b/ui/component/viewers/videoViewer/view.jsx index 3feb1037d..0d04783d3 100644 --- a/ui/component/viewers/videoViewer/view.jsx +++ b/ui/component/viewers/videoViewer/view.jsx @@ -3,7 +3,9 @@ import React, { useRef, useEffect, useState } from 'react'; import { stopContextMenu } from 'util/context-menu'; import videojs from 'video.js/dist/alt/video.core.novtt.min.js'; import 'video.js/dist/alt/video-js-cdn.min.css'; +import eventTracking from 'videojs-event-tracking'; import isUserTyping from 'util/detect-typing'; +import analytics from 'analytics'; const F11_KEYCODE = 122; const SPACE_BAR_KEYCODE = 32; @@ -42,10 +44,23 @@ type Props = { thumbnail: string, hasFileInfo: boolean, onEndedCB: any, + claim: Claim, }; function VideoViewer(props: Props) { - const { contentType, source, setPlayingUri, onEndedCB, changeVolume, changeMute, volume, muted, thumbnail } = props; + const { + contentType, + source, + setPlayingUri, + onEndedCB, + changeVolume, + changeMute, + volume, + muted, + thumbnail, + claim, + } = props; + const claimId = claim && claim.claim_id; const videoRef = useRef(); const isAudio = contentType.includes('audio'); let forceTypes = [ @@ -65,6 +80,10 @@ function VideoViewer(props: Props) { useEffect(() => { const currentVideo: HTMLVideoElement | null = document.querySelector('video'); + if (!Object.keys(videojs.getPlugins()).includes('eventTracking')) { + videojs.registerPlugin('eventTracking', eventTracking); + } + function doEnded() { // clear position setPlayingUri(null); @@ -105,6 +124,7 @@ function VideoViewer(props: Props) { type: forceMp4 ? 'video/mp4' : contentType, }, ], + plugins: { eventTracking: true }, }; if (isAudio) { @@ -185,6 +205,30 @@ function VideoViewer(props: Props) { // include requireRedraw here so the event listener is re-added when we need to manually remove/add the video player }, [videoRef, requireRedraw]); + // player analytics + useEffect(() => { + function doTrackingBuffered(e: Event, data: any) { + analytics.videoBufferEvent(claimId, data.currentTime); + } + function doTrackingFirstPlay(e: Event, data: any) { + analytics.videoStartEvent(claimId, data.secondsToLoad); + } + function doError(e: Event) { + console.log('ERROR', e); + } + if (player) { + player.on('tracking:buffered', (e, d) => doTrackingBuffered(e, d)); + player.on('tracking:firstplay', (e, d) => doTrackingFirstPlay(e, d)); + player.on('error', e => doError(e)); + } + return () => { + if (player) { + player.off(); + } + }; + // include requireRedraw here so the event listener is re-added when we need to manually remove/add the video player + }, [player]); + return (
{!requireRedraw && ( diff --git a/yarn.lock b/yarn.lock index 591b8d441..c29938dbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -909,6 +909,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.4.5": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.3.tgz#0811944f73a6c926bb2ad35e918dcc1bfab279f1" + integrity sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/runtime@^7.6.3": version "7.7.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.4.tgz#b23a856751e4bf099262f867767889c0e3fe175b" @@ -1214,6 +1221,19 @@ resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.14.1.tgz#0d8a53f308f017c53a5ddc3d07f4d6fa76b790d7" integrity sha512-0Ki9jAAhKDSuLDXOIMADg54Hu60SuBTEsWaJGGy5cV+SSUQ63J2a+RrYYGrErzz39fXzTibhKrAQJAb8M7PNcA== +"@videojs/http-streaming@1.10.6": + version "1.10.6" + resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-1.10.6.tgz#a9119b1828b354c5cc17b42ea051cc7bcce2dca0" + integrity sha512-uPBuunHnxWeFRYxRX0j6h1IIWv3+QKvSkZGmW9TvqxWBqeNGSrQymR6tm1nVjQ2HhMVxVphQTUhUTTPDVWqmQg== + dependencies: + aes-decrypter "3.0.0" + global "^4.3.0" + m3u8-parser "4.4.0" + mpd-parser "0.8.1" + mux.js "5.2.1" + url-toolkit "^2.1.3" + video.js "^6.8.0 || ^7.0.0" + "@videojs/http-streaming@1.9.3": version "1.9.3" resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-1.9.3.tgz#c971050495fb58d2b4c6ee0246bb03cc750635b1" @@ -7472,6 +7492,13 @@ m3u8-parser@4.3.0: dependencies: global "^4.3.2" +m3u8-parser@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.4.0.tgz#adf606c0af6d97f6750095a42006c2ae03dde177" + integrity sha512-iH2AygTFILtato+XAgnoPYzLHM4R3DjATj7Ozbk7EHdB2XoLF2oyOUguM7Kc4UVHbQHHL/QPaw98r7PbWzG0gg== + dependencies: + global "^4.3.2" + macos-release@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.2.0.tgz#ab58d55dd4714f0a05ad4b0e90f4370fef5cdea8" @@ -7909,6 +7936,14 @@ mpd-parser@0.7.0: global "^4.3.2" url-toolkit "^2.1.1" +mpd-parser@0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.8.1.tgz#db299dbec337999fbbbace989d227c7b03dc8ea7" + integrity sha512-WBTJ1bKk8OLUIxBh6s1ju1e2yz/5CzhPbgi6P3F3kJHKhGy1Z+ElvEnuzEbtC/dnbRcJtMXazE3f93N5LLdp9Q== + dependencies: + global "^4.3.2" + url-toolkit "^2.1.1" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -7947,6 +7982,11 @@ mux.js@5.1.1: resolved "https://registry.yarnpkg.com/mux.js/-/mux.js-5.1.1.tgz#0e95f048b4ac51d413c9ddc2d78e4cefad8d06de" integrity sha512-Mf/UYmh5b8jvUP+jmrTbETnyFZprMdbT0RxKm/lJ/4d2Q3xdc5GaHaRPI1zVV5D3+6uxArVPm78QEb1RsrmaQw== +mux.js@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/mux.js/-/mux.js-5.2.1.tgz#6698761fc88da5acecea0758ac25f11d3a08bee8" + integrity sha512-1t2payD3Y8izfZRq7tfUQlhL2fKzjeLr9v1/2qNCTkEQnd9Abtn1JgzsBgGZubEXh6lM5L8B0iLGoWQiukjtbQ== + nan@2.13.2: version "2.13.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7" @@ -12641,12 +12681,39 @@ vfile@^2.0.0: videojs-vtt.js "0.14.1" xhr "2.4.0" +video.js@^7.0.0: + version "7.6.6" + resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.6.6.tgz#e7c9163d53f9b0e05ccb5ac0f79d02fa49b4d3ac" + integrity sha512-AXzHwymhvMpS7c7rF29u0j0/3tSs+v2gIk5UY8OkiDHSEHL7T0+t3hid4JHW7aGvTruUUgwyf4C74cX2RDL1Pw== + dependencies: + "@babel/runtime" "^7.4.5" + "@videojs/http-streaming" "1.10.6" + global "4.3.2" + keycode "^2.2.0" + safe-json-parse "4.0.0" + videojs-font "3.2.0" + videojs-vtt.js "^0.14.1" + xhr "2.4.0" + +videojs-event-tracking@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/videojs-event-tracking/-/videojs-event-tracking-1.0.1.tgz#382e8b1293d32021f3bac65c4310ee454a659bcf" + integrity sha512-SdL4utk5U3S8PNTXFiJtlZeR4L4DbJxjK+1fv7ugLMw7v20NZbp1S0ag+9fHJCKSQrkc2QkXUqSZHXpqwkw/3Q== + dependencies: + global "^4.3.2" + video.js "^7.0.0" + videojs-font@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-3.1.0.tgz#ac33be9b517fe19299f61cccd2b3c7d75a1c6960" integrity sha512-rxB68SVgbHD+kSwoNWNCHicKJuR2ga3bGfvGxmB+8fupsiLbnyCwTBVtrZUq4bZnD64mrKP1DxHiutxwrs59pQ== -videojs-vtt.js@0.14.1: +videojs-font@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-3.2.0.tgz#212c9d3f4e4ec3fa7345167d64316add35e92232" + integrity sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA== + +videojs-vtt.js@0.14.1, videojs-vtt.js@^0.14.1: version "0.14.1" resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.14.1.tgz#da583eb1fc9c81c826a9432b706040e8dea49911" integrity sha512-YxOiywx6N9t3J5nqsE5WN2Sw4CSqVe3zV+AZm2T4syOc2buNJaD6ZoexSdeszx2sHLU/RRo2r4BJAXFDQ7Qo2Q==