dev - hls

This commit is contained in:
Victor
2025-09-17 13:51:02 +02:00
parent 2d4642891d
commit 12f87f0c92
6 changed files with 443 additions and 139 deletions
+1
View File
@@ -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");
+161 -5
View File
@@ -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",
+3 -1
View File
@@ -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",
+161
View File
@@ -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);
}
}
);
+67 -79
View File
@@ -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",
"Lenregistrement vidéo nest pas disponible sur le simulateur. Merci dutiliser un appareil réel.",
'Indisponible sur simulateur',
'Lenregistrement vidéo nest pas disponible sur le simulateur. Merci dutiliser un appareil réel.',
[
{
text: "OK",
text: 'OK',
onPress: () => {
try {
goBack();
@@ -384,17 +384,15 @@ const RecordPlayback = ({ route }) => {
<CameraView
ref={cameraRef}
style={{ flex: 1 }}
facing="front"
mode="video"
>
facing='front'
mode='video'>
<View
style={{
paddingHorizontal: gutters,
paddingTop: top,
flex: 1,
paddingBottom: gutters * 2,
}}
>
}}>
<MusicLandHeader progress={9} onPressBack={goBack} />
<View style={{ flex: 1, marginTop: 11 }}>
@@ -402,22 +400,20 @@ const RecordPlayback = ({ route }) => {
{isPreparing && countdown >= 1 && !showProgress && (
<View
style={{
position: "absolute",
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: "center",
justifyContent: "center",
}}
>
alignItems: 'center',
justifyContent: 'center',
}}>
<Text
style={{
fontSize: 72,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueBold,
}}
>
}}>
{countdown}
</Text>
</View>
@@ -427,15 +423,14 @@ const RecordPlayback = ({ route }) => {
{!permissionsGranted && (
<View
style={{
position: "absolute",
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
padding: gutters,
}}
>
}}>
<GradientButton
title="Autoriser la caméra"
title='Autoriser la caméra'
onPress={async () => {
try {
if (!cameraPermission?.granted)
@@ -450,8 +445,8 @@ const RecordPlayback = ({ route }) => {
{/* Start button */}
{permissionsGranted && !isPreparing && !isRecording && (
<GradientButton
title="Lancer ma musique"
containerStyle={{ width: "80%", alignSelf: "center" }}
title='Lancer ma musique'
containerStyle={{ width: '80%', alignSelf: 'center' }}
disabled={!songUrl}
onPress={startCountdownThenRecord}
/>
@@ -461,23 +456,21 @@ const RecordPlayback = ({ route }) => {
{permissionsGranted && (isRecording || showProgress) && (
<View
style={{
position: "absolute",
position: 'absolute',
left: 0,
right: 0,
bottom: gutters,
alignItems: "center",
justifyContent: "center",
}}
>
alignItems: 'center',
justifyContent: 'center',
}}>
<View
style={{
width: 100,
height: 100,
borderRadius: 105,
alignItems: "center",
justifyContent: "center",
}}
>
alignItems: 'center',
justifyContent: 'center',
}}>
<ProgressRing
size={100}
strokeWidth={8}
@@ -489,7 +482,7 @@ const RecordPlayback = ({ route }) => {
/>
<View
style={{
position: "absolute",
position: 'absolute',
width: 50,
height: 50,
borderRadius: 55,
@@ -503,15 +496,14 @@ const RecordPlayback = ({ route }) => {
</CameraView>
{/* CreateLyricsHeader overlay outside of CameraView */}
<View
pointerEvents="box-none"
pointerEvents='box-none'
style={{
position: "absolute",
position: 'absolute',
left: 0,
right: 0,
bottom: bottom + 130,
paddingHorizontal: gutters,
}}
>
}}>
<CreateLyricsHeader>
{(isRecording || showProgress) && alignedWords?.length > 0 ? (
<KaraokeLyrics
@@ -524,8 +516,7 @@ const RecordPlayback = ({ route }) => {
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.
</Text>
@@ -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,
}}
>
}}>
<Circle
cx={size / 2}
cy={size / 2}
r={r}
stroke={Palette.white}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinecap='round'
strokeDasharray={`${c} ${c}`}
strokeDashoffset={offset}
fill="transparent"
fill='transparent'
/>
</Svg>
);
@@ -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 (
<View style={{ gap: 4 }}>
{/* {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}
</Text>
{next ? (
<Text
style={{
color: "#FFFFFF99",
color: '#FFFFFF99',
fontSize: 14,
textAlign: "center",
textAlign: 'center',
fontFamily: FONT_FAMILY.InterMedium,
}}
>
}}>
{next}
</Text>
) : null}
+50 -54
View File
@@ -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 (
<Page
backgroundImg={
action === "playback"
action === 'playback'
? background.playbackBG2
: background.productionBG2
}
headerType="NONE"
>
headerType='NONE'>
<MusicLandHeader onPressBack={goBack} progress={50} />
<View style={{ flex: 1, paddingTop: responsiveHeight(15) }}>
<CreateLyricsHeader
gradientProps={{
start: { x: 0, y: 0 },
end: { x: 0, y: 1 },
}}
>
}}>
<Image
source={img.placeholder2}
style={{
...size({ size: 158 }),
borderRadius: 13,
alignSelf: "center",
alignSelf: 'center',
}}
/>
<View style={{ gap: 12, paddingVertical: 12 }}>
<Text style={styles.title}>
Prêt à télécharger{" "}
{action === "playback" ? "ton Playback" : "ta chanson"} ?
Prêt à télécharger{' '}
{action === 'playback' ? 'ton Playback' : 'ta chanson'} ?
</Text>
<View style={{ gap: 10 }}>
<Pressable
style={styles.itemContainer}
onPress={
() => {
console.log("test");
console.log('test');
handleDownloadUri();
}
@@ -118,19 +116,18 @@ const DownloadSongs = ({ route }) => {
// action,
// uri,
// })
}
>
}>
<BlurView
style={styles.blurView}
intensity={Platform.OS !== "ios" ? 10 : 40}
tint="dark"
intensity={Platform.OS !== 'ios' ? 10 : 40}
tint='dark'
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<Text style={styles.label}>
Télécharger{" "}
{action === "playback" ? "mon Playback" : "ma chanson"}
Télécharger{' '}
{action === 'playback' ? 'mon Playback' : 'ma chanson'}
</Text>
<Text style={styles.description}>Pour moi uniquement</Text>
</BlurView>
@@ -141,26 +138,25 @@ const DownloadSongs = ({ route }) => {
navigate(Routes.StreamSong, {
action,
})
}
>
}>
<BlurView
style={styles.blurView}
intensity={Platform.OS !== "ios" ? 10 : 40}
tint="dark"
intensity={Platform.OS !== 'ios' ? 10 : 40}
tint='dark'
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<Text style={styles.label}>
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
</Text>
<Text style={styles.description}>
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'})
</Text>
</BlurView>
</Pressable>
@@ -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,