Android: Expose VideoCapturer.isCapturing() and add tests

Add a boolean isCapturing() method to the VideoCapturer interface and
implement it in FileVideoCapturer, ScreenCapturerAndroid, and
CameraCapturer. Allows callers to query capture state without tracking
it externally.

Tests:
- FileVideoCapturerTest: lifecycle assertion (start -> stop).
- CameraVideoCapturerTestFixtures: shared start/stop/restart helper,
wired into Camera1 (byte buffer + texture) and Camera2 tests.

Bug: webrtc:512514764
Change-Id: I576fec9721b69e328fdd8d2cd91c819cf67c19f4
Co-authored-by: Danilo Bargen <danilo.bargen@threema.ch>
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/472200
Commit-Queue: Philipp Hancke <philipp.hancke@googlemail.com>
Reviewed-by: Magnus Jedvert <magjed@webrtc.org>
Reviewed-by: Fredrik Solenberg <solenberg@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#48054}
diff --git a/AUTHORS b/AUTHORS
index 7c4ea2c..77a7557 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -209,6 +209,7 @@
 RingCentral, Inc. <*@ringcentral.com>
 Signal Messenger, LLC <*@signal.org>
 Sinch AB <*@sinch.com>
+SoftAvail, Inc. <*@softavail.com>
 struktur AG <*@struktur.de>
 Telenor Digital AS <*@telenor.com>
 Temasys Communications <*@temasys.io>
diff --git a/sdk/android/api/org/webrtc/FileVideoCapturer.java b/sdk/android/api/org/webrtc/FileVideoCapturer.java
index 408d8b5..2cddd30 100644
--- a/sdk/android/api/org/webrtc/FileVideoCapturer.java
+++ b/sdk/android/api/org/webrtc/FileVideoCapturer.java
@@ -141,6 +141,7 @@
   private final VideoReader videoReader;
   private CapturerObserver capturerObserver;
   private final Timer timer = new Timer();
+  private boolean isCapturing;
 
   private final TimerTask tickTask = new TimerTask() {
     @Override
@@ -173,11 +174,13 @@
   @Override
   public void startCapture(int width, int height, int framerate) {
     timer.schedule(tickTask, 0, 1000 / framerate);
+    isCapturing = true;
   }
 
   @Override
   public void stopCapture() throws InterruptedException {
     timer.cancel();
+    isCapturing = false;
   }
 
   @Override
@@ -186,6 +189,11 @@
   }
 
   @Override
+  public boolean isCapturing() {
+    return isCapturing;
+  }
+
+  @Override
   public void dispose() {
     videoReader.close();
   }
diff --git a/sdk/android/api/org/webrtc/ScreenCapturerAndroid.java b/sdk/android/api/org/webrtc/ScreenCapturerAndroid.java
index 08b03bd..2855b17 100644
--- a/sdk/android/api/org/webrtc/ScreenCapturerAndroid.java
+++ b/sdk/android/api/org/webrtc/ScreenCapturerAndroid.java
@@ -217,6 +217,12 @@
     return true;
   }
 
+  @Override
+  public boolean isCapturing() {
+    // The virtual display is created in startCapture() and set to null in stopCapture()
+    return virtualDisplay != null;
+  }
+
   public long getNumCapturedFrames() {
     return numCapturedFrames;
   }
diff --git a/sdk/android/api/org/webrtc/VideoCapturer.java b/sdk/android/api/org/webrtc/VideoCapturer.java
index 67eb7ab..39a7607 100644
--- a/sdk/android/api/org/webrtc/VideoCapturer.java
+++ b/sdk/android/api/org/webrtc/VideoCapturer.java
@@ -42,6 +42,11 @@
   void changeCaptureFormat(int width, int height, int framerate);
 
   /**
+   * @return true if capturing is currently active.
+   */
+  boolean isCapturing();
+
+  /**
    * Perform any final cleanup here. No more capturing will be done after this call.
    */
   void dispose();
diff --git a/sdk/android/instrumentationtests/src/org/webrtc/Camera1CapturerUsingByteBufferTest.java b/sdk/android/instrumentationtests/src/org/webrtc/Camera1CapturerUsingByteBufferTest.java
index b83a77d..a0ad59f 100644
--- a/sdk/android/instrumentationtests/src/org/webrtc/Camera1CapturerUsingByteBufferTest.java
+++ b/sdk/android/instrumentationtests/src/org/webrtc/Camera1CapturerUsingByteBufferTest.java
@@ -202,4 +202,12 @@
   public void testStartWhileCameraIsAlreadyOpenAndStop() throws InterruptedException {
     fixtures.startWhileCameraIsAlreadyOpenAndStop();
   }
+
+  // This test that VideoCapturer.isCapturing() returns the correct value across the
+  // start -> stop -> restart -> stop lifecycle.
+  @Test
+  @MediumTest
+  public void testIsCapturing() throws InterruptedException {
+    fixtures.capturerReportsCapturingState();
+  }
 }
diff --git a/sdk/android/instrumentationtests/src/org/webrtc/Camera1CapturerUsingTextureTest.java b/sdk/android/instrumentationtests/src/org/webrtc/Camera1CapturerUsingTextureTest.java
index 582c3b8..3714dc6 100644
--- a/sdk/android/instrumentationtests/src/org/webrtc/Camera1CapturerUsingTextureTest.java
+++ b/sdk/android/instrumentationtests/src/org/webrtc/Camera1CapturerUsingTextureTest.java
@@ -205,4 +205,12 @@
   public void testStartWhileCameraIsAlreadyOpenAndStop() throws InterruptedException {
     fixtures.startWhileCameraIsAlreadyOpenAndStop();
   }
+
+  // This test that VideoCapturer.isCapturing() returns the correct value across the
+  // start -> stop -> restart -> stop lifecycle.
+  @Test
+  @MediumTest
+  public void testIsCapturing() throws InterruptedException {
+    fixtures.capturerReportsCapturingState();
+  }
 }
diff --git a/sdk/android/instrumentationtests/src/org/webrtc/Camera2CapturerTest.java b/sdk/android/instrumentationtests/src/org/webrtc/Camera2CapturerTest.java
index 5851146..781ec4e 100644
--- a/sdk/android/instrumentationtests/src/org/webrtc/Camera2CapturerTest.java
+++ b/sdk/android/instrumentationtests/src/org/webrtc/Camera2CapturerTest.java
@@ -332,4 +332,12 @@
   public void testStartWhileCameraIsAlreadyOpenAndStop() throws InterruptedException {
     fixtures.startWhileCameraIsAlreadyOpenAndStop();
   }
+
+  // This test that VideoCapturer.isCapturing() returns the correct value across the
+  // start -> stop -> restart -> stop lifecycle.
+  @Test
+  @MediumTest
+  public void testIsCapturing() throws InterruptedException {
+    fixtures.capturerReportsCapturingState();
+  }
 }
diff --git a/sdk/android/instrumentationtests/src/org/webrtc/CameraVideoCapturerTestFixtures.java b/sdk/android/instrumentationtests/src/org/webrtc/CameraVideoCapturerTestFixtures.java
index 551d75f..b42e287 100644
--- a/sdk/android/instrumentationtests/src/org/webrtc/CameraVideoCapturerTestFixtures.java
+++ b/sdk/android/instrumentationtests/src/org/webrtc/CameraVideoCapturerTestFixtures.java
@@ -790,4 +790,40 @@
 
     testObjectFactory.rawCloseCamera(competingCamera);
   }
+
+  // Verifies that VideoCapturer.isCapturing() reflects the lifecycle of
+  // startCapture()/stopCapture() across a full start/stop/restart cycle.
+  public void capturerReportsCapturingState() throws InterruptedException {
+    final CapturerInstance capturerInstance = createCapturer(true /* initialize */);
+
+    assertFalse("Should not be capturing before startCapture()",
+        capturerInstance.capturer.isCapturing());
+
+    startCapture(capturerInstance);
+    assertTrue(capturerInstance.observer.waitForCapturerToStart());
+    capturerInstance.observer.waitForNextCapturedFrame();
+    assertTrue("Should be capturing after the capturer has started",
+        capturerInstance.capturer.isCapturing());
+
+    capturerInstance.capturer.stopCapture();
+    capturerInstance.observer.releaseFrame();
+    capturerInstance.cameraEvents.waitForCameraClosed();
+    assertFalse("Should not be capturing after stopCapture()",
+        capturerInstance.capturer.isCapturing());
+
+    // Restart to verify the flag flips back to true on a subsequent start.
+    startCapture(capturerInstance);
+    assertTrue(capturerInstance.observer.waitForCapturerToStart());
+    capturerInstance.observer.waitForNextCapturedFrame();
+    assertTrue("Should be capturing again after restart",
+        capturerInstance.capturer.isCapturing());
+
+    capturerInstance.capturer.stopCapture();
+    capturerInstance.observer.releaseFrame();
+    capturerInstance.cameraEvents.waitForCameraClosed();
+    assertFalse(capturerInstance.capturer.isCapturing());
+
+    capturerInstance.capturer.dispose();
+    capturerInstance.surfaceTextureHelper.dispose();
+  }
 }
diff --git a/sdk/android/instrumentationtests/src/org/webrtc/FileVideoCapturerTest.java b/sdk/android/instrumentationtests/src/org/webrtc/FileVideoCapturerTest.java
index 8584ddf..c09f458 100644
--- a/sdk/android/instrumentationtests/src/org/webrtc/FileVideoCapturerTest.java
+++ b/sdk/android/instrumentationtests/src/org/webrtc/FileVideoCapturerTest.java
@@ -11,6 +11,7 @@
 package org.webrtc;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import android.os.Environment;
@@ -120,6 +121,33 @@
     }
   }
 
+  // This test that FileVideoCapturer.isCapturing() returns the correct value across the
+  // initialize -> start -> stop lifecycle.
+  @Test
+  @SmallTest
+  public void testIsCapturing() throws InterruptedException, IOException {
+    final FileVideoCapturer fileVideoCapturer =
+        new FileVideoCapturer(Environment.getExternalStorageDirectory().getPath()
+            + "/chromium_tests_root/sdk/android/instrumentationtests/src/org/webrtc/"
+            + "capturetestvideo.y4m");
+    final MockCapturerObserver capturerObserver = new MockCapturerObserver();
+    fileVideoCapturer.initialize(
+        null /* surfaceTextureHelper */, null /* applicationContext */, capturerObserver);
+
+    assertFalse(
+        "Should not be capturing before startCapture()", fileVideoCapturer.isCapturing());
+
+    fileVideoCapturer.startCapture(4 /* width */, 4 /* height */, 30 /* fps */);
+    assertTrue(
+        "Should be capturing after startCapture()", fileVideoCapturer.isCapturing());
+
+    fileVideoCapturer.stopCapture();
+    assertFalse(
+        "Should not be capturing after stopCapture()", fileVideoCapturer.isCapturing());
+
+    fileVideoCapturer.dispose();
+  }
+
   private static void assertByteBufferContents(byte[] expected, ByteBuffer actual) {
     assertEquals("Unexpected ByteBuffer size.", expected.length, actual.remaining());
     for (int i = 0; i < expected.length; i++) {
diff --git a/sdk/android/src/java/org/webrtc/CameraCapturer.java b/sdk/android/src/java/org/webrtc/CameraCapturer.java
index 1922a52..2375fba 100644
--- a/sdk/android/src/java/org/webrtc/CameraCapturer.java
+++ b/sdk/android/src/java/org/webrtc/CameraCapturer.java
@@ -313,6 +313,13 @@
   }
 
   @Override
+  public boolean isCapturing() {
+    synchronized (stateLock) {
+      return sessionOpening || currentSession != null;
+    }
+  }
+
+  @Override
   public void changeCaptureFormat(int width, int height, int framerate) {
     Logging.d(TAG, "changeCaptureFormat: " + width + "x" + height + "@" + framerate);
     synchronized (stateLock) {