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

Experiment/google ima csai ads #986

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7d667c7
WIP mux-video + google ima
cjpillsbury Sep 17, 2024
95a411d
example PLUS HACK IN ADTAGURL IN PLAYER TEMPLATE.
cjpillsbury Sep 17, 2024
8c69a44
ad basic api for ad break times.
cjpillsbury Sep 17, 2024
486bda3
WIP add basic impl for ad tag url to mux-player (probably move to sep…
cjpillsbury Sep 17, 2024
08a933e
first pass on type defs for google ima sdk.
cjpillsbury Sep 17, 2024
3dd2ddd
more types cleanup and minor changes.
cjpillsbury Sep 17, 2024
453f62e
cleanup google ima types for external consumption and builds (e.g. mu…
cjpillsbury Sep 17, 2024
facc26a
more typescriptrobatics.
cjpillsbury Sep 17, 2024
04b0a48
chore: update vanilla example page title for google ima usage.
cjpillsbury Sep 17, 2024
e9698d1
try to see if this fixes silly template test fails.
cjpillsbury Sep 17, 2024
41d44b3
volume control for ads.
cjpillsbury Sep 17, 2024
5d5367e
basic autoplay and ready state for ads
cjpillsbury Sep 17, 2024
e33497e
basic postroll impl
cjpillsbury Sep 17, 2024
dd08e6a
cleanup ad break code.
cjpillsbury Sep 18, 2024
d0fdc37
ad state and various cleanups.
cjpillsbury Sep 19, 2024
7531af4
prove out mux player ad-specific UI.
cjpillsbury Sep 19, 2024
9097f4c
update vanilla ads example to demo new UI.
cjpillsbury Sep 19, 2024
bfaf5d0
no more types to pull in so removing presumptuous cp from playback-core.
cjpillsbury Sep 19, 2024
ed5699c
add resizing logic for IMA crud.
cjpillsbury Sep 23, 2024
9c1f484
fix linting errors
cjpillsbury Sep 23, 2024
a85c0c4
force exit pip on ad play.
cjpillsbury Sep 23, 2024
2bf4ce7
account for user active+inactive (NOTE: Needs followup for ad clicks).
cjpillsbury Sep 23, 2024
131beb1
more acrobatics for different playback scenarios
cjpillsbury Sep 23, 2024
e9a2b7c
bug fix for ad counter.
cjpillsbury Sep 23, 2024
115c139
minor cleanup
cjpillsbury Sep 24, 2024
d104c0b
for now no autohide of any controls during adbreak.
cjpillsbury Sep 25, 2024
84c5b06
general cleanup using more official APIs instead of data structures.
cjpillsbury Sep 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions examples/vanilla-ts-esm/public/mux-player-google-ima.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>&lt;mux-player&gt; Google IMA CSAI example</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css" />
<link rel="stylesheet" href="./styles.css" />
<script type="module" src="./dist/mux-player.js"></script>
<script type="text/javascript" src="https://imasdk.googleapis.com/js/sdkloader/ima3.js"></script>
<style>
mux-player {
display: block;
width: 100%;
margin: 1rem 0 2rem;
background-color: #000;
}

mux-player:not([audio]) {
aspect-ratio: 16 / 9;
}
</style>
</head>
<body>
<header>
<div class="left-header">
<a class="mux-logo" href="https://www.mux.com/player" target="_blank">
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="
https://user-images.githubusercontent.com/360826/233653989-11cd8603-c20f-4008-8bf7-dc15b743c52b.svg
"
/>
<source
media="(prefers-color-scheme: light)"
srcset="
https://user-images.githubusercontent.com/360826/233653583-50dda726-cbe7-4182-a113-059a91ae83e6.svg
"
/>
<img
alt="Mux Logo"
src="https://user-images.githubusercontent.com/360826/233653583-50dda726-cbe7-4182-a113-059a91ae83e6.svg"
/>
</picture>
</a>
<h1><a href="/">Elements</a></h1>
</div>
<div class="right-header">
<a class="github-logo" href="https://github.com/muxinc/elements" target="_blank">
<img width="32" height="32" src="./images/github-logo.svg" alt="Github logo" />
</a>
</div>
</header>

<!--
single preroll skippable
adtagurl="https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_preroll_skippable&sz=640x480&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator="

VMAP - Pre-roll Single Ad, Mid-roll Standard Pod with 3 ads, Post-roll Single Ad
adtagurl="https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostpod&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator="
-->
<mux-player
id="muxPlayer"
title="Big Buck Bunny"
stream-type="on-demand"
playback-id="VcmKA6aqzIzlg3MayLJDnbF55kX00mds028Z65QxvBYaA"
muted
adtagurl="https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostpod&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator="
></mux-player>

<a href="../">Browse Elements</a>
</body>
</html>
72 changes: 71 additions & 1 deletion packages/mux-player/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ const PlayerAttributes = {
NO_VOLUME_PREF: 'no-volume-pref',
CAST_RECEIVER: 'cast-receiver',
NO_TOOLTIPS: 'no-tooltips',
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
AD_TAG_URL: 'adtagurl',
};

const ThemeAttributeNames = [
Expand Down Expand Up @@ -172,6 +174,14 @@ function getProps(el: MuxPlayerElement, state?: any): MuxTemplateProps {
// NOTE: since the attribute value is used as the "source of truth" for the property getter,
// moving this below the `...state` spread so it resolves to the default value when unset (CJP)
extraSourceParams: el.extraSourceParams,
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
adTagUrl: el.adTagUrl,
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
adBreak: el.adBreak,
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
adBreakTotalAds: el.adBreakTotalAds,
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
adBreakAdPosition: el.adBreakAdPosition,
};

return props;
Expand Down Expand Up @@ -365,6 +375,35 @@ class MuxPlayerElement extends VideoApiElement implements MuxPlayerElement {

// NOTE: Make sure we re-render when <source> tags are appended so hasSrc is updated.
this.media?.addEventListener('loadstart', () => this.#render());

/** @TODO remove me when migrated to media chrome */
this.media?.addEventListener('adbreakchange', () => {
// MediaUIEvents.MEDIA_EXIT_PIP_REQUEST
this.mediaController?.dispatchEvent(new CustomEvent('mediaexitpiprequest'));
this.#render();
});
this.media?.addEventListener('adbreakadpositionchange', () => {
this.#render();
});
this.media?.addEventListener('adbreaktotaladschange', () => {
this.#render();
});
this.mediaController?.addEventListener('mediaisfullscreen', () => {
const { mediaIsFullscreen = false } = this.mediaController?.mediaStore.getState() ?? {};
/** @TODO Figure out API design (CJP) */
if (this.media) {
this.media.mediaIsFullscreen = mediaIsFullscreen;
}
});

/** @TODO Tests for user inactive crud. remove before merging (CJP) */
// this.media?.addEventListener('pointermove', () => {
// console.log('POINTER MOVING MEDIA');
// });

// this.mediaController?.addEventListener('pointermove', () => {
// console.log('POINTER MOVING MEDIA CONTROLLER');
// });
}

#setupCSSProperties() {
Expand Down Expand Up @@ -397,7 +436,7 @@ class MuxPlayerElement extends VideoApiElement implements MuxPlayerElement {
}

connectedCallback() {
const muxVideo = this.shadowRoot?.querySelector('mux-video') as MuxVideoElement;
const muxVideo = this.shadowRoot?.querySelector('mux-video') as unknown as MuxVideoElement;
if (muxVideo) {
muxVideo.metadata = getMetadataFromAttrs(this);
}
Expand Down Expand Up @@ -1743,6 +1782,37 @@ class MuxPlayerElement extends VideoApiElement implements MuxPlayerElement {
}
this.setAttribute(PlayerAttributes.NO_TOOLTIPS, '');
}

/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
get adTagUrl() {
return this.media?.adTagUrl ?? this.getAttribute(PlayerAttributes.AD_TAG_URL) ?? undefined;
}

/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
set adTagUrl(val: string | undefined) {
if (val === this.adTagUrl) return;

if (val) {
this.setAttribute(PlayerAttributes.AD_TAG_URL, val);
} else {
this.removeAttribute(PlayerAttributes.AD_TAG_URL);
}
}

/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
get adBreak() {
return this.media?.adBreak ?? false;
}

/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
get adBreakTotalAds() {
return this.media?.adBreakTotalAds;
}

/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
get adBreakAdPosition() {
return this.media?.adBreakAdPosition;
}
}

export function getVideoAttribute(el: MuxPlayerElement, name: string) {
Expand Down
155 changes: 155 additions & 0 deletions packages/mux-player/src/media-chrome/ads/media-ad-count-display.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { MediaTextDisplay } from 'media-chrome/dist/media-text-display.js';
import { getNumericAttr, getStringAttr, setNumericAttr, setStringAttr } from 'media-chrome/dist/utils/element-utils.js';
import { globalThis } from 'media-chrome/dist/utils/server-safe-globals.js';
import { MediaUIAttributes as MediaUIAttributesBase } from 'media-chrome/dist/constants.js';
// import { nouns } from 'media-chrome/dist/labels/labels.js';

const MediaUIAttributes = {
...MediaUIAttributesBase,
MEDIA_AD_BREAK_TOTAL_ADS: 'mediaadbreaktotalads',
MEDIA_AD_BREAK_AD_POSITION: 'mediaadbreakadposition',
} as const;

export const Attributes = {
PREFIX: 'prefix',
};

const CombinedAttributes = [
...Object.values(Attributes),
MediaUIAttributes.MEDIA_AD_BREAK_TOTAL_ADS,
MediaUIAttributes.MEDIA_AD_BREAK_AD_POSITION,
];

// Todo: Use data locals: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString

const DEFAULT_COUNT_SEP = 'of';
const DEFAULT_PREFIX = 'Advertisement';

const formatLabel = (el: MediaAdCountDisplay, { countSep = DEFAULT_COUNT_SEP } = {}): string => {
const prefixPart = el.prefix ? `${el.prefix}: ` : '';
return `${prefixPart}${el.mediaAdBreakAdPosition} ${countSep} ${el.mediaAdBreakTotalAds}`;
};

// const DEFAULT_MISSING_TIME_PHRASE = 'video not loaded, unknown time.';

const updateAriaValueText = (el: MediaAdCountDisplay): void => {
const fullPhrase = formatLabel(el);
el.setAttribute('aria-valuetext', fullPhrase);
};

/**
* @attr {string} prefix - the prefix string for the display. 'Advertisement' by default.
* @attr {number} mediaadbreaktotalads - (read-only) total number of ads in the current ad break
* @attr {number} mediaadbreakadposition - (read-only) current ad index playing in the current ad break
*/
class MediaAdCountDisplay extends MediaTextDisplay {
#slot: HTMLSlotElement;

static get observedAttributes(): string[] {
return [...super.observedAttributes, ...CombinedAttributes, 'disabled'];
}

constructor() {
super();

this.#slot = this.shadowRoot?.querySelector('slot') as HTMLSlotElement;
this.#slot.innerHTML = `${formatLabel(this)}`;
}

connectedCallback(): void {
if (!this.hasAttribute('disabled')) {
this.enable();
}

/** @TODO Implement these */
this.setAttribute('role', 'progressbar');
this.setAttribute('aria-label', 'FILL ME IN');

super.connectedCallback();
}

disconnectedCallback(): void {
this.disable();
super.disconnectedCallback();
}

attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string | null): void {
if (CombinedAttributes.includes(attrName)) {
this.update();
} else if (attrName === 'disabled' && newValue !== oldValue) {
if (newValue == null) {
this.enable();
} else {
this.disable();
}
}

super.attributeChangedCallback(attrName, oldValue, newValue);
}

enable(): void {
this.tabIndex = 0;
}

disable(): void {
this.tabIndex = -1;
}

// Own props

/**
* Describe me
*/
get prefix(): string {
return getStringAttr(this, Attributes.PREFIX, DEFAULT_PREFIX);
}

set prefix(val: string | undefined) {
/** @TODO inaccurate type def in media chrome. Accepts/expects nullish. (CJP) */
/** @ts-ignore */
setStringAttr(this, Attributes.PREFIX, val);
}

// Props derived from media UI attributes

/**
* Describe me
*/
get mediaAdBreakTotalAds(): number | undefined {
return getNumericAttr(this, MediaUIAttributes.MEDIA_AD_BREAK_TOTAL_ADS);
}

set mediaAdBreakTotalAds(val: number | undefined) {
/** @TODO inaccurate type def in media chrome. Accepts/expects nullish. (CJP) */
/** @ts-ignore */
setNumericAttr(this, MediaUIAttributes.MEDIA_AD_BREAK_TOTAL_ADS, val);
}

/**
* Describe me
*/
get mediaAdBreakAdPosition(): number | undefined {
return getNumericAttr(this, MediaUIAttributes.MEDIA_AD_BREAK_AD_POSITION);
}

set mediaAdBreakAdPosition(val: number | undefined) {
/** @TODO inaccurate type def in media chrome. Accepts/expects nullish. (CJP) */
/** @ts-ignore */
setNumericAttr(this, MediaUIAttributes.MEDIA_AD_BREAK_AD_POSITION, val);
}

update(): void {
const label = formatLabel(this);
updateAriaValueText(this);
// Only update if it changed, timeupdate events are called a few times per second.
if (label !== this.#slot.innerHTML) {
this.#slot.innerHTML = label;
}
}
}

if (!globalThis.customElements.get('media-ad-count-display')) {
globalThis.customElements.define('media-ad-count-display', MediaAdCountDisplay);
}

export default MediaAdCountDisplay;
9 changes: 9 additions & 0 deletions packages/mux-player/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'media-chrome/dist/media-theme-element.js';
// @ts-ignore
import cssStr from './styles.css';
import './dialog';
import './media-chrome/ads/media-ad-count-display';
import { getStreamTypeFromAttr } from './helpers';
import { html } from './html';
import { i18n, stylePropsToString } from './utils';
Expand Down Expand Up @@ -83,6 +84,13 @@ export const partsListStr = Object.values(Parts).join(', ');
export const content = (props: MuxTemplateProps) => html`
<media-theme
template="${props.themeTemplate || false}"
mediaadbreak="${/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */ props.adBreak ?? false}"
mediaadbreaktotalads="${
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */ props.adBreakTotalAds ?? false
}"
mediaadbreakadposition="${
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */ props.adBreakAdPosition ?? false
}"
defaultstreamtype="${props.defaultStreamType ?? false}"
hotkeys="${getHotKeys(props) || false}"
nohotkeys="${props.noHotKeys || !props.hasSrc || props.isDialogOpen || false}"
Expand Down Expand Up @@ -131,6 +139,7 @@ export const content = (props: MuxTemplateProps) => html`
cast-receiver="${props.castReceiver ?? false}"
drm-token="${props.tokens?.drm ?? false}"
exportparts="video"
adtagurl="${/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */ props.adTagUrl ?? false}"
>
${props.storyboard
? html`<track label="thumbnails" default kind="metadata" src="${props.storyboard}" />`
Expand Down
Loading