import { useFocusEffect } from '@react-navigation/native' import useSharedAudioPlayer from '../../hooks/useSharedAudioPlayer' import * as FileSystem from 'expo-file-system' import { VideoView, useVideoPlayer } from 'expo-video' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Image, Pressable, View } from 'react-native' import { background, icons } from '../../assets' import BorderGradientButton from '../../components/BorderGradientButton' import GradientButton from '../../components/GradientButton' import MusicLandHeader from '../../components/MusicLandHeader' import ProgressSlider from '../../components/player/ProgressSlider' import Page from '../../layouts/Page' import { Routes } from '../../navigation' import { goBack, navigate } from '../../navigation/NavigationService' import { gutters, Palette } from '../../styles' import { FONT_FAMILY } from '../../styles/Fonts' const RecordedPlayback = ({ route }) => { const { videoUri, project, syncOffsetMs = 0 } = route.params || {} const songUrl = project?.songUrl || null const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, { id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined, title: typeof project?.title === 'string' ? project.title : 'Sans titre', artist: typeof project?.userName === 'string' ? project.userName : 'MusicLand', artwork: project?.coverUrl || null, coverUrl: project?.coverUrl || null, metadata: { projectId: project?.id, screen: 'RecordedPlayback' }, }) 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, isPlaying: false, }) const playbackEndedRef = useRef(false) const syncOffsetRef = useRef(Math.max(0, Number(syncOffsetMs) || 0)) // Start both players on mount const stopPlayback = useCallback(async () => { try { if (audioPlayer) await audioPlayer.pause?.() } catch (e) { } try { videoPlayer?.pause?.() } catch (e) { } }, [audioPlayer, videoPlayer]) useFocusEffect( useCallback(() => { return () => { void stopPlayback() } }, [stopPlayback]) ) useEffect(() => { playbackEndedRef.current = false const start = async () => { try { if (videoPlayer && syncOffsetRef.current > 0) { videoPlayer.currentTime = syncOffsetRef.current / 1000 } if (audioPlayer && songUrl) await audioPlayer.play?.() if (videoPlayer) videoPlayer.play() } catch (e) { } } start() return () => { playbackEndedRef.current = false void stopPlayback() } }, [audioPlayer, songUrl, stopPlayback, videoPlayer]) // 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 const playing = !!audioPlayer?.playing || !!videoPlayer?.playing setProgressInfo({ pos, dur, isPlaying: playing }) // basic drift correction: if desync > 300ms, align video if (videoPlayer && !Number.isNaN(videoPlayer.currentTime)) { const offset = syncOffsetRef.current || 0 const expected = Math.max(0, (pos || 0) + offset) const v = (videoPlayer.currentTime || 0) * 1000 const drift = Math.abs(v - expected) if (drift > 350) { videoPlayer.currentTime = Math.max(0, expected / 1000) } } } catch (e) { } }, 250) return () => global.clearInterval(id) }, [audioPlayer, videoPlayer]) const onSeek = async (targetMs) => { try { const dur = progressInfo.dur || 0 const pos = Math.max(0, Math.min(dur, Math.floor(targetMs))) if (audioPlayer && dur > 0) await audioPlayer.seekTo?.(Math.floor(pos / 1000)) if (videoPlayer) { const offset = syncOffsetRef.current || 0 videoPlayer.currentTime = Math.max(0, (pos + offset) / 1000) } } catch (e) { } } const onSeekStart = useCallback(() => { playbackEndedRef.current = false }, []) const pauseDuringSeek = useCallback(async () => { try { if (audioPlayer?.playing) await audioPlayer.pause?.() } catch (e) { } try { if (videoPlayer?.playing) videoPlayer.pause() } catch (e) { } }, [audioPlayer, videoPlayer]) const resumeAfterSeek = useCallback(async () => { try { if (audioPlayer?.resume) { await audioPlayer.resume?.() } else if (audioPlayer) { await audioPlayer.play?.() } } catch (e) { } try { if (videoPlayer) videoPlayer.play() } catch (e) { } }, [audioPlayer, videoPlayer]) const sliderProgress = useMemo(() => { return progressInfo.dur ? Math.min(1, (progressInfo.pos || 0) / progressInfo.dur) : 0 }, [progressInfo]) useEffect(() => { const duration = progressInfo?.dur || 0 if (!duration) return const position = progressInfo?.pos || 0 if (playbackEndedRef.current && duration - position > 1000) { playbackEndedRef.current = false return } const remaining = Math.max(0, duration - position) if (remaining <= 400 && !playbackEndedRef.current) { playbackEndedRef.current = true void stopPlayback() } }, [progressInfo, stopPlayback]) const handleTogglePlayback = async () => { try { const duration = progressInfo?.dur || 0 const position = progressInfo?.pos || 0 const isAtEnd = duration > 0 && duration - position < 1000 const isCurrentlyPlaying = !!audioPlayer?.playing || !!videoPlayer?.playing if (isCurrentlyPlaying) { playbackEndedRef.current = false if (audioPlayer?.playing) await audioPlayer.pause?.() if (videoPlayer?.playing) videoPlayer.pause() return } if (isAtEnd) { if (audioPlayer) await audioPlayer.seekTo?.(0) if (videoPlayer) { const offset = syncOffsetRef.current || 0 videoPlayer.currentTime = Math.max(0, offset / 1000) } } playbackEndedRef.current = false if (songUrl && audioPlayer) await audioPlayer.play?.() if (videoPlayer) videoPlayer.play() } catch (e) { } } return ( {!!videoUri && ( )} { try { await stopPlayback() } catch (e) { } navigate(Routes.PlaybackDownload, { action: 'playback', uri: videoUri, project, }) }} /> { try { await stopPlayback() } 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 }) }} /> ) } export default RecordedPlayback