diff --git a/src/main/java/org/thoughtcrime/securesms/calls/CallActivity.java b/src/main/java/org/thoughtcrime/securesms/calls/CallActivity.java index fcfc3c1ba..a1a1de168 100644 --- a/src/main/java/org/thoughtcrime/securesms/calls/CallActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/calls/CallActivity.java @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.R; import org.webrtc.RendererCommon; import org.webrtc.SurfaceViewRenderer; +import org.webrtc.TextureViewRenderer; import org.webrtc.VideoTrack; /** Full-screen call activity for VoIP calls */ @@ -62,7 +63,7 @@ public class CallActivity extends AppCompatActivity { private CallViewModel viewModel; - private SurfaceViewRenderer localVideoView; + private TextureViewRenderer localVideoView; private SurfaceViewRenderer remoteVideoView; // Status and info @@ -287,7 +288,6 @@ private void initializeVideoRenderers() { // Local video (the small one) localVideoView.init(EglUtils.getEglBase().getEglBaseContext(), null); localVideoView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT); - localVideoView.setZOrderMediaOverlay(true); localVideoView.setEnableHardwareScaler(true); // Remote video (full screen one) diff --git a/src/main/java/org/webrtc/TextureViewRenderer.java b/src/main/java/org/webrtc/TextureViewRenderer.java new file mode 100644 index 000000000..b661ccdce --- /dev/null +++ b/src/main/java/org/webrtc/TextureViewRenderer.java @@ -0,0 +1,241 @@ +package org.webrtc; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.SurfaceTexture; +import android.util.AttributeSet; +import android.view.TextureView; +import java.util.concurrent.CountDownLatch; + +/** + * Displays WebRTC video on a TextureView. + * + *

Unlike SurfaceViewRenderer (which extends SurfaceView and punches a transparent hole in the + * window surface), TextureView is composited as part of the normal Android view hierarchy. This + * means its contents are properly clipped by parent-view outlines, e.g. the rounded corners of a + * CardView with {@code app:cardCornerRadius}. + * + *

API mirrors the commonly used subset of SurfaceViewRenderer so callers can swap the two with + * minimal changes. + */ +public class TextureViewRenderer extends TextureView + implements TextureView.SurfaceTextureListener, VideoSink, RendererCommon.RendererEvents { + + private final String resourceName; + private final RendererCommon.VideoLayoutMeasure videoLayoutMeasure = + new RendererCommon.VideoLayoutMeasure(); + private final EglRenderer eglRenderer; + + private volatile RendererCommon.RendererEvents rendererEvents; + + private final Object layoutLock = new Object(); + + private int rotatedFrameWidth; + private int rotatedFrameHeight; + + private boolean isFirstFrameRendered; + + public TextureViewRenderer(Context context) { + super(context); + resourceName = getResourceName(); + eglRenderer = new EglRenderer(resourceName); + setSurfaceTextureListener(this); + } + + public TextureViewRenderer(Context context, AttributeSet attrs) { + super(context, attrs); + resourceName = getResourceName(); + eglRenderer = new EglRenderer(resourceName); + setSurfaceTextureListener(this); + } + + /** + * Initialize the renderer. Must be called on the main thread. + * + * @param sharedContext EGL context whose textures/framebuffers can be shared, or null. + * @param rendererEvents listener for first-frame and resolution-change callbacks, or null. + */ + public void init( + EglBase.Context sharedContext, RendererCommon.RendererEvents rendererEvents) { + ThreadUtils.checkIsOnMainThread(); + synchronized (layoutLock) { + this.rendererEvents = rendererEvents; + isFirstFrameRendered = false; + rotatedFrameWidth = 0; + rotatedFrameHeight = 0; + } + eglRenderer.init(sharedContext, EglBase.CONFIG_PLAIN, new GlRectDrawer()); + } + + /** Block until any pending EGL work is done, then release all resources. */ + public void release() { + eglRenderer.release(); + } + + public void setScalingType(RendererCommon.ScalingType scalingType) { + ThreadUtils.checkIsOnMainThread(); + videoLayoutMeasure.setScalingType(scalingType); + requestLayout(); + } + + public void setScalingType( + RendererCommon.ScalingType scalingTypeMatchOrientation, + RendererCommon.ScalingType scalingTypeMismatchOrientation) { + ThreadUtils.checkIsOnMainThread(); + videoLayoutMeasure.setScalingType( + scalingTypeMatchOrientation, scalingTypeMismatchOrientation); + requestLayout(); + } + + /** + * No-op. Hardware scaling is a SurfaceView-specific optimization and does not apply to + * TextureView. + */ + public void setEnableHardwareScaler(boolean enabled) {} + + public void setMirror(boolean mirror) { + eglRenderer.setMirror(mirror); + } + + public void setFpsReduction(float fps) { + eglRenderer.setFpsReduction(fps); + } + + public void disableFpsReduction() { + eglRenderer.disableFpsReduction(); + } + + public void pauseVideo() { + eglRenderer.pauseVideo(); + } + + public void addFrameListener( + EglRenderer.FrameListener listener, float scale, RendererCommon.GlDrawer drawerParam) { + eglRenderer.addFrameListener(listener, scale, drawerParam); + } + + public void addFrameListener(EglRenderer.FrameListener listener, float scale) { + eglRenderer.addFrameListener(listener, scale); + } + + public void removeFrameListener(EglRenderer.FrameListener listener) { + eglRenderer.removeFrameListener(listener); + } + + public void clearImage() { + eglRenderer.clearImage(); + } + + // ---- VideoSink ---- + + @Override + public void onFrame(VideoFrame frame) { + updateFrameDimensions(frame); + eglRenderer.onFrame(frame); + } + + // ---- SurfaceTextureListener ---- + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + eglRenderer.createEglSurface(surface); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + final CountDownLatch completionLatch = new CountDownLatch(1); + eglRenderer.releaseEglSurface(completionLatch::countDown); + ThreadUtils.awaitUninterruptibly(completionLatch); + return true; + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {} + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) {} + + // ---- View measurement ---- + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + ThreadUtils.checkIsOnMainThread(); + final Point size; + synchronized (layoutLock) { + size = + videoLayoutMeasure.measure(widthSpec, heightSpec, rotatedFrameWidth, rotatedFrameHeight); + } + setMeasuredDimension(size.x, size.y); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + eglRenderer.setLayoutAspectRatio((right - left) / (float) (bottom - top)); + } + + // ---- RendererCommon.RendererEvents ---- + + @Override + public void onFirstFrameRendered() { + if (rendererEvents != null) { + rendererEvents.onFirstFrameRendered(); + } + } + + @Override + public void onFrameResolutionChanged(int videoWidth, int videoHeight, int rotation) { + if (rendererEvents != null) { + rendererEvents.onFrameResolutionChanged(videoWidth, videoHeight, rotation); + } + } + + // ---- Private helpers ---- + + /** + * Track frame dimensions and fire renderer-events callbacks. Called on the render thread from + * onFrame(), so synchronize access to layout fields. + */ + private void updateFrameDimensions(VideoFrame frame) { + final boolean fireFirstFrame; + final boolean fireSizeChange; + final int newWidth; + final int newHeight; + final int rotation; + + synchronized (layoutLock) { + fireFirstFrame = !isFirstFrameRendered; + if (fireFirstFrame) { + isFirstFrameRendered = true; + } + + final int newRotatedWidth = frame.getRotatedWidth(); + final int newRotatedHeight = frame.getRotatedHeight(); + fireSizeChange = + rotatedFrameWidth != newRotatedWidth || rotatedFrameHeight != newRotatedHeight; + if (fireSizeChange) { + rotatedFrameWidth = newRotatedWidth; + rotatedFrameHeight = newRotatedHeight; + } + + newWidth = frame.getBuffer().getWidth(); + newHeight = frame.getBuffer().getHeight(); + rotation = frame.getRotation(); + } + + if (fireFirstFrame) { + post(this::onFirstFrameRendered); + } + if (fireSizeChange) { + post(this::requestLayout); + post(() -> onFrameResolutionChanged(newWidth, newHeight, rotation)); + } + } + + private String getResourceName() { + try { + return getResources().getResourceEntryName(getId()) + ": "; + } catch (android.content.res.Resources.NotFoundException e) { + return ""; + } + } +} diff --git a/src/main/res/layout/activity_call.xml b/src/main/res/layout/activity_call.xml index 200a1b193..0424bc3c2 100644 --- a/src/main/res/layout/activity_call.xml +++ b/src/main/res/layout/activity_call.xml @@ -89,7 +89,7 @@ app:layout_constraintTop_toBottomOf="@id/top_bar" app:layout_constraintEnd_toEndOf="parent"> -