Compare commits

...
10 Commits
Author SHA1 Message Date
Li ZongYing 0285f1b3b1 remove left headers 2023-12-20 14:55:39 +08:00
Li ZongYing e6fa9a50db change sign 2023-12-18 13:37:27 +08:00
Li ZongYing 7b7db88422 test sign 2023-12-18 13:23:55 +08:00
Li ZongYing 4e3123019d remove guid store 2023-12-18 12:47:21 +08:00
Li ZongYing 9191d58513 backup source 2023-12-18 10:12:52 +08:00
Li ZongYing 434915fd3d keep screen on 2023-12-16 15:09:09 +08:00
Li ZongYing d8fffbb402 optimize ysp 2023-12-16 08:19:02 +08:00
Li ZongYing cc2dc25e35 optimize ysp 2023-12-15 18:12:11 +08:00
Li ZongYing aef165504f change to exoplayer 2023-12-15 15:41:59 +08:00
Li ZongYing dc5f988baa fix bug 2023-12-15 14:32:02 +08:00
24 changed files with 737 additions and 278 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
distribution: 'temurin'
- name: Run build with Gradle wrapper
run: ./gradlew assembleRelease
run: ./gradlew assembleRelease -PIS_SO_BUILD=false
- name: Sign app APK
id: sign_app
+1 -1
View File
@@ -3,4 +3,4 @@
/cmake_install.cmake
/CMakeFiles/
/Makefile
/src/main/cpp/native-lib.c
/src/main/cpp/native.c
+8 -8
View File
@@ -15,17 +15,17 @@ if (IS_SO_BUILD)
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.
add_library( # Specifies the name of the library.
native-lib
native
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.c)
src/main/cpp/native.c)
# 设置编译输出路径
set_target_properties(
native-lib
native
PROPERTIES
LIBRARY_OUTPUT_DIRECTORY
${CMAKE_SOURCE_DIR}/src/main/cpp/${ANDROID_ABI}
@@ -36,18 +36,18 @@ else ()
SHARED
src/main/cpp/nothing.c)
add_library(native-lib
add_library(native
SHARED
IMPORTED)
set_target_properties( # Specifies the target library.
native-lib
native
# Specifies the parameter you want to define.
PROPERTIES IMPORTED_LOCATION
# Provides the path to the library you want to import.
${CMAKE_SOURCE_DIR}/src/main/cpp/${ANDROID_ABI}/libnative-lib.so)
${CMAKE_SOURCE_DIR}/src/main/cpp/${ANDROID_ABI}/libnative.so)
endif ()
add_library(libssl
@@ -87,7 +87,7 @@ find_library( # Defines the name of the path variable that stores the
if (IS_SO_BUILD)
# Links your native library against one or more other native libraries.
target_link_libraries( # Specifies the target library.
native-lib
native
libssl
libcrypto
${log-lib})
@@ -96,6 +96,6 @@ else ()
nothing
libssl
libcrypto
native-lib
native
${log-lib})
endif ()
+10 -3
View File
@@ -24,8 +24,7 @@ android {
// For ndk-build, instead use the ndkBuild block.
cmake {
// arguments "-DIS_SO_BUILD=${project.hasProperty('IS_SO_BUILD') ? project.IS_SO_BUILD : false}"
arguments "-DIS_SO_BUILD=${false}"
arguments "-DIS_SO_BUILD=${project.hasProperty('IS_SO_BUILD') ? project.IS_SO_BUILD : true}"
abiFilters "armeabi-v7a", "arm64-v8a"
}
@@ -90,13 +89,21 @@ static def VersionName() {
}
dependencies {
def media3_version = "1.1.1"
// For media playback using ExoPlayer
implementation "androidx.media3:media3-exoplayer:$media3_version"
// For HLS playback support with ExoPlayer
implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
implementation 'com.google.protobuf:protobuf-kotlin:3.25.1'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:converter-protobuf:2.9.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"
implementation 'androidx.core:core-ktx:1.11.0-beta02'
implementation 'androidx.leanback:leanback:1.0.0'
implementation 'androidx.leanback:leanback:1.2.0-alpha02'
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0-RC")
+1
View File
@@ -14,6 +14,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.MyTV">
<activity
android:keepScreenOn="true"
android:name=".MainActivity"
android:banner="@drawable/tv"
android:exported="true"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -49,13 +49,12 @@ class CardPresenter(
}
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
val tv = item as TV
val tvViewModel = item as TVViewModel
val cardView = viewHolder.view as ImageCardView
cardView.titleText = tv.title
cardView.titleText = tvViewModel.title.value
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT)
cardView.tag = tv.videoUrl
Log.i(TAG, "bind ${tv.videoUrl}")
cardView.tag = tvViewModel.videoUrl.value
// lifecycleScope.launch(Dispatchers.IO) {
// val videoThumbnail = tv.videoUrl?.let { getVideoThumbnail(it) }
@@ -84,17 +83,17 @@ class CardPresenter(
private fun updateCardBackgroundColor(view: ImageCardView) {
val currentTag = view.tag
lifecycleScope.launch(Dispatchers.IO) {
delay(1000)
if (view.isSelected && view.tag != null && currentTag == view.tag) {
val videoThumbnail = view.tag.toString().let { getVideoThumbnail(it) }
withContext(Dispatchers.Main) {
if (view.isSelected && currentTag == view.tag) {
view.mainImageView.setImageBitmap(videoThumbnail)
}
}
}
}
// lifecycleScope.launch(Dispatchers.IO) {
// delay(1000)
// if (view.isSelected && view.tag != null && currentTag == view.tag) {
// val videoThumbnail = view.tag.toString().let { getVideoThumbnail(it) }
// withContext(Dispatchers.Main) {
// if (view.isSelected && currentTag == view.tag) {
// view.mainImageView.setImageBitmap(videoThumbnail)
// }
// }
// }
// }
}
private fun getVideoThumbnail(url: String): Bitmap? {
@@ -113,7 +112,7 @@ class CardPresenter(
companion object {
private const val TAG = "CardPresenter"
private const val CARD_WIDTH = 313
private const val CARD_HEIGHT = 176
private const val CARD_WIDTH = 300
private const val CARD_HEIGHT = 169
}
}
@@ -11,7 +11,7 @@ class Encryptor {
companion object {
init {
System.loadLibrary("native-lib")
System.loadLibrary("native")
}
}
}
@@ -0,0 +1,272 @@
package com.lizongying.mytv
import android.content.Context
import android.media.MediaPlayer
import android.net.Uri
import android.os.Handler
import android.view.SurfaceHolder
import androidx.annotation.OptIn
import androidx.leanback.media.PlaybackGlueHost
import androidx.leanback.media.PlayerAdapter
import androidx.leanback.media.SurfaceHolderGlueHost
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.hls.HlsMediaSource
import java.io.IOException
open class ExoPlayerAdapter(private var mContext: Context?) : PlayerAdapter() {
val mPlayer = mContext?.let { ExoPlayer.Builder(it).build() }
var mSurfaceHolderGlueHost: SurfaceHolderGlueHost? = null
val mRunnable: Runnable = object : Runnable {
override fun run() {
callback.onCurrentPositionChanged(this@ExoPlayerAdapter)
mHandler.postDelayed(this, getProgressUpdatingInterval().toLong())
}
};
val mHandler = Handler()
var mInitialized = false // true when the MediaPlayer is prepared/initialized
var mMediaSourceUri: Uri? = null
var mHasDisplay = false
var mBufferedProgress: Long = 0
var mBufferingStart = false
open fun notifyBufferingStartEnd() {
callback.onBufferingStateChanged(
this@ExoPlayerAdapter,
mBufferingStart || !mInitialized
)
}
override fun onAttachedToHost(host: PlaybackGlueHost?) {
if (host is SurfaceHolderGlueHost) {
mSurfaceHolderGlueHost = host
mSurfaceHolderGlueHost!!.setSurfaceHolderCallback(VideoPlayerSurfaceHolderCallback(this))
}
}
/**
* Will reset the [MediaPlayer] and the glue such that a new file can be played. You are
* not required to call this method before playing the first file. However you have to call it
* before playing a second one.
*/
open fun reset() {
changeToUnitialized()
// mPlayer.reset()
}
open fun changeToUnitialized() {
if (mInitialized) {
mInitialized = false
notifyBufferingStartEnd()
if (mHasDisplay) {
callback.onPreparedStateChanged(this@ExoPlayerAdapter)
}
}
}
/**
* Release internal MediaPlayer. Should not use the object after call release().
*/
open fun release() {
changeToUnitialized()
mHasDisplay = false
mPlayer?.release()
}
override fun onDetachedFromHost() {
if (mSurfaceHolderGlueHost != null) {
mSurfaceHolderGlueHost!!.setSurfaceHolderCallback(null)
mSurfaceHolderGlueHost = null
}
reset()
release()
}
/**
* @see MediaPlayer.setDisplay
*/
fun setDisplay(surfaceHolder: SurfaceHolder?) {
val hadDisplay = mHasDisplay
mHasDisplay = surfaceHolder != null
if (hadDisplay == mHasDisplay) {
return
}
mPlayer?.setVideoSurfaceHolder(surfaceHolder)
if (mHasDisplay) {
if (mInitialized) {
callback.onPreparedStateChanged(this@ExoPlayerAdapter)
}
} else {
if (mInitialized) {
callback.onPreparedStateChanged(this@ExoPlayerAdapter)
}
}
}
override fun setProgressUpdatingEnabled(enabled: Boolean) {
mHandler.removeCallbacks(mRunnable)
if (!enabled) {
return
}
mHandler.postDelayed(mRunnable, getProgressUpdatingInterval().toLong())
}
/**
* Return updating interval of progress UI in milliseconds. Subclass may override.
* @return Update interval of progress UI in milliseconds.
*/
open fun getProgressUpdatingInterval(): Int {
return 16
}
override fun isPlaying(): Boolean {
return mInitialized && mPlayer?.isPlaying ?: false
}
override fun getDuration(): Long {
if (mInitialized) {
val duration = mPlayer?.duration
if (duration != null) {
return duration.toLong()
}
}
return -1
}
override fun getCurrentPosition(): Long {
if (mInitialized) {
val currentPosition = mPlayer?.currentPosition
if (currentPosition != null) {
return currentPosition.toLong()
}
}
return -1
}
override fun play() {
if (!mInitialized || mPlayer?.isPlaying == true) {
return
}
mPlayer?.play()
callback.onPlayStateChanged(this@ExoPlayerAdapter)
callback.onCurrentPositionChanged(this@ExoPlayerAdapter)
}
override fun pause() {
if (isPlaying) {
mPlayer?.pause()
callback.onPlayStateChanged(this@ExoPlayerAdapter)
}
}
override fun seekTo(newPosition: Long) {
if (!mInitialized) {
return
}
mPlayer?.seekTo(newPosition.toInt().toLong())
}
override fun getBufferedPosition(): Long {
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)
}
}
/**
* Sets the media source of the player witha given URI.
*
* @return Returns `true` if uri represents a new media; `false`
* otherwise.
* @see MediaPlayer.setDataSource
*/
open fun setDataSource(uri: Uri?): Boolean {
if (if (mMediaSourceUri != null) mMediaSourceUri == uri else uri == null) {
return false
}
mMediaSourceUri = uri
prepareMediaForPlaying()
mPlayer?.playWhenReady = true
if (mPlayerErrorListener == null) {
mPlayerErrorListener = PlayerErrorListener()
mPlayer?.addListener(mPlayerErrorListener!!)
}
return true
}
private var mHeaders: Map<String, String>? = mapOf()
fun setHeaders(headers: Map<String, String>) {
mHeaders = headers
}
@OptIn(UnstableApi::class)
private fun prepareMediaForPlaying() {
reset()
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
}
} catch (e: IOException) {
e.printStackTrace()
throw RuntimeException(e)
}
mPlayer?.prepare()
callback.onPlayStateChanged(this@ExoPlayerAdapter)
}
/**
* @return True if MediaPlayer OnPreparedListener is invoked and got a SurfaceHolder if
* [PlaybackGlueHost] provides SurfaceHolder.
*/
override fun isPrepared(): Boolean {
return mInitialized && (mSurfaceHolderGlueHost == null || mHasDisplay)
}
companion object {
private const val TAG = "ExoPlayerAdapter"
}
}
internal class VideoPlayerSurfaceHolderCallback(private val playerAdapter: ExoPlayerAdapter) :
SurfaceHolder.Callback {
override fun surfaceCreated(surfaceHolder: SurfaceHolder) {
playerAdapter.setDisplay(surfaceHolder)
}
override fun surfaceChanged(surfaceHolder: SurfaceHolder, i: Int, i1: Int, i2: Int) {
// Handle surface changes if needed
}
override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) {
playerAdapter.setDisplay(null)
}
}
@@ -1,9 +0,0 @@
package com.lizongying.mytv
import java.io.Serializable
data class Info(
var rowPosition: Int = 0,
var itemPosition: Int = 0,
var item: TV,
) : Serializable
@@ -11,12 +11,14 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.KeyEvent
import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import com.lizongying.mytv.models.TVViewModel
import java.security.MessageDigest
@@ -32,6 +34,8 @@ class MainActivity : FragmentActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.add(R.id.main_browse_fragment, playbackFragment)
@@ -63,9 +67,9 @@ class MainActivity : FragmentActivity() {
.commit()
}
fun play(tv: TV) {
Log.i(TAG, "play: $tv")
playbackFragment.play(tv)
fun play(tvViewModel: TVViewModel) {
Log.i(TAG, "play: ${tvViewModel.getTV()}")
playbackFragment.play(tvViewModel)
}
fun prev() {
@@ -168,30 +172,57 @@ class MainActivity : FragmentActivity() {
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
Log.i(TAG, "KEYCODE_DPAD_CENTER")
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_UP -> {
if (mainFragment.isHidden) {
prev()
} else {
if (mainFragment.selectedPosition == 0) {
mainFragment.setSelectedPosition(
mainFragment.tvListViewModel.maxNum.size - 1,
false
)
}
}
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
if (mainFragment.isHidden) {
next()
} else {
if (mainFragment.selectedPosition == mainFragment.tvListViewModel.maxNum.size - 1) {
// mainFragment.setSelectedPosition(0, false)
hideMainFragment()
return false
}
}
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
if (mainFragment.isHidden) {
prevSource()
} else {
if (mainFragment.tvListViewModel.getTVViewModelCurrent()
?.getItemPosition() == 0
) {
// mainFragment.toLastPosition()
hideMainFragment()
}
}
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (mainFragment.isHidden) {
nextSource()
} else {
if (mainFragment.tvListViewModel.getTVViewModelCurrent()
?.getItemPosition() == mainFragment.tvListViewModel.maxNum[mainFragment.selectedPosition] - 1
) {
mainFragment.toFirstPosition()
}
}
}
}
@@ -19,22 +19,25 @@ import androidx.leanback.widget.Row
import androidx.leanback.widget.RowPresenter
import androidx.lifecycle.lifecycleScope
import com.lizongying.mytv.models.TVListViewModel
import com.lizongying.mytv.models.TVViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainFragment : BrowseSupportFragment() {
var itemPosition: Int = 0
private val list2: MutableList<Info> = mutableListOf()
private var request: Request? = null
private var rowsAdapter: ArrayObjectAdapter? = null
private var tvListViewModel = TVListViewModel()
var tvListViewModel = TVListViewModel()
private var sharedPref: SharedPreferences? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
headersState = HEADERS_DISABLED
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
@@ -47,64 +50,112 @@ class MainFragment : BrowseSupportFragment() {
setupEventListeners()
view?.post {
request?.fetchPage()
// request?.fetchPage()
}
tvListViewModel.getListLiveData().value?.forEach { tvViewModel ->
tvViewModel.videoUrl.observe(viewLifecycleOwner) { _ ->
Log.i(TAG, "tv ${tvViewModel.getTV()}")
if (tvViewModel.updateByYSP()) {
val tv = tvViewModel.getTV()
if (tv.id == itemPosition) {
(activity as? MainActivity)?.play(tv)
tvListViewModel.getTVListViewModel().value?.forEach { tvViewModel ->
tvViewModel.ready.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.ready.value != null) {
Log.i(TAG, "ready ${tvViewModel.title.value}")
if (tvViewModel.id.value == itemPosition) {
(activity as? MainActivity)?.play(tvViewModel)
// (activity as? MainActivity)?.switchInfoFragment(tv)
}
}
}
tvViewModel.change.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.change.value != null) {
Log.i(TAG, "switch to ${tvViewModel.title.value}")
if (tvViewModel.ysp() != null) {
Log.i(TAG, "${tvViewModel.title.value} to get ysp")
lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { request?.fetchData(it) }
}
} else {
(activity as? MainActivity)?.play(tvViewModel)
// (activity as? MainActivity)?.switchInfoFragment(item)
}
setSelectedPosition(
tvViewModel.getRowPosition(), true,
SelectItemViewHolderTask(tvViewModel.getItemPosition())
)
Toast.makeText(
activity,
tvViewModel.title.value,
Toast.LENGTH_SHORT
).show()
}
}
}
}
fun toLastPosition() {
setSelectedPosition(
selectedPosition, false,
SelectItemViewHolderTask(tvListViewModel.maxNum[selectedPosition] - 1)
)
}
fun toFirstPosition() {
setSelectedPosition(
selectedPosition, false,
SelectItemViewHolderTask(0)
)
}
override fun startHeadersTransition(withHeaders: Boolean) {
// check(mCanShowHeaders) { "Cannot start headers transition" }
// if (isInHeadersTransition || mShowingHeaders == withHeaders) {
// return
// }
// startHeadersTransitionInternal(withHeaders)
}
private fun setupUIElements() {
brandColor = ContextCompat.getColor(context!!, R.color.fastlane_background)
// headersState = HEADERS_DISABLED
// var headers = headersSupportFragment
// headers.setMenuVisibility(false)
// Log.i(TAG, "headers $headers")
// setHeadersState(HEADERS_DISABLED);
//
// setHeaderPresenterSelector(object : PresenterSelector() {
// override fun getPresenter(o: Any): Presenter {
// return IconHeaderItemPresenter()
// }
// })
// showHeaders(true)
}
private fun updateRows(tv: TV) {
// 获取适配器中的数据
val dataList = rowsAdapter?.replace(tv.id, tv)
//
//// 修改数据
//// 这里假设 dataList 是一个可变的列表
// dataList[position] = updatedData
//
//// 刷新适配器
// rowsAdapter.notifyItemChanged(position)
// rowsAdapter.notifyItemRangeChanged()
}
// private fun updateRows(tv: TV) {
//// 获取适配器中的数据
// val dataList = rowsAdapter?.replace(tv.id, tv)
////
////// 修改数据
////// 这里假设 dataList 是一个可变的列表
//// dataList[position] = updatedData
////
////// 刷新适配器
//// rowsAdapter.notifyItemChanged(position)
//// rowsAdapter.notifyItemRangeChanged()
// }
private fun loadRows() {
val list = TVList.list
rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
// val cardPresenter = CardPresenter(lifecycleScope, viewLifecycleOwner)
val cardPresenter = CardPresenter(lifecycleScope)
var idx: Long = 0
for ((k, v) in list) {
for ((k, v) in TVList.list) {
val listRowAdapter = ArrayObjectAdapter(cardPresenter)
var idx2 = 0
for ((_, v1) in v) {
list2.add(
Info(
idx.toInt(), idx2, v1
)
)
listRowAdapter.add(v1)
idx2++
tvListViewModel.addTV(v1)
for ((idx2, v1) in v.withIndex()) {
val tvViewModel = TVViewModel(v1)
tvViewModel.setRowPosition(idx.toInt())
tvViewModel.setItemPosition(idx2)
tvListViewModel.addTVViewModel(tvViewModel)
listRowAdapter.add(tvViewModel)
}
tvListViewModel.maxNum.add(v.size)
val header = HeaderItem(idx, k)
rowsAdapter!!.add(ListRow(header, listRowAdapter))
idx++
@@ -113,17 +164,14 @@ class MainFragment : BrowseSupportFragment() {
adapter = rowsAdapter
itemPosition = sharedPref?.getInt("position", 0)!!
val tvModel = tvListViewModel.getTVModel(itemPosition)
if (tvModel?.ysp() != null) {
lifecycleScope.launch(Dispatchers.IO) {
tvModel.let { request?.fetchData(it) }
}
} else {
(activity as? MainActivity)?.play(list2[itemPosition].item)
// (activity as? MainActivity)?.switchInfoFragment(list2[itemPosition].item)
if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0
savePosition(0)
}
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
(activity as? MainActivity)?.switchMainFragment()
}
@@ -134,6 +182,7 @@ class MainFragment : BrowseSupportFragment() {
}
fun savePosition(position: Int) {
tvListViewModel.setItemPosition(position)
with(sharedPref!!.edit()) {
putInt("position", position)
apply()
@@ -144,95 +193,63 @@ class MainFragment : BrowseSupportFragment() {
view?.post {
itemPosition--
if (itemPosition == -1) {
itemPosition = list2.size - 1
itemPosition = tvListViewModel.size() - 1
}
savePosition(itemPosition)
val l = list2[itemPosition]
val tvModel = tvListViewModel.getTVModel(itemPosition)
if (tvModel?.ysp() != null) {
lifecycleScope.launch(Dispatchers.IO) {
tvModel.let { request?.fetchData(it) }
}
} else {
l.item.let { (activity as? MainActivity)?.play(it) }
}
setSelectedPosition(
l.rowPosition, false,
SelectItemViewHolderTask(l.itemPosition)
)
Toast.makeText(
activity,
l.item.title,
Toast.LENGTH_SHORT
).show()
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
}
}
fun next() {
view?.post {
itemPosition++
if (itemPosition == list2.size) {
if (itemPosition == tvListViewModel.size()) {
itemPosition = 0
}
savePosition(itemPosition)
val l = list2[itemPosition]
val tvModel = tvListViewModel.getTVModel(itemPosition)
if (tvModel?.ysp() != null) {
lifecycleScope.launch(Dispatchers.IO) {
tvModel.let { request?.fetchData(it) }
}
} else {
l.item.let { (activity as? MainActivity)?.play(it) }
}
setSelectedPosition(
l.rowPosition, false,
SelectItemViewHolderTask(l.itemPosition)
)
Toast.makeText(
activity,
l.item.title,
Toast.LENGTH_SHORT
).show()
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
}
}
fun prevSource() {
view?.post {
val tv = list2[itemPosition].item
if (tv.videoUrl.size > 1) {
tv.videoIndex--
if (tv.videoIndex == -1) {
tv.videoIndex = tv.videoUrl.size - 1
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
if (tvViewModel != null) {
if (tvViewModel.videoUrl.value!!.size > 1) {
val videoIndex = tvViewModel.videoIndex.value?.minus(1)
if (videoIndex == -1) {
tvViewModel.setVideoIndex(tvViewModel.videoUrl.value!!.size - 1)
}
tvViewModel.changed()
}
(activity as? MainActivity)?.play(tv)
// (activity as? MainActivity)?.switchInfoFragment(tv)
}
}
}
fun nextSource() {
view?.post {
val tv = list2[itemPosition].item
if (tv.videoUrl.size > 1) {
tv.videoIndex++
if (tv.videoIndex == tv.videoUrl.size) {
tv.videoIndex = 0
}
(activity as? MainActivity)?.play(tv)
// (activity as? MainActivity)?.switchInfoFragment(tv)
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
if (tvViewModel != null) {
tvViewModel.changed()
// if (tvViewModel.videoUrl.value!!.size > 1) {
// val videoIndex = tvViewModel.videoIndex.value?.plus(1)
// if (videoIndex == tvViewModel.videoUrl.value!!.size) {
// tvViewModel.setVideoIndex(0)
// }
// tvViewModel.changed()
// }
}
}
}
fun tvViewModel(): TVViewModel? {
return tvListViewModel.getTVViewModel(itemPosition)
}
private fun setupEventListeners() {
onItemViewClickedListener = ItemViewClickedListener()
onItemViewSelectedListener = ItemViewSelectedListener()
@@ -245,19 +262,12 @@ class MainFragment : BrowseSupportFragment() {
rowViewHolder: RowPresenter.ViewHolder,
row: Row
) {
if (item is TV) {
itemPosition = item.id
if (item is TVViewModel) {
itemPosition = item.id.value!!
savePosition(itemPosition)
val tvModel = tvListViewModel.getTVModel(itemPosition)
if (tvModel?.ysp() != null) {
lifecycleScope.launch(Dispatchers.IO) {
tvModel.let { request?.fetchData(it) }
}
} else {
(activity as? MainActivity)?.play(item)
// (activity as? MainActivity)?.switchInfoFragment(item)
}
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
(activity as? MainActivity)?.switchMainFragment()
}
@@ -271,13 +281,18 @@ class MainFragment : BrowseSupportFragment() {
) {
if (itemViewHolder == null) {
view?.post {
val it = list2[itemPosition]
setSelectedPosition(
it.rowPosition, false,
SelectItemViewHolderTask(it.itemPosition)
)
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
if (tvViewModel != null) {
setSelectedPosition(
tvViewModel.getRowPosition(), false,
SelectItemViewHolderTask(tvViewModel.getItemPosition())
)
}
}
}
if (item is TVViewModel) {
tvListViewModel.setItemPositionCurrent(item.id.value!!)
}
}
}
@@ -1,21 +1,24 @@
package com.lizongying.mytv
import android.content.Context
import android.util.Log
import android.view.KeyEvent
import android.view.View
import androidx.leanback.media.MediaPlayerAdapter
import androidx.leanback.media.PlaybackTransportControlGlue
import androidx.leanback.media.PlayerAdapter
class PlaybackControlGlue(
context: Context?,
playerAdapter: MediaPlayerAdapter?,
playerAdapter: PlayerAdapter?,
) :
PlaybackTransportControlGlue<MediaPlayerAdapter>(context, playerAdapter) {
PlaybackTransportControlGlue<PlayerAdapter>(context, playerAdapter) {
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
if (event!!.action == KeyEvent.ACTION_DOWN) {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_CENTER -> {
Log.i(TAG, "KEYCODE_DPAD_CENTER")
(context as? MainActivity)?.switchMainFragment()
}
@@ -49,6 +52,6 @@ class PlaybackControlGlue(
}
companion object {
private const val TAG = "CustomPlaybackControlGlue"
private const val TAG = "PlaybackControlGlue"
}
}
@@ -5,21 +5,22 @@ import android.os.Bundle
import android.util.Log
import androidx.leanback.app.VideoSupportFragment
import androidx.leanback.app.VideoSupportFragmentGlueHost
import androidx.leanback.media.MediaPlayerAdapter
import androidx.leanback.media.PlaybackTransportControlGlue
import androidx.leanback.media.PlayerAdapter
import androidx.leanback.widget.PlaybackControlsRow
import com.lizongying.mytv.models.TVViewModel
import java.io.IOException
class PlaybackFragment : VideoSupportFragment() {
private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<MediaPlayerAdapter>
private var playerAdapter: MediaPlayerAdapter? = null
private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<PlayerAdapter>
private var playerAdapter: ExoPlayerAdapter? = null
private var lastVideoUrl: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
playerAdapter = MediaPlayerAdapter(context)
playerAdapter = ExoPlayerAdapter(context)
playerAdapter?.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_NONE)
view?.isFocusable = false
@@ -29,12 +30,10 @@ class PlaybackFragment : VideoSupportFragment() {
override fun showControlsOverlay(runAnimation: Boolean) {
}
fun play(tv: TV) {
val videoUrl = tv.videoUrl[tv.videoIndex]
if (videoUrl == "") {
Log.e(TAG, "tv $tv")
Log.e(TAG, "videoUrl is empty")
fun play(tvModel: TVViewModel) {
val videoUrl = tvModel.videoIndex.value?.let { tvModel.videoUrl.value?.get(it) }
if (videoUrl == null || videoUrl == "") {
Log.e(TAG, "${tvModel.title.value} videoUrl is empty")
return
}
@@ -45,28 +44,29 @@ class PlaybackFragment : VideoSupportFragment() {
lastVideoUrl = videoUrl
playerAdapter?.reset()
val glueHost = VideoSupportFragmentGlueHost(this@PlaybackFragment)
mTransportControlGlue = PlaybackControlGlue(activity, playerAdapter)
mTransportControlGlue.host = glueHost
mTransportControlGlue.playWhenPrepared()
playerAdapter?.callback = PlayerCallback(tvModel)
try {
playerAdapter?.setDataSource(Uri.parse(videoUrl))
} catch (e: IOException) {
Log.e(TAG, "error $e")
return
}
hideControlsOverlay(false)
}
override fun onDestroy() {
if (playerAdapter?.mediaPlayer != null) {
playerAdapter?.release()
Log.d(TAG, "playerAdapter released")
private inner class PlayerCallback(private var tvModel: TVViewModel) :
PlayerAdapter.Callback() {
override fun onError(adapter: PlayerAdapter?, errorCode: Int, errorMessage: String?) {
Log.e(TAG, "on error: $errorMessage")
if (tvModel.ysp() != null && tvModel.videoIndex.value!! > 0 && errorMessage == "Source error") {
tvModel.changed()
}
}
super.onDestroy()
}
companion object {
@@ -1,6 +1,7 @@
package com.lizongying.mytv
import android.content.Context
import android.util.Base64
import android.util.Log
import com.lizongying.mytv.api.ApiClient
import com.lizongying.mytv.api.LiveInfo
@@ -14,6 +15,9 @@ import com.lizongying.mytv.proto.Ysp.cn.yangshipin.oms.common.proto.pageModel
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class Request(var context: Context) {
@@ -33,17 +37,17 @@ class Request(var context: Context) {
"CCTV10" to "CCTV10 科教",
"CCTV11" to "CCTV11 戏曲",
"CCTV12" to "CCTV12 社会与法",
// "CCTV13" to "CCTV13",
"CCTV13" to "CCTV13 新闻",
"CCTV14" to "CCTV14 少儿",
"CCTV15" to "CCTV15 音乐",
"CCTV16-HD" to "CCTV16 奥林匹克",
"CCTV17" to "CCTV17 农业农村",
"CGTN" to "CGTN",
"CGTN外语纪录频道" to "CGTN 纪录频道",
"CGTN法语频道" to "CGTN 法语频道",
"CGTN俄语频道" to "CGTN 俄语频道",
"CGTN阿拉伯语频道" to "CGTN 阿拉伯语频道",
"CGTN西班牙语频道" to "CGTN 西班牙语频道",
// "CGTN外语纪录频道" to "CGTN外语纪录频道",
"东方卫视" to "东方卫视",
"湖南卫视" to "湖南卫视",
@@ -78,6 +82,7 @@ class Request(var context: Context) {
fun fetchData(tvModel: TVViewModel) {
val data = ysp?.switch(tvModel)
val title = tvModel.title.value
val request = data?.let { LiveInfoRequest(it) }
request?.let { yspApiService?.getLiveInfo(it) }
@@ -86,12 +91,42 @@ class Request(var context: Context) {
if (response.isSuccessful) {
val liveInfo = response.body()
if (liveInfo?.data?.playurl != null) {
tvModel.updateVideoUrlByYSP(liveInfo.data.playurl)
val chanll = liveInfo.data.chanll
val decodedBytes = Base64.decode(
chanll.substring(9, chanll.length - 3),
Base64.DEFAULT
)
val decodedString = String(decodedBytes)
val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""")
val matchResult = regex.find(decodedString)
if (matchResult != null) {
val (key, iv) = matchResult.destructured
val keyBytes = Base64.decode(key, Base64.DEFAULT)
val ivBytes = Base64.decode(iv, Base64.DEFAULT)
val url = liveInfo.data.playurl + "&revoi=" + encryptTripleDES(
keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0),
ivBytes
).uppercase()
Log.i(TAG, "$title url $url")
tvModel.addVideoUrl(url)
tvModel.allReady()
} else {
Log.e(TAG, "$title key error")
tvModel.firstSource()
}
} else {
Log.e(TAG, "$title url error $request")
tvModel.firstSource()
}
} else {
Log.e(TAG, "$title status error")
tvModel.firstSource()
}
}
override fun onFailure(call: Call<LiveInfo>, t: Throwable) {
Log.e(TAG, "$title request error")
tvModel.firstSource()
}
})
}
@@ -121,7 +156,8 @@ class Request(var context: Context) {
if (!mapping.containsKey(item.channelName)) {
continue
}
val tv = TVList.list[channelType]?.get(mapping[item.channelName])
val tv =
TVList.list[channelType]?.find { it.title == mapping[item.channelName] }
if (tv != null) {
tv.logo = item.tvLogo
tv.pid = item.pid
@@ -152,6 +188,22 @@ class Request(var context: Context) {
})
}
private fun encryptTripleDES(key: ByteArray, iv: ByteArray): String {
val plaintext =
"""{"mver":"1","subver":"1.2","host":"www.yangshipin.cn/#/tv/home?pid=","referer":"","canvas":"YSPANGLE(Apple,AppleM1Pro,OpenGL4.1)"}"""
return try {
val keySpec = SecretKeySpec(key, "DESede")
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
val encryptedBytes = cipher.doFinal(plaintext.toByteArray())
return encryptedBytes.let { it -> it.joinToString("") { "%02x".format(it) } }
} catch (e: Exception) {
e.printStackTrace()
""
}
}
companion object {
private const val TAG = "Request"
}
+24 -30
View File
@@ -1,50 +1,45 @@
package com.lizongying.mytv
import android.util.Log
object TVList {
val list: Map<String, Map<String, TV>> by lazy {
val list: Map<String, List<TV>> by lazy {
setupTV()
}
private var count: Int = 0
private fun setupTV(): Map<String, Map<String, TV>> {
private fun setupTV(): Map<String, List<TV>> {
val tvs = """
央视频道
CCTV4K,,https://resources.yangshipin.cn/assets/oms/image/202306/3e9d06fd7244d950df5838750f1c6ac3456e172b51caca2c16d2282125b111e8.png?imageMogr2/format/webp,600002264,2000266303
CCTV1 综合,http://tvpull.dxhmt.cn/tv/11481-4.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/d57905b93540bd15f0c48230dbbbff7ee0d645ff539e38866e2d15c8b9f7dfcd.png?imageMogr2/format/webp,600001859,2000210103
CCTV2 财经,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226195/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/20115388de0207131af17eac86c33049b95d69eaff064e55653a1b941810a006.png?imageMogr2/format/webp,600001800,2000203603
CCTV3 综艺,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226397/index.m3u8
CCTV4 中文国际,http://39.134.24.161/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226191/index.m3u8;http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv4hd/1500/index.m3u8?&encrypt=,https://resources.yangshipin.cn/assets/oms/image/202306/f357e58fdbcc076a3d65e1f958c942b2e14f14342c60736ceed98b092d35356a.png?imageMogr2/format/webp,600001814,2000204803
CCTV4 中文国际美洲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4meihd/57/index.m3u8?&encrypt=
CCTV4 中文国际欧洲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4ouhd/51/index.m3u8?&encrypt=
CCTV5 体育,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv5hd265/57/20191230/index.m3u8?&encrypt=,https://resources.yangshipin.cn/assets/oms/image/202306/0a6a7138952675983a3d854df7688557b286d59aa06166edae51506f9204d655.png?imageMogr2/format/webp,600001818,2000205103
CCTV5+ 体育赛事,http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv5plusnew/2500/index.m3u8?&encrypt=,https://resources.yangshipin.cn/assets/oms/image/202306/649ad76a90bfef55b05db9fe52e006487280f619089099d5dc971e387fc6eff0.png?imageMogr2/format/webp,600001817,2000204503
CCTV6 电影,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv6hd/3000/index.m3u8?&encrypt=
CCTV7 国防军事,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv7hd/51/index.m3u8?&encrypt=,https://resources.yangshipin.cn/assets/oms/image/202306/b29af94e295ebdf646cefb68122c429b9cd921f498ca20d2d8070252536f9ff9.png?imageMogr2/format/webp,600004092,2000510003
CCTV8 电视剧,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv8hd/3000/index.m3u8?&encrypt=
CCTV9 记录,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv9hd/57/index.m3u8?&encrypt=,https://resources.yangshipin.cn/assets/oms/image/202306/2ed1b4deeca179d5db806bb941790f82eb92a1b7299c1c38fe027f95a5caee5e.png?imageMogr2/format/webp,600004078,2000499403
CCTV10 科教,http://hlsbkmgsplive.miguvideo.com/wd_r2/2018/ocn/cctv10hd/2000/index.m3u8?&encrypt=,https://resources.yangshipin.cn/assets/oms/image/202306/aa6157ec65188cd41826e5a2f088c3d6d153205f5f6428258d12c59999e221aa.png?imageMogr2/format/webp,600001805,2000203503
CCTV11 戏曲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv11hd/57/20200103/index.m3u8?&encrypt=,https://resources.yangshipin.cn/assets/oms/image/202306/ed12ed7c7a1034dae4350011fe039284c5d5a836506b28c9e32e3c75299625c0.png?imageMogr2/format/webp,600001806,2000204103
CCTV12 社会与法,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv12hd/57/index.m3u8?&encrypt=, https://resources.yangshipin.cn/assets/oms/image/202306/484083cffaa40df7e659565e8cb4d1cc740158a185512114167aa21fa0c59240.png?imageMogr2/format/webp,600001807,2000202603
CCTV13 新闻,https://live-play.cctvnews.cctv.com/cctv/merge_cctv13.m3u8;http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/cctv13/2000/index.m3u8?&encrypt=
CCTV14 少儿,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226193/index.m3u8;http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv14hd/3000/index.m3u8?&encrypt=,https://resources.yangshipin.cn/assets/oms/image/202306/af6b603896938dc346fbb16abfc63c12cba54b0ec9d18770a15d347d115f12d5.png?imageMogr2/format/webp,600001809,2000204403
CCTV15 音乐,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225785/index.m3u8;http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv15hd/51/index.m3u8?&encrypt=,https://resources.yangshipin.cn/assets/oms/image/202306/2ceee92188ef684efe0d8b90839c4f3ad450d179dc64d59beff417059453af47.png?imageMogr2/format/webp,600001815,2000205003
CCTV4 中文国际,http://39.134.24.161/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226191/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/f357e58fdbcc076a3d65e1f958c942b2e14f14342c60736ceed98b092d35356a.png?imageMogr2/format/webp,600001814,2000204803
CCTV5 体育,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226395/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/0a6a7138952675983a3d854df7688557b286d59aa06166edae51506f9204d655.png?imageMogr2/format/webp,600001818,2000205103
CCTV5+ 体育赛事,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226221/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/649ad76a90bfef55b05db9fe52e006487280f619089099d5dc971e387fc6eff0.png?imageMogr2/format/webp,600001817,2000204503
CCTV6 电影,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226393/index.m3u8
CCTV7 国防军事,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226192/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/b29af94e295ebdf646cefb68122c429b9cd921f498ca20d2d8070252536f9ff9.png?imageMogr2/format/webp,600004092,2000510003
CCTV8 电视剧,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226391/index.m3u8
CCTV9 记录,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226197/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/2ed1b4deeca179d5db806bb941790f82eb92a1b7299c1c38fe027f95a5caee5e.png?imageMogr2/format/webp,600004078,2000499403
CCTV10 科教,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226189/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/aa6157ec65188cd41826e5a2f088c3d6d153205f5f6428258d12c59999e221aa.png?imageMogr2/format/webp,600001805,2000203503
CCTV11 戏曲,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226240/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/ed12ed7c7a1034dae4350011fe039284c5d5a836506b28c9e32e3c75299625c0.png?imageMogr2/format/webp,600001806,2000204103
CCTV12 社会与法,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226190/index.m3u8, https://resources.yangshipin.cn/assets/oms/image/202306/484083cffaa40df7e659565e8cb4d1cc740158a185512114167aa21fa0c59240.png?imageMogr2/format/webp,600001807,2000202603
CCTV13 新闻,https://live-play.cctvnews.cctv.com/cctv/merge_cctv13.m3u8
CCTV14 少儿,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226193/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/af6b603896938dc346fbb16abfc63c12cba54b0ec9d18770a15d347d115f12d5.png?imageMogr2/format/webp,600001809,2000204403
CCTV15 音乐,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225785/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/2ceee92188ef684efe0d8b90839c4f3ad450d179dc64d59beff417059453af47.png?imageMogr2/format/webp,600001815,2000205003
CCTV16 奥林匹克,http://39.134.24.162/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226921/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/53793fa7bacd3a93ff6dc5d2758418985e1f952a316c335d663b572d8bdcd74d.png?imageMogr2/format/webp,600098637,2012375003
CCTV17 农业农村,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv17hd/57/index.m3u8?&encrypt=,https://resources.yangshipin.cn/assets/oms/image/202306/ddef563072f8bad2bea5b9e52674cb7b4ed50efb20c26e61994dfbdf05c1e3c0.png?imageMogr2/format/webp,600001810,2000204203
CGTN,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/cctvnews/1000/index.m3u8?&encrypt=,https://resources.yangshipin.cn/assets/oms/image/202306/a72dff758ca1c17cd0ecc8cedc11b893d208f409d5e6302faa0e9d298848abc3.png?imageMogr2/format/webp,600014550,2001656803
CCTV17 农业农村,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226198/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/ddef563072f8bad2bea5b9e52674cb7b4ed50efb20c26e61994dfbdf05c1e3c0.png?imageMogr2/format/webp,600001810,2000204203
CGTN,,https://resources.yangshipin.cn/assets/oms/image/202306/a72dff758ca1c17cd0ecc8cedc11b893d208f409d5e6302faa0e9d298848abc3.png?imageMogr2/format/webp,600014550,2001656803
CGTN 新闻频道,http://live.cgtn.com/1000/prog_index.m3u8
CGTN 纪录频道,https://livedoc.cgtn.com/500d/prog_index.m3u8
CGTN 法语频道,https://livefr.cgtn.com/1000f/prog_index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/a8d0046a47433d952bf6ed17062deb8bd2184ba9aec0f7781df6bf9487a3ffcf.png?imageMogr2/format/webp,600084704,2010153503
CGTN 俄语频道,http://liveru.cgtn.com/1000r/prog_index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/bf0a820893cbaf20dd0333e27042e1ef9c8806e5b602b6a8c95af399db0bc77a.png?imageMogr2/format/webp,600084758,2010152603
CGTN 西班牙语频道,http://livees.cgtn.com/500e/prog_index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202309/7c337e3dbe64402ec7e4678a619a4a6d95144e42f35161181ff78e143b7cf67a.png?imageMogr2/format/webp,600084744,2010152503
CGTN 西班牙语频道,http://livees.cgtn.com/500e/prog_index.m3u8;http://livees.cgtn.com/1000e/prog_index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202309/7c337e3dbe64402ec7e4678a619a4a6d95144e42f35161181ff78e143b7cf67a.png?imageMogr2/format/webp,600084744,2010152503
CGTN 阿拉伯语频道,http://livear.cgtn.com/1000a/prog_index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/2e44e2aa3e7a1cedf07fd0ae59fe69e86a60a2632660a006e3e9e7397b2d107e.png?imageMogr2/format/webp,600084782,2010155203
CGTN 拉丁美洲频道,http://livees.cgtn.com/1000e/prog_index.m3u8
书画频道,http://211.103.180.178:8234/live_hls/hdmi.m3u8
地方频道
东方卫视,,https://resources.yangshipin.cn/assets/oms/image/202306/9bd372ca292a82ce3aa08772b07efc4af1f85c21d1f268ea33440c49e9a0a488.png?imageMogr2/format/webp,600002483,2000292403
东方卫视,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226217/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/9bd372ca292a82ce3aa08772b07efc4af1f85c21d1f268ea33440c49e9a0a488.png?imageMogr2/format/webp,600002483,2000292403
内蒙古卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226389/index.m3u8
湖南卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226307/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/4120e89d3079d08aa17d382f69a2308ec70839b278367763c34a34666c75cb88.png?imageMogr2/format/webp,600002475,2000296203
湖北卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226477/index.m3u8,https://resources.yangshipin.cn/assets/oms/image/202306/7a6be5a2bb1dc53a945c016ff1f525dc4a84c51db371c15c89aa55404b0ba784.png?imageMogr2/format/webp,600002508,2000294503
@@ -148,7 +143,6 @@ CETV4,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226225/index.m3u8
兵团卫视,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226214/index.m3u8
延边卫视,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226220/index.m3u8
内蒙古卫视,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225786/index.m3u8
康巴卫视,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226234/index.m3u8
电视指南,http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226987/index.m3u8
风云足球,http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226984/index.m3u8
风云剧场,http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226950/index.m3u8
@@ -211,7 +205,7 @@ CHC动作电影,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226465/in
老故事,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226236/index.m3u8
""".trimIndent()
val map: MutableMap<String, MutableMap<String, TV>> = mutableMapOf()
val map: MutableMap<String, MutableList<TV>> = mutableMapOf()
var channel = ""
for (i in tvs.split("\n")) {
@@ -223,14 +217,13 @@ CHC动作电影,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226465/in
continue
}
val p = i.split(",")
val titleMap = map[channel] ?: mutableMapOf()
val titleMap = map[channel] ?: mutableListOf()
val tv = titleMap[p[0]] ?: TV(
val tv = TV(
count,
p[0],
p[1].split(";").map { it.trim() }
)
count++
if (p.size > 2) {
tv.logo = p[2]
@@ -242,8 +235,9 @@ CHC动作电影,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226465/in
tv.sid = p[4]
}
titleMap[p[0]] = tv
titleMap.add(tv)
map[channel] = titleMap
count++
}
return map
@@ -13,7 +13,8 @@ data class LiveInfo(
)
data class LiveInfoData(
val playurl: String
val chanll: String,
val playurl: String,
)
data class LiveInfoRequest(
@@ -64,11 +64,11 @@ class YSP(var context: Context) {
}
fun switch(tvModel: TVViewModel): String {
livepid = tvModel.getTV().pid
cnlid = tvModel.getTV().sid
livepid = tvModel.pid.value!!
cnlid = tvModel.sid.value!!
defn = "fhd"
guid = getGuid()
// guid = getGuid()
randStr = getRand()
timeStr = getTimeStr()
@@ -80,7 +80,6 @@ class YSP(var context: Context) {
encryptor!!.encrypt(cnlid, timeStr, appVer, guid, platform)
signature = getSignature()
return """{"cnlid":"$cnlid","livepid":"$livepid","stream":"$stream","guid":"$guid","cKey":"$cKey","adjust":$adjust,"sphttps":"$sphttps","platform":"$platform","cmd":"$cmd","encryptVer":"$encryptVer","dtype":"$dtype","devid":"$devid","otype":"$otype","appVer":"$appVer","app_version":"$appVersion","rand_str":"$randStr","channel":"$channel","defn":"$defn","signature":"$signature"}"""
}
private fun getTimeStr(): String {
@@ -100,8 +99,8 @@ class YSP(var context: Context) {
}
private fun getGuid(): String {
// var guid = sharedPref?.getString("guid", "").toString()
if (guid == "") {
var guid = sharedPref?.getString("guid", "")
if (guid == null || guid.length < 18) {
guid = generateGuid()
with(sharedPref!!.edit()) {
putString("guid", guid)
@@ -1,44 +1,61 @@
package com.lizongying.mytv.models
import android.util.Log
import android.content.SharedPreferences
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.lizongying.mytv.TV
class TVListViewModel : ViewModel() {
private val tvListLiveData = MutableLiveData<MutableList<TV>>()
private val tvModelListLiveData = MutableLiveData<MutableList<TVViewModel>>()
var maxNum = mutableListOf<Int>()
fun getListLiveData(): MutableLiveData<MutableList<TVViewModel>> {
return tvModelListLiveData
private var sharedPref: SharedPreferences? = null
private val tvListViewModel = MutableLiveData<MutableList<TVViewModel>>()
private val _itemPosition = MutableLiveData<Int>()
val itemPosition: LiveData<Int>
get() = _itemPosition
private val _itemPositionCurrent = MutableLiveData<Int>()
val itemPositionCurrent: LiveData<Int>
get() = _itemPositionCurrent
fun getTVListViewModel(): MutableLiveData<MutableList<TVViewModel>> {
return tvListViewModel
}
fun addTV(tv: TV) {
val currentList = tvListLiveData.value ?: mutableListOf()
currentList.add(tv)
tvListLiveData.value = currentList
val currentModelList = tvModelListLiveData.value ?: mutableListOf()
currentModelList.add(TVViewModel(tv))
tvModelListLiveData.value = currentModelList
fun addTVViewModel(tvViewModel: TVViewModel) {
val currentTVModelList = tvListViewModel.value ?: mutableListOf()
currentTVModelList.add(tvViewModel)
tvListViewModel.value = currentTVModelList
}
fun updateTV(tv: TV) {
val currentList = tvListLiveData.value ?: mutableListOf()
currentList[tv.id] = tv
tvListLiveData.value = currentList
val currentModelList = tvModelListLiveData.value ?: mutableListOf()
currentModelList[tv.id].update(tv)
tvModelListLiveData.value = currentModelList
fun getTVViewModel(id: Int): TVViewModel? {
return tvListViewModel.value?.get(id)
}
fun getTV(id: Int): TV? {
return tvListLiveData.value?.get(id)
fun getTVViewModelCurrent(): TVViewModel? {
return _itemPositionCurrent.value?.let { tvListViewModel.value?.get(it) }
}
fun getTVModel(id: Int): TVViewModel? {
return tvModelListLiveData.value?.get(id)
fun setItemPosition(position: Int) {
_itemPosition.value = position
_itemPositionCurrent.value = position
}
fun setItemPositionCurrent(position: Int) {
_itemPositionCurrent.value = position
}
fun savePosition(position: Int) {
with(sharedPref!!.edit()) {
putInt("position", position)
apply()
}
}
fun size(): Int {
return tvListViewModel.value!!.size
}
}
@@ -1,5 +1,6 @@
package com.lizongying.mytv.models
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@@ -8,7 +9,7 @@ import com.lizongying.mytv.TV
class TVViewModel(private var tv: TV) : ViewModel() {
private var mapping = mapOf(
"CCTV4K" to "CCTV4K",
// "CCTV1" to "CCTV1 综合",
"CCTV1" to "CCTV1 综合",
"CCTV2" to "CCTV2 财经",
"CCTV4" to "CCTV4 中文国际",
"CCTV5" to "CCTV5 体育",
@@ -18,17 +19,17 @@ class TVViewModel(private var tv: TV) : ViewModel() {
"CCTV10" to "CCTV10 科教",
"CCTV11" to "CCTV11 戏曲",
"CCTV12" to "CCTV12 社会与法",
// "CCTV13" to "CCTV13",
"CCTV13" to "CCTV13 新闻",
"CCTV14" to "CCTV14 少儿",
"CCTV15" to "CCTV15 音乐",
"CCTV16-HD" to "CCTV16 奥林匹克",
"CCTV17" to "CCTV17 农业农村",
"CGTN" to "CGTN",
"CGTN外语纪录频道" to "CGTN 纪录频道",
"CGTN法语频道" to "CGTN 法语频道",
"CGTN俄语频道" to "CGTN 俄语频道",
"CGTN阿拉伯语频道" to "CGTN 阿拉伯语频道",
"CGTN西班牙语频道" to "CGTN 西班牙语频道",
// "CGTN外语纪录频道" to "CGTN外语纪录频道",
"东方卫视" to "东方卫视",
"湖南卫视" to "湖南卫视",
@@ -53,12 +54,31 @@ class TVViewModel(private var tv: TV) : ViewModel() {
"海南卫视" to "海南卫视",
).entries.associate { (key, value) -> value to key }
private var updateByYSP = false
private var isFirstTime = true
private var rowPosition: Int = 0
private var itemPosition: Int = 0
private val _id = MutableLiveData<Int>()
val id: LiveData<Int>
get() = _id
private val _title = MutableLiveData<String>()
val title: LiveData<String>
get() = _title
private val _videoUrl = MutableLiveData<List<String>>()
val videoUrl: LiveData<List<String>>
get() = _videoUrl
private val _videoIndex = MutableLiveData<Int>()
val videoIndex: LiveData<Int>
get() = _videoIndex
private val _logo = MutableLiveData<String>()
val logo: LiveData<String>
get() = _logo
private val _pid = MutableLiveData<String>()
val pid: LiveData<String>
get() = _pid
@@ -67,34 +87,85 @@ class TVViewModel(private var tv: TV) : ViewModel() {
val sid: LiveData<String>
get() = _sid
private val _backgroundImage = MutableLiveData<String>()
val backgroundImage: LiveData<String>
get() = _backgroundImage
private val _change = MutableLiveData<Boolean>()
val change: LiveData<Boolean>
get() = _change
// 方法用于更新背景图信息
fun updateBackgroundImage(url: String) {
_backgroundImage.value = url
private val _ready = MutableLiveData<Boolean>()
val ready: LiveData<Boolean>
get() = _ready
fun addVideoUrl(url: String) {
if (_videoUrl.value?.isNotEmpty() == true) {
if (_videoUrl.value!!.last().contains("cctv.cn")) {
tv.videoUrl = tv.videoUrl.subList(0, tv.videoUrl.lastIndex) + listOf(url)
} else {
tv.videoUrl = tv.videoUrl + listOf(url)
}
} else {
tv.videoUrl = tv.videoUrl + listOf(url)
}
tv.videoIndex = tv.videoUrl.lastIndex
_videoUrl.value = tv.videoUrl
_videoIndex.value = tv.videoIndex
}
fun updateVideoUrl(url: String) {
tv.videoUrl = listOf(url)
tv.videoIndex = 0
_videoUrl.value = listOf(url)
fun firstSource() {
if (_videoUrl.value!!.isNotEmpty()) {
setVideoIndex(0)
allReady()
} else {
Log.e(TAG, "no first")
}
}
fun updateVideoUrlByYSP(url: String) {
updateByYSP = true
updateVideoUrl(url)
fun changed() {
_change.value = true
}
fun allReady() {
_ready.value = true
}
fun setVideoIndex(videoIndex: Int) {
_videoIndex.value = videoIndex
}
fun setLogo(url: String) {
_logo.value = url
}
init {
_id.value = tv.id
_title.value = tv.title
_videoUrl.value = tv.videoUrl
_videoIndex.value = tv.videoIndex
_pid.value = tv.pid
_sid.value = tv.sid
}
fun updateByYSP(): Boolean {
return updateByYSP
fun getIsFirstTime(): Boolean {
return isFirstTime
}
fun isFirstTime(firstTime: Boolean) {
isFirstTime = firstTime
}
fun getRowPosition(): Int {
return rowPosition
}
fun getItemPosition(): Int {
return itemPosition
}
fun setRowPosition(position: Int) {
rowPosition = position
}
fun setItemPosition(position: Int) {
itemPosition = position
}
fun update(t: TV) {
@@ -106,10 +177,13 @@ class TVViewModel(private var tv: TV) : ViewModel() {
}
fun ysp(): String? {
if (tv.pid == "") {
return null
}
return mapping[tv.title]
}
fun getBackgroundImage(): String {
return tv.logo ?: ""
companion object {
private const val TAG = "TVViewModel"
}
}
+4 -1
View File
@@ -1,4 +1,7 @@
<resources>
<style name="Theme.MyTV" parent="@style/Theme.Leanback" />
<style name="Theme.MyTV" parent="@style/Theme.Leanback">
<item name="browseRowsMarginTop">40dp</item>
</style>
</resources>