diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 084c143..bbd17e6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.iptv.app" minSdk = 24 targetSdk = 34 - versionCode = 1 - versionName = "1.0" + versionCode = 2 + versionName = "1.0.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bc2eed2..7382234 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ @@ -124,8 +128,17 @@ class UpdateService(context: Context) { */ fun downloadUpdate(updateInfo: UpdateInfo, outputDir: File): Flow = flow { try { + // Construir URL de descarga con autenticación para repos privados + val downloadUrl = if (updateInfo.downloadUrl.contains("/attachments/")) { + // Para Gitea, usar API con token + "$GITEA_API_URL/repos/$REPO_OWNER/$REPO_NAME/releases/attachments/${updateInfo.fileName}" + } else { + updateInfo.downloadUrl + } + val request = Request.Builder() - .url(updateInfo.downloadUrl) + .url(downloadUrl) + .header("Authorization", "token $GITEA_TOKEN") .build() client.newCall(request).execute().use { response -> diff --git a/app/src/main/java/com/iptv/app/data/repository/ChannelRepository.kt b/app/src/main/java/com/iptv/app/data/repository/ChannelRepository.kt index 2dafb68..50610be 100644 --- a/app/src/main/java/com/iptv/app/data/repository/ChannelRepository.kt +++ b/app/src/main/java/com/iptv/app/data/repository/ChannelRepository.kt @@ -107,6 +107,38 @@ class ChannelRepository( } .flowOn(Dispatchers.Default) + /** + * Returns sports channels for testing purposes. + */ + fun getSportsChannels(): Flow> = _allChannels + .map { channels -> + channels.filter { channel -> + val category = channel.category.lowercase() + val name = channel.name.lowercase() + category.contains("sport") || + category.contains("deport") || + name.contains("espn") || + name.contains("fox sport") || + name.contains("bein") || + name.contains("sky sport") || + name.contains("bt sport") || + name.contains("eurosport") || + name.contains("tsn") || + name.contains("nba") || + name.contains("nfl") || + name.contains("mlb") || + name.contains("ufc") || + name.contains("wwe") || + name.contains("golf") || + name.contains("tennis") || + name.contains("f1") || + name.contains("formula 1") || + name.contains("racing") || + name.contains("motorsport") + } + } + .flowOn(Dispatchers.Default) + /** * Searches channels by name with debouncing. */ diff --git a/app/src/main/java/com/iptv/app/ui/components/VideoPlayer.kt b/app/src/main/java/com/iptv/app/ui/components/VideoPlayer.kt index 34bddc2..1e8e1af 100644 --- a/app/src/main/java/com/iptv/app/ui/components/VideoPlayer.kt +++ b/app/src/main/java/com/iptv/app/ui/components/VideoPlayer.kt @@ -37,6 +37,7 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView +import android.widget.FrameLayout import com.iptv.app.utils.DnsConfigurator import com.iptv.app.utils.PlayerManager @@ -200,15 +201,16 @@ private fun createExoPlayer( val trackSelector = DefaultTrackSelector(context).apply { setParameters( this.buildUponParameters() - .setMaxVideoSizeSd() .setPreferredAudioLanguage("en") + .setSelectUndeterminedTextLanguage(false) + .setPreferredTextLanguage(null) ) } val loadControl = DefaultLoadControl.Builder() .setBufferDurationsMs( - DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, - DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, + DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * 2, + DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS ) @@ -220,15 +222,16 @@ private fun createExoPlayer( .setMediaSourceFactory(DefaultMediaSourceFactory(context)) .setAudioAttributes(playerManager.getAudioAttributes(), true) .setHandleAudioBecomingNoisy(true) + .setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT) .build() .apply { - videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING + videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT repeatMode = Player.REPEAT_MODE_OFF } } /** - * Creates the PlayerView for displaying video + * Creates the PlayerView for displaying video using SurfaceView for best performance */ @OptIn(UnstableApi::class) private fun createPlayerView(context: Context, player: ExoPlayer): PlayerView { @@ -236,10 +239,13 @@ private fun createPlayerView(context: Context, player: ExoPlayer): PlayerView { this.player = player useController = false // We use custom controls resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT - layoutParams = ViewGroup.LayoutParams( + setBackgroundColor(android.graphics.Color.BLACK) + + layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) + setKeepContentOnPlayerReset(true) setShowBuffering(PlayerView.SHOW_BUFFERING_NEVER) } diff --git a/app/src/main/java/com/iptv/app/ui/screens/PlayerScreen.kt b/app/src/main/java/com/iptv/app/ui/screens/PlayerScreen.kt index 2906c22..5c01900 100644 --- a/app/src/main/java/com/iptv/app/ui/screens/PlayerScreen.kt +++ b/app/src/main/java/com/iptv/app/ui/screens/PlayerScreen.kt @@ -58,13 +58,17 @@ fun PlayerScreen( val activity = context as? Activity val lifecycleOwner = LocalLifecycleOwner.current - // ExoPlayer setup + // ExoPlayer setup with proper video configuration val exoPlayer = remember { - ExoPlayer.Builder(context).build().apply { - setMediaItem(MediaItem.fromUri(channel.streamUrl)) - prepare() - playWhenReady = true - } + ExoPlayer.Builder(context) + .setVideoScalingMode(androidx.media3.common.C.VIDEO_SCALING_MODE_SCALE_TO_FIT) + .build() + .apply { + videoScalingMode = androidx.media3.common.C.VIDEO_SCALING_MODE_SCALE_TO_FIT + setMediaItem(MediaItem.fromUri(channel.streamUrl)) + prepare() + playWhenReady = true + } } // UI state @@ -169,6 +173,8 @@ fun PlayerScreen( player = exoPlayer useController = false resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT + setBackgroundColor(android.graphics.Color.BLACK) + setKeepContentOnPlayerReset(true) } }, modifier = Modifier.fillMaxSize() diff --git a/app/src/main/java/com/iptv/app/ui/viewmodel/ChannelsViewModel.kt b/app/src/main/java/com/iptv/app/ui/viewmodel/ChannelsViewModel.kt index 71b8565..de66bb7 100644 --- a/app/src/main/java/com/iptv/app/ui/viewmodel/ChannelsViewModel.kt +++ b/app/src/main/java/com/iptv/app/ui/viewmodel/ChannelsViewModel.kt @@ -65,6 +65,37 @@ class ChannelsViewModel( setupSearchAndFilter() } + /** + * Gets sports channels for testing video playback. + */ + fun getSportsChannels(): Flow> { + return channelRepository.getSportsChannels() + } + + /** + * Shows only sports channels in the UI for testing. + */ + fun showSportsChannelsOnly() { + viewModelScope.launch { + channelRepository.getSportsChannels() + .onEach { sportsChannels -> + _uiState.update { state -> + state.copy( + channels = sportsChannels, + filteredChannels = sportsChannels, + selectedCategory = "Sports" + ) + } + } + .catch { error -> + _uiState.update { state -> + state.copy(errorMessage = "Error loading sports channels: ${error.message}") + } + } + .launchIn(viewModelScope) + } + } + /** * Loads channels from the repository. */