diff --git a/MainModule/src/main/java/com/xscm/modulemain/activity/room/activity/RoomActivity.kt b/MainModule/src/main/java/com/xscm/modulemain/activity/room/activity/RoomActivity.kt index 8f8c87f0..5b1e0baf 100644 --- a/MainModule/src/main/java/com/xscm/modulemain/activity/room/activity/RoomActivity.kt +++ b/MainModule/src/main/java/com/xscm/modulemain/activity/room/activity/RoomActivity.kt @@ -3195,12 +3195,8 @@ class RoomActivity : BaseMvpActivity(), } } - 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(), ClickUtils.clearAllClickRecords() AgoraManager.getInstance().cleanup() roomId = roomId2 - isSwitchRoom = true // 重新连接房间相关服务 resumeRoomState() publicScreenFragment?.onDestroy() diff --git a/MainModule/src/main/java/com/xscm/modulemain/activity/room/fragment/RoomMentorShipFragment.kt b/MainModule/src/main/java/com/xscm/modulemain/activity/room/fragment/RoomMentorShipFragment.kt index cc88b8d7..28575e41 100644 --- a/MainModule/src/main/java/com/xscm/modulemain/activity/room/fragment/RoomMentorShipFragment.kt +++ b/MainModule/src/main/java/com/xscm/modulemain/activity/room/fragment/RoomMentorShipFragment.kt @@ -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 diff --git a/MainModule/src/main/java/com/xscm/modulemain/widget/RoomMentorShipWheatView.kt b/MainModule/src/main/java/com/xscm/modulemain/widget/RoomMentorShipWheatView.kt index cce307d3..a4f01f5a 100644 --- a/MainModule/src/main/java/com/xscm/modulemain/widget/RoomMentorShipWheatView.kt +++ b/MainModule/src/main/java/com/xscm/modulemain/widget/RoomMentorShipWheatView.kt @@ -75,7 +75,7 @@ class RoomMentorShipWheatView : BaseWheatView { mTvName?.text = bean.nickname } else { mTvName.visibility = VISIBLE - mCharmView.visibility = GONE + mCharmView.visibility = INVISIBLE hostTv?.visibility = GONE } } diff --git a/MainModule/src/main/res/layout/fragment_mentor_ship.xml b/MainModule/src/main/res/layout/fragment_mentor_ship.xml index e2712b7c..f5d7e2ae 100644 --- a/MainModule/src/main/res/layout/fragment_mentor_ship.xml +++ b/MainModule/src/main/res/layout/fragment_mentor_ship.xml @@ -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" /> diff --git a/animplayer/build.gradle b/animplayer/build.gradle new file mode 100644 index 00000000..0e72e2e5 --- /dev/null +++ b/animplayer/build.gradle @@ -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" diff --git a/animplayer/proguard-rules.pro b/animplayer/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/animplayer/proguard-rules.pro @@ -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 diff --git a/animplayer/publish.gradle b/animplayer/publish.gradle new file mode 100644 index 00000000..89cbfb92 --- /dev/null +++ b/animplayer/publish.gradle @@ -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' diff --git a/animplayer/src/main/AndroidManifest.xml b/animplayer/src/main/AndroidManifest.xml new file mode 100644 index 00000000..fc0f9fe2 --- /dev/null +++ b/animplayer/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimConfig.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimConfig.kt new file mode 100644 index 00000000..bb88f052 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimConfig.kt @@ -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) //参考宽&高 \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimConfigManager.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimConfigManager.kt new file mode 100644 index 00000000..2a346df1 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimConfigManager.kt @@ -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 + } + + + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimPlayer.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimPlayer.kt new file mode 100644 index 00000000..6c281b01 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimPlayer.kt @@ -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 + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimView.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimView.kt new file mode 100644 index 00000000..3d9b0567 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/AnimView.kt @@ -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 { + 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() } + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/AudioPlayer.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/AudioPlayer.kt new file mode 100644 index 00000000..80b0f9a4 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/AudioPlayer.kt @@ -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") + } + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/Constant.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/Constant.kt new file mode 100644 index 00000000..cc553e17 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/Constant.kt @@ -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 ?: ""}" + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/Decoder.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/Decoder.kt new file mode 100644 index 00000000..c1b42eee --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/Decoder.kt @@ -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?) \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/EGLUtil.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/EGLUtil.kt new file mode 100644 index 00000000..21d01900 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/EGLUtil.kt @@ -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(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 + } + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/HardDecoder.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/HardDecoder.kt new file mode 100644 index 00000000..2b0b253d --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/HardDecoder.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/IAnimView.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/IAnimView.kt new file mode 100644 index 00000000..1bfbcafe --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/IAnimView.kt @@ -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 +} diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/IRenderListener.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/IRenderListener.kt new file mode 100644 index 00000000..b92d3b9e --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/IRenderListener.kt @@ -0,0 +1,54 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 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?) {} +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/Render.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/Render.kt new file mode 100644 index 00000000..a4b5de53 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/Render.kt @@ -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) + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/RenderConstant.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/RenderConstant.kt new file mode 100644 index 00000000..a1135fa0 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/RenderConstant.kt @@ -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" + + "}" +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/YUVRender.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/YUVRender.kt new file mode 100644 index 00000000..9bee304b --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/YUVRender.kt @@ -0,0 +1,207 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 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) + } + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/YUVShader.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/YUVShader.kt new file mode 100644 index 00000000..b120c7e5 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/YUVShader.kt @@ -0,0 +1,60 @@ +/* + * Tencent is pleased to support the open source community by making vap available. + * + * Copyright (C) 2020 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) +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/file/AssetsFileContainer.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/file/AssetsFileContainer.kt new file mode 100644 index 00000000..6adbcd1d --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/file/AssetsFileContainer.kt @@ -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() + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/file/FileContainer.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/file/FileContainer.kt new file mode 100644 index 00000000..ae27f919 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/file/FileContainer.kt @@ -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() { + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/file/IFileContainer.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/file/IFileContainer.kt new file mode 100644 index 00000000..69e25317 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/file/IFileContainer.kt @@ -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() + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/file/StreamContainer.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/file/StreamContainer.kt new file mode 100644 index 00000000..a2858c74 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/file/StreamContainer.kt @@ -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() + } +} diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/file/StreamMediaDataSource.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/file/StreamMediaDataSource.kt new file mode 100644 index 00000000..27ec4280 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/file/StreamMediaDataSource.kt @@ -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() + } + } +} diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/inter/IAnimListener.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/inter/IAnimListener.kt new file mode 100644 index 00000000..cc812752 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/inter/IAnimListener.kt @@ -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?) +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/inter/IFetchResource.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/inter/IFetchResource.kt new file mode 100644 index 00000000..766ab27f --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/inter/IFetchResource.kt @@ -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) +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/inter/OnResourceClickListener.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/inter/OnResourceClickListener.kt new file mode 100644 index 00000000..3e8484d6 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/inter/OnResourceClickListener.kt @@ -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) +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskAnimPlugin.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskAnimPlugin.kt new file mode 100644 index 00000000..31143afa --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskAnimPlugin.kt @@ -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() + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskConfig.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskConfig.kt new file mode 100644 index 00000000..421f5d24 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskConfig.kt @@ -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? = null //遮罩坐标矩形 + var maskPositionPair: Pair? = null //内容坐标矩形 + + constructor(bitmap: Bitmap?, positionPair :Pair?, texPair: Pair?) : 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 + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskRender.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskRender.kt new file mode 100644 index 00000000..efd685f6 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskRender.kt @@ -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) + } + + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskShader.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskShader.kt new file mode 100644 index 00000000..29baddd3 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/mask/MaskShader.kt @@ -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) + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/Frame.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/Frame.kt new file mode 100644 index 00000000..0eaff044 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/Frame.kt @@ -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() + 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() + + 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) + } + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixAnimPlugin.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixAnimPlugin.kt new file mode 100644 index 00000000..780b2da0 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixAnimPlugin.kt @@ -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() + 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 + } + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixRender.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixRender.kt new file mode 100644 index 00000000..1f82da40 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixRender.kt @@ -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 + } + + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixShader.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixShader.kt new file mode 100644 index 00000000..a5c56102 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixShader.kt @@ -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) + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixTouch.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixTouch.kt new file mode 100644 index 00000000..70f9a6fd --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/MixTouch.kt @@ -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) + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/Resource.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/Resource.kt new file mode 100644 index 00000000..10de17e5 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/Resource.kt @@ -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 + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/Src.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/Src.kt new file mode 100644 index 00000000..0b78f0cd --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/mix/Src.kt @@ -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() + + 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 + } + } + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/plugin/AnimPluginManager.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/plugin/AnimPluginManager.kt new file mode 100644 index 00000000..25d6490b --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/plugin/AnimPluginManager.kt @@ -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(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 + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/plugin/IAnimPlugin.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/plugin/IAnimPlugin.kt new file mode 100644 index 00000000..3af7155c --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/plugin/IAnimPlugin.kt @@ -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} +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/textureview/InnerTextureView.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/textureview/InnerTextureView.kt new file mode 100644 index 00000000..43f828e9 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/textureview/InnerTextureView.kt @@ -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 + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/util/ALog.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/ALog.kt new file mode 100644 index 00000000..7c74f7fe --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/ALog.kt @@ -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) {} +} + + diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/util/BitmapUtil.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/BitmapUtil.kt new file mode 100644 index 00000000..f55e38a1 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/BitmapUtil.kt @@ -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 + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/util/GlFloatArray.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/GlFloatArray.kt new file mode 100644 index 00000000..3a28e0bf --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/GlFloatArray.kt @@ -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) + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/util/MediaUtil.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/MediaUtil.kt new file mode 100644 index 00000000..fb92d24f --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/MediaUtil.kt @@ -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() + + 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") + } + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/util/ScaleTypeUtil.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/ScaleTypeUtil.kt new file mode 100644 index 00000000..9298c115 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/ScaleTypeUtil.kt @@ -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 +} + +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 { + 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 { + return Pair(realWidth, realHeight) + } + + private fun getFitCenterSize( + layoutWidth: Int, + layoutHeight: Int, + videoWidth: Int, + videoHeight: Int + ): Pair { + + 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 { + return Pair(realWidth, realHeight) + } + + private fun getCenterCropSize( + layoutWidth: Int, + layoutHeight: Int, + videoWidth: Int, + videoHeight: Int + ): Pair { + + 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 { + 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 + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/util/ShaderUtil.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/ShaderUtil.kt new file mode 100644 index 00000000..b2d99f69 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/ShaderUtil.kt @@ -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 + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/util/SpeedControlUtil.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/SpeedControlUtil.kt new file mode 100644 index 00000000..332377dc --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/SpeedControlUtil.kt @@ -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 + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/util/TexCoordsUtil.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/TexCoordsUtil.kt new file mode 100644 index 00000000..a1c47c56 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/TexCoordsUtil.kt @@ -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 + } + +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/util/TextureLoadUtil.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/TextureLoadUtil.kt new file mode 100644 index 00000000..d8981753 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/TextureLoadUtil.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/animplayer/src/main/java/com/tencent/qgame/animplayer/util/VertexUtil.kt b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/VertexUtil.kt new file mode 100644 index 00000000..1cb0bc60 --- /dev/null +++ b/animplayer/src/main/java/com/tencent/qgame/animplayer/util/VertexUtil.kt @@ -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 + } + +} \ No newline at end of file diff --git a/animplayer/src/main/res/values/strings.xml b/animplayer/src/main/res/values/strings.xml new file mode 100644 index 00000000..b5221ab4 --- /dev/null +++ b/animplayer/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + animplayer +