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/package.json CHANGED
@@ -4,11 +4,16 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.1.0",
7
+ "version": "0.2.0-canary.0",
8
8
  "type": "module",
9
9
  "author": "Karelle Hofler",
10
10
  "main": "dist/index.js",
11
11
  "types": "dist/index.d.ts",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/itskarelleh/oddysee-hls-audio-player.git"
15
+ },
16
+ "license": "MIT",
12
17
  "exports": {
13
18
  ".": {
14
19
  "import": "./dist/index.js",
@@ -19,12 +24,13 @@
19
24
  "build": "tsup src/index.ts --format esm,cjs --dts --tsconfig tsconfig.lib.json",
20
25
  "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
21
26
  "lint": "eslint .",
27
+ "test": "vitest",
22
28
  "preview": "vite preview",
23
29
  "pub": "npm run build && npm publish",
24
30
  "pub:test": "npm run build && npm publish --tag test"
25
31
  },
26
32
  "dependencies": {
27
- "@hls-audio-player/core": "^0.1.0",
33
+ "oddysee-typescript": "^0.2.0-canary.0",
28
34
  "react": "^19.2.0",
29
35
  "react-dom": "^19.2.0"
30
36
  },
@@ -41,6 +47,7 @@
41
47
  "tsup": "7.2.0",
42
48
  "typescript": "~5.9.3",
43
49
  "typescript-eslint": "^8.46.4",
44
- "vite": "^7.2.4"
50
+ "vite": "^7.2.4",
51
+ "vitest": "^4.0.16"
45
52
  }
46
53
  }
@@ -1,4 +1,14 @@
1
- import { useEffect, useMemo, useState } from 'react'
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'
2
12
  import {
3
13
  HLSAudioPlayer,
4
14
  type HLSAudioPlayerInterface,
@@ -9,7 +19,8 @@ import {
9
19
  type Track,
10
20
  type QualityLevel,
11
21
  type PlayerState
12
- } from '@hls-audio-player/core'
22
+ }
23
+ from 'oddysee-typescript'
13
24
 
14
25
  // Local mirror of the core's PlayerEventMap so we don't depend on it being exported
15
26
  export type PlayerEventMap = {
@@ -43,6 +54,27 @@ export interface UseHlsAudioPlayerResult {
43
54
  loading: boolean
44
55
  error: PlayerError | null
45
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
+ }
46
78
  controls: {
47
79
  setSource: (
48
80
  url: string,
@@ -53,6 +85,10 @@ export interface UseHlsAudioPlayerResult {
53
85
  pause: () => void
54
86
  setVolume: (volume: number) => void
55
87
  setCurrentTime: (time: number) => void
88
+ beginSeek: () => void
89
+ updateSeek: (time: number) => void
90
+ commitSeek: () => void
91
+ retry: (count?: number, interval?: number) => void
56
92
  }
57
93
  }
58
94
 
@@ -87,6 +123,9 @@ export function useHlsAudioPlayer(
87
123
  const [isPlaying, setIsPlaying] = useState<boolean>(player.isPlaying ?? false)
88
124
  const [duration, setDuration] = useState<number>(player.getState()?.duration ?? 0)
89
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)
90
129
 
91
130
  useEffect(() => {
92
131
  const handleStateChange = () => {
@@ -223,10 +262,56 @@ export function useHlsAudioPlayer(
223
262
  const audioElement = player.getAudioElement()
224
263
  audioElement.currentTime = time
225
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
+ },
226
274
  }),
227
275
  [player, autoPlay],
228
276
  )
229
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
+
230
315
  return {
231
316
  player,
232
317
  state,
@@ -236,6 +321,59 @@ export function useHlsAudioPlayer(
236
321
  loading,
237
322
  error,
238
323
  readyState,
239
- controls,
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
240
378
  }
241
379
  }
@@ -1 +0,0 @@
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}