This view must be created programmatically, as it is necessary to specify whether a context
+ * supporting protected content should be created at construction time.
+ */
+public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
+
+ /** Processes video frames, provided via a GL texture. */
+ public interface VideoProcessor {
+ /** Performs any required GL initialization. */
+ void initialize();
+
+ /** Sets the size of the output surface in pixels. */
+ void setSurfaceSize(int width, int height);
+
+ /**
+ * Draws using GL operations.
+ *
+ * @param frameTexture The ID of a GL texture containing a video frame.
+ * @param frameTimestampUs The presentation timestamp of the frame, in microseconds.
+ */
+ void draw(int frameTexture, long frameTimestampUs);
+ }
+
+ private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
+
+ private final VideoRenderer renderer;
+ private final Handler mainHandler;
+
+ @Nullable private SurfaceTexture surfaceTexture;
+ @Nullable private Surface surface;
+ @Nullable private Player.VideoComponent videoComponent;
+
+ /**
+ * Creates a new instance. Pass {@code true} for {@code requireSecureContext} if the {@link
+ * GLSurfaceView GLSurfaceView's} associated GL context should handle secure content (if the
+ * device supports it).
+ *
+ * @param context The {@link Context}.
+ * @param requireSecureContext Whether a GL context supporting protected content should be
+ * created, if supported by the device.
+ * @param videoProcessor Processor that draws to the view.
+ */
+ public VideoProcessingGLSurfaceView(
+ Context context, boolean requireSecureContext, VideoProcessor videoProcessor) {
+ super(context);
+ renderer = new VideoRenderer(videoProcessor);
+ mainHandler = new Handler();
+ setEGLContextClientVersion(2);
+ setEGLConfigChooser(
+ /* redSize= */ 8,
+ /* greenSize= */ 8,
+ /* blueSize= */ 8,
+ /* alphaSize= */ 8,
+ /* depthSize= */ 0,
+ /* stencilSize= */ 0);
+ setEGLContextFactory(
+ new EGLContextFactory() {
+ @Override
+ public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
+ int[] glAttributes;
+ if (requireSecureContext) {
+ glAttributes =
+ new int[] {
+ EGL14.EGL_CONTEXT_CLIENT_VERSION,
+ 2,
+ EGL_PROTECTED_CONTENT_EXT,
+ EGL14.EGL_TRUE,
+ EGL14.EGL_NONE
+ };
+ } else {
+ glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
+ }
+ return egl.eglCreateContext(
+ display, eglConfig, /* share_context= */ EGL10.EGL_NO_CONTEXT, glAttributes);
+ }
+
+ @Override
+ public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
+ egl.eglDestroyContext(display, context);
+ }
+ });
+ setEGLWindowSurfaceFactory(
+ new EGLWindowSurfaceFactory() {
+ @Override
+ public EGLSurface createWindowSurface(
+ EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) {
+ int[] attribsList =
+ requireSecureContext
+ ? new int[] {EGL_PROTECTED_CONTENT_EXT, EGL14.EGL_TRUE, EGL10.EGL_NONE}
+ : new int[] {EGL10.EGL_NONE};
+ return egl.eglCreateWindowSurface(display, config, nativeWindow, attribsList);
+ }
+
+ @Override
+ public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) {
+ egl.eglDestroySurface(display, surface);
+ }
+ });
+ setRenderer(renderer);
+ setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+ }
+
+ /**
+ * Attaches or detaches (if {@code newVideoComponent} is {@code null}) this view from the video
+ * component of the player.
+ *
+ * @param newVideoComponent The new video component, or {@code null} to detach this view.
+ */
+ public void setVideoComponent(@Nullable Player.VideoComponent newVideoComponent) {
+ if (newVideoComponent == videoComponent) {
+ return;
+ }
+ if (videoComponent != null) {
+ if (surface != null) {
+ videoComponent.clearVideoSurface(surface);
+ }
+ videoComponent.clearVideoFrameMetadataListener(renderer);
+ }
+ videoComponent = newVideoComponent;
+ if (videoComponent != null) {
+ videoComponent.setVideoFrameMetadataListener(renderer);
+ videoComponent.setVideoSurface(surface);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ // Post to make sure we occur in order with any onSurfaceTextureAvailable calls.
+ mainHandler.post(
+ () -> {
+ if (surface != null) {
+ if (videoComponent != null) {
+ videoComponent.setVideoSurface(null);
+ }
+ releaseSurface(surfaceTexture, surface);
+ surfaceTexture = null;
+ surface = null;
+ }
+ });
+ }
+
+ private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
+ mainHandler.post(
+ () -> {
+ SurfaceTexture oldSurfaceTexture = this.surfaceTexture;
+ Surface oldSurface = VideoProcessingGLSurfaceView.this.surface;
+ this.surfaceTexture = surfaceTexture;
+ this.surface = new Surface(surfaceTexture);
+ releaseSurface(oldSurfaceTexture, oldSurface);
+ if (videoComponent != null) {
+ videoComponent.setVideoSurface(surface);
+ }
+ });
+ }
+
+ private static void releaseSurface(
+ @Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {
+ if (oldSurfaceTexture != null) {
+ oldSurfaceTexture.release();
+ }
+ if (oldSurface != null) {
+ oldSurface.release();
+ }
+ }
+
+ private final class VideoRenderer implements GLSurfaceView.Renderer, VideoFrameMetadataListener {
+
+ private final VideoProcessor videoProcessor;
+ private final AtomicBoolean frameAvailable;
+ private final TimedValueQueue Should be called before each drawing call.
+ */
+ public void bind() {
+ Buffer buffer = Assertions.checkNotNull(this.buffer, "call setBuffer before bind");
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+ GLES20.glVertexAttribPointer(
+ location,
+ size, // count
+ GLES20.GL_FLOAT, // type
+ false, // normalize
+ 0, // stride
+ buffer);
+ GLES20.glEnableVertexAttribArray(index);
+ checkGlError();
+ }
+ }
+
+ /**
+ * GL uniform, which can be attached to a sampler using {@link Uniform#setSamplerTexId(int, int)}.
+ */
+ public static final class Uniform {
+
+ /** The name of the uniform in the GLSL sources. */
+ public final String name;
+
+ private final int location;
+ private final int type;
+ private final float[] value;
+
+ private int texId;
+ private int unit;
+
+ /**
+ * Creates a new GL uniform.
+ *
+ * @param program The identifier of a compiled and linked GLSL shader program.
+ * @param index The index of the uniform. After this instance has been constructed, the name of
+ * the uniform is available via the {@link #name} field.
+ */
+ public Uniform(int program, int index) {
+ int[] len = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, len, 0);
+
+ int[] type = new int[1];
+ int[] size = new int[1];
+ byte[] name = new byte[len[0]];
+ int[] ignore = new int[1];
+
+ GLES20.glGetActiveUniform(program, index, len[0], ignore, 0, size, 0, type, 0, name, 0);
+ this.name = new String(name, 0, strlen(name));
+ location = GLES20.glGetUniformLocation(program, this.name);
+ this.type = type[0];
+
+ value = new float[1];
+ }
+
+ /**
+ * Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform.
+ *
+ * @param texId The GL texture identifier from which to sample.
+ * @param unit The GL texture unit index.
+ */
+ public void setSamplerTexId(int texId, int unit) {
+ this.texId = texId;
+ this.unit = unit;
+ }
+
+ /** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */
+ public void setFloat(float value) {
+ this.value[0] = value;
+ }
+
+ /**
+ * Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)} or
+ * {@link #setFloat(float)}.
+ *
+ * Should be called before each drawing call.
+ */
+ public void bind() {
+ if (type == GLES20.GL_FLOAT) {
+ GLES20.glUniform1fv(location, 1, value, 0);
+ checkGlError();
+ return;
+ }
+
+ if (texId == 0) {
+ throw new IllegalStateException("call setSamplerTexId before bind");
+ }
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit);
+ if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES) {
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
+ } else if (type == GLES20.GL_SAMPLER_2D) {
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
+ } else {
+ throw new IllegalStateException("unexpected uniform type: " + type);
+ }
+ GLES20.glUniform1i(location, unit);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+ checkGlError();
+ }
+ }
+
private static final String TAG = "GlUtil";
+ private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content";
+ private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
+
/** Class only contains static methods. */
private GlUtil() {}
+ /**
+ * Returns whether creating a GL context with {@value EXTENSION_PROTECTED_CONTENT} is possible. If
+ * {@code true}, the device supports a protected output path for DRM content when using GL.
+ */
+ @TargetApi(24)
+ public static boolean isProtectedContentExtensionSupported(Context context) {
+ if (Util.SDK_INT < 24) {
+ return false;
+ }
+ if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) {
+ // Samsung devices running Nougat are known to be broken. See
+ // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802].
+ // Moto Z XT1650 is also affected. See
+ // https://github.com/google/ExoPlayer/issues/3215.
+ return false;
+ }
+ if (Util.SDK_INT < 26
+ && !context
+ .getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) {
+ // Pre API level 26 devices were not well tested unless they supported VR mode.
+ return false;
+ }
+
+ EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+ @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);
+ return eglExtensions != null && eglExtensions.contains(EXTENSION_PROTECTED_CONTENT);
+ }
+
+ /**
+ * Returns whether creating a GL context with {@value EXTENSION_SURFACELESS_CONTEXT} is possible.
+ */
+ @TargetApi(17)
+ public static boolean isSurfacelessContextExtensionSupported() {
+ if (Util.SDK_INT < 17) {
+ return false;
+ }
+ EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+ @Nullable String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);
+ return eglExtensions != null && eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT);
+ }
+
/**
* If there is an OpenGl error, logs the error and if {@link
* ExoPlayerLibraryInfo#GL_ASSERTIONS_ENABLED} is true throws a {@link RuntimeException}.
@@ -90,6 +302,34 @@ public static int compileProgram(String vertexCode, String fragmentCode) {
return program;
}
+ /** Returns the {@link Attribute}s in the specified {@code program}. */
+ public static Attribute[] getAttributes(int program) {
+ int[] attributeCount = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, 0);
+ if (attributeCount[0] != 2) {
+ throw new IllegalStateException("expected two attributes");
+ }
+
+ Attribute[] attributes = new Attribute[attributeCount[0]];
+ for (int i = 0; i < attributeCount[0]; i++) {
+ attributes[i] = new Attribute(program, i);
+ }
+ return attributes;
+ }
+
+ /** Returns the {@link Uniform}s in the specified {@code program}. */
+ public static Uniform[] getUniforms(int program) {
+ int[] uniformCount = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, 0);
+
+ Uniform[] uniforms = new Uniform[uniformCount[0]];
+ for (int i = 0; i < uniformCount[0]; i++) {
+ uniforms[i] = new Uniform(program, i);
+ }
+
+ return uniforms;
+ }
+
/**
* Allocates a FloatBuffer with the given data.
*
@@ -151,4 +391,14 @@ private static void throwGlError(String errorMsg) {
throw new RuntimeException(errorMsg);
}
}
+
+ /** Returns the length of the null-terminated string in {@code strVal}. */
+ private static int strlen(byte[] strVal) {
+ for (int i = 0; i < strVal.length; ++i) {
+ if (strVal[i] == '\0') {
+ return i;
+ }
+ }
+ return strVal.length;
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
index c03341e1574..4c7c212a799 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java
@@ -20,10 +20,7 @@
import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_SURFACELESS_CONTEXT;
import android.content.Context;
-import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
-import android.opengl.EGL14;
-import android.opengl.EGLDisplay;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.HandlerThread;
@@ -34,9 +31,9 @@
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.EGLSurfaceTexture;
import com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode;
+import com.google.android.exoplayer2.util.GlUtil;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
-import javax.microedition.khronos.egl.EGL10;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A dummy {@link Surface}. */
@@ -45,9 +42,6 @@ public final class DummySurface extends Surface {
private static final String TAG = "DummySurface";
- private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content";
- private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
-
/**
* Whether the surface is secure.
*/
@@ -67,7 +61,7 @@ public final class DummySurface extends Surface {
*/
public static synchronized boolean isSecureSupported(Context context) {
if (!secureModeInitialized) {
- secureMode = Util.SDK_INT < 24 ? SECURE_MODE_NONE : getSecureModeV24(context);
+ secureMode = getSecureMode(context);
secureModeInitialized = true;
}
return secureMode != SECURE_MODE_NONE;
@@ -119,34 +113,21 @@ private static void assertApiLevel17OrHigher() {
}
}
- @RequiresApi(24)
- private static @SecureMode int getSecureModeV24(Context context) {
- if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) {
- // Samsung devices running Nougat are known to be broken. See
- // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802].
- // Moto Z XT1650 is also affected. See
- // https://github.com/google/ExoPlayer/issues/3215.
- return SECURE_MODE_NONE;
- }
- if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) {
- // Pre API level 26 devices were not well tested unless they supported VR mode.
- return SECURE_MODE_NONE;
- }
- EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
- String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS);
- if (eglExtensions == null) {
- return SECURE_MODE_NONE;
- }
- if (!eglExtensions.contains(EXTENSION_PROTECTED_CONTENT)) {
+ @SecureMode
+ private static int getSecureMode(Context context) {
+ if (GlUtil.isProtectedContentExtensionSupported(context)) {
+ if (GlUtil.isSurfacelessContextExtensionSupported()) {
+ return SECURE_MODE_SURFACELESS_CONTEXT;
+ } else {
+ // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface.
+ // This may require support for EXT_protected_surface, but in practice it works on some
+ // devices that don't have that extension. See also
+ // https://github.com/google/ExoPlayer/issues/3558.
+ return SECURE_MODE_PROTECTED_PBUFFER;
+ }
+ } else {
return SECURE_MODE_NONE;
}
- // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. This may
- // require support for EXT_protected_surface, but in practice it works on some devices that
- // don't have that extension. See also https://github.com/google/ExoPlayer/issues/3558.
- return eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT)
- ? SECURE_MODE_SURFACELESS_CONTEXT
- : SECURE_MODE_PROTECTED_PBUFFER;
}
private static class DummySurfaceThread extends HandlerThread implements Callback {
diff --git a/settings.gradle b/settings.gradle
index 39e4791bb59..946b5b78de8 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -20,10 +20,12 @@ if (gradle.ext.has('exoplayerModulePrefix')) {
include modulePrefix + 'demo'
include modulePrefix + 'demo-cast'
+include modulePrefix + 'demo-gl'
include modulePrefix + 'demo-surface'
include modulePrefix + 'playbacktests'
project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main')
project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast')
+project(modulePrefix + 'demo-gl').projectDir = new File(rootDir, 'demos/gl')
project(modulePrefix + 'demo-surface').projectDir = new File(rootDir, 'demos/surface')
project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests')