記錄一款Unity VR視頻播放器插件的開發


效果圖##

先上一個效果圖:
效果圖

背景

公司最近在做VR直播平台,VR開發我們用到了Unity,而在Unity中播放視頻就需要一款視頻插件,我們調研了幾個視頻插件,記錄兩個,如下:

Unity視頻插件調研

網上搜了搜,最流行的有以下兩款Unity插件:

  • AVPro 這個在Unity商店售價150$,最新release版本為1.6.15,功能包括:

Powerful cross-platform video playback solution for Unity.

Native video playback on Android, iOS, macOS and tvOS (Apple TV), WebGL, Windows, Windows Phone and UWP.

Features include:

  • New Unity 2017 supported
  • New New iOS video playback path that uses less memory
  • One API for video playback on all supported platforms
  • Unity 4.6 - 5.x supported
  • 8K video (on supported hardware)
  • VR Support (mono, stereo, equirectangular and cubemap)
  • Transparency support (native and packed)
  • Subtitles support (external SRT)
  • Fast flexible video playback
  • In-editor playback support for Windows and macOS
  • Free watermarked trial version available
  • Components for IMGUI, uGUI and NGUI
  • Over 64 PlayMaker actions included
  • Easy to use drag and drop components
  • Linear and Gamma colour spaces supported
  • Fast native Direct3D, OpenGL and Metal texture updates
  • Desktop support for Hap, Hap Alpha, Hap Q and Hap Q Alpha
  • Streaming video from URL (when supported by platform)

此插件支持HLS視頻播放,使用文檔很詳細,但是此插件沒有源碼,不適合做以后的個性化開發。

Supported resolutions:

  • Android: General devices support up to 1920 * 1080.
    The latest device supports up to 4k.
  • iOS: General devices support up to 1920 * 1080.
    The latest device is support up to 2560 * 1440.
    iPhone 6s Plus supports up to 4k.
  • It also supports StreamingAssets, external storage, and streaming services.
  • Android streaming support list: http, HLS (http live streaming),rtsp
  • iOS streaming support list: http,HLS (http live streaming)
  • EasyMovieTexture requires Android 4.0 or above.
  • EasyMovieTexture requires iOS 6.0 or Above.
  • Unity 4.X requires an iOS Pro.
  • In Unity 5.X it does not require a Pro.
  • Supports multithreaded rendering options. (Only supports Unity 5.X.)

這個插件貌似是個人開發的,沒有說明文檔,有部分java源碼,native code並沒有給出。我們需要有源碼的插件方便以后的個性化開發。

自己動手,風衣足食##

綜合以上調研結果,我們決定自己動手實現一個簡單能滿足我們要求的Unity播放器插件,有兩個難點要突破:

  • 一個是找一個合適的開源播放器。
  • 另一個就是如何把播放視頻畫面映射到Unity中的物體表面,這個是最關鍵的。

尋找素材

從下面這個帖子中,找到了一些可以參考的資料。

unity 3d 中如何實現以物體的表面作為播放視頻的位置,比如在牆面播放視頻?

尋找開源播放器

本來打算使用VLC播放器的,但是同事發現有一個商用的開源播放器,並且使用的人數也不少,B站的ijkplayer。正好在上面的帖子中回復人也提到了這個播放器,我們決定使用這個播放器。

如何做視頻畫面映射

沒有一點Unity開發經驗,只能從頭一點點學起,知乎的帖子里面,有個人回復可以參考OVR里面的例子。閱讀了里面的代碼,同時也參考了easyMovieTexture中的源碼(easyMovie中只有java代碼,關鍵的native code並沒有給)。看的有些似懂非懂,嘗試了之后,居然成功了。

最關鍵的一點我描述成下面的話:

將Ijkplayer的AndroidSurfaceTexture紋理ID和Unity中Texture2D的紋理ID分別同時綁定到不同的目標上。AndroidSurfaceTexture綁定到GL_TEXTURE_EXTERNAL_OES,Unity的紋理ID綁定到GL_TEXTURE_2D

從頭到尾梳理一遍流程

初始化####

  • Unity

Unity端初始化一個Texture2D紋理ID用於顯示視頻幀。

m_VideoTexture = new Texture2D (Call_GetVideoWidth (), Call_GetVideoHeight (), TextureFormat.RGB565, false);
  • OVR

這里使用了OVR里面的native code,OVR中初始化AndroidSurfaceTexture和相關的函數:

static const char * className = "android/graphics/SurfaceTexture";
	const jclass surfaceTextureClass = jni->FindClass(className);
	if ( surfaceTextureClass == 0 ) {
		FAIL( "FindClass( %s ) failed", className );
	}

	// find the constructor that takes an int
	const jmethodID constructor = jni->GetMethodID( surfaceTextureClass, "<init>", "(I)V" );
	if ( constructor == 0 ) {
		FAIL( "GetMethodID( <init> ) failed" );
	}

	jobject obj = jni->NewObject( surfaceTextureClass, constructor, textureId );
	if ( obj == 0 ) {
		FAIL( "NewObject() failed" );
	}

	javaObject = jni->NewGlobalRef( obj );
	if ( javaObject == 0 ) {
		FAIL( "NewGlobalRef() failed" );
	}

	// Now that we have a globalRef, we can free the localRef
	jni->DeleteLocalRef( obj );

    updateTexImageMethodId = jni->GetMethodID( surfaceTextureClass, "updateTexImage", "()V" );
    if ( !updateTexImageMethodId ) {
    	FAIL( "couldn't get updateTexImageMethodId" );
    }

    getTimestampMethodId = jni->GetMethodID( surfaceTextureClass, "getTimestamp", "()J" );
    if ( !getTimestampMethodId ) {
    	FAIL( "couldn't get getTimestampMethodId" );
    }

	setDefaultBufferSizeMethodId = jni->GetMethodID( surfaceTextureClass, "setDefaultBufferSize", "(II)V" );
    if ( !setDefaultBufferSizeMethodId ) {
		FAIL( "couldn't get setDefaultBufferSize" );
    }

	// jclass objects are localRefs that need to be freed
	jni->DeleteLocalRef( surfaceTextureClass );

初始化紋理ID,並將其綁定到目標GL_TEXTURE_2D上:

glGenTextures( 1, &textureId );
	glBindTexture( GL_TEXTURE_EXTERNAL_OES, textureId );
	glTexParameterf( GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
	glTexParameterf( GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
	glTexParameterf( GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
	glTexParameterf( GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
	glBindTexture( GL_TEXTURE_EXTERNAL_OES, 0 );

將Unity的紋理ID傳遞到OVR中,用於綁定到目標GL_TEXTURE_EXTERNAL_OES上:


jobject OVR_Media_Surface( void * texPtr, int const width, int const height )
{
	GLuint texId = (GLuint)(size_t)(texPtr);
	LOG( "OVR_Media_Surface(%i, %i, %i)", texId, width, height );
	return _msp.VideoSurface.Bind( texId, width, height );
}
  • Ijkplayer

創建一個播放器,注意這里我們使用OVR中已經實例化的AndroidMovieTexture來初始化播放器。

 m_IjkMediaPlayer.setSurface(m_Surface);

刷新####

刷新操作由Unity中的Update函數觸發,最終在OVR中執行,首先調用AndroidMovieTexture中的Update函數,接下來就是綁定紋理操作,Ijkplayer的紋理ID每刷新一次綁定一次。而Unity的紋理ID只有在視頻圖像長度或者寬度發生變化才會綁定。

void MediaSurface::Update()
{
	if ( !AndroidSurfaceTexture )
	{
		LOG( "!AndroidSurfaceTexture" );
		return;
	}
	if ( TexId <= 0 )
	{
		//LOG( "TexId <= 0" );
		return;
	}
	AndroidSurfaceTexture->Update();
	if ( AndroidSurfaceTexture->GetNanoTimeStamp() == LastSurfaceTexNanoTimeStamp )
	{
		//LOG( "No new surface!" );
		return;
	}
	LastSurfaceTexNanoTimeStamp = AndroidSurfaceTexture->GetNanoTimeStamp()

   // If the SurfaceTexture has changed dimensions, we need to
	// reallocate the texture and FBO.
	glActiveTexture( GL_TEXTURE0 );
	glBindTexture( GL_TEXTURE_EXTERNAL_OES, AndroidSurfaceTexture->GetTextureId() );
	if ( TexIdWidth != BoundWidth || TexIdHeight != BoundHeight )
	{
		LOG( "New surface size: %ix%i", BoundWidth, BoundHeight );

		TexIdWidth = BoundWidth;
		TexIdHeight = BoundHeight;

		if ( Fbo )
		{
			glDeleteFramebuffers( 1, &Fbo );
		}

		glActiveTexture( GL_TEXTURE1 );
		glBindTexture( GL_TEXTURE_2D, TexId );
		glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA,
				TexIdWidth, TexIdHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

		glBindTexture( GL_TEXTURE_2D, 0 );
		glActiveTexture( GL_TEXTURE0 );

		glGenFramebuffers( 1, &Fbo );
		glBindFramebuffer( GL_FRAMEBUFFER, Fbo );
		glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
				TexId, 0 );
		glBindFramebuffer( GL_FRAMEBUFFER, 0 );
	}
}

最后的結果可能是這個樣子的:Ijkplayer負責推動視頻不停向前播放,播放器的紋理也會不停刷新,這會帶動Unity紋理跟着刷新,最終顯示在Unity的Material上。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM