1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 05:17:39 +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;
/** 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() {}
}

View File

@@ -31,6 +31,12 @@ public final class CastResolution {
encH = maxH & ~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));
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). */
public enum Resolution {
FULL(1.0f, 0),
THREE_QUARTERS(0.75f, 0),
HALF(0.5f, 0),
HD_720P(1.0f, 720);
FULL(1.0f, 0, 0),
THREE_QUARTERS(0.75f, 0, 0),
HALF(0.5f, 0, 0),
HD_720P(1.0f, 720, 1280);
public final float scale;
public final int maxHeight;
public final int maxWidth;
Resolution(float scale, int maxHeight) {
Resolution(float scale, int maxHeight, int maxWidth) {
this.scale = scale;
this.maxHeight = maxHeight;
this.maxWidth = maxWidth;
}
}
@@ -97,4 +99,8 @@ public class CastSettings implements Serializable {
public int getMaxHeight() {
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 long statsWindowBytes;
private long statsWindowStartMs;
private long sendWindowBytes;
private long sendWindowStartMs;
@Override
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) {
long now = System.currentTimeMillis();
if (statsWindowStartMs == 0) {
@@ -50,6 +52,14 @@ public class PassThroughNetworkControlPlane implements NetworkControlPlane {
statsWindowBytes += bytes;
}
public void recordSendBytes(int bytes) {
long now = System.currentTimeMillis();
if (sendWindowStartMs == 0) {
sendWindowStartMs = now;
}
sendWindowBytes += bytes;
}
@Override
public NetworkStatsSnapshot buildLocalStats(long nowMs) {
NetworkStatsSnapshot s = new NetworkStatsSnapshot();
@@ -57,13 +67,29 @@ public class PassThroughNetworkControlPlane implements NetworkControlPlane {
s.timestampMs = nowMs;
s.rttMs = lastPeer != null ? lastPeer.rttMs : 0;
s.lossRatio = 0f;
s.sendBitrateKbps = 0;
s.sendBitrateKbps = estimateSendKbps(nowMs);
s.recvBitrateKbps = estimateRecvKbps(nowMs);
s.congestionLevel = 0;
s.jitterMs = 0;
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) {
if (statsWindowStartMs == 0 || statsWindowBytes == 0) {
return 0;

View File

@@ -208,6 +208,7 @@ public class ReceiverCastService extends Service {
}
if (keyFrame) {
awaitingKeyframe = false;
reconfiguring = false;
}
final int frameBytes = data != null ? data.length : 0;
mainHandler.post(() -> {
@@ -392,10 +393,12 @@ public class ReceiverCastService extends Service {
Log.d(TAG, "Stream config unchanged " + size.width + "x" + size.height);
return;
}
boolean firstConfig = !hasPendingConfig;
phase = Phase.STREAMING;
streamIdle = false;
streamMetrics.reset();
lastVideoFrameMs = 0;
if (firstConfig) {
streamMetrics.reset();
}
reconfiguring = true;
awaitingKeyframe = true;
openPlaybackAwaiting(R.string.playback_awaiting_stream);
@@ -405,10 +408,8 @@ public class ReceiverCastService extends Service {
pendingCsd1 = csd1;
hasPendingConfig = true;
decoderActive = false;
mainHandler.postDelayed(() -> awaitingKeyframe = false, 2000);
notifyPlaybackDimensions(pendingWidth, pendingHeight);
scheduleDecoderConfigureRetries();
reconfiguring = false;
Log.i(TAG, "Stream config " + pendingWidth + "x" + pendingHeight);
}
@@ -514,6 +515,7 @@ public class ReceiverCastService extends Service {
}
});
decoderActive = true;
reconfiguring = false;
decoderWidth = pendingWidth;
decoderHeight = 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(" RTT: ").append(peerRttMs > 0 ? peerRttMs + " ms" : "n/a").append('\n');
if (peerRecvKbps > 0 || peerSendKbps > 0) {
sb.append("Peer stats: rx ").append(peerRecvKbps).append(" / tx ")
.append(peerSendKbps).append(" kbps\n");
sb.append("Sender (peer): tx ").append(peerSendKbps).append(" / rx ")
.append(peerRecvKbps).append(" kbps\n");
}
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("Video bytes: ").append(formatBytes(videoBytes));
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.CastSession;
import com.foxx.androidcast.network.control.PassThroughNetworkControlPlane;
import java.io.IOException;
import java.util.concurrent.LinkedBlockingQueue;
@@ -21,10 +22,25 @@ public final class CastSendPump {
private Thread thread;
private CastSession session;
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) {
start(session, onSendFailed, null);
}
public void start(CastSession session, Runnable onSendFailed, PassThroughNetworkControlPlane statsPlane) {
this.session = session;
this.onSendFailed = onSendFailed;
this.statsPlane = statsPlane;
if (!running.compareAndSet(false, true)) {
return;
}
@@ -57,11 +73,14 @@ public final class CastSendPump {
return;
}
if (queue.size() >= MAX_P_FRAMES_BACKLOG) {
droppedPFrames++;
return;
}
if (!queue.offer(item)) {
queue.poll();
queue.offer(item);
if (!queue.offer(item)) {
droppedPFrames++;
}
}
}
@@ -82,7 +101,11 @@ public final class CastSendPump {
if (item == null || session == null) {
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) {
if (!running.get()) {
break;

View File

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

View File

@@ -28,7 +28,7 @@
<string name="resolution_full">Full display</string>
<string name="resolution_75">75% 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="select_receiver">Select a receiver first</string>
<string name="enter_pin">Enter PIN</string>