diff --git a/packages/common/src/utils/analytics.ts b/packages/common/src/utils/analytics.ts deleted file mode 100644 index 09a576f85..000000000 --- a/packages/common/src/utils/analytics.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useAccountStore } from '../stores/AccountStore'; -import type { PlaylistItem, Source } from '../../types/playlist'; - -export const attachAnalyticsParams = (item: PlaylistItem) => { - // @todo pass this as param instead of reading from store - const { user } = useAccountStore.getState(); - - const { sources, mediaid } = item; - - const userId = user?.id; - - sources.map((source: Source) => { - const url = new URL(source.file); - - const mediaId = mediaid.toLowerCase(); - const sourceUrl = url.href.toLowerCase(); - - // Attach user_id only for VOD and BCL SaaS Live Streams - const isVOD = sourceUrl === `https://cdn.jwplayer.com/manifests/${mediaId}.m3u8`; - const isBCL = sourceUrl === `https://content.jwplatform.com/live/broadcast/${mediaId}.m3u8`; - - if ((isVOD || isBCL) && userId) { - url.searchParams.set('user_id', userId); - } - - source.file = url.toString(); - }); -}; diff --git a/packages/common/src/utils/configSchema.ts b/packages/common/src/utils/configSchema.ts index e72d1993f..43c2d60af 100644 --- a/packages/common/src/utils/configSchema.ts +++ b/packages/common/src/utils/configSchema.ts @@ -50,6 +50,8 @@ export const configSchema: SchemaOf = object({ description: string().defined(), analyticsToken: string().nullable(), adSchedule: string().nullable(), + adDeliveryMethod: mixed().oneOf(['csai', 'ssai']).notRequired(), + adConfig: string().nullable(), siteId: string().defined(), assets: object({ banner: string().notRequired().nullable(), diff --git a/packages/common/src/utils/sources.ts b/packages/common/src/utils/sources.ts new file mode 100644 index 000000000..c96a35022 --- /dev/null +++ b/packages/common/src/utils/sources.ts @@ -0,0 +1,43 @@ +import type { PlaylistItem, Source } from '@jwp/ott-common/types/playlist'; +import type { Config } from '@jwp/ott-common/types/config'; +import type { Customer } from '@jwp/ott-common/types/account'; + +const isVODManifestType = (sourceUrl: string, baseUrl: string, mediaId: string, extensions: ('m3u8' | 'mpd')[]) => { + return extensions.some((ext) => sourceUrl === `${baseUrl}/manifests/${mediaId}.${ext}`); +}; + +const isBCLManifestType = (sourceUrl: string, baseUrl: string, mediaId: string, extensions: ('m3u8' | 'mpd')[]) => { + return extensions.some((ext) => sourceUrl === `${baseUrl}/live/broadcast/${mediaId}.${ext}`); +}; + +export const getSources = ({ item, baseUrl, config, user }: { item: PlaylistItem; baseUrl: string; config: Config; user: Customer | null }) => { + const { sources, mediaid } = item; + const { adConfig, siteId, adDeliveryMethod } = config; + + const userId = user?.id; + const hasServerAds = !!adConfig && adDeliveryMethod === 'ssai'; + + return sources.map((source: Source) => { + const url = new URL(source.file); + + const sourceUrl = url.href; + + const isBCLManifest = isBCLManifestType(sourceUrl, baseUrl, mediaid, ['m3u8', 'mpd']); + const isVODManifest = isVODManifestType(sourceUrl, baseUrl, mediaid, ['m3u8', 'mpd']); + const isDRM = url.searchParams.has('exp') && url.searchParams.has('sig'); + + // Use SSAI URL for configs with server side ads, DRM is not supported + if (isVODManifest && hasServerAds && !isDRM) { + // Only HLS is supported now + url.href = `${baseUrl}/v2/sites/${siteId}/media/${mediaid}/ssai.m3u8`; + url.searchParams.set('ad_config_id', adConfig); + // Attach user_id only for VOD and BCL SaaS Live Streams (doesn't work with SSAI items) + } else if ((isVODManifest || isBCLManifest) && userId) { + url.searchParams.set('user_id', userId); + } + + source.file = url.toString(); + + return source; + }); +}; diff --git a/packages/common/types/ad-schedule.ts b/packages/common/types/ad-schedule.ts index 4ead8eded..d01c00c1e 100644 --- a/packages/common/types/ad-schedule.ts +++ b/packages/common/types/ad-schedule.ts @@ -8,3 +8,5 @@ export type AdScheduleUrls = { json?: string | null; xml?: string | null; }; + +export type AdDeliveryMethod = 'csai' | 'ssai'; diff --git a/packages/common/types/config.ts b/packages/common/types/config.ts index babd67c44..d3b62cfe9 100644 --- a/packages/common/types/config.ts +++ b/packages/common/types/config.ts @@ -1,6 +1,6 @@ import type { PLAYLIST_TYPE } from '../src/constants'; -import type { AdScheduleUrls } from './ad-schedule'; +import type { AdScheduleUrls, AdDeliveryMethod } from './ad-schedule'; /** * Set config setup changes in both config.services.ts and config.d.ts @@ -11,6 +11,8 @@ export type Config = { description: string; analyticsToken?: string | null; adSchedule?: string | null; + adConfig?: string | null; + adDeliveryMethod?: AdDeliveryMethod; adScheduleUrls?: AdScheduleUrls; integrations: { cleeng?: Cleeng; diff --git a/packages/hooks-react/src/useAds.ts b/packages/hooks-react/src/useAds.ts index b18e05bc4..f5249d8d4 100644 --- a/packages/hooks-react/src/useAds.ts +++ b/packages/hooks-react/src/useAds.ts @@ -7,7 +7,7 @@ import { createURL } from '@jwp/ott-common/src/utils/urlFormatting'; const CACHE_TIME = 60 * 1000 * 20; /** - * @deprecated Use adScheduleUrls.xml form the config instead. + * @deprecated Use ad-config instead. */ const useLegacyStandaloneAds = ({ adScheduleId, enabled }: { adScheduleId: string | null | undefined; enabled: boolean }) => { const apiService = getModule(ApiService); @@ -25,21 +25,24 @@ const useLegacyStandaloneAds = ({ adScheduleId, enabled }: { adScheduleId: strin }; export const useAds = ({ mediaId }: { mediaId: string }) => { - const { adSchedule: adScheduleId, adScheduleUrls } = useConfigStore((s) => s.config); - - // adScheduleUrls.xml prop exists when ad-config is attached to the App Config - const useAppBasedFlow = !!adScheduleUrls?.xml; - - const { data: adSchedule, isLoading: isAdScheduleLoading } = useLegacyStandaloneAds({ adScheduleId, enabled: !useAppBasedFlow }); - const adConfig = { - client: 'vast', - schedule: createURL(adScheduleUrls?.xml || '', { - media_id: mediaId, - }), - }; + const { adSchedule: adScheduleId, adConfig: adConfigId, adScheduleUrls, adDeliveryMethod } = useConfigStore((s) => s.config); + + // We use client side ads only when delivery method is not pointing at server ads + // adConfig and adScheduled can't be enabled at the same time + const useAdConfigFlow = !!adConfigId && adDeliveryMethod !== 'ssai'; + + const { data: adSchedule, isLoading: isAdScheduleLoading } = useLegacyStandaloneAds({ adScheduleId, enabled: !!adScheduleId }); + const adConfig = useAdConfigFlow + ? { + client: 'vast', + schedule: createURL(adScheduleUrls?.xml || '', { + media_id: mediaId, + }), + } + : undefined; return { - isLoading: useAppBasedFlow ? false : isAdScheduleLoading, - data: useAppBasedFlow ? adConfig : adSchedule, + isLoading: useAdConfigFlow ? false : isAdScheduleLoading, + data: useAdConfigFlow ? adConfig : adSchedule, }; }; diff --git a/packages/hooks-react/src/useMediaSources.ts b/packages/hooks-react/src/useMediaSources.ts new file mode 100644 index 000000000..8f344f018 --- /dev/null +++ b/packages/hooks-react/src/useMediaSources.ts @@ -0,0 +1,13 @@ +import { useMemo } from 'react'; +import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; +import { useAccountStore } from '@jwp/ott-common/src/stores/AccountStore'; +import type { PlaylistItem, Source } from '@jwp/ott-common/types/playlist'; +import { getSources } from '@jwp/ott-common/src/utils/sources'; + +/** Modify manifest URLs to handle server ads and analytics params */ +export const useMediaSources = ({ item, baseUrl }: { item: PlaylistItem; baseUrl: string }): Source[] => { + const config = useConfigStore((s) => s.config); + const user = useAccountStore((s) => s.user); + + return useMemo(() => getSources({ item, baseUrl, config, user }), [item, baseUrl, config, user]); +}; diff --git a/packages/ui-react/src/components/Player/Player.tsx b/packages/ui-react/src/components/Player/Player.tsx index f71aee20c..3d2a27503 100644 --- a/packages/ui-react/src/components/Player/Player.tsx +++ b/packages/ui-react/src/components/Player/Player.tsx @@ -7,7 +7,7 @@ import { testId } from '@jwp/ott-common/src/utils/common'; import { logInfo } from '@jwp/ott-common/src/logger'; import useEventCallback from '@jwp/ott-hooks-react/src/useEventCallback'; import useOttAnalytics from '@jwp/ott-hooks-react/src/useOttAnalytics'; -import { attachAnalyticsParams } from '@jwp/ott-common/src/utils/analytics'; +import { useMediaSources } from '@jwp/ott-hooks-react/src/useMediaSources'; import env from '@jwp/ott-common/src/env'; import type { JWPlayer } from '../../../types/jwplayer'; @@ -62,6 +62,7 @@ const Player: React.FC = ({ const backClickRef = useRef(false); const [libLoaded, setLibLoaded] = useState(!!window.jwplayer); const startTimeRef = useRef(startTime); + const sources = useMediaSources({ item, baseUrl: env.APP_API_BASE_URL }); const setPlayer = useOttAnalytics(item, feedId); @@ -183,10 +184,6 @@ const Player: React.FC = ({ playerRef.current = window.jwplayer(playerElementRef.current) as JWPlayer; - // Inject user_id into the CDN analytics - // @todo this currently depends on stores - attachAnalyticsParams(item); - // Player options are untyped const playerOptions: { [key: string]: unknown } = { advertising: { @@ -209,7 +206,7 @@ const Player: React.FC = ({ mute: false, playbackRateControls: true, pipIcon: 'disabled', - playlist: [deepCopy({ ...item, starttime: startTimeRef.current, feedid: feedId })], + playlist: [deepCopy({ ...item, starttime: startTimeRef.current, feedid: feedId, sources })], repeat: false, cast: {}, stretching: 'uniform', @@ -239,7 +236,7 @@ const Player: React.FC = ({ if (libLoaded) { initializePlayer(); } - }, [libLoaded, item, detachEvents, attachEvents, playerId, setPlayer, autostart, adsData, playerLicenseKey, feedId]); + }, [libLoaded, item, detachEvents, attachEvents, playerId, setPlayer, autostart, adsData, playerLicenseKey, sources, feedId]); useEffect(() => { return () => {