diff --git a/.idea/.name b/.idea/.name index d0fb625..1f596d0 100644 --- a/.idea/.name +++ b/.idea/.name @@ -1 +1 @@ -My TV \ No newline at end of file +DuckTV \ No newline at end of file diff --git a/app/src/main/java/me/lsong/mytv/ui/MainContent.kt b/app/src/main/java/me/lsong/mytv/ui/MainContent.kt index d55076c..ab22ea8 100644 --- a/app/src/main/java/me/lsong/mytv/ui/MainContent.kt +++ b/app/src/main/java/me/lsong/mytv/ui/MainContent.kt @@ -3,6 +3,7 @@ package me.lsong.mytv.ui import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -38,19 +39,7 @@ fun LeanbackMainContent( groupList: TVGroupList = TVGroupList(), settingsViewModel: MyTvSettingsViewModel = viewModel(), ) { - val configuration = LocalConfiguration.current - val videoPlayerState = rememberLeanbackVideoPlayerState( - defaultAspectRatioProvider = { - when (settingsViewModel.videoPlayerAspectRatio) { - Settings.VideoPlayerAspectRatio.ORIGINAL -> null - Settings.VideoPlayerAspectRatio.SIXTEEN_NINE -> 16f / 9f - Settings.VideoPlayerAspectRatio.FOUR_THREE -> 4f / 3f - Settings.VideoPlayerAspectRatio.AUTO -> { - configuration.screenHeightDp.toFloat() / configuration.screenWidthDp.toFloat() - } - } - } - ) + val videoPlayerState = rememberLeanbackVideoPlayerState() val mainContentState = rememberMainContentState( videoPlayerState = videoPlayerState, tvGroupList = groupList, @@ -84,8 +73,10 @@ fun LeanbackMainContent( ) { MyTvVideoScreen( state = videoPlayerState, + aspectRatioProvider = { settingsViewModel.videoPlayerAspectRatio }, showMetadataProvider = { settingsViewModel.debugShowVideoPlayerMetadata }, modifier = Modifier + .fillMaxSize() .focusRequester(focusRequester) .focusable() .handleLeanbackKeyEvents( diff --git a/app/src/main/java/me/lsong/mytv/ui/player/Media3VideoPlayer.kt b/app/src/main/java/me/lsong/mytv/ui/player/Media3VideoPlayer.kt index 7bdfe19..aa7df67 100644 --- a/app/src/main/java/me/lsong/mytv/ui/player/Media3VideoPlayer.kt +++ b/app/src/main/java/me/lsong/mytv/ui/player/Media3VideoPlayer.kt @@ -56,7 +56,6 @@ class LeanbackMedia3VideoPlayer( }) val mediaItem = MediaItem.fromUri(uri) - val mediaSource = when (val type = contentType ?: Util.inferContentType(uri)) { C.CONTENT_TYPE_HLS -> { HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem) diff --git a/app/src/main/java/me/lsong/mytv/ui/player/VideoScreen.kt b/app/src/main/java/me/lsong/mytv/ui/player/VideoScreen.kt index de9d29b..3c2396b 100644 --- a/app/src/main/java/me/lsong/mytv/ui/player/VideoScreen.kt +++ b/app/src/main/java/me/lsong/mytv/ui/player/VideoScreen.kt @@ -5,18 +5,23 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import me.lsong.mytv.rememberLeanbackChildPadding import me.lsong.mytv.ui.components.LeanbackVideoPlayerMetadata +import me.lsong.mytv.utils.Settings @Composable fun MyTvVideoScreen( modifier: Modifier = Modifier, state: LeanbackVideoPlayerState = rememberLeanbackVideoPlayerState(), + aspectRatioProvider: () -> Settings.VideoPlayerAspectRatio, showMetadataProvider: () -> Boolean = { false }, ) { val context = LocalContext.current @@ -24,22 +29,26 @@ fun MyTvVideoScreen( Box(modifier = modifier.fillMaxSize()) { AndroidView( - modifier = Modifier - .align(Alignment.Center) - .aspectRatio(state.aspectRatio), - factory = { - // PlayerView 切换视频时黑屏闪烁,使用 SurfaceView 代替 - SurfaceView(context) - }, - update = { surfaceView -> - state.setVideoSurfaceView(surfaceView) - }, + modifier = when (aspectRatioProvider()) { + Settings.VideoPlayerAspectRatio.ORIGINAL -> Modifier + Settings.VideoPlayerAspectRatio.ASPECT_16_9 -> Modifier.aspectRatio(16f / 9f) + Settings.VideoPlayerAspectRatio.ASPECT_4_3 -> Modifier.aspectRatio( 4f / 3f) + Settings.VideoPlayerAspectRatio.FULL_SCREEN -> { + val configuration = LocalConfiguration.current + Modifier.aspectRatio(configuration.screenWidthDp.toFloat() / configuration.screenHeightDp.toFloat()) + } + + }.fillMaxSize().align(Alignment.Center), + factory = { SurfaceView(context) }, + update = { surfaceView -> state.setVideoSurfaceView(surfaceView) }, ) LeanbackVideoPlayerErrorScreen( errorProvider = { state.error }, ) + // Text(text = "$aspectRatio") + if (showMetadataProvider()) { LeanbackVideoPlayerMetadata( modifier = Modifier.padding(start = childPadding.start, top = childPadding.top), diff --git a/app/src/main/java/me/lsong/mytv/ui/settings/components/SettingsCategoryAbout.kt b/app/src/main/java/me/lsong/mytv/ui/settings/components/SettingsCategoryAbout.kt index 907e646..a2fca0e 100644 --- a/app/src/main/java/me/lsong/mytv/ui/settings/components/SettingsCategoryAbout.kt +++ b/app/src/main/java/me/lsong/mytv/ui/settings/components/SettingsCategoryAbout.kt @@ -4,7 +4,9 @@ import android.content.Context import android.content.pm.PackageInfo import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -13,9 +15,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.tv.foundation.lazy.list.TvLazyColumn +import me.lsong.mytv.ui.components.LeanbackQrcode import me.lsong.mytv.ui.settings.MyTvSettingsViewModel import me.lsong.mytv.ui.theme.LeanbackTheme import me.lsong.mytv.utils.Constants +import me.lsong.mytv.utils.HttpServer @Composable fun LeanbackSettingsCategoryAbout( @@ -39,19 +43,6 @@ fun LeanbackSettingsCategoryAbout( ) } - item { - LeanbackSettingsCategoryListItem( - headlineContent = "显示FPS", - supportingContent = "在屏幕左上角显示fps和柱状图", - trailingContent = { - Switch(checked = settingsViewModel.debugShowFps, onCheckedChange = null) - }, - onSelected = { - settingsViewModel.debugShowFps = !settingsViewModel.debugShowFps - }, - ) - } - item { LeanbackSettingsCategoryListItem( headlineContent = "显示播放器信息", @@ -68,6 +59,33 @@ fun LeanbackSettingsCategoryAbout( }, ) } + item { + LeanbackSettingsCategoryListItem( + headlineContent = "显示FPS", + supportingContent = "在屏幕左上角显示fps和柱状图", + trailingContent = { + Switch(checked = settingsViewModel.debugShowFps, onCheckedChange = null) + }, + onSelected = { + settingsViewModel.debugShowFps = !settingsViewModel.debugShowFps + }, + ) + } + + item{ + LeanbackSettingsCategoryListItem( + headlineContent = "扫码进入设置页面", + supportingContent = HttpServer.serverUrl, + trailingContent = { LeanbackQrcode( + text = HttpServer.serverUrl, + modifier = Modifier + .width(80.dp) + .height(80.dp), + ) + } + ) + + } } } } diff --git a/app/src/main/java/me/lsong/mytv/ui/settings/components/SettingsCategoryApp.kt b/app/src/main/java/me/lsong/mytv/ui/settings/components/SettingsCategoryApp.kt index f4475ff..dbbffbd 100644 --- a/app/src/main/java/me/lsong/mytv/ui/settings/components/SettingsCategoryApp.kt +++ b/app/src/main/java/me/lsong/mytv/ui/settings/components/SettingsCategoryApp.kt @@ -33,95 +33,14 @@ fun LeanbackSettingsCategoryApp( verticalArrangement = Arrangement.spacedBy(10.dp), ) { - item { - LeanbackSettingsCategoryListItem( - headlineContent = "开机自启", - supportingContent = "请确保当前设备支持该功能", - trailingContent = { - Switch(checked = settingsViewModel.appBootLaunch, onCheckedChange = null) - }, - onSelected = { - settingsViewModel.appBootLaunch = !settingsViewModel.appBootLaunch - }, - ) - } - - item { - val defaultScale = 1f - val minScale = 1f - val maxScale = 2f - val stepScale = 0.1f - - LeanbackSettingsCategoryListItem( - headlineContent = "界面整体缩放比例", - supportingContent = "短按切换缩放比例,长按恢复默认;部分界面受影响", - trailingContent = "×${DecimalFormat("#.#").format(settingsViewModel.uiDensityScaleRatio)}", - onSelected = { - if (settingsViewModel.uiDensityScaleRatio >= maxScale) { - settingsViewModel.uiDensityScaleRatio = minScale - } else { - settingsViewModel.uiDensityScaleRatio = - (settingsViewModel.uiDensityScaleRatio + stepScale).coerceIn( - minScale, maxScale - ) - } - }, - onLongSelected = { - settingsViewModel.uiDensityScaleRatio = defaultScale - }, - ) - } - - item { - val defaultScale = 1f - val minScale = 1f - val maxScale = 2f - val stepScale = 0.1f - - LeanbackSettingsCategoryListItem( - headlineContent = "界面字体缩放比例", - supportingContent = "短按切换缩放比例,长按恢复默认;部分界面受影响", - trailingContent = "×${DecimalFormat("#.#").format(settingsViewModel.uiFontScaleRatio)}", - onSelected = { - if (settingsViewModel.uiFontScaleRatio >= maxScale) { - settingsViewModel.uiFontScaleRatio = minScale - } else { - settingsViewModel.uiFontScaleRatio = - (settingsViewModel.uiFontScaleRatio + stepScale).coerceIn( - minScale, maxScale - ) - } - }, - onLongSelected = { - settingsViewModel.uiFontScaleRatio = defaultScale - }, - ) - } - - item { - LeanbackSettingsCategoryListItem( - headlineContent = "HTTP请求重试次数", - supportingContent = "影响直播源、节目单数据获取", - trailingContent = Constants.HTTP_RETRY_COUNT.toString(), - ) - } - - item { - LeanbackSettingsCategoryListItem( - headlineContent = "HTTP请求重试间隔时间", - supportingContent = "影响直播源、节目单数据获取", - trailingContent = Constants.HTTP_RETRY_INTERVAL.humanizeMs(), - ) - } - item { LeanbackSettingsCategoryListItem( headlineContent = "全局画面比例", trailingContent = when (settingsViewModel.videoPlayerAspectRatio) { Settings.VideoPlayerAspectRatio.ORIGINAL -> "原始" - Settings.VideoPlayerAspectRatio.SIXTEEN_NINE -> "16:9" - Settings.VideoPlayerAspectRatio.FOUR_THREE -> "4:3" - Settings.VideoPlayerAspectRatio.AUTO -> "自动拉伸" + Settings.VideoPlayerAspectRatio.ASPECT_16_9 -> "16:9" + Settings.VideoPlayerAspectRatio.ASPECT_4_3 -> "4:3" + Settings.VideoPlayerAspectRatio.FULL_SCREEN -> "自动拉伸" }, onSelected = { settingsViewModel.videoPlayerAspectRatio = @@ -132,22 +51,101 @@ fun LeanbackSettingsCategoryApp( ) } - - item { - val min = 1000 * 5L - val max = 1000 * 30L - val step = 1000 * 5L - - LeanbackSettingsCategoryListItem( - headlineContent = "播放器加载超时", - supportingContent = "影响超时换源、断线重连", - trailingContent = settingsViewModel.videoPlayerLoadTimeout.humanizeMs(), - onSelected = { - settingsViewModel.videoPlayerLoadTimeout = - max(min, (settingsViewModel.videoPlayerLoadTimeout + step) % (max + step)) - }, - ) - } + // item { + // LeanbackSettingsCategoryListItem( + // headlineContent = "开机自启", + // supportingContent = "请确保当前设备支持该功能", + // trailingContent = { + // Switch(checked = settingsViewModel.appBootLaunch, onCheckedChange = null) + // }, + // onSelected = { + // settingsViewModel.appBootLaunch = !settingsViewModel.appBootLaunch + // }, + // ) + // } + + // item { + // val defaultScale = 1f + // val minScale = 1f + // val maxScale = 2f + // val stepScale = 0.1f + // + // LeanbackSettingsCategoryListItem( + // headlineContent = "界面整体缩放比例", + // supportingContent = "短按切换缩放比例,长按恢复默认;部分界面受影响", + // trailingContent = "×${DecimalFormat("#.#").format(settingsViewModel.uiDensityScaleRatio)}", + // onSelected = { + // if (settingsViewModel.uiDensityScaleRatio >= maxScale) { + // settingsViewModel.uiDensityScaleRatio = minScale + // } else { + // settingsViewModel.uiDensityScaleRatio = + // (settingsViewModel.uiDensityScaleRatio + stepScale).coerceIn( + // minScale, maxScale + // ) + // } + // }, + // onLongSelected = { + // settingsViewModel.uiDensityScaleRatio = defaultScale + // }, + // ) + // } + // + // item { + // val defaultScale = 1f + // val minScale = 1f + // val maxScale = 2f + // val stepScale = 0.1f + // + // LeanbackSettingsCategoryListItem( + // headlineContent = "界面字体缩放比例", + // supportingContent = "短按切换缩放比例,长按恢复默认;部分界面受影响", + // trailingContent = "×${DecimalFormat("#.#").format(settingsViewModel.uiFontScaleRatio)}", + // onSelected = { + // if (settingsViewModel.uiFontScaleRatio >= maxScale) { + // settingsViewModel.uiFontScaleRatio = minScale + // } else { + // settingsViewModel.uiFontScaleRatio = + // (settingsViewModel.uiFontScaleRatio + stepScale).coerceIn( + // minScale, maxScale + // ) + // } + // }, + // onLongSelected = { + // settingsViewModel.uiFontScaleRatio = defaultScale + // }, + // ) + // } + // + // item { + // LeanbackSettingsCategoryListItem( + // headlineContent = "HTTP请求重试次数", + // supportingContent = "影响直播源、节目单数据获取", + // trailingContent = Constants.HTTP_RETRY_COUNT.toString(), + // ) + // } + // + // item { + // LeanbackSettingsCategoryListItem( + // headlineContent = "HTTP请求重试间隔时间", + // supportingContent = "影响直播源、节目单数据获取", + // trailingContent = Constants.HTTP_RETRY_INTERVAL.humanizeMs(), + // ) + // } + // item { + // val min = 1000 * 5L + // val max = 1000 * 30L + // val step = 1000 * 5L + // + // LeanbackSettingsCategoryListItem( + // headlineContent = "播放器加载超时", + // supportingContent = "影响超时换源、断线重连", + // trailingContent = settingsViewModel.videoPlayerLoadTimeout.humanizeMs(), + // onSelected = { + // settingsViewModel.videoPlayerLoadTimeout = + // max(min, (settingsViewModel.videoPlayerLoadTimeout + step) % (max + step)) + // }, + // ) + // } item { LeanbackSettingsCategoryListItem( diff --git a/app/src/main/java/me/lsong/mytv/utils/HttpServer.kt b/app/src/main/java/me/lsong/mytv/utils/HttpServer.kt index eab95d6..7a8f270 100644 --- a/app/src/main/java/me/lsong/mytv/utils/HttpServer.kt +++ b/app/src/main/java/me/lsong/mytv/utils/HttpServer.kt @@ -15,6 +15,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import me.lsong.mytv.R +import org.json.JSONObject import java.net.Inet4Address import java.net.NetworkInterface import java.net.SocketException @@ -25,7 +26,6 @@ object HttpServer { val serverUrl: String by lazy { "http://${getLocalIpAddress()}:$SERVER_PORT" } - fun start(context: Context, showToast: (String) -> Unit) { CoroutineScope(Dispatchers.IO).launch { try { @@ -84,11 +84,8 @@ object HttpServer { Json.encodeToString( AllSettings( appTitle = Constants.APP_NAME, - // epgUrls = SP.epgUrls, - // iptvSourceUrls = SP.iptvSourceUrls, - epgUrls = emptySet(), - iptvSourceUrls = emptySet(), - videoPlayerUserAgent = Settings.videoPlayerUserAgent, + epgUrls = Settings.epgUrls, + iptvSourceUrls = Settings.iptvSourceUrls, ) ) ) @@ -100,34 +97,38 @@ object HttpServer { response: AsyncHttpServerResponse, ) { val body = request.getBody().get() - val iptvSourceUrl = body.get("iptvSourceUrl").toString() - val epgXmlUrl = body.get("epgXmlUrl").toString() - val videoPlayerUserAgent = body.get("videoPlayerUserAgent").toString() + try { + val jsonObject = JSONObject(body.toString()) + val epgUrls = jsonObject.getJSONArray("epgUrls").let { array -> + (0 until array.length()).map { array.getString(it) }.toSet() + } + val iptvSourceUrls = jsonObject.getJSONArray("iptvSourceUrls").let { array -> + (0 until array.length()).map { array.getString(it) }.toSet() + } - // if (SP.iptvSourceUrls != iptvSourceUrl) { - // SP.iptvSourceUrl = iptvSourceUrl - // IptvRepository().clearCache() - // } - // - // if (SP.epgXmlUrl != epgXmlUrl) { - // SP.epgXmlUrl = epgXmlUrl - // EpgRepository().clearCache() - // } - // - // SP.videoPlayerUserAgent = videoPlayerUserAgent + // 保存设置 + Settings.epgUrls = epgUrls + Settings.iptvSourceUrls = iptvSourceUrls - wrapResponse(response).send("success") + // 显示提示信息 + CoroutineScope(Dispatchers.Main).launch { + showToast("设置已保存") + } + + wrapResponse(response).send("设置已成功保存") + } catch (e: Exception) { + wrapResponse(response).code(400).send("无效的JSON格式: ${e.message}") + } } private fun getLocalIpAddress(): String { val defaultIp = "0.0.0.0" - try { - val en = NetworkInterface.getNetworkInterfaces() - while (en.hasMoreElements()) { - val intf = en.nextElement() - val enumIpAddr = intf.inetAddresses - while (enumIpAddr.hasMoreElements()) { - val inetAddress = enumIpAddr.nextElement() + val interfaces = NetworkInterface.getNetworkInterfaces() + while (interfaces.hasMoreElements()) { + val iface = interfaces.nextElement() + val addr = iface.inetAddresses + while (addr.hasMoreElements()) { + val inetAddress = addr.nextElement() if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) { return inetAddress.hostAddress ?: defaultIp } @@ -146,5 +147,4 @@ private data class AllSettings( val appTitle: String, val epgUrls: Set, val iptvSourceUrls: Set, - val videoPlayerUserAgent: String, ) \ No newline at end of file diff --git a/app/src/main/java/me/lsong/mytv/utils/Settings.kt b/app/src/main/java/me/lsong/mytv/utils/Settings.kt index 11b916d..59288c3 100644 --- a/app/src/main/java/me/lsong/mytv/utils/Settings.kt +++ b/app/src/main/java/me/lsong/mytv/utils/Settings.kt @@ -2,6 +2,8 @@ package me.lsong.mytv.utils import android.content.Context import android.content.SharedPreferences +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalConfiguration /** * 应用配置存储 @@ -240,13 +242,13 @@ object Settings { ORIGINAL(0), /** 16:9 */ - SIXTEEN_NINE(1), + ASPECT_16_9(1), /** 4:3 */ - FOUR_THREE(2), + ASPECT_4_3(2), - /** 自动拉伸 */ - AUTO(3); + /** full screen */ + FULL_SCREEN(3); companion object { fun fromValue(value: Int): VideoPlayerAspectRatio { diff --git a/app/src/main/res/raw/index.html b/app/src/main/res/raw/index.html index 83db25f..310d06a 100644 --- a/app/src/main/res/raw/index.html +++ b/app/src/main/res/raw/index.html @@ -1,47 +1,70 @@ - - + + - - + DuckTV +

DuckTV

+
+

EPG URLs

+ + +

IPTV Source URLs

+ + + +
+ + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 8f3511c..2ebfd3b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,6 +19,6 @@ dependencyResolutionManagement { } } -rootProject.name = "My TV" +rootProject.name = "DuckTV" include(":app") \ No newline at end of file