diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a43242ab8a4..ba20700dae3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,7 @@ ([#6753](https://github.com/google/ExoPlayer/issues/6753)). * Select multiple metadata tracks if multiple metadata renderers are available ([#6676](https://github.com/google/ExoPlayer/issues/6676)). + * Add support for ID3 genres added in Wimamp 5.6 (2010). * UI: * Show ad group markers in `DefaultTimeBar` even if they are after the end of the current window @@ -54,7 +55,10 @@ This issue caused FLAC streams with other bit depths to sound like white noise on earlier releases, but only when embedded in a non-FLAC container such as Matroska or MP4. -* Add support for ID3 genres added in Wimamp 5.6 (2010). +* Demo apps: Add + [GL demo app](https://github.com/google/ExoPlayer/tree/dev-v2/demos/gl) to + show how to render video to a `GLSurfaceView` while applying a GL shader. + ([#6920](https://github.com/google/ExoPlayer/issues/6920)). ### 2.11.1 (2019-12-20) ### diff --git a/demos/gl/README.md b/demos/gl/README.md new file mode 100644 index 00000000000..12dabe902be --- /dev/null +++ b/demos/gl/README.md @@ -0,0 +1,11 @@ +# ExoPlayer GL demo + +This app demonstrates how to render video to a [GLSurfaceView][] while applying +a GL shader. + +The shader shows an overlap bitmap on top of the video. The overlay bitmap is +drawn using an Android canvas, and includes the current frame's presentation +timestamp, to show how to get the timestamp of the frame currently in the +off-screen surface texture. + +[GLSurfaceView]: https://developer.android.com/reference/android/opengl/GLSurfaceView diff --git a/demos/gl/build.gradle b/demos/gl/build.gradle new file mode 100644 index 00000000000..8fe3e040454 --- /dev/null +++ b/demos/gl/build.gradle @@ -0,0 +1,53 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +apply from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + versionName project.ext.releaseVersion + versionCode project.ext.releaseVersionCode + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.appTargetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + } + + lintOptions { + // This demo app does not have translations. + disable 'MissingTranslation' + } +} + +dependencies { + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-ui') + implementation project(modulePrefix + 'library-dash') + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion +} diff --git a/demos/gl/src/main/AndroidManifest.xml b/demos/gl/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..de47da531ed --- /dev/null +++ b/demos/gl/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/gl/src/main/assets/bitmap_overlay_video_processor_fragment.glsl b/demos/gl/src/main/assets/bitmap_overlay_video_processor_fragment.glsl new file mode 100644 index 00000000000..e54d0c256dd --- /dev/null +++ b/demos/gl/src/main/assets/bitmap_overlay_video_processor_fragment.glsl @@ -0,0 +1,35 @@ +// Copyright 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#extension GL_OES_EGL_image_external : require +precision mediump float; +// External texture containing video decoder output. +uniform samplerExternalOES tex_sampler_0; +// Texture containing the overlap bitmap. +uniform sampler2D tex_sampler_1; +// Horizontal scaling factor for the overlap bitmap. +uniform float scaleX; +// Vertical scaling factory for the overlap bitmap. +uniform float scaleY; +varying vec2 v_texcoord; +void main() { + vec4 videoColor = texture2D(tex_sampler_0, v_texcoord); + vec4 overlayColor = texture2D(tex_sampler_1, + vec2(v_texcoord.x * scaleX, + v_texcoord.y * scaleY)); + // Blend the video decoder output and the overlay bitmap. + gl_FragColor = videoColor * (1.0 - overlayColor.a) + + overlayColor * overlayColor.a; +} + diff --git a/demos/gl/src/main/assets/bitmap_overlay_video_processor_vertex.glsl b/demos/gl/src/main/assets/bitmap_overlay_video_processor_vertex.glsl new file mode 100644 index 00000000000..e333d977b2e --- /dev/null +++ b/demos/gl/src/main/assets/bitmap_overlay_video_processor_vertex.glsl @@ -0,0 +1,21 @@ +// Copyright 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +attribute vec4 a_position; +attribute vec3 a_texcoord; +varying vec2 v_texcoord; +void main() { + gl_Position = a_position; + v_texcoord = a_texcoord.xy; +} + diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java new file mode 100644 index 00000000000..063b6607513 --- /dev/null +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.gldemo; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.drawable.BitmapDrawable; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.GlUtil; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import javax.microedition.khronos.opengles.GL10; + +/** + * Video processor that demonstrates how to overlay a bitmap on video output using a GL shader. The + * bitmap is drawn using an Android {@link Canvas}. + */ +/* package */ final class BitmapOverlayVideoProcessor + implements VideoProcessingGLSurfaceView.VideoProcessor { + + private static final int OVERLAY_WIDTH = 512; + private static final int OVERLAY_HEIGHT = 256; + + private final Context context; + private final Paint paint; + private final int[] textures; + private final Bitmap overlayBitmap; + private final Bitmap logoBitmap; + private final Canvas overlayCanvas; + + private int program; + @Nullable private GlUtil.Attribute[] attributes; + @Nullable private GlUtil.Uniform[] uniforms; + + private float bitmapScaleX; + private float bitmapScaleY; + + public BitmapOverlayVideoProcessor(Context context) { + this.context = context.getApplicationContext(); + paint = new Paint(); + paint.setTextSize(64); + paint.setAntiAlias(true); + paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF); + textures = new int[1]; + overlayBitmap = Bitmap.createBitmap(OVERLAY_WIDTH, OVERLAY_HEIGHT, Bitmap.Config.ARGB_8888); + overlayCanvas = new Canvas(overlayBitmap); + try { + logoBitmap = + ((BitmapDrawable) + context.getPackageManager().getApplicationIcon(context.getPackageName())) + .getBitmap(); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException(e); + } + } + + @Override + public void initialize() { + String vertexShaderCode = + loadAssetAsString(context, "bitmap_overlay_video_processor_vertex.glsl"); + String fragmentShaderCode = + loadAssetAsString(context, "bitmap_overlay_video_processor_fragment.glsl"); + program = GlUtil.compileProgram(vertexShaderCode, fragmentShaderCode); + GlUtil.Attribute[] attributes = GlUtil.getAttributes(program); + GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program); + for (GlUtil.Attribute attribute : attributes) { + if (attribute.name.equals("a_position")) { + attribute.setBuffer( + new float[] { + -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, + 1.0f, 0.0f, 1.0f, + }, + 4); + } else if (attribute.name.equals("a_texcoord")) { + attribute.setBuffer( + new float[] { + 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, + }, + 3); + } + } + this.attributes = attributes; + this.uniforms = uniforms; + GLES20.glGenTextures(1, textures, 0); + GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT); + GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT); + GLUtils.texImage2D(GL10.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0); + } + + @Override + public void setSurfaceSize(int width, int height) { + bitmapScaleX = (float) width / OVERLAY_WIDTH; + bitmapScaleY = (float) height / OVERLAY_HEIGHT; + } + + @Override + public void draw(int frameTexture, long frameTimestampUs) { + // Draw to the canvas and store it in a texture. + String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND); + overlayBitmap.eraseColor(Color.TRANSPARENT); + overlayCanvas.drawBitmap(logoBitmap, /* left= */ 32, /* top= */ 32, paint); + overlayCanvas.drawText(text, /* x= */ 200, /* y= */ 130, paint); + GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); + GLUtils.texSubImage2D( + GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap); + GlUtil.checkGlError(); + + // Run the shader program. + GlUtil.Uniform[] uniforms = Assertions.checkNotNull(this.uniforms); + GlUtil.Attribute[] attributes = Assertions.checkNotNull(this.attributes); + GLES20.glUseProgram(program); + for (GlUtil.Uniform uniform : uniforms) { + switch (uniform.name) { + case "tex_sampler_0": + uniform.setSamplerTexId(frameTexture, /* unit= */ 0); + break; + case "tex_sampler_1": + uniform.setSamplerTexId(textures[0], /* unit= */ 1); + break; + case "scaleX": + uniform.setFloat(bitmapScaleX); + break; + case "scaleY": + uniform.setFloat(bitmapScaleY); + break; + } + } + for (GlUtil.Attribute copyExternalAttribute : attributes) { + copyExternalAttribute.bind(); + } + for (GlUtil.Uniform copyExternalUniform : uniforms) { + copyExternalUniform.bind(); + } + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); + GlUtil.checkGlError(); + } + + private static String loadAssetAsString(Context context, String assetFileName) { + @Nullable InputStream inputStream = null; + try { + inputStream = context.getAssets().open(assetFileName); + return Util.fromUtf8Bytes(Util.toByteArray(inputStream)); + } catch (IOException e) { + throw new IllegalStateException(e); + } finally { + Util.closeQuietly(inputStream); + } + } +} diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java new file mode 100644 index 00000000000..bec1cd81f5c --- /dev/null +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.gldemo; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.FrameLayout; +import android.widget.Toast; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.ui.PlayerView; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.EventLogger; +import com.google.android.exoplayer2.util.GlUtil; +import com.google.android.exoplayer2.util.Util; +import java.util.UUID; + +/** + * Activity that demonstrates playback of video to an {@link android.opengl.GLSurfaceView} with + * postprocessing of the video content using GL. + */ +public final class MainActivity extends Activity { + + private static final String DEFAULT_MEDIA_URI = + "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"; + + private static final String ACTION_VIEW = "com.google.android.exoplayer.gldemo.action.VIEW"; + private static final String EXTENSION_EXTRA = "extension"; + private static final String DRM_SCHEME_EXTRA = "drm_scheme"; + private static final String DRM_LICENSE_URL_EXTRA = "drm_license_url"; + + @Nullable private PlayerView playerView; + @Nullable private VideoProcessingGLSurfaceView videoProcessingGLSurfaceView; + + @Nullable private SimpleExoPlayer player; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + playerView = findViewById(R.id.player_view); + + Context context = getApplicationContext(); + boolean requestSecureSurface = getIntent().hasExtra(DRM_SCHEME_EXTRA); + if (requestSecureSurface && !GlUtil.isProtectedContentExtensionSupported(context)) { + Toast.makeText( + context, R.string.error_protected_content_extension_not_supported, Toast.LENGTH_LONG) + .show(); + } + + VideoProcessingGLSurfaceView videoProcessingGLSurfaceView = + new VideoProcessingGLSurfaceView( + context, requestSecureSurface, new BitmapOverlayVideoProcessor(context)); + FrameLayout contentFrame = findViewById(R.id.exo_content_frame); + contentFrame.addView(videoProcessingGLSurfaceView); + this.videoProcessingGLSurfaceView = videoProcessingGLSurfaceView; + } + + @Override + public void onStart() { + super.onStart(); + if (Util.SDK_INT > 23) { + initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + if (Util.SDK_INT <= 23 || player == null) { + initializePlayer(); + if (playerView != null) { + playerView.onResume(); + } + } + } + + @Override + public void onPause() { + super.onPause(); + if (Util.SDK_INT <= 23) { + if (playerView != null) { + playerView.onPause(); + } + releasePlayer(); + } + } + + @Override + public void onStop() { + super.onStop(); + if (Util.SDK_INT > 23) { + if (playerView != null) { + playerView.onPause(); + } + releasePlayer(); + } + } + + private void initializePlayer() { + Intent intent = getIntent(); + String action = intent.getAction(); + Uri uri = + ACTION_VIEW.equals(action) + ? Assertions.checkNotNull(intent.getData()) + : Uri.parse(DEFAULT_MEDIA_URI); + String userAgent = Util.getUserAgent(this, getString(R.string.application_name)); + DrmSessionManager drmSessionManager; + if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) { + String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); + String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); + UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); + HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent); + HttpMediaDrmCallback drmCallback = + new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); + drmSessionManager = + new DefaultDrmSessionManager.Builder() + .setUuidAndExoMediaDrmProvider(drmSchemeUuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .build(drmCallback); + } else { + drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); + } + + DataSource.Factory dataSourceFactory = + new DefaultDataSourceFactory( + this, Util.getUserAgent(this, getString(R.string.application_name))); + MediaSource mediaSource; + @C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); + if (type == C.TYPE_DASH) { + mediaSource = + new DashMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + } else if (type == C.TYPE_OTHER) { + mediaSource = + new ProgressiveMediaSource.Factory(dataSourceFactory) + .setDrmSessionManager(drmSessionManager) + .createMediaSource(uri); + } else { + throw new IllegalStateException(); + } + + SimpleExoPlayer player = new SimpleExoPlayer.Builder(getApplicationContext()).build(); + player.setRepeatMode(Player.REPEAT_MODE_ALL); + player.prepare(mediaSource); + player.setPlayWhenReady(true); + VideoProcessingGLSurfaceView videoProcessingGLSurfaceView = + Assertions.checkNotNull(this.videoProcessingGLSurfaceView); + videoProcessingGLSurfaceView.setVideoComponent( + Assertions.checkNotNull(player.getVideoComponent())); + Assertions.checkNotNull(playerView).setPlayer(player); + player.addAnalyticsListener(new EventLogger(/* trackSelector= */ null)); + this.player = player; + } + + private void releasePlayer() { + Assertions.checkNotNull(playerView).setPlayer(null); + if (player != null) { + player.release(); + Assertions.checkNotNull(videoProcessingGLSurfaceView).setVideoComponent(null); + player = null; + } + } +} diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java new file mode 100644 index 00000000000..1a41d9ec82d --- /dev/null +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.gldemo; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.media.MediaFormat; +import android.opengl.EGL14; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.os.Handler; +import android.view.Surface; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.GlUtil; +import com.google.android.exoplayer2.util.TimedValueQueue; +import com.google.android.exoplayer2.video.VideoFrameMetadataListener; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL10; + +/** + * {@link GLSurfaceView} that creates a GL context (optionally for protected content) and passes + * video frames to a {@link VideoProcessor} for drawing to the view. + * + *

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 sampleTimestampQueue; + + private int texture; + @Nullable private SurfaceTexture surfaceTexture; + + private boolean initialized; + private int width; + private int height; + private long frameTimestampUs; + + public VideoRenderer(VideoProcessor videoProcessor) { + this.videoProcessor = videoProcessor; + frameAvailable = new AtomicBoolean(); + sampleTimestampQueue = new TimedValueQueue<>(); + width = -1; + height = -1; + } + + @Override + public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) { + texture = GlUtil.createExternalTexture(); + surfaceTexture = new SurfaceTexture(texture); + surfaceTexture.setOnFrameAvailableListener( + surfaceTexture -> { + frameAvailable.set(true); + requestRender(); + }); + onSurfaceTextureAvailable(surfaceTexture); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + GLES20.glViewport(0, 0, width, height); + this.width = width; + this.height = height; + } + + @Override + public void onDrawFrame(GL10 gl) { + if (videoProcessor == null) { + return; + } + + if (!initialized) { + videoProcessor.initialize(); + initialized = true; + } + + if (width != -1 && height != -1) { + videoProcessor.setSurfaceSize(width, height); + width = -1; + height = -1; + } + + if (frameAvailable.compareAndSet(true, false)) { + SurfaceTexture surfaceTexture = Assertions.checkNotNull(this.surfaceTexture); + surfaceTexture.updateTexImage(); + long lastFrameTimestampNs = surfaceTexture.getTimestamp(); + Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs); + if (frameTimestampUs != null) { + this.frameTimestampUs = frameTimestampUs; + } + } + + videoProcessor.draw(texture, frameTimestampUs); + } + + @Override + public void onVideoFrameAboutToBeRendered( + long presentationTimeUs, + long releaseTimeNs, + Format format, + @Nullable MediaFormat mediaFormat) { + sampleTimestampQueue.add(releaseTimeNs, presentationTimeUs); + } + } +} diff --git a/demos/gl/src/main/res/layout/main_activity.xml b/demos/gl/src/main/res/layout/main_activity.xml new file mode 100644 index 00000000000..ec3868d6a88 --- /dev/null +++ b/demos/gl/src/main/res/layout/main_activity.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000000..adaa93220eb Binary files /dev/null and b/demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000000..9b6f7d5e806 Binary files /dev/null and b/demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000000..2101026c9fe Binary files /dev/null and b/demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000000..223ec8bd113 Binary files /dev/null and b/demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000000..698ed68c429 Binary files /dev/null and b/demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/demos/gl/src/main/res/values/strings.xml b/demos/gl/src/main/res/values/strings.xml new file mode 100644 index 00000000000..7e9e5d9961d --- /dev/null +++ b/demos/gl/src/main/res/values/strings.xml @@ -0,0 +1,22 @@ + + + + + ExoPlayer GL demo + + The GL protected content extension is not supported. + + diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java index c7feff516ae..cc4866118db 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java @@ -17,23 +17,235 @@ import static android.opengl.GLU.gluErrorString; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.opengl.EGL14; +import android.opengl.EGLDisplay; import android.opengl.GLES11Ext; import android.opengl.GLES20; import android.text.TextUtils; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import java.nio.IntBuffer; +import javax.microedition.khronos.egl.EGL10; -/** GL utility methods. */ +/** GL utilities. */ public final class GlUtil { + + /** + * GL attribute, which can be attached to a buffer with {@link Attribute#setBuffer(float[], int)}. + */ + public static final class Attribute { + + /** The name of the attribute in the GLSL sources. */ + public final String name; + + private final int index; + private final int location; + + @Nullable private Buffer buffer; + private int size; + + /** + * Creates a new GL attribute. + * + * @param program The identifier of a compiled and linked GLSL shader program. + * @param index The index of the attribute. After this instance has been constructed, the name + * of the attribute is available via the {@link #name} field. + */ + public Attribute(int program, int index) { + int[] len = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, len, 0); + + int[] type = new int[1]; + int[] size = new int[1]; + byte[] nameBytes = new byte[len[0]]; + int[] ignore = new int[1]; + + GLES20.glGetActiveAttrib(program, index, len[0], ignore, 0, size, 0, type, 0, nameBytes, 0); + name = new String(nameBytes, 0, strlen(nameBytes)); + location = GLES20.glGetAttribLocation(program, name); + this.index = index; + } + + /** + * Configures {@link #bind()} to attach vertices in {@code buffer} (each of size {@code size} + * elements) to this {@link Attribute}. + * + * @param buffer Buffer to bind to this attribute. + * @param size Number of elements per vertex. + */ + public void setBuffer(float[] buffer, int size) { + this.buffer = createBuffer(buffer); + this.size = size; + } + + /** + * Sets the vertex attribute to whatever was attached via {@link #setBuffer(float[], int)}. + * + *

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 d1f874b428d..0a900999b19 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 @@ -21,10 +21,7 @@ import android.annotation.TargetApi; 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,22 +31,17 @@ 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}. - */ +/** A dummy {@link Surface}. */ @TargetApi(17) 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. */ @@ -69,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; @@ -121,34 +113,21 @@ private static void assertApiLevel17OrHigher() { } } - @TargetApi(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')