playback
This commit is contained in:
+1
-1
@@ -65,7 +65,7 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.9",
|
"react-native": "0.76.9",
|
||||||
"react-native-actions-sheet": "^0.9.7",
|
"react-native-actions-sheet": "^0.9.7",
|
||||||
"react-native-compressor": "^1.8.24",
|
"react-native-compressor": "^1.12.0",
|
||||||
"react-native-country-picker-modal": "^2.0.0",
|
"react-native-country-picker-modal": "^2.0.0",
|
||||||
"react-native-dialog": "^9.3.0",
|
"react-native-dialog": "^9.3.0",
|
||||||
"react-native-figma-squircle": "^0.3.4",
|
"react-native-figma-squircle": "^0.3.4",
|
||||||
|
|||||||
@@ -1,13 +1,36 @@
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
import Compressor from "react-native-compressor";
|
||||||
import firebase from "../config/firebase";
|
import firebase from "../config/firebase";
|
||||||
|
|
||||||
export function uploadFileToFirebase({ uri, path }) {
|
export function uploadFileToFirebase({
|
||||||
|
uri,
|
||||||
|
path,
|
||||||
|
shouldCompress = false,
|
||||||
|
fileType = "",
|
||||||
|
}) {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
let resultResize = { uri };
|
let workingURI = uri;
|
||||||
|
|
||||||
const response = await fetch(resultResize.uri);
|
// Optionally compress before upload
|
||||||
|
try {
|
||||||
|
if (shouldCompress && Platform.OS !== "web") {
|
||||||
|
if (fileType === "VIDEO") {
|
||||||
|
console.log("Compressing video...");
|
||||||
|
workingURI = await Compressor.Video.compress(workingURI);
|
||||||
|
} else if (fileType === "IMAGE") {
|
||||||
|
// Basic image compression
|
||||||
|
workingURI = await Compressor.Image.compress(workingURI, {
|
||||||
|
compressionMethod: "auto",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Compression failed, uploading original file", e?.message);
|
||||||
|
workingURI = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(workingURI);
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
|
|
||||||
const uploadTask = firebase.storage().ref(path).put(blob);
|
const uploadTask = firebase.storage().ref(path).put(blob);
|
||||||
|
|||||||
+226
-112
@@ -1,126 +1,240 @@
|
|||||||
import { View, Text, Image, Pressable, Platform } from "react-native";
|
import { useAudioPlayer } from "expo-audio";
|
||||||
import React from "react";
|
import { BlurView } from "expo-blur";
|
||||||
|
import { VideoView, useVideoPlayer } from "expo-video";
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { Image, Platform, Pressable, Text, View } from "react-native";
|
||||||
|
import Carousel from "react-native-reanimated-carousel";
|
||||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||||
import { icons, img } from "../assets";
|
import { icons, img } from "../assets";
|
||||||
import { size } from "../styles/Style";
|
import { projectsRef } from "../config/firebase";
|
||||||
import { BlurView } from "expo-blur";
|
import useDataFromRef from "../hooks/useDataFromRef";
|
||||||
import { Palette } from "../styles";
|
import { Palette } from "../styles";
|
||||||
import { FONT_FAMILY } from "../styles/Fonts";
|
import { FONT_FAMILY } from "../styles/Fonts";
|
||||||
import Carousel from "react-native-reanimated-carousel";
|
import { size } from "../styles/Style";
|
||||||
|
|
||||||
|
const PlaybackItem = ({ item, isActive }) => {
|
||||||
|
const videoUrl = item?.playbackUrl || null;
|
||||||
|
const audioSource = useMemo(() => {
|
||||||
|
const fromSong = item?.songUrl ? { uri: item.songUrl } : null;
|
||||||
|
return fromSong;
|
||||||
|
}, [item]);
|
||||||
|
|
||||||
|
const hasExternalAudio = !!audioSource;
|
||||||
|
console.log("has external audio : ", hasExternalAudio);
|
||||||
|
|
||||||
|
const audioPlayer = useAudioPlayer(audioSource || undefined);
|
||||||
|
const videoPlayer = useVideoPlayer(videoUrl || null, (p) => {
|
||||||
|
p.loop = false; // vidéo et musique ont la même durée
|
||||||
|
p.muted = !!hasExternalAudio; // mute seulement si audio externe
|
||||||
|
p.timeUpdateEventInterval = 0.2;
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const toggle = async () => {
|
||||||
|
try {
|
||||||
|
if (isActive) {
|
||||||
|
try {
|
||||||
|
if (audioPlayer && hasExternalAudio) await audioPlayer.seekTo?.(0);
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
if (videoPlayer) videoPlayer.currentTime = 0;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Lancer quasi simultanément (éviter await pour limiter le décalage)
|
||||||
|
try {
|
||||||
|
if (videoPlayer) videoPlayer.play();
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
if (audioPlayer && hasExternalAudio) audioPlayer.play?.();
|
||||||
|
} catch (e) {}
|
||||||
|
} else {
|
||||||
|
if (audioPlayer?.playing) await audioPlayer.pause?.();
|
||||||
|
if (videoPlayer?.playing) videoPlayer.pause();
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
toggle();
|
||||||
|
}, [isActive, audioPlayer, videoPlayer, hasExternalAudio]);
|
||||||
|
|
||||||
|
// Micro-correction initiale uniquement pour absorber un léger décalage réseau
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive || !hasExternalAudio) return;
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const a = audioPlayer?.currentTime || 0;
|
||||||
|
const v = videoPlayer?.currentTime || 0;
|
||||||
|
if (Math.abs(a - v) > 0.2 && videoPlayer) {
|
||||||
|
videoPlayer.currentTime = Math.max(0, a);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, [isActive, hasExternalAudio, audioPlayer, videoPlayer]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
if (audioPlayer?.playing) audioPlayer.pause?.();
|
||||||
|
if (videoPlayer?.playing) videoPlayer.pause();
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
}, [audioPlayer, videoPlayer]);
|
||||||
|
|
||||||
const Playbacks = () => {
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View
|
||||||
<Carousel
|
style={{
|
||||||
data={Array.from({ length: 5 })}
|
height: responsiveHeight(100),
|
||||||
vertical
|
position: "relative",
|
||||||
height={responsiveHeight(100)}
|
backgroundColor: "black",
|
||||||
renderItem={() => (
|
}}
|
||||||
<View
|
>
|
||||||
style={{
|
{!!videoUrl ? (
|
||||||
height: responsiveHeight(100),
|
<VideoView
|
||||||
position: "relative",
|
player={videoPlayer}
|
||||||
}}
|
nativeControls={false}
|
||||||
>
|
contentFit="cover"
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
source={img.placeholder3}
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Right side actions */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
width: "100%",
|
||||||
|
bottom: 130,
|
||||||
|
gap: 18,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 20,
|
||||||
|
paddingHorizontal: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ gap: 6, alignItems: "center" }}>
|
||||||
|
<Pressable>
|
||||||
|
<Image
|
||||||
|
source={img.profile}
|
||||||
|
style={{
|
||||||
|
...size({ size: 45 }),
|
||||||
|
borderRadius: 100,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable>
|
||||||
|
<BlurView
|
||||||
|
tint="dark"
|
||||||
|
intensity={20}
|
||||||
|
style={{
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Palette.white,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
experimentalBlurMethod={
|
||||||
|
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Suivre
|
||||||
|
</Text>
|
||||||
|
</BlurView>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
<Pressable>
|
||||||
<Image
|
<Image
|
||||||
source={img.placeholder3}
|
source={icons.heartOutline}
|
||||||
style={{ width: "100%", height: "100%" }}
|
style={size({ size: 26 })}
|
||||||
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
<View
|
</Pressable>
|
||||||
|
<Pressable>
|
||||||
|
<Image source={icons.share} style={size({ size: 26 })} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
<View style={{ paddingHorizontal: 28 }}>
|
||||||
|
<BlurView
|
||||||
|
tint="dark"
|
||||||
|
intensity={20}
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: Palette.glass,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
experimentalBlurMethod={
|
||||||
|
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
fontSize: 12,
|
||||||
width: "100%",
|
color: Palette.white,
|
||||||
bottom: 130,
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
gap: 18,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
{item?.title || "Description chanson"}
|
||||||
style={{
|
</Text>
|
||||||
alignSelf: "flex-end",
|
</BlurView>
|
||||||
alignItems: "center",
|
</View>
|
||||||
gap: 20,
|
</View>
|
||||||
paddingHorizontal: 13,
|
</View>
|
||||||
}}
|
);
|
||||||
>
|
};
|
||||||
<View style={{ gap: 6, alignItems: "center" }}>
|
|
||||||
<Pressable>
|
const Playbacks = () => {
|
||||||
<Image
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
source={img.profile}
|
const { data: playbacks = [], loadMore } = useDataFromRef({
|
||||||
style={{
|
ref: projectsRef.where("playbackUrl", "!=", null),
|
||||||
...size({ size: 45 }),
|
simpleRef: false,
|
||||||
borderRadius: 100,
|
listener: false,
|
||||||
}}
|
usePagination: true,
|
||||||
/>
|
batchSize: 2,
|
||||||
</Pressable>
|
});
|
||||||
<Pressable>
|
|
||||||
<BlurView
|
const onSnap = useCallback(
|
||||||
tint="dark"
|
(index) => {
|
||||||
intensity={20}
|
setActiveIndex(index);
|
||||||
style={{
|
// Pré-charge la suite 2 par 2
|
||||||
paddingVertical: 6,
|
if (index >= (playbacks?.length || 0) - 2) {
|
||||||
paddingHorizontal: 12,
|
loadMore?.();
|
||||||
borderRadius: 12,
|
}
|
||||||
borderWidth: 1,
|
},
|
||||||
borderColor: Palette.white,
|
[playbacks?.length, loadMore]
|
||||||
overflow: "hidden",
|
);
|
||||||
}}
|
|
||||||
experimentalBlurMethod={
|
return (
|
||||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
<View style={{ flex: 1, backgroundColor: "black" }}>
|
||||||
}
|
<Carousel
|
||||||
>
|
data={playbacks}
|
||||||
<Text
|
vertical
|
||||||
style={{
|
height={responsiveHeight(100)}
|
||||||
fontSize: 13,
|
pagingEnabled
|
||||||
color: Palette.white,
|
windowSize={5}
|
||||||
fontFamily: FONT_FAMILY.InterMedium,
|
onSnapToItem={onSnap}
|
||||||
}}
|
renderItem={({ item, index }) => (
|
||||||
>
|
<PlaybackItem
|
||||||
Suivre
|
key={item?.id || index}
|
||||||
</Text>
|
item={item}
|
||||||
</BlurView>
|
isActive={index === activeIndex}
|
||||||
</Pressable>
|
/>
|
||||||
</View>
|
|
||||||
<Pressable>
|
|
||||||
<Image
|
|
||||||
source={icons.heartOutline}
|
|
||||||
style={size({ size: 26 })}
|
|
||||||
resizeMode="contain"
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable>
|
|
||||||
<Image source={icons.share} style={size({ size: 26 })} />
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
<View style={{ paddingHorizontal: 28 }}>
|
|
||||||
<BlurView
|
|
||||||
tint="dark"
|
|
||||||
intensity={20}
|
|
||||||
style={{
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
paddingVertical: 8,
|
|
||||||
borderRadius: 20,
|
|
||||||
backgroundColor: Palette.glass,
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
experimentalBlurMethod={
|
|
||||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
fontSize: 12,
|
|
||||||
color: Palette.white,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Description chanson. Viverra enim risus enim enim placerat.
|
|
||||||
Integer pulvinar tristique suscipit risus. Id hendrerit in
|
|
||||||
odio phasellus interdum
|
|
||||||
</Text>
|
|
||||||
</BlurView>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -31,11 +31,14 @@ const DownloadSongs = ({ route }) => {
|
|||||||
const handleDownloadUri = async () => {
|
const handleDownloadUri = async () => {
|
||||||
if (action === "playback" && project?.id) {
|
if (action === "playback" && project?.id) {
|
||||||
// Publication du playback
|
// Publication du playback
|
||||||
|
console.log("project id : ", project.id);
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const { resultURI = null } = await uploadFileToFirebase({
|
const { resultURI = null } = await uploadFileToFirebase({
|
||||||
uri: uri,
|
uri: uri,
|
||||||
path: `musics/${project.id}/playback.mp4`,
|
path: `musics/${project.id}/playback.mp4`,
|
||||||
|
shouldCompress: true,
|
||||||
|
fileType: "VIDEO",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resultURI) throw new Error("Téléversement de l'image impossible");
|
if (!resultURI) throw new Error("Téléversement de l'image impossible");
|
||||||
@@ -59,6 +62,7 @@ const DownloadSongs = ({ route }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log("error upload playback", error);
|
||||||
setTooltip({
|
setTooltip({
|
||||||
type: "error",
|
type: "error",
|
||||||
text: "Erreur lors de la publication du playback",
|
text: "Erreur lors de la publication du playback",
|
||||||
|
|||||||
@@ -8665,7 +8665,7 @@ react-native-calendars@^1.1300.0:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
moment "^2.29.4"
|
moment "^2.29.4"
|
||||||
|
|
||||||
react-native-compressor@^1.8.24:
|
react-native-compressor@^1.12.0:
|
||||||
version "1.12.0"
|
version "1.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-compressor/-/react-native-compressor-1.12.0.tgz#4c387f100ec6d98adbee10c22496d0e42397d8bf"
|
resolved "https://registry.yarnpkg.com/react-native-compressor/-/react-native-compressor-1.12.0.tgz#4c387f100ec6d98adbee10c22496d0e42397d8bf"
|
||||||
integrity sha512-NMAYpXnTLwx/KecwlLF+9Dnrn/tKV5UVfta8Lk9eROsb05JYnUoKLprRzSSpHcyLjIdQAVaTtx2lPYsappMezw==
|
integrity sha512-NMAYpXnTLwx/KecwlLF+9Dnrn/tKV5UVfta8Lk9eROsb05JYnUoKLprRzSSpHcyLjIdQAVaTtx2lPYsappMezw==
|
||||||
|
|||||||
Reference in New Issue
Block a user