diff --git a/Log/微信图片_20251216175455_1_40.png b/Log/微信图片_20251216175455_1_40.png deleted file mode 100644 index 62559037..00000000 Binary files a/Log/微信图片_20251216175455_1_40.png and /dev/null differ diff --git a/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/RoomController.java b/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/RoomController.java index c0af23eb..7f7ed7ed 100644 --- a/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/RoomController.java +++ b/Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/RoomController.java @@ -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; } diff --git a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java index 8a043646..bc647286 100644 --- a/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java +++ b/Zhibo/zhibo-h/crmeb-front/src/main/java/com/zbkj/front/controller/LiveRoomController.java @@ -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; diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts index 07ad6f8d..e918f5e4 100644 --- a/android-app/app/build.gradle.kts +++ b/android-app/app/build.gradle.kts @@ -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 库 diff --git a/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java b/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java index 3e2e274f..53c7b0e3 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/BroadcastActivity.java @@ -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); diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java index 345a6995..8eb58b17 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java @@ -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>() { @@ -1032,7 +1074,7 @@ public class RoomDetailActivity extends AppCompatActivity { public void onResponse(Call> call, Response> 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> 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(); diff --git a/android-app/app/src/main/java/com/example/livestreaming/call/CallActivity.java b/android-app/app/src/main/java/com/example/livestreaming/call/CallActivity.java index 93afb271..d3602255 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/call/CallActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/call/CallActivity.java @@ -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 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); diff --git a/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java b/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java index 46ba96fe..587a9ad9 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java +++ b/android-app/app/src/main/java/com/example/livestreaming/call/CallManager.java @@ -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()); + } } } diff --git a/android-app/app/src/main/res/layout/activity_room_detail_new.xml b/android-app/app/src/main/res/layout/activity_room_detail_new.xml index 1138a7b2..cabb5be2 100644 --- a/android-app/app/src/main/res/layout/activity_room_detail_new.xml +++ b/android-app/app/src/main/res/layout/activity_room_detail_new.xml @@ -94,6 +94,12 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/topBar"> + + + + +