Remove VR code
- Leaving GvrAudioProcessor for now. - Removing GvrPlayerActivity because it was never released. Also removing related UI classes. These were released, but it's unlikely anyone would have been using them directly. PiperOrigin-RevId: 275822516
This commit is contained in:
parent
3101a5df58
commit
7ccbc4c436
@ -40,7 +40,6 @@
|
||||
even if they are listed lower in the `MediaCodecList`.
|
||||
* Add a workaround for broken raw audio decoding on Oppo R9
|
||||
([#5782](https://github.com/google/ExoPlayer/issues/5782)).
|
||||
* Add VR player demo.
|
||||
* Wrap decoder exceptions in a new `DecoderException` class and report as
|
||||
renderer error.
|
||||
* Do not pass the manifest to callbacks of `Player.EventListener` and
|
||||
@ -99,6 +98,7 @@
|
||||
[#6315](https://github.com/google/ExoPlayer/issues/6315) and
|
||||
[#5658](https://github.com/google/ExoPlayer/issues/5658)).
|
||||
* Pass the codec output `MediaFormat` to `VideoFrameMetadataListener`.
|
||||
* Deprecate the GVR extension.
|
||||
|
||||
### 2.10.6 (2019-10-17) ###
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
# ExoPlayer VR player demo #
|
||||
|
||||
This folder contains a demo application that showcases 360 video playback using
|
||||
ExoPlayer GVR extension.
|
@ -1,59 +0,0 @@
|
||||
// Copyright (C) 2019 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion 19
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app isn't indexed and doesn't have translations.
|
||||
disable 'GoogleAppIndexingWarning','MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(modulePrefix + 'library-core')
|
||||
implementation project(modulePrefix + 'library-ui')
|
||||
implementation project(modulePrefix + 'library-dash')
|
||||
implementation project(modulePrefix + 'library-hls')
|
||||
implementation project(modulePrefix + 'library-smoothstreaming')
|
||||
implementation project(modulePrefix + 'extension-gvr')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
|
@ -1,60 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2019 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.gvrdemo">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name"
|
||||
android:largeHeap="true">
|
||||
|
||||
<activity
|
||||
android:name="com.google.android.exoplayer2.gvrdemo.SampleChooserActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:exported="true"
|
||||
android:label="@string/application_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.google.android.exoplayer2.gvrdemo.PlayerActivity"
|
||||
android:configChanges="density|keyboardHidden|navigation|orientation|screenSize|uiMode"
|
||||
android:exported="false"
|
||||
android:label="@string/application_name"
|
||||
android:launchMode="singleTask"
|
||||
android:enableVrMode="@string/gvr_vr_mode_component"
|
||||
android:resizeableActivity="false"
|
||||
android:screenOrientation="landscape"
|
||||
android:theme="@style/VrActivityTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="com.google.intent.category.CARDBOARD"/>
|
||||
<category android:name="com.google.intent.category.DAYDREAM"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -1,189 +0,0 @@
|
||||
/*
|
||||
* 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.gvrdemo;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.C.ContentType;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.ext.gvr.GvrPlayerActivity;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.util.EventLogger;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||
public class PlayerActivity extends GvrPlayerActivity {
|
||||
|
||||
public static final String EXTENSION_EXTRA = "extension";
|
||||
|
||||
public static final String SPHERICAL_STEREO_MODE_EXTRA = "spherical_stereo_mode";
|
||||
public static final String SPHERICAL_STEREO_MODE_MONO = "mono";
|
||||
public static final String SPHERICAL_STEREO_MODE_TOP_BOTTOM = "top_bottom";
|
||||
public static final String SPHERICAL_STEREO_MODE_LEFT_RIGHT = "left_right";
|
||||
|
||||
private SimpleExoPlayer player;
|
||||
private DefaultTrackSelector trackSelector;
|
||||
private TrackGroupArray lastSeenTrackGroupArray;
|
||||
private boolean startAutoPlay;
|
||||
private int startWindow;
|
||||
private long startPosition;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String sphericalStereoMode = getIntent().getStringExtra(SPHERICAL_STEREO_MODE_EXTRA);
|
||||
if (sphericalStereoMode != null) {
|
||||
int stereoMode;
|
||||
if (SPHERICAL_STEREO_MODE_MONO.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_MONO;
|
||||
} else if (SPHERICAL_STEREO_MODE_TOP_BOTTOM.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_TOP_BOTTOM;
|
||||
} else if (SPHERICAL_STEREO_MODE_LEFT_RIGHT.equals(sphericalStereoMode)) {
|
||||
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
|
||||
} else {
|
||||
showToast(R.string.error_unrecognized_stereo_mode);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
setDefaultStereoMode(stereoMode);
|
||||
}
|
||||
|
||||
clearStartPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Player createPlayer() {
|
||||
Intent intent = getIntent();
|
||||
Uri uri = intent.getData();
|
||||
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this);
|
||||
|
||||
trackSelector = new DefaultTrackSelector(/* context= */ this);
|
||||
lastSeenTrackGroupArray = null;
|
||||
|
||||
player =
|
||||
new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory)
|
||||
.setTrackSelector(trackSelector)
|
||||
.build();
|
||||
player.addListener(new PlayerEventListener());
|
||||
player.setPlayWhenReady(startAutoPlay);
|
||||
player.addAnalyticsListener(new EventLogger(trackSelector));
|
||||
MediaSource mediaSource = buildMediaSource(uri, intent.getStringExtra(EXTENSION_EXTRA));
|
||||
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
|
||||
if (haveStartPosition) {
|
||||
player.seekTo(startWindow, startPosition);
|
||||
}
|
||||
player.prepare(mediaSource, !haveStartPosition, false);
|
||||
return player;
|
||||
}
|
||||
|
||||
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
||||
String userAgent = Util.getUserAgent(this, "ExoPlayerVrDemo");
|
||||
DefaultDataSourceFactory dataSourceFactory =
|
||||
new DefaultDataSourceFactory(this, new DefaultHttpDataSourceFactory(userAgent));
|
||||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStartPosition() {
|
||||
if (player != null) {
|
||||
startAutoPlay = player.getPlayWhenReady();
|
||||
startWindow = player.getCurrentWindowIndex();
|
||||
startPosition = Math.max(0, player.getContentPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private void clearStartPosition() {
|
||||
startAutoPlay = true;
|
||||
startWindow = C.INDEX_UNSET;
|
||||
startPosition = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
private void showToast(int messageId) {
|
||||
showToast(getString(messageId));
|
||||
}
|
||||
|
||||
private void showToast(String message) {
|
||||
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private class PlayerEventListener implements Player.EventListener {
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
|
||||
if (player.getPlaybackError() != null) {
|
||||
// The user has performed a seek whilst in the error state. Update the resume position so
|
||||
// that if the user then retries, playback resumes from the position to which they seeked.
|
||||
updateStartPosition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException e) {
|
||||
updateStartPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
if (trackGroups != lastSeenTrackGroupArray) {
|
||||
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (mappedTrackInfo != null) {
|
||||
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO)
|
||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
showToast(R.string.error_unsupported_video);
|
||||
}
|
||||
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO)
|
||||
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
|
||||
showToast(R.string.error_unsupported_audio);
|
||||
}
|
||||
}
|
||||
lastSeenTrackGroupArray = trackGroups;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.gvrdemo;
|
||||
|
||||
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_LEFT_RIGHT;
|
||||
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_MONO;
|
||||
import static com.google.android.exoplayer2.gvrdemo.PlayerActivity.SPHERICAL_STEREO_MODE_TOP_BOTTOM;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
|
||||
/** An activity for selecting from a list of media samples. */
|
||||
public class SampleChooserActivity extends Activity {
|
||||
|
||||
private final Sample[] samples =
|
||||
new Sample[] {
|
||||
new Sample(
|
||||
"Congo (360 top-bottom stereo)",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/360/congo.mp4",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
new Sample(
|
||||
"Sphericalv2 (180 top-bottom stereo)",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/360/sphericalv2.mp4",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
new Sample(
|
||||
"Iceland (360 top-bottom stereo ts)",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/360/iceland0.ts",
|
||||
SPHERICAL_STEREO_MODE_TOP_BOTTOM),
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.sample_chooser_activity);
|
||||
ListView sampleListView = findViewById(R.id.sample_list);
|
||||
sampleListView.setAdapter(
|
||||
new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, samples));
|
||||
sampleListView.setOnItemClickListener(
|
||||
(parent, view, position, id) ->
|
||||
startActivity(
|
||||
samples[position].buildIntent(/* context= */ SampleChooserActivity.this)));
|
||||
}
|
||||
|
||||
private static final class Sample {
|
||||
public final String name;
|
||||
public final String uri;
|
||||
public final String extension;
|
||||
public final String sphericalStereoMode;
|
||||
|
||||
public Sample(String name, String uri, String sphericalStereoMode) {
|
||||
this(name, uri, sphericalStereoMode, null);
|
||||
}
|
||||
|
||||
public Sample(String name, String uri, String sphericalStereoMode, String extension) {
|
||||
this.name = name;
|
||||
this.uri = uri;
|
||||
this.extension = extension;
|
||||
this.sphericalStereoMode = sphericalStereoMode;
|
||||
}
|
||||
|
||||
public Intent buildIntent(Context context) {
|
||||
Intent intent = new Intent(context, PlayerActivity.class);
|
||||
return intent
|
||||
.setData(Uri.parse(uri))
|
||||
.putExtra(PlayerActivity.EXTENSION_EXTRA, extension)
|
||||
.putExtra(PlayerActivity.SPHERICAL_STEREO_MODE_EXTRA, sphericalStereoMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2016 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.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ListView android:id="@+id/sample_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</LinearLayout>
|
Binary file not shown.
Before Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2019 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<string name="application_name">ExoPlayer VR Demo</string>
|
||||
|
||||
<string name="error_cleartext_not_permitted">Cleartext traffic not permitted</string>
|
||||
|
||||
<string name="error_unrecognized_stereo_mode">Unrecognized stereo mode</string>
|
||||
|
||||
<string name="error_unsupported_video">Media includes video tracks, but none are playable by this device</string>
|
||||
|
||||
<string name="error_unsupported_audio">Media includes audio tracks, but none are playable by this device</string>
|
||||
|
||||
</resources>
|
@ -1,11 +1,15 @@
|
||||
# ExoPlayer GVR extension #
|
||||
|
||||
**DEPRECATED - If you still need this extension, please contact us by filing an
|
||||
issue on our [issue tracker][].**
|
||||
|
||||
The GVR extension wraps the [Google VR SDK for Android][]. It provides a
|
||||
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
|
||||
of surround sound and ambisonic soundfields.
|
||||
|
||||
[Google VR SDK for Android]: https://developers.google.com/vr/android/
|
||||
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
|
||||
[issue tracker]: https://github.com/google/ExoPlayer/issues
|
||||
|
||||
## Getting the extension ##
|
||||
|
||||
|
@ -28,7 +28,11 @@ import java.nio.ByteOrder;
|
||||
/**
|
||||
* An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of
|
||||
* surround sound and ambisonic soundfields.
|
||||
*
|
||||
* @deprecated If you still need this component, please contact us by filing an issue on our <a
|
||||
* href="https://github.com/google/ExoPlayer/issues">issue tracker</a>.
|
||||
*/
|
||||
@Deprecated
|
||||
public final class GvrAudioProcessor implements AudioProcessor {
|
||||
|
||||
static {
|
||||
|
@ -1,344 +0,0 @@
|
||||
/*
|
||||
* 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.ext.gvr;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.opengl.Matrix;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import androidx.annotation.BinderThread;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ui.PlayerControlView;
|
||||
import com.google.android.exoplayer2.ui.spherical.PointerRenderer;
|
||||
import com.google.android.exoplayer2.ui.spherical.SceneRenderer;
|
||||
import com.google.android.exoplayer2.ui.spherical.ViewRenderer;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.vr.ndk.base.DaydreamApi;
|
||||
import com.google.vr.sdk.base.AndroidCompat;
|
||||
import com.google.vr.sdk.base.Eye;
|
||||
import com.google.vr.sdk.base.GvrActivity;
|
||||
import com.google.vr.sdk.base.GvrView;
|
||||
import com.google.vr.sdk.base.HeadTransform;
|
||||
import com.google.vr.sdk.base.Viewport;
|
||||
import com.google.vr.sdk.controller.Controller;
|
||||
import com.google.vr.sdk.controller.ControllerManager;
|
||||
import com.google.vr.sdk.controller.Orientation;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Base activity for VR 360 video playback. */
|
||||
public abstract class GvrPlayerActivity extends GvrActivity {
|
||||
|
||||
private static final int EXIT_FROM_VR_REQUEST_CODE = 42;
|
||||
|
||||
@Nullable private Player player;
|
||||
private @MonotonicNonNull ControllerManager controllerManager;
|
||||
private @MonotonicNonNull SurfaceTexture surfaceTexture;
|
||||
private @MonotonicNonNull Surface surface;
|
||||
private @MonotonicNonNull SceneRenderer sceneRenderer;
|
||||
private @MonotonicNonNull PlayerControlView playerControlView;
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setScreenAlwaysOn(true);
|
||||
|
||||
GvrView gvrView = new GvrView(/* context= */ this);
|
||||
gvrView.setRenderTargetScale(getRenderTargetScale());
|
||||
|
||||
// If a custom theme isn't specified, the Context's theme is used. For VR Activities, this is
|
||||
// the old Android default theme rather than a modern theme. Override this with a custom theme.
|
||||
Context theme = new ContextThemeWrapper(this, R.style.ExoVrTheme);
|
||||
View viewGroup = LayoutInflater.from(theme).inflate(R.layout.exo_vr_ui, /* root= */ null);
|
||||
|
||||
ViewRenderer viewRenderer = new ViewRenderer(/* context= */ this, gvrView, viewGroup);
|
||||
|
||||
playerControlView = Assertions.checkNotNull(viewGroup.findViewById(R.id.controller));
|
||||
playerControlView.setShowVrButton(true);
|
||||
playerControlView.setVrButtonListener(v -> exit());
|
||||
|
||||
sceneRenderer = new SceneRenderer();
|
||||
PointerRenderer pointerRenderer = new PointerRenderer();
|
||||
Renderer renderer = new Renderer(sceneRenderer, pointerRenderer, viewRenderer);
|
||||
|
||||
// Standard GvrView configuration
|
||||
gvrView.setEGLConfigChooser(
|
||||
8, 8, 8, 8, // RGBA bits.
|
||||
16, // Depth bits.
|
||||
0); // Stencil bits.
|
||||
gvrView.setRenderer(renderer);
|
||||
setContentView(gvrView);
|
||||
|
||||
if (gvrView.setAsyncReprojectionEnabled(true)) {
|
||||
AndroidCompat.setSustainedPerformanceMode(/* activity= */ this, true);
|
||||
}
|
||||
|
||||
// Handle the user clicking on the 'X' in the top left corner. Since this is done when the user
|
||||
// has taken the headset out of VR, it should launch the app's exit flow directly rather than
|
||||
// using Daydream's exit transition.
|
||||
gvrView.setOnCloseButtonListener(this::finish);
|
||||
|
||||
controllerManager =
|
||||
new ControllerManager(/* context= */ this, new ControllerManagerEventListener());
|
||||
Controller controller = controllerManager.getController();
|
||||
ControllerEventListener controllerEventListener =
|
||||
new ControllerEventListener(controller, pointerRenderer, viewRenderer);
|
||||
controller.setEventListener(controllerEventListener);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent unused) {
|
||||
if (requestCode == EXIT_FROM_VR_REQUEST_CODE && resultCode == RESULT_OK) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
player = createPlayer();
|
||||
Player.VideoComponent videoComponent = player.getVideoComponent();
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoFrameMetadataListener(Assertions.checkNotNull(sceneRenderer));
|
||||
videoComponent.setCameraMotionListener(sceneRenderer);
|
||||
videoComponent.setVideoSurface(surface);
|
||||
}
|
||||
Assertions.checkNotNull(playerControlView).setPlayer(player);
|
||||
Assertions.checkNotNull(controllerManager).start();
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onPause() {
|
||||
Assertions.checkNotNull(controllerManager).stop();
|
||||
Assertions.checkNotNull(playerControlView).setPlayer(null);
|
||||
Assertions.checkNotNull(player).release();
|
||||
player = null;
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
releaseSurface(surfaceTexture, surface);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by {@link #onCreate(Bundle)} to get the render target scale value that will be passed to
|
||||
* {@link GvrView#setRenderTargetScale(float)}. Since videos typically have fewer pixels per
|
||||
* degree than the phone displays, the target can normally be lower than 1 to reduce the amount of
|
||||
* work required to render the scene. The default value is 0.5.
|
||||
*
|
||||
* @return The render target scale value that will be passed to {@link
|
||||
* GvrView#setRenderTargetScale(float)}.
|
||||
*/
|
||||
protected float getRenderTargetScale() {
|
||||
return 0.5f;
|
||||
}
|
||||
|
||||
/** Called by {@link #onResume()} to create a player instance for this activity to use. */
|
||||
protected abstract Player createPlayer();
|
||||
|
||||
/**
|
||||
* Sets the stereo mode that will be used for video content that does not specify its own mode.
|
||||
*
|
||||
* @param stereoMode The default {@link C.StereoMode}.
|
||||
*/
|
||||
protected void setDefaultStereoMode(@C.StereoMode int stereoMode) {
|
||||
Assertions.checkNotNull(sceneRenderer).setDefaultStereoMode(stereoMode);
|
||||
}
|
||||
|
||||
/** Tries to exit gracefully from VR using a VR transition dialog. */
|
||||
@SuppressWarnings("nullness:argument.type.incompatible")
|
||||
protected void exit() {
|
||||
DaydreamApi daydreamApi = DaydreamApi.create(this);
|
||||
if (daydreamApi != null) {
|
||||
// Use Daydream's exit transition to avoid disorienting the user. This will cause
|
||||
// onActivityResult to be called.
|
||||
daydreamApi.exitFromVr(/* activity= */ this, EXIT_FROM_VR_REQUEST_CODE, /* data= */ null);
|
||||
daydreamApi.close();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggles PlayerControl visibility. */
|
||||
@UiThread
|
||||
protected void togglePlayerControlVisibility() {
|
||||
if (Assertions.checkNotNull(playerControlView).isVisible()) {
|
||||
playerControlView.hide();
|
||||
} else {
|
||||
playerControlView.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
|
||||
// Called on the GL thread. Post to the main thread.
|
||||
runOnUiThread(
|
||||
() -> {
|
||||
SurfaceTexture oldSurfaceTexture = this.surfaceTexture;
|
||||
Surface oldSurface = this.surface;
|
||||
this.surfaceTexture = surfaceTexture;
|
||||
this.surface = new Surface(surfaceTexture);
|
||||
if (player != null) {
|
||||
Player.VideoComponent videoComponent = player.getVideoComponent();
|
||||
if (videoComponent != null) {
|
||||
videoComponent.setVideoSurface(surface);
|
||||
}
|
||||
}
|
||||
releaseSurface(oldSurfaceTexture, oldSurface);
|
||||
});
|
||||
}
|
||||
|
||||
private static void releaseSurface(
|
||||
@Nullable SurfaceTexture oldSurfaceTexture, @Nullable Surface oldSurface) {
|
||||
if (oldSurfaceTexture != null) {
|
||||
oldSurfaceTexture.release();
|
||||
}
|
||||
if (oldSurface != null) {
|
||||
oldSurface.release();
|
||||
}
|
||||
}
|
||||
|
||||
private class Renderer implements GvrView.StereoRenderer {
|
||||
private static final float Z_NEAR = 0.1f;
|
||||
private static final float Z_FAR = 100;
|
||||
|
||||
private final SceneRenderer sceneRenderer;
|
||||
private final PointerRenderer pointerRenderer;
|
||||
private final ViewRenderer viewRenderer;
|
||||
private final float[] viewProjectionMatrix;
|
||||
|
||||
public Renderer(
|
||||
SceneRenderer sceneRenderer, PointerRenderer pointerRenderer, ViewRenderer viewRenderer) {
|
||||
this.sceneRenderer = sceneRenderer;
|
||||
this.pointerRenderer = pointerRenderer;
|
||||
this.viewRenderer = viewRenderer;
|
||||
viewProjectionMatrix = new float[16];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewFrame(HeadTransform headTransform) {}
|
||||
|
||||
@Override
|
||||
public void onDrawEye(Eye eye) {
|
||||
Matrix.multiplyMM(
|
||||
viewProjectionMatrix, 0, eye.getPerspective(Z_NEAR, Z_FAR), 0, eye.getEyeView(), 0);
|
||||
sceneRenderer.drawFrame(viewProjectionMatrix, eye.getType() == Eye.Type.RIGHT);
|
||||
if (viewRenderer.isVisible()) {
|
||||
viewRenderer.draw(viewProjectionMatrix);
|
||||
pointerRenderer.draw(viewProjectionMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinishFrame(Viewport viewport) {}
|
||||
|
||||
@Override
|
||||
public void onSurfaceCreated(EGLConfig config) {
|
||||
onSurfaceTextureAvailable(sceneRenderer.init());
|
||||
viewRenderer.init();
|
||||
pointerRenderer.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceChanged(int width, int height) {}
|
||||
|
||||
@Override
|
||||
public void onRendererShutdown() {
|
||||
viewRenderer.shutdown();
|
||||
pointerRenderer.shutdown();
|
||||
sceneRenderer.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private class ControllerEventListener extends Controller.EventListener {
|
||||
|
||||
private final Controller controller;
|
||||
private final PointerRenderer pointerRenderer;
|
||||
private final ViewRenderer viewRenderer;
|
||||
private final float[] controllerOrientationMatrix;
|
||||
private boolean clickButtonDown;
|
||||
private boolean appButtonDown;
|
||||
|
||||
public ControllerEventListener(
|
||||
Controller controller, PointerRenderer pointerRenderer, ViewRenderer viewRenderer) {
|
||||
this.controller = controller;
|
||||
this.pointerRenderer = pointerRenderer;
|
||||
this.viewRenderer = viewRenderer;
|
||||
controllerOrientationMatrix = new float[16];
|
||||
}
|
||||
|
||||
@Override
|
||||
@BinderThread
|
||||
public void onUpdate() {
|
||||
controller.update();
|
||||
Orientation orientation = controller.orientation;
|
||||
orientation.toRotationMatrix(controllerOrientationMatrix);
|
||||
pointerRenderer.setControllerOrientation(controllerOrientationMatrix);
|
||||
|
||||
if (clickButtonDown || controller.clickButtonState) {
|
||||
int action;
|
||||
if (clickButtonDown != controller.clickButtonState) {
|
||||
clickButtonDown = controller.clickButtonState;
|
||||
action = clickButtonDown ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP;
|
||||
} else {
|
||||
action = MotionEvent.ACTION_MOVE;
|
||||
}
|
||||
float[] yawPitchRoll = orientation.toYawPitchRollRadians(new float[3]);
|
||||
runOnUiThread(() -> dispatchClick(action, yawPitchRoll[0], yawPitchRoll[1]));
|
||||
} else if (!appButtonDown && controller.appButtonState) {
|
||||
runOnUiThread(GvrPlayerActivity.this::togglePlayerControlVisibility);
|
||||
}
|
||||
appButtonDown = controller.appButtonState;
|
||||
}
|
||||
|
||||
private void dispatchClick(int action, float yaw, float pitch) {
|
||||
boolean clickedOnView = viewRenderer.simulateClick(action, yaw, pitch);
|
||||
if (action == MotionEvent.ACTION_DOWN && !clickedOnView) {
|
||||
togglePlayerControlVisibility();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ControllerManagerEventListener implements ControllerManager.EventListener {
|
||||
|
||||
@Override
|
||||
public void onApiStatusChanged(int status) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecentered() {
|
||||
// TODO: If in cardboard mode call gvrView.recenterHeadTracker().
|
||||
runOnUiThread(() -> Assertions.checkNotNull(playerControlView).show());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
# ExoPlayer Firebase JobDispatcher extension #
|
||||
|
||||
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][] instead.**
|
||||
**DEPRECATED - Please use [WorkManager extension][] or [PlatformScheduler][]
|
||||
instead.**
|
||||
|
||||
This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][].
|
||||
|
||||
|
@ -1,294 +0,0 @@
|
||||
/*
|
||||
* 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.util.GlUtil.checkGlError;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.opengl.GLES11Ext;
|
||||
import android.opengl.GLES20;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.util.GlUtil;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* Renders a canvas on a quad.
|
||||
*
|
||||
* <p>A CanvasRenderer can be created on any thread, but {@link #init()} needs to be called on the
|
||||
* GL thread before it can be rendered.
|
||||
*/
|
||||
public final class CanvasRenderer {
|
||||
|
||||
private static final float WIDTH_UNIT = 0.8f;
|
||||
private static final float DISTANCE_UNIT = 1f;
|
||||
private static final float X_UNIT = -WIDTH_UNIT / 2;
|
||||
private static final float Y_UNIT = -0.3f;
|
||||
|
||||
// Standard vertex shader that passes through the texture data.
|
||||
private static final String[] VERTEX_SHADER_CODE = {
|
||||
"uniform mat4 uMvpMatrix;",
|
||||
// 3D position data.
|
||||
"attribute vec3 aPosition;",
|
||||
// 2D UV vertices.
|
||||
"attribute vec2 aTexCoords;",
|
||||
"varying vec2 vTexCoords;",
|
||||
|
||||
// Standard transformation.
|
||||
"void main() {",
|
||||
" gl_Position = uMvpMatrix * vec4(aPosition, 1);",
|
||||
" vTexCoords = aTexCoords;",
|
||||
"}"
|
||||
};
|
||||
|
||||
private static final String[] FRAGMENT_SHADER_CODE = {
|
||||
// This is required since the texture data is GL_TEXTURE_EXTERNAL_OES.
|
||||
"#extension GL_OES_EGL_image_external : require",
|
||||
"precision mediump float;",
|
||||
"uniform samplerExternalOES uTexture;",
|
||||
"varying vec2 vTexCoords;",
|
||||
"void main() {",
|
||||
" gl_FragColor = texture2D(uTexture, vTexCoords);",
|
||||
"}"
|
||||
};
|
||||
|
||||
// The quad has 2 triangles built from 4 total vertices. Each vertex has 3 position and 2 texture
|
||||
// coordinates.
|
||||
private static final int POSITION_COORDS_PER_VERTEX = 3;
|
||||
private static final int TEXTURE_COORDS_PER_VERTEX = 2;
|
||||
private static final int COORDS_PER_VERTEX =
|
||||
POSITION_COORDS_PER_VERTEX + TEXTURE_COORDS_PER_VERTEX;
|
||||
private static final int VERTEX_STRIDE_BYTES = COORDS_PER_VERTEX * C.BYTES_PER_FLOAT;
|
||||
private static final int VERTEX_COUNT = 4;
|
||||
private static final float HALF_PI = (float) (Math.PI / 2);
|
||||
|
||||
private final FloatBuffer vertexBuffer;
|
||||
private final AtomicBoolean surfaceDirty;
|
||||
|
||||
private int width;
|
||||
private int height;
|
||||
private float heightUnit;
|
||||
|
||||
// Program-related GL items. These are only valid if program != 0.
|
||||
private int program = 0;
|
||||
private int mvpMatrixHandle;
|
||||
private int positionHandle;
|
||||
private int textureCoordsHandle;
|
||||
private int textureHandle;
|
||||
private int textureId;
|
||||
|
||||
// Components used to manage the Canvas that the View is rendered to. These are only valid after
|
||||
// GL initialization. The client of this class acquires a Canvas from the Surface, writes to it
|
||||
// and posts it. This marks the Surface as dirty. The GL code then updates the SurfaceTexture
|
||||
// when rendering only if it is dirty.
|
||||
private @MonotonicNonNull SurfaceTexture displaySurfaceTexture;
|
||||
private @MonotonicNonNull Surface displaySurface;
|
||||
|
||||
public CanvasRenderer() {
|
||||
vertexBuffer = GlUtil.createBuffer(COORDS_PER_VERTEX * VERTEX_COUNT);
|
||||
surfaceDirty = new AtomicBoolean();
|
||||
}
|
||||
|
||||
public void setSize(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
heightUnit = WIDTH_UNIT * height / width;
|
||||
|
||||
float[] vertexData = new float[COORDS_PER_VERTEX * VERTEX_COUNT];
|
||||
int vertexDataIndex = 0;
|
||||
for (int y = 0; y < 2; y++) {
|
||||
for (int x = 0; x < 2; x++) {
|
||||
vertexData[vertexDataIndex++] = X_UNIT + (WIDTH_UNIT * x);
|
||||
vertexData[vertexDataIndex++] = Y_UNIT + (heightUnit * y);
|
||||
vertexData[vertexDataIndex++] = -DISTANCE_UNIT;
|
||||
vertexData[vertexDataIndex++] = x;
|
||||
vertexData[vertexDataIndex++] = 1 - y;
|
||||
}
|
||||
}
|
||||
vertexBuffer.position(0);
|
||||
vertexBuffer.put(vertexData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link Surface#lockCanvas(Rect)}.
|
||||
*
|
||||
* @return {@link Canvas} for the View to render to or {@code null} if {@link #init()} has not yet
|
||||
* been called.
|
||||
*/
|
||||
@Nullable
|
||||
public Canvas lockCanvas() {
|
||||
return displaySurface == null ? null : displaySurface.lockCanvas(/* inOutDirty= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link Surface#unlockCanvasAndPost(Canvas)} and marks the SurfaceTexture as dirty.
|
||||
*
|
||||
* @param canvas the canvas returned from {@link #lockCanvas()}
|
||||
*/
|
||||
public void unlockCanvasAndPost(@Nullable Canvas canvas) {
|
||||
if (canvas == null || displaySurface == null) {
|
||||
// glInit() hasn't run yet.
|
||||
return;
|
||||
}
|
||||
displaySurface.unlockCanvasAndPost(canvas);
|
||||
}
|
||||
|
||||
/** Finishes constructing this object on the GL Thread. */
|
||||
public void init() {
|
||||
if (program != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the program.
|
||||
program = GlUtil.compileProgram(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE);
|
||||
mvpMatrixHandle = GLES20.glGetUniformLocation(program, "uMvpMatrix");
|
||||
positionHandle = GLES20.glGetAttribLocation(program, "aPosition");
|
||||
textureCoordsHandle = GLES20.glGetAttribLocation(program, "aTexCoords");
|
||||
textureHandle = GLES20.glGetUniformLocation(program, "uTexture");
|
||||
textureId = GlUtil.createExternalTexture();
|
||||
checkGlError();
|
||||
|
||||
// Create the underlying SurfaceTexture with the appropriate size.
|
||||
displaySurfaceTexture = new SurfaceTexture(textureId);
|
||||
displaySurfaceTexture.setOnFrameAvailableListener(surfaceTexture -> surfaceDirty.set(true));
|
||||
displaySurfaceTexture.setDefaultBufferSize(width, height);
|
||||
displaySurface = new Surface(displaySurfaceTexture);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the quad.
|
||||
*
|
||||
* @param viewProjectionMatrix Array of floats containing the quad's 4x4 perspective matrix in the
|
||||
* {@link android.opengl.Matrix} format.
|
||||
*/
|
||||
public void draw(float[] viewProjectionMatrix) {
|
||||
if (displaySurfaceTexture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
GLES20.glUseProgram(program);
|
||||
checkGlError();
|
||||
|
||||
GLES20.glEnableVertexAttribArray(positionHandle);
|
||||
GLES20.glEnableVertexAttribArray(textureCoordsHandle);
|
||||
checkGlError();
|
||||
|
||||
GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, viewProjectionMatrix, 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.
|
||||
vertexBuffer.position(POSITION_COORDS_PER_VERTEX);
|
||||
GLES20.glVertexAttribPointer(
|
||||
textureCoordsHandle,
|
||||
TEXTURE_COORDS_PER_VERTEX,
|
||||
GLES20.GL_FLOAT,
|
||||
false,
|
||||
VERTEX_STRIDE_BYTES,
|
||||
vertexBuffer);
|
||||
checkGlError();
|
||||
|
||||
if (surfaceDirty.compareAndSet(true, false)) {
|
||||
// If the Surface has been written to, get the new data onto the SurfaceTexture.
|
||||
displaySurfaceTexture.updateTexImage();
|
||||
}
|
||||
|
||||
// Render.
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT);
|
||||
checkGlError();
|
||||
|
||||
GLES20.glDisableVertexAttribArray(positionHandle);
|
||||
GLES20.glDisableVertexAttribArray(textureCoordsHandle);
|
||||
}
|
||||
|
||||
/** Frees GL resources. */
|
||||
public void shutdown() {
|
||||
if (program != 0) {
|
||||
GLES20.glDeleteProgram(program);
|
||||
GLES20.glDeleteTextures(1, new int[] {textureId}, 0);
|
||||
}
|
||||
|
||||
if (displaySurfaceTexture != null) {
|
||||
displaySurfaceTexture.release();
|
||||
}
|
||||
if (displaySurface != null) {
|
||||
displaySurface.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates an orientation into pixel coordinates on the canvas.
|
||||
*
|
||||
* <p>This is a minimal hit detection system that works for this quad because it has no model
|
||||
* matrix. All the math is based on the fact that its size and distance are hard-coded into this
|
||||
* class. For a more complex 3D mesh, a general bounding box and ray collision system would be
|
||||
* required.
|
||||
*
|
||||
* @param yaw Yaw of the orientation in radians.
|
||||
* @param pitch Pitch of the orientation in radians.
|
||||
* @return A {@link PointF} which contains the translated coordinate, or null if the point is
|
||||
* outside of the quad's bounds.
|
||||
*/
|
||||
@Nullable
|
||||
public PointF translateClick(float yaw, float pitch) {
|
||||
return internalTranslateClick(
|
||||
yaw, pitch, X_UNIT, Y_UNIT, WIDTH_UNIT, heightUnit, width, height);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
/* package */ static PointF internalTranslateClick(
|
||||
float yaw,
|
||||
float pitch,
|
||||
float xUnit,
|
||||
float yUnit,
|
||||
float widthUnit,
|
||||
float heightUnit,
|
||||
int widthPixel,
|
||||
int heightPixel) {
|
||||
if (yaw >= HALF_PI || yaw <= -HALF_PI || pitch >= HALF_PI || pitch <= -HALF_PI) {
|
||||
return null;
|
||||
}
|
||||
double clickXUnit = Math.tan(yaw) * DISTANCE_UNIT - xUnit;
|
||||
double clickYUnit = Math.tan(pitch) * DISTANCE_UNIT - yUnit;
|
||||
if (clickXUnit < 0 || clickXUnit > widthUnit || clickYUnit < 0 || clickYUnit > heightUnit) {
|
||||
return null;
|
||||
}
|
||||
// Convert from the polar coordinates of the controller to the rectangular coordinates of the
|
||||
// View. Note the negative yaw and pitch used to generate Android-compliant x and y coordinates.
|
||||
float clickXPixel = (float) (widthPixel - clickXUnit * widthPixel / widthUnit);
|
||||
float clickYPixel = (float) (heightPixel - clickYUnit * heightPixel / heightUnit);
|
||||
return new PointF(clickXPixel, clickYPixel);
|
||||
}
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
/*
|
||||
* 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.util.GlUtil.checkGlError;
|
||||
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.Matrix;
|
||||
import com.google.android.exoplayer2.util.GlUtil;
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
/** Renders a pointer. */
|
||||
public final class PointerRenderer {
|
||||
// The pointer quad is 2 * SIZE units.
|
||||
private static final float SIZE = 0.01f;
|
||||
private static final float DISTANCE = 1;
|
||||
|
||||
// Standard vertex shader.
|
||||
private static final String[] VERTEX_SHADER_CODE =
|
||||
new String[] {
|
||||
"uniform mat4 uMvpMatrix;",
|
||||
"attribute vec3 aPosition;",
|
||||
"varying vec2 vCoords;",
|
||||
|
||||
// Pass through normalized vertex coordinates.
|
||||
"void main() {",
|
||||
" gl_Position = uMvpMatrix * vec4(aPosition, 1);",
|
||||
" vCoords = aPosition.xy / vec2(" + SIZE + ", " + SIZE + ");",
|
||||
"}"
|
||||
};
|
||||
|
||||
// Procedurally render a ring on the quad between the specified radii.
|
||||
private static final String[] FRAGMENT_SHADER_CODE =
|
||||
new String[] {
|
||||
"precision mediump float;",
|
||||
"varying vec2 vCoords;",
|
||||
|
||||
// Simple ring shader that is white between the radii and transparent elsewhere.
|
||||
"void main() {",
|
||||
" float r = length(vCoords);",
|
||||
// Blend the edges of the ring at .55 +/- .05 and .85 +/- .05.
|
||||
" float alpha = smoothstep(0.5, 0.6, r) * (1.0 - smoothstep(0.8, 0.9, r));",
|
||||
" if (alpha == 0.0) {",
|
||||
" discard;",
|
||||
" } else {",
|
||||
" gl_FragColor = vec4(alpha);",
|
||||
" }",
|
||||
"}"
|
||||
};
|
||||
|
||||
// Simple quad mesh.
|
||||
private static final int COORDS_PER_VERTEX = 3;
|
||||
private static final float[] VERTEX_DATA = {
|
||||
-SIZE, -SIZE, -DISTANCE, SIZE, -SIZE, -DISTANCE, -SIZE, SIZE, -DISTANCE, SIZE, SIZE, -DISTANCE,
|
||||
};
|
||||
private final FloatBuffer vertexBuffer;
|
||||
|
||||
// The pointer doesn't have a real modelMatrix. Its distance is baked into the mesh and it
|
||||
// uses a rotation matrix when rendered.
|
||||
private final float[] modelViewProjectionMatrix;
|
||||
// This is accessed on the binder & GL Threads.
|
||||
private final float[] controllerOrientationMatrix;
|
||||
|
||||
// Program-related GL items. These are only valid if program != 0.
|
||||
private int program = 0;
|
||||
private int mvpMatrixHandle;
|
||||
private int positionHandle;
|
||||
|
||||
public PointerRenderer() {
|
||||
vertexBuffer = GlUtil.createBuffer(VERTEX_DATA);
|
||||
modelViewProjectionMatrix = new float[16];
|
||||
controllerOrientationMatrix = new float[16];
|
||||
Matrix.setIdentityM(controllerOrientationMatrix, 0);
|
||||
}
|
||||
|
||||
/** Finishes initialization of this object on the GL thread. */
|
||||
public void init() {
|
||||
if (program != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
program = GlUtil.compileProgram(VERTEX_SHADER_CODE, FRAGMENT_SHADER_CODE);
|
||||
mvpMatrixHandle = GLES20.glGetUniformLocation(program, "uMvpMatrix");
|
||||
positionHandle = GLES20.glGetAttribLocation(program, "aPosition");
|
||||
checkGlError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the pointer.
|
||||
*
|
||||
* @param viewProjectionMatrix Scene's view projection matrix.
|
||||
*/
|
||||
public void draw(float[] viewProjectionMatrix) {
|
||||
// Configure shader.
|
||||
GLES20.glUseProgram(program);
|
||||
checkGlError();
|
||||
|
||||
synchronized (controllerOrientationMatrix) {
|
||||
Matrix.multiplyMM(
|
||||
modelViewProjectionMatrix, 0, viewProjectionMatrix, 0, controllerOrientationMatrix, 0);
|
||||
}
|
||||
GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, modelViewProjectionMatrix, 0);
|
||||
checkGlError();
|
||||
|
||||
// Render quad.
|
||||
GLES20.glEnableVertexAttribArray(positionHandle);
|
||||
checkGlError();
|
||||
|
||||
GLES20.glVertexAttribPointer(
|
||||
positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, /* stride= */ 0, vertexBuffer);
|
||||
checkGlError();
|
||||
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_DATA.length / COORDS_PER_VERTEX);
|
||||
checkGlError();
|
||||
|
||||
GLES20.glDisableVertexAttribArray(positionHandle);
|
||||
}
|
||||
|
||||
/** Frees GL resources. */
|
||||
public void shutdown() {
|
||||
if (program != 0) {
|
||||
GLES20.glDeleteProgram(program);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the pointer's position with the latest Controller pose. */
|
||||
public void setControllerOrientation(float[] rotationMatrix) {
|
||||
synchronized (controllerOrientationMatrix) {
|
||||
System.arraycopy(rotationMatrix, 0, controllerOrientationMatrix, 0, rotationMatrix.length);
|
||||
}
|
||||
}
|
||||
}
|
@ -37,7 +37,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Renders a GL Scene. */
|
||||
public final class SceneRenderer implements VideoFrameMetadataListener, CameraMotionListener {
|
||||
/* package */ final class SceneRenderer
|
||||
implements VideoFrameMetadataListener, CameraMotionListener {
|
||||
|
||||
private final AtomicBoolean frameAvailable;
|
||||
private final AtomicBoolean resetRotationAtNextFrame;
|
||||
|
@ -1,137 +0,0 @@
|
||||
/*
|
||||
* 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.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.SystemClock;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/** Renders a {@link View} on a quad and supports simulated clicks on the view. */
|
||||
public final class ViewRenderer {
|
||||
|
||||
private final CanvasRenderer canvasRenderer;
|
||||
private final View view;
|
||||
private final InternalFrameLayout frameLayout;
|
||||
|
||||
/**
|
||||
* @param context A context.
|
||||
* @param parentView The parent view.
|
||||
* @param view The view to render.
|
||||
*/
|
||||
public ViewRenderer(Context context, ViewGroup parentView, View view) {
|
||||
this.canvasRenderer = new CanvasRenderer();
|
||||
this.view = view;
|
||||
// Wrap the view in an internal view that redirects rendering.
|
||||
frameLayout = new InternalFrameLayout(context, view, canvasRenderer);
|
||||
canvasRenderer.setSize(frameLayout.getMeasuredWidth(), frameLayout.getMeasuredHeight());
|
||||
// The internal view must be added to the parent to ensure proper delivery of UI events.
|
||||
parentView.addView(frameLayout);
|
||||
}
|
||||
|
||||
/** Finishes constructing this object on the GL Thread. */
|
||||
public void init() {
|
||||
canvasRenderer.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the view as a quad.
|
||||
*
|
||||
* @param viewProjectionMatrix Array of floats containing the quad's 4x4 perspective matrix in the
|
||||
* {@link android.opengl.Matrix} format.
|
||||
*/
|
||||
public void draw(float[] viewProjectionMatrix) {
|
||||
canvasRenderer.draw(viewProjectionMatrix);
|
||||
}
|
||||
|
||||
/** Frees GL resources. */
|
||||
public void shutdown() {
|
||||
canvasRenderer.shutdown();
|
||||
}
|
||||
|
||||
/** Returns whether the view is currently visible. */
|
||||
@UiThread
|
||||
public boolean isVisible() {
|
||||
return view.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates a click on the view.
|
||||
*
|
||||
* @param action Click action.
|
||||
* @param yaw Yaw of the click's orientation in radians.
|
||||
* @param pitch Pitch of the click's orientation in radians.
|
||||
* @return Whether the click was simulated. If false then the view is not visible or the click was
|
||||
* outside of its bounds.
|
||||
*/
|
||||
@UiThread
|
||||
public boolean simulateClick(int action, float yaw, float pitch) {
|
||||
if (!isVisible()) {
|
||||
return false;
|
||||
}
|
||||
@Nullable PointF point = canvasRenderer.translateClick(yaw, pitch);
|
||||
if (point == null) {
|
||||
return false;
|
||||
}
|
||||
long now = SystemClock.uptimeMillis();
|
||||
MotionEvent event = MotionEvent.obtain(now, now, action, point.x, point.y, /* metaState= */ 1);
|
||||
frameLayout.dispatchTouchEvent(event);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final class InternalFrameLayout extends FrameLayout {
|
||||
|
||||
private final CanvasRenderer canvasRenderer;
|
||||
|
||||
public InternalFrameLayout(Context context, View wrappedView, CanvasRenderer canvasRenderer) {
|
||||
super(context);
|
||||
this.canvasRenderer = canvasRenderer;
|
||||
addView(wrappedView);
|
||||
measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
int width = getMeasuredWidth();
|
||||
int height = getMeasuredHeight();
|
||||
Assertions.checkState(width > 0 && height > 0);
|
||||
setLayoutParams(new FrameLayout.LayoutParams(width, height));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchDraw(Canvas notUsed) {
|
||||
@Nullable Canvas glCanvas = canvasRenderer.lockCanvas();
|
||||
if (glCanvas == null) {
|
||||
// This happens if Android tries to draw this View before GL initialization completes. We
|
||||
// need to retry until the draw call happens after GL invalidation.
|
||||
postInvalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the canvas first.
|
||||
glCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
// Have Android render the child views.
|
||||
super.dispatchDraw(glCanvas);
|
||||
// Commit the changes.
|
||||
canvasRenderer.unlockCanvasAndPost(glCanvas);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* 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.graphics.PointF;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Tests for {@link CanvasRenderer}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class CanvasRendererTest {
|
||||
|
||||
private static final float JUST_BELOW_45_DEGREES = (float) (Math.PI / 4 - 1.0E-08);
|
||||
private static final float JUST_ABOVE_45_DEGREES = (float) (Math.PI / 4 + 1.0E-08);
|
||||
private static final float TOLERANCE = .00001f;
|
||||
|
||||
@Test
|
||||
public void testClicksOnCanvas() {
|
||||
assertClick(translateClick(JUST_BELOW_45_DEGREES, JUST_BELOW_45_DEGREES), 0, 0);
|
||||
assertClick(translateClick(JUST_BELOW_45_DEGREES, -JUST_BELOW_45_DEGREES), 0, 100);
|
||||
assertClick(translateClick(0, 0), 50, 50);
|
||||
assertClick(translateClick(-JUST_BELOW_45_DEGREES, JUST_BELOW_45_DEGREES), 100, 0);
|
||||
assertClick(translateClick(-JUST_BELOW_45_DEGREES, -JUST_BELOW_45_DEGREES), 100, 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClicksNotOnCanvas() {
|
||||
assertThat(translateClick(JUST_ABOVE_45_DEGREES, JUST_ABOVE_45_DEGREES)).isNull();
|
||||
assertThat(translateClick(JUST_ABOVE_45_DEGREES, -JUST_ABOVE_45_DEGREES)).isNull();
|
||||
assertThat(translateClick(-JUST_ABOVE_45_DEGREES, JUST_ABOVE_45_DEGREES)).isNull();
|
||||
assertThat(translateClick(-JUST_ABOVE_45_DEGREES, -JUST_ABOVE_45_DEGREES)).isNull();
|
||||
assertThat(translateClick((float) (Math.PI / 2), 0)).isNull();
|
||||
assertThat(translateClick(0, (float) Math.PI)).isNull();
|
||||
}
|
||||
|
||||
private static PointF translateClick(float yaw, float pitch) {
|
||||
return CanvasRenderer.internalTranslateClick(
|
||||
yaw,
|
||||
pitch,
|
||||
/* xUnit= */ -1,
|
||||
/* yUnit= */ -1,
|
||||
/* widthUnit= */ 2,
|
||||
/* heightUnit= */ 2,
|
||||
/* widthPixel= */ 100,
|
||||
/* heightPixel= */ 100);
|
||||
}
|
||||
|
||||
private static void assertClick(@Nullable PointF actual, float expectedX, float expectedY) {
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(actual.x).isWithin(TOLERANCE).of(expectedX);
|
||||
assertThat(actual.y).isWithin(TOLERANCE).of(expectedY);
|
||||
}
|
||||
}
|
@ -20,12 +20,10 @@ if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||
|
||||
include modulePrefix + 'demo'
|
||||
include modulePrefix + 'demo-cast'
|
||||
include modulePrefix + 'demo-gvr'
|
||||
include modulePrefix + 'demo-surface'
|
||||
include modulePrefix + 'playbacktests'
|
||||
project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main')
|
||||
project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast')
|
||||
project(modulePrefix + 'demo-gvr').projectDir = new File(rootDir, 'demos/gvr')
|
||||
project(modulePrefix + 'demo-surface').projectDir = new File(rootDir, 'demos/surface')
|
||||
project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests')
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user