Skip to content
Extraits de code Groupes Projets
Valider b62c4a1a rédigé par Ubuntu's avatar Ubuntu
Parcourir les fichiers

updated recording logic

parent 8cce8ef3
Aucune requête de fusion associée trouvée
-----BEGIN CERTIFICATE REQUEST-----
MIICxDCCAawCAQAwfzELMAkGA1UEBhMCRlIxDzANBgNVBAgMBmZyYW5jZTEOMAwG
A1UEBwwFUGFyaXMxDjAMBgNVBAoMBUl6ZW14MR8wHQYDVQQDDBYqLm15ZHJlc3Np
bi1zZXJ2ZXIuY29tMR4wHAYJKoZIhvcNAQkBFg9hZG1pbkBpemVteC5jb20wggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDvHvr5l32rvUp/dvbz3fY1Pglz
CWN5q2oRNmGLg9d3TychS7DsG0ibPK40da8PeLCEL5IaNY17d73r2cMoOZTthq8v
1uZPk+4X8RPZn29CL0+n+d2X/9muKZlzJvtGHUIpoW6wA3t3zrzFzD0+sU2jsZhN
BGwLi0fkgtq9UVcEbbjAV0xsoQUTpYnXEIXawETxjep36fViQDNhanzGGOl7mXdI
FkLe691z2NuLNudgPh9N9XBVQMHcol74a4N1ZrGunicuDyznXF4nM5BZm+43G0mc
RJyizAjFemt8r+5O9sQjvx7/HuoxvcneRyLNBlNeQ8H003htFt8ozxAU2benAgMB
AAGgADANBgkqhkiG9w0BAQsFAAOCAQEAfV3hvbMvY5f2VY1RUBOmr58/xP4e/HyK
1DLNlp3p1JbIxwgfVww6ABRpEPrHwnObsmunqoRj4j5bADO1vhtZX/ajKJyfmxTp
EpQJUdWpI1/zrzC6XFOpLcs+cRtkTm5xL/R182sbwgXBG+DGxEfo1oS/mKcKY+yy
PMTXkUmcz72TKrPS4bqdPSdyugizC2Wse5u8VDEFGWEhxmK5zLqVrf9F63/PrMDB
CLuhEbMZW/SUeaWiRPgS/fKVg+TPrNwwi5T97Khxd6z3hyvCy9uHmR2lGjPwStF/
nDP71dS+A8ruyKV0pp9DyoZm23muFLkjw8rQRPMjq0iWEIA5vtRO9Q==
-----END CERTIFICATE REQUEST-----
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDvHvr5l32rvUp/
dvbz3fY1PglzCWN5q2oRNmGLg9d3TychS7DsG0ibPK40da8PeLCEL5IaNY17d73r
2cMoOZTthq8v1uZPk+4X8RPZn29CL0+n+d2X/9muKZlzJvtGHUIpoW6wA3t3zrzF
zD0+sU2jsZhNBGwLi0fkgtq9UVcEbbjAV0xsoQUTpYnXEIXawETxjep36fViQDNh
anzGGOl7mXdIFkLe691z2NuLNudgPh9N9XBVQMHcol74a4N1ZrGunicuDyznXF4n
M5BZm+43G0mcRJyizAjFemt8r+5O9sQjvx7/HuoxvcneRyLNBlNeQ8H003htFt8o
zxAU2benAgMBAAECggEBALNGp+XfJVJQPf1oxOe7Xy81zsHXpSQAVyqGegWumCmU
R4MYC5JJNltk2DrujIxt8PF59PD/e465DMQ55K5Q1Y5rrwaP+OZqw1Rh438CoNif
PfpzRCcEpECGYczKyhMcNpGdva3rPnNppXAqmzRYnBwBN21uxk61YvY5ASR1Smc/
JO01yEs8BWAuSk/a/MhnzheVXGx37T7J6XHTslj8HqGwX83CtE+17aSH3S7iXRVl
poHzUoaRgobwkgi5BRfrL8vf2QEDx8QCWKzRygkGXo22vAXuB6F7afUTicl0Q5a7
FWhpWh51y+4GLIf74AywNkxsuTwJpKlZ3p9sH6tyWtkCgYEA/XGyB3gvAvmCS62J
NY4vrb6QWAJjdEiBTmoCDu7WO0Pt869sw1jmq4tQFAwlzbK9FdzERxbLFZ8Qmz53
nNnnYoDYK7lJ9RD8PhdsZ4dvl9dLAFFSHm/G87qRF/+uqW2+IXfMpXZLP1MAFKgO
92QcYLthsRHm6cvT2fZwqZoWOtsCgYEA8YhOw0pGhXAp+MSF+KmdYQBXJmGEQ/Ng
nvmPGQgtDiXAliN1HB9VHdjQC4Vs7y9k5GkcrCDcVolhriunOrsHD5QPg3ZUT3dq
PAteJmI0jP80rohb3B1RxJR3gu03IuzjcEbTLLjxX6WnLnoUTYXZfofA975Ze1eP
yVyufB1AgiUCgYAS6xW68QGxWyaat41ybfapJXxo1WTPHUpprAgTTUi4i82LTyDz
RAOT8oY7uNxpiloK87vDArSOHJ2EuRj8oFdhRvTb7qzSmj359I2m/LkbwHpcv7U9
iGJ1dwu1muRSyVpT7TldcIMVawCqihDz7okfv1z/drKh6REbSAYI4vOd2QKBgQDY
XVbRMrRBzNyMcN/ihW7p+jgXtbuac2bWgpBhyCU0SzVeSZ0a+CZzeknESF3xVe93
fYGl4DoBe0f5kjlYLzqABg5voYydM0gDSdupXsfclrFGt/gyEkGxa54ztvRxYOvN
JGT/5xyypd5BkDKnz4OqCUofpHDcQPAZXeEgZcPn0QKBgQCSJWfvwJCkJtyFuC31
8xL8BYU06i8H8UJjjAxxULssycPo9GR5F+OjQCZ/4AhKUZZJz33yl+1tElXvQpc5
knyVcZq08169LmKMUn9SqGl9F1QB1ZXCjW9j+xAFrZVJdfVKYEENKivFiZbFZ/dR
u/2D6G+E9F+YnW0LJWQn1QwY7A==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIGTDCCBTSgAwIBAgIRAMSlcYMVlQY1xdrFWJMh9WUwDQYJKoZIhvcNAQELBQAw
gY8xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE
AxMuU2VjdGlnbyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD
QTAeFw0yNDA4MjUwMDAwMDBaFw0yNTA4MjUyMzU5NTlaMCExHzAdBgNVBAMMFiou
bXlkcmVzc2luLXNlcnZlci5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDvHvr5l32rvUp/dvbz3fY1PglzCWN5q2oRNmGLg9d3TychS7DsG0ibPK40
da8PeLCEL5IaNY17d73r2cMoOZTthq8v1uZPk+4X8RPZn29CL0+n+d2X/9muKZlz
JvtGHUIpoW6wA3t3zrzFzD0+sU2jsZhNBGwLi0fkgtq9UVcEbbjAV0xsoQUTpYnX
EIXawETxjep36fViQDNhanzGGOl7mXdIFkLe691z2NuLNudgPh9N9XBVQMHcol74
a4N1ZrGunicuDyznXF4nM5BZm+43G0mcRJyizAjFemt8r+5O9sQjvx7/Huoxvcne
RyLNBlNeQ8H003htFt8ozxAU2benAgMBAAGjggMOMIIDCjAfBgNVHSMEGDAWgBSN
jF7EVK2K4Xfpm/mbBeG4AY1h4TAdBgNVHQ4EFgQUJlNqViuEX273zyNlb3wUlegH
CXkwDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYB
BQUHAwEGCCsGAQUFBwMCMEkGA1UdIARCMEAwNAYLKwYBBAGyMQECAgcwJTAjBggr
BgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQIBMIGEBggr
BgEFBQcBAQR4MHYwTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jcnQuc2VjdGlnby5jb20v
U2VjdGlnb1JTQURvbWFpblZhbGlkYXRpb25TZWN1cmVTZXJ2ZXJDQS5jcnQwIwYI
KwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMDcGA1UdEQQwMC6CFiou
bXlkcmVzc2luLXNlcnZlci5jb22CFG15ZHJlc3Npbi1zZXJ2ZXIuY29tMIIBfgYK
KwYBBAHWeQIEAgSCAW4EggFqAWgAdgDd3Mo0ldfhFgXnlTL6x5/4PRxQ39sAOhQS
dgosrLvIKgAAAZGG6JynAAAEAwBHMEUCIAa7wqXGUb0LG3Uq4GNHWgXa41EViOS1
PY48tJJPyOlDAiEAwa/kM19AB3jlZpoyP6qfG8n1tDGIjfqcGRLwLVgiIoAAdgAN
4fIwK9MNwUBiEgnqVS78R3R8sdfpMO8OQh60fk6qNAAAAZGG6JyHAAAEAwBHMEUC
IQDgDCX1yoFAxs8Bm+RDU5atFgq42NbXiGq1tO5MljAanAIgVu2RzGQhehUO6sC5
Nj9gOwLuf/DV1lFiPJeZWl5ft9UAdgAS8U40vVNyTIQGGcOPP3oT+Oe1YoeInG0w
BYTr5YYmOgAAAZGG6JyJAAAEAwBHMEUCIQC7ZKrBaoZbXQ+7N2MGUkCXDJE1s+53
uOxNCoWfm+CHiwIgNmyPqTe20dBUrBQM4pAva7W2iwk0upclXe7YcDs8JfYwDQYJ
KoZIhvcNAQELBQADggEBABbM8N/EkkA1QWLZmtxZS3YDwAaOMAceRhLmRZZj9gfZ
NMycrv6y46WLnDxMQGy2jP8WSpX1FGMMar1XCQgBJA8109omI3NP5fa+X1WMdN/W
S5aq/hIiPKM4xh7SYEX8UVklKN65logzjTAA3WT5teWwQ1+MPGUC2CNCbQeY/EhP
GTMpwc+SEtM5SENsyHotx1DPMjrTRVaa9/zD7X30PC0nh+ANB8u44119bzvMptvU
UGetT5JyFXkBi9wIl7TwxBq7Vpk7GPY66OpE59unV90WvBD3doXfH4x1Hwj4XwWY
y4ajQF3FlmIZQc0e8CZdz55LrAAwZd9oI3G3SGMBjPE=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgx
MTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzAZBgNV
BAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UE
ChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb21haW4g
VmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/LEpgPLT+N
TQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHxYyQdqAJrkj
eocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPAXJpz9OTTG0E
oKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCSo1yfu4iYCBsk
Haswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDCg/r8uoydajotY
uK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbjCCAWowHwYDVR0j
BBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFI2MXsRUrYrhd+mb
+ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G
A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSAEFDASMAYGBFUdIAAw
CAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0
LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2Bggr
BgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNv
bS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDov
L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMr9hvQ5Iw0/H
ukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvVKqJjUAXaPgREHzSyrHxVYbH
7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5ROVm+yezu/Coa9zcV3HAO4OLGi
H19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg6F0AnwnJOKf0hPHzPE/uWLMUx
RP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg6s1SQ36yOoeNDT5++SR2RiOSLv
xvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGjmO+wea03KbNIaiGCpXZLoUmGv38
sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9p5v//cWoaasm56ekBYdbqbe4oyAL
l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq
6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qHSihrZe68Zk6s+go/lunrotEbaGmAhY
LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5
yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K
00u/I5sUKUErmgQfky3xxzlIPK1aEn8=
-----END CERTIFICATE-----
\ No newline at end of file
......@@ -49,7 +49,7 @@ http {
# Example: SSL-enabled server block for HLS
server {
listen 443 ssl;
server_name hls.mydressin-server.com;
server_name hls-ovh.mydressin-server.com;
ssl_certificate /usr/local/nginx/conf/ssl/PositiveSSL_Wildcard_mydressin-server.com.pem;
ssl_certificate_key /usr/local/nginx/conf/ssl/PositiveSSL_Wildcard_mydressin-server.com.pem;
......@@ -97,13 +97,13 @@ http {
server {
listen 80;
server_name hls.mydressin-server.com;
server_name hls-ovh.mydressin-server.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name status.mydressin-server.com;
server_name status-ovh.mydressin-server.com;
ssl_certificate /usr/local/nginx/conf/ssl/PositiveSSL_Wildcard_mydressin-server.com.pem;
ssl_certificate_key /usr/local/nginx/conf/ssl/PositiveSSL_Wildcard_mydressin-server.com.pem;
......@@ -119,7 +119,7 @@ http {
server {
listen 80;
server_name status.mydressin-server.com;
server_name status-ovh.mydressin-server.com;
return 301 https://$server_name$request_uri;
}
}
......@@ -5,67 +5,106 @@ require('dotenv').config();
const s3 = new AWS.S3({
credentials: {
accessKeyId: "AKIA3FLDZT775ARNAJHT",
secretAccessKey: "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"
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
},
region: process.env.AWS_REGION || "eu-north-1",
endpoint: process.env.AWS_ENDPOINT || "https://s3.eu-north-1.amazonaws.com"
region: process.env.AWS_REGION || "eu-west-3",
endpoint: process.env.AWS_ENDPOINT || "https://s3.eu-west-3.amazonaws.com"
});
// Track active recordings using Map for efficient lookups
const MAX_RETRIES = 15;
const RETRY_DELAY = 1 * 60 * 1000; // 15 minutes en millisecondes
const activeRecordings = new Map();
/**
* Starts recording an RTMP stream
* @param {string} streamKey - Unique identifier for the stream
* @returns {string} Returns the streamKey for reference
* Démarre l'enregistrement d'un flux RTMP
* @param {string} streamKey - Identifiant unique du flux
* @returns {string} Retourne le streamKey pour référence
*/
function startRecording(streamKey) {
const outputFile = `/tmp/${streamKey}.mp4`;
const ffmpegArgs = [
'-timeout', '5000000',
'-reconnect', '1',
'-reconnect_at_eof', '1',
'-reconnect_streamed', '1',
'-reconnect_delay_max', '2',
'-i', `http://127.0.0.1/hls/${streamKey}.m3u8`,
'-c', 'copy',
outputFile,
];
// Spawn FFmpeg process
const ffmpegProcess = spawn('ffmpeg', ffmpegArgs);
// Handle stderr output (FFmpeg logs to stderr)
ffmpegProcess.stderr.on('data', (data) => {
console.error(`[${streamKey}] FFmpeg Error: ${data}`);
});
const tsOutputFile = `/tmp/${streamKey}.ts`;
const mp4OutputFile = `/tmp/${streamKey}.mp4`;
// Handle process errors
ffmpegProcess.on('error', (err) => {
console.error(`[${streamKey}] Process Error: ${err}`);
activeRecordings.delete(streamKey);
});
function attemptRecording() {
const ffmpegArgs = [
'-timeout', '5000000',
'-reconnect', '1',
'-reconnect_at_eof', '1',
'-reconnect_streamed', '1',
'-reconnect_delay_max', '4', // 4 secondes max pour HTTP
'-i', `https://ant-media-ovh.mydressin-server.com:5443/live/streams/${streamKey}_adaptive.m3u8`,
'-c', 'copy',
'-copyts',
'-avoid_negative_ts', 'make_zero',
'-f', 'mpegts', // Utiliser le format TS qui supporte l'ajout (append)
tsOutputFile,
];
// Lance le processus FFmpeg
const ffmpegProcess = spawn('ffmpeg', ffmpegArgs);
// Handle process exit
ffmpegProcess.on('exit', (code, signal) => {
console.log(`[${streamKey}] FFmpeg exited with code ${code} (${signal})`);
activeRecordings.delete(streamKey);
});
// Gère la sortie d'erreur de FFmpeg
ffmpegProcess.stderr.on('data', (data) => {
console.info(`[${streamKey}] FFmpeg Data: ${data}`);
});
// Store recording information in Map
activeRecordings.set(streamKey, {
process: ffmpegProcess,
outputFile,
startTime: Date.now()
});
// Gère les erreurs du processus
ffmpegProcess.on('error', (err) => {
console.error(`[${streamKey}] Process Error: ${err}`);
handleReconnect(streamKey);
});
// Gère la sortie du processus
ffmpegProcess.on('exit', (code, signal) => {
console.log(`[${streamKey}] FFmpeg exited with code ${code} (${signal})`);
handleReconnect(streamKey);
});
let initialRetryCount = 0;
const recording = activeRecordings.get(streamKey);
if (recording) {
initialRetryCount = recording.retryCount;
}
// Enregistre les informations du processus
activeRecordings.set(streamKey, {
process: ffmpegProcess,
tsOutputFile,
mp4OutputFile,
retryCount: initialRetryCount,
startTime: Date.now(),
isStopped: false
});
return ffmpegProcess.pid;
console.log(`[${streamKey}] FFmpeg process started in TS format for continuous recording`);
}
function handleReconnect(streamKey) {
const recording = activeRecordings.get(streamKey);
if (!recording || recording.isStopped) {
console.log(`[${streamKey}] Enregistrement arrêté, aucune reconnexion nécessaire`);
return;
}
if (recording.retryCount < MAX_RETRIES) {
recording.retryCount++;
console.log(`[${streamKey}] Tentative de reconnexion ${recording.retryCount}/${MAX_RETRIES}`);
setTimeout(attemptRecording, RETRY_DELAY);
} else {
console.log(`[${streamKey}] Nombre maximal de tentatives de reconnexion atteint. Arrêt des tentatives.`);
activeRecordings.delete(streamKey);
}
}
// Démarre la première tentative d'enregistrement
setImmediate(attemptRecording);
return 0;
}
/**
* Stops recording and initiates upload process
* @param {string} streamKey - Identifier for the stream to stop
......@@ -73,71 +112,143 @@ function startRecording(streamKey) {
*/
async function stopRecording(streamKey) {
const recording = activeRecordings.get(streamKey);
if (!recording) {
throw new Error(`No active recording for ${streamKey}`);
}
const newFileName = `${streamKey}.mp4`;
const tsOutputFile = `/tmp/${streamKey}.ts`;
const mp4OutputFile = `/tmp/${streamKey}.mp4`;
const { process: ffmpegProcess, outputFile } = recording;
const newFileName = `${streamKey}.mp4`; // Unique filename with timestamp
if (!recording) {
console.error(`[${streamKey}] No active recording found`);
// Use setImmediate to defer the upload and cleanup
setImmediate(() => uploadAndCleanup(mp4OutputFile, newFileName, tsOutputFile));
return {
status: 'error',
message: `No active recording for ${streamKey}`
};
} else {
// Marquer l'enregistrement comme arrêté
recording.isStopped = true;
try {
// Graceful shutdown with SIGINT
ffmpegProcess.kill('SIGINT');
// Wait for graceful exit with timeout
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`[${streamKey}] FFmpeg shutdown timeout`));
}, 30000); // 30-second timeout
ffmpegProcess.once('exit', (code, signal) => {
clearTimeout(timeout);
if (code === 0) {
console.log(`[${streamKey}] FFmpeg exited gracefully`);
resolve();
} else {
reject(new Error(`[${streamKey}] FFmpeg exited with code ${code} (${signal})`));
}
});
});
} catch (err) {
console.error(`[${streamKey}] Error stopping recording:`, err.message);
try {
// Force kill if graceful shutdown failed
ffmpegProcess.kill('SIGKILL');
} catch (killErr) {
console.error(`[${streamKey}] Force kill failed:`, killErr.message);
const { process: ffmpegProcess } = recording;
ffmpegProcess.kill('SIGINT');
// Wait for graceful exit with timeout
await new Promise((resolve, reject) => {
ffmpegProcess.once('exit', (code, signal) => {
if (code === 0) {
console.log(`[${streamKey}] FFmpeg exited gracefully`);
resolve();
} else {
reject(new Error(`[${streamKey}] FFmpeg exited with code ${code} (${signal})`));
}
});
});
} catch (err) {
console.error(`[${streamKey}] Error stopping recording:`, err.message);
try {
// Force kill if graceful shutdown failed
ffmpegProcess.kill('SIGKILL');
} catch (killErr) {
console.error(`[${streamKey}] Force kill failed:`, killErr.message);
}
} finally {
// Always clean up from active recordings
activeRecordings.delete(streamKey);
}
} finally {
// Always clean up from active recordings
activeRecordings.delete(streamKey);
}
try {
await uploadAndCleanup(outputFile, newFileName);
return {
// Use setImmediate to defer the TS to MP4 conversion, upload and cleanup
setImmediate(async () => {
try {
console.log(`[${streamKey}] Converting TS to MP4 before upload`);
await convertTStoMP4(tsOutputFile, mp4OutputFile);
await uploadAndCleanup(mp4OutputFile, newFileName, tsOutputFile);
console.log(`[${streamKey}] Recording uploaded as ${newFileName}`);
} catch (err) {
console.error(`[${streamKey}] Conversion or upload failed:`, err.message);
}
});
return {
status: 'success',
message: `Recording uploaded as ${newFileName}`,
message: `Recording process initiated for ${newFileName}`,
file: newFileName
};
} catch (err) {
console.error(`[${streamKey}] Upload failed:`, err.message);
throw err;
}
}
/**
* Converts TS file to MP4 before upload
* @param {string} tsFile - Path to TS file
* @param {string} mp4File - Path to output MP4 file
* @returns {Promise} Conversion promise
*/
function convertTStoMP4(tsFile, mp4File) {
return new Promise((resolve, reject) => {
console.log(`Converting ${tsFile} to ${mp4File}`);
const ffmpegArgs = [
'-i', tsFile,
'-c', 'copy',
'-bsf:a', 'aac_adtstoasc',
mp4File
];
const ffmpegProcess = spawn('ffmpeg', ffmpegArgs);
ffmpegProcess.stderr.on('data', (data) => {
console.info(`Conversion Data: ${data}`);
});
ffmpegProcess.on('close', (code) => {
if (code === 0) {
console.log(`Conversion successful: ${tsFile} -> ${mp4File}`);
resolve();
} else {
reject(new Error(`FFmpeg conversion failed with code ${code}`));
}
});
ffmpegProcess.on('error', (err) => {
reject(new Error(`FFmpeg conversion error: ${err.message}`));
});
});
}
/**
* Handles file upload and local cleanup
* @param {string} outputFile - Path to local file
* @param {string} mp4File - Path to local MP4 file
* @param {string} newFileName - Target filename in S3
* @param {string} tsFile - Path to TS file for cleanup
*/
async function uploadAndCleanup(outputFile, newFileName) {
async function uploadAndCleanup(mp4File, newFileName, tsFile) {
try {
await uploadToS3(outputFile, newFileName);
fs.unlinkSync(outputFile); // Delete local file after upload
console.log(`[${newFileName}] Local file cleaned up`);
await uploadToS3(mp4File, newFileName);
// Clean up both files
if (fs.existsSync(mp4File)) {
fs.unlinkSync(mp4File);
}
if (tsFile && fs.existsSync(tsFile)) {
fs.unlinkSync(tsFile);
}
console.log(`[${newFileName}] Local files cleaned up`);
} catch (err) {
console.error(`[${newFileName}] Cleanup error:`, err.message);
// Attempt to clean up anyway
try {
if (fs.existsSync(mp4File)) {
fs.unlinkSync(mp4File);
}
if (tsFile && fs.existsSync(tsFile)) {
fs.unlinkSync(tsFile);
}
} catch (cleanupErr) {
console.error(`[${newFileName}] Additional cleanup error:`, cleanupErr.message);
}
throw err;
}
}
......@@ -149,27 +260,106 @@ async function uploadAndCleanup(outputFile, newFileName) {
* @returns {Promise} S3 upload promise
*/
async function uploadToS3(filePath, newFileName) {
return new Promise((resolve, reject) => {
// Use file stream for memory efficiency
const fileStream = fs.createReadStream(filePath);
try {
// Vérifier la taille du fichier
const stats = fs.statSync(filePath);
const fileSizeInBytes = stats.size;
const fileSizeInMB = fileSizeInBytes / (1024 * 1024);
console.log(`[${newFileName}] Taille du fichier: ${fileSizeInMB.toFixed(2)} MB`);
// Si le fichier est volumineux, utiliser le téléchargement multipart
if (fileSizeInMB > 100) { // Par exemple, pour les fichiers > 100 MB
return uploadLargeFile(filePath, newFileName);
}
const params = {
Bucket: 'bucket-rtmp',
Key: newFileName,
Body: fileStream,
// Pour les petits fichiers, continuer avec la méthode simple
return new Promise((resolve, reject) => {
const fileStream = fs.createReadStream(filePath);
const params = {
Bucket: process.env.AWS_BUCKET_NAME || 'mydressin-live-records',
Key: newFileName,
Body: fileStream,
ContentType: 'video/mp4',
};
s3.putObject(params)
.then(() => {
console.log(`[${newFileName}] Téléchargement terminé`);
resolve();
})
.catch((err) => {
reject(new Error(`S3 Upload Error: ${err.message}`));
})
.finally(() => {
fileStream.close();
});
});
} catch (err) {
throw new Error(`Erreur de préparation du téléchargement: ${err.message}`);
}
}
/**
* Gère le téléchargement des fichiers volumineux via multipart upload
* @param {string} filePath - Chemin du fichier local
* @param {string} fileName - Nom du fichier cible dans S3
* @returns {Promise} Promise du téléchargement
*/
async function uploadLargeFile(filePath, fileName) {
try {
console.log(`[${fileName}] Démarrage du téléchargement multipart`);
// Créer un téléchargement multipart
const createMultipartUploadCommand = {
Bucket: 'mydressin-live-records',
Key: fileName,
ContentType: 'video/mp4',
};
s3.putObject(params, (err) => {
fileStream.close();
if (err) {
reject(new Error(`S3 Upload Error: ${err.message}`));
} else {
console.log(`[${newFileName}] Upload completed`);
resolve();
}
});
});
const { UploadId } = await s3.createMultipartUpload(createMultipartUploadCommand);
// Lire le fichier et le télécharger par parties
const fileSize = fs.statSync(filePath).size;
const partSize = 5 * 1024 * 1024; // 5MB par partie (minimum)
const numParts = Math.ceil(fileSize / partSize);
const uploadedParts = [];
for (let i = 1; i <= numParts; i++) {
const start = (i - 1) * partSize;
const end = Math.min(i * partSize, fileSize);
const fileStream = fs.createReadStream(filePath, { start, end: end - 1 });
const uploadPartCommand = {
Bucket: 'mydressin-live-records',
Key: fileName,
UploadId: UploadId,
PartNumber: i,
Body: fileStream,
};
console.log(`[${fileName}] Téléchargement partie ${i}/${numParts}`);
const { ETag } = await s3.uploadPart(uploadPartCommand);
uploadedParts.push({ PartNumber: i, ETag });
}
// Compléter le téléchargement multipart
const completeMultipartUploadCommand = {
Bucket: 'mydressin-live-records',
Key: fileName,
UploadId: UploadId,
MultipartUpload: { Parts: uploadedParts.sort((a, b) => a.PartNumber - b.PartNumber) },
};
await s3.completeMultipartUpload(completeMultipartUploadCommand);
console.log(`[${fileName}] Téléchargement multipart terminé avec succès`);
return true;
} catch (error) {
console.error(`[${fileName}] Erreur de téléchargement multipart: ${error.message}`);
throw error;
}
}
module.exports = { startRecording, stopRecording };
0% ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter