diff --git a/functions/index.js b/functions/index.js index fe8b6af..530300d 100644 --- a/functions/index.js +++ b/functions/index.js @@ -13,3 +13,4 @@ exports.music = require("./src/music"); exports.lyrics = require("./src/lyrics"); exports.cover = require("./src/cover"); exports.projects = require("./src/project"); +exports.hls = require("./src/hls"); diff --git a/functions/package-lock.json b/functions/package-lock.json index 44c40d7..33c787a 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -8,8 +8,10 @@ "dependencies": { "@genkit-ai/googleai": "^1.18.0", "axios": "^1.11.0", + "ffmpeg-static": "^5.2.0", "firebase-admin": "^12.7.0", "firebase-functions": "^6.4.0", + "fluent-ffmpeg": "^2.1.2", "genkit": "^1.18.0" }, "devDependencies": { @@ -42,6 +44,21 @@ "kuler": "^2.0.0" } }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "license": "MIT", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", @@ -2819,6 +2836,12 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/bufrw": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/bufrw/-/bufrw-1.4.0.tgz", @@ -2881,6 +2904,12 @@ "node": ">=6" } }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3011,6 +3040,21 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -3250,6 +3294,15 @@ "once": "^1.4.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/error": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", @@ -3721,6 +3774,47 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/ffmpeg-static": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz", + "integrity": "sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==", + "hasInstallScript": true, + "license": "GPL-3.0-or-later", + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ffmpeg-static/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ffmpeg-static/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3852,6 +3946,37 @@ "dev": true, "license": "ISC" }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", + "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "async": "^0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fluent-ffmpeg/node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "node_modules/fluent-ffmpeg/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", @@ -4448,6 +4573,21 @@ "node": ">= 6.0.0" } }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "license": "MIT", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -4639,7 +4779,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/jaeger-client": { @@ -5297,6 +5436,11 @@ "node": ">=6" } }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5449,6 +5593,15 @@ "node": ">= 0.6.0" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proto3-json-serializer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", @@ -5603,7 +5756,6 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", - "optional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -6042,7 +6194,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", - "optional": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -6324,6 +6475,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -6379,8 +6536,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", diff --git a/functions/package.json b/functions/package.json index 0a01f0b..fda7ae9 100644 --- a/functions/package.json +++ b/functions/package.json @@ -18,7 +18,9 @@ "axios": "^1.11.0", "firebase-admin": "^12.7.0", "firebase-functions": "^6.4.0", - "genkit": "^1.18.0" + "genkit": "^1.18.0", + "fluent-ffmpeg": "^2.1.2", + "ffmpeg-static": "^5.2.0" }, "devDependencies": { "eslint": "^8.15.0", diff --git a/functions/src/hls.js b/functions/src/hls.js new file mode 100644 index 0000000..2b52d80 --- /dev/null +++ b/functions/src/hls.js @@ -0,0 +1,161 @@ +const { onObjectFinalized } = require('firebase-functions/v2/storage'); +const functions = require('firebase-functions'); +const admin = require('firebase-admin'); +const path = require('path'); +const os = require('os'); +const fs = require('fs'); + +// These deps must be installed in functions/: ffmpeg-static, fluent-ffmpeg +let ffmpeg; +let ffmpegPath; +try { + ffmpeg = require('fluent-ffmpeg'); + ffmpegPath = require('ffmpeg-static'); + ffmpeg.setFfmpegPath(ffmpegPath); +} catch (e) { + console.log( + 'ffmpeg modules not installed yet — deploy after npm i', + e?.message + ); +} + +const bucket = admin.storage().bucket(); + +exports.transcodePlaybackToHLS = onObjectFinalized( + { + region: 'europe-west1', + memory: '2GiB', + timeoutSeconds: 540, + cpu: 2, + }, + async (event) => { + const object = event.data; + const filePath = object?.name || ''; // e.g., musics/{projectId}/source.mp4 + const contentType = object?.contentType || ''; + + try { + // Only process our target path + video files + const match = filePath.match(/^musics\/(.+?)\/source\.mp4$/i); + if (!match || !contentType.startsWith('video/')) { + return; + } + const projectId = match[1]; + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), `hls_${projectId}_`) + ); + const localSource = path.join(tempDir, 'source.mp4'); + const hlsDir = path.join(tempDir, 'hls'); + fs.mkdirSync(hlsDir); + + // Download source + await bucket.file(filePath).download({ destination: localSource }); + + if (!ffmpeg) { + console.warn('ffmpeg not available; skip transcode'); + return; + } + + // Transcode to HLS (single rendition for simplicity) + const masterPath = path.join(hlsDir, 'master.m3u8'); + const segmentPattern = path.join(hlsDir, 'segment_%03d.ts'); + + await new Promise((resolve, reject) => { + ffmpeg(localSource) + .outputOptions([ + '-profile:v baseline', + '-level 3.0', + '-start_number 0', + '-hls_time 4', + '-hls_list_size 0', + '-f hls', + `-hls_segment_filename ${segmentPattern}`, + ]) + .output(masterPath) + .on('end', resolve) + .on('error', reject) + .run(); + }); + + // Upload all files in hlsDir to Storage under musics/{projectId}/hls/ + const fileNames = fs.readdirSync(hlsDir); + const destPrefix = `musics/${projectId}/hls/`; + + // Upload all segments first + const uploads = fileNames + .filter((n) => n.endsWith('.ts')) + .map((name) => { + const local = path.join(hlsDir, name); + const dest = `${destPrefix}${name}`; + return bucket.upload(local, { + destination: dest, + metadata: { + contentType: 'video/MP2T', + cacheControl: 'public, max-age=3600', + }, + }); + }); + await Promise.all(uploads); + + // Generate signed URLs for segments and rewrite playlist with absolute URLs + const segmentNames = fileNames.filter((n) => n.endsWith('.ts')); + const signedSegments = {}; + await Promise.all( + segmentNames.map(async (name) => { + const file = bucket.file(`${destPrefix}${name}`); + const [url] = await file.getSignedUrl({ + action: 'read', + expires: Date.now() + 1000 * 60 * 60 * 24 * 365, + }); + signedSegments[name] = url; + }) + ); + + // Rewrite master playlist to use absolute signed segment URLs + let masterContent = fs.readFileSync(masterPath, 'utf8'); + masterContent = masterContent + .split(/\r?\n/) + .map((line) => + line.endsWith('.ts') && signedSegments[line] + ? signedSegments[line] + : line + ) + .join('\n'); + const rewrittenMasterLocal = path.join(hlsDir, 'master.abs.m3u8'); + fs.writeFileSync(rewrittenMasterLocal, masterContent, 'utf8'); + + // Upload rewritten playlist + await bucket.upload(rewrittenMasterLocal, { + destination: `${destPrefix}master.m3u8`, + metadata: { + contentType: 'application/vnd.apple.mpegurl', + cacheControl: 'public, max-age=3600', + }, + }); + + // Compute signed URL for playlist + const [signedUrl] = await bucket + .file(`${destPrefix}master.m3u8`) + .getSignedUrl({ + action: 'read', + expires: Date.now() + 1000 * 60 * 60 * 24 * 365, + }); + + // Update Firestore project doc with playbackUrl + await admin.firestore().collection('projects').doc(projectId).set( + { + playbackUrl: signedUrl, + playbackStatus: 'ready', + updatedAt: admin.firestore.FieldValue.serverTimestamp(), + }, + { merge: true } + ); + + // Cleanup tmp dir + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch (e) {} + } catch (e) { + console.error('HLS transcode error', e); + } + } +); diff --git a/src/screens/Playback/RecordPlayback.js b/src/screens/Playback/RecordPlayback.js index 2af04ac..1100b26 100644 --- a/src/screens/Playback/RecordPlayback.js +++ b/src/screens/Playback/RecordPlayback.js @@ -1,25 +1,25 @@ -import { useFocusEffect } from "@react-navigation/native"; -import { useAudioPlayer } from "expo-audio"; -import { CameraView, useCameraPermissions } from "expo-camera"; +import { useFocusEffect } from '@react-navigation/native'; +import { useAudioPlayer } from 'expo-audio'; +import { CameraView, useCameraPermissions } from 'expo-camera'; import React, { useCallback, useEffect, useMemo, useRef, useState, -} from "react"; -import { Alert, Text, View } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import Svg, { Circle } from "react-native-svg"; -import GradientButton from "../../components/GradientButton"; -import KaraokeLyrics from "../../components/KaraokeLyrics"; -import MusicLandHeader from "../../components/MusicLandHeader"; -import { increment, projectsRef } from "../../config/firebase"; -import { Routes } from "../../navigation"; -import { goBack, navigate } from "../../navigation/NavigationService"; -import { gutters, Palette } from "../../styles"; -import { FONT_FAMILY } from "../../styles/Fonts"; -import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; +} from 'react'; +import { Alert, Text, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import Svg, { Circle } from 'react-native-svg'; +import GradientButton from '../../components/GradientButton'; +import KaraokeLyrics from '../../components/KaraokeLyrics'; +import MusicLandHeader from '../../components/MusicLandHeader'; +import { increment, projectsRef } from '../../config/firebase'; +import { Routes } from '../../navigation'; +import { goBack, navigate } from '../../navigation/NavigationService'; +import { gutters, Palette } from '../../styles'; +import { FONT_FAMILY } from '../../styles/Fonts'; +import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'; const TIME_BEFORE_INCREMENT_MS = 20000; // 20s @@ -65,7 +65,7 @@ const RecordPlayback = ({ route }) => { const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []; return arr.map((w) => ({ - word: String(w?.word ?? ""), + word: String(w?.word ?? ''), startS: Number(w?.startS ?? 0), endS: Number(w?.endS ?? 0), })); @@ -77,16 +77,16 @@ const RecordPlayback = ({ route }) => { let buf = []; let start = null; const clean = (txt) => - String(txt || "") - .replace(/\s+/g, " ") + String(txt || '') + .replace(/\s+/g, ' ') .trim(); - const isSentenceEnd = (txt) => /[.!?]$/.test((txt || "").trim()); + const isSentenceEnd = (txt) => /[.!?]$/.test((txt || '').trim()); const isSectionTag = (txt) => - /^\s*\[[^\]]+\]\s*$/i.test((txt || "").trim()); + /^\s*\[[^\]]+\]\s*$/i.test((txt || '').trim()); for (let i = 0; i < alignedWords.length; i++) { const w = alignedWords[i]; - const original = String(w.word || ""); - const textNoNewline = original.replace(/\n/g, " "); + const original = String(w.word || ''); + const textNoNewline = original.replace(/\n/g, ' '); const gapToNext = i < alignedWords.length - 1 ? Math.max(0, alignedWords[i + 1].startS - w.endS) @@ -113,7 +113,7 @@ const RecordPlayback = ({ route }) => { const isLast = i === alignedWords.length - 1; if (eolByNewline || eolByPause || eolByPunct || isLast) { - const joined = clean(buf.join(" ")); + const joined = clean(buf.join(' ')); if (joined) out.push({ text: joined, startS: start ?? w.startS, endS: w.endS }); buf = []; @@ -343,7 +343,7 @@ const RecordPlayback = ({ route }) => { navigate(Routes.RecordedPlayback, { videoUri: video.uri, project }); else navigate(Routes.RecordedPlayback, { project }); } catch (e) { - console.log("RecordPlayback error:", e); + console.log('RecordPlayback error:', e); setIsRecording(false); setIsPreparing(false); setShowProgress(false); @@ -356,14 +356,14 @@ const RecordPlayback = ({ route }) => { checkSongEndRef.current = null; } // Inform the user when using a simulator where recording isn't supported - const msg = String(e?.message || e || ""); + const msg = String(e?.message || e || ''); if (/not supported on the simulator/i.test(msg)) { Alert.alert( - "Indisponible sur simulateur", - "L’enregistrement vidéo n’est pas disponible sur le simulateur. Merci d’utiliser un appareil réel.", + 'Indisponible sur simulateur', + 'L’enregistrement vidéo n’est pas disponible sur le simulateur. Merci d’utiliser un appareil réel.', [ { - text: "OK", + text: 'OK', onPress: () => { try { goBack(); @@ -384,17 +384,15 @@ const RecordPlayback = ({ route }) => { + facing='front' + mode='video'> + }}> @@ -402,22 +400,20 @@ const RecordPlayback = ({ route }) => { {isPreparing && countdown >= 1 && !showProgress && ( + alignItems: 'center', + justifyContent: 'center', + }}> + }}> {countdown} @@ -427,15 +423,14 @@ const RecordPlayback = ({ route }) => { {!permissionsGranted && ( + }}> { try { if (!cameraPermission?.granted) @@ -450,8 +445,8 @@ const RecordPlayback = ({ route }) => { {/* Start button */} {permissionsGranted && !isPreparing && !isRecording && ( @@ -461,23 +456,21 @@ const RecordPlayback = ({ route }) => { {permissionsGranted && (isRecording || showProgress) && ( + alignItems: 'center', + justifyContent: 'center', + }}> + alignItems: 'center', + justifyContent: 'center', + }}> { /> { {/* CreateLyricsHeader overlay outside of CameraView */} + }}> {(isRecording || showProgress) && alignedWords?.length > 0 ? ( { fontSize: 16, color: Palette.white, fontFamily: FONT_FAMILY.InterMedium, - }} - > + }}> L'enregistrement de ta vidéo commencera lorsque tu lanceras ta musique, et s'arrêtera à la fin du morceau. @@ -547,21 +538,20 @@ const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => { width={size} height={size} style={{ - transform: [{ rotate: "-90deg" }], - backgroundColor: "#ffffff3d", + transform: [{ rotate: '-90deg' }], + backgroundColor: '#ffffff3d', borderRadius: 55, - }} - > + }}> ); @@ -571,10 +561,10 @@ export default RecordPlayback; // Render previous/current/next line to reduce jumpiness const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => { - const prev = currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : ""; - const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : ""; + const prev = currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : ''; + const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : ''; const next = - currentLineIdx + 1 < lines.length ? lines[currentLineIdx + 1]?.text : ""; + currentLineIdx + 1 < lines.length ? lines[currentLineIdx + 1]?.text : ''; return ( {/* {prev ? ( @@ -586,21 +576,19 @@ const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => { style={{ color: Palette.white, fontSize: 18, - textAlign: "center", + textAlign: 'center', fontFamily: FONT_FAMILY.InterSemiBold, - }} - > + }}> {curr} {next ? ( + }}> {next} ) : null} diff --git a/src/screens/Production/DownloadSongs.js b/src/screens/Production/DownloadSongs.js index ed4c842..adebc85 100644 --- a/src/screens/Production/DownloadSongs.js +++ b/src/screens/Production/DownloadSongs.js @@ -1,5 +1,5 @@ -import { BlurView } from "expo-blur"; -import React from "react"; +import { BlurView } from 'expo-blur'; +import React from 'react'; import { Image, Platform, @@ -7,38 +7,38 @@ import { StyleSheet, Text, View, -} from "react-native"; -import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; -import { responsiveHeight } from "react-native-responsive-dimensions"; -import { background, img } from "../../assets"; -import MusicLandHeader from "../../components/MusicLandHeader"; -import { projectsRef, serverTimestamp } from "../../config/firebase"; -import { uploadFileToFirebase } from "../../helpers/uploadToFirebase"; -import Page from "../../layouts/Page"; -import { Routes } from "../../navigation"; -import { goBack, navigate } from "../../navigation/NavigationService"; -import { Palette } from "../../styles"; -import { FONT_FAMILY } from "../../styles/Fonts"; +} from 'react-native'; +import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'; +import { responsiveHeight } from 'react-native-responsive-dimensions'; +import { background, img } from '../../assets'; +import MusicLandHeader from '../../components/MusicLandHeader'; +import { projectsRef, serverTimestamp } from '../../config/firebase'; +import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'; +import Page from '../../layouts/Page'; +import { Routes } from '../../navigation'; +import { goBack, navigate } from '../../navigation/NavigationService'; +import { Palette } from '../../styles'; +import { FONT_FAMILY } from '../../styles/Fonts'; -import { size } from "../../styles/Style"; -import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; +import { size } from '../../styles/Style'; +import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'; const DownloadSongs = ({ route }) => { // const params = useRoute().params; const { action, uri, project } = route.params || {}; - console.log("project id", project?.id); + console.log('project id', project?.id); const { setIsLoading, setTooltip } = useMinuit(); const handleDownloadUri = async () => { - if (action === "playback" && project?.id) { + if (action === 'playback' && project?.id) { // Publication du playback - console.log("project id : ", project.id); + console.log('project id : ', project.id); try { setIsLoading(true); const { resultURI = null } = await uploadFileToFirebase({ uri: uri, - path: `musics/${project.id}/playback.mp4`, + path: `musics/${project.id}/source.mp4`, shouldCompress: true, - fileType: "VIDEO", + fileType: 'VIDEO', }); if (!resultURI) throw new Error("Téléversement de l'image impossible"); @@ -52,20 +52,20 @@ const DownloadSongs = ({ route }) => { { merge: true } ); setTooltip({ - type: "success", - text: "Playback publié avec succès", + type: 'success', + text: 'Vidéo uploadée, conversion HLS en cours…', }); } else { setTooltip({ - type: "error", - text: "Erreur lors de la publication du playback", + type: 'error', + text: 'Erreur lors de la publication du playback', }); } } catch (error) { - console.log("error upload playback", error); + console.log('error upload playback', error); setTooltip({ - type: "error", - text: "Erreur lors de la publication du playback", + type: 'error', + text: 'Erreur lors de la publication du playback', }); } finally { setIsLoading(false); @@ -78,39 +78,37 @@ const DownloadSongs = ({ route }) => { return ( + headerType='NONE'> + }}> - Prêt à télécharger{" "} - {action === "playback" ? "ton Playback" : "ta chanson"} ? + Prêt à télécharger{' '} + {action === 'playback' ? 'ton Playback' : 'ta chanson'} ? { - console.log("test"); + console.log('test'); handleDownloadUri(); } @@ -118,19 +116,18 @@ const DownloadSongs = ({ route }) => { // action, // uri, // }) - } - > + }> - Télécharger{" "} - {action === "playback" ? "mon Playback" : "ma chanson"} + Télécharger{' '} + {action === 'playback' ? 'mon Playback' : 'ma chanson'} Pour moi uniquement @@ -141,26 +138,25 @@ const DownloadSongs = ({ route }) => { navigate(Routes.StreamSong, { action, }) - } - > + }> - Diffuser{" "} - {action === "playback" ? "mon Playback" : "ma chanson"} sur + Diffuser{' '} + {action === 'playback' ? 'mon Playback' : 'ma chanson'} sur la plateforme de MusicLand (+ réseaux sociaux) et participer au concours Top 3: 1er 15% du CA ML - 2ème 10% du CA ML - 3ème 5% du CA - ML (catégorie{" "} - {action === "playback" ? "Playback" : "chanson"}) + ML (catégorie{' '} + {action === 'playback' ? 'Playback' : 'chanson'}) @@ -197,9 +193,9 @@ const styles = StyleSheet.create({ }, itemContainer: { borderRadius: 12, - overflow: "hidden", - backgroundColor: "#FFFFFF03", - shadowColor: "#0000001A", + overflow: 'hidden', + backgroundColor: '#FFFFFF03', + shadowColor: '#0000001A', shadowOffset: { width: 0, height: 2,