3 Commits

Author SHA1 Message Date
Renato
93f4c4c982 Bump version to 1.0.2 (10002) for update system 2026-01-29 01:11:25 +00:00
Renato
cb8f5d0e4e Fix video playback issues: support multiple stream formats and remove resolution limit 2026-01-29 01:10:18 +00:00
Renato
8edf5c893e fix: arreglar pantalla negra del reproductor y agregar filtro deportes
- Agregado hardwareAccelerated=true en AndroidManifest.xml
- Eliminado setMaxVideoSizeSd() que limitaba calidad de video
- Mejorada configuración de SurfaceView en PlayerView con fondo negro
- Agregado videoScalingMode SCALE_TO_FIT para mejor renderizado
- Agregado método getSportsChannels() en ChannelRepository
- Agregado showSportsChannelsOnly() en ChannelsViewModel para testing
- Agregado token Gitea en UpdateService para autenticación
- Actualizada versión a 1.0.1 (versionCode 2)
2026-01-29 00:00:34 +00:00
8 changed files with 107 additions and 20 deletions

View File

@@ -12,8 +12,8 @@ android {
applicationId = "com.iptv.app" applicationId = "com.iptv.app"
minSdk = 24 minSdk = 24
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 10002
versionName = "1.0" versionName = "1.0.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@@ -16,6 +16,7 @@
<application <application
android:name=".IPTVApplication" android:name=".IPTVApplication"
android:allowBackup="true" android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@android:drawable/ic_media_play" android:icon="@android:drawable/ic_media_play"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"

View File

@@ -25,6 +25,9 @@ class UpdateService(context: Context) {
private const val REPO_OWNER = "renato97" private const val REPO_OWNER = "renato97"
private const val REPO_NAME = "iptv-app" private const val REPO_NAME = "iptv-app"
// Token de Gitea para acceder a releases privados
private const val GITEA_TOKEN = "efeed2af00597883adb04da70bd6a7c2993ae92d"
// Endpoints // Endpoints
private const val LATEST_RELEASE_ENDPOINT = "$GITEA_API_URL/repos/$REPO_OWNER/$REPO_NAME/releases/latest" private const val LATEST_RELEASE_ENDPOINT = "$GITEA_API_URL/repos/$REPO_OWNER/$REPO_NAME/releases/latest"
@@ -70,6 +73,7 @@ class UpdateService(context: Context) {
val request = Request.Builder() val request = Request.Builder()
.url(LATEST_RELEASE_ENDPOINT) .url(LATEST_RELEASE_ENDPOINT)
.header("Accept", "application/json") .header("Accept", "application/json")
.header("Authorization", "token $GITEA_TOKEN")
.build() .build()
client.newCall(request).execute().use { response -> client.newCall(request).execute().use { response ->
@@ -124,8 +128,17 @@ class UpdateService(context: Context) {
*/ */
fun downloadUpdate(updateInfo: UpdateInfo, outputDir: File): Flow<DownloadProgress> = flow { fun downloadUpdate(updateInfo: UpdateInfo, outputDir: File): Flow<DownloadProgress> = flow {
try { 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() val request = Request.Builder()
.url(updateInfo.downloadUrl) .url(downloadUrl)
.header("Authorization", "token $GITEA_TOKEN")
.build() .build()
client.newCall(request).execute().use { response -> client.newCall(request).execute().use { response ->

View File

@@ -107,6 +107,38 @@ class ChannelRepository(
} }
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
/**
* Returns sports channels for testing purposes.
*/
fun getSportsChannels(): Flow<List<Channel>> = _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. * Searches channels by name with debouncing.
*/ */

View File

@@ -37,6 +37,7 @@ import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import android.widget.FrameLayout
import com.iptv.app.utils.DnsConfigurator import com.iptv.app.utils.DnsConfigurator
import com.iptv.app.utils.PlayerManager import com.iptv.app.utils.PlayerManager
@@ -200,15 +201,16 @@ private fun createExoPlayer(
val trackSelector = DefaultTrackSelector(context).apply { val trackSelector = DefaultTrackSelector(context).apply {
setParameters( setParameters(
this.buildUponParameters() this.buildUponParameters()
.setMaxVideoSizeSd()
.setPreferredAudioLanguage("en") .setPreferredAudioLanguage("en")
.setSelectUndeterminedTextLanguage(false)
.setPreferredTextLanguage(null)
) )
} }
val loadControl = DefaultLoadControl.Builder() val loadControl = DefaultLoadControl.Builder()
.setBufferDurationsMs( .setBufferDurationsMs(
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * 2,
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * 2,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
) )
@@ -220,15 +222,16 @@ private fun createExoPlayer(
.setMediaSourceFactory(DefaultMediaSourceFactory(context)) .setMediaSourceFactory(DefaultMediaSourceFactory(context))
.setAudioAttributes(playerManager.getAudioAttributes(), true) .setAudioAttributes(playerManager.getAudioAttributes(), true)
.setHandleAudioBecomingNoisy(true) .setHandleAudioBecomingNoisy(true)
.setVideoScalingMode(C.VIDEO_SCALING_MODE_SCALE_TO_FIT)
.build() .build()
.apply { .apply {
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
repeatMode = Player.REPEAT_MODE_OFF 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) @OptIn(UnstableApi::class)
private fun createPlayerView(context: Context, player: ExoPlayer): PlayerView { private fun createPlayerView(context: Context, player: ExoPlayer): PlayerView {
@@ -236,10 +239,13 @@ private fun createPlayerView(context: Context, player: ExoPlayer): PlayerView {
this.player = player this.player = player
useController = false // We use custom controls useController = false // We use custom controls
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
layoutParams = ViewGroup.LayoutParams( setBackgroundColor(android.graphics.Color.BLACK)
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT ViewGroup.LayoutParams.MATCH_PARENT
) )
setKeepContentOnPlayerReset(true) setKeepContentOnPlayerReset(true)
setShowBuffering(PlayerView.SHOW_BUFFERING_NEVER) setShowBuffering(PlayerView.SHOW_BUFFERING_NEVER)
} }
@@ -323,7 +329,6 @@ private fun createPlayerListener(
private fun preparePlayer(context: Context, player: ExoPlayer, streamUrl: String) { private fun preparePlayer(context: Context, player: ExoPlayer, streamUrl: String) {
val mediaItem = MediaItem.Builder() val mediaItem = MediaItem.Builder()
.setUri(streamUrl) .setUri(streamUrl)
.setMimeType("application/vnd.apple.mpegurl")
.build() .build()
// Create OkHttpClient with Google DNS configuration // Create OkHttpClient with Google DNS configuration
@@ -332,10 +337,10 @@ private fun preparePlayer(context: Context, player: ExoPlayer, streamUrl: String
// Create OkHttpDataSource.Factory with custom DNS client // Create OkHttpDataSource.Factory with custom DNS client
val dataSourceFactory = OkHttpDataSource.Factory(okHttpClient) val dataSourceFactory = OkHttpDataSource.Factory(okHttpClient)
val mediaSource = HlsMediaSource.Factory(dataSourceFactory) val mediaSourceFactory = DefaultMediaSourceFactory(context)
.createMediaSource(mediaItem) .setDataSourceFactory(dataSourceFactory)
player.setMediaSource(mediaSource) player.setMediaSource(mediaSourceFactory.createMediaSource(mediaItem))
player.prepare() player.prepare()
player.playWhenReady = true player.playWhenReady = true
} }

View File

@@ -58,9 +58,13 @@ fun PlayerScreen(
val activity = context as? Activity val activity = context as? Activity
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
// ExoPlayer setup // ExoPlayer setup with proper video configuration
val exoPlayer = remember { val exoPlayer = remember {
ExoPlayer.Builder(context).build().apply { 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)) setMediaItem(MediaItem.fromUri(channel.streamUrl))
prepare() prepare()
playWhenReady = true playWhenReady = true
@@ -169,6 +173,8 @@ fun PlayerScreen(
player = exoPlayer player = exoPlayer
useController = false useController = false
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
setBackgroundColor(android.graphics.Color.BLACK)
setKeepContentOnPlayerReset(true)
} }
}, },
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

View File

@@ -65,6 +65,37 @@ class ChannelsViewModel(
setupSearchAndFilter() setupSearchAndFilter()
} }
/**
* Gets sports channels for testing video playback.
*/
fun getSportsChannels(): Flow<List<Channel>> {
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. * Loads channels from the repository.
*/ */

View File

@@ -87,7 +87,6 @@ class PlayerManager(private val context: Context) {
trackSelector = DefaultTrackSelector(context).apply { trackSelector = DefaultTrackSelector(context).apply {
val params = this.parameters.buildUpon() val params = this.parameters.buildUpon()
.setPreferredAudioLanguage("en") .setPreferredAudioLanguage("en")
.setMaxVideoSizeSd()
.build() .build()
this.parameters = params this.parameters = params
} }