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 CHANGED
@@ -1,21 +1,24 @@
1
- # @use-hls-player/react
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 @use-hls-player/react
11
+ npm install oddysee-react
9
12
  # or
10
- yarn add @use-hls-player/react
13
+ yarn add oddysee-react
11
14
  # or
12
- pnpm add @use-hls-player/react
15
+ pnpm add oddysee-react
13
16
  ```
14
17
 
15
18
  ## Quick Start
16
19
 
17
20
  ```tsx
18
- import { useHlsAudioPlayer } from '@use-hls-player/react';
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 '@use-hls-player/react';
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 '@use-hls-player/react';
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 '@use-hls-player/react';
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 '@use-hls-player/react';
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
- - @hls-audio-player/core (peer dependency)
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
+ })