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,