package com.example.livestreaming; 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(); 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; // 创建低延迟播放器配置 ExoPlayer exo = new ExoPlayer.Builder(this) .setLoadControl(new androidx.media3.exoplayer.DefaultLoadControl.Builder() // 减少缓冲区大小,降低延迟 .setBufferDurationsMs( 1000, // 最小缓冲时长 1秒 3000, // 最大缓冲时长 3秒 500, // 播放缓冲时长 0.5秒 1000 // 播放后缓冲时长 1秒 ) .build()) .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) { if (triedAltUrl) return; if (finalComputedAltUrl == null || finalComputedAltUrl.trim().isEmpty()) return; triedAltUrl = true; exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl)); exo.prepare(); exo.setPlayWhenReady(true); } }); exo.setMediaItem(MediaItem.fromUri(url)); exo.prepare(); exo.setPlayWhenReady(true); player = exo; } @Override protected void onStop() { super.onStop(); releaseExoPlayer(); releaseIjkPlayer(); } private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) { ensureIjkLibsLoaded(); releaseExoPlayer(); releaseIjkPlayer(); ijkUrl = flvUrl; ijkFallbackHlsUrl = fallbackHlsUrl; ijkFallbackTried = false; if (binding != null) { binding.playerView.setVisibility(android.view.View.GONE); binding.flvTextureView.setVisibility(android.view.View.VISIBLE); } TextureView view = binding.flvTextureView; TextureView.SurfaceTextureListener listener = new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(android.graphics.SurfaceTexture surfaceTexture, int width, int height) { ijkSurface = new Surface(surfaceTexture); prepareIjk(flvUrl); } @Override public void onSurfaceTextureSizeChanged(android.graphics.SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(android.graphics.SurfaceTexture surface) { releaseIjkPlayer(); return true; } @Override public void onSurfaceTextureUpdated(android.graphics.SurfaceTexture surface) { } }; view.setSurfaceTextureListener(listener); if (view.isAvailable() && view.getSurfaceTexture() != null) { ijkSurface = new Surface(view.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); p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer"); p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1); p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024); p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1); p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 300); p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1); p.setOnPreparedListener(mp -> mp.start()); p.setOnErrorListener((IMediaPlayer mp, int what, int 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); p.prepareAsync(); } catch (Exception e) { 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"; } }