oddysee-react 0.1.0 → 0.2.0-canary.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 +14 -12
- package/__tests__/retry.test.ts +148 -0
- package/__tests__/seek.test.ts +184 -0
- package/dist/index.cjs +33399 -2
- package/dist/index.d.cts +184 -1
- package/dist/index.d.ts +184 -1
- package/dist/index.js +33404 -3
- package/package.json +10 -3
- package/src/use-hls-audio-player.ts +141 -3
- package/dist/assets/index-COcDBgFa.css +0 -1
- package/dist/assets/index-DzSVBDbV.js +0 -9
- package/dist/assets/react-CHdo91hT.svg +0 -1
- package/dist/index.html +0 -14
- package/dist/vite.svg +0 -1
- package/public/vite.svg +0 -1
package/README.md
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Oddysee React
|
|
2
2
|
|
|
3
3
|
A React hook that provides a simple, intuitive interface for HLS audio streaming using the core HLS audio player.
|
|
4
4
|
|
|
5
|
+
## ⚠️ Status: Beta
|
|
6
|
+
Oddysee is actively evolving. APIs may change, and breaking changes can occur between minor releases.
|
|
7
|
+
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
7
10
|
```bash
|
|
8
|
-
npm install
|
|
11
|
+
npm install oddysee-react
|
|
9
12
|
# or
|
|
10
|
-
yarn add
|
|
13
|
+
yarn add oddysee-react
|
|
11
14
|
# or
|
|
12
|
-
pnpm add
|
|
15
|
+
pnpm add oddysee-react
|
|
13
16
|
```
|
|
14
17
|
|
|
15
18
|
## Quick Start
|
|
16
19
|
|
|
17
20
|
```tsx
|
|
18
|
-
import { useHlsAudioPlayer } from '
|
|
21
|
+
import { useHlsAudioPlayer } from 'oddysee-react';
|
|
19
22
|
|
|
20
23
|
function AudioPlayer() {
|
|
21
24
|
const { state, controls, isLoading, isPlaying } = useHlsAudioPlayer({
|
|
@@ -129,7 +132,7 @@ interface SourceOptions {
|
|
|
129
132
|
### Basic Audio Player
|
|
130
133
|
|
|
131
134
|
```tsx
|
|
132
|
-
import { useHlsAudioPlayer } from '
|
|
135
|
+
import { useHlsAudioPlayer } from 'oddysee-react';
|
|
133
136
|
|
|
134
137
|
export default function BasicPlayer() {
|
|
135
138
|
const { state, controls, isLoading, isPlaying } = useHlsAudioPlayer({
|
|
@@ -185,7 +188,7 @@ export default function BasicPlayer() {
|
|
|
185
188
|
|
|
186
189
|
```tsx
|
|
187
190
|
import { useState } from 'react';
|
|
188
|
-
import { useHlsAudioPlayer } from '
|
|
191
|
+
import { useHlsAudioPlayer } from 'oddysee-react';
|
|
189
192
|
|
|
190
193
|
const playlist = [
|
|
191
194
|
{ id: 1, url: 'https://pl.streamingvideoprovider.com/mp3-playlist/playlist.m3u8', title: 'Music Playlist' },
|
|
@@ -254,7 +257,7 @@ export default function PlaylistPlayer() {
|
|
|
254
257
|
### Advanced Configuration with Headers
|
|
255
258
|
|
|
256
259
|
```tsx
|
|
257
|
-
import { useHlsAudioPlayer } from '
|
|
260
|
+
import { useHlsAudioPlayer } from 'oddysee-react';
|
|
258
261
|
|
|
259
262
|
export default function AuthenticatedPlayer() {
|
|
260
263
|
const { state, controls, isLoading } = useHlsAudioPlayer({
|
|
@@ -299,7 +302,7 @@ export default function AuthenticatedPlayer() {
|
|
|
299
302
|
|
|
300
303
|
```tsx
|
|
301
304
|
import { useState } from 'react';
|
|
302
|
-
import { useHlsAudioPlayer } from '
|
|
305
|
+
import { useHlsAudioPlayer } from 'oddysee-react';
|
|
303
306
|
|
|
304
307
|
export default function EventHandlingPlayer() {
|
|
305
308
|
const [events, setEvents] = useState<string[]>([]);
|
|
@@ -349,8 +352,7 @@ export default function EventHandlingPlayer() {
|
|
|
349
352
|
## Dependencies
|
|
350
353
|
|
|
351
354
|
- React 18+
|
|
352
|
-
-
|
|
355
|
+
- oddysee-typescript (peer dependency)
|
|
353
356
|
|
|
354
357
|
## License
|
|
355
|
-
|
|
356
|
-
Apache-2.0
|
|
358
|
+
MIT
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import { useHlsAudioPlayer } from '../src/use-hls-audio-player'
|
|
3
|
+
|
|
4
|
+
type HookDeps = unknown[] | undefined
|
|
5
|
+
|
|
6
|
+
const hookState: unknown[] = []
|
|
7
|
+
let hookIndex = 0
|
|
8
|
+
|
|
9
|
+
const resetHookIndex = () => {
|
|
10
|
+
hookIndex = 0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const clearHookState = () => {
|
|
14
|
+
hookState.length = 0
|
|
15
|
+
hookIndex = 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const depsEqual = (left: HookDeps, right: HookDeps) => {
|
|
19
|
+
if (!left || !right) return false
|
|
20
|
+
if (left.length !== right.length) return false
|
|
21
|
+
return left.every((dep, index) => Object.is(dep, right[index]))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
vi.mock('react', () => ({
|
|
25
|
+
useState: (initial: unknown) => {
|
|
26
|
+
const index = hookIndex++
|
|
27
|
+
if (!(index in hookState)) {
|
|
28
|
+
hookState[index] =
|
|
29
|
+
typeof initial === 'function' ? (initial as () => unknown)() : initial
|
|
30
|
+
}
|
|
31
|
+
const setState = (next: unknown) => {
|
|
32
|
+
hookState[index] =
|
|
33
|
+
typeof next === 'function' ? (next as (prev: unknown) => unknown)(hookState[index]) : next
|
|
34
|
+
}
|
|
35
|
+
return [hookState[index], setState]
|
|
36
|
+
},
|
|
37
|
+
useRef: (initial: unknown) => {
|
|
38
|
+
const index = hookIndex++
|
|
39
|
+
if (!hookState[index]) {
|
|
40
|
+
hookState[index] = { current: initial }
|
|
41
|
+
}
|
|
42
|
+
return hookState[index]
|
|
43
|
+
},
|
|
44
|
+
useMemo: (factory: () => unknown, deps?: HookDeps) => {
|
|
45
|
+
const index = hookIndex++
|
|
46
|
+
const record = hookState[index] as { deps: HookDeps; value: unknown } | undefined
|
|
47
|
+
if (record && depsEqual(record.deps, deps)) {
|
|
48
|
+
return record.value
|
|
49
|
+
}
|
|
50
|
+
const value = factory()
|
|
51
|
+
hookState[index] = { deps, value }
|
|
52
|
+
return value
|
|
53
|
+
},
|
|
54
|
+
useCallback: (callback: (...args: unknown[]) => unknown, deps?: HookDeps) => {
|
|
55
|
+
const index = hookIndex++
|
|
56
|
+
const record = hookState[index] as { deps: HookDeps; value: unknown } | undefined
|
|
57
|
+
if (record && depsEqual(record.deps, deps)) {
|
|
58
|
+
return record.value
|
|
59
|
+
}
|
|
60
|
+
hookState[index] = { deps, value: callback }
|
|
61
|
+
return callback
|
|
62
|
+
},
|
|
63
|
+
useEffect: () => {},
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
const MockHLSAudioPlayer = vi.hoisted(() => {
|
|
67
|
+
return class MockHLSAudioPlayer {
|
|
68
|
+
loading = false
|
|
69
|
+
error = null
|
|
70
|
+
readyState = 0
|
|
71
|
+
isPlaying = false
|
|
72
|
+
audioElement = { currentTime: 0 }
|
|
73
|
+
beginSeek = vi.fn()
|
|
74
|
+
updateSeek = vi.fn()
|
|
75
|
+
commitSeek = vi.fn()
|
|
76
|
+
retry = vi.fn()
|
|
77
|
+
destroy = vi.fn()
|
|
78
|
+
on = vi.fn()
|
|
79
|
+
off = vi.fn()
|
|
80
|
+
setSource = vi.fn().mockResolvedValue(this)
|
|
81
|
+
play = vi.fn().mockReturnValue(this)
|
|
82
|
+
playAsync = vi.fn().mockResolvedValue(this)
|
|
83
|
+
pause = vi.fn().mockReturnValue(this)
|
|
84
|
+
setVolume = vi.fn().mockReturnValue(this)
|
|
85
|
+
getState = vi.fn(() => ({
|
|
86
|
+
track: null,
|
|
87
|
+
currentTime: 5,
|
|
88
|
+
duration: 60,
|
|
89
|
+
volume: 1,
|
|
90
|
+
loading: false,
|
|
91
|
+
error: null,
|
|
92
|
+
readyState: 0,
|
|
93
|
+
isPlaying: false,
|
|
94
|
+
}))
|
|
95
|
+
getAudioElement = vi.fn(() => this.audioElement)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
vi.mock('oddysee-typescript', () => ({
|
|
100
|
+
HLSAudioPlayer: MockHLSAudioPlayer,
|
|
101
|
+
}))
|
|
102
|
+
|
|
103
|
+
const renderHook = () => {
|
|
104
|
+
resetHookIndex()
|
|
105
|
+
return useHlsAudioPlayer({})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const setup = () => {
|
|
109
|
+
let result = renderHook()
|
|
110
|
+
return {
|
|
111
|
+
get result() {
|
|
112
|
+
return result
|
|
113
|
+
},
|
|
114
|
+
rerender: () => {
|
|
115
|
+
result = renderHook()
|
|
116
|
+
return result
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
clearHookState()
|
|
123
|
+
vi.clearAllMocks()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
vi.clearAllMocks()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('controls.retry()', () => {
|
|
131
|
+
it('delegates to the player retry method', () => {
|
|
132
|
+
const hook = setup()
|
|
133
|
+
const player = hook.result.player as unknown as InstanceType<typeof MockHLSAudioPlayer>
|
|
134
|
+
|
|
135
|
+
hook.result.controls.retry()
|
|
136
|
+
|
|
137
|
+
expect(player.retry).toHaveBeenCalledWith(undefined, undefined)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('passes retry arguments to the player', () => {
|
|
141
|
+
const hook = setup()
|
|
142
|
+
const player = hook.result.player as unknown as InstanceType<typeof MockHLSAudioPlayer>
|
|
143
|
+
|
|
144
|
+
hook.result.controls.retry(3, 1500)
|
|
145
|
+
|
|
146
|
+
expect(player.retry).toHaveBeenCalledWith(3, 1500)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import { useHlsAudioPlayer } from '../src/use-hls-audio-player'
|
|
3
|
+
|
|
4
|
+
type HookDeps = unknown[] | undefined
|
|
5
|
+
|
|
6
|
+
const hookState: unknown[] = []
|
|
7
|
+
let hookIndex = 0
|
|
8
|
+
|
|
9
|
+
const resetHookIndex = () => {
|
|
10
|
+
hookIndex = 0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const clearHookState = () => {
|
|
14
|
+
hookState.length = 0
|
|
15
|
+
hookIndex = 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const depsEqual = (left: HookDeps, right: HookDeps) => {
|
|
19
|
+
if (!left || !right) return false
|
|
20
|
+
if (left.length !== right.length) return false
|
|
21
|
+
return left.every((dep, index) => Object.is(dep, right[index]))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
vi.mock('react', () => ({
|
|
25
|
+
useState: (initial: unknown) => {
|
|
26
|
+
const index = hookIndex++
|
|
27
|
+
if (!(index in hookState)) {
|
|
28
|
+
hookState[index] =
|
|
29
|
+
typeof initial === 'function' ? (initial as () => unknown)() : initial
|
|
30
|
+
}
|
|
31
|
+
const setState = (next: unknown) => {
|
|
32
|
+
hookState[index] =
|
|
33
|
+
typeof next === 'function' ? (next as (prev: unknown) => unknown)(hookState[index]) : next
|
|
34
|
+
}
|
|
35
|
+
return [hookState[index], setState]
|
|
36
|
+
},
|
|
37
|
+
useRef: (initial: unknown) => {
|
|
38
|
+
const index = hookIndex++
|
|
39
|
+
if (!hookState[index]) {
|
|
40
|
+
hookState[index] = { current: initial }
|
|
41
|
+
}
|
|
42
|
+
return hookState[index]
|
|
43
|
+
},
|
|
44
|
+
useMemo: (factory: () => unknown, deps?: HookDeps) => {
|
|
45
|
+
const index = hookIndex++
|
|
46
|
+
const record = hookState[index] as { deps: HookDeps; value: unknown } | undefined
|
|
47
|
+
if (record && depsEqual(record.deps, deps)) {
|
|
48
|
+
return record.value
|
|
49
|
+
}
|
|
50
|
+
const value = factory()
|
|
51
|
+
hookState[index] = { deps, value }
|
|
52
|
+
return value
|
|
53
|
+
},
|
|
54
|
+
useCallback: (callback: (...args: unknown[]) => unknown, deps?: HookDeps) => {
|
|
55
|
+
const index = hookIndex++
|
|
56
|
+
const record = hookState[index] as { deps: HookDeps; value: unknown } | undefined
|
|
57
|
+
if (record && depsEqual(record.deps, deps)) {
|
|
58
|
+
return record.value
|
|
59
|
+
}
|
|
60
|
+
hookState[index] = { deps, value: callback }
|
|
61
|
+
return callback
|
|
62
|
+
},
|
|
63
|
+
useEffect: () => {},
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
const MockHLSAudioPlayer = vi.hoisted(() => {
|
|
67
|
+
return class MockHLSAudioPlayer {
|
|
68
|
+
loading = false
|
|
69
|
+
error = null
|
|
70
|
+
readyState = 0
|
|
71
|
+
isPlaying = false
|
|
72
|
+
audioElement = { currentTime: 0 }
|
|
73
|
+
beginSeek = vi.fn()
|
|
74
|
+
updateSeek = vi.fn()
|
|
75
|
+
commitSeek = vi.fn()
|
|
76
|
+
destroy = vi.fn()
|
|
77
|
+
on = vi.fn()
|
|
78
|
+
off = vi.fn()
|
|
79
|
+
setSource = vi.fn().mockResolvedValue(this)
|
|
80
|
+
play = vi.fn().mockReturnValue(this)
|
|
81
|
+
playAsync = vi.fn().mockResolvedValue(this)
|
|
82
|
+
pause = vi.fn().mockReturnValue(this)
|
|
83
|
+
setVolume = vi.fn().mockReturnValue(this)
|
|
84
|
+
getState = vi.fn(() => ({
|
|
85
|
+
track: null,
|
|
86
|
+
currentTime: 5,
|
|
87
|
+
duration: 60,
|
|
88
|
+
volume: 1,
|
|
89
|
+
loading: false,
|
|
90
|
+
error: null,
|
|
91
|
+
readyState: 0,
|
|
92
|
+
isPlaying: false,
|
|
93
|
+
}))
|
|
94
|
+
getAudioElement = vi.fn(() => this.audioElement)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
vi.mock('oddysee-typescript', () => ({
|
|
99
|
+
HLSAudioPlayer: MockHLSAudioPlayer,
|
|
100
|
+
}))
|
|
101
|
+
|
|
102
|
+
const renderHook = () => {
|
|
103
|
+
resetHookIndex()
|
|
104
|
+
return useHlsAudioPlayer({})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const setup = () => {
|
|
108
|
+
let result = renderHook()
|
|
109
|
+
return {
|
|
110
|
+
get result() {
|
|
111
|
+
return result
|
|
112
|
+
},
|
|
113
|
+
rerender: () => {
|
|
114
|
+
result = renderHook()
|
|
115
|
+
return result
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
clearHookState()
|
|
122
|
+
vi.clearAllMocks()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
afterEach(() => {
|
|
126
|
+
vi.clearAllMocks()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('scrub.begin()', () => {
|
|
130
|
+
it('enters scrubbing mode and triggers player seek', () => {
|
|
131
|
+
const hook = setup()
|
|
132
|
+
const player = hook.result.player as unknown as InstanceType<typeof MockHLSAudioPlayer>
|
|
133
|
+
|
|
134
|
+
hook.result.scrub.begin()
|
|
135
|
+
hook.rerender()
|
|
136
|
+
|
|
137
|
+
expect(player.beginSeek).toHaveBeenCalledTimes(1)
|
|
138
|
+
expect(hook.result.scrub.isScrubbing).toBe(true)
|
|
139
|
+
expect(hook.result.scrub.displayTime).toBe(5)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('scrub.update()', () => {
|
|
144
|
+
it('updates the preview time while scrubbing', () => {
|
|
145
|
+
const hook = setup()
|
|
146
|
+
|
|
147
|
+
hook.result.scrub.begin()
|
|
148
|
+
hook.rerender()
|
|
149
|
+
|
|
150
|
+
hook.result.scrub.update(12)
|
|
151
|
+
hook.rerender()
|
|
152
|
+
|
|
153
|
+
expect(hook.result.scrub.displayTime).toBe(12)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('scrub.commit()', () => {
|
|
158
|
+
it('commits the preview time through the player', () => {
|
|
159
|
+
const hook = setup()
|
|
160
|
+
const player = hook.result.player as unknown as InstanceType<typeof MockHLSAudioPlayer>
|
|
161
|
+
|
|
162
|
+
hook.result.scrub.begin()
|
|
163
|
+
hook.rerender()
|
|
164
|
+
|
|
165
|
+
hook.result.scrub.update(12)
|
|
166
|
+
hook.result.scrub.commit()
|
|
167
|
+
hook.rerender()
|
|
168
|
+
|
|
169
|
+
expect(player.updateSeek).toHaveBeenCalledWith(12)
|
|
170
|
+
expect(player.commitSeek).toHaveBeenCalledTimes(1)
|
|
171
|
+
expect(hook.result.scrub.isScrubbing).toBe(false)
|
|
172
|
+
expect(hook.result.scrub.displayTime).toBe(5)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('does nothing when commit is called outside scrubbing', () => {
|
|
176
|
+
const hook = setup()
|
|
177
|
+
const player = hook.result.player as unknown as InstanceType<typeof MockHLSAudioPlayer>
|
|
178
|
+
|
|
179
|
+
hook.result.scrub.commit()
|
|
180
|
+
|
|
181
|
+
expect(player.updateSeek).not.toHaveBeenCalled()
|
|
182
|
+
expect(player.commitSeek).not.toHaveBeenCalled()
|
|
183
|
+
})
|
|
184
|
+
})
|