feat: fix merge audio
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -41,6 +41,7 @@ const HIDDEN_ROUTE_NAMES = new Set([
|
||||
Routes.ChooseDecor,
|
||||
Routes.CreatingDecor,
|
||||
Routes.VideoFinalize,
|
||||
Routes.PublishYoutube,
|
||||
Routes.FlowSelection,
|
||||
Routes.ChooseCoverType,
|
||||
Routes.Settings,
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user