mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:17:39 +03:00
Merge branch 'feature/crash-triage-tags-api' into next
This commit is contained in:
78
README.md
78
README.md
@@ -1,35 +1,37 @@
|
|||||||
# Android Cast (POC)
|
# Android Cast (POC)
|
||||||
|
|
||||||
**Roadmap & alpha scope:** [docs/ROADMAP.md](docs/ROADMAP.md) · **LAN demo QA:** [docs/ALPHA_SOAK.md](docs/ALPHA_SOAK.md) · USB-C/HDMI: [docs/USB_HDMI_CAST.md](docs/USB_HDMI_CAST.md) · Commercial (dev-gated): [docs/COMMERCIAL.md](docs/COMMERCIAL.md)
|
**Roadmap & alpha:** [docs/ROADMAP.md](docs/ROADMAP.md) · **Alpha QA & feature freeze:** [docs/ALPHA.md](docs/ALPHA.md) · USB-C/HDMI: [docs/USB_HDMI_CAST.md](docs/USB_HDMI_CAST.md) · Commercial (dev-gated): [docs/COMMERCIAL.md](docs/COMMERCIAL.md)
|
||||||
|
|
||||||
Minimal LAN screen-casting proof of concept in **Java**. Captures screen + playback audio, encodes **H.264 + AAC**, streams over **UDP or TCP** to a receiver that decodes and plays back.
|
Minimal LAN screen-casting proof of concept in **Java**. Captures screen + playback audio, encodes **H.264 + AAC**, streams over **TCP or UDP** to a receiver that decodes and plays back.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- UDP broadcast discovery on the local network
|
- UDP broadcast discovery on the local network
|
||||||
- TCP or UDP stream transport (selectable; must match on both devices)
|
- **UDP (default)** or TCP stream transport (selectable; must match on both devices)
|
||||||
- PIN auth (SHA-256 hash, default `1234`)
|
- PIN auth (SHA-256 hash, default `1234`)
|
||||||
- Screen capture → H.264 (AVC) with quality presets and scaled capture resolution
|
- Screen capture → H.264 (AVC) with quality presets and scaled capture resolution
|
||||||
- Playback audio capture (API 29+) → AAC-LC
|
- Playback audio capture (API 29+) → AAC-LC
|
||||||
|
- Camera capture modes (API 29+, rear/front) — best-effort
|
||||||
- Runtime notification permission (Android 13+)
|
- Runtime notification permission (Android 13+)
|
||||||
|
|
||||||
## Quick start (alpha demo)
|
## Quick start (alpha)
|
||||||
|
|
||||||
1. Install the same APK on two devices on the **same Wi‑Fi**.
|
1. Install the same APK on two devices on the **same Wi‑Fi**.
|
||||||
2. **Receiver:** **Receive** → transport **UDP** → stream protection **None** → **Start listening**.
|
2. **Receiver:** **Receive** → transport **UDP** → stream protection **None** → **Start listening** (notification *Listening* / *Ready*).
|
||||||
3. **Sender:** **Send** → **UDP** → pick **one** receiver → PIN → approve screen capture.
|
3. **Sender:** **Send** → **UDP** → protection **None** → discover receiver → PIN → approve screen capture.
|
||||||
4. Sender resolution: **Original**, **720p**, **480p**, or **Auto** (settings drawer).
|
4. Watch **notifications** on both devices for status.
|
||||||
5. Watch **notifications** on both devices for status (not only toasts).
|
5. Stop from either notification **Stop** action.
|
||||||
6. Stop from the sender notification **Stop** action.
|
|
||||||
|
|
||||||
Full soak script: [docs/ALPHA_SOAK.md](docs/ALPHA_SOAK.md).
|
Full sign-off checklist: [docs/ALPHA.md](docs/ALPHA.md).
|
||||||
|
|
||||||
### If video never appears
|
### If video never appears
|
||||||
|
|
||||||
- Transport must match on both sides (e.g. both TCP).
|
- Transport must match on both sides (e.g. both UDP).
|
||||||
- Receiver must show *Listening* or *Ready* in the notification before you cast.
|
- Stream protection **None** on both sides for alpha.
|
||||||
- Sender notification should reach *Casting via TCP · WxH* — if it shows *Screen capture data missing*, reinstall and try again (Android 13+ intent fix).
|
- Receiver must show *Listening* or *Ready* before you cast.
|
||||||
- Keep the receiver UI open once so the video `Surface` attaches (or return to it after rotation).
|
- Sender notification should reach *Casting* with resolution — if it shows *Screen capture data missing*, reinstall (Android 13+ intent fix).
|
||||||
|
- Open receiver playback once so the video `Surface` attaches (or return after rotation).
|
||||||
|
- On rotation-sensitive senders, try **system rotation lock** during cast.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -42,24 +44,29 @@ cd "/home/foxx/repos/My Projects/android cast"
|
|||||||
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
## Stream options
|
## Stream options (defaults)
|
||||||
|
|
||||||
| Option | Effect |
|
| Option | Default / alpha note |
|
||||||
|--------|--------|
|
|--------|----------------------|
|
||||||
| **Transport UDP** | Default; lower latency |
|
| **Transport** | **UDP** (`CastConfig.DEFAULT_TRANSPORT`) |
|
||||||
| **Transport TCP** | Reliable fallback |
|
| **Stream protection** | **None** |
|
||||||
| **Sender resolution** | **Original** (native), **720p** / **480p** (long-edge cap), **Auto** (adaptive from peer stats) |
|
| **Quality** | Medium — 5 Mbps, 30 fps |
|
||||||
| **Bitrate** | Estimated / quality / speed modes in settings |
|
| **Resolution** | Adaptive (sender) |
|
||||||
| **Stream protection** | FEC/NACK optional on UDP — use **None** for alpha demo |
|
| **Multi-receiver** | **Off** while `CastConfig.ALPHA_FEATURE_FREEZE` |
|
||||||
| **Audio** | `AudioPlaybackCapture` + AAC (Android 10+) |
|
| **Audio** | On when API 29+ (`AudioPlaybackCapture` + AAC) |
|
||||||
| **Camera capture** | Rear/front Camera2 (Android 10+); requires camera permission |
|
|
||||||
|
| Quality | Video bitrate | FPS |
|
||||||
|
|---------|---------------|-----|
|
||||||
|
| Low | 1.5 Mbps | 24 |
|
||||||
|
| Medium | 5 Mbps | 30 |
|
||||||
|
| High | 10 Mbps | 30 |
|
||||||
|
|
||||||
## Protocol
|
## Protocol
|
||||||
|
|
||||||
| Step | Description |
|
| Step | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| Discovery | UDP `41234`, JSON with `magic`, `name`, `port`, `transport` |
|
| Discovery | UDP `41234`, JSON with `magic`, `name`, `port`, `transport` |
|
||||||
| Connect | TCP or UDP port `41235` |
|
| Connect | TCP or UDP port `41235` (QUIC `41236` when enabled post-alpha) |
|
||||||
| Auth | `HELLO` → `AUTH` → `AUTH_OK` → `CAST_SETTINGS` |
|
| Auth | `HELLO` → `AUTH` → `AUTH_OK` → `CAST_SETTINGS` |
|
||||||
| Stream | `STREAM_CONFIG`, `VIDEO_FRAME`, `AUDIO_CONFIG`, `AUDIO_FRAME` |
|
| Stream | `STREAM_CONFIG`, `VIDEO_FRAME`, `AUDIO_CONFIG`, `AUDIO_FRAME` |
|
||||||
|
|
||||||
@@ -67,12 +74,7 @@ UDP frames use `ACUD` header and fragmentation for payloads > ~1.2 KB.
|
|||||||
|
|
||||||
## Bluetooth / BLE
|
## Bluetooth / BLE
|
||||||
|
|
||||||
**Not viable for this video stream.** Practical throughput:
|
**Not viable for this video stream.** Use **Wi‑Fi** for media; BLE could be added later only for discovery/pairing.
|
||||||
|
|
||||||
- **BLE**: ~100–300 kb/s — fine for control/metadata only
|
|
||||||
- **Bluetooth Classic (SPP)**: often ~1–3 Mb/s theoretical — still far below 720p H.264 (~2–8 Mb/s), with high latency
|
|
||||||
|
|
||||||
A phone cannot “passthrough” a live encoded screen stream over BLE to another device for playback without a custom high-bandwidth profile (which consumer Android does not expose for arbitrary apps). Use **Wi‑Fi** for media; BT could be added later only for discovery/pairing if needed.
|
|
||||||
|
|
||||||
## Persistent services (AIDL)
|
## Persistent services (AIDL)
|
||||||
|
|
||||||
@@ -83,18 +85,16 @@ Casting runs in **foreground services** with tray notifications:
|
|||||||
| `ScreenCastService` | Sender — capture, encode, stream |
|
| `ScreenCastService` | Sender — capture, encode, stream |
|
||||||
| `ReceiverCastService` | Receiver — listen, decode, play |
|
| `ReceiverCastService` | Receiver — listen, decode, play |
|
||||||
|
|
||||||
Activities bind via AIDL (`ICastSenderService` / `ICastReceiverService`) for status updates and (on the receiver) attaching the video `Surface`. Rotating the screen or leaving the app does **not** stop the receiver listener; the sender keeps casting until you tap **Stop** in the notification.
|
Activities bind via AIDL for status and (on the receiver) attaching the video `Surface`. Leaving the app does **not** stop the receiver listener; the sender keeps casting until **Stop** in the notification.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
- No TLS encryption (LAN cleartext POC)
|
- No TLS encryption (LAN cleartext POC)
|
||||||
- UDP mode may glitch under load
|
- UDP mode may glitch under load (try TCP)
|
||||||
- Audio captures **playback** routed through the system (not microphone); some apps may be silent by policy
|
- Audio captures **playback** routed through the system (not microphone)
|
||||||
- Single sender → single receiver
|
- Single receiver per session during alpha freeze
|
||||||
|
- No live sender preview while casting (stability)
|
||||||
|
|
||||||
## Development (git flow)
|
## Development (git flow)
|
||||||
|
|
||||||
We use a **green `master`** plus integration branch **`next`**: features merge to `next` first; releases merge `next` → `master` and are tagged there; production hotfixes branch from `master` and sync back into `next`.
|
Green **`master`** plus integration branch **`next`**. See [docs/GIT_FLOW.md](docs/GIT_FLOW.md).
|
||||||
|
|
||||||
- Narrative: [docs/GIT_FLOW.md](docs/GIT_FLOW.md)
|
|
||||||
- PDF: [docs/GIT_FLOW.pdf](docs/GIT_FLOW.pdf) — `pip install -r docs/_pdf_build/requirements-pdf.txt`, then `python3 docs/_pdf_build/build_git_flow_pdf.py`
|
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ public final class AppPreferences {
|
|||||||
if (s.getNetworkAdoption() == CastSettings.NetworkAdoption.AUTO) {
|
if (s.getNetworkAdoption() == CastSettings.NetworkAdoption.AUTO) {
|
||||||
s.setNetworkAdoption(CastSettings.NetworkAdoption.ESTIMATED);
|
s.setNetworkAdoption(CastSettings.NetworkAdoption.ESTIMATED);
|
||||||
}
|
}
|
||||||
|
clampAlphaTransport(s);
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +402,7 @@ public final class AppPreferences {
|
|||||||
if (s.getNetworkAdoption() == CastSettings.NetworkAdoption.AUTO) {
|
if (s.getNetworkAdoption() == CastSettings.NetworkAdoption.AUTO) {
|
||||||
s.setNetworkAdoption(CastSettings.NetworkAdoption.ESTIMATED);
|
s.setNetworkAdoption(CastSettings.NetworkAdoption.ESTIMATED);
|
||||||
}
|
}
|
||||||
|
clampAlphaTransport(s);
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,6 +478,15 @@ public final class AppPreferences {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void clampAlphaTransport(CastSettings settings) {
|
||||||
|
if (settings == null || !CastConfig.ALPHA_FEATURE_FREEZE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!CastConfig.isReleaseTransport(settings.getTransport())) {
|
||||||
|
settings.setTransport(CastConfig.DEFAULT_TRANSPORT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static SharedPreferences prefs(Context context) {
|
private static SharedPreferences prefs(Context context) {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
throw new IllegalStateException("context is null");
|
throw new IllegalStateException("context is null");
|
||||||
|
|||||||
@@ -29,8 +29,19 @@ public final class CastConfig {
|
|||||||
public static final int CAST_PORT = 41235;
|
public static final int CAST_PORT = 41235;
|
||||||
public static final String DISCOVERY_MAGIC = "ACAST1";
|
public static final String DISCOVERY_MAGIC = "ACAST1";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alpha / feature-freeze gate: limits transports in settings and disables 1:N cast until post-alpha soak.
|
||||||
|
* Set to {@code false} on a release branch when re-enabling QUIC and multi-receiver experiments.
|
||||||
|
*/
|
||||||
|
public static final boolean ALPHA_FEATURE_FREEZE = true;
|
||||||
|
|
||||||
/** When true, sender list allows selecting multiple receivers (1:N cast). */
|
/** When true, sender list allows selecting multiple receivers (1:N cast). */
|
||||||
public static final boolean MULTI_RECEIVER_ENABLED = true;
|
public static final boolean MULTI_RECEIVER_ENABLED = !ALPHA_FEATURE_FREEZE;
|
||||||
|
|
||||||
|
/** Transports exposed in cast settings while {@link #ALPHA_FEATURE_FREEZE} is on. */
|
||||||
|
public static boolean isReleaseTransport(String transport) {
|
||||||
|
return TRANSPORT_UDP.equals(transport) || TRANSPORT_TCP.equals(transport);
|
||||||
|
}
|
||||||
|
|
||||||
public static final int MAX_RECEIVERS = 5;
|
public static final int MAX_RECEIVERS = 5;
|
||||||
public static final String DEFAULT_PIN = "1234";
|
public static final String DEFAULT_PIN = "1234";
|
||||||
|
|||||||
@@ -161,8 +161,10 @@ public final class CastSettingsBinder {
|
|||||||
valueList.add(CastConfig.TRANSPORT_UDP);
|
valueList.add(CastConfig.TRANSPORT_UDP);
|
||||||
labelList.add(activity.getString(R.string.transport_tcp));
|
labelList.add(activity.getString(R.string.transport_tcp));
|
||||||
valueList.add(CastConfig.TRANSPORT_TCP);
|
valueList.add(CastConfig.TRANSPORT_TCP);
|
||||||
labelList.add(activity.getString(R.string.transport_quic));
|
if (!CastConfig.ALPHA_FEATURE_FREEZE) {
|
||||||
valueList.add(CastConfig.TRANSPORT_QUIC);
|
labelList.add(activity.getString(R.string.transport_quic));
|
||||||
|
valueList.add(CastConfig.TRANSPORT_QUIC);
|
||||||
|
}
|
||||||
if (AppPreferences.isDevUsbTetherTransportEnabled(activity)) {
|
if (AppPreferences.isDevUsbTetherTransportEnabled(activity)) {
|
||||||
labelList.add(activity.getString(R.string.transport_usb_tether));
|
labelList.add(activity.getString(R.string.transport_usb_tether));
|
||||||
valueList.add(CastConfig.TRANSPORT_USB_TETHER);
|
valueList.add(CastConfig.TRANSPORT_USB_TETHER);
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ public class ReceiverCastService extends Service {
|
|||||||
private boolean streamIdle;
|
private boolean streamIdle;
|
||||||
private volatile boolean castEnded;
|
private volatile boolean castEnded;
|
||||||
private long lastVideoFrameMs;
|
private long lastVideoFrameMs;
|
||||||
|
private long lastStreamConfigMs;
|
||||||
private final TransportLossStats transportLoss = new TransportLossStats();
|
private final TransportLossStats transportLoss = new TransportLossStats();
|
||||||
private SessionStatsRecorder sessionStatsRecorder;
|
private SessionStatsRecorder sessionStatsRecorder;
|
||||||
|
|
||||||
@@ -300,6 +301,8 @@ public class ReceiverCastService extends Service {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onVideoFrame(long ptsUs, boolean keyFrame, byte[] data) {
|
public void onVideoFrame(long ptsUs, boolean keyFrame, byte[] data) {
|
||||||
|
final long nowMs = System.currentTimeMillis();
|
||||||
|
mainHandler.post(() -> lastVideoFrameMs = nowMs);
|
||||||
if (awaitingKeyframe && !keyFrame) {
|
if (awaitingKeyframe && !keyFrame) {
|
||||||
synchronized (transportLoss) {
|
synchronized (transportLoss) {
|
||||||
transportLoss.recvVideoSkippedAwaitingKey++;
|
transportLoss.recvVideoSkippedAwaitingKey++;
|
||||||
@@ -313,7 +316,6 @@ public class ReceiverCastService extends Service {
|
|||||||
final int frameBytes = data != null ? data.length : 0;
|
final int frameBytes = data != null ? data.length : 0;
|
||||||
final byte[] frameData = data;
|
final byte[] frameData = data;
|
||||||
mainHandler.post(() -> {
|
mainHandler.post(() -> {
|
||||||
lastVideoFrameMs = System.currentTimeMillis();
|
|
||||||
if (streamIdle) {
|
if (streamIdle) {
|
||||||
onStreamResumedAfterIdle();
|
onStreamResumedAfterIdle();
|
||||||
}
|
}
|
||||||
@@ -484,11 +486,16 @@ public class ReceiverCastService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkStreamIdle() {
|
private void checkStreamIdle() {
|
||||||
if (phase != Phase.STREAMING || streamIdle || lastVideoFrameMs <= 0
|
if (phase != Phase.STREAMING || streamIdle || lastVideoFrameMs <= 0) {
|
||||||
|| awaitingKeyframe || reconfiguring) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
long idleMs = System.currentTimeMillis() - lastVideoFrameMs;
|
long nowMs = System.currentTimeMillis();
|
||||||
|
if (reconfiguring || awaitingKeyframe) {
|
||||||
|
if (lastStreamConfigMs > 0 && nowMs - lastStreamConfigMs < 20_000L) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long idleMs = nowMs - lastVideoFrameMs;
|
||||||
if (idleMs >= CastConfig.STREAM_IDLE_TIMEOUT_MS) {
|
if (idleMs >= CastConfig.STREAM_IDLE_TIMEOUT_MS) {
|
||||||
Log.i(TAG, "Stream idle " + idleMs + "ms — awaiting");
|
Log.i(TAG, "Stream idle " + idleMs + "ms — awaiting");
|
||||||
if (sessionStatsRecorder != null) {
|
if (sessionStatsRecorder != null) {
|
||||||
@@ -594,6 +601,8 @@ public class ReceiverCastService extends Service {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean firstConfig = !hasPendingConfig;
|
boolean firstConfig = !hasPendingConfig;
|
||||||
|
boolean wasIdle = streamIdle;
|
||||||
|
boolean decoderWasActive = decoderActive;
|
||||||
phase = Phase.STREAMING;
|
phase = Phase.STREAMING;
|
||||||
streamIdle = false;
|
streamIdle = false;
|
||||||
if (firstConfig) {
|
if (firstConfig) {
|
||||||
@@ -602,7 +611,10 @@ public class ReceiverCastService extends Service {
|
|||||||
reconfiguring = true;
|
reconfiguring = true;
|
||||||
enterAwaitingKeyframe();
|
enterAwaitingKeyframe();
|
||||||
lastVideoFrameMs = System.currentTimeMillis();
|
lastVideoFrameMs = System.currentTimeMillis();
|
||||||
openPlaybackAwaiting(R.string.playback_awaiting_stream);
|
lastStreamConfigMs = lastVideoFrameMs;
|
||||||
|
if (firstConfig || wasIdle || !decoderWasActive) {
|
||||||
|
openPlaybackAwaiting(R.string.playback_awaiting_stream);
|
||||||
|
}
|
||||||
pendingWidth = size.width;
|
pendingWidth = size.width;
|
||||||
pendingHeight = size.height;
|
pendingHeight = size.height;
|
||||||
displayWidth = size.width;
|
displayWidth = size.width;
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ public class ScreenCastService extends Service implements
|
|||||||
private static final String TAG = "ScreenCastService";
|
private static final String TAG = "ScreenCastService";
|
||||||
private static final int CONNECT_RETRIES = 20;
|
private static final int CONNECT_RETRIES = 20;
|
||||||
private static final long CONNECT_RETRY_MS = 1000;
|
private static final long CONNECT_RETRY_MS = 1000;
|
||||||
|
private static final long CAPTURE_SIZE_UPDATE_DEBOUNCE_MS = 2_000L;
|
||||||
|
|
||||||
public static final String EXTRA_RESULT_CODE = "result_code";
|
public static final String EXTRA_RESULT_CODE = "result_code";
|
||||||
public static final String EXTRA_RESULT_DATA = "result_data";
|
public static final String EXTRA_RESULT_DATA = "result_data";
|
||||||
@@ -126,6 +127,9 @@ public class ScreenCastService extends Service implements
|
|||||||
private int captureDensity;
|
private int captureDensity;
|
||||||
private DisplayManager displayManager;
|
private DisplayManager displayManager;
|
||||||
private DisplayManager.DisplayListener displayListener;
|
private DisplayManager.DisplayListener displayListener;
|
||||||
|
private Runnable pendingCaptureSizeUpdate;
|
||||||
|
private CastResolution.Size pendingCaptureSize;
|
||||||
|
private final AtomicBoolean captureReconfigureInFlight = new AtomicBoolean(false);
|
||||||
private com.foxx.androidcast.sender.calibration.CalibrationCastDriver calibrationDriver;
|
private com.foxx.androidcast.sender.calibration.CalibrationCastDriver calibrationDriver;
|
||||||
private volatile boolean calibrationMode;
|
private volatile boolean calibrationMode;
|
||||||
private volatile boolean cameraMode;
|
private volatile boolean cameraMode;
|
||||||
@@ -188,6 +192,7 @@ public class ScreenCastService extends Service implements
|
|||||||
return START_STICKY;
|
return START_STICKY;
|
||||||
}
|
}
|
||||||
CastSettings startSettings = IntentExtras.getCastSettings(intent);
|
CastSettings startSettings = IntentExtras.getCastSettings(intent);
|
||||||
|
CastActiveState.setSenderCasting(true);
|
||||||
startSenderForeground(getString(R.string.connecting),
|
startSenderForeground(getString(R.string.connecting),
|
||||||
startSettings != null && startSettings.isAudioEnabled());
|
startSettings != null && startSettings.isAudioEnabled());
|
||||||
ioThread = new Thread(() -> runCast(intent), "ScreenCastIO");
|
ioThread = new Thread(() -> runCast(intent), "ScreenCastIO");
|
||||||
@@ -609,17 +614,74 @@ public class ScreenCastService extends Service implements
|
|||||||
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
|
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||||
wm.getDefaultDisplay().getRealMetrics(metrics);
|
wm.getDefaultDisplay().getRealMetrics(metrics);
|
||||||
CastResolution.Size size = CastResolution.compute(metrics, activeSettings, getPeerStats());
|
CastResolution.Size size = CastResolution.compute(metrics, activeSettings, getPeerStats());
|
||||||
|
scheduleCaptureSizeUpdate(size, "rotation");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean captureSizeChangeSignificant(CastResolution.Size size) {
|
||||||
if (size.width == captureWidth && size.height == captureHeight) {
|
if (size.width == captureWidth && size.height == captureHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int dw = Math.abs(size.width - captureWidth);
|
||||||
|
int dh = Math.abs(size.height - captureHeight);
|
||||||
|
return dw >= Math.max(32, captureWidth / 10) || dh >= Math.max(32, captureHeight / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleCaptureSizeUpdate(CastResolution.Size size, String reason) {
|
||||||
|
if (calibrationMode || cameraMode || !casting.get() || projection == null
|
||||||
|
|| activeSettings == null || stopping.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!captureSizeChangeSignificant(size)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingCaptureSize = size;
|
||||||
|
Handler handler = projectionHandler != null ? projectionHandler : mainHandler;
|
||||||
|
if (pendingCaptureSizeUpdate != null) {
|
||||||
|
handler.removeCallbacks(pendingCaptureSizeUpdate);
|
||||||
|
}
|
||||||
|
pendingCaptureSizeUpdate = () -> applyCaptureSizeUpdate(reason);
|
||||||
|
handler.postDelayed(pendingCaptureSizeUpdate, CAPTURE_SIZE_UPDATE_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyCaptureSizeUpdate(String reason) {
|
||||||
|
pendingCaptureSizeUpdate = null;
|
||||||
|
if (calibrationMode || cameraMode || !casting.get() || projection == null
|
||||||
|
|| activeSettings == null || stopping.get() || pendingCaptureSize == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CastResolution.Size size = pendingCaptureSize;
|
||||||
|
pendingCaptureSize = null;
|
||||||
|
if (!captureSizeChangeSignificant(size)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!captureReconfigureInFlight.compareAndSet(false, true)) {
|
||||||
|
Log.d(TAG, "Capture size update skipped (" + reason + "): reconfigure in flight");
|
||||||
|
scheduleCaptureSizeUpdate(size, reason);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setupVideoPipeline(size, activeSettings, false);
|
setupVideoPipeline(size, activeSettings, false);
|
||||||
updateStatus(getString(R.string.casting_rotated, captureWidth, captureHeight));
|
if ("rotation".equals(reason)) {
|
||||||
|
updateStatus(getString(R.string.casting_rotated, captureWidth, captureHeight));
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Capture reconfigured (" + reason + ") " + captureWidth + "x" + captureHeight);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(TAG, "Rotation reconfigure failed", e);
|
Log.e(TAG, "Capture reconfigure failed (" + reason + ")", e);
|
||||||
|
} finally {
|
||||||
|
captureReconfigureInFlight.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cancelPendingCaptureSizeUpdate() {
|
||||||
|
pendingCaptureSize = null;
|
||||||
|
if (pendingCaptureSizeUpdate == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Handler handler = projectionHandler != null ? projectionHandler : mainHandler;
|
||||||
|
handler.removeCallbacks(pendingCaptureSizeUpdate);
|
||||||
|
pendingCaptureSizeUpdate = null;
|
||||||
|
}
|
||||||
|
|
||||||
private NetworkStatsSnapshot getPeerStats() {
|
private NetworkStatsSnapshot getPeerStats() {
|
||||||
if (networkFeedback != null && networkFeedback.getPlane() instanceof PassThroughNetworkControlPlane) {
|
if (networkFeedback != null && networkFeedback.getPlane() instanceof PassThroughNetworkControlPlane) {
|
||||||
return ((PassThroughNetworkControlPlane) networkFeedback.getPlane()).getLastPeerStats();
|
return ((PassThroughNetworkControlPlane) networkFeedback.getPlane()).getLastPeerStats();
|
||||||
@@ -709,19 +771,7 @@ public class ScreenCastService extends Service implements
|
|||||||
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
|
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||||
wm.getDefaultDisplay().getRealMetrics(metrics);
|
wm.getDefaultDisplay().getRealMetrics(metrics);
|
||||||
CastResolution.Size size = CastResolution.compute(metrics, activeSettings, peer);
|
CastResolution.Size size = CastResolution.compute(metrics, activeSettings, peer);
|
||||||
int dw = Math.abs(size.width - captureWidth);
|
scheduleCaptureSizeUpdate(size, "adaptive");
|
||||||
int dh = Math.abs(size.height - captureHeight);
|
|
||||||
if (dw < Math.max(32, captureWidth / 10) && dh < Math.max(32, captureHeight / 10)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
runOnProjectionThread(() -> {
|
|
||||||
try {
|
|
||||||
setupVideoPipeline(size, activeSettings, false);
|
|
||||||
Log.i(TAG, "Adaptive capture " + captureWidth + "x" + captureHeight);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, "Adaptive resize failed", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runOnProjectionThread(Runnable action) {
|
private void runOnProjectionThread(Runnable action) {
|
||||||
@@ -1131,6 +1181,8 @@ public class ScreenCastService extends Service implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void releaseProjectionCapture() {
|
private void releaseProjectionCapture() {
|
||||||
|
cancelPendingCaptureSizeUpdate();
|
||||||
|
captureReconfigureInFlight.set(false);
|
||||||
unregisterDisplayListener();
|
unregisterDisplayListener();
|
||||||
if (virtualDisplay != null) {
|
if (virtualDisplay != null) {
|
||||||
virtualDisplay.release();
|
virtualDisplay.release();
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ public final class SenderScreenPreview {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void attach(TextureView textureView, CastSettings.CaptureMode mode) {
|
public void attach(TextureView textureView, CastSettings.CaptureMode mode) {
|
||||||
if (!needsProjection(mode) || !hasConsent()) {
|
if (CastActiveState.isSenderCasting() || !needsProjection(mode) || !hasConsent()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boundTexture = textureView;
|
boundTexture = textureView;
|
||||||
@@ -137,7 +137,7 @@ public final class SenderScreenPreview {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void ensureVirtualDisplay(TextureView textureView, CastSettings.CaptureMode mode) {
|
private void ensureVirtualDisplay(TextureView textureView, CastSettings.CaptureMode mode) {
|
||||||
if (!needsProjection(mode) || !hasConsent()) {
|
if (CastActiveState.isSenderCasting() || !needsProjection(mode) || !hasConsent()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -212,6 +212,14 @@ public final class SenderScreenPreview {
|
|||||||
boundTexture = null;
|
boundTexture = null;
|
||||||
boundMode = null;
|
boundMode = null;
|
||||||
pausePreview();
|
pausePreview();
|
||||||
|
if (projection != null) {
|
||||||
|
try {
|
||||||
|
projection.stop();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
projection = null;
|
||||||
|
}
|
||||||
|
SenderPreviewService.stop(appContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Release consent and active capture (leaving sender screen). */
|
/** Release consent and active capture (leaving sender screen). */
|
||||||
|
|||||||
156
docs/ALPHA.md
Normal file
156
docs/ALPHA.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Android Cast — alpha release & QA
|
||||||
|
|
||||||
|
Last updated: 2026-05-23. Use this document for **feature freeze**, **alpha builds**, and **sign-off** before widening testing.
|
||||||
|
|
||||||
|
**Related:** [ROADMAP.md](ROADMAP.md) · [README.md](../README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alpha scope (what we ship)
|
||||||
|
|
||||||
|
| In scope | Out of scope (frozen) |
|
||||||
|
|----------|------------------------|
|
||||||
|
| LAN discover → PIN → cast → stop | Internet relay, TLS on stream |
|
||||||
|
| **UDP** or **TCP** transport (must match on both devices) | QUIC, WebRTC, USB-tether transport in UI |
|
||||||
|
| H.264 + AAC (Android 10+ audio) | Passthrough video, Opus/Speex as default |
|
||||||
|
| **Single receiver** per cast session | 1:N multi-receiver (disabled in `CastConfig`) |
|
||||||
|
| Stream protection **None** (default) | FEC/NACK soak required for alpha sign-off |
|
||||||
|
| Screen capture (full / user-choice on API 34+) | Secondary-display capture, Miracast in-app |
|
||||||
|
| Camera cast (API 29+, best-effort) | Live sender mirror while casting |
|
||||||
|
| Receiver playback + notification stop | Play Store / AdMob production |
|
||||||
|
|
||||||
|
Code gate: `CastConfig.ALPHA_FEATURE_FREEZE = true` hides experimental transports and turns off multi-receiver selection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended defaults (both devices)
|
||||||
|
|
||||||
|
Set in **Settings** (or use fresh install defaults):
|
||||||
|
|
||||||
|
| Setting | Sender | Receiver |
|
||||||
|
|---------|--------|----------|
|
||||||
|
| Transport | **UDP** | **UDP** |
|
||||||
|
| Stream protection | **None** | **None** |
|
||||||
|
| Quality | Medium | Medium |
|
||||||
|
| Resolution | Adaptive (or HD 720p if rotation is shaky) | — |
|
||||||
|
| Video codec | Auto | Auto |
|
||||||
|
| Audio | On (if API 29+) | Play incoming audio: On |
|
||||||
|
| Capture mode | Full screen (or User choice on Android 14+) | — |
|
||||||
|
| PIN | Same on both (default `1234`) | Same |
|
||||||
|
|
||||||
|
**Rotation soak:** On Mediatek / Freeme senders (e.g. BL6000), prefer **system rotation lock ON** during first alpha pass; then repeat with lock **OFF** to validate debounced reconfigure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature freeze checklist (engineering)
|
||||||
|
|
||||||
|
| Item | Status |
|
||||||
|
|------|--------|
|
||||||
|
| `ALPHA_FEATURE_FREEZE` — UDP/TCP only in settings | Done |
|
||||||
|
| Multi-receiver disabled for alpha | Done |
|
||||||
|
| Stored QUIC/WebRTC transport clamped to UDP on load | Done |
|
||||||
|
| Camera strings (permission vs unavailable) | Done |
|
||||||
|
| README matches `CastConfig` / `CastSettings` defaults | Done |
|
||||||
|
| Receiver: keyframe idle grace + debounced sender resize | Done (verify on soak) |
|
||||||
|
| Preview paused during cast (single MediaProjection) | Done |
|
||||||
|
| Developer-only: FEC, QUIC, USB-tether, immersive presets | Gated (dev settings) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alpha app sign-off (manual QA)
|
||||||
|
|
||||||
|
Run on **two physical devices**, same Wi‑Fi subnet. Record build: About / `versionName` from APK, git commit, date.
|
||||||
|
|
||||||
|
### A. Smoke (required — all must pass)
|
||||||
|
|
||||||
|
| # | Step | Pass |
|
||||||
|
|---|------|------|
|
||||||
|
| A1 | Install same debug/release APK on sender + receiver | ☐ |
|
||||||
|
| A2 | Receiver: **Receive** → Start listening → notification *Listening* / *Ready* | ☐ |
|
||||||
|
| A3 | Sender: **Send** → discover receiver → correct PIN → approve capture | ☐ |
|
||||||
|
| A4 | Video appears on receiver within **15 s** of auth | ☐ |
|
||||||
|
| A5 | Audio audible (if enabled and API 29+) within **20 s** | ☐ |
|
||||||
|
| A6 | Sender notification shows *Casting* with resolution | ☐ |
|
||||||
|
| A7 | **Stop** from sender notification → stream ends, receiver returns to idle | ☐ |
|
||||||
|
| A8 | **Stop** from receiver notification → sender stops cleanly | ☐ |
|
||||||
|
| A9 | Second cast without reinstall (A2→A7 again) | ☐ |
|
||||||
|
|
||||||
|
### B. Resilience (required)
|
||||||
|
|
||||||
|
| # | Step | Pass |
|
||||||
|
|---|------|------|
|
||||||
|
| B1 | Sender home → cast 5 min, no reboot / ANR | ☐ |
|
||||||
|
| B2 | Receiver app to background 2 min → return → video recovers or clean idle | ☐ |
|
||||||
|
| B3 | Receiver rotate once → video continues or recovers within 10 s | ☐ |
|
||||||
|
| B4 | Sender rotate **with rotation lock ON** → 5 min stable | ☐ |
|
||||||
|
| B5 | Sender rotate **with lock OFF** (optional / device-specific) → no permanent freeze; note if reboot | ☐ |
|
||||||
|
|
||||||
|
### C. Settings matrix (pick one row for official alpha demo)
|
||||||
|
|
||||||
|
| Profile | Transport | Protection | Use |
|
||||||
|
|---------|-----------|------------|-----|
|
||||||
|
| **Alpha default** | UDP | None | Primary sign-off |
|
||||||
|
| Fallback | TCP | None | If UDP glitches on your AP |
|
||||||
|
|
||||||
|
| # | Step | Pass |
|
||||||
|
|---|------|------|
|
||||||
|
| C1 | UDP + None: 10 min soak | ☐ |
|
||||||
|
| C2 | (Optional) TCP + None: 10 min soak | ☐ |
|
||||||
|
|
||||||
|
### D. Regression guards (quick)
|
||||||
|
|
||||||
|
| # | Step | Pass |
|
||||||
|
|---|------|------|
|
||||||
|
| D1 | Wrong PIN → clear error, no crash | ☐ |
|
||||||
|
| D2 | Mismatched transport (sender TCP / receiver UDP) → fails fast with message | ☐ |
|
||||||
|
| D3 | Camera mode (if used): permission grant → cast 2 min | ☐ |
|
||||||
|
| D4 | Calibration test mode → pattern on receiver | ☐ |
|
||||||
|
|
||||||
|
### E. Known limitations (not alpha blockers)
|
||||||
|
|
||||||
|
- No TLS; LAN cleartext.
|
||||||
|
- UDP may drop frames under congestion.
|
||||||
|
- No live preview on sender while casting.
|
||||||
|
- Some apps silent under playback capture policy.
|
||||||
|
- Heavy rotation on some OEMs may still stress system WFD/WM (watchdog) — document device + lock state.
|
||||||
|
|
||||||
|
**Alpha approved when:** all **A**, **B1–B4**, and **C1** pass on your reference sender + receiver pair.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Log capture (when something fails)
|
||||||
|
|
||||||
|
From repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/alpha-qa-logcat.sh SENDER_SERIAL RECEIVER_SERIAL
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb -s SENDER logcat -s ScreenCastService:* ReceiverCastService:* AndroidCast:* SenderScreenPreview:*
|
||||||
|
adb -s RECEIVER logcat -s ReceiverCastService:* AndroidCast:*
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alpha backend (optional for LAN-only alpha)
|
||||||
|
|
||||||
|
LAN demo does **not** require OTA/crash backend. For **full alpha** with updates and crash upload:
|
||||||
|
|
||||||
|
| Item | Notes |
|
||||||
|
|------|--------|
|
||||||
|
| Host `examples/ota/v0/` | See [OTA.md](OTA.md) |
|
||||||
|
| Deploy crash reporter | See [CRASH_REPORTER.md](CRASH_REPORTER.md) |
|
||||||
|
| Configure URLs | Developer settings or `local.properties` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ending feature freeze
|
||||||
|
|
||||||
|
1. Complete sign-off table above; note devices + APK build id in git tag message.
|
||||||
|
2. Tag release on `master` per [GIT_FLOW.md](GIT_FLOW.md).
|
||||||
|
3. To re-enable experiments on a dev branch: set `CastConfig.ALPHA_FEATURE_FREEZE = false` and restore `MULTI_RECEIVER_ENABLED` as needed.
|
||||||
|
|
||||||
|
Post-alpha priorities: rotation-stable encode path, encoder thumbnail on sender, secondary-display capture (roadmap D).
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Android Cast — implementation roadmap
|
# Android Cast — implementation roadmap
|
||||||
|
|
||||||
Last updated: 2026-05-21. Living document — refine as alpha and commercial tracks progress.
|
Last updated: 2026-05-23. Living document — refine as alpha and commercial tracks progress.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -15,13 +15,17 @@ Last updated: 2026-05-21. Living document — refine as alpha and commercial tra
|
|||||||
|
|
||||||
### Alpha app checklist
|
### Alpha app checklist
|
||||||
|
|
||||||
- [ ] Happy path: UDP + H.264/AAC + single receiver, stream protection **None** — see [ALPHA_SOAK.md](ALPHA_SOAK.md)
|
**Runbook:** [ALPHA.md](ALPHA.md) (feature freeze, defaults, sign-off tables, log script).
|
||||||
- [x] README matches `CastSettings` / `CastConfig` defaults (UDP default, resolution presets)
|
|
||||||
- [x] Fix misleading strings (camera permission / API messaging)
|
| Checkpoint | Engineering | Manual sign-off |
|
||||||
- [x] Two-device QA script — [ALPHA_SOAK.md](ALPHA_SOAK.md)
|
|------------|---------------|-----------------|
|
||||||
- [x] Sender resolution UI: Original / 720p / 480p / Auto (release builds; legacy modes dev-only)
|
| Happy path: UDP + H.264/AAC + single receiver, protection **None** | Defaults + freeze in code | ☐ [ALPHA.md](ALPHA.md) § A, C1 |
|
||||||
- [x] Remove orphan `ReceiverActivity` (receiver entry is `ReceiverPlaybackActivity`)
|
| README matches `CastSettings` / `CastConfig` | Done | — |
|
||||||
- [ ] Optional: cap multi-receiver at 1 for show, or soak 1:N — soak doc recommends one receiver
|
| Fix misleading strings (camera permission vs unavailable) | Done | — |
|
||||||
|
| Two-device QA (rotate, background, notification stop) | Script in `scripts/alpha-qa-logcat.sh` | ☐ [ALPHA.md](ALPHA.md) § B |
|
||||||
|
| Cap multi-receiver at 1 for alpha | `MULTI_RECEIVER_ENABLED` off while freeze on | — |
|
||||||
|
| Hide QUIC/WebRTC/USB in settings for alpha | `ALPHA_FEATURE_FREEZE` | — |
|
||||||
|
| Receiver stuck on rotation / STREAM_CONFIG storm | Debounce + idle grace in code | ☐ B4–B5 |
|
||||||
|
|
||||||
### Alpha backend checklist
|
### Alpha backend checklist
|
||||||
|
|
||||||
@@ -135,8 +139,8 @@ See [ndk/README.md](../ndk/README.md).
|
|||||||
|------|--------|
|
|------|--------|
|
||||||
| `ReceiverPlaybackActivity` (primary receiver UI) | Done |
|
| `ReceiverPlaybackActivity` (primary receiver UI) | Done |
|
||||||
| Settings drawer + themes + RU locale | Done |
|
| Settings drawer + themes + RU locale | Done |
|
||||||
| Multi-receiver 1:N | Done (soak or gate for alpha) |
|
| Multi-receiver 1:N | Done (gated off while `ALPHA_FEATURE_FREEZE`) |
|
||||||
| Legacy `ReceiverActivity` | Removed — use `ReceiverPlaybackActivity` only |
|
| Legacy `ReceiverActivity` in manifest | Orphan — remove or wire |
|
||||||
| Immersive A/V presets (dev) | Experimental |
|
| Immersive A/V presets (dev) | Experimental |
|
||||||
| Sender resolution UI modes (dev: Separated / Legacy / Combined) | Done (test) |
|
| Sender resolution UI modes (dev: Separated / Legacy / Combined) | Done (test) |
|
||||||
| True dual-stream encode (480p + 720p simultaneous) | Deferred |
|
| True dual-stream encode (480p + 720p simultaneous) | Deferred |
|
||||||
@@ -158,7 +162,7 @@ See [ndk/README.md](../ndk/README.md).
|
|||||||
|
|
||||||
- [GIT_FLOW.md](GIT_FLOW.md) — green `master` / `next` development cycle ([PDF](GIT_FLOW.pdf))
|
- [GIT_FLOW.md](GIT_FLOW.md) — green `master` / `next` development cycle ([PDF](GIT_FLOW.pdf))
|
||||||
- [README.md](../README.md) — quick start
|
- [README.md](../README.md) — quick start
|
||||||
- [ALPHA_SOAK.md](ALPHA_SOAK.md) — two-device LAN demo QA
|
- [ALPHA.md](ALPHA.md) — alpha freeze & QA sign-off
|
||||||
- [USB_HDMI_CAST.md](USB_HDMI_CAST.md) — D & E
|
- [USB_HDMI_CAST.md](USB_HDMI_CAST.md) — D & E
|
||||||
- [COMMERCIAL.md](COMMERCIAL.md) — ads / IAP / Play
|
- [COMMERCIAL.md](COMMERCIAL.md) — ads / IAP / Play
|
||||||
- [PRO_AAR.md](PRO_AAR.md) — cast-core / cast-pro entitlements
|
- [PRO_AAR.md](PRO_AAR.md) — cast-core / cast-pro entitlements
|
||||||
|
|||||||
47
scripts/alpha-qa-logcat.sh
Executable file
47
scripts/alpha-qa-logcat.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Capture paired sender/receiver logcat for alpha QA (run from repo root).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SENDER="${1:-}"
|
||||||
|
RECEIVER="${2:-}"
|
||||||
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
OUT_DIR="$(dirname "$0")/../alpha-logs-${STAMP}"
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
TAGS='ScreenCastService:* ReceiverCastService:* AndroidCast:* SenderScreenPreview:* CastProtocol:*'
|
||||||
|
|
||||||
|
adb_cmd() {
|
||||||
|
local serial="$1"
|
||||||
|
shift
|
||||||
|
if [[ -n "$serial" ]]; then
|
||||||
|
adb -s "$serial" "$@"
|
||||||
|
else
|
||||||
|
adb "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Writing logs to $OUT_DIR"
|
||||||
|
echo "Press Ctrl+C to stop."
|
||||||
|
|
||||||
|
if [[ -n "$SENDER" ]]; then
|
||||||
|
adb_cmd "$SENDER" logcat -c
|
||||||
|
fi
|
||||||
|
if [[ -n "$RECEIVER" && "$RECEIVER" != "$SENDER" ]]; then
|
||||||
|
adb_cmd "$RECEIVER" logcat -c
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap 'echo; echo "Stopped."' INT
|
||||||
|
|
||||||
|
if [[ -n "$SENDER" ]]; then
|
||||||
|
adb_cmd "$SENDER" logcat $TAGS | tee "$OUT_DIR/sender.log" &
|
||||||
|
PID_S=$!
|
||||||
|
fi
|
||||||
|
if [[ -n "$RECEIVER" && "$RECEIVER" != "$SENDER" ]]; then
|
||||||
|
adb_cmd "$RECEIVER" logcat $TAGS | tee "$OUT_DIR/receiver.log" &
|
||||||
|
PID_R=$!
|
||||||
|
elif [[ -z "$SENDER" ]]; then
|
||||||
|
adb logcat $TAGS | tee "$OUT_DIR/combined.log"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait ${PID_S:-} ${PID_R:-}
|
||||||
Reference in New Issue
Block a user