課程介紹
在學(xué)習(xí)了前面章節(jié)OpenGL基礎(chǔ)知識(shí)后,讀者應(yīng)該具備了復(fù)雜界面特效谦去、圖片高效處理的開發(fā)能力。接下來的章節(jié)主要轉(zhuǎn)向Android視頻應(yīng)用開發(fā)中的OpenGL ES部分。
一. 視頻播放器搭建
1. 視圖容器
界面視圖容器依舊使用GLSurfaceView稍算,繪制方式是RENDERMODE_CONTINUOUSLY持續(xù)繪制的模式(課程演示鸣奔,減少框架部分墨技,相應(yīng)的有不必要的性能損耗)。
2. 必要框架
因?yàn)楸竟?jié)涉及外部文件讀取挎狸,所以會(huì)涉及到外部存儲(chǔ)讀寫權(quán)限的獲取扣汪、文件URI的解析、媒體文件數(shù)據(jù)解析锨匆,該部分內(nèi)容非本節(jié)重點(diǎn)崭别,因此詳情見工程代碼。
3. 媒體播放器
要驅(qū)動(dòng)視頻進(jìn)行播放恐锣,需要借助到系統(tǒng)的媒體播放器MediaPlayer茅主,它的相關(guān)方法如下:
- setDataSource:設(shè)置數(shù)據(jù)源,這里直接傳入文件路徑
- isLooping:是否循環(huán)播放
- prepare:進(jìn)入準(zhǔn)備狀態(tài)
- start:開始播放
- pause:暫停
- stop:停止
- release:釋放資源
具體的API可以參見官網(wǎng)侥蒙,生命周期流程圖如下:
播放器生命周期的邏輯處理暗膜,詳見工程代碼。這里重點(diǎn)講解下如何將MediaPlayer和GLSurfaceView進(jìn)行綁定使用鞭衩,從而可以在GL上進(jìn)行視頻渲染播放学搜。
①. 綁定紋理ID
// 1. 創(chuàng)建紋理ID
val textureIds = ......
// 2. 創(chuàng)建SurfaceTexture娃善、Surface,并綁定到MediaPlayer上瑞佩,接收畫面驅(qū)動(dòng)回調(diào)
surfaceTexture = SurfaceTexture(textureIds[0])
surfaceTexture!!.setOnFrameAvailableListener(this)
val surface = Surface(surfaceTexture)
mediaPlayer.setSurface(surface)
這里分為以下幾步:
- 創(chuàng)建紋理ID聚磺,用于GL渲染
- 通過紋理ID創(chuàng)建一個(gè)SurfaceTexture對象
- 通過SurfaceTexture對象創(chuàng)建一個(gè)Surface對象
- 將Surface傳入MediaPlayer中
通過以上4步,將紋理ID和MediaPlayer進(jìn)行關(guān)聯(lián)炬丸。
②. 接收畫面解析完畢的回調(diào)
當(dāng)視頻幀解析完畢時(shí)瘫寝,MediaPlayer會(huì)通過層層的接口調(diào)用到SurfaceTexture的onFrameAvailable接口,這時(shí)候我們可以標(biāo)志下畫面已經(jīng)解析完畢稠炬。
/**
* MediaPlayer有新的畫面幀刷新時(shí)焕阿,通過SurfaceTexture的onFrameAvailable接口進(jìn)行回調(diào)
*/
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
updateSurface = true
}
③. 驅(qū)動(dòng)畫面更新紋理ID
override fun onDrawFrame(glUnused: GL10) {
if (updateSurface) {
// 當(dāng)有畫面幀解析完畢時(shí),驅(qū)動(dòng)SurfaceTexture更新紋理ID到最近一幀解析完的畫面首启,并且驅(qū)動(dòng)底層去解析下一幀畫面
surfaceTexture!!.updateTexImage()
updateSurface = false
}
// ...之后通過紋理ID繪制畫面
}
在這里必須將updateTexImage放在onDrawFrame中進(jìn)行調(diào)用暮屡,而不能放在第二步的onFrameAvailable方法中,因?yàn)檫@個(gè)方法內(nèi)部介紹了毅桃,必須是在GL線程中進(jìn)行調(diào)用褒纲,而不能在主線程中。而GL線程的生命周期方法有onSurfaceCreated钥飞、onSurfaceChanged莺掠、onDrawFrame,想要不停地更新畫面读宙,自然是放在onDrawFrame中最為合適彻秆。
二. GL渲染視頻畫面
之前課程中,我們渲染的紋理都是2D紋理论悴,而在紋理繪制的課程中掖棉,有介紹到紋理單元是包括多種類型的紋理目標(biāo),包括了GL_TEXTURE_1D膀估、GL_TEXTURE_2D幔亥、CUBE_MAP、GL_TEXTURE_OES察纯,而本節(jié)課用到了GL_TEXTURE_OES這個(gè)類型的紋理目標(biāo)帕棉。
要啟用GL_TEXTURE_OES這種拓展類型的紋理目標(biāo),需要對之前課程中的2D紋理進(jìn)行以下修改饼记。
1. 修改紋理類型聲明
private const val FRAGMENT_SHADER = """
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 v_TexCoord;
uniform samplerExternalOES u_TextureUnit;
void main() {
gl_FragColor = texture2D(u_TextureUnit, v_TexCoord);
}
"""
在編寫Shader時(shí)香伴,需要聲明當(dāng)前使用的是拓展紋理GL_OES_EGL_image_external,然后采樣器類型必須是samplerExternalOES具则。
另外經(jīng)過測試即纲,在Fragment Shader中,不能同時(shí)啟用2D紋理和OES紋理的繪制博肋,即使只是聲明而不執(zhí)行也是不可以的低斋。
2. 創(chuàng)建紋理蜂厅、繪制紋理
創(chuàng)建紋理、繪制紋理時(shí)膊畴,我們綁定的紋理目標(biāo)類型必須改為是OES類型的掘猿。
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId)
在完成這一步時(shí),應(yīng)該就可以進(jìn)行視頻畫面的播放了唇跨,不過還會(huì)有些小問題稠通,比如視頻比例、方向的處理买猖,下面會(huì)為大家解決這兩個(gè)問題改橘。
三. 解決視頻角度問題
手機(jī)相機(jī)拍攝出來的文件,若帶了Exif文件信息政勃,那么就需要對文件解析展示的過程中進(jìn)行處理唧龄。否則就會(huì)出現(xiàn)和我們預(yù)期不一致的效果。
可交換圖像文件格式(英語:Exchangeable image file format奸远,官方簡稱Exif),是專門為數(shù)碼相機(jī)的照片設(shè)定的讽挟,可以記錄數(shù)碼照片的屬性信息和拍攝數(shù)據(jù)懒叛。
角度問題效果如下圖:
視頻Exif信息包含了視頻的方向,通過以下方法可以獲取到耽梅。
/**
* 初始化視頻信息
*/
fun initMetadata() {
if (TextUtils.isEmpty(path)) {
return
}
try {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(path)
degrees = getInteger(retriever.extractMetadata(METADATA_KEY_VIDEO_ROTATION))
duration = getInteger(retriever.extractMetadata(METADATA_KEY_DURATION))
bitRate = getInteger(retriever.extractMetadata(METADATA_KEY_BITRATE))
width = getInteger(retriever.extractMetadata(METADATA_KEY_VIDEO_WIDTH))
height = getInteger(retriever.extractMetadata(METADATA_KEY_VIDEO_HEIGHT))
displayWidth = if (isDisplayRotate) height else width
displayHeight = if (isDisplayRotate) width else height
retriever.release()
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getInteger(value: String): Int {
return if (TextUtils.isEmpty(value)) 0 else Integer.valueOf(value)
}
這里獲取到的Degree信息就是視頻的角度信息薛窥,這里返回的Int值有4個(gè)可能:0、90眼姐、180诅迷、270,這代表了視頻若想要正確播放众旗,那么需要順時(shí)針方向旋轉(zhuǎn)這個(gè)角度值罢杉。
想要解決視頻方向問題,可以通過旋轉(zhuǎn)頂點(diǎn)坐標(biāo)或旋轉(zhuǎn)紋理坐標(biāo)的方式去實(shí)現(xiàn)贡歧,這里我采用的是旋轉(zhuǎn)頂點(diǎn)坐標(biāo)的方式滩租,工具類如下:
/**
* 頂點(diǎn)旋轉(zhuǎn)工具類
*
* @author Benhero
* @date 2019/2/11
*/
object VertexRotationUtil {
enum class Rotation {
NORMAL, ROTATION_90, ROTATION_180, ROTATION_270;
}
fun getRotation(rotation: Int): Rotation {
return when (rotation) {
0 -> VertexRotationUtil.Rotation.NORMAL
90 -> VertexRotationUtil.Rotation.ROTATION_90
180 -> VertexRotationUtil.Rotation.ROTATION_180
270 -> VertexRotationUtil.Rotation.ROTATION_270
else -> VertexRotationUtil.Rotation.NORMAL
}
}
fun rotate(rotation: Int, srcArray: FloatArray): FloatArray {
return VertexRotationUtil.rotate(getRotation(rotation), srcArray)
}
fun rotate(rotation: VertexRotationUtil.Rotation, srcArray: FloatArray): FloatArray {
return when (rotation) {
VertexRotationUtil.Rotation.ROTATION_90 -> floatArrayOf(
srcArray[2], srcArray[3],
srcArray[4], srcArray[5],
srcArray[6], srcArray[7],
srcArray[0], srcArray[1])
VertexRotationUtil.Rotation.ROTATION_180 -> floatArrayOf(
srcArray[4], srcArray[5],
srcArray[6], srcArray[7],
srcArray[0], srcArray[1],
srcArray[2], srcArray[3])
VertexRotationUtil.Rotation.ROTATION_270 -> floatArrayOf(
srcArray[6], srcArray[7],
srcArray[0], srcArray[1],
srcArray[2], srcArray[3],
srcArray[4], srcArray[5])
else -> srcArray
}
}
}
問題解決后效果如下圖:
四. 解決視頻比例問題
在播放橫向視頻時(shí),可能會(huì)遇到下面這種情況利朵,這是由于我們之前視頻的容器都是充滿屏幕的律想,而屏幕比例和視頻的比例不一致,所以才會(huì)有拉伸的問題绍弟。所以只要調(diào)整視頻容器的方向和視頻比例一致即可技即。
問題現(xiàn)象如下圖:
這里我們借助一個(gè)自定義View來解決這個(gè)比例問題,只要將這個(gè)View作為GLSurfaceView的父容器即可樟遣。
/**
* 自動(dòng)適配比例的布局
*/
class AspectFrameLayout : FrameLayout {
private var mTargetAspect = -1.0
/**
* 是否自動(dòng)適配尺寸
*/
private var mIsAutoFit = true
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
fun setAspectRatio(aspectRatio: Double) {
if (aspectRatio < 0) {
throw IllegalArgumentException()
}
Log.w(TAG, "Setting aspect ratio to $aspectRatio (was $mTargetAspect)")
if (mTargetAspect != aspectRatio) {
mTargetAspect = aspectRatio
requestLayout()
}
}
fun setAutoFit(autoFit: Boolean) {
mIsAutoFit = autoFit
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var widthMeasure = widthMeasureSpec
var heightMeasure = heightMeasureSpec
if (!mIsAutoFit) {
super.onMeasure(widthMeasure, heightMeasure)
return
}
if (mTargetAspect > 0) {
var initialWidth = View.MeasureSpec.getSize(widthMeasure)
var initialHeight = View.MeasureSpec.getSize(heightMeasure)
val horizontalPadding = paddingLeft + paddingRight
val verticalPadding = paddingTop + paddingBottom
initialWidth -= horizontalPadding
initialHeight -= verticalPadding
val viewAspectRatio = initialWidth.toDouble() / initialHeight
val aspectDiff = mTargetAspect / viewAspectRatio - 1
if (Math.abs(aspectDiff) < 0.01) {
Log.w(TAG, "aspect ratio is good (target=" + mTargetAspect +
", view=" + initialWidth + "x" + initialHeight + ")")
} else {
if (aspectDiff > 0) {
initialHeight = (initialWidth / mTargetAspect).toInt()
} else {
initialWidth = (initialHeight * mTargetAspect).toInt()
}
initialWidth += horizontalPadding
initialHeight += verticalPadding
widthMeasure = View.MeasureSpec.makeMeasureSpec(initialWidth, View.MeasureSpec.EXACTLY)
heightMeasure = View.MeasureSpec.makeMeasureSpec(initialHeight, View.MeasureSpec.EXACTLY)
}
}
super.onMeasure(widthMeasure, heightMeasure)
}
companion object {
private const val TAG = "AspectFrameLayout"
}
}
最后只需要在解析到視頻文件信息而叼,讀取到視頻的比例后身笤,通過setAspectRatio將比例值設(shè)置進(jìn)這個(gè)自定義View,就可以解決比例問題澈歉。
解決效果如下圖
對于這個(gè)縱向問題的解決方案展鸡,可能有些讀者會(huì)疑惑,為什么不通過OpenGL自身來解決呢埃难?如果通過OpenGL來解決莹弊,確實(shí)是可以通過按照比例修改頂點(diǎn)坐標(biāo)參數(shù)。但是涡尘,這樣的話忍弛,會(huì)引起幾個(gè)問題:
- 視頻內(nèi)容外區(qū)域的顏色: 由于背景色會(huì)受GLES20.glClear影響,或者如果我們在當(dāng)前畫面添加了一個(gè)反色濾鏡考抄,那么示例圖中的黑色部分就會(huì)變成白色细疚,也就是說這塊不屬于視頻內(nèi)容的區(qū)域,被GL所控制川梅,這應(yīng)該不是我們想看到的產(chǎn)品效果疯兼;
- 水印坐標(biāo):如果想要在視頻畫面上添加內(nèi)容,那么我們定了水印的坐標(biāo)在視頻的右下角贫途,那么如果傳進(jìn)來的值是(1.0 - 水印寬度, -1.0 - 水印高度)吧彪,那么我們會(huì)看到的效果是,水印添加到了手機(jī)的右下角丢早,而不是視頻內(nèi)容區(qū)域的右下角姨裸,這也不是我們想看到的,這會(huì)導(dǎo)致我們需要對之后所有繪制在視頻上的圖層做坐標(biāo)換算處理怨酝,帶來很多工作量傀缩。
其他
- 本系列課程目錄詳見 簡書 - Android OpenGL ES教程規(guī)劃
- 參考資料見Android OpenGL ES學(xué)習(xí)資料所列舉的博客、資料农猬。
?GitHub工程?
本系列課程所有相關(guān)代碼請參考我的GitHub項(xiàng)目?GLStudio?赡艰,喜歡的請給個(gè)小星星。??