feat: merge audio
This commit is contained in:
@@ -27,7 +27,7 @@ import Style, { gutters, size } from '../../styles/Style'
|
|||||||
import { getArtistDisplayName } from '../../utils/artistName'
|
import { getArtistDisplayName } from '../../utils/artistName'
|
||||||
import { ensureAuthenticated } from '../../utils/authRedirect'
|
import { ensureAuthenticated } from '../../utils/authRedirect'
|
||||||
import { createMusicSharePayload, openShareSheet } from '../../utils/shareSheet'
|
import { createMusicSharePayload, openShareSheet } from '../../utils/shareSheet'
|
||||||
// Import de la fonction utilitaire (à créer dans src/utils/lyricsUtils.js)
|
// Assure-toi que le fichier lyricsUtils.js contient la fonction processKaraokeSections fournie précédemment
|
||||||
import { processKaraokeSections } from './utils/lyricsUtils'
|
import { processKaraokeSections } from './utils/lyricsUtils'
|
||||||
|
|
||||||
const timeBeforeIncrement = 20000
|
const timeBeforeIncrement = 20000
|
||||||
@@ -50,7 +50,7 @@ const MusicDetails = ({ route }) => {
|
|||||||
|
|
||||||
const { isLooping, setLooping } = usePlayer() || {}
|
const { isLooping, setLooping } = usePlayer() || {}
|
||||||
|
|
||||||
// --- DATA ---
|
// --- DATA FETCHING ---
|
||||||
const { data: project } = useDataFromRef({
|
const { data: project } = useDataFromRef({
|
||||||
ref: projectId ? projectsRef.doc(projectId) : null,
|
ref: projectId ? projectsRef.doc(projectId) : null,
|
||||||
simpleRef: true,
|
simpleRef: true,
|
||||||
@@ -65,7 +65,7 @@ const MusicDetails = ({ route }) => {
|
|||||||
condition: !!project?.userId,
|
condition: !!project?.userId,
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- MEMOS & STABILISATION ---
|
// --- METADATA & PLAYER STABILIZATION ---
|
||||||
const title = project?.title || 'Sans titre'
|
const title = project?.title || 'Sans titre'
|
||||||
const artist = useMemo(() => {
|
const artist = useMemo(() => {
|
||||||
const ownerName = getArtistDisplayName(owner, '')
|
const ownerName = getArtistDisplayName(owner, '')
|
||||||
@@ -76,7 +76,7 @@ const MusicDetails = ({ route }) => {
|
|||||||
const coverUrl = project?.coverUrl || null
|
const coverUrl = project?.coverUrl || null
|
||||||
const songUrl = project?.songUrl || null
|
const songUrl = project?.songUrl || null
|
||||||
|
|
||||||
// IMPORTANT: Stabiliser cet objet pour éviter les sauts iOS
|
// On mémoïse le descriptor pour éviter les re-chargements intempestifs sur iOS
|
||||||
const trackDescriptor = useMemo(() => {
|
const trackDescriptor = useMemo(() => {
|
||||||
if (!songUrl) return null
|
if (!songUrl) return null
|
||||||
const id = projectId ? `project-${projectId}` : `song-${songUrl}`
|
const id = projectId ? `project-${projectId}` : `song-${songUrl}`
|
||||||
@@ -91,11 +91,10 @@ const MusicDetails = ({ route }) => {
|
|||||||
metadata: { projectId, screen: 'MusicDetails' },
|
metadata: { projectId, screen: 'MusicDetails' },
|
||||||
context: { projectId, screen: 'MusicDetails' },
|
context: { projectId, screen: 'MusicDetails' },
|
||||||
}
|
}
|
||||||
console.log('[DEBUG PLAYER] Nouveau Track Descriptor:', desc.id)
|
console.log('[PLAYER] Nouveau descriptor généré:', id)
|
||||||
return desc
|
return desc
|
||||||
}, [projectId, songUrl, title, artist, coverUrl])
|
}, [projectId, songUrl, title, artist, coverUrl])
|
||||||
|
|
||||||
// --- CONTROLLER ---
|
|
||||||
const {
|
const {
|
||||||
isCurrent,
|
isCurrent,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
@@ -108,9 +107,30 @@ const MusicDetails = ({ route }) => {
|
|||||||
seekBy,
|
seekBy,
|
||||||
} = useTrackController(trackDescriptor)
|
} = useTrackController(trackDescriptor)
|
||||||
|
|
||||||
const loopEnabled = isCurrent && !!isLooping
|
// --- DURATION CALCULATIONS (Pour le clic sur le slider) ---
|
||||||
|
const estimatedDurationMs = useMemo(() => {
|
||||||
|
const toMs = (val) => {
|
||||||
|
const n = Number(val); return (!n || n <= 0) ? 0 : (n > 1000 ? Math.round(n) : Math.round(n * 1000))
|
||||||
|
}
|
||||||
|
const idx = Number(project?.songIndex) || 0
|
||||||
|
const ts = project?.musicTimestamps?.[idx]
|
||||||
|
|
||||||
// --- LOGIQUE LYRICS ---
|
// Priorité 1: Metadata
|
||||||
|
const fromMeta = toMs(ts?.durationMs || ts?.duration || ts?.audioDuration)
|
||||||
|
if (fromMeta > 0) return fromMeta
|
||||||
|
|
||||||
|
// Priorité 2: Dernier mot synchronisé
|
||||||
|
const words = ts?.alignedWords || []
|
||||||
|
if (words.length > 0) {
|
||||||
|
const last = words[words.length - 1]
|
||||||
|
return Math.round((last.endS || last.startS || 0) * 1000)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}, [project])
|
||||||
|
|
||||||
|
const sliderDurationMs = durationMs > 0 ? durationMs : estimatedDurationMs
|
||||||
|
|
||||||
|
// --- LYRICS LOGIC ---
|
||||||
const sections = useMemo(() => {
|
const sections = useMemo(() => {
|
||||||
const idx = Number(project?.songIndex) || 0
|
const idx = Number(project?.songIndex) || 0
|
||||||
const ts = project?.musicTimestamps?.[idx]
|
const ts = project?.musicTimestamps?.[idx]
|
||||||
@@ -118,9 +138,7 @@ const MusicDetails = ({ route }) => {
|
|||||||
}, [project?.musicTimestamps, project?.songIndex])
|
}, [project?.musicTimestamps, project?.songIndex])
|
||||||
|
|
||||||
const flatLines = useMemo(() => sections.flatMap((s) => s.lines), [sections])
|
const flatLines = useMemo(() => sections.flatMap((s) => s.lines), [sections])
|
||||||
|
const visibleTimeS = (positionMs || 0) / 1000 + 0.3 // Offset de 300ms pour fluidité visuelle
|
||||||
const currentTimeS = (positionMs || 0) / 1000
|
|
||||||
const visibleTimeS = currentTimeS + 0.3 // Offset pour fluidité iOS
|
|
||||||
|
|
||||||
const currentLineIdx = useMemo(() => {
|
const currentLineIdx = useMemo(() => {
|
||||||
if (!flatLines.length) return -1
|
if (!flatLines.length) return -1
|
||||||
@@ -128,31 +146,26 @@ const MusicDetails = ({ route }) => {
|
|||||||
}, [flatLines, visibleTimeS])
|
}, [flatLines, visibleTimeS])
|
||||||
|
|
||||||
// --- EFFECTS ---
|
// --- EFFECTS ---
|
||||||
|
|
||||||
// Sync favoris
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (project && currentUID) {
|
if (project && currentUID) {
|
||||||
setFav(Array.isArray(project.likedBy) ? project.likedBy.includes(currentUID) : false)
|
setFav(Array.isArray(project.likedBy) ? project.likedBy.includes(currentUID) : false)
|
||||||
}
|
}
|
||||||
}, [project?.likedBy, currentUID])
|
}, [project?.likedBy, currentUID])
|
||||||
|
|
||||||
// Timer pour les vues
|
// Vues
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const clearTimer = () => { if (timerRef.current) clearInterval(timerRef.current) }
|
const clear = () => timerRef.current && clearInterval(timerRef.current)
|
||||||
if (isPlaying && projectId && !incrementDoneRef.current) {
|
if (isPlaying && projectId && !incrementDoneRef.current) {
|
||||||
timerRef.current = setInterval(async () => {
|
timerRef.current = setInterval(() => {
|
||||||
listenedMsRef.current += 1000
|
listenedMsRef.current += 1000
|
||||||
if (listenedMsRef.current >= timeBeforeIncrement) {
|
if (listenedMsRef.current >= timeBeforeIncrement) {
|
||||||
console.log('[DEBUG] Incrémentation vue pour:', projectId)
|
projectsRef.doc(projectId).update({ views: increment(1) }).catch(() => {})
|
||||||
incrementDoneRef.current = true
|
incrementDoneRef.current = true
|
||||||
projectsRef.doc(projectId).set({ views: increment(1) }, { merge: true }).catch(() => {})
|
clear()
|
||||||
clearTimer()
|
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
} else {
|
} else { clear() }
|
||||||
clearTimer()
|
return clear
|
||||||
}
|
|
||||||
return clearTimer
|
|
||||||
}, [isPlaying, projectId])
|
}, [isPlaying, projectId])
|
||||||
|
|
||||||
// Auto-scroll
|
// Auto-scroll
|
||||||
@@ -163,56 +176,59 @@ const MusicDetails = ({ route }) => {
|
|||||||
}
|
}
|
||||||
}, [currentLineIdx])
|
}, [currentLineIdx])
|
||||||
|
|
||||||
// Autoplay requested
|
// Autoplay
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoPlayRequested && !hasAutoPlayedRef.current && trackDescriptor) {
|
if (autoPlayRequested && !hasAutoPlayedRef.current && trackDescriptor) {
|
||||||
const start = async () => {
|
const run = async () => {
|
||||||
try {
|
try {
|
||||||
if (!isCurrent) await ensureLoaded({ startPositionMs: 0, autoPlay: true })
|
if (!isCurrent) await ensureLoaded({ startPositionMs: 0, autoPlay: true })
|
||||||
else if (!isPlaying) await resume()
|
else if (!isPlaying) await resume()
|
||||||
hasAutoPlayedRef.current = true
|
hasAutoPlayedRef.current = true
|
||||||
} catch (e) { console.log('[PLAYER ERROR] Autoplay:', e.message) }
|
} catch (e) { console.log('[PLAYER] Autoplay error:', e.message) }
|
||||||
}
|
}
|
||||||
start()
|
run()
|
||||||
}
|
}
|
||||||
}, [autoPlayRequested, trackDescriptor, isCurrent, isPlaying, ensureLoaded, resume])
|
}, [autoPlayRequested, trackDescriptor, isCurrent, isPlaying, ensureLoaded, resume])
|
||||||
|
|
||||||
// --- ACTIONS ---
|
// --- CALLBACKS ---
|
||||||
const togglePlay = useCallback(async () => {
|
const togglePlay = useCallback(async () => {
|
||||||
if (!trackDescriptor) return
|
if (!trackDescriptor) return
|
||||||
try {
|
try {
|
||||||
if (!isCurrent) {
|
if (!isCurrent) {
|
||||||
console.log('[PLAYER] Chargement nouveau media:', trackDescriptor.title)
|
console.log('[PLAYER] Chargement via togglePlay')
|
||||||
await ensureLoaded({ startPositionMs: positionMs, autoPlay: true })
|
await ensureLoaded({ startPositionMs: positionMs, autoPlay: true })
|
||||||
} else {
|
} else {
|
||||||
isPlaying ? await pause() : await resume()
|
isPlaying ? await pause() : await resume()
|
||||||
}
|
}
|
||||||
} catch (e) { console.log('[PLAYER ERROR] Toggle:', e.message) }
|
} catch (e) { console.log('[PLAYER] Toggle error:', e.message) }
|
||||||
}, [trackDescriptor, isCurrent, isPlaying, ensureLoaded, positionMs, pause, resume])
|
}, [trackDescriptor, isCurrent, isPlaying, ensureLoaded, positionMs, pause, resume])
|
||||||
|
|
||||||
const handleSliderSeek = useCallback(async (ms) => {
|
const handleSliderSeek = useCallback(async (targetMs) => {
|
||||||
if (!trackDescriptor) return
|
if (!trackDescriptor || sliderDurationMs <= 0) return
|
||||||
|
const ms = Math.max(0, Math.min(sliderDurationMs, Math.floor(targetMs)))
|
||||||
|
console.log(`[PLAYER] Seek demandé vers: ${ms}ms`)
|
||||||
try {
|
try {
|
||||||
if (!isCurrent) await ensureLoaded({ startPositionMs: ms, autoPlay: false })
|
if (!isCurrent) {
|
||||||
else await seekTo(Math.floor(ms))
|
await ensureLoaded({ startPositionMs: ms, autoPlay: isPlaying })
|
||||||
} catch (e) { console.log('[PLAYER ERROR] Seek:', e.message) }
|
} else {
|
||||||
}, [trackDescriptor, isCurrent, ensureLoaded, seekTo])
|
await seekTo(ms)
|
||||||
|
}
|
||||||
|
} catch (e) { console.log('[PLAYER] Seek error:', e.message) }
|
||||||
|
}, [trackDescriptor, sliderDurationMs, isCurrent, ensureLoaded, isPlaying, seekTo])
|
||||||
|
|
||||||
const handleLyricsSeek = async (timeS) => {
|
const handleLyricsSeek = async (timeS) => {
|
||||||
const ms = timeS * 1000
|
const ms = Math.max(0, timeS * 1000)
|
||||||
try {
|
try {
|
||||||
if (!isCurrent) await ensureLoaded({ startPositionMs: ms, autoPlay: true })
|
if (!isCurrent) await ensureLoaded({ startPositionMs: ms, autoPlay: true })
|
||||||
else {
|
else {
|
||||||
await seekTo(ms)
|
await seekTo(ms)
|
||||||
if (!isPlaying) await resume()
|
if (!isPlaying) await resume()
|
||||||
}
|
}
|
||||||
} catch (e) { console.log('[PLAYER ERROR] LyricsSeek:', e.message) }
|
} catch (e) { console.log('[PLAYER] Lyrics Seek error:', e.message) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- UI PARTS (Options / Share) ---
|
|
||||||
const sharePayload = useMemo(() => createMusicSharePayload({ projectId, title, artist }), [artist, projectId, title])
|
|
||||||
|
|
||||||
const handleOpenOptions = () => {
|
const handleOpenOptions = () => {
|
||||||
|
const sharePayload = createMusicSharePayload({ projectId, title, artist })
|
||||||
SheetManager.show('MusicOptions', {
|
SheetManager.show('MusicOptions', {
|
||||||
payload: {
|
payload: {
|
||||||
projectId, title, ownerId: project?.userId || owner?.id || null,
|
projectId, title, ownerId: project?.userId || owner?.id || null,
|
||||||
@@ -241,7 +257,7 @@ const MusicDetails = ({ route }) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1, paddingTop: 20, paddingBottom: gutters * 2 }}>
|
<View style={{ flex: 1, paddingTop: 20, paddingBottom: gutters * 2 }}>
|
||||||
{/* HEADER SECTION */}
|
{/* INFOS MEDIA */}
|
||||||
<View style={{ gap: 28 }}>
|
<View style={{ gap: 28 }}>
|
||||||
{coverUrl && (
|
{coverUrl && (
|
||||||
<ExpoImage
|
<ExpoImage
|
||||||
@@ -265,13 +281,12 @@ const MusicDetails = ({ route }) => {
|
|||||||
disabled={!isCurrent}
|
disabled={!isCurrent}
|
||||||
style={{ ...size({ size: 32 }), alignItems: 'center', justifyContent: 'center', opacity: isCurrent ? 1 : 0.4 }}
|
style={{ ...size({ size: 32 }), alignItems: 'center', justifyContent: 'center', opacity: isCurrent ? 1 : 0.4 }}
|
||||||
>
|
>
|
||||||
<Feather name="repeat" size={24} color={loopEnabled ? Palette.primary : Palette.white} />
|
<Feather name="repeat" size={24} color={isCurrent && isLooping ? Palette.primary : Palette.white} />
|
||||||
</PressableScale>
|
</PressableScale>
|
||||||
<PressableScale
|
<PressableScale
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
if (!projectId || !ensureAuthenticated(currentUID)) return
|
if (!projectId || !ensureAuthenticated(currentUID)) return
|
||||||
const next = !fav
|
const next = !fav; setFav(next)
|
||||||
setFav(next)
|
|
||||||
projectsRef.doc(projectId).update({
|
projectsRef.doc(projectId).update({
|
||||||
likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID),
|
likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID),
|
||||||
}).catch(() => setFav(!next))
|
}).catch(() => setFav(!next))
|
||||||
@@ -292,7 +307,7 @@ const MusicDetails = ({ route }) => {
|
|||||||
<View style={{ paddingTop: 22 }}>
|
<View style={{ paddingTop: 22 }}>
|
||||||
<ProgressSlider
|
<ProgressSlider
|
||||||
positionMs={positionMs}
|
positionMs={positionMs}
|
||||||
durationMs={durationMs || 0}
|
durationMs={sliderDurationMs}
|
||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
onSeek={handleSliderSeek}
|
onSeek={handleSliderSeek}
|
||||||
onPause={pause}
|
onPause={pause}
|
||||||
@@ -344,7 +359,9 @@ const MusicDetails = ({ route }) => {
|
|||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
) : (
|
) : (
|
||||||
<Text style={{ color: 'gray', textAlign: 'center', marginTop: 20 }}>Chargement des paroles...</Text>
|
<Text style={{ color: 'rgba(255,255,255,0.4)', textAlign: 'center', marginTop: 40, fontFamily: FONT_FAMILY.InterRegular }}>
|
||||||
|
{songUrl ? "Chargement des paroles..." : "Aucune parole disponible"}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -199,8 +199,6 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
}
|
}
|
||||||
}, [projectForDownload?.playbackUrl])
|
}, [projectForDownload?.playbackUrl])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const resolveAudioUrl = useCallback(() => {
|
const resolveAudioUrl = useCallback(() => {
|
||||||
if (typeof projectForDownload?.songUrl === 'string' && projectForDownload.songUrl.trim()) {
|
if (typeof projectForDownload?.songUrl === 'string' && projectForDownload.songUrl.trim()) {
|
||||||
return projectForDownload.songUrl.trim()
|
return projectForDownload.songUrl.trim()
|
||||||
@@ -217,61 +215,32 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
return null
|
return null
|
||||||
}, [projectForDownload])
|
}, [projectForDownload])
|
||||||
|
|
||||||
const handleDownloadUri = useCallback(async () => {
|
const processMerge = useCallback(async () => {
|
||||||
if (isPublishing || isDownloading) return
|
|
||||||
if (action !== 'playback') {
|
if (action !== 'playback') {
|
||||||
setTooltip({
|
throw new Error('Téléchargement indisponible pour cette action')
|
||||||
type: 'error',
|
|
||||||
text: 'Téléchargement indisponible pour cette action',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const playbackUrlToDownload = pendingPlaybackUrl || projectForDownload?.playbackUrl || null
|
const cachedPlaybackUrl = pendingPlaybackUrl || projectForDownload?.playbackUrl || null
|
||||||
if (playbackUrlToDownload) {
|
if (cachedPlaybackUrl) return cachedPlaybackUrl
|
||||||
setIsDownloading(true)
|
|
||||||
try {
|
|
||||||
await triggerDownload(playbackUrlToDownload, projectForDownload?.title)
|
|
||||||
} finally {
|
|
||||||
setIsDownloading(false)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!projectForDownload?.id) {
|
if (!projectForDownload?.id) {
|
||||||
setTooltip({
|
throw new Error('Projet introuvable pour ce playback')
|
||||||
type: 'error',
|
|
||||||
text: 'Projet introuvable pour ce playback',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
setTooltip({
|
throw new Error('Aucune vidéo trouvée pour ce playback')
|
||||||
type: 'error',
|
|
||||||
text: 'Aucune vidéo trouvée pour ce playback',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentUID) {
|
if (!currentUID) {
|
||||||
setTooltip({
|
throw new Error('Utilisateur non authentifié')
|
||||||
type: 'error',
|
|
||||||
text: 'Utilisateur non authentifié',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioUrl = resolveAudioUrl()
|
const audioUrl = resolveAudioUrl()
|
||||||
if (!audioUrl) {
|
if (!audioUrl) {
|
||||||
setTooltip({
|
throw new Error('Aucune piste audio disponible pour ce projet')
|
||||||
type: 'error',
|
|
||||||
text: 'Aucune piste audio disponible pour ce projet',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[PlaybackDownload] handleDownloadUri playback', {
|
console.log('[PlaybackDownload] processMerge playback', {
|
||||||
projectId: projectForDownload.id,
|
projectId: projectForDownload.id,
|
||||||
uri,
|
uri,
|
||||||
currentUID,
|
currentUID,
|
||||||
@@ -279,7 +248,6 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
|
|
||||||
let tempSourcePath = null
|
let tempSourcePath = null
|
||||||
try {
|
try {
|
||||||
setIsDownloading(true)
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const { sourcePath, videoUrl } = await uploadSourceRecording({
|
const { sourcePath, videoUrl } = await uploadSourceRecording({
|
||||||
uri,
|
uri,
|
||||||
@@ -289,7 +257,7 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
tempSourcePath = sourcePath
|
tempSourcePath = sourcePath
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
projectId: projectForDownload?.id,
|
projectId: projectForDownload.id,
|
||||||
videoUrl,
|
videoUrl,
|
||||||
audioUrl,
|
audioUrl,
|
||||||
storagePath: `users/${currentUID}/projects/${projectForDownload.id}/playback.mp4`,
|
storagePath: `users/${currentUID}/projects/${projectForDownload.id}/playback.mp4`,
|
||||||
@@ -303,29 +271,12 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
|
|
||||||
const resultURI = result?.url || null
|
const resultURI = result?.url || null
|
||||||
|
|
||||||
if (resultURI) {
|
if (!resultURI) {
|
||||||
setPendingPlaybackUrl(resultURI)
|
throw new Error('Erreur lors de la publication du playback')
|
||||||
setTooltip({
|
|
||||||
type: 'success',
|
|
||||||
text: 'Playback prêt à télécharger',
|
|
||||||
})
|
|
||||||
await triggerDownload(resultURI, projectForDownload?.title)
|
|
||||||
if (tempSourcePath) {
|
|
||||||
try {
|
|
||||||
await firebase.storage().ref(tempSourcePath).delete()
|
|
||||||
} catch (cleanupError) {
|
|
||||||
console.log('[PlaybackDownload] unable to delete temp source', {
|
|
||||||
message: cleanupError?.message,
|
|
||||||
code: cleanupError?.code,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setTooltip({
|
|
||||||
type: 'error',
|
|
||||||
text: 'Erreur lors de la publication du playback',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPendingPlaybackUrl(resultURI)
|
||||||
|
return resultURI
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('[PlaybackDownload] error upload playback', {
|
console.log('[PlaybackDownload] error upload playback', {
|
||||||
message: error?.message,
|
message: error?.message,
|
||||||
@@ -333,31 +284,47 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
name: error?.name,
|
name: error?.name,
|
||||||
details: error?.details,
|
details: error?.details,
|
||||||
})
|
})
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
if (tempSourcePath) {
|
if (tempSourcePath) {
|
||||||
try {
|
try {
|
||||||
await firebase.storage().ref(tempSourcePath).delete()
|
await firebase.storage().ref(tempSourcePath).delete()
|
||||||
} catch { }
|
} catch (cleanupError) {
|
||||||
|
console.log('[PlaybackDownload] unable to delete temp source', {
|
||||||
|
message: cleanupError?.message,
|
||||||
|
code: cleanupError?.code,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
releaseBlobUrl(uri || null)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [action, currentUID, pendingPlaybackUrl, projectForDownload, resolveAudioUrl, setIsLoading, uri])
|
||||||
|
|
||||||
|
const handleDownloadUri = useCallback(async () => {
|
||||||
|
if (isPublishing || isDownloading) return
|
||||||
|
try {
|
||||||
|
setIsDownloading(true)
|
||||||
|
const resultURI = await processMerge()
|
||||||
|
setTooltip({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Playback prêt à télécharger',
|
||||||
|
})
|
||||||
|
await triggerDownload(resultURI, projectForDownload?.title)
|
||||||
|
} catch (error) {
|
||||||
setTooltip({
|
setTooltip({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: String(error?.message || 'Erreur lors de la publication du playback'),
|
text: String(error?.message || 'Erreur lors de la publication du playback'),
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
releaseBlobUrl(uri || null)
|
|
||||||
setIsDownloading(false)
|
setIsDownloading(false)
|
||||||
setIsLoading(false)
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
action,
|
|
||||||
currentUID,
|
|
||||||
isDownloading,
|
isDownloading,
|
||||||
isPublishing,
|
isPublishing,
|
||||||
pendingPlaybackUrl,
|
processMerge,
|
||||||
projectForDownload,
|
projectForDownload,
|
||||||
resolveAudioUrl,
|
|
||||||
setIsLoading,
|
|
||||||
setTooltip,
|
setTooltip,
|
||||||
uri,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const startPlaybackDownloadCheckout = useCallback(
|
const startPlaybackDownloadCheckout = useCallback(
|
||||||
@@ -397,34 +364,21 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleDownloadPress = useCallback(() => {
|
const handleDownloadPress = async () => {
|
||||||
if (isDownloading || isCheckoutLaunching) {
|
if (!canDownloadPlayback) return startPlaybackDownloadCheckout()
|
||||||
return
|
|
||||||
|
if (isDownloading || isPublishing) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsDownloading(true)
|
||||||
|
const url = await processMerge()
|
||||||
|
await triggerDownload(url, projectForDownload?.title)
|
||||||
|
} catch (err) {
|
||||||
|
setTooltip({ type: 'error', text: err.message })
|
||||||
|
} finally {
|
||||||
|
setIsDownloading(false)
|
||||||
}
|
}
|
||||||
if (canDownloadPlayback) {
|
}
|
||||||
handleDownloadUri()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (downloadPaymentPending) {
|
|
||||||
Alert.alert(
|
|
||||||
'Paiement en attente',
|
|
||||||
"Le paiement n'est pas encore confirmé. Si tu as déjà payé, patiente quelques instants.",
|
|
||||||
[
|
|
||||||
{ text: 'Attendre', style: 'cancel' },
|
|
||||||
{ text: 'Relancer le paiement', onPress: () => startPlaybackDownloadCheckout({ force: true }) },
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
startPlaybackDownloadCheckout()
|
|
||||||
}, [
|
|
||||||
canDownloadPlayback,
|
|
||||||
downloadPaymentPending,
|
|
||||||
handleDownloadUri,
|
|
||||||
isCheckoutLaunching,
|
|
||||||
isDownloading,
|
|
||||||
startPlaybackDownloadCheckout,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!downloadPaymentPending || !hasPaidPlaybackDownload || isDownloading) {
|
if (!downloadPaymentPending || !hasPaidPlaybackDownload || isDownloading) {
|
||||||
@@ -435,135 +389,29 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
}, [downloadPaymentPending, handleDownloadUri, hasPaidPlaybackDownload, isDownloading])
|
}, [downloadPaymentPending, handleDownloadUri, hasPaidPlaybackDownload, isDownloading])
|
||||||
|
|
||||||
const handlePublish = async () => {
|
const handlePublish = async () => {
|
||||||
if (isPublishing) {
|
if (!hasAcceptedPublication) return
|
||||||
console.log('[PlaybackDownload] publish blocked: already publishing')
|
|
||||||
return
|
if (isPublishing) return
|
||||||
}
|
|
||||||
if (!hasAcceptedPublication) {
|
|
||||||
console.log('[PlaybackDownload] publish blocked: publication not accepted')
|
|
||||||
setTooltip({
|
|
||||||
type: 'error',
|
|
||||||
text: 'Confirme la diffusion de ton playback sur Musicland avant de publier',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!projectForDownload?.id) {
|
|
||||||
console.log('[PlaybackDownload] publish: missing project id, navigating directly', {
|
|
||||||
projectForDownload: projectForDownload?.id,
|
|
||||||
})
|
|
||||||
setTooltip({
|
|
||||||
type: 'success',
|
|
||||||
text: publishSuccessMessage,
|
|
||||||
})
|
|
||||||
navigate(Routes.PublishYoutube)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let tempSourcePath = null
|
|
||||||
let playbackUrlToSave = pendingPlaybackUrl || projectForDownload?.playbackUrl || null
|
|
||||||
const audioUrl = resolveAudioUrl()
|
|
||||||
if (!audioUrl && !playbackUrlToSave) {
|
|
||||||
console.log('[PlaybackDownload] publish blocked: missing audioUrl and playbackUrl', {
|
|
||||||
audioUrl,
|
|
||||||
playbackUrlToSave,
|
|
||||||
})
|
|
||||||
setTooltip({
|
|
||||||
type: 'error',
|
|
||||||
text: 'Aucune piste audio disponible pour ce projet',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!currentUID) {
|
|
||||||
console.log('[PlaybackDownload] publish blocked: missing currentUID')
|
|
||||||
setTooltip({
|
|
||||||
type: 'error',
|
|
||||||
text: 'Utilisateur non authentifié',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[PlaybackDownload] publish start', {
|
|
||||||
projectId: projectForDownload?.id,
|
|
||||||
hasPendingPlaybackUrl: Boolean(pendingPlaybackUrl),
|
|
||||||
hasPlaybackUrl: Boolean(projectForDownload?.playbackUrl),
|
|
||||||
hasAudioUrl: Boolean(audioUrl),
|
|
||||||
hasUri: Boolean(uri),
|
|
||||||
})
|
|
||||||
setIsPublishing(true)
|
setIsPublishing(true)
|
||||||
setIsLoading(true)
|
const url = await processMerge()
|
||||||
|
|
||||||
if (!playbackUrlToSave) {
|
|
||||||
console.log('[PlaybackDownload] publish: no playback url, uploading source')
|
|
||||||
const { sourcePath, videoUrl } = await uploadSourceRecording({
|
|
||||||
uri,
|
|
||||||
uid: currentUID,
|
|
||||||
projectId: projectForDownload.id,
|
|
||||||
})
|
|
||||||
tempSourcePath = sourcePath
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
projectId: projectForDownload?.id,
|
|
||||||
videoUrl,
|
|
||||||
audioUrl,
|
|
||||||
storagePath: `users/${currentUID}/projects/${projectForDownload.id}/playback.mp4`,
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[PlaybackDownload] publish: calling merge', payload)
|
|
||||||
|
|
||||||
const { data: result } = await callMergeVideoAndAudio(payload)
|
|
||||||
|
|
||||||
playbackUrlToSave = result?.url || null
|
|
||||||
if (!playbackUrlToSave) {
|
|
||||||
console.log('[PlaybackDownload] publish: merge result missing url', result)
|
|
||||||
throw new Error('merge_failed')
|
|
||||||
}
|
|
||||||
setPendingPlaybackUrl(playbackUrlToSave)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[PlaybackDownload] publish: saving playback url', {
|
|
||||||
projectId: projectForDownload.id,
|
|
||||||
playbackUrlToSave,
|
|
||||||
})
|
|
||||||
await projectsRef.doc(projectForDownload.id).set(
|
await projectsRef.doc(projectForDownload.id).set(
|
||||||
{
|
{
|
||||||
playbackUrl: playbackUrlToSave,
|
playbackUrl: url,
|
||||||
updatedAt: serverTimestamp(),
|
updatedAt: serverTimestamp(),
|
||||||
},
|
},
|
||||||
{ merge: true }
|
{ merge: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
setTooltip({
|
|
||||||
type: 'success',
|
|
||||||
text: publishSuccessMessage,
|
|
||||||
})
|
|
||||||
console.log('[PlaybackDownload] publish: navigate to PublishYoutube', {
|
|
||||||
projectId: projectForDownload.id,
|
|
||||||
})
|
|
||||||
navigate(Routes.PublishYoutube, { projectId: projectForDownload.id })
|
navigate(Routes.PublishYoutube, { projectId: projectForDownload.id })
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.log('[PlaybackDownload] publish error', {
|
|
||||||
message: error?.message,
|
|
||||||
code: error?.code,
|
|
||||||
name: error?.name,
|
|
||||||
details: error?.details,
|
|
||||||
})
|
|
||||||
setTooltip({
|
setTooltip({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: String(error?.message || 'Publication impossible'),
|
text: String(err?.message || 'Erreur de publication'),
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
if (tempSourcePath) {
|
|
||||||
try {
|
|
||||||
await firebase.storage().ref(tempSourcePath).delete()
|
|
||||||
} catch (cleanupError) {
|
|
||||||
console.log('[PlaybackDownload] unable to delete temp source', {
|
|
||||||
message: cleanupError?.message,
|
|
||||||
code: cleanupError?.code,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setIsPublishing(false)
|
setIsPublishing(false)
|
||||||
setIsLoading(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user