| /* |
| * Copyright 2017 Google Inc. All Rights Reserved. |
| |
| * 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.vr.sdk.samples.simplevideowidget; |
| |
| import android.app.Activity; |
| import android.content.Intent; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.text.Html; |
| import android.text.method.LinkMovementMethod; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.view.View; |
| import android.widget.ImageButton; |
| import android.widget.SeekBar; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| import com.google.vr.sdk.widgets.video.VrVideoEventListener; |
| import com.google.vr.sdk.widgets.video.VrVideoView; |
| import com.google.vr.sdk.widgets.video.VrVideoView.Options; |
| import java.io.IOException; |
| |
| /** |
| * A test activity that renders a 360 video using {@link VrVideoView}. |
| * It loads the congo video from app's assets by default. User can use it to load any video files |
| * using adb shell commands such as: |
| * |
| * <p>Video from asset folder |
| * adb shell am start -a android.intent.action.VIEW \ |
| * -n com.google.vr.sdk.samples.simplevideowidget/.SimpleVrVideoActivity \ |
| * -d "file:///android_asset/congo.mp4" |
| * |
| * <p>Video located on the phone's SD card. |
| * adb shell am start -a android.intent.action.VIEW \ |
| * -n com.google.vr.sdk.samples.simplevideowidget/.SimpleVrVideoActivity \ |
| * -d /sdcard/FILENAME.MP4 |
| * |
| * <p>Video hosted on a website: |
| * adb shell am start -a android.intent.action.VIEW \ |
| * -n com.google.vr.sdk.samples.simplevideowidget/.SimpleVrVideoActivity \ |
| * -d "https://EXAMPLE.COM/FILENAME.MP4" |
| * |
| * <p>To load HLS files add "--ei inputFormat 2" to pass in an integer extra which will set |
| * VrVideoView.Options.inputFormat to FORMAT_HLS. e.g. |
| * adb shell am start -a android.intent.action.VIEW \ |
| * -n com.google.vr.sdk.samples.simplevideowidget/.SimpleVrVideoActivity \ |
| * -d "file:///android_asset/hls/iceland.m3u8" \ |
| * --ei inputFormat 2 --ei inputType 2 |
| * |
| * <p>To load MPEG-DASH files add "--ei inputFormat 3" to pass in an integer extra which will set |
| * VrVideoView.Options.inputFormat to FORMAT_DASH. e.g. |
| * adb shell am start -a android.intent.action.VIEW \ |
| * -n com.google.vr.sdk.samples.simplevideowidget/.SimpleVrVideoActivity \ |
| * -d "file:///android_asset/dash/congo_dash.mpd" \ |
| * --ei inputFormat 3 --ei inputType 2 |
| * |
| * <p>To specify that the video is of type stereo over under (has images for left and right eyes), |
| * add "--ei inputType 2" to pass in an integer extra which will set VrVideoView.Options.inputType |
| * to TYPE_STEREO_OVER_UNDER. This can be combined with other extras, e.g: |
| * adb shell am start -a android.intent.action.VIEW \ |
| * -n com.google.vr.sdk.samples.simplevideowidget/.SimpleVrVideoActivity \ |
| * -d "https://EXAMPLE.COM/FILENAME.MP4" \ |
| * --ei inputType 2 |
| */ |
| public class SimpleVrVideoActivity extends Activity { |
| private static final String TAG = SimpleVrVideoActivity.class.getSimpleName(); |
| |
| /** |
| * Preserve the video's state when rotating the phone. |
| */ |
| private static final String STATE_IS_PAUSED = "isPaused"; |
| private static final String STATE_PROGRESS_TIME = "progressTime"; |
| /** |
| * The video duration doesn't need to be preserved, but it is saved in this example. This allows |
| * the seekBar to be configured during {@link #onRestoreInstanceState(Bundle)} rather than waiting |
| * for the video to be reloaded and analyzed. This avoid UI jank. |
| */ |
| private static final String STATE_VIDEO_DURATION = "videoDuration"; |
| |
| /** |
| * Arbitrary constants and variable to track load status. In this example, this variable should |
| * only be accessed on the UI thread. In a real app, this variable would be code that performs |
| * some UI actions when the video is fully loaded. |
| */ |
| public static final int LOAD_VIDEO_STATUS_UNKNOWN = 0; |
| public static final int LOAD_VIDEO_STATUS_SUCCESS = 1; |
| public static final int LOAD_VIDEO_STATUS_ERROR = 2; |
| |
| private int loadVideoStatus = LOAD_VIDEO_STATUS_UNKNOWN; |
| |
| /** Tracks the file to be loaded across the lifetime of this app. **/ |
| private Uri fileUri; |
| |
| /** Configuration information for the video. **/ |
| private Options videoOptions = new Options(); |
| |
| private VideoLoaderTask backgroundVideoLoaderTask; |
| |
| /** |
| * The video view and its custom UI elements. |
| */ |
| protected VrVideoView videoWidgetView; |
| |
| /** |
| * Seeking UI & progress indicator. The seekBar's progress value represents milliseconds in the |
| * video. |
| */ |
| private SeekBar seekBar; |
| private TextView statusText; |
| |
| private ImageButton volumeToggle; |
| private boolean isMuted; |
| |
| /** |
| * By default, the video will start playing as soon as it is loaded. This can be changed by using |
| * {@link VrVideoView#pauseVideo()} after loading the video. |
| */ |
| private boolean isPaused = false; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setContentView(R.layout.main_layout); |
| |
| seekBar = (SeekBar) findViewById(R.id.seek_bar); |
| seekBar.setOnSeekBarChangeListener(new SeekBarListener()); |
| statusText = (TextView) findViewById(R.id.status_text); |
| |
| // Make the source link clickable. |
| TextView sourceText = (TextView) findViewById(R.id.source); |
| sourceText.setText(Html.fromHtml(getString(R.string.source))); |
| sourceText.setMovementMethod(LinkMovementMethod.getInstance()); |
| |
| // Bind input and output objects for the view. |
| videoWidgetView = (VrVideoView) findViewById(R.id.video_view); |
| videoWidgetView.setEventListener(new ActivityEventListener()); |
| |
| volumeToggle = (ImageButton) findViewById(R.id.volume_toggle); |
| volumeToggle.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| setIsMuted(!isMuted); |
| } |
| }); |
| |
| loadVideoStatus = LOAD_VIDEO_STATUS_UNKNOWN; |
| |
| // Initial launch of the app or an Activity recreation due to rotation. |
| handleIntent(getIntent()); |
| } |
| |
| /** |
| * Called when the Activity is already running and it's given a new intent. |
| */ |
| @Override |
| protected void onNewIntent(Intent intent) { |
| Log.i(TAG, this.hashCode() + ".onNewIntent()"); |
| // Save the intent. This allows the getIntent() call in onCreate() to use this new Intent during |
| // future invocations. |
| setIntent(intent); |
| // Load the new video. |
| handleIntent(intent); |
| } |
| |
| public int getLoadVideoStatus() { |
| return loadVideoStatus; |
| } |
| |
| private void setIsMuted(boolean isMuted) { |
| this.isMuted = isMuted; |
| volumeToggle.setImageResource(isMuted ? R.drawable.volume_off : R.drawable.volume_on); |
| videoWidgetView.setVolume(isMuted ? 0.0f : 1.0f); |
| } |
| |
| public boolean isMuted() { |
| return isMuted; |
| } |
| |
| /** |
| * Load custom videos based on the Intent or load the default video. See the Javadoc for this |
| * class for information on generating a custom intent via adb. |
| */ |
| private void handleIntent(Intent intent) { |
| // Determine if the Intent contains a file to load. |
| if (Intent.ACTION_VIEW.equals(intent.getAction())) { |
| Log.i(TAG, "ACTION_VIEW Intent received"); |
| |
| fileUri = intent.getData(); |
| if (fileUri == null) { |
| Log.w(TAG, "No data uri specified. Use \"-d /path/filename\"."); |
| } else { |
| Log.i(TAG, "Using file " + fileUri.toString()); |
| } |
| |
| videoOptions.inputFormat = intent.getIntExtra("inputFormat", Options.FORMAT_DEFAULT); |
| videoOptions.inputType = intent.getIntExtra("inputType", Options.TYPE_MONO); |
| } else { |
| Log.i(TAG, "Intent is not ACTION_VIEW. Using the default video."); |
| fileUri = null; |
| } |
| |
| // Load the bitmap in a background thread to avoid blocking the UI thread. This operation can |
| // take 100s of milliseconds. |
| if (backgroundVideoLoaderTask != null) { |
| // Cancel any task from a previous intent sent to this activity. |
| backgroundVideoLoaderTask.cancel(true); |
| } |
| backgroundVideoLoaderTask = new VideoLoaderTask(); |
| backgroundVideoLoaderTask.execute(Pair.create(fileUri, videoOptions)); |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle savedInstanceState) { |
| savedInstanceState.putLong(STATE_PROGRESS_TIME, videoWidgetView.getCurrentPosition()); |
| savedInstanceState.putLong(STATE_VIDEO_DURATION, videoWidgetView.getDuration()); |
| savedInstanceState.putBoolean(STATE_IS_PAUSED, isPaused); |
| super.onSaveInstanceState(savedInstanceState); |
| } |
| |
| @Override |
| public void onRestoreInstanceState(Bundle savedInstanceState) { |
| super.onRestoreInstanceState(savedInstanceState); |
| |
| long progressTime = savedInstanceState.getLong(STATE_PROGRESS_TIME); |
| videoWidgetView.seekTo(progressTime); |
| seekBar.setMax((int) savedInstanceState.getLong(STATE_VIDEO_DURATION)); |
| seekBar.setProgress((int) progressTime); |
| |
| isPaused = savedInstanceState.getBoolean(STATE_IS_PAUSED); |
| if (isPaused) { |
| videoWidgetView.pauseVideo(); |
| } |
| } |
| |
| @Override |
| protected void onPause() { |
| super.onPause(); |
| // Prevent the view from rendering continuously when in the background. |
| videoWidgetView.pauseRendering(); |
| // If the video is playing when onPause() is called, the default behavior will be to pause |
| // the video and keep it paused when onResume() is called. |
| isPaused = true; |
| } |
| |
| @Override |
| protected void onResume() { |
| super.onResume(); |
| // Resume the 3D rendering. |
| videoWidgetView.resumeRendering(); |
| // Update the text to account for the paused video in onPause(). |
| updateStatusText(); |
| } |
| |
| @Override |
| protected void onDestroy() { |
| // Destroy the widget and free memory. |
| videoWidgetView.shutdown(); |
| super.onDestroy(); |
| } |
| |
| private void togglePause() { |
| if (isPaused) { |
| videoWidgetView.playVideo(); |
| } else { |
| videoWidgetView.pauseVideo(); |
| } |
| isPaused = !isPaused; |
| updateStatusText(); |
| } |
| |
| private void updateStatusText() { |
| StringBuilder status = new StringBuilder(); |
| status.append(isPaused ? "Paused: " : "Playing: "); |
| status.append(String.format("%.2f", videoWidgetView.getCurrentPosition() / 1000f)); |
| status.append(" / "); |
| status.append(videoWidgetView.getDuration() / 1000f); |
| status.append(" seconds."); |
| statusText.setText(status.toString()); |
| } |
| |
| /** |
| * When the user manipulates the seek bar, update the video position. |
| */ |
| private class SeekBarListener implements SeekBar.OnSeekBarChangeListener { |
| @Override |
| public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { |
| if (fromUser) { |
| videoWidgetView.seekTo(progress); |
| updateStatusText(); |
| } // else this was from the ActivityEventHandler.onNewFrame()'s seekBar.setProgress update. |
| } |
| |
| @Override |
| public void onStartTrackingTouch(SeekBar seekBar) { } |
| |
| @Override |
| public void onStopTrackingTouch(SeekBar seekBar) { } |
| } |
| |
| /** |
| * Listen to the important events from widget. |
| */ |
| private class ActivityEventListener extends VrVideoEventListener { |
| /** |
| * Called by video widget on the UI thread when it's done loading the video. |
| */ |
| @Override |
| public void onLoadSuccess() { |
| Log.i(TAG, "Successfully loaded video " + videoWidgetView.getDuration()); |
| loadVideoStatus = LOAD_VIDEO_STATUS_SUCCESS; |
| seekBar.setMax((int) videoWidgetView.getDuration()); |
| updateStatusText(); |
| } |
| |
| /** |
| * Called by video widget on the UI thread on any asynchronous error. |
| */ |
| @Override |
| public void onLoadError(String errorMessage) { |
| // An error here is normally due to being unable to decode the video format. |
| loadVideoStatus = LOAD_VIDEO_STATUS_ERROR; |
| Toast.makeText( |
| SimpleVrVideoActivity.this, "Error loading video: " + errorMessage, Toast.LENGTH_LONG) |
| .show(); |
| Log.e(TAG, "Error loading video: " + errorMessage); |
| } |
| |
| @Override |
| public void onClick() { |
| togglePause(); |
| } |
| |
| /** |
| * Update the UI every frame. |
| */ |
| @Override |
| public void onNewFrame() { |
| updateStatusText(); |
| seekBar.setProgress((int) videoWidgetView.getCurrentPosition()); |
| } |
| |
| /** |
| * Make the video play in a loop. This method could also be used to move to the next video in |
| * a playlist. |
| */ |
| @Override |
| public void onCompletion() { |
| videoWidgetView.seekTo(0); |
| } |
| } |
| |
| /** |
| * Helper class to manage threading. |
| */ |
| class VideoLoaderTask extends AsyncTask<Pair<Uri, Options>, Void, Boolean> { |
| @Override |
| protected Boolean doInBackground(Pair<Uri, Options>... fileInformation) { |
| try { |
| if (fileInformation == null || fileInformation.length < 1 |
| || fileInformation[0] == null || fileInformation[0].first == null) { |
| // No intent was specified, so we default to playing the local stereo-over-under video. |
| Options options = new Options(); |
| options.inputType = Options.TYPE_STEREO_OVER_UNDER; |
| videoWidgetView.loadVideoFromAsset("congo.mp4", options); |
| } else { |
| videoWidgetView.loadVideo(fileInformation[0].first, fileInformation[0].second); |
| } |
| } catch (IOException e) { |
| // An error here is normally due to being unable to locate the file. |
| loadVideoStatus = LOAD_VIDEO_STATUS_ERROR; |
| // Since this is a background thread, we need to switch to the main thread to show a toast. |
| videoWidgetView.post(new Runnable() { |
| @Override |
| public void run() { |
| Toast |
| .makeText(SimpleVrVideoActivity.this, "Error opening file. ", Toast.LENGTH_LONG) |
| .show(); |
| } |
| }); |
| Log.e(TAG, "Could not open video: " + e); |
| } |
| |
| return true; |
| } |
| } |
| } |