2026-01-03 12:24:05 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-01-04 20:50:37 +08:00
|
|
|
|
// 推流参数 - 平衡画质和流畅度
|
|
|
|
|
|
private static final int VIDEO_WIDTH = 720;
|
2026-01-03 12:24:05 +08:00
|
|
|
|
private static final int VIDEO_HEIGHT = 480;
|
2026-01-04 20:50:37 +08:00
|
|
|
|
private static final int VIDEO_FPS = 24;
|
|
|
|
|
|
private static final int VIDEO_BITRATE = 1200 * 1024; // 1.2Mbps
|
2026-01-03 12:24:05 +08:00
|
|
|
|
private static final int AUDIO_BITRATE = 64 * 1024;
|
2026-01-04 20:50:37 +08:00
|
|
|
|
private static final int AUDIO_SAMPLE_RATE = 44100;
|
2026-01-03 12:24:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 直播计时
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-01-04 20:50:37 +08:00
|
|
|
|
// 先开始预览,再准备编码器(推流时再准备)
|
|
|
|
|
|
String cameraId = isFrontCamera ? "1" : "0";
|
|
|
|
|
|
rtmpCamera2.startPreview(cameraId);
|
|
|
|
|
|
useCamera2 = true;
|
|
|
|
|
|
cameraInitialized = true;
|
|
|
|
|
|
Log.d(TAG, "Camera2 预览已开始");
|
|
|
|
|
|
return;
|
2026-01-03 12:24:05 +08:00
|
|
|
|
} catch (Exception e) {
|
2026-01-04 20:50:37 +08:00
|
|
|
|
Log.w(TAG, "Camera2 初始化失败: " + e.getMessage(), e);
|
|
|
|
|
|
if (rtmpCamera2 != null) {
|
|
|
|
|
|
try { rtmpCamera2.stopPreview(); } catch (Exception ignored) {}
|
|
|
|
|
|
}
|
2026-01-03 12:24:05 +08:00
|
|
|
|
rtmpCamera2 = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 回退到 Camera1 API
|
|
|
|
|
|
try {
|
|
|
|
|
|
Log.d(TAG, "尝试使用 Camera1 API...");
|
|
|
|
|
|
rtmpCamera1 = new RtmpCamera1(binding.surfaceView, this);
|
|
|
|
|
|
|
2026-01-04 20:50:37 +08:00
|
|
|
|
CameraHelper.Facing facing = isFrontCamera ? CameraHelper.Facing.FRONT : CameraHelper.Facing.BACK;
|
|
|
|
|
|
rtmpCamera1.startPreview(facing);
|
|
|
|
|
|
useCamera2 = false;
|
|
|
|
|
|
cameraInitialized = true;
|
|
|
|
|
|
Log.d(TAG, "Camera1 预览已开始");
|
|
|
|
|
|
return;
|
2026-01-03 12:24:05 +08:00
|
|
|
|
} catch (Exception e) {
|
2026-01-04 20:50:37 +08:00
|
|
|
|
Log.e(TAG, "Camera1 初始化也失败: " + e.getMessage(), e);
|
|
|
|
|
|
if (rtmpCamera1 != null) {
|
|
|
|
|
|
try { rtmpCamera1.stopPreview(); } catch (Exception ignored) {}
|
|
|
|
|
|
}
|
2026-01-03 12:24:05 +08:00
|
|
|
|
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 {
|
2026-01-04 20:50:37 +08:00
|
|
|
|
if (useCamera2 && rtmpCamera2 != null) {
|
2026-01-03 12:24:05 +08:00
|
|
|
|
Log.d(TAG, "使用 Camera2 API 推流");
|
2026-01-04 20:50:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 推流前准备编码器 - 使用优化后的低码率参数
|
|
|
|
|
|
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) {
|
2026-01-03 12:24:05 +08:00
|
|
|
|
Log.d(TAG, "使用 Camera1 API 推流");
|
2026-01-04 20:50:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 推流前准备编码器 - 使用优化参数
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-01-03 12:24:05 +08:00
|
|
|
|
} 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|