oddysee-react 0.2.0-canary.0 → 0.2.0-canary.1
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/package.json +8 -3
- package/__tests__/retry.test.ts +0 -148
- package/__tests__/seek.test.ts +0 -184
- package/eslint.config.js +0 -23
- package/src/index.ts +0 -2
- package/src/use-hls-audio-player.ts +0 -379
- package/tsconfig.app.json +0 -28
- package/tsconfig.json +0 -7
- package/tsconfig.lib.json +0 -16
- package/tsconfig.node.json +0 -26
- package/vite.config.ts +0 -7
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.2.0-canary.
|
|
7
|
+
"version": "0.2.0-canary.1",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"author": "Karelle Hofler",
|
|
10
10
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"pub:test": "npm run build && npm publish --tag test"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"oddysee-typescript": "^0.2.0-canary.
|
|
33
|
+
"oddysee-typescript": "^0.2.0-canary.1",
|
|
34
34
|
"react": "^19.2.0",
|
|
35
35
|
"react-dom": "^19.2.0"
|
|
36
36
|
},
|
|
@@ -49,5 +49,10 @@
|
|
|
49
49
|
"typescript-eslint": "^8.46.4",
|
|
50
50
|
"vite": "^7.2.4",
|
|
51
51
|
"vitest": "^4.0.16"
|
|
52
|
-
}
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist",
|
|
55
|
+
"README.md",
|
|
56
|
+
"LICENSE"
|
|
57
|
+
]
|
|
53
58
|
}
|
package/__tests__/retry.test.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
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
|
-
})
|
package/__tests__/seek.test.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
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
|
-
})
|
package/eslint.config.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import js from '@eslint/js'
|
|
2
|
-
import globals from 'globals'
|
|
3
|
-
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
-
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
-
import tseslint from 'typescript-eslint'
|
|
6
|
-
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
7
|
-
|
|
8
|
-
export default defineConfig([
|
|
9
|
-
globalIgnores(['dist']),
|
|
10
|
-
{
|
|
11
|
-
files: ['**/*.{ts,tsx}'],
|
|
12
|
-
extends: [
|
|
13
|
-
js.configs.recommended,
|
|
14
|
-
tseslint.configs.recommended,
|
|
15
|
-
reactHooks.configs.flat.recommended,
|
|
16
|
-
reactRefresh.configs.vite,
|
|
17
|
-
],
|
|
18
|
-
languageOptions: {
|
|
19
|
-
ecmaVersion: 2020,
|
|
20
|
-
globals: globals.browser,
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
])
|
package/src/index.ts
DELETED
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useCallback,
|
|
3
|
-
useEffect,
|
|
4
|
-
useMemo,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
type ChangeEvent,
|
|
8
|
-
type PointerEvent,
|
|
9
|
-
type MouseEvent,
|
|
10
|
-
type TouchEvent,
|
|
11
|
-
} from 'react'
|
|
12
|
-
import {
|
|
13
|
-
HLSAudioPlayer,
|
|
14
|
-
type HLSAudioPlayerInterface,
|
|
15
|
-
type PlayerConfig,
|
|
16
|
-
type SourceOptions,
|
|
17
|
-
type PlayerEvent,
|
|
18
|
-
type PlayerError,
|
|
19
|
-
type Track,
|
|
20
|
-
type QualityLevel,
|
|
21
|
-
type PlayerState
|
|
22
|
-
}
|
|
23
|
-
from 'oddysee-typescript'
|
|
24
|
-
|
|
25
|
-
// Local mirror of the core's PlayerEventMap so we don't depend on it being exported
|
|
26
|
-
export type PlayerEventMap = {
|
|
27
|
-
play: void
|
|
28
|
-
pause: void
|
|
29
|
-
'track-end': Track | null
|
|
30
|
-
error: PlayerError
|
|
31
|
-
'quality-change': QualityLevel
|
|
32
|
-
'playlist-ready': void
|
|
33
|
-
loadedmetadata: Track | null
|
|
34
|
-
timeupdate: { currentTime: number; duration: number | null }
|
|
35
|
-
loading: void
|
|
36
|
-
canplay: void
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface UseHlsAudioPlayerOptions {
|
|
40
|
-
config?: PlayerConfig
|
|
41
|
-
src?: { url: string; options?: SourceOptions }
|
|
42
|
-
autoPlay?: boolean
|
|
43
|
-
on?: Partial<{
|
|
44
|
-
[K in PlayerEvent]: (data: PlayerEventMap[K]) => void
|
|
45
|
-
}>
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface UseHlsAudioPlayerResult {
|
|
49
|
-
player: HLSAudioPlayerInterface | null
|
|
50
|
-
state: PlayerState
|
|
51
|
-
isPlaying: boolean
|
|
52
|
-
duration: number
|
|
53
|
-
isLoading: boolean
|
|
54
|
-
loading: boolean
|
|
55
|
-
error: PlayerError | null
|
|
56
|
-
readyState: number
|
|
57
|
-
scrub: {
|
|
58
|
-
isScrubbing: boolean
|
|
59
|
-
displayTime: number
|
|
60
|
-
begin: () => void
|
|
61
|
-
update: (time: number) => void
|
|
62
|
-
commit: (time?: number) => void
|
|
63
|
-
}
|
|
64
|
-
seekBar: {
|
|
65
|
-
isScrubbing: boolean
|
|
66
|
-
displayTime: number
|
|
67
|
-
onChange: (event: ChangeEvent<HTMLInputElement>) => void
|
|
68
|
-
onPointerDown: () => void
|
|
69
|
-
onPointerUp: (event: PointerEvent<HTMLInputElement>) => void
|
|
70
|
-
onPointerCancel: () => void
|
|
71
|
-
onMouseDown: () => void
|
|
72
|
-
onMouseUp: (event: MouseEvent<HTMLInputElement>) => void
|
|
73
|
-
onTouchStart: () => void
|
|
74
|
-
onTouchEnd: (event: TouchEvent<HTMLInputElement>) => void
|
|
75
|
-
onFocus: () => void
|
|
76
|
-
onBlur: () => void
|
|
77
|
-
}
|
|
78
|
-
controls: {
|
|
79
|
-
setSource: (
|
|
80
|
-
url: string,
|
|
81
|
-
options?: SourceOptions,
|
|
82
|
-
) => Promise<HLSAudioPlayerInterface | null>
|
|
83
|
-
play: () => void
|
|
84
|
-
playAsync: () => Promise<HLSAudioPlayerInterface | null>
|
|
85
|
-
pause: () => void
|
|
86
|
-
setVolume: (volume: number) => void
|
|
87
|
-
setCurrentTime: (time: number) => void
|
|
88
|
-
beginSeek: () => void
|
|
89
|
-
updateSeek: (time: number) => void
|
|
90
|
-
commitSeek: () => void
|
|
91
|
-
retry: (count?: number, interval?: number) => void
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const defaultState: PlayerState = {
|
|
96
|
-
track: null,
|
|
97
|
-
currentTime: 0,
|
|
98
|
-
duration: null,
|
|
99
|
-
volume: 1,
|
|
100
|
-
loading: false,
|
|
101
|
-
error: null,
|
|
102
|
-
readyState: 0,
|
|
103
|
-
isPlaying: false,
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function useHlsAudioPlayer(
|
|
107
|
-
options: UseHlsAudioPlayerOptions = {},
|
|
108
|
-
): UseHlsAudioPlayerResult {
|
|
109
|
-
const { config, src, autoPlay, on } = options
|
|
110
|
-
|
|
111
|
-
const player = useMemo(() => {
|
|
112
|
-
return new HLSAudioPlayer(config)
|
|
113
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
114
|
-
}, [])
|
|
115
|
-
|
|
116
|
-
const [state, setState] = useState<PlayerState>(
|
|
117
|
-
() => player.getState() ?? defaultState,
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
const [loading, setLoading] = useState<boolean>(player.loading ?? false)
|
|
121
|
-
const [error, setError] = useState<PlayerError | null>(player.error ?? null)
|
|
122
|
-
const [readyState, setReadyState] = useState<number>(player.readyState ?? 0)
|
|
123
|
-
const [isPlaying, setIsPlaying] = useState<boolean>(player.isPlaying ?? false)
|
|
124
|
-
const [duration, setDuration] = useState<number>(player.getState()?.duration ?? 0)
|
|
125
|
-
const [isLoading, setIsLoading] = useState<boolean>(player.loading ?? false)
|
|
126
|
-
const [isScrubbing, setIsScrubbing] = useState(false)
|
|
127
|
-
const [scrubTime, setScrubTime] = useState(0)
|
|
128
|
-
const scrubTimeRef = useRef(0)
|
|
129
|
-
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
const handleStateChange = () => {
|
|
132
|
-
const next = player.getState()
|
|
133
|
-
setState(next)
|
|
134
|
-
setLoading(next.loading)
|
|
135
|
-
setError(next.error)
|
|
136
|
-
setReadyState(next.readyState)
|
|
137
|
-
setIsPlaying(next.isPlaying)
|
|
138
|
-
setDuration(next.duration ?? 0)
|
|
139
|
-
setIsLoading(next.loading)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const listeners: { [K in PlayerEvent]?: (data: PlayerEventMap[K]) => void } = {
|
|
143
|
-
play: data => {
|
|
144
|
-
handleStateChange()
|
|
145
|
-
on?.play?.(data as PlayerEventMap['play'])
|
|
146
|
-
},
|
|
147
|
-
pause: data => {
|
|
148
|
-
handleStateChange()
|
|
149
|
-
on?.pause?.(data as PlayerEventMap['pause'])
|
|
150
|
-
},
|
|
151
|
-
'track-end': data => {
|
|
152
|
-
handleStateChange()
|
|
153
|
-
on?.['track-end']?.(data as PlayerEventMap['track-end'])
|
|
154
|
-
},
|
|
155
|
-
error: data => {
|
|
156
|
-
handleStateChange()
|
|
157
|
-
on?.error?.(data as PlayerEventMap['error'])
|
|
158
|
-
},
|
|
159
|
-
'quality-change': data => {
|
|
160
|
-
handleStateChange()
|
|
161
|
-
on?.['quality-change']?.(data as PlayerEventMap['quality-change'])
|
|
162
|
-
},
|
|
163
|
-
'playlist-ready': data => {
|
|
164
|
-
handleStateChange()
|
|
165
|
-
on?.['playlist-ready']?.(data as PlayerEventMap['playlist-ready'])
|
|
166
|
-
},
|
|
167
|
-
loadedmetadata: data => {
|
|
168
|
-
handleStateChange()
|
|
169
|
-
on?.loadedmetadata?.(data as PlayerEventMap['loadedmetadata'])
|
|
170
|
-
},
|
|
171
|
-
timeupdate: data => {
|
|
172
|
-
handleStateChange()
|
|
173
|
-
on?.timeupdate?.(data as PlayerEventMap['timeupdate'])
|
|
174
|
-
},
|
|
175
|
-
loading: data => {
|
|
176
|
-
handleStateChange()
|
|
177
|
-
on?.loading?.(data as PlayerEventMap['loading'])
|
|
178
|
-
},
|
|
179
|
-
canplay: data => {
|
|
180
|
-
handleStateChange()
|
|
181
|
-
on?.canplay?.(data as PlayerEventMap['canplay'])
|
|
182
|
-
},
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
;(Object.keys(listeners) as PlayerEvent[]).forEach(event => {
|
|
186
|
-
const handler = listeners[event]!
|
|
187
|
-
player.on(event, handler as any)
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
return () => {
|
|
191
|
-
;(Object.keys(listeners) as PlayerEvent[]).forEach(event => {
|
|
192
|
-
const handler = listeners[event]
|
|
193
|
-
if (handler) {
|
|
194
|
-
player.off(event, handler as any)
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
}
|
|
198
|
-
}, [player, on])
|
|
199
|
-
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
let cancelled = false
|
|
202
|
-
|
|
203
|
-
if (!src) return
|
|
204
|
-
|
|
205
|
-
player
|
|
206
|
-
.setSource(src.url, src.options)
|
|
207
|
-
.then((p: HLSAudioPlayerInterface) => {
|
|
208
|
-
if (cancelled) return
|
|
209
|
-
if (autoPlay) {
|
|
210
|
-
p.play()
|
|
211
|
-
}
|
|
212
|
-
})
|
|
213
|
-
.catch((err: unknown) => {
|
|
214
|
-
if (cancelled) return
|
|
215
|
-
console.error('Failed to set HLS source', err)
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
return () => {
|
|
219
|
-
cancelled = true
|
|
220
|
-
}
|
|
221
|
-
}, [player, src?.url, src?.options, autoPlay])
|
|
222
|
-
|
|
223
|
-
useEffect(() => {
|
|
224
|
-
return () => {
|
|
225
|
-
player.destroy()
|
|
226
|
-
}
|
|
227
|
-
}, [player])
|
|
228
|
-
|
|
229
|
-
const controls = useMemo(
|
|
230
|
-
() => ({
|
|
231
|
-
setSource: async (url: string, options?: SourceOptions) => {
|
|
232
|
-
try {
|
|
233
|
-
const p = await player.setSource(url, options)
|
|
234
|
-
if (autoPlay) {
|
|
235
|
-
p.play()
|
|
236
|
-
}
|
|
237
|
-
return p
|
|
238
|
-
} catch {
|
|
239
|
-
return null
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
play: () => {
|
|
243
|
-
player.play()
|
|
244
|
-
},
|
|
245
|
-
playAsync: async () => {
|
|
246
|
-
try {
|
|
247
|
-
const p = await player.playAsync()
|
|
248
|
-
return p
|
|
249
|
-
} catch {
|
|
250
|
-
return null
|
|
251
|
-
}
|
|
252
|
-
},
|
|
253
|
-
pause: () => {
|
|
254
|
-
player.pause()
|
|
255
|
-
},
|
|
256
|
-
setVolume: (volume: number) => {
|
|
257
|
-
player.setVolume(volume)
|
|
258
|
-
const next = player.getState()
|
|
259
|
-
setState(next)
|
|
260
|
-
},
|
|
261
|
-
setCurrentTime: (time: number) => {
|
|
262
|
-
const audioElement = player.getAudioElement()
|
|
263
|
-
audioElement.currentTime = time
|
|
264
|
-
},
|
|
265
|
-
beginSeek:() =>
|
|
266
|
-
player.beginSeek(),
|
|
267
|
-
updateSeek: (time: number) =>
|
|
268
|
-
player.updateSeek(time),
|
|
269
|
-
commitSeek: () =>
|
|
270
|
-
player.commitSeek(),
|
|
271
|
-
retry: (count?: number, interval?: number) => {
|
|
272
|
-
player.retry(count, interval)
|
|
273
|
-
},
|
|
274
|
-
}),
|
|
275
|
-
[player, autoPlay],
|
|
276
|
-
)
|
|
277
|
-
|
|
278
|
-
const beginScrub = useCallback(() => {
|
|
279
|
-
if (!state.duration) return
|
|
280
|
-
setIsScrubbing(true)
|
|
281
|
-
scrubTimeRef.current = state.currentTime
|
|
282
|
-
setScrubTime(state.currentTime)
|
|
283
|
-
player.beginSeek()
|
|
284
|
-
}, [player, state.currentTime, state.duration])
|
|
285
|
-
|
|
286
|
-
const updateScrub = useCallback((time: number) => {
|
|
287
|
-
scrubTimeRef.current = time
|
|
288
|
-
setScrubTime(time)
|
|
289
|
-
}, [])
|
|
290
|
-
|
|
291
|
-
const commitScrub = useCallback(
|
|
292
|
-
(time?: number) => {
|
|
293
|
-
if (!isScrubbing) return
|
|
294
|
-
if (typeof time === 'number' && !Number.isNaN(time)) {
|
|
295
|
-
scrubTimeRef.current = time
|
|
296
|
-
setScrubTime(time)
|
|
297
|
-
}
|
|
298
|
-
setIsScrubbing(false)
|
|
299
|
-
player.updateSeek(scrubTimeRef.current)
|
|
300
|
-
player.commitSeek()
|
|
301
|
-
},
|
|
302
|
-
[player, isScrubbing],
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
const commitScrubFromElement = useCallback(
|
|
306
|
-
(element: HTMLInputElement) => {
|
|
307
|
-
requestAnimationFrame(() => {
|
|
308
|
-
const nextTime = Number(element.value)
|
|
309
|
-
commitScrub(nextTime)
|
|
310
|
-
})
|
|
311
|
-
},
|
|
312
|
-
[commitScrub],
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
player,
|
|
317
|
-
state,
|
|
318
|
-
isPlaying,
|
|
319
|
-
duration,
|
|
320
|
-
isLoading,
|
|
321
|
-
loading,
|
|
322
|
-
error,
|
|
323
|
-
readyState,
|
|
324
|
-
seekBar: {
|
|
325
|
-
isScrubbing,
|
|
326
|
-
displayTime: isScrubbing ? scrubTime : state.currentTime,
|
|
327
|
-
onChange: (event: ChangeEvent<HTMLInputElement>) => {
|
|
328
|
-
const nextTime = Number(event.target.value)
|
|
329
|
-
if (!isScrubbing) {
|
|
330
|
-
beginScrub()
|
|
331
|
-
}
|
|
332
|
-
updateScrub(nextTime)
|
|
333
|
-
},
|
|
334
|
-
onPointerDown: () => {
|
|
335
|
-
if (!isScrubbing) {
|
|
336
|
-
beginScrub()
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
onPointerUp: (event: PointerEvent<HTMLInputElement>) => {
|
|
340
|
-
commitScrubFromElement(event.currentTarget)
|
|
341
|
-
},
|
|
342
|
-
onPointerCancel: () => {
|
|
343
|
-
commitScrub()
|
|
344
|
-
},
|
|
345
|
-
onMouseDown: () => {
|
|
346
|
-
if (!isScrubbing) {
|
|
347
|
-
beginScrub()
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
onMouseUp: (event: MouseEvent<HTMLInputElement>) => {
|
|
351
|
-
commitScrubFromElement(event.currentTarget)
|
|
352
|
-
},
|
|
353
|
-
onTouchStart: () => {
|
|
354
|
-
if (!isScrubbing) {
|
|
355
|
-
beginScrub()
|
|
356
|
-
}
|
|
357
|
-
},
|
|
358
|
-
onTouchEnd: (event: TouchEvent<HTMLInputElement>) => {
|
|
359
|
-
commitScrubFromElement(event.currentTarget)
|
|
360
|
-
},
|
|
361
|
-
onFocus: () => {
|
|
362
|
-
if (!isScrubbing) {
|
|
363
|
-
beginScrub()
|
|
364
|
-
}
|
|
365
|
-
},
|
|
366
|
-
onBlur: () => {
|
|
367
|
-
commitScrub()
|
|
368
|
-
},
|
|
369
|
-
},
|
|
370
|
-
scrub: {
|
|
371
|
-
isScrubbing,
|
|
372
|
-
displayTime: isScrubbing ? scrubTime : state.currentTime,
|
|
373
|
-
begin: beginScrub,
|
|
374
|
-
update: updateScrub,
|
|
375
|
-
commit: commitScrub,
|
|
376
|
-
},
|
|
377
|
-
controls
|
|
378
|
-
}
|
|
379
|
-
}
|
package/tsconfig.app.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
-
"target": "ES2022",
|
|
5
|
-
"useDefineForClassFields": true,
|
|
6
|
-
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
-
"module": "ESNext",
|
|
8
|
-
"types": ["vite/client"],
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
|
|
11
|
-
/* Bundler mode */
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"allowImportingTsExtensions": true,
|
|
14
|
-
"verbatimModuleSyntax": true,
|
|
15
|
-
"moduleDetection": "force",
|
|
16
|
-
"noEmit": true,
|
|
17
|
-
"jsx": "react-jsx",
|
|
18
|
-
|
|
19
|
-
/* Linting */
|
|
20
|
-
"strict": true,
|
|
21
|
-
"noUnusedLocals": true,
|
|
22
|
-
"noUnusedParameters": true,
|
|
23
|
-
"erasableSyntaxOnly": true,
|
|
24
|
-
"noFallthroughCasesInSwitch": true,
|
|
25
|
-
"noUncheckedSideEffectImports": true
|
|
26
|
-
},
|
|
27
|
-
"include": ["src"]
|
|
28
|
-
}
|
package/tsconfig.json
DELETED
package/tsconfig.lib.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "node",
|
|
6
|
-
"esModuleInterop": true,
|
|
7
|
-
"strict": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"forceConsistentCasingInFileNames": true,
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"declarationMap": true,
|
|
12
|
-
"jsx": "react-jsx"
|
|
13
|
-
},
|
|
14
|
-
"include": ["src/index.ts", "src/use-hls-audio-player.ts"],
|
|
15
|
-
"exclude": ["dist", "node_modules"]
|
|
16
|
-
}
|
package/tsconfig.node.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
-
"target": "ES2023",
|
|
5
|
-
"lib": ["ES2023"],
|
|
6
|
-
"module": "ESNext",
|
|
7
|
-
"types": ["node"],
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
|
|
10
|
-
/* Bundler mode */
|
|
11
|
-
"moduleResolution": "bundler",
|
|
12
|
-
"allowImportingTsExtensions": true,
|
|
13
|
-
"verbatimModuleSyntax": true,
|
|
14
|
-
"moduleDetection": "force",
|
|
15
|
-
"noEmit": true,
|
|
16
|
-
|
|
17
|
-
/* Linting */
|
|
18
|
-
"strict": true,
|
|
19
|
-
"noUnusedLocals": true,
|
|
20
|
-
"noUnusedParameters": true,
|
|
21
|
-
"erasableSyntaxOnly": true,
|
|
22
|
-
"noFallthroughCasesInSwitch": true,
|
|
23
|
-
"noUncheckedSideEffectImports": true
|
|
24
|
-
},
|
|
25
|
-
"include": ["vite.config.ts"]
|
|
26
|
-
}
|