From a7e3bceb6c4380bf1265c7fa44a990e1ec5a17fb Mon Sep 17 00:00:00 2001 From: DispatchCommit Date: Wed, 30 Jun 2021 09:29:00 -0700 Subject: [PATCH] Add pre-roll ads change ad macro url rework video.js load logic fix autoplay and retry errors yeet player.ended error --- package.json | 2 + .../plugins/videojs-aniview/plugin.js | 100 ++++++++++ .../plugins/videojs-mobile-ui/plugin.js | 2 +- .../plugins/videojs-mobile-ui/touchOverlay.js | 2 +- .../plugins/videojs-overlay/plugin.js | 2 +- .../internal/plugins/videojs-recsys/plugin.js | 2 +- .../viewers/videoViewer/internal/videojs.jsx | 172 ++++++++++++------ ui/component/viewers/videoViewer/view.jsx | 28 +-- ui/scss/component/_file-render.scss | 6 +- yarn.lock | 68 ++++++- 10 files changed, 303 insertions(+), 81 deletions(-) create mode 100644 ui/component/viewers/videoViewer/internal/plugins/videojs-aniview/plugin.js diff --git a/package.json b/package.json index 9d19ade6e..4a4e651f4 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,8 @@ "remove-markdown": "^0.3.0", "source-map-explorer": "^2.5.2", "tempy": "^0.6.0", + "videojs-contrib-ads": "^6.9.0", + "videojs-ima": "^1.11.0", "videojs-logo": "^2.1.4" }, "devDependencies": { diff --git a/ui/component/viewers/videoViewer/internal/plugins/videojs-aniview/plugin.js b/ui/component/viewers/videoViewer/internal/plugins/videojs-aniview/plugin.js new file mode 100644 index 000000000..eb1034750 --- /dev/null +++ b/ui/component/viewers/videoViewer/internal/plugins/videojs-aniview/plugin.js @@ -0,0 +1,100 @@ +// Created by xander on 6/21/2021 +import videojs from 'video.js'; +import 'videojs-ima'; +const VERSION = '0.0.1'; + +const macroUrl = + 'https://vast.aniview.com/api/adserver61/vast/?AV_PUBLISHERID=60afcbc58cfdb065440d2426&AV_CHANNELID=60b354389c7adb506d0bd9a4&AV_URL=[URL_MACRO]&cb=[TIMESTAMP_MACRO]&AV_WIDTH=[WIDTH_MACRO]&AV_HEIGHT=[HEIGHT_MACRO]&AV_SCHAIN=[SCHAIN_MACRO]&AV_CCPA=[CCPA_MACRO]&AV_GDPR=[GDPR_MACRO]&AV_CONSENT=[CONSENT_MACRO]&skip=true&skiptimer=5&usevslot=true&hidecontrols=false'; + +const defaults = { + adTagUrl: macroUrl, + debug: false, +}; + +const Component = videojs.getComponent('Component'); +const registerPlugin = videojs.registerPlugin || videojs.plugin; + +class AniviewPlugin extends Component { + constructor(player, options) { + super(player, options); + + // Plugin started + if (options.debug) { + this.log(`Created aniview plugin.`); + } + + // To help with debugging, we'll add a global vjs object with the video js player + window.aniview = player; + + this.player = player; + + player.ima({ + // adTagUrl: macroUrl, + id: 'ad_content_video', + vpaidMode: google.ima.ImaSdkSettings.VpaidMode.INSECURE, + adTagUrl: + 'https://vast.aniview.com/api/adserver61/vast/?AV_PUBLISHERID=60afcbc58cfdb065440d2426&AV_CHANNELID=60b354389c7adb506d0bd9a4', + }); + + // this.player.ads(); + // const serverUrl = this.player.ads.adMacroReplacement(macroUrl); + // this.log(serverUrl); + + // request ads whenever there's new video content + player.on('contentchanged', () => { + // in a real plugin, you might fetch your ad inventory here + player.trigger('adsready'); + }); + + // Plugin event listeners + player.on('readyforpreroll', (event) => this.onReadyForPreroll(event)); + } + + onReadyForPreroll(event) { + this.player.ads.startLinearAdMode(); + + // play your linear ad content + // in this example, we use a static mp4 + this.player.src('kitteh.mp4'); + + // send event when ad is playing to remove loading spinner + this.player.one('adplaying', () => { + this.player.trigger('ads-ad-started'); + }); + + // resume content when all your linear ads have finished + this.player.one('adended', () => { + this.player.ads.endLinearAdMode(); + }); + } + + log(...args) { + if (this.options_.debug) { + console.log(`Aniview Debug:`, JSON.stringify(args)); + } + } +} + +videojs.registerComponent('recsys', AniviewPlugin); + +const onPlayerReady = (player, options) => { + player.aniview = new AniviewPlugin(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('aniview', plugin); + +export default plugin; diff --git a/ui/component/viewers/videoViewer/internal/plugins/videojs-mobile-ui/plugin.js b/ui/component/viewers/videoViewer/internal/plugins/videojs-mobile-ui/plugin.js index d0039d315..8bb9e0d23 100644 --- a/ui/component/viewers/videoViewer/internal/plugins/videojs-mobile-ui/plugin.js +++ b/ui/component/viewers/videoViewer/internal/plugins/videojs-mobile-ui/plugin.js @@ -1,4 +1,4 @@ -import videojs from 'video.js/dist/video.min.js'; +import videojs from 'video.js'; import './touchOverlay.js'; import window from 'global/window'; import './plugin.scss'; diff --git a/ui/component/viewers/videoViewer/internal/plugins/videojs-mobile-ui/touchOverlay.js b/ui/component/viewers/videoViewer/internal/plugins/videojs-mobile-ui/touchOverlay.js index 880c6a7aa..00fe9271d 100644 --- a/ui/component/viewers/videoViewer/internal/plugins/videojs-mobile-ui/touchOverlay.js +++ b/ui/component/viewers/videoViewer/internal/plugins/videojs-mobile-ui/touchOverlay.js @@ -3,7 +3,7 @@ * Touch UI component */ -import videojs from 'video.js/dist/video.min.js'; +import videojs from 'video.js'; import window from 'global/window'; const Component = videojs.getComponent('Component'); diff --git a/ui/component/viewers/videoViewer/internal/plugins/videojs-overlay/plugin.js b/ui/component/viewers/videoViewer/internal/plugins/videojs-overlay/plugin.js index dbf876c88..682c8ea02 100644 --- a/ui/component/viewers/videoViewer/internal/plugins/videojs-overlay/plugin.js +++ b/ui/component/viewers/videoViewer/internal/plugins/videojs-overlay/plugin.js @@ -1,4 +1,4 @@ -import videojs from 'video.js/dist/video.min.js'; +import videojs from 'video.js'; import window from 'global/window'; const VERSION = '2.1.4'; diff --git a/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js b/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js index aa2d5f82d..ea97be239 100644 --- a/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js +++ b/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js @@ -1,5 +1,5 @@ // Created by xander on 6/21/2021 -import videojs from 'video.js/dist/video.min.js'; +import videojs from 'video.js'; import { v4 as uuidV4 } from 'uuid'; const VERSION = '0.0.1'; diff --git a/ui/component/viewers/videoViewer/internal/videojs.jsx b/ui/component/viewers/videoViewer/internal/videojs.jsx index 14dbaa010..d1942433f 100644 --- a/ui/component/viewers/videoViewer/internal/videojs.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs.jsx @@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from 'react'; import Button from 'component/button'; import * as ICONS from 'constants/icons'; import classnames from 'classnames'; -import videojs from 'video.js/dist/video.min.js'; +import videojs from 'video.js'; // import 'video.js/dist/alt/video-js-cdn.min.css'; --> 'scss/third-party.scss' import eventTracking from 'videojs-event-tracking'; import * as OVERLAY from './overlays'; @@ -12,8 +12,13 @@ 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'; +import 'videojs-contrib-ads'; +import 'videojs-ima'; +import aniview from './plugins/videojs-aniview/plugin'; const isDev = process.env.NODE_ENV !== 'production'; +const macroUrl = + 'https://vast.aniview.com/api/adserver61/vast/?AV_PUBLISHERID=60afcbc58cfdb065440d2426&AV_CHANNELID=60b354389c7adb506d0bd9a4&AV_URL=[URL_MACRO]&cb=[TIMESTAMP_MACRO]&AV_WIDTH=[WIDTH_MACRO]&AV_HEIGHT=[HEIGHT_MACRO]&AV_SCHAIN=[SCHAIN_MACRO]&AV_CCPA=[CCPA_MACRO]&AV_GDPR=[GDPR_MACRO]&AV_CONSENT=[CONSENT_MACRO]&skip=true&skiptimer=5&usevslot=true&hidecontrols=false'; export type Player = { on: (string, (any) => void) => void, @@ -77,7 +82,7 @@ const VIDEO_JS_OPTIONS = { responsive: true, controls: true, html5: { - hls: { + vhs: { overrideNative: !videojs.browser.IS_ANY_SAFARI, }, }, @@ -336,6 +341,8 @@ export default React.memo(function VideoJs(props: Props) { // The css starts as "hidden". We make it visible here without // re-rendering the whole thing. showTapButton(TAP.UNMUTE); + } else { + showTapButton(TAP.NONE); } } @@ -350,6 +357,9 @@ export default React.memo(function VideoJs(props: Props) { const player = playerRef.current; showTapButton(TAP.RETRY); + // reattach initial play listener in case we recover from error successfully + player.one('play', onInitialPlay); + if (player && player.loadingSpinner) { player.loadingSpinner.hide(); } @@ -472,9 +482,7 @@ export default React.memo(function VideoJs(props: Props) { // Create the video DOM element and wrapper function createVideoPlayerDOM(container) { - if (!container) { - return; - } + if (!container) return; // This seems like a poor way to generate the DOM for video.js const wrapper = document.createElement('div'); @@ -488,19 +496,51 @@ export default React.memo(function VideoJs(props: Props) { return el; } + function detectFileType() { + console.log(`Detecting file type via pre-fetch...`); + return new Promise(async (res, rej) => { + try { + const response = await fetch(source, { method: 'HEAD', cache: 'no-store' }); + + // Temp variables to hold results + let finalType = sourceType; + let finalSource = source; + + // override type if we receive an .m3u8 (transcoded mp4) + // do we need to check if explicitly redirected + // or is checking extension only a safer method + if (response && response.redirected && response.url && response.url.endsWith('m3u8')) { + finalType = 'application/x-mpegURL'; + finalSource = response.url; + } + + console.log(`File type is: ${finalType}`); + + // Modify video source in options + videoJsOptions.sources = [ + { + src: finalSource, + type: finalType, + }, + ]; + + return res(videoJsOptions); + } catch (error) { + console.error(`Failed to pre-fetch video!`); + return rej(error); + } + }); + } + // Initialize video.js function initializeVideoPlayer(el) { - if (!el) { - return; - } + if (!el) return; const vjs = videojs(el, videoJsOptions, () => { const player = playerRef.current; // this seems like a weird thing to have to check for here - if (!player) { - return; - } + if (!player) return; // Add various event listeners to player player.one('play', onInitialPlay); @@ -516,11 +556,38 @@ export default React.memo(function VideoJs(props: Props) { // Replace volume bar with custom LBRY volume bar LbryVolumeBarClass.replaceExisting(player); + // Add reloadSourceOnError plugin + player.reloadSourceOnError({ errorInterval: 10 }); + // initialize mobile UI player.mobileUi(); // Inits mobile version. No-op if Desktop. // I think this is a callback function onPlayerReady(player); + + // Add quality selector to player + player.hlsQualitySelector({ + displayCurrentQuality: true, + }); + + // Add recsys plugin + // TODO: Add an if(odysee.com) around this function to only use recsys on odysee + player.recsys({ + videoId: claimId, + userId: userId, + }); + + // player.aniview(); + + player.ima({ + // id: 'ad_content_video', + vpaidMode: google.ima.ImaSdkSettings.VpaidMode.INSECURE, + adTagUrl: macroUrl, + }); + + // set playsinline for mobile + // TODO: make this better + player.children_[0].setAttribute('playsinline', ''); }); // fixes #3498 (https://github.com/lbryio/lbry-desktop/issues/3498) @@ -533,16 +600,21 @@ export default React.memo(function VideoJs(props: Props) { // This lifecycle hook is only called once (on mount), or when `isAudio` changes. useEffect(() => { const vjsElement = createVideoPlayerDOM(containerRef.current); - const vjsPlayer = initializeVideoPlayer(vjsElement); - // Add reference to player to global scope - window.player = vjsPlayer; + // Detect source file type via pre-fetch (async) + detectFileType().then(() => { + // Initialize Video.js + const vjsPlayer = initializeVideoPlayer(vjsElement); - // Set reference in component state - playerRef.current = vjsPlayer; + // Add reference to player to global scope + window.player = vjsPlayer; - // Add event listener for keyboard shortcuts - window.addEventListener('keydown', handleKeyDown); + // Set reference in component state + playerRef.current = vjsPlayer; + + // Add event listener for keyboard shortcuts + window.addEventListener('keydown', handleKeyDown); + }); // Cleanup return () => { @@ -560,50 +632,28 @@ export default React.memo(function VideoJs(props: Props) { useEffect(() => { // For some reason the video player is responsible for detecting content type this way fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => { - const player = playerRef.current; - - if (!player) { - return; - } - - let type = sourceType; + let finalType = sourceType; let finalSource = source; + // override type if we receive an .m3u8 (transcoded mp4) + // do we need to check if explicitly redirected + // or is checking extension only a safer method if (response && response.redirected && response.url && response.url.endsWith('m3u8')) { - type = 'application/x-mpegURL'; + finalType = 'application/x-mpegURL'; finalSource = response.url; } - // Update player poster - // note: the poster prop seems to return null usually. - if (poster) player.poster(poster); + // Modify video source in options + videoJsOptions.sources = [ + { + src: finalSource, + type: finalType, + }, + ]; // Update player source - player.src({ - src: finalSource, - type: type, - }); - - // set playsinline for mobile - player.children_[0].setAttribute('playsinline', ''); - - // Add quality selector to player - player.hlsQualitySelector({ - displayCurrentQuality: true, - }); - - // Add recsys plugin - // TODO: Add an if(odysee.com) around this function to only use recsys on odysee - player.recsys({ - videoId: claimId, - userId: userId, - }); - - // Update player source - player.src({ - src: finalSource, - type: type, - }); + const player = playerRef.current; + if (!player) return; // PR #5570: Temp workaround to avoid double Play button until the next re-architecture. if (!player.paused()) { @@ -612,6 +662,20 @@ export default React.memo(function VideoJs(props: Props) { }); }, [source, reload]); + // Load IMA3 SDK for aniview + useEffect(() => { + const script = document.createElement('script'); + script.src = `https://imasdk.googleapis.com/js/sdkloader/ima3.js`; + script.async = true; + // $FlowFixMe + document.body.appendChild(script); + + return () => { + // $FlowFixMe + document.body.removeChild(script); + }; + }, [source, reload]); + return ( // $FlowFixMe
diff --git a/ui/component/viewers/videoViewer/view.jsx b/ui/component/viewers/videoViewer/view.jsx index 13aff2fe3..abc9a9bd4 100644 --- a/ui/component/viewers/videoViewer/view.jsx +++ b/ui/component/viewers/videoViewer/view.jsx @@ -148,7 +148,10 @@ function VideoViewer(props: Props) { }, [embedded, videoPlaybackRate]); function doTrackingBuffered(e: Event, data: any) { + console.log(`tracking buffered`, e); + console.log(`Source (tracking buffered): ${source}`); fetch(source, { method: 'HEAD' }).then((response) => { + console.log(`Source Response (tracking buffered)`, response); data.playerPoweredBy = response.headers.get('x-powered-by'); doAnalyticsBuffer(uri, data); }); @@ -179,7 +182,9 @@ function VideoViewer(props: Props) { } else if (autoplaySetting) { setShowAutoplayCountdown(true); } - }, [embedded, setIsEndededEmbed, autoplaySetting, setShowAutoplayCountdown, adUrl, setAdUrl]); + + clearPosition(uri); + }, [embedded, setIsEndededEmbed, autoplaySetting, setShowAutoplayCountdown, adUrl, setAdUrl, clearPosition, uri]); function onPlay() { setIsLoading(false); @@ -198,11 +203,7 @@ function VideoViewer(props: Props) { } function handlePosition(player) { - if (player.ended()) { - clearPosition(uri); - } else { - savePosition(uri, player.currentTime()); - } + savePosition(uri, player.currentTime()); } function restorePlaybackRate(player) { @@ -235,21 +236,12 @@ function VideoViewer(props: Props) { Promise.race([playPromise, timeoutPromise]).catch((error) => { if (typeof error === 'object' && error.name && error.name === 'NotAllowedError') { if (player.autoplay() && !player.muted()) { - player.muted(true); + // player.muted(true); // another version had player.play() } } - - if (PLAY_TIMEOUT_ERROR) { - const retryPlayPromise = player.play(); - Promise.race([retryPlayPromise, timeoutPromise]).catch((error) => { - setIsLoading(false); - setIsPlaying(false); - }); - } else { - setIsLoading(false); - setIsPlaying(false); - } + setIsLoading(false); + setIsPlaying(false); }); } diff --git a/ui/scss/component/_file-render.scss b/ui/scss/component/_file-render.scss index 6eeef4059..0228025fa 100644 --- a/ui/scss/component/_file-render.scss +++ b/ui/scss/component/_file-render.scss @@ -589,9 +589,9 @@ video::-internal-media-controls-overlay-cast-button { .file-render { .video-js { - display: flex; - align-items: center; - justify-content: center; + /*display: flex;*/ + /*align-items: center;*/ + /*justify-content: center;*/ } .vjs-big-play-button { diff --git a/yarn.lock b/yarn.lock index 23a0071b8..6ecba1c5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1266,6 +1266,25 @@ tough-cookie "^2.2.2" tough-cookie-web-storage-store "^1.0.0" +"@hapi/boom@9.x.x": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-9.1.2.tgz#48bd41d67437164a2d636e3b5bc954f8c8dc5e38" + integrity sha512-uJEJtiNHzKw80JpngDGBCGAmWjBtzxDCz17A9NO2zCi8LLBlb5Frpq4pXwyN+2JQMod4pKz5BALwyneCgDg89Q== + dependencies: + "@hapi/hoek" "9.x.x" + +"@hapi/cryptiles@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/cryptiles/-/cryptiles-5.1.0.tgz#655de4cbbc052c947f696148c83b187fc2be8f43" + integrity sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA== + dependencies: + "@hapi/boom" "9.x.x" + +"@hapi/hoek@9.x.x": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131" + integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug== + "@hot-loader/react-dom@^16.13": version "16.13.0" resolved "https://registry.yarnpkg.com/@hot-loader/react-dom/-/react-dom-16.13.0.tgz#de245b42358110baf80aaf47a0592153d4047997" @@ -2981,6 +3000,11 @@ camelcase@^5.0.0, camelcase@^5.2.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" +can-autoplay@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/can-autoplay/-/can-autoplay-3.0.0.tgz#fc2de8f1d41b36f6d860d9336b66841d30f8b62d" + integrity sha512-qQXGGYPWgF8nPjEt305o3TJ/BkN15l6/wG+VU4N93YYXD3OtYkBBx+l5un7ihIk2UU1OLytAVJjW7ZR39j/CAQ== + caniuse-api@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" @@ -5124,7 +5148,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.1: +extend@>=3.0.2, extend@^3.0.0, extend@~3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -7421,6 +7445,11 @@ lodash-es@^4.17.14, lodash-es@^4.2.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -7470,6 +7499,21 @@ lodash.snakecase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" +lodash.template@>=4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.toarray@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" @@ -7482,7 +7526,7 @@ lodash.unset@^4.5.2: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.unset/-/lodash.unset-4.5.2.tgz#370d1d3e85b72a7e1b0cdf2d272121306f23e4ed" -lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.6.1: +lodash@>=4.17.19, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.6.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -12064,6 +12108,14 @@ vfile@^2.0.0: videojs-font "3.2.0" videojs-vtt.js "^0.15.2" +videojs-contrib-ads@^6.6.5, videojs-contrib-ads@^6.9.0: + version "6.9.0" + resolved "https://registry.yarnpkg.com/videojs-contrib-ads/-/videojs-contrib-ads-6.9.0.tgz#c792d6fda77254b277545cc3222352fc653b5833" + integrity sha512-nzKz+jhCGMTYffSNVYrmp9p70s05v6jUMOY3Z7DpVk3iFrWK4Zi/BIkokDWrMoHpKjdmCdKzfJVBT+CrUj6Spw== + dependencies: + global "^4.3.2" + video.js "^6 || ^7" + videojs-contrib-quality-levels@^2.0.9: version "2.0.9" resolved "https://registry.yarnpkg.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-2.0.9.tgz#b5d533d5092a6fc7d29eae1b43e4597d89bd527b" @@ -12084,6 +12136,18 @@ videojs-font@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-ima@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/videojs-ima/-/videojs-ima-1.11.0.tgz#26ad385e388c3da72372298d7d755b001d05a91d" + integrity sha512-ZRoWuGyJ75zamwZgpr0i/gZ6q7Evda/Q6R46gpW88WN7u0ORU7apw/lM1MSG4c3YDXW8LDENgzMAvMZUdifWhg== + dependencies: + "@hapi/cryptiles" "^5.1.0" + can-autoplay "^3.0.0" + extend ">=3.0.2" + lodash ">=4.17.19" + lodash.template ">=4.5.0" + videojs-contrib-ads "^6.6.5" + videojs-logo@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/videojs-logo/-/videojs-logo-2.1.4.tgz#56675b3f95949910bad3c217835ea57aa6fdf212"