基于GVR(Google VR)安卓平台下的 3D全景视频播放器

  • Google GVR
  • GVR简介
  • 示例应用
  • 源码实现
  • GVR关键的api调用
  • Gradle配置
  • 效果图
  • 布局
  • m3u8和hls协议(自己记录理解)


Google GVR

本文纯属记录做项目时遇到的问题和方法以及源码的分享

如果引用了别的文章,作者将会注明文章出处

最近项目里要求做一个全景视频播放器,并且要求播放的视频必须通过UDP协议从服务器端传输到客户端,基于H.264编码,翻了很久GVR提供的相关API,并没有找到底层的视频流接收方法,但Android平台下的GVR提供了两种方法:

1.对本地的360全景视频进行播放
2.通过HLS协议对实时流进行播放

GVR简介


android vr播放 vr安卓播放器_android vr播放

官方给我们提供了两套解决观看VR视频的方式:
Daydream
Cardboard
给我们提供了三个平台的API,分别是:Unity 3D 、Android、IOS

使用Google VR SDK为Daydream和Cardboard构建应用。官网展示如何为Google VR开发设置Android Studio并试用示例应用程序。
设置Google VR依赖项

说明一下配置项目级别的build.gradle文件:

因为现在Android studio下的AndroidX 一些老的Demo可能要配置时间长一些 调整原先支持的v7和v4

确保jcenter()已声明默认存储库位置。
声明一个Android Gradle插件依赖项:
Google VR SDK项目:使用gradle:2.3.3或更高版本。
Google VR NDK项目:使用gradle-experimental:0.9.3或更高版本。

allprojects {
 repositories {
 jcenter()
 }
 }dependencies {
 // The Google VR SDK requires version 2.3.3 or higher.
 classpath ‘com.android.tools.build:gradle:2.3.3’
// The Google VR NDK requires experimental version 0.9.3 or higher.
// classpath 'com.android.tools.build:gradle-experimental:0.9.3'
}

在模块级别的build.gradle 文件中添加Google VR SDK库依赖项。您可以在gvr-android-sdk > 库中查看可用的库及其版本 。

例如,dependencies在gvr-android-sdk > 示例 > sdk-hellovr > build.gradle中查看示例应用的声明。

dependencies {
 // Adds Google VR spatial audio support
 compile ‘com.google.vr:sdk-audio:1.160.0’// Required for all Google VR apps
 compile 'com.google.vr:sdk-base:1.160.0'}

有关更多信息,请参阅 Android Studio指南中的添加构建依赖项。

示例应用

SDK中有两个示例应用程序,它们演示了如何嵌入 360°媒体。这两个示例都是单活动应用程序,它们显示嵌入式全景图像或视频:

Vr全景视图

VrVideoView

android vr播放 vr安卓播放器_android vr播放_02


VrPanoramaView和VrVideoView样本的VR视图处于嵌入模式。它们允许用户通过旋转手机来查看全景的不同部分。该VrVideoView示例还允许用户暂停,并通过轻触VR查看发挥自己的视频,并使用滑块寻求通过视频。这些样本 在VR视图中显示了全屏模式和Cardboard模式按钮,允许用户更改模式。

android vr播放 vr安卓播放器_android_03

具体的Android平台下的Demo参考相关官网。

源码实现

GVR关键的api调用

上述文章说过:
GVR支持本地视频流和基于HTTP的HLS协议(类似于直播形式的视频流)

Gradle配置

配置为AndroidX并且添加gvr依赖

android {
    compileSdkVersion 29
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "com.qj.gvr_test"
        minSdkVersion 19 //使用gvr 不能低于19
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('androidx.test.espresso:espresso-core:3.1.0', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    //noinspection GradleCompatible
    implementation 'androidx.appcompat:appcompat:1.0.0'
    testImplementation 'junit:junit:4.12'

    // 使用google vr 添加的
    implementation 'com.google.vr:sdk-base:1.10.0'
    implementation 'com.google.vr:sdk-audio:1.10.0'
    implementation 'com.google.vr:sdk-videowidget:1.10.0'//使用播放控件需要添加(VrVideoView)
}

API中这样定义访问视频方法

//通过uri访问m3u8封装的视频流,点播和直播的形式
 public void loadVideo(android.net.Uri uri, com.google.vr.sdk.widgets.video.VrVideoView.Options options) throws java.io.IOException { /* compiled code */ }
//通过Assets资源文件夹直接访问视频资源
 public void loadVideoFromAsset(java.lang.String filename, com.google.vr.sdk.widgets.video.VrVideoView.Options options) throws java.io.IOException { /* compiled code */ }
--------------------------------------------------------------
//支持格式中 支持FORMAT_HLS 
public static class Options {
        private static final int FORMAT_START_MARKER = 0;
        public static final int FORMAT_DEFAULT = 1;
        public static final int FORMAT_HLS = 2;
        private static final int FORMAT_END_MARKER = 3;
        public int inputFormat;
        private static final int TYPE_START_MARKER = 0;
        public static final int TYPE_MONO = 1;
        public static final int TYPE_STEREO_OVER_UNDER = 2;
        private static final int TYPE_END_MARKER = 3;
        public int inputType;

        public Options() { /* compiled code */ }
package com.dty.gvr_test;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;

import com.google.vr.sdk.widgets.video.VrVideoEventListener;
import com.google.vr.sdk.widgets.video.VrVideoView;

import java.io.IOException;

public class PlayerActivity extends Activity implements View.OnClickListener {
    private VrVideoView mVideoView;
    private String mUrl;
    private String mTotalDuration;
    private TextView mVideoDuration;
    private SeekBar mSeekBar;
    private View mVideoPorgressContainer;
    private View mVideoVr;
    private ImageView mPlayView;
    private boolean isPlaying;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_player);
        Intent intent = getIntent();
        mUrl = intent.getStringExtra("url");//上一个Activity传递过来的url播放地址
        initView();
        initUrlData();
        initListener();
    }

    @Override
    protected void onNewIntent(Intent intent) {
        setIntent(intent);
        //TODO handleIntent
    }

    private void initView() {
        mVideoView   = findViewById(R.id.video_view);
        mPlayView    = findViewById(R.id.play);
        mSeekBar     = findViewById(R.id.video_progress);
        mVideoVr     = findViewById(R.id.video_vr);

        mVideoDuration          = findViewById(R.id.video_duration);
        mVideoPorgressContainer = findViewById(R.id.video_progress_container);


        mVideoView.setInfoButtonEnabled(true);
        mVideoView.setFullscreenButtonEnabled(true);
        mVideoView.setStereoModeButtonEnabled(true);
    }

//播放本地的全景视频
    private void initLocalData() {
        VrVideoView.Options option = new VrVideoView.Options();

        option.inputType   = VrVideoView.Options.TYPE_STEREO_OVER_UNDER;
        option.inputFormat = VrVideoView.Options.FORMAT_DEFAULT;
        try {
            mVideoView.loadVideoFromAsset("congo_2048.mp4",option);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
//基于Url的视频流播放
    private void initUrlData() {
        VrVideoView.Options option = new VrVideoView.Options();

        option.inputType   = VrVideoView.Options.TYPE_STEREO_OVER_UNDER;
        option.inputFormat = VrVideoView.Options.FORMAT_HLS;
        Uri uri;
        if ("".equals(mUrl)) {
            uri = Uri.parse(" ");
        } else {
            uri = Uri.parse("http://cache.utovr.com/201508270528174780.m3u8");
        }

        try {
          mVideoView.loadVideo(uri, option);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    private void initListener() {
        mPlayView.setOnClickListener(this);
        mVideoVr.setOnClickListener(this);
        mSeekBar.setOnSeekBarChangeListener(new SeekBarListener());
        mVideoView.setEventListener(new VrVideoEventListener() {
            @Override
            public void onClick() {
                //处理控制面板的显示和隐藏
                int visibility = mVideoPorgressContainer.getVisibility();
                mVideoPorgressContainer.setVisibility(visibility == View.VISIBLE ? View.GONE : View.VISIBLE);
            }

            /**
             * Make the video mPlayView in a loop. This method could also be used to move to the next video in
             * a playlist.
             */
            @Override
            public void onCompletion() {
                mVideoView.seekTo(0);//循环播放效果
            }

            @Override
            public void onNewFrame() {
                updateVideoProgress();
            }

            @Override
            public void onLoadSuccess() {
                long duration = mVideoView.getDuration();//视频总时长,毫秒
                mTotalDuration = RegularExpress.parseDuration(duration);
                mSeekBar.setMax((int) duration);
            }

            @Override
            public void onLoadError(String errorMessage) {
                super.onLoadError(errorMessage);
                Toast.makeText(PlayerActivity.this, "加载视频失败", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onDisplayModeChanged(int newDisplayMode) {
                super.onDisplayModeChanged(newDisplayMode);
            }
        });
    }

    /**
     * 更新播放进度
     */
    private void updateVideoProgress() {
        long currentPosition = mVideoView.getCurrentPosition();
        String currentPos = RegularExpress.parseDuration(currentPosition);
        mSeekBar.setProgress((int) (currentPosition));//更新播放进度
        StringBuilder sb = new StringBuilder();
        sb.append(currentPos);
        sb.append(" / ");
        sb.append(mTotalDuration);
        mVideoDuration.setText(sb);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.play:
                performClickPlay();
                break;
            case R.id.video_vr:
                performClickVideoVr();
                break;

            default:
                break;
        }
    }

    private void performClickVideoVr() {
        mVideoView.setDisplayMode(3);//enterStereoMode,眼镜模式
    }

    /**
     * 播放暂停切换
     */
    private void performClickPlay() {
        if (isPlaying) {
            mVideoView.pauseVideo();
            mPlayView.setImageResource(R.mipmap.play);
            isPlaying = false;
        } else {
            mVideoView.playVideo();
            mPlayView.setImageResource(R.mipmap.stop);
            isPlaying = true;
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        // Prevent the view from rendering continuously when in the background.
        mVideoView.pauseRendering();
        // If the video is playing when onPause() is called, the default behavior will be to pause
        // the video and keep it paused when onResume() is called.
        isPlaying = false;
    }

    @Override
    protected void onResume() {
        super.onResume();
        // Resume the 3D rendering.
        mVideoView.resumeRendering();
    }

    @Override
    protected void onDestroy() {
        mVideoView.shutdown();
        super.onDestroy();
    }

    /**
     * 播放器进度条监听
     */
    private class SeekBarListener implements SeekBar.OnSeekBarChangeListener {

        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            if (fromUser) {
                mVideoView.seekTo(progress);
            }
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            //手指离开进度条,三秒钟后隐藏控制面板
        }
    }
}

效果图

android vr播放 vr安卓播放器_android vr播放_04

参考:

布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.dty.gvr_test.MainActivity">

    <EditText
        android:id="@+id/url"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/y100"
        android:hint="输入url"/>

    <Button
        android:id="@+id/play"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/url"
        android:layout_marginTop="46dp"
        android:text="播放流视频" />

    <Button
        android:id="@+id/playurl"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/url"
        android:layout_marginTop="0dp"
        android:text="播放本地视频" />
</RelativeLayout>

m3u8和hls协议(自己记录理解)