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.lyrics = require("./src/lyrics");
exports.cover = require("./src/cover"); exports.cover = require("./src/cover");
exports.projects = require("./src/project"); exports.projects = require("./src/project");
exports.hls = require("./src/hls");
+161 -5
View File
@@ -8,8 +8,10 @@
"dependencies": { "dependencies": {
"@genkit-ai/googleai": "^1.18.0", "@genkit-ai/googleai": "^1.18.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"ffmpeg-static": "^5.2.0",
"firebase-admin": "^12.7.0", "firebase-admin": "^12.7.0",
"firebase-functions": "^6.4.0", "firebase-functions": "^6.4.0",
"fluent-ffmpeg": "^2.1.2",
"genkit": "^1.18.0" "genkit": "^1.18.0"
}, },
"devDependencies": { "devDependencies": {
@@ -42,6 +44,21 @@
"kuler": "^2.0.0" "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": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.8.0", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz",
@@ -2819,6 +2836,12 @@
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause" "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": { "node_modules/bufrw": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/bufrw/-/bufrw-1.4.0.tgz", "resolved": "https://registry.npmjs.org/bufrw/-/bufrw-1.4.0.tgz",
@@ -2881,6 +2904,12 @@
"node": ">=6" "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": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3011,6 +3040,21 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -3250,6 +3294,15 @@
"once": "^1.4.0" "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": { "node_modules/error": {
"version": "7.0.2", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz",
@@ -3721,6 +3774,47 @@
"node": "^12.20 || >= 14.13" "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": { "node_modules/file-entry-cache": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -3852,6 +3946,37 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/fn.name": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
@@ -4448,6 +4573,21 @@
"node": ">= 6.0.0" "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": { "node_modules/https-proxy-agent": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@@ -4639,7 +4779,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/jaeger-client": { "node_modules/jaeger-client": {
@@ -5297,6 +5436,11 @@
"node": ">=6" "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": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -5449,6 +5593,15 @@
"node": ">= 0.6.0" "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": { "node_modules/proto3-json-serializer": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", "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", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
"string_decoder": "^1.1.1", "string_decoder": "^1.1.1",
@@ -6042,7 +6194,6 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
} }
@@ -6324,6 +6475,12 @@
"node": ">= 0.6" "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": { "node_modules/uglify-js": {
"version": "3.19.3", "version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
@@ -6379,8 +6536,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
+3 -1
View File
@@ -18,7 +18,9 @@
"axios": "^1.11.0", "axios": "^1.11.0",
"firebase-admin": "^12.7.0", "firebase-admin": "^12.7.0",
"firebase-functions": "^6.4.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": { "devDependencies": {
"eslint": "^8.15.0", "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 { useFocusEffect } from '@react-navigation/native';
import { useAudioPlayer } from "expo-audio"; import { useAudioPlayer } from 'expo-audio';
import { CameraView, useCameraPermissions } from "expo-camera"; import { CameraView, useCameraPermissions } from 'expo-camera';
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from 'react';
import { Alert, Text, View } from "react-native"; import { Alert, Text, View } from 'react-native';
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Svg, { Circle } from "react-native-svg"; import Svg, { Circle } from 'react-native-svg';
import GradientButton from "../../components/GradientButton"; import GradientButton from '../../components/GradientButton';
import KaraokeLyrics from "../../components/KaraokeLyrics"; import KaraokeLyrics from '../../components/KaraokeLyrics';
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from '../../components/MusicLandHeader';
import { increment, projectsRef } from "../../config/firebase"; import { increment, projectsRef } from '../../config/firebase';
import { Routes } from "../../navigation"; import { Routes } from '../../navigation';
import { goBack, navigate } from "../../navigation/NavigationService"; import { goBack, navigate } from '../../navigation/NavigationService';
import { gutters, Palette } from "../../styles"; import { gutters, Palette } from '../../styles';
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from '../../styles/Fonts';
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader';
const TIME_BEFORE_INCREMENT_MS = 20000; // 20s const TIME_BEFORE_INCREMENT_MS = 20000; // 20s
@@ -65,7 +65,7 @@ const RecordPlayback = ({ route }) => {
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : []; const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
return arr.map((w) => ({ return arr.map((w) => ({
word: String(w?.word ?? ""), word: String(w?.word ?? ''),
startS: Number(w?.startS ?? 0), startS: Number(w?.startS ?? 0),
endS: Number(w?.endS ?? 0), endS: Number(w?.endS ?? 0),
})); }));
@@ -77,16 +77,16 @@ const RecordPlayback = ({ route }) => {
let buf = []; let buf = [];
let start = null; let start = null;
const clean = (txt) => const clean = (txt) =>
String(txt || "") String(txt || '')
.replace(/\s+/g, " ") .replace(/\s+/g, ' ')
.trim(); .trim();
const isSentenceEnd = (txt) => /[.!?]$/.test((txt || "").trim()); const isSentenceEnd = (txt) => /[.!?]$/.test((txt || '').trim());
const isSectionTag = (txt) => const isSectionTag = (txt) =>
/^\s*\[[^\]]+\]\s*$/i.test((txt || "").trim()); /^\s*\[[^\]]+\]\s*$/i.test((txt || '').trim());
for (let i = 0; i < alignedWords.length; i++) { for (let i = 0; i < alignedWords.length; i++) {
const w = alignedWords[i]; const w = alignedWords[i];
const original = String(w.word || ""); const original = String(w.word || '');
const textNoNewline = original.replace(/\n/g, " "); const textNoNewline = original.replace(/\n/g, ' ');
const gapToNext = const gapToNext =
i < alignedWords.length - 1 i < alignedWords.length - 1
? Math.max(0, alignedWords[i + 1].startS - w.endS) ? Math.max(0, alignedWords[i + 1].startS - w.endS)
@@ -113,7 +113,7 @@ const RecordPlayback = ({ route }) => {
const isLast = i === alignedWords.length - 1; const isLast = i === alignedWords.length - 1;
if (eolByNewline || eolByPause || eolByPunct || isLast) { if (eolByNewline || eolByPause || eolByPunct || isLast) {
const joined = clean(buf.join(" ")); const joined = clean(buf.join(' '));
if (joined) if (joined)
out.push({ text: joined, startS: start ?? w.startS, endS: w.endS }); out.push({ text: joined, startS: start ?? w.startS, endS: w.endS });
buf = []; buf = [];
@@ -343,7 +343,7 @@ const RecordPlayback = ({ route }) => {
navigate(Routes.RecordedPlayback, { videoUri: video.uri, project }); navigate(Routes.RecordedPlayback, { videoUri: video.uri, project });
else navigate(Routes.RecordedPlayback, { project }); else navigate(Routes.RecordedPlayback, { project });
} catch (e) { } catch (e) {
console.log("RecordPlayback error:", e); console.log('RecordPlayback error:', e);
setIsRecording(false); setIsRecording(false);
setIsPreparing(false); setIsPreparing(false);
setShowProgress(false); setShowProgress(false);
@@ -356,14 +356,14 @@ const RecordPlayback = ({ route }) => {
checkSongEndRef.current = null; checkSongEndRef.current = null;
} }
// Inform the user when using a simulator where recording isn't supported // 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)) { if (/not supported on the simulator/i.test(msg)) {
Alert.alert( Alert.alert(
"Indisponible sur simulateur", 'Indisponible sur simulateur',
"Lenregistrement vidéo nest pas disponible sur le simulateur. Merci dutiliser un appareil réel.", 'Lenregistrement vidéo nest pas disponible sur le simulateur. Merci dutiliser un appareil réel.',
[ [
{ {
text: "OK", text: 'OK',
onPress: () => { onPress: () => {
try { try {
goBack(); goBack();
@@ -384,17 +384,15 @@ const RecordPlayback = ({ route }) => {
<CameraView <CameraView
ref={cameraRef} ref={cameraRef}
style={{ flex: 1 }} style={{ flex: 1 }}
facing="front" facing='front'
mode="video" mode='video'>
>
<View <View
style={{ style={{
paddingHorizontal: gutters, paddingHorizontal: gutters,
paddingTop: top, paddingTop: top,
flex: 1, flex: 1,
paddingBottom: gutters * 2, paddingBottom: gutters * 2,
}} }}>
>
<MusicLandHeader progress={9} onPressBack={goBack} /> <MusicLandHeader progress={9} onPressBack={goBack} />
<View style={{ flex: 1, marginTop: 11 }}> <View style={{ flex: 1, marginTop: 11 }}>
@@ -402,22 +400,20 @@ const RecordPlayback = ({ route }) => {
{isPreparing && countdown >= 1 && !showProgress && ( {isPreparing && countdown >= 1 && !showProgress && (
<View <View
style={{ style={{
position: "absolute", position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
}} }}>
>
<Text <Text
style={{ style={{
fontSize: 72, fontSize: 72,
color: Palette.white, color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueBold, fontFamily: FONT_FAMILY.HelveticaNeueBold,
}} }}>
>
{countdown} {countdown}
</Text> </Text>
</View> </View>
@@ -427,15 +423,14 @@ const RecordPlayback = ({ route }) => {
{!permissionsGranted && ( {!permissionsGranted && (
<View <View
style={{ style={{
position: "absolute", position: 'absolute',
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
padding: gutters, padding: gutters,
}} }}>
>
<GradientButton <GradientButton
title="Autoriser la caméra" title='Autoriser la caméra'
onPress={async () => { onPress={async () => {
try { try {
if (!cameraPermission?.granted) if (!cameraPermission?.granted)
@@ -450,8 +445,8 @@ const RecordPlayback = ({ route }) => {
{/* Start button */} {/* Start button */}
{permissionsGranted && !isPreparing && !isRecording && ( {permissionsGranted && !isPreparing && !isRecording && (
<GradientButton <GradientButton
title="Lancer ma musique" title='Lancer ma musique'
containerStyle={{ width: "80%", alignSelf: "center" }} containerStyle={{ width: '80%', alignSelf: 'center' }}
disabled={!songUrl} disabled={!songUrl}
onPress={startCountdownThenRecord} onPress={startCountdownThenRecord}
/> />
@@ -461,23 +456,21 @@ const RecordPlayback = ({ route }) => {
{permissionsGranted && (isRecording || showProgress) && ( {permissionsGranted && (isRecording || showProgress) && (
<View <View
style={{ style={{
position: "absolute", position: 'absolute',
left: 0, left: 0,
right: 0, right: 0,
bottom: gutters, bottom: gutters,
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
}} }}>
>
<View <View
style={{ style={{
width: 100, width: 100,
height: 100, height: 100,
borderRadius: 105, borderRadius: 105,
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
}} }}>
>
<ProgressRing <ProgressRing
size={100} size={100}
strokeWidth={8} strokeWidth={8}
@@ -489,7 +482,7 @@ const RecordPlayback = ({ route }) => {
/> />
<View <View
style={{ style={{
position: "absolute", position: 'absolute',
width: 50, width: 50,
height: 50, height: 50,
borderRadius: 55, borderRadius: 55,
@@ -503,15 +496,14 @@ const RecordPlayback = ({ route }) => {
</CameraView> </CameraView>
{/* CreateLyricsHeader overlay outside of CameraView */} {/* CreateLyricsHeader overlay outside of CameraView */}
<View <View
pointerEvents="box-none" pointerEvents='box-none'
style={{ style={{
position: "absolute", position: 'absolute',
left: 0, left: 0,
right: 0, right: 0,
bottom: bottom + 130, bottom: bottom + 130,
paddingHorizontal: gutters, paddingHorizontal: gutters,
}} }}>
>
<CreateLyricsHeader> <CreateLyricsHeader>
{(isRecording || showProgress) && alignedWords?.length > 0 ? ( {(isRecording || showProgress) && alignedWords?.length > 0 ? (
<KaraokeLyrics <KaraokeLyrics
@@ -524,8 +516,7 @@ const RecordPlayback = ({ route }) => {
fontSize: 16, fontSize: 16,
color: Palette.white, color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium, fontFamily: FONT_FAMILY.InterMedium,
}} }}>
>
L'enregistrement de ta vidéo commencera lorsque tu lanceras ta L'enregistrement de ta vidéo commencera lorsque tu lanceras ta
musique, et s'arrêtera à la fin du morceau. musique, et s'arrêtera à la fin du morceau.
</Text> </Text>
@@ -547,21 +538,20 @@ const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => {
width={size} width={size}
height={size} height={size}
style={{ style={{
transform: [{ rotate: "-90deg" }], transform: [{ rotate: '-90deg' }],
backgroundColor: "#ffffff3d", backgroundColor: '#ffffff3d',
borderRadius: 55, borderRadius: 55,
}} }}>
>
<Circle <Circle
cx={size / 2} cx={size / 2}
cy={size / 2} cy={size / 2}
r={r} r={r}
stroke={Palette.white} stroke={Palette.white}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
strokeLinecap="round" strokeLinecap='round'
strokeDasharray={`${c} ${c}`} strokeDasharray={`${c} ${c}`}
strokeDashoffset={offset} strokeDashoffset={offset}
fill="transparent" fill='transparent'
/> />
</Svg> </Svg>
); );
@@ -571,10 +561,10 @@ export default RecordPlayback;
// Render previous/current/next line to reduce jumpiness // Render previous/current/next line to reduce jumpiness
const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => { const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => {
const prev = currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : ""; const prev = currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : '';
const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : ""; const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : '';
const next = const next =
currentLineIdx + 1 < lines.length ? lines[currentLineIdx + 1]?.text : ""; currentLineIdx + 1 < lines.length ? lines[currentLineIdx + 1]?.text : '';
return ( return (
<View style={{ gap: 4 }}> <View style={{ gap: 4 }}>
{/* {prev ? ( {/* {prev ? (
@@ -586,21 +576,19 @@ const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => {
style={{ style={{
color: Palette.white, color: Palette.white,
fontSize: 18, fontSize: 18,
textAlign: "center", textAlign: 'center',
fontFamily: FONT_FAMILY.InterSemiBold, fontFamily: FONT_FAMILY.InterSemiBold,
}} }}>
>
{curr} {curr}
</Text> </Text>
{next ? ( {next ? (
<Text <Text
style={{ style={{
color: "#FFFFFF99", color: '#FFFFFF99',
fontSize: 14, fontSize: 14,
textAlign: "center", textAlign: 'center',
fontFamily: FONT_FAMILY.InterMedium, fontFamily: FONT_FAMILY.InterMedium,
}} }}>
>
{next} {next}
</Text> </Text>
) : null} ) : null}
+50 -54
View File
@@ -1,5 +1,5 @@
import { BlurView } from "expo-blur"; import { BlurView } from 'expo-blur';
import React from "react"; import React from 'react';
import { import {
Image, Image,
Platform, Platform,
@@ -7,38 +7,38 @@ import {
StyleSheet, StyleSheet,
Text, Text,
View, View,
} from "react-native"; } from 'react-native';
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js';
import { responsiveHeight } from "react-native-responsive-dimensions"; import { responsiveHeight } from 'react-native-responsive-dimensions';
import { background, img } from "../../assets"; import { background, img } from '../../assets';
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from '../../components/MusicLandHeader';
import { projectsRef, serverTimestamp } from "../../config/firebase"; import { projectsRef, serverTimestamp } from '../../config/firebase';
import { uploadFileToFirebase } from "../../helpers/uploadToFirebase"; import { uploadFileToFirebase } from '../../helpers/uploadToFirebase';
import Page from "../../layouts/Page"; import Page from '../../layouts/Page';
import { Routes } from "../../navigation"; import { Routes } from '../../navigation';
import { goBack, navigate } from "../../navigation/NavigationService"; import { goBack, navigate } from '../../navigation/NavigationService';
import { Palette } from "../../styles"; import { Palette } from '../../styles';
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from '../../styles/Fonts';
import { size } from "../../styles/Style"; import { size } from '../../styles/Style';
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader';
const DownloadSongs = ({ route }) => { const DownloadSongs = ({ route }) => {
// const params = useRoute().params; // const params = useRoute().params;
const { action, uri, project } = route.params || {}; const { action, uri, project } = route.params || {};
console.log("project id", project?.id); console.log('project id', project?.id);
const { setIsLoading, setTooltip } = useMinuit(); const { setIsLoading, setTooltip } = useMinuit();
const handleDownloadUri = async () => { const handleDownloadUri = async () => {
if (action === "playback" && project?.id) { if (action === 'playback' && project?.id) {
// Publication du playback // Publication du playback
console.log("project id : ", project.id); console.log('project id : ', project.id);
try { try {
setIsLoading(true); setIsLoading(true);
const { resultURI = null } = await uploadFileToFirebase({ const { resultURI = null } = await uploadFileToFirebase({
uri: uri, uri: uri,
path: `musics/${project.id}/playback.mp4`, path: `musics/${project.id}/source.mp4`,
shouldCompress: true, shouldCompress: true,
fileType: "VIDEO", fileType: 'VIDEO',
}); });
if (!resultURI) throw new Error("Téléversement de l'image impossible"); if (!resultURI) throw new Error("Téléversement de l'image impossible");
@@ -52,20 +52,20 @@ const DownloadSongs = ({ route }) => {
{ merge: true } { merge: true }
); );
setTooltip({ setTooltip({
type: "success", type: 'success',
text: "Playback publié avec succès", text: 'Vidéo uploadée, conversion HLS en cours…',
}); });
} else { } else {
setTooltip({ setTooltip({
type: "error", type: 'error',
text: "Erreur lors de la publication du playback", text: 'Erreur lors de la publication du playback',
}); });
} }
} catch (error) { } catch (error) {
console.log("error upload playback", error); console.log('error upload playback', error);
setTooltip({ setTooltip({
type: "error", type: 'error',
text: "Erreur lors de la publication du playback", text: 'Erreur lors de la publication du playback',
}); });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -78,39 +78,37 @@ const DownloadSongs = ({ route }) => {
return ( return (
<Page <Page
backgroundImg={ backgroundImg={
action === "playback" action === 'playback'
? background.playbackBG2 ? background.playbackBG2
: background.productionBG2 : background.productionBG2
} }
headerType="NONE" headerType='NONE'>
>
<MusicLandHeader onPressBack={goBack} progress={50} /> <MusicLandHeader onPressBack={goBack} progress={50} />
<View style={{ flex: 1, paddingTop: responsiveHeight(15) }}> <View style={{ flex: 1, paddingTop: responsiveHeight(15) }}>
<CreateLyricsHeader <CreateLyricsHeader
gradientProps={{ gradientProps={{
start: { x: 0, y: 0 }, start: { x: 0, y: 0 },
end: { x: 0, y: 1 }, end: { x: 0, y: 1 },
}} }}>
>
<Image <Image
source={img.placeholder2} source={img.placeholder2}
style={{ style={{
...size({ size: 158 }), ...size({ size: 158 }),
borderRadius: 13, borderRadius: 13,
alignSelf: "center", alignSelf: 'center',
}} }}
/> />
<View style={{ gap: 12, paddingVertical: 12 }}> <View style={{ gap: 12, paddingVertical: 12 }}>
<Text style={styles.title}> <Text style={styles.title}>
Prêt à télécharger{" "} Prêt à télécharger{' '}
{action === "playback" ? "ton Playback" : "ta chanson"} ? {action === 'playback' ? 'ton Playback' : 'ta chanson'} ?
</Text> </Text>
<View style={{ gap: 10 }}> <View style={{ gap: 10 }}>
<Pressable <Pressable
style={styles.itemContainer} style={styles.itemContainer}
onPress={ onPress={
() => { () => {
console.log("test"); console.log('test');
handleDownloadUri(); handleDownloadUri();
} }
@@ -118,19 +116,18 @@ const DownloadSongs = ({ route }) => {
// action, // action,
// uri, // uri,
// }) // })
} }>
>
<BlurView <BlurView
style={styles.blurView} style={styles.blurView}
intensity={Platform.OS !== "ios" ? 10 : 40} intensity={Platform.OS !== 'ios' ? 10 : 40}
tint="dark" tint='dark'
// experimentalBlurMethod={ // experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none" // Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// } // }
> >
<Text style={styles.label}> <Text style={styles.label}>
Télécharger{" "} Télécharger{' '}
{action === "playback" ? "mon Playback" : "ma chanson"} {action === 'playback' ? 'mon Playback' : 'ma chanson'}
</Text> </Text>
<Text style={styles.description}>Pour moi uniquement</Text> <Text style={styles.description}>Pour moi uniquement</Text>
</BlurView> </BlurView>
@@ -141,26 +138,25 @@ const DownloadSongs = ({ route }) => {
navigate(Routes.StreamSong, { navigate(Routes.StreamSong, {
action, action,
}) })
} }>
>
<BlurView <BlurView
style={styles.blurView} style={styles.blurView}
intensity={Platform.OS !== "ios" ? 10 : 40} intensity={Platform.OS !== 'ios' ? 10 : 40}
tint="dark" tint='dark'
// experimentalBlurMethod={ // experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none" // Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// } // }
> >
<Text style={styles.label}> <Text style={styles.label}>
Diffuser{" "} Diffuser{' '}
{action === "playback" ? "mon Playback" : "ma chanson"} sur {action === 'playback' ? 'mon Playback' : 'ma chanson'} sur
la plateforme de MusicLand (+ réseaux sociaux) et participer la plateforme de MusicLand (+ réseaux sociaux) et participer
au concours au concours
</Text> </Text>
<Text style={styles.description}> <Text style={styles.description}>
Top 3: 1er 15% du CA ML - 2ème 10% du CA ML - 3ème 5% du CA Top 3: 1er 15% du CA ML - 2ème 10% du CA ML - 3ème 5% du CA
ML (catégorie{" "} ML (catégorie{' '}
{action === "playback" ? "Playback" : "chanson"}) {action === 'playback' ? 'Playback' : 'chanson'})
</Text> </Text>
</BlurView> </BlurView>
</Pressable> </Pressable>
@@ -197,9 +193,9 @@ const styles = StyleSheet.create({
}, },
itemContainer: { itemContainer: {
borderRadius: 12, borderRadius: 12,
overflow: "hidden", overflow: 'hidden',
backgroundColor: "#FFFFFF03", backgroundColor: '#FFFFFF03',
shadowColor: "#0000001A", shadowColor: '#0000001A',
shadowOffset: { shadowOffset: {
width: 0, width: 0,
height: 2, height: 2,