fix: support streamtp event playback on tv

This commit is contained in:
Renato
2026-03-11 15:58:03 -03:00
parent 49ed737663
commit 3ca31c70b3
4 changed files with 298 additions and 57 deletions

View File

@@ -48,7 +48,9 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.media3:media3-exoplayer:1.4.1' implementation 'androidx.media3:media3-exoplayer:1.4.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.4.1' implementation 'androidx.media3:media3-exoplayer-hls:1.4.1'
implementation 'androidx.media3:media3-exoplayer-dash:1.4.1'
implementation 'androidx.media3:media3-ui:1.4.1' implementation 'androidx.media3:media3-ui:1.4.1'
implementation 'androidx.media3:media3-datasource-okhttp:1.4.1'
// OkHttp con DNS over HTTPS (para StreamUrlResolver) // OkHttp con DNS over HTTPS (para StreamUrlResolver)
implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.squareup.okhttp3:okhttp:4.12.0'

View File

@@ -8,7 +8,6 @@ import java.util.List;
public final class ChannelRepository { public final class ChannelRepository {
private static final List<StreamChannel> CHANNELS = createChannels();
private static final Comparator<StreamChannel> CHANNEL_NAME_COMPARATOR = private static final Comparator<StreamChannel> CHANNEL_NAME_COMPARATOR =
new Comparator<StreamChannel>() { new Comparator<StreamChannel>() {
@Override @Override
@@ -16,6 +15,7 @@ public final class ChannelRepository {
return String.CASE_INSENSITIVE_ORDER.compare(left.getName(), right.getName()); return String.CASE_INSENSITIVE_ORDER.compare(left.getName(), right.getName());
} }
}; };
private static final List<StreamChannel> CHANNELS = createChannels();
private static List<StreamChannel> createChannels() { private static List<StreamChannel> createChannels() {
List<StreamChannel> channels = new ArrayList<>(Arrays.asList( List<StreamChannel> channels = new ArrayList<>(Arrays.asList(

View File

@@ -5,6 +5,7 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Base64;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
@@ -14,16 +15,23 @@ import android.widget.TextView;
import androidx.annotation.OptIn; import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.datasource.HttpDataSource;
import androidx.media3.datasource.okhttp.OkHttpDataSource;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.hls.HlsMediaSource; import androidx.media3.exoplayer.drm.DefaultDrmSessionManager;
import androidx.media3.exoplayer.drm.FrameworkMediaDrm;
import androidx.media3.exoplayer.drm.LocalMediaDrmCallback;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.ui.PlayerView; import androidx.media3.ui.PlayerView;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@@ -48,7 +56,7 @@ public class PlayerActivity extends AppCompatActivity {
private String channelUrl; private String channelUrl;
private boolean overlayVisible = true; private boolean overlayVisible = true;
private int retryCount = 0; private int retryCount = 0;
private String lastStreamUrl; private StreamUrlResolver.ResolvedStream lastResolvedStream;
private String currentChannelPageUrl; private String currentChannelPageUrl;
private boolean playbackStarted = false; private boolean playbackStarted = false;
private boolean alternateSourceAttempted = false; private boolean alternateSourceAttempted = false;
@@ -118,14 +126,17 @@ public class PlayerActivity extends AppCompatActivity {
new Thread(() -> { new Thread(() -> {
try { try {
String resolvedUrl = StreamUrlResolver.resolve(pageUrl); StreamUrlResolver.ResolvedStream resolvedStream = StreamUrlResolver.resolve(pageUrl);
Log.d(TAG, "Stream resuelto: " + resolvedUrl + " (req=" + requestGeneration + ")"); Log.d(TAG, "Stream resuelto: " + resolvedStream.getStreamUrl()
+ " | mime=" + resolvedStream.getMimeType()
+ " | drm=" + resolvedStream.hasClearKey()
+ " (req=" + requestGeneration + ")");
runOnUiThread(() -> { runOnUiThread(() -> {
if (!isLatestResolveRequest(requestGeneration)) { if (!isLatestResolveRequest(requestGeneration)) {
Log.d(TAG, "Ignorando resultado viejo (req=" + requestGeneration + ")"); Log.d(TAG, "Ignorando resultado viejo (req=" + requestGeneration + ")");
return; return;
} }
startPlayback(resolvedUrl); startPlayback(resolvedStream);
}); });
} catch (IOException e) { } catch (IOException e) {
runOnUiThread(() -> { runOnUiThread(() -> {
@@ -155,28 +166,18 @@ public class PlayerActivity extends AppCompatActivity {
} }
} }
private void startPlayback(String streamUrl) { private void startPlayback(StreamUrlResolver.ResolvedStream resolvedStream) {
try { try {
releasePlayer(); releasePlayer();
lastStreamUrl = streamUrl; lastResolvedStream = resolvedStream;
retryCount = 0; retryCount = 0;
playbackStarted = false; playbackStarted = false;
scheduleStartupTimeout(); scheduleStartupTimeout();
Log.d(TAG, "Iniciando reproducción: " + streamUrl); Log.d(TAG, "Iniciando reproducción: " + resolvedStream.getStreamUrl()
+ " | mime=" + resolvedStream.getMimeType()
+ " | drm=" + resolvedStream.hasClearKey());
DefaultHttpDataSource.Factory httpFactory = new DefaultHttpDataSource.Factory() MediaSource mediaSource = buildMediaSource(resolvedStream);
.setUserAgent(VlcPlayerConfig.USER_AGENT)
.setAllowCrossProtocolRedirects(true)
.setConnectTimeoutMs(15000)
.setReadTimeoutMs(20000);
Map<String, String> 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(); player = new ExoPlayer.Builder(this).build();
playerView.setPlayer(player); playerView.setPlayer(player);
@@ -195,6 +196,85 @@ public class PlayerActivity extends AppCompatActivity {
} }
} }
private MediaSource buildMediaSource(StreamUrlResolver.ResolvedStream resolvedStream) {
HttpDataSource.Factory httpFactory = createHttpDataSourceFactory(currentChannelPageUrl);
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder()
.setUri(Uri.parse(resolvedStream.getStreamUrl()))
.setMimeType(resolvedStream.getMimeType());
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(httpFactory);
if (resolvedStream.hasClearKey()) {
mediaItemBuilder.setDrmConfiguration(
new MediaItem.DrmConfiguration.Builder(C.CLEARKEY_UUID).build());
DefaultDrmSessionManager drmSessionManager = new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(C.CLEARKEY_UUID, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(new LocalMediaDrmCallback(buildClearKeyLicenseResponse(
resolvedStream.getClearKeyIdHex(),
resolvedStream.getClearKeyHex())));
mediaSourceFactory.setDrmSessionManagerProvider(mediaItem -> drmSessionManager);
}
return mediaSourceFactory.createMediaSource(mediaItemBuilder.build());
}
private HttpDataSource.Factory createHttpDataSourceFactory(String pageUrl) {
Map<String, String> headers = new HashMap<>();
headers.put("User-Agent", VlcPlayerConfig.USER_AGENT);
headers.put("Accept", "*/*");
String origin = buildOrigin(pageUrl);
if (origin != null) {
headers.put("Origin", origin);
headers.put("Referer", origin + "/");
}
return new OkHttpDataSource.Factory(NetworkUtils.getClient())
.setUserAgent(VlcPlayerConfig.USER_AGENT)
.setDefaultRequestProperties(headers);
}
private String buildOrigin(String pageUrl) {
if (pageUrl == null || pageUrl.isEmpty()) {
return null;
}
Uri uri = Uri.parse(pageUrl);
if (uri.getScheme() == null || uri.getHost() == null) {
return null;
}
StringBuilder origin = new StringBuilder()
.append(uri.getScheme())
.append("://")
.append(uri.getHost());
if (uri.getPort() != -1) {
origin.append(":").append(uri.getPort());
}
return origin.toString();
}
private byte[] buildClearKeyLicenseResponse(String keyIdHex, String keyHex) {
String keyIdBase64Url = encodeBase64Url(hexToBytes(keyIdHex));
String keyBase64Url = encodeBase64Url(hexToBytes(keyHex));
String response = "{\"keys\":[{\"k\":\"" + keyBase64Url
+ "\",\"kid\":\"" + keyIdBase64Url
+ "\",\"kty\":\"oct\"}],\"type\":\"temporary\"}";
return response.getBytes(StandardCharsets.UTF_8);
}
private byte[] hexToBytes(String value) {
int length = value.length();
byte[] bytes = new byte[length / 2];
for (int i = 0; i < length; i += 2) {
bytes[i / 2] = (byte) Integer.parseInt(value.substring(i, i + 2), 16);
}
return bytes;
}
private String encodeBase64Url(byte[] value) {
return Base64.encodeToString(value, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE);
}
private void setupPlayerListener() { private void setupPlayerListener() {
if (player == null) { if (player == null) {
return; return;
@@ -278,8 +358,8 @@ public class PlayerActivity extends AppCompatActivity {
}); });
mainHandler.postDelayed(() -> { mainHandler.postDelayed(() -> {
if (lastStreamUrl != null) { if (lastResolvedStream != null) {
startPlayback(lastStreamUrl); startPlayback(lastResolvedStream);
} else { } else {
loadChannel(); loadChannel();
} }

View File

@@ -2,6 +2,8 @@ package com.streamplayer;
import android.util.Base64; import android.util.Base64;
import androidx.media3.common.MimeTypes;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
@@ -32,13 +34,13 @@ public final class StreamUrlResolver {
private static final Pattern VIDEO_SOURCE_PATTERN = private static final Pattern VIDEO_SOURCE_PATTERN =
Pattern.compile("<source[^>]+src=[\"']([^\"']+)[\"']"); Pattern.compile("<source[^>]+src=[\"']([^\"']+)[\"']");
// Patrón para URLs M3U8 en cualquier parte del HTML // Patrón para URLs HLS/DASH en cualquier parte del HTML
private static final Pattern M3U8_URL_PATTERN = private static final Pattern STREAM_MANIFEST_URL_PATTERN =
Pattern.compile("(https?://[^\\s'\"<>]+\\.m3u8[^\\s'\"<>]*)"); Pattern.compile("(https?://[^\\s'\"<>]+\\.(?:m3u8|mpd)[^\\s'\"<>]*)");
// Patrón para URLs de stream en comillas dobles o simples // Patrón para URLs de stream en comillas dobles o simples
private static final Pattern STREAM_URL_PATTERN = private static final Pattern STREAM_URL_PATTERN =
Pattern.compile("['\"](https?://[^'\"<>\\s]+?\\.(?:m3u8|mp4|ts)[^'\"<>\\s]*)['\"]"); Pattern.compile("['\"](https?://[^'\"<>\\s]+?\\.(?:m3u8|mpd|mp4|ts)[^'\"<>\\s]*)['\"]");
// Patrón para file: o url: en JavaScript // Patrón para file: o url: en JavaScript
private static final Pattern JS_URL_PATTERN = private static final Pattern JS_URL_PATTERN =
@@ -47,7 +49,8 @@ public final class StreamUrlResolver {
// Patrón para JWPlayer sources con "file": "url" // Patrón para JWPlayer sources con "file": "url"
private static final Pattern JWPLAYER_FILE_PATTERN = private static final Pattern JWPLAYER_FILE_PATTERN =
Pattern.compile("\"file\"\\s*:\\s*\"([^\"]+\\.m3u8[^\"]*)\""); Pattern.compile("\"file\"\\s*:\\s*\"([^\"]+\\.(?:m3u8|mpd)[^\"]*)\"",
Pattern.CASE_INSENSITIVE);
// Patrón para pares [indice, "base64"] del nuevo playbackURL ofuscado // Patrón para pares [indice, "base64"] del nuevo playbackURL ofuscado
private static final Pattern OBFUSCATED_PAIR_PATTERN = private static final Pattern OBFUSCATED_PAIR_PATTERN =
@@ -62,20 +65,32 @@ public final class StreamUrlResolver {
Pattern.compile("function\\s+([A-Za-z_$][\\w$]*)\\s*\\(\\)\\s*\\{\\s*return\\s*(\\d+)\\s*;?\\s*\\}", Pattern.compile("function\\s+([A-Za-z_$][\\w$]*)\\s*\\(\\)\\s*\\{\\s*return\\s*(\\d+)\\s*;?\\s*\\}",
Pattern.DOTALL); Pattern.DOTALL);
// Patrón para IIFEs que calculan k con dos returns inline.
private static final Pattern INLINE_OBFUSCATED_K_PATTERN =
Pattern.compile("k\\s*=\\s*\\(function\\s+[A-Za-z_$][\\w$]*\\(\\)\\s*\\{\\s*return\\s*(\\d+)\\s*\\}\\)\\(\\)\\s*\\+\\s*\\(function\\s+[A-Za-z_$][\\w$]*\\(\\)\\s*\\{\\s*return\\s*(\\d+)\\s*\\}\\)\\(\\)",
Pattern.DOTALL);
private static final Pattern CLEAR_KEY_HEX_PATTERN =
Pattern.compile("^[0-9a-fA-F]{32}$");
private StreamUrlResolver() { private StreamUrlResolver() {
} }
public static String resolve(String pageUrl) throws IOException { public static ResolvedStream resolve(String pageUrl) throws IOException {
// Primero verificar si la URL ya parece ser un stream directo // Primero verificar si la URL ya parece ser un stream directo
if (isDirectStreamUrl(pageUrl)) { if (isDirectStreamUrl(pageUrl)) {
return pageUrl; return ResolvedStream.fromUrl(pageUrl);
} }
String html = downloadPage(pageUrl); String html = downloadPage(pageUrl);
String trimmedHtml = html.trim();
// Si el contenido ya parece ser un stream M3U8, retornarlo directamente // Si el contenido ya es un manifiesto directo, reproducirlo como tal.
if (html.startsWith("#EXTM3U") || html.startsWith("#EXT")) { if (trimmedHtml.startsWith("#EXTM3U") || trimmedHtml.startsWith("#EXT")) {
return pageUrl; return ResolvedStream.hls(pageUrl);
}
if (isDashManifest(trimmedHtml)) {
return ResolvedStream.dash(pageUrl);
} }
// Intentar múltiples patrones de extracción // Intentar múltiples patrones de extracción
@@ -84,56 +99,56 @@ public final class StreamUrlResolver {
// 1. Patrón original: var playbackURL = "..." // 1. Patrón original: var playbackURL = "..."
streamUrl = extractWithPattern(html, PLAYBACK_URL_PATTERN); streamUrl = extractWithPattern(html, PLAYBACK_URL_PATTERN);
if (isValidStreamUrl(streamUrl)) { if (isValidStreamUrl(streamUrl)) {
return streamUrl; return ResolvedStream.fromUrl(streamUrl);
} }
// 2. Patrón: <source src="..."> // 2. Patrón: <source src="...">
streamUrl = extractWithPattern(html, VIDEO_SOURCE_PATTERN); streamUrl = extractWithPattern(html, VIDEO_SOURCE_PATTERN);
if (isValidStreamUrl(streamUrl)) { if (isValidStreamUrl(streamUrl)) {
return streamUrl; return ResolvedStream.fromUrl(streamUrl);
} }
// 3. Patrón: URLs M3U8 directas // 3. Patrón: URLs HLS/DASH directas
streamUrl = extractWithPattern(html, M3U8_URL_PATTERN); streamUrl = extractWithPattern(html, STREAM_MANIFEST_URL_PATTERN);
if (isValidStreamUrl(streamUrl)) { if (isValidStreamUrl(streamUrl)) {
return streamUrl; return ResolvedStream.fromUrl(streamUrl);
} }
// 4. Patrón: URLs de stream en comillas // 4. Patrón: URLs de stream en comillas
streamUrl = extractWithPattern(html, STREAM_URL_PATTERN); streamUrl = extractWithPattern(html, STREAM_URL_PATTERN);
if (isValidStreamUrl(streamUrl)) { if (isValidStreamUrl(streamUrl)) {
return streamUrl; return ResolvedStream.fromUrl(streamUrl);
} }
// 5. Patrón: JavaScript file: / url: / stream: // 5. Patrón: JavaScript file: / url: / stream:
streamUrl = extractWithPattern(html, JS_URL_PATTERN); streamUrl = extractWithPattern(html, JS_URL_PATTERN);
if (isValidStreamUrl(streamUrl)) { if (isValidStreamUrl(streamUrl)) {
return streamUrl; return ResolvedStream.fromUrl(streamUrl);
} }
// 6. Patrón: JWPlayer "file": "url.m3u8" (para reproductores web y otros) // 6. Patrón: JWPlayer "file": "url" (para reproductores web y otros)
streamUrl = extractWithPattern(html, JWPLAYER_FILE_PATTERN); streamUrl = extractWithPattern(html, JWPLAYER_FILE_PATTERN);
if (isValidStreamUrl(streamUrl)) { if (isValidStreamUrl(streamUrl)) {
return streamUrl; return ResolvedStream.fromUrl(streamUrl);
} }
// 7. Nuevo formato ofuscado: playbackURL generado con atob + fromCharCode // 7. Nuevo formato ofuscado: playbackURL generado con atob + fromCharCode
streamUrl = decodeObfuscatedPlaybackUrl(html); streamUrl = decodeObfuscatedPlaybackUrl(html);
if (isValidStreamUrl(streamUrl)) { if (isValidStreamUrl(streamUrl)) {
return streamUrl; return ResolvedStream.fromUrl(streamUrl);
} }
// Si no encontramos nada con patrones, intentar usar la URL original // 8. Eventos "transmision*.php": DASH + ClearKey en variables ofuscadas.
// como stream directo (útil para URLs que ya son streams) ResolvedStream dashClearKeyStream = decodeDashClearKeyStream(html);
if (html.contains(".m3u8") || html.contains("stream") || html.contains("video")) { if (dashClearKeyStream != null) {
return pageUrl; return dashClearKeyStream;
} }
// Último recurso: si la URL viene de sudamericaplay.com o similares, // Último recurso: si la URL viene de sudamericaplay.com o similares,
// intentar usarla directamente // intentar usarla directamente
if (pageUrl.contains("sudamericaplay.com") || if (pageUrl.contains("sudamericaplay.com") ||
pageUrl.contains("paramount")) { pageUrl.contains("paramount")) {
return pageUrl; return ResolvedStream.fromUrl(pageUrl);
} }
// Si no encontramos la URL, mostrar un fragmento del HTML para debug // Si no encontramos la URL, mostrar un fragmento del HTML para debug
@@ -194,6 +209,64 @@ public final class StreamUrlResolver {
long k = valueA + valueB; long k = valueA + valueB;
return decodePairs(extractEncodedPairs(script), k);
}
private static ResolvedStream decodeDashClearKeyStream(String html) {
if (html == null ||
!html.contains("\"type\": \"dash\"") ||
!html.contains("clearkey")) {
return null;
}
String dashUrl = decodeObfuscatedVariable(html, "_u");
String keyId = decodeObfuscatedVariable(html, "_ki");
String key = decodeObfuscatedVariable(html, "_k");
if (!isValidStreamUrl(dashUrl) ||
!isValidClearKeyHex(keyId) ||
!isValidClearKeyHex(key)) {
return null;
}
return ResolvedStream.dashClearKey(dashUrl, keyId, key);
}
private static String decodeObfuscatedVariable(String html, String variableName) {
String marker = "var " + variableName + "='';";
int start = html.indexOf(marker);
if (start < 0) {
return null;
}
int end = html.indexOf("var _", start + marker.length());
if (end < 0) {
end = html.indexOf("var data = jwplayer", start + marker.length());
}
if (end < 0) {
end = html.indexOf("</script>", start + marker.length());
}
if (end <= start) {
return null;
}
String block = html.substring(start, end);
Matcher kMatcher = INLINE_OBFUSCATED_K_PATTERN.matcher(block);
if (!kMatcher.find()) {
return null;
}
long k;
try {
k = Long.parseLong(kMatcher.group(1)) + Long.parseLong(kMatcher.group(2));
} catch (NumberFormatException e) {
return null;
}
return decodePairs(extractEncodedPairs(block), k);
}
private static List<EncodedPair> extractEncodedPairs(String script) {
List<EncodedPair> pairs = new ArrayList<>(); List<EncodedPair> pairs = new ArrayList<>();
Matcher pairMatcher = OBFUSCATED_PAIR_PATTERN.matcher(script); Matcher pairMatcher = OBFUSCATED_PAIR_PATTERN.matcher(script);
while (pairMatcher.find()) { while (pairMatcher.find()) {
@@ -204,18 +277,21 @@ public final class StreamUrlResolver {
} }
} }
if (pairs.isEmpty()) {
return null;
}
Collections.sort(pairs, new Comparator<EncodedPair>() { Collections.sort(pairs, new Comparator<EncodedPair>() {
@Override @Override
public int compare(EncodedPair left, EncodedPair right) { public int compare(EncodedPair left, EncodedPair right) {
return Integer.compare(left.index, right.index); return Integer.compare(left.index, right.index);
} }
}); });
StringBuilder decoded = new StringBuilder(pairs.size()); return pairs;
}
private static String decodePairs(List<EncodedPair> pairs, long k) {
if (pairs.isEmpty()) {
return null;
}
StringBuilder decoded = new StringBuilder(pairs.size());
for (EncodedPair pair : pairs) { for (EncodedPair pair : pairs) {
byte[] decodedBytes; byte[] decodedBytes;
try { try {
@@ -249,12 +325,23 @@ public final class StreamUrlResolver {
return null; return null;
} }
String url = decoded.toString().trim() return decoded.toString().trim()
.replace("\\/", "/") .replace("\\/", "/")
.replace("\\u0026", "&") .replace("\\u0026", "&")
.replace("\\u002F", "/"); .replace("\\u002F", "/");
}
return isValidStreamUrl(url) ? url : null; private static boolean isDashManifest(String body) {
if (body == null || body.isEmpty()) {
return false;
}
String lower = body.toLowerCase(Locale.ROOT);
return lower.startsWith("<mpd") ||
(lower.startsWith("<?xml") && lower.contains("<mpd"));
}
private static boolean isValidClearKeyHex(String value) {
return value != null && CLEAR_KEY_HEX_PATTERN.matcher(value).matches();
} }
/** /**
@@ -327,4 +414,76 @@ public final class StreamUrlResolver {
this.encodedValue = encodedValue; this.encodedValue = encodedValue;
} }
} }
public static final class ResolvedStream {
private final String streamUrl;
private final String mimeType;
private final String clearKeyIdHex;
private final String clearKeyHex;
private ResolvedStream(String streamUrl,
String mimeType,
String clearKeyIdHex,
String clearKeyHex) {
this.streamUrl = streamUrl;
this.mimeType = mimeType;
this.clearKeyIdHex = clearKeyIdHex;
this.clearKeyHex = clearKeyHex;
}
public static ResolvedStream fromUrl(String streamUrl) {
String lower = streamUrl.toLowerCase(Locale.ROOT);
if (lower.contains(".mpd")) {
return dash(streamUrl);
}
if (lower.contains(".mp4")) {
return progressive(streamUrl, MimeTypes.VIDEO_MP4);
}
if (lower.contains(".ts")) {
return progressive(streamUrl, MimeTypes.VIDEO_MP2T);
}
return hls(streamUrl);
}
public static ResolvedStream hls(String streamUrl) {
return new ResolvedStream(streamUrl, MimeTypes.APPLICATION_M3U8, null, null);
}
public static ResolvedStream dash(String streamUrl) {
return new ResolvedStream(streamUrl, MimeTypes.APPLICATION_MPD, null, null);
}
public static ResolvedStream dashClearKey(String streamUrl,
String clearKeyIdHex,
String clearKeyHex) {
return new ResolvedStream(streamUrl,
MimeTypes.APPLICATION_MPD,
clearKeyIdHex,
clearKeyHex);
}
public static ResolvedStream progressive(String streamUrl, String mimeType) {
return new ResolvedStream(streamUrl, mimeType, null, null);
}
public String getStreamUrl() {
return streamUrl;
}
public String getMimeType() {
return mimeType;
}
public String getClearKeyIdHex() {
return clearKeyIdHex;
}
public String getClearKeyHex() {
return clearKeyHex;
}
public boolean hasClearKey() {
return clearKeyIdHex != null && clearKeyHex != null;
}
}
} }