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: cast custom data #976

Merged
merged 9 commits into from
Aug 13, 2024
Merged
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 |
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wasn't sure how to document the default value here, which is a bunch of mappings of mux player props to an 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}"
luwes marked this conversation as resolved.
Show resolved Hide resolved
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
Loading