zhibo/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.java
2026-01-03 12:24:05 +08:00

341 lines
13 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.example.livestreaming;
import android.graphics.SurfaceTexture;
import android.os.Bundle;
import android.view.Surface;
import android.view.TextureView;
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;
import tv.danmaku.ijk.media.player.IMediaPlayer;
import tv.danmaku.ijk.media.player.IjkMediaPlayer;
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;
private IjkMediaPlayer ijkPlayer;
private Surface ijkSurface;
private String ijkUrl;
private String ijkFallbackHlsUrl;
private boolean ijkFallbackTried;
private static boolean ijkLibLoaded;
@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();
// TODO: 接入后端接口 - 记录播放开始
// 接口路径: POST /api/play/start
// 请求参数:
// - userId: 当前用户ID从token中获取可选
// - roomId: 房间ID从Intent中获取
// - playUrl: 播放地址
// - timestamp: 开始播放时间戳
// 返回数据格式: ApiResponse<{success: boolean}>
// 用于统计播放数据和用户行为分析
String url = getIntent().getStringExtra(EXTRA_PLAY_URL);
if (url == null || url.trim().isEmpty()) return;
triedAltUrl = false;
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;
// 优化缓冲配置 - 针对低延迟 HLS1秒分片
androidx.media3.exoplayer.DefaultLoadControl loadControl =
new androidx.media3.exoplayer.DefaultLoadControl.Builder()
.setBufferDurationsMs(
2500, // 最小缓冲 2.5秒
10000, // 最大缓冲 10秒
1500, // 播放前缓冲 1.5秒
2500 // 重新缓冲 2.5秒
)
.setPrioritizeTimeOverSizeThresholds(true)
.build();
ExoPlayer exo = new ExoPlayer.Builder(this)
.setLoadControl(loadControl)
.build();
binding.playerView.setPlayer(exo);
binding.playerView.setUseController(true);
binding.playerView.setControllerAutoShow(false);
String computedAltUrl = altUrl;
if (computedAltUrl == null || computedAltUrl.trim().isEmpty()) {
computedAltUrl = getAltHlsUrl(url);
}
String finalComputedAltUrl = computedAltUrl;
exo.addListener(new Player.Listener() {
@Override
public void onPlayerError(PlaybackException error) {
android.util.Log.e("PlayerActivity", "播放错误: " + error.getMessage());
if (triedAltUrl) return;
if (finalComputedAltUrl == null || finalComputedAltUrl.trim().isEmpty()) return;
triedAltUrl = true;
exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl));
exo.prepare();
exo.setPlayWhenReady(true);
}
});
android.util.Log.d("PlayerActivity", "开始播放: " + url);
exo.setMediaItem(MediaItem.fromUri(url));
exo.prepare();
exo.setPlayWhenReady(true);
player = exo;
}
@Override
protected void onStop() {
super.onStop();
// TODO: 接入后端接口 - 记录播放结束
// 接口路径: POST /api/play/end
// 请求参数:
// - userId: 当前用户ID从token中获取可选
// - roomId: 房间ID从Intent中获取
// - playDuration: 播放时长(秒)
// - timestamp: 结束播放时间戳
// 返回数据格式: ApiResponse<{success: boolean}>
// 用于统计播放时长和用户观看行为
releaseExoPlayer();
releaseIjkPlayer();
}
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
android.util.Log.d("PlayerActivity", "使用IJK播放FLV流: " + flvUrl);
// 释放 ExoPlayer
releaseExoPlayer();
releaseIjkPlayer();
// 确保 IJK 库已加载
ensureIjkLibsLoaded();
// 保存回退地址
ijkUrl = flvUrl;
ijkFallbackHlsUrl = fallbackHlsUrl;
if (ijkFallbackHlsUrl == null || ijkFallbackHlsUrl.trim().isEmpty()) {
ijkFallbackHlsUrl = flvUrl.replace(".flv", ".m3u8");
}
ijkFallbackTried = false;
// 显示 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);
}
}
}
private void prepareIjk(String url) {
if (ijkSurface == null) return;
IjkMediaPlayer p = new IjkMediaPlayer();
// ========== 超低延迟核心配置 ==========
// 禁用数据包缓冲,直接播放
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
// 准备好立即播放
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
// 无限缓冲模式配合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);
// ========== 格式/解复用配置 ==========
// 禁用缓冲
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
// 分析时长 100us极短
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 100);
// 探测大小 1KB极小
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024);
// 刷新数据包
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);
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;
});
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
android.util.Log.e("PlayerActivity", "IJK播放错误: what=" + what + ", extra=" + extra);
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);
android.util.Log.d("PlayerActivity", "IJK开始准备播放: " + url);
p.prepareAsync();
} catch (Exception e) {
android.util.Log.e("PlayerActivity", "IJK播放异常: " + e.getMessage());
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() {
if (player != null) {
player.release();
player = null;
}
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);
}
}
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";
}
}