@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 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
@@ -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;
@@ -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 };
@@ -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 };
@@ -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.9.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",