2025-12-16 16:00:11 +08:00
|
|
|
|
package com.example.livestreaming;
|
|
|
|
|
|
|
2026-01-03 12:24:05 +08:00
|
|
|
|
import android.graphics.SurfaceTexture;
|
2025-12-16 16:00:11 +08:00
|
|
|
|
import android.os.Bundle;
|
2025-12-21 17:35:06 +08:00
|
|
|
|
import android.view.Surface;
|
|
|
|
|
|
import android.view.TextureView;
|
2025-12-16 16:00:11 +08:00
|
|
|
|
|
|
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
|
|
import androidx.appcompat.app.AppCompatActivity;
|
|
|
|
|
|
import androidx.media3.common.MediaItem;
|
|
|
|
|
|
import androidx.media3.common.PlaybackException;
|
|
|
|
|
|
import androidx.media3.common.Player;
|
|
|
|
|
|
import androidx.media3.exoplayer.ExoPlayer;
|
|
|
|
|
|
|
|
|
|
|
|
import com.example.livestreaming.databinding.ActivityPlayerBinding;
|
|
|
|
|
|
|
2025-12-21 17:35:06 +08:00
|
|
|
|
import tv.danmaku.ijk.media.player.IMediaPlayer;
|
|
|
|
|
|
import tv.danmaku.ijk.media.player.IjkMediaPlayer;
|
|
|
|
|
|
|
2025-12-16 16:00:11 +08:00
|
|
|
|
public class PlayerActivity extends AppCompatActivity {
|
|
|
|
|
|
|
|
|
|
|
|
public static final String EXTRA_PLAY_URL = "extra_play_url";
|
|
|
|
|
|
public static final String EXTRA_TITLE = "extra_title";
|
|
|
|
|
|
|
|
|
|
|
|
private ActivityPlayerBinding binding;
|
|
|
|
|
|
private ExoPlayer player;
|
|
|
|
|
|
private boolean triedAltUrl = false;
|
|
|
|
|
|
|
2025-12-21 17:35:06 +08:00
|
|
|
|
private IjkMediaPlayer ijkPlayer;
|
|
|
|
|
|
private Surface ijkSurface;
|
|
|
|
|
|
private String ijkUrl;
|
|
|
|
|
|
private String ijkFallbackHlsUrl;
|
|
|
|
|
|
private boolean ijkFallbackTried;
|
|
|
|
|
|
|
|
|
|
|
|
private static boolean ijkLibLoaded;
|
|
|
|
|
|
|
2025-12-16 16:00:11 +08:00
|
|
|
|
@Override
|
|
|
|
|
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
|
|
|
|
super.onCreate(savedInstanceState);
|
|
|
|
|
|
binding = ActivityPlayerBinding.inflate(getLayoutInflater());
|
|
|
|
|
|
setContentView(binding.getRoot());
|
|
|
|
|
|
|
|
|
|
|
|
String t = getIntent().getStringExtra(EXTRA_TITLE);
|
|
|
|
|
|
setTitle(t != null ? t : "Live");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
protected void onStart() {
|
|
|
|
|
|
super.onStart();
|
2025-12-23 18:09:56 +08:00
|
|
|
|
// TODO: 接入后端接口 - 记录播放开始
|
|
|
|
|
|
// 接口路径: POST /api/play/start
|
|
|
|
|
|
// 请求参数:
|
|
|
|
|
|
// - userId: 当前用户ID(从token中获取,可选)
|
|
|
|
|
|
// - roomId: 房间ID(从Intent中获取)
|
|
|
|
|
|
// - playUrl: 播放地址
|
|
|
|
|
|
// - timestamp: 开始播放时间戳
|
|
|
|
|
|
// 返回数据格式: ApiResponse<{success: boolean}>
|
|
|
|
|
|
// 用于统计播放数据和用户行为分析
|
2025-12-16 16:00:11 +08:00
|
|
|
|
String url = getIntent().getStringExtra(EXTRA_PLAY_URL);
|
|
|
|
|
|
if (url == null || url.trim().isEmpty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
triedAltUrl = false;
|
|
|
|
|
|
|
2025-12-21 17:35:06 +08:00
|
|
|
|
if (url.endsWith(".flv")) {
|
|
|
|
|
|
startFlv(url, null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
startHls(url, null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void startHls(String url, @Nullable String altUrl) {
|
|
|
|
|
|
releaseIjkPlayer();
|
|
|
|
|
|
if (binding != null) {
|
|
|
|
|
|
binding.flvTextureView.setVisibility(android.view.View.GONE);
|
|
|
|
|
|
binding.playerView.setVisibility(android.view.View.VISIBLE);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
releaseExoPlayer();
|
|
|
|
|
|
triedAltUrl = false;
|
|
|
|
|
|
|
2026-01-03 12:24:05 +08:00
|
|
|
|
// 优化缓冲配置 - 针对低延迟 HLS(1秒分片)
|
2025-12-31 14:44:51 +08:00
|
|
|
|
androidx.media3.exoplayer.DefaultLoadControl loadControl =
|
|
|
|
|
|
new androidx.media3.exoplayer.DefaultLoadControl.Builder()
|
|
|
|
|
|
.setBufferDurationsMs(
|
2026-01-03 12:24:05 +08:00
|
|
|
|
2500, // 最小缓冲 2.5秒
|
|
|
|
|
|
10000, // 最大缓冲 10秒
|
2025-12-31 14:44:51 +08:00
|
|
|
|
1500, // 播放前缓冲 1.5秒
|
2026-01-03 12:24:05 +08:00
|
|
|
|
2500 // 重新缓冲 2.5秒
|
2025-12-31 14:44:51 +08:00
|
|
|
|
)
|
|
|
|
|
|
.setPrioritizeTimeOverSizeThresholds(true)
|
|
|
|
|
|
.build();
|
|
|
|
|
|
|
2025-12-17 17:23:54 +08:00
|
|
|
|
ExoPlayer exo = new ExoPlayer.Builder(this)
|
2025-12-31 14:44:51 +08:00
|
|
|
|
.setLoadControl(loadControl)
|
2025-12-17 17:23:54 +08:00
|
|
|
|
.build();
|
|
|
|
|
|
|
2025-12-16 16:00:11 +08:00
|
|
|
|
binding.playerView.setPlayer(exo);
|
2025-12-17 17:23:54 +08:00
|
|
|
|
binding.playerView.setUseController(true);
|
|
|
|
|
|
binding.playerView.setControllerAutoShow(false);
|
2025-12-16 16:00:11 +08:00
|
|
|
|
|
2025-12-21 17:35:06 +08:00
|
|
|
|
String computedAltUrl = altUrl;
|
2025-12-31 14:44:51 +08:00
|
|
|
|
if (computedAltUrl == null || computedAltUrl.trim().isEmpty()) {
|
|
|
|
|
|
computedAltUrl = getAltHlsUrl(url);
|
|
|
|
|
|
}
|
2025-12-21 17:35:06 +08:00
|
|
|
|
|
|
|
|
|
|
String finalComputedAltUrl = computedAltUrl;
|
2025-12-16 16:00:11 +08:00
|
|
|
|
exo.addListener(new Player.Listener() {
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onPlayerError(PlaybackException error) {
|
2025-12-31 14:44:51 +08:00
|
|
|
|
android.util.Log.e("PlayerActivity", "播放错误: " + error.getMessage());
|
2025-12-16 16:00:11 +08:00
|
|
|
|
if (triedAltUrl) return;
|
2025-12-21 17:35:06 +08:00
|
|
|
|
if (finalComputedAltUrl == null || finalComputedAltUrl.trim().isEmpty()) return;
|
2025-12-16 16:00:11 +08:00
|
|
|
|
triedAltUrl = true;
|
|
|
|
|
|
|
2025-12-21 17:35:06 +08:00
|
|
|
|
exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl));
|
2025-12-16 16:00:11 +08:00
|
|
|
|
exo.prepare();
|
|
|
|
|
|
exo.setPlayWhenReady(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-31 14:44:51 +08:00
|
|
|
|
android.util.Log.d("PlayerActivity", "开始播放: " + url);
|
2025-12-16 16:00:11 +08:00
|
|
|
|
exo.setMediaItem(MediaItem.fromUri(url));
|
|
|
|
|
|
exo.prepare();
|
|
|
|
|
|
exo.setPlayWhenReady(true);
|
|
|
|
|
|
player = exo;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
protected void onStop() {
|
|
|
|
|
|
super.onStop();
|
2025-12-23 18:09:56 +08:00
|
|
|
|
// TODO: 接入后端接口 - 记录播放结束
|
|
|
|
|
|
// 接口路径: POST /api/play/end
|
|
|
|
|
|
// 请求参数:
|
|
|
|
|
|
// - userId: 当前用户ID(从token中获取,可选)
|
|
|
|
|
|
// - roomId: 房间ID(从Intent中获取)
|
|
|
|
|
|
// - playDuration: 播放时长(秒)
|
|
|
|
|
|
// - timestamp: 结束播放时间戳
|
|
|
|
|
|
// 返回数据格式: ApiResponse<{success: boolean}>
|
|
|
|
|
|
// 用于统计播放时长和用户观看行为
|
2025-12-21 17:35:06 +08:00
|
|
|
|
releaseExoPlayer();
|
|
|
|
|
|
releaseIjkPlayer();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
|
2026-01-03 12:24:05 +08:00
|
|
|
|
android.util.Log.d("PlayerActivity", "使用IJK播放FLV流: " + flvUrl);
|
2025-12-31 14:44:51 +08:00
|
|
|
|
|
2026-01-03 12:24:05 +08:00
|
|
|
|
// 释放 ExoPlayer
|
|
|
|
|
|
releaseExoPlayer();
|
|
|
|
|
|
releaseIjkPlayer();
|
|
|
|
|
|
|
|
|
|
|
|
// 确保 IJK 库已加载
|
|
|
|
|
|
ensureIjkLibsLoaded();
|
|
|
|
|
|
|
|
|
|
|
|
// 保存回退地址
|
|
|
|
|
|
ijkUrl = flvUrl;
|
|
|
|
|
|
ijkFallbackHlsUrl = fallbackHlsUrl;
|
|
|
|
|
|
if (ijkFallbackHlsUrl == null || ijkFallbackHlsUrl.trim().isEmpty()) {
|
|
|
|
|
|
ijkFallbackHlsUrl = flvUrl.replace(".flv", ".m3u8");
|
2025-12-21 17:35:06 +08:00
|
|
|
|
}
|
2026-01-03 12:24:05 +08:00
|
|
|
|
ijkFallbackTried = false;
|
2025-12-31 14:44:51 +08:00
|
|
|
|
|
2026-01-03 12:24:05 +08:00
|
|
|
|
// 显示 TextureView,隐藏 ExoPlayer 的 PlayerView
|
|
|
|
|
|
if (binding != null) {
|
|
|
|
|
|
binding.playerView.setVisibility(android.view.View.GONE);
|
|
|
|
|
|
binding.flvTextureView.setVisibility(android.view.View.VISIBLE);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置 TextureView 监听器
|
|
|
|
|
|
binding.flvTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onSurfaceTextureAvailable(android.graphics.SurfaceTexture surface, int width, int height) {
|
|
|
|
|
|
android.util.Log.d("PlayerActivity", "SurfaceTexture可用,准备IJK播放器");
|
|
|
|
|
|
ijkSurface = new Surface(surface);
|
|
|
|
|
|
prepareIjk(flvUrl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onSurfaceTextureSizeChanged(android.graphics.SurfaceTexture surface, int width, int height) {
|
|
|
|
|
|
// 尺寸变化时不需要处理
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public boolean onSurfaceTextureDestroyed(android.graphics.SurfaceTexture surface) {
|
|
|
|
|
|
android.util.Log.d("PlayerActivity", "SurfaceTexture销毁");
|
|
|
|
|
|
releaseIjkPlayer();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onSurfaceTextureUpdated(android.graphics.SurfaceTexture surface) {
|
|
|
|
|
|
// 更新时不需要处理
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 如果 TextureView 已经可用,直接准备播放
|
|
|
|
|
|
if (binding.flvTextureView.isAvailable()) {
|
|
|
|
|
|
android.util.Log.d("PlayerActivity", "TextureView已可用,直接准备IJK播放器");
|
|
|
|
|
|
ijkSurface = new Surface(binding.flvTextureView.getSurfaceTexture());
|
|
|
|
|
|
prepareIjk(flvUrl);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-21 17:35:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void prepareIjk(String url) {
|
|
|
|
|
|
if (ijkSurface == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
IjkMediaPlayer p = new IjkMediaPlayer();
|
2026-01-03 12:24:05 +08:00
|
|
|
|
|
|
|
|
|
|
// ========== 超低延迟核心配置 ==========
|
|
|
|
|
|
// 禁用数据包缓冲,直接播放
|
2025-12-21 17:35:06 +08:00
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
|
2026-01-03 12:24:05 +08:00
|
|
|
|
// 准备好立即播放
|
2025-12-21 17:35:06 +08:00
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
|
2026-01-03 12:24:05 +08:00
|
|
|
|
// 无限缓冲模式(配合max_cached_duration使用)
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
|
|
|
|
|
|
// 最大缓存时长 200ms(极低延迟)
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 200);
|
|
|
|
|
|
// 最小帧数 2(快速启动)
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", 2);
|
|
|
|
|
|
// 允许丢帧追赶延迟
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 5);
|
|
|
|
|
|
// 同步类型:视频为主
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "sync-av-start", 0);
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 格式/解复用配置 ==========
|
|
|
|
|
|
// 禁用缓冲
|
2025-12-21 17:35:06 +08:00
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
|
2026-01-03 12:24:05 +08:00
|
|
|
|
// 分析时长 100us(极短)
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 100);
|
|
|
|
|
|
// 探测大小 1KB(极小)
|
2025-12-21 17:35:06 +08:00
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024);
|
2026-01-03 12:24:05 +08:00
|
|
|
|
// 刷新数据包
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1);
|
|
|
|
|
|
// 禁用DNS缓存
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1);
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 编解码配置 ==========
|
|
|
|
|
|
// 使用硬件解码(如果可用)
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 1);
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1);
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 1);
|
|
|
|
|
|
// 跳过循环滤波(加速解码)
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48);
|
|
|
|
|
|
// 跳过帧(加速解码)
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_frame", 0);
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 网络配置 ==========
|
|
|
|
|
|
// 重连次数
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);
|
|
|
|
|
|
// 超时时间 3秒
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 3000000);
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 音频配置 ==========
|
|
|
|
|
|
// 开启音频
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "an", 0);
|
|
|
|
|
|
// OpenSL ES 音频输出(低延迟)
|
|
|
|
|
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 1);
|
2025-12-21 17:35:06 +08:00
|
|
|
|
|
2026-01-03 12:24:05 +08:00
|
|
|
|
p.setOnPreparedListener(mp -> {
|
|
|
|
|
|
android.util.Log.d("PlayerActivity", "IJK播放器准备完成,开始播放");
|
|
|
|
|
|
mp.start();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
p.setOnInfoListener((mp, what, extra) -> {
|
|
|
|
|
|
if (what == IMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
|
|
|
|
|
|
android.util.Log.d("PlayerActivity", "IJK首帧渲染完成");
|
|
|
|
|
|
} else if (what == IMediaPlayer.MEDIA_INFO_BUFFERING_START) {
|
|
|
|
|
|
android.util.Log.d("PlayerActivity", "IJK开始缓冲");
|
|
|
|
|
|
} else if (what == IMediaPlayer.MEDIA_INFO_BUFFERING_END) {
|
|
|
|
|
|
android.util.Log.d("PlayerActivity", "IJK缓冲结束");
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-21 17:35:06 +08:00
|
|
|
|
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
|
2026-01-03 12:24:05 +08:00
|
|
|
|
android.util.Log.e("PlayerActivity", "IJK播放错误: what=" + what + ", extra=" + extra);
|
2025-12-21 17:35:06 +08:00
|
|
|
|
if (ijkFallbackTried || ijkFallbackHlsUrl == null || ijkFallbackHlsUrl.trim().isEmpty()) return true;
|
|
|
|
|
|
ijkFallbackTried = true;
|
|
|
|
|
|
startHls(ijkFallbackHlsUrl, null);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
ijkPlayer = p;
|
|
|
|
|
|
try {
|
|
|
|
|
|
p.setSurface(ijkSurface);
|
|
|
|
|
|
p.setDataSource(url);
|
2026-01-03 12:24:05 +08:00
|
|
|
|
android.util.Log.d("PlayerActivity", "IJK开始准备播放: " + url);
|
2025-12-21 17:35:06 +08:00
|
|
|
|
p.prepareAsync();
|
|
|
|
|
|
} catch (Exception e) {
|
2026-01-03 12:24:05 +08:00
|
|
|
|
android.util.Log.e("PlayerActivity", "IJK播放异常: " + e.getMessage());
|
2025-12-21 17:35:06 +08:00
|
|
|
|
if (ijkFallbackHlsUrl != null && !ijkFallbackHlsUrl.trim().isEmpty()) {
|
|
|
|
|
|
startHls(ijkFallbackHlsUrl, null);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void ensureIjkLibsLoaded() {
|
|
|
|
|
|
if (ijkLibLoaded) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
IjkMediaPlayer.loadLibrariesOnce(null);
|
|
|
|
|
|
IjkMediaPlayer.native_profileBegin("libijkplayer.so");
|
|
|
|
|
|
} catch (Throwable ignored) {
|
|
|
|
|
|
}
|
|
|
|
|
|
ijkLibLoaded = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void releaseExoPlayer() {
|
2025-12-16 16:00:11 +08:00
|
|
|
|
if (player != null) {
|
|
|
|
|
|
player.release();
|
|
|
|
|
|
player = null;
|
|
|
|
|
|
}
|
2025-12-21 17:35:06 +08:00
|
|
|
|
if (binding != null) binding.playerView.setPlayer(null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void releaseIjkPlayer() {
|
|
|
|
|
|
if (ijkPlayer != null) {
|
|
|
|
|
|
ijkPlayer.reset();
|
|
|
|
|
|
ijkPlayer.release();
|
|
|
|
|
|
ijkPlayer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ijkSurface != null) {
|
|
|
|
|
|
ijkSurface.release();
|
|
|
|
|
|
ijkSurface = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
ijkUrl = null;
|
|
|
|
|
|
ijkFallbackHlsUrl = null;
|
|
|
|
|
|
ijkFallbackTried = false;
|
|
|
|
|
|
if (binding != null) {
|
|
|
|
|
|
binding.flvTextureView.setVisibility(android.view.View.GONE);
|
|
|
|
|
|
binding.playerView.setVisibility(android.view.View.VISIBLE);
|
|
|
|
|
|
}
|
2025-12-16 16:00:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private String getAltHlsUrl(String url) {
|
|
|
|
|
|
if (!url.endsWith(".m3u8")) return null;
|
|
|
|
|
|
if (url.contains("/index.m3u8")) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return url.substring(0, url.length() - ".m3u8".length()) + "/index.m3u8";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|