zhibo/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.java

261 lines
9.0 KiB
Java
Raw Normal View History

2025-12-16 16:00:11 +08:00
package com.example.livestreaming;
import android.os.Bundle;
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;
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;
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;
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;
2025-12-17 17:23:54 +08:00
// 创建低延迟播放器配置
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();
// 设置播放器视图
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
String computedAltUrl = altUrl;
if (computedAltUrl == null || computedAltUrl.trim().isEmpty()) computedAltUrl = getAltHlsUrl(url);
String finalComputedAltUrl = computedAltUrl;
2025-12-16 16:00:11 +08:00
exo.addListener(new Player.Listener() {
@Override
public void onPlayerError(PlaybackException error) {
if (triedAltUrl) return;
if (finalComputedAltUrl == null || finalComputedAltUrl.trim().isEmpty()) return;
2025-12-16 16:00:11 +08:00
triedAltUrl = true;
exo.setMediaItem(MediaItem.fromUri(finalComputedAltUrl));
2025-12-16 16:00:11 +08:00
exo.prepare();
exo.setPlayWhenReady(true);
}
});
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}>
// 用于统计播放时长和用户观看行为
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() {
2025-12-16 16:00:11 +08:00
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);
}
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";
}
}