Android OpenGL ES 10.1 視頻播放器

課程介紹

在學(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)侥蒙,生命周期流程圖如下:

mediaplayer_state_diagram

播放器生命周期的邏輯處理暗膜,詳見工程代碼。這里重點(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)

這里分為以下幾步:

  1. 創(chuàng)建紋理ID聚磺,用于GL渲染
  2. 通過紋理ID創(chuàng)建一個(gè)SurfaceTexture對象
  3. 通過SurfaceTexture對象創(chuàng)建一個(gè)Surface對象
  4. 將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)象如下圖:


視頻比例問題.png

這里我們借助一個(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è)問題:

  1. 視頻內(nèi)容外區(qū)域的顏色: 由于背景色會(huì)受GLES20.glClear影響,或者如果我們在當(dāng)前畫面添加了一個(gè)反色濾鏡考抄,那么示例圖中的黑色部分就會(huì)變成白色细疚,也就是說這塊不屬于視頻內(nèi)容的區(qū)域,被GL所控制川梅,這應(yīng)該不是我們想看到的產(chǎn)品效果疯兼;
  2. 水印坐標(biāo):如果想要在視頻畫面上添加內(nèi)容,那么我們定了水印的坐標(biāo)在視頻的右下角贫途,那么如果傳進(jìn)來的值是(1.0 - 水印寬度, -1.0 - 水印高度)吧彪,那么我們會(huì)看到的效果是,水印添加到了手機(jī)的右下角丢早,而不是視頻內(nèi)容區(qū)域的右下角姨裸,這也不是我們想看到的,這會(huì)導(dǎo)致我們需要對之后所有繪制在視頻上的圖層做坐標(biāo)換算處理怨酝,帶來很多工作量傀缩。

其他

?GitHub工程?

本系列課程所有相關(guān)代碼請參考我的GitHub項(xiàng)目?GLStudio?赡艰,喜歡的請給個(gè)小星星。??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末盛险,一起剝皮案震驚了整個(gè)濱河市瞄摊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌苦掘,老刑警劉巖换帜,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鹤啡,居然都是意外死亡惯驼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來祟牲,“玉大人隙畜,你說我怎么就攤上這事∷当矗” “怎么了议惰?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長乡恕。 經(jīng)常有香客問我言询,道長,這世上最難降的妖魔是什么傲宜? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任运杭,我火速辦了婚禮,結(jié)果婚禮上函卒,老公的妹妹穿的比我還像新娘辆憔。我一直安慰自己,他們只是感情好报嵌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布虱咧。 她就那樣靜靜地躺著,像睡著了一般锚国。 火紅的嫁衣襯著肌膚如雪彤钟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天跷叉,我揣著相機(jī)與錄音,去河邊找鬼营搅。 笑死云挟,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的转质。 我是一名探鬼主播园欣,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼休蟹!你這毒婦竟也來了沸枯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤赂弓,失蹤者是張志新(化名)和其女友劉穎绑榴,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盈魁,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡翔怎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赤套。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡飘痛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出容握,到底是詐尸還是另有隱情宣脉,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布剔氏,位于F島的核電站塑猖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏介蛉。R本人自食惡果不足惜萌庆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望币旧。 院中可真熱鬧践险,春花似錦、人聲如沸吹菱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鳍刷。三九已至占遥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間输瓜,已是汗流浹背瓦胎。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留尤揣,地道東北人搔啊。 一個(gè)月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像北戏,于是被迫代替她去往敵國和親负芋。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353