adding playback controls for audio and video

This commit is contained in:
balzack 2025-01-05 13:14:04 -08:00
parent b3dc1459ca
commit ee8169f168
5 changed files with 131 additions and 17 deletions

View File

@ -1,5 +1,5 @@
import {StyleSheet} from 'react-native'; import {StyleSheet} from 'react-native';
import {Colors} from '../constants/Colors'; import {Colors} from '../../constants/Colors';
export const styles = StyleSheet.create({ export const styles = StyleSheet.create({
modal: { modal: {
@ -9,6 +9,12 @@ export const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
failed: {
color: Colors.offsync,
},
control: {
backgroundColor: 'transparent',
},
button: { button: {
position: 'absolute', position: 'absolute',
borderRadius: 4, borderRadius: 4,
@ -44,12 +50,30 @@ export const styles = StyleSheet.create({
width: '100%', width: '100%',
height: '100%', height: '100%',
}, },
close: { info: {
fontSize: 12,
position: 'absolute', position: 'absolute',
top: 0, top: 0,
right: 0, },
label: {
flexGrow: 1,
fontSize: 32,
paddingLeft: 16,
minWidth: 0,
textOverflow: 'ellipsis',
flexShrink: 1,
},
close: {
display: 'flex',
flexDirection: 'row',
position: 'absolute',
alignItems: 'center',
top: 0,
minWidth: 0,
width: '100%',
}, },
closeIcon: { closeIcon: {
flexShrink: 0,
backgroundColor: 'transparent', backgroundColor: 'transparent',
}, },
progress: { progress: {

View File

@ -1,17 +1,20 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { SafeAreaView, Modal, Pressable, View, Image, Animated, useAnimatedValue } from 'react-native' import { SafeAreaView, Modal, Pressable, View, Image, Animated, useAnimatedValue } from 'react-native'
import { Icon, ProgressBar, IconButton } from 'react-native-paper' import { Icon, Text, ProgressBar, IconButton } from 'react-native-paper'
import { useAudioAsset } from './useAudioAsset.hook'; import { useAudioAsset } from './useAudioAsset.hook';
import { MediaAsset } from '../../conversation/Conversation'; import { MediaAsset } from '../../conversation/Conversation';
import { styles } from './AudioAsset.styled' import { styles } from './AudioAsset.styled'
import {BlurView} from '@react-native-community/blur'; import {BlurView} from '@react-native-community/blur';
import Video from 'react-native-video' import Video, { VideoRef } from 'react-native-video'
import thumb from '../../images/audio.png'; import thumb from '../../images/audio.png';
import {Colors} from '../../constants/Colors';
export function AudioAsset({ topicId, asset, loaded, show }: { topicId: string, asset: MediaAsset, loaded: ()=>void, show: boolean }) { export function AudioAsset({ topicId, asset, loaded, show }: { topicId: string, asset: MediaAsset, loaded: ()=>void, show: boolean }) {
const { state, actions } = useAudioAsset(topicId, asset); const { state, actions } = useAudioAsset(topicId, asset);
const [modal, setModal] = useState(false); const [modal, setModal] = useState(false);
const opacity = useAnimatedValue(0); const opacity = useAnimatedValue(0);
const videoRef = useRef<VideoRef>(null as null | VideoRef);
const [status, setStatus] = useState('loading');
useEffect(() => { useEffect(() => {
if (show) { if (show) {
@ -33,6 +36,30 @@ export function AudioAsset({ topicId, asset, loaded, show }: { topicId: string,
actions.cancelLoad(); actions.cancelLoad();
} }
const play = () => {
videoRef.current.resume();
}
const pause = () => {
videoRef.current.pause();
}
const error = () => {
setStatus('failed');
}
const end = () => {
videoRef.current.seek(0);
}
const playbackRateChange = (e) => {
if (e.playbackRate === 0) {
setStatus('paused');
} else {
setStatus('playing');
}
}
return ( return (
<View style={styles.audio}> <View style={styles.audio}>
<Pressable onPress={showAudio}> <Pressable onPress={showAudio}>
@ -48,6 +75,7 @@ export function AudioAsset({ topicId, asset, loaded, show }: { topicId: string,
<View style={styles.button}> <View style={styles.button}>
<Icon size={28} source="play-box-outline" /> <Icon size={28} source="play-box-outline" />
</View> </View>
<Text style={styles.info} numberOfLines={1}>{ asset.audio?.label || asset.encrypted?.label }</Text>
</Animated.View> </Animated.View>
</Pressable> </Pressable>
<Modal animationType="fade" transparent={true} supportedOrientations={['portrait', 'landscape']} visible={modal} onRequestClose={hideAudio}> <Modal animationType="fade" transparent={true} supportedOrientations={['portrait', 'landscape']} visible={modal} onRequestClose={hideAudio}>
@ -59,8 +87,18 @@ export function AudioAsset({ topicId, asset, loaded, show }: { topicId: string,
source={thumb} source={thumb}
/> />
{ state.dataUrl && ( { state.dataUrl && (
<Video source={{ uri: state.dataUrl }} style={styles.full} paused={false} <Video source={{ uri: state.dataUrl }} style={styles.full} paused={false} ref={videoRef}
onLoad={(e)=>console.log(e)} onError={(e)=>console.log(e)} controls={false} resizeMode="contain" /> onPlaybackRateChange={playbackRateChange} onEnd={end} onError={error}
controls={false} resizeMode="contain" />
)}
{ status === 'failed' && (
<Icon color={Colors.offsync} size={64} source="fire" />
)}
{ status === 'playing' && (
<IconButton style={styles.control} size={64} icon="pause" onPress={pause} />
)}
{ status === 'paused' && (
<IconButton style={styles.control} size={64} icon="play" onPress={play} />
)} )}
{ state.loading && ( { state.loading && (
<View style={styles.progress}> <View style={styles.progress}>
@ -68,6 +106,7 @@ export function AudioAsset({ topicId, asset, loaded, show }: { topicId: string,
</View> </View>
)} )}
<SafeAreaView style={styles.close}> <SafeAreaView style={styles.close}>
<Text style={styles.label} adjustsFontSizeToFit={true} numberOfLines={1}>{ asset.audio?.label || asset.encrypted?.label }</Text>
<IconButton style={styles.closeIcon} icon="close" compact="true" mode="contained" size={28} onPress={hideAudio} /> <IconButton style={styles.closeIcon} icon="close" compact="true" mode="contained" size={28} onPress={hideAudio} />
</SafeAreaView> </SafeAreaView>
</View> </View>

View File

@ -31,15 +31,13 @@ export function useAudioAsset(topicId: string, asset: MediaAsset) {
updateState({ loading: true, loadPercent: 0 }); updateState({ loading: true, loadPercent: 0 });
try { try {
const dataUrl = await focus.getTopicAssetUrl(topicId, assetId, (loadPercent: number)=>{ updateState({ loadPercent }); return !cancelled.current }); const dataUrl = await focus.getTopicAssetUrl(topicId, assetId, (loadPercent: number)=>{ updateState({ loadPercent }); return !cancelled.current });
console.log("AUDIO", dataUrl);
updateState({ dataUrl }); updateState({ dataUrl });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
updateState({ loading: false }); updateState({ loading: false });
} }
} },
} }
return { state, actions } return { state, actions }

View File

@ -1,5 +1,5 @@
import {StyleSheet} from 'react-native'; import {StyleSheet} from 'react-native';
import {Colors} from '../constants/Colors'; import {Colors} from '../../constants/Colors';
export const styles = StyleSheet.create({ export const styles = StyleSheet.create({
modal: { modal: {
@ -9,6 +9,13 @@ export const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
failed: {
color: Colors.offsync,
},
control: {
position: 'absolute',
backgroundColor: 'transparent',
},
button: { button: {
position: 'absolute', position: 'absolute',
borderRadius: 13, borderRadius: 13,

View File

@ -5,12 +5,16 @@ import { useVideoAsset } from './useVideoAsset.hook';
import { MediaAsset } from '../../conversation/Conversation'; import { MediaAsset } from '../../conversation/Conversation';
import { styles } from './VideoAsset.styled' import { styles } from './VideoAsset.styled'
import {BlurView} from '@react-native-community/blur'; import {BlurView} from '@react-native-community/blur';
import Video from 'react-native-video' import Video, { VideoRef } from 'react-native-video'
export function VideoAsset({ topicId, asset, loaded, show }: { topicId: string, asset: MediaAsset, loaded: ()=>void, show: boolean }) { export function VideoAsset({ topicId, asset, loaded, show }: { topicId: string, asset: MediaAsset, loaded: ()=>void, show: boolean }) {
const { state, actions } = useVideoAsset(topicId, asset); const { state, actions } = useVideoAsset(topicId, asset);
const [modal, setModal] = useState(false); const [modal, setModal] = useState(false);
const opacity = useAnimatedValue(0); const opacity = useAnimatedValue(0);
const videoRef = useRef<VideoRef>(null as null | VideoRef);
const [status, setStatus] = useState('loading');
const [showControl, setShowControl] = useState(false);
const clear = useRef();
useEffect(() => { useEffect(() => {
if (state.loaded && show) { if (state.loaded && show) {
@ -35,6 +39,38 @@ export function VideoAsset({ topicId, asset, loaded, show }: { topicId: string,
actions.cancelLoad(); actions.cancelLoad();
} }
const controls = () => {
clearTimeout(clear.current);
setShowControl(true);
clear.current = setTimeout(() => {
setShowControl(false);
}, 3000);
}
const play = () => {
videoRef.current.resume();
}
const pause = () => {
videoRef.current.pause();
}
const error = () => {
setStatus('failed');
}
const end = () => {
videoRef.current.seek(0);
}
const playbackRateChange = (e) => {
if (e.playbackRate === 0) {
setStatus('paused');
} else {
setStatus('playing');
}
}
return ( return (
<View style={styles.video}> <View style={styles.video}>
{ state.thumbUrl && ( { state.thumbUrl && (
@ -54,7 +90,7 @@ export function VideoAsset({ topicId, asset, loaded, show }: { topicId: string,
</Pressable> </Pressable>
)} )}
<Modal animationType="fade" transparent={true} supportedOrientations={['portrait', 'landscape']} visible={modal} onRequestClose={hideVideo}> <Modal animationType="fade" transparent={true} supportedOrientations={['portrait', 'landscape']} visible={modal} onRequestClose={hideVideo}>
<View style={styles.modal}> <Pressable style={styles.modal} onPress={controls}>
<BlurView style={styles.blur} blurType="dark" blurAmount={16} reducedTransparencyFallbackColor="dark" /> <BlurView style={styles.blur} blurType="dark" blurAmount={16} reducedTransparencyFallbackColor="dark" />
<Image <Image
style={styles.full} style={styles.full}
@ -62,8 +98,18 @@ export function VideoAsset({ topicId, asset, loaded, show }: { topicId: string,
source={{ uri: state.thumbUrl }} source={{ uri: state.thumbUrl }}
/> />
{ state.dataUrl && ( { state.dataUrl && (
<Video source={{ uri: state.dataUrl }} style={styles.full} <Video source={{ uri: state.dataUrl }} style={styles.full} ref={videoRef}
controls={false} resizeMode="contain" /> onPlaybackRateChange={playbackRateChange} onEnd={end} onError={error}
controls={false} resizeMode="contain" />
)}
{ status === 'failed' && (
<Icon color={Colors.offsync} size={64} source="fire" />
)}
{ status === 'playing' && showControl && (
<IconButton style={styles.control} size={64} icon="pause" onPress={pause} />
)}
{ status === 'paused' && showControl && (
<IconButton style={styles.control} size={64} icon="play" onPress={play} />
)} )}
{ state.loading && ( { state.loading && (
<View style={styles.progress}> <View style={styles.progress}>
@ -73,7 +119,7 @@ export function VideoAsset({ topicId, asset, loaded, show }: { topicId: string,
<SafeAreaView style={styles.close}> <SafeAreaView style={styles.close}>
<IconButton style={styles.closeIcon} icon="close" compact="true" mode="contained" size={28} onPress={hideVideo} /> <IconButton style={styles.closeIcon} icon="close" compact="true" mode="contained" size={28} onPress={hideVideo} />
</SafeAreaView> </SafeAreaView>
</View> </Pressable>
</Modal> </Modal>
</View> </View>
); );