Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e269164
video-chat: prevent duplicate websocket connects
createreadupdate May 1, 2026
f3ffca4
video-chat: bound websocket negotiation timeout
createreadupdate May 1, 2026
7212060
video-chat: extend websocket negotiation timeout
createreadupdate May 1, 2026
0a823e9
video-chat: close websocket edge tunnels symmetrically
createreadupdate May 1, 2026
8a39969
video-chat: tighten sfu publisher backpressure
createreadupdate May 1, 2026
5b4bab5
king: gate http1 reuseport for trusted workers
createreadupdate May 1, 2026
785845f
video-chat: normalize sfu asset probe failover
createreadupdate May 1, 2026
b27314e
video-chat: flush websocket edge rejections
createreadupdate May 1, 2026
fde584f
video-chat: route external edge domains
createreadupdate May 2, 2026
70eb4eb
video-chat: add social invite previews
createreadupdate May 2, 2026
23210d1
Update Buy Me a Coffee link format
debuggerone May 3, 2026
5098f5e
branch linked to security and quality alerts
May 3, 2026
6559f03
clean up code, follow CodeQL guidance
May 3, 2026
068bc70
clean up code, follow CodeQL guidance
May 4, 2026
bf98cd9
Fix late-bound call workspace callbacks
May 4, 2026
049d88c
Apply suggested fix to extension/tests/740-http1-listener-exclusive-b…
debuggerone May 4, 2026
a036eb6
Apply suggested fix to extension/tests/740-http1-listener-exclusive-b…
debuggerone May 4, 2026
bacc966
Apply suggested fix to extension/tests/740-http1-listener-exclusive-b…
debuggerone May 4, 2026
a7951bb
Apply suggested fix to demo/video-chat/frontend-vue/src/domain/realti…
debuggerone May 4, 2026
8fe5009
Apply suggested fix to demo/video-chat/frontend-vue/src/domain/realti…
debuggerone May 4, 2026
e6f9e06
Apply suggested fix to demo/video-chat/frontend-vue/src/domain/realti…
debuggerone May 4, 2026
2cc00bc
Apply suggested fix to demo/video-chat/frontend-vue/src/domain/realti…
debuggerone May 4, 2026
76071ea
Apply suggested fix to demo/video-chat/frontend-vue/src/domain/realti…
debuggerone May 4, 2026
4d87271
Apply suggested fix to extension/tests/server_websocket_wire_helper.i…
debuggerone May 4, 2026
ed59000
Apply suggested fix to extension/tests/server_websocket_wire_helper.i…
debuggerone May 4, 2026
abed970
Apply suggested fix to extension/tests/server_websocket_wire_helper.i…
debuggerone May 4, 2026
2bf676d
Apply suggested fix to extension/tests/server_websocket_wire_helper.i…
debuggerone May 4, 2026
998968c
Apply suggested fix to extension/tests/740-http1-listener-exclusive-b…
debuggerone May 4, 2026
66dec5f
Apply suggested fix to extension/tests/740-http1-listener-exclusive-b…
debuggerone May 4, 2026
bfb4e1f
Apply suggested fix to extension/tests/740-http1-listener-exclusive-b…
debuggerone May 4, 2026
7a69373
Apply suggested fix to demo/video-chat/frontend-vue/src/lib/wasm/cpp/…
debuggerone May 4, 2026
b5e4f01
Apply suggested fix to extension/tests/server_websocket_wire_helper.i…
debuggerone May 4, 2026
4e4d149
Apply suggested fix to extension/tests/server_websocket_wire_helper.i…
debuggerone May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
buy_me_a_coffee: https://buymeacoffee.com/kingrt
buy_me_a_coffee: kingrt
8 changes: 8 additions & 0 deletions demo/video-chat/backend-king-php/run-dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,14 @@ start_backend() {
local bind_port="$2"
local worker_count
worker_count="$(worker_count_for_mode "${mode}")"
local reuseport_value="${KING_HTTP1_ENABLE_REUSEPORT:-}"
if [[ -z "${reuseport_value}" && "${worker_count}" -gt 1 ]]; then
reuseport_value="1"
fi

local worker_index=1
while (( worker_index <= worker_count )); do
KING_HTTP1_ENABLE_REUSEPORT="${reuseport_value}" \
VIDEOCHAT_KING_PORT="${bind_port}" \
VIDEOCHAT_KING_SERVER_MODE="${mode}" \
VIDEOCHAT_KING_WORKER_INDEX="${worker_index}" \
Expand All @@ -131,6 +136,9 @@ start_backend() {
done

echo "[video-chat][king-php-backend] started ${worker_count} worker(s) in ${mode} mode on port ${bind_port}"
if [[ "${reuseport_value}" == "1" && "${worker_count}" -gt 1 ]]; then
echo "[video-chat][king-php-backend] enabled SO_REUSEPORT for trusted ${mode} worker group"
fi
}

normalized_mode_override="$(echo "${SERVER_MODE_OVERRIDE}" | tr '[:upper:]' '[:lower:]' | xargs || true)"
Expand Down
6 changes: 4 additions & 2 deletions demo/video-chat/docker-compose.v1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ services:
VIDEOCHAT_V1_TURN_STATIC_AUTH_SECRET_FILE: "${VIDEOCHAT_V1_TURN_STATIC_AUTH_SECRET_FILE:-}"
VIDEOCHAT_V1_TURN_EXTERNAL_IP: "${VIDEOCHAT_V1_TURN_EXTERNAL_IP:-}"
VIDEOCHAT_V1_TURN_RELAY_MIN_PORT: "${VIDEOCHAT_V1_TURN_RELAY_MIN_PORT:-49160}"
VIDEOCHAT_V1_TURN_RELAY_MAX_PORT: "${VIDEOCHAT_V1_TURN_RELAY_MAX_PORT:-49200}"
VIDEOCHAT_V1_TURN_RELAY_MAX_PORT: "${VIDEOCHAT_V1_TURN_RELAY_MAX_PORT:-49660}"
entrypoint:
- sh
- -lc
Expand Down Expand Up @@ -161,7 +161,7 @@ services:
ports:
- "${VIDEOCHAT_V1_TURN_PORT:-3478}:3478/tcp"
- "${VIDEOCHAT_V1_TURN_PORT:-3478}:3478/udp"
- "${VIDEOCHAT_V1_TURN_RELAY_MIN_PORT:-49160}-${VIDEOCHAT_V1_TURN_RELAY_MAX_PORT:-49200}:${VIDEOCHAT_V1_TURN_RELAY_MIN_PORT:-49160}-${VIDEOCHAT_V1_TURN_RELAY_MAX_PORT:-49200}/udp"
- "${VIDEOCHAT_V1_TURN_RELAY_MIN_PORT:-49160}-${VIDEOCHAT_V1_TURN_RELAY_MAX_PORT:-49660}:${VIDEOCHAT_V1_TURN_RELAY_MIN_PORT:-49160}-${VIDEOCHAT_V1_TURN_RELAY_MAX_PORT:-49660}/udp"
restart: unless-stopped

videochat-frontend-v1:
Expand Down Expand Up @@ -254,6 +254,8 @@ services:
VIDEOCHAT_EDGE_TURN_DOMAIN: "${VIDEOCHAT_DEPLOY_TURN_DOMAIN:-turn.${VIDEOCHAT_V1_PUBLIC_HOST:-127.0.0.1}}"
VIDEOCHAT_EDGE_CDN_DOMAIN: "${VIDEOCHAT_DEPLOY_CDN_DOMAIN:-cdn.${VIDEOCHAT_V1_PUBLIC_HOST:-127.0.0.1}}"
VIDEOCHAT_EDGE_CDN_ALIASES: "${VIDEOCHAT_DEPLOY_CDN_ALIASES:-cnd.${VIDEOCHAT_V1_PUBLIC_HOST:-127.0.0.1}}"
VIDEOCHAT_EDGE_EXTERNAL_DOMAINS: "${VIDEOCHAT_DEPLOY_EXTERNAL_DOMAINS:-}"
VIDEOCHAT_EDGE_EXTERNAL_UPSTREAM: "${VIDEOCHAT_DEPLOY_EXTERNAL_UPSTREAM:-}"
VIDEOCHAT_EDGE_CERT_FILE: /run/certs/live/fullchain.pem
VIDEOCHAT_EDGE_KEY_FILE: /run/certs/live/privkey.pem
VIDEOCHAT_EDGE_API_UPSTREAM: videochat-backend-v1:18080
Expand Down
137 changes: 135 additions & 2 deletions demo/video-chat/edge/edge.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@
$turnDomain = strtolower(trim((string) (getenv('VIDEOCHAT_EDGE_TURN_DOMAIN') ?: 'turn.' . $domain)));
$cdnDomain = strtolower(trim((string) (getenv('VIDEOCHAT_EDGE_CDN_DOMAIN') ?: 'cdn.' . $domain)));
$cdnAliasInput = trim((string) (getenv('VIDEOCHAT_EDGE_CDN_ALIASES') ?: 'cnd.' . $domain));
$externalDomainInput = trim((string) getenv('VIDEOCHAT_EDGE_EXTERNAL_DOMAINS'));
$externalDomains = [];
foreach (preg_split('/\s*,\s*/', $externalDomainInput) ?: [] as $externalDomain) {
$externalDomain = strtolower(trim((string) $externalDomain));
if ($externalDomain !== '') {
$externalDomains[] = $externalDomain;
}
}
$externalDomains = array_values(array_unique($externalDomains));
$cdnDomains = [$cdnDomain];
foreach (preg_split('/\s*,\s*/', $cdnAliasInput) ?: [] as $alias) {
$alias = strtolower(trim((string) $alias));
Expand All @@ -26,6 +35,8 @@
$apiUpstream = getenv('VIDEOCHAT_EDGE_API_UPSTREAM') ?: 'videochat-backend-v1:18080';
$wsUpstream = getenv('VIDEOCHAT_EDGE_WS_UPSTREAM') ?: 'videochat-backend-ws-v1:18080';
$sfuUpstream = getenv('VIDEOCHAT_EDGE_SFU_UPSTREAM') ?: 'videochat-backend-sfu-v1:18080';
$externalUpstream = trim((string) getenv('VIDEOCHAT_EDGE_EXTERNAL_UPSTREAM'));
$socialPreviewImagePath = getenv('VIDEOCHAT_EDGE_SOCIAL_PREVIEW_IMAGE') ?: '/assets/orgas/kingrt/social/invitation-preview.png';
$maxHeaderBytes = (int) (getenv('VIDEOCHAT_EDGE_MAX_HEADER_BYTES') ?: '65536');
$connectTimeout = (float) (getenv('VIDEOCHAT_EDGE_CONNECT_TIMEOUT_SECONDS') ?: '5');
$httpIdleTimeout = (int) (getenv('VIDEOCHAT_EDGE_HTTP_IDLE_TIMEOUT_SECONDS') ?: '60');
Expand Down Expand Up @@ -286,7 +297,61 @@
};
};

$serveStatic = static function ($client, array $request) use ($staticRoot, $writeResponse, $contentType, $cdnDomains, $assetVersion): void {
$escapeHtml = static function (string $value): string {
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
};

$absoluteHttpsUrl = static function (string $host, string $target) use ($domain): string {
$host = trim($host) !== '' ? $host : $domain;
$target = trim($target) !== '' ? $target : '/';
if ($target[0] !== '/') {
$target = '/' . $target;
}
return 'https://' . $host . $target;
};

$injectSocialPreview = static function (string $body, array $request) use ($domain, $cdnDomain, $socialPreviewImagePath, $escapeHtml, $absoluteHttpsUrl): string {
$target = (string) ($request['target'] ?: ($request['path'] ?? '/'));
$path = (string) ($request['path'] ?? '/');
$host = (string) ($request['host'] ?: $domain);
$assetHost = $cdnDomain !== '' ? $cdnDomain : $host;
$pageUrl = $absoluteHttpsUrl($host, $target);
$imageUrl = $absoluteHttpsUrl($assetHost, $socialPreviewImagePath);
$isInvite = str_starts_with($path, '/join/');
$title = $isInvite ? "You're invited to a KINGRT video call" : 'KINGRT Video Chat';
$description = $isInvite
? 'Join the video call on KINGRT.'
: 'Run your own calls with KINGRT open-source video collaboration.';

$tags = [
'<meta name="description" content="' . $escapeHtml($description) . '" />',
'<link rel="canonical" href="' . $escapeHtml($pageUrl) . '" />',
'<meta property="og:type" content="website" />',
'<meta property="og:site_name" content="KINGRT" />',
'<meta property="og:title" content="' . $escapeHtml($title) . '" />',
'<meta property="og:description" content="' . $escapeHtml($description) . '" />',
'<meta property="og:url" content="' . $escapeHtml($pageUrl) . '" />',
'<meta property="og:image" content="' . $escapeHtml($imageUrl) . '" />',
'<meta property="og:image:secure_url" content="' . $escapeHtml($imageUrl) . '" />',
'<meta property="og:image:type" content="image/png" />',
'<meta property="og:image:width" content="1076" />',
'<meta property="og:image:height" content="562" />',
'<meta property="og:image:alt" content="' . $escapeHtml($title) . '" />',
'<meta name="twitter:card" content="summary_large_image" />',
'<meta name="twitter:title" content="' . $escapeHtml($title) . '" />',
'<meta name="twitter:description" content="' . $escapeHtml($description) . '" />',
'<meta name="twitter:image" content="' . $escapeHtml($imageUrl) . '" />',
];
$meta = implode("\n ", $tags);

if (str_contains($body, '<!-- kingrt-social-preview -->')) {
return str_replace('<!-- kingrt-social-preview -->', $meta, $body);
}

return str_replace('</head>', " {$meta}\n </head>", $body);
};

$serveStatic = static function ($client, array $request) use ($staticRoot, $writeResponse, $contentType, $cdnDomains, $assetVersion, $injectSocialPreview): void {
$path = rawurldecode((string) $request['path']);
$isCdnAsset = in_array($request['host'], $cdnDomains, true) || str_starts_with($path, '/cdn/');
$corsHeaders = $isCdnAsset
Expand Down Expand Up @@ -324,6 +389,9 @@
}

$body = (string) @file_get_contents($candidate);
if (basename($candidate) === 'index.html') {
$body = $injectSocialPreview($body, $request);
}
$headers = [
'Content-Type' => $contentType($candidate),
'Cache-Control' => basename($candidate) === 'index.html'
Expand Down Expand Up @@ -395,11 +463,27 @@
$lastUpstreamReadProgress = $lastActivity;
$lastClientWriteProgress = $lastActivity;
$lastUpstreamWriteProgress = $lastActivity;
$closeWebSocketTunnel = static function () use (&$clientOpen, &$upstreamOpen, &$toClient, &$toUpstream): void {
// WebSocket tunnels cannot stay half-open: otherwise browser requests
// remain pending while the closed upstream socket sits in CLOSE_WAIT.
$clientOpen = false;
$upstreamOpen = false;
$toClient = '';
$toUpstream = '';
};

while ($clientOpen || $upstreamOpen || $toUpstream !== '' || $toClient !== '') {
if ((microtime(true) - $lastActivity) > $idleTimeout) {
break;
}
// Upstream may reject a websocket handshake with HTTP bytes, then close.
// Keep the client side alive until that buffered response is flushed.
if ($isWebSocket && !$upstreamOpen && $toClient === '') {
$clientOpen = false;
}
if ($isWebSocket && !$clientOpen && $toUpstream === '') {
$upstreamOpen = false;
}
if (!$clientOpen) {
$toClient = '';
}
Expand Down Expand Up @@ -447,6 +531,21 @@
foreach ($read as $stream) {
$chunk = @fread($stream, 16384);
if ($chunk === false) {
if ($isWebSocket) {
if ($stream === $upstreamStream && $toClient !== '') {
$upstreamOpen = false;
$madeProgress = true;
continue;
}
if ($stream === $client && $toUpstream !== '') {
$clientOpen = false;
$madeProgress = true;
continue;
}
$closeWebSocketTunnel();
$madeProgress = true;
continue;
}
if ($stream === $client) {
$clientOpen = false;
} else {
Expand All @@ -456,6 +555,21 @@
}
if ($chunk === '') {
if (feof($stream)) {
if ($isWebSocket) {
if ($stream === $upstreamStream && $toClient !== '') {
$upstreamOpen = false;
$madeProgress = true;
continue;
}
if ($stream === $client && $toUpstream !== '') {
$clientOpen = false;
$madeProgress = true;
continue;
}
$closeWebSocketTunnel();
$madeProgress = true;
continue;
}
if ($stream === $client) {
$clientOpen = false;
} else {
Expand Down Expand Up @@ -510,12 +624,20 @@
if ($stream === $upstreamStream && $toUpstream !== '') {
$written = @fwrite($upstreamStream, $toUpstream);
if ($written === false) {
if ($isWebSocket) {
$closeWebSocketTunnel();
continue;
}
$upstreamOpen = false;
$toUpstream = '';
continue;
}
if ($written === 0) {
if ((microtime(true) - $lastUpstreamWriteProgress) >= $writeStallTimeout) {
if ($isWebSocket) {
$closeWebSocketTunnel();
continue;
}
$upstreamOpen = false;
$toUpstream = '';
} else {
Expand All @@ -531,12 +653,20 @@
if ($stream === $client && $toClient !== '') {
$written = @fwrite($client, $toClient);
if ($written === false) {
if ($isWebSocket) {
$closeWebSocketTunnel();
continue;
}
$clientOpen = false;
$toClient = '';
continue;
}
if ($written === 0) {
if ((microtime(true) - $lastClientWriteProgress) >= $writeStallTimeout) {
if ($isWebSocket) {
$closeWebSocketTunnel();
continue;
}
$clientOpen = false;
$toClient = '';
} else {
Expand All @@ -559,9 +689,12 @@
@fclose($upstreamStream);
};

$route = static function (array $request) use ($domain, $apiDomain, $wsDomain, $sfuDomain, $turnDomain, $cdnDomains, $apiUpstream, $wsUpstream, $sfuUpstream): ?string {
$route = static function (array $request) use ($domain, $apiDomain, $wsDomain, $sfuDomain, $turnDomain, $cdnDomains, $externalDomains, $apiUpstream, $wsUpstream, $sfuUpstream, $externalUpstream): ?string {
$host = $request['host'];
$path = $request['path'];
if ($externalUpstream !== '' && in_array($host, $externalDomains, true)) {
return $externalUpstream;
}
if (in_array($host, $cdnDomains, true)) {
return 'static';
}
Expand Down
3 changes: 2 additions & 1 deletion demo/video-chat/frontend-vue/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/assets/orgas/kingrt/icon.svg" type="image/svg+xml" />
<title>King Video Chat (Vue)</title>
<!-- kingrt-social-preview -->
<title>KINGRT Video Chat</title>
</head>
<body>
<div id="app"></div>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ import {
handleAssetVersionSocketPayload,
} from '../../../support/assetVersion';
import { attachForegroundReconnectHandlers } from '../../../support/foregroundReconnect';
import { buildOptionalCallAudioCaptureConstraints } from '../../realtime/media/audioCaptureConstraints';
import {
applyCallBackgroundPreset as applyBackgroundPreset,
attachCallMediaDeviceWatcher,
Expand Down Expand Up @@ -272,7 +273,11 @@ const {
playSpeakerTestSound,
startPreview,
stopPreview,
} = createJoinAccessPreviewController({ previewVideoRef, state });
} = createJoinAccessPreviewController({
previewVideoRef,
state,
buildOptionalCallAudioCaptureConstraints,
});

function normalizeAccessId(value) {
return String(value || '').trim().toLowerCase();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import { BackgroundFilterController } from '../../realtime/background/controller';
import { buildOptionalCallAudioCaptureConstraints } from '../../realtime/media/audioCaptureConstraints';
import { buildOptionalCallAudioCaptureConstraints as defaultBuildOptionalCallAudioCaptureConstraints } from '../../realtime/media/audioCaptureConstraints';
import { callMediaPrefs } from '../../realtime/media/preferences';

function finiteNumber(value, fallback) {
Expand Down Expand Up @@ -74,7 +74,9 @@ function applyVolumeToStreams(streams) {
}
}

function buildPreviewConstraints() {
function buildPreviewConstraints(
buildOptionalCallAudioCaptureConstraints = defaultBuildOptionalCallAudioCaptureConstraints,
) {
const cameraDeviceId = String(callMediaPrefs.selectedCameraId || '').trim();
const microphoneDeviceId = String(callMediaPrefs.selectedMicrophoneId || '').trim();
return {
Expand All @@ -94,7 +96,11 @@ function stopStreams(streams) {
}
}

export function createJoinAccessPreviewController({ previewVideoRef, state }) {
export function createJoinAccessPreviewController({
previewVideoRef,
state,
buildOptionalCallAudioCaptureConstraints = defaultBuildOptionalCallAudioCaptureConstraints,
}) {
const backgroundController = new BackgroundFilterController();
let rawStream = null;
let previewStream = null;
Expand Down Expand Up @@ -133,7 +139,7 @@ export function createJoinAccessPreviewController({ previewVideoRef, state }) {
}

try {
rawStream = await navigator.mediaDevices.getUserMedia(buildPreviewConstraints());
rawStream = await navigator.mediaDevices.getUserMedia(buildPreviewConstraints(buildOptionalCallAudioCaptureConstraints));
applyVolumeToStreams([rawStream]);

previewStream = rawStream;
Expand Down
Loading
Loading