feat: fix merge audio

This commit is contained in:
2026-02-27 13:42:14 +01:00
parent 582992551e
commit cdaf7b2727
5 changed files with 177 additions and 18 deletions
+3 -2
View File
@@ -12,6 +12,7 @@ const ffmpeg = require('fluent-ffmpeg')
const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg')
const { Buffer } = require('node:buffer')
const { FieldValue } = require('firebase-admin/firestore')
const { REGION } = require('../index')
if (!admin.apps.length) admin.initializeApp()
ffmpeg.setFfmpegPath(ffmpegInstaller.path)
@@ -151,7 +152,7 @@ async function markPlaybackCompatibility({ projectId, initiatorUid = null }) {
}
exports.mergeVideoAndAudio = onCall(
{ timeoutSeconds: 540, memory: '1GiB' },
{ region: REGION, timeoutSeconds: 540, memory: '1GiB' },
async ({ data = {}, auth }) => {
const uid = auth?.uid
if (!uid) throw new HttpsError('unauthenticated', 'Authentification requise')
@@ -176,7 +177,7 @@ exports.mergeVideoAndAudio = onCall(
)
exports.reencodePlayback = onCall(
{ timeoutSeconds: 540, memory: '1GiB' },
{ region: REGION, timeoutSeconds: 540, memory: '1GiB' },
async ({ data = {}, auth }) => {
const uid = auth?.uid
if (!uid) throw new HttpsError('unauthenticated', 'Authentification requise')
+131 -4
View File
@@ -1,7 +1,24 @@
import { Platform } from 'react-native'
import * as FileSystem from 'expo-file-system'
import Compressor from 'react-native-compressor'
import firebase from '../config/firebase'
const fetchBlobFromUri = (uri) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onload = () => resolve(xhr.response)
xhr.onerror = () => reject(new TypeError('Network request failed'))
xhr.responseType = 'blob'
xhr.open('GET', uri, true)
xhr.send(null)
})
const getContentTypeForFile = (fileType) => {
if (fileType === 'VIDEO') return 'video/mp4'
if (fileType === 'IMAGE') return 'image/jpeg'
return 'application/octet-stream'
}
export function uploadFileToFirebase({
uri,
path,
@@ -11,53 +28,163 @@ export function uploadFileToFirebase({
}) {
return new Promise(async (resolve, reject) => {
try {
console.log('[uploadToFirebase] start', {
path,
shouldCompress,
fileType,
hasBlob: Boolean(providedBlob),
uri,
})
let workingURI = uri
let uploadBlob = providedBlob
let uploadTask = null
// Optionally compress before upload
try {
if (shouldCompress && Platform.OS !== 'web') {
if (fileType === 'VIDEO') {
console.log('Compressing video...')
const compressStart = Date.now()
console.log('[uploadToFirebase] compress video start')
workingURI = await Compressor.Video.compress(workingURI)
console.log('[uploadToFirebase] compress video done', {
durationMs: Date.now() - compressStart,
workingURI,
})
} else if (fileType === 'IMAGE') {
// Basic image compression
const compressStart = Date.now()
console.log('[uploadToFirebase] compress image start')
workingURI = await Compressor.Image.compress(workingURI, {
compressionMethod: 'auto',
})
console.log('[uploadToFirebase] compress image done', {
durationMs: Date.now() - compressStart,
workingURI,
})
}
}
} catch (e) {
console.warn('Compression failed, uploading original file', e?.message)
console.warn('[uploadToFirebase] compression failed, using original file', {
message: e?.message,
code: e?.code,
name: e?.name,
})
workingURI = uri
}
if (!uploadBlob) {
const isLocalUri =
Platform.OS !== 'web' &&
typeof workingURI === 'string' &&
(workingURI.startsWith('file://') || workingURI.startsWith('content://'))
if (!uploadBlob && isLocalUri) {
try {
const bucket = firebase.app()?.options?.storageBucket
const token = await firebase.auth().currentUser?.getIdToken?.()
if (!bucket || !token) {
throw new Error('missing_bucket_or_token')
}
const contentType = getContentTypeForFile(fileType)
const uploadUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket}/o?uploadType=media&name=${encodeURIComponent(
path
)}`
console.log('[uploadToFirebase] upload via FileSystem', {
uploadUrl,
contentType,
})
const uploadResult = await FileSystem.uploadAsync(uploadUrl, workingURI, {
httpMethod: 'POST',
uploadType: FileSystem.FileSystemUploadType.BINARY_CONTENT,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': contentType,
},
})
console.log('[uploadToFirebase] uploadAsync result', {
status: uploadResult?.status,
})
if (uploadResult?.status < 200 || uploadResult?.status >= 300) {
throw new Error(`upload_failed_${uploadResult?.status || 'unknown'}`)
}
const resultURI = await firebase.storage().ref(path).getDownloadURL()
console.log('[uploadToFirebase] upload success (uploadAsync)', { resultURI })
resolve({ resultURI })
return
} catch (e) {
console.log('[uploadToFirebase] uploadAsync failed, fallback to xhr', {
message: e?.message,
code: e?.code,
name: e?.name,
})
}
console.log('[uploadToFirebase] fetching blob via xhr', { workingURI })
uploadBlob = await fetchBlobFromUri(workingURI)
console.log('[uploadToFirebase] blob ready (xhr)', {
size: uploadBlob?.size,
type: uploadBlob?.type,
})
} else if (!uploadBlob) {
console.log('[uploadToFirebase] fetching blob', { workingURI })
const response = await fetch(workingURI)
console.log('[uploadToFirebase] fetch blob response', {
ok: response?.ok,
status: response?.status,
type: response?.type,
})
uploadBlob = await response.blob()
console.log('[uploadToFirebase] blob ready', {
size: uploadBlob?.size,
type: uploadBlob?.type,
})
}
const uploadTask = firebase.storage().ref(path).put(uploadBlob)
if (!uploadTask) {
console.log('[uploadToFirebase] upload start', { path })
uploadTask = firebase.storage().ref(path).put(uploadBlob)
}
uploadTask.on(
Platform.OS === 'web' ? 'state_changed' : firebase.storage.TaskEvent.STATE_CHANGED,
(snapshot) => {
console.log('[uploadToFirebase] upload progress', {
state: snapshot?.state,
bytesTransferred: snapshot?.bytesTransferred,
totalBytes: snapshot?.totalBytes,
})
if (Platform.OS !== 'web' && snapshot.state === firebase.storage.TaskState.SUCCESS) {
snapshot.ref.getDownloadURL().then((resultURI) => {
console.log('[uploadToFirebase] upload success (native)', { resultURI })
if (uploadBlob && typeof uploadBlob.close === 'function') {
uploadBlob.close()
}
resolve({ resultURI })
})
}
},
(error) => {
console.log('[uploadToFirebase] upload error', {
message: error?.message,
code: error?.code,
name: error?.name,
})
reject(error)
},
async () => {
const url = await firebase.storage().ref(path).getDownloadURL()
console.log('[uploadToFirebase] upload success (finalize)', { resultURI: url })
if (uploadBlob && typeof uploadBlob.close === 'function') {
uploadBlob.close()
}
resolve({ resultURI: url })
}
)
} catch (e) {
console.log('[uploadToFirebase] unexpected error', {
message: e?.message,
code: e?.code,
name: e?.name,
})
reject(e)
}
})
+1
View File
@@ -41,6 +41,7 @@ const HIDDEN_ROUTE_NAMES = new Set([
Routes.ChooseDecor,
Routes.CreatingDecor,
Routes.VideoFinalize,
Routes.PublishYoutube,
Routes.FlowSelection,
Routes.ChooseCoverType,
Routes.Settings,
+38 -8
View File
@@ -9,7 +9,7 @@ import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import AppCheckbox from '../../components/AppCheckbox'
import firebase, { projectsRef, serverTimestamp } from '../../config/firebase'
import firebase, { getFunctionsClient, projectsRef, serverTimestamp } from '../../config/firebase'
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
@@ -135,6 +135,12 @@ const uploadSourceRecording = async ({ uri, uid, projectId }) => {
return { sourcePath, videoUrl }
}
const FUNCTIONS_REGION = 'europe-west1'
const callMergeVideoAndAudio = (payload) => {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable('upload-mergeVideoAndAudio')
return callable(payload)
}
const PlaybackDownload = ({ route }) => {
const { currentUID, selectedProject } = useUserData()
const { hasActiveSubscription, hasPurchased, videos } = useUser() || {}
@@ -282,8 +288,6 @@ const PlaybackDownload = ({ route }) => {
})
tempSourcePath = sourcePath
const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio')
const payload = {
projectId: projectForDownload?.id,
videoUrl,
@@ -293,7 +297,7 @@ const PlaybackDownload = ({ route }) => {
console.log('[PlaybackDownload] calling upload-mergeVideoAndAudio', payload)
const { data: result } = await callable(payload)
const { data: result } = await callMergeVideoAndAudio(payload)
console.log('[PlaybackDownload] upload-mergeVideoAndAudio result', result)
@@ -431,8 +435,12 @@ const PlaybackDownload = ({ route }) => {
}, [downloadPaymentPending, handleDownloadUri, hasPaidPlaybackDownload, isDownloading])
const handlePublish = async () => {
if (isPublishing) return
if (isPublishing) {
console.log('[PlaybackDownload] publish blocked: already publishing')
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',
@@ -440,6 +448,9 @@ const PlaybackDownload = ({ route }) => {
return
}
if (!projectForDownload?.id) {
console.log('[PlaybackDownload] publish: missing project id, navigating directly', {
projectForDownload: projectForDownload?.id,
})
setTooltip({
type: 'success',
text: publishSuccessMessage,
@@ -451,6 +462,10 @@ const PlaybackDownload = ({ route }) => {
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',
@@ -458,6 +473,7 @@ const PlaybackDownload = ({ route }) => {
return
}
if (!currentUID) {
console.log('[PlaybackDownload] publish blocked: missing currentUID')
setTooltip({
type: 'error',
text: 'Utilisateur non authentifié',
@@ -466,10 +482,18 @@ const PlaybackDownload = ({ route }) => {
}
try {
console.log('[PlaybackDownload] publish start', {
projectId: projectForDownload?.id,
hasPendingPlaybackUrl: Boolean(pendingPlaybackUrl),
hasPlaybackUrl: Boolean(projectForDownload?.playbackUrl),
hasAudioUrl: Boolean(audioUrl),
hasUri: Boolean(uri),
})
setIsPublishing(true)
setIsLoading(true)
if (!playbackUrlToSave) {
console.log('[PlaybackDownload] publish: no playback url, uploading source')
const { sourcePath, videoUrl } = await uploadSourceRecording({
uri,
uid: currentUID,
@@ -477,8 +501,6 @@ const PlaybackDownload = ({ route }) => {
})
tempSourcePath = sourcePath
const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio')
const payload = {
projectId: projectForDownload?.id,
videoUrl,
@@ -488,15 +510,20 @@ const PlaybackDownload = ({ route }) => {
console.log('[PlaybackDownload] publish: calling merge', payload)
const { data: result } = await callable(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(
{
playbackUrl: playbackUrlToSave,
@@ -509,6 +536,9 @@ const PlaybackDownload = ({ route }) => {
type: 'success',
text: publishSuccessMessage,
})
console.log('[PlaybackDownload] publish: navigate to PublishYoutube', {
projectId: projectForDownload.id,
})
navigate(Routes.PublishYoutube, { projectId: projectForDownload.id })
} catch (error) {
console.log('[PlaybackDownload] publish error', {
+4 -4
View File
@@ -119,7 +119,7 @@ const PublishYoutube = () => {
if (!hasAcceptedPublication) {
setTooltip?.({
type: 'error',
text: 'Confirme la diffusion sur Musicland et YouTube avant de publier',
text: 'Confirme la diffusion sur YouTube avant de publier',
})
return
}
@@ -131,7 +131,7 @@ const PublishYoutube = () => {
: 'Confirmer la publication'
const confirmDescription = hasYoutubePublication
? 'La vidéo existe déjà sur YouTube. Confirme que tu souhaites remplacer la version publiée.'
: 'Confirme que tu souhaites publier ce playback sur la chaîne YouTube MusicLand. Il sera visible publiquement.'
: 'Confirme que tu souhaites publier ce playback sur YouTube. Il sera visible publiquement.'
alert(confirmTitle, confirmDescription, [
{ text: 'Annuler', style: 'cancel' },
{
@@ -249,10 +249,10 @@ const PublishYoutube = () => {
<AppCheckbox
selected={hasAcceptedPublication}
onPress={() => setHasAcceptedPublication((prevState) => !prevState)}
label="J'accepte la diffusion de mon contenu sur Musicland et YouTube."
label="J'accepte la diffusion de mon contenu sur YouTube."
/>
<Text style={styles.consentDescription}>
Cette confirmation est requise avant toute mise en ligne.
Cette confirmation est requise avant toute publication sur YouTube.
</Text>
</View>
<GradientButton