feat(player): integrate HLS streaming with ABR quality switching

- Connect useHLSPlayer hook to useAudioPlayerLifecycle for automatic
  HLS activation when feature flag and browser support are available
- Wire quality selector to HLS level switching via hlsPlayer.setQuality
- Expose isHLSActive and hlsLevels from lifecycle hook for UI components
- Create MSW handlers for HLS endpoints (info, status, master/quality
  playlists) for Storybook and testing
- Enable VITE_FEATURE_HLS_STREAMING in .env.storybook
This commit is contained in:
senke 2026-02-22 21:24:40 +01:00
parent 218b4b33d6
commit e64968e761
4 changed files with 93 additions and 1 deletions

View file

@ -5,3 +5,4 @@
VITE_API_URL=/api/v1
VITE_IS_STORYBOOK=true
VITE_USE_MSW=true
VITE_FEATURE_HLS_STREAMING=true

View file

@ -1,8 +1,11 @@
import { useRef, useEffect, useState } from 'react';
import Hls from 'hls.js';
import { usePlayer } from '../../hooks/usePlayer';
import { useStreamSync } from '../../hooks/useStreamSync';
import { usePlayerStore } from '../../store/playerStore';
import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts';
import { useHLSPlayer } from '@/features/streaming/hooks/useHLSPlayer';
import { FEATURES } from '@/config/features';
import type { AudioQuality } from '../QualitySelector';
import type { PlaybackSpeed } from '../PlaybackSpeedControl';
@ -23,6 +26,14 @@ export function useAudioPlayerLifecycle({
const [playbackSpeed, setPlaybackSpeed] = useState<PlaybackSpeed>(1);
const currentTrack = usePlayerStore((state) => state.currentTrack);
const hlsEnabled = FEATURES.HLS_STREAMING && Hls.isSupported();
const streamStatus = currentTrack?.url ? 'ready' : undefined;
const hlsPlayer = useHLSPlayer(
audioRef,
hlsEnabled ? (currentTrack?.id ?? null) : null,
hlsEnabled ? streamStatus : undefined,
);
const sessionId = currentTrack?.id ? `session_${currentTrack.id}` : null;
const { isSynced } = useStreamSync({
sessionId,
@ -78,6 +89,13 @@ export function useAudioPlayerLifecycle({
if (audio) audio.playbackRate = playbackSpeed;
}, [playbackSpeed]);
const handleSetQuality = (q: AudioQuality) => {
setQuality(q);
if (hlsPlayer.isHLSActive) {
hlsPlayer.setQuality(q);
}
};
const handlePlayPause = () => {
if (player.isPlaying) {
player.pause();
@ -103,7 +121,7 @@ export function useAudioPlayerLifecycle({
error,
isLoading,
quality,
setQuality,
setQuality: handleSetQuality,
playbackSpeed,
setPlaybackSpeed,
isSynced,
@ -112,5 +130,7 @@ export function useAudioPlayerLifecycle({
handleRetry,
canGoNext,
canGoPrevious,
isHLSActive: hlsPlayer.isHLSActive,
hlsLevels: hlsPlayer.levels,
};
}

View file

@ -0,0 +1,69 @@
import { http, HttpResponse } from 'msw';
export const handlersStreaming = [
http.get('*/api/v1/tracks/:id/hls/info', () => {
return HttpResponse.json({
success: true,
data: {
track_id: 'mock-track-id',
available: true,
qualities: [
{ bitrate: 128000, codec: 'aac', label: 'low' },
{ bitrate: 256000, codec: 'aac', label: 'medium' },
{ bitrate: 320000, codec: 'aac', label: 'high' },
],
duration: 240.5,
format: 'hls',
},
});
}),
http.get('*/api/v1/tracks/:id/hls/status', () => {
return HttpResponse.json({
success: true,
data: {
track_id: 'mock-track-id',
status: 'ready',
progress: 100,
created_at: new Date().toISOString(),
},
});
}),
http.get('*/api/v1/tracks/:id/hls/master.m3u8', () => {
const playlist = [
'#EXTM3U',
'#EXT-X-VERSION:3',
'#EXT-X-STREAM-INF:BANDWIDTH=128000,CODECS="mp4a.40.2"',
'128000/playlist.m3u8',
'#EXT-X-STREAM-INF:BANDWIDTH=256000,CODECS="mp4a.40.2"',
'256000/playlist.m3u8',
'#EXT-X-STREAM-INF:BANDWIDTH=320000,CODECS="mp4a.40.2"',
'320000/playlist.m3u8',
].join('\n');
return new HttpResponse(playlist, {
headers: {
'Content-Type': 'application/vnd.apple.mpegurl',
},
});
}),
http.get('*/api/v1/tracks/:id/hls/:bitrate/playlist.m3u8', () => {
const playlist = [
'#EXTM3U',
'#EXT-X-VERSION:3',
'#EXT-X-TARGETDURATION:10',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXTINF:10.0,',
'segment_000.ts',
'#EXT-X-ENDLIST',
].join('\n');
return new HttpResponse(playlist, {
headers: {
'Content-Type': 'application/vnd.apple.mpegurl',
},
});
}),
];

View file

@ -24,6 +24,7 @@ import { handlersTracks } from './handlers-tracks';
import { handlersPlaylists } from './handlers-playlists';
import { handlersMisc } from './handlers-misc';
import { handlersCloud } from './handlers-cloud';
import { handlersStreaming } from './handlers-streaming';
export const handlers = [
...handlersCommon,
@ -35,6 +36,7 @@ export const handlers = [
...handlersPlaylists,
...handlersMisc,
...handlersCloud,
...handlersStreaming,
// Catch-all for API to prevent network leaks (Phase 1: Stabilization)
http.all('*/api/v1/*', ({ request }) => {