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:
parent
218b4b33d6
commit
e64968e761
4 changed files with 93 additions and 1 deletions
|
|
@ -5,3 +5,4 @@
|
|||
VITE_API_URL=/api/v1
|
||||
VITE_IS_STORYBOOK=true
|
||||
VITE_USE_MSW=true
|
||||
VITE_FEATURE_HLS_STREAMING=true
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
69
apps/web/src/mocks/handlers-streaming.ts
Normal file
69
apps/web/src/mocks/handlers-streaming.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue