mirror of
git://f0xx.org/android_cast
synced 2026-07-29 08:19:00 +03:00
triage tags api
This commit is contained in:
@@ -264,6 +264,7 @@ public final class AppPreferences {
|
||||
if (s.getNetworkAdoption() == CastSettings.NetworkAdoption.AUTO) {
|
||||
s.setNetworkAdoption(CastSettings.NetworkAdoption.ESTIMATED);
|
||||
}
|
||||
clampAlphaTransport(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -401,6 +402,7 @@ public final class AppPreferences {
|
||||
if (s.getNetworkAdoption() == CastSettings.NetworkAdoption.AUTO) {
|
||||
s.setNetworkAdoption(CastSettings.NetworkAdoption.ESTIMATED);
|
||||
}
|
||||
clampAlphaTransport(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -472,6 +474,15 @@ public final class AppPreferences {
|
||||
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) {
|
||||
if (context == 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 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). */
|
||||
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 String DEFAULT_PIN = "1234";
|
||||
|
||||
@@ -161,8 +161,10 @@ public final class CastSettingsBinder {
|
||||
valueList.add(CastConfig.TRANSPORT_UDP);
|
||||
labelList.add(activity.getString(R.string.transport_tcp));
|
||||
valueList.add(CastConfig.TRANSPORT_TCP);
|
||||
labelList.add(activity.getString(R.string.transport_quic));
|
||||
valueList.add(CastConfig.TRANSPORT_QUIC);
|
||||
if (!CastConfig.ALPHA_FEATURE_FREEZE) {
|
||||
labelList.add(activity.getString(R.string.transport_quic));
|
||||
valueList.add(CastConfig.TRANSPORT_QUIC);
|
||||
}
|
||||
if (AppPreferences.isDevUsbTetherTransportEnabled(activity)) {
|
||||
labelList.add(activity.getString(R.string.transport_usb_tether));
|
||||
valueList.add(CastConfig.TRANSPORT_USB_TETHER);
|
||||
|
||||
@@ -120,6 +120,7 @@ public class ReceiverCastService extends Service {
|
||||
private boolean streamIdle;
|
||||
private volatile boolean castEnded;
|
||||
private long lastVideoFrameMs;
|
||||
private long lastStreamConfigMs;
|
||||
private final TransportLossStats transportLoss = new TransportLossStats();
|
||||
private SessionStatsRecorder sessionStatsRecorder;
|
||||
|
||||
@@ -300,6 +301,8 @@ public class ReceiverCastService extends Service {
|
||||
|
||||
@Override
|
||||
public void onVideoFrame(long ptsUs, boolean keyFrame, byte[] data) {
|
||||
final long nowMs = System.currentTimeMillis();
|
||||
mainHandler.post(() -> lastVideoFrameMs = nowMs);
|
||||
if (awaitingKeyframe && !keyFrame) {
|
||||
synchronized (transportLoss) {
|
||||
transportLoss.recvVideoSkippedAwaitingKey++;
|
||||
@@ -313,7 +316,6 @@ public class ReceiverCastService extends Service {
|
||||
final int frameBytes = data != null ? data.length : 0;
|
||||
final byte[] frameData = data;
|
||||
mainHandler.post(() -> {
|
||||
lastVideoFrameMs = System.currentTimeMillis();
|
||||
if (streamIdle) {
|
||||
onStreamResumedAfterIdle();
|
||||
}
|
||||
@@ -484,11 +486,16 @@ public class ReceiverCastService extends Service {
|
||||
}
|
||||
|
||||
private void checkStreamIdle() {
|
||||
if (phase != Phase.STREAMING || streamIdle || lastVideoFrameMs <= 0
|
||||
|| awaitingKeyframe || reconfiguring) {
|
||||
if (phase != Phase.STREAMING || streamIdle || lastVideoFrameMs <= 0) {
|
||||
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) {
|
||||
Log.i(TAG, "Stream idle " + idleMs + "ms — awaiting");
|
||||
if (sessionStatsRecorder != null) {
|
||||
@@ -594,6 +601,8 @@ public class ReceiverCastService extends Service {
|
||||
return;
|
||||
}
|
||||
boolean firstConfig = !hasPendingConfig;
|
||||
boolean wasIdle = streamIdle;
|
||||
boolean decoderWasActive = decoderActive;
|
||||
phase = Phase.STREAMING;
|
||||
streamIdle = false;
|
||||
if (firstConfig) {
|
||||
@@ -602,7 +611,10 @@ public class ReceiverCastService extends Service {
|
||||
reconfiguring = true;
|
||||
enterAwaitingKeyframe();
|
||||
lastVideoFrameMs = System.currentTimeMillis();
|
||||
openPlaybackAwaiting(R.string.playback_awaiting_stream);
|
||||
lastStreamConfigMs = lastVideoFrameMs;
|
||||
if (firstConfig || wasIdle || !decoderWasActive) {
|
||||
openPlaybackAwaiting(R.string.playback_awaiting_stream);
|
||||
}
|
||||
pendingWidth = size.width;
|
||||
pendingHeight = size.height;
|
||||
displayWidth = size.width;
|
||||
|
||||
@@ -78,6 +78,7 @@ public class ScreenCastService extends Service implements
|
||||
private static final String TAG = "ScreenCastService";
|
||||
private static final int CONNECT_RETRIES = 20;
|
||||
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_DATA = "result_data";
|
||||
@@ -126,6 +127,9 @@ public class ScreenCastService extends Service implements
|
||||
private int captureDensity;
|
||||
private DisplayManager displayManager;
|
||||
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 volatile boolean calibrationMode;
|
||||
private volatile boolean cameraMode;
|
||||
@@ -188,6 +192,7 @@ public class ScreenCastService extends Service implements
|
||||
return START_STICKY;
|
||||
}
|
||||
CastSettings startSettings = IntentExtras.getCastSettings(intent);
|
||||
CastActiveState.setSenderCasting(true);
|
||||
startSenderForeground(getString(R.string.connecting),
|
||||
startSettings != null && startSettings.isAudioEnabled());
|
||||
ioThread = new Thread(() -> runCast(intent), "ScreenCastIO");
|
||||
@@ -609,17 +614,74 @@ public class ScreenCastService extends Service implements
|
||||
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||
wm.getDefaultDisplay().getRealMetrics(metrics);
|
||||
CastResolution.Size size = CastResolution.compute(metrics, activeSettings, getPeerStats());
|
||||
scheduleCaptureSizeUpdate(size, "rotation");
|
||||
}
|
||||
|
||||
private boolean captureSizeChangeSignificant(CastResolution.Size size) {
|
||||
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;
|
||||
}
|
||||
try {
|
||||
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) {
|
||||
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() {
|
||||
if (networkFeedback != null && networkFeedback.getPlane() instanceof PassThroughNetworkControlPlane) {
|
||||
return ((PassThroughNetworkControlPlane) networkFeedback.getPlane()).getLastPeerStats();
|
||||
@@ -709,19 +771,7 @@ public class ScreenCastService extends Service implements
|
||||
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||
wm.getDefaultDisplay().getRealMetrics(metrics);
|
||||
CastResolution.Size size = CastResolution.compute(metrics, activeSettings, peer);
|
||||
int dw = Math.abs(size.width - captureWidth);
|
||||
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);
|
||||
}
|
||||
});
|
||||
scheduleCaptureSizeUpdate(size, "adaptive");
|
||||
}
|
||||
|
||||
private void runOnProjectionThread(Runnable action) {
|
||||
@@ -1131,6 +1181,8 @@ public class ScreenCastService extends Service implements
|
||||
}
|
||||
|
||||
private void releaseProjectionCapture() {
|
||||
cancelPendingCaptureSizeUpdate();
|
||||
captureReconfigureInFlight.set(false);
|
||||
unregisterDisplayListener();
|
||||
if (virtualDisplay != null) {
|
||||
virtualDisplay.release();
|
||||
|
||||
@@ -462,7 +462,7 @@ public class SenderActivity extends DrawerHostActivity {
|
||||
if (grantResults.length > 0 && grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
launchCastService(null, Activity.RESULT_OK);
|
||||
} else {
|
||||
Toast.makeText(this, R.string.capture_camera_not_implemented, Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(this, R.string.capture_camera_permission_denied, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ public final class SenderCapturePreview {
|
||||
Context ctx = textureView.getContext();
|
||||
if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.CAMERA)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
showLabel(R.string.capture_camera_not_implemented);
|
||||
showLabel(R.string.capture_camera_permission_denied);
|
||||
return;
|
||||
}
|
||||
labelView.setVisibility(android.view.View.GONE);
|
||||
|
||||
@@ -78,7 +78,7 @@ public final class SenderScreenPreview {
|
||||
}
|
||||
|
||||
public void attach(TextureView textureView, CastSettings.CaptureMode mode) {
|
||||
if (!needsProjection(mode) || !hasConsent()) {
|
||||
if (CastActiveState.isSenderCasting() || !needsProjection(mode) || !hasConsent()) {
|
||||
return;
|
||||
}
|
||||
boundTexture = textureView;
|
||||
@@ -137,7 +137,7 @@ public final class SenderScreenPreview {
|
||||
}
|
||||
|
||||
private void ensureVirtualDisplay(TextureView textureView, CastSettings.CaptureMode mode) {
|
||||
if (!needsProjection(mode) || !hasConsent()) {
|
||||
if (CastActiveState.isSenderCasting() || !needsProjection(mode) || !hasConsent()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -212,6 +212,14 @@ public final class SenderScreenPreview {
|
||||
boundTexture = null;
|
||||
boundMode = null;
|
||||
pausePreview();
|
||||
if (projection != null) {
|
||||
try {
|
||||
projection.stop();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
projection = null;
|
||||
}
|
||||
SenderPreviewService.stop(appContext);
|
||||
}
|
||||
|
||||
/** Release consent and active capture (leaving sender screen). */
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
<string name="capture_calibration_test">Тест сигнала</string>
|
||||
<string name="capture_camera">Камера (лицевая)</string>
|
||||
<string name="capture_camera_unavailable">Источник для камеры (Android 10+)</string>
|
||||
<string name="capture_camera_not_implemented">Захват с камеры в стадии разработки. Выберите другой метод.</string>
|
||||
<string name="capture_camera_not_implemented">Камера недоступна на этом устройстве. Выберите экран или калибровку.</string>
|
||||
<string name="capture_camera_permission_denied">Нет доступа к камере. Разрешите в настройках системы.</string>
|
||||
<string name="casting_calibration">Передача калибрации %1$dx%2$d</string>
|
||||
<string name="casting_camera">Трансляция с камеры %1$dx%2$d</string>
|
||||
<string name="quality_low">Низкий (~1.5 Mbps, 24 fps)</string>
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
<string name="capture_calibration_test">Calibration test pattern</string>
|
||||
<string name="capture_camera">Camera (rear Camera2)</string>
|
||||
<string name="capture_camera_unavailable">Camera source (requires Android 10+)</string>
|
||||
<string name="capture_camera_not_implemented">Camera capture is not implemented yet. Use screen or calibration test.</string>
|
||||
<string name="capture_camera_not_implemented">Camera unavailable on this device. Use screen or calibration test.</string>
|
||||
<string name="capture_camera_permission_denied">Camera permission denied. Enable it in system settings to use camera cast.</string>
|
||||
<string name="casting_calibration">Calibration cast %1$dx%2$d</string>
|
||||
<string name="casting_camera">Camera cast %1$dx%2$d</string>
|
||||
<string name="quality_low">Low (~1.5 Mbps, 24 fps)</string>
|
||||
|
||||
Reference in New Issue
Block a user