部署:部署远程

This commit is contained in:
xiao12feng8 2026-01-04 20:50:37 +08:00
parent 6d811689a6
commit 69cfc3624f
9 changed files with 354 additions and 105 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -227,9 +227,9 @@ public class RoomController {
}
LiveRoomStreamUrlsResponse urls = new LiveRoomStreamUrlsResponse();
urls.setRtmp(String.format("rtmp://%s:%d/live/%s", host, rtmpPort, room.getStreamKey()));
urls.setFlv(String.format("http://%s:%d/live/%s.flv", host, httpPort, room.getStreamKey()));
urls.setHls(String.format("http://%s:%d/live/%s.m3u8", host, httpPort, room.getStreamKey()));
urls.setRtmp(String.format("rtmp://%s:%d/__defaultApp__/%s", host, rtmpPort, room.getStreamKey()));
urls.setFlv(String.format("http://%s:%d/__defaultApp__/%s.flv", host, httpPort, room.getStreamKey()));
urls.setHls(String.format("http://%s:%d/__defaultApp__/%s.m3u8", host, httpPort, room.getStreamKey()));
resp.setStreamUrls(urls);
return resp;
}

View File

@ -642,9 +642,9 @@ public class LiveRoomController {
int httpPort = parsePort(publicHttpPort, 8080);
StreamUrlsResponse urls = new StreamUrlsResponse();
urls.setRtmp(String.format("rtmp://%s:%d/live/%s", host, rtmpPort, room.getStreamKey()));
urls.setFlv(String.format("http://%s:%d/live/%s.flv", host, httpPort, room.getStreamKey()));
urls.setHls(String.format("http://%s:%d/live/%s.m3u8", host, httpPort, room.getStreamKey()));
urls.setRtmp(String.format("rtmp://%s:%d/__defaultApp__/%s", host, rtmpPort, room.getStreamKey()));
urls.setFlv(String.format("http://%s:%d/__defaultApp__/%s.flv", host, httpPort, room.getStreamKey()));
urls.setHls(String.format("http://%s:%d/__defaultApp__/%s.m3u8", host, httpPort, room.getStreamKey()));
resp.setStreamUrls(urls);
return resp;

View File

@ -118,10 +118,9 @@ dependencies {
implementation("androidx.media3:media3-exoplayer-hls:$media3Version")
implementation("androidx.media3:media3-ui:$media3Version")
// HTTP-FLV low-latency playback (IjkPlayer via JitPack)
implementation("com.github.andnux:ijkplayer:0.0.1") {
exclude("com.google.android.exoplayer", "exoplayer")
}
// GSYVideoPlayer - 支持多内核的视频播放器IjkPlayer/ExoPlayer
// 用于FLV低延迟直播播放已内置IjkPlayer
implementation("com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer:v8.6.0-release-jitpack")
// WebRTC for voice/video calls
// 使用 Google 官方 WebRTC 库

View File

@ -68,13 +68,13 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
private boolean streamerVerified = false;
private boolean cameraInitialized = false;
// 推流参数
private static final int VIDEO_WIDTH = 640;
// 推流参数 - 平衡画质和流畅度
private static final int VIDEO_WIDTH = 720;
private static final int VIDEO_HEIGHT = 480;
private static final int VIDEO_FPS = 25;
private static final int VIDEO_BITRATE = 1500 * 1024; // 1.5Mbps
private static final int VIDEO_FPS = 24;
private static final int VIDEO_BITRATE = 1200 * 1024; // 1.2Mbps
private static final int AUDIO_BITRATE = 64 * 1024;
private static final int AUDIO_SAMPLE_RATE = 32000;
private static final int AUDIO_SAMPLE_RATE = 44100;
// 直播计时
private Handler timerHandler = new Handler(Looper.getMainLooper());
@ -316,23 +316,18 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
Log.d(TAG, "尝试使用 Camera2 API...");
rtmpCamera2 = new RtmpCamera2(binding.surfaceView, this);
// 准备编码器
boolean audioReady = rtmpCamera2.prepareAudio(AUDIO_BITRATE, AUDIO_SAMPLE_RATE, false);
boolean videoReady = rtmpCamera2.prepareVideo(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS, VIDEO_BITRATE, 0);
Log.d(TAG, "Camera2 编码器准备: audio=" + audioReady + ", video=" + videoReady);
if (videoReady) {
// 开始预览
String cameraId = isFrontCamera ? "1" : "0";
rtmpCamera2.startPreview(cameraId);
useCamera2 = true;
cameraInitialized = true;
Log.d(TAG, "Camera2 预览已开始");
return;
}
// 先开始预览再准备编码器推流时再准备
String cameraId = isFrontCamera ? "1" : "0";
rtmpCamera2.startPreview(cameraId);
useCamera2 = true;
cameraInitialized = true;
Log.d(TAG, "Camera2 预览已开始");
return;
} catch (Exception e) {
Log.w(TAG, "Camera2 初始化失败: " + e.getMessage());
Log.w(TAG, "Camera2 初始化失败: " + e.getMessage(), e);
if (rtmpCamera2 != null) {
try { rtmpCamera2.stopPreview(); } catch (Exception ignored) {}
}
rtmpCamera2 = null;
}
}
@ -342,21 +337,17 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
Log.d(TAG, "尝试使用 Camera1 API...");
rtmpCamera1 = new RtmpCamera1(binding.surfaceView, this);
boolean audioReady = rtmpCamera1.prepareAudio();
boolean videoReady = rtmpCamera1.prepareVideo();
Log.d(TAG, "Camera1 编码器准备: audio=" + audioReady + ", video=" + videoReady);
if (videoReady) {
CameraHelper.Facing facing = isFrontCamera ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK;
rtmpCamera1.startPreview(facing);
useCamera2 = false;
cameraInitialized = true;
Log.d(TAG, "Camera1 预览已开始");
return;
}
CameraHelper.Facing facing = isFrontCamera ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK;
rtmpCamera1.startPreview(facing);
useCamera2 = false;
cameraInitialized = true;
Log.d(TAG, "Camera1 预览已开始");
return;
} catch (Exception e) {
Log.e(TAG, "Camera1 初始化也失败: " + e.getMessage());
Log.e(TAG, "Camera1 初始化也失败: " + e.getMessage(), e);
if (rtmpCamera1 != null) {
try { rtmpCamera1.stopPreview(); } catch (Exception ignored) {}
}
rtmpCamera1 = null;
}
@ -493,12 +484,53 @@ public class BroadcastActivity extends AppCompatActivity implements ConnectCheck
Log.d(TAG, "开始推流到: " + rtmpUrl);
try {
if (useCamera2 && rtmpCamera2 != null && !rtmpCamera2.isStreaming()) {
if (useCamera2 && rtmpCamera2 != null) {
Log.d(TAG, "使用 Camera2 API 推流");
rtmpCamera2.startStream(rtmpUrl);
} else if (rtmpCamera1 != null && !rtmpCamera1.isStreaming()) {
// 推流前准备编码器 - 使用优化后的低码率参数
boolean audioReady = rtmpCamera2.prepareAudio(AUDIO_BITRATE, AUDIO_SAMPLE_RATE, false);
// 使用 640x360 低分辨率更流畅
boolean videoReady = rtmpCamera2.prepareVideo(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS, VIDEO_BITRATE, 0);
Log.d(TAG, "编码器准备: audio=" + audioReady + ", video=" + videoReady);
if (!videoReady) {
// 尝试更低的分辨率
Log.w(TAG, "640x360 失败,尝试 480x270");
videoReady = rtmpCamera2.prepareVideo(480, 270, VIDEO_FPS, VIDEO_BITRATE, 0);
}
if (!videoReady) {
Log.e(TAG, "视频编码器准备失败");
binding.progressBar.setVisibility(View.GONE);
binding.btnStartLive.setEnabled(true);
Toast.makeText(this, "视频编码器初始化失败", Toast.LENGTH_SHORT).show();
return;
}
if (!rtmpCamera2.isStreaming()) {
rtmpCamera2.startStream(rtmpUrl);
}
} else if (rtmpCamera1 != null) {
Log.d(TAG, "使用 Camera1 API 推流");
rtmpCamera1.startStream(rtmpUrl);
// 推流前准备编码器 - 使用优化参数
boolean audioReady = rtmpCamera1.prepareAudio(AUDIO_BITRATE, AUDIO_SAMPLE_RATE, false, false, false);
boolean videoReady = rtmpCamera1.prepareVideo(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS, VIDEO_BITRATE, 0);
Log.d(TAG, "编码器准备: audio=" + audioReady + ", video=" + videoReady);
if (!videoReady) {
Log.e(TAG, "视频编码器准备失败");
binding.progressBar.setVisibility(View.GONE);
binding.btnStartLive.setEnabled(true);
Toast.makeText(this, "视频编码器初始化失败", Toast.LENGTH_SHORT).show();
return;
}
if (!rtmpCamera1.isStreaming()) {
rtmpCamera1.startStream(rtmpUrl);
}
} else {
Log.e(TAG, "没有可用的摄像头推流器");
binding.progressBar.setVisibility(View.GONE);

View File

@ -8,6 +8,8 @@ import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.KeyEvent;
import android.view.View;
@ -52,6 +54,15 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import tv.danmaku.ijk.media.player.IMediaPlayer;
import tv.danmaku.ijk.media.player.IjkMediaPlayer;
// GSYVideoPlayer imports
import com.shuyu.gsyvideoplayer.GSYVideoManager;
import com.shuyu.gsyvideoplayer.listener.GSYSampleCallBack;
import com.shuyu.gsyvideoplayer.utils.GSYVideoType;
import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer;
import com.shuyu.gsyvideoplayer.builder.GSYVideoOptionBuilder;
import com.shuyu.gsyvideoplayer.player.PlayerFactory;
import com.shuyu.gsyvideoplayer.player.IjkPlayerManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -75,7 +86,7 @@ import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class RoomDetailActivity extends AppCompatActivity {
public class RoomDetailActivity extends AppCompatActivity implements SurfaceHolder.Callback {
public static final String EXTRA_ROOM_ID = "extra_room_id";
@ -90,9 +101,15 @@ public class RoomDetailActivity extends AppCompatActivity {
private boolean triedAltUrl;
private IjkMediaPlayer ijkPlayer;
private Surface ijkSurface;
private SurfaceHolder ijkSurfaceHolder;
private String ijkUrl;
private String ijkFallbackHlsUrl;
private boolean ijkFallbackTried;
private String pendingFlvUrl;
// GSYVideoPlayer 相关
private StandardGSYVideoPlayer gsyPlayer;
private boolean useGSYPlayer = true; // 默认使用GSYVideoPlayer播放FLV
private static boolean ijkLibLoaded;
private boolean isFullscreen = false;
@ -158,17 +175,22 @@ public class RoomDetailActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
try {
android.util.Log.d("RoomDetail", "onCreate开始");
// 隐藏ActionBar使用自定义顶部栏
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
android.util.Log.d("RoomDetail", "开始inflate布局");
binding = ActivityRoomDetailNewBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
android.util.Log.d("RoomDetail", "布局inflate完成");
ApiClient.getService(getApplicationContext());
roomId = getIntent().getStringExtra(EXTRA_ROOM_ID);
android.util.Log.d("RoomDetail", "roomId=" + roomId);
if (TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "直播间ID无效", Toast.LENGTH_SHORT).show();
finish();
@ -177,18 +199,28 @@ public class RoomDetailActivity extends AppCompatActivity {
triedAltUrl = false;
android.util.Log.d("RoomDetail", "开始setupUI");
setupUI();
android.util.Log.d("RoomDetail", "setupUI完成开始setupSurface");
setupSurface();
android.util.Log.d("RoomDetail", "setupSurface完成开始setupChat");
setupChat();
android.util.Log.d("RoomDetail", "setupChat完成开始setupGifts");
setupGifts();
android.util.Log.d("RoomDetail", "setupGifts完成");
// 加载房间信息
android.util.Log.d("RoomDetail", "开始loadRoomInfo");
loadRoomInfo();
android.util.Log.d("RoomDetail", "loadRoomInfo完成");
// 记录观看历史异步不阻塞
recordWatchHistory();
android.util.Log.d("RoomDetail", "onCreate完成");
} catch (Exception e) {
android.util.Log.e("RoomDetail", "onCreate异常: " + e.getMessage(), e);
Toast.makeText(this, "加载直播间失败", Toast.LENGTH_SHORT).show();
e.printStackTrace();
Toast.makeText(this, "加载直播间失败: " + e.getMessage(), Toast.LENGTH_LONG).show();
finish();
}
}
@ -237,6 +269,16 @@ public class RoomDetailActivity extends AppCompatActivity {
// 分享按钮
binding.shareButton.setOnClickListener(v -> shareRoom());
}
/**
* 初始化 SurfaceView 用于 IjkPlayer 播放 FLV
*/
private void setupSurface() {
if (binding.flvSurfaceView != null) {
binding.flvSurfaceView.getHolder().addCallback(this);
android.util.Log.d("RoomDetail", "SurfaceView callback 已设置");
}
}
private void setupChat() {
// 设置弹幕适配器
@ -1024,7 +1066,7 @@ public class RoomDetailActivity extends AppCompatActivity {
// 只在首次加载时显示loading
if (isFirstLoad) {
binding.loading.setVisibility(View.VISIBLE);
binding.loadingIndicator.setVisibility(View.VISIBLE);
}
ApiClient.getService(getApplicationContext()).getRoom(roomId).enqueue(new Callback<ApiResponse<Room>>() {
@ -1032,7 +1074,7 @@ public class RoomDetailActivity extends AppCompatActivity {
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
if (isFinishing() || isDestroyed()) return;
binding.loading.setVisibility(View.GONE);
binding.loadingIndicator.setVisibility(View.GONE);
boolean firstLoad = isFirstLoad;
isFirstLoad = false;
@ -1059,7 +1101,7 @@ public class RoomDetailActivity extends AppCompatActivity {
@Override
public void onFailure(Call<ApiResponse<Room>> call, Throwable t) {
if (isFinishing() || isDestroyed()) return;
binding.loading.setVisibility(View.GONE);
binding.loadingIndicator.setVisibility(View.GONE);
isFirstLoad = false;
android.util.Log.e("RoomDetail", "onFailure: " + t.getMessage());
}
@ -1298,69 +1340,135 @@ public class RoomDetailActivity extends AppCompatActivity {
}
private void startFlv(String flvUrl, @Nullable String fallbackHlsUrl) {
android.util.Log.d("RoomDetail", "尝试使用IjkPlayer播放FLV: " + flvUrl);
android.util.Log.d("RoomDetail", "播放FLV: " + flvUrl);
// 先尝试加载 IjkPlayer
ensureIjkLibsLoaded();
// 如果 IjkPlayer 加载失败直接使用 HLS
if (ijkLibLoadFailed) {
android.util.Log.w("RoomDetail", "IjkPlayer 不可用,回退到 HLS 播放");
String hlsUrl = fallbackHlsUrl;
if (TextUtils.isEmpty(hlsUrl)) {
hlsUrl = flvUrl.replace(".flv", ".m3u8");
}
startHls(hlsUrl, null);
return;
}
android.util.Log.d("RoomDetail", "使用IjkPlayer播放FLV低延迟: " + flvUrl);
// 释放之前的播放器
// 释放其他播放器
releaseExoPlayer();
releaseIjkPlayer();
releaseGSYPlayer();
// 保存备用地址
ijkUrl = flvUrl;
ijkFallbackHlsUrl = fallbackHlsUrl;
if (TextUtils.isEmpty(ijkFallbackHlsUrl)) {
ijkFallbackHlsUrl = flvUrl.replace(".flv", ".m3u8");
}
ijkFallbackTried = false;
hasShownConnectedMessage = false;
// 显示 FLV 播放视图
// 显示 FLV 播放视图使用 SurfaceView
if (binding != null) {
binding.playerView.setVisibility(View.GONE);
binding.flvTextureView.setVisibility(View.VISIBLE);
binding.flvTextureView.setVisibility(View.GONE);
binding.flvSurfaceView.setVisibility(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("IjkPlayer", "Surface 可用,开始准备播放");
ijkSurface = new Surface(surface);
prepareIjk(flvUrl);
// 使用 IjkPlayer 播放 FLV
pendingFlvUrl = flvUrl;
ijkUrl = flvUrl;
// 如果 SurfaceHolder 已经准备好直接播放
if (ijkSurfaceHolder != null) {
prepareIjkWithHolder(flvUrl, ijkSurfaceHolder);
} else {
android.util.Log.d("RoomDetail", "等待 SurfaceHolder 准备...");
// SurfaceHolder 会在 surfaceCreated 回调中触发播放
}
}
/**
* 释放 GSYVideoPlayer
*/
private void releaseGSYPlayer() {
if (gsyPlayer != null) {
gsyPlayer.release();
}
GSYVideoManager.releaseAllVideos();
}
// SurfaceHolder.Callback 实现
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
android.util.Log.d("IjkPlayer", "SurfaceView created");
ijkSurfaceHolder = holder;
if (pendingFlvUrl != null) {
prepareIjkWithHolder(pendingFlvUrl, holder);
}
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
android.util.Log.d("IjkPlayer", "SurfaceView changed: " + width + "x" + height);
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
android.util.Log.d("IjkPlayer", "SurfaceView destroyed");
ijkSurfaceHolder = null;
}
private void prepareIjkWithHolder(String url, SurfaceHolder holder) {
if (holder == null) return;
IjkMediaPlayer p = new IjkMediaPlayer();
// ========== 关键视频解码器设置 ==========
// 优先使用软解码更稳定
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 0);
// 视频输出格式
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32);
// 开启视频
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "vn", 0);
// ========== 缓冲和延迟优化 ==========
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1000000);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024 * 16);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);
p.setOnPreparedListener(mp -> {
android.util.Log.d("IjkPlayer", "准备完成,开始播放");
binding.offlineLayout.setVisibility(View.GONE);
mp.start();
if (!hasShownConnectedMessage) {
hasShownConnectedMessage = true;
addChatMessage(new ChatMessage("直播已连接,开始观看吧!", true));
}
@Override
public void onSurfaceTextureSizeChanged(android.graphics.SurfaceTexture surface, int width, int height) {}
@Override
public boolean onSurfaceTextureDestroyed(android.graphics.SurfaceTexture surface) {
});
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
android.util.Log.e("IjkPlayer", "播放错误: what=" + what + ", extra=" + extra);
if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) {
binding.offlineLayout.setVisibility(View.VISIBLE);
return true;
}
@Override
public void onSurfaceTextureUpdated(android.graphics.SurfaceTexture surface) {}
ijkFallbackTried = true;
startHls(ijkFallbackHlsUrl, null);
return true;
});
// 如果 Surface 已经可用直接开始播放
if (binding.flvTextureView.isAvailable()) {
android.util.Log.d("IjkPlayer", "Surface 已可用,直接开始播放");
ijkSurface = new Surface(binding.flvTextureView.getSurfaceTexture());
prepareIjk(flvUrl);
p.setOnInfoListener((mp, what, extra) -> {
if (what == IMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
android.util.Log.d("IjkPlayer", "视频开始渲染");
binding.offlineLayout.setVisibility(View.GONE);
}
return false;
});
ijkPlayer = p;
try {
p.setDisplay(holder);
p.setDataSource(url);
android.util.Log.d("IjkPlayer", "开始准备播放: " + url);
p.prepareAsync();
} catch (Exception e) {
android.util.Log.e("IjkPlayer", "播放器初始化失败: " + e.getMessage());
if (!TextUtils.isEmpty(ijkFallbackHlsUrl)) {
startHls(ijkFallbackHlsUrl, null);
}
}
}
@ -1368,7 +1476,22 @@ public class RoomDetailActivity extends AppCompatActivity {
if (ijkSurface == null) return;
IjkMediaPlayer p = new IjkMediaPlayer();
// 优化缓冲设置减少卡顿
// ========== 关键视频解码器设置 ==========
// 使用硬件解码MediaCodec
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_PLAYER, "mediacodec-hevc", 1);
// 视频输出格式 - 使用 OpenGL ES 渲染
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32);
// 开启视频
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "vn", 0);
// ========== 缓冲和延迟优化 ==========
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
@ -1381,6 +1504,10 @@ public class RoomDetailActivity extends AppCompatActivity {
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_streamed", 1);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_delay_max", 5);
// ========== 音视频同步 ==========
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "sync-av-start", 0);
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 0);
p.setOnPreparedListener(mp -> {
binding.offlineLayout.setVisibility(View.GONE);
@ -1476,10 +1603,14 @@ public class RoomDetailActivity extends AppCompatActivity {
private void releasePlayer() {
releaseExoPlayer();
releaseIjkPlayer();
releaseGSYPlayer();
pendingFlvUrl = null;
if (binding != null) {
binding.playerView.setPlayer(null);
binding.flvTextureView.setVisibility(View.GONE);
binding.flvSurfaceView.setVisibility(View.GONE);
binding.playerView.setVisibility(View.VISIBLE);
binding.gsyPlayer.setVisibility(View.GONE);
}
}
@ -1492,14 +1623,20 @@ public class RoomDetailActivity extends AppCompatActivity {
private void releaseIjkPlayer() {
if (ijkPlayer != null) {
ijkPlayer.reset();
ijkPlayer.release();
try {
ijkPlayer.stop();
ijkPlayer.reset();
ijkPlayer.release();
} catch (Exception e) {
android.util.Log.e("IjkPlayer", "释放播放器失败: " + e.getMessage());
}
ijkPlayer = null;
}
if (ijkSurface != null) {
ijkSurface.release();
ijkSurface = null;
}
ijkSurfaceHolder = null;
ijkUrl = null;
ijkFallbackHlsUrl = null;
ijkFallbackTried = false;
@ -1512,6 +1649,33 @@ public class RoomDetailActivity extends AppCompatActivity {
return url.substring(0, url.length() - ".m3u8".length()) + "/index.m3u8";
}
@Override
protected void onPause() {
super.onPause();
// GSYVideoPlayer 暂停
if (gsyPlayer != null) {
gsyPlayer.onVideoPause();
}
}
@Override
protected void onResume() {
super.onResume();
// GSYVideoPlayer 恢复
if (gsyPlayer != null) {
gsyPlayer.onVideoResume();
}
}
@Override
public void onBackPressed() {
// GSYVideoPlayer 返回处理
if (GSYVideoManager.backFromWindowFull(this)) {
return;
}
super.onBackPressed();
}
@Override
protected void onDestroy() {
super.onDestroy();

View File

@ -1,6 +1,10 @@
package com.example.livestreaming.call;
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.os.Bundle;
@ -92,6 +96,19 @@ public class CallActivity extends AppCompatActivity implements
// ICE Candidate 缓存在远程 SDP 设置前收到的
private List<IceCandidate> pendingIceCandidates = new ArrayList<>();
private boolean remoteDescriptionSet = false;
// 广播接收器 - 用于接收通话接听通知备用方案
private BroadcastReceiver callAcceptedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String receivedCallId = intent.getStringExtra("callId");
Log.d(TAG, "收到广播通知: callId=" + receivedCallId + ", 当前callId=" + callId);
if (callId != null && callId.equals(receivedCallId)) {
Log.d(TAG, "广播通知匹配触发onCallConnected");
runOnUiThread(() -> onCallConnected());
}
}
};
private Runnable durationRunnable = new Runnable() {
@Override
@ -120,6 +137,10 @@ public class CallActivity extends AppCompatActivity implements
);
setContentView(R.layout.activity_call);
// 注册广播接收器
IntentFilter filter = new IntentFilter("com.example.livestreaming.CALL_ACCEPTED");
registerReceiver(callAcceptedReceiver, filter);
initViews();
initData();
@ -523,7 +544,10 @@ public class CallActivity extends AppCompatActivity implements
@Override
public void onCallConnected(String callId) {
Log.d(TAG, "onCallConnected: " + callId);
Log.d(TAG, "========== onCallConnected 回调 ==========");
Log.d(TAG, "callId=" + callId + ", 当前callId=" + this.callId);
Log.d(TAG, "isCaller=" + isCaller + ", isConnected=" + isConnected);
Toast.makeText(this, "收到接听通知!", Toast.LENGTH_SHORT).show();
runOnUiThread(this::onCallConnected);
}
@ -701,6 +725,13 @@ public class CallActivity extends AppCompatActivity implements
super.onDestroy();
handler.removeCallbacks(durationRunnable);
// 注销广播接收器
try {
unregisterReceiver(callAcceptedReceiver);
} catch (Exception e) {
Log.w(TAG, "注销广播接收器失败: " + e.getMessage());
}
// 恢复音频模式
if (audioManager != null) {
audioManager.setMode(AudioManager.MODE_NORMAL);

View File

@ -440,12 +440,22 @@ public class CallManager implements CallSignalingClient.SignalingListener {
Log.d(TAG, "callId: " + callId);
Log.d(TAG, "currentCallId: " + currentCallId);
Log.d(TAG, "stateListener: " + (stateListener != null ? stateListener.getClass().getSimpleName() : "null"));
Log.d(TAG, "signalingClient连接状态: " + (signalingClient != null && signalingClient.isConnected()));
if (stateListener != null) {
Log.d(TAG, "调用 stateListener.onCallConnected");
stateListener.onCallConnected(callId);
} else {
Log.w(TAG, "stateListener 为空,无法通知界面");
// 尝试通过广播通知
try {
Intent intent = new Intent("com.example.livestreaming.CALL_ACCEPTED");
intent.putExtra("callId", callId);
context.sendBroadcast(intent);
Log.d(TAG, "已发送广播通知");
} catch (Exception e) {
Log.e(TAG, "发送广播失败: " + e.getMessage());
}
}
}

View File

@ -94,6 +94,12 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topBar">
<SurfaceView
android:id="@+id/flvSurfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<TextureView
android:id="@+id/flvTextureView"
android:layout_width="match_parent"
@ -107,6 +113,13 @@
app:use_controller="false"
app:show_buffering="when_playing" />
<!-- GSYVideoPlayer 播放器视图 -->
<com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer
android:id="@+id/gsyPlayer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<ImageButton
android:id="@+id/exitFullscreenButton"
android:layout_width="44dp"
@ -365,7 +378,7 @@
<!-- 加载指示器 -->
<ProgressBar
android:id="@+id/loading"
android:id="@+id/loadingIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"