Files
app/VLC_MIGRATION_PLAN.md

19 KiB

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:

// 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:

// 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):

<androidx.media3.ui.PlayerView
    android:id="@+id/player_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:resize_mode="fill"
    app:use_controller="true" />

Replace with VLC SurfaceView:

<org.videolan.libvlc.util.VLCVideoLayout
    android:id="@+id/player_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

<!-- OR use SurfaceView directly for more control -->
<SurfaceView
    android:id="@+id/player_surface"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

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:

// 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):

// 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):

// 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)

private void startPlayback(String streamUrl) {
    try {
        releasePlayer();
        lastStreamUrl = streamUrl;
        retryCount = 0;

        // Create LibVLC instance with options
        ArrayList<String> 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<String, String> 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()

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)

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):

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:

@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

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<String, String> keys) {
        if (media == null || keys == null || keys.isEmpty()) {
            return;
        }

        media.addOption(":demux=avformat");
        media.addOption(":key-format=" + CLEARKEY_KEY_SYSTEM);

        for (Map.Entry<String, String> 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:

/**
 * Extract ClearKey DRM keys from JSON in HTML
 * @return Map of keyId -> key pairs, or null if no DRM found
 */
public static Map<String, String> extractClearKeyKeys(String html) {
    Map<String, String> 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:

// After loading stream URL, also check for DRM keys
new Thread(() -> {
    try {
        String resolvedUrl = StreamUrlResolver.resolve(channelUrl);
        Map<String, String> 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<String, String> 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

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=<value>
    • :http-referrer=<value>
    • :http-cookie=<value>

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