react-native-audio-api 0.13.3 → 0.13.5

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.
Files changed (96) hide show
  1. package/android/src/main/java/com/swmansion/audioapi/system/AudioEvent.kt +1 -0
  2. package/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.cpp +8 -2
  3. package/common/cpp/audioapi/HostObjects/sources/AudioFileSourceNodeHostObject.h +1 -0
  4. package/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp +2 -0
  5. package/common/cpp/audioapi/core/sources/AudioFileSourceNode.cpp +15 -1
  6. package/common/cpp/audioapi/core/sources/AudioFileSourceNode.h +19 -0
  7. package/common/cpp/audioapi/core/sources/RecorderAdapterNode.cpp +12 -1
  8. package/common/cpp/audioapi/core/sources/RecorderAdapterNode.h +6 -0
  9. package/common/cpp/audioapi/core/utils/param/ParamQueueBase.hpp +1 -1
  10. package/common/cpp/audioapi/core/utils/param/ParamRenderQueue.cpp +35 -12
  11. package/common/cpp/audioapi/core/utils/param/ParamRenderQueue.h +9 -0
  12. package/common/cpp/audioapi/core/utils/worklets/SafeIncludes.h +3 -3
  13. package/common/cpp/audioapi/core/utils/worklets/WorkletsRunner.cpp +2 -2
  14. package/common/cpp/audioapi/events/AudioEvent.h +1 -0
  15. package/common/cpp/audioapi/events/AudioEventPayload.h +11 -0
  16. package/common/cpp/audioapi/events/AudioEventPayloadMapping.h +1 -0
  17. package/common/cpp/audioapi/utils/events/BufferingStateDispatcher.cpp +49 -0
  18. package/common/cpp/audioapi/utils/events/BufferingStateDispatcher.h +42 -0
  19. package/ios/audioapi/ios/core/IOSAudioRecorder.h +12 -1
  20. package/ios/audioapi/ios/core/IOSAudioRecorder.mm +195 -72
  21. package/ios/audioapi/ios/core/NativeAudioRecorder.h +3 -1
  22. package/ios/audioapi/ios/core/NativeAudioRecorder.m +41 -14
  23. package/ios/audioapi/ios/core/utils/FileOptions.mm +18 -5
  24. package/ios/audioapi/ios/core/utils/IOSFileWriter.h +8 -0
  25. package/ios/audioapi/ios/core/utils/IOSFileWriter.mm +55 -8
  26. package/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.h +3 -0
  27. package/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.mm +16 -0
  28. package/ios/audioapi/ios/system/AudioEngine.h +2 -1
  29. package/ios/audioapi/ios/system/AudioEngine.mm +49 -23
  30. package/ios/audioapi/ios/system/SystemNotificationManager.mm +10 -0
  31. package/lib/commonjs/Audio/Audio.js +30 -3
  32. package/lib/commonjs/Audio/Audio.js.map +1 -1
  33. package/lib/commonjs/Audio/Audio.web.js +21 -3
  34. package/lib/commonjs/Audio/Audio.web.js.map +1 -1
  35. package/lib/commonjs/Audio/AudioFileSourceNode.js +20 -0
  36. package/lib/commonjs/Audio/AudioFileSourceNode.js.map +1 -1
  37. package/lib/commonjs/Audio/controls/AudioControls.js +90 -94
  38. package/lib/commonjs/Audio/controls/AudioControls.js.map +1 -1
  39. package/lib/commonjs/Audio/controls/audioControlUtils.js +48 -18
  40. package/lib/commonjs/Audio/controls/audioControlUtils.js.map +1 -1
  41. package/lib/commonjs/Audio/index.js +7 -0
  42. package/lib/commonjs/Audio/index.js.map +1 -1
  43. package/lib/commonjs/Audio/utils.js +16 -5
  44. package/lib/commonjs/Audio/utils.js.map +1 -1
  45. package/lib/commonjs/AudioAPIModule/AudioAPIModule.js +1 -1
  46. package/lib/commonjs/core/AudioDecoder.js +4 -1
  47. package/lib/commonjs/core/AudioDecoder.js.map +1 -1
  48. package/lib/module/Audio/Audio.js +30 -3
  49. package/lib/module/Audio/Audio.js.map +1 -1
  50. package/lib/module/Audio/Audio.web.js +21 -3
  51. package/lib/module/Audio/Audio.web.js.map +1 -1
  52. package/lib/module/Audio/AudioFileSourceNode.js +20 -0
  53. package/lib/module/Audio/AudioFileSourceNode.js.map +1 -1
  54. package/lib/module/Audio/controls/AudioControls.js +92 -96
  55. package/lib/module/Audio/controls/AudioControls.js.map +1 -1
  56. package/lib/module/Audio/controls/audioControlUtils.js +48 -19
  57. package/lib/module/Audio/controls/audioControlUtils.js.map +1 -1
  58. package/lib/module/Audio/index.js +1 -0
  59. package/lib/module/Audio/index.js.map +1 -1
  60. package/lib/module/Audio/utils.js +15 -5
  61. package/lib/module/Audio/utils.js.map +1 -1
  62. package/lib/module/AudioAPIModule/AudioAPIModule.js +1 -1
  63. package/lib/module/core/AudioDecoder.js +4 -1
  64. package/lib/module/core/AudioDecoder.js.map +1 -1
  65. package/lib/typescript/Audio/Audio.d.ts.map +1 -1
  66. package/lib/typescript/Audio/Audio.web.d.ts.map +1 -1
  67. package/lib/typescript/Audio/AudioFileSourceNode.d.ts +2 -0
  68. package/lib/typescript/Audio/AudioFileSourceNode.d.ts.map +1 -1
  69. package/lib/typescript/Audio/controls/AudioControls.d.ts.map +1 -1
  70. package/lib/typescript/Audio/controls/audioControlUtils.d.ts +11 -4
  71. package/lib/typescript/Audio/controls/audioControlUtils.d.ts.map +1 -1
  72. package/lib/typescript/Audio/index.d.ts +1 -0
  73. package/lib/typescript/Audio/index.d.ts.map +1 -1
  74. package/lib/typescript/Audio/types.d.ts +15 -1
  75. package/lib/typescript/Audio/types.d.ts.map +1 -1
  76. package/lib/typescript/Audio/utils.d.ts +2 -1
  77. package/lib/typescript/Audio/utils.d.ts.map +1 -1
  78. package/lib/typescript/core/AudioDecoder.d.ts.map +1 -1
  79. package/lib/typescript/events/types.d.ts +5 -0
  80. package/lib/typescript/events/types.d.ts.map +1 -1
  81. package/lib/typescript/jsi-interfaces.d.ts +1 -0
  82. package/lib/typescript/jsi-interfaces.d.ts.map +1 -1
  83. package/package.json +8 -8
  84. package/scripts/validate-worklets-version.js +1 -1
  85. package/src/Audio/Audio.tsx +38 -2
  86. package/src/Audio/Audio.web.tsx +26 -2
  87. package/src/Audio/AudioFileSourceNode.ts +28 -0
  88. package/src/Audio/controls/AudioControls.tsx +117 -116
  89. package/src/Audio/controls/audioControlUtils.ts +63 -31
  90. package/src/Audio/index.ts +1 -0
  91. package/src/Audio/types.ts +15 -1
  92. package/src/Audio/utils.ts +20 -2
  93. package/src/AudioAPIModule/AudioAPIModule.ts +1 -1
  94. package/src/core/AudioDecoder.ts +5 -3
  95. package/src/events/types.ts +6 -0
  96. package/src/jsi-interfaces.ts +1 -0
@@ -39,6 +39,10 @@ export class AudioFileSourceNode extends AudioScheduledSourceNode {
39
39
  }
40
40
 
41
41
  play(): void {
42
+ if (!(this.node as IAudioFileSourceNode).routedThroughMediaElement) {
43
+ this.connect(this.context.destination);
44
+ }
45
+ // copied from audioscheduledsourcenode, so it can bypass requirement of being started only once
42
46
  (this.node as IAudioScheduledSourceNode).start(this.context.currentTime);
43
47
  }
44
48
 
@@ -103,9 +107,33 @@ export class AudioFileSourceNode extends AudioScheduledSourceNode {
103
107
  }
104
108
  }
105
109
 
110
+ startBufferingTracking(
111
+ onBufferingChange: (buffering: boolean) => void
112
+ ): void {
113
+ if (!this.node) {
114
+ return;
115
+ }
116
+ this.stopBufferingTracking();
117
+ const sub = this.emitter.addAudioEventListener(
118
+ 'bufferingStateChanged',
119
+ (event) => {
120
+ onBufferingChange(event.value);
121
+ }
122
+ );
123
+ (this.node as IAudioFileSourceNode).onBufferingStateChanged =
124
+ sub.subscriptionId;
125
+ }
126
+
127
+ stopBufferingTracking(): void {
128
+ if (this.node) {
129
+ (this.node as IAudioFileSourceNode).onBufferingStateChanged = '0';
130
+ }
131
+ }
132
+
106
133
  private resetNodeAndSubscriptions(): void {
107
134
  if (this.node) {
108
135
  (this.node as IAudioFileSourceNode).onPositionChanged = '0';
136
+ (this.node as IAudioFileSourceNode).onBufferingStateChanged = '0';
109
137
  (this.node as IAudioFileSourceNode).onEnded = '0';
110
138
  this.node.disconnect(undefined);
111
139
  }
@@ -1,22 +1,22 @@
1
1
  import React, { useCallback, useMemo, useRef, useState } from 'react';
2
2
  import {
3
3
  ActivityIndicator,
4
+ Animated,
4
5
  Image,
6
+ PanResponder,
5
7
  Platform,
6
8
  Pressable,
7
9
  StyleSheet,
8
10
  Text,
9
11
  View,
10
12
  } from 'react-native';
11
- import { Gesture, GestureDetector } from 'react-native-gesture-handler';
12
- import Animated, {
13
- useAnimatedRef,
14
- useSharedValue,
15
- } from 'react-native-reanimated';
13
+ import type { LayoutChangeEvent } from 'react-native';
16
14
  import { useAudioTagContext } from '../AudioTagContext';
15
+ import { isPlaybackActive } from '../utils';
17
16
  import {
18
17
  formatTime,
19
18
  timeFromLocationX,
19
+ useBufferingSweep,
20
20
  useExpandableTrackHeight,
21
21
  } from './audioControlUtils';
22
22
 
@@ -28,7 +28,11 @@ import MuteIcon from './icons/speaker-x.png';
28
28
  const TRACK_BAR_HEIGHT = 12;
29
29
  const TRACK_BAR_HEIGHT_PRESSED = 18;
30
30
  const TRACK_BAR_ANIM_MS = 150;
31
- const SCRUB_PAN_MIN_DISTANCE = 8;
31
+ const BUFFERING_SWEEP_MS = 1100;
32
+ const BUFFERING_SWEEP_WIDTH_RATIO = 0.35;
33
+ const BUFFERING_SWEEP_SLICE_OPACITIES = [
34
+ 0.04, 0.18, 0.45, 0.6, 0.45, 0.18, 0.04,
35
+ ];
32
36
 
33
37
  const AudioControls: React.FC = () => {
34
38
  const {
@@ -50,107 +54,83 @@ const AudioControls: React.FC = () => {
50
54
  );
51
55
 
52
56
  const [scrubTime, setScrubTime] = useState<number | null>(null);
53
- const progressTrackRef = useAnimatedRef<View>();
54
- const progressMetricsWidth = useSharedValue(0);
55
- const durationRef = useRef(duration);
56
- durationRef.current = duration;
57
-
58
- const onStart = useCallback(
59
- (x: number) => {
60
- progressTrackAnim.expand();
61
- const d = durationRef.current;
62
- progressTrackRef.current?.measureInWindow((_left, _y, width, _h) => {
63
- progressMetricsWidth.value = width;
64
- setScrubTime(timeFromLocationX(x, width, d));
65
- });
66
- },
67
- [progressTrackAnim, progressMetricsWidth, setScrubTime, progressTrackRef]
68
- );
69
-
70
- const onUpdate = useCallback(
71
- (x: number) => {
72
- const d = durationRef.current;
73
- const w = progressMetricsWidth.value;
74
- setScrubTime(timeFromLocationX(x, w, d));
75
- },
76
- [progressMetricsWidth]
77
- );
78
-
79
- const seekTo = useCallback(
80
- (x: number) => {
81
- const d = durationRef.current;
82
- const w = progressMetricsWidth.value;
83
- const t = timeFromLocationX(x, w, d);
84
- seekToTime(t);
85
- },
86
- [progressMetricsWidth, seekToTime]
57
+ const [trackWidth, setTrackWidth] = useState(0);
58
+ const isBuffering = playbackState === 'buffering';
59
+ const bufferingSweepStyle = useBufferingSweep(
60
+ isBuffering,
61
+ trackWidth,
62
+ BUFFERING_SWEEP_WIDTH_RATIO,
63
+ BUFFERING_SWEEP_MS
87
64
  );
88
65
 
89
- const onEnd = useCallback(
90
- (x: number) => {
91
- seekTo(x);
92
- progressTrackAnim.collapse();
93
- setScrubTime(null);
94
- },
95
- [progressTrackAnim, seekTo]
96
- );
97
-
98
- const onCancel = useCallback(() => {
99
- progressTrackAnim.collapse();
100
- setScrubTime(null);
101
- }, [progressTrackAnim]);
66
+ const scrub = useRef({
67
+ startX: 0,
68
+ trackWidth: 0,
69
+ duration,
70
+ seekToTime,
71
+ expand: progressTrackAnim.expand,
72
+ collapse: progressTrackAnim.collapse,
73
+ });
74
+ scrub.current.duration = duration;
75
+ scrub.current.seekToTime = seekToTime;
76
+ scrub.current.expand = progressTrackAnim.expand;
77
+ scrub.current.collapse = progressTrackAnim.collapse;
102
78
 
103
- const onTapSeek = useCallback(
104
- (x: number) => {
105
- onEnd(x);
106
- },
107
- [onEnd]
79
+ const panResponder = useMemo(
80
+ () =>
81
+ PanResponder.create({
82
+ onStartShouldSetPanResponder: () => true,
83
+ onMoveShouldSetPanResponder: () => true,
84
+ onPanResponderTerminationRequest: () => false,
85
+ onPanResponderGrant: (event) => {
86
+ const s = scrub.current;
87
+ s.startX = event.nativeEvent.locationX;
88
+ s.expand();
89
+ setScrubTime(timeFromLocationX(s.startX, s.trackWidth, s.duration));
90
+ },
91
+ onPanResponderMove: (_event, gestureState) => {
92
+ const s = scrub.current;
93
+ setScrubTime(
94
+ timeFromLocationX(
95
+ s.startX + gestureState.dx,
96
+ s.trackWidth,
97
+ s.duration
98
+ )
99
+ );
100
+ },
101
+ onPanResponderRelease: (_event, gestureState) => {
102
+ const s = scrub.current;
103
+ s.seekToTime(
104
+ timeFromLocationX(
105
+ s.startX + gestureState.dx,
106
+ s.trackWidth,
107
+ s.duration
108
+ )
109
+ );
110
+ s.collapse();
111
+ setScrubTime(null);
112
+ },
113
+ onPanResponderTerminate: () => {
114
+ scrub.current.collapse();
115
+ setScrubTime(null);
116
+ },
117
+ }),
118
+ []
108
119
  );
109
120
 
110
- const scrubGesture = useMemo(() => {
111
- const panGesture = Gesture.Pan()
112
- .runOnJS(true)
113
- .minDistance(SCRUB_PAN_MIN_DISTANCE)
114
- .onStart((e) => {
115
- onStart(e.x);
116
- })
117
- .onUpdate((e) => {
118
- onUpdate(e.x);
119
- })
120
- .onEnd((e) => {
121
- onEnd(e.x);
122
- })
123
- .onFinalize((_e, success) => {
124
- if (!success) {
125
- onCancel();
126
- }
127
- });
128
-
129
- const tapGesture = Gesture.Tap()
130
- .runOnJS(true)
131
- .maxDistance(14)
132
- .onEnd((e, success) => {
133
- if (success) {
134
- onTapSeek(e.x);
135
- }
136
- });
137
-
138
- return Gesture.Race(panGesture, tapGesture);
139
- }, [onStart, onUpdate, onEnd, onCancel, onTapSeek]);
140
-
141
121
  const onPlayPausePress = useCallback(() => {
142
- if (playbackState === 'playing') {
122
+ if (isPlaybackActive(playbackState)) {
143
123
  pause();
144
124
  } else {
145
125
  play();
146
126
  }
147
127
  }, [playbackState, pause, play]);
148
128
 
149
- const onProgressTrackLayout = useCallback(() => {
150
- progressTrackRef.current?.measureInWindow((_left, _y, width, _h) => {
151
- progressMetricsWidth.value = width;
152
- });
153
- }, [progressMetricsWidth, progressTrackRef]);
129
+ const onProgressTrackLayout = useCallback((event: LayoutChangeEvent) => {
130
+ const { width } = event.nativeEvent.layout;
131
+ scrub.current.trackWidth = width;
132
+ setTrackWidth(width);
133
+ }, []);
154
134
 
155
135
  if (!ready) {
156
136
  return (
@@ -167,10 +147,10 @@ const AudioControls: React.FC = () => {
167
147
  <View style={styles.container}>
168
148
  <View style={styles.topRow}>
169
149
  <Pressable style={styles.playPause} onPress={onPlayPausePress}>
170
- {playbackState === 'playing' ? (
171
- <Image source={PauseIcon} style={{ width: 24, height: 24 }} />
150
+ {isPlaybackActive(playbackState) ? (
151
+ <Image source={PauseIcon} style={styles.icon} />
172
152
  ) : (
173
- <Image source={PlayIcon} style={{ width: 24, height: 24 }} />
153
+ <Image source={PlayIcon} style={styles.icon} />
174
154
  )}
175
155
  </Pressable>
176
156
 
@@ -178,28 +158,34 @@ const AudioControls: React.FC = () => {
178
158
  {formatTime(displayTime)} / {formatTime(duration)}
179
159
  </Text>
180
160
 
181
- <GestureDetector gesture={scrubGesture}>
182
- {/* prettier-ignore */}
183
- <View
184
- ref={progressTrackRef}
185
- onLayout={onProgressTrackLayout}
186
- style={styles.progressTrack}
187
- collapsable={false}>
188
- <Animated.View
189
- style={[styles.trackInner, progressTrackAnim.animatedStyle]}
190
- >
191
- <View
192
- style={[styles.trackFill, { width: `${progress * 100}%` }]}
193
- />
194
- </Animated.View>
195
- </View>
196
- </GestureDetector>
161
+ {/* prettier-ignore */}
162
+ <View
163
+ onLayout={onProgressTrackLayout}
164
+ style={styles.progressTrack}
165
+ {...panResponder.panHandlers}>
166
+ <Animated.View
167
+ style={[styles.trackInner, progressTrackAnim.animatedStyle]}>
168
+ <View style={[styles.trackFill, { width: `${progress * 100}%` }]} />
169
+ {isBuffering && (
170
+ <Animated.View
171
+ pointerEvents="none"
172
+ style={[styles.bufferingSweep, bufferingSweepStyle]}>
173
+ {BUFFERING_SWEEP_SLICE_OPACITIES.map((opacity, index) => (
174
+ <View
175
+ key={index}
176
+ style={[styles.bufferingSweepSlice, { opacity }]}
177
+ />
178
+ ))}
179
+ </Animated.View>
180
+ )}
181
+ </Animated.View>
182
+ </View>
197
183
 
198
184
  <Pressable style={styles.volumeIcon} onPress={() => setMuted(!muted)}>
199
185
  {muted ? (
200
- <Image source={MuteIcon} style={{ width: 24, height: 24 }} />
186
+ <Image source={MuteIcon} style={styles.icon} />
201
187
  ) : (
202
- <Image source={VolumeIcon} style={{ width: 24, height: 24 }} />
188
+ <Image source={VolumeIcon} style={styles.icon} />
203
189
  )}
204
190
  </Pressable>
205
191
  </View>
@@ -234,6 +220,10 @@ const styles = StyleSheet.create({
234
220
  flexDirection: 'row',
235
221
  alignItems: 'center',
236
222
  },
223
+ icon: {
224
+ width: 24,
225
+ height: 24,
226
+ },
237
227
  playPause: {
238
228
  padding: 4,
239
229
  marginRight: 12,
@@ -259,6 +249,17 @@ const styles = StyleSheet.create({
259
249
  height: '100%',
260
250
  backgroundColor: '#000',
261
251
  },
252
+ bufferingSweep: {
253
+ position: 'absolute',
254
+ top: 0,
255
+ bottom: 0,
256
+ left: 0,
257
+ flexDirection: 'row',
258
+ },
259
+ bufferingSweepSlice: {
260
+ flex: 1,
261
+ backgroundColor: '#000',
262
+ },
262
263
  volumeIcon: {
263
264
  padding: 4,
264
265
  marginRight: 12,
@@ -1,9 +1,5 @@
1
- import { useMemo } from 'react';
2
- import {
3
- useSharedValue,
4
- useAnimatedStyle,
5
- withTiming,
6
- } from 'react-native-reanimated';
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { Animated, Easing } from 'react-native';
7
3
 
8
4
  export function formatTime(seconds: number): string {
9
5
  if (!Number.isFinite(seconds) || seconds < 0) {
@@ -36,32 +32,68 @@ export function useExpandableTrackHeight(
36
32
  trackBarHeightPressed: number,
37
33
  trackBarAnimMs: number
38
34
  ) {
39
- const height = useSharedValue(trackBarHeight);
40
- const animatedStyle = useAnimatedStyle(() => ({
41
- height: height.value,
42
- borderRadius: height.value / 2,
43
- }));
35
+ const height = useRef(new Animated.Value(trackBarHeight)).current;
44
36
 
45
- return useMemo(
46
- () => ({
47
- animatedStyle,
48
- expand: () => {
49
- height.value = withTiming(trackBarHeightPressed, {
50
- duration: trackBarAnimMs,
51
- });
37
+ return useMemo(() => {
38
+ const animateTo = (toValue: number) => {
39
+ Animated.timing(height, {
40
+ toValue,
41
+ duration: trackBarAnimMs,
42
+ useNativeDriver: false,
43
+ }).start();
44
+ };
45
+
46
+ return {
47
+ animatedStyle: {
48
+ height,
49
+ borderRadius: Animated.divide(height, 2),
52
50
  },
53
- collapse: () => {
54
- height.value = withTiming(trackBarHeight, {
55
- duration: trackBarAnimMs,
56
- });
51
+ expand: () => animateTo(trackBarHeightPressed),
52
+ collapse: () => animateTo(trackBarHeight),
53
+ };
54
+ }, [height, trackBarHeight, trackBarHeightPressed, trackBarAnimMs]);
55
+ }
56
+
57
+ export function useBufferingSweep(
58
+ isBuffering: boolean,
59
+ trackWidth: number,
60
+ sweepWidthRatio: number,
61
+ sweepDurationMs: number
62
+ ) {
63
+ const sweepProgress = useRef(new Animated.Value(0)).current;
64
+
65
+ useEffect(() => {
66
+ if (!isBuffering) {
67
+ return;
68
+ }
69
+
70
+ const sweep = Animated.loop(
71
+ Animated.timing(sweepProgress, {
72
+ toValue: 1,
73
+ duration: sweepDurationMs,
74
+ easing: Easing.inOut((t) => Easing.quad(t)),
75
+ useNativeDriver: true,
76
+ })
77
+ );
78
+ sweep.start();
79
+
80
+ return () => {
81
+ sweep.stop();
82
+ sweepProgress.setValue(0);
83
+ };
84
+ }, [isBuffering, sweepDurationMs, sweepProgress]);
85
+
86
+ const sweepWidth = trackWidth * sweepWidthRatio;
87
+
88
+ return {
89
+ width: sweepWidth,
90
+ transform: [
91
+ {
92
+ translateX: sweepProgress.interpolate({
93
+ inputRange: [0, 1],
94
+ outputRange: [-sweepWidth, trackWidth],
95
+ }),
57
96
  },
58
- }),
59
- [
60
- animatedStyle,
61
- height,
62
- trackBarHeight,
63
- trackBarHeightPressed,
64
- trackBarAnimMs,
65
- ]
66
- );
97
+ ],
98
+ };
67
99
  }
@@ -1,5 +1,6 @@
1
1
  export { default } from './Audio';
2
2
  export { useAudioTagContext } from './AudioTagContext';
3
+ export { isPlaybackActive } from './utils';
3
4
 
4
5
  export type {
5
6
  AudioTagHandle,
@@ -23,7 +23,11 @@ export type AudioSource = AudioURISource | AudioRequireSource | string;
23
23
 
24
24
  export type PreloadType = 'auto' | 'metadata' | 'none';
25
25
 
26
- export type AudioTagPlaybackState = 'idle' | 'playing' | 'paused';
26
+ /**
27
+ * `'buffering'` is a sub-state of `'playing'` — stalled on decoded data. See
28
+ * `onWaiting`/`onPlaying`.
29
+ */
30
+ export type AudioTagPlaybackState = 'idle' | 'playing' | 'paused' | 'buffering';
27
31
 
28
32
  export interface AudioTagHandle {
29
33
  play: () => void;
@@ -83,6 +87,16 @@ interface AudioEventProps {
83
87
  onPlay: TMPEmptyEventHandler;
84
88
  onPause: TMPEmptyEventHandler;
85
89
  onVolumeChange: TMPNumberEventHandler;
90
+ /**
91
+ * Fires on a genuine stall (not a deliberate pause) — mirrors HTML media's
92
+ * `waiting`.
93
+ */
94
+ onWaiting: TMPEmptyEventHandler;
95
+ /**
96
+ * Fires when playback resumes after `onWaiting` — mirrors HTML media's
97
+ * `playing`.
98
+ */
99
+ onPlaying: TMPEmptyEventHandler;
86
100
  }
87
101
 
88
102
  export interface AudioPropsBase
@@ -2,7 +2,16 @@ import { useMemo } from 'react';
2
2
  import { Image, Platform } from 'react-native';
3
3
  import AudioContext from '../core/AudioContext';
4
4
  import type BaseAudioContext from '../core/BaseAudioContext';
5
- import { AudioProps, AudioPropsBase, AudioSource } from './types';
5
+ import {
6
+ AudioProps,
7
+ AudioPropsBase,
8
+ AudioSource,
9
+ AudioTagPlaybackState,
10
+ } from './types';
11
+
12
+ export function isPlaybackActive(state: AudioTagPlaybackState): boolean {
13
+ return state === 'playing' || state === 'buffering';
14
+ }
6
15
 
7
16
  const noop = () => {};
8
17
  const noopError = (_error: Error) => {};
@@ -38,6 +47,8 @@ export function withPropsDefaults(
38
47
  onPlay: props.onPlay ?? noop,
39
48
  onPause: props.onPause ?? noop,
40
49
  onVolumeChange: props.onVolumeChange ?? noopNumber,
50
+ onWaiting: props.onWaiting ?? noop,
51
+ onPlaying: props.onPlaying ?? noop,
41
52
  };
42
53
  }
43
54
 
@@ -72,6 +83,8 @@ export function useStableAudioProps(props: AudioProps): AudioPropsBase {
72
83
  onPlay,
73
84
  onPause,
74
85
  onVolumeChange,
86
+ onWaiting,
87
+ onPlaying,
75
88
  } = withPropsDefaults(props, resolvedContext);
76
89
 
77
90
  return useMemo(
@@ -98,6 +111,8 @@ export function useStableAudioProps(props: AudioProps): AudioPropsBase {
98
111
  onPlay,
99
112
  onPause,
100
113
  onVolumeChange,
114
+ onWaiting,
115
+ onPlaying,
101
116
  }),
102
117
  [
103
118
  autoPlay,
@@ -119,6 +134,8 @@ export function useStableAudioProps(props: AudioProps): AudioPropsBase {
119
134
  onPlay,
120
135
  onPause,
121
136
  onVolumeChange,
137
+ onWaiting,
138
+ onPlaying,
122
139
  ]
123
140
  );
124
141
  }
@@ -129,7 +146,8 @@ export function resolveSourcePath(source: AudioSource): string {
129
146
  }
130
147
 
131
148
  if (typeof source === 'number') {
132
- return Image.resolveAssetSource(source).uri;
149
+ const uri = Image.resolveAssetSource(source)?.uri;
150
+ return uri ?? '';
133
151
  }
134
152
 
135
153
  return source.uri ?? '';
@@ -12,7 +12,7 @@ class AudioAPIModule implements IAudioAPIModule {
12
12
  #canUseWorklets_ = false;
13
13
  #workletsVersion = 'unknown';
14
14
  #workletsAvailable_ = false;
15
- private static readonly MIN_WORKLETS_VERSION = '0.6.0';
15
+ private static readonly MIN_WORKLETS_VERSION = '0.7.0';
16
16
 
17
17
  constructor() {
18
18
  // Important! Verify and import worklets first
@@ -55,9 +55,11 @@ class AudioDecoder {
55
55
  }
56
56
 
57
57
  private resolveStringSource(input: number | string): string | number {
58
- return typeof input === 'number'
59
- ? Image.resolveAssetSource(input).uri
60
- : input;
58
+ if (typeof input !== 'number') {
59
+ return input;
60
+ }
61
+
62
+ return Image.resolveAssetSource(input)?.uri ?? '';
61
63
  }
62
64
 
63
65
  private assertSupportedStringSource(
@@ -7,6 +7,10 @@ export interface EventTypeWithValue {
7
7
  value: number;
8
8
  }
9
9
 
10
+ export interface EventTypeWithBool {
11
+ value: boolean;
12
+ }
13
+
10
14
  export type InterruptionType = 'began' | 'ended';
11
15
 
12
16
  export interface OnInterruptionEventType {
@@ -75,6 +79,8 @@ interface AudioAPIEvents {
75
79
  positionChanged: EventTypeWithValue;
76
80
  bufferEnded: OnBufferEndEventType;
77
81
  recorderError: OnRecorderErrorEventType;
82
+ /** `value` is true while an `<Audio>` source is stalled on decoded data. */
83
+ bufferingStateChanged: EventTypeWithBool;
78
84
  }
79
85
 
80
86
  type AudioEvents = SystemEvents & AudioAPIEvents & NotificationEvents;
@@ -247,6 +247,7 @@ export interface IAudioFileSourceNode extends IAudioScheduledSourceNode {
247
247
 
248
248
  // passing subscriptionId(uint_64 in cpp, string in js) to the cpp
249
249
  onPositionChanged: string;
250
+ onBufferingStateChanged: string;
250
251
  }
251
252
 
252
253
  export interface IMediaElementAudioSourceNode extends IAudioNode {}