Skip to content

Commit 1ec35f3

Browse files
committed
Merge branch 'sid2934-feature/video-cli-fine-control'
2 parents 328f368 + a2d8e96 commit 1ec35f3

File tree

2 files changed

+160
-78
lines changed

2 files changed

+160
-78
lines changed

src/cli/commands/video-command.ts

Lines changed: 159 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { parseArgs } from 'node:util';
33
import path from 'node:path';
44
import fs from 'fs-extra';
55
import { Command } from './command';
6+
import type { Player } from 'csdm/common/types/player';
67
import { migrateSettings } from 'csdm/node/settings/migrate-settings';
78
import { getSettings } from 'csdm/node/settings/get-settings';
89
import { getDemoFromFilePath } from 'csdm/node/demo/get-demo-from-file-path';
910
import { generateVideo } from 'csdm/node/video/generation/generate-video';
10-
import type { Sequence } from 'csdm/common/types/sequence';
11+
import type { Parameters } from 'csdm/node/video/generation/generate-video';
1112
import { EncoderSoftware } from 'csdm/common/types/encoder-software';
1213
import { isValidEncoderSoftware } from 'csdm/common/types/encoder-software';
1314
import { RecordingSystem } from 'csdm/common/types/recording-system';
@@ -58,6 +59,7 @@ export class VideoCommand extends Command {
5859
private readonly deathNoticesDurationFlag = 'death-notices-duration';
5960
private readonly cfgFlag = 'cfg';
6061
private readonly focusPlayerFlag = 'focus-player';
62+
private readonly videoConfigFileFlag = 'video-config-file';
6163
private outputFolderPath: string | undefined;
6264
private demoPath: string = '';
6365
private startTick: number = 0;
@@ -85,6 +87,7 @@ export class VideoCommand extends Command {
8587
private deathNoticesDuration: number | undefined;
8688
private cfg: string | undefined;
8789
private focusPlayerSteamId: string | undefined;
90+
private videoConfigJson: Partial<Parameters> | undefined;
8891

8992
public getDescription() {
9093
return 'Generate videos from demos.';
@@ -128,6 +131,7 @@ export class VideoCommand extends Command {
128131
console.log(` --${this.deathNoticesDurationFlag} <number>`);
129132
console.log(` --${this.cfgFlag} <string>`);
130133
console.log(` --${this.focusPlayerFlag} <steamId>`);
134+
console.log(` --${this.videoConfigFileFlag} <path> (JSON file for video config)`);
131135
}
132136

133137
public async run() {
@@ -137,97 +141,151 @@ export class VideoCommand extends Command {
137141
await migrateSettings();
138142

139143
const settings = await getSettings();
140-
const demo = await getDemoFromFilePath(this.demoPath);
141-
const outputFolderPath = this.outputFolderPath ?? path.dirname(this.demoPath);
142-
const sequence: Sequence = {
143-
number: 1,
144-
startTick: this.startTick,
145-
endTick: this.endTick,
146-
showXRay: this.showXRay ?? settings.video.showXRay,
147-
showAssists: this.showAssists ?? settings.video.showAssists,
148-
showOnlyDeathNotices: this.showOnlyDeathNotices ?? settings.video.showOnlyDeathNotices,
149-
playersOptions: [],
150-
playerCameras: [],
151-
cameras: [],
152-
recordAudio: this.recordAudio ?? settings.video.recordAudio,
153-
playerVoicesEnabled: this.playerVoices ?? settings.video.playerVoicesEnabled,
154-
deathNoticesDuration: this.deathNoticesDuration ?? settings.video.deathNoticesDuration,
155-
cfg: this.cfg,
156-
};
144+
const controller = new AbortController();
145+
146+
let parameters: Parameters;
147+
// Handle requests using video configuration JSON file.
148+
if (this.videoConfigJson) {
149+
// Null check required for type safety
150+
if (!this.videoConfigJson.demoPath) {
151+
throw new InvalidArgument('demoPath is required in video config JSON');
152+
}
157153

158-
if (this.focusPlayerSteamId) {
159-
const player = await fetchPlayer(this.focusPlayerSteamId);
160-
sequence.playerCameras.push({
161-
tick: this.startTick,
162-
playerSteamId: this.focusPlayerSteamId,
163-
playerName: player.name,
164-
});
154+
const demo = await getDemoFromFilePath(this.videoConfigJson.demoPath);
155+
156+
parameters = {
157+
videoId: this.videoConfigJson.videoId ?? randomUUID(),
158+
checksum: demo.checksum,
159+
game: demo.game,
160+
tickrate: demo.tickrate,
161+
recordingSystem: this.videoConfigJson?.recordingSystem ?? settings.video.recordingSystem,
162+
recordingOutput: this.videoConfigJson?.recordingOutput ?? settings.video.recordingOutput,
163+
encoderSoftware: this.videoConfigJson?.encoderSoftware ?? settings.video.encoderSoftware,
164+
framerate: this.videoConfigJson?.framerate ?? settings.video.framerate,
165+
width: this.videoConfigJson?.width ?? settings.video.width,
166+
height: this.videoConfigJson?.height ?? settings.video.height,
167+
closeGameAfterRecording:
168+
this.videoConfigJson?.closeGameAfterRecording ?? settings.video.closeGameAfterRecording,
169+
concatenateSequences: this.videoConfigJson?.concatenateSequences ?? settings.video.concatenateSequences,
170+
ffmpegSettings: this.videoConfigJson?.ffmpegSettings ?? settings.video.ffmpegSettings,
171+
outputFolderPath: this.videoConfigJson?.outputFolderPath ?? path.dirname(this.demoPath),
172+
demoPath: this.videoConfigJson?.demoPath ?? this.demoPath,
173+
sequences: this.videoConfigJson?.sequences ?? [],
174+
signal: controller.signal,
175+
onGameStart: () => {
176+
console.log('Counter-Strike started');
177+
},
178+
onMoveFilesStart: () => {
179+
console.log('Moving files...');
180+
},
181+
onSequenceStart: (number) => {
182+
console.log(`Converting sequence ${number}...`);
183+
},
184+
onConcatenateSequencesStart: () => {
185+
console.log('Concatenating sequences...');
186+
},
187+
};
188+
} else {
189+
const demo = await getDemoFromFilePath(this.demoPath);
190+
191+
const player: Player | undefined = this.focusPlayerSteamId
192+
? await fetchPlayer(this.focusPlayerSteamId)
193+
: undefined;
194+
195+
parameters = {
196+
videoId: randomUUID(),
197+
checksum: demo.checksum,
198+
game: demo.game,
199+
tickrate: demo.tickrate,
200+
recordingSystem: this.recordingSystem ?? settings.video.recordingSystem,
201+
recordingOutput: this.recordingOutput ?? settings.video.recordingOutput,
202+
encoderSoftware: this.encoderSoftware ?? settings.video.encoderSoftware,
203+
framerate: this.framerate ?? settings.video.framerate,
204+
width: this.width ?? settings.video.width,
205+
height: this.height ?? settings.video.height,
206+
closeGameAfterRecording: this.closeGameAfterRecording ?? settings.video.closeGameAfterRecording,
207+
concatenateSequences: this.concatenateSequences ?? settings.video.concatenateSequences,
208+
ffmpegSettings: {
209+
...settings.video.ffmpegSettings,
210+
audioBitrate: this.ffmpegAudioBitrate ?? settings.video.ffmpegSettings.audioBitrate,
211+
constantRateFactor: this.ffmpegCrf ?? settings.video.ffmpegSettings.constantRateFactor,
212+
videoCodec: this.ffmpegVideoCodec ?? settings.video.ffmpegSettings.videoCodec,
213+
audioCodec: this.ffmpegAudioCodec ?? settings.video.ffmpegSettings.audioCodec,
214+
videoContainer: this.ffmpegVideoContainer ?? settings.video.ffmpegSettings.videoContainer,
215+
inputParameters: this.ffmpegInputParameters ?? settings.video.ffmpegSettings.inputParameters,
216+
outputParameters: this.ffmpegOutputParameters ?? settings.video.ffmpegSettings.outputParameters,
217+
},
218+
outputFolderPath: this.outputFolderPath ?? path.dirname(this.demoPath),
219+
demoPath: this.demoPath ?? this.demoPath,
220+
sequences: [
221+
{
222+
number: 1,
223+
startTick: this.startTick,
224+
endTick: this.endTick,
225+
showXRay: this.showXRay ?? settings.video.showXRay,
226+
showAssists: this.showAssists ?? settings.video.showAssists,
227+
showOnlyDeathNotices: this.showOnlyDeathNotices ?? settings.video.showOnlyDeathNotices,
228+
playersOptions: [],
229+
cameras: [],
230+
recordAudio: this.recordAudio ?? settings.video.recordAudio,
231+
playerCameras:
232+
player !== undefined && this.focusPlayerSteamId
233+
? [
234+
{
235+
tick: this.startTick,
236+
playerSteamId: this.focusPlayerSteamId,
237+
playerName: player.name,
238+
},
239+
]
240+
: [],
241+
playerVoicesEnabled: this.playerVoices ?? settings.video.playerVoicesEnabled,
242+
deathNoticesDuration: this.deathNoticesDuration ?? settings.video.deathNoticesDuration,
243+
cfg: this.cfg,
244+
},
245+
],
246+
signal: controller.signal,
247+
onGameStart: () => {
248+
console.log('Counter-Strike started');
249+
},
250+
onMoveFilesStart: () => {
251+
console.log('Moving files...');
252+
},
253+
onSequenceStart: (number) => {
254+
console.log(`Converting sequence ${number}...`);
255+
},
256+
onConcatenateSequencesStart: () => {
257+
console.log('Concatenating sequences...');
258+
},
259+
};
165260
}
166261

167-
const recordingSystem = this.recordingSystem ?? settings.video.recordingSystem;
168-
if (recordingSystem === RecordingSystem.HLAE && !(await isHlaeInstalled())) {
262+
if (parameters.recordingSystem === RecordingSystem.HLAE && !(await isHlaeInstalled())) {
169263
console.log('Installing HLAE...');
170264
await installHlae();
171265
}
172266

173-
const recordingOutput = this.recordingOutput ?? settings.video.recordingOutput;
174-
const shouldGenerateVideo = recordingOutput !== RecordingOutput.Images;
175-
const encoderSoftware = this.encoderSoftware ?? settings.video.encoderSoftware;
176-
if (shouldGenerateVideo && encoderSoftware === EncoderSoftware.VirtualDub && !(await isVirtualDubInstalled())) {
267+
const shouldGenerateVideo = parameters.recordingOutput !== RecordingOutput.Images;
268+
if (
269+
shouldGenerateVideo &&
270+
parameters.encoderSoftware === EncoderSoftware.VirtualDub &&
271+
!(await isVirtualDubInstalled())
272+
) {
177273
console.log('Installing VirtualDub...');
178274
await downloadAndExtractVirtualDub();
179275
}
180276

181-
if (shouldGenerateVideo && encoderSoftware === EncoderSoftware.FFmpeg && !(await isFfmpegInstalled())) {
277+
if (
278+
shouldGenerateVideo &&
279+
parameters.encoderSoftware === EncoderSoftware.FFmpeg &&
280+
!(await isFfmpegInstalled())
281+
) {
182282
console.log('Installing FFmpeg...');
183283
await installFfmpeg();
184284
}
285+
console.log(`Starting video generation...\n${JSON.stringify(parameters)}`);
286+
await generateVideo(parameters);
185287

186-
const videoId = randomUUID();
187-
const controller = new AbortController();
188-
189-
await generateVideo({
190-
videoId,
191-
checksum: demo.checksum,
192-
game: demo.game,
193-
tickrate: demo.tickrate,
194-
recordingSystem,
195-
recordingOutput,
196-
encoderSoftware,
197-
framerate: this.framerate ?? settings.video.framerate,
198-
width: this.width ?? settings.video.width,
199-
height: this.height ?? settings.video.height,
200-
closeGameAfterRecording: this.closeGameAfterRecording ?? settings.video.closeGameAfterRecording,
201-
concatenateSequences: this.concatenateSequences ?? settings.video.concatenateSequences,
202-
ffmpegSettings: {
203-
...settings.video.ffmpegSettings,
204-
audioBitrate: this.ffmpegAudioBitrate ?? settings.video.ffmpegSettings.audioBitrate,
205-
constantRateFactor: this.ffmpegCrf ?? settings.video.ffmpegSettings.constantRateFactor,
206-
videoCodec: this.ffmpegVideoCodec ?? settings.video.ffmpegSettings.videoCodec,
207-
audioCodec: this.ffmpegAudioCodec ?? settings.video.ffmpegSettings.audioCodec,
208-
videoContainer: this.ffmpegVideoContainer ?? settings.video.ffmpegSettings.videoContainer,
209-
inputParameters: this.ffmpegInputParameters ?? settings.video.ffmpegSettings.inputParameters,
210-
outputParameters: this.ffmpegOutputParameters ?? settings.video.ffmpegSettings.outputParameters,
211-
},
212-
outputFolderPath,
213-
demoPath: this.demoPath,
214-
sequences: [sequence],
215-
signal: controller.signal,
216-
onGameStart: () => {
217-
console.log('Counter-Strike started');
218-
},
219-
onMoveFilesStart: () => {
220-
console.log('Moving files...');
221-
},
222-
onSequenceStart: (number) => {
223-
console.log(`Converting sequence ${number}...`);
224-
},
225-
onConcatenateSequencesStart: () => {
226-
console.log('Concatenating sequences...');
227-
},
228-
});
229-
230-
console.log(`Video generated in ${outputFolderPath}`);
288+
console.log(`Video generated in ${parameters.outputFolderPath}`);
231289
} catch (error) {
232290
if (error instanceof Error) {
233291
console.error(error.message);
@@ -276,11 +334,35 @@ export class VideoCommand extends Command {
276334
[this.deathNoticesDurationFlag]: { type: 'string' },
277335
[this.cfgFlag]: { type: 'string' },
278336
[this.focusPlayerFlag]: { type: 'string' },
337+
[this.videoConfigFileFlag]: { type: 'string' },
279338
},
280339
allowPositionals: true,
281340
args: this.args,
282341
});
283342

343+
// Parse video config file if provided
344+
if (values[this.videoConfigFileFlag]) {
345+
const videoConfigFilePath = values[this.videoConfigFileFlag];
346+
try {
347+
if (!videoConfigFilePath) {
348+
throw new InvalidArgument('Video config file path is undefined');
349+
}
350+
351+
const jsonStr = await fs.readFile(videoConfigFilePath, { encoding: 'utf8' });
352+
353+
const matchHashComment = new RegExp(/(#.*)/, 'gi');
354+
// Remove comments (//, /* */ and #) from JSONC file
355+
const commentFreeJson = jsonStr
356+
.replace(matchHashComment, '')
357+
.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, '')
358+
.trim();
359+
this.videoConfigJson = JSON.parse(commentFreeJson);
360+
return;
361+
} catch (err) {
362+
throw new InvalidArgument(`Failed to read or parse video config file: ${err}`);
363+
}
364+
}
365+
284366
if (positionals.length < 3) {
285367
throw new InvalidArgument('Missing arguments');
286368
}

src/node/video/generation/generate-video.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type FfmpegSettings = {
3838
outputParameters: string;
3939
};
4040

41-
type Parameters = {
41+
export type Parameters = {
4242
videoId: string;
4343
checksum: string;
4444
game: Game;

0 commit comments

Comments
 (0)