From 862cc5f76f6bd132573a1a03287b23354d783493 Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Fri, 13 Aug 2021 10:54:41 -0500 Subject: [PATCH 01/17] chore(scaffold): Issue Templates. gitignore. package setup. various standard markdowns. License. nvmrc & npmrc for version enforcement. --- .github/ISSUE_TEMPLATE/bug_report.md | 70 + .gitignore | 118 + .npmrc | 1 + .nvmrc | 1 + CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md | 0 LICENSE | 9 + README_INTERNAL.md | 0 package.json | 23 + yarn.lock | 5601 ++++++++++++++++++++++++++ 10 files changed, 5823 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README_INTERNAL.md create mode 100644 package.json create mode 100644 yarn.lock diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..1f5122041 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,70 @@ +--- +name: "🐛 Bug Report" +about: Report a reproducible bug or regression. +title: 'Bug: ' +labels: 'bug, Status: Unconfirmed' + +--- + + +### Checklist +- [] Verified whether your issue exists in the latest version Mux Element(s) release: `npm install @mux/[element_name]` or `yarn @mux/[element_name]`. + - (NOTE: If the issue is not occurring in the latest version, we may not resolve it.) + +### Which Mux Elements/Packages has a bug? +- [] mux-video +- [] mux-audio +- [] common + +### Which browser(s) are you using? +- [] Chrome +- [] Safari +- [] Firefox +- [] Edge ("Edgeium") +- [] Chrome Android +- [] Safari on iOS +- [] Other (NOTE: Currently, Mux Elements are only officially supported in new versions of the browsers listed above) + +If "Other", please list the browser names (e.g. "Samsung Internet", "Opera") below: + +### Which operating system(s) are you using? +(For example: macOS, Windows, iOS, Android) + +### How are you using Mux Elements? +(For example: npm package, ` + + + + + + + diff --git a/packages/mux-video/package.json b/packages/mux-video/package.json index e44999ab5..ad5bc80e2 100644 --- a/packages/mux-video/package.json +++ b/packages/mux-video/package.json @@ -11,6 +11,9 @@ "author": "Mux, Inc.", "license": "MIT", "private": true, + "scripts": { + "dev": "snowpack dev --config snowpack.dev.config.js" + }, "dependencies": { "custom-video-element": "^0.0.2", "hls.js": "^1.0.9" diff --git a/packages/mux-video/snowpack.common.config.js b/packages/mux-video/snowpack.common.config.js new file mode 100644 index 000000000..266476d8a --- /dev/null +++ b/packages/mux-video/snowpack.common.config.js @@ -0,0 +1,21 @@ +// Snowpack Configuration File +// See all supported options: https://www.snowpack.dev/reference/configuration + +/** @type {import("snowpack").SnowpackUserConfig } */ +module.exports = { + mount: { + /* ... */ + }, + plugins: [ + /* ... */ + ], + packageOptions: { + /* ... */ + }, + devOptions: { + /* ... */ + }, + buildOptions: { + /* ... */ + }, +}; diff --git a/packages/mux-video/snowpack.dev.config.js b/packages/mux-video/snowpack.dev.config.js new file mode 100644 index 000000000..582c12b8c --- /dev/null +++ b/packages/mux-video/snowpack.dev.config.js @@ -0,0 +1,23 @@ +// Snowpack Configuration File +// See all supported options: https://www.snowpack.dev/reference/configuration + +/** @type {import("snowpack").SnowpackUserConfig } */ +module.exports = { + extends: './snowpack.common.config.js', + mount: { + 'src': { url: '/dist' }, + // Mount "public" to the root URL path ("/*") and serve files with zero transformations: + examples: { url: '/examples', static: true, resolve: false }, + }, + plugins: [ + /* ... */ + ], + packageOptions: { + /* ... */ + }, + devOptions: { + /* ... */ + }, + buildOptions: { + }, +}; diff --git a/packages/mux-video/snowpack.prod.config.js b/packages/mux-video/snowpack.prod.config.js new file mode 100644 index 000000000..c57a37210 --- /dev/null +++ b/packages/mux-video/snowpack.prod.config.js @@ -0,0 +1,27 @@ +// Snowpack Configuration File +// See all supported options: https://www.snowpack.dev/reference/configuration + +/** @type {import("snowpack").SnowpackUserConfig } */ +module.exports = { + extends: './snowpack.common.config.js', + mount: { + 'src/js': { url: '/' }, + }, + optimize: { + bundle: false, + minify: true, + target: 'es2019', + }, + plugins: [ + /* ... */ + ], + packageOptions: { + /* ... */ + }, + devOptions: { + /* ... */ + }, + buildOptions: { + out: './dist' + }, +}; diff --git a/packages/mux-video/src/index.ts b/packages/mux-video/src/index.ts index a36acf38a..48b01f7bf 100644 --- a/packages/mux-video/src/index.ts +++ b/packages/mux-video/src/index.ts @@ -84,8 +84,10 @@ declare global { interface Window { MuxVideoElement: typeof MuxVideoElement; } } -if (!window.customElements.get('hls-video')) { - window.customElements.define('hls-video', MuxVideoElement); +/** @TODO Refactor once using `globalThis` polyfills */ +if (!window.customElements.get('mux-video')) { + window.customElements.define('mux-video', MuxVideoElement); + /** @TODO consider externalizing this (breaks standard modularity) */ window.MuxVideoElement = MuxVideoElement; } diff --git a/tsconfig.json b/tsconfig.json index 7af2471ef..209c7c055 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "incremental": true, - "target": "ES2020", - "module": "ES2020", + "target": "ES2019", + "module": "ES2019", "declaration": true, "sourceMap": true, "composite": true, From 8e050c6f38c36ec4c8f46b0e5bf63f1c5c332631 Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Mon, 16 Aug 2021 11:17:39 -0500 Subject: [PATCH 08/17] feat(mux-video-init): add controls to simplify using example --- packages/mux-video/examples/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mux-video/examples/index.html b/packages/mux-video/examples/index.html index 1df13b3f8..598233671 100644 --- a/packages/mux-video/examples/index.html +++ b/packages/mux-video/examples/index.html @@ -9,6 +9,7 @@ Date: Mon, 16 Aug 2021 12:03:09 -0500 Subject: [PATCH 09/17] feat(mux-video-init): Adding basic integration of mux-embed (untested). --- packages/mux-video/examples/index.html | 2 +- packages/mux-video/package.json | 3 +- .../src/customTypings/mux-embed.d.ts | 5 ++ packages/mux-video/src/index.ts | 74 ++++++++++++++++--- tsconfig.json | 2 +- yarn.lock | 5 ++ 6 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 packages/mux-video/src/customTypings/mux-embed.d.ts diff --git a/packages/mux-video/examples/index.html b/packages/mux-video/examples/index.html index 598233671..3e0d8deba 100644 --- a/packages/mux-video/examples/index.html +++ b/packages/mux-video/examples/index.html @@ -2,7 +2,7 @@ - Media Chrome HLS Media Example + Basic mux-video example diff --git a/packages/mux-video/package.json b/packages/mux-video/package.json index ad5bc80e2..2c85a0e0c 100644 --- a/packages/mux-video/package.json +++ b/packages/mux-video/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "custom-video-element": "^0.0.2", - "hls.js": "^1.0.9" + "hls.js": "^1.0.9", + "mux-embed": "^4.1.1" } } diff --git a/packages/mux-video/src/customTypings/mux-embed.d.ts b/packages/mux-video/src/customTypings/mux-embed.d.ts new file mode 100644 index 000000000..3c6ed5d11 --- /dev/null +++ b/packages/mux-video/src/customTypings/mux-embed.d.ts @@ -0,0 +1,5 @@ +/** @TODO Add type defs to mux-embed directly */ +declare module 'mux-embed' { + /** @TODO Add better type def for options */ + export function monitor(id: string | HTMLMediaElement, options: {}) {} +} \ No newline at end of file diff --git a/packages/mux-video/src/index.ts b/packages/mux-video/src/index.ts index 48b01f7bf..8ebc8a6b4 100644 --- a/packages/mux-video/src/index.ts +++ b/packages/mux-video/src/index.ts @@ -1,14 +1,32 @@ // Web Components: Extending Native Elements, A working example -import CustomVideoElement from 'custom-video-element'; -import Hls from 'hls.js'; +import CustomVideoElement from "custom-video-element"; +import mux from "mux-embed"; + +import Hls from "hls.js"; + +type Attributes = { + ENV_KEY: "env-key"; +}; + +const Attributes: Attributes = { + ENV_KEY: "env-key", +}; class MuxVideoElement extends CustomVideoElement { + static get observedAttributes() { + return [ + Attributes.ENV_KEY, + ...(CustomVideoElement.observedAttributes ?? []), + ]; + } protected __hls?: Hls; + protected __muxPlayerInitTime: number; constructor() { super(); + this.__muxPlayerInitTime = Date.now(); } get hls() { @@ -19,28 +37,30 @@ class MuxVideoElement extends CustomVideoElement { // 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'); + return this.getAttribute("src"); } set src(val) { // If being set by attributeChangedCallback, // dont' cause an infinite loop if (val === this.src) return; - + if (val == null) { - this.removeAttribute('src'); + this.removeAttribute("src"); } else { - this.setAttribute('src', val); + this.setAttribute("src", val); } } load() { /** @TODO Add custom errors + error codes */ if (!this.src) { - console.error('DONT DO THIS'); + console.error("DONT DO THIS"); return; } + const env_key = this.getAttribute(Attributes.ENV_KEY); + if (Hls.isSupported()) { const hls = new Hls({ // Kind of like preload metadata, but causes spinner. @@ -52,8 +72,36 @@ class MuxVideoElement extends CustomVideoElement { this.__hls = hls; - } else if (this.nativeEl.canPlayType('application/vnd.apple.mpegurl')) { + if (env_key) { + mux.monitor(this.nativeEl, { + debug: false, + hlsjs: hls, + Hls: Hls, + data: { + env_key, // required + // Metadata fields + player_name: "mux-video", // any arbitrary string you want to use to identify this player + // Should this be the initialization of *THIS* player (instance) or the page? + player_init_time: this.__muxPlayerInitTime, // ex: 1451606400000 + // ... + }, + }); + } + } else if (this.nativeEl.canPlayType("application/vnd.apple.mpegurl")) { this.nativeEl.src = this.src; + if (env_key) { + mux.monitor(this.nativeEl, { + debug: false, + data: { + env_key, // required + // Metadata fields + player_name: "mux-video", // any arbitrary string you want to use to identify this player + // Should this be the initialization of *THIS* player (instance) or the page? + player_init_time: this.__muxPlayerInitTime, // ex: 1451606400000 + // ... + }, + }); + } } } @@ -81,14 +129,16 @@ class MuxVideoElement extends CustomVideoElement { } declare global { - interface Window { MuxVideoElement: typeof MuxVideoElement; } + interface Window { + MuxVideoElement: typeof MuxVideoElement; + } } /** @TODO Refactor once using `globalThis` polyfills */ -if (!window.customElements.get('mux-video')) { - window.customElements.define('mux-video', MuxVideoElement); +if (!window.customElements.get("mux-video")) { + window.customElements.define("mux-video", MuxVideoElement); /** @TODO consider externalizing this (breaks standard modularity) */ window.MuxVideoElement = MuxVideoElement; } -export default MuxVideoElement; \ No newline at end of file +export default MuxVideoElement; diff --git a/tsconfig.json b/tsconfig.json index 209c7c055..043328edd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "incremental": true, "target": "ES2019", - "module": "ES2019", + "module": "es6", "declaration": true, "sourceMap": true, "composite": true, diff --git a/yarn.lock b/yarn.lock index f8de5c5b4..6f12591b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3675,6 +3675,11 @@ mute-stream@0.0.8, mute-stream@~0.0.4: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mux-embed@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/mux-embed/-/mux-embed-4.1.1.tgz#d61c297c6043c59793ec1837b778b294d60a66cd" + integrity sha512-AUjuWWRrKJ8byyd4vrZvLrA8yQGV3eoZ70u9n0HvA8AdY67xt+YBEAQbh5TYHEKfXLf1WpZlB9UScjgegUkrbQ== + nanoid@^3.1.23: version "3.1.25" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" From 71bd1f34f10ea604ee9e11ef268e9c51550ca92f Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Mon, 16 Aug 2021 12:49:55 -0500 Subject: [PATCH 10/17] feat(mux-video-init): Adding Hls as export of mux-video root module. Adding stub for env-key to use mux data/mux-embed. --- packages/mux-video/examples/index.html | 6 +++++- packages/mux-video/src/index.ts | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/mux-video/examples/index.html b/packages/mux-video/examples/index.html index 3e0d8deba..1f8379c68 100644 --- a/packages/mux-video/examples/index.html +++ b/packages/mux-video/examples/index.html @@ -2,14 +2,18 @@ - Basic mux-video example + Basic <mux-video/> example + Date: Mon, 16 Aug 2021 13:03:05 -0500 Subject: [PATCH 11/17] feat(mux-video-init): 'bake in' hls error recovery. Only auto-load if we have a src. --- packages/mux-video/src/index.ts | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/mux-video/src/index.ts b/packages/mux-video/src/index.ts index 7f694906d..27bee6b3d 100644 --- a/packages/mux-video/src/index.ts +++ b/packages/mux-video/src/index.ts @@ -1,5 +1,3 @@ -// Web Components: Extending Native Elements, A working example - import CustomVideoElement from "custom-video-element"; import mux from "mux-embed"; @@ -67,6 +65,27 @@ class MuxVideoElement extends CustomVideoElement { // autoStartLoad: false, }); + hls.on(Hls.Events.ERROR, (_event, data) => { + if (data.fatal) { + switch (data.type) { + case Hls.ErrorTypes.NETWORK_ERROR: + // try to recover network error + console.error("fatal network error encountered, try to recover"); + hls.startLoad(); + break; + case Hls.ErrorTypes.MEDIA_ERROR: + console.error("fatal media error encountered, try to recover"); + hls.recoverMediaError(); + break; + default: + // cannot recover + console.error("unrecoverable fatal error encountered, cannot recover (check logs for more info)"); + hls.destroy(); + break; + } + } + }); + hls.loadSource(this.src); hls.attachMedia(this.nativeEl); @@ -116,7 +135,10 @@ class MuxVideoElement extends CustomVideoElement { // } connectedCallback() { - this.load(); + // Only auto-load if we have a src + if (this.src) { + this.load(); + } // Not preloading might require faking the play() promise // so that you can call play(), call load() within that @@ -141,8 +163,6 @@ if (!window.customElements.get("mux-video")) { window.MuxVideoElement = MuxVideoElement; } -export { - Hls -}; +export { Hls }; export default MuxVideoElement; From 07d995340296a6eff9b99c68e3a2761a91dc66ac Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Mon, 16 Aug 2021 15:34:56 -0500 Subject: [PATCH 12/17] feat(mux-video-init): Beef up typings for mux-embed. --- .../src/customTypings/mux-embed.d.ts | 78 ++++++++++++++++++- packages/mux-video/src/index.ts | 17 ++-- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/packages/mux-video/src/customTypings/mux-embed.d.ts b/packages/mux-video/src/customTypings/mux-embed.d.ts index 3c6ed5d11..e782de46b 100644 --- a/packages/mux-video/src/customTypings/mux-embed.d.ts +++ b/packages/mux-video/src/customTypings/mux-embed.d.ts @@ -1,5 +1,75 @@ /** @TODO Add type defs to mux-embed directly */ -declare module 'mux-embed' { - /** @TODO Add better type def for options */ - export function monitor(id: string | HTMLMediaElement, options: {}) {} -} \ No newline at end of file +declare module "mux-embed" { + import Hls from "hls.js"; + /** @TODO Add better type def for options */ + type RequiredMetadata = { + env_key: string; + }; + + type HighPriorityMetadata = { + video_id: string; + video_title: string; + viewer_user_id: string; + }; + + type OptionalMetadata = { + experiment_name: string; + page_type: string; + player_init_time: number; + player_name: string; + player_version: string; + sub_property_id: string; + video_cdn: string; + video_content_type: string; + video_duration: number; + video_encoding_variant: string; + video_language_code: string; + video_producer: string; + video_series: string; + video_stream_type: string; + video_variant_name: string; + video_variant_id: string; + view_session_id: string; + }; + + type OverridableMetadata = { + browser: string; + browser_version: string; + cdn: string; + operating_system: string; + operating_system_version: string; + page_url: string; + player_autoplay: boolean; + player_height: number; + player_instance_id: string; + player_language: string; + player_poster: string; + player_preload: boolean; + player_remote_played: boolean; + player_software_name: string; + player_software_version: string; + player_source_height: number; + player_source_width: number; + player_width: number; + source_type: string; + used_fullscreen: boolean; + viewer_connection_type: string; + viewer_device_category: string; + viewer_device_manufacturer: string; + viewer_device_name: string; + }; + + type Metadata = RequiredMetadata & + Partial & + Partial & + Partial; + + type Options = { + debug?: boolean; + hlsjs?: Hls; + Hls?: typeof Hls; + data: Metadata; + }; + + export function monitor(id: string | HTMLMediaElement, options: Options) {} +} diff --git a/packages/mux-video/src/index.ts b/packages/mux-video/src/index.ts index 27bee6b3d..0a92d5c76 100644 --- a/packages/mux-video/src/index.ts +++ b/packages/mux-video/src/index.ts @@ -3,24 +3,29 @@ import mux from "mux-embed"; import Hls from "hls.js"; -type Attributes = { +/** @TODO make the relationship between name+value smarter and more deriveable (CJP) */ +type AttributeNames = { ENV_KEY: "env-key"; + DEBUG: "debug"; }; -const Attributes: Attributes = { +const Attributes: AttributeNames = { ENV_KEY: "env-key", + DEBUG: "debug", }; class MuxVideoElement extends CustomVideoElement { static get observedAttributes() { return [ Attributes.ENV_KEY, + Attributes.DEBUG, ...(CustomVideoElement.observedAttributes ?? []), ]; } protected __hls?: Hls; protected __muxPlayerInitTime: number; + // protected __metadata: constructor() { super(); @@ -79,7 +84,9 @@ class MuxVideoElement extends CustomVideoElement { break; default: // cannot recover - console.error("unrecoverable fatal error encountered, cannot recover (check logs for more info)"); + console.error( + "unrecoverable fatal error encountered, cannot recover (check logs for more info)" + ); hls.destroy(); break; } @@ -93,7 +100,7 @@ class MuxVideoElement extends CustomVideoElement { if (env_key) { mux.monitor(this.nativeEl, { - debug: false, + debug: true, hlsjs: hls, Hls: Hls, data: { @@ -110,7 +117,7 @@ class MuxVideoElement extends CustomVideoElement { this.nativeEl.src = this.src; if (env_key) { mux.monitor(this.nativeEl, { - debug: false, + debug: true, data: { env_key, // required // Metadata fields From e122113d87f7be473d22e6277fd8bef7a844a873 Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Mon, 16 Aug 2021 16:56:03 -0500 Subject: [PATCH 13/17] feat(mux-video-init): Add support initial support for debug, mux data metadata. Update typedefs. Update example. --- packages/mux-video/examples/index.html | 1 + .../customTypings/custom-media-element.d.ts | 18 +++-- .../src/customTypings/mux-embed.d.ts | 12 ++- packages/mux-video/src/index.ts | 77 +++++++++++++++++-- 4 files changed, 92 insertions(+), 16 deletions(-) diff --git a/packages/mux-video/examples/index.html b/packages/mux-video/examples/index.html index 1f8379c68..139e506c6 100644 --- a/packages/mux-video/examples/index.html +++ b/packages/mux-video/examples/index.html @@ -13,6 +13,7 @@ controls autoplay muted + debug env-key="" > ; - readonly nativeEl: HTMLVideoElement - } -} \ No newline at end of file +declare module "custom-video-element" { + export default class CustomVideoElement< + V extends HTMLVideoElement = HTMLVideoElement + > + extends HTMLElement + implements HTMLVideoElement + { + static readonly observedAttributes: Array; + readonly nativeEl: V; + attributeChangedCallback(attrName, oldValue, newValue): void; + } +} diff --git a/packages/mux-video/src/customTypings/mux-embed.d.ts b/packages/mux-video/src/customTypings/mux-embed.d.ts index e782de46b..126cc54f9 100644 --- a/packages/mux-video/src/customTypings/mux-embed.d.ts +++ b/packages/mux-video/src/customTypings/mux-embed.d.ts @@ -64,12 +64,20 @@ declare module "mux-embed" { Partial & Partial; - type Options = { + export type Options = { debug?: boolean; hlsjs?: Hls; Hls?: typeof Hls; - data: Metadata; + data: M; + }; + + /** @TODO Add well defined event types (CJP) */ + type EventTypesMap = { + hb: Partial; }; export function monitor(id: string | HTMLMediaElement, options: Options) {} + + export function emit(type: K, payload: EventTypesMap[K]): void; +// export function emit(type: string, payload: any); } diff --git a/packages/mux-video/src/index.ts b/packages/mux-video/src/index.ts index 0a92d5c76..271de9235 100644 --- a/packages/mux-video/src/index.ts +++ b/packages/mux-video/src/index.ts @@ -1,8 +1,10 @@ import CustomVideoElement from "custom-video-element"; -import mux from "mux-embed"; +import mux, { Options } from "mux-embed"; import Hls from "hls.js"; +type Metadata = Partial; + /** @TODO make the relationship between name+value smarter and more deriveable (CJP) */ type AttributeNames = { ENV_KEY: "env-key"; @@ -14,7 +16,9 @@ const Attributes: AttributeNames = { DEBUG: "debug", }; -class MuxVideoElement extends CustomVideoElement { +type HTMLVideoElementWithMux = HTMLVideoElement & { mux: typeof mux }; + +class MuxVideoElement extends CustomVideoElement { static get observedAttributes() { return [ Attributes.ENV_KEY, @@ -25,7 +29,7 @@ class MuxVideoElement extends CustomVideoElement { protected __hls?: Hls; protected __muxPlayerInitTime: number; - // protected __metadata: + protected __metadata: Readonly = {}; constructor() { super(); @@ -36,6 +40,10 @@ class MuxVideoElement extends CustomVideoElement { return this.__hls; } + get mux(): Readonly { + return this.nativeEl.mux; + } + get src() { // Use the attribute value as the source of truth. // No need to store it in two places. @@ -55,6 +63,32 @@ class MuxVideoElement extends CustomVideoElement { } } + /** @TODO write a generic module for well defined primitive types -> attribute getter/setters/removers (CJP) */ + get debug(): boolean { + return this.getAttribute("debug") != null; + } + + set debug(val: boolean) { + if (val) { + this.setAttribute(Attributes.DEBUG, ""); + } else { + this.removeAttribute(Attributes.DEBUG); + } + } + + get metadata() { + return this.__metadata; + } + + set metadata(val: Readonly | undefined) { + this.__metadata = val ?? {}; + if (!!this.mux) { + /** @TODO Link to docs for a more detailed discussion (CJP) */ + console.info('Some metadata values may not be overridable at this time. Make sure you set all metadata to override before setting the src.'); + this.mux.emit('hb', this.__metadata); + } + } + load() { /** @TODO Add custom errors + error codes */ if (!this.src) { @@ -63,11 +97,13 @@ class MuxVideoElement extends CustomVideoElement { } const env_key = this.getAttribute(Attributes.ENV_KEY); + const debug = this.debug; if (Hls.isSupported()) { const hls = new Hls({ // Kind of like preload metadata, but causes spinner. // autoStartLoad: false, + debug }); hls.on(Hls.Events.ERROR, (_event, data) => { @@ -100,7 +136,7 @@ class MuxVideoElement extends CustomVideoElement { if (env_key) { mux.monitor(this.nativeEl, { - debug: true, + debug, hlsjs: hls, Hls: Hls, data: { @@ -108,8 +144,8 @@ class MuxVideoElement extends CustomVideoElement { // Metadata fields player_name: "mux-video", // any arbitrary string you want to use to identify this player // Should this be the initialization of *THIS* player (instance) or the page? - player_init_time: this.__muxPlayerInitTime, // ex: 1451606400000 - // ... + player_init_time: this.__muxPlayerInitTime, // ex: 1451606400000, + ...this.__metadata, }, }); } @@ -117,14 +153,14 @@ class MuxVideoElement extends CustomVideoElement { this.nativeEl.src = this.src; if (env_key) { mux.monitor(this.nativeEl, { - debug: true, + debug, data: { env_key, // required // Metadata fields player_name: "mux-video", // any arbitrary string you want to use to identify this player // Should this be the initialization of *THIS* player (instance) or the page? player_init_time: this.__muxPlayerInitTime, // ex: 1451606400000 - // ... + ...this.__metadata, }, }); } @@ -141,6 +177,31 @@ class MuxVideoElement extends CustomVideoElement { // } // } + attributeChangedCallback( + attrName: string, + oldValue: string | null, + newValue: string | null + ) { + if (attrName === "src") { + // Handle 3 cases: + // 1. no src -> src + // 2. src -> (different) src + // 3. src -> no src + } + if (attrName === Attributes.DEBUG) { + const debug = this.debug; + if (!!this.mux) { + /** @TODO Link to docs for a more detailed discussion (CJP) */ + console.info('Cannot toggle debug mode of mux data after initialization. Make sure you set all metadata to override before setting the src.'); + } + if (!!this.hls) { + this.hls.config.debug = debug; + } + } + + super.attributeChangedCallback(attrName, oldValue, newValue); + } + connectedCallback() { // Only auto-load if we have a src if (this.src) { From f08a63c0ac1a745b1bb8949900dcbd83655f3d74 Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Mon, 16 Aug 2021 18:01:02 -0500 Subject: [PATCH 14/17] feat(mux-video-init): Adding support for playback id. Clean up example. --- packages/mux-video/examples/index.html | 9 +-- .../src/customTypings/mux-embed.d.ts | 1 - packages/mux-video/src/index.ts | 57 ++++++++++++------- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/packages/mux-video/examples/index.html b/packages/mux-video/examples/index.html index 139e506c6..7385061db 100644 --- a/packages/mux-video/examples/index.html +++ b/packages/mux-video/examples/index.html @@ -8,20 +8,13 @@ - diff --git a/packages/mux-video/src/customTypings/mux-embed.d.ts b/packages/mux-video/src/customTypings/mux-embed.d.ts index 126cc54f9..783ab4cf4 100644 --- a/packages/mux-video/src/customTypings/mux-embed.d.ts +++ b/packages/mux-video/src/customTypings/mux-embed.d.ts @@ -79,5 +79,4 @@ declare module "mux-embed" { export function monitor(id: string | HTMLMediaElement, options: Options) {} export function emit(type: K, payload: EventTypesMap[K]): void; -// export function emit(type: string, payload: any); } diff --git a/packages/mux-video/src/index.ts b/packages/mux-video/src/index.ts index 271de9235..4641a792a 100644 --- a/packages/mux-video/src/index.ts +++ b/packages/mux-video/src/index.ts @@ -9,20 +9,26 @@ type Metadata = Partial; type AttributeNames = { ENV_KEY: "env-key"; DEBUG: "debug"; + PLAYBACK_ID: "playback-id"; }; const Attributes: AttributeNames = { ENV_KEY: "env-key", DEBUG: "debug", + PLAYBACK_ID: "playback-id", }; +const AttributeNameValues = Object.values(Attributes); + +const toMuxVideoURL = (playbackId: string | null) => + playbackId ? `https://stream.mux.com/${playbackId}.m3u8` : null; + type HTMLVideoElementWithMux = HTMLVideoElement & { mux: typeof mux }; class MuxVideoElement extends CustomVideoElement { static get observedAttributes() { return [ - Attributes.ENV_KEY, - Attributes.DEBUG, + ...AttributeNameValues, ...(CustomVideoElement.observedAttributes ?? []), ]; } @@ -84,8 +90,10 @@ class MuxVideoElement extends CustomVideoElement { this.__metadata = val ?? {}; if (!!this.mux) { /** @TODO Link to docs for a more detailed discussion (CJP) */ - console.info('Some metadata values may not be overridable at this time. Make sure you set all metadata to override before setting the src.'); - this.mux.emit('hb', this.__metadata); + console.info( + "Some metadata values may not be overridable at this time. Make sure you set all metadata to override before setting the src." + ); + this.mux.emit("hb", this.__metadata); } } @@ -103,7 +111,7 @@ class MuxVideoElement extends CustomVideoElement { const hls = new Hls({ // Kind of like preload metadata, but causes spinner. // autoStartLoad: false, - debug + debug, }); hls.on(Hls.Events.ERROR, (_event, data) => { @@ -182,21 +190,30 @@ class MuxVideoElement extends CustomVideoElement { oldValue: string | null, newValue: string | null ) { - if (attrName === "src") { - // Handle 3 cases: - // 1. no src -> src - // 2. src -> (different) src - // 3. src -> no src - } - if (attrName === Attributes.DEBUG) { - const debug = this.debug; - if (!!this.mux) { - /** @TODO Link to docs for a more detailed discussion (CJP) */ - console.info('Cannot toggle debug mode of mux data after initialization. Make sure you set all metadata to override before setting the src.'); - } - if (!!this.hls) { - this.hls.config.debug = debug; - } + switch (attrName) { + case "src": + // Handle 3 cases: + // 1. no src -> src + // 2. src -> (different) src + // 3. src -> no src + break; + case Attributes.PLAYBACK_ID: + this.src = toMuxVideoURL(newValue); + break; + case Attributes.DEBUG: + const debug = this.debug; + if (!!this.mux) { + /** @TODO Link to docs for a more detailed discussion (CJP) */ + console.info( + "Cannot toggle debug mode of mux data after initialization. Make sure you set all metadata to override before setting the src." + ); + } + if (!!this.hls) { + this.hls.config.debug = debug; + } + break; + default: + break; } super.attributeChangedCallback(attrName, oldValue, newValue); From 6bd92a2bc883eb70ada0be0eed3f3c35e7ddcfaa Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Mon, 16 Aug 2021 20:06:22 -0500 Subject: [PATCH 15/17] feat(mux-video-init): example of loading metadata from a URL --- packages/mux-video/examples/index.html | 1 + packages/mux-video/examples/metadata.json | 3 +++ packages/mux-video/src/index.ts | 10 ++++++++++ 3 files changed, 14 insertions(+) create mode 100644 packages/mux-video/examples/metadata.json diff --git a/packages/mux-video/examples/index.html b/packages/mux-video/examples/index.html index 7385061db..c6d58e8a8 100644 --- a/packages/mux-video/examples/index.html +++ b/packages/mux-video/examples/index.html @@ -9,6 +9,7 @@ { } set debug(val: boolean) { + // dont' cause an infinite loop + if (val === this.debug) return; + if (val) { this.setAttribute(Attributes.DEBUG, ""); } else { @@ -212,6 +217,11 @@ class MuxVideoElement extends CustomVideoElement { this.hls.config.debug = debug; } break; + case Attributes.METADATA_URL: + if (newValue) { + fetch(newValue).then(resp => resp.json()).then(json => this.metadata = json); + } + break; default: break; } From 156ce9bbc775627a75f0e55bc10420eeafe9e05d Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Mon, 16 Aug 2021 20:16:22 -0500 Subject: [PATCH 16/17] feat(mux-video-init): (temporarily) adding polyfill to mux-video to allow server side smoke testing. --- packages/mux-video/src/index.ts | 2 + packages/mux-video/src/polyfills/window.ts | 65 ++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 packages/mux-video/src/polyfills/window.ts diff --git a/packages/mux-video/src/index.ts b/packages/mux-video/src/index.ts index b5991a58f..34593256f 100644 --- a/packages/mux-video/src/index.ts +++ b/packages/mux-video/src/index.ts @@ -1,3 +1,5 @@ +import './polyfills/window'; + import CustomVideoElement from "custom-video-element"; import mux, { Options } from "mux-embed"; diff --git a/packages/mux-video/src/polyfills/window.ts b/packages/mux-video/src/polyfills/window.ts new file mode 100644 index 000000000..f3d600176 --- /dev/null +++ b/packages/mux-video/src/polyfills/window.ts @@ -0,0 +1,65 @@ +if (!globalThis.customElements) { + globalThis.customElements = { + get() {}, + define( + _name: string, + _constructor: CustomElementConstructor, + _options: ElementDefinitionOptions + ) {}, + upgrade(_root: Node) {}, + whenDefined(_name: string) { + return Promise.resolve(); + }, + }; +} + +if (!globalThis.CustomEvent) { + class CustomEvent extends Event implements CustomEvent { + readonly detail: T; + constructor (typeArg: string, eventInitDict: CustomEventInit = {}) { + super(typeArg, eventInitDict); + // NOTE: Lazy fix for global env expectations + this.detail = eventInitDict?.detail as T; + }; + initCustomEvent( + _typeArg: string, + _canBubbleArg: boolean, + _cancelableArg: boolean, + _detailArg: T + ) {} + } + globalThis.CustomEvent = CustomEvent; +} + +if (!globalThis.HTMLElement) { + class HTMLElement { + addEventListener() {} + removeEventListener() {} + dispatchEvent() {} + } + + // NOTE: Adding ts-ignore since `HTMLElement` typedef is much larger than what we're stubbing. Consider more robust TypeScript solution (e.g. downstream usage) + // @ts-ignore + globalThis.HTMLElement = HTMLElement; +} + +if (!globalThis.document) { + const document = { + createElement( + _tagName: string, + _options?: ElementCreationOptions + ): HTMLElement { + return new HTMLElement(); + }, + }; + + // NOTE: Adding ts-ignore since `document` typedef is much larger than what we're stubbing. Consider more robust TypeScript solution (e.g. downstream usage) + // @ts-ignore + globalThis.document = document; +} + +if (!globalThis.window) { + // NOTE: Adding ts-ignore since `window` typedef is much larger than what we're stubbing. Consider more robust TypeScript solution (e.g. downstream usage) + // @ts-ignore + globalThis.window = globalThis; +} From 13a68b6ec89e30124338dfeda2b5903487939bbf Mon Sep 17 00:00:00 2001 From: Christian Pillsbury Date: Tue, 17 Aug 2021 09:04:32 -0500 Subject: [PATCH 17/17] feat(mux-video-init): handle various src attr changes. Add unload. --- .../src/customTypings/mux-embed.d.ts | 4 +- packages/mux-video/src/index.ts | 51 +++++++++++++++---- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/packages/mux-video/src/customTypings/mux-embed.d.ts b/packages/mux-video/src/customTypings/mux-embed.d.ts index 783ab4cf4..64d6ddf9b 100644 --- a/packages/mux-video/src/customTypings/mux-embed.d.ts +++ b/packages/mux-video/src/customTypings/mux-embed.d.ts @@ -76,7 +76,9 @@ declare module "mux-embed" { hb: Partial; }; - export function monitor(id: string | HTMLMediaElement, options: Options) {} + export function monitor(id: string | HTMLMediaElement, options: Options): void; export function emit(type: K, payload: EventTypesMap[K]): void; + + export function destroy(): void; } diff --git a/packages/mux-video/src/index.ts b/packages/mux-video/src/index.ts index 34593256f..490c24802 100644 --- a/packages/mux-video/src/index.ts +++ b/packages/mux-video/src/index.ts @@ -1,4 +1,4 @@ -import './polyfills/window'; +import "./polyfills/window"; import CustomVideoElement from "custom-video-element"; import mux, { Options } from "mux-embed"; @@ -27,7 +27,9 @@ const AttributeNameValues = Object.values(Attributes); const toMuxVideoURL = (playbackId: string | null) => playbackId ? `https://stream.mux.com/${playbackId}.m3u8` : null; -type HTMLVideoElementWithMux = HTMLVideoElement & { mux: typeof mux }; +const hlsSupported = Hls.isSupported(); + +type HTMLVideoElementWithMux = HTMLVideoElement & { mux?: typeof mux }; class MuxVideoElement extends CustomVideoElement { static get observedAttributes() { @@ -50,7 +52,7 @@ class MuxVideoElement extends CustomVideoElement { return this.__hls; } - get mux(): Readonly { + get mux(): Readonly | undefined { return this.nativeEl.mux; } @@ -104,6 +106,7 @@ class MuxVideoElement extends CustomVideoElement { } } + /** @TODO Refactor as an independent function (CJP) */ load() { /** @TODO Add custom errors + error codes */ if (!this.src) { @@ -114,7 +117,7 @@ class MuxVideoElement extends CustomVideoElement { const env_key = this.getAttribute(Attributes.ENV_KEY); const debug = this.debug; - if (Hls.isSupported()) { + if (hlsSupported) { const hls = new Hls({ // Kind of like preload metadata, but causes spinner. // autoStartLoad: false, @@ -182,6 +185,20 @@ class MuxVideoElement extends CustomVideoElement { } } + unload() { + // NOTE: I believe we cannot reliably "recycle" hls player instances, but should confirm at least for optimization reasons. + if (this.__hls) { + this.__hls.detachMedia(); + this.__hls.destroy(); + this.__hls = undefined; + } + if (this.nativeEl.mux) { + this.nativeEl.mux.destroy(); + delete this.nativeEl.mux; + } + } + + // NOTE: This was carried over from hls-video-element. Is it needed for an edge case? // play() { // if (this.readyState === 0 && this.networkState < 2) { // this.load(); @@ -199,12 +216,20 @@ class MuxVideoElement extends CustomVideoElement { ) { switch (attrName) { case "src": - // Handle 3 cases: - // 1. no src -> src - // 2. src -> (different) src - // 3. src -> no src + const hadSrc = !!oldValue; + const hasSrc = !!newValue; + if (!hadSrc && hasSrc) { + this.load(); + } else if (hadSrc && !hasSrc) { + this.unload(); + /** @TODO Test this thoroughly (async?) and confirm unload() necessary (CJP) */ + } else if (hadSrc && hasSrc) { + this.unload(); + this.load(); + } break; case Attributes.PLAYBACK_ID: + /** @TODO Improv+Discuss - how should playback-id update wrt src attr changes (and vice versa) (CJP) */ this.src = toMuxVideoURL(newValue); break; case Attributes.DEBUG: @@ -221,7 +246,9 @@ class MuxVideoElement extends CustomVideoElement { break; case Attributes.METADATA_URL: if (newValue) { - fetch(newValue).then(resp => resp.json()).then(json => this.metadata = json); + fetch(newValue) + .then((resp) => resp.json()) + .then((json) => (this.metadata = json)); } break; default: @@ -231,12 +258,18 @@ class MuxVideoElement extends CustomVideoElement { super.attributeChangedCallback(attrName, oldValue, newValue); } + disconnectedCallback() { + this.unload(); + } + + /** @TODO Followup - investigate why this is necessary (attributeChanged not invoked on initial load when setting playback-id) (CJP) */ connectedCallback() { // Only auto-load if we have a src if (this.src) { this.load(); } + // NOTE: This was carried over from hls-video-element. Is it needed for an edge case? // Not preloading might require faking the play() promise // so that you can call play(), call load() within that // But wait until MANIFEST_PARSED to actually call play()