部署:部署远程
This commit is contained in:
parent
6d811689a6
commit
69cfc3624f
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 库
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user