Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4e33058
WIP mux-video + google ima
cjpillsbury Sep 17, 2024
1bd74f4
example PLUS HACK IN ADTAGURL IN PLAYER TEMPLATE.
cjpillsbury Sep 17, 2024
f8868c6
ad basic api for ad break times.
cjpillsbury Sep 17, 2024
91c8ab2
WIP add basic impl for ad tag url to mux-player (probably move to sep…
cjpillsbury Sep 17, 2024
ea0005e
first pass on type defs for google ima sdk.
cjpillsbury Sep 17, 2024
7250184
more types cleanup and minor changes.
cjpillsbury Sep 17, 2024
d687d12
cleanup google ima types for external consumption and builds (e.g. mu…
cjpillsbury Sep 17, 2024
ec2dbc9
more typescriptrobatics.
cjpillsbury Sep 17, 2024
b159fff
chore: update vanilla example page title for google ima usage.
cjpillsbury Sep 17, 2024
3530e02
try to see if this fixes silly template test fails.
cjpillsbury Sep 17, 2024
b84b95d
volume control for ads.
cjpillsbury Sep 17, 2024
d4f0676
basic autoplay and ready state for ads
cjpillsbury Sep 17, 2024
e010b3c
basic postroll impl
cjpillsbury Sep 17, 2024
c216fae
cleanup ad break code.
cjpillsbury Sep 18, 2024
aeaf3e9
ad state and various cleanups.
cjpillsbury Sep 19, 2024
5be7a1d
prove out mux player ad-specific UI.
cjpillsbury Sep 19, 2024
485a57a
update vanilla ads example to demo new UI.
cjpillsbury Sep 19, 2024
0afd4b8
add resizing logic for IMA crud.
cjpillsbury Sep 23, 2024
97fb80a
fix linting errors
cjpillsbury Sep 23, 2024
237c08a
force exit pip on ad play.
cjpillsbury Sep 23, 2024
d3100e3
account for user active+inactive (NOTE: Needs followup for ad clicks).
cjpillsbury Sep 23, 2024
6dc93b7
more acrobatics for different playback scenarios
cjpillsbury Sep 23, 2024
f12c90b
bug fix for ad counter.
cjpillsbury Sep 23, 2024
7178556
minor cleanup
cjpillsbury Sep 24, 2024
0182a54
for now no autohide of any controls during adbreak.
cjpillsbury Sep 25, 2024
77241f8
general cleanup using more official APIs instead of data structures.
cjpillsbury Sep 25, 2024
5d3d20b
minor improvements for ad state and state change events.
cjpillsbury Mar 11, 2025
9b5bdc5
update example for preroll + MSE (iPhone problem case validation)
cjpillsbury Mar 12, 2025
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
74 changes: 74 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,74 @@
<!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"
prefer-playback="mse"
playback-id="VcmKA6aqzIzlg3MayLJDnbF55kX00mds028Z65QxvBYaA"
muted
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="
></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 @@ -86,6 +86,8 @@ const PlayerAttributes = {
CAST_RECEIVER: 'cast-receiver',
NO_TOOLTIPS: 'no-tooltips',
PROUDLY_DISPLAY_MUX_BADGE: 'proudly-display-mux-badge',
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
AD_TAG_URL: 'adtagurl',
};

const ThemeAttributeNames = [
Expand Down Expand Up @@ -179,6 +181,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 @@ -416,6 +426,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 @@ -447,7 +486,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 @@ -1862,6 +1901,37 @@ class MuxPlayerElement extends VideoApiElement implements MuxPlayerElement {
this.setAttribute(PlayerAttributes.PROUDLY_DISPLAY_MUX_BADGE, '');
}
}

/** @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
@@ -1,6 +1,7 @@
import 'media-chrome/dist/media-theme-element.js';
// @ts-ignore
import cssStr from './styles.css';
import './media-chrome/ads/media-ad-count-display';
import { getStreamTypeFromAttr } from './helpers';
import { html } from './html';
import { stylePropsToString } from './utils';
Expand Down Expand Up @@ -82,6 +83,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 || false}"
Expand Down Expand Up @@ -134,6 +142,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
Loading