@lightbird/core 0.9.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 +7 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/react/index.cjs +172 -0
- package/dist/react/index.d.cts +32 -13
- package/dist/react/index.d.ts +32 -13
- package/dist/react/index.js +172 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -836,6 +836,13 @@ var HLSPlayer = class {
|
|
|
836
836
|
setQualityLevel(levelIndex) {
|
|
837
837
|
if (this.hls) this.hls.currentLevel = levelIndex;
|
|
838
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
|
+
}
|
|
839
846
|
/**
|
|
840
847
|
* HLS-specific metadata for the video info panel, derived from the active
|
|
841
848
|
* rendition. Returns an empty object before the manifest loads or on the
|
package/dist/index.d.cts
CHANGED
|
@@ -229,6 +229,11 @@ declare class HLSPlayer implements VideoPlayer {
|
|
|
229
229
|
getQualityLevels(): QualityLevel[];
|
|
230
230
|
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
231
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;
|
|
232
237
|
/**
|
|
233
238
|
* HLS-specific metadata for the video info panel, derived from the active
|
|
234
239
|
* rendition. Returns an empty object before the manifest loads or on the
|
package/dist/index.d.ts
CHANGED
|
@@ -229,6 +229,11 @@ declare class HLSPlayer implements VideoPlayer {
|
|
|
229
229
|
getQualityLevels(): QualityLevel[];
|
|
230
230
|
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
231
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;
|
|
232
237
|
/**
|
|
233
238
|
* HLS-specific metadata for the video info panel, derived from the active
|
|
234
239
|
* rendition. Returns an empty object before the manifest loads or on the
|
package/dist/index.js
CHANGED
|
@@ -833,6 +833,13 @@ var HLSPlayer = class {
|
|
|
833
833
|
setQualityLevel(levelIndex) {
|
|
834
834
|
if (this.hls) this.hls.currentLevel = levelIndex;
|
|
835
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
|
+
}
|
|
836
843
|
/**
|
|
837
844
|
* HLS-specific metadata for the video info panel, derived from the active
|
|
838
845
|
* rendition. Returns an empty object before the manifest loads or on the
|
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
|
@@ -221,18 +221,6 @@ declare function useVideoInfo(videoRef: RefObject<HTMLVideoElement | null>, curr
|
|
|
221
221
|
enrichMetadata: (extra: Partial<VideoMetadata>) => void;
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
-
interface UseMediaSessionOptions {
|
|
225
|
-
title: string | null;
|
|
226
|
-
artwork?: string | null;
|
|
227
|
-
onPlay: () => void;
|
|
228
|
-
onPause: () => void;
|
|
229
|
-
onNext: () => void;
|
|
230
|
-
onPrev: () => void;
|
|
231
|
-
onSeekForward: () => void;
|
|
232
|
-
onSeekBackward: () => void;
|
|
233
|
-
}
|
|
234
|
-
declare function useMediaSession(options: UseMediaSessionOptions): void;
|
|
235
|
-
|
|
236
224
|
interface SimplePlayerFile {
|
|
237
225
|
name: string;
|
|
238
226
|
file: File;
|
|
@@ -272,6 +260,37 @@ interface VideoPlayer {
|
|
|
272
260
|
onMetadataChange?(callback: () => void): () => void;
|
|
273
261
|
}
|
|
274
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;
|
|
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;
|
|
293
|
+
|
|
275
294
|
declare function useChapters(videoRef: RefObject<HTMLVideoElement>, playerRef: RefObject<VideoPlayer | null>): {
|
|
276
295
|
chapters: Chapter[];
|
|
277
296
|
currentChapter: Chapter | null;
|
|
@@ -404,4 +423,4 @@ interface TouchGesturesState {
|
|
|
404
423
|
*/
|
|
405
424
|
declare function useTouchGestures(targetRef: RefObject<HTMLElement | null>, handlers: TouchGestureHandlers, options?: UseTouchGesturesOptions): TouchGesturesState;
|
|
406
425
|
|
|
407
|
-
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
|
@@ -221,18 +221,6 @@ declare function useVideoInfo(videoRef: RefObject<HTMLVideoElement | null>, curr
|
|
|
221
221
|
enrichMetadata: (extra: Partial<VideoMetadata>) => void;
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
-
interface UseMediaSessionOptions {
|
|
225
|
-
title: string | null;
|
|
226
|
-
artwork?: string | null;
|
|
227
|
-
onPlay: () => void;
|
|
228
|
-
onPause: () => void;
|
|
229
|
-
onNext: () => void;
|
|
230
|
-
onPrev: () => void;
|
|
231
|
-
onSeekForward: () => void;
|
|
232
|
-
onSeekBackward: () => void;
|
|
233
|
-
}
|
|
234
|
-
declare function useMediaSession(options: UseMediaSessionOptions): void;
|
|
235
|
-
|
|
236
224
|
interface SimplePlayerFile {
|
|
237
225
|
name: string;
|
|
238
226
|
file: File;
|
|
@@ -272,6 +260,37 @@ interface VideoPlayer {
|
|
|
272
260
|
onMetadataChange?(callback: () => void): () => void;
|
|
273
261
|
}
|
|
274
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;
|
|
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;
|
|
293
|
+
|
|
275
294
|
declare function useChapters(videoRef: RefObject<HTMLVideoElement>, playerRef: RefObject<VideoPlayer | null>): {
|
|
276
295
|
chapters: Chapter[];
|
|
277
296
|
currentChapter: Chapter | null;
|
|
@@ -404,4 +423,4 @@ interface TouchGesturesState {
|
|
|
404
423
|
*/
|
|
405
424
|
declare function useTouchGestures(targetRef: RefObject<HTMLElement | null>, handlers: TouchGestureHandlers, options?: UseTouchGesturesOptions): TouchGesturesState;
|
|
406
425
|
|
|
407
|
-
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",
|