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
8 changes: 7 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
timeout-minutes: 60
env:
LIBCLANG_PATH: C:\Program Files\LLVM\bin
ETERNAL_DIAGNOSTICS_ENDPOINT: ${{ secrets.ETERNAL_DIAGNOSTICS_ENDPOINT }}
ETERNAL_DIAGNOSTICS_INGEST_KEY: ${{ secrets.ETERNAL_DIAGNOSTICS_INGEST_KEY }}
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -89,7 +91,11 @@ jobs:

- name: Build the installer
shell: pwsh
run: .\scripts\build-installer.ps1
run: |
if (!$env:ETERNAL_DIAGNOSTICS_ENDPOINT -or !$env:ETERNAL_DIAGNOSTICS_INGEST_KEY) {
throw "Release diagnostics endpoint and ingest credential must be configured."
}
.\scripts\build-installer.ps1

- name: Compute SHA-256
id: hash
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/testflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
- name: Archive and upload, or compile when secrets are absent
if: github.event_name != 'pull_request'
env:
ETERNAL_DIAGNOSTICS_ENDPOINT: ${{ secrets.ETERNAL_DIAGNOSTICS_ENDPOINT }}
ETERNAL_DIAGNOSTICS_INGEST_KEY: ${{ secrets.ETERNAL_DIAGNOSTICS_INGEST_KEY }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

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

11 changes: 11 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## EternalMonitor unreleased

- Windows and iPad can share diagnostics automatically with support, queue events while offline and retry after reconnecting. Automatic sharing can be disabled in Settings.

- Windows and iPad now retain bounded support logs across launches, including device/build details, encoder/decoder errors and stream summaries. Export iPad reports from Settings → Diagnostics. Windows Copy logs includes native FFmpeg driver errors and removes pairing credentials.

- Fix Intel Quick Sync startup by converting capture frames to its supported
NV12 input format for H.264 and HEVC.
- Try the other Windows hardware encoders when the preferred encoder cannot
open, before falling back to CPU encoding. Record each failure in the host log.

## EternalMonitor v0.3.0

v0.3.0 is the current stable release. Install the Windows app with iPad
Expand Down
25 changes: 21 additions & 4 deletions SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ or in the controls while streaming.
| The iPad never connects over USB | Open Apple Devices on the PC and leave it open. Unlock the iPad, accept **Trust**, and try another cable. Then unplug and plug in again. |
| The iPad can't find the PC at all | Open the iPad's **Settings** app, then **Privacy & Security**, then **Local Network**, and turn on **EternalMonitor**. |
| The picture stutters or freezes on Wi-Fi | Move closer to the router, or use a 5 GHz network. Connect the PC to the router with a network cable, or use USB. |
| **Hardware encoder unavailable** on the PC | Update your graphics driver from NVIDIA, AMD or Intel, then click **Restart**. |
| **Hardware encoder unavailable** on the PC | In **Settings**, select **Auto** for the encoder and **YUV420** for encoder input, then restart the stream. Update the driver from your GPU manufacturer. If this persists, use **Performance** → **Copy logs** and include the iPad report from **Settings → Diagnostics**. Some GPUs have no supported hardware encoder. |
| **Extended display unavailable** on the PC | Run EternalMonitor-Setup.exe again to repair the install, then restart the stream. |

**Using a VPN such as Tailscale?** In the PC window, open **Settings** and set
Expand All @@ -207,10 +207,27 @@ In the PC window, open **Performance** and click **Copy logs**. Paste the
result into a [bug report on GitHub](https://github.com/sidebandstudio/EternalMonitor/issues/new/choose)
or an email to [hello@sideband.studio](mailto:hello@sideband.studio).
Say what you tried, what you saw, your iPad model, and whether you used Wi-Fi or
USB. The logs include the current pairing code. Click **New code** in the PC
window after sharing them. Don't post the QR code publicly either, because
anyone who scans it can connect.
USB. On iPad, open **Settings → Diagnostics**, then **Export diagnostics**
for a text file or **Copy diagnostics**. Include both reports when possible.

Reports include Windows build, CPU, GPU and driver details; iPad hardware,
iPadOS and app build; encoder/decoder setup, connection events and periodic
stream statistics. Each side records its own device information. Matching
session IDs and timestamps help compare the reports without changing the
wire protocol. Logs survive app restarts and rotate automatically. Windows
keeps three files up to 2 MiB each; iPad keeps three up to 1 MiB each.

Pairing codes and tokens are redacted from saved support logs and exports.
Reports still contain PC/device names and local network addresses. Review
them before posting publicly. Keep the QR code private because anyone who
scans it can connect.

Pairing keeps other people on your network from connecting, but the stream
itself is not encrypted. Use EternalMonitor on networks you trust, such as
your home Wi-Fi.

### Automatic diagnostics

Configured releases send device information, errors and stream statistics to EternalMonitor support automatically. Windows Settings and iPad Settings → Diagnostics show the current upload state and an automatic sharing control. Offline events retry after reconnection or relaunch. PC names and network addresses may be included; pairing credentials, screen frames and audio are excluded. Manual exports remain available.

Uploads use encrypted HTTPS. Support history is retained for up to 30 days and may be removed sooner when storage limits are reached. Disabling sharing clears pending uploads; diagnostics already sent expire under the retention policy. Existing releases require an update to enable uploads.
6 changes: 4 additions & 2 deletions docs/host-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

The host offers 30, 60, 90 and 120 fps. An iPad requesting 30, 60 or 120 fps caps the host at the lower of the two settings. Disconnecting clears the client cap without changing the host preference. The iPad's Frame Rate setting applies on the next connection.

Encoder input defaults to YUV420. BGRA and Auto are opt-in until both NVENC and AMF pass the reference PC's color comparisons and ten-minute stability runs. Auto chooses BGRA, then BGR0, only when the encoder advertises support. It otherwise uses YUV420P. libx264 always uses YUV420P. BGRA avoids the host's color conversion step. An unsupported forced BGRA setting reports an encoder error. Changing this setting requires a stream restart; the Settings page offers Restart now. `ETERNAL_INPUT=auto`, `bgra` or `yuv420` overrides the saved setting at launch.
Encoder input defaults to YUV420. BGRA and Auto are opt-in until both NVENC and AMF pass the reference PC's color comparisons and ten-minute stability runs. Auto chooses BGRA, then BGR0, only when the encoder advertises support. It otherwise chooses an advertised 8-bit 4:2:0 format, preferring YUV420P and using NV12 for Intel Quick Sync. The default YUV420 setting follows the same format selection. libx264 always uses YUV420P. BGRA avoids the host's color conversion step. An unsupported forced BGRA setting reports an encoder error. Changing this setting requires a stream restart; the Settings page offers Restart now. `ETERNAL_INPUT=auto`, `bgra` or `yuv420` overrides the saved setting at launch.

At stream startup the host tries the selected hardware encoder, then the other Windows hardware backends if it cannot open. It uses libx264 only after those attempts fail. Selecting libx264 explicitly keeps CPU encoding. The session log records each failed encoder and the one that opened.

The Connected iPad card shows the latest receiver report. Unrecovered loss counts fragments still missing after repair. Repaired fragments have their own percentage. Both percentages use received, repaired and still-missing fragments as the total. A zero latency estimate displays Measuring until a measurement is available.

The update check runs in a background thread with a three-second request timeout. It checks GitHub's latest stable release at most once per day, including after a failed check or an application restart. Turn off Check for updates in Settings to opt out. Dismiss hides the current banner for the rest of that host session. The host and iPad versions must match.

Each host launch rotates `eternal-host-session.log` into `.1`, retaining `.2` as well. The Windows headless host handles Ctrl+C and Ctrl+Break through the normal shutdown path.
Each host launch and each 2 MiB of output rotates `eternal-host-session.log` into `.1`, retaining `.2` as well. Copy logs includes these files, startup evidence and the most recent memory log so queued errors are included. Reports expose queue drops and storage failures. Saved support logs and exports redact pairing codes and tokens; the headless console still prints the pairing code for local automation. Startup reports include Windows build, CPU/RAM, all DXGI adapters and driver versions, FFmpeg library versions and compiled encoder availability. Encoder attempts include advertised pixel formats and numeric FFmpeg errors; native FFmpeg warnings and errors enter the same support log. A ten-second summary records session ID, actual codec, capture/encode/send counters, USB state, heartbeat ages and effective frame/packet settings. The Windows headless host handles Ctrl+C and Ctrl+Break through the normal shutdown path.

The installer registers UDP and TCP firewall rules for its installed host executable on private and public networks. Uninstall removes those rules. It also grants local users read and execute access to its display tasks. A failed task produces the reinstall guidance and records Windows' exact error in the host log. A task that runs but times out waiting for a display instead asks the user to check the display and driver.

Expand Down
4 changes: 4 additions & 0 deletions host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,15 @@ mdns-sd = "0.13.11"
# Shared state
parking_lot = "0.12"
once_cell = "1"
regex = "1"

# Socket options (send-buffer sizing) before handing the socket to tokio
socket2 = "0.5"

[build-dependencies]
winresource = "0.1"
cc = "1"
pkg-config = "0.3"

[target.'cfg(target_os = "macos")'.dev-dependencies]
egui_kittest = { version = "0.36", features = ["eframe", "wgpu", "snapshot"] }
Expand All @@ -63,6 +66,7 @@ features = [
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Gdi",
"Win32_System_Registry",
"Win32_System_SystemInformation",
"Win32_System_IO",
"Win32_NetworkManagement_IpHelper",
"Win32_Networking_WinSock",
Expand Down
46 changes: 46 additions & 0 deletions host/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,50 @@
fn main() {
println!("cargo:rerun-if-env-changed=ETERNAL_BUILD_REVISION");
println!("cargo:rerun-if-changed=src");
for args in [
vec!["rev-parse", "--git-path", "HEAD"],
vec!["rev-parse", "--git-path", "refs"],
] {
if let Ok(output) = std::process::Command::new("git").args(args).output() {
if output.status.success() {
println!(
"cargo:rerun-if-changed={}",
String::from_utf8_lossy(&output.stdout).trim()
);
}
}
}
let revision = std::env::var("ETERNAL_BUILD_REVISION").unwrap_or_else(|_| {
let hash = std::process::Command::new("git")
.args(["rev-parse", "--short=12", "HEAD"])
.output()
.ok()
.filter(|output| output.status.success())
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
.unwrap_or_else(|| "unknown".to_string());
let dirty = std::process::Command::new("git")
.args(["diff", "--quiet", "HEAD"])
.status()
.is_ok_and(|status| !status.success());
format!("{hash}{}", if dirty { "+dirty" } else { "" })
});
println!("cargo:rustc-env=ETERNAL_BUILD_REVISION={revision}");
println!("cargo:rerun-if-changed=src/ffmpeg_log.c");
println!("cargo:rerun-if-env-changed=FFMPEG_DIR");
let mut native = cc::Build::new();
native.file("src/ffmpeg_log.c");
if let Some(root) = std::env::var_os("FFMPEG_DIR") {
native.include(std::path::PathBuf::from(root).join("include"));
} else {
let library = pkg_config::Config::new()
.probe("libavutil")
.expect("libavutil headers");
for path in library.include_paths {
native.include(path);
}
}
native.compile("eternal_ffmpeg_log");

if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
let mut res = winresource::WindowsResource::new();
res.set_icon("assets/icon.ico");
Expand Down
8 changes: 8 additions & 0 deletions host/src/audio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ pub struct AudioStats {
pub packets: u64,
pub quiet_packets: u64,
pub dropped: u64,
pub capture_queue_dropped: u64,
pub stale_dropped: u64,
pub oversized_dropped: u64,
pub send_failed: u64,
samples: VecDeque<(Instant, usize)>,
}

Expand All @@ -73,6 +77,10 @@ impl Default for AudioStats {
packets: 0,
quiet_packets: 0,
dropped: 0,
capture_queue_dropped: 0,
stale_dropped: 0,
oversized_dropped: 0,
send_failed: 0,
samples: VecDeque::with_capacity(64),
}
}
Expand Down
14 changes: 9 additions & 5 deletions host/src/audio/stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ impl CaptureSession {
Ok(()) => self.mark_discontinuity = false,
Err(TrySendError::Full(_)) => {
self.mark_discontinuity = true;
PIPELINE_STATS.lock().audio.dropped += 1;
let mut stats = PIPELINE_STATS.lock();
stats.audio.dropped += 1;
stats.audio.capture_queue_dropped += 1;
}
Err(TrySendError::Closed(_)) => return Ok(false),
}
Expand Down Expand Up @@ -254,10 +256,12 @@ pub fn run_audio_stage(
}
if Instant::now() >= next_log {
next_log = Instant::now() + Duration::from_secs(1);
let stats = PIPELINE_STATS.lock();
tracing::info!(device = %stats.audio.device, kbps = stats.audio.kbps,
packets_per_sec = stats.audio.packets_per_sec, quiet_packets = stats.audio.quiet_packets,
dropped = stats.audio.dropped, "Audio stream stats");
let stats = PIPELINE_STATS.lock().audio.clone();
tracing::info!(device = %stats.device, kbps = stats.kbps,
packets_per_sec = stats.packets_per_sec, quiet_packets = stats.quiet_packets,
dropped = stats.dropped, capture_queue_dropped = stats.capture_queue_dropped,
stale_dropped = stats.stale_dropped, oversized_dropped = stats.oversized_dropped,
send_failed = stats.send_failed, "Audio stream stats");
}
}
drop(active);
Expand Down
Loading
Loading