diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index 17f77245df6..895aa359826 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -2543,6 +2543,10 @@ const onMediaRequest = (state, xhr, type, tabId) => { const parsed = ledgerUtil.getMediaData(xhr, type) const mediaId = ledgerUtil.getMediaId(parsed, type) + if (clientOptions.loggingP) { + console.log('Media request', parsed, `Media id: ${mediaId}`) + } + if (mediaId == null) { return state } @@ -2562,6 +2566,9 @@ const onMediaRequest = (state, xhr, type, tabId) => { if (!ledgerPublisher) { ledgerPublisher = require('bat-publisher') } + if (clientOptions.loggingP) { + console.log('LOGGED EVENT', parsed, `Media id: ${mediaId}`, `Media key: ${mediaKey}`, `Duration: ${duration}ms (${duration / 1000}s)`) + } let revisited = true const activeTabId = tabState.getActiveTabId(state) @@ -2570,8 +2577,11 @@ const onMediaRequest = (state, xhr, type, tabId) => { currentMediaKey = mediaKey } - const stateData = ledgerUtil.generateMediaCacheData(parsed, type) + const stateData = ledgerUtil.generateMediaCacheData(state, parsed, type, mediaKey) const cache = ledgerVideoCache.getDataByVideoId(state, mediaKey) + if (clientOptions.loggingP) { + console.log('Media cache data: ', stateData.toJS()) + } if (!cache.isEmpty()) { if (!stateData.isEmpty()) { diff --git a/app/common/constants/twitchEvents.js b/app/common/constants/twitchEvents.js index 6d94e753264..a73ba043c2c 100644 --- a/app/common/constants/twitchEvents.js +++ b/app/common/constants/twitchEvents.js @@ -3,10 +3,15 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const events = { + BUFFER_EMPTY: 'buffer-empty', + BUFFER_REFILL: 'buffer-refill', MINUTE_WATCHED: 'minute-watched', - START: 'video-play', + PLAY_MANIFEST: 'video_play_master_manifest', PLAY_PAUSE: 'player_click_playpause', - SEEK: 'vod_seek' + SEEK: 'player_click_vod_seek', + START: 'video-play', + END: 'video_end', + VIDEO_ERROR: 'video_error' } module.exports = events diff --git a/app/common/lib/ledgerUtil.js b/app/common/lib/ledgerUtil.js index a38c6553900..cd576297106 100644 --- a/app/common/lib/ledgerUtil.js +++ b/app/common/lib/ledgerUtil.js @@ -227,12 +227,7 @@ const getMediaId = (data, type) => { case ledgerMediaProviders.TWITCH: { if ( - ([ - twitchEvents.MINUTE_WATCHED, - twitchEvents.START, - twitchEvents.PLAY_PAUSE, - twitchEvents.SEEK - ].includes(data.event)) && + Object.values(twitchEvents).includes(data.event) && data.properties ) { id = data.properties.channel @@ -327,7 +322,7 @@ const getMediaDuration = (state, data, mediaKey, type) => { return duration } -const generateMediaCacheData = (parsed, type) => { +const generateMediaCacheData = (state, parsed, type, mediaKey) => { let data = Immutable.Map() if (parsed == null) { @@ -337,7 +332,7 @@ const generateMediaCacheData = (parsed, type) => { switch (type) { case ledgerMediaProviders.TWITCH: { - data = generateTwitchCacheData(parsed) + data = generateTwitchCacheData(state, parsed, mediaKey) break } } @@ -345,20 +340,57 @@ const generateMediaCacheData = (parsed, type) => { return data } -const generateTwitchCacheData = (parsed) => { +const generateTwitchCacheData = (state, parsed, mediaKey) => { if (parsed == null) { return Immutable.Map() } + const statusConst = { + playing: 'playing', + paused: 'paused' + } + + const previousData = ledgerVideoCache.getDataByVideoId(state, mediaKey) + let status = statusConst.playing + + if ( + ( + parsed.event === twitchEvents.PLAY_PAUSE && + previousData.get('event') !== twitchEvents.PLAY_PAUSE + ) || // user clicked pause (we need to exclude seeking while paused) + ( + parsed.event === twitchEvents.PLAY_PAUSE && + previousData.get('event') === twitchEvents.PLAY_PAUSE && + previousData.get('status') === statusConst.playing + ) || // user clicked pause as soon as he clicked played + ( + parsed.event === twitchEvents.SEEK && + previousData.get('status') === statusConst.paused + ) // seeking video while it is paused + ) { + status = statusConst.paused + } + + // User pauses a video, then seek it and play it again + if ( + parsed.event === twitchEvents.PLAY_PAUSE && + previousData.get('event') === twitchEvents.SEEK && + previousData.get('status') === statusConst.paused + ) { + status = statusConst.playing + } + if (parsed.properties) { return Immutable.fromJS({ event: parsed.event, - time: parsed.properties.time + time: parsed.properties.time, + status }) } return Immutable.fromJS({ - event: parsed.event + event: parsed.event, + status }) } @@ -393,31 +425,42 @@ const getTwitchDuration = (state, data, mediaKey) => { } const previousData = ledgerVideoCache.getDataByVideoId(state, mediaKey) + const oldEvent = previousData.get('event') const twitchMinimumSeconds = 10 - if (previousData.isEmpty() && data.event === twitchEvents.START) { + if (data.event === twitchEvents.START && oldEvent === twitchEvents.START) { + return 0 + } + + if (data.event === twitchEvents.START) { return twitchMinimumSeconds * milliseconds.second } - const oldMedia = ledgerVideoCache.getDataByVideoId(state, mediaKey) let time = 0 const currentTime = parseFloat(data.properties.time) const oldTime = parseFloat(previousData.get('time')) + const currentEvent = data.event - if ( - data.event === twitchEvents.PLAY_PAUSE && - oldMedia.get('event') !== twitchEvents.PLAY_PAUSE - ) { - // User paused a video - time = currentTime - oldTime - } else if (previousData.get('event') === twitchEvents.START) { + if (oldEvent === twitchEvents.START) { // From video play event to x event time = currentTime - oldTime - twitchMinimumSeconds - } else if (data.event === twitchEvents.MINUTE_WATCHED) { - // Minute watched event - time = currentTime - oldTime - } else if (data.event === twitchEvents.SEEK && oldMedia.get('event') !== twitchEvents.PLAY_PAUSE) { - // Vod seek event + } else if ( + currentEvent === twitchEvents.MINUTE_WATCHED || // Minute watched + currentEvent === twitchEvents.BUFFER_EMPTY || // Run out of buffer + currentEvent === twitchEvents.VIDEO_ERROR || // Video has some problems + currentEvent === twitchEvents.END || // Video ended + (currentEvent === twitchEvents.SEEK && previousData.get('status') !== 'paused') || // Vod seek + ( + currentEvent === twitchEvents.PLAY_PAUSE && + ( + ( + oldEvent !== twitchEvents.PLAY_PAUSE && + oldEvent !== twitchEvents.SEEK + ) || + previousData.get('status') === 'playing' + ) + ) // User paused a video + ) { time = currentTime - oldTime } @@ -436,7 +479,7 @@ const getTwitchDuration = (state, data, mediaKey) => { // we get seconds back, so we need to convert it into ms time = time * milliseconds.second - return Math.floor(time) + return time } const getYouTubeDuration = (data) => { diff --git a/docs/state.md b/docs/state.md index aa5f8bbc5b9..ec7f5abcb1d 100644 --- a/docs/state.md +++ b/docs/state.md @@ -93,7 +93,10 @@ AppStore ledgerVideos: { [mediaKey]: { publisher: string // publisher key - beatData: object // data that we get from a heartbeat + // Twitch + event: string, // event that was send to Twitch + time: number, // timestamp that we will log in the ledger + status: string // playing status: playing or paused } } } diff --git a/test/unit/app/common/lib/ledgerUtilTest.js b/test/unit/app/common/lib/ledgerUtilTest.js index 1f453b4c8c7..70551c9f464 100644 --- a/test/unit/app/common/lib/ledgerUtilTest.js +++ b/test/unit/app/common/lib/ledgerUtilTest.js @@ -5,6 +5,7 @@ const Immutable = require('immutable') require('../../../braveUnit') const settings = require('../../../../../js/constants/settings') const ledgerMediaProviders = require('../../../../../app/common/constants/ledgerMediaProviders') +const twitchEvents = require('../../../../../app/common/constants/twitchEvents') const baseState = Immutable.fromJS({ cache: { @@ -16,7 +17,7 @@ const stateWithData = Immutable.fromJS({ ledgerVideos: { 'twitch_test': { publisher: 'twitch#author:test', - event: 'video-play', + event: twitchEvents.START, time: 1519279886 } } @@ -430,14 +431,14 @@ describe('ledgerUtil unit test', function () { it('properties are missing', function () { const result = ledgerUtil.getMediaId({ - event: 'minute-watched' + event: twitchEvents.MINUTE_WATCHED }, ledgerMediaProviders.TWITCH) assert.equal(result, null) }) it('content is a live stream', function () { const result = ledgerUtil.getMediaId({ - event: 'minute-watched', + event: twitchEvents.MINUTE_WATCHED, properties: { channel: 'tchannel' } @@ -447,7 +448,7 @@ describe('ledgerUtil unit test', function () { it('content is a vod', function () { const result = ledgerUtil.getMediaId({ - event: 'minute-watched', + event: twitchEvents.MINUTE_WATCHED, properties: { channel: 'tchannel', vod: 'v12343234' @@ -455,36 +456,6 @@ describe('ledgerUtil unit test', function () { }, ledgerMediaProviders.TWITCH) assert.equal(result, 'tchannel_vod_12343234') }) - - it('event is video-play', function () { - const result = ledgerUtil.getMediaId({ - event: 'video-play', - properties: { - channel: 'tchannel' - } - }, ledgerMediaProviders.TWITCH) - assert.equal(result, 'tchannel') - }) - - it('event is player_click_playpause', function () { - const result = ledgerUtil.getMediaId({ - event: 'player_click_playpause', - properties: { - channel: 'tchannel' - } - }, ledgerMediaProviders.TWITCH) - assert.equal(result, 'tchannel') - }) - - it('event is vod_seek', function () { - const result = ledgerUtil.getMediaId({ - event: 'vod_seek', - properties: { - channel: 'tchannel' - } - }, ledgerMediaProviders.TWITCH) - assert.equal(result, 'tchannel') - }) }) }) @@ -563,7 +534,7 @@ describe('ledgerUtil unit test', function () { it('obj is parsed correctly', function () { const result = ledgerUtil.getMediaData('https://api.mixpanel.com?data=eyJldmVudCI6Im1pbnV0ZS13YXRjaGVkIiwicHJvcGVydGllcyI6eyJjaGFubmVsIjoidHcifX0=', ledgerMediaProviders.TWITCH) assert.deepEqual(result, { - event: 'minute-watched', + event: twitchEvents.MINUTE_WATCHED, properties: { channel: 'tw' } @@ -698,27 +669,159 @@ describe('ledgerUtil unit test', function () { }) it('properties are missing', function () { - const result = ledgerUtil.generateTwitchCacheData({ - event: 'video-play', + const result = ledgerUtil.generateTwitchCacheData(baseState, { + event: twitchEvents.START, channel: 'test' - }) + }, 'twitch_test') + assert.deepEqual(result.toJS(), { - event: 'video-play' + event: twitchEvents.START, + status: 'playing' }) }) it('properties are present', function () { - const result = ledgerUtil.generateTwitchCacheData({ - event: 'video-play', + const result = ledgerUtil.generateTwitchCacheData(baseState, { + event: twitchEvents.START, properties: { time: 100, minute_logged: 1 }, channel: 'test' - }) + }, 'twitch_test') + assert.deepEqual(result.toJS(), { - event: 'video-play', - time: 100 + event: twitchEvents.START, + time: 100, + status: 'playing' + }) + }) + + describe('user actions: ', function () { + it('start -> pause', function () { + const state = baseState + .setIn(['cache', 'ledgerVideos', 'twitch_test'], Immutable.fromJS({ + event: twitchEvents.START, + status: 'playing' + })) + + const result = ledgerUtil.generateTwitchCacheData(state, { + event: twitchEvents.PLAY_PAUSE, + channel: 'test' + }, 'twitch_test') + + assert.deepEqual(result.toJS(), { + event: twitchEvents.PLAY_PAUSE, + status: 'paused' + }) + }) + + it('start -> seek', function () { + const state = baseState + .setIn(['cache', 'ledgerVideos', 'twitch_test'], Immutable.fromJS({ + event: twitchEvents.START, + status: 'playing' + })) + + const result = ledgerUtil.generateTwitchCacheData(state, { + event: twitchEvents.SEEK, + channel: 'test' + }, 'twitch_test') + + assert.deepEqual(result.toJS(), { + event: twitchEvents.SEEK, + status: 'playing' + }) + }) + + it('play -> pause -> play', function () { + const state = baseState + .setIn(['cache', 'ledgerVideos', 'twitch_test'], Immutable.fromJS({ + event: twitchEvents.PLAY_PAUSE, + status: 'paused' + })) + + const result = ledgerUtil.generateTwitchCacheData(state, { + event: twitchEvents.PLAY_PAUSE, + channel: 'test' + }, 'twitch_test') + + assert.deepEqual(result.toJS(), { + event: twitchEvents.PLAY_PAUSE, + status: 'playing' + }) + }) + + it('pause -> play -> pause', function () { + const state = baseState + .setIn(['cache', 'ledgerVideos', 'twitch_test'], Immutable.fromJS({ + event: twitchEvents.PLAY_PAUSE, + status: 'playing' + })) + + const result = ledgerUtil.generateTwitchCacheData(state, { + event: twitchEvents.PLAY_PAUSE, + channel: 'test' + }, 'twitch_test') + + assert.deepEqual(result.toJS(), { + event: twitchEvents.PLAY_PAUSE, + status: 'paused' + }) + }) + + it('play -> pause -> seek', function () { + const state = baseState + .setIn(['cache', 'ledgerVideos', 'twitch_test'], Immutable.fromJS({ + event: twitchEvents.PLAY_PAUSE, + status: 'paused' + })) + + const result = ledgerUtil.generateTwitchCacheData(state, { + event: twitchEvents.SEEK, + channel: 'test' + }, 'twitch_test') + + assert.deepEqual(result.toJS(), { + event: twitchEvents.SEEK, + status: 'paused' + }) + }) + + it('pause -> seek -> play', function () { + const state = baseState + .setIn(['cache', 'ledgerVideos', 'twitch_test'], Immutable.fromJS({ + event: twitchEvents.SEEK, + status: 'paused' + })) + + const result = ledgerUtil.generateTwitchCacheData(state, { + event: twitchEvents.PLAY_PAUSE, + channel: 'test' + }, 'twitch_test') + + assert.deepEqual(result.toJS(), { + event: twitchEvents.PLAY_PAUSE, + status: 'playing' + }) + }) + + it('play -> seek -> pause', function () { + const state = baseState + .setIn(['cache', 'ledgerVideos', 'twitch_test'], Immutable.fromJS({ + event: twitchEvents.SEEK, + status: 'playing' + })) + + const result = ledgerUtil.generateTwitchCacheData(state, { + event: twitchEvents.PLAY_PAUSE, + channel: 'test' + }, 'twitch_test') + + assert.deepEqual(result.toJS(), { + event: twitchEvents.PLAY_PAUSE, + status: 'paused' + }) }) }) }) @@ -731,7 +834,7 @@ describe('ledgerUtil unit test', function () { it('we just video playing', function () { const result = ledgerUtil.getTwitchDuration(baseState, { - event: 'video-play', + event: twitchEvents.START, properties: { time: '1223fa' } @@ -741,7 +844,7 @@ describe('ledgerUtil unit test', function () { it('properties are missing', function () { const result = ledgerUtil.getTwitchDuration(baseState, { - event: 'minute-watched', + event: twitchEvents.MINUTE_WATCHED, properties: { time: '1223fa' } @@ -751,7 +854,7 @@ describe('ledgerUtil unit test', function () { it('current time is not a number', function () { const result = ledgerUtil.getTwitchDuration(stateWithData, { - event: 'minute-watched', + event: twitchEvents.MINUTE_WATCHED, properties: { time: '1223fa' } @@ -761,17 +864,17 @@ describe('ledgerUtil unit test', function () { it('user paused a video', function () { const result = ledgerUtil.getTwitchDuration(stateWithData, { - event: 'player_click_playpause', + event: twitchEvents.PLAY_PAUSE, properties: { time: 1519279926 } }, 'twitch_test') - assert.deepEqual(result, 40000) + assert.deepEqual(result, 30000) }) it('first minute watched', function () { const result = ledgerUtil.getTwitchDuration(stateWithData, { - event: 'minute-watched', + event: twitchEvents.MINUTE_WATCHED, properties: { time: 1519279926 } @@ -781,10 +884,10 @@ describe('ledgerUtil unit test', function () { it('second minute watched', function () { const state = stateWithData - .setIn(['cache', 'ledgerVideos', 'twitch_test', 'event'], 'minute-watched') + .setIn(['cache', 'ledgerVideos', 'twitch_test', 'event'], twitchEvents.MINUTE_WATCHED) const result = ledgerUtil.getTwitchDuration(state, { - event: 'minute-watched', + event: twitchEvents.MINUTE_WATCHED, properties: { time: 1519279926 } @@ -794,10 +897,10 @@ describe('ledgerUtil unit test', function () { it('vod seeked', function () { const state = stateWithData - .setIn(['cache', 'ledgerVideos', 'twitch_test', 'event'], 'minute-watched') + .setIn(['cache', 'ledgerVideos', 'twitch_test', 'event'], twitchEvents.MINUTE_WATCHED) const result = ledgerUtil.getTwitchDuration(state, { - event: 'vod_seek', + event: twitchEvents.SEEK, properties: { time: 1519279926 } @@ -807,7 +910,7 @@ describe('ledgerUtil unit test', function () { it('end time is negative', function () { const result = ledgerUtil.getTwitchDuration(stateWithData, { - event: 'minute-watched', + event: twitchEvents.MINUTE_WATCHED, properties: { time: 1519249926 } @@ -817,7 +920,7 @@ describe('ledgerUtil unit test', function () { it('end time is more then 2 minutes', function () { const result = ledgerUtil.getTwitchDuration(stateWithData, { - event: 'minute-watched', + event: twitchEvents.MINUTE_WATCHED, properties: { time: 1519449926 } @@ -825,17 +928,17 @@ describe('ledgerUtil unit test', function () { assert.deepEqual(result, 120000) }) - it('we need to floor end time', function () { + it('start event is send twice', function () { const state = stateWithData - .setIn(['cache', 'ledgerVideos', 'twitch_test', 'event'], 'minute-watched') + .setIn(['cache', 'ledgerVideos', 'twitch_test', 'event'], twitchEvents.START) const result = ledgerUtil.getTwitchDuration(state, { - event: 'minute-watched', + event: twitchEvents.START, properties: { - time: 1519279926.74353453 + time: 1519279926 } }, 'twitch_test') - assert.deepEqual(result, 40743) + assert.deepEqual(result, 0) }) }) })