pre-roll ads demo

This commit is contained in:
Sean Yesmunt 2021-03-11 16:27:03 -05:00 committed by DispatchCommit
parent e3ea004f67
commit 6041c53748
11 changed files with 344 additions and 112 deletions

View file

@ -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

View file

@ -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,

View file

@ -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",

View file

@ -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}`;

View file

@ -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));

View file

@ -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;

View file

@ -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
View 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];
}

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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"