1
0
mirror of git://f0xx.org/ac/ac-deploy synced 2026-07-29 07:17:38 +03:00

Enable lab media_pipelines in composed config and wire ac-ms-media-transcode.

Overlay media pipeline PHP/JS/SQL on compose, nginx HLS location, enable
feature in lab config.php, and ensure media storage dirs on BE nodes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-07-11 15:27:01 +02:00
parent 54174e7497
commit b8bd0e673c
9 changed files with 898 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../src/bootstrap.php';
require_once __DIR__ . '/../../src/MediaPipelinesRepository.php';
require_once __DIR__ . '/../../src/MediaPipelineRunner.php';
header('Content-Type: application/json; charset=utf-8');
Auth::check();
if (!MediaPipelinesRepository::gateAllowed()) {
json_out(['ok' => false, 'error' => 'forbidden'], 403);
}
$user = Auth::user();
$userId = (int) ($user['id'] ?? 0);
$companyId = (int) cfg('rbac.default_company_id', 1);
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$action = trim((string) ($_GET['action'] ?? ''));
if ($method === 'GET') {
if ($action === 'config') {
json_out([
'ok' => true,
'config' => [
'hls_url_prefix' => MediaPipelineRunner::hlsPublicPrefix(),
'ytdlp_enabled' => (bool) cfg('media_pipelines.ytdlp_enabled', true),
'script_root' => MediaPipelineRunner::scriptRoot(),
],
'permissions' => [
'can_operate' => MediaPipelinesRepository::gateCanOperate(),
],
]);
}
if ($action === 'list') {
json_out([
'ok' => true,
'pipelines' => MediaPipelinesRepository::listForUser($userId, $companyId),
]);
}
$id = (int) ($_GET['id'] ?? 0);
if ($action === 'get' && $id > 0) {
$row = MediaPipelinesRepository::getById($id, $userId, $companyId);
if ($row === null) {
json_out(['ok' => false, 'error' => 'not_found'], 404);
}
json_out(['ok' => true, 'pipeline' => $row]);
}
if ($action === 'logs' && $id > 0) {
$row = MediaPipelinesRepository::getById($id, $userId, $companyId);
if ($row === null) {
json_out(['ok' => false, 'error' => 'not_found'], 404);
}
json_out(['ok' => true, 'lines' => MediaPipelineRunner::tailLog($row)]);
}
json_out(['ok' => false, 'error' => 'unknown_action'], 400);
}
if ($method !== 'POST') {
json_out(['ok' => false, 'error' => 'method_not_allowed'], 405);
}
if (!MediaPipelinesRepository::gateCanOperate()) {
json_out(['ok' => false, 'error' => 'forbidden'], 403);
}
$raw = file_get_contents('php://input') ?: '';
$body = json_decode($raw, true);
if (!is_array($body)) {
json_out(['ok' => false, 'error' => 'invalid_json'], 400);
}
if ($action === 'create') {
try {
$row = MediaPipelinesRepository::create($userId, $companyId, $body);
json_out(['ok' => true, 'pipeline' => $row]);
} catch (InvalidArgumentException $e) {
json_out(['ok' => false, 'error' => $e->getMessage()], 400);
} catch (Throwable $e) {
json_out(['ok' => false, 'error' => 'create_failed'], 500);
}
}
$id = (int) ($body['id'] ?? $_GET['id'] ?? 0);
if ($id <= 0) {
json_out(['ok' => false, 'error' => 'missing_id'], 400);
}
$pipeline = MediaPipelinesRepository::getById($id, $userId, $companyId);
if ($pipeline === null) {
json_out(['ok' => false, 'error' => 'not_found'], 404);
}
if ($action === 'probe') {
$result = MediaPipelineRunner::probe($pipeline);
if ($result['ok'] ?? false) {
MediaPipelinesRepository::updateRuntime($id, [
'probe_json' => json_encode($result['probe'], JSON_UNESCAPED_SLASHES),
]);
}
json_out($result);
}
if ($action === 'start') {
try {
json_out(MediaPipelineRunner::start($pipeline));
} catch (Throwable $e) {
MediaPipelinesRepository::updateStatus($id, 'error', $e->getMessage());
json_out(['ok' => false, 'error' => $e->getMessage()], 500);
}
}
if ($action === 'stop') {
json_out(MediaPipelineRunner::stop($id, $userId, $companyId));
}
json_out(['ok' => false, 'error' => 'unknown_action'], 400);

View File

@@ -0,0 +1,233 @@
(function () {
const API = 'media_pipelines.php';
let canOperate = document.body.getAttribute('data-can-mp-operate') === '1';
let player = null;
let selectedId = null;
let pollTimer = null;
function basePath() {
return document.body.getAttribute('data-base-path') || '';
}
function apiUrl(action, params) {
const u = new URL(basePath() + '/api/' + API, window.location.origin);
u.searchParams.set('action', action);
if (params) {
Object.keys(params).forEach(function (k) {
u.searchParams.set(k, params[k]);
});
}
return u.toString();
}
function setStatus(msg, isError) {
const el = document.getElementById('mp-status');
if (!el) return;
el.textContent = msg;
el.classList.toggle('reports-status--error', !!isError);
}
async function fetchJson(url, options) {
const res = await fetch(url, Object.assign({ credentials: 'same-origin' }, options || {}));
const data = await res.json().catch(function () { return { ok: false, error: 'invalid_json' }; });
if (!res.ok && data.ok !== false) {
data.ok = false;
data.error = data.error || ('http_' + res.status);
}
return data;
}
function esc(s) {
const d = document.createElement('div');
d.textContent = s == null ? '' : String(s);
return d.innerHTML;
}
function truncate(s, n) {
s = String(s || '');
return s.length > n ? s.slice(0, n) + '…' : s;
}
function destroyPlayer() {
if (player && typeof player.dispose === 'function') {
try { player.dispose(); } catch (e) { /* ignore */ }
}
player = null;
}
function loadPlayer(hlsUrl) {
const section = document.getElementById('mp-player-section');
const urlEl = document.getElementById('mp-player-url');
if (!section || !hlsUrl) {
if (section) section.hidden = true;
destroyPlayer();
return;
}
section.hidden = false;
if (urlEl) urlEl.textContent = hlsUrl;
if (typeof window.videojs !== 'function') {
setStatus('Video.js not loaded — check nuevo-player assets', true);
return;
}
destroyPlayer();
player = window.videojs('mp-player', {
controls: true,
autoplay: false,
preload: 'auto',
fluid: true,
html5: { vhs: { overrideNative: true }, nativeAudioTracks: false, nativeTextTracks: false },
});
player.src({ src: hlsUrl, type: 'application/x-mpegURL' });
if (typeof player.nuevo === 'function') {
player.nuevo({});
}
}
async function loadList() {
const data = await fetchJson(apiUrl('list'));
if (!data.ok) {
setStatus(data.error || 'list failed', true);
return;
}
renderTable(data.pipelines || []);
setStatus('Updated — ' + (data.pipelines || []).length + ' pipeline(s)');
}
function renderTable(rows) {
const tbody = document.getElementById('mp-tbody');
if (!tbody) return;
tbody.innerHTML = '';
rows.forEach(function (p) {
const tr = document.createElement('tr');
const hls = p.hls_playlist_url
? '<a href="' + esc(p.hls_playlist_url) + '" target="_blank" rel="noopener">playlist</a>'
: '—';
const actions = [];
actions.push('<button type="button" class="btn btn--sm" data-mp-select="' + p.id + '">Select</button>');
if (canOperate) {
actions.push('<button type="button" class="btn btn--sm" data-mp-probe="' + p.id + '">Probe</button>');
actions.push('<button type="button" class="btn btn--sm" data-mp-start="' + p.id + '">Start</button>');
actions.push('<button type="button" class="btn btn--sm" data-mp-stop="' + p.id + '">Stop</button>');
actions.push('<button type="button" class="btn btn--sm" data-mp-logs="' + p.id + '">Logs</button>');
}
tr.innerHTML =
'<td>' + esc(p.name) + '</td>' +
'<td><span class="tag-pill">' + esc(p.status) + '</span></td>' +
'<td>' + esc(p.run_mode) + '</td>' +
'<td title="' + esc(p.source_url) + '">' + esc(truncate(p.source_url, 48)) + '</td>' +
'<td>' + hls + '</td>' +
'<td class="toolbar-actions">' + actions.join(' ') + '</td>';
tbody.appendChild(tr);
});
}
async function selectPipeline(id) {
selectedId = id;
const data = await fetchJson(apiUrl('get', { id: id }));
if (!data.ok || !data.pipeline) return;
const p = data.pipeline;
if (p.probe) {
document.getElementById('mp-probe-section').hidden = false;
document.getElementById('mp-probe-json').textContent = JSON.stringify(p.probe, null, 2);
}
if (p.hls_playlist_url) {
loadPlayer(p.hls_playlist_url);
}
}
async function onCreate(ev) {
ev.preventDefault();
if (!canOperate) return;
const form = ev.target;
const body = {
name: form.name.value,
source_url: form.source_url.value,
source_fmt: form.source_fmt.value,
audio_source: form.audio_source.value,
filter_profile: form.filter_profile.value,
output_preset: form.output_preset.value,
run_mode: form.run_mode.value,
};
setStatus('Creating…');
const data = await fetchJson(apiUrl('create'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!data.ok) {
setStatus(data.error || 'create failed', true);
return;
}
form.reset();
await loadList();
if (data.pipeline && data.pipeline.id) {
await selectPipeline(data.pipeline.id);
}
}
async function postAction(action, id) {
setStatus(action + '…');
const data = await fetchJson(apiUrl(action, { id: id }), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: id }),
});
if (!data.ok) {
setStatus(data.error || action + ' failed', true);
return data;
}
await loadList();
await selectPipeline(id);
if (action === 'start' && data.hls_url) {
loadPlayer(data.hls_url);
}
setStatus(action + ' OK');
return data;
}
async function showLogs(id) {
const data = await fetchJson(apiUrl('logs', { id: id }));
const sec = document.getElementById('mp-logs-section');
const pre = document.getElementById('mp-logs');
if (!sec || !pre) return;
sec.hidden = false;
pre.textContent = (data.lines || []).join('\n') || '(no log yet)';
}
function onTableClick(ev) {
const t = ev.target;
if (!t || !t.getAttribute) return;
const id = t.getAttribute('data-mp-select') || t.getAttribute('data-mp-probe')
|| t.getAttribute('data-mp-start') || t.getAttribute('data-mp-stop') || t.getAttribute('data-mp-logs');
if (!id) return;
if (t.hasAttribute('data-mp-select')) selectPipeline(id);
else if (t.hasAttribute('data-mp-probe')) postAction('probe', id);
else if (t.hasAttribute('data-mp-start')) postAction('start', id);
else if (t.hasAttribute('data-mp-stop')) postAction('stop', id);
else if (t.hasAttribute('data-mp-logs')) showLogs(id);
}
function init() {
const app = document.getElementById('media-pipelines-app');
if (!app) return;
document.getElementById('mp-create-form')?.addEventListener('submit', onCreate);
document.getElementById('mp-tbody')?.addEventListener('click', onTableClick);
document.getElementById('mp-refresh')?.addEventListener('click', loadList);
if (!canOperate) {
document.getElementById('mp-create-section')?.setAttribute('hidden', '');
}
loadList();
pollTimer = window.setInterval(loadList, 15000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();