Hooks de Scripting y Almacenamiento
plugNmeet cuenta con un potente sistema de "hooks" (ganchos) que le permite ejecutar scripts o comandos personalizados en puntos clave del ciclo de vida de la gestión de archivos y medios. Esto posibilita una personalización profunda, permitiéndole integrarse con cualquier proveedor de almacenamiento externo (por ejemplo, S3, Google Cloud), llamar a APIs personalizadas u orquestar complejas canalizaciones (pipelines) multiservidor.
Los hooks están disponibles tanto en los componentes server como recorder.
Esta página cubre los detalles técnicos del sistema de hooks. Para ejemplos prácticos del mundo real en varios lenguajes de programación, consulta nuestras publicaciones de blog etiquetadas con hooks.
Las estructuras de datos JSON para los hooks pueden cambiar con el tiempo. Esta documentación proporciona ejemplos, pero para las definiciones más actualizadas, por favor consulte el repositorio oficial plugnmeet-protocol:
https://github.com/mynaparrot/plugnmeet-protocol/tree/main/hooks
Conceptos Fundamentales
Todos los hooks, independientemente de dónde se ejecuten, comparten el mismo diseño fundamental. Comprender estos conceptos es esencial antes de implementar sus propios scripts personalizados.
Modelos de Ejecución
Para obtener el máximo rendimiento y flexibilidad, los scripts de los hooks pueden configurarse como procesos de larga duración o como comandos de única ejecución.
- Procesos de Larga Duración: Cuando un componente de plugNmeet (como
serverorecorder) se inicia, lanza su script una sola vez. El script se ejecuta de forma continua, escuchando solicitudes. Este modelo es altamente eficiente ya que evita la sobrecarga de iniciar un nuevo proceso para cada evento. Este es el enfoque recomendado para la mayoría de los casos de uso. - Comandos de Única Ejecución: Son comandos simples (como
curl,wget, o la utilidad integradahttp-request) que se ejecutan para cada evento de hook. Son adecuados para tareas sencillas y autocontenidas.
Protocolo de Comunicación
La comunicación con su script de hook se realiza a través de las tuberías de E/S estándar utilizando JSON delimitado por saltos de línea.
stdin: Su script lee solicitudes desdestdin. Cada línea es un objeto JSON completo que representa una única solicitud.stdout: Por cada solicitud recibida, su script debe imprimir una única línea de JSON enstdoutcomo respuesta.stderr: Puede usarstderrpara registrar información y depurar dentro de su script. plugNmeet ignora esta salida, pero es invaluable para el desarrollo.
La llamada a un script de hook es síncrona y bloqueante. Su script DEBE escribir una respuesta en stdout por cada solicitud que reciba. Si un script no devuelve una respuesta, el servicio de plugNmeet se quedará colgado indefinidamente.
El Modelo de Tubería (Pipeline)
Si define múltiples scripts para un solo hook, estos forman una tubería. La respuesta stdout del primer script se convierte en la solicitud stdin para el segundo, y así sucesivamente.
Si un script en la cadena no necesita modificar los datos (por ejemplo, solo registra el evento), debe aun así pasar el objeto JSON original y sin modificar a stdout.
Manejo de Errores y Datos
- Respuesta JSON Válida: Si su script devuelve un JSON válido, este se pasa al siguiente script o se utiliza como el resultado final.
- Respuesta JSON Inválida: Si la respuesta no es un JSON válido, plugNmeet registra una advertencia y descarta la salida. Los datos JSON originales (el
stdinde su script) se pasarán al siguiente script en la tubería. Esto evita que un solo script defectuoso rompa toda la cadena. - Reporte de Errores: Si su script encuentra un error, debe poblar el campo
erroren su respuesta JSON. Es crucial devolver siempre el objeto JSON de entrada completo, con el campoerrorpoblado, para asegurar que los scripts posteriores en una tubería reciban la estructura de datos esperada. La aplicación principal registrará este error.
Cómo Crear un Hook
- Cree un Script o Comando: Escriba su lógica en cualquier lenguaje (Shell, NodeJS, Go, etc.).
- Hágalo Ejecutable: Asegúrese de que su script tenga permisos de ejecución (p. ej.,
chmod +x su_script.js). - Configure en
config.yaml: Añada la ruta absoluta de su ejecutable (o el comando) en la secciónhooksapropiada de suconfig.yaml.
Utilidad Integrada http-request
plugNmeet proporciona un comando de única ejecución conveniente, http-request, para enviar la carga útil (payload) JSON del hook a un punto final HTTP/HTTPS.
Uso:
http-request <URL>
Ejemplo en config.yaml:
scripts:
- script: "http-request http://localhost:8090/su/endpoint"
is_one_shot: true
Responsabilidades Clave
Responsabilidad de la Limpieza de Archivos
Cuando habilita un hook que recibe una ruta de archivo (como upload_hook o post_transcoding), plugNmeet desactiva su propia limpieza automática de archivos para esa etapa.
Esta es una característica de seguridad crítica. La aplicación delega la gestión de archivos a su script porque no puede conocer su naturaleza (p. ej., si es un cargador o solo un notificador). Si la aplicación eliminara un archivo antes de que su script pudiera procesarlo, causaría un error.
Por lo tanto, la responsabilidad de la gestión de archivos se le transfiere a usted, y el rol de su script determina su responsabilidad:
-
Si su script MUEVE o SUBE el archivo (p. ej., a S3), DEBE eliminar el archivo fuente local de
input_pathdespués de que la transferencia sea exitosa. Esto es esencial para evitar que el disco de su servidor se llene. -
Si su script solo OBSERVA el archivo (p. ej., para registro, análisis o envío de una notificación) y no lo mueve, NO DEBE eliminar el archivo. El archivo todavía es necesario para el almacenamiento interno de plugNmeet o para hooks posteriores en una tubería.
Responsabilidad de la Consistencia de las Rutas
plugNmeet no valida la output_path que usted devuelve desde un hook. Se almacena como una cadena de texto y se utiliza como input_path para las llamadas posteriores a download_hook y delete_hook.
-
Si su script modifica
output_path(por ejemplo, cambiando una ruta local por una clave de S3 en unupload_hookopost_transcoding_hook), asume la total responsabilidad de esa ruta. DEBE implementar también los correspondientesdownload_hookydelete_hookque puedan entender y procesar el formato de ruta personalizado que ha definido. -
Si su script es solo para observación (por ejemplo, para registrar estadísticas o enviar una notificación) y no modifica la
output_path, entonces no necesita proporcionar los otros hooks. El flujo de trabajo predeterminado continuará con la ruta original.
No proporcionar scripts download_hook y delete_hook compatibles después de cambiar la output_path resultará en descargas y eliminaciones rotas.
Hooks de Almacenamiento del Servidor (server)
Los hooks del servidor le permiten anular el almacenamiento de archivos local por defecto para artefactos de sala, archivos de chat y grabaciones, permitiendo la integración con cualquier proveedor de almacenamiento externo.
Configuración en server/config.yaml:
hooks:
# 'pool_size' controla la ejecución en paralelo. Por defecto: 1.
# 'hook_timeout' establece un tiempo de espera. Por defecto: 5m.
upload_hook:
pool_size: 2
scripts:
- script: "/ruta/a/su/script_de_subida.sh"
is_one_shot: false
download_hook:
scripts:
- script: "/ruta/a/su/script_de_descarga.sh"
is_one_shot: false
# ... otros hooks ...
Tipos de Hooks
upload_hook
- Propósito: Subir un archivo o directorio (p. ej., analíticas de sala, imágenes de pizarra) a un almacenamiento externo.
- Entrada (
UploadHookData):{"input_path": "/ruta/en/disco/servidor/analytics.json","input_directory_path": "", // o "/ruta/a/imagenes/convertidas/""hook_file_type": "artifact","room_id": "sala01","room_sid": "SID_d82k3s9d2l"} - Tarea del Script: Subir el contenido de
input_pathoinput_directory_path. Devolver el JSON conoutput_pathestablecido a un identificador de almacenamiento único (p. ej.,artifacts/sala01/analytics.json). Después de una subida exitosa, debe eliminar el archivo/directorio fuente local.
download_hook
- Propósito: Proporcionar una forma segura para que los usuarios descarguen un archivo desde el almacenamiento externo.
- Entrada (
DownloadHookData):{"input_path": "artifacts/sala01/analytics.json", // El identificador de almacenamiento"hook_file_type": "artifact"} - Tarea del Script: Devolver un JSON especificando una
action.action: "redirect"(Recomendado): Generar una URL temporal y pre-firmada y devolverla en el camporedirect_url.action: "serve_local": Descargar el archivo a una ruta local temporal en el servidor y devolver la ruta enoutput_pathy elmime_typedel archivo.
delete_hook
- Propósito: Eliminar un archivo del almacenamiento externo.
- Entrada (
DeleteHookData):{"input_path": "artifacts/sala01/analytics.json", // El identificador de almacenamiento"hook_file_type": "artifact"} - Tarea del Script: Eliminar el archivo del almacenamiento y devolver una respuesta de confirmación.
resumable_upload_hook
- Propósito: Manejar subidas de archivos en trozos (chunks) para la función de chat, típicamente delegando la lógica a un servicio como S3 Multipart Upload.
- Entrada (
ResumableUploadHookData): Contiene un campotype(part-check,part-upload,merge) que dicta la acción requerida. - Tarea del Script: Interactuar con su backend de almacenamiento para verificar, subir o fusionar trozos de archivo. La respuesta debe indicar el resultado (p. ej.,
part_exists,merge_success).
room_end_hook
- Propósito: Realizar tareas de limpieza después de que una sesión de sala ha finalizado completamente (p. ej., limpiar trozos de subidas reanudables abandonadas).
- Entrada (
RoomEndHookData):{"room_id": "sala01","room_sid": "SID_d82k3s9d2l"} - Tarea del Script: Realizar la limpieza y devolver un mensaje de confirmación.
Hooks del Grabador (recorder)
Los hooks del grabador se utilizan para gestionar el archivo de grabación a medida que avanza por la tubería de transcodificación. Esto es esencial para despliegues multiservidor donde la grabación y la transcodificación pueden ocurrir en máquinas diferentes.
Configuración en recorder/config.yaml:
hooks:
# 'pool_size' controla cuántas tuberías de hooks pueden ejecutarse en paralelo. Por defecto: 1.
# 'hook_timeout' establece un tiempo de espera para toda la cadena de hooks. Por defecto: 1h.
post_recording:
pool_size: 2
hook_timeout: 2h
scripts:
- script: "./scripts/post-recording/upload.sh"
is_one_shot: false
pre_transcoding:
scripts:
- script: "./scripts/pre-transcoding/download.sh"
is_one_shot: false
post_transcoding:
scripts:
- script: "./scripts/post-transcoding/notify.sh"
is_one_shot: false
Carga de Datos (Payload): RecordingHookData
Todos los hooks del grabador reciben y se espera que devuelvan un objeto JSON con esta estructura.
{
"task": "single",
"recording_id": "REC_ax9s3djn2s",
"room_table_id": 123,
"room_id": "sala01",
"room_sid": "SID_d82k3s9d2l",
"file_name": "REC_ax9s3djn2s.mp4",
"recorder_id": "node_01",
"file_size": 123.45,
"input_path": "/ruta/al/archivo.mp4",
"input_paths": [],
"output_path": "",
"error": "",
"should_cleanup": false,
"source_for_cleanup": ""
}
Etapas del Hook
1. post_recording
- Cuándo: Se ejecuta en el nodo GRABADOR después de que se guarda el archivo de grabación en bruto.
- Propósito: Subir el archivo en bruto a una ubicación accesible por red (p. ej., S3, NFS) para que pueda ser accedido por un transcodificador.
- Entrada: El campo
input_pathcontiene la ruta al archivo de grabación en bruto en el disco local del grabador. - Tarea del Script: Subir el archivo y devolver el JSON con
output_pathestablecido a la nueva ubicación/identificador de red del archivo (p. ej., una clave de S3). Después de una subida exitosa, debe eliminar el archivo fuente local deinput_path.
2. pre_transcoding
- Cuándo: Se ejecuta en el nodo TRANSCODIFICADOR antes de que comience el procesamiento con
ffmpeg. - Propósito: Descargar el archivo en bruto desde una ubicación de red a una ruta local temporal en el transcodificador.
- Entrada: El campo
input_pathcontiene la ubicación/identificador de red del archivo en bruto (este es eloutput_pathde la etapapost_recording). - Tarea del Script: Descargar el archivo y devolver el JSON con
output_pathestablecido a la nueva ruta local en el disco del transcodificador.
3. post_transcoding
- Cuándo: Se ejecuta en el nodo TRANSCODIFICADOR después de que
ffmpegcrea con éxito el archivo.mp4final. - Propósito: Subir el archivo procesado final al almacenamiento permanente y realizar la limpieza.
- Entrada: El campo
input_pathcontiene la ruta al archivo.mp4final y transcodificado en el disco local del nodo transcodificador. - Tarea del Script: Subir el archivo final y devolver el JSON, actualizando opcionalmente
output_path. Los camposshould_cleanupysource_for_cleanupse pueden usar para gestionar la limpieza de archivos temporales de la etapapre_transcoding.
Ejemplo: Script de Larga Duración en Node.js
Este ejemplo muestra la estructura básica de un script de larga duración que analiza JSON de forma segura, realiza una acción y devuelve una respuesta.
#!/usr/bin/env node
// scripts/mi_hook.js
const readline = require('readline');
const fs = require('fs');
// Usar stderr para el registro para que no interfiera con stdout
const log = (message) => {
console.error(`[MiHook] ${new Date().toISOString()}: ${message}`);
};
log('Iniciando script de hook de larga duración...');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
rl.on('line', (line) => {
let requestData;
try {
requestData = JSON.parse(line);
log(`Solicitud recibida para la sala: ${requestData.room_id || 'N/A'}`);
// --- Su Lógica Personalizada Aquí ---
// ej., subir a S3, llamar a una API, etc.
// Después de una subida exitosa, recuerde limpiar el archivo de origen.
if (requestData.input_path) {
// En un script real, haría esto DESPUÉS de una subida exitosa.
// fs.unlinkSync(requestData.input_path);
// log(`Limpiado ${requestData.input_path}`);
}
requestData.processed_by_hook = true;
// ---
// SIEMPRE escriba una respuesta JSON válida en stdout
process.stdout.write(JSON.stringify(requestData) + '\n');
} catch (e) {
log(`ERROR: ${e.message}`);
// Si ocurre un error, devuelva el objeto requestData original con un campo 'error'.
// Esto asegura que la estructura completa se mantenga para los scripts posteriores.
const errorResponse = requestData
? { ...requestData, error: e.message, output_path: "" } // Limpiar output_path en caso de error
: { error: `Fallo al analizar el JSON entrante: ${e.message}` };
process.stdout.write(JSON.stringify(errorResponse) + '\n');
}
});
rl.on('close', () => {
log('Stdin cerrado. Saliendo del script.');
process.exit(0);
});