Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(project): per media ads #370

Merged
merged 10 commits into from
Nov 8, 2023
4 changes: 2 additions & 2 deletions src/components/Player/Player.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import { render } from '@testing-library/react';

import Player from './Player';

import type { PlaylistItem } from '#types/playlist';
import { renderWithRouter } from '#test/testUtils';

describe('<Player>', () => {
test('renders and matches snapshot', () => {
Expand All @@ -24,7 +24,7 @@ describe('<Player>', () => {
title: 'Test item title',
tracks: [],
} as PlaylistItem;
const { container } = render(<Player playerId="123456" playerLicenseKey="testkey" item={item} onPlay={() => null} onPause={() => null} />);
const { container } = renderWithRouter(<Player playerId="123456" playerLicenseKey="testkey" item={item} onPlay={() => null} onPause={() => null} />);

expect(container).toMatchSnapshot();
});
Expand Down
17 changes: 12 additions & 5 deletions src/components/Player/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { PlaylistItem } from '#types/playlist';
import useEventCallback from '#src/hooks/useEventCallback';
import useOttAnalytics from '#src/hooks/useOttAnalytics';
import { logDev, testId } from '#src/utils/common';
import { useConfigStore } from '#src/stores/ConfigStore';
import type { AdSchedule } from '#types/ad-schedule';

type Props = {
playerId: string;
Expand All @@ -18,6 +18,7 @@ type Props = {
item: PlaylistItem;
startTime?: number;
autostart?: boolean;
adsData?: AdSchedule;
onReady?: (player?: JWPlayer) => void;
onPlay?: () => void;
onPause?: () => void;
Expand All @@ -36,6 +37,7 @@ const Player: React.FC<Props> = ({
playerId,
playerLicenseKey,
item,
adsData,
onReady,
onPlay,
onPause,
Expand All @@ -59,8 +61,6 @@ const Player: React.FC<Props> = ({
const startTimeRef = useRef(startTime);
const setPlayer = useOttAnalytics(item, feedId);

const { adScheduleData } = useConfigStore((s) => s);

const handleBeforePlay = useEventCallback(onBeforePlay);
const handlePlay = useEventCallback(onPlay);
const handlePause = useEventCallback(onPause);
Expand Down Expand Up @@ -161,7 +161,14 @@ const Player: React.FC<Props> = ({

// Player options are untyped
const playerOptions: { [key: string]: unknown } = {
advertising: adScheduleData,
advertising: {
dbudzins marked this conversation as resolved.
Show resolved Hide resolved
...adsData,
// Beta feature
showCountdown: true,
},
timeSlider: {
showAdMarkers: false,
},
aspectratio: false,
controls: true,
displaytitle: false,
Expand Down Expand Up @@ -204,7 +211,7 @@ const Player: React.FC<Props> = ({
if (libLoaded) {
initializePlayer();
}
}, [libLoaded, item, detachEvents, attachEvents, playerId, setPlayer, autostart, adScheduleData, playerLicenseKey, feedId]);
}, [libLoaded, item, detachEvents, attachEvents, playerId, setPlayer, autostart, adsData, playerLicenseKey, feedId]);

useEffect(() => {
return () => {
Expand Down
5 changes: 4 additions & 1 deletion src/containers/PlayerContainer/PlayerContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useContentProtection from '#src/hooks/useContentProtection';
import { getMediaById } from '#src/services/api.service';
import LoadingOverlay from '#components/LoadingOverlay/LoadingOverlay';
import { useSettingsStore } from '#src/stores/SettingsStore';
import { useAds } from '#src/hooks/useAds';

type Props = {
item: PlaylistItem;
Expand Down Expand Up @@ -45,6 +46,7 @@ const PlayerContainer: React.FC<Props> = ({
// data
const { data: playableItem, isLoading } = useContentProtection('media', item.mediaid, (token, drmPolicyId) => getMediaById(item.mediaid, token, drmPolicyId));
const { playerId, playerLicenseKey } = useSettingsStore((s) => s);
const { data: adsData, isLoading: isAdsLoading } = useAds({ mediaId: item?.mediaid });

// state
const [playerInstance, setPlayerInstance] = useState<JWPlayer>();
Expand All @@ -68,7 +70,7 @@ const PlayerContainer: React.FC<Props> = ({

const handlePlaylistItemCallback = usePlaylistItemCallback(liveStartDateTime, liveEndDateTime);

if (!playableItem || isLoading) {
if (!playableItem || isLoading || isAdsLoading) {
return <LoadingOverlay inline />;
}

Expand All @@ -78,6 +80,7 @@ const PlayerContainer: React.FC<Props> = ({
playerLicenseKey={playerLicenseKey}
feedId={feedId}
item={playableItem}
adsData={adsData}
onReady={handleReady}
onFirstFrame={handleFirstFrame}
onPlay={onPlay}
Expand Down
59 changes: 59 additions & 0 deletions src/hooks/useAds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useQuery } from 'react-query';

import { getMediaAds, getAdSchedule } from '#src/services/api.service';
import { useConfigStore } from '#src/stores/ConfigStore';

const CACHE_TIME = 60 * 1000 * 20;

/**
* @deprecated Use {@link useAppBasedAds} instead.
*/
const useLegacyStandaloneAds = ({ adScheduleId, enabled }: { adScheduleId: string | null | undefined; enabled: boolean }) => {
const { isLoading, data } = useQuery(
['ad-schedule', adScheduleId],
async () => {
const adSchedule = await getAdSchedule(adScheduleId);

return adSchedule;
},
{ enabled: enabled && !!adScheduleId, cacheTime: CACHE_TIME, staleTime: CACHE_TIME },
);

return {
isLoading,
data,
};
};

const useAppBasedAds = ({ jsonUrl, mediaId, enabled }: { jsonUrl: string | null | undefined; mediaId: string; enabled: boolean }) => {
const { isLoading, data } = useQuery(
['media-ads', mediaId],
async () => {
// Waiting for `prd` deploy to remove `replace`
const mediaAds = await getMediaAds(jsonUrl?.replace('advertising/site', 'sites') as string, mediaId);

return mediaAds;
},
{ enabled: enabled && !!mediaId, cacheTime: CACHE_TIME, staleTime: CACHE_TIME },
);

return {
isLoading,
data,
};
};

export const useAds = ({ mediaId }: { mediaId: string }) => {
const { adSchedule: adScheduleId, adScheduleUrls } = useConfigStore((s) => s.config);
dbudzins marked this conversation as resolved.
Show resolved Hide resolved

// adScheduleUrls.json prop exists when ad-config is attached to the App Config
const useAppBasedFlow = !!adScheduleUrls?.json;

const { data: mediaAds, isLoading: isMediaAdsLoading } = useAppBasedAds({ jsonUrl: adScheduleUrls?.json, mediaId, enabled: useAppBasedFlow });
const { data: adSchedule, isLoading: isAdScheduleLoading } = useLegacyStandaloneAds({ adScheduleId, enabled: !useAppBasedFlow });

return {
dbudzins marked this conversation as resolved.
Show resolved Hide resolved
isLoading: useAppBasedFlow ? isMediaAdsLoading : isAdScheduleLoading,
data: useAppBasedFlow ? mediaAds : adSchedule,
};
};
15 changes: 14 additions & 1 deletion src/services/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const getPlaylistById = async (id?: string, params: GetPlaylistParams = {
/**
* Get watchlist by playlistId
* @param {string} playlistId
* @param {string[]} mediaIds
* @param {string} [token]
*/
export const getMediaByWatchlist = async (playlistId: string, mediaIds: string[], token?: string): Promise<PlaylistItem[] | undefined> => {
Expand Down Expand Up @@ -242,8 +243,20 @@ export const getAdSchedule = async (id: string | undefined | null): Promise<AdSc
}

const url = import.meta.env.APP_API_BASE_URL + `/v2/advertising/schedules/${id}.json`;
const response = await fetch(url);
const response = await fetch(url, { credentials: 'omit' });
const data = await getDataOrThrow(response);

return data;
};

export const getMediaAds = async (url: string, mediaId: string): Promise<AdSchedule | undefined> => {
const urlWithQuery = addQueryParams(url, {
media_id: mediaId,
});

const response = await fetch(urlWithQuery, { credentials: 'omit' });

const data = (await getDataOrThrow(response)) as AdSchedule;

return data;
};
4 changes: 4 additions & 0 deletions src/services/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ const configSchema: SchemaOf<Config> = object({
description: string().defined(),
analyticsToken: string().nullable(),
adSchedule: string().nullable(),
adScheduleUrls: object({
json: string().notRequired().nullable(),
xml: string().notRequired().nullable(),
}).notRequired(),
assets: object({
banner: string().notRequired().nullable(),
}).notRequired(),
Expand Down
4 changes: 3 additions & 1 deletion test-e2e/tests/live_channel_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ Scenario('I can navigate to live channels from the header', ({ I }) => {
I.see('On Channel 1', locate('div').inside(videoDetailsLocator));
});

Scenario('I can watch the current live program on the live channel screen', async ({ I }) => {
// TODO: add BCL SaaS stream
// eslint-disable-next-line codeceptjs/no-skipped-tests
AntonLantukh marked this conversation as resolved.
Show resolved Hide resolved
Scenario.skip('I can watch the current live program on the live channel screen', async ({ I }) => {
await I.openVideoCard('Channel 1');

I.see('The Daily Show with Trevor Noah: Ears Edition', locate('h2').inside(videoDetailsLocator));
Expand Down
4 changes: 3 additions & 1 deletion types/Config.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { AdScheduleUrls } from '#types/ad-schedule';

/**
* Set config setup changes in both config.services.ts and config.d.ts
* */

export type Config = {
id?: string;
siteName?: string;
description: string;
analyticsToken?: string | null;
adSchedule?: string | null;
adScheduleUrls?: AdScheduleUrls;
integrations: {
cleeng?: Cleeng;
jwp?: JWP;
Expand Down
5 changes: 5 additions & 0 deletions types/ad-schedule.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ export type AdSchedule = {
client: string;
schedule: string;
};

export type AdScheduleUrls = {
json?: string | null;
xml?: string | null;
};