feat: download playback mp4

This commit is contained in:
2026-01-16 15:25:14 +01:00
parent f947cd194f
commit 9581a6fd4c
+35 -13
View File
@@ -2,6 +2,8 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'
import { Image, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native' import { Image, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
import { MaterialCommunityIcons } from '@expo/vector-icons' import { MaterialCommunityIcons } from '@expo/vector-icons'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js' import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import * as FileSystem from 'expo-file-system'
import { shareAsync } from 'expo-sharing'
import { background } from '../../assets' import { background } from '../../assets'
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo' import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
import BorderGradientButton from '../../components/BorderGradientButton' import BorderGradientButton from '../../components/BorderGradientButton'
@@ -24,14 +26,16 @@ import { Image as ExpoImage } from 'expo-image'
import SubscriptionConfirmModal from '../../components/SubscriptionConfirmModal' import SubscriptionConfirmModal from '../../components/SubscriptionConfirmModal'
import { isWeb } from '../../hooks/useLayoutType' import { isWeb } from '../../hooks/useLayoutType'
const triggerWebDownload = async (url, title) => { const triggerDownload = async (url, title) => {
if (Platform.OS !== 'web') return
if (!url) return if (!url) return
if (typeof document === 'undefined') return
const baseName = (title || 'Playback').toString().trim() || 'Playback' const baseName = (title || 'Playback').toString().trim() || 'Playback'
const sanitized = baseName.replace(/[\\/:*?"<>|]/g, '-') const sanitized = baseName.replace(/[\\/:*?"<>|]/g, '-')
const extension = guessExtension(url) || 'mp4' const extension = guessExtension(url) || 'mp4'
const filename = `${sanitized}.${extension}` const filename = `${sanitized}.${extension}`
// WEB
if (Platform.OS === 'web') {
if (typeof document === 'undefined') return
try { try {
const response = await fetch(url) const response = await fetch(url)
if (!response.ok || response.type === 'opaque') { if (!response.ok || response.type === 'opaque') {
@@ -46,11 +50,13 @@ const triggerWebDownload = async (url, title) => {
document.body.appendChild(anchor) document.body.appendChild(anchor)
anchor.click() anchor.click()
document.body.removeChild(anchor) document.body.removeChild(anchor)
URL.revokeObjectURL(blobUrl) // Revoke with delay to ensure browser captures it
setTimeout(() => URL.revokeObjectURL(blobUrl), 150)
} catch (error) { } catch (error) {
console.log('[PlaybackDownload] web download fallback', { console.log('[PlaybackDownload] web download fallback', {
message: error?.message, message: error?.message,
}) })
// First fallback: Anchor with _blank
try { try {
const anchor = document.createElement('a') const anchor = document.createElement('a')
anchor.href = url anchor.href = url
@@ -60,17 +66,32 @@ const triggerWebDownload = async (url, title) => {
document.body.appendChild(anchor) document.body.appendChild(anchor)
anchor.click() anchor.click()
document.body.removeChild(anchor) document.body.removeChild(anchor)
return
} catch (fallbackError) { } catch (fallbackError) {
console.log('[PlaybackDownload] anchor fallback failed', { console.log('[PlaybackDownload] anchor fallback failed', {
message: fallbackError?.message, message: fallbackError?.message,
}) })
} // Ultimate fallback: direct window open
try { try {
window.open(url, '_blank', 'noopener,noreferrer') window.open(url, '_blank', 'noopener,noreferrer')
} catch { } } catch { }
} }
} }
} else {
// NATIVE (Android / iOS)
try {
if (FileSystem?.downloadAsync) {
const fileUri = FileSystem.documentDirectory + filename
const { uri: localUri } = await FileSystem.downloadAsync(url, fileUri)
console.log('Finished downloading to ', localUri)
if (shareAsync) {
await shareAsync(localUri)
}
}
} catch (e) {
console.error(e)
}
}
}
const guessExtension = (inputUri = '') => { const guessExtension = (inputUri = '') => {
const cleaned = inputUri.split('?')[0] || '' const cleaned = inputUri.split('?')[0] || ''
@@ -156,17 +177,13 @@ const PlaybackDownload = ({ route }) => {
} }
}, [project?.playbackUrl]) }, [project?.playbackUrl])
useEffect(() => {
return () => {
releaseBlobUrl(uri || null)
}
}, [uri])
const handleDownloadUri = async () => { const handleDownloadUri = async () => {
if (isPublishing) return if (isPublishing) return
if (action === 'playback' && project?.id) { if (action === 'playback' && project?.id) {
if (pendingPlaybackUrl) { if (pendingPlaybackUrl) {
await triggerWebDownload(pendingPlaybackUrl, project?.title) await triggerDownload(pendingPlaybackUrl, project?.title)
return return
} }
// Publication du playback // Publication du playback
@@ -216,7 +233,7 @@ const PlaybackDownload = ({ route }) => {
type: 'success', type: 'success',
text: 'Playback prêt à télécharger', text: 'Playback prêt à télécharger',
}) })
await triggerWebDownload(resultURI, project?.title) await triggerDownload(resultURI, project?.title)
if (tempSourcePath) { if (tempSourcePath) {
try { try {
await firebase.storage().ref(tempSourcePath).delete() await firebase.storage().ref(tempSourcePath).delete()
@@ -369,6 +386,11 @@ const PlaybackDownload = ({ route }) => {
const continueLabel = hasActiveSubscription ? 'Publier' : 'Publier sans générer de revenus' const continueLabel = hasActiveSubscription ? 'Publier' : 'Publier sans générer de revenus'
return ( return (
<> <>
<FullscreenIntroVideo
visible={isAfterPlaybackVideoVisible}
url={afterPlaybackUrl}
onClose={() => setIsAfterPlaybackVideoVisible(false)}
/>
<Page <Page
// backgroundImg={ // backgroundImg={
// action === "playback" // action === "playback"