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
This commit is contained in:
parent
01b69854ff
commit
9c337c8806
@ -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
|
|||||||
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
|
|||||||
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
|
|||||||
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
|
|||||||
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 {
|
|||||||
/** 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 {
|
|||||||
*/
|
*/
|
||||||
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 {
|
|||||||
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;
|
|||||||
|
|
||||||
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;
|
|||||||
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;
|
|||||||
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;
|
|||||||
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;
|
|||||||
* <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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
* 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 {
|
|||||||
* <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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,303 @@
|
|||||||
|
/*
|
||||||
|
* 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.annotation.TargetApi;
|
||||||
|
import android.opengl.GLES11Ext;
|
||||||
|
import android.opengl.GLES20;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to generate & render spherical meshes for video or images. Use the static creation
|
||||||
|
* methods to construct the Mesh's data. Then call the Mesh constructor on the GL thread when ready.
|
||||||
|
* Use glDraw method to render it.
|
||||||
|
*/
|
||||||
|
@TargetApi(15)
|
||||||
|
/*package*/ final class Mesh {
|
||||||
|
|
||||||
|
/** Defines the constants identifying the current eye type. */
|
||||||
|
public interface EyeType {
|
||||||
|
/** Single eye in monocular rendering. */
|
||||||
|
int MONOCULAR = 0;
|
||||||
|
|
||||||
|
/** The left eye in stereo rendering. */
|
||||||
|
int LEFT = 1;
|
||||||
|
|
||||||
|
/** The right eye in stereo rendering. */
|
||||||
|
int RIGHT = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Standard media where a single camera frame takes up the entire media frame. */
|
||||||
|
public static final int MEDIA_MONOSCOPIC = 0;
|
||||||
|
/**
|
||||||
|
* Stereo media where the left & right halves of the frame are rendered for the left & right eyes,
|
||||||
|
* respectively. If the stereo media is rendered in a non-VR display, only the left half is used.
|
||||||
|
*/
|
||||||
|
public static final int MEDIA_STEREO_LEFT_RIGHT = 1;
|
||||||
|
/**
|
||||||
|
* Stereo media where the top & bottom halves of the frame are rendered for the left & right eyes,
|
||||||
|
* respectively. If the stereo media is rendered in a non-VR display, only the top half is used.
|
||||||
|
*/
|
||||||
|
public static final int MEDIA_STEREO_TOP_BOTTOM = 2;
|
||||||
|
|
||||||
|
// Basic vertex & fragment shaders to render a mesh with 3D position & 2D texture data.
|
||||||
|
private static final String[] VERTEX_SHADER_CODE =
|
||||||
|
new String[] {
|
||||||
|
"uniform mat4 uMvpMatrix;",
|
||||||
|
"attribute vec4 aPosition;",
|
||||||
|
"attribute vec2 aTexCoords;",
|
||||||
|
"varying vec2 vTexCoords;",
|
||||||
|
|
||||||
|
// Standard transformation.
|
||||||
|
"void main() {",
|
||||||
|
" gl_Position = uMvpMatrix * aPosition;",
|
||||||
|
" vTexCoords = aTexCoords;",
|
||||||
|
"}"
|
||||||
|
};
|
||||||
|
private static final String[] FRAGMENT_SHADER_CODE =
|
||||||
|
new String[] {
|
||||||
|
// This is required since the texture data is GL_TEXTURE_EXTERNAL_OES.
|
||||||
|
"#extension GL_OES_EGL_image_external : require",
|
||||||
|
"precision mediump float;",
|
||||||
|
|
||||||
|
// Standard texture rendering shader.
|
||||||
|
"uniform samplerExternalOES uTexture;",
|
||||||
|
"varying vec2 vTexCoords;",
|
||||||
|
"void main() {",
|
||||||
|
" gl_FragColor = texture2D(uTexture, vTexCoords);",
|
||||||
|
"}"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Constants related to vertex data.
|
||||||
|
private static final int POSITION_COORDS_PER_VERTEX = 3; // X, Y, Z.
|
||||||
|
// The vertex contains texture coordinates for both the left & right eyes. If the scene is
|
||||||
|
// rendered in VR, the appropriate part of the vertex will be selected at runtime. For a mono
|
||||||
|
// scene, only the left eye's UV coordinates are used.
|
||||||
|
// For mono media, the UV coordinates are duplicated in each. For stereo media, the UV coords
|
||||||
|
// point to the appropriate part of the source media.
|
||||||
|
private static final int TEXTURE_COORDS_PER_VERTEX = 2 * 2;
|
||||||
|
private static final int COORDS_PER_VERTEX =
|
||||||
|
POSITION_COORDS_PER_VERTEX + TEXTURE_COORDS_PER_VERTEX;
|
||||||
|
// Data is tightly packed. Each vertex is [x, y, z, u_left, v_left, u_right, v_right].
|
||||||
|
private static final int VERTEX_STRIDE_BYTES = COORDS_PER_VERTEX * C.BYTES_PER_FLOAT;
|
||||||
|
|
||||||
|
// Vertices for the mesh with 3D position + left 2D texture UV + right 2D texture UV.
|
||||||
|
private final int vertixCount;
|
||||||
|
private final FloatBuffer vertexBuffer;
|
||||||
|
|
||||||
|
// Program related GL items. These are only valid if program != 0.
|
||||||
|
private int program;
|
||||||
|
private int mvpMatrixHandle;
|
||||||
|
private int positionHandle;
|
||||||
|
private int texCoordsHandle;
|
||||||
|
private int textureHandle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a 3D UV sphere for rendering monoscopic or stereoscopic video.
|
||||||
|
*
|
||||||
|
* <p>This can be called on any thread. The returned {@link Mesh} isn't valid until {@link
|
||||||
|
* #init()} is called.
|
||||||
|
*
|
||||||
|
* @param radius Size of the sphere. Must be > 0.
|
||||||
|
* @param latitudes Number of rows that make up the sphere. Must be >= 1.
|
||||||
|
* @param longitudes Number of columns that make up the sphere. Must be >= 1.
|
||||||
|
* @param verticalFovDegrees Total latitudinal degrees that are covered by the sphere. Must be in
|
||||||
|
* (0, 180].
|
||||||
|
* @param horizontalFovDegrees Total longitudinal degrees that are covered by the sphere.Must be
|
||||||
|
* in (0, 360].
|
||||||
|
* @param mediaFormat A MEDIA_* value.
|
||||||
|
* @return Unintialized Mesh.
|
||||||
|
*/
|
||||||
|
public static Mesh createUvSphere(
|
||||||
|
float radius,
|
||||||
|
int latitudes,
|
||||||
|
int longitudes,
|
||||||
|
float verticalFovDegrees,
|
||||||
|
float horizontalFovDegrees,
|
||||||
|
int mediaFormat) {
|
||||||
|
return new Mesh(
|
||||||
|
createUvSphereVertexData(
|
||||||
|
radius, latitudes, longitudes, verticalFovDegrees, horizontalFovDegrees, mediaFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Used by static constructors. */
|
||||||
|
private Mesh(float[] vertexData) {
|
||||||
|
vertixCount = vertexData.length / COORDS_PER_VERTEX;
|
||||||
|
vertexBuffer = Utils.createBuffer(vertexData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Initializes of the GL components. */
|
||||||
|
/* package */ void init() {
|
||||||
|
program = Utils.compileProgram(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE);
|
||||||
|
mvpMatrixHandle = GLES20.glGetUniformLocation(program, "uMvpMatrix");
|
||||||
|
positionHandle = GLES20.glGetAttribLocation(program, "aPosition");
|
||||||
|
texCoordsHandle = GLES20.glGetAttribLocation(program, "aTexCoords");
|
||||||
|
textureHandle = GLES20.glGetUniformLocation(program, "uTexture");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the mesh. This must be called on the GL thread.
|
||||||
|
*
|
||||||
|
* @param textureId GL_TEXTURE_EXTERNAL_OES used for this mesh.
|
||||||
|
* @param mvpMatrix The Model View Projection matrix.
|
||||||
|
* @param eyeType An {@link EyeType} value.
|
||||||
|
*/
|
||||||
|
/* package */ void draw(int textureId, float[] mvpMatrix, int eyeType) {
|
||||||
|
// Configure shader.
|
||||||
|
GLES20.glUseProgram(program);
|
||||||
|
checkGlError();
|
||||||
|
|
||||||
|
GLES20.glEnableVertexAttribArray(positionHandle);
|
||||||
|
GLES20.glEnableVertexAttribArray(texCoordsHandle);
|
||||||
|
checkGlError();
|
||||||
|
|
||||||
|
GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0);
|
||||||
|
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
|
||||||
|
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
|
||||||
|
GLES20.glUniform1i(textureHandle, 0);
|
||||||
|
checkGlError();
|
||||||
|
|
||||||
|
// Load position data.
|
||||||
|
vertexBuffer.position(0);
|
||||||
|
GLES20.glVertexAttribPointer(
|
||||||
|
positionHandle,
|
||||||
|
POSITION_COORDS_PER_VERTEX,
|
||||||
|
GLES20.GL_FLOAT,
|
||||||
|
false,
|
||||||
|
VERTEX_STRIDE_BYTES,
|
||||||
|
vertexBuffer);
|
||||||
|
checkGlError();
|
||||||
|
|
||||||
|
// Load texture data. Eye.Type.RIGHT uses the left eye's data.
|
||||||
|
int textureOffset =
|
||||||
|
(eyeType == EyeType.RIGHT) ? POSITION_COORDS_PER_VERTEX + 2 : POSITION_COORDS_PER_VERTEX;
|
||||||
|
vertexBuffer.position(textureOffset);
|
||||||
|
GLES20.glVertexAttribPointer(
|
||||||
|
texCoordsHandle,
|
||||||
|
TEXTURE_COORDS_PER_VERTEX,
|
||||||
|
GLES20.GL_FLOAT,
|
||||||
|
false,
|
||||||
|
VERTEX_STRIDE_BYTES,
|
||||||
|
vertexBuffer);
|
||||||
|
checkGlError();
|
||||||
|
|
||||||
|
// Render.
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, vertixCount);
|
||||||
|
checkGlError();
|
||||||
|
|
||||||
|
GLES20.glDisableVertexAttribArray(positionHandle);
|
||||||
|
GLES20.glDisableVertexAttribArray(texCoordsHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cleans up the GL resources. */
|
||||||
|
/* package */ void shutdown() {
|
||||||
|
if (program != 0) {
|
||||||
|
GLES20.glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @VisibleForTesting
|
||||||
|
/*package*/ static float[] createUvSphereVertexData(
|
||||||
|
float radius,
|
||||||
|
int latitudes,
|
||||||
|
int longitudes,
|
||||||
|
float verticalFovDegrees,
|
||||||
|
float horizontalFovDegrees,
|
||||||
|
int mediaFormat) {
|
||||||
|
if (radius <= 0
|
||||||
|
|| latitudes < 1
|
||||||
|
|| longitudes < 1
|
||||||
|
|| verticalFovDegrees <= 0
|
||||||
|
|| verticalFovDegrees > 180
|
||||||
|
|| horizontalFovDegrees <= 0
|
||||||
|
|| horizontalFovDegrees > 360) {
|
||||||
|
throw new IllegalArgumentException("Invalid parameters for sphere.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute angular size in radians of each UV quad.
|
||||||
|
float verticalFovRads = (float) Math.toRadians(verticalFovDegrees);
|
||||||
|
float horizontalFovRads = (float) Math.toRadians(horizontalFovDegrees);
|
||||||
|
float quadHeightRads = verticalFovRads / latitudes;
|
||||||
|
float quadWidthRads = horizontalFovRads / longitudes;
|
||||||
|
|
||||||
|
// Each latitude strip has 2 * (longitudes quads + extra edge) vertices + 2 degenerate vertices.
|
||||||
|
int vertexCount = (2 * (longitudes + 1) + 2) * latitudes;
|
||||||
|
// Buffer to return.
|
||||||
|
float[] vertexData = new float[vertexCount * COORDS_PER_VERTEX];
|
||||||
|
|
||||||
|
// Generate the data for the sphere which is a set of triangle strips representing each
|
||||||
|
// latitude band.
|
||||||
|
int offset = 0; // Offset into the vertexData array.
|
||||||
|
// (i, j) represents a quad in the equirectangular sphere.
|
||||||
|
for (int j = 0; j < latitudes; ++j) { // For each horizontal triangle strip.
|
||||||
|
// Each latitude band lies between the two phi values. Each vertical edge on a band lies on
|
||||||
|
// a theta value.
|
||||||
|
float phiLow = (quadHeightRads * j - verticalFovRads / 2);
|
||||||
|
float phiHigh = (quadHeightRads * (j + 1) - verticalFovRads / 2);
|
||||||
|
|
||||||
|
for (int i = 0; i < longitudes + 1; ++i) { // For each vertical edge in the band.
|
||||||
|
for (int k = 0; k < 2; ++k) { // For low and high points on an edge.
|
||||||
|
// For each point, determine it's position in polar coordinates.
|
||||||
|
float phi = (k == 0) ? phiLow : phiHigh;
|
||||||
|
float theta = quadWidthRads * i + (float) Math.PI - horizontalFovRads / 2;
|
||||||
|
|
||||||
|
// Set vertex position data as Cartesian coordinates.
|
||||||
|
vertexData[offset + 0] = -(float) (radius * Math.sin(theta) * Math.cos(phi));
|
||||||
|
vertexData[offset + 1] = (float) (radius * Math.sin(phi));
|
||||||
|
vertexData[offset + 2] = (float) (radius * Math.cos(theta) * Math.cos(phi));
|
||||||
|
|
||||||
|
// Set vertex texture.x data.
|
||||||
|
if (mediaFormat == MEDIA_STEREO_LEFT_RIGHT) {
|
||||||
|
// For left-right media, each eye's x coordinate points to the left or right half of the
|
||||||
|
// texture.
|
||||||
|
vertexData[offset + 3] = (i * quadWidthRads / horizontalFovRads) / 2;
|
||||||
|
vertexData[offset + 5] = (i * quadWidthRads / horizontalFovRads) / 2 + .5f;
|
||||||
|
} else {
|
||||||
|
// For top-bottom or monoscopic media, the eye's x spans the full width of the texture.
|
||||||
|
vertexData[offset + 3] = i * quadWidthRads / horizontalFovRads;
|
||||||
|
vertexData[offset + 5] = i * quadWidthRads / horizontalFovRads;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set vertex texture.y data. The "1 - ..." is due to Canvas vs GL coords.
|
||||||
|
if (mediaFormat == MEDIA_STEREO_TOP_BOTTOM) {
|
||||||
|
// For top-bottom media, each eye's y coordinate points to the top or bottom half of the
|
||||||
|
// texture.
|
||||||
|
vertexData[offset + 4] = 1 - (((j + k) * quadHeightRads / verticalFovRads) / 2 + .5f);
|
||||||
|
vertexData[offset + 6] = 1 - ((j + k) * quadHeightRads / verticalFovRads) / 2;
|
||||||
|
} else {
|
||||||
|
// For left-right or monoscopic media, the eye's y spans the full height of the texture.
|
||||||
|
vertexData[offset + 4] = 1 - (j + k) * quadHeightRads / verticalFovRads;
|
||||||
|
vertexData[offset + 6] = 1 - (j + k) * quadHeightRads / verticalFovRads;
|
||||||
|
}
|
||||||
|
offset += COORDS_PER_VERTEX;
|
||||||
|
|
||||||
|
// Break up the triangle strip with degenerate vertices by copying first and last points.
|
||||||
|
if ((i == 0 && k == 0) || (i == longitudes && k == 1)) {
|
||||||
|
System.arraycopy(
|
||||||
|
vertexData, offset - COORDS_PER_VERTEX, vertexData, offset, COORDS_PER_VERTEX);
|
||||||
|
offset += COORDS_PER_VERTEX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Move on to the next vertical edge in the triangle strip.
|
||||||
|
}
|
||||||
|
// Move on to the next triangle strip.
|
||||||
|
}
|
||||||
|
return vertexData;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,492 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.PointF;
|
||||||
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.hardware.Sensor;
|
||||||
|
import android.hardware.SensorEvent;
|
||||||
|
import android.hardware.SensorEventListener;
|
||||||
|
import android.hardware.SensorManager;
|
||||||
|
import android.opengl.GLES20;
|
||||||
|
import android.opengl.GLSurfaceView;
|
||||||
|
import android.opengl.Matrix;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.support.annotation.AnyThread;
|
||||||
|
import android.support.annotation.BinderThread;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.UiThread;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.Display;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import com.google.android.exoplayer2.ui.spherical.Mesh.EyeType;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import javax.microedition.khronos.egl.EGLConfig;
|
||||||
|
import javax.microedition.khronos.opengles.GL10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a GL scene in a non-VR Activity that is affected by phone orientation and touch input.
|
||||||
|
*
|
||||||
|
* <p>The two input components are the TYPE_GAME_ROTATION_VECTOR Sensor and a TouchListener. The GL
|
||||||
|
* renderer combines these two inputs to render a scene with the appropriate camera orientation.
|
||||||
|
*
|
||||||
|
* <p>The primary complexity in this class is related to the various rotations. It is important to
|
||||||
|
* apply the touch and sensor rotations in the correct order or the user's touch manipulations won't
|
||||||
|
* match what they expect.
|
||||||
|
*/
|
||||||
|
@TargetApi(15)
|
||||||
|
public final class SphericalSurfaceView extends GLSurfaceView {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This listener can be used to be notified when the {@link Surface} associated with this view is
|
||||||
|
* changed.
|
||||||
|
*/
|
||||||
|
public interface SurfaceListener {
|
||||||
|
/**
|
||||||
|
* Invoked when the surface is changed or there isn't one anymore. Any previous surface
|
||||||
|
* shouldn't be used after this call.
|
||||||
|
*
|
||||||
|
* @param surface The new surface or null if there isn't one anymore.
|
||||||
|
*/
|
||||||
|
void surfaceChanged(@Nullable Surface surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A spherical mesh for video should be large enough that there are no stereo artifacts.
|
||||||
|
private static final int SPHERE_RADIUS_METERS = 50;
|
||||||
|
|
||||||
|
// TODO These should be configured based on the video type. It's assumed 360 video here.
|
||||||
|
private static final int DEFAULT_SPHERE_HORIZONTAL_DEGREES = 360;
|
||||||
|
private static final int DEFAULT_SPHERE_VERTICAL_DEGREES = 180;
|
||||||
|
|
||||||
|
// The 360 x 180 sphere has 5 degree quads. Increase these if lines in videos look wavy.
|
||||||
|
private static final int DEFAULT_SPHERE_COLUMNS = 72;
|
||||||
|
private static final int DEFAULT_SPHERE_ROWS = 36;
|
||||||
|
|
||||||
|
// Arbitrary vertical field of view.
|
||||||
|
private static final int FIELD_OF_VIEW_DEGREES = 90;
|
||||||
|
private static final float Z_NEAR = .1f;
|
||||||
|
private static final float Z_FAR = 100;
|
||||||
|
|
||||||
|
// Arbitrary touch speed number. This should be tweaked so the scene smoothly follows the
|
||||||
|
// finger or derived from DisplayMetrics.
|
||||||
|
private static final float PX_PER_DEGREES = 25;
|
||||||
|
// Touch input won't change the pitch beyond +/- 45 degrees. This reduces awkward situations
|
||||||
|
// where the touch-based pitch and gyro-based pitch interact badly near the poles.
|
||||||
|
private static final float MAX_PITCH_DEGREES = 45;
|
||||||
|
|
||||||
|
private static final float UPRIGHT_ROLL = (float) Math.PI;
|
||||||
|
|
||||||
|
private final SensorManager sensorManager;
|
||||||
|
private final @Nullable Sensor orientationSensor;
|
||||||
|
private final PhoneOrientationListener phoneOrientationListener;
|
||||||
|
private final Renderer renderer;
|
||||||
|
private final Handler mainHandler;
|
||||||
|
private @Nullable SurfaceListener surfaceListener;
|
||||||
|
private @Nullable SurfaceTexture surfaceTexture;
|
||||||
|
private @Nullable Surface surface;
|
||||||
|
|
||||||
|
public SphericalSurfaceView(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SphericalSurfaceView(Context context, @Nullable AttributeSet attributeSet) {
|
||||||
|
super(context, attributeSet);
|
||||||
|
|
||||||
|
mainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
// Configure sensors and touch.
|
||||||
|
sensorManager =
|
||||||
|
(SensorManager) Assertions.checkNotNull(context.getSystemService(Context.SENSOR_SERVICE));
|
||||||
|
// TYPE_GAME_ROTATION_VECTOR is the easiest sensor since it handles all the complex math for
|
||||||
|
// fusion. It's used instead of TYPE_ROTATION_VECTOR since the latter uses the magnetometer on
|
||||||
|
// devices. When used indoors, the magnetometer can take some time to settle depending on the
|
||||||
|
// device and amount of metal in the environment.
|
||||||
|
int type = Util.SDK_INT >= 18 ? Sensor.TYPE_GAME_ROTATION_VECTOR : Sensor.TYPE_ROTATION_VECTOR;
|
||||||
|
orientationSensor = sensorManager.getDefaultSensor(type);
|
||||||
|
|
||||||
|
renderer = new Renderer();
|
||||||
|
|
||||||
|
TouchTracker touchTracker = new TouchTracker(renderer);
|
||||||
|
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||||
|
Display display = Assertions.checkNotNull(windowManager).getDefaultDisplay();
|
||||||
|
phoneOrientationListener = new PhoneOrientationListener(display, touchTracker, renderer);
|
||||||
|
|
||||||
|
setEGLContextClientVersion(2);
|
||||||
|
setRenderer(renderer);
|
||||||
|
setOnTouchListener(touchTracker);
|
||||||
|
|
||||||
|
Mesh mesh =
|
||||||
|
Mesh.createUvSphere(
|
||||||
|
SPHERE_RADIUS_METERS,
|
||||||
|
DEFAULT_SPHERE_ROWS,
|
||||||
|
DEFAULT_SPHERE_COLUMNS,
|
||||||
|
DEFAULT_SPHERE_VERTICAL_DEGREES,
|
||||||
|
DEFAULT_SPHERE_HORIZONTAL_DEGREES,
|
||||||
|
Mesh.MEDIA_MONOSCOPIC);
|
||||||
|
queueEvent(() -> renderer.scene.setMesh(mesh));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the {@link Surface} associated with this view. */
|
||||||
|
public @Nullable Surface getSurface() {
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link SurfaceListener} used to listen to surface events.
|
||||||
|
*
|
||||||
|
* @param listener The listener for surface events.
|
||||||
|
*/
|
||||||
|
public void setSurfaceListener(@Nullable SurfaceListener listener) {
|
||||||
|
surfaceListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
if (orientationSensor != null) {
|
||||||
|
sensorManager.registerListener(
|
||||||
|
phoneOrientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
if (orientationSensor != null) {
|
||||||
|
sensorManager.unregisterListener(phoneOrientationListener);
|
||||||
|
}
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDetachedFromWindow() {
|
||||||
|
// This call stops GL thread.
|
||||||
|
super.onDetachedFromWindow();
|
||||||
|
|
||||||
|
// Post to make sure we occur in order with any onSurfaceTextureAvailable calls.
|
||||||
|
mainHandler.post(
|
||||||
|
() -> {
|
||||||
|
if (surface != null) {
|
||||||
|
if (surfaceListener != null) {
|
||||||
|
surfaceListener.surfaceChanged(null);
|
||||||
|
}
|
||||||
|
releaseSurface(surfaceTexture, surface);
|
||||||
|
surfaceTexture = null;
|
||||||
|
surface = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called on GL thread.
|
||||||
|
private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
|
||||||
|
mainHandler.post(
|
||||||
|
() -> {
|
||||||
|
SurfaceTexture oldSurfaceTexture = this.surfaceTexture;
|
||||||
|
Surface oldSurface = this.surface;
|
||||||
|
this.surfaceTexture = surfaceTexture;
|
||||||
|
this.surface = new Surface(surfaceTexture);
|
||||||
|
if (surfaceListener != null) {
|
||||||
|
surfaceListener.surfaceChanged(surface);
|
||||||
|
}
|
||||||
|
releaseSurface(oldSurfaceTexture, oldSurface);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void releaseSurface(
|
||||||
|
@Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {
|
||||||
|
if (oldSurfaceTexture != null) {
|
||||||
|
oldSurfaceTexture.release();
|
||||||
|
}
|
||||||
|
if (oldSurface != null) {
|
||||||
|
oldSurface.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Detects sensor events and saves them as a matrix. */
|
||||||
|
private static class PhoneOrientationListener implements SensorEventListener {
|
||||||
|
private final float[] phoneInWorldSpaceMatrix = new float[16];
|
||||||
|
private final float[] remappedPhoneMatrix = new float[16];
|
||||||
|
private final float[] angles = new float[3];
|
||||||
|
private final Display display;
|
||||||
|
private final TouchTracker touchTracker;
|
||||||
|
private final Renderer renderer;
|
||||||
|
|
||||||
|
public PhoneOrientationListener(Display display, TouchTracker touchTracker, Renderer renderer) {
|
||||||
|
this.display = display;
|
||||||
|
this.touchTracker = touchTracker;
|
||||||
|
this.renderer = renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@BinderThread
|
||||||
|
public void onSensorChanged(SensorEvent event) {
|
||||||
|
SensorManager.getRotationMatrixFromVector(remappedPhoneMatrix, event.values);
|
||||||
|
|
||||||
|
// If we're not in upright portrait mode, remap the axes of the coordinate system according to
|
||||||
|
// the display rotation.
|
||||||
|
int xAxis;
|
||||||
|
int yAxis;
|
||||||
|
switch (display.getRotation()) {
|
||||||
|
case Surface.ROTATION_270:
|
||||||
|
xAxis = SensorManager.AXIS_MINUS_Y;
|
||||||
|
yAxis = SensorManager.AXIS_X;
|
||||||
|
break;
|
||||||
|
case Surface.ROTATION_180:
|
||||||
|
xAxis = SensorManager.AXIS_MINUS_X;
|
||||||
|
yAxis = SensorManager.AXIS_MINUS_Y;
|
||||||
|
break;
|
||||||
|
case Surface.ROTATION_90:
|
||||||
|
xAxis = SensorManager.AXIS_Y;
|
||||||
|
yAxis = SensorManager.AXIS_MINUS_X;
|
||||||
|
break;
|
||||||
|
case Surface.ROTATION_0:
|
||||||
|
default:
|
||||||
|
xAxis = SensorManager.AXIS_X;
|
||||||
|
yAxis = SensorManager.AXIS_Y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SensorManager.remapCoordinateSystem(
|
||||||
|
remappedPhoneMatrix, xAxis, yAxis, phoneInWorldSpaceMatrix);
|
||||||
|
|
||||||
|
// Extract the phone's roll and pass it on to touchTracker & renderer. Remapping is required
|
||||||
|
// since we need the calculated roll of the phone to be independent of the phone's pitch &
|
||||||
|
// yaw. Any operation that decomposes rotation to Euler angles needs to be performed
|
||||||
|
// carefully.
|
||||||
|
SensorManager.remapCoordinateSystem(
|
||||||
|
phoneInWorldSpaceMatrix,
|
||||||
|
SensorManager.AXIS_X,
|
||||||
|
SensorManager.AXIS_MINUS_Z,
|
||||||
|
remappedPhoneMatrix);
|
||||||
|
SensorManager.getOrientation(remappedPhoneMatrix, angles);
|
||||||
|
float roll = angles[2];
|
||||||
|
touchTracker.setRoll(roll);
|
||||||
|
|
||||||
|
// Rotate from Android coordinates to OpenGL coordinates. Android's coordinate system
|
||||||
|
// assumes Y points North and Z points to the sky. OpenGL has Y pointing up and Z pointing
|
||||||
|
// toward the user.
|
||||||
|
Matrix.rotateM(phoneInWorldSpaceMatrix, 0, 90, 1, 0, 0);
|
||||||
|
renderer.setDeviceOrientation(phoneInWorldSpaceMatrix, roll);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic touch input system.
|
||||||
|
*
|
||||||
|
* <p>Mixing touch input and gyro input results in a complicated UI so this should be used
|
||||||
|
* carefully. This touch system implements a basic (X, Y) -> (yaw, pitch) transform. This works
|
||||||
|
* for basic UI but fails in edge cases where the user tries to drag scene up or down. There is no
|
||||||
|
* good UX solution for this. The least bad solution is to disable pitch manipulation and only let
|
||||||
|
* the user adjust yaw. This example tries to limit the awkwardness by restricting pitch
|
||||||
|
* manipulation to +/- 45 degrees.
|
||||||
|
*
|
||||||
|
* <p>It is also important to get the order of operations correct. To match what users expect,
|
||||||
|
* touch interaction manipulates the scene by rotating the world by the yaw offset and tilting the
|
||||||
|
* camera by the pitch offset. If the order of operations is incorrect, the sensors & touch
|
||||||
|
* rotations will have strange interactions. The roll of the phone is also tracked so that the x &
|
||||||
|
* y are correctly mapped to yaw & pitch no matter how the user holds their phone.
|
||||||
|
*
|
||||||
|
* <p>This class doesn't handle any scrolling inertia but Android's
|
||||||
|
* com.google.vr.sdk.widgets.common.TouchTracker.FlingGestureListener can be used with this code
|
||||||
|
* for a nicer UI. An even more advanced UI would reproject the user's touch point into 3D and
|
||||||
|
* drag the Mesh as the user moves their finger. However, that requires quaternion interpolation
|
||||||
|
* and is beyond the scope of this sample.
|
||||||
|
*/
|
||||||
|
// @VisibleForTesting
|
||||||
|
/*package*/ static class TouchTracker implements OnTouchListener {
|
||||||
|
// With every touch event, update the accumulated degrees offset by the new pixel amount.
|
||||||
|
private final PointF previousTouchPointPx = new PointF();
|
||||||
|
private final PointF accumulatedTouchOffsetDegrees = new PointF();
|
||||||
|
// The conversion from touch to yaw & pitch requires compensating for device roll. This is set
|
||||||
|
// on the sensor thread and read on the UI thread.
|
||||||
|
private volatile float roll;
|
||||||
|
|
||||||
|
private final Renderer renderer;
|
||||||
|
|
||||||
|
public TouchTracker(Renderer renderer) {
|
||||||
|
this.renderer = renderer;
|
||||||
|
roll = UPRIGHT_ROLL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts ACTION_MOVE events to pitch & yaw events while compensating for device roll.
|
||||||
|
*
|
||||||
|
* @return true if we handled the event
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
// Initialize drag gesture.
|
||||||
|
previousTouchPointPx.set(event.getX(), event.getY());
|
||||||
|
return true;
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
// Calculate the touch delta in screen space.
|
||||||
|
float touchX = (event.getX() - previousTouchPointPx.x) / PX_PER_DEGREES;
|
||||||
|
float touchY = (event.getY() - previousTouchPointPx.y) / PX_PER_DEGREES;
|
||||||
|
previousTouchPointPx.set(event.getX(), event.getY());
|
||||||
|
|
||||||
|
float r = roll; // Copy volatile state.
|
||||||
|
float cr = (float) Math.cos(r);
|
||||||
|
float sr = (float) Math.sin(r);
|
||||||
|
// To convert from screen space to the 3D space, we need to adjust the drag vector based
|
||||||
|
// on the roll of the phone. This is standard rotationMatrix(roll) * vector math but has
|
||||||
|
// an inverted y-axis due to the screen-space coordinates vs GL coordinates.
|
||||||
|
// Handle yaw.
|
||||||
|
accumulatedTouchOffsetDegrees.x -= cr * touchX - sr * touchY;
|
||||||
|
// Handle pitch and limit it to 45 degrees.
|
||||||
|
accumulatedTouchOffsetDegrees.y += sr * touchX + cr * touchY;
|
||||||
|
accumulatedTouchOffsetDegrees.y =
|
||||||
|
Math.max(
|
||||||
|
-MAX_PITCH_DEGREES, Math.min(MAX_PITCH_DEGREES, accumulatedTouchOffsetDegrees.y));
|
||||||
|
|
||||||
|
renderer.setPitchOffset(accumulatedTouchOffsetDegrees.y);
|
||||||
|
renderer.setYawOffset(accumulatedTouchOffsetDegrees.x);
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BinderThread
|
||||||
|
public void setRoll(float roll) {
|
||||||
|
// We compensate for roll by rotating in the opposite direction.
|
||||||
|
this.roll = -roll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard GL Renderer implementation. The notable code is the matrix multiplication in
|
||||||
|
* onDrawFrame and updatePitchMatrix.
|
||||||
|
*/
|
||||||
|
// @VisibleForTesting
|
||||||
|
/*package*/ class Renderer implements GLSurfaceView.Renderer {
|
||||||
|
private final SceneRenderer scene;
|
||||||
|
private final float[] projectionMatrix = new float[16];
|
||||||
|
|
||||||
|
// There is no model matrix for this scene so viewProjectionMatrix is used for the mvpMatrix.
|
||||||
|
private final float[] viewProjectionMatrix = new float[16];
|
||||||
|
|
||||||
|
// Device orientation is derived from sensor data. This is accessed in the sensor's thread and
|
||||||
|
// the GL thread.
|
||||||
|
private final float[] deviceOrientationMatrix = new float[16];
|
||||||
|
|
||||||
|
// Optional pitch and yaw rotations are applied to the sensor orientation. These are accessed on
|
||||||
|
// the UI, sensor and GL Threads.
|
||||||
|
private final float[] touchPitchMatrix = new float[16];
|
||||||
|
private final float[] touchYawMatrix = new float[16];
|
||||||
|
private float touchPitch;
|
||||||
|
private float deviceRoll;
|
||||||
|
|
||||||
|
// viewMatrix = touchPitch * deviceOrientation * touchYaw.
|
||||||
|
private final float[] viewMatrix = new float[16];
|
||||||
|
private final float[] tempMatrix = new float[16];
|
||||||
|
|
||||||
|
public Renderer() {
|
||||||
|
scene = new SceneRenderer();
|
||||||
|
Matrix.setIdentityM(deviceOrientationMatrix, 0);
|
||||||
|
Matrix.setIdentityM(touchPitchMatrix, 0);
|
||||||
|
Matrix.setIdentityM(touchYawMatrix, 0);
|
||||||
|
deviceRoll = UPRIGHT_ROLL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
||||||
|
onSurfaceTextureAvailable(scene.init());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSurfaceChanged(GL10 gl, int width, int height) {
|
||||||
|
GLES20.glViewport(0, 0, width, height);
|
||||||
|
float aspect = (float) width / height;
|
||||||
|
float fovY = calculateFieldOfViewInYDirection(aspect);
|
||||||
|
Matrix.perspectiveM(projectionMatrix, 0, fovY, aspect, Z_NEAR, Z_FAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDrawFrame(GL10 gl) {
|
||||||
|
// Combine touch & sensor data.
|
||||||
|
// Orientation = pitch * sensor * yaw since that is closest to what most users expect the
|
||||||
|
// behavior to be.
|
||||||
|
synchronized (this) {
|
||||||
|
Matrix.multiplyMM(tempMatrix, 0, deviceOrientationMatrix, 0, touchYawMatrix, 0);
|
||||||
|
Matrix.multiplyMM(viewMatrix, 0, touchPitchMatrix, 0, tempMatrix, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix.multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
|
||||||
|
scene.drawFrame(viewProjectionMatrix, EyeType.MONOCULAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adjusts the GL camera's rotation based on device rotation. Runs on the sensor thread. */
|
||||||
|
@BinderThread
|
||||||
|
public synchronized void setDeviceOrientation(float[] matrix, float deviceRoll) {
|
||||||
|
System.arraycopy(matrix, 0, deviceOrientationMatrix, 0, deviceOrientationMatrix.length);
|
||||||
|
this.deviceRoll = -deviceRoll;
|
||||||
|
updatePitchMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the pitch matrix after a physical rotation or touch input. The pitch matrix rotation
|
||||||
|
* is applied on an axis that is dependent on device rotation so this must be called after
|
||||||
|
* either touch or sensor update.
|
||||||
|
*/
|
||||||
|
@AnyThread
|
||||||
|
private void updatePitchMatrix() {
|
||||||
|
// The camera's pitch needs to be rotated along an axis that is parallel to the real world's
|
||||||
|
// horizon. This is the <1, 0, 0> axis after compensating for the device's roll.
|
||||||
|
Matrix.setRotateM(
|
||||||
|
touchPitchMatrix,
|
||||||
|
0,
|
||||||
|
-touchPitch,
|
||||||
|
(float) Math.cos(deviceRoll),
|
||||||
|
(float) Math.sin(deviceRoll),
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the pitch offset matrix. */
|
||||||
|
@UiThread
|
||||||
|
public synchronized void setPitchOffset(float pitchDegrees) {
|
||||||
|
touchPitch = pitchDegrees;
|
||||||
|
updatePitchMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set the yaw offset matrix. */
|
||||||
|
@UiThread
|
||||||
|
public synchronized void setYawOffset(float yawDegrees) {
|
||||||
|
Matrix.setRotateM(touchYawMatrix, 0, -yawDegrees, 0, 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float calculateFieldOfViewInYDirection(float aspect) {
|
||||||
|
boolean landscapeMode = aspect > 1;
|
||||||
|
if (landscapeMode) {
|
||||||
|
double halfFovX = FIELD_OF_VIEW_DEGREES / 2;
|
||||||
|
double tanY = Math.tan(Math.toRadians(halfFovX)) / aspect;
|
||||||
|
double halfFovY = Math.toDegrees(Math.atan(tanY));
|
||||||
|
return (float) (halfFovY * 2);
|
||||||
|
} else {
|
||||||
|
return FIELD_OF_VIEW_DEGREES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* 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 @@
|
|||||||
<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"/>
|
||||||
|
@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user