Commit 9c337c88 by eguven Committed by Oliver Woodman

Add monoscopic 360 surface type to PlayerView

Using this surface it's possible to play 360 videos in a non-VR Activity that is
affected by phone and touch input.

RELNOTES=true

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=205720776
parent 01b69854
...@@ -65,6 +65,7 @@ ...@@ -65,6 +65,7 @@
* Allow setting the `Looper`, which is used to access the player, in * Allow setting the `Looper`, which is used to access the player, in
`ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)). `ExoPlayerFactory` ([#4278](https://github.com/google/ExoPlayer/issues/4278)).
* Use default Deserializers if non given to DownloadManager. * Use default Deserializers if non given to DownloadManager.
* Add monoscopic 360 surface type to PlayerView.
* Deprecate `Player.DefaultEventListener` as selective listener overrides can * Deprecate `Player.DefaultEventListener` as selective listener overrides can
be directly made with the `Player.EventListener` interface. be directly made with the `Player.EventListener` interface.
* Deprecate `DefaultAnalyticsListener` as selective listener overrides can be * Deprecate `DefaultAnalyticsListener` as selective listener overrides can be
......
...@@ -191,6 +191,9 @@ public class PlayerActivity extends Activity ...@@ -191,6 +191,9 @@ public class PlayerActivity extends Activity
super.onStart(); super.onStart();
if (Util.SDK_INT > 23) { if (Util.SDK_INT > 23) {
initializePlayer(); initializePlayer();
if (playerView != null) {
playerView.onResume();
}
} }
} }
...@@ -199,6 +202,9 @@ public class PlayerActivity extends Activity ...@@ -199,6 +202,9 @@ public class PlayerActivity extends Activity
super.onResume(); super.onResume();
if (Util.SDK_INT <= 23 || player == null) { if (Util.SDK_INT <= 23 || player == null) {
initializePlayer(); initializePlayer();
if (playerView != null) {
playerView.onResume();
}
} }
} }
...@@ -206,6 +212,9 @@ public class PlayerActivity extends Activity ...@@ -206,6 +212,9 @@ public class PlayerActivity extends Activity
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
if (Util.SDK_INT <= 23) { if (Util.SDK_INT <= 23) {
if (playerView != null) {
playerView.onPause();
}
releasePlayer(); releasePlayer();
} }
} }
...@@ -214,6 +223,9 @@ public class PlayerActivity extends Activity ...@@ -214,6 +223,9 @@ public class PlayerActivity extends Activity
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
if (Util.SDK_INT > 23) { if (Util.SDK_INT > 23) {
if (playerView != null) {
playerView.onPause();
}
releasePlayer(); releasePlayer();
} }
} }
......
...@@ -80,6 +80,9 @@ public final class C { ...@@ -80,6 +80,9 @@ public final class C {
/** The number of bits per byte. */ /** The number of bits per byte. */
public static final int BITS_PER_BYTE = 8; public static final int BITS_PER_BYTE = 8;
/** The number of bytes per float. */
public static final int BYTES_PER_FLOAT = 4;
/** /**
* The name of the ASCII charset. * The name of the ASCII charset.
*/ */
......
...@@ -51,6 +51,9 @@ public final class ExoPlayerLibraryInfo { ...@@ -51,6 +51,9 @@ public final class ExoPlayerLibraryInfo {
*/ */
public static final boolean ASSERTIONS_ENABLED = true; public static final boolean ASSERTIONS_ENABLED = true;
/** Whether an exception should be thrown in case of an OpenGl error. */
public static final boolean GL_ASSERTIONS_ENABLED = false;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.TraceUtil} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.TraceUtil}
* trace enabled. * trace enabled.
......
...@@ -42,6 +42,7 @@ dependencies { ...@@ -42,6 +42,7 @@ dependencies {
implementation 'com.android.support:support-media-compat:' + supportLibraryVersion implementation 'com.android.support:support-media-compat:' + supportLibraryVersion
implementation 'com.android.support:support-annotations:' + supportLibraryVersion implementation 'com.android.support:support-annotations:' + supportLibraryVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
} }
ext { ext {
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ui; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ui;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.content.res.TypedArray; import android.content.res.TypedArray;
...@@ -30,6 +31,7 @@ import android.util.AttributeSet; ...@@ -30,6 +31,7 @@ import android.util.AttributeSet;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceView; import android.view.SurfaceView;
import android.view.TextureView; import android.view.TextureView;
import android.view.View; import android.view.View;
...@@ -44,6 +46,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; ...@@ -44,6 +46,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Player.VideoComponent;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
...@@ -52,6 +55,7 @@ import com.google.android.exoplayer2.text.TextOutput; ...@@ -52,6 +55,7 @@ import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.ErrorMessageProvider;
import com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.RepeatModeUtil;
...@@ -118,11 +122,11 @@ import java.util.List; ...@@ -118,11 +122,11 @@ import java.util.List;
* <li>Default: {@code fit} * <li>Default: {@code fit}
* </ul> * </ul>
* <li><b>{@code surface_type}</b> - The type of surface view used for video playbacks. Valid * <li><b>{@code surface_type}</b> - The type of surface view used for video playbacks. Valid
* values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} * values are {@code surface_view}, {@code texture_view}, {@code spherical_view} and {@code
* is recommended for audio only applications, since creating the surface can be expensive. * none}. Using {@code none} is recommended for audio only applications, since creating the
* Using {@code surface_view} is recommended for video applications. Note, TextureView can * surface can be expensive. Using {@code surface_view} is recommended for video applications.
* only be used in a hardware accelerated window. When rendered in software, TextureView will * Note, TextureView can only be used in a hardware accelerated window. When rendered in
* draw nothing. * software, TextureView will draw nothing.
* <ul> * <ul>
* <li>Corresponding method: None * <li>Corresponding method: None
* <li>Default: {@code surface_view} * <li>Default: {@code surface_view}
...@@ -231,6 +235,7 @@ public class PlayerView extends FrameLayout { ...@@ -231,6 +235,7 @@ public class PlayerView extends FrameLayout {
private static final int SURFACE_TYPE_NONE = 0; private static final int SURFACE_TYPE_NONE = 0;
private static final int SURFACE_TYPE_SURFACE_VIEW = 1; private static final int SURFACE_TYPE_SURFACE_VIEW = 1;
private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; private static final int SURFACE_TYPE_TEXTURE_VIEW = 2;
private static final int SURFACE_TYPE_MONO360_VIEW = 3;
private final AspectRatioFrameLayout contentFrame; private final AspectRatioFrameLayout contentFrame;
private final View shutterView; private final View shutterView;
...@@ -351,10 +356,20 @@ public class PlayerView extends FrameLayout { ...@@ -351,10 +356,20 @@ public class PlayerView extends FrameLayout {
ViewGroup.LayoutParams params = ViewGroup.LayoutParams params =
new ViewGroup.LayoutParams( new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
surfaceView = switch (surfaceType) {
surfaceType == SURFACE_TYPE_TEXTURE_VIEW case SURFACE_TYPE_TEXTURE_VIEW:
? new TextureView(context) surfaceView = new TextureView(context);
: new SurfaceView(context); break;
case SURFACE_TYPE_MONO360_VIEW:
Assertions.checkState(Util.SDK_INT >= 15);
SphericalSurfaceView sphericalSurfaceView = new SphericalSurfaceView(context);
sphericalSurfaceView.setSurfaceListener(componentListener);
surfaceView = sphericalSurfaceView;
break;
default:
surfaceView = new SurfaceView(context);
break;
}
surfaceView.setLayoutParams(params); surfaceView.setLayoutParams(params);
contentFrame.addView(surfaceView, 0); contentFrame.addView(surfaceView, 0);
} else { } else {
...@@ -469,6 +484,8 @@ public class PlayerView extends FrameLayout { ...@@ -469,6 +484,8 @@ public class PlayerView extends FrameLayout {
oldVideoComponent.removeVideoListener(componentListener); oldVideoComponent.removeVideoListener(componentListener);
if (surfaceView instanceof TextureView) { if (surfaceView instanceof TextureView) {
oldVideoComponent.clearVideoTextureView((TextureView) surfaceView); oldVideoComponent.clearVideoTextureView((TextureView) surfaceView);
} else if (surfaceView instanceof SphericalSurfaceView) {
oldVideoComponent.clearVideoSurface(((SphericalSurfaceView) surfaceView).getSurface());
} else if (surfaceView instanceof SurfaceView) { } else if (surfaceView instanceof SurfaceView) {
oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView); oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView);
} }
...@@ -493,6 +510,8 @@ public class PlayerView extends FrameLayout { ...@@ -493,6 +510,8 @@ public class PlayerView extends FrameLayout {
if (newVideoComponent != null) { if (newVideoComponent != null) {
if (surfaceView instanceof TextureView) { if (surfaceView instanceof TextureView) {
newVideoComponent.setVideoTextureView((TextureView) surfaceView); newVideoComponent.setVideoTextureView((TextureView) surfaceView);
} else if (surfaceView instanceof SphericalSurfaceView) {
newVideoComponent.setVideoSurface(((SphericalSurfaceView) surfaceView).getSurface());
} else if (surfaceView instanceof SurfaceView) { } else if (surfaceView instanceof SurfaceView) {
newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView); newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView);
} }
...@@ -636,7 +655,7 @@ public class PlayerView extends FrameLayout { ...@@ -636,7 +655,7 @@ public class PlayerView extends FrameLayout {
* Sets whether a buffering spinner is displayed when the player is in the buffering state. The * Sets whether a buffering spinner is displayed when the player is in the buffering state. The
* buffering spinner is not displayed by default. * buffering spinner is not displayed by default.
* *
* @param showBuffering Whether the buffering icon is displayer * @param showBuffering Whether the buffering icon is displayed
*/ */
public void setShowBuffering(boolean showBuffering) { public void setShowBuffering(boolean showBuffering) {
if (this.showBuffering != showBuffering) { if (this.showBuffering != showBuffering) {
...@@ -913,10 +932,12 @@ public class PlayerView extends FrameLayout { ...@@ -913,10 +932,12 @@ public class PlayerView extends FrameLayout {
* <li>{@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code * <li>{@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code
* surface_view}. * surface_view}.
* <li>{@link TextureView} if {@code surface_type} is {@code texture_view}. * <li>{@link TextureView} if {@code surface_type} is {@code texture_view}.
* <li>{@link SphericalSurfaceView} if {@code surface_type} is {@code spherical_view}.
* <li>{@code null} if {@code surface_type} is {@code none}. * <li>{@code null} if {@code surface_type} is {@code none}.
* </ul> * </ul>
* *
* @return The {@link SurfaceView}, {@link TextureView} or {@code null}. * @return The {@link SurfaceView}, {@link TextureView}, {@link SphericalSurfaceView} or {@code
* null}.
*/ */
public View getVideoSurfaceView() { public View getVideoSurfaceView() {
return surfaceView; return surfaceView;
...@@ -965,6 +986,32 @@ public class PlayerView extends FrameLayout { ...@@ -965,6 +986,32 @@ public class PlayerView extends FrameLayout {
return true; return true;
} }
/**
* Should be called when the player is visible to the user and if {@code surface_type} is {@code
* spherical_view}. It is the counterpart to {@link #onPause()}.
*
* <p>This method should typically be called in {@link Activity#onStart()} (or {@link
* Activity#onResume()} for API version <= 23).
*/
public void onResume() {
if (surfaceView instanceof SphericalSurfaceView) {
((SphericalSurfaceView) surfaceView).onResume();
}
}
/**
* Should be called when the player is no longer visible to the user and if {@code surface_type}
* is {@code spherical_view}. It is the counterpart to {@link #onResume()}.
*
* <p>This method should typically be called in {@link Activity#onStop()} (or {@link
* Activity#onPause()} for API version <= 23).
*/
public void onPause() {
if (surfaceView instanceof SphericalSurfaceView) {
((SphericalSurfaceView) surfaceView).onPause();
}
}
/** Shows the playback controls, but only if forced or shown indefinitely. */ /** Shows the playback controls, but only if forced or shown indefinitely. */
private void maybeShowController(boolean isForced) { private void maybeShowController(boolean isForced) {
if (isPlayingAd() && controllerHideDuringAds) { if (isPlayingAd() && controllerHideDuringAds) {
...@@ -1180,7 +1227,11 @@ public class PlayerView extends FrameLayout { ...@@ -1180,7 +1227,11 @@ public class PlayerView extends FrameLayout {
} }
private final class ComponentListener private final class ComponentListener
implements Player.EventListener, TextOutput, VideoListener, OnLayoutChangeListener { implements Player.EventListener,
TextOutput,
VideoListener,
OnLayoutChangeListener,
SphericalSurfaceView.SurfaceListener {
// TextOutput implementation // TextOutput implementation
...@@ -1219,6 +1270,8 @@ public class PlayerView extends FrameLayout { ...@@ -1219,6 +1270,8 @@ public class PlayerView extends FrameLayout {
surfaceView.addOnLayoutChangeListener(this); surfaceView.addOnLayoutChangeListener(this);
} }
applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); applyTextureViewRotation((TextureView) surfaceView, textureViewRotation);
} else if (surfaceView instanceof SphericalSurfaceView) {
videoAspectRatio = 0;
} }
contentFrame.setAspectRatio(videoAspectRatio); contentFrame.setAspectRatio(videoAspectRatio);
...@@ -1271,5 +1324,17 @@ public class PlayerView extends FrameLayout { ...@@ -1271,5 +1324,17 @@ public class PlayerView extends FrameLayout {
int oldBottom) { int oldBottom) {
applyTextureViewRotation((TextureView) view, textureViewRotation); applyTextureViewRotation((TextureView) view, textureViewRotation);
} }
// SphericalSurfaceView.SurfaceTextureListener implementation
@Override
public void surfaceChanged(@Nullable Surface surface) {
if (player != null) {
VideoComponent videoComponent = player.getVideoComponent();
if (videoComponent != null) {
videoComponent.setVideoSurface(surface);
}
}
}
} }
} }
/*
* Copyright (C) 2018 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.ui.spherical;
import static com.google.android.exoplayer2.ui.spherical.Utils.checkGlError;
import android.graphics.SurfaceTexture;
import android.opengl.GLES20;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.ui.spherical.Mesh.EyeType;
import com.google.android.exoplayer2.util.Assertions;
import java.util.concurrent.atomic.AtomicBoolean;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Renders a GL Scene.
*
* <p>All methods should be called only on the GL thread unless GL thread is stopped.
*/
/*package*/ final class SceneRenderer {
private final AtomicBoolean frameAvailable;
private int textureId;
@Nullable private SurfaceTexture surfaceTexture;
@MonotonicNonNull private Mesh mesh;
private boolean meshInitialized;
public SceneRenderer() {
frameAvailable = new AtomicBoolean();
}
/** Initializes the renderer. */
public SurfaceTexture init() {
// Set the background frame color. This is only visible if the display mesh isn't a full sphere.
GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
checkGlError();
textureId = Utils.createExternalTexture();
surfaceTexture = new SurfaceTexture(textureId);
surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> frameAvailable.set(true));
return surfaceTexture;
}
/** Sets a {@link Mesh} to be used to display video. */
public void setMesh(Mesh mesh) {
if (this.mesh != null) {
this.mesh.shutdown();
}
this.mesh = mesh;
meshInitialized = false;
}
/**
* Draws the scene with a given eye pose and type.
*
* @param viewProjectionMatrix 16 element GL matrix.
* @param eyeType an {@link EyeType} value
*/
public void drawFrame(float[] viewProjectionMatrix, int eyeType) {
if (mesh == null) {
return;
}
if (!meshInitialized) {
meshInitialized = true;
mesh.init();
}
// glClear isn't strictly necessary when rendering fully spherical panoramas, but it can improve
// performance on tiled renderers by causing the GPU to discard previous data.
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
checkGlError();
if (frameAvailable.compareAndSet(true, false)) {
Assertions.checkNotNull(surfaceTexture).updateTexImage();
checkGlError();
}
mesh.draw(textureId, viewProjectionMatrix, eyeType);
}
}
/*
* Copyright (C) 2018 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.ui.spherical;
import static android.opengl.GLU.gluErrorString;
import android.annotation.TargetApi;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.text.TextUtils;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
/** GL utility methods. */
/*package*/ final class Utils {
private static final String TAG = "Spherical.Utils";
/** Class only contains static methods. */
private Utils() {}
/**
* If there is an OpenGl error, logs the error and if {@link
* ExoPlayerLibraryInfo#GL_ASSERTIONS_ENABLED} is true throws a {@link RuntimeException}.
*/
public static void checkGlError() {
int error = GLES20.glGetError();
int lastError;
if (error != GLES20.GL_NO_ERROR) {
do {
lastError = error;
Log.e(TAG, "glError " + gluErrorString(lastError));
error = GLES20.glGetError();
} while (error != GLES20.GL_NO_ERROR);
if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED) {
throw new RuntimeException("glError " + gluErrorString(lastError));
}
}
}
/**
* Builds a GL shader program from vertex & fragment shader code. The vertex and fragment shaders
* are passed as arrays of strings in order to make debugging compilation issues easier.
*
* @param vertexCode GLES20 vertex shader program.
* @param fragmentCode GLES20 fragment shader program.
* @return GLES20 program id.
*/
public static int compileProgram(String[] vertexCode, String[] fragmentCode) {
checkGlError();
// prepare shaders and OpenGL program
int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
GLES20.glShaderSource(vertexShader, TextUtils.join("\n", vertexCode));
GLES20.glCompileShader(vertexShader);
checkGlError();
int fragmentShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
GLES20.glShaderSource(fragmentShader, TextUtils.join("\n", fragmentCode));
GLES20.glCompileShader(fragmentShader);
checkGlError();
int program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
// Link and check for errors.
GLES20.glLinkProgram(program);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
String errorMsg = "Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(program);
Log.e(TAG, errorMsg);
if (ExoPlayerLibraryInfo.GL_ASSERTIONS_ENABLED) {
throw new RuntimeException(errorMsg);
}
}
checkGlError();
return program;
}
/** Allocates a FloatBuffer with the given data. */
public static FloatBuffer createBuffer(float[] data) {
ByteBuffer bb = ByteBuffer.allocateDirect(data.length * C.BYTES_PER_FLOAT);
bb.order(ByteOrder.nativeOrder());
FloatBuffer buffer = bb.asFloatBuffer();
buffer.put(data);
buffer.position(0);
return buffer;
}
/**
* Creates a GL_TEXTURE_EXTERNAL_OES with default configuration of GL_LINEAR filtering and
* GL_CLAMP_TO_EDGE wrapping.
*/
@TargetApi(15)
public static int createExternalTexture() {
int[] texId = new int[1];
GLES20.glGenTextures(1, IntBuffer.wrap(texId));
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId[0]);
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
checkGlError();
return texId[0];
}
}
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
<enum name="none" value="0"/> <enum name="none" value="0"/>
<enum name="surface_view" value="1"/> <enum name="surface_view" value="1"/>
<enum name="texture_view" value="2"/> <enum name="texture_view" value="2"/>
<enum name="spherical_view" value="3"/>
</attr> </attr>
<attr name="show_timeout" format="integer"/> <attr name="show_timeout" format="integer"/>
<attr name="rewind_increment" format="integer"/> <attr name="rewind_increment" format="integer"/>
......
/*
* Copyright (C) 2018 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.ui.spherical;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link Mesh}. */
@RunWith(RobolectricTestRunner.class)
public class MeshTest {
private static final float EPSILON = .00001f;
// This is a copy of Mesh.COORDS_PER_VERTEX which is private.
private static final int COORDS_PER_VERTEX = 7;
// Default 360 sphere.
private static final float RADIUS = 1;
private static final int LATITUDES = 12;
private static final int LONGITUDES = 24;
private static final float VERTICAL_FOV_DEGREES = 180;
private static final float HORIZONTAL_FOV_DEGREES = 360;
@Test
public void testSphericalMesh() throws Exception {
// Only the first param is important in this test.
float[] data =
Mesh.createUvSphereVertexData(
RADIUS,
LATITUDES,
LONGITUDES,
VERTICAL_FOV_DEGREES,
HORIZONTAL_FOV_DEGREES,
Mesh.MEDIA_STEREO_TOP_BOTTOM);
assertThat(data.length).isGreaterThan(LATITUDES * LONGITUDES * COORDS_PER_VERTEX);
assertEquals(0, data.length % COORDS_PER_VERTEX);
for (int i = 0; i < data.length / COORDS_PER_VERTEX; ++i) {
float x = data[i * COORDS_PER_VERTEX + 0];
float y = data[i * COORDS_PER_VERTEX + 1];
float z = data[i * COORDS_PER_VERTEX + 2];
assertEquals(RADIUS, Math.sqrt(x * x + y * y + z * z), EPSILON);
}
}
@Test
public void testMeshTextureCoordinates() throws Exception {
// 360 mono video.
float[] data =
Mesh.createUvSphereVertexData(
RADIUS,
LATITUDES,
LONGITUDES,
VERTICAL_FOV_DEGREES,
HORIZONTAL_FOV_DEGREES,
Mesh.MEDIA_MONOSCOPIC);
// There should be more vertices than quads.
assertThat(data.length).isGreaterThan(LATITUDES * LONGITUDES * COORDS_PER_VERTEX);
assertEquals(0, data.length % COORDS_PER_VERTEX);
for (int i = 0; i < data.length; i += COORDS_PER_VERTEX) {
// For monoscopic meshes, the (3, 4) and (5, 6) tex coords in each vertex should be the same.
assertEquals(data[i + 3], data[i + 5], EPSILON);
assertEquals(data[i + 4], data[i + 6], EPSILON);
}
// Hemispherical stereo where longitudes := latitudes. This is not exactly Wally format, but
// it's close.
data =
Mesh.createUvSphereVertexData(
RADIUS,
LATITUDES,
LATITUDES,
VERTICAL_FOV_DEGREES,
VERTICAL_FOV_DEGREES,
Mesh.MEDIA_STEREO_LEFT_RIGHT);
assertThat(data.length).isGreaterThan(LATITUDES * LATITUDES * COORDS_PER_VERTEX);
assertEquals(0, data.length % COORDS_PER_VERTEX);
for (int i = 0; i < data.length; i += COORDS_PER_VERTEX) {
// U coordinates should be on the left & right halves of the texture.
assertThat(data[i + 3]).isAtMost(.5f);
assertThat(data[i + 5]).isAtLeast(.5f);
// V coordinates should be the same.
assertEquals(data[i + 4], data[i + 6], EPSILON);
}
// Flat stereo.
data =
Mesh.createUvSphereVertexData(
RADIUS,
1,
1, // Single quad.
30,
60, // Approximate "cinematic" screen.
Mesh.MEDIA_STEREO_TOP_BOTTOM);
assertEquals(0, data.length % COORDS_PER_VERTEX);
for (int i = 0; i < data.length; i += COORDS_PER_VERTEX) {
// U coordinates should be the same
assertEquals(data[i + 3], data[i + 5], EPSILON);
// V coordinates should be on the top & bottom halves of the texture.
assertThat(data[i + 4]).isAtMost(.5f);
assertThat(data[i + 6]).isAtLeast(.5f);
}
}
@Test
public void testArgumentValidation() {
checkIllegalArgumentException(0, 1, 1, 1, 1);
checkIllegalArgumentException(1, 0, 1, 1, 1);
checkIllegalArgumentException(1, 1, 0, 1, 1);
checkIllegalArgumentException(1, 1, 1, 0, 1);
checkIllegalArgumentException(1, 1, 1, 181, 1);
checkIllegalArgumentException(1, 1, 1, 1, 0);
checkIllegalArgumentException(1, 1, 1, 1, 361);
}
private void checkIllegalArgumentException(
float radius,
int latitudes,
int longitudes,
float verticalFovDegrees,
float horizontalFovDegrees) {
try {
Mesh.createUvSphereVertexData(
radius,
latitudes,
longitudes,
verticalFovDegrees,
horizontalFovDegrees,
Mesh.MEDIA_MONOSCOPIC);
fail();
} catch (IllegalArgumentException e) {
// Do nothing. Expected.
}
}
}
/*
* Copyright (C) 2018 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.ui.spherical;
import static com.google.common.truth.Truth.assertThat;
import android.view.MotionEvent;
import com.google.android.exoplayer2.ui.spherical.SphericalSurfaceView.TouchTracker;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Tests the interaction between the View's input (TouchTracker) and output (Renderer). */
@RunWith(RobolectricTestRunner.class)
public class SphericalSurfaceViewTouchTrackerTest {
private static final float EPSILON = 0.00001f;
private static final int SWIPE_PX = 100;
private static class MockRenderer extends SphericalSurfaceView.Renderer {
private float yaw;
private float pitch;
public MockRenderer() {
super(null);
}
@Override
public synchronized void setPitchOffset(float pitch) {
this.pitch = pitch;
}
@Override
public synchronized void setYawOffset(float yaw) {
this.yaw = yaw;
}
};
private final MockRenderer mockRenderer = new MockRenderer();
private TouchTracker tracker;
private static void swipe(TouchTracker tracker, float x0, float y0, float x1, float y1) {
tracker.onTouch(null, MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, x0, y0, 0));
tracker.onTouch(null, MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, x1, y1, 0));
tracker.onTouch(null, MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, x1, y1, 0));
}
@Before
public void setUp() {
tracker = new TouchTracker(mockRenderer);
}
@Test
public void testTap() {
// Tap is a noop.
swipe(tracker, 0, 0, 0, 0);
assertThat(mockRenderer.yaw).isWithin(EPSILON).of(0);
assertThat(mockRenderer.pitch).isWithin(EPSILON).of(0);
}
@Test
public void testBasicYaw() {
swipe(tracker, 0, 0, SWIPE_PX, 0);
assertThat(mockRenderer.yaw).isWithin(EPSILON).of(-SWIPE_PX / TouchTracker.PX_PER_DEGREES);
assertThat(mockRenderer.pitch).isWithin(EPSILON).of(0);
}
@Test
public void testBigYaw() {
swipe(tracker, 0, 0, -10 * SWIPE_PX, 0);
assertThat(mockRenderer.yaw).isEqualTo(10 * SWIPE_PX / TouchTracker.PX_PER_DEGREES);
assertThat(mockRenderer.pitch).isWithin(EPSILON).of(0);
}
@Test
public void testYawUnaffectedByPitch() {
swipe(tracker, 0, 0, 0, SWIPE_PX);
assertThat(mockRenderer.yaw).isWithin(EPSILON).of(0);
swipe(tracker, 0, 0, SWIPE_PX, SWIPE_PX);
assertThat(mockRenderer.yaw).isWithin(EPSILON).of(-SWIPE_PX / TouchTracker.PX_PER_DEGREES);
}
@Test
public void testBasicPitch() {
swipe(tracker, 0, 0, 0, SWIPE_PX);
assertThat(mockRenderer.yaw).isWithin(EPSILON).of(0);
assertThat(mockRenderer.pitch).isWithin(EPSILON).of(SWIPE_PX / TouchTracker.PX_PER_DEGREES);
}
@Test
public void testPitchClipped() {
// Big reverse pitch should be clipped.
swipe(tracker, 0, 0, 0, -20 * SWIPE_PX);
assertThat(mockRenderer.yaw).isWithin(EPSILON).of(0);
assertThat(mockRenderer.pitch).isEqualTo(-TouchTracker.MAX_PITCH_DEGREES);
// Big forward pitch should be clipped.
swipe(tracker, 0, 0, 0, 50 * SWIPE_PX);
assertThat(mockRenderer.yaw).isWithin(EPSILON).of(0);
assertThat(mockRenderer.pitch).isEqualTo(TouchTracker.MAX_PITCH_DEGREES);
}
@Test
public void testWithRoll90() {
tracker.setRoll((float) Math.toRadians(90));
// Y-axis should now control yaw.
swipe(tracker, 0, 0, 0, 2 * SWIPE_PX);
assertThat(mockRenderer.yaw).isWithin(EPSILON).of(-2 * SWIPE_PX / TouchTracker.PX_PER_DEGREES);
// X-axis should now control reverse pitch.
swipe(tracker, 0, 0, -3 * SWIPE_PX, 0);
assertThat(mockRenderer.pitch).isWithin(EPSILON).of(3 * SWIPE_PX / TouchTracker.PX_PER_DEGREES);
}
@Test
public void testWithRoll180() {
tracker.setRoll((float) Math.toRadians(180));
// X-axis should now control reverse yaw.
swipe(tracker, 0, 0, -2 * SWIPE_PX, 0);
assertThat(mockRenderer.yaw).isWithin(EPSILON).of(-2 * SWIPE_PX / TouchTracker.PX_PER_DEGREES);
// Y-axis should now control reverse pitch.
swipe(tracker, 0, 0, 0, -3 * SWIPE_PX);
assertThat(mockRenderer.pitch).isWithin(EPSILON).of(3 * SWIPE_PX / TouchTracker.PX_PER_DEGREES);
}
@Test
public void testWithRoll270() {
tracker.setRoll((float) Math.toRadians(270));
// Y-axis should now control reverse yaw.
swipe(tracker, 0, 0, 0, -2 * SWIPE_PX);
assertThat(mockRenderer.yaw).isWithin(EPSILON).of(-2 * SWIPE_PX / TouchTracker.PX_PER_DEGREES);
// X-axis should now control pitch.
swipe(tracker, 0, 0, 3 * SWIPE_PX, 0);
assertThat(mockRenderer.pitch).isWithin(EPSILON).of(3 * SWIPE_PX / TouchTracker.PX_PER_DEGREES);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment