diff --git a/apps/common-app/package.json b/apps/common-app/package.json index 5d9b23e54..4b64c88bf 100644 --- a/apps/common-app/package.json +++ b/apps/common-app/package.json @@ -17,11 +17,11 @@ "react-native-audio-worklets": "workspace:*", "react-native-background-timer": "2.4.1", "react-native-gesture-handler": "3.1.0", - "react-native-reanimated": "4.6.0-nightly-20260812-0f8cf6dcc", + "react-native-reanimated": "4.6.0", "react-native-safe-area-context": "5.9.0", "react-native-screens": "4.27.0", "react-native-svg": "15.15.4", - "react-native-worklets": "0.12.0" + "react-native-worklets": "0.12.1" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/apps/fabric-example/ios/Podfile.lock b/apps/fabric-example/ios/Podfile.lock index f330e1426..3a40a05a1 100644 --- a/apps/fabric-example/ios/Podfile.lock +++ b/apps/fabric-example/ios/Podfile.lock @@ -1859,7 +1859,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNReanimated (4.6.0-nightly-20260812-0f8cf6dcc): + - RNReanimated (4.6.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -1881,12 +1881,12 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNReanimated/apple (= 4.6.0-nightly-20260812-0f8cf6dcc) - - RNReanimated/common (= 4.6.0-nightly-20260812-0f8cf6dcc) - - RNReanimated/view (= 4.6.0-nightly-20260812-0f8cf6dcc) + - RNReanimated/apple (= 4.6.0) + - RNReanimated/common (= 4.6.0) + - RNReanimated/view (= 4.6.0) - RNWorklets - Yoga - - RNReanimated/apple (4.6.0-nightly-20260812-0f8cf6dcc): + - RNReanimated/apple (4.6.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -1910,7 +1910,7 @@ PODS: - ReactNativeDependencies - RNWorklets - Yoga - - RNReanimated/common (4.6.0-nightly-20260812-0f8cf6dcc): + - RNReanimated/common (4.6.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -1934,7 +1934,7 @@ PODS: - ReactNativeDependencies - RNWorklets - Yoga - - RNReanimated/view (4.6.0-nightly-20260812-0f8cf6dcc): + - RNReanimated/view (4.6.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2050,7 +2050,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNWorklets (0.12.0): + - RNWorklets (0.12.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2072,10 +2072,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNWorklets/apple (= 0.12.0) - - RNWorklets/common (= 0.12.0) + - RNWorklets/apple (= 0.12.1) + - RNWorklets/common (= 0.12.1) - Yoga - - RNWorklets/apple (0.12.0): + - RNWorklets/apple (0.12.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2098,7 +2098,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNWorklets/common (0.12.0): + - RNWorklets/common (0.12.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2507,10 +2507,10 @@ SPEC CHECKSUMS: RNAudioAPI: 5fe6257beb4b9d735cb0916630067b6d2f57b22c RNAudioWorklets: e763c48efcbd47d25f61eb5bb49a72d8205b5f4e RNGestureHandler: f09eca0b47053142f7543f5c71d183f215ba5d81 - RNReanimated: eec73cef964aed3be56e059cae1e9cf8d3b42cfe + RNReanimated: eabb9b5c492b8e1f16f5523c354ab3a8770c4102 RNScreens: a2103f13fa603c405092daf70ca4eb3a6291fd6d RNSVG: 2db63ac8e69ffb8575003599f6871f9b890a9b1e - RNWorklets: 43618a5f1408b84ed727788aaa54a5a618880659 + RNWorklets: 1d81d29f943ad914556a0ad6b5f6b1c50aa844c0 SocketRocket: 37aec555668fb852ec12e3c0de59a86ca58f0871 Yoga: d1c536142c5ff8ec8cd856ab2a7c227a1d875c8e diff --git a/packages/react-native-audio-api/src/Audio/controls/AudioControls.tsx b/packages/react-native-audio-api/src/Audio/controls/AudioControls.tsx index ac9e86eb1..b49ff6024 100644 --- a/packages/react-native-audio-api/src/Audio/controls/AudioControls.tsx +++ b/packages/react-native-audio-api/src/Audio/controls/AudioControls.tsx @@ -1,7 +1,9 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, + Animated, Image, + PanResponder, Platform, Pressable, StyleSheet, @@ -9,11 +11,6 @@ import { View, } from 'react-native'; import type { LayoutChangeEvent } from 'react-native'; -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { - useAnimatedRef, - useSharedValue, -} from 'react-native-reanimated'; import { useAudioTagContext } from '../AudioTagContext'; import { isPlaybackActive } from '../utils'; import { @@ -31,10 +28,8 @@ import MuteIcon from './icons/speaker-x.png'; const TRACK_BAR_HEIGHT = 12; const TRACK_BAR_HEIGHT_PRESSED = 18; const TRACK_BAR_ANIM_MS = 150; -const SCRUB_PAN_MIN_DISTANCE = 8; const BUFFERING_SWEEP_MS = 1100; const BUFFERING_SWEEP_WIDTH_RATIO = 0.35; -// Opacity ramp faking a soft-edged gradient band const BUFFERING_SWEEP_SLICE_OPACITIES = [ 0.04, 0.18, 0.45, 0.6, 0.45, 0.18, 0.04, ]; @@ -59,103 +54,70 @@ const AudioControls: React.FC = () => { ); const [scrubTime, setScrubTime] = useState(null); - const progressTrackRef = useAnimatedRef(); - const progressMetricsWidth = useSharedValue(0); + const [trackWidth, setTrackWidth] = useState(0); const isBuffering = playbackState === 'buffering'; const bufferingSweepStyle = useBufferingSweep( isBuffering, - progressMetricsWidth, + trackWidth, BUFFERING_SWEEP_WIDTH_RATIO, BUFFERING_SWEEP_MS ); - const durationRef = useRef(duration); - durationRef.current = duration; - const onStart = useCallback( - (x: number) => { - progressTrackAnim.expand(); - const d = durationRef.current; - progressTrackRef.current?.measureInWindow( - (_left: number, _y: number, width: number, _h: number) => { - progressMetricsWidth.value = width; - setScrubTime(timeFromLocationX(x, width, d)); - } - ); - }, - [progressTrackAnim, progressMetricsWidth, setScrubTime, progressTrackRef] - ); - - const onUpdate = useCallback( - (x: number) => { - const d = durationRef.current; - const w = progressMetricsWidth.value; - setScrubTime(timeFromLocationX(x, w, d)); - }, - [progressMetricsWidth] - ); - - const seekTo = useCallback( - (x: number) => { - const d = durationRef.current; - const w = progressMetricsWidth.value; - const t = timeFromLocationX(x, w, d); - seekToTime(t); - }, - [progressMetricsWidth, seekToTime] - ); - - const onEnd = useCallback( - (x: number) => { - seekTo(x); - progressTrackAnim.collapse(); - setScrubTime(null); - }, - [progressTrackAnim, seekTo] - ); - - const onCancel = useCallback(() => { - progressTrackAnim.collapse(); - setScrubTime(null); - }, [progressTrackAnim]); + const scrub = useRef({ + startX: 0, + trackWidth: 0, + duration, + seekToTime, + expand: progressTrackAnim.expand, + collapse: progressTrackAnim.collapse, + }); + scrub.current.duration = duration; + scrub.current.seekToTime = seekToTime; + scrub.current.expand = progressTrackAnim.expand; + scrub.current.collapse = progressTrackAnim.collapse; - const onTapSeek = useCallback( - (x: number) => { - onEnd(x); - }, - [onEnd] + const panResponder = useMemo( + () => + PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onPanResponderTerminationRequest: () => false, + onPanResponderGrant: (event) => { + const s = scrub.current; + s.startX = event.nativeEvent.locationX; + s.expand(); + setScrubTime(timeFromLocationX(s.startX, s.trackWidth, s.duration)); + }, + onPanResponderMove: (_event, gestureState) => { + const s = scrub.current; + setScrubTime( + timeFromLocationX( + s.startX + gestureState.dx, + s.trackWidth, + s.duration + ) + ); + }, + onPanResponderRelease: (_event, gestureState) => { + const s = scrub.current; + s.seekToTime( + timeFromLocationX( + s.startX + gestureState.dx, + s.trackWidth, + s.duration + ) + ); + s.collapse(); + setScrubTime(null); + }, + onPanResponderTerminate: () => { + scrub.current.collapse(); + setScrubTime(null); + }, + }), + [] ); - const scrubGesture = useMemo(() => { - const panGesture = Gesture.Pan() - .runOnJS(true) - .minDistance(SCRUB_PAN_MIN_DISTANCE) - .onStart((e) => { - onStart(e.x); - }) - .onUpdate((e) => { - onUpdate(e.x); - }) - .onEnd((e) => { - onEnd(e.x); - }) - .onFinalize((_e, success) => { - if (!success) { - onCancel(); - } - }); - - const tapGesture = Gesture.Tap() - .runOnJS(true) - .maxDistance(14) - .onEnd((e, success) => { - if (success) { - onTapSeek(e.x); - } - }); - - return Gesture.Race(panGesture, tapGesture); - }, [onStart, onUpdate, onEnd, onCancel, onTapSeek]); - const onPlayPausePress = useCallback(() => { if (isPlaybackActive(playbackState)) { pause(); @@ -164,12 +126,11 @@ const AudioControls: React.FC = () => { } }, [playbackState, pause, play]); - const onProgressTrackLayout = useCallback( - (event: LayoutChangeEvent) => { - progressMetricsWidth.value = event.nativeEvent.layout.width; - }, - [progressMetricsWidth] - ); + const onProgressTrackLayout = useCallback((event: LayoutChangeEvent) => { + const { width } = event.nativeEvent.layout; + scrub.current.trackWidth = width; + setTrackWidth(width); + }, []); if (!ready) { return ( @@ -187,9 +148,9 @@ const AudioControls: React.FC = () => { {isPlaybackActive(playbackState) ? ( - + ) : ( - + )} @@ -197,40 +158,34 @@ const AudioControls: React.FC = () => { {formatTime(displayTime)} / {formatTime(duration)} - - {/* prettier-ignore */} - - - - {isBuffering && ( - - {BUFFERING_SWEEP_SLICE_OPACITIES.map((opacity, index) => ( - - ))} - - )} - - - + {/* prettier-ignore */} + + + + {isBuffering && ( + + {BUFFERING_SWEEP_SLICE_OPACITIES.map((opacity, index) => ( + + ))} + + )} + + setMuted(!muted)}> {muted ? ( - + ) : ( - + )} @@ -265,6 +220,10 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', }, + icon: { + width: 24, + height: 24, + }, playPause: { padding: 4, marginRight: 12, diff --git a/packages/react-native-audio-api/src/Audio/controls/audioControlUtils.ts b/packages/react-native-audio-api/src/Audio/controls/audioControlUtils.ts index fbc7aecc2..d85a7dc9e 100644 --- a/packages/react-native-audio-api/src/Audio/controls/audioControlUtils.ts +++ b/packages/react-native-audio-api/src/Audio/controls/audioControlUtils.ts @@ -1,13 +1,5 @@ -import { useEffect, useMemo } from 'react'; -import { - cancelAnimation, - Easing, - useSharedValue, - useAnimatedStyle, - withRepeat, - withTiming, -} from 'react-native-reanimated'; -import type { SharedValue } from 'react-native-reanimated'; +import { useEffect, useMemo, useRef } from 'react'; +import { Animated, Easing } from 'react-native'; export function formatTime(seconds: number): string { if (!Number.isFinite(seconds) || seconds < 0) { @@ -40,83 +32,68 @@ export function useExpandableTrackHeight( trackBarHeightPressed: number, trackBarAnimMs: number ) { - const height = useSharedValue(trackBarHeight); - const animatedStyle = useAnimatedStyle(() => ({ - height: height.value, - borderRadius: height.value / 2, - })); + const height = useRef(new Animated.Value(trackBarHeight)).current; - return useMemo( - () => ({ - animatedStyle, - expand: () => { - height.value = withTiming(trackBarHeightPressed, { - duration: trackBarAnimMs, - }); - }, - collapse: () => { - height.value = withTiming(trackBarHeight, { - duration: trackBarAnimMs, - }); + return useMemo(() => { + const animateTo = (toValue: number) => { + Animated.timing(height, { + toValue, + duration: trackBarAnimMs, + useNativeDriver: false, + }).start(); + }; + + return { + animatedStyle: { + height, + borderRadius: Animated.divide(height, 2), }, - }), - [ - animatedStyle, - height, - trackBarHeight, - trackBarHeightPressed, - trackBarAnimMs, - ] - ); + expand: () => animateTo(trackBarHeightPressed), + collapse: () => animateTo(trackBarHeight), + }; + }, [height, trackBarHeight, trackBarHeightPressed, trackBarAnimMs]); } -/** - * Indeterminate buffering sweep: a highlight that repeatedly travels the width - * of the progress track while playback is stalled. It says "still waiting", not - * "this much is buffered" — there is no buffered-ahead figure behind it. - * - * `trackWidth` is a shared value because the track is measured on layout, after - * the first render. - */ export function useBufferingSweep( isBuffering: boolean, - trackWidth: SharedValue, + trackWidth: number, sweepWidthRatio: number, sweepDurationMs: number ) { - const sweepProgress = useSharedValue(0); + const sweepProgress = useRef(new Animated.Value(0)).current; useEffect(() => { if (!isBuffering) { return; } - sweepProgress.value = withRepeat( - withTiming(1, { + const sweep = Animated.loop( + Animated.timing(sweepProgress, { + toValue: 1, duration: sweepDurationMs, - easing: Easing.inOut(Easing.quad), - }), - -1, - false + easing: Easing.inOut((t) => Easing.quad(t)), + useNativeDriver: true, + }) ); + sweep.start(); return () => { - cancelAnimation(sweepProgress); - sweepProgress.value = 0; + sweep.stop(); + sweepProgress.setValue(0); }; }, [isBuffering, sweepDurationMs, sweepProgress]); - return useAnimatedStyle(() => { - const sweepWidth = trackWidth.value * sweepWidthRatio; - // Travel the track plus the sweep's own width, starting one width off the - // left edge, so it slides fully in and fully out instead of popping. - const travelDistance = trackWidth.value + sweepWidth; + const sweepWidth = trackWidth * sweepWidthRatio; - return { - width: sweepWidth, - transform: [ - { translateX: -sweepWidth + sweepProgress.value * travelDistance }, - ], - }; - }); + return { + width: sweepWidth, + transform: [ + { + translateX: sweepProgress.interpolate({ + inputRange: [0, 1], + outputRange: [-sweepWidth, trackWidth], + }), + }, + ], + }; } diff --git a/packages/react-native-audio-worklets/package.json b/packages/react-native-audio-worklets/package.json index 5bdcd3814..c76059a25 100644 --- a/packages/react-native-audio-worklets/package.json +++ b/packages/react-native-audio-worklets/package.json @@ -113,7 +113,7 @@ "react-native": "0.87.0", "react-native-audio-api": "workspace:*", "react-native-builder-bob": "0.33.1", - "react-native-worklets": "0.12.0", + "react-native-worklets": "0.12.1", "typescript": "~6.0.3" }, "react-native-builder-bob": { diff --git a/yarn.lock b/yarn.lock index 8bd5f55e0..488342900 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5516,11 +5516,11 @@ __metadata: react-native-audio-worklets: "workspace:*" react-native-background-timer: "npm:2.4.1" react-native-gesture-handler: "npm:3.1.0" - react-native-reanimated: "npm:4.6.0-nightly-20260812-0f8cf6dcc" + react-native-reanimated: "npm:4.6.0" react-native-safe-area-context: "npm:5.9.0" react-native-screens: "npm:4.27.0" react-native-svg: "npm:15.15.4" - react-native-worklets: "npm:0.12.0" + react-native-worklets: "npm:0.12.1" react-test-renderer: "npm:19.2.3" typescript: "npm:~6.0.3" peerDependencies: @@ -11988,7 +11988,7 @@ __metadata: react-native: "npm:0.87.0" react-native-audio-api: "workspace:*" react-native-builder-bob: "npm:0.33.1" - react-native-worklets: "npm:0.12.0" + react-native-worklets: "npm:0.12.1" semver: "npm:^7.7.3" typescript: "npm:~6.0.3" peerDependencies: @@ -12063,17 +12063,17 @@ __metadata: languageName: node linkType: hard -"react-native-reanimated@npm:4.6.0-nightly-20260812-0f8cf6dcc": - version: 4.6.0-nightly-20260812-0f8cf6dcc - resolution: "react-native-reanimated@npm:4.6.0-nightly-20260812-0f8cf6dcc" +"react-native-reanimated@npm:4.6.0": + version: 4.6.0 + resolution: "react-native-reanimated@npm:4.6.0" dependencies: react-native-is-edge-to-edge: "npm:^1.3.1" semver: "npm:^7.7.3" peerDependencies: react: "*" react-native: 0.83 - 0.87 - react-native-worklets: "*" - checksum: 10/cf0d5469a7778e0d96946fe0e2351ab74ccdb4714a4628d6d3f8bab449f2df1252be09db281a991a2d32190c0e4f7ffefc935815edc9d9902f7ff442dd3c6e42 + react-native-worklets: 0.12.x + checksum: 10/195b968876fcc0b83705c606b6b0f3fe7e55a9a066260c1e59367836cfcbd3b5ff6e0328ff47b1ed610b01a2696b7a2706c4245d6d03ec6cb9ff98af38249ff4 languageName: node linkType: hard @@ -12142,9 +12142,9 @@ __metadata: languageName: node linkType: hard -"react-native-worklets@npm:0.12.0": - version: 0.12.0 - resolution: "react-native-worklets@npm:0.12.0" +"react-native-worklets@npm:0.12.1": + version: 0.12.1 + resolution: "react-native-worklets@npm:0.12.1" dependencies: "@babel/generator": "npm:^7.27.1" "@babel/plugin-transform-arrow-functions": "npm:^7.27.1" @@ -12165,7 +12165,7 @@ __metadata: "@react-native/metro-config": "*" react: "*" react-native: 0.83 - 0.87 - checksum: 10/258ad007eee94f70cc1805e451797172d09605676e4abdef2d6a5fad38173ea64d2a5c2fe5ae55c7a9d61c5759b9a918dcfcb368840b00d439842fd44d1862b2 + checksum: 10/df5368ab370a93355c653befb4fda80307aa68858a05d242f374fe3d453d1fddd64bc1eb217a06fac44d730f1fb629e4b9806f9a611a7ed1253e9eae483dea43 languageName: node linkType: hard