更换vap库

This commit is contained in:
2025-12-03 09:09:57 +08:00
parent 10e5504ce1
commit dc9726d5b4
56 changed files with 5063 additions and 9 deletions

View File

@@ -3195,12 +3195,8 @@ class RoomActivity : BaseMvpActivity<RoomPresenter?, ActivityRoomBinding?>(),
}
}
private var isSwitchRoom: Boolean = false
// TODO:不进入 2025/8/26 加入房间
override fun roomInfo(resp: RoomInfoResp) {
if (!isSwitchRoom)
return
getHour()
mRoomInfoResp = resp
isOnline = true
@@ -3608,7 +3604,6 @@ class RoomActivity : BaseMvpActivity<RoomPresenter?, ActivityRoomBinding?>(),
ClickUtils.clearAllClickRecords()
AgoraManager.getInstance().cleanup()
roomId = roomId2
isSwitchRoom = true
// 重新连接房间相关服务
resumeRoomState()
publicScreenFragment?.onDestroy()

View File

@@ -69,7 +69,7 @@ class RoomMentorShipFragment(var mRoomInfo: RoomInfoResp?) :
private val timer = CountdownTimer()
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val startOrDelay = arrayOf("开始", "")
private val startOrDelay = arrayOf("开始", "")
private var mUserInfo: RoomUserBean? = mRoomInfo?.user_info

View File

@@ -75,7 +75,7 @@ class RoomMentorShipWheatView : BaseWheatView {
mTvName?.text = bean.nickname
} else {
mTvName.visibility = VISIBLE
mCharmView.visibility = GONE
mCharmView.visibility = INVISIBLE
hostTv?.visibility = GONE
}
}

View File

@@ -224,12 +224,13 @@
android:id="@+id/tv_sign_day"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/dp_18"
android:layout_marginTop="@dimen/dp_10"
android:layout_marginEnd="@dimen/dp_30"
android:drawableLeft="@mipmap/icon_time"
android:text="签约7天"
android:textColor="@color/white"
android:textSize="@dimen/sp_10"
android:visibility="gone"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/gl_left_price" />

39
animplayer/build.gradle Normal file
View File

@@ -0,0 +1,39 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
//apply plugin: 'kotlin-android-extensions'
apply plugin: 'maven-publish'
android {
namespace 'com.tencent.qgame.animplayer'
compileSdk 35
defaultConfig {
minSdkVersion 24
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50") {
exclude module: 'annotations'
}
}
// jcenter 上传(这个要在底部)
// 上传需要执行此任务 IDE -> gradle-> Tasks/publishing/bintrayUpload
// apply from: file("publish.gradle")
// maven central
// 上传指令./gradlew uploadArchives
// https://s01.oss.sonatype.org/
// Staging Repositories -> close -> release
// apply from: "../publish-mavencentral.gradle"

21
animplayer/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

33
animplayer/publish.gradle Normal file
View File

@@ -0,0 +1,33 @@
ext {
// 此处填写刚才建立的maven仓库的仓库名称
bintrayRepo = 'maven'
// library的group id
publishedGroupId = 'com.egame.vap'
// library网站地址
siteUrl = 'https://github.com/Tencent/vap'
// library仓库地址
gitUrl = 'https://github.com/Tencent/vap'
// 注册时候的bintray username
developerId = 'hexleo'
// 开发者名称
developerName = 'hexleo'
// 开发者邮箱
developerEmail = 'wanghailiang333@gmail.com'
// 开源许可证
licenseName = 'MIT'
licenseUrl = 'http://opensource.org/licenses/MIT'
allLicenses = ["MIT"]
// library artifact单个module一般就填写library name
artifact = 'animplayer'
libraryName = 'animplayer'
libraryVersion = '2.0.15'
libraryDescription = ''
// bintrayName 是你在网页Repository页面能看到的名称
bintrayName = 'vap'
}
apply from: '../installv1.gradle'
apply from: '../bintrayv1.gradle'

View File

@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tencent.qgame.animplayer"/>

View File

@@ -0,0 +1,92 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.Bitmap
import android.graphics.MaskFilter
import com.tencent.qgame.animplayer.mask.MaskConfig
import com.tencent.qgame.animplayer.util.ALog
import org.json.JSONException
import org.json.JSONObject
/**
* vapc里读取出来的基础配置
*/
class AnimConfig {
companion object {
private const val TAG = "${Constant.TAG}.AnimConfig"
}
val version = 2 // 不同版本号不兼容
var totalFrames = 0 // 总帧数
var width = 0 // 需要显示视频的真实宽高
var height = 0
var videoWidth = 0 // 视频实际宽高
var videoHeight = 0
var orien = Constant.ORIEN_DEFAULT // 0-兼容模式 1-竖屏 2-横屏
var fps = 0
var isMix = false // 是否为融合动画
var alphaPointRect = PointRect(0, 0 ,0 ,0) // alpha区域
var rgbPointRect = PointRect(0, 0, 0, 0) // rgb区域
var isDefaultConfig = false // 没有vapc配置时默认逻辑
var defaultVideoMode = Constant.VIDEO_MODE_SPLIT_HORIZONTAL
var maskConfig: MaskConfig ?= null
var jsonConfig: JSONObject? = null
/**
* @return 解析是否成功,失败按默认配置走
*/
fun parse(json: JSONObject): Boolean {
return try {
json.getJSONObject("info").apply {
val v = getInt("v")
if (version != v) {
ALog.e(TAG, "current version=$version target=$v")
return false
}
totalFrames = getInt("f")
width = getInt("w")
height = getInt("h")
videoWidth = getInt("videoW")
videoHeight = getInt("videoH")
orien = getInt("orien")
fps = getInt("fps")
isMix = getInt("isVapx") == 1
val a = getJSONArray("aFrame") ?: return false
alphaPointRect = PointRect(a.getInt(0), a.getInt(1), a.getInt(2), a.getInt(3))
val c = getJSONArray("rgbFrame") ?: return false
rgbPointRect = PointRect(c.getInt(0), c.getInt(1), c.getInt(2), c.getInt(3))
}
true
} catch (e : JSONException) {
ALog.e(TAG, "json parse fail $e", e)
false
}
}
override fun toString(): String {
return "AnimConfig(version=$version, totalFrames=$totalFrames, width=$width, height=$height, videoWidth=$videoWidth, videoHeight=$videoHeight, orien=$orien, fps=$fps, isMix=$isMix, alphaPointRect=$alphaPointRect, rgbPointRect=$rgbPointRect, isDefaultConfig=$isDefaultConfig)"
}
}
data class PointRect(val x: Int, val y: Int, val w: Int, val h: Int)
data class RefVec2(val w: Int, val h: Int) //参考宽&高

View File

@@ -0,0 +1,188 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.os.SystemClock
import com.tencent.qgame.animplayer.file.IFileContainer
import com.tencent.qgame.animplayer.util.ALog
import org.json.JSONObject
import java.nio.charset.Charset
/**
* 配置管理
*/
class AnimConfigManager(val player: AnimPlayer) {
companion object {
private const val TAG = "${Constant.TAG}.AnimConfigManager"
}
var config: AnimConfig? = null
var isParsingConfig = false // 是否正在读取配置
/**
* 解析配置
* @return true 解析成功 false 解析失败
*/
fun parseConfig(fileContainer: IFileContainer, enableVersion1: Boolean, defaultVideoMode: Int, defaultFps: Int): Int {
try {
isParsingConfig = true
// 解析vapc
val time = SystemClock.elapsedRealtime()
val result = parse(fileContainer, defaultVideoMode, defaultFps)
ALog.i(TAG, "parseConfig cost=${SystemClock.elapsedRealtime() - time}ms enableVersion1=$enableVersion1 result=$result")
if (!result) {
isParsingConfig = false
return Constant.REPORT_ERROR_TYPE_PARSE_CONFIG
}
if (config?.isDefaultConfig == true && !enableVersion1) {
isParsingConfig = false
return Constant.REPORT_ERROR_TYPE_PARSE_CONFIG
}
// 插件解析配置
val resultCode = config?.let {
player.pluginManager.onConfigCreate(it)
} ?: Constant.OK
isParsingConfig = false
return resultCode
} catch (e : Throwable) {
ALog.e(TAG, "parseConfig error $e", e)
isParsingConfig = false
return Constant.REPORT_ERROR_TYPE_PARSE_CONFIG
}
}
/**
* 默认配置解析(兼容老视频格式)
*/
fun defaultConfig(_videoWidth: Int, _videoHeight: Int) {
if (config?.isDefaultConfig == false) return
config?.apply {
videoWidth = _videoWidth
videoHeight = _videoHeight
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)
}
}
}
}
private fun parse(fileContainer: IFileContainer, defaultVideoMode: Int, defaultFps: Int): Boolean {
val config = AnimConfig()
this.config = config
// 查找vapc box
fileContainer.startRandomRead()
val boxHead = ByteArray(8)
var head: BoxHead? = null
var vapcStartIndex: Long = 0
while (fileContainer.read(boxHead, 0, boxHead.size) == 8) {
val h = parseBoxHead(boxHead) ?: break
if ("vapc" == h.type) {
h.startIndex = vapcStartIndex
head = h
break
}
vapcStartIndex += h.length
fileContainer.skip(h.length - 8L)
}
if (head == null) {
ALog.e(TAG, "vapc box head not found")
// 按照默认配置生成config
config.apply {
isDefaultConfig = true
this.defaultVideoMode = defaultVideoMode
fps = defaultFps
}
player.fps = config.fps
return true
}
// 读取vapc box
val vapcBuf = ByteArray(head.length - 8) // ps: OOM exception
fileContainer.read(vapcBuf, 0 , vapcBuf.size)
fileContainer.closeRandomRead()
val json = String(vapcBuf, 0, vapcBuf.size, Charset.forName("UTF-8"))
val jsonObj = JSONObject(json)
config.jsonConfig = jsonObj
val result = config.parse(jsonObj)
if (defaultFps > 0) {
config.fps = defaultFps
}
player.fps = config.fps
return result
}
private fun parseBoxHead(boxHead: ByteArray): BoxHead? {
if (boxHead.size != 8) return null
val head = BoxHead()
var length: Int = 0
length = length or (boxHead[0].toInt() and 0xff shl 24)
length = length or (boxHead[1].toInt() and 0xff shl 16)
length = length or (boxHead[2].toInt() and 0xff shl 8)
length = length or (boxHead[3].toInt() and 0xff)
head.length = length
head.type = String(boxHead, 4, 4, Charset.forName("US-ASCII"))
return head
}
private class BoxHead {
var startIndex: Long = 0
var length: Int = 0
var type: String? = null
}
}

View File

@@ -0,0 +1,157 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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 com.tencent.qgame.animplayer.file.IFileContainer
import com.tencent.qgame.animplayer.inter.IAnimListener
import com.tencent.qgame.animplayer.mask.MaskConfig
import com.tencent.qgame.animplayer.plugin.AnimPluginManager
import com.tencent.qgame.animplayer.util.ALog
class AnimPlayer(val animView: IAnimView) {
companion object {
private const val TAG = "${Constant.TAG}.AnimPlayer"
}
var animListener: IAnimListener? = null
var decoder: Decoder? = null
var audioPlayer: AudioPlayer? = null
var fps: Int = 0
set(value) {
decoder?.fps = value
field = value
}
// 设置默认的fps <= 0 表示以vapc配置为准 > 0 表示以此设置为准
var defaultFps: Int = 0
var playLoop: Int = 0
set(value) {
decoder?.playLoop = value
audioPlayer?.playLoop = value
field = value
}
var supportMaskBoolean : Boolean = false
var maskEdgeBlurBoolean : Boolean = false
// 是否兼容老版本 默认不兼容
var enableVersion1 : Boolean = false
// 视频模式
var videoMode: Int = Constant.VIDEO_MODE_SPLIT_HORIZONTAL
var isDetachedFromWindow = false
var isSurfaceAvailable = false
var startRunnable: Runnable? = null
var isStartRunning = false // 启动时运行状态
var isMute = false // 是否静音
val configManager = AnimConfigManager(this)
val pluginManager = AnimPluginManager(this)
fun onSurfaceTextureDestroyed() {
isSurfaceAvailable = false
isStartRunning = false
decoder?.destroy()
audioPlayer?.destroy()
}
fun onSurfaceTextureAvailable(width: Int, height: Int) {
isSurfaceAvailable = true
startRunnable?.run()
startRunnable = null
}
fun onSurfaceTextureSizeChanged(width: Int, height: Int) {
decoder?.onSurfaceSizeChanged(width, height)
}
fun startPlay(fileContainer: IFileContainer) {
isStartRunning = true
prepareDecoder()
if (decoder?.prepareThread() == false) {
isStartRunning = false
decoder?.onFailed(Constant.REPORT_ERROR_TYPE_CREATE_THREAD, Constant.ERROR_MSG_CREATE_THREAD)
decoder?.onVideoComplete()
return
}
// 在线程中解析配置
decoder?.renderThread?.handler?.post {
val result = configManager.parseConfig(fileContainer, enableVersion1, videoMode, defaultFps)
if (result != Constant.OK) {
isStartRunning = false
decoder?.onFailed(result, Constant.getErrorMsg(result))
decoder?.onVideoComplete()
return@post
}
ALog.i(TAG, "parse ${configManager.config}")
val config = configManager.config
// 如果是默认配置因为信息不完整onVideoConfigReady不会被调用
if (config != null && (config.isDefaultConfig || animListener?.onVideoConfigReady(config) == true)) {
innerStartPlay(fileContainer)
} else {
ALog.i(TAG, "onVideoConfigReady return false")
}
}
}
private fun innerStartPlay(fileContainer: IFileContainer) {
synchronized(AnimPlayer::class.java) {
if (isSurfaceAvailable) {
isStartRunning = false
decoder?.start(fileContainer)
if (!isMute) {
audioPlayer?.start(fileContainer)
}
} else {
startRunnable = Runnable {
innerStartPlay(fileContainer)
}
animView.prepareTextureView()
}
}
}
fun stopPlay() {
decoder?.stop()
audioPlayer?.stop()
}
fun isRunning(): Boolean {
return isStartRunning // 启动过程运行状态
|| (decoder?.isRunning ?: false) // 解码过程运行状态
}
private fun prepareDecoder() {
if (decoder == null) {
decoder = HardDecoder(this).apply {
playLoop = this@AnimPlayer.playLoop
fps = this@AnimPlayer.fps
}
}
if (audioPlayer == null) {
audioPlayer = AudioPlayer(this).apply {
playLoop = this@AnimPlayer.playLoop
}
}
}
fun updateMaskConfig(maskConfig: MaskConfig?) {
configManager.config?.maskConfig = configManager.config?.maskConfig ?: MaskConfig()
configManager.config?.maskConfig?.safeSetMaskBitmapAndReleasePre(maskConfig?.alphaMaskBitmap)
configManager.config?.maskConfig?.maskPositionPair = maskConfig?.maskPositionPair
configManager.config?.maskConfig?.maskTexPair = maskConfig?.maskTexPair
}
}

View File

@@ -0,0 +1,311 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.content.Context
import android.content.res.AssetManager
import android.graphics.SurfaceTexture
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.TextureView
import android.view.View
import android.widget.FrameLayout
import com.tencent.qgame.animplayer.file.AssetsFileContainer
import com.tencent.qgame.animplayer.file.FileContainer
import com.tencent.qgame.animplayer.file.IFileContainer
import com.tencent.qgame.animplayer.inter.IAnimListener
import com.tencent.qgame.animplayer.inter.IFetchResource
import com.tencent.qgame.animplayer.inter.OnResourceClickListener
import com.tencent.qgame.animplayer.mask.MaskConfig
import com.tencent.qgame.animplayer.textureview.InnerTextureView
import com.tencent.qgame.animplayer.util.ALog
import com.tencent.qgame.animplayer.util.IScaleType
import com.tencent.qgame.animplayer.util.ScaleType
import com.tencent.qgame.animplayer.util.ScaleTypeUtil
import java.io.File
open class AnimView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0):
IAnimView,
FrameLayout(context, attrs, defStyleAttr),
TextureView.SurfaceTextureListener {
companion object {
private const val TAG = "${Constant.TAG}.AnimView"
}
private lateinit var player: AnimPlayer
private val uiHandler by lazy { Handler(Looper.getMainLooper()) }
private var surface: SurfaceTexture? = null
private var animListener: IAnimListener? = null
private var innerTextureView: InnerTextureView? = null
private var lastFile: IFileContainer? = null
private val scaleTypeUtil = ScaleTypeUtil()
// 代理监听
private val animProxyListener by lazy {
object : IAnimListener {
override fun onVideoConfigReady(config: AnimConfig): Boolean {
scaleTypeUtil.setVideoSize(config.width, config.height)
return animListener?.onVideoConfigReady(config) ?: super.onVideoConfigReady(config)
}
override fun onVideoStart() {
animListener?.onVideoStart()
}
override fun onVideoRender(frameIndex: Int, config: AnimConfig?) {
animListener?.onVideoRender(frameIndex, config)
}
override fun onVideoComplete() {
hide()
animListener?.onVideoComplete()
}
override fun onVideoDestroy() {
hide()
animListener?.onVideoDestroy()
}
override fun onFailed(errorType: Int, errorMsg: String?) {
animListener?.onFailed(errorType, errorMsg)
}
}
}
// 保证AnimView已经布局完成才加入TextureView
private var onSizeChangedCalled = false
private var needPrepareTextureView = false
private val prepareTextureViewRunnable = Runnable {
removeAllViews()
innerTextureView = InnerTextureView(context).apply {
player = this@AnimView.player
isOpaque = false
surfaceTextureListener = this@AnimView
layoutParams = scaleTypeUtil.getLayoutParam(this)
}
addView(innerTextureView)
}
init {
hide()
player = AnimPlayer(this)
player.animListener = animProxyListener
}
override fun prepareTextureView() {
if (onSizeChangedCalled) {
uiHandler.post(prepareTextureViewRunnable)
} else {
ALog.e(TAG, "onSizeChanged not called")
needPrepareTextureView = true
}
}
override fun getSurfaceTexture(): SurfaceTexture? {
return innerTextureView?.surfaceTexture ?: surface
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
ALog.i(TAG, "onSurfaceTextureSizeChanged $width x $height")
player.onSurfaceTextureSizeChanged(width, height)
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
ALog.i(TAG, "onSurfaceTextureDestroyed")
this.surface = null
player.onSurfaceTextureDestroyed()
uiHandler.post {
innerTextureView?.surfaceTextureListener = null
innerTextureView = null
removeAllViews()
}
return true
}
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
ALog.i(TAG, "onSurfaceTextureAvailable width=$width height=$height")
this.surface = surface
player.onSurfaceTextureAvailable(width, height)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
ALog.i(TAG, "onSizeChanged w=$w, h=$h")
scaleTypeUtil.setLayoutSize(w, h)
onSizeChangedCalled = true
// 需要保证onSizeChanged被调用
if (needPrepareTextureView) {
needPrepareTextureView = false
prepareTextureView()
}
}
override fun onAttachedToWindow() {
ALog.i(TAG, "onAttachedToWindow")
super.onAttachedToWindow()
player.isDetachedFromWindow = false
// 自动恢复播放
if (player.playLoop > 0) {
lastFile?.apply {
startPlay(this)
}
}
}
override fun onDetachedFromWindow() {
ALog.i(TAG, "onDetachedFromWindow")
super.onDetachedFromWindow()
player.isDetachedFromWindow = true
player.onSurfaceTextureDestroyed()
}
override fun setAnimListener(animListener: IAnimListener?) {
this.animListener = animListener
}
override fun setFetchResource(fetchResource: IFetchResource?) {
player.pluginManager.getMixAnimPlugin()?.resourceRequest = fetchResource
}
override fun setOnResourceClickListener(resourceClickListener: OnResourceClickListener?) {
player.pluginManager.getMixAnimPlugin()?.resourceClickListener = resourceClickListener
}
/**
* 兼容方案,优先保证表情显示
*/
open fun enableAutoTxtColorFill(enable: Boolean) {
player.pluginManager.getMixAnimPlugin()?.autoTxtColorFill = enable
}
override fun setLoop(playLoop: Int) {
player.playLoop = playLoop
}
override fun supportMask(isSupport : Boolean, isEdgeBlur : Boolean) {
player.supportMaskBoolean = isSupport
player.maskEdgeBlurBoolean = isEdgeBlur
}
override fun updateMaskConfig(maskConfig: MaskConfig?) {
player.updateMaskConfig(maskConfig)
}
@Deprecated("Compatible older version mp4, default false")
fun enableVersion1(enable: Boolean) {
player.enableVersion1 = enable
}
// 兼容老版本视频模式
@Deprecated("Compatible older version mp4")
fun setVideoMode(mode: Int) {
player.videoMode = mode
}
override fun setFps(fps: Int) {
ALog.i(TAG, "setFps=$fps")
player.defaultFps = fps
}
override fun setScaleType(type : ScaleType) {
scaleTypeUtil.currentScaleType = type
}
override fun setScaleType(scaleType: IScaleType) {
scaleTypeUtil.scaleTypeImpl = scaleType
}
/**
* @param isMute true 静音
*/
override fun setMute(isMute: Boolean) {
ALog.e(TAG, "set mute=$isMute")
player.isMute = isMute
}
override fun startPlay(file: File) {
try {
val fileContainer = FileContainer(file)
startPlay(fileContainer)
} catch (e: Throwable) {
animProxyListener.onFailed(Constant.REPORT_ERROR_TYPE_FILE_ERROR, Constant.ERROR_MSG_FILE_ERROR)
animProxyListener.onVideoComplete()
}
}
override fun startPlay(assetManager: AssetManager, assetsPath: String) {
try {
val fileContainer = AssetsFileContainer(assetManager, assetsPath)
startPlay(fileContainer)
} catch (e: Throwable) {
animProxyListener.onFailed(Constant.REPORT_ERROR_TYPE_FILE_ERROR, Constant.ERROR_MSG_FILE_ERROR)
animProxyListener.onVideoComplete()
}
}
override fun startPlay(fileContainer: IFileContainer) {
ui {
if (visibility != View.VISIBLE) {
ALog.e(TAG, "AnimView is GONE, can't play")
return@ui
}
if (!player.isRunning()) {
lastFile = fileContainer
player.startPlay(fileContainer)
} else {
ALog.e(TAG, "is running can not start")
}
}
}
override fun stopPlay() {
player.stopPlay()
}
override fun isRunning(): Boolean {
return player.isRunning()
}
override fun getRealSize(): Pair<Int, Int> {
return scaleTypeUtil.getRealSize()
}
private fun hide() {
lastFile?.close()
ui {
removeAllViews()
}
}
private fun ui(f:()->Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) f() else uiHandler.post { f() }
}
}

View File

@@ -0,0 +1,211 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.media.*
import com.tencent.qgame.animplayer.file.IFileContainer
import com.tencent.qgame.animplayer.util.ALog
import com.tencent.qgame.animplayer.util.MediaUtil
import java.lang.RuntimeException
class AudioPlayer(val player: AnimPlayer) {
companion object {
private const val TAG = "${Constant.TAG}.AudioPlayer"
}
var extractor: MediaExtractor? = null
var decoder: MediaCodec? = null
var audioTrack: AudioTrack? = null
val decodeThread = HandlerHolder(null, null)
var isRunning = false
var playLoop = 0
var isStopReq = false
var needDestroy = false
private fun prepareThread(): Boolean {
return Decoder.createThread(decodeThread, "anim_audio_thread")
}
fun start(fileContainer: IFileContainer) {
isStopReq = false
needDestroy = false
if (!prepareThread()) return
if (isRunning) {
stop()
}
isRunning = true
decodeThread.handler?.post {
try {
startPlay(fileContainer)
} catch (e: Throwable) {
ALog.e(TAG, "Audio exception=$e", e)
release()
}
}
}
fun stop() {
isStopReq = true
}
private fun startPlay(fileContainer: IFileContainer) {
val extractor = MediaUtil.getExtractor(fileContainer)
this.extractor = extractor
val audioIndex = MediaUtil.selectAudioTrack(extractor)
if (audioIndex < 0) {
ALog.e(TAG, "cannot find audio track")
release()
return
}
extractor.selectTrack(audioIndex)
val format = extractor.getTrackFormat(audioIndex)
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
ALog.i(TAG, "audio mime=$mime")
if (!MediaUtil.checkSupportCodec(mime)) {
ALog.e(TAG, "mime=$mime not support")
release()
return
}
val decoder = MediaCodec.createDecoderByType(mime).apply {
configure(format, null, null, 0)
start()
}
this.decoder = decoder
val decodeInputBuffers = decoder.inputBuffers
var decodeOutputBuffers = decoder.outputBuffers
val bufferInfo = MediaCodec.BufferInfo()
val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
val channelConfig = getChannelConfig(format.getInteger(MediaFormat.KEY_CHANNEL_COUNT))
val bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT)
val audioTrack = AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT, bufferSize, AudioTrack.MODE_STREAM)
this.audioTrack = audioTrack
val state = audioTrack.state
if (state != AudioTrack.STATE_INITIALIZED) {
release()
ALog.e(TAG, "init audio track failure")
return
}
audioTrack.play()
val timeOutUs = 1000L
var isEOS = false
while (!isStopReq) {
if (!isEOS) {
val inputIndex = decoder.dequeueInputBuffer(timeOutUs)
if (inputIndex >= 0) {
val inputBuffer = decodeInputBuffers[inputIndex]
inputBuffer.clear()
val sampleSize = extractor.readSampleData(inputBuffer, 0)
if (sampleSize < 0) {
isEOS = true
decoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
} else {
decoder.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0)
extractor.advance()
}
}
}
val outputIndex = decoder.dequeueOutputBuffer(bufferInfo, 0)
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
decodeOutputBuffers = decoder.outputBuffers
}
if (outputIndex >= 0) {
val outputBuffer = decodeOutputBuffers[outputIndex]
val chunkPCM = ByteArray(bufferInfo.size)
outputBuffer.get(chunkPCM)
outputBuffer.clear()
audioTrack.write(chunkPCM, 0, bufferInfo.size)
decoder.releaseOutputBuffer(outputIndex, false)
}
if (isEOS && bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
if (--playLoop > 0) {
ALog.d(TAG, "Reached EOS, looping -> playLoop")
extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
decoder.flush()
isEOS = false
} else {
ALog.i(TAG, "decode finish")
release()
break
}
}
}
release()
}
private fun release() {
try {
decoder?.apply {
stop()
release()
}
decoder = null
extractor?.release()
extractor = null
audioTrack?.apply {
pause()
flush()
stop()
release()
}
audioTrack = null
} catch (e: Throwable) {
ALog.e(TAG, "release exception=$e", e)
}
isRunning = false
if (needDestroy) {
destroyInner()
}
}
fun destroy() {
if (isRunning) {
needDestroy = true
stop()
} else {
destroyInner()
}
}
private fun destroyInner() {
if (player.isDetachedFromWindow) {
ALog.i(TAG, "destroyThread")
decodeThread.handler?.removeCallbacksAndMessages(null)
decodeThread.thread = Decoder.quitSafely(decodeThread.thread)
}
}
private fun getChannelConfig(channelCount: Int): Int {
return when (channelCount) {
1 -> AudioFormat.CHANNEL_CONFIGURATION_MONO
2 -> AudioFormat.CHANNEL_OUT_STEREO
3 -> AudioFormat.CHANNEL_OUT_STEREO or AudioFormat.CHANNEL_OUT_FRONT_CENTER
4 -> AudioFormat.CHANNEL_OUT_QUAD
5 -> AudioFormat.CHANNEL_OUT_QUAD or AudioFormat.CHANNEL_OUT_FRONT_CENTER
6 -> AudioFormat.CHANNEL_OUT_5POINT1
7 -> AudioFormat.CHANNEL_OUT_5POINT1 or AudioFormat.CHANNEL_OUT_BACK_CENTER
else -> throw RuntimeException("Unsupported channel count: $channelCount")
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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 Constant {
const val TAG = "AnimPlayer"
// 视频适配的屏幕方向
const val ORIEN_DEFAULT = 0 // 兼容模式
const val ORIEN_PORTRAIT = 1 // 适配竖屏的视频
const val ORIEN_LANDSCAPE = 2 // 适配横屏的视频
// 视频对齐方式 (兼容老版本视频模式)
@Deprecated("Compatible older version mp4")
const val VIDEO_MODE_SPLIT_HORIZONTAL = 1 // 视频左右对齐alpha左\rgb右
@Deprecated("Compatible older version mp4")
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 // 成功
const val REPORT_ERROR_TYPE_EXTRACTOR_EXC = 10001 // MediaExtractor exception
const val REPORT_ERROR_TYPE_DECODE_EXC = 10002 // MediaCodec exception
const val REPORT_ERROR_TYPE_CREATE_THREAD = 10003 // 线程创建失败
const val REPORT_ERROR_TYPE_CREATE_RENDER = 10004 // render创建失败
const val REPORT_ERROR_TYPE_PARSE_CONFIG = 10005 // 配置解析失败
const val REPORT_ERROR_TYPE_CONFIG_PLUGIN_MIX = 10006 // vapx融合动画资源获取失败
const val REPORT_ERROR_TYPE_FILE_ERROR = 10007 // 文件无法读取
const val REPORT_ERROR_TYPE_HEVC_NOT_SUPPORT = 10008 // 不支持h265
const val ERROR_MSG_EXTRACTOR_EXC = "0x1 MediaExtractor exception" // MediaExtractor exception
const val ERROR_MSG_DECODE_EXC = "0x2 MediaCodec exception" // MediaCodec exception
const val ERROR_MSG_CREATE_THREAD = "0x3 thread create fail" // 线程创建失败
const val ERROR_MSG_CREATE_RENDER = "0x4 render create fail" // render创建失败
const val ERROR_MSG_PARSE_CONFIG = "0x5 parse config fail" // 配置解析失败
const val ERROR_MSG_CONFIG_PLUGIN_MIX = "0x6 vapx fail" // vapx融合动画资源获取失败
const val ERROR_MSG_FILE_ERROR = "0x7 file can't read" // 文件无法读取
const val ERROR_MSG_HEVC_NOT_SUPPORT = "0x8 hevc not support" // 不支持h265
fun getErrorMsg(errorType: Int, errorMsg: String? = null): String {
return when(errorType) {
REPORT_ERROR_TYPE_EXTRACTOR_EXC -> ERROR_MSG_EXTRACTOR_EXC
REPORT_ERROR_TYPE_DECODE_EXC -> ERROR_MSG_DECODE_EXC
REPORT_ERROR_TYPE_CREATE_THREAD -> ERROR_MSG_CREATE_THREAD
REPORT_ERROR_TYPE_CREATE_RENDER -> ERROR_MSG_CREATE_RENDER
REPORT_ERROR_TYPE_PARSE_CONFIG -> ERROR_MSG_PARSE_CONFIG
REPORT_ERROR_TYPE_CONFIG_PLUGIN_MIX -> ERROR_MSG_CONFIG_PLUGIN_MIX
else -> "unknown"
} + " ${errorMsg ?: ""}"
}
}

View File

@@ -0,0 +1,171 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.os.Build
import android.os.HandlerThread
import android.os.Handler
import com.tencent.qgame.animplayer.file.IFileContainer
import com.tencent.qgame.animplayer.inter.IAnimListener
import com.tencent.qgame.animplayer.util.ALog
import com.tencent.qgame.animplayer.util.SpeedControlUtil
abstract class Decoder(val player: AnimPlayer) : IAnimListener {
companion object {
private const val TAG = "${Constant.TAG}.Decoder"
fun createThread(handlerHolder: HandlerHolder, name: String): Boolean {
try {
if (handlerHolder.thread == null || handlerHolder.thread?.isAlive == false) {
handlerHolder.thread = HandlerThread(name).apply {
start()
handlerHolder.handler = Handler(looper)
}
}
return true
} catch (e: OutOfMemoryError) {
ALog.e(TAG, "createThread OOM", e)
}
return false
}
fun quitSafely(thread: HandlerThread?): HandlerThread? {
thread?.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
thread.quitSafely()
} else {
thread.quit()
}
}
return null
}
}
var render: IRenderListener? = null
val renderThread = HandlerHolder(null, null)
val decodeThread = HandlerHolder(null, null)
private var surfaceWidth = 0
private var surfaceHeight = 0
var fps: Int = 0
set(value) {
speedControlUtil.setFixedPlaybackRate(value)
field = value
}
var playLoop = 0 // 循环播放次数
var isRunning = false // 是否正在运行
var isStopReq = false // 是否需要停止
val speedControlUtil by lazy { SpeedControlUtil() }
abstract fun start(fileContainer: IFileContainer)
fun stop() {
isStopReq = true
}
abstract fun destroy()
fun prepareThread(): Boolean {
return createThread(renderThread, "anim_render_thread") && createThread(decodeThread, "anim_decode_thread")
}
fun prepareRender(needYUV: Boolean): Boolean {
if (render == null) {
ALog.i(TAG, "prepareRender")
player.animView.getSurfaceTexture()?.apply {
if (needYUV) {
ALog.i(TAG, "use yuv render")
render = YUVRender(this)
} else {
render = Render(this).apply {
updateViewPort(surfaceWidth, surfaceHeight)
}
}
}
}
return render != null
}
fun preparePlay(videoWidth: Int, videoHeight: Int) {
player.configManager.defaultConfig(videoWidth, videoHeight)
player.configManager.config?.apply {
render?.setAnimConfig(this)
}
player.pluginManager.onRenderCreate()
}
/**
* decode过程中视频尺寸变化
* 主要是没有16进制对齐的老视频
*/
fun videoSizeChange(newWidth: Int, newHeight: Int) {
if (newWidth <= 0 || newHeight <= 0) return
val config = player.configManager.config ?: return
if (config.videoWidth != newWidth || config.videoHeight != newHeight) {
ALog.i(TAG, "videoSizeChange old=(${config.videoWidth},${config.videoHeight}), new=($newWidth,$newHeight)")
config.videoWidth = newWidth
config.videoHeight = newHeight
render?.setAnimConfig(config)
}
}
fun destroyThread() {
if (player.isDetachedFromWindow) {
ALog.i(TAG, "destroyThread")
renderThread.handler?.removeCallbacksAndMessages(null)
decodeThread.handler?.removeCallbacksAndMessages(null)
renderThread.thread = quitSafely(renderThread.thread)
decodeThread.thread = quitSafely(decodeThread.thread)
renderThread.handler = null
decodeThread.handler = null
}
}
fun onSurfaceSizeChanged(width: Int, height: Int) {
surfaceWidth = width
surfaceHeight = height
render?.updateViewPort(width, height)
}
override fun onVideoStart() {
ALog.i(TAG, "onVideoStart")
player.animListener?.onVideoStart()
}
override fun onVideoRender(frameIndex: Int, config: AnimConfig?) {
ALog.d(TAG, "onVideoRender")
player.animListener?.onVideoRender(frameIndex, config)
}
override fun onVideoComplete() {
ALog.i(TAG, "onVideoComplete")
player.animListener?.onVideoComplete()
}
override fun onVideoDestroy() {
ALog.i(TAG, "onVideoDestroy")
player.animListener?.onVideoDestroy()
}
override fun onFailed(errorType: Int, errorMsg: String?) {
ALog.e(TAG, "onFailed errorType=$errorType, errorMsg=$errorMsg")
player.animListener?.onFailed(errorType, errorMsg)
}
}
data class HandlerHolder(var thread: HandlerThread?, var handler: Handler?)

View File

@@ -0,0 +1,115 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.EGL14
import android.view.Surface
import com.tencent.qgame.animplayer.util.ALog
import javax.microedition.khronos.egl.*
class EGLUtil {
companion object {
private const val TAG = "${Constant.TAG}.EGLUtil"
}
private var egl: EGL10? = null
private var eglDisplay: EGLDisplay? = null
private var eglSurface: EGLSurface? = null
private var eglContext: EGLContext? = null
private var eglConfig: EGLConfig? = null
private var surface: Surface? = null
init {
eglDisplay = EGL10.EGL_NO_DISPLAY
eglSurface = EGL10.EGL_NO_SURFACE
eglContext = EGL10.EGL_NO_CONTEXT
}
fun start(surfaceTexture: SurfaceTexture) {
try {
egl = EGLContext.getEGL() as EGL10
eglDisplay = egl?.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY)
val version = IntArray(2)
egl?.eglInitialize(eglDisplay, version)
eglConfig = chooseConfig()
surface = Surface(surfaceTexture)
eglSurface = egl?.eglCreateWindowSurface(eglDisplay, eglConfig, surface, null)
eglContext = createContext(egl, eglDisplay, eglConfig)
if (eglSurface == null || eglSurface == EGL10.EGL_NO_SURFACE) {
ALog.e(TAG, "error:${Integer.toHexString(egl?.eglGetError() ?: 0)}")
return
}
if (egl?.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) == false) {
ALog.e(TAG, "make current error:${Integer.toHexString(egl?.eglGetError() ?: 0)}")
}
} catch (e: Throwable) {
ALog.e(TAG, "error:$e", e)
}
}
private fun chooseConfig(): EGLConfig? {
val configsCount = IntArray(1)
val configs = arrayOfNulls<EGLConfig>(1)
val attributes =getAttributes()
val confSize = 1
if (egl?.eglChooseConfig(eglDisplay, attributes, configs, confSize, configsCount) == true) {
return configs[0]
}
return null
}
private fun getAttributes(): IntArray {
return intArrayOf(
EGL10.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, //指定渲染api类别
EGL10.EGL_RED_SIZE, 8,
EGL10.EGL_GREEN_SIZE, 8,
EGL10.EGL_BLUE_SIZE, 8,
EGL10.EGL_ALPHA_SIZE, 8,
EGL10.EGL_DEPTH_SIZE, 0,
EGL10.EGL_STENCIL_SIZE, 0,
EGL10.EGL_NONE
)
}
private fun createContext(egl: EGL10?, eglDisplay: EGLDisplay?, eglConfig: EGLConfig?): EGLContext? {
val attrs = intArrayOf(
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL10.EGL_NONE
)
return egl?.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attrs)
}
fun swapBuffers() {
if (eglDisplay == null || eglSurface == null) return
egl?.eglSwapBuffers(eglDisplay, eglSurface)
}
fun release() {
egl?.apply {
eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT)
eglDestroySurface(eglDisplay, eglSurface)
eglDestroyContext(eglDisplay, eglContext)
eglTerminate(eglDisplay)
surface?.release()
surface = null
}
}
}

View File

@@ -0,0 +1,397 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaExtractor
import android.media.MediaFormat
import android.os.Build
import android.view.Surface
import com.tencent.qgame.animplayer.file.IFileContainer
import com.tencent.qgame.animplayer.util.ALog
import com.tencent.qgame.animplayer.util.MediaUtil
class HardDecoder(player: AnimPlayer) : Decoder(player), SurfaceTexture.OnFrameAvailableListener {
companion object {
private const val TAG = "${Constant.TAG}.HardDecoder"
}
private var surface: Surface? = null
private var glTexture: SurfaceTexture? = null
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
isRunning = true
renderThread.handler?.post {
startPlay(fileContainer)
}
}
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
if (isStopReq) return
ALog.d(TAG, "onFrameAvailable")
renderData()
}
private fun renderData() {
renderThread.handler?.post {
try {
glTexture?.apply {
updateTexImage()
render?.renderFrame()
player.pluginManager.onRendering()
render?.swapBuffers()
}
} catch (e: Throwable) {
ALog.e(TAG, "render exception=$e", e)
}
}
}
private fun startPlay(fileContainer: IFileContainer) {
var extractor: MediaExtractor? = null
var decoder: MediaCodec? = null
var format: MediaFormat? = null
var trackIndex = 0
try {
extractor = MediaUtil.getExtractor(fileContainer)
trackIndex = MediaUtil.selectVideoTrack(extractor)
if (trackIndex < 0) {
throw RuntimeException("No video track found")
}
extractor.selectTrack(trackIndex)
format = extractor.getTrackFormat(trackIndex)
if (format == null) throw RuntimeException("format is null")
// 是否支持h265
if (MediaUtil.checkIsHevc(format)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
|| !MediaUtil.checkSupportCodec(MediaUtil.MIME_HEVC)) {
onFailed(Constant.REPORT_ERROR_TYPE_HEVC_NOT_SUPPORT,
"${Constant.ERROR_MSG_HEVC_NOT_SUPPORT} " +
"sdk:${Build.VERSION.SDK_INT}" +
",support hevc:" + MediaUtil.checkSupportCodec(MediaUtil.MIME_HEVC))
release(null, null)
return
}
}
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 {
glTexture = SurfaceTexture(getExternalTexture()).apply {
setOnFrameAvailableListener(this@HardDecoder)
setDefaultBufferSize(videoWidth, videoHeight)
}
clearFrame()
}
} catch (e: Throwable) {
ALog.e(TAG, "MediaExtractor exception e=$e", e)
onFailed(Constant.REPORT_ERROR_TYPE_EXTRACTOR_EXC, "${Constant.ERROR_MSG_EXTRACTOR_EXC} e=$e")
release(decoder, extractor)
return
}
try {
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
ALog.i(TAG, "Video MIME is $mime")
decoder = MediaCodec.createDecoderByType(mime).apply {
if (needYUV) {
format.setInteger(
MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
)
configure(format, null, null, 0)
} else {
surface = Surface(glTexture)
configure(format, surface, null, 0)
}
start()
decodeThread.handler?.post {
try {
startDecode(extractor, this)
} catch (e: Throwable) {
ALog.e(TAG, "MediaCodec exception e=$e", e)
onFailed(Constant.REPORT_ERROR_TYPE_DECODE_EXC, "${Constant.ERROR_MSG_DECODE_EXC} e=$e")
release(decoder, extractor)
}
}
}
} catch (e: Throwable) {
ALog.e(TAG, "MediaCodec configure exception e=$e", e)
onFailed(Constant.REPORT_ERROR_TYPE_DECODE_EXC, "${Constant.ERROR_MSG_DECODE_EXC} e=$e")
release(decoder, extractor)
return
}
}
private fun startDecode(extractor: MediaExtractor ,decoder: MediaCodec) {
val TIMEOUT_USEC = 10000L
var inputChunk = 0
var outputDone = false
var inputDone = false
var frameIndex = 0
var isLoop = false
val decoderInputBuffers = decoder.inputBuffers
while (!outputDone) {
if (isStopReq) {
ALog.i(TAG, "stop decode")
release(decoder, extractor)
return
}
if (!inputDone) {
val inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC)
if (inputBufIndex >= 0) {
val inputBuf = decoderInputBuffers[inputBufIndex]
val chunkSize = extractor.readSampleData(inputBuf, 0)
if (chunkSize < 0) {
decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
inputDone = true
ALog.d(TAG, "decode EOS")
} else {
val presentationTimeUs = extractor.sampleTime
decoder.queueInputBuffer(inputBufIndex, 0, chunkSize, presentationTimeUs, 0)
ALog.d(TAG, "submitted frame $inputChunk to dec, size=$chunkSize")
inputChunk++
extractor.advance()
}
} else {
ALog.d(TAG, "input buffer not available")
}
}
if (!outputDone) {
val decoderStatus = decoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC)
when {
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 -> {
outputFormat = decoder.outputFormat
outputFormat?.apply {
try {
// 有可能取到空值,做一层保护
val stride = getInteger("stride")
val sliceHeight = getInteger("slice-height")
if (stride > 0 && sliceHeight > 0) {
alignWidth = stride
alignHeight = sliceHeight
}
} catch (t: Throwable) {
ALog.e(TAG, "$t", t)
}
}
ALog.i(TAG, "decoder output format changed: $outputFormat")
}
decoderStatus < 0 -> {
throw RuntimeException("unexpected result from decoder.dequeueOutputBuffer: $decoderStatus")
}
else -> {
var loop = 0
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
loop = --playLoop
player.playLoop = playLoop // 消耗loop次数 自动恢复后能有正确的loop次数
outputDone = playLoop <= 0
}
val doRender = !outputDone
if (doRender) {
speedControlUtil.preRender(bufferInfo.presentationTimeUs)
}
if (needYUV && doRender) {
yuvProcess(decoder, decoderStatus)
}
// release & render
decoder.releaseOutputBuffer(decoderStatus, doRender && !needYUV)
if (frameIndex == 0 && !isLoop) {
onVideoStart()
}
player.pluginManager.onDecoding(frameIndex)
onVideoRender(frameIndex, player.configManager.config)
frameIndex++
ALog.d(TAG, "decode frameIndex=$frameIndex")
if (loop > 0) {
ALog.d(TAG, "Reached EOD, looping")
player.pluginManager.onLoopStart()
extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC)
inputDone = false
decoder.flush()
speedControlUtil.reset()
frameIndex = 0
isLoop = true
}
if (outputDone) {
release(decoder, extractor)
}
}
}
}
}
}
/**
* 获取到解码后每一帧的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)
}
}
}
private fun release(decoder: MediaCodec?, extractor: MediaExtractor?) {
renderThread.handler?.post {
render?.clearFrame()
try {
ALog.i(TAG, "release")
decoder?.apply {
stop()
release()
}
extractor?.release()
glTexture?.release()
glTexture = null
speedControlUtil.reset()
player.pluginManager.onRelease()
render?.releaseTexture()
surface?.release()
surface = null
} catch (e: Throwable) {
ALog.e(TAG, "release e=$e", e)
}
isRunning = false
onVideoComplete()
if (needDestroy) {
destroyInner()
}
}
}
override fun destroy() {
if (isRunning) {
needDestroy = true
stop()
} else {
destroyInner()
}
}
private fun destroyInner() {
ALog.i(TAG, "destroyInner")
renderThread.handler?.post {
player.pluginManager.onDestroy()
render?.destroyRender()
render = null
onVideoDestroy()
destroyThread()
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.content.res.AssetManager
import android.graphics.SurfaceTexture
import com.tencent.qgame.animplayer.file.IFileContainer
import com.tencent.qgame.animplayer.inter.IAnimListener
import com.tencent.qgame.animplayer.inter.IFetchResource
import com.tencent.qgame.animplayer.inter.OnResourceClickListener
import com.tencent.qgame.animplayer.mask.MaskConfig
import com.tencent.qgame.animplayer.util.IScaleType
import com.tencent.qgame.animplayer.util.ScaleType
import java.io.File
interface IAnimView {
fun prepareTextureView()
fun getSurfaceTexture(): SurfaceTexture?
fun setAnimListener(animListener: IAnimListener?)
fun setFetchResource(fetchResource: IFetchResource?)
fun setOnResourceClickListener(resourceClickListener: OnResourceClickListener?)
fun setLoop(playLoop: Int)
fun supportMask(isSupport: Boolean, isEdgeBlur: Boolean)
fun updateMaskConfig(maskConfig: MaskConfig?)
fun setFps(fps: Int)
fun setScaleType(type: ScaleType)
fun setScaleType(scaleType: IScaleType)
fun setMute(isMute: Boolean)
fun startPlay(file: File)
fun startPlay(assetManager: AssetManager, assetsPath: String)
fun startPlay(fileContainer: IFileContainer)
fun stopPlay()
fun isRunning(): Boolean
fun getRealSize(): Pair<Int, Int>
}

View File

@@ -0,0 +1,54 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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?) {}
}

View File

@@ -0,0 +1,153 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.GLES11Ext
import android.opengl.GLES20
import com.tencent.qgame.animplayer.util.GlFloatArray
import com.tencent.qgame.animplayer.util.ShaderUtil
import com.tencent.qgame.animplayer.util.TexCoordsUtil
import com.tencent.qgame.animplayer.util.VertexUtil
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.ShortBuffer
class Render(surfaceTexture: SurfaceTexture): IRenderListener {
companion object {
private const val TAG = "${Constant.TAG}.Render"
}
private val vertexArray = GlFloatArray()
private val alphaArray = GlFloatArray()
private val rgbArray = GlFloatArray()
private var surfaceSizeChanged = false
private var surfaceWidth = 0
private var surfaceHeight = 0
private val eglUtil: EGLUtil = EGLUtil()
private var shaderProgram = 0
private var genTexture = IntArray(1)
private var uTextureLocation: Int = 0
private var aPositionLocation: Int = 0
private var aTextureAlphaLocation: Int = 0
private var aTextureRgbLocation: Int = 0
init {
eglUtil.start(surfaceTexture)
initRender()
}
private fun setVertexBuf(config: AnimConfig) {
vertexArray.setArray(VertexUtil.create(config.width, config.height, PointRect(0, 0, config.width, config.height), vertexArray.array))
}
private fun setTexCoords(config: AnimConfig) {
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 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())
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat())
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
}
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)
}
/**
* 设置视频配置
*/
override fun setAnimConfig(config: AnimConfig) {
setVertexBuf(config)
setTexCoords(config)
}
/**
* 显示区域大小变化
*/
override fun updateViewPort(width: Int, height: Int) {
if (width <=0 || height <=0) return
surfaceSizeChanged = true
surfaceWidth = width
surfaceHeight = height
}
override fun swapBuffers() {
eglUtil.swapBuffers()
}
/**
* mediaCodec渲染使用的
*/
override fun getExternalTexture(): Int {
return genTexture[0]
}
private fun draw() {
GLES20.glUseProgram(shaderProgram)
// 设置顶点坐标
vertexArray.setVertexAttribPointer(aPositionLocation)
// 绑定纹理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, genTexture[0])
GLES20.glUniform1i(uTextureLocation, 0)
// 设置纹理坐标
// alpha 通道坐标
alphaArray.setVertexAttribPointer(aTextureAlphaLocation)
// rgb 通道坐标
rgbArray.setVertexAttribPointer(aTextureRgbLocation)
// draw
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
}
}

View File

@@ -0,0 +1,43 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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 RenderConstant {
const val VERTEX_SHADER = "attribute vec4 vPosition;\n" +
"attribute vec4 vTexCoordinateAlpha;\n" +
"attribute vec4 vTexCoordinateRgb;\n" +
"varying vec2 v_TexCoordinateAlpha;\n" +
"varying vec2 v_TexCoordinateRgb;\n" +
"\n" +
"void main() {\n" +
" v_TexCoordinateAlpha = vec2(vTexCoordinateAlpha.x, vTexCoordinateAlpha.y);\n" +
" v_TexCoordinateRgb = vec2(vTexCoordinateRgb.x, vTexCoordinateRgb.y);\n" +
" gl_Position = vPosition;\n" +
"}"
const val FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;\n" +
"uniform samplerExternalOES texture;\n" +
"varying vec2 v_TexCoordinateAlpha;\n" +
"varying vec2 v_TexCoordinateRgb;\n" +
"\n" +
"void main () {\n" +
" vec4 alphaColor = texture2D(texture, v_TexCoordinateAlpha);\n" +
" vec4 rgbColor = texture2D(texture, v_TexCoordinateRgb);\n" +
" gl_FragColor = vec4(rgbColor.r, rgbColor.g, rgbColor.b, alphaColor.r);\n" +
"}"
}

View File

@@ -0,0 +1,207 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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)
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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)
}

View File

@@ -0,0 +1,65 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.file
import android.content.res.AssetFileDescriptor
import android.content.res.AssetManager
import android.media.MediaExtractor
import com.tencent.qgame.animplayer.Constant
import com.tencent.qgame.animplayer.util.ALog
class AssetsFileContainer(assetManager: AssetManager, assetsPath: String): IFileContainer {
companion object {
private const val TAG = "${Constant.TAG}.FileContainer"
}
private val assetFd: AssetFileDescriptor = assetManager.openFd(assetsPath)
private val assetsInputStream: AssetManager.AssetInputStream =
assetManager.open(assetsPath, AssetManager.ACCESS_STREAMING) as AssetManager.AssetInputStream
init {
ALog.i(TAG, "AssetsFileContainer init")
}
override fun setDataSource(extractor: MediaExtractor) {
if (assetFd.declaredLength < 0) {
extractor.setDataSource(assetFd.fileDescriptor)
} else {
extractor.setDataSource(assetFd.fileDescriptor, assetFd.startOffset, assetFd.declaredLength)
}
}
override fun startRandomRead() {
}
override fun read(b: ByteArray, off: Int, len: Int): Int {
return assetsInputStream.read(b, off, len)
}
override fun skip(pos: Long) {
assetsInputStream.skip(pos)
}
override fun closeRandomRead() {
assetsInputStream.close()
}
override fun close() {
assetFd.close()
assetsInputStream.close()
}
}

View File

@@ -0,0 +1,60 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.file
import android.media.MediaExtractor
import com.tencent.qgame.animplayer.Constant
import com.tencent.qgame.animplayer.util.ALog
import java.io.File
import java.io.FileNotFoundException
import java.io.RandomAccessFile
class FileContainer(private val file: File) : IFileContainer {
companion object {
private const val TAG = "${Constant.TAG}.FileContainer"
}
private var randomAccessFile: RandomAccessFile? = null
init {
ALog.i(TAG, "FileContainer init")
if (!(file.exists() && file.isFile && file.canRead())) throw FileNotFoundException("Unable to read $file")
}
override fun setDataSource(extractor: MediaExtractor) {
extractor.setDataSource(file.toString())
}
override fun startRandomRead() {
randomAccessFile = RandomAccessFile(file, "r")
}
override fun read(b: ByteArray, off: Int, len: Int): Int {
return randomAccessFile?.read(b, off, len) ?: -1
}
override fun skip(pos: Long) {
randomAccessFile?.skipBytes(pos.toInt())
}
override fun closeRandomRead() {
randomAccessFile?.close()
}
override fun close() {
}
}

View File

@@ -0,0 +1,34 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.file
import android.media.MediaExtractor
interface IFileContainer {
fun setDataSource(extractor: MediaExtractor)
fun startRandomRead()
fun read(b: ByteArray, off: Int, len: Int): Int
fun skip(pos: Long)
fun closeRandomRead()
fun close()
}

View File

@@ -0,0 +1,50 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.file
import android.annotation.TargetApi
import android.media.MediaExtractor
import android.os.Build
import java.io.ByteArrayInputStream
@TargetApi(Build.VERSION_CODES.M)
class StreamContainer(private val bytes: ByteArray) : IFileContainer {
private var stream: ByteArrayInputStream = ByteArrayInputStream(bytes)
override fun setDataSource(extractor: MediaExtractor) {
val dataSource = StreamMediaDataSource(bytes)
extractor.setDataSource(dataSource)
}
override fun startRandomRead() {
}
override fun read(b: ByteArray, off: Int, len: Int): Int {
return stream.read(b, off, len)
}
override fun skip(pos: Long) {
stream.skip(pos)
}
override fun closeRandomRead() {
}
override fun close() {
stream.close()
}
}

View File

@@ -0,0 +1,49 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.file
import android.annotation.TargetApi
import android.media.MediaDataSource
import android.os.Build
@TargetApi(Build.VERSION_CODES.M)
class StreamMediaDataSource(val bytes: ByteArray) : MediaDataSource() {
override fun close() {
}
override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int {
var newSize = size
synchronized(StreamMediaDataSource::class) {
val length = bytes.size
if (position >= length) {
return -1
}
if (position + newSize > length) {
newSize -= (position + newSize).toInt() - length
}
System.arraycopy(bytes, position.toInt(), buffer, offset, newSize)
return newSize
}
}
override fun getSize(): Long {
synchronized(StreamMediaDataSource::class) {
return bytes.size.toLong()
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.
*/
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.inter
import com.tencent.qgame.animplayer.AnimConfig
interface IAnimListener {
/**
* 配置准备好后回调
* ps:如果是默认配置(没有发现vapc配置)因为信息不完整onVideoConfigReady不会被调用默认播放
* @return true 继续播放 false 结束播放
*/
fun onVideoConfigReady(config: AnimConfig): Boolean {
return true // 默认继续播放
}
/**
* 开始播放
*/
fun onVideoStart()
/**
* 视频渲染每一帧时的回调
* @param frameIndex 帧索引
*/
fun onVideoRender(frameIndex: Int, config: AnimConfig?)
/**
* 视频播放结束
*/
fun onVideoComplete()
/**
* 视频被销毁
*/
fun onVideoDestroy()
/**
* 失败回调
* @param errorType 错误类型
* @param errorMsg 错误消息
*/
fun onFailed(errorType: Int, errorMsg: String?)
}

View File

@@ -0,0 +1,33 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.inter
import android.graphics.Bitmap
import com.tencent.qgame.animplayer.mix.Resource
/**
* 获取资源
*/
interface IFetchResource {
// 获取图片 (暂时不支持Bitmap.Config.ALPHA_8 主要是因为一些机型opengl兼容问题)
fun fetchImage(resource: Resource, result:(Bitmap?) -> Unit)
// 获取文字
fun fetchText(resource: Resource, result:(String?) -> Unit)
// 资源释放通知
fun releaseResource(resources: List<Resource>)
}

View File

@@ -0,0 +1,24 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.inter
import com.tencent.qgame.animplayer.mix.Resource
interface OnResourceClickListener {
// 返回被点击的资源
fun onClick(resource: Resource)
}

View File

@@ -0,0 +1,66 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.mask
import com.tencent.qgame.animplayer.AnimConfig
import com.tencent.qgame.animplayer.AnimPlayer
import com.tencent.qgame.animplayer.Constant
import com.tencent.qgame.animplayer.plugin.IAnimPlugin
import com.tencent.qgame.animplayer.util.ALog
class MaskAnimPlugin(val player: AnimPlayer): IAnimPlugin {
companion object {
private const val TAG = "${Constant.TAG}.MaskAnimPlugin"
}
private var maskRender: MaskRender? = null
private var animConfig : AnimConfig ?= null
override fun onConfigCreate(config: AnimConfig): Int {
return Constant.OK
}
override fun onRenderCreate() {
ALog.i(TAG, "mask render init")
maskRender = if(player.supportMaskBoolean) MaskRender(this) else return
maskRender?.initMaskShader(player.maskEdgeBlurBoolean)
}
override fun onRendering(frameIndex: Int) {
animConfig = if(player.supportMaskBoolean && player.configManager.config is AnimConfig) player.configManager.config else return
animConfig?.let {config ->
maskRender?.renderFrame(config)
}
}
override fun onRelease() {
destroy()
}
override fun onDestroy() {
destroy()
}
private fun destroy() {
// 强制结束等待
animConfig?.maskConfig?.release()
}
}

View File

@@ -0,0 +1,74 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.mask
import android.graphics.Bitmap
import com.tencent.qgame.animplayer.PointRect
import com.tencent.qgame.animplayer.RefVec2
import com.tencent.qgame.animplayer.util.TextureLoadUtil
class MaskConfig() {
var maskTexPair: Pair<PointRect, RefVec2>? = null //遮罩坐标矩形
var maskPositionPair: Pair<PointRect, RefVec2>? = null //内容坐标矩形
constructor(bitmap: Bitmap?, positionPair :Pair<PointRect, RefVec2>?, texPair: Pair<PointRect, RefVec2>?) : this() {
maskPositionPair = positionPair
maskTexPair = texPair
alphaMaskBitmap = bitmap
}
private var maskTexId = 0
fun getMaskTexId() : Int {
return maskTexId
}
fun updateMaskTex() : Int {
maskTexId = TextureLoadUtil.loadTexture(alphaMaskBitmap)
return maskTexId
}
var alphaMaskBitmap: Bitmap? = null //遮罩
private set(value) {
field = value
}
fun safeSetMaskBitmapAndReleasePre(bitmap: Bitmap?) {
if (maskTexId > 0) { //释放
TextureLoadUtil.releaseTexure(maskTexId)
maskTexId = 0
}
alphaMaskBitmap = bitmap
}
fun release() {
alphaMaskBitmap = null
maskTexPair = null
maskPositionPair = null
}
override fun equals(other: Any?): Boolean {
return other is MaskConfig && this.alphaMaskBitmap != other.alphaMaskBitmap
&& this.maskTexPair?.first != other.maskTexPair?.first && this.maskTexPair?.second != other.maskTexPair?.second
&& this.maskPositionPair?.first != other.maskPositionPair?.first && this.maskPositionPair?.second != other.maskPositionPair?.second
}
override fun hashCode(): Int {
var result = alphaMaskBitmap?.hashCode() ?: 0
result = 31 * result + (maskTexPair?.hashCode() ?: 0)
result = 31 * result + (maskPositionPair?.hashCode() ?: 0)
return result
}
}

View File

@@ -0,0 +1,130 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.mask
import android.opengl.GLES11Ext
import android.opengl.GLES20
import com.tencent.qgame.animplayer.AnimConfig
import com.tencent.qgame.animplayer.Constant
import com.tencent.qgame.animplayer.PointRect
import com.tencent.qgame.animplayer.RefVec2
import com.tencent.qgame.animplayer.util.GlFloatArray
import com.tencent.qgame.animplayer.util.TexCoordsUtil
import com.tencent.qgame.animplayer.util.VertexUtil
/**
* vapx 渲染
*/
class MaskRender(private val maskAnimPlugin: MaskAnimPlugin) {
companion object {
private const val TAG = "${Constant.TAG}.MaskRender"
}
var maskShader: MaskShader? = null
var vertexArray = GlFloatArray()
private var maskArray = GlFloatArray()
/**
* shader 与 texture初始化
*/
fun initMaskShader(edgeBlur: Boolean) {
// shader 初始化
maskShader = MaskShader(edgeBlur)
GLES20.glDisable(GLES20.GL_DEPTH_TEST) // 关闭深度测试
}
fun renderFrame(config: AnimConfig) {
val videoTextureId = maskAnimPlugin.player.decoder?.render?.getExternalTexture() ?: return
if (videoTextureId <= 0) return
val shader = this.maskShader ?: return
var maskTexId: Int = config.maskConfig?.getMaskTexId() ?: return
val maskBitmap = config.maskConfig?.alphaMaskBitmap ?: return
val maskTexRect = config.maskConfig?.maskTexPair?.first ?: return
val maskTexRefVec2 = config.maskConfig?.maskTexPair?.second ?: return
val maskPositionRect = config.maskConfig?.maskPositionPair?.first ?: PointRect(
0,
0,
config.width,
config.height
)
val maskPositionRefVec2 =
config.maskConfig?.maskPositionPair?.second ?: RefVec2(config.width, config.height)
shader.useProgram()
// 顶点坐标
vertexArray.setArray(
VertexUtil.create(
maskPositionRefVec2.w,
maskPositionRefVec2.h,
maskPositionRect,
vertexArray.array
)
)
vertexArray.setVertexAttribPointer(shader.aPositionLocation)
if (maskTexId <= 0 && !maskBitmap.isRecycled) {
maskTexId = config.maskConfig?.updateMaskTex() ?: 0
}
if (maskTexId > 0) {
maskArray.setArray(
TexCoordsUtil.create(
maskTexRefVec2.w,
maskTexRefVec2.h,
maskTexRect,
maskArray.array
)
)
maskArray.setVertexAttribPointer(shader.aTextureMaskCoordinatesLocation)
// 绑定alpha纹理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, maskTexId)
GLES20.glTexParameterf(
GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_NEAREST.toFloat()
)
GLES20.glTexParameterf(
GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR.toFloat()
)
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_CLAMP_TO_EDGE
)
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE
)
GLES20.glUniform1i(shader.uTextureMaskUnitLocation, 0)
GLES20.glEnable(GLES20.GL_BLEND)
// 基于源象素alpha通道值的半透明混合函数
GLES20.glBlendFuncSeparate(
GLES20.GL_ONE,
GLES20.GL_SRC_ALPHA,
GLES20.GL_ZERO,
GLES20.GL_SRC_ALPHA
)
// draw
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
GLES20.glDisable(GLES20.GL_BLEND)
}
}
}

View File

@@ -0,0 +1,114 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.mask
import android.opengl.GLES20
import android.util.Log
import com.tencent.qgame.animplayer.util.ShaderUtil
class MaskShader(edgeBlurBoolean : Boolean) {
companion object {
private const val VERTEX =
"attribute vec4 vPosition;\n" +
"attribute vec4 vTexCoordinateAlphaMask;\n" +
"varying vec2 v_TexCoordinateAlphaMask;\n" +
"\n" +
"void main() {\n" +
" v_TexCoordinateAlphaMask = vec2(vTexCoordinateAlphaMask.x, vTexCoordinateAlphaMask.y);\n" +
" gl_Position = vPosition;\n" +
"}"
//边缘做高斯模糊
private const val FRAGMENT_BLUR_EDGE =
"precision mediump float;\n" +
"uniform sampler2D uTextureAlphaMask;\n" +
"varying vec2 v_TexCoordinateAlphaMask;\n" +
"mat3 weight = mat3(0.0625,0.125,0.0625,0.125,0.25,0.125,0.0625,0.125,0.0625);\n " +
"int coreSize=3;\n" +
"float texelOffset = .01;\n" +
"\n" +
"void main() {\n" +
" float alphaResult = 0.;\n" +
" for(int y = 0; y < coreSize; y++) {\n" +
" for(int x = 0;x < coreSize; x++) {\n" +
" alphaResult += texture2D(uTextureAlphaMask, vec2(v_TexCoordinateAlphaMask.x + (-1.0 + float(x)) * texelOffset,v_TexCoordinateAlphaMask.y + (-1.0 + float(y)) * texelOffset)).a * weight[x][y];\n" +
" }\n" +
" }\n" +
" gl_FragColor = vec4(0, 0, 0, alphaResult);\n" +
"}"
private const val FRAGMENT_NO_BLUR_EDGE =
"precision mediump float;\n" +
"uniform sampler2D uTextureAlphaMask;\n" +
"varying vec2 v_TexCoordinateAlphaMask;\n" +
"\n" +
"void main () {\n" +
" vec4 alphaMaskColor = texture2D(uTextureAlphaMask, v_TexCoordinateAlphaMask);\n" +
" gl_FragColor = vec4(0, 0, 0, alphaMaskColor.a);\n" +
"}"
private const val FRAGMENT_ROW =
"precision mediump float;\n" +
"uniform sampler2D uTextureAlphaMask;\n" +
"varying vec2 v_TexCoordinateAlphaMask;\n" +
"vec3 weight = vec3(0.4026,0.2442,0.0545);\n " +
"\n" +
"void main() {\n" +
" float texelOffset = .01;\n" +
" vec2 uv[5];\n" +
" uv[0]= v_TexCoordinateAlphaMask;\n" +
" uv[1]=vec2(uv[0].x+texelOffset*1.0, uv[0].y);\n" +
" uv[2]=vec2(uv[0].x-texelOffset*1.0, uv[0].y);\n" +
" uv[3]=vec2(uv[0].x+texelOffset*2.0, uv[0].y);\n" +
" uv[4]=vec2(uv[0].x-texelOffset*2.0, uv[0].y);\n" +
" float alphaResult = texture2D(uTextureAlphaMask, uv[0]).a * weight[0];\n" +
" for(int i = 1; i < 3; ++i) {\n" +
" alphaResult += texture2D(uTextureAlphaMask, uv[2*i-1]).a * weight[i];\n" +
" alphaResult += texture2D(uTextureAlphaMask, uv[2*i]).a * weight[i];\n" +
" }\n" +
" gl_FragColor = vec4(0, 0, 0, alphaResult);\n" +
"}"
// Uniform constants
private const val U_TEXTURE_ALPHA_MASK_UNIT = "uTextureAlphaMask"
// Attribute constants
private const val A_POSITION = "vPosition"
private const val A_TEXTURE_MASK_COORDINATES = "vTexCoordinateAlphaMask"
}
// Shader program
private val program: Int
// Uniform locations
val uTextureMaskUnitLocation: Int
// Attribute locations
val aPositionLocation: Int
val aTextureMaskCoordinatesLocation: Int
init {
program = if(edgeBlurBoolean) ShaderUtil.createProgram(VERTEX, FRAGMENT_BLUR_EDGE) else ShaderUtil.createProgram(VERTEX, FRAGMENT_NO_BLUR_EDGE)
uTextureMaskUnitLocation = GLES20.glGetUniformLocation(program, U_TEXTURE_ALPHA_MASK_UNIT)
aPositionLocation = GLES20.glGetAttribLocation(program, A_POSITION)
aTextureMaskCoordinatesLocation = GLES20.glGetAttribLocation(program, A_TEXTURE_MASK_COORDINATES)
}
fun useProgram() {
GLES20.glUseProgram(program)
}
}

View File

@@ -0,0 +1,83 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.mix
import android.util.SparseArray
import com.tencent.qgame.animplayer.PointRect
import org.json.JSONObject
/**
* 单帧
*/
class Frame(val index: Int, json: JSONObject) {
var srcId = ""
var z = 0
var frame: PointRect
var mFrame: PointRect
var mt = 0 // 遮罩旋转角度v2 版本只支持 0 与 90度
init {
srcId = json.getString("srcId")
z = json.getInt("z")
val f = json.getJSONArray("frame")
frame = PointRect(f.getInt(0), f.getInt(1), f.getInt(2), f.getInt(3))
val m = json.getJSONArray("mFrame")
mFrame = PointRect(m.getInt(0), m.getInt(1), m.getInt(2), m.getInt(3))
mt = json.getInt("mt")
}
}
/**
* 一帧的集合
*/
class FrameSet(json: JSONObject) {
var index = 0 // 哪一帧
val list = ArrayList<Frame>()
init {
index = json.getInt("i")
val objJsonArray = json.getJSONArray("obj")
val objLen = objJsonArray?.length() ?: 0
for (i in 0 until objLen) {
val frameJson = objJsonArray?.getJSONObject(i) ?: continue
val frame = Frame(index, frameJson)
list.add(frame)
}
// 绘制顺序排序
list.sortBy {it.z}
}
}
/**
* 所有帧集合
*/
class FrameAll(json: JSONObject) {
// 每一帧的集合
val map = SparseArray<FrameSet>()
init {
val frameJsonArray = json.getJSONArray("frame")
val frameLen = frameJsonArray?.length() ?: 0
for (i in 0 until frameLen) {
val frameSetJson = frameJsonArray?.getJSONObject(i) ?: continue
val frameSet = FrameSet(frameSetJson)
map.put(frameSet.index, frameSet)
}
}
}

View File

@@ -0,0 +1,229 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.mix
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.view.MotionEvent
import com.tencent.qgame.animplayer.AnimConfig
import com.tencent.qgame.animplayer.AnimPlayer
import com.tencent.qgame.animplayer.Constant
import com.tencent.qgame.animplayer.inter.IFetchResource
import com.tencent.qgame.animplayer.inter.OnResourceClickListener
import com.tencent.qgame.animplayer.plugin.IAnimPlugin
import com.tencent.qgame.animplayer.util.ALog
import com.tencent.qgame.animplayer.util.BitmapUtil
class MixAnimPlugin(val player: AnimPlayer): IAnimPlugin {
companion object {
private const val TAG = "${Constant.TAG}.MixAnimPlugin"
}
var resourceRequest: IFetchResource? = null
var resourceClickListener: OnResourceClickListener? = null
var srcMap: SrcMap? = null
var frameAll: FrameAll? = null
var curFrameIndex = -1 // 当前帧
private var resultCbCount = 0 // 回调次数
private var mixRender:MixRender? = null
private val mixTouch by lazy { MixTouch(this) }
var autoTxtColorFill = true // 是否启动自动文字填充 默认开启
// 同步锁
private val lock = Object()
private var forceStopLock = false
override fun onConfigCreate(config: AnimConfig): Int {
if (!config.isMix) return Constant.OK
if (resourceRequest == null) {
ALog.e(TAG, "IFetchResource is empty")
// 没有设置IFetchResource 当成普通视频播放
return Constant.OK
}
// step 1 parse src
parseSrc(config)
// step 2 parse frame
parseFrame(config)
// step 3 fetch resource
fetchResourceSync()
// step 4 生成文字bitmap
val result = createBitmap()
if (!result) {
return Constant.REPORT_ERROR_TYPE_CONFIG_PLUGIN_MIX
}
// step 5 check resource
ALog.i(TAG, "load resource $resultCbCount")
srcMap?.map?.values?.forEach {
if (it.bitmap == null) {
ALog.e(TAG, "missing src $it")
return Constant.REPORT_ERROR_TYPE_CONFIG_PLUGIN_MIX
} else if (it.bitmap?.config == Bitmap.Config.ALPHA_8) {
ALog.e(TAG, "src $it bitmap must not be ALPHA_8")
return Constant.REPORT_ERROR_TYPE_CONFIG_PLUGIN_MIX
}
}
return Constant.OK
}
override fun onRenderCreate() {
if (player.configManager.config?.isMix == false) return
ALog.i(TAG, "mix render init")
mixRender = MixRender(this)
mixRender?.init()
}
override fun onRendering(frameIndex: Int) {
val config = player.configManager.config ?: return
if (!config.isMix) return
curFrameIndex = frameIndex
val list = frameAll?.map?.get(frameIndex)?.list ?: return
list.forEach {frame ->
val src = srcMap?.map?.get(frame.srcId) ?: return@forEach
mixRender?.renderFrame(config, frame, src)
}
}
override fun onRelease() {
destroy()
}
override fun onDestroy() {
destroy()
}
override fun onDispatchTouchEvent(ev: MotionEvent): Boolean {
if (player.configManager.config?.isMix == false || resourceClickListener == null) {
return super.onDispatchTouchEvent(ev)
}
mixTouch.onTouchEvent(ev)?.let {resource ->
Handler(Looper.getMainLooper()).post {
resourceClickListener?.onClick(resource)
}
}
// 只要注册监听则拦截所有事件
return true
}
private fun destroy() {
// 强制结束等待
forceStopLockThread()
if (player.configManager.config?.isMix == false) return
val resources = ArrayList<Resource>()
srcMap?.map?.values?.forEach {src ->
mixRender?.release(src.srcTextureId)
when(src.srcType) {
Src.SrcType.IMG -> resources.add(Resource(src))
Src.SrcType.TXT -> src.bitmap?.recycle()
else -> {}
}
}
resourceRequest?.releaseResource(resources)
// 清理
curFrameIndex = -1
srcMap?.map?.clear()
frameAll?.map?.clear()
}
private fun parseSrc(config: AnimConfig) {
config.jsonConfig?.apply {
srcMap = SrcMap(this)
}
}
private fun parseFrame(config: AnimConfig) {
config.jsonConfig?.apply {
frameAll = FrameAll(this)
}
}
private fun fetchResourceSync() {
synchronized(lock) {
forceStopLock = false // 开始时不会强制关闭
}
val time = SystemClock.elapsedRealtime()
val totalSrc = srcMap?.map?.size ?: 0
ALog.i(TAG, "load resource totalSrc = $totalSrc")
resultCbCount = 0
srcMap?.map?.values?.forEach {src ->
if (src.srcType == Src.SrcType.IMG) {
ALog.i(TAG, "fetch image ${src.srcId}")
resourceRequest?.fetchImage(Resource(src)) {
src.bitmap = if (it == null) {
ALog.e(TAG, "fetch image ${src.srcId} bitmap return null")
BitmapUtil.createEmptyBitmap()
} else it
ALog.i(TAG, "fetch image ${src.srcId} finish bitmap is ${it?.hashCode()}")
resultCall()
}
} else if (src.srcType == Src.SrcType.TXT) {
ALog.i(TAG, "fetch txt ${src.srcId}")
resourceRequest?.fetchText(Resource(src)) {
src.txt = it ?: ""
ALog.i(TAG, "fetch text ${src.srcId} finish txt is $it")
resultCall()
}
}
}
// 同步等待所有资源完成
synchronized(lock) {
while (resultCbCount < totalSrc && !forceStopLock) {
lock.wait()
}
}
ALog.i(TAG, "fetchResourceSync cost=${SystemClock.elapsedRealtime() - time}ms")
}
private fun forceStopLockThread() {
synchronized(lock) {
forceStopLock = true
lock.notifyAll()
}
}
private fun resultCall() {
synchronized(lock) {
resultCbCount++
lock.notifyAll()
}
}
private fun createBitmap(): Boolean {
return try {
srcMap?.map?.values?.forEach { src ->
if (src.srcType == Src.SrcType.TXT) {
src.bitmap = BitmapUtil.createTxtBitmap(src)
}
}
true
} catch (e: OutOfMemoryError) {
ALog.e(TAG, "draw text OOM $e", e)
false
}
}
}

View File

@@ -0,0 +1,151 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.mix
import android.opengl.GLES11Ext
import android.opengl.GLES20
import com.tencent.qgame.animplayer.AnimConfig
import com.tencent.qgame.animplayer.Constant
import com.tencent.qgame.animplayer.PointRect
import com.tencent.qgame.animplayer.util.*
/**
* vapx 渲染
*/
class MixRender(private val mixAnimPlugin: MixAnimPlugin) {
companion object {
private const val TAG = "${Constant.TAG}.MixRender"
}
var shader: MixShader? = null
var vertexArray = GlFloatArray()
var srcArray = GlFloatArray()
var maskArray = GlFloatArray()
/**
* shader 与 texture初始化
*/
fun init() {
// shader 初始化
shader = MixShader()
GLES20.glDisable(GLES20.GL_DEPTH_TEST) // 关闭深度测试
mixAnimPlugin.srcMap?.map?.values?.forEach {src->
ALog.i(TAG, "init srcId=${src.srcId}")
src.srcTextureId = TextureLoadUtil.loadTexture(src.bitmap)
ALog.i(TAG, "textureProgram=${shader?.program},textureId=${src.srcTextureId}")
}
}
fun renderFrame(config:AnimConfig, frame:Frame, src: Src) {
val videoTextureId = mixAnimPlugin.player.decoder?.render?.getExternalTexture() ?: return
if (videoTextureId <= 0) return
val shader = this.shader ?: return
shader.useProgram()
// 定点坐标
vertexArray.setArray(VertexUtil.create(config.width, config.height, frame.frame, vertexArray.array))
vertexArray.setVertexAttribPointer(shader.aPositionLocation)
// src 纹理坐标
srcArray.setArray(genSrcCoordsArray(srcArray.array, frame.frame.w, frame.frame.h, src.drawWidth, src.drawHeight, src.fitType))
srcArray.setVertexAttribPointer(shader.aTextureSrcCoordinatesLocation)
// 绑定 src纹理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, src.srcTextureId)
GLES20.glUniform1i(shader.uTextureSrcUnitLocation, 0)
// mask 纹理
maskArray.setArray(TexCoordsUtil.create(config.videoWidth, config.videoHeight, frame.mFrame, maskArray.array))
if (frame.mt == 90) {
maskArray.setArray(TexCoordsUtil.rotate90(maskArray.array))
}
maskArray.setVertexAttribPointer(shader.aTextureMaskCoordinatesLocation)
// 绑定 mask纹理
GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTextureId)
GLES20.glUniform1i(shader.uTextureMaskUnitLocation, 1)
// 属性处理
if (src.srcType == Src.SrcType.TXT && mixAnimPlugin.autoTxtColorFill) { // // 文字需要颜色填充
GLES20.glUniform1i(shader.uIsFillLocation, 1)
val argb = transColor(src.color)
GLES20.glUniform4f(shader.uColorLocation, argb[1], argb[2], argb[3], argb[0])
} else {
GLES20.glUniform1i(shader.uIsFillLocation, 0)
GLES20.glUniform4f(shader.uColorLocation, 0f, 0f, 0f, 0f)
}
GLES20.glEnable(GLES20.GL_BLEND)
// 基于源象素alpha通道值的半透明混合函数
GLES20.glBlendFuncSeparate(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA, GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA)
// draw
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
GLES20.glDisable(GLES20.GL_BLEND)
}
fun release(textureId: Int) {
if (textureId != 0) {
GLES20.glDeleteTextures(1, intArrayOf(textureId), 0)
}
}
/**
* CENTER_FULL 并不是严格的centerCrop(centerCrop已经前置处理)此处主要是为防抖动做处理复杂遮罩情况下需要固定src大小进行绘制防止抖动
*/
private fun genSrcCoordsArray(array: FloatArray, fw: Int, fh: Int, sw: Int, sh: Int, fitType: Src.FitType): FloatArray {
return if (fitType == Src.FitType.CENTER_FULL) {
if (fw <= sw && fh <= sh) {
// 中心对齐,不拉伸
val gw = (sw - fw) / 2
val gh = (sh - fh) / 2
TexCoordsUtil.create(sw, sh, PointRect(gw, gh, fw, fh), array)
} else { // centerCrop
val fScale = fw * 1.0f / fh
val sScale = sw * 1.0f / sh
val srcRect = if (fScale > sScale) {
val w = sw
val x = 0
val h = (sw / fScale).toInt()
val y = (sh - h) / 2
PointRect(x, y, w, h)
} else {
val h = sh
val y = 0
val w = (sh * fScale).toInt()
val x = (sw - w) / 2
PointRect(x, y, w, h)
}
TexCoordsUtil.create(sw, sh, srcRect, array)
}
} else { // 默认 fitXY
TexCoordsUtil.create(fw, fh, PointRect(0, 0, fw, fh), array)
}
}
private fun transColor(color: Int): FloatArray {
val argb = FloatArray(4)
argb[0] = (color.ushr(24) and 0x000000ff) / 255f
argb[1] = (color.ushr(16) and 0x000000ff) / 255f
argb[2] = (color.ushr(8) and 0x000000ff) / 255f
argb[3] = (color and 0x000000ff) / 255f
return argb
}
}

View File

@@ -0,0 +1,94 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.mix
import android.opengl.GLES20
import com.tencent.qgame.animplayer.util.ShaderUtil
class MixShader {
companion object {
private const val VERTEX = "attribute vec4 a_Position; \n" +
"attribute vec2 a_TextureSrcCoordinates;\n" +
"attribute vec2 a_TextureMaskCoordinates;\n" +
"varying vec2 v_TextureSrcCoordinates;\n" +
"varying vec2 v_TextureMaskCoordinates;\n" +
"void main()\n" +
"{\n" +
" v_TextureSrcCoordinates = a_TextureSrcCoordinates;\n" +
" v_TextureMaskCoordinates = a_TextureMaskCoordinates;\n" +
" gl_Position = a_Position;\n" +
"}"
private const val FRAGMENT = "#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float; \n" +
"uniform sampler2D u_TextureSrcUnit;\n" +
"uniform samplerExternalOES u_TextureMaskUnit;\n" +
"uniform int u_isFill;\n" +
"uniform vec4 u_Color;\n" +
"varying vec2 v_TextureSrcCoordinates;\n" +
"varying vec2 v_TextureMaskCoordinates;\n" +
"void main()\n" +
"{\n" +
" vec4 srcRgba = texture2D(u_TextureSrcUnit, v_TextureSrcCoordinates);\n" +
" vec4 maskRgba = texture2D(u_TextureMaskUnit, v_TextureMaskCoordinates);\n" +
" float isFill = step(0.5, float(u_isFill));\n" +
" vec4 srcRgbaCal = isFill * vec4(u_Color.r, u_Color.g, u_Color.b, srcRgba.a) + (1.0 - isFill) * srcRgba;\n" +
" gl_FragColor = vec4(srcRgbaCal.r, srcRgbaCal.g, srcRgbaCal.b, srcRgba.a * maskRgba.r);\n" +
"}"
// Uniform constants
private const val U_TEXTURE_SRC_UNIT = "u_TextureSrcUnit"
private const val U_TEXTURE_MASK_UNIT = "u_TextureMaskUnit"
private const val U_IS_FILL = "u_isFill"
private const val U_COLOR = "u_Color"
// Attribute constants
private const val A_POSITION = "a_Position"
private const val A_TEXTURE_SRC_COORDINATES = "a_TextureSrcCoordinates"
private const val A_TEXTURE_MASK_COORDINATES = "a_TextureMaskCoordinates"
}
// Shader program
val program: Int
// Uniform locations
val uTextureSrcUnitLocation: Int
val uTextureMaskUnitLocation: Int
val uIsFillLocation: Int
val uColorLocation: Int
// Attribute locations
val aPositionLocation: Int
val aTextureSrcCoordinatesLocation: Int
val aTextureMaskCoordinatesLocation: Int
init {
program = ShaderUtil.createProgram(VERTEX, FRAGMENT)
uTextureSrcUnitLocation = GLES20.glGetUniformLocation(program, U_TEXTURE_SRC_UNIT)
uTextureMaskUnitLocation = GLES20.glGetUniformLocation(program, U_TEXTURE_MASK_UNIT)
uIsFillLocation = GLES20.glGetUniformLocation(program, U_IS_FILL)
uColorLocation = GLES20.glGetUniformLocation(program, U_COLOR)
aPositionLocation = GLES20.glGetAttribLocation(program, A_POSITION)
aTextureSrcCoordinatesLocation = GLES20.glGetAttribLocation(program, A_TEXTURE_SRC_COORDINATES)
aTextureMaskCoordinatesLocation = GLES20.glGetAttribLocation(program, A_TEXTURE_MASK_COORDINATES)
}
fun useProgram() {
GLES20.glUseProgram(program)
}
}

View File

@@ -0,0 +1,56 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.mix
import android.view.MotionEvent
import com.tencent.qgame.animplayer.PointRect
/**
* 触摸事件
*/
class MixTouch(private val mixAnimPlugin: MixAnimPlugin) {
fun onTouchEvent(ev: MotionEvent): Resource? {
val (viewWith, viewHeight) = mixAnimPlugin.player.animView.getRealSize()
val videoWith = mixAnimPlugin.player.configManager.config?.width ?: return null
val videoHeight = mixAnimPlugin.player.configManager.config?.height ?: return null
if (viewWith == 0 || viewHeight == 0) return null
when(ev.action) {
MotionEvent.ACTION_UP -> {
val x = ev.x * videoWith / viewWith.toFloat()
val y = ev.y * videoHeight / viewHeight.toFloat()
val list = mixAnimPlugin.frameAll?.map?.get(mixAnimPlugin.curFrameIndex)?.list
list?.forEach {frame ->
val src = mixAnimPlugin.srcMap?.map?.get(frame.srcId) ?: return@forEach
if (calClick(x.toInt(), y.toInt(), frame.frame)) {
return Resource(src).apply {
curPoint = frame.frame
}
}
}
}
}
return null
}
private fun calClick(x: Int, y: Int, frame: PointRect): Boolean {
return x >= frame.x && x <= (frame.x + frame.w)
&& y >= frame.y && y <= (frame.y + frame.h)
}
}

View File

@@ -0,0 +1,39 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.mix
import android.graphics.Bitmap
import com.tencent.qgame.animplayer.PointRect
/**
* 资源描述
*/
class Resource(src: Src) {
var id = ""
var type = Src.SrcType.UNKNOWN
var loadType = Src.LoadType.UNKNOWN
var tag = ""
var bitmap: Bitmap? = null
var curPoint: PointRect? = null // src在当前帧的位置信息
init {
id = src.srcId
type = src.srcType
loadType = src.loadType
tag = src.srcTag
bitmap = src.bitmap
}
}

View File

@@ -0,0 +1,152 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.mix
import android.graphics.Bitmap
import android.graphics.Color
import com.tencent.qgame.animplayer.Constant
import com.tencent.qgame.animplayer.util.ALog
import org.json.JSONObject
class Src {
companion object {
private const val TAG = "${Constant.TAG}.Src"
}
enum class SrcType(val type: String) {
UNKNOWN("unknown"),
IMG("img"),
TXT("txt"),
}
enum class LoadType(val type: String) {
UNKNOWN("unknown"),
NET("net"), // 网络加载的图片
LOCAL("local"), // 本地加载的图片
}
enum class FitType(val type: String) {
FIT_XY("fitXY"), // 按原始大小填充纹理
CENTER_FULL("centerFull"), // 以纹理中心点放置
}
enum class Style(val style: String) {
DEFAULT("default"),
BOLD("b"), // 文字粗体
}
var srcId = ""
var w = 0
var h = 0
var drawWidth = 0
var drawHeight = 0
var srcType = SrcType.UNKNOWN
var loadType = LoadType.UNKNOWN
var srcTag = ""
var txt = ""
var style = Style.DEFAULT
var color: Int = 0
var fitType = FitType.FIT_XY
var srcTextureId = 0
var bitmap: Bitmap? = null
set(value) {
field = value
genDrawSize(value)
}
constructor(json: JSONObject) {
srcId = json.getString("srcId")
w = json.getInt("w")
h = json.getInt("h")
// 可选
var colorStr = json.optString("color", "#000000")
if (colorStr.isEmpty()) {
colorStr = "#000000"
}
color = Color.parseColor(colorStr)
srcTag = json.getString("srcTag")
txt = srcTag
srcType = when(json.getString("srcType")) {
SrcType.IMG.type -> SrcType.IMG
SrcType.TXT.type -> SrcType.TXT
else -> SrcType.UNKNOWN
}
loadType = when(json.getString("loadType")) {
LoadType.NET.type -> LoadType.NET
LoadType.LOCAL.type -> LoadType.LOCAL
else -> LoadType.UNKNOWN
}
fitType = when(json.getString("fitType")) {
FitType.CENTER_FULL.type -> FitType.CENTER_FULL
else -> FitType.FIT_XY
}
// 可选
style = when(json.optString("style", "")) {
Style.BOLD.style -> Style.BOLD
else -> Style.DEFAULT
}
ALog.i(TAG, "${toString()} color=$colorStr")
}
private fun genDrawSize(bitmap: Bitmap?) {
val bw = bitmap?.width?: w
val bh = bitmap?.height?: h
drawWidth = bw
drawHeight = bh
if (fitType == FitType.CENTER_FULL) {
if (w == 0 || h == 0) {
return
}
// 按src w h进行centerCrop处理
val srcRate = w.toFloat() / h.toFloat()
val bitmapRate = bw.toFloat() / bh.toFloat()
if (bitmapRate >= srcRate) {
drawHeight = h
drawWidth = (h * bitmapRate).toInt()
} else {
drawWidth = w
drawHeight = (w / bitmapRate).toInt()
}
}
}
override fun toString(): String {
return "Src(srcId='$srcId', srcType=$srcType, loadType=$loadType, srcTag='$srcTag', bitmap=$bitmap, txt='$txt')"
}
}
class SrcMap(json: JSONObject) {
val map = HashMap<String, Src>()
init {
val srcJsonArray = json.getJSONArray("src")
val srcLen = srcJsonArray?.length() ?: 0
for (i in 0 until srcLen) {
val srcJson = srcJsonArray?.getJSONObject(i) ?: continue
val src = Src(srcJson)
if (src.srcType != Src.SrcType.UNKNOWN) { // 不认识的srcType丢弃
map[src.srcId] = src
}
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.plugin
import android.view.MotionEvent
import com.tencent.qgame.animplayer.AnimConfig
import com.tencent.qgame.animplayer.AnimPlayer
import com.tencent.qgame.animplayer.Constant
import com.tencent.qgame.animplayer.mask.MaskAnimPlugin
import com.tencent.qgame.animplayer.mix.MixAnimPlugin
import com.tencent.qgame.animplayer.util.ALog
/**
* 动画插件管理
*/
class AnimPluginManager(val player: AnimPlayer) {
companion object {
private const val TAG = "${Constant.TAG}.AnimPluginManager"
private const val DIFF_TIMES = 4
}
private val mixAnimPlugin = MixAnimPlugin(player)
private val maskAnimPlugin = MaskAnimPlugin(player)
private val plugins = listOf<IAnimPlugin>(mixAnimPlugin, maskAnimPlugin)
// 当前渲染的帧
private var frameIndex = 0
// 当前解码的帧
private var decodeIndex = 0
// 帧不相同的次数, 连续多次不同则直接使用decodeIndex
private var frameDiffTimes = 0
fun getMixAnimPlugin(): MixAnimPlugin? {
return mixAnimPlugin
}
fun getMaskAnimPlugin() : MaskAnimPlugin? {
return maskAnimPlugin
}
fun onConfigCreate(config: AnimConfig): Int {
ALog.i(TAG, "onConfigCreate")
plugins.forEach {
val res = it.onConfigCreate(config)
if (res != Constant.OK) {
return res
}
}
return Constant.OK
}
fun onRenderCreate() {
ALog.i(TAG, "onRenderCreate")
frameIndex = 0
decodeIndex = 0
plugins.forEach {
it.onRenderCreate()
}
}
fun onDecoding(decodeIndex: Int) {
ALog.d(TAG, "onDecoding decodeIndex=$decodeIndex")
this.decodeIndex = decodeIndex
plugins.forEach {
it.onDecoding(decodeIndex)
}
}
// 开始循环调用
fun onLoopStart() {
ALog.i(TAG, "onLoopStart")
frameIndex = 0
decodeIndex = 0
}
fun onRendering() {
if (decodeIndex > frameIndex + 1 || frameDiffTimes >= DIFF_TIMES) {
ALog.i(TAG, "jump frameIndex= $frameIndex,decodeIndex=$decodeIndex,frameDiffTimes=$frameDiffTimes")
frameIndex = decodeIndex
}
if (decodeIndex != frameIndex) {
frameDiffTimes++
} else {
frameDiffTimes = 0
}
ALog.d(TAG, "onRendering frameIndex=$frameIndex")
plugins.forEach {
it.onRendering(frameIndex) // 第一帧 0
}
frameIndex++
}
fun onRelease() {
ALog.i(TAG, "onRelease")
plugins.forEach {
it.onRelease()
}
}
fun onDestroy() {
ALog.i(TAG, "onDestroy")
plugins.forEach {
it.onDestroy()
}
}
fun onDispatchTouchEvent(ev: MotionEvent): Boolean {
plugins.forEach {
if (it.onDispatchTouchEvent(ev)) return true
}
return false
}
}

View File

@@ -0,0 +1,48 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.plugin
import android.view.MotionEvent
import com.tencent.qgame.animplayer.AnimConfig
import com.tencent.qgame.animplayer.Constant
interface IAnimPlugin {
// 配置生成
fun onConfigCreate(config: AnimConfig): Int {
return Constant.OK
}
// 渲染初始化
fun onRenderCreate() {}
// 解码通知
fun onDecoding(decodeIndex: Int) {}
// 每一帧渲染
fun onRendering(frameIndex: Int) {}
// 每次播放完毕
fun onRelease() {}
// 销毁
fun onDestroy() {}
/** 触摸事件
* @return false 不拦截 true 拦截
*/
fun onDispatchTouchEvent(ev: MotionEvent): Boolean {return false}
}

View File

@@ -0,0 +1,21 @@
package com.tencent.qgame.animplayer.textureview
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.TextureView
import com.tencent.qgame.animplayer.AnimPlayer
class InnerTextureView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextureView(context, attrs, defStyleAttr) {
var player: AnimPlayer? = null
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
val res = player?.isRunning() == true
&& ev != null
&& player?.pluginManager?.onDispatchTouchEvent(ev) == true
return if (!res) super.dispatchTouchEvent(ev) else true
}
}

View File

@@ -0,0 +1,54 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.util
object ALog {
var isDebug = false
var log: IALog? = null
fun i(tag: String, msg: String) {
log?.i(tag, msg)
}
fun d(tag: String, msg: String) {
if (isDebug) {
log?.d(tag, msg)
}
}
fun e(tag: String, msg: String) {
log?.e(tag, msg)
}
fun e(tag: String, msg: String, tr: Throwable) {
log?.e(tag, msg, tr)
}
}
interface IALog {
fun i(tag: String, msg: String) {}
fun d(tag: String, msg: String) {}
fun e(tag: String, msg: String) {}
fun e(tag: String, msg: String, tr: Throwable) {}
}

View File

@@ -0,0 +1,68 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.util
import android.graphics.*
import android.text.TextPaint
import com.tencent.qgame.animplayer.mix.Src
object BitmapUtil {
fun createEmptyBitmap() : Bitmap {
return Bitmap.createBitmap(16, 16, Bitmap.Config.ARGB_8888).apply {
eraseColor(Color.TRANSPARENT)
}
}
fun createTxtBitmap(src: Src): Bitmap {
val w = src.w
val h = src.h
// 这里使用ALPHA_8 在opengl渲染的时候图像出现错位
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val rect = Rect(0, 0, w, h)
val bounds = Rect()
var sizeR = 0.8f
val paint = TextPaint().apply {
textSize = h * sizeR
textAlign = Paint.Align.CENTER
style = Paint.Style.FILL
isAntiAlias = true
if (src.style == Src.Style.BOLD) {
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
}
color = src.color
}
val text = src.txt
while (sizeR > 0.1f) {
paint.getTextBounds(text, 0, text.length, bounds)
if (bounds.width() <= rect.width()) {
break
}
sizeR -= 0.1f
paint.textSize = h * sizeR
}
val fontMetrics = paint.fontMetricsInt
val top = fontMetrics.top
val bottom = fontMetrics.bottom
val baseline = rect.centerY() - top/2 - bottom/2
canvas.drawText(text, rect.centerX().toFloat(), baseline.toFloat(), paint)
return bitmap
}
}

View File

@@ -0,0 +1,50 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.util
import android.opengl.GLES20
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import kotlin.FloatArray
class GlFloatArray {
val array = FloatArray(8)
private var floatBuffer: FloatBuffer
init {
floatBuffer = ByteBuffer
.allocateDirect(array.size * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(array)
}
fun setArray(array: FloatArray) {
floatBuffer.position(0)
floatBuffer.put(array)
}
fun setVertexAttribPointer(attributeLocation: Int) {
floatBuffer.position(0)
GLES20.glVertexAttribPointer(attributeLocation, 2, GLES20.GL_FLOAT, false, 0, floatBuffer)
GLES20.glEnableVertexAttribArray(attributeLocation)
}
}

View File

@@ -0,0 +1,107 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.util
import android.media.MediaCodecList
import android.media.MediaExtractor
import android.media.MediaFormat
import com.tencent.qgame.animplayer.Constant
import com.tencent.qgame.animplayer.file.IFileContainer
import kotlin.collections.HashMap
object MediaUtil {
private const val TAG = "${Constant.TAG}.MediaUtil"
private var isTypeMapInit = false
private val supportTypeMap = HashMap<String, Boolean>()
const val MIME_HEVC = "video/hevc"
fun getExtractor(file: IFileContainer): MediaExtractor {
val extractor = MediaExtractor()
file.setDataSource(extractor)
return extractor
}
/**
* 是否为h265的视频
*/
fun checkIsHevc(videoFormat: MediaFormat):Boolean {
val mime = videoFormat.getString(MediaFormat.KEY_MIME) ?: ""
return mime.contains("hevc")
}
fun selectVideoTrack(extractor: MediaExtractor): Int {
val numTracks = extractor.trackCount
for (i in 0 until numTracks) {
val format = extractor.getTrackFormat(i)
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
if (mime.startsWith("video/")) {
ALog.i(TAG, "Extractor selected track $i ($mime): $format")
return i
}
}
return -1
}
fun selectAudioTrack(extractor: MediaExtractor): Int {
val numTracks = extractor.trackCount
for (i in 0 until numTracks) {
val format = extractor.getTrackFormat(i)
val mime = format.getString(MediaFormat.KEY_MIME) ?: ""
if (mime.startsWith("audio/")) {
ALog.i(TAG, "Extractor selected track $i ($mime): $format")
return i
}
}
return -1
}
/**
* 检查设备解码支持类型
*/
@Synchronized
fun checkSupportCodec(mimeType: String): Boolean {
if (!isTypeMapInit) {
isTypeMapInit = true
getSupportType()
}
return supportTypeMap.containsKey(mimeType.toLowerCase())
}
private fun getSupportType() {
try {
val numCodecs = MediaCodecList.getCodecCount()
for (i in 0 until numCodecs) {
val codecInfo = MediaCodecList.getCodecInfoAt(i)
if (codecInfo.isEncoder) {
continue
}
val types = codecInfo.supportedTypes
for (j in types.indices) {
supportTypeMap[types[j].toLowerCase()] = true
}
}
ALog.i(TAG, "supportType=${supportTypeMap.keys}")
} catch (t: Throwable) {
ALog.e(TAG, "getSupportType $t")
}
}
}

View File

@@ -0,0 +1,252 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.util
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import com.tencent.qgame.animplayer.Constant
enum class ScaleType {
FIT_XY, // 完整填充整个布局 default
FIT_CENTER, // 按视频比例在布局中间完整显示
CENTER_CROP, // 按视频比例完整填充布局(多余部分不显示)
}
interface IScaleType {
fun getLayoutParam(
layoutWidth: Int,
layoutHeight: Int,
videoWidth: Int,
videoHeight: Int,
layoutParams: FrameLayout.LayoutParams
): FrameLayout.LayoutParams
fun getRealSize(): Pair<Int, Int>
}
class ScaleTypeFitXY : IScaleType {
private var realWidth = 0
private var realHeight = 0
override fun getLayoutParam(
layoutWidth: Int,
layoutHeight: Int,
videoWidth: Int,
videoHeight: Int,
layoutParams: FrameLayout.LayoutParams
): FrameLayout.LayoutParams {
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
realWidth = layoutWidth
realHeight = layoutHeight
return layoutParams
}
override fun getRealSize(): Pair<Int, Int> {
return Pair(realWidth, realHeight)
}
}
class ScaleTypeFitCenter : IScaleType {
private var realWidth = 0
private var realHeight = 0
override fun getLayoutParam(
layoutWidth: Int,
layoutHeight: Int,
videoWidth: Int,
videoHeight: Int,
layoutParams: FrameLayout.LayoutParams
): FrameLayout.LayoutParams {
val (w, h) = getFitCenterSize(layoutWidth, layoutHeight, videoWidth, videoHeight)
if (w <= 0 && h <= 0) return layoutParams
realWidth = w
realHeight = h
layoutParams.width = w
layoutParams.height = h
layoutParams.gravity = Gravity.CENTER
return layoutParams
}
override fun getRealSize(): Pair<Int, Int> {
return Pair(realWidth, realHeight)
}
private fun getFitCenterSize(
layoutWidth: Int,
layoutHeight: Int,
videoWidth: Int,
videoHeight: Int
): Pair<Int, Int> {
val layoutRatio = layoutWidth.toFloat() / layoutHeight
val videoRatio = videoWidth.toFloat() / videoHeight
val realWidth: Int
val realHeight: Int
if (layoutRatio > videoRatio) {
realHeight = layoutHeight
realWidth = (videoRatio * realHeight).toInt()
} else {
realWidth = layoutWidth
realHeight = (realWidth / videoRatio).toInt()
}
return Pair(realWidth, realHeight)
}
}
class ScaleTypeCenterCrop : IScaleType {
private var realWidth = 0
private var realHeight = 0
override fun getLayoutParam(
layoutWidth: Int,
layoutHeight: Int,
videoWidth: Int,
videoHeight: Int,
layoutParams: FrameLayout.LayoutParams
): FrameLayout.LayoutParams {
val (w, h) = getCenterCropSize(layoutWidth, layoutHeight, videoWidth, videoHeight)
if (w <= 0 && h <= 0) return layoutParams
realWidth = w
realHeight = h
layoutParams.width = w
layoutParams.height = h
layoutParams.gravity = Gravity.CENTER
return layoutParams
}
override fun getRealSize(): Pair<Int, Int> {
return Pair(realWidth, realHeight)
}
private fun getCenterCropSize(
layoutWidth: Int,
layoutHeight: Int,
videoWidth: Int,
videoHeight: Int
): Pair<Int, Int> {
val layoutRatio = layoutWidth.toFloat() / layoutHeight
val videoRatio = videoWidth.toFloat() / videoHeight
val realWidth: Int
val realHeight: Int
if (layoutRatio > videoRatio) {
realWidth = layoutWidth
realHeight = (realWidth / videoRatio).toInt()
} else {
realHeight = layoutHeight
realWidth = (videoRatio * realHeight).toInt()
}
return Pair(realWidth, realHeight)
}
}
class ScaleTypeUtil {
companion object {
private const val TAG = "${Constant.TAG}.ScaleTypeUtil"
}
private val scaleTypeFitXY by lazy { ScaleTypeFitXY() }
private val scaleTypeFitCenter by lazy { ScaleTypeFitCenter() }
private val scaleTypeCenterCrop by lazy { ScaleTypeCenterCrop() }
private var layoutWidth = 0
private var layoutHeight = 0
private var videoWidth = 0
private var videoHeight = 0
var currentScaleType = ScaleType.FIT_XY
var scaleTypeImpl: IScaleType? = null
fun setLayoutSize(w: Int, h: Int) {
layoutWidth = w
layoutHeight = h
}
fun setVideoSize(w: Int, h: Int) {
videoWidth = w
videoHeight = h
}
/**
* 获取实际视频容器宽高
* @return w h
*/
fun getRealSize(): Pair<Int, Int> {
val size = getCurrentScaleType().getRealSize()
ALog.i(TAG, "get real size (${size.first}, ${size.second})")
return size
}
fun getLayoutParam(view: View?): FrameLayout.LayoutParams {
val layoutParams = (view?.layoutParams as? FrameLayout.LayoutParams)
?: FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
if (!checkParams()) {
ALog.e(
TAG,
"params error: layoutWidth=$layoutWidth, layoutHeight=$layoutHeight, videoWidth=$videoWidth, videoHeight=$videoHeight"
)
return layoutParams
}
return getCurrentScaleType().getLayoutParam(
layoutWidth,
layoutHeight,
videoWidth,
videoHeight,
layoutParams
)
}
private fun getCurrentScaleType(): IScaleType {
val tmpScaleType = scaleTypeImpl
return if (tmpScaleType != null) {
ALog.i(TAG, "custom scaleType")
tmpScaleType
} else {
ALog.i(TAG, "scaleType=$currentScaleType")
when (currentScaleType) {
ScaleType.FIT_XY -> scaleTypeFitXY
ScaleType.FIT_CENTER -> scaleTypeFitCenter
ScaleType.CENTER_CROP -> scaleTypeCenterCrop
}
}
}
private fun checkParams(): Boolean {
return layoutWidth > 0
&& layoutHeight > 0
&& videoWidth > 0
&& videoHeight > 0
}
}

View File

@@ -0,0 +1,74 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.util
import android.opengl.GLES20
import com.tencent.qgame.animplayer.Constant
object ShaderUtil {
private const val TAG = "${Constant.TAG}.ShaderUtil"
fun createProgram(vertexSource: String, fragmentSource: String): Int {
val vertexShaderHandle = compileShader(GLES20.GL_VERTEX_SHADER, vertexSource)
val fragmentShaderHandle = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource)
return createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle)
}
private fun compileShader(shaderType: Int, shaderSource: String): Int {
var shaderHandle = GLES20.glCreateShader(shaderType)
if (shaderHandle != 0) {
GLES20.glShaderSource(shaderHandle, shaderSource)
GLES20.glCompileShader(shaderHandle)
val compileStatus = IntArray(1)
GLES20.glGetShaderiv(shaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0)
if (compileStatus[0] == 0) {
ALog.e(TAG, "Error compiling shader: " + GLES20.glGetShaderInfoLog(shaderHandle))
GLES20.glDeleteShader(shaderHandle)
shaderHandle = 0
}
}
if (shaderHandle == 0) {
throw RuntimeException("Error creating shader.")
}
return shaderHandle
}
private fun createAndLinkProgram(vertexShaderHandle: Int, fragmentShaderHandle: Int): Int {
var programHandle = GLES20.glCreateProgram()
if (programHandle != 0) {
GLES20.glAttachShader(programHandle, vertexShaderHandle)
GLES20.glAttachShader(programHandle, fragmentShaderHandle)
GLES20.glLinkProgram(programHandle)
val linkStatus = IntArray(1)
GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0)
if (linkStatus[0] == 0) {
ALog.e(TAG, "Error compiling program: " + GLES20.glGetProgramInfoLog(programHandle))
GLES20.glDeleteProgram(programHandle)
programHandle = 0
}
}
if (programHandle == 0) {
throw RuntimeException("Error creating program.")
}
return programHandle
}
}

View File

@@ -0,0 +1,81 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.util
import com.tencent.qgame.animplayer.Constant
class SpeedControlUtil {
companion object {
private const val TAG = "${Constant.TAG}.SpeedControlUtil"
}
private val ONE_MILLION = 1000000L
private var prevPresentUsec: Long = 0
private var prevMonoUsec: Long = 0
private var fixedFrameDurationUsec: Long = 0
private var loopReset = true
fun setFixedPlaybackRate(fps: Int) {
if (fps <=0) return
fixedFrameDurationUsec = ONE_MILLION / fps
}
fun preRender(presentationTimeUsec: Long) {
if (prevMonoUsec == 0L) {
prevMonoUsec = System.nanoTime() / 1000
prevPresentUsec = presentationTimeUsec
} else {
var frameDelta: Long
if (loopReset) {
prevPresentUsec = presentationTimeUsec - ONE_MILLION / 30
loopReset = false
}
frameDelta = if (fixedFrameDurationUsec != 0L) {
fixedFrameDurationUsec
} else {
presentationTimeUsec - prevPresentUsec
}
when {
frameDelta < 0 -> frameDelta = 0
frameDelta > 10 * ONE_MILLION -> frameDelta = 5 * ONE_MILLION
}
val desiredUsec = prevMonoUsec + frameDelta
var nowUsec = System.nanoTime() / 1000
while (nowUsec < desiredUsec - 100 ) {
var sleepTimeUsec = desiredUsec - nowUsec
if (sleepTimeUsec > 500000) {
sleepTimeUsec = 500000
}
try {
Thread.sleep(sleepTimeUsec / 1000, (sleepTimeUsec % 1000).toInt() * 1000)
} catch (e: InterruptedException) {
ALog.e(TAG, "e=$e", e)
}
nowUsec = System.nanoTime() / 1000
}
prevMonoUsec += frameDelta
prevPresentUsec += frameDelta
}
}
fun reset() {
prevPresentUsec = 0
prevMonoUsec = 0
}
}

View File

@@ -0,0 +1,84 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.util
import com.tencent.qgame.animplayer.PointRect
/**
* 纹理坐标工具
* 坐标顺序是倒N
*/
object TexCoordsUtil {
/**
* @param width 纹理的宽高
* @param height
*/
fun create(width: Int, height: Int, rect: PointRect, array: FloatArray): FloatArray {
// x0
array[0] = rect.x.toFloat() / width
// y0
array[1] = rect.y.toFloat() / height
// x1
array[2] = rect.x.toFloat() / width
// y1
array[3] = (rect.y.toFloat() + rect.h) / height
// x2
array[4] = (rect.x.toFloat() + rect.w) / width
// y2
array[5] = rect.y.toFloat() / height
// x3
array[6] = (rect.x.toFloat() + rect.w) / width
// y3
array[7] = (rect.y.toFloat() + rect.h) / height
return array
}
/**
* 顺时针90度
*/
fun rotate90(array: FloatArray): FloatArray {
// 0->2 1->0 3->1 2->3
val tx = array[0]
val ty = array[1]
// 1->0
array[0] = array[2]
array[1] = array[3]
// 3->1
array[2] = array[6]
array[3] = array[7]
// 2->3
array[6] = array[4]
array[7] = array[5]
// 0->2
array[4] = tx
array[5] = ty
return array
}
}

View File

@@ -0,0 +1,58 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.util
import android.graphics.Bitmap
import android.opengl.GLES20
import android.opengl.GLUtils
object TextureLoadUtil {
private const val TAG = "TextureUtil"
fun loadTexture(bitmap: Bitmap?): Int {
val textureObjectIds = IntArray(1)
GLES20.glGenTextures(1, textureObjectIds, 0)
if (textureObjectIds[0] == 0) {
return 0
}
if (bitmap == null) {
GLES20.glDeleteTextures(1, textureObjectIds, 0)
return 0
}
if (bitmap.isRecycled) {
ALog.e(TAG, "bitmap isRecycled")
return 0
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectIds[0])
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D)
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
return textureObjectIds[0]
}
fun releaseTexure(textureId: Int) {
if (textureId != 0) {
GLES20.glDeleteTextures(1, intArrayOf(textureId), 0)
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Tencent is pleased to support the open source community by making vap available.
*
* Copyright (C) 2020 Tencent. 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.util
import com.tencent.qgame.animplayer.PointRect
/**
* 顶点坐标工具
* 坐标顺序是倒N
*/
object VertexUtil {
/**
* @param width 画布大大小
* @param height
*/
fun create(width: Int, height: Int, rect: PointRect, array: FloatArray): FloatArray {
// x0
array[0] = switchX(rect.x.toFloat() / width)
// y0
array[1] = switchY(rect.y.toFloat() / height)
// x1
array[2] = switchX(rect.x.toFloat() / width)
// y1
array[3] = switchY((rect.y.toFloat() + rect.h) / height)
// x2
array[4] = switchX((rect.x.toFloat() + rect.w) / width)
// y2
array[5] = switchY(rect.y.toFloat() / height)
// x3
array[6] = switchX((rect.x.toFloat() + rect.w) / width)
// y3
array[7] = switchY((rect.y.toFloat() + rect.h) / height)
return array
}
private fun switchX(x: Float): Float {
return x * 2f -1f
}
private fun switchY(y: Float): Float {
return ((y * 2f - 2f) * -1f) - 1f
}
}

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">animplayer</string>
</resources>