playback web

This commit is contained in:
2025-10-02 11:08:42 +02:00
parent f78dc64ec9
commit 11adfa3e21
5 changed files with 1073 additions and 507 deletions
+65 -60
View File
@@ -361,6 +361,7 @@ const MusicDetails = ({ route }) => {
padding: 10,
borderWidth: 1,
borderColor: Palette.transparentWhite,
maxWidth: "50%",
}}
>
<View>
@@ -500,69 +501,73 @@ const MusicDetails = ({ route }) => {
</View>
</BlurView>
{/* Right column: lyrics */}
<BlurView
intensity={1}
style={{
flex: 1,
paddingLeft: 8,
padding: 10,
borderWidth: 1,
borderColor: Palette.transparentWhite,
borderRadius: 12,
height: 400,
}}
>
{lines?.length ? (
<ScrollView
ref={lyricsRef}
style={{ flex: 1 }}
contentContainerStyle={{ paddingBottom: 40, height: 400 }}
>
{lines.map((L, i) => (
<View
key={`line-${i}`}
onLayout={(e) => {
lineYRef.current[i] = e.nativeEvent.layout.y;
}}
{lines.length > 0 && (
<BlurView
intensity={1}
style={{
flex: 1,
paddingLeft: 8,
padding: 10,
borderWidth: 1,
borderColor: Palette.transparentWhite,
borderRadius: 12,
height: 400,
maxWidth: "50%",
}}
>
{lines?.length ? (
<ScrollView
ref={lyricsRef}
style={{ flex: 1 }}
contentContainerStyle={{ paddingBottom: 40, height: 400 }}
>
{lines.map((L, i) => (
<View
key={`line-${i}`}
onLayout={(e) => {
lineYRef.current[i] = e.nativeEvent.layout.y;
}}
style={{
marginBottom: 8,
flexDirection: "row",
flexWrap: "wrap",
}}
>
{(L.words || []).map((w, j) => (
<Text
key={`w-${i}-${j}`}
style={
visibleTimeS >= (w.startS || 0)
? styles.karaokeWordActive
: styles.karaokeWord
}
>
{w.text}
{j < (L.words?.length || 1) - 1 ? " " : ""}
</Text>
))}
</View>
))}
</ScrollView>
) : description?.length > 0 ? (
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ paddingBottom: 40 }}
>
<Text
style={{
marginBottom: 8,
flexDirection: "row",
flexWrap: "wrap",
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{(L.words || []).map((w, j) => (
<Text
key={`w-${i}-${j}`}
style={
visibleTimeS >= (w.startS || 0)
? styles.karaokeWordActive
: styles.karaokeWord
}
>
{w.text}
{j < (L.words?.length || 1) - 1 ? " " : ""}
</Text>
))}
</View>
))}
</ScrollView>
) : description?.length > 0 ? (
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ paddingBottom: 40 }}
>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{description}
</Text>
</ScrollView>
) : null}
</BlurView>
{description}
</Text>
</ScrollView>
) : null}
</BlurView>
)}
</View>
</Page>
);
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -15,7 +15,7 @@ import { gutters } from "../../styles";
const RecordedPlayback = ({ route }) => {
const { videoUri, project } = route.params || {};
const songUrl = project?.songUrl || null;
console.log("video uri is : ", videoUri);
const audioPlayer = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
const videoPlayer = useVideoPlayer(videoUri || null, (p) => {
p.loop = false;
@@ -0,0 +1,235 @@
import { useAudioPlayer } from "expo-audio";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Text, View } from "react-native";
import { background } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import Slider from "../../components/Slider";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters, Palette } from "../../styles";
const fmtSeconds = (s) => {
const total = Math.max(0, Math.floor(Number(s || 0)));
const m = Math.floor(total / 60).toString();
const sec = (total % 60).toString().padStart(2, "0");
return `${m}:${sec}`;
};
const toSeconds = (value) => {
const n = Number(value ?? 0);
if (!Number.isFinite(n) || n < 0) return 0;
return n > 10000 ? n / 1000 : n; // heuristique ms → s
};
const RecordedPlayback = ({ route }) => {
const { videoUri, project } = route.params || {};
const songUrl = project?.songUrl || null;
console.log("video uri is : ", videoUri);
// AUDIO PLAYER (expo-audio → seconds)
const audioPlayer = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
// Optionnel: si tu veux quand même un rendu vidéo sur web si tu as une source
const videoElRef = useRef(null);
const [progress, setProgress] = useState({
posS: 0, // secondes
durS: 0, // secondes
playing: false,
});
// Démarrage / arrêt
useEffect(() => {
let mounted = true;
const start = async () => {
try {
if (audioPlayer && songUrl) {
await audioPlayer.play?.();
}
if (videoElRef.current && videoUri) {
// Lecture vidéo HTML5 (muet pour éviter les policies)
videoElRef.current.muted = true;
videoElRef.current.play().catch(() => {});
}
} catch {}
};
start();
return () => {
mounted = false;
try {
if (audioPlayer?.playing) audioPlayer.pause?.();
} catch {}
try {
if (videoElRef.current) {
videoElRef.current.pause();
}
} catch {}
};
}, [audioPlayer, songUrl, videoUri]);
// Boucle de progression + éventuelle sync de la vidéo si fournie
useEffect(() => {
const id = setInterval(() => {
try {
const durS = toSeconds(audioPlayer?.duration); // secondes
const posS = toSeconds(audioPlayer?.currentTime); // secondes
setProgress({
posS,
durS,
playing: !!audioPlayer?.playing,
});
// Sync vidéo si on a une source vidéo
if (
videoElRef.current &&
videoUri &&
!Number.isNaN(videoElRef.current.currentTime)
) {
const v = Number(videoElRef.current.currentTime || 0);
const drift = Math.abs(v - posS);
if (drift > 0.35) {
videoElRef.current.currentTime = Math.max(0, posS);
}
}
} catch {}
}, 250);
return () => clearInterval(id);
}, [audioPlayer, videoUri]);
// Slider: ratio 0..1
const sliderProgress = useMemo(() => {
return progress.durS > 0 ? Math.min(1, progress.posS / progress.durS) : 0;
}, [progress]);
const onSeek = async (ratio) => {
try {
const durS = Number(progress.durS || 0);
const target = durS * ratio; // secondes
if (audioPlayer && durS > 0) {
await audioPlayer.seekTo?.(Math.floor(target)); // seek en secondes
}
if (videoElRef.current && videoUri) {
videoElRef.current.currentTime = Math.max(0, target);
}
} catch {}
};
const wasPlayingRef = useRef(false);
const onSeekStart = async () => {
try {
wasPlayingRef.current = !!audioPlayer?.playing;
if (audioPlayer?.playing) await audioPlayer.pause?.();
if (videoElRef.current && !videoElRef.current.paused) {
videoElRef.current.pause();
}
} catch {}
};
const onSeekEnd = async () => {
try {
if (wasPlayingRef.current) {
if (audioPlayer) await audioPlayer.play?.();
if (videoElRef.current && videoUri)
videoElRef.current.play().catch(() => {});
}
} catch {}
};
return (
<Page backgroundImg={background.playbackBG2} headerType="NONE">
<MusicLandHeader progress={19} onPressBack={goBack} />
<View
style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}
>
<View style={{ flex: 1, gap: 22, alignItems: "center" }}>
{/* Bloc vidéo optionnel si jamais tu as un videoUri sur web */}
{videoUri ? (
<View
style={{
width: "80%",
aspectRatio: 9 / 16,
borderRadius: 16,
backgroundColor: "#00000066",
overflow: "hidden",
}}
>
<video
ref={videoElRef}
src={videoUri}
muted
playsInline
style={{
width: "100%",
height: "100%",
objectFit: "contain",
backgroundColor: "black",
}}
controls={false}
/>
</View>
) : (
// Placeholder quand pas de vidéo sur web
<View
style={{
width: "80%",
aspectRatio: 9 / 16,
borderRadius: 16,
backgroundColor: "#00000066",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
}}
>
<Text style={{ color: Palette.white, opacity: 0.8 }}>
Aperçu vidéo non disponible sur le web
</Text>
</View>
)}
<Slider
value={fmtSeconds(progress.posS)} // mm:ss (seconds)
maxValue={fmtSeconds(progress.durS)} // mm:ss (seconds)
progress={sliderProgress}
seekEnabled={!!songUrl}
onSeek={onSeek}
onSeekStart={onSeekStart}
onSeekEnd={onSeekEnd}
/>
</View>
<View
style={{ width: "80%", alignSelf: "center", marginTop: 4, gap: 12 }}
>
<GradientButton
title="Je valide"
onPress={() => {
navigate(Routes.DownloadSongs, {
action: "playback",
uri: videoUri || null, // peut être null sur web
project,
});
}}
/>
<BorderGradientButton
title="Recommencer"
onPress={() => {
try {
if (audioPlayer?.playing) audioPlayer.pause?.();
} catch {}
try {
if (videoElRef.current && !videoElRef.current.paused)
videoElRef.current.pause();
} catch {}
navigate(Routes.RecordPlayback, { project });
}}
/>
</View>
</View>
</Page>
);
};
export default RecordedPlayback;
+3 -3
View File
@@ -17,11 +17,11 @@ import { uploadFileToFirebase } from "../../helpers/uploadToFirebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useUserData } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { size } from "../../styles/Style";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { useUserData } from "../../providers/UserDataProvider";
const DownloadSongs = ({ route }) => {
const { currentUID } = useUserData();
@@ -50,11 +50,11 @@ const DownloadSongs = ({ route }) => {
playbackUrl: resultURI,
updatedAt: serverTimestamp(),
},
{ merge: true },
{ merge: true }
);
setTooltip({
type: "success",
text: "Vidéo uploadée, conversion HLS en cours…",
text: "Vidéo uploadée",
});
} else {
setTooltip({