Skip to content
alexfrihPublic

About

Film a CS2 highlight from the match demo, off-screen, on Linux

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

frag

Film a CS2 highlight from the match demo, off-screen, then cut it into a montage.

Valve gives no video of a match, only a replay file (the demo) that the game plays back. frag finds the match, downloads its demo, plays it in a hidden CS2 and films it. Nothing opens on the desktop, no game sound reaches the headphones, and the game settings it touches are put back.

Install

git clone https://github.com/alexfrih/frag-cli && cd frag-cli
ln -sfn "$PWD/frag.sh" ~/.local/bin/frag

Needs:

  • Linux with PipeWire, and an NVIDIA card: the picture is encoded by NVENC. Not tested on anything else. CS2 has no macOS build.
  • Steam with CS2 installed, signed in once on this machine. frag films the matches of the Steam account used last.
  • gamescope, gst-launch-1.0 with the PipeWire and NVENC plugins, ffmpeg with h264_nvenc, jq, uv, pactl, pw-link, python3, curl, unzip.

The usual case

frag ls            # recent matches, with the count of 3, 4 and 5 kill rounds
frag ace           # newest five-kill round, filmed, scoreboard at the end
frag film 2 --round 9 --scoreboard

The mp4 lands in ~/Desktop/WIP/CS2/, or in the folder named by FRAG_OUT. frag rounds <n> lists the rounds worth filming. frag check says whether anything was left running.

How it works, and the traps already paid for

  • Match list: the Leetify public API, by Steam id. No key.
  • Demo link: only Valve's game coordinator knows it. A signed-in Steam asks for it through boiler-writter (pinned release, checksum checked). The demo stays about a month.
  • Off-screen: gamescope --backend headless runs Steam and CS2 with no window. Steam on the desktop must be closed first: there is one Steam per user.
  • Picture: gamescope's PipeWire stream, read by GStreamer and encoded by NVENC. The frames are copied out at once and queued, or the stream runs out of buffers and drops half of them. A capture stopped with anything but INT, or started with a format the stream does not offer, kills the stream until the session restarts.
  • Sound: the game plays into a silent output created for the run (PIPEWIRE_NODE), and a watchdog relinks it if it ever lands on the headphones.
  • Smoothness: two takes. Real time for the sound, half speed for the picture, which then holds 60 frames a second even on a busy machine.
  • Clock: each take is timed on the kill feed (lib/sync.py): the line of a kill appears top right on the tick of the death, in every view. The first frame that moves is not a clock: a player who holds an angle does not move, and it put two takes out of twelve half a second off. The shot is seen 0.044 game seconds and 0.122 real seconds before its line appears.
  • Ten captures in one session crashed gamescope (SIGSEGV on the tenth). frag down, then film the rest with --reuse: the takes already on disk are kept.
  • The game's own startmovie writes nothing on Linux. Do not try it again.
  • Playback bar: demo_ui_mode 0 only works on the launch command line, before the demo loads.
  • Settings CS2 saves to disk (spec_show_xray, r_show_build_info, engine_no_focus_sleep) are recorded before the run and restored by frag down. The display mode too: filming launches the game windowed at 1920x1080 and CS2 saves that to cs2_video.txt, so the file is copied before the launch and put back by frag down. mangohud in the launch options is hidden with MANGOHUD_CONFIG=no_display.
  • Unfocused cap: engine_no_focus_sleep 20 limits an unfocused game to about 35 frames a second.

Montage

frag film 2 --round 9 --clean --scoreboard --lead 10 --tail 6 --takes ~/.local/share/frag/montage
frag sound music ~/.local/share/frag/montage/music.mp3   # sfx and announcer are in montage/audio
frag beats ~/.local/share/frag/montage/music.mp3         # where the beats and the drops are
$EDITOR ~/.local/share/frag/montage/plan.json            # start from montage/plan-example.json
frag montage ~/.local/share/frag/montage

--clean leaves only the crosshair and the kill feed on screen. The plan says on which beat each kill lands and how game time bends around it (warp: output second, game second). montage.py does the rest: slow motion, punch-in, flash, the kill ladder with each opponent's rating, the announcer word, the versus card, the scoreboard, the sound mix. Names and ratings come from the demo.

Two things the montages need that filming does not:

  • Sounds: the effects and the announcer are in montage/audio. frag sound generates the music (and the kit again) with ElevenLabs: set ELEVENLABS_API_KEY, or copy .env.example to ~/.config/frag/elevenlabs.env and fill it. It is the only key, and filming needs none. Any other track works too: name it music.mp3.
  • Fonts: a title face, a numbers face and a CJK face for names. Set FRAG_FONT_TITLE, FRAG_FONT_NUM and FRAG_FONT_CJK to font files; without them, fontconfig picks the closest installed ones.

Recap: several rounds, your view over each victim's

frag film 3 --round 11 --clean --victims --lead 10 --tail 6 --takes ~/.local/share/frag/recap/r11
frag film 3 --round 22 --clean --victims --lead 10 --tail 6 --takes ~/.local/share/frag/recap/r22
frag sound music ~/.local/share/frag/recap/music.mp3
frag beats ~/.local/share/frag/recap/music.mp3      # read where the drops really are: the model does not follow the plan
$EDITOR ~/.local/share/frag/recap/recap.json        # start from montage/recap-example.json
frag recap ~/.local/share/frag/recap [--solo] [--still 4.95]

--victims films the last seven seconds of each victim through their own eyes (v1.mkv ...). recap.py stacks two views in a 1080x1920 frame: the player on top, the next victim below, the same instant. --solo keeps your view alone: a native 1080 square over a blurred fill, the five names as a ladder under it. recap.json holds only the takes, the captions and the beat of each first kill (drops, then one kill every every beats); the timeline is derived from the beats. The final scoreboard is a screenshot (score.png) taken with +showscores after the last round.

The whole round, raw

frag film 3 --round 11 --full --xray --height 1440 --takes ~/.local/share/frag/aced/r11
frag film 3 --round 22 --full --xray --height 1440 --takes ~/.local/share/frag/aced/r22
frag join ~/Desktop/WIP/CS2/aced.mp4 ~/.local/share/frag/aced/r11/round.mp4 ~/.local/share/frag/aced/r22/round.mp4

--full films from the last second of the freeze time to the win banner, with the whole game interface (players alive, radar, timer, kill feed); --xray keeps the enemies outlined through walls, as the game's own replay shows them; no music, no title, nothing added. The sound is the game's, in stereo: where a step comes from is the point. --reuse keeps a take already on disk, so deleting real.mkv films the sound again alone.

Uncut: the round as it was played, with a music bed

frag film 3 --round 11 --clean --lead 10 --tail 6 --takes ~/.local/share/frag/uncut/r11
frag sound music ~/.local/share/frag/uncut/music.mp3 150 dnb   # seconds, phonk or dnb
frag beats ~/.local/share/frag/uncut/music.mp3                 # tempo and loudness over time: where the drops are
$EDITOR ~/.local/share/frag/uncut/uncut.json                   # start from montage/uncut-example.json
frag uncut ~/.local/share/frag/uncut [--still 63.75]

Nothing is cut inside a round and time is never bent: the music is what gets fitted. uncut.json says which second of the track (music.sync.music, a drop) lands on which kill; music.tempo stretches the track by a fraction of a percent and music.cuts drops whole bars (cut in the breakdown, never inside a drop) so a second drop lands too; the lead of a later round is set so its first kill falls on it. Additions on screen: a title, the round, 1K to 4K and ACE, the real scoreboard. No announcer: the Heroes of Newerth words stay in montage and recap. Exported at 1440p (height) so YouTube serves its better codec.

The music model follows the lengths asked, not the section plan exactly: measure the track (frag beats), then fit.

Takes weigh gigabytes; they are deleted after a week.

For an agent

SKILL.md tells a coding agent when to use frag and what not to do with it. For Claude Code:

ln -sfn "$PWD" ~/.claude/skills/frag

License

MIT. Counter-Strike 2 belongs to Valve; this project is not affiliated with Valve, Leetify or ElevenLabs.

About

Film a CS2 highlight from the match demo, off-screen, on Linux

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages