chore: 화상회의 네이티브 lk 시각/부하 테스트 러너 추가
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 를 넣는다.
|
||||
생략 시 임의 이름으로 들어가지만, 그 경우 브라우저는 해당 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> 의 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
|
||||
Reference in New Issue
Block a user