Browse Source

Merge pull request #113 from fengdancui/bugfix/fix-old-version-1500

修复播放老版本视频素材异常问题
hexleo 4 years ago
parent
commit
a6c165dbe8

+ 36 - 10
Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimConfigManager.kt

@@ -73,16 +73,42 @@ class AnimConfigManager(val player: AnimPlayer) {
         config?.apply {
             videoWidth = _videoWidth
             videoHeight = _videoHeight
-            if (defaultVideoMode == Constant.VIDEO_MODE_SPLIT_VERTICAL) { // 上下对齐
-                width = _videoWidth
-                height = _videoHeight / 2
-                alphaPointRect = PointRect(0, 0, width, height)
-                rgbPointRect = PointRect(0, height, width, height)
-            } else { // 默认左右对齐
-                width = _videoWidth / 2
-                height = _videoHeight
-                alphaPointRect = PointRect(0, 0, width, height)
-                rgbPointRect = PointRect(width, 0, width, height)
+            when (defaultVideoMode) {
+                Constant.VIDEO_MODE_SPLIT_HORIZONTAL -> {
+                    // 视频左右对齐(alpha左\rgb右)
+                    width = _videoWidth / 2
+                    height = _videoHeight
+                    alphaPointRect = PointRect(0, 0, width, height)
+                    rgbPointRect = PointRect(width, 0, width, height)
+                }
+                Constant.VIDEO_MODE_SPLIT_VERTICAL -> {
+                    // 视频上下对齐(alpha上\rgb下)
+                    width = _videoWidth
+                    height = _videoHeight / 2
+                    alphaPointRect = PointRect(0, 0, width, height)
+                    rgbPointRect = PointRect(0, height, width, height)
+                }
+                Constant.VIDEO_MODE_SPLIT_HORIZONTAL_REVERSE -> {
+                    // 视频左右对齐(rgb左\alpha右)
+                    width = _videoWidth / 2
+                    height = _videoHeight
+                    rgbPointRect = PointRect(0, 0, width, height)
+                    alphaPointRect = PointRect(width, 0, width, height)
+                }
+                Constant.VIDEO_MODE_SPLIT_VERTICAL_REVERSE -> {
+                    // 视频上下对齐(rgb上\alpha下)
+                    width = _videoWidth
+                    height = _videoHeight / 2
+                    rgbPointRect = PointRect(0, 0, width, height)
+                    alphaPointRect = PointRect(0, height, width, height)
+                }
+                else -> {
+                    // 默认视频左右对齐(alpha左\rgb右)
+                    width = _videoWidth / 2
+                    height = _videoHeight
+                    alphaPointRect = PointRect(0, 0, width, height)
+                    rgbPointRect = PointRect(width, 0, width, height)
+                }
             }
         }
     }

+ 6 - 2
Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/Constant.kt

@@ -25,9 +25,13 @@ object Constant {
 
     // 视频对齐方式 (兼容老版本视频模式)
     @Deprecated("Compatible older version mp4")
-    const val VIDEO_MODE_SPLIT_HORIZONTAL = 1 // 视频左右对齐
+    const val VIDEO_MODE_SPLIT_HORIZONTAL = 1 // 视频左右对齐(alpha左\rgb右)
     @Deprecated("Compatible older version mp4")
-    const val VIDEO_MODE_SPLIT_VERTICAL = 2 // 视频上下对齐
+    const val VIDEO_MODE_SPLIT_VERTICAL = 2 // 视频上下对齐(alpha上\rgb下)
+    @Deprecated("Compatible older version mp4")
+    const val VIDEO_MODE_SPLIT_HORIZONTAL_REVERSE = 3 // 视频左右对齐(rgb左\alpha右)
+    @Deprecated("Compatible older version mp4")
+    const val VIDEO_MODE_SPLIT_VERTICAL_REVERSE = 4 // 视频上下对齐(rgb上\alpha下)
 
 
     const val OK = 0 // 成功

+ 9 - 5
Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/Decoder.kt

@@ -56,7 +56,7 @@ abstract class Decoder(val player: AnimPlayer) : IAnimListener {
         }
     }
 
-    var render: Render? = null
+    var render: IRenderListener? = null
     val renderThread = HandlerHolder(null, null)
     val decodeThread = HandlerHolder(null, null)
     private var surfaceWidth = 0
@@ -83,16 +83,20 @@ abstract class Decoder(val player: AnimPlayer) : IAnimListener {
         return createThread(renderThread, "anim_render_thread") && createThread(decodeThread, "anim_decode_thread")
     }
 
-    fun prepareRender(): Boolean {
+    fun prepareRender(needYUV: Boolean): Boolean {
         if (render == null) {
             ALog.i(TAG, "prepareRender")
             player.animView.getSurfaceTexture()?.apply {
-                render = Render(this).apply {
-                    updateViewPort(surfaceWidth, surfaceHeight)
+                if (needYUV) {
+                    ALog.i(TAG, "use yuv render")
+                    render = YUVRender(this)
+                } else {
+                    render = Render(this).apply {
+                        updateViewPort(surfaceWidth, surfaceHeight)
+                    }
                 }
             }
         }
-        render?.createTexture()
         return render != null
     }
 

+ 113 - 33
Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/HardDecoder.kt

@@ -17,6 +17,7 @@ package com.tencent.qgame.animplayer
 
 import android.graphics.SurfaceTexture
 import android.media.MediaCodec
+import android.media.MediaCodecInfo
 import android.media.MediaExtractor
 import android.media.MediaFormat
 import android.os.Build
@@ -36,6 +37,18 @@ class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameA
     private val bufferInfo by lazy { MediaCodec.BufferInfo() }
     private var needDestroy = false
 
+    // 动画的原始尺寸
+    private var videoWidth = 0
+    private var videoHeight = 0
+
+    // 动画对齐后的尺寸
+    private var alignWidth = 0
+    private var alignHeight = 0
+
+    // 动画是否需要走YUV渲染逻辑的标志位
+    private var needYUV = false
+    private var outputFormat: MediaFormat? = null
+
     override fun start(fileContainer: IFileContainer) {
         isStopReq = false
         needDestroy = false
@@ -45,15 +58,18 @@ class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameA
         }
     }
 
-
     override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
         if (isStopReq) return
         ALog.d(TAG, "onFrameAvailable")
+        renderData()
+    }
+
+    fun renderData() {
         renderThread.handler?.post {
             try {
                 glTexture?.apply {
                     updateTexImage()
-                    render?.renderFrame(player.configManager.config)
+                    render?.renderFrame()
                     player.pluginManager.onRendering()
                     render?.swapBuffers()
                 }
@@ -64,15 +80,6 @@ class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameA
     }
 
     private fun startPlay(fileContainer: IFileContainer) {
-        try {
-            if (!prepareRender()) {
-                throw RuntimeException("render create fail")
-            }
-        } catch (t: Throwable) {
-            onFailed(Constant.REPORT_ERROR_TYPE_CREATE_RENDER, "${Constant.ERROR_MSG_CREATE_RENDER} e=$t")
-            release(null, null)
-            return
-        }
 
         var extractor: MediaExtractor? = null
         var decoder: MediaCodec? = null
@@ -103,9 +110,28 @@ class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameA
                 }
             }
 
-            val videoWidth = format.getInteger(MediaFormat.KEY_WIDTH)
-            val videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT)
+            videoWidth = format.getInteger(MediaFormat.KEY_WIDTH)
+            videoHeight = format.getInteger(MediaFormat.KEY_HEIGHT)
+            // 防止没有INFO_OUTPUT_FORMAT_CHANGED时导致alignWidth和alignHeight不会被赋值一直是0
+            alignWidth = videoWidth
+            alignHeight = videoHeight
             ALog.i(TAG, "Video size is $videoWidth x $videoHeight")
+
+            // 由于使用mediacodec解码老版本素材时对宽度1500尺寸的视频进行数据对齐,解码后的宽度变成1504,导致采样点出现偏差播放异常
+            // 所以当开启兼容老版本视频模式并且老版本视频的宽度不能被16整除时要走YUV渲染逻辑
+            // 但是这样直接判断有风险,后期想办法改
+            needYUV = !(videoWidth % 16 == 0) && player.enableVersion1
+
+            try {
+                if (!prepareRender(needYUV)) {
+                    throw RuntimeException("render create fail")
+                }
+            } catch (t: Throwable) {
+                onFailed(Constant.REPORT_ERROR_TYPE_CREATE_RENDER, "${Constant.ERROR_MSG_CREATE_RENDER} e=$t")
+                release(null, null)
+                return
+            }
+
             preparePlay(videoWidth, videoHeight)
 
             render?.apply {
@@ -123,12 +149,20 @@ class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameA
             return
         }
 
-
         try {
             val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
             ALog.i(TAG, "Video MIME is $mime")
             decoder = MediaCodec.createDecoderByType(mime).apply {
-                configure(format, Surface(glTexture), null, 0)
+                if (needYUV) {
+                    format.setInteger(
+                            MediaFormat.KEY_COLOR_FORMAT,
+                            MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
+                    )
+                    configure(format, null, null, 0)
+                } else {
+                    configure(format, Surface(glTexture), null, 0)
+                }
+
                 start()
                 decodeThread.handler?.post {
                     try {
@@ -148,8 +182,6 @@ class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameA
         }
     }
 
-
-
     private fun startDecode(extractor: MediaExtractor ,decoder: MediaCodec) {
         val TIMEOUT_USEC = 10000L
         var inputChunk = 0
@@ -193,10 +225,14 @@ class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameA
                     decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> ALog.d(TAG, "no output from decoder available")
                     decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> ALog.d(TAG, "decoder output buffers changed")
                     decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
-                        val format = decoder.outputFormat
-                        ALog.i(TAG, "decoder output format changed: $format")
-                        // val (w,h) = formatChange(format)
-                        // videoSizeChange(w, h)
+                        outputFormat = decoder.outputFormat
+                        outputFormat?.apply {
+                            if (this.getInteger(MediaFormat.KEY_STRIDE) > 0 && this.getInteger(MediaFormat.KEY_SLICE_HEIGHT) > 0) {
+                                alignWidth = this.getInteger(MediaFormat.KEY_STRIDE)
+                                alignHeight = this.getInteger(MediaFormat.KEY_SLICE_HEIGHT)
+                            }
+                        }
+                        ALog.i(TAG, "decoder output format changed: $outputFormat")
                     }
                     decoderStatus < 0 -> {
                         throw RuntimeException("unexpected result from decoder.dequeueOutputBuffer: $decoderStatus")
@@ -213,8 +249,12 @@ class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameA
                             speedControlUtil.preRender(bufferInfo.presentationTimeUs)
                         }
 
+                        if (needYUV && doRender) {
+                            yuvProcess(decoder, decoderStatus)
+                        }
+
                         // release & render
-                        decoder.releaseOutputBuffer(decoderStatus, doRender)
+                        decoder.releaseOutputBuffer(decoderStatus, doRender && !needYUV)
 
                         if (frameIndex == 0) {
                             onVideoStart()
@@ -242,16 +282,57 @@ class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameA
 
     }
 
-    private fun formatChange(format: MediaFormat): Pair<Int, Int> {
-        try {
-            // 实际视频的纹理大小
-            val width = format.getInteger(MediaFormat.KEY_WIDTH)
-            val height = format.getInteger(MediaFormat.KEY_HEIGHT)
-            return Pair(width, height)
-        } catch (t: Throwable) {
-            ALog.e(TAG, "formatChange $t", t)
+    /**
+     * 获取到解码后每一帧的YUV数据,裁剪出正确的尺寸
+     */
+    private fun yuvProcess(decoder: MediaCodec, outputIndex: Int) {
+        val outputBuffer = decoder.outputBuffers[outputIndex]
+        outputBuffer?.let {
+            it.position(0)
+            it.limit(bufferInfo.offset + bufferInfo.size)
+            var yuvData = ByteArray(outputBuffer.remaining())
+            outputBuffer.get(yuvData)
+
+            if (yuvData.isNotEmpty()) {
+                var yData = ByteArray(videoWidth * videoHeight)
+                var uData = ByteArray(videoWidth * videoHeight / 4)
+                var vData = ByteArray(videoWidth * videoHeight / 4)
+
+                if (outputFormat?.getInteger(MediaFormat.KEY_COLOR_FORMAT) == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) {
+                    yuvData = yuv420spTop(yuvData)
+                }
+
+                yuvCopy(yuvData, 0, alignWidth, alignHeight, yData, videoWidth, videoHeight)
+                yuvCopy(yuvData, alignWidth * alignHeight, alignWidth / 2, alignHeight / 2, uData, videoWidth / 2, videoHeight / 2)
+                yuvCopy(yuvData, alignWidth * alignHeight * 5 / 4, alignWidth / 2, alignHeight / 2, vData, videoWidth / 2, videoHeight / 2)
+
+                render?.setYUVData(videoWidth, videoHeight, yData, uData, vData)
+                renderData()
+            }
+        }
+    }
+
+    private fun yuv420spTop(yuv420sp: ByteArray): ByteArray {
+        val yuv420p = ByteArray(yuv420sp.size)
+        val ySize = alignWidth * alignHeight
+        System.arraycopy(yuv420sp, 0, yuv420p, 0, alignWidth * alignHeight)
+        var i = ySize
+        var j = ySize
+        while (i < ySize * 3 / 2) {
+            yuv420p[j] = yuv420sp[i]
+            yuv420p[j + ySize / 4] = yuv420sp[i + 1]
+            i += 2
+            j++
+        }
+        return yuv420p
+    }
+
+    private fun yuvCopy(src: ByteArray, srcOffset: Int, inWidth: Int, inHeight: Int, dest: ByteArray, outWidth: Int, outHeight: Int) {
+        for (h in 0 until inHeight) {
+            if (h < outHeight) {
+                System.arraycopy(src, srcOffset + h * inWidth, dest, h * outWidth, outWidth)
+            }
         }
-        return Pair(0,0)
     }
 
     private fun release(decoder: MediaCodec?, extractor: MediaExtractor?) {
@@ -280,7 +361,6 @@ class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameA
         }
     }
 
-
     override fun destroy() {
         needDestroy = true
         if (isRunning) {
@@ -293,7 +373,7 @@ class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameA
     private fun destroyInner() {
         renderThread.handler?.post {
             player.pluginManager.onDestroy()
-            render?.destroy()
+            render?.destroyRender()
             render = null
             onVideoDestroy()
             destroyThread()

+ 54 - 0
Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/IRenderListener.kt

@@ -0,0 +1,54 @@
+/*
+ * Tencent is pleased to support the open source community by making vap available.
+ *
+ * Copyright (C) 2020 THL A29 Limited, a Tencent company.  All rights reserved.
+ *
+ * Licensed under the MIT License (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/MIT
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.tencent.qgame.animplayer
+
+interface IRenderListener {
+
+    /**
+     * 初始化渲染环境,获取shader字段,创建绑定纹理
+     */
+    fun initRender()
+
+    /**
+     * 渲染上屏
+     */
+    fun renderFrame()
+
+    fun clearFrame()
+
+    /**
+     * 释放纹理
+     */
+    fun destroyRender()
+
+    /**
+     * 设置视频配置
+     */
+    fun setAnimConfig(config: AnimConfig)
+
+    /**
+     * 显示区域大小变化
+     */
+    fun updateViewPort(width: Int, height: Int) {}
+
+    fun getExternalTexture(): Int
+
+    fun releaseTexture()
+
+    fun swapBuffers()
+
+    fun setYUVData(width: Int, height: Int, y: ByteArray?, u: ByteArray?, v: ByteArray?) {}
+}

+ 36 - 43
Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/Render.kt

@@ -26,7 +26,7 @@ import java.nio.ByteBuffer
 import java.nio.ByteOrder
 import java.nio.ShortBuffer
 
-class Render(surfaceTexture: SurfaceTexture) {
+class Render(surfaceTexture: SurfaceTexture): IRenderListener {
 
     companion object {
         private const val TAG = "${Constant.TAG}.Render"
@@ -48,11 +48,7 @@ class Render(surfaceTexture: SurfaceTexture) {
 
     init {
         eglUtil.start(surfaceTexture)
-        initGL()
-    }
-
-    private fun initGL() {
-        compileShader()
+        initRender()
     }
 
     private fun setVertexBuf(config: AnimConfig) {
@@ -66,7 +62,13 @@ class Render(surfaceTexture: SurfaceTexture) {
         rgbArray.setArray(rgb)
     }
 
-    fun createTexture() {
+    override fun initRender() {
+        shaderProgram = ShaderUtil.createProgram(RenderConstant.VERTEX_SHADER, RenderConstant.FRAGMENT_SHADER)
+        uTextureLocation = GLES20.glGetUniformLocation(shaderProgram, "texture")
+        aPositionLocation = GLES20.glGetAttribLocation(shaderProgram, "vPosition")
+        aTextureAlphaLocation = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinateAlpha")
+        aTextureRgbLocation = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinateRgb")
+
         GLES20.glGenTextures(genTexture.size, genTexture, 0)
         GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, genTexture[0])
         GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST.toFloat())
@@ -75,18 +77,35 @@ class Render(surfaceTexture: SurfaceTexture) {
         GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
     }
 
-    private fun compileShader() {
-        shaderProgram = ShaderUtil.createProgram(RenderConstant.VERTEX_SHADER, RenderConstant.FRAGMENT_SHADER)
-        uTextureLocation = GLES20.glGetUniformLocation(shaderProgram, "texture")
-        aPositionLocation = GLES20.glGetAttribLocation(shaderProgram, "vPosition")
-        aTextureAlphaLocation = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinateAlpha")
-        aTextureRgbLocation = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinateRgb")
+    override fun renderFrame() {
+        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
+        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+        if (surfaceSizeChanged && surfaceWidth>0 && surfaceHeight>0) {
+            surfaceSizeChanged = false
+            GLES20.glViewport(0,0, surfaceWidth, surfaceHeight)
+        }
+        draw()
+    }
+
+    override fun clearFrame() {
+        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
+        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+        eglUtil.swapBuffers()
+    }
+
+    override fun destroyRender() {
+        releaseTexture()
+        eglUtil.release()
+    }
+
+    override fun releaseTexture() {
+        GLES20.glDeleteTextures(genTexture.size, genTexture, 0)
     }
 
     /**
      * 设置视频配置
      */
-    fun setAnimConfig(config: AnimConfig) {
+    override fun setAnimConfig(config: AnimConfig) {
         setVertexBuf(config)
         setTexCoords(config)
     }
@@ -94,47 +113,21 @@ class Render(surfaceTexture: SurfaceTexture) {
     /**
      * 显示区域大小变化
      */
-    fun updateViewPort(width: Int, height: Int) {
+    override fun updateViewPort(width: Int, height: Int) {
         if (width <=0 || height <=0) return
         surfaceSizeChanged = true
         surfaceWidth = width
         surfaceHeight = height
     }
 
-    fun clearFrame() {
-        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
-        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+    override fun swapBuffers() {
         eglUtil.swapBuffers()
     }
 
-    fun renderFrame(config: AnimConfig?) {
-        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
-        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
-        if (surfaceSizeChanged && surfaceWidth>0 && surfaceHeight>0) {
-            surfaceSizeChanged = false
-            GLES20.glViewport(0,0, surfaceWidth, surfaceHeight)
-        }
-        draw()
-    }
-
-    fun swapBuffers() {
-        eglUtil.swapBuffers()
-    }
-
-
-    fun destroy() {
-        releaseTexture()
-        eglUtil.release()
-    }
-
-    fun releaseTexture() {
-        GLES20.glDeleteTextures(genTexture.size, genTexture, 0)
-    }
-
     /**
      * mediaCodec渲染使用的
      */
-    fun getExternalTexture(): Int {
+    override fun getExternalTexture(): Int {
         return genTexture[0]
     }
 

+ 207 - 0
Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/YUVRender.kt

@@ -0,0 +1,207 @@
+/*
+ * Tencent is pleased to support the open source community by making vap available.
+ *
+ * Copyright (C) 2020 THL A29 Limited, a Tencent company.  All rights reserved.
+ *
+ * Licensed under the MIT License (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/MIT
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.tencent.qgame.animplayer
+
+import android.graphics.SurfaceTexture
+import android.opengl.GLES20
+import com.tencent.qgame.animplayer.util.GlFloatArray
+import com.tencent.qgame.animplayer.util.ShaderUtil.createProgram
+import com.tencent.qgame.animplayer.util.TexCoordsUtil
+import com.tencent.qgame.animplayer.util.VertexUtil
+import java.nio.ByteBuffer
+import java.nio.FloatBuffer
+
+class YUVRender (surfaceTexture: SurfaceTexture): IRenderListener {
+
+    companion object {
+        private const val TAG = "${Constant.TAG}.YUVRender"
+    }
+
+    private val vertexArray = GlFloatArray()
+    private val alphaArray = GlFloatArray()
+    private val rgbArray = GlFloatArray()
+
+    private var shaderProgram = 0
+
+    //顶点位置
+    private var avPosition = 0
+
+    //rgb纹理位置
+    private var rgbPosition = 0
+
+    //alpha纹理位置
+    private var alphaPosition = 0
+
+    //shader  yuv变量
+    private var samplerY = 0
+    private var samplerU = 0
+    private var samplerV = 0
+    private var textureId = IntArray(3)
+    private var convertMatrixUniform = 0
+    private var convertOffsetUniform = 0
+
+    //YUV数据
+    private var widthYUV = 0
+    private var heightYUV = 0
+    private var y: ByteBuffer? = null
+    private var u: ByteBuffer? = null
+    private var v: ByteBuffer? = null
+
+    private val eglUtil: EGLUtil = EGLUtil()
+
+    // 像素数据向GPU传输时默认以4字节对齐
+    private var unpackAlign = 4
+
+    // YUV offset
+    private val YUV_OFFSET = floatArrayOf(
+            0f, -0.501960814f, -0.501960814f
+    )
+
+    // RGB coefficients
+    private val YUV_MATRIX = floatArrayOf(
+            1f, 1f, 1f,
+            0f, -0.3441f, 1.772f,
+            1.402f, -0.7141f, 0f
+    )
+
+    init {
+        eglUtil.start(surfaceTexture)
+        initRender()
+    }
+
+    override fun initRender() {
+        shaderProgram = createProgram(YUVShader.VERTEX_SHADER, YUVShader.FRAGMENT_SHADER)
+        //获取顶点坐标字段
+        avPosition = GLES20.glGetAttribLocation(shaderProgram, "v_Position")
+        //获取纹理坐标字段
+        rgbPosition = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinateRgb")
+        alphaPosition = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinateAlpha")
+
+        //获取yuv字段
+        samplerY = GLES20.glGetUniformLocation(shaderProgram, "sampler_y")
+        samplerU = GLES20.glGetUniformLocation(shaderProgram, "sampler_u")
+        samplerV = GLES20.glGetUniformLocation(shaderProgram, "sampler_v")
+        convertMatrixUniform = GLES20.glGetUniformLocation(shaderProgram, "convertMatrix")
+        convertOffsetUniform = GLES20.glGetUniformLocation(shaderProgram, "offset")
+        //创建3个纹理
+        GLES20.glGenTextures(textureId.size, textureId, 0)
+
+        //绑定纹理
+        for (id in textureId) {
+            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, id)
+            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT)
+            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT)
+            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
+            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
+        }
+    }
+
+    override fun renderFrame() {
+        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
+        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+        draw()
+    }
+
+    override fun clearFrame() {
+        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
+        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+        eglUtil.swapBuffers()
+    }
+
+    override fun destroyRender() {
+        releaseTexture()
+        eglUtil.release()
+    }
+
+    override fun setAnimConfig(config: AnimConfig) {
+        vertexArray.setArray(VertexUtil.create(config.width, config.height, PointRect(0, 0, config.width, config.height), vertexArray.array))
+        val alpha = TexCoordsUtil.create(config.videoWidth, config.videoHeight, config.alphaPointRect, alphaArray.array)
+        val rgb = TexCoordsUtil.create(config.videoWidth, config.videoHeight, config.rgbPointRect, rgbArray.array)
+        alphaArray.setArray(alpha)
+        rgbArray.setArray(rgb)
+    }
+
+    override fun getExternalTexture(): Int {
+        return textureId[0]
+    }
+
+    override fun releaseTexture() {
+        GLES20.glDeleteTextures(textureId.size, textureId, 0)
+    }
+
+    override fun swapBuffers() {
+        eglUtil.swapBuffers()
+    }
+
+    override fun setYUVData(width: Int, height: Int, y: ByteArray?, u: ByteArray?, v: ByteArray?) {
+        widthYUV = width
+        heightYUV = height
+        this.y = ByteBuffer.wrap(y)
+        this.u = ByteBuffer.wrap(u)
+        this.v = ByteBuffer.wrap(v)
+
+        // 当视频帧的u或者v分量的宽度不能被4整除时,用默认的4字节对齐会导致存取最后一行时越界,所以在向GPU传输数据前指定对齐方式
+        if ((widthYUV / 2) % 4 != 0) {
+            this.unpackAlign = if ((widthYUV / 2) % 2 == 0) 2 else 1
+        }
+    }
+
+    private fun draw() {
+        if (widthYUV > 0 && heightYUV > 0 && y != null && u != null && v != null) {
+            GLES20.glUseProgram(shaderProgram)
+            vertexArray.setVertexAttribPointer(avPosition)
+            alphaArray.setVertexAttribPointer(alphaPosition)
+            rgbArray.setVertexAttribPointer(rgbPosition)
+
+            GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, unpackAlign)
+
+            //激活纹理0来绑定y数据
+            GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
+            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0])
+            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, widthYUV, heightYUV, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, y)
+
+            //激活纹理1来绑定u数据
+            GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
+            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[1])
+            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, widthYUV / 2, heightYUV / 2, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, u)
+
+            //激活纹理2来绑定v数据
+            GLES20.glActiveTexture(GLES20.GL_TEXTURE2)
+            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[2])
+            GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, widthYUV / 2, heightYUV / 2, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, v)
+
+            //给fragment_shader里面yuv变量设置值   0 1 标识纹理x
+            GLES20.glUniform1i(samplerY, 0)
+            GLES20.glUniform1i(samplerU, 1)
+            GLES20.glUniform1i(samplerV, 2)
+
+            GLES20.glUniform3fv(convertOffsetUniform, 1, FloatBuffer.wrap(YUV_OFFSET))
+            GLES20.glUniformMatrix3fv(convertMatrixUniform, 1, false, YUV_MATRIX, 0)
+
+            //绘制
+            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
+            y?.clear()
+            u?.clear()
+            v?.clear()
+            y = null
+            u = null
+            v = null
+            GLES20.glDisableVertexAttribArray(avPosition)
+            GLES20.glDisableVertexAttribArray(rgbPosition)
+            GLES20.glDisableVertexAttribArray(alphaPosition)
+        }
+    }
+}

+ 60 - 0
Android/PlayerProj/animplayer/src/main/java/com/tencent/qgame/animplayer/YUVShader.kt

@@ -0,0 +1,60 @@
+/*
+ * Tencent is pleased to support the open source community by making vap available.
+ *
+ * Copyright (C) 2020 THL A29 Limited, a Tencent company.  All rights reserved.
+ *
+ * Licensed under the MIT License (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/MIT
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.tencent.qgame.animplayer
+
+object YUVShader {
+
+    const val VERTEX_SHADER = "attribute vec4 v_Position;\n" +
+            "attribute vec2 vTexCoordinateAlpha;\n" +
+            "attribute vec2 vTexCoordinateRgb;\n" +
+            "varying vec2 v_TexCoordinateAlpha;\n" +
+            "varying vec2 v_TexCoordinateRgb;\n" +
+            "\n" +
+            "void main() {\n" +
+            "    v_TexCoordinateAlpha = vTexCoordinateAlpha;\n" +
+            "    v_TexCoordinateRgb = vTexCoordinateRgb;\n" +
+            "    gl_Position = v_Position;\n" +
+            "}"
+
+    const val FRAGMENT_SHADER = "precision mediump float;\n" +
+            "uniform sampler2D sampler_y;\n" +
+            "uniform sampler2D sampler_u;\n" +
+            "uniform sampler2D sampler_v;\n" +
+            "varying vec2 v_TexCoordinateAlpha;\n" +
+            "varying vec2 v_TexCoordinateRgb;\n" +
+            "uniform mat3 convertMatrix;\n" +
+            "uniform vec3 offset;\n" +
+            "\n" +
+            "void main() {\n" +
+            "   highp vec3 yuvColorAlpha;\n" +
+            "   highp vec3 yuvColorRGB;\n" +
+            "   highp vec3 rgbColorAlpha;\n" +
+            "   highp vec3 rgbColorRGB;\n" +
+            "   yuvColorAlpha.x = texture2D(sampler_y,v_TexCoordinateAlpha).r;\n" +
+            "   yuvColorRGB.x = texture2D(sampler_y,v_TexCoordinateRgb).r;\n" +
+            "   yuvColorAlpha.y = texture2D(sampler_u,v_TexCoordinateAlpha).r;\n" +
+            "   yuvColorAlpha.z = texture2D(sampler_v,v_TexCoordinateAlpha).r;\n" +
+            "   yuvColorRGB.y = texture2D(sampler_u,v_TexCoordinateRgb).r;\n" +
+            "   yuvColorRGB.z = texture2D(sampler_v,v_TexCoordinateRgb).r;\n" +
+            "   yuvColorAlpha += offset;\n" +
+            "   yuvColorRGB += offset;\n" +
+            "   rgbColorAlpha = convertMatrix * yuvColorAlpha; \n" +
+            "   rgbColorRGB = convertMatrix * yuvColorRGB; \n" +
+            "   gl_FragColor=vec4(rgbColorRGB, rgbColorAlpha.r);\n" +
+            "}"
+
+    // RGB2*Alpha+RGB1*(1-Alpha)
+}

+ 1 - 1
Android/PlayerProj/app/src/main/AndroidManifest.xml

@@ -22,7 +22,7 @@
         <activity android:name=".player.AnimSimpleDemoActivity" android:screenOrientation="portrait"/>
         <activity android:name=".player.AnimVapxDemoActivity" android:screenOrientation="portrait"/>
         <activity android:name=".player.AnimActiveDemoActivity" android:screenOrientation="portrait"/>
-
+        <activity android:name=".player.AnimSpecialSizeDemoActivity" android:screenOrientation="portrait"/>
 
     </application>
 

BIN
Android/PlayerProj/app/src/main/assets/special_size_750.mp4


+ 4 - 0
Android/PlayerProj/app/src/main/java/com/tencent/qgame/playerproj/MainActivity.kt

@@ -20,6 +20,7 @@ import android.content.Intent
 import android.os.Bundle
 import com.tencent.qgame.playerproj.player.AnimActiveDemoActivity
 import com.tencent.qgame.playerproj.player.AnimSimpleDemoActivity
+import com.tencent.qgame.playerproj.player.AnimSpecialSizeDemoActivity
 import com.tencent.qgame.playerproj.player.AnimVapxDemoActivity
 import kotlinx.android.synthetic.main.activity_main.*
 
@@ -38,6 +39,9 @@ class MainActivity : Activity(){
         btn3.setOnClickListener {
             startActivity(Intent(this, AnimActiveDemoActivity::class.java))
         }
+        btn4.setOnClickListener {
+            startActivity(Intent(this, AnimSpecialSizeDemoActivity::class.java))
+        }
     }
 
 

+ 228 - 0
Android/PlayerProj/app/src/main/java/com/tencent/qgame/playerproj/player/AnimSpecialSizeDemoActivity.kt

@@ -0,0 +1,228 @@
+/*
+ * Tencent is pleased to support the open source community by making vap available.
+ *
+ * Copyright (C) 2020 THL A29 Limited, a Tencent company.  All rights reserved.
+ *
+ * Licensed under the MIT License (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://opensource.org/licenses/MIT
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.tencent.qgame.playerproj.player
+
+import android.app.Activity
+import android.content.Context
+import android.os.Bundle
+import android.os.Environment
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.View
+import com.tencent.qgame.animplayer.AnimConfig
+import com.tencent.qgame.animplayer.AnimView
+import com.tencent.qgame.animplayer.Constant
+import com.tencent.qgame.animplayer.inter.IAnimListener
+import com.tencent.qgame.animplayer.util.ALog
+import com.tencent.qgame.animplayer.util.IALog
+import com.tencent.qgame.animplayer.util.ScaleType
+import com.tencent.qgame.playerproj.R
+import kotlinx.android.synthetic.main.activity_anim_simple_demo.*
+import java.io.File
+
+/**
+ * 播放宽高不是16的倍数的特殊尺寸的动画demo,这里以special_size_750.mp4为例,size = 750 x 814
+ */
+class AnimSpecialSizeDemoActivity : Activity(), IAnimListener {
+
+    companion object {
+        private const val TAG = "AnimSpecialSizeActivity"
+    }
+
+    private val dir by lazy {
+        // 存放在sdcard应用缓存文件中
+        getExternalFilesDir(null)?.absolutePath ?: Environment.getExternalStorageDirectory().path
+    }
+
+    // 视频信息
+    data class VideoInfo(val fileName: String,val md5:String)
+
+    // ps:每次修改mp4文件,但文件名不变,记得先卸载app,因为assets同名文件不会进行替换
+    private val videoInfo = VideoInfo("special_size_750.mp4", "2acde1639ad74b8bd843083246902e23")
+
+    // 动画View
+    private lateinit var animView: AnimView
+
+    private val uiHandler by lazy {
+        Handler(Looper.getMainLooper())
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_anim_simple_demo)
+        // 文件加载完成后会调用init方法
+        loadFile()
+    }
+
+    private fun init() {
+        // 初始化日志
+        initLog()
+        // 初始化调试开关
+        initTestView()
+        // 获取动画view
+        animView = playerView
+        // 视频左右对齐(rgb左\alpha右)
+        animView.setVideoMode(Constant.VIDEO_MODE_SPLIT_HORIZONTAL_REVERSE)
+        // 兼容老版本视频资源
+        animView.enableVersion1(true)
+        // 居中(根据父布局按比例居中并全部显示,默认fitXY)
+        animView.setScaleType(ScaleType.FIT_CENTER)
+        // 注册动画监听
+        animView.setAnimListener(this)
+        /**
+         * 开始播放主流程
+         * ps: 主要流程都是对AnimView的操作,其它比如队列,或改变窗口大小等操作都不是必须的
+         */
+        play(videoInfo)
+    }
+
+
+    private fun play(videoInfo: VideoInfo) {
+        // 播放前强烈建议检查文件的md5是否有改变
+        // 因为下载或文件存储过程中会出现文件损坏,导致无法播放
+        Thread {
+            val file = File(dir + "/" + videoInfo.fileName)
+            val md5 = FileUtil.getFileMD5(file)
+            if (videoInfo.md5 == md5) {
+                // 开始播放动画文件
+                animView.startPlay(file)
+            } else {
+                Log.e(TAG, "md5 is not match, error md5=$md5")
+            }
+        }.start()
+    }
+
+
+    /**
+     * 视频信息准备好后的回调,用于检查视频准备好后是否继续播放
+     * @return true 继续播放 false 停止播放
+     */
+    override fun onVideoConfigReady(config: AnimConfig): Boolean {
+
+        uiHandler.post {
+            val w = dp2px(this,400f).toInt()
+            val lp = animView.layoutParams
+            lp.width = w
+            lp.height = (w * config.height *1f / config.width).toInt()
+            animView.layoutParams = lp
+        }
+        return true
+    }
+
+    /**
+     * 视频开始回调
+     */
+    override fun onVideoStart() {
+        Log.i(TAG, "onVideoStart")
+    }
+
+    /**
+     * 视频渲染每一帧时的回调
+     * @param frameIndex 帧索引
+     */
+    override fun onVideoRender(frameIndex: Int, config: AnimConfig?) {
+    }
+
+    /**
+     * 视频播放结束(失败也会回调onComplete)
+     */
+    override fun onVideoComplete() {
+        Log.i(TAG, "onVideoComplete")
+    }
+
+    /**
+     * 播放器被销毁情况下会调用onVideoDestroy
+     */
+    override fun onVideoDestroy() {
+        Log.i(TAG, "onVideoDestroy")
+    }
+
+    /**
+     * 失败回调
+     * 一次播放时可能会调用多次,建议onFailed只做错误上报
+     * @param errorType 错误类型
+     * @param errorMsg 错误消息
+     */
+    override fun onFailed(errorType: Int, errorMsg: String?) {
+        Log.i(TAG, "onFailed errorType=$errorType errorMsg=$errorMsg")
+    }
+
+
+
+    override fun onPause() {
+        super.onPause()
+        // 页面切换是停止播放
+        animView.stopPlay()
+    }
+
+
+    private fun initLog() {
+        ALog.isDebug = false
+        ALog.log = object : IALog {
+            override fun i(tag: String, msg: String) {
+                Log.i(tag, msg)
+            }
+
+            override fun d(tag: String, msg: String) {
+                Log.d(tag, msg)
+            }
+
+            override fun e(tag: String, msg: String) {
+                Log.e(tag, msg)
+            }
+
+            override fun e(tag: String, msg: String, tr: Throwable) {
+                Log.e(tag, msg, tr)
+            }
+        }
+    }
+
+
+    private fun initTestView() {
+        btnLayout.visibility = View.VISIBLE
+        /**
+         * 开始播放按钮
+         */
+        btnPlay.setOnClickListener {
+            play(videoInfo)
+        }
+        /**
+         * 结束视频按钮
+         */
+        btnStop.setOnClickListener {
+            animView.stopPlay()
+        }
+    }
+
+    private fun loadFile() {
+        val files = Array(1) {
+            videoInfo.fileName
+        }
+        FileUtil.copyAssetsToStorage(this, dir, files) {
+            uiHandler.post {
+                init()
+            }
+        }
+    }
+
+
+    private fun dp2px(context: Context, dp: Float): Float {
+        val scale = context.resources.displayMetrics.density
+        return dp * scale + 0.5f
+    }
+}
+

+ 25 - 20
Android/PlayerProj/app/src/main/res/layout/activity_main.xml

@@ -10,30 +10,35 @@
     <LinearLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:orientation="vertical"
         android:layout_gravity="center"
-        >
+        android:orientation="vertical">
+
+        <Button
+            android:id="@+id/btn1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Simple Demo" />
+
+        <Button
+            android:id="@+id/btn2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="VAPX Demo(融合动画)" />
+
+        <Button
+            android:id="@+id/btn3"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Active Demo(可变动画)" />
+
+        <Button
+            android:id="@+id/btn4"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Special Size Demo" />
 
-    <Button
-        android:id="@+id/btn1"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="Simple Demo" />
-
-    <Button
-        android:id="@+id/btn2"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="VAPX Demo(融合动画)" />
-
-    <Button
-        android:id="@+id/btn3"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="Active Demo(可变动画)" />
     </LinearLayout>
 
-
 </FrameLayout>