fix: support streamtp event playback on tv
This commit is contained in:
@@ -48,7 +48,9 @@ dependencies {
|
||||
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-exoplayer-dash: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)
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
|
||||
@@ -8,7 +8,6 @@ import java.util.List;
|
||||
|
||||
public final class ChannelRepository {
|
||||
|
||||
private static final List<StreamChannel> CHANNELS = createChannels();
|
||||
private static final Comparator<StreamChannel> CHANNEL_NAME_COMPARATOR =
|
||||
new Comparator<StreamChannel>() {
|
||||
@Override
|
||||
@@ -16,6 +15,7 @@ public final class ChannelRepository {
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(left.getName(), right.getName());
|
||||
}
|
||||
};
|
||||
private static final List<StreamChannel> CHANNELS = createChannels();
|
||||
|
||||
private static List<StreamChannel> createChannels() {
|
||||
List<StreamChannel> channels = new ArrayList<>(Arrays.asList(
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
@@ -14,16 +15,23 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.Player;
|
||||
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.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 java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -48,7 +56,7 @@ public class PlayerActivity extends AppCompatActivity {
|
||||
private String channelUrl;
|
||||
private boolean overlayVisible = true;
|
||||
private int retryCount = 0;
|
||||
private String lastStreamUrl;
|
||||
private StreamUrlResolver.ResolvedStream lastResolvedStream;
|
||||
private String currentChannelPageUrl;
|
||||
private boolean playbackStarted = false;
|
||||
private boolean alternateSourceAttempted = false;
|
||||
@@ -118,14 +126,17 @@ public class PlayerActivity extends AppCompatActivity {
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String resolvedUrl = StreamUrlResolver.resolve(pageUrl);
|
||||
Log.d(TAG, "Stream resuelto: " + resolvedUrl + " (req=" + requestGeneration + ")");
|
||||
StreamUrlResolver.ResolvedStream resolvedStream = StreamUrlResolver.resolve(pageUrl);
|
||||
Log.d(TAG, "Stream resuelto: " + resolvedStream.getStreamUrl()
|
||||
+ " | mime=" + resolvedStream.getMimeType()
|
||||
+ " | drm=" + resolvedStream.hasClearKey()
|
||||
+ " (req=" + requestGeneration + ")");
|
||||
runOnUiThread(() -> {
|
||||
if (!isLatestResolveRequest(requestGeneration)) {
|
||||
Log.d(TAG, "Ignorando resultado viejo (req=" + requestGeneration + ")");
|
||||
return;
|
||||
}
|
||||
startPlayback(resolvedUrl);
|
||||
startPlayback(resolvedStream);
|
||||
});
|
||||
} catch (IOException e) {
|
||||
runOnUiThread(() -> {
|
||||
@@ -155,28 +166,18 @@ public class PlayerActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void startPlayback(String streamUrl) {
|
||||
private void startPlayback(StreamUrlResolver.ResolvedStream resolvedStream) {
|
||||
try {
|
||||
releasePlayer();
|
||||
lastStreamUrl = streamUrl;
|
||||
lastResolvedStream = resolvedStream;
|
||||
retryCount = 0;
|
||||
playbackStarted = false;
|
||||
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()
|
||||
.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)));
|
||||
MediaSource mediaSource = buildMediaSource(resolvedStream);
|
||||
|
||||
player = new ExoPlayer.Builder(this).build();
|
||||
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() {
|
||||
if (player == null) {
|
||||
return;
|
||||
@@ -278,8 +358,8 @@ public class PlayerActivity extends AppCompatActivity {
|
||||
});
|
||||
|
||||
mainHandler.postDelayed(() -> {
|
||||
if (lastStreamUrl != null) {
|
||||
startPlayback(lastStreamUrl);
|
||||
if (lastResolvedStream != null) {
|
||||
startPlayback(lastResolvedStream);
|
||||
} else {
|
||||
loadChannel();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.streamplayer;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.media3.common.MimeTypes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
@@ -32,13 +34,13 @@ public final class StreamUrlResolver {
|
||||
private static final Pattern VIDEO_SOURCE_PATTERN =
|
||||
Pattern.compile("<source[^>]+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 HLS/DASH en cualquier parte del HTML
|
||||
private static final Pattern STREAM_MANIFEST_URL_PATTERN =
|
||||
Pattern.compile("(https?://[^\\s'\"<>]+\\.(?:m3u8|mpd)[^\\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]*)['\"]");
|
||||
Pattern.compile("['\"](https?://[^'\"<>\\s]+?\\.(?:m3u8|mpd|mp4|ts)[^'\"<>\\s]*)['\"]");
|
||||
|
||||
// Patrón para file: o url: en JavaScript
|
||||
private static final Pattern JS_URL_PATTERN =
|
||||
@@ -47,7 +49,8 @@ public final class StreamUrlResolver {
|
||||
|
||||
// Patrón para JWPlayer sources con "file": "url"
|
||||
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
|
||||
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.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() {
|
||||
}
|
||||
|
||||
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
|
||||
if (isDirectStreamUrl(pageUrl)) {
|
||||
return pageUrl;
|
||||
return ResolvedStream.fromUrl(pageUrl);
|
||||
}
|
||||
|
||||
String html = downloadPage(pageUrl);
|
||||
String trimmedHtml = html.trim();
|
||||
|
||||
// Si el contenido ya parece ser un stream M3U8, retornarlo directamente
|
||||
if (html.startsWith("#EXTM3U") || html.startsWith("#EXT")) {
|
||||
return pageUrl;
|
||||
// Si el contenido ya es un manifiesto directo, reproducirlo como tal.
|
||||
if (trimmedHtml.startsWith("#EXTM3U") || trimmedHtml.startsWith("#EXT")) {
|
||||
return ResolvedStream.hls(pageUrl);
|
||||
}
|
||||
if (isDashManifest(trimmedHtml)) {
|
||||
return ResolvedStream.dash(pageUrl);
|
||||
}
|
||||
|
||||
// Intentar múltiples patrones de extracción
|
||||
@@ -84,56 +99,56 @@ public final class StreamUrlResolver {
|
||||
// 1. Patrón original: var playbackURL = "..."
|
||||
streamUrl = extractWithPattern(html, PLAYBACK_URL_PATTERN);
|
||||
if (isValidStreamUrl(streamUrl)) {
|
||||
return streamUrl;
|
||||
return ResolvedStream.fromUrl(streamUrl);
|
||||
}
|
||||
|
||||
// 2. Patrón: <source src="...">
|
||||
streamUrl = extractWithPattern(html, VIDEO_SOURCE_PATTERN);
|
||||
if (isValidStreamUrl(streamUrl)) {
|
||||
return streamUrl;
|
||||
return ResolvedStream.fromUrl(streamUrl);
|
||||
}
|
||||
|
||||
// 3. Patrón: URLs M3U8 directas
|
||||
streamUrl = extractWithPattern(html, M3U8_URL_PATTERN);
|
||||
// 3. Patrón: URLs HLS/DASH directas
|
||||
streamUrl = extractWithPattern(html, STREAM_MANIFEST_URL_PATTERN);
|
||||
if (isValidStreamUrl(streamUrl)) {
|
||||
return streamUrl;
|
||||
return ResolvedStream.fromUrl(streamUrl);
|
||||
}
|
||||
|
||||
// 4. Patrón: URLs de stream en comillas
|
||||
streamUrl = extractWithPattern(html, STREAM_URL_PATTERN);
|
||||
if (isValidStreamUrl(streamUrl)) {
|
||||
return streamUrl;
|
||||
return ResolvedStream.fromUrl(streamUrl);
|
||||
}
|
||||
|
||||
// 5. Patrón: JavaScript file: / url: / stream:
|
||||
streamUrl = extractWithPattern(html, JS_URL_PATTERN);
|
||||
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);
|
||||
if (isValidStreamUrl(streamUrl)) {
|
||||
return streamUrl;
|
||||
return ResolvedStream.fromUrl(streamUrl);
|
||||
}
|
||||
|
||||
// 7. Nuevo formato ofuscado: playbackURL generado con atob + fromCharCode
|
||||
streamUrl = decodeObfuscatedPlaybackUrl(html);
|
||||
if (isValidStreamUrl(streamUrl)) {
|
||||
return streamUrl;
|
||||
return ResolvedStream.fromUrl(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;
|
||||
// 8. Eventos "transmision*.php": DASH + ClearKey en variables ofuscadas.
|
||||
ResolvedStream dashClearKeyStream = decodeDashClearKeyStream(html);
|
||||
if (dashClearKeyStream != null) {
|
||||
return dashClearKeyStream;
|
||||
}
|
||||
|
||||
// Último recurso: si la URL viene de sudamericaplay.com o similares,
|
||||
// intentar usarla directamente
|
||||
if (pageUrl.contains("sudamericaplay.com") ||
|
||||
pageUrl.contains("paramount")) {
|
||||
return pageUrl;
|
||||
return ResolvedStream.fromUrl(pageUrl);
|
||||
}
|
||||
|
||||
// Si no encontramos la URL, mostrar un fragmento del HTML para debug
|
||||
@@ -194,6 +209,64 @@ public final class StreamUrlResolver {
|
||||
|
||||
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<>();
|
||||
Matcher pairMatcher = OBFUSCATED_PAIR_PATTERN.matcher(script);
|
||||
while (pairMatcher.find()) {
|
||||
@@ -204,18 +277,21 @@ public final class StreamUrlResolver {
|
||||
}
|
||||
}
|
||||
|
||||
if (pairs.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Collections.sort(pairs, new Comparator<EncodedPair>() {
|
||||
@Override
|
||||
public int compare(EncodedPair left, EncodedPair right) {
|
||||
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) {
|
||||
byte[] decodedBytes;
|
||||
try {
|
||||
@@ -249,12 +325,23 @@ public final class StreamUrlResolver {
|
||||
return null;
|
||||
}
|
||||
|
||||
String url = decoded.toString().trim()
|
||||
return decoded.toString().trim()
|
||||
.replace("\\/", "/")
|
||||
.replace("\\u0026", "&")
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user