Skip to content
GitLab
Explorer
Connexion
Navigation principale
Rechercher ou aller à…
Projet
S
Server Rtmp
Gestion
Activité
Membres
Labels
Programmation
Tickets
0
Tableaux des tickets
Jalons
Wiki
Jira
Code
Requêtes de fusion
0
Dépôt
Branches
Validations
Étiquettes
Graphe du dépôt
Comparer les révisions
Extraits de code
Compilation
Pipelines
Jobs
Planifications de pipeline
Artéfacts
Déploiement
Releases
Registre de paquets
Opération
Environnements
Modules Terraform
Surveillance
Incidents
Service d'assistance
Analyse
Données d'analyse des chaînes de valeur
Contributor analytics
Données d'analyse CI/CD
Données d'analyse du dépôt
Expériences du modèle
Aide
Aide
Support
Documentation de GitLab
Comparer les forfaits GitLab
Forum de la communauté
Contribuer à GitLab
Donner votre avis
Raccourcis clavier
?
Extraits de code
Groupes
Projets
marketing confort
Server Rtmp
Validations
b62c4a1a
Valider
b62c4a1a
rédigé
il y a 5 mois
par
Ubuntu
Parcourir les fichiers
Options
Téléchargements
Correctifs
Plain Diff
updated recording logic
parent
8cce8ef3
Aucune requête de fusion associée trouvée
Modifications
3
Masquer les modifications d'espaces
En ligne
Côte à côte
Affichage de
3 fichiers modifiés
PositiveSSL_Wildcard_mydressin-server.com.pem
+119
-0
119 ajouts, 0 suppression
PositiveSSL_Wildcard_mydressin-server.com.pem
nginx.conf
+4
-4
4 ajouts, 4 suppressions
nginx.conf
register-live-api/recorder.js
+303
-113
303 ajouts, 113 suppressions
register-live-api/recorder.js
avec
426 ajouts
et
117 suppressions
PositiveSSL_Wildcard_mydressin-server.com.pem
0 → 100644
+
119
−
0
Voir le fichier @
b62c4a1a
-----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
This diff is collapsed.
Click to expand it.
nginx.conf
+
4
−
4
Voir le fichier @
b62c4a1a
...
...
@@ -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
;
}
}
This diff is collapsed.
Click to expand it.
register-live-api/recorder.js
+
303
−
113
Voir le fichier @
b62c4a1a
...
...
@@ -5,67 +5,106 @@ require('dotenv').config();
const
s3
=
new
AWS
.
S3
({
credentials
:
{
accessKeyId
:
"
AKIA3FLDZT775ARNAJHT
"
,
secretAccessKey
:
"
KE7/v+viEDQgevGH5CVoF5QXhDZ54vgp651RwW6s
"
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 lookup
s
const
MAX_RETRIES
=
15
;
const
RETRY_DELAY
=
1
*
60
*
1000
;
// 15 minutes en milliseconde
s
const
activeRecordings
=
new
Map
();
/**
*
Starts recording an RTMP stream
* @param {string} streamKey -
Unique identifier for the stream
* @returns {string} Return
s th
e streamKey
for refe
rence
*
Démarre l'enregistrement d'un flux RTMP
* @param {string} streamKey -
Identifiant unique du flux
* @returns {string} Ret
o
urn
e l
e 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}
output
File - Path to local file
* @param {string}
mp4
File - Path to local
MP4
file
* @param {string} newFileName - Target filename in S3
* @param {string} tsFile - Path to TS file for cleanup
*/
async
function
uploadAndCleanup
(
output
File
,
newFileName
)
{
async
function
uploadAndCleanup
(
mp4
File
,
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
};
This diff is collapsed.
Click to expand it.
Aperçu
0%
Veuillez réessayer
ou
joindre un nouveau fichier
.
Annuler
You are about to add
0
people
to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Enregistrer le commentaire
Annuler
Veuillez vous
inscrire
ou vous
se connecter
pour commenter