diff --git a/app/assets/js/video-ad-policy.ts b/app/assets/js/video-ad-policy.ts new file mode 100644 index 00000000..aff849e2 --- /dev/null +++ b/app/assets/js/video-ad-policy.ts @@ -0,0 +1,30 @@ +export interface PauseAdEligibility { + isAdActive: boolean + isContentEnded: boolean + isProgrammatic: boolean + isRequestPending: boolean + isVisible: boolean + wasShown: boolean +} + +export function getVideoAdSchedule(videoCount: number) { + return { + pauseRoll: videoCount > 0 && videoCount % 2 === 0, + preRoll: videoCount > 3 && videoCount % 3 === 0 + } +} + +export function shouldLoadVideoAds(isPremium: boolean, schedule: ReturnType) { + return !isPremium && (schedule.pauseRoll || schedule.preRoll) +} + +export function isEligiblePauseAd({ + isAdActive, + isContentEnded, + isProgrammatic, + isRequestPending, + isVisible, + wasShown +}: PauseAdEligibility) { + return !isAdActive && !isContentEnded && !isProgrammatic && !isRequestPending && isVisible && !wasShown +} diff --git a/app/components/pages/posts/post/PostMedia.vue b/app/components/pages/posts/post/PostMedia.vue index e04be94f..1b700999 100644 --- a/app/components/pages/posts/post/PostMedia.vue +++ b/app/components/pages/posts/post/PostMedia.vue @@ -2,11 +2,13 @@ import type { IPost, PostMediaType } from '~/assets/js/post.dto' import { vIntersectionObserver } from '@vueuse/components' import { proxyUrl } from '~/assets/js/proxy' + import { getVideoAdSchedule, isEligiblePauseAd, shouldLoadVideoAds } from '~/assets/js/video-ad-policy' const localePath = useLocalePath() const { t } = useI18n() const { isPremium } = useUserData() const { autoplayAnimatedMedia } = useUserSettings() + const { hasInteracted } = useInteractionDetector() const { timesVideoHasRendered } = useEthics() const { wasCurrentPageSSR } = useSSRDetection() const { schedule: scheduleIdleTask } = useIdleTask() @@ -25,15 +27,19 @@ const props = defineProps() type MediaElementRef = HTMLElement | { $el?: Element } | null - type FluidPlayerOptionsWithPlaybackRates = Partial & { - layoutControls?: Partial< - Omit & { - controlBar?: Partial - } - > + type VideoJsPlayer = ReturnType<(typeof import('video.js'))['default']> + type ImaPlugin = { + changeAdTag(adTag: string): void + initializeAdDisplayContainer(): void + requestAds(): void + } + type VideoJsPlayerWithIma = VideoJsPlayer & { + ads(): void + ima: ImaPlugin & ((options: Record) => void) } const mediaElement = shallowRef(null) + const videoElementGeneration = shallowRef(0) const localSrc = shallowRef(props.mediaSrc ?? '') const localPosterSrc = shallowRef(props.mediaPosterSrc ?? undefined) @@ -82,16 +88,36 @@ const triedToLoadWithProxy = shallowRef(false) const triedToLoadPosterWithProxy = shallowRef(false) - let videoPlayer: FluidPlayerInstance | undefined + const IMA_SDK_URL = 'https://imasdk.googleapis.com/js/sdkloader/ima3.js' + const PRE_ROLL_AD_TAG = 'https://s.magsrv.com/splash.php?idzone=5386496' + const PAUSE_ROLL_AD_TAG = 'https://s.magsrv.com/splash.php?idzone=5386214' + const IMA_SDK_TIMEOUT = 15000 + + let videoPlayer: VideoJsPlayerWithIma | undefined let videoPlayerInitPromise: Promise | null = null let videoPlayerIdleScheduled = false let videoPlayerInitTimeout: number | null = null let isUnmounted = false + let playerGeneration = 0 + let isVideoVisible = true + let isProgrammaticPause = false + let isAdActive = false + let pauseAdWasRequested = false + let pauseAdWasShown = false + let videoAdSchedule: ReturnType | null = null + let adsInitPromise: Promise | null = null + let imaSdkPromise: Promise | null = null const isAnimatedMediaLoading = ref(false) const isAnimatedMediaPlaying = ref(false) const mediaHasLoaded = ref(false) + watch(hasInteracted, (hasInteracted) => { + if (hasInteracted) { + initializeVideoAds()?.catch(() => {}) + } + }) + onMounted(() => { const resolvedMediaElement = getResolvedMediaElement() @@ -139,166 +165,180 @@ } }) - async function createVideoPlayer() { - const videoElement = getVideoElement() - - if (!videoElement) { - throw new Error('Media element not found') + function loadImaSdk() { + if (window.google?.ima) { + return Promise.resolve() } - if (!isVideo.value) { - throw new Error('Media is not a video') + if (imaSdkPromise) { + return imaSdkPromise } - const [fluidPlayerModule] = await Promise.all([ - import('fluid-player'), - import('fluid-player/src/css/fluidplayer.css') - ]) + imaSdkPromise = new Promise((resolve, reject) => { + const existingScript = document.querySelector(`script[src="${IMA_SDK_URL}"]`) + const script = existingScript ?? document.createElement('script') + const timeoutId = window.setTimeout(() => finish(new Error('Google IMA SDK load timed out')), IMA_SDK_TIMEOUT) - const initializedVideoElement = getVideoElement() + function finish(loadError?: Error) { + window.clearTimeout(timeoutId) + script.removeEventListener('load', onLoad) + script.removeEventListener('error', onError) - if (isUnmounted || !initializedVideoElement) { - return - } + if (loadError || !window.google?.ima) { + imaSdkPromise = null + script.remove() + reject(loadError ?? new Error('Google IMA SDK unavailable')) + return + } - const fluidPlayer = fluidPlayerModule.default - const adList: NonNullable = [] + resolve() + } - const fluidPlayerOptions: FluidPlayerOptionsWithPlaybackRates = { - layoutControls: { - primaryColor: 'rgba(0, 0, 0, 0.7)', + function onLoad() { + finish() + } - fillToContainer: true, + function onError() { + finish(new Error('Google IMA SDK failed to load')) + } - preload: 'none', + script.addEventListener('load', onLoad, { once: true }) + script.addEventListener('error', onError, { once: true }) - loop: true, + if (!existingScript) { + script.src = IMA_SDK_URL + script.async = true + document.head.append(script) + } + }) - playbackRateEnabled: true, + return imaSdkPromise + } - allowTheatre: false, + async function initializeAds( + player: VideoJsPlayerWithIma, + generation: number, + schedule: ReturnType + ) { + await loadImaSdk() + await Promise.all([import('videojs-ima'), import('videojs-ima/dist/videojs.ima.css')]) - autoRotateFullScreen: true, + if (isUnmounted || generation !== playerGeneration || player.isDisposed()) { + return + } - // Fix: Opening in fullscreen when searching something with "F" - keyboardControl: false, + player.ima({ + adLabel: t('media.adText'), + ...(schedule.preRoll ? { adTagUrl: PRE_ROLL_AD_TAG } : {}), + adsRenderingSettings: { loadVideoTimeout: 3000 }, + autoPlayAdBreaks: true, + contribAdsSettings: { timeout: 3500 }, + disableAdControls: false, + disableCustomPlaybackForIOS10Plus: false, + forceNonLinearFullSlot: false, + numRedirects: 4, + preventLateAdStart: true, + showControlsForJSAds: false, + showCountdown: true, + vastLoadTimeout: 3000, + vpaidMode: window.google!.ima!.ImaSdkSettings.VpaidMode.DISABLED + }) + + player.on('ads-ad-started', () => { + isAdActive = true + + if (pauseAdWasRequested) { + pauseAdWasShown = true + } + }) + player.on(['adended', 'adserror', 'adtimeout'], () => { + isAdActive = false + pauseAdWasRequested = false + }) + player.on('pause', () => requestPauseAd(player, schedule)) + } - controlBar: { - autoHide: true, + function requestPauseAd(player: VideoJsPlayerWithIma, schedule: ReturnType) { + if ( + !schedule.pauseRoll || + !isEligiblePauseAd({ + isAdActive, + isContentEnded: player.ended(), + isProgrammatic: isProgrammaticPause, + isRequestPending: pauseAdWasRequested, + isVisible: isVideoVisible, + wasShown: pauseAdWasShown + }) + ) { + return + } - playbackRates: ['x2', 'x1.5', 'x1', 'x0.75', 'x0.5', 'x0.25'] - }, + pauseAdWasRequested = true - contextMenu: { - controls: true, + try { + player.ima.changeAdTag(PAUSE_ROLL_AD_TAG) + player.ima.requestAds() + } catch { + isAdActive = false + pauseAdWasRequested = false + } + } - links: [ - { - label: t('media.removeAds'), - href: localePath('/premium?utm_source=internal&utm_medium=player-context-menu#pricing') - }, - { - label: t('media.download'), - href: localSrc.value - } - ] - }, - - miniPlayer: { - enabled: false - } - }, + function initializeVideoAds() { + if (adsInitPromise || !hasInteracted.value || !videoPlayer || !videoAdSchedule) { + return adsInitPromise + } - onBeforeXMLHttpRequest(request) { - request.withCredentials = false - }, + const generation = playerGeneration + adsInitPromise = initializeAds(videoPlayer, generation, videoAdSchedule).finally(() => { + adsInitPromise = null + }) - vastOptions: { - adText: t('media.adText'), + return adsInitPromise + } - vastAdvanced: { - /** - * Handle empty VAST - */ - vastVideoEndedCallback() { - const currentVideoElement = getVideoElement() + async function createVideoPlayer() { + const videoElement = getVideoElement() - if (!currentVideoElement?.src.endsWith('/null')) { - return - } + if (!videoElement || !isVideo.value) { + throw new Error('Video element not found') + } - currentVideoElement.src = localSrc.value - videoPlayer?.play() - } - }, + const generation = ++playerGeneration + const videoCount = isPremium.value ? timesVideoHasRendered.value : ++timesVideoHasRendered.value + const schedule = getVideoAdSchedule(videoCount) + const shouldLoadAds = shouldLoadVideoAds(isPremium.value, schedule) + const [{ default: videojs }] = await Promise.all([ + import('video.js'), + import('video.js/dist/video-js.css'), + import('videojs-contrib-ads') + ]) - adList - } + if (isUnmounted || generation !== playerGeneration || getVideoElement() !== videoElement) { + return } - if (!isPremium.value) { - timesVideoHasRendered.value++ - - // Only show pause roll ads every 2 videos - if (timesVideoHasRendered.value % 2 === 0) { - adList.push( - // In-Video Banner - { - roll: 'onPauseRoll', - vastTag: - /** - * ExoClick - * Pros: - * Cons: Low revenue (7) - */ - 'https://s.magsrv.com/splash.php?idzone=5386214' - } - ) - } - // + const player = videojs(videoElement, { + controls: true, + fluid: true, + loop: true, + playbackRates: [0.25, 0.5, 0.75, 1, 1.5, 2], + playsinline: true, + preload: 'none', + responsive: true + }) as VideoJsPlayerWithIma - // Only show preroll ads after 3 videos, and every 3 videos - if (timesVideoHasRendered.value > 3 && timesVideoHasRendered.value % 3 === 0) { - adList.push( - // In-Stream Video - { - roll: 'preRoll', - vastTag: - /** - * ExoClick - * Pros: - * Cons: Low revenue (9) - */ - 'https://s.magsrv.com/splash.php?idzone=5386496' - - /** - * HilltopAds - * Pros: - * Cons: Low revenue (4) - */ - // 'https://ellipticaltrack.com/dCm.FXz/doGMNPv/Z-GhUX/OermX9/u-ZqUEltk/PYTgYBy/ODTZQI5oNHDDEHtdNbjLIS5eNvDhk/0uMGgu?limit=1' - - /** - * Clickadu - * Pros: - * Cons: - */ - // 'https://anewfeedliberty.com/ceef/gdt3g0/tbt/2034767/tlk.xml' - - /** - * AdSession - * Pros: - * Cons: - */ - // 'https://s.eunow4u.com/v1/vast.php?idzone=2310' - } - ) - } + if (shouldLoadAds) { + player.ads() } - videoPlayer = fluidPlayer(initializedVideoElement, fluidPlayerOptions) + player.src({ src: localSrc.value, type: 'video/mp4' }) + videoPlayer = player - // TODO: Handle poster error + if (shouldLoadAds) { + videoAdSchedule = schedule + initializeVideoAds()?.catch(() => {}) + } } function initializeVideoPlayer() { @@ -340,12 +380,25 @@ } function destroyVideoPlayer() { + playerGeneration++ + if (!videoPlayer) { return } - videoPlayer.destroy() + videoPlayer.dispose() videoPlayer = undefined + videoElementGeneration.value++ + videoAdSchedule = null + adsInitPromise = null + isAdActive = false + pauseAdWasRequested = false + pauseAdWasShown = false + } + + function onVideoUserInteraction() { + videoPlayer?.ima?.initializeAdDisplayContainer?.() + initializeVideoPlayer() } async function reloadVideoPlayer(shouldPlay: boolean = false) { @@ -477,13 +530,19 @@ return } + isVideoVisible = entry.isIntersecting + if (entry.isIntersecting) { scheduleVideoPlayerInitialization(isLikelyLcpMedia.value ? 1200 : 1600) return } - if (!entry.isIntersecting) { - videoPlayer?.pause() + if (!entry.isIntersecting && videoPlayer) { + isProgrammaticPause = true + videoPlayer.pause() + window.setTimeout(() => { + isProgrammaticPause = false + }) } } @@ -755,7 +814,8 @@
@@ -767,14 +827,14 @@ :src="localSrc" :style="mediaAspectRatio ? `aspect-ratio: ${mediaAspectRatio};` : undefined" :width="mediaSrcWidthAttribute" - class="h-auto w-full rounded-t-md" + class="video-js h-full! w-full! rounded-t-md" controls loop playsinline preload="none" @error="onMediaError" - @focus="initializeVideoPlayer" - @pointerdown="initializeVideoPlayer" + @focus="onVideoUserInteraction" + @pointerdown="onVideoUserInteraction" @pointerenter="initializeVideoPlayer" />
diff --git a/app/types/videojs-ima.d.ts b/app/types/videojs-ima.d.ts new file mode 100644 index 00000000..1c6b3a3a --- /dev/null +++ b/app/types/videojs-ima.d.ts @@ -0,0 +1,16 @@ +declare module 'videojs-contrib-ads' +declare module 'videojs-ima' + +interface GoogleImaGlobal { + ImaSdkSettings: { + VpaidMode: { + DISABLED: number + } + } +} + +interface Window { + google?: { + ima?: GoogleImaGlobal + } +} diff --git a/nuxt.config.ts b/nuxt.config.ts index ff7c7961..19207f66 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -442,10 +442,9 @@ export default defineNuxtConfig({ // Fix: enable any origin for images 'img-src': ["'self'", 'http:', 'https:', 'data:', 'blob:'], - // Fix: enable fluid player fullscreen eval // @see https://nuxt-security.vercel.app/documentation/advanced/faq#cloudflare // @see https://nuxt-security.vercel.app/documentation/getting-started/configuration#defaults - 'script-src': ["'self'", 'https:', "'unsafe-inline'", "'strict-dynamic'", "'nonce-{{nonce}}'", "'unsafe-eval'"], + 'script-src': ["'self'", 'https:', "'unsafe-inline'", "'strict-dynamic'", "'nonce-{{nonce}}'"], // Fix: enable inline execution 'script-src-attr': ["'unsafe-inline'"], diff --git a/package.json b/package.json index d0f988f4..c400098f 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@vueuse/rxjs": "^14.3.0", "core-js": "^3.49.0", "es-toolkit": "^1.49.0", - "fluid-player": "^3.59.0", + "import-in-the-middle": "^3.3.1", "js-abbreviation-number": "^1.4.0", "nuxt-headlessui": "^1.2.2", "nuxt-schema-org": "^6.2.3", @@ -67,6 +67,9 @@ "sortablejs": "^1.15.7", "tailwindcss": "^4.3.2", "ufo": "^1.6.4", + "video.js": "^8.23.9", + "videojs-contrib-ads": "^7.5.2", + "videojs-ima": "^2.6.0", "vue-sonner": "^2.0.9" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e315eb0..a301864c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,9 +111,9 @@ importers: es-toolkit: specifier: ^1.49.0 version: 1.49.0 - fluid-player: - specifier: ^3.59.0 - version: 3.59.0 + import-in-the-middle: + specifier: ^3.3.1 + version: 3.3.1 js-abbreviation-number: specifier: ^1.4.0 version: 1.4.0 @@ -144,6 +144,15 @@ importers: ufo: specifier: ^1.6.4 version: 1.6.4 + video.js: + specifier: ^8.23.9 + version: 8.23.9 + videojs-contrib-ads: + specifier: ^7.5.2 + version: 7.5.2(video.js@8.23.9) + videojs-ima: + specifier: ^2.6.0 + version: 2.6.0(video.js@8.23.9) vue-sonner: specifier: ^2.0.9 version: 2.0.9(@nuxt/kit@4.4.8(magicast@0.5.3))(@nuxt/schema@4.4.8)(nuxt@4.4.8(@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7))(@parcel/watcher@2.5.6)(@rollup/plugin-babel@6.1.0(@babel/core@7.29.7)(rollup@4.62.2))(@types/node@26.1.1)(@vue/compiler-sfc@3.5.39)(cac@6.7.14)(db0@0.3.4)(esbuild@0.28.1)(eslint@10.6.0(jiti@2.7.0))(ioredis@5.11.1)(lightningcss@1.32.0)(magicast@0.5.3)(optionator@0.9.4)(rolldown@1.1.5)(rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.2))(rollup@4.62.2)(srvx@0.11.21)(terser@5.49.0)(typescript@6.0.3)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.49.0)(yaml@2.9.0))(vue-tsc@3.3.7(typescript@6.0.3))(yaml@2.9.0)) @@ -1434,6 +1443,19 @@ packages: resolution: { integrity: sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA== } + '@hapi/boom@9.1.4': + resolution: + { integrity: sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw== } + + '@hapi/cryptiles@5.1.0': + resolution: + { integrity: sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA== } + engines: { node: '>=12.0.0' } + + '@hapi/hoek@9.3.0': + resolution: + { integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== } + '@headlessui/tailwindcss@0.2.2': resolution: { integrity: sha512-xNe42KjdyA4kfUKLLPGzME9zkH7Q3rOZ5huFihWNWOQFxnItxPB3/67yBI8/qBfY8nwBRx5GHn4VprsoluVMGw== } @@ -3230,29 +3252,6 @@ packages: { integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ== } engines: { node: '>= 10.0.0' } - '@photo-sphere-viewer/core@5.14.3': - resolution: - { integrity: sha512-gSDzM1fuVfWQiDf8Y4AgI+un/wsKmeFO7cY4IFEbjhD1BTXUviSL8GuCXABNfPK2EThZLJqpOXxrbYAhblJrfQ== } - - '@photo-sphere-viewer/equirectangular-video-adapter@5.14.3': - resolution: - { integrity: sha512-jgdFMAGkVgtybq0d+zP3Qbu8SNw1Makw/EOpqyeo1klHK+S5JBRp1ibtq/9NRPtYTlf3Ral03iYR6iDgJuxJbg== } - peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@photo-sphere-viewer/video-plugin': 5.14.3 - - '@photo-sphere-viewer/gyroscope-plugin@5.14.3': - resolution: - { integrity: sha512-5JyDcndnHoJmFEfamzYncslcfDY+9gD/A7wuxLlw0mEkj2S0Vdue414nxI+xgwTTPd6UAvOwM3KWQyWWcE1Vlg== } - peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 - - '@photo-sphere-viewer/video-plugin@5.14.3': - resolution: - { integrity: sha512-yMUd3JG0ROJpJkPXJbdFiIkz+m6lDMRrIeLm/bwXvaX+Q1uSgyX0ipIkJrQnAtdk/IU5fdyqBwfn9ExlQ27c2A== } - peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@pkgjs/parseargs@0.11.0': resolution: { integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== } @@ -4332,6 +4331,22 @@ packages: engines: { node: '>=20' } hasBin: true + '@videojs/http-streaming@3.17.5': + resolution: + { integrity: sha512-PRRQVu0hYW5h0XvRWpIyIpEatpMZzZtf7L1avV89Hnu+69f+0c7oLsSL9jwpg2nbvzGkdX5rQxQkz5Vc7tFR9w== } + engines: { node: '>=8', npm: '>=5' } + peerDependencies: + video.js: ^8.19.0 + + '@videojs/vhs-utils@4.1.2': + resolution: + { integrity: sha512-1Dsv5dtWOq0LP1G37O46FQFA2tKAZON5UMe6dFPxT7JvPhEx8QIXcFQxan8+hHI3fX6aomFRFrUBJNgRYQDSXA== } + engines: { node: '>=8', npm: '>=5' } + + '@videojs/xhr@2.7.0': + resolution: + { integrity: sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ== } + '@vite-pwa/assets-generator@1.0.2': resolution: { integrity: sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg== } @@ -4576,6 +4591,11 @@ packages: peerDependencies: vue: ^3 + '@xmldom/xmldom@0.8.13': + resolution: + { integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw== } + engines: { node: '>=10.0.0' } + JSONStream@1.3.5: resolution: { integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== } @@ -4613,6 +4633,10 @@ packages: resolution: { integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ== } + aes-decrypter@4.0.2: + resolution: + { integrity: sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw== } + agent-base@6.0.2: resolution: { integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== } @@ -4856,18 +4880,6 @@ packages: { integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== } engines: { node: '>= 0.8' } - bcp-47-match@1.0.3: - resolution: - { integrity: sha512-LggQ4YTdjWQSKELZF5JwchnBa1u0pIQSZf5lSdOHEdbVP55h0qICA/FUp3+W99q0xqxYa1ZQizTUH87gecII5w== } - - bcp-47-normalize@1.1.1: - resolution: - { integrity: sha512-jWZ1Jdu3cs0EZdfCkS0UE9Gg01PtxnChjEBySeB+Zo6nkqtFfnvtoQQgP1qU1Oo4qgJgxhTI6Sf9y/pZIhPs0A== } - - bcp-47@1.0.8: - resolution: - { integrity: sha512-Y9y1QNBBtYtv7hcmoX0tR+tUNSFZGZ6OL6vKPObq8BbOhkCoyayF6ogfLTgAli/KuAEbsYHYUNq2AQuY6IuLag== } - bindings@1.5.0: resolution: { integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== } @@ -4975,6 +4987,10 @@ packages: { integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== } engines: { node: '>=6' } + can-autoplay@3.0.2: + resolution: + { integrity: sha512-Ih6wc7yJB4TylS/mLyAW0Dj5Nh3Gftq/g966TcxgvpNCOzlbqTs85srAq7mwIspo4w8gnLCVVGroyCHfh6l9aA== } + caniuse-api@4.0.0: resolution: { integrity: sha512-B0hQ1OLyJuHTQSOWXvwibWqM6DCoqJdvBA6X1S/53bd4XU7LJ1yurIPlrsouol3mw1jh9pGI4ivubSpmJeIqCA== } @@ -5043,10 +5059,6 @@ packages: { integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw== } engines: { node: '>=0.10.0' } - codem-isoboxer@0.3.9: - resolution: - { integrity: sha512-4XOTqEzBWrGOZaMd+sTED2hLpzfBbiQCf1W6OBGkIHqk1D8uwy8WFLazVbdQwfDpQ+vf39lqTGPa9IhWW0roTA== } - color-convert@1.9.3: resolution: { integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== } @@ -5367,10 +5379,6 @@ packages: { integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== } engines: { node: '>=8' } - dashjs@4.7.4: - resolution: - { integrity: sha512-+hldo25QPP3H/NOwqUrvt4uKdMse60/Gsz9AUAnoYfhga8qHWq4nWiojUosOiigbigkDTCAn9ORcvUaKCvmfCA== } - data-view-buffer@1.0.2: resolution: { integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== } @@ -5703,10 +5711,6 @@ packages: resolution: { integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g== } - es6-promise@4.2.8: - resolution: - { integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== } - esbuild@0.25.12: resolution: { integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg== } @@ -5963,15 +5967,15 @@ packages: resolution: { integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw== } + extend@3.0.2: + resolution: + { integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== } + fake-indexeddb@6.2.5: resolution: { integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w== } engines: { node: '>=18' } - fast-deep-equal@2.0.1: - resolution: - { integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w== } - fast-deep-equal@3.1.3: resolution: { integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== } @@ -6111,10 +6115,6 @@ packages: resolution: { integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== } - fluid-player@3.59.0: - resolution: - { integrity: sha512-92VCXEDvIn0ajEXCk7rWfzKFcoKWASZmwRl7vhEDn+sOOosS1qCAW+gcpV3DpzbjjMfB0HEyOMHn6x03L7/o7Q== } - for-each@0.3.5: resolution: { integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== } @@ -6396,10 +6396,6 @@ packages: { integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== } engines: { node: '>= 0.4' } - hls.js@1.6.16: - resolution: - { integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA== } - hookable@5.5.3: resolution: { integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== } @@ -6417,10 +6413,6 @@ packages: { integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== } engines: { node: '>=10' } - html-entities@1.4.0: - resolution: - { integrity: sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== } - html-entities@2.6.0: resolution: { integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ== } @@ -6480,10 +6472,6 @@ packages: resolution: { integrity: sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA== } - immediate@3.0.6: - resolution: - { integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== } - import-in-the-middle@3.3.1: resolution: { integrity: sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA== } @@ -6493,10 +6481,6 @@ packages: resolution: { integrity: sha512-5AUn+QE0UofqNHu5f2Skf6Svukdg4ehOIq8O0EtqIx4jta0CDZYBPqpIHt0zrlUTiFVYlLpeH39DoikXBjPKpA== } - imsc@1.1.5: - resolution: - { integrity: sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ== } - imurmurhash@0.1.4: resolution: { integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== } @@ -6544,14 +6528,6 @@ packages: resolution: { integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg== } - is-alphabetical@1.0.4: - resolution: - { integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== } - - is-alphanumerical@1.0.4: - resolution: - { integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== } - is-array-buffer@3.0.5: resolution: { integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== } @@ -6605,10 +6581,6 @@ packages: { integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== } engines: { node: '>= 0.4' } - is-decimal@1.0.4: - resolution: - { integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== } - is-docker@3.0.0: resolution: { integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== } @@ -6635,6 +6607,10 @@ packages: { integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== } engines: { node: '>=8' } + is-function@1.0.2: + resolution: + { integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ== } + is-generator-function@1.1.2: resolution: { integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== } @@ -6947,10 +6923,6 @@ packages: { integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== } engines: { node: '>= 0.8.0' } - lie@3.1.1: - resolution: - { integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== } - lightningcss-android-arm64@1.32.0: resolution: { integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg== } @@ -7061,10 +7033,6 @@ packages: { integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q== } engines: { node: '>=14' } - localforage@1.10.0: - resolution: - { integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== } - locate-path@2.0.0: resolution: { integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA== } @@ -7124,6 +7092,10 @@ packages: { integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== } engines: { node: '>=10' } + m3u8-parser@7.2.0: + resolution: + { integrity: sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ== } + magic-regexp@0.10.0: resolution: { integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg== } @@ -7277,6 +7249,11 @@ packages: resolution: { integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w== } + mpd-parser@1.4.0: + resolution: + { integrity: sha512-blbA7XpAaOdC/PR0Tu8GiUxlx6CpBuRowQPYV7lhi9Yr9G9+dkPXIUjqjzc/NCn/uZZPopluaJlIYntIEI/VLw== } + hasBin: true + mrmime@2.0.1: resolution: { integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ== } @@ -7294,6 +7271,12 @@ packages: resolution: { integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ== } + mux.js@7.1.0: + resolution: + { integrity: sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA== } + engines: { node: '>=8', npm: '>=5' } + hasBin: true + nanoid@3.3.15: resolution: { integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA== } @@ -7763,6 +7746,11 @@ packages: { integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== } engines: { node: '>=4' } + pkcs7@1.0.4: + resolution: + { integrity: sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ== } + hasBin: true + pkg-types@1.3.1: resolution: { integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== } @@ -8369,10 +8357,6 @@ packages: { integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== } engines: { node: '>= 0.4' } - sax@1.2.1: - resolution: - { integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== } - sax@1.6.0: resolution: { integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA== } @@ -8818,14 +8802,6 @@ packages: { integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== } engines: { node: '>=0.10' } - three@0.179.1: - resolution: - { integrity: sha512-5y/elSIQbrvKOISxpwXCR4sQqHtGiOI+MKLc3SsBdDXA2hz3Mdp3X59aUp8DyybMa34aeBwbFTpdoLJaUDEWSw== } - - three@0.184.0: - resolution: - { integrity: sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg== } - through2@2.0.5: resolution: { integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== } @@ -8979,11 +8955,6 @@ packages: engines: { node: '>=14.17' } hasBin: true - ua-parser-js@1.0.41: - resolution: - { integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug== } - hasBin: true - ufo@1.6.4: resolution: { integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA== } @@ -9263,6 +9234,35 @@ packages: resolution: { integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== } + video.js@8.23.9: + resolution: + { integrity: sha512-3UCqyFW4wIusQu4TuhgRUxqtbzbj0WPT9hps0XIEXST43QBD11zb+bG7tBPrinrZztwVYdXCiOBo2pcGj9L05w== } + + videojs-contrib-ads@7.5.2: + resolution: + { integrity: sha512-hrLnWwAVL0CJJPFNuWR0jV+SpW/TWQx7nQkZxMVn2CWZZGMZe2fowtUfjf8U9gozTfM09wKBvHn1mtNg9m3VPg== } + engines: { node: '>=8', npm: '>=5' } + peerDependencies: + video.js: ^8.11.6 + + videojs-contrib-quality-levels@4.1.0: + resolution: + { integrity: sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA== } + engines: { node: '>=16', npm: '>=8' } + peerDependencies: + video.js: ^8 + + videojs-font@4.2.0: + resolution: + { integrity: sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ== } + + videojs-ima@2.6.0: + resolution: + { integrity: sha512-/OxGZfDSWxhGHllAyzco51yWHMKMTrJLZBM7gAMjX5VF/pqFobH9QGUy4V+DNiHkuv18NEoneP3NhpWSgaTpWw== } + engines: { node: '>=0.8.0' } + peerDependencies: + video.js: ^5.19.2 || ^6 || ^7 || ^8 + videojs-vtt.js@0.15.5: resolution: { integrity: sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ== } @@ -10927,6 +10927,16 @@ snapshots: '@formkit/auto-animate@0.9.0': {} + '@hapi/boom@9.1.4': + dependencies: + '@hapi/hoek': 9.3.0 + + '@hapi/cryptiles@5.1.0': + dependencies: + '@hapi/boom': 9.1.4 + + '@hapi/hoek@9.3.0': {} + '@headlessui/tailwindcss@0.2.2(tailwindcss@4.3.2)': dependencies: tailwindcss: 4.3.2 @@ -12380,23 +12390,6 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.6 '@parcel/watcher-win32-x64': 2.5.6 - '@photo-sphere-viewer/core@5.14.3': - dependencies: - three: 0.184.0 - - '@photo-sphere-viewer/equirectangular-video-adapter@5.14.3(@photo-sphere-viewer/core@5.14.3)(@photo-sphere-viewer/video-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3))': - dependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@photo-sphere-viewer/video-plugin': 5.14.3(@photo-sphere-viewer/core@5.14.3) - - '@photo-sphere-viewer/gyroscope-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)': - dependencies: - '@photo-sphere-viewer/core': 5.14.3 - - '@photo-sphere-viewer/video-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)': - dependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@pkgjs/parseargs@0.11.0': optional: true @@ -13182,6 +13175,28 @@ snapshots: - rollup - supports-color + '@videojs/http-streaming@3.17.5(video.js@8.23.9)': + dependencies: + '@babel/runtime': 7.29.7 + '@videojs/vhs-utils': 4.1.2 + aes-decrypter: 4.0.2 + global: 4.4.0 + m3u8-parser: 7.2.0 + mpd-parser: 1.4.0 + mux.js: 7.1.0 + video.js: 8.23.9 + + '@videojs/vhs-utils@4.1.2': + dependencies: + '@babel/runtime': 7.29.7 + global: 4.4.0 + + '@videojs/xhr@2.7.0': + dependencies: + '@babel/runtime': 7.29.7 + global: 4.4.0 + is-function: 1.0.2 + '@vite-pwa/assets-generator@1.0.2': dependencies: cac: 6.7.14 @@ -13436,6 +13451,8 @@ snapshots: dependencies: vue: 3.5.39(typescript@6.0.3) + '@xmldom/xmldom@0.8.13': {} + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -13459,6 +13476,13 @@ snapshots: add-stream@1.0.0: {} + aes-decrypter@4.0.2: + dependencies: + '@babel/runtime': 7.29.7 + '@videojs/vhs-utils': 4.1.2 + global: 4.4.0 + pkcs7: 1.0.4 + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -13656,19 +13680,6 @@ snapshots: dependencies: safe-buffer: 5.1.2 - bcp-47-match@1.0.3: {} - - bcp-47-normalize@1.1.1: - dependencies: - bcp-47: 1.0.8 - bcp-47-match: 1.0.3 - - bcp-47@1.0.8: - dependencies: - is-alphabetical: 1.0.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -13765,6 +13776,8 @@ snapshots: camelcase@5.3.1: {} + can-autoplay@3.0.2: {} + caniuse-api@4.0.0: dependencies: browserslist: 4.28.5 @@ -13818,8 +13831,6 @@ snapshots: cluster-key-slot@1.1.1: {} - codem-isoboxer@0.3.9: {} - color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -14106,19 +14117,6 @@ snapshots: dargs@7.0.0: {} - dashjs@4.7.4: - dependencies: - bcp-47-match: 1.0.3 - bcp-47-normalize: 1.1.1 - codem-isoboxer: 0.3.9 - es6-promise: 4.2.8 - fast-deep-equal: 2.0.1 - html-entities: 1.4.0 - imsc: 1.1.5 - localforage: 1.10.0 - path-browserify: 1.0.1 - ua-parser-js: 1.0.41 - data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -14410,8 +14408,6 @@ snapshots: es-toolkit@1.49.0: {} - es6-promise@4.2.8: {} - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -14723,9 +14719,9 @@ snapshots: exsolve@1.1.0: {} - fake-indexeddb@6.2.5: {} + extend@3.0.2: {} - fast-deep-equal@2.0.1: {} + fake-indexeddb@6.2.5: {} fast-deep-equal@3.1.3: {} @@ -14839,18 +14835,6 @@ snapshots: flatted@3.4.2: {} - fluid-player@3.59.0: - dependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@photo-sphere-viewer/equirectangular-video-adapter': 5.14.3(@photo-sphere-viewer/core@5.14.3)(@photo-sphere-viewer/video-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)) - '@photo-sphere-viewer/gyroscope-plugin': 5.14.3(@photo-sphere-viewer/core@5.14.3) - '@photo-sphere-viewer/video-plugin': 5.14.3(@photo-sphere-viewer/core@5.14.3) - dashjs: 4.7.4 - es6-promise: 4.2.8 - hls.js: 1.6.16 - three: 0.179.1 - videojs-vtt.js: 0.15.5 - for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -15092,8 +15076,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hls.js@1.6.16: {} - hookable@5.5.3: {} hookable@6.1.1: {} @@ -15104,8 +15086,6 @@ snapshots: dependencies: lru-cache: 6.0.0 - html-entities@1.4.0: {} - html-entities@2.6.0: {} http-errors@2.0.1: @@ -15148,8 +15128,6 @@ snapshots: image-meta@0.2.2: {} - immediate@3.0.6: {} - import-in-the-middle@3.3.1: dependencies: cjs-module-lexer: 2.2.0 @@ -15174,10 +15152,6 @@ snapshots: - vite - webpack - imsc@1.1.5: - dependencies: - sax: 1.2.1 - imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -15251,13 +15225,6 @@ snapshots: iron-webcrypto@1.2.1: {} - is-alphabetical@1.0.4: {} - - is-alphanumerical@1.0.4: - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -15306,8 +15273,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-decimal@1.0.4: {} - is-docker@3.0.0: {} is-document.all@1.0.0: @@ -15322,6 +15287,8 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-function@1.0.2: {} + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 @@ -15531,10 +15498,6 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lie@3.1.1: - dependencies: - immediate: 3.0.6 - lightningcss-android-arm64@1.32.0: optional: true @@ -15624,10 +15587,6 @@ snapshots: pkg-types: 2.3.1 quansync: 0.2.11 - localforage@1.10.0: - dependencies: - lie: 3.1.1 - locate-path@2.0.0: dependencies: p-locate: 2.0.0 @@ -15670,6 +15629,12 @@ snapshots: dependencies: yallist: 4.0.0 + m3u8-parser@7.2.0: + dependencies: + '@babel/runtime': 7.29.7 + '@videojs/vhs-utils': 4.1.2 + global: 4.4.0 + magic-regexp@0.10.0: dependencies: estree-walker: 3.0.3 @@ -15805,6 +15770,13 @@ snapshots: module-details-from-path@1.0.4: {} + mpd-parser@1.4.0: + dependencies: + '@babel/runtime': 7.29.7 + '@videojs/vhs-utils': 4.1.2 + '@xmldom/xmldom': 0.8.13 + global: 4.4.0 + mrmime@2.0.1: {} ms@2.0.0: {} @@ -15813,6 +15785,11 @@ snapshots: muggle-string@0.4.1: {} + mux.js@7.1.0: + dependencies: + '@babel/runtime': 7.29.7 + global: 4.4.0 + nanoid@3.3.15: {} nanotar@0.3.0: {} @@ -16579,6 +16556,10 @@ snapshots: pify@3.0.0: {} + pkcs7@1.0.4: + dependencies: + '@babel/runtime': 7.29.7 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -17068,8 +17049,6 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 - sax@1.2.1: {} - sax@1.6.0: {} scslre@0.3.0: @@ -17536,10 +17515,6 @@ snapshots: text-extensions@1.9.0: {} - three@0.179.1: {} - - three@0.184.0: {} - through2@2.0.5: dependencies: readable-stream: 2.3.8 @@ -17648,8 +17623,6 @@ snapshots: typescript@6.0.3: {} - ua-parser-js@1.0.41: {} - ufo@1.6.4: {} uglify-js@3.19.3: @@ -17883,6 +17856,41 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + video.js@8.23.9: + dependencies: + '@babel/runtime': 7.29.7 + '@videojs/http-streaming': 3.17.5(video.js@8.23.9) + '@videojs/vhs-utils': 4.1.2 + '@videojs/xhr': 2.7.0 + aes-decrypter: 4.0.2 + global: 4.4.0 + m3u8-parser: 7.2.0 + mpd-parser: 1.4.0 + mux.js: 7.1.0 + videojs-contrib-quality-levels: 4.1.0(video.js@8.23.9) + videojs-font: 4.2.0 + videojs-vtt.js: 0.15.5 + + videojs-contrib-ads@7.5.2(video.js@8.23.9): + dependencies: + global: 4.4.0 + video.js: 8.23.9 + + videojs-contrib-quality-levels@4.1.0(video.js@8.23.9): + dependencies: + global: 4.4.0 + video.js: 8.23.9 + + videojs-font@4.2.0: {} + + videojs-ima@2.6.0(video.js@8.23.9): + dependencies: + '@hapi/cryptiles': 5.1.0 + can-autoplay: 3.0.2 + extend: 3.0.2 + video.js: 8.23.9 + videojs-contrib-ads: 7.5.2(video.js@8.23.9) + videojs-vtt.js@0.15.5: dependencies: global: 4.4.0 diff --git a/sentry.client.options.ts b/sentry.client.options.ts index 317498f1..13be59bb 100644 --- a/sentry.client.options.ts +++ b/sentry.client.options.ts @@ -67,7 +67,6 @@ const denyUrls: RegExp[] = [ */ // Specific files /\/js\/popunder\.js/, - /\/fluid-player\//i, // Random plugins and extensions. /^resource:\/\//i, diff --git a/test/assets/video-ad-policy.test.ts b/test/assets/video-ad-policy.test.ts new file mode 100644 index 00000000..739bc525 --- /dev/null +++ b/test/assets/video-ad-policy.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest' +import { getVideoAdSchedule, isEligiblePauseAd, shouldLoadVideoAds } from '../../app/assets/js/video-ad-policy' + +describe('getVideoAdSchedule', () => { + it.each([ + [1, false, false], + [2, true, false], + [3, false, false], + [4, true, false], + [5, false, false], + [6, true, true] + ])('preserves the existing cadence for video %i', (videoCount, pauseRoll, preRoll) => { + expect(getVideoAdSchedule(videoCount)).toEqual({ pauseRoll, preRoll }) + }) + + it('does not schedule ads before a video is counted', () => { + expect(getVideoAdSchedule(0)).toEqual({ pauseRoll: false, preRoll: false }) + }) +}) + +describe('shouldLoadVideoAds', () => { + it.each([ + [false, { pauseRoll: false, preRoll: false }, false], + [false, { pauseRoll: true, preRoll: false }, true], + [false, { pauseRoll: false, preRoll: true }, true], + [true, { pauseRoll: true, preRoll: true }, false] + ])('returns %s for premium=%s and schedule=%o', (isPremium, schedule, expected) => { + expect(shouldLoadVideoAds(isPremium, schedule)).toBe(expected) + }) +}) + +describe('isEligiblePauseAd', () => { + it('allows a visible user-initiated content pause', () => { + expect( + isEligiblePauseAd({ + isAdActive: false, + isContentEnded: false, + isProgrammatic: false, + isRequestPending: false, + isVisible: true, + wasShown: false + }) + ).toBe(true) + }) + + it.each([ + ['an ad is active', { isAdActive: true }], + ['content ended', { isContentEnded: true }], + ['the pause is programmatic', { isProgrammatic: true }], + ['an ad request is pending', { isRequestPending: true }], + ['the player is offscreen', { isVisible: false }], + ['the pause ad was already shown', { wasShown: true }] + ])('rejects the pause when %s', (_reason, override) => { + expect( + isEligiblePauseAd({ + isAdActive: false, + isContentEnded: false, + isProgrammatic: false, + isRequestPending: false, + isVisible: true, + wasShown: false, + ...override + }) + ).toBe(false) + }) +}) diff --git a/test/pages/posts.test.ts b/test/pages/posts.test.ts index f9ed451e..e2d2f4bc 100644 --- a/test/pages/posts.test.ts +++ b/test/pages/posts.test.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'node:fs' import { describe, expect, it } from 'vitest' import { setup, url } from '@nuxt/test-utils' import { mockPostsPage0, mockPostsPage1, mockPostsPageWithOfflineMedia } from './posts.mock-data' @@ -56,6 +57,35 @@ async function getPostImageSrc(page: TrackedPage, testId: string) { return page.evaluate((id) => document.querySelector(`[data-testid="${id}"] img`)?.getAttribute('src') ?? null, testId) } +describe('PostMedia video player', () => { + const component = readFileSync('app/components/pages/posts/post/PostMedia.vue', 'utf8') + + it('fills the reserved aspect-ratio slot before Video.js initializes', () => { + expect(component).toContain('class="h-full w-full"') + expect(component).toContain('class="video-js h-full! w-full! rounded-t-md"') + }) + + it('initializes contrib-ads before assigning the content source', () => { + const createPlayer = component.slice( + component.indexOf('async function createVideoPlayer()'), + component.indexOf('function initializeVideoPlayer()') + ) + + expect(createPlayer).toContain("import('videojs-contrib-ads')") + expect(createPlayer).toContain('player.ads()') + expect(createPlayer.indexOf('player.ads()')).toBeLessThan(createPlayer.indexOf('player.src(')) + expect(createPlayer).not.toContain('sources:') + }) + + it('loads the IMA SDK before importing its Video.js plugin', () => { + expect(component).toMatch(/await loadImaSdk\(\)[\s\S]*import\('videojs-ima'\)/) + }) + + it('allows the hosted IMA SDK enough time to load on slower connections', () => { + expect(component).toContain('const IMA_SDK_TIMEOUT = 15000') + }) +}) + describe('/', async () => { await setup(defaultSetupConfig)