diff --git a/ui/component/viewers/videoViewer/index.js b/ui/component/viewers/videoViewer/index.js index 52fe1db17..29def1a40 100644 --- a/ui/component/viewers/videoViewer/index.js +++ b/ui/component/viewers/videoViewer/index.js @@ -9,13 +9,14 @@ import { withRouter } from 'react-router'; import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; import { makeSelectClientSetting, selectHomepageData } from 'redux/selectors/settings'; import { toggleVideoTheaterMode, doSetClientSetting } from 'redux/actions/settings'; -import { selectUserVerifiedEmail } from 'redux/selectors/user'; +import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user'; const select = (state, props) => { const { search } = props.location; const urlParams = new URLSearchParams(search); const autoplay = urlParams.get('autoplay'); const position = urlParams.get('t') !== null ? urlParams.get('t') : makeSelectContentPositionForUri(props.uri)(state); + const userId = selectUser(state).id; return { autoplayIfEmbedded: Boolean(autoplay), @@ -29,6 +30,7 @@ const select = (state, props) => { claim: makeSelectClaimForUri(props.uri)(state), homepageData: selectHomepageData(state), authenticated: selectUserVerifiedEmail(state), + userId: userId, }; }; diff --git a/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js b/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js new file mode 100644 index 000000000..ee1ad8009 --- /dev/null +++ b/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js @@ -0,0 +1,136 @@ +// Created by xander on 6/21/2021 +import videojs from 'video.js/dist/video.min.js'; +const VERSION = '0.0.1'; + +const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view'; +const recsysId = 'lighthouse-v0'; + +/* RecSys */ +const RecsysData = { + event: { + start: 0, + stop: 1, + scrub: 2, + speed: 3, + }, +}; + +function newRecsysEvent(eventType, offset, arg) { + if (arg) { + return { + event: eventType, + offset: offset, + arg: arg, + }; + } else { + return { + event: eventType, + offset: offset, + }; + } +} + +function sendRecsysEvent(recsysEvent) { + const requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(recsysEvent), + }; + + + fetch(recsysEndpoint, requestOptions) + .then((response) => response.json()) + .then((data) => { + console.log(`Recsys response data:`, data); + }); +} + +const defaults = { + endpoint: recsysEndpoint, + recsysId: recsysId, + videoId: '0', + userId: '0', + debug: false, +}; + +const Component = videojs.getComponent('Component'); +const registerPlugin = videojs.registerPlugin || videojs.plugin; + +class RecsysPlugin extends Component { + constructor(player, options) { + super(player, options); + + // Plugin started + console.log(`created recsys plugin for: videoId:${options.videoId}, userId:${options.userId}`); + + // To help with debugging, we'll add a global vjs object with the video js player + window.vjs = player; + + this.player = player; + + // 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('seeking', (event) => this.onSeeking(event)); + } + + onPlay(event) { + const recsysEvent = newRecsysEvent(RecsysData.event.start, this.player.currentTime()); + this.log('onPlay', recsysEvent); + sendRecsysEvent(recsysEvent); + } + + onPause(event) { + const recsysEvent = newRecsysEvent(RecsysData.event.stop, this.player.currentTime()); + this.log('onPause', recsysEvent); + sendRecsysEvent(recsysEvent); + } + + onEnded(event) { + const recsysEvent = newRecsysEvent(RecsysData.event.stop, this.player.currentTime()); + this.log('onEnded', recsysEvent); + sendRecsysEvent(recsysEvent); + } + + onRateChange(event) { + const recsysEvent = newRecsysEvent(RecsysData.event.speed, this.player.currentTime()); + this.log('onRateChange', recsysEvent); + sendRecsysEvent(recsysEvent); + } + + onSeeking(event) { + const recsysEvent = newRecsysEvent(RecsysData.event.scrub, this.player.currentTime()); + this.log('onSeeking', recsysEvent); + sendRecsysEvent(recsysEvent); + } + + log(...args) { + console.log(`Recsys Debug:`, JSON.stringify(args)); + } +} + +videojs.registerComponent('recsys', RecsysPlugin); + +const onPlayerReady = (player, options) => { + player.recsys = new RecsysPlugin(player, options); +}; + +/** + * Initialize the plugin. + * + * @function plugin + * @param {Object} [options={}] + */ +const plugin = function(options) { + this.ready(() => { + onPlayerReady(this, videojs.mergeOptions(defaults, options)); + }); +}; + +plugin.VERSION = VERSION; + +registerPlugin('recsys', plugin); + +export default plugin; diff --git a/ui/component/viewers/videoViewer/internal/videojs.jsx b/ui/component/viewers/videoViewer/internal/videojs.jsx index 6df222ca6..167bd93a8 100644 --- a/ui/component/viewers/videoViewer/internal/videojs.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs.jsx @@ -9,6 +9,7 @@ import eventTracking from 'videojs-event-tracking'; import * as OVERLAY from './overlays'; import './plugins/videojs-mobile-ui/plugin'; import hlsQualitySelector from './plugins/videojs-hls-quality-selector/plugin'; +import recsys from './plugins/videojs-recsys/plugin'; import qualityLevels from 'videojs-contrib-quality-levels'; import isUserTyping from 'util/detect-typing'; @@ -50,6 +51,7 @@ type Props = { autoplay: boolean, toggleVideoTheaterMode: () => void, adUrl: ?string, + claimId: ?string, }; // type VideoJSOptions = { @@ -121,6 +123,10 @@ if (!Object.keys(videojs.getPlugins()).includes('qualityLevels')) { videojs.registerPlugin('qualityLevels', qualityLevels); } +if (!Object.keys(videojs.getPlugins()).includes('recsys')) { + videojs.registerPlugin('recsys', recsys); +} + // **************************************************************************** // LbryVolumeBarClass // **************************************************************************** @@ -180,6 +186,8 @@ export default React.memo(function VideoJs(props: Props) { onPlayerReady, toggleVideoTheaterMode, adUrl, + claimId, + userId, } = props; const [reload, setReload] = useState('initial'); @@ -578,6 +586,12 @@ export default React.memo(function VideoJs(props: Props) { displayCurrentQuality: true, }); + // Add recsys plugin + player.recsys({ + videoId: claimId, + userId: userId, + }); + // Update player source player.src({ src: finalSource, diff --git a/ui/component/viewers/videoViewer/view.jsx b/ui/component/viewers/videoViewer/view.jsx index 76158ee30..7179ac1ef 100644 --- a/ui/component/viewers/videoViewer/view.jsx +++ b/ui/component/viewers/videoViewer/view.jsx @@ -24,48 +24,6 @@ import { useHistory } from 'react-router'; const PLAY_TIMEOUT_ERROR = 'play_timeout_error'; const PLAY_TIMEOUT_LIMIT = 2000; -/* TODO: Move constants elsewhere */ -const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view'; -const recsysId = 'lighthouse-v0'; - -/* RecSys */ -const Recsys = { - event: { - start: 0, - stop: 1, - scrub: 2, - speed: 3, - }, -}; - -function newRecsysEvent(eventType, offset, arg) { - if (arg) { - return { - event: eventType, - offset: offset, - arg: arg, - }; - } else { - return { - event: eventType, - offset: offset, - }; - } -} - -function sendRecsysEvent(recsysEvent) { - const requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(recsysEvent), - }; - fetch(recsysEndpoint, requestOptions) - .then((response) => response.json()) - .then((data) => { - console.log(`Recsys response data:`, data); - }); -} - type Props = { position: number, changeVolume: (number) => void, @@ -89,6 +47,7 @@ type Props = { toggleVideoTheaterMode: () => void, setVideoPlaybackRate: (number) => void, authenticated: boolean, + userId: number, homepageData: { PRIMARY_CONTENT_CHANNEL_IDS?: Array, ENLIGHTENMENT_CHANNEL_IDS?: Array, @@ -130,6 +89,7 @@ function VideoViewer(props: Props) { setVideoPlaybackRate, homepageData, authenticated, + userId, } = props; const { PRIMARY_CONTENT_CHANNEL_IDS = [], @@ -186,14 +146,6 @@ function VideoViewer(props: Props) { }; }, [embedded, videoPlaybackRate]); - // Used to detect and send recsys events - useEffect(() => { - history.listen((location) => { - console.log(`You changed the page to: ${location.pathname}`); - // todo: recsys videoid change goes here - }); - }, [history]); - function doTrackingBuffered(e: Event, data: any) { fetch(source, { method: 'HEAD' }).then((response) => { data.playerPoweredBy = response.headers.get('x-powered-by'); @@ -249,8 +201,6 @@ function VideoViewer(props: Props) { clearPosition(uri); } else { savePosition(uri, player.currentTime()); - const rsevent = newRecsysEvent(Recsys.event.scrub, player.currentTime()); - sendRecsysEvent(rsevent); } } @@ -306,7 +256,7 @@ function VideoViewer(props: Props) { player.on('tracking:buffered', doTrackingBuffered); player.on('tracking:firstplay', doTrackingFirstPlay); player.on('ended', onEnded); - player.on('play', onPlay); + player.on('play', () => onPlay(player)); player.on('pause', () => onPause(player)); player.on('dispose', () => onDispose(player)); @@ -393,6 +343,8 @@ function VideoViewer(props: Props) { startMuted={autoplayIfEmbedded} toggleVideoTheaterMode={toggleVideoTheaterMode} autoplay={!embedded || autoplayIfEmbedded} + claimId={claimId} + userId={userId} /> )}