Files
Anton Afanasyeu 0c5c079179 SFU M0: browser lab E2E client for Janus VideoRoom.
Add lab page with screen-share publisher and subscriber viewer, join API
feed support, shared session auth, static assets route, and lab docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 22:15:42 +02:00

77 lines
2.5 KiB
PHP

<?php
declare(strict_types=1);
/**
* SFU join API — POST /api/join
*
* Client sends: { room_id, jsep: { type: "offer", sdp: "..." }, role: "publisher"|"subscriber" }
* Server relays to Janus VideoRoom, returns: { jsep: { type: "answer", sdp: "..." }, handle_id, session_id }
*
* Client subsequently maintains a WebSocket to Janus directly (proxied via nginx /sfu/ws).
*/
require_once dirname(__DIR__, 2) . '/src/bootstrap.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sfuJsonErr('method not allowed', 405);
}
$user = sfuRequireAuth();
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$janusRoomId = (int) ($input['room_id'] ?? 0);
$jsep = $input['jsep'] ?? null;
$role = in_array($input['role'] ?? '', ['publisher', 'subscriber'], true)
? $input['role'] : 'publisher';
$feedId = (int) ($input['feed_id'] ?? 0);
if (!$janusRoomId) { sfuJsonErr('room_id required'); }
if (!$jsep || empty($jsep['sdp'])) { sfuJsonErr('jsep.sdp required'); }
if ($role === 'subscriber' && $feedId <= 0) { sfuJsonErr('feed_id required for subscriber'); }
$repo = new \AndroidCast\Sfu\SfuRoomRepository(sfuPdo());
$janus = janusClient();
$room = $repo->getByJanusId($janusRoomId);
if (!$room) { sfuJsonErr('room not found', 404); }
/* Create a fresh Janus session + handle for this participant */
$sid = $janus->createSession();
if (!$sid) { sfuJsonErr('Janus session failed'); }
$hid = $janus->attachVideoRoom($sid);
if (!$hid) { sfuJsonErr('Janus attach failed'); }
/* Send join + offer to Janus */
$joinBody = [
'request' => 'join',
'room' => $janusRoomId,
'ptype' => $role === 'publisher' ? 'publisher' : 'subscriber',
'display' => $user['email'] ?? 'anon',
];
if ($role === 'subscriber') {
$joinBody['streams'] = [['feed' => $feedId]];
}
$resp = $janus->sendJsep($sid, $hid, $joinBody, [
'type' => $jsep['type'],
'sdp' => $jsep['sdp'],
]);
/* Record participant in DB */
$participantId = $repo->joinRoom(
(int) $room['id'],
(int) $user['id'],
$hid,
$role
);
/* Return negotiated JSEP answer + WS connection details */
$answer = $resp['jsep'] ?? null;
$plugin = $resp['plugindata']['data'] ?? [];
$publisherId = isset($plugin['id']) ? (int) $plugin['id'] : null;
sfuJsonOk([
'session_id' => $sid,
'handle_id' => $hid,
'participant_id' => $participantId,
'publisher_id' => $publisherId,
'jsep' => $answer,
'ws_url' => '/app/androidcast_project/sfu/ws',
]);