Skip to content

Commit

Permalink
feat(mux-player, mux-player-react, mux-video): cast custom data
Browse files Browse the repository at this point in the history
* Exposes `castReceiver` / `cast-receiver` prop / attr to specify a
custom receiver app
* Adds mux-specific custom data for a custom Google Chromecast receiver
app (minimally necessary for DRM license requests).
* Adds `drmType` to Mux Data metadata once the DRM type is determined
via key session initialization.
* Relates to muxinc/media-elements#32
* Ensures omitted storyboard tokens and thumbnail tokens don't cause
errors.
  • Loading branch information
cjpillsbury authored Aug 13, 2024
1 parent 94210d8 commit 2722b6e
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 35 deletions.
1 change: 1 addition & 0 deletions packages/mux-player-react/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
| `programEndTime` | `number` | Apply PDT-based [instant clips](https://docs.mux.com/guides/create-instant-clips) to the end of the media stream. | N/A |
| `metadata` | `object`\* | An object for configuring any metadata you'd like to send to [Mux Data](https://docs.mux.com/guides/data/make-your-data-actionable-with-metadata) | `undefined` |
| `tokens` | `object`\* | An object for setting all signed URL tokens with the signature `{ playback?: string; thumbnail?: string; storyboard?: string; drm?: string; }` | `undefined` |
| `castCustomData` | `object` (JSON-serializable) | [Custom Data](https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.MediaInfo#customData) to send to your Google cast receiver on initial load. If none is provided, various Mux key/value pairs will be sent. | Mux-specific object |
| `ref` | [React `ref`](https://reactjs.org/docs/refs-and-the-dom.html) | A [React `ref`](https://reactjs.org/docs/refs-and-the-dom.html) to the underlying [`MuxPlayerElement`](../mux-player/REFERENCE.md) web component | `undefined` |

<!-- UNDOCUMENTED
Expand Down
4 changes: 4 additions & 0 deletions packages/mux-player-react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export type MuxPlayerProps = {
className?: string;
hotkeys?: string;
nohotkeys?: boolean;
castReceiver?: string | undefined;
castCustomData?: Record<string, any> | undefined;
defaultHiddenCaptions?: boolean;
playerSoftwareVersion?: string;
playerSoftwareName?: string;
Expand Down Expand Up @@ -190,6 +192,7 @@ const usePlayer = (
currentTime,
themeProps,
extraSourceParams,
castCustomData,
_hlsConfig,
...remainingProps
} = props;
Expand All @@ -200,6 +203,7 @@ const usePlayer = (
useObjectPropEffect('themeProps', themeProps, ref);
useObjectPropEffect('tokens', tokens, ref);
useObjectPropEffect('playbackId', playbackId, ref);
useObjectPropEffect('castCustomData', castCustomData, ref);
useObjectPropEffect(
'paused',
paused,
Expand Down
3 changes: 3 additions & 0 deletions packages/mux-player/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
| `default-show-remaining-time` | `boolean` | Show remaining playback time (instead of current playback time) by default | `false` |
| `title` | `string` | Title text to show for your content in the Mux Player UI. | `""` |
| `placeholder` | `string` (URI) | Image to show as various assets load. Typically a [data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs) when used | N/A |
| `cast-receiver` | `string` (Receiver ID) | The app ID to use for a custom [Google cast receiver](https://developers.google.com/cast/docs/web_receiver/basic). If none is provided, the default receiver app will be used. | N/A |

<!-- UNDOCUMENTED
// NEW STREAM TYPE VALUES
Expand Down Expand Up @@ -150,6 +151,8 @@
| `chapters` <sub><sup>Read only</sup></sub> | `Array<{ startTime: number; endTime?: number, value: string; }>` | The array of Chapters for the current media, added via `addChapters(chapters)`. | `[]` |
| `activeCuePoint` <sub><sup>Read only</sup></sub> | `{ startTime: number; endTime?: number, value: any; }` | The current active CuePoint, determined based on the player's `currentTime`. | `undefined` |
| `activeChapter` <sub><sup>Read only</sup></sub> | `{ startTime: number; endTime?: number, value: string; }` | The current active Chapter, determined based on the player's `currentTime`. | `undefined` |
| `castReceiver` | `string` (Receiver ID) | The app ID to use for a custom [Google cast receiver](https://developers.google.com/cast/docs/web_receiver/basic). If none is provided, the default receiver app will be used. | `undefined` |
| `castCustomData` | `object` (JSON-serializable) | [Custom Data](https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.MediaInfo#customData) to send to your Google cast receiver on initial load. If none is provided, various Mux key/value pairs will be sent. | Mux-specific object |

<!-- UNDOCUMENTED
// NEW STREAM TYPE VALUES
Expand Down
86 changes: 72 additions & 14 deletions packages/mux-player/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const PlayerAttributes = {
TARGET_LIVE_WINDOW: 'target-live-window',
EXTRA_SOURCE_PARAMS: 'extra-source-params',
NO_VOLUME_PREF: 'no-volume-pref',
CAST_RECEIVER: 'cast-receiver',
};

const ThemeAttributeNames = [
Expand Down Expand Up @@ -163,6 +164,7 @@ function getProps(el: MuxPlayerElement, state?: any): MuxTemplateProps {
customDomain: el.getAttribute(MuxVideoAttributes.CUSTOM_DOMAIN) ?? undefined,
title: el.getAttribute(PlayerAttributes.TITLE),
novolumepref: el.hasAttribute(PlayerAttributes.NO_VOLUME_PREF),
castReceiver: el.castReceiver,
...state,
// 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)
Expand Down Expand Up @@ -636,20 +638,35 @@ class MuxPlayerElement extends VideoApiElement implements MuxPlayerElement {
break;
}
case PlayerAttributes.THUMBNAIL_TOKEN: {
const { aud } = parseJwt(newValue);
if (newValue && aud !== 't') {
logger.warn(
i18n(`The provided thumbnail-token should have audience value 't' instead of '{aud}'.`).format({ aud })
);
if (newValue) {
const { aud } = parseJwt(newValue);
if (aud !== 't') {
logger.warn(
i18n(`The provided thumbnail-token should have audience value 'd' instead of '{aud}'.`).format({ aud })
);
}
}
break;
}
case PlayerAttributes.STORYBOARD_TOKEN: {
const { aud } = parseJwt(newValue);
if (newValue && aud !== 's') {
logger.warn(
i18n(`The provided storyboard-token should have audience value 's' instead of '{aud}'.`).format({ aud })
);
if (newValue) {
const { aud } = parseJwt(newValue);
if (aud !== 's') {
logger.warn(
i18n(`The provided storyboard-token should have audience value 'd' instead of '{aud}'.`).format({ aud })
);
}
}
break;
}
case PlayerAttributes.DRM_TOKEN: {
if (newValue) {
const { aud } = parseJwt(newValue);
if (aud !== 'd') {
logger.warn(
i18n(`The provided drm-token should have audience value 'd' instead of '{aud}'.`).format({ aud })
);
}
}
break;
}
Expand Down Expand Up @@ -850,14 +867,22 @@ class MuxPlayerElement extends VideoApiElement implements MuxPlayerElement {
get poster() {
const val = this.getAttribute(VideoAttributes.POSTER);
if (val != null) return val;
// If a playback token but no thumbnail token is provided,
// assume a token is required for the thumbnail/poster URL and
// simply avoid requesting it in this case.
const { tokens } = this;
if (tokens.playback && !tokens.thumbnail) {
logger.warn('Missing expected thumbnail token. No poster image will be shown');
return undefined;
}

// Get the derived poster if a playbackId is present.
if (this.playbackId && !this.audio) {
return getPosterURLFromPlaybackId(this.playbackId, {
customDomain: this.customDomain,
thumbnailTime: this.thumbnailTime ?? this.startTime,
programTime: this.programStartTime,
token: this.tokens.thumbnail,
token: tokens.thumbnail,
});
}

Expand Down Expand Up @@ -898,21 +923,26 @@ class MuxPlayerElement extends VideoApiElement implements MuxPlayerElement {
* we aren't an audio player and the stream-type isn't live.
*/
get storyboard() {
const { tokens } = this;
// If the storyboardSrc has been explicitly set, assume it should be used
if (this.storyboardSrc && !this.tokens.storyboard) return this.storyboardSrc;
if (this.storyboardSrc && !tokens.storyboard) return this.storyboardSrc;
if (
// NOTE: Some audio use cases may have a storyboard (e.g. it's an audio+video stream being played *as* audio)
// Consider supporting cases (CJP)
this.audio ||
!this.playbackId ||
!this.streamType ||
[StreamTypes.LIVE, StreamTypes.UNKNOWN].includes(this.streamType as any)
[StreamTypes.LIVE, StreamTypes.UNKNOWN].includes(this.streamType as any) ||
// If a playback token but no storyboard token is provided,
// assume a token is required for the storyboard URL URL and
// simply avoid requesting it in this case.
(tokens.playback && !tokens.storyboard)
) {
return undefined;
}
return getStoryboardURLFromPlaybackId(this.playbackId, {
customDomain: this.customDomain,
token: this.tokens.storyboard,
token: tokens.storyboard,
programStartTime: this.programStartTime,
programEndTime: this.programEndTime,
});
Expand Down Expand Up @@ -1664,6 +1694,34 @@ class MuxPlayerElement extends VideoApiElement implements MuxPlayerElement {
get textTracks() {
return this.media?.textTracks;
}

get castReceiver(): string | undefined {
return this.getAttribute(PlayerAttributes.CAST_RECEIVER) ?? undefined;
}

set castReceiver(val: string | undefined) {
if (val === this.castReceiver) return;
if (val) {
this.setAttribute(PlayerAttributes.CAST_RECEIVER, val);
} else {
this.removeAttribute(PlayerAttributes.CAST_RECEIVER);
}
}

get castCustomData() {
return this.media?.castCustomData;
}

set castCustomData(val) {
// NOTE: This condition should never be met. If it is, there is a bug (CJP)
if (!this.media) {
logger.error(
'underlying media element missing when trying to set castCustomData. castCustomData will not be set.'
);
return;
}
this.media.castCustomData = val;
}
}

export function getVideoAttribute(el: MuxPlayerElement, name: string) {
Expand Down
3 changes: 2 additions & 1 deletion packages/mux-player/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export const content = (props: MuxTemplateProps) => html`
custom-domain="${props.customDomain ?? false}"
src="${!!props.src ? props.src : props.playbackId ? toMuxVideoURL(props) : false}"
cast-src="${!!props.src ? props.src : props.playbackId ? toMuxVideoURL(props) : false}"
cast-receiver="${props.castReceiver ?? false}"
drm-token="${props.tokens?.drm ?? false}"
exportparts="video"
>
Expand All @@ -139,7 +140,7 @@ export const content = (props: MuxTemplateProps) => html`
<media-poster-image
part="poster"
exportparts="poster, img"
src="${props.poster === '' ? false : props.poster ?? false}"
src="${!!props.poster ? props.poster : false}"
placeholdersrc="${props.placeholder ?? false}"
></media-poster-image>
</slot>
Expand Down
1 change: 1 addition & 0 deletions packages/mux-player/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type MuxTemplateProps = Partial<MuxPlayerProps> & {
hotKeys: AttributeTokenList;
title: string;
defaultStreamType?: ValueOf<StreamTypes>;
castReceiver: string | undefined;
};

export type DialogOptions = {
Expand Down
2 changes: 1 addition & 1 deletion packages/mux-video/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
},
"dependencies": {
"@mux/playback-core": "0.25.2",
"castable-video": "~1.0.9",
"castable-video": "~1.1.0",
"custom-media-element": "~1.3.1",
"media-tracks": "~0.3.2"
},
Expand Down
43 changes: 42 additions & 1 deletion packages/mux-video/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,48 @@ 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)) {}
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
// CastableMediaMixin). (CJP)
#castCustomData: Record<string, any> | undefined;

get muxCastCustomData() {
return {
mux: {
// Mux Video values
playbackId: this.playbackId,
minResolution: this.minResolution,
maxResolution: this.maxResolution,
renditionOrder: this.renditionOrder,
customDomain: this.customDomain,
/** @TODO Add this.tokens to MuxVideoElement (CJP) */
tokens: {
drm: this.drmToken,
},
// Mux Data values
envKey: this.envKey,
metadata: this.metadata,
disableCookies: this.disableCookies,
disableTracking: this.disableTracking,
beaconCollectionDomain: this.beaconCollectionDomain,
// Playback values
startTime: this.startTime,
// Other values
preferCmcd: this.preferCmcd,
},
} as const;
}

get castCustomData() {
return this.#castCustomData ?? this.muxCastCustomData;
}

set castCustomData(val: Record<string, any> | undefined) {
this.#castCustomData = val;
}
}

type MuxVideoElementType = typeof MuxVideoElement;
declare global {
Expand Down
Loading

0 comments on commit 2722b6e

Please sign in to comment.