810 lines
30 KiB
Java
810 lines
30 KiB
Java
package com.example.livestreaming;
|
||
|
||
import android.Manifest;
|
||
import android.content.Intent;
|
||
import android.content.pm.PackageManager;
|
||
import android.os.Build;
|
||
import android.os.Bundle;
|
||
import android.os.Handler;
|
||
import android.os.Looper;
|
||
import android.text.TextUtils;
|
||
import android.util.Log;
|
||
import android.util.Size;
|
||
import android.view.SurfaceHolder;
|
||
import android.view.View;
|
||
import android.view.WindowManager;
|
||
import android.widget.Toast;
|
||
|
||
import androidx.annotation.NonNull;
|
||
import androidx.appcompat.app.AlertDialog;
|
||
import androidx.appcompat.app.AppCompatActivity;
|
||
import androidx.core.app.ActivityCompat;
|
||
import androidx.core.content.ContextCompat;
|
||
|
||
import com.example.livestreaming.databinding.ActivityBroadcastBinding;
|
||
import com.example.livestreaming.net.ApiClient;
|
||
import com.example.livestreaming.net.ApiResponse;
|
||
import com.example.livestreaming.net.AuthStore;
|
||
import com.example.livestreaming.net.CreateRoomRequest;
|
||
import com.example.livestreaming.net.Room;
|
||
import com.pedro.encoder.input.video.CameraHelper;
|
||
import com.pedro.rtmp.utils.ConnectCheckerRtmp;
|
||
import com.pedro.rtplibrary.rtmp.RtmpCamera1;
|
||
import com.pedro.rtplibrary.rtmp.RtmpCamera2;
|
||
|
||
import java.util.Locale;
|
||
import java.util.Map;
|
||
|
||
import retrofit2.Call;
|
||
import retrofit2.Callback;
|
||
import retrofit2.Response;
|
||
|
||
/**
|
||
* 手机开播界面
|
||
* 使用 RootEncoder 进行 RTMP 推流
|
||
* 优先使用 Camera2 API (RtmpCamera2),兼容性更好
|
||
*/
|
||
public class BroadcastActivity extends AppCompatActivity implements ConnectCheckerRtmp, SurfaceHolder.Callback {
|
||
|
||
private static final String TAG = "BroadcastActivity";
|
||
private static final int REQUEST_PERMISSIONS = 100;
|
||
private static final String[] REQUIRED_PERMISSIONS = {
|
||
Manifest.permission.CAMERA,
|
||
Manifest.permission.RECORD_AUDIO
|
||
};
|
||
|
||
private ActivityBroadcastBinding binding;
|
||
|
||
// 使用 Camera2 API 的推流器
|
||
private RtmpCamera2 rtmpCamera2;
|
||
// 备用:使用 Camera1 API 的推流器
|
||
private RtmpCamera1 rtmpCamera1;
|
||
private boolean useCamera2 = true;
|
||
|
||
private Room currentRoom;
|
||
private boolean isStreaming = false;
|
||
private boolean isFrontCamera = true;
|
||
private boolean surfaceReady = false;
|
||
private boolean streamerVerified = false;
|
||
private boolean cameraInitialized = false;
|
||
|
||
// 推流参数 - 平衡画质和流畅度
|
||
private static final int VIDEO_WIDTH = 720;
|
||
private static final int VIDEO_HEIGHT = 480;
|
||
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 = 44100;
|
||
|
||
// 直播计时
|
||
private Handler timerHandler = new Handler(Looper.getMainLooper());
|
||
private Handler mainHandler = new Handler(Looper.getMainLooper());
|
||
private long startTime = 0;
|
||
private Runnable timerRunnable;
|
||
|
||
@Override
|
||
protected void onCreate(Bundle savedInstanceState) {
|
||
super.onCreate(savedInstanceState);
|
||
|
||
// 保持屏幕常亮
|
||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||
|
||
binding = ActivityBroadcastBinding.inflate(getLayoutInflater());
|
||
setContentView(binding.getRoot());
|
||
|
||
// 检查登录状态
|
||
if (!AuthHelper.isLoggedIn(this)) {
|
||
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
|
||
finish();
|
||
return;
|
||
}
|
||
|
||
setupUI();
|
||
setupSurface();
|
||
|
||
// 先检查主播资格,通过后再检查权限
|
||
checkStreamerStatus();
|
||
}
|
||
|
||
/**
|
||
* 检查主播资格
|
||
*/
|
||
private void checkStreamerStatus() {
|
||
binding.progressBar.setVisibility(View.VISIBLE);
|
||
binding.btnStartLive.setEnabled(false);
|
||
|
||
ApiClient.getService(this).checkStreamerStatus()
|
||
.enqueue(new Callback<ApiResponse<Map<String, Object>>>() {
|
||
@Override
|
||
public void onResponse(Call<ApiResponse<Map<String, Object>>> call,
|
||
Response<ApiResponse<Map<String, Object>>> response) {
|
||
binding.progressBar.setVisibility(View.GONE);
|
||
|
||
if (!response.isSuccessful() || response.body() == null) {
|
||
// 接口调用失败,可能是旧版本后端,允许继续
|
||
Log.w(TAG, "检查主播资格接口失败,允许继续");
|
||
streamerVerified = true;
|
||
binding.btnStartLive.setEnabled(true);
|
||
checkPermissions();
|
||
return;
|
||
}
|
||
|
||
ApiResponse<Map<String, Object>> body = response.body();
|
||
if (body.getCode() != 200 || body.getData() == null) {
|
||
// 接口返回错误,可能是旧版本后端,允许继续
|
||
Log.w(TAG, "检查主播资格返回错误,允许继续");
|
||
streamerVerified = true;
|
||
binding.btnStartLive.setEnabled(true);
|
||
checkPermissions();
|
||
return;
|
||
}
|
||
|
||
Map<String, Object> data = body.getData();
|
||
Boolean isStreamer = data.get("isStreamer") != null && (Boolean) data.get("isStreamer");
|
||
Boolean isBanned = data.get("isBanned") != null && (Boolean) data.get("isBanned");
|
||
Boolean hasApplication = data.get("hasApplication") != null && (Boolean) data.get("hasApplication");
|
||
Object appStatusObj = data.get("applicationStatus");
|
||
Integer applicationStatus = appStatusObj != null ? ((Number) appStatusObj).intValue() : null;
|
||
|
||
if (isBanned) {
|
||
// 被封禁
|
||
String banReason = (String) data.get("banReason");
|
||
showBlockedDialog("您的主播资格已被封禁" + (banReason != null ? ":" + banReason : ""));
|
||
return;
|
||
}
|
||
|
||
if (!isStreamer) {
|
||
// 不是主播
|
||
if (hasApplication && applicationStatus != null && applicationStatus == 0) {
|
||
// 有待审核的申请
|
||
showBlockedDialog("您的主播认证申请正在审核中,请耐心等待");
|
||
} else {
|
||
// 没有申请或申请被拒绝,提示申请认证
|
||
showApplyStreamerDialog();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 是认证主播,可以开播
|
||
streamerVerified = true;
|
||
binding.btnStartLive.setEnabled(true);
|
||
checkPermissions();
|
||
}
|
||
|
||
@Override
|
||
public void onFailure(Call<ApiResponse<Map<String, Object>>> call, Throwable t) {
|
||
binding.progressBar.setVisibility(View.GONE);
|
||
// 网络错误,可能是旧版本后端,允许继续
|
||
Log.w(TAG, "检查主播资格网络错误,允许继续", t);
|
||
streamerVerified = true;
|
||
binding.btnStartLive.setEnabled(true);
|
||
checkPermissions();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 显示被阻止的对话框
|
||
*/
|
||
private void showBlockedDialog(String message) {
|
||
new AlertDialog.Builder(this)
|
||
.setTitle("无法开播")
|
||
.setMessage(message)
|
||
.setPositiveButton("确定", (dialog, which) -> finish())
|
||
.setCancelable(false)
|
||
.show();
|
||
}
|
||
|
||
/**
|
||
* 显示申请主播认证的对话框
|
||
*/
|
||
private void showApplyStreamerDialog() {
|
||
new AlertDialog.Builder(this)
|
||
.setTitle("需要主播认证")
|
||
.setMessage("只有认证主播才能开播,是否现在申请主播认证?")
|
||
.setPositiveButton("去申请", (dialog, which) -> {
|
||
// 跳转到主播认证申请页面
|
||
Intent intent = new Intent(this, StreamerApplyActivity.class);
|
||
startActivity(intent);
|
||
finish();
|
||
})
|
||
.setNegativeButton("取消", (dialog, which) -> finish())
|
||
.setCancelable(false)
|
||
.show();
|
||
}
|
||
|
||
private void setupUI() {
|
||
// 关闭按钮
|
||
binding.btnClose.setOnClickListener(v -> {
|
||
if (isStreaming) {
|
||
showStopConfirmDialog();
|
||
} else {
|
||
finish();
|
||
}
|
||
});
|
||
|
||
// 切换摄像头
|
||
binding.btnSwitchCamera.setOnClickListener(v -> switchCamera());
|
||
|
||
// 设置按钮
|
||
binding.btnSettings.setOnClickListener(v -> {
|
||
Toast.makeText(this, "设置功能开发中", Toast.LENGTH_SHORT).show();
|
||
});
|
||
|
||
// 开始直播
|
||
binding.btnStartLive.setOnClickListener(v -> startLive());
|
||
|
||
// 停止直播
|
||
binding.btnStopLive.setOnClickListener(v -> showStopConfirmDialog());
|
||
}
|
||
|
||
private void setupSurface() {
|
||
binding.surfaceView.getHolder().addCallback(this);
|
||
}
|
||
|
||
private void checkPermissions() {
|
||
boolean allGranted = true;
|
||
for (String permission : REQUIRED_PERMISSIONS) {
|
||
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
|
||
allGranted = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (allGranted) {
|
||
initCamera();
|
||
} else {
|
||
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_PERMISSIONS);
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||
if (requestCode == REQUEST_PERMISSIONS) {
|
||
boolean allGranted = true;
|
||
for (int result : grantResults) {
|
||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||
allGranted = false;
|
||
break;
|
||
}
|
||
}
|
||
if (allGranted) {
|
||
initCamera();
|
||
} else {
|
||
Toast.makeText(this, "需要摄像头和麦克风权限才能开播", Toast.LENGTH_LONG).show();
|
||
finish();
|
||
}
|
||
}
|
||
}
|
||
|
||
private void initCamera() {
|
||
if (!surfaceReady) {
|
||
Log.d(TAG, "Surface 未就绪,等待...");
|
||
return;
|
||
}
|
||
|
||
if (cameraInitialized) {
|
||
Log.d(TAG, "摄像头已初始化");
|
||
return;
|
||
}
|
||
|
||
// 检查是否有摄像头
|
||
if (!hasCamera()) {
|
||
Log.e(TAG, "设备没有摄像头");
|
||
Toast.makeText(this, "设备没有摄像头,无法开播", Toast.LENGTH_LONG).show();
|
||
return;
|
||
}
|
||
|
||
Log.d(TAG, "开始初始化摄像头...");
|
||
|
||
// 延迟初始化,确保 Surface 完全准备好
|
||
mainHandler.postDelayed(() -> {
|
||
try {
|
||
initCameraInternal();
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "摄像头初始化异常: " + e.getMessage(), e);
|
||
Toast.makeText(this, "摄像头初始化失败", Toast.LENGTH_LONG).show();
|
||
}
|
||
}, 500);
|
||
}
|
||
|
||
private void initCameraInternal() {
|
||
// 优先尝试 Camera2 API (Android 5.0+)
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||
try {
|
||
Log.d(TAG, "尝试使用 Camera2 API...");
|
||
rtmpCamera2 = new RtmpCamera2(binding.surfaceView, this);
|
||
|
||
// 先开始预览,再准备编码器(推流时再准备)
|
||
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(), e);
|
||
if (rtmpCamera2 != null) {
|
||
try { rtmpCamera2.stopPreview(); } catch (Exception ignored) {}
|
||
}
|
||
rtmpCamera2 = null;
|
||
}
|
||
}
|
||
|
||
// 回退到 Camera1 API
|
||
try {
|
||
Log.d(TAG, "尝试使用 Camera1 API...");
|
||
rtmpCamera1 = new RtmpCamera1(binding.surfaceView, this);
|
||
|
||
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(), e);
|
||
if (rtmpCamera1 != null) {
|
||
try { rtmpCamera1.stopPreview(); } catch (Exception ignored) {}
|
||
}
|
||
rtmpCamera1 = null;
|
||
}
|
||
|
||
Toast.makeText(this, "摄像头初始化失败,请检查权限或重启应用", Toast.LENGTH_LONG).show();
|
||
}
|
||
|
||
/**
|
||
* 检查设备是否有摄像头
|
||
*/
|
||
private boolean hasCamera() {
|
||
return getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||
}
|
||
|
||
private void switchCamera() {
|
||
isFrontCamera = !isFrontCamera;
|
||
|
||
if (useCamera2 && rtmpCamera2 != null) {
|
||
try {
|
||
String cameraId = isFrontCamera ? "1" : "0";
|
||
rtmpCamera2.switchCamera();
|
||
Toast.makeText(this, isFrontCamera ? "前置摄像头" : "后置摄像头", Toast.LENGTH_SHORT).show();
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "切换摄像头失败: " + e.getMessage());
|
||
}
|
||
} else if (rtmpCamera1 != null) {
|
||
try {
|
||
rtmpCamera1.switchCamera();
|
||
Toast.makeText(this, isFrontCamera ? "前置摄像头" : "后置摄像头", Toast.LENGTH_SHORT).show();
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "切换摄像头失败: " + e.getMessage());
|
||
}
|
||
}
|
||
}
|
||
|
||
private void startLive() {
|
||
// 检查主播资格是否已验证
|
||
if (!streamerVerified) {
|
||
Toast.makeText(this, "正在验证主播资格...", Toast.LENGTH_SHORT).show();
|
||
checkStreamerStatus();
|
||
return;
|
||
}
|
||
|
||
String title = binding.etTitle.getText() != null ?
|
||
binding.etTitle.getText().toString().trim() : "";
|
||
|
||
if (TextUtils.isEmpty(title)) {
|
||
Toast.makeText(this, "请输入直播标题", Toast.LENGTH_SHORT).show();
|
||
return;
|
||
}
|
||
|
||
if (!cameraInitialized) {
|
||
Toast.makeText(this, "摄像头未初始化,请稍候", Toast.LENGTH_SHORT).show();
|
||
return;
|
||
}
|
||
|
||
binding.progressBar.setVisibility(View.VISIBLE);
|
||
binding.btnStartLive.setEnabled(false);
|
||
|
||
// 先创建直播间
|
||
createRoom(title);
|
||
}
|
||
|
||
private void createRoom(String title) {
|
||
String nickname = AuthStore.getNickname(this);
|
||
if (TextUtils.isEmpty(nickname)) {
|
||
nickname = "主播";
|
||
}
|
||
|
||
CreateRoomRequest request = new CreateRoomRequest();
|
||
request.setTitle(title);
|
||
request.setStreamerName(nickname);
|
||
|
||
ApiClient.getService(this).createRoom(request).enqueue(new Callback<ApiResponse<Room>>() {
|
||
@Override
|
||
public void onResponse(Call<ApiResponse<Room>> call, Response<ApiResponse<Room>> response) {
|
||
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
|
||
currentRoom = response.body().getData();
|
||
Log.d(TAG, "直播间创建成功: " + currentRoom.getId());
|
||
startStreaming();
|
||
} else {
|
||
binding.progressBar.setVisibility(View.GONE);
|
||
binding.btnStartLive.setEnabled(true);
|
||
String msg = response.body() != null ? response.body().getMessage() : "创建直播间失败";
|
||
Toast.makeText(BroadcastActivity.this, msg, Toast.LENGTH_SHORT).show();
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void onFailure(Call<ApiResponse<Room>> call, Throwable t) {
|
||
binding.progressBar.setVisibility(View.GONE);
|
||
binding.btnStartLive.setEnabled(true);
|
||
Toast.makeText(BroadcastActivity.this, "网络错误: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||
}
|
||
});
|
||
}
|
||
|
||
private void startStreaming() {
|
||
if (currentRoom == null) {
|
||
Log.e(TAG, "currentRoom 为空");
|
||
binding.progressBar.setVisibility(View.GONE);
|
||
binding.btnStartLive.setEnabled(true);
|
||
Toast.makeText(this, "获取房间信息失败", Toast.LENGTH_SHORT).show();
|
||
return;
|
||
}
|
||
|
||
if (currentRoom.getStreamUrls() == null) {
|
||
Log.e(TAG, "streamUrls 为空, roomId=" + currentRoom.getId() + ", streamKey=" + currentRoom.getStreamKey());
|
||
binding.progressBar.setVisibility(View.GONE);
|
||
binding.btnStartLive.setEnabled(true);
|
||
Toast.makeText(this, "获取推流地址失败", Toast.LENGTH_SHORT).show();
|
||
return;
|
||
}
|
||
|
||
String rtmpUrl = currentRoom.getStreamUrls().getRtmp();
|
||
String flvUrl = currentRoom.getStreamUrls().getFlv();
|
||
String hlsUrl = currentRoom.getStreamUrls().getHls();
|
||
|
||
Log.d(TAG, "========== 流地址信息 ==========");
|
||
Log.d(TAG, "房间ID: " + currentRoom.getId());
|
||
Log.d(TAG, "StreamKey: " + currentRoom.getStreamKey());
|
||
Log.d(TAG, "RTMP推流地址: " + rtmpUrl);
|
||
Log.d(TAG, "FLV播放地址: " + flvUrl);
|
||
Log.d(TAG, "HLS播放地址: " + hlsUrl);
|
||
Log.d(TAG, "================================");
|
||
|
||
if (TextUtils.isEmpty(rtmpUrl)) {
|
||
Log.e(TAG, "RTMP推流地址为空");
|
||
binding.progressBar.setVisibility(View.GONE);
|
||
binding.btnStartLive.setEnabled(true);
|
||
Toast.makeText(this, "推流地址无效", Toast.LENGTH_SHORT).show();
|
||
return;
|
||
}
|
||
|
||
Log.d(TAG, "开始推流到: " + rtmpUrl);
|
||
|
||
try {
|
||
if (useCamera2 && rtmpCamera2 != null) {
|
||
Log.d(TAG, "使用 Camera2 API 推流");
|
||
|
||
// 推流前准备编码器 - 使用优化后的低码率参数
|
||
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 推流");
|
||
|
||
// 推流前准备编码器 - 使用优化参数
|
||
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);
|
||
binding.btnStartLive.setEnabled(true);
|
||
Toast.makeText(this, "摄像头未初始化", Toast.LENGTH_SHORT).show();
|
||
}
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "推流失败: " + e.getMessage(), e);
|
||
binding.progressBar.setVisibility(View.GONE);
|
||
binding.btnStartLive.setEnabled(true);
|
||
Toast.makeText(this, "推流失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||
}
|
||
}
|
||
|
||
private void stopStreaming() {
|
||
try {
|
||
if (useCamera2 && rtmpCamera2 != null && rtmpCamera2.isStreaming()) {
|
||
rtmpCamera2.stopStream();
|
||
} else if (rtmpCamera1 != null && rtmpCamera1.isStreaming()) {
|
||
rtmpCamera1.stopStream();
|
||
}
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "停止推流失败: " + e.getMessage());
|
||
}
|
||
|
||
isStreaming = false;
|
||
stopTimer();
|
||
updateUI(false);
|
||
|
||
// 删除直播间
|
||
if (currentRoom != null) {
|
||
ApiClient.getService(this).deleteRoom(currentRoom.getId()).enqueue(new Callback<ApiResponse<Object>>() {
|
||
@Override
|
||
public void onResponse(Call<ApiResponse<Object>> call, Response<ApiResponse<Object>> response) {
|
||
Log.d(TAG, "直播间已删除");
|
||
}
|
||
|
||
@Override
|
||
public void onFailure(Call<ApiResponse<Object>> call, Throwable t) {
|
||
Log.e(TAG, "删除直播间失败: " + t.getMessage());
|
||
}
|
||
});
|
||
currentRoom = null;
|
||
}
|
||
}
|
||
|
||
private void showStopConfirmDialog() {
|
||
new com.google.android.material.dialog.MaterialAlertDialogBuilder(this)
|
||
.setTitle("结束直播")
|
||
.setMessage("确定要结束直播吗?")
|
||
.setPositiveButton("结束", (dialog, which) -> {
|
||
stopStreaming();
|
||
finish();
|
||
})
|
||
.setNegativeButton("取消", null)
|
||
.show();
|
||
}
|
||
|
||
private void updateUI(boolean streaming) {
|
||
if (streaming) {
|
||
binding.cardLiveInfo.setVisibility(View.GONE);
|
||
binding.btnStartLive.setVisibility(View.GONE);
|
||
binding.btnStopLive.setVisibility(View.VISIBLE);
|
||
binding.liveStatusBar.setVisibility(View.VISIBLE);
|
||
} else {
|
||
binding.cardLiveInfo.setVisibility(View.VISIBLE);
|
||
binding.btnStartLive.setVisibility(View.VISIBLE);
|
||
binding.btnStopLive.setVisibility(View.GONE);
|
||
binding.liveStatusBar.setVisibility(View.GONE);
|
||
}
|
||
binding.progressBar.setVisibility(View.GONE);
|
||
binding.btnStartLive.setEnabled(true);
|
||
}
|
||
|
||
private void startTimer() {
|
||
startTime = System.currentTimeMillis();
|
||
timerRunnable = new Runnable() {
|
||
@Override
|
||
public void run() {
|
||
long elapsed = System.currentTimeMillis() - startTime;
|
||
int seconds = (int) (elapsed / 1000) % 60;
|
||
int minutes = (int) (elapsed / 1000 / 60) % 60;
|
||
int hours = (int) (elapsed / 1000 / 60 / 60);
|
||
binding.tvLiveTime.setText(String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds));
|
||
timerHandler.postDelayed(this, 1000);
|
||
}
|
||
};
|
||
timerHandler.post(timerRunnable);
|
||
}
|
||
|
||
private void stopTimer() {
|
||
if (timerRunnable != null) {
|
||
timerHandler.removeCallbacks(timerRunnable);
|
||
timerRunnable = null;
|
||
}
|
||
}
|
||
|
||
// ========== SurfaceHolder.Callback ==========
|
||
|
||
@Override
|
||
public void surfaceCreated(@NonNull SurfaceHolder holder) {
|
||
Log.d(TAG, "Surface created");
|
||
surfaceReady = true;
|
||
initCamera();
|
||
}
|
||
|
||
@Override
|
||
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
|
||
Log.d(TAG, "Surface changed: " + width + "x" + height);
|
||
}
|
||
|
||
@Override
|
||
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
|
||
Log.d(TAG, "Surface destroyed");
|
||
surfaceReady = false;
|
||
|
||
try {
|
||
if (rtmpCamera2 != null) {
|
||
if (rtmpCamera2.isStreaming()) {
|
||
rtmpCamera2.stopStream();
|
||
}
|
||
if (rtmpCamera2.isOnPreview()) {
|
||
rtmpCamera2.stopPreview();
|
||
}
|
||
}
|
||
if (rtmpCamera1 != null) {
|
||
if (rtmpCamera1.isStreaming()) {
|
||
rtmpCamera1.stopStream();
|
||
}
|
||
if (rtmpCamera1.isOnPreview()) {
|
||
rtmpCamera1.stopPreview();
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "停止预览失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
// ========== ConnectCheckerRtmp 回调 ==========
|
||
|
||
@Override
|
||
public void onConnectionStartedRtmp(String rtmpUrl) {
|
||
Log.d(TAG, "========== RTMP连接开始 ==========");
|
||
Log.d(TAG, "正在连接: " + rtmpUrl);
|
||
}
|
||
|
||
@Override
|
||
public void onConnectionSuccessRtmp() {
|
||
runOnUiThread(() -> {
|
||
Log.d(TAG, "========== RTMP连接成功 ==========");
|
||
Log.d(TAG, "推流已成功连接到服务器");
|
||
isStreaming = true;
|
||
updateUI(true);
|
||
startTimer();
|
||
Toast.makeText(this, "直播已开始", Toast.LENGTH_SHORT).show();
|
||
});
|
||
}
|
||
|
||
@Override
|
||
public void onConnectionFailedRtmp(String reason) {
|
||
runOnUiThread(() -> {
|
||
Log.e(TAG, "========== RTMP连接失败 ==========");
|
||
Log.e(TAG, "失败原因: " + reason);
|
||
Log.e(TAG, "请检查:");
|
||
Log.e(TAG, "1. SRS服务器是否运行");
|
||
Log.e(TAG, "2. RTMP端口(1935/25002)是否开放");
|
||
Log.e(TAG, "3. 手机网络是否能访问服务器");
|
||
isStreaming = false;
|
||
updateUI(false);
|
||
Toast.makeText(this, "连接失败: " + reason, Toast.LENGTH_LONG).show();
|
||
});
|
||
}
|
||
|
||
@Override
|
||
public void onNewBitrateRtmp(long bitrate) {
|
||
// 码率变化回调,可用于显示当前上传速度
|
||
Log.v(TAG, "当前码率: " + (bitrate / 1024) + " kbps");
|
||
}
|
||
|
||
@Override
|
||
public void onDisconnectRtmp() {
|
||
runOnUiThread(() -> {
|
||
Log.d(TAG, "推流断开");
|
||
if (isStreaming) {
|
||
isStreaming = false;
|
||
updateUI(false);
|
||
Toast.makeText(this, "直播已断开", Toast.LENGTH_SHORT).show();
|
||
}
|
||
});
|
||
}
|
||
|
||
@Override
|
||
public void onAuthErrorRtmp() {
|
||
runOnUiThread(() -> {
|
||
Log.e(TAG, "推流认证失败");
|
||
Toast.makeText(this, "推流认证失败", Toast.LENGTH_SHORT).show();
|
||
});
|
||
}
|
||
|
||
@Override
|
||
public void onAuthSuccessRtmp() {
|
||
Log.d(TAG, "推流认证成功");
|
||
}
|
||
|
||
@Override
|
||
protected void onResume() {
|
||
super.onResume();
|
||
if (cameraInitialized && surfaceReady && !isStreaming) {
|
||
try {
|
||
if (useCamera2 && rtmpCamera2 != null && !rtmpCamera2.isOnPreview()) {
|
||
String cameraId = isFrontCamera ? "1" : "0";
|
||
rtmpCamera2.startPreview(cameraId);
|
||
} else if (rtmpCamera1 != null && !rtmpCamera1.isOnPreview()) {
|
||
CameraHelper.Facing facing = isFrontCamera ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK;
|
||
rtmpCamera1.startPreview(facing);
|
||
}
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "恢复预览失败: " + e.getMessage());
|
||
}
|
||
}
|
||
}
|
||
|
||
@Override
|
||
protected void onPause() {
|
||
super.onPause();
|
||
// 如果正在直播,不停止预览
|
||
if (!isStreaming) {
|
||
try {
|
||
if (rtmpCamera2 != null && rtmpCamera2.isOnPreview()) {
|
||
rtmpCamera2.stopPreview();
|
||
}
|
||
if (rtmpCamera1 != null && rtmpCamera1.isOnPreview()) {
|
||
rtmpCamera1.stopPreview();
|
||
}
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "暂停预览失败: " + e.getMessage());
|
||
}
|
||
}
|
||
}
|
||
|
||
@Override
|
||
protected void onDestroy() {
|
||
super.onDestroy();
|
||
stopTimer();
|
||
|
||
try {
|
||
if (rtmpCamera2 != null) {
|
||
if (rtmpCamera2.isStreaming()) {
|
||
rtmpCamera2.stopStream();
|
||
}
|
||
if (rtmpCamera2.isOnPreview()) {
|
||
rtmpCamera2.stopPreview();
|
||
}
|
||
}
|
||
if (rtmpCamera1 != null) {
|
||
if (rtmpCamera1.isStreaming()) {
|
||
rtmpCamera1.stopStream();
|
||
}
|
||
if (rtmpCamera1.isOnPreview()) {
|
||
rtmpCamera1.stopPreview();
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "销毁时清理失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void onBackPressed() {
|
||
if (isStreaming) {
|
||
showStopConfirmDialog();
|
||
} else {
|
||
super.onBackPressed();
|
||
}
|
||
}
|
||
}
|