react-native-audio-api
Version:
react-native-audio-api provides system for controlling audio in React Native environment compatible with Web Audio API specification
237 lines (236 loc) • 6.87 kB
JavaScript
"use strict";
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { ActivityIndicator, Image, Platform, Pressable, StyleSheet, Text, View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useAnimatedRef, useSharedValue } from 'react-native-reanimated';
import { useAudioTagContext } from "../AudioTagContext.js";
import { formatTime, timeFromLocationX, useExpandableTrackHeight } from "./audioControlUtils.js";
import PlayIcon from './icons/play.png';
import PauseIcon from './icons/pause.png';
import VolumeIcon from './icons/speaker.png';
import MuteIcon from './icons/speaker-x.png';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const TRACK_BAR_HEIGHT = 12;
const TRACK_BAR_HEIGHT_PRESSED = 18;
const TRACK_BAR_ANIM_MS = 150;
const SCRUB_PAN_MIN_DISTANCE = 8;
const AudioControls = () => {
const {
ready,
play,
pause,
seekToTime,
playbackState,
muted,
setMuted,
currentTime,
duration
} = useAudioTagContext();
const progressTrackAnim = useExpandableTrackHeight(TRACK_BAR_HEIGHT, TRACK_BAR_HEIGHT_PRESSED, TRACK_BAR_ANIM_MS);
const [scrubTime, setScrubTime] = useState(null);
const progressTrackRef = useAnimatedRef();
const progressMetricsWidth = useSharedValue(0);
const durationRef = useRef(duration);
durationRef.current = duration;
const onStart = useCallback(x => {
progressTrackAnim.expand();
const d = durationRef.current;
progressTrackRef.current?.measureInWindow((_left, _y, width, _h) => {
progressMetricsWidth.value = width;
setScrubTime(timeFromLocationX(x, width, d));
});
}, [progressTrackAnim, progressMetricsWidth, setScrubTime, progressTrackRef]);
const onUpdate = useCallback(x => {
const d = durationRef.current;
const w = progressMetricsWidth.value;
setScrubTime(timeFromLocationX(x, w, d));
}, [progressMetricsWidth]);
const seekTo = useCallback(x => {
const d = durationRef.current;
const w = progressMetricsWidth.value;
const t = timeFromLocationX(x, w, d);
seekToTime(t);
}, [progressMetricsWidth, seekToTime]);
const onEnd = useCallback(x => {
seekTo(x);
progressTrackAnim.collapse();
setScrubTime(null);
}, [progressTrackAnim, seekTo]);
const onCancel = useCallback(() => {
progressTrackAnim.collapse();
setScrubTime(null);
}, [progressTrackAnim]);
const onTapSeek = useCallback(x => {
onEnd(x);
}, [onEnd]);
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 (playbackState === 'playing') {
pause();
} else {
play();
}
}, [playbackState, pause, play]);
const onProgressTrackLayout = useCallback(() => {
progressTrackRef.current?.measureInWindow((_left, _y, width, _h) => {
progressMetricsWidth.value = width;
});
}, [progressMetricsWidth, progressTrackRef]);
if (!ready) {
return /*#__PURE__*/_jsx(View, {
style: styles.container,
children: /*#__PURE__*/_jsx(ActivityIndicator, {
color: "#333",
size: "small"
})
});
}
const displayTime = scrubTime ?? currentTime;
const progress = duration > 0 ? displayTime / duration : 0;
return /*#__PURE__*/_jsx(View, {
style: styles.container,
children: /*#__PURE__*/_jsxs(View, {
style: styles.topRow,
children: [/*#__PURE__*/_jsx(Pressable, {
style: styles.playPause,
onPress: onPlayPausePress,
children: playbackState === 'playing' ? /*#__PURE__*/_jsx(Image, {
source: PauseIcon,
style: {
width: 24,
height: 24
}
}) : /*#__PURE__*/_jsx(Image, {
source: PlayIcon,
style: {
width: 24,
height: 24
}
})
}), /*#__PURE__*/_jsxs(Text, {
style: styles.timeText,
children: [formatTime(displayTime), " / ", formatTime(duration)]
}), /*#__PURE__*/_jsx(GestureDetector, {
gesture: scrubGesture,
children: /*#__PURE__*/_jsx(View, {
ref: progressTrackRef,
onLayout: onProgressTrackLayout,
style: styles.progressTrack,
collapsable: false,
children: /*#__PURE__*/_jsx(Animated.View, {
style: [styles.trackInner, progressTrackAnim.animatedStyle],
children: /*#__PURE__*/_jsx(View, {
style: [styles.trackFill, {
width: `${progress * 100}%`
}]
})
})
})
}), /*#__PURE__*/_jsx(Pressable, {
style: styles.volumeIcon,
onPress: () => setMuted(!muted),
children: muted ? /*#__PURE__*/_jsx(Image, {
source: MuteIcon,
style: {
width: 24,
height: 24
}
}) : /*#__PURE__*/_jsx(Image, {
source: VolumeIcon,
style: {
width: 24,
height: 24
}
})
})]
})
});
};
const styles = StyleSheet.create({
container: {
flexDirection: 'column',
alignSelf: 'stretch',
minWidth: 200,
paddingVertical: 10,
paddingHorizontal: 12,
backgroundColor: '#f5f5f5',
borderRadius: 8,
borderWidth: 1,
borderColor: '#333',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2
},
shadowOpacity: 0.15,
shadowRadius: 4
},
android: {
elevation: 4
}
})
},
topRow: {
flexDirection: 'row',
alignItems: 'center'
},
playPause: {
padding: 4,
marginRight: 12
},
timeText: {
color: '#000',
fontSize: 12,
marginRight: 10,
minWidth: 48
},
progressTrack: {
flex: 1,
minWidth: 40,
justifyContent: 'center',
marginRight: 10
},
trackInner: {
alignSelf: 'stretch',
backgroundColor: '#ccc',
overflow: 'hidden'
},
trackFill: {
height: '100%',
backgroundColor: '#000'
},
volumeIcon: {
padding: 4,
marginRight: 12
},
loadingRow: {
flexDirection: 'row',
alignItems: 'center'
},
loadingText: {
color: '#333',
fontSize: 14
}
});
export default AudioControls;
//# sourceMappingURL=AudioControls.js.map