Skip to content

Commit 15b59b0

Browse files
committed
feat: improve AudioPlayer and useUserMedia
1 parent 7a6269a commit 15b59b0

File tree

5 files changed

+165
-101
lines changed

5 files changed

+165
-101
lines changed

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,36 @@
22

33
# Changelog
44

5+
## 1.4.10
6+
7+
2025-9-3
8+
9+
### Features
10+
11+
#### `AudioPlayer`
12+
13+
- ✨ audio source is extended to support `ArrayBuffer`, `Uint8Array`, and `Blob` types
14+
- ✨ add `seekForward` and `seekBackward` methods for seeking audio playback
15+
- ✨ add `seek` method for setting the current playback time
16+
17+
#### `useUserMedia`
18+
19+
- ✨ add `streamSliceMode` and `streamSliceValue` options for controlling the slicing behavior of the media stream.
20+
21+
### Notable Changes
22+
23+
#### `AudioPlayer`
24+
25+
- 👀 `getVolume` method is renamed to `volume` getter
26+
- 👀 `getCurrentTime` method is renamed to `currentTime` getter
27+
- 👀 `getDuration` method is renamed to `duration` getter
28+
- 👀 `gotoTime` method is removed
29+
- 👀 `gotoPercent` method is removed
30+
31+
#### `useUserMedia`
32+
33+
- 👀 `streamSliceMs` is removed, please use `streamSliceMode` and `streamSliceValue` instead.
34+
535
## 1.4.9
636

737
2025-9-2

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tiny-codes/react-easy",
3-
"version": "1.4.9",
3+
"version": "1.4.10",
44
"description": "Simplify React and AntDesign development with practical components and hooks",
55
"keywords": [
66
"react",

src/hooks/useUserMedia.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useContext, useEffect, useMemo, useRef, useState } from 'react';
22
import { App, Checkbox, Flex, Form, Modal, notification, Select, Typography } from 'antd';
33
import EasyConfigProvider from '../components/ConfigProvider';
44
import ReactEasyContext from '../components/ConfigProvider/context';
5-
import { StreamTimeSlicerClass } from '../utils/stream';
5+
import { StreamTimeSlicerClass, type StreamTimeSlicerOptions } from '../utils/stream';
66
import useRefFunction from './useRefFunction';
77
import useRefValue from './useRefValue';
88
import useT from './useT';
@@ -67,11 +67,17 @@ export interface UseUserMediaProps {
6767
*/
6868
disabled?: boolean;
6969
/**
70-
* - **EN:** The slicing time period (milliseconds) for each fragment of the audio and video stream,
71-
* each time slice will trigger the `onStreamChunk` callback. Default is `500`.
72-
* - **CN:** 媒体流每个分片的切片时间段(毫秒),每个时间分片会触发一次 `onStreamChunk` 回调,默认值为 `500`。
70+
* - **EN:** The slicing mode for the audio and video stream.
71+
* - **CN:** 媒体流的切片模式。
7372
*/
74-
streamSliceMs?: number;
73+
streamSliceMode?: StreamTimeSlicerOptions['sliceMode'];
74+
/**
75+
* - **EN:** The slicing value (milliseconds or bytes) for the audio and video stream, when
76+
* `streamSliceMode` is `time`, it represents milliseconds, and when it is `size`, it represents
77+
* bytes.
78+
* - **CN:** 媒体流切片的切片值(毫秒或字节),当 `streamSliceMode` 为 `time` 时表示毫秒,为 `size` 时表示字节。
79+
*/
80+
streamSliceValue?: StreamTimeSlicerOptions['value'];
7581
/**
7682
* - **EN:** The silence detection threshold (0-1) for the audio stream, below which the audio is
7783
* considered silent. Default is `0`.
@@ -92,7 +98,8 @@ const useUserMedia = (props: UseUserMediaProps): UseUserMediaResult => {
9298
media,
9399
pcmAudioOptions,
94100
disabled,
95-
streamSliceMs = 500,
101+
streamSliceMode = 'time',
102+
streamSliceValue,
96103
soundDetectionThreshold = 0,
97104
soundDetectionTimeout = 3000,
98105
onStartRecording,
@@ -132,7 +139,8 @@ const useUserMedia = (props: UseUserMediaProps): UseUserMediaResult => {
132139
const onPcmStreamChunkRef = useRefValue(onPcmStreamChunk);
133140
const pcmStreamSlicerRef = useRef(
134141
new StreamTimeSlicerClass({
135-
timeSlice: streamSliceMs,
142+
sliceMode: streamSliceMode,
143+
value: streamSliceValue || 0,
136144
onSlice: (channels) => {
137145
onPcmStreamChunkRef.current?.(channels, pcmSampleRateRef.current);
138146
},
@@ -194,8 +202,8 @@ const useUserMedia = (props: UseUserMediaProps): UseUserMediaResult => {
194202
onStreamChunk?.(event.data);
195203
}
196204
};
197-
if (streamSliceMs) {
198-
recorder.start(streamSliceMs);
205+
if (streamSliceMode === 'time' && streamSliceValue) {
206+
recorder.start(streamSliceValue);
199207
} else {
200208
recorder.start();
201209
}
@@ -437,10 +445,10 @@ const useUserMedia = (props: UseUserMediaProps): UseUserMediaResult => {
437445

438446
// Update PCM stream slicer time slice when input sample rate changes
439447
useEffect(() => {
440-
if (streamSliceMs && pcmStreamSlicerRef.current.timeSlice !== streamSliceMs) {
441-
pcmStreamSlicerRef.current.timeSlice = streamSliceMs;
448+
if (streamSliceValue && pcmStreamSlicerRef.current.value !== streamSliceValue) {
449+
pcmStreamSlicerRef.current.value = streamSliceValue;
442450
}
443-
}, [streamSliceMs]);
451+
}, [streamSliceValue]);
444452

445453
// Detect sound activity (only for audio or media with audio)
446454
useEffect(() => {

src/utils/AudioPlayer.ts

Lines changed: 86 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type AudioSource = string | ReadableStreamDefaultReader<Uint8Array>;
1+
export type AudioSource = string | ReadableStreamDefaultReader<Uint8Array> | ArrayBuffer | Uint8Array | Blob;
22

33
export interface AudioPlayerInit {
44
/**
@@ -46,7 +46,7 @@ export interface AudioPlayerInit {
4646
*/
4747
class AudioPlayer {
4848
private audio: HTMLAudioElement;
49-
private volume: number;
49+
private _volume: number;
5050
private audioContext: AudioContext | null = null;
5151
private gainNode: GainNode | null = null;
5252
private sourceNode: MediaElementAudioSourceNode | null = null;
@@ -64,8 +64,8 @@ class AudioPlayer {
6464
const { source, volume } = options || {};
6565
this.options = options;
6666
this.audio = new Audio();
67-
this.volume = volume != null ? Math.min(1.0, Math.max(0, volume)) : 0.5; // Default volume 50%
68-
this.audio.volume = this.volume;
67+
this._volume = volume != null ? Math.min(1.0, Math.max(0, volume)) : 0.5; // Default volume 50%
68+
this.audio.volume = this._volume;
6969
if (typeof source === 'function') {
7070
const result = source();
7171
if (typeof result === 'object' && 'then' in result && typeof result.then === 'function') {
@@ -89,6 +89,27 @@ class AudioPlayer {
8989
public get isPlaying() {
9090
return this.audioContext?.state === 'running';
9191
}
92+
/**
93+
* - **EN:** Get current playback time (seconds)
94+
* - **CN:** 获取当前播放时间(秒)
95+
*/
96+
get currentTime(): number {
97+
return this.audio.currentTime;
98+
}
99+
/**
100+
* - **EN:** Get total audio duration (seconds)
101+
* - **CN:** 获取音频总时长(秒)
102+
*/
103+
get duration(): number {
104+
return this.audio.duration;
105+
}
106+
/**
107+
* - **EN:** Get current volume value (0-1)
108+
* - **CN:** 获取当前音量值(0-1)
109+
*/
110+
get volume(): number {
111+
return this._volume;
112+
}
92113

93114
/**
94115
* - **EN:** Play audio. If previously paused, will resume from the pause position
@@ -112,16 +133,43 @@ class AudioPlayer {
112133
}
113134
}
114135

136+
/**
137+
* - **EN:** Seek forward by a certain number of seconds
138+
* - **CN:** 向前跳转一定秒数
139+
*
140+
* @param seconds - number of seconds to seek forward | 要向前跳转的秒数
141+
*/
142+
seekForward(seconds: number) {
143+
if (seconds < 0) {
144+
return;
145+
}
146+
if (!isNaN(this.audio.duration)) {
147+
this.audio.currentTime = Math.min(this.audio.currentTime + seconds, this.audio.duration);
148+
} else {
149+
this.audio.currentTime += seconds;
150+
}
151+
}
152+
/**
153+
* - **EN:** Seek backward by a certain number of seconds
154+
* - **CN:** 向后跳转一定秒数
155+
*
156+
* @param seconds - number of seconds to seek backward | 要向后跳转的秒数
157+
*/
158+
seekBackward(seconds: number) {
159+
if (seconds < 0) {
160+
return;
161+
}
162+
this.audio.currentTime = Math.max(this.audio.currentTime - seconds, 0);
163+
}
115164
/**
116165
* - **EN:** Set current playback time (in seconds)
117166
* - **CN:** 设置当前播放时间(以秒为单位)
118167
*
119168
* @param time - time in seconds | 时间(秒)
120169
*/
121-
gotoTime(time: number): void {
170+
seek(time: number) {
122171
// Ensure time is not less than 0
123172
const newTime = Math.max(0, time);
124-
125173
// Ensure time is not greater than duration (if known)
126174
if (!isNaN(this.audio.duration)) {
127175
this.audio.currentTime = Math.min(newTime, this.audio.duration);
@@ -130,25 +178,6 @@ class AudioPlayer {
130178
}
131179
}
132180

133-
/**
134-
* - **EN:** Set playback position by percentage
135-
* - **CN:** 按百分比设置播放位置
136-
*
137-
* @param percent - percentage (0-1) | 百分比(0-1)
138-
*/
139-
gotoPercent(percent: number): void {
140-
if (isNaN(this.audio.duration)) {
141-
return; // Can't set position if duration is unknown
142-
}
143-
144-
// Clamp percent to 0-1 range
145-
const clampedPercent = Math.min(1, Math.max(0, percent));
146-
147-
// Calculate time based on percentage
148-
const newTime = this.audio.duration * clampedPercent;
149-
this.audio.currentTime = newTime;
150-
}
151-
152181
/**
153182
* - **EN:** Pause audio playback. When played again, will continue from current position
154183
* - **CN:** 暂停音频播放 再次播放时将从当前位置继续
@@ -193,7 +222,7 @@ class AudioPlayer {
193222
* @param percent - increase percentage (default 10%) | 增加百分比(默认10%)
194223
*/
195224
volumeUp(percent = 0.1): void {
196-
this.volume = Math.min(1.0, this.volume + percent);
225+
this._volume = Math.min(1.0, this._volume + percent);
197226
this.updateVolume();
198227
}
199228

@@ -204,7 +233,7 @@ class AudioPlayer {
204233
* @param percent - decrease percentage (default 10%) | 降低百分比(默认10%)
205234
*/
206235
volumeDown(percent = 0.1): void {
207-
this.volume = Math.max(0, this.volume - percent);
236+
this._volume = Math.max(0, this._volume - percent);
208237
this.updateVolume();
209238
}
210239

@@ -215,34 +244,10 @@ class AudioPlayer {
215244
* @param value - new volume value (0-1) | 新的音量值(0-1)
216245
*/
217246
setVolume(value: number): void {
218-
this.volume = Math.min(1.0, Math.max(0, value));
247+
this._volume = Math.min(1.0, Math.max(0, value));
219248
this.updateVolume();
220249
}
221250

222-
/**
223-
* - **EN:** Get current volume value (0-1)
224-
* - **CN:** 获取当前音量值(0-1)
225-
*/
226-
getVolume(): number {
227-
return this.volume;
228-
}
229-
230-
/**
231-
* - **EN:** Get current playback time (seconds)
232-
* - **CN:** 获取当前播放时间(秒)
233-
*/
234-
getCurrentTime(): number {
235-
return this.audio.currentTime;
236-
}
237-
238-
/**
239-
* - **EN:** Get total audio duration (seconds)
240-
* - **CN:** 获取音频总时长(秒)
241-
*/
242-
getDuration(): number {
243-
return this.audio.duration;
244-
}
245-
246251
/**
247252
* - **EN:** Add audio event listener
248253
* - **CN:** 添加音频事件监听器
@@ -288,28 +293,35 @@ class AudioPlayer {
288293
}
289294

290295
/** Process streaming data source */
291-
private async handleStreamSource(reader?: ReadableStreamDefaultReader<Uint8Array>) {
292-
if (!reader) return;
296+
private async handleStreamSource(source: Exclude<AudioSource, string> | undefined) {
297+
if (!source) return;
293298
try {
294-
// Create a new ReadableStream to read data from the reader
295-
const stream = new ReadableStream({
296-
async pull(controller) {
297-
try {
298-
const { done, value } = await reader.read();
299-
if (done) {
300-
controller.close();
301-
} else {
302-
controller.enqueue(value);
299+
let blob: Blob;
300+
if (source instanceof Blob) {
301+
blob = source;
302+
} else if (source instanceof ArrayBuffer || source instanceof Uint8Array) {
303+
blob = new Blob([source]);
304+
} else {
305+
// Create a new ReadableStream to read data from the reader
306+
const stream = new ReadableStream({
307+
async pull(controller) {
308+
try {
309+
const { done, value } = await source.read();
310+
if (done) {
311+
controller.close();
312+
} else {
313+
controller.enqueue(value);
314+
}
315+
} catch (err) {
316+
controller.error(err);
303317
}
304-
} catch (err) {
305-
controller.error(err);
306-
}
307-
},
308-
});
318+
},
319+
});
320+
// Convert stream to Blob and create URL
321+
const response = new Response(stream);
322+
blob = await response.blob();
323+
}
309324

310-
// Convert stream to Blob and create URL
311-
const response = new Response(stream);
312-
const blob = await response.blob();
313325
const url = URL.createObjectURL(blob);
314326

315327
this.audio.src = url;
@@ -334,15 +346,15 @@ class AudioPlayer {
334346
this.sourceNode.connect(this.gainNode);
335347
this.gainNode.connect(this.audioContext.destination);
336348

337-
this.gainNode.gain.value = this.volume;
349+
this.gainNode.gain.value = this._volume;
338350
}
339351

340352
/** Update audio playback volume */
341353
private updateVolume(): void {
342354
if (this.gainNode) {
343-
this.gainNode.gain.value = this.volume;
355+
this.gainNode.gain.value = this._volume;
344356
} else {
345-
this.audio.volume = this.volume;
357+
this.audio.volume = this._volume;
346358
}
347359
}
348360
}

0 commit comments

Comments
 (0)