342 lines
12 KiB
Java
342 lines
12 KiB
Java
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<String, String> 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();
|
|
}
|
|
}
|
|
}
|