From 9ce31584503b2790beed5b374f2b45628e3397bd Mon Sep 17 00:00:00 2001 From: ttipo Date: Tue, 30 Jun 2026 18:20:33 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20=ED=99=94=EC=83=81=ED=9A=8C=EC=9D=98?= =?UTF-8?q?=20=EB=84=A4=EC=9D=B4=ED=8B=B0=EB=B8=8C=20lk=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=81/=EB=B6=80=ED=95=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=9F=AC=EB=84=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- livekit/visual-test.ps1 | 138 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 livekit/visual-test.ps1 diff --git a/livekit/visual-test.ps1 b/livekit/visual-test.ps1 new file mode 100644 index 0000000..ba3f66f --- /dev/null +++ b/livekit/visual-test.ps1 @@ -0,0 +1,138 @@ +<# +.SYNOPSIS + 화상회의 "화면 띄우기" 시각 테스트 러너 (Windows 네이티브 lk) + +.DESCRIPTION + livekit-cli(lk)로 가상 video publisher 를 대량 접속시켜, 운영(또는 지정) LiveKit + 서버의 특정 방(code)에 합성 영상을 발행한다. 사용자가 같은 방에 브라우저로 입장하면 + 그 가상 참가자들이 실제 영상 타일로 화면에 표시된다(=50명 화면 띄우기 검증). + + ⚠️ 왜 Docker(load-test.sh) 가 아니라 네이티브 lk 인가: + Docker Desktop(Windows)은 컨테이너 UDP/ICE NAT 한계로 WebRTC 미디어 연결이 + 타임아웃된다. 브라우저와 동일한 네트워크 경로를 쓰려면 호스트 네이티브 lk 가 필요하다. + 설치: winget install --id LiveKit.LiveKitCLI + + 자격증명/URL 은 루트 .env.{APP_ENV} 에서 읽는다(기본 .env.production). + - LIVEKIT_API_KEY / LIVEKIT_API_SECRET : lk 가 방 토큰 서명에 사용(브라우저 백엔드와 동일 키) + - LIVEKIT_WS_URL : 브라우저가 접속하는 공개 wss URL (백엔드용 LIVEKIT_URL=내부주소 가 아님!) + (.env 의 "==" 오타나 CRLF 는 자동 보정한다) + +.PARAMETER Room + 대상 방의 입장 code. 앱(relay)에서 방을 먼저 생성한 뒤 /meeting/ 의 code 를 넣는다. + 생략 시 임의 이름으로 들어가지만, 그 경우 브라우저는 해당 code 의 DB 방이 없어 입장 불가하다. + +.EXAMPLE + # 운영 서버, 특정 방에 50명 저해상도 10분 + .\livekit\visual-test.ps1 -Room a1b2c3d4e5 -Publishers 50 -Duration 10m + +.EXAMPLE + # 화질을 올리되 업로드 대역폭 절약 위해 simulcast 끄기 + .\livekit\visual-test.ps1 -Room a1b2c3d4e5 -Publishers 30 -Resolution medium -NoSimulcast +#> +[CmdletBinding()] +param( + [string] $Room, + [int] $Publishers = 50, + [int] $Subscribers = 0, + [string] $Duration = '10m', + [ValidateSet('low', 'medium', 'high')] [string] $Resolution = 'low', + [int] $NumPerSecond = 5, + [switch] $NoSimulcast, + [switch] $WithAudio, # 각 가상 유저가 마이크(오디오)도 발행 (영상 인원만큼) + [int] $AudioPublishers = 0, # 오디오 발행 인원 직접 지정(고급). -WithAudio 보다 우선 + [string] $IdentityPrefix = 'load', + [string] $EnvFile +) + +$ErrorActionPreference = 'Stop' + +# 인원/방 코드를 인자로 안 넘기면 대화형으로 입력받는다(매번 플래그 외울 필요 없음). +if (-not $PSBoundParameters.ContainsKey('Room') -or [string]::IsNullOrWhiteSpace($Room)) { + $Room = (Read-Host '입장할 방 코드(code) 입력 (앱 /meeting/ 의 code)').Trim() +} +if ([string]::IsNullOrWhiteSpace($Room)) { throw '방 코드(code)가 필요합니다.' } + +$promptedCount = $false +if (-not $PSBoundParameters.ContainsKey('Publishers')) { + $promptedCount = $true + $ans = (Read-Host "입장시킬 가상 인원 수 [기본 $Publishers]").Trim() + if ($ans) { + if ($ans -as [int]) { $Publishers = [int]$ans } + else { throw "인원 수는 숫자여야 합니다: $ans" } + } +} +if ($Publishers -lt 1) { throw "인원 수는 1명 이상이어야 합니다: $Publishers" } + +# 마이크(오디오) 처리. +# - -AudioPublishers N : 그 값 그대로 사용(고급). +# - -WithAudio : 영상 인원만큼 오디오도 발행(각 가상 유저가 캠+마이크). +# - 둘 다 없고 대화형이면 켤지 물어본다(스크립트 인자모드에서는 안 물어봐 멈춤 없음). +if (-not $PSBoundParameters.ContainsKey('AudioPublishers')) { + if ($WithAudio) { + $AudioPublishers = $Publishers + } + elseif ($promptedCount) { + $a = (Read-Host '마이크(오디오)도 켤까요? 각 가상 유저가 마이크 발행 [y/N]').Trim() + if ($a -match '^(y|yes)$') { $AudioPublishers = $Publishers } + } +} +if ($AudioPublishers -lt 0) { $AudioPublishers = 0 } + +# 루트 경로 / env 파일 결정 +$root = Split-Path -Parent $PSScriptRoot +if (-not $EnvFile) { + $appEnv = if ($env:APP_ENV) { $env:APP_ENV } else { 'production' } + $EnvFile = Join-Path $root ".env.$appEnv" +} +if (-not (Test-Path $EnvFile)) { throw "env 파일을 찾을 수 없습니다: $EnvFile" } + +# .env 에서 필요한 값만 읽기(주석/CRLF 무시, 마지막 정의 우선) +function Read-EnvValue([string]$key) { + $line = Get-Content $EnvFile | Where-Object { $_ -match "^\s*$([regex]::Escape($key))=" } | Select-Object -Last 1 + if (-not $line) { return $null } + $val = ($line -split '=', 2)[1] + return ($val -replace "`r", '').Trim() +} + +$apiKey = Read-EnvValue 'LIVEKIT_API_KEY' +$apiSecret = Read-EnvValue 'LIVEKIT_API_SECRET' +# 브라우저 접속용 공개 WS URL. .env 의 "==" 오타(앞 등호) 자동 보정. +$wsUrl = Read-EnvValue 'LIVEKIT_WS_URL' +if ($wsUrl) { $wsUrl = $wsUrl.TrimStart('=').Trim() } + +if (-not $apiKey -or -not $apiSecret -or -not $wsUrl) { + throw "LIVEKIT_API_KEY / LIVEKIT_API_SECRET / LIVEKIT_WS_URL 를 $EnvFile 에서 찾을 수 없습니다." +} + +# lk 실행 파일 경로 해석 (PATH 우선, 없으면 winget Links 폴백) +$lk = (Get-Command lk -ErrorAction SilentlyContinue).Source +if (-not $lk) { $lk = Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Links\lk.exe' } +if (-not (Test-Path $lk)) { + throw "lk(livekit-cli) 를 찾을 수 없습니다. 설치: winget install --id LiveKit.LiveKitCLI" +} + +# 자격증명은 환경변수로만 전달(명령줄 노출 금지) +$env:LIVEKIT_URL = $wsUrl +$env:LIVEKIT_API_KEY = $apiKey +$env:LIVEKIT_API_SECRET = $apiSecret + +Write-Host "▶ LiveKit 시각 테스트 (네이티브 lk)" -ForegroundColor Cyan +Write-Host " URL=$wsUrl ROOM=$Room" +Write-Host " publishers=$Publishers audio=$AudioPublishers subscribers=$Subscribers duration=$Duration resolution=$Resolution simulcast=$([bool](-not $NoSimulcast))" +Write-Host " 브라우저에서 같은 방(code=$Room)에 입장하면 가상 참가자 영상이 타일로 보입니다. 중단: Ctrl+C" -ForegroundColor DarkGray +Write-Host "" + +$lkArgs = @( + 'load-test', + '--room', $Room, + '--duration', $Duration, + '--video-publishers', $Publishers, + '--audio-publishers', $AudioPublishers, + '--subscribers', $Subscribers, + '--video-resolution', $Resolution, + '--num-per-second', $NumPerSecond, + '--identity-prefix', $IdentityPrefix +) +if ($NoSimulcast) { $lkArgs += '--no-simulcast' } + +& $lk @lkArgs