@classic-homes/theme-svelte 1.2.3 → 1.3.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/README.md +43 -0
- package/dist/__tests__/a11y/Media.a11y.test.js +145 -0
- package/dist/__tests__/media-components.test.js +206 -0
- package/dist/__tests__/media-hls.test.js +273 -0
- package/dist/__tests__/media-matterport.test.js +296 -0
- package/dist/__tests__/media-players.test.js +244 -0
- package/dist/__tests__/media-scrubber.test.js +44 -0
- package/dist/__tests__/media-utils.test.js +427 -0
- package/dist/__tests__/setup.js +50 -0
- package/dist/lib/components/FileUpload.svelte +221 -4
- package/dist/lib/components/FileUpload.svelte.d.ts +13 -1
- package/dist/lib/components/media/AspectRatio.svelte +47 -0
- package/dist/lib/components/media/AspectRatio.svelte.d.ts +20 -0
- package/dist/lib/components/media/Audio.svelte +247 -0
- package/dist/lib/components/media/Audio.svelte.d.ts +34 -0
- package/dist/lib/components/media/Image.svelte +209 -0
- package/dist/lib/components/media/Image.svelte.d.ts +52 -0
- package/dist/lib/components/media/Lightbox.svelte +449 -0
- package/dist/lib/components/media/Lightbox.svelte.d.ts +33 -0
- package/dist/lib/components/media/MediaCarousel.svelte +359 -0
- package/dist/lib/components/media/MediaCarousel.svelte.d.ts +30 -0
- package/dist/lib/components/media/MediaEmbed.svelte +314 -0
- package/dist/lib/components/media/MediaEmbed.svelte.d.ts +99 -0
- package/dist/lib/components/media/MediaGrid.svelte +327 -0
- package/dist/lib/components/media/MediaGrid.svelte.d.ts +57 -0
- package/dist/lib/components/media/Video.svelte +455 -0
- package/dist/lib/components/media/Video.svelte.d.ts +81 -0
- package/dist/lib/components/media/index.d.ts +14 -0
- package/dist/lib/components/media/index.js +14 -0
- package/dist/lib/components/media/internal/MediaControls.svelte +283 -0
- package/dist/lib/components/media/internal/MediaControls.svelte.d.ts +32 -0
- package/dist/lib/components/media/internal/MediaScrubber.svelte +211 -0
- package/dist/lib/components/media/internal/MediaScrubber.svelte.d.ts +27 -0
- package/dist/lib/components/media/internal/MediaTile.svelte +129 -0
- package/dist/lib/components/media/internal/MediaTile.svelte.d.ts +22 -0
- package/dist/lib/composables/index.d.ts +3 -0
- package/dist/lib/composables/index.js +4 -0
- package/dist/lib/composables/useHlsSource.svelte.d.ts +70 -0
- package/dist/lib/composables/useHlsSource.svelte.js +119 -0
- package/dist/lib/composables/useMediaPlayer.svelte.d.ts +114 -0
- package/dist/lib/composables/useMediaPlayer.svelte.js +422 -0
- package/dist/lib/composables/useMediaZoom.svelte.d.ts +59 -0
- package/dist/lib/composables/useMediaZoom.svelte.js +199 -0
- package/dist/lib/index.d.ts +8 -2
- package/dist/lib/index.js +14 -1
- package/dist/lib/types/components.d.ts +172 -0
- package/dist/lib/utils/hls.d.ts +76 -0
- package/dist/lib/utils/hls.js +83 -0
- package/dist/lib/utils/media.d.ts +379 -0
- package/dist/lib/utils/media.js +715 -0
- package/package.json +6 -2
package/dist/lib/index.d.ts
CHANGED
|
@@ -58,7 +58,9 @@ export { default as Signature } from './components/Signature.svelte';
|
|
|
58
58
|
export { default as DataTable } from './components/DataTable.svelte';
|
|
59
59
|
export { default as Tabs } from './components/Tabs.svelte';
|
|
60
60
|
export { default as TabPanel } from './components/TabPanel.svelte';
|
|
61
|
-
export
|
|
61
|
+
export { AspectRatio, Image, Video, Audio, MediaEmbed, MediaGrid, MediaCarousel, Lightbox, } from './components/media/index.js';
|
|
62
|
+
export type { MediaGridColumns } from './components/media/MediaGrid.svelte';
|
|
63
|
+
export type { DropdownMenuItem, DropdownMenuGroup, SelectOption, SelectGroup, Tab, FileMetadata, DataTableColumn, DataTableFilterFn, DataTableFilterOption, RadioOption, ComboboxOption, MultiSelectOption, SignatureData, SignatureStroke, SignaturePoint, SignatureMetadata, SignatureFont, SignatureValidationResult, MediaKind, MediaEmbedProvider, MediaSource, MediaTextTrack, MediaItemBase, ImageMediaItem, VideoMediaItem, AudioMediaItem, EmbedMediaItem, MediaItem, MediaAspectRatio, MediaFit, CloudflareStreamAccount, MatterportPlayerOptions, } from './types/components.js';
|
|
62
64
|
export { createDataTable, type DataTableOptions, type DataTableRow, type DataTableInstance, type ColumnMeta, type BaseColumnMeta, type DataColumnMeta, type FilterableColumnMeta, type SelectionColumnMeta, type RowSelectionState, type ColumnVisibilityState, type ColumnFiltersState, type PaginationState, type DataTableProps, type DataTableCoreProps, type DataTablePaginationProps, type InferRowType, type FormatResult, getAlignClass, formatCellValue, getColumnValue, defaultGetRowId, DataTableToolbar, DataTableSearch, DataTableColumnVisibility, DataTableColumnFilter, DataTableFacetedFilter, DataTablePagination, DataTableVirtual, DataTableEditableCell, } from './components/data-table/index.js';
|
|
63
65
|
export { default as LogoMain } from './components/LogoMain.svelte';
|
|
64
66
|
export { default as LoadingLogo } from './components/LoadingLogo.svelte';
|
|
@@ -88,9 +90,13 @@ export type { PanelEdge, PanelMode, PanelVariant, SnapPoint, CardConfig, Positio
|
|
|
88
90
|
export { cn } from './utils.js';
|
|
89
91
|
export { parseDate, toISOString, formatDate, formatDateTime, isSameDay } from './utils/date.js';
|
|
90
92
|
export { generateId, computeDescribedBy } from './utils/form.js';
|
|
93
|
+
export { formatDuration, formatDurationSpoken, inferMediaKind, parseEmbedUrl, buildEmbedSrc, getEmbedThumbnail, getMediaThumbnail, getMediaLabel, isTimeBasedMedia, isSpatialMedia, isHlsSource, resolveAspectRatio, toMediaSources, HLS_MIME_TYPE, DASH_MIME_TYPE, type ParsedEmbed, type EmbedSrcOptions, } from './utils/media.js';
|
|
94
|
+
export { cloudflareStream, cloudflareStreamUrls, cloudflareStreamThumbnail, cloudflareStreamHost, type CloudflareStreamItemOptions, type CloudflareStreamUrlOptions, type CloudflareStreamUrls, type CloudflareStreamThumbnailOptions, } from './utils/media.js';
|
|
95
|
+
export { matterportTour, buildMatterportSrc, fetchMatterportMetadata, type MatterportTourOptions, type MatterportMetadata, } from './utils/media.js';
|
|
96
|
+
export { setHlsLoader, hasHlsLoader, loadHls, canPlayHlsNatively, type HlsLoader, type HlsConstructor, type HlsInstance, } from './utils/hls.js';
|
|
91
97
|
export { tv, type VariantProps } from 'tailwind-variants';
|
|
92
98
|
export { validateNonEmptyArray, validateRequired, validateOneOf, validateRange, validateProps, createValidator, type ValidationResult, } from './validation.js';
|
|
93
99
|
export { perfStart, perfEnd, measure, measureAsync, getPerformanceEntries, clearPerformanceEntries, createPerfMonitor, type PerformanceMark, } from './performance.js';
|
|
94
|
-
export { useForm, useAsync, runAsync, usePersistedForm, useFocusTrap, useFocusRef, useReducedMotion, useArrowNavigation, useRovingTabindex, useAnnounce, useEscapeKey, useClickOutside, clickOutsideAction, type UseFormOptions, type UseFormReturn, type FieldError, type FormState, type InferFormData, type UseAsyncOptions, type UseAsyncReturn, type UsePersistedFormOptions, type UsePersistedFormReturn, type InferPersistedFormData, type UseFocusTrapOptions, type UseFocusRefOptions, type UseArrowNavigationOptions, type ArrowNavigationOrientation, type UseRovingTabindexOptions, type UseAnnounceOptions, type UseAnnounceReturn, type UseEscapeKeyOptions, type UseClickOutsideOptions, } from './composables/index.js';
|
|
100
|
+
export { useForm, useAsync, runAsync, usePersistedForm, useFocusTrap, useFocusRef, useReducedMotion, useArrowNavigation, useRovingTabindex, useAnnounce, useMediaPlayer, useMediaZoom, useHlsSource, useEscapeKey, useClickOutside, clickOutsideAction, type UseFormOptions, type UseFormReturn, type FieldError, type FormState, type InferFormData, type UseAsyncOptions, type UseAsyncReturn, type UsePersistedFormOptions, type UsePersistedFormReturn, type InferPersistedFormData, type UseFocusTrapOptions, type UseFocusRefOptions, type UseArrowNavigationOptions, type ArrowNavigationOrientation, type UseRovingTabindexOptions, type UseAnnounceOptions, type UseAnnounceReturn, type UseEscapeKeyOptions, type UseClickOutsideOptions, type UseMediaPlayerOptions, type MediaPlayerApi, type MediaTimeRange, type MediaTrackInfo, type UseMediaZoomOptions, type MediaZoomApi, type UseHlsSourceOptions, type HlsSourceApi, type HlsStatus, } from './composables/index.js';
|
|
95
101
|
export * from './schemas/index.js';
|
|
96
102
|
export { type Notice, type NoticeConfig, type NoticeLocation, type NoticeVariant, type NoticePage, type ActiveNotice, NoticeSchema, NoticeLinkSchema, validateNotice, validateNotices, initializeNotices, setCurrentPath, dismissNotice, clearDismissedNotices, isNoticeDismissed, activeNotices, bannerNotices, cardNotices, getNoticesForPage, refreshNotices, cleanupDismissedNotices, isNoticeActive, isNoticeForPage, isNoticeForLocation, sanitizeHtml, initSanitizer, NoticeBanner, NoticeCard, } from './notices/index.js';
|
package/dist/lib/index.js
CHANGED
|
@@ -70,6 +70,8 @@ export { default as DataTable } from './components/DataTable.svelte';
|
|
|
70
70
|
// Tabs
|
|
71
71
|
export { default as Tabs } from './components/Tabs.svelte';
|
|
72
72
|
export { default as TabPanel } from './components/TabPanel.svelte';
|
|
73
|
+
// Media Components
|
|
74
|
+
export { AspectRatio, Image, Video, Audio, MediaEmbed, MediaGrid, MediaCarousel, Lightbox, } from './components/media/index.js';
|
|
73
75
|
// DataTable sub-components and utilities
|
|
74
76
|
export {
|
|
75
77
|
// Core state management (native Svelte 5 runes)
|
|
@@ -113,6 +115,15 @@ export { cn } from './utils.js';
|
|
|
113
115
|
export { parseDate, toISOString, formatDate, formatDateTime, isSameDay } from './utils/date.js';
|
|
114
116
|
// Form utilities
|
|
115
117
|
export { generateId, computeDescribedBy } from './utils/form.js';
|
|
118
|
+
// Media utilities
|
|
119
|
+
export { formatDuration, formatDurationSpoken, inferMediaKind, parseEmbedUrl, buildEmbedSrc, getEmbedThumbnail, getMediaThumbnail, getMediaLabel, isTimeBasedMedia, isSpatialMedia, isHlsSource, resolveAspectRatio, toMediaSources, HLS_MIME_TYPE, DASH_MIME_TYPE, } from './utils/media.js';
|
|
120
|
+
// Cloudflare Stream helpers
|
|
121
|
+
export { cloudflareStream, cloudflareStreamUrls, cloudflareStreamThumbnail, cloudflareStreamHost, } from './utils/media.js';
|
|
122
|
+
// Matterport 3D tours
|
|
123
|
+
export { matterportTour, buildMatterportSrc, fetchMatterportMetadata, } from './utils/media.js';
|
|
124
|
+
// HLS support — hls.js is an optional peer dependency; register a loader once
|
|
125
|
+
// at app startup to enable adaptive streams outside Safari
|
|
126
|
+
export { setHlsLoader, hasHlsLoader, loadHls, canPlayHlsNatively, } from './utils/hls.js';
|
|
116
127
|
// Re-export tailwind-variants types for consumer convenience
|
|
117
128
|
export { tv } from 'tailwind-variants';
|
|
118
129
|
// Validation utilities (dev-only, tree-shaken in production)
|
|
@@ -122,7 +133,9 @@ export { perfStart, perfEnd, measure, measureAsync, getPerformanceEntries, clear
|
|
|
122
133
|
// Composables - reusable Svelte 5 state management patterns
|
|
123
134
|
export { useForm, useAsync, runAsync, usePersistedForm,
|
|
124
135
|
// Accessibility composables
|
|
125
|
-
useFocusTrap, useFocusRef, useReducedMotion, useArrowNavigation, useRovingTabindex, useAnnounce,
|
|
136
|
+
useFocusTrap, useFocusRef, useReducedMotion, useArrowNavigation, useRovingTabindex, useAnnounce,
|
|
137
|
+
// Media composables
|
|
138
|
+
useMediaPlayer, useMediaZoom, useHlsSource, useEscapeKey, useClickOutside, clickOutsideAction, } from './composables/index.js';
|
|
126
139
|
// Validation schemas - Zod schemas for forms and data validation
|
|
127
140
|
export * from './schemas/index.js';
|
|
128
141
|
// Notice System - Scheduled, page-specific, dismissible announcements
|
|
@@ -60,6 +60,12 @@ export interface FileMetadata {
|
|
|
60
60
|
error?: string;
|
|
61
61
|
/** Whether the file is currently uploading */
|
|
62
62
|
uploading?: boolean;
|
|
63
|
+
/** Playback duration in seconds, for audio and video files */
|
|
64
|
+
duration?: number;
|
|
65
|
+
/** Intrinsic width in pixels, for images and video */
|
|
66
|
+
width?: number;
|
|
67
|
+
/** Intrinsic height in pixels, for images and video */
|
|
68
|
+
height?: number;
|
|
63
69
|
}
|
|
64
70
|
/**
|
|
65
71
|
* DataTable Types
|
|
@@ -205,3 +211,169 @@ export interface SignatureValidationResult {
|
|
|
205
211
|
valid: boolean;
|
|
206
212
|
error?: string;
|
|
207
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Media Types
|
|
216
|
+
*/
|
|
217
|
+
/** The kind of media an item represents */
|
|
218
|
+
export type MediaKind = 'image' | 'video' | 'audio' | 'embed';
|
|
219
|
+
/**
|
|
220
|
+
* Third-party providers supported by MediaEmbed.
|
|
221
|
+
*
|
|
222
|
+
* `matterport` embeds an interactive 3D space rather than a video, so it has
|
|
223
|
+
* no timeline: it shows no play affordance or duration in grids and strips.
|
|
224
|
+
*/
|
|
225
|
+
export type MediaEmbedProvider = 'youtube' | 'vimeo' | 'cloudflare' | 'matterport';
|
|
226
|
+
/**
|
|
227
|
+
* Location of a Cloudflare Stream account.
|
|
228
|
+
*
|
|
229
|
+
* Supply `customerCode` (the `customer-<CODE>` part of the subdomain shown in
|
|
230
|
+
* the Stream dashboard), or `domain` when Stream is served from a custom
|
|
231
|
+
* hostname.
|
|
232
|
+
*/
|
|
233
|
+
export interface CloudflareStreamAccount {
|
|
234
|
+
/** Customer code from the Stream dashboard */
|
|
235
|
+
customerCode?: string;
|
|
236
|
+
/** Full hostname, when serving Stream from a custom domain */
|
|
237
|
+
domain?: string;
|
|
238
|
+
}
|
|
239
|
+
/** A single `<source>` entry for a video or audio element */
|
|
240
|
+
export interface MediaSource {
|
|
241
|
+
/** Source URL */
|
|
242
|
+
src: string;
|
|
243
|
+
/** MIME type, e.g. `video/mp4` — lets the browser skip sources it cannot play */
|
|
244
|
+
type?: string;
|
|
245
|
+
}
|
|
246
|
+
/** A `<track>` entry providing captions, subtitles, or descriptions */
|
|
247
|
+
export interface MediaTextTrack {
|
|
248
|
+
/** URL of the WebVTT file */
|
|
249
|
+
src: string;
|
|
250
|
+
/** Track kind (defaults to `subtitles`) */
|
|
251
|
+
kind?: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
|
|
252
|
+
/** BCP 47 language tag, e.g. `en` */
|
|
253
|
+
srclang: string;
|
|
254
|
+
/** Human-readable label shown in the captions menu */
|
|
255
|
+
label: string;
|
|
256
|
+
/** Whether this track is enabled by default */
|
|
257
|
+
default?: boolean;
|
|
258
|
+
}
|
|
259
|
+
/** Fields shared by every media item */
|
|
260
|
+
export interface MediaItemBase {
|
|
261
|
+
/** Unique identifier, used as the keyed-each key */
|
|
262
|
+
id: string;
|
|
263
|
+
/** Short title shown in the lightbox and used as an accessible name */
|
|
264
|
+
title?: string;
|
|
265
|
+
/** Longer caption shown beneath the media */
|
|
266
|
+
caption?: string;
|
|
267
|
+
/** Thumbnail URL used in grids, carousels, and thumbnail strips */
|
|
268
|
+
thumbnail?: string;
|
|
269
|
+
}
|
|
270
|
+
/** An image item */
|
|
271
|
+
export interface ImageMediaItem extends MediaItemBase {
|
|
272
|
+
kind: 'image';
|
|
273
|
+
/** Image URL */
|
|
274
|
+
src: string;
|
|
275
|
+
/** Alternative text — required; use an empty string only for decorative images */
|
|
276
|
+
alt: string;
|
|
277
|
+
/** Responsive candidate list */
|
|
278
|
+
srcset?: string;
|
|
279
|
+
/** Sizes attribute paired with `srcset` */
|
|
280
|
+
sizes?: string;
|
|
281
|
+
/** Low-resolution placeholder (typically a data URL) blurred in while loading */
|
|
282
|
+
placeholder?: string;
|
|
283
|
+
/** Intrinsic width in pixels — set it to reserve layout space */
|
|
284
|
+
width?: number;
|
|
285
|
+
/** Intrinsic height in pixels — set it to reserve layout space */
|
|
286
|
+
height?: number;
|
|
287
|
+
/** Download URL, when it differs from `src` */
|
|
288
|
+
downloadUrl?: string;
|
|
289
|
+
}
|
|
290
|
+
/** A self-hosted video item */
|
|
291
|
+
export interface VideoMediaItem extends MediaItemBase {
|
|
292
|
+
kind: 'video';
|
|
293
|
+
/** A single URL, or several sources for format fallback */
|
|
294
|
+
src: string | MediaSource[];
|
|
295
|
+
/** Poster image shown before playback starts */
|
|
296
|
+
poster?: string;
|
|
297
|
+
/** Caption and subtitle tracks */
|
|
298
|
+
tracks?: MediaTextTrack[];
|
|
299
|
+
/** Known duration in seconds, shown before metadata loads */
|
|
300
|
+
duration?: number;
|
|
301
|
+
/** Download URL, when downloads are offered */
|
|
302
|
+
downloadUrl?: string;
|
|
303
|
+
}
|
|
304
|
+
/** A self-hosted audio item */
|
|
305
|
+
export interface AudioMediaItem extends MediaItemBase {
|
|
306
|
+
kind: 'audio';
|
|
307
|
+
/** A single URL, or several sources for format fallback */
|
|
308
|
+
src: string | MediaSource[];
|
|
309
|
+
/** Performer or author, shown beside the title */
|
|
310
|
+
artist?: string;
|
|
311
|
+
/** Artwork image URL */
|
|
312
|
+
artwork?: string;
|
|
313
|
+
/** Caption and transcript tracks */
|
|
314
|
+
tracks?: MediaTextTrack[];
|
|
315
|
+
/** Known duration in seconds, shown before metadata loads */
|
|
316
|
+
duration?: number;
|
|
317
|
+
/** Download URL, when downloads are offered */
|
|
318
|
+
downloadUrl?: string;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Options for the Matterport showcase player.
|
|
322
|
+
*
|
|
323
|
+
* Matterport's own defaults are left alone unless an option asks otherwise,
|
|
324
|
+
* so an embed looks the way the space's owner configured it.
|
|
325
|
+
*/
|
|
326
|
+
export interface MatterportPlayerOptions {
|
|
327
|
+
/** Start the space moving as soon as it loads */
|
|
328
|
+
autoplay?: boolean;
|
|
329
|
+
/** Load a lightweight first view so the space is explorable sooner (default `true`) */
|
|
330
|
+
quickStart?: boolean;
|
|
331
|
+
/** Show Matterport's "Presented by" branding and contact details (default `true`) */
|
|
332
|
+
brand?: boolean;
|
|
333
|
+
/** Show the in-player help button (default `true`) */
|
|
334
|
+
help?: boolean;
|
|
335
|
+
/** Show the highlight reel along the bottom (default `true`) */
|
|
336
|
+
highlightReel?: boolean;
|
|
337
|
+
/** Show the VR button (default `true`) */
|
|
338
|
+
vr?: boolean;
|
|
339
|
+
/** Show the floor selector (default `true`) */
|
|
340
|
+
floorSelector?: boolean;
|
|
341
|
+
/** Seconds after load before the guided tour starts on its own */
|
|
342
|
+
guidedTourStart?: number;
|
|
343
|
+
/** Use the MLS-compliant presentation, which strips branding, links, and VR */
|
|
344
|
+
mls?: boolean;
|
|
345
|
+
/** Interface language, as a two-letter code */
|
|
346
|
+
lang?: string;
|
|
347
|
+
/**
|
|
348
|
+
* Any further showcase parameters, passed through untouched. Booleans are
|
|
349
|
+
* sent as Matterport's `1`/`0`.
|
|
350
|
+
*/
|
|
351
|
+
params?: Record<string, string | number | boolean>;
|
|
352
|
+
}
|
|
353
|
+
/** A third-party embedded video item */
|
|
354
|
+
export interface EmbedMediaItem extends MediaItemBase, CloudflareStreamAccount {
|
|
355
|
+
kind: 'embed';
|
|
356
|
+
/** Hosting provider */
|
|
357
|
+
provider: MediaEmbedProvider;
|
|
358
|
+
/**
|
|
359
|
+
* Provider-specific identifier. The video id for YouTube and Vimeo; for
|
|
360
|
+
* Cloudflare Stream the video UID, or a signed playback token when the
|
|
361
|
+
* video is private; for Matterport the space's model id.
|
|
362
|
+
*/
|
|
363
|
+
videoId: string;
|
|
364
|
+
/**
|
|
365
|
+
* Poster image. Derived automatically for YouTube and Cloudflare Stream;
|
|
366
|
+
* required for Vimeo, whose thumbnails need a network lookup.
|
|
367
|
+
*/
|
|
368
|
+
poster?: string;
|
|
369
|
+
/** Start offset in seconds */
|
|
370
|
+
start?: number;
|
|
371
|
+
/** Showcase options, for `provider: 'matterport'` */
|
|
372
|
+
matterport?: MatterportPlayerOptions;
|
|
373
|
+
}
|
|
374
|
+
/** Any item renderable by the media components */
|
|
375
|
+
export type MediaItem = ImageMediaItem | VideoMediaItem | AudioMediaItem | EmbedMediaItem;
|
|
376
|
+
/** Named aspect ratios accepted by AspectRatio and media components */
|
|
377
|
+
export type MediaAspectRatio = 'square' | 'video' | 'photo' | 'portrait' | 'wide';
|
|
378
|
+
/** How media should be fitted into its box */
|
|
379
|
+
export type MediaFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HLS support
|
|
3
|
+
*
|
|
4
|
+
* Adaptive streams — Cloudflare Stream among them — are delivered as HLS
|
|
5
|
+
* playlists. Safari and iOS play those natively; Chrome and Firefox need
|
|
6
|
+
* `hls.js`.
|
|
7
|
+
*
|
|
8
|
+
* `hls.js` is an *optional* peer dependency, and the import that loads it
|
|
9
|
+
* deliberately lives in application code rather than in this package. A
|
|
10
|
+
* dynamic `import('hls.js')` written here would be resolved at build time by
|
|
11
|
+
* every consumer's bundler, so anyone who had not installed hls.js would fail
|
|
12
|
+
* to build even if they never touched a stream. Registering a loader keeps the
|
|
13
|
+
* dependency genuinely optional.
|
|
14
|
+
*
|
|
15
|
+
* Three ways to provide it, checked in order:
|
|
16
|
+
*
|
|
17
|
+
* 1. A loader passed to the component: `<Video hlsLoader={...} />`
|
|
18
|
+
* 2. A loader registered once at app startup via {@link setHlsLoader}
|
|
19
|
+
* 3. A global `Hls`, as set by the library's CDN build in a `<script>` tag
|
|
20
|
+
*
|
|
21
|
+
* @example Register once at app startup
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { setHlsLoader } from '@classic-homes/theme-svelte';
|
|
24
|
+
*
|
|
25
|
+
* setHlsLoader(() => import('hls.js').then((m) => m.default));
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The shape of `hls.js` this package relies on.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately structural rather than an `import type` of the real package —
|
|
32
|
+
* typing against `hls.js` would make it a required dependency for type
|
|
33
|
+
* checking, which is exactly what an optional peer dependency must avoid.
|
|
34
|
+
*/
|
|
35
|
+
export interface HlsInstance {
|
|
36
|
+
loadSource(url: string): void;
|
|
37
|
+
attachMedia(media: HTMLMediaElement): void;
|
|
38
|
+
destroy(): void;
|
|
39
|
+
on(event: string, callback: (event: string, data: HlsErrorData) => void): void;
|
|
40
|
+
startLoad(): void;
|
|
41
|
+
recoverMediaError(): void;
|
|
42
|
+
}
|
|
43
|
+
/** The error payload hls.js passes to its `hlsError` event. */
|
|
44
|
+
export interface HlsErrorData {
|
|
45
|
+
type?: string;
|
|
46
|
+
details?: string;
|
|
47
|
+
fatal?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** The `hls.js` default export: a constructor plus a static support probe. */
|
|
50
|
+
export interface HlsConstructor {
|
|
51
|
+
new (config?: Record<string, unknown>): HlsInstance;
|
|
52
|
+
isSupported(): boolean;
|
|
53
|
+
readonly Events: Record<string, string>;
|
|
54
|
+
readonly ErrorTypes: Record<string, string>;
|
|
55
|
+
}
|
|
56
|
+
/** A function that resolves the `hls.js` constructor. */
|
|
57
|
+
export type HlsLoader = () => Promise<HlsConstructor> | HlsConstructor;
|
|
58
|
+
/**
|
|
59
|
+
* Register how `hls.js` should be loaded, once, at application startup.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* setHlsLoader(() => import('hls.js').then((m) => m.default));
|
|
63
|
+
*/
|
|
64
|
+
export declare function setHlsLoader(loader: HlsLoader | null): void;
|
|
65
|
+
/** Whether an `hls.js` loader is available, without loading it. */
|
|
66
|
+
export declare function hasHlsLoader(): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the `hls.js` constructor, or `null` when none is available.
|
|
69
|
+
*
|
|
70
|
+
* The result is cached, so several players on one page share a single load.
|
|
71
|
+
*/
|
|
72
|
+
export declare function loadHls(loader?: HlsLoader): Promise<HlsConstructor | null>;
|
|
73
|
+
/** Whether the browser can play HLS without `hls.js`. */
|
|
74
|
+
export declare function canPlayHlsNatively(element?: HTMLMediaElement | null): boolean;
|
|
75
|
+
/** Reset the cached loader. Exported for tests. */
|
|
76
|
+
export declare function resetHlsLoader(): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HLS support
|
|
3
|
+
*
|
|
4
|
+
* Adaptive streams — Cloudflare Stream among them — are delivered as HLS
|
|
5
|
+
* playlists. Safari and iOS play those natively; Chrome and Firefox need
|
|
6
|
+
* `hls.js`.
|
|
7
|
+
*
|
|
8
|
+
* `hls.js` is an *optional* peer dependency, and the import that loads it
|
|
9
|
+
* deliberately lives in application code rather than in this package. A
|
|
10
|
+
* dynamic `import('hls.js')` written here would be resolved at build time by
|
|
11
|
+
* every consumer's bundler, so anyone who had not installed hls.js would fail
|
|
12
|
+
* to build even if they never touched a stream. Registering a loader keeps the
|
|
13
|
+
* dependency genuinely optional.
|
|
14
|
+
*
|
|
15
|
+
* Three ways to provide it, checked in order:
|
|
16
|
+
*
|
|
17
|
+
* 1. A loader passed to the component: `<Video hlsLoader={...} />`
|
|
18
|
+
* 2. A loader registered once at app startup via {@link setHlsLoader}
|
|
19
|
+
* 3. A global `Hls`, as set by the library's CDN build in a `<script>` tag
|
|
20
|
+
*
|
|
21
|
+
* @example Register once at app startup
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { setHlsLoader } from '@classic-homes/theme-svelte';
|
|
24
|
+
*
|
|
25
|
+
* setHlsLoader(() => import('hls.js').then((m) => m.default));
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
let registeredLoader = null;
|
|
29
|
+
let cached = null;
|
|
30
|
+
/**
|
|
31
|
+
* Register how `hls.js` should be loaded, once, at application startup.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* setHlsLoader(() => import('hls.js').then((m) => m.default));
|
|
35
|
+
*/
|
|
36
|
+
export function setHlsLoader(loader) {
|
|
37
|
+
registeredLoader = loader;
|
|
38
|
+
// Drop any cached resolution so a newly registered loader takes effect.
|
|
39
|
+
cached = null;
|
|
40
|
+
}
|
|
41
|
+
/** Whether an `hls.js` loader is available, without loading it. */
|
|
42
|
+
export function hasHlsLoader() {
|
|
43
|
+
return registeredLoader !== null || getGlobalHls() !== null;
|
|
44
|
+
}
|
|
45
|
+
function getGlobalHls() {
|
|
46
|
+
if (typeof globalThis === 'undefined')
|
|
47
|
+
return null;
|
|
48
|
+
const candidate = globalThis.Hls;
|
|
49
|
+
return typeof candidate === 'function' ? candidate : null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the `hls.js` constructor, or `null` when none is available.
|
|
53
|
+
*
|
|
54
|
+
* The result is cached, so several players on one page share a single load.
|
|
55
|
+
*/
|
|
56
|
+
export function loadHls(loader) {
|
|
57
|
+
// A per-call loader bypasses the shared cache; it may differ per component.
|
|
58
|
+
if (loader) {
|
|
59
|
+
return Promise.resolve(loader()).catch(() => null);
|
|
60
|
+
}
|
|
61
|
+
if (!cached) {
|
|
62
|
+
const resolved = registeredLoader ?? (() => getGlobalHls());
|
|
63
|
+
cached = Promise.resolve()
|
|
64
|
+
.then(() => resolved())
|
|
65
|
+
.then((ctor) => ctor ?? null)
|
|
66
|
+
.catch(() => null);
|
|
67
|
+
}
|
|
68
|
+
return cached;
|
|
69
|
+
}
|
|
70
|
+
/** Whether the browser can play HLS without `hls.js`. */
|
|
71
|
+
export function canPlayHlsNatively(element) {
|
|
72
|
+
const probe = element ?? (typeof document === 'undefined' ? null : document.createElement('video'));
|
|
73
|
+
if (!probe)
|
|
74
|
+
return false;
|
|
75
|
+
// Safari answers 'maybe' or 'probably'; everything else answers ''.
|
|
76
|
+
return (probe.canPlayType('application/vnd.apple.mpegurl') !== '' ||
|
|
77
|
+
probe.canPlayType('application/x-mpegURL') !== '');
|
|
78
|
+
}
|
|
79
|
+
/** Reset the cached loader. Exported for tests. */
|
|
80
|
+
export function resetHlsLoader() {
|
|
81
|
+
registeredLoader = null;
|
|
82
|
+
cached = null;
|
|
83
|
+
}
|