# 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 protected 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 Example **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 protected 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 protected streaming 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 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