Phase 2: services/mam-api/src/routes/recorders.js
This commit is contained in:
parent
6994e2d697
commit
8ab7ea4d8d
1 changed files with 394 additions and 0 deletions
394
services/mam-api/src/routes/recorders.js
Normal file
394
services/mam-api/src/routes/recorders.js
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
import express from 'express';
|
||||
import http from 'http';
|
||||
import pool from '../db/pool.js';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
// Docker API helper function
|
||||
function dockerApi(method, path, body = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
socketPath: '/var/run/docker.sock',
|
||||
path: `/v1.43${path}`,
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
const req = http.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve({ status: res.statusCode, data: data ? JSON.parse(data) : {} });
|
||||
} catch {
|
||||
resolve({ status: res.statusCode, data });
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (body) req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to generate clip name with timestamp
|
||||
function generateClipName(recorderName) {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
return `${recorderName}_${year}${month}${day}_${hours}${minutes}${seconds}`;
|
||||
}
|
||||
|
||||
// GET / - List all recorders
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM recorders ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// POST / - Create a new recorder
|
||||
router.post('/', async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
source_type,
|
||||
source_config,
|
||||
recording_codec,
|
||||
recording_resolution,
|
||||
proxy_enabled,
|
||||
proxy_codec,
|
||||
proxy_resolution,
|
||||
project_id,
|
||||
} = req.body;
|
||||
|
||||
if (!name || !source_type) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Name and source_type are required' });
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO recorders (
|
||||
id,
|
||||
name,
|
||||
source_type,
|
||||
source_config,
|
||||
recording_codec,
|
||||
recording_resolution,
|
||||
proxy_enabled,
|
||||
proxy_codec,
|
||||
proxy_resolution,
|
||||
project_id,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
[
|
||||
id,
|
||||
name,
|
||||
source_type,
|
||||
source_config || {},
|
||||
recording_codec || 'prores_hq',
|
||||
recording_resolution || 'native',
|
||||
proxy_enabled !== false,
|
||||
proxy_codec || 'libx264',
|
||||
proxy_resolution || '1920x1080',
|
||||
project_id || null,
|
||||
'stopped',
|
||||
]
|
||||
);
|
||||
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /:id - Get single recorder
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM recorders WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Recorder not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// POST /:id/start - Start recording
|
||||
router.post('/:id/start', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get recorder config from DB
|
||||
const recorderResult = await pool.query(
|
||||
'SELECT * FROM recorders WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (recorderResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Recorder not found' });
|
||||
}
|
||||
|
||||
const recorder = recorderResult.rows[0];
|
||||
|
||||
if (recorder.status === 'recording') {
|
||||
return res.status(400).json({ error: 'Recorder is already recording' });
|
||||
}
|
||||
|
||||
// Get S3 config from environment
|
||||
const s3Endpoint = process.env.S3_ENDPOINT;
|
||||
const s3Bucket = process.env.S3_BUCKET;
|
||||
const s3AccessKey = process.env.S3_ACCESS_KEY;
|
||||
const s3SecretKey = process.env.S3_SECRET_KEY;
|
||||
const mamApiUrl = process.env.MAM_API_URL || 'http://mam-api:3000';
|
||||
const dockerNetwork = process.env.DOCKER_NETWORK || 'wild-dragon_wild-dragon';
|
||||
|
||||
// Generate clip name with timestamp
|
||||
const clipName = generateClipName(recorder.name);
|
||||
|
||||
// Build container config
|
||||
const containerConfig = {
|
||||
Image: 'wild-dragon-capture:latest',
|
||||
Env: [
|
||||
`S3_ENDPOINT=${s3Endpoint}`,
|
||||
`S3_BUCKET=${s3Bucket}`,
|
||||
`S3_ACCESS_KEY=${s3AccessKey}`,
|
||||
`S3_SECRET_KEY=${s3SecretKey}`,
|
||||
`MAM_API_URL=${mamApiUrl}`,
|
||||
`RECORDER_ID=${id}`,
|
||||
`SOURCE_TYPE=${recorder.source_type}`,
|
||||
`SOURCE_CONFIG=${JSON.stringify(recorder.source_config)}`,
|
||||
`RECORDING_CODEC=${recorder.recording_codec}`,
|
||||
`RECORDING_RESOLUTION=${recorder.recording_resolution}`,
|
||||
`PROXY_ENABLED=${recorder.proxy_enabled}`,
|
||||
`PROXY_CODEC=${recorder.proxy_codec}`,
|
||||
`PROXY_RESOLUTION=${recorder.proxy_resolution}`,
|
||||
`PROJECT_ID=${recorder.project_id}`,
|
||||
`CLIP_NAME=${clipName}`,
|
||||
],
|
||||
HostConfig: {
|
||||
Privileged: true,
|
||||
NetworkMode: dockerNetwork,
|
||||
},
|
||||
Hostname: `recorder-${recorder.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`,
|
||||
};
|
||||
|
||||
// Create container
|
||||
const createRes = await dockerApi('POST', '/containers/create', containerConfig);
|
||||
|
||||
if (createRes.status !== 201) {
|
||||
return res.status(500).json({
|
||||
error: 'Failed to create container',
|
||||
details: createRes.data,
|
||||
});
|
||||
}
|
||||
|
||||
const containerId = createRes.data.Id;
|
||||
|
||||
// Start container
|
||||
const startRes = await dockerApi('POST', `/containers/${containerId}/start`);
|
||||
|
||||
if (startRes.status !== 204) {
|
||||
return res.status(500).json({
|
||||
error: 'Failed to start container',
|
||||
details: startRes.data,
|
||||
});
|
||||
}
|
||||
|
||||
// Update recorder in DB
|
||||
const updateResult = await pool.query(
|
||||
`UPDATE recorders
|
||||
SET container_id = $1, status = $2, current_session_id = $3, updated_at = NOW()
|
||||
WHERE id = $4
|
||||
RETURNING *`,
|
||||
[containerId, 'recording', clipName, id]
|
||||
);
|
||||
|
||||
res.json(updateResult.rows[0]);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// POST /:id/stop - Stop recording
|
||||
router.post('/:id/stop', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get recorder from DB
|
||||
const recorderResult = await pool.query(
|
||||
'SELECT * FROM recorders WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (recorderResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Recorder not found' });
|
||||
}
|
||||
|
||||
const recorder = recorderResult.rows[0];
|
||||
|
||||
if (!recorder.container_id) {
|
||||
return res.status(400).json({ error: 'No container running' });
|
||||
}
|
||||
|
||||
// Stop container
|
||||
const stopRes = await dockerApi(
|
||||
'POST',
|
||||
`/containers/${recorder.container_id}/stop`
|
||||
);
|
||||
|
||||
if (stopRes.status !== 204) {
|
||||
return res.status(500).json({
|
||||
error: 'Failed to stop container',
|
||||
details: stopRes.data,
|
||||
});
|
||||
}
|
||||
|
||||
// Remove container
|
||||
const removeRes = await dockerApi(
|
||||
'DELETE',
|
||||
`/containers/${recorder.container_id}`
|
||||
);
|
||||
|
||||
if (removeRes.status !== 204) {
|
||||
return res.status(500).json({
|
||||
error: 'Failed to remove container',
|
||||
details: removeRes.data,
|
||||
});
|
||||
}
|
||||
|
||||
// Update recorder in DB
|
||||
const updateResult = await pool.query(
|
||||
`UPDATE recorders
|
||||
SET container_id = NULL, status = $1, updated_at = NOW()
|
||||
WHERE id = $2
|
||||
RETURNING *`,
|
||||
['stopped', id]
|
||||
);
|
||||
|
||||
res.json(updateResult.rows[0]);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /:id/status - Get live status
|
||||
router.get('/:id/status', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get recorder from DB
|
||||
const recorderResult = await pool.query(
|
||||
'SELECT * FROM recorders WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (recorderResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Recorder not found' });
|
||||
}
|
||||
|
||||
const recorder = recorderResult.rows[0];
|
||||
|
||||
if (!recorder.container_id) {
|
||||
return res.json({
|
||||
status: recorder.status,
|
||||
duration: 0,
|
||||
containerId: null,
|
||||
});
|
||||
}
|
||||
|
||||
// Query Docker API for container status
|
||||
const inspectRes = await dockerApi(
|
||||
'GET',
|
||||
`/containers/${recorder.container_id}/json`
|
||||
);
|
||||
|
||||
if (inspectRes.status !== 200) {
|
||||
return res.json({
|
||||
status: 'unknown',
|
||||
duration: 0,
|
||||
containerId: recorder.container_id,
|
||||
});
|
||||
}
|
||||
|
||||
const container = inspectRes.data;
|
||||
const startedAt = new Date(container.State.StartedAt).getTime();
|
||||
const now = Date.now();
|
||||
const duration = Math.floor((now - startedAt) / 1000);
|
||||
|
||||
res.json({
|
||||
status: container.State.Running ? 'recording' : 'stopped',
|
||||
duration,
|
||||
containerId: recorder.container_id,
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /:id - Delete recorder
|
||||
router.delete('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Get recorder from DB
|
||||
const recorderResult = await pool.query(
|
||||
'SELECT * FROM recorders WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (recorderResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Recorder not found' });
|
||||
}
|
||||
|
||||
const recorder = recorderResult.rows[0];
|
||||
|
||||
// If recording, stop the container first
|
||||
if (recorder.container_id) {
|
||||
try {
|
||||
await dockerApi('POST', `/containers/${recorder.container_id}/stop`);
|
||||
await dockerApi('DELETE', `/containers/${recorder.container_id}`);
|
||||
} catch (err) {
|
||||
console.error('Error stopping container during delete:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from DB
|
||||
const deleteResult = await pool.query(
|
||||
'DELETE FROM recorders WHERE id = $1 RETURNING *',
|
||||
[id]
|
||||
);
|
||||
|
||||
res.json({ message: 'Recorder deleted', recorder: deleteResult.rows[0] });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Loading…
Reference in a new issue