fix: single stream playback and history saving issues

This commit is contained in:
tunbmoon
2026-02-13 14:27:36 +08:00
parent 19fa6b6108
commit 165fce4cec
5 changed files with 54 additions and 17 deletions
@@ -27,7 +27,6 @@ object UrlInterceptor {
throw NetworkBlockedException("HTTP $httpCode: Network blocked or restricted")
}
val finalUrl = response.request.url.toString()
val content = response.body.string()
if (!content.trim().startsWith("#EXTM3U")) {
@@ -35,14 +34,14 @@ object UrlInterceptor {
if (contentLower.startsWith("<!doctype") || contentLower.startsWith("<html")) {
throw NetworkBlockedException("Network blocked or restricted (HTML response)")
}
return@withContext finalUrl
return@withContext currentUrl
}
val isMasterPlaylist = content.contains("#EXT-X-STREAM-INF")
val isMediaPlaylist = content.contains("#EXTINF")
if (isMediaPlaylist) {
return@withContext finalUrl
return@withContext currentUrl
}
if (isMasterPlaylist) {
@@ -52,7 +51,7 @@ object UrlInterceptor {
}
if (variantUrl != null) {
val uri = finalUrl.toUri()
val uri = currentUrl.toUri()
val baseUrl = "${uri.scheme}://${uri.host}${if (uri.port == -1) "" else ":${uri.port}"}${uri.path?.substringBeforeLast("/") ?: ""}"
currentUrl = if (variantUrl.startsWith("http")) {
@@ -66,7 +65,7 @@ object UrlInterceptor {
}
}
return@withContext finalUrl
return@withContext currentUrl
}
url
@@ -247,6 +247,7 @@ fun ChannelItem(
Card(
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 96.dp)
.clickable(onClick = onClick),
colors = CardDefaults.cardColors(
containerColor = if (isPlaying)
@@ -258,6 +259,7 @@ fun ChannelItem(
Row(
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 96.dp)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
@@ -380,6 +380,7 @@ private fun PlayerPageContent(
}
}
} else {
viewModel.saveHistory()
onBack()
}
}
@@ -82,17 +82,46 @@ object M3U8Parser {
}
}
private data class FetchResult(
val channels: List<M3U8Channel>,
val finalUrl: String
)
suspend fun parseM3U8FromUrl(url: String): Result<List<M3U8Channel>> = withContext(Dispatchers.IO) {
val httpsUrl = NetworkUtils.convertToHttps(url)
try {
val channels = fetchM3U8Channels(httpsUrl)
Result.Success(channels)
val result = fetchM3U8Channels(httpsUrl)
if (result.channels.isEmpty()) {
val singleChannel = listOf(
M3U8Channel(
id = "${result.finalUrl.hashCode()}_0",
name = result.finalUrl.substringAfterLast("/").substringBeforeLast(".").ifEmpty {
result.finalUrl.substringAfterLast("/").substringBefore("?").ifEmpty { "Stream" }
},
url = result.finalUrl
)
)
return@withContext Result.Success(singleChannel)
}
Result.Success(result.channels)
} catch (e: Exception) {
if (httpsUrl != url) {
try {
val channels = fetchM3U8Channels(url)
Result.Success(channels)
val result = fetchM3U8Channels(url)
if (result.channels.isEmpty()) {
val singleChannel = listOf(
M3U8Channel(
id = "${result.finalUrl.hashCode()}_0",
name = result.finalUrl.substringAfterLast("/").substringBeforeLast(".").ifEmpty {
result.finalUrl.substringAfterLast("/").substringBefore("?").ifEmpty { "Stream" }
},
url = result.finalUrl
)
)
return@withContext Result.Success(singleChannel)
}
Result.Success(result.channels)
} catch (fallbackException: Exception) {
Result.Error(fallbackException, "Failed to fetch M3U8 from URL: ${fallbackException.message}")
}
@@ -102,7 +131,7 @@ object M3U8Parser {
}
}
private fun fetchM3U8Channels(url: String): List<M3U8Channel> {
private fun fetchM3U8Channels(url: String): FetchResult {
val request = Request.Builder().url(url).build()
val response = client.newCall(request).execute()
@@ -110,6 +139,8 @@ object M3U8Parser {
throw Exception("HTTP ${response.code}")
}
val finalUrl = response.request.url.toString()
val contentLength = response.body.contentLength()
if (contentLength > PlayerConstants.M3U8_MAX_SIZE_BYTES) {
response.close()
@@ -173,7 +204,7 @@ object M3U8Parser {
}
}
return channels
return FetchResult(channels, finalUrl)
}
private fun extractChannelName(line: String): String {
@@ -107,7 +107,6 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
localPlayer?.stop()
viewModelScope.launch {
android.util.Log.d("PlayerViewModel", "loadChannels called: url=$url")
_uiState.value = _uiState.value.copy(
channels = emptyList(),
isLoadingChannels = true,
@@ -133,7 +132,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
url.startsWith("rtmp") || url.startsWith("rtsp") -> {
repository.parseM3U8FromUrl(url).onSuccess { channels ->
parsedChannels = channels
isM3U8Playlist = channels.size > 1
isM3U8Playlist = channels.size >= 1
}.onError { _, _ ->
}
}
@@ -145,7 +144,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
if (content.contains("#EXTM3U")) {
repository.parseM3U8FromContent(content).onSuccess { channels ->
parsedChannels = channels
isM3U8Playlist = channels.size > 1
isM3U8Playlist = channels.size >= 1
}
}
}
@@ -155,7 +154,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
url.contains("#EXTM3U") -> {
repository.parseM3U8FromContent(url).onSuccess { channels ->
parsedChannels = channels
isM3U8Playlist = channels.size > 1
isM3U8Playlist = channels.size >= 1
}
}
}
@@ -360,10 +359,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
fun saveHistory() {
val state = _uiState.value
if (state.playlistUrl.isNotEmpty() && state.videoTitle.isNotEmpty()) {
val watchTime = watchTimeTracker.getAccumulatedTime()
if (watchTime == 0L) {
return
}
val urlToSave = state.playlistUrl.ifEmpty { state.currentChannelUrl }
if (urlToSave.isNotEmpty() && state.videoTitle.isNotEmpty()) {
HistoryManager.addOrUpdateHistory(
getApplication(),
state.playlistUrl,
urlToSave,
state.videoTitle,
state.currentChannelUrl,
state.currentChannelId