├── .build └── copy-pkg-files.js ├── .editorconfig ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── docs.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── REPRO.md └── workflows │ └── weekly.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.cjs ├── .scripts ├── copy.js ├── release.js ├── run-on-change.js ├── sandbox.js └── test-node-esm.js ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASES.md ├── assets ├── audio-player.png ├── cypress-dark.png ├── cypress-light.png ├── media-icons.png ├── mux.png ├── vercel-dark.png ├── vercel-light.png └── video-player.png ├── cliff.toml ├── examples └── README.md ├── package.json ├── packages ├── react │ ├── .templates │ │ └── sandbox │ │ │ ├── document.css │ │ │ ├── favicon-32x32.png │ │ │ ├── index.html │ │ │ ├── main.tsx │ │ │ ├── player.css │ │ │ ├── player.tsx │ │ │ └── tracks.ts │ ├── LICENSE │ ├── README.md │ ├── analyze.config.ts │ ├── build │ │ └── build-icons.js │ ├── npm │ │ └── analyze.json.d.ts │ ├── package.json │ ├── rollup.config.ts │ ├── src │ │ ├── components │ │ │ ├── announcer.tsx │ │ │ ├── layouts │ │ │ │ ├── default │ │ │ │ │ ├── audio-layout.tsx │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── hooks.ts │ │ │ │ │ ├── icons.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── media-layout.tsx │ │ │ │ │ ├── slots.tsx │ │ │ │ │ ├── ui.ts │ │ │ │ │ ├── ui │ │ │ │ │ │ ├── announcer.tsx │ │ │ │ │ │ ├── buttons.tsx │ │ │ │ │ │ ├── captions.tsx │ │ │ │ │ │ ├── controls.tsx │ │ │ │ │ │ ├── keyboard-display.tsx │ │ │ │ │ │ ├── menus │ │ │ │ │ │ │ ├── accessibility-menu.tsx │ │ │ │ │ │ │ ├── audio-menu.tsx │ │ │ │ │ │ │ ├── captions-menu.tsx │ │ │ │ │ │ │ ├── chapters-menu.tsx │ │ │ │ │ │ │ ├── font-menu.tsx │ │ │ │ │ │ │ ├── items │ │ │ │ │ │ │ │ ├── menu-checkbox.tsx │ │ │ │ │ │ │ │ ├── menu-items.tsx │ │ │ │ │ │ │ │ └── menu-slider.tsx │ │ │ │ │ │ │ ├── playback-menu.tsx │ │ │ │ │ │ │ ├── settings-menu.tsx │ │ │ │ │ │ │ └── utils.ts │ │ │ │ │ │ ├── sliders.tsx │ │ │ │ │ │ ├── time.tsx │ │ │ │ │ │ ├── title.tsx │ │ │ │ │ │ └── tooltip.tsx │ │ │ │ │ └── video-layout.tsx │ │ │ │ ├── plyr │ │ │ │ │ ├── context.ts │ │ │ │ │ ├── icons.tsx │ │ │ │ │ ├── icons │ │ │ │ │ │ ├── plyr-airplay.js │ │ │ │ │ │ ├── plyr-captions-off.js │ │ │ │ │ │ ├── plyr-captions-on.js │ │ │ │ │ │ ├── plyr-download.js │ │ │ │ │ │ ├── plyr-enter-fullscreen.js │ │ │ │ │ │ ├── plyr-exit-fullscreen.js │ │ │ │ │ │ ├── plyr-fast-forward.js │ │ │ │ │ │ ├── plyr-muted.js │ │ │ │ │ │ ├── plyr-pause.js │ │ │ │ │ │ ├── plyr-pip.js │ │ │ │ │ │ ├── plyr-play.js │ │ │ │ │ │ ├── plyr-restart.js │ │ │ │ │ │ ├── plyr-rewind.js │ │ │ │ │ │ ├── plyr-settings.js │ │ │ │ │ │ └── plyr-volume.js │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── props.ts │ │ │ │ │ └── slots.tsx │ │ │ │ ├── remotion-ui.ts │ │ │ │ └── utils.ts │ │ │ ├── player-callbacks.ts │ │ │ ├── player.tsx │ │ │ ├── primitives │ │ │ │ ├── instances.ts │ │ │ │ ├── nodes.tsx │ │ │ │ └── slot.tsx │ │ │ ├── provider.tsx │ │ │ ├── text-track.tsx │ │ │ └── ui │ │ │ │ ├── buttons │ │ │ │ ├── airplay-button.tsx │ │ │ │ ├── caption-button.tsx │ │ │ │ ├── fullscreen-button.tsx │ │ │ │ ├── google-cast-button.tsx │ │ │ │ ├── live-button.tsx │ │ │ │ ├── mute-button.tsx │ │ │ │ ├── pip-button.tsx │ │ │ │ ├── play-button.tsx │ │ │ │ ├── seek-button.tsx │ │ │ │ └── toggle-button.tsx │ │ │ │ ├── caption.tsx │ │ │ │ ├── captions.tsx │ │ │ │ ├── chapter-title.tsx │ │ │ │ ├── controls.tsx │ │ │ │ ├── gesture.tsx │ │ │ │ ├── menu.tsx │ │ │ │ ├── poster.tsx │ │ │ │ ├── radio-group.tsx │ │ │ │ ├── sliders │ │ │ │ ├── audio-gain-slider.tsx │ │ │ │ ├── quality-slider.tsx │ │ │ │ ├── slider-callbacks.ts │ │ │ │ ├── slider-value.tsx │ │ │ │ ├── slider.tsx │ │ │ │ ├── speed-slider.tsx │ │ │ │ ├── time-slider.tsx │ │ │ │ └── volume-slider.tsx │ │ │ │ ├── spinner.tsx │ │ │ │ ├── thumbnail.tsx │ │ │ │ ├── time.tsx │ │ │ │ ├── title.tsx │ │ │ │ └── tooltip.tsx │ │ ├── globals.d.ts │ │ ├── hooks │ │ │ ├── create-text-track.ts │ │ │ ├── options │ │ │ │ ├── use-audio-gain-options.ts │ │ │ │ ├── use-audio-options.ts │ │ │ │ ├── use-caption-options.ts │ │ │ │ ├── use-chapter-options.ts │ │ │ │ ├── use-playback-rate-options.ts │ │ │ │ └── use-video-quality-options.ts │ │ │ ├── use-active-text-cues.ts │ │ │ ├── use-active-text-track.ts │ │ │ ├── use-chapter-title.ts │ │ │ ├── use-dom.ts │ │ │ ├── use-media-context.ts │ │ │ ├── use-media-player.ts │ │ │ ├── use-media-provider.ts │ │ │ ├── use-media-remote.ts │ │ │ ├── use-media-state.ts │ │ │ ├── use-signals.ts │ │ │ ├── use-slider-preview.ts │ │ │ ├── use-slider-state.ts │ │ │ ├── use-state.ts │ │ │ ├── use-text-cues.ts │ │ │ └── use-thumbnails.ts │ │ ├── icon.ts │ │ ├── index.ts │ │ ├── providers │ │ │ └── remotion │ │ │ │ ├── index.ts │ │ │ │ ├── layout-engine.ts │ │ │ │ ├── loader.ts │ │ │ │ ├── playback-engine.ts │ │ │ │ ├── provider.tsx │ │ │ │ ├── type-check.ts │ │ │ │ ├── types.ts │ │ │ │ ├── ui │ │ │ │ ├── context.tsx │ │ │ │ ├── error-boundary.tsx │ │ │ │ ├── poster.tsx │ │ │ │ ├── slider-thumbnail.tsx │ │ │ │ └── thumbnail.tsx │ │ │ │ └── validate.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vite.config.ts └── vidstack │ ├── .templates │ └── sandbox │ │ ├── favicon-32x32.png │ │ ├── index.html │ │ ├── main.ts │ │ └── styles.css │ ├── LICENSE │ ├── README.md │ ├── analyze.config.ts │ ├── build │ ├── build-styles.js │ ├── rollup-decorators.ts │ ├── rollup-minify.js │ └── rollup-ts.js │ ├── npm │ ├── analyze.json.d.ts │ ├── bundle.d.ts │ ├── dom.d.ts │ ├── empty.vtt │ ├── google-cast.d.ts │ ├── player │ │ ├── index.d.ts │ │ ├── layouts │ │ │ ├── default.d.ts │ │ │ ├── index.d.ts │ │ │ └── plyr.d.ts │ │ └── ui.d.ts │ ├── tailwind.cjs │ └── tailwind.d.cts │ ├── package.json │ ├── rollup.config.ts │ ├── src │ ├── components │ │ ├── aria │ │ │ └── announcer.ts │ │ ├── layouts │ │ │ ├── default │ │ │ │ ├── audio-layout.ts │ │ │ │ ├── context.ts │ │ │ │ ├── default-layout.ts │ │ │ │ ├── props.ts │ │ │ │ ├── translations.ts │ │ │ │ └── video-layout.ts │ │ │ └── plyr │ │ │ │ ├── context.ts │ │ │ │ ├── plyr-layout.ts │ │ │ │ ├── props.ts │ │ │ │ └── translations.ts │ │ ├── player.ts │ │ ├── provider │ │ │ ├── provider.ts │ │ │ ├── source-select.ts │ │ │ └── tracks.ts │ │ └── ui │ │ │ ├── buttons │ │ │ ├── airplay-button.ts │ │ │ ├── caption-button.ts │ │ │ ├── fullscreen-button.ts │ │ │ ├── google-cast-button.ts │ │ │ ├── live-button.ts │ │ │ ├── mute-button.ts │ │ │ ├── pip-button.ts │ │ │ ├── play-button.ts │ │ │ ├── seek-button.ts │ │ │ ├── toggle-button-controller.ts │ │ │ └── toggle-button.ts │ │ │ ├── captions │ │ │ ├── captions-renderer.ts │ │ │ └── captions.ts │ │ │ ├── controls-group.ts │ │ │ ├── controls.ts │ │ │ ├── gesture.ts │ │ │ ├── menu │ │ │ ├── menu-button.ts │ │ │ ├── menu-context.ts │ │ │ ├── menu-focus-controller.ts │ │ │ ├── menu-item.ts │ │ │ ├── menu-items.ts │ │ │ ├── menu-portal.ts │ │ │ ├── menu.ts │ │ │ ├── radio-groups │ │ │ │ ├── audio-gain-radio-group.ts │ │ │ │ ├── audio-radio-group.ts │ │ │ │ ├── captions-radio-group.ts │ │ │ │ ├── chapters-radio-group.ts │ │ │ │ ├── quality-radio-group.ts │ │ │ │ └── speed-radio-group.ts │ │ │ └── radio │ │ │ │ ├── radio-controller.ts │ │ │ │ ├── radio-group-controller.ts │ │ │ │ ├── radio-group.ts │ │ │ │ └── radio.ts │ │ │ ├── popper │ │ │ └── popper.ts │ │ │ ├── poster.ts │ │ │ ├── sliders │ │ │ ├── audio-gain-slider.ts │ │ │ ├── quality-slider.ts │ │ │ ├── slider-preview.ts │ │ │ ├── slider-thumbnail.ts │ │ │ ├── slider-value.ts │ │ │ ├── slider-video.ts │ │ │ ├── slider │ │ │ │ ├── api │ │ │ │ │ ├── cssvars.ts │ │ │ │ │ ├── events.ts │ │ │ │ │ └── state.ts │ │ │ │ ├── events-controller.ts │ │ │ │ ├── format.ts │ │ │ │ ├── slider-context.ts │ │ │ │ ├── slider-controller.ts │ │ │ │ ├── slider.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── speed-slider.ts │ │ │ ├── time-slider │ │ │ │ ├── slider-chapters.ts │ │ │ │ └── time-slider.ts │ │ │ └── volume-slider.ts │ │ │ ├── thumbnails │ │ │ ├── thumbnail-loader.ts │ │ │ └── thumbnail.ts │ │ │ ├── time.ts │ │ │ └── tooltip │ │ │ ├── tooltip-content.ts │ │ │ ├── tooltip-context.ts │ │ │ ├── tooltip-trigger.ts │ │ │ └── tooltip.ts │ ├── core │ │ ├── api │ │ │ ├── media-attrs.ts │ │ │ ├── media-context.ts │ │ │ ├── media-events.ts │ │ │ ├── media-request-events.ts │ │ │ ├── player-controller.ts │ │ │ ├── player-events.ts │ │ │ ├── player-props.ts │ │ │ ├── player-state.ts │ │ │ ├── src-types.ts │ │ │ └── types.ts │ │ ├── controls.ts │ │ ├── font │ │ │ ├── font-options.ts │ │ │ └── font-vars.ts │ │ ├── keyboard │ │ │ ├── aria-shortcuts.ts │ │ │ ├── controller.ts │ │ │ └── types.ts │ │ ├── quality │ │ │ ├── events.ts │ │ │ ├── symbols.ts │ │ │ ├── utils.ts │ │ │ └── video-quality.ts │ │ ├── state │ │ │ ├── media-events-logger.ts │ │ │ ├── media-load-controller.ts │ │ │ ├── media-player-delegate.ts │ │ │ ├── media-request-manager.ts │ │ │ ├── media-state-manager.ts │ │ │ ├── media-state-sync.ts │ │ │ ├── media-storage.ts │ │ │ ├── navigator-media-session.ts │ │ │ ├── remote-control.ts │ │ │ └── tracked-media-events.ts │ │ ├── time-ranges.ts │ │ └── tracks │ │ │ ├── audio │ │ │ ├── audio-tracks.ts │ │ │ └── events.ts │ │ │ └── text │ │ │ ├── events.ts │ │ │ ├── render │ │ │ ├── libass-text-renderer.ts │ │ │ ├── native-text-renderer.ts │ │ │ └── text-renderer.ts │ │ │ ├── symbols.ts │ │ │ ├── text-track.ts │ │ │ ├── text-tracks.ts │ │ │ └── utils.ts │ ├── elements │ │ ├── bundles │ │ │ ├── cdn-legacy │ │ │ │ ├── player-with-default.ts │ │ │ │ ├── player-with-layouts.ts │ │ │ │ ├── player-with-plyr.ts │ │ │ │ └── player.ts │ │ │ ├── cdn │ │ │ │ ├── player.core.ts │ │ │ │ ├── player.ts │ │ │ │ └── plyr.ts │ │ │ ├── icons.ts │ │ │ ├── player-layouts │ │ │ │ ├── default.ts │ │ │ │ ├── index.ts │ │ │ │ └── plyr.ts │ │ │ ├── player-ui.ts │ │ │ └── player.ts │ │ ├── define │ │ │ ├── announcer-element.ts │ │ │ ├── buttons │ │ │ │ ├── airplay-button-element.ts │ │ │ │ ├── caption-button-element.ts │ │ │ │ ├── fullscreen-button-element.ts │ │ │ │ ├── google-cast-button-element.ts │ │ │ │ ├── live-button-element.ts │ │ │ │ ├── mute-button-element.ts │ │ │ │ ├── pip-button-element.ts │ │ │ │ ├── play-button-element.ts │ │ │ │ ├── seek-button-element.ts │ │ │ │ └── toggle-button-element.ts │ │ │ ├── captions-element.ts │ │ │ ├── chapter-title-element.ts │ │ │ ├── controls-element.ts │ │ │ ├── controls-group-element.ts │ │ │ ├── gesture-element.ts │ │ │ ├── layouts │ │ │ │ ├── default │ │ │ │ │ ├── audio-layout-element.ts │ │ │ │ │ ├── audio-layout.ts │ │ │ │ │ ├── icons-loader.ts │ │ │ │ │ ├── icons.ts │ │ │ │ │ ├── slots.ts │ │ │ │ │ ├── ui │ │ │ │ │ │ ├── announcer.ts │ │ │ │ │ │ ├── buttons.ts │ │ │ │ │ │ ├── captions.ts │ │ │ │ │ │ ├── controls.ts │ │ │ │ │ │ ├── keyboard-display.ts │ │ │ │ │ │ ├── menu │ │ │ │ │ │ │ ├── accessibility-menu.ts │ │ │ │ │ │ │ ├── audio-menu.ts │ │ │ │ │ │ │ ├── captions-menu.ts │ │ │ │ │ │ │ ├── chapters-menu.ts │ │ │ │ │ │ │ ├── font-menu.ts │ │ │ │ │ │ │ ├── items │ │ │ │ │ │ │ │ ├── menu-checkbox.ts │ │ │ │ │ │ │ │ ├── menu-items.ts │ │ │ │ │ │ │ │ └── menu-slider.ts │ │ │ │ │ │ │ ├── menu-portal.ts │ │ │ │ │ │ │ ├── playback-menu.ts │ │ │ │ │ │ │ └── settings-menu.ts │ │ │ │ │ │ ├── slider.ts │ │ │ │ │ │ ├── time.ts │ │ │ │ │ │ ├── title.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── video-layout-element.ts │ │ │ │ │ └── video-layout.ts │ │ │ │ ├── icons │ │ │ │ │ ├── icons-loader.ts │ │ │ │ │ └── layout-icons-loader.ts │ │ │ │ ├── layout-element.ts │ │ │ │ ├── layout-name.ts │ │ │ │ ├── plyr │ │ │ │ │ ├── icons-loader.ts │ │ │ │ │ ├── icons.ts │ │ │ │ │ ├── icons │ │ │ │ │ │ ├── plyr-airplay.js │ │ │ │ │ │ ├── plyr-captions-off.js │ │ │ │ │ │ ├── plyr-captions-on.js │ │ │ │ │ │ ├── plyr-download.js │ │ │ │ │ │ ├── plyr-enter-fullscreen.js │ │ │ │ │ │ ├── plyr-exit-fullscreen.js │ │ │ │ │ │ ├── plyr-fast-forward.js │ │ │ │ │ │ ├── plyr-muted.js │ │ │ │ │ │ ├── plyr-pause.js │ │ │ │ │ │ ├── plyr-pip.js │ │ │ │ │ │ ├── plyr-play.js │ │ │ │ │ │ ├── plyr-restart.js │ │ │ │ │ │ ├── plyr-rewind.js │ │ │ │ │ │ ├── plyr-settings.js │ │ │ │ │ │ └── plyr-volume.js │ │ │ │ │ ├── plyr-layout-element.ts │ │ │ │ │ └── ui.ts │ │ │ │ ├── slot-manager.ts │ │ │ │ └── slot-observer.ts │ │ │ ├── menus │ │ │ │ ├── _template.ts │ │ │ │ ├── audio-gain-group-element.ts │ │ │ │ ├── audio-radio-group-element.ts │ │ │ │ ├── captions-radio-group-element.ts │ │ │ │ ├── chapters-radio-group-element.ts │ │ │ │ ├── menu-button-element.ts │ │ │ │ ├── menu-element.ts │ │ │ │ ├── menu-item-element.ts │ │ │ │ ├── menu-items-element.ts │ │ │ │ ├── menu-portal-element.ts │ │ │ │ ├── quality-radio-group-element.ts │ │ │ │ ├── radio-element.ts │ │ │ │ ├── radio-group-element.ts │ │ │ │ └── speed-radio-group-element.ts │ │ │ ├── player-element.ts │ │ │ ├── poster-element.ts │ │ │ ├── provider-cast-display.ts │ │ │ ├── provider-element.ts │ │ │ ├── sliders │ │ │ │ ├── audio-gain-slider-element.ts │ │ │ │ ├── quality-slider-element.ts │ │ │ │ ├── slider-chapters-element.ts │ │ │ │ ├── slider-element.ts │ │ │ │ ├── slider-preview-element.ts │ │ │ │ ├── slider-steps-element.ts │ │ │ │ ├── slider-thumbnail-element.ts │ │ │ │ ├── slider-value-element.ts │ │ │ │ ├── slider-video-element.ts │ │ │ │ ├── speed-slider-element.ts │ │ │ │ ├── time-slider-element.ts │ │ │ │ └── volume-slider-element.ts │ │ │ ├── spinner-element.ts │ │ │ ├── thumbnail-element.ts │ │ │ ├── time-element.ts │ │ │ ├── title-element.ts │ │ │ └── tooltips │ │ │ │ ├── tooltip-content-element.ts │ │ │ │ ├── tooltip-element.ts │ │ │ │ └── tooltip-trigger-element.ts │ │ ├── icon.ts │ │ ├── index.ts │ │ ├── lit │ │ │ ├── directives │ │ │ │ └── signal.ts │ │ │ ├── html.ts │ │ │ └── lit-element.ts │ │ └── state-controller.ts │ ├── exports │ │ ├── components.ts │ │ ├── core.ts │ │ ├── events.ts │ │ ├── font.ts │ │ ├── foundation.ts │ │ ├── maverick.ts │ │ ├── providers.ts │ │ └── utils.ts │ ├── foundation │ │ ├── fullscreen │ │ │ ├── controller.ts │ │ │ └── events.ts │ │ ├── list │ │ │ ├── list.ts │ │ │ ├── select-list.ts │ │ │ └── symbols.ts │ │ ├── logger │ │ │ ├── colors.ts │ │ │ ├── controller.ts │ │ │ ├── events.ts │ │ │ ├── grouped-log.ts │ │ │ ├── log-level.ts │ │ │ ├── log-printer.ts │ │ │ └── ms.ts │ │ ├── observers │ │ │ ├── focus-visible.ts │ │ │ ├── intersection-observer.ts │ │ │ ├── page-visibility.ts │ │ │ └── raf-loop.ts │ │ ├── orientation │ │ │ ├── controller.ts │ │ │ ├── events.ts │ │ │ └── types.ts │ │ └── queue │ │ │ ├── queue.ts │ │ │ ├── request-queue.test.ts │ │ │ └── request-queue.ts │ ├── global │ │ ├── layouts │ │ │ ├── default.ts │ │ │ ├── loader.ts │ │ │ └── plyr.ts │ │ ├── player.ts │ │ └── plyr.ts │ ├── globals.d.ts │ ├── index.ts │ ├── plugins.ts │ ├── providers │ │ ├── audio │ │ │ ├── loader.ts │ │ │ └── provider.ts │ │ ├── dash │ │ │ ├── dash.ts │ │ │ ├── events.ts │ │ │ ├── lib-loader.ts │ │ │ ├── loader.ts │ │ │ ├── provider.ts │ │ │ └── types.ts │ │ ├── embed │ │ │ └── EmbedProvider.ts │ │ ├── google-cast │ │ │ ├── events.ts │ │ │ ├── loader.ts │ │ │ ├── media-info.ts │ │ │ ├── provider.ts │ │ │ ├── tracks.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── hls │ │ │ ├── events.ts │ │ │ ├── hls.ts │ │ │ ├── lib-loader.ts │ │ │ ├── loader.ts │ │ │ ├── provider.ts │ │ │ └── types.ts │ │ ├── html │ │ │ ├── audio │ │ │ │ ├── audio-context.ts │ │ │ │ └── audio-gain.ts │ │ │ ├── html–media-events.ts │ │ │ ├── native-audio-tracks.ts │ │ │ ├── provider.ts │ │ │ └── remote-playback.ts │ │ ├── type-check.ts │ │ ├── types.ts │ │ ├── video │ │ │ ├── loader.ts │ │ │ ├── native-hls-text-tracks.ts │ │ │ ├── picture-in-picture.ts │ │ │ ├── presentation │ │ │ │ ├── events.ts │ │ │ │ └── video-presentation.ts │ │ │ └── provider.ts │ │ ├── vimeo │ │ │ ├── embed │ │ │ │ ├── command.ts │ │ │ │ ├── event.ts │ │ │ │ ├── message.ts │ │ │ │ ├── misc.ts │ │ │ │ └── params.ts │ │ │ ├── loader.ts │ │ │ ├── provider.ts │ │ │ └── utils.ts │ │ └── youtube │ │ │ ├── embed │ │ │ ├── command.ts │ │ │ ├── event.ts │ │ │ ├── message.ts │ │ │ ├── params.ts │ │ │ ├── quality.ts │ │ │ └── state.ts │ │ │ ├── loader.ts │ │ │ ├── provider.ts │ │ │ └── utils.ts │ ├── tailwind.test.ts │ ├── test-utils │ │ ├── index.ts │ │ └── setup.ts │ └── utils │ │ ├── aria.ts │ │ ├── array.ts │ │ ├── color.ts │ │ ├── dom.ts │ │ ├── error.ts │ │ ├── language.ts │ │ ├── manifest.ts │ │ ├── mime.ts │ │ ├── network.ts │ │ ├── number.ts │ │ ├── promise.ts │ │ ├── scroll.ts │ │ ├── support.ts │ │ ├── time.test.ts │ │ └── time.ts │ ├── styles │ └── player │ │ ├── base.css │ │ ├── default │ │ ├── buffering.css │ │ ├── buttons.css │ │ ├── captions.css │ │ ├── chapter-title.css │ │ ├── controls.css │ │ ├── gestures.css │ │ ├── icons.css │ │ ├── keyboard.css │ │ ├── layouts │ │ │ ├── audio.css │ │ │ └── video.css │ │ ├── menus.css │ │ ├── poster.css │ │ ├── sliders.css │ │ ├── thumbnail.css │ │ ├── time.css │ │ └── tooltips.css │ │ └── plyr │ │ └── theme.css │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── turbo.json /.build/copy-pkg-files.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import fs from 'fs-extra'; 4 | 5 | const cwd = process.cwd(); 6 | 7 | export function copyPkgFiles() { 8 | const pkgPath = path.resolve(cwd, 'package.json'), 9 | pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')), 10 | distDir = path.resolve(cwd, 'dist-npm'); 11 | 12 | const root = (p) => path.resolve(cwd, p), 13 | dist = (p) => path.resolve(distDir, p); 14 | 15 | const validPkgFields = [ 16 | 'name', 17 | 'description', 18 | 'private', 19 | 'version', 20 | 'license', 21 | 'type', 22 | 'types', 23 | 'sideEffects', 24 | 'engines', 25 | 'dependencies', 26 | 'peerDependencies', 27 | 'contributors', 28 | 'repository', 29 | 'bugs', 30 | 'exports', 31 | 'publishConfig', 32 | 'keywords', 33 | ]; 34 | 35 | // Create package.json. 36 | const distPkg = {}; 37 | for (const field of validPkgFields) distPkg[field] = pkg[field]; 38 | 39 | // Use publish fields. 40 | distPkg.types = pkg.$types; 41 | distPkg.exports = pkg.$exports; 42 | 43 | if (!fs.existsSync(distDir)) { 44 | fs.mkdirSync(distDir); 45 | } 46 | 47 | fs.writeFileSync(dist('package.json'), JSON.stringify(distPkg, null, 2), 'utf-8'); 48 | 49 | // Copy over license and readme. 50 | fs.copyFileSync(root('LICENSE'), dist('LICENSE')); 51 | 52 | if (fs.existsSync(root('README.md'))) { 53 | fs.copyFileSync(root('README.md'), dist('README.md')); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: If you're sure it's a bug, or confirmed it via discord/discussions then can create a bug report. 4 | labels: bug 5 | --- 6 | 7 | ### Current Behavior: 8 | 9 | <!-- A concise description of what you're experiencing. --> 10 | 11 | ### Expected Behavior: 12 | 13 | <!-- A concise description of what you expected to happen. --> 14 | 15 | ### Steps To Reproduce: 16 | 17 | <!-- 18 | Example: steps to reproduce the behavior: 19 | 1. In this environment... 20 | 2. With this config... 21 | 3. Run '...' 22 | 4. See error... 23 | --> 24 | 25 | **Reproduction Link:** [How to create a repro?][repro] 26 | 27 | [repro]: https://github.com/vidstack/player/blob/main/.github/REPRO.md 28 | 29 | ### Environment: 30 | 31 | <!-- 32 | Example: 33 | - Framework: React 34 | - Meta Framework: Next.js 35 | - Node: 16.0.0 36 | - Device: iPhone@13 37 | - OS: iOS@14 38 | - Browser: Chrome@22 39 | --> 40 | 41 | ### Anything Else? 42 | 43 | <!-- 44 | Links? Screenshots? Anything that will give us more context about the issue that you are encountering. 45 | --> 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ❓ Get Help 4 | about: Having a hard time with something? Open a question in discussions to get help. 5 | url: https://github.com/vidstack/player/discussions/new?category=q-a 6 | - name: 💬 Chat 7 | about: Like to chat with the community about something general? Join our Discord channel. 8 | url: https://discord.gg/QAjfh2gZE4 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✍️ Documentation 3 | about: Let us know how we can improve our docs. 4 | labels: docs 5 | --- 6 | 7 | ### URL: 8 | 9 | <!-- 10 | The URL of an existing or new documentation page that is being addressed. 11 | 12 | Example: https://www.vidstack.io/docs/player/getting-started/installation 13 | --> 14 | 15 | ### Describe: 16 | 17 | <!-- Clearly describe how the documentation can be improved. --> 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature Request 3 | about: Suggest an idea for this project. 4 | labels: feature 5 | --- 6 | 7 | ### Related Problem: 8 | 9 | <!-- If related to a problem, provide a clear and concise description of what the problem is. --> 10 | 11 | ### Describe: 12 | 13 | <!-- A clear and concise description of what you want to happen. --> 14 | 15 | ### Alternatives: 16 | 17 | <!-- A clear and concise description of alternative solutions or features you've considered. --> 18 | 19 | ### Anything Else? 20 | 21 | <!-- 22 | Links? Screenshots? Anything that will give us more context about this feature request. 23 | --> 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Related: 2 | 3 | <!-- If possible, link to other issues and PRs as appropriate. --> 4 | 5 | ### Description: 6 | 7 | <!-- A clear and concise description of what the PR does. --> 8 | 9 | ### Ready? 10 | 11 | <!-- Is this PR ready to be reviewed? --> 12 | 13 | ### Anything Else? 14 | 15 | <!-- Links? References? Screenshots? Anything that will give us more context about the PR. --> 16 | 17 | ### Review Process: 18 | 19 | <!-- If possible, provide a checklist for what you'd expect us to do in order to review this PR. --> 20 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .code 2 | globals.d.ts 3 | *.png 4 | *.jpeg 5 | *.mp4 6 | *.mp3 7 | *.vtt 8 | *.ico 9 | *.toml 10 | 11 | **/partials/markup 12 | **/partials/import 13 | **/dist 14 | 15 | .gitignore 16 | .gitkeep 17 | .prettierignore 18 | 19 | mangle.json 20 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | useTabs: false, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | printWidth: 100, 6 | tabWidth: 2, 7 | plugins: [ 8 | require.resolve('prettier-plugin-tailwindcss'), 9 | require.resolve('@ianvs/prettier-plugin-sort-imports'), 10 | ], 11 | // Sort Imports 12 | importOrder: [ 13 | '^clsx#39;, 14 | '^react#39;, 15 | '^next', 16 | '', 17 | '~.*icons', 18 | '', 19 | '.css#39;, 20 | '', 21 | '^node:', 22 | '<THIRD_PARTY_MODULES>', 23 | '', 24 | '.webp?#39;, 25 | '.mp4?#39;, 26 | '', 27 | '^[$]', 28 | '^[../]', 29 | '^(?!.*[.](png|webp|mp4)$)[./].*#39;, 30 | ], 31 | importOrderParserPlugins: ['jsx', 'typescript', 'decorators'], 32 | importOrderSeparation: true, 33 | importOrderSortSpecifiers: true, 34 | importOrderCaseInsensitive: true, 35 | }; 36 | -------------------------------------------------------------------------------- /.scripts/copy.js: -------------------------------------------------------------------------------- 1 | import chokidar from 'chokidar'; 2 | import fs from 'fs-extra'; 3 | import { globbySync } from 'globby'; 4 | import kleur from 'kleur'; 5 | import minimist from 'minimist'; 6 | import path from 'path'; 7 | 8 | const args = minimist(process.argv.slice(2)); 9 | 10 | const watch = args.watch || args.w; 11 | 12 | const targetDir = path.resolve(process.cwd(), args.entry); 13 | const destDir = path.resolve(process.cwd(), args.outdir); 14 | const overwrite = args.overwrite !== 'false'; 15 | const glob = args.glob ?? '**'; 16 | 17 | if (!args.entry) { 18 | console.error(kleur.red(`\n\n🚨 Missing entry argument \`--entry\`\n\n`)); 19 | } 20 | 21 | if (!args.outdir) { 22 | console.error(kleur.red(`\n\n🚨 Missing outdir argument \`--outdir\`\n\n`)); 23 | } 24 | 25 | if (!fs.existsSync(destDir)) { 26 | fs.mkdir(destDir); 27 | } 28 | 29 | function resolveDest(file) { 30 | return path.resolve(destDir, path.relative(targetDir, file)); 31 | } 32 | 33 | if (watch) { 34 | chokidar 35 | .watch(glob, { cwd: targetDir }) 36 | .on('change', (file) => fs.copy(file, resolveDest(file))) 37 | .on('add', (file) => fs.copy(file, resolveDest(file))) 38 | .on('unlink', (file) => fs.remove(resolveDest(file))); 39 | } else { 40 | const files = globbySync(glob, { absolute: true, cwd: targetDir }); 41 | files.forEach((file) => { 42 | fs.copy(file, resolveDest(file), { overwrite }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /.scripts/run-on-change.js: -------------------------------------------------------------------------------- 1 | import chokidar from 'chokidar'; 2 | import { execa } from 'execa'; 3 | import kleur from 'kleur'; 4 | import minimist from 'minimist'; 5 | 6 | const args = minimist(process.argv.slice(2)); 7 | 8 | if (!args.glob) { 9 | console.error(kleur.red(`\n\n🚨 Missing glob argument \`--glob\`\n\n`)); 10 | } 11 | 12 | if (!args.scripts) { 13 | console.error(kleur.red(`\n\n🚨 Missing scripts argument \`--scripts\`\n\n`)); 14 | } 15 | 16 | const scripts = args.scripts.includes(',') ? args.scripts.split(',') : [args.scripts]; 17 | 18 | let running = false; 19 | async function onChange() { 20 | if (running) return; 21 | 22 | running = true; 23 | for (const script of scripts) await execa('pnpm', ['run', script], { stdio: 'inherit' }); 24 | running = false; 25 | } 26 | 27 | onChange(); 28 | chokidar.watch(args.glob).on('change', onChange).on('unlink', onChange); 29 | -------------------------------------------------------------------------------- /.scripts/sandbox.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | import { execa } from 'execa'; 5 | 6 | const __cwd = process.cwd(); 7 | 8 | async function main() { 9 | const SANDBOX_DIR = path.resolve(__cwd, 'sandbox'); 10 | const SANDBOX_SERVER_FILE = path.resolve(SANDBOX_DIR, 'server.js'); 11 | const SANDBOX_NODE_MODULES = path.resolve(SANDBOX_DIR, 'node_modules'); 12 | const SANDBOX_PKG = path.resolve(SANDBOX_DIR, 'package.json'); 13 | 14 | if (!fs.existsSync(SANDBOX_DIR)) { 15 | await execa( 16 | 'node', 17 | [ 18 | '../../.scripts/copy.js', 19 | '--entry=.templates/sandbox', 20 | '--outdir=sandbox', 21 | '--overwrite=false', 22 | ], 23 | { stdio: 'inherit' }, 24 | ); 25 | } 26 | 27 | if (fs.existsSync(SANDBOX_PKG) && !fs.existsSync(SANDBOX_NODE_MODULES)) { 28 | await execa('pnpm', ['-C', 'sandbox', 'i'], { stdio: 'inherit' }); 29 | } 30 | 31 | if (fs.existsSync(SANDBOX_SERVER_FILE)) { 32 | await execa('node', [SANDBOX_SERVER_FILE], { stdio: 'inherit' }); 33 | } else { 34 | await execa('vite', ['--open=/sandbox/index.html', '--port=3100', '--host'], { 35 | stdio: 'inherit', 36 | }); 37 | } 38 | } 39 | 40 | main().catch((e) => { 41 | if (e.exitCode === 1) return; 42 | console.error(e); 43 | process.exit(1); 44 | }); 45 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["crossorigin", "lucide", "plyr", "vercel", "vime"], 3 | "css.customData": [], 4 | "html.customData": ["./packages/vidstack/vscode.html-data.json"], 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | "typescript.preferences.autoImportFileExcludePatterns": ["src/index.ts", "exports/**"], 7 | "prettier.documentSelectors": ["**/*.astro", "**/*.svelte"], 8 | "[astro]": { 9 | "editor.defaultFormatter": "astro-build.astro-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rahim Alwer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/audio-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidstack/player/f35caca9409e649fa2cec49b42956ef184b7fcac/assets/audio-player.png -------------------------------------------------------------------------------- /assets/cypress-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidstack/player/f35caca9409e649fa2cec49b42956ef184b7fcac/assets/cypress-dark.png -------------------------------------------------------------------------------- /assets/cypress-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidstack/player/f35caca9409e649fa2cec49b42956ef184b7fcac/assets/cypress-light.png -------------------------------------------------------------------------------- /assets/media-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidstack/player/f35caca9409e649fa2cec49b42956ef184b7fcac/assets/media-icons.png -------------------------------------------------------------------------------- /assets/mux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidstack/player/f35caca9409e649fa2cec49b42956ef184b7fcac/assets/mux.png -------------------------------------------------------------------------------- /assets/vercel-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidstack/player/f35caca9409e649fa2cec49b42956ef184b7fcac/assets/vercel-dark.png -------------------------------------------------------------------------------- /assets/vercel-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidstack/player/f35caca9409e649fa2cec49b42956ef184b7fcac/assets/vercel-light.png -------------------------------------------------------------------------------- /assets/video-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidstack/player/f35caca9409e649fa2cec49b42956ef184b7fcac/assets/video-player.png -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | You can find all Vidstack examples [here](https://github.com/vidstack/examples). 2 | -------------------------------------------------------------------------------- /packages/react/.templates/sandbox/document.css: -------------------------------------------------------------------------------- 1 | /************************************************************************************************* 2 | * Document 3 | *************************************************************************************************/ 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | html, 10 | body { 11 | margin: 0; 12 | padding: 0; 13 | width: 100vw; 14 | height: 100vh; 15 | } 16 | 17 | body { 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | } 22 | 23 | main { 24 | width: 100%; 25 | max-width: 980px; 26 | margin-inline: auto; 27 | } 28 | -------------------------------------------------------------------------------- /packages/react/.templates/sandbox/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidstack/player/f35caca9409e649fa2cec49b42956ef184b7fcac/packages/react/.templates/sandbox/favicon-32x32.png -------------------------------------------------------------------------------- /packages/react/.templates/sandbox/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | <title>Vidstack React Sandbox</title> 4 | 5 | <meta charset="utf-8" /> 6 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 | 8 | <link rel="icon" type="image/png" sizes="32x32" href="/sandbox/favicon-32x32.png" /> 9 | 10 | <link rel="stylesheet" href="./document.css" /> 11 | <link rel="stylesheet" href="./player.css" /> 12 | 13 | <script src="./main.tsx" type="module"></script> 14 | </head> 15 | 16 | <body> 17 | <main> 18 | <div id="player"></div> 19 | </main> 20 | </body> 21 | </html> 22 | -------------------------------------------------------------------------------- /packages/react/.templates/sandbox/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ReactDOM from 'react-dom/client'; 4 | 5 | import { Player } from './player'; 6 | 7 | const root = document.getElementById('player')!; 8 | ReactDOM.createRoot(root).render( 9 | <React.StrictMode> 10 | <Player /> 11 | </React.StrictMode>, 12 | ); 13 | -------------------------------------------------------------------------------- /packages/react/.templates/sandbox/player.css: -------------------------------------------------------------------------------- 1 | @import '../../vidstack/styles/player/base.css'; 2 | @import '../../vidstack/styles/player/default/theme.css'; 3 | @import '../../vidstack/styles/player/default/layouts/audio.css'; 4 | @import '../../vidstack/styles/player/default/layouts/video.css'; 5 | @import '../../vidstack/styles/player/plyr/theme.css'; 6 | 7 | .player { 8 | --brand-color: #f5f5f5; 9 | --focus-color: #4e9cf6; 10 | 11 | --audio-brand: var(--brand-color); 12 | --audio-focus-ring-color: var(--focus-color); 13 | --audio-border-radius: 2px; 14 | 15 | --video-brand: var(--brand-color); 16 | --video-focus-ring-color: var(--focus-color); 17 | --video-border-radius: 2px; 18 | 19 | /* 👉 https://vidstack.io/docs/player/components/layouts/default#css-variables for more. */ 20 | 21 | width: 100%; 22 | } 23 | 24 | .player[data-view-type='audio'] .vds-poster { 25 | display: none; 26 | } 27 | 28 | .player[data-view-type='video'] { 29 | aspect-ratio: 16 /9; 30 | } 31 | 32 | .src-buttons { 33 | display: flex; 34 | align-items: center; 35 | justify-content: space-evenly; 36 | margin-top: 40px; 37 | margin-inline: auto; 38 | max-width: 300px; 39 | } 40 | -------------------------------------------------------------------------------- /packages/react/.templates/sandbox/tracks.ts: -------------------------------------------------------------------------------- 1 | export const textTracks = [ 2 | // Subtitles 3 | { 4 | src: 'https://files.vidstack.io/sprite-fight/subs/english.vtt', 5 | label: 'English', 6 | language: 'en-US', 7 | kind: 'subtitles', 8 | default: true, 9 | }, 10 | { 11 | src: 'https://files.vidstack.io/sprite-fight/subs/spanish.vtt', 12 | label: 'Spanish', 13 | language: 'es-ES', 14 | kind: 'subtitles', 15 | }, 16 | // Chapters 17 | { 18 | src: 'https://files.vidstack.io/sprite-fight/chapters.vtt', 19 | kind: 'chapters', 20 | language: 'en-US', 21 | default: true, 22 | }, 23 | ] as const; 24 | -------------------------------------------------------------------------------- /packages/react/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rahim Alwer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # Vidstack React 2 | 3 | [![package-badge]][package] 4 | [![discord-badge]][discord] 5 | 6 | Vidstack is a video/audio platform for frontend developers to build high-quality and accessible 7 | experiences on the web. 8 | 9 | ## 📖 Docs 10 | 11 | You can find our documentation at [vidstack.io](https://www.vidstack.io). 12 | 13 | ## 📝 License 14 | 15 | Vidstack React is [MIT licensed](./LICENSE). 16 | 17 | [vime]: https://github.com/vime-js/vime 18 | [plyr]: https://github.com/sampotts/plyr 19 | [package]: https://www.npmjs.com/package/@vidstack/react 20 | [package-badge]: https://img.shields.io/npm/v/@vidstack/react/next?style=flat-square 21 | [discord]: https://discord.gg/QAjfh2gZE4 22 | [discord-badge]: https://img.shields.io/discord/742612686679965696?color=%235865F2&label=%20&logo=discord&logoColor=white&style=flat-square 23 | [discussions]: https://github.com/vidstack/player/discussions 24 | -------------------------------------------------------------------------------- /packages/react/analyze.config.ts: -------------------------------------------------------------------------------- 1 | import { createJSONPlugin } from '@maverick-js/cli/analyze'; 2 | 3 | export default [ 4 | createJSONPlugin({ 5 | outFile: 'dist-npm/analyze.json', 6 | }), 7 | ]; 8 | -------------------------------------------------------------------------------- /packages/react/npm/analyze.json.d.ts: -------------------------------------------------------------------------------- 1 | import type { ReactComponentMeta } from '@maverick-js/cli/analyze'; 2 | 3 | declare const json: { 4 | react: ReactComponentMeta[]; 5 | }; 6 | 7 | export default json; 8 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/default/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import type { ReadSignal, WriteSignal } from 'maverick.js'; 4 | 5 | import type { DefaultLayoutProps } from './media-layout'; 6 | 7 | export const DefaultLayoutContext = React.createContext<DefaultLayoutContext>({} as any); 8 | DefaultLayoutContext.displayName = 'DefaultLayoutContext'; 9 | 10 | interface DefaultLayoutContext extends DefaultLayoutProps { 11 | layoutEl: ReadSignal<HTMLElement | null>; 12 | isSmallLayout: boolean; 13 | userPrefersAnnouncements: WriteSignal<boolean>; 14 | userPrefersKeyboardAnimations: WriteSignal<boolean>; 15 | } 16 | 17 | export function useDefaultLayoutContext() { 18 | return React.useContext(DefaultLayoutContext); 19 | } 20 | 21 | export function useDefaultLayoutWord(word: string) { 22 | const { translations } = useDefaultLayoutContext(); 23 | return i18n(translations, word); 24 | } 25 | 26 | export function i18n(translations: any, word: string) { 27 | return translations?.[word] ?? word; 28 | } 29 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/default/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useColorSchemePreference } from '../../../hooks/use-dom'; 2 | import type { DefaultLayoutProps } from './media-layout'; 3 | 4 | export function useColorSchemeClass(colorScheme: DefaultLayoutProps['colorScheme']) { 5 | const systemColorPreference = useColorSchemePreference(); 6 | if (colorScheme === 'default') { 7 | return null; 8 | } else if (colorScheme === 'system') { 9 | return systemColorPreference; 10 | } else { 11 | return colorScheme; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/default/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | DefaultLayoutSlots, 3 | DefaultAudioLayoutSlots, 4 | DefaultVideoLayoutSlots, 5 | DefaultLayoutSlotName, 6 | DefaultLayoutMenuSlotName, 7 | } from './slots'; 8 | export type { DefaultLayoutTranslations, DefaultLayoutWord } from 'vidstack'; 9 | export type { DefaultLayoutProps } from './media-layout'; 10 | export * from './context'; 11 | export * from './ui'; 12 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/default/ui.ts: -------------------------------------------------------------------------------- 1 | export * from './audio-layout'; 2 | export * from './video-layout'; 3 | export * from './ui/menus/items/menu-checkbox'; 4 | export * from './ui/menus/items/menu-items'; 5 | export * from './ui/menus/items/menu-slider'; 6 | export * from './ui/tooltip'; 7 | export * from './icons'; 8 | export { DefaultKeyboardDisplay, type DefaultKeyboardDisplayProps } from './ui/keyboard-display'; 9 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/default/ui/announcer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { useSignal } from 'maverick.js/react'; 4 | 5 | import { MediaAnnouncer } from '../../../announcer'; 6 | import { useDefaultLayoutContext } from '../context'; 7 | 8 | /* ------------------------------------------------------------------------------------------------- 9 | * DefaultAnnouncer 10 | * -----------------------------------------------------------------------------------------------*/ 11 | 12 | function DefaultAnnouncer() { 13 | const { userPrefersAnnouncements, translations } = useDefaultLayoutContext(), 14 | $userPrefersAnnouncements = useSignal(userPrefersAnnouncements); 15 | 16 | if (!$userPrefersAnnouncements) return null; 17 | 18 | return <MediaAnnouncer translations={translations} />; 19 | } 20 | 21 | DefaultAnnouncer.displayName = 'DefaultAnnouncer'; 22 | export { DefaultAnnouncer }; 23 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/default/ui/captions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Captions } from '../../../ui/captions'; 4 | import { useDefaultLayoutWord } from '../context'; 5 | 6 | /* ------------------------------------------------------------------------------------------------- 7 | * DefaultCaptions 8 | * -----------------------------------------------------------------------------------------------*/ 9 | 10 | function DefaultCaptions() { 11 | const exampleText = useDefaultLayoutWord('Captions look like this'); 12 | return <Captions className="vds-captions" exampleText={exampleText} />; 13 | } 14 | 15 | DefaultCaptions.displayName = 'DefaultCaptions'; 16 | export { DefaultCaptions }; 17 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/default/ui/controls.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | /* ------------------------------------------------------------------------------------------------- 4 | * DefaultControlsSpacer 5 | * -----------------------------------------------------------------------------------------------*/ 6 | 7 | function DefaultControlsSpacer() { 8 | return <div className="vds-controls-spacer" />; 9 | } 10 | 11 | DefaultControlsSpacer.displayName = 'DefaultControlsSpacer'; 12 | export { DefaultControlsSpacer }; 13 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/default/ui/menus/utils.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { useSignal } from 'maverick.js/react'; 4 | 5 | import { useDefaultLayoutContext } from '../../context'; 6 | 7 | export function useParentDialogEl() { 8 | const { layoutEl } = useDefaultLayoutContext(), 9 | $layoutEl = useSignal(layoutEl); 10 | 11 | return React.useMemo(() => $layoutEl?.closest('dialog'), [$layoutEl]); 12 | } 13 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/default/ui/title.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { useActiveTextTrack } from '../../../../hooks/use-active-text-track'; 4 | import { useMediaState } from '../../../../hooks/use-media-state'; 5 | import { ChapterTitle } from '../../../ui/chapter-title'; 6 | import { Title } from '../../../ui/title'; 7 | 8 | /* ------------------------------------------------------------------------------------------------- 9 | * DefaultTitle 10 | * -----------------------------------------------------------------------------------------------*/ 11 | 12 | function DefaultTitle() { 13 | const $started = useMediaState('started'), 14 | $title = useMediaState('title'), 15 | $hasChapters = useActiveTextTrack('chapters'); 16 | return $hasChapters && ($started || !$title) ? ( 17 | <ChapterTitle className="vds-chapter-title" /> 18 | ) : ( 19 | <Title className="vds-chapter-title" /> 20 | ); 21 | } 22 | 23 | DefaultTitle.displayName = 'DefaultTitle'; 24 | export { DefaultTitle }; 25 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/default/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import type { TooltipPlacement } from 'vidstack'; 4 | 5 | import * as Tooltip from '../../../ui/tooltip'; 6 | import { useDefaultLayoutContext } from '../context'; 7 | 8 | export interface DefaultTooltipProps { 9 | content: string; 10 | placement?: TooltipPlacement; 11 | children: React.ReactNode; 12 | } 13 | 14 | function DefaultTooltip({ content, placement, children }: DefaultTooltipProps) { 15 | const { showTooltipDelay } = useDefaultLayoutContext(); 16 | return ( 17 | <Tooltip.Root showDelay={showTooltipDelay}> 18 | <Tooltip.Trigger asChild>{children}</Tooltip.Trigger> 19 | <Tooltip.Content className="vds-tooltip-content" placement={placement}> 20 | {content} 21 | </Tooltip.Content> 22 | </Tooltip.Root> 23 | ); 24 | } 25 | 26 | DefaultTooltip.displayName = 'DefaultTooltip'; 27 | export { DefaultTooltip }; 28 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { type WriteSignal } from 'maverick.js'; 4 | import type { PlyrLayoutWord } from 'vidstack'; 5 | 6 | import type { PlyrLayoutProps } from './props'; 7 | 8 | interface PlyrLayoutContext extends PlyrLayoutProps { 9 | previewTime: WriteSignal<number>; 10 | } 11 | 12 | export const PlyrLayoutContext = React.createContext<PlyrLayoutContext>({} as any); 13 | PlyrLayoutContext.displayName = 'PlyrLayoutContext'; 14 | 15 | export function usePlyrLayoutContext() { 16 | return React.useContext(PlyrLayoutContext); 17 | } 18 | 19 | export function usePlyrLayoutWord(word: PlyrLayoutWord) { 20 | const { translations } = usePlyrLayoutContext(); 21 | return i18n(translations, word); 22 | } 23 | 24 | export function i18n(translations: any, word: string) { 25 | return translations?.[word] ?? word; 26 | } 27 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-airplay.js: -------------------------------------------------------------------------------- 1 | export default `<g><path d="M16,1 L2,1 C1.447,1 1,1.447 1,2 L1,12 C1,12.553 1.447,13 2,13 L5,13 L5,11 L3,11 L3,3 L15,3 L15,11 L13,11 L13,13 L16,13 C16.553,13 17,12.553 17,12 L17,2 C17,1.447 16.553,1 16,1 L16,1 Z"></path><polygon points="4 17 14 17 9 11"></polygon></g>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-captions-off.js: -------------------------------------------------------------------------------- 1 | export default `<g fill-rule="evenodd" fill-opacity="0.5"><path d="M1,1 C0.4,1 0,1.4 0,2 L0,13 C0,13.6 0.4,14 1,14 L5.6,14 L8.3,16.7 C8.5,16.9 8.7,17 9,17 C9.3,17 9.5,16.9 9.7,16.7 L12.4,14 L17,14 C17.6,14 18,13.6 18,13 L18,2 C18,1.4 17.6,1 17,1 L1,1 Z M5.52,11.15 C7.51,11.15 8.53,9.83 8.8,8.74 L7.51,8.35 C7.32,9.01 6.73,9.8 5.52,9.8 C4.38,9.8 3.32,8.97 3.32,7.46 C3.32,5.85 4.44,5.09 5.5,5.09 C6.73,5.09 7.28,5.84 7.45,6.52 L8.75,6.11 C8.47,4.96 7.46,3.76 5.5,3.76 C3.6,3.76 1.89,5.2 1.89,7.46 C1.89,9.72 3.54,11.15 5.52,11.15 Z M13.09,11.15 C15.08,11.15 16.1,9.83 16.37,8.74 L15.08,8.35 C14.89,9.01 14.3,9.8 13.09,9.8 C11.95,9.8 10.89,8.97 10.89,7.46 C10.89,5.85 12.01,5.09 13.07,5.09 C14.3,5.09 14.85,5.84 15.02,6.52 L16.32,6.11 C16.04,4.96 15.03,3.76 13.07,3.76 C11.17,3.76 9.46,5.2 9.46,7.46 C9.46,9.72 11.11,11.15 13.09,11.15 Z"></path></g>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-captions-on.js: -------------------------------------------------------------------------------- 1 | export default `<g fill-rule="evenodd"><path d="M1,1 C0.4,1 0,1.4 0,2 L0,13 C0,13.6 0.4,14 1,14 L5.6,14 L8.3,16.7 C8.5,16.9 8.7,17 9,17 C9.3,17 9.5,16.9 9.7,16.7 L12.4,14 L17,14 C17.6,14 18,13.6 18,13 L18,2 C18,1.4 17.6,1 17,1 L1,1 Z M5.52,11.15 C7.51,11.15 8.53,9.83 8.8,8.74 L7.51,8.35 C7.32,9.01 6.73,9.8 5.52,9.8 C4.38,9.8 3.32,8.97 3.32,7.46 C3.32,5.85 4.44,5.09 5.5,5.09 C6.73,5.09 7.28,5.84 7.45,6.52 L8.75,6.11 C8.47,4.96 7.46,3.76 5.5,3.76 C3.6,3.76 1.89,5.2 1.89,7.46 C1.89,9.72 3.54,11.15 5.52,11.15 Z M13.09,11.15 C15.08,11.15 16.1,9.83 16.37,8.74 L15.08,8.35 C14.89,9.01 14.3,9.8 13.09,9.8 C11.95,9.8 10.89,8.97 10.89,7.46 C10.89,5.85 12.01,5.09 13.07,5.09 C14.3,5.09 14.85,5.84 15.02,6.52 L16.32,6.11 C16.04,4.96 15.03,3.76 13.07,3.76 C11.17,3.76 9.46,5.2 9.46,7.46 C9.46,9.72 11.11,11.15 13.09,11.15 Z"></path></g>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-download.js: -------------------------------------------------------------------------------- 1 | export default `<g transform="translate(2 1)"><path d="M7,12 C7.3,12 7.5,11.9 7.7,11.7 L13.4,6 L12,4.6 L8,8.6 L8,0 L6,0 L6,8.6 L2,4.6 L0.6,6 L6.3,11.7 C6.5,11.9 6.7,12 7,12 Z" /><rect width="14" height="2" y="14" /></g>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-enter-fullscreen.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="10 3 13.6 3 9.6 7 11 8.4 15 4.4 15 8 17 8 17 1 10 1"></polygon><polygon points="7 9.6 3 13.6 3 10 1 10 1 17 8 17 8 15 4.4 15 8.4 11"></polygon>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-exit-fullscreen.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="1 12 4.6 12 0.6 16 2 17.4 6 13.4 6 17 8 17 8 10 1 10"></polygon><polygon points="16 0.6 12 4.6 12 1 10 1 10 8 17 8 17 6 13.4 6 17.4 2"></polygon>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-fast-forward.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="7.875 7.17142857 0 1 0 17 7.875 10.8285714 7.875 17 18 9 7.875 1"></polygon>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-muted.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="12.4 12.5 14.5 10.4 16.6 12.5 18 11.1 15.9 9 18 6.9 16.6 5.5 14.5 7.6 12.4 5.5 11 6.9 13.1 9 11 11.1"></polygon><path d="M3.78571429,6.00820648 L0.714285714,6.00820648 C0.285714286,6.00820648 0,6.30901277 0,6.76022222 L0,11.2723167 C0,11.7235261 0.285714286,12.0243324 0.714285714,12.0243324 L3.78571429,12.0243324 L7.85714286,15.8819922 C8.35714286,16.1827985 9,15.8819922 9,15.2803796 L9,2.75215925 C9,2.15054666 8.35714286,1.77453879 7.85714286,2.15054666 L3.78571429,6.00820648 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-pause.js: -------------------------------------------------------------------------------- 1 | export default `<path d="M6,1 L3,1 C2.4,1 2,1.4 2,2 L2,16 C2,16.6 2.4,17 3,17 L6,17 C6.6,17 7,16.6 7,16 L7,2 C7,1.4 6.6,1 6,1 L6,1 Z"></path><path d="M12,1 C11.4,1 11,1.4 11,2 L11,16 C11,16.6 11.4,17 12,17 L15,17 C15.6,17 16,16.6 16,16 L16,2 C16,1.4 15.6,1 15,1 L12,1 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-pip.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="13.293 3.293 7.022 9.564 8.436 10.978 14.707 4.707 17 7 17 1 11 1"></polygon><path d="M13,15 L3,15 L3,5 L8,5 L8,3 L2,3 C1.448,3 1,3.448 1,4 L1,16 C1,16.552 1.448,17 2,17 L14,17 C14.552,17 15,16.552 15,16 L15,10 L13,10 L13,15 L13,15 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-play.js: -------------------------------------------------------------------------------- 1 | export default `<path d="M15.5615866,8.10002147 L3.87056367,0.225209313 C3.05219207,-0.33727727 2,0.225209313 2,1.12518784 L2,16.8748122 C2,17.7747907 3.05219207,18.3372773 3.87056367,17.7747907 L15.5615866,9.89997853 C16.1461378,9.44998927 16.1461378,8.55001073 15.5615866,8.10002147 L15.5615866,8.10002147 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-restart.js: -------------------------------------------------------------------------------- 1 | export default `<path d="M9.7,1.2 L10.4,7.6 L12.5,5.5 C14.4,7.4 14.4,10.6 12.5,12.5 C11.6,13.5 10.3,14 9,14 C7.7,14 6.4,13.5 5.5,12.5 C3.6,10.6 3.6,7.4 5.5,5.5 C6.1,4.9 6.9,4.4 7.8,4.2 L7.2,2.3 C6,2.6 4.9,3.2 4,4.1 C1.3,6.8 1.3,11.2 4,14 C5.3,15.3 7.1,16 8.9,16 C10.8,16 12.5,15.3 13.8,14 C16.5,11.3 16.5,6.9 13.8,4.1 L16,1.9 L9.7,1.2 L9.7,1.2 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-rewind.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="10.125 1 0 9 10.125 17 10.125 10.8285714 18 17 18 1 10.125 7.17142857"></polygon>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-settings.js: -------------------------------------------------------------------------------- 1 | export default `<path d="M16.135,7.784 C14.832,7.458 14.214,5.966 14.905,4.815 C15.227,4.279 15.13,3.817 14.811,3.499 L14.501,3.189 C14.183,2.871 13.721,2.774 13.185,3.095 C12.033,3.786 10.541,3.168 10.216,1.865 C10.065,1.258 9.669,1 9.219,1 L8.781,1 C8.331,1 7.936,1.258 7.784,1.865 C7.458,3.168 5.966,3.786 4.815,3.095 C4.279,2.773 3.816,2.87 3.498,3.188 L3.188,3.498 C2.87,3.816 2.773,4.279 3.095,4.815 C3.786,5.967 3.168,7.459 1.865,7.784 C1.26,7.935 1,8.33 1,8.781 L1,9.219 C1,9.669 1.258,10.064 1.865,10.216 C3.168,10.542 3.786,12.034 3.095,13.185 C2.773,13.721 2.87,14.183 3.189,14.501 L3.499,14.811 C3.818,15.13 4.281,15.226 4.815,14.905 C5.967,14.214 7.459,14.832 7.784,16.135 C7.935,16.742 8.331,17 8.781,17 L9.219,17 C9.669,17 10.064,16.742 10.216,16.135 C10.542,14.832 12.034,14.214 13.185,14.905 C13.72,15.226 14.182,15.13 14.501,14.811 L14.811,14.501 C15.129,14.183 15.226,13.72 14.905,13.185 C14.214,12.033 14.832,10.541 16.135,10.216 C16.742,10.065 17,9.669 17,9.219 L17,8.781 C17,8.33 16.74,7.935 16.135,7.784 L16.135,7.784 Z M9,12 C7.343,12 6,10.657 6,9 C6,7.343 7.343,6 9,6 C10.657,6 12,7.343 12,9 C12,10.657 10.657,12 9,12 L9,12 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/icons/plyr-volume.js: -------------------------------------------------------------------------------- 1 | export default `<path d="M15.5999996,3.3 C15.1999996,2.9 14.5999996,2.9 14.1999996,3.3 C13.7999996,3.7 13.7999996,4.3 14.1999996,4.7 C15.3999996,5.9 15.9999996,7.4 15.9999996,9 C15.9999996,10.6 15.3999996,12.1 14.1999996,13.3 C13.7999996,13.7 13.7999996,14.3 14.1999996,14.7 C14.3999996,14.9 14.6999996,15 14.8999996,15 C15.1999996,15 15.3999996,14.9 15.5999996,14.7 C17.0999996,13.2 17.9999996,11.2 17.9999996,9 C17.9999996,6.8 17.0999996,4.8 15.5999996,3.3 L15.5999996,3.3 Z"></path><path d="M11.2819745,5.28197449 C10.9060085,5.65794047 10.9060085,6.22188944 11.2819745,6.59785542 C12.0171538,7.33303477 12.2772954,8.05605449 12.2772954,9.00000021 C12.2772954,9.93588462 11.851678,10.9172014 11.2819745,11.4869049 C10.9060085,11.8628709 10.9060085,12.4268199 11.2819745,12.8027859 C11.4271642,12.9479755 11.9176724,13.0649528 12.2998149,12.9592565 C12.4124479,12.9281035 12.5156669,12.8776063 12.5978555,12.8027859 C13.773371,11.732654 14.1311161,10.1597914 14.1312523,9.00000021 C14.1312723,8.8299555 14.1286311,8.66015647 14.119665,8.4897429 C14.0674781,7.49784946 13.8010171,6.48513613 12.5978554,5.28197449 C12.2218894,4.9060085 11.6579405,4.9060085 11.2819745,5.28197449 Z"></path><path d="M3.78571429,6.00820648 L0.714285714,6.00820648 C0.285714286,6.00820648 0,6.30901277 0,6.76022222 L0,11.2723167 C0,11.7235261 0.285714286,12.0243324 0.714285714,12.0243324 L3.78571429,12.0243324 L7.85714286,15.8819922 C8.35714286,16.1827985 9,15.8819922 9,15.2803796 L9,2.75215925 C9,2.15054666 8.35714286,1.77453879 7.85714286,2.15054666 L3.78571429,6.00820648 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | type PlyrLayoutElementProps, 3 | PlyrLayout, 4 | PlyrAudioLayout, 5 | PlyrVideoLayout, 6 | } from './layout'; 7 | export type { PlyrLayoutProps } from './props'; 8 | export type { PlyrLayoutSlots, PlyrLayoutSlotName } from './slots'; 9 | export type { PlyrControl, PlyrLayoutTranslations, PlyrLayoutWord, PlyrMarker } from 'vidstack'; 10 | export * from './icons'; 11 | export * from './context'; 12 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/plyr/slots.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { isUndefined, uppercaseFirstChar } from 'maverick.js/std'; 4 | 5 | import { usePlyrLayoutContext } from './context'; 6 | 7 | export type SlotPositions<Name extends string> = 8 | | `before${Capitalize<Name>}` 9 | | Name 10 | | `after${Capitalize<Name>}`; 11 | 12 | export type Slots<Names extends string> = { 13 | [slotName in SlotPositions<Names>]?: React.ReactNode; 14 | }; 15 | 16 | export type PlyrLayoutSlotName = 17 | | 'airPlayButton' 18 | | 'captionsButton' 19 | | 'currentTime' 20 | | 'download' 21 | | 'duration' 22 | | 'fastForwardButton' 23 | | 'fullscreenButton' 24 | | 'liveButton' 25 | | 'muteButton' 26 | | 'pipButton' 27 | | 'playButton' 28 | | 'playLargeButton' 29 | | 'poster' 30 | | 'restartButton' 31 | | 'rewindButton' 32 | | 'rewindButton' 33 | | 'settings' 34 | | 'settingsButton' 35 | | 'timeSlider' 36 | | 'volumeSlider' 37 | | 'settingsMenu'; 38 | 39 | export interface PlyrLayoutSlots extends Slots<PlyrLayoutSlotName> {} 40 | 41 | export function slot(name: PlyrLayoutSlotName, defaultValue: React.ReactNode): React.ReactNode { 42 | const { slots } = usePlyrLayoutContext(), 43 | slot = slots?.[name], 44 | capitalizedName = uppercaseFirstChar(name as string); 45 | return ( 46 | <> 47 | {slots?.[`before${capitalizedName}`]} 48 | {isUndefined(slot) ? defaultValue : slot} 49 | {slots?.[`after${capitalizedName}`]} 50 | </> 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/remotion-ui.ts: -------------------------------------------------------------------------------- 1 | import { signal } from 'maverick.js'; 2 | 3 | export const RemotionThumbnail = /* @__PURE__ */ signal<React.LazyExoticComponent< 4 | React.ComponentType<any> 5 | > | null>(null); 6 | 7 | export const RemotionSliderThumbnail = /* @__PURE__ */ signal<React.LazyExoticComponent< 8 | React.ComponentType<any> 9 | > | null>(null); 10 | 11 | export const RemotionPoster = /* @__PURE__ */ signal<React.LazyExoticComponent< 12 | React.ComponentType<any> 13 | > | null>(null); 14 | -------------------------------------------------------------------------------- /packages/react/src/components/layouts/utils.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { effect } from 'maverick.js'; 4 | 5 | import { useMediaPlayer } from '../../hooks/use-media-player'; 6 | 7 | export function useLayoutName(name: string) { 8 | const player = useMediaPlayer(); 9 | React.useEffect(() => { 10 | if (!player) return; 11 | return effect(() => { 12 | const el = player.$el; 13 | el?.setAttribute('data-layout', name); 14 | return () => el?.removeAttribute('data-layout'); 15 | }); 16 | }, [player]); 17 | } 18 | -------------------------------------------------------------------------------- /packages/react/src/components/ui/captions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { createReactComponent, type ReactElementProps } from 'maverick.js/react'; 4 | 5 | import { CaptionsInstance } from '../primitives/instances'; 6 | import { Primitive } from '../primitives/nodes'; 7 | 8 | /* ------------------------------------------------------------------------------------------------- 9 | * Captions 10 | * -----------------------------------------------------------------------------------------------*/ 11 | 12 | const CaptionsBridge = createReactComponent(CaptionsInstance); 13 | 14 | export interface CaptionsProps extends ReactElementProps<CaptionsInstance> { 15 | asChild?: boolean; 16 | children?: React.ReactNode; 17 | ref?: React.Ref<CaptionsInstance>; 18 | } 19 | 20 | /** 21 | * Renders and displays captions/subtitles. This will be an overlay for video and a simple 22 | * captions box for audio. 23 | * 24 | * @docs {@link https://www.vidstack.io/docs/player/components/display/captions} 25 | * @example 26 | * ```tsx 27 | * <Captions /> 28 | * ``` 29 | */ 30 | const Captions = React.forwardRef<CaptionsInstance, CaptionsProps>( 31 | ({ children, ...props }, forwardRef) => { 32 | return ( 33 | <CaptionsBridge {...props} ref={forwardRef}> 34 | {(props) => <Primitive.div {...props}>{children}</Primitive.div>} 35 | </CaptionsBridge> 36 | ); 37 | }, 38 | ); 39 | 40 | Captions.displayName = 'Captions'; 41 | export { Captions }; 42 | -------------------------------------------------------------------------------- /packages/react/src/components/ui/chapter-title.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { useChapterTitle } from '../../hooks/use-chapter-title'; 4 | import { Primitive, type PrimitivePropsWithRef } from '../primitives/nodes'; 5 | 6 | /* ------------------------------------------------------------------------------------------------- 7 | * Chapter Title 8 | * -----------------------------------------------------------------------------------------------*/ 9 | 10 | export interface ChapterTitleProps extends PrimitivePropsWithRef<'span'> { 11 | /** 12 | * Specify text to be displayed when no chapter title is available. 13 | */ 14 | defaultText?: string; 15 | } 16 | 17 | /** 18 | * This component is used to load and display the current chapter title based on the text tracks 19 | * provided. 20 | * 21 | * @docs {@link https://www.vidstack.io/docs/player/components/display/chapter-title} 22 | * @example 23 | * ```tsx 24 | * <ChapterTitle /> 25 | * ``` 26 | */ 27 | const ChapterTitle = React.forwardRef<HTMLElement, ChapterTitleProps>( 28 | ({ defaultText = '', children, ...props }, forwardRef) => { 29 | const $chapterTitle = useChapterTitle(); 30 | return ( 31 | <Primitive.span {...props} ref={forwardRef as React.Ref<any>}> 32 | {$chapterTitle || defaultText} 33 | {children} 34 | </Primitive.span> 35 | ); 36 | }, 37 | ); 38 | 39 | ChapterTitle.displayName = 'ChapterTitle'; 40 | export { ChapterTitle }; 41 | -------------------------------------------------------------------------------- /packages/react/src/components/ui/gesture.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { createReactComponent, type ReactElementProps } from 'maverick.js/react'; 4 | 5 | import { GestureInstance } from '../primitives/instances'; 6 | import { Primitive } from '../primitives/nodes'; 7 | 8 | /* ------------------------------------------------------------------------------------------------- 9 | * Gesture 10 | * -----------------------------------------------------------------------------------------------*/ 11 | 12 | const GestureBridge = createReactComponent(GestureInstance, { 13 | events: ['onWillTrigger', 'onTrigger'], 14 | }); 15 | 16 | export interface GestureProps extends ReactElementProps<GestureInstance> { 17 | asChild?: boolean; 18 | children?: React.ReactNode; 19 | ref?: React.Ref<GestureInstance>; 20 | } 21 | 22 | /** 23 | * This component enables actions to be performed on the media based on user gestures. 24 | * 25 | * @docs {@link https://www.vidstack.io/docs/player/components/media/gesture} 26 | * @example 27 | * ```tsx 28 | * <Gesture event="pointerup" action="toggle:paused" /> 29 | * <Gesture event="dblpointerup" action="toggle:fullscreen" /> 30 | * ``` 31 | */ 32 | const Gesture = React.forwardRef<GestureInstance, GestureProps>( 33 | ({ children, ...props }, forwardRef) => { 34 | return ( 35 | <GestureBridge {...props} ref={forwardRef}> 36 | {(props) => <Primitive.div {...props}>{children}</Primitive.div>} 37 | </GestureBridge> 38 | ); 39 | }, 40 | ); 41 | 42 | Gesture.displayName = 'Gesture'; 43 | export { Gesture }; 44 | -------------------------------------------------------------------------------- /packages/react/src/components/ui/sliders/slider-callbacks.ts: -------------------------------------------------------------------------------- 1 | import type { InferComponentEvents } from 'maverick.js'; 2 | import type { ReactEventCallbacks } from 'maverick.js/react'; 3 | 4 | import type { SliderInstance } from '../../primitives/instances'; 5 | 6 | type SliderCallbacks = keyof ReactEventCallbacks<InferComponentEvents<SliderInstance>>; 7 | 8 | export const sliderCallbacks: SliderCallbacks[] = [ 9 | 'onDragStart', 10 | 'onDragEnd', 11 | 'onDragValueChange', 12 | 'onValueChange', 13 | 'onPointerValueChange', 14 | ]; 15 | -------------------------------------------------------------------------------- /packages/react/src/components/ui/sliders/slider-value.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { createReactComponent, type ReactElementProps } from 'maverick.js/react'; 4 | 5 | import { SliderValueInstance } from '../../primitives/instances'; 6 | 7 | export const SliderValueBridge = createReactComponent(SliderValueInstance); 8 | 9 | export interface SliderValueProps extends ReactElementProps<SliderValueInstance> { 10 | asChild?: boolean; 11 | children?: React.ReactNode; 12 | ref?: React.Ref<HTMLElement>; 13 | } 14 | -------------------------------------------------------------------------------- /packages/react/src/components/ui/title.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { useMediaState } from '../../hooks/use-media-state'; 4 | import { Primitive, type PrimitivePropsWithRef } from '../primitives/nodes'; 5 | 6 | /* ------------------------------------------------------------------------------------------------- 7 | * Title 8 | * -----------------------------------------------------------------------------------------------*/ 9 | 10 | export interface TitleProps extends PrimitivePropsWithRef<'span'> {} 11 | 12 | /** 13 | * This component is used to load and display the current media title. 14 | * 15 | * @docs {@link https://www.vidstack.io/docs/player/components/display/title} 16 | * @example 17 | * ```tsx 18 | * <Title /> 19 | * ``` 20 | */ 21 | const Title = React.forwardRef<HTMLElement, TitleProps>(({ children, ...props }, forwardRef) => { 22 | const $title = useMediaState('title'); 23 | return ( 24 | <Primitive.span {...props} ref={forwardRef as React.Ref<any>}> 25 | {$title} 26 | {children} 27 | </Primitive.span> 28 | ); 29 | }); 30 | 31 | Title.displayName = 'Title'; 32 | export { Title }; 33 | -------------------------------------------------------------------------------- /packages/react/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference path="../../vidstack/src/globals.d.ts" /> 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /packages/react/src/hooks/create-text-track.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { TextTrack, type TextTrackInit } from 'vidstack'; 4 | 5 | import { useMediaContext } from './use-media-context'; 6 | 7 | /** 8 | * Creates a new `TextTrack` object and adds it to the player. 9 | * 10 | * @docs {@link https://www.vidstack.io/docs/player/api/hooks/create-text-track} 11 | */ 12 | export function createTextTrack(init: TextTrackInit) { 13 | const media = useMediaContext(), 14 | track = React.useMemo(() => new TextTrack(init), Object.values(init)); 15 | 16 | React.useEffect(() => { 17 | media.textTracks.add(track); 18 | return () => void media.textTracks.remove(track); 19 | }, [track]); 20 | 21 | return track; 22 | } 23 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-active-text-cues.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { listenEvent } from 'maverick.js/std'; 4 | import type { VTTCue } from 'media-captions'; 5 | import type { TextTrack } from 'vidstack'; 6 | 7 | /** 8 | * @docs {@link https://www.vidstack.io/docs/player/api/hooks/use-active-text-cues} 9 | */ 10 | export function useActiveTextCues(track: TextTrack | null): VTTCue[] { 11 | const [activeCues, setActiveCues] = React.useState<VTTCue[]>([]); 12 | 13 | React.useEffect(() => { 14 | if (!track) { 15 | setActiveCues([]); 16 | return; 17 | } 18 | 19 | function onCuesChange() { 20 | if (track) setActiveCues(track.activeCues as VTTCue[]); 21 | } 22 | 23 | onCuesChange(); 24 | return listenEvent(track, 'cue-change', onCuesChange); 25 | }, [track]); 26 | 27 | return activeCues; 28 | } 29 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-active-text-track.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { watchActiveTextTrack, type TextTrack } from 'vidstack'; 4 | 5 | import { useMediaContext } from './use-media-context'; 6 | 7 | /** 8 | * @docs {@link https://www.vidstack.io/docs/player/api/hooks/use-active-text-track} 9 | */ 10 | export function useActiveTextTrack(kind: TextTrackKind | TextTrackKind[]): TextTrack | null { 11 | const media = useMediaContext(), 12 | [track, setTrack] = React.useState<TextTrack | null>(null); 13 | 14 | React.useEffect(() => { 15 | return watchActiveTextTrack(media.textTracks, kind, setTrack); 16 | }, [kind]); 17 | 18 | return track; 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-chapter-title.ts: -------------------------------------------------------------------------------- 1 | import { useActiveTextCues } from './use-active-text-cues'; 2 | import { useActiveTextTrack } from './use-active-text-track'; 3 | 4 | /** 5 | * @docs {@link https://www.vidstack.io/docs/player/api/hooks/use-chapter-title} 6 | */ 7 | export function useChapterTitle(): string { 8 | const $track = useActiveTextTrack('chapters'), 9 | $cues = useActiveTextCues($track); 10 | 11 | return $cues[0]?.text || ''; 12 | } 13 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-media-context.ts: -------------------------------------------------------------------------------- 1 | import { useReactContext } from 'maverick.js/react'; 2 | import { mediaContext } from 'vidstack'; 3 | 4 | export function useMediaContext() { 5 | return useReactContext(mediaContext)!; 6 | } 7 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-media-player.ts: -------------------------------------------------------------------------------- 1 | import type { MediaPlayerInstance } from '../components/primitives/instances'; 2 | import { useMediaContext } from './use-media-context'; 3 | 4 | /** 5 | * Returns the nearest parent player component. 6 | * 7 | * @docs {@link https://www.vidstack.io/docs/player/api/hooks/use-media-player} 8 | */ 9 | export function useMediaPlayer(): MediaPlayerInstance | null { 10 | const context = useMediaContext(); 11 | 12 | if (__DEV__ && !context) { 13 | throw Error( 14 | '[vidstack] no media context was found - was this called outside of `<MediaPlayer>`?', 15 | ); 16 | } 17 | 18 | return context?.player || null; 19 | } 20 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-media-provider.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { effect } from 'maverick.js'; 4 | import { type MediaProviderAdapter } from 'vidstack'; 5 | 6 | import { useMediaContext } from './use-media-context'; 7 | 8 | /** 9 | * Returns the current parent media provider. 10 | * 11 | * @docs {@link https://www.vidstack.io/docs/player/api/hooks/use-media-provider} 12 | */ 13 | export function useMediaProvider(): MediaProviderAdapter | null { 14 | const [provider, setProvider] = React.useState<MediaProviderAdapter | null>(null), 15 | context = useMediaContext(); 16 | 17 | if (__DEV__ && !context) { 18 | throw Error( 19 | '[vidstack] no media context was found - was this called outside of `<MediaPlayer>`?', 20 | ); 21 | } 22 | 23 | React.useEffect(() => { 24 | if (!context) return; 25 | return effect(() => { 26 | setProvider(context.$provider()); 27 | }); 28 | }, []); 29 | 30 | return provider; 31 | } 32 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-media-remote.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { MediaRemoteControl } from 'vidstack'; 4 | 5 | import { MediaPlayerInstance } from '../components/primitives/instances'; 6 | import { useMediaContext } from './use-media-context'; 7 | 8 | /** 9 | * A media remote provides a simple facade for dispatching media requests to the nearest media 10 | * player. 11 | * 12 | * @param target - The DOM event target to dispatch request events from. Defaults to player 13 | * if no target is provided. 14 | * 15 | * @docs {@link https://www.vidstack.io/docs/player/api/hooks/use-media-remote} 16 | */ 17 | export function useMediaRemote( 18 | target?: EventTarget | null | React.RefObject<EventTarget | null>, 19 | ): MediaRemoteControl { 20 | const media = useMediaContext(), 21 | remote = React.useRef<MediaRemoteControl>(null!); 22 | 23 | if (!remote.current) { 24 | remote.current = new MediaRemoteControl(); 25 | } 26 | 27 | React.useEffect(() => { 28 | const ref = target && 'current' in target ? target.current : target, 29 | isPlayerRef = ref instanceof MediaPlayerInstance, 30 | player = isPlayerRef ? ref : media?.player; 31 | 32 | remote.current!.setPlayer(player ?? null); 33 | remote.current!.setTarget(ref ?? null); 34 | }, [media, target && 'current' in target ? target.current : target]); 35 | 36 | return remote.current; 37 | } 38 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-signals.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { computed, effect, scoped, signal, type MaybeStopEffect } from 'maverick.js'; 4 | import { useReactScope } from 'maverick.js/react'; 5 | 6 | export function createSignal<T>(initialValue: T, deps: any[] = []) { 7 | const scope = useReactScope(); 8 | return React.useMemo(() => scoped(() => signal(initialValue), scope)!, [scope, ...deps]); 9 | } 10 | 11 | export function createComputed<T>(compute: () => T, deps: any[] = []) { 12 | const scope = useReactScope(); 13 | return React.useMemo(() => scoped(() => computed(compute), scope)!, [scope, ...deps]); 14 | } 15 | 16 | export function createEffect(compute: () => MaybeStopEffect, deps: any[] = []) { 17 | const scope = useReactScope(); 18 | React.useEffect(() => scoped(() => effect(compute), scope)!, [scope, ...deps]); 19 | } 20 | 21 | export function useScoped<T>(compute: () => T) { 22 | const scope = useReactScope(); 23 | return React.useMemo(() => scoped(compute, scope)!, [scope]); 24 | } 25 | -------------------------------------------------------------------------------- /packages/react/src/hooks/use-text-cues.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { EventsController } from 'maverick.js/std'; 4 | import type { VTTCue } from 'media-captions'; 5 | import type { TextTrack } from 'vidstack'; 6 | 7 | /** 8 | * @docs {@link https://www.vidstack.io/docs/player/api/hooks/use-text-cues} 9 | */ 10 | export function useTextCues(track: TextTrack | null): VTTCue[] { 11 | const [cues, setCues] = React.useState<VTTCue[]>([]); 12 | 13 | React.useEffect(() => { 14 | if (!track) return; 15 | 16 | function onCuesChange() { 17 | if (track) setCues([...track.cues]); 18 | } 19 | 20 | const events = new EventsController(track) 21 | .add('add-cue', onCuesChange) 22 | .add('remove-cue', onCuesChange); 23 | 24 | onCuesChange(); 25 | 26 | return () => { 27 | setCues([]); 28 | events.abort(); 29 | }; 30 | }, [track]); 31 | 32 | return cues; 33 | } 34 | -------------------------------------------------------------------------------- /packages/react/src/icon.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface IconProps 4 | extends React.PropsWithoutRef<React.SVGProps<SVGSVGElement>>, 5 | React.RefAttributes<SVGElement | SVGSVGElement> { 6 | /** 7 | * The horizontal (width) and vertical (height) length of the underlying `<svg>` element. 8 | * 9 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/width} 10 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/height} 11 | */ 12 | size?: number; 13 | part?: string; 14 | /** @internal */ 15 | paths?: string; 16 | } 17 | 18 | export interface IconComponent extends React.ForwardRefExoticComponent<IconProps> {} 19 | 20 | const Icon: IconComponent = /* #__PURE__*/ React.forwardRef((props, ref) => { 21 | const { width, height, size = null, paths, ...restProps } = props; 22 | return React.createElement('svg', { 23 | viewBox: '0 0 32 32', 24 | ...restProps, 25 | width: width ?? size, 26 | height: height ?? size, 27 | fill: 'none', 28 | 'aria-hidden': 'true', 29 | focusable: 'false', 30 | xmlns: 'http://www.w3.org/2000/svg', 31 | ref, 32 | dangerouslySetInnerHTML: { __html: paths }, 33 | }); 34 | }); 35 | 36 | Icon.displayName = 'VidstackIcon'; 37 | export { Icon }; 38 | -------------------------------------------------------------------------------- /packages/react/src/providers/remotion/index.ts: -------------------------------------------------------------------------------- 1 | export { type RemotionThumbnailProps, default as RemotionThumbnail } from './ui/thumbnail'; 2 | export { type RemotionPosterProps, default as RemotionPoster } from './ui/poster'; 3 | export { 4 | type RemotionSliderThumbnailProps, 5 | default as RemotionSliderThumbnail, 6 | } from './ui/slider-thumbnail'; 7 | export { RemotionProviderLoader } from './loader'; 8 | export type { RemotionProvider } from './provider'; 9 | export type * from './types'; 10 | export { isRemotionProvider, isRemotionSrc as isRemotionSource, isRemotionSrc } from './type-check'; 11 | -------------------------------------------------------------------------------- /packages/react/src/providers/remotion/loader.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import type { 4 | MediaContext, 5 | MediaProviderAdapter, 6 | MediaProviderLoader, 7 | MediaType, 8 | Src, 9 | } from 'vidstack'; 10 | 11 | import * as UI from '../../components/layouts/remotion-ui'; 12 | 13 | export class RemotionProviderLoader implements MediaProviderLoader { 14 | readonly name = 'remotion'; 15 | 16 | target!: HTMLElement; 17 | 18 | constructor() { 19 | UI.RemotionThumbnail.set(React.lazy(() => import('./ui/thumbnail'))); 20 | UI.RemotionSliderThumbnail.set(React.lazy(() => import('./ui/slider-thumbnail'))); 21 | UI.RemotionPoster.set(React.lazy(() => import('./ui/poster'))); 22 | } 23 | 24 | canPlay(src: Src): boolean { 25 | return src.type === 'video/remotion'; 26 | } 27 | 28 | mediaType(): MediaType { 29 | return 'video'; 30 | } 31 | 32 | async load(ctx: MediaContext): Promise<MediaProviderAdapter> { 33 | return new (await import('./provider')).RemotionProvider(this.target, ctx); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/react/src/providers/remotion/type-check.ts: -------------------------------------------------------------------------------- 1 | import type { Src } from 'vidstack'; 2 | 3 | import type { RemotionProvider } from './provider'; 4 | import type { RemotionSrc } from './types'; 5 | 6 | /** @see {@link https://www.vidstack.io/docs/player/providers/remotion} */ 7 | export function isRemotionProvider(provider: any): provider is RemotionProvider { 8 | return provider?.$PROVIDER_TYPE === 'REMOTION'; 9 | } 10 | 11 | export function isRemotionSrc(src?: Src | null): src is RemotionSrc { 12 | return src?.type === 'video/remotion'; 13 | } 14 | -------------------------------------------------------------------------------- /packages/react/src/providers/remotion/ui/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const errorStyle: React.CSSProperties = __DEV__ 4 | ? { 5 | display: 'flex', 6 | alignItems: 'center', 7 | justifyContent: 'center', 8 | color: ' #ff3333', 9 | padding: '24px', 10 | position: 'absolute', 11 | inset: '0', 12 | width: '100%', 13 | height: '100%', 14 | } 15 | : {}; 16 | 17 | export interface ErrorBoundaryProps { 18 | children: React.ReactNode; 19 | fallback?: (error: Error) => React.ReactNode; 20 | onError?: (error: Error) => void; 21 | } 22 | 23 | interface ErrorBoundaryState { 24 | hasError: Error | null; 25 | } 26 | 27 | export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> { 28 | static displayName = 'ErrorBoundary'; 29 | 30 | override state: { hasError: Error | null } = { hasError: null }; 31 | 32 | static getDerivedStateFromError(hasError: Error) { 33 | return { hasError }; 34 | } 35 | 36 | override componentDidCatch(error: Error) { 37 | this.props.onError?.(error); 38 | } 39 | 40 | override render() { 41 | const error = this.state.hasError; 42 | 43 | if (error) { 44 | return ( 45 | <div style={errorStyle}> 46 | {this.props.fallback?.(error) ?? ( 47 | <div style={{ fontWeight: 'bold' }}> 48 | An error has occurred, see console for more information. 49 | </div> 50 | )} 51 | </div> 52 | ); 53 | } 54 | 55 | return this.props.children; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/react/src/providers/remotion/ui/poster.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { useMediaState } from '../../../hooks/use-media-state'; 4 | import RemotionThumbnail, { type RemotionThumbnailProps } from './thumbnail'; 5 | 6 | export interface RemotionPosterProps extends RemotionThumbnailProps {} 7 | 8 | /** 9 | * @attr data-visible - Whether poster should be shown. 10 | * @docs {@link https://www.vidstack.io/docs/player/components/remotion/remotion-poster} 11 | * @example 12 | * ```tsx 13 | * <MediaPlayer> 14 | * <MediaProvider> 15 | * <RemotionPoster frame={100} /> 16 | * </MediaProvider> 17 | * </MediaPlayer> 18 | * ``` 19 | */ 20 | const RemotionPoster = React.forwardRef<HTMLElement, RemotionPosterProps>((props, ref) => { 21 | const $isVisible = !useMediaState('started'); 22 | return ( 23 | <RemotionThumbnail 24 | {...props} 25 | ref={ref} 26 | data-remotion-poster 27 | data-visible={$isVisible || null} 28 | /> 29 | ); 30 | }); 31 | 32 | RemotionPoster.displayName = 'RemotionPoster'; 33 | export default RemotionPoster; 34 | -------------------------------------------------------------------------------- /packages/react/src/providers/remotion/ui/slider-thumbnail.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { useMediaState } from '../../../hooks/use-media-state'; 4 | import { useSliderState } from '../../../hooks/use-slider-state'; 5 | import { isRemotionSrc } from '../type-check'; 6 | import RemotionThumbnail, { type RemotionThumbnailProps } from './thumbnail'; 7 | 8 | export interface RemotionSliderThumbnailProps extends Omit<RemotionThumbnailProps, 'frame'> {} 9 | 10 | /** 11 | * @docs {@link https://www.vidstack.io/docs/player/components/remotion/remotion-slider-thumbnail} 12 | * @example 13 | * ```tsx 14 | * <TimeSlider.Root> 15 | * <TimeSlider.Preview> 16 | * <RemotionSliderThumbnail /> 17 | * </TimeSlider.Preview> 18 | * </TimeSlider.Root> 19 | * ``` 20 | */ 21 | const RemotionSliderThumbnail = React.forwardRef<HTMLElement, RemotionSliderThumbnailProps>( 22 | (props, ref) => { 23 | const $src = useMediaState('currentSrc'), 24 | $percent = useSliderState('pointerPercent'); 25 | 26 | if (!isRemotionSrc($src)) return null; 27 | 28 | return ( 29 | <RemotionThumbnail 30 | {...props} 31 | frame={$src.durationInFrames * ($percent / 100)} 32 | ref={ref} 33 | data-remotion-slider-thumbnail 34 | /> 35 | ); 36 | }, 37 | ); 38 | 39 | RemotionSliderThumbnail.displayName = 'RemotionSliderThumbnail'; 40 | 41 | export default RemotionSliderThumbnail; 42 | -------------------------------------------------------------------------------- /packages/react/src/source.ts: -------------------------------------------------------------------------------- 1 | import type { PlayerSrc as BasePlayerSrc } from 'vidstack'; 2 | 3 | import type { RemotionSrc } from './providers/remotion/types'; 4 | 5 | export type PlayerSrc = BasePlayerSrc | RemotionSrc; 6 | -------------------------------------------------------------------------------- /packages/react/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'maverick.js/std'; 2 | import type { VTTCue } from 'media-captions'; 3 | 4 | export function createVTTCue(startTime = 0, endTime = 0, text = ''): VTTCue { 5 | if (__SERVER__) { 6 | return { 7 | startTime, 8 | endTime, 9 | text, 10 | addEventListener: noop, 11 | removeEventListener: noop, 12 | dispatchEvent: noop, 13 | } as VTTCue; 14 | } 15 | 16 | return new window.VTTCue(startTime, endTime, text); 17 | } 18 | 19 | export function appendParamsToURL(baseUrl: string, params: Record<string, any>) { 20 | const url = new URL(baseUrl); 21 | 22 | for (const key of Object.keys(params)) { 23 | url.searchParams.set(key, params[key] + ''); 24 | } 25 | 26 | return url.toString(); 27 | } 28 | -------------------------------------------------------------------------------- /packages/react/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "types", 6 | "paths": {} 7 | }, 8 | "include": ["src/**/*.ts", "src/**/*.tsx"], 9 | "exclude": ["src/**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "paths": { 6 | "@vidstack/react": ["./src/index.ts"] 7 | }, 8 | "types": ["@types/node"] 9 | }, 10 | "include": ["src/**/*.ts", "src/**/*.tsx", "sandbox"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | import react from '@vitejs/plugin-react'; 4 | import { defineConfig } from 'vite'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | define: { 9 | __DEV__: 'true', 10 | __SERVER__: 'false', 11 | }, 12 | plugins: [ 13 | { 14 | name: 'ts-paths', 15 | enforce: 'pre', 16 | }, 17 | react(), 18 | ], 19 | optimizeDeps: { 20 | noDiscovery: true, 21 | include: ['react', 'react-dom', 'react-dom/client'], 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /packages/vidstack/.templates/sandbox/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidstack/player/f35caca9409e649fa2cec49b42956ef184b7fcac/packages/vidstack/.templates/sandbox/favicon-32x32.png -------------------------------------------------------------------------------- /packages/vidstack/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rahim Alwer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/vidstack/README.md: -------------------------------------------------------------------------------- 1 | # Vidstack 2 | 3 | [![package-badge]][package] 4 | [![discord-badge]][discord] 5 | 6 | Vidstack is a video/audio platform for frontend developers to build high-quality and accessible 7 | experiences on the web. 8 | 9 | ## 📖 Docs 10 | 11 | You can find our documentation at [vidstack.io](https://www.vidstack.io). 12 | 13 | ## 📝 License 14 | 15 | Vidstack is [MIT licensed](./LICENSE). 16 | 17 | [vime]: https://github.com/vime-js/vime 18 | [plyr]: https://github.com/sampotts/plyr 19 | [package]: https://www.npmjs.com/package/vidstack 20 | [package-badge]: https://img.shields.io/npm/v/vidstack/next?style=flat-square 21 | [discord]: https://discord.gg/QAjfh2gZE4 22 | [discord-badge]: https://img.shields.io/discord/742612686679965696?color=%235865F2&label=%20&logo=discord&logoColor=white&style=flat-square 23 | -------------------------------------------------------------------------------- /packages/vidstack/build/build-styles.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | 5 | import chokidar from 'chokidar'; 6 | 7 | const DIRNAME = path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | const STYLES_DIR = path.resolve(DIRNAME, '../styles'), 10 | BASE_STYLES_FILE = path.resolve(STYLES_DIR, 'player/base.css'), 11 | DEFAULT_THEME_DIR = path.resolve(STYLES_DIR, 'player/default'), 12 | DEFAULT_THEME_FILE = path.resolve(DEFAULT_THEME_DIR, 'theme.css'); 13 | 14 | export async function buildDefaultTheme() { 15 | // CSS merge. 16 | let styles = await fs.readFile(BASE_STYLES_FILE, 'utf-8'), 17 | themeDirFiles = await fs.readdir(DEFAULT_THEME_DIR, 'utf-8'); 18 | 19 | for (const file of themeDirFiles) { 20 | if (file === 'theme.css' || file === 'layouts') continue; 21 | styles += '\n' + (await fs.readFile(path.resolve(DEFAULT_THEME_DIR, file), 'utf-8')); 22 | } 23 | 24 | await fs.writeFile(DEFAULT_THEME_FILE, styles); 25 | } 26 | 27 | export function watchStyles(onChange) { 28 | chokidar.watch(STYLES_DIR).on('all', async (_, path) => { 29 | if (path.endsWith('theme.css')) return; 30 | await buildDefaultTheme(); 31 | onChange?.(); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /packages/vidstack/build/rollup-minify.js: -------------------------------------------------------------------------------- 1 | import { transform as esbuild } from 'esbuild'; 2 | 3 | /** @returns {import('rollup').Plugin} */ 4 | export function minify() { 5 | const opts = /** @type {import('esbuild').TransformOptions} */ ({ 6 | target: 'esnext', 7 | format: 'esm', 8 | platform: 'browser', 9 | minify: true, 10 | loader: 'js', 11 | legalComments: 'none', 12 | }); 13 | 14 | return { 15 | name: 'minify', 16 | renderChunk(code) { 17 | return esbuild(code, opts); 18 | }, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/vidstack/npm/analyze.json.d.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentMeta, CustomElementMeta } from '@maverick-js/cli/analyze'; 2 | 3 | declare const json: { 4 | elements: CustomElementMeta[]; 5 | components: ComponentMeta[]; 6 | }; 7 | 8 | export default json; 9 | -------------------------------------------------------------------------------- /packages/vidstack/npm/bundle.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference path="./elements.d.ts" /> 2 | -------------------------------------------------------------------------------- /packages/vidstack/npm/empty.vtt: -------------------------------------------------------------------------------- 1 | WEBVTT 2 | -------------------------------------------------------------------------------- /packages/vidstack/npm/player/index.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference path="../elements.d.ts" /> 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /packages/vidstack/npm/player/layouts/default.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference path="../../elements.d.ts" /> 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /packages/vidstack/npm/player/layouts/index.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference path="../../elements.d.ts" /> 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /packages/vidstack/npm/player/layouts/plyr.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference path="../../elements.d.ts" /> 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /packages/vidstack/npm/player/ui.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference path="../elements.d.ts" /> 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /packages/vidstack/npm/tailwind.d.cts: -------------------------------------------------------------------------------- 1 | declare function plugin(options?: PluginOptions): { 2 | handler: () => void; 3 | }; 4 | 5 | declare namespace plugin { 6 | const __isOptionsFunction: true; 7 | } 8 | 9 | export = plugin; 10 | 11 | export interface PluginOptions { 12 | selector?: string; 13 | prefix?: string; 14 | webComponents?: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/layouts/default/audio-layout.ts: -------------------------------------------------------------------------------- 1 | import { DefaultLayout } from './default-layout'; 2 | import type { DefaultLayoutProps } from './props'; 3 | 4 | /** 5 | * The audio layout is our production-ready UI that's displayed when the media view type is set to 6 | * 'audio'. It includes support for audio tracks, slider chapters, captions, live streams, and much 7 | * more out of the box. 8 | * 9 | * @attr data-match - Whether this layout is being used (query match). 10 | * @attr data-sm - The small layout is active 11 | * @attr data-lg - The large layout is active. 12 | * @attr data-size - The active layout size (sm or lg). 13 | */ 14 | export class DefaultAudioLayout extends DefaultLayout { 15 | static override props: DefaultLayoutProps = { 16 | ...super.props, 17 | when: ({ viewType }) => viewType === 'audio', 18 | smallWhen: ({ width }) => width < 576, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/layouts/default/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, type ReadSignalRecord, type WriteSignal } from 'maverick.js'; 2 | 3 | import type { DefaultLayoutProps } from './props'; 4 | 5 | export interface DefaultLayoutContext extends ReadSignalRecord<DefaultLayoutProps> { 6 | menuPortal: WriteSignal<HTMLElement | null>; 7 | userPrefersAnnouncements: WriteSignal<boolean>; 8 | userPrefersKeyboardAnimations: WriteSignal<boolean>; 9 | } 10 | 11 | export const defaultLayoutContext = createContext<DefaultLayoutContext>(); 12 | 13 | export function useDefaultLayoutContext() { 14 | return useContext(defaultLayoutContext); 15 | } 16 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/layouts/default/translations.ts: -------------------------------------------------------------------------------- 1 | import type { ReadSignal } from 'maverick.js'; 2 | 3 | export type DefaultLayoutWord = 4 | | 'Announcements' 5 | | 'Accessibility' 6 | | 'AirPlay' 7 | | 'Audio' 8 | | 'Auto' 9 | | 'Boost' 10 | | 'Captions' 11 | | 'Caption Styles' 12 | | 'Captions look like this' 13 | | 'Chapters' 14 | | 'Closed-Captions Off' 15 | | 'Closed-Captions On' 16 | | 'Connected' 17 | | 'Continue' 18 | | 'Connecting' 19 | | 'Default' 20 | | 'Disabled' 21 | | 'Disconnected' 22 | | 'Display Background' 23 | | 'Download' 24 | | 'Enter Fullscreen' 25 | | 'Enter PiP' 26 | | 'Exit Fullscreen' 27 | | 'Exit PiP' 28 | | 'Font' 29 | | 'Family' 30 | | 'Fullscreen' 31 | | 'Google Cast' 32 | | 'Keyboard Animations' 33 | | 'LIVE' 34 | | 'Loop' 35 | | 'Mute' 36 | | 'Normal' 37 | | 'Off' 38 | | 'Pause' 39 | | 'Play' 40 | | 'Playback' 41 | | 'PiP' 42 | | 'Quality' 43 | | 'Replay' 44 | | 'Reset' 45 | | 'Seek Backward' 46 | | 'Seek Forward' 47 | | 'Seek' 48 | | 'Settings' 49 | | 'Skip To Live' 50 | | 'Speed' 51 | | 'Size' 52 | | 'Color' 53 | | 'Opacity' 54 | | 'Shadow' 55 | | 'Text' 56 | | 'Text Background' 57 | | 'Track' 58 | | 'Unmute' 59 | | 'Volume'; 60 | 61 | export type DefaultLayoutTranslations = { 62 | [word in DefaultLayoutWord]: string; 63 | }; 64 | 65 | export function i18n( 66 | translations: ReadSignal<Partial<DefaultLayoutTranslations> | null>, 67 | word: string, 68 | ) { 69 | return translations()?.[word] ?? word; 70 | } 71 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/layouts/default/video-layout.ts: -------------------------------------------------------------------------------- 1 | import { DefaultLayout } from './default-layout'; 2 | import type { DefaultLayoutProps } from './props'; 3 | 4 | /** 5 | * The video layout is our production-ready UI that's displayed when the media view type is set to 6 | * 'video'. It includes support for picture-in-picture, fullscreen, slider chapters, slider 7 | * previews, captions, audio/quality settings, live streams, and much more out of the box. 8 | * 9 | * @attr data-match - Whether this layout is being used (query match). 10 | * @attr data-sm - The small layout is active 11 | * @attr data-lg - The large layout is active. 12 | * @attr data-size - The active layout size. 13 | */ 14 | export class DefaultVideoLayout extends DefaultLayout { 15 | static override props: DefaultLayoutProps = { 16 | ...super.props, 17 | when: ({ viewType }) => viewType === 'video', 18 | smallWhen: ({ width, height }) => width < 576 || height < 380, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/layouts/plyr/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, type ReadSignalRecord, type WriteSignal } from 'maverick.js'; 2 | 3 | import type { PlyrLayoutProps } from './props'; 4 | 5 | export interface PlyrLayoutContext extends ReadSignalRecord<PlyrLayoutProps> { 6 | previewTime: WriteSignal<number>; 7 | } 8 | 9 | export const plyrLayoutContext = createContext<PlyrLayoutContext>(); 10 | 11 | export function usePlyrLayoutContext() { 12 | return useContext(plyrLayoutContext); 13 | } 14 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/layouts/plyr/translations.ts: -------------------------------------------------------------------------------- 1 | import type { ReadSignal } from 'maverick.js'; 2 | 3 | export type PlyrLayoutWord = 4 | | 'Ad' 5 | | 'All' 6 | | 'AirPlay' 7 | | 'Audio' 8 | | 'Auto' 9 | | 'Buffered' 10 | | 'Captions' 11 | | 'Current time' 12 | | 'Default' 13 | | 'Disable captions' 14 | | 'Disabled' 15 | | 'Download' 16 | | 'Duration' 17 | | 'Enable captions' 18 | | 'Enabled' 19 | | 'End' 20 | | 'Enter Fullscreen' 21 | | 'Exit Fullscreen' 22 | | 'Forward' 23 | | 'Go back to previous menu' 24 | | 'LIVE' 25 | | 'Loop' 26 | | 'Mute' 27 | | 'Normal' 28 | | 'Pause' 29 | | 'Enter PiP' 30 | | 'Exit PiP' 31 | | 'Play' 32 | | 'Played' 33 | | 'Quality' 34 | | 'Reset' 35 | | 'Restart' 36 | | 'Rewind' 37 | | 'Seek' 38 | | 'Settings' 39 | | 'Speed' 40 | | 'Start' 41 | | 'Unmute' 42 | | 'Volume'; 43 | 44 | export type PlyrLayoutTranslations = { 45 | [word in PlyrLayoutWord]: string; 46 | }; 47 | 48 | export function i18n( 49 | translations: ReadSignal<Partial<PlyrLayoutTranslations> | null>, 50 | word: string, 51 | ) { 52 | return translations()?.[word] ?? word; 53 | } 54 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/provider/tracks.ts: -------------------------------------------------------------------------------- 1 | import { effect, type ReadSignal } from 'maverick.js'; 2 | 3 | import type { MediaContext } from '../../core/api/media-context'; 4 | import { TextTrack, type TextTrackInit } from '../../core/tracks/text/text-track'; 5 | 6 | export class Tracks { 7 | #domTracks: ReadSignal<TextTrackInit[]>; 8 | #media: MediaContext; 9 | #prevTracks: (TextTrack | TextTrackInit)[] = []; 10 | 11 | constructor(domTracks: ReadSignal<TextTrackInit[]>, media: MediaContext) { 12 | this.#domTracks = domTracks; 13 | this.#media = media; 14 | effect(this.#onTracksChange.bind(this)); 15 | } 16 | 17 | #onTracksChange() { 18 | const newTracks = this.#domTracks(); 19 | 20 | for (const oldTrack of this.#prevTracks) { 21 | if (!newTracks.some((t) => t.id === oldTrack.id)) { 22 | const track = oldTrack.id && this.#media.textTracks.getById(oldTrack.id); 23 | if (track) this.#media.textTracks.remove(track); 24 | } 25 | } 26 | 27 | for (const newTrack of newTracks) { 28 | const id = newTrack.id || TextTrack.createId(newTrack); 29 | if (!this.#media.textTracks.getById(id)) { 30 | newTrack.id = id; 31 | this.#media.textTracks.add(newTrack); 32 | } 33 | } 34 | 35 | this.#prevTracks = newTracks; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/buttons/toggle-button.ts: -------------------------------------------------------------------------------- 1 | import { Component, prop, signal } from 'maverick.js'; 2 | 3 | import { ToggleButtonController } from './toggle-button-controller'; 4 | 5 | export interface ToggleButtonProps { 6 | /** 7 | * Whether it should start in the on (pressed) state. 8 | */ 9 | defaultPressed: boolean; 10 | /** 11 | * Whether the button should be disabled (non-interactive). 12 | */ 13 | disabled: boolean; 14 | } 15 | 16 | /** 17 | * A toggle button is a two-state button that can be either off (not pressed) or on (pressed). 18 | * 19 | * @attr data-pressed - Whether the toggle is in an "on" state (pressed). 20 | * @attr aria-pressed - Same as `data-pressed` but `"true"` or `"false"`. 21 | * @attr data-focus - Whether button is being keyboard focused. 22 | * @attr data-hocus - Whether button is being keyboard focused or hovered over. 23 | * @docs {@link https://www.vidstack.io/docs/player/components/buttons/toggle-button} 24 | */ 25 | export class ToggleButton< 26 | Props extends ToggleButtonProps = ToggleButtonProps, 27 | > extends Component<Props> { 28 | static props: ToggleButtonProps = { 29 | disabled: false, 30 | defaultPressed: false, 31 | }; 32 | 33 | #pressed = signal(false); 34 | 35 | /** 36 | * Whether the toggle is currently in a `pressed` state. 37 | */ 38 | @prop 39 | get pressed() { 40 | return this.#pressed(); 41 | } 42 | 43 | constructor() { 44 | super(); 45 | new ToggleButtonController({ 46 | isPresssed: this.#pressed, 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/controls-group.ts: -------------------------------------------------------------------------------- 1 | import { Component } from 'maverick.js'; 2 | import { setStyle } from 'maverick.js/std'; 3 | 4 | /** 5 | * This component creates a container for media controls. 6 | * 7 | * @docs {@link https://www.vidstack.io/docs/player/components/media/controls#group} 8 | */ 9 | export class ControlsGroup extends Component { 10 | protected override onAttach(el: HTMLElement): void { 11 | if (!el.style.pointerEvents) setStyle(el, 'pointer-events', 'auto'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/menu/menu-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, type ReadSignal, type WriteSignal } from 'maverick.js'; 2 | 3 | import type { Menu } from './menu'; 4 | import type { MenuButton } from './menu-button'; 5 | import type { MenuItems } from './menu-items'; 6 | 7 | export interface MenuContext { 8 | readonly submenu: boolean; 9 | readonly expanded: ReadSignal<boolean>; 10 | readonly hint: WriteSignal<string>; 11 | readonly button: ReadSignal<HTMLElement | null>; 12 | readonly content: ReadSignal<HTMLElement | null>; 13 | attachMenuButton(button: MenuButton): void; 14 | attachMenuItems(menuItems: MenuItems): void; 15 | attachObserver(observer: MenuObserver): void; 16 | disable(disable: boolean): void; 17 | disableMenuButton(disable: boolean): void; 18 | addSubmenu(menu: Menu): void; 19 | onTransitionEvent(callback: (event: TransitionEvent) => void): void; 20 | } 21 | 22 | export interface MenuObserver { 23 | onOpen?(trigger?: Event): void; 24 | onClose?(trigger?: Event): void; 25 | } 26 | 27 | export const menuContext = createContext<MenuContext>(); 28 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/menu/menu-item.ts: -------------------------------------------------------------------------------- 1 | import { MenuButton } from './menu-button'; 2 | 3 | /** 4 | * Represents a specific option or action, typically displayed as a text label or icon, which 5 | * users can select to access or perform a particular function or view related content. 6 | * 7 | * @docs {@link https://www.vidstack.io/docs/player/components/menu/menu} 8 | */ 9 | export class MenuItem extends MenuButton {} 10 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/menu/radio/radio-controller.ts: -------------------------------------------------------------------------------- 1 | import { createContext, type ReadSignal } from 'maverick.js'; 2 | 3 | export interface RadioController { 4 | value: ReadSignal<string>; 5 | check(checked: boolean, trigger?: Event); 6 | onCheck: RadioChangeCallback | null; 7 | } 8 | 9 | export interface RadioChangeCallback { 10 | (value: string, trigger?: Event): void; 11 | } 12 | 13 | export interface RadioGroupContext { 14 | add(radio: RadioController): void; 15 | remove(radio: RadioController): void; 16 | } 17 | 18 | export const radioControllerContext = createContext<RadioGroupContext>(); 19 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/sliders/slider-thumbnail.ts: -------------------------------------------------------------------------------- 1 | import { useState, type StateContext } from 'maverick.js'; 2 | 3 | import { Thumbnail } from '../thumbnails/thumbnail'; 4 | import { sliderState } from './slider/api/state'; 5 | import { Slider } from './slider/slider'; 6 | 7 | /** 8 | * Used to display preview thumbnails when the user is hovering or dragging the time slider. 9 | * The time ranges in the WebVTT file will automatically be matched based on the current slider 10 | * pointer position. 11 | * 12 | * @attr data-loading - Whether thumbnail image is loading. 13 | * @attr data-error - Whether an error occurred loading thumbnail. 14 | * @attr data-hidden - Whether thumbnail is not available or failed to load. 15 | * @docs {@link https://www.vidstack.io/docs/player/components/sliders/slider-thumbnail} 16 | */ 17 | export class SliderThumbnail extends Thumbnail { 18 | #slider!: StateContext<typeof sliderState>; 19 | 20 | protected override onAttach(el: HTMLElement) { 21 | this.#slider = useState(Slider.state); 22 | } 23 | 24 | protected override getTime() { 25 | const { duration, clipStartTime } = this.media.$state; 26 | return clipStartTime() + this.#slider.pointerRate() * duration(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/sliders/slider/api/cssvars.ts: -------------------------------------------------------------------------------- 1 | export interface SliderCSSVars { 2 | /** 3 | * The fill rate expressed as a percentage. 4 | */ 5 | readonly 'slider-fill': string; 6 | /** 7 | * The pointer rate expressed as a percentage. 8 | */ 9 | readonly 'slider-pointer': string; 10 | } 11 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/sliders/slider/format.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'maverick.js'; 2 | 3 | import type { FormatTimeOptions } from '../../../../utils/time'; 4 | 5 | export const sliderValueFormatContext = createContext<SliderValueFormat>(() => ({})); 6 | 7 | export interface SliderValueFormat { 8 | default?: 'value' | 'percent' | 'time'; 9 | value?(value: number): string | number; 10 | percent?(percent: number, decimalPlaces: number): string | number; 11 | time?(value: number, options?: FormatTimeOptions): string; 12 | } 13 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/sliders/slider/slider-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, type ReadSignal, type WriteSignal } from 'maverick.js'; 2 | 3 | import type { SliderOrientation } from './types'; 4 | 5 | export interface SliderContext { 6 | disabled: ReadSignal<boolean>; 7 | orientation: ReadSignal<SliderOrientation>; 8 | preview: WriteSignal<HTMLElement | null>; 9 | } 10 | 11 | export const sliderContext = createContext<SliderContext>(); 12 | 13 | export interface SliderObserverContext { 14 | onDragStart?(): void; 15 | onDragEnd?(): void; 16 | } 17 | 18 | export const sliderObserverContext = createContext<SliderObserverContext>(); 19 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/sliders/slider/types.ts: -------------------------------------------------------------------------------- 1 | export type SliderOrientation = 'horizontal' | 'vertical'; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/sliders/slider/utils.ts: -------------------------------------------------------------------------------- 1 | import { clampNumber, getNumberOfDecimalPlaces, round } from '../../../../utils/number'; 2 | 3 | export function getClampedValue(min: number, max: number, value: number, step: number) { 4 | return clampNumber(min, round(value, getNumberOfDecimalPlaces(step)), max); 5 | } 6 | 7 | export function getValueFromRate(min: number, max: number, rate: number, step: number) { 8 | const boundRate = clampNumber(0, rate, 1), 9 | range = max - min, 10 | fill = range * boundRate, 11 | stepRatio = fill / step, 12 | steps = step * Math.round(stepRatio); 13 | return min + steps; 14 | } 15 | -------------------------------------------------------------------------------- /packages/vidstack/src/components/ui/tooltip/tooltip-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, type ReadSignal } from 'maverick.js'; 2 | 3 | export interface TooltipContext { 4 | trigger: ReadSignal<HTMLElement | null>; 5 | content: ReadSignal<HTMLElement | null>; 6 | showing: ReadSignal<boolean>; 7 | attachTrigger(el: HTMLElement): void; 8 | detachTrigger(el: HTMLElement): void; 9 | attachContent(el: HTMLElement): void; 10 | detachContent(el: HTMLElement): void; 11 | } 12 | 13 | export const tooltipContext = createContext<TooltipContext>(); 14 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/api/media-attrs.ts: -------------------------------------------------------------------------------- 1 | import type { MediaState } from './player-state'; 2 | 3 | export const MEDIA_ATTRIBUTES = Symbol(__DEV__ ? 'MEDIA_ATTRIBUTES' : 0); 4 | 5 | export const mediaAttributes: (keyof MediaState)[] = [ 6 | 'autoPlay', 7 | 'canAirPlay', 8 | 'canFullscreen', 9 | 'canGoogleCast', 10 | 'canLoad', 11 | 'canLoadPoster', 12 | 'canPictureInPicture', 13 | 'canPlay', 14 | 'canSeek', 15 | 'ended', 16 | 'fullscreen', 17 | 'isAirPlayConnected', 18 | 'isGoogleCastConnected', 19 | 'live', 20 | 'liveEdge', 21 | 'loop', 22 | 'mediaType', 23 | 'muted', 24 | 'paused', 25 | 'pictureInPicture', 26 | 'playing', 27 | 'playsInline', 28 | 'remotePlaybackState', 29 | 'remotePlaybackType', 30 | 'seeking', 31 | 'started', 32 | 'streamType', 33 | 'viewType', 34 | 'waiting', 35 | ]; 36 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/api/player-controller.ts: -------------------------------------------------------------------------------- 1 | import { ViewController } from 'maverick.js'; 2 | import type { MediaPlayerEvents } from './player-events'; 3 | import type { MediaPlayerProps } from './player-props'; 4 | import type { MediaPlayerState } from './player-state'; 5 | 6 | export class MediaPlayerController extends ViewController< 7 | MediaPlayerProps, 8 | MediaPlayerState, 9 | MediaPlayerEvents 10 | > {} 11 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/keyboard/aria-shortcuts.ts: -------------------------------------------------------------------------------- 1 | import { onDispose, ViewController } from 'maverick.js'; 2 | import { isArray, isString } from 'maverick.js/std'; 3 | 4 | import { useMediaContext } from '../api/media-context'; 5 | 6 | export class ARIAKeyShortcuts extends ViewController { 7 | #shortcut: string; 8 | 9 | constructor(shortcut: string) { 10 | super(); 11 | this.#shortcut = shortcut; 12 | } 13 | 14 | protected override onAttach(el: HTMLElement): void { 15 | const { $props, ariaKeys } = useMediaContext(), 16 | keys = el.getAttribute('aria-keyshortcuts'); 17 | 18 | if (keys) { 19 | ariaKeys[this.#shortcut] = keys; 20 | 21 | if (!__SERVER__) { 22 | onDispose(() => { 23 | delete ariaKeys[this.#shortcut]; 24 | }); 25 | } 26 | 27 | return; 28 | } 29 | 30 | const shortcuts = $props.keyShortcuts()[this.#shortcut]; 31 | 32 | if (shortcuts) { 33 | const keys = isArray(shortcuts) 34 | ? shortcuts.join(' ') 35 | : isString(shortcuts) 36 | ? shortcuts 37 | : shortcuts?.keys; 38 | 39 | el.setAttribute('aria-keyshortcuts', isArray(keys) ? keys.join(' ') : keys); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/keyboard/types.ts: -------------------------------------------------------------------------------- 1 | import type { MediaPlayer } from '../../components/player'; 2 | import type { MediaRemoteControl } from '../state/remote-control'; 3 | 4 | export type MediaKeyTarget = 'document' | 'player'; 5 | 6 | export interface MediaKeyShortcuts { 7 | [keys: string]: MediaKeyShortcut | undefined; 8 | togglePaused?: MediaKeyShortcut; 9 | toggleMuted?: MediaKeyShortcut; 10 | toggleFullscreen?: MediaKeyShortcut; 11 | togglePictureInPicture?: MediaKeyShortcut; 12 | toggleCaptions?: MediaKeyShortcut; 13 | seekBackward?: MediaKeyShortcut; 14 | seekForward?: MediaKeyShortcut; 15 | speedUp?: MediaKeyShortcut; 16 | slowDown?: MediaKeyShortcut; 17 | volumeUp?: MediaKeyShortcut; 18 | volumeDown?: MediaKeyShortcut; 19 | } 20 | 21 | export type MediaKeyShortcut = MediaKeysCallback | string | string[] | null; 22 | 23 | export interface MediaKeysCallback { 24 | keys: string | string[]; 25 | 26 | /** @deprecated - use `onKeyUp` or `onKeyDown` */ 27 | callback?(event: KeyboardEvent, remote: MediaRemoteControl): void; 28 | 29 | onKeyUp?(context: { 30 | event: KeyboardEvent; 31 | player: MediaPlayer; 32 | remote: MediaRemoteControl; 33 | }): void; 34 | 35 | onKeyDown?(context: { 36 | event: KeyboardEvent; 37 | player: MediaPlayer; 38 | remote: MediaRemoteControl; 39 | }): void; 40 | } 41 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/quality/events.ts: -------------------------------------------------------------------------------- 1 | import type { DOMEvent } from 'maverick.js/std'; 2 | 3 | import type { ListReadonlyChangeEvent } from '../../foundation/list/list'; 4 | import type { VideoQuality, VideoQualityList } from './video-quality'; 5 | 6 | export interface VideoQualityListEvents { 7 | add: VideoQualityAddEvent; 8 | remove: VideoQualityRemoveEvent; 9 | change: VideoQualityChangeEvent; 10 | 'auto-change': VideoQualityAutoChangeEvent; 11 | 'readonly-change': ListReadonlyChangeEvent; 12 | } 13 | 14 | export interface VideoQualityListEvent<T> extends DOMEvent<T> { 15 | target: VideoQualityList; 16 | } 17 | 18 | /** 19 | * Fired when a video quality has been added to the list. 20 | * 21 | * @detail newQuality 22 | */ 23 | export interface VideoQualityAddEvent extends VideoQualityListEvent<VideoQuality> {} 24 | 25 | /** 26 | * Fired when a video quality has been removed from the list. 27 | * 28 | * @detail removedQuality 29 | */ 30 | export interface VideoQualityRemoveEvent extends VideoQualityListEvent<VideoQuality> {} 31 | 32 | /** 33 | * Fired when the selected video quality has changed. 34 | * 35 | * @detail change 36 | */ 37 | export interface VideoQualityChangeEvent 38 | extends VideoQualityListEvent<VideoQualityChangeEventDetail> {} 39 | 40 | export interface VideoQualityChangeEventDetail { 41 | prev: VideoQuality | null; 42 | current: VideoQuality; 43 | } 44 | 45 | /** 46 | * Fired when auto quality selection is enabled or disabled. 47 | */ 48 | export interface VideoQualityAutoChangeEvent extends VideoQualityListEvent<boolean> {} 49 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/quality/symbols.ts: -------------------------------------------------------------------------------- 1 | export const SET_AUTO = Symbol(__DEV__ ? 'SET_AUTO_QUALITY' : 0), 2 | ENABLE_AUTO = Symbol(__DEV__ ? 'ENABLE_AUTO_QUALITY' : 0); 3 | 4 | /** @internal */ 5 | export const QualitySymbol = { 6 | setAuto: SET_AUTO, 7 | enableAuto: ENABLE_AUTO, 8 | } as const; 9 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/quality/utils.ts: -------------------------------------------------------------------------------- 1 | import type { VideoQuality } from './video-quality'; 2 | 3 | export function sortVideoQualities(qualities: VideoQuality[], desc?: boolean) { 4 | return [...qualities].sort(desc ? compareVideoQualityDesc : compareVideoQualityAsc); 5 | } 6 | 7 | function compareVideoQualityAsc(a: VideoQuality, b: VideoQuality) { 8 | return a.height === b.height ? (a.bitrate ?? 0) - (b.bitrate ?? 0) : a.height - b.height; 9 | } 10 | 11 | function compareVideoQualityDesc(a: VideoQuality, b: VideoQuality) { 12 | return b.height === a.height ? (b.bitrate ?? 0) - (a.bitrate ?? 0) : b.height - a.height; 13 | } 14 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/state/media-load-controller.ts: -------------------------------------------------------------------------------- 1 | import { onDispose } from 'maverick.js'; 2 | import { waitIdlePeriod } from 'maverick.js/std'; 3 | 4 | import { MediaPlayerController } from '../api/player-controller'; 5 | 6 | export class MediaLoadController extends MediaPlayerController { 7 | #type: 'load' | 'posterLoad'; 8 | #callback: () => void; 9 | 10 | constructor(type: 'load' | 'posterLoad', callback: () => void) { 11 | super(); 12 | this.#type = type; 13 | this.#callback = callback; 14 | } 15 | 16 | override async onAttach(el: HTMLElement) { 17 | if (__SERVER__) return; 18 | 19 | const load = this.$props[this.#type](); 20 | 21 | if (load === 'eager') { 22 | requestAnimationFrame(this.#callback); 23 | } else if (load === 'idle') { 24 | waitIdlePeriod(this.#callback); 25 | } else if (load === 'visible') { 26 | let dispose: (() => void) | undefined, 27 | observer = new IntersectionObserver((entries) => { 28 | if (!this.scope) return; 29 | if (entries[0].isIntersecting) { 30 | dispose?.(); 31 | dispose = undefined; 32 | this.#callback(); 33 | } 34 | }); 35 | 36 | observer.observe(el); 37 | dispose = onDispose(() => observer.disconnect()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/state/tracked-media-events.ts: -------------------------------------------------------------------------------- 1 | import type { MediaEvents } from '../api/media-events'; 2 | 3 | export const TRACKED_EVENT = new Set<keyof MediaEvents>([ 4 | 'auto-play', 5 | 'auto-play-fail', 6 | 'can-load', 7 | 'sources-change', 8 | 'source-change', 9 | 'load-start', 10 | 'abort', 11 | 'error', 12 | 'loaded-metadata', 13 | 'loaded-data', 14 | 'can-play', 15 | 'play', 16 | 'play-fail', 17 | 'pause', 18 | 'playing', 19 | 'seeking', 20 | 'seeked', 21 | 'waiting', 22 | ]); 23 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/tracks/audio/audio-tracks.ts: -------------------------------------------------------------------------------- 1 | import { SelectList, type SelectListItem } from '../../../foundation/list/select-list'; 2 | import type { AudioTrackListEvents } from './events'; 3 | 4 | /** 5 | * @see {@link https://vidstack.io/docs/player/api/audio-tracks} 6 | */ 7 | export class AudioTrackList extends SelectList<AudioTrack, AudioTrackListEvents> {} 8 | 9 | /** 10 | * @see {@link https://vidstack.io/docs/player/api/audio-tracks} 11 | */ 12 | export interface AudioTrack extends SelectListItem { 13 | /** 14 | * A string which uniquely identifies the track within the media. 15 | */ 16 | readonly id: string; 17 | /** 18 | * A human-readable label for the track, or an empty string if unknown. 19 | */ 20 | readonly label: string; 21 | /** 22 | * A string specifying the audio track's primary language, or an empty string if unknown. The 23 | * language is specified as a BCP 47 (RFC 5646) language code, such as "en-US" or "pt-BR". 24 | */ 25 | readonly language: string; 26 | /** 27 | * A string specifying the category into which the track falls. For example, the main audio 28 | * track would have a kind of "main". 29 | * 30 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioTrack/kind} 31 | */ 32 | readonly kind: string; 33 | } 34 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/tracks/audio/events.ts: -------------------------------------------------------------------------------- 1 | import type { DOMEvent } from 'maverick.js/std'; 2 | 3 | import type { ListReadonlyChangeEvent } from '../../../foundation/list/list'; 4 | import type { AudioTrack, AudioTrackList } from './audio-tracks'; 5 | 6 | export interface AudioTrackListEvents { 7 | add: AudioTrackAddEvent; 8 | remove: AudioTrackRemoveEvent; 9 | change: AudioTrackChangeEvent; 10 | 'readonly-change': ListReadonlyChangeEvent; 11 | } 12 | 13 | export interface AudioTrackListEvent<T> extends DOMEvent<T> { 14 | target: AudioTrackList; 15 | } 16 | 17 | /** 18 | * Fired when an audio track has been added to the list. 19 | * 20 | * @detail newTrack 21 | */ 22 | export interface AudioTrackAddEvent extends AudioTrackListEvent<AudioTrack> {} 23 | 24 | /** 25 | * Fired when an audio track has been removed from the list. 26 | * 27 | * @detail removedTrack 28 | */ 29 | export interface AudioTrackRemoveEvent extends AudioTrackListEvent<AudioTrack> {} 30 | 31 | /** 32 | * Fired when the selected audio track has changed. 33 | * 34 | * @detail change 35 | */ 36 | export interface AudioTrackChangeEvent extends AudioTrackListEvent<ChangeAudioTrackEventDetail> {} 37 | 38 | export interface ChangeAudioTrackEventDetail { 39 | prev: AudioTrack | null; 40 | current: AudioTrack; 41 | } 42 | -------------------------------------------------------------------------------- /packages/vidstack/src/core/tracks/text/symbols.ts: -------------------------------------------------------------------------------- 1 | const CROSS_ORIGIN = Symbol(__DEV__ ? 'TEXT_TRACK_CROSS_ORIGIN' : 0), 2 | READY_STATE = Symbol(__DEV__ ? 'TEXT_TRACK_READY_STATE' : 0), 3 | UPDATE_ACTIVE_CUES = Symbol(__DEV__ ? 'TEXT_TRACK_UPDATE_ACTIVE_CUES' : 0), 4 | CAN_LOAD = Symbol(__DEV__ ? 'TEXT_TRACK_CAN_LOAD' : 0), 5 | ON_MODE_CHANGE = Symbol(__DEV__ ? 'TEXT_TRACK_ON_MODE_CHANGE' : 0), 6 | NATIVE = Symbol(__DEV__ ? 'TEXT_TRACK_NATIVE' : 0), 7 | NATIVE_HLS = Symbol(__DEV__ ? 'TEXT_TRACK_NATIVE_HLS' : 0); 8 | 9 | export const TextTrackSymbol = { 10 | crossOrigin: CROSS_ORIGIN, 11 | readyState: READY_STATE, 12 | updateActiveCues: UPDATE_ACTIVE_CUES, 13 | canLoad: CAN_LOAD, 14 | onModeChange: ON_MODE_CHANGE, 15 | native: NATIVE, 16 | nativeHLS: NATIVE_HLS, 17 | } as const; 18 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/bundles/cdn-legacy/player-with-default.ts: -------------------------------------------------------------------------------- 1 | import '../player'; 2 | import '../player-layouts'; 3 | import '../player-ui'; 4 | import '../icons'; 5 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/bundles/cdn-legacy/player-with-layouts.ts: -------------------------------------------------------------------------------- 1 | import '../player'; 2 | import '../player-layouts'; 3 | import '../player-ui'; 4 | import '../icons'; 5 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/bundles/cdn-legacy/player-with-plyr.ts: -------------------------------------------------------------------------------- 1 | import '../player'; 2 | import '../player-layouts/plyr'; 3 | import '../icons'; 4 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/bundles/cdn-legacy/player.ts: -------------------------------------------------------------------------------- 1 | import '../player'; 2 | import '../player-ui'; 3 | import '../icons'; 4 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/bundles/cdn/player.core.ts: -------------------------------------------------------------------------------- 1 | import '../player'; 2 | import '../player-ui'; 3 | import '../../../global/player'; 4 | 5 | export * from '../../../exports/core'; 6 | export * from '../../../global/player'; 7 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/bundles/cdn/player.ts: -------------------------------------------------------------------------------- 1 | import '../player'; 2 | import '../player-layouts/default'; 3 | import '../player-ui'; 4 | import '../icons'; 5 | import '../../../global/player'; 6 | 7 | export * from '../../../exports/core'; 8 | export * from '../../../global/player'; 9 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/bundles/cdn/plyr.ts: -------------------------------------------------------------------------------- 1 | import '../player'; 2 | import '../player-layouts/plyr'; 3 | import '../../../global/plyr'; 4 | 5 | export * from '../../../exports/core'; 6 | export * from '../../../global/plyr'; 7 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/bundles/icons.ts: -------------------------------------------------------------------------------- 1 | import 'media-icons/element'; 2 | 3 | export type { IconType } from 'media-icons'; 4 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/bundles/player-layouts/default.ts: -------------------------------------------------------------------------------- 1 | import { defineCustomElement } from 'maverick.js/element'; 2 | 3 | import { MediaAudioLayoutElement } from '../../define/layouts/default/audio-layout-element'; 4 | import { MediaVideoLayoutElement } from '../../define/layouts/default/video-layout-element'; 5 | 6 | defineCustomElement(MediaAudioLayoutElement); 7 | defineCustomElement(MediaVideoLayoutElement); 8 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/bundles/player-layouts/index.ts: -------------------------------------------------------------------------------- 1 | import { defineCustomElement } from 'maverick.js/element'; 2 | 3 | import { MediaAudioLayoutElement } from '../../define/layouts/default/audio-layout-element'; 4 | import { MediaVideoLayoutElement } from '../../define/layouts/default/video-layout-element'; 5 | import { MediaPlyrLayoutElement } from '../../define/layouts/plyr/plyr-layout-element'; 6 | 7 | defineCustomElement(MediaAudioLayoutElement); 8 | defineCustomElement(MediaVideoLayoutElement); 9 | defineCustomElement(MediaPlyrLayoutElement); 10 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/bundles/player.ts: -------------------------------------------------------------------------------- 1 | import { defineCustomElement } from 'maverick.js/element'; 2 | 3 | import { MediaPlayerElement } from '../define/player-element'; 4 | import { MediaProviderElement } from '../define/provider-element'; 5 | 6 | defineCustomElement(MediaPlayerElement); 7 | defineCustomElement(MediaProviderElement); 8 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/announcer-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { MediaAnnouncer } from '../../components/aria/announcer'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/display/announcer} 7 | * @example 8 | * ```html 9 | * <media-announcer></media-announcer> 10 | * ``` 11 | */ 12 | export class MediaAnnouncerElement extends Host(HTMLElement, MediaAnnouncer) { 13 | static tagName = 'media-announcer'; 14 | } 15 | 16 | declare global { 17 | interface HTMLElementTagNameMap { 18 | 'media-announcer': MediaAnnouncerElement; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/buttons/airplay-button-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { AirPlayButton } from '../../../components/ui/buttons/airplay-button'; 4 | 5 | /** 6 | * @example 7 | * ```html 8 | * <media-airplay-button> 9 | * <media-icon type="airplay"></media-icon> 10 | * </media-airplay-button> 11 | * ``` 12 | */ 13 | export class MediaAirPlayButtonElement extends Host(HTMLElement, AirPlayButton) { 14 | static tagName = 'media-airplay-button'; 15 | } 16 | 17 | declare global { 18 | interface HTMLElementTagNameMap { 19 | 'media-airplay-button': MediaAirPlayButtonElement; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/buttons/caption-button-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { CaptionButton } from '../../../components/ui/buttons/caption-button'; 4 | 5 | /** 6 | * @example 7 | * ```html 8 | * <media-caption-button> 9 | * <media-icon type="closed-captions-on"></media-icon> 10 | * <media-icon type="closed-captions"></media-icon> 11 | * </media-caption-button> 12 | * ``` 13 | */ 14 | export class MediaCaptionButtonElement extends Host(HTMLElement, CaptionButton) { 15 | static tagName = 'media-caption-button'; 16 | } 17 | 18 | declare global { 19 | interface HTMLElementTagNameMap { 20 | 'media-caption-button': MediaCaptionButtonElement; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/buttons/fullscreen-button-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { FullscreenButton } from '../../../components/ui/buttons/fullscreen-button'; 4 | 5 | /** 6 | * @example 7 | * ```html 8 | * <media-fullscreen-button> 9 | * <media-icon type="fullscreen"></media-icon> 10 | * <media-icon type="fullscreen-exit"></media-icon> 11 | * </media-fullscreen-button> 12 | * ``` 13 | */ 14 | export class MediaFullscreenButtonElement extends Host(HTMLElement, FullscreenButton) { 15 | static tagName = 'media-fullscreen-button'; 16 | } 17 | 18 | declare global { 19 | interface HTMLElementTagNameMap { 20 | 'media-fullscreen-button': MediaFullscreenButtonElement; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/buttons/google-cast-button-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { GoogleCastButton } from '../../../components/ui/buttons/google-cast-button'; 4 | 5 | /** 6 | * @example 7 | * ```html 8 | * <media-google-cast-button> 9 | * <media-icon type="chromecast"></media-icon> 10 | * </media-google-cast-button> 11 | * ``` 12 | */ 13 | export class MediaGoogleCastButtonElement extends Host(HTMLElement, GoogleCastButton) { 14 | static tagName = 'media-google-cast-button'; 15 | } 16 | 17 | declare global { 18 | interface HTMLElementTagNameMap { 19 | 'media-google-cast-button': MediaGoogleCastButtonElement; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/buttons/live-button-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { LiveButton } from '../../../components/ui/buttons/live-button'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/buttons/live-button} 7 | * @example 8 | * ```html 9 | * <media-live-button> 10 | * <!-- ... --> 11 | * </media-live-button> 12 | * ``` 13 | */ 14 | export class MediaLiveButtonElement extends Host(HTMLElement, LiveButton) { 15 | static tagName = 'media-live-button'; 16 | } 17 | 18 | declare global { 19 | interface HTMLElementTagNameMap { 20 | 'media-live-button': MediaLiveButtonElement; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/buttons/mute-button-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { MuteButton } from '../../../components/ui/buttons/mute-button'; 4 | 5 | /** 6 | * @example 7 | * ```html 8 | * <media-mute-button> 9 | * <media-icon type="mute"></media-icon> 10 | * <media-icon type="volume-low"></media-icon> 11 | * <media-icon type="volume-high"></media-icon> 12 | * </media-mute-button> 13 | * ``` 14 | */ 15 | export class MediaMuteButtonElement extends Host(HTMLElement, MuteButton) { 16 | static tagName = 'media-mute-button'; 17 | } 18 | 19 | declare global { 20 | interface HTMLElementTagNameMap { 21 | 'media-mute-button': MediaMuteButtonElement; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/buttons/pip-button-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { PIPButton } from '../../../components/ui/buttons/pip-button'; 4 | 5 | /** 6 | * @example 7 | * ```html 8 | * <media-pip-button> 9 | * <media-icon type="picture-in-picture"></media-icon> 10 | * <media-icon type="picture-in-picture-exit"></media-icon> 11 | * </media-pip-button> 12 | * ``` 13 | */ 14 | export class MediaPIPButtonElement extends Host(HTMLElement, PIPButton) { 15 | static tagName = 'media-pip-button'; 16 | } 17 | 18 | declare global { 19 | interface HTMLElementTagNameMap { 20 | 'media-pip-button': MediaPIPButtonElement; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/buttons/play-button-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { PlayButton } from '../../../components/ui/buttons/play-button'; 4 | 5 | /** 6 | * @example 7 | * ```html 8 | * <media-play-button> 9 | * <media-icon type="play"></media-icon> 10 | * <media-icon type="pause"></media-icon> 11 | * <media-icon type="replay"></media-icon> 12 | * </media-play-button> 13 | * ``` 14 | */ 15 | export class MediaPlayButtonElement extends Host(HTMLElement, PlayButton) { 16 | static tagName = 'media-play-button'; 17 | } 18 | 19 | declare global { 20 | interface HTMLElementTagNameMap { 21 | 'media-play-button': MediaPlayButtonElement; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/buttons/seek-button-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { SeekButton } from '../../../components/ui/buttons/seek-button'; 4 | 5 | /** 6 | * @example 7 | * ```html 8 | * <!-- Forward +30s on each press. --> 9 | * <media-seek-button seconds="+30"> 10 | * <media-icon type="seek-forward"></media-icon> 11 | * </media-seek-button> 12 | * <!-- Backward -30s on each press. --> 13 | * <media-seek-button seconds="-30"> 14 | * <media-icon type="seek-backward"></media-icon> 15 | * </media-seek-button> 16 | * ``` 17 | */ 18 | export class MediaSeekButtonElement extends Host(HTMLElement, SeekButton) { 19 | static tagName = 'media-seek-button'; 20 | } 21 | 22 | declare global { 23 | interface HTMLElementTagNameMap { 24 | 'media-seek-button': MediaSeekButtonElement; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/buttons/toggle-button-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { ToggleButton } from '../../../components/ui/buttons/toggle-button'; 4 | 5 | /** 6 | * @example 7 | * ```html 8 | * <media-toggle-button aria-label="..."> 9 | * <!-- ... --> 10 | * </media-toggle-button> 11 | * ``` 12 | */ 13 | export class MediaToggleButtonElement extends Host(HTMLElement, ToggleButton) { 14 | static tagName = 'media-toggle-button'; 15 | } 16 | 17 | declare global { 18 | interface HTMLElementTagNameMap { 19 | 'media-toggle-button': MediaToggleButtonElement; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/captions-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { Captions } from '../../components/ui/captions/captions'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/display/captions} 7 | * @example 8 | * ```html 9 | * <media-captions></media-captions> 10 | * ``` 11 | */ 12 | export class MediaCaptionsElement extends Host(HTMLElement, Captions) { 13 | static tagName = 'media-captions'; 14 | } 15 | 16 | declare global { 17 | interface HTMLElementTagNameMap { 18 | 'media-captions': MediaCaptionsElement; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/controls-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { Controls } from '../../components/ui/controls'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/display/controls} 7 | * @example 8 | * ```html 9 | * <media-player> 10 | * <!-- ... --> 11 | * <media-controls> 12 | * <media-controls-group></media-controls-group> 13 | * <media-controls-group></media-controls-group> 14 | * </media-controls> 15 | * </media-player> 16 | * ``` 17 | */ 18 | export class MediaControlsElement extends Host(HTMLElement, Controls) { 19 | static tagName = 'media-controls'; 20 | } 21 | 22 | declare global { 23 | interface HTMLElementTagNameMap { 24 | 'media-controls': MediaControlsElement; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/controls-group-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { ControlsGroup } from '../../components/ui/controls-group'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/display/controls} 7 | * @example 8 | * ```html 9 | * <media-player> 10 | * <!-- ... --> 11 | * <media-controls> 12 | * <media-controls-group></media-controls-group> 13 | * <media-controls-group></media-controls-group> 14 | * </media-controls> 15 | * </media-player> 16 | * ``` 17 | */ 18 | export class MediaControlsGroupElement extends Host(HTMLElement, ControlsGroup) { 19 | static tagName = 'media-controls-group'; 20 | } 21 | 22 | declare global { 23 | interface HTMLElementTagNameMap { 24 | 'media-controls-group': MediaControlsGroupElement; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/gesture-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { Gesture } from '../../components/ui/gesture'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/display/gesture} 7 | * @example 8 | * ```html 9 | * <media-player> 10 | * <media-provider> 11 | * <media-gesture event="pointerup" action="toggle:paused"></media-gesture> 12 | * </media-provider> 13 | * </media-player> 14 | * ``` 15 | */ 16 | export class MediaGestureElement extends Host(HTMLElement, Gesture) { 17 | static tagName = 'media-gesture'; 18 | } 19 | 20 | declare global { 21 | interface HTMLElementTagNameMap { 22 | 'media-gesture': MediaGestureElement; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/default/icons-loader.ts: -------------------------------------------------------------------------------- 1 | import type { TemplateResult } from 'lit-html'; 2 | 3 | import { Icon } from '../../../icon'; 4 | import { LayoutIconsLoader } from '../icons/layout-icons-loader'; 5 | 6 | export class DefaultLayoutIconsLoader extends LayoutIconsLoader { 7 | async loadIcons() { 8 | const paths = (await import('./icons')).icons, 9 | icons: Record<string, TemplateResult> = {}; 10 | 11 | for (const iconName of Object.keys(paths)) { 12 | icons[iconName] = Icon({ name: iconName, paths: paths[iconName] }); 13 | } 14 | 15 | return icons; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/default/slots.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-html'; 2 | 3 | export function IconSlot(name: string, classes = '') { 4 | return html`<slot 5 | name=${`${name}-icon`} 6 | data-class=${`vds-icon vds-${name}-icon${classes ? ` ${classes}` : ''}`} 7 | ></slot>`; 8 | } 9 | 10 | export function IconSlots(names: string[]) { 11 | return names.map((name) => IconSlot(name)); 12 | } 13 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/default/ui/announcer.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-html'; 2 | 3 | import { useDefaultLayoutContext } from '../../../../../components/layouts/default/context'; 4 | import { $signal } from '../../../../lit/directives/signal'; 5 | 6 | export function DefaultAnnouncer() { 7 | return $signal(() => { 8 | const { translations, userPrefersAnnouncements } = useDefaultLayoutContext(); 9 | 10 | if (!userPrefersAnnouncements()) return null; 11 | 12 | return html`<media-announcer .translations=${$signal(translations)}></media-announcer>`; 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/default/ui/captions.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-html'; 2 | 3 | import { useDefaultLayoutContext } from '../../../../../components/layouts/default/context'; 4 | import { $i18n } from './utils'; 5 | 6 | export function DefaultCaptions() { 7 | const { translations } = useDefaultLayoutContext(); 8 | return html` 9 | <media-captions 10 | class="vds-captions" 11 | .exampleText=${$i18n(translations, 'Captions look like this')} 12 | ></media-captions> 13 | `; 14 | } 15 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/default/ui/controls.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-html'; 2 | 3 | export function DefaultControlsSpacer() { 4 | return html`<div class="vds-controls-spacer"></div>`; 5 | } 6 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/default/ui/time.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-html'; 2 | 3 | import { useMediaState } from '../../../../../core/api/media-context'; 4 | import { $signal } from '../../../../lit/directives/signal'; 5 | import { DefaultLiveButton } from './buttons'; 6 | 7 | export function DefaultTimeGroup() { 8 | return html` 9 | <div class="vds-time-group"> 10 | ${$signal(() => { 11 | const { duration } = useMediaState(); 12 | 13 | if (!duration()) return null; 14 | 15 | return [ 16 | html`<media-time class="vds-time" type="current"></media-time>`, 17 | html`<div class="vds-time-divider">/</div>`, 18 | html`<media-time class="vds-time" type="duration"></media-time>`, 19 | ]; 20 | })} 21 | </div> 22 | `; 23 | } 24 | 25 | export function DefaultTimeInvert() { 26 | return $signal(() => { 27 | const { live, duration } = useMediaState(); 28 | return live() 29 | ? DefaultLiveButton() 30 | : duration() 31 | ? html`<media-time class="vds-time" type="current" toggle remainder></media-time>` 32 | : null; 33 | }); 34 | } 35 | 36 | export function DefaultTimeInfo(): any { 37 | return $signal(() => { 38 | const { live } = useMediaState(); 39 | return live() ? DefaultLiveButton() : DefaultTimeGroup(); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/default/ui/title.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-html'; 2 | import { signal } from 'maverick.js'; 3 | 4 | import { useMediaContext, useMediaState } from '../../../../../core/api/media-context'; 5 | import type { TextTrack } from '../../../../../core/tracks/text/text-track'; 6 | import { watchActiveTextTrack } from '../../../../../core/tracks/text/utils'; 7 | import { $signal } from '../../../../lit/directives/signal'; 8 | 9 | export function DefaultTitle() { 10 | return $signal(() => { 11 | const { textTracks } = useMediaContext(), 12 | { title, started } = useMediaState(), 13 | $hasChapters = signal<TextTrack | null>(null); 14 | 15 | watchActiveTextTrack(textTracks, 'chapters', $hasChapters.set); 16 | 17 | return $hasChapters() && (started() || !title()) 18 | ? DefaultChapterTitle() 19 | : html`<media-title class="vds-chapter-title"></media-title>`; 20 | }); 21 | } 22 | 23 | export function DefaultChapterTitle() { 24 | return html`<media-chapter-title class="vds-chapter-title"></media-chapter-title>`; 25 | } 26 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/default/ui/utils.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultLayoutContext } from '../../../../../components/layouts/default/context'; 2 | import { 3 | i18n, 4 | type DefaultLayoutWord, 5 | } from '../../../../../components/layouts/default/translations'; 6 | import { $signal } from '../../../../lit/directives/signal'; 7 | 8 | export function $i18n(translations: DefaultLayoutContext['translations'], word: DefaultLayoutWord) { 9 | return $signal(() => i18n(translations, word)); 10 | } 11 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/icons/icons-loader.ts: -------------------------------------------------------------------------------- 1 | import { type TemplateResult } from 'lit-html'; 2 | 3 | import { SlotObserver } from '../slot-observer'; 4 | 5 | export type IconsRecord = Record<string, Element | TemplateResult>; 6 | 7 | export abstract class IconsLoader { 8 | #icons: IconsRecord = {}; 9 | #loaded = false; 10 | 11 | readonly slots: SlotObserver; 12 | 13 | constructor(roots: HTMLElement[]) { 14 | this.slots = new SlotObserver(roots, this.#insertIcons.bind(this)); 15 | } 16 | 17 | connect() { 18 | this.slots.connect(); 19 | } 20 | 21 | load() { 22 | this.loadIcons().then((icons) => { 23 | this.#icons = icons; 24 | this.#loaded = true; 25 | this.#insertIcons(); 26 | }); 27 | } 28 | 29 | abstract loadIcons(): Promise<IconsRecord>; 30 | 31 | *#iterate() { 32 | for (const iconName of Object.keys(this.#icons)) { 33 | const slotName = `${iconName}-icon`; 34 | for (const slot of this.slots.elements) { 35 | if (slot.name !== slotName) continue; 36 | yield { icon: this.#icons[iconName], slot }; 37 | } 38 | } 39 | } 40 | 41 | #insertIcons() { 42 | if (!this.#loaded) return; 43 | for (const { icon, slot } of this.#iterate()) { 44 | this.slots.assign(icon, slot); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/icons/layout-icons-loader.ts: -------------------------------------------------------------------------------- 1 | import { onDispose } from 'maverick.js'; 2 | 3 | import { useMediaContext } from '../../../../core/api/media-context'; 4 | import { IconsLoader } from './icons-loader'; 5 | 6 | export abstract class LayoutIconsLoader extends IconsLoader { 7 | override connect() { 8 | super.connect(); 9 | 10 | const { player } = useMediaContext(); 11 | if (!player.el) return; 12 | 13 | let dispose: (() => void) | undefined, 14 | observer = new IntersectionObserver((entries) => { 15 | if (!entries[0]?.isIntersecting) return; 16 | dispose?.(); 17 | dispose = undefined; 18 | this.load(); 19 | }); 20 | 21 | observer.observe(player.el); 22 | dispose = onDispose(() => observer.disconnect()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/layout-name.ts: -------------------------------------------------------------------------------- 1 | import { effect, type ReadSignal } from 'maverick.js'; 2 | import { setAttribute } from 'maverick.js/std'; 3 | 4 | import { useMediaContext } from '../../../core/api/media-context'; 5 | 6 | export function setLayoutName(name: string, isMatch: ReadSignal<boolean>) { 7 | effect(() => { 8 | const { player } = useMediaContext(), 9 | el = player.el; 10 | 11 | el && setAttribute(el, 'data-layout', isMatch() && name); 12 | 13 | return () => el?.removeAttribute('data-layout'); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons-loader.ts: -------------------------------------------------------------------------------- 1 | import type { TemplateResult } from 'lit-html'; 2 | 3 | import { Icon } from '../../../icon'; 4 | import { LayoutIconsLoader } from '../icons/layout-icons-loader'; 5 | 6 | export class PlyrLayoutIconsLoader extends LayoutIconsLoader { 7 | async loadIcons() { 8 | const paths = (await import('./icons')).icons, 9 | icons: Record<string, TemplateResult> = {}; 10 | 11 | for (const iconName of Object.keys(paths)) { 12 | icons[iconName] = Icon({ 13 | name: iconName, 14 | paths: paths[iconName], 15 | viewBox: '0 0 18 18', 16 | }); 17 | } 18 | 19 | return icons; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons.ts: -------------------------------------------------------------------------------- 1 | import airplay from './icons/plyr-airplay'; 2 | import captionsOff from './icons/plyr-captions-off'; 3 | import captionsOn from './icons/plyr-captions-on'; 4 | import download from './icons/plyr-download'; 5 | import exitFullscreen from './icons/plyr-enter-fullscreen'; 6 | import enterFullscreen from './icons/plyr-exit-fullscreen'; 7 | import fastForward from './icons/plyr-fast-forward'; 8 | import muted from './icons/plyr-muted'; 9 | import pause from './icons/plyr-pause'; 10 | import pip from './icons/plyr-pip'; 11 | import play from './icons/plyr-play'; 12 | import restart from './icons/plyr-restart'; 13 | import rewind from './icons/plyr-rewind'; 14 | import settings from './icons/plyr-settings'; 15 | import volume from './icons/plyr-volume'; 16 | 17 | export const icons = { 18 | airplay, 19 | 'captions-off': captionsOff, 20 | 'captions-on': captionsOn, 21 | download, 22 | 'enter-fullscreen': enterFullscreen, 23 | 'exit-fullscreen': exitFullscreen, 24 | 'fast-forward': fastForward, 25 | muted, 26 | pause, 27 | 'enter-pip': pip, 28 | 'exit-pip': pip, 29 | play, 30 | restart, 31 | rewind, 32 | settings, 33 | volume, 34 | }; 35 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-airplay.js: -------------------------------------------------------------------------------- 1 | export default `<g><path d="M16,1 L2,1 C1.447,1 1,1.447 1,2 L1,12 C1,12.553 1.447,13 2,13 L5,13 L5,11 L3,11 L3,3 L15,3 L15,11 L13,11 L13,13 L16,13 C16.553,13 17,12.553 17,12 L17,2 C17,1.447 16.553,1 16,1 L16,1 Z"></path><polygon points="4 17 14 17 9 11"></polygon></g>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-captions-off.js: -------------------------------------------------------------------------------- 1 | export default `<g fill-rule="evenodd" fill-opacity="0.5"><path d="M1,1 C0.4,1 0,1.4 0,2 L0,13 C0,13.6 0.4,14 1,14 L5.6,14 L8.3,16.7 C8.5,16.9 8.7,17 9,17 C9.3,17 9.5,16.9 9.7,16.7 L12.4,14 L17,14 C17.6,14 18,13.6 18,13 L18,2 C18,1.4 17.6,1 17,1 L1,1 Z M5.52,11.15 C7.51,11.15 8.53,9.83 8.8,8.74 L7.51,8.35 C7.32,9.01 6.73,9.8 5.52,9.8 C4.38,9.8 3.32,8.97 3.32,7.46 C3.32,5.85 4.44,5.09 5.5,5.09 C6.73,5.09 7.28,5.84 7.45,6.52 L8.75,6.11 C8.47,4.96 7.46,3.76 5.5,3.76 C3.6,3.76 1.89,5.2 1.89,7.46 C1.89,9.72 3.54,11.15 5.52,11.15 Z M13.09,11.15 C15.08,11.15 16.1,9.83 16.37,8.74 L15.08,8.35 C14.89,9.01 14.3,9.8 13.09,9.8 C11.95,9.8 10.89,8.97 10.89,7.46 C10.89,5.85 12.01,5.09 13.07,5.09 C14.3,5.09 14.85,5.84 15.02,6.52 L16.32,6.11 C16.04,4.96 15.03,3.76 13.07,3.76 C11.17,3.76 9.46,5.2 9.46,7.46 C9.46,9.72 11.11,11.15 13.09,11.15 Z"></path></g>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-captions-on.js: -------------------------------------------------------------------------------- 1 | export default `<g fill-rule="evenodd"><path d="M1,1 C0.4,1 0,1.4 0,2 L0,13 C0,13.6 0.4,14 1,14 L5.6,14 L8.3,16.7 C8.5,16.9 8.7,17 9,17 C9.3,17 9.5,16.9 9.7,16.7 L12.4,14 L17,14 C17.6,14 18,13.6 18,13 L18,2 C18,1.4 17.6,1 17,1 L1,1 Z M5.52,11.15 C7.51,11.15 8.53,9.83 8.8,8.74 L7.51,8.35 C7.32,9.01 6.73,9.8 5.52,9.8 C4.38,9.8 3.32,8.97 3.32,7.46 C3.32,5.85 4.44,5.09 5.5,5.09 C6.73,5.09 7.28,5.84 7.45,6.52 L8.75,6.11 C8.47,4.96 7.46,3.76 5.5,3.76 C3.6,3.76 1.89,5.2 1.89,7.46 C1.89,9.72 3.54,11.15 5.52,11.15 Z M13.09,11.15 C15.08,11.15 16.1,9.83 16.37,8.74 L15.08,8.35 C14.89,9.01 14.3,9.8 13.09,9.8 C11.95,9.8 10.89,8.97 10.89,7.46 C10.89,5.85 12.01,5.09 13.07,5.09 C14.3,5.09 14.85,5.84 15.02,6.52 L16.32,6.11 C16.04,4.96 15.03,3.76 13.07,3.76 C11.17,3.76 9.46,5.2 9.46,7.46 C9.46,9.72 11.11,11.15 13.09,11.15 Z"></path></g>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-download.js: -------------------------------------------------------------------------------- 1 | export default `<g transform="translate(2 1)"><path d="M7,12 C7.3,12 7.5,11.9 7.7,11.7 L13.4,6 L12,4.6 L8,8.6 L8,0 L6,0 L6,8.6 L2,4.6 L0.6,6 L6.3,11.7 C6.5,11.9 6.7,12 7,12 Z" /><rect width="14" height="2" y="14" /></g>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-enter-fullscreen.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="10 3 13.6 3 9.6 7 11 8.4 15 4.4 15 8 17 8 17 1 10 1"></polygon><polygon points="7 9.6 3 13.6 3 10 1 10 1 17 8 17 8 15 4.4 15 8.4 11"></polygon>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-exit-fullscreen.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="1 12 4.6 12 0.6 16 2 17.4 6 13.4 6 17 8 17 8 10 1 10"></polygon><polygon points="16 0.6 12 4.6 12 1 10 1 10 8 17 8 17 6 13.4 6 17.4 2"></polygon>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-fast-forward.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="7.875 7.17142857 0 1 0 17 7.875 10.8285714 7.875 17 18 9 7.875 1"></polygon>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-muted.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="12.4 12.5 14.5 10.4 16.6 12.5 18 11.1 15.9 9 18 6.9 16.6 5.5 14.5 7.6 12.4 5.5 11 6.9 13.1 9 11 11.1"></polygon><path d="M3.78571429,6.00820648 L0.714285714,6.00820648 C0.285714286,6.00820648 0,6.30901277 0,6.76022222 L0,11.2723167 C0,11.7235261 0.285714286,12.0243324 0.714285714,12.0243324 L3.78571429,12.0243324 L7.85714286,15.8819922 C8.35714286,16.1827985 9,15.8819922 9,15.2803796 L9,2.75215925 C9,2.15054666 8.35714286,1.77453879 7.85714286,2.15054666 L3.78571429,6.00820648 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-pause.js: -------------------------------------------------------------------------------- 1 | export default `<path d="M6,1 L3,1 C2.4,1 2,1.4 2,2 L2,16 C2,16.6 2.4,17 3,17 L6,17 C6.6,17 7,16.6 7,16 L7,2 C7,1.4 6.6,1 6,1 L6,1 Z"></path><path d="M12,1 C11.4,1 11,1.4 11,2 L11,16 C11,16.6 11.4,17 12,17 L15,17 C15.6,17 16,16.6 16,16 L16,2 C16,1.4 15.6,1 15,1 L12,1 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-pip.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="13.293 3.293 7.022 9.564 8.436 10.978 14.707 4.707 17 7 17 1 11 1"></polygon><path d="M13,15 L3,15 L3,5 L8,5 L8,3 L2,3 C1.448,3 1,3.448 1,4 L1,16 C1,16.552 1.448,17 2,17 L14,17 C14.552,17 15,16.552 15,16 L15,10 L13,10 L13,15 L13,15 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-play.js: -------------------------------------------------------------------------------- 1 | export default `<path d="M15.5615866,8.10002147 L3.87056367,0.225209313 C3.05219207,-0.33727727 2,0.225209313 2,1.12518784 L2,16.8748122 C2,17.7747907 3.05219207,18.3372773 3.87056367,17.7747907 L15.5615866,9.89997853 C16.1461378,9.44998927 16.1461378,8.55001073 15.5615866,8.10002147 L15.5615866,8.10002147 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-restart.js: -------------------------------------------------------------------------------- 1 | export default `<path d="M9.7,1.2 L10.4,7.6 L12.5,5.5 C14.4,7.4 14.4,10.6 12.5,12.5 C11.6,13.5 10.3,14 9,14 C7.7,14 6.4,13.5 5.5,12.5 C3.6,10.6 3.6,7.4 5.5,5.5 C6.1,4.9 6.9,4.4 7.8,4.2 L7.2,2.3 C6,2.6 4.9,3.2 4,4.1 C1.3,6.8 1.3,11.2 4,14 C5.3,15.3 7.1,16 8.9,16 C10.8,16 12.5,15.3 13.8,14 C16.5,11.3 16.5,6.9 13.8,4.1 L16,1.9 L9.7,1.2 L9.7,1.2 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-rewind.js: -------------------------------------------------------------------------------- 1 | export default `<polygon points="10.125 1 0 9 10.125 17 10.125 10.8285714 18 17 18 1 10.125 7.17142857"></polygon>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-settings.js: -------------------------------------------------------------------------------- 1 | export default `<path d="M16.135,7.784 C14.832,7.458 14.214,5.966 14.905,4.815 C15.227,4.279 15.13,3.817 14.811,3.499 L14.501,3.189 C14.183,2.871 13.721,2.774 13.185,3.095 C12.033,3.786 10.541,3.168 10.216,1.865 C10.065,1.258 9.669,1 9.219,1 L8.781,1 C8.331,1 7.936,1.258 7.784,1.865 C7.458,3.168 5.966,3.786 4.815,3.095 C4.279,2.773 3.816,2.87 3.498,3.188 L3.188,3.498 C2.87,3.816 2.773,4.279 3.095,4.815 C3.786,5.967 3.168,7.459 1.865,7.784 C1.26,7.935 1,8.33 1,8.781 L1,9.219 C1,9.669 1.258,10.064 1.865,10.216 C3.168,10.542 3.786,12.034 3.095,13.185 C2.773,13.721 2.87,14.183 3.189,14.501 L3.499,14.811 C3.818,15.13 4.281,15.226 4.815,14.905 C5.967,14.214 7.459,14.832 7.784,16.135 C7.935,16.742 8.331,17 8.781,17 L9.219,17 C9.669,17 10.064,16.742 10.216,16.135 C10.542,14.832 12.034,14.214 13.185,14.905 C13.72,15.226 14.182,15.13 14.501,14.811 L14.811,14.501 C15.129,14.183 15.226,13.72 14.905,13.185 C14.214,12.033 14.832,10.541 16.135,10.216 C16.742,10.065 17,9.669 17,9.219 L17,8.781 C17,8.33 16.74,7.935 16.135,7.784 L16.135,7.784 Z M9,12 C7.343,12 6,10.657 6,9 C6,7.343 7.343,6 9,6 C10.657,6 12,7.343 12,9 C12,10.657 10.657,12 9,12 L9,12 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/layouts/plyr/icons/plyr-volume.js: -------------------------------------------------------------------------------- 1 | export default `<path d="M15.5999996,3.3 C15.1999996,2.9 14.5999996,2.9 14.1999996,3.3 C13.7999996,3.7 13.7999996,4.3 14.1999996,4.7 C15.3999996,5.9 15.9999996,7.4 15.9999996,9 C15.9999996,10.6 15.3999996,12.1 14.1999996,13.3 C13.7999996,13.7 13.7999996,14.3 14.1999996,14.7 C14.3999996,14.9 14.6999996,15 14.8999996,15 C15.1999996,15 15.3999996,14.9 15.5999996,14.7 C17.0999996,13.2 17.9999996,11.2 17.9999996,9 C17.9999996,6.8 17.0999996,4.8 15.5999996,3.3 L15.5999996,3.3 Z"></path><path d="M11.2819745,5.28197449 C10.9060085,5.65794047 10.9060085,6.22188944 11.2819745,6.59785542 C12.0171538,7.33303477 12.2772954,8.05605449 12.2772954,9.00000021 C12.2772954,9.93588462 11.851678,10.9172014 11.2819745,11.4869049 C10.9060085,11.8628709 10.9060085,12.4268199 11.2819745,12.8027859 C11.4271642,12.9479755 11.9176724,13.0649528 12.2998149,12.9592565 C12.4124479,12.9281035 12.5156669,12.8776063 12.5978555,12.8027859 C13.773371,11.732654 14.1311161,10.1597914 14.1312523,9.00000021 C14.1312723,8.8299555 14.1286311,8.66015647 14.119665,8.4897429 C14.0674781,7.49784946 13.8010171,6.48513613 12.5978554,5.28197449 C12.2218894,4.9060085 11.6579405,4.9060085 11.2819745,5.28197449 Z"></path><path d="M3.78571429,6.00820648 L0.714285714,6.00820648 C0.285714286,6.00820648 0,6.30901277 0,6.76022222 L0,11.2723167 C0,11.7235261 0.285714286,12.0243324 0.714285714,12.0243324 L3.78571429,12.0243324 L7.85714286,15.8819922 C8.35714286,16.1827985 9,15.8819922 9,15.2803796 L9,2.75215925 C9,2.15054666 8.35714286,1.77453879 7.85714286,2.15054666 L3.78571429,6.00820648 Z"></path>`; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/menus/audio-gain-group-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { AudioGainRadioGroup } from '../../../components/ui/menu/radio-groups/audio-gain-radio-group'; 4 | import { renderMenuItemsTemplate } from './_template'; 5 | 6 | /** 7 | * @part label - Contains the audio gain option label. 8 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/menu/audio-gain-group} 9 | * @example 10 | * ```html 11 | * <media-menu> 12 | * <!-- ... --> 13 | * <media-menu-items> 14 | * <media-audio-gain-radio-group> 15 | * <template> 16 | * <media-radio> 17 | * <span data-part="label"></span> 18 | * </media-radio> 19 | * </template> 20 | * </media-audio-gain-radio-group> 21 | * </media-menu-items> 22 | * </media-menu> 23 | * ``` 24 | */ 25 | export class MediaAudioGainRadioGroupElement extends Host(HTMLElement, AudioGainRadioGroup) { 26 | static tagName = 'media-audio-gain-radio-group'; 27 | 28 | protected onConnect(): void { 29 | renderMenuItemsTemplate(this); 30 | } 31 | } 32 | 33 | declare global { 34 | interface HTMLElementTagNameMap { 35 | 'media-audio-gain-radio-group': MediaAudioGainRadioGroupElement; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/menus/audio-radio-group-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { AudioRadioGroup } from '../../../components/ui/menu/radio-groups/audio-radio-group'; 4 | import { renderMenuItemsTemplate } from './_template'; 5 | 6 | /** 7 | * @part label - Contains the audio track option label. 8 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/menu/audio-radio-group} 9 | * @example 10 | * ```html 11 | * <media-menu> 12 | * <!-- ... --> 13 | * <media-menu-items> 14 | * <media-audio-radio-group> 15 | * <template> 16 | * <media-radio> 17 | * <span data-part="label"></span> 18 | * </media-radio> 19 | * </template> 20 | * </media-audio-radio-group> 21 | * </media-menu-items> 22 | * </media-menu> 23 | * ``` 24 | */ 25 | export class MediaAudioRadioGroupElement extends Host(HTMLElement, AudioRadioGroup) { 26 | static tagName = 'media-audio-radio-group'; 27 | 28 | protected onConnect(): void { 29 | renderMenuItemsTemplate(this); 30 | } 31 | } 32 | 33 | declare global { 34 | interface HTMLElementTagNameMap { 35 | 'media-audio-radio-group': MediaAudioRadioGroupElement; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/menus/captions-radio-group-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { CaptionsRadioGroup } from '../../../components/ui/menu/radio-groups/captions-radio-group'; 4 | import { renderMenuItemsTemplate } from './_template'; 5 | 6 | /** 7 | * @part label - Contains the caption/subtitle option label. 8 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/menu/captions-radio-group} 9 | * @example 10 | * ```html 11 | * <media-menu> 12 | * <!-- ... --> 13 | * <media-menu-items> 14 | * <media-captions-radio-group> 15 | * <template> 16 | * <media-radio> 17 | * <span data-part="label"></span> 18 | * </media-radio> 19 | * </template> 20 | * </media-captions-radio-group> 21 | * </media-menu-items> 22 | * </media-menu> 23 | * ``` 24 | */ 25 | export class MediaCaptionsRadioGroupElement extends Host(HTMLElement, CaptionsRadioGroup) { 26 | static tagName = 'media-captions-radio-group'; 27 | 28 | protected onConnect(): void { 29 | renderMenuItemsTemplate(this); 30 | } 31 | } 32 | 33 | declare global { 34 | interface HTMLElementTagNameMap { 35 | 'media-captions-radio-group': MediaCaptionsRadioGroupElement; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/menus/menu-button-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { MenuButton } from '../../../components/ui/menu/menu-button'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/menu/menu} 7 | * @example 8 | * ```html 9 | * <media-menu> 10 | * <media-menu-button aria-label="Settings"> 11 | * <media-icon type="settings"></media-icon> 12 | * </media-menu-button> 13 | * <media-menu-items> 14 | * <!-- ... --> 15 | * </media-menu-items> 16 | * </media-menu> 17 | * ``` 18 | */ 19 | export class MediaMenuButtonElement extends Host(HTMLElement, MenuButton) { 20 | static tagName = 'media-menu-button'; 21 | } 22 | 23 | declare global { 24 | interface HTMLElementTagNameMap { 25 | 'media-menu-button': MediaMenuButtonElement; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/menus/menu-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { Menu } from '../../../components/ui/menu/menu'; 4 | 5 | /** 6 | * @part close-target - Closes menu when pressed. 7 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/menu/menu} 8 | * @example 9 | * ```html 10 | * <media-menu> 11 | * <media-menu-button aria-label="Settings"> 12 | * <media-icon type="settings"></media-icon> 13 | * </media-menu-button> 14 | * <media-menu-items> 15 | * <!-- ... --> 16 | * </media-menu-items> 17 | * </media-menu> 18 | * ``` 19 | */ 20 | export class MediaMenuElement extends Host(HTMLElement, Menu) { 21 | static tagName = 'media-menu'; 22 | } 23 | 24 | declare global { 25 | interface HTMLElementTagNameMap { 26 | 'media-menu': MediaMenuElement; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/menus/menu-item-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { MenuItem } from '../../../components/ui/menu/menu-item'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/menu/menu} 7 | * @example 8 | * ```html 9 | * <media-menu> 10 | * <media-menu-items> 11 | * <media-menu-item></media-menu-item> 12 | * </media-menu-items> 13 | * </media-menu> 14 | * ``` 15 | */ 16 | export class MediaMenuItemElement extends Host(HTMLElement, MenuItem) { 17 | static tagName = 'media-menu-item'; 18 | } 19 | 20 | declare global { 21 | interface HTMLElementTagNameMap { 22 | 'media-menu-item': MediaMenuItemElement; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/menus/menu-items-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { MenuItems } from '../../../components/ui/menu/menu-items'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/menu/menu} 7 | * @example 8 | * ```html 9 | * <media-menu> 10 | * <media-menu-button aria-label="Settings"> 11 | * <media-icon type="settings"></media-icon> 12 | * </media-menu-button> 13 | * <media-menu-items> 14 | * <!-- ... --> 15 | * </media-menu-items> 16 | * </media-menu> 17 | * ``` 18 | */ 19 | export class MediaMenuItemsElement extends Host(HTMLElement, MenuItems) { 20 | static tagName = 'media-menu-items'; 21 | } 22 | 23 | declare global { 24 | interface HTMLElementTagNameMap { 25 | 'media-menu-items': MediaMenuItemsElement; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/menus/menu-portal-element.ts: -------------------------------------------------------------------------------- 1 | import { Host, type Attributes } from 'maverick.js/element'; 2 | import { isString } from 'maverick.js/std'; 3 | 4 | import { MenuPortal, type MenuPortalProps } from '../../../components/ui/menu/menu-portal'; 5 | 6 | /** 7 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/menu/menu#portal} 8 | * @example 9 | * ```html 10 | * <media-menu> 11 | * <!-- ... --> 12 | * <media-menu-portal> 13 | * <media-menu-items></media-menu-items> 14 | * </media-menu-portal> 15 | * </media-menu> 16 | * ``` 17 | */ 18 | export class MediaMenuPortalElement extends Host(HTMLElement, MenuPortal) { 19 | static tagName = 'media-menu-portal'; 20 | 21 | static override attrs: Attributes<MenuPortalProps> = { 22 | disabled: { 23 | converter(value) { 24 | if (isString(value)) return value as 'fullscreen'; 25 | return value !== null; 26 | }, 27 | }, 28 | }; 29 | } 30 | 31 | declare global { 32 | interface HTMLElementTagNameMap { 33 | 'media-menu-portal': MediaMenuPortalElement; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/menus/radio-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { Radio } from '../../../components/ui/menu/radio/radio'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/menu/radio} 7 | * @example 8 | * ```html 9 | * <media-radio-group value="720"> 10 | * <media-radio value="1080">1080p</media-radio> 11 | * <media-radio value="720">720p</media-radio> 12 | * <!-- ... --> 13 | * </media-radio-group> 14 | * ``` 15 | */ 16 | export class MediaRadioElement extends Host(HTMLElement, Radio) { 17 | static tagName = 'media-radio'; 18 | } 19 | 20 | declare global { 21 | interface HTMLElementTagNameMap { 22 | 'media-radio': MediaRadioElement; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/menus/radio-group-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { RadioGroup } from '../../../components/ui/menu/radio/radio-group'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/menu/radio-group} 7 | * @example 8 | * ```html 9 | * <media-radio-group value="720"> 10 | * <media-radio value="1080">1080p</media-radio> 11 | * <media-radio value="720">720p</media-radio> 12 | * <!-- ... --> 13 | * </media-radio-group> 14 | * ``` 15 | */ 16 | export class MediaRadioGroupElement extends Host(HTMLElement, RadioGroup) { 17 | static tagName = 'media-radio-group'; 18 | } 19 | 20 | declare global { 21 | interface HTMLElementTagNameMap { 22 | 'media-radio-group': MediaRadioGroupElement; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/menus/speed-radio-group-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { SpeedRadioGroup } from '../../../components/ui/menu/radio-groups/speed-radio-group'; 4 | import { renderMenuItemsTemplate } from './_template'; 5 | 6 | /** 7 | * @part label - Contains the speed option label. 8 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/menu/speed-radio-group} 9 | * @example 10 | * ```html 11 | * <media-menu> 12 | * <!-- ... --> 13 | * <media-menu-items> 14 | * <media-speed-radio-group> 15 | * <template> 16 | * <media-radio> 17 | * <span data-part="label"></span> 18 | * </media-radio> 19 | * </template> 20 | * </media-speed-radio-group> 21 | * </media-menu-items> 22 | * </media-menu> 23 | * ``` 24 | */ 25 | export class MediaSpeedRadioGroupElement extends Host(HTMLElement, SpeedRadioGroup) { 26 | static tagName = 'media-speed-radio-group'; 27 | 28 | protected onConnect(): void { 29 | renderMenuItemsTemplate(this); 30 | } 31 | } 32 | 33 | declare global { 34 | interface HTMLElementTagNameMap { 35 | 'media-speed-radio-group': MediaSpeedRadioGroupElement; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/player-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | import type { Attributes } from 'maverick.js/element'; 3 | 4 | import { MediaPlayer } from '../../components/player'; 5 | import type { MediaPlayerProps } from '../../core/api/player-props'; 6 | 7 | /** 8 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/core/player} 9 | * @example 10 | * ```html 11 | * <media-player src="..."> 12 | * <media-provider></media-provider> 13 | * <!-- Other components that use/manage media state here. --> 14 | * </media-player> 15 | * ``` 16 | */ 17 | export class MediaPlayerElement extends Host(HTMLElement, MediaPlayer) { 18 | static tagName = 'media-player'; 19 | 20 | static override attrs: Attributes<MediaPlayerProps> = { 21 | autoPlay: 'autoplay', 22 | crossOrigin: 'crossorigin', 23 | playsInline: 'playsinline', 24 | preferNativeHLS: 'prefer-native-hls', 25 | minLiveDVRWindow: 'min-live-dvr-window', 26 | }; 27 | } 28 | 29 | declare global { 30 | interface HTMLElementTagNameMap { 31 | 'media-player': MediaPlayerElement; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/provider-cast-display.ts: -------------------------------------------------------------------------------- 1 | import { effect } from 'maverick.js'; 2 | import chromecastIconPaths from 'media-icons/dist/icons/chromecast.js'; 3 | 4 | import type { MediaStore } from '../../core/api/player-state'; 5 | import { cloneTemplateContent, createTemplate } from '../../utils/dom'; 6 | 7 | const svgTemplate = /* #__PURE__*/ createTemplate( 8 | `<svg viewBox="0 0 32 32" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"></svg>`, 9 | ); 10 | 11 | export function insertContent(container: HTMLElement, $state: MediaStore) { 12 | const icon = cloneTemplateContent<SVGElement>(svgTemplate); 13 | icon.innerHTML = chromecastIconPaths; 14 | container.append(icon); 15 | 16 | const text = document.createElement('span'); 17 | text.classList.add('vds-google-cast-info'); 18 | container.append(text); 19 | 20 | const deviceName = document.createElement('span'); 21 | deviceName.classList.add('vds-google-cast-device-name'); 22 | 23 | effect(() => { 24 | const { remotePlaybackInfo } = $state, 25 | info = remotePlaybackInfo(); 26 | 27 | if (info?.deviceName) { 28 | deviceName.textContent = info.deviceName; 29 | text.append('Google Cast on ', deviceName); 30 | } 31 | 32 | return () => { 33 | text.textContent = ''; 34 | }; 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/sliders/audio-gain-slider-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { AudioGainSlider } from '../../../components/ui/sliders/audio-gain-slider'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/sliders/audio-gain-slider} 7 | * @example 8 | * ```html 9 | * <media-audio-gain-slider> 10 | * <div class="track"></div> 11 | * <div class="track-fill"></div> 12 | * <div class="track-progress"></div> 13 | * <div class="thumb"></div> 14 | * </media-audio-gain-slider> 15 | * ``` 16 | * @example 17 | * ```html 18 | * <media-audio-gain-slider> 19 | * <!-- ... --> 20 | * <media-slider-preview> 21 | * <media-slider-value></media-slider-value> 22 | * </media-slider-preview> 23 | * </media-audio-gain-slider> 24 | * ``` 25 | */ 26 | export class MediaAudioGainSliderElement extends Host(HTMLElement, AudioGainSlider) { 27 | static tagName = 'media-audio-gain-slider'; 28 | } 29 | 30 | declare global { 31 | interface HTMLElementTagNameMap { 32 | 'media-audio-gain-slider': MediaAudioGainSliderElement; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/sliders/quality-slider-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { QualitySlider } from '../../../components/ui/sliders/quality-slider'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/sliders/quality-slider} 7 | * @example 8 | * ```html 9 | * <media-quality-slider> 10 | * <div class="track"></div> 11 | * <div class="track-fill"></div> 12 | * <div class="track-progress"></div> 13 | * <div class="thumb"></div> 14 | * </media-quality-slider> 15 | * ``` 16 | * @example 17 | * ```html 18 | * <media-quality-slider> 19 | * <!-- ... --> 20 | * <media-slider-preview> 21 | * <media-slider-value></media-slider-value> 22 | * </media-slider-preview> 23 | * </media-quality-slider> 24 | * ``` 25 | */ 26 | export class MediaQualitySliderElement extends Host(HTMLElement, QualitySlider) { 27 | static tagName = 'media-quality-slider'; 28 | } 29 | 30 | declare global { 31 | interface HTMLElementTagNameMap { 32 | 'media-quality-slider': MediaQualitySliderElement; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/sliders/slider-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { Slider } from '../../../components/ui/sliders/slider/slider'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/sliders/slider} 7 | * @example 8 | * ```html 9 | * <media-slider min="0" max="100" value="50" aria-label="..."> 10 | * <div class="track"></div> 11 | * <div class="track-fill"></div> 12 | * <div class="track-progress"></div> 13 | * <div class="thumb"></div> 14 | * </media-slider> 15 | * ``` 16 | */ 17 | export class MediaSliderElement extends Host(HTMLElement, Slider) { 18 | static tagName = 'media-slider'; 19 | } 20 | 21 | declare global { 22 | interface HTMLElementTagNameMap { 23 | 'media-slider': MediaSliderElement; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/sliders/slider-preview-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { SliderPreview } from '../../../components/ui/sliders/slider-preview'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/sliders/slider#preview} 7 | */ 8 | export class MediaSliderPreviewElement extends Host(HTMLElement, SliderPreview) { 9 | static tagName = 'media-slider-preview'; 10 | } 11 | 12 | declare global { 13 | interface HTMLElementTagNameMap { 14 | 'media-slider-preview': MediaSliderPreviewElement; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/sliders/slider-value-element.ts: -------------------------------------------------------------------------------- 1 | import { effect } from 'maverick.js'; 2 | import { BOOLEAN, Host, type Attributes } from 'maverick.js/element'; 3 | 4 | import { SliderValue, type SliderValueProps } from '../../../components/ui/sliders/slider-value'; 5 | 6 | /** 7 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/sliders/slider-value} 8 | * @example 9 | * ```html 10 | * <media-time-slider> 11 | * <media-slider-preview> 12 | * <media-slider-value></media-slider-value> 13 | * </media-slider-preview> 14 | * </media-time-slider> 15 | * ``` 16 | * @example 17 | * ```html 18 | * <media-slider-value type="current"></media-slider-value> 19 | * ``` 20 | * @example 21 | * ```html 22 | * <media-slider-value show-hours pad-hours></media-slider-value> 23 | * ``` 24 | * @example 25 | * ```html 26 | * <media-slider-value decimal-places="2"></media-slider-value> 27 | * ``` 28 | */ 29 | export class MediaSliderValueElement extends Host(HTMLElement, SliderValue) { 30 | static tagName = 'media-slider-value'; 31 | 32 | static override attrs: Attributes<SliderValueProps> = { 33 | padMinutes: { 34 | converter: BOOLEAN, 35 | }, 36 | }; 37 | 38 | protected onConnect() { 39 | effect(() => { 40 | this.textContent = this.getValueText(); 41 | }); 42 | } 43 | } 44 | 45 | declare global { 46 | interface HTMLElementTagNameMap { 47 | 'media-slider-value': MediaSliderValueElement; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/sliders/speed-slider-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { SpeedSlider } from '../../../components/ui/sliders/speed-slider'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/sliders/speed-slider} 7 | * @example 8 | * ```html 9 | * <media-speed-slider> 10 | * <div class="track"></div> 11 | * <div class="track-fill"></div> 12 | * <div class="track-progress"></div> 13 | * <div class="thumb"></div> 14 | * </media-speed-slider> 15 | * ``` 16 | * @example 17 | * ```html 18 | * <media-speed-slider> 19 | * <!-- ... --> 20 | * <media-slider-preview> 21 | * <media-slider-value></media-slider-value> 22 | * </media-slider-preview> 23 | * </media-speed-slider> 24 | * ``` 25 | */ 26 | export class MediaSpeedSliderElement extends Host(HTMLElement, SpeedSlider) { 27 | static tagName = 'media-speed-slider'; 28 | } 29 | 30 | declare global { 31 | interface HTMLElementTagNameMap { 32 | 'media-speed-slider': MediaSpeedSliderElement; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/sliders/time-slider-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { TimeSlider } from '../../../components/ui/sliders/time-slider/time-slider'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/sliders/time-slider} 7 | * @example 8 | * ```html 9 | * <media-time-slider> 10 | * <div class="track"></div> 11 | * <div class="track-fill"></div> 12 | * <div class="track-progress"></div> 13 | * <div class="thumb"></div> 14 | * </media-time-slider> 15 | * ``` 16 | * @example 17 | * ```html 18 | * <media-time-slider> 19 | * <!-- ... --> 20 | * <media-slider-preview> 21 | * <media-slider-value></media-slider-value> 22 | * <media-slider-preview> 23 | * </media-time-slider> 24 | * ``` 25 | */ 26 | export class MediaTimeSliderElement extends Host(HTMLElement, TimeSlider) { 27 | static tagName = 'media-time-slider'; 28 | } 29 | 30 | declare global { 31 | interface HTMLElementTagNameMap { 32 | 'media-time-slider': MediaTimeSliderElement; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/sliders/volume-slider-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { VolumeSlider } from '../../../components/ui/sliders/volume-slider'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/sliders/volume-slider} 7 | * @example 8 | * ```html 9 | * <media-volume-slider> 10 | * <div class="track"></div> 11 | * <div class="track-fill"></div> 12 | * <div class="track-progress"></div> 13 | * <div class="thumb"></div> 14 | * </media-volume-slider> 15 | * ``` 16 | * @example 17 | * ```html 18 | * <media-volume-slider> 19 | * <!-- ... --> 20 | * <media-slider-preview> 21 | * <media-slider-value></media-slider-value> 22 | * </media-slider-preview> 23 | * </media-volume-slider> 24 | * ``` 25 | */ 26 | export class MediaVolumeSliderElement extends Host(HTMLElement, VolumeSlider) { 27 | static tagName = 'media-volume-slider'; 28 | } 29 | 30 | declare global { 31 | interface HTMLElementTagNameMap { 32 | 'media-volume-slider': MediaVolumeSliderElement; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/time-element.ts: -------------------------------------------------------------------------------- 1 | import { effect } from 'maverick.js'; 2 | import { Host } from 'maverick.js/element'; 3 | 4 | import { Time } from '../../components/ui/time'; 5 | 6 | /** 7 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/display/time} 8 | * @example 9 | * ```html 10 | * <media-time type="current"></media-time> 11 | * ``` 12 | * @example 13 | * ```html 14 | * <!-- Remaining time. --> 15 | * <media-time type="current" remainder></media-time> 16 | * ``` 17 | */ 18 | export class MediaTimeElement extends Host(HTMLElement, Time) { 19 | static tagName = 'media-time'; 20 | 21 | protected onConnect() { 22 | effect(() => { 23 | this.textContent = this.$state.timeText(); 24 | }); 25 | } 26 | } 27 | 28 | declare global { 29 | interface HTMLElementTagNameMap { 30 | 'media-time': MediaTimeElement; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/title-element.ts: -------------------------------------------------------------------------------- 1 | import { Component, effect } from 'maverick.js'; 2 | import { Host } from 'maverick.js/element'; 3 | 4 | import { useMediaContext, type MediaContext } from '../../core/api/media-context'; 5 | 6 | class Title extends Component {} 7 | 8 | /** 9 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/display/title} 10 | * @example 11 | * ```html 12 | * <media-title></media-title> 13 | * ``` 14 | */ 15 | export class MediaTitleElement extends Host(HTMLElement, Title) { 16 | static tagName = 'media-title'; 17 | 18 | #media!: MediaContext; 19 | 20 | protected onSetup() { 21 | this.#media = useMediaContext(); 22 | } 23 | 24 | protected onConnect() { 25 | effect(this.#watchTitle.bind(this)); 26 | } 27 | 28 | #watchTitle() { 29 | const { title } = this.#media.$state; 30 | this.textContent = title(); 31 | } 32 | } 33 | 34 | declare global { 35 | interface HTMLElementTagNameMap { 36 | 'media-title': MediaTitleElement; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/tooltips/tooltip-content-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { TooltipContent } from '../../../components/ui/tooltip/tooltip-content'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/tooltip} 7 | * @example 8 | * ```html 9 | * <media-tooltip> 10 | * <media-tooltip-trigger> 11 | * <media-play-button></media-play-button> 12 | * </media-tooltip-trigger> 13 | * <media-tooltip-content placement="top"> 14 | * <span class="play-tooltip-text">Play</span> 15 | * <span class="pause-tooltip-text">Pause</span> 16 | * </media-tooltip-content> 17 | * </media-tooltip> 18 | * ``` 19 | */ 20 | export class MediaTooltipContentElement extends Host(HTMLElement, TooltipContent) { 21 | static tagName = 'media-tooltip-content'; 22 | } 23 | 24 | declare global { 25 | interface HTMLElementTagNameMap { 26 | 'media-tooltip-content': MediaTooltipContentElement; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/tooltips/tooltip-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { Tooltip } from '../../../components/ui/tooltip/tooltip'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/tooltip} 7 | * @example 8 | * ```html 9 | * <media-tooltip> 10 | * <media-tooltip-trigger> 11 | * <media-play-button></media-play-button> 12 | * </media-tooltip-trigger> 13 | * <media-tooltip-content placement="top start"> 14 | * <span class="play-tooltip-text">Play</span> 15 | * <span class="pause-tooltip-text">Pause</span> 16 | * </media-tooltip-content> 17 | * </media-tooltip> 18 | * ``` 19 | */ 20 | export class MediaTooltipElement extends Host(HTMLElement, Tooltip) { 21 | static tagName = 'media-tooltip'; 22 | } 23 | 24 | declare global { 25 | interface HTMLElementTagNameMap { 26 | 'media-tooltip': MediaTooltipElement; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/define/tooltips/tooltip-trigger-element.ts: -------------------------------------------------------------------------------- 1 | import { Host } from 'maverick.js/element'; 2 | 3 | import { TooltipTrigger } from '../../../components/ui/tooltip/tooltip-trigger'; 4 | 5 | /** 6 | * @docs {@link https://www.vidstack.io/docs/wc/player/components/tooltip} 7 | * @example 8 | * ```html 9 | * <media-tooltip> 10 | * <media-tooltip-trigger> 11 | * <media-play-button></media-play-button> 12 | * </media-tooltip-trigger> 13 | * <media-tooltip-content placement="top start"> 14 | * <span class="play-tooltip-text">Play</span> 15 | * <span class="pause-tooltip-text">Pause</span> 16 | * </media-tooltip-content> 17 | * </media-tooltip> 18 | * ``` 19 | */ 20 | export class MediaTooltipTriggerElement extends Host(HTMLElement, TooltipTrigger) { 21 | static tagName = 'media-tooltip-trigger'; 22 | 23 | onConnect() { 24 | this.style.display = 'contents'; 25 | } 26 | } 27 | 28 | declare global { 29 | interface HTMLElementTagNameMap { 30 | 'media-tooltip-trigger': MediaTooltipTriggerElement; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/icon.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-html'; 2 | import type { DirectiveResult } from 'lit-html/directive.js'; 3 | import { ifDefined } from 'lit-html/directives/if-defined.js'; 4 | import { unsafeSVG } from 'lit-html/directives/unsafe-svg.js'; 5 | import { type ReadSignal } from 'maverick.js'; 6 | import { isString } from 'maverick.js/std'; 7 | 8 | import { $signal } from './lit/directives/signal'; 9 | 10 | export function Icon({ name, class: _class, state, paths, viewBox = '0 0 32 32' }: IconProps) { 11 | return html`<svg 12 | class="${'vds-icon' + (_class ? ` ${_class}` : '')}" 13 | viewBox="${viewBox}" 14 | fill="none" 15 | aria-hidden="true" 16 | focusable="false" 17 | xmlns="http://www.w3.org/2000/svg" 18 | data-icon=${ifDefined(name ?? state)} 19 | > 20 | ${!isString(paths) ? $signal(paths) : unsafeSVG(paths)} 21 | </svg>`; 22 | } 23 | 24 | export interface IconProps { 25 | name?: string; 26 | class?: string; 27 | state?: string; 28 | viewBox?: string; 29 | paths: string | ReadSignal<string> | ReadSignal<DirectiveResult>; 30 | } 31 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/lit/html.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-html'; 2 | import { isReadSignal } from 'maverick.js'; 3 | 4 | import { $signal } from './directives/signal'; 5 | 6 | /** 7 | * Extends `lit-html` by setting all signal values as a `SignalDirective`. 8 | */ 9 | export const $html = (strings: TemplateStringsArray, ...values: any[]) => { 10 | return html(strings, ...values.map((val) => (isReadSignal(val) ? $signal(val) : val))); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/lit/lit-element.ts: -------------------------------------------------------------------------------- 1 | import { render } from 'lit-html'; 2 | 3 | export class LitElement extends HTMLElement { 4 | rootPart: any = null; 5 | 6 | connectedCallback() { 7 | this.rootPart = render((this as unknown as LitRenderer).render(), this, { 8 | renderBefore: this.firstChild, 9 | }); 10 | 11 | this.rootPart.setConnected(true); 12 | } 13 | 14 | disconnectedCallback() { 15 | this.rootPart?.setConnected(false); 16 | this.rootPart = null; 17 | render(null, this); 18 | } 19 | } 20 | 21 | export interface LitRenderer { 22 | render(): any; 23 | } 24 | -------------------------------------------------------------------------------- /packages/vidstack/src/elements/state-controller.ts: -------------------------------------------------------------------------------- 1 | import { effect, onDispose, type ReadSignal } from 'maverick.js'; 2 | 3 | import { requestScopedAnimationFrame } from '../utils/dom'; 4 | 5 | export class StateController { 6 | #el: HTMLElement | null; 7 | #states: ReadSignal<Record<string, boolean>>; 8 | 9 | constructor(el: HTMLElement | null, states: ReadSignal<Record<string, boolean>>) { 10 | this.#el = el; 11 | this.#states = states; 12 | 13 | if (this.#el) this.#observe(this.#el); 14 | 15 | onDispose(() => { 16 | this.#el = null; 17 | }); 18 | 19 | requestScopedAnimationFrame(() => { 20 | const tooltip = this.#getTooltip(); 21 | if (tooltip) this.#observe(tooltip); 22 | }); 23 | } 24 | 25 | #getTooltip() { 26 | if (!this.#el) return; 27 | 28 | const describedBy = 29 | this.#el.getAttribute('aria-describedby') ?? this.#el.getAttribute('data-describedby'); 30 | 31 | return describedBy && document.getElementById(describedBy); 32 | } 33 | 34 | #observe(root: HTMLElement) { 35 | effect(this.#update.bind(this, root)); 36 | } 37 | 38 | #update(root: HTMLElement) { 39 | const states = this.#states(); 40 | for (const state of Object.keys(states)) { 41 | const el = root.querySelector<HTMLElement>(`[data-state="${state}"]`); 42 | if (el) { 43 | const display = el.localName === 'slot' ? 'contents' : 'inline-block'; 44 | el.style.display = !states[state] ? 'none' : display; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/vidstack/src/exports/font.ts: -------------------------------------------------------------------------------- 1 | // Font 2 | export * from '../core/font/font-options'; 3 | export { updateFontCssVars } from '../core/font/font-vars'; 4 | -------------------------------------------------------------------------------- /packages/vidstack/src/exports/foundation.ts: -------------------------------------------------------------------------------- 1 | export { Logger } from '../foundation/logger/controller'; 2 | export * from '../foundation/list/list'; 3 | export * from '../foundation/fullscreen/controller'; 4 | export * from '../foundation/fullscreen/events'; 5 | export * from '../foundation/logger/events'; 6 | export * from '../foundation/orientation/controller'; 7 | export * from '../foundation/orientation/events'; 8 | export * from '../foundation/orientation/types'; 9 | -------------------------------------------------------------------------------- /packages/vidstack/src/exports/maverick.ts: -------------------------------------------------------------------------------- 1 | export { 2 | hasTriggerEvent, 3 | walkTriggerEventChain, 4 | findTriggerEvent, 5 | appendTriggerEvent, 6 | isPointerEvent, 7 | isKeyboardClick, 8 | isKeyboardEvent, 9 | } from 'maverick.js/std'; 10 | -------------------------------------------------------------------------------- /packages/vidstack/src/exports/utils.ts: -------------------------------------------------------------------------------- 1 | // Network 2 | export { getDownloadFile, type FileDownloadInfo } from '../utils/network'; 3 | 4 | // Time 5 | export { formatTime, formatSpokenTime } from '../utils/time'; 6 | 7 | // MIME 8 | export * from '../utils/mime'; 9 | 10 | // Support 11 | export { 12 | canChangeVolume, 13 | canOrientScreen, 14 | canPlayHLSNatively, 15 | canUsePictureInPicture, 16 | canUseVideoPresentation, 17 | canRotateScreen, 18 | } from '../utils/support'; 19 | -------------------------------------------------------------------------------- /packages/vidstack/src/foundation/fullscreen/events.ts: -------------------------------------------------------------------------------- 1 | import type { DOMEvent } from 'maverick.js/std'; 2 | 3 | export interface FullscreenEvents { 4 | 'fullscreen-change': FullscreenChangeEvent; 5 | 'fullscreen-error': FullscreenErrorEvent; 6 | } 7 | 8 | /** 9 | * Fired when an element enters/exits fullscreen. The event detail is a `boolean` indicating 10 | * if fullscreen was entered (`true`) or exited (`false`). 11 | * 12 | * @bubbles 13 | * @composed 14 | * @detail isFullscreen 15 | */ 16 | export interface FullscreenChangeEvent extends DOMEvent<boolean> {} 17 | 18 | /** 19 | * Fired when an error occurs either entering or exiting fullscreen. This will generally occur 20 | * if the user has not interacted with the page yet. 21 | * 22 | * @bubbles 23 | * @composed 24 | * @detail error 25 | */ 26 | export interface FullscreenErrorEvent extends DOMEvent<unknown> {} 27 | -------------------------------------------------------------------------------- /packages/vidstack/src/foundation/list/symbols.ts: -------------------------------------------------------------------------------- 1 | const ADD = Symbol(__DEV__ ? 'LIST_ADD' : 0), 2 | REMOVE = Symbol(__DEV__ ? 'LIST_REMOVE' : 0), 3 | RESET = Symbol(__DEV__ ? 'LIST_RESET' : 0), 4 | SELECT = Symbol(__DEV__ ? 'LIST_SELECT' : 0), 5 | READONLY = Symbol(__DEV__ ? 'LIST_READONLY' : 0), 6 | SET_READONLY = Symbol(__DEV__ ? 'LIST_SET_READONLY' : 0), 7 | ON_RESET = Symbol(__DEV__ ? 'LIST_ON_RESET' : 0), 8 | ON_REMOVE = Symbol(__DEV__ ? 'LIST_ON_REMOVE' : 0), 9 | ON_USER_SELECT = Symbol(__DEV__ ? 'LIST_ON_USER_SELECT' : 0); 10 | 11 | /** @internal */ 12 | export const ListSymbol = { 13 | add: ADD, 14 | remove: REMOVE, 15 | reset: RESET, 16 | select: SELECT, 17 | readonly: READONLY, 18 | setReadonly: SET_READONLY, 19 | onReset: ON_RESET, 20 | onRemove: ON_REMOVE, 21 | onUserSelect: ON_USER_SELECT, 22 | } as const; 23 | -------------------------------------------------------------------------------- /packages/vidstack/src/foundation/logger/colors.ts: -------------------------------------------------------------------------------- 1 | const LOCAL_STORAGE_KEY = '@vidstack/log-colors'; 2 | 3 | const savedColors = init(); 4 | 5 | export function getLogColor(key: string): string | undefined { 6 | return savedColors.get(key); 7 | } 8 | 9 | export function saveLogColor(key: string, { color = generateColor(), overwrite = false } = {}) { 10 | if (!__DEV__) return; 11 | if (!savedColors.has(key) || overwrite) { 12 | savedColors.set(key, color); 13 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(Object.entries(savedColors))); 14 | } 15 | } 16 | 17 | function generateColor() { 18 | return `hsl(${Math.random() * 360}, 55%, 70%)`; 19 | } 20 | 21 | function init(): Map<string, string> { 22 | if (!__DEV__) return new Map(); 23 | 24 | let colors; 25 | 26 | try { 27 | colors = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)!); 28 | } catch { 29 | // no-op 30 | } 31 | 32 | return new Map(Object.entries(colors ?? {})); 33 | } 34 | -------------------------------------------------------------------------------- /packages/vidstack/src/foundation/logger/events.ts: -------------------------------------------------------------------------------- 1 | import type { DOMEvent } from 'maverick.js/std'; 2 | 3 | import type { LogLevel } from './log-level'; 4 | 5 | declare global { 6 | interface HTMLElementEventMap extends LoggerEvents {} 7 | } 8 | 9 | export interface LoggerEvents { 10 | 'vds-log': LogEvent; 11 | } 12 | 13 | export interface LogEventDetail { 14 | /** 15 | * The log level. 16 | */ 17 | level: LogLevel; 18 | /** 19 | * Data to be logged. 20 | */ 21 | data?: any[]; 22 | } 23 | 24 | /** 25 | * @bubbles 26 | * @composed 27 | * @detail log 28 | */ 29 | export interface LogEvent extends DOMEvent<LogEventDetail> {} 30 | -------------------------------------------------------------------------------- /packages/vidstack/src/foundation/logger/grouped-log.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from './controller'; 2 | import type { LogLevel } from './log-level'; 3 | 4 | export const GROUPED_LOG = Symbol(__DEV__ ? 'GROUPED_LOG' : 0); 5 | 6 | export class GroupedLog { 7 | readonly [GROUPED_LOG] = true; 8 | readonly logs: ({ label?: string; data: any[] } | GroupedLog)[] = []; 9 | 10 | constructor( 11 | readonly logger: Logger, 12 | readonly level: LogLevel, 13 | readonly title: string, 14 | readonly root?: GroupedLog, 15 | readonly parent?: GroupedLog, 16 | ) {} 17 | 18 | log(...data: any[]): GroupedLog { 19 | this.logs.push({ data }); 20 | return this; 21 | } 22 | 23 | labelledLog(label: string, ...data: any[]): GroupedLog { 24 | this.logs.push({ label, data }); 25 | return this; 26 | } 27 | 28 | groupStart(title: string): GroupedLog { 29 | return new GroupedLog(this.logger, this.level, title, this.root ?? this, this); 30 | } 31 | 32 | groupEnd(): GroupedLog { 33 | this.parent?.logs.push(this); 34 | return this.parent ?? this; 35 | } 36 | 37 | dispatch(): boolean { 38 | return this.logger.dispatch(this.level, this.root ?? this); 39 | } 40 | } 41 | 42 | export function isGroupedLog(data: any): data is GroupedLog { 43 | return !!data?.[GROUPED_LOG]; 44 | } 45 | -------------------------------------------------------------------------------- /packages/vidstack/src/foundation/logger/log-level.ts: -------------------------------------------------------------------------------- 1 | export type LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug'; 2 | 3 | export const LogLevelValue = Object.freeze({ 4 | silent: 0, 5 | error: 1, 6 | warn: 2, 7 | info: 3, 8 | debug: 4, 9 | }); 10 | 11 | export const LogLevelColor = Object.freeze({ 12 | silent: 'white', 13 | error: 'hsl(6, 58%, 50%)', 14 | warn: 'hsl(51, 58%, 50%)', 15 | info: 'hsl(219, 58%, 50%)', 16 | debug: 'hsl(280, 58%, 50%)', 17 | }); 18 | -------------------------------------------------------------------------------- /packages/vidstack/src/foundation/logger/ms.ts: -------------------------------------------------------------------------------- 1 | import { round } from '../../utils/number'; 2 | 3 | const s = 1000; 4 | const m = s * 60; 5 | const h = m * 60; 6 | const d = h * 24; 7 | 8 | /** 9 | * @see https://github.com/vercel/ms 10 | */ 11 | export function ms(val: number): string { 12 | const msAbs = Math.abs(val); 13 | 14 | if (msAbs >= d) { 15 | return Math.round(val / d) + 'd'; 16 | } 17 | 18 | if (msAbs >= h) { 19 | return Math.round(val / h) + 'h'; 20 | } 21 | 22 | if (msAbs >= m) { 23 | return Math.round(val / m) + 'm'; 24 | } 25 | 26 | if (msAbs >= s) { 27 | return Math.round(val / s) + 's'; 28 | } 29 | 30 | return round(val, 2) + 'ms'; 31 | } 32 | -------------------------------------------------------------------------------- /packages/vidstack/src/foundation/observers/raf-loop.ts: -------------------------------------------------------------------------------- 1 | import { isNumber, isUndefined } from 'maverick.js/std'; 2 | 3 | export class RAFLoop { 4 | #id: number | undefined; 5 | #callback: () => void; 6 | 7 | constructor(callback: () => void) { 8 | this.#callback = callback; 9 | } 10 | 11 | start() { 12 | // Time updates are already in progress. 13 | if (!isUndefined(this.#id)) return; 14 | this.#loop(); 15 | } 16 | 17 | stop() { 18 | if (isNumber(this.#id)) window.cancelAnimationFrame(this.#id); 19 | this.#id = undefined; 20 | } 21 | 22 | #loop() { 23 | this.#id = window.requestAnimationFrame(() => { 24 | if (isUndefined(this.#id)) return; 25 | this.#callback(); 26 | this.#loop(); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/vidstack/src/foundation/orientation/events.ts: -------------------------------------------------------------------------------- 1 | import type { DOMEvent } from 'maverick.js/std'; 2 | 3 | import type { ScreenOrientationLockType, ScreenOrientationType } from './types'; 4 | 5 | export interface ScreenOrientationEvents { 6 | 'orientation-change': ScreenOrientationChangeEvent; 7 | } 8 | 9 | export interface ScreenOrientationChangeEventDetail { 10 | orientation: ScreenOrientationType; 11 | lock?: ScreenOrientationLockType; 12 | } 13 | 14 | /** 15 | * Fired when the current screen orientation changes. 16 | * 17 | * @detail orientation 18 | */ 19 | export interface ScreenOrientationChangeEvent 20 | extends DOMEvent<ScreenOrientationChangeEventDetail> {} 21 | -------------------------------------------------------------------------------- /packages/vidstack/src/foundation/queue/queue.ts: -------------------------------------------------------------------------------- 1 | export class Queue<Items> { 2 | #queue = new Map<keyof Items, any>(); 3 | 4 | /** 5 | * Queue the given `item` under the given `key` to be processed at a later time by calling 6 | * `serve(key)`. 7 | */ 8 | enqueue<T extends keyof Items>(key: T, item: Items[T]) { 9 | this.#queue.set(key, item); 10 | } 11 | 12 | /** 13 | * Process item in queue for the given `key`. 14 | */ 15 | serve<T extends keyof Items>(key: T): Items[T] | undefined { 16 | const value = this.peek(key); 17 | this.#queue.delete(key); 18 | return value; 19 | } 20 | 21 | /** 22 | * Peek at item in queue for the given `key`. 23 | */ 24 | peek<T extends keyof Items>(key: T): Items[T] | undefined { 25 | return this.#queue.get(key); 26 | } 27 | 28 | /** 29 | * Removes queued item under the given `key`. 30 | */ 31 | delete(key: keyof Items) { 32 | this.#queue.delete(key); 33 | } 34 | 35 | /** 36 | * Clear all items in the queue. 37 | */ 38 | clear() { 39 | this.#queue.clear(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/vidstack/src/global/layouts/default.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultLayoutProps } from '../../components/layouts/default/props'; 2 | import type { VidstackPlayerLayoutLoader } from './loader'; 3 | 4 | export class VidstackPlayerLayout implements VidstackPlayerLayoutLoader { 5 | constructor(readonly props?: Partial<DefaultLayoutProps>) {} 6 | 7 | readonly name = 'vidstack'; 8 | 9 | async load() { 10 | await import('../../elements/bundles/player-layouts/default'); 11 | await import('../../elements/bundles/player-ui'); 12 | } 13 | 14 | create() { 15 | const layouts = [ 16 | document.createElement('media-audio-layout'), 17 | document.createElement('media-video-layout'), 18 | ]; 19 | 20 | if (this.props) { 21 | for (const [prop, value] of Object.entries(this.props)) { 22 | for (const el of layouts) el[prop] = value; 23 | } 24 | } 25 | 26 | return layouts; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/vidstack/src/global/layouts/loader.ts: -------------------------------------------------------------------------------- 1 | export interface VidstackPlayerLayoutLoader { 2 | readonly name: string; 3 | load(): void | Promise<void>; 4 | create(): HTMLElement[] | Promise<HTMLElement[]>; 5 | } 6 | -------------------------------------------------------------------------------- /packages/vidstack/src/global/layouts/plyr.ts: -------------------------------------------------------------------------------- 1 | import type { PlyrLayoutProps } from '../../components/layouts/plyr/props'; 2 | import type { VidstackPlayerLayoutLoader } from './loader'; 3 | 4 | export class PlyrLayout implements VidstackPlayerLayoutLoader { 5 | constructor(readonly props?: Partial<PlyrLayoutProps>) {} 6 | 7 | readonly name = 'plyr'; 8 | 9 | async load() { 10 | await import('../../elements/bundles/player-layouts/plyr'); 11 | } 12 | 13 | create() { 14 | const layout = document.createElement('media-plyr-layout'); 15 | 16 | if (this.props) { 17 | for (const [prop, value] of Object.entries(this.props)) { 18 | layout[prop] = value; 19 | } 20 | } 21 | 22 | return [layout]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/vidstack/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference path="../npm/dom.d.ts" /> 2 | /// <reference path="../npm/google-cast.d.ts" /> 3 | 4 | declare global { 5 | const __DEV__: boolean; 6 | const __SERVER__: boolean; 7 | const __TEST__: boolean; 8 | const __CDN__: boolean; 9 | } 10 | 11 | export {}; 12 | -------------------------------------------------------------------------------- /packages/vidstack/src/index.ts: -------------------------------------------------------------------------------- 1 | if (__DEV__) { 2 | console.warn('[vidstack] dev mode!'); 3 | } 4 | 5 | export * from './exports/foundation'; 6 | export * from './exports/core'; 7 | export * from './exports/providers'; 8 | export * from './exports/components'; 9 | export * from './exports/utils'; 10 | export * from './exports/maverick'; 11 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/audio/loader.ts: -------------------------------------------------------------------------------- 1 | import { isString } from 'maverick.js/std'; 2 | 3 | import type { MediaContext } from '../../core/api/media-context'; 4 | import type { Src } from '../../core/api/src-types'; 5 | import type { MediaType } from '../../core/api/types'; 6 | import { isAudioSrc } from '../../utils/mime'; 7 | import { canPlayAudioType } from '../../utils/support'; 8 | import type { MediaProviderLoader } from '../types'; 9 | import type { AudioProvider } from './provider'; 10 | 11 | export class AudioProviderLoader implements MediaProviderLoader<AudioProvider> { 12 | readonly name = 'audio'; 13 | 14 | target!: HTMLAudioElement; 15 | 16 | canPlay(src: Src) { 17 | if (!isAudioSrc(src)) return false; 18 | // Let this pass through on the server, we can figure out which type to play client-side. The 19 | // important thing is that the correct provider is loaded. 20 | return ( 21 | __SERVER__ || 22 | !isString(src.src) || 23 | src.type === '?' || 24 | canPlayAudioType(this.target, src.type) 25 | ); 26 | } 27 | 28 | mediaType(): MediaType { 29 | return 'audio'; 30 | } 31 | 32 | async load(ctx: MediaContext) { 33 | if (__SERVER__) { 34 | throw Error('[vidstack] can not load audio provider server-side'); 35 | } 36 | 37 | if (__DEV__ && !this.target) { 38 | throw Error( 39 | '[vidstack] `<audio>` element was not found - did you forget to include `<media-provider>`?', 40 | ); 41 | } 42 | 43 | return new (await import('./provider')).AudioProvider(this.target, ctx); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/dash/loader.ts: -------------------------------------------------------------------------------- 1 | import type { Src } from '../../core/api/src-types'; 2 | import { isDASHSrc } from '../../utils/mime'; 3 | import { isDASHSupported } from '../../utils/support'; 4 | import type { MediaProviderLoader } from '../types'; 5 | import { VideoProviderLoader } from '../video/loader'; 6 | import { DASHProvider } from './provider'; 7 | 8 | export class DASHProviderLoader 9 | extends VideoProviderLoader 10 | implements MediaProviderLoader<DASHProvider> 11 | { 12 | static supported = isDASHSupported(); 13 | 14 | override readonly name = 'dash'; 15 | 16 | override canPlay(src: Src) { 17 | return DASHProviderLoader.supported && isDASHSrc(src); 18 | } 19 | 20 | override async load(context) { 21 | if (__SERVER__) { 22 | throw Error('[vidstack] can not load dash provider server-side'); 23 | } 24 | 25 | if (__DEV__ && !this.target) { 26 | throw Error( 27 | '[vidstack] `<video>` element was not found - did you forget to include `<media-provider>`?', 28 | ); 29 | } 30 | 31 | return new (await import('./provider')).DASHProvider(this.target, context); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/dash/types.ts: -------------------------------------------------------------------------------- 1 | import type DASH from 'dashjs'; 2 | 3 | import type { DASHProviderEvents } from './events'; 4 | 5 | export { type DASHProviderEvents }; 6 | 7 | export type DASHConstructor = typeof DASH.MediaPlayer; 8 | export type DASHConstructorLoader = () => Promise<{ default: DASHConstructor } | undefined>; 9 | 10 | export type DASHNamespace = typeof DASH; 11 | export type DASHNamespaceLoader = () => Promise<{ default: typeof DASH } | undefined>; 12 | 13 | export type DASHLibrary = 14 | | DASHConstructor 15 | | DASHConstructorLoader 16 | | DASHNamespace 17 | | DASHNamespaceLoader 18 | | string 19 | | undefined; 20 | 21 | export type DASHInstanceCallback = (player: DASH.MediaPlayerClass) => void; 22 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/google-cast/types.ts: -------------------------------------------------------------------------------- 1 | export interface GoogleCastOptions extends Partial<cast.framework.CastOptions> {} 2 | 3 | export type { GoogleCastEvents } from './events'; 4 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/hls/loader.ts: -------------------------------------------------------------------------------- 1 | import type { Src } from '../../core/api/src-types'; 2 | import { isHLSSrc } from '../../utils/mime'; 3 | import { isHLSSupported } from '../../utils/support'; 4 | import type { MediaProviderLoader } from '../types'; 5 | import { VideoProviderLoader } from '../video/loader'; 6 | import type { HLSProvider } from './provider'; 7 | 8 | export class HLSProviderLoader 9 | extends VideoProviderLoader 10 | implements MediaProviderLoader<HLSProvider> 11 | { 12 | static supported = isHLSSupported(); 13 | 14 | override readonly name = 'hls'; 15 | 16 | override canPlay(src: Src) { 17 | return HLSProviderLoader.supported && isHLSSrc(src); 18 | } 19 | 20 | override async load(context) { 21 | if (__SERVER__) { 22 | throw Error('[vidstack] can not load hls provider server-side'); 23 | } 24 | 25 | if (__DEV__ && !this.target) { 26 | throw Error( 27 | '[vidstack] `<video>` element was not found - did you forget to include `<media-provider>`?', 28 | ); 29 | } 30 | 31 | return new (await import('./provider')).HLSProvider(this.target, context); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/hls/types.ts: -------------------------------------------------------------------------------- 1 | import type * as HLS from 'hls.js'; 2 | 3 | import type { HLSProviderEvents } from './events'; 4 | 5 | export { type HLSProviderEvents }; 6 | 7 | export type HLSConstructor = typeof HLS.default; 8 | export type HLSConstructorLoader = () => Promise<{ default: HLSConstructor } | undefined>; 9 | export type HLSLibrary = HLSConstructor | HLSConstructorLoader | string | undefined; 10 | export type HLSInstanceCallback = (hls: HLS.default) => void; 11 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/video/presentation/events.ts: -------------------------------------------------------------------------------- 1 | import type { DOMEvent } from 'maverick.js/std'; 2 | 3 | export type VideoPresentationEvents = { 4 | 'video-presentation-change': VideoPresentationChangeEvent; 5 | }; 6 | 7 | /** 8 | * Fired when the video presentation mode changes. Only available in Safari. 9 | * 10 | * @detail mode 11 | */ 12 | export interface VideoPresentationChangeEvent extends DOMEvent<WebKitPresentationMode> {} 13 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/vimeo/embed/message.ts: -------------------------------------------------------------------------------- 1 | import type { VimeoCommand } from './command'; 2 | import type { VimeoEventPayload } from './event'; 3 | 4 | export interface VimeoMessage { 5 | data?: any; 6 | value?: any; 7 | method?: VimeoCommand; 8 | event?: keyof VimeoEventPayload; 9 | } 10 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/vimeo/embed/misc.ts: -------------------------------------------------------------------------------- 1 | export interface VimeoVideoInfo { 2 | title: string; 3 | poster: string; 4 | duration: number; 5 | pro: boolean; 6 | } 7 | 8 | export interface VimeoChapter { 9 | startTime: number; 10 | title: string; 11 | index: number; 12 | } 13 | 14 | export interface VimeoTextTrack { 15 | label: string; 16 | language: string; 17 | kind: 'captions' | 'subtitles'; 18 | mode: 'showing' | 'disabled'; 19 | } 20 | 21 | export interface VimeoTextCue { 22 | html: string; 23 | kind: 'captions' | 'subtitles'; 24 | label: string; 25 | language: string; 26 | text: string; 27 | } 28 | 29 | export interface VimeoQuality { 30 | id: string; 31 | label: string; 32 | active: boolean; 33 | } 34 | 35 | export interface VimeoOEmbedData { 36 | account_type: 'basic' | 'pro' | 'business'; 37 | author_name: string; 38 | author_url: string; 39 | description: string; 40 | duration: number; 41 | height: number; 42 | html: string; 43 | is_plus: '0' | '1'; 44 | provider_name: string; 45 | provider_url: string; 46 | thumbnail_height: number; 47 | thumbnail_url_with_play_button: string; 48 | thumbnail_url: string; 49 | thumbnail_width: number; 50 | title: string; 51 | type: 'audio' | 'video'; 52 | upload_date: string; 53 | uri: string; 54 | version: string; 55 | video_id: number; 56 | width: number; 57 | } 58 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/youtube/embed/command.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see {@link https://developers.google.com/youtube/iframe_api_reference#Playback_controls} 3 | */ 4 | export type YouTubeCommand = 5 | | 'playVideo' 6 | | 'pauseVideo' 7 | | 'seekTo' 8 | | 'mute' 9 | | 'unMute' 10 | | 'setVolume' 11 | | 'setPlaybackRate'; 12 | 13 | export interface YouTubeCommandArg { 14 | playVideo: void; 15 | pauseVideo: void; 16 | seekTo: number; 17 | mute: void; 18 | unMute: void; 19 | setVolume: number; 20 | setPlaybackRate: number; 21 | } 22 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/youtube/embed/event.ts: -------------------------------------------------------------------------------- 1 | export type YouTubeEvent = 'initialDelivery' | 'onReady' | 'infoDelivery' | 'apiInfoDelivery'; 2 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/youtube/embed/quality.ts: -------------------------------------------------------------------------------- 1 | export type YouTubePlaybackQuality = 2 | | 'unknown' 3 | | 'tiny' 4 | | 'small' 5 | | 'medium' 6 | | 'large' 7 | | 'hd720' 8 | | 'hd1080' 9 | | 'highres' 10 | | 'max'; 11 | 12 | export function mapYouTubePlaybackQuality(quality: YouTubePlaybackQuality) { 13 | switch (quality) { 14 | case 'unknown': 15 | return undefined; 16 | case 'tiny': 17 | return 144; 18 | case 'small': 19 | return 240; 20 | case 'medium': 21 | return 360; 22 | case 'large': 23 | return 480; 24 | case 'hd720': 25 | return 720; 26 | case 'hd1080': 27 | return 1080; 28 | case 'highres': 29 | return 1440; 30 | case 'max': 31 | return 2160; 32 | default: 33 | return undefined; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/vidstack/src/providers/youtube/embed/state.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see {@link https://developers.google.com/youtube/iframe_api_reference#onStateChange} 3 | */ 4 | export const YouTubePlayerState = { 5 | Unstarted: -1, 6 | Ended: 0, 7 | Playing: 1, 8 | Paused: 2, 9 | Buffering: 3, 10 | Cued: 5, 11 | } as const; 12 | 13 | export type YouTubePlayerStateValue = (typeof YouTubePlayerState)[keyof typeof YouTubePlayerState]; 14 | -------------------------------------------------------------------------------- /packages/vidstack/src/test-utils/index.ts: -------------------------------------------------------------------------------- 1 | import { listenEvent } from 'maverick.js/std'; 2 | 3 | export async function waitForEvent<Event>( 4 | target: EventTarget, 5 | type: string, 6 | options?: (EventListenerOptions | AddEventListenerOptions) & { timeout?: number }, 7 | ): Promise<Event> { 8 | return new Promise((resolve, reject) => { 9 | const timerId = window.setTimeout(() => { 10 | reject(`Timed out waiting for event \`${type}\`.`); 11 | }, options?.timeout ?? 1000); 12 | listenEvent( 13 | target, 14 | type as any, 15 | (event: any) => { 16 | window.clearTimeout(timerId); 17 | resolve(event); 18 | }, 19 | options, 20 | ); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /packages/vidstack/src/test-utils/setup.ts: -------------------------------------------------------------------------------- 1 | import { chaiDomDiff } from '@open-wc/semantic-dom-diff'; 2 | 3 | chai.use(chaiDomDiff); 4 | -------------------------------------------------------------------------------- /packages/vidstack/src/utils/aria.ts: -------------------------------------------------------------------------------- 1 | import type { ReadSignal } from 'maverick.js'; 2 | 3 | export function ariaBool(value: boolean): 'true' | 'false' { 4 | return value ? 'true' : 'false'; 5 | } 6 | 7 | export function $ariaBool(signal: ReadSignal<boolean>): ReadSignal<'true' | 'false'> { 8 | return () => ariaBool(signal()); 9 | } 10 | 11 | export function prefersReducedMotion() { 12 | if (typeof window === 'undefined') return false; 13 | return window.matchMedia('(prefers-reduced-motion: reduce)').matches; 14 | } 15 | -------------------------------------------------------------------------------- /packages/vidstack/src/utils/array.ts: -------------------------------------------------------------------------------- 1 | export function isArrayEqual(a: unknown[], b: unknown[]) { 2 | return a.length === b.length && a.every((value, i) => value === b[i]); 3 | } 4 | -------------------------------------------------------------------------------- /packages/vidstack/src/utils/color.ts: -------------------------------------------------------------------------------- 1 | export function hexToRgb(hex: string) { 2 | const { style } = new Option(); 3 | style.color = hex; 4 | return style.color.match(/\((.*?)\)/)![1].replace(/,/g, ' '); 5 | } 6 | -------------------------------------------------------------------------------- /packages/vidstack/src/utils/error.ts: -------------------------------------------------------------------------------- 1 | export function coerceToError(error: unknown) { 2 | return error instanceof Error 3 | ? error 4 | : Error(typeof error === 'string' ? error : JSON.stringify(error)); 5 | } 6 | 7 | export function assert(condition: any, message?: string | false): asserts condition { 8 | if (!condition) { 9 | throw Error(message || 'Assertion failed.'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/vidstack/src/utils/language.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets the language name corresponding to the provided language code. 3 | * 4 | * @param {string} langCode - The language code (e.g.,"en", "en-us", "es-es", "fr-fr"). 5 | * @returns {string} The localized language name based on the user's preferred languages, 6 | * or `null` if the language code is not recognized. 7 | */ 8 | export function getLangName(langCode: string) { 9 | try { 10 | const displayNames = new Intl.DisplayNames(navigator.languages, { type: 'language' }); 11 | const languageName = displayNames.of(langCode); 12 | return languageName ?? null; 13 | } catch (err) { 14 | return null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vidstack/src/utils/number.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Round a number to the given number of `decimalPlaces`. 3 | */ 4 | export function round(num: number, decimalPlaces = 2): number { 5 | return Number(num.toFixed(decimalPlaces)); 6 | } 7 | 8 | /** 9 | * Get the number of decimal places in the given `num`. 10 | * 11 | * @example `1 -> 0` 12 | * @example `1.0 -> 0` 13 | * @example `1.1 -> 1` 14 | * @example `1.12 -> 2` 15 | */ 16 | export function getNumberOfDecimalPlaces(num: number): number { 17 | return String(num).split('.')[1]?.length ?? 0; 18 | } 19 | 20 | /** 21 | * Clamp a given `value` between a minimum and maximum value. 22 | */ 23 | export function clampNumber(min: number, value: number, max: number): number { 24 | return Math.max(min, Math.min(max, value)); 25 | } 26 | -------------------------------------------------------------------------------- /packages/vidstack/src/utils/promise.ts: -------------------------------------------------------------------------------- 1 | import { deferredPromise } from 'maverick.js/std'; 2 | 3 | export function timedPromise<Resolved, Rejected>(callback: () => Rejected | void, ms = 3000) { 4 | const promise = deferredPromise<Resolved, Rejected>(); 5 | 6 | setTimeout(() => { 7 | const rejection = callback(); 8 | if (rejection) promise.reject(rejection); 9 | }, ms); 10 | 11 | return promise; 12 | } 13 | -------------------------------------------------------------------------------- /packages/vidstack/src/utils/scroll.ts: -------------------------------------------------------------------------------- 1 | import { compute, type Options as ComputeScrollOptions } from 'compute-scroll-into-view'; 2 | 3 | export interface ScrollIntoViewOptions extends ComputeScrollOptions, ScrollOptions {} 4 | 5 | export function scrollIntoView(el: HTMLElement, options: ScrollIntoViewOptions) { 6 | const scrolls = compute(el, options); 7 | for (const { el, top, left } of scrolls) { 8 | el.scroll({ top, left, behavior: options.behavior }); 9 | } 10 | } 11 | 12 | export function scrollIntoCenter(el: HTMLElement, options: ScrollIntoViewOptions = {}) { 13 | scrollIntoView(el, { 14 | scrollMode: 'if-needed', 15 | block: 'center', 16 | inline: 'center', 17 | ...options, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/vidstack/styles/player/default/chapter-title.css: -------------------------------------------------------------------------------- 1 | :where(.vds-chapter-title) { 2 | --color: var(--media-chapter-title-color, rgba(255 255 255 / 0.64)); 3 | display: inline-block; 4 | font-family: var(--media-font-family, sans-serif); 5 | font-size: var(--media-chapter-title-font-size, 16px); 6 | font-weight: var(--media-chapter-title-font-weight, 400); 7 | color: var(--color); 8 | flex: 1 1 0%; 9 | padding-inline: 6px; 10 | overflow: hidden; 11 | text-align: start; 12 | white-space: nowrap; 13 | text-overflow: ellipsis; 14 | } 15 | 16 | .vds-chapter-title::before { 17 | content: var(--media-chapter-title-separator, '\2022'); 18 | display: inline-block; 19 | margin-right: var(--media-chapter-title-separator-gap, 6px); 20 | color: var(--media-chapter-title-separator-color, var(--color)); 21 | } 22 | 23 | .vds-chapter-title:empty::before { 24 | content: ''; 25 | margin: 0; 26 | } 27 | -------------------------------------------------------------------------------- /packages/vidstack/styles/player/default/gestures.css: -------------------------------------------------------------------------------- 1 | /* 2 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | * Gesture 4 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | */ 6 | 7 | :where(.vds-gestures) { 8 | display: contents; 9 | } 10 | 11 | :where(.vds-gesture) { 12 | position: absolute; 13 | display: block; 14 | contain: content; 15 | z-index: 0; 16 | opacity: 0; 17 | visibility: hidden; 18 | pointer-events: none !important; 19 | } 20 | -------------------------------------------------------------------------------- /packages/vidstack/styles/player/default/icons.css: -------------------------------------------------------------------------------- 1 | :where(.vds-icon svg) { 2 | display: block; 3 | width: 100%; 4 | height: 100%; 5 | vertical-align: middle; 6 | } 7 | -------------------------------------------------------------------------------- /packages/vidstack/styles/player/default/poster.css: -------------------------------------------------------------------------------- 1 | /* 2 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | * Poster 4 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | */ 6 | 7 | :where(.vds-poster) { 8 | display: block; 9 | contain: content; 10 | position: absolute; 11 | top: 50%; 12 | transform: translateY(-50%); 13 | left: 0; 14 | opacity: 0; 15 | width: 100%; 16 | height: 100%; 17 | z-index: 1; 18 | border: 0; 19 | pointer-events: none; 20 | box-sizing: border-box; 21 | transition: opacity 0.2s ease-out; 22 | background-color: var(--media-poster-bg, black); 23 | } 24 | 25 | :where(.vds-poster img) { 26 | object-fit: inherit; 27 | object-position: inherit; 28 | pointer-events: none; 29 | user-select: none; 30 | -webkit-user-select: none; 31 | box-sizing: border-box; 32 | } 33 | 34 | .vds-poster :where(img) { 35 | border: 0; 36 | width: 100%; 37 | height: 100%; 38 | object-fit: contain; 39 | } 40 | 41 | :where(.vds-poster[data-hidden]) { 42 | display: none; 43 | } 44 | 45 | :where(.vds-poster[data-visible]) { 46 | opacity: 1; 47 | } 48 | 49 | .vds-poster:not(:defined), 50 | .vds-poster img:not([src]) { 51 | display: none; 52 | } 53 | -------------------------------------------------------------------------------- /packages/vidstack/styles/player/default/thumbnail.css: -------------------------------------------------------------------------------- 1 | /* 2 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | * Thumbnail 4 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | */ 6 | 7 | :where(.vds-thumbnail) { 8 | --min-width: var(--media-thumbnail-min-width, 140px); 9 | --max-width: var(--media-thumbnail-max-width, 180px); 10 | --aspect-ratio: var(--media-thumbnail-aspect-ratio, var(--thumbnail-aspect-ratio)); 11 | display: block; 12 | width: var(--thumbnail-width); 13 | height: var(--thumbnail-height); 14 | background-color: var(--media-thumbnail-bg, black); 15 | contain: strict; 16 | overflow: hidden; 17 | box-sizing: border-box; 18 | min-width: var(--min-width); 19 | min-height: var(--media-thumbnail-min-height, calc(var(--min-width) / var(--aspect-ratio))); 20 | max-width: var(--max-width); 21 | max-height: var(--media-thumbnail-max-height, calc(var(--max-width) / var(--aspect-ratio))); 22 | } 23 | 24 | .vds-thumbnail { 25 | border: var(--media-thumbnail-border, 1px solid white); 26 | } 27 | 28 | :where(.vds-thumbnail img) { 29 | min-width: unset !important; 30 | max-width: unset !important; 31 | will-change: width, height, transform; 32 | } 33 | 34 | :where(.vds-thumbnail[data-loading] img) { 35 | opacity: 0; 36 | } 37 | 38 | :where(.vds-thumbnail[aria-hidden='true']) { 39 | display: none !important; 40 | } 41 | -------------------------------------------------------------------------------- /packages/vidstack/styles/player/default/time.css: -------------------------------------------------------------------------------- 1 | /* 2 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | * Time 4 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | */ 6 | 7 | :where(.vds-time-group) { 8 | display: flex; 9 | align-items: center; 10 | } 11 | 12 | .vds-time-divider { 13 | margin: 0 var(--media-time-divider-gap, 2.5px); 14 | color: var(--media-time-divider-color, #e0e0e0); 15 | } 16 | 17 | :where(.vds-time) { 18 | display: inline-block; 19 | contain: content; 20 | font-size: var(--media-time-font-size, 15px); 21 | font-weight: var(--media-time-font-weight, 400); 22 | font-family: var(--media-font-family, sans-serif); 23 | border-radius: var(--media-time-border-radius, 2px); 24 | letter-spacing: var(--media-time-letter-spacing, 0.025em); 25 | } 26 | 27 | .vds-time { 28 | outline: 0; 29 | color: var(--media-time-color, var(--default-color)); 30 | background-color: var(--media-time-bg); 31 | border: var(--media-time-border); 32 | padding: var(--media-time-padding, 2px); 33 | } 34 | 35 | :where(.vds-time:focus-visible) { 36 | box-shadow: var(--media-focus-ring); 37 | } 38 | 39 | .light .vds-time { 40 | --default-color: rgb(10 10 10); 41 | } 42 | 43 | .dark .vds-time { 44 | --default-color: #f5f5f5; 45 | } 46 | -------------------------------------------------------------------------------- /packages/vidstack/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "types", 6 | "paths": {} 7 | }, 8 | "exclude": ["src/**/*.test.ts", "src/test-utils"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/vidstack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "paths": { 6 | "$test-utils": ["./src/test-utils/index.ts"] 7 | }, 8 | "types": ["@types/node", "vitest/globals"] 9 | }, 10 | "include": ["src/**/*.ts", "src/**/*.tsx"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/vidstack/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vitest" /> 2 | 3 | import { transform } from 'esbuild'; 4 | import { defineConfig } from 'vite'; 5 | 6 | const SERVER = !!process.env.SERVER; 7 | 8 | export default defineConfig({ 9 | define: { 10 | __DEV__: 'true', 11 | __TEST__: 'true', 12 | __SERVER__: SERVER ? 'true' : 'false', 13 | __CDN__: 'true', 14 | }, 15 | build: { 16 | target: 'es2019', 17 | }, 18 | publicDir: 'sandbox/public', 19 | resolve: { 20 | alias: { 21 | '$test-utils': '/src/test-utils', 22 | 'vidstack/elements': '/src/elements', 23 | 'vidstack/player': '/src/player', 24 | }, 25 | }, 26 | optimizeDeps: { 27 | noDiscovery: true, 28 | include: [], 29 | }, 30 | // https://vitest.dev/config 31 | test: { 32 | include: ['src/**/*.test.ts'], 33 | globals: true, 34 | environment: 'jsdom', 35 | setupFiles: ['src/test-utils/setup.ts'], 36 | testTimeout: 2500, 37 | }, 38 | plugins: [legacyPlugin()], 39 | }); 40 | 41 | function legacyPlugin() { 42 | return { 43 | name: 'legacy', 44 | enforce: 'pre', 45 | async transform(code, id) { 46 | if (/\.(j|t)s/.test(id)) { 47 | return ( 48 | await transform(code, { 49 | loader: 'ts', 50 | target: 'es2019', 51 | tsconfigRaw: { 52 | compilerOptions: { 53 | experimentalDecorators: true, 54 | }, 55 | }, 56 | }) 57 | ).code; 58 | } 59 | }, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "allowUnreachableCode": false, 5 | "alwaysStrict": true, 6 | "allowImportingTsExtensions": true, 7 | "checkJs": true, 8 | "declaration": true, 9 | "emitDeclarationOnly": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "incremental": true, 14 | "importHelpers": true, 15 | "lib": ["dom", "dom.iterable", "es2020"], 16 | "module": "esnext", 17 | "moduleResolution": "bundler", 18 | "noImplicitAny": false, 19 | "noImplicitOverride": true, 20 | "noImplicitReturns": false, 21 | "noUnusedParameters": false, 22 | "preserveWatchOutput": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "target": "esnext", 26 | "useDefineForClassFields": true, 27 | "verbatimModuleSyntax": true 28 | }, 29 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.cjs", "**/*.mjs", "**/*.astro"], 30 | "exclude": [ 31 | "node_modules", 32 | "dist", 33 | "local", 34 | "cdn", 35 | "packages/*/elements", 36 | "packages/*/globals.d.ts", 37 | "packages/*/index.d.ts" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "tasks": { 4 | "analyze": { 5 | "dependsOn": ["clean"], 6 | "outputs": [ 7 | "dist-npm/analyze.json", 8 | "dist-npm/vscode.html-data.json", 9 | "dist-npm/vue.d.ts", 10 | "dist-npm/svelte.d.ts", 11 | "dist-npm/solid.d.ts" 12 | ] 13 | }, 14 | "build": { 15 | "dependsOn": ["clean", "^build"], 16 | "outputs": ["cdn/**", "dist-npm/**", "styles/**"] 17 | }, 18 | "build:cdn": { 19 | "outputs": ["dist-cdn/**"] 20 | }, 21 | "clean": { 22 | "cache": false 23 | }, 24 | "types": { 25 | "dependsOn": ["clean"], 26 | "outputs": ["types/**", "dist-npm/**/*.d.ts", "tsconfig.build.tsbuildinfo"] 27 | }, 28 | "format": { 29 | "inputs": ["src/**"] 30 | }, 31 | "test": { 32 | "inputs": ["src/**"] 33 | } 34 | } 35 | } 36 | --------------------------------------------------------------------------------