mirror of
git://f0xx.org/android_cast
synced 2026-07-29 02:18:40 +03:00
snap
This commit is contained in:
275
AV_QUALITY_QA_SESSION.md
Normal file
275
AV_QUALITY_QA_SESSION.md
Normal 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 | 10–40 ms PCM chunk | **0–1 frame** (~0–33 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 (3–8)** | Short FIFO | Ring + STFT state | **100–400 ms+** | **Movie cast**, VOD-like |
|
||||
| **Heavy stream ML** | Long context | Streaming neural | **200 ms–2 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 3–4 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** | Small–medium | Trivial | Yes (tiles) |
|
||||
| **Single: tone / CLAHE-lite** | LUT + luma histogram tiles | Low–med | 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 | Med–high | Medium | **+1 frame** | Yes |
|
||||
| **2-frame: residual / delta enhance** | `enhanced = f(curr) + α·(curr − warped_prev)` | Medium | Yes | Maybe | Medium | Med–high on repetitive motion | **+1 frame** | Yes |
|
||||
| **2-frame: RIFE-lite / FILM-small** | Interpolate between frames | Med–high | Borderline 720p | Unlikely 1080p30 | High if custom | High for large gaps | Timing care | Yes |
|
||||
| **Stream: IIR TNR / motion-mask accumulate** | 3–4 frame buffer | Medium | Yes | Maybe | High | High static; trail risk | **+2–3 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 | +10–20 ms | Yes (infer thread) |
|
||||
| **2-window: Wiener / min-stats STFT** | 20 ms, 50% hop | Medium | Yes | Low | Med–high | +1 hop (~10 ms) | Yes |
|
||||
| **2-window: PLC** | Predict from last 2 frames | Low–med | Yes | Low | High only on loss | Keep clock | Yes |
|
||||
| **Stream: multiband + AGC** | Ring buffer envelopes | Low–med | Yes | Low | Medium intelligibility | 5–30 ms | Yes |
|
||||
| **Stream: DeepFilterNet2** | Stateful STFT+GRU | Med–high | Yes modern ARM | NNAPI | High | 20–40 ms | Yes |
|
||||
|
||||
---
|
||||
|
||||
## 5. Single vs 2-frame vs stream — decision matrix
|
||||
|
||||
| Criterion | Single (2.2) | 2-frame (2.1) | Stream (3–8) |
|
||||
|-----------|--------------|---------------|--------------|
|
||||
| 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) | Low–medium | Medium + motion | High static / medium action |
|
||||
| Quality gain (audio) | Medium | Med–high | 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` | **3–4** (new GLES: decode → FBO → display) |
|
||||
| **2.2 Audio receiver** | `AudioDecoder` PCM before `AudioTrack` | **2** |
|
||||
| **2.1 Video sender** | Between capture and `VideoEncoder.prepare()` Surface | **4–5** (intermediate EGL Surface, rotation) |
|
||||
| **2.1 Audio sender** | Before `AudioEncoder` | **2–3** |
|
||||
| **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 @ 720p–1080p30 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 | **3–4** |
|
||||
| **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 | Medium–high | **3** |
|
||||
| **5** | Sender audio light prep | 2.1 | SpeexDSP / RNNoise → AAC | Low–medium | **2–3** |
|
||||
| **6** | Sender video denoise only | 2.1 | Bilateral / fast NLMeans (not SR) | Low–medium (bitrate) | **4–5** |
|
||||
| **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) | 1–2 | Double-buffered FBO / AHardwareBuffer |
|
||||
| CPU fallback (NEON) | 2–4 | 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**; **2–4 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 (3–4)** 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 |
|
||||
|
||||
**User’s 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.*
|
||||
@@ -25,7 +25,6 @@ import org.json.JSONObject;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Builds anonymous crash report JSON (schema v1). */
|
||||
public final class CrashReportBuilder {
|
||||
@@ -85,19 +84,34 @@ public final class CrashReportBuilder {
|
||||
JSONObject root = new JSONObject();
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
String stamp = CrashReportStore.formatTimestamp(now);
|
||||
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("crash_type", crashType);
|
||||
root.put("process", "main");
|
||||
root.put("scenario", CrashRuntimeContext.scenarioLabel());
|
||||
root.put("process", currentProcessName());
|
||||
root.put("device", deviceJson());
|
||||
root.put("app", appJson(context));
|
||||
root.put("build", buildJson());
|
||||
root.put("runtime_context", CrashRuntimeContext.snapshot(context));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
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 {
|
||||
JSONObject d = new JSONObject();
|
||||
d.put("manufacturer", Build.MANUFACTURER);
|
||||
|
||||
@@ -17,16 +17,22 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Pending/uploaded crash JSON in app-private storage (shared across processes). */
|
||||
public final class CrashReportStore {
|
||||
private static final String ROOT = "crash_reports";
|
||||
private static final String PENDING = "pending";
|
||||
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() {}
|
||||
|
||||
@@ -46,8 +52,21 @@ public final class CrashReportStore {
|
||||
return dir;
|
||||
}
|
||||
|
||||
public static File newPendingFile(File filesDir, String reportId) {
|
||||
return new File(pendingDir(filesDir), "crash_" + reportId + ".json");
|
||||
public static String formatTimestamp(long epochMs) {
|
||||
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 {
|
||||
@@ -71,7 +90,7 @@ public final class CrashReportStore {
|
||||
|
||||
public static List<File> listPending(File 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) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ public final class CrashReporter {
|
||||
|
||||
public static void install(Context context) {
|
||||
Context app = context.getApplicationContext();
|
||||
if (app instanceof android.app.Application) {
|
||||
CrashRuntimeContext.init((android.app.Application) app);
|
||||
}
|
||||
CrashNativeBridge.init(app);
|
||||
final Thread.UncaughtExceptionHandler previous =
|
||||
Thread.getDefaultUncaughtExceptionHandler();
|
||||
@@ -70,8 +73,12 @@ public final class CrashReporter {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String id = report.optString("report_id", String.valueOf(System.currentTimeMillis()));
|
||||
File file = CrashReportStore.newPendingFile(context.getFilesDir(), id);
|
||||
long generatedAt = report.optLong("generated_at_epoch_ms", System.currentTimeMillis());
|
||||
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.pruneOldestPending(context.getFilesDir(),
|
||||
CrashSettingsStore.load(context).maxPendingFiles);
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,8 +127,16 @@ public final class CrashWatcherService extends Service {
|
||||
signal = text.substring(sigIdx + 7, end > sigIdx ? end : text.length()).trim();
|
||||
}
|
||||
JSONObject report = CrashReportBuilder.buildNativeCrash(this, signal, text);
|
||||
String id = report.optString("report_id", stub.getName());
|
||||
CrashReportStore.writeJson(CrashReportStore.newPendingFile(filesDir, id), report);
|
||||
long generatedAt = report.optLong("generated_at_epoch_ms", stub.lastModified());
|
||||
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();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "ingest native stub failed: " + stub.getName());
|
||||
|
||||
@@ -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("_"));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
## 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.
|
||||
- **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).
|
||||
@@ -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).
|
||||
|
||||
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`.
|
||||
|
||||
@@ -3,8 +3,19 @@
|
||||
"report_id": "00000000-0000-4000-8000-000000000001",
|
||||
"generated_at_epoch_ms": 1710000000000,
|
||||
"crash_type": "java",
|
||||
"scenario": "app_runtime",
|
||||
"process": "main",
|
||||
"report_file": "crash_20260520_153000.json",
|
||||
"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": {
|
||||
"manufacturer": "Doogee",
|
||||
"brand": "DOOGEE",
|
||||
|
||||
@@ -1,48 +1,68 @@
|
||||
#!/bin/bash
|
||||
set -u
|
||||
|
||||
DEVICES_LIST="$(adb devices -l)"
|
||||
echo "${DEVICES_LIST}"
|
||||
|
||||
|
||||
filter_out() {
|
||||
local -n dev_string=${1}
|
||||
[[ ${dev_string} =~ List* ]] && dev_string=""
|
||||
return 0
|
||||
}
|
||||
|
||||
LOG_ROOT="log_capture/session_stats/"
|
||||
CLEAN_LOGS="no"
|
||||
|
||||
while true; do
|
||||
read -p "clean logs on device after fetch? [Y/n]" yn
|
||||
case $yn in
|
||||
[Y|Yes|y|yes]* )
|
||||
yn=""
|
||||
read -rp "clean logs on device after fetch? [Y/n] " yn
|
||||
yn="${yn,,}"
|
||||
case "${yn}" in
|
||||
y|yes)
|
||||
CLEAN_LOGS="yes"
|
||||
break
|
||||
;;
|
||||
[N|No|n|no]* )
|
||||
;;
|
||||
n|no)
|
||||
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
|
||||
done
|
||||
|
||||
echo "${DEVICES_LIST}" | while IFS= read -r DEV; do
|
||||
SERIAL="$(echo "${DEV}" | awk '{print $1}')"
|
||||
filter_out SERIAL
|
||||
[ -z "${SERIAL}" ] && continue
|
||||
# echo "Device: ${DEV}"
|
||||
# echo "Serial: ${SERIAL}"
|
||||
adb -s "${SERIAL}" shell run-as com.foxx.androidcast ls files/session_stats/
|
||||
echo "Known devices list:"
|
||||
echo "${DEVICES_LIST}"
|
||||
|
||||
while IFS= read -r DEV; do
|
||||
[[ -z "${DEV}" ]] && continue
|
||||
[[ "${DEV}" == List* ]] && continue
|
||||
|
||||
SERIAL="$(awk '{print $1}' <<<"${DEV}")"
|
||||
STATE="$(awk '{print $2}' <<<"${DEV}")"
|
||||
[[ "${STATE}" != "device" ]] && continue
|
||||
|
||||
LOG_PATH="${LOG_ROOT}/${SERIAL}/"
|
||||
echo "------------ dumping in ${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"
|
||||
[[ "no" != "${CLEAN_LOGS}" ]] && adb -s ${SERIAL} shell run-as com.foxx.androidcast rm -f "files/session_stats/$f"
|
||||
|
||||
mapfile -t FILES < <(adb -s "${SERIAL}" shell run-as com.foxx.androidcast ls files/session_stats/ </dev/null \
|
||||
| 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 <<<"${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}"
|
||||
|
||||
|
||||
52
scripts/header-post-commit.py
Executable file
52
scripts/header-post-commit.py
Executable 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())
|
||||
Reference in New Issue
Block a user