From 46507f9d0d02bfd0b7b7957b8986dd278a8962cb Mon Sep 17 00:00:00 2001 From: publish Date: Wed, 5 Aug 2026 18:57:42 +0200 Subject: [PATCH] build(media): build26 from 45504a2b --- README.md | 30 + dist/default/core/bridge-events.js | 10 + dist/default/core/bridge-events.js.map | 1 + dist/default/core/constants.js | 19 + dist/default/core/constants.js.map | 1 + dist/default/core/media-error.js | 33 ++ dist/default/core/media-error.js.map | 1 + .../core/media-tracks/audio-rendition-list.js | 106 ++++ .../media-tracks/audio-rendition-list.js.map | 1 + .../core/media-tracks/audio-rendition.js | 25 + .../core/media-tracks/audio-rendition.js.map | 1 + .../core/media-tracks/audio-track-list.js | 99 ++++ .../core/media-tracks/audio-track-list.js.map | 1 + dist/default/core/media-tracks/audio-track.js | 35 ++ .../core/media-tracks/audio-track.js.map | 1 + .../default/core/media-tracks/change-event.js | 12 + .../core/media-tracks/change-event.js.map | 1 + dist/default/core/media-tracks/mixin.js | 159 ++++++ dist/default/core/media-tracks/mixin.js.map | 1 + .../core/media-tracks/rendition-event.js | 12 + .../core/media-tracks/rendition-event.js.map | 1 + dist/default/core/media-tracks/utils.js | 14 + dist/default/core/media-tracks/utils.js.map | 1 + .../core/media-tracks/video-rendition-list.js | 116 ++++ .../media-tracks/video-rendition-list.js.map | 1 + .../core/media-tracks/video-rendition.js | 37 ++ .../core/media-tracks/video-rendition.js.map | 1 + .../core/media-tracks/video-track-list.js | 109 ++++ .../core/media-tracks/video-track-list.js.map | 1 + dist/default/core/media-tracks/video-track.js | 39 ++ .../core/media-tracks/video-track.js.map | 1 + dist/default/core/predicate.js | 78 +++ dist/default/core/predicate.js.map | 1 + dist/default/core/types.js | 23 + dist/default/core/types.js.map | 1 + dist/default/dom.js | 0 dist/default/dom/audio-host/audio-host.js | 7 + dist/default/dom/audio-host/audio-host.js.map | 1 + dist/default/dom/audio-host/index.js | 2 + .../custom-media-element.js | 295 ++++++++++ .../custom-media-element.js.map | 1 + .../default/dom/custom-media-element/index.js | 2 + dist/default/dom/dash/index.js | 2 + dist/default/dom/dash/media.js | 47 ++ dist/default/dom/dash/media.js.map | 1 + .../dom/google-cast/google-cast-provider.js | 363 ++++++++++++ .../google-cast/google-cast-provider.js.map | 1 + dist/default/dom/google-cast/index.js | 2 + dist/default/dom/google-cast/media.js | 110 ++++ dist/default/dom/google-cast/media.js.map | 1 + dist/default/dom/google-cast/registry.js | 41 ++ dist/default/dom/google-cast/registry.js.map | 1 + .../dom/google-cast/remote-playback.js | 73 +++ .../dom/google-cast/remote-playback.js.map | 1 + dist/default/dom/google-cast/utils.js | 143 +++++ dist/default/dom/google-cast/utils.js.map | 1 + dist/default/dom/hls-js/airplay-bridge.js | 63 +++ dist/default/dom/hls-js/airplay-bridge.js.map | 1 + dist/default/dom/hls-js/errors.js | 58 ++ dist/default/dom/hls-js/errors.js.map | 1 + dist/default/dom/hls-js/hls-js-only.js | 58 ++ dist/default/dom/hls-js/hls-js-only.js.map | 1 + dist/default/dom/hls-js/index.js | 2 + dist/default/dom/hls-js/live.js | 125 +++++ dist/default/dom/hls-js/live.js.map | 1 + dist/default/dom/hls-js/media-tracks.js | 123 +++++ dist/default/dom/hls-js/media-tracks.js.map | 1 + dist/default/dom/hls-js/media.js | 200 +++++++ dist/default/dom/hls-js/media.js.map | 1 + dist/default/dom/hls-js/metadata-tracks.js | 42 ++ .../default/dom/hls-js/metadata-tracks.js.map | 1 + dist/default/dom/hls-js/preload.js | 70 +++ dist/default/dom/hls-js/preload.js.map | 1 + dist/default/dom/hls-js/stream-type.js | 43 ++ dist/default/dom/hls-js/stream-type.js.map | 1 + dist/default/dom/hls-js/text-tracks.js | 104 ++++ dist/default/dom/hls-js/text-tracks.js.map | 1 + dist/default/dom/media-host/index.js | 3 + dist/default/dom/media-host/media-host.js | 214 ++++++++ dist/default/dom/media-host/media-host.js.map | 1 + dist/default/dom/media-played-ranges/index.js | 2 + .../media-played-ranges.js | 112 ++++ .../media-played-ranges.js.map | 1 + dist/default/dom/mux/env.js | 13 + dist/default/dom/mux/env.js.map | 1 + dist/default/dom/mux/index.js | 4 + dist/default/dom/mux/media.js | 67 +++ dist/default/dom/mux/media.js.map | 1 + dist/default/dom/mux/mux-data.js | 195 +++++++ dist/default/dom/mux/mux-data.js.map | 1 + dist/default/dom/mux/utils.js | 98 ++++ dist/default/dom/mux/utils.js.map | 1 + dist/default/dom/native-hls/errors.js | 56 ++ dist/default/dom/native-hls/errors.js.map | 1 + dist/default/dom/native-hls/index.js | 2 + dist/default/dom/native-hls/live.js | 90 +++ dist/default/dom/native-hls/live.js.map | 1 + dist/default/dom/native-hls/m3u8-utils.js | 103 ++++ dist/default/dom/native-hls/m3u8-utils.js.map | 1 + dist/default/dom/native-hls/media.js | 48 ++ dist/default/dom/native-hls/media.js.map | 1 + dist/default/dom/native-hls/stream-type.js | 73 +++ .../default/dom/native-hls/stream-type.js.map | 1 + .../dom/simple-hls-audio-only/index.js | 2 + .../dom/simple-hls-audio-only/media.js | 9 + .../dom/simple-hls-audio-only/media.js.map | 1 + dist/default/dom/simple-hls/index.js | 2 + dist/default/dom/simple-hls/media-tracks.js | 125 +++++ .../dom/simple-hls/media-tracks.js.map | 1 + dist/default/dom/simple-hls/media.js | 11 + dist/default/dom/simple-hls/media.js.map | 1 + dist/default/dom/utils/media-components.js | 41 ++ .../default/dom/utils/media-components.js.map | 1 + dist/default/dom/video-host/index.js | 2 + dist/default/dom/video-host/video-host.js | 72 +++ dist/default/dom/video-host/video-host.js.map | 1 + dist/default/dom/vimeo/index.js | 2 + dist/default/dom/vimeo/media.js | 516 ++++++++++++++++++ dist/default/dom/vimeo/media.js.map | 1 + dist/default/index.js | 5 + dist/dev/core/bridge-events.js | 10 + dist/dev/core/bridge-events.js.map | 1 + dist/dev/core/constants.d.ts | 11 + dist/dev/core/constants.d.ts.map | 1 + dist/dev/core/constants.js | 19 + dist/dev/core/constants.js.map | 1 + dist/dev/core/media-error.d.ts | 29 + dist/dev/core/media-error.d.ts.map | 1 + dist/dev/core/media-error.js | 33 ++ dist/dev/core/media-error.js.map | 1 + .../media-tracks/audio-rendition-list.d.ts | 29 + .../audio-rendition-list.d.ts.map | 1 + .../core/media-tracks/audio-rendition-list.js | 106 ++++ .../media-tracks/audio-rendition-list.js.map | 1 + .../core/media-tracks/audio-rendition.d.ts | 17 + .../media-tracks/audio-rendition.d.ts.map | 1 + dist/dev/core/media-tracks/audio-rendition.js | 25 + .../core/media-tracks/audio-rendition.js.map | 1 + .../core/media-tracks/audio-track-list.d.ts | 28 + .../media-tracks/audio-track-list.d.ts.map | 1 + .../dev/core/media-tracks/audio-track-list.js | 99 ++++ .../core/media-tracks/audio-track-list.js.map | 1 + dist/dev/core/media-tracks/audio-track.d.ts | 18 + .../core/media-tracks/audio-track.d.ts.map | 1 + dist/dev/core/media-tracks/audio-track.js | 35 ++ dist/dev/core/media-tracks/audio-track.js.map | 1 + dist/dev/core/media-tracks/change-event.js | 12 + .../dev/core/media-tracks/change-event.js.map | 1 + dist/dev/core/media-tracks/global.d.ts | 21 + dist/dev/core/media-tracks/global.d.ts.map | 1 + dist/dev/core/media-tracks/index.d.ts | 9 + dist/dev/core/media-tracks/mixin.d.ts | 8 + dist/dev/core/media-tracks/mixin.d.ts.map | 1 + dist/dev/core/media-tracks/mixin.js | 159 ++++++ dist/dev/core/media-tracks/mixin.js.map | 1 + dist/dev/core/media-tracks/rendition-event.js | 12 + .../core/media-tracks/rendition-event.js.map | 1 + dist/dev/core/media-tracks/utils.js | 14 + dist/dev/core/media-tracks/utils.js.map | 1 + .../media-tracks/video-rendition-list.d.ts | 29 + .../video-rendition-list.d.ts.map | 1 + .../core/media-tracks/video-rendition-list.js | 116 ++++ .../media-tracks/video-rendition-list.js.map | 1 + .../core/media-tracks/video-rendition.d.ts | 22 + .../media-tracks/video-rendition.d.ts.map | 1 + dist/dev/core/media-tracks/video-rendition.js | 37 ++ .../core/media-tracks/video-rendition.js.map | 1 + .../core/media-tracks/video-track-list.d.ts | 29 + .../media-tracks/video-track-list.d.ts.map | 1 + .../dev/core/media-tracks/video-track-list.js | 109 ++++ .../core/media-tracks/video-track-list.js.map | 1 + dist/dev/core/media-tracks/video-track.d.ts | 18 + .../core/media-tracks/video-track.d.ts.map | 1 + dist/dev/core/media-tracks/video-track.js | 39 ++ dist/dev/core/media-tracks/video-track.js.map | 1 + dist/dev/core/predicate.d.ts | 31 ++ dist/dev/core/predicate.d.ts.map | 1 + dist/dev/core/predicate.js | 78 +++ dist/dev/core/predicate.js.map | 1 + dist/dev/core/state.d.ts | 353 ++++++++++++ dist/dev/core/state.d.ts.map | 1 + dist/dev/core/types.d.ts | 375 +++++++++++++ dist/dev/core/types.d.ts.map | 1 + dist/dev/core/types.js | 23 + dist/dev/core/types.js.map | 1 + dist/dev/dom.d.ts | 2 + dist/dev/dom.js | 0 dist/dev/dom/audio-host/audio-host.d.ts | 7 + dist/dev/dom/audio-host/audio-host.d.ts.map | 1 + dist/dev/dom/audio-host/audio-host.js | 7 + dist/dev/dom/audio-host/audio-host.js.map | 1 + dist/dev/dom/audio-host/index.d.ts | 2 + dist/dev/dom/audio-host/index.js | 2 + .../custom-media-element.d.ts | 38 ++ .../custom-media-element.d.ts.map | 1 + .../custom-media-element.js | 295 ++++++++++ .../custom-media-element.js.map | 1 + dist/dev/dom/custom-media-element/index.d.ts | 2 + dist/dev/dom/custom-media-element/index.js | 2 + dist/dev/dom/dash/index.d.ts | 2 + dist/dev/dom/dash/index.js | 2 + dist/dev/dom/dash/media.d.ts | 29 + dist/dev/dom/dash/media.d.ts.map | 1 + dist/dev/dom/dash/media.js | 47 ++ dist/dev/dom/dash/media.js.map | 1 + .../dom/google-cast/google-cast-provider.js | 363 ++++++++++++ .../google-cast/google-cast-provider.js.map | 1 + dist/dev/dom/google-cast/index.d.ts | 2 + dist/dev/dom/google-cast/index.js | 2 + dist/dev/dom/google-cast/media.d.ts | 44 ++ dist/dev/dom/google-cast/media.d.ts.map | 1 + dist/dev/dom/google-cast/media.js | 110 ++++ dist/dev/dom/google-cast/media.js.map | 1 + dist/dev/dom/google-cast/registry.js | 44 ++ dist/dev/dom/google-cast/registry.js.map | 1 + dist/dev/dom/google-cast/remote-playback.js | 73 +++ .../dom/google-cast/remote-playback.js.map | 1 + dist/dev/dom/google-cast/utils.js | 143 +++++ dist/dev/dom/google-cast/utils.js.map | 1 + dist/dev/dom/hls-js/airplay-bridge.js | 63 +++ dist/dev/dom/hls-js/airplay-bridge.js.map | 1 + dist/dev/dom/hls-js/errors.js | 58 ++ dist/dev/dom/hls-js/errors.js.map | 1 + dist/dev/dom/hls-js/hls-js-only.js | 58 ++ dist/dev/dom/hls-js/hls-js-only.js.map | 1 + dist/dev/dom/hls-js/index.d.ts | 2 + dist/dev/dom/hls-js/index.js | 2 + dist/dev/dom/hls-js/live.js | 125 +++++ dist/dev/dom/hls-js/live.js.map | 1 + dist/dev/dom/hls-js/media-tracks.js | 123 +++++ dist/dev/dom/hls-js/media-tracks.js.map | 1 + dist/dev/dom/hls-js/media.d.ts | 94 ++++ dist/dev/dom/hls-js/media.d.ts.map | 1 + dist/dev/dom/hls-js/media.js | 200 +++++++ dist/dev/dom/hls-js/media.js.map | 1 + dist/dev/dom/hls-js/metadata-tracks.js | 42 ++ dist/dev/dom/hls-js/metadata-tracks.js.map | 1 + dist/dev/dom/hls-js/preload.js | 70 +++ dist/dev/dom/hls-js/preload.js.map | 1 + dist/dev/dom/hls-js/stream-type.js | 43 ++ dist/dev/dom/hls-js/stream-type.js.map | 1 + dist/dev/dom/hls-js/text-tracks.js | 104 ++++ dist/dev/dom/hls-js/text-tracks.js.map | 1 + dist/dev/dom/media-host/index.d.ts | 3 + dist/dev/dom/media-host/index.js | 3 + dist/dev/dom/media-host/media-host.d.ts | 95 ++++ dist/dev/dom/media-host/media-host.d.ts.map | 1 + dist/dev/dom/media-host/media-host.js | 214 ++++++++ dist/dev/dom/media-host/media-host.js.map | 1 + dist/dev/dom/media-played-ranges/index.d.ts | 2 + dist/dev/dom/media-played-ranges/index.js | 2 + .../media-played-ranges.d.ts | 36 ++ .../media-played-ranges.d.ts.map | 1 + .../media-played-ranges.js | 112 ++++ .../media-played-ranges.js.map | 1 + dist/dev/dom/mux/env.js | 13 + dist/dev/dom/mux/env.js.map | 1 + dist/dev/dom/mux/index.d.ts | 4 + dist/dev/dom/mux/index.js | 4 + dist/dev/dom/mux/media.d.ts | 41 ++ dist/dev/dom/mux/media.d.ts.map | 1 + dist/dev/dom/mux/media.js | 67 +++ dist/dev/dom/mux/media.js.map | 1 + dist/dev/dom/mux/mux-data.d.ts | 56 ++ dist/dev/dom/mux/mux-data.d.ts.map | 1 + dist/dev/dom/mux/mux-data.js | 195 +++++++ dist/dev/dom/mux/mux-data.js.map | 1 + dist/dev/dom/mux/types.d.ts | 3 + dist/dev/dom/mux/utils.d.ts | 112 ++++ dist/dev/dom/mux/utils.d.ts.map | 1 + dist/dev/dom/mux/utils.js | 101 ++++ dist/dev/dom/mux/utils.js.map | 1 + dist/dev/dom/native-hls/errors.js | 56 ++ dist/dev/dom/native-hls/errors.js.map | 1 + dist/dev/dom/native-hls/index.d.ts | 2 + dist/dev/dom/native-hls/index.js | 2 + dist/dev/dom/native-hls/live.js | 90 +++ dist/dev/dom/native-hls/live.js.map | 1 + dist/dev/dom/native-hls/m3u8-utils.js | 103 ++++ dist/dev/dom/native-hls/m3u8-utils.js.map | 1 + dist/dev/dom/native-hls/media.d.ts | 45 ++ dist/dev/dom/native-hls/media.d.ts.map | 1 + dist/dev/dom/native-hls/media.js | 48 ++ dist/dev/dom/native-hls/media.js.map | 1 + dist/dev/dom/native-hls/stream-type.js | 73 +++ dist/dev/dom/native-hls/stream-type.js.map | 1 + dist/dev/dom/simple-hls-audio-only/index.d.ts | 2 + dist/dev/dom/simple-hls-audio-only/index.js | 2 + dist/dev/dom/simple-hls-audio-only/media.d.ts | 10 + .../dom/simple-hls-audio-only/media.d.ts.map | 1 + dist/dev/dom/simple-hls-audio-only/media.js | 9 + .../dom/simple-hls-audio-only/media.js.map | 1 + dist/dev/dom/simple-hls/index.d.ts | 2 + dist/dev/dom/simple-hls/index.js | 2 + dist/dev/dom/simple-hls/media-tracks.js | 125 +++++ dist/dev/dom/simple-hls/media-tracks.js.map | 1 + dist/dev/dom/simple-hls/media.d.ts | 11 + dist/dev/dom/simple-hls/media.d.ts.map | 1 + dist/dev/dom/simple-hls/media.js | 11 + dist/dev/dom/simple-hls/media.js.map | 1 + dist/dev/dom/utils/index.d.ts | 1 + dist/dev/dom/utils/media-components.d.ts | 12 + dist/dev/dom/utils/media-components.d.ts.map | 1 + dist/dev/dom/utils/media-components.js | 41 ++ dist/dev/dom/utils/media-components.js.map | 1 + dist/dev/dom/video-host/index.d.ts | 2 + dist/dev/dom/video-host/index.js | 2 + dist/dev/dom/video-host/video-host.d.ts | 28 + dist/dev/dom/video-host/video-host.d.ts.map | 1 + dist/dev/dom/video-host/video-host.js | 72 +++ dist/dev/dom/video-host/video-host.js.map | 1 + dist/dev/dom/vimeo/index.d.ts | 2 + dist/dev/dom/vimeo/index.js | 2 + dist/dev/dom/vimeo/media.d.ts | 109 ++++ dist/dev/dom/vimeo/media.d.ts.map | 1 + dist/dev/dom/vimeo/media.js | 516 ++++++++++++++++++ dist/dev/dom/vimeo/media.js.map | 1 + dist/dev/index.d.ts | 6 + dist/dev/index.js | 5 + package.json | 55 ++ 320 files changed, 11597 insertions(+) create mode 100644 README.md create mode 100644 dist/default/core/bridge-events.js create mode 100644 dist/default/core/bridge-events.js.map create mode 100644 dist/default/core/constants.js create mode 100644 dist/default/core/constants.js.map create mode 100644 dist/default/core/media-error.js create mode 100644 dist/default/core/media-error.js.map create mode 100644 dist/default/core/media-tracks/audio-rendition-list.js create mode 100644 dist/default/core/media-tracks/audio-rendition-list.js.map create mode 100644 dist/default/core/media-tracks/audio-rendition.js create mode 100644 dist/default/core/media-tracks/audio-rendition.js.map create mode 100644 dist/default/core/media-tracks/audio-track-list.js create mode 100644 dist/default/core/media-tracks/audio-track-list.js.map create mode 100644 dist/default/core/media-tracks/audio-track.js create mode 100644 dist/default/core/media-tracks/audio-track.js.map create mode 100644 dist/default/core/media-tracks/change-event.js create mode 100644 dist/default/core/media-tracks/change-event.js.map create mode 100644 dist/default/core/media-tracks/mixin.js create mode 100644 dist/default/core/media-tracks/mixin.js.map create mode 100644 dist/default/core/media-tracks/rendition-event.js create mode 100644 dist/default/core/media-tracks/rendition-event.js.map create mode 100644 dist/default/core/media-tracks/utils.js create mode 100644 dist/default/core/media-tracks/utils.js.map create mode 100644 dist/default/core/media-tracks/video-rendition-list.js create mode 100644 dist/default/core/media-tracks/video-rendition-list.js.map create mode 100644 dist/default/core/media-tracks/video-rendition.js create mode 100644 dist/default/core/media-tracks/video-rendition.js.map create mode 100644 dist/default/core/media-tracks/video-track-list.js create mode 100644 dist/default/core/media-tracks/video-track-list.js.map create mode 100644 dist/default/core/media-tracks/video-track.js create mode 100644 dist/default/core/media-tracks/video-track.js.map create mode 100644 dist/default/core/predicate.js create mode 100644 dist/default/core/predicate.js.map create mode 100644 dist/default/core/types.js create mode 100644 dist/default/core/types.js.map create mode 100644 dist/default/dom.js create mode 100644 dist/default/dom/audio-host/audio-host.js create mode 100644 dist/default/dom/audio-host/audio-host.js.map create mode 100644 dist/default/dom/audio-host/index.js create mode 100644 dist/default/dom/custom-media-element/custom-media-element.js create mode 100644 dist/default/dom/custom-media-element/custom-media-element.js.map create mode 100644 dist/default/dom/custom-media-element/index.js create mode 100644 dist/default/dom/dash/index.js create mode 100644 dist/default/dom/dash/media.js create mode 100644 dist/default/dom/dash/media.js.map create mode 100644 dist/default/dom/google-cast/google-cast-provider.js create mode 100644 dist/default/dom/google-cast/google-cast-provider.js.map create mode 100644 dist/default/dom/google-cast/index.js create mode 100644 dist/default/dom/google-cast/media.js create mode 100644 dist/default/dom/google-cast/media.js.map create mode 100644 dist/default/dom/google-cast/registry.js create mode 100644 dist/default/dom/google-cast/registry.js.map create mode 100644 dist/default/dom/google-cast/remote-playback.js create mode 100644 dist/default/dom/google-cast/remote-playback.js.map create mode 100644 dist/default/dom/google-cast/utils.js create mode 100644 dist/default/dom/google-cast/utils.js.map create mode 100644 dist/default/dom/hls-js/airplay-bridge.js create mode 100644 dist/default/dom/hls-js/airplay-bridge.js.map create mode 100644 dist/default/dom/hls-js/errors.js create mode 100644 dist/default/dom/hls-js/errors.js.map create mode 100644 dist/default/dom/hls-js/hls-js-only.js create mode 100644 dist/default/dom/hls-js/hls-js-only.js.map create mode 100644 dist/default/dom/hls-js/index.js create mode 100644 dist/default/dom/hls-js/live.js create mode 100644 dist/default/dom/hls-js/live.js.map create mode 100644 dist/default/dom/hls-js/media-tracks.js create mode 100644 dist/default/dom/hls-js/media-tracks.js.map create mode 100644 dist/default/dom/hls-js/media.js create mode 100644 dist/default/dom/hls-js/media.js.map create mode 100644 dist/default/dom/hls-js/metadata-tracks.js create mode 100644 dist/default/dom/hls-js/metadata-tracks.js.map create mode 100644 dist/default/dom/hls-js/preload.js create mode 100644 dist/default/dom/hls-js/preload.js.map create mode 100644 dist/default/dom/hls-js/stream-type.js create mode 100644 dist/default/dom/hls-js/stream-type.js.map create mode 100644 dist/default/dom/hls-js/text-tracks.js create mode 100644 dist/default/dom/hls-js/text-tracks.js.map create mode 100644 dist/default/dom/media-host/index.js create mode 100644 dist/default/dom/media-host/media-host.js create mode 100644 dist/default/dom/media-host/media-host.js.map create mode 100644 dist/default/dom/media-played-ranges/index.js create mode 100644 dist/default/dom/media-played-ranges/media-played-ranges.js create mode 100644 dist/default/dom/media-played-ranges/media-played-ranges.js.map create mode 100644 dist/default/dom/mux/env.js create mode 100644 dist/default/dom/mux/env.js.map create mode 100644 dist/default/dom/mux/index.js create mode 100644 dist/default/dom/mux/media.js create mode 100644 dist/default/dom/mux/media.js.map create mode 100644 dist/default/dom/mux/mux-data.js create mode 100644 dist/default/dom/mux/mux-data.js.map create mode 100644 dist/default/dom/mux/utils.js create mode 100644 dist/default/dom/mux/utils.js.map create mode 100644 dist/default/dom/native-hls/errors.js create mode 100644 dist/default/dom/native-hls/errors.js.map create mode 100644 dist/default/dom/native-hls/index.js create mode 100644 dist/default/dom/native-hls/live.js create mode 100644 dist/default/dom/native-hls/live.js.map create mode 100644 dist/default/dom/native-hls/m3u8-utils.js create mode 100644 dist/default/dom/native-hls/m3u8-utils.js.map create mode 100644 dist/default/dom/native-hls/media.js create mode 100644 dist/default/dom/native-hls/media.js.map create mode 100644 dist/default/dom/native-hls/stream-type.js create mode 100644 dist/default/dom/native-hls/stream-type.js.map create mode 100644 dist/default/dom/simple-hls-audio-only/index.js create mode 100644 dist/default/dom/simple-hls-audio-only/media.js create mode 100644 dist/default/dom/simple-hls-audio-only/media.js.map create mode 100644 dist/default/dom/simple-hls/index.js create mode 100644 dist/default/dom/simple-hls/media-tracks.js create mode 100644 dist/default/dom/simple-hls/media-tracks.js.map create mode 100644 dist/default/dom/simple-hls/media.js create mode 100644 dist/default/dom/simple-hls/media.js.map create mode 100644 dist/default/dom/utils/media-components.js create mode 100644 dist/default/dom/utils/media-components.js.map create mode 100644 dist/default/dom/video-host/index.js create mode 100644 dist/default/dom/video-host/video-host.js create mode 100644 dist/default/dom/video-host/video-host.js.map create mode 100644 dist/default/dom/vimeo/index.js create mode 100644 dist/default/dom/vimeo/media.js create mode 100644 dist/default/dom/vimeo/media.js.map create mode 100644 dist/default/index.js create mode 100644 dist/dev/core/bridge-events.js create mode 100644 dist/dev/core/bridge-events.js.map create mode 100644 dist/dev/core/constants.d.ts create mode 100644 dist/dev/core/constants.d.ts.map create mode 100644 dist/dev/core/constants.js create mode 100644 dist/dev/core/constants.js.map create mode 100644 dist/dev/core/media-error.d.ts create mode 100644 dist/dev/core/media-error.d.ts.map create mode 100644 dist/dev/core/media-error.js create mode 100644 dist/dev/core/media-error.js.map create mode 100644 dist/dev/core/media-tracks/audio-rendition-list.d.ts create mode 100644 dist/dev/core/media-tracks/audio-rendition-list.d.ts.map create mode 100644 dist/dev/core/media-tracks/audio-rendition-list.js create mode 100644 dist/dev/core/media-tracks/audio-rendition-list.js.map create mode 100644 dist/dev/core/media-tracks/audio-rendition.d.ts create mode 100644 dist/dev/core/media-tracks/audio-rendition.d.ts.map create mode 100644 dist/dev/core/media-tracks/audio-rendition.js create mode 100644 dist/dev/core/media-tracks/audio-rendition.js.map create mode 100644 dist/dev/core/media-tracks/audio-track-list.d.ts create mode 100644 dist/dev/core/media-tracks/audio-track-list.d.ts.map create mode 100644 dist/dev/core/media-tracks/audio-track-list.js create mode 100644 dist/dev/core/media-tracks/audio-track-list.js.map create mode 100644 dist/dev/core/media-tracks/audio-track.d.ts create mode 100644 dist/dev/core/media-tracks/audio-track.d.ts.map create mode 100644 dist/dev/core/media-tracks/audio-track.js create mode 100644 dist/dev/core/media-tracks/audio-track.js.map create mode 100644 dist/dev/core/media-tracks/change-event.js create mode 100644 dist/dev/core/media-tracks/change-event.js.map create mode 100644 dist/dev/core/media-tracks/global.d.ts create mode 100644 dist/dev/core/media-tracks/global.d.ts.map create mode 100644 dist/dev/core/media-tracks/index.d.ts create mode 100644 dist/dev/core/media-tracks/mixin.d.ts create mode 100644 dist/dev/core/media-tracks/mixin.d.ts.map create mode 100644 dist/dev/core/media-tracks/mixin.js create mode 100644 dist/dev/core/media-tracks/mixin.js.map create mode 100644 dist/dev/core/media-tracks/rendition-event.js create mode 100644 dist/dev/core/media-tracks/rendition-event.js.map create mode 100644 dist/dev/core/media-tracks/utils.js create mode 100644 dist/dev/core/media-tracks/utils.js.map create mode 100644 dist/dev/core/media-tracks/video-rendition-list.d.ts create mode 100644 dist/dev/core/media-tracks/video-rendition-list.d.ts.map create mode 100644 dist/dev/core/media-tracks/video-rendition-list.js create mode 100644 dist/dev/core/media-tracks/video-rendition-list.js.map create mode 100644 dist/dev/core/media-tracks/video-rendition.d.ts create mode 100644 dist/dev/core/media-tracks/video-rendition.d.ts.map create mode 100644 dist/dev/core/media-tracks/video-rendition.js create mode 100644 dist/dev/core/media-tracks/video-rendition.js.map create mode 100644 dist/dev/core/media-tracks/video-track-list.d.ts create mode 100644 dist/dev/core/media-tracks/video-track-list.d.ts.map create mode 100644 dist/dev/core/media-tracks/video-track-list.js create mode 100644 dist/dev/core/media-tracks/video-track-list.js.map create mode 100644 dist/dev/core/media-tracks/video-track.d.ts create mode 100644 dist/dev/core/media-tracks/video-track.d.ts.map create mode 100644 dist/dev/core/media-tracks/video-track.js create mode 100644 dist/dev/core/media-tracks/video-track.js.map create mode 100644 dist/dev/core/predicate.d.ts create mode 100644 dist/dev/core/predicate.d.ts.map create mode 100644 dist/dev/core/predicate.js create mode 100644 dist/dev/core/predicate.js.map create mode 100644 dist/dev/core/state.d.ts create mode 100644 dist/dev/core/state.d.ts.map create mode 100644 dist/dev/core/types.d.ts create mode 100644 dist/dev/core/types.d.ts.map create mode 100644 dist/dev/core/types.js create mode 100644 dist/dev/core/types.js.map create mode 100644 dist/dev/dom.d.ts create mode 100644 dist/dev/dom.js create mode 100644 dist/dev/dom/audio-host/audio-host.d.ts create mode 100644 dist/dev/dom/audio-host/audio-host.d.ts.map create mode 100644 dist/dev/dom/audio-host/audio-host.js create mode 100644 dist/dev/dom/audio-host/audio-host.js.map create mode 100644 dist/dev/dom/audio-host/index.d.ts create mode 100644 dist/dev/dom/audio-host/index.js create mode 100644 dist/dev/dom/custom-media-element/custom-media-element.d.ts create mode 100644 dist/dev/dom/custom-media-element/custom-media-element.d.ts.map create mode 100644 dist/dev/dom/custom-media-element/custom-media-element.js create mode 100644 dist/dev/dom/custom-media-element/custom-media-element.js.map create mode 100644 dist/dev/dom/custom-media-element/index.d.ts create mode 100644 dist/dev/dom/custom-media-element/index.js create mode 100644 dist/dev/dom/dash/index.d.ts create mode 100644 dist/dev/dom/dash/index.js create mode 100644 dist/dev/dom/dash/media.d.ts create mode 100644 dist/dev/dom/dash/media.d.ts.map create mode 100644 dist/dev/dom/dash/media.js create mode 100644 dist/dev/dom/dash/media.js.map create mode 100644 dist/dev/dom/google-cast/google-cast-provider.js create mode 100644 dist/dev/dom/google-cast/google-cast-provider.js.map create mode 100644 dist/dev/dom/google-cast/index.d.ts create mode 100644 dist/dev/dom/google-cast/index.js create mode 100644 dist/dev/dom/google-cast/media.d.ts create mode 100644 dist/dev/dom/google-cast/media.d.ts.map create mode 100644 dist/dev/dom/google-cast/media.js create mode 100644 dist/dev/dom/google-cast/media.js.map create mode 100644 dist/dev/dom/google-cast/registry.js create mode 100644 dist/dev/dom/google-cast/registry.js.map create mode 100644 dist/dev/dom/google-cast/remote-playback.js create mode 100644 dist/dev/dom/google-cast/remote-playback.js.map create mode 100644 dist/dev/dom/google-cast/utils.js create mode 100644 dist/dev/dom/google-cast/utils.js.map create mode 100644 dist/dev/dom/hls-js/airplay-bridge.js create mode 100644 dist/dev/dom/hls-js/airplay-bridge.js.map create mode 100644 dist/dev/dom/hls-js/errors.js create mode 100644 dist/dev/dom/hls-js/errors.js.map create mode 100644 dist/dev/dom/hls-js/hls-js-only.js create mode 100644 dist/dev/dom/hls-js/hls-js-only.js.map create mode 100644 dist/dev/dom/hls-js/index.d.ts create mode 100644 dist/dev/dom/hls-js/index.js create mode 100644 dist/dev/dom/hls-js/live.js create mode 100644 dist/dev/dom/hls-js/live.js.map create mode 100644 dist/dev/dom/hls-js/media-tracks.js create mode 100644 dist/dev/dom/hls-js/media-tracks.js.map create mode 100644 dist/dev/dom/hls-js/media.d.ts create mode 100644 dist/dev/dom/hls-js/media.d.ts.map create mode 100644 dist/dev/dom/hls-js/media.js create mode 100644 dist/dev/dom/hls-js/media.js.map create mode 100644 dist/dev/dom/hls-js/metadata-tracks.js create mode 100644 dist/dev/dom/hls-js/metadata-tracks.js.map create mode 100644 dist/dev/dom/hls-js/preload.js create mode 100644 dist/dev/dom/hls-js/preload.js.map create mode 100644 dist/dev/dom/hls-js/stream-type.js create mode 100644 dist/dev/dom/hls-js/stream-type.js.map create mode 100644 dist/dev/dom/hls-js/text-tracks.js create mode 100644 dist/dev/dom/hls-js/text-tracks.js.map create mode 100644 dist/dev/dom/media-host/index.d.ts create mode 100644 dist/dev/dom/media-host/index.js create mode 100644 dist/dev/dom/media-host/media-host.d.ts create mode 100644 dist/dev/dom/media-host/media-host.d.ts.map create mode 100644 dist/dev/dom/media-host/media-host.js create mode 100644 dist/dev/dom/media-host/media-host.js.map create mode 100644 dist/dev/dom/media-played-ranges/index.d.ts create mode 100644 dist/dev/dom/media-played-ranges/index.js create mode 100644 dist/dev/dom/media-played-ranges/media-played-ranges.d.ts create mode 100644 dist/dev/dom/media-played-ranges/media-played-ranges.d.ts.map create mode 100644 dist/dev/dom/media-played-ranges/media-played-ranges.js create mode 100644 dist/dev/dom/media-played-ranges/media-played-ranges.js.map create mode 100644 dist/dev/dom/mux/env.js create mode 100644 dist/dev/dom/mux/env.js.map create mode 100644 dist/dev/dom/mux/index.d.ts create mode 100644 dist/dev/dom/mux/index.js create mode 100644 dist/dev/dom/mux/media.d.ts create mode 100644 dist/dev/dom/mux/media.d.ts.map create mode 100644 dist/dev/dom/mux/media.js create mode 100644 dist/dev/dom/mux/media.js.map create mode 100644 dist/dev/dom/mux/mux-data.d.ts create mode 100644 dist/dev/dom/mux/mux-data.d.ts.map create mode 100644 dist/dev/dom/mux/mux-data.js create mode 100644 dist/dev/dom/mux/mux-data.js.map create mode 100644 dist/dev/dom/mux/types.d.ts create mode 100644 dist/dev/dom/mux/utils.d.ts create mode 100644 dist/dev/dom/mux/utils.d.ts.map create mode 100644 dist/dev/dom/mux/utils.js create mode 100644 dist/dev/dom/mux/utils.js.map create mode 100644 dist/dev/dom/native-hls/errors.js create mode 100644 dist/dev/dom/native-hls/errors.js.map create mode 100644 dist/dev/dom/native-hls/index.d.ts create mode 100644 dist/dev/dom/native-hls/index.js create mode 100644 dist/dev/dom/native-hls/live.js create mode 100644 dist/dev/dom/native-hls/live.js.map create mode 100644 dist/dev/dom/native-hls/m3u8-utils.js create mode 100644 dist/dev/dom/native-hls/m3u8-utils.js.map create mode 100644 dist/dev/dom/native-hls/media.d.ts create mode 100644 dist/dev/dom/native-hls/media.d.ts.map create mode 100644 dist/dev/dom/native-hls/media.js create mode 100644 dist/dev/dom/native-hls/media.js.map create mode 100644 dist/dev/dom/native-hls/stream-type.js create mode 100644 dist/dev/dom/native-hls/stream-type.js.map create mode 100644 dist/dev/dom/simple-hls-audio-only/index.d.ts create mode 100644 dist/dev/dom/simple-hls-audio-only/index.js create mode 100644 dist/dev/dom/simple-hls-audio-only/media.d.ts create mode 100644 dist/dev/dom/simple-hls-audio-only/media.d.ts.map create mode 100644 dist/dev/dom/simple-hls-audio-only/media.js create mode 100644 dist/dev/dom/simple-hls-audio-only/media.js.map create mode 100644 dist/dev/dom/simple-hls/index.d.ts create mode 100644 dist/dev/dom/simple-hls/index.js create mode 100644 dist/dev/dom/simple-hls/media-tracks.js create mode 100644 dist/dev/dom/simple-hls/media-tracks.js.map create mode 100644 dist/dev/dom/simple-hls/media.d.ts create mode 100644 dist/dev/dom/simple-hls/media.d.ts.map create mode 100644 dist/dev/dom/simple-hls/media.js create mode 100644 dist/dev/dom/simple-hls/media.js.map create mode 100644 dist/dev/dom/utils/index.d.ts create mode 100644 dist/dev/dom/utils/media-components.d.ts create mode 100644 dist/dev/dom/utils/media-components.d.ts.map create mode 100644 dist/dev/dom/utils/media-components.js create mode 100644 dist/dev/dom/utils/media-components.js.map create mode 100644 dist/dev/dom/video-host/index.d.ts create mode 100644 dist/dev/dom/video-host/index.js create mode 100644 dist/dev/dom/video-host/video-host.d.ts create mode 100644 dist/dev/dom/video-host/video-host.d.ts.map create mode 100644 dist/dev/dom/video-host/video-host.js create mode 100644 dist/dev/dom/video-host/video-host.js.map create mode 100644 dist/dev/dom/vimeo/index.d.ts create mode 100644 dist/dev/dom/vimeo/index.js create mode 100644 dist/dev/dom/vimeo/media.d.ts create mode 100644 dist/dev/dom/vimeo/media.d.ts.map create mode 100644 dist/dev/dom/vimeo/media.js create mode 100644 dist/dev/dom/vimeo/media.js.map create mode 100644 dist/dev/index.d.ts create mode 100644 dist/dev/index.js create mode 100644 package.json diff --git a/README.md b/README.md new file mode 100644 index 00000000..1a50d985 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# @videojs/media + +[![package-badge]][package] + +> **⚠️ Beta** Close to stable. Experimental adoption in real projects. + +## Overview + +`@videojs/media` provides the media contracts, state types, DOM hosts, and playback engine adapters +used by Video.js packages. + +Runtime-agnostic media APIs are exported from `@videojs/media`. Browser-specific APIs and playback +engines are available from the `@videojs/media/dom` subpaths. + +## Community + +If you need help with anything related to Video.js v10, or if you'd like to casually chat with other +members: + +- [Join Discord Server][discord] +- [See GitHub Discussions][gh-discussions] + +## License + +[Apache-2.0](./LICENSE) + +[package]: https://www.npmjs.com/package/@videojs/media +[package-badge]: https://img.shields.io/npm/v/@videojs/media?label=@videojs/media +[discord]: https://discord.gg/JBqHh485uF +[gh-discussions]: https://github.com/videojs/v10/discussions diff --git a/dist/default/core/bridge-events.js b/dist/default/core/bridge-events.js new file mode 100644 index 00000000..6390be1e --- /dev/null +++ b/dist/default/core/bridge-events.js @@ -0,0 +1,10 @@ +//#region src/core/bridge-events.ts +/** Wrap `source.dispatchEvent` so every event is also re-dispatched on `target`. */ +function bridgeEvents(source, target) { + if (!source.dispatchEvent) return; + source.dispatchEvent = (event) => target.dispatchEvent(new event.constructor(event.type, event)); +} +//#endregion +export { bridgeEvents }; + +//# sourceMappingURL=bridge-events.js.map \ No newline at end of file diff --git a/dist/default/core/bridge-events.js.map b/dist/default/core/bridge-events.js.map new file mode 100644 index 00000000..06585f3f --- /dev/null +++ b/dist/default/core/bridge-events.js.map @@ -0,0 +1 @@ +{"version":3,"file":"bridge-events.js","names":[],"sources":["../../../src/core/bridge-events.ts"],"sourcesContent":["/** Wrap `source.dispatchEvent` so every event is also re-dispatched on `target`. */\nexport function bridgeEvents(source: EventTarget, target: EventTarget): void {\n if (!source.dispatchEvent) return;\n source.dispatchEvent = (event: Event) =>\n target.dispatchEvent(new (event.constructor as typeof Event)(event.type, event));\n}\n"],"mappings":";;AACA,SAAgB,aAAa,QAAqB,QAA2B;AAC3E,KAAI,CAAC,OAAO,cAAe;AAC3B,QAAO,iBAAiB,UACtB,OAAO,cAAc,IAAK,MAAM,YAA6B,MAAM,MAAM,MAAM,CAAC"} \ No newline at end of file diff --git a/dist/default/core/constants.js b/dist/default/core/constants.js new file mode 100644 index 00000000..ea325610 --- /dev/null +++ b/dist/default/core/constants.js @@ -0,0 +1,19 @@ +//#region src/core/constants.ts +/** A frozen, empty `TimeRanges`-like value for hosts with no ranges. */ +const EMPTY_TIME_RANGES = Object.freeze({ + length: 0, + start: () => 0, + end: () => 0 +}); +/** A frozen, empty `TextTrackList`-like value for hosts with no text tracks. */ +const EMPTY_TEXT_TRACKS = Object.assign(new EventTarget(), { + length: 0, + *[Symbol.iterator]() {}, + getTrackById: () => null +}); +const EMPTY_REMOTE = new EventTarget(); +Object.freeze({}); +//#endregion +export { EMPTY_REMOTE, EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES }; + +//# sourceMappingURL=constants.js.map \ No newline at end of file diff --git a/dist/default/core/constants.js.map b/dist/default/core/constants.js.map new file mode 100644 index 00000000..66d6e6d4 --- /dev/null +++ b/dist/default/core/constants.js.map @@ -0,0 +1 @@ +{"version":3,"file":"constants.js","names":[],"sources":["../../../src/core/constants.ts"],"sourcesContent":["import type { RemotePlaybackLike, TextTrackListLike, TimeRangeLike } from './types';\n\n/** A frozen, empty `TimeRanges`-like value for hosts with no ranges. */\nexport const EMPTY_TIME_RANGES: TimeRangeLike = Object.freeze({\n length: 0,\n start: () => 0,\n end: () => 0,\n});\n\n/** A frozen, empty `TextTrackList`-like value for hosts with no text tracks. */\nexport const EMPTY_TEXT_TRACKS: TextTrackListLike = Object.assign(new EventTarget(), {\n length: 0,\n *[Symbol.iterator]() {},\n getTrackById: () => null,\n}) as unknown as TextTrackListLike;\n\nexport const EMPTY_REMOTE = new EventTarget() as unknown as RemotePlaybackLike;\n\nexport const EMPTY_CONFIG: Record = Object.freeze({});\n"],"mappings":";;AAGA,MAAa,oBAAmC,OAAO,OAAO;CAC5D,QAAQ;CACR,aAAa;CACb,WAAW;CACZ,CAAC;;AAGF,MAAa,oBAAuC,OAAO,OAAO,IAAI,aAAa,EAAE;CACnF,QAAQ;CACR,EAAE,OAAO,YAAY;CACrB,oBAAoB;CACrB,CAAC;AAEF,MAAa,eAAe,IAAI,aAAa;AAEQ,OAAO,OAAO,EAAE,CAAC"} \ No newline at end of file diff --git a/dist/default/core/media-error.js b/dist/default/core/media-error.js new file mode 100644 index 00000000..df88e7c0 --- /dev/null +++ b/dist/default/core/media-error.js @@ -0,0 +1,33 @@ +//#region src/core/media-error.ts +var MediaError = class MediaError extends Error { + static MEDIA_ERR_ABORTED = 1; + static MEDIA_ERR_NETWORK = 2; + static MEDIA_ERR_DECODE = 3; + static MEDIA_ERR_SRC_NOT_SUPPORTED = 4; + static MEDIA_ERR_ENCRYPTED = 5; + static MEDIA_ERR_CUSTOM = 100; + static defaultMessages = { + 1: "You stopped media playback before it finished.", + 2: "This media could not be loaded due to a network or server issue.", + 3: "This media could not be played. It may be corrupted, or your browser may not support its format.", + 4: "This media could not be loaded. It may be unavailable, or your browser may not support its format.", + 5: "This media could not be played because it could not be decrypted." + }; + name; + code; + context; + fatal; + data; + constructor(message, code = MediaError.MEDIA_ERR_CUSTOM, fatal, context) { + super(message); + this.name = "MediaError"; + this.code = code; + this.context = context; + this.fatal = fatal ?? (code >= MediaError.MEDIA_ERR_NETWORK && code <= MediaError.MEDIA_ERR_ENCRYPTED); + if (!this.message) this.message = MediaError.defaultMessages[this.code] ?? ""; + } +}; +//#endregion +export { MediaError }; + +//# sourceMappingURL=media-error.js.map \ No newline at end of file diff --git a/dist/default/core/media-error.js.map b/dist/default/core/media-error.js.map new file mode 100644 index 00000000..74d45e88 --- /dev/null +++ b/dist/default/core/media-error.js.map @@ -0,0 +1 @@ +{"version":3,"file":"media-error.js","names":[],"sources":["../../../src/core/media-error.ts"],"sourcesContent":["// Typescript says it's strictly a string, but it can also be a number or an object with a toString method.\n// https://github.com/microsoft/TypeScript/issues/6032\n// https://262.ecma-international.org/6.0/#sec-error-message\n\ntype Stringable = string | { toString(): string };\n\ndeclare global {\n interface ErrorConstructor {\n new (message?: Stringable): Error;\n (message?: Stringable): Error;\n readonly prototype: Error;\n }\n}\n\nexport class MediaError extends Error {\n static MEDIA_ERR_ABORTED = 1 as const;\n static MEDIA_ERR_NETWORK = 2 as const;\n static MEDIA_ERR_DECODE = 3 as const;\n static MEDIA_ERR_SRC_NOT_SUPPORTED = 4 as const;\n static MEDIA_ERR_ENCRYPTED = 5 as const;\n // Technically this is Mux specific but it's generic enough to be used here.\n // @see https://docs.mux.com/guides/data/monitor-html5-video-element#customize-error-tracking-behavior\n static MEDIA_ERR_CUSTOM = 100 as const;\n\n static defaultMessages: Record = {\n 1: 'You stopped media playback before it finished.',\n 2: 'This media could not be loaded due to a network or server issue.',\n 3: 'This media could not be played. It may be corrupted, or your browser may not support its format.',\n 4: 'This media could not be loaded. It may be unavailable, or your browser may not support its format.',\n 5: 'This media could not be played because it could not be decrypted.',\n };\n\n name: string;\n code: number;\n context: string | undefined;\n fatal: boolean;\n data?: any;\n\n constructor(message?: Stringable, code: number = MediaError.MEDIA_ERR_CUSTOM, fatal?: boolean, context?: string) {\n super(message);\n this.name = 'MediaError';\n this.code = code;\n this.context = context;\n this.fatal = fatal ?? (code >= MediaError.MEDIA_ERR_NETWORK && code <= MediaError.MEDIA_ERR_ENCRYPTED);\n\n if (!this.message) {\n this.message = MediaError.defaultMessages[this.code] ?? '';\n }\n }\n}\n"],"mappings":";AAcA,IAAa,aAAb,MAAa,mBAAmB,MAAM;CACpC,OAAO,oBAAoB;CAC3B,OAAO,oBAAoB;CAC3B,OAAO,mBAAmB;CAC1B,OAAO,8BAA8B;CACrC,OAAO,sBAAsB;CAG7B,OAAO,mBAAmB;CAE1B,OAAO,kBAA0C;EAC/C,GAAG;EACH,GAAG;EACH,GAAG;EACH,GAAG;EACH,GAAG;EACJ;CAED;CACA;CACA;CACA;CACA;CAEA,YAAY,SAAsB,OAAe,WAAW,kBAAkB,OAAiB,SAAkB;AAC/G,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,OAAO;AACZ,OAAK,UAAU;AACf,OAAK,QAAQ,UAAU,QAAQ,WAAW,qBAAqB,QAAQ,WAAW;AAElF,MAAI,CAAC,KAAK,QACR,MAAK,UAAU,WAAW,gBAAgB,KAAK,SAAS"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/audio-rendition-list.js b/dist/default/core/media-tracks/audio-rendition-list.js new file mode 100644 index 00000000..12a64ef5 --- /dev/null +++ b/dist/default/core/media-tracks/audio-rendition-list.js @@ -0,0 +1,106 @@ +import { RenditionEvent } from "./rendition-event.js"; +import { getPrivate } from "./utils.js"; +import { isFunction } from "@videojs/utils/predicate"; +//#region src/core/media-tracks/audio-rendition-list.ts +function addRendition(track, rendition) { + const renditionList = getPrivate(track).media?.deref()?.audioRenditions; + getPrivate(rendition).media = getPrivate(track).media; + getPrivate(rendition).track = track; + const renditionSet = getPrivate(track).renditionSet; + renditionSet.add(rendition); + const index = renditionSet.size - 1; + if (!(index in AudioRenditionList.prototype)) Object.defineProperty(AudioRenditionList.prototype, index, { get() { + return getCurrentRenditions(this)[index]; + } }); + queueMicrotask(() => { + if (!renditionList || !track.enabled) return; + renditionList.dispatchEvent(new RenditionEvent("addrendition", { rendition })); + }); +} +function removeRendition(rendition) { + const renditionList = getPrivate(rendition).media?.deref()?.audioRenditions; + const track = getPrivate(rendition).track; + getPrivate(track).renditionSet.delete(rendition); + queueMicrotask(() => { + if (!renditionList || !track.enabled) return; + renditionList.dispatchEvent(new RenditionEvent("removerendition", { rendition })); + }); +} +function selectedChanged(rendition) { + const renditionList = getPrivate(rendition).media?.deref()?.audioRenditions; + if (!renditionList || getPrivate(renditionList).changeRequested) return; + getPrivate(renditionList).changeRequested = true; + queueMicrotask(() => { + delete getPrivate(renditionList).changeRequested; + if (!getPrivate(rendition).track.enabled) return; + renditionList.dispatchEvent(new Event("change")); + }); +} +function getCurrentRenditions(renditionList) { + const media = getPrivate(renditionList).media?.deref(); + if (!media) return []; + return [...media.audioTracks].filter((track) => track.enabled).flatMap((track) => [...getPrivate(track).renditionSet]); +} +var AudioRenditionList = class extends EventTarget { + #addRenditionCallback; + #removeRenditionCallback; + #changeCallback; + [Symbol.iterator]() { + return getCurrentRenditions(this).values(); + } + get length() { + return getCurrentRenditions(this).length; + } + getRenditionById(id) { + return getCurrentRenditions(this).find((rendition) => `${rendition.id}` === `${id}`) ?? null; + } + get selectedIndex() { + return getCurrentRenditions(this).findIndex((rendition) => rendition.selected); + } + set selectedIndex(index) { + for (const [i, rendition] of getCurrentRenditions(this).entries()) rendition.selected = i === index; + } + get onaddrendition() { + return this.#addRenditionCallback; + } + set onaddrendition(callback) { + if (this.#addRenditionCallback) { + this.removeEventListener("addrendition", this.#addRenditionCallback); + this.#addRenditionCallback = void 0; + } + if (isFunction(callback)) { + this.#addRenditionCallback = callback; + this.addEventListener("addrendition", callback); + } + } + get onremoverendition() { + return this.#removeRenditionCallback; + } + set onremoverendition(callback) { + if (this.#removeRenditionCallback) { + this.removeEventListener("removerendition", this.#removeRenditionCallback); + this.#removeRenditionCallback = void 0; + } + if (isFunction(callback)) { + this.#removeRenditionCallback = callback; + this.addEventListener("removerendition", callback); + } + } + get onchange() { + return this.#changeCallback; + } + set onchange(callback) { + if (this.#changeCallback) { + this.removeEventListener("change", this.#changeCallback); + this.#changeCallback = void 0; + } + if (isFunction(callback)) { + this.#changeCallback = callback; + this.addEventListener("change", callback); + } + } +}; +//#endregion +export { AudioRenditionList, addRendition, removeRendition, selectedChanged }; + +//# sourceMappingURL=audio-rendition-list.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/audio-rendition-list.js.map b/dist/default/core/media-tracks/audio-rendition-list.js.map new file mode 100644 index 00000000..c6142aa2 --- /dev/null +++ b/dist/default/core/media-tracks/audio-rendition-list.js.map @@ -0,0 +1 @@ +{"version":3,"file":"audio-rendition-list.js","names":["#addRenditionCallback","#removeRenditionCallback","#changeCallback"],"sources":["../../../../src/core/media-tracks/audio-rendition-list.ts"],"sourcesContent":["import { isFunction } from '@videojs/utils/predicate';\nimport type { AudioRendition } from './audio-rendition';\nimport type { AudioTrack } from './audio-track';\nimport { RenditionEvent } from './rendition-event';\nimport { getPrivate } from './utils';\n\nexport function addRendition(track: AudioTrack, rendition: AudioRendition) {\n const renditionList = getPrivate(track).media?.deref()?.audioRenditions as AudioRenditionList | undefined;\n\n getPrivate(rendition).media = getPrivate(track).media;\n getPrivate(rendition).track = track;\n\n const renditionSet = getPrivate(track).renditionSet as Set;\n renditionSet.add(rendition);\n const index = renditionSet.size - 1;\n\n if (!(index in AudioRenditionList.prototype)) {\n Object.defineProperty(AudioRenditionList.prototype, index, {\n get() {\n return getCurrentRenditions(this)[index];\n },\n });\n }\n\n queueMicrotask(() => {\n if (!renditionList || !track.enabled) return;\n\n renditionList.dispatchEvent(new RenditionEvent('addrendition', { rendition }));\n });\n}\n\nexport function removeRendition(rendition: AudioRendition) {\n const renditionList = getPrivate(rendition).media?.deref()?.audioRenditions as AudioRenditionList | undefined;\n const track = getPrivate(rendition).track as AudioTrack;\n const renditionSet = getPrivate(track).renditionSet as Set;\n renditionSet.delete(rendition);\n\n queueMicrotask(() => {\n if (!renditionList || !track.enabled) return;\n\n renditionList.dispatchEvent(new RenditionEvent('removerendition', { rendition }));\n });\n}\n\nexport function selectedChanged(rendition: AudioRendition) {\n const renditionList = getPrivate(rendition).media?.deref()?.audioRenditions as AudioRenditionList | undefined;\n\n // Prevent firing a rendition list `change` event multiple times per tick.\n if (!renditionList || getPrivate(renditionList).changeRequested) return;\n getPrivate(renditionList).changeRequested = true;\n\n queueMicrotask(() => {\n delete getPrivate(renditionList).changeRequested;\n\n const track = getPrivate(rendition).track as AudioTrack;\n if (!track.enabled) return;\n\n renditionList.dispatchEvent(new Event('change'));\n });\n}\n\nfunction getCurrentRenditions(renditionList: AudioRenditionList): AudioRendition[] {\n const media = getPrivate(renditionList).media?.deref() as HTMLMediaElement | undefined;\n if (!media) return [];\n return [...media.audioTracks]\n .filter((track) => track.enabled)\n .flatMap((track) => [...(getPrivate(track).renditionSet as Set)]);\n}\n\nexport class AudioRenditionList extends EventTarget {\n [index: number]: AudioRendition;\n #addRenditionCallback: (() => void) | undefined;\n #removeRenditionCallback: (() => void) | undefined;\n #changeCallback: (() => void) | undefined;\n\n [Symbol.iterator]() {\n return getCurrentRenditions(this).values();\n }\n\n get length() {\n return getCurrentRenditions(this).length;\n }\n\n getRenditionById(id: string): AudioRendition | null {\n return getCurrentRenditions(this).find((rendition) => `${rendition.id}` === `${id}`) ?? null;\n }\n\n get selectedIndex() {\n return getCurrentRenditions(this).findIndex((rendition) => rendition.selected);\n }\n\n set selectedIndex(index) {\n for (const [i, rendition] of getCurrentRenditions(this).entries()) {\n rendition.selected = i === index;\n }\n }\n\n get onaddrendition() {\n return this.#addRenditionCallback;\n }\n\n set onaddrendition(callback: ((event?: { rendition: AudioRendition }) => void) | undefined) {\n if (this.#addRenditionCallback) {\n this.removeEventListener('addrendition', this.#addRenditionCallback);\n this.#addRenditionCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#addRenditionCallback = callback;\n this.addEventListener('addrendition', callback as unknown as EventListener);\n }\n }\n\n get onremoverendition() {\n return this.#removeRenditionCallback;\n }\n\n set onremoverendition(callback: ((event?: { rendition: AudioRendition }) => void) | undefined) {\n if (this.#removeRenditionCallback) {\n this.removeEventListener('removerendition', this.#removeRenditionCallback);\n this.#removeRenditionCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#removeRenditionCallback = callback;\n this.addEventListener('removerendition', callback as unknown as EventListener);\n }\n }\n\n get onchange() {\n return this.#changeCallback;\n }\n\n set onchange(callback) {\n if (this.#changeCallback) {\n this.removeEventListener('change', this.#changeCallback);\n this.#changeCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#changeCallback = callback;\n this.addEventListener('change', callback);\n }\n }\n}\n"],"mappings":"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"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/audio-rendition.js b/dist/default/core/media-tracks/audio-rendition.js new file mode 100644 index 00000000..7e7e5993 --- /dev/null +++ b/dist/default/core/media-tracks/audio-rendition.js @@ -0,0 +1,25 @@ +import { selectedChanged } from "./audio-rendition-list.js"; +//#region src/core/media-tracks/audio-rendition.ts +/** +* The consumer should use the `selected` setter to select one or multiple +* renditions that the engine is allowed to play. +*/ +var AudioRendition = class { + src; + id; + bitrate; + codec; + #selected = false; + get selected() { + return this.#selected; + } + set selected(value) { + if (this.#selected === value) return; + this.#selected = value; + selectedChanged(this); + } +}; +//#endregion +export { AudioRendition }; + +//# sourceMappingURL=audio-rendition.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/audio-rendition.js.map b/dist/default/core/media-tracks/audio-rendition.js.map new file mode 100644 index 00000000..bc4047ff --- /dev/null +++ b/dist/default/core/media-tracks/audio-rendition.js.map @@ -0,0 +1 @@ +{"version":3,"file":"audio-rendition.js","names":["#selected"],"sources":["../../../../src/core/media-tracks/audio-rendition.ts"],"sourcesContent":["import { selectedChanged } from './audio-rendition-list';\n\n/**\n * The consumer should use the `selected` setter to select one or multiple\n * renditions that the engine is allowed to play.\n */\nexport class AudioRendition {\n src: string | undefined;\n id: string | undefined;\n bitrate: number | undefined;\n codec: string | undefined;\n #selected = false;\n\n get selected(): boolean {\n return this.#selected;\n }\n\n set selected(value: boolean) {\n if (this.#selected === value) return;\n this.#selected = value;\n\n selectedChanged(this);\n }\n}\n"],"mappings":";;;;;;AAMA,IAAa,iBAAb,MAA4B;CAC1B;CACA;CACA;CACA;CACA,YAAY;CAEZ,IAAI,WAAoB;AACtB,SAAO,MAAA;;CAGT,IAAI,SAAS,OAAgB;AAC3B,MAAI,MAAA,aAAmB,MAAO;AAC9B,QAAA,WAAiB;AAEjB,kBAAgB,KAAK"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/audio-track-list.js b/dist/default/core/media-tracks/audio-track-list.js new file mode 100644 index 00000000..ed07d5e0 --- /dev/null +++ b/dist/default/core/media-tracks/audio-track-list.js @@ -0,0 +1,99 @@ +import { getPrivate } from "./utils.js"; +import { TrackEvent } from "./change-event.js"; +import { isFunction } from "@videojs/utils/predicate"; +//#region src/core/media-tracks/audio-track-list.ts +function addAudioTrack(media, track) { + const trackList = media.audioTracks; + getPrivate(track).media = new WeakRef(media); + if (!getPrivate(track).renditionSet) getPrivate(track).renditionSet = /* @__PURE__ */ new Set(); + const trackSet = getPrivate(trackList).trackSet; + trackSet.add(track); + const index = trackSet.size - 1; + if (!(index in AudioTrackList.prototype)) Object.defineProperty(AudioTrackList.prototype, index, { get() { + return [...getPrivate(this).trackSet][index]; + } }); + queueMicrotask(() => { + trackList.dispatchEvent(new TrackEvent("addtrack", { track })); + }); +} +function removeAudioTrack(track) { + const trackList = getPrivate(track).media?.deref()?.audioTracks; + if (!trackList) return; + if (!getPrivate(trackList).trackSet.delete(track)) return; + queueMicrotask(() => { + trackList.dispatchEvent(new TrackEvent("removetrack", { track })); + }); +} +function enabledChanged(track) { + const trackList = getPrivate(track).media?.deref()?.audioTracks; + if (!trackList || getPrivate(trackList).changeRequested) return; + getPrivate(trackList).changeRequested = true; + queueMicrotask(() => { + delete getPrivate(trackList).changeRequested; + trackList.dispatchEvent(new Event("change")); + }); +} +var AudioTrackList = class extends EventTarget { + #addTrackCallback; + #removeTrackCallback; + #changeCallback; + constructor() { + super(); + getPrivate(this).trackSet = /* @__PURE__ */ new Set(); + } + get #tracks() { + return getPrivate(this).trackSet; + } + [Symbol.iterator]() { + return this.#tracks.values(); + } + get length() { + return this.#tracks.size; + } + getTrackById(id) { + return [...this.#tracks].find((track) => track.id === id) ?? null; + } + get onaddtrack() { + return this.#addTrackCallback; + } + set onaddtrack(callback) { + if (this.#addTrackCallback) { + this.removeEventListener("addtrack", this.#addTrackCallback); + this.#addTrackCallback = void 0; + } + if (isFunction(callback)) { + this.#addTrackCallback = callback; + this.addEventListener("addtrack", callback); + } + } + get onremovetrack() { + return this.#removeTrackCallback; + } + set onremovetrack(callback) { + if (this.#removeTrackCallback) { + this.removeEventListener("removetrack", this.#removeTrackCallback); + this.#removeTrackCallback = void 0; + } + if (isFunction(callback)) { + this.#removeTrackCallback = callback; + this.addEventListener("removetrack", callback); + } + } + get onchange() { + return this.#changeCallback; + } + set onchange(callback) { + if (this.#changeCallback) { + this.removeEventListener("change", this.#changeCallback); + this.#changeCallback = void 0; + } + if (isFunction(callback)) { + this.#changeCallback = callback; + this.addEventListener("change", callback); + } + } +}; +//#endregion +export { AudioTrackList, addAudioTrack, enabledChanged, removeAudioTrack }; + +//# sourceMappingURL=audio-track-list.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/audio-track-list.js.map b/dist/default/core/media-tracks/audio-track-list.js.map new file mode 100644 index 00000000..7fc6aa45 --- /dev/null +++ b/dist/default/core/media-tracks/audio-track-list.js.map @@ -0,0 +1 @@ +{"version":3,"file":"audio-track-list.js","names":["#tracks","#addTrackCallback","#removeTrackCallback","#changeCallback"],"sources":["../../../../src/core/media-tracks/audio-track-list.ts"],"sourcesContent":["import { isFunction } from '@videojs/utils/predicate';\nimport type { AudioTrack } from './audio-track';\nimport { TrackEvent } from './change-event';\nimport { getPrivate } from './utils';\n\nexport function addAudioTrack(media: HTMLMediaElement, track: AudioTrack) {\n const trackList = media.audioTracks;\n getPrivate(track).media = new WeakRef(media);\n\n if (!getPrivate(track).renditionSet) {\n getPrivate(track).renditionSet = new Set();\n }\n\n const trackSet = getPrivate(trackList).trackSet as Set;\n trackSet.add(track);\n const index = trackSet.size - 1;\n\n if (!(index in AudioTrackList.prototype)) {\n Object.defineProperty(AudioTrackList.prototype, index, {\n get() {\n return [...(getPrivate(this).trackSet as Set)][index];\n },\n });\n }\n\n // The event is queued to align with native `addtrack` behavior.\n queueMicrotask(() => {\n trackList.dispatchEvent(new TrackEvent('addtrack', { track }));\n });\n}\n\nexport function removeAudioTrack(track: AudioTrack) {\n const trackList = getPrivate(track).media?.deref()?.audioTracks as AudioTrackList | undefined;\n if (!trackList) return;\n\n const trackSet = getPrivate(trackList).trackSet as Set;\n if (!trackSet.delete(track)) return;\n\n queueMicrotask(() => {\n trackList.dispatchEvent(new TrackEvent('removetrack', { track }));\n });\n}\n\nexport function enabledChanged(track: AudioTrack) {\n const trackList = getPrivate(track).media?.deref()?.audioTracks as AudioTrackList | undefined;\n\n // Prevent firing a track list `change` event multiple times per tick.\n if (!trackList || getPrivate(trackList).changeRequested) return;\n getPrivate(trackList).changeRequested = true;\n\n queueMicrotask(() => {\n delete getPrivate(trackList).changeRequested;\n trackList.dispatchEvent(new Event('change'));\n });\n}\n\nexport class AudioTrackList extends EventTarget {\n [index: number]: AudioTrack;\n #addTrackCallback: (() => void) | undefined;\n #removeTrackCallback: (() => void) | undefined;\n #changeCallback: (() => void) | undefined;\n\n constructor() {\n super();\n getPrivate(this).trackSet = new Set();\n }\n\n get #tracks() {\n return getPrivate(this).trackSet as Set;\n }\n\n [Symbol.iterator]() {\n return this.#tracks.values();\n }\n\n get length() {\n return this.#tracks.size;\n }\n\n getTrackById(id: string): AudioTrack | null {\n return [...this.#tracks].find((track) => track.id === id) ?? null;\n }\n\n get onaddtrack() {\n return this.#addTrackCallback;\n }\n\n set onaddtrack(callback: ((event?: { track: AudioTrack }) => void) | undefined) {\n if (this.#addTrackCallback) {\n this.removeEventListener('addtrack', this.#addTrackCallback);\n this.#addTrackCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#addTrackCallback = callback;\n this.addEventListener('addtrack', callback as unknown as EventListener);\n }\n }\n\n get onremovetrack() {\n return this.#removeTrackCallback;\n }\n\n set onremovetrack(callback: ((event?: { track: AudioTrack }) => void) | undefined) {\n if (this.#removeTrackCallback) {\n this.removeEventListener('removetrack', this.#removeTrackCallback);\n this.#removeTrackCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#removeTrackCallback = callback;\n this.addEventListener('removetrack', callback as unknown as EventListener);\n }\n }\n\n get onchange() {\n return this.#changeCallback;\n }\n\n set onchange(callback) {\n if (this.#changeCallback) {\n this.removeEventListener('change', this.#changeCallback);\n this.#changeCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#changeCallback = callback;\n this.addEventListener('change', callback);\n }\n }\n}\n"],"mappings":";;;;AAKA,SAAgB,cAAc,OAAyB,OAAmB;CACxE,MAAM,YAAY,MAAM;AACxB,YAAW,MAAM,CAAC,QAAQ,IAAI,QAAQ,MAAM;AAE5C,KAAI,CAAC,WAAW,MAAM,CAAC,aACrB,YAAW,MAAM,CAAC,+BAAe,IAAI,KAAK;CAG5C,MAAM,WAAW,WAAW,UAAU,CAAC;AACvC,UAAS,IAAI,MAAM;CACnB,MAAM,QAAQ,SAAS,OAAO;AAE9B,KAAI,EAAE,SAAS,eAAe,WAC5B,QAAO,eAAe,eAAe,WAAW,OAAO,EACrD,MAAM;AACJ,SAAO,CAAC,GAAI,WAAW,KAAK,CAAC,SAA6B,CAAC;IAE9D,CAAC;AAIJ,sBAAqB;AACnB,YAAU,cAAc,IAAI,WAAW,YAAY,EAAE,OAAO,CAAC,CAAC;GAC9D;;AAGJ,SAAgB,iBAAiB,OAAmB;CAClD,MAAM,YAAY,WAAW,MAAM,CAAC,OAAO,OAAO,EAAE;AACpD,KAAI,CAAC,UAAW;AAGhB,KAAI,CADa,WAAW,UAAU,CAAC,SACzB,OAAO,MAAM,CAAE;AAE7B,sBAAqB;AACnB,YAAU,cAAc,IAAI,WAAW,eAAe,EAAE,OAAO,CAAC,CAAC;GACjE;;AAGJ,SAAgB,eAAe,OAAmB;CAChD,MAAM,YAAY,WAAW,MAAM,CAAC,OAAO,OAAO,EAAE;AAGpD,KAAI,CAAC,aAAa,WAAW,UAAU,CAAC,gBAAiB;AACzD,YAAW,UAAU,CAAC,kBAAkB;AAExC,sBAAqB;AACnB,SAAO,WAAW,UAAU,CAAC;AAC7B,YAAU,cAAc,IAAI,MAAM,SAAS,CAAC;GAC5C;;AAGJ,IAAa,iBAAb,cAAoC,YAAY;CAE9C;CACA;CACA;CAEA,cAAc;AACZ,SAAO;AACP,aAAW,KAAK,CAAC,2BAAW,IAAI,KAAK;;CAGvC,KAAA,SAAc;AACZ,SAAO,WAAW,KAAK,CAAC;;CAG1B,CAAC,OAAO,YAAY;AAClB,SAAO,MAAA,OAAa,QAAQ;;CAG9B,IAAI,SAAS;AACX,SAAO,MAAA,OAAa;;CAGtB,aAAa,IAA+B;AAC1C,SAAO,CAAC,GAAG,MAAA,OAAa,CAAC,MAAM,UAAU,MAAM,OAAO,GAAG,IAAI;;CAG/D,IAAI,aAAa;AACf,SAAO,MAAA;;CAGT,IAAI,WAAW,UAAiE;AAC9E,MAAI,MAAA,kBAAwB;AAC1B,QAAK,oBAAoB,YAAY,MAAA,iBAAuB;AAC5D,SAAA,mBAAyB,KAAA;;AAE3B,MAAI,WAAW,SAAS,EAAE;AACxB,SAAA,mBAAyB;AACzB,QAAK,iBAAiB,YAAY,SAAqC;;;CAI3E,IAAI,gBAAgB;AAClB,SAAO,MAAA;;CAGT,IAAI,cAAc,UAAiE;AACjF,MAAI,MAAA,qBAA2B;AAC7B,QAAK,oBAAoB,eAAe,MAAA,oBAA0B;AAClE,SAAA,sBAA4B,KAAA;;AAE9B,MAAI,WAAW,SAAS,EAAE;AACxB,SAAA,sBAA4B;AAC5B,QAAK,iBAAiB,eAAe,SAAqC;;;CAI9E,IAAI,WAAW;AACb,SAAO,MAAA;;CAGT,IAAI,SAAS,UAAU;AACrB,MAAI,MAAA,gBAAsB;AACxB,QAAK,oBAAoB,UAAU,MAAA,eAAqB;AACxD,SAAA,iBAAuB,KAAA;;AAEzB,MAAI,WAAW,SAAS,EAAE;AACxB,SAAA,iBAAuB;AACvB,QAAK,iBAAiB,UAAU,SAAS"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/audio-track.js b/dist/default/core/media-tracks/audio-track.js new file mode 100644 index 00000000..a63351a6 --- /dev/null +++ b/dist/default/core/media-tracks/audio-track.js @@ -0,0 +1,35 @@ +import { addRendition, removeRendition } from "./audio-rendition-list.js"; +import { AudioRendition } from "./audio-rendition.js"; +import { enabledChanged } from "./audio-track-list.js"; +//#region src/core/media-tracks/audio-track.ts +var AudioTrack = class { + id; + kind; + label = ""; + language = ""; + sourceBuffer; + #enabled = false; + addRendition(src, codec, bitrate) { + const rendition = new AudioRendition(); + rendition.src = src; + rendition.codec = codec; + rendition.bitrate = bitrate; + addRendition(this, rendition); + return rendition; + } + removeRendition(rendition) { + removeRendition(rendition); + } + get enabled() { + return this.#enabled; + } + set enabled(value) { + if (this.#enabled === value) return; + this.#enabled = value; + enabledChanged(this); + } +}; +//#endregion +export { AudioTrack }; + +//# sourceMappingURL=audio-track.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/audio-track.js.map b/dist/default/core/media-tracks/audio-track.js.map new file mode 100644 index 00000000..ef40bad4 --- /dev/null +++ b/dist/default/core/media-tracks/audio-track.js.map @@ -0,0 +1 @@ +{"version":3,"file":"audio-track.js","names":["#enabled"],"sources":["../../../../src/core/media-tracks/audio-track.ts"],"sourcesContent":["import { AudioRendition } from './audio-rendition';\nimport { addRendition, removeRendition } from './audio-rendition-list';\nimport { enabledChanged } from './audio-track-list';\n\nexport const AudioTrackKind = {\n alternative: 'alternative',\n descriptions: 'descriptions',\n main: 'main',\n 'main-desc': 'main-desc',\n translation: 'translation',\n commentary: 'commentary',\n};\n\nexport class AudioTrack {\n id: string | undefined;\n kind: string | undefined;\n label = '';\n language = '';\n sourceBuffer: unknown;\n #enabled = false;\n\n addRendition(src: string, codec?: string, bitrate?: number) {\n const rendition = new AudioRendition();\n rendition.src = src;\n rendition.codec = codec;\n rendition.bitrate = bitrate;\n addRendition(this, rendition);\n return rendition;\n }\n\n removeRendition(rendition: AudioRendition) {\n removeRendition(rendition);\n }\n\n get enabled(): boolean {\n return this.#enabled;\n }\n\n set enabled(value: boolean) {\n if (this.#enabled === value) return;\n this.#enabled = value;\n\n enabledChanged(this);\n }\n}\n"],"mappings":";;;;AAaA,IAAa,aAAb,MAAwB;CACtB;CACA;CACA,QAAQ;CACR,WAAW;CACX;CACA,WAAW;CAEX,aAAa,KAAa,OAAgB,SAAkB;EAC1D,MAAM,YAAY,IAAI,gBAAgB;AACtC,YAAU,MAAM;AAChB,YAAU,QAAQ;AAClB,YAAU,UAAU;AACpB,eAAa,MAAM,UAAU;AAC7B,SAAO;;CAGT,gBAAgB,WAA2B;AACzC,kBAAgB,UAAU;;CAG5B,IAAI,UAAmB;AACrB,SAAO,MAAA;;CAGT,IAAI,QAAQ,OAAgB;AAC1B,MAAI,MAAA,YAAkB,MAAO;AAC7B,QAAA,UAAgB;AAEhB,iBAAe,KAAK"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/change-event.js b/dist/default/core/media-tracks/change-event.js new file mode 100644 index 00000000..55974955 --- /dev/null +++ b/dist/default/core/media-tracks/change-event.js @@ -0,0 +1,12 @@ +//#region src/core/media-tracks/change-event.ts +var TrackEvent = class extends Event { + track; + constructor(type, init) { + super(type); + this.track = init.track; + } +}; +//#endregion +export { TrackEvent }; + +//# sourceMappingURL=change-event.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/change-event.js.map b/dist/default/core/media-tracks/change-event.js.map new file mode 100644 index 00000000..36898f49 --- /dev/null +++ b/dist/default/core/media-tracks/change-event.js.map @@ -0,0 +1 @@ +{"version":3,"file":"change-event.js","names":[],"sources":["../../../../src/core/media-tracks/change-event.ts"],"sourcesContent":["import type { AudioTrack } from './audio-track';\nimport type { VideoTrack } from './video-track';\n\nexport class TrackEvent extends Event {\n track: AudioTrack | VideoTrack;\n\n constructor(type: string, init: { track: AudioTrack | VideoTrack }) {\n super(type);\n this.track = init.track;\n }\n}\n"],"mappings":";AAGA,IAAa,aAAb,cAAgC,MAAM;CACpC;CAEA,YAAY,MAAc,MAA0C;AAClE,QAAM,KAAK;AACX,OAAK,QAAQ,KAAK"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/mixin.js b/dist/default/core/media-tracks/mixin.js new file mode 100644 index 00000000..ec5a7fb5 --- /dev/null +++ b/dist/default/core/media-tracks/mixin.js @@ -0,0 +1,159 @@ +import { getPrivate } from "./utils.js"; +import { AudioRenditionList } from "./audio-rendition-list.js"; +import { AudioTrackList, addAudioTrack, removeAudioTrack } from "./audio-track-list.js"; +import { AudioTrack } from "./audio-track.js"; +import { VideoRenditionList } from "./video-rendition-list.js"; +import { VideoTrackList, addVideoTrack, removeVideoTrack } from "./video-track-list.js"; +import { VideoTrack } from "./video-track.js"; +//#region src/core/media-tracks/mixin.ts +const HTMLMediaElementConstructor = globalThis.HTMLMediaElement; +const nativeVideoTracksFn = getBaseMediaTracksFn(HTMLMediaElementConstructor, "video"); +const nativeAudioTracksFn = getBaseMediaTracksFn(HTMLMediaElementConstructor, "audio"); +function MediaTracksMixin(MediaElementClass) { + if (!MediaElementClass?.prototype) return MediaElementClass; + const prototype = MediaElementClass.prototype; + const videoTracksFn = getBaseMediaTracksFn(MediaElementClass, "video"); + if (!videoTracksFn || `${videoTracksFn}`.includes("[native code]")) Object.defineProperty(prototype, "videoTracks", { get() { + return getVideoTracks(this); + } }); + const audioTracksFn = getBaseMediaTracksFn(MediaElementClass, "audio"); + if (!audioTracksFn || `${audioTracksFn}`.includes("[native code]")) Object.defineProperty(prototype, "audioTracks", { get() { + return getAudioTracks(this); + } }); + if (!hasOwn(prototype, "addVideoTrack")) prototype.addVideoTrack = function(kind, label = "", language = "") { + const track = new VideoTrack(); + track.kind = kind; + track.label = label; + track.language = language; + addVideoTrack(this, track); + return track; + }; + if (!hasOwn(prototype, "removeVideoTrack")) prototype.removeVideoTrack = removeVideoTrack; + if (!hasOwn(prototype, "addAudioTrack")) prototype.addAudioTrack = function(kind, label = "", language = "") { + const track = new AudioTrack(); + track.kind = kind; + track.label = label; + track.language = language; + addAudioTrack(this, track); + return track; + }; + if (!hasOwn(prototype, "removeAudioTrack")) prototype.removeAudioTrack = removeAudioTrack; + if (!hasOwn(prototype, "detach")) { + const baseDetach = prototype.detach; + prototype.detach = function() { + const priv = getPrivate(this); + priv.videoTracksCleanup?.abort(); + priv.audioTracksCleanup?.abort(); + delete priv.videoTracks; + delete priv.audioTracks; + delete priv.videoTracksCleanup; + delete priv.audioTracksCleanup; + baseDetach?.call(this); + }; + } + if (!hasOwn(prototype, "videoRenditions")) Object.defineProperty(prototype, "videoRenditions", { get() { + return initVideoRenditions(this); + } }); + if (!hasOwn(prototype, "audioRenditions")) Object.defineProperty(prototype, "audioRenditions", { get() { + return initAudioRenditions(this); + } }); + return MediaElementClass; +} +function hasOwn(value, key) { + return Object.hasOwn(value, key); +} +function initVideoRenditions(media) { + let renditions = getPrivate(media).videoRenditions; + if (!renditions) { + renditions = new VideoRenditionList(); + getPrivate(renditions).media = new WeakRef(media); + getPrivate(media).videoRenditions = renditions; + } + return renditions; +} +function initAudioRenditions(media) { + let renditions = getPrivate(media).audioRenditions; + if (!renditions) { + renditions = new AudioRenditionList(); + getPrivate(renditions).media = new WeakRef(media); + getPrivate(media).audioRenditions = renditions; + } + return renditions; +} +function getBaseMediaTracksFn(MediaElementClass, type) { + if (MediaElementClass?.prototype) return Object.getOwnPropertyDescriptor(MediaElementClass.prototype, `${type}Tracks`)?.get; +} +function getVideoTracks(media) { + let tracks = getPrivate(media).videoTracks; + if (!tracks) { + tracks = new VideoTrackList(); + getPrivate(media).videoTracks = tracks; + const nativeEl = media.target; + if (nativeVideoTracksFn && nativeEl) { + const currentTracks = tracks; + const nativeTracks = nativeVideoTracksFn.call(nativeEl); + for (const nativeTrack of nativeTracks) addVideoTrack(media, nativeTrack); + const onChange = () => { + currentTracks.dispatchEvent(new Event("change")); + }; + const onAddTrack = (event) => { + if ([...currentTracks].some((track) => track instanceof VideoTrack)) return; + addVideoTrack(media, event.track); + }; + const onRemoveTrack = (event) => { + removeVideoTrack(event.track); + }; + const onCustomAddTrack = (event) => { + if (!(event.track instanceof VideoTrack)) return; + for (const nativeTrack of nativeTracks) removeVideoTrack(nativeTrack); + }; + const controller = new AbortController(); + const { signal } = controller; + getPrivate(media).videoTracksCleanup = controller; + nativeTracks.addEventListener("change", onChange, { signal }); + nativeTracks.addEventListener("addtrack", onAddTrack, { signal }); + nativeTracks.addEventListener("removetrack", onRemoveTrack, { signal }); + currentTracks.addEventListener("addtrack", onCustomAddTrack, { signal }); + } + } + return tracks; +} +function getAudioTracks(media) { + let tracks = getPrivate(media).audioTracks; + if (!tracks) { + tracks = new AudioTrackList(); + getPrivate(media).audioTracks = tracks; + const nativeEl = media.target; + if (nativeAudioTracksFn && nativeEl) { + const currentTracks = tracks; + const nativeTracks = nativeAudioTracksFn.call(nativeEl); + for (const nativeTrack of nativeTracks) addAudioTrack(media, nativeTrack); + const onChange = () => { + currentTracks.dispatchEvent(new Event("change")); + }; + const onAddTrack = (event) => { + if ([...currentTracks].some((track) => track instanceof AudioTrack)) return; + addAudioTrack(media, event.track); + }; + const onRemoveTrack = (event) => { + removeAudioTrack(event.track); + }; + const onCustomAddTrack = (event) => { + if (!(event.track instanceof AudioTrack)) return; + for (const nativeTrack of nativeTracks) removeAudioTrack(nativeTrack); + }; + const controller = new AbortController(); + const { signal } = controller; + getPrivate(media).audioTracksCleanup = controller; + nativeTracks.addEventListener("change", onChange, { signal }); + nativeTracks.addEventListener("addtrack", onAddTrack, { signal }); + nativeTracks.addEventListener("removetrack", onRemoveTrack, { signal }); + currentTracks.addEventListener("addtrack", onCustomAddTrack, { signal }); + } + } + return tracks; +} +//#endregion +export { MediaTracksMixin }; + +//# sourceMappingURL=mixin.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/mixin.js.map b/dist/default/core/media-tracks/mixin.js.map new file mode 100644 index 00000000..97c8026e --- /dev/null +++ b/dist/default/core/media-tracks/mixin.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mixin.js","names":[],"sources":["../../../../src/core/media-tracks/mixin.ts"],"sourcesContent":["import type { AnyConstructor, MixinReturn } from '@videojs/utils/types';\nimport type {\n MediaAudioRenditionCapability,\n MediaAudioTrackCapability,\n MediaVideoRenditionCapability,\n MediaVideoTrackCapability,\n} from '../types';\nimport { AudioRenditionList } from './audio-rendition-list';\nimport { AudioTrack } from './audio-track';\nimport { AudioTrackList, addAudioTrack, removeAudioTrack } from './audio-track-list';\nimport type { TrackEvent } from './change-event';\nimport './global';\nimport { getPrivate } from './utils';\nimport { VideoRenditionList } from './video-rendition-list';\nimport { VideoTrack } from './video-track';\nimport { addVideoTrack, removeVideoTrack, VideoTrackList } from './video-track-list';\n\nexport type WithMediaTracks> = MixinReturn<\n Base,\n MediaVideoTrackCapability & MediaAudioTrackCapability & MediaVideoRenditionCapability & MediaAudioRenditionCapability\n>;\n\nconst HTMLMediaElementConstructor = (globalThis as { HTMLMediaElement?: AnyConstructor })\n .HTMLMediaElement;\nconst nativeVideoTracksFn = getBaseMediaTracksFn(HTMLMediaElementConstructor, 'video');\nconst nativeAudioTracksFn = getBaseMediaTracksFn(HTMLMediaElementConstructor, 'audio');\n\n// Safari supports native media tracks, but native implementations cannot\n// reliably represent manifest-derived MSE tracks or manually-added tracks.\nexport function MediaTracksMixin>(MediaElementClass: Base): WithMediaTracks {\n if (!MediaElementClass?.prototype) return MediaElementClass as WithMediaTracks;\n\n const prototype = MediaElementClass.prototype as Record;\n const videoTracksFn = getBaseMediaTracksFn(MediaElementClass, 'video');\n\n if (!videoTracksFn || `${videoTracksFn}`.includes('[native code]')) {\n Object.defineProperty(prototype, 'videoTracks', {\n get() {\n return getVideoTracks(this);\n },\n });\n }\n\n const audioTracksFn = getBaseMediaTracksFn(MediaElementClass, 'audio');\n\n if (!audioTracksFn || `${audioTracksFn}`.includes('[native code]')) {\n Object.defineProperty(prototype, 'audioTracks', {\n get() {\n return getAudioTracks(this);\n },\n });\n }\n\n if (!hasOwn(prototype, 'addVideoTrack')) {\n prototype.addVideoTrack = function (this: HTMLMediaElement, kind: string, label = '', language = '') {\n const track = new VideoTrack();\n track.kind = kind;\n track.label = label;\n track.language = language;\n addVideoTrack(this, track);\n return track;\n };\n }\n\n if (!hasOwn(prototype, 'removeVideoTrack')) {\n prototype.removeVideoTrack = removeVideoTrack;\n }\n\n if (!hasOwn(prototype, 'addAudioTrack')) {\n prototype.addAudioTrack = function (this: HTMLMediaElement, kind: string, label = '', language = '') {\n const track = new AudioTrack();\n track.kind = kind;\n track.label = label;\n track.language = language;\n addAudioTrack(this, track);\n return track;\n };\n }\n\n if (!hasOwn(prototype, 'removeAudioTrack')) {\n prototype.removeAudioTrack = removeAudioTrack;\n }\n\n // Tear down the native-track listeners wired in getVideoTracks/getAudioTracks\n // when the target is removed, and drop the cached lists so a re-attach\n // re-mirrors against the new target.\n if (!hasOwn(prototype, 'detach')) {\n const baseDetach = prototype.detach as ((this: object) => void) | undefined;\n prototype.detach = function (this: object) {\n const priv = getPrivate(this);\n (priv.videoTracksCleanup as AbortController | undefined)?.abort();\n (priv.audioTracksCleanup as AbortController | undefined)?.abort();\n delete priv.videoTracks;\n delete priv.audioTracks;\n delete priv.videoTracksCleanup;\n delete priv.audioTracksCleanup;\n baseDetach?.call(this);\n };\n }\n\n if (!hasOwn(prototype, 'videoRenditions')) {\n Object.defineProperty(prototype, 'videoRenditions', {\n get() {\n return initVideoRenditions(this);\n },\n });\n }\n\n if (!hasOwn(prototype, 'audioRenditions')) {\n Object.defineProperty(prototype, 'audioRenditions', {\n get() {\n return initAudioRenditions(this);\n },\n });\n }\n\n return MediaElementClass as WithMediaTracks;\n}\n\nfunction hasOwn(value: object, key: PropertyKey) {\n return Object.hasOwn(value, key);\n}\n\nfunction initVideoRenditions(media: HTMLMediaElement) {\n let renditions = getPrivate(media).videoRenditions as VideoRenditionList | undefined;\n if (!renditions) {\n renditions = new VideoRenditionList();\n getPrivate(renditions).media = new WeakRef(media);\n getPrivate(media).videoRenditions = renditions;\n }\n return renditions;\n}\n\nfunction initAudioRenditions(media: HTMLMediaElement) {\n let renditions = getPrivate(media).audioRenditions as AudioRenditionList | undefined;\n if (!renditions) {\n renditions = new AudioRenditionList();\n getPrivate(renditions).media = new WeakRef(media);\n getPrivate(media).audioRenditions = renditions;\n }\n return renditions;\n}\n\nfunction getBaseMediaTracksFn(MediaElementClass: any, type: string): (() => any) | undefined {\n if (MediaElementClass?.prototype) {\n return Object.getOwnPropertyDescriptor(MediaElementClass.prototype, `${type}Tracks`)?.get;\n }\n return undefined;\n}\n\nfunction getVideoTracks(media: any) {\n let tracks = getPrivate(media).videoTracks as VideoTrackList | undefined;\n if (!tracks) {\n tracks = new VideoTrackList();\n getPrivate(media).videoTracks = tracks;\n\n const nativeEl = media.target;\n\n if (nativeVideoTracksFn && nativeEl) {\n const currentTracks = tracks;\n const nativeTracks = nativeVideoTracksFn.call(nativeEl);\n\n for (const nativeTrack of nativeTracks) {\n addVideoTrack(media, nativeTrack);\n }\n\n const onChange = () => {\n currentTracks.dispatchEvent(new Event('change'));\n };\n\n const onAddTrack = (event: TrackEvent) => {\n if ([...currentTracks].some((track) => track instanceof VideoTrack)) return;\n addVideoTrack(media, event.track as VideoTrack);\n };\n\n const onRemoveTrack = (event: TrackEvent) => {\n removeVideoTrack(event.track as VideoTrack);\n };\n\n // Adding a custom track replaces any mirrored native tracks.\n const onCustomAddTrack = (event: Event) => {\n if (!((event as TrackEvent).track instanceof VideoTrack)) return;\n for (const nativeTrack of nativeTracks) {\n removeVideoTrack(nativeTrack as VideoTrack);\n }\n };\n\n const controller = new AbortController();\n const { signal } = controller;\n getPrivate(media).videoTracksCleanup = controller;\n\n nativeTracks.addEventListener('change', onChange, { signal });\n nativeTracks.addEventListener('addtrack', onAddTrack, { signal });\n nativeTracks.addEventListener('removetrack', onRemoveTrack, { signal });\n currentTracks.addEventListener('addtrack', onCustomAddTrack, { signal });\n }\n }\n return tracks;\n}\n\nfunction getAudioTracks(media: any) {\n let tracks = getPrivate(media).audioTracks as AudioTrackList | undefined;\n if (!tracks) {\n tracks = new AudioTrackList();\n getPrivate(media).audioTracks = tracks;\n\n const nativeEl = media.target;\n\n if (nativeAudioTracksFn && nativeEl) {\n const currentTracks = tracks;\n const nativeTracks = nativeAudioTracksFn.call(nativeEl);\n\n for (const nativeTrack of nativeTracks) {\n addAudioTrack(media, nativeTrack);\n }\n\n const onChange = () => {\n currentTracks.dispatchEvent(new Event('change'));\n };\n\n const onAddTrack = (event: TrackEvent) => {\n if ([...currentTracks].some((track) => track instanceof AudioTrack)) return;\n addAudioTrack(media, event.track as AudioTrack);\n };\n\n const onRemoveTrack = (event: TrackEvent) => {\n removeAudioTrack(event.track as AudioTrack);\n };\n\n // Adding a custom track replaces any mirrored native tracks.\n const onCustomAddTrack = (event: Event) => {\n if (!((event as TrackEvent).track instanceof AudioTrack)) return;\n for (const nativeTrack of nativeTracks) {\n removeAudioTrack(nativeTrack as AudioTrack);\n }\n };\n\n const controller = new AbortController();\n const { signal } = controller;\n getPrivate(media).audioTracksCleanup = controller;\n\n nativeTracks.addEventListener('change', onChange, { signal });\n nativeTracks.addEventListener('addtrack', onAddTrack, { signal });\n nativeTracks.addEventListener('removetrack', onRemoveTrack, { signal });\n currentTracks.addEventListener('addtrack', onCustomAddTrack, { signal });\n }\n }\n return tracks;\n}\n"],"mappings":"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"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/rendition-event.js b/dist/default/core/media-tracks/rendition-event.js new file mode 100644 index 00000000..97307144 --- /dev/null +++ b/dist/default/core/media-tracks/rendition-event.js @@ -0,0 +1,12 @@ +//#region src/core/media-tracks/rendition-event.ts +var RenditionEvent = class extends Event { + rendition; + constructor(type, init) { + super(type); + this.rendition = init.rendition; + } +}; +//#endregion +export { RenditionEvent }; + +//# sourceMappingURL=rendition-event.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/rendition-event.js.map b/dist/default/core/media-tracks/rendition-event.js.map new file mode 100644 index 00000000..ad448490 --- /dev/null +++ b/dist/default/core/media-tracks/rendition-event.js.map @@ -0,0 +1 @@ +{"version":3,"file":"rendition-event.js","names":[],"sources":["../../../../src/core/media-tracks/rendition-event.ts"],"sourcesContent":["import type { AudioRendition } from './audio-rendition';\nimport type { VideoRendition } from './video-rendition';\n\nexport class RenditionEvent extends Event {\n rendition: AudioRendition | VideoRendition;\n\n constructor(type: string, init: { rendition: AudioRendition | VideoRendition }) {\n super(type);\n this.rendition = init.rendition;\n }\n}\n"],"mappings":";AAGA,IAAa,iBAAb,cAAoC,MAAM;CACxC;CAEA,YAAY,MAAc,MAAsD;AAC9E,QAAM,KAAK;AACX,OAAK,YAAY,KAAK"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/utils.js b/dist/default/core/media-tracks/utils.js new file mode 100644 index 00000000..2478e4f3 --- /dev/null +++ b/dist/default/core/media-tracks/utils.js @@ -0,0 +1,14 @@ +//#region src/core/media-tracks/utils.ts +const privateProps = /* @__PURE__ */ new WeakMap(); +function getPrivate(instance) { + return privateProps.get(instance) ?? setPrivate(instance, {}); +} +function setPrivate(instance, props) { + let saved = privateProps.get(instance); + if (!saved) privateProps.set(instance, saved = {}); + return Object.assign(saved, props); +} +//#endregion +export { getPrivate }; + +//# sourceMappingURL=utils.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/utils.js.map b/dist/default/core/media-tracks/utils.js.map new file mode 100644 index 00000000..afd9e6a0 --- /dev/null +++ b/dist/default/core/media-tracks/utils.js.map @@ -0,0 +1 @@ +{"version":3,"file":"utils.js","names":[],"sources":["../../../../src/core/media-tracks/utils.ts"],"sourcesContent":["const privateProps = new WeakMap>();\n\nexport function getPrivate(instance: object) {\n return privateProps.get(instance) ?? setPrivate(instance, {});\n}\n\nexport function setPrivate(instance: object, props: Record) {\n let saved = privateProps.get(instance);\n if (!saved) privateProps.set(instance, (saved = {}));\n\n return Object.assign(saved, props);\n}\n"],"mappings":";AAAA,MAAM,+BAAe,IAAI,SAAsC;AAE/D,SAAgB,WAAW,UAAkB;AAC3C,QAAO,aAAa,IAAI,SAAS,IAAI,WAAW,UAAU,EAAE,CAAC;;AAG/D,SAAgB,WAAW,UAAkB,OAA4B;CACvE,IAAI,QAAQ,aAAa,IAAI,SAAS;AACtC,KAAI,CAAC,MAAO,cAAa,IAAI,UAAW,QAAQ,EAAE,CAAE;AAEpD,QAAO,OAAO,OAAO,OAAO,MAAM"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/video-rendition-list.js b/dist/default/core/media-tracks/video-rendition-list.js new file mode 100644 index 00000000..becb0dc8 --- /dev/null +++ b/dist/default/core/media-tracks/video-rendition-list.js @@ -0,0 +1,116 @@ +import { RenditionEvent } from "./rendition-event.js"; +import { getPrivate } from "./utils.js"; +import { isFunction } from "@videojs/utils/predicate"; +//#region src/core/media-tracks/video-rendition-list.ts +function addRendition(track, rendition) { + const renditionList = getPrivate(track).media?.deref()?.videoRenditions; + getPrivate(rendition).media = getPrivate(track).media; + getPrivate(rendition).track = track; + const renditionSet = getPrivate(track).renditionSet; + renditionSet.add(rendition); + const index = renditionSet.size - 1; + if (!(index in VideoRenditionList.prototype)) Object.defineProperty(VideoRenditionList.prototype, index, { get() { + return getCurrentRenditions(this)[index]; + } }); + queueMicrotask(() => { + if (!renditionList || !track.selected) return; + renditionList.dispatchEvent(new RenditionEvent("addrendition", { rendition })); + }); +} +function removeRendition(rendition) { + const renditionList = getPrivate(rendition).media?.deref()?.videoRenditions; + const track = getPrivate(rendition).track; + getPrivate(track).renditionSet.delete(rendition); + queueMicrotask(() => { + if (!renditionList || !track.selected) return; + renditionList.dispatchEvent(new RenditionEvent("removerendition", { rendition })); + }); +} +function selectedChanged(rendition) { + const renditionList = getPrivate(rendition).media?.deref()?.videoRenditions; + if (!renditionList || getPrivate(renditionList).changeRequested) return; + getPrivate(renditionList).changeRequested = true; + queueMicrotask(() => { + delete getPrivate(renditionList).changeRequested; + if (!getPrivate(rendition).track.selected) return; + renditionList.dispatchEvent(new Event("change")); + }); +} +function activeChanged(rendition) { + const renditionList = getPrivate(rendition).media?.deref()?.videoRenditions; + if (!renditionList || getPrivate(renditionList).activeChangeRequested) return; + getPrivate(renditionList).activeChangeRequested = true; + queueMicrotask(() => { + delete getPrivate(renditionList).activeChangeRequested; + if (!getPrivate(rendition).track.selected) return; + renditionList.dispatchEvent(new Event("activechange")); + }); +} +function getCurrentRenditions(renditionList) { + const media = getPrivate(renditionList).media?.deref(); + if (!media) return []; + return [...media.videoTracks].filter((track) => track.selected).flatMap((track) => [...getPrivate(track).renditionSet]); +} +var VideoRenditionList = class extends EventTarget { + #addRenditionCallback; + #removeRenditionCallback; + #changeCallback; + [Symbol.iterator]() { + return getCurrentRenditions(this).values(); + } + get length() { + return getCurrentRenditions(this).length; + } + getRenditionById(id) { + return getCurrentRenditions(this).find((rendition) => `${rendition.id}` === `${id}`) ?? null; + } + get selectedIndex() { + return getCurrentRenditions(this).findIndex((rendition) => rendition.selected); + } + set selectedIndex(index) { + for (const [i, rendition] of getCurrentRenditions(this).entries()) rendition.selected = i === index; + } + get onaddrendition() { + return this.#addRenditionCallback; + } + set onaddrendition(callback) { + if (this.#addRenditionCallback) { + this.removeEventListener("addrendition", this.#addRenditionCallback); + this.#addRenditionCallback = void 0; + } + if (isFunction(callback)) { + this.#addRenditionCallback = callback; + this.addEventListener("addrendition", callback); + } + } + get onremoverendition() { + return this.#removeRenditionCallback; + } + set onremoverendition(callback) { + if (this.#removeRenditionCallback) { + this.removeEventListener("removerendition", this.#removeRenditionCallback); + this.#removeRenditionCallback = void 0; + } + if (isFunction(callback)) { + this.#removeRenditionCallback = callback; + this.addEventListener("removerendition", callback); + } + } + get onchange() { + return this.#changeCallback; + } + set onchange(callback) { + if (this.#changeCallback) { + this.removeEventListener("change", this.#changeCallback); + this.#changeCallback = void 0; + } + if (isFunction(callback)) { + this.#changeCallback = callback; + this.addEventListener("change", callback); + } + } +}; +//#endregion +export { VideoRenditionList, activeChanged, addRendition, removeRendition, selectedChanged }; + +//# sourceMappingURL=video-rendition-list.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/video-rendition-list.js.map b/dist/default/core/media-tracks/video-rendition-list.js.map new file mode 100644 index 00000000..260d850d --- /dev/null +++ b/dist/default/core/media-tracks/video-rendition-list.js.map @@ -0,0 +1 @@ +{"version":3,"file":"video-rendition-list.js","names":["#addRenditionCallback","#removeRenditionCallback","#changeCallback"],"sources":["../../../../src/core/media-tracks/video-rendition-list.ts"],"sourcesContent":["import { isFunction } from '@videojs/utils/predicate';\nimport { RenditionEvent } from './rendition-event';\nimport { getPrivate } from './utils';\nimport type { VideoRendition } from './video-rendition';\nimport type { VideoTrack } from './video-track';\n\nexport function addRendition(track: VideoTrack, rendition: VideoRendition) {\n const renditionList = getPrivate(track).media?.deref()?.videoRenditions as VideoRenditionList | undefined;\n\n getPrivate(rendition).media = getPrivate(track).media;\n getPrivate(rendition).track = track;\n\n const renditionSet = getPrivate(track).renditionSet as Set;\n renditionSet.add(rendition);\n const index = renditionSet.size - 1;\n\n if (!(index in VideoRenditionList.prototype)) {\n Object.defineProperty(VideoRenditionList.prototype, index, {\n get() {\n return getCurrentRenditions(this)[index];\n },\n });\n }\n\n queueMicrotask(() => {\n if (!renditionList || !track.selected) return;\n\n renditionList.dispatchEvent(new RenditionEvent('addrendition', { rendition }));\n });\n}\n\nexport function removeRendition(rendition: VideoRendition) {\n const renditionList = getPrivate(rendition).media?.deref()?.videoRenditions as VideoRenditionList | undefined;\n const track = getPrivate(rendition).track as VideoTrack;\n const renditionSet = getPrivate(track).renditionSet as Set;\n renditionSet.delete(rendition);\n\n queueMicrotask(() => {\n if (!renditionList || !track.selected) return;\n\n renditionList.dispatchEvent(new RenditionEvent('removerendition', { rendition }));\n });\n}\n\nexport function selectedChanged(rendition: VideoRendition) {\n const renditionList = getPrivate(rendition).media?.deref()?.videoRenditions as VideoRenditionList | undefined;\n\n // Prevent firing a rendition list `change` event multiple times per tick.\n if (!renditionList || getPrivate(renditionList).changeRequested) return;\n getPrivate(renditionList).changeRequested = true;\n\n queueMicrotask(() => {\n delete getPrivate(renditionList).changeRequested;\n\n const track = getPrivate(rendition).track as VideoTrack;\n if (!track.selected) return;\n\n renditionList.dispatchEvent(new Event('change'));\n });\n}\n\nexport function activeChanged(rendition: VideoRendition) {\n const renditionList = getPrivate(rendition).media?.deref()?.videoRenditions as VideoRenditionList | undefined;\n\n if (!renditionList || getPrivate(renditionList).activeChangeRequested) return;\n getPrivate(renditionList).activeChangeRequested = true;\n\n queueMicrotask(() => {\n delete getPrivate(renditionList).activeChangeRequested;\n\n const track = getPrivate(rendition).track as VideoTrack;\n if (!track.selected) return;\n\n renditionList.dispatchEvent(new Event('activechange'));\n });\n}\n\nfunction getCurrentRenditions(renditionList: VideoRenditionList): VideoRendition[] {\n const media = getPrivate(renditionList).media?.deref() as HTMLMediaElement | undefined;\n if (!media) return [];\n return [...media.videoTracks]\n .filter((track) => track.selected)\n .flatMap((track) => [...(getPrivate(track).renditionSet as Set)]);\n}\n\nexport class VideoRenditionList extends EventTarget {\n [index: number]: VideoRendition;\n #addRenditionCallback: (() => void) | undefined;\n #removeRenditionCallback: (() => void) | undefined;\n #changeCallback: (() => void) | undefined;\n\n [Symbol.iterator]() {\n return getCurrentRenditions(this).values();\n }\n\n get length() {\n return getCurrentRenditions(this).length;\n }\n\n getRenditionById(id: string): VideoRendition | null {\n return getCurrentRenditions(this).find((rendition) => `${rendition.id}` === `${id}`) ?? null;\n }\n\n get selectedIndex() {\n return getCurrentRenditions(this).findIndex((rendition) => rendition.selected);\n }\n\n set selectedIndex(index) {\n for (const [i, rendition] of getCurrentRenditions(this).entries()) {\n rendition.selected = i === index;\n }\n }\n\n get onaddrendition() {\n return this.#addRenditionCallback;\n }\n\n set onaddrendition(callback: ((event?: { rendition: VideoRendition }) => void) | undefined) {\n if (this.#addRenditionCallback) {\n this.removeEventListener('addrendition', this.#addRenditionCallback);\n this.#addRenditionCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#addRenditionCallback = callback;\n this.addEventListener('addrendition', callback as unknown as EventListener);\n }\n }\n\n get onremoverendition() {\n return this.#removeRenditionCallback;\n }\n\n set onremoverendition(callback: ((event?: { rendition: VideoRendition }) => void) | undefined) {\n if (this.#removeRenditionCallback) {\n this.removeEventListener('removerendition', this.#removeRenditionCallback);\n this.#removeRenditionCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#removeRenditionCallback = callback;\n this.addEventListener('removerendition', callback as unknown as EventListener);\n }\n }\n\n get onchange() {\n return this.#changeCallback;\n }\n\n set onchange(callback) {\n if (this.#changeCallback) {\n this.removeEventListener('change', this.#changeCallback);\n this.#changeCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#changeCallback = callback;\n this.addEventListener('change', callback);\n }\n }\n}\n"],"mappings":"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"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/video-rendition.js b/dist/default/core/media-tracks/video-rendition.js new file mode 100644 index 00000000..db0db86b --- /dev/null +++ b/dist/default/core/media-tracks/video-rendition.js @@ -0,0 +1,37 @@ +import { activeChanged, selectedChanged } from "./video-rendition-list.js"; +//#region src/core/media-tracks/video-rendition.ts +/** +* The consumer should use the `selected` setter to select one or multiple +* renditions that the engine is allowed to play. +*/ +var VideoRendition = class { + src; + id; + width; + height; + bitrate; + frameRate; + codec; + #selected = false; + #active = false; + get selected() { + return this.#selected; + } + set selected(value) { + if (this.#selected === value) return; + this.#selected = value; + selectedChanged(this); + } + get active() { + return this.#active; + } + set active(value) { + if (this.#active === value) return; + this.#active = value; + activeChanged(this); + } +}; +//#endregion +export { VideoRendition }; + +//# sourceMappingURL=video-rendition.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/video-rendition.js.map b/dist/default/core/media-tracks/video-rendition.js.map new file mode 100644 index 00000000..5c7da8ff --- /dev/null +++ b/dist/default/core/media-tracks/video-rendition.js.map @@ -0,0 +1 @@ +{"version":3,"file":"video-rendition.js","names":["#selected","#active"],"sources":["../../../../src/core/media-tracks/video-rendition.ts"],"sourcesContent":["import { activeChanged, selectedChanged } from './video-rendition-list';\n\n/**\n * The consumer should use the `selected` setter to select one or multiple\n * renditions that the engine is allowed to play.\n */\nexport class VideoRendition {\n src: string | undefined;\n id: string | undefined;\n width: number | undefined;\n height: number | undefined;\n bitrate: number | undefined;\n frameRate: number | undefined;\n codec: string | undefined;\n #selected = false;\n #active = false;\n\n get selected(): boolean {\n return this.#selected;\n }\n\n set selected(value: boolean) {\n if (this.#selected === value) return;\n this.#selected = value;\n\n selectedChanged(this);\n }\n\n get active(): boolean {\n return this.#active;\n }\n\n set active(value: boolean) {\n if (this.#active === value) return;\n this.#active = value;\n\n activeChanged(this);\n }\n}\n"],"mappings":";;;;;;AAMA,IAAa,iBAAb,MAA4B;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA,YAAY;CACZ,UAAU;CAEV,IAAI,WAAoB;AACtB,SAAO,MAAA;;CAGT,IAAI,SAAS,OAAgB;AAC3B,MAAI,MAAA,aAAmB,MAAO;AAC9B,QAAA,WAAiB;AAEjB,kBAAgB,KAAK;;CAGvB,IAAI,SAAkB;AACpB,SAAO,MAAA;;CAGT,IAAI,OAAO,OAAgB;AACzB,MAAI,MAAA,WAAiB,MAAO;AAC5B,QAAA,SAAe;AAEf,gBAAc,KAAK"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/video-track-list.js b/dist/default/core/media-tracks/video-track-list.js new file mode 100644 index 00000000..858328bc --- /dev/null +++ b/dist/default/core/media-tracks/video-track-list.js @@ -0,0 +1,109 @@ +import { getPrivate } from "./utils.js"; +import { TrackEvent } from "./change-event.js"; +import { isFunction } from "@videojs/utils/predicate"; +//#region src/core/media-tracks/video-track-list.ts +function addVideoTrack(media, track) { + const trackList = media.videoTracks; + getPrivate(track).media = new WeakRef(media); + if (!getPrivate(track).renditionSet) getPrivate(track).renditionSet = /* @__PURE__ */ new Set(); + const trackSet = getPrivate(trackList).trackSet; + trackSet.add(track); + const index = trackSet.size - 1; + if (!(index in VideoTrackList.prototype)) Object.defineProperty(VideoTrackList.prototype, index, { get() { + return [...getPrivate(this).trackSet][index]; + } }); + queueMicrotask(() => { + trackList.dispatchEvent(new TrackEvent("addtrack", { track })); + }); +} +function removeVideoTrack(track) { + const trackList = getPrivate(track).media?.deref()?.videoTracks; + if (!trackList) return; + if (!getPrivate(trackList).trackSet.delete(track)) return; + queueMicrotask(() => { + trackList.dispatchEvent(new TrackEvent("removetrack", { track })); + }); +} +function selectedChanged(selected) { + const trackList = getPrivate(selected).media?.deref()?.videoTracks ?? []; + let hasUnselected = false; + for (const track of trackList) { + if (track === selected) continue; + track.selected = false; + hasUnselected = true; + } + if (!hasUnselected) return; + if (getPrivate(trackList).changeRequested) return; + getPrivate(trackList).changeRequested = true; + queueMicrotask(() => { + delete getPrivate(trackList).changeRequested; + trackList.dispatchEvent(new Event("change")); + }); +} +var VideoTrackList = class extends EventTarget { + #addTrackCallback; + #removeTrackCallback; + #changeCallback; + constructor() { + super(); + getPrivate(this).trackSet = /* @__PURE__ */ new Set(); + } + get #tracks() { + return getPrivate(this).trackSet; + } + [Symbol.iterator]() { + return this.#tracks.values(); + } + get length() { + return this.#tracks.size; + } + getTrackById(id) { + return [...this.#tracks].find((track) => track.id === id) ?? null; + } + get selectedIndex() { + return [...this.#tracks].findIndex((track) => track.selected); + } + get onaddtrack() { + return this.#addTrackCallback; + } + set onaddtrack(callback) { + if (this.#addTrackCallback) { + this.removeEventListener("addtrack", this.#addTrackCallback); + this.#addTrackCallback = void 0; + } + if (isFunction(callback)) { + this.#addTrackCallback = callback; + this.addEventListener("addtrack", callback); + } + } + get onremovetrack() { + return this.#removeTrackCallback; + } + set onremovetrack(callback) { + if (this.#removeTrackCallback) { + this.removeEventListener("removetrack", this.#removeTrackCallback); + this.#removeTrackCallback = void 0; + } + if (isFunction(callback)) { + this.#removeTrackCallback = callback; + this.addEventListener("removetrack", callback); + } + } + get onchange() { + return this.#changeCallback; + } + set onchange(callback) { + if (this.#changeCallback) { + this.removeEventListener("change", this.#changeCallback); + this.#changeCallback = void 0; + } + if (isFunction(callback)) { + this.#changeCallback = callback; + this.addEventListener("change", callback); + } + } +}; +//#endregion +export { VideoTrackList, addVideoTrack, removeVideoTrack, selectedChanged }; + +//# sourceMappingURL=video-track-list.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/video-track-list.js.map b/dist/default/core/media-tracks/video-track-list.js.map new file mode 100644 index 00000000..eefb8fc5 --- /dev/null +++ b/dist/default/core/media-tracks/video-track-list.js.map @@ -0,0 +1 @@ +{"version":3,"file":"video-track-list.js","names":["#tracks","#addTrackCallback","#removeTrackCallback","#changeCallback"],"sources":["../../../../src/core/media-tracks/video-track-list.ts"],"sourcesContent":["import { isFunction } from '@videojs/utils/predicate';\nimport { TrackEvent } from './change-event';\nimport { getPrivate } from './utils';\nimport type { VideoTrack } from './video-track';\n\nexport function addVideoTrack(media: HTMLMediaElement, track: VideoTrack) {\n const trackList = media.videoTracks;\n getPrivate(track).media = new WeakRef(media);\n\n if (!getPrivate(track).renditionSet) {\n getPrivate(track).renditionSet = new Set();\n }\n\n const trackSet = getPrivate(trackList).trackSet as Set;\n trackSet.add(track);\n const index = trackSet.size - 1;\n\n if (!(index in VideoTrackList.prototype)) {\n Object.defineProperty(VideoTrackList.prototype, index, {\n get() {\n return [...(getPrivate(this).trackSet as Set)][index];\n },\n });\n }\n\n // The event is queued to align with native `addtrack` behavior.\n queueMicrotask(() => {\n trackList.dispatchEvent(new TrackEvent('addtrack', { track }));\n });\n}\n\nexport function removeVideoTrack(track: VideoTrack) {\n const trackList = getPrivate(track).media?.deref()?.videoTracks as VideoTrackList | undefined;\n if (!trackList) return;\n\n const trackSet = getPrivate(trackList).trackSet as Set;\n if (!trackSet.delete(track)) return;\n\n queueMicrotask(() => {\n trackList.dispatchEvent(new TrackEvent('removetrack', { track }));\n });\n}\n\nexport function selectedChanged(selected: VideoTrack) {\n const trackList = (getPrivate(selected).media?.deref()?.videoTracks ?? []) as VideoTrackList | VideoTrack[];\n let hasUnselected = false;\n\n for (const track of trackList) {\n if (track === selected) continue;\n track.selected = false;\n hasUnselected = true;\n }\n\n if (!hasUnselected) return;\n\n // Prevent firing a track list `change` event multiple times per tick.\n if (getPrivate(trackList).changeRequested) return;\n getPrivate(trackList).changeRequested = true;\n\n queueMicrotask(() => {\n delete getPrivate(trackList).changeRequested;\n (trackList as VideoTrackList).dispatchEvent(new Event('change'));\n });\n}\n\nexport class VideoTrackList extends EventTarget {\n [index: number]: VideoTrack;\n #addTrackCallback: (() => void) | undefined;\n #removeTrackCallback: (() => void) | undefined;\n #changeCallback: (() => void) | undefined;\n\n constructor() {\n super();\n getPrivate(this).trackSet = new Set();\n }\n\n get #tracks() {\n return getPrivate(this).trackSet as Set;\n }\n\n [Symbol.iterator]() {\n return this.#tracks.values();\n }\n\n get length() {\n return this.#tracks.size;\n }\n\n getTrackById(id: string): VideoTrack | null {\n return [...this.#tracks].find((track) => track.id === id) ?? null;\n }\n\n get selectedIndex() {\n return [...this.#tracks].findIndex((track) => track.selected);\n }\n\n get onaddtrack() {\n return this.#addTrackCallback;\n }\n\n set onaddtrack(callback: ((event?: { track: VideoTrack }) => void) | undefined) {\n if (this.#addTrackCallback) {\n this.removeEventListener('addtrack', this.#addTrackCallback);\n this.#addTrackCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#addTrackCallback = callback;\n this.addEventListener('addtrack', callback as unknown as EventListener);\n }\n }\n\n get onremovetrack() {\n return this.#removeTrackCallback;\n }\n\n set onremovetrack(callback: ((event?: { track: VideoTrack }) => void) | undefined) {\n if (this.#removeTrackCallback) {\n this.removeEventListener('removetrack', this.#removeTrackCallback);\n this.#removeTrackCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#removeTrackCallback = callback;\n this.addEventListener('removetrack', callback as unknown as EventListener);\n }\n }\n\n get onchange() {\n return this.#changeCallback;\n }\n\n set onchange(callback) {\n if (this.#changeCallback) {\n this.removeEventListener('change', this.#changeCallback);\n this.#changeCallback = undefined;\n }\n if (isFunction(callback)) {\n this.#changeCallback = callback;\n this.addEventListener('change', callback);\n }\n }\n}\n"],"mappings":"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"} \ No newline at end of file diff --git a/dist/default/core/media-tracks/video-track.js b/dist/default/core/media-tracks/video-track.js new file mode 100644 index 00000000..06bb4571 --- /dev/null +++ b/dist/default/core/media-tracks/video-track.js @@ -0,0 +1,39 @@ +import { addRendition, removeRendition } from "./video-rendition-list.js"; +import { VideoRendition } from "./video-rendition.js"; +import { selectedChanged } from "./video-track-list.js"; +//#region src/core/media-tracks/video-track.ts +var VideoTrack = class { + id; + kind; + label = ""; + language = ""; + sourceBuffer; + #selected = false; + addRendition(src, width, height, codec, bitrate, frameRate) { + const rendition = new VideoRendition(); + rendition.src = src; + rendition.width = width; + rendition.height = height; + rendition.frameRate = frameRate; + rendition.bitrate = bitrate; + rendition.codec = codec; + addRendition(this, rendition); + return rendition; + } + removeRendition(rendition) { + removeRendition(rendition); + } + get selected() { + return this.#selected; + } + set selected(value) { + if (this.#selected === value) return; + this.#selected = value; + if (value !== true) return; + selectedChanged(this); + } +}; +//#endregion +export { VideoTrack }; + +//# sourceMappingURL=video-track.js.map \ No newline at end of file diff --git a/dist/default/core/media-tracks/video-track.js.map b/dist/default/core/media-tracks/video-track.js.map new file mode 100644 index 00000000..be1639ba --- /dev/null +++ b/dist/default/core/media-tracks/video-track.js.map @@ -0,0 +1 @@ +{"version":3,"file":"video-track.js","names":["#selected"],"sources":["../../../../src/core/media-tracks/video-track.ts"],"sourcesContent":["import { VideoRendition } from './video-rendition';\nimport { addRendition, removeRendition } from './video-rendition-list';\nimport { selectedChanged } from './video-track-list';\n\nexport const VideoTrackKind = {\n alternative: 'alternative',\n captions: 'captions',\n main: 'main',\n sign: 'sign',\n subtitles: 'subtitles',\n commentary: 'commentary',\n};\n\nexport class VideoTrack {\n id: string | undefined;\n kind: string | undefined;\n label = '';\n language = '';\n sourceBuffer: unknown;\n #selected = false;\n\n addRendition(src: string, width?: number, height?: number, codec?: string, bitrate?: number, frameRate?: number) {\n const rendition = new VideoRendition();\n rendition.src = src;\n rendition.width = width;\n rendition.height = height;\n rendition.frameRate = frameRate;\n rendition.bitrate = bitrate;\n rendition.codec = codec;\n addRendition(this, rendition);\n return rendition;\n }\n\n removeRendition(rendition: VideoRendition) {\n removeRendition(rendition);\n }\n\n get selected(): boolean {\n return this.#selected;\n }\n\n set selected(value: boolean) {\n if (this.#selected === value) return;\n this.#selected = value;\n\n if (value !== true) return;\n\n selectedChanged(this);\n }\n}\n"],"mappings":";;;;AAaA,IAAa,aAAb,MAAwB;CACtB;CACA;CACA,QAAQ;CACR,WAAW;CACX;CACA,YAAY;CAEZ,aAAa,KAAa,OAAgB,QAAiB,OAAgB,SAAkB,WAAoB;EAC/G,MAAM,YAAY,IAAI,gBAAgB;AACtC,YAAU,MAAM;AAChB,YAAU,QAAQ;AAClB,YAAU,SAAS;AACnB,YAAU,YAAY;AACtB,YAAU,UAAU;AACpB,YAAU,QAAQ;AAClB,eAAa,MAAM,UAAU;AAC7B,SAAO;;CAGT,gBAAgB,WAA2B;AACzC,kBAAgB,UAAU;;CAG5B,IAAI,WAAoB;AACtB,SAAO,MAAA;;CAGT,IAAI,SAAS,OAAgB;AAC3B,MAAI,MAAA,aAAmB,MAAO;AAC9B,QAAA,WAAiB;AAEjB,MAAI,UAAU,KAAM;AAEpB,kBAAgB,KAAK"} \ No newline at end of file diff --git a/dist/default/core/predicate.js b/dist/default/core/predicate.js new file mode 100644 index 00000000..aaf57507 --- /dev/null +++ b/dist/default/core/predicate.js @@ -0,0 +1,78 @@ +import { EMPTY_REMOTE, EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES } from "./constants.js"; +import { isFunction, isObject, isUndefined } from "@videojs/utils/predicate"; +//#region src/core/predicate.ts +function hasMetadata(media) { + return media.readyState >= 1; +} +function isMediaPauseCapable(value) { + if (!isObject(value)) return false; + const media = value; + return !isUndefined(media.paused) && !isUndefined(media.ended) && isFunction(media.pause); +} +function isMediaSeekCapable(value) { + if (!isObject(value)) return false; + const media = value; + return !isUndefined(media.currentTime) && !isUndefined(media.duration) && !isUndefined(media.seeking); +} +function isMediaSourceCapable(value) { + if (!isObject(value)) return false; + const media = value; + return !isUndefined(media.src) && !isUndefined(media.currentSrc) && !isUndefined(media.readyState) && isFunction(media.load); +} +function isMediaVolumeCapable(value) { + if (!isObject(value)) return false; + const media = value; + return !isUndefined(media.volume) && !isUndefined(media.muted); +} +function isMediaPlaybackRateCapable(value) { + if (!isObject(value)) return false; + return !isUndefined(value.playbackRate); +} +function isMediaBufferCapable(value) { + if (!isObject(value)) return false; + const media = value; + return !isUndefined(media.buffered) && media.buffered !== EMPTY_TIME_RANGES && !isUndefined(media.seekable) && media.seekable !== EMPTY_TIME_RANGES; +} +function isMediaErrorCapable(value) { + if (!isObject(value)) return false; + return !isUndefined(value.error); +} +function isMediaTextTrackCapable(value) { + if (!isObject(value)) return false; + const media = value; + return !isUndefined(media.textTracks) && media.textTracks !== EMPTY_TEXT_TRACKS; +} +function isMediaVideoRenditionCapable(value) { + if (!isObject(value)) return false; + return !isUndefined(value.videoRenditions); +} +function isMediaAudioTrackCapable(value) { + if (!isObject(value)) return false; + return !isUndefined(value.audioTracks); +} +function isMediaVideoDimensionsCapable(value) { + if (!isObject(value)) return false; + const media = value; + return !isUndefined(media.videoWidth) && !isUndefined(media.videoHeight); +} +function isMediaRemotePlaybackCapable(value) { + if (!isObject(value)) return false; + const media = value; + return isObject(media.remote) && media.remote !== EMPTY_REMOTE; +} +function isMediaStreamTypeCapable(value) { + if (!isObject(value)) return false; + return !isUndefined(value.streamType); +} +function isMediaLiveCapable(value) { + if (!isObject(value)) return false; + const media = value; + return !isUndefined(media.liveEdgeStart) && !isUndefined(media.targetLiveWindow); +} +function isQuerySelectorAllCapable(value) { + return isObject(value) && "querySelectorAll" in value && isFunction(value.querySelectorAll); +} +//#endregion +export { hasMetadata, isMediaAudioTrackCapable, isMediaBufferCapable, isMediaErrorCapable, isMediaLiveCapable, isMediaPauseCapable, isMediaPlaybackRateCapable, isMediaRemotePlaybackCapable, isMediaSeekCapable, isMediaSourceCapable, isMediaStreamTypeCapable, isMediaTextTrackCapable, isMediaVideoDimensionsCapable, isMediaVideoRenditionCapable, isMediaVolumeCapable, isQuerySelectorAllCapable }; + +//# sourceMappingURL=predicate.js.map \ No newline at end of file diff --git a/dist/default/core/predicate.js.map b/dist/default/core/predicate.js.map new file mode 100644 index 00000000..b0d27f3c --- /dev/null +++ b/dist/default/core/predicate.js.map @@ -0,0 +1 @@ +{"version":3,"file":"predicate.js","names":["media"],"sources":["../../../src/core/predicate.ts"],"sourcesContent":["import { isFunction, isObject, isUndefined } from '@videojs/utils/predicate';\n\nimport { EMPTY_REMOTE, EMPTY_TEXT_TRACKS, EMPTY_TIME_RANGES } from './constants';\nimport type {\n MediaAudioTrackCapability,\n MediaBufferCapability,\n MediaErrorCapability,\n MediaLiveCapability,\n MediaPauseCapability,\n MediaPlaybackRateCapability,\n MediaRemotePlaybackCapability,\n MediaSeekCapability,\n MediaSourceCapability,\n MediaStreamTypeCapability,\n MediaTextTrackCapability,\n MediaVideoDimensionsCapability,\n MediaVideoRenditionCapability,\n MediaVolumeCapability,\n} from './types';\n\nexport function hasMetadata(media: MediaSourceCapability): boolean {\n return media.readyState >= 1;\n}\n\nexport function isMediaPauseCapable(value: unknown): value is MediaPauseCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return !isUndefined(media.paused) && !isUndefined(media.ended) && isFunction(media.pause);\n}\n\nexport function isMediaSeekCapable(value: unknown): value is MediaSeekCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return !isUndefined(media.currentTime) && !isUndefined(media.duration) && !isUndefined(media.seeking);\n}\n\nexport function isMediaSourceCapable(value: unknown): value is MediaSourceCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return (\n !isUndefined(media.src) &&\n !isUndefined(media.currentSrc) &&\n !isUndefined(media.readyState) &&\n isFunction(media.load)\n );\n}\n\nexport function isMediaVolumeCapable(value: unknown): value is MediaVolumeCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return !isUndefined(media.volume) && !isUndefined(media.muted);\n}\n\nexport function isMediaPlaybackRateCapable(value: unknown): value is MediaPlaybackRateCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return !isUndefined(media.playbackRate);\n}\n\nexport function isMediaBufferCapable(value: unknown): value is MediaBufferCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return (\n !isUndefined(media.buffered) &&\n media.buffered !== EMPTY_TIME_RANGES &&\n !isUndefined(media.seekable) &&\n media.seekable !== EMPTY_TIME_RANGES\n );\n}\n\nexport function isMediaErrorCapable(value: unknown): value is MediaErrorCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return !isUndefined(media.error);\n}\n\nexport function isMediaTextTrackCapable(value: unknown): value is MediaTextTrackCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return !isUndefined(media.textTracks) && media.textTracks !== EMPTY_TEXT_TRACKS;\n}\n\nexport function isMediaVideoRenditionCapable(value: unknown): value is MediaVideoRenditionCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return !isUndefined(media.videoRenditions);\n}\n\nexport function isMediaAudioTrackCapable(value: unknown): value is MediaAudioTrackCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return !isUndefined(media.audioTracks);\n}\n\nexport function isMediaVideoDimensionsCapable(value: unknown): value is MediaVideoDimensionsCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return !isUndefined(media.videoWidth) && !isUndefined(media.videoHeight);\n}\n\nexport function isMediaRemotePlaybackCapable(value: unknown): value is MediaRemotePlaybackCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return isObject(media.remote) && media.remote !== EMPTY_REMOTE;\n}\n\nexport function isMediaStreamTypeCapable(value: unknown): value is MediaStreamTypeCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return !isUndefined(media.streamType);\n}\n\nexport function isMediaLiveCapable(value: unknown): value is MediaLiveCapability {\n if (!isObject(value)) return false;\n const media = value as Record;\n return !isUndefined(media.liveEdgeStart) && !isUndefined(media.targetLiveWindow);\n}\n\n/** Framework-agnostic `NodeList`-like shape returned by `querySelectorAll`. */\nexport interface NodeListLike {\n readonly length: number;\n readonly [index: number]: Element;\n item(index: number): Element | null;\n [Symbol.iterator](): Iterator;\n}\n\nexport function isQuerySelectorAllCapable(\n value: unknown\n): value is { querySelectorAll: (selectors: string) => NodeListLike } {\n return (\n isObject(value) && 'querySelectorAll' in value && isFunction((value as Record).querySelectorAll)\n );\n}\n"],"mappings":";;;AAoBA,SAAgB,YAAY,OAAuC;AACjE,QAAO,MAAM,cAAc;;AAG7B,SAAgB,oBAAoB,OAA+C;AACjF,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;CAC7B,MAAM,QAAQ;AACd,QAAO,CAAC,YAAY,MAAM,OAAO,IAAI,CAAC,YAAY,MAAM,MAAM,IAAI,WAAW,MAAM,MAAM;;AAG3F,SAAgB,mBAAmB,OAA8C;AAC/E,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;CAC7B,MAAM,QAAQ;AACd,QAAO,CAAC,YAAY,MAAM,YAAY,IAAI,CAAC,YAAY,MAAM,SAAS,IAAI,CAAC,YAAY,MAAM,QAAQ;;AAGvG,SAAgB,qBAAqB,OAAgD;AACnF,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;CAC7B,MAAM,QAAQ;AACd,QACE,CAAC,YAAY,MAAM,IAAI,IACvB,CAAC,YAAY,MAAM,WAAW,IAC9B,CAAC,YAAY,MAAM,WAAW,IAC9B,WAAW,MAAM,KAAK;;AAI1B,SAAgB,qBAAqB,OAAgD;AACnF,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;CAC7B,MAAM,QAAQ;AACd,QAAO,CAAC,YAAY,MAAM,OAAO,IAAI,CAAC,YAAY,MAAM,MAAM;;AAGhE,SAAgB,2BAA2B,OAAsD;AAC/F,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;AAE7B,QAAO,CAAC,YAAYA,MAAM,aAAa;;AAGzC,SAAgB,qBAAqB,OAAgD;AACnF,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;CAC7B,MAAM,QAAQ;AACd,QACE,CAAC,YAAY,MAAM,SAAS,IAC5B,MAAM,aAAa,qBACnB,CAAC,YAAY,MAAM,SAAS,IAC5B,MAAM,aAAa;;AAIvB,SAAgB,oBAAoB,OAA+C;AACjF,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;AAE7B,QAAO,CAAC,YAAYA,MAAM,MAAM;;AAGlC,SAAgB,wBAAwB,OAAmD;AACzF,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;CAC7B,MAAM,QAAQ;AACd,QAAO,CAAC,YAAY,MAAM,WAAW,IAAI,MAAM,eAAe;;AAGhE,SAAgB,6BAA6B,OAAwD;AACnG,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;AAE7B,QAAO,CAAC,YAAYA,MAAM,gBAAgB;;AAG5C,SAAgB,yBAAyB,OAAoD;AAC3F,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;AAE7B,QAAO,CAAC,YAAYA,MAAM,YAAY;;AAGxC,SAAgB,8BAA8B,OAAyD;AACrG,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;CAC7B,MAAM,QAAQ;AACd,QAAO,CAAC,YAAY,MAAM,WAAW,IAAI,CAAC,YAAY,MAAM,YAAY;;AAG1E,SAAgB,6BAA6B,OAAwD;AACnG,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;CAC7B,MAAM,QAAQ;AACd,QAAO,SAAS,MAAM,OAAO,IAAI,MAAM,WAAW;;AAGpD,SAAgB,yBAAyB,OAAoD;AAC3F,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;AAE7B,QAAO,CAAC,YAAYA,MAAM,WAAW;;AAGvC,SAAgB,mBAAmB,OAA8C;AAC/E,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;CAC7B,MAAM,QAAQ;AACd,QAAO,CAAC,YAAY,MAAM,cAAc,IAAI,CAAC,YAAY,MAAM,iBAAiB;;AAWlF,SAAgB,0BACd,OAC6E;AAC7E,QACE,SAAS,MAAM,IAAI,sBAAsB,SAAS,WAAY,MAAkC,iBAAiB"} \ No newline at end of file diff --git a/dist/default/core/types.js b/dist/default/core/types.js new file mode 100644 index 00000000..18de76f4 --- /dev/null +++ b/dist/default/core/types.js @@ -0,0 +1,23 @@ +//#region src/core/types.ts +function TypedEventTarget() { + return EventTarget; +} +/** +* Canonical values for {@link MediaStreamType}. +* +* - `ON_DEMAND` — a finite-duration asset (VOD). Scrubbing is generally +* supported across the full timeline. +* - `LIVE` — a live or DVR stream. The seekable window may slide as new +* segments are published, and `duration` is typically `Infinity`. +* - `UNKNOWN` — the stream type has not been determined yet (no source, +* or metadata has not loaded). +*/ +const MediaStreamTypes = { + ON_DEMAND: "on-demand", + LIVE: "live", + UNKNOWN: "unknown" +}; +//#endregion +export { MediaStreamTypes, TypedEventTarget }; + +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/dist/default/core/types.js.map b/dist/default/core/types.js.map new file mode 100644 index 00000000..4abf5f98 --- /dev/null +++ b/dist/default/core/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","names":[],"sources":["../../../src/core/types.ts"],"sourcesContent":["// ----------------------------------------\n// Event primitives\n// ----------------------------------------\n\nexport interface EventLike {\n readonly type: string;\n readonly timeStamp: number;\n readonly detail?: Detail;\n}\n\nexport interface EventTargetLike {\n addEventListener(\n type: K,\n listener: (event: Events[K]) => void,\n options?: { signal?: AbortSignal }\n ): void;\n removeEventListener(type: K, listener: (event: Events[K]) => void): void;\n dispatchEvent(event: EventLike): boolean;\n}\n\nexport function TypedEventTarget() {\n return EventTarget as unknown as { new (): EventTargetLike };\n}\n\n// ----------------------------------------\n// Shared value types\n// ----------------------------------------\n\nexport type MediaFeatureAvailability = 'available' | 'unavailable' | 'unsupported';\n\n// ----------------------------------------\n// Controls\n// ----------------------------------------\n\nexport interface MediaControlsCapability {\n controls: boolean;\n}\n\n// ----------------------------------------\n// Playback\n// ----------------------------------------\n\nexport interface MediaPlaybackEvents {\n play: EventLike;\n playing: EventLike;\n waiting: EventLike;\n}\n\nexport interface MediaPlaybackCapability {\n play(): Promise;\n}\n\n// ----------------------------------------\n// Autoplay\n// ----------------------------------------\n\nexport interface MediaAutoplayCapability {\n autoplay: boolean;\n}\n\n// ----------------------------------------\n// Pause\n// ----------------------------------------\n\nexport interface MediaPauseEvents {\n pause: EventLike;\n ended: EventLike;\n}\n\nexport interface MediaPauseCapability {\n pause(): void;\n readonly paused: boolean;\n readonly ended: boolean;\n}\n\n// ----------------------------------------\n// Seek\n// ----------------------------------------\n\nexport interface MediaSeekEvents {\n timeupdate: EventLike;\n durationchange: EventLike;\n seeking: EventLike;\n seeked: EventLike;\n loadedmetadata: EventLike;\n}\n\nexport interface MediaSeekCapability {\n currentTime: number;\n loop: boolean;\n readonly duration: number;\n readonly seeking: boolean;\n}\n\n// ----------------------------------------\n// Source\n// ----------------------------------------\n\nexport type MediaPreloadType = '' | 'none' | 'metadata' | 'auto';\n\nconst MediaReadyState = {\n HAVE_NOTHING: 0,\n HAVE_METADATA: 1,\n HAVE_CURRENT_DATA: 2,\n HAVE_FUTURE_DATA: 3,\n HAVE_ENOUGH_DATA: 4,\n} as const;\n\nexport type MediaReadyStateValue = (typeof MediaReadyState)[keyof typeof MediaReadyState];\n\nexport interface MediaSourceEvents {\n loadstart: EventLike;\n emptied: EventLike;\n canplay: EventLike;\n canplaythrough: EventLike;\n loadeddata: EventLike;\n abort: EventLike;\n stalled: EventLike;\n suspend: EventLike;\n}\n\n/** Result of {@link MediaSourceCapability.canPlayType}. */\nexport type CanPlayTypeResult = '' | 'maybe' | 'probably';\n\nexport interface MediaSourceCapability {\n src: string;\n readonly currentSrc: string;\n readonly readyState: MediaReadyStateValue | number;\n preload: MediaPreloadType;\n crossOrigin: string | null;\n load(): Promise | void;\n canPlayType(type: string): CanPlayTypeResult;\n}\n\n// ----------------------------------------\n// Volume\n// ----------------------------------------\n\nexport interface MediaVolumeEvents {\n volumechange: EventLike;\n}\n\nexport interface MediaVolumeCapability {\n volume: number;\n muted: boolean;\n defaultMuted: boolean;\n}\n\n// ----------------------------------------\n// Playback rate\n// ----------------------------------------\n\nexport interface MediaPlaybackRateEvents {\n ratechange: EventLike;\n}\n\nexport interface MediaPlaybackRateCapability {\n playbackRate: number;\n defaultPlaybackRate: number;\n}\n\n// ----------------------------------------\n// Buffer\n// ----------------------------------------\n\nexport interface TimeRangeLike {\n readonly length: number;\n start(index: number): number;\n end(index: number): number;\n}\n\nexport interface MediaBufferEvents {\n progress: EventLike;\n}\n\nexport interface MediaBufferCapability {\n readonly buffered: TimeRangeLike;\n readonly seekable: TimeRangeLike;\n}\n\n// ----------------------------------------\n// Played\n// ----------------------------------------\n\nexport interface MediaPlayedCapability {\n readonly played: TimeRangeLike;\n}\n\n// ----------------------------------------\n// Error\n// ----------------------------------------\n\nexport interface ErrorLike {\n readonly code: number;\n readonly message: string;\n}\n\nexport interface MediaErrorEvents {\n error: EventLike;\n}\n\nexport interface MediaErrorCapability {\n readonly error: ErrorLike | null;\n}\n\n// ----------------------------------------\n// Text tracks\n// ----------------------------------------\n\nexport interface TextCueLike {\n readonly startTime: number;\n readonly endTime: number;\n readonly text?: string;\n}\n\nexport interface TextCueListLike {\n readonly length: number;\n [Symbol.iterator](): Iterator;\n getCueById?(id: string): TextCueLike | null;\n}\n\n/**\n * The kind of text track.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack/kind\n */\nexport type TextTrackKind = 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';\n\nexport interface TextTrackLike {\n readonly kind: string;\n readonly label: string;\n readonly language: string;\n readonly id: string;\n readonly src?: string;\n mode: 'showing' | 'disabled' | 'hidden';\n readonly cues: TextCueListLike | null;\n addCue?(cue: TextCueLike): void;\n}\n\nexport interface TextTrackListEvents {\n addtrack: EventLike;\n removetrack: EventLike;\n change: EventLike;\n}\n\nexport interface TextTrackListLike extends EventTargetLike {\n readonly length: number;\n readonly [index: number]: TextTrackLike;\n [Symbol.iterator](): Iterator;\n getTrackById?(id: string): TextTrackLike | null;\n}\n\nexport interface MediaTextTrackCapability {\n readonly textTracks: TextTrackListLike;\n addTextTrack(kind: TextTrackKind, label?: string, language?: string): TextTrackLike;\n}\n\n// ----------------------------------------\n// Media tracks\n// ----------------------------------------\n\ninterface MediaTrackEventLike extends EventLike {\n readonly track: Track;\n}\n\ninterface MediaTrackListEvents {\n addtrack: MediaTrackEventLike;\n removetrack: MediaTrackEventLike;\n change: EventLike;\n}\n\nexport interface AudioTrackLike {\n id: string | undefined;\n readonly kind: string | undefined;\n readonly label: string;\n readonly language: string;\n enabled: boolean;\n addRendition(src: string, codec?: string | undefined, bitrate?: number | undefined): AudioRenditionLike;\n removeRendition(rendition: AudioRenditionLike): void;\n}\n\nexport interface AudioTrackListLike extends EventTargetLike> {\n readonly length: number;\n readonly [index: number]: AudioTrackLike;\n [Symbol.iterator](): Iterator;\n getTrackById(id: string): AudioTrackLike | null;\n}\n\nexport interface VideoTrackLike {\n id: string | undefined;\n readonly kind: string | undefined;\n readonly label: string;\n readonly language: string;\n selected: boolean;\n addRendition(\n src: string,\n width?: number | undefined,\n height?: number | undefined,\n codec?: string | undefined,\n bitrate?: number | undefined,\n frameRate?: number | undefined\n ): VideoRenditionLike;\n removeRendition(rendition: VideoRenditionLike): void;\n}\n\nexport interface VideoTrackListLike extends EventTargetLike> {\n readonly length: number;\n readonly [index: number]: VideoTrackLike;\n [Symbol.iterator](): Iterator;\n getTrackById(id: string): VideoTrackLike | null;\n readonly selectedIndex: number;\n}\n\nexport interface MediaAudioTrackCapability {\n readonly audioTracks: AudioTrackListLike;\n addAudioTrack(kind: string, label?: string, language?: string): AudioTrackLike;\n removeAudioTrack(track: AudioTrackLike): void;\n}\n\nexport interface MediaVideoTrackCapability {\n readonly videoTracks: VideoTrackListLike;\n addVideoTrack(kind: string, label?: string, language?: string): VideoTrackLike;\n removeVideoTrack(track: VideoTrackLike): void;\n}\n\n// ----------------------------------------\n// Renditions\n// ----------------------------------------\n\ninterface RenditionEventLike extends EventLike {\n readonly rendition: Rendition;\n}\n\ninterface RenditionListEvents {\n addrendition: RenditionEventLike;\n removerendition: RenditionEventLike;\n change: EventLike;\n}\n\nexport interface AudioRenditionLike {\n id: string | undefined;\n readonly bitrate: number | undefined;\n readonly codec: string | undefined;\n selected: boolean;\n}\n\nexport interface AudioRenditionListLike extends EventTargetLike> {\n readonly length: number;\n readonly [index: number]: AudioRenditionLike;\n [Symbol.iterator](): Iterator;\n getRenditionById(id: string): AudioRenditionLike | null;\n selectedIndex: number;\n}\n\nexport interface VideoRenditionLike {\n id: string | undefined;\n readonly width: number | undefined;\n readonly height: number | undefined;\n readonly bitrate: number | undefined;\n readonly frameRate: number | undefined;\n readonly codec: string | undefined;\n selected: boolean;\n active?: boolean | undefined;\n}\n\ninterface VideoRenditionListEvents extends RenditionListEvents {\n activechange: EventLike;\n}\n\nexport interface VideoRenditionListLike extends EventTargetLike {\n readonly length: number;\n readonly [index: number]: VideoRenditionLike;\n [Symbol.iterator](): Iterator;\n getRenditionById(id: string): VideoRenditionLike | null;\n selectedIndex: number;\n}\n\nexport interface MediaAudioRenditionCapability {\n readonly audioRenditions: AudioRenditionListLike;\n}\n\nexport interface MediaVideoRenditionCapability {\n readonly videoRenditions: VideoRenditionListLike;\n}\n\n// ----------------------------------------\n// Fullscreen\n// ----------------------------------------\n\nexport interface MediaFullscreenCapability {\n readonly isFullscreen: boolean;\n requestFullscreen(): Promise;\n exitFullscreen(): Promise;\n}\n\n// ----------------------------------------\n// Picture-in-picture\n// ----------------------------------------\n\nexport interface MediaPictureInPictureEvents {\n enterpictureinpicture: EventLike;\n leavepictureinpicture: EventLike;\n}\n\nexport interface MediaPictureInPictureCapability {\n readonly isPictureInPicture: boolean;\n disablePictureInPicture: boolean;\n requestPictureInPicture(): Promise;\n exitPictureInPicture(): Promise;\n}\n\n// ----------------------------------------\n// Stream type\n// ----------------------------------------\n\n/**\n * Canonical values for {@link MediaStreamType}.\n *\n * - `ON_DEMAND` — a finite-duration asset (VOD). Scrubbing is generally\n * supported across the full timeline.\n * - `LIVE` — a live or DVR stream. The seekable window may slide as new\n * segments are published, and `duration` is typically `Infinity`.\n * - `UNKNOWN` — the stream type has not been determined yet (no source,\n * or metadata has not loaded).\n */\nexport const MediaStreamTypes = {\n ON_DEMAND: 'on-demand',\n LIVE: 'live',\n UNKNOWN: 'unknown',\n} as const;\n\nexport type MediaStreamType = (typeof MediaStreamTypes)[keyof typeof MediaStreamTypes];\n\nexport interface MediaStreamTypeEvents {\n streamtypechange: EventLike;\n}\n\nexport interface MediaStreamTypeCapability {\n streamType: MediaStreamType;\n}\n\nexport interface MediaLiveEvents {\n targetlivewindowchange: EventLike;\n}\n\nexport interface MediaLiveCapability {\n /**\n * Presentation time marking the start of the Live Edge Window. Playing at\n * the live edge when `currentTime >= liveEdgeStart`. `NaN` when the stream\n * isn't live or the value is unknown.\n *\n * Derived — no dedicated change event; re-read when `seekable`,\n * `targetLiveWindow`, or `streamType` change.\n *\n * @see https://github.com/video-dev/media-ui-extensions/blob/main/proposals/0007-live-edge.md\n */\n readonly liveEdgeStart: number;\n /**\n * Offset representing the seekable range size for live content. `0` for\n * standard latency live, `Infinity` for DVR, `NaN` for on-demand or\n * unknown. Fires `targetlivewindowchange` when the value changes.\n */\n readonly targetLiveWindow: number;\n}\n\n// ----------------------------------------\n// Remote playback\n// ----------------------------------------\n\nexport interface RemotePlaybackEvents {\n connecting: EventLike;\n connect: EventLike;\n disconnect: EventLike;\n}\n\nexport interface RemotePlaybackLike extends EventTargetLike {\n readonly state: 'connecting' | 'connected' | 'disconnected';\n prompt(): Promise;\n watchAvailability(callback: (available: boolean) => void): Promise;\n cancelWatchAvailability(id?: number): Promise;\n}\n\nexport interface MediaRemotePlaybackCapability {\n readonly remote: RemotePlaybackLike;\n disableRemotePlayback: boolean;\n}\n\n// ----------------------------------------\n// Plays inline (video-only)\n// ----------------------------------------\n\nexport interface MediaPlaysInlineCapability {\n playsInline: boolean;\n}\n\n// ----------------------------------------\n// Poster (video-only)\n// ----------------------------------------\n\nexport interface MediaPosterCapability {\n poster: string;\n}\n\n// ----------------------------------------\n// Video dimensions (video-only)\n// ----------------------------------------\n\nexport interface MediaVideoDimensionsEvents {\n resize: EventLike;\n}\n\nexport interface MediaVideoDimensionsCapability {\n readonly videoWidth: number;\n readonly videoHeight: number;\n}\n\n// ----------------------------------------\n// Config\n// ----------------------------------------\n\nexport interface MediaConfigCapability {\n config: Record;\n}\n\n// ----------------------------------------\n// Base Media\n// ----------------------------------------\n\nexport interface MediaEvents extends MediaPlaybackEvents {}\n\nexport interface Media\n extends MediaPlaybackCapability,\n EventTargetLike {}\n\n// ----------------------------------------\n// Composed shapes\n// ----------------------------------------\n\nexport interface MediaFullEvents\n extends MediaEvents,\n MediaPauseEvents,\n MediaSeekEvents,\n MediaSourceEvents,\n MediaVolumeEvents,\n MediaPlaybackRateEvents,\n MediaBufferEvents,\n MediaErrorEvents,\n TextTrackListEvents,\n MediaStreamTypeEvents,\n MediaLiveEvents {}\n\nexport interface MediaFull\n extends Media,\n MediaPauseCapability,\n MediaSeekCapability,\n MediaSourceCapability,\n MediaVolumeCapability,\n MediaPlaybackRateCapability,\n MediaBufferCapability,\n MediaPlayedCapability,\n MediaErrorCapability,\n MediaTextTrackCapability,\n MediaStreamTypeCapability,\n MediaLiveCapability,\n MediaRemotePlaybackCapability,\n MediaControlsCapability,\n MediaAutoplayCapability,\n MediaConfigCapability {}\n\nexport interface VideoEvents extends MediaFullEvents, MediaPictureInPictureEvents, MediaVideoDimensionsEvents {}\n\nexport interface Video\n extends MediaFull,\n MediaPlaysInlineCapability,\n MediaPosterCapability,\n MediaFullscreenCapability,\n MediaPictureInPictureCapability,\n MediaVideoDimensionsCapability {}\n\nexport interface AudioEvents extends MediaFullEvents {}\n\nexport interface Audio extends MediaFull {}\n\n// ----------------------------------------\n// Target shapes\n// ----------------------------------------\n\nexport interface MediaTargetLike\n extends MediaPlaybackCapability,\n MediaPauseCapability,\n MediaSeekCapability,\n MediaSourceCapability,\n MediaVolumeCapability,\n MediaPlaybackRateCapability,\n MediaBufferCapability,\n MediaPlayedCapability,\n MediaErrorCapability,\n MediaTextTrackCapability,\n MediaRemotePlaybackCapability,\n MediaControlsCapability,\n MediaAutoplayCapability,\n Partial,\n Partial,\n Partial {\n title: string;\n}\n\nexport interface VideoTargetLike\n extends MediaTargetLike,\n MediaPosterCapability,\n MediaPlaysInlineCapability,\n MediaVideoDimensionsCapability {\n disablePictureInPicture: boolean;\n requestPictureInPicture(): Promise;\n requestFullscreen(): Promise;\n}\n\nexport interface MediaEngineHost {\n readonly engine: Engine | null;\n attach?(target: Target): void;\n detach?(): void;\n destroy(): void;\n}\n"],"mappings":";AAoBA,SAAgB,mBAAsE;AACpF,QAAO;;;;;;;;;;;;AAoZT,MAAa,mBAAmB;CAC9B,WAAW;CACX,MAAM;CACN,SAAS;CACV"} \ No newline at end of file diff --git a/dist/default/dom.js b/dist/default/dom.js new file mode 100644 index 00000000..e69de29b diff --git a/dist/default/dom/audio-host/audio-host.js b/dist/default/dom/audio-host/audio-host.js new file mode 100644 index 00000000..4799df5a --- /dev/null +++ b/dist/default/dom/audio-host/audio-host.js @@ -0,0 +1,7 @@ +import { HTMLMediaElementHost } from "../media-host/media-host.js"; +//#region src/dom/audio-host/audio-host.ts +var HTMLAudioElementHost = class extends HTMLMediaElementHost {}; +//#endregion +export { HTMLAudioElementHost }; + +//# sourceMappingURL=audio-host.js.map \ No newline at end of file diff --git a/dist/default/dom/audio-host/audio-host.js.map b/dist/default/dom/audio-host/audio-host.js.map new file mode 100644 index 00000000..e973da3f --- /dev/null +++ b/dist/default/dom/audio-host/audio-host.js.map @@ -0,0 +1 @@ +{"version":3,"file":"audio-host.js","names":[],"sources":["../../../../src/dom/audio-host/audio-host.ts"],"sourcesContent":["import type { Audio, AudioEvents } from '../../core/types';\nimport { HTMLMediaElementHost, type HTMLMediaTargetLike } from '../media-host';\n\nexport class HTMLAudioElementHost extends HTMLMediaElementHost implements Audio {}\n"],"mappings":";;AAGA,IAAa,uBAAb,cAA0C,qBAAwE"} \ No newline at end of file diff --git a/dist/default/dom/audio-host/index.js b/dist/default/dom/audio-host/index.js new file mode 100644 index 00000000..eb5afeb1 --- /dev/null +++ b/dist/default/dom/audio-host/index.js @@ -0,0 +1,2 @@ +import { HTMLAudioElementHost } from "./audio-host.js"; +export { HTMLAudioElementHost }; diff --git a/dist/default/dom/custom-media-element/custom-media-element.js b/dist/default/dom/custom-media-element/custom-media-element.js new file mode 100644 index 00000000..88546c7a --- /dev/null +++ b/dist/default/dom/custom-media-element/custom-media-element.js @@ -0,0 +1,295 @@ +import { namedNodeMapToObject, serializeAttributes } from "@videojs/utils/dom"; +import { omit, pick } from "@videojs/utils/object"; +import { kebabCase } from "@videojs/utils/string"; +//#region src/dom/custom-media-element/custom-media-element.ts +/** CSS custom property names for video elements. */ +const VideoCSSVars = { + /** Border radius of the video element. */ + borderRadius: "--media-video-border-radius", + /** Object fit for the video. */ + objectFit: "--media-object-fit", + /** Object position for the video. */ + objectPosition: "--media-object-position", + /** Duration of the caption track transition. */ + captionTrackDuration: "--media-caption-track-duration", + /** Delay before the caption track transition. */ + captionTrackDelay: "--media-caption-track-delay", + /** Vertical offset of the caption track. */ + captionTrackY: "--media-caption-track-y" +}; +/** CSS custom property names for audio elements. */ +const AudioCSSVars = {}; +/** Helper function to generate the HTML template for video elements. */ +function getVideoTemplateHTML(attrs) { + return ` + + + + + + `; +} +/** Helper function to generate the HTML template for other elements. */ +function getCommonTemplateHTML(tag) { + return (attrs) => { + return ` + + + <${tag}${serializeAttributes(attrs)}> + + + `; + }; +} +const excludedProperties = [ + "attach", + "detach", + "destroy" +]; +function CustomMediaElement(tag, MediaHost) { + const syncTargetAttributes = tag !== "iframe"; + const mediaHostAttrToProp = /* @__PURE__ */ new Map(); + let isDefined = false; + class CustomMedia extends (globalThis.HTMLElement ?? class {}) { + static getTemplateHTML = tag.endsWith("video") ? getVideoTemplateHTML : getCommonTemplateHTML(tag); + static shadowRootOptions = { mode: "open" }; + static properties = { + autoPictureInPicture: { type: Boolean }, + autoplay: { type: Boolean }, + controls: { type: Boolean }, + controlsList: { type: String }, + crossOrigin: { type: String }, + defaultMuted: { + type: Boolean, + attribute: "muted" + }, + disablePictureInPicture: { type: Boolean }, + disableRemotePlayback: { type: Boolean }, + loading: { type: String }, + loop: { type: Boolean }, + playsInline: { type: Boolean }, + poster: { + type: String, + empty: "" + }, + preload: { + type: String, + empty: null + }, + src: { + type: String, + empty: "" + }, + streamType: { + type: String, + attribute: "stream-type", + empty: "unknown" + } + }; + static get observedAttributes() { + CustomMedia.#define(this); + return [...getAttrsFromProps(this.properties)]; + } + static #define(ctor) { + if (isDefined) return; + isDefined = true; + const properties = ctor.properties; + for (let proto = MediaHost.prototype; proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) for (const prop of Object.getOwnPropertyNames(proto)) { + if (prop in CustomMedia.prototype || excludedProperties.includes(prop)) continue; + const propConfig = properties[prop]; + if (propConfig && (propConfig.attribute ?? prop.toLowerCase()) !== kebabCase(prop)) continue; + const descriptor = Object.getOwnPropertyDescriptor(proto, prop); + if (!descriptor) continue; + const config = { + enumerable: true, + configurable: true + }; + if (typeof descriptor.value === "function") config.value = function(...args) { + return this.#mediaHost[prop](...args); + }; + else if (descriptor.get) { + config.get = function() { + return this.#mediaHost[prop]; + }; + if (descriptor.set) { + const attr = kebabCase(prop); + if (ctor.observedAttributes.includes(attr)) { + mediaHostAttrToProp.set(attr, prop); + config.set = function(val) { + if (val === true || val === false || val == null) this.toggleAttribute(attr, Boolean(val)); + else this.setAttribute(attr, String(val)); + }; + } else config.set = function(val) { + this.#mediaHost[prop] = val; + }; + } + } + Object.defineProperty(CustomMedia.prototype, prop, config); + } + for (const [prop, { type, attribute }] of Object.entries(properties)) { + if (prop in CustomMedia.prototype) continue; + const attr = attribute ?? prop.toLowerCase(); + Object.defineProperty(CustomMedia.prototype, prop, { + get: function() { + return type === Boolean ? this.hasAttribute(attr) : this.getAttribute(attr); + }, + set: function(val) { + if (type === Boolean) this.toggleAttribute(attr, Boolean(val)); + else this.setAttribute(attr, val); + }, + enumerable: true, + configurable: true + }); + } + } + #mediaHost; + #bridgedEventTypes = /* @__PURE__ */ new Set(); + #childMap = /* @__PURE__ */ new Map(); + #childObserver; + constructor() { + super(); + if (!this.shadowRoot) { + const ctor = this.constructor; + this.attachShadow(ctor.shadowRootOptions); + const allowedKeys = getAttrsFromProps(ctor.properties); + const disallowedKeys = [...mediaHostAttrToProp.keys()]; + const pickedAttrs = pick(namedNodeMapToObject(this.attributes), allowedKeys); + const attrs = syncTargetAttributes ? omit(pickedAttrs, disallowedKeys) : pickedAttrs; + if (tag && !attrs.part) attrs.part = tag; + this.shadowRoot.innerHTML = ctor.getTemplateHTML(attrs); + } + this.#mediaHost = new MediaHost(); + this.#attachToTarget(); + this.#childObserver = new MutationObserver(this.#syncMediaChildAttribute.bind(this)); + this.shadowRoot.addEventListener("slotchange", () => { + this.#attachToTarget(); + this.#syncMediaChildren(); + }); + this.#syncMediaChildren(); + } + #attachToTarget() { + const target = this.target; + if (target === this.#mediaHost.target) return; + if (this.#mediaHost.target) this.#mediaHost.detach(); + this.#mediaHost.attach(target); + } + get host() { + return this.#mediaHost; + } + get target() { + return this.querySelector(":scope > [slot=media]") ?? this.querySelector(tag) ?? this.shadowRoot?.querySelector(tag) ?? null; + } + connectedCallback() { + if (tag !== "iframe") return; + if (!this.hasAttribute("data-cross-origin-frame")) this.setAttribute("data-cross-origin-frame", ""); + } + disconnectedCallback() { + if (this.hasAttribute("keep-alive")) return; + queueMicrotask(() => { + if (!this.isConnected) this.#mediaHost.destroy(); + }); + } + addEventListener(type, listener, options) { + super.addEventListener(type, listener, options); + if (!this.#bridgedEventTypes.has(type)) { + this.#bridgedEventTypes.add(type); + this.#mediaHost.addEventListener(type, this.#bridgeEvent); + } + } + removeEventListener(type, listener, options) { + super.removeEventListener(type, listener, options); + } + #bridgeEvent = (event) => { + if (!event.composed) this.dispatchEvent(new event.constructor(event.type, event)); + }; + attributeChangedCallback(attrName, oldValue, newValue) { + const prop = mediaHostAttrToProp.get(attrName); + if (prop) { + if (oldValue !== newValue) { + const valueType = typeof this.#mediaHost[prop]; + const propConfig = this.constructor.properties[prop]; + const emptyValue = propConfig && "empty" in propConfig ? propConfig.empty : ""; + this.#mediaHost[prop] = valueType === "boolean" ? newValue !== null : valueType === "number" ? Number(newValue) : newValue ?? emptyValue; + } + return; + } + if (!CustomMedia.observedAttributes.includes(attrName) && this.constructor.observedAttributes.includes(attrName)) return; + if (!syncTargetAttributes) return; + if (newValue === null) this.target?.removeAttribute(attrName); + else if (this.target?.getAttribute(attrName) !== newValue) this.target?.setAttribute(attrName, newValue); + } + #syncMediaChildren() { + if (tag === "iframe") return; + const defaultSlot = this.shadowRoot?.querySelector("slot:not([name])"); + const mediaChildren = new Set(defaultSlot?.assignedElements({ flatten: true }).filter((el) => el.localName === "track" || el.localName === "source")); + for (const [el, clone] of this.#childMap) if (!mediaChildren.has(el)) { + clone.remove(); + this.#childMap.delete(el); + } + for (const el of mediaChildren) { + let clone = this.#childMap.get(el); + if (!clone) { + clone = el.cloneNode(); + this.#childMap.set(el, clone); + this.#childObserver?.observe(el, { attributes: true }); + } + this.target?.append(clone); + this.#enableDefaultTrack(clone); + } + } + #syncMediaChildAttribute(mutations) { + for (const mutation of mutations) if (mutation.type === "attributes") { + const { target, attributeName } = mutation; + const clone = this.#childMap.get(target); + if (clone && attributeName) { + clone.setAttribute(attributeName, target.getAttribute(attributeName) ?? ""); + this.#enableDefaultTrack(clone); + } + } + } + #enableDefaultTrack(trackEl) { + if (trackEl && trackEl.localName === "track" && trackEl.default && (trackEl.kind === "chapters" || trackEl.kind === "metadata") && trackEl.track.mode === "disabled") trackEl.track.mode = "hidden"; + } + } + return CustomMedia; +} +function getAttrsFromProps(props) { + return Object.keys(props).map((prop) => props[prop]?.attribute ?? prop.toLowerCase()); +} +//#endregion +export { AudioCSSVars, CustomMediaElement, VideoCSSVars }; + +//# sourceMappingURL=custom-media-element.js.map \ No newline at end of file diff --git a/dist/default/dom/custom-media-element/custom-media-element.js.map b/dist/default/dom/custom-media-element/custom-media-element.js.map new file mode 100644 index 00000000..97f7bfea --- /dev/null +++ b/dist/default/dom/custom-media-element/custom-media-element.js.map @@ -0,0 +1 @@ +{"version":3,"file":"custom-media-element.js","names":["#define","#mediaHost","#attachToTarget","#childObserver","#syncMediaChildAttribute","#syncMediaChildren","#bridgedEventTypes","#bridgeEvent","#childMap","#enableDefaultTrack"],"sources":["../../../../src/dom/custom-media-element/custom-media-element.ts"],"sourcesContent":["import { namedNodeMapToObject, serializeAttributes } from '@videojs/utils/dom';\nimport { omit, pick } from '@videojs/utils/object';\nimport { kebabCase } from '@videojs/utils/string';\nimport type { Constructor } from '@videojs/utils/types';\n\n/** CSS custom property names for video elements. */\nexport const VideoCSSVars = {\n /** Border radius of the video element. */\n borderRadius: '--media-video-border-radius',\n /** Object fit for the video. */\n objectFit: '--media-object-fit',\n /** Object position for the video. */\n objectPosition: '--media-object-position',\n /** Duration of the caption track transition. */\n captionTrackDuration: '--media-caption-track-duration',\n /** Delay before the caption track transition. */\n captionTrackDelay: '--media-caption-track-delay',\n /** Vertical offset of the caption track. */\n captionTrackY: '--media-caption-track-y',\n} as const;\n\n/** CSS custom property names for audio elements. */\nexport const AudioCSSVars = {} as const;\n\n/** Helper function to generate the HTML template for video elements. */\nfunction getVideoTemplateHTML(attrs: Record): string {\n return /*html*/ `\n \n \n \n \n \n `;\n}\n\n/** Helper function to generate the HTML template for other elements. */\nfunction getCommonTemplateHTML(tag: string) {\n return (attrs: Record) => {\n return /*html*/ `\n \n \n <${tag}${serializeAttributes(attrs)}>\n \n \n `;\n };\n}\n\nconst excludedProperties = ['attach', 'detach', 'destroy'];\n\nexport interface MediaHost extends EventTarget {\n attach(target: EventTarget | null): void;\n detach(): void;\n destroy(): void;\n /** Index signature for dynamic property forwarding (includes the host's protected `target`). */\n [key: string]: any;\n}\n\ntype CustomMediaConstructor> = Constructor<\n HTMLElement &\n InstanceType & {\n readonly host: InstanceType;\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;\n }\n> & {\n properties: Record;\n getTemplateHTML: (attrs: Record) => string;\n shadowRootOptions: ShadowRootInit;\n readonly observedAttributes: string[];\n};\n\nexport function CustomMediaElement>(\n tag: string,\n MediaHost: T\n): CustomMediaConstructor {\n // Embed hosts (iframe) drive an external player rather than a native media\n // element, so attribute changes are not mirrored onto the iframe target and\n // there is no `` / `` syncing.\n const syncTargetAttributes = tag !== 'iframe';\n const mediaHostAttrToProp = new Map();\n let isDefined = false;\n\n class CustomMedia extends (globalThis.HTMLElement ?? class {}) {\n static getTemplateHTML = tag.endsWith('video') ? getVideoTemplateHTML : getCommonTemplateHTML(tag);\n static shadowRootOptions: ShadowRootInit = { mode: 'open' };\n static properties = {\n autoPictureInPicture: { type: Boolean },\n autoplay: { type: Boolean },\n controls: { type: Boolean },\n controlsList: { type: String },\n crossOrigin: { type: String },\n defaultMuted: { type: Boolean, attribute: 'muted' },\n disablePictureInPicture: { type: Boolean },\n disableRemotePlayback: { type: Boolean },\n loading: { type: String },\n loop: { type: Boolean },\n playsInline: { type: Boolean },\n poster: { type: String, empty: '' },\n preload: { type: String, empty: null },\n src: { type: String, empty: '' },\n streamType: { type: String, attribute: 'stream-type', empty: 'unknown' },\n };\n\n static get observedAttributes() {\n // biome-ignore lint/complexity/noThisInStatic: resolves to the subclass that may override `properties`\n CustomMedia.#define(this);\n return [\n // biome-ignore lint/complexity/noThisInStatic: intentional use of this\n ...getAttrsFromProps(this.properties),\n ];\n }\n\n static #define(ctor: typeof CustomMedia) {\n if (isDefined) return;\n isDefined = true;\n\n const properties = ctor.properties as Record;\n\n for (let proto = MediaHost.prototype; proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {\n for (const prop of Object.getOwnPropertyNames(proto)) {\n if (prop in CustomMedia.prototype || excludedProperties.includes(prop)) continue;\n // Defer to the explicit `ctor.properties` loop when its attribute\n // mapping diverges from `kebabCase(prop)`. Covers multi-word camelCase\n // props (`playsInline` → `'playsinline'`) and explicit overrides\n // (`defaultMuted` → `attribute: 'muted'`). Single-word props in\n // `properties` (like `loop`, `preload`) keep their legacy proto-walk\n // path so the mediaHost still receives the setter call.\n const propConfig = properties[prop];\n if (propConfig && (propConfig.attribute ?? prop.toLowerCase()) !== kebabCase(prop)) continue;\n\n const descriptor = Object.getOwnPropertyDescriptor(proto, prop);\n if (!descriptor) continue;\n\n const config: PropertyDescriptor = {\n enumerable: true,\n configurable: true,\n };\n\n if (typeof descriptor.value === 'function') {\n config.value = function (this: CustomMedia, ...args: any[]) {\n return this.#mediaHost[prop](...args);\n };\n } else if (descriptor.get) {\n config.get = function (this: CustomMedia) {\n return this.#mediaHost[prop];\n };\n\n if (descriptor.set) {\n const attr = kebabCase(prop);\n if (ctor.observedAttributes.includes(attr)) {\n mediaHostAttrToProp.set(attr, prop);\n\n config.set = function (this: CustomMedia, val: any) {\n if (val === true || val === false || val == null) {\n this.toggleAttribute(attr, Boolean(val));\n } else {\n this.setAttribute(attr, String(val));\n }\n };\n } else {\n config.set = function (this: CustomMedia, val: any) {\n this.#mediaHost[prop] = val;\n };\n }\n }\n }\n\n Object.defineProperty(CustomMedia.prototype, prop, config);\n }\n }\n\n for (const [prop, { type, attribute }] of Object.entries(properties)) {\n if (prop in CustomMedia.prototype) continue;\n\n const attr = attribute ?? prop.toLowerCase();\n Object.defineProperty(CustomMedia.prototype, prop, {\n get: function (this: CustomMedia) {\n return type === Boolean ? this.hasAttribute(attr) : this.getAttribute(attr);\n },\n set: function (this: CustomMedia, val: any) {\n if (type === Boolean) {\n this.toggleAttribute(attr, Boolean(val));\n } else {\n this.setAttribute(attr, val);\n }\n },\n enumerable: true,\n configurable: true,\n });\n }\n }\n\n #mediaHost: MediaHost;\n #bridgedEventTypes = new Set();\n #childMap = new Map();\n #childObserver?: MutationObserver;\n\n constructor() {\n super();\n\n if (!this.shadowRoot) {\n const ctor = this.constructor as typeof CustomMedia;\n this.attachShadow(ctor.shadowRootOptions);\n\n const allowedKeys = getAttrsFromProps(ctor.properties);\n const disallowedKeys = [...mediaHostAttrToProp.keys()];\n const pickedAttrs = pick(namedNodeMapToObject(this.attributes), allowedKeys);\n // Embed templates (iframe) need host-bound attrs (e.g. `src`) to build the initial URL.\n const attrs: Record = syncTargetAttributes ? omit(pickedAttrs, disallowedKeys) : pickedAttrs;\n if (tag && !attrs.part) attrs.part = tag;\n this.shadowRoot!.innerHTML = ctor.getTemplateHTML(attrs);\n }\n\n this.#mediaHost = new MediaHost();\n this.#attachToTarget();\n\n this.#childObserver = new MutationObserver(this.#syncMediaChildAttribute.bind(this));\n this.shadowRoot!.addEventListener('slotchange', () => {\n this.#attachToTarget();\n this.#syncMediaChildren();\n });\n\n this.#syncMediaChildren();\n }\n\n #attachToTarget(): void {\n const target = this.target;\n if (target === this.#mediaHost.target) return;\n if (this.#mediaHost.target) this.#mediaHost.detach();\n this.#mediaHost.attach(target);\n }\n\n get host(): MediaHost {\n return this.#mediaHost;\n }\n\n get target(): HTMLElement | null {\n return (\n this.querySelector(':scope > [slot=media]') ??\n this.querySelector(tag) ??\n this.shadowRoot?.querySelector(tag) ??\n null\n );\n }\n\n connectedCallback() {\n if (tag !== 'iframe') return;\n // Add data attribute for styling and avoiding cross-origin issues. e.g. backdrop-filter\n if (!this.hasAttribute('data-cross-origin-frame')) {\n this.setAttribute('data-cross-origin-frame', '');\n }\n }\n\n disconnectedCallback() {\n if (this.hasAttribute('keep-alive')) return;\n // Defer so a synchronous reparent (remove + insert) doesn't tear down\n // the host and its registered components.\n queueMicrotask(() => {\n if (!this.isConnected) this.#mediaHost.destroy();\n });\n }\n\n addEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject | ((event: never) => void) | null,\n options?: boolean | AddEventListenerOptions\n ) {\n super.addEventListener(type, listener as EventListener, options);\n if (!this.#bridgedEventTypes.has(type)) {\n this.#bridgedEventTypes.add(type);\n this.#mediaHost.addEventListener(type, this.#bridgeEvent);\n }\n }\n\n removeEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject | ((event: never) => void) | null,\n options?: boolean | EventListenerOptions\n ): void {\n super.removeEventListener(type, listener as EventListener, options);\n }\n\n #bridgeEvent = (event: Event) => {\n if (!event.composed) {\n this.dispatchEvent(new (event.constructor as typeof Event)(event.type, event));\n }\n };\n\n attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string | null): void {\n const prop = mediaHostAttrToProp.get(attrName);\n if (prop) {\n if (oldValue !== newValue) {\n const valueType = typeof this.#mediaHost[prop];\n const propConfig = (this.constructor as CustomMediaConstructor).properties[prop];\n const emptyValue = propConfig && 'empty' in propConfig ? propConfig.empty : '';\n this.#mediaHost[prop] =\n valueType === 'boolean'\n ? newValue !== null\n : valueType === 'number'\n ? Number(newValue)\n : (newValue ?? emptyValue);\n }\n return;\n }\n\n if (\n !CustomMedia.observedAttributes.includes(attrName) &&\n (this.constructor as typeof CustomMedia).observedAttributes.includes(attrName)\n ) {\n return;\n }\n\n if (!syncTargetAttributes) return;\n\n if (newValue === null) {\n this.target?.removeAttribute(attrName);\n } else if (this.target?.getAttribute(attrName) !== newValue) {\n this.target?.setAttribute(attrName, newValue);\n }\n }\n\n #syncMediaChildren(): void {\n if (tag === 'iframe') return;\n\n const defaultSlot = this.shadowRoot?.querySelector('slot:not([name])') as HTMLSlotElement;\n const mediaChildren = new Set(\n defaultSlot\n ?.assignedElements({ flatten: true })\n .filter((el) => el.localName === 'track' || el.localName === 'source') as (\n | HTMLTrackElement\n | HTMLSourceElement\n )[]\n );\n\n for (const [el, clone] of this.#childMap) {\n if (!mediaChildren.has(el)) {\n clone.remove();\n this.#childMap.delete(el);\n }\n }\n\n for (const el of mediaChildren) {\n let clone = this.#childMap.get(el);\n if (!clone) {\n clone = el.cloneNode() as HTMLTrackElement | HTMLSourceElement;\n this.#childMap.set(el, clone);\n this.#childObserver?.observe(el, { attributes: true });\n }\n this.target?.append(clone);\n this.#enableDefaultTrack(clone as HTMLTrackElement);\n }\n }\n\n #syncMediaChildAttribute(mutations: MutationRecord[]): void {\n for (const mutation of mutations) {\n if (mutation.type === 'attributes') {\n const { target, attributeName } = mutation;\n const clone = this.#childMap.get(target as HTMLTrackElement | HTMLSourceElement);\n if (clone && attributeName) {\n clone.setAttribute(\n attributeName,\n (target as HTMLTrackElement | HTMLSourceElement).getAttribute(attributeName) ?? ''\n );\n this.#enableDefaultTrack(clone as HTMLTrackElement);\n }\n }\n }\n }\n\n #enableDefaultTrack(trackEl: HTMLTrackElement): void {\n // Browsers don't honor the `default` attribute if a track is added via JS.\n // Enable default tracks for chapters or metadata.\n if (\n trackEl &&\n trackEl.localName === 'track' &&\n trackEl.default &&\n (trackEl.kind === 'chapters' || trackEl.kind === 'metadata') &&\n trackEl.track.mode === 'disabled'\n ) {\n trackEl.track.mode = 'hidden';\n }\n }\n }\n\n return CustomMedia as any;\n}\n\nfunction getAttrsFromProps(props: Record): string[] {\n return Object.keys(props).map((prop) => props[prop]?.attribute ?? prop.toLowerCase());\n}\n"],"mappings":"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"} \ No newline at end of file diff --git a/dist/default/dom/custom-media-element/index.js b/dist/default/dom/custom-media-element/index.js new file mode 100644 index 00000000..cd39a12c --- /dev/null +++ b/dist/default/dom/custom-media-element/index.js @@ -0,0 +1,2 @@ +import { AudioCSSVars, CustomMediaElement, VideoCSSVars } from "./custom-media-element.js"; +export { AudioCSSVars, CustomMediaElement, VideoCSSVars }; diff --git a/dist/default/dom/dash/index.js b/dist/default/dom/dash/index.js new file mode 100644 index 00000000..f3adde10 --- /dev/null +++ b/dist/default/dom/dash/index.js @@ -0,0 +1,2 @@ +import { DashMedia, dashMediaDefaultProps } from "./media.js"; +export { DashMedia, dashMediaDefaultProps }; diff --git a/dist/default/dom/dash/media.js b/dist/default/dom/dash/media.js new file mode 100644 index 00000000..0f237db6 --- /dev/null +++ b/dist/default/dom/dash/media.js @@ -0,0 +1,47 @@ +import { HTMLVideoElementHost } from "../video-host/video-host.js"; +import { MediaTracksMixin } from "../../core/media-tracks/mixin.js"; +import * as dashjs from "dashjs"; +//#region src/dom/dash/media.ts +const dashMediaDefaultProps = { src: "" }; +const DashMediaBase = MediaTracksMixin(HTMLVideoElementHost); +var DashMedia = class extends DashMediaBase { + #engine; + #src = dashMediaDefaultProps.src; + constructor() { + super(); + this.#engine = dashjs.MediaPlayer().create(); + this.#engine.initialize(void 0, void 0, false); + } + attach(target) { + super.attach(target); + this.#engine.attachView(target); + } + detach() { + super.detach(); + this.#engine.attachView(null); + } + destroy() { + this.detach(); + this.#engine.destroy(); + super.destroy(); + } + /** + * Underlying playback engine — the dash.js `MediaPlayerClass` instance. An + * advanced escape hatch for direct engine access; normal playback is driven + * through this element's own properties and methods. + */ + get engine() { + return this.#engine; + } + get src() { + return this.#src; + } + set src(src) { + this.#src = src; + this.#engine.attachSource(src); + } +}; +//#endregion +export { DashMedia, dashMediaDefaultProps }; + +//# sourceMappingURL=media.js.map \ No newline at end of file diff --git a/dist/default/dom/dash/media.js.map b/dist/default/dom/dash/media.js.map new file mode 100644 index 00000000..adcceda3 --- /dev/null +++ b/dist/default/dom/dash/media.js.map @@ -0,0 +1 @@ +{"version":3,"file":"media.js","names":["#engine","#src"],"sources":["../../../../src/dom/dash/media.ts"],"sourcesContent":["import * as dashjs from 'dashjs';\nimport { MediaTracksMixin } from '../../core/media-tracks';\nimport type { MediaEngineHost } from '../../core/types';\nimport { HTMLVideoElementHost } from '../video-host';\n\nexport interface DashMediaProps {\n src: string;\n}\n\nexport const dashMediaDefaultProps: DashMediaProps = {\n src: '',\n};\n\nconst DashMediaBase = MediaTracksMixin(HTMLVideoElementHost);\n\nexport class DashMedia\n extends DashMediaBase\n implements MediaEngineHost, DashMediaProps\n{\n #engine: dashjs.MediaPlayerClass;\n #src = dashMediaDefaultProps.src;\n\n constructor() {\n super();\n this.#engine = dashjs.MediaPlayer().create();\n this.#engine.initialize(undefined, undefined, false);\n }\n\n attach(target: HTMLVideoElement) {\n super.attach(target);\n this.#engine.attachView(target);\n }\n\n detach() {\n super.detach();\n // dash.js types don't reflect null support, but null is valid for detaching\n this.#engine.attachView(null as unknown as HTMLVideoElement);\n }\n\n destroy() {\n this.detach();\n this.#engine.destroy();\n super.destroy();\n }\n\n /**\n * Underlying playback engine — the dash.js `MediaPlayerClass` instance. An\n * advanced escape hatch for direct engine access; normal playback is driven\n * through this element's own properties and methods.\n */\n get engine() {\n return this.#engine;\n }\n\n get src() {\n return this.#src;\n }\n\n set src(src) {\n this.#src = src;\n this.#engine.attachSource(src);\n }\n}\n"],"mappings":";;;;AASA,MAAa,wBAAwC,EACnD,KAAK,IACN;AAED,MAAM,gBAAgB,iBAAiB,qBAAqB;AAE5D,IAAa,YAAb,cACU,cAEV;CACE;CACA,OAAO,sBAAsB;CAE7B,cAAc;AACZ,SAAO;AACP,QAAA,SAAe,OAAO,aAAa,CAAC,QAAQ;AAC5C,QAAA,OAAa,WAAW,KAAA,GAAW,KAAA,GAAW,MAAM;;CAGtD,OAAO,QAA0B;AAC/B,QAAM,OAAO,OAAO;AACpB,QAAA,OAAa,WAAW,OAAO;;CAGjC,SAAS;AACP,QAAM,QAAQ;AAEd,QAAA,OAAa,WAAW,KAAoC;;CAG9D,UAAU;AACR,OAAK,QAAQ;AACb,QAAA,OAAa,SAAS;AACtB,QAAM,SAAS;;;;;;;CAQjB,IAAI,SAAS;AACX,SAAO,MAAA;;CAGT,IAAI,MAAM;AACR,SAAO,MAAA;;CAGT,IAAI,IAAI,KAAK;AACX,QAAA,MAAY;AACZ,QAAA,OAAa,aAAa,IAAI"} \ No newline at end of file diff --git a/dist/default/dom/google-cast/google-cast-provider.js b/dist/default/dom/google-cast/google-cast-provider.js new file mode 100644 index 00000000..29de3e59 --- /dev/null +++ b/dist/default/dom/google-cast/google-cast-provider.js @@ -0,0 +1,363 @@ +import { InvalidStateError, NotSupportedError, currentMedia, currentSession, editTracksInfo, getCastContext, getMediaStatus, getPlaylistSegmentFormat, isHls, setCastOptions, setPlaybackRate } from "./utils.js"; +import { castFramework, ensureCastFramework, googleCastInstances } from "./registry.js"; +import { RemotePlayback } from "./remote-playback.js"; +import { isCaptionOrSubtitleTrack } from "@videojs/utils/dom"; +//#region src/dom/google-cast/google-cast-provider.ts +/** +* Cast provider + lifecycle. Created by the {@link GoogleCast} component and +* installed as the host's `targetOverride` while a cast session is connected, +* so its getters/setters route through the cast receiver; when disconnected the +* host falls through to the attached target. Also owns the cast framework +* integration, the `RemotePlayback` instance exposed via +* {@link GoogleCastProvider#remote}, and dispatches media events on the attached +* target (forwarded by the host) while casting. +*/ +var GoogleCastProvider = class { + target = null; + #googleCast; + #hooks = {}; + #remotePlayback; + #isInit = false; + #isCasting = false; + #seeking = false; + #remotePlayer; + #remoteListeners; + #listenersAttached = false; + #playbackRate = 1; + #localPaused = false; + #onTextTrackChange = () => this.#updateRemoteTextTrack(); + #onMediaUpdate = () => this.#checkPlaybackRate(); + constructor(googleCast) { + this.#googleCast = googleCast; + this.#remotePlayback = new RemotePlayback(this); + googleCastInstances.add(this); + this.onCastFrameworkAvailable(); + } + get remote() { + if (this.target && !this.target.disableRemotePlayback) ensureCastFramework(); + return this.#remotePlayback; + } + attach(target) { + this.target = target; + target.textTracks.addEventListener("change", this.#onTextTrackChange); + } + detach() { + this.target?.textTracks.removeEventListener("change", this.#onTextTrackChange); + this.target = null; + } + destroy() { + this.detach(); + googleCastInstances.delete(this); + currentMedia()?.removeUpdateListener(this.#onMediaUpdate); + this.#detachRemoteListeners(); + this.#isCasting = false; + this.#isInit = false; + } + /** @internal Wires up callbacks pushed from {@link RemotePlayback}; not part of the public surface. */ + bindHooks(hooks) { + Object.assign(this.#hooks, hooks); + } + hasDevicesAvailable() { + const state = getCastContext()?.getCastState(); + return !!state && state !== cast.framework.CastState.NO_DEVICES_AVAILABLE; + } + async requestCastSession() { + if (this.target?.disableRemotePlayback) throw new InvalidStateError("disableRemotePlayback attribute is present."); + await ensureCastFramework(); + if (!this.#isCastApiAvailable()) throw new NotSupportedError("The RemotePlayback API is disabled on this platform."); + const willDisconnect = this.#isCasting; + this.#isCasting = true; + this.#applyCastOptions(); + this.#attachRemoteListeners(); + try { + await getCastContext().requestSession(); + } catch (err) { + if (!willDisconnect) this.#isCasting = false; + if (err === "cancel") return; + throw new Error(err); + } + this.#localPaused = this.target?.paused ?? true; + this.target?.pause(); + this.muted = this.target?.muted ?? false; + try { + await this.load(); + } catch (err) { + console.error(err); + } + } + async load() { + if (!this.#isCasting) { + await this.target?.load(); + return; + } + if (!this.#googleCast.src) return; + const mediaInfo = new chrome.cast.media.MediaInfo(this.#googleCast.src, this.#googleCast.contentType ?? ""); + mediaInfo.customData = this.#googleCast.customData ?? null; + const { target } = this; + const subtitles = [...target?.querySelectorAll("track") ?? []].filter((el) => el.src && isCaptionOrSubtitleTrack(el)); + const { Track, TrackType, TextTrackType } = chrome.cast.media; + const activeTrackIds = []; + if (subtitles.length) mediaInfo.tracks = subtitles.map((el, i) => { + const trackId = i + 1; + if (!activeTrackIds.length && el.track.mode === "showing") activeTrackIds.push(trackId); + const track = new Track(trackId, TrackType.TEXT); + track.trackContentId = el.src; + track.trackContentType = "text/vtt"; + track.subtype = el.kind === "captions" ? TextTrackType.CAPTIONS : TextTrackType.SUBTITLES; + track.name = el.label; + track.language = el.srclang; + return track; + }); + mediaInfo.streamType = this.#googleCast.streamType === "live" ? chrome.cast.media.StreamType.LIVE : chrome.cast.media.StreamType.BUFFERED; + mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata(); + const castMeta = mediaInfo.customData ?? {}; + if (typeof castMeta.title === "string") mediaInfo.metadata.title = castMeta.title; + if (typeof castMeta.subtitle === "string") mediaInfo.metadata.subtitle = castMeta.subtitle; + const castImage = typeof castMeta.poster === "string" ? castMeta.poster : target?.poster; + if (castImage) mediaInfo.metadata.images = [new chrome.cast.Image(castImage)]; + if (await isHls(this.#googleCast.src)) { + mediaInfo.contentType ||= "application/x-mpegURL"; + const fmt = await getPlaylistSegmentFormat(this.#googleCast.src) ?? ""; + const { HlsSegmentFormat: HS, HlsVideoSegmentFormat: HVS } = chrome.cast.media; + if (fmt.includes("m4s") || fmt.includes("mp4")) { + mediaInfo.hlsSegmentFormat = HS.FMP4; + mediaInfo.hlsVideoSegmentFormat = HVS.FMP4; + } else if (fmt.includes("ts")) { + mediaInfo.hlsSegmentFormat = HS.TS; + mediaInfo.hlsVideoSegmentFormat = HVS.TS; + } + } + const request = new chrome.cast.media.LoadRequest(mediaInfo); + request.currentTime = this.target?.currentTime ?? 0; + request.autoplay = !this.#localPaused; + request.activeTrackIds = activeTrackIds; + await currentSession()?.loadMedia(request); + this.target?.dispatchEvent(new Event("volumechange")); + } + get paused() { + if (!this.#remotePlayer.isMediaLoaded) return this.target?.paused ?? true; + return this.#remotePlayer.isPaused || this.ended; + } + get ended() { + return this.#remotePlayer.playerState === chrome.cast.media.PlayerState.IDLE && currentMedia()?.idleReason === chrome.cast.media.IdleReason.FINISHED; + } + get seeking() { + return this.#seeking; + } + get readyState() { + switch (this.#remotePlayer.playerState) { + case chrome.cast.media.PlayerState.IDLE: return 0; + case chrome.cast.media.PlayerState.BUFFERING: return 2; + default: return 3; + } + } + get duration() { + if (!this.#remotePlayer.isMediaLoaded) return this.target?.duration ?? NaN; + return this.#remotePlayer.duration ?? NaN; + } + get currentTime() { + if (!this.#remotePlayer.isMediaLoaded) return this.target?.currentTime ?? 0; + return this.#remotePlayer.currentTime ?? 0; + } + set currentTime(value) { + this.#remotePlayer.currentTime = value; + this.#notifySeeking(); + this.#remotePlayer.controller?.seek(); + } + get muted() { + return this.#remotePlayer.isMuted; + } + set muted(value) { + if (value !== this.#remotePlayer.isMuted) this.#remotePlayer.controller?.muteOrUnmute(); + } + get volume() { + return this.#remotePlayer.volumeLevel ?? 1; + } + set volume(value) { + this.#remotePlayer.volumeLevel = +value; + this.#remotePlayer.controller?.setVolumeLevel(); + } + get playbackRate() { + return currentMedia()?.playbackRate ?? 1; + } + set playbackRate(value) { + setPlaybackRate(value); + } + async play() { + if (!this.#remotePlayer.isMediaLoaded) { + this.#localPaused = false; + await this.load(); + return; + } + if (this.paused) { + this.#remotePlayer.controller?.playOrPause(); + return new Promise((resolve) => { + this.target?.addEventListener("play", () => resolve(), { once: true }); + }); + } + } + pause() { + if (!this.paused) this.#remotePlayer.controller?.playOrPause(); + } + onCastFrameworkAvailable() { + if (!castFramework || this.#isInit) return; + this.#isInit = true; + this.#applyCastOptions(); + this.onCastStateChanged(); + this.#remotePlayer = new castFramework.RemotePlayer(); + new castFramework.RemotePlayerController(this.#remotePlayer); + this.#remoteListeners = { + [castFramework.RemotePlayerEventType.IS_CONNECTED_CHANGED]: (event) => { + if (event?.value === true) this.#hooks.setState?.("connected"); + else { + this.#disconnect(); + this.#hooks.setState?.("disconnected"); + } + }, + [castFramework.RemotePlayerEventType.DURATION_CHANGED]: () => { + this.target?.dispatchEvent(new Event("durationchange")); + }, + [castFramework.RemotePlayerEventType.VOLUME_LEVEL_CHANGED]: () => { + this.target?.dispatchEvent(new Event("volumechange")); + }, + [castFramework.RemotePlayerEventType.IS_MUTED_CHANGED]: () => { + this.target?.dispatchEvent(new Event("volumechange")); + }, + [castFramework.RemotePlayerEventType.CURRENT_TIME_CHANGED]: () => { + if (!this.#isCasting || !this.#remotePlayer.isMediaLoaded) return; + this.#notifySeeked(); + this.target?.dispatchEvent(new Event("timeupdate")); + }, + [castFramework.RemotePlayerEventType.VIDEO_INFO_CHANGED]: () => { + this.target?.dispatchEvent(new Event("resize")); + }, + [castFramework.RemotePlayerEventType.IS_PAUSED_CHANGED]: () => { + this.target?.dispatchEvent(new Event(this.#isCasting && this.#remotePlayer.isPaused ? "pause" : "play")); + }, + [castFramework.RemotePlayerEventType.PLAYER_STATE_CHANGED]: () => { + const PS = chrome.cast.media.PlayerState; + const state = this.#isCasting ? this.#remotePlayer.playerState : void 0; + if (state !== PS.BUFFERING) this.#notifySeeked(); + if (state === PS.PAUSED) return; + if (state === PS.IDLE) { + const finished = currentMedia()?.idleReason === chrome.cast.media.IdleReason.FINISHED; + this.target?.dispatchEvent(new Event(finished ? "ended" : "emptied")); + return; + } + if (state === PS.PLAYING) this.target?.dispatchEvent(new Event("playing")); + else if (state === PS.BUFFERING) this.target?.dispatchEvent(new Event("waiting")); + }, + [castFramework.RemotePlayerEventType.IS_MEDIA_LOADED_CHANGED]: async () => { + if (!this.#isCasting || !this.#remotePlayer.isMediaLoaded) return; + await Promise.resolve(); + this.#onRemoteMediaLoaded(); + } + }; + } + onCastStateChanged() { + if (!this.#isInit) return; + const CS = cast.framework.CastState; + const state = getCastContext().getCastState(); + if (this.#isCasting && state === CS.CONNECTING) this.#hooks.setState?.("connecting"); + this.#hooks.setAvailable?.(!!state && state !== CS.NO_DEVICES_AVAILABLE); + } + async onSessionStateChanged() { + if (!this.#isInit) return; + const { SESSION_RESUMED } = castFramework.SessionState; + if (getCastContext().getSessionState() === SESSION_RESUMED) { + if (this.#googleCast.src === currentMedia()?.media?.contentId) { + this.#isCasting = true; + this.#attachRemoteListeners(); + try { + await getMediaStatus(new chrome.cast.media.GetStatusRequest()); + } catch (error) { + console.error(error); + } + this.#remoteListeners[castFramework.RemotePlayerEventType.IS_PAUSED_CHANGED](); + this.#remoteListeners[castFramework.RemotePlayerEventType.PLAYER_STATE_CHANGED](); + this.target?.dispatchEvent(new Event("ratechange")); + } + } + } + #isCastApiAvailable() { + return Boolean(globalThis.chrome?.cast?.isAvailable); + } + #applyCastOptions() { + const { receiver } = this.#googleCast; + setCastOptions(receiver ? { receiverApplicationId: receiver } : {}); + } + #attachRemoteListeners() { + if (this.#listenersAttached) return; + const controller = this.#remotePlayer?.controller; + if (!controller) return; + for (const [type, handler] of Object.entries(this.#remoteListeners)) controller.addEventListener(type, handler); + this.#listenersAttached = true; + } + #detachRemoteListeners() { + if (!this.#listenersAttached) return; + const controller = this.#remotePlayer?.controller; + if (controller) for (const [type, handler] of Object.entries(this.#remoteListeners)) controller.removeEventListener(type, handler); + this.#listenersAttached = false; + } + #disconnect() { + if (!this.#isCasting) return; + currentMedia()?.removeUpdateListener(this.#onMediaUpdate); + this.#detachRemoteListeners(); + this.#seeking = false; + this.#playbackRate = 1; + this.#isCasting = false; + if (this.target) this.target.muted = this.#remotePlayer.isMuted; + const saved = this.#remotePlayer.savedPlayerState; + if (saved) { + if (this.target) this.target.currentTime = saved.currentTime; + if (saved.isPaused === false && this.target) this.target.play(); + } + } + #notifySeeking() { + this.#seeking = true; + this.target?.dispatchEvent(new Event("seeking")); + } + #notifySeeked() { + if (!this.#seeking) return; + this.#seeking = false; + this.target?.dispatchEvent(new Event("seeked")); + } + #onRemoteMediaLoaded() { + this.#playbackRate = currentMedia()?.playbackRate ?? 1; + currentMedia()?.addUpdateListener(this.#onMediaUpdate); + this.#updateRemoteTextTrack(); + } + #checkPlaybackRate() { + const rate = currentMedia()?.playbackRate ?? 1; + if (rate !== this.#playbackRate) { + this.#playbackRate = rate; + this.target?.dispatchEvent(new Event("ratechange")); + } + } + async #updateRemoteTextTrack() { + if (!this.#isCasting || !this.target) return; + const localSubs = [...this.target.textTracks].filter(isCaptionOrSubtitleTrack); + const matched = (this.#remotePlayer.mediaInfo?.tracks ?? []).filter(({ type }) => type === chrome.cast.media.TrackType.TEXT).flatMap(({ language, name, trackId }) => { + const local = localSubs.find((l) => l.language === language && l.label === name); + return local?.mode ? [{ + mode: local.mode, + trackId + }] : []; + }); + const hidden = new Set(matched.filter((m) => m.mode !== "showing").map((m) => m.trackId)); + const showing = matched.find((m) => m.mode === "showing")?.trackId; + const active = currentSession()?.getSessionObj().media[0]?.activeTrackIds ?? []; + const next = new Set(active.filter((id) => !hidden.has(id))); + if (showing) next.add(showing); + if (next.size === active.length && active.every((id) => next.has(id))) return; + try { + await editTracksInfo(new chrome.cast.media.EditTracksInfoRequest([...next])); + } catch (error) { + console.error(error); + } + } +}; +//#endregion +export { GoogleCastProvider }; + +//# sourceMappingURL=google-cast-provider.js.map \ No newline at end of file diff --git a/dist/default/dom/google-cast/google-cast-provider.js.map b/dist/default/dom/google-cast/google-cast-provider.js.map new file mode 100644 index 00000000..f3cd90b9 --- /dev/null +++ b/dist/default/dom/google-cast/google-cast-provider.js.map @@ -0,0 +1 @@ +{"version":3,"file":"google-cast-provider.js","names":["#updateRemoteTextTrack","#checkPlaybackRate","#googleCast","#remotePlayback","#onTextTrackChange","#onMediaUpdate","#detachRemoteListeners","#isCasting","#isInit","#hooks","#isCastApiAvailable","#applyCastOptions","#attachRemoteListeners","#localPaused","#remotePlayer","#seeking","#notifySeeking","#remoteListeners","#disconnect","#notifySeeked","#onRemoteMediaLoaded","#listenersAttached","#playbackRate"],"sources":["../../../../src/dom/google-cast/google-cast-provider.ts"],"sourcesContent":["import { isCaptionOrSubtitleTrack } from '@videojs/utils/dom';\nimport type { HTMLMediaTargetLike } from '../media-host';\nimport type { GoogleCastProps } from './index';\nimport { castFramework, ensureCastFramework, googleCastInstances } from './registry';\nimport { RemotePlayback, type RemotePlaybackHooks } from './remote-playback';\nimport {\n currentMedia,\n currentSession,\n editTracksInfo,\n getCastContext,\n getMediaStatus,\n getPlaylistSegmentFormat,\n InvalidStateError,\n isHls,\n NotSupportedError,\n setCastOptions,\n setPlaybackRate,\n} from './utils';\n\ntype RemotePlayerListener = (event?: cast.framework.RemotePlayerChangedEvent) => void;\n\ntype GoogleCastConfig = GoogleCastProps;\n\n/**\n * Cast provider + lifecycle. Created by the {@link GoogleCast} component and\n * installed as the host's `targetOverride` while a cast session is connected,\n * so its getters/setters route through the cast receiver; when disconnected the\n * host falls through to the attached target. Also owns the cast framework\n * integration, the `RemotePlayback` instance exposed via\n * {@link GoogleCastProvider#remote}, and dispatches media events on the attached\n * target (forwarded by the host) while casting.\n */\nexport class GoogleCastProvider {\n target: HTMLMediaTargetLike | null = null;\n #googleCast: GoogleCastConfig;\n #hooks: Partial = {};\n #remotePlayback: RemotePlayback;\n #isInit = false;\n #isCasting = false;\n #seeking = false;\n #remotePlayer!: cast.framework.RemotePlayer;\n #remoteListeners!: Record;\n #listenersAttached = false;\n #playbackRate = 1;\n #localPaused = false;\n #onTextTrackChange = () => this.#updateRemoteTextTrack();\n #onMediaUpdate = () => this.#checkPlaybackRate();\n\n constructor(googleCast: GoogleCastConfig) {\n this.#googleCast = googleCast;\n this.#remotePlayback = new RemotePlayback(this);\n googleCastInstances.add(this);\n this.onCastFrameworkAvailable();\n }\n\n get remote() {\n if (this.target && !this.target.disableRemotePlayback) {\n ensureCastFramework();\n }\n return this.#remotePlayback;\n }\n\n attach(target: HTMLMediaTargetLike) {\n this.target = target;\n target.textTracks.addEventListener('change', this.#onTextTrackChange);\n }\n\n detach() {\n this.target?.textTracks.removeEventListener('change', this.#onTextTrackChange);\n this.target = null;\n }\n\n destroy() {\n this.detach();\n googleCastInstances.delete(this);\n currentMedia()?.removeUpdateListener(this.#onMediaUpdate);\n this.#detachRemoteListeners();\n this.#isCasting = false;\n this.#isInit = false;\n }\n\n /** @internal Wires up callbacks pushed from {@link RemotePlayback}; not part of the public surface. */\n bindHooks(hooks: Partial) {\n Object.assign(this.#hooks, hooks);\n }\n\n hasDevicesAvailable() {\n const state = getCastContext()?.getCastState();\n return !!state && state !== cast.framework.CastState.NO_DEVICES_AVAILABLE;\n }\n\n async requestCastSession() {\n if (this.target?.disableRemotePlayback) {\n throw new InvalidStateError('disableRemotePlayback attribute is present.');\n }\n\n await ensureCastFramework();\n\n if (!this.#isCastApiAvailable()) {\n throw new NotSupportedError('The RemotePlayback API is disabled on this platform.');\n }\n\n const willDisconnect = this.#isCasting;\n this.#isCasting = true;\n\n this.#applyCastOptions();\n this.#attachRemoteListeners();\n\n try {\n await getCastContext()!.requestSession();\n } catch (err) {\n if (!willDisconnect) {\n this.#isCasting = false;\n }\n\n if (err === 'cancel') {\n return;\n }\n\n throw new Error(err as string);\n }\n\n this.#localPaused = this.target?.paused ?? true;\n this.target?.pause();\n this.muted = this.target?.muted ?? false;\n\n try {\n await this.load();\n } catch (err) {\n console.error(err);\n }\n }\n\n async load() {\n if (!this.#isCasting) {\n await this.target?.load();\n return;\n }\n\n if (!this.#googleCast.src) {\n // TODO: handle unloading the media?\n return;\n }\n\n const mediaInfo = new chrome.cast.media.MediaInfo(this.#googleCast.src, this.#googleCast.contentType ?? '');\n mediaInfo.customData = this.#googleCast.customData ?? null;\n\n const { target } = this;\n const subtitles = [...(target?.querySelectorAll('track') ?? [])].filter(\n (el) => el.src && isCaptionOrSubtitleTrack(el)\n );\n\n const { Track, TrackType, TextTrackType } = chrome.cast.media;\n const activeTrackIds: number[] = [];\n\n if (subtitles.length) {\n mediaInfo.tracks = subtitles.map((el, i) => {\n const trackId = i + 1;\n if (!activeTrackIds.length && el.track.mode === 'showing') activeTrackIds.push(trackId);\n\n const track = new Track(trackId, TrackType.TEXT);\n track.trackContentId = el.src;\n track.trackContentType = 'text/vtt';\n track.subtype = el.kind === 'captions' ? TextTrackType.CAPTIONS : TextTrackType.SUBTITLES;\n track.name = el.label;\n track.language = el.srclang;\n return track;\n });\n }\n\n mediaInfo.streamType =\n this.#googleCast.streamType === 'live'\n ? chrome.cast.media.StreamType.LIVE\n : chrome.cast.media.StreamType.BUFFERED;\n\n mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata();\n const castMeta = (mediaInfo.customData ?? {}) as {\n title?: unknown;\n subtitle?: unknown;\n poster?: unknown;\n };\n if (typeof castMeta.title === 'string') mediaInfo.metadata.title = castMeta.title;\n if (typeof castMeta.subtitle === 'string') mediaInfo.metadata.subtitle = castMeta.subtitle;\n const castImage =\n typeof castMeta.poster === 'string' ? castMeta.poster : (target as HTMLVideoElement | null)?.poster;\n if (castImage) mediaInfo.metadata.images = [new chrome.cast.Image(castImage)];\n\n if (await isHls(this.#googleCast.src)) {\n mediaInfo.contentType ||= 'application/x-mpegURL';\n\n const fmt = (await getPlaylistSegmentFormat(this.#googleCast.src)) ?? '';\n const { HlsSegmentFormat: HS, HlsVideoSegmentFormat: HVS } = chrome.cast.media;\n\n if (fmt.includes('m4s') || fmt.includes('mp4')) {\n mediaInfo.hlsSegmentFormat = HS.FMP4;\n mediaInfo.hlsVideoSegmentFormat = HVS.FMP4;\n } else if (fmt.includes('ts')) {\n mediaInfo.hlsSegmentFormat = HS.TS;\n mediaInfo.hlsVideoSegmentFormat = HVS.TS;\n }\n }\n\n const request = new chrome.cast.media.LoadRequest(mediaInfo);\n // Use `super.currentTime` to read the local element's time even though our\n // own `currentTime` getter is overridden to return the remote player's.\n request.currentTime = this.target?.currentTime ?? 0;\n request.autoplay = !this.#localPaused;\n request.activeTrackIds = activeTrackIds;\n\n await currentSession()?.loadMedia(request);\n\n this.target?.dispatchEvent(new Event('volumechange'));\n }\n\n get paused() {\n if (!this.#remotePlayer.isMediaLoaded) return this.target?.paused ?? true;\n return this.#remotePlayer.isPaused || this.ended;\n }\n\n get ended() {\n return (\n this.#remotePlayer.playerState === chrome.cast.media.PlayerState.IDLE &&\n currentMedia()?.idleReason === chrome.cast.media.IdleReason.FINISHED\n );\n }\n\n get seeking() {\n return this.#seeking;\n }\n\n get readyState() {\n switch (this.#remotePlayer.playerState) {\n case chrome.cast.media.PlayerState.IDLE:\n return 0;\n case chrome.cast.media.PlayerState.BUFFERING:\n return 2;\n default:\n return 3;\n }\n }\n\n get duration() {\n if (!this.#remotePlayer.isMediaLoaded) return this.target?.duration ?? NaN;\n return this.#remotePlayer.duration ?? NaN;\n }\n\n get currentTime() {\n if (!this.#remotePlayer.isMediaLoaded) return this.target?.currentTime ?? 0;\n return this.#remotePlayer.currentTime ?? 0;\n }\n\n set currentTime(value: number) {\n this.#remotePlayer.currentTime = value;\n this.#notifySeeking();\n this.#remotePlayer.controller?.seek();\n }\n\n get muted() {\n return this.#remotePlayer.isMuted;\n }\n\n set muted(value: boolean) {\n if (value !== this.#remotePlayer.isMuted) {\n this.#remotePlayer.controller?.muteOrUnmute();\n }\n }\n\n get volume() {\n return this.#remotePlayer.volumeLevel ?? 1;\n }\n\n set volume(value: number) {\n this.#remotePlayer.volumeLevel = +value;\n this.#remotePlayer.controller?.setVolumeLevel();\n }\n\n get playbackRate() {\n return currentMedia()?.playbackRate ?? 1;\n }\n\n set playbackRate(value: number) {\n setPlaybackRate(value);\n }\n\n async play() {\n // When a casted media ends, the media is unloaded and the player state is IDLE.\n if (!this.#remotePlayer.isMediaLoaded) {\n this.#localPaused = false;\n await this.load();\n return;\n }\n if (this.paused) {\n this.#remotePlayer.controller?.playOrPause();\n return new Promise((resolve) => {\n this.target?.addEventListener('play', () => resolve(), { once: true });\n });\n }\n }\n\n pause() {\n if (!this.paused) {\n this.#remotePlayer.controller?.playOrPause();\n }\n }\n\n onCastFrameworkAvailable() {\n if (!castFramework || this.#isInit) return;\n this.#isInit = true;\n\n this.#applyCastOptions();\n\n this.onCastStateChanged();\n\n this.#remotePlayer = new castFramework.RemotePlayer();\n new castFramework.RemotePlayerController(this.#remotePlayer);\n\n this.#remoteListeners = {\n [castFramework.RemotePlayerEventType.IS_CONNECTED_CHANGED]: (event?: cast.framework.RemotePlayerChangedEvent) => {\n const value = event?.value;\n if (value === true) {\n this.#hooks.setState?.('connected');\n } else {\n this.#disconnect();\n this.#hooks.setState?.('disconnected');\n }\n },\n [castFramework.RemotePlayerEventType.DURATION_CHANGED]: () => {\n this.target?.dispatchEvent(new Event('durationchange'));\n },\n [castFramework.RemotePlayerEventType.VOLUME_LEVEL_CHANGED]: () => {\n this.target?.dispatchEvent(new Event('volumechange'));\n },\n [castFramework.RemotePlayerEventType.IS_MUTED_CHANGED]: () => {\n this.target?.dispatchEvent(new Event('volumechange'));\n },\n [castFramework.RemotePlayerEventType.CURRENT_TIME_CHANGED]: () => {\n if (!this.#isCasting || !this.#remotePlayer.isMediaLoaded) return;\n this.#notifySeeked();\n this.target?.dispatchEvent(new Event('timeupdate'));\n },\n [castFramework.RemotePlayerEventType.VIDEO_INFO_CHANGED]: () => {\n this.target?.dispatchEvent(new Event('resize'));\n },\n [castFramework.RemotePlayerEventType.IS_PAUSED_CHANGED]: () => {\n this.target?.dispatchEvent(new Event(this.#isCasting && this.#remotePlayer.isPaused ? 'pause' : 'play'));\n },\n [castFramework.RemotePlayerEventType.PLAYER_STATE_CHANGED]: () => {\n const PS = chrome.cast.media.PlayerState;\n const state = this.#isCasting ? this.#remotePlayer.playerState : undefined;\n\n if (state !== PS.BUFFERING) this.#notifySeeked();\n if (state === PS.PAUSED) return;\n\n if (state === PS.IDLE) {\n const finished = currentMedia()?.idleReason === chrome.cast.media.IdleReason.FINISHED;\n this.target?.dispatchEvent(new Event(finished ? 'ended' : 'emptied'));\n return;\n }\n\n if (state === PS.PLAYING) this.target?.dispatchEvent(new Event('playing'));\n else if (state === PS.BUFFERING) this.target?.dispatchEvent(new Event('waiting'));\n },\n [castFramework.RemotePlayerEventType.IS_MEDIA_LOADED_CHANGED]: async () => {\n if (!this.#isCasting || !this.#remotePlayer.isMediaLoaded) return;\n\n await Promise.resolve();\n this.#onRemoteMediaLoaded();\n },\n };\n }\n\n onCastStateChanged() {\n if (!this.#isInit) return;\n const CS = cast.framework.CastState;\n const state = getCastContext()!.getCastState();\n\n if (this.#isCasting && state === CS.CONNECTING) {\n this.#hooks.setState?.('connecting');\n }\n\n this.#hooks.setAvailable?.(!!state && state !== CS.NO_DEVICES_AVAILABLE);\n }\n\n async onSessionStateChanged() {\n if (!this.#isInit) return;\n const { SESSION_RESUMED } = castFramework!.SessionState;\n if (getCastContext()!.getSessionState() === SESSION_RESUMED) {\n if (this.#googleCast.src === currentMedia()?.media?.contentId) {\n this.#isCasting = true;\n\n this.#attachRemoteListeners();\n\n try {\n await getMediaStatus(new chrome.cast.media.GetStatusRequest());\n } catch (error) {\n console.error(error);\n }\n\n this.#remoteListeners[castFramework!.RemotePlayerEventType.IS_PAUSED_CHANGED]!();\n this.#remoteListeners[castFramework!.RemotePlayerEventType.PLAYER_STATE_CHANGED]!();\n this.target?.dispatchEvent(new Event('ratechange'));\n\n // TODO: sync remote enabled text track state to local text tracks\n }\n }\n }\n\n #isCastApiAvailable() {\n return Boolean(globalThis.chrome?.cast?.isAvailable);\n }\n\n #applyCastOptions() {\n const { receiver } = this.#googleCast;\n setCastOptions(receiver ? { receiverApplicationId: receiver } : {});\n }\n\n // CAF's RemotePlayerController does not deduplicate handlers, so we guard\n // attach/detach with a flag to prevent duplicate event dispatches across\n // cancel/retry and stop-casting flows.\n #attachRemoteListeners() {\n if (this.#listenersAttached) return;\n const controller = this.#remotePlayer?.controller;\n if (!controller) return;\n\n for (const [type, handler] of Object.entries(this.#remoteListeners)) {\n controller.addEventListener(type as cast.framework.RemotePlayerEventType, handler);\n }\n this.#listenersAttached = true;\n }\n\n #detachRemoteListeners() {\n if (!this.#listenersAttached) return;\n const controller = this.#remotePlayer?.controller;\n\n if (controller) {\n for (const [type, handler] of Object.entries(this.#remoteListeners)) {\n controller.removeEventListener(type as cast.framework.RemotePlayerEventType, handler);\n }\n }\n this.#listenersAttached = false;\n }\n\n #disconnect() {\n if (!this.#isCasting) return;\n\n currentMedia()?.removeUpdateListener(this.#onMediaUpdate);\n this.#detachRemoteListeners();\n this.#seeking = false;\n this.#playbackRate = 1;\n this.#isCasting = false;\n\n if (this.target) {\n this.target.muted = this.#remotePlayer.isMuted;\n }\n\n const saved = this.#remotePlayer.savedPlayerState;\n if (saved) {\n if (this.target) {\n this.target.currentTime = saved.currentTime;\n }\n if (saved.isPaused === false && this.target) {\n this.target.play();\n }\n }\n }\n\n #notifySeeking() {\n this.#seeking = true;\n this.target?.dispatchEvent(new Event('seeking'));\n }\n\n #notifySeeked() {\n if (!this.#seeking) return;\n this.#seeking = false;\n this.target?.dispatchEvent(new Event('seeked'));\n }\n\n #onRemoteMediaLoaded() {\n this.#playbackRate = currentMedia()?.playbackRate ?? 1;\n currentMedia()?.addUpdateListener(this.#onMediaUpdate);\n this.#updateRemoteTextTrack();\n }\n\n #checkPlaybackRate() {\n const rate = currentMedia()?.playbackRate ?? 1;\n if (rate !== this.#playbackRate) {\n this.#playbackRate = rate;\n this.target?.dispatchEvent(new Event('ratechange'));\n }\n }\n\n async #updateRemoteTextTrack() {\n if (!this.#isCasting || !this.target) return;\n\n const localSubs = [...this.target.textTracks].filter(isCaptionOrSubtitleTrack);\n\n const matched = (this.#remotePlayer.mediaInfo?.tracks ?? [])\n .filter(({ type }) => type === chrome.cast.media.TrackType.TEXT)\n .flatMap(({ language, name, trackId }) => {\n const local = localSubs.find((l) => l.language === language && l.label === name);\n return local?.mode ? [{ mode: local.mode, trackId }] : [];\n });\n\n const hidden = new Set(matched.filter((m) => m.mode !== 'showing').map((m) => m.trackId));\n const showing = matched.find((m) => m.mode === 'showing')?.trackId;\n\n const active = currentSession()?.getSessionObj().media[0]?.activeTrackIds ?? [];\n const next = new Set(active.filter((id) => !hidden.has(id)));\n if (showing) next.add(showing);\n\n if (next.size === active.length && active.every((id) => next.has(id))) return;\n\n try {\n await editTracksInfo(new chrome.cast.media.EditTracksInfoRequest([...next]));\n } catch (error) {\n console.error(error);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAgCA,IAAa,qBAAb,MAAgC;CAC9B,SAAqC;CACrC;CACA,SAAuC,EAAE;CACzC;CACA,UAAU;CACV,aAAa;CACb,WAAW;CACX;CACA;CACA,qBAAqB;CACrB,gBAAgB;CAChB,eAAe;CACf,2BAA2B,MAAA,uBAA6B;CACxD,uBAAuB,MAAA,mBAAyB;CAEhD,YAAY,YAA8B;AACxC,QAAA,aAAmB;AACnB,QAAA,iBAAuB,IAAI,eAAe,KAAK;AAC/C,sBAAoB,IAAI,KAAK;AAC7B,OAAK,0BAA0B;;CAGjC,IAAI,SAAS;AACX,MAAI,KAAK,UAAU,CAAC,KAAK,OAAO,sBAC9B,sBAAqB;AAEvB,SAAO,MAAA;;CAGT,OAAO,QAA6B;AAClC,OAAK,SAAS;AACd,SAAO,WAAW,iBAAiB,UAAU,MAAA,kBAAwB;;CAGvE,SAAS;AACP,OAAK,QAAQ,WAAW,oBAAoB,UAAU,MAAA,kBAAwB;AAC9E,OAAK,SAAS;;CAGhB,UAAU;AACR,OAAK,QAAQ;AACb,sBAAoB,OAAO,KAAK;AAChC,gBAAc,EAAE,qBAAqB,MAAA,cAAoB;AACzD,QAAA,uBAA6B;AAC7B,QAAA,YAAkB;AAClB,QAAA,SAAe;;;CAIjB,UAAU,OAAqC;AAC7C,SAAO,OAAO,MAAA,OAAa,MAAM;;CAGnC,sBAAsB;EACpB,MAAM,QAAQ,gBAAgB,EAAE,cAAc;AAC9C,SAAO,CAAC,CAAC,SAAS,UAAU,KAAK,UAAU,UAAU;;CAGvD,MAAM,qBAAqB;AACzB,MAAI,KAAK,QAAQ,sBACf,OAAM,IAAI,kBAAkB,8CAA8C;AAG5E,QAAM,qBAAqB;AAE3B,MAAI,CAAC,MAAA,oBAA0B,CAC7B,OAAM,IAAI,kBAAkB,uDAAuD;EAGrF,MAAM,iBAAiB,MAAA;AACvB,QAAA,YAAkB;AAElB,QAAA,kBAAwB;AACxB,QAAA,uBAA6B;AAE7B,MAAI;AACF,SAAM,gBAAgB,CAAE,gBAAgB;WACjC,KAAK;AACZ,OAAI,CAAC,eACH,OAAA,YAAkB;AAGpB,OAAI,QAAQ,SACV;AAGF,SAAM,IAAI,MAAM,IAAc;;AAGhC,QAAA,cAAoB,KAAK,QAAQ,UAAU;AAC3C,OAAK,QAAQ,OAAO;AACpB,OAAK,QAAQ,KAAK,QAAQ,SAAS;AAEnC,MAAI;AACF,SAAM,KAAK,MAAM;WACV,KAAK;AACZ,WAAQ,MAAM,IAAI;;;CAItB,MAAM,OAAO;AACX,MAAI,CAAC,MAAA,WAAiB;AACpB,SAAM,KAAK,QAAQ,MAAM;AACzB;;AAGF,MAAI,CAAC,MAAA,WAAiB,IAEpB;EAGF,MAAM,YAAY,IAAI,OAAO,KAAK,MAAM,UAAU,MAAA,WAAiB,KAAK,MAAA,WAAiB,eAAe,GAAG;AAC3G,YAAU,aAAa,MAAA,WAAiB,cAAc;EAEtD,MAAM,EAAE,WAAW;EACnB,MAAM,YAAY,CAAC,GAAI,QAAQ,iBAAmC,QAAQ,IAAI,EAAE,CAAE,CAAC,QAChF,OAAO,GAAG,OAAO,yBAAyB,GAAG,CAC/C;EAED,MAAM,EAAE,OAAO,WAAW,kBAAkB,OAAO,KAAK;EACxD,MAAM,iBAA2B,EAAE;AAEnC,MAAI,UAAU,OACZ,WAAU,SAAS,UAAU,KAAK,IAAI,MAAM;GAC1C,MAAM,UAAU,IAAI;AACpB,OAAI,CAAC,eAAe,UAAU,GAAG,MAAM,SAAS,UAAW,gBAAe,KAAK,QAAQ;GAEvF,MAAM,QAAQ,IAAI,MAAM,SAAS,UAAU,KAAK;AAChD,SAAM,iBAAiB,GAAG;AAC1B,SAAM,mBAAmB;AACzB,SAAM,UAAU,GAAG,SAAS,aAAa,cAAc,WAAW,cAAc;AAChF,SAAM,OAAO,GAAG;AAChB,SAAM,WAAW,GAAG;AACpB,UAAO;IACP;AAGJ,YAAU,aACR,MAAA,WAAiB,eAAe,SAC5B,OAAO,KAAK,MAAM,WAAW,OAC7B,OAAO,KAAK,MAAM,WAAW;AAEnC,YAAU,WAAW,IAAI,OAAO,KAAK,MAAM,sBAAsB;EACjE,MAAM,WAAY,UAAU,cAAc,EAAE;AAK5C,MAAI,OAAO,SAAS,UAAU,SAAU,WAAU,SAAS,QAAQ,SAAS;AAC5E,MAAI,OAAO,SAAS,aAAa,SAAU,WAAU,SAAS,WAAW,SAAS;EAClF,MAAM,YACJ,OAAO,SAAS,WAAW,WAAW,SAAS,SAAU,QAAoC;AAC/F,MAAI,UAAW,WAAU,SAAS,SAAS,CAAC,IAAI,OAAO,KAAK,MAAM,UAAU,CAAC;AAE7E,MAAI,MAAM,MAAM,MAAA,WAAiB,IAAI,EAAE;AACrC,aAAU,gBAAgB;GAE1B,MAAM,MAAO,MAAM,yBAAyB,MAAA,WAAiB,IAAI,IAAK;GACtE,MAAM,EAAE,kBAAkB,IAAI,uBAAuB,QAAQ,OAAO,KAAK;AAEzE,OAAI,IAAI,SAAS,MAAM,IAAI,IAAI,SAAS,MAAM,EAAE;AAC9C,cAAU,mBAAmB,GAAG;AAChC,cAAU,wBAAwB,IAAI;cAC7B,IAAI,SAAS,KAAK,EAAE;AAC7B,cAAU,mBAAmB,GAAG;AAChC,cAAU,wBAAwB,IAAI;;;EAI1C,MAAM,UAAU,IAAI,OAAO,KAAK,MAAM,YAAY,UAAU;AAG5D,UAAQ,cAAc,KAAK,QAAQ,eAAe;AAClD,UAAQ,WAAW,CAAC,MAAA;AACpB,UAAQ,iBAAiB;AAEzB,QAAM,gBAAgB,EAAE,UAAU,QAAQ;AAE1C,OAAK,QAAQ,cAAc,IAAI,MAAM,eAAe,CAAC;;CAGvD,IAAI,SAAS;AACX,MAAI,CAAC,MAAA,aAAmB,cAAe,QAAO,KAAK,QAAQ,UAAU;AACrE,SAAO,MAAA,aAAmB,YAAY,KAAK;;CAG7C,IAAI,QAAQ;AACV,SACE,MAAA,aAAmB,gBAAgB,OAAO,KAAK,MAAM,YAAY,QACjE,cAAc,EAAE,eAAe,OAAO,KAAK,MAAM,WAAW;;CAIhE,IAAI,UAAU;AACZ,SAAO,MAAA;;CAGT,IAAI,aAAa;AACf,UAAQ,MAAA,aAAmB,aAA3B;GACE,KAAK,OAAO,KAAK,MAAM,YAAY,KACjC,QAAO;GACT,KAAK,OAAO,KAAK,MAAM,YAAY,UACjC,QAAO;GACT,QACE,QAAO;;;CAIb,IAAI,WAAW;AACb,MAAI,CAAC,MAAA,aAAmB,cAAe,QAAO,KAAK,QAAQ,YAAY;AACvE,SAAO,MAAA,aAAmB,YAAY;;CAGxC,IAAI,cAAc;AAChB,MAAI,CAAC,MAAA,aAAmB,cAAe,QAAO,KAAK,QAAQ,eAAe;AAC1E,SAAO,MAAA,aAAmB,eAAe;;CAG3C,IAAI,YAAY,OAAe;AAC7B,QAAA,aAAmB,cAAc;AACjC,QAAA,eAAqB;AACrB,QAAA,aAAmB,YAAY,MAAM;;CAGvC,IAAI,QAAQ;AACV,SAAO,MAAA,aAAmB;;CAG5B,IAAI,MAAM,OAAgB;AACxB,MAAI,UAAU,MAAA,aAAmB,QAC/B,OAAA,aAAmB,YAAY,cAAc;;CAIjD,IAAI,SAAS;AACX,SAAO,MAAA,aAAmB,eAAe;;CAG3C,IAAI,OAAO,OAAe;AACxB,QAAA,aAAmB,cAAc,CAAC;AAClC,QAAA,aAAmB,YAAY,gBAAgB;;CAGjD,IAAI,eAAe;AACjB,SAAO,cAAc,EAAE,gBAAgB;;CAGzC,IAAI,aAAa,OAAe;AAC9B,kBAAgB,MAAM;;CAGxB,MAAM,OAAO;AAEX,MAAI,CAAC,MAAA,aAAmB,eAAe;AACrC,SAAA,cAAoB;AACpB,SAAM,KAAK,MAAM;AACjB;;AAEF,MAAI,KAAK,QAAQ;AACf,SAAA,aAAmB,YAAY,aAAa;AAC5C,UAAO,IAAI,SAAe,YAAY;AACpC,SAAK,QAAQ,iBAAiB,cAAc,SAAS,EAAE,EAAE,MAAM,MAAM,CAAC;KACtE;;;CAIN,QAAQ;AACN,MAAI,CAAC,KAAK,OACR,OAAA,aAAmB,YAAY,aAAa;;CAIhD,2BAA2B;AACzB,MAAI,CAAC,iBAAiB,MAAA,OAAc;AACpC,QAAA,SAAe;AAEf,QAAA,kBAAwB;AAExB,OAAK,oBAAoB;AAEzB,QAAA,eAAqB,IAAI,cAAc,cAAc;AACrD,MAAI,cAAc,uBAAuB,MAAA,aAAmB;AAE5D,QAAA,kBAAwB;IACrB,cAAc,sBAAsB,wBAAwB,UAAoD;AAE/G,QADc,OAAO,UACP,KACZ,OAAA,MAAY,WAAW,YAAY;SAC9B;AACL,WAAA,YAAkB;AAClB,WAAA,MAAY,WAAW,eAAe;;;IAGzC,cAAc,sBAAsB,yBAAyB;AAC5D,SAAK,QAAQ,cAAc,IAAI,MAAM,iBAAiB,CAAC;;IAExD,cAAc,sBAAsB,6BAA6B;AAChE,SAAK,QAAQ,cAAc,IAAI,MAAM,eAAe,CAAC;;IAEtD,cAAc,sBAAsB,yBAAyB;AAC5D,SAAK,QAAQ,cAAc,IAAI,MAAM,eAAe,CAAC;;IAEtD,cAAc,sBAAsB,6BAA6B;AAChE,QAAI,CAAC,MAAA,aAAmB,CAAC,MAAA,aAAmB,cAAe;AAC3D,UAAA,cAAoB;AACpB,SAAK,QAAQ,cAAc,IAAI,MAAM,aAAa,CAAC;;IAEpD,cAAc,sBAAsB,2BAA2B;AAC9D,SAAK,QAAQ,cAAc,IAAI,MAAM,SAAS,CAAC;;IAEhD,cAAc,sBAAsB,0BAA0B;AAC7D,SAAK,QAAQ,cAAc,IAAI,MAAM,MAAA,aAAmB,MAAA,aAAmB,WAAW,UAAU,OAAO,CAAC;;IAEzG,cAAc,sBAAsB,6BAA6B;IAChE,MAAM,KAAK,OAAO,KAAK,MAAM;IAC7B,MAAM,QAAQ,MAAA,YAAkB,MAAA,aAAmB,cAAc,KAAA;AAEjE,QAAI,UAAU,GAAG,UAAW,OAAA,cAAoB;AAChD,QAAI,UAAU,GAAG,OAAQ;AAEzB,QAAI,UAAU,GAAG,MAAM;KACrB,MAAM,WAAW,cAAc,EAAE,eAAe,OAAO,KAAK,MAAM,WAAW;AAC7E,UAAK,QAAQ,cAAc,IAAI,MAAM,WAAW,UAAU,UAAU,CAAC;AACrE;;AAGF,QAAI,UAAU,GAAG,QAAS,MAAK,QAAQ,cAAc,IAAI,MAAM,UAAU,CAAC;aACjE,UAAU,GAAG,UAAW,MAAK,QAAQ,cAAc,IAAI,MAAM,UAAU,CAAC;;IAElF,cAAc,sBAAsB,0BAA0B,YAAY;AACzE,QAAI,CAAC,MAAA,aAAmB,CAAC,MAAA,aAAmB,cAAe;AAE3D,UAAM,QAAQ,SAAS;AACvB,UAAA,qBAA2B;;GAE9B;;CAGH,qBAAqB;AACnB,MAAI,CAAC,MAAA,OAAc;EACnB,MAAM,KAAK,KAAK,UAAU;EAC1B,MAAM,QAAQ,gBAAgB,CAAE,cAAc;AAE9C,MAAI,MAAA,aAAmB,UAAU,GAAG,WAClC,OAAA,MAAY,WAAW,aAAa;AAGtC,QAAA,MAAY,eAAe,CAAC,CAAC,SAAS,UAAU,GAAG,qBAAqB;;CAG1E,MAAM,wBAAwB;AAC5B,MAAI,CAAC,MAAA,OAAc;EACnB,MAAM,EAAE,oBAAoB,cAAe;AAC3C,MAAI,gBAAgB,CAAE,iBAAiB,KAAK;OACtC,MAAA,WAAiB,QAAQ,cAAc,EAAE,OAAO,WAAW;AAC7D,UAAA,YAAkB;AAElB,UAAA,uBAA6B;AAE7B,QAAI;AACF,WAAM,eAAe,IAAI,OAAO,KAAK,MAAM,kBAAkB,CAAC;aACvD,OAAO;AACd,aAAQ,MAAM,MAAM;;AAGtB,UAAA,gBAAsB,cAAe,sBAAsB,oBAAqB;AAChF,UAAA,gBAAsB,cAAe,sBAAsB,uBAAwB;AACnF,SAAK,QAAQ,cAAc,IAAI,MAAM,aAAa,CAAC;;;;CAOzD,sBAAsB;AACpB,SAAO,QAAQ,WAAW,QAAQ,MAAM,YAAY;;CAGtD,oBAAoB;EAClB,MAAM,EAAE,aAAa,MAAA;AACrB,iBAAe,WAAW,EAAE,uBAAuB,UAAU,GAAG,EAAE,CAAC;;CAMrE,yBAAyB;AACvB,MAAI,MAAA,kBAAyB;EAC7B,MAAM,aAAa,MAAA,cAAoB;AACvC,MAAI,CAAC,WAAY;AAEjB,OAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,MAAA,gBAAsB,CACjE,YAAW,iBAAiB,MAA8C,QAAQ;AAEpF,QAAA,oBAA0B;;CAG5B,yBAAyB;AACvB,MAAI,CAAC,MAAA,kBAAyB;EAC9B,MAAM,aAAa,MAAA,cAAoB;AAEvC,MAAI,WACF,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,MAAA,gBAAsB,CACjE,YAAW,oBAAoB,MAA8C,QAAQ;AAGzF,QAAA,oBAA0B;;CAG5B,cAAc;AACZ,MAAI,CAAC,MAAA,UAAiB;AAEtB,gBAAc,EAAE,qBAAqB,MAAA,cAAoB;AACzD,QAAA,uBAA6B;AAC7B,QAAA,UAAgB;AAChB,QAAA,eAAqB;AACrB,QAAA,YAAkB;AAElB,MAAI,KAAK,OACP,MAAK,OAAO,QAAQ,MAAA,aAAmB;EAGzC,MAAM,QAAQ,MAAA,aAAmB;AACjC,MAAI,OAAO;AACT,OAAI,KAAK,OACP,MAAK,OAAO,cAAc,MAAM;AAElC,OAAI,MAAM,aAAa,SAAS,KAAK,OACnC,MAAK,OAAO,MAAM;;;CAKxB,iBAAiB;AACf,QAAA,UAAgB;AAChB,OAAK,QAAQ,cAAc,IAAI,MAAM,UAAU,CAAC;;CAGlD,gBAAgB;AACd,MAAI,CAAC,MAAA,QAAe;AACpB,QAAA,UAAgB;AAChB,OAAK,QAAQ,cAAc,IAAI,MAAM,SAAS,CAAC;;CAGjD,uBAAuB;AACrB,QAAA,eAAqB,cAAc,EAAE,gBAAgB;AACrD,gBAAc,EAAE,kBAAkB,MAAA,cAAoB;AACtD,QAAA,uBAA6B;;CAG/B,qBAAqB;EACnB,MAAM,OAAO,cAAc,EAAE,gBAAgB;AAC7C,MAAI,SAAS,MAAA,cAAoB;AAC/B,SAAA,eAAqB;AACrB,QAAK,QAAQ,cAAc,IAAI,MAAM,aAAa,CAAC;;;CAIvD,OAAA,wBAA+B;AAC7B,MAAI,CAAC,MAAA,aAAmB,CAAC,KAAK,OAAQ;EAEtC,MAAM,YAAY,CAAC,GAAG,KAAK,OAAO,WAAW,CAAC,OAAO,yBAAyB;EAE9E,MAAM,WAAW,MAAA,aAAmB,WAAW,UAAU,EAAE,EACxD,QAAQ,EAAE,WAAW,SAAS,OAAO,KAAK,MAAM,UAAU,KAAK,CAC/D,SAAS,EAAE,UAAU,MAAM,cAAc;GACxC,MAAM,QAAQ,UAAU,MAAM,MAAM,EAAE,aAAa,YAAY,EAAE,UAAU,KAAK;AAChF,UAAO,OAAO,OAAO,CAAC;IAAE,MAAM,MAAM;IAAM;IAAS,CAAC,GAAG,EAAE;IACzD;EAEJ,MAAM,SAAS,IAAI,IAAI,QAAQ,QAAQ,MAAM,EAAE,SAAS,UAAU,CAAC,KAAK,MAAM,EAAE,QAAQ,CAAC;EACzF,MAAM,UAAU,QAAQ,MAAM,MAAM,EAAE,SAAS,UAAU,EAAE;EAE3D,MAAM,SAAS,gBAAgB,EAAE,eAAe,CAAC,MAAM,IAAI,kBAAkB,EAAE;EAC/E,MAAM,OAAO,IAAI,IAAI,OAAO,QAAQ,OAAO,CAAC,OAAO,IAAI,GAAG,CAAC,CAAC;AAC5D,MAAI,QAAS,MAAK,IAAI,QAAQ;AAE9B,MAAI,KAAK,SAAS,OAAO,UAAU,OAAO,OAAO,OAAO,KAAK,IAAI,GAAG,CAAC,CAAE;AAEvE,MAAI;AACF,SAAM,eAAe,IAAI,OAAO,KAAK,MAAM,sBAAsB,CAAC,GAAG,KAAK,CAAC,CAAC;WACrE,OAAO;AACd,WAAQ,MAAM,MAAM"} \ No newline at end of file diff --git a/dist/default/dom/google-cast/index.js b/dist/default/dom/google-cast/index.js new file mode 100644 index 00000000..613c9389 --- /dev/null +++ b/dist/default/dom/google-cast/index.js @@ -0,0 +1,2 @@ +import { GoogleCast, googleCastDefaultProps } from "./media.js"; +export { GoogleCast, googleCastDefaultProps }; diff --git a/dist/default/dom/google-cast/media.js b/dist/default/dom/google-cast/media.js new file mode 100644 index 00000000..4858a864 --- /dev/null +++ b/dist/default/dom/google-cast/media.js @@ -0,0 +1,110 @@ +import { requiresCastFramework } from "./utils.js"; +import { GoogleCastProvider } from "./google-cast-provider.js"; +//#region src/dom/google-cast/media.ts +const googleCastDefaultProps = { + src: void 0, + contentType: void 0, + streamType: void 0, + receiver: void 0, + customData: void 0 +}; +var GoogleCast = class { + #src; + #contentType; + #streamType; + #receiver; + #customData; + #media = null; + #provider = null; + #override = null; + constructor(props = {}) { + Object.assign(this, props); + } + setMedia(host) { + if (!requiresCastFramework()) return; + this.#media = host; + if (!this.#provider) { + this.#provider = new GoogleCastProvider(this); + this.#provider.remote.addEventListener("connect", this.#onStateChange); + this.#provider.remote.addEventListener("disconnect", this.#onStateChange); + this.#override = this.#createRemoteOverride(); + } + } + attach(target) { + this.#provider?.attach(target); + } + detach() { + this.#provider?.detach(); + } + destroy() { + this.#provider?.destroy(); + this.#provider = null; + this.#media = null; + } + #onStateChange = () => { + if (!this.#provider) return; + if (this.#provider.remote.state === "connected") this.#override = this.#provider; + else this.#override = this.#createRemoteOverride(); + }; + #createRemoteOverride() { + const provider = this.#provider; + return { get remote() { + return provider.remote; + } }; + } + get targetOverride() { + return this.#override; + } + /** Source URL loaded on the Cast receiver. Falls back to a `` child, `src`, then `currentSrc`. */ + get src() { + return this.#src ?? this.#media?.querySelector("source")?.src ?? this.#media?.src ?? this.#media?.currentSrc ?? ""; + } + set src(value) { + if (this.#src === value) return; + this.#src = value; + this.#load(); + } + /** MIME type of the Cast source. When unset, the receiver infers it from the URL. */ + get contentType() { + return this.#contentType; + } + set contentType(value) { + if (this.#contentType === value) return; + this.#contentType = value; + this.#load(); + } + /** Stream type used on the Cast receiver. Falls back to the host's `streamType` if it exposes one. */ + get streamType() { + return this.#streamType ?? this.#media?.streamType; + } + set streamType(value) { + if (this.#streamType === value) return; + this.#streamType = value; + this.#load(); + } + /** Cast receiver application ID. Read on session start; falls back to the layer's default. */ + get receiver() { + return this.#receiver; + } + set receiver(value) { + if (this.#receiver === value) return; + this.#receiver = value; + this.#load(); + } + /** Custom data sent to the Cast receiver with the load request. */ + get customData() { + return this.#customData; + } + set customData(value) { + if (this.#customData === value) return; + this.#customData = value; + this.#load(); + } + #load() { + if (this.#media?.remote.state === "connected") this.#media.load(); + } +}; +//#endregion +export { GoogleCast, googleCastDefaultProps }; + +//# sourceMappingURL=media.js.map \ No newline at end of file diff --git a/dist/default/dom/google-cast/media.js.map b/dist/default/dom/google-cast/media.js.map new file mode 100644 index 00000000..309c71d3 --- /dev/null +++ b/dist/default/dom/google-cast/media.js.map @@ -0,0 +1 @@ +{"version":3,"file":"media.js","names":["#media","#provider","#onStateChange","#override","#createRemoteOverride","#src","#load","#contentType","#streamType","#receiver","#customData"],"sources":["../../../../src/dom/google-cast/media.ts"],"sourcesContent":["import type { MediaStreamType } from '../../core/types';\nimport type { HTMLMediaElementHost, HTMLMediaTargetLike, MediaComponent } from '../media-host';\nimport { GoogleCastProvider } from './google-cast-provider';\nimport { requiresCastFramework } from './utils';\n\ntype MediaHost = HTMLMediaElementHost;\n\nexport interface GoogleCastProps {\n /** Source URL loaded on the Cast receiver. Falls back to the host's `src` / `currentSrc`. */\n src?: string | undefined;\n /** MIME type of the Cast source. When unset, the receiver infers it from the URL. */\n contentType?: string | undefined;\n /** Stream type used on the Cast receiver. */\n streamType?: MediaStreamType | undefined;\n /** Cast receiver application ID. Defaults to Google's default media receiver. */\n receiver?: string | undefined;\n /** Custom data sent to the Cast receiver with the load request. */\n customData?: Record | null | undefined;\n}\n\nexport const googleCastDefaultProps: GoogleCastProps = {\n src: undefined,\n contentType: undefined,\n streamType: undefined,\n receiver: undefined,\n customData: undefined,\n};\n\nexport class GoogleCast implements GoogleCastProps, MediaComponent {\n #src: string | undefined;\n #contentType: string | undefined;\n #streamType: MediaStreamType | undefined;\n #receiver: string | undefined;\n #customData: Record | null | undefined;\n #media: MediaHost | null = null;\n #provider: GoogleCastProvider | null = null;\n #override: Partial | null = null;\n\n constructor(props: GoogleCastProps = {}) {\n Object.assign(this, props);\n }\n\n setMedia(host: MediaHost) {\n if (!requiresCastFramework()) return;\n\n this.#media = host;\n\n if (!this.#provider) {\n this.#provider = new GoogleCastProvider(this);\n this.#provider.remote.addEventListener('connect', this.#onStateChange);\n this.#provider.remote.addEventListener('disconnect', this.#onStateChange);\n this.#override = this.#createRemoteOverride();\n }\n }\n\n attach(target: HTMLMediaTargetLike) {\n this.#provider?.attach(target);\n }\n\n detach() {\n this.#provider?.detach();\n }\n\n destroy() {\n this.#provider?.destroy();\n this.#provider = null;\n this.#media = null;\n }\n\n #onStateChange = () => {\n if (!this.#provider) return;\n\n if (this.#provider.remote.state === 'connected') {\n this.#override = this.#provider;\n } else {\n this.#override = this.#createRemoteOverride();\n }\n };\n\n #createRemoteOverride(): Partial {\n const provider = this.#provider!;\n return {\n get remote() {\n return provider.remote;\n },\n };\n }\n\n get targetOverride() {\n return this.#override;\n }\n\n /** Source URL loaded on the Cast receiver. Falls back to a `` child, `src`, then `currentSrc`. */\n get src() {\n return this.#src ?? this.#media?.querySelector('source')?.src ?? this.#media?.src ?? this.#media?.currentSrc ?? '';\n }\n\n set src(value: string | undefined) {\n if (this.#src === value) return;\n this.#src = value;\n this.#load();\n }\n\n /** MIME type of the Cast source. When unset, the receiver infers it from the URL. */\n get contentType() {\n return this.#contentType;\n }\n\n set contentType(value: string | undefined) {\n if (this.#contentType === value) return;\n this.#contentType = value;\n this.#load();\n }\n\n /** Stream type used on the Cast receiver. Falls back to the host's `streamType` if it exposes one. */\n get streamType() {\n return this.#streamType ?? (this.#media as { streamType?: MediaStreamType } | null)?.streamType;\n }\n\n set streamType(value: MediaStreamType | undefined) {\n if (this.#streamType === value) return;\n this.#streamType = value;\n this.#load();\n }\n\n /** Cast receiver application ID. Read on session start; falls back to the layer's default. */\n get receiver() {\n return this.#receiver;\n }\n\n set receiver(value: string | undefined) {\n if (this.#receiver === value) return;\n this.#receiver = value;\n this.#load();\n }\n\n /** Custom data sent to the Cast receiver with the load request. */\n get customData() {\n return this.#customData;\n }\n\n set customData(value: Record | null | undefined) {\n if (this.#customData === value) return;\n this.#customData = value;\n this.#load();\n }\n\n #load() {\n if (this.#media?.remote.state === 'connected') {\n this.#media.load();\n }\n }\n}\n"],"mappings":";;;AAoBA,MAAa,yBAA0C;CACrD,KAAK,KAAA;CACL,aAAa,KAAA;CACb,YAAY,KAAA;CACZ,UAAU,KAAA;CACV,YAAY,KAAA;CACb;AAED,IAAa,aAAb,MAAmE;CACjE;CACA;CACA;CACA;CACA;CACA,SAA2B;CAC3B,YAAuC;CACvC,YAAiD;CAEjD,YAAY,QAAyB,EAAE,EAAE;AACvC,SAAO,OAAO,MAAM,MAAM;;CAG5B,SAAS,MAAiB;AACxB,MAAI,CAAC,uBAAuB,CAAE;AAE9B,QAAA,QAAc;AAEd,MAAI,CAAC,MAAA,UAAgB;AACnB,SAAA,WAAiB,IAAI,mBAAmB,KAAK;AAC7C,SAAA,SAAe,OAAO,iBAAiB,WAAW,MAAA,cAAoB;AACtE,SAAA,SAAe,OAAO,iBAAiB,cAAc,MAAA,cAAoB;AACzE,SAAA,WAAiB,MAAA,sBAA4B;;;CAIjD,OAAO,QAA6B;AAClC,QAAA,UAAgB,OAAO,OAAO;;CAGhC,SAAS;AACP,QAAA,UAAgB,QAAQ;;CAG1B,UAAU;AACR,QAAA,UAAgB,SAAS;AACzB,QAAA,WAAiB;AACjB,QAAA,QAAc;;CAGhB,uBAAuB;AACrB,MAAI,CAAC,MAAA,SAAgB;AAErB,MAAI,MAAA,SAAe,OAAO,UAAU,YAClC,OAAA,WAAiB,MAAA;MAEjB,OAAA,WAAiB,MAAA,sBAA4B;;CAIjD,wBAAsD;EACpD,MAAM,WAAW,MAAA;AACjB,SAAO,EACL,IAAI,SAAS;AACX,UAAO,SAAS;KAEnB;;CAGH,IAAI,iBAAiB;AACnB,SAAO,MAAA;;;CAIT,IAAI,MAAM;AACR,SAAO,MAAA,OAAa,MAAA,OAAa,cAAc,SAAS,EAAE,OAAO,MAAA,OAAa,OAAO,MAAA,OAAa,cAAc;;CAGlH,IAAI,IAAI,OAA2B;AACjC,MAAI,MAAA,QAAc,MAAO;AACzB,QAAA,MAAY;AACZ,QAAA,MAAY;;;CAId,IAAI,cAAc;AAChB,SAAO,MAAA;;CAGT,IAAI,YAAY,OAA2B;AACzC,MAAI,MAAA,gBAAsB,MAAO;AACjC,QAAA,cAAoB;AACpB,QAAA,MAAY;;;CAId,IAAI,aAAa;AACf,SAAO,MAAA,cAAqB,MAAA,OAAyD;;CAGvF,IAAI,WAAW,OAAoC;AACjD,MAAI,MAAA,eAAqB,MAAO;AAChC,QAAA,aAAmB;AACnB,QAAA,MAAY;;;CAId,IAAI,WAAW;AACb,SAAO,MAAA;;CAGT,IAAI,SAAS,OAA2B;AACtC,MAAI,MAAA,aAAmB,MAAO;AAC9B,QAAA,WAAiB;AACjB,QAAA,MAAY;;;CAId,IAAI,aAAa;AACf,SAAO,MAAA;;CAGT,IAAI,WAAW,OAAmD;AAChE,MAAI,MAAA,eAAqB,MAAO;AAChC,QAAA,aAAmB;AACnB,QAAA,MAAY;;CAGd,QAAQ;AACN,MAAI,MAAA,OAAa,OAAO,UAAU,YAChC,OAAA,MAAY,MAAM"} \ No newline at end of file diff --git a/dist/default/dom/google-cast/registry.js b/dist/default/dom/google-cast/registry.js new file mode 100644 index 00000000..0b7b4c6f --- /dev/null +++ b/dist/default/dom/google-cast/registry.js @@ -0,0 +1,41 @@ +import { IterableWeakSet, getCastContext, loadCastFramework, onCastApiAvailable } from "./utils.js"; +//#region src/dom/google-cast/registry.ts +const googleCastInstances = new IterableWeakSet(); +let castFramework; +let pendingCastFramework = null; +async function ensureCastFramework() { + if (castFramework) return castFramework; + if (!pendingCastFramework) { + pendingCastFramework = loadCastFramework().then(() => new Promise((resolve, reject) => { + onCastApiAvailable(() => { + registerCastFramework(); + if (castFramework) { + resolve(castFramework); + return; + } + reject(new DOMException("Google Cast framework is unavailable.", "NotSupportedError")); + }); + })); + pendingCastFramework.catch(() => { + pendingCastFramework = null; + }); + } + return pendingCastFramework; +} +function registerCastFramework() { + if (!globalThis.chrome?.cast?.isAvailable) return; + if (!castFramework) { + castFramework = cast.framework; + getCastContext().addEventListener(castFramework.CastContextEventType.CAST_STATE_CHANGED, () => { + googleCastInstances.forEach((provider) => provider.onCastStateChanged()); + }); + getCastContext().addEventListener(castFramework.CastContextEventType.SESSION_STATE_CHANGED, () => { + googleCastInstances.forEach((provider) => provider.onSessionStateChanged()); + }); + googleCastInstances.forEach((provider) => provider.onCastFrameworkAvailable()); + } +} +//#endregion +export { castFramework, ensureCastFramework, googleCastInstances }; + +//# sourceMappingURL=registry.js.map \ No newline at end of file diff --git a/dist/default/dom/google-cast/registry.js.map b/dist/default/dom/google-cast/registry.js.map new file mode 100644 index 00000000..f5298c2b --- /dev/null +++ b/dist/default/dom/google-cast/registry.js.map @@ -0,0 +1 @@ +{"version":3,"file":"registry.js","names":[],"sources":["../../../../src/dom/google-cast/registry.ts"],"sourcesContent":["import type { GoogleCastProvider } from './google-cast-provider';\nimport { getCastContext, IterableWeakSet, loadCastFramework, onCastApiAvailable } from './utils';\n\nexport const googleCastInstances = new IterableWeakSet();\n\nexport let castFramework: typeof cast.framework | undefined;\n\nlet pendingCastFramework: Promise | null = null;\n\nexport async function ensureCastFramework(): Promise {\n if (castFramework) return castFramework;\n\n if (!pendingCastFramework) {\n pendingCastFramework = loadCastFramework().then(\n () =>\n new Promise((resolve, reject) => {\n onCastApiAvailable(() => {\n registerCastFramework();\n\n if (castFramework) {\n resolve(castFramework);\n return;\n }\n\n reject(new DOMException('Google Cast framework is unavailable.', 'NotSupportedError'));\n });\n })\n );\n\n // Reset on failure (e.g. script blocked by an ad-blocker) so a later\n // attempt can retry instead of replaying a cached rejection.\n pendingCastFramework.catch(() => {\n pendingCastFramework = null;\n });\n }\n\n return pendingCastFramework;\n}\n\nfunction registerCastFramework(): void {\n if (!globalThis.chrome?.cast?.isAvailable) {\n if (__DEV__) {\n console.debug('chrome.cast.isAvailable', globalThis.chrome?.cast?.isAvailable);\n }\n return;\n }\n\n if (!castFramework) {\n castFramework = cast.framework;\n\n getCastContext()!.addEventListener(castFramework.CastContextEventType.CAST_STATE_CHANGED, () => {\n googleCastInstances.forEach((provider) => provider.onCastStateChanged());\n });\n\n getCastContext()!.addEventListener(castFramework.CastContextEventType.SESSION_STATE_CHANGED, () => {\n googleCastInstances.forEach((provider) => provider.onSessionStateChanged());\n });\n\n googleCastInstances.forEach((provider) => provider.onCastFrameworkAvailable());\n }\n}\n"],"mappings":";;AAGA,MAAa,sBAAsB,IAAI,iBAAqC;AAE5E,IAAW;AAEX,IAAI,uBAA8D;AAElE,eAAsB,sBAAsD;AAC1E,KAAI,cAAe,QAAO;AAE1B,KAAI,CAAC,sBAAsB;AACzB,yBAAuB,mBAAmB,CAAC,WAEvC,IAAI,SAAgC,SAAS,WAAW;AACtD,4BAAyB;AACvB,2BAAuB;AAEvB,QAAI,eAAe;AACjB,aAAQ,cAAc;AACtB;;AAGF,WAAO,IAAI,aAAa,yCAAyC,oBAAoB,CAAC;KACtF;IACF,CACL;AAID,uBAAqB,YAAY;AAC/B,0BAAuB;IACvB;;AAGJ,QAAO;;AAGT,SAAS,wBAA8B;AACrC,KAAI,CAAC,WAAW,QAAQ,MAAM,YAI5B;AAGF,KAAI,CAAC,eAAe;AAClB,kBAAgB,KAAK;AAErB,kBAAgB,CAAE,iBAAiB,cAAc,qBAAqB,0BAA0B;AAC9F,uBAAoB,SAAS,aAAa,SAAS,oBAAoB,CAAC;IACxE;AAEF,kBAAgB,CAAE,iBAAiB,cAAc,qBAAqB,6BAA6B;AACjG,uBAAoB,SAAS,aAAa,SAAS,uBAAuB,CAAC;IAC3E;AAEF,sBAAoB,SAAS,aAAa,SAAS,0BAA0B,CAAC"} \ No newline at end of file diff --git a/dist/default/dom/google-cast/remote-playback.js b/dist/default/dom/google-cast/remote-playback.js new file mode 100644 index 00000000..fb000175 --- /dev/null +++ b/dist/default/dom/google-cast/remote-playback.js @@ -0,0 +1,73 @@ +import { InvalidStateError, NotFoundError } from "./utils.js"; +//#region src/dom/google-cast/remote-playback.ts +let callbackIdCount = 0; +/** +* Implementation of the W3C [`RemotePlayback`](https://developer.mozilla.org/en-US/docs/Web/API/RemotePlayback) +* interface backed by Google Cast. +* +* Surfaced via `host.remote` while the {@link GoogleCastProvider} is in the +* provider chain. The public API must strictly conform to the W3C spec: +* +* - Properties: `state` +* - Methods: `watchAvailability`, `cancelWatchAvailability`, `prompt` +* - Events: `connecting`, `connect`, `disconnect` +* +* Internal state mutations are pushed by {@link GoogleCastProvider} through +* private callbacks registered via `provider.bindHooks(...)` in the constructor — +* do not add public methods or properties that aren't part of the spec. +*/ +var RemotePlayback = class extends EventTarget { + #provider; + #state = "disconnected"; + #available = false; + #callbacks = /* @__PURE__ */ new Map(); + constructor(provider) { + super(); + this.#provider = provider; + provider.bindHooks({ + setState: (next) => this.#setState(next), + setAvailable: (available) => this.#setAvailable(available) + }); + } + get state() { + return this.#state; + } + async watchAvailability(callback) { + this.#assertEnabled(); + const id = ++callbackIdCount; + this.#callbacks.set(id, callback); + queueMicrotask(() => callback(this.#provider.hasDevicesAvailable())); + return id; + } + async cancelWatchAvailability(callbackId) { + this.#assertEnabled(); + if (callbackId === void 0) { + this.#callbacks.clear(); + return; + } + if (!this.#callbacks.delete(callbackId)) throw new NotFoundError(`Callback not found for id ${callbackId}.`); + } + async prompt() { + this.#assertEnabled(); + await this.#provider.requestCastSession(); + } + #assertEnabled() { + if (this.#provider.target?.disableRemotePlayback) throw new InvalidStateError("disableRemotePlayback attribute is present."); + } + #setState(next) { + if (this.#state === next) return; + this.#state = next; + if (next === "connecting") this.dispatchEvent(new Event("connecting")); + else if (next === "connected") this.dispatchEvent(new Event("connect")); + else this.dispatchEvent(new Event("disconnect")); + } + #setAvailable(available) { + if (this.#available === available) return; + this.#available = available; + for (const callback of this.#callbacks.values()) callback(available); + } +}; +//#endregion +export { RemotePlayback }; + +//# sourceMappingURL=remote-playback.js.map \ No newline at end of file diff --git a/dist/default/dom/google-cast/remote-playback.js.map b/dist/default/dom/google-cast/remote-playback.js.map new file mode 100644 index 00000000..e23888b7 --- /dev/null +++ b/dist/default/dom/google-cast/remote-playback.js.map @@ -0,0 +1 @@ +{"version":3,"file":"remote-playback.js","names":["#provider","#setState","#setAvailable","#state","#assertEnabled","#callbacks","#available"],"sources":["../../../../src/dom/google-cast/remote-playback.ts"],"sourcesContent":["import type { GoogleCastProvider } from './google-cast-provider';\nimport { InvalidStateError, NotFoundError } from './utils';\n\nexport type RemotePlaybackState = 'disconnected' | 'connecting' | 'connected';\n\ntype AvailabilityCallback = (available: boolean) => void;\n\nexport type RemotePlaybackHooks = {\n setState(next: RemotePlaybackState): void;\n setAvailable(available: boolean): void;\n};\n\nlet callbackIdCount = 0;\n\n/**\n * Implementation of the W3C [`RemotePlayback`](https://developer.mozilla.org/en-US/docs/Web/API/RemotePlayback)\n * interface backed by Google Cast.\n *\n * Surfaced via `host.remote` while the {@link GoogleCastProvider} is in the\n * provider chain. The public API must strictly conform to the W3C spec:\n *\n * - Properties: `state`\n * - Methods: `watchAvailability`, `cancelWatchAvailability`, `prompt`\n * - Events: `connecting`, `connect`, `disconnect`\n *\n * Internal state mutations are pushed by {@link GoogleCastProvider} through\n * private callbacks registered via `provider.bindHooks(...)` in the constructor —\n * do not add public methods or properties that aren't part of the spec.\n */\nexport class RemotePlayback extends EventTarget {\n #provider: GoogleCastProvider;\n #state: RemotePlaybackState = 'disconnected';\n #available = false;\n #callbacks = new Map();\n\n constructor(provider: GoogleCastProvider) {\n super();\n this.#provider = provider;\n provider.bindHooks({\n setState: (next) => this.#setState(next),\n setAvailable: (available) => this.#setAvailable(available),\n });\n }\n\n get state() {\n return this.#state;\n }\n\n async watchAvailability(callback: AvailabilityCallback) {\n this.#assertEnabled();\n const id = ++callbackIdCount;\n this.#callbacks.set(id, callback);\n queueMicrotask(() => callback(this.#provider.hasDevicesAvailable()));\n return id;\n }\n\n async cancelWatchAvailability(callbackId?: number) {\n this.#assertEnabled();\n\n if (callbackId === undefined) {\n this.#callbacks.clear();\n return;\n }\n\n if (!this.#callbacks.delete(callbackId)) {\n throw new NotFoundError(`Callback not found for id ${callbackId}.`);\n }\n }\n\n async prompt() {\n this.#assertEnabled();\n await this.#provider.requestCastSession();\n }\n\n #assertEnabled() {\n if (this.#provider.target?.disableRemotePlayback) {\n throw new InvalidStateError('disableRemotePlayback attribute is present.');\n }\n }\n\n #setState(next: RemotePlaybackState) {\n if (this.#state === next) return;\n this.#state = next;\n if (next === 'connecting') this.dispatchEvent(new Event('connecting'));\n else if (next === 'connected') this.dispatchEvent(new Event('connect'));\n else this.dispatchEvent(new Event('disconnect'));\n }\n\n #setAvailable(available: boolean) {\n if (this.#available === available) return;\n this.#available = available;\n for (const callback of this.#callbacks.values()) callback(available);\n }\n}\n"],"mappings":";;AAYA,IAAI,kBAAkB;;;;;;;;;;;;;;;;AAiBtB,IAAa,iBAAb,cAAoC,YAAY;CAC9C;CACA,SAA8B;CAC9B,aAAa;CACb,6BAAa,IAAI,KAAmC;CAEpD,YAAY,UAA8B;AACxC,SAAO;AACP,QAAA,WAAiB;AACjB,WAAS,UAAU;GACjB,WAAW,SAAS,MAAA,SAAe,KAAK;GACxC,eAAe,cAAc,MAAA,aAAmB,UAAU;GAC3D,CAAC;;CAGJ,IAAI,QAAQ;AACV,SAAO,MAAA;;CAGT,MAAM,kBAAkB,UAAgC;AACtD,QAAA,eAAqB;EACrB,MAAM,KAAK,EAAE;AACb,QAAA,UAAgB,IAAI,IAAI,SAAS;AACjC,uBAAqB,SAAS,MAAA,SAAe,qBAAqB,CAAC,CAAC;AACpE,SAAO;;CAGT,MAAM,wBAAwB,YAAqB;AACjD,QAAA,eAAqB;AAErB,MAAI,eAAe,KAAA,GAAW;AAC5B,SAAA,UAAgB,OAAO;AACvB;;AAGF,MAAI,CAAC,MAAA,UAAgB,OAAO,WAAW,CACrC,OAAM,IAAI,cAAc,6BAA6B,WAAW,GAAG;;CAIvE,MAAM,SAAS;AACb,QAAA,eAAqB;AACrB,QAAM,MAAA,SAAe,oBAAoB;;CAG3C,iBAAiB;AACf,MAAI,MAAA,SAAe,QAAQ,sBACzB,OAAM,IAAI,kBAAkB,8CAA8C;;CAI9E,UAAU,MAA2B;AACnC,MAAI,MAAA,UAAgB,KAAM;AAC1B,QAAA,QAAc;AACd,MAAI,SAAS,aAAc,MAAK,cAAc,IAAI,MAAM,aAAa,CAAC;WAC7D,SAAS,YAAa,MAAK,cAAc,IAAI,MAAM,UAAU,CAAC;MAClE,MAAK,cAAc,IAAI,MAAM,aAAa,CAAC;;CAGlD,cAAc,WAAoB;AAChC,MAAI,MAAA,cAAoB,UAAW;AACnC,QAAA,YAAkB;AAClB,OAAK,MAAM,YAAY,MAAA,UAAgB,QAAQ,CAAE,UAAS,UAAU"} \ No newline at end of file diff --git a/dist/default/dom/google-cast/utils.js b/dist/default/dom/google-cast/utils.js new file mode 100644 index 00000000..7e118a0d --- /dev/null +++ b/dist/default/dom/google-cast/utils.js @@ -0,0 +1,143 @@ +import { loadScript } from "@videojs/utils/dom"; +//#region src/dom/google-cast/utils.ts +var InvalidStateError = class extends Error {}; +var NotSupportedError = class extends Error {}; +var NotFoundError = class extends Error {}; +const GOOGLE_CAST_FRAMEWORK_URL = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"; +const HLS_RESPONSE_HEADERS = [ + "application/x-mpegURL", + "application/vnd.apple.mpegurl", + "audio/mpegurl" +]; +var IterableWeakSet = class { + #refs = /* @__PURE__ */ new Set(); + #seen = /* @__PURE__ */ new WeakMap(); + add(value) { + if (this.#seen.has(value)) return this; + const ref = new WeakRef(value); + this.#seen.set(value, ref); + this.#refs.add(ref); + return this; + } + delete(value) { + const ref = this.#seen.get(value); + if (!ref) return false; + this.#seen.delete(value); + return this.#refs.delete(ref); + } + forEach(fn) { + for (const ref of this.#refs) { + const value = ref.deref(); + if (value) fn(value); + else this.#refs.delete(ref); + } + } +}; +function onCastApiAvailable(callback) { + const whenDefined = () => customElements.whenDefined("google-cast-button").then(callback); + if (!globalThis.chrome?.cast?.isAvailable) globalThis.__onGCastApiAvailable = whenDefined; + else if (typeof cast === "undefined" || !cast.framework) whenDefined(); + else callback(); +} +function requiresCastFramework() { + return Boolean(globalThis.chrome); +} +async function loadCastFramework() { + if (globalThis.chrome?.cast) return; + await loadScript(GOOGLE_CAST_FRAMEWORK_URL); +} +function getCastContext() { + return typeof cast === "undefined" ? void 0 : cast.framework?.CastContext.getInstance(); +} +function currentSession() { + return getCastContext()?.getCurrentSession(); +} +function currentMedia() { + return currentSession()?.getSessionObj().media[0] ?? void 0; +} +function editTracksInfo(request) { + return new Promise((resolve, reject) => { + currentMedia().editTracksInfo(request, resolve, reject); + }); +} +function getMediaStatus(request) { + return new Promise((resolve, reject) => { + currentMedia().getStatus(request, resolve, reject); + }); +} +const MEDIA_NAMESPACE = "urn:x-cast:com.google.cast.media"; +let requestId = 0; +function setPlaybackRate(rate) { + const media = currentMedia(); + return currentSession().sendMessage(MEDIA_NAMESPACE, { + type: "SET_PLAYBACK_RATE", + playbackRate: rate, + mediaSessionId: media?.mediaSessionId, + requestId: ++requestId + }); +} +function setCastOptions(options) { + getCastContext().setOptions({ + ...getDefaultCastOptions(), + ...options + }); +} +function getDefaultCastOptions() { + return { + receiverApplicationId: "CC1AD845", + autoJoinPolicy: globalThis.chrome?.cast?.AutoJoinPolicy?.ORIGIN_SCOPED ?? "origin_scoped", + androidReceiverCompatible: false, + language: "en-US", + resumeSavedSession: true + }; +} +function getFormat(segment) { + if (!segment) return void 0; + const match = segment.match(/\.([a-zA-Z0-9]+)(?:\?.*)?$/); + return match ? match[1] : null; +} +function parsePlaylistUrls(playlistContent) { + const lines = playlistContent.split("\n"); + const urls = []; + for (let i = 0; i < lines.length; i++) if (lines[i].trim().startsWith("#EXT-X-STREAM-INF")) { + const nextLine = lines[i + 1] ? lines[i + 1].trim() : ""; + if (nextLine && !nextLine.startsWith("#")) urls.push(nextLine); + } + return urls; +} +function parseSegment(playlistContent) { + return playlistContent.split("\n").find((line) => !line.trim().startsWith("#") && line.trim() !== ""); +} +async function isHls(url) { + if (!url) return false; + if (/\.m3u8?(\?.*)?$/i.test(url)) return true; + if (url.startsWith("blob:")) return false; + try { + const contentType = (await fetch(url, { method: "HEAD" })).headers.get("Content-Type"); + if (!contentType) return false; + const normalizedContentType = contentType.toLowerCase().split(";")[0].trim(); + return HLS_RESPONSE_HEADERS.some((header) => normalizedContentType === header.toLowerCase()); + } catch (err) { + console.error("Error while trying to get the Content-Type of the manifest", err); + return false; + } +} +async function getPlaylistSegmentFormat(url) { + try { + const mainManifestContent = await (await fetch(url)).text(); + let availableChunksContent = mainManifestContent; + const playlists = parsePlaylistUrls(mainManifestContent); + if (playlists.length > 0) { + const chosenPlaylistUrl = new URL(playlists[0], url).toString(); + availableChunksContent = await (await fetch(chosenPlaylistUrl)).text(); + } + return getFormat(parseSegment(availableChunksContent)); + } catch (err) { + console.error("Error while trying to parse the manifest playlist", err); + return; + } +} +//#endregion +export { InvalidStateError, IterableWeakSet, NotFoundError, NotSupportedError, currentMedia, currentSession, editTracksInfo, getCastContext, getMediaStatus, getPlaylistSegmentFormat, isHls, loadCastFramework, onCastApiAvailable, requiresCastFramework, setCastOptions, setPlaybackRate }; + +//# sourceMappingURL=utils.js.map \ No newline at end of file diff --git a/dist/default/dom/google-cast/utils.js.map b/dist/default/dom/google-cast/utils.js.map new file mode 100644 index 00000000..b138ff3f --- /dev/null +++ b/dist/default/dom/google-cast/utils.js.map @@ -0,0 +1 @@ +{"version":3,"file":"utils.js","names":["#refs","#seen"],"sources":["../../../../src/dom/google-cast/utils.ts"],"sourcesContent":["import { loadScript } from '@videojs/utils/dom';\n\nexport class InvalidStateError extends Error {}\nexport class NotSupportedError extends Error {}\nexport class NotFoundError extends Error {}\n\nexport const GOOGLE_CAST_FRAMEWORK_URL = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';\n\nconst HLS_RESPONSE_HEADERS = ['application/x-mpegURL', 'application/vnd.apple.mpegurl', 'audio/mpegurl'];\n\nexport class IterableWeakSet {\n readonly #refs = new Set>();\n readonly #seen = new WeakMap>();\n\n add(value: T): this {\n if (this.#seen.has(value)) return this;\n const ref = new WeakRef(value);\n this.#seen.set(value, ref);\n this.#refs.add(ref);\n return this;\n }\n\n delete(value: T): boolean {\n const ref = this.#seen.get(value);\n if (!ref) return false;\n this.#seen.delete(value);\n return this.#refs.delete(ref);\n }\n\n forEach(fn: (value: T) => void): void {\n for (const ref of this.#refs) {\n const value = ref.deref();\n if (value) fn(value);\n else this.#refs.delete(ref);\n }\n }\n}\n\nexport function onCastApiAvailable(callback: () => void) {\n const whenDefined = () => customElements.whenDefined('google-cast-button').then(callback);\n\n if (!globalThis.chrome?.cast?.isAvailable) {\n (globalThis as { __onGCastApiAvailable?: () => void }).__onGCastApiAvailable = whenDefined;\n } else if (typeof cast === 'undefined' || !cast.framework) {\n whenDefined();\n } else {\n callback();\n }\n}\n\nexport function requiresCastFramework() {\n // todo: exclude for Android>=56 which supports the Remote Playback API natively.\n return Boolean(globalThis.chrome);\n}\n\nexport async function loadCastFramework() {\n if (globalThis.chrome?.cast) return;\n await loadScript(GOOGLE_CAST_FRAMEWORK_URL);\n}\n\nexport function getCastContext() {\n return typeof cast === 'undefined' ? undefined : cast.framework?.CastContext.getInstance();\n}\n\nexport function currentSession() {\n return getCastContext()?.getCurrentSession();\n}\n\nexport function currentMedia() {\n return currentSession()?.getSessionObj().media[0] ?? undefined;\n}\n\nexport function editTracksInfo(request: chrome.cast.media.EditTracksInfoRequest) {\n return new Promise((resolve, reject) => {\n currentMedia()!.editTracksInfo(request, resolve, reject);\n });\n}\n\nexport function getMediaStatus(request: chrome.cast.media.GetStatusRequest) {\n return new Promise((resolve, reject) => {\n currentMedia()!.getStatus(request, resolve, reject);\n });\n}\n\nconst MEDIA_NAMESPACE = 'urn:x-cast:com.google.cast.media';\nlet requestId = 0;\n\nexport function setPlaybackRate(rate: number) {\n const media = currentMedia();\n return currentSession()!.sendMessage(MEDIA_NAMESPACE, {\n type: 'SET_PLAYBACK_RATE',\n playbackRate: rate,\n mediaSessionId: media?.mediaSessionId,\n requestId: ++requestId,\n });\n}\n\nexport type CastOptions = cast.framework.CastOptions;\n\nexport function setCastOptions(options: Partial) {\n getCastContext()!.setOptions({\n ...getDefaultCastOptions(),\n ...options,\n });\n}\n\nexport function getDefaultCastOptions(): CastOptions {\n return {\n receiverApplicationId: 'CC1AD845',\n autoJoinPolicy:\n globalThis.chrome?.cast?.AutoJoinPolicy?.ORIGIN_SCOPED ?? ('origin_scoped' as chrome.cast.AutoJoinPolicy),\n androidReceiverCompatible: false,\n language: 'en-US',\n resumeSavedSession: true,\n };\n}\n\nfunction getFormat(segment: string | undefined) {\n if (!segment) return undefined;\n\n const regex = /\\.([a-zA-Z0-9]+)(?:\\?.*)?$/;\n const match = segment.match(regex);\n return match ? match[1] : null;\n}\n\nfunction parsePlaylistUrls(playlistContent: string) {\n const lines = playlistContent.split('\\n');\n const urls: string[] = [];\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!.trim();\n\n if (line.startsWith('#EXT-X-STREAM-INF')) {\n const nextLine = lines[i + 1] ? lines[i + 1]!.trim() : '';\n if (nextLine && !nextLine.startsWith('#')) {\n urls.push(nextLine);\n }\n }\n }\n\n return urls;\n}\n\nfunction parseSegment(playlistContent: string) {\n const lines = playlistContent.split('\\n');\n return lines.find((line) => !line.trim().startsWith('#') && line.trim() !== '');\n}\n\nexport async function isHls(url: string) {\n if (!url) return false;\n if (/\\.m3u8?(\\?.*)?$/i.test(url)) return true;\n if (url.startsWith('blob:')) return false;\n\n try {\n const response = await fetch(url, { method: 'HEAD' });\n const contentType = response.headers.get('Content-Type');\n if (!contentType) return false;\n const normalizedContentType = contentType.toLowerCase().split(';')[0]!.trim();\n return HLS_RESPONSE_HEADERS.some((header) => normalizedContentType === header.toLowerCase());\n } catch (err) {\n console.error('Error while trying to get the Content-Type of the manifest', err);\n return false;\n }\n}\n\nexport async function getPlaylistSegmentFormat(url: string) {\n try {\n const mainManifestContent = await (await fetch(url)).text();\n let availableChunksContent = mainManifestContent;\n\n const playlists = parsePlaylistUrls(mainManifestContent);\n if (playlists.length > 0) {\n const chosenPlaylistUrl = new URL(playlists[0]!, url).toString();\n availableChunksContent = await (await fetch(chosenPlaylistUrl)).text();\n }\n\n const segment = parseSegment(availableChunksContent);\n const format = getFormat(segment);\n return format;\n } catch (err) {\n console.error('Error while trying to parse the manifest playlist', err);\n return undefined;\n }\n}\n"],"mappings":"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"} \ No newline at end of file diff --git a/dist/default/dom/hls-js/airplay-bridge.js b/dist/default/dom/hls-js/airplay-bridge.js new file mode 100644 index 00000000..80bfa5cd --- /dev/null +++ b/dist/default/dom/hls-js/airplay-bridge.js @@ -0,0 +1,63 @@ +import { isWebKitAirPlayCapable, listen } from "@videojs/utils/dom"; +import Hls from "hls.js"; +//#region src/dom/hls-js/airplay-bridge.ts +/** +* Adds an AirPlay-capable fallback `` to the attached video element so +* Safari can hand the original HLS manifest off to AirPlay receivers while +* local playback continues through hls.js (MSE). +* When wireless-target changes, suspends hls.js loading so we don't double-fetch +* alongside the AirPlay receiver. +* +* Implements the WebKit-recommended pattern: +* https://webkit.org/blog/15036/how-to-use-media-source-extensions-with-airplay/ +* +* No-op on non-WebKit platforms (Chromium, Firefox). +*/ +function HlsJsMediaAirPlayMixin(BaseClass) { + class HlsJsMediaAirPlay extends BaseClass { + #sourceEl = null; + #disconnect = null; + constructor(...args) { + super(...args); + this.engine?.on(Hls.Events.MEDIA_ATTACHED, () => this.#init()); + this.engine?.on(Hls.Events.MEDIA_DETACHED, () => this.#destroy()); + this.engine?.on(Hls.Events.DESTROYING, () => this.#destroy()); + this.engine?.on(Hls.Events.MANIFEST_LOADING, (_event, data) => { + if (this.#sourceEl) this.#sourceEl.src = data.url; + }); + } + #init() { + this.#destroy(); + const target = this.target; + if (!target || !isWebKitAirPlayCapable(target)) return; + target.disableRemotePlayback = false; + this.#attachSource(target); + this.#setupLoadControl(target); + } + #attachSource(target) { + this.#sourceEl = document.createElement("source"); + this.#sourceEl.type = "application/x-mpegURL"; + this.#sourceEl.src = this.engine?.url ?? ""; + target.append(this.#sourceEl); + } + #setupLoadControl(target) { + const sync = () => { + if (target.webkitCurrentPlaybackTargetIsWireless) this.engine?.stopLoad(); + }; + this.#disconnect = new AbortController(); + listen(target, "webkitcurrentplaybacktargetiswirelesschanged", sync, { signal: this.#disconnect.signal }); + sync(); + } + #destroy() { + this.#disconnect?.abort(); + this.#disconnect = null; + this.#sourceEl?.remove(); + this.#sourceEl = null; + } + } + return HlsJsMediaAirPlay; +} +//#endregion +export { HlsJsMediaAirPlayMixin }; + +//# sourceMappingURL=airplay-bridge.js.map \ No newline at end of file diff --git a/dist/default/dom/hls-js/airplay-bridge.js.map b/dist/default/dom/hls-js/airplay-bridge.js.map new file mode 100644 index 00000000..be1aac6b --- /dev/null +++ b/dist/default/dom/hls-js/airplay-bridge.js.map @@ -0,0 +1 @@ +{"version":3,"file":"airplay-bridge.js","names":["#init","#destroy","#sourceEl","#attachSource","#setupLoadControl","#disconnect"],"sources":["../../../../src/dom/hls-js/airplay-bridge.ts"],"sourcesContent":["import { isWebKitAirPlayCapable, listen, type WebKitVideoElement } from '@videojs/utils/dom';\nimport type { Constructor } from '@videojs/utils/types';\nimport Hls from 'hls.js';\nimport type { HlsEngineHost } from './types';\n\n/**\n * Adds an AirPlay-capable fallback `` to the attached video element so\n * Safari can hand the original HLS manifest off to AirPlay receivers while\n * local playback continues through hls.js (MSE).\n * When wireless-target changes, suspends hls.js loading so we don't double-fetch\n * alongside the AirPlay receiver.\n *\n * Implements the WebKit-recommended pattern:\n * https://webkit.org/blog/15036/how-to-use-media-source-extensions-with-airplay/\n *\n * No-op on non-WebKit platforms (Chromium, Firefox).\n */\nexport function HlsJsMediaAirPlayMixin>(BaseClass: Base) {\n class HlsJsMediaAirPlay extends (BaseClass as Constructor) {\n #sourceEl: HTMLSourceElement | null = null;\n #disconnect: AbortController | null = null;\n\n constructor(...args: any[]) {\n super(...args);\n\n this.engine?.on(Hls.Events.MEDIA_ATTACHED, () => this.#init());\n this.engine?.on(Hls.Events.MEDIA_DETACHED, () => this.#destroy());\n this.engine?.on(Hls.Events.DESTROYING, () => this.#destroy());\n this.engine?.on(Hls.Events.MANIFEST_LOADING, (_event, data) => {\n if (this.#sourceEl) this.#sourceEl.src = data.url;\n });\n }\n\n #init(): void {\n this.#destroy();\n\n const target = this.target;\n if (!target || !isWebKitAirPlayCapable(target)) return;\n\n // Counter the `disableRemotePlayback = true` that other code paths may\n // set for MSE; AirPlay requires the picker to be available on this\n // element.\n target.disableRemotePlayback = false;\n this.#attachSource(target);\n this.#setupLoadControl(target);\n }\n\n #attachSource(target: WebKitVideoElement) {\n this.#sourceEl = document.createElement('source');\n this.#sourceEl.type = 'application/x-mpegURL';\n this.#sourceEl.src = this.engine?.url ?? '';\n target.append(this.#sourceEl);\n }\n\n #setupLoadControl(target: WebKitVideoElement) {\n const sync = () => {\n /*\n * From HLS.loadStart \"Depending on default config,\n * client starts loading automatically when a source is set.\"\n * Safari re-sets the source when we turn AirPlay off, so there\n * is no need to call start load here when current playback\n * target is not wireless.\n */\n if (target.webkitCurrentPlaybackTargetIsWireless) {\n this.engine?.stopLoad();\n }\n };\n\n this.#disconnect = new AbortController();\n listen(target as EventTarget, 'webkitcurrentplaybacktargetiswirelesschanged', sync, {\n signal: this.#disconnect.signal,\n });\n\n // AirPlay may already be active at (re)attach.\n sync();\n }\n\n #destroy(): void {\n this.#disconnect?.abort();\n this.#disconnect = null;\n this.#sourceEl?.remove();\n this.#sourceEl = null;\n }\n }\n\n return HlsJsMediaAirPlay as unknown as Base;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBA,SAAgB,uBAAgE,WAAiB;CAC/F,MAAM,0BAA2B,UAAyC;EACxE,YAAsC;EACtC,cAAsC;EAEtC,YAAY,GAAG,MAAa;AAC1B,SAAM,GAAG,KAAK;AAEd,QAAK,QAAQ,GAAG,IAAI,OAAO,sBAAsB,MAAA,MAAY,CAAC;AAC9D,QAAK,QAAQ,GAAG,IAAI,OAAO,sBAAsB,MAAA,SAAe,CAAC;AACjE,QAAK,QAAQ,GAAG,IAAI,OAAO,kBAAkB,MAAA,SAAe,CAAC;AAC7D,QAAK,QAAQ,GAAG,IAAI,OAAO,mBAAmB,QAAQ,SAAS;AAC7D,QAAI,MAAA,SAAgB,OAAA,SAAe,MAAM,KAAK;KAC9C;;EAGJ,QAAc;AACZ,SAAA,SAAe;GAEf,MAAM,SAAS,KAAK;AACpB,OAAI,CAAC,UAAU,CAAC,uBAAuB,OAAO,CAAE;AAKhD,UAAO,wBAAwB;AAC/B,SAAA,aAAmB,OAAO;AAC1B,SAAA,iBAAuB,OAAO;;EAGhC,cAAc,QAA4B;AACxC,SAAA,WAAiB,SAAS,cAAc,SAAS;AACjD,SAAA,SAAe,OAAO;AACtB,SAAA,SAAe,MAAM,KAAK,QAAQ,OAAO;AACzC,UAAO,OAAO,MAAA,SAAe;;EAG/B,kBAAkB,QAA4B;GAC5C,MAAM,aAAa;AAQjB,QAAI,OAAO,sCACT,MAAK,QAAQ,UAAU;;AAI3B,SAAA,aAAmB,IAAI,iBAAiB;AACxC,UAAO,QAAuB,gDAAgD,MAAM,EAClF,QAAQ,MAAA,WAAiB,QAC1B,CAAC;AAGF,SAAM;;EAGR,WAAiB;AACf,SAAA,YAAkB,OAAO;AACzB,SAAA,aAAmB;AACnB,SAAA,UAAgB,QAAQ;AACxB,SAAA,WAAiB;;;AAIrB,QAAO"} \ No newline at end of file diff --git a/dist/default/dom/hls-js/errors.js b/dist/default/dom/hls-js/errors.js new file mode 100644 index 00000000..cdb4ce0b --- /dev/null +++ b/dist/default/dom/hls-js/errors.js @@ -0,0 +1,58 @@ +import { MediaError } from "../../core/media-error.js"; +import Hls from "hls.js"; +//#region src/dom/hls-js/errors.ts +const hlsErrorTypeToCode = { + [Hls.ErrorTypes.NETWORK_ERROR]: MediaError.MEDIA_ERR_NETWORK, + [Hls.ErrorTypes.MEDIA_ERROR]: MediaError.MEDIA_ERR_DECODE, + [Hls.ErrorTypes.KEY_SYSTEM_ERROR]: MediaError.MEDIA_ERR_ENCRYPTED, + [Hls.ErrorTypes.MUX_ERROR]: MediaError.MEDIA_ERR_DECODE, + [Hls.ErrorTypes.OTHER_ERROR]: MediaError.MEDIA_ERR_CUSTOM +}; +function HlsJsMediaErrorsMixin(BaseClass) { + class HlsJsMediaErrors extends BaseClass { + #disconnect = null; + #error = null; + constructor(...args) { + super(...args); + this.engine?.on(Hls.Events.MANIFEST_LOADING, () => this.#init()); + this.engine?.on(Hls.Events.MEDIA_ATTACHED, () => this.#init()); + this.engine?.on(Hls.Events.MEDIA_DETACHED, () => this.#destroy()); + this.engine?.on(Hls.Events.DESTROYING, () => this.#destroy()); + } + get error() { + return this.#error; + } + #destroy() { + this.#disconnect?.abort(); + this.#disconnect = null; + } + #init() { + this.#disconnect?.abort(); + this.#disconnect = new AbortController(); + const { engine, target } = this; + if (!engine || !target) return; + const onError = (_event, data) => { + if (!data.fatal) return; + const code = hlsErrorTypeToCode[data.type] ?? MediaError.MEDIA_ERR_CUSTOM; + const error = new MediaError(data.error?.message, code, true, data.details); + error.data = data; + this.#error = error; + const event = new ErrorEvent("error", { + error, + message: error.message + }); + this.dispatchEvent(event); + }; + engine.on(Hls.Events.ERROR, onError); + this.#disconnect.signal.addEventListener("abort", () => { + engine.off(Hls.Events.ERROR, onError); + this.#error = null; + }, { once: true }); + } + } + return HlsJsMediaErrors; +} +//#endregion +export { HlsJsMediaErrorsMixin }; + +//# sourceMappingURL=errors.js.map \ No newline at end of file diff --git a/dist/default/dom/hls-js/errors.js.map b/dist/default/dom/hls-js/errors.js.map new file mode 100644 index 00000000..9a546e18 --- /dev/null +++ b/dist/default/dom/hls-js/errors.js.map @@ -0,0 +1 @@ +{"version":3,"file":"errors.js","names":["#init","#destroy","#error","#disconnect"],"sources":["../../../../src/dom/hls-js/errors.ts"],"sourcesContent":["import type { Constructor, MixinReturn } from '@videojs/utils/types';\nimport type { ErrorData } from 'hls.js';\nimport Hls from 'hls.js';\nimport { MediaError } from '../../core/media-error';\nimport type { HlsEngineHost } from './types';\n\nconst hlsErrorTypeToCode: Record = {\n [Hls.ErrorTypes.NETWORK_ERROR]: MediaError.MEDIA_ERR_NETWORK,\n [Hls.ErrorTypes.MEDIA_ERROR]: MediaError.MEDIA_ERR_DECODE,\n [Hls.ErrorTypes.KEY_SYSTEM_ERROR]: MediaError.MEDIA_ERR_ENCRYPTED,\n [Hls.ErrorTypes.MUX_ERROR]: MediaError.MEDIA_ERR_DECODE,\n [Hls.ErrorTypes.OTHER_ERROR]: MediaError.MEDIA_ERR_CUSTOM,\n};\n\nexport function HlsJsMediaErrorsMixin>(BaseClass: Base) {\n class HlsJsMediaErrors extends BaseClass {\n #disconnect: AbortController | null = null;\n #error: MediaError | null = null;\n\n constructor(...args: any[]) {\n super(...args);\n\n this.engine?.on(Hls.Events.MANIFEST_LOADING, () => this.#init());\n this.engine?.on(Hls.Events.MEDIA_ATTACHED, () => this.#init());\n this.engine?.on(Hls.Events.MEDIA_DETACHED, () => this.#destroy());\n this.engine?.on(Hls.Events.DESTROYING, () => this.#destroy());\n }\n\n get error(): MediaError | null {\n return this.#error;\n }\n\n #destroy(): void {\n this.#disconnect?.abort();\n this.#disconnect = null;\n }\n\n #init(): void {\n this.#disconnect?.abort();\n this.#disconnect = new AbortController();\n\n const { engine, target } = this;\n if (!engine || !target) return;\n\n const onError = (_event: string, data: ErrorData) => {\n if (!data.fatal) return;\n\n const code = hlsErrorTypeToCode[data.type] ?? MediaError.MEDIA_ERR_CUSTOM;\n const error = new MediaError(data.error?.message, code, true, data.details);\n error.data = data;\n\n this.#error = error;\n\n const event = new ErrorEvent('error', { error, message: error.message });\n this.dispatchEvent(event);\n };\n\n engine.on(Hls.Events.ERROR, onError);\n\n this.#disconnect.signal.addEventListener(\n 'abort',\n () => {\n engine.off(Hls.Events.ERROR, onError);\n this.#error = null;\n },\n { once: true }\n );\n }\n }\n\n return HlsJsMediaErrors as unknown as MixinReturn;\n}\n"],"mappings":";;;AAMA,MAAM,qBAA6C;EAChD,IAAI,WAAW,gBAAgB,WAAW;EAC1C,IAAI,WAAW,cAAc,WAAW;EACxC,IAAI,WAAW,mBAAmB,WAAW;EAC7C,IAAI,WAAW,YAAY,WAAW;EACtC,IAAI,WAAW,cAAc,WAAW;CAC1C;AAED,SAAgB,sBAA+D,WAAiB;CAC9F,MAAM,yBAAyB,UAAU;EACvC,cAAsC;EACtC,SAA4B;EAE5B,YAAY,GAAG,MAAa;AAC1B,SAAM,GAAG,KAAK;AAEd,QAAK,QAAQ,GAAG,IAAI,OAAO,wBAAwB,MAAA,MAAY,CAAC;AAChE,QAAK,QAAQ,GAAG,IAAI,OAAO,sBAAsB,MAAA,MAAY,CAAC;AAC9D,QAAK,QAAQ,GAAG,IAAI,OAAO,sBAAsB,MAAA,SAAe,CAAC;AACjE,QAAK,QAAQ,GAAG,IAAI,OAAO,kBAAkB,MAAA,SAAe,CAAC;;EAG/D,IAAI,QAA2B;AAC7B,UAAO,MAAA;;EAGT,WAAiB;AACf,SAAA,YAAkB,OAAO;AACzB,SAAA,aAAmB;;EAGrB,QAAc;AACZ,SAAA,YAAkB,OAAO;AACzB,SAAA,aAAmB,IAAI,iBAAiB;GAExC,MAAM,EAAE,QAAQ,WAAW;AAC3B,OAAI,CAAC,UAAU,CAAC,OAAQ;GAExB,MAAM,WAAW,QAAgB,SAAoB;AACnD,QAAI,CAAC,KAAK,MAAO;IAEjB,MAAM,OAAO,mBAAmB,KAAK,SAAS,WAAW;IACzD,MAAM,QAAQ,IAAI,WAAW,KAAK,OAAO,SAAS,MAAM,MAAM,KAAK,QAAQ;AAC3E,UAAM,OAAO;AAEb,UAAA,QAAc;IAEd,MAAM,QAAQ,IAAI,WAAW,SAAS;KAAE;KAAO,SAAS,MAAM;KAAS,CAAC;AACxE,SAAK,cAAc,MAAM;;AAG3B,UAAO,GAAG,IAAI,OAAO,OAAO,QAAQ;AAEpC,SAAA,WAAiB,OAAO,iBACtB,eACM;AACJ,WAAO,IAAI,IAAI,OAAO,OAAO,QAAQ;AACrC,UAAA,QAAc;MAEhB,EAAE,MAAM,MAAM,CACf;;;AAIL,QAAO"} \ No newline at end of file diff --git a/dist/default/dom/hls-js/hls-js-only.js b/dist/default/dom/hls-js/hls-js-only.js new file mode 100644 index 00000000..96112ddb --- /dev/null +++ b/dist/default/dom/hls-js/hls-js-only.js @@ -0,0 +1,58 @@ +import { HTMLVideoElementHost } from "../video-host/video-host.js"; +import { MediaTracksMixin } from "../../core/media-tracks/mixin.js"; +import { HlsJsMediaAirPlayMixin } from "./airplay-bridge.js"; +import { HlsJsMediaErrorsMixin } from "./errors.js"; +import { HlsJsMediaLiveMixin } from "./live.js"; +import { HlsJsMediaMediaTracksMixin } from "./media-tracks.js"; +import { HlsJsMediaMetadataTracksMixin } from "./metadata-tracks.js"; +import { HlsJsMediaPreloadMixin } from "./preload.js"; +import { HlsJsMediaStreamTypeMixin } from "./stream-type.js"; +import { HlsJsMediaTextTracksMixin } from "./text-tracks.js"; +import Hls from "hls.js"; +//#region src/dom/hls-js/hls-js-only.ts +const defaultHlsConfig = { + backBufferLength: 30, + renderTextTracksNatively: false, + liveDurationInfinity: true, + capLevelToPlayerSize: true, + capLevelOnFPSDrop: true, + autoStartLoad: false +}; +var HlsJsOnlyMediaBase = class extends HTMLVideoElementHost { + #engine = null; + constructor(params) { + super(); + this.#engine = new Hls({ + ...defaultHlsConfig, + ...params.config + }); + } + get engine() { + return this.#engine; + } + get src() { + return this.#engine?.url ?? ""; + } + set src(src) { + this.#engine?.loadSource(src); + } + attach(target) { + super.attach(target); + this.#engine?.attachMedia(target); + } + detach() { + this.#engine?.detachMedia(); + super.detach(); + } + destroy() { + this.detach(); + this.#engine?.destroy(); + this.#engine = null; + } +}; +const HlsJsOnlyMediaComposed = HlsJsMediaAirPlayMixin(HlsJsMediaPreloadMixin(HlsJsMediaLiveMixin(HlsJsMediaStreamTypeMixin(HlsJsMediaMediaTracksMixin(HlsJsMediaMetadataTracksMixin(HlsJsMediaTextTracksMixin(HlsJsMediaErrorsMixin(MediaTracksMixin(HlsJsOnlyMediaBase))))))))); +var HlsJsOnlyMedia = class extends HlsJsOnlyMediaComposed {}; +//#endregion +export { HlsJsOnlyMedia }; + +//# sourceMappingURL=hls-js-only.js.map \ No newline at end of file diff --git a/dist/default/dom/hls-js/hls-js-only.js.map b/dist/default/dom/hls-js/hls-js-only.js.map new file mode 100644 index 00000000..c4dfd8bf --- /dev/null +++ b/dist/default/dom/hls-js/hls-js-only.js.map @@ -0,0 +1 @@ +{"version":3,"file":"hls-js-only.js","names":["#engine"],"sources":["../../../../src/dom/hls-js/hls-js-only.ts"],"sourcesContent":["import type { MixinReturn } from '@videojs/utils/types';\nimport Hls, { type HlsConfig } from 'hls.js';\nimport type { MediaError } from '../../core/media-error';\nimport { MediaTracksMixin, type WithMediaTracks } from '../../core/media-tracks';\nimport type {\n MediaEngineHost,\n MediaLiveCapability,\n MediaSourceCapability,\n MediaStreamTypeCapability,\n} from '../../core/types';\nimport { HTMLVideoElementHost } from '../video-host';\nimport { HlsJsMediaAirPlayMixin } from './airplay-bridge';\nimport { HlsJsMediaErrorsMixin } from './errors';\nimport { HlsJsMediaLiveMixin } from './live';\nimport { HlsJsMediaMediaTracksMixin } from './media-tracks';\nimport { HlsJsMediaMetadataTracksMixin } from './metadata-tracks';\nimport { HlsJsMediaPreloadMixin } from './preload';\nimport { HlsJsMediaStreamTypeMixin } from './stream-type';\nimport { HlsJsMediaTextTracksMixin } from './text-tracks';\n\nexport const defaultHlsConfig: Partial = {\n backBufferLength: 30,\n renderTextTracksNatively: false,\n liveDurationInfinity: true,\n capLevelToPlayerSize: true,\n capLevelOnFPSDrop: true,\n autoStartLoad: false,\n};\n\nclass HlsJsOnlyMediaBase extends HTMLVideoElementHost implements MediaEngineHost {\n #engine: Hls | null = null;\n\n constructor(params: { config: Partial }) {\n super();\n this.#engine = new Hls({\n ...defaultHlsConfig,\n ...params.config,\n });\n }\n\n get engine() {\n return this.#engine;\n }\n\n get src() {\n return this.#engine?.url ?? '';\n }\n\n set src(src: string) {\n this.#engine?.loadSource(src);\n }\n\n attach(target: HTMLVideoElement) {\n super.attach(target);\n this.#engine?.attachMedia(target);\n }\n\n detach() {\n this.#engine?.detachMedia();\n super.detach();\n }\n\n destroy() {\n this.detach();\n this.#engine?.destroy();\n this.#engine = null;\n }\n}\n\ninterface HlsJsMediaCapabilities\n extends MediaStreamTypeCapability,\n MediaLiveCapability,\n Pick {\n readonly error: MediaError | null;\n}\n\nconst HlsJsOnlyMediaComposed = HlsJsMediaAirPlayMixin(\n HlsJsMediaPreloadMixin(\n HlsJsMediaLiveMixin(\n HlsJsMediaStreamTypeMixin(\n HlsJsMediaMediaTracksMixin(\n HlsJsMediaMetadataTracksMixin(\n HlsJsMediaTextTracksMixin(HlsJsMediaErrorsMixin(MediaTracksMixin(HlsJsOnlyMediaBase)))\n )\n )\n )\n )\n )\n) as unknown as MixinReturn, HlsJsMediaCapabilities>;\n\nexport class HlsJsOnlyMedia extends HlsJsOnlyMediaComposed {}\n"],"mappings":";;;;;;;;;;;;AAoBA,MAAa,mBAAuC;CAClD,kBAAkB;CAClB,0BAA0B;CAC1B,sBAAsB;CACtB,sBAAsB;CACtB,mBAAmB;CACnB,eAAe;CAChB;AAED,IAAM,qBAAN,cAAiC,qBAAuE;CACtG,UAAsB;CAEtB,YAAY,QAAwC;AAClD,SAAO;AACP,QAAA,SAAe,IAAI,IAAI;GACrB,GAAG;GACH,GAAG,OAAO;GACX,CAAC;;CAGJ,IAAI,SAAS;AACX,SAAO,MAAA;;CAGT,IAAI,MAAM;AACR,SAAO,MAAA,QAAc,OAAO;;CAG9B,IAAI,IAAI,KAAa;AACnB,QAAA,QAAc,WAAW,IAAI;;CAG/B,OAAO,QAA0B;AAC/B,QAAM,OAAO,OAAO;AACpB,QAAA,QAAc,YAAY,OAAO;;CAGnC,SAAS;AACP,QAAA,QAAc,aAAa;AAC3B,QAAM,QAAQ;;CAGhB,UAAU;AACR,OAAK,QAAQ;AACb,QAAA,QAAc,SAAS;AACvB,QAAA,SAAe;;;AAWnB,MAAM,yBAAyB,uBAC7B,uBACE,oBACE,0BACE,2BACE,8BACE,0BAA0B,sBAAsB,iBAAiB,mBAAmB,CAAC,CAAC,CACvF,CACF,CACF,CACF,CACF,CACF;AAED,IAAa,iBAAb,cAAoC,uBAAuB"} \ No newline at end of file diff --git a/dist/default/dom/hls-js/index.js b/dist/default/dom/hls-js/index.js new file mode 100644 index 00000000..f0cf8a6b --- /dev/null +++ b/dist/default/dom/hls-js/index.js @@ -0,0 +1,2 @@ +import { ContentTypes, Hls, HlsJsMedia, PlaybackTypes, StreamTypes, hlsMediaDefaultProps } from "./media.js"; +export { ContentTypes, Hls, HlsJsMedia, PlaybackTypes, StreamTypes, hlsMediaDefaultProps }; diff --git a/dist/default/dom/hls-js/live.js b/dist/default/dom/hls-js/live.js new file mode 100644 index 00000000..1a803595 --- /dev/null +++ b/dist/default/dom/hls-js/live.js @@ -0,0 +1,125 @@ +import { MediaStreamTypes } from "../../core/types.js"; +import Hls from "hls.js"; +//#region src/dom/hls-js/live.ts +function HlsJsMediaLiveMixin(BaseClass) { + class HlsJsMediaLive extends BaseClass { + #targetLiveWindow = NaN; + #liveEdgeStartOffset; + #seekToLiveAbort = null; + #seekToLivePending = false; + constructor(...args) { + super(...args); + const { engine } = this; + engine?.on(Hls.Events.MANIFEST_LOADING, () => { + this.#reset(); + this.#armSeekToLive(); + }); + engine?.on(Hls.Events.MEDIA_ATTACHED, () => this.#armSeekToLive()); + engine?.on(Hls.Events.MEDIA_DETACHED, () => this.#disarmSeekToLive()); + engine?.on(Hls.Events.DESTROYING, () => { + this.#reset(); + this.#disarmSeekToLive(); + }); + engine?.on(Hls.Events.LEVEL_LOADED, (_event, data) => { + this.#derive(data.details); + if (this.#seekToLivePending) this.#trySeekToLive(); + }); + } + get targetLiveWindow() { + return this.#targetLiveWindow; + } + get liveEdgeStart() { + if (this.#liveEdgeStartOffset === void 0) return NaN; + const { target } = this; + if (!target) return NaN; + const { seekable } = target; + if (!seekable.length) return NaN; + return seekable.end(seekable.length - 1) - this.#liveEdgeStartOffset; + } + #derive(details) { + if (!details.live) return this.#reset(); + const info = getStreamInfoFromHlsjsLevelDetails(details); + this.#liveEdgeStartOffset = info.liveEdgeStartOffset; + this.#updateConfig(info); + this.#setTargetLiveWindow(info.targetLiveWindow); + } + #reset() { + this.#liveEdgeStartOffset = void 0; + this.#setTargetLiveWindow(NaN); + } + #updateConfig({ streamType, lowLatency }) { + if (streamType === MediaStreamTypes.LIVE) { + const hls = this.engine; + if (!hls) return; + if (lowLatency) { + hls.config.backBufferLength = hls.userConfig.backBufferLength ?? 4; + hls.config.maxFragLookUpTolerance = hls.userConfig.maxFragLookUpTolerance ?? .001; + hls.config.abrBandWidthUpFactor = hls.userConfig.abrBandWidthUpFactor ?? hls.config.abrBandWidthFactor; + } else hls.config.backBufferLength = hls.userConfig.backBufferLength ?? 8; + } + } + #setTargetLiveWindow(value) { + if (Object.is(this.#targetLiveWindow, value)) return; + this.#targetLiveWindow = value; + this.dispatchEvent(new Event("targetlivewindowchange")); + } + /** + * Arm a one-shot seek-to-live on the first user-initiated `play`. Skipped + * when `autoplay` is set, since hls.js positions at the live edge during + * its own startup sequence and a programmatic seek would race that. + */ + #armSeekToLive() { + this.#disarmSeekToLive(); + const target = this.target; + if (!target || target.autoplay) return; + this.#seekToLiveAbort = new AbortController(); + target.addEventListener("play", () => { + this.#seekToLivePending = true; + this.#trySeekToLive(); + }, { + signal: this.#seekToLiveAbort.signal, + once: true + }); + } + #disarmSeekToLive() { + this.#seekToLiveAbort?.abort(); + this.#seekToLiveAbort = null; + this.#seekToLivePending = false; + } + #trySeekToLive() { + const target = this.target; + if (!target) return; + const { liveEdgeStart } = this; + if (!Number.isFinite(liveEdgeStart)) return; + if (target.currentTime < liveEdgeStart) target.currentTime = liveEdgeStart; + this.#seekToLivePending = false; + } + } + return HlsJsMediaLive; +} +const getStreamInfoFromHlsjsLevelDetails = (levelDetails) => { + const playlistType = levelDetails.type; + const streamType = toStreamTypeFromPlaylistType(playlistType); + const targetLiveWindow = toTargetLiveWindowFromPlaylistType(playlistType); + const lowLatency = !!levelDetails.partList?.length; + let liveEdgeStartOffset; + if (streamType === MediaStreamTypes.LIVE) liveEdgeStartOffset = lowLatency ? levelDetails.partHoldBack || levelDetails.partTarget * 2 : levelDetails.holdBack || levelDetails.targetduration * 3; + return { + streamType, + targetLiveWindow, + liveEdgeStartOffset, + lowLatency + }; +}; +const toStreamTypeFromPlaylistType = (playlistType) => { + return playlistType === "VOD" ? MediaStreamTypes.ON_DEMAND : MediaStreamTypes.LIVE; +}; +const toTargetLiveWindowFromPlaylistType = (playlistType) => { + if (playlistType === "EVENT") return Number.POSITIVE_INFINITY; + if (playlistType === "VOD") return NaN; + return 0; +}; +//#endregion +export { HlsJsMediaLiveMixin }; + +//# sourceMappingURL=live.js.map \ No newline at end of file diff --git a/dist/default/dom/hls-js/live.js.map b/dist/default/dom/hls-js/live.js.map new file mode 100644 index 00000000..7bf567f5 --- /dev/null +++ b/dist/default/dom/hls-js/live.js.map @@ -0,0 +1 @@ +{"version":3,"file":"live.js","names":["#reset","#armSeekToLive","#disarmSeekToLive","#derive","#seekToLivePending","#trySeekToLive","#targetLiveWindow","#liveEdgeStartOffset","#updateConfig","#setTargetLiveWindow","#seekToLiveAbort"],"sources":["../../../../src/dom/hls-js/live.ts"],"sourcesContent":["import type { Constructor } from '@videojs/utils/types';\nimport type { LevelLoadedData } from 'hls.js';\nimport Hls from 'hls.js';\nimport { MediaStreamTypes } from '../../core/types';\nimport type { HlsEngineHost, HlsPlaylistTypes } from './types';\n\nexport function HlsJsMediaLiveMixin>(BaseClass: Base) {\n class HlsJsMediaLive extends (BaseClass as Constructor) {\n #targetLiveWindow = Number.NaN;\n #liveEdgeStartOffset: number | undefined;\n #seekToLiveAbort: AbortController | null = null;\n #seekToLivePending = false;\n\n constructor(...args: any[]) {\n super(...args);\n\n const { engine } = this;\n engine?.on(Hls.Events.MANIFEST_LOADING, () => {\n this.#reset();\n this.#armSeekToLive();\n });\n engine?.on(Hls.Events.MEDIA_ATTACHED, () => this.#armSeekToLive());\n engine?.on(Hls.Events.MEDIA_DETACHED, () => this.#disarmSeekToLive());\n engine?.on(Hls.Events.DESTROYING, () => {\n this.#reset();\n this.#disarmSeekToLive();\n });\n engine?.on(Hls.Events.LEVEL_LOADED, (_event: string, data: LevelLoadedData) => {\n this.#derive(data.details);\n // For `preload=\"none\"`/`\"metadata\"` the manifest only loads after the\n // first play, so retry the seek once `liveEdgeStart` becomes finite.\n if (this.#seekToLivePending) this.#trySeekToLive();\n });\n }\n\n get targetLiveWindow() {\n return this.#targetLiveWindow;\n }\n\n // Derived from seekable + offset at read time. No cached state, no event.\n get liveEdgeStart() {\n if (this.#liveEdgeStartOffset === undefined) return Number.NaN;\n const { target } = this;\n if (!target) return Number.NaN;\n const { seekable } = target;\n if (!seekable.length) return Number.NaN;\n return seekable.end(seekable.length - 1) - this.#liveEdgeStartOffset;\n }\n\n #derive(details: LevelLoadedData['details']) {\n if (!details.live) return this.#reset();\n\n const info = getStreamInfoFromHlsjsLevelDetails(details);\n\n this.#liveEdgeStartOffset = info.liveEdgeStartOffset;\n this.#updateConfig(info);\n this.#setTargetLiveWindow(info.targetLiveWindow);\n }\n\n #reset() {\n this.#liveEdgeStartOffset = undefined;\n this.#setTargetLiveWindow(Number.NaN);\n }\n\n #updateConfig({ streamType, lowLatency }: StreamInfo) {\n if (streamType === MediaStreamTypes.LIVE) {\n // Update hls.js config for live/ll-live\n const hls = this.engine;\n if (!hls) return;\n\n if (lowLatency) {\n hls.config.backBufferLength = hls.userConfig.backBufferLength ?? 4;\n hls.config.maxFragLookUpTolerance = hls.userConfig.maxFragLookUpTolerance ?? 0.001;\n // For ll-hls, ensure that up switches are weighted the same as down switches to mitigate\n // cases of getting stuck at lower bitrates.\n hls.config.abrBandWidthUpFactor = hls.userConfig.abrBandWidthUpFactor ?? hls.config.abrBandWidthFactor;\n } else {\n hls.config.backBufferLength = hls.userConfig.backBufferLength ?? 8;\n }\n }\n }\n\n #setTargetLiveWindow(value: number) {\n if (Object.is(this.#targetLiveWindow, value)) return;\n this.#targetLiveWindow = value;\n this.dispatchEvent(new Event('targetlivewindowchange'));\n }\n\n /**\n * Arm a one-shot seek-to-live on the first user-initiated `play`. Skipped\n * when `autoplay` is set, since hls.js positions at the live edge during\n * its own startup sequence and a programmatic seek would race that.\n */\n #armSeekToLive() {\n this.#disarmSeekToLive();\n\n const target = this.target as HTMLVideoElement | null;\n if (!target || target.autoplay) return;\n\n this.#seekToLiveAbort = new AbortController();\n target.addEventListener(\n 'play',\n () => {\n this.#seekToLivePending = true;\n this.#trySeekToLive();\n },\n { signal: this.#seekToLiveAbort.signal, once: true }\n );\n }\n\n #disarmSeekToLive() {\n this.#seekToLiveAbort?.abort();\n this.#seekToLiveAbort = null;\n this.#seekToLivePending = false;\n }\n\n #trySeekToLive() {\n const target = this.target as HTMLVideoElement | null;\n if (!target) return;\n const { liveEdgeStart } = this;\n if (!Number.isFinite(liveEdgeStart)) return;\n\n if (target.currentTime < liveEdgeStart) {\n target.currentTime = liveEdgeStart;\n }\n this.#seekToLivePending = false;\n }\n }\n\n return HlsJsMediaLive as unknown as Base &\n Constructor<{ readonly liveEdgeStart: number; readonly targetLiveWindow: number }>;\n}\n\ntype StreamInfo = ReturnType;\n\nconst getStreamInfoFromHlsjsLevelDetails = (levelDetails: LevelLoadedData['details']) => {\n const playlistType: HlsPlaylistTypes = levelDetails.type as HlsPlaylistTypes;\n const streamType = toStreamTypeFromPlaylistType(playlistType);\n const targetLiveWindow = toTargetLiveWindowFromPlaylistType(playlistType);\n const lowLatency = !!levelDetails.partList?.length;\n let liveEdgeStartOffset: number | undefined;\n if (streamType === MediaStreamTypes.LIVE) {\n // Prefer manifest-declared HOLD-BACK / PART-HOLD-BACK when present;\n // otherwise fall back to the per-spec multiples of the target durations.\n // See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-12\n liveEdgeStartOffset = lowLatency\n ? levelDetails.partHoldBack || levelDetails.partTarget * 2\n : levelDetails.holdBack || levelDetails.targetduration * 3;\n }\n\n return {\n streamType,\n targetLiveWindow,\n liveEdgeStartOffset,\n lowLatency,\n };\n};\n\nconst toStreamTypeFromPlaylistType = (playlistType: HlsPlaylistTypes) => {\n return playlistType === 'VOD' ? MediaStreamTypes.ON_DEMAND : MediaStreamTypes.LIVE;\n};\n\nconst toTargetLiveWindowFromPlaylistType = (playlistType: HlsPlaylistTypes) => {\n if (playlistType === 'EVENT') return Number.POSITIVE_INFINITY;\n if (playlistType === 'VOD') return Number.NaN;\n return 0;\n};\n"],"mappings":"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"} \ No newline at end of file diff --git a/dist/default/dom/hls-js/media-tracks.js b/dist/default/dom/hls-js/media-tracks.js new file mode 100644 index 00000000..ed9b0131 --- /dev/null +++ b/dist/default/dom/hls-js/media-tracks.js @@ -0,0 +1,123 @@ +import Hls from "hls.js"; +//#region src/dom/hls-js/media-tracks.ts +function getLevelKey(level) { + return `${level.url[0] ?? ""}|${level.width}x${level.height}|${level.videoCodec}|${level.bitrate}`; +} +/** +* Mirrors hls.js manifest levels and alternate audio into the media element's +* `videoRenditions` / `audioTracks` lists, and wires user selection back to +* `engine.nextLevel` and `engine.audioTrack`. +* +* Requires the media-tracks mixin (track-list infrastructure) to be applied +* earlier in the chain so the host exposes `addVideoTrack`, `videoRenditions`, +* and friends. +*/ +function HlsJsMediaMediaTracksMixin(BaseClass) { + class HlsJsMediaMediaTracks extends BaseClass { + #levelIdMap = /* @__PURE__ */ new Map(); + #currentVideoTrack = null; + constructor(...args) { + super(...args); + const { engine } = this; + if (!engine) return; + engine.on(Hls.Events.MANIFEST_PARSED, this.#onManifestParsed); + engine.on(Hls.Events.AUDIO_TRACKS_UPDATED, this.#onAudioTracksUpdated); + engine.on(Hls.Events.AUDIO_TRACK_SWITCHING, this.#onAudioTrackSwitched); + engine.on(Hls.Events.AUDIO_TRACK_SWITCHED, this.#onAudioTrackSwitched); + engine.on(Hls.Events.LEVELS_UPDATED, this.#onLevelsUpdated); + engine.on(Hls.Events.LEVEL_SWITCHED, this.#onLevelSwitched); + engine.once(Hls.Events.DESTROYING, this.#teardown); + this.audioTracks.addEventListener("change", this.#switchAudioTrack); + this.videoRenditions.addEventListener("change", this.#switchRendition); + } + #onManifestParsed = (_event, data) => { + this.#removeAllMediaTracks(); + this.#levelIdMap.clear(); + const videoTrack = this.addVideoTrack("main"); + this.#currentVideoTrack = videoTrack; + videoTrack.selected = true; + for (const [id, level] of data.levels.entries()) { + const rendition = videoTrack.addRendition(level.url[0] ?? "", level.width, level.height, level.videoCodec, level.bitrate); + this.#levelIdMap.set(getLevelKey(level), `${id}`); + rendition.id = `${id}`; + } + }; + #onAudioTracksUpdated = (_event, data) => { + if (this.#audioTracksMatch(data.audioTracks)) return; + this.#removeAudioTracks(); + for (const hlsAudioTrack of data.audioTracks) { + const kind = hlsAudioTrack.default ? "main" : "alternative"; + const audioTrack = this.addAudioTrack(kind, hlsAudioTrack.name, hlsAudioTrack.lang); + audioTrack.id = `${hlsAudioTrack.id}`; + } + }; + #onAudioTrackSwitched = (_event, data) => { + const selectedId = `${data.id}`; + for (const track of this.audioTracks) track.enabled = track.id === selectedId; + }; + #audioTracksMatch(incoming) { + const current = [...this.audioTracks]; + if (current.length !== incoming.length) return false; + return incoming.every((hlsAudioTrack, index) => { + const existing = current[index]; + return existing?.id === `${hlsAudioTrack.id}` && existing.label === (hlsAudioTrack.name ?? "") && existing.language === (hlsAudioTrack.lang ?? ""); + }); + } + #switchAudioTrack = () => { + const { engine } = this; + if (!engine) return; + const enabledTracks = [...this.audioTracks].filter((track) => track.enabled); + const selectedTrack = enabledTracks.find((track) => Number(track.id) !== engine.audioTrack) ?? enabledTracks[0]; + if (!selectedTrack?.id) return; + const audioTrackId = Number(selectedTrack.id); + const availableIds = engine.audioTracks.map((track) => track.id); + if (audioTrackId !== engine.audioTrack && availableIds.includes(audioTrackId)) engine.audioTrack = audioTrackId; + for (const track of enabledTracks) if (track !== selectedTrack) track.enabled = false; + }; + #onLevelsUpdated = (_event, data) => { + if (!this.#currentVideoTrack) return; + const levelIds = data.levels.map((level) => this.#levelIdMap.get(getLevelKey(level))); + for (const rendition of this.videoRenditions) if (rendition.id && !levelIds.includes(rendition.id)) this.#currentVideoTrack.removeRendition(rendition); + }; + #onLevelSwitched = (_event, data) => { + const activeId = `${data.level}`; + for (const rendition of this.videoRenditions) rendition.active = rendition.id === activeId; + }; + #switchRendition = () => { + const { engine } = this; + if (!engine) return; + const level = this.videoRenditions.selectedIndex; + if (level !== engine.nextLevel) engine.nextLevel = level; + }; + #teardown = () => { + const { engine } = this; + engine?.off(Hls.Events.MANIFEST_PARSED, this.#onManifestParsed); + engine?.off(Hls.Events.AUDIO_TRACKS_UPDATED, this.#onAudioTracksUpdated); + engine?.off(Hls.Events.AUDIO_TRACK_SWITCHING, this.#onAudioTrackSwitched); + engine?.off(Hls.Events.AUDIO_TRACK_SWITCHED, this.#onAudioTrackSwitched); + engine?.off(Hls.Events.LEVELS_UPDATED, this.#onLevelsUpdated); + engine?.off(Hls.Events.LEVEL_SWITCHED, this.#onLevelSwitched); + engine?.off(Hls.Events.DESTROYING, this.#teardown); + this.audioTracks.removeEventListener("change", this.#switchAudioTrack); + this.videoRenditions.removeEventListener("change", this.#switchRendition); + this.#removeAllMediaTracks(); + this.#levelIdMap.clear(); + this.#currentVideoTrack = null; + }; + #removeAllMediaTracks() { + this.#removeVideoTracks(); + this.#removeAudioTracks(); + } + #removeVideoTracks() { + for (const videoTrack of this.videoTracks) this.removeVideoTrack(videoTrack); + } + #removeAudioTracks() { + for (const audioTrack of this.audioTracks) this.removeAudioTrack(audioTrack); + } + } + return HlsJsMediaMediaTracks; +} +//#endregion +export { HlsJsMediaMediaTracksMixin }; + +//# sourceMappingURL=media-tracks.js.map \ No newline at end of file diff --git a/dist/default/dom/hls-js/media-tracks.js.map b/dist/default/dom/hls-js/media-tracks.js.map new file mode 100644 index 00000000..9bd2ae3c --- /dev/null +++ b/dist/default/dom/hls-js/media-tracks.js.map @@ -0,0 +1 @@ +{"version":3,"file":"media-tracks.js","names":["#onManifestParsed","#onAudioTracksUpdated","#onAudioTrackSwitched","#onLevelsUpdated","#onLevelSwitched","#teardown","#switchAudioTrack","#switchRendition","#removeAllMediaTracks","#levelIdMap","#currentVideoTrack","#audioTracksMatch","#removeAudioTracks","#removeVideoTracks"],"sources":["../../../../src/dom/hls-js/media-tracks.ts"],"sourcesContent":["import type { Constructor } from '@videojs/utils/types';\nimport Hls from 'hls.js';\nimport type {\n MediaAudioTrackCapability,\n MediaVideoRenditionCapability,\n MediaVideoTrackCapability,\n VideoTrackLike,\n} from '../../core/types';\nimport type { HlsEngineHost } from './types';\n\ntype MediaTracksHost = HlsEngineHost &\n MediaVideoTrackCapability &\n MediaAudioTrackCapability &\n MediaVideoRenditionCapability;\n\ntype HlsJsMediaTrackLevel = {\n url: string[];\n width?: number | undefined;\n height?: number | undefined;\n videoCodec?: string | undefined;\n bitrate?: number | undefined;\n};\n\ntype HlsJsMediaAudioTrack = {\n id: number;\n default?: boolean | undefined;\n name?: string | undefined;\n lang?: string | undefined;\n};\n\n// hls.js may rebuild its levels array with new object references between\n// events, so key levels by their identifying attributes instead of identity.\nfunction getLevelKey(level: HlsJsMediaTrackLevel): string {\n return `${level.url[0] ?? ''}|${level.width}x${level.height}|${level.videoCodec}|${level.bitrate}`;\n}\n\n/**\n * Mirrors hls.js manifest levels and alternate audio into the media element's\n * `videoRenditions` / `audioTracks` lists, and wires user selection back to\n * `engine.nextLevel` and `engine.audioTrack`.\n *\n * Requires the media-tracks mixin (track-list infrastructure) to be applied\n * earlier in the chain so the host exposes `addVideoTrack`, `videoRenditions`,\n * and friends.\n */\nexport function HlsJsMediaMediaTracksMixin>(BaseClass: Base) {\n class HlsJsMediaMediaTracks extends (BaseClass as Constructor) {\n #levelIdMap = new Map();\n #currentVideoTrack: VideoTrackLike | null = null;\n\n constructor(...args: any[]) {\n super(...args);\n\n const { engine } = this;\n if (!engine) return;\n\n engine.on(Hls.Events.MANIFEST_PARSED, this.#onManifestParsed);\n engine.on(Hls.Events.AUDIO_TRACKS_UPDATED, this.#onAudioTracksUpdated);\n engine.on(Hls.Events.AUDIO_TRACK_SWITCHING, this.#onAudioTrackSwitched);\n engine.on(Hls.Events.AUDIO_TRACK_SWITCHED, this.#onAudioTrackSwitched);\n engine.on(Hls.Events.LEVELS_UPDATED, this.#onLevelsUpdated);\n engine.on(Hls.Events.LEVEL_SWITCHED, this.#onLevelSwitched);\n engine.once(Hls.Events.DESTROYING, this.#teardown);\n\n this.audioTracks.addEventListener('change', this.#switchAudioTrack);\n this.videoRenditions.addEventListener('change', this.#switchRendition);\n }\n\n #onManifestParsed = (_event: string, data: { levels: HlsJsMediaTrackLevel[] }) => {\n this.#removeAllMediaTracks();\n this.#levelIdMap.clear();\n\n const videoTrack = this.addVideoTrack('main');\n this.#currentVideoTrack = videoTrack;\n videoTrack.selected = true;\n\n for (const [id, level] of data.levels.entries()) {\n const rendition = videoTrack.addRendition(\n level.url[0] ?? '',\n level.width,\n level.height,\n level.videoCodec,\n level.bitrate\n );\n\n this.#levelIdMap.set(getLevelKey(level), `${id}`);\n rendition.id = `${id}`;\n }\n };\n\n #onAudioTracksUpdated = (_event: string, data: { audioTracks: HlsJsMediaAudioTrack[] }) => {\n if (this.#audioTracksMatch(data.audioTracks)) return;\n\n this.#removeAudioTracks();\n\n for (const hlsAudioTrack of data.audioTracks) {\n const kind = hlsAudioTrack.default ? 'main' : 'alternative';\n const audioTrack = this.addAudioTrack(kind, hlsAudioTrack.name, hlsAudioTrack.lang);\n audioTrack.id = `${hlsAudioTrack.id}`;\n }\n };\n\n #onAudioTrackSwitched = (_event: string, data: { id: number }) => {\n const selectedId = `${data.id}`;\n for (const track of this.audioTracks) {\n track.enabled = track.id === selectedId;\n }\n };\n\n #audioTracksMatch(incoming: HlsJsMediaAudioTrack[]): boolean {\n const current = [...this.audioTracks];\n if (current.length !== incoming.length) return false;\n\n return incoming.every((hlsAudioTrack, index) => {\n const existing = current[index];\n return (\n existing?.id === `${hlsAudioTrack.id}` &&\n existing.label === (hlsAudioTrack.name ?? '') &&\n existing.language === (hlsAudioTrack.lang ?? '')\n );\n });\n }\n\n #switchAudioTrack = () => {\n const { engine } = this;\n if (!engine) return;\n\n // `enabled` is not exclusive like video `selected`, so prefer a newly\n // enabled track over the one that is already playing.\n const enabledTracks = [...this.audioTracks].filter((track) => track.enabled);\n const selectedTrack = enabledTracks.find((track) => Number(track.id) !== engine.audioTrack) ?? enabledTracks[0];\n if (!selectedTrack?.id) return;\n\n const audioTrackId = Number(selectedTrack.id);\n const availableIds = engine.audioTracks.map((track) => track.id);\n\n if (audioTrackId !== engine.audioTrack && availableIds.includes(audioTrackId)) {\n engine.audioTrack = audioTrackId;\n }\n\n // Disable the rest so future change events resolve unambiguously.\n for (const track of enabledTracks) {\n if (track !== selectedTrack) track.enabled = false;\n }\n };\n\n #onLevelsUpdated = (_event: string, data: { levels: HlsJsMediaTrackLevel[] }) => {\n if (!this.#currentVideoTrack) return;\n\n const levelIds = data.levels.map((level) => this.#levelIdMap.get(getLevelKey(level)));\n\n for (const rendition of this.videoRenditions) {\n if (rendition.id && !levelIds.includes(rendition.id)) {\n this.#currentVideoTrack.removeRendition(rendition);\n }\n }\n };\n\n #onLevelSwitched = (_event: string, data: { level: number }) => {\n const activeId = `${data.level}`;\n\n for (const rendition of this.videoRenditions) {\n rendition.active = rendition.id === activeId;\n }\n };\n\n #switchRendition = () => {\n const { engine } = this;\n if (!engine) return;\n\n const level = this.videoRenditions.selectedIndex;\n if (level !== engine.nextLevel) engine.nextLevel = level;\n };\n\n #teardown = () => {\n const { engine } = this;\n\n engine?.off(Hls.Events.MANIFEST_PARSED, this.#onManifestParsed);\n engine?.off(Hls.Events.AUDIO_TRACKS_UPDATED, this.#onAudioTracksUpdated);\n engine?.off(Hls.Events.AUDIO_TRACK_SWITCHING, this.#onAudioTrackSwitched);\n engine?.off(Hls.Events.AUDIO_TRACK_SWITCHED, this.#onAudioTrackSwitched);\n engine?.off(Hls.Events.LEVELS_UPDATED, this.#onLevelsUpdated);\n engine?.off(Hls.Events.LEVEL_SWITCHED, this.#onLevelSwitched);\n engine?.off(Hls.Events.DESTROYING, this.#teardown);\n\n this.audioTracks.removeEventListener('change', this.#switchAudioTrack);\n this.videoRenditions.removeEventListener('change', this.#switchRendition);\n\n this.#removeAllMediaTracks();\n this.#levelIdMap.clear();\n this.#currentVideoTrack = null;\n };\n\n #removeAllMediaTracks() {\n this.#removeVideoTracks();\n this.#removeAudioTracks();\n }\n\n #removeVideoTracks() {\n for (const videoTrack of this.videoTracks) {\n this.removeVideoTrack(videoTrack);\n }\n }\n\n #removeAudioTracks() {\n for (const audioTrack of this.audioTracks) {\n this.removeAudioTrack(audioTrack);\n }\n }\n }\n\n return HlsJsMediaMediaTracks as unknown as Base;\n}\n"],"mappings":"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"} \ No newline at end of file diff --git a/dist/default/dom/hls-js/media.js b/dist/default/dom/hls-js/media.js new file mode 100644 index 00000000..4a7bcee4 --- /dev/null +++ b/dist/default/dom/hls-js/media.js @@ -0,0 +1,200 @@ +import { MediaStreamTypes } from "../../core/types.js"; +import { HTMLVideoElementHost } from "../video-host/video-host.js"; +import { bridgeEvents } from "../../core/bridge-events.js"; +import { NativeHlsMedia } from "../native-hls/media.js"; +import { HlsJsOnlyMedia } from "./hls-js-only.js"; +import { shallowEqual } from "@videojs/utils/object"; +import Hls$1 from "hls.js"; +//#region src/dom/hls-js/media.ts +const PlaybackTypes = { + MSE: "mse", + NATIVE: "native" +}; +const ContentTypes = { + M3U8: "application/vnd.apple.mpegurl", + MP4: "video/mp4" +}; +const StreamTypes = MediaStreamTypes; +const hlsMediaDefaultProps = { + src: "", + preload: "metadata", + streamType: MediaStreamTypes.UNKNOWN, + config: {} +}; +var HlsMediaEvent = class extends Event {}; +/** +* @fires streamtypechange - Fired when the detected stream type changes. Read `streamType` for the new value. +* @fires targetlivewindowchange - Fired when the target live window changes. Read `targetLiveWindow` for the new value. +*/ +var HlsJsMedia = class extends HTMLVideoElementHost { + #delegate = null; + #mediaElement = null; + #src = hlsMediaDefaultProps.src; + #preload = hlsMediaDefaultProps.preload; + #streamType = hlsMediaDefaultProps.streamType; + #isUserStreamType = false; + #loadRequested; + #prevEngineConfigKey; + constructor() { + super(); + this.addEventListener("loadstart", this.#stopTargetLoadStartEvent); + } + attach(target) { + this.#mediaElement = target; + super.attach(target); + this.#delegate?.attach(target); + } + detach() { + this.#delegate?.detach(); + super.detach(); + this.#mediaElement = null; + } + destroy() { + this.detach(); + this.#engineDestroy(); + super.destroy(); + this.removeEventListener("loadstart", this.#stopTargetLoadStartEvent); + } + /** + * Underlying playback engine — the hls.js `Hls` instance when playing via + * MSE, otherwise `null`. An advanced escape hatch for direct engine access; + * normal playback is driven through this element's own properties and methods. + */ + get engine() { + return this.#delegate?.engine ?? null; + } + /** + * Playback configuration: a preferred playback path, an explicit content + * type, and options forwarded to hls.js. Reassigning reloads the engine when + * an engine-relevant option changes. + */ + get config() { + return super.config; + } + set config(config) { + super.config = config; + if (this.#shouldEngineUpdate(this.#engineConfigKey())) this.#requestLoad(); + } + get error() { + return this.#delegate?.error ?? null; + } + /** Populated only while the hls.js (MSE) engine is active; otherwise `undefined`. */ + get videoTracks() { + return this.#delegate instanceof HlsJsOnlyMedia ? this.#delegate.videoTracks : void 0; + } + /** Populated only while the hls.js (MSE) engine is active; otherwise `undefined`. */ + get audioTracks() { + return this.#delegate instanceof HlsJsOnlyMedia ? this.#delegate.audioTracks : void 0; + } + /** Selectable quality levels, populated only while the hls.js (MSE) engine is active; otherwise `undefined`. */ + get videoRenditions() { + return this.#delegate instanceof HlsJsOnlyMedia ? this.#delegate.videoRenditions : void 0; + } + /** Selectable audio variants, populated only while the hls.js (MSE) engine is active; otherwise `undefined`. */ + get audioRenditions() { + return this.#delegate instanceof HlsJsOnlyMedia ? this.#delegate.audioRenditions : void 0; + } + get src() { + return this.#src; + } + set src(src) { + this.#src = src; + this.#requestLoad(); + } + /** Preload type (`'none'` / `'metadata'` / `'auto'`). */ + get preload() { + return this.#preload; + } + set preload(value) { + this.#preload = value; + if (this.#delegate) this.#delegate.preload = value; + } + /** Current stream type (`'on-demand'` / `'live'` / `'unknown'`). */ + get streamType() { + return this.#delegate?.streamType ?? this.#streamType; + } + set streamType(value) { + this.#isUserStreamType = value !== StreamTypes.UNKNOWN; + if (this.#delegate) { + this.#delegate.streamType = value; + this.#streamType = this.#delegate.streamType; + return; + } + if (this.#streamType === value) return; + this.#streamType = value; + this.dispatchEvent(new HlsMediaEvent("streamtypechange")); + } + /** + * Presentation time marking the start of the Live Edge Window. + * + * Derived from the delegate on every read; `NaN` when no delegate is + * attached or the stream is not live. + */ + get liveEdgeStart() { + return this.#delegate?.liveEdgeStart ?? NaN; + } + /** + * Seekable range size for live content. `0` for standard live, `Infinity` + * for DVR, `NaN` for on-demand or unknown. Fires `targetlivewindowchange` + * when the value changes (bridged from the delegate). + */ + get targetLiveWindow() { + return this.#delegate?.targetLiveWindow ?? NaN; + } + async load() { + this.#loadRequested = null; + if (this.remote.state === "connected") { + this.dispatchEvent(new HlsMediaEvent("loadstart")); + return super.load(); + } + if (this.#shouldEngineUpdate(this.#engineConfigKey())) { + this.#engineDestroy(); + this.#prevEngineConfigKey = this.#engineConfigKey(); + const contentType = this.config.contentType ?? inferContentType(this.#src); + const useMse = Hls$1.isSupported() && contentType === ContentTypes.M3U8 && this.config.preferPlayback !== PlaybackTypes.NATIVE; + this.#delegate = useMse ? new HlsJsOnlyMedia({ config: { ...this.config?.hlsJs } }) : new NativeHlsMedia(); + bridgeEvents(this.#delegate, this); + if (this.#isUserStreamType) this.#delegate.streamType = this.#streamType; + this.#delegate.preload = this.preload; + if (this.#mediaElement) this.#delegate.attach(this.#mediaElement); + } + if (this.#delegate) { + this.dispatchEvent(new HlsMediaEvent("loadstart")); + this.#delegate.src = this.#src; + } + } + #stopTargetLoadStartEvent = (event) => { + if (!(event instanceof HlsMediaEvent)) event.stopImmediatePropagation(); + }; + async #requestLoad() { + if (this.#loadRequested) return; + await (this.#loadRequested = Promise.resolve()); + this.#loadRequested = null; + this.load(); + } + #shouldEngineUpdate(nextEngineConfigKey) { + return !shallowEqual(this.#prevEngineConfigKey, nextEngineConfigKey); + } + #engineConfigKey() { + return { + ...this.config.hlsJs, + preferPlayback: this.config.preferPlayback, + contentType: this.config.contentType ?? inferContentType(this.#src) + }; + } + #engineDestroy() { + this.#delegate?.destroy(); + this.#delegate = null; + this.#prevEngineConfigKey = null; + this.#loadRequested = null; + if (!this.#isUserStreamType) this.#streamType = StreamTypes.UNKNOWN; + } +}; +function inferContentType(src) { + if ((src.split(/[?#]/)[0] ?? "").endsWith(".mp4")) return ContentTypes.MP4; + return ContentTypes.M3U8; +} +//#endregion +export { ContentTypes, Hls$1 as Hls, HlsJsMedia, PlaybackTypes, StreamTypes, hlsMediaDefaultProps }; + +//# sourceMappingURL=media.js.map \ No newline at end of file diff --git a/dist/default/dom/hls-js/media.js.map b/dist/default/dom/hls-js/media.js.map new file mode 100644 index 00000000..c1ca902f --- /dev/null +++ b/dist/default/dom/hls-js/media.js.map @@ -0,0 +1 @@ +{"version":3,"file":"media.js","names":["#stopTargetLoadStartEvent","#mediaElement","#delegate","#engineDestroy","#shouldEngineUpdate","#engineConfigKey","#requestLoad","#src","#preload","#streamType","#isUserStreamType","#loadRequested","#prevEngineConfigKey","Hls"],"sources":["../../../../src/dom/hls-js/media.ts"],"sourcesContent":["import { shallowEqual } from '@videojs/utils/object';\nimport Hls, { type HlsConfig as HlsJsConfig } from 'hls.js';\nimport { bridgeEvents } from '../../core/bridge-events';\nimport { type MediaStreamType, MediaStreamTypes } from '../../core/types';\nimport type { MediaConfig } from '../media-host';\nimport { NativeHlsMedia } from '../native-hls';\nimport { HTMLVideoElementHost } from '../video-host';\nimport { HlsJsOnlyMedia } from './hls-js-only';\n\nexport type PreloadType = '' | 'none' | 'metadata' | 'auto';\n\nexport { Hls };\n\nexport type PlaybackType = (typeof PlaybackTypes)[keyof typeof PlaybackTypes];\nexport type SourceType = (typeof ContentTypes)[keyof typeof ContentTypes];\nexport type StreamType = MediaStreamType;\n\nexport const PlaybackTypes = {\n MSE: 'mse',\n NATIVE: 'native',\n};\n\nexport const ContentTypes = {\n M3U8: 'application/vnd.apple.mpegurl',\n MP4: 'video/mp4',\n};\n\nexport const StreamTypes = MediaStreamTypes;\n\nexport interface HlsMediaProps {\n src: string;\n preload: PreloadType;\n streamType: StreamType;\n config?: HlsMediaConfig;\n}\n\nexport interface HlsMediaConfig extends MediaConfig {\n preferPlayback?: PlaybackType | undefined;\n contentType?: SourceType | undefined;\n hlsJs?: Partial;\n}\n\nexport const hlsMediaDefaultProps: HlsMediaProps = {\n src: '',\n preload: 'metadata',\n streamType: MediaStreamTypes.UNKNOWN,\n config: {},\n};\n\nclass HlsMediaEvent extends Event {}\n\n/**\n * @fires streamtypechange - Fired when the detected stream type changes. Read `streamType` for the new value.\n * @fires targetlivewindowchange - Fired when the target live window changes. Read `targetLiveWindow` for the new value.\n */\nexport class HlsJsMedia extends HTMLVideoElementHost implements HlsMediaProps {\n #delegate: HlsJsOnlyMedia | NativeHlsMedia | null = null;\n #mediaElement: HTMLVideoElement | null = null;\n #src = hlsMediaDefaultProps.src;\n #preload = hlsMediaDefaultProps.preload;\n #streamType: StreamType = hlsMediaDefaultProps.streamType;\n #isUserStreamType = false;\n #loadRequested?: Promise | null;\n #prevEngineConfigKey?: Record | null;\n\n constructor() {\n super();\n // Cancel the native loadstart event, it's handled in the load method.\n this.addEventListener('loadstart', this.#stopTargetLoadStartEvent);\n }\n\n attach(target: HTMLVideoElement) {\n this.#mediaElement = target;\n super.attach(target);\n this.#delegate?.attach(target);\n }\n\n detach() {\n this.#delegate?.detach();\n super.detach();\n this.#mediaElement = null;\n }\n\n destroy() {\n this.detach();\n this.#engineDestroy();\n super.destroy();\n this.removeEventListener('loadstart', this.#stopTargetLoadStartEvent);\n }\n\n /**\n * Underlying playback engine — the hls.js `Hls` instance when playing via\n * MSE, otherwise `null`. An advanced escape hatch for direct engine access;\n * normal playback is driven through this element's own properties and methods.\n */\n get engine() {\n return this.#delegate?.engine ?? null;\n }\n\n /**\n * Playback configuration: a preferred playback path, an explicit content\n * type, and options forwarded to hls.js. Reassigning reloads the engine when\n * an engine-relevant option changes.\n */\n get config(): HlsMediaConfig {\n return super.config;\n }\n\n set config(config: HlsMediaConfig) {\n super.config = config;\n if (this.#shouldEngineUpdate(this.#engineConfigKey())) this.#requestLoad();\n }\n\n get error() {\n return this.#delegate?.error ?? null;\n }\n\n /** Populated only while the hls.js (MSE) engine is active; otherwise `undefined`. */\n get videoTracks() {\n return this.#delegate instanceof HlsJsOnlyMedia ? this.#delegate.videoTracks : undefined;\n }\n\n /** Populated only while the hls.js (MSE) engine is active; otherwise `undefined`. */\n get audioTracks() {\n return this.#delegate instanceof HlsJsOnlyMedia ? this.#delegate.audioTracks : undefined;\n }\n\n /** Selectable quality levels, populated only while the hls.js (MSE) engine is active; otherwise `undefined`. */\n get videoRenditions() {\n return this.#delegate instanceof HlsJsOnlyMedia ? this.#delegate.videoRenditions : undefined;\n }\n\n /** Selectable audio variants, populated only while the hls.js (MSE) engine is active; otherwise `undefined`. */\n get audioRenditions() {\n return this.#delegate instanceof HlsJsOnlyMedia ? this.#delegate.audioRenditions : undefined;\n }\n\n get src() {\n return this.#src;\n }\n\n set src(src: string) {\n this.#src = src;\n this.#requestLoad();\n }\n\n /** Preload type (`'none'` / `'metadata'` / `'auto'`). */\n get preload() {\n return this.#preload;\n }\n\n set preload(value) {\n this.#preload = value;\n if (this.#delegate) {\n this.#delegate.preload = value;\n }\n }\n\n /** Current stream type (`'on-demand'` / `'live'` / `'unknown'`). */\n get streamType(): StreamType {\n return this.#delegate?.streamType ?? this.#streamType;\n }\n\n set streamType(value: StreamType) {\n this.#isUserStreamType = value !== StreamTypes.UNKNOWN;\n\n if (this.#delegate) {\n this.#delegate.streamType = value;\n this.#streamType = this.#delegate.streamType;\n return;\n }\n\n if (this.#streamType === value) return;\n this.#streamType = value;\n this.dispatchEvent(new HlsMediaEvent('streamtypechange'));\n }\n\n /**\n * Presentation time marking the start of the Live Edge Window.\n *\n * Derived from the delegate on every read; `NaN` when no delegate is\n * attached or the stream is not live.\n */\n get liveEdgeStart() {\n return this.#delegate?.liveEdgeStart ?? Number.NaN;\n }\n\n /**\n * Seekable range size for live content. `0` for standard live, `Infinity`\n * for DVR, `NaN` for on-demand or unknown. Fires `targetlivewindowchange`\n * when the value changes (bridged from the delegate).\n */\n get targetLiveWindow() {\n return this.#delegate?.targetLiveWindow ?? Number.NaN;\n }\n\n async load() {\n this.#loadRequested = null;\n\n if (this.remote.state === 'connected') {\n this.dispatchEvent(new HlsMediaEvent('loadstart'));\n return super.load();\n }\n\n if (this.#shouldEngineUpdate(this.#engineConfigKey())) {\n this.#engineDestroy();\n this.#prevEngineConfigKey = this.#engineConfigKey();\n\n const contentType = this.config.contentType ?? inferContentType(this.#src);\n const useMse =\n Hls.isSupported() && contentType === ContentTypes.M3U8 && this.config.preferPlayback !== PlaybackTypes.NATIVE;\n\n this.#delegate = useMse ? new HlsJsOnlyMedia({ config: { ...this.config?.hlsJs } }) : new NativeHlsMedia();\n\n bridgeEvents(this.#delegate, this);\n\n // Apply user `streamType` before `attach()` so native delegates do not run\n // synchronous duration-based detection first and emit a transient value.\n if (this.#isUserStreamType) {\n this.#delegate.streamType = this.#streamType;\n }\n\n this.#delegate.preload = this.preload;\n\n if (this.#mediaElement) {\n this.#delegate.attach(this.#mediaElement);\n }\n }\n\n if (this.#delegate) {\n this.dispatchEvent(new HlsMediaEvent('loadstart'));\n this.#delegate.src = this.#src;\n }\n }\n\n #stopTargetLoadStartEvent = (event: Event) => {\n if (!(event instanceof HlsMediaEvent)) event.stopImmediatePropagation();\n };\n\n async #requestLoad() {\n if (this.#loadRequested) return;\n await (this.#loadRequested = Promise.resolve());\n this.#loadRequested = null;\n this.load();\n }\n\n #shouldEngineUpdate(nextEngineConfigKey: Record) {\n return !shallowEqual(this.#prevEngineConfigKey, nextEngineConfigKey);\n }\n\n #engineConfigKey() {\n return {\n ...this.config.hlsJs,\n preferPlayback: this.config.preferPlayback,\n contentType: this.config.contentType ?? inferContentType(this.#src),\n };\n }\n\n #engineDestroy() {\n this.#delegate?.destroy();\n this.#delegate = null;\n this.#prevEngineConfigKey = null;\n this.#loadRequested = null;\n // Delegate teardown already emits `streamtypechange` (bridged); only sync cache.\n if (!this.#isUserStreamType) this.#streamType = StreamTypes.UNKNOWN;\n }\n}\n\nfunction inferContentType(src: string): SourceType {\n const path = src.split(/[?#]/)[0] ?? '';\n if (path.endsWith('.mp4')) return ContentTypes.MP4;\n return ContentTypes.M3U8;\n}\n"],"mappings":"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"} \ No newline at end of file diff --git a/dist/default/dom/hls-js/metadata-tracks.js b/dist/default/dom/hls-js/metadata-tracks.js new file mode 100644 index 00000000..c58311e8 --- /dev/null +++ b/dist/default/dom/hls-js/metadata-tracks.js @@ -0,0 +1,42 @@ +import Hls from "hls.js"; +//#region src/dom/hls-js/metadata-tracks.ts +/** +* Ensures user-authored metadata and chapters `` elements stay loaded +* when hls.js is active. +* +* hls.js forcibly clears all cues from text tracks on manifest loads and media +* attaches. This mixin re-enables those tracks by forcing `mode = 'hidden'` +* and reloading the track source when cues have been wiped. +*/ +function HlsJsMediaMetadataTracksMixin(BaseClass) { + class HlsJsMediaMetadataTracks extends BaseClass { + constructor(...args) { + super(...args); + this.engine?.on(Hls.Events.MANIFEST_LOADED, () => this.#forceHiddenTracks()); + this.engine?.on(Hls.Events.MEDIA_ATTACHED, () => this.#forceHiddenTracks()); + } + #forceHiddenTracks() { + const target = this.target; + if (!target) return; + [...target.textTracks].forEach((track) => { + if (!(track.kind === "metadata" || track.kind === "chapters")) return; + let selector = "track"; + if (track.kind) selector += `[kind="${track.kind}"]`; + if (track.label) selector += `[label="${track.label}"]`; + const trackEl = target.querySelector(selector); + if (!trackEl) return; + if ((trackEl.getAttribute("src") ?? "") && trackEl.readyState === 2 && !track.cues?.length) { + const clonedTrackEl = trackEl.cloneNode(); + target.replaceChild(clonedTrackEl, trackEl); + } + const currentTrackEl = target.querySelector(selector); + if (currentTrackEl?.default && currentTrackEl.track.mode !== "hidden") currentTrackEl.track.mode = "hidden"; + }); + } + } + return HlsJsMediaMetadataTracks; +} +//#endregion +export { HlsJsMediaMetadataTracksMixin }; + +//# sourceMappingURL=metadata-tracks.js.map \ No newline at end of file diff --git a/dist/default/dom/hls-js/metadata-tracks.js.map b/dist/default/dom/hls-js/metadata-tracks.js.map new file mode 100644 index 00000000..dcec434d --- /dev/null +++ b/dist/default/dom/hls-js/metadata-tracks.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metadata-tracks.js","names":["#forceHiddenTracks"],"sources":["../../../../src/dom/hls-js/metadata-tracks.ts"],"sourcesContent":["import type { Constructor } from '@videojs/utils/types';\nimport Hls from 'hls.js';\nimport type { HlsEngineHost } from './types';\n\n/**\n * Ensures user-authored metadata and chapters `` elements stay loaded\n * when hls.js is active.\n *\n * hls.js forcibly clears all cues from text tracks on manifest loads and media\n * attaches. This mixin re-enables those tracks by forcing `mode = 'hidden'`\n * and reloading the track source when cues have been wiped.\n */\nexport function HlsJsMediaMetadataTracksMixin>(BaseClass: Base) {\n class HlsJsMediaMetadataTracks extends (BaseClass as Constructor) {\n constructor(...args: any[]) {\n super(...args);\n\n // Watch out here, AFTER the manifest is loaded!\n this.engine?.on(Hls.Events.MANIFEST_LOADED, () => this.#forceHiddenTracks());\n this.engine?.on(Hls.Events.MEDIA_ATTACHED, () => this.#forceHiddenTracks());\n }\n\n #forceHiddenTracks(): void {\n // The hls.js delegate always binds to the real `