fix playback offset

This commit is contained in:
Thomas Demirdjian
2026-04-16 15:19:39 +02:00
parent 5c2bdc593c
commit 5a6833c1a2
11 changed files with 623 additions and 274 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const TIME_BEFORE_INCREMENT_MS = 20000 // 20s
const COUNTDOWN_SECONDS = 10
const CAMERA_STARTUP_DELAY_MS = 250
const KARAOKE_LEAD_S = 0.18
const KARAOKE_LEAD_S = 0.68
const LOG_PREFIX = '[RecordPlayback]'
const KEEP_AWAKE_TAG = 'record-playback'
const log =
+1 -1
View File
@@ -30,7 +30,7 @@ const MEDIA_BOOTSTRAP_DELAY_MS = 250
const MEDIA_RETRY_DELAY_MS = 700
const MEDIA_MAX_RETRIES = 2
const CAMERA_STARTUP_DELAY_MS = 250
const KARAOKE_LEAD_S = 0.18
const KARAOKE_LEAD_S = 0.68
const toSeconds = (v) => {
const n = Number(v ?? 0)
+163 -47
View File
@@ -3,13 +3,12 @@ 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 { Image, Pressable, Text, View } from 'react-native'
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'
@@ -277,65 +276,182 @@ const RecordedPlayback = ({ route }) => {
return (
<Page backgroundColor={Palette.grayMid} headerType="NONE">
<MusicLandHeader progress={19} onPressBack={goBack} />
<View style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}>
<View style={{ flex: 1, gap: 22 }}>
<View style={{ flex: 1, paddingBottom: gutters * 2 }}>
{/* Vidéo avec play/pause en overlay */}
<Pressable
onPress={handleTogglePlayback}
style={{
flex: 1,
width: '92%',
alignSelf: 'center',
borderRadius: 18,
backgroundColor: '#00000066',
overflow: 'hidden',
marginTop: 10,
}}
>
{!!videoUri && (
<VideoView
player={videoPlayer}
nativeControls={false}
contentFit="contain"
style={{
width: '80%',
flex: 1,
alignSelf: 'center',
borderRadius: 16,
backgroundColor: '#00000066',
overflow: 'hidden',
}}
style={{ flex: 1, width: '100%', backgroundColor: 'transparent' }}
/>
)}
<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}
<View
pointerEvents="none"
style={{
width: 72,
height: 72,
borderRadius: 36,
alignSelf: 'center',
marginTop: 4,
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(10, 5, 24, 0.65)',
borderWidth: 1,
borderColor: '#F94697',
}}
>
<Image
source={progressInfo.isPlaying ? icons.pause : icons.play}
style={{ width: 26, height: 26 }}
/>
</Pressable>
{!progressInfo.isPlaying && (
<View
style={{
width: 80,
height: 80,
borderRadius: 40,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(10, 5, 24, 0.72)',
borderWidth: 1.5,
borderColor: '#F94697',
}}
>
<Image source={icons.play} style={{ width: 30, height: 30 }} />
</View>
)}
</View>
</Pressable>
{/* Contrôles */}
<View style={{ width: '92%', alignSelf: 'center', gap: 12, marginTop: 14 }}>
<ProgressSlider
positionMs={progressInfo.pos}
durationMs={progressInfo.dur}
isPlaying={progressInfo.isPlaying}
onSeek={onSeek}
onSeekStart={onSeekStart}
onPause={pauseDuringSeek}
onPlay={resumeAfterSeek}
disabled={!songUrl}
/>
<View
style={{
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: 16,
paddingVertical: 14,
paddingHorizontal: 16,
gap: 12,
}}
>
<Text
style={{
color: 'rgba(255,255,255,0.5)',
fontSize: 11,
letterSpacing: 1.2,
textAlign: 'center',
textTransform: 'uppercase',
}}
>
Synchro audio / vidéo
</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 16 }}>
<Pressable
onPress={() => handleSyncOffsetChange(clampSyncOffsetMs(currentSyncOffsetMs - 100))}
disabled={!songUrl || currentSyncOffsetMs <= -2000}
hitSlop={10}
style={({ pressed }) => ({
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: pressed ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.1)',
alignItems: 'center',
justifyContent: 'center',
opacity: !songUrl || currentSyncOffsetMs <= -2000 ? 0.3 : 1,
})}
>
<Text style={{ color: Palette.white, fontSize: 26, lineHeight: 30 }}>−</Text>
</Pressable>
<View style={{ alignItems: 'center', minWidth: 90 }}>
<Text
style={{
color: Palette.white,
fontSize: 28,
fontWeight: '700',
letterSpacing: -0.5,
}}
>
{currentSyncOffsetMs === 0
? '0'
: (currentSyncOffsetMs > 0 ? '+' : '') + currentSyncOffsetMs}
</Text>
<Text style={{ color: 'rgba(255,255,255,0.45)', fontSize: 11, marginTop: 2 }}>
{currentSyncOffsetMs === 0
? 'ms · synchronisé'
: currentSyncOffsetMs > 0
? 'ms · son en avance'
: 'ms · son en retard'}
</Text>
</View>
<Pressable
onPress={() => handleSyncOffsetChange(clampSyncOffsetMs(currentSyncOffsetMs + 100))}
disabled={!songUrl || currentSyncOffsetMs >= 2000}
hitSlop={10}
style={({ pressed }) => ({
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: pressed ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.1)',
alignItems: 'center',
justifyContent: 'center',
opacity: !songUrl || currentSyncOffsetMs >= 2000 ? 0.3 : 1,
})}
>
<Text style={{ color: Palette.white, fontSize: 26, lineHeight: 30 }}>+</Text>
</Pressable>
</View>
<Pressable
onPress={() => handleSyncOffsetChange(0)}
disabled={!songUrl || currentSyncOffsetMs === 0}
style={({ pressed }) => ({
alignSelf: 'center',
paddingHorizontal: 20,
paddingVertical: 6,
borderRadius: 14,
backgroundColor:
!songUrl || currentSyncOffsetMs === 0
? 'rgba(255,255,255,0.06)'
: pressed
? 'rgba(255,255,255,0.18)'
: 'rgba(255,255,255,0.1)',
opacity: !songUrl || currentSyncOffsetMs === 0 ? 0.4 : 1,
})}
>
<Text
style={{
color: Palette.white,
fontSize: 13,
fontWeight: '500',
}}
>
Remettre à zéro
</Text>
</Pressable>
</View>
</View>
<View style={{ width: '80%', alignSelf: 'center', marginTop: 4, gap: 12 }}>
<View style={{ width: '92%', alignSelf: 'center', marginTop: 14, gap: 12 }}>
<GradientButton
title="Je valide"
onPress={async () => {
+128 -29
View File
@@ -6,7 +6,6 @@ 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'
@@ -23,7 +22,7 @@ import {
snapSyncOffsetMs,
} from '../../utils/playbackSync'
const WEB_PREVIEW_WIDTH = 360
const WEB_PREVIEW_WIDTH = 420
const RecordedPlayback = ({ route }) => {
const { videoUri, project, syncOffsetMs = 0 } = route.params || {}
@@ -306,26 +305,23 @@ const RecordedPlayback = ({ route }) => {
progress={19}
onPressBack={goBack}
style={{
marginBottom: 40,
marginBottom: 20,
}}
/>
<View
style={{
width: '100%',
}}
>
<View style={{ width: '100%' }}>
{videoUri ? (
<Pressable
onPress={handleTogglePlayback}
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : '80%',
width: isWeb ? WEB_PREVIEW_WIDTH : '92%',
maxWidth: '100%',
aspectRatio: 9 / 16,
borderRadius: 12,
borderRadius: 18,
backgroundColor: '#00000066',
overflow: 'hidden',
alignSelf: 'center',
position: 'relative',
cursor: 'pointer',
}}
>
<video
@@ -348,28 +344,28 @@ const RecordedPlayback = ({ route }) => {
position: 'absolute',
left: '50%',
top: '50%',
transform: [{ translateX: -36 }, { translateY: -36 }],
width: 72,
height: 72,
borderRadius: 36,
transform: [{ translateX: -40 }, { translateY: -40 }],
width: 80,
height: 80,
borderRadius: 40,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(10, 5, 24, 0.75)',
borderWidth: 1,
backgroundColor: 'rgba(10, 5, 24, 0.72)',
borderWidth: 1.5,
borderColor: '#F94697',
}}
>
<Image source={icons.play} style={{ width: 26, height: 26 }} />
<Image source={icons.play} style={{ width: 30, height: 30 }} />
</View>
)}
</Pressable>
) : (
<View
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : '80%',
width: isWeb ? WEB_PREVIEW_WIDTH : '92%',
maxWidth: '100%',
aspectRatio: 9 / 16,
borderRadius: 12,
borderRadius: 18,
backgroundColor: '#00000066',
alignItems: 'center',
justifyContent: 'center',
@@ -385,11 +381,11 @@ const RecordedPlayback = ({ route }) => {
<View
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : '80%',
width: isWeb ? WEB_PREVIEW_WIDTH : '92%',
maxWidth: '100%',
alignSelf: 'center',
marginTop: 12,
gap: 18,
marginTop: 14,
gap: 12,
}}
>
<ProgressSlider
@@ -402,21 +398,124 @@ const RecordedPlayback = ({ route }) => {
onPlay={resumeAfterSeek}
disabled={!songUrl}
/>
<SyncOffsetSlider
valueMs={currentSyncOffsetMs}
onChange={handleSyncOffsetChange}
disabled={!songUrl}
/>
<View
style={{
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: 16,
paddingVertical: 14,
paddingHorizontal: 16,
gap: 12,
}}
>
<Text
style={{
color: 'rgba(255,255,255,0.5)',
fontSize: 11,
letterSpacing: 1.2,
textAlign: 'center',
textTransform: 'uppercase',
}}
>
Synchro audio / vidéo
</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 16 }}>
<Pressable
onPress={() => handleSyncOffsetChange(clampSyncOffsetMs(currentSyncOffsetMs - 100))}
disabled={!songUrl || currentSyncOffsetMs <= -2000}
style={({ pressed }) => ({
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: pressed ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.1)',
alignItems: 'center',
justifyContent: 'center',
cursor: !songUrl || currentSyncOffsetMs <= -2000 ? 'not-allowed' : 'pointer',
opacity: !songUrl || currentSyncOffsetMs <= -2000 ? 0.3 : 1,
})}
>
<Text style={{ color: Palette.white, fontSize: 26, lineHeight: 30 }}>−</Text>
</Pressable>
<View style={{ alignItems: 'center', minWidth: 90 }}>
<Text
style={{
color: Palette.white,
fontSize: 28,
fontWeight: '700',
letterSpacing: -0.5,
}}
>
{currentSyncOffsetMs === 0
? '0'
: (currentSyncOffsetMs > 0 ? '+' : '') + currentSyncOffsetMs}
</Text>
<Text style={{ color: 'rgba(255,255,255,0.45)', fontSize: 11, marginTop: 2 }}>
{currentSyncOffsetMs === 0
? 'ms · synchronisé'
: currentSyncOffsetMs > 0
? 'ms · son en avance'
: 'ms · son en retard'}
</Text>
</View>
<Pressable
onPress={() => handleSyncOffsetChange(clampSyncOffsetMs(currentSyncOffsetMs + 100))}
disabled={!songUrl || currentSyncOffsetMs >= 2000}
style={({ pressed }) => ({
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: pressed ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.1)',
alignItems: 'center',
justifyContent: 'center',
cursor: !songUrl || currentSyncOffsetMs >= 2000 ? 'not-allowed' : 'pointer',
opacity: !songUrl || currentSyncOffsetMs >= 2000 ? 0.3 : 1,
})}
>
<Text style={{ color: Palette.white, fontSize: 26, lineHeight: 30 }}>+</Text>
</Pressable>
</View>
<Pressable
onPress={() => handleSyncOffsetChange(0)}
disabled={!songUrl || currentSyncOffsetMs === 0}
style={({ pressed }) => ({
alignSelf: 'center',
paddingHorizontal: 20,
paddingVertical: 6,
borderRadius: 14,
backgroundColor:
!songUrl || currentSyncOffsetMs === 0
? 'rgba(255,255,255,0.06)'
: pressed
? 'rgba(255,255,255,0.18)'
: 'rgba(255,255,255,0.1)',
opacity: !songUrl || currentSyncOffsetMs === 0 ? 0.4 : 1,
cursor: !songUrl || currentSyncOffsetMs === 0 ? 'not-allowed' : 'pointer',
})}
>
<Text
style={{
color: Palette.white,
fontSize: 13,
fontWeight: '500',
}}
>
Remettre à zéro
</Text>
</Pressable>
</View>
</View>
</View>
<View
style={{
width: isWeb ? WEB_PREVIEW_WIDTH : '80%',
width: isWeb ? WEB_PREVIEW_WIDTH : '92%',
maxWidth: '100%',
alignSelf: 'center',
gap: 12,
marginTop: 50,
marginTop: 16,
}}
>
<GradientButton