1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 04:57:40 +03:00
This commit is contained in:
Anton Afanasyeu
2026-05-21 11:21:56 +02:00
parent ee4947a348
commit e2d51fbb27
11 changed files with 591 additions and 38 deletions

275
AV_QUALITY_QA_SESSION.md Normal file
View File

@@ -0,0 +1,275 @@
# AV quality enhancement — Q&A session notes
Research and recommendations for **movie cast** vs **video cast** quality enhancement in the Android Cast project. Combines user requirements, comparison tables, algorithm choices, placement (sender vs receiver), and fit with the current codebase.
**Status:** Planning / research only — not implemented.
**Related code (today):** sender `ScreenCastService``VideoEncoder` / `AudioEncoder` → network; receiver network → `VideoDecoder` / `LibvpxCapableVideoDecoder` / `AudioDecoder``TextureView` / `AudioTrack`.
---
## 1. User requirements (preliminary conditions)
1. Content is either **movie cast** or **video cast** (live screen/camera); **both** must be supported with different tuning.
2. **Both** video and audio need quality enhancement.
3. Two processing shapes:
- **2.1 Temporal (N=2 frames/windows):** small gap between sequential video frames → predict/enhance using deltas between frames; same idea for audio (overlapping buffers).
- **2.2 Single frame/chunk:** static image from sink/source; one video frame; one audio PCM chunk.
4. **2.3 Reassembly:** output stream must keep **original PTS/DTS** (or fixed delay with uniform compensation); avoid changing frame/sample count on live paths unless explicitly designed.
**Comparison dimensions requested:** implementation details, difficulty, realtime on modern ARM (GPU Android/Linux where applicable), input vs output quality, single vs 2-frame vs stream (with delay), multithreading.
---
## 2. Mode overview
| Mode | Video input | Audio input | Typical delay | Best when |
|------|-------------|-------------|---------------|-----------|
| **2.2 Single** | One buffer / frame | 1040 ms PCM chunk | **01 frame** (~033 ms @ 30 fps) | Live **video cast**, UI, games |
| **2.1 Two-frame (N=2)** | Pair with small temporal gap | Overlapping windows | **~1 frame** (+ overlap) | Motion; mild temporal help |
| **Stream (38)** | Short FIFO | Ring + STFT state | **100400 ms+** | **Movie cast**, VOD-like |
| **Heavy stream ML** | Long context | Streaming neural | **200 ms2 s+** | Offline / non-live only |
**Quality (typical):** stream/temporal > 2-frame > single frame
**Latency (best first):** single frame > 2-frame > stream
**Policy split:**
| Cast type | Video | Audio |
|-----------|-------|-------|
| **Video cast (live)** | 2.2 default; optional light 2.1 (+1 frame max) | 2.2 (**RNNoise** + limiter) |
| **Movie cast** | 2.1 or 34 frame **TNR** / **MCTF-lite** | 2-window spectral or **DeepFilterNet2** streaming |
---
## 3. Video — method comparison
| Method | Implementation | Difficulty | ARM @ 720p30 | ARM @ 1080p30 | GPU (Android/Linux) | Quality vs input | PTS/DTS | Multithread |
|--------|----------------|------------|--------------|---------------|---------------------|------------------|---------|-------------|
| **Single: bilateral / guided filter denoise** | GLES/Vulkan on YUV; NEON fallback | Low | Yes | Yes (tuned) | **High** | Smallmedium | Trivial | Yes (tiles) |
| **Single: tone / CLAHE-lite** | LUT + luma histogram tiles | Lowmed | Yes | Yes | Medium | Medium in flat scenes | Trivial | Yes |
| **Single: light SR (ESPCN-class tiny CNN)** | TFLite/NCNN → NNAPI/GPU | Medium | Maybe | Tight | High | Medium (edges/text) | Trivial | Yes (async infer) |
| **Single: heavy SR (Real-ESRGAN, SwinIR)** | Large CNN | High | No | No | BW-bound | High but slow | Trivial | Limited |
| **2-frame: DIS / Farneback flow + warp blend** | Flow on downscaled luma → warp prev | Medium | Yes | Maybe | Medhigh | Medium | **+1 frame** | Yes |
| **2-frame: residual / delta enhance** | `enhanced = f(curr) + α·(curr warped_prev)` | Medium | Yes | Maybe | Medium | Medhigh on repetitive motion | **+1 frame** | Yes |
| **2-frame: RIFE-lite / FILM-small** | Interpolate between frames | Medhigh | Borderline 720p | Unlikely 1080p30 | High if custom | High for large gaps | Timing care | Yes |
| **Stream: IIR TNR / motion-mask accumulate** | 34 frame buffer | Medium | Yes | Maybe | High | High static; trail risk | **+23 frames** | Yes |
| **Stream: ML temporal (VRT/RVRT-class)** | Stateful temporal CNN | Very high | No live | No | Medium | Very high | Buffer delay | Some |
---
## 4. Audio — method comparison
| Method | Implementation | Difficulty | ARM realtime 48 kHz | GPU | Quality vs input | PTS/DTS | Multithread |
|--------|----------------|------------|---------------------|-----|------------------|---------|-------------|
| **Single: DC block + EQ + limiter** | Biquads per chunk | Low | Yes | Low | Small | Preserve samples | Yes |
| **Single: RNNoise / DeepFilterNet (10 ms)** | TFLite/ONNX streaming | Medium | Yes mono; maybe stereo | NNAPI | High for noise | +1020 ms | Yes (infer thread) |
| **2-window: Wiener / min-stats STFT** | 20 ms, 50% hop | Medium | Yes | Low | Medhigh | +1 hop (~10 ms) | Yes |
| **2-window: PLC** | Predict from last 2 frames | Lowmed | Yes | Low | High only on loss | Keep clock | Yes |
| **Stream: multiband + AGC** | Ring buffer envelopes | Lowmed | Yes | Low | Medium intelligibility | 530 ms | Yes |
| **Stream: DeepFilterNet2** | Stateful STFT+GRU | Medhigh | Yes modern ARM | NNAPI | High | 2040 ms | Yes |
---
## 5. Single vs 2-frame vs stream — decision matrix
| Criterion | Single (2.2) | 2-frame (2.1) | Stream (38) |
|-----------|--------------|---------------|--------------|
| Implementation complexity | Lowest | Medium | Highest |
| Encoder integration | Easiest | +1 frame pipeline | Delay queue |
| ARM realtime 720p30 | **Best** | Good | Good (TNR); poor heavy ML |
| ARM realtime 1080p30 | Good | Marginal | Marginal |
| GPU fit | **Best** | Good (warp) | Good (TNR) |
| Quality gain (video) | Lowmedium | Medium + motion | High static / medium action |
| Quality gain (audio) | Medium | Medhigh | High steady noise |
| Artifact risk | Banding, oversharpen | Ghosting, warp error | Trails, smear |
| PTS/DTS | Trivial | Fixed +1 frame delay | Uniform delay all outputs |
| Multithread | Capture ∥ GPU ∥ encode | Flow ∥ enhance | Buffer thread + pool |
| **Movie cast** | OK quick polish | **Recommended** | **Recommended** if delay OK |
| **Video cast** | **Recommended** | Optional 1-frame | **Not recommended** live |
---
## 6. PTS/DTS reassembly (2.3)
| Approach | Description | Modes |
|----------|-------------|-------|
| **In-place payload replace** | Same PTS/DTS, duration; swap enhanced payload | Single |
| **Delay compensation** | Queue enhanced frames; release with **original PTS** | 2-frame, stream |
| **Composition timestamp** | `presentationTimeUs` from source → enhanced buffer (MediaCodec) | All on Android |
| **Audio** | Keep **sample count** constant → PTS unchanged | Prefer for live |
**Rule:** Do not change frame/sample count on live cast unless explicit insert/drop policy exists.
---
## 7. Pipeline placement: sender (2.1) vs receiver (2.2)
### User question
- **2.1 Sender:** enhancement between capture (camera/screen) and encoder.
- **2.2 Receiver:** enhancement between decoded stream and renderer.
- **User intuition:** 2.1 has limited value because encoder lossy output destroys pre-enhance detail; increases sender load.
### Assessment
| Claim | Verdict |
|-------|---------|
| Encoder “kills” sharpen/SR/local contrast pushed pre-encode | **True** |
| 2.1 is pointless | **False** for **denoise-before-encode** (helps bitrate/artifacts at same rate) |
| 2.1 burns sender CPU for little gain | **Often true** on thermally limited phones |
### Preference (consensus from session)
| Placement | Role |
|-----------|------|
| **Receiver (2.2)** | **Primary perceived quality** — deblock, denoise, mild sharpen; optional 2-frame for movie |
| **Sender (2.1)** | **Optional compression prep only** (bilateral/NLMeans denoise), not main beauty pipeline |
| **Both** | Receiver polishes; sender light or off on live video cast |
**Current app hooks:**
| Pipeline | Integration point | Difficulty (1=easy, 5=hard) |
|----------|-------------------|------------------------------|
| **2.2 Video receiver** | After `VideoDecoder` / `LibvpxCapableVideoDecoder`, before `TextureView` | **34** (new GLES: decode → FBO → display) |
| **2.2 Audio receiver** | `AudioDecoder` PCM before `AudioTrack` | **2** |
| **2.1 Video sender** | Between capture and `VideoEncoder.prepare()` Surface | **45** (intermediate EGL Surface, rotation) |
| **2.1 Audio sender** | Before `AudioEncoder` | **23** |
| **2-frame video** | Ring buffer +1 frame delay | **+1** on above |
**Recommended implementation fork:** decoded-buffer → **GLES** → Surface (consistent for MediaCodec and libvpx VP9), not only `TextureView` overlay.
---
## 8. Named algorithm pipelines (best practices)
### A. Receiver — live video cast (default)
**Video (2.2, GPU-first):**
1. Post-decode **deblocking filter** (lite H.264-style / mild dering; VP9 loop filter already in decode)
2. **Guided filter** or **bilateral filter** (edge-preserving denoise) on luma
3. Mild **unsharp mask** (limit on blocky regions)
4. Optional: **gamma** / **BT.1886** + **CLAHE-lite**
**Audio (2.2):**
1. **RNNoise** (10 ms frames)
2. **Limiter** + high-pass (DC removal)
3. Optional: **SpeexDSP** `preprocess` (AGC, mild denoise)
### B. Receiver — movie cast (+1 frame delay)
**Video (2.1):**
1. **DIS** or **Farneback** optical flow (downscaled)
2. **MCTF-lite** or **IIR temporal denoise** on stable regions
3. Spatial chain from A (guided/bilateral + mild unsharp)
**Audio (2.1):**
1. **STFT overlap-add** + **Wiener filter**, or **DeepFilterNet2** (streaming)
2. **Multiband compressor**
### C. Sender — optional compression prep only
**Video:** **Bilateral** or fast **NLMeans** only when bitrate-starved — **no** Real-ESRGAN / strong sharpen pre-encode.
**Audio:** **SpeexDSP preprocess** or light **RNNoise** before AAC.
### D. Avoid for realtime in this project
| Algorithm | Reason |
|-----------|--------|
| **Real-ESRGAN**, **SwinIR**, **BasicVSR++** | Too heavy ARM @ 720p1080p30 live |
| **RIFE** / **FILM** @ 1080p30 full rate | Borderline; movie-only, lower rate |
| **BM3D**, **VRT/RVRT** | Offline-tier |
| Heavy **2.1 sender SR** before VP9 | Encoder erases; thermals |
---
## 9. Ranked preferences (quality benefit vs implementation in android cast)
Descending order — build in this sequence:
| Rank | Choice | Placement | Core algorithms | Quality benefit | Impl difficulty (this app) |
|------|--------|-----------|-----------------|-----------------|---------------------------|
| **1** | Receiver video **2.2** (GPU) | 2.2 | Guided/bilateral + deblock + mild unsharp | High vs blocky stream | **34** |
| **2** | Receiver audio **2.2** | 2.2 | RNNoise + limiter (+ SpeexDSP) | High for noise | **2** |
| **3** | Receiver video **2.1** (movie) | 2.2 + 1f delay | DIS/Farneback + MCTF-lite / IIR TNR + spatial | Higher static/grain | **4** |
| **4** | Receiver audio **2.1** (movie) | 2.2 | DeepFilterNet2 or Wiener STFT | Mediumhigh | **3** |
| **5** | Sender audio light prep | 2.1 | SpeexDSP / RNNoise → AAC | Lowmedium | **23** |
| **6** | Sender video denoise only | 2.1 | Bilateral / fast NLMeans (not SR) | Lowmedium (bitrate) | **45** |
| **7** | Sender video **2.1** beauty | 2.1 | Flow + enhance pre-encode | Low (washed by codec) | **5** |
| **8** | Heavy SR either side | either | Real-ESRGAN class | High offline only | **5**, not realtime |
---
## 10. Multithreading pipeline (target architecture)
```
[Capture / Network decode] → queue → [Enhance worker(s)] → queue → [Encode / Render]
N=2: previous frame
Stream: ring buffer
```
| Stage | Threads | Notes |
|-------|---------|-------|
| Capture / decode callback | 1 | Non-blocking |
| Enhance (GPU) | 12 | Double-buffered FBO / AHardwareBuffer |
| CPU fallback (NEON) | 24 | Tile-based |
| ML audio | 1 | Async from video |
| Encode / display | 1 | MediaCodec async; TextureView main |
**Golden rule:** max **one frame** enhancement queue for **video cast**; **24 frames** for **movie cast**.
Receiver already has `videoDecodeThread` in `ReceiverCastService` — natural host for GL enhance stage.
---
## 11. Short answers to numbered conditions
| # | Requirement | Preference |
|---|-------------|------------|
| 1 | Movie or video cast | Different latency policies; same codebase, different presets |
| 2 | Process both A+V | Shared audio chain; video policy switches by cast mode |
| 2.1.1 | 2-frame video deltas | **DIS/Farneback + residual** — not full RIFE on ARM live |
| 2.1.2 | 2-frame audio | Overlap-add STFT or streaming **DeepFilterNet** |
| 2.2.1 | Single image video | **GPU bilateral/guided + tone**; tiny SR @ 720p only if ever |
| 2.2.2 | Single chunk audio | **RNNoise + limiter** |
| 2.3 | PTS/DTS reassembly | In-place or fixed-delay queue; no timeline shift on live |
---
## 12. Bottom line
| Goal | Best option |
|------|-------------|
| Lowest latency + ARM-safe | **Single frame receiver (2.2)** + GPU shaders |
| Best quality per ms delay | **2-frame receiver (2.1)** + 2-window/stream audio |
| Best absolute quality (movie) | Small **stream buffer (34)** video TNR + streaming audio ML |
| Avoid realtime cast | Heavy per-frame SR, RIFE @ 1080p30, deep temporal video CNN |
| **Placement** | **Receiver-first**; sender **denoise-only** at most |
**Users encoder observation is correct for “beauty” enhancement pre-encode;** denoise-before-encode remains the only strong sender-side exception.
---
## 13. Other work in same project session (context only)
Unrelated to AV enhancement but done in parallel on this branch:
- OTA optional `.otabundle.zip`, crash reporter (`:crashwatcher`), HEADER.txt automation, PHP crash backend — see `docs/OTA.md`, `docs/CRASH_REPORTER.md`, `examples/crash_reporter/`.
---
## 14. Open decisions when implementation starts
1. Enhance on **display path** (`TextureView`) vs **decode buffer → GLES → Surface** (recommended: latter).
2. User-toggle vs automatic preset for movie vs video cast.
3. Whether sender **denoise-only** preset is worth thermals on low-end devices.
4. GPU API: **OpenGL ES 3.x compute** vs **Vulkan** (ES3 often faster to integrate in Android media apps).
---
*Document generated from AV quality Q&A session. Update as prototypes prove FPS/latency on target devices.*

View File

@@ -25,7 +25,6 @@ import org.json.JSONObject;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.Locale; import java.util.Locale;
import java.util.UUID;
/** Builds anonymous crash report JSON (schema v1). */ /** Builds anonymous crash report JSON (schema v1). */
public final class CrashReportBuilder { public final class CrashReportBuilder {
@@ -85,19 +84,34 @@ public final class CrashReportBuilder {
JSONObject root = new JSONObject(); JSONObject root = new JSONObject();
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
String stamp = CrashReportStore.formatTimestamp(now);
root.put("schema_version", 1); root.put("schema_version", 1);
root.put("report_id", UUID.randomUUID().toString()); root.put("report_id", stamp);
root.put("generated_at_epoch_ms", now); root.put("generated_at_epoch_ms", now);
root.put("crash_type", crashType); root.put("crash_type", crashType);
root.put("process", "main"); root.put("scenario", CrashRuntimeContext.scenarioLabel());
root.put("process", currentProcessName());
root.put("device", deviceJson()); root.put("device", deviceJson());
root.put("app", appJson(context)); root.put("app", appJson(context));
root.put("build", buildJson()); root.put("build", buildJson());
root.put("runtime_context", CrashRuntimeContext.snapshot(context));
} catch (Exception ignored) { } catch (Exception ignored) {
} }
return root; return root;
} }
private static String currentProcessName() {
try {
Class<?> at = Class.forName("android.app.ActivityThread");
Object proc = at.getMethod("currentProcessName").invoke(null);
if (proc instanceof String && !((String) proc).isEmpty()) {
return (String) proc;
}
} catch (Exception ignored) {
}
return "main";
}
private static JSONObject deviceJson() throws Exception { private static JSONObject deviceJson() throws Exception {
JSONObject d = new JSONObject(); JSONObject d = new JSONObject();
d.put("manufacturer", Build.MANUFACTURER); d.put("manufacturer", Build.MANUFACTURER);

View File

@@ -17,16 +17,22 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale;
/** Pending/uploaded crash JSON in app-private storage (shared across processes). */ /** Pending/uploaded crash JSON in app-private storage (shared across processes). */
public final class CrashReportStore { public final class CrashReportStore {
private static final String ROOT = "crash_reports"; private static final String ROOT = "crash_reports";
private static final String PENDING = "pending"; private static final String PENDING = "pending";
private static final String UPLOADED = "uploaded"; private static final String UPLOADED = "uploaded";
/** Same stamp as {@code session_send_yyyyMMdd_HHmmss.json} / {@code session_recv_*.json}. */
public static final String PREFIX = "crash_";
private static final String SUFFIX = ".json";
private CrashReportStore() {} private CrashReportStore() {}
@@ -46,8 +52,21 @@ public final class CrashReportStore {
return dir; return dir;
} }
public static File newPendingFile(File filesDir, String reportId) { public static String formatTimestamp(long epochMs) {
return new File(pendingDir(filesDir), "crash_" + reportId + ".json"); return new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date(epochMs));
}
/** {@code crash_yyyyMMdd_HHmmss.json} (suffix {@code _N} if the second collides). */
public static File newPendingFile(File filesDir, long generatedAtMs) {
String base = PREFIX + formatTimestamp(generatedAtMs);
File dir = pendingDir(filesDir);
File candidate = new File(dir, base + SUFFIX);
int n = 1;
while (candidate.exists()) {
candidate = new File(dir, base + "_" + n + SUFFIX);
n++;
}
return candidate;
} }
public static void writeJson(File file, JSONObject json) throws Exception { public static void writeJson(File file, JSONObject json) throws Exception {
@@ -71,7 +90,7 @@ public final class CrashReportStore {
public static List<File> listPending(File filesDir) { public static List<File> listPending(File filesDir) {
File dir = pendingDir(filesDir); File dir = pendingDir(filesDir);
File[] files = dir.listFiles((d, n) -> n.endsWith(".json")); File[] files = dir.listFiles((d, n) -> n.startsWith(PREFIX) && n.endsWith(SUFFIX));
if (files == null || files.length == 0) { if (files == null || files.length == 0) {
return Collections.emptyList(); return Collections.emptyList();
} }

View File

@@ -29,6 +29,9 @@ public final class CrashReporter {
public static void install(Context context) { public static void install(Context context) {
Context app = context.getApplicationContext(); Context app = context.getApplicationContext();
if (app instanceof android.app.Application) {
CrashRuntimeContext.init((android.app.Application) app);
}
CrashNativeBridge.init(app); CrashNativeBridge.init(app);
final Thread.UncaughtExceptionHandler previous = final Thread.UncaughtExceptionHandler previous =
Thread.getDefaultUncaughtExceptionHandler(); Thread.getDefaultUncaughtExceptionHandler();
@@ -70,8 +73,12 @@ public final class CrashReporter {
return; return;
} }
try { try {
String id = report.optString("report_id", String.valueOf(System.currentTimeMillis())); long generatedAt = report.optLong("generated_at_epoch_ms", System.currentTimeMillis());
File file = CrashReportStore.newPendingFile(context.getFilesDir(), id); File file = CrashReportStore.newPendingFile(context.getFilesDir(), generatedAt);
report.put("report_id", file.getName().substring(
CrashReportStore.PREFIX.length(),
file.getName().length() - ".json".length()));
report.put("report_file", file.getName());
CrashReportStore.writeJson(file, report); CrashReportStore.writeJson(file, report);
CrashReportStore.pruneOldestPending(context.getFilesDir(), CrashReportStore.pruneOldestPending(context.getFilesDir(),
CrashSettingsStore.load(context).maxPendingFiles); CrashSettingsStore.load(context).maxPendingFiles);

View File

@@ -0,0 +1,108 @@
package com.foxx.androidcast.crash;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import com.foxx.androidcast.CastActiveState;
import com.foxx.androidcast.stats.SessionStatsContext;
import com.foxx.androidcast.stats.SessionStatsRecorder;
import com.foxx.androidcast.stats.SessionStatsStore;
import org.json.JSONObject;
import java.io.File;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
/**
* App-wide runtime snapshot for crash reports (cast idle, UI foreground, last session file).
* Updated from {@link AndroidCastApplication} lifecycle and cast services.
*/
public final class CrashRuntimeContext {
private static final AtomicLong APP_STARTED_MS = new AtomicLong(0L);
private static final AtomicReference<String> LAST_ACTIVITY = new AtomicReference<>("");
private CrashRuntimeContext() {}
public static void init(Application app) {
APP_STARTED_MS.compareAndSet(0L, System.currentTimeMillis());
app.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityResumed(Activity activity) {
LAST_ACTIVITY.set(activity.getClass().getSimpleName());
}
@Override
public void onActivityCreated(Activity a, Bundle b) {}
@Override
public void onActivityStarted(Activity a) {}
@Override
public void onActivityPaused(Activity a) {}
@Override
public void onActivityStopped(Activity a) {}
@Override
public void onActivitySaveInstanceState(Activity a, Bundle b) {}
@Override
public void onActivityDestroyed(Activity a) {}
});
}
public static boolean isCastSessionActive() {
return CastActiveState.isSenderCasting() || CastActiveState.isReceiverListening();
}
/** JSON attached to every crash report (with or without an active cast stats recorder). */
public static JSONObject snapshot(Context context) {
JSONObject runtime = new JSONObject();
try {
long started = APP_STARTED_MS.get();
if (started <= 0L) {
started = System.currentTimeMillis();
}
runtime.put("app_started_epoch_ms", started);
runtime.put("last_activity", LAST_ACTIVITY.get());
runtime.put("sender_cast_active", CastActiveState.isSenderCasting());
runtime.put("receiver_cast_active", CastActiveState.isReceiverListening());
runtime.put("cast_active", isCastSessionActive());
SessionStatsRecorder active = SessionStatsContext.get();
runtime.put("session_stats_active", active != null);
if (active != null) {
runtime.put("session_direction", active.getDirection());
}
String lastSession = lastSessionFileName(context);
if (!lastSession.isEmpty()) {
runtime.put("last_session_file", lastSession);
}
} catch (Exception ignored) {
}
return runtime;
}
public static String scenarioLabel() {
if (SessionStatsContext.get() != null || isCastSessionActive()) {
return "cast_session";
}
return "app_runtime";
}
private static String lastSessionFileName(Context context) {
try {
List<File> files = SessionStatsStore.listSessionFiles(context);
if (files.isEmpty()) {
return "";
}
return files.get(0).getName();
} catch (Exception e) {
return "";
}
}
}

View File

@@ -127,8 +127,16 @@ public final class CrashWatcherService extends Service {
signal = text.substring(sigIdx + 7, end > sigIdx ? end : text.length()).trim(); signal = text.substring(sigIdx + 7, end > sigIdx ? end : text.length()).trim();
} }
JSONObject report = CrashReportBuilder.buildNativeCrash(this, signal, text); JSONObject report = CrashReportBuilder.buildNativeCrash(this, signal, text);
String id = report.optString("report_id", stub.getName()); long generatedAt = report.optLong("generated_at_epoch_ms", stub.lastModified());
CrashReportStore.writeJson(CrashReportStore.newPendingFile(filesDir, id), report); if (generatedAt <= 0L) {
generatedAt = System.currentTimeMillis();
}
File pending = CrashReportStore.newPendingFile(filesDir, generatedAt);
report.put("report_id", pending.getName().substring(
CrashReportStore.PREFIX.length(),
pending.getName().length() - ".json".length()));
report.put("report_file", pending.getName());
CrashReportStore.writeJson(pending, report);
stub.delete(); stub.delete();
} catch (Exception e) { } catch (Exception e) {
Log.w(TAG, "ingest native stub failed: " + stub.getName()); Log.w(TAG, "ingest native stub failed: " + stub.getName());

View File

@@ -0,0 +1,29 @@
package com.foxx.androidcast.crash;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
public class CrashReportStoreTest {
@org.junit.Rule
public TemporaryFolder tmp = new TemporaryFolder();
@Test
public void pendingFile_usesSessionStyleTimestamp() throws Exception {
File files = tmp.newFolder("files");
long ms = 1_704_067_200_000L; // 2024-01-01 00:00:00 UTC — pattern only
File f = CrashReportStore.newPendingFile(files, ms);
assertTrue(f.getName().matches("crash_\\d{8}_\\d{6}\\.json"));
}
@Test
public void formatTimestamp_matchesSessionStatsPattern() {
String stamp = CrashReportStore.formatTimestamp(171_000_000_0000L);
assertEquals(15, stamp.length());
assertTrue(stamp.contains("_"));
}
}

View File

@@ -2,7 +2,8 @@
## Android ## Android
- **Main process:** `CrashReporter.install()`Java uncaught exceptions → JSON under `files/crash_reports/pending/`, session stats flushed with `globals.reason: "crash"`. - **Main process:** `CrashReporter.install()`any uncaught Java exception (cast or idle UI) → `files/crash_reports/pending/crash_yyyyMMdd_HHmmss.json` (same stamp as `session_*.json`). Active cast session stats are flushed with `globals.reason: "crash"` when applicable.
- **Runtime context:** every report includes `runtime_context` (foreground activity, `CastActiveState`, last `session_*.json` name) and `scenario` (`cast_session` vs `app_runtime`).
- **Watcher process:** `CrashWatcherService` in `:crashwatcher` — reads `settings.json``crash`, uploads pending reports, ingests native stub files. - **Watcher process:** `CrashWatcherService` in `:crashwatcher` — reads `settings.json``crash`, uploads pending reports, ingests native stub files.
- **Native:** `crash_hook.c` (unwind + `dladdr`) writes stubs to `crash_reports/native/`; watcher converts to schema v1 JSON. - **Native:** `crash_hook.c` (unwind + `dladdr`) writes stubs to `crash_reports/native/`; watcher converts to schema v1 JSON.
- **User toggle:** Settings drawer → **Send anonymous crash logs** (`AppPreferences.send_anonymous_crash_logs`, default on). - **User toggle:** Settings drawer → **Send anonymous crash logs** (`AppPreferences.send_anonymous_crash_logs`, default on).
@@ -24,3 +25,12 @@ Deploy tuning via app-private `files/settings.json` (see `examples/settings.json
PHP console and ingest API: `examples/crash_reporter/backend/` — see [README](../examples/crash_reporter/backend/README.md). PHP console and ingest API: `examples/crash_reporter/backend/` — see [README](../examples/crash_reporter/backend/README.md).
Roadmap: `examples/crash_reporter/ROADMAP.md`. Roadmap: `examples/crash_reporter/ROADMAP.md`.
## Report files
| Pattern | Example |
|---------|---------|
| Pending crash JSON | `crash_20260520_153045.json` |
| Session stats (compare) | `session_send_20260520_120000.json` |
JSON fields: `schema_version`, `report_id`, `report_file`, `generated_at_epoch_ms`, `scenario`, `runtime_context`, optional `session_context`, `java` / `native`, `device`, `app`, `build`, `fingerprint`.

View File

@@ -3,8 +3,19 @@
"report_id": "00000000-0000-4000-8000-000000000001", "report_id": "00000000-0000-4000-8000-000000000001",
"generated_at_epoch_ms": 1710000000000, "generated_at_epoch_ms": 1710000000000,
"crash_type": "java", "crash_type": "java",
"scenario": "app_runtime",
"process": "main", "process": "main",
"report_file": "crash_20260520_153000.json",
"fingerprint": "abc123", "fingerprint": "abc123",
"runtime_context": {
"app_started_epoch_ms": 1710000000000,
"last_activity": "MainActivity",
"sender_cast_active": false,
"receiver_cast_active": false,
"cast_active": false,
"session_stats_active": false,
"last_session_file": "session_send_20260520_120000.json"
},
"device": { "device": {
"manufacturer": "Doogee", "manufacturer": "Doogee",
"brand": "DOOGEE", "brand": "DOOGEE",

View File

@@ -1,48 +1,68 @@
#!/bin/bash #!/bin/bash
set -u
DEVICES_LIST="$(adb devices -l)" DEVICES_LIST="$(adb devices -l)"
echo "${DEVICES_LIST}" echo "${DEVICES_LIST}"
filter_out() {
local -n dev_string=${1}
[[ ${dev_string} =~ List* ]] && dev_string=""
return 0
}
LOG_ROOT="log_capture/session_stats/" LOG_ROOT="log_capture/session_stats/"
CLEAN_LOGS="no" CLEAN_LOGS="no"
while true; do while true; do
read -p "clean logs on device after fetch? [Y/n]" yn yn=""
case $yn in read -rp "clean logs on device after fetch? [Y/n] " yn
[Y|Yes|y|yes]* ) yn="${yn,,}"
case "${yn}" in
y|yes)
CLEAN_LOGS="yes" CLEAN_LOGS="yes"
break break
;; ;;
[N|No|n|no]* ) n|no)
break break
;; ;;
"")
CLEAN_LOGS="yes"
break
;;
*) *)
echo "answer Y(es) or N(no) (short y/n or yes/no)" echo "answer Y(es) or N(o) (y/n or yes/no)"
;; ;;
esac esac
done done
echo "${DEVICES_LIST}" | while IFS= read -r DEV; do echo "Known devices list:"
SERIAL="$(echo "${DEV}" | awk '{print $1}')" echo "${DEVICES_LIST}"
filter_out SERIAL
[ -z "${SERIAL}" ] && continue while IFS= read -r DEV; do
# echo "Device: ${DEV}" [[ -z "${DEV}" ]] && continue
# echo "Serial: ${SERIAL}" [[ "${DEV}" == List* ]] && continue
adb -s "${SERIAL}" shell run-as com.foxx.androidcast ls files/session_stats/
SERIAL="$(awk '{print $1}' <<<"${DEV}")"
STATE="$(awk '{print $2}' <<<"${DEV}")"
[[ "${STATE}" != "device" ]] && continue
LOG_PATH="${LOG_ROOT}/${SERIAL}/" LOG_PATH="${LOG_ROOT}/${SERIAL}/"
echo "------------ dumping in ${LOG_PATH}" echo "------------ dumping in ${LOG_PATH}"
mkdir -p "${LOG_PATH}" mkdir -p "${LOG_PATH}"
for f in $(adb -s ${SERIAL} shell run-as com.foxx.androidcast ls files/session_stats/); do
adb -s ${SERIAL} exec-out run-as com.foxx.androidcast cat "files/session_stats/$f" > "${LOG_PATH}/$f" mapfile -t FILES < <(adb -s "${SERIAL}" shell run-as com.foxx.androidcast ls files/session_stats/ </dev/null \
[[ "no" != "${CLEAN_LOGS}" ]] && adb -s ${SERIAL} shell run-as com.foxx.androidcast rm -f "files/session_stats/$f" | tr -d '\r' | grep -v '^$' || true)
if ((${#FILES[@]} == 0)); then
echo " (no session_stats files on ${SERIAL})"
continue
fi
for f in "${FILES[@]}"; do
adb -s "${SERIAL}" exec-out run-as com.foxx.androidcast cat "files/session_stats/${f}" </dev/null \
> "${LOG_PATH}/${f}"
if [[ "${CLEAN_LOGS}" == "yes" ]]; then
adb -s "${SERIAL}" shell run-as com.foxx.androidcast rm -f "files/session_stats/${f}" </dev/null
fi
done done
done done <<<"${DEVICES_LIST}"
echo "showing all the logs in ${LOG_ROOT} folder and derivatives:"
ls -a ${LOG_ROOT}/*/*
echo "run './gradlew :session-studio:run' to analyze logs; find the latest logs in ${LOG_ROOT}" echo "run './gradlew :session-studio:run' to analyze logs; find the latest logs in ${LOG_ROOT}"

52
scripts/header-post-commit.py Executable file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Post-commit: set Commit: SHA on sources touched in HEAD (amends commit once if needed)."""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
from header_lib import (
REPO_ROOT,
files_in_git_commit,
git_head_sha,
hook_skip_requested,
update_header_commit,
)
def main() -> int:
if hook_skip_requested():
return 0
sha = git_head_sha()
if not sha:
return 0
touched: list[Path] = []
for path in files_in_git_commit("HEAD"):
text = path.read_text(encoding="utf-8", errors="replace")
new_text = update_header_commit(text, path, sha)
if new_text != text:
path.write_text(new_text, encoding="utf-8", newline="\n")
touched.append(path)
if not touched:
return 0
env = os.environ.copy()
env["HEADER_HOOK_SKIP"] = "1"
for path in touched:
rel = path.relative_to(REPO_ROOT).as_posix()
subprocess.check_call(["git", "add", "--", rel], cwd=REPO_ROOT, env=env)
subprocess.check_call(
["git", "commit", "--amend", "--no-edit", "--no-verify"],
cwd=REPO_ROOT,
env=env,
)
return 0
if __name__ == "__main__":
sys.exit(main())