From 646e690d9f0e72b5a603438421aef322aaa0e3d4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 6 Feb 2020 10:36:36 +0000 Subject: [PATCH] Add GL demo app Demonstrates rendering to a GLSurfaceView while applying a GL shader. Issue: #6920 PiperOrigin-RevId: 293551724 --- RELEASENOTES.md | 6 +- demos/gl/README.md | 11 + demos/gl/build.gradle | 53 ++++ demos/gl/src/main/AndroidManifest.xml | 49 +++ ...tmap_overlay_video_processor_fragment.glsl | 35 +++ ...bitmap_overlay_video_processor_vertex.glsl | 21 ++ .../gldemo/BitmapOverlayVideoProcessor.java | 176 +++++++++++ .../exoplayer2/gldemo/MainActivity.java | 197 ++++++++++++ .../gldemo/VideoProcessingGLSurfaceView.java | 290 ++++++++++++++++++ .../gl/src/main/res/layout/main_activity.xml | 30 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3394 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2184 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4886 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7492 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10801 bytes demos/gl/src/main/res/values/strings.xml | 22 ++ .../android/exoplayer2/util/GlUtil.java | 252 ++++++++++++++- .../exoplayer2/video/DummySurface.java | 53 +--- settings.gradle | 2 + 19 files changed, 1158 insertions(+), 39 deletions(-) create mode 100644 demos/gl/README.md create mode 100644 demos/gl/build.gradle create mode 100644 demos/gl/src/main/AndroidManifest.xml create mode 100644 demos/gl/src/main/assets/bitmap_overlay_video_processor_fragment.glsl create mode 100644 demos/gl/src/main/assets/bitmap_overlay_video_processor_vertex.glsl create mode 100644 demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java create mode 100644 demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java create mode 100644 demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java create mode 100644 demos/gl/src/main/res/layout/main_activity.xml create mode 100644 demos/gl/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 demos/gl/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 demos/gl/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 demos/gl/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 demos/gl/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 demos/gl/src/main/res/values/strings.xml 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 0000000000000000000000000000000000000000..adaa93220eb81c58e5c85874d1cf127a3188e419 GIT binary patch literal 3394 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 KMNUMnLSTZLL=Vyc literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2101026c9fe1b48ab11a23fd440cf2f80af2afae GIT binary patch literal 4886 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 IM6N<$f{P}YasU7T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..223ec8bd1132aab759469bd5ad2232f35f7be2c2 GIT binary patch literal 7492 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%0Dj^J4_SbOxi?LOAH?pqcJIPrNphD%I2G8C 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 literal 0 HcmV?d00001 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')