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 28 commits into
base: main
Choose a base branch
from
Draft
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
@@ -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 = [
@@ -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;
@@ -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() {
@@ -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);
}
@@ -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) {
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';
@@ -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}"
@@ -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}" />`
79 changes: 56 additions & 23 deletions packages/mux-player/src/themes/gerwig/gerwig.html
Original file line number Diff line number Diff line change
@@ -589,6 +589,20 @@
media-controller[mediahasplayed][mediapaused] [part='mux-badge'] {
bottom: calc(28px + var(--media-control-height, 0px) + var(--media-control-padding, 0px) * 2);
}

/**
* @TODO This is needed because media-container disables pointer-events on gestures-layer by default (CJP)
* @TODO This means the ad container is no longer clickable. Likely needed for mobile anyway, but this means
* Click div needs to be implemented for ad click through (CJP)
* @TODO This solution is mutually exclusive from use of Google IMA baked in skip ads UI (CJP)
*/
/* media-controller::part(gesture-layer) {
pointer-events: auto;
}
media-controller::part(media-layer) {
pointer-events: none;
} */
</style>

<template partial="TitleDisplay">
@@ -995,6 +1009,15 @@
</div>
</template>

<template partial="AdCountDisplay">
<media-ad-count-display
part="top title display"
class="title-display"
mediaadbreaktotalads="{{mediaadbreaktotalads}}"
mediaadbreakadposition="{{mediaadbreakadposition}}"
></media-ad-count-display>
</template>

<media-controller
part="controller"
defaultstreamtype="{{defaultstreamtype ?? 'on-demand'}}"
@@ -1048,32 +1071,42 @@
<div role="button" class="autoplay-unmute-btn">Unmute</div>
</div> -->

<template if="streamtype == 'on-demand'">
<template if="breakpointsm">
<media-control-bar part="control-bar top" slot="top-chrome">{{>TitleDisplay}} </media-control-bar>
<template if="!mediaadbreak">
<template if="streamtype == 'on-demand'">
<template if="breakpointsm">
<media-control-bar part="control-bar top" slot="top-chrome">{{>TitleDisplay}} </media-control-bar>
</template>
{{>TimeRange}}
<media-control-bar part="control-bar bottom">
{{>PlayButton}} {{>SeekBackwardButton}} {{>SeekForwardButton}} {{>TimeDisplay}} {{>MuteButton}}
{{>VolumeRange}}
<div class="spacer"></div>
{{>RenditionMenu}} {{>PlaybackRateMenu}} {{>AudioTrackMenu}} {{>CaptionsMenu}} {{>AirplayButton}}
{{>CastButton}} {{>PipButton}} {{>FullscreenButton}}
</media-control-bar>
</template>
{{>TimeRange}}
<media-control-bar part="control-bar bottom">
{{>PlayButton}} {{>SeekBackwardButton}} {{>SeekForwardButton}} {{>TimeDisplay}} {{>MuteButton}}
{{>VolumeRange}}
<div class="spacer"></div>
{{>RenditionMenu}} {{>PlaybackRateMenu}} {{>AudioTrackMenu}} {{>CaptionsMenu}} {{>AirplayButton}}
{{>CastButton}} {{>PipButton}} {{>FullscreenButton}}
</media-control-bar>
</template>

<template if="streamtype == 'live'">
<media-control-bar part="control-bar top" slot="top-chrome">
{{>LiveButton}}
<template if="breakpointsm"> {{>TitleDisplay}} </template>
</media-control-bar>
<template if="targetlivewindow > 0">{{>TimeRange}}</template>
<media-control-bar part="control-bar bottom">
{{>PlayButton}}
<template if="targetlivewindow > 0">{{>SeekBackwardButton}} {{>SeekForwardButton}}</template>
{{>MuteButton}} {{>VolumeRange}}
<template if="streamtype == 'live'">
<media-control-bar part="control-bar top" slot="top-chrome">
{{>LiveButton}}
<template if="breakpointsm"> {{>TitleDisplay}} </template>
</media-control-bar>
<template if="targetlivewindow > 0">{{>TimeRange}}</template>
<media-control-bar part="control-bar bottom">
{{>PlayButton}}
<template if="targetlivewindow > 0">{{>SeekBackwardButton}} {{>SeekForwardButton}}</template>
{{>MuteButton}} {{>VolumeRange}}
<div class="spacer"></div>
{{>RenditionMenu}} {{>AudioTrackMenu}} {{>CaptionsMenu}} {{>AirplayButton}} {{>CastButton}} {{>PipButton}}
{{>FullscreenButton}}
</media-control-bar>
</template>
</template>
<template if="mediaadbreak">
<media-control-bar noautohide part="control-bar top" slot="top-chrome"> {{>AdCountDisplay}} </media-control-bar>
<media-control-bar noautohide part="control-bar bottom">
{{>PlayButton}} {{>TimeDisplay}} {{>MuteButton}} {{>VolumeRange}}
<div class="spacer"></div>
{{>RenditionMenu}} {{>AudioTrackMenu}} {{>CaptionsMenu}} {{>AirplayButton}} {{>CastButton}} {{>PipButton}}
{{>FullscreenButton}}
</media-control-bar>
</template>
8 changes: 8 additions & 0 deletions packages/mux-player/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -56,6 +56,14 @@ export type MuxTemplateProps = Partial<MuxPlayerProps> & {
defaultStreamType?: ValueOf<StreamTypes>;
castReceiver: string | undefined;
proudlyDisplayMuxBadge?: boolean;
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
adTagUrl: string | undefined;
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
adBreak: boolean;
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
adBreakTotalAds: number | undefined;
/** @TODO Move to separate/extended, ads-only impl/module? (CJP) */
adBreakAdPosition: number | undefined;
};

export type DialogOptions = {
1,275 changes: 1,275 additions & 0 deletions packages/mux-video/src/google-ima-html5-sdk.ts

Large diffs are not rendered by default.

512 changes: 512 additions & 0 deletions packages/mux-video/src/google-ima-video-element-mixin.ts

Large diffs are not rendered by default.

25 changes: 22 additions & 3 deletions packages/mux-video/src/index.ts
Original file line number Diff line number Diff line change
@@ -48,6 +48,7 @@ import { CustomVideoElement, Events as VideoEvents } from 'custom-media-element'
import { CastableMediaMixin } from 'castable-video/castable-mixin.js';
import { MediaTracksMixin } from 'media-tracks';
import type { HlsConfig } from 'hls.js';
import { GoogleIMAVideoMixin } from './google-ima-video-element-mixin';

// Must mutate so the added events are available in custom-media-element.
VideoEvents.push('castchange', 'entercast', 'leavecast');
@@ -87,7 +88,8 @@ const AttributeNameValues = Object.values(Attributes);
export const playerSoftwareVersion = getPlayerVersion();
export const playerSoftwareName = 'mux-video';

class MuxVideoBaseElement extends CustomVideoElement implements Partial<MuxMediaProps> {
/** @TODO POC - THIS SHOULD PROBABLY BE SPLIT OUT AS A SEPARATE IMPORT/MODULE FOR-ADS-FLAVORED MUX-VIDEO (CJP) */
export class MuxVideoBaseElement extends GoogleIMAVideoMixin(CustomVideoElement) implements Partial<MuxMediaProps> {
static get NAME() {
return playerSoftwareName;
}
@@ -96,6 +98,7 @@ class MuxVideoBaseElement extends CustomVideoElement implements Partial<MuxMedia
return playerSoftwareVersion;
}

// export class MuxVideoBaseElement extends CustomVideoElement implements Partial<MuxMediaProps> {
static get observedAttributes() {
return [...AttributeNameValues, ...(CustomVideoElement.observedAttributes ?? [])];
}
@@ -167,10 +170,14 @@ class MuxVideoBaseElement extends CustomVideoElement implements Partial<MuxMedia
return this.#core?.engine;
}

/** @TODO Figure out Issues with types when using google ima mixin (CJP) */
/** @ts-ignore */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice find 😎

get mux(): Readonly<HTMLVideoElement['mux']> | undefined {
return this.nativeEl?.mux;
}

/** @TODO Figure out Issues with types when using google ima mixin (CJP) */
/** @ts-ignore */
get error() {
return getError(this.nativeEl) ?? null;
}
@@ -183,13 +190,17 @@ class MuxVideoBaseElement extends CustomVideoElement implements Partial<MuxMedia
this.#errorTranslator = value;
}

/** @TODO Figure out Issues with types when using google ima mixin (CJP) */
/** @ts-ignore */
get src() {
// Use the attribute value as the source of truth.
// No need to store it in two places.
// This avoids needing a to read the attribute initially and update the src.
return this.getAttribute('src') as string;
}

/** @TODO Figure out Issues with types when using google ima mixin (CJP) */
/** @ts-ignore */
set src(val: string) {
// If being set by attributeChangedCallback,
// dont' cause an infinite loop
@@ -244,13 +255,17 @@ class MuxVideoBaseElement extends CustomVideoElement implements Partial<MuxMedia
}
}

/** @TODO Figure out Issues with types when using google ima mixin (CJP) */
/** @ts-ignore */
get preload() {
const val = this.getAttribute('preload') as HTMLMediaElement['preload'];
if (val === '') return 'auto';
if (['none', 'metadata', 'auto'].includes(val)) return val;
return super.preload;
}

/** @TODO Figure out Issues with types when using google ima mixin (CJP) */
/** @ts-ignore */
set preload(val) {
// don't cause an infinite loop
// check the attribute because an empty string maps to the `auto` prop
@@ -518,6 +533,8 @@ class MuxVideoBaseElement extends CustomVideoElement implements Partial<MuxMedia
this.#tokens = val ?? {};
}

/** @TODO There is a type mismatch (readonly property vs. accessor here... May be related to some type defs in elements monorepo defs (CJP)) */
/** @ts-ignore */
get ended() {
// This ensures that edge case media that doesn't properly end will
// still announce itself as "ended".
@@ -615,6 +632,8 @@ class MuxVideoBaseElement extends CustomVideoElement implements Partial<MuxMedia
}
}

/** @TODO Figure out Issues with types when using google ima mixin (CJP) */
/** @ts-ignore */
get seekable() {
return getSeekable(this.nativeEl);
}
@@ -815,7 +834,7 @@ class MuxVideoBaseElement extends CustomVideoElement implements Partial<MuxMedia
}

// castable-video should be mixed in last so that it can override load().
class MuxVideoElement extends CastableMediaMixin(MediaTracksMixin(MuxVideoBaseElement)) {
export class MuxVideoElement extends CastableMediaMixin(MediaTracksMixin(MuxVideoBaseElement)) {
// NOTE: CastableMediaMixin needs to be a subclass of whatever implements the load() method
// (i.e. MuxVideoBaseElement), but we're overriding castCustomData to provide mux-specific
// values by default, so it needs to be defined here (i.e. in the composed subclass of
@@ -858,7 +877,7 @@ class MuxVideoElement extends CastableMediaMixin(MediaTracksMixin(MuxVideoBaseEl
}
}

type MuxVideoElementType = typeof MuxVideoElement;
export type MuxVideoElementType = typeof MuxVideoElement;
declare global {
var MuxVideoElement: MuxVideoElementType; // eslint-disable-line
}
38 changes: 38 additions & 0 deletions packages/mux-video/src/resize-observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { globalThis } from './polyfills';

/** @TODO MOVE TO PLAYBACK-CORE TO SHARE ACROSS ELEMENT PACKAGES (CJP) */

// Use 1 resize observer instance for many elements for best performance.
// https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ

const callbacksMap = new WeakMap<Element, Set<ResizeCallback>>();

type ResizeCallback = (entry: ResizeObserverEntry) => void;

const getCallbacks = (element: Element): Set<ResizeCallback> => {
let callbacks = callbacksMap.get(element);
if (!callbacks) callbacksMap.set(element, (callbacks = new Set<ResizeCallback>()));
return callbacks;
};

const observer = new globalThis.ResizeObserver((entries: ResizeObserverEntry[]) => {
for (const entry of entries) {
for (const callback of getCallbacks(entry.target)) {
callback(entry);
}
}
});

export function observeResize(element: Element, callback: ResizeCallback): void {
getCallbacks(element).add(callback);
observer.observe(element);
}

export function unobserveResize(element: Element, callback: ResizeCallback): void {
const callbacks = getCallbacks(element);
callbacks.delete(callback);

if (!callbacks.size) {
observer.unobserve(element);
}
}