diff --git a/app/src/main/java/com/lizongying/mytv/CustomLoadErrorHandlingPolicy.kt b/app/src/main/java/com/lizongying/mytv/CustomLoadErrorHandlingPolicy.kt new file mode 100644 index 0000000..35bf14b --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/CustomLoadErrorHandlingPolicy.kt @@ -0,0 +1,23 @@ +package com.lizongying.mytv + +import androidx.media3.common.C +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy + + +@UnstableApi +class CustomLoadErrorHandlingPolicy(private val minimumLoadableRetryCount: Int) : + DefaultLoadErrorHandlingPolicy(minimumLoadableRetryCount) { + + override fun getMinimumLoadableRetryCount(dataType: Int): Int { + return if (minimumLoadableRetryCount == -1) { + if (dataType == C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE) { + DEFAULT_MIN_LOADABLE_RETRY_COUNT_PROGRESSIVE_LIVE + } else { + DEFAULT_MIN_LOADABLE_RETRY_COUNT + } + } else { + minimumLoadableRetryCount + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv/ExoPlayerAdapter.kt b/app/src/main/java/com/lizongying/mytv/ExoPlayerAdapter.kt index efbca9e..433b1ad 100644 --- a/app/src/main/java/com/lizongying/mytv/ExoPlayerAdapter.kt +++ b/app/src/main/java/com/lizongying/mytv/ExoPlayerAdapter.kt @@ -38,6 +38,22 @@ open class ExoPlayerAdapter(private var mContext: Context?) : PlayerAdapter() { var mBufferingStart = false + + private var mMinimumLoadableRetryCount = 3 + + + private var mPlayerErrorListener: PlayerErrorListener? = null + + init { + mPlayer?.playWhenReady = true + + if (mPlayerErrorListener == null) { + mPlayerErrorListener = PlayerErrorListener() + mPlayer?.addListener(mPlayerErrorListener!!) + } + } + + open fun notifyBufferingStartEnd() { callback.onBufferingStateChanged( this@ExoPlayerAdapter, @@ -178,14 +194,19 @@ open class ExoPlayerAdapter(private var mContext: Context?) : PlayerAdapter() { return mBufferedProgress } - private var mPlayerErrorListener: PlayerErrorListener? = null - private inner class PlayerErrorListener : Player.Listener { override fun onPlayerError(error: PlaybackException) { callback.onError(this@ExoPlayerAdapter, error.errorCode, error.message) } } + + private var mHeaders: Map? = mapOf() + + fun setHeaders(headers: Map) { + mHeaders = headers + } + /** * Sets the media source of the player witha given URI. * @@ -193,46 +214,43 @@ open class ExoPlayerAdapter(private var mContext: Context?) : PlayerAdapter() { * otherwise. * @see MediaPlayer.setDataSource */ + + @OptIn(UnstableApi::class) open fun setDataSource(uri: Uri?): Boolean { if (if (mMediaSourceUri != null) mMediaSourceUri == uri else uri == null) { return false } mMediaSourceUri = uri - prepareMediaForPlaying() - mPlayer?.playWhenReady = true + val httpDataSource = DefaultHttpDataSource.Factory() + mHeaders?.let { httpDataSource.setDefaultRequestProperties(it) } - if (mPlayerErrorListener == null) { - mPlayerErrorListener = PlayerErrorListener() - mPlayer?.addListener(mPlayerErrorListener!!) - } + val hlsMediaSource = + HlsMediaSource.Factory(httpDataSource).setLoadErrorHandlingPolicy( + CustomLoadErrorHandlingPolicy(mMinimumLoadableRetryCount) + ).createMediaSource( + MediaItem.fromUri( + mMediaSourceUri!! + ) + ) + prepareMediaForPlaying(hlsMediaSource) return true } - private var mHeaders: Map? = mapOf() + @OptIn(UnstableApi::class) + open fun setDataSource(hlsMediaSource: HlsMediaSource): Boolean { + prepareMediaForPlaying(hlsMediaSource) + return true + } - fun setHeaders(headers: Map) { - mHeaders = headers + fun setMinimumLoadableRetryCount(minimumLoadableRetryCount: Int) { + mMinimumLoadableRetryCount = minimumLoadableRetryCount } @OptIn(UnstableApi::class) - private fun prepareMediaForPlaying() { - reset() + private fun prepareMediaForPlaying(hlsMediaSource: HlsMediaSource) { try { - if (mMediaSourceUri != null) { - val httpDataSource = DefaultHttpDataSource.Factory() - mHeaders?.let { httpDataSource.setDefaultRequestProperties(it) } - - val hlsMediaSource = - HlsMediaSource.Factory(httpDataSource).createMediaSource( - MediaItem.fromUri( - mMediaSourceUri!! - ) - ) - mPlayer?.setMediaSource(hlsMediaSource) - } else { - return - } + mPlayer?.setMediaSource(hlsMediaSource) } catch (e: IOException) { e.printStackTrace() throw RuntimeException(e) diff --git a/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt b/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt index 2f4b3ca..3f98c42 100644 --- a/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt +++ b/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt @@ -25,6 +25,11 @@ class PlaybackFragment : VideoSupportFragment() { view?.isFocusable = false view?.isFocusableInTouchMode = false + + val glueHost = VideoSupportFragmentGlueHost(this@PlaybackFragment) + mTransportControlGlue = PlaybackControlGlue(activity, playerAdapter) + mTransportControlGlue.host = glueHost + mTransportControlGlue.playWhenPrepared() } override fun showControlsOverlay(runAnimation: Boolean) { @@ -44,12 +49,10 @@ class PlaybackFragment : VideoSupportFragment() { lastVideoUrl = videoUrl - val glueHost = VideoSupportFragmentGlueHost(this@PlaybackFragment) - mTransportControlGlue = PlaybackControlGlue(activity, playerAdapter) - mTransportControlGlue.host = glueHost - mTransportControlGlue.playWhenPrepared() - playerAdapter?.callback = PlayerCallback(tvModel) + if (tvModel.ysp() != null) { + playerAdapter?.setMinimumLoadableRetryCount(0) + } try { playerAdapter?.setDataSource(Uri.parse(videoUrl)) } catch (e: IOException) { diff --git a/app/src/main/java/com/lizongying/mytv/Request.kt b/app/src/main/java/com/lizongying/mytv/Request.kt index d0906bf..0dc720d 100644 --- a/app/src/main/java/com/lizongying/mytv/Request.kt +++ b/app/src/main/java/com/lizongying/mytv/Request.kt @@ -1,14 +1,18 @@ package com.lizongying.mytv import android.content.Context +import android.os.Handler +import android.os.Looper import android.util.Base64 import android.util.Log import com.lizongying.mytv.api.ApiClient +import com.lizongying.mytv.api.BtraceClient import com.lizongying.mytv.api.LiveInfo import com.lizongying.mytv.api.LiveInfoRequest import com.lizongying.mytv.api.ProtoClient import com.lizongying.mytv.api.YSP import com.lizongying.mytv.api.YSPApiService +import com.lizongying.mytv.api.YSPBtraceService import com.lizongying.mytv.api.YSPProtoService import com.lizongying.mytv.models.TVViewModel import com.lizongying.mytv.proto.Ysp.cn.yangshipin.oms.common.proto.pageModel @@ -27,8 +31,12 @@ import javax.crypto.spec.SecretKeySpec class Request(var context: Context) { private var ysp: YSP? = null private var yspApiService: YSPApiService? = null + private var yspBtraceService: YSPBtraceService? = null private var yspProtoService: YSPProtoService? = null + private val handler = Handler(Looper.getMainLooper()) + private lateinit var myRunnable: MyRunnable + private var mapping = mapOf( "CCTV4K" to "CCTV4K 超高清", "CCTV1" to "CCTV1 综合", @@ -81,10 +89,16 @@ class Request(var context: Context) { ysp = YSP(context) } yspApiService = ApiClient().yspApiService + yspBtraceService = BtraceClient().yspBtraceService yspProtoService = ProtoClient().yspProtoService } fun fetchData(tvModel: TVViewModel) { + if (::myRunnable.isInitialized) { + handler.removeCallbacks(myRunnable) + } + + tvModel.seq = 0 val data = ysp?.switch(tvModel) val title = tvModel.title.value @@ -114,6 +128,9 @@ class Request(var context: Context) { Log.i(TAG, "$title url $url") tvModel.addVideoUrl(url) tvModel.allReady() + + myRunnable = MyRunnable(tvModel) + handler.post(myRunnable) } else { Log.e(TAG, "$title key error") tvModel.firstSource() @@ -135,6 +152,49 @@ class Request(var context: Context) { }) } + inner class MyRunnable(private val tvModel: TVViewModel) : Runnable { + override fun run() { + fetchBtrace(tvModel) + handler.postDelayed(this, 60000) + } + } + + fun fetchBtrace(tvModel: TVViewModel) { + val title = tvModel.title.value + + val guid = ysp?.getGuid()!! + val pid = tvModel.pid.value!! + val sid = tvModel.sid.value!! + yspBtraceService?.kvcollect( + c_timestamp = ysp?.generateGuid()!!, + guid = guid, + c_guid = guid, + prog = sid, + viewid = sid, + fpid = pid, + livepid = pid, + sUrl = "https://www.yangshipin.cn/#/tv/home?pid=$pid", + playno = ysp?.getRand()!!, + ftime = getCurrentDate2(), + seq = tvModel.seq.toString(), + ) + ?.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + Log.i(TAG, "$title kvcollect success") + } else { + Log.e(TAG, "$title status error") + tvModel.firstSource() + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.e(TAG, "$title btrace error") + } + }) + tvModel.seq++ + } + fun fetchPage() { yspProtoService?.getPage()?.enqueue(object : Callback { override fun onResponse( @@ -184,6 +244,12 @@ class Request(var context: Context) { return formatter.format(currentDate) } + private fun getCurrentDate2(): String { + val currentDate = Date() + val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) + return formatter.format(currentDate) + } + fun fetchProgram(tvModel: TVViewModel) { yspProtoService?.getProgram(tvModel.programId.value!!, getCurrentDate()) ?.enqueue(object : Callback { diff --git a/app/src/main/java/com/lizongying/mytv/api/BtraceClient.kt b/app/src/main/java/com/lizongying/mytv/api/BtraceClient.kt new file mode 100644 index 0000000..ba91d6b --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/api/BtraceClient.kt @@ -0,0 +1,17 @@ +package com.lizongying.mytv.api + + +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + + +class BtraceClient { + private val yspUrl = "https://btrace.yangshipin.cn/" + + val yspBtraceService: YSPBtraceService by lazy { + Retrofit.Builder() + .baseUrl(yspUrl) + .addConverterFactory(GsonConverterFactory.create()) + .build().create(YSPBtraceService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv/api/YSP.kt b/app/src/main/java/com/lizongying/mytv/api/YSP.kt index 430ffb8..75e3d0b 100644 --- a/app/src/main/java/com/lizongying/mytv/api/YSP.kt +++ b/app/src/main/java/com/lizongying/mytv/api/YSP.kt @@ -85,7 +85,7 @@ class YSP(var context: Context) { return (Date().time / 1000).toString() } - private fun generateGuid(): String { + fun generateGuid(): String { val timestamp = (System.currentTimeMillis()).toString(36) val originalString = Math.random().toString() val resultString = if (originalString.startsWith("0.")) { @@ -97,7 +97,7 @@ class YSP(var context: Context) { return timestamp + "_" + randomPart } - private fun getGuid(): String { + fun getGuid(): String { var guid = sharedPref?.getString("guid", "") if (guid == null || guid.length < 18) { guid = generateGuid() @@ -109,7 +109,7 @@ class YSP(var context: Context) { return guid } - private fun getRand(): String { + fun getRand(): String { var n = "" val e = "ABCDEFGHIJKlMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" val r = e.length diff --git a/app/src/main/java/com/lizongying/mytv/api/YSPBtraceService.kt b/app/src/main/java/com/lizongying/mytv/api/YSPBtraceService.kt new file mode 100644 index 0000000..1106875 --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/api/YSPBtraceService.kt @@ -0,0 +1,71 @@ +package com.lizongying.mytv.api + +import retrofit2.Call +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + + +interface YSPBtraceService { + @FormUrlEncoded + @POST("kvcollect") + @Headers( + "content-type: application/x-www-form-urlencoded", + "referer: https://www.yangshipin.cn/", + ) + fun kvcollect( + @Query("BossId") BossId: String = "2727", + @Query("c_timestamp") c_timestamp: String = "", + @Field("Pwd") Pwd: String = "1424084450", + @Field("fpid") fpid: String = "", + @Field("livepid") livepid: String = "", + @Field("prd") prd: String = "60000", + @Field("ftime") ftime: String = "", + @Field("prog") prog: String = "", + @Field("playno") playno: String = "", + @Field("guid") guid: String = "", + @Field("hh_ua") hh_ua: String = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", + @Field("cdn") cdn: String = "waibao", + @Field("platform") platform: String = "5910204", + @Field("errcode") errcode: String = "-", + @Field("sUrl") sUrl: String = "", + @Field("seq") seq: String = "", + @Field("login_type") login_type: String = "undefined", + @Field("open_id") open_id: String = "undefined", + @Field("openid") openid: String = "undefined", + @Field("defn") defn: String = "fhd", + @Field("durl") durl: String = "-", + @Field("sdtfrom") sdtfrom: String = "ysp_pc_01", + @Field("firstreport") firstreport: String = "0", + @Field("fplayerver") fplayerver: String = "89", + @Field("cmd") cmd: String = "263", + @Field("fact1") fact1: String = "ysp_pc_live_b", + @Field("sRef") sRef: String = "-", + @Field("viewid") viewid: String = "", + @Field("geturltime") geturltime: String = "0", + @Field("hc_openid") hc_openid: String = "undefined", + @Field("downspeed") downspeed: String = "10", + @Field("c_host") c_host: String = "www.yangshipin.cn", + @Field("c_pathname") c_pathname: String = "www.yangshipin.cn/", + @Field("c_url") c_url: String = "www.yangshipin.cn/", + @Field("c_channel") c_channel: String = "-", + @Field("c_referrer") c_referrer: String = "-", + @Field("c_ssize") c_ssize: String = "618", + @Field("c_ua") c_ua: String = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", + @Field("c_os") c_os: String = "mac os", + @Field("c_osv") c_osv: String = "os10.15.7", + @Field("c_browser") c_browser: String = "chrome", + @Field("c_browserv") c_browserv: String = "chrome119", + @Field("c_dvendor") c_dvendor: String = "apple", + @Field("c_dmodel") c_dmodel: String = "macintosh", + @Field("c_dtype") c_dtype: String = "unkown", + @Field("c_city") c_city: String = "disabled", + @Field("c_nation") c_nation: String = "disabled", + @Field("c_province") c_province: String = "disabled", + @Field("c_guid") c_guid: String = "", + @Field("c_vuid") c_vuid: String = "-", + ): Call +} \ No newline at end of file diff --git a/app/src/main/java/com/lizongying/mytv/models/TVViewModel.kt b/app/src/main/java/com/lizongying/mytv/models/TVViewModel.kt index 90aacba..ab760cc 100644 --- a/app/src/main/java/com/lizongying/mytv/models/TVViewModel.kt +++ b/app/src/main/java/com/lizongying/mytv/models/TVViewModel.kt @@ -1,9 +1,16 @@ package com.lizongying.mytv.models +import android.net.Uri import android.util.Log +import androidx.annotation.OptIn import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.exoplayer.hls.HlsMediaSource +import com.lizongying.mytv.CustomLoadErrorHandlingPolicy import com.lizongying.mytv.TV import com.lizongying.mytv.proto.Ysp.cn.yangshipin.omstv.common.proto.programModel.Program import java.util.Date @@ -242,6 +249,10 @@ class TVViewModel(private var tv: TV) : ViewModel() { val ready: LiveData get() = _ready + private var mMinimumLoadableRetryCount = 3 + + var seq = 0 + fun addVideoUrl(url: String) { if (_videoUrl.value?.isNotEmpty() == true) { if (_videoUrl.value!!.last().contains("cctv.cn")) { @@ -370,6 +381,32 @@ class TVViewModel(private var tv: TV) : ViewModel() { } } + + private var mHeaders: Map? = mapOf() + + fun setHeaders(headers: Map) { + mHeaders = headers + } + + + fun setMinimumLoadableRetryCount(minimumLoadableRetryCount: Int) { + mMinimumLoadableRetryCount = minimumLoadableRetryCount + } + + @OptIn(UnstableApi::class) + fun buildSource(videoUrl: String, mHeaders: Map?): HlsMediaSource { + val httpDataSource = DefaultHttpDataSource.Factory() + mHeaders?.let { httpDataSource.setDefaultRequestProperties(it) } + + return HlsMediaSource.Factory(httpDataSource).setLoadErrorHandlingPolicy( + CustomLoadErrorHandlingPolicy(mMinimumLoadableRetryCount) + ).createMediaSource( + MediaItem.fromUri( + Uri.parse(videoUrl) + ) + ) + } + companion object { private const val TAG = "TVViewModel" }