Compare commits

...
7 Commits
Author SHA1 Message Date
Li ZongYing a340ab73b1 change dialog 2023-12-07 11:19:22 +08:00
Li ZongYing bc5a0cad71 fix exit on control 2023-12-07 09:47:31 +08:00
Li ZongYing 709cc2ccdf fix empty string bug 2023-12-07 01:05:26 +08:00
Li ZongYing 3b01debf7f switch source 2023-12-06 23:51:17 +08:00
Li ZongYing 20ddba6ad7 clean 2023-12-06 20:07:27 +08:00
Li ZongYing c524e6c0fb change some channel source 2023-12-06 19:27:57 +08:00
Li ZongYing 2bd9841971 auto get version 2023-12-05 15:26:13 +08:00
13 changed files with 210 additions and 194 deletions
+1 -1
View File
@@ -52,5 +52,5 @@ jobs:
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ steps.sign_app.outputs.signedReleaseFile }} asset_path: ${{ steps.sign_app.outputs.signedReleaseFile }}
asset_name: my-tv.apk asset_name: my-tv-${{ github.ref_name }}.apk
asset_content_type: application/vnd.android.package-archive asset_content_type: application/vnd.android.package-archive
+3
View File
@@ -6,6 +6,9 @@
下载安装[releases](https://github.com/lizongying/my-tv/releases) 下载安装[releases](https://github.com/lizongying/my-tv/releases)
![image](./screenshots/img.png)
![image](./screenshots/img_1.png)
## 其他 ## 其他
小米电视可以使用小米电视助手进行安装 小米电视可以使用小米电视助手进行安装
+5 -8
View File
@@ -11,10 +11,8 @@ android {
applicationId "com.lizongying.mytv" applicationId "com.lizongying.mytv"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 11 versionCode VersionCode()
versionName "1.1" versionName VersionName()
versionCode getVersionCode()
versionName getVersionName()
} }
buildTypes { buildTypes {
@@ -34,7 +32,7 @@ android {
} }
} }
static def getVersionCode() { static def VersionCode() {
try { try {
def process = 'git rev-list --count HEAD'.execute() def process = 'git rev-list --count HEAD'.execute()
process.waitFor() process.waitFor()
@@ -44,18 +42,17 @@ static def getVersionCode() {
} }
} }
static def getVersionName() { static def VersionName() {
try { try {
def process = 'git describe --tags --always'.execute() def process = 'git describe --tags --always'.execute()
process.waitFor() process.waitFor()
return process.text.trim() return process.text.trim() - "v"
} catch (ignored) { } catch (ignored) {
return "1.0.0" return "1.0.0"
} }
} }
dependencies { dependencies {
implementation 'androidx.core:core-ktx:1.11.0-beta02' implementation 'androidx.core:core-ktx:1.11.0-beta02'
implementation 'androidx.leanback:leanback:1.0.0' implementation 'androidx.leanback:leanback:1.0.0'
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
@@ -13,11 +13,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
/**
* A CardPresenter is used to generate Views and bind Objects to them on demand.
* It contains an ImageCardView.
*/
class CardPresenter(private val lifecycleScope: LifecycleCoroutineScope) : Presenter() { class CardPresenter(private val lifecycleScope: LifecycleCoroutineScope) : Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
@@ -38,10 +33,9 @@ class CardPresenter(private val lifecycleScope: LifecycleCoroutineScope) : Prese
val tv = item as TV val tv = item as TV
val cardView = viewHolder.view as ImageCardView val cardView = viewHolder.view as ImageCardView
if (tv.videoUrl != null) { cardView.titleText = tv.title
cardView.titleText = tv.title cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT)
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT) cardView.tag = tv.videoUrl
cardView.tag = tv.videoUrl
// lifecycleScope.launch(Dispatchers.IO) { // lifecycleScope.launch(Dispatchers.IO) {
// val videoThumbnail = tv.videoUrl?.let { getVideoThumbnail(it) } // val videoThumbnail = tv.videoUrl?.let { getVideoThumbnail(it) }
@@ -50,7 +44,6 @@ class CardPresenter(private val lifecycleScope: LifecycleCoroutineScope) : Prese
// cardView.mainImageView.setImageBitmap(videoThumbnail) // cardView.mainImageView.setImageBitmap(videoThumbnail)
// } // }
// } // }
}
} }
override fun onUnbindViewHolder(viewHolder: ViewHolder) { override fun onUnbindViewHolder(viewHolder: ViewHolder) {
@@ -91,9 +84,7 @@ class CardPresenter(private val lifecycleScope: LifecycleCoroutineScope) : Prese
companion object { companion object {
private const val TAG = "CardPresenter" private const val TAG = "CardPresenter"
private const val CARD_WIDTH = 313 private const val CARD_WIDTH = 313
private const val CARD_HEIGHT = 176 private const val CARD_HEIGHT = 176
} }
} }
@@ -2,9 +2,8 @@ package com.lizongying.mytv
import java.io.Serializable import java.io.Serializable
data class Info( data class Info(
var rowPosition: Int = 0, var rowPosition: Int = 0,
var itemPosition: Int = 0, var itemPosition: Int = 0,
var item: TV? = null, var item: TV,
) : Serializable ) : Serializable
@@ -4,19 +4,14 @@ import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.Gravity
import android.view.KeyEvent import android.view.KeyEvent
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
/**
* Loads [MainFragment].
*/
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity() {
private val playbackFragment = PlaybackFragment() private val playbackFragment = PlaybackFragment()
@@ -48,6 +43,14 @@ class MainActivity : FragmentActivity() {
mainFragment.next() mainFragment.next()
} }
fun prevSource() {
mainFragment.prevSource()
}
fun nextSource() {
mainFragment.nextSource()
}
fun switchMainFragment() { fun switchMainFragment() {
val transaction = supportFragmentManager.beginTransaction() val transaction = supportFragmentManager.beginTransaction()
@@ -61,7 +64,7 @@ class MainActivity : FragmentActivity() {
transaction.commit() transaction.commit()
} }
fun focusMainFragment() { private fun focusMainFragment() {
mainFragment.focus() mainFragment.focus()
} }
@@ -69,7 +72,7 @@ class MainActivity : FragmentActivity() {
return mainFragment.isHidden return mainFragment.isHidden
} }
fun hideMainFragment() { private fun hideMainFragment() {
if (!mainFragment.isHidden) { if (!mainFragment.isHidden) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.hide(mainFragment) .hide(mainFragment)
@@ -100,31 +103,32 @@ class MainActivity : FragmentActivity() {
} }
KeyEvent.KEYCODE_MENU -> { KeyEvent.KEYCODE_MENU -> {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
val versionName = packageInfo.versionName
val textView = TextView(this)
textView.text =
"当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
val imageView = ImageView(this) val imageView = ImageView(this)
val drawable = ContextCompat.getDrawable(this, R.drawable.appreciate) val drawable = ContextCompat.getDrawable(this, R.drawable.appreciate)
imageView.setImageDrawable(drawable) imageView.setImageDrawable(drawable)
val parent = imageView.parent as? ViewGroup
parent?.removeView(imageView)
val linearLayout = LinearLayout(this) val linearLayout = LinearLayout(this)
linearLayout.orientation = LinearLayout.VERTICAL
linearLayout.addView(textView)
linearLayout.addView(imageView) linearLayout.addView(imageView)
val layoutParams = LinearLayout.LayoutParams( val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT LinearLayout.LayoutParams.WRAP_CONTENT
) )
layoutParams.gravity = Gravity.BOTTOM
imageView.layoutParams = layoutParams imageView.layoutParams = layoutParams
textView.layoutParams = layoutParams
val packageInfo = packageManager.getPackageInfo(packageName, 0)
val versionName = packageInfo.versionName
val builder: AlertDialog.Builder = AlertDialog.Builder(this) val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder builder
.setTitle("当前版本: $versionName, 获取最新: https://github.com/lizongying/my-tv/releases/")
.setView(linearLayout) .setView(linearLayout)
val dialog: AlertDialog = builder.create() val dialog: AlertDialog = builder.create()
@@ -150,13 +154,13 @@ class MainActivity : FragmentActivity() {
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT -> {
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
prev() prevSource()
} }
} }
KeyEvent.KEYCODE_DPAD_RIGHT -> { KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
next() nextSource()
} }
} }
} }
@@ -2,9 +2,7 @@ package com.lizongying.mytv
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.leanback.app.BrowseSupportFragment import androidx.leanback.app.BrowseSupportFragment
import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.ArrayObjectAdapter
import androidx.leanback.widget.HeaderItem import androidx.leanback.widget.HeaderItem
@@ -18,9 +16,6 @@ import androidx.leanback.widget.Row
import androidx.leanback.widget.RowPresenter import androidx.leanback.widget.RowPresenter
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
/**
* Loads a grid of cards with movies to browse.
*/
class MainFragment : BrowseSupportFragment() { class MainFragment : BrowseSupportFragment() {
var itemPosition: Int = 0 var itemPosition: Int = 0
@@ -32,24 +27,17 @@ class MainFragment : BrowseSupportFragment() {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
setupUIElements() setupUIElements()
loadRows() loadRows()
setupEventListeners() setupEventListeners()
} }
fun show() {
if (!view?.isVisible!!) {
view?.visibility = View.VISIBLE
}
}
private fun setupUIElements() { private fun setupUIElements() {
// set fastLane (or headers) background color
brandColor = ContextCompat.getColor(context!!, R.color.fastlane_background) brandColor = ContextCompat.getColor(context!!, R.color.fastlane_background)
// headersState = HEADERS_DISABLED // headersState = HEADERS_DISABLED
} }
private var count: Int = 0
private fun loadRows() { private fun loadRows() {
val list = TVList.list val list = TVList.list
val rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
@@ -58,9 +46,21 @@ class MainFragment : BrowseSupportFragment() {
var idx: Long = 0 var idx: Long = 0
for ((k, v) in list) { for ((k, v) in list) {
val listRowAdapter = ArrayObjectAdapter(cardPresenter) val listRowAdapter = ArrayObjectAdapter(cardPresenter)
for ((idx2, v1) in v.withIndex()) { var idx2 = 0
listRowAdapter.add(v1) for ((k1, v1) in v) {
list2.add(Info(idx.toInt(), idx2, v1)) val tv = TV(
count,
k1,
v1.toList()
)
list2.add(
Info(
idx.toInt(), idx2, tv
)
)
listRowAdapter.add(tv)
count++
idx2++
} }
val header = HeaderItem(idx, k) val header = HeaderItem(idx, k)
rowsAdapter.add(ListRow(header, listRowAdapter)) rowsAdapter.add(ListRow(header, listRowAdapter))
@@ -69,7 +69,7 @@ class MainFragment : BrowseSupportFragment() {
adapter = rowsAdapter adapter = rowsAdapter
(activity as? MainActivity)?.play(list.values.first()[0]) (activity as? MainActivity)?.play(list2.first().item as TV)
(activity as? MainActivity)?.switchMainFragment() (activity as? MainActivity)?.switchMainFragment()
} }
@@ -94,7 +94,7 @@ class MainFragment : BrowseSupportFragment() {
) )
// Toast.makeText( // Toast.makeText(
// activity, // activity,
// "${l.title} $selectedPosition $itemPosition", // "${l.title} ${tv.videoIndex}",
// Toast.LENGTH_SHORT // Toast.LENGTH_SHORT
// ).show() // ).show()
} }
@@ -115,7 +115,45 @@ class MainFragment : BrowseSupportFragment() {
) )
// Toast.makeText( // Toast.makeText(
// activity, // activity,
// "${l.title} $selectedPosition $itemPosition", // "${l.title} ${tv.videoIndex}",
// Toast.LENGTH_SHORT
// ).show()
}
}
fun prevSource() {
view?.post {
val item = list2[itemPosition]
val tv = item.item as TV
tv.videoIndex--
if (tv.videoIndex == -1) {
tv.videoIndex = tv.videoUrl.size - 1
}
(activity as? MainActivity)?.play(tv)
// Toast.makeText(
// activity,
// "${l.title} ${tv.videoIndex}",
// Toast.LENGTH_SHORT
// ).show()
}
}
fun nextSource() {
view?.post {
val item = list2[itemPosition]
val tv = item.item as TV
tv.videoIndex++
if (tv.videoIndex == tv.videoUrl.size) {
tv.videoIndex = 0
}
(activity as? MainActivity)?.play(tv)
// Toast.makeText(
// activity,
// "${l.title} ${tv.videoIndex}",
// Toast.LENGTH_SHORT // Toast.LENGTH_SHORT
// ).show() // ).show()
} }
@@ -33,13 +33,13 @@ class PlaybackControlGlue(
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT -> {
if ((context as? MainActivity)?.mainFragmentIsHidden() == true) { if ((context as? MainActivity)?.mainFragmentIsHidden() == true) {
(context as? MainActivity)?.prev() (context as? MainActivity)?.prevSource()
} }
} }
KeyEvent.KEYCODE_DPAD_RIGHT -> { KeyEvent.KEYCODE_DPAD_RIGHT -> {
if ((context as? MainActivity)?.mainFragmentIsHidden() == true) { if ((context as? MainActivity)?.mainFragmentIsHidden() == true) {
(context as? MainActivity)?.next() (context as? MainActivity)?.nextSource()
} }
} }
} }
@@ -10,7 +10,6 @@ import androidx.leanback.media.PlaybackTransportControlGlue
import androidx.leanback.widget.PlaybackControlsRow import androidx.leanback.widget.PlaybackControlsRow
import java.io.IOException import java.io.IOException
/** Handles video playback with media controls. */
class PlaybackFragment : VideoSupportFragment() { class PlaybackFragment : VideoSupportFragment() {
private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<MediaPlayerAdapter> private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<MediaPlayerAdapter>
@@ -28,21 +27,22 @@ class PlaybackFragment : VideoSupportFragment() {
} }
override fun showControlsOverlay(runAnimation: Boolean) { override fun showControlsOverlay(runAnimation: Boolean) {
// We will do nothing here, and thus controls will never be shown
} }
fun play(tv: TV) { fun play(tv: TV) {
if (tv.videoUrl.isNullOrBlank()) { val videoUrl = tv.videoUrl[tv.videoIndex]
if (videoUrl == "") {
Log.e(TAG, "videoUrl is empty") Log.e(TAG, "videoUrl is empty")
return return
} }
if (tv.videoUrl.equals(lastVideoUrl)) { if (videoUrl == lastVideoUrl) {
Log.e(TAG, "videoUrl is duplication") Log.e(TAG, "videoUrl is duplication")
return return
} }
lastVideoUrl = tv.videoUrl!! lastVideoUrl = videoUrl
playerAdapter?.reset() playerAdapter?.reset()
@@ -52,7 +52,7 @@ class PlaybackFragment : VideoSupportFragment() {
mTransportControlGlue.playWhenPrepared() mTransportControlGlue.playWhenPrepared()
try { try {
playerAdapter?.setDataSource(Uri.parse(tv.videoUrl)) playerAdapter?.setDataSource(Uri.parse(videoUrl))
} catch (e: IOException) { } catch (e: IOException) {
return return
} }
+4 -5
View File
@@ -2,13 +2,11 @@ package com.lizongying.mytv
import java.io.Serializable import java.io.Serializable
/**
* Movie class represents video entity with title, description, image thumbs and video url.
*/
data class TV( data class TV(
var id: Int = 0, var id: Int = 0,
var title: String? = null, var title: String,
var videoUrl: String? = null, var videoUrl: List<String>,
var videoIndex: Int = 0,
) : Serializable { ) : Serializable {
override fun toString(): String { override fun toString(): String {
@@ -16,6 +14,7 @@ data class TV(
"id=" + id + "id=" + id +
", title='" + title + '\'' + ", title='" + title + '\'' +
", videoUrl='" + videoUrl + '\'' + ", videoUrl='" + videoUrl + '\'' +
", videoIndex='" + videoIndex + '\'' +
'}' '}'
} }
+103 -118
View File
@@ -1,131 +1,116 @@
package com.lizongying.mytv package com.lizongying.mytv
object TVList { object TVList {
val list: Map<String, List<TV>> by lazy { val list: Map<String, Map<String, Set<String>>> by lazy {
setupTV() setupTV()
} }
private var count: Int = 0
private fun setupTV(): Map<String, List<TV>> { private fun setupTV(): Map<String, Map<String, Set<String>>> {
val tv = arrayOf( val tvs = """
"央视频道,CCTV1,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv1hd265/57/20191230/index.m3u8?&encrypt=", 央视频道,CCTV1 综合,http://tvpull.dxhmt.cn/tv/11481-4.m3u8
"央视频道,CCTV2,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv2hd265/55/20200407/index.m3u8?&encrypt=", 央视频道,CCTV1 综合,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv1hd265/57/20191230/index.m3u8?&encrypt=
"央视频道,CCTV3,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv3hd/3000/index.m3u8?&encrypt=", 央视频道,CCTV2 财经,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv2hd265/55/20200407/index.m3u8?&encrypt=
"央视频道,CCTV4,http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv4hd/1500/index.m3u8?&encrypt=", 央视频道,CCTV3 综艺,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv3hd/3000/index.m3u8?&encrypt=
"央视频道,CCTV4美洲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4meihd/57/index.m3u8?&encrypt=", 央视频道,CCTV4 中文国际,http://39.134.24.161/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226191/index.m3u8
"央视频道,CCTV4欧洲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4ouhd/51/index.m3u8?&encrypt=", 央视频道,CCTV4 中文国际,http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv4hd/1500/index.m3u8?&encrypt=
"央视频道,CCTV5,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv5hd265/57/20191230/index.m3u8?&encrypt=", 央视频道,CCTV4 中文国际美洲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4meihd/57/index.m3u8?&encrypt=
"央视频道,CCTV5+,http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv5plusnew/2500/index.m3u8?&encrypt=", 央视频道,CCTV4 中文国际欧洲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4ouhd/51/index.m3u8?&encrypt=
"央视频道,CCTV6,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv6hd/3000/index.m3u8?&encrypt=", 央视频道,CCTV5 体育,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv5hd265/57/20191230/index.m3u8?&encrypt=
"央视频道,CCTV7,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv7hd/51/index.m3u8?&encrypt=", 央视频道,CCTV5+ 体育赛事,http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv5plusnew/2500/index.m3u8?&encrypt=
"央视频道,CCTV8,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv8hd/3000/index.m3u8?&encrypt=", 央视频道,CCTV6 电影,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv6hd/3000/index.m3u8?&encrypt=
"央视频道,CCTV9,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv9hd/57/index.m3u8?&encrypt=", 央视频道,CCTV7 国防军事,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv7hd/51/index.m3u8?&encrypt=
"央视频道,CCTV10,http://hlsbkmgsplive.miguvideo.com/wd_r2/2018/ocn/cctv10hd/2000/index.m3u8?&encrypt=", 央视频道,CCTV8 电视剧,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv8hd/3000/index.m3u8?&encrypt=
"央视频道,CCTV11,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv11hd/57/20200103/index.m3u8?&encrypt=", 央视频道,CCTV9 记录,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv9hd/57/index.m3u8?&encrypt=
"央视频道,CCTV12,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv12hd/57/index.m3u8?&encrypt=", 央视频道,CCTV10 科教,http://hlsbkmgsplive.miguvideo.com/wd_r2/2018/ocn/cctv10hd/2000/index.m3u8?&encrypt=
"央视频道,CCTV13,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/cctv13/2000/index.m3u8?&encrypt=", 央视频道,CCTV11 戏曲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv11hd/57/20200103/index.m3u8?&encrypt=
"央视频道,CCTV14,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv14hd/3000/index.m3u8?&encrypt=", 央视频道,CCTV12 社会与法,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv12hd/57/index.m3u8?&encrypt=
"央视频道,CCTV15,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv15hd/51/index.m3u8?&encrypt=", 央视频道,CCTV13 新闻,https://live-play.cctvnews.cctv.com/cctv/merge_cctv13.m3u8
"央视频道,CCTV17,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv17hd/57/index.m3u8?&encrypt=", 央视频道,CCTV13 新闻,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/cctv13/2000/index.m3u8?&encrypt=
"央视频道,CGTN,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/cctvnews/1000/index.m3u8?&encrypt=", 央视频道,CCTV14 少儿,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv14hd/3000/index.m3u8?&encrypt=
"央视频道,CCTV2,https://iptv.luas.edu.cn/liverespath/6b13fe5368d391761312a985ace065c0ecad2f5e/877097d2fa-0-0-b7736e6941fd5cb71f45ef9397b68092/index.m3u8", 央视频道,CCTV15 音乐,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225785/index.m3u8
"央视频道,CCTV3,https://iptv.luas.edu.cn/liverespath/f76f9947c68be18d7a456e25aa59a08c5747e6a5/0df24da9ec-0-0-dca40ddadd2a051ce1a83536d9310820/index.m3u8", 央视频道,CCTV15 音乐,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv15hd/51/index.m3u8?&encrypt=
"央视频道,CCTV4,http://cyz32.livehbindex.hbcatv.cn/live/5b0dff5235e94ba8bff8731427145289.m3u8?bitrate=2300&pt=5", 央视频道,CCTV17 农业农村,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv17hd/57/index.m3u8?&encrypt=
"央视频道,CCTV6,https://iptv.luas.edu.cn/liverespath/f2f39ee2105c85c32df375728a51b5d89d3afab4/113ed89f48-0-0-85216e55861329ec31ba1437a2ff37c9/index.m3u8", 央视频道,CGTN,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/cctvnews/1000/index.m3u8?&encrypt=
"央视频道,CCTV7,https://iptv.luas.edu.cn/liverespath/f116a0a5035935a3435155998163d8eaa60554c3/4ba629f762-0-0-0a66b18805ff859ce68ab1137157079e/index.m3u8", 央视频道,CGTN 新闻频道,http://live.cgtn.com/1000/prog_index.m3u8
"央视频道,CCTV8,https://iptv.luas.edu.cn/liverespath/9e6e3b618b5dc902d992949f0c669bb674f6cde8/cae4471c68-0-0-316245c664c3311072c7279ec29672fe/index.m3u8", 央视频道,CGTN 纪录频道,https://livedoc.cgtn.com/500d/prog_index.m3u8
"央视频道,CCTV9,http://39.134.24.162/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226197/index.m3u8", 央视频道,CGTN 法语频道,https://livefr.cgtn.com/1000f/prog_index.m3u8
"央视频道,CCTV10,https://iptv.luas.edu.cn/liverespath/924e6292a0e6f7440e8380075908fed9915b3c00/780499a193-0-0-c50d57fd93cf32a5144ec8d889eb3ed9/index.m3u8", 央视频道,CGTN 俄语频道,http://liveru.cgtn.com/1000r/prog_index.m3u8
"央视频道,CCTV11,https://iptv.luas.edu.cn/liverespath/3d0aa0f1604f13f0ae90c2dc0590ca22a1bcaaa2/646e868153-0-0-a53121a1df5b93a74d1980270e652878/index.m3u8", 央视频道,CGTN 西班牙语频道,http://livees.cgtn.com/500e/prog_index.m3u8
"央视频道,CCTV12,https://iptv.luas.edu.cn/liverespath/fb84bde1de15cdb0308a7910cebc0497594ae94e/e2d8885f42-0-0-6e1f55986652ba786606a116c5cc0775/index.m3u8", 央视频道,CGTN 阿拉伯语频道,http://livear.cgtn.com/1000a/prog_index.m3u8
"央视频道,CCTV13,https://live-play.cctvnews.cctv.com/cctv/merge_cctv13.m3u8", 央视频道,CGTN 拉丁美洲频道,http://livees.cgtn.com/1000e/prog_index.m3u8
"央视频道,CCTV14,https://iptv.luas.edu.cn/liverespath/0e0973e58d4835f4b872548164462930003f77b4/0ef01acfdb-0-0-3c10e95bcde07ba14f0f93a8d831b684/index.m3u8", 央视频道,书画频道,http://211.103.180.178:8234/live_hls/hdmi.m3u8
"央视频道,CCTV15,http://cyz32.livehbindex.hbcatv.cn/live/50000020f44f4f4b8aa33925dba216df.m3u8?bitrate=2300&pt=5",
"地方频道,东方卫视,http://hlsbkmgsplive.miguvideo.com/wd_r4/dfl/dongfangwshd/3000/index.m3u8?&encrypt=",
"地方频道,内蒙古卫视,http://hlsbkmgsplive.miguvideo.com/envivo_w/2018/SD/neimeng/1000/index.m3u8?&encrypt=",
"地方频道,湖南卫视,http://hlsbkmgsplive.miguvideo.com/wd-hunanhd-2500/index.m3u8?&encrypt=",
"地方频道,湖北卫视,http://hlsbkmgsplive.miguvideo.com/wd_r2/2018/ocn/hubeiwshd/2000/index.m3u8?&encrypt=",
"地方频道,辽宁卫视,http://hlsbkmgsplive.miguvideo.com/wd_r2/2018/ocn/liaoningwshd/2000/index.m3u8?&encrypt=",
"地方频道,江苏卫视,http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/jiangsuhd/2500/index.m3u8?&encrypt=",
"地方频道,江西卫视,http://hlsbkmgsplive.miguvideo.com/migu/kailu/jxwshd/57/20190820/index.m3u8?&encrypt=",
"地方频道,西藏卫视,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/xizang/1000/index.m3u8?&encrypt=",
"地方频道,山东卫视,http://hlsbkmgsplive.miguvideo.com/wd-shandonghd-1200/index.m3u8?&encrypt=",
"地方频道,广东卫视,http://hlsbkmgsplive.miguvideo.com/ws_w/2018/gdws/gdws2000/2000/index.m3u8?&encrypt=",
"地方频道,广西卫视,http://hlsbkmgsplive.miguvideo.com/wd-guangxiwssd-600/index.m3u8?&encrypt=",
"地方频道,重庆卫视,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/chongqing/1000/index.m3u8?&encrypt=",
"地方频道,甘肃卫视,http://hlsbkmgsplive.miguvideo.com/envivo_v/2018/SD/gansu/1000/index.m3u8?&encrypt=",
"地方频道,青海卫视,http://hlsbkmgsplive.miguvideo.com/envivo_w/2018/SD/qinghai/1000/index.m3u8?&encrypt=",
"地方频道,陕西卫视,http://hlsbkmgsplive.miguvideo.com/envivo_w/2018/SD/shan3xi/1000/index.m3u8?&encrypt=",
"地方频道,河南卫视,http://hlsbkmgsplive.miguvideo.com/wd-henanwssd-600/index.m3u8?&encrypt=",
"地方频道,大湾区卫视,http://hlsbkmgsplive.miguvideo.com/wd_r3/2018/nfmedia/nfws/1000/index.m3u8?&encrypt=",
"地方频道,云南卫视,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/yunnan/1000/index.m3u8?&encrypt=",
"地方频道,贵州卫视,http://hlsbkmgsplive.miguvideo.com/wd-guizhouwssd-600/index.m3u8?&encrypt=",
"地方频道,新疆卫视,http://hlsbkmgsplive.miguvideo.com/wd-xinjiangwssd-600/index.m3u8?&encrypt=",
"地方频道,北京卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020539e48f392fbcbba5cf7f245.m3u8?bitrate=2300&pt=5",
"地方频道,重庆卫视,https://sjlivecdn9.cbg.cn/202312010019/app_2/_definst_/ls_2.stream/chunklist.m3u8",
"地方频道,天津卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020bbc84ee1afd7177a073393d7.m3u8?bitrate=2300&pt=5",
"地方频道,黑龙江卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020281749ed995f2824759051fa.m3u8?bitrate=2300&pt=5",
"地方频道,辽宁卫视,http://cyz32.livehbindex.hbcatv.cn/live/5000002019d84b0ba201007677bbf28c.m3u8?bitrate=2300&pt=5",
"地方频道,吉林卫视,http://cyz32.livehbindex.hbcatv.cn/live/500000205c15461b956f19f9b6896ad6.m3u8?bitrate=2300&pt=5",
"地方频道,河北卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020d6be4c4ba70a4e3261b14544.m3u8?bitrate=2300&pt=5",
"地方频道,河北农民,http://cyz32.livehbindex.hbcatv.cn/live/500000202fa64c9bac01e09eaf06afd8.m3u8?bitrate=2300&pt=5",
"地方频道,河南卫视,https://1gaanhqjqgezdgco.ourdvsss.com/tvcdn.stream3.hndt.com/tv/65c4a6d5017e1000b2b6ea2500000000_transios/playlist.m3u8?wsSecret=e9e7400c1a48039c4975cbfa69c33ba5&wsTime=1701372036&wsSession=eb0bfe9b75597f4505b2d9ba-170135801075815&wsIPSercert=5b0db1aecf3122e1ca8cb9bb5b4ee144&wsiphost=local&wsBindIP=1",
"地方频道,山东卫视,http://39.134.24.162/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226012/index.m3u8",
"地方频道,山西卫视,https://livehhhttps.sxrtv.com/lsdream/q8RVWgs/1000/n4sh8sV.m3u8",
"地方频道,湖北卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020273747628129718a5717b2dd.m3u8?bitrate=2300&pt=5",
"地方频道,湖南卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020c9ea4ce4bf3cfbbb7f8b8bb7.m3u8?bitrate=2300&pt=5",
"地方频道,浙江卫视,http://cyz32.livehbindex.hbcatv.cn/live/5000002045414da0bcca7fb08fd34c8a.m3u8?bitrate=2300&pt=5",
"地方频道,江苏卫视,http://cyz32.livehbindex.hbcatv.cn/live/500000201708473e9f4e1dbb0361de6b.m3u8?bitrate=2300&pt=5",
"地方频道,江西卫视,https://yun-live.jxtvcn.com.cn/live-jxtv/tv_jxtv1.m3u8?source=pc&t=170136577478931&token=6f10e030312c8c458b3ac4e9f908712c",
"地方频道,安徽卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020d0ab40578faaa8a023ce4d5c.m3u8?bitrate=2300&pt=5",
"地方频道,广东卫视,http://ci.emdoor.tech:8080/touchtv.php?pk=1182",
"地方频道,广西卫视,http://live.gxrb.com.cn/tv/gxtvlive03/index.m3u8",
"地方频道,深圳卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020289041678b7585eb32637331.m3u8?bitrate=2300&pt=5",
"地方频道,四川卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020407a4a109f5dfc6f26dbf5eb.m3u8?bitrate=2300&pt=5",
"地方频道,贵州卫视,https://9bwaz8y2.gzstv.com/live/CH01_lo.m3u8?txSecret=6fa8934c1ce61d48a6f746ec2982b887&txTime=6568D143",
"地方频道,云南卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020a2f74b03928347fb0b02768b.m3u8?bitrate=2300&pt=5",
"地方频道,东南卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020c18c4f03a2107aa78ced8fff.m3u8?bitrate=2300&pt=5",
"地方频道,海南卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020741c4046820cd89d5ecbd401.m3u8?bitrate=2300&pt=5",
"地方频道,内蒙古卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020eeba44449d4f4913a2897e4e.m3u8?bitrate=2300&pt=5",
"地方频道,陕西卫视,http://cyz32.livehbindex.hbcatv.cn/live/5000002019984c0fa29ae0d57380da3a.m3u8?bitrate=2300&pt=5",
"地方频道,甘肃卫视,http://cyz32.livehbindex.hbcatv.cn/live/500000206da44fde810a98a3d7848f82.m3u8?bitrate=2300&pt=5",
"地方频道,宁夏卫视,http://cyz32.livehbindex.hbcatv.cn/live/500000205dfa4e06bf7243e30e5182c8.m3u8?bitrate=2300&pt=5",
"地方频道,新疆卫视,http://cyz32.livehbindex.hbcatv.cn/live/500000201e484cbeb064a31201e3dd05.m3u8?bitrate=2300&pt=5",
"地方频道,青海卫视,http://stream.qhbtv.com/qhws/sd/live.m3u8",
"地方频道,安多卫视,http://stream.qhbtv.com/adws/sd/live.m3u8",
"地方频道,西藏卫视,http://cyz32.livehbindex.hbcatv.cn/live/50000020816449349b10593cfdde8311.m3u8?bitrate=2300&pt=5",
"地方频道,延边卫视,http://live.ybtvyun.com/video/s10006-44f040627ca1/index.m3u8",
"地方频道,兵团卫视,http://cyz32.livehbindex.hbcatv.cn/live/500000200a7e4caf973012bf85f33406.m3u8?bitrate=2300&pt=5",
"地方频道,香港卫视,http://zhibo.hkstv.tv/livestream/mutfysrq/playlist.m3u8",
"地方频道,河北农民,http://cyz32.livehbindex.hbcatv.cn/live/500000202fa64c9bac01e09eaf06afd8.m3u8?bitrate=2300&pt=5",
"地方频道,河北影视剧,http://cyz32.livehbindex.hbcatv.cn/live/50000020b87d48a78fc68b1f51e8a926.m3u8?bitrate=2300&pt=5",
"地方频道,河北公共,http://cyz32.livehbindex.hbcatv.cn/live/50000020f9834d34b527dec2d5ad8bb5.m3u8?bitrate=2300&pt=5",
"地方频道,河北都市,http://cyz32.livehbindex.hbcatv.cn/live/50000020072541118d32f3491df3fd31.m3u8?bitrate=2300&pt=5",
"地方频道,河北少儿科教,http://cyz32.livehbindex.hbcatv.cn/live/500000206980450095b990d63f377cea.m3u8?bitrate=2300&pt=5",
"地方频道,河北杂技,http://cyz32.livehbindex.hbcatv.cn/live/50000020344549cc94c638aab1e2cd4c.m3u8?bitrate=2300&pt=5",
"地方频道,东方影视,http://hlsbkmgsplive.miguvideo.com/wd_r4/dfl/dianshijuhd/3000/index.m3u8?&encrypt=",
)
val map: MutableMap<String, MutableList<TV>> = mutableMapOf() 地方频道,东方卫视,http://hlsbkmgsplive.miguvideo.com/wd_r4/dfl/dongfangwshd/3000/index.m3u8?&encrypt=
地方频道,内蒙古卫视,http://hlsbkmgsplive.miguvideo.com/envivo_w/2018/SD/neimeng/1000/index.m3u8?&encrypt=
地方频道,湖南卫视,http://hlsbkmgsplive.miguvideo.com/wd-hunanhd-2500/index.m3u8?&encrypt=
地方频道,湖北卫视,http://hlsbkmgsplive.miguvideo.com/wd_r2/2018/ocn/hubeiwshd/2000/index.m3u8?&encrypt=
地方频道,辽宁卫视,http://hlsbkmgsplive.miguvideo.com/wd_r2/2018/ocn/liaoningwshd/2000/index.m3u8?&encrypt=
地方频道,江苏卫视,http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/jiangsuhd/2500/index.m3u8?&encrypt=
地方频道,江西卫视,http://hlsbkmgsplive.miguvideo.com/migu/kailu/jxwshd/57/20190820/index.m3u8?&encrypt=
地方频道,西藏卫视,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/xizang/1000/index.m3u8?&encrypt=
地方频道,山东卫视,http://hlsbkmgsplive.miguvideo.com/wd-shandonghd-1200/index.m3u8?&encrypt=
地方频道,广东卫视,http://hlsbkmgsplive.miguvideo.com/ws_w/2018/gdws/gdws2000/2000/index.m3u8?&encrypt=
地方频道,广西卫视,http://hlsbkmgsplive.miguvideo.com/wd-guangxiwssd-600/index.m3u8?&encrypt=
地方频道,重庆卫视,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/chongqing/1000/index.m3u8?&encrypt=
地方频道,甘肃卫视,http://hlsbkmgsplive.miguvideo.com/envivo_v/2018/SD/gansu/1000/index.m3u8?&encrypt=
地方频道,青海卫视,http://hlsbkmgsplive.miguvideo.com/envivo_w/2018/SD/qinghai/1000/index.m3u8?&encrypt=
地方频道,陕西卫视,http://hlsbkmgsplive.miguvideo.com/envivo_w/2018/SD/shan3xi/1000/index.m3u8?&encrypt=
地方频道,河南卫视,http://hlsbkmgsplive.miguvideo.com/wd-henanwssd-600/index.m3u8?&encrypt=
地方频道,大湾区卫视,http://hlsbkmgsplive.miguvideo.com/wd_r3/2018/nfmedia/nfws/1000/index.m3u8?&encrypt=
地方频道,云南卫视,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/yunnan/1000/index.m3u8?&encrypt=
地方频道,贵州卫视,http://hlsbkmgsplive.miguvideo.com/wd-guizhouwssd-600/index.m3u8?&encrypt=
地方频道,新疆卫视,http://hlsbkmgsplive.miguvideo.com/wd-xinjiangwssd-600/index.m3u8?&encrypt=
地方频道,北京卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221225728/index.m3u8
地方频道,重庆卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226409/index.m3u8
地方频道,天津卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221225740/index.m3u8
地方频道,黑龙江卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226327/index.m3u8
地方频道,辽宁卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226546/index.m3u8
地方频道,吉林卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226397/index.m3u8
地方频道,河北卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226406/index.m3u8
地方频道,河南卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226480/index.m3u8
地方频道,山东卫视,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226209/index.m3u8
地方频道,山西卫视,http://39.134.24.161/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225763/index.m3u8
地方频道,湖北卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226477/index.m3u8
地方频道,湖南卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226307/index.m3u8
地方频道,浙江卫视,http://hw-m-l.cztv.com/channels/lantian/channel01/1080p.m3u8
地方频道,江苏卫视,http://39.134.24.166/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226200/index.m3u8
地方频道,江西卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226344/index.m3u8
地方频道,安徽卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226391/index.m3u8
地方频道,广东卫视,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226216/index.m3u8
地方频道,广西卫视,http://live.gxrb.com.cn/tv/gxtvlive03/index.m3u8
地方频道,深圳卫视,http://39.134.24.166/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226205/index.m3u8
地方频道,四川卫视,http://39.134.24.166/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225768/index.m3u8
地方频道,贵州卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226474/index.m3u8
地方频道,云南卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226444/index.m3u8
地方频道,东南卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226341/index.m3u8
地方频道,海南卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226465/index.m3u8
地方频道,内蒙古卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226389/index.m3u8
地方频道,陕西卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226457/index.m3u8
地方频道,甘肃卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221225633/index.m3u8
地方频道,宁夏卫视,https://hls.ningxiahuangheyun.com/tv/nxws.m3u8
地方频道,新疆卫视,http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226460/index.m3u8
地方频道,青海卫视,http://stream.qhbtv.com/qhws/sd/live.m3u8
地方频道,安多卫视,http://stream.qhbtv.com/adws/sd/live.m3u8
地方频道,西藏卫视,http://39.134.24.161/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226212/index.m3u8
地方频道,延边卫视,http://live.ybtvyun.com/video/s10006-44f040627ca1/index.m3u8
""".trimIndent()
for (i in tv) { val map: MutableMap<String, MutableMap<String, LinkedHashSet<String>>> = mutableMapOf()
val (channel, title, videoUrl) = i.split(",")
val videoList = map[channel] ?: mutableListOf() for (i in tvs.split("\n")) {
videoList.add(buildTV(title, videoUrl)) if (!i.contains(",")) {
map[channel] = videoList continue
}
val (channel, title, url) = i.split(",")
val titleMap = map[channel] ?: mutableMapOf()
val urlSet = titleMap[title] ?: LinkedHashSet()
urlSet.add(url)
titleMap[title] = urlSet
map[channel] = titleMap
} }
return map return map
} }
private fun buildTV(
title: String,
videoUrl: String,
): TV {
val tv = TV()
tv.id = count++
tv.title = title
tv.videoUrl = videoUrl
return tv
}
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB