初始提交:第三方合作伙伴 App 测试项目

- 支持 EPG Intent 传参启动(deviceUid + programCode 自动播放)
- 代销模式 init/checkBenefits/SDK初始化/获取播放地址/播放
- Android 4.2 (API 17) 兼容
- OkHttp 3.12.x + TLS 1.2
- ExoPlayer 2.19.1 视频播放
This commit is contained in:
zhaogd
2026-07-26 13:05:04 +08:00
commit 9fd12c5eae
38 changed files with 2618 additions and 0 deletions

View File

@@ -0,0 +1,269 @@
package com.pukka.mediaapp.sdk
import android.content.Context
import android.text.TextUtils
import android.util.Log
import com.jetsenxsdk.api.JetsenxSdk
import com.jetsenxsdk.bean.VideoInfo
import com.jetsenxsdk.callback.JetsenxInitCallback
import com.jetsenxsdk.callback.JetsenxPricesCallback
import com.jetsenxsdk.callback.JetsenxUrlCallback
import com.pukka.mediaapp.utils.FileLogger
/**
* JetsenxSdk Kotlin 封装
*
* 视频地址获取流程:
* 1. init() 初始化SDK(用 thirdCardNo 作为 uid)
* 2. getAllVideoUrl() -> getAllAssetPrices(获取productCode) -> getVideoUrls/getFreeVideoUrls
*/
object JetsenxSdkUtil {
private const val TAG = "JetsenxSdkUtil"
// 渠道配置
private const val AGCODE = "1100101010200492358120169"
private const val TENANT = "huashitv"
private const val ACCESSER = "JSTASJD"
private const val SECRET = "kMeWsBu0QsTXpnft"
private const val SERVICE_COMBO_CODE = "1100121010200494755850172"
private const val FALLBACK_MAC = "CC:8C:DA:C6:E9:9B"
private var productCodeSp: String? = null
private var isInitialized = false
interface InitCallback {
fun onSuccess(memberCode: String)
fun onFail(message: String)
}
interface VideoUrlCallback {
fun onSuccess(videoUrl1080: String, videoUrl720: String, isFree: Boolean)
fun onError(code: String, message: String)
}
/**
* 初始化 SDK
* @param context 上下文
* @param uid thirdCardNo(天卡权益接口返回)
* @param mac 设备 MAC(由调用方传入,为空则从 DeviceInfoUtil 获取)
* @param callback 初始化回调
*/
fun init(context: Context, uid: String = "test_user", mac: String? = null, callback: InitCallback? = null) {
// 优先用传入的 mac,其次从 DeviceInfoUtil 获取,最后用 FALLBACK_MAC
val finalMac = when {
!mac.isNullOrEmpty() -> mac
else -> {
val identity = com.pukka.mediaapp.utils.DeviceInfoUtil.getDeviceIdentity(context)
identity.mac ?: FALLBACK_MAC
}
}
val finalUid = if (TextUtils.isEmpty(uid)) finalMac else uid
val utype = if (finalUid.matches(Regex("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"))) "3" else "2"
Log.d(TAG, "init: agcode=$AGCODE mac=$finalMac uid=$finalUid tenant=$TENANT utype=$utype")
FileLogger.log(TAG, "init: agcode=$AGCODE mac=$finalMac uid=$finalUid tenant=$TENANT utype=$utype")
try {
JetsenxSdk.init(
context, AGCODE, finalMac, finalUid,
TENANT, SECRET, ACCESSER, utype, "",
SERVICE_COMBO_CODE,
object : JetsenxInitCallback {
override fun onInitSuccess(memberCode: String, p1: Boolean) {
Log.d(TAG, "onInitSuccess: memberCode=$memberCode")
FileLogger.log(TAG, "SDK init成功: memberCode=$memberCode")
isInitialized = true
callback?.onSuccess(memberCode)
}
override fun onFail(code: String, msg: String) {
Log.e(TAG, "onInitFail: code=$code msg=$msg")
FileLogger.error(TAG, "SDK init失败: code=$code msg=$msg")
isInitialized = false
callback?.onFail(if (msg.isNotEmpty()) msg else "SDK初始化失败")
}
}
)
} catch (e: Exception) {
Log.e(TAG, "init exception", e)
callback?.onFail("SDK初始化异常: ${e.message}")
}
}
/**
* 获取视频播放地址
* @param parentContentType "8"=免费, 其他=付费
* @param programCode 节目编码
*/
fun getAllVideoUrl(
parentContentType: String,
programCode: String,
callback: VideoUrlCallback
) {
Log.d(TAG, "getAllVideoUrl: parentContentType=$parentContentType programCode=$programCode")
FileLogger.log(TAG, "getAllVideoUrl: parentContentType=$parentContentType programCode=$programCode")
if (TextUtils.isEmpty(programCode)) {
callback.onError("-1", "programCode is empty")
return
}
getAllVideoUrlNext(parentContentType, programCode, callback)
}
private fun getAllVideoUrlNext(
parentContentType: String,
programCode: String,
callback: VideoUrlCallback
) {
val cachedProductCode = productCodeSp
if (!cachedProductCode.isNullOrEmpty()) {
getAllVideoUrl2(parentContentType, cachedProductCode, programCode, callback)
} else {
JetsenxSdk.getAllAssetPrices(ACCESSER, SECRET, object : JetsenxPricesCallback {
override fun onAllAssetPrices(list: MutableList<Map<String, Any>>?) {
FileLogger.log(TAG, "getAllAssetPrices返回: count=${list?.size}")
if (list != null && list.isNotEmpty()) {
list.forEachIndexed { index, map ->
FileLogger.log(TAG, " assetPrice[$index]: $map")
}
val productCode = list[0]["packageCode"]?.toString() ?: ""
Log.d(TAG, "onAllAssetPrices: productCode=$productCode")
FileLogger.log(TAG, "使用productCode=$productCode")
productCodeSp = productCode
getAllVideoUrl2(parentContentType, productCode, programCode, callback)
} else {
callback.onError("-2", "productCode is NULL")
}
}
override fun onFail(code: String, msg: String) {
Log.e(TAG, "getAllAssetPrices onFail: code=$code msg=$msg")
FileLogger.error(TAG, "getAllAssetPrices失败: code=$code msg=$msg")
callback.onError(code, msg)
}
})
}
}
private fun getAllVideoUrl2(
parentContentType: String,
productCode: String,
programCode: String,
callback: VideoUrlCallback
) {
if (TextUtils.isEmpty(parentContentType)) {
callback.onError("-1", "parentContentType is NULL")
return
}
if (parentContentType != "8") {
getVideoUrls2(productCode, programCode, callback)
} else {
getFreeVideoUrls2(programCode, callback)
}
}
private fun getVideoUrls2(productCode: String, programCode: String, callback: VideoUrlCallback) {
JetsenxSdk.getVideoUrls(productCode, programCode, object : JetsenxUrlCallback {
override fun onVideoUrls(videoInfos: Array<VideoInfo>?) {
FileLogger.log(TAG, "getVideoUrls返回: count=${videoInfos?.size}")
if (videoInfos != null && videoInfos.isNotEmpty()) {
val (url1080, url720) = extractUrls(videoInfos)
if (url1080.isNotEmpty() || url720.isNotEmpty()) {
callback.onSuccess(url1080, url720, false)
} else {
callback.onError("411", "未返回播放地址")
}
} else {
callback.onError("411", "未返回播放地址")
}
}
override fun onFail(code: String, msg: String) {
FileLogger.error(TAG, "getVideoUrls(付费)失败: code=$code msg=$msg")
if (msg.contains("未订购") || code == "-110") {
getFreeVideoUrls2(programCode, callback)
} else {
callback.onError(code, msg)
}
}
})
}
private fun getFreeVideoUrls2(programCode: String, callback: VideoUrlCallback) {
JetsenxSdk.getFreeVideoUrls(programCode, object : JetsenxUrlCallback {
override fun onVideoUrls(videoInfos: Array<VideoInfo>?) {
FileLogger.log(TAG, "getFreeVideoUrls返回: count=${videoInfos?.size}")
if (videoInfos != null && videoInfos.isNotEmpty()) {
val (url1080, url720) = extractUrls(videoInfos)
if (url1080.isNotEmpty() || url720.isNotEmpty()) {
callback.onSuccess(url1080, url720, true)
} else {
callback.onError("410", "未返回播放地址")
}
} else {
callback.onError("410", "未返回播放地址")
}
}
override fun onFail(code: String, msg: String) {
FileLogger.error(TAG, "getFreeVideoUrls(免费)失败: code=$code msg=$msg")
// 免费接口失败时,回退尝试付费接口
val cachedProductCode = productCodeSp
if (!cachedProductCode.isNullOrEmpty()) {
FileLogger.log(TAG, "免费接口失败,回退尝试付费接口 getVideoUrls")
JetsenxSdk.getVideoUrls(cachedProductCode, programCode, object : JetsenxUrlCallback {
override fun onVideoUrls(videoInfos: Array<VideoInfo>?) {
FileLogger.log(TAG, "回退getVideoUrls返回: count=${videoInfos?.size}")
if (videoInfos != null && videoInfos.isNotEmpty()) {
val (url1080, url720) = extractUrls(videoInfos)
if (url1080.isNotEmpty() || url720.isNotEmpty()) {
callback.onSuccess(url1080, url720, false)
} else {
callback.onError(code, msg)
}
} else {
callback.onError(code, msg)
}
}
override fun onFail(code2: String, msg2: String) {
FileLogger.error(TAG, "回退getVideoUrls也失败: code=$code2 msg=$msg2")
callback.onError(code, msg)
}
})
} else {
callback.onError(code, msg)
}
}
})
}
private fun extractUrls(videoInfos: Array<VideoInfo>): Pair<String, String> {
var url1080 = ""
var url720 = ""
for (info in videoInfos) {
val format = info.format ?: ""
if (format.contains("720") && url720.isEmpty()) {
url720 = info.videoUrl ?: ""
} else if (format.contains("1080") && url1080.isEmpty()) {
url1080 = info.videoUrl ?: ""
}
}
FileLogger.log(TAG, "extractUrls: 1080=$url1080 720=$url720")
return Pair(url1080, url720)
}
private fun getMacAddress(context: Context): String {
return try {
val wifiManager = context.applicationContext
.getSystemService(Context.WIFI_SERVICE) as android.net.wifi.WifiManager
val wifiInfo = wifiManager.connectionInfo
val mac = wifiInfo.macAddress
if (mac.isNullOrEmpty() || mac == "02:00:00:00:00:00") "" else mac
} catch (e: Exception) { "" }
}
fun isReady(): Boolean = isInitialized
}