oddysee-react 0.1.1 → 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 +4 -2
- 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 +5 -3
- package/src/use-hls-audio-player.ts +141 -3
package/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
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
|
|
@@ -352,5 +355,4 @@ export default function EventHandlingPlayer() {
|
|
|
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
|
+
})
|