@lightbird/core 0.8.0 → 0.10.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/dist/index.cjs +70 -0
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +70 -1
- package/dist/react/index.cjs +172 -0
- package/dist/react/index.d.cts +38 -13
- package/dist/react/index.d.ts +38 -13
- package/dist/react/index.js +172 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -765,14 +765,32 @@ function isHlsUrl(url) {
|
|
|
765
765
|
if (HLS_MIME_HINTS.some((hint) => lower.includes(hint))) return true;
|
|
766
766
|
return lower.split(/[?#]/)[0].endsWith(".m3u8");
|
|
767
767
|
}
|
|
768
|
+
function parseHlsCodec(codec) {
|
|
769
|
+
if (!codec) return null;
|
|
770
|
+
switch (codec.slice(0, 4).toLowerCase()) {
|
|
771
|
+
case "avc1":
|
|
772
|
+
return "H.264 (AVC)";
|
|
773
|
+
case "hvc1":
|
|
774
|
+
case "hev1":
|
|
775
|
+
return "H.265 (HEVC)";
|
|
776
|
+
case "vp09":
|
|
777
|
+
return "VP9";
|
|
778
|
+
case "av01":
|
|
779
|
+
return "AV1";
|
|
780
|
+
default:
|
|
781
|
+
return codec;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
768
784
|
var HLSPlayer = class {
|
|
769
785
|
constructor(url) {
|
|
770
786
|
this.hls = null;
|
|
787
|
+
this.hlsCtor = null;
|
|
771
788
|
this.url = url;
|
|
772
789
|
this.playerFile = { url, qualityLevels: [] };
|
|
773
790
|
}
|
|
774
791
|
async initialize(videoElement) {
|
|
775
792
|
const { default: HlsCtor } = await import('hls.js');
|
|
793
|
+
this.hlsCtor = HlsCtor;
|
|
776
794
|
if (!HlsCtor.isSupported()) {
|
|
777
795
|
videoElement.src = this.url;
|
|
778
796
|
return this.playerFile;
|
|
@@ -818,6 +836,57 @@ var HLSPlayer = class {
|
|
|
818
836
|
setQualityLevel(levelIndex) {
|
|
819
837
|
if (this.hls) this.hls.currentLevel = levelIndex;
|
|
820
838
|
}
|
|
839
|
+
/**
|
|
840
|
+
* Index of the active rendition, or `-1` for automatic (ABR) selection /
|
|
841
|
+
* before the manifest loads / on the native-HLS path.
|
|
842
|
+
*/
|
|
843
|
+
getCurrentLevel() {
|
|
844
|
+
return this.hls?.currentLevel ?? -1;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* HLS-specific metadata for the video info panel, derived from the active
|
|
848
|
+
* rendition. Returns an empty object before the manifest loads or on the
|
|
849
|
+
* native-HLS path (where hls.js is never instantiated).
|
|
850
|
+
*/
|
|
851
|
+
getMetadata() {
|
|
852
|
+
const hls = this.hls;
|
|
853
|
+
if (!hls) return {};
|
|
854
|
+
const levels = hls.levels ?? [];
|
|
855
|
+
const activeIndex = hls.currentLevel >= 0 && hls.currentLevel < levels.length ? hls.currentLevel : 0;
|
|
856
|
+
const active = levels[activeIndex];
|
|
857
|
+
return {
|
|
858
|
+
container: "HLS",
|
|
859
|
+
videoCodec: parseHlsCodec(active?.videoCodec),
|
|
860
|
+
videoBitrate: active?.bitrate ?? null,
|
|
861
|
+
streamRenditions: levels.length,
|
|
862
|
+
audioTracks: (hls.audioTracks ?? []).map((track, index) => ({
|
|
863
|
+
index,
|
|
864
|
+
codec: track.audioCodec ?? null,
|
|
865
|
+
channels: null,
|
|
866
|
+
sampleRate: null,
|
|
867
|
+
language: track.lang ?? null,
|
|
868
|
+
bitrate: null
|
|
869
|
+
}))
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Subscribe to HLS events that change the info-panel metadata (manifest
|
|
874
|
+
* parsed, quality level switched, audio tracks updated). Returns an
|
|
875
|
+
* unsubscribe function. A no-op on the native-HLS path.
|
|
876
|
+
*/
|
|
877
|
+
onMetadataChange(callback) {
|
|
878
|
+
const hls = this.hls;
|
|
879
|
+
const HlsCtor = this.hlsCtor;
|
|
880
|
+
if (!hls || !HlsCtor) return () => {
|
|
881
|
+
};
|
|
882
|
+
const events = [
|
|
883
|
+
HlsCtor.Events.MANIFEST_PARSED,
|
|
884
|
+
HlsCtor.Events.LEVEL_SWITCHED,
|
|
885
|
+
HlsCtor.Events.AUDIO_TRACKS_UPDATED
|
|
886
|
+
];
|
|
887
|
+
events.forEach((event) => hls.on(event, callback));
|
|
888
|
+
return () => events.forEach((event) => hls.off(event, callback));
|
|
889
|
+
}
|
|
821
890
|
destroy() {
|
|
822
891
|
if (this.hls) {
|
|
823
892
|
this.hls.destroy();
|
|
@@ -1823,6 +1892,7 @@ exports.loadShortcuts = loadShortcuts;
|
|
|
1823
1892
|
exports.matchesShortcut = matchesShortcut;
|
|
1824
1893
|
exports.parseChaptersFromFFmpegLog = parseChaptersFromFFmpegLog;
|
|
1825
1894
|
exports.parseChaptersFromVtt = parseChaptersFromVtt;
|
|
1895
|
+
exports.parseHlsCodec = parseHlsCodec;
|
|
1826
1896
|
exports.parseM3U8 = parseM3U8;
|
|
1827
1897
|
exports.parseMediaError = parseMediaError;
|
|
1828
1898
|
exports.resetFFmpeg = resetFFmpeg;
|
package/dist/index.d.cts
CHANGED
|
@@ -69,6 +69,8 @@ interface VideoMetadata {
|
|
|
69
69
|
colorSpace: string | null;
|
|
70
70
|
audioTracks: AudioTrackMeta[];
|
|
71
71
|
subtitleTracks: SubtitleTrackMeta[];
|
|
72
|
+
/** Number of HLS/adaptive renditions, when playing an adaptive stream. */
|
|
73
|
+
streamRenditions?: number | null;
|
|
72
74
|
}
|
|
73
75
|
interface AudioTrackMeta {
|
|
74
76
|
index: number;
|
|
@@ -196,16 +198,27 @@ interface VideoPlayer {
|
|
|
196
198
|
* For all other players/paths this is already resolved when initialize() returns.
|
|
197
199
|
*/
|
|
198
200
|
tracksReady?: Promise<void>;
|
|
201
|
+
/** HLS-only: metadata derived from the active rendition (see HLSPlayer). */
|
|
202
|
+
getMetadata?(): Partial<VideoMetadata>;
|
|
203
|
+
/** HLS-only: subscribe to stream events that change metadata. Returns an unsubscribe fn. */
|
|
204
|
+
onMetadataChange?(callback: () => void): () => void;
|
|
199
205
|
}
|
|
200
206
|
declare function createVideoPlayer(source: File | string, externalSubtitles?: File[], onProgress?: (progress: number) => void): VideoPlayer;
|
|
201
207
|
|
|
202
208
|
/** True for HLS playlist URLs — a `.m3u8` path or an HLS MIME hint. */
|
|
203
209
|
declare function isHlsUrl(url: string): boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Map an hls.js MIME codec string (e.g. `avc1.640028`) to a human-friendly
|
|
212
|
+
* label by inspecting the leading four-character codec family. Unknown
|
|
213
|
+
* families fall back to the raw string; empty input yields `null`.
|
|
214
|
+
*/
|
|
215
|
+
declare function parseHlsCodec(codec: string | null | undefined): string | null;
|
|
204
216
|
/** Plays HLS (`.m3u8`) streams via hls.js, falling back to native HLS (Safari). */
|
|
205
217
|
declare class HLSPlayer implements VideoPlayer {
|
|
206
218
|
private readonly url;
|
|
207
219
|
private readonly playerFile;
|
|
208
220
|
private hls;
|
|
221
|
+
private hlsCtor;
|
|
209
222
|
constructor(url: string);
|
|
210
223
|
initialize(videoElement: HTMLVideoElement): Promise<HLSPlayerFile>;
|
|
211
224
|
getAudioTracks(): AudioTrack[];
|
|
@@ -216,6 +229,23 @@ declare class HLSPlayer implements VideoPlayer {
|
|
|
216
229
|
getQualityLevels(): QualityLevel[];
|
|
217
230
|
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
218
231
|
setQualityLevel(levelIndex: number): void;
|
|
232
|
+
/**
|
|
233
|
+
* Index of the active rendition, or `-1` for automatic (ABR) selection /
|
|
234
|
+
* before the manifest loads / on the native-HLS path.
|
|
235
|
+
*/
|
|
236
|
+
getCurrentLevel(): number;
|
|
237
|
+
/**
|
|
238
|
+
* HLS-specific metadata for the video info panel, derived from the active
|
|
239
|
+
* rendition. Returns an empty object before the manifest loads or on the
|
|
240
|
+
* native-HLS path (where hls.js is never instantiated).
|
|
241
|
+
*/
|
|
242
|
+
getMetadata(): Partial<VideoMetadata>;
|
|
243
|
+
/**
|
|
244
|
+
* Subscribe to HLS events that change the info-panel metadata (manifest
|
|
245
|
+
* parsed, quality level switched, audio tracks updated). Returns an
|
|
246
|
+
* unsubscribe function. A no-op on the native-HLS path.
|
|
247
|
+
*/
|
|
248
|
+
onMetadataChange(callback: () => void): () => void;
|
|
219
249
|
destroy(): void;
|
|
220
250
|
static isCompatible(url: string): boolean;
|
|
221
251
|
}
|
|
@@ -495,4 +525,4 @@ declare function resetFFmpeg(): void;
|
|
|
495
525
|
*/
|
|
496
526
|
declare function getLanguageName(code?: string | null): string | undefined;
|
|
497
527
|
|
|
498
|
-
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, type ExportFrameOptions, FLAG_MAGNET_LINK, HLSPlayer, type HLSPlayerFile, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type QualityLevel, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, type TorrentStatus, UniversalSubtitleManager, VIDEO_EXTENSIONS, type VideoFilters, type VideoMetadata, type VideoPlayer, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, downloadDataUrl, exportPlaylist, exportVideoFrame, extractNativeMetadata, formatShortcutKey, frameExportFilename, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
528
|
+
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, type ExportFrameOptions, FLAG_MAGNET_LINK, HLSPlayer, type HLSPlayerFile, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type QualityLevel, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, type TorrentStatus, UniversalSubtitleManager, VIDEO_EXTENSIONS, type VideoFilters, type VideoMetadata, type VideoPlayer, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, downloadDataUrl, exportPlaylist, exportVideoFrame, extractNativeMetadata, formatShortcutKey, frameExportFilename, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseHlsCodec, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/index.d.ts
CHANGED
|
@@ -69,6 +69,8 @@ interface VideoMetadata {
|
|
|
69
69
|
colorSpace: string | null;
|
|
70
70
|
audioTracks: AudioTrackMeta[];
|
|
71
71
|
subtitleTracks: SubtitleTrackMeta[];
|
|
72
|
+
/** Number of HLS/adaptive renditions, when playing an adaptive stream. */
|
|
73
|
+
streamRenditions?: number | null;
|
|
72
74
|
}
|
|
73
75
|
interface AudioTrackMeta {
|
|
74
76
|
index: number;
|
|
@@ -196,16 +198,27 @@ interface VideoPlayer {
|
|
|
196
198
|
* For all other players/paths this is already resolved when initialize() returns.
|
|
197
199
|
*/
|
|
198
200
|
tracksReady?: Promise<void>;
|
|
201
|
+
/** HLS-only: metadata derived from the active rendition (see HLSPlayer). */
|
|
202
|
+
getMetadata?(): Partial<VideoMetadata>;
|
|
203
|
+
/** HLS-only: subscribe to stream events that change metadata. Returns an unsubscribe fn. */
|
|
204
|
+
onMetadataChange?(callback: () => void): () => void;
|
|
199
205
|
}
|
|
200
206
|
declare function createVideoPlayer(source: File | string, externalSubtitles?: File[], onProgress?: (progress: number) => void): VideoPlayer;
|
|
201
207
|
|
|
202
208
|
/** True for HLS playlist URLs — a `.m3u8` path or an HLS MIME hint. */
|
|
203
209
|
declare function isHlsUrl(url: string): boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Map an hls.js MIME codec string (e.g. `avc1.640028`) to a human-friendly
|
|
212
|
+
* label by inspecting the leading four-character codec family. Unknown
|
|
213
|
+
* families fall back to the raw string; empty input yields `null`.
|
|
214
|
+
*/
|
|
215
|
+
declare function parseHlsCodec(codec: string | null | undefined): string | null;
|
|
204
216
|
/** Plays HLS (`.m3u8`) streams via hls.js, falling back to native HLS (Safari). */
|
|
205
217
|
declare class HLSPlayer implements VideoPlayer {
|
|
206
218
|
private readonly url;
|
|
207
219
|
private readonly playerFile;
|
|
208
220
|
private hls;
|
|
221
|
+
private hlsCtor;
|
|
209
222
|
constructor(url: string);
|
|
210
223
|
initialize(videoElement: HTMLVideoElement): Promise<HLSPlayerFile>;
|
|
211
224
|
getAudioTracks(): AudioTrack[];
|
|
@@ -216,6 +229,23 @@ declare class HLSPlayer implements VideoPlayer {
|
|
|
216
229
|
getQualityLevels(): QualityLevel[];
|
|
217
230
|
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
218
231
|
setQualityLevel(levelIndex: number): void;
|
|
232
|
+
/**
|
|
233
|
+
* Index of the active rendition, or `-1` for automatic (ABR) selection /
|
|
234
|
+
* before the manifest loads / on the native-HLS path.
|
|
235
|
+
*/
|
|
236
|
+
getCurrentLevel(): number;
|
|
237
|
+
/**
|
|
238
|
+
* HLS-specific metadata for the video info panel, derived from the active
|
|
239
|
+
* rendition. Returns an empty object before the manifest loads or on the
|
|
240
|
+
* native-HLS path (where hls.js is never instantiated).
|
|
241
|
+
*/
|
|
242
|
+
getMetadata(): Partial<VideoMetadata>;
|
|
243
|
+
/**
|
|
244
|
+
* Subscribe to HLS events that change the info-panel metadata (manifest
|
|
245
|
+
* parsed, quality level switched, audio tracks updated). Returns an
|
|
246
|
+
* unsubscribe function. A no-op on the native-HLS path.
|
|
247
|
+
*/
|
|
248
|
+
onMetadataChange(callback: () => void): () => void;
|
|
219
249
|
destroy(): void;
|
|
220
250
|
static isCompatible(url: string): boolean;
|
|
221
251
|
}
|
|
@@ -495,4 +525,4 @@ declare function resetFFmpeg(): void;
|
|
|
495
525
|
*/
|
|
496
526
|
declare function getLanguageName(code?: string | null): string | undefined;
|
|
497
527
|
|
|
498
|
-
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, type ExportFrameOptions, FLAG_MAGNET_LINK, HLSPlayer, type HLSPlayerFile, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type QualityLevel, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, type TorrentStatus, UniversalSubtitleManager, VIDEO_EXTENSIONS, type VideoFilters, type VideoMetadata, type VideoPlayer, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, downloadDataUrl, exportPlaylist, exportVideoFrame, extractNativeMetadata, formatShortcutKey, frameExportFilename, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
528
|
+
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, type ExportFrameOptions, FLAG_MAGNET_LINK, HLSPlayer, type HLSPlayerFile, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type QualityLevel, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, type TorrentStatus, UniversalSubtitleManager, VIDEO_EXTENSIONS, type VideoFilters, type VideoMetadata, type VideoPlayer, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, downloadDataUrl, exportPlaylist, exportVideoFrame, extractNativeMetadata, formatShortcutKey, frameExportFilename, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseHlsCodec, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/index.js
CHANGED
|
@@ -762,14 +762,32 @@ function isHlsUrl(url) {
|
|
|
762
762
|
if (HLS_MIME_HINTS.some((hint) => lower.includes(hint))) return true;
|
|
763
763
|
return lower.split(/[?#]/)[0].endsWith(".m3u8");
|
|
764
764
|
}
|
|
765
|
+
function parseHlsCodec(codec) {
|
|
766
|
+
if (!codec) return null;
|
|
767
|
+
switch (codec.slice(0, 4).toLowerCase()) {
|
|
768
|
+
case "avc1":
|
|
769
|
+
return "H.264 (AVC)";
|
|
770
|
+
case "hvc1":
|
|
771
|
+
case "hev1":
|
|
772
|
+
return "H.265 (HEVC)";
|
|
773
|
+
case "vp09":
|
|
774
|
+
return "VP9";
|
|
775
|
+
case "av01":
|
|
776
|
+
return "AV1";
|
|
777
|
+
default:
|
|
778
|
+
return codec;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
765
781
|
var HLSPlayer = class {
|
|
766
782
|
constructor(url) {
|
|
767
783
|
this.hls = null;
|
|
784
|
+
this.hlsCtor = null;
|
|
768
785
|
this.url = url;
|
|
769
786
|
this.playerFile = { url, qualityLevels: [] };
|
|
770
787
|
}
|
|
771
788
|
async initialize(videoElement) {
|
|
772
789
|
const { default: HlsCtor } = await import('hls.js');
|
|
790
|
+
this.hlsCtor = HlsCtor;
|
|
773
791
|
if (!HlsCtor.isSupported()) {
|
|
774
792
|
videoElement.src = this.url;
|
|
775
793
|
return this.playerFile;
|
|
@@ -815,6 +833,57 @@ var HLSPlayer = class {
|
|
|
815
833
|
setQualityLevel(levelIndex) {
|
|
816
834
|
if (this.hls) this.hls.currentLevel = levelIndex;
|
|
817
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* Index of the active rendition, or `-1` for automatic (ABR) selection /
|
|
838
|
+
* before the manifest loads / on the native-HLS path.
|
|
839
|
+
*/
|
|
840
|
+
getCurrentLevel() {
|
|
841
|
+
return this.hls?.currentLevel ?? -1;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* HLS-specific metadata for the video info panel, derived from the active
|
|
845
|
+
* rendition. Returns an empty object before the manifest loads or on the
|
|
846
|
+
* native-HLS path (where hls.js is never instantiated).
|
|
847
|
+
*/
|
|
848
|
+
getMetadata() {
|
|
849
|
+
const hls = this.hls;
|
|
850
|
+
if (!hls) return {};
|
|
851
|
+
const levels = hls.levels ?? [];
|
|
852
|
+
const activeIndex = hls.currentLevel >= 0 && hls.currentLevel < levels.length ? hls.currentLevel : 0;
|
|
853
|
+
const active = levels[activeIndex];
|
|
854
|
+
return {
|
|
855
|
+
container: "HLS",
|
|
856
|
+
videoCodec: parseHlsCodec(active?.videoCodec),
|
|
857
|
+
videoBitrate: active?.bitrate ?? null,
|
|
858
|
+
streamRenditions: levels.length,
|
|
859
|
+
audioTracks: (hls.audioTracks ?? []).map((track, index) => ({
|
|
860
|
+
index,
|
|
861
|
+
codec: track.audioCodec ?? null,
|
|
862
|
+
channels: null,
|
|
863
|
+
sampleRate: null,
|
|
864
|
+
language: track.lang ?? null,
|
|
865
|
+
bitrate: null
|
|
866
|
+
}))
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Subscribe to HLS events that change the info-panel metadata (manifest
|
|
871
|
+
* parsed, quality level switched, audio tracks updated). Returns an
|
|
872
|
+
* unsubscribe function. A no-op on the native-HLS path.
|
|
873
|
+
*/
|
|
874
|
+
onMetadataChange(callback) {
|
|
875
|
+
const hls = this.hls;
|
|
876
|
+
const HlsCtor = this.hlsCtor;
|
|
877
|
+
if (!hls || !HlsCtor) return () => {
|
|
878
|
+
};
|
|
879
|
+
const events = [
|
|
880
|
+
HlsCtor.Events.MANIFEST_PARSED,
|
|
881
|
+
HlsCtor.Events.LEVEL_SWITCHED,
|
|
882
|
+
HlsCtor.Events.AUDIO_TRACKS_UPDATED
|
|
883
|
+
];
|
|
884
|
+
events.forEach((event) => hls.on(event, callback));
|
|
885
|
+
return () => events.forEach((event) => hls.off(event, callback));
|
|
886
|
+
}
|
|
818
887
|
destroy() {
|
|
819
888
|
if (this.hls) {
|
|
820
889
|
this.hls.destroy();
|
|
@@ -1779,4 +1848,4 @@ function resetFFmpeg() {
|
|
|
1779
1848
|
loading = null;
|
|
1780
1849
|
}
|
|
1781
1850
|
|
|
1782
|
-
export { ASSRenderer, CancellationError, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, HLSPlayer, MKVPlayer, ProgressEstimator, SimplePlayer, SubtitleConverter, UniversalSubtitleManager, VIDEO_EXTENSIONS, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, downloadDataUrl, exportPlaylist, exportVideoFrame, extractNativeMetadata, formatShortcutKey, frameExportFilename, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
1851
|
+
export { ASSRenderer, CancellationError, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, HLSPlayer, MKVPlayer, ProgressEstimator, SimplePlayer, SubtitleConverter, UniversalSubtitleManager, VIDEO_EXTENSIONS, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, downloadDataUrl, exportPlaylist, exportVideoFrame, extractNativeMetadata, formatShortcutKey, frameExportFilename, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseHlsCodec, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/react/index.cjs
CHANGED
|
@@ -836,6 +836,177 @@ function useVideoInfo(videoRef, currentFile) {
|
|
|
836
836
|
}, []);
|
|
837
837
|
return { metadata, enrichMetadata };
|
|
838
838
|
}
|
|
839
|
+
|
|
840
|
+
// src/players/hls-player.ts
|
|
841
|
+
var HLS_MIME_HINTS = ["application/x-mpegurl", "application/vnd.apple.mpegurl"];
|
|
842
|
+
function isHlsUrl(url) {
|
|
843
|
+
if (typeof url !== "string" || url.length === 0) return false;
|
|
844
|
+
const lower = url.toLowerCase();
|
|
845
|
+
if (HLS_MIME_HINTS.some((hint) => lower.includes(hint))) return true;
|
|
846
|
+
return lower.split(/[?#]/)[0].endsWith(".m3u8");
|
|
847
|
+
}
|
|
848
|
+
function parseHlsCodec(codec) {
|
|
849
|
+
if (!codec) return null;
|
|
850
|
+
switch (codec.slice(0, 4).toLowerCase()) {
|
|
851
|
+
case "avc1":
|
|
852
|
+
return "H.264 (AVC)";
|
|
853
|
+
case "hvc1":
|
|
854
|
+
case "hev1":
|
|
855
|
+
return "H.265 (HEVC)";
|
|
856
|
+
case "vp09":
|
|
857
|
+
return "VP9";
|
|
858
|
+
case "av01":
|
|
859
|
+
return "AV1";
|
|
860
|
+
default:
|
|
861
|
+
return codec;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
var HLSPlayer = class {
|
|
865
|
+
constructor(url) {
|
|
866
|
+
this.hls = null;
|
|
867
|
+
this.hlsCtor = null;
|
|
868
|
+
this.url = url;
|
|
869
|
+
this.playerFile = { url, qualityLevels: [] };
|
|
870
|
+
}
|
|
871
|
+
async initialize(videoElement) {
|
|
872
|
+
const { default: HlsCtor } = await import('hls.js');
|
|
873
|
+
this.hlsCtor = HlsCtor;
|
|
874
|
+
if (!HlsCtor.isSupported()) {
|
|
875
|
+
videoElement.src = this.url;
|
|
876
|
+
return this.playerFile;
|
|
877
|
+
}
|
|
878
|
+
const hls = new HlsCtor();
|
|
879
|
+
this.hls = hls;
|
|
880
|
+
hls.loadSource(this.url);
|
|
881
|
+
hls.attachMedia(videoElement);
|
|
882
|
+
return this.playerFile;
|
|
883
|
+
}
|
|
884
|
+
getAudioTracks() {
|
|
885
|
+
if (!this.hls) return [];
|
|
886
|
+
return this.hls.audioTracks.map((track) => ({
|
|
887
|
+
id: String(track.id),
|
|
888
|
+
name: track.name,
|
|
889
|
+
lang: track.lang || "unknown"
|
|
890
|
+
}));
|
|
891
|
+
}
|
|
892
|
+
getSubtitles() {
|
|
893
|
+
return [];
|
|
894
|
+
}
|
|
895
|
+
switchAudioTrack(trackId) {
|
|
896
|
+
if (this.hls) {
|
|
897
|
+
const id = Number(trackId);
|
|
898
|
+
if (!Number.isNaN(id)) this.hls.audioTrack = id;
|
|
899
|
+
}
|
|
900
|
+
return Promise.resolve();
|
|
901
|
+
}
|
|
902
|
+
switchSubtitle() {
|
|
903
|
+
return Promise.resolve();
|
|
904
|
+
}
|
|
905
|
+
/** Quality renditions — not on `VideoPlayer`; read directly by the quality hook. */
|
|
906
|
+
getQualityLevels() {
|
|
907
|
+
if (!this.hls) return [];
|
|
908
|
+
return this.hls.levels.map((level, index) => ({
|
|
909
|
+
index,
|
|
910
|
+
height: level.height,
|
|
911
|
+
bitrate: level.bitrate,
|
|
912
|
+
name: level.name || (level.height > 0 ? `${level.height}p` : `Level ${index + 1}`)
|
|
913
|
+
}));
|
|
914
|
+
}
|
|
915
|
+
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
916
|
+
setQualityLevel(levelIndex) {
|
|
917
|
+
if (this.hls) this.hls.currentLevel = levelIndex;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Index of the active rendition, or `-1` for automatic (ABR) selection /
|
|
921
|
+
* before the manifest loads / on the native-HLS path.
|
|
922
|
+
*/
|
|
923
|
+
getCurrentLevel() {
|
|
924
|
+
return this.hls?.currentLevel ?? -1;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* HLS-specific metadata for the video info panel, derived from the active
|
|
928
|
+
* rendition. Returns an empty object before the manifest loads or on the
|
|
929
|
+
* native-HLS path (where hls.js is never instantiated).
|
|
930
|
+
*/
|
|
931
|
+
getMetadata() {
|
|
932
|
+
const hls = this.hls;
|
|
933
|
+
if (!hls) return {};
|
|
934
|
+
const levels = hls.levels ?? [];
|
|
935
|
+
const activeIndex = hls.currentLevel >= 0 && hls.currentLevel < levels.length ? hls.currentLevel : 0;
|
|
936
|
+
const active = levels[activeIndex];
|
|
937
|
+
return {
|
|
938
|
+
container: "HLS",
|
|
939
|
+
videoCodec: parseHlsCodec(active?.videoCodec),
|
|
940
|
+
videoBitrate: active?.bitrate ?? null,
|
|
941
|
+
streamRenditions: levels.length,
|
|
942
|
+
audioTracks: (hls.audioTracks ?? []).map((track, index) => ({
|
|
943
|
+
index,
|
|
944
|
+
codec: track.audioCodec ?? null,
|
|
945
|
+
channels: null,
|
|
946
|
+
sampleRate: null,
|
|
947
|
+
language: track.lang ?? null,
|
|
948
|
+
bitrate: null
|
|
949
|
+
}))
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Subscribe to HLS events that change the info-panel metadata (manifest
|
|
954
|
+
* parsed, quality level switched, audio tracks updated). Returns an
|
|
955
|
+
* unsubscribe function. A no-op on the native-HLS path.
|
|
956
|
+
*/
|
|
957
|
+
onMetadataChange(callback) {
|
|
958
|
+
const hls = this.hls;
|
|
959
|
+
const HlsCtor = this.hlsCtor;
|
|
960
|
+
if (!hls || !HlsCtor) return () => {
|
|
961
|
+
};
|
|
962
|
+
const events = [
|
|
963
|
+
HlsCtor.Events.MANIFEST_PARSED,
|
|
964
|
+
HlsCtor.Events.LEVEL_SWITCHED,
|
|
965
|
+
HlsCtor.Events.AUDIO_TRACKS_UPDATED
|
|
966
|
+
];
|
|
967
|
+
events.forEach((event) => hls.on(event, callback));
|
|
968
|
+
return () => events.forEach((event) => hls.off(event, callback));
|
|
969
|
+
}
|
|
970
|
+
destroy() {
|
|
971
|
+
if (this.hls) {
|
|
972
|
+
this.hls.destroy();
|
|
973
|
+
this.hls = null;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
static isCompatible(url) {
|
|
977
|
+
return isHlsUrl(url);
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
// src/react/use-hls-quality.ts
|
|
982
|
+
function useHlsQuality(playerRef) {
|
|
983
|
+
const [qualityLevels, setQualityLevels] = react.useState([]);
|
|
984
|
+
const [currentLevel, setCurrentLevel] = react.useState(-1);
|
|
985
|
+
const player = playerRef.current;
|
|
986
|
+
react.useEffect(() => {
|
|
987
|
+
if (!(player instanceof HLSPlayer)) {
|
|
988
|
+
setQualityLevels([]);
|
|
989
|
+
setCurrentLevel(-1);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const refresh = () => {
|
|
993
|
+
setQualityLevels(player.getQualityLevels());
|
|
994
|
+
setCurrentLevel(player.getCurrentLevel());
|
|
995
|
+
};
|
|
996
|
+
refresh();
|
|
997
|
+
return player.onMetadataChange(refresh);
|
|
998
|
+
}, [player]);
|
|
999
|
+
const setLevel = react.useCallback(
|
|
1000
|
+
(idx) => {
|
|
1001
|
+
const current = playerRef.current;
|
|
1002
|
+
if (!(current instanceof HLSPlayer)) return;
|
|
1003
|
+
current.setQualityLevel(idx);
|
|
1004
|
+
setCurrentLevel(idx);
|
|
1005
|
+
},
|
|
1006
|
+
[playerRef]
|
|
1007
|
+
);
|
|
1008
|
+
return { qualityLevels, currentLevel, setLevel };
|
|
1009
|
+
}
|
|
839
1010
|
function useMediaSession(options) {
|
|
840
1011
|
const { title, artwork, onPlay, onPause, onNext, onPrev, onSeekForward, onSeekBackward } = options;
|
|
841
1012
|
react.useEffect(() => {
|
|
@@ -1523,6 +1694,7 @@ function useTouchGestures(targetRef, handlers, options = {}) {
|
|
|
1523
1694
|
exports.useABLoop = useABLoop;
|
|
1524
1695
|
exports.useChapters = useChapters;
|
|
1525
1696
|
exports.useFullscreen = useFullscreen;
|
|
1697
|
+
exports.useHlsQuality = useHlsQuality;
|
|
1526
1698
|
exports.useKeyboardShortcuts = useKeyboardShortcuts;
|
|
1527
1699
|
exports.useMagnet = useMagnet;
|
|
1528
1700
|
exports.useMediaSession = useMediaSession;
|
package/dist/react/index.d.cts
CHANGED
|
@@ -81,6 +81,8 @@ interface VideoMetadata {
|
|
|
81
81
|
colorSpace: string | null;
|
|
82
82
|
audioTracks: AudioTrackMeta[];
|
|
83
83
|
subtitleTracks: SubtitleTrackMeta[];
|
|
84
|
+
/** Number of HLS/adaptive renditions, when playing an adaptive stream. */
|
|
85
|
+
streamRenditions?: number | null;
|
|
84
86
|
}
|
|
85
87
|
interface AudioTrackMeta {
|
|
86
88
|
index: number;
|
|
@@ -219,18 +221,6 @@ declare function useVideoInfo(videoRef: RefObject<HTMLVideoElement | null>, curr
|
|
|
219
221
|
enrichMetadata: (extra: Partial<VideoMetadata>) => void;
|
|
220
222
|
};
|
|
221
223
|
|
|
222
|
-
interface UseMediaSessionOptions {
|
|
223
|
-
title: string | null;
|
|
224
|
-
artwork?: string | null;
|
|
225
|
-
onPlay: () => void;
|
|
226
|
-
onPause: () => void;
|
|
227
|
-
onNext: () => void;
|
|
228
|
-
onPrev: () => void;
|
|
229
|
-
onSeekForward: () => void;
|
|
230
|
-
onSeekBackward: () => void;
|
|
231
|
-
}
|
|
232
|
-
declare function useMediaSession(options: UseMediaSessionOptions): void;
|
|
233
|
-
|
|
234
224
|
interface SimplePlayerFile {
|
|
235
225
|
name: string;
|
|
236
226
|
file: File;
|
|
@@ -264,7 +254,42 @@ interface VideoPlayer {
|
|
|
264
254
|
* For all other players/paths this is already resolved when initialize() returns.
|
|
265
255
|
*/
|
|
266
256
|
tracksReady?: Promise<void>;
|
|
257
|
+
/** HLS-only: metadata derived from the active rendition (see HLSPlayer). */
|
|
258
|
+
getMetadata?(): Partial<VideoMetadata>;
|
|
259
|
+
/** HLS-only: subscribe to stream events that change metadata. Returns an unsubscribe fn. */
|
|
260
|
+
onMetadataChange?(callback: () => void): () => void;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface UseHlsQualityReturn {
|
|
264
|
+
/** Available renditions. Empty unless an HLS stream with levels is loaded. */
|
|
265
|
+
qualityLevels: QualityLevel[];
|
|
266
|
+
/** Index of the active level, or -1 for automatic (ABR) selection. */
|
|
267
|
+
currentLevel: number;
|
|
268
|
+
/** Switch to a level by index (-1 = auto). No-op when not playing HLS. */
|
|
269
|
+
setLevel: (idx: number) => void;
|
|
267
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Exposes the quality levels of the currently loaded HLS stream and lets the
|
|
273
|
+
* caller switch between them. Returns an empty list and a no-op `setLevel` when
|
|
274
|
+
* the player is not an {@link HLSPlayer}.
|
|
275
|
+
*
|
|
276
|
+
* The list and the active level are kept in sync via
|
|
277
|
+
* {@link HLSPlayer.onMetadataChange}, which fires on manifest parse and on
|
|
278
|
+
* every `LEVEL_SWITCHED` event.
|
|
279
|
+
*/
|
|
280
|
+
declare function useHlsQuality(playerRef: RefObject<VideoPlayer | null>): UseHlsQualityReturn;
|
|
281
|
+
|
|
282
|
+
interface UseMediaSessionOptions {
|
|
283
|
+
title: string | null;
|
|
284
|
+
artwork?: string | null;
|
|
285
|
+
onPlay: () => void;
|
|
286
|
+
onPause: () => void;
|
|
287
|
+
onNext: () => void;
|
|
288
|
+
onPrev: () => void;
|
|
289
|
+
onSeekForward: () => void;
|
|
290
|
+
onSeekBackward: () => void;
|
|
291
|
+
}
|
|
292
|
+
declare function useMediaSession(options: UseMediaSessionOptions): void;
|
|
268
293
|
|
|
269
294
|
declare function useChapters(videoRef: RefObject<HTMLVideoElement>, playerRef: RefObject<VideoPlayer | null>): {
|
|
270
295
|
chapters: Chapter[];
|
|
@@ -398,4 +423,4 @@ interface TouchGesturesState {
|
|
|
398
423
|
*/
|
|
399
424
|
declare function useTouchGestures(targetRef: RefObject<HTMLElement | null>, handlers: TouchGestureHandlers, options?: UseTouchGesturesOptions): TouchGesturesState;
|
|
400
425
|
|
|
401
|
-
export { type ABLoopState, type SeekPreviewState, type ShortcutHandlers, type TouchGestureFeedback, type TouchGestureHandlers, type TouchGesturesState, type UseMagnetReturn, type UseMediaSessionOptions, type UseSeekPreviewOptions, type UseSubtitlesOptions, type UseTouchGesturesOptions, useABLoop, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSmoothProgress, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
|
|
426
|
+
export { type ABLoopState, type SeekPreviewState, type ShortcutHandlers, type TouchGestureFeedback, type TouchGestureHandlers, type TouchGesturesState, type UseHlsQualityReturn, type UseMagnetReturn, type UseMediaSessionOptions, type UseSeekPreviewOptions, type UseSubtitlesOptions, type UseTouchGesturesOptions, useABLoop, useChapters, useFullscreen, useHlsQuality, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSmoothProgress, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
|
package/dist/react/index.d.ts
CHANGED
|
@@ -81,6 +81,8 @@ interface VideoMetadata {
|
|
|
81
81
|
colorSpace: string | null;
|
|
82
82
|
audioTracks: AudioTrackMeta[];
|
|
83
83
|
subtitleTracks: SubtitleTrackMeta[];
|
|
84
|
+
/** Number of HLS/adaptive renditions, when playing an adaptive stream. */
|
|
85
|
+
streamRenditions?: number | null;
|
|
84
86
|
}
|
|
85
87
|
interface AudioTrackMeta {
|
|
86
88
|
index: number;
|
|
@@ -219,18 +221,6 @@ declare function useVideoInfo(videoRef: RefObject<HTMLVideoElement | null>, curr
|
|
|
219
221
|
enrichMetadata: (extra: Partial<VideoMetadata>) => void;
|
|
220
222
|
};
|
|
221
223
|
|
|
222
|
-
interface UseMediaSessionOptions {
|
|
223
|
-
title: string | null;
|
|
224
|
-
artwork?: string | null;
|
|
225
|
-
onPlay: () => void;
|
|
226
|
-
onPause: () => void;
|
|
227
|
-
onNext: () => void;
|
|
228
|
-
onPrev: () => void;
|
|
229
|
-
onSeekForward: () => void;
|
|
230
|
-
onSeekBackward: () => void;
|
|
231
|
-
}
|
|
232
|
-
declare function useMediaSession(options: UseMediaSessionOptions): void;
|
|
233
|
-
|
|
234
224
|
interface SimplePlayerFile {
|
|
235
225
|
name: string;
|
|
236
226
|
file: File;
|
|
@@ -264,7 +254,42 @@ interface VideoPlayer {
|
|
|
264
254
|
* For all other players/paths this is already resolved when initialize() returns.
|
|
265
255
|
*/
|
|
266
256
|
tracksReady?: Promise<void>;
|
|
257
|
+
/** HLS-only: metadata derived from the active rendition (see HLSPlayer). */
|
|
258
|
+
getMetadata?(): Partial<VideoMetadata>;
|
|
259
|
+
/** HLS-only: subscribe to stream events that change metadata. Returns an unsubscribe fn. */
|
|
260
|
+
onMetadataChange?(callback: () => void): () => void;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface UseHlsQualityReturn {
|
|
264
|
+
/** Available renditions. Empty unless an HLS stream with levels is loaded. */
|
|
265
|
+
qualityLevels: QualityLevel[];
|
|
266
|
+
/** Index of the active level, or -1 for automatic (ABR) selection. */
|
|
267
|
+
currentLevel: number;
|
|
268
|
+
/** Switch to a level by index (-1 = auto). No-op when not playing HLS. */
|
|
269
|
+
setLevel: (idx: number) => void;
|
|
267
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Exposes the quality levels of the currently loaded HLS stream and lets the
|
|
273
|
+
* caller switch between them. Returns an empty list and a no-op `setLevel` when
|
|
274
|
+
* the player is not an {@link HLSPlayer}.
|
|
275
|
+
*
|
|
276
|
+
* The list and the active level are kept in sync via
|
|
277
|
+
* {@link HLSPlayer.onMetadataChange}, which fires on manifest parse and on
|
|
278
|
+
* every `LEVEL_SWITCHED` event.
|
|
279
|
+
*/
|
|
280
|
+
declare function useHlsQuality(playerRef: RefObject<VideoPlayer | null>): UseHlsQualityReturn;
|
|
281
|
+
|
|
282
|
+
interface UseMediaSessionOptions {
|
|
283
|
+
title: string | null;
|
|
284
|
+
artwork?: string | null;
|
|
285
|
+
onPlay: () => void;
|
|
286
|
+
onPause: () => void;
|
|
287
|
+
onNext: () => void;
|
|
288
|
+
onPrev: () => void;
|
|
289
|
+
onSeekForward: () => void;
|
|
290
|
+
onSeekBackward: () => void;
|
|
291
|
+
}
|
|
292
|
+
declare function useMediaSession(options: UseMediaSessionOptions): void;
|
|
268
293
|
|
|
269
294
|
declare function useChapters(videoRef: RefObject<HTMLVideoElement>, playerRef: RefObject<VideoPlayer | null>): {
|
|
270
295
|
chapters: Chapter[];
|
|
@@ -398,4 +423,4 @@ interface TouchGesturesState {
|
|
|
398
423
|
*/
|
|
399
424
|
declare function useTouchGestures(targetRef: RefObject<HTMLElement | null>, handlers: TouchGestureHandlers, options?: UseTouchGesturesOptions): TouchGesturesState;
|
|
400
425
|
|
|
401
|
-
export { type ABLoopState, type SeekPreviewState, type ShortcutHandlers, type TouchGestureFeedback, type TouchGestureHandlers, type TouchGesturesState, type UseMagnetReturn, type UseMediaSessionOptions, type UseSeekPreviewOptions, type UseSubtitlesOptions, type UseTouchGesturesOptions, useABLoop, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSmoothProgress, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
|
|
426
|
+
export { type ABLoopState, type SeekPreviewState, type ShortcutHandlers, type TouchGestureFeedback, type TouchGestureHandlers, type TouchGesturesState, type UseHlsQualityReturn, type UseMagnetReturn, type UseMediaSessionOptions, type UseSeekPreviewOptions, type UseSubtitlesOptions, type UseTouchGesturesOptions, useABLoop, useChapters, useFullscreen, useHlsQuality, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSmoothProgress, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
|
package/dist/react/index.js
CHANGED
|
@@ -834,6 +834,177 @@ function useVideoInfo(videoRef, currentFile) {
|
|
|
834
834
|
}, []);
|
|
835
835
|
return { metadata, enrichMetadata };
|
|
836
836
|
}
|
|
837
|
+
|
|
838
|
+
// src/players/hls-player.ts
|
|
839
|
+
var HLS_MIME_HINTS = ["application/x-mpegurl", "application/vnd.apple.mpegurl"];
|
|
840
|
+
function isHlsUrl(url) {
|
|
841
|
+
if (typeof url !== "string" || url.length === 0) return false;
|
|
842
|
+
const lower = url.toLowerCase();
|
|
843
|
+
if (HLS_MIME_HINTS.some((hint) => lower.includes(hint))) return true;
|
|
844
|
+
return lower.split(/[?#]/)[0].endsWith(".m3u8");
|
|
845
|
+
}
|
|
846
|
+
function parseHlsCodec(codec) {
|
|
847
|
+
if (!codec) return null;
|
|
848
|
+
switch (codec.slice(0, 4).toLowerCase()) {
|
|
849
|
+
case "avc1":
|
|
850
|
+
return "H.264 (AVC)";
|
|
851
|
+
case "hvc1":
|
|
852
|
+
case "hev1":
|
|
853
|
+
return "H.265 (HEVC)";
|
|
854
|
+
case "vp09":
|
|
855
|
+
return "VP9";
|
|
856
|
+
case "av01":
|
|
857
|
+
return "AV1";
|
|
858
|
+
default:
|
|
859
|
+
return codec;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
var HLSPlayer = class {
|
|
863
|
+
constructor(url) {
|
|
864
|
+
this.hls = null;
|
|
865
|
+
this.hlsCtor = null;
|
|
866
|
+
this.url = url;
|
|
867
|
+
this.playerFile = { url, qualityLevels: [] };
|
|
868
|
+
}
|
|
869
|
+
async initialize(videoElement) {
|
|
870
|
+
const { default: HlsCtor } = await import('hls.js');
|
|
871
|
+
this.hlsCtor = HlsCtor;
|
|
872
|
+
if (!HlsCtor.isSupported()) {
|
|
873
|
+
videoElement.src = this.url;
|
|
874
|
+
return this.playerFile;
|
|
875
|
+
}
|
|
876
|
+
const hls = new HlsCtor();
|
|
877
|
+
this.hls = hls;
|
|
878
|
+
hls.loadSource(this.url);
|
|
879
|
+
hls.attachMedia(videoElement);
|
|
880
|
+
return this.playerFile;
|
|
881
|
+
}
|
|
882
|
+
getAudioTracks() {
|
|
883
|
+
if (!this.hls) return [];
|
|
884
|
+
return this.hls.audioTracks.map((track) => ({
|
|
885
|
+
id: String(track.id),
|
|
886
|
+
name: track.name,
|
|
887
|
+
lang: track.lang || "unknown"
|
|
888
|
+
}));
|
|
889
|
+
}
|
|
890
|
+
getSubtitles() {
|
|
891
|
+
return [];
|
|
892
|
+
}
|
|
893
|
+
switchAudioTrack(trackId) {
|
|
894
|
+
if (this.hls) {
|
|
895
|
+
const id = Number(trackId);
|
|
896
|
+
if (!Number.isNaN(id)) this.hls.audioTrack = id;
|
|
897
|
+
}
|
|
898
|
+
return Promise.resolve();
|
|
899
|
+
}
|
|
900
|
+
switchSubtitle() {
|
|
901
|
+
return Promise.resolve();
|
|
902
|
+
}
|
|
903
|
+
/** Quality renditions — not on `VideoPlayer`; read directly by the quality hook. */
|
|
904
|
+
getQualityLevels() {
|
|
905
|
+
if (!this.hls) return [];
|
|
906
|
+
return this.hls.levels.map((level, index) => ({
|
|
907
|
+
index,
|
|
908
|
+
height: level.height,
|
|
909
|
+
bitrate: level.bitrate,
|
|
910
|
+
name: level.name || (level.height > 0 ? `${level.height}p` : `Level ${index + 1}`)
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
914
|
+
setQualityLevel(levelIndex) {
|
|
915
|
+
if (this.hls) this.hls.currentLevel = levelIndex;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Index of the active rendition, or `-1` for automatic (ABR) selection /
|
|
919
|
+
* before the manifest loads / on the native-HLS path.
|
|
920
|
+
*/
|
|
921
|
+
getCurrentLevel() {
|
|
922
|
+
return this.hls?.currentLevel ?? -1;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* HLS-specific metadata for the video info panel, derived from the active
|
|
926
|
+
* rendition. Returns an empty object before the manifest loads or on the
|
|
927
|
+
* native-HLS path (where hls.js is never instantiated).
|
|
928
|
+
*/
|
|
929
|
+
getMetadata() {
|
|
930
|
+
const hls = this.hls;
|
|
931
|
+
if (!hls) return {};
|
|
932
|
+
const levels = hls.levels ?? [];
|
|
933
|
+
const activeIndex = hls.currentLevel >= 0 && hls.currentLevel < levels.length ? hls.currentLevel : 0;
|
|
934
|
+
const active = levels[activeIndex];
|
|
935
|
+
return {
|
|
936
|
+
container: "HLS",
|
|
937
|
+
videoCodec: parseHlsCodec(active?.videoCodec),
|
|
938
|
+
videoBitrate: active?.bitrate ?? null,
|
|
939
|
+
streamRenditions: levels.length,
|
|
940
|
+
audioTracks: (hls.audioTracks ?? []).map((track, index) => ({
|
|
941
|
+
index,
|
|
942
|
+
codec: track.audioCodec ?? null,
|
|
943
|
+
channels: null,
|
|
944
|
+
sampleRate: null,
|
|
945
|
+
language: track.lang ?? null,
|
|
946
|
+
bitrate: null
|
|
947
|
+
}))
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Subscribe to HLS events that change the info-panel metadata (manifest
|
|
952
|
+
* parsed, quality level switched, audio tracks updated). Returns an
|
|
953
|
+
* unsubscribe function. A no-op on the native-HLS path.
|
|
954
|
+
*/
|
|
955
|
+
onMetadataChange(callback) {
|
|
956
|
+
const hls = this.hls;
|
|
957
|
+
const HlsCtor = this.hlsCtor;
|
|
958
|
+
if (!hls || !HlsCtor) return () => {
|
|
959
|
+
};
|
|
960
|
+
const events = [
|
|
961
|
+
HlsCtor.Events.MANIFEST_PARSED,
|
|
962
|
+
HlsCtor.Events.LEVEL_SWITCHED,
|
|
963
|
+
HlsCtor.Events.AUDIO_TRACKS_UPDATED
|
|
964
|
+
];
|
|
965
|
+
events.forEach((event) => hls.on(event, callback));
|
|
966
|
+
return () => events.forEach((event) => hls.off(event, callback));
|
|
967
|
+
}
|
|
968
|
+
destroy() {
|
|
969
|
+
if (this.hls) {
|
|
970
|
+
this.hls.destroy();
|
|
971
|
+
this.hls = null;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
static isCompatible(url) {
|
|
975
|
+
return isHlsUrl(url);
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
// src/react/use-hls-quality.ts
|
|
980
|
+
function useHlsQuality(playerRef) {
|
|
981
|
+
const [qualityLevels, setQualityLevels] = useState([]);
|
|
982
|
+
const [currentLevel, setCurrentLevel] = useState(-1);
|
|
983
|
+
const player = playerRef.current;
|
|
984
|
+
useEffect(() => {
|
|
985
|
+
if (!(player instanceof HLSPlayer)) {
|
|
986
|
+
setQualityLevels([]);
|
|
987
|
+
setCurrentLevel(-1);
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const refresh = () => {
|
|
991
|
+
setQualityLevels(player.getQualityLevels());
|
|
992
|
+
setCurrentLevel(player.getCurrentLevel());
|
|
993
|
+
};
|
|
994
|
+
refresh();
|
|
995
|
+
return player.onMetadataChange(refresh);
|
|
996
|
+
}, [player]);
|
|
997
|
+
const setLevel = useCallback(
|
|
998
|
+
(idx) => {
|
|
999
|
+
const current = playerRef.current;
|
|
1000
|
+
if (!(current instanceof HLSPlayer)) return;
|
|
1001
|
+
current.setQualityLevel(idx);
|
|
1002
|
+
setCurrentLevel(idx);
|
|
1003
|
+
},
|
|
1004
|
+
[playerRef]
|
|
1005
|
+
);
|
|
1006
|
+
return { qualityLevels, currentLevel, setLevel };
|
|
1007
|
+
}
|
|
837
1008
|
function useMediaSession(options) {
|
|
838
1009
|
const { title, artwork, onPlay, onPause, onNext, onPrev, onSeekForward, onSeekBackward } = options;
|
|
839
1010
|
useEffect(() => {
|
|
@@ -1518,4 +1689,4 @@ function useTouchGestures(targetRef, handlers, options = {}) {
|
|
|
1518
1689
|
return { feedback };
|
|
1519
1690
|
}
|
|
1520
1691
|
|
|
1521
|
-
export { useABLoop, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSmoothProgress, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
|
|
1692
|
+
export { useABLoop, useChapters, useFullscreen, useHlsQuality, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSmoothProgress, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightbird/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Client-side video player engine. Plays MKV, MP4, WebM with full subtitle, audio track, and chapter support. No server required.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Punyam Singh",
|