Enregistrement des appels Asterisk + Interface Web
- Objectifs pédagogiques
- Architecture du système
- Prérequis et installation des dépendances
- Étape 1 — Création de la base de données MySQL
- Étape 2 — Scripts Python (conversion + stockage)
- Étape 3 — Configuration Asterisk (extensions.conf)
- Étape 4 — File d'attente et traitement automatique (cron)
- Étape 5 — Interface Web PHP
- Étape 6 — Tests et validation
- Résultat final
À la fin de ce TP, vous serez capable de :
- Configurer l'enregistrement des appels dans Asterisk avec MixMonitor
- Stocker les métadonnées des appels dans une base MySQL
- Convertir automatiquement les fichiers WAV en MP3
- Développer une interface Web PHP sécurisée
- Lire les fichiers audio directement depuis le navigateur
- Gérer un historique d'appels avec filtres et pagination
- Sécuriser l'accès aux enregistrements (auth HTTP Basic)
Le système s'articule autour de 5 composants qui travaillent ensemble :
/image%2F7223427%2F20260424%2Fob_185e7c_chatgpt-image-24-avr-2026-09-19-08.png)
Fichiers du projet
| Fichier | Rôle |
|---|---|
| /etc/asterisk/extensions.conf | Dialplan Asterisk — MixMonitor + extension h |
| /var/lib/asterisk/recording/db_config.py | Configuration MySQL Python |
| /var/lib/asterisk/recording/post_record.py | Conversion WAV→MP3 et insertion MySQL |
| /var/lib/asterisk/recording/process_queue.sh | Processeur de la file d'attente (cron) |
| /var/www/html/recordings/config.php | Configuration MySQL PHP + auth |
| /var/www/html/recordings/index.php | Interface Web principale |
| /var/www/html/recordings/audio.php | Serveur audio sécurisé (streaming MP3) |
| /tmp/rec_queue.txt | File d'attente des enregistrements à traiter |
Avant de commencer, installez les paquets nécessaires :
Terminal — root@aquila
# Dépendances système
apt-get update
apt-get install -y lame inotify-tools php-mysql
# Module Python MySQL
pip3 install mysql-connector-python --break-system-packages
# Vérifications
lame --version # Doit afficher : LAME 64bits version 3.100
php -v # Doit afficher : PHP 8.x
python3 -c "import mysql.connector; print('OK')" # Doit afficher : OK
# Créer les dossiers nécessaires
mkdir -p /var/spool/asterisk/monitor
mkdir -p /var/lib/asterisk/recording
chown -R asterisk:asterisk /var/spool/asterisk/monitor
chmod 775 /var/spool/asterisk/monitor
# Créer la file avec les bonnes permissions
touch /tmp/rec_queue.txt
chmod 777 /tmp/rec_queue.txt
chown asterisk:asterisk /tmp/rec_queue.txt
# Ajouter www-data au groupe asterisk (pour lire les MP3)
usermod -aG asterisk www-data
systemctl restart apache2
Création de la base de données MySQL
Créez le fichier SQL puis exécutez-le :
/image%2F7223427%2F20260424%2Fob_fb8153_capture-bd.png)
# mysql -u root -p
sql
-- Créer la base de données
CREATE DATABASE IF NOT EXISTS asterisk_recordings
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE asterisk_recordings;
-- Table principale des enregistrements
CREATE TABLE IF NOT EXISTS recordings (
id INT AUTO_INCREMENT PRIMARY KEY,
uniqueid VARCHAR(64) NOT NULL DEFAULT '',
calldate DATETIME NOT NULL,
caller VARCHAR(50) NOT NULL DEFAULT '',
callee VARCHAR(50) NOT NULL DEFAULT '',
context VARCHAR(50) NOT NULL DEFAULT '',
duration INT NOT NULL DEFAULT 0,
wav_file VARCHAR(255) DEFAULT NULL,
mp3_file VARCHAR(255) DEFAULT NULL,
status ENUM('pending','done','error') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_calldate (calldate),
INDEX idx_caller (caller),
INDEX idx_callee (callee)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Utilisateur dédié avec droits limités
CREATE USER IF NOT EXISTS 'asterisk_rec'@'localhost'
IDENTIFIED BY 'Esmt@2025!';
GRANT SELECT, INSERT, UPDATE ON
asterisk_recordings.recordings TO 'asterisk_rec'@'localhost';
FLUSH PRIVILEGES;
mysql -u asterisk_rec -p'Esmt@2025!' asterisk_recordings \
-e "SHOW TABLES;"
# Doit afficher : recordings
L'utilisateur asterisk_rec n'a que les droits SELECT, INSERT, UPDATE — jamais DROP ou DELETE. C'est une bonne pratique de sécurité.
Scripts Python — conversion WAV→MP3 et stockage MySQL
2.1 — Fichier de configuration (db_config.py)
/image%2F7223427%2F20260424%2Fob_8ed727_capture-fichierbd.png)
/var/lib/asterisk/recording/db_config.py
python
DB_CONFIG = {
"host": "localhost",
"user": "asterisk_rec",
"password": "Esmt@2025!",
"database": "asterisk_recordings",
"charset": "utf8mb4",
}
AUDIO_DIR = "/var/spool/asterisk/monitor"
LOG_FILE = "/var/log/asterisk/recording_errors.log"
LAME_BIN = "/usr/bin/lame"
2.2 — Script principal (post_record.py)
Ce script est appelé avec 6 arguments : fichier WAV, caller, callee, durée, uniqueid, contexte.
/var/lib/asterisk/recording/post_record.py
/image%2F7223427%2F20260424%2Fob_447d33_capture-postpy.png)
python
import sys, os, subprocess, logging
from datetime import datetime
sys.path.insert(0, "/var/lib/asterisk/recording")
from db_config import DB_CONFIG, AUDIO_DIR, LOG_FILE, LAME_BIN
logging.basicConfig(filename=LOG_FILE, level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s")
# Lecture des arguments
def get_arg(idx, default=""):
return sys.argv[idx].strip() if len(sys.argv) > idx else default
WAV_FILE = get_arg(1)
CALLER = get_arg(2, "inconnu")
CALLEE = get_arg(3, "inconnu")
BILLSEC = int(get_arg(4, "0"))
UNIQUEID = get_arg(5, "")
CONTEXT = get_arg(6, "")
def convert_to_mp3(wav_path):
mp3_path = wav_path.replace(".wav", ".mp3")
subprocess.run([LAME_BIN, "--silent", "-b", "32", "-m", "m",
wav_path, mp3_path], check=True, timeout=60)
os.remove(wav_path)
logging.info(f"WAV converti → {mp3_path}")
return mp3_path
def insert_db(wav_file, mp3_file, status="done"):
import mysql.connector
conn = mysql.connector.connect(**DB_CONFIG)
cur = conn.cursor()
cur.execute("""
INSERT INTO recordings
(uniqueid, calldate, caller, callee, context,
duration, wav_file, mp3_file, status)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
""", (UNIQUEID, datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
CALLER, CALLEE, CONTEXT, BILLSEC,
os.path.basename(wav_file) if wav_file else None,
os.path.basename(mp3_file) if mp3_file else None, status))
conn.commit(); cur.close(); conn.close()
if __name__ == "__main__":
if not WAV_FILE or not os.path.exists(WAV_FILE):
logging.warning("Aucun fichier WAV fourni — appel ignoré.")
sys.exit(0)
try:
mp3 = convert_to_mp3(WAV_FILE)
insert_db(WAV_FILE, mp3)
logging.info(f"OK | caller={CALLER} callee={CALLEE} dur={BILLSEC}s")
except Exception as e:
logging.error(f"ERREUR : {e}")
insert_db(WAV_FILE, None, status="error")
Permissions
bash
chown asterisk:asterisk /var/lib/asterisk/recording/*.py
chmod +x /var/lib/asterisk/recording/post_record.py
python3 /var/lib/asterisk/recording/post_record.py
cat /var/log/asterisk/recording_errors.log
# Doit afficher : WARNING Aucun fichier WAV fourni — appel ignoré.
Configuration Asterisk — extensions.conf
extensions complet
; ============================================================
; Contexte appels internes
; ============================================================
[from-internal]
; --- 1000 et 1001 fusionnés dans _1XXX (comportement identique) ---
; [REC] Pattern _1XXX gère désormais 1000 et 1001 avec enregistrement
exten => _1XXX,1,NoOp(Appel interne vers ${EXTEN})
same => n,Set(CALLED_NUM=${EXTEN})
same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
same => n,MixMonitor(${RECFILE}.wav,b)
same => n,Dial(PJSIP/${EXTEN})
same => n,Hangup()
exten => 1441,1,Goto(message,1441,1)
exten => _2XXX,1,NoOp(Appel de A vers B : ${EXTEN})
same => n,Set(CALLED_NUM=${EXTEN})
same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
same => n,MixMonitor(${RECFILE}.wav,b)
same => n,Dial(PJSIP/${EXTEN}@trunk-to-b,30,tT)
same => n,Hangup()
exten => 5081,1,Goto(ivr-esmt,s,1)
; [REC] Appels sortants GSM enregistrés
exten => _X.,1,NoOp(Appel sortant GSM : ${EXTEN})
same => n,Set(CALLED_NUM=${EXTEN})
same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
same => n,MixMonitor(${RECFILE}.wav,b)
same => n,Dial(PJSIP/${EXTEN}@goip)
same => n,Hangup()
exten => 3000,1,Goto(svi-ia,3000,1)
; [REC] Extension h — post-traitement au raccrochage
exten => h,1,NoOp(Raccrochage — enregistrement en cours de traitement)
same => n,GotoIf($["${RECFILE}" = ""]?end)
same => n,Set(REC_BILLSEC=${CDR(billsec)})
same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|from-internal" >> /tmp/rec_queue.txt)
same => n(end),NoOp(Fin h from-internal)
; ============================================================
; SVI Intelligent IA — extension 3000
; ============================================================
[svi-ia]
exten => 3000,1,Answer()
same => n,AGI(googletts.agi,"Bienvenue dans votre assistant administratif intelligent de l'ESMT",fr,any)
same => n,AGI(googletts.agi,"Posez votre question après le signal",fr,any)
same => n,Record(/var/spool/asterisk/tmp/question.wav,3,30)
same => n,AGI(svi_ia.agi)
same => n,Playback(/var/spool/asterisk/tmp/reponse)
same => n,Hangup()
; ============================================================
; Appels entrants depuis le site B
; ============================================================
[from-trunk-b]
exten => _1XXX,1,NoOp(Appel reçu de B vers A : ${EXTEN})
same => n,Set(CALLED_NUM=${EXTEN})
same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
same => n,MixMonitor(${RECFILE}.wav,b)
same => n,Dial(PJSIP/${EXTEN},20,tT)
same => n,Hangup()
; [REC] Extension h pour from-trunk-b
exten => h,1,NoOp(Raccrochage trunk-b)
same => n,GotoIf($["${RECFILE}" = ""]?end)
same => n,Set(REC_BILLSEC=${CDR(billsec)})
same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|from-trunk-b" >> /tmp/rec_queue.txt)
same => n(end),NoOp(Fin h from-trunk-b)
; ============================================================
; Rappel étudiant (inchangé)
; ============================================================
[student-callback]
exten => s,1,NoOp(=== Rappel etudiant Ext:${STUDENT_EXT} Keyword:${STUDENT_KEYWORD} ===)
same => n,Answer()
same => n,Wait(1)
same => n,Playback(beep)
same => n,Goto(svi-ia,3000,1)
same => n,Hangup()
; ============================================================
; Contexte SVI ESMT
; ============================================================
[ivr-esmt]
; ---- Entrée principale ------------------------------------
exten => s,1,Answer()
same => n,Set(ATTEMPT=0)
same => n,Goto(menu,1)
; ---- Menu -------------------------------------------------
exten => menu,1,NoOp(Tentative ${ATTEMPT}/3)
same => n,Set(ATTEMPT=$[${ATTEMPT} + 1])
same => n,GotoIf($[${ATTEMPT} > 3]?bye,1)
same => n,AGI(googletts.agi,"Assalamou anleykum Nagadef Bienvenue à l'ESMT.",fr,any)
same => n,GotoIf($[${ATTEMPT} > 1]?options,1)
same => n,AGI(googletts.agi,"Vous êtes dans le serveur vocal interactif.",fr,any)
same => n(options),AGI(googletts.agi,"Pour joindre Bao, appuyez sur 1.",fr,any)
same => n,AGI(googletts.agi,"Pour joindre Fatou, appuyez sur 2.",fr,any)
same => n,AGI(googletts.agi,"Pour réécouter, appuyez sur dièse.",fr,any)
same => n,Set(TIMEOUT(response)=10)
same => n,WaitExten(10)
; ---- Choix 1 : Bao ----------------------------------------
exten => 1,1,AGI(googletts.agi,"Transfert vers Bao.",fr,any)
same => n,Set(CALLED_NUM=1000)
same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-1000)
same => n,MixMonitor(${RECFILE}.wav,b)
same => n,Dial(PJSIP/1000,30,tT)
same => n,GotoIf($["${DIALSTATUS}" = "NOANSWER"]?vmail-bao,1)
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy,1)
same => n,Hangup()
; ---- Choix 2 : Fatou --------------------------------------
exten => 2,1,AGI(googletts.agi,"Transfert vers Fatou.",fr,any)
same => n,Set(CALLED_NUM=1001)
same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-1001)
same => n,MixMonitor(${RECFILE}.wav,b)
same => n,Dial(PJSIP/1001,30,tT)
same => n,GotoIf($["${DIALSTATUS}" = "NOANSWER"]?vmail-fatou,1)
same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy,1)
same => n,Hangup()
; ---- Rejouer ----------------------------------------------
exten => #,1,Set(ATTEMPT=0)
same => n,Goto(menu,1)
exten => _[3-9],1,Goto(menu,1)
; ---- Timeout ----------------------------------------------
exten => t,1,Goto(menu,1)
; ---- Touche invalide --------------------------------------
exten => i,1,AGI(googletts.agi,"Choix invalide.",fr,any)
same => n,Goto(menu,1)
; ---- Occupé -----------------------------------------------
exten => busy,1,AGI(googletts.agi,"La ligne est occupée. Veuillez rappeler ultérieurement.",fr,any)
same => n,Hangup()
; ---- Messagerie Bao ---------------------------------------
exten => vmail-bao,1,AGI(googletts.agi,"Bao est absent. Laissez votre message après le signal.",fr,any)
same => n,VoiceMail(1000@default,u)
same => n,Hangup()
; ---- Messagerie Fatou -------------------------------------
exten => vmail-fatou,1,AGI(googletts.agi,"Fatou est absente. Laissez votre message après le signal.",fr,any)
same => n,VoiceMail(1001@default,u)
same => n,Hangup()
; ---- Au revoir --------------------------------------------
exten => bye,1,AGI(googletts.agi,"Nous n'avons pas reçu votre choix. Au revoir.",fr,any)
same => n,Hangup()
; [REC] Extension h pour ivr-esmt
exten => h,1,NoOp(Raccrochage ivr-esmt)
same => n,GotoIf($["${RECFILE}" = ""]?end)
same => n,Set(REC_BILLSEC=${CDR(billsec)})
same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|ivr-esmt" >> /tmp/rec_queue.txt)
same => n(end),NoOp(Fin h ivr-esmt)
; ============================================================
; GoIP — appels entrants
; ============================================================
[from-goip]
exten => _X.,1,NoOp(Appel entrant GoIP depuis ${CALLERID(num)})
same => n,Goto(ivr-esmt,s,1)
same => n,Hangup()
; ============================================================
; Messages SIP internes
; ============================================================
[message]
exten => _10XX,1,NoOp(==> Message reçu de ${MESSAGE(from)} pour ${MESSAGE(to)})
same => n,Set(TO=${EXTEN})
same => n,Set(FROM=${MESSAGE(from)})
same => n,Set(MSG=${MESSAGE(body)})
same => n,Set(MESSAGE(body)=${MSG})
same => n,MessageSend(pjsip:${TO},${FROM})
same => n,NoOp(==> Message envoyé à ${TO})
same => n,Hangup()
exten => 1441,1,NoOp(==> Message reçu de ${MESSAGE(from)})
same => n,Set(FROM=${MESSAGE(from)})
same => n,Set(MSG=${MESSAGE(body)})
same => n,Set(EXT_RAW=${CUT(FROM,<,2)})
same => n,Set(EXT_RAW=${CUT(EXT_RAW,@,1)})
same => n,Set(EXT=${CUT(EXT_RAW,:,2)})
same => n,NoOp(Numero : ${EXT})
same => n,GotoIf($["${MSG}" = "note"]?call_api,1)
same => n,Set(MESSAGE(body)=Commande inconnue. Envoyez "note" à 1441.)
same => n,MessageSend(pjsip:${EXT},pjsip:1441)
same => n,Hangup()
exten => call_api,1,System(/usr/bin/python3 /var/lib/asterisk/note/get_note.py ${EXT})
same => n,Set(NOTE=${FILE(/var/lib/asterisk/note/note_${EXT}.txt)})
same => n,NoOp(==> Contenu du fichier : ${NOTE})
same => n,GotoIf($["${NOTE}" = ""]?file_not_found)
same => n,Set(MESSAGE(body)=Votre note est : ${NOTE})
same => n,MessageSend(pjsip:${EXT},pjsip:1441)
same => n,Hangup()
exten => file_not_found,1,Set(MESSAGE(body)=Note non trouvée pour l'extension ${EXT})
same => n,MessageSend(pjsip:${EXT},pjsip:1441)
same => n,Hangup()
; ============================================================
; Dongle GSM — SMS et appels entrants (inchangé)
; ============================================================
[from-dongle]
exten => sms,1,NoOp(SMS reçu de ${CALLERID(num)})
same => n,Set(MSG=${BASE64_DECODE(${SMS_BASE64})})
same => n,Set(NUM=${CALLERID(num)})
same => n,NoOp(Message décodé : ${MSG})
same => n,System(echo "${NUM}|${MSG}" >> /tmp/sms_queue.txt)
same => n,Hangup()
exten => report,1,NoOp(Rapport SMS reçu)
same => n,Hangup()
exten => s,1,NoOp(Appel entrant GSM depuis ${CALLERID(num)})
same => n,Goto(ivr-esmt,s,1)
exten => _X.,1,Goto(ivr-esmt,s,1)
3.1 — Principe : 3 ajouts par contexte
Pour chaque extension qui fait un appel, on ajoute ces 2 lignes avant Dial() :
Ajout avant chaque Dial()
asterisk
; 1. Sauvegarder le numéro appelé
same => n,Set(CALLED_NUM=${EXTEN})
; 2. Définir le nom du fichier d'enregistrement
same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
; 3. Démarrer l'enregistrement (b = both legs)
same => n,MixMonitor(${RECFILE}.wav,b)
3.2 — Extension h (déclenchée au raccrochage)
L'extension h s'exécute automatiquement à la fin de chaque appel. Elle écrit dans la file d'attente :
Extension h — à placer dans chaque contexte
asterisk
; Extension h pour from-internal
exten => h,1,NoOp(Raccrochage from-internal)
same => n,GotoIf($["${RECFILE}" = ""]?end)
same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|from-internal" >> /tmp/rec_queue.txt)
same => n(end),NoOp(Fin)
; Extension h pour from-trunk-b
exten => h,1,NoOp(Raccrochage trunk-b)
same => n,GotoIf($["${RECFILE}" = ""]?end)
same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|from-trunk-b" >> /tmp/rec_queue.txt)
same => n(end),NoOp(Fin)
; Extension h pour ivr-esmt
exten => h,1,NoOp(Raccrochage ivr-esmt)
same => n,GotoIf($["${RECFILE}" = ""]?end)
same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|ivr-esmt" >> /tmp/rec_queue.txt)
same => n(end),NoOp(Fin)
3.3 — Recharger le dialplan
Terminal
bash
asterisk -rx "dialplan reload"
# Vérifier que MixMonitor est bien dans le dialplan
asterisk -rx "dialplan show from-internal" | grep MixMonitor
# Doit afficher 3 lignes contenant MixMonitor
# Vérifier que h est présent
asterisk -rx "dialplan show from-internal" | grep -A4 "exten => h"
Pourquoi RECFILE et pas MIXMONITOR_FILENAME ?
La variable MIXMONITOR_FILENAME est vide dans l'extension h car elle appartient à un autre thread Asterisk. On utilise notre propre variable RECFILE définie sur le canal.
Pourquoi une file d'attente ? MixMonitor ferme le fichier WAV après l'extension h. On ne peut donc pas convertir immédiatement. La solution : écrire dans une file et traiter 1 minute plus tard avec cron.
/var/lib/asterisk/recording/process_queue.sh
/image%2F7223427%2F20260424%2Fob_a6cfa4_capture-queue.png)
bash
#!/bin/bash
QUEUE="/tmp/rec_queue.txt"
LOCK="/tmp/rec_queue.lock"
SCRIPT="/usr/bin/python3 /var/lib/asterisk/recording/post_record.py"
# Éviter les exécutions simultanées
[ -f "$LOCK" ] && exit 0
touch "$LOCK"
[ ! -f "$QUEUE" ] && rm -f "$LOCK" && exit 0
[ ! -s "$QUEUE" ] && rm -f "$LOCK" && exit 0
# Copier et vider la file atomiquement
cp "$QUEUE" "${QUEUE}.processing"
> "$QUEUE"
# Traiter chaque ligne : WAV|CALLER|CALLEE|BILLSEC|UNIQUEID|CONTEXT
while IFS='|' read -r WAV CALLER CALLEE BILLSEC UNIQUEID CONTEXT; do
[ -z "$WAV" ] && continue
[ ! -f "$WAV" ] && continue
$SCRIPT "$WAV" "$CALLER" "$CALLEE" "$BILLSEC" "$UNIQUEID" "$CONTEXT"
done < "${QUEUE}.processing"
rm -f "${QUEUE}.processing" "$LOCK"
Terminal
bash
chmod +x /var/lib/asterisk/recording/process_queue.sh
chown asterisk:asterisk /var/lib/asterisk/recording/process_queue.sh
# Ajouter au crontab root
crontab -e
Ajoutez cette ligne dans le crontab :
bash
# Traiter les enregistrements toutes les minutes
* * * * * /var/lib/asterisk/recording/process_queue.sh
/image%2F7223427%2F20260424%2Fob_a69b7e_capture-cont.png)
Interface Web PHP
L'interface se compose de 3 fichiers PHP dans /var/www/html/recordings/ :
/var/www/html/recordings/config.php
/image%2F7223427%2F20260424%2Fob_841fb7_capture-configpy.png)
php
<?php
define('DB_HOST', 'localhost');
define('DB_USER', 'asterisk_rec');
define('DB_PASS', 'Esmt@2025!');
define('DB_NAME', 'asterisk_recordings');
define('WEB_USER', 'admin');
define('WEB_PASS', 'esmt2025');
define('AUDIO_DIR','/var/spool/asterisk/monitor');
define('PER_PAGE', 20);
function get_pdo(): PDO {
static $pdo = null;
if ($pdo === null) {
$dsn = "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4";
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
return $pdo;
}
function require_auth(): void {
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
$pass = $_SERVER['PHP_AUTH_PW'] ?? '';
if ($user !== WEB_USER || $pass !== WEB_PASS) {
header('WWW-Authenticate: Basic realm="ESMT Recordings"');
header('HTTP/1.0 401 Unauthorized');
exit;
}
}
Ce fichier sert les MP3 avec support Range (lecture partielle HTML5) et vérifie l'authentification avant chaque accès.
/image%2F7223427%2F20260424%2Fob_87118d_capture-audiphp.png)
Fonctionnement de audio.php
text
1. require_auth() → vérifie login/password
2. basename($_GET['f']) → sécurité anti path-traversal
3. preg_match('/\.mp3$/i') → accepte uniquement les MP3
4. strpos(realpath($path), ...) → double vérification du chemin
5. HTTP Range support → permet seek dans l'audio HTML5
5.3 — Permissions des fichiers Web
Terminal
bash
chown -R www-data:www-data /var/www/html/recordings/
chmod 644 /var/www/html/recordings/*.php
# Vérifier que PHP-MySQL est installé
php -m | grep -i pdo
# Doit afficher : PDO et pdo_mysql
# Si absent :
apt-get install php-mysql -y
systemctl restart apache2
Test 1 — Vérifier la chaîne complète
Terminal — surveillance
bash
# Terminal 1 : surveiller les fichiers
watch -n1 "ls -lh /var/spool/asterisk/monitor/ | tail -5"
# Terminal 2 : surveiller les logs
tail -f /var/log/asterisk/recording_errors.log
# Terminal 3 : surveiller la file
watch -n1 "cat /tmp/rec_queue.txt 2>/dev/null || echo 'File vide'"
Appelez 1000 → 1001 depuis Linphone/Zoiper, parlez 5 secondes, raccrochez.
Séquence attendue
Log Asterisk console (asterisk -rvvv)
text
-- MixMonitor Recording PJSIP/1000-XXXXXXXX ← enregistrement démarré
-- PJSIP/1001 answered ← appel décroché
-- Channel left 'simple_bridge' ← appel terminé
-- Executing [h@from-internal:1] NoOp(...) ← extension h lancée
-- Executing [h@from-internal:2] GotoIf(...) ← RECFILE non vide → continue
-- Executing [h@from-internal:3] System(echo ...) ← écriture dans la file
== End MixMonitor Recording ← WAV fermé
Après < 1 minute — log post_record.py
text
[INFO] WAV converti → /var/spool/asterisk/monitor/20260423-XXXXXX-1000-1001.mp3
[INFO] OK | caller=1000 callee=1001 dur=5s uid=...
Test 2 — Vérifier MySQL
Terminal
bash
mysql -u asterisk_rec -p'Esmt@2025!' asterisk_recordings \
-e "SELECT id, calldate, caller, callee, duration, status
FROM recordings ORDER BY id DESC LIMIT 5;"
URL : http://VOTREIP/recordings/
Login : admin
Password : esmt2025
/image%2F7223427%2F20260424%2Fob_ccdb70_capture-coninter.png)
Le lecteur audio HTML5 utilise audio.php?f=fichier.mp3 qui vérifie l'authentification à chaque accès. Les fichiers MP3 ne sont jamais exposés directement.
/image%2F7223427%2F20260424%2Fob_06ea9d_capture-resultat.png)
Dépannage rapide
| Problème | Cause probable | Solution |
| WAV reste, pas de MP3 | lame absent | apt-get install lame -y |
| File vide après appel | RECFILE vide ou h absent | Vérifier dialplan show from-internal |
| MySQL vide | mysql-connector absent | pip3 install mysql-connector-python |
| Audio ne joue pas | www-data sans accès monitor | usermod -aG asterisk www-data |
| Page Web : accès refusé | config.php absent | Vérifier ls /var/www/html/recordings/ |
Résultat final
À la fin du TP, le système complet est opérationnel :
- Chaque appel SIP est automatiquement enregistré en WAV par MixMonitor
- Au raccrochage, l'extension h écrit dans /tmp/rec_queue.txt
- Le cron (toutes les minutes) lance process_queue.sh
- Python convertit WAV→MP3 avec lame et insère dans MySQL
- L'interface Web affiche l'historique avec filtres (numéro, date, contexte)
- Chaque enregistrement est écoutable directement dans le navigateur
- L'accès est protégé par authentification HTTP Basic
- Les fichiers audio ne sont jamais exposés directement (proxy PHP)
RÉSUMÉ DES COMPOSANTS
| Composant | Technologie | Rôle |
| MixMonitor | Asterisk | Capture audio des deux côtés de l'appel |
| Extension h | Asterisk dialplan | Déclenchée au raccrochage → écrit dans la file |
| process_queue.sh | Bash + cron | Lit la file toutes les minutes |
| post_record.py | Python 3 | Convertit WAV→MP3, insère dans MySQL |
| recordings (table) | MySQL 8 | Stocke toutes les métadonnées |
| index.php | PHP 8.3 | Interface Web : historique + filtres |
| audio.php | PHP 8.3 | Streaming MP3 sécurisé (HTTP Range) |
🎬 Démo vidéo Démonstration complète du système en action 📞 Appel SIP entre 1000 et 1001 — enregistrement automatique et consultation via l'interface Web