更换vap库
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class RoomMentorShipWheatView : BaseWheatView {
|
||||
mTvName?.text = bean.nickname
|
||||
} else {
|
||||
mTvName.visibility = VISIBLE
|
||||
mCharmView.visibility = GONE
|
||||
mCharmView.visibility = INVISIBLE
|
||||
hostTv?.visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
39
animplayer/build.gradle
Normal 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
21
animplayer/proguard-rules.pro
vendored
Normal 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
33
animplayer/publish.gradle
Normal 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'
|
||||
2
animplayer/src/main/AndroidManifest.xml
Normal file
2
animplayer/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.tencent.qgame.animplayer"/>
|
||||
@@ -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) //参考宽&高
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ?: ""}"
|
||||
}
|
||||
|
||||
}
|
||||
171
animplayer/src/main/java/com/tencent/qgame/animplayer/Decoder.kt
Normal file
171
animplayer/src/main/java/com/tencent/qgame/animplayer/Decoder.kt
Normal 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?)
|
||||
115
animplayer/src/main/java/com/tencent/qgame/animplayer/EGLUtil.kt
Normal file
115
animplayer/src/main/java/com/tencent/qgame/animplayer/EGLUtil.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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?) {}
|
||||
}
|
||||
153
animplayer/src/main/java/com/tencent/qgame/animplayer/Render.kt
Normal file
153
animplayer/src/main/java/com/tencent/qgame/animplayer/Render.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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" +
|
||||
"}"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?)
|
||||
}
|
||||
@@ -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>)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
152
animplayer/src/main/java/com/tencent/qgame/animplayer/mix/Src.kt
Normal file
152
animplayer/src/main/java/com/tencent/qgame/animplayer/mix/Src.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
3
animplayer/src/main/res/values/strings.xml
Normal file
3
animplayer/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">animplayer</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user