mirror of
git://f0xx.org/android_cast
synced 2026-07-29 08:37:37 +03:00
befoer bwest
This commit is contained in:
@@ -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() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user