diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 31de6e645..6028a6fc6 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -35,6 +35,7 @@ add_library( # Sets the name of the library. src/main/cpp/JNIUtil.cpp src/main/cpp/SplashAnimation.cpp src/main/cpp/VRBrowser.cpp + src/main/cpp/VRVideo.cpp src/main/cpp/Widget.cpp src/main/cpp/WidgetPlacement.cpp src/main/cpp/WidgetResizer.cpp diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java index a69afb722..0aab56079 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java @@ -48,6 +48,7 @@ import org.mozilla.vrbrowser.ui.widgets.RootWidget; import org.mozilla.vrbrowser.ui.widgets.TopBarWidget; import org.mozilla.vrbrowser.ui.widgets.TrayWidget; +import org.mozilla.vrbrowser.ui.widgets.VideoProjectionMenuWidget; import org.mozilla.vrbrowser.ui.widgets.Widget; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; @@ -806,6 +807,11 @@ public void setTrayVisible(boolean visible) { } } + @Override + public void setControllersVisible(final boolean aVisible) { + queueRunnable(() -> setControllersVisibleNative(aVisible)); + } + @Override public void setBrowserSize(float targetWidth, float targetHeight) { mBrowserWidget.setBrowserSize(targetWidth, targetHeight, 1.0f); @@ -849,6 +855,21 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in } } + @Override + public void showVRVideo(final int aWindowHandle, final @VideoProjectionMenuWidget.VideoProjectionFlags int aVideoProjection) { + queueRunnable(() -> showVRVideoNative(aWindowHandle, aVideoProjection)); + } + + @Override + public void hideVRVideo() { + queueRunnable(this::hideVRVideoNative); + } + + @Override + public void resetUIYaw() { + queueRunnable(this::resetUIYawNative); + } + private native void addWidgetNative(int aHandle, WidgetPlacement aPlacement); private native void updateWidgetNative(int aHandle, WidgetPlacement aPlacement); private native void removeWidgetNative(int aHandle); @@ -860,4 +881,8 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in private native void workaroundGeckoSigAction(); private native void updateEnvironmentNative(); private native void updatePointerColorNative(); + private native void showVRVideoNative(int aWindowHandler, int aVideoProjection); + private native void hideVRVideoNative(); + private native void resetUIYawNative(); + private native void setControllersVisibleNative(boolean aVisible); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/Media.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/Media.java new file mode 100644 index 000000000..5f7059b04 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/Media.java @@ -0,0 +1,198 @@ +package org.mozilla.vrbrowser.browser; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import org.mozilla.geckoview.MediaElement; + +public class Media implements MediaElement.Delegate { + private static final String LOGTAG = "VRB"; + private boolean mIsFullscreen = false; + private double mCurrentTime = 0.0f; + private MediaElement.Metadata mMetaData; + private double mPlaybackRate = 1.0f; + private int mReadyState = MediaElement.MEDIA_READY_STATE_HAVE_NOTHING; + private boolean mPlaying = false; + private boolean mEnded = false; + private double mVolume = 1.0f; + private boolean mIsMuted = false; + private boolean mIsUnloaded = false; + private org.mozilla.geckoview.MediaElement mMedia; + private MediaElement.Delegate mDelegate; + + public Media(@NonNull MediaElement aMediaElement) { + mMedia = aMediaElement; + aMediaElement.setDelegate(this); + } + + public void setDelegate(@Nullable MediaElement.Delegate aDelegate) { + mDelegate = aDelegate; + } + + public double getDuration() { + if (mMetaData != null) { + return mMetaData.duration; + } + return -1.0f; + } + + public boolean isFullscreen() { + return mIsFullscreen; + } + + public double getCurrentTime() { + return mCurrentTime; + } + + public MediaElement.Metadata getMetaData() { + return mMetaData; + } + + public double getPlaybackRate() { + return mPlaybackRate; + } + + public int getReadyState() { + return mReadyState; + } + + public boolean isPlaying() { + return mPlaying; + } + + public boolean isEnded() { + return mEnded; + } + + public double getVolume() { + return mVolume; + } + + public boolean isMuted() { + return mIsMuted; + } + + public boolean isUnloaded() { + return mIsUnloaded; + } + + public MediaElement getMediaElement() { + return mMedia; + } + + public void seek(double aTime) { + mMedia.seek(aTime); + } + + public void play() { + mMedia.play(); + } + + public void pause() { + mMedia.pause(); + } + + public void setVolume(double aVolume) { + mMedia.setVolume(aVolume); + } + + public void setMuted(boolean aIsMuted) { + mMedia.setMuted(aIsMuted); + } + + public void unload() { + mIsUnloaded = true; + mDelegate = null; + } + + public int getWidth() { + return mMetaData != null ? (int)mMetaData.width : 0; + } + + public int getHeight() { + return mMetaData != null ? (int)mMetaData.height : 0; + } + + // Media Element delegate + @Override + public void onPlaybackStateChange(MediaElement mediaElement, int playbackState) { + if (playbackState == MediaElement.MEDIA_STATE_PLAY) { + mPlaying = true; + } else if (playbackState == MediaElement.MEDIA_STATE_PAUSE) { + mPlaying = false; + } else if (playbackState == MediaElement.MEDIA_STATE_ENDED) { + mEnded = true; + } + if (mDelegate != null) { + mDelegate.onPlaybackStateChange(mediaElement, playbackState); + } + } + + @Override + public void onReadyStateChange(MediaElement mediaElement, int readyState) { + mReadyState = readyState; + if (mDelegate != null) { + mDelegate.onReadyStateChange(mediaElement, readyState); + } + } + + @Override + public void onMetadataChange(MediaElement mediaElement, MediaElement.Metadata metaData) { + mMetaData = metaData; + if (mDelegate != null) { + mDelegate.onMetadataChange(mediaElement, metaData); + } + } + + @Override + public void onLoadProgress(MediaElement mediaElement, MediaElement.LoadProgressInfo progressInfo) { + if (mDelegate != null) { + mDelegate.onLoadProgress(mediaElement, progressInfo); + } + } + + @Override + public void onVolumeChange(MediaElement mediaElement, double volume, boolean muted) { + mVolume = volume; + mIsMuted = muted; + if (mDelegate != null) { + mDelegate.onVolumeChange(mediaElement, volume, muted); + } + } + + @Override + public void onTimeChange(MediaElement mediaElement, double time) { + mCurrentTime = time; + double duration = getDuration(); + if (duration <= 0 || mCurrentTime < getDuration()) { + mEnded = false; + } + if (mDelegate != null) { + mDelegate.onTimeChange(mediaElement, time); + } + } + + @Override + public void onPlaybackRateChange(MediaElement mediaElement, double rate) { + mPlaybackRate = rate; + if (mDelegate != null) { + mDelegate.onPlaybackRateChange(mediaElement, rate); + } + } + + @Override + public void onFullscreenChange(MediaElement mediaElement, boolean fullscreen) { + mIsFullscreen = fullscreen; + if (mDelegate != null) { + mDelegate.onFullscreenChange(mediaElement, fullscreen); + } + } + + @Override + public void onError(MediaElement mediaElement, int code) { + if (mDelegate != null) { + mDelegate.onError(mediaElement, code); + } + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionStore.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionStore.java index 70f24fe17..e2e2cb3c4 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionStore.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionStore.java @@ -20,6 +20,7 @@ import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoProfile; import org.mozilla.geckoview.AllowOrDeny; +import org.mozilla.geckoview.MediaElement; import org.mozilla.geckoview.GeckoResult; import org.mozilla.geckoview.GeckoRuntime; import org.mozilla.geckoview.GeckoRuntimeSettings; @@ -55,7 +56,7 @@ public class SessionStore implements GeckoSession.NavigationDelegate, GeckoSession.ProgressDelegate, GeckoSession.ContentDelegate, GeckoSession.TextInputDelegate, GeckoSession.TrackingProtectionDelegate, - GeckoSession.PromptDelegate, SharedPreferences.OnSharedPreferenceChangeListener { + GeckoSession.PromptDelegate, GeckoSession.MediaDelegate, SharedPreferences.OnSharedPreferenceChangeListener { private static SessionStore mInstance; private static final String LOGTAG = "VRB"; @@ -103,6 +104,7 @@ class State { boolean mFullScreen; GeckoSession mSession; SessionSettings mSettings; + ArrayList mMediaElements = new ArrayList<>(); } private GeckoRuntime mRuntime; @@ -328,6 +330,7 @@ int createSession(SessionSettings aSettings) { state.mSession.getTextInput().setDelegate(this); state.mSession.setPermissionDelegate(mPermissionDelegate); state.mSession.setTrackingProtectionDelegate(this); + state.mSession.setMediaDelegate(this); for (SessionChangeListener listener: mSessionChangeListeners) { listener.onNewSession(state.mSession, result); } @@ -345,6 +348,7 @@ public void removeSession(int aSessionId) { session.setPromptDelegate(null); session.setPermissionDelegate(null); session.setTrackingProtectionDelegate(null); + session.setMediaDelegate(null); mSessions.remove(aSessionId); for (SessionChangeListener listener: mSessionChangeListeners) { listener.onRemoveSession(session, aSessionId); @@ -499,6 +503,26 @@ public String getPreviousUri() { return result; } + public Media getFullScreenVideo() { + Media result = null; + if (mCurrentSession != null) { + State state = mSessions.get(mCurrentSession.hashCode()); + if (state == null) { + return result; + } + if (state.mMediaElements.size() > 0) { + return state.mMediaElements.get(state.mMediaElements.size() - 1); + } + for (Media media: state.mMediaElements) { + if (media.isFullscreen()) { + result = media; + break; + } + } + } + return result; + } + public boolean isInputActive(int aSessionId) { SessionStore.State state = mSessions.get(aSessionId); if (state != null) { @@ -1190,8 +1214,33 @@ public GeckoResult onPopupRequest(final GeckoSession session, final return GeckoResult.fromValue(AllowOrDeny.DENY); } - // SharedPreferences.OnSharedPreferenceChangeListener + // MediaDelegate + @Override + public void onMediaAdd(GeckoSession session, MediaElement element) { + SessionStore.State state = mSessions.get(getSessionId(session)); + if (state == null) { + return; + } + Media media = new Media(element); + state.mMediaElements.add(media); + } + @Override + public void onMediaRemove(GeckoSession session, MediaElement element) { + SessionStore.State state = mSessions.get(getSessionId(session)); + if (state == null) { + return; + } + for (int i = 0; i < state.mMediaElements.size(); ++i) { + Media media = state.mMediaElements.get(i); + if (media.getMediaElement() == element) { + media.unload(); + return; + } + } + } + + // SharedPreferences.OnSharedPreferenceChangeListener @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (mContext != null) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/MediaSeekBar.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/MediaSeekBar.java new file mode 100644 index 000000000..8154a906b --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/MediaSeekBar.java @@ -0,0 +1,222 @@ +package org.mozilla.vrbrowser.ui.views; + +import android.content.Context; +import android.os.Handler; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import org.mozilla.vrbrowser.R; + +public class MediaSeekBar extends LinearLayout implements SeekBar.OnSeekBarChangeListener { + private SeekBar mSeekBar; + private TextView mLeftText; + private TextView mRightText; + private ImageView mLiveIcon; + private double mDuration; + private double mCurrentTime; + private double mBuffered; + private boolean mTouching; + private boolean mSeekable = true; + private Delegate mDelegate; + private Handler mHandler; + + public MediaSeekBar(Context context) { + super(context); + initialize(); + } + + public MediaSeekBar(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initialize(); + } + + public MediaSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(); + } + + public MediaSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initialize(); + } + + public interface Delegate { + void onSeekDragStart(); + void onSeek(double aTargetTime); + void onSeekDragEnd(); + void onSeekHoverStart(); + void onSeekHoverEnd(); + void onSeekPreview(String aText, double aRatio); + } + + private void initialize() { + inflate(getContext(), R.layout.media_controls_seek_bar, this); + mHandler = new Handler(); + mSeekBar = findViewById(R.id.mediaSeekBar); + mLeftText = findViewById(R.id.mediaSeekLeftLabel); + mRightText = findViewById(R.id.mediaSeekRightLabel); + mLiveIcon = findViewById(R.id.mediaIconLive); + mLeftText.setText("0:00"); + mRightText.setText("0:00"); + mSeekBar.setProgress(0); + mSeekBar.setOnSeekBarChangeListener(this); + mSeekBar.setEnabled(false); + mSeekBar.setOnHoverListener((view, event) -> { + if (mDelegate == null || mDuration <= 0) { + return false; + } + boolean notify = false; + if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) { + mDelegate.onSeekHoverStart(); + notify = true; + } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) { + mDelegate.onSeekHoverEnd(); + } else if (event.getAction() == MotionEvent.ACTION_HOVER_MOVE) { + notify = true; + } + + if (notify) { + final float ratio = event.getX() / view.getWidth(); + notifySeekPreview(mDuration * ratio); + } + + return false; + }); + } + + private String formatTime(double aSeconds) { + final int total = (int)Math.floor(aSeconds); + + final int seconds = total % 60; + final int minutes = (total / 60) % 60; + final int hours = total / 3600; + if (mDuration >= 3600 | hours > 0) { + return String.format("%d:%02d:%02d", hours, minutes, seconds); + } else { + return String.format("%d:%02d", minutes, seconds); + } + } + + public void setDelegate(Delegate aDelegate) { + mDelegate = aDelegate; + } + + public void setCurrentTime(double aTime) { + mCurrentTime = aTime; + if (mSeekable) { + mLeftText.setText(formatTime(aTime)); + updateProgress(); + } + } + + public void setDuration(double aDuration) { + mDuration = aDuration; + mRightText.setText(formatTime(aDuration)); + if (mDuration > 0 && mSeekable) { + updateProgress(); + updateBufferedProgress(); + mSeekBar.setEnabled(true); + } + } + + public void setSeekable(boolean aSeekable) { + if (mSeekable == aSeekable) { + return; + } + mSeekable = aSeekable; + if (mSeekable) { + mLeftText.setText(formatTime(mCurrentTime)); + if (mDuration > 0) { + updateProgress(); + updateBufferedProgress(); + } + } else { + mLeftText.setText(R.string.video_controls_live); + mSeekBar.setProgress(mSeekBar.getMax()); + } + + mRightText.setVisibility(mSeekable ? View.VISIBLE : View.GONE); + mSeekBar.getThumb().mutate().setAlpha(mSeekable ? 255 : 0); + mSeekBar.setEnabled(mSeekable && mDuration > 0); + mLiveIcon.setVisibility(aSeekable ? View.GONE : View.VISIBLE); + } + + public void setBuffered(double aBuffered) { + mBuffered = aBuffered; + if (mSeekable) { + updateBufferedProgress(); + } + } + + public View getSeekBarView() { + return mSeekBar; + } + + private void updateProgress() { + if (mTouching || mDuration <= 0) { + return; + } + double t = mCurrentTime / mDuration; + mSeekBar.setProgress((int)(t * mSeekBar.getMax())); + } + + private void updateBufferedProgress() { + if (mDuration <= 0) { + mSeekBar.setSecondaryProgress(0); + return; + } + double t = mBuffered / mDuration; + mSeekBar.setSecondaryProgress((int)(t * mSeekBar.getMax())); + } + + private void notifySeekProgress() { + if (mDelegate != null) { + mDelegate.onSeek(getTargetTime()); + } + } + + private void notifySeekPreview(double aTargetTime) { + if (mDelegate != null) { + mDelegate.onSeekPreview(formatTime(aTargetTime), aTargetTime / mDuration); + } + } + + private double getTargetTime() { + return mDuration * (double) mSeekBar.getProgress() / (double) mSeekBar.getMax(); + } + + // SeekBar.OnSeekBarChangeListener + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser && mDelegate != null) { + mHandler.removeCallbacksAndMessages(null); + mHandler.postDelayed(this::notifySeekProgress, 250); + notifySeekPreview(getTargetTime()); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mTouching = true; + if (mDelegate != null) { + mDelegate.onSeekDragStart(); + notifySeekPreview(getTargetTime()); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mTouching = false; + if (mDelegate != null) { + mHandler.removeCallbacksAndMessages(null); + notifySeekProgress(); + mDelegate.onSeekDragEnd(); + } + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VolumeControl.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VolumeControl.java new file mode 100644 index 000000000..208c45b4b --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VolumeControl.java @@ -0,0 +1,97 @@ +package org.mozilla.vrbrowser.ui.views; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import org.mozilla.vrbrowser.R; + +public class VolumeControl extends FrameLayout implements SeekBar.OnSeekBarChangeListener { + private SeekBar mSeekBar; + private double mVolume; + private boolean mMuted; + private boolean mTouching; + private Delegate mDelegate; + + public VolumeControl(Context context) { + super(context); + initialize(); + } + + public VolumeControl(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initialize(); + } + + public VolumeControl(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(); + } + + public VolumeControl(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initialize(); + } + + public interface Delegate { + void onVolumeChange(double aVolume); + } + + private void initialize() { + inflate(getContext(), R.layout.volume_control, this); + mSeekBar = findViewById(R.id.volumeSeekBar); + mSeekBar.setProgress(100); + mSeekBar.setOnSeekBarChangeListener(this); + } + + public void setDelegate(Delegate aDelegate) { + mDelegate = aDelegate; + } + + + public void setVolume(double aVolume) { + mVolume = aVolume; + if (!mTouching) { + updateProgress(); + } + } + + public void setMuted(boolean aMuted) { + if (mMuted == aMuted) { + return; + } + mMuted = aMuted; + if (mMuted && !mTouching) { + mSeekBar.setProgress(0); + } + else if (!mMuted) { + updateProgress(); + } + } + + private void updateProgress() { + mSeekBar.setProgress((int) (mVolume * mSeekBar.getMax())); + } + + // SeekBar.OnSeekBarChangeListener + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser && mDelegate != null) { + mDelegate.onVolumeChange((double) progress / (double) seekBar.getMax()); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mTouching = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mTouching = false; + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrightnessMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrightnessMenuWidget.java new file mode 100644 index 000000000..865b8d830 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrightnessMenuWidget.java @@ -0,0 +1,68 @@ +package org.mozilla.vrbrowser.ui.widgets; + +import android.content.Context; + +import org.mozilla.vrbrowser.R; + +import java.util.ArrayList; + +public class BrightnessMenuWidget extends MenuWidget { + + ArrayList mItems; + public BrightnessMenuWidget(Context aContext) { + super(aContext); + createMenuItems(); + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + aPlacement.visible = false; + aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.brightness_menu_width); + aPlacement.parentAnchorX = 0.75f; + aPlacement.parentAnchorY = 1.0f; + aPlacement.anchorX = 0.0f; + aPlacement.anchorY = 0.0f; + aPlacement.translationY = WidgetPlacement.dpDimension(getContext(), R.dimen.video_projection_menu_translation_y); + aPlacement.translationZ = 1.0f; + } + + public void setParentWidget(UIWidget aParent) { + mWidgetPlacement.parentHandle = aParent.getHandle(); + } + + private void createMenuItems() { + mItems = new ArrayList<>(); + + final Runnable action = new Runnable() { + @Override + public void run() { + handleClick(); + } + }; + + mItems.add(new MenuItem(R.string.brightness_mode_normal, 0, action)); + mItems.add(new MenuItem(R.string.brightness_mode_dark, 0, action)); + mItems.add(new MenuItem(R.string.brightness_mode_void, 0, action)); + + + super.updateMenuItems(mItems); + super.setSelectedItem(1); + + mWidgetPlacement.height = mItems.size() * WidgetPlacement.dpDimension(getContext(), R.dimen.menu_item_height); + } + + public float getSelectedBrightness() { + switch (super.getSelectedItem()) { + case 0: return 1.0f; + case 1: return 0.5f; + case 2: return 0.0f; + } + return 1.0f; + } + + private void handleClick() { + mWidgetManager.setWorldBrightness(this, getSelectedBrightness()); + mWidgetPlacement.visible = false; + mWidgetManager.updateWidget(this); + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrowserWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrowserWidget.java index 9cbc48306..bdb2de66e 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrowserWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/BrowserWidget.java @@ -41,6 +41,8 @@ public class BrowserWidget extends View implements Widget, SessionStore.SessionC private WidgetPlacement mWidgetPlacement; private WidgetManagerDelegate mWidgetManager; private ChoicePromptWidget mChoicePrompt; + private int mWidthBackup; + private int mHeightBackup; public BrowserWidget(Context aContext, int aSessionId) { super(aContext); @@ -63,7 +65,6 @@ public BrowserWidget(Context aContext, int aSessionId) { private void initializeWidgetPlacement(WidgetPlacement aPlacement) { Context context = getContext(); - aPlacement.worldWidth = WidgetPlacement.floatDimension(context, R.dimen.browser_world_width); aPlacement.width = SettingsStore.getInstance(getContext()).getWindowWidth(); aPlacement.height = SettingsStore.getInstance(getContext()).getWindowHeight(); aPlacement.density = 1.0f; @@ -93,6 +94,30 @@ public void resumeCompositor() { mDisplay.surfaceChanged(mSurface, mWidth, mHeight); } + public void enableVRVideoMode(int aVideoWidth, int aVideoHeight) { + mWidthBackup = mWidth; + mHeightBackup = mHeight; + if (aVideoWidth == mWidth && aVideoHeight == mHeight) { + return; + } + mWidgetPlacement.width = aVideoWidth; + mWidgetPlacement.height = aVideoHeight; + resizeSurfaceTexture(aVideoWidth, aVideoHeight); + Log.e(LOGTAG, "onMetadataChange resize browser " + aVideoWidth + " " + aVideoHeight); + } + + public void disableVRVideoMode() { + if (mWidthBackup == 0 || mHeightBackup == 0) { + return; + } + if (mWidthBackup == mWidth && mHeightBackup == mHeight) { + return; + } + mWidgetPlacement.width = mWidthBackup; + mWidgetPlacement.height = mHeightBackup; + resizeSurfaceTexture(mWidthBackup, mWidthBackup); + } + public void setBrowserSize(float windowWidth, float windowHeight, float multiplier) { float worldWidth = WidgetPlacement.floatDimension(getContext(), R.dimen.browser_world_width); float aspect = windowWidth / windowHeight; @@ -208,6 +233,23 @@ public boolean getFirstDraw() { return mWidgetPlacement.firstDraw; } + @Override + public boolean isVisible() { + return mWidgetPlacement.visible; + } + + @Override + public void setVisible(boolean aVisible) { + if (mWidgetPlacement.visible == aVisible) { + return; + } + mWidgetPlacement.visible = aVisible; + mWidgetManager.updateWidget(this); + if (!aVisible) { + clearFocus(); + } + } + // SessionStore.GeckoSessionChange @Override public void onNewSession(GeckoSession aSession, int aId) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MediaControlsWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MediaControlsWidget.java new file mode 100644 index 000000000..c191695ae --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MediaControlsWidget.java @@ -0,0 +1,331 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.vrbrowser.ui.widgets; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; + +import org.mozilla.vrbrowser.R; +import org.mozilla.geckoview.MediaElement; +import org.mozilla.vrbrowser.browser.Media; +import org.mozilla.vrbrowser.ui.views.MediaSeekBar; +import org.mozilla.vrbrowser.ui.views.UIButton; +import org.mozilla.vrbrowser.ui.views.VolumeControl; + +public class MediaControlsWidget extends UIWidget implements MediaElement.Delegate { + + private static final String LOGTAG = "VRB"; + private Media mMedia; + private MediaSeekBar mSeekBar; + private VolumeControl mVolumeControl; + private UIButton mMediaPlayButton; + private UIButton mMediaSeekBackButton; + private UIButton mMediaSeekForwardButton; + private UIButton mMediaProjectionButton; + private UIButton mMediaVolumeButton; + private UIButton mMediaBackButton; + private TextView mMediaSeekLabel; + private Drawable mPlayIcon; + private Drawable mPauseIcon; + private Drawable mVolumeIcon; + private Drawable mMutedIcon; + private Runnable mBackHandler; + private boolean mPlayOnSeekEnd; + private Rect mOffsetViewBounds; + private VideoProjectionMenuWidget mProjectionMenu; + + public MediaControlsWidget(Context aContext) { + super(aContext); + initialize(aContext); + } + + public MediaControlsWidget(Context aContext, AttributeSet aAttrs) { + super(aContext, aAttrs); + initialize(aContext); + } + + public MediaControlsWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { + super(aContext, aAttrs, aDefStyle); + initialize(aContext); + } + + private void initialize(Context aContext) { + inflate(aContext, R.layout.media_controls, this); + + mSeekBar = findViewById(R.id.mediaControlSeekBar); + mVolumeControl = findViewById(R.id.volumeControl); + mMediaPlayButton = findViewById(R.id.mediaPlayButton); + mMediaSeekBackButton = findViewById(R.id.mediaSeekBackwardButton); + mMediaSeekForwardButton = findViewById(R.id.mediaSeekForwardButton); + mMediaProjectionButton = findViewById(R.id.mediaProjectionButton); + mMediaVolumeButton = findViewById(R.id.mediaVolumeButton); + mMediaBackButton = findViewById(R.id.mediaBackButton); + mMediaSeekLabel = findViewById(R.id.mediaControlSeekLabel); + mPlayIcon = aContext.getDrawable(R.drawable.ic_icon_media_play); + mPauseIcon = aContext.getDrawable(R.drawable.ic_icon_media_pause); + mMutedIcon = aContext.getDrawable(R.drawable.ic_icon_meda_volume_muted); + mVolumeIcon = aContext.getDrawable(R.drawable.ic_icon_media_volume); + mOffsetViewBounds = new Rect(); + + mMediaPlayButton.setOnClickListener(v -> { + if (mMedia.isEnded()) { + mMedia.seek(0); + mMedia.play(); + } else if (mMedia.isPlaying()) { + mMedia.pause(); + } else { + mMedia.play(); + } + }); + + mMediaSeekBackButton.setOnClickListener(v -> { + mMedia.seek(Math.max(0, mMedia.getCurrentTime() - 10.0f)); + + }); + + mMediaSeekForwardButton.setOnClickListener(v -> { + double t = mMedia.getCurrentTime() + 30; + if (mMedia.getDuration() > 0) { + t = Math.min(mMedia.getDuration(), t); + } + mMedia.seek(t); + }); + + mMediaProjectionButton.setOnClickListener(v -> { + WidgetPlacement placement = mProjectionMenu.getPlacement(); + placement.parentHandle = this.getHandle(); + placement.worldWidth = 0.5f; + placement.parentAnchorX = 0.65f; + placement.parentAnchorY = 0.4f; + mProjectionMenu.getPlacement().visible = !mProjectionMenu.getPlacement().visible; + mWidgetManager.updateWidget(mProjectionMenu); + }); + + mMediaVolumeButton.setOnClickListener(v -> { + if (mMedia.isMuted()) { + mMedia.setMuted(false); + } else { + mMedia.setMuted(true); + mVolumeControl.setVolume(0); + } + }); + + mMediaBackButton.setOnClickListener(v -> { + if (mBackHandler != null) { + mBackHandler.run(); + } + + }); + + mSeekBar.setDelegate(new MediaSeekBar.Delegate() { + @Override + public void onSeekDragStart() { + mPlayOnSeekEnd = mMedia.isPlaying(); + mMediaSeekLabel.setVisibility(View.VISIBLE); + mMedia.pause(); + } + + @Override + public void onSeek(double aTargetTime) { + mMedia.seek(aTargetTime); + } + + @Override + public void onSeekDragEnd() { + if (mPlayOnSeekEnd) { + mMedia.play(); + } + mMediaSeekLabel.setVisibility(View.GONE); + } + + @Override + public void onSeekHoverStart() { + mMediaSeekLabel.setVisibility(View.VISIBLE); + } + + @Override + public void onSeekHoverEnd() { + mMediaSeekLabel.setVisibility(View.GONE); + } + + @Override + public void onSeekPreview(String aText, double aRatio) { + mMediaSeekLabel.setText(aText); + View childView = mSeekBar.getSeekBarView(); + childView.getDrawingRect(mOffsetViewBounds); + MediaControlsWidget.this.offsetDescendantRectToMyCoords(childView, mOffsetViewBounds); + + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams)mMediaSeekLabel.getLayoutParams(); + params.setMarginStart(mOffsetViewBounds.left + (int)(aRatio * mOffsetViewBounds.width()) - mMediaSeekLabel.getMeasuredWidth() / 2); + mMediaSeekLabel.setLayoutParams(params); + } + }); + + mVolumeControl.setDelegate(v -> { + mMedia.setVolume(v); + if (mMedia.isMuted()) { + mMedia.setMuted(false); + } + }); + + this.setOnHoverListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_HOVER_MOVE || event.getAction() == MotionEvent.ACTION_HOVER_ENTER) { + float threshold = (float)MediaControlsWidget.this.getMeasuredWidth() * 0.65f; + boolean isVisible = mVolumeControl.getVisibility() == View.VISIBLE; + boolean makeVisible = event.getX() >= threshold; + if (isVisible != makeVisible) { + mVolumeControl.setVisibility(makeVisible ? View.VISIBLE : View.GONE); + } + } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT && !mMediaVolumeButton.isHovered() && this.isInTouchMode()) { + mVolumeControl.setVisibility(View.INVISIBLE); + } + return false; + }); + + mMediaVolumeButton.setOnHoverListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER || event.getAction() == MotionEvent.ACTION_HOVER_MOVE) { + mVolumeControl.setVisibility(View.VISIBLE); + } + return false; + }); + + mMediaProjectionButton.setOnHoverListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER || event.getAction() == MotionEvent.ACTION_HOVER_MOVE) { + mVolumeControl.setVisibility(View.INVISIBLE); + } + return false; + }); + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + Context context = getContext(); + aPlacement.width = WidgetPlacement.dpDimension(context, R.dimen.media_controls_container_width); + aPlacement.height = WidgetPlacement.dpDimension(context, R.dimen.media_controls_container_height); + aPlacement.translationZ = WidgetPlacement.unitFromMeters(context, R.dimen.media_controls_world_z); + aPlacement.anchorX = 0.45f; + aPlacement.anchorY = 0.5f; + aPlacement.parentAnchorX = 0.5f; + aPlacement.parentAnchorY = 0.5f; + } + + public void setParentWidget(int aHandle) { + mWidgetPlacement.parentHandle = aHandle; + } + + public void setProjectionMenuWidget(VideoProjectionMenuWidget aWidget) { + mProjectionMenu = aWidget; + } + + public void setBackHandler(Runnable aRunnable) { + mBackHandler = aRunnable; + } + + @Override + public void releaseWidget() { + super.releaseWidget(); + } + + public void setMedia(Media aMedia) { + if (mMedia == aMedia) { + return; + } + if (mMedia != null) { + mMedia.setDelegate(null); + } + mMedia = aMedia; + if (mMedia == null) { + return; + } + onMetadataChange(mMedia.getMediaElement(), mMedia.getMetaData()); + onVolumeChange(mMedia.getMediaElement(), mMedia.getVolume(), mMedia.isMuted()); + onTimeChange(mMedia.getMediaElement(), mMedia.getCurrentTime()); + onVolumeChange(mMedia.getMediaElement(), mMedia.getVolume(), mMedia.isMuted()); + onReadyStateChange(mMedia.getMediaElement(), mMedia.getReadyState()); + if (mMedia.isPlaying()) { + onPlaybackStateChange(mMedia.getMediaElement(), MediaElement.MEDIA_STATE_PLAY); + } + + mMedia.setDelegate(this); + } + + // Media Element delegate + @Override + public void onPlaybackStateChange(MediaElement mediaElement, int playbackState) { + if (playbackState == MediaElement.MEDIA_STATE_PLAY) { + mMediaPlayButton.setImageDrawable(mPauseIcon); + } else if (playbackState == MediaElement.MEDIA_STATE_PAUSE) { + mMediaPlayButton.setImageDrawable(mPlayIcon); + } + } + + + @Override + public void onReadyStateChange(MediaElement mediaElement, int readyState) { + + } + + @Override + public void onMetadataChange(MediaElement mediaElement, MediaElement.Metadata metaData) { + if (metaData == null) { + return; + } + mSeekBar.setDuration(metaData.duration); + if (metaData.audioTrackCount == 0) { + mMediaVolumeButton.setImageDrawable(mMutedIcon); + mMediaVolumeButton.setEnabled(false); + } else { + mMediaVolumeButton.setEnabled(true); + } + mSeekBar.setSeekable(metaData.isSeekable); + } + + @Override + public void onLoadProgress(MediaElement mediaElement, MediaElement.LoadProgressInfo progressInfo) { + if (progressInfo.buffered != null) { + mSeekBar.setBuffered(progressInfo.buffered[progressInfo.buffered.length -1].end); + } + } + + @Override + public void onVolumeChange(MediaElement mediaElement, double volume, boolean muted) { + if (!mMediaVolumeButton.isEnabled()) { + return; + } + mMediaVolumeButton.setImageDrawable(muted ? mMutedIcon : mVolumeIcon); + mVolumeControl.setVolume(volume); + mVolumeControl.setMuted(muted); + } + + @Override + public void onTimeChange(MediaElement mediaElement, double time) { + mSeekBar.setCurrentTime(time); + } + + @Override + public void onPlaybackRateChange(MediaElement mediaElement, double rate) { + + } + + @Override + public void onFullscreenChange(MediaElement mediaElement, boolean fullscreen) { + if (!fullscreen) { + hide(); + } + } + + @Override + public void onError(MediaElement mediaElement, int code) { + + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MenuWidget.java new file mode 100644 index 000000000..8ff177ace --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/MenuWidget.java @@ -0,0 +1,172 @@ + +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.vrbrowser.ui.widgets; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import org.mozilla.vrbrowser.R; + +import java.util.ArrayList; + +public abstract class MenuWidget extends UIWidget { + protected MenuAdapter mAdapter; + protected ListView mListView; + + public MenuWidget(Context aContext) { + super(aContext); + initialize(aContext, null); + } + + public MenuWidget(Context aContext, ArrayList aItems) { + super(aContext); + initialize(aContext, aItems); + } + + private void initialize(Context aContext, ArrayList aItems) { + inflate(aContext, R.layout.menu, this); + mListView = findViewById(R.id.menuListView); + + + mAdapter = new MenuAdapter(aContext, aItems); + mListView.setAdapter(mAdapter); + mListView.setSoundEffectsEnabled(false); + + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, + long id) { + setSelectedItem(position); + MenuItem item = mAdapter.mItems.get(position); + if (item.mCallback != null) { + item.mCallback.run(); + } + } + }); + } + + public void updateMenuItems(ArrayList aItems) { + mAdapter.mItems = aItems; + mAdapter.notifyDataSetChanged(); + } + + public void setSelectedItem(int aPosition) { + mListView.setItemChecked(aPosition, true); + } + + public int getSelectedItem() { + return mListView.getCheckedItemPosition(); + } + + public class MenuItem { + int mStringId; + int mImageId; + Runnable mCallback; + + MenuItem(int aStringId, int aImage, Runnable aCallback) { + mStringId = aStringId; + mImageId = aImage; + mCallback = aCallback; + } + } + + class MenuAdapter extends BaseAdapter implements OnHoverListener { + private Context mContext; + private ArrayList mItems; + private LayoutInflater mInflater; + private Drawable firstItemDrawable; + private Drawable lastItemDrawable; + + MenuAdapter(Context aContext, ArrayList aItems) { + mContext = aContext; + mItems = aItems != null ? aItems : new ArrayList(); + mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + firstItemDrawable = aContext.getDrawable(R.drawable.menu_item_background_first); + lastItemDrawable = aContext.getDrawable(R.drawable.menu_item_background_last); + } + + + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public Object getItem(int position) { + return mItems.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + view = mInflater.inflate(R.layout.menu_item_image_text, parent, false); + view.setOnHoverListener(this); + } + view.setTag(R.string.position_tag, position); + if (position == 0) { + view.setBackground(firstItemDrawable); + } else if (position == mItems.size() - 1) { + view.setBackground(lastItemDrawable); + } + + MenuItem item = mItems.get(position); + + TextView textView = view.findViewById(R.id.listItemText); + ImageView imageView = view.findViewById(R.id.listItemImage); + + textView.setText(mContext.getString(item.mStringId)); + if (item.mImageId > 0) { + imageView.setImageResource(item.mImageId); + } else { + imageView.setVisibility(View.GONE); + textView.setTextAlignment(TEXT_ALIGNMENT_CENTER); + } + + return view; + } + + @Override + public boolean onHover(View view, MotionEvent event) { + int position = (int)view.getTag(R.string.position_tag); + if (!isEnabled(position)) + return false; + + TextView label = view.findViewById(R.id.listItemText); + ImageView image = view.findViewById(R.id.listItemImage); + switch (event.getActionMasked()) { + case MotionEvent.ACTION_HOVER_ENTER: + view.setHovered(true); + label.setHovered(true); + image.setHovered(true); + return true; + + case MotionEvent.ACTION_HOVER_EXIT: + view.setHovered(false); + label.setHovered(false); + image.setHovered(false); + return true; + } + + return false; + } + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java index 6488fce78..0c8ddc600 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java @@ -15,7 +15,6 @@ import android.util.Log; import android.view.View; import android.view.ViewGroup; -import android.widget.LinearLayout; import org.mozilla.geckoview.AllowOrDeny; import org.mozilla.geckoview.GeckoResult; @@ -24,6 +23,7 @@ import org.mozilla.geckoview.WebRequestError; import org.mozilla.vrbrowser.*; import org.mozilla.vrbrowser.audio.AudioEngine; +import org.mozilla.vrbrowser.browser.Media; import org.mozilla.vrbrowser.browser.SessionStore; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.search.SearchEngineWrapper; @@ -38,7 +38,7 @@ import java.util.Arrays; public class NavigationBarWidget extends UIWidget implements GeckoSession.NavigationDelegate, - GeckoSession.ProgressDelegate, GeckoSession.ContentDelegate, + GeckoSession.ProgressDelegate, GeckoSession.ContentDelegate, WidgetManagerDelegate.WorldClickListener, WidgetManagerDelegate.UpdateListener, SessionStore.SessionChangeListener, NavigationURLBar.NavigationURLBarDelegate, VoiceSearchWidget.VoiceSearchDelegate, SharedPreferences.OnSharedPreferenceChangeListener, SuggestionsWidget.URLBarPopupDelegate { @@ -53,26 +53,36 @@ public class NavigationBarWidget extends UIWidget implements GeckoSession.Naviga private UIButton mServoButton; private NavigationURLBar mURLBar; private ViewGroup mNavigationContainer; - private ViewGroup mFocusModeContainer; + private ViewGroup mFullScreenModeContainer; private ViewGroup mResizeModeContainer; private BrowserWidget mBrowserWidget; private boolean mIsLoading; - private boolean mIsInFocusMode; + private boolean mIsInFullScreenMode; private boolean mIsResizing; + private boolean mIsInVRVideo; private Runnable mResizeBackHandler; + private Runnable mFullScreenBackHandler; + private Runnable mVRVideoBackHandler; private UIButton mResizeEnterButton; private UIButton mResizeExitButton; + private UIButton mFullScreenExitButton; + private UIButton mBrightnessButton; + private UIButton mFullScreenResizeButton; + private UIButton mProjectionButton; private UITextButton mPreset0; private UITextButton mPreset1; private UITextButton mPreset2; private UITextButton mPreset3; private ArrayList mButtons; - private int mURLBarLayoutIndex; private VoiceSearchWidget mVoiceSearchWidget; private Context mAppContext; private SharedPreferences mPrefs; private SuggestionsWidget mPopup; private SearchEngineWrapper mSearchEngineWrapper; + private VideoProjectionMenuWidget mProjectionMenu; + private WidgetPlacement mProjectionMenuPlacement; + private BrightnessMenuWidget mBrigthnessWidget; + private MediaControlsWidget mMediaControlsWidget; public NavigationBarWidget(Context aContext) { super(aContext); @@ -100,15 +110,18 @@ private void initialize(Context aContext) { mServoButton = findViewById(R.id.servoButton); mURLBar = findViewById(R.id.urlBar); mNavigationContainer = findViewById(R.id.navigationBarContainer); - mFocusModeContainer = findViewById(R.id.focusModeContainer); + mFullScreenModeContainer = findViewById(R.id.fullScreenModeContainer); mResizeModeContainer = findViewById(R.id.resizeModeContainer); + mFullScreenExitButton = findViewById(R.id.fullScreenExitButton); + mBrightnessButton = findViewById(R.id.brightnessButton); + mFullScreenResizeButton = findViewById(R.id.fullScreenResizeEnterButton); + mProjectionButton = findViewById(R.id.projectionButton); - mResizeBackHandler = new Runnable() { - @Override - public void run() { - exitResizeMode(true); - } - }; + + mResizeBackHandler = () -> exitResizeMode(true); + + mFullScreenBackHandler = this::exitFullScreenMode; + mVRVideoBackHandler = this::exitVRVideo; mBackButton.setOnClickListener(new OnClickListener() { @Override @@ -202,6 +215,62 @@ public void onClick(View view) { } }); + mFullScreenResizeButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + view.requestFocusFromTouch(); + enterResizeMode(); + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + } + }); + + mFullScreenExitButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + view.requestFocusFromTouch(); + exitFullScreenMode(); + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + } + }); + + mProjectionButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + view.requestFocusFromTouch(); + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + + boolean wasVisible = mProjectionMenu.isVisible(); + closeFloatingMenus(); + if (!wasVisible) { + mProjectionMenu.setVisible(true); + } + } + }); + + mBrightnessButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + view.requestFocusFromTouch(); + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + boolean wasVisible = mBrigthnessWidget.isVisible(); + closeFloatingMenus(); + if (!wasVisible) { + float anchor = 0.5f + (float)mBrightnessButton.getMeasuredWidth() / (float)NavigationBarWidget.this.getMeasuredWidth(); + mBrigthnessWidget.getPlacement().parentAnchorX = anchor; + mBrigthnessWidget.setVisible(true); + } + } + }); + + mPreset0.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { @@ -257,6 +326,7 @@ public void onClick(View view) { SessionStore.get().addProgressListener(this); SessionStore.get().addContentListener(this); mWidgetManager.addUpdateListener(this); + mWidgetManager.addWorldClickListener(this); mVoiceSearchWidget = createChild(VoiceSearchWidget.class, false); mVoiceSearchWidget.setDelegate(this); @@ -276,6 +346,7 @@ public void onClick(View view) { @Override public void releaseWidget() { mWidgetManager.removeUpdateListener(this); + mWidgetManager.removeWorldClickListener(this); mPrefs.unregisterOnSharedPreferenceChangeListener(this); SessionStore.get().removeNavigationListener(this); SessionStore.get().removeProgressListener(this); @@ -305,58 +376,63 @@ public void setBrowserWidget(BrowserWidget aWidget) { mBrowserWidget = aWidget; } - private void enterFocusMode() { - if (mIsInFocusMode) { + private void enterFullScreenMode() { + if (mIsInFullScreenMode) { return; } - mIsInFocusMode = true; - AnimationHelper.fadeIn(mFocusModeContainer, AnimationHelper.FADE_ANIMATION_DURATION, new Runnable() { - @Override - public void run() { - // Set up required to show the URLBar while in focus mode - mURLBarLayoutIndex = mNavigationContainer.indexOfChild(mURLBar); - mNavigationContainer.removeView(mURLBar); - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mURLBar.getLayoutParams(); - params.width = (int)(WidgetPlacement.pixelDimension(getContext(), R.dimen.browser_width_pixels) * 0.8); - params.weight = 1; - mURLBar.setLayoutParams(params); - mFocusModeContainer.addView(mURLBar, 0); - mURLBar.setVisibility(View.INVISIBLE); - mURLBar.setClickable(false); - } - }); + mWidgetManager.pushBackHandler(mFullScreenBackHandler); + mIsInFullScreenMode = true; + AnimationHelper.fadeIn(mFullScreenModeContainer, AnimationHelper.FADE_ANIMATION_DURATION, null); + AnimationHelper.fadeOut(mNavigationContainer, 0, null); mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); + mWidgetManager.setTrayVisible(false); + + if (mProjectionMenu == null) { + mProjectionMenu = new VideoProjectionMenuWidget(getContext()); + mProjectionMenu.setParentWidget(this); + mProjectionMenuPlacement = new WidgetPlacement(getContext()); + mWidgetManager.addWidget(mProjectionMenu); + mProjectionMenu.setDelegate((projection )-> { + if (mIsInVRVideo) { + // Reproject while reproducing VRVideo + mWidgetManager.showVRVideo(mBrowserWidget.getHandle(), projection); + closeFloatingMenus(); + } else { + enterVRVideo(projection); + } + }); + } + if (mBrigthnessWidget == null) { + mBrigthnessWidget = new BrightnessMenuWidget(getContext()); + mBrigthnessWidget.setParentWidget(this); + mWidgetManager.addWidget(mBrigthnessWidget); + } + closeFloatingMenus(); + mWidgetManager.pushWorldBrightness(mBrigthnessWidget, mBrigthnessWidget.getSelectedBrightness()); } - private void exitFocusMode() { - if (!mIsInFocusMode) { + private void exitFullScreenMode() { + if (!mIsInFullScreenMode) { return; } - mIsInFocusMode = false; - - // Restore URL bar to normal mode - mFocusModeContainer.removeView(mURLBar); - mNavigationContainer.addView(mURLBar, mURLBarLayoutIndex); - mURLBar.setVisibility(View.VISIBLE); - mURLBar.setAlpha(1.0f); - LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mURLBar.getLayoutParams(); - params.width = LayoutParams.WRAP_CONTENT; - params.weight = 100; - mURLBar.setLayoutParams(params); - mURLBar.setClickable(true); + mIsInFullScreenMode = false; + mWidgetManager.popBackHandler(mFullScreenBackHandler); AnimationHelper.fadeIn(mNavigationContainer, AnimationHelper.FADE_ANIMATION_DURATION, null); - AnimationHelper.fadeOut(mFocusModeContainer, 0, null); + mWidgetManager.popWorldBrightness(this); + AnimationHelper.fadeOut(mFullScreenModeContainer, 0, null); if (SessionStore.get().isInFullScreen()) { SessionStore.get().exitFullScreen(); } mWidgetManager.setTrayVisible(true); + closeFloatingMenus(); + mWidgetManager.popWorldBrightness(mBrigthnessWidget); } private void enterResizeMode() { @@ -366,8 +442,13 @@ private void enterResizeMode() { mIsResizing = true; mWidgetManager.startWidgetResize(mBrowserWidget); AnimationHelper.fadeIn(mResizeModeContainer, AnimationHelper.FADE_ANIMATION_DURATION, null); - AnimationHelper.fadeOut(mNavigationContainer, 0, null); + if (mIsInFullScreenMode) { + AnimationHelper.fadeOut(mFullScreenModeContainer, 0, null); + } else { + AnimationHelper.fadeOut(mNavigationContainer, 0, null); + } mWidgetManager.pushBackHandler(mResizeBackHandler); + closeFloatingMenus(); } private void exitResizeMode(boolean aCommitChanges) { @@ -376,7 +457,11 @@ private void exitResizeMode(boolean aCommitChanges) { } mIsResizing = false; mWidgetManager.finishWidgetResize(mBrowserWidget); - AnimationHelper.fadeIn(mNavigationContainer, AnimationHelper.FADE_ANIMATION_DURATION, null); + if (mIsInFullScreenMode) { + AnimationHelper.fadeIn(mFullScreenModeContainer, AnimationHelper.FADE_ANIMATION_DURATION, null); + } else { + AnimationHelper.fadeIn(mNavigationContainer, AnimationHelper.FADE_ANIMATION_DURATION, null); + } AnimationHelper.fadeOut(mResizeModeContainer, 0, new Runnable() { @Override public void run() { @@ -384,6 +469,59 @@ public void run() { } }); mWidgetManager.popBackHandler(mResizeBackHandler); + closeFloatingMenus(); + } + + private void enterVRVideo(@VideoProjectionMenuWidget.VideoProjectionFlags int aProjection) { + if (mIsInVRVideo) { + return; + } + mIsInVRVideo = true; + mWidgetManager.pushBackHandler(mVRVideoBackHandler); + // Backup the placement because the same widget is reused in FullScreen & MediaControl menus + mProjectionMenuPlacement.copyFrom(mProjectionMenu.getPlacement()); + + Media fullscreenMedia = SessionStore.get().getFullScreenVideo(); + + this.setVisible(false); + if (fullscreenMedia != null && fullscreenMedia.getWidth() > 0 && fullscreenMedia.getHeight() > 0) { + mBrowserWidget.enableVRVideoMode(fullscreenMedia.getWidth(), fullscreenMedia.getHeight()); + } + mBrowserWidget.setVisible(false); + + closeFloatingMenus(); + if (mProjectionMenu.getSelectedProjection() != VideoProjectionMenuWidget.VIDEO_PROJECTION_3D_SIDE_BY_SIDE) { + mWidgetManager.setControllersVisible(false); + } + + if (mMediaControlsWidget == null) { + mMediaControlsWidget = new MediaControlsWidget(getContext()); + mMediaControlsWidget.setParentWidget(mBrowserWidget.getHandle()); + mMediaControlsWidget.getPlacement().visible = false; + mWidgetManager.addWidget(mMediaControlsWidget); + mMediaControlsWidget.setBackHandler(this::exitVRVideo); + } + mMediaControlsWidget.setProjectionMenuWidget(mProjectionMenu); + mMediaControlsWidget.setMedia(fullscreenMedia); + mWidgetManager.updateWidget(mMediaControlsWidget); + mWidgetManager.showVRVideo(mBrowserWidget.getHandle(), aProjection); + } + + private void exitVRVideo() { + if (!mIsInVRVideo) { + return; + } + mIsInVRVideo = false; + mWidgetManager.popBackHandler(mVRVideoBackHandler); + mWidgetManager.hideVRVideo(); + mProjectionMenu.getPlacement().copyFrom(mProjectionMenuPlacement); + closeFloatingMenus(); + mWidgetManager.setControllersVisible(true); + + this.setVisible(true); + mBrowserWidget.disableVRVideoMode(); + mBrowserWidget.setVisible(true); + mMediaControlsWidget.setVisible(false); } private void setResizePreset(float aResizeMode) { @@ -393,10 +531,6 @@ private void setResizePreset(float aResizeMode) { aResizeMode); } - public boolean isInFocusMode() { - return mIsInFocusMode; - } - public void showVoiceSearch() { mURLBar.showVoiceSearch(true); } @@ -409,6 +543,15 @@ public void updateServoButton() { } } + private void closeFloatingMenus() { + if (mProjectionMenu != null) { + mProjectionMenu.setVisible(false); + } + if (mBrigthnessWidget != null) { + mBrigthnessWidget.setVisible(false); + } + } + @Override public GeckoResult onNewSession(@NonNull GeckoSession aSession, @NonNull String aUri) { return null; @@ -500,7 +643,7 @@ public void onPageStart(GeckoSession aSession, String aUri) { if (mReloadButton != null) { mReloadButton.setImageResource(R.drawable.ic_icon_exit); } - if (mIsInFocusMode && !mIsResizing) { + if (mIsInFullScreenMode && !mIsResizing) { AnimationHelper.fadeIn(mURLBar, 0, null); } } @@ -512,7 +655,7 @@ public void onPageStop(GeckoSession aSession, boolean b) { if (mReloadButton != null) { mReloadButton.setImageResource(R.drawable.ic_icon_reload); } - if (mIsInFocusMode) { + if (mIsInFullScreenMode) { AnimationHelper.fadeOut(mURLBar, 0, null); } } @@ -550,15 +693,18 @@ public void onCloseRequest(GeckoSession session) { @Override public void onFullScreen(GeckoSession session, boolean aFullScreen) { if (aFullScreen) { - if (!mIsInFocusMode) { - enterFocusMode(); + if (!mIsInFullScreenMode) { + enterFullScreenMode(); } if (mIsResizing) { exitResizeMode(false); } } else { - exitFocusMode(); + if (mIsInVRVideo) { + exitVRVideo(); + } + exitFullScreenMode(); } } @@ -621,7 +767,7 @@ public void onCurrentSessionChange(GeckoSession aSession, int aId) { @Override public void OnVoiceSearchClicked() { - if (mVoiceSearchWidget.getPlacement().visible) { + if (mVoiceSearchWidget.isVisible()) { mVoiceSearchWidget.hide(); } else { @@ -725,8 +871,29 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin } } - // URLBarPopupWidgetDelegate + // WorldClickListener + @Override + public void onWorldClick() { + if (mIsInVRVideo && mMediaControlsWidget != null) { + mMediaControlsWidget.setVisible(!mMediaControlsWidget.isVisible()); + if (mProjectionMenu.getSelectedProjection() != VideoProjectionMenuWidget.VIDEO_PROJECTION_3D_SIDE_BY_SIDE) { + if (mMediaControlsWidget.isVisible()) { + // Reorient the MediaControl UI when the users clicks to show it. + // So you can look at any point of the 180/360 video and the UI always shows in front of you. + mWidgetManager.resetUIYaw(); + } + } + + if (mMediaControlsWidget.isVisible()) { + mWidgetManager.setControllersVisible(true); + } else if (mProjectionMenu.getSelectedProjection() != VideoProjectionMenuWidget.VIDEO_PROJECTION_3D_SIDE_BY_SIDE) { + mWidgetManager.setControllersVisible(false); + } + } + closeFloatingMenus(); + } + // URLBarPopupWidgetDelegate @Override public void OnItemClicked(SuggestionsWidget.SuggestionItem item) { mURLBar.handleURLEdit(item.url); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java index b2acb14fd..69d8cb2cc 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java @@ -249,6 +249,7 @@ public void hide() { clearFocus(); } + @Override public boolean isVisible() { for (UIWidget child : mChildren.values()) { if (child.isVisible()) @@ -258,6 +259,18 @@ public boolean isVisible() { return mWidgetPlacement.visible; } + @Override + public void setVisible(boolean aVisible) { + if (mWidgetPlacement.visible == aVisible) { + return; + } + mWidgetPlacement.visible = aVisible; + mWidgetManager.updateWidget(this); + if (!aVisible) { + clearFocus(); + } + } + protected T createChild(@NonNull Class aChildClassName) { return createChild(aChildClassName, true); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/VideoProjectionMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/VideoProjectionMenuWidget.java new file mode 100644 index 000000000..29f29e60f --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/VideoProjectionMenuWidget.java @@ -0,0 +1,119 @@ +package org.mozilla.vrbrowser.ui.widgets; + +import android.content.Context; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; + +import org.mozilla.vrbrowser.R; + +import java.util.ArrayList; + +public class VideoProjectionMenuWidget extends MenuWidget { + + @IntDef(value = { VIDEO_PROJECTION_3D_SIDE_BY_SIDE, VIDEO_PROJECTION_360, + VIDEO_PROJECTION_360_STEREO, VIDEO_PROJECTION_180, + VIDEO_PROJECTION_180_STEREO_LEFT_RIGTH, VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM }) + public @interface VideoProjectionFlags {} + + public static final int VIDEO_PROJECTION_3D_SIDE_BY_SIDE = 0; + public static final int VIDEO_PROJECTION_360 = 1; + public static final int VIDEO_PROJECTION_360_STEREO = 2; + public static final int VIDEO_PROJECTION_180 = 3; + public static final int VIDEO_PROJECTION_180_STEREO_LEFT_RIGTH = 4; + public static final int VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM = 5; + + public interface Delegate { + void onVideoProjectionClick(@VideoProjectionFlags int aProjection); + } + + ArrayList mItems; + Delegate mDelegate; + @VideoProjectionFlags int mSelectedProjection = VIDEO_PROJECTION_3D_SIDE_BY_SIDE; + + public VideoProjectionMenuWidget(Context aContext) { + super(aContext); + createMenuItems(); + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + aPlacement.visible = false; + aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.video_projection_menu_width); + aPlacement.parentAnchorX = 0.5f; + aPlacement.parentAnchorY = 1.0f; + aPlacement.anchorX = 0.0f; + aPlacement.anchorY = 0.0f; + aPlacement.translationY = WidgetPlacement.dpDimension(getContext(), R.dimen.video_projection_menu_translation_y); + aPlacement.translationZ = 1.0f; + } + + public void setParentWidget(UIWidget aParent) { + mWidgetPlacement.parentHandle = aParent.getHandle(); + } + + public void setDelegate(@Nullable Delegate aDelegate) { + mDelegate = aDelegate; + } + + private void createMenuItems() { + mItems = new ArrayList<>(); + + mItems.add(new MenuItem(R.string.video_mode_3d_side, R.drawable.ic_icon_videoplayback_3dsidebyside, new Runnable() { + @Override + public void run() { + handleClick(VIDEO_PROJECTION_3D_SIDE_BY_SIDE); + } + })); + + mItems.add(new MenuItem(R.string.video_mode_360, R.drawable.ic_icon_videoplayback_360, new Runnable() { + @Override + public void run() { + handleClick(VIDEO_PROJECTION_360); + } + })); + + mItems.add(new MenuItem(R.string.video_mode_360_stereo, R.drawable.ic_icon_videoplayback_360_stereo, new Runnable() { + @Override + public void run() { + handleClick(VIDEO_PROJECTION_360_STEREO); + } + })); + + mItems.add(new MenuItem(R.string.video_mode_180, R.drawable.ic_icon_videoplayback_180, new Runnable() { + @Override + public void run() { + handleClick(VIDEO_PROJECTION_180); + } + })); + + mItems.add(new MenuItem(R.string.video_mode_180_left_right, R.drawable.ic_icon_videoplayback_180_stereo_leftright, new Runnable() { + @Override + public void run() { + handleClick(VIDEO_PROJECTION_180_STEREO_LEFT_RIGTH); + } + })); + + mItems.add(new MenuItem(R.string.video_mode_180_top_bottom, R.drawable.ic_icon_videoplayback_180_stereo_topbottom, new Runnable() { + @Override + public void run() { + handleClick(VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM); + } + })); + + + super.updateMenuItems(mItems); + + mWidgetPlacement.height = mItems.size() * WidgetPlacement.dpDimension(getContext(), R.dimen.menu_item_height); + } + + private void handleClick(@VideoProjectionFlags int aVideoProjection) { + mSelectedProjection = aVideoProjection; + if (mDelegate != null) { + mDelegate.onVideoProjectionClick(aVideoProjection); + } + } + + public @VideoProjectionFlags int getSelectedProjection() { + return mSelectedProjection; + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Widget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Widget.java index a20b93ab9..858c31cdd 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Widget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Widget.java @@ -19,4 +19,6 @@ public interface Widget { void releaseWidget(); void setFirstDraw(boolean aIsFirstDraw); boolean getFirstDraw(); + boolean isVisible(); + void setVisible(boolean aVisible); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java index d66ec2c6e..adea29147 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java @@ -35,10 +35,14 @@ interface WorldClickListener { void setWorldBrightness(Object aKey, float aBrightness); void popWorldBrightness(Object aKey); void setTrayVisible(boolean visible); + void setControllersVisible(boolean visible); void setBrowserSize(float targetWidth, float targetHeight); void keyboardDismissed(); void updateEnvironment(); void updatePointerColor(); + void showVRVideo(int aWindowHandle, @VideoProjectionMenuWidget.VideoProjectionFlags int aVideoProjection); + void hideVRVideo(); + void resetUIYaw(); void addFocusChangeListener(@NonNull FocusChangeListener aListener); void removeFocusChangeListener(@NonNull FocusChangeListener aListener); void addPermissionListener(PermissionListener aListener); diff --git a/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp b/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp index 22e1f3695..8b887d576 100644 --- a/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp +++ b/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp @@ -418,6 +418,11 @@ DeviceDelegateGoogleVR::GetReorientTransform() const { return m.reorientMatrix; } +void +DeviceDelegateGoogleVR::SetReorientTransform(const vrb::Matrix& aMatrix) { + m.reorientMatrix = aMatrix; +} + void DeviceDelegateGoogleVR::SetClearColor(const vrb::Color& aColor) { m.clearColor = aColor; diff --git a/app/src/googlevr/cpp/DeviceDelegateGoogleVR.h b/app/src/googlevr/cpp/DeviceDelegateGoogleVR.h index ae3e2cafe..a5d32e819 100644 --- a/app/src/googlevr/cpp/DeviceDelegateGoogleVR.h +++ b/app/src/googlevr/cpp/DeviceDelegateGoogleVR.h @@ -27,6 +27,7 @@ class DeviceDelegateGoogleVR : public DeviceDelegate { vrb::CameraPtr GetCamera(const device::Eye aWhich) override; const vrb::Matrix& GetHeadTransform() const override; const vrb::Matrix& GetReorientTransform() const override; + void SetReorientTransform(const vrb::Matrix& aMatrix) override; void SetClearColor(const vrb::Color& aColor) override; void SetClipPlanes(const float aNear, const float aFar) override; void SetControllerDelegate(ControllerDelegatePtr& aController) override; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f4197a400..79a4b47a2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="org.mozilla.vrbrowser" android:installLocation="preferExternal"> diff --git a/app/src/main/cpp/BrowserWorld.cpp b/app/src/main/cpp/BrowserWorld.cpp index dac6abf96..04fcea745 100644 --- a/app/src/main/cpp/BrowserWorld.cpp +++ b/app/src/main/cpp/BrowserWorld.cpp @@ -17,6 +17,8 @@ #include "Widget.h" #include "WidgetPlacement.h" #include "VRBrowser.h" +#include "Quad.h" +#include "VRVideo.h" #include "vrb/CameraSimple.h" #include "vrb/Color.h" #include "vrb/ConcreteClass.h" @@ -43,7 +45,6 @@ #include "vrb/Transform.h" #include "vrb/VertexArray.h" #include "vrb/Vector.h" -#include "Quad.h" #include #include @@ -154,14 +155,14 @@ struct BrowserWorld::State { ExternalVRPtr externalVR; ExternalBlitterPtr blitter; bool windowsInitialized; - TransformPtr skybox; + TogglePtr skybox; FadeBlitterPtr fadeBlitter; uint32_t loaderDelay; bool exitImmersiveRequested; WidgetPtr resizingWidget; LoadingAnimationPtr loadingAnimation; SplashAnimationPtr splashAnimation; - int colorIndex; + VRVideoPtr vrVideo; State() : paused(true), glInitialized(false), modelsLoaded(false), env(nullptr), nearClip(0.1f), farClip(300.0f), activity(nullptr), windowsInitialized(false), exitImmersiveRequested(false), loaderDelay(0) { @@ -185,7 +186,6 @@ struct BrowserWorld::State { fadeBlitter = FadeBlitter::Create(create); loadingAnimation = LoadingAnimation::Create(create); splashAnimation = SplashAnimation::Create(create); - colorIndex = 0; } void CheckBackButton(); @@ -841,6 +841,50 @@ BrowserWorld::ExitImmersive() { m.exitImmersiveRequested = true; } +void +BrowserWorld::ShowVRVideo(const int aWindowHandle, const int aVideoProjection) { + WidgetPtr widget = m.GetWidget(aWindowHandle); + if (!widget) { + VRB_ERROR("Can't find Widget for VRVideo with handle: %d", aWindowHandle); + return; + } + + auto projection = static_cast(aVideoProjection); + m.vrVideo = VRVideo::Create(m.create, widget, projection); + if (m.skybox && projection != VRVideo::VRVideoProjection::VIDEO_PROJECTION_3D_SIDE_BY_SIDE) { + m.skybox->ToggleAll(false); + } + if (m.fadeBlitter && projection != VRVideo::VRVideoProjection::VIDEO_PROJECTION_3D_SIDE_BY_SIDE) { + m.fadeBlitter->SetVisible(false); + } +} + +void +BrowserWorld::HideVRVideo() { + m.vrVideo = nullptr; + if (m.skybox) { + m.skybox->ToggleAll(true); + } + if (m.fadeBlitter) { + m.fadeBlitter->SetVisible(true); + } +} + +void +BrowserWorld::SetControllersVisible(const bool aVisible) { + m.controllers->SetVisible(aVisible); +} + +void +BrowserWorld::ResetUIYaw() { + vrb::Matrix head = m.device->GetHeadTransform(); + vrb::Vector vector = head.MultiplyDirection(vrb::Vector(1.0f, 0.0f, 0.0f)); + const float yaw = atan2(vector.z(), vector.x()); + + vrb::Matrix matrix = vrb::Matrix::Rotation(vrb::Vector(0.0f, 1.0f, 0.0f), -yaw); + m.device->SetReorientTransform(matrix); +} + JNIEnv* BrowserWorld::GetJNIEnv() const { ASSERT_ON_RENDER_THREAD(nullptr); @@ -867,7 +911,8 @@ BrowserWorld::DrawWorld() { vrb::Vector headPosition = m.device->GetHeadTransform().GetTranslation(); vrb::Vector headDirection = m.device->GetHeadTransform().MultiplyDirection(vrb::Vector(0.0f, 0.0f, -1.0f)); if (m.skybox) { - m.skybox->SetTransform(vrb::Matrix::Translation(headPosition)); + vrb::TransformPtr t = std::dynamic_pointer_cast(m.skybox->GetNode(0)); + t->SetTransform(vrb::Matrix::Translation(headPosition)); } m.rootTransparent->SortNodes([=](const NodePtr& a, const NodePtr& b) { const float kMaxFloat = 9999999.0f; @@ -893,6 +938,12 @@ BrowserWorld::DrawWorld() { if (m.fadeBlitter && m.fadeBlitter->IsVisible()) { m.fadeBlitter->Draw(); } + if (m.vrVideo) { + m.vrVideo->SelectEye(device::Eye::Left); + m.drawList->Reset(); + m.vrVideo->GetRoot()->Cull(*m.cullVisitor, *m.drawList); + m.drawList->Draw(*m.leftCamera); + } m.drawList->Reset(); m.rootController->Cull(*m.cullVisitor, *m.drawList); m.drawList->Draw(*m.leftCamera); @@ -910,6 +961,12 @@ BrowserWorld::DrawWorld() { if (m.fadeBlitter && m.fadeBlitter->IsVisible()) { m.fadeBlitter->Draw(); } + if (m.vrVideo) { + m.vrVideo->SelectEye(device::Eye::Right); + m.drawList->Reset(); + m.vrVideo->GetRoot()->Cull(*m.cullVisitor, *m.drawList); + m.drawList->Draw(*m.leftCamera); + } m.drawList->Reset(); m.rootController->Cull(*m.cullVisitor, *m.drawList); m.drawList->Draw(*m.rightCamera); @@ -998,13 +1055,15 @@ BrowserWorld::DrawSplashAnimation() { } } -vrb::TransformPtr +vrb::TogglePtr BrowserWorld::CreateSkyBox(const std::string& basePath) { ASSERT_ON_RENDER_THREAD(nullptr); + vrb::TogglePtr toggle = vrb::Toggle::Create(m.create); vrb::TransformPtr transform = vrb::Transform::Create(m.create); transform->SetTransform(Matrix::Position(vrb::Vector(0.0f, 0.0f, 0.0f))); LoadSkybox(transform, basePath); - return transform; + toggle->AddNode(transform); + return toggle; } void @@ -1219,4 +1278,25 @@ JNI_METHOD(void, updatePointerColorNative) crow::BrowserWorld::Instance().UpdatePointerColor(); } +JNI_METHOD(void, showVRVideoNative) +(JNIEnv* aEnv, jobject, jint aWindowHandle, jint aVideoProjection) { + crow::BrowserWorld::Instance().ShowVRVideo(aWindowHandle, aVideoProjection); +} + +JNI_METHOD(void, hideVRVideoNative) +(JNIEnv* aEnv, jobject) { + crow::BrowserWorld::Instance().HideVRVideo(); +} + +JNI_METHOD(void, setControllersVisibleNative) +(JNIEnv* aEnv, jobject, jboolean aVisible) { + crow::BrowserWorld::Instance().SetControllersVisible(aVisible); +} + +JNI_METHOD(void, resetUIYawNative) +(JNIEnv* aEnv, jobject) { + crow::BrowserWorld::Instance().ResetUIYaw(); +} + + } // extern "C" diff --git a/app/src/main/cpp/BrowserWorld.h b/app/src/main/cpp/BrowserWorld.h index d572bb382..5f48a9b16 100644 --- a/app/src/main/cpp/BrowserWorld.h +++ b/app/src/main/cpp/BrowserWorld.h @@ -51,6 +51,10 @@ class BrowserWorld { void LayoutWidget(int32_t aHandle); void SetBrightness(const float aBrightness); void ExitImmersive(); + void ShowVRVideo(const int aWindowHandle, const int aVideoProjection); + void HideVRVideo(); + void SetControllersVisible(const bool aVisible); + void ResetUIYaw(); JNIEnv* GetJNIEnv() const; protected: struct State; @@ -61,7 +65,7 @@ class BrowserWorld { void DrawImmersive(); void DrawLoadingAnimation(); void DrawSplashAnimation(); - vrb::TransformPtr CreateSkyBox(const std::string& basePath); + vrb::TogglePtr CreateSkyBox(const std::string& basePath); void LoadSkybox(const vrb::TransformPtr transform, const std::string& basePath); void CreateFloor(); float DistanceToNode(const vrb::NodePtr& aNode, const vrb::Vector& aPosition) const; diff --git a/app/src/main/cpp/ControllerContainer.cpp b/app/src/main/cpp/ControllerContainer.cpp index 380c4211a..856de22d6 100644 --- a/app/src/main/cpp/ControllerContainer.cpp +++ b/app/src/main/cpp/ControllerContainer.cpp @@ -28,10 +28,12 @@ struct ControllerContainer::State { TogglePtr root; std::vector models; GeometryPtr pointerModel; + bool visible; void Initialize(vrb::CreationContextPtr& aContext) { context = aContext; root = Toggle::Create(aContext); + visible = true; } bool Contains(const int32_t aControllerIndex) { @@ -204,7 +206,7 @@ ControllerContainer::SetVisible(const int32_t aControllerIndex, const bool aVisi return; } Controller& controller = m.list[aControllerIndex]; - if (controller.transform) { + if (controller.transform && m.visible) { m.root->ToggleChild(*controller.transform, aVisible); } } @@ -323,6 +325,23 @@ void ControllerContainer::SetPointerColor(const vrb::Color& aColor) const { } } +void +ControllerContainer::SetVisible(const bool aVisible) { + if (m.visible == aVisible) { + return; + } + m.visible = aVisible; + if (aVisible) { + for (int i = 0; i < m.list.size(); ++i) { + if (m.list[i].enabled) { + m.root->ToggleChild(*m.list[i].transform, true); + } + } + } else { + m.root->ToggleAll(false); + } +} + ControllerContainer::ControllerContainer(State& aState, vrb::CreationContextPtr& aContext) : m(aState) { m.Initialize(aContext); } diff --git a/app/src/main/cpp/ControllerContainer.h b/app/src/main/cpp/ControllerContainer.h index 4ebe52969..af758531e 100644 --- a/app/src/main/cpp/ControllerContainer.h +++ b/app/src/main/cpp/ControllerContainer.h @@ -45,6 +45,7 @@ class ControllerContainer : public crow::ControllerDelegate { void EndTouch(const int32_t aControllerIndex) override; void SetScrolledDelta(const int32_t aControllerIndex, const float aScrollDeltaX, const float aScrollDeltaY) override; void SetPointerColor(const vrb::Color& color) const; + void SetVisible(const bool aVisible); protected: struct State; ControllerContainer(State& aState, vrb::CreationContextPtr& aContext); diff --git a/app/src/main/cpp/DeviceDelegate.h b/app/src/main/cpp/DeviceDelegate.h index 51c9e3bf5..9380739a1 100644 --- a/app/src/main/cpp/DeviceDelegate.h +++ b/app/src/main/cpp/DeviceDelegate.h @@ -45,6 +45,7 @@ class DeviceDelegate { virtual vrb::CameraPtr GetCamera(const device::Eye aWhich) = 0; virtual const vrb::Matrix& GetHeadTransform() const = 0; virtual const vrb::Matrix& GetReorientTransform() const = 0; + virtual void SetReorientTransform(const vrb::Matrix& aMatrix) = 0; virtual void SetClearColor(const vrb::Color& aColor) = 0; virtual void SetClipPlanes(const float aNear, const float aFar) = 0; virtual void SetControllerDelegate(ControllerDelegatePtr& aController) = 0; diff --git a/app/src/main/cpp/FadeBlitter.cpp b/app/src/main/cpp/FadeBlitter.cpp index 1881637e3..4b5c9f522 100644 --- a/app/src/main/cpp/FadeBlitter.cpp +++ b/app/src/main/cpp/FadeBlitter.cpp @@ -57,6 +57,7 @@ struct FadeBlitter::State : public vrb::ResourceGL::State { float animationEndAlpha; int animations; float currentBrightness; + bool visible; State() : vertexShader(0) , fragmentShader(0) @@ -68,6 +69,7 @@ struct FadeBlitter::State : public vrb::ResourceGL::State { , animationEndAlpha(0.0f) , animations(-1) , currentBrightness(1.0f) + , visible(true) {} }; @@ -105,7 +107,7 @@ FadeBlitter::Draw() { bool FadeBlitter::IsVisible() const { - return m.animations >= 0 || m.fadeColor.Alpha() > 0.0f; + return m.visible && (m.animations >= 0 || m.fadeColor.Alpha() > 0.0f); } @@ -122,6 +124,11 @@ void FadeBlitter::FadeIn() { m.animations = kAnimationLength; } +void +FadeBlitter::SetVisible(const bool aVisible) { + m.visible = aVisible; +} + FadeBlitter::FadeBlitter(State& aState, vrb::CreationContextPtr& aContext) : vrb::ResourceGL(aState, aContext) , m(aState) diff --git a/app/src/main/cpp/FadeBlitter.h b/app/src/main/cpp/FadeBlitter.h index 42f5faf04..238da0319 100644 --- a/app/src/main/cpp/FadeBlitter.h +++ b/app/src/main/cpp/FadeBlitter.h @@ -23,6 +23,7 @@ class FadeBlitter : protected vrb::ResourceGL { bool IsVisible() const; void SetBrightness(const float aBrightness); void FadeIn(); + void SetVisible(const bool aVisible); protected: struct State; FadeBlitter(State& aState, vrb::CreationContextPtr& aContext); diff --git a/app/src/main/cpp/Quad.cpp b/app/src/main/cpp/Quad.cpp index 692fb1b97..b42fd8d28 100644 --- a/app/src/main/cpp/Quad.cpp +++ b/app/src/main/cpp/Quad.cpp @@ -145,6 +145,12 @@ Quad::Create(vrb::CreationContextPtr aContext, const float aWorldWidth, const fl vrb::GeometryPtr Quad::CreateGeometry(vrb::CreationContextPtr aContext, const vrb::Vector &aMin, const vrb::Vector &aMax) { + device::EyeRect rect(0.0f, 0.0f, 1.0f, 1.0f); + return Quad::CreateGeometry(aContext, aMin, aMax, rect); +} + +vrb::GeometryPtr +Quad::CreateGeometry(vrb::CreationContextPtr aContext, const vrb::Vector &aMin, const vrb::Vector &aMax, const device::EyeRect& aRect) { vrb::VertexArrayPtr array = vrb::VertexArray::Create(aContext); const vrb::Vector bottomRight(aMax.x(), aMin.y(), aMin.z()); array->AppendVertex(aMin); // Bottom left @@ -152,10 +158,10 @@ Quad::CreateGeometry(vrb::CreationContextPtr aContext, const vrb::Vector &aMin, array->AppendVertex(aMax); // Top right array->AppendVertex(vrb::Vector(aMin.x(), aMax.y(), aMax.z())); // Top left - array->AppendUV(vrb::Vector(0.0f, 1.0f, 0.0f)); - array->AppendUV(vrb::Vector(1.0f, 1.0f, 0.0f)); - array->AppendUV(vrb::Vector(1.0f, 0.0f, 0.0f)); - array->AppendUV(vrb::Vector(0.0f, 0.0f, 0.0f)); + array->AppendUV(vrb::Vector(aRect.mX, aRect.mY + aRect.mHeight, 0.0f)); + array->AppendUV(vrb::Vector(aRect.mX + aRect.mWidth, aRect.mY + aRect.mHeight, 0.0f)); + array->AppendUV(vrb::Vector(aRect.mX + aRect.mWidth, aRect.mY, 0.0f)); + array->AppendUV(vrb::Vector(aRect.mX, aRect.mY, 0.0f)); vrb::Vector normal = (bottomRight - aMin).Cross(aMax - aMin).Normalize(); array->AppendNormal(normal); diff --git a/app/src/main/cpp/Quad.h b/app/src/main/cpp/Quad.h index d06349e14..2d9f53855 100644 --- a/app/src/main/cpp/Quad.h +++ b/app/src/main/cpp/Quad.h @@ -8,6 +8,7 @@ #include "vrb/Forward.h" #include "vrb/MacroUtils.h" +#include "Device.h" #include #include @@ -30,6 +31,7 @@ class Quad { static QuadPtr Create(vrb::CreationContextPtr aContext, const float aWorldWidth, const float aWorldHeight); static vrb::GeometryPtr CreateGeometry(vrb::CreationContextPtr aContext, const vrb::Vector& aMin, const vrb::Vector& aMax); static vrb::GeometryPtr CreateGeometry(vrb::CreationContextPtr aContext, const float aWorldWidth, const float aWorldHeight); + static vrb::GeometryPtr CreateGeometry(vrb::CreationContextPtr aContext, const vrb::Vector& aMin, const vrb::Vector& aMax, const device::EyeRect& aRect); void SetTexture(const vrb::TexturePtr& aTexture, int32_t aWidth, int32_t aHeight); void SetMaterial(const vrb::Color& aAmbient, const vrb::Color& aDiffuse, const vrb::Color& aSpecular, const float aSpecularExponent); void SetScaleMode(ScaleMode aScaleMode); diff --git a/app/src/main/cpp/VRVideo.cpp b/app/src/main/cpp/VRVideo.cpp new file mode 100644 index 000000000..3b2273452 --- /dev/null +++ b/app/src/main/cpp/VRVideo.cpp @@ -0,0 +1,203 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "VRVideo.h" +#include "vrb/ConcreteClass.h" +#include "vrb/Color.h" +#include "vrb/CreationContext.h" +#include "vrb/Geometry.h" +#include "vrb/Matrix.h" +#include "vrb/ModelLoaderAndroid.h" +#include "vrb/RenderState.h" +#include "vrb/RenderContext.h" +#include "vrb/TextureGL.h" +#include "vrb/TextureSurface.h" +#include "vrb/Toggle.h" +#include "vrb/Transform.h" +#include "vrb/VertexArray.h" + +#include "Quad.h" +#include "Widget.h" + +namespace crow { + +struct VRVideo::State { + vrb::CreationContextWeak context; + WidgetPtr window; + VRVideoProjection projection; + vrb::TogglePtr root; + vrb::TogglePtr leftEye; + vrb::TogglePtr rightEye; + State() + { + } + + void Initialize(const WidgetPtr& aWindow, const VRVideoProjection aProjection) { + vrb::CreationContextPtr create = context.lock(); + window = aWindow; + projection = aProjection; + root = vrb::Toggle::Create(create); + updateProjection(); + root->AddNode(leftEye); + if (rightEye) { + root->AddNode(rightEye); + } + } + + void updateProjection() { + switch (projection) { + case VRVideoProjection::VIDEO_PROJECTION_3D_SIDE_BY_SIDE: + leftEye = createQuadProjection(device::EyeRect(0.0f, 0.0f, 0.5f, 1.0f)); + rightEye = createQuadProjection(device::EyeRect(0.5f, 0.0f, 0.5f, 1.0f)); + break; + case VRVideoProjection::VIDEO_PROJECTION_360: + leftEye = createSphereProjection(false, device::EyeRect(0.0f, 0.0f, 1.0f, 1.0f)); + break; + case VRVideoProjection::VIDEO_PROJECTION_360_STEREO: + leftEye = createSphereProjection(false, device::EyeRect(0.0f, 0.5f, 1.0f, 0.5f)); + rightEye = createSphereProjection(false, device::EyeRect(0.0f, 0.0f, 1.0f, 0.5f)); + break; + case VRVideoProjection::VIDEO_PROJECTION_180: + leftEye = createSphereProjection(true, device::EyeRect(0.0f, 0.0f, 1.0f, 1.0f)); + break; + case VRVideoProjection::VIDEO_PROJECTION_180_STEREO_LEFT_RIGHT: + leftEye = createSphereProjection(true, device::EyeRect(0.0f, 0.0f, 0.5f, 1.0f)); + rightEye = createSphereProjection(true, device::EyeRect(0.5f, 0.0f, 0.5f, 1.0f)); + break; + case VRVideoProjection::VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM: + leftEye = createSphereProjection(true, device::EyeRect(0.0f, 0.5f, 1.0f, 0.5f)); + rightEye = createSphereProjection(true, device::EyeRect(0.0f, 0.0f, 1.0f, 0.5f)); + break; + } + } + + vrb::TogglePtr createSphereProjection(bool half, device::EyeRect aUVRect) { + const int kCols = 30; + const int kRows = 30; + const float kRadius = 20.0f; + + vrb::CreationContextPtr create = context.lock(); + vrb::VertexArrayPtr array = vrb::VertexArray::Create(create); + + + for (float row = 0; row <= kRows; row+= 1.0f) { + const float alpha = row * (float)M_PI / kRows; + const float sinAlpha = sinf(alpha); + const float cosAlpha = cosf(alpha); + + for (float col = 0; col <= kCols; col++) { + const float beta = col * (half ? 1.0f : 2.0f) * (float)M_PI / kCols; + const float sinBeta = sinf(beta); + const float cosBeta = cosf(beta); + + vrb::Vector vertex; + vrb::Vector uv; + vrb::Vector normal; + normal.x() = cosBeta * sinAlpha; + normal.y() = cosAlpha; + normal.z() = sinBeta * sinAlpha; + uv.x() = aUVRect.mX + (col / kCols) * aUVRect.mWidth; // u + uv.y() = aUVRect.mY + (row / kRows) * aUVRect.mHeight; // v + vertex.x() = kRadius * normal.x(); + vertex.y() = kRadius * normal.y(); + vertex.z() = kRadius * normal.z(); + + array->AppendVertex(vertex); + array->AppendUV(uv); + array->AppendNormal(vertex.Normalize()); + } + } + + std::vector indices; + + vrb::RenderStatePtr state = vrb::RenderState::Create(create); + state->SetLightsEnabled(false); + vrb::TexturePtr texture = std::dynamic_pointer_cast(window->GetSurfaceTexture()); + state->SetTexture(texture); + vrb::GeometryPtr geometry = vrb::Geometry::Create(create); + geometry->SetVertexArray(array); + geometry->SetRenderState(state); + + for (int row = 0; row < kRows; row++) { + for (int col = 0; col < kCols; col++) { + int first = 1 + (row * (kCols + 1)) + col; + int second = first + kCols + 1; + + indices.clear(); + indices.push_back(first); + indices.push_back(second); + indices.push_back(first + 1); + + indices.push_back(second); + indices.push_back(second + 1); + indices.push_back(first + 1); + geometry->AddFace(indices, indices, indices); + } + } + + vrb::TransformPtr transform = vrb::Transform::Create(create); + if (half) { + vrb::Matrix matrix = vrb::Matrix::Rotation(vrb::Vector(0.0f, 1.0f, 0.0f), (float) M_PI); + transform->SetTransform(matrix); + } else { + transform->SetTransform(vrb::Matrix::Rotation(vrb::Vector(0.0f, 1.0f, 0.0f), (float) M_PI * -0.5f)); + } + transform->AddNode(geometry); + + vrb::TogglePtr result = vrb::Toggle::Create(create); + result->AddNode(transform); + return result; + } + + vrb::TogglePtr createQuadProjection(device::EyeRect aUVRect) { + vrb::CreationContextPtr create = context.lock(); + vrb::Vector min, max; + window->GetWidgetMinAndMax(min, max); + vrb::GeometryPtr geometry = Quad::CreateGeometry(create, min, max, aUVRect); + vrb::RenderStatePtr state = vrb::RenderState::Create(create); + state->SetLightsEnabled(false); + vrb::TexturePtr texture = std::dynamic_pointer_cast(window->GetSurfaceTexture()); + state->SetTexture(texture); + geometry->SetRenderState(state); + + vrb::TransformPtr transform = vrb::Transform::Create(create); + transform->SetTransform(window->GetTransform()); + transform->AddNode(geometry); + vrb::TogglePtr result = vrb::Toggle::Create(create); + result->AddNode(transform); + return result; + } +}; + +void +VRVideo::SelectEye(device::Eye aEye) { + if (!m.rightEye) { + // Not stereo projection, always show the left eye. + return; + } + m.leftEye->ToggleAll(aEye == device::Eye::Left); + m.rightEye->ToggleAll(aEye == device::Eye::Right); +} + +vrb::NodePtr +VRVideo::GetRoot() const { + return m.root; +} + +VRVideoPtr +VRVideo::Create(vrb::CreationContextPtr aContext, const WidgetPtr& aWindow, const VRVideoProjection aProjection) { + VRVideoPtr result = std::make_shared >(aContext); + result->m.Initialize(aWindow, aProjection); + return result; +} + + +VRVideo::VRVideo(State& aState, vrb::CreationContextPtr& aContext) : m(aState) { + m.context = aContext; +} + +VRVideo::~VRVideo() {} + +} // namespace crow diff --git a/app/src/main/cpp/VRVideo.h b/app/src/main/cpp/VRVideo.h new file mode 100644 index 000000000..c2718d387 --- /dev/null +++ b/app/src/main/cpp/VRVideo.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef VRBROWSER_VR_VIDEO_H +#define VRBROWSER_VR_VIDEO_H + +#include "vrb/Forward.h" +#include "vrb/MacroUtils.h" +#include "Device.h" + +#include +#include +#include +#include + +namespace crow { + +class VRVideo; +typedef std::shared_ptr VRVideoPtr; + +class Widget; +typedef std::shared_ptr WidgetPtr; + +class VRVideo { +public: + // Should match the values in VideoProjectionMenuWidget.java + enum class VRVideoProjection { + VIDEO_PROJECTION_3D_SIDE_BY_SIDE = 0, + VIDEO_PROJECTION_360 = 1, + VIDEO_PROJECTION_360_STEREO = 2, + VIDEO_PROJECTION_180 = 3, + VIDEO_PROJECTION_180_STEREO_LEFT_RIGHT = 4, + VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM = 5, + }; + static VRVideoPtr Create(vrb::CreationContextPtr aContext, const WidgetPtr& aWindow, const VRVideoProjection aProjection); + void SelectEye(device::Eye aEye); + vrb::NodePtr GetRoot() const; + + struct State; + VRVideo(State& aState, vrb::CreationContextPtr& aContext); + ~VRVideo(); +private: + State& m; + VRVideo() = delete; + VRB_NO_DEFAULTS(VRVideo) +}; + +} // namespace crow + +#endif // VRBROWSER_VR_VIDEO_H diff --git a/app/src/main/cpp/Widget.cpp b/app/src/main/cpp/Widget.cpp index 82944e635..279ec2a0d 100644 --- a/app/src/main/cpp/Widget.cpp +++ b/app/src/main/cpp/Widget.cpp @@ -184,6 +184,11 @@ Widget::GetSurfaceTextureName() const { return m.name; } +const vrb::TextureSurfacePtr +Widget::GetSurfaceTexture() const { + return m.surface; +} + void Widget::GetSurfaceTextureSize(int32_t& aWidth, int32_t& aHeight) const { m.quad->GetTextureSize(aWidth, aHeight); diff --git a/app/src/main/cpp/Widget.h b/app/src/main/cpp/Widget.h index 25dc0c29c..3e03d9ce3 100644 --- a/app/src/main/cpp/Widget.h +++ b/app/src/main/cpp/Widget.h @@ -33,6 +33,7 @@ class Widget { uint32_t GetHandle() const; void ResetFirstDraw(); const std::string& GetSurfaceTextureName() const; + const vrb::TextureSurfacePtr GetSurfaceTexture() const; void GetSurfaceTextureSize(int32_t& aWidth, int32_t& aHeight) const; void SetSurfaceTextureSize(int32_t aWidth, int32_t aHeight); void GetWidgetMinAndMax(vrb::Vector& aMin, vrb::Vector& aMax) const; diff --git a/app/src/main/res/drawable/fullscreen_button.xml b/app/src/main/res/drawable/fullscreen_button.xml new file mode 100644 index 000000000..b1027f412 --- /dev/null +++ b/app/src/main/res/drawable/fullscreen_button.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/fullscreen_button_first.xml b/app/src/main/res/drawable/fullscreen_button_first.xml new file mode 100644 index 000000000..a3aed5ebc --- /dev/null +++ b/app/src/main/res/drawable/fullscreen_button_first.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/fullscreen_button_last.xml b/app/src/main/res/drawable/fullscreen_button_last.xml new file mode 100644 index 000000000..7bd2a9114 --- /dev/null +++ b/app/src/main/res/drawable/fullscreen_button_last.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_icon_brightness.xml b/app/src/main/res/drawable/ic_icon_brightness.xml new file mode 100644 index 000000000..bc02f552a --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_brightness.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_icon_meda_seek_forward.xml b/app/src/main/res/drawable/ic_icon_meda_seek_forward.xml new file mode 100644 index 000000000..3f9a31382 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_meda_seek_forward.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_icon_meda_volume_muted.xml b/app/src/main/res/drawable/ic_icon_meda_volume_muted.xml new file mode 100644 index 000000000..749b73894 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_meda_volume_muted.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_icon_media_pause.xml b/app/src/main/res/drawable/ic_icon_media_pause.xml new file mode 100644 index 000000000..ddd673b56 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_media_pause.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_icon_media_play.xml b/app/src/main/res/drawable/ic_icon_media_play.xml new file mode 100644 index 000000000..f0eebdda9 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_media_play.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_icon_media_seek_backward.xml b/app/src/main/res/drawable/ic_icon_media_seek_backward.xml new file mode 100644 index 000000000..947696672 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_media_seek_backward.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_icon_media_volume.xml b/app/src/main/res/drawable/ic_icon_media_volume.xml new file mode 100644 index 000000000..adb139f9e --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_media_volume.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_icon_videoplayback_180.xml b/app/src/main/res/drawable/ic_icon_videoplayback_180.xml new file mode 100644 index 000000000..03be4313b --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_videoplayback_180.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_icon_videoplayback_180_stereo_leftright.xml b/app/src/main/res/drawable/ic_icon_videoplayback_180_stereo_leftright.xml new file mode 100644 index 000000000..393c16c27 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_videoplayback_180_stereo_leftright.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_icon_videoplayback_180_stereo_topbottom.xml b/app/src/main/res/drawable/ic_icon_videoplayback_180_stereo_topbottom.xml new file mode 100644 index 000000000..a1cf0d2be --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_videoplayback_180_stereo_topbottom.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_icon_videoplayback_360.xml b/app/src/main/res/drawable/ic_icon_videoplayback_360.xml new file mode 100644 index 000000000..8eb9ac93a --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_videoplayback_360.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_icon_videoplayback_360_stereo.xml b/app/src/main/res/drawable/ic_icon_videoplayback_360_stereo.xml new file mode 100644 index 000000000..1dfd5bb29 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_videoplayback_360_stereo.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_icon_videoplayback_3dsidebyside.xml b/app/src/main/res/drawable/ic_icon_videoplayback_3dsidebyside.xml new file mode 100644 index 000000000..b52c8d926 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_videoplayback_3dsidebyside.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_icon_vr_projection.xml b/app/src/main/res/drawable/ic_icon_vr_projection.xml new file mode 100644 index 000000000..9d8d330bd --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_vr_projection.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/media_button.xml b/app/src/main/res/drawable/media_button.xml new file mode 100644 index 000000000..9670c7f13 --- /dev/null +++ b/app/src/main/res/drawable/media_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/media_controls_background.xml b/app/src/main/res/drawable/media_controls_background.xml new file mode 100644 index 000000000..219671546 --- /dev/null +++ b/app/src/main/res/drawable/media_controls_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/media_seekbar_background.xml b/app/src/main/res/drawable/media_seekbar_background.xml new file mode 100644 index 000000000..bfb6dd4c4 --- /dev/null +++ b/app/src/main/res/drawable/media_seekbar_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/media_seekbar_label_background.xml b/app/src/main/res/drawable/media_seekbar_label_background.xml new file mode 100644 index 000000000..d2affa889 --- /dev/null +++ b/app/src/main/res/drawable/media_seekbar_label_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/media_seekbar_thumb.xml b/app/src/main/res/drawable/media_seekbar_thumb.xml new file mode 100644 index 000000000..2c3f670db --- /dev/null +++ b/app/src/main/res/drawable/media_seekbar_thumb.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/menu_background.xml b/app/src/main/res/drawable/menu_background.xml new file mode 100644 index 000000000..58dea0a8d --- /dev/null +++ b/app/src/main/res/drawable/menu_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/menu_item_background.xml b/app/src/main/res/drawable/menu_item_background.xml new file mode 100644 index 000000000..ae8c5b2bd --- /dev/null +++ b/app/src/main/res/drawable/menu_item_background.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/menu_item_background_first.xml b/app/src/main/res/drawable/menu_item_background_first.xml new file mode 100644 index 000000000..d29b41d7f --- /dev/null +++ b/app/src/main/res/drawable/menu_item_background_first.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/menu_item_background_last.xml b/app/src/main/res/drawable/menu_item_background_last.xml new file mode 100644 index 000000000..718283cce --- /dev/null +++ b/app/src/main/res/drawable/menu_item_background_last.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/menu_item_color.xml b/app/src/main/res/drawable/menu_item_color.xml new file mode 100644 index 000000000..253d1fe39 --- /dev/null +++ b/app/src/main/res/drawable/menu_item_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/volume_seekbar_background.xml b/app/src/main/res/drawable/volume_seekbar_background.xml new file mode 100644 index 000000000..c86ba988b --- /dev/null +++ b/app/src/main/res/drawable/volume_seekbar_background.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/media_controls.xml b/app/src/main/res/layout/media_controls.xml new file mode 100644 index 000000000..fd95b7e66 --- /dev/null +++ b/app/src/main/res/layout/media_controls.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/media_controls_seek_bar.xml b/app/src/main/res/layout/media_controls_seek_bar.xml new file mode 100644 index 000000000..9b261f229 --- /dev/null +++ b/app/src/main/res/layout/media_controls_seek_bar.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/menu.xml b/app/src/main/res/layout/menu.xml new file mode 100644 index 000000000..25ea87654 --- /dev/null +++ b/app/src/main/res/layout/menu.xml @@ -0,0 +1,26 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/menu_item_image_text.xml b/app/src/main/res/layout/menu_item_image_text.xml new file mode 100644 index 000000000..7989c1035 --- /dev/null +++ b/app/src/main/res/layout/menu_item_image_text.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/app/src/main/res/layout/navigation_bar.xml b/app/src/main/res/layout/navigation_bar.xml index 386d9afbb..a1678d7a0 100644 --- a/app/src/main/res/layout/navigation_bar.xml +++ b/app/src/main/res/layout/navigation_bar.xml @@ -66,13 +66,31 @@ + + + + diff --git a/app/src/main/res/layout/volume_control.xml b/app/src/main/res/layout/volume_control.xml new file mode 100644 index 000000000..b17814818 --- /dev/null +++ b/app/src/main/res/layout/volume_control.xml @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 971b1537e..cc37a20bd 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -52,6 +52,25 @@ 500dp 320dp + + 88dp + 14dp + + + 330dp + 7dp + + + 150dp + + + 324dp + 100dp + 284dp + 85dp + 15dp + 2.5 + 1.6 -1.8 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f8093cf93..0ba23ed13 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -5,6 +5,10 @@ + + + + @@ -45,6 +49,28 @@ @drawable/main_button_private + + + + + + + +