oddysee-react 0.1.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/README.md +356 -0
- package/dist/assets/index-COcDBgFa.css +1 -0
- package/dist/assets/index-DzSVBDbV.js +9 -0
- package/dist/assets/react-CHdo91hT.svg +1 -0
- package/dist/index.cjs +199 -0
- package/dist/index.d.cts +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.html +14 -0
- package/dist/index.js +174 -0
- package/dist/vite.svg +1 -0
- package/eslint.config.js +23 -0
- package/package.json +46 -0
- package/public/vite.svg +1 -0
- package/src/index.ts +2 -0
- package/src/use-hls-audio-player.ts +241 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.lib.json +16 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# @use-hls-player/react
|
|
2
|
+
|
|
3
|
+
A React hook that provides a simple, intuitive interface for HLS audio streaming using the core HLS audio player.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @use-hls-player/react
|
|
9
|
+
# or
|
|
10
|
+
yarn add @use-hls-player/react
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @use-hls-player/react
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { useHlsAudioPlayer } from '@use-hls-player/react';
|
|
19
|
+
|
|
20
|
+
function AudioPlayer() {
|
|
21
|
+
const { state, controls, isLoading, isPlaying } = useHlsAudioPlayer({
|
|
22
|
+
src: { url: 'https://example.com/playlist.m3u8' },
|
|
23
|
+
autoPlay: false,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div>
|
|
28
|
+
<button onClick={() => isPlaying ? controls.pause() : controls.play()}>
|
|
29
|
+
{isPlaying ? 'Pause' : 'Play'}
|
|
30
|
+
</button>
|
|
31
|
+
<div>
|
|
32
|
+
{isLoading ? 'Loading...' : `Duration: ${state.duration}s`}
|
|
33
|
+
</div>
|
|
34
|
+
{state.error && <div>Error: {state.error.message}</div>}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## API Reference
|
|
41
|
+
|
|
42
|
+
### `useHlsAudioPlayer(options)`
|
|
43
|
+
|
|
44
|
+
#### Options
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
interface UseHlsAudioPlayerOptions {
|
|
48
|
+
config?: PlayerConfig;
|
|
49
|
+
src?: { url: string; options?: SourceOptions };
|
|
50
|
+
autoPlay?: boolean;
|
|
51
|
+
on?: Partial<{
|
|
52
|
+
[K in PlayerEvent]: (data: PlayerEventMap[K]) => void
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- **config**: Global player configuration (see PlayerConfig below)
|
|
58
|
+
- **src**: Initial HLS source to load
|
|
59
|
+
- **autoPlay**: Automatically start playback when source is ready
|
|
60
|
+
- **on**: Event callbacks for player events
|
|
61
|
+
|
|
62
|
+
#### Return Value
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
interface UseHlsAudioPlayerResult {
|
|
66
|
+
player: HLSAudioPlayerInterface | null;
|
|
67
|
+
state: PlayerState;
|
|
68
|
+
isPlaying: boolean;
|
|
69
|
+
duration: number;
|
|
70
|
+
isLoading: boolean;
|
|
71
|
+
loading: boolean;
|
|
72
|
+
error: PlayerError | null;
|
|
73
|
+
readyState: number;
|
|
74
|
+
controls: {
|
|
75
|
+
setSource: (url: string, options?: SourceOptions) => Promise<void>;
|
|
76
|
+
play: () => Promise<void>;
|
|
77
|
+
pause: () => void;
|
|
78
|
+
setVolume: (volume: number) => void;
|
|
79
|
+
setCurrentTime: (time: number) => void;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Player State
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
interface PlayerState {
|
|
88
|
+
track: Track | null;
|
|
89
|
+
currentTime: number;
|
|
90
|
+
duration: number | null;
|
|
91
|
+
volume: number;
|
|
92
|
+
loading: boolean;
|
|
93
|
+
error: PlayerError | null;
|
|
94
|
+
readyState: number;
|
|
95
|
+
isPlaying: boolean;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Configuration Types
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
interface PlayerConfig {
|
|
103
|
+
network?: {
|
|
104
|
+
headers?: Record<string, string>;
|
|
105
|
+
timeout?: number;
|
|
106
|
+
retryCount?: number;
|
|
107
|
+
};
|
|
108
|
+
audio?: {
|
|
109
|
+
crossfade?: boolean;
|
|
110
|
+
normalization?: boolean;
|
|
111
|
+
preload?: boolean;
|
|
112
|
+
volume?: number;
|
|
113
|
+
};
|
|
114
|
+
playback?: {
|
|
115
|
+
autoPlay?: boolean;
|
|
116
|
+
startTime?: number;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface SourceOptions {
|
|
121
|
+
headers?: Record<string, string>;
|
|
122
|
+
startTime?: number;
|
|
123
|
+
[key: string]: any;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Examples
|
|
128
|
+
|
|
129
|
+
### Basic Audio Player
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { useHlsAudioPlayer } from '@use-hls-player/react';
|
|
133
|
+
|
|
134
|
+
export default function BasicPlayer() {
|
|
135
|
+
const { state, controls, isLoading, isPlaying } = useHlsAudioPlayer({
|
|
136
|
+
src: { url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8' },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const togglePlay = () => {
|
|
140
|
+
if (isPlaying) {
|
|
141
|
+
controls.pause();
|
|
142
|
+
} else {
|
|
143
|
+
controls.play();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="player">
|
|
149
|
+
<button onClick={togglePlay} disabled={isLoading}>
|
|
150
|
+
{isLoading ? 'Loading...' : isPlaying ? 'Pause' : 'Play'}
|
|
151
|
+
</button>
|
|
152
|
+
|
|
153
|
+
<div className="progress">
|
|
154
|
+
<input
|
|
155
|
+
type="range"
|
|
156
|
+
min={0}
|
|
157
|
+
max={state.duration ?? 0}
|
|
158
|
+
value={state.currentTime}
|
|
159
|
+
onChange={(e) => controls.setCurrentTime(Number(e.target.value))}
|
|
160
|
+
/>
|
|
161
|
+
<span>{state.currentTime.toFixed(0)}s / {state.duration?.toFixed(0) ?? '--'}s</span>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div className="volume">
|
|
165
|
+
<input
|
|
166
|
+
type="range"
|
|
167
|
+
min={0}
|
|
168
|
+
max={1}
|
|
169
|
+
step={0.01}
|
|
170
|
+
value={state.volume}
|
|
171
|
+
onChange={(e) => controls.setVolume(parseFloat(e.target.value))}
|
|
172
|
+
/>
|
|
173
|
+
<span>Volume: {Math.round(state.volume * 100)}%</span>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{state.error && (
|
|
177
|
+
<div className="error">Error: {state.error.message}</div>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Playlist Player
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
import { useState } from 'react';
|
|
188
|
+
import { useHlsAudioPlayer } from '@use-hls-player/react';
|
|
189
|
+
|
|
190
|
+
const playlist = [
|
|
191
|
+
{ id: 1, url: 'https://pl.streamingvideoprovider.com/mp3-playlist/playlist.m3u8', title: 'Music Playlist' },
|
|
192
|
+
{ id: 2, url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', title: 'Test Stream' },
|
|
193
|
+
{ id: 3, url: 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', title: 'Live Stream' },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
export default function PlaylistPlayer() {
|
|
197
|
+
const [currentTrackIndex, setCurrentTrackIndex] = useState(0);
|
|
198
|
+
const currentTrack = playlist[currentTrackIndex];
|
|
199
|
+
|
|
200
|
+
const { state, controls, isLoading, isPlaying } = useHlsAudioPlayer({
|
|
201
|
+
src: { url: currentTrack.url },
|
|
202
|
+
autoPlay: true,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const playTrack = (index: number) => {
|
|
206
|
+
setCurrentTrackIndex(index);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const playNext = () => {
|
|
210
|
+
const nextIndex = (currentTrackIndex + 1) % playlist.length;
|
|
211
|
+
setCurrentTrackIndex(nextIndex);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const playPrevious = () => {
|
|
215
|
+
const prevIndex = currentTrackIndex === 0 ? playlist.length - 1 : currentTrackIndex - 1;
|
|
216
|
+
setCurrentTrackIndex(prevIndex);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div className="playlist-player">
|
|
221
|
+
<div className="current-track">
|
|
222
|
+
<h3>{currentTrack.title}</h3>
|
|
223
|
+
<p>{isLoading ? 'Loading...' : isPlaying ? 'Playing' : 'Paused'}</p>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div className="controls">
|
|
227
|
+
<button onClick={playPrevious}>Previous</button>
|
|
228
|
+
<button onClick={() => isPlaying ? controls.pause() : controls.play()}>
|
|
229
|
+
{isPlaying ? 'Pause' : 'Play'}
|
|
230
|
+
</button>
|
|
231
|
+
<button onClick={playNext}>Next</button>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div className="playlist">
|
|
235
|
+
{playlist.map((track, index) => (
|
|
236
|
+
<button
|
|
237
|
+
key={track.id}
|
|
238
|
+
onClick={() => playTrack(index)}
|
|
239
|
+
className={index === currentTrackIndex ? 'active' : ''}
|
|
240
|
+
>
|
|
241
|
+
{track.title}
|
|
242
|
+
</button>
|
|
243
|
+
))}
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{state.error && (
|
|
247
|
+
<div className="error">Error: {state.error.message}</div>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Advanced Configuration with Headers
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
import { useHlsAudioPlayer } from '@use-hls-player/react';
|
|
258
|
+
|
|
259
|
+
export default function AuthenticatedPlayer() {
|
|
260
|
+
const { state, controls, isLoading } = useHlsAudioPlayer({
|
|
261
|
+
src: {
|
|
262
|
+
url: 'https://protected-stream.example.com/playlist.m3u8',
|
|
263
|
+
options: {
|
|
264
|
+
headers: {
|
|
265
|
+
'Authorization': 'Bearer your-token-here',
|
|
266
|
+
'User-Agent': 'MyAudioPlayer/1.0'
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
config: {
|
|
271
|
+
network: {
|
|
272
|
+
timeout: 10000,
|
|
273
|
+
retryCount: 3
|
|
274
|
+
},
|
|
275
|
+
audio: {
|
|
276
|
+
volume: 0.8,
|
|
277
|
+
preload: true
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
on: {
|
|
281
|
+
error: (error) => console.error('Player error:', error),
|
|
282
|
+
loading: () => console.log('Loading started'),
|
|
283
|
+
canplay: () => console.log('Can play')
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<div>
|
|
289
|
+
<button onClick={() => controls.play()} disabled={isLoading}>
|
|
290
|
+
{isLoading ? 'Loading...' : 'Play'}
|
|
291
|
+
</button>
|
|
292
|
+
{state.error && <div>Authentication failed: {state.error.message}</div>}
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Event Handling
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import { useState } from 'react';
|
|
302
|
+
import { useHlsAudioPlayer } from '@use-hls-player/react';
|
|
303
|
+
|
|
304
|
+
export default function EventHandlingPlayer() {
|
|
305
|
+
const [events, setEvents] = useState<string[]>([]);
|
|
306
|
+
|
|
307
|
+
const { state, controls } = useHlsAudioPlayer({
|
|
308
|
+
src: { url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8' },
|
|
309
|
+
on: {
|
|
310
|
+
play: () => setEvents(prev => [...prev, 'Playback started']),
|
|
311
|
+
pause: () => setEvents(prev => [...prev, 'Playback paused']),
|
|
312
|
+
loading: () => setEvents(prev => [...prev, 'Loading...']),
|
|
313
|
+
canplay: () => setEvents(prev => [...prev, 'Ready to play']),
|
|
314
|
+
error: (error) => setEvents(prev => [...prev, `Error: ${error.message}`]),
|
|
315
|
+
timeupdate: ({ currentTime, duration }) =>
|
|
316
|
+
setEvents(prev => [...prev, `Time: ${currentTime.toFixed(1)}s / ${duration?.toFixed(1) ?? '--'}s`]),
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<div>
|
|
322
|
+
<div className="controls">
|
|
323
|
+
<button onClick={() => controls.play()}>Play</button>
|
|
324
|
+
<button onClick={() => controls.pause()}>Pause</button>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div className="events">
|
|
328
|
+
<h4>Events:</h4>
|
|
329
|
+
{events.slice(-5).map((event, index) => (
|
|
330
|
+
<div key={index}>{event}</div>
|
|
331
|
+
))}
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Features
|
|
339
|
+
|
|
340
|
+
- **Simple Hook Interface**: Just call `useHlsAudioPlayer()` and get everything you need
|
|
341
|
+
- **Reactive State**: All player state is automatically synchronized with React
|
|
342
|
+
- **Event Handling**: Built-in support for all player events
|
|
343
|
+
- **TypeScript Support**: Full type safety and IntelliSense
|
|
344
|
+
- **Custom Headers**: Support for authenticated streams
|
|
345
|
+
- **Playlist Management**: Easy switching between multiple streams
|
|
346
|
+
- **Error Handling**: Comprehensive error reporting
|
|
347
|
+
- **Performance Optimized**: Efficient re-renders and cleanup
|
|
348
|
+
|
|
349
|
+
## Dependencies
|
|
350
|
+
|
|
351
|
+
- React 18+
|
|
352
|
+
- @hls-audio-player/core (peer dependency)
|
|
353
|
+
|
|
354
|
+
## License
|
|
355
|
+
|
|
356
|
+
Apache-2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}@media(prefers-color-scheme:light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}#root{max-width:1280px;margin:0 auto;padding:2rem;text-align:center}.logo{height:6em;padding:1.5em;will-change:filter;transition:filter .3s}.logo:hover{filter:drop-shadow(0 0 2em #646cffaa)}.logo.react:hover{filter:drop-shadow(0 0 2em #61dafbaa)}@keyframes logo-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media(prefers-reduced-motion:no-preference){a:nth-of-type(2) .logo{animation:logo-spin infinite 20s linear}}.card{padding:2em}.read-the-docs{color:#888}
|