diff --git a/README.md b/README.md index 78f7d91..2507a07 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Android Cast (POC) -**Roadmap & alpha scope:** [docs/ROADMAP.md](docs/ROADMAP.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 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) -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. +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. ## Features @@ -13,13 +13,16 @@ Minimal LAN screen-casting proof of concept in **Java**. Captures screen + playb - Playback audio capture (API 29+) → AAC-LC - Runtime notification permission (Android 13+) -## Quick start +## Quick start (alpha demo) 1. Install the same APK on two devices on the **same Wi‑Fi**. -2. **Receiver** (TV): **Receive** → match transport (TCP/UDP) & options → tap **Start listening** (notification appears; service keeps running). -3. **Sender** (tablet): **Send** → **same transport** → pick receiver → PIN → approve screen capture. -4. Watch the **notification** on both devices for connection status (not only the toast). -5. Stop from the notification **Stop** action. +2. **Receiver:** **Receive** → transport **UDP** → stream protection **None** → **Start listening**. +3. **Sender:** **Send** → **UDP** → pick **one** receiver → PIN → approve screen capture. +4. Sender resolution: **Original**, **720p**, **480p**, or **Auto** (settings drawer). +5. Watch **notifications** on both devices for status (not only toasts). +6. Stop from the sender notification **Stop** action. + +Full soak script: [docs/ALPHA_SOAK.md](docs/ALPHA_SOAK.md). ### If video never appears @@ -43,11 +46,13 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk | Option | Effect | |--------|--------| -| **Transport TCP** | Reliable delivery; default | -| **Transport UDP** | Lower latency, may drop frames (POC) | -| **Quality Low/Med/High** | Video bitrate 0.8 / 2 / 4 Mbps and FPS 20 / 24 / 30 | -| **Resolution** | Scales VirtualDisplay before encode (full, 75%, 50%, or max 720p height) | +| **Transport UDP** | Default; lower latency | +| **Transport TCP** | Reliable fallback | +| **Sender resolution** | **Original** (native), **720p** / **480p** (long-edge cap), **Auto** (adaptive from peer stats) | +| **Bitrate** | Estimated / quality / speed modes in settings | +| **Stream protection** | FEC/NACK optional on UDP — use **None** for alpha demo | | **Audio** | `AudioPlaybackCapture` + AAC (Android 10+) | +| **Camera capture** | Rear/front Camera2 (Android 10+); requires camera permission | ## Protocol diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e4e6476..ad90760 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,11 +42,6 @@ android:exported="false" android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden|density" /> - - - * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8 - * class ReceiverActivity - * Contributors: - * - Anton Afanasyeu (6 commits, 163 lines) - * - Cursor Agent (project assistant) - * Digest: SHA256 5ca140ebc07f2ed3353cce1db093f850230b3ed1657466551f96c7c1b8349f88 -**********************************************************************/ -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.TextView; - -import androidx.appcompat.app.AppCompatActivity; - -import com.foxx.androidcast.AppPreferences; -import com.foxx.androidcast.CastActivityTracker; -import com.foxx.androidcast.CastSettings; -import com.foxx.androidcast.ICastReceiverService; -import com.foxx.androidcast.ICastStatusCallback; -import com.foxx.androidcast.PermissionHelper; -import com.foxx.androidcast.R; - -/** Receiver setup — options live in {@link com.foxx.androidcast.CastSettingsActivity}. */ -public class ReceiverActivity extends AppCompatActivity { - private final CastSettings castSettings = new CastSettings(); - - private TextView statusText; - private ICastReceiverService receiverService; - private boolean bound; - - private final ICastStatusCallback.Stub statusCallback = new ICastStatusCallback.Stub() { - @Override - public void onStatus(String status) { - runOnUiThread(() -> statusText.setText(status)); - } - - @Override - public void onAuthenticated(String senderName) { - runOnUiThread(() -> statusText.setText(getString(R.string.casting_from, senderName))); - } - - @Override - public void onStreamStarted(int width, int height) { - runOnUiThread(() -> statusText.setText(getString(R.string.playback_opened, width, height))); - } - - @Override - public void onVideoRendering() { - runOnUiThread(() -> statusText.setText(R.string.playback_rendering)); - } - - @Override - public void onStreamIdle() { - runOnUiThread(() -> statusText.setText(R.string.playback_awaiting_stream)); - } - - @Override - public void onDisconnected() { - runOnUiThread(() -> statusText.setText(R.string.waiting_sender)); - } - }; - - private final ServiceConnection connection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - receiverService = ICastReceiverService.Stub.asInterface(service); - bound = true; - try { - receiverService.registerCallback(statusCallback); - statusText.setText(receiverService.getStatus()); - } catch (RemoteException e) { - statusText.setText(e.getMessage()); - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - receiverService = null; - bound = false; - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_receiver); - PermissionHelper.request(this); - castSettings.copyFrom(AppPreferences.loadReceiverDefaults(this)); - - statusText = findViewById(R.id.text_status); - findViewById(R.id.btn_start_receiver).setOnClickListener(v -> startReceiverService()); - } - - @Override - protected void onResume() { - super.onResume(); - CastActivityTracker.onResume(this); - PermissionHelper.remindIfMissing(this, AppPreferences.isPlayIncomingAudio(this)); - updateKeepScreenOn(); - } - - @Override - protected void onStart() { - super.onStart(); - bindService(new Intent(this, ReceiverCastService.class), connection, Context.BIND_AUTO_CREATE); - } - - @Override - protected void onStop() { - if (bound && receiverService != null) { - try { - receiverService.unregisterCallback(statusCallback); - } catch (RemoteException ignored) { - } - unbindService(connection); - bound = false; - receiverService = null; - } - super.onStop(); - } - - @Override - protected void onPause() { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - super.onPause(); - } - - private void updateKeepScreenOn() { - if (bound && receiverService != null) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - } - - private void startReceiverService() { - String pin = AppPreferences.getPin(this); - boolean allowAudio = AppPreferences.isPlayIncomingAudio(this); - ReceiverPrefs.setAllowAudio(this, allowAudio); - - Intent intent = new Intent(this, ReceiverCastService.class); - intent.setAction(ReceiverCastService.ACTION_START); - intent.putExtra(ReceiverCastService.EXTRA_PIN, pin); - intent.putExtra(ReceiverCastService.EXTRA_ALLOW_AUDIO, allowAudio); - intent.putExtra(CastSettings.EXTRA, castSettings); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent); - } else { - startService(intent); - } - statusText.setText(R.string.receiver_starting); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } -} diff --git a/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java b/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java index 2500186..5ba3000 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java +++ b/app/src/main/java/com/foxx/androidcast/sender/ScreenCastService.java @@ -231,7 +231,7 @@ public class ScreenCastService extends Service implements cameraMode = settings.isCameraCapture(); calibrationMode = settings.getCaptureMode() == CastSettings.CaptureMode.CALIBRATION_TEST; if (cameraMode && !com.foxx.androidcast.PlatformCastSupport.isCameraCaptureModeSupported()) { - failAndStop(getString(R.string.capture_camera_not_implemented)); + failAndStop(getString(R.string.capture_camera_requires_api29)); return; } int castPort = com.foxx.androidcast.CastConfig.portForTransport(settings.getTransport()); diff --git a/app/src/main/java/com/foxx/androidcast/sender/SenderActivity.java b/app/src/main/java/com/foxx/androidcast/sender/SenderActivity.java index 50a11f6..c41d554 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/SenderActivity.java +++ b/app/src/main/java/com/foxx/androidcast/sender/SenderActivity.java @@ -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(); } } } diff --git a/app/src/main/java/com/foxx/androidcast/sender/SenderCapturePreview.java b/app/src/main/java/com/foxx/androidcast/sender/SenderCapturePreview.java index 75fb7bb..83f5a1c 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/SenderCapturePreview.java +++ b/app/src/main/java/com/foxx/androidcast/sender/SenderCapturePreview.java @@ -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); @@ -207,7 +207,7 @@ public final class SenderCapturePreview { CameraManager mgr = ctx.getSystemService(CameraManager.class); String id = chooseCamera(mgr, useFront); if (id == null) { - showLabel(R.string.capture_camera_not_implemented); + showLabel(R.string.capture_camera_open_failed); return; } surface.setDefaultBufferSize(640, 480); @@ -256,7 +256,7 @@ public final class SenderCapturePreview { }, handler); } catch (CameraAccessException | SecurityException e) { Log.w(TAG, "Open camera failed", e); - showLabel(R.string.capture_camera_not_implemented); + showLabel(R.string.capture_camera_open_failed); } } diff --git a/app/src/main/java/com/foxx/androidcast/sender/SenderResolutionUi.java b/app/src/main/java/com/foxx/androidcast/sender/SenderResolutionUi.java index 19ad957..d2ece53 100644 --- a/app/src/main/java/com/foxx/androidcast/sender/SenderResolutionUi.java +++ b/app/src/main/java/com/foxx/androidcast/sender/SenderResolutionUi.java @@ -6,6 +6,7 @@ import androidx.appcompat.app.AppCompatActivity; import android.widget.Spinner; +import com.foxx.androidcast.BuildConfig; import com.foxx.androidcast.AppPreferences; import com.foxx.androidcast.CastSettings; import com.foxx.androidcast.CastSettingsBinder; @@ -16,6 +17,9 @@ public final class SenderResolutionUi { private SenderResolutionUi() {} public static SenderResolutionUiMode mode(Context context) { + if (!BuildConfig.DEBUG) { + return SenderResolutionUiMode.SEPARATED; + } return AppPreferences.getDevSenderResolutionUiMode(context); } diff --git a/app/src/main/res/layout/activity_receiver.xml b/app/src/main/res/layout/activity_receiver.xml deleted file mode 100644 index bcca5d3..0000000 --- a/app/src/main/res/layout/activity_receiver.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - -