Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ opus = "0.4"
# BSD-2-Clause; the default `source` feature compiles Cisco's openh264 from vendored source.
openh264 = "0.9"
tracing = "0.1"
tracing-subscriber = "0.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
pollster = "1"
# Config file (docs/adr/0005): TOML deserialized via serde. Both MIT/Apache.
serde = { version = "1", features = ["derive"] }
Expand Down Expand Up @@ -116,6 +116,8 @@ cidre = { version = "=0.16.1", default-features = false, features = [
# windows-capture drives the WGC session/frame pool; the `windows` crate (same 0.62.x
# its API surfaces, so the D3D11 types unify) creates the shareable textures + NT handles.
windows-capture = "2.0"
# `#[implement]` (the process-loopback completion handler) expands to `::windows_core` paths.
windows-core = "0.62"
windows = { version = "0.62", features = [
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Dxgi",
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ The app checks for updates on launch; one click updates both binaries in place.
- Optional uploads to [ganked.tv](https://ganked.tv) or YouTube, only when you ask.
Clips never leave your machine on their own.

## Logs

The recorder writes what it is doing to a small set of log files next to its data, capped at
2 MB each and three files in total, so they never grow beyond 6 MB:

- Windows: `%LOCALAPPDATA%\rewynd\logs\rewynd-recorder.log`
- Linux: `$XDG_DATA_HOME/rewynd/logs/rewynd-recorder.log` (usually `~/.local/share/rewynd/logs/`)
- macOS: `~/Library/Application Support/rewynd/logs/rewynd-recorder.log`

Attach that file to a bug report about clips with no sound, a game that is not picked up,
or a recorder that stops: it says which audio path ran, whether audio was flowing (a
`peak` line a minute), and what the capture saw.

## Workspace layout

| Crate | Role |
Expand Down
444 changes: 323 additions & 121 deletions crates/app/src/main.rs

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion crates/app/src/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ static ICON: LazyLock<Vec<Icon>> = LazyLock::new(|| {
.collect()
});

/// The tooltip title while nothing is wrong.
pub const DEFAULT_STATUS: &str = "rewynd is recording";

pub struct RewyndTray {
tx: UnboundedSender<TrayCmd>,
/// One-line pipeline status shown as the tooltip title; the recorder updates it on failures.
Expand Down Expand Up @@ -135,7 +138,7 @@ pub async fn spawn(
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let tray = RewyndTray {
tx,
status: "rewynd is recording".to_owned(),
status: DEFAULT_STATUS.to_owned(),
mic_enabled,
};
let handle = tray.spawn().await?;
Expand Down
1 change: 1 addition & 0 deletions crates/capture/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ futures-util.workspace = true
windows-capture.workspace = true
# Window style/state queries (IsIconic, GWL_STYLE) behind the game heuristic.
windows = { workspace = true, features = ["Win32_UI_WindowsAndMessaging"] }
windows-core.workspace = true

[target.'cfg(target_os = "windows")'.dev-dependencies]
# The probe opens the shared handle on a second D3D11 device (D3D11CreateDevice
Expand Down
30 changes: 29 additions & 1 deletion crates/capture/src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,17 @@ pub fn steam_app_name(appid: u32) -> Option<String> {
/// already the best stable key we have.
fn clean_app_id(app_id: &str) -> String {
let base = app_id.trim();
let base = base.strip_suffix(".exe").unwrap_or(base);
// A Windows process name keeps its whole stem: the dots in `Minecraft.Windows.exe`
// are part of the name, not a reverse-DNS id. The suffix is matched in any case,
// since executables are named `GAME.EXE` often enough.
if let Some((stem, suffix)) = base
.len()
.checked_sub(4)
.and_then(|at| base.is_char_boundary(at).then(|| base.split_at(at)))
&& suffix.eq_ignore_ascii_case(".exe")
{
return stem.trim().to_owned();
}
// Reverse-DNS desktop ids: keep the final segment, which names the app.
let base = base.rsplit('.').next().unwrap_or(base);
base.trim().to_owned()
Expand Down Expand Up @@ -158,6 +168,24 @@ mod tests {
);
}

#[test]
fn display_name_keeps_a_dotted_exe_stem_whole() {
let no_steam = |_: u32| None;
assert_eq!(
info("Minecraft.Windows.exe", "Minecraft").display_name_via(no_steam),
"Minecraft.Windows"
);
assert_eq!(
info("Battle.net.exe", "").display_name_via(no_steam),
"Battle.net"
);
assert_eq!(
info("ELDENRING.EXE", "").display_name_via(no_steam),
"ELDENRING"
);
assert_eq!(info(".exe", "Title").display_name_via(no_steam), "Title");
}

#[test]
fn display_name_prefers_the_steam_title() {
let lookup = |appid: u32| (appid == 1245620).then(|| "ELDEN RING".to_owned());
Expand Down
Loading