diff --git a/DisneyPlayer/app/build.gradle b/DisneyPlayer/app/build.gradle
new file mode 100644
index 0000000..6845bda
--- /dev/null
+++ b/DisneyPlayer/app/build.gradle
@@ -0,0 +1,31 @@
+apply plugin: 'com.android.application'
+
+android {
+ namespace "com.disneyplayer"
+ compileSdk 35
+
+ defaultConfig {
+ applicationId "com.disneyplayer"
+ minSdk 21
+ targetSdk 35
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'org.videolan.android:libvlc-all:3.6.0-eap9'
+ implementation 'com.squareup.okhttp3:okhttp:4.12.0'
+}
diff --git a/DisneyPlayer/app/src/main/AndroidManifest.xml b/DisneyPlayer/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..821a672
--- /dev/null
+++ b/DisneyPlayer/app/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DisneyPlayer/app/src/main/java/com/disneyplayer/MainActivity.java b/DisneyPlayer/app/src/main/java/com/disneyplayer/MainActivity.java
new file mode 100644
index 0000000..be1fa43
--- /dev/null
+++ b/DisneyPlayer/app/src/main/java/com/disneyplayer/MainActivity.java
@@ -0,0 +1,186 @@
+package com.disneyplayer;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+import android.widget.TextView;
+
+import org.videolan.libvlc.LibVLC;
+import org.videolan.libvlc.Media;
+import org.videolan.libvlc.MediaPlayer;
+import org.videolan.libvlc.interfaces.IVLCVout;
+
+import java.util.ArrayList;
+
+public class MainActivity extends Activity {
+
+ private SurfaceView surfaceView;
+ private ProgressBar progressBar;
+ private Button playButton;
+ private TextView statusText;
+ private LibVLC libVlc;
+ private MediaPlayer mediaPlayer;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Create layout programmatically
+ LinearLayout layout = new LinearLayout(this);
+ layout.setOrientation(LinearLayout.VERTICAL);
+ layout.setGravity(android.view.Gravity.CENTER);
+
+ statusText = new TextView(this);
+ statusText.setText("Disney Player Ready");
+ statusText.setTextSize(18);
+ statusText.setPadding(20, 20, 20, 20);
+
+ playButton = new Button(this);
+ playButton.setText("▶ Reproducir Disney+");
+ playButton.setTextSize(18);
+ playButton.setPadding(40, 40, 40, 40);
+
+ progressBar = new ProgressBar(this);
+ progressBar.setVisibility(View.GONE);
+
+ surfaceView = new SurfaceView(this);
+ surfaceView.setVisibility(View.GONE);
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ 0
+ );
+ params.weight = 1;
+ surfaceView.setLayoutParams(params);
+
+ layout.addView(statusText);
+ layout.addView(progressBar);
+ layout.addView(playButton);
+ layout.addView(surfaceView);
+
+ setContentView(layout);
+
+ Toast.makeText(this, "App iniciada correctamente", Toast.LENGTH_LONG).show();
+
+ playButton.setOnClickListener(v -> {
+ Toast.makeText(this, "Botón presionado", Toast.LENGTH_SHORT).show();
+ playButton.setEnabled(false);
+ progressBar.setVisibility(View.VISIBLE);
+ statusText.setText("Iniciando reproducción...");
+
+ new Thread(() -> {
+ try {
+ String streamUrl = "https://live-ftc-sa-east-2.media.dssott.com/dvt2=exp=1772064227~url=%2Fgru1%2Fva01%2Fdisneyplus%2Fevent%2F2026%2F02%2F25%2FIndependiente_Medellin_CO_20260225_1771970452022%2F~psid=17da5759-0dba-4eb5-9375-616e2a1b1443~aid=71e116db-825e-477c-b3f4-df5d3f2c8923~did=2e11e811-1ed0-4193-859b-5c4ff40a44c5~country=AR~kid=k02~hmac=9da7486b6390fbbef9a9bf47aac4a6b18fe424f1d0c3c6c8623cf2785e74c60f/gru1/va01/disneyplus/event/2026/02/25/Independiente_Medellin_CO_20260225_1771970452022/ctr-all-hdri-complete.m3u8";
+ String drmKeyId = "4f37dd7a0f4d41b5947f627c0efcf654";
+ String drmKey = "01987a05f2f3ab9b0e245a4f1b36474e";
+
+ Thread.sleep(500); // Small delay to show the toast
+ runOnUiThread(() -> startPlayback(streamUrl, drmKeyId, drmKey));
+ } catch (Exception e) {
+ runOnUiThread(() -> {
+ Toast.makeText(this, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show();
+ playButton.setEnabled(true);
+ progressBar.setVisibility(View.GONE);
+ });
+ }
+ }).start();
+ });
+ }
+
+ private void startPlayback(String streamUrl, String drmKeyId, String drmKey) {
+ try {
+ releasePlayer();
+
+ ArrayList options = new ArrayList<>();
+ options.add("--network-caching=3000");
+ options.add(":no-cert-check");
+ options.add(":demux=avformat");
+ options.add(":key-format=org.w3.clearkey");
+ options.add(":key=" + drmKeyId + ":" + drmKey);
+ options.add(":http-user-agent=Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36");
+ options.add(":http-referrer=https://sudamericaplay.com/");
+
+ libVlc = new LibVLC(this, options);
+ mediaPlayer = new MediaPlayer(libVlc);
+
+ final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+ mediaPlayer.setEventListener(new MediaPlayer.EventListener() {
+ @Override
+ public void onEvent(MediaPlayer.Event event) {
+ mainHandler.post(() -> {
+ switch (event.type) {
+ case MediaPlayer.Event.Playing:
+ statusText.setText("Reproduciendo...");
+ progressBar.setVisibility(View.GONE);
+ surfaceView.setVisibility(View.VISIBLE);
+ Toast.makeText(MainActivity.this, "Video iniciado", Toast.LENGTH_SHORT).show();
+ break;
+
+ case MediaPlayer.Event.EncounteredError:
+ statusText.setText("Error de reproducción");
+ progressBar.setVisibility(View.GONE);
+ Toast.makeText(MainActivity.this, "Error al reproducir", Toast.LENGTH_LONG).show();
+ playButton.setEnabled(true);
+ break;
+
+ case MediaPlayer.Event.EndReached:
+ statusText.setText("Video terminado");
+ break;
+
+ case MediaPlayer.Event.Buffering:
+ float buffer = event.getBuffering();
+ if (buffer < 1.0f) {
+ statusText.setText("Buffering: " + (int)(buffer * 100) + "%");
+ } else {
+ statusText.setText("Reproduciendo...");
+ }
+ break;
+ }
+ });
+ }
+ });
+
+ Media media = new Media(libVlc, Uri.parse(streamUrl));
+ mediaPlayer.setMedia(media);
+ media.release();
+
+ IVLCVout vout = mediaPlayer.getVLCVout();
+ vout.setVideoView(surfaceView);
+ vout.attachViews();
+
+ mediaPlayer.play();
+
+ } catch (Exception e) {
+ Toast.makeText(this, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show();
+ playButton.setEnabled(true);
+ progressBar.setVisibility(View.GONE);
+ }
+ }
+
+ private void releasePlayer() {
+ if (mediaPlayer != null) {
+ mediaPlayer.stop();
+ mediaPlayer.getVLCVout().detachViews();
+ mediaPlayer.release();
+ mediaPlayer = null;
+ }
+ if (libVlc != null) {
+ libVlc.release();
+ libVlc = null;
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ releasePlayer();
+ }
+}
diff --git a/DisneyPlayer/app/src/main/res/layout/activity_main.xml b/DisneyPlayer/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..e6f7738
--- /dev/null
+++ b/DisneyPlayer/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DisneyPlayer/app/src/main/res/mipmap-hdpi/ic_launcher.png b/DisneyPlayer/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..dea13c7
Binary files /dev/null and b/DisneyPlayer/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/DisneyPlayer/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/DisneyPlayer/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..dea13c7
Binary files /dev/null and b/DisneyPlayer/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/DisneyPlayer/app/src/main/res/values/strings.xml b/DisneyPlayer/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..b658df3
--- /dev/null
+++ b/DisneyPlayer/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Disney Player
+
diff --git a/DisneyPlayer/app/src/main/res/values/themes.xml b/DisneyPlayer/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..d805658
--- /dev/null
+++ b/DisneyPlayer/app/src/main/res/values/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/DisneyPlayer/build.gradle b/DisneyPlayer/build.gradle
new file mode 100644
index 0000000..f9d04af
--- /dev/null
+++ b/DisneyPlayer/build.gradle
@@ -0,0 +1,21 @@
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.5.1'
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ maven { url "https://jitpack.io" }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/DisneyPlayer/gradle.properties b/DisneyPlayer/gradle.properties
new file mode 100644
index 0000000..3c5e113
--- /dev/null
+++ b/DisneyPlayer/gradle.properties
@@ -0,0 +1,2 @@
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+android.useAndroidX=true
diff --git a/DisneyPlayer/gradle/wrapper/gradle-wrapper.jar b/DisneyPlayer/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..033e24c
Binary files /dev/null and b/DisneyPlayer/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/DisneyPlayer/gradle/wrapper/gradle-wrapper.properties b/DisneyPlayer/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..b82aa23
--- /dev/null
+++ b/DisneyPlayer/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/DisneyPlayer/settings.gradle b/DisneyPlayer/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/DisneyPlayer/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/VLC_MIGRATION_PLAN.md b/VLC_MIGRATION_PLAN.md
new file mode 100644
index 0000000..ce20c05
--- /dev/null
+++ b/VLC_MIGRATION_PLAN.md
@@ -0,0 +1,639 @@
+# VLC Player Migration Plan for Android App
+
+## Current State Analysis
+- **Location**: `/home/ren/futbol/`
+- **Current Player**: ExoPlayer/Media3 1.5.0
+- **Main Files**:
+ - `app/src/main/java/com/streamplayer/PlayerActivity.java`
+ - `app/src/main/java/com/streamplayer/StreamUrlResolver.java`
+ - `app/src/main/res/layout/activity_player.xml`
+- **Key Features**: HLS streams, custom headers, retry logic, loading indicators, error handling
+
+## Target State
+- **New Player**: libvlc for Android (VLC Android SDK)
+- **DRM Support**: ClearKey DRM for Disney+ streams
+- **Preserve**: All existing functionality
+
+---
+
+## Phase 1: Build Configuration Changes
+
+### File: `app/build.gradle`
+
+**Remove Media3 dependencies:**
+```gradle
+// REMOVE these lines (52-57):
+implementation 'androidx.media3:media3-exoplayer:1.5.0'
+implementation 'androidx.media3:media3-exoplayer-hls:1.5.0'
+implementation 'androidx.media3:media3-datasource-okhttp:1.5.0'
+implementation 'androidx.media3:media3-ui:1.5.0'
+implementation 'androidx.media3:media3-ui-leanback:1.5.0'
+implementation 'androidx.media3:media3-session:1.5.0'
+```
+
+**Add VLC Android SDK dependency:**
+```gradle
+// ADD after line 57:
+// VLC Android SDK
+implementation 'org.videolan.android:libvlc-all:3.5.4'
+// Alternative: Use specific modules for smaller APK
+// implementation 'org.videolan.android:libvlc:3.5.4'
+```
+
+---
+
+## Phase 2: Layout Changes
+
+### File: `app/src/main/res/layout/activity_player.xml`
+
+**Current (lines 10-15):**
+```xml
+
+```
+
+**Replace with VLC SurfaceView:**
+```xml
+
+
+
+
+```
+
+**Note**: Keep all other UI elements unchanged (toolbar, loading indicator, error message).
+
+---
+
+## Phase 3: PlayerActivity Rewrite
+
+### File: `app/src/main/java/com/streamplayer/PlayerActivity.java`
+
+**Import Changes:**
+```java
+// REMOVE these imports (14-28):
+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.exoplayer.drm.DrmSessionManager;
+import androidx.media3.ui.PlayerView;
+import androidx.media3.common.util.Util;
+import androidx.annotation.OptIn;
+import androidx.media3.common.util.UnstableApi;
+
+// ADD VLC imports:
+import org.videolan.libvlc.LibVLC;
+import org.videolan.libvlc.Media;
+import org.videolan.libvlc.MediaPlayer;
+import org.videolan.libvlc.interfaces.IVLCVout;
+import org.videolan.libvlc.util.VLCVideoLayout;
+import android.view.SurfaceView;
+import android.view.SurfaceHolder;
+```
+
+**Class Member Variables (lines 51-66):**
+```java
+// REPLACE:
+private PlayerView playerView;
+private ExoPlayer player;
+private DefaultTrackSelector trackSelector;
+
+// WITH:
+private VLCVideoLayout playerView; // OR SurfaceView playerSurface
+private LibVLC libVlc;
+private MediaPlayer mediaPlayer;
+private SurfaceView surfaceView;
+```
+
+**onCreate Method (lines 68-98):**
+```java
+// MODIFY initViews() call:
+private void initViews() {
+ playerView = findViewById(R.id.player_view); // VLCVideoLayout
+ // OR if using SurfaceView:
+ surfaceView = findViewById(R.id.player_surface);
+
+ 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());
+ // For SurfaceView:
+ // surfaceView.setOnClickListener(v -> toggleOverlay());
+}
+```
+
+---
+
+## Phase 4: VLC Player Implementation Details
+
+### 4.1 Initialize VLC with Custom Headers
+
+**New Method: `startPlayback(String streamUrl)`**
+```java
+private void startPlayback(String streamUrl) {
+ try {
+ releasePlayer();
+ lastStreamUrl = streamUrl;
+ retryCount = 0;
+
+ // Create LibVLC instance with options
+ ArrayList options = new ArrayList<>();
+
+ // Network options
+ options.add("--network-caching=1500");
+ options.add("--clock-jitter=0");
+ options.add("--clock-synchro=0");
+
+ // HTTP options for headers
+ options.add(":http-user-agent=" + USER_AGENT);
+ options.add(":http-referrer=" + "http://streamtp10.com/");
+
+ // SSL options (accept all certificates)
+ options.add("--no-xlib");
+ options.add(":rtsp-tcp");
+ options.add(":no-cert-check");
+
+ // Create LibVLC
+ libVlc = new LibVLC(this, options);
+
+ // Create MediaPlayer
+ mediaPlayer = new MediaPlayer(libVlc);
+
+ // Set up event listeners
+ setupMediaPlayerListeners();
+
+ // Create Media with custom headers
+ Media media = new Media(libVlc, android.net.Uri.parse(streamUrl));
+
+ // Add headers via Media options
+ Map headers = new HashMap<>();
+ headers.put("Referer", "http://streamtp10.com/");
+ headers.put("User-Agent", USER_AGENT);
+ media.addOption(":http-user-agent=" + USER_AGENT);
+ media.addOption(":http-referrer=http://streamtp10.com/");
+
+ mediaPlayer.setMedia(media);
+ media.release();
+
+ // Set up video output
+ IVLCVout vout = mediaPlayer.getVLCVout();
+ if (playerView instanceof VLCVideoLayout) {
+ vout.setVideoView(playerView);
+ } else if (surfaceView != null) {
+ vout.setVideoView(surfaceView);
+ }
+ vout.attachViews();
+
+ // Start playback
+ mediaPlayer.play();
+ setOverlayVisible(false);
+
+ } catch (Exception e) {
+ showError("Error al inicializar reproductor: " + e.getMessage());
+ }
+}
+```
+
+### 4.2 Player Event Listeners
+
+**New Method: `setupMediaPlayerListeners()`**
+```java
+private void setupMediaPlayerListeners() {
+ // MediaPlayer.EventListener using the VLC event system
+ mediaPlayer.setEventListener(new MediaPlayer.EventListener() {
+ @Override
+ public void onEvent(MediaPlayer.Event event) {
+ switch (event.type) {
+ case MediaPlayer.Event.Playing:
+ // Equivalent to STATE_READY
+ runOnUiThread(() -> {
+ showLoading(false);
+ retryCount = 0;
+ });
+ break;
+
+ case MediaPlayer.Event.Buffering:
+ // Buffering state (0.0 to 1.0)
+ float bufferPercent = event.getBuffering();
+ runOnUiThread(() -> showLoading(bufferPercent < 1.0f));
+ break;
+
+ case MediaPlayer.Event.EncounteredError:
+ // Error occurred
+ runOnUiThread(() -> handlePlaybackError("Error de reproducción VLC"));
+ break;
+
+ case MediaPlayer.Event.EndReached:
+ // Stream ended
+ runOnUiThread(() -> finish());
+ break;
+
+ case MediaPlayer.Event.Stopped:
+ // Playback stopped
+ break;
+
+ case MediaPlayer.Event.Paused:
+ // Playback paused
+ break;
+
+ case MediaPlayer.Event.Opening:
+ // Stream opening
+ runOnUiThread(() -> showLoading(true));
+ break;
+ }
+ }
+ });
+}
+```
+
+### 4.3 Error Handling with Retry
+
+**New Method: `handlePlaybackError(String errorMessage)`**
+```java
+private void handlePlaybackError(String errorMsg) {
+ boolean isRetryableError =
+ errorMsg.contains("404") ||
+ errorMsg.contains("403") ||
+ errorMsg.contains("timeout") ||
+ errorMsg.contains("Network") ||
+ errorMsg.contains("Connection");
+
+ if (isRetryableError && retryCount < MAX_RETRIES) {
+ retryCount++;
+ runOnUiThread(() -> {
+ showLoading(true);
+ showError("Error de conexión. Reintentando... (" + retryCount + "/" + MAX_RETRIES + ")");
+ });
+
+ new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
+ if (lastStreamUrl != null) {
+ startPlayback(lastStreamUrl);
+ } else {
+ loadChannel();
+ }
+ }, 2000);
+ } else {
+ String finalMessage = "Error al reproducir: " + errorMsg;
+ if (retryCount >= MAX_RETRIES) {
+ finalMessage += "\n\nSe agotaron los reintentos (" + MAX_RETRIES + ").";
+ }
+ showError(finalMessage);
+ }
+}
+```
+
+### 4.4 Player Lifecycle Methods
+
+**Replace releasePlayer() (lines 257-262):**
+```java
+private void releasePlayer() {
+ if (mediaPlayer != null) {
+ mediaPlayer.stop();
+ mediaPlayer.getVLCVout().detachViews();
+ mediaPlayer.release();
+ mediaPlayer = null;
+ }
+ if (libVlc != null) {
+ libVlc.release();
+ libVlc = null;
+ }
+}
+```
+
+**Update lifecycle methods:**
+```java
+@Override
+protected void onStart() {
+ super.onStart();
+ if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
+ mediaPlayer.play();
+ } else if (channelUrl != null && libVlc == null) {
+ loadChannel();
+ }
+}
+
+@Override
+protected void onResume() {
+ super.onResume();
+ if (mediaPlayer != null) {
+ mediaPlayer.play();
+ }
+}
+
+@Override
+protected void onPause() {
+ super.onPause();
+ if (mediaPlayer != null && mediaPlayer.isPlaying()) {
+ mediaPlayer.pause();
+ }
+}
+
+@Override
+protected void onStop() {
+ super.onStop();
+ // Keep player for quick resume, don't release
+}
+
+@Override
+protected void onDestroy() {
+ super.onDestroy();
+ releasePlayer();
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+}
+```
+
+---
+
+## Phase 5: DRM Support (ClearKey)
+
+### ClearKey DRM Implementation for Disney+
+
+**New Class: `VlcDrmManager.java`**
+**Location**: `app/src/main/java/com/streamplayer/VlcDrmManager.java`
+
+```java
+package com.streamplayer;
+
+import org.videolan.libvlc.Media;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Handles DRM configuration for VLC Media Player
+ * Supports ClearKey DRM for Disney+ and other streaming services
+ */
+public class VlcDrmManager {
+
+ // ClearKey DRM configuration
+ private static final String CLEARKEY_KEY_SYSTEM = "org.w3.clearkey";
+
+ /**
+ * Configure ClearKey DRM for a Media object
+ * @param media The VLC Media object
+ * @param keyId The key ID (extracted from manifest or license server)
+ * @param key The content key
+ */
+ public static void configureClearKey(Media media, String keyId, String key) {
+ if (media == null || keyId == null || key == null) {
+ return;
+ }
+
+ // VLC uses a specific format for ClearKey
+ // Format: keyid:key
+ String keyPair = keyId + ":" + key;
+ media.addOption(":demux=avformat");
+ media.addOption(":key-format=" + CLEARKEY_KEY_SYSTEM);
+ media.addOption(":key=" + keyPair);
+ }
+
+ /**
+ * Configure ClearKey DRM with multiple keys
+ * @param media The VLC Media object
+ * @param keys Map of keyId -> key pairs
+ */
+ public static void configureClearKey(Media media, Map keys) {
+ if (media == null || keys == null || keys.isEmpty()) {
+ return;
+ }
+
+ media.addOption(":demux=avformat");
+ media.addOption(":key-format=" + CLEARKEY_KEY_SYSTEM);
+
+ for (Map.Entry entry : keys.entrySet()) {
+ String keyPair = entry.getKey() + ":" + entry.getValue();
+ media.addOption(":key=" + keyPair);
+ }
+ }
+
+ /**
+ * Configure Widevine DRM (for reference, if needed later)
+ * VLC supports Widevine via specific options
+ */
+ public static void configureWidevine(Media media, String drmServerUrl) {
+ if (media == null || drmServerUrl == null) {
+ return;
+ }
+
+ media.addOption(":drm=widevine");
+ media.addOption(":aes-key=" + drmServerUrl);
+ }
+}
+```
+
+### Extracting ClearKey Keys from Stream
+
+**Update `StreamUrlResolver.java` to extract DRM info:**
+
+Add this method to `StreamUrlResolver`:
+```java
+/**
+ * Extract ClearKey DRM keys from JSON in HTML
+ * @return Map of keyId -> key pairs, or null if no DRM found
+ */
+public static Map extractClearKeyKeys(String html) {
+ Map keys = new HashMap<>();
+
+ try {
+ // Pattern to find ClearKey key IDs and keys
+ // Common patterns in Disney+ and similar services
+ Pattern clearkeyPattern = Pattern.compile(
+ "\"kid\"\\s*:\\s*\"([^\"]+)\".*?\"k\"\\s*:\\s*\"([^\"]+)\"",
+ Pattern.DOTALL
+ );
+
+ Matcher matcher = clearkeyPattern.matcher(html);
+ while (matcher.find()) {
+ String keyId = matcher.group(1);
+ String key = matcher.group(2);
+ keys.put(keyId, key);
+ }
+
+ // Alternative pattern for JWPlayer with DRM
+ Pattern jwDrmPattern = Pattern.compile(
+ "\"drm\"\\s*:\\s*\\{[^}]*\"clearkey\"\\s*:\\s*\\{[^}]*\"keyId\"\\s*:\\s*\"([^\"]+)\"[^}]*\"key\"\\s*:\\s*\"([^\"]+)\"",
+ Pattern.DOTALL
+ );
+
+ Matcher jwMatcher = jwDrmPattern.matcher(html);
+ while (jwMatcher.find()) {
+ String keyId = jwMatcher.group(1);
+ String key = jwMatcher.group(2);
+ keys.put(keyId, key);
+ }
+ } catch (Exception e) {
+ // Return empty map on error
+ }
+
+ return keys.isEmpty() ? null : keys;
+}
+```
+
+### Using DRM in PlayerActivity
+
+**Modify `startPlayback()` to handle DRM:**
+```java
+// After loading stream URL, also check for DRM keys
+new Thread(() -> {
+ try {
+ String resolvedUrl = StreamUrlResolver.resolve(channelUrl);
+ Map drmKeys = StreamUrlResolver.extractClearKeyKeys(html); // Need to modify resolver to also return HTML
+
+ runOnUiThread(() -> startPlayback(resolvedUrl, drmKeys));
+ } catch (IOException e) {
+ runOnUiThread(() -> showError("No se pudo conectar con el canal: " + e.getMessage()));
+ }
+}).start();
+
+private void startPlayback(String streamUrl, Map drmKeys) {
+ // ... existing VLC setup code ...
+
+ Media media = new Media(libVlc, android.net.Uri.parse(streamUrl));
+
+ // Add headers
+ media.addOption(":http-user-agent=" + USER_AGENT);
+ media.addOption(":http-referrer=http://streamtp10.com/");
+
+ // Add DRM if available
+ if (drmKeys != null && !drmKeys.isEmpty()) {
+ VlcDrmManager.configureClearKey(media, drmKeys);
+ }
+
+ // ... rest of the setup ...
+}
+```
+
+---
+
+## Phase 6: Additional Files
+
+### New File: `VlcPlayerConfig.java`
+**Location**: `app/src/main/java/com/streamplayer/VlcPlayerConfig.java`
+
+```java
+package com.streamplayer;
+
+/**
+ * Configuration constants for VLC Player
+ */
+public class VlcPlayerConfig {
+
+ // Network caching (ms)
+ public static final int NETWORK_CACHING = 1500;
+
+ // Live streaming caching (ms)
+ public static final int LIVE_CACHING = 5000;
+
+ // User Agent
+ public static final String USER_AGENT =
+ "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36";
+
+ // Hardware acceleration options
+ public static final String HW_ACCELERATION = "automatic"; // or "full", "none", "decoding", "rendering"
+
+ // Chroma format
+ public static final String CHROMA = "RV32"; // or YV12, NV12
+
+ // Audio output
+ public static final String AUDIO_OUTPUT = "opensles";
+}
+```
+
+---
+
+## Phase 7: Summary of File Changes
+
+| File Path | Change Type | Description |
+|-----------|-------------|-------------|
+| `app/build.gradle` | Modify | Remove Media3 deps, add VLC SDK |
+| `app/src/main/res/layout/activity_player.xml` | Modify | Replace PlayerView with VLCVideoLayout |
+| `app/src/main/java/com/streamplayer/PlayerActivity.java` | Rewrite | Complete VLC integration |
+| `app/src/main/java/com/streamplayer/VlcDrmManager.java` | New | DRM configuration handler |
+| `app/src/main/java/com/streamplayer/VlcPlayerConfig.java` | New | VLC configuration constants |
+| `app/src/main/java/com/streamplayer/StreamUrlResolver.java` | Modify | Add DRM key extraction |
+
+---
+
+## Phase 8: VLC-Specific Notes
+
+### HTTP Headers in VLC
+VLC handles HTTP headers differently than ExoPlayer:
+- Headers are set via Media.addOption() with colon prefix
+- Format: `:http-header-name=value`
+- Common options:
+ - `:http-user-agent=`
+ - `:http-referrer=`
+ - `:http-cookie=`
+
+### VLC Events vs ExoPlayer States
+
+| ExoPlayer State | VLC Event |
+|----------------|-----------|
+| STATE_IDLE | N/A (not initialized) |
+| STATE_BUFFERING | Event.Buffering |
+| STATE_READY | Event.Playing |
+| STATE_ENDED | Event.EndReached |
+
+### DRM Support Comparison
+| Feature | ExoPlayer | VLC |
+|---------|-----------|-----|
+| Widevine | Native | Limited |
+| ClearKey | Via DRM module | Via libavformat |
+| PlayReady | Native | Limited |
+| FairPlay | No | Limited |
+
+---
+
+## Phase 9: Testing Checklist
+
+1. [ ] Basic HLS stream playback
+2. [ ] HTTP headers (Referer, User-Agent) applied correctly
+3. [ ] Loading indicator shows/hides correctly
+4. [ ] Error messages display properly
+5. [ ] Retry logic works on connection failure
+6. [ ] Screen stays on during playback
+7. [ ] Overlay toggle works on tap
+8. [ ] Close button returns to main activity
+9. [ ] App handles pause/resume correctly
+10. [ ] Memory leaks checked (no retained VLC instances)
+11. [ ] DRM streams play correctly (Disney+)
+12. [ ] SSL certificate bypass works
+
+---
+
+## Phase 10: Fallback Strategy
+
+If VLC doesn't work as expected, consider:
+1. **Hybrid approach**: Keep ExoPlayer as fallback, use VLC for DRM-only streams
+2. **Alternative libraries**:
+ - Vitamio (deprecated but still works)
+ - NKD-Player (wrapper around FFmpeg)
+ - Build custom FFmpeg integration
+3. **Webview approach**: Use embedded browser for DRM content
+
+---
+
+## Appendix: VLC SDK Documentation Links
+
+- VLC Android SDK: https://code.videolan.org/videolan/vlc-android
+- VLC LibVLC API: https://code.videolan.org/videolan/vlc-android/-/tree/master/libvlc
+- VLC Wiki on Android: https://wiki.videolan.org/AndroidCompile
+- ClearKey DRM spec: https://w3c.github.io/encrypted-media/format-registry/initdata/cenc.html
diff --git a/app/build.gradle b/app/build.gradle
index 7013431..ef683e8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -8,8 +8,8 @@ android {
applicationId "com.streamplayer"
minSdk 21
targetSdk 35
- versionCode 100111
- versionName "10.1.11"
+ versionCode 100200
+ versionName "11.0.0"
buildConfigField "String", "DEVICE_REGISTRY_URL", '"http://194.163.191.200:4000"'
}
@@ -47,16 +47,14 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
+ implementation 'androidx.media3:media3-exoplayer:1.4.1'
+ implementation 'androidx.media3:media3-exoplayer-hls:1.4.1'
+ implementation 'androidx.media3:media3-ui:1.4.1'
- // Media3 para reproduccion de video (Android TV optimizado)
- implementation 'androidx.media3:media3-exoplayer:1.5.0'
- implementation 'androidx.media3:media3-exoplayer-hls:1.5.0'
- implementation 'androidx.media3:media3-datasource-okhttp:1.5.0'
- implementation 'androidx.media3:media3-ui:1.5.0'
- implementation 'androidx.media3:media3-ui-leanback:1.5.0'
- implementation 'androidx.media3:media3-session:1.5.0'
+ // VLC Player para reproduccion de video (soporte DRM mejorado)
+ implementation 'org.videolan.android:libvlc-all:3.4.4'
- // OkHttp con DNS over HTTPS
+ // OkHttp con DNS over HTTPS (para StreamUrlResolver)
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 606252c..f8500fa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
Cloudflare -> AdGuard -> Quad9 -> Sistema
+ * Utilidad centralizada para configuración de red.
+ * Fuerza DNS over HTTPS con fallback Google -> Cloudflare -> DNS del sistema.
*/
public class NetworkUtils {
private static final OkHttpClient CLIENT;
private static final String USER_AGENT = "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36";
-
- // URLs de servidores DNS over HTTPS
private static final String GOOGLE_DOH_URL = "https://dns.google/dns-query";
private static final String CLOUDFLARE_DOH_URL = "https://cloudflare-dns.com/dns-query";
- private static final String ADGUARD_DOH_URL = "https://dns.adguard-dns.com/dns-query";
- private static final String QUAD9_DOH_URL = "https://dns.quad9.net/dns-query";
static {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
@@ -61,109 +57,60 @@ public class NetworkUtils {
builder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]);
builder.hostnameVerifier((hostname, session) -> true);
- // Cliente bootstrap para resolver los dominios de DNS
OkHttpClient bootstrap = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(10, TimeUnit.SECONDS)
+ .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0])
+ .hostnameVerifier((hostname, session) -> true)
.retryOnConnectionFailure(true)
.build();
- // 1. Google DNS over HTTPS (Primario)
final DnsOverHttps googleDns = new DnsOverHttps.Builder()
.client(bootstrap)
.url(HttpUrl.get(GOOGLE_DOH_URL))
.bootstrapDnsHosts(
- getByIp("8.8.8.8"),
- getByIp("8.8.4.4"))
+ InetAddress.getByName("8.8.8.8"),
+ InetAddress.getByName("8.8.4.4"))
.includeIPv6(false)
.build();
- // 2. Cloudflare DNS over HTTPS (Secundario)
final DnsOverHttps cloudflareDns = new DnsOverHttps.Builder()
.client(bootstrap)
.url(HttpUrl.get(CLOUDFLARE_DOH_URL))
.bootstrapDnsHosts(
- getByIp("1.1.1.1"),
- getByIp("1.0.0.1"))
+ InetAddress.getByName("1.1.1.1"),
+ InetAddress.getByName("1.0.0.1"))
.includeIPv6(false)
.build();
- // 3. AdGuard DNS over HTTPS (Terciario)
- final DnsOverHttps adGuardDns = new DnsOverHttps.Builder()
- .client(bootstrap)
- .url(HttpUrl.get(ADGUARD_DOH_URL))
- .bootstrapDnsHosts(
- getByIp("94.140.14.14"),
- getByIp("94.140.15.15"))
- .includeIPv6(false)
- .build();
-
- // 4. Quad9 DNS over HTTPS (Cuaternario)
- final DnsOverHttps quad9Dns = new DnsOverHttps.Builder()
- .client(bootstrap)
- .url(HttpUrl.get(QUAD9_DOH_URL))
- .bootstrapDnsHosts(
- getByIp("9.9.9.9"),
- getByIp("149.112.112.112"))
- .includeIPv6(false)
- .build();
-
- // Configurar DNS con fallback: Google -> Cloudflare -> AdGuard -> Quad9 -> Sistema
- builder.dns(new Dns() {
- @Override
- public List lookup(String hostname) throws UnknownHostException {
- // Intento 1: Google DNS
- try {
- List result = googleDns.lookup(hostname);
- if (result != null && !result.isEmpty()) return result;
- } catch (Exception ignored) {
- // Falló Google, continuar
- }
-
- // Intento 2: Cloudflare DNS
- try {
- List result = cloudflareDns.lookup(hostname);
- if (result != null && !result.isEmpty()) return result;
- } catch (Exception ignored) {
- // Falló Cloudflare, continuar
- }
-
- // Intento 3: AdGuard DNS
- try {
- List result = adGuardDns.lookup(hostname);
- if (result != null && !result.isEmpty()) return result;
- } catch (Exception ignored) {
- // Falló AdGuard, continuar
- }
-
- // Intento 4: Quad9 DNS
- try {
- List result = quad9Dns.lookup(hostname);
- if (result != null && !result.isEmpty()) return result;
- } catch (Exception ignored) {
- // Falló Quad9, continuar
- }
-
- // Intento 5: DNS del Sistema (Fallback final)
- try {
- return Dns.SYSTEM.lookup(hostname);
- } catch (UnknownHostException e) {
- throw e;
+ builder.dns(hostname -> {
+ try {
+ List result = googleDns.lookup(hostname);
+ if (result != null && !result.isEmpty()) {
+ return result;
}
+ } catch (Exception ignored) {
}
+
+ try {
+ List result = cloudflareDns.lookup(hostname);
+ if (result != null && !result.isEmpty()) {
+ return result;
+ }
+ } catch (Exception ignored) {
+ }
+
+ return Dns.SYSTEM.lookup(hostname);
});
} catch (Exception e) {
- // Si algo falla en la configuración DNS, usamos por defecto (implícito en el builder)
+ builder.dns(Dns.SYSTEM);
System.out.println("Error configurando DNS over HTTPS: " + e.getMessage());
}
CLIENT = builder.build();
}
- private static InetAddress getByIp(String ip) throws UnknownHostException {
- return InetAddress.getByName(ip);
- }
-
public static OkHttpClient getClient() {
return CLIENT;
}
diff --git a/app/src/main/java/com/streamplayer/PlayerActivity.java b/app/src/main/java/com/streamplayer/PlayerActivity.java
index bc05e73..93fff25 100644
--- a/app/src/main/java/com/streamplayer/PlayerActivity.java
+++ b/app/src/main/java/com/streamplayer/PlayerActivity.java
@@ -1,8 +1,12 @@
package com.streamplayer;
import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.os.StrictMode;
+import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
@@ -10,42 +14,24 @@ 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.datasource.DefaultHttpDataSource;
+import androidx.media3.exoplayer.ExoPlayer;
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.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.TimeUnit;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
-
-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 static final String TAG = "PlayerActivity";
+ private static final long STARTUP_TIMEOUT_MS = 12000L;
private PlayerView playerView;
private ProgressBar loadingIndicator;
@@ -55,14 +41,18 @@ public class PlayerActivity extends AppCompatActivity {
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;
+ private String currentChannelPageUrl;
+ private boolean playbackStarted = false;
+ private boolean alternateSourceAttempted = false;
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
+ private Runnable startupTimeoutRunnable;
+ private final Object resolveLock = new Object();
+ private int resolveGeneration = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -88,11 +78,11 @@ public class PlayerActivity extends AppCompatActivity {
finish();
return;
}
+ currentChannelPageUrl = channelUrl;
initViews();
channelLabel.setText(channelName);
- // DNS configurado en StreamUrlResolver
loadChannel();
}
@@ -106,118 +96,261 @@ public class PlayerActivity extends AppCompatActivity {
closeButton.setOnClickListener(v -> finish());
playerView.setOnClickListener(v -> toggleOverlay());
+ playerView.setUseController(false);
}
private void loadChannel() {
showLoading(true);
- retryCount = 0; // Resetear contador al cargar nuevo canal
+ retryCount = 0;
+ alternateSourceAttempted = false;
+ currentChannelPageUrl = channelUrl;
+ loadChannelFromPageUrl(channelUrl);
+ }
+
+ private void loadChannelFromPageUrl(String pageUrl) {
+ currentChannelPageUrl = pageUrl;
+ final int requestGeneration;
+ synchronized (resolveLock) {
+ resolveGeneration++;
+ requestGeneration = resolveGeneration;
+ }
+
+ Log.d(TAG, "Resolviendo stream desde: " + pageUrl + " (req=" + requestGeneration + ")");
+
new Thread(() -> {
try {
- String resolvedUrl = StreamUrlResolver.resolve(channelUrl);
- runOnUiThread(() -> startPlayback(resolvedUrl));
+ String resolvedUrl = StreamUrlResolver.resolve(pageUrl);
+ Log.d(TAG, "Stream resuelto: " + resolvedUrl + " (req=" + requestGeneration + ")");
+ runOnUiThread(() -> {
+ if (!isLatestResolveRequest(requestGeneration)) {
+ Log.d(TAG, "Ignorando resultado viejo (req=" + requestGeneration + ")");
+ return;
+ }
+ startPlayback(resolvedUrl);
+ });
} catch (IOException e) {
- runOnUiThread(() -> showError("No se pudo conectar con el canal: " + e.getMessage()));
+ runOnUiThread(() -> {
+ if (!isLatestResolveRequest(requestGeneration)) {
+ return;
+ }
+ if (!tryAlternateSource("No se pudo conectar con el canal. " + e.getMessage())) {
+ showError("No se pudo conectar con el canal: " + e.getMessage());
+ }
+ });
} catch (Exception e) {
- runOnUiThread(() -> showError("Error inesperado: " + e.getMessage()));
+ runOnUiThread(() -> {
+ if (!isLatestResolveRequest(requestGeneration)) {
+ return;
+ }
+ if (!tryAlternateSource("Error inesperado al resolver stream. " + e.getMessage())) {
+ showError("Error inesperado: " + e.getMessage());
+ }
+ });
}
}).start();
}
+ private boolean isLatestResolveRequest(int requestGeneration) {
+ synchronized (resolveLock) {
+ return requestGeneration == resolveGeneration;
+ }
+ }
+
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();
+ lastStreamUrl = streamUrl;
+ retryCount = 0;
+ playbackStarted = false;
+ scheduleStartupTimeout();
+ Log.d(TAG, "Iniciando reproducción: " + streamUrl);
+
+ DefaultHttpDataSource.Factory httpFactory = new DefaultHttpDataSource.Factory()
+ .setUserAgent(VlcPlayerConfig.USER_AGENT)
+ .setAllowCrossProtocolRedirects(true)
+ .setConnectTimeoutMs(15000)
+ .setReadTimeoutMs(20000);
+
+ Map headers = new HashMap<>();
+ headers.put("User-Agent", VlcPlayerConfig.USER_AGENT);
+ httpFactory.setDefaultRequestProperties(headers);
+
+ HlsMediaSource mediaSource = new HlsMediaSource.Factory(httpFactory)
+ .setAllowChunklessPreparation(true)
+ .createMediaSource(MediaItem.fromUri(Uri.parse(streamUrl)));
+
+ player = new ExoPlayer.Builder(this).build();
playerView.setPlayer(player);
+ setupPlayerListener();
- 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);
- }
- }
+ player.setMediaSource(mediaSource);
+ player.prepare();
+ player.play();
+ setOverlayVisible(false);
+ } catch (Exception e) {
+ cancelStartupTimeout();
+ Log.e(TAG, "Error al iniciar reproducción", e);
+ if (!tryAlternateSource("Error al inicializar reproductor. Probando fuente alterna...")) {
+ showError("Error al inicializar reproductor: " + e.getMessage());
+ }
+ }
+ }
- @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;
+ private void setupPlayerListener() {
+ if (player == null) {
+ return;
+ }
- // 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++;
+ player.addListener(new Player.Listener() {
+ @Override
+ public void onPlaybackStateChanged(int playbackState) {
+ switch (playbackState) {
+ case Player.STATE_BUFFERING:
+ Log.d(TAG, "Exo Event: BUFFERING");
+ runOnUiThread(() -> showLoading(true));
+ break;
+ case Player.STATE_READY:
+ Log.d(TAG, "Exo Event: READY");
runOnUiThread(() -> {
- showLoading(true);
- showError("Error de conexión. Reintentando... (" + retryCount + "/" + MAX_RETRIES + ")");
+ playbackStarted = true;
+ cancelStartupTimeout();
+ showLoading(false);
+ retryCount = 0;
});
-
- // 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);
- }
+ break;
+ case Player.STATE_ENDED:
+ Log.d(TAG, "Exo Event: ENDED");
+ runOnUiThread(() -> {
+ cancelStartupTimeout();
+ finish();
+ });
+ break;
+ default:
+ break;
}
+ }
+
+ @Override
+ public void onIsPlayingChanged(boolean isPlaying) {
+ Log.d(TAG, "Exo Event: isPlaying=" + isPlaying);
+ if (isPlaying) {
+ runOnUiThread(() -> {
+ playbackStarted = true;
+ cancelStartupTimeout();
+ showLoading(false);
+ });
+ }
+ }
+
+ @Override
+ public void onPlayerError(PlaybackException error) {
+ String message = error.getMessage() != null
+ ? error.getMessage()
+ : "code=" + error.errorCode;
+ Log.e(TAG, "Exo Error: " + message, error);
+ runOnUiThread(() -> {
+ cancelStartupTimeout();
+ handlePlaybackError("Error de reproducción Exo: " + message);
+ });
+ }
+ });
+ }
+
+ private void handlePlaybackError(String errorMsg) {
+ if (tryAlternateSource("Falló la reproducción. " + errorMsg)) {
+ return;
+ }
+
+ String lower = errorMsg.toLowerCase();
+ boolean isRetryableError =
+ lower.contains("404") ||
+ lower.contains("403") ||
+ lower.contains("timeout") ||
+ lower.contains("network") ||
+ lower.contains("connection") ||
+ lower.contains("source");
+
+ if (isRetryableError && retryCount < VlcPlayerConfig.MAX_RETRIES) {
+ retryCount++;
+ runOnUiThread(() -> {
+ showLoading(true);
+ errorMessage.setVisibility(View.VISIBLE);
+ errorMessage.setText("Error de conexión. Reintentando... (" + retryCount + "/" + VlcPlayerConfig.MAX_RETRIES + ")");
});
- 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());
+ mainHandler.postDelayed(() -> {
+ if (lastStreamUrl != null) {
+ startPlayback(lastStreamUrl);
+ } else {
+ loadChannel();
+ }
+ }, 1500);
+ } else {
+ String finalMessage = "Error al reproducir: " + errorMsg;
+ if (retryCount >= VlcPlayerConfig.MAX_RETRIES) {
+ finalMessage += "\n\nSe agotaron los reintentos (" + VlcPlayerConfig.MAX_RETRIES + ").";
+ }
+ showError(finalMessage);
}
}
+ private void scheduleStartupTimeout() {
+ cancelStartupTimeout();
+ startupTimeoutRunnable = () -> {
+ if (!playbackStarted) {
+ Log.w(TAG, "Timeout de inicio de reproducción");
+ if (!tryAlternateSource("El canal no inició a tiempo. Probando fuente alterna...")) {
+ handlePlaybackError("Timeout al iniciar stream");
+ }
+ }
+ };
+ mainHandler.postDelayed(startupTimeoutRunnable, STARTUP_TIMEOUT_MS);
+ }
+
+ private void cancelStartupTimeout() {
+ if (startupTimeoutRunnable != null) {
+ mainHandler.removeCallbacks(startupTimeoutRunnable);
+ startupTimeoutRunnable = null;
+ }
+ }
+
+ private boolean tryAlternateSource(String reason) {
+ if (alternateSourceAttempted) {
+ return false;
+ }
+
+ String alternateUrl = buildAlternateGlobalUrl(currentChannelPageUrl);
+ if (alternateUrl == null || alternateUrl.equals(currentChannelPageUrl)) {
+ return false;
+ }
+
+ alternateSourceAttempted = true;
+ Log.w(TAG, "Probando fuente alterna: " + alternateUrl + " | motivo: " + reason);
+
+ showLoading(true);
+ errorMessage.setVisibility(View.VISIBLE);
+ errorMessage.setText("Problema con la fuente actual.\nProbando fuente alterna...");
+ loadChannelFromPageUrl(alternateUrl);
+ return true;
+ }
+
+ private String buildAlternateGlobalUrl(String url) {
+ if (url == null || url.isEmpty()) {
+ return null;
+ }
+ if (url.contains("global2.php")) {
+ return url.replace("global2.php", "global1.php");
+ }
+ if (url.contains("global1.php")) {
+ return url.replace("global1.php", "global2.php");
+ }
+ return null;
+ }
+
private void showLoading(boolean show) {
loadingIndicator.setVisibility(show ? View.VISIBLE : View.GONE);
errorMessage.setVisibility(View.GONE);
- playerView.setVisibility(show ? View.GONE : View.VISIBLE);
+ playerView.setVisibility(View.VISIBLE);
if (show) {
setOverlayVisible(true);
}
@@ -232,85 +365,20 @@ public class PlayerActivity extends AppCompatActivity {
}
private void releasePlayer() {
+ cancelStartupTimeout();
+ playbackStarted = false;
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 {
- final TrustManager[] trustAllCerts = new TrustManager[]{
- new X509TrustManager() {
- @Override
- public void checkClientTrusted(X509Certificate[] chain, String authType) {}
- @Override
- public void checkServerTrusted(X509Certificate[] chain, String authType) {}
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[]{};
- }
- }
- };
-
- final SSLContext sslContext = SSLContext.getInstance("SSL");
- sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
-
- OkHttpClient.Builder builder = new OkHttpClient.Builder()
- .connectTimeout(20, TimeUnit.SECONDS)
- .readTimeout(30, TimeUnit.SECONDS)
- .retryOnConnectionFailure(true)
- .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0])
- .hostnameVerifier((hostname, session) -> true);
-
- OkHttpClient bootstrap = builder.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 = builder.dns(dohDns).build();
- } catch (Exception e) {
- okHttpClient = new OkHttpClient.Builder()
- .connectTimeout(20, TimeUnit.SECONDS)
- .readTimeout(30, TimeUnit.SECONDS)
- .retryOnConnectionFailure(true)
- .build();
- }
-
- return okHttpClient;
+ playerView.setPlayer(null);
}
@Override
protected void onStart() {
super.onStart();
- if (player != null) {
- playerView.onResume();
- } else if (channelUrl != null) {
- loadChannel();
+ if (player != null && !player.isPlaying()) {
+ player.play();
}
}
@@ -318,7 +386,7 @@ public class PlayerActivity extends AppCompatActivity {
protected void onResume() {
super.onResume();
if (player != null) {
- playerView.onResume();
+ player.play();
}
}
@@ -326,14 +394,14 @@ public class PlayerActivity extends AppCompatActivity {
protected void onPause() {
super.onPause();
if (player != null) {
- playerView.onPause();
+ player.pause();
}
}
@Override
protected void onStop() {
super.onStop();
- releasePlayer();
+ // Keep player for quick resume.
}
@Override
diff --git a/app/src/main/java/com/streamplayer/StreamUrlResolver.java b/app/src/main/java/com/streamplayer/StreamUrlResolver.java
index 26b7dfe..07be1d9 100644
--- a/app/src/main/java/com/streamplayer/StreamUrlResolver.java
+++ b/app/src/main/java/com/streamplayer/StreamUrlResolver.java
@@ -1,8 +1,17 @@
package com.streamplayer;
+import android.util.Base64;
+
import java.io.IOException;
import java.net.InetAddress;
import java.security.cert.X509Certificate;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -11,6 +20,7 @@ import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
+import okhttp3.Dns;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -19,16 +29,52 @@ import okhttp3.dnsoverhttps.DnsOverHttps;
/**
* Resuelve la URL real del stream extrayendo playbackURL de la página.
- * Utiliza DNS de Google para evitar bloqueos.
+ * Utiliza DNS over HTTPS (Google + Cloudflare) para evitar bloqueos.
+ * Soporta múltiples formatos de páginas y streams directos.
+ * Soporta JWPlayer con DRM ClearKey.
*/
public final class StreamUrlResolver {
- // Patrón para extraer la URL del stream directamente
- private static final Pattern PLAYBACK_URL_PATTERN =
+ // Patrón original para streamtp10.com
+ private static final Pattern PLAYBACK_URL_PATTERN =
Pattern.compile("var\\s+playbackURL\\s*=\\s*[\"']([^\"']+)[\"']");
-
+
+ // Patrón para source src en tags video
+ private static final Pattern VIDEO_SOURCE_PATTERN =
+ Pattern.compile("]+src=[\"']([^\"']+)[\"']");
+
+ // Patrón para URLs M3U8 en cualquier parte del HTML
+ private static final Pattern M3U8_URL_PATTERN =
+ Pattern.compile("(https?://[^\\s'\"<>]+\\.m3u8[^\\s'\"<>]*)");
+
+ // Patrón para URLs de stream en comillas dobles o simples
+ private static final Pattern STREAM_URL_PATTERN =
+ Pattern.compile("['\"](https?://[^'\"<>\\s]+?\\.(?:m3u8|mp4|ts)[^'\"<>\\s]*)['\"]");
+
+ // Patrón para file: o url: en JavaScript
+ private static final Pattern JS_URL_PATTERN =
+ Pattern.compile("(?:file|url|stream|source)\\s*[:=]\\s*[\"'](https?://[^\"']+)[\"']",
+ Pattern.CASE_INSENSITIVE);
+
+ // Patrón para JWPlayer sources con "file": "url"
+ private static final Pattern JWPLAYER_FILE_PATTERN =
+ Pattern.compile("\"file\"\\s*:\\s*\"([^\"]+\\.m3u8[^\"]*)\"");
+
+ // Patrón para pares [indice, "base64"] del nuevo playbackURL ofuscado
+ private static final Pattern OBFUSCATED_PAIR_PATTERN =
+ Pattern.compile("\\[(\\d+)\\s*,\\s*[\"']([^\"']+)[\"']\\]");
+
+ // Patrón para k = fn1() + fn2()
+ private static final Pattern OBFUSCATED_K_PATTERN =
+ Pattern.compile("var\\s+k\\s*=\\s*([A-Za-z_$][\\w$]*)\\(\\)\\s*\\+\\s*([A-Za-z_$][\\w$]*)\\(\\)");
+
+ // Patrón para function fn() { return 12345; }
+ private static final Pattern JS_RETURN_NUMBER_FUNCTION_PATTERN =
+ Pattern.compile("function\\s+([A-Za-z_$][\\w$]*)\\s*\\(\\)\\s*\\{\\s*return\\s*(\\d+)\\s*;?\\s*\\}",
+ Pattern.DOTALL);
+
private static final String USER_AGENT = "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36";
-
+
private static final OkHttpClient CLIENT;
static {
@@ -58,18 +104,49 @@ public final class StreamUrlResolver {
.hostnameVerifier((hostname, session) -> true);
OkHttpClient bootstrap = new OkHttpClient.Builder()
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(10, TimeUnit.SECONDS)
.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0])
.hostnameVerifier((hostname, session) -> true)
.build();
- DnsOverHttps dns = new DnsOverHttps.Builder()
+ final DnsOverHttps googleDns = 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"))
+ .includeIPv6(false)
.build();
- builder.dns(dns);
+
+ final DnsOverHttps cloudflareDns = new DnsOverHttps.Builder()
+ .client(bootstrap)
+ .url(HttpUrl.get("https://cloudflare-dns.com/dns-query"))
+ .bootstrapDnsHosts(
+ InetAddress.getByName("1.1.1.1"),
+ InetAddress.getByName("1.0.0.1"))
+ .includeIPv6(false)
+ .build();
+
+ builder.dns(hostname -> {
+ try {
+ List result = googleDns.lookup(hostname);
+ if (result != null && !result.isEmpty()) {
+ return result;
+ }
+ } catch (Exception ignored) {
+ }
+
+ try {
+ List result = cloudflareDns.lookup(hostname);
+ if (result != null && !result.isEmpty()) {
+ return result;
+ }
+ } catch (Exception ignored) {
+ }
+
+ return Dns.SYSTEM.lookup(hostname);
+ });
client = builder.build();
} catch (Exception e) {
@@ -77,6 +154,7 @@ public final class StreamUrlResolver {
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.followRedirects(true)
+ .dns(Dns.SYSTEM)
.build();
}
CLIENT = client;
@@ -86,20 +164,251 @@ public final class StreamUrlResolver {
}
public static String resolve(String pageUrl) throws IOException {
- String html = downloadPage(pageUrl);
-
- // Buscar playbackURL directamente en el HTML
- Matcher matcher = PLAYBACK_URL_PATTERN.matcher(html);
- if (matcher.find()) {
- String url = matcher.group(1);
- if (url != null && !url.isEmpty() && url.startsWith("http")) {
- return url;
- }
+ // Primero verificar si la URL ya parece ser un stream directo
+ if (isDirectStreamUrl(pageUrl)) {
+ return pageUrl;
}
-
+
+ String html = downloadPage(pageUrl);
+
+ // Si el contenido ya parece ser un stream M3U8, retornarlo directamente
+ if (html.startsWith("#EXTM3U") || html.startsWith("#EXT")) {
+ return pageUrl;
+ }
+
+ // Intentar múltiples patrones de extracción
+ String streamUrl = null;
+
+ // 1. Patrón original: var playbackURL = "..."
+ streamUrl = extractWithPattern(html, PLAYBACK_URL_PATTERN);
+ if (isValidStreamUrl(streamUrl)) {
+ return streamUrl;
+ }
+
+ // 2. Patrón:
+ streamUrl = extractWithPattern(html, VIDEO_SOURCE_PATTERN);
+ if (isValidStreamUrl(streamUrl)) {
+ return streamUrl;
+ }
+
+ // 3. Patrón: URLs M3U8 directas
+ streamUrl = extractWithPattern(html, M3U8_URL_PATTERN);
+ if (isValidStreamUrl(streamUrl)) {
+ return streamUrl;
+ }
+
+ // 4. Patrón: URLs de stream en comillas
+ streamUrl = extractWithPattern(html, STREAM_URL_PATTERN);
+ if (isValidStreamUrl(streamUrl)) {
+ return streamUrl;
+ }
+
+ // 5. Patrón: JavaScript file: / url: / stream:
+ streamUrl = extractWithPattern(html, JS_URL_PATTERN);
+ if (isValidStreamUrl(streamUrl)) {
+ return streamUrl;
+ }
+
+ // 6. Patrón: JWPlayer "file": "url.m3u8" (para Disney+ y otros)
+ streamUrl = extractWithPattern(html, JWPLAYER_FILE_PATTERN);
+ if (isValidStreamUrl(streamUrl)) {
+ return streamUrl;
+ }
+
+ // 7. Nuevo formato ofuscado: playbackURL generado con atob + fromCharCode
+ streamUrl = decodeObfuscatedPlaybackUrl(html);
+ if (isValidStreamUrl(streamUrl)) {
+ return streamUrl;
+ }
+
+ // Si no encontramos nada con patrones, intentar usar la URL original
+ // como stream directo (útil para URLs que ya son streams)
+ if (html.contains(".m3u8") || html.contains("stream") || html.contains("video")) {
+ return pageUrl;
+ }
+
+ // Último recurso: si la URL viene de sudamericaplay.com o similares,
+ // intentar usarla directamente
+ if (pageUrl.contains("sudamericaplay.com") ||
+ pageUrl.contains("disney") ||
+ pageUrl.contains("paramount")) {
+ return pageUrl;
+ }
+
// Si no encontramos la URL, mostrar un fragmento del HTML para debug
String preview = html.length() > 500 ? html.substring(0, 500) : html;
- throw new IOException("No se encontró la URL del stream en la página. Vista previa: " + preview);
+ throw new IOException("No se encontró la URL del stream en la página. URL: " + pageUrl + ". Vista previa: " + preview);
+ }
+
+ /**
+ * Decodifica páginas donde playbackURL se arma carácter por carácter con:
+ * playbackURL += String.fromCharCode(parseInt(atob(v).replace(/\D/g,'')) - k)
+ */
+ private static String decodeObfuscatedPlaybackUrl(String html) {
+ if (html == null ||
+ !html.contains("var playbackURL") ||
+ !html.contains("playbackURL+=") ||
+ !html.contains("String.fromCharCode") ||
+ !html.contains("atob(")) {
+ return null;
+ }
+
+ int scriptStart = html.indexOf("var playbackURL");
+ if (scriptStart < 0) {
+ return null;
+ }
+
+ int scriptEnd = html.indexOf("var p2pConfig", scriptStart);
+ if (scriptEnd < 0) {
+ scriptEnd = html.indexOf("", scriptStart);
+ }
+ if (scriptEnd < 0 || scriptEnd <= scriptStart) {
+ scriptEnd = Math.min(html.length(), scriptStart + 20000);
+ }
+
+ String script = html.substring(scriptStart, scriptEnd);
+
+ Matcher kMatcher = OBFUSCATED_K_PATTERN.matcher(script);
+ if (!kMatcher.find()) {
+ return null;
+ }
+
+ String functionA = kMatcher.group(1);
+ String functionB = kMatcher.group(2);
+
+ Map functionValues = new HashMap<>();
+ Matcher functionMatcher = JS_RETURN_NUMBER_FUNCTION_PATTERN.matcher(script);
+ while (functionMatcher.find()) {
+ try {
+ functionValues.put(functionMatcher.group(1), Long.parseLong(functionMatcher.group(2)));
+ } catch (NumberFormatException ignored) {
+ }
+ }
+
+ Long valueA = functionValues.get(functionA);
+ Long valueB = functionValues.get(functionB);
+ if (valueA == null || valueB == null) {
+ return null;
+ }
+
+ long k = valueA + valueB;
+
+ List pairs = new ArrayList<>();
+ Matcher pairMatcher = OBFUSCATED_PAIR_PATTERN.matcher(script);
+ while (pairMatcher.find()) {
+ try {
+ int index = Integer.parseInt(pairMatcher.group(1));
+ pairs.add(new EncodedPair(index, pairMatcher.group(2)));
+ } catch (NumberFormatException ignored) {
+ }
+ }
+
+ if (pairs.isEmpty()) {
+ return null;
+ }
+
+ Collections.sort(pairs, Comparator.comparingInt(pair -> pair.index));
+ StringBuilder decoded = new StringBuilder(pairs.size());
+
+ for (EncodedPair pair : pairs) {
+ byte[] decodedBytes;
+ try {
+ decodedBytes = Base64.decode(pair.encodedValue, Base64.DEFAULT);
+ } catch (IllegalArgumentException e) {
+ continue;
+ }
+
+ String decodedText = new String(decodedBytes, StandardCharsets.UTF_8);
+ String digits = decodedText.replaceAll("\\D", "");
+ if (digits.isEmpty()) {
+ continue;
+ }
+
+ long numericValue;
+ try {
+ numericValue = Long.parseLong(digits);
+ } catch (NumberFormatException e) {
+ continue;
+ }
+
+ long charCode = numericValue - k;
+ if (charCode < 0 || charCode > Character.MAX_VALUE) {
+ return null;
+ }
+
+ decoded.append((char) charCode);
+ }
+
+ if (decoded.length() == 0) {
+ return null;
+ }
+
+ String url = decoded.toString().trim()
+ .replace("\\/", "/")
+ .replace("\\u0026", "&")
+ .replace("\\u002F", "/");
+
+ return isValidStreamUrl(url) ? url : null;
+ }
+
+ /**
+ * Verifica si una URL parece ser un stream directo (M3U8, MP4, etc.)
+ */
+ private static boolean isDirectStreamUrl(String url) {
+ if (url == null || url.isEmpty()) {
+ return false;
+ }
+ String lower = url.toLowerCase();
+ return lower.contains(".m3u8") ||
+ lower.contains(".mpd") ||
+ lower.contains("stream") && lower.contains(".php") == false ||
+ lower.endsWith(".mp4") ||
+ lower.endsWith(".ts");
+ }
+
+ /**
+ * Verifica si una URL extraída es válida
+ */
+ private static boolean isValidStreamUrl(String url) {
+ return url != null && !url.isEmpty() && url.startsWith("http");
+ }
+
+ /**
+ * Extrae la primera coincidencia de un patrón regex
+ */
+ private static String extractWithPattern(String html, Pattern pattern) {
+ Matcher matcher = pattern.matcher(html);
+ if (matcher.find()) {
+ String url = matcher.group(1);
+ // Limpiar URL de caracteres basura
+ if (url != null) {
+ url = url.trim();
+ // Remover caracteres especiales al final
+ url = url.replaceAll("[\"'<>\\s].*$", "");
+ }
+ return url;
+ }
+ return null;
+ }
+
+ /**
+ * Intenta determinar el tipo de contenido del HTML
+ */
+ private static String getContentTypeFromHtml(String html) {
+ if (html == null || html.isEmpty()) {
+ return "unknown";
+ }
+ String trimmed = html.trim();
+ if (trimmed.startsWith("#EXTM3U") || trimmed.startsWith("#EXT")) {
+ return "m3u8";
+ }
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
+ return "json";
+ }
+ if (trimmed.startsWith("<")) {
+ return "html";
+ }
+ return "text";
}
private static String downloadPage(String pageUrl) throws IOException {
@@ -122,4 +431,14 @@ public final class StreamUrlResolver {
return response.body().string();
}
}
+
+ private static final class EncodedPair {
+ final int index;
+ final String encodedValue;
+
+ EncodedPair(int index, String encodedValue) {
+ this.index = index;
+ this.encodedValue = encodedValue;
+ }
+ }
}
diff --git a/app/src/main/java/com/streamplayer/VlcDrmManager.java b/app/src/main/java/com/streamplayer/VlcDrmManager.java
new file mode 100644
index 0000000..8050fc6
--- /dev/null
+++ b/app/src/main/java/com/streamplayer/VlcDrmManager.java
@@ -0,0 +1,66 @@
+package com.streamplayer;
+
+import org.videolan.libvlc.Media;
+
+import java.util.Map;
+
+/**
+ * Handles DRM configuration for VLC Media Player
+ * Supports ClearKey DRM for Disney+ and other streaming services
+ */
+public class VlcDrmManager {
+
+ // ClearKey DRM configuration
+ private static final String CLEARKEY_KEY_SYSTEM = "org.w3.clearkey";
+
+ /**
+ * Configure ClearKey DRM for a Media object
+ * @param media The VLC Media object
+ * @param keyId The key ID (extracted from manifest or license server)
+ * @param key The content key
+ */
+ public static void configureClearKey(Media media, String keyId, String key) {
+ if (media == null || keyId == null || key == null) {
+ return;
+ }
+
+ // VLC uses a specific format for ClearKey
+ // Format: keyid:key
+ String keyPair = keyId + ":" + key;
+ media.addOption(":demux=avformat");
+ media.addOption(":key-format=" + CLEARKEY_KEY_SYSTEM);
+ media.addOption(":key=" + keyPair);
+ }
+
+ /**
+ * Configure ClearKey DRM with multiple keys
+ * @param media The VLC Media object
+ * @param keys Map of keyId -> key pairs
+ */
+ public static void configureClearKey(Media media, Map keys) {
+ if (media == null || keys == null || keys.isEmpty()) {
+ return;
+ }
+
+ media.addOption(":demux=avformat");
+ media.addOption(":key-format=" + CLEARKEY_KEY_SYSTEM);
+
+ for (Map.Entry entry : keys.entrySet()) {
+ String keyPair = entry.getKey() + ":" + entry.getValue();
+ media.addOption(":key=" + keyPair);
+ }
+ }
+
+ /**
+ * Configure Widevine DRM (for reference, if needed later)
+ * VLC supports Widevine via specific options
+ */
+ public static void configureWidevine(Media media, String drmServerUrl) {
+ if (media == null || drmServerUrl == null) {
+ return;
+ }
+
+ media.addOption(":drm=widevine");
+ media.addOption(":aes-key=" + drmServerUrl);
+ }
+}
diff --git a/app/src/main/java/com/streamplayer/VlcPlayerConfig.java b/app/src/main/java/com/streamplayer/VlcPlayerConfig.java
new file mode 100644
index 0000000..a3851b7
--- /dev/null
+++ b/app/src/main/java/com/streamplayer/VlcPlayerConfig.java
@@ -0,0 +1,29 @@
+package com.streamplayer;
+
+/**
+ * Configuration constants for VLC Player
+ */
+public class VlcPlayerConfig {
+
+ // Network caching (ms)
+ public static final int NETWORK_CACHING = 1500;
+
+ // Live streaming caching (ms)
+ public static final int LIVE_CACHING = 5000;
+
+ // User Agent
+ public static final String USER_AGENT =
+ "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36";
+
+ // Hardware acceleration
+ public static final String HW_ACCELERATION = "automatic";
+
+ // Chroma format
+ public static final String CHROMA = "RV32";
+
+ // Audio output
+ public static final String AUDIO_OUTPUT = "opensles";
+
+ // Maximum retries for playback
+ public static final int MAX_RETRIES = 3;
+}
diff --git a/app/src/main/res/layout/activity_player.xml b/app/src/main/res/layout/activity_player.xml
index c209d18..e9ee77f 100644
--- a/app/src/main/res/layout/activity_player.xml
+++ b/app/src/main/res/layout/activity_player.xml
@@ -11,8 +11,10 @@
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
- app:resize_mode="fill"
- app:use_controller="true" />
+ android:keepScreenOn="true"
+ app:show_buffering="never"
+ app:surface_type="surface_view"
+ app:use_controller="false" />