131 lines
4.6 KiB
PHP
131 lines
4.6 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace Blax\WebRtc\Tests;
|
||
|
|
|
||
|
|
use Blax\WebRtc\Media\Rust\BinaryManager;
|
||
|
|
use Blax\WebRtc\Media\Rust\ControlClient;
|
||
|
|
use Blax\WebRtc\Media\Rust\SidecarException;
|
||
|
|
use Blax\WebRtc\Media\Rust\SidecarSupervisor;
|
||
|
|
use Blax\WebRtc\Media\RustMediaEngine;
|
||
|
|
use PHPUnit\Framework\TestCase;
|
||
|
|
|
||
|
|
final class RustMediaEngineTest extends TestCase
|
||
|
|
{
|
||
|
|
/** @var resource */
|
||
|
|
private $sidecar;
|
||
|
|
|
||
|
|
private RustMediaEngine $engine;
|
||
|
|
|
||
|
|
protected function setUp(): void
|
||
|
|
{
|
||
|
|
[$theirs, $ours] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
|
||
|
|
$this->sidecar = $theirs;
|
||
|
|
|
||
|
|
$supervisor = new SidecarSupervisor(new BinaryManager([]), ['auto_spawn' => false]);
|
||
|
|
$this->engine = new RustMediaEngine($supervisor, ControlClient::fromStream($ours));
|
||
|
|
}
|
||
|
|
|
||
|
|
private function sidecarSays(array $line): void
|
||
|
|
{
|
||
|
|
fwrite($this->sidecar, json_encode($line)."\n");
|
||
|
|
}
|
||
|
|
|
||
|
|
private function sidecarHeard(): array
|
||
|
|
{
|
||
|
|
return json_decode((string) fgets($this->sidecar), true);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_add_peer_speaks_the_control_protocol(): void
|
||
|
|
{
|
||
|
|
$this->sidecarSays(['id' => 1, 'ok' => true, 'answer' => 'v=answer']);
|
||
|
|
|
||
|
|
$answer = $this->engine->addPeer('lobby', 'alice', 'v=offer', '/rec/alice.ogg');
|
||
|
|
|
||
|
|
$this->assertSame('v=answer', $answer);
|
||
|
|
$this->assertSame(
|
||
|
|
[
|
||
|
|
'id' => 1,
|
||
|
|
'cmd' => 'add_peer',
|
||
|
|
'room' => 'lobby',
|
||
|
|
'peer' => 'alice',
|
||
|
|
'offer' => 'v=offer',
|
||
|
|
'record' => '/rec/alice.ogg',
|
||
|
|
],
|
||
|
|
$this->sidecarHeard(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_media_engine_contract_splits_composite_peer_ids(): void
|
||
|
|
{
|
||
|
|
$this->sidecarSays(['id' => 1, 'ok' => true, 'answer' => 'v=a']);
|
||
|
|
$this->engine->offer('lobby/alice', 'v=offer');
|
||
|
|
$heard = $this->sidecarHeard();
|
||
|
|
$this->assertSame(['lobby', 'alice'], [$heard['room'], $heard['peer']]);
|
||
|
|
$this->assertArrayNotHasKey('record', $heard);
|
||
|
|
|
||
|
|
$this->sidecarSays(['id' => 2, 'ok' => true]);
|
||
|
|
$this->engine->startRecording('lobby/alice', '/rec/alice.ogg');
|
||
|
|
$heard = $this->sidecarHeard();
|
||
|
|
$this->assertSame('record_start', $heard['cmd']);
|
||
|
|
$this->assertSame('/rec/alice.ogg', $heard['path']);
|
||
|
|
|
||
|
|
$this->sidecarSays(['id' => 3, 'ok' => true]);
|
||
|
|
$this->engine->close('lobby/alice');
|
||
|
|
$this->assertSame('remove_peer', $this->sidecarHeard()['cmd']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_bare_peer_ids_fall_back_to_the_default_room(): void
|
||
|
|
{
|
||
|
|
$this->sidecarSays(['id' => 1, 'ok' => true, 'answer' => 'v=a']);
|
||
|
|
$this->engine->offer('alice', 'v=offer');
|
||
|
|
$heard = $this->sidecarHeard();
|
||
|
|
$this->assertSame(['default', 'alice'], [$heard['room'], $heard['peer']]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_mute_stats_and_events(): void
|
||
|
|
{
|
||
|
|
$this->sidecarSays(['id' => 1, 'ok' => true]);
|
||
|
|
$this->engine->mutePeer('lobby', 'alice', true);
|
||
|
|
$this->assertTrue($this->sidecarHeard()['muted']);
|
||
|
|
|
||
|
|
$this->sidecarSays([
|
||
|
|
'id' => 2,
|
||
|
|
'ok' => true,
|
||
|
|
'rooms' => [['room' => 'lobby', 'peers' => [['peer' => 'alice', 'connected' => true, 'muted' => true, 'seconds' => 4.2]]]],
|
||
|
|
]);
|
||
|
|
$stats = $this->engine->stats();
|
||
|
|
$this->assertSame('lobby', $stats[0]['room']);
|
||
|
|
|
||
|
|
fwrite($this->sidecar, json_encode(['event' => 'peer_connected', 'room' => 'lobby', 'peer' => 'bob'])."\n");
|
||
|
|
$this->sidecarSays(['id' => 3, 'ok' => true]);
|
||
|
|
$this->engine->removePeer('lobby', 'bob');
|
||
|
|
$this->assertSame('peer_connected', $this->engine->drainEvents()[0]['event']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_ice_candidates_and_connect_peers_are_deliberate_noops(): void
|
||
|
|
{
|
||
|
|
// Neither may touch the socket: str0m learns remote candidates from
|
||
|
|
// STUN, and room membership already routes media.
|
||
|
|
$this->engine->addIceCandidate('lobby/alice', ['candidate' => 'x']);
|
||
|
|
$this->engine->connectPeers('lobby/alice', 'lobby/bob');
|
||
|
|
|
||
|
|
stream_set_blocking($this->sidecar, false);
|
||
|
|
$this->assertFalse(fgets($this->sidecar), 'no bytes were written to the sidecar');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_bridge_is_not_implemented_in_the_data_plane_yet(): void
|
||
|
|
{
|
||
|
|
$this->expectException(SidecarException::class);
|
||
|
|
$this->expectExceptionMessage('OpenAiRealtimeBridge');
|
||
|
|
|
||
|
|
$this->engine->bridge('lobby/alice', ['model' => 'gpt-realtime']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function test_engine_name(): void
|
||
|
|
{
|
||
|
|
$this->assertSame('rust-sfu', $this->engine->name());
|
||
|
|
}
|
||
|
|
}
|