diff --git a/src/assets/index.js b/src/assets/index.js
index 12a95b5..1d0550e 100644
--- a/src/assets/index.js
+++ b/src/assets/index.js
@@ -210,10 +210,6 @@ export const tutorial = {
tasks: tutorialTasks,
};
-export const mockups = {
- loginAppDashboard,
-};
-
export const background = {
writingBG,
studioBG,
diff --git a/src/assets/progress.png b/src/assets/progress.png
new file mode 100644
index 0000000..11df4ce
Binary files /dev/null and b/src/assets/progress.png differ
diff --git a/src/hooks/useDataFromRef.js b/src/hooks/useDataFromRef.js
index a345d13..88549bb 100644
--- a/src/hooks/useDataFromRef.js
+++ b/src/hooks/useDataFromRef.js
@@ -55,9 +55,6 @@ export default function useDataFromRef({
if (!_.isEqual(data, initialState)) {
onUpdate(initialState);
}
-
- console.log("Reset state", initialState);
-
setEndReached(false);
setLastVisible(null);
setData(initialState);
@@ -77,10 +74,10 @@ export default function useDataFromRef({
paginate && lastVisible
? ref.startAfter(lastVisible).limit(batchSize)
: initialDoc
- ? ref.startAt(initialDoc).limit(batchSize)
- : paginate
- ? ref.limit(batchSize)
- : ref;
+ ? ref.startAt(initialDoc).limit(batchSize)
+ : paginate
+ ? ref.limit(batchSize)
+ : ref;
const dataSnap = await dynamicRef.get();
@@ -113,7 +110,7 @@ export default function useDataFromRef({
if (e.code === "firestore/permission-denied") {
console.warn(
"Permission denied for ref: ",
- ref?._collectionPath?.relativeName
+ ref?._collectionPath?.relativeName,
);
} else {
console.log(e);
@@ -143,14 +140,14 @@ export default function useDataFromRef({
if (e.code === "firestore/permission-denied") {
console.warn(
"Permission denied for ref: ",
- ref?._collectionPath?.relativeName
+ ref?._collectionPath?.relativeName,
);
} else {
console.log(e);
}
await updateData([]);
setLoading(false);
- }
+ },
);
};
diff --git a/src/screens/Library/Library.js b/src/screens/Library/Library.js
index d0c26ac..5f51be1 100644
--- a/src/screens/Library/Library.js
+++ b/src/screens/Library/Library.js
@@ -40,10 +40,10 @@ const Library = () => {
-
+ {/**/}
-
+ {/**/}
diff --git a/src/screens/Library/MusicDetails.js b/src/screens/Library/MusicDetails.js
index d257a7a..28ad34c 100644
--- a/src/screens/Library/MusicDetails.js
+++ b/src/screens/Library/MusicDetails.js
@@ -1,5 +1,5 @@
import { useRoute } from "@react-navigation/core";
-import { Audio } from "expo-audio";
+import { useAudioPlayer } from "expo-audio";
import { Image as ExpoImage } from "expo-image";
import React, { useEffect, useMemo, useState } from "react";
import {
@@ -11,10 +11,14 @@ import {
View,
} from "react-native";
import { SheetManager } from "react-native-actions-sheet";
+import {
+ responsiveHeight,
+ responsiveWidth,
+} from "react-native-responsive-dimensions";
import { useGlobal } from "reactn";
-import { background, icons, img } from "../../assets";
+import { background, icons } from "../../assets";
import Slider from "../../components/Slider";
-import firebase, {
+import {
arrayRemove,
arrayUnion,
increment,
@@ -44,9 +48,7 @@ const MusicDetails = () => {
const timerRef = React.useRef(null);
const { data: project } = useDataFromRef({
- ref: projectId
- ? firebase.firestore().collection("projects").doc(projectId)
- : null,
+ ref: projectId ? projectsRef.doc(projectId) : null,
simpleRef: true,
listener: true,
condition: !!projectId,
@@ -71,61 +73,27 @@ const MusicDetails = () => {
const title = project?.title || "Sans titre";
const artist = owner?.userName || "MusicLand";
const coverUrl = project?.coverUrl || null;
- const songUrl = useMemo(() => {
- if (project?.song?.url) return project.song.url;
- const arr = Array.isArray(project?.musicUrls) ? project.musicUrls : [];
- return arr[0] || null;
- }, [project]);
+ const songUrl = project?.songUrl || null;
- const soundRef = React.useRef(null);
+ const player = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
- // Load/unload audio with expo-audio for reliable status updates on iOS/Android
+ // Reset counters when the track changes
useEffect(() => {
- let isMounted = true;
- const load = async () => {
- try {
- // Unload previous
- if (soundRef.current) {
- await soundRef.current.unloadAsync();
- soundRef.current.setOnPlaybackStatusUpdate(null);
- soundRef.current = null;
- }
- // Reset listen tracking per song load
- listenedMsRef.current = 0;
- incrementDoneRef.current = false;
- if (!songUrl) return;
- const { sound } = await Audio.Sound.createAsync(
- { uri: songUrl },
- { shouldPlay: false }
- );
- sound.setOnPlaybackStatusUpdate((status) => {
- if (!isMounted) return;
- if (!status || !status.isLoaded) return;
- const pos = status.positionMillis || 0;
- const dur = status.durationMillis || 0;
- setProgressInfo({ pos, dur });
- setIsPlaying(!!status.isPlaying);
- });
- soundRef.current = sound;
- } catch (e) {
- console.log("Audio load error", e?.message);
- }
- };
- load();
- return () => {
- isMounted = false;
- (async () => {
- try {
- if (soundRef.current) {
- await soundRef.current.unloadAsync();
- soundRef.current.setOnPlaybackStatusUpdate(null);
- soundRef.current = null;
- }
- } catch (_) {}
- })();
- };
+ listenedMsRef.current = 0;
+ incrementDoneRef.current = false;
}, [songUrl]);
+ // Poll player state to update progress and play state
+ useEffect(() => {
+ const id = setInterval(() => {
+ const dur = (player?.duration || 0) * 1000;
+ const pos = (player?.currentTime || 0) * 1000;
+ setProgressInfo({ pos, dur });
+ setIsPlaying(!!player?.playing);
+ }, 300);
+ return () => clearInterval(id);
+ }, [player]);
+
// Start/stop a timer to accumulate listened milliseconds while playing
useEffect(() => {
const clearTimer = () => {
@@ -170,15 +138,13 @@ const MusicDetails = () => {
};
const togglePlay = async () => {
- const sound = soundRef.current;
- if (!sound || !songUrl) return;
+ if (!player || !songUrl) return;
try {
- const status = await sound.getStatusAsync();
- if (status?.isLoaded && status.isPlaying) {
- await sound.pauseAsync();
+ if (player.playing) {
+ await player.pause?.();
setIsPlaying(false);
} else {
- await sound.playAsync();
+ await player.play?.();
setIsPlaying(true);
}
} catch (e) {
@@ -190,9 +156,8 @@ const MusicDetails = () => {
try {
const dur = progressInfo.dur || 0;
const pos = Math.floor(dur * ratio);
- const sound = soundRef.current;
- if (sound && dur > 0) {
- await sound.setPositionAsync(pos);
+ if (player && dur > 0) {
+ await player.seekTo?.(Math.floor((pos || 0) / 1000));
}
} catch (e) {
console.log("MusicDetails seek error", e?.message);
@@ -203,8 +168,7 @@ const MusicDetails = () => {
try {
const cur = Math.floor((progressInfo.pos || 0) / 1000);
const next = Math.max(0, cur + deltaSeconds);
- const sound = soundRef.current;
- if (sound) await sound.setPositionAsync(next * 1000);
+ if (player) await player.seekTo?.(next);
} catch (e) {
console.log("MusicDetails seekBy error", e?.message);
}
@@ -248,10 +212,9 @@ const MusicDetails = () => {
paddingTop: 20,
paddingBottom: gutters * 2,
}}
- // stickyHeaderIndices={[1]}
>
- {coverUrl ? (
+ {coverUrl && (
{
transition={150}
style={styles.img}
/>
- ) : (
-
)}
@@ -305,79 +266,81 @@ const MusicDetails = () => {
-
- {
- try {
- const sound = soundRef.current;
- const status = await sound?.getStatusAsync?.();
- wasPlayingBeforeSeek.current =
- !!status?.isLoaded && !!status?.isPlaying;
- if (status?.isLoaded && status?.isPlaying) {
- await sound.pauseAsync();
- setIsPlaying(false);
- }
- } catch (e) {
- console.log("Pause on seek start error", e?.message);
+ {songUrl && (
+
+ {
- try {
- const sound = soundRef.current;
- if (sound && wasPlayingBeforeSeek.current) {
- await sound.playAsync();
- setIsPlaying(true);
+ seekEnabled={!!songUrl}
+ onSeekStart={async () => {
+ try {
+ wasPlayingBeforeSeek.current = !!player?.playing;
+ if (player?.playing) {
+ await player.pause?.();
+ setIsPlaying(false);
+ }
+ } catch (e) {
+ console.log("Pause on seek start error", e?.message);
}
- wasPlayingBeforeSeek.current = false;
- } catch (e) {
- console.log("Resume after seek error", e?.message);
- }
- }}
- />
-
- {/* Previous (rewind 10s) */}
- seekBy(-10)}>
-
-
- {/* Play / Pause */}
- {
+ try {
+ if (player && wasPlayingBeforeSeek.current) {
+ await player.play?.();
+ setIsPlaying(true);
+ }
+ wasPlayingBeforeSeek.current = false;
+ } catch (e) {
+ console.log("Resume after seek error", e?.message);
+ }
+ }}
+ />
+
-
-
- {/* Next (forward 10s) */}
- seekBy(10)}>
- seekBy(-10)}>
+
+
+ {/* Play / Pause */}
+
-
+ onPress={togglePlay}
+ >
+
+
+ {/* Next (forward 10s) */}
+ seekBy(10)}>
+
+
+
-
+ )}
{description?.length > 0 && (
{
/>
- {(!selected &&
- !projectsLoading &&
- !usersLoading &&
- filteredProjects.length === 0 &&
- filteredUsers.length === 0) && (
-
- )}
{((!selected && (filteredProjects.length > 0 || projectsLoading)) ||
selected === "Musiques") && (
diff --git a/src/screens/Library/components/MusicCard.js b/src/screens/Library/components/MusicCard.js
index 912afe9..9593c53 100644
--- a/src/screens/Library/components/MusicCard.js
+++ b/src/screens/Library/components/MusicCard.js
@@ -1,6 +1,13 @@
import { BlurView } from "expo-blur";
import React, { useEffect, useRef, useState } from "react";
-import { Platform, Pressable, StyleSheet, Text, View, Image as RNImage } from "react-native";
+import {
+ Platform,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+ Image as RNImage,
+} from "react-native";
import { Image as ExpoImage } from "expo-image";
import { useGlobal } from "reactn";
import { icons, img } from "../../../assets";
@@ -8,6 +15,7 @@ import { arrayRemove, arrayUnion, projectsRef } from "../../../config/firebase";
import { Palette, Style } from "../../../styles";
import { FONT_FAMILY } from "../../../styles/Fonts";
import { size } from "../../../styles/Style";
+import { responsiveWidth } from "react-native-responsive-dimensions";
const MusicCard = ({
onPress,
@@ -90,7 +98,7 @@ const MusicCard = ({
/>
) : (
)}
@@ -107,7 +115,9 @@ const MusicCard = ({
}
>
- {title || "Sans titre"}
+
+ {title || "Sans titre"}
+
{subtitle || "MusicLand"}
{
if (isLocked(index)) return;
switch (index) {
case 0:
- navigate(Routes.WritingLyrics, {});
+ navigate(Routes.WritingLyrics, {
+ projectId: currentProjet?.id || null,
+ });
break;
case 1:
- navigate(Routes.Studio, { action: item.type });
+ navigate(Routes.Compose, { projectId: currentProjet.id });
break;
case 2:
- navigate(Routes.Production, { action: item.type });
+ navigate(Routes.PouchReady, { projectId: currentProjet.id });
break;
case 3:
console.log("test");
navigate(Routes.Playback, {
- action: item.type,
project: currentProjet,
});
default:
diff --git a/src/screens/Playback/RecordPlayback.js b/src/screens/Playback/RecordPlayback.js
index dadd0b7..9396382 100644
--- a/src/screens/Playback/RecordPlayback.js
+++ b/src/screens/Playback/RecordPlayback.js
@@ -1,4 +1,3 @@
-import { Audio } from "expo-audio";
import { CameraView, useCameraPermissions } from "expo-camera";
import React from "react";
import { Text, View } from "react-native";
@@ -10,7 +9,12 @@ import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
-
+// ADD: Firestore helpers to increment views
+import { useFocusEffect } from "@react-navigation/native";
+import { useAudioPlayer } from "expo-audio";
+import { useCallback, useEffect, useRef, useState } from "react";
+import Svg, { Circle } from "react-native-svg";
+import { increment, projectsRef } from "../../config/firebase";
const RecordPlayback = ({ route }) => {
const { top } = useSafeAreaInsets();
const { project } = route.params || {};
@@ -19,20 +23,48 @@ const RecordPlayback = ({ route }) => {
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
// Refs
- const cameraRef = React.useRef(null);
- const soundRef = React.useRef(null);
- const countdownTimerRef = React.useRef(null);
- const stopRequestedRef = React.useRef(false);
+ const cameraRef = useRef(null);
+ const countdownTimerRef = useRef(null);
+ const stopRequestedRef = useRef(false);
+
+ // Listen counter refs (inspired by MusicDetails)
+ const listenedMsRef = useRef(0);
+ const incrementDoneRef = useRef(false);
+ const timerRef = useRef(null);
+ const timeBeforeIncrement = 20000; // 20 seconds
// UI / state
- const [isPreparing, setIsPreparing] = React.useState(false);
- const [countdown, setCountdown] = React.useState(0);
- const [isRecording, setIsRecording] = React.useState(false);
+ const [isPreparing, setIsPreparing] = useState(false);
+ const [countdown, setCountdown] = useState(0);
+ const [isRecording, setIsRecording] = useState(false);
+ const [showProgress, setShowProgress] = useState(false);
+ const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
// Derive song URL robustly
const songUrl = project?.songUrl || null;
+ const player = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
+ // useEffect(() => {
+ // setVideo(null);
+ // }, []);
+ useEffect(() => {
+ listenedMsRef.current = 0;
+ incrementDoneRef.current = false;
+ }, [songUrl]);
- React.useEffect(() => {
+ // Poll player state to update progress ring
+ useEffect(() => {
+ if (!player) return;
+ const id = global.setInterval(() => {
+ try {
+ const dur = (player?.duration || 0) * 1000;
+ const pos = (player?.currentTime || 0) * 1000;
+ setProgressInfo({ pos, dur });
+ } catch (_) {}
+ }, 250);
+ return () => global.clearInterval(id);
+ }, [player]);
+
+ useEffect(() => {
// Request permissions on mount if not granted
(async () => {
try {
@@ -40,40 +72,88 @@ const RecordPlayback = ({ route }) => {
} catch (_) {}
})();
return () => {
- // Cleanup timers and audio on unmount
+ // Cleanup timers on unmount
try {
if (countdownTimerRef.current) {
global.clearInterval(countdownTimerRef.current);
countdownTimerRef.current = null;
}
- (async () => {
- try {
- if (soundRef.current) {
- soundRef.current.setOnPlaybackStatusUpdate(null);
- await soundRef.current.unloadAsync();
- soundRef.current = null;
- }
- } catch (_) {}
- })();
+ if (timerRef.current) {
+ global.clearInterval(timerRef.current);
+ timerRef.current = null;
+ }
} catch (_) {}
};
}, []);
+ // Fully reset audio and recording state (used on focus and before new session)
+ const resetSession = useCallback(async () => {
+ try {
+ if (countdownTimerRef.current) {
+ global.clearInterval(countdownTimerRef.current);
+ countdownTimerRef.current = null;
+ }
+ if (timerRef.current) {
+ global.clearInterval(timerRef.current);
+ timerRef.current = null;
+ }
+ stopRequestedRef.current = false;
+ listenedMsRef.current = 0;
+ incrementDoneRef.current = false;
+ setIsPreparing(false);
+ setIsRecording(false);
+ setShowProgress(false);
+ setCountdown(0);
+ if (player) {
+ try {
+ if (player.playing) await player.pause?.();
+ await player.seekTo?.(0);
+ } catch (_) {}
+ }
+ } catch (_) {}
+ }, [player]);
+
+ // Reset when screen gains focus (coming from "Recommencer", etc.)
+ useFocusEffect(
+ useCallback(() => {
+ void resetSession();
+ return () => {};
+ }, [resetSession])
+ );
+
const startCountdownThenRecord = async () => {
console.log("start countdown");
if (!songUrl) return;
console.log("songUrl exists");
+ // Ensure clean state and audio at t=0
+ await resetSession();
setIsPreparing(true);
setCountdown(5);
- // Start music + recording immediately when countdown starts
- void startRecordingWithMusic();
- // 5 -> 0 countdown display only
+ setShowProgress(false);
+
+ // Start countdown display; start recording+music WHEN countdown reaches 0
+ if (countdownTimerRef.current) {
+ global.clearInterval(countdownTimerRef.current);
+ countdownTimerRef.current = null;
+ }
countdownTimerRef.current = global.setInterval(() => {
setCountdown((c) => {
const next = (c || 0) - 1;
if (next <= 0) {
+ console.log("clear timer");
+ // clear timer
global.clearInterval(countdownTimerRef.current);
countdownTimerRef.current = null;
+ // Ensure countdown overlay disappears and progress shows immediately
+ setIsPreparing(false);
+ setShowProgress(true);
+ // also set countdown to 0 explicitly in case of frame drop
+ if (c !== 0) {
+ // guard against stale value
+ setCountdown(0);
+ }
+ // start recording + music at the same time
+ void startRecordingWithMusic();
}
return Math.max(0, next);
});
@@ -83,70 +163,90 @@ const RecordPlayback = ({ route }) => {
const startRecordingWithMusic = async () => {
try {
stopRequestedRef.current = false;
- console.log("stop requested ref is : ", soundRef.current);
- // Prepare audio
- if (soundRef.current) {
- console.log("if sound ref current");
- try {
- soundRef.current.setOnPlaybackStatusUpdate(null);
- await soundRef.current.unloadAsync();
- console.log("unload sound ref");
- } catch (_) {}
- soundRef.current = null;
- }
- // Ensure audio mode allows playback alongside camera recording
- try {
- await Audio.setAudioModeAsync({
- playsInSilentMode: true,
- interruptionMode: "mixWithOthers",
- allowsRecording: true,
- shouldPlayInBackground: false,
- });
- } catch (_) {}
-
- const { sound } = await Audio.Sound.createAsync(
- { uri: songUrl },
- { shouldPlay: false }
- );
-
- sound.setOnPlaybackStatusUpdate((status) => {
- if (!status || !status.isLoaded) return;
- if (status.didJustFinish && !stopRequestedRef.current) {
- stopRequestedRef.current = true;
- try {
- cameraRef.current?.stopRecording?.();
- } catch (_) {}
- }
- });
- soundRef.current = sound;
+ // Reset listen counters for this track/session
+ listenedMsRef.current = 0;
+ incrementDoneRef.current = false;
// Start recording
setIsRecording(true);
+ setCountdown(0);
+ setIsPreparing(false);
+ setShowProgress(true);
const recordPromise = cameraRef.current?.recordAsync?.({
mute: true,
maxDuration: 600, // safety cap (10 min)
});
- // Start playing
- await sound.playAsync();
+ // Start playing with useAudioPlayer (like MusicDetails)
+ if (player && songUrl) {
+ try {
+ await player.seekTo?.(0);
+ } catch (_) {}
+ await player.play?.();
+ }
- // Wait for recording to stop (either by song end or manual stop)
+ // Start timer to track listening time and increment views (like MusicDetails)
+ if (!timerRef.current && project?.id) {
+ timerRef.current = global.setInterval(async () => {
+ try {
+ if (player?.playing) {
+ listenedMsRef.current += 500;
+ if (
+ !incrementDoneRef.current &&
+ listenedMsRef.current >= timeBeforeIncrement
+ ) {
+ incrementDoneRef.current = true;
+ try {
+ await projectsRef
+ .doc(project.id)
+ .set({ views: increment(1) }, { merge: true });
+ } catch (_) {}
+ }
+ }
+ } catch (_) {}
+ }, 500);
+ }
+
+ // Monitor when song ends to stop recording
+ const checkSongEnd = global.setInterval(async () => {
+ try {
+ if (player && !player.playing && !stopRequestedRef.current) {
+ const duration = (player?.duration || 0) * 1000;
+ const currentTime = (player?.currentTime || 0) * 1000;
+
+ // If we're near the end or stopped, stop recording
+ if (duration > 0 && currentTime >= duration - 1000) {
+ stopRequestedRef.current = true;
+ global.clearInterval(checkSongEnd);
+ try {
+ cameraRef.current?.stopRecording?.();
+ } catch (_) {}
+ }
+ }
+ } catch (_) {}
+ }, 1000);
+
+ // Wait for recording to stop
const video = await recordPromise;
+ global.clearInterval(checkSongEnd);
- // Ensure audio stops and cleanup
+ // Stop audio
try {
- const s = soundRef.current;
- if (s) {
- s.setOnPlaybackStatusUpdate(null);
- await s.stopAsync().catch(() => {});
- await s.unloadAsync().catch(() => {});
+ if (player?.playing) {
+ await player.pause?.();
}
} catch (_) {}
- soundRef.current = null;
+
+ // Clear listen timer after recording ends
+ if (timerRef.current) {
+ global.clearInterval(timerRef.current);
+ timerRef.current = null;
+ }
setIsRecording(false);
setIsPreparing(false);
+ setShowProgress(false);
// Navigate to next screen with video uri if available
if (video?.uri) {
@@ -156,13 +256,14 @@ const RecordPlayback = ({ route }) => {
}
} catch (e) {
// Fallback on error
+ console.log("error : ", e);
setIsRecording(false);
setIsPreparing(false);
- try {
- soundRef.current?.setOnPlaybackStatusUpdate?.(null);
- await soundRef.current?.unloadAsync?.();
- } catch (_) {}
- soundRef.current = null;
+ setShowProgress(false);
+ if (timerRef.current) {
+ global.clearInterval(timerRef.current);
+ timerRef.current = null;
+ }
}
};
@@ -200,7 +301,7 @@ const RecordPlayback = ({ route }) => {
{/* Centered countdown overlay */}
- {(isPreparing || isRecording) && countdown > 0 && (
+ {isPreparing && countdown > 0 && !showProgress && (
{
onPress={startCountdownThenRecord}
/>
)}
+
+ {/* Circular progress when countdown finished / recording */}
+ {permissionsGranted && (isRecording || showProgress) && (
+
+ {/* Dimmed circular backdrop */}
+
+ {/* Progress ring */}
+
+ {/* Center white button-like circle */}
+
+
+
+ )}
);
};
+// Simple SVG circular progress ring
+const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => {
+ const r = size / 2 - strokeWidth / 2;
+ const c = 2 * Math.PI * r;
+ const clamped = Math.max(0, Math.min(1, progress || 0));
+ const offset = c * (1 - clamped);
+ return (
+
+ );
+};
+
export default RecordPlayback;
diff --git a/src/screens/Playback/RecordedPlayback.js b/src/screens/Playback/RecordedPlayback.js
index c4e4eb3..e2076be 100644
--- a/src/screens/Playback/RecordedPlayback.js
+++ b/src/screens/Playback/RecordedPlayback.js
@@ -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 (
@@ -18,26 +113,54 @@ const RecordedPlayback = () => {
style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}
>
-
+ )}
+
-
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 });
+ }}
/>
-
diff --git a/src/screens/Studio/SongReady.js b/src/screens/Studio/SongReady.js
index 677fbf5..b22102d 100644
--- a/src/screens/Studio/SongReady.js
+++ b/src/screens/Studio/SongReady.js
@@ -124,7 +124,7 @@ const SongReady = () => {
.doc(projectId)
.set(
{
- song: { index: selectedIndex, url },
+ songUrl: url,
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },