1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 08:37:37 +03:00

befoer bwest

This commit is contained in:
Anton Afanasyeu
2026-05-15 19:03:03 +02:00
parent ff96afc12b
commit a0cb4325ad
9 changed files with 98 additions and 20 deletions

View File

@@ -24,7 +24,8 @@ public final class CastConfig {
public static final boolean SHOW_CAST_DIAGNOSTICS = true; public static final boolean SHOW_CAST_DIAGNOSTICS = true;
/** No video packets for this long → treat stream as stopped (robot overlay). */ /** No video packets for this long → treat stream as stopped (robot overlay). */
public static final long STREAM_IDLE_TIMEOUT_MS = 4_000; /** No inbound video this long → robot overlay (keep above ~2× slowest expected frame interval). */
public static final long STREAM_IDLE_TIMEOUT_MS = 8_000;
private CastConfig() {} private CastConfig() {}
} }

View File

@@ -31,6 +31,12 @@ public final class CastResolution {
encH = maxH & ~1; encH = maxH & ~1;
encW = Math.max(2, ((int) (encW * ratio)) & ~1); encW = Math.max(2, ((int) (encW * ratio)) & ~1);
} }
int maxW = settings.getMaxWidth();
if (maxW > 0 && encW > maxW) {
float ratio = (float) maxW / encW;
encW = maxW & ~1;
encH = Math.max(2, ((int) (encH * ratio)) & ~1);
}
int density = Math.max(120, (int) (display.densityDpi * scale)); int density = Math.max(120, (int) (display.densityDpi * scale));
return new Size(encW, encH, density); return new Size(encW, encH, density);

View File

@@ -22,17 +22,19 @@ public class CastSettings implements Serializable {
/** Capture scale relative to physical display (1.0 = native). */ /** Capture scale relative to physical display (1.0 = native). */
public enum Resolution { public enum Resolution {
FULL(1.0f, 0), FULL(1.0f, 0, 0),
THREE_QUARTERS(0.75f, 0), THREE_QUARTERS(0.75f, 0, 0),
HALF(0.5f, 0), HALF(0.5f, 0, 0),
HD_720P(1.0f, 720); HD_720P(1.0f, 720, 1280);
public final float scale; public final float scale;
public final int maxHeight; public final int maxHeight;
public final int maxWidth;
Resolution(float scale, int maxHeight) { Resolution(float scale, int maxHeight, int maxWidth) {
this.scale = scale; this.scale = scale;
this.maxHeight = maxHeight; this.maxHeight = maxHeight;
this.maxWidth = maxWidth;
} }
} }
@@ -97,4 +99,8 @@ public class CastSettings implements Serializable {
public int getMaxHeight() { public int getMaxHeight() {
return resolution.maxHeight; return resolution.maxHeight;
} }
public int getMaxWidth() {
return resolution.maxWidth;
}
} }

View File

@@ -15,6 +15,8 @@ public class PassThroughNetworkControlPlane implements NetworkControlPlane {
private final NetworkControlAdvice outbound = NetworkControlAdvice.none(); private final NetworkControlAdvice outbound = NetworkControlAdvice.none();
private long statsWindowBytes; private long statsWindowBytes;
private long statsWindowStartMs; private long statsWindowStartMs;
private long sendWindowBytes;
private long sendWindowStartMs;
@Override @Override
public NetworkControlAdvice tick(long nowMs) { public NetworkControlAdvice tick(long nowMs) {
@@ -41,7 +43,7 @@ public class PassThroughNetworkControlPlane implements NetworkControlPlane {
} }
} }
/** Receiver calls this when a video/audio packet arrives (for local bitrate estimate). */ /** Record inbound (receiver) or outbound (sender) payload bytes for bitrate stats. */
public void recordBytes(int bytes) { public void recordBytes(int bytes) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (statsWindowStartMs == 0) { if (statsWindowStartMs == 0) {
@@ -50,6 +52,14 @@ public class PassThroughNetworkControlPlane implements NetworkControlPlane {
statsWindowBytes += bytes; statsWindowBytes += bytes;
} }
public void recordSendBytes(int bytes) {
long now = System.currentTimeMillis();
if (sendWindowStartMs == 0) {
sendWindowStartMs = now;
}
sendWindowBytes += bytes;
}
@Override @Override
public NetworkStatsSnapshot buildLocalStats(long nowMs) { public NetworkStatsSnapshot buildLocalStats(long nowMs) {
NetworkStatsSnapshot s = new NetworkStatsSnapshot(); NetworkStatsSnapshot s = new NetworkStatsSnapshot();
@@ -57,13 +67,29 @@ public class PassThroughNetworkControlPlane implements NetworkControlPlane {
s.timestampMs = nowMs; s.timestampMs = nowMs;
s.rttMs = lastPeer != null ? lastPeer.rttMs : 0; s.rttMs = lastPeer != null ? lastPeer.rttMs : 0;
s.lossRatio = 0f; s.lossRatio = 0f;
s.sendBitrateKbps = 0; s.sendBitrateKbps = estimateSendKbps(nowMs);
s.recvBitrateKbps = estimateRecvKbps(nowMs); s.recvBitrateKbps = estimateRecvKbps(nowMs);
s.congestionLevel = 0; s.congestionLevel = 0;
s.jitterMs = 0; s.jitterMs = 0;
return s; return s;
} }
private int estimateSendKbps(long nowMs) {
if (sendWindowStartMs == 0 || sendWindowBytes == 0) {
return 0;
}
long elapsed = nowMs - sendWindowStartMs;
if (elapsed < 200) {
return 0;
}
int kbps = (int) (sendWindowBytes * 8L / elapsed);
if (elapsed > 4000) {
sendWindowStartMs = nowMs;
sendWindowBytes = 0;
}
return kbps;
}
private int estimateRecvKbps(long nowMs) { private int estimateRecvKbps(long nowMs) {
if (statsWindowStartMs == 0 || statsWindowBytes == 0) { if (statsWindowStartMs == 0 || statsWindowBytes == 0) {
return 0; return 0;

View File

@@ -208,6 +208,7 @@ public class ReceiverCastService extends Service {
} }
if (keyFrame) { if (keyFrame) {
awaitingKeyframe = false; awaitingKeyframe = false;
reconfiguring = false;
} }
final int frameBytes = data != null ? data.length : 0; final int frameBytes = data != null ? data.length : 0;
mainHandler.post(() -> { mainHandler.post(() -> {
@@ -392,10 +393,12 @@ public class ReceiverCastService extends Service {
Log.d(TAG, "Stream config unchanged " + size.width + "x" + size.height); Log.d(TAG, "Stream config unchanged " + size.width + "x" + size.height);
return; return;
} }
boolean firstConfig = !hasPendingConfig;
phase = Phase.STREAMING; phase = Phase.STREAMING;
streamIdle = false; streamIdle = false;
if (firstConfig) {
streamMetrics.reset(); streamMetrics.reset();
lastVideoFrameMs = 0; }
reconfiguring = true; reconfiguring = true;
awaitingKeyframe = true; awaitingKeyframe = true;
openPlaybackAwaiting(R.string.playback_awaiting_stream); openPlaybackAwaiting(R.string.playback_awaiting_stream);
@@ -405,10 +408,8 @@ public class ReceiverCastService extends Service {
pendingCsd1 = csd1; pendingCsd1 = csd1;
hasPendingConfig = true; hasPendingConfig = true;
decoderActive = false; decoderActive = false;
mainHandler.postDelayed(() -> awaitingKeyframe = false, 2000);
notifyPlaybackDimensions(pendingWidth, pendingHeight); notifyPlaybackDimensions(pendingWidth, pendingHeight);
scheduleDecoderConfigureRetries(); scheduleDecoderConfigureRetries();
reconfiguring = false;
Log.i(TAG, "Stream config " + pendingWidth + "x" + pendingHeight); Log.i(TAG, "Stream config " + pendingWidth + "x" + pendingHeight);
} }
@@ -514,6 +515,7 @@ public class ReceiverCastService extends Service {
} }
}); });
decoderActive = true; decoderActive = true;
reconfiguring = false;
decoderWidth = pendingWidth; decoderWidth = pendingWidth;
decoderHeight = pendingHeight; decoderHeight = pendingHeight;
updateStatus(getString(R.string.playing_resolution, pendingWidth, pendingHeight)); updateStatus(getString(R.string.playing_resolution, pendingWidth, pendingHeight));

View File

@@ -112,11 +112,22 @@ public final class StreamMetrics {
sb.append("BW in (est): ").append(localKbps > 0 ? localKbps + " kbps" : "n/a"); sb.append("BW in (est): ").append(localKbps > 0 ? localKbps + " kbps" : "n/a");
sb.append(" RTT: ").append(peerRttMs > 0 ? peerRttMs + " ms" : "n/a").append('\n'); sb.append(" RTT: ").append(peerRttMs > 0 ? peerRttMs + " ms" : "n/a").append('\n');
if (peerRecvKbps > 0 || peerSendKbps > 0) { if (peerRecvKbps > 0 || peerSendKbps > 0) {
sb.append("Peer stats: rx ").append(peerRecvKbps).append(" / tx ") sb.append("Sender (peer): tx ").append(peerSendKbps).append(" / rx ")
.append(peerSendKbps).append(" kbps\n"); .append(peerRecvKbps).append(" kbps\n");
} }
sb.append("Video pkts: ").append(videoPackets); sb.append("Video pkts: ").append(videoPackets);
sb.append(" (IDR ").append(keyFrames).append(" / P ").append(deltaFrames).append(")\n"); sb.append(" (IDR ").append(keyFrames).append(" / P ").append(deltaFrames).append(')');
if (videoPackets > 0) {
int avgKb = (int) (videoBytes / videoPackets / 1024);
sb.append(" ~").append(Math.max(1, avgKb)).append(" KB/frame");
}
sb.append('\n');
if (inFps > 0 && inFps < 10f) {
sb.append("Hint: low In FPS → sender encode or TCP backlog\n");
}
if (renderFps > 0 && inFps > renderFps + 3f) {
sb.append("Hint: decode slower than network\n");
}
sb.append("Audio pkts: ").append(audioPackets).append('\n'); sb.append("Audio pkts: ").append(audioPackets).append('\n');
sb.append("Video bytes: ").append(formatBytes(videoBytes)); sb.append("Video bytes: ").append(formatBytes(videoBytes));
sb.append(" Audio bytes: ").append(formatBytes(audioBytes)).append('\n'); sb.append(" Audio bytes: ").append(formatBytes(audioBytes)).append('\n');

View File

@@ -4,6 +4,7 @@ import android.util.Log;
import com.foxx.androidcast.network.CastProtocol; import com.foxx.androidcast.network.CastProtocol;
import com.foxx.androidcast.network.CastSession; import com.foxx.androidcast.network.CastSession;
import com.foxx.androidcast.network.control.PassThroughNetworkControlPlane;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
@@ -21,10 +22,25 @@ public final class CastSendPump {
private Thread thread; private Thread thread;
private CastSession session; private CastSession session;
private Runnable onSendFailed; private Runnable onSendFailed;
private PassThroughNetworkControlPlane statsPlane;
private volatile int droppedPFrames;
public int getDroppedPFrames() {
return droppedPFrames;
}
public int getQueueDepth() {
return queue.size();
}
public void start(CastSession session, Runnable onSendFailed) { public void start(CastSession session, Runnable onSendFailed) {
start(session, onSendFailed, null);
}
public void start(CastSession session, Runnable onSendFailed, PassThroughNetworkControlPlane statsPlane) {
this.session = session; this.session = session;
this.onSendFailed = onSendFailed; this.onSendFailed = onSendFailed;
this.statsPlane = statsPlane;
if (!running.compareAndSet(false, true)) { if (!running.compareAndSet(false, true)) {
return; return;
} }
@@ -57,11 +73,14 @@ public final class CastSendPump {
return; return;
} }
if (queue.size() >= MAX_P_FRAMES_BACKLOG) { if (queue.size() >= MAX_P_FRAMES_BACKLOG) {
droppedPFrames++;
return; return;
} }
if (!queue.offer(item)) { if (!queue.offer(item)) {
queue.poll(); queue.poll();
queue.offer(item); if (!queue.offer(item)) {
droppedPFrames++;
}
} }
} }
@@ -82,7 +101,11 @@ public final class CastSendPump {
if (item == null || session == null) { if (item == null || session == null) {
continue; continue;
} }
session.send(item.type, item.buildPayload()); byte[] payload = item.buildPayload();
session.send(item.type, payload);
if (statsPlane != null) {
statsPlane.recordSendBytes(payload.length + 5);
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
if (!running.get()) { if (!running.get()) {
break; break;

View File

@@ -31,6 +31,7 @@ import com.foxx.androidcast.network.CastProtocol;
import com.foxx.androidcast.network.CastSession; import com.foxx.androidcast.network.CastSession;
import com.foxx.androidcast.network.CastTransportFactory; import com.foxx.androidcast.network.CastTransportFactory;
import com.foxx.androidcast.network.control.NetworkFeedbackManager; import com.foxx.androidcast.network.control.NetworkFeedbackManager;
import com.foxx.androidcast.network.control.PassThroughNetworkControlPlane;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@@ -159,9 +160,11 @@ public class ScreenCastService extends Service {
connectWithRetry(host, port, settings); connectWithRetry(host, port, settings);
updateStatus(getString(R.string.authenticating)); updateStatus(getString(R.string.authenticating));
session.clientHandshake(deviceName != null ? deviceName : "Sender", pin, settings); session.clientHandshake(deviceName != null ? deviceName : "Sender", pin, settings);
sendPump = new CastSendPump();
sendPump.start(session, () -> stopping.set(true));
networkFeedback = new NetworkFeedbackManager(session, true); networkFeedback = new NetworkFeedbackManager(session, true);
PassThroughNetworkControlPlane plane =
(PassThroughNetworkControlPlane) networkFeedback.getPlane();
sendPump = new CastSendPump();
sendPump.start(session, () -> stopping.set(true), plane);
casting.set(true); casting.set(true);
activeSettings = settings; activeSettings = settings;
startCaptureOnMainThread(resultCode, new Intent(data), settings); startCaptureOnMainThread(resultCode, new Intent(data), settings);

View File

@@ -28,7 +28,7 @@
<string name="resolution_full">Full display</string> <string name="resolution_full">Full display</string>
<string name="resolution_75">75% scale</string> <string name="resolution_75">75% scale</string>
<string name="resolution_half">50% scale</string> <string name="resolution_half">50% scale</string>
<string name="resolution_720p">Cap height 720p</string> <string name="resolution_720p">720p (max 1280×720)</string>
<string name="transport_mismatch">Transport must match the receiver (TCP/UDP)</string> <string name="transport_mismatch">Transport must match the receiver (TCP/UDP)</string>
<string name="select_receiver">Select a receiver first</string> <string name="select_receiver">Select a receiver first</string>
<string name="enter_pin">Enter PIN</string> <string name="enter_pin">Enter PIN</string>