@sveltesentio/media 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0](https://github.com/golusoris/sveltesentio/compare/media-v0.1.0...media-v0.2.0)
4
+
5
+
6
+ ### Features
7
+
8
+ * **player:** headless HLS source model — `pickRendition` (separate-rendition quality switching with `maxHeight` / `preferCodec`), `buildMediaSessionMetadata`, a pure `playbackReducer` play/pause/quality state machine, and a bring-your-own-`hls.js` `createHlsAttachment` seam ([#67](https://github.com/golusoris/sveltesentio/issues/67), [#68](https://github.com/golusoris/sveltesentio/issues/68))
9
+ * **image:** pure responsive-image `buildSrcSet` / `buildSizes` / `buildResponsiveImage` builders with template + query-merge URL strategies ([#67](https://github.com/golusoris/sveltesentio/issues/67))
10
+ * sub-exports `./player` and `./image`; `hls.js` declared as an optional peer (no runtime media deps)
11
+
12
+
13
+ ### Notes
14
+
15
+ * The `vidstack@next` `<Player>` UI shell and the `./carousel` re-export remain follow-throughs; README and AGENTS status reconciled per [#67](https://github.com/golusoris/sveltesentio/issues/67).
16
+
3
17
  ## [0.1.0](https://github.com/golusoris/sveltesentio/compare/media-v0.0.1...media-v0.1.0) (2026-06-14)
4
18
 
5
19
 
package/README.md CHANGED
@@ -1,20 +1,69 @@
1
1
  # @sveltesentio/media
2
2
 
3
- > vidstack + HLS.js + embla-carousel wrappers for media server UIs
3
+ > Headless media-player logic + responsive-image builders for media-server UIs
4
4
 
5
5
  Part of the [sveltesentio](https://github.com/golusoris/sveltesentio) composable SvelteKit framework.
6
6
 
7
7
  ## Status
8
8
 
9
- 🚧 Phase 1 stub implementation begins in Phase 2+.
9
+ v0.2.0 headless core landed. The pure logic for HLS rendition selection,
10
+ OS media-session metadata, a play/pause/quality state machine, a
11
+ bring-your-own-`hls.js` attachment seam, and responsive-image `srcset` / `sizes`
12
+ builders all ship and are unit-tested. The full `<Player>` UI shell
13
+ (`vidstack`) and the `./carousel` re-export are tracked as follow-throughs (see
14
+ [AGENTS.md](AGENTS.md)) and are **not** in this release.
10
15
 
11
- ## Installation
16
+ ## Install
12
17
 
13
18
  ```bash
14
19
  pnpm add @sveltesentio/media
20
+ # optional, only if you drive adaptive HLS yourself:
21
+ pnpm add hls.js
15
22
  ```
16
23
 
17
- See the [monorepo README](../../README.md) and [`docs/`](../../docs/) for design principles and usage.
24
+ `hls.js` is an **optional** peer nothing in this package imports it. You
25
+ inject its constructor at the `createHlsAttachment` seam.
26
+
27
+ ## `@sveltesentio/media/player`
28
+
29
+ ```ts
30
+ import {
31
+ pickRendition,
32
+ buildMediaSessionMetadata,
33
+ playbackReducer,
34
+ initialPlaybackState,
35
+ createHlsAttachment,
36
+ } from '@sveltesentio/media/player';
37
+
38
+ // Separate-rendition (un-muxed) quality switching: prefer HEVC, cap at 1080p.
39
+ const best = pickRendition(renditions, { maxHeight: 1080, preferCodec: 'hvc1' });
40
+
41
+ // OS lock-screen / media-keys metadata (pass to `new MediaMetadata(...)`).
42
+ const meta = buildMediaSessionMetadata({ title, artist, artwork });
43
+
44
+ // Pure play/pause/quality machine — invalid transitions are no-ops.
45
+ let state = playbackReducer(initialPlaybackState, { type: 'load' });
46
+
47
+ // Bring-your-own hls.js — no dynamic import, no bundled engine.
48
+ import Hls from 'hls.js';
49
+ const handle = createHlsAttachment(Hls).attach(videoEl, manifestUrl);
50
+ // handle.destroy();
51
+ ```
52
+
53
+ ## `@sveltesentio/media/image`
54
+
55
+ ```ts
56
+ import { buildResponsiveImage } from '@sveltesentio/media/image';
57
+
58
+ const attrs = buildResponsiveImage('/images/poster/{path}', [320, 640, 1280], {
59
+ template: '/images/poster/w{w}/abc.avif',
60
+ sizes: [{ condition: '(min-width: 768px)', size: '33vw' }],
61
+ });
62
+ // → { src, srcset, sizes } — spread onto <img>.
63
+ ```
64
+
65
+ See the [monorepo README](../../README.md) and [`docs/`](../../docs/) for design
66
+ principles.
18
67
 
19
68
  ## License
20
69
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sveltesentio/media",
3
- "version": "0.1.0",
4
- "description": "Vidstack + HLS.js + embla-carousel wrappers, artwork grid, 10-foot UI preset",
3
+ "version": "0.2.0",
4
+ "description": "Headless media player model (HLS rendition picking, media-session, play/pause/quality machine, BYO hls.js seam) + responsive-image srcset/sizes builders",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "sideEffects": false,
@@ -9,20 +9,22 @@
9
9
  ".": {
10
10
  "import": "./src/index.ts",
11
11
  "types": "./src/index.ts"
12
+ },
13
+ "./player": {
14
+ "import": "./src/player.ts",
15
+ "types": "./src/player.ts"
16
+ },
17
+ "./image": {
18
+ "import": "./src/image.ts",
19
+ "types": "./src/image.ts"
12
20
  }
13
21
  },
14
22
  "peerDependencies": {
15
- "vidstack": ">=0.6.0",
16
- "hls.js": ">=1.6.0",
17
- "embla-carousel-svelte": ">=8.0.0",
18
- "svelte": ">=5.0.0"
23
+ "hls.js": ">=1.6.0"
19
24
  },
20
25
  "peerDependenciesMeta": {
21
26
  "hls.js": {
22
27
  "optional": true
23
- },
24
- "embla-carousel-svelte": {
25
- "optional": true
26
28
  }
27
29
  },
28
30
  "devDependencies": {
@@ -32,9 +34,9 @@
32
34
  "keywords": [
33
35
  "sveltesentio",
34
36
  "media",
35
- "vidstack",
36
37
  "hls",
37
- "carousel",
38
+ "srcset",
39
+ "media-session",
38
40
  "10-foot-ui"
39
41
  ],
40
42
  "publishConfig": {
@@ -48,6 +50,6 @@
48
50
  "build": "tsc",
49
51
  "lint": "eslint src/",
50
52
  "typecheck": "tsc --noEmit",
51
- "test": "vitest run --passWithNoTests"
53
+ "test": "vitest run"
52
54
  }
53
55
  }
package/src/image.ts ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Pure responsive-image helpers: `srcset` / `sizes` string builders plus a
3
+ * width-descriptor candidate model. Framework-agnostic — no DOM, no Svelte —
4
+ * so the same logic drives a `<picture>`, an `<img srcset>`, or a server-side
5
+ * `Link: rel=preload` header.
6
+ */
7
+
8
+ /** A `src`-templating function: maps a target pixel width to a URL. */
9
+ export type SrcWidthTemplate = (width: number) => string;
10
+
11
+ /** One `srcset` candidate: a URL paired with its intrinsic pixel width. */
12
+ export interface SrcSetCandidate {
13
+ readonly url: string;
14
+ readonly width: number;
15
+ }
16
+
17
+ export interface SrcSetOptions {
18
+ /**
19
+ * How to derive each candidate URL from a width. Either a template function
20
+ * or a token-substitution string containing `{w}` (replaced with the width).
21
+ * When omitted, `?w=<width>` is appended to `src` (or merged into an
22
+ * existing query string).
23
+ */
24
+ readonly template?: SrcWidthTemplate | string;
25
+ }
26
+
27
+ const WIDTH_TOKEN = /\{w\}/g;
28
+
29
+ /** De-duplicate, drop non-positive/non-finite entries, and sort ascending. */
30
+ function normaliseWidths(widths: readonly number[]): number[] {
31
+ const seen = new Set<number>();
32
+ for (const w of widths) {
33
+ if (Number.isFinite(w) && w > 0) seen.add(Math.round(w));
34
+ }
35
+ return [...seen].sort((a, b) => a - b);
36
+ }
37
+
38
+ /** Append/merge a `w` query param onto a URL without a URL parser dependency. */
39
+ function appendWidthQuery(src: string, width: number): string {
40
+ const [base, hash = ''] = src.split('#', 2);
41
+ const safeBase = base ?? src;
42
+ const sep = safeBase.includes('?') ? '&' : '?';
43
+ const suffix = hash ? `#${hash}` : '';
44
+ return `${safeBase}${sep}w=${width}${suffix}`;
45
+ }
46
+
47
+ function resolveTemplate(
48
+ src: string,
49
+ template: SrcWidthTemplate | string | undefined,
50
+ ): SrcWidthTemplate {
51
+ if (typeof template === 'function') return template;
52
+ if (typeof template === 'string') {
53
+ return (width) => template.replace(WIDTH_TOKEN, String(width));
54
+ }
55
+ return (width) => appendWidthQuery(src, width);
56
+ }
57
+
58
+ /**
59
+ * Build the structured candidate list backing a `srcset`. Widths are
60
+ * normalised (de-duped, positive, integer, ascending) so callers can pass a
61
+ * raw breakpoint list in any order.
62
+ */
63
+ export function buildSrcSetCandidates(
64
+ src: string,
65
+ widths: readonly number[],
66
+ options: SrcSetOptions = {},
67
+ ): SrcSetCandidate[] {
68
+ const resolved = resolveTemplate(src, options.template);
69
+ return normaliseWidths(widths).map((width) => ({
70
+ url: resolved(width),
71
+ width,
72
+ }));
73
+ }
74
+
75
+ /**
76
+ * Build a `srcset` attribute string with `w` width descriptors, e.g.
77
+ * `"/img?w=320 320w, /img?w=640 640w"`. Returns `""` for an empty width list.
78
+ */
79
+ export function buildSrcSet(
80
+ src: string,
81
+ widths: readonly number[],
82
+ options: SrcSetOptions = {},
83
+ ): string {
84
+ return buildSrcSetCandidates(src, widths, options)
85
+ .map((c) => `${c.url} ${c.width}w`)
86
+ .join(', ');
87
+ }
88
+
89
+ /** One `sizes` media-condition / length pair. */
90
+ export interface SizesRule {
91
+ /** A media condition, e.g. `"(min-width: 768px)"`. */
92
+ readonly condition: string;
93
+ /** A CSS length, e.g. `"50vw"` or `"600px"`. */
94
+ readonly size: string;
95
+ }
96
+
97
+ export interface BuildSizesOptions {
98
+ /**
99
+ * The trailing default length applied when no condition matches. Defaults to
100
+ * `"100vw"` — the responsive-image-correct fallback for a fluid layout.
101
+ */
102
+ readonly fallback?: string;
103
+ }
104
+
105
+ /**
106
+ * Build a `sizes` attribute string from ordered conditional rules plus a
107
+ * fallback length, e.g. `"(min-width: 768px) 50vw, 100vw"`. Rules are emitted
108
+ * in array order — the browser uses the first matching condition.
109
+ */
110
+ export function buildSizes(
111
+ rules: readonly SizesRule[],
112
+ options: BuildSizesOptions = {},
113
+ ): string {
114
+ const fallback = options.fallback ?? '100vw';
115
+ const conditional = rules.map((r) => `${r.condition} ${r.size}`);
116
+ return [...conditional, fallback].join(', ');
117
+ }
118
+
119
+ export interface ResponsiveImageAttrs {
120
+ readonly src: string;
121
+ readonly srcset: string;
122
+ readonly sizes: string;
123
+ }
124
+
125
+ export interface BuildResponsiveImageOptions extends SrcSetOptions {
126
+ readonly sizes?: readonly SizesRule[];
127
+ readonly sizesFallback?: string;
128
+ /**
129
+ * Which width to use for the legacy `src` fallback attribute. Defaults to
130
+ * the largest provided width so non-`srcset` browsers get full quality.
131
+ */
132
+ readonly fallbackWidth?: number;
133
+ }
134
+
135
+ /**
136
+ * Compose `{ src, srcset, sizes }` ready to spread onto an `<img>`. The `src`
137
+ * fallback targets `fallbackWidth` (default: largest width) so legacy browsers
138
+ * without `srcset` support still receive a sensibly-sized asset.
139
+ */
140
+ export function buildResponsiveImage(
141
+ src: string,
142
+ widths: readonly number[],
143
+ options: BuildResponsiveImageOptions = {},
144
+ ): ResponsiveImageAttrs {
145
+ const candidates = buildSrcSetCandidates(src, widths, options);
146
+ const fallbackWidth =
147
+ options.fallbackWidth ?? candidates.at(-1)?.width;
148
+ const resolved = resolveTemplate(src, options.template);
149
+ const fallbackSrc =
150
+ fallbackWidth === undefined ? src : resolved(fallbackWidth);
151
+ return {
152
+ src: fallbackSrc,
153
+ srcset: candidates.map((c) => `${c.url} ${c.width}w`).join(', '),
154
+ sizes: buildSizes(options.sizes ?? [], {
155
+ ...(options.sizesFallback === undefined
156
+ ? {}
157
+ : { fallback: options.sizesFallback }),
158
+ }),
159
+ };
160
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,37 @@
1
- // @sveltesentio/media — not yet implemented (see .workingdir/PLAN.md)
2
- export {};
1
+ export type {
2
+ HlsRendition,
3
+ PickRenditionOptions,
4
+ MediaSessionArtwork,
5
+ MediaSessionMetadataInit,
6
+ MediaSessionMetadata,
7
+ PlaybackStatus,
8
+ PlaybackEvent,
9
+ PlaybackState,
10
+ HlsLike,
11
+ HlsConstructorLike,
12
+ HlsAttachmentOptions,
13
+ HlsAttachment,
14
+ } from './player.js';
15
+ export {
16
+ pickRendition,
17
+ buildMediaSessionMetadata,
18
+ initialPlaybackState,
19
+ playbackReducer,
20
+ createHlsAttachment,
21
+ } from './player.js';
22
+
23
+ export type {
24
+ SrcWidthTemplate,
25
+ SrcSetCandidate,
26
+ SrcSetOptions,
27
+ SizesRule,
28
+ BuildSizesOptions,
29
+ ResponsiveImageAttrs,
30
+ BuildResponsiveImageOptions,
31
+ } from './image.js';
32
+ export {
33
+ buildSrcSet,
34
+ buildSrcSetCandidates,
35
+ buildSizes,
36
+ buildResponsiveImage,
37
+ } from './image.js';
package/src/player.ts ADDED
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Headless player-source model: rendition selection, OS media-session
3
+ * metadata, a typed play/pause/quality state machine, and a bring-your-own
4
+ * `hls.js` attachment seam. Pure and framework-agnostic — the heavy
5
+ * `<Player>` UI shell (vidstack) is a follow-through and intentionally absent
6
+ * here so this package stays free of runtime media dependencies.
7
+ *
8
+ * @see ADR-0042 (Vidstack `@next` + `hls.js`) — issues #67 / #68.
9
+ */
10
+
11
+ /**
12
+ * One HLS variant. Models separate-rendition (un-muxed) streaming as used by
13
+ * media servers: a rendition may carry video, audio, or both, so quality and
14
+ * audio-track switching are independent concerns.
15
+ */
16
+ export interface HlsRendition {
17
+ /** Stable identifier (e.g. the HLS level index or a server stream id). */
18
+ readonly id: string;
19
+ /** Vertical resolution in pixels; omit for audio-only renditions. */
20
+ readonly height?: number;
21
+ /** Average/peak bitrate in bits per second, if known. */
22
+ readonly bitrate?: number;
23
+ /** RFC 6381 codec string, e.g. `"hvc1.1.6.L93.B0"` or `"avc1.640028"`. */
24
+ readonly codec?: string;
25
+ /** BCP 47 language tag for audio renditions, e.g. `"en"`. */
26
+ readonly language?: string;
27
+ /** Whether this is the server-default rendition. */
28
+ readonly default?: boolean;
29
+ }
30
+
31
+ export interface PickRenditionOptions {
32
+ /** Cap selection to renditions at or below this height. */
33
+ readonly maxHeight?: number;
34
+ /**
35
+ * Prefer renditions whose `codec` starts with this prefix (case-insensitive),
36
+ * e.g. `"hvc1"` / `"hev1"` for HEVC, `"avc1"` for H.264. Non-matching
37
+ * renditions remain eligible as a fallback.
38
+ */
39
+ readonly preferCodec?: string;
40
+ }
41
+
42
+ function isAtOrBelowHeight(r: HlsRendition, maxHeight: number | undefined): boolean {
43
+ if (maxHeight === undefined) return true;
44
+ if (r.height === undefined) return true; // audio-only is never excluded by height
45
+ return r.height <= maxHeight;
46
+ }
47
+
48
+ function rank(r: HlsRendition): number {
49
+ if (r.height !== undefined) return r.height * 1_000_000 + (r.bitrate ?? 0);
50
+ return r.bitrate ?? 0;
51
+ }
52
+
53
+ /**
54
+ * Select the best HLS rendition under the given constraints. Picks the highest
55
+ * eligible height (then bitrate). When `preferCodec` is set, codec-matching
56
+ * renditions win over non-matching ones of equal-or-greater rank, so a HEVC
57
+ * preference is honoured without discarding an H.264 fallback. Returns
58
+ * `undefined` only when `renditions` is empty.
59
+ */
60
+ export function pickRendition(
61
+ renditions: readonly HlsRendition[],
62
+ options: PickRenditionOptions = {},
63
+ ): HlsRendition | undefined {
64
+ const { maxHeight, preferCodec } = options;
65
+ const eligible = renditions.filter((r) => isAtOrBelowHeight(r, maxHeight));
66
+ const pool = eligible.length > 0 ? eligible : renditions;
67
+ if (pool.length === 0) return undefined;
68
+
69
+ const prefix = preferCodec?.toLowerCase();
70
+ const matches = (r: HlsRendition): boolean =>
71
+ prefix !== undefined && (r.codec?.toLowerCase().startsWith(prefix) ?? false);
72
+
73
+ let best: HlsRendition | undefined;
74
+ for (const r of pool) {
75
+ if (best === undefined) {
76
+ best = r;
77
+ continue;
78
+ }
79
+ const rMatch = matches(r);
80
+ const bestMatch = matches(best);
81
+ if (rMatch !== bestMatch) {
82
+ if (rMatch) best = r;
83
+ continue;
84
+ }
85
+ if (rank(r) > rank(best)) best = r;
86
+ }
87
+ return best;
88
+ }
89
+
90
+ /** One album-art / poster image for the OS media session. */
91
+ export interface MediaSessionArtwork {
92
+ readonly src: string;
93
+ /** e.g. `"512x512"`. */
94
+ readonly sizes?: string;
95
+ /** e.g. `"image/png"`. */
96
+ readonly type?: string;
97
+ }
98
+
99
+ export interface MediaSessionMetadataInit {
100
+ readonly title: string;
101
+ readonly artist?: string;
102
+ readonly album?: string;
103
+ readonly artwork?: readonly MediaSessionArtwork[];
104
+ }
105
+
106
+ /**
107
+ * Shape compatible with the `MediaMetadataInit` accepted by the browser
108
+ * `navigator.mediaSession.metadata = new MediaMetadata(...)` API. Returned as a
109
+ * plain object so the caller — not this package — owns the DOM boundary.
110
+ */
111
+ export interface MediaSessionMetadata {
112
+ readonly title: string;
113
+ readonly artist: string;
114
+ readonly album: string;
115
+ readonly artwork: readonly MediaSessionArtwork[];
116
+ }
117
+
118
+ /**
119
+ * Build a normalised media-session metadata object for the OS lock-screen /
120
+ * media keys. Empty `artist` / `album` default to `""` (the API's own
121
+ * defaults) so the result can be passed straight to `new MediaMetadata(...)`.
122
+ */
123
+ export function buildMediaSessionMetadata(
124
+ init: MediaSessionMetadataInit,
125
+ ): MediaSessionMetadata {
126
+ return {
127
+ title: init.title,
128
+ artist: init.artist ?? '',
129
+ album: init.album ?? '',
130
+ artwork: init.artwork ?? [],
131
+ };
132
+ }
133
+
134
+ /** Playback lifecycle states for the headless machine. */
135
+ export type PlaybackStatus = 'idle' | 'loading' | 'playing' | 'paused' | 'ended';
136
+
137
+ /** Events the state machine accepts. */
138
+ export type PlaybackEvent =
139
+ | { readonly type: 'load' }
140
+ | { readonly type: 'ready' }
141
+ | { readonly type: 'play' }
142
+ | { readonly type: 'pause' }
143
+ | { readonly type: 'end' }
144
+ | { readonly type: 'selectQuality'; readonly renditionId: string }
145
+ | { readonly type: 'reset' };
146
+
147
+ export interface PlaybackState {
148
+ readonly status: PlaybackStatus;
149
+ /** Currently-selected rendition id, or `null` for automatic (ABR). */
150
+ readonly renditionId: string | null;
151
+ }
152
+
153
+ export const initialPlaybackState: PlaybackState = {
154
+ status: 'idle',
155
+ renditionId: null,
156
+ };
157
+
158
+ /**
159
+ * Pure reducer for the play/pause/quality machine. Invalid transitions (e.g.
160
+ * `play` while `idle`) are no-ops that return the input state unchanged, so the
161
+ * machine never throws on a stray event. Quality selection is orthogonal to the
162
+ * play/pause lifecycle and is accepted in any non-terminal state.
163
+ */
164
+ export function playbackReducer(
165
+ state: PlaybackState,
166
+ event: PlaybackEvent,
167
+ ): PlaybackState {
168
+ switch (event.type) {
169
+ case 'load':
170
+ return state.status === 'idle'
171
+ ? { ...state, status: 'loading' }
172
+ : state;
173
+ case 'ready':
174
+ return state.status === 'loading'
175
+ ? { ...state, status: 'paused' }
176
+ : state;
177
+ case 'play':
178
+ return state.status === 'paused' || state.status === 'ended'
179
+ ? { ...state, status: 'playing' }
180
+ : state;
181
+ case 'pause':
182
+ return state.status === 'playing'
183
+ ? { ...state, status: 'paused' }
184
+ : state;
185
+ case 'end':
186
+ return state.status === 'playing'
187
+ ? { ...state, status: 'ended' }
188
+ : state;
189
+ case 'selectQuality':
190
+ return state.status === 'idle'
191
+ ? state
192
+ : { ...state, renditionId: event.renditionId };
193
+ case 'reset':
194
+ return initialPlaybackState;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Minimal structural view of an `hls.js` instance — only the members the
200
+ * attachment seam touches. Kept local so this package needs no `hls.js`
201
+ * dependency (it is an optional peer).
202
+ */
203
+ export interface HlsLike {
204
+ loadSource(url: string): void;
205
+ attachMedia(media: HTMLMediaElement): void;
206
+ destroy(): void;
207
+ /** Manual quality override; `-1` restores automatic ABR. */
208
+ currentLevel?: number;
209
+ }
210
+
211
+ /** A constructor compatible with `new Hls(config)`. */
212
+ export type HlsConstructorLike = new (config?: unknown) => HlsLike;
213
+
214
+ export interface HlsAttachmentOptions {
215
+ /** Optional `hls.js` config passed straight to its constructor. */
216
+ readonly config?: unknown;
217
+ }
218
+
219
+ export interface HlsAttachment {
220
+ /** Wire an HLS source to a media element; returns a detach/destroy handle. */
221
+ attach(media: HTMLMediaElement, source: string): { destroy(): void };
222
+ }
223
+
224
+ /**
225
+ * Bring-your-own-`hls.js` seam. The caller injects the `hls.js` constructor
226
+ * (so this package neither bundles nor dynamically imports it), and gets back a
227
+ * tiny attachment helper that wires a source to a media element. Downstreams
228
+ * already on raw `hls.js` can adopt this without pulling any UI shell.
229
+ *
230
+ * @example
231
+ * import Hls from 'hls.js';
232
+ * const hls = createHlsAttachment(Hls);
233
+ * const handle = hls.attach(videoEl, manifestUrl);
234
+ * // later: handle.destroy();
235
+ */
236
+ export function createHlsAttachment(
237
+ HlsCtor: HlsConstructorLike,
238
+ options: HlsAttachmentOptions = {},
239
+ ): HlsAttachment {
240
+ return {
241
+ attach(media, source) {
242
+ const instance =
243
+ options.config === undefined
244
+ ? new HlsCtor()
245
+ : new HlsCtor(options.config);
246
+ instance.attachMedia(media);
247
+ instance.loadSource(source);
248
+ return {
249
+ destroy: () => {
250
+ instance.destroy();
251
+ },
252
+ };
253
+ },
254
+ };
255
+ }