This commit is contained in:
Zeeshaun
2026-04-23 02:23:23 +00:00
parent 45354397f1
commit a7afe2285b
8 changed files with 443 additions and 42 deletions
@@ -128,7 +128,7 @@ public sealed class Cs2ScriptedRenderBackend : IClipRenderBackend
return _options.RenderWorker.Cs2.Arguments;
}
return "-ExecutionPolicy Bypass -File \"{wrapperScriptPath}\" -DemoPath \"{demoPath}\" -OutputPath \"{outputPath}\" -StartTick {startTick} -EndTick {endTick} -CameraMode \"{cameraMode}\" -Resolution \"{resolution}\" -Fps {fps} -ArtifactsDirectory \"{artifactsDirectory}\"";
return "-ExecutionPolicy Bypass -File \"{wrapperScriptPath}\" -DemoPath \"{demoPath}\" -OutputPath \"{outputPath}\" -StartTick {startTick} -EndTick {endTick} -TickRate {tickRate} -CameraMode \"{cameraMode}\" -Resolution \"{resolution}\" -Fps {fps} -ArtifactsDirectory \"{artifactsDirectory}\"";
}
private string ResolveWorkingDirectory()
@@ -171,6 +171,7 @@ public sealed class Cs2ScriptedRenderBackend : IClipRenderBackend
.Replace("{outputPath}", outputPath, StringComparison.Ordinal)
.Replace("{startTick}", renderJob.StartTick.ToString(), StringComparison.Ordinal)
.Replace("{endTick}", renderJob.EndTick.ToString(), StringComparison.Ordinal)
.Replace("{tickRate}", renderJob.TickRate.ToString(), StringComparison.Ordinal)
.Replace("{cameraMode}", renderJob.CameraMode, StringComparison.Ordinal)
.Replace("{resolution}", renderJob.Resolution, StringComparison.Ordinal)
.Replace("{fps}", renderJob.Fps, StringComparison.Ordinal)
@@ -12,7 +12,7 @@
"RetryDelaySeconds": 30,
"Cs2": {
"ExecutablePath": "powershell.exe",
"Arguments": "-ExecutionPolicy Bypass -File \"{wrapperScriptPath}\" -DemoPath \"{demoPath}\" -OutputPath \"{outputPath}\" -StartTick {startTick} -EndTick {endTick} -CameraMode \"{cameraMode}\" -Resolution \"{resolution}\" -Fps {fps} -ArtifactsDirectory \"{artifactsDirectory}\"",
"Arguments": "-ExecutionPolicy Bypass -File \"{wrapperScriptPath}\" -DemoPath \"{demoPath}\" -OutputPath \"{outputPath}\" -StartTick {startTick} -EndTick {endTick} -TickRate {tickRate} -CameraMode \"{cameraMode}\" -Resolution \"{resolution}\" -Fps {fps} -ArtifactsDirectory \"{artifactsDirectory}\"",
"WorkingDirectory": "",
"TimeoutSeconds": 900,
"ArtifactsDirectory": "data/render-artifacts",
@@ -11,6 +11,9 @@ param(
[Parameter(Mandatory = $true)]
[int]$EndTick,
[Parameter(Mandatory = $false)]
[int]$TickRate = 64,
[Parameter(Mandatory = $true)]
[string]$CameraMode,
@@ -34,67 +37,380 @@ param(
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
function Resolve-OptionalPath {
param([string]$PathValue)
if ([string]::IsNullOrWhiteSpace($PathValue)) {
return ""
}
return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($PathValue)
}
function Write-Log {
param(
[string]$Message,
[string]$LogPath
)
$timestamp = [DateTime]::UtcNow.ToString("O")
Add-Content -LiteralPath $LogPath -Value "[$timestamp] $Message"
}
function Invoke-ExternalCommand {
param(
[string]$FilePath,
[string]$Arguments,
[string]$WorkingDirectory,
[string]$LogPath,
[switch]$Optional
)
if ([string]::IsNullOrWhiteSpace($FilePath)) {
if ($Optional) {
Write-Log "Skipping optional command because no executable was provided." $LogPath
return
}
throw "Command executable path was not provided."
}
$workingDirectoryToUse = if ([string]::IsNullOrWhiteSpace($WorkingDirectory)) {
Split-Path -Parent $FilePath
}
else {
$WorkingDirectory
}
$stdoutPath = [System.IO.Path]::GetTempFileName()
$stderrPath = [System.IO.Path]::GetTempFileName()
try {
Write-Log "Running: $FilePath $Arguments" $LogPath
$process = Start-Process `
-FilePath $FilePath `
-ArgumentList $Arguments `
-WorkingDirectory $workingDirectoryToUse `
-RedirectStandardOutput $stdoutPath `
-RedirectStandardError $stderrPath `
-PassThru `
-Wait
foreach ($line in Get-Content -LiteralPath $stdoutPath -ErrorAction SilentlyContinue) {
Write-Log "$line" $LogPath
}
foreach ($line in Get-Content -LiteralPath $stderrPath -ErrorAction SilentlyContinue) {
Write-Log "$line" $LogPath
}
if ($process.ExitCode -ne 0 -and -not $Optional) {
throw "Command failed with exit code $($process.ExitCode): $FilePath $Arguments"
}
}
finally {
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
}
}
function Invoke-TemplateCommand {
param(
[string]$Template,
[hashtable]$Context,
[string]$LogPath,
[switch]$Optional
)
if ([string]::IsNullOrWhiteSpace($Template)) {
if ($Optional) {
Write-Log "Skipping optional templated command because no template was configured." $LogPath
return
}
throw "Required templated command was not configured."
}
$expanded = $Template
foreach ($key in $Context.Keys) {
$expanded = $expanded.Replace("{$key}", [string]$Context[$key], [System.StringComparison]::Ordinal)
}
Write-Log "Executing templated PowerShell command: $expanded" $LogPath
Invoke-Expression $expanded 2>&1 | ForEach-Object {
Write-Log "$_" $LogPath
}
if ($LASTEXITCODE -ne 0 -and -not $Optional) {
throw "Templated command failed with exit code $LASTEXITCODE."
}
}
function Parse-Resolution {
param([string]$ResolutionValue)
if ($ResolutionValue -notmatch '^(?<width>\d+)x(?<height>\d+)$') {
throw "Resolution must use WIDTHxHEIGHT format, got '$ResolutionValue'."
}
return @{
Width = [int]$Matches.width
Height = [int]$Matches.height
}
}
function Get-EnvDouble {
param(
[string]$Name,
[double]$Fallback
)
if ([string]::IsNullOrWhiteSpace((Get-Item "env:$Name" -ErrorAction SilentlyContinue).Value)) {
return $Fallback
}
$value = (Get-Item "env:$Name").Value
$parsed = 0.0
if ([double]::TryParse($value, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$parsed)) {
return $parsed
}
return $Fallback
}
function Write-CfgFile {
param(
[string]$CfgPath,
[string]$DemoAbsolutePath,
[int]$StartTickValue,
[int]$PrerollTicks,
[string]$SafeCameraMode
)
$demoForCfg = $DemoAbsolutePath.Replace('\', '/')
$gotoTick = [Math]::Max(0, $StartTickValue - $PrerollTicks)
@(
"echo ""[Fragalytics] render bootstrap started"""
"sv_cheats 1"
"mirv_deathmsg lifetime 0"
"spec_show_xray 0"
"demo_pause"
"playdemo ""$demoForCfg"""
"demo_goto $gotoTick"
"spec_mode 4"
"echo ""[Fragalytics] camera mode requested: $SafeCameraMode"""
"demo_resume"
) | Set-Content -LiteralPath $CfgPath -Encoding ASCII
}
if (-not (Test-Path -LiteralPath $DemoPath)) {
throw "Demo file was not found: $DemoPath"
}
$outputDirectory = Split-Path -Parent $OutputPath
if ($EndTick -le $StartTick) {
throw "EndTick must be greater than StartTick."
}
if ($TickRate -le 0) {
throw "TickRate must be greater than zero."
}
if ($Fps -le 0) {
throw "Fps must be greater than zero."
}
$resolutionParts = Parse-Resolution $Resolution.ToLowerInvariant()
$demoAbsolutePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DemoPath)
$outputAbsolutePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
$outputDirectory = Split-Path -Parent $outputAbsolutePath
if (-not [string]::IsNullOrWhiteSpace($outputDirectory)) {
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
}
if (-not [string]::IsNullOrWhiteSpace($ArtifactsDirectory)) {
New-Item -ItemType Directory -Force -Path $ArtifactsDirectory | Out-Null
}
$manifestPath = if ([string]::IsNullOrWhiteSpace($ArtifactsDirectory)) {
[System.IO.Path]::ChangeExtension($OutputPath, ".render.json")
$artifactsAbsolutePath = if ([string]::IsNullOrWhiteSpace($ArtifactsDirectory)) {
Split-Path -Parent $outputAbsolutePath
}
else {
Join-Path $ArtifactsDirectory "render-manifest.json"
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ArtifactsDirectory)
}
New-Item -ItemType Directory -Force -Path $artifactsAbsolutePath | Out-Null
$logPath = Join-Path $artifactsAbsolutePath "render-wrapper.log"
$manifestPath = Join-Path $artifactsAbsolutePath "render-manifest.json"
$cfgPath = Join-Path $artifactsAbsolutePath "fragalytics-render.cfg"
$copyCfgPath = Join-Path $artifactsAbsolutePath "copy-render-cfg.cmd"
$launchHintPath = Join-Path $artifactsAbsolutePath "launch-cs2.cmd"
$steamPath = Resolve-OptionalPath $SteamPath
$cs2Path = Resolve-OptionalPath $Cs2Path
$obsCliPath = Resolve-OptionalPath $ObsCliPath
Set-Content -LiteralPath $logPath -Value ""
Write-Log "Preparing scripted CS2 render workflow." $logPath
$tickWindow = $EndTick - $StartTick
$clipDurationSeconds = [Math]::Ceiling([double]$tickWindow / [double]$TickRate)
$prerollTicks = [Math]::Min([Math]::Max([int]($TickRate * 2), 64), $StartTick)
$postrollSeconds = Get-EnvDouble -Name "FRAGALYTICS_RENDER_POSTROLL_SECONDS" -Fallback 1.0
$warmupSeconds = Get-EnvDouble -Name "FRAGALYTICS_RENDER_WARMUP_SECONDS" -Fallback 8.0
$captureLeadInSeconds = Get-EnvDouble -Name "FRAGALYTICS_RENDER_CAPTURE_LEAD_IN_SECONDS" -Fallback 1.0
$shutdownGraceSeconds = Get-EnvDouble -Name "FRAGALYTICS_RENDER_SHUTDOWN_GRACE_SECONDS" -Fallback 5.0
$captureDurationSeconds = [Math]::Max(1.0, $clipDurationSeconds + $postrollSeconds)
$outputExtension = [System.IO.Path]::GetExtension($outputAbsolutePath)
$captureStartTemplate = $env:FRAGALYTICS_RENDER_CAPTURE_START_COMMAND
$captureStopTemplate = $env:FRAGALYTICS_RENDER_CAPTURE_STOP_COMMAND
$prepareTemplate = $env:FRAGALYTICS_RENDER_PREPARE_COMMAND
$postProcessTemplate = $env:FRAGALYTICS_RENDER_POSTPROCESS_COMMAND
$launchArgsTemplate = if ([string]::IsNullOrWhiteSpace($env:FRAGALYTICS_CS2_LAUNCH_ARGS)) {
'-allow_third_party_software -insecure -novid -console -windowed -w {width} -h {height} +exec "{cfgPath}"'
}
else {
$env:FRAGALYTICS_CS2_LAUNCH_ARGS
}
Write-CfgFile -CfgPath $cfgPath -DemoAbsolutePath $demoAbsolutePath -StartTickValue $StartTick -PrerollTicks $prerollTicks -SafeCameraMode $CameraMode
$launchContext = @{
artifactsDirectory = $artifactsAbsolutePath
cfgPath = $cfgPath
cs2Path = $cs2Path
demoPath = $demoAbsolutePath
endTick = $EndTick
fps = $Fps
height = $resolutionParts.Height
logPath = $logPath
obsCliPath = $obsCliPath
outputExtension = $outputExtension
outputPath = $outputAbsolutePath
resolution = $Resolution.ToLowerInvariant()
startTick = $StartTick
steamPath = $steamPath
tickRate = $TickRate
warmupSeconds = $warmupSeconds
captureLeadInSeconds = $captureLeadInSeconds
captureDurationSeconds = $captureDurationSeconds
shutdownGraceSeconds = $shutdownGraceSeconds
width = $resolutionParts.Width
}
$expandedLaunchArgs = $launchArgsTemplate
foreach ($key in $launchContext.Keys) {
$expandedLaunchArgs = $expandedLaunchArgs.Replace("{$key}", [string]$launchContext[$key], [System.StringComparison]::Ordinal)
}
$manifest = [ordered]@{
demoPath = $DemoPath
outputPath = $OutputPath
demoPath = $demoAbsolutePath
outputPath = $outputAbsolutePath
startTick = $StartTick
endTick = $EndTick
tickRate = $TickRate
cameraMode = $CameraMode
resolution = $Resolution
resolution = $Resolution.ToLowerInvariant()
fps = $Fps
cs2Path = $Cs2Path
steamPath = $SteamPath
obsCliPath = $ObsCliPath
clipDurationSeconds = $clipDurationSeconds
captureDurationSeconds = $captureDurationSeconds
prerollTicks = $prerollTicks
warmupSeconds = $warmupSeconds
captureLeadInSeconds = $captureLeadInSeconds
shutdownGraceSeconds = $shutdownGraceSeconds
cs2Path = $cs2Path
steamPath = $steamPath
obsCliPath = $obsCliPath
launchArguments = $expandedLaunchArgs
cfgPath = $cfgPath
prepareCommand = $prepareTemplate
captureStartCommand = $captureStartTemplate
captureStopCommand = $captureStopTemplate
postProcessCommand = $postProcessTemplate
createdAtUtc = [DateTime]::UtcNow.ToString("O")
}
$manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
$manifest | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
@(
"@echo off"
"copy /Y ""$cfgPath"" ""%USERPROFILE%\game\csgo\cfg\fragalytics-render.cfg"""
) | Set-Content -LiteralPath $copyCfgPath -Encoding ASCII
if ([string]::IsNullOrWhiteSpace($Cs2Path)) {
throw "FRAGALYTICS_CS2_PATH or -Cs2Path is not configured. Review $manifestPath and update render-cs2-clip.ps1 with your capture workflow."
@(
"@echo off"
"""$cs2Path"" $expandedLaunchArgs"
) | Set-Content -LiteralPath $launchHintPath -Encoding ASCII
if ([string]::IsNullOrWhiteSpace($cs2Path)) {
throw "FRAGALYTICS_CS2_PATH or -Cs2Path is not configured. Review $manifestPath."
}
if (-not (Test-Path -LiteralPath $Cs2Path)) {
throw "Configured CS2 executable was not found: $Cs2Path"
if (-not (Test-Path -LiteralPath $cs2Path)) {
throw "Configured CS2 executable was not found: $cs2Path"
}
throw @"
CS2 scripted wrapper was invoked successfully, but the actual capture workflow is not implemented yet.
if (-not [string]::IsNullOrWhiteSpace($prepareTemplate)) {
Invoke-TemplateCommand -Template $prepareTemplate -Context $launchContext -LogPath $logPath
}
Edit:
$PSScriptRoot\render-cs2-clip.ps1
$captureStarted = $false
$cs2Process = $null
try {
Write-Log "Starting CS2 process." $logPath
$cs2Process = Start-Process -FilePath $cs2Path -ArgumentList $expandedLaunchArgs -WorkingDirectory (Split-Path -Parent $cs2Path) -PassThru
Write-Log "CS2 process id: $($cs2Process.Id)" $logPath
Inputs captured in:
Start-Sleep -Seconds $warmupSeconds
if (-not [string]::IsNullOrWhiteSpace($captureStartTemplate)) {
Invoke-TemplateCommand -Template $captureStartTemplate -Context $launchContext -LogPath $logPath
$captureStarted = $true
}
elseif (-not [string]::IsNullOrWhiteSpace($obsCliPath)) {
Invoke-ExternalCommand -FilePath $obsCliPath -Arguments "recording start" -WorkingDirectory $artifactsAbsolutePath -LogPath $logPath
$captureStarted = $true
}
else {
Write-Log "No capture start command was configured. The wrapper will launch CS2 and wait, but capture must be handled externally." $logPath
}
Start-Sleep -Seconds $captureLeadInSeconds
Write-Log "Waiting $captureDurationSeconds seconds for the clip window to play." $logPath
Start-Sleep -Seconds $captureDurationSeconds
if ($captureStarted) {
if (-not [string]::IsNullOrWhiteSpace($captureStopTemplate)) {
Invoke-TemplateCommand -Template $captureStopTemplate -Context $launchContext -LogPath $logPath -Optional
}
elseif (-not [string]::IsNullOrWhiteSpace($obsCliPath)) {
Invoke-ExternalCommand -FilePath $obsCliPath -Arguments "recording stop" -WorkingDirectory $artifactsAbsolutePath -LogPath $logPath -Optional
}
}
if (-not [string]::IsNullOrWhiteSpace($postProcessTemplate)) {
Invoke-TemplateCommand -Template $postProcessTemplate -Context $launchContext -LogPath $logPath
}
if (-not (Test-Path -LiteralPath $outputAbsolutePath)) {
throw @"
No output file was produced at:
$outputAbsolutePath
Review:
$manifestPath
$logPath
$cfgPath
Expected output:
$OutputPath
Recommended next automation step:
1. Launch CS2 with your preferred startup flags
2. Load the demo at $DemoPath
3. Seek near tick $StartTick and capture until $EndTick
4. Encode to $OutputPath
5. Exit 0 only if the output file exists
Common fixes:
- set FRAGALYTICS_RENDER_CAPTURE_START_COMMAND and FRAGALYTICS_RENDER_CAPTURE_STOP_COMMAND
- or point FRAGALYTICS_OBS_CLI_PATH at obs-cli
- update FRAGALYTICS_CS2_LAUNCH_ARGS so CS2 loads the generated cfg
"@
}
Write-Log "Render completed successfully." $logPath
}
finally {
if ($cs2Process -and -not $cs2Process.HasExited) {
Write-Log "Stopping CS2 process." $logPath
Stop-Process -Id $cs2Process.Id -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds $shutdownGraceSeconds
}
}
+32 -3
View File
@@ -190,6 +190,7 @@ public sealed class RenderJobContract
public const string DefaultOutputFormat = "mp4";
public const string DefaultResolution = "1920x1080";
public const string DefaultFps = "60";
public const int DefaultTickRate = 64;
public string Version { get; set; } = CurrentVersion;
public int DemoId { get; set; }
@@ -198,13 +199,24 @@ public sealed class RenderJobContract
public int SourceEventId { get; set; }
public int StartTick { get; set; }
public int EndTick { get; set; }
public int TickRate { get; set; } = DefaultTickRate;
public string CameraMode { get; set; } = DefaultCameraMode;
public string OutputFormat { get; set; } = DefaultOutputFormat;
public string Resolution { get; set; } = DefaultResolution;
public string Fps { get; set; } = DefaultFps;
public void Normalize()
{
Version = string.IsNullOrWhiteSpace(Version) ? CurrentVersion : Version.Trim();
CameraMode = NormalizeString(CameraMode, DefaultCameraMode);
OutputFormat = NormalizeString(OutputFormat, DefaultOutputFormat).ToLowerInvariant();
Resolution = NormalizeString(Resolution, DefaultResolution).ToLowerInvariant();
Fps = NormalizePositiveNumber(Fps, DefaultFps);
}
public Dictionary<string, string> ToPayload()
{
Normalize();
return new Dictionary<string, string>
{
["version"] = Version,
@@ -214,6 +226,7 @@ public sealed class RenderJobContract
["sourceEventId"] = SourceEventId.ToString(),
["startTick"] = StartTick.ToString(),
["endTick"] = EndTick.ToString(),
["tickRate"] = TickRate.ToString(),
["cameraMode"] = CameraMode,
["outputFormat"] = OutputFormat,
["resolution"] = Resolution,
@@ -223,7 +236,7 @@ public sealed class RenderJobContract
public static RenderJobContract FromPayload(IReadOnlyDictionary<string, string> payload)
{
return new RenderJobContract
var contract = new RenderJobContract
{
Version = ReadString(payload, "version", CurrentVersion),
DemoId = ReadInt(payload, "demoId"),
@@ -232,11 +245,15 @@ public sealed class RenderJobContract
SourceEventId = ReadInt(payload, "sourceEventId"),
StartTick = ReadInt(payload, "startTick"),
EndTick = ReadInt(payload, "endTick"),
TickRate = ReadInt(payload, "tickRate", DefaultTickRate),
CameraMode = ReadString(payload, "cameraMode", DefaultCameraMode),
OutputFormat = ReadString(payload, "outputFormat", DefaultOutputFormat),
Resolution = ReadString(payload, "resolution", DefaultResolution),
Fps = ReadString(payload, "fps", DefaultFps)
};
contract.Normalize();
return contract;
}
public string GetSuggestedFileExtension()
@@ -244,15 +261,27 @@ public sealed class RenderJobContract
return ClipMediaFormats.GetSuggestedFileExtension(OutputFormat);
}
private static int ReadInt(IReadOnlyDictionary<string, string> payload, string key)
private static int ReadInt(IReadOnlyDictionary<string, string> payload, string key, int fallback = 0)
{
return payload.TryGetValue(key, out var raw) && int.TryParse(raw, out var value) ? value : 0;
return payload.TryGetValue(key, out var raw) && int.TryParse(raw, out var value) ? value : fallback;
}
private static string ReadString(IReadOnlyDictionary<string, string> payload, string key, string fallback)
{
return payload.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value) ? value : fallback;
}
private static string NormalizeString(string? value, string fallback)
{
return string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
}
private static string NormalizePositiveNumber(string? value, string fallback)
{
return int.TryParse(value, out var parsed) && parsed > 0
? parsed.ToString()
: fallback;
}
}
public sealed class ClipAssetRecord
@@ -50,8 +50,36 @@
<strong>@row.ClipJob.AttemptCount</strong>
</div>
<div class="explanation-row">
<span>Payload</span>
<code>@string.Join(", ", row.ClipJob.Payload.Select(item => $"{item.Key}={item.Value}"))</code>
<span>Highlight</span>
<strong>#@row.RenderJob.HighlightId</strong>
</div>
<div class="explanation-row">
<span>Demo / Match</span>
<strong>#@row.RenderJob.DemoId / #@row.RenderJob.MatchId</strong>
</div>
<div class="explanation-row">
<span>Camera</span>
<code>@row.RenderJob.CameraMode</code>
</div>
<div class="explanation-row">
<span>Output</span>
<code>@row.RenderJob.OutputFormat</code>
</div>
<div class="explanation-row">
<span>Resolution</span>
<code>@row.RenderJob.Resolution</code>
</div>
<div class="explanation-row">
<span>FPS</span>
<code>@row.RenderJob.Fps</code>
</div>
<div class="explanation-row">
<span>Tick Rate</span>
<code>@row.RenderJob.TickRate</code>
</div>
<div class="explanation-row">
<span>Contract</span>
<code>[email protected]</code>
</div>
@if (!string.IsNullOrWhiteSpace(row.ClipJob.ErrorMessage))
{
@@ -80,6 +80,7 @@ public sealed class QueryService
.Select(job => new ClipJobWithHighlightViewModel
{
ClipJob = job,
RenderJob = RenderJobContract.FromPayload(job.Payload),
Highlight = state.Highlights.FirstOrDefault(highlight => highlight.Id == job.HighlightId),
ClipAsset = state.ClipAssets.FirstOrDefault(asset => asset.Id == job.ClipAssetId)
})
@@ -204,6 +205,7 @@ public sealed class HighlightDetailsRow
public sealed class ClipJobWithHighlightViewModel
{
public ClipJobRecord ClipJob { get; set; } = new();
public RenderJobContract RenderJob { get; set; } = new();
public HighlightRecord? Highlight { get; set; }
public ClipAssetRecord? ClipAsset { get; set; }
}
@@ -106,7 +106,8 @@ public sealed class MatchScoringService
HighlightId = highlight.Id,
SourceEventId = candidate.Id,
StartTick = highlight.StartTick,
EndTick = highlight.EndTick
EndTick = highlight.EndTick,
TickRate = match.TickRate.GetValueOrDefault(RenderJobContract.DefaultTickRate)
};
var clipJob = new ClipJobRecord
+25 -1
View File
@@ -214,11 +214,12 @@ static async Task RunFlowTestAsync()
Assert(state.ClipJobs[0].Payload.TryGetValue("version", out var version) && version == RenderJobContract.CurrentVersion, "Expected versioned render payload.");
Assert(state.ClipJobs[0].Payload.TryGetValue("demoId", out var demoIdPayload) && demoIdPayload == "1", "Expected demo id in render payload.");
Assert(state.ClipJobs[0].Payload.ContainsKey("cameraMode"), "Expected render payload defaults on clip job.");
Assert(state.ClipJobs[0].Payload.Keys.All(key => key is "version" or "demoId" or "matchId" or "highlightId" or "sourceEventId" or "startTick" or "endTick" or "cameraMode" or "outputFormat" or "resolution" or "fps"), "Expected payload to contain only render contract fields.");
Assert(state.ClipJobs[0].Payload.Keys.All(key => key is "version" or "demoId" or "matchId" or "highlightId" or "sourceEventId" or "startTick" or "endTick" or "tickRate" or "cameraMode" or "outputFormat" or "resolution" or "fps"), "Expected payload to contain only render contract fields.");
var renderJob = RenderJobContract.FromPayload(state.ClipJobs[0].Payload);
Assert(renderJob.MatchId == state.Matches[0].Id, "Expected match id in typed render payload.");
Assert(renderJob.OutputFormat == RenderJobContract.DefaultOutputFormat, "Expected default output format.");
Assert(renderJob.TickRate == state.Matches[0].TickRate, "Expected match tick rate in typed render payload.");
await ingest.RescoreAllAsync(CancellationToken.None);
state = await store.GetSnapshotAsync();
@@ -317,10 +318,33 @@ static void RunRenderJobContractDefaultsTest()
Assert(contract.OutputFormat == RenderJobContract.DefaultOutputFormat, "Expected default output format.");
Assert(contract.Resolution == RenderJobContract.DefaultResolution, "Expected default resolution.");
Assert(contract.Fps == RenderJobContract.DefaultFps, "Expected default fps.");
Assert(contract.TickRate == RenderJobContract.DefaultTickRate, "Expected default tick rate.");
Assert(contract.GetSuggestedFileExtension() == ClipMediaFormats.Mp4Extension, "Expected mp4 extension for default output format.");
contract.OutputFormat = "webm";
Assert(contract.GetSuggestedFileExtension() == ClipMediaFormats.WebMExtension, "Expected webm extension override.");
var messyPayload = new Dictionary<string, string>
{
["demoId"] = "9",
["matchId"] = "17",
["highlightId"] = "23",
["sourceEventId"] = "42",
["startTick"] = "1500",
["endTick"] = "1800",
["tickRate"] = "128",
["cameraMode"] = " killer_pov ",
["outputFormat"] = " WEBM ",
["resolution"] = " 1280X720 ",
["fps"] = "0"
};
var normalized = RenderJobContract.FromPayload(messyPayload);
Assert(normalized.CameraMode == RenderJobContract.DefaultCameraMode, "Expected camera mode to be trimmed.");
Assert(normalized.OutputFormat == "webm", "Expected output format to be normalized.");
Assert(normalized.Resolution == "1280x720", "Expected resolution to be normalized.");
Assert(normalized.Fps == RenderJobContract.DefaultFps, "Expected invalid fps to fall back to the default.");
Assert(normalized.TickRate == 128, "Expected tick rate to survive normalization.");
}
static async Task RunRescoreReplacementTestAsync()