Skip to content

Commit

Permalink
feat: add media-error-dialog (#1024)
Browse files Browse the repository at this point in the history
https://media-chrome-git-fork-luwes-error-dialog-mux.vercel.app/examples/vanilla/control-elements/media-error-dialog.html

use of `<media-chrome-dialog>` here:

https://media-chrome-git-fork-luwes-error-dialog-mux.vercel.app/examples/vanilla/control-elements/media-settings-menu.html

- Simplified the media-chrome-dialog removing many CSS vars that are not
essential.
- Format the error message by overriding
`MediaErrorDialog.formatErrorMessage(error)`
- Change the error message via slots
```html
<media-error-dialog mediaerrorcode="2">
  <div slot="error-2">This is a custom message</div>
</media-error-dialog>
```

- Supports custom media elements errors with any code and message.
- Built with React SSR compatible `getTemplateHTML` static methods
(could be nice to start working towards this)

---------

Co-authored-by: Christian Pillsbury <cjpillsbury@gmail.com>
  • Loading branch information
luwes and cjpillsbury authored Dec 5, 2024
1 parent 0c161f4 commit 3fc95a2
Show file tree
Hide file tree
Showing 19 changed files with 620 additions and 198 deletions.
121 changes: 121 additions & 0 deletions examples/vanilla/control-elements/media-error-dialog.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width" />
<title>Media Error Dialog</title>
<script type="module" src="../../../dist/index.js"></script>
<script type="module">
import { CustomVideoElement } from 'https://cdn.jsdelivr.net/npm/custom-media-element/+esm';

class CustomVideo extends CustomVideoElement {
#error;

get error() {
return this.#error ?? super.error;
}

set error(value) {
this.#error = value;
}
}

globalThis.customElements.define('custom-video', CustomVideo);
</script>

<style>
h2 {
margin-top: 2rem;
}

/** add styles to prevent CLS (Cumulative Layout Shift) */
media-controller:not([audio]) {
display: grid; /* expands the container if preload=none */
max-width: 540px; /* allows the container to shrink if small */
aspect-ratio: 16 / 9; /* set container aspect ratio if preload=none */
}

video,
img[slot="poster"] {
grid-area: 1 / 1; /* position poster img on top of video element */
width: 100%; /* prevents video to expand beyond its container */
}

.examples {
margin-top: 20px;
}
</style>
</head>
<body>
<h1>MediaErrorDialog</h1>

<h2>Non existing mp4</h2>

<media-controller defaultsubtitles>
<custom-video
slot="media"
src="https://stream.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB00/low.mp4"
muted
crossorigin
></custom-video>
<img slot="poster" src="https://image.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB008/thumbnail.webp?time=13" />
<media-loading-indicator slot="centered-chrome" noautohide></media-loading-indicator>
<media-error-dialog slot="dialog"></media-error-dialog>
<media-control-bar>
<media-play-button></media-play-button>
<media-mute-button></media-mute-button>
<media-time-range></media-time-range>
<media-time-display></media-time-display>
<media-fullscreen-button></media-fullscreen-button>
</media-control-bar>
</media-controller>

<h2>Custom error code for custom media elements</h2>
<script>
function setError() {
const video = document.querySelector('custom-video');
const customError = new Error('This is a custom error with code 42!');
customError.code = 42;
video.error = customError;
video.dispatchEvent(new Event('error'));
}
</script>
<button onclick="setError()">Set error code 42</button>

<div style="max-width: 540px">
<h2>Error 2 (MEDIA_ERR_NETWORK)</h2>
<media-error-dialog mediaerrorcode="2"></media-error-dialog>

<h2>Error 3 (MEDIA_ERR_DECODE)</h2>
<media-error-dialog mediaerrorcode="3"></media-error-dialog>

<h2>Error 4 (MEDIA_ERR_SRC_NOT_SUPPORTED)</h2>
<media-error-dialog mediaerrorcode="4"></media-error-dialog>

<h2>Error 5 (MEDIA_ERR_ENCRYPTED)</h2>
<media-error-dialog mediaerrorcode="5"></media-error-dialog>

<h2>Error 2 with custom error content via <code>error-2</code> slot</h2>
<media-error-dialog mediaerrorcode="2">
<h3 slot="error-2">Custom Error 2</h3>
<p slot="error-2">This is a custom message</p>
</media-error-dialog>

<h2>Error 2 with custom error title via <code>error-2-title</code> slot</h2>
<media-error-dialog mediaerrorcode="2">
<h3 slot="error-2-title">Custom Error 2 Title</h3>
</media-error-dialog>

<h2>Error 2 with custom error message via <code>error-2-message</code> slot</h2>
<media-error-dialog mediaerrorcode="2">
<p slot="error-2-message">This is a custom message</p>
</media-error-dialog>

</div>

<br>

<div class="examples">
<a href="../">View more examples</a>
</div>
</body>
</html>
8 changes: 2 additions & 6 deletions examples/vanilla/control-elements/media-settings-menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,7 @@ <h2>menu children</h2>
crossorigin
></mux-video>
<media-loading-indicator slot="centered-chrome" noautohide></media-loading-indicator>
<media-chrome-dialog
id="settings"
style="position: absolute; top: 5%;"
hidden
>
<media-chrome-dialog id="settings">
<media-captions-menu>
<div slot="header">Captions</div>
</media-captions-menu>
Expand All @@ -119,7 +115,7 @@ <h2>menu children</h2>
<media-mute-button></media-mute-button>
<media-time-range></media-time-range>
<media-time-display></media-time-display>
<!-- Link menu button and menu with invoketarget<->id -->
<!-- Link menu button and dialog with invoketarget<->id -->
<media-settings-menu-button invoketarget="settings"></media-settings-menu-button>
<media-fullscreen-button></media-fullscreen-button>
</media-control-bar>
Expand Down
1 change: 1 addition & 0 deletions examples/vanilla/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ <h2>Individual controls</h2>
</a>
</li>
<li><a href="components.html">All components (useful for testing)</a></li>
<li><a href="control-elements/media-error-dialog.html">Error Dialog</a></li>
<li><a href="control-elements/media-settings-menu.html">Settings Menu</a></li>
<li><a href="control-elements/media-chrome-menu.html">Chrome Menu</a></li>
<li><a href="control-elements/media-captions-menu.html">Captions menu</a></li>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@
],
"rules": {
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
},
"parserOptions": {
"ecmaVersion": 2022,
Expand Down
61 changes: 32 additions & 29 deletions src/js/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,44 +37,47 @@ export type MediaStateReceiverAttributes = typeof MediaStateReceiverAttributes;

export const MediaUIProps = {
MEDIA_AIRPLAY_UNAVAILABLE: 'mediaAirplayUnavailable',
MEDIA_FULLSCREEN_UNAVAILABLE: 'mediaFullscreenUnavailable',
MEDIA_PIP_UNAVAILABLE: 'mediaPipUnavailable',
MEDIA_CAST_UNAVAILABLE: 'mediaCastUnavailable',
MEDIA_RENDITION_UNAVAILABLE: 'mediaRenditionUnavailable',
MEDIA_AUDIO_TRACK_ENABLED: 'mediaAudioTrackEnabled',
MEDIA_AUDIO_TRACK_LIST: 'mediaAudioTrackList',
MEDIA_AUDIO_TRACK_UNAVAILABLE: 'mediaAudioTrackUnavailable',
MEDIA_WIDTH: 'mediaWidth',
MEDIA_HEIGHT: 'mediaHeight',
MEDIA_PAUSED: 'mediaPaused',
MEDIA_HAS_PLAYED: 'mediaHasPlayed',
MEDIA_BUFFERED: 'mediaBuffered',
MEDIA_CAST_UNAVAILABLE: 'mediaCastUnavailable',
MEDIA_CHAPTERS_CUES: 'mediaChaptersCues',
MEDIA_CURRENT_TIME: 'mediaCurrentTime',
MEDIA_DURATION: 'mediaDuration',
MEDIA_ENDED: 'mediaEnded',
MEDIA_MUTED: 'mediaMuted',
MEDIA_VOLUME_LEVEL: 'mediaVolumeLevel',
MEDIA_VOLUME: 'mediaVolume',
MEDIA_VOLUME_UNAVAILABLE: 'mediaVolumeUnavailable',
MEDIA_IS_PIP: 'mediaIsPip',
MEDIA_IS_CASTING: 'mediaIsCasting',
MEDIA_ERROR: 'mediaError',
MEDIA_ERROR_CODE: 'mediaErrorCode',
MEDIA_ERROR_MESSAGE: 'mediaErrorMessage',
MEDIA_FULLSCREEN_UNAVAILABLE: 'mediaFullscreenUnavailable',
MEDIA_HAS_PLAYED: 'mediaHasPlayed',
MEDIA_HEIGHT: 'mediaHeight',
MEDIA_IS_AIRPLAYING: 'mediaIsAirplaying',
MEDIA_SUBTITLES_LIST: 'mediaSubtitlesList',
MEDIA_SUBTITLES_SHOWING: 'mediaSubtitlesShowing',
MEDIA_IS_CASTING: 'mediaIsCasting',
MEDIA_IS_FULLSCREEN: 'mediaIsFullscreen',
MEDIA_IS_PIP: 'mediaIsPip',
MEDIA_LOADING: 'mediaLoading',
MEDIA_MUTED: 'mediaMuted',
MEDIA_PAUSED: 'mediaPaused',
MEDIA_PIP_UNAVAILABLE: 'mediaPipUnavailable',
MEDIA_PLAYBACK_RATE: 'mediaPlaybackRate',
MEDIA_CURRENT_TIME: 'mediaCurrentTime',
MEDIA_DURATION: 'mediaDuration',
MEDIA_SEEKABLE: 'mediaSeekable',
MEDIA_PREVIEW_TIME: 'mediaPreviewTime',
MEDIA_PREVIEW_IMAGE: 'mediaPreviewImage',
MEDIA_PREVIEW_COORDS: 'mediaPreviewCoords',
MEDIA_PREVIEW_CHAPTER: 'mediaPreviewChapter',
MEDIA_LOADING: 'mediaLoading',
MEDIA_BUFFERED: 'mediaBuffered',
MEDIA_PREVIEW_COORDS: 'mediaPreviewCoords',
MEDIA_PREVIEW_IMAGE: 'mediaPreviewImage',
MEDIA_PREVIEW_TIME: 'mediaPreviewTime',
MEDIA_RENDITION_LIST: 'mediaRenditionList',
MEDIA_RENDITION_SELECTED: 'mediaRenditionSelected',
MEDIA_RENDITION_UNAVAILABLE: 'mediaRenditionUnavailable',
MEDIA_SEEKABLE: 'mediaSeekable',
MEDIA_STREAM_TYPE: 'mediaStreamType',
MEDIA_SUBTITLES_LIST: 'mediaSubtitlesList',
MEDIA_SUBTITLES_SHOWING: 'mediaSubtitlesShowing',
MEDIA_TARGET_LIVE_WINDOW: 'mediaTargetLiveWindow',
MEDIA_TIME_IS_LIVE: 'mediaTimeIsLive',
MEDIA_RENDITION_LIST: 'mediaRenditionList',
MEDIA_RENDITION_SELECTED: 'mediaRenditionSelected',
MEDIA_AUDIO_TRACK_LIST: 'mediaAudioTrackList',
MEDIA_AUDIO_TRACK_ENABLED: 'mediaAudioTrackEnabled',
MEDIA_CHAPTERS_CUES: 'mediaChaptersCues',
MEDIA_VOLUME: 'mediaVolume',
MEDIA_VOLUME_LEVEL: 'mediaVolumeLevel',
MEDIA_VOLUME_UNAVAILABLE: 'mediaVolumeUnavailable',
MEDIA_WIDTH: 'mediaWidth',
} as const;

export type MediaUIProps = typeof MediaUIProps;
Expand Down
2 changes: 2 additions & 0 deletions src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import MediaChromeDialog from './media-chrome-dialog.js';
import MediaChromeRange from './media-chrome-range.js';
import MediaControlBar from './media-control-bar.js';
import MediaDurationDisplay from './media-duration-display.js';
import MediaErrorDialog from './media-error-dialog.js';
import MediaFullscreenButton from './media-fullscreen-button.js';
import MediaGestureReceiver from './media-gesture-receiver.js';
import MediaLiveButton from './media-live-button.js';
Expand Down Expand Up @@ -42,6 +43,7 @@ export {
MediaControlBar,
MediaController,
MediaDurationDisplay,
MediaErrorDialog,
MediaFullscreenButton,
MediaGestureReceiver,
MediaLiveButton,
Expand Down
29 changes: 29 additions & 0 deletions src/js/labels/labels.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
export type LabelOptions = { seekOffset?: number; playbackRate?: number };

export type MediaErrorLike = {
code: number;
message: string;
[key: string]: any;
};

const defaultErrorTitles = {
2: 'Network Error',
3: 'Decode Error',
4: 'Source Not Supported',
5: 'Encryption Error',
};

const defaultErrorMessages = {
2: 'A network error caused the media download to fail.',
3: 'A media error caused playback to be aborted. The media could be corrupt or your browser does not support this format.',
4: 'An unsupported error occurred. The server or network failed, or your browser does not support this format.',
5: 'The media is encrypted and there are no keys to decrypt it.',
};

// Returning null makes the error not show up in the UI.
export const formatError = (error: MediaErrorLike) => {
if (error.code === 1) return null;
return {
title: defaultErrorTitles[error.code] ?? `Error ${error.code}`,
message: defaultErrorMessages[error.code] ?? error.message
};
}

export const tooltipLabels = {
ENTER_AIRPLAY: 'Start airplay',
EXIT_AIRPLAY: 'Stop airplay',
Expand Down
Loading

0 comments on commit 3fc95a2

Please sign in to comment.