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