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; // 优化缓冲配置 - 针对低延迟 HLS(1秒分片) 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"; } }