Add GL demo app

Demonstrates rendering to a GLSurfaceView while applying a GL shader.

Issue: #6920
PiperOrigin-RevId: 293551724
This commit is contained in:
andrewlewis 2020-02-06 10:36:36 +00:00 committed by kim-vde
parent e3f43ee2ce
commit 0dd8e6a7c1
19 changed files with 1159 additions and 35 deletions

View File

@ -42,6 +42,10 @@
* UI * UI
* move logic of prev, next, fast forward and rewind to ControlDispatcher * move logic of prev, next, fast forward and rewind to ControlDispatcher
[#6926](https://github.com/google/ExoPlayer/issues/6926)). [#6926](https://github.com/google/ExoPlayer/issues/6926)).
* 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.2 (TBD) ### ### 2.11.2 (TBD) ###

11
demos/gl/README.md Normal file
View File

@ -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

53
demos/gl/build.gradle Normal file
View File

@ -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
}

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.gldemo">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-sdk/>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="com.google.android.exoplayer.gldemo.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="content"/>
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,200 @@
/*
* 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 TAG = "MainActivity";
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<ExoMediaCrypto> 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.setMediaSource(mediaSource);
player.prepare();
player.play();
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;
}
}
}

View File

@ -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.
*
* <p>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<Long> 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);
}
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:surface_type="none"/>
</FrameLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources>
<string name="application_name">ExoPlayer GL demo</string>
<string name="error_protected_content_extension_not_supported">The GL protected content extension is not supported.</string>
</resources>

View File

@ -17,23 +17,235 @@ package com.google.android.exoplayer2.util;
import static android.opengl.GLU.gluErrorString; 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.GLES11Ext;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import java.nio.Buffer;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
import java.nio.IntBuffer; import java.nio.IntBuffer;
import javax.microedition.khronos.egl.EGL10;
/** GL utility methods. */ /** GL utilities. */
public final class GlUtil { 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)}.
*
* <p>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)}.
*
* <p>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 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. */ /** Class only contains static methods. */
private GlUtil() {} 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 * If there is an OpenGl error, logs the error and if {@link
* ExoPlayerLibraryInfo#GL_ASSERTIONS_ENABLED} is true throws a {@link RuntimeException}. * ExoPlayerLibraryInfo#GL_ASSERTIONS_ENABLED} is true throws a {@link RuntimeException}.
@ -90,6 +302,34 @@ public final class GlUtil {
return program; 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. * Allocates a FloatBuffer with the given data.
* *
@ -151,4 +391,14 @@ public final class GlUtil {
throw new RuntimeException(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;
}
} }

View File

@ -20,10 +20,7 @@ import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_P
import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_SURFACELESS_CONTEXT; import static com.google.android.exoplayer2.util.EGLSurfaceTexture.SECURE_MODE_SURFACELESS_CONTEXT;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
import android.opengl.EGLDisplay;
import android.os.Handler; import android.os.Handler;
import android.os.Handler.Callback; import android.os.Handler.Callback;
import android.os.HandlerThread; import android.os.HandlerThread;
@ -34,9 +31,9 @@ import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.EGLSurfaceTexture; import com.google.android.exoplayer2.util.EGLSurfaceTexture;
import com.google.android.exoplayer2.util.EGLSurfaceTexture.SecureMode; 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.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import javax.microedition.khronos.egl.EGL10;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A dummy {@link Surface}. */ /** A dummy {@link Surface}. */
@ -45,9 +42,6 @@ public final class DummySurface extends Surface {
private static final String TAG = "DummySurface"; 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. * Whether the surface is secure.
*/ */
@ -67,7 +61,7 @@ public final class DummySurface extends Surface {
*/ */
public static synchronized boolean isSecureSupported(Context context) { public static synchronized boolean isSecureSupported(Context context) {
if (!secureModeInitialized) { if (!secureModeInitialized) {
secureMode = Util.SDK_INT < 24 ? SECURE_MODE_NONE : getSecureModeV24(context); secureMode = getSecureMode(context);
secureModeInitialized = true; secureModeInitialized = true;
} }
return secureMode != SECURE_MODE_NONE; return secureMode != SECURE_MODE_NONE;
@ -119,34 +113,21 @@ public final class DummySurface extends Surface {
} }
} }
@RequiresApi(24) @SecureMode
private static @SecureMode int getSecureModeV24(Context context) { private static int getSecureMode(Context context) {
if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { if (GlUtil.isProtectedContentExtensionSupported(context)) {
// Samsung devices running Nougat are known to be broken. See if (GlUtil.isSurfacelessContextExtensionSupported()) {
// https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. return SECURE_MODE_SURFACELESS_CONTEXT;
// Moto Z XT1650 is also affected. See } else {
// https://github.com/google/ExoPlayer/issues/3215. // 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; 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)) {
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 { private static class DummySurfaceThread extends HandlerThread implements Callback {

View File

@ -20,10 +20,12 @@ if (gradle.ext.has('exoplayerModulePrefix')) {
include modulePrefix + 'demo' include modulePrefix + 'demo'
include modulePrefix + 'demo-cast' include modulePrefix + 'demo-cast'
include modulePrefix + 'demo-gl'
include modulePrefix + 'demo-surface' include modulePrefix + 'demo-surface'
include modulePrefix + 'playbacktests' include modulePrefix + 'playbacktests'
project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main') project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main')
project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast') 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 + 'demo-surface').projectDir = new File(rootDir, 'demos/surface')
project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests')