This commit is contained in:
2025-09-02 14:28:01 +02:00
11 changed files with 527 additions and 261 deletions
+139 -16
View File
@@ -1,6 +1,9 @@
import React from "react";
import { Image, View } from "react-native";
import { background, img } from "../../assets";
import { useAudioPlayer } from "expo-audio";
import * as FileSystem from "expo-file-system";
import { VideoView, useVideoPlayer } from "expo-video";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { View } from "react-native";
import { background } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
@@ -9,8 +12,100 @@ import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters } from "../../styles";
const RecordedPlayback = ({ route }) => {
const { videoUri, project } = route.params || {};
const songUrl = project?.songUrl || null;
const audioPlayer = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
const videoPlayer = useVideoPlayer(videoUri || null, (p) => {
p.loop = false;
p.muted = true; // recorded video has no audio; keep muted anyway
p.timeUpdateEventInterval = 0.2;
});
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
const wasPlayingBeforeSeek = useRef(false);
// Format mm:ss
const fmt = (ms) => {
const total = Math.max(0, Math.floor((ms || 0) / 1000));
const m = Math.floor(total / 60)
.toString()
.padStart(1, "0");
const s = (total % 60).toString().padStart(2, "0");
return `${m}:${s}`;
};
// Start both players on mount
useEffect(() => {
const start = async () => {
try {
if (audioPlayer && songUrl) await audioPlayer.play?.();
if (videoPlayer) videoPlayer.play();
} catch (e) {}
};
start();
return () => {
try {
if (audioPlayer?.playing) audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) {}
};
}, [audioPlayer, videoPlayer, songUrl]);
// Poll from audio player for progress display; keep video in sync if drifting
useEffect(() => {
const id = global.setInterval(() => {
try {
const dur = (audioPlayer?.duration || 0) * 1000;
const pos = (audioPlayer?.currentTime || 0) * 1000;
setProgressInfo({ pos, dur });
// basic drift correction: if desync > 300ms, align video
if (videoPlayer && !Number.isNaN(videoPlayer.currentTime)) {
const v = (videoPlayer.currentTime || 0) * 1000;
const drift = Math.abs(v - pos);
if (drift > 350) {
videoPlayer.currentTime = Math.max(0, (pos || 0) / 1000);
}
}
} catch (e) {}
}, 250);
return () => global.clearInterval(id);
}, [audioPlayer, videoPlayer]);
const onSeek = async (ratio) => {
try {
const dur = progressInfo.dur || 0;
const pos = Math.floor(dur * ratio);
if (audioPlayer && dur > 0)
await audioPlayer.seekTo?.(Math.floor(pos / 1000));
if (videoPlayer) videoPlayer.currentTime = Math.max(0, pos / 1000);
} catch (e) {}
};
const onSeekStart = async () => {
try {
wasPlayingBeforeSeek.current = !!audioPlayer?.playing;
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) {}
};
const onSeekEnd = async () => {
try {
if (wasPlayingBeforeSeek.current) {
if (audioPlayer) await audioPlayer.play?.();
if (videoPlayer) videoPlayer.play();
}
} catch (e) {}
};
const sliderProgress = useMemo(() => {
return progressInfo.dur
? Math.min(1, (progressInfo.pos || 0) / progressInfo.dur)
: 0;
}, [progressInfo]);
const RecordedPlayback = () => {
return (
<Page backgroundImg={background.playbackBG2} headerType="NONE">
<MusicLandHeader progress={19} onPressBack={goBack} />
@@ -18,26 +113,54 @@ const RecordedPlayback = () => {
style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}
>
<View style={{ flex: 1, gap: 22 }}>
<Image
source={img.placeholder3}
style={{
width: "80%",
flex: 1,
alignSelf: "center",
borderRadius: 16,
}}
{!!videoUri && (
<VideoView
player={videoPlayer}
nativeControls={false}
contentFit="contain"
style={{
width: "80%",
flex: 1,
alignSelf: "center",
borderRadius: 16,
backgroundColor: "#00000066",
overflow: "hidden",
}}
/>
)}
<Slider
value={fmt(progressInfo.pos)}
maxValue={fmt(progressInfo.dur)}
progress={sliderProgress}
seekEnabled={!!songUrl}
onSeek={onSeek}
onSeekStart={onSeekStart}
onSeekEnd={onSeekEnd}
/>
<Slider value="0:00" maxValue="2:00" />
</View>
<View
style={{ width: "80%", alignSelf: "center", marginTop: 4, gap: 12 }}
>
<GradientButton title="Je valide" />
<BorderGradientButton
title="Je change de décor"
onPress={() => navigate(Routes.ChooseDecor)}
title="Recommencer"
onPress={async () => {
try {
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoPlayer?.playing) videoPlayer.pause();
} catch (e) {}
try {
if (videoUri) {
const info = await FileSystem.getInfoAsync(videoUri);
if (info?.exists)
await FileSystem.deleteAsync(videoUri, {
idempotent: true,
});
}
} catch (e) {}
navigate(Routes.RecordPlayback, { project });
}}
/>
<BorderGradientButton title="Recommencer" />
</View>
</View>
</Page>