pre-roll ads demo
This commit is contained in:
parent
e3ea004f67
commit
6041c53748
11 changed files with 344 additions and 112 deletions
|
@ -32,6 +32,7 @@ ENABLE_FILE_REACTIONS=false
|
||||||
ENABLE_CREATOR_REACTIONS=false
|
ENABLE_CREATOR_REACTIONS=false
|
||||||
ENABLE_NO_SOURCE_CLAIMS=false
|
ENABLE_NO_SOURCE_CLAIMS=false
|
||||||
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS=4
|
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS=4
|
||||||
|
ENABLE_PREROLL_ADS=false
|
||||||
|
|
||||||
# OG
|
# OG
|
||||||
OG_TITLE_SUFFIX=| lbry.tv
|
OG_TITLE_SUFFIX=| lbry.tv
|
||||||
|
|
|
@ -37,6 +37,7 @@ const config = {
|
||||||
ENABLE_CREATOR_REACTIONS: process.env.ENABLE_CREATOR_REACTIONS === 'true',
|
ENABLE_CREATOR_REACTIONS: process.env.ENABLE_CREATOR_REACTIONS === 'true',
|
||||||
ENABLE_NO_SOURCE_CLAIMS: process.env.ENABLE_NO_SOURCE_CLAIMS === 'true',
|
ENABLE_NO_SOURCE_CLAIMS: process.env.ENABLE_NO_SOURCE_CLAIMS === 'true',
|
||||||
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS: process.env.CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS,
|
CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS: process.env.CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS,
|
||||||
|
ENABLE_PREROLL_ADS: process.env.ENABLE_PREROLL_ADS === 'true',
|
||||||
SIMPLE_SITE: process.env.SIMPLE_SITE === 'true',
|
SIMPLE_SITE: process.env.SIMPLE_SITE === 'true',
|
||||||
SHOW_ADS: process.env.SHOW_ADS === 'true',
|
SHOW_ADS: process.env.SHOW_ADS === 'true',
|
||||||
PINNED_URI_1: process.env.PINNED_URI_1,
|
PINNED_URI_1: process.env.PINNED_URI_1,
|
||||||
|
|
|
@ -203,6 +203,7 @@
|
||||||
"tree-kill": "^1.1.0",
|
"tree-kill": "^1.1.0",
|
||||||
"unist-util-visit": "^2.0.3",
|
"unist-util-visit": "^2.0.3",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
|
"vast-client": "^3.1.1",
|
||||||
"video.js": "^7.10.1",
|
"video.js": "^7.10.1",
|
||||||
"videojs-contrib-quality-levels": "^2.0.9",
|
"videojs-contrib-quality-levels": "^2.0.9",
|
||||||
"videojs-event-tracking": "^1.0.1",
|
"videojs-event-tracking": "^1.0.1",
|
||||||
|
|
|
@ -26,10 +26,10 @@ if (isProduction) {
|
||||||
// @endif
|
// @endif
|
||||||
|
|
||||||
type Analytics = {
|
type Analytics = {
|
||||||
error: string => Promise<any>,
|
error: (string) => Promise<any>,
|
||||||
sentryError: ({} | string, {}) => Promise<any>,
|
sentryError: ({} | string, {}) => Promise<any>,
|
||||||
pageView: (string, ?string) => void,
|
pageView: (string, ?string) => void,
|
||||||
setUser: Object => void,
|
setUser: (Object) => void,
|
||||||
toggleInternal: (boolean, ?boolean) => void,
|
toggleInternal: (boolean, ?boolean) => void,
|
||||||
apiLogView: (string, string, string, ?number, ?() => void) => Promise<any>,
|
apiLogView: (string, string, string, ?number, ?() => void) => Promise<any>,
|
||||||
apiLogPublish: (ChannelClaim | StreamClaim) => void,
|
apiLogPublish: (ChannelClaim | StreamClaim) => void,
|
||||||
|
@ -51,13 +51,16 @@ type Analytics = {
|
||||||
readyState: number,
|
readyState: number,
|
||||||
}
|
}
|
||||||
) => void,
|
) => void,
|
||||||
|
adsFetchedEvent: () => void,
|
||||||
|
adsReceivedEvent: (any) => void,
|
||||||
|
adsErrorEvent: (any) => void,
|
||||||
emailProvidedEvent: () => void,
|
emailProvidedEvent: () => void,
|
||||||
emailVerifiedEvent: () => void,
|
emailVerifiedEvent: () => void,
|
||||||
rewardEligibleEvent: () => void,
|
rewardEligibleEvent: () => void,
|
||||||
startupEvent: () => void,
|
startupEvent: () => void,
|
||||||
purchaseEvent: number => void,
|
purchaseEvent: (number) => void,
|
||||||
readyEvent: number => void,
|
readyEvent: (number) => void,
|
||||||
openUrlEvent: string => void,
|
openUrlEvent: (string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
type LogPublishParams = {
|
type LogPublishParams = {
|
||||||
|
@ -75,8 +78,8 @@ if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEna
|
||||||
// @endif
|
// @endif
|
||||||
|
|
||||||
const analytics: Analytics = {
|
const analytics: Analytics = {
|
||||||
error: message => {
|
error: (message) => {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
if (internalAnalyticsEnabled && isProduction) {
|
if (internalAnalyticsEnabled && isProduction) {
|
||||||
return Lbryio.call('event', 'desktop_error', { error_message: message }).then(() => {
|
return Lbryio.call('event', 'desktop_error', { error_message: message }).then(() => {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
|
@ -87,9 +90,9 @@ const analytics: Analytics = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sentryError: (error, errorInfo) => {
|
sentryError: (error, errorInfo) => {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
if (internalAnalyticsEnabled && isProduction) {
|
if (internalAnalyticsEnabled && isProduction) {
|
||||||
Sentry.withScope(scope => {
|
Sentry.withScope((scope) => {
|
||||||
scope.setExtras(errorInfo);
|
scope.setExtras(errorInfo);
|
||||||
const eventId = Sentry.captureException(error);
|
const eventId = Sentry.captureException(error);
|
||||||
resolve(eventId);
|
resolve(eventId);
|
||||||
|
@ -114,7 +117,7 @@ const analytics: Analytics = {
|
||||||
MatomoInstance.trackPageView(params);
|
MatomoInstance.trackPageView(params);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setUser: userId => {
|
setUser: (userId) => {
|
||||||
if (internalAnalyticsEnabled && userId) {
|
if (internalAnalyticsEnabled && userId) {
|
||||||
window._paq.push(['setUserId', String(userId)]);
|
window._paq.push(['setUserId', String(userId)]);
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
|
@ -188,7 +191,7 @@ const analytics: Analytics = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
apiSyncTags: params => {
|
apiSyncTags: (params) => {
|
||||||
if (internalAnalyticsEnabled && isProduction) {
|
if (internalAnalyticsEnabled && isProduction) {
|
||||||
Lbryio.call('content_tags', 'sync', params);
|
Lbryio.call('content_tags', 'sync', params);
|
||||||
}
|
}
|
||||||
|
@ -238,10 +241,19 @@ const analytics: Analytics = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
playerLoadedEvent: embedded => {
|
adsFetchedEvent: () => {
|
||||||
|
sendMatomoEvent('Media', 'AdsFetched');
|
||||||
|
},
|
||||||
|
adsReceivedEvent: (response) => {
|
||||||
|
sendMatomoEvent('Media', 'AdsReceived', JSON.stringify(response));
|
||||||
|
},
|
||||||
|
adsErrorEvent: (response) => {
|
||||||
|
sendMatomoEvent('Media', 'AdsError', JSON.stringify(response));
|
||||||
|
},
|
||||||
|
playerLoadedEvent: (embedded) => {
|
||||||
sendMatomoEvent('Player', 'Loaded', embedded ? 'embedded' : 'onsite');
|
sendMatomoEvent('Player', 'Loaded', embedded ? 'embedded' : 'onsite');
|
||||||
},
|
},
|
||||||
playerStartedEvent: embedded => {
|
playerStartedEvent: (embedded) => {
|
||||||
sendMatomoEvent('Player', 'Started', embedded ? 'embedded' : 'onsite');
|
sendMatomoEvent('Player', 'Started', embedded ? 'embedded' : 'onsite');
|
||||||
},
|
},
|
||||||
tagFollowEvent: (tag, following) => {
|
tagFollowEvent: (tag, following) => {
|
||||||
|
@ -316,7 +328,7 @@ analytics.pageView(
|
||||||
|
|
||||||
// Listen for url changes and report
|
// Listen for url changes and report
|
||||||
// This will include search queries
|
// This will include search queries
|
||||||
history.listen(location => {
|
history.listen((location) => {
|
||||||
const { pathname, search } = location;
|
const { pathname, search } = location;
|
||||||
|
|
||||||
const page = `${pathname}${search}`;
|
const page = `${pathname}${search}`;
|
||||||
|
|
|
@ -7,8 +7,9 @@ import { makeSelectContentPositionForUri } from 'redux/selectors/content';
|
||||||
import VideoViewer from './view';
|
import VideoViewer from './view';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectHomepageData } from 'redux/selectors/settings';
|
||||||
import { toggleVideoTheaterMode, doSetClientSetting } from 'redux/actions/settings';
|
import { toggleVideoTheaterMode, doSetClientSetting } from 'redux/actions/settings';
|
||||||
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const { search } = props.location;
|
const { search } = props.location;
|
||||||
|
@ -26,19 +27,21 @@ const select = (state, props) => {
|
||||||
hasFileInfo: Boolean(makeSelectFileInfoForUri(props.uri)(state)),
|
hasFileInfo: Boolean(makeSelectFileInfoForUri(props.uri)(state)),
|
||||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
homepageData: selectHomepageData(state),
|
||||||
|
authenticated: selectUserVerifiedEmail(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
changeVolume: volume => dispatch(doChangeVolume(volume)),
|
changeVolume: (volume) => dispatch(doChangeVolume(volume)),
|
||||||
savePosition: (uri, position) => dispatch(savePosition(uri, position)),
|
savePosition: (uri, position) => dispatch(savePosition(uri, position)),
|
||||||
clearPosition: uri => dispatch(clearPosition(uri)),
|
clearPosition: (uri) => dispatch(clearPosition(uri)),
|
||||||
changeMute: muted => dispatch(doChangeMute(muted)),
|
changeMute: (muted) => dispatch(doChangeMute(muted)),
|
||||||
doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
|
doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
|
||||||
doAnalyticsBuffer: (uri, bufferData) => dispatch(doAnalyticsBuffer(uri, bufferData)),
|
doAnalyticsBuffer: (uri, bufferData) => dispatch(doAnalyticsBuffer(uri, bufferData)),
|
||||||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||||
toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()),
|
toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()),
|
||||||
setVideoPlaybackRate: rate => dispatch(doSetClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE, rate)),
|
setVideoPlaybackRate: (rate) => dispatch(doSetClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE, rate)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(VideoViewer));
|
export default withRouter(connect(select, perform)(VideoViewer));
|
||||||
|
|
|
@ -49,6 +49,7 @@ type Props = {
|
||||||
startMuted: boolean,
|
startMuted: boolean,
|
||||||
autoplay: boolean,
|
autoplay: boolean,
|
||||||
toggleVideoTheaterMode: () => void,
|
toggleVideoTheaterMode: () => void,
|
||||||
|
adUrl: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
type VideoJSOptions = {
|
type VideoJSOptions = {
|
||||||
|
@ -171,7 +172,17 @@ class LbryVolumeBarClass extends videojs.getComponent(VIDEOJS_VOLUME_BAR_CLASS)
|
||||||
properties for this component should be kept to ONLY those that if changed should REQUIRE an entirely new videojs element
|
properties for this component should be kept to ONLY those that if changed should REQUIRE an entirely new videojs element
|
||||||
*/
|
*/
|
||||||
export default React.memo<Props>(function VideoJs(props: Props) {
|
export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
const { autoplay, startMuted, source, sourceType, poster, isAudio, onPlayerReady, toggleVideoTheaterMode } = props;
|
const {
|
||||||
|
autoplay,
|
||||||
|
startMuted,
|
||||||
|
source,
|
||||||
|
sourceType,
|
||||||
|
poster,
|
||||||
|
isAudio,
|
||||||
|
onPlayerReady,
|
||||||
|
toggleVideoTheaterMode,
|
||||||
|
adUrl,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const [reload, setReload] = useState('initial');
|
const [reload, setReload] = useState('initial');
|
||||||
|
|
||||||
|
@ -194,6 +205,13 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// if (adUrl) {
|
||||||
|
// // Add the adUrl to the first entry in `sources`
|
||||||
|
// // After the ad is finished, it will be removed as a prop to this component
|
||||||
|
// videoJsOptions.sources.unshift({ src: adUrl, type: 'video/mp4' });
|
||||||
|
// console.log('added ad');
|
||||||
|
// }
|
||||||
|
|
||||||
const tapToUnmuteRef = useRef();
|
const tapToUnmuteRef = useRef();
|
||||||
const tapToRetryRef = useRef();
|
const tapToRetryRef = useRef();
|
||||||
|
|
||||||
|
@ -333,9 +351,11 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEnded() {
|
const onEnded = React.useCallback(() => {
|
||||||
showTapButton(TAP.NONE);
|
if (!adUrl) {
|
||||||
}
|
showTapButton(TAP.NONE);
|
||||||
|
}
|
||||||
|
}, [adUrl]);
|
||||||
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
const player = playerRef.current;
|
const player = playerRef.current;
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { ENABLE_PREROLL_ADS } from 'config';
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { useEffect, useState, useContext, useCallback } from 'react';
|
import React, { useEffect, useState, useContext, useCallback } from 'react';
|
||||||
import { stopContextMenu } from 'util/context-menu';
|
import { stopContextMenu } from 'util/context-menu';
|
||||||
import type { Player } from './internal/videojs';
|
import type { Player } from './internal/videojs';
|
||||||
|
@ -13,6 +16,10 @@ import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded';
|
||||||
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
||||||
import LoadingScreen from 'component/common/loading-screen';
|
import LoadingScreen from 'component/common/loading-screen';
|
||||||
import { addTheaterModeButton } from './internal/theater-mode';
|
import { addTheaterModeButton } from './internal/theater-mode';
|
||||||
|
import { useGetAds } from 'effects/use-get-ads';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
|
||||||
const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
||||||
const PLAY_TIMEOUT_LIMIT = 2000;
|
const PLAY_TIMEOUT_LIMIT = 2000;
|
||||||
|
@ -39,6 +46,16 @@ type Props = {
|
||||||
clearPosition: (string) => void,
|
clearPosition: (string) => void,
|
||||||
toggleVideoTheaterMode: () => void,
|
toggleVideoTheaterMode: () => void,
|
||||||
setVideoPlaybackRate: (number) => void,
|
setVideoPlaybackRate: (number) => void,
|
||||||
|
authenticated: boolean,
|
||||||
|
homepageData: {
|
||||||
|
PRIMARY_CONTENT_CHANNEL_IDS?: Array<string>,
|
||||||
|
ENLIGHTENMENT_CHANNEL_IDS?: Array<string>,
|
||||||
|
GAMING_CHANNEL_IDS?: Array<string>,
|
||||||
|
SCIENCE_CHANNEL_IDS?: Array<string>,
|
||||||
|
TECHNOLOGY_CHANNEL_IDS?: Array<string>,
|
||||||
|
COMMUNITY_CHANNEL_IDS?: Array<string>,
|
||||||
|
FINCANCE_CHANNEL_IDS?: Array<string>,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -69,22 +86,48 @@ function VideoViewer(props: Props) {
|
||||||
desktopPlayStartTime,
|
desktopPlayStartTime,
|
||||||
toggleVideoTheaterMode,
|
toggleVideoTheaterMode,
|
||||||
setVideoPlaybackRate,
|
setVideoPlaybackRate,
|
||||||
|
homepageData,
|
||||||
|
authenticated,
|
||||||
} = props;
|
} = props;
|
||||||
|
const {
|
||||||
|
PRIMARY_CONTENT_CHANNEL_IDS = [],
|
||||||
|
ENLIGHTENMENT_CHANNEL_IDS = [],
|
||||||
|
GAMING_CHANNEL_IDS = [],
|
||||||
|
SCIENCE_CHANNEL_IDS = [],
|
||||||
|
TECHNOLOGY_CHANNEL_IDS = [],
|
||||||
|
COMMUNITY_CHANNEL_IDS = [],
|
||||||
|
FINCANCE_CHANNEL_IDS = [],
|
||||||
|
} = homepageData;
|
||||||
|
const adApprovedChannelIds = [
|
||||||
|
...PRIMARY_CONTENT_CHANNEL_IDS,
|
||||||
|
...ENLIGHTENMENT_CHANNEL_IDS,
|
||||||
|
...GAMING_CHANNEL_IDS,
|
||||||
|
...SCIENCE_CHANNEL_IDS,
|
||||||
|
...TECHNOLOGY_CHANNEL_IDS,
|
||||||
|
...COMMUNITY_CHANNEL_IDS,
|
||||||
|
...FINCANCE_CHANNEL_IDS,
|
||||||
|
];
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
|
const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
|
||||||
const isAudio = contentType.includes('audio');
|
const isAudio = contentType.includes('audio');
|
||||||
const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
||||||
|
const {
|
||||||
|
location: { pathname },
|
||||||
|
} = useHistory();
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
|
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
|
||||||
const [isEndededEmbed, setIsEndededEmbed] = useState(false);
|
const [isEndededEmbed, setIsEndededEmbed] = useState(false);
|
||||||
const vjsCallbackDataRef: any = React.useRef();
|
const vjsCallbackDataRef: any = React.useRef();
|
||||||
|
|
||||||
|
const previousUri = usePrevious(uri);
|
||||||
|
const embedded = useContext(EmbedContext);
|
||||||
|
const approvedVideo = Boolean(channelClaimId) && adApprovedChannelIds.includes(channelClaimId);
|
||||||
|
const adsEnabled = ENABLE_PREROLL_ADS && !authenticated && !embedded && approvedVideo;
|
||||||
|
const [adUrl, setAdUrl, isFetchingAd] = useGetAds(adsEnabled);
|
||||||
/* isLoading was designed to show loading screen on first play press, rather than completely black screen, but
|
/* isLoading was designed to show loading screen on first play press, rather than completely black screen, but
|
||||||
breaks because some browsers (e.g. Firefox) block autoplay but leave the player.play Promise pending */
|
breaks because some browsers (e.g. Firefox) block autoplay but leave the player.play Promise pending */
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const previousUri = usePrevious(uri);
|
|
||||||
const embedded = useContext(EmbedContext);
|
|
||||||
|
|
||||||
// force everything to recent when URI changes, can cause weird corner cases otherwise (e.g. navigate while autoplay is true)
|
// force everything to recent when URI changes, can cause weird corner cases otherwise (e.g. navigate while autoplay is true)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (uri && previousUri && uri !== previousUri) {
|
if (uri && previousUri && uri !== previousUri) {
|
||||||
|
@ -123,13 +166,18 @@ function VideoViewer(props: Props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEnded() {
|
const onEnded = React.useCallback(() => {
|
||||||
|
if (adUrl) {
|
||||||
|
setAdUrl(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (embedded) {
|
if (embedded) {
|
||||||
setIsEndededEmbed(true);
|
setIsEndededEmbed(true);
|
||||||
} else if (autoplaySetting) {
|
} else if (autoplaySetting) {
|
||||||
setShowAutoplayCountdown(true);
|
setShowAutoplayCountdown(true);
|
||||||
}
|
}
|
||||||
}
|
}, [embedded, setIsEndededEmbed, autoplaySetting, setShowAutoplayCountdown, adUrl, setAdUrl]);
|
||||||
|
|
||||||
function onPlay() {
|
function onPlay() {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -151,86 +199,83 @@ function VideoViewer(props: Props) {
|
||||||
player.playbackRate(vjsCallbackDataRef.current.videoPlaybackRate);
|
player.playbackRate(vjsCallbackDataRef.current.videoPlaybackRate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const playerReadyDependencyList = [uri, adUrl, embedded, autoplayIfEmbedded];
|
||||||
|
if (!IS_WEB) {
|
||||||
|
playerReadyDependencyList.push(desktopPlayStartTime);
|
||||||
|
}
|
||||||
|
|
||||||
const onPlayerReady = useCallback(
|
const onPlayerReady = useCallback((player: Player) => {
|
||||||
(player: Player) => {
|
if (!embedded) {
|
||||||
if (!embedded) {
|
player.muted(muted);
|
||||||
player.muted(muted);
|
player.volume(volume);
|
||||||
player.volume(volume);
|
player.playbackRate(videoPlaybackRate);
|
||||||
player.playbackRate(videoPlaybackRate);
|
addTheaterModeButton(player, toggleVideoTheaterMode);
|
||||||
addTheaterModeButton(player, toggleVideoTheaterMode);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const shouldPlay = !embedded || autoplayIfEmbedded;
|
const shouldPlay = !embedded || autoplayIfEmbedded;
|
||||||
// https://blog.videojs.com/autoplay-best-practices-with-video-js/#Programmatic-Autoplay-and-Success-Failure-Detection
|
// https://blog.videojs.com/autoplay-best-practices-with-video-js/#Programmatic-Autoplay-and-Success-Failure-Detection
|
||||||
if (shouldPlay) {
|
if (shouldPlay) {
|
||||||
const playPromise = player.play();
|
const playPromise = player.play();
|
||||||
const timeoutPromise = new Promise((resolve, reject) =>
|
const timeoutPromise = new Promise((resolve, reject) =>
|
||||||
setTimeout(() => reject(PLAY_TIMEOUT_ERROR), PLAY_TIMEOUT_LIMIT)
|
setTimeout(() => reject(PLAY_TIMEOUT_ERROR), PLAY_TIMEOUT_LIMIT)
|
||||||
);
|
);
|
||||||
|
|
||||||
Promise.race([playPromise, timeoutPromise]).catch((error) => {
|
Promise.race([playPromise, timeoutPromise]).catch((error) => {
|
||||||
if (PLAY_TIMEOUT_ERROR) {
|
if (PLAY_TIMEOUT_ERROR) {
|
||||||
const retryPlayPromise = player.play();
|
const retryPlayPromise = player.play();
|
||||||
Promise.race([retryPlayPromise, timeoutPromise]).catch((error) => {
|
Promise.race([retryPlayPromise, timeoutPromise]).catch((error) => {
|
||||||
setIsLoading(false);
|
|
||||||
setIsPlaying(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
}
|
});
|
||||||
});
|
} else {
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsPlaying(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(shouldPlay); // if we are here outside of an embed, we're playing
|
||||||
|
|
||||||
|
// PR: #5535
|
||||||
|
// Move the restoration to a later `loadedmetadata` phase to counter the
|
||||||
|
// delay from the header-fetch. This is a temp change until the next
|
||||||
|
// re-factoring.
|
||||||
|
player.on('loadedmetadata', () => restorePlaybackRate(player));
|
||||||
|
|
||||||
|
player.on('tracking:buffered', doTrackingBuffered);
|
||||||
|
player.on('tracking:firstplay', doTrackingFirstPlay);
|
||||||
|
player.on('ended', onEnded);
|
||||||
|
player.on('play', onPlay);
|
||||||
|
player.on('pause', () => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
handlePosition(player);
|
||||||
|
});
|
||||||
|
player.on('error', () => {
|
||||||
|
const error = player.error();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
analytics.sentryError('Video.js error', error);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
setIsLoading(shouldPlay); // if we are here outside of an embed, we're playing
|
player.on('volumechange', () => {
|
||||||
|
if (player) {
|
||||||
// PR: #5535
|
changeVolume(player.volume());
|
||||||
// Move the restoration to a later `loadedmetadata` phase to counter the
|
changeMute(player.muted());
|
||||||
// delay from the header-fetch. This is a temp change until the next
|
|
||||||
// re-factoring.
|
|
||||||
player.on('loadedmetadata', () => restorePlaybackRate(player));
|
|
||||||
|
|
||||||
player.on('tracking:buffered', doTrackingBuffered);
|
|
||||||
player.on('tracking:firstplay', doTrackingFirstPlay);
|
|
||||||
player.on('ended', onEnded);
|
|
||||||
player.on('play', onPlay);
|
|
||||||
player.on('pause', () => {
|
|
||||||
setIsPlaying(false);
|
|
||||||
handlePosition(player);
|
|
||||||
});
|
|
||||||
player.on('error', () => {
|
|
||||||
const error = player.error();
|
|
||||||
if (error) {
|
|
||||||
analytics.sentryError('Video.js error', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
player.on('volumechange', () => {
|
|
||||||
if (player) {
|
|
||||||
changeVolume(player.volume());
|
|
||||||
changeMute(player.muted());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
player.on('ratechange', () => {
|
|
||||||
const HAVE_NOTHING = 0; // https://docs.videojs.com/player#readyState
|
|
||||||
if (player && player.readyState() !== HAVE_NOTHING) {
|
|
||||||
// The playbackRate occasionally resets to 1, typically when loading a fresh video or when 'src' changes.
|
|
||||||
// Videojs says it's a browser quirk (https://github.com/videojs/video.js/issues/2516).
|
|
||||||
// [x] Don't update 'videoPlaybackRate' in this scenario.
|
|
||||||
// [ ] Ideally, the controlBar should be hidden to prevent users from changing the rate while loading.
|
|
||||||
setVideoPlaybackRate(player.playbackRate());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (position) {
|
|
||||||
player.currentTime(position);
|
|
||||||
}
|
}
|
||||||
player.on('dispose', () => {
|
});
|
||||||
handlePosition(player);
|
player.on('ratechange', () => {
|
||||||
});
|
if (player) {
|
||||||
},
|
setVideoPlaybackRate(player.playbackRate());
|
||||||
IS_WEB ? [uri] : [uri, desktopPlayStartTime]
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
|
if (position) {
|
||||||
|
player.currentTime(position);
|
||||||
|
}
|
||||||
|
player.on('dispose', () => {
|
||||||
|
handlePosition(player);
|
||||||
|
});
|
||||||
|
}, playerReadyDependencyList);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -245,16 +290,50 @@ function VideoViewer(props: Props) {
|
||||||
{embedded && !isEndededEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
{embedded && !isEndededEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
||||||
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
|
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
|
||||||
{isLoading && <LoadingScreen status={__('Loading')} />}
|
{isLoading && <LoadingScreen status={__('Loading')} />}
|
||||||
<VideoJs
|
|
||||||
source={source}
|
{!isFetchingAd && adUrl && (
|
||||||
isAudio={isAudio}
|
<>
|
||||||
poster={isAudio || (embedded && !autoplayIfEmbedded) ? thumbnail : ''}
|
<span className="ads__video-notify">
|
||||||
sourceType={forcePlayer ? 'video/mp4' : contentType}
|
{__('Advertisement')}{' '}
|
||||||
onPlayerReady={onPlayerReady}
|
<Button
|
||||||
startMuted={autoplayIfEmbedded}
|
className="ads__video-close"
|
||||||
toggleVideoTheaterMode={toggleVideoTheaterMode}
|
icon={ICONS.REMOVE}
|
||||||
autoplay={!embedded || autoplayIfEmbedded}
|
title={__('Close')}
|
||||||
/>
|
onClick={() => setAdUrl(null)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="ads__video-nudge">
|
||||||
|
<I18nMessage
|
||||||
|
tokens={{
|
||||||
|
sign_up: (
|
||||||
|
<Button
|
||||||
|
button="secondary"
|
||||||
|
className="ads__video-link"
|
||||||
|
label={__('Sign Up')}
|
||||||
|
navigate={`/$/${PAGES.AUTH}?redirect=${pathname}&src=video-ad`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
%sign_up% to turn ads off.
|
||||||
|
</I18nMessage>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isFetchingAd && (
|
||||||
|
<VideoJs
|
||||||
|
adUrl={adUrl}
|
||||||
|
source={adUrl || source}
|
||||||
|
sourceType={forcePlayer || adUrl ? 'video/mp4' : contentType}
|
||||||
|
isAudio={isAudio}
|
||||||
|
poster={isAudio || (embedded && !autoplayIfEmbedded) ? thumbnail : ''}
|
||||||
|
onPlayerReady={onPlayerReady}
|
||||||
|
startMuted={autoplayIfEmbedded}
|
||||||
|
toggleVideoTheaterMode={toggleVideoTheaterMode}
|
||||||
|
autoplay={!embedded || autoplayIfEmbedded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
53
ui/effects/use-get-ads.js
Normal file
53
ui/effects/use-get-ads.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { VASTClient } from 'vast-client';
|
||||||
|
import analytics from 'analytics';
|
||||||
|
|
||||||
|
// const PRE_ROLL_ADS_PROVIDER = '`https://tag.targeting.unrulymedia.com/rmp/216276/0/vast2?vastfw=vpaid&w=300&h=500&url=';
|
||||||
|
|
||||||
|
// Ignores any call made 1 minutes or less after the last successful ad
|
||||||
|
const ADS_CAP_LEVEL = 1 * 60 * 1000;
|
||||||
|
const vastClient = new VASTClient(0, ADS_CAP_LEVEL);
|
||||||
|
|
||||||
|
export function useGetAds(adsEnabled: boolean): [?string, (?string) => void, boolean] {
|
||||||
|
const [isFetching, setIsFetching] = React.useState(true);
|
||||||
|
const [adUrl, setAdUrl] = React.useState();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// if (!adsEnabled) {
|
||||||
|
// setIsFetching(false);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
analytics.adsFetchedEvent();
|
||||||
|
// const encodedHref = encodeURI(window.location.href);
|
||||||
|
// const url = `${PRE_ROLL_ADS_PROVIDER}${encodedHref}`;
|
||||||
|
|
||||||
|
const url = 'https://raw.githubusercontent.com/dailymotion/vast-client-js/master/test/vastfiles/sample.xml';
|
||||||
|
|
||||||
|
vastClient
|
||||||
|
.get(url)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.ads.length > 0) {
|
||||||
|
// Let this line error if res.ads is empty
|
||||||
|
// I took this from an example response from Dailymotion
|
||||||
|
// It will be caught below and sent to matomo to figure out if there if this needs to be something changed to deal with unrulys data
|
||||||
|
// const adUrl = res.ads[0].creatives[0].mediaFiles[0].fileURL;
|
||||||
|
|
||||||
|
// Dummy video file
|
||||||
|
const adUrl = 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4';
|
||||||
|
|
||||||
|
if (adUrl) {
|
||||||
|
setAdUrl(adUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFetching(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setIsFetching(false);
|
||||||
|
});
|
||||||
|
}, [adsEnabled]);
|
||||||
|
|
||||||
|
return [adUrl, setAdUrl, isFetching];
|
||||||
|
}
|
|
@ -83,3 +83,52 @@
|
||||||
.ads__claim-text--small {
|
.ads__claim-text--small {
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-roll ads
|
||||||
|
.ads__video-nudge,
|
||||||
|
.ads__video-notify {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ads__video-nudge {
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--color-white);
|
||||||
|
font-weight: bold;
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ads__video-notify {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
background-color: black;
|
||||||
|
border-bottom-left-radius: var(--border-radius);
|
||||||
|
color: var(--color-white);
|
||||||
|
font-size: var(--font-small);
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ads__video-link.button--secondary {
|
||||||
|
font-size: var(--font-small);
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ads__video-close {
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
stroke: var(--color-white);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
stroke: var(--color-black);
|
||||||
|
background-color: var(--color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -137,6 +137,7 @@
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
margin-top: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -11498,6 +11498,13 @@ vary@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||||
|
|
||||||
|
vast-client@^3.1.1:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/vast-client/-/vast-client-3.1.1.tgz#75f044ff1554e0f193302dfa1c20c7f7006fd1f8"
|
||||||
|
integrity sha512-ED32RnLthWgAjQiEPsbqqC4LkN8+EhFyevHVh2SsmlPr6auugjswdbv+VgaQ/d7KUH/vpZ675HzVkIqkB2ibiQ==
|
||||||
|
dependencies:
|
||||||
|
xmldom "^0.3.0"
|
||||||
|
|
||||||
vendors@^1.0.0:
|
vendors@^1.0.0:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
|
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
|
||||||
|
@ -11998,6 +12005,11 @@ xmldom@^0.1.27:
|
||||||
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
|
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
|
||||||
integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
|
integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
|
||||||
|
|
||||||
|
xmldom@^0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.3.0.tgz#e625457f4300b5df9c2e1ecb776147ece47f3e5a"
|
||||||
|
integrity sha512-z9s6k3wxE+aZHgXYxSTpGDo7BYOUfJsIRyoZiX6HTjwpwfS2wpQBQKa2fD+ShLyPkqDYo5ud7KitmLZ2Cd6r0g==
|
||||||
|
|
||||||
xpipe@*:
|
xpipe@*:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/xpipe/-/xpipe-1.0.5.tgz#8dd8bf45fc3f7f55f0e054b878f43a62614dafdf"
|
resolved "https://registry.yarnpkg.com/xpipe/-/xpipe-1.0.5.tgz#8dd8bf45fc3f7f55f0e054b878f43a62614dafdf"
|
||||||
|
|
Loading…
Reference in a new issue