offset audio after playback and add songs
This commit is contained in:
@@ -129,6 +129,17 @@ const AllMyPlaylist = ({ route }) => {
|
||||
})
|
||||
}
|
||||
|
||||
const openAddTracks = useCallback(() => {
|
||||
if (!selectedPlaylistId) return
|
||||
|
||||
SheetManager.show('PlaylistAddTracks', {
|
||||
payload: {
|
||||
playlistId: selectedPlaylistId,
|
||||
existingMusicIds: Array.isArray(playlist?.musics) ? playlist.musics : [],
|
||||
},
|
||||
})
|
||||
}, [playlist?.musics, selectedPlaylistId])
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATION"
|
||||
@@ -141,9 +152,18 @@ const AllMyPlaylist = ({ route }) => {
|
||||
rightComponent={() => (
|
||||
<>
|
||||
{selectedPlaylistId != null && (
|
||||
<Pressable onPress={confirmDelete}>
|
||||
<Image source={icons.trash} style={{ width: 24, height: 24 }} />
|
||||
</Pressable>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
||||
<Pressable onPress={openAddTracks}>
|
||||
<BlurView intensity={20} tint="dark" style={styles.addTracksButton}>
|
||||
<Text numberOfLines={1} style={styles.addTracksText}>
|
||||
Ajouter des morceaux
|
||||
</Text>
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
<Pressable onPress={confirmDelete}>
|
||||
<Image source={icons.trash} style={{ width: 24, height: 24 }} />
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -288,3 +308,18 @@ const AllMyPlaylist = ({ route }) => {
|
||||
}
|
||||
|
||||
export default AllMyPlaylist
|
||||
|
||||
const styles = {
|
||||
addTracksButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 999,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: Palette.glass,
|
||||
},
|
||||
addTracksText: {
|
||||
color: Palette.white,
|
||||
fontSize: 12,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BlurView } from 'expo-blur'
|
||||
import { useRoute } from '@react-navigation/native'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Image, Pressable, View } from 'react-native'
|
||||
import { Image, Pressable, Text, View } from 'react-native'
|
||||
import { SheetManager } from 'react-native-actions-sheet'
|
||||
import { useDataFromRef } from 'react-native-minuit/src/hooks'
|
||||
import useDataFromArrayId from 'react-native-minuit/src/hooks/useDataFromArrayId'
|
||||
@@ -12,7 +13,8 @@ import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
|
||||
import usePlayer from '../../hooks/usePlayer'
|
||||
import Page from '../../layouts/Page'
|
||||
import { goBack } from '../../navigation/NavigationService'
|
||||
import { gutters } from '../../styles'
|
||||
import { gutters, Palette } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
import { getProjectLikes, LIKE_TARGET } from '../../utils/likes'
|
||||
import MusicCard from './components/MusicCard'
|
||||
|
||||
@@ -130,15 +132,35 @@ const PlaylistDetails = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const openAddTracks = useCallback(() => {
|
||||
if (!playlistId) return
|
||||
|
||||
SheetManager.show('PlaylistAddTracks', {
|
||||
payload: {
|
||||
playlistId,
|
||||
existingMusicIds: Array.isArray(playlist?.musics) ? playlist.musics : [],
|
||||
},
|
||||
})
|
||||
}, [playlist?.musics, playlistId])
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATION"
|
||||
backgroundImg={background.libraryBG}
|
||||
title={playlist?.name || 'Playlist'}
|
||||
rightComponent={() => (
|
||||
<Pressable onPress={confirmDelete}>
|
||||
<Image source={icons.trash} style={{ width: 24, height: 24 }} />
|
||||
</Pressable>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
||||
<Pressable onPress={openAddTracks}>
|
||||
<BlurView intensity={20} tint="dark" style={styles.addTracksButton}>
|
||||
<Text numberOfLines={1} style={styles.addTracksText}>
|
||||
Ajouter des morceaux
|
||||
</Text>
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
<Pressable onPress={confirmDelete}>
|
||||
<Image source={icons.trash} style={{ width: 24, height: 24 }} />
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
contentContainerStyle={{ paddingBottom: gutters * 2 }}
|
||||
>
|
||||
@@ -177,3 +199,18 @@ const PlaylistDetails = () => {
|
||||
}
|
||||
|
||||
export default PlaylistDetails
|
||||
|
||||
const styles = {
|
||||
addTracksButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 999,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: Palette.glass,
|
||||
},
|
||||
addTracksText: {
|
||||
color: Palette.white,
|
||||
fontSize: 12,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { playlistsRef } from '../../../config/firebase'
|
||||
import { arrayUnion, playlistsRef } from '../../../config/firebase'
|
||||
|
||||
export const createPlaylist = async (payload = {}) => {
|
||||
try {
|
||||
@@ -21,3 +21,19 @@ export const createPlaylist = async (payload = {}) => {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const addMusicToPlaylist = async ({ playlistId, projectId }) => {
|
||||
try {
|
||||
if (typeof playlistId !== 'string' || typeof projectId !== 'string') {
|
||||
throw new Error('Identifiants de playlist ou de morceau invalides')
|
||||
}
|
||||
|
||||
await playlistsRef.doc(playlistId).update({
|
||||
musics: arrayUnion(projectId),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error adding music to playlist:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ import Palette from '../styles/Palette.js'
|
||||
|
||||
export default ({ navigation }) => {
|
||||
const [, setTooltip] = useGlobal('_tooltip')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [email, setEmail] = useState(__DEV__ ? 'az@az.az' : '')
|
||||
const [password, setPassword] = useState(__DEV__ ? 'Minuit33' : '')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const afterLoginNavigate = useCallback(async () => {
|
||||
const uid = firebase.auth().currentUser?.uid
|
||||
|
||||
@@ -18,13 +18,13 @@ import { Routes } from '../../navigation'
|
||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||
import { gutters, Palette } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
import { clampSyncOffsetMs } from '../../utils/playbackSync'
|
||||
import trimLeadingDuplicateSection from '../../utils/trimLeadingDuplicateSection'
|
||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||
|
||||
const TIME_BEFORE_INCREMENT_MS = 20000 // 20s
|
||||
const COUNTDOWN_SECONDS = 10
|
||||
const CAMERA_STARTUP_DELAY_MS = 250
|
||||
const MAX_SYNC_OFFSET_MS = 2000
|
||||
const KARAOKE_LEAD_S = 0.18
|
||||
const LOG_PREFIX = '[RecordPlayback]'
|
||||
const KEEP_AWAKE_TAG = 'record-playback'
|
||||
@@ -758,7 +758,7 @@ const RecordPlayback = ({ route }) => {
|
||||
playbackStartedAtRef.current && recordingStartTimeRef.current
|
||||
? playbackStartedAtRef.current - recordingStartTimeRef.current
|
||||
: 0
|
||||
const syncOffsetMs = Math.max(0, Math.min(MAX_SYNC_OFFSET_MS, Math.round(rawOffsetMs || 0)))
|
||||
const syncOffsetMs = clampSyncOffsetMs(Math.round(rawOffsetMs || 0))
|
||||
log('Computed sync offset', { rawOffsetMs, syncOffsetMs })
|
||||
|
||||
if (video?.uri) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||
import { background } from '../../assets'
|
||||
import RestartSpinnerIcon from '../../assets/UI/RestartSpinnerIcon'
|
||||
import { registerBlobUrl, releaseBlobUrl } from '../../utils/blobUrlCache'
|
||||
import { clampSyncOffsetMs } from '../../utils/playbackSync'
|
||||
import trimLeadingDuplicateSection from '../../utils/trimLeadingDuplicateSection'
|
||||
|
||||
const TIME_BEFORE_INCREMENT_MS = 20000
|
||||
@@ -29,7 +30,6 @@ const MEDIA_BOOTSTRAP_DELAY_MS = 250
|
||||
const MEDIA_RETRY_DELAY_MS = 700
|
||||
const MEDIA_MAX_RETRIES = 2
|
||||
const CAMERA_STARTUP_DELAY_MS = 250
|
||||
const MAX_SYNC_OFFSET_MS = 2000
|
||||
const KARAOKE_LEAD_S = 0.18
|
||||
|
||||
const toSeconds = (v) => {
|
||||
@@ -547,7 +547,7 @@ const RecordPlayback = ({ route }) => {
|
||||
playbackStartedAtRef.current != null && recordingStartAtRef.current != null
|
||||
? playbackStartedAtRef.current - recordingStartAtRef.current
|
||||
: 0
|
||||
const syncOffsetMs = Math.max(0, Math.min(MAX_SYNC_OFFSET_MS, Math.round(rawOffsetMs || 0)))
|
||||
const syncOffsetMs = clampSyncOffsetMs(Math.round(rawOffsetMs || 0))
|
||||
|
||||
navigate(Routes.RecordedPlayback, {
|
||||
project,
|
||||
|
||||
@@ -4,18 +4,28 @@ 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 { 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 SyncOffsetSlider from '../../components/player/SyncOffsetSlider'
|
||||
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'
|
||||
import {
|
||||
clampSyncOffsetMs,
|
||||
getAudioPositionMs,
|
||||
getTimelineDurationMs,
|
||||
getTimelinePositionMs,
|
||||
getVideoPositionMs,
|
||||
snapSyncOffsetMs,
|
||||
} from '../../utils/playbackSync'
|
||||
|
||||
const RecordedPlayback = ({ route }) => {
|
||||
const { videoUri, project, syncOffsetMs = 0 } = route.params || {}
|
||||
const initialSyncOffsetMs = useMemo(() => snapSyncOffsetMs(syncOffsetMs), [syncOffsetMs])
|
||||
|
||||
const songUrl = project?.songUrl || null
|
||||
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
||||
@@ -26,10 +36,10 @@ const RecordedPlayback = ({ route }) => {
|
||||
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 videoPlayer = useVideoPlayer(videoUri || null, (player) => {
|
||||
player.loop = false
|
||||
player.muted = true
|
||||
player.timeUpdateEventInterval = 0.2
|
||||
})
|
||||
|
||||
const [progressInfo, setProgressInfo] = useState({
|
||||
@@ -37,18 +47,46 @@ const RecordedPlayback = ({ route }) => {
|
||||
dur: 0,
|
||||
isPlaying: false,
|
||||
})
|
||||
const [currentSyncOffsetMs, setCurrentSyncOffsetMs] = useState(initialSyncOffsetMs)
|
||||
|
||||
const playbackEndedRef = useRef(false)
|
||||
const syncOffsetRef = useRef(initialSyncOffsetMs)
|
||||
|
||||
const syncOffsetRef = useRef(Math.max(0, Number(syncOffsetMs) || 0))
|
||||
const getCurrentRawAudioPositionMs = useCallback(
|
||||
() => Math.max(0, (audioPlayer?.currentTime || 0) * 1000),
|
||||
[audioPlayer]
|
||||
)
|
||||
|
||||
const getCurrentRawAudioDurationMs = useCallback(
|
||||
() => Math.max(0, (audioPlayer?.duration || 0) * 1000),
|
||||
[audioPlayer]
|
||||
)
|
||||
|
||||
const alignPlaybackToTimeline = useCallback(
|
||||
async (timelineMs, offsetMs = syncOffsetRef.current) => {
|
||||
const safeOffsetMs = clampSyncOffsetMs(offsetMs)
|
||||
const safeTimelineMs = Math.max(0, Number(timelineMs) || 0)
|
||||
const audioTargetMs = getAudioPositionMs(safeTimelineMs, safeOffsetMs)
|
||||
const videoTargetMs = getVideoPositionMs(safeTimelineMs, safeOffsetMs)
|
||||
|
||||
if (audioPlayer) {
|
||||
await audioPlayer.seekTo?.(audioTargetMs / 1000)
|
||||
}
|
||||
|
||||
if (videoPlayer) {
|
||||
videoPlayer.currentTime = videoTargetMs / 1000
|
||||
}
|
||||
},
|
||||
[audioPlayer, videoPlayer]
|
||||
)
|
||||
|
||||
// Start both players on mount
|
||||
const stopPlayback = useCallback(async () => {
|
||||
try {
|
||||
if (audioPlayer) await audioPlayer.pause?.()
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
try {
|
||||
videoPlayer?.pause?.()
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
}, [audioPlayer, videoPlayer])
|
||||
|
||||
useFocusEffect(
|
||||
@@ -61,57 +99,67 @@ const RecordedPlayback = ({ route }) => {
|
||||
|
||||
useEffect(() => {
|
||||
playbackEndedRef.current = false
|
||||
syncOffsetRef.current = initialSyncOffsetMs
|
||||
setCurrentSyncOffsetMs(initialSyncOffsetMs)
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
if (videoPlayer && syncOffsetRef.current > 0) {
|
||||
videoPlayer.currentTime = syncOffsetRef.current / 1000
|
||||
}
|
||||
await alignPlaybackToTimeline(0, initialSyncOffsetMs)
|
||||
if (audioPlayer && songUrl) await audioPlayer.play?.()
|
||||
if (videoPlayer) videoPlayer.play()
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
}
|
||||
start()
|
||||
|
||||
void start()
|
||||
|
||||
return () => {
|
||||
playbackEndedRef.current = false
|
||||
void stopPlayback()
|
||||
}
|
||||
}, [audioPlayer, songUrl, stopPlayback, videoPlayer])
|
||||
}, [alignPlaybackToTimeline, audioPlayer, initialSyncOffsetMs, 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 })
|
||||
const rawDurationMs = getCurrentRawAudioDurationMs()
|
||||
const rawAudioMs = getCurrentRawAudioPositionMs()
|
||||
const nextDurationMs = getTimelineDurationMs(rawDurationMs, syncOffsetRef.current)
|
||||
const nextPositionMs = getTimelinePositionMs(rawAudioMs, syncOffsetRef.current)
|
||||
const safePositionMs =
|
||||
nextDurationMs > 0 ? Math.min(nextPositionMs, nextDurationMs) : nextPositionMs
|
||||
const isPlaying = !!audioPlayer?.playing || !!videoPlayer?.playing
|
||||
|
||||
setProgressInfo({
|
||||
pos: safePositionMs,
|
||||
dur: nextDurationMs,
|
||||
isPlaying,
|
||||
})
|
||||
|
||||
// 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)
|
||||
const expectedVideoMs = getVideoPositionMs(safePositionMs, syncOffsetRef.current)
|
||||
const currentVideoMs = (videoPlayer.currentTime || 0) * 1000
|
||||
const driftMs = Math.abs(currentVideoMs - expectedVideoMs)
|
||||
|
||||
if (driftMs > 350) {
|
||||
videoPlayer.currentTime = Math.max(0, expectedVideoMs / 1000)
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
} 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) { }
|
||||
}
|
||||
return () => global.clearInterval(id)
|
||||
}, [audioPlayer, getCurrentRawAudioDurationMs, getCurrentRawAudioPositionMs, videoPlayer])
|
||||
|
||||
const onSeek = useCallback(
|
||||
async (targetMs) => {
|
||||
try {
|
||||
const durationMs = progressInfo.dur || 0
|
||||
const safeTargetMs = Math.max(0, Math.min(durationMs, Math.floor(targetMs)))
|
||||
await alignPlaybackToTimeline(safeTargetMs)
|
||||
} catch (e) {}
|
||||
},
|
||||
[alignPlaybackToTimeline, progressInfo.dur]
|
||||
)
|
||||
|
||||
const onSeekStart = useCallback(() => {
|
||||
playbackEndedRef.current = false
|
||||
@@ -120,10 +168,10 @@ const RecordedPlayback = ({ route }) => {
|
||||
const pauseDuringSeek = useCallback(async () => {
|
||||
try {
|
||||
if (audioPlayer?.playing) await audioPlayer.pause?.()
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (videoPlayer?.playing) videoPlayer.pause()
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
}, [audioPlayer, videoPlayer])
|
||||
|
||||
const resumeAfterSeek = useCallback(async () => {
|
||||
@@ -133,36 +181,72 @@ const RecordedPlayback = ({ route }) => {
|
||||
} else if (audioPlayer) {
|
||||
await audioPlayer.play?.()
|
||||
}
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (videoPlayer) videoPlayer.play()
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
}, [audioPlayer, videoPlayer])
|
||||
|
||||
const sliderProgress = useMemo(() => {
|
||||
return progressInfo.dur ? Math.min(1, (progressInfo.pos || 0) / progressInfo.dur) : 0
|
||||
}, [progressInfo])
|
||||
const handleSyncOffsetChange = useCallback(
|
||||
(nextOffsetMs) => {
|
||||
const safeOffsetMs = snapSyncOffsetMs(nextOffsetMs)
|
||||
setCurrentSyncOffsetMs((prevOffsetMs) =>
|
||||
prevOffsetMs === safeOffsetMs ? prevOffsetMs : safeOffsetMs
|
||||
)
|
||||
|
||||
const previousOffsetMs = syncOffsetRef.current
|
||||
if (safeOffsetMs === previousOffsetMs) return
|
||||
|
||||
playbackEndedRef.current = false
|
||||
|
||||
const rawAudioMs = getCurrentRawAudioPositionMs()
|
||||
const rawDurationMs = getCurrentRawAudioDurationMs()
|
||||
const currentTimelineMs = getTimelinePositionMs(rawAudioMs, previousOffsetMs)
|
||||
const nextDurationMs = getTimelineDurationMs(rawDurationMs, safeOffsetMs)
|
||||
const nextTimelineMs =
|
||||
nextDurationMs > 0 ? Math.min(currentTimelineMs, nextDurationMs) : currentTimelineMs
|
||||
|
||||
syncOffsetRef.current = safeOffsetMs
|
||||
|
||||
setProgressInfo({
|
||||
pos: nextTimelineMs,
|
||||
dur: nextDurationMs,
|
||||
isPlaying: !!audioPlayer?.playing || !!videoPlayer?.playing,
|
||||
})
|
||||
|
||||
void alignPlaybackToTimeline(nextTimelineMs, safeOffsetMs)
|
||||
},
|
||||
[
|
||||
alignPlaybackToTimeline,
|
||||
audioPlayer?.playing,
|
||||
getCurrentRawAudioDurationMs,
|
||||
getCurrentRawAudioPositionMs,
|
||||
videoPlayer?.playing,
|
||||
]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const duration = progressInfo?.dur || 0
|
||||
if (!duration) return
|
||||
const position = progressInfo?.pos || 0
|
||||
if (playbackEndedRef.current && duration - position > 1000) {
|
||||
const durationMs = progressInfo?.dur || 0
|
||||
if (!durationMs) return
|
||||
|
||||
const positionMs = progressInfo?.pos || 0
|
||||
if (playbackEndedRef.current && durationMs - positionMs > 1000) {
|
||||
playbackEndedRef.current = false
|
||||
return
|
||||
}
|
||||
const remaining = Math.max(0, duration - position)
|
||||
if (remaining <= 400 && !playbackEndedRef.current) {
|
||||
|
||||
const remainingMs = Math.max(0, durationMs - positionMs)
|
||||
if (remainingMs <= 400 && !playbackEndedRef.current) {
|
||||
playbackEndedRef.current = true
|
||||
void stopPlayback()
|
||||
}
|
||||
}, [progressInfo, stopPlayback])
|
||||
|
||||
const handleTogglePlayback = async () => {
|
||||
const handleTogglePlayback = useCallback(async () => {
|
||||
try {
|
||||
const duration = progressInfo?.dur || 0
|
||||
const position = progressInfo?.pos || 0
|
||||
const isAtEnd = duration > 0 && duration - position < 1000
|
||||
const durationMs = progressInfo?.dur || 0
|
||||
const positionMs = progressInfo?.pos || 0
|
||||
const isAtEnd = durationMs > 0 && durationMs - positionMs < 1000
|
||||
const isCurrentlyPlaying = !!audioPlayer?.playing || !!videoPlayer?.playing
|
||||
|
||||
if (isCurrentlyPlaying) {
|
||||
@@ -173,18 +257,22 @@ const RecordedPlayback = ({ route }) => {
|
||||
}
|
||||
|
||||
if (isAtEnd) {
|
||||
if (audioPlayer) await audioPlayer.seekTo?.(0)
|
||||
if (videoPlayer) {
|
||||
const offset = syncOffsetRef.current || 0
|
||||
videoPlayer.currentTime = Math.max(0, offset / 1000)
|
||||
}
|
||||
await alignPlaybackToTimeline(0)
|
||||
}
|
||||
|
||||
playbackEndedRef.current = false
|
||||
if (songUrl && audioPlayer) await audioPlayer.play?.()
|
||||
|
||||
if (songUrl && audioPlayer) {
|
||||
if (audioPlayer?.resume) {
|
||||
await audioPlayer.resume?.()
|
||||
} else {
|
||||
await audioPlayer.play?.()
|
||||
}
|
||||
}
|
||||
|
||||
if (videoPlayer) videoPlayer.play()
|
||||
} catch (e) { }
|
||||
}
|
||||
} catch (e) {}
|
||||
}, [alignPlaybackToTimeline, audioPlayer, progressInfo, songUrl, videoPlayer])
|
||||
|
||||
return (
|
||||
<Page backgroundColor={Palette.grayMid} headerType="NONE">
|
||||
@@ -206,16 +294,25 @@ const RecordedPlayback = ({ route }) => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ProgressSlider
|
||||
positionMs={progressInfo.pos}
|
||||
durationMs={progressInfo.dur}
|
||||
isPlaying={progressInfo.isPlaying}
|
||||
onSeek={onSeek}
|
||||
onSeekStart={onSeekStart}
|
||||
onPause={pauseDuringSeek}
|
||||
onPlay={resumeAfterSeek}
|
||||
disabled={!songUrl}
|
||||
/>
|
||||
|
||||
<View style={{ width: '80%', alignSelf: 'center', gap: 18 }}>
|
||||
<ProgressSlider
|
||||
positionMs={progressInfo.pos}
|
||||
durationMs={progressInfo.dur}
|
||||
isPlaying={progressInfo.isPlaying}
|
||||
onSeek={onSeek}
|
||||
onSeekStart={onSeekStart}
|
||||
onPause={pauseDuringSeek}
|
||||
onPlay={resumeAfterSeek}
|
||||
disabled={!songUrl}
|
||||
/>
|
||||
<SyncOffsetSlider
|
||||
valueMs={currentSyncOffsetMs}
|
||||
onChange={handleSyncOffsetChange}
|
||||
disabled={!songUrl}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
onPress={handleTogglePlayback}
|
||||
style={{
|
||||
@@ -237,17 +334,19 @@ const RecordedPlayback = ({ route }) => {
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={{ width: '80%', alignSelf: 'center', marginTop: 4, gap: 12 }}>
|
||||
<GradientButton
|
||||
title="Je valide"
|
||||
onPress={async () => {
|
||||
try {
|
||||
await stopPlayback()
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
navigate(Routes.PlaybackDownload, {
|
||||
action: 'playback',
|
||||
uri: videoUri,
|
||||
project,
|
||||
syncOffsetMs: syncOffsetRef.current,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
@@ -256,16 +355,17 @@ const RecordedPlayback = ({ route }) => {
|
||||
onPress={async () => {
|
||||
try {
|
||||
await stopPlayback()
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (videoUri) {
|
||||
const info = await FileSystem.getInfoAsync(videoUri)
|
||||
if (info?.exists)
|
||||
if (info?.exists) {
|
||||
await FileSystem.deleteAsync(videoUri, {
|
||||
idempotent: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
navigate(Routes.RecordPlayback, { project })
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Image, Pressable, Text, View } from 'react-native'
|
||||
import { background, icons } from '../../assets'
|
||||
import { icons } from '../../assets'
|
||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
||||
import GradientButton from '../../components/GradientButton'
|
||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||
import Slider from '../../components/Slider'
|
||||
import ProgressSlider from '../../components/player/ProgressSlider'
|
||||
import SyncOffsetSlider from '../../components/player/SyncOffsetSlider'
|
||||
import Page from '../../layouts/Page'
|
||||
import { Routes } from '../../navigation'
|
||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||
@@ -13,26 +14,22 @@ import { gutters, Palette } from '../../styles'
|
||||
import { isWeb } from '../../hooks/useLayoutType'
|
||||
import useSharedAudioPlayer from '../../hooks/useSharedAudioPlayer'
|
||||
import { releaseBlobUrl } from '../../utils/blobUrlCache'
|
||||
|
||||
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
|
||||
}
|
||||
import {
|
||||
clampSyncOffsetMs,
|
||||
getAudioPositionMs,
|
||||
getTimelineDurationMs,
|
||||
getTimelinePositionMs,
|
||||
getVideoPositionMs,
|
||||
snapSyncOffsetMs,
|
||||
} from '../../utils/playbackSync'
|
||||
|
||||
const WEB_PREVIEW_WIDTH = 360
|
||||
|
||||
const RecordedPlayback = ({ route }) => {
|
||||
const { videoUri, project, syncOffsetMs = 0 } = route.params || {}
|
||||
const initialSyncOffsetMs = useMemo(() => snapSyncOffsetMs(syncOffsetMs), [syncOffsetMs])
|
||||
const songUrl = project?.songUrl || null
|
||||
// AUDIO PLAYER (expo-audio → seconds)
|
||||
|
||||
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',
|
||||
@@ -42,27 +39,55 @@ const RecordedPlayback = ({ route }) => {
|
||||
metadata: { projectId: project?.id, screen: 'RecordedPlayback' },
|
||||
})
|
||||
|
||||
// Optionnel: si tu veux quand même un rendu vidéo sur web si tu as une source
|
||||
const videoElRef = useRef(null)
|
||||
const syncOffsetRef = useRef(Math.max(0, Number(syncOffsetMs) || 0) / 1000)
|
||||
const playbackEndedRef = useRef(false)
|
||||
const shouldPreserveBlobRef = useRef(false)
|
||||
const syncOffsetRef = useRef(initialSyncOffsetMs)
|
||||
|
||||
const [progress, setProgress] = useState({
|
||||
posS: 0, // secondes
|
||||
durS: 0, // secondes
|
||||
playing: false,
|
||||
const [progressInfo, setProgressInfo] = useState({
|
||||
pos: 0,
|
||||
dur: 0,
|
||||
isPlaying: false,
|
||||
})
|
||||
const [currentSyncOffsetMs, setCurrentSyncOffsetMs] = useState(initialSyncOffsetMs)
|
||||
|
||||
const getCurrentRawAudioPositionMs = useCallback(
|
||||
() => Math.max(0, (audioPlayer?.currentTime || 0) * 1000),
|
||||
[audioPlayer]
|
||||
)
|
||||
|
||||
const getCurrentRawAudioDurationMs = useCallback(
|
||||
() => Math.max(0, (audioPlayer?.duration || 0) * 1000),
|
||||
[audioPlayer]
|
||||
)
|
||||
|
||||
const alignPlaybackToTimeline = useCallback(
|
||||
async (timelineMs, offsetMs = syncOffsetRef.current) => {
|
||||
const safeOffsetMs = clampSyncOffsetMs(offsetMs)
|
||||
const safeTimelineMs = Math.max(0, Number(timelineMs) || 0)
|
||||
const audioTargetMs = getAudioPositionMs(safeTimelineMs, safeOffsetMs)
|
||||
const videoTargetMs = getVideoPositionMs(safeTimelineMs, safeOffsetMs)
|
||||
|
||||
if (audioPlayer) {
|
||||
await audioPlayer.seekTo?.(audioTargetMs / 1000)
|
||||
}
|
||||
|
||||
if (videoElRef.current && videoUri) {
|
||||
videoElRef.current.currentTime = Math.max(0, videoTargetMs / 1000)
|
||||
}
|
||||
},
|
||||
[audioPlayer, videoUri]
|
||||
)
|
||||
|
||||
const stopPlayback = useCallback(async () => {
|
||||
try {
|
||||
if (audioPlayer) await audioPlayer.pause?.()
|
||||
} catch { }
|
||||
} catch {}
|
||||
try {
|
||||
if (videoElRef.current && !videoElRef.current.paused) {
|
||||
videoElRef.current.pause()
|
||||
}
|
||||
} catch { }
|
||||
} catch {}
|
||||
}, [audioPlayer])
|
||||
|
||||
useFocusEffect(
|
||||
@@ -81,153 +106,166 @@ const RecordedPlayback = ({ route }) => {
|
||||
}
|
||||
}, [videoUri])
|
||||
|
||||
// Démarrage / arrêt
|
||||
useEffect(() => {
|
||||
playbackEndedRef.current = false
|
||||
syncOffsetRef.current = initialSyncOffsetMs
|
||||
setCurrentSyncOffsetMs(initialSyncOffsetMs)
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
if (videoElRef.current && videoUri && syncOffsetRef.current > 0) {
|
||||
videoElRef.current.currentTime = Math.max(0, syncOffsetRef.current)
|
||||
}
|
||||
await alignPlaybackToTimeline(0, initialSyncOffsetMs)
|
||||
|
||||
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(() => { })
|
||||
videoElRef.current.play().catch(() => {})
|
||||
}
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
start()
|
||||
|
||||
void start()
|
||||
|
||||
return () => {
|
||||
playbackEndedRef.current = false
|
||||
void stopPlayback()
|
||||
}
|
||||
}, [audioPlayer, songUrl, stopPlayback, videoUri])
|
||||
}, [alignPlaybackToTimeline, audioPlayer, initialSyncOffsetMs, songUrl, stopPlayback, 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
|
||||
const rawDurationMs = getCurrentRawAudioDurationMs()
|
||||
const rawAudioMs = getCurrentRawAudioPositionMs()
|
||||
const nextDurationMs = getTimelineDurationMs(rawDurationMs, syncOffsetRef.current)
|
||||
const nextPositionMs = getTimelinePositionMs(rawAudioMs, syncOffsetRef.current)
|
||||
const safePositionMs =
|
||||
nextDurationMs > 0 ? Math.min(nextPositionMs, nextDurationMs) : nextPositionMs
|
||||
const isVideoPlaying = !!(videoElRef.current && !videoElRef.current.paused)
|
||||
const isPlaying = !!audioPlayer?.playing || isVideoPlaying
|
||||
|
||||
setProgress({
|
||||
posS,
|
||||
durS,
|
||||
playing: !!audioPlayer?.playing,
|
||||
setProgressInfo({
|
||||
pos: safePositionMs,
|
||||
dur: nextDurationMs,
|
||||
isPlaying,
|
||||
})
|
||||
|
||||
// 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 expected = Math.max(0, posS + (syncOffsetRef.current || 0))
|
||||
const drift = Math.abs(v - expected)
|
||||
if (drift > 0.35) {
|
||||
videoElRef.current.currentTime = expected
|
||||
const expectedVideoMs = getVideoPositionMs(safePositionMs, syncOffsetRef.current)
|
||||
const currentVideoMs = Number(videoElRef.current.currentTime || 0) * 1000
|
||||
const driftMs = Math.abs(currentVideoMs - expectedVideoMs)
|
||||
|
||||
if (driftMs > 350) {
|
||||
videoElRef.current.currentTime = Math.max(0, expectedVideoMs / 1000)
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
} 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 pendingSeekRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
const duration = progress?.durS || 0
|
||||
if (!duration) return
|
||||
const position = progress?.posS || 0
|
||||
if (playbackEndedRef.current && duration - position > 1) {
|
||||
playbackEndedRef.current = false
|
||||
return
|
||||
}
|
||||
const remaining = Math.max(0, duration - position)
|
||||
if (remaining <= 0.35 && !playbackEndedRef.current) {
|
||||
playbackEndedRef.current = true
|
||||
void (async () => {
|
||||
await stopPlayback()
|
||||
try {
|
||||
if (audioPlayer) await audioPlayer.seekTo?.(0)
|
||||
} catch { }
|
||||
try {
|
||||
if (videoElRef.current) {
|
||||
videoElRef.current.currentTime = Math.max(0, syncOffsetRef.current || 0)
|
||||
}
|
||||
} catch { }
|
||||
})()
|
||||
}
|
||||
}, [progress, stopPlayback, audioPlayer])
|
||||
}, [audioPlayer, getCurrentRawAudioDurationMs, getCurrentRawAudioPositionMs, videoUri])
|
||||
|
||||
const onSeek = useCallback(
|
||||
async (ratio) => {
|
||||
const durS = Number(progress.durS || 0)
|
||||
const target = durS > 0 ? durS * ratio : 0 // secondes
|
||||
const promise = (async () => {
|
||||
try {
|
||||
if (audioPlayer && durS > 0) {
|
||||
await audioPlayer.seekTo?.(Math.max(0, target))
|
||||
}
|
||||
if (videoElRef.current && videoUri) {
|
||||
const offset = syncOffsetRef.current || 0
|
||||
videoElRef.current.currentTime = Math.max(0, target + offset)
|
||||
}
|
||||
} catch { }
|
||||
})()
|
||||
pendingSeekRef.current = promise
|
||||
await promise
|
||||
async (targetMs) => {
|
||||
try {
|
||||
const durationMs = progressInfo.dur || 0
|
||||
const safeTargetMs = Math.max(0, Math.min(durationMs, Math.floor(targetMs)))
|
||||
await alignPlaybackToTimeline(safeTargetMs)
|
||||
} catch {}
|
||||
},
|
||||
[audioPlayer, progress.durS, videoUri]
|
||||
[alignPlaybackToTimeline, progressInfo.dur]
|
||||
)
|
||||
|
||||
const waitForPendingSeek = useCallback(async () => {
|
||||
const promise = pendingSeekRef.current
|
||||
pendingSeekRef.current = null
|
||||
if (!promise) return
|
||||
try {
|
||||
await promise
|
||||
} catch { }
|
||||
const onSeekStart = useCallback(() => {
|
||||
playbackEndedRef.current = false
|
||||
}, [])
|
||||
|
||||
const wasPlayingRef = useRef(false)
|
||||
const isSeekingRef = useRef(false)
|
||||
const onSeekStart = useCallback(async () => {
|
||||
const pauseDuringSeek = useCallback(async () => {
|
||||
try {
|
||||
if (isSeekingRef.current) return
|
||||
isSeekingRef.current = true
|
||||
wasPlayingRef.current = !!audioPlayer?.playing
|
||||
pendingSeekRef.current = null
|
||||
playbackEndedRef.current = false
|
||||
if (audioPlayer?.playing) await audioPlayer.pause?.()
|
||||
} catch {}
|
||||
try {
|
||||
if (videoElRef.current && !videoElRef.current.paused) {
|
||||
videoElRef.current.pause()
|
||||
}
|
||||
} catch { }
|
||||
} catch {}
|
||||
}, [audioPlayer])
|
||||
const onSeekEnd = useCallback(async () => {
|
||||
|
||||
const resumeAfterSeek = useCallback(async () => {
|
||||
try {
|
||||
if (!isSeekingRef.current) return
|
||||
await waitForPendingSeek()
|
||||
isSeekingRef.current = false
|
||||
if (wasPlayingRef.current) {
|
||||
if (audioPlayer) await audioPlayer.resume?.()
|
||||
if (videoElRef.current && videoUri) videoElRef.current.play().catch(() => { })
|
||||
if (audioPlayer?.resume) {
|
||||
await audioPlayer.resume?.()
|
||||
} else if (audioPlayer) {
|
||||
await audioPlayer.play?.()
|
||||
}
|
||||
} catch { }
|
||||
}, [audioPlayer, videoUri, waitForPendingSeek])
|
||||
} catch {}
|
||||
try {
|
||||
if (videoElRef.current && videoUri) {
|
||||
videoElRef.current.muted = true
|
||||
videoElRef.current.play().catch(() => {})
|
||||
}
|
||||
} catch {}
|
||||
}, [audioPlayer, videoUri])
|
||||
|
||||
const handleSyncOffsetChange = useCallback(
|
||||
(nextOffsetMs) => {
|
||||
const safeOffsetMs = snapSyncOffsetMs(nextOffsetMs)
|
||||
setCurrentSyncOffsetMs((prevOffsetMs) =>
|
||||
prevOffsetMs === safeOffsetMs ? prevOffsetMs : safeOffsetMs
|
||||
)
|
||||
|
||||
const previousOffsetMs = syncOffsetRef.current
|
||||
if (safeOffsetMs === previousOffsetMs) return
|
||||
|
||||
playbackEndedRef.current = false
|
||||
|
||||
const rawAudioMs = getCurrentRawAudioPositionMs()
|
||||
const rawDurationMs = getCurrentRawAudioDurationMs()
|
||||
const currentTimelineMs = getTimelinePositionMs(rawAudioMs, previousOffsetMs)
|
||||
const nextDurationMs = getTimelineDurationMs(rawDurationMs, safeOffsetMs)
|
||||
const nextTimelineMs =
|
||||
nextDurationMs > 0 ? Math.min(currentTimelineMs, nextDurationMs) : currentTimelineMs
|
||||
const isVideoPlaying = !!(videoElRef.current && !videoElRef.current.paused)
|
||||
|
||||
syncOffsetRef.current = safeOffsetMs
|
||||
|
||||
setProgressInfo({
|
||||
pos: nextTimelineMs,
|
||||
dur: nextDurationMs,
|
||||
isPlaying: !!audioPlayer?.playing || isVideoPlaying,
|
||||
})
|
||||
|
||||
void alignPlaybackToTimeline(nextTimelineMs, safeOffsetMs)
|
||||
},
|
||||
[alignPlaybackToTimeline, audioPlayer?.playing, getCurrentRawAudioDurationMs, getCurrentRawAudioPositionMs]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const durationMs = progressInfo?.dur || 0
|
||||
if (!durationMs) return
|
||||
|
||||
const positionMs = progressInfo?.pos || 0
|
||||
if (playbackEndedRef.current && durationMs - positionMs > 1000) {
|
||||
playbackEndedRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
const remainingMs = Math.max(0, durationMs - positionMs)
|
||||
if (remainingMs <= 400 && !playbackEndedRef.current) {
|
||||
playbackEndedRef.current = true
|
||||
void stopPlayback()
|
||||
}
|
||||
}, [progressInfo, stopPlayback])
|
||||
|
||||
const handleTogglePlayback = useCallback(async () => {
|
||||
try {
|
||||
const duration = Number(progress?.durS || 0)
|
||||
const position = Number(progress?.posS || 0)
|
||||
const isAtEnd = duration > 0 && duration - position < 0.35
|
||||
const isCurrentlyPlaying = !!audioPlayer?.playing
|
||||
const durationMs = progressInfo?.dur || 0
|
||||
const positionMs = progressInfo?.pos || 0
|
||||
const isAtEnd = durationMs > 0 && durationMs - positionMs < 1000
|
||||
const isVideoPlaying = !!(videoElRef.current && !videoElRef.current.paused)
|
||||
const isCurrentlyPlaying = !!audioPlayer?.playing || isVideoPlaying
|
||||
|
||||
if (isCurrentlyPlaying) {
|
||||
playbackEndedRef.current = false
|
||||
@@ -236,22 +274,25 @@ const RecordedPlayback = ({ route }) => {
|
||||
}
|
||||
|
||||
if (isAtEnd) {
|
||||
if (audioPlayer) await audioPlayer.seekTo?.(0)
|
||||
if (videoElRef.current && videoUri) {
|
||||
videoElRef.current.currentTime = Math.max(0, syncOffsetRef.current || 0)
|
||||
}
|
||||
await alignPlaybackToTimeline(0)
|
||||
}
|
||||
|
||||
playbackEndedRef.current = false
|
||||
|
||||
if (songUrl && audioPlayer) {
|
||||
await audioPlayer.play?.()
|
||||
if (audioPlayer?.resume) {
|
||||
await audioPlayer.resume?.()
|
||||
} else {
|
||||
await audioPlayer.play?.()
|
||||
}
|
||||
}
|
||||
|
||||
if (videoElRef.current && videoUri) {
|
||||
videoElRef.current.muted = true
|
||||
videoElRef.current.play().catch(() => { })
|
||||
videoElRef.current.play().catch(() => {})
|
||||
}
|
||||
} catch { }
|
||||
}, [audioPlayer, songUrl, stopPlayback, progress, videoUri])
|
||||
} catch {}
|
||||
}, [alignPlaybackToTimeline, audioPlayer, progressInfo, songUrl, stopPlayback, videoUri])
|
||||
|
||||
return (
|
||||
<Page
|
||||
@@ -273,7 +314,6 @@ const RecordedPlayback = ({ route }) => {
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{/* Bloc vidéo optionnel si jamais tu as un videoUri sur web */}
|
||||
{videoUri ? (
|
||||
<Pressable
|
||||
onPress={handleTogglePlayback}
|
||||
@@ -301,7 +341,7 @@ const RecordedPlayback = ({ route }) => {
|
||||
}}
|
||||
controls={false}
|
||||
/>
|
||||
{!progress.playing && (
|
||||
{!progressInfo.isPlaying && (
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={{
|
||||
@@ -324,7 +364,6 @@ const RecordedPlayback = ({ route }) => {
|
||||
)}
|
||||
</Pressable>
|
||||
) : (
|
||||
// Placeholder quand pas de vidéo sur web
|
||||
<View
|
||||
style={{
|
||||
width: isWeb ? WEB_PREVIEW_WIDTH : '80%',
|
||||
@@ -349,21 +388,25 @@ const RecordedPlayback = ({ route }) => {
|
||||
width: isWeb ? WEB_PREVIEW_WIDTH : '80%',
|
||||
maxWidth: '100%',
|
||||
alignSelf: 'center',
|
||||
marginTop: 6,
|
||||
alignItems: 'center',
|
||||
marginTop: 12,
|
||||
gap: 18,
|
||||
}}
|
||||
>
|
||||
<View style={{ width: '100%' }}>
|
||||
<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>
|
||||
<ProgressSlider
|
||||
positionMs={progressInfo.pos}
|
||||
durationMs={progressInfo.dur}
|
||||
isPlaying={progressInfo.isPlaying}
|
||||
onSeek={onSeek}
|
||||
onSeekStart={onSeekStart}
|
||||
onPause={pauseDuringSeek}
|
||||
onPlay={resumeAfterSeek}
|
||||
disabled={!songUrl}
|
||||
/>
|
||||
<SyncOffsetSlider
|
||||
valueMs={currentSyncOffsetMs}
|
||||
onChange={handleSyncOffsetChange}
|
||||
disabled={!songUrl}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -382,11 +425,12 @@ const RecordedPlayback = ({ route }) => {
|
||||
shouldPreserveBlobRef.current = true
|
||||
try {
|
||||
await stopPlayback()
|
||||
} catch { }
|
||||
} catch {}
|
||||
navigate(Routes.PlaybackDownload, {
|
||||
action: 'playback',
|
||||
uri: videoUri || null, // peut être null sur web
|
||||
uri: videoUri || null,
|
||||
project,
|
||||
syncOffsetMs: syncOffsetRef.current,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
@@ -395,7 +439,7 @@ const RecordedPlayback = ({ route }) => {
|
||||
onPress={async () => {
|
||||
try {
|
||||
await stopPlayback()
|
||||
} catch { }
|
||||
} catch {}
|
||||
navigate(Routes.RecordPlayback, { project })
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,7 @@ import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||
import { Image as ExpoImage } from 'expo-image'
|
||||
import SubscriptionConfirmModal from '../../components/SubscriptionConfirmModal'
|
||||
import { isWeb } from '../../hooks/useLayoutType'
|
||||
import { clampSyncOffsetMs, toSyncOffsetSeconds } from '../../utils/playbackSync'
|
||||
|
||||
const triggerDownload = async (url, title) => {
|
||||
if (!url) return
|
||||
@@ -144,13 +145,15 @@ const PlaybackDownload = ({ route }) => {
|
||||
const { currentUID, selectedProject } = useUserData()
|
||||
const { hasActiveSubscription, hasPurchased, videos } = useUser() || {}
|
||||
const { createPlaybackDownloadCheckout } = useStripe()
|
||||
const { action: routeAction, uri, project: routeProject } = route.params || {}
|
||||
const { action: routeAction, uri, project: routeProject, syncOffsetMs: routeSyncOffsetMs = 0 } = route.params || {}
|
||||
const action = routeAction || 'playback'
|
||||
const syncOffsetMs = clampSyncOffsetMs(routeSyncOffsetMs)
|
||||
console.log('[PlaybackDownload] route params', {
|
||||
action,
|
||||
projectId: routeProject?.id,
|
||||
hasUri: Boolean(uri),
|
||||
currentUID,
|
||||
syncOffsetMs,
|
||||
})
|
||||
const { setIsLoading, setTooltip } = useMinuit()
|
||||
const [isAfterPlaybackVideoVisible, setIsAfterPlaybackVideoVisible] = useState(false)
|
||||
@@ -259,6 +262,7 @@ const PlaybackDownload = ({ route }) => {
|
||||
videoUrl,
|
||||
audioUrl,
|
||||
storagePath: `users/${currentUID}/projects/${projectForDownload.id}/playback.mp4`,
|
||||
syncOffset: toSyncOffsetSeconds(syncOffsetMs),
|
||||
}
|
||||
|
||||
console.log('[PlaybackDownload] calling upload-mergeVideoAndAudio', payload)
|
||||
@@ -297,7 +301,7 @@ const PlaybackDownload = ({ route }) => {
|
||||
releaseBlobUrl(uri || null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [action, currentUID, pendingPlaybackUrl, projectForDownload, resolveAudioUrl, setIsLoading, uri])
|
||||
}, [action, currentUID, pendingPlaybackUrl, projectForDownload, resolveAudioUrl, setIsLoading, syncOffsetMs, uri])
|
||||
|
||||
const handleDownloadUri = useCallback(async () => {
|
||||
if (isPublishing || isDownloading) return
|
||||
|
||||
Reference in New Issue
Block a user