package com.streamplayer; import android.content.Intent; import android.os.Bundle; import android.os.StrictMode; import android.view.View; import android.view.WindowManager; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.datasource.okhttp.OkHttpDataSource; import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.hls.HlsMediaSource; import androidx.media3.ui.PlayerView; import androidx.media3.common.util.Util; import androidx.annotation.OptIn; import androidx.media3.common.util.UnstableApi; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.dnsoverhttps.DnsOverHttps; @OptIn(markerClass = UnstableApi.class) public class PlayerActivity extends AppCompatActivity { public static final String EXTRA_CHANNEL_NAME = "extra_channel_name"; public static final String EXTRA_CHANNEL_URL = "extra_channel_url"; private PlayerView playerView; private ProgressBar loadingIndicator; private TextView errorMessage; private TextView channelLabel; private Button closeButton; private View playerToolbar; private ExoPlayer player; private DefaultTrackSelector trackSelector; private String channelName; private String channelUrl; private boolean overlayVisible = true; private OkHttpClient okHttpClient; private int retryCount = 0; private static final int MAX_RETRIES = 3; private String lastStreamUrl; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); StrictMode.setThreadPolicy( new StrictMode.ThreadPolicy.Builder().permitAll().build() ); setContentView(R.layout.activity_player); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); Intent intent = getIntent(); if (intent == null) { finish(); return; } channelName = intent.getStringExtra(EXTRA_CHANNEL_NAME); channelUrl = intent.getStringExtra(EXTRA_CHANNEL_URL); if (channelName == null || channelUrl == null) { finish(); return; } initViews(); channelLabel.setText(channelName); // DNS configurado en StreamUrlResolver loadChannel(); } private void initViews() { playerView = findViewById(R.id.player_view); loadingIndicator = findViewById(R.id.loading_indicator); errorMessage = findViewById(R.id.error_message); channelLabel = findViewById(R.id.player_channel_label); closeButton = findViewById(R.id.close_button); playerToolbar = findViewById(R.id.player_toolbar); closeButton.setOnClickListener(v -> finish()); playerView.setOnClickListener(v -> toggleOverlay()); } private void loadChannel() { showLoading(true); retryCount = 0; // Resetear contador al cargar nuevo canal new Thread(() -> { try { String resolvedUrl = StreamUrlResolver.resolve(channelUrl); runOnUiThread(() -> startPlayback(resolvedUrl)); } catch (IOException e) { runOnUiThread(() -> showError("No se pudo conectar con el canal: " + e.getMessage())); } catch (Exception e) { runOnUiThread(() -> showError("Error inesperado: " + e.getMessage())); } }).start(); } private void startPlayback(String streamUrl) { try { releasePlayer(); lastStreamUrl = streamUrl; // Guardar URL para reintentos retryCount = 0; // Resetear contador al iniciar nueva reproducción DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this) .setEnableDecoderFallback(true) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON); // Configurar track selector para calidad adaptativa (no forzar máxima calidad) trackSelector = new DefaultTrackSelector(this); DefaultTrackSelector.Parameters params = trackSelector.buildUponParameters() .setForceHighestSupportedBitrate(false) // Permitir calidad adaptativa .setMaxVideoBitrate(Integer.MAX_VALUE) // Sin límite máximo de bitrate .build(); trackSelector.setParameters(params); player = new ExoPlayer.Builder(this, renderersFactory) .setTrackSelector(trackSelector) .setSeekForwardIncrementMs(10_000) .setSeekBackIncrementMs(10_000) .build(); playerView.setPlayer(player); player.addListener(new Player.Listener() { @Override public void onPlaybackStateChanged(int playbackState) { if (playbackState == Player.STATE_READY) { showLoading(false); retryCount = 0; // Resetear contador de reintentos al reproducir exitosamente } else if (playbackState == Player.STATE_BUFFERING) { showLoading(true); } } @Override public void onPlayerError(PlaybackException error) { String errorMsg = error.getMessage() != null ? error.getMessage() : ""; String detail = error.getCause() != null ? error.getCause().getMessage() : ""; String fullError = errorMsg + " " + detail; // Verificar si es un error que justifica reintento (404, conectividad, etc.) boolean isRetryableError = fullError.contains("404") || fullError.contains("403") || fullError.contains("timeout") || fullError.contains("Unable to connect") || fullError.contains("Network") || fullError.contains("source error") || error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED || error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT || error.errorCode == PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS; if (isRetryableError && retryCount < MAX_RETRIES) { retryCount++; runOnUiThread(() -> { showLoading(true); showError("Error de conexión. Reintentando... (" + retryCount + "/" + MAX_RETRIES + ")"); }); // Reintentar después de 2 segundos new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> { if (lastStreamUrl != null) { startPlayback(lastStreamUrl); } else { loadChannel(); } }, 2000); } else { // Mostrar error final después de agotar reintentos String finalMessage = "Error al reproducir: " + fullError; if (retryCount >= MAX_RETRIES) { finalMessage += "\n\nSe agotaron los reintentos (" + MAX_RETRIES + ")."; } showError(finalMessage); } } }); MediaItem mediaItem = MediaItem.fromUri(streamUrl); player.setMediaSource(buildMediaSource(mediaItem)); player.prepare(); player.setPlayWhenReady(true); setOverlayVisible(false); } catch (Exception e) { showError("Error al inicializar reproductor: " + e.getMessage()); } } private void showLoading(boolean show) { loadingIndicator.setVisibility(show ? View.VISIBLE : View.GONE); errorMessage.setVisibility(View.GONE); playerView.setVisibility(show ? View.GONE : View.VISIBLE); if (show) { setOverlayVisible(true); } } private void showError(String message) { loadingIndicator.setVisibility(View.GONE); playerView.setVisibility(View.GONE); errorMessage.setVisibility(View.VISIBLE); errorMessage.setText(message); setOverlayVisible(true); } private void releasePlayer() { if (player != null) { player.release(); player = null; } } private MediaSource buildMediaSource(MediaItem mediaItem) { Map headers = new HashMap<>(); headers.put("Referer", channelUrl); headers.put("Origin", "http://streamtp10.com"); headers.put("Accept", "*/*"); headers.put("Connection", "keep-alive"); String userAgent = Util.getUserAgent(this, "StreamPlayer"); OkHttpDataSource.Factory factory = new OkHttpDataSource.Factory(provideOkHttpClient()) .setUserAgent(userAgent) .setDefaultRequestProperties(headers); return new HlsMediaSource.Factory(factory).createMediaSource(mediaItem); } private OkHttpClient provideOkHttpClient() { if (okHttpClient != null) { return okHttpClient; } try { OkHttpClient bootstrap = new OkHttpClient.Builder() .connectTimeout(20, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build(); DnsOverHttps dohDns = new DnsOverHttps.Builder() .client(bootstrap) .url(HttpUrl.get("https://dns.google/dns-query")) .bootstrapDnsHosts( InetAddress.getByName("8.8.8.8"), InetAddress.getByName("8.8.4.4")) .build(); okHttpClient = bootstrap.newBuilder() .dns(dohDns) .build(); } catch (UnknownHostException e) { okHttpClient = new OkHttpClient.Builder() .connectTimeout(20, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build(); } return okHttpClient; } @Override protected void onStart() { super.onStart(); if (player != null) { playerView.onResume(); } else if (channelUrl != null) { loadChannel(); } } @Override protected void onResume() { super.onResume(); if (player != null) { playerView.onResume(); } } @Override protected void onPause() { super.onPause(); if (player != null) { playerView.onPause(); } } @Override protected void onStop() { super.onStop(); releasePlayer(); } @Override protected void onDestroy() { super.onDestroy(); releasePlayer(); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } private void toggleOverlay() { setOverlayVisible(!overlayVisible); } private void setOverlayVisible(boolean visible) { overlayVisible = visible; playerToolbar.setVisibility(visible ? View.VISIBLE : View.GONE); } @Override public void onBackPressed() { if (!overlayVisible) { setOverlayVisible(true); } else { super.onBackPressed(); } } }