功能:增加语音和视频通话的功能

This commit is contained in:
xiao12feng8 2025-12-30 16:08:43 +08:00
parent b97c82899f
commit c37cf4884b
14 changed files with 1561 additions and 67 deletions

View File

@ -229,4 +229,3 @@ public class CallController {
return response; return response;
} }
} }

View File

@ -259,10 +259,7 @@ public class LiveRoomController {
} }
} }
// ========== 关注主播接口 ========== // ========== 直播控制接口 ==========
@Autowired
private com.zbkj.service.service.FollowRecordService followRecordService;
@ApiOperation(value = "开始直播") @ApiOperation(value = "开始直播")
@PostMapping("/room/{id}/start") @PostMapping("/room/{id}/start")

View File

@ -53,6 +53,8 @@ public class CallSignalingHandler extends TextWebSocketHandler {
@Override @Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception { public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("[CallSignaling] ========== 连接建立 ==========");
System.out.println("[CallSignaling] sessionId=" + session.getId());
logger.info("[CallSignaling] 连接建立: sessionId={}", session.getId()); logger.info("[CallSignaling] 连接建立: sessionId={}", session.getId());
} }
@ -63,6 +65,9 @@ public class CallSignalingHandler extends TextWebSocketHandler {
String type = json.has("type") ? json.get("type").asText() : ""; String type = json.has("type") ? json.get("type").asText() : "";
switch (type) { switch (type) {
case "ping":
handlePing(session);
break;
case "register": case "register":
handleRegister(session, json); handleRegister(session, json);
break; break;
@ -97,6 +102,9 @@ public class CallSignalingHandler extends TextWebSocketHandler {
private void handleRegister(WebSocketSession session, JsonNode json) throws IOException { private void handleRegister(WebSocketSession session, JsonNode json) throws IOException {
Integer userId = json.has("userId") ? json.get("userId").asInt() : null; Integer userId = json.has("userId") ? json.get("userId").asInt() : null;
System.out.println("[CallSignaling] ========== 用户注册 ==========");
System.out.println("[CallSignaling] userId=" + userId);
if (userId == null) { if (userId == null) {
sendError(session, "userId不能为空"); sendError(session, "userId不能为空");
return; return;
@ -105,6 +113,7 @@ public class CallSignalingHandler extends TextWebSocketHandler {
// 关闭旧连接 // 关闭旧连接
WebSocketSession oldSession = userCallSessions.get(userId); WebSocketSession oldSession = userCallSessions.get(userId);
if (oldSession != null && oldSession.isOpen() && !oldSession.getId().equals(session.getId())) { if (oldSession != null && oldSession.isOpen() && !oldSession.getId().equals(session.getId())) {
System.out.println("[CallSignaling] 关闭旧连接: userId=" + userId);
logger.info("[CallSignaling] 关闭旧连接: userId={}, oldSessionId={}", userId, oldSession.getId()); logger.info("[CallSignaling] 关闭旧连接: userId={}, oldSessionId={}", userId, oldSession.getId());
try { try {
oldSession.close(); oldSession.close();
@ -118,6 +127,8 @@ public class CallSignalingHandler extends TextWebSocketHandler {
response.put("type", "registered"); response.put("type", "registered");
response.put("userId", userId); response.put("userId", userId);
session.sendMessage(new TextMessage(response.toString())); session.sendMessage(new TextMessage(response.toString()));
System.out.println("[CallSignaling] 用户注册成功: userId=" + userId + ", 当前在线用户=" + userCallSessions.keySet());
logger.info("[CallSignaling] 用户注册成功: userId={}, sessionId={}, 当前在线用户数={}", logger.info("[CallSignaling] 用户注册成功: userId={}, sessionId={}, 当前在线用户数={}",
userId, session.getId(), userCallSessions.size()); userId, session.getId(), userCallSessions.size());
} }
@ -328,15 +339,38 @@ public class CallSignalingHandler extends TextWebSocketHandler {
private void handleSignaling(WebSocketSession session, JsonNode json, String type) throws IOException { private void handleSignaling(WebSocketSession session, JsonNode json, String type) throws IOException {
String callId = json.has("callId") ? json.get("callId").asText() : sessionCallMap.get(session.getId()); String callId = json.has("callId") ? json.get("callId").asText() : sessionCallMap.get(session.getId());
Integer senderId = sessionUserMap.get(session.getId());
System.out.println("[CallSignaling] ========== 处理信令消息 ==========");
System.out.println("[CallSignaling] type=" + type + ", callId=" + callId + ", senderId=" + senderId);
System.out.println("[CallSignaling] senderSessionId=" + session.getId());
if (callId == null) { if (callId == null) {
sendError(session, "callId不能为空"); sendError(session, "callId不能为空");
return; return;
} }
// 确保通话会话存在
Set<WebSocketSession> sessions = callSessions.get(callId); Set<WebSocketSession> sessions = callSessions.get(callId);
if (sessions == null) { if (sessions == null) {
sendError(session, "通话不存在"); // 尝试创建会话可能是REST API发起的通话
return; System.out.println("[CallSignaling] 通话会话不存在,创建新会话: callId=" + callId);
logger.warn("[CallSignaling] 通话会话不存在,尝试创建: callId={}", callId);
sessions = callSessions.computeIfAbsent(callId, k -> new CopyOnWriteArraySet<>());
}
System.out.println("[CallSignaling] 当前会话中的参与者数量: " + sessions.size());
for (WebSocketSession s : sessions) {
Integer userId = sessionUserMap.get(s.getId());
System.out.println("[CallSignaling] - sessionId=" + s.getId() + ", userId=" + userId + ", isOpen=" + s.isOpen());
}
// 确保当前用户在通话会话中
if (!sessions.contains(session)) {
sessions.add(session);
sessionCallMap.put(session.getId(), callId);
System.out.println("[CallSignaling] 将发送者加入通话会话: sessionId=" + session.getId() + ", userId=" + senderId);
logger.info("[CallSignaling] 将用户加入通话会话: callId={}, sessionId={}", callId, session.getId());
} }
// 转发信令给通话中的其他参与者 // 转发信令给通话中的其他参与者
@ -351,13 +385,22 @@ public class CallSignalingHandler extends TextWebSocketHandler {
} }
String forwardMsg = forward.toString(); String forwardMsg = forward.toString();
int forwardCount = 0;
for (WebSocketSession s : sessions) { for (WebSocketSession s : sessions) {
if (s.isOpen() && !s.getId().equals(session.getId())) { Integer targetUserId = sessionUserMap.get(s.getId());
boolean isSender = s.getId().equals(session.getId());
System.out.println("[CallSignaling] 检查转发目标: targetSessionId=" + s.getId() +
", targetUserId=" + targetUserId + ", isOpen=" + s.isOpen() + ", isSender=" + isSender);
if (s.isOpen() && !isSender) {
s.sendMessage(new TextMessage(forwardMsg)); s.sendMessage(new TextMessage(forwardMsg));
forwardCount++;
System.out.println("[CallSignaling] >>> 已转发给: userId=" + targetUserId);
} }
} }
logger.debug("[CallSignaling] 转发信令: type={}, callId={}", type, callId); System.out.println("[CallSignaling] 转发完成: type=" + type + ", 转发给" + forwardCount + "个用户");
logger.info("[CallSignaling] 转发信令: type={}, callId={}, senderId={}, 转发给{}个用户", type, callId, senderId, forwardCount);
} }
@Override @Override
@ -461,11 +504,49 @@ public class CallSignalingHandler extends TextWebSocketHandler {
return "calling".equals(status) || "ringing".equals(status) || "connected".equals(status); return "calling".equals(status) || "ringing".equals(status) || "connected".equals(status);
} }
private void sendError(WebSocketSession session, String message) throws IOException { /**
ObjectNode error = objectMapper.createObjectNode(); * 处理心跳 ping 消息
error.put("type", "error"); */
error.put("message", message); private void handlePing(WebSocketSession session) {
session.sendMessage(new TextMessage(error.toString())); try {
if (session != null && session.isOpen()) {
ObjectNode pong = objectMapper.createObjectNode();
pong.put("type", "pong");
session.sendMessage(new TextMessage(pong.toString()));
}
} catch (Exception e) {
logger.warn("[CallSignaling] 发送 pong 失败: {}", e.getMessage());
}
}
private void sendError(WebSocketSession session, String message) {
try {
if (session != null && session.isOpen()) {
synchronized (session) {
ObjectNode error = objectMapper.createObjectNode();
error.put("type", "error");
error.put("message", message);
session.sendMessage(new TextMessage(error.toString()));
}
}
} catch (Exception e) {
logger.warn("[CallSignaling] 发送错误消息失败: {}", e.getMessage());
}
}
/**
* 安全发送消息带同步锁
*/
private void sendMessage(WebSocketSession session, String message) {
try {
if (session != null && session.isOpen()) {
synchronized (session) {
session.sendMessage(new TextMessage(message));
}
}
} catch (Exception e) {
logger.warn("[CallSignaling] 发送消息失败: {}", e.getMessage());
}
} }
/** /**
@ -473,9 +554,27 @@ public class CallSignalingHandler extends TextWebSocketHandler {
*/ */
public void notifyIncomingCall(String callId, Integer callerId, String callerName, String callerAvatar, public void notifyIncomingCall(String callId, Integer callerId, String callerName, String callerAvatar,
Integer calleeId, String callType) { Integer calleeId, String callType) {
System.out.println("[CallSignaling] ========== 通知来电 ==========");
System.out.println("[CallSignaling] callId=" + callId + ", callerId=" + callerId + ", calleeId=" + calleeId);
System.out.println("[CallSignaling] 当前在线用户=" + userCallSessions.keySet());
try { try {
// 记录通话创建时间 // 记录通话创建时间
callCreateTime.put(callId, System.currentTimeMillis()); callCreateTime.put(callId, System.currentTimeMillis());
// 初始化通话会话重要这样后续的信令才能正确转发
callSessions.computeIfAbsent(callId, k -> new CopyOnWriteArraySet<>());
// 将主叫方加入通话会话
WebSocketSession callerSession = userCallSessions.get(callerId);
if (callerSession != null && callerSession.isOpen()) {
joinCallSession(callId, callerSession);
sessionCallMap.put(callerSession.getId(), callId);
System.out.println("[CallSignaling] 主叫方加入通话会话: callerId=" + callerId);
logger.info("[CallSignaling] 主叫方加入通话会话: callId={}, callerId={}", callId, callerId);
} else {
System.out.println("[CallSignaling] 主叫方未在线: callerId=" + callerId);
}
// 通知被叫方 // 通知被叫方
WebSocketSession calleeSession = userCallSessions.get(calleeId); WebSocketSession calleeSession = userCallSessions.get(calleeId);
@ -488,11 +587,14 @@ public class CallSignalingHandler extends TextWebSocketHandler {
incoming.put("callerAvatar", callerAvatar != null ? callerAvatar : ""); incoming.put("callerAvatar", callerAvatar != null ? callerAvatar : "");
incoming.put("callType", callType); incoming.put("callType", callType);
calleeSession.sendMessage(new TextMessage(incoming.toString())); calleeSession.sendMessage(new TextMessage(incoming.toString()));
System.out.println("[CallSignaling] 已发送来电通知给被叫方: calleeId=" + calleeId);
logger.info("[CallSignaling] 通知来电: callId={}, callee={}", callId, calleeId); logger.info("[CallSignaling] 通知来电: callId={}, callee={}", callId, calleeId);
} else { } else {
logger.warn("[CallSignaling] 被叫方未在线: calleeId={}", calleeId); System.out.println("[CallSignaling] !!!!! 被叫方未在线 !!!!! calleeId=" + calleeId);
logger.warn("[CallSignaling] 被叫方未在线: calleeId={}, 当前在线用户={}", calleeId, userCallSessions.keySet());
} }
} catch (Exception e) { } catch (Exception e) {
System.out.println("[CallSignaling] 通知来电异常: " + e.getMessage());
logger.error("[CallSignaling] 通知来电异常: callId={}", callId, e); logger.error("[CallSignaling] 通知来电异常: callId={}", callId, e);
} }
} }
@ -512,8 +614,15 @@ public class CallSignalingHandler extends TextWebSocketHandler {
logger.info("[CallSignaling] REST API调用notifyCallAccepted: callId={}, callerId={}, 当前在线用户={}", logger.info("[CallSignaling] REST API调用notifyCallAccepted: callId={}, callerId={}, 当前在线用户={}",
callId, callerId, userCallSessions.keySet()); callId, callerId, userCallSessions.keySet());
try { try {
// 确保通话会话存在
callSessions.computeIfAbsent(callId, k -> new CopyOnWriteArraySet<>());
WebSocketSession callerSession = userCallSessions.get(callerId); WebSocketSession callerSession = userCallSessions.get(callerId);
if (callerSession != null && callerSession.isOpen()) { if (callerSession != null && callerSession.isOpen()) {
// 确保主叫方在通话会话中
joinCallSession(callId, callerSession);
sessionCallMap.put(callerSession.getId(), callId);
ObjectNode notify = objectMapper.createObjectNode(); ObjectNode notify = objectMapper.createObjectNode();
notify.put("type", "call_accepted"); notify.put("type", "call_accepted");
notify.put("callId", callId); notify.put("callId", callId);

View File

@ -84,7 +84,8 @@ debug: true
logging: logging:
level: level:
io.swagger.*: error io.swagger.*: error
com.zbjk.crmeb: debug com.zbkj: debug
com.zbkj.front.websocket: info
org.springframework.boot.autoconfigure: ERROR org.springframework.boot.autoconfigure: ERROR
config: classpath:logback-spring.xml config: classpath:logback-spring.xml
file: file:

View File

@ -113,4 +113,8 @@ dependencies {
implementation("com.github.andnux:ijkplayer:0.0.1") { implementation("com.github.andnux:ijkplayer:0.0.1") {
exclude("com.google.android.exoplayer", "exoplayer") exclude("com.google.android.exoplayer", "exoplayer")
} }
// WebRTC for voice/video calls
// 使用 Google 官方 WebRTC 库
implementation("io.getstream:stream-webrtc-android:1.1.1")
} }

View File

@ -12,6 +12,13 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Android 10+ 需要此权限来显示来电全屏界面 -->
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- 前台服务权限,用于保持通话连接 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<!-- 系统警报窗口权限,用于在锁屏上显示来电 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application <application
android:name=".LiveStreamingApplication" android:name=".LiveStreamingApplication"

View File

@ -832,6 +832,10 @@ public class MainActivity extends AppCompatActivity {
// 更新未读消息徽章 // 更新未读消息徽章
UnreadMessageManager.updateBadge(bottomNavigation); UnreadMessageManager.updateBadge(bottomNavigation);
} }
// 确保通话信令 WebSocket 保持连接用于接收来电通知
LiveStreamingApplication app = (LiveStreamingApplication) getApplication();
app.connectCallSignalingIfLoggedIn();
} }
/** /**

View File

@ -1,27 +1,51 @@
package com.example.livestreaming.call; package com.example.livestreaming.call;
import android.Manifest;
import android.content.pm.PackageManager;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.example.livestreaming.R; import com.example.livestreaming.R;
/** import org.json.JSONException;
* 通话界面 import org.json.JSONObject;
*/ import org.webrtc.IceCandidate;
public class CallActivity extends AppCompatActivity implements CallManager.CallStateListener { import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.SessionDescription;
import org.webrtc.SurfaceViewRenderer;
import java.util.ArrayList;
import java.util.List;
/**
* 通话界面 - 集成 WebRTC
*/
public class CallActivity extends AppCompatActivity implements
CallManager.CallStateListener,
WebRTCClient.WebRTCListener {
private static final String TAG = "CallActivity";
private static final int PERMISSION_REQUEST_CODE = 100;
// UI 组件
private ImageView ivBackgroundAvatar; private ImageView ivBackgroundAvatar;
private ImageView ivAvatar; private ImageView ivAvatar;
private TextView tvUserName; private TextView tvUserName;
@ -36,11 +60,20 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
private ImageButton btnSwitchCamera; private ImageButton btnSwitchCamera;
private LinearLayout layoutCallControls; private LinearLayout layoutCallControls;
private LinearLayout layoutVideoToggle; private LinearLayout layoutVideoToggle;
private FrameLayout layoutLocalVideo;
private FrameLayout layoutRemoteVideo;
// WebRTC 视频渲染器
private SurfaceViewRenderer localRenderer;
private SurfaceViewRenderer remoteRenderer;
// 管理器
private CallManager callManager; private CallManager callManager;
private WebRTCClient webRTCClient;
private AudioManager audioManager; private AudioManager audioManager;
private Handler handler; private Handler handler;
// 通话信息
private String callId; private String callId;
private String callType; private String callType;
private boolean isCaller; private boolean isCaller;
@ -48,12 +81,18 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
private String otherUserName; private String otherUserName;
private String otherUserAvatar; private String otherUserAvatar;
// 状态
private boolean isMuted = false; private boolean isMuted = false;
private boolean isSpeakerOn = false; private boolean isSpeakerOn = false;
private boolean isVideoEnabled = true; private boolean isVideoEnabled = true;
private boolean isConnected = false; private boolean isConnected = false;
private boolean isWebRTCInitialized = false;
private long callStartTime = 0; private long callStartTime = 0;
// ICE Candidate 缓存在远程 SDP 设置前收到的
private List<IceCandidate> pendingIceCandidates = new ArrayList<>();
private boolean remoteDescriptionSet = false;
private Runnable durationRunnable = new Runnable() { private Runnable durationRunnable = new Runnable() {
@Override @Override
public void run() { public void run() {
@ -70,6 +109,8 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Log.d(TAG, "========== CallActivity onCreate ==========");
Toast.makeText(this, "通话界面已打开", Toast.LENGTH_SHORT).show();
// 保持屏幕常亮显示在锁屏上方 // 保持屏幕常亮显示在锁屏上方
getWindow().addFlags( getWindow().addFlags(
@ -82,9 +123,20 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
initViews(); initViews();
initData(); initData();
initCallManager();
setupListeners(); Log.d(TAG, "callId=" + callId + ", callType=" + callType + ", isCaller=" + isCaller);
updateUI(); Toast.makeText(this, "通话类型: " + callType + ", 主叫: " + isCaller, Toast.LENGTH_SHORT).show();
// 检查权限
if (checkPermissions()) {
Log.d(TAG, "权限已授予,初始化通话");
Toast.makeText(this, "权限OK初始化WebRTC", Toast.LENGTH_SHORT).show();
initCallAndWebRTC();
} else {
Log.d(TAG, "请求权限");
Toast.makeText(this, "请求权限中...", Toast.LENGTH_SHORT).show();
requestPermissions();
}
} }
private void initViews() { private void initViews() {
@ -102,9 +154,62 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
btnSwitchCamera = findViewById(R.id.btnSwitchCamera); btnSwitchCamera = findViewById(R.id.btnSwitchCamera);
layoutCallControls = findViewById(R.id.layoutCallControls); layoutCallControls = findViewById(R.id.layoutCallControls);
layoutVideoToggle = findViewById(R.id.layoutVideoToggle); layoutVideoToggle = findViewById(R.id.layoutVideoToggle);
layoutLocalVideo = findViewById(R.id.layoutLocalVideo);
layoutRemoteVideo = findViewById(R.id.layoutRemoteVideo);
handler = new Handler(Looper.getMainLooper()); handler = new Handler(Looper.getMainLooper());
audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
// 设置音频模式为通话模式重要WebRTC音频需要此设置
setupAudioForCall();
setupListeners();
}
/**
* 设置音频为通话模式
*/
private void setupAudioForCall() {
Log.d(TAG, "========== 设置音频模式 ==========");
try {
// 保存原始音频模式
int originalMode = audioManager.getMode();
Log.d(TAG, "原始音频模式: " + originalMode);
// 设置为通话模式
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
Log.d(TAG, "设置音频模式为 MODE_IN_COMMUNICATION");
// 请求音频焦点
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
android.media.AudioAttributes playbackAttributes = new android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_SPEECH)
.build();
android.media.AudioFocusRequest focusRequest = new android.media.AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
.setAudioAttributes(playbackAttributes)
.build();
audioManager.requestAudioFocus(focusRequest);
} else {
audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
}
Log.d(TAG, "已请求音频焦点");
// 默认使用听筒语音通话或扬声器视频通话
if ("video".equals(callType)) {
audioManager.setSpeakerphoneOn(true);
isSpeakerOn = true;
Log.d(TAG, "视频通话,默认开启扬声器");
} else {
audioManager.setSpeakerphoneOn(false);
isSpeakerOn = false;
Log.d(TAG, "语音通话,默认使用听筒");
}
Log.d(TAG, "音频设置完成");
} catch (Exception e) {
Log.e(TAG, "设置音频模式失败", e);
}
} }
private void initData() { private void initData() {
@ -118,36 +223,175 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
// 如果是被叫方接听直接进入通话状态 // 如果是被叫方接听直接进入通话状态
boolean alreadyConnected = getIntent().getBooleanExtra("isConnected", false); boolean alreadyConnected = getIntent().getBooleanExtra("isConnected", false);
if (alreadyConnected) { if (alreadyConnected) {
android.util.Log.d("CallActivity", "被叫方接听,直接进入通话状态"); Log.d(TAG, "被叫方接听,直接进入通话状态");
isConnected = true; isConnected = true;
callStartTime = System.currentTimeMillis(); callStartTime = System.currentTimeMillis();
} }
updateUI();
}
private boolean checkPermissions() {
boolean audioPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED;
boolean cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED;
if ("video".equals(callType)) {
return audioPermission && cameraPermission;
}
return audioPermission;
}
private void requestPermissions() {
List<String> permissions = new ArrayList<>();
permissions.add(Manifest.permission.RECORD_AUDIO);
if ("video".equals(callType)) {
permissions.add(Manifest.permission.CAMERA);
}
ActivityCompat.requestPermissions(this, permissions.toArray(new String[0]), PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
initCallAndWebRTC();
} else {
Toast.makeText(this, "需要麦克风和摄像头权限才能进行通话", Toast.LENGTH_LONG).show();
finish();
}
}
}
private void initCallAndWebRTC() {
initCallManager();
initWebRTC();
} }
private void initCallManager() { private void initCallManager() {
callManager = CallManager.getInstance(this); callManager = CallManager.getInstance(this);
callManager.setStateListener(this); callManager.setStateListener(this);
// 确保WebSocket已连接主叫方需要接收接听/拒绝通知 // 确保WebSocket已连接
String userId = com.example.livestreaming.net.AuthStore.getUserId(this); String userId = com.example.livestreaming.net.AuthStore.getUserId(this);
if (userId != null && !userId.isEmpty()) { if (userId != null && !userId.isEmpty()) {
try { try {
int uid = (int) Double.parseDouble(userId); int uid = (int) Double.parseDouble(userId);
if (uid > 0) { if (uid > 0) {
android.util.Log.d("CallActivity", "确保WebSocket连接userId: " + uid); Log.d(TAG, "确保WebSocket连接userId: " + uid);
callManager.connect(uid); callManager.connect(uid);
} }
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
android.util.Log.e("CallActivity", "解析用户ID失败", e); Log.e(TAG, "解析用户ID失败", e);
} }
} }
} }
private void initWebRTC() {
Log.d(TAG, "========== 初始化 WebRTC ==========");
Toast.makeText(this, "开始初始化WebRTC...", Toast.LENGTH_SHORT).show();
boolean isVideoCall = "video".equals(callType);
Log.d(TAG, "isVideoCall=" + isVideoCall + ", callType=" + callType);
try {
// 创建 WebRTC 客户端
Toast.makeText(this, "创建WebRTC客户端...", Toast.LENGTH_SHORT).show();
webRTCClient = new WebRTCClient(this);
webRTCClient.setListener(this);
Toast.makeText(this, "调用initialize...", Toast.LENGTH_SHORT).show();
webRTCClient.initialize(isVideoCall);
Log.d(TAG, "WebRTC 客户端创建成功");
Toast.makeText(this, "initialize完成!", Toast.LENGTH_SHORT).show();
// 如果是视频通话设置视频渲染器
if (isVideoCall) {
try {
Toast.makeText(this, "设置视频渲染器...", Toast.LENGTH_SHORT).show();
setupVideoRenderers();
Toast.makeText(this, "视频渲染器设置成功", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e(TAG, "视频渲染器设置失败", e);
Toast.makeText(this, "视频渲染器失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
// 创建本地媒体流
Log.d(TAG, "创建本地媒体流...");
try {
Toast.makeText(this, "创建本地媒体流...", Toast.LENGTH_SHORT).show();
webRTCClient.createLocalStream();
Toast.makeText(this, "本地媒体流创建成功", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e(TAG, "本地媒体流创建失败", e);
Toast.makeText(this, "媒体流失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
// 创建 PeerConnection
Log.d(TAG, "创建 PeerConnection...");
try {
webRTCClient.createPeerConnection();
Toast.makeText(this, "PeerConnection创建成功", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Log.e(TAG, "PeerConnection创建失败", e);
Toast.makeText(this, "PeerConnection失败: " + e.getMessage(), Toast.LENGTH_LONG).show();
return;
}
isWebRTCInitialized = true;
Log.d(TAG, "WebRTC 初始化完成");
Toast.makeText(this, "WebRTC初始化完成!", Toast.LENGTH_SHORT).show();
// 主叫方等待对方接听后再创建 Offer onCallConnected 中创建
// 被叫方等待收到 Offer 后创建 Answer
if (isCaller) {
Log.d(TAG, "主叫方,等待对方接听后创建 Offer");
Toast.makeText(this, "等待对方接听...", Toast.LENGTH_SHORT).show();
} else {
Log.d(TAG, "被叫方,等待 Offer");
Toast.makeText(this, "被叫方等待Offer...", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e(TAG, "WebRTC 初始化失败", e);
Toast.makeText(this, "WebRTC初始化失败: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
}
private void setupVideoRenderers() {
Log.d(TAG, "设置视频渲染器");
// 创建本地视频渲染器
localRenderer = new SurfaceViewRenderer(this);
layoutLocalVideo.addView(localRenderer);
webRTCClient.setLocalRenderer(localRenderer);
// 创建远程视频渲染器
remoteRenderer = new SurfaceViewRenderer(this);
layoutRemoteVideo.addView(remoteRenderer);
webRTCClient.setRemoteRenderer(remoteRenderer);
// 显示视频布局
layoutLocalVideo.setVisibility(View.VISIBLE);
layoutRemoteVideo.setVisibility(View.VISIBLE);
// 隐藏头像视频通话时
ivBackgroundAvatar.setVisibility(View.GONE);
ivAvatar.setVisibility(View.GONE);
}
private void setupListeners() { private void setupListeners() {
btnMinimize.setOnClickListener(v -> { btnMinimize.setOnClickListener(v -> moveTaskToBack(true));
// 最小化通话后台运行
moveTaskToBack(true);
});
btnMute.setOnClickListener(v -> toggleMute()); btnMute.setOnClickListener(v -> toggleMute());
btnSpeaker.setOnClickListener(v -> toggleSpeaker()); btnSpeaker.setOnClickListener(v -> toggleSpeaker());
@ -162,7 +406,7 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
} else { } else {
callManager.rejectCall(callId); callManager.rejectCall(callId);
} }
finish(); releaseAndFinish();
}); });
} }
@ -183,14 +427,11 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
// 根据连接状态设置界面 // 根据连接状态设置界面
if (isConnected) { if (isConnected) {
// 已接通显示通话中界面
tvCallStatus.setVisibility(View.GONE); tvCallStatus.setVisibility(View.GONE);
tvCallDuration.setVisibility(View.VISIBLE); tvCallDuration.setVisibility(View.VISIBLE);
layoutCallControls.setVisibility(View.VISIBLE); layoutCallControls.setVisibility(View.VISIBLE);
handler.post(durationRunnable); handler.post(durationRunnable);
android.util.Log.d("CallActivity", "updateUI: 已接通状态,显示计时器");
} else { } else {
// 未接通显示等待状态
if (isCaller) { if (isCaller) {
tvCallStatus.setText("正在呼叫..."); tvCallStatus.setText("正在呼叫...");
} else { } else {
@ -203,7 +444,10 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
isMuted = !isMuted; isMuted = !isMuted;
btnMute.setImageResource(isMuted ? R.drawable.ic_mic_off : R.drawable.ic_mic); btnMute.setImageResource(isMuted ? R.drawable.ic_mic_off : R.drawable.ic_mic);
btnMute.setBackgroundResource(isMuted ? R.drawable.bg_call_button_active : R.drawable.bg_call_button); btnMute.setBackgroundResource(isMuted ? R.drawable.bg_call_button_active : R.drawable.bg_call_button);
// TODO: 实际静音控制
if (webRTCClient != null) {
webRTCClient.setMuted(isMuted);
}
Toast.makeText(this, isMuted ? "已静音" : "已取消静音", Toast.LENGTH_SHORT).show(); Toast.makeText(this, isMuted ? "已静音" : "已取消静音", Toast.LENGTH_SHORT).show();
} }
@ -217,17 +461,35 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
private void toggleVideo() { private void toggleVideo() {
isVideoEnabled = !isVideoEnabled; isVideoEnabled = !isVideoEnabled;
btnVideo.setBackgroundResource(isVideoEnabled ? R.drawable.bg_call_button : R.drawable.bg_call_button_active); btnVideo.setBackgroundResource(isVideoEnabled ? R.drawable.bg_call_button : R.drawable.bg_call_button_active);
// TODO: 实际视频控制
if (webRTCClient != null) {
webRTCClient.setVideoEnabled(isVideoEnabled);
}
if (localRenderer != null) {
localRenderer.setVisibility(isVideoEnabled ? View.VISIBLE : View.GONE);
}
Toast.makeText(this, isVideoEnabled ? "已开启摄像头" : "已关闭摄像头", Toast.LENGTH_SHORT).show(); Toast.makeText(this, isVideoEnabled ? "已开启摄像头" : "已关闭摄像头", Toast.LENGTH_SHORT).show();
} }
private void switchCamera() { private void switchCamera() {
// TODO: 切换前后摄像头 if (webRTCClient != null) {
webRTCClient.switchCamera();
}
Toast.makeText(this, "切换摄像头", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "切换摄像头", Toast.LENGTH_SHORT).show();
} }
private void onCallConnected() { private void onCallConnected() {
android.util.Log.d("CallActivity", "onCallConnected() 开始执行"); Log.d(TAG, "========== 通话已连接 ==========");
Log.d(TAG, "isCaller=" + isCaller + ", isWebRTCInitialized=" + isWebRTCInitialized);
// 如果是主叫方收到 call_accepted现在创建 Offer
if (isCaller && isWebRTCInitialized && webRTCClient != null) {
Log.d(TAG, "主叫方收到接听通知,开始创建 Offer");
Toast.makeText(this, "对方已接听,建立连接...", Toast.LENGTH_SHORT).show();
webRTCClient.createOffer();
}
isConnected = true; isConnected = true;
callStartTime = System.currentTimeMillis(); callStartTime = System.currentTimeMillis();
@ -236,14 +498,22 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
layoutCallControls.setVisibility(View.VISIBLE); layoutCallControls.setVisibility(View.VISIBLE);
handler.post(durationRunnable); handler.post(durationRunnable);
android.util.Log.d("CallActivity", "onCallConnected() 执行完成,通话已连接");
Toast.makeText(this, "通话已接通", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "通话已接通", Toast.LENGTH_SHORT).show();
} }
// CallStateListener 实现 private void releaseAndFinish() {
if (webRTCClient != null) {
webRTCClient.release();
webRTCClient = null;
}
finish();
}
// ==================== CallStateListener 实现 ====================
@Override @Override
public void onCallStateChanged(String state, String callId) { public void onCallStateChanged(String state, String callId) {
// 状态变化处理 Log.d(TAG, "onCallStateChanged: " + state);
} }
@Override @Override
@ -253,13 +523,8 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
@Override @Override
public void onCallConnected(String callId) { public void onCallConnected(String callId) {
android.util.Log.d("CallActivity", "========== onCallConnected 被调用 =========="); Log.d(TAG, "onCallConnected: " + callId);
android.util.Log.d("CallActivity", "callId: " + callId); runOnUiThread(this::onCallConnected);
android.util.Log.d("CallActivity", "this.callId: " + this.callId);
runOnUiThread(() -> {
android.util.Log.d("CallActivity", "执行 onCallConnected UI更新");
onCallConnected();
});
} }
@Override @Override
@ -286,7 +551,7 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
message = "通话已结束"; message = "通话已结束";
} }
Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
finish(); releaseAndFinish();
}); });
} }
@ -297,18 +562,163 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
}); });
} }
// ==================== WebRTCListener 实现 ====================
@Override
public void onLocalStream(MediaStream stream) {
Log.d(TAG, "本地媒体流已创建");
}
@Override
public void onRemoteStream(MediaStream stream) {
Log.d(TAG, "收到远程媒体流");
runOnUiThread(() -> {
if (!isConnected) {
onCallConnected();
}
});
}
@Override
public void onIceCandidate(IceCandidate candidate) {
Log.d(TAG, "========== 发送 ICE Candidate ==========");
// 通过信令服务器发送 ICE Candidate
if (callManager != null && callId != null) {
try {
JSONObject candidateJson = new JSONObject();
candidateJson.put("sdpMid", candidate.sdpMid);
candidateJson.put("sdpMLineIndex", candidate.sdpMLineIndex);
candidateJson.put("candidate", candidate.sdp);
callManager.sendIceCandidate(callId, candidateJson);
Log.d(TAG, "ICE Candidate 已发送");
} catch (JSONException e) {
Log.e(TAG, "构建 ICE Candidate JSON 失败", e);
}
} else {
Log.e(TAG, "无法发送ICE: callManager=" + callManager + ", callId=" + callId);
}
}
@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState state) {
Log.d(TAG, "ICE 连接状态变化: " + state);
runOnUiThread(() -> {
switch (state) {
case CONNECTED:
case COMPLETED:
if (!isConnected) {
onCallConnected();
}
break;
case DISCONNECTED:
case FAILED:
Toast.makeText(this, "连接断开", Toast.LENGTH_SHORT).show();
releaseAndFinish();
break;
}
});
}
@Override
public void onOfferCreated(SessionDescription sdp) {
Log.d(TAG, "========== Offer 已创建 ==========");
Log.d(TAG, "callId=" + callId);
Log.d(TAG, "callManager=" + (callManager != null ? "存在" : "null"));
if (callManager != null && callId != null) {
callManager.sendOffer(callId, sdp.description);
Log.d(TAG, "Offer 已发送");
runOnUiThread(() -> Toast.makeText(this, "Offer已发送!", Toast.LENGTH_SHORT).show());
} else {
Log.e(TAG, "无法发送Offer: callManager=" + callManager + ", callId=" + callId);
runOnUiThread(() -> Toast.makeText(this, "发送Offer失败!", Toast.LENGTH_SHORT).show());
}
}
@Override
public void onAnswerCreated(SessionDescription sdp) {
Log.d(TAG, "Answer 已创建,发送给对方");
if (callManager != null && callId != null) {
callManager.sendAnswer(callId, sdp.description);
Log.d(TAG, "Answer 已发送");
}
}
// 处理收到的 Offer
public void handleRemoteOffer(String sdp) {
Log.d(TAG, "收到远程 Offer");
if (webRTCClient != null) {
SessionDescription offer = new SessionDescription(SessionDescription.Type.OFFER, sdp);
webRTCClient.setRemoteDescription(offer);
remoteDescriptionSet = true;
// 处理缓存的 ICE Candidates
for (IceCandidate candidate : pendingIceCandidates) {
webRTCClient.addIceCandidate(candidate);
}
pendingIceCandidates.clear();
}
}
// 处理收到的 Answer
public void handleRemoteAnswer(String sdp) {
Log.d(TAG, "收到远程 Answer");
if (webRTCClient != null) {
SessionDescription answer = new SessionDescription(SessionDescription.Type.ANSWER, sdp);
webRTCClient.setRemoteDescription(answer);
remoteDescriptionSet = true;
// 处理缓存的 ICE Candidates
for (IceCandidate candidate : pendingIceCandidates) {
webRTCClient.addIceCandidate(candidate);
}
pendingIceCandidates.clear();
}
}
// 处理收到的 ICE Candidate
public void handleRemoteIceCandidate(JSONObject candidateJson) {
try {
String sdpMid = candidateJson.getString("sdpMid");
int sdpMLineIndex = candidateJson.getInt("sdpMLineIndex");
String sdp = candidateJson.getString("candidate");
IceCandidate candidate = new IceCandidate(sdpMid, sdpMLineIndex, sdp);
if (remoteDescriptionSet && webRTCClient != null) {
webRTCClient.addIceCandidate(candidate);
} else {
// 缓存 ICE Candidate等远程 SDP 设置后再添加
pendingIceCandidates.add(candidate);
}
} catch (JSONException e) {
Log.e(TAG, "解析 ICE Candidate 失败", e);
}
}
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
handler.removeCallbacks(durationRunnable); handler.removeCallbacks(durationRunnable);
// 恢复音频模式
if (audioManager != null) {
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.setSpeakerphoneOn(false);
Log.d(TAG, "音频模式已恢复");
}
if (callManager != null) { if (callManager != null) {
callManager.setStateListener(null); callManager.setStateListener(null);
} }
if (webRTCClient != null) {
webRTCClient.release();
webRTCClient = null;
}
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
// 禁止返回键退出需要点击挂断
moveTaskToBack(true); moveTaskToBack(true);
} }
} }

View File

@ -351,18 +351,86 @@ public class CallManager implements CallSignalingClient.SignalingListener {
// 启动来电界面 // 启动来电界面
Log.d(TAG, "启动来电界面 IncomingCallActivity"); Log.d(TAG, "启动来电界面 IncomingCallActivity");
Intent intent = new Intent(context, IncomingCallActivity.class); Intent intent = new Intent(context, IncomingCallActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 添加多个 flags 确保能从后台启动
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_SINGLE_TOP
| Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
intent.putExtra("callId", callId); intent.putExtra("callId", callId);
intent.putExtra("callType", callType); intent.putExtra("callType", callType);
intent.putExtra("callerId", callerId); intent.putExtra("callerId", callerId);
intent.putExtra("callerName", callerName); intent.putExtra("callerName", callerName);
intent.putExtra("callerAvatar", callerAvatar); intent.putExtra("callerAvatar", callerAvatar);
context.startActivity(intent);
try {
context.startActivity(intent);
Log.d(TAG, "来电界面启动成功");
} catch (Exception e) {
Log.e(TAG, "启动来电界面失败: " + e.getMessage(), e);
// 如果直接启动失败尝试使用通知方式
showIncomingCallNotification(callId, callerId, callerName, callerAvatar, callType);
}
if (stateListener != null) { if (stateListener != null) {
stateListener.onIncomingCall(callId, callerId, callerName, callerAvatar, callType); stateListener.onIncomingCall(callId, callerId, callerName, callerAvatar, callType);
} }
} }
/**
* 显示来电通知当无法直接启动Activity时使用
*/
private void showIncomingCallNotification(String callId, int callerId, String callerName, String callerAvatar, String callType) {
try {
android.app.NotificationManager notificationManager =
(android.app.NotificationManager) context.getSystemService(android.content.Context.NOTIFICATION_SERVICE);
// 创建通知渠道 (Android 8.0+)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
android.app.NotificationChannel channel = new android.app.NotificationChannel(
"incoming_call",
"来电通知",
android.app.NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("显示来电通知");
channel.enableVibration(true);
channel.setLockscreenVisibility(android.app.Notification.VISIBILITY_PUBLIC);
notificationManager.createNotificationChannel(channel);
}
// 创建点击通知时启动的Intent
Intent intent = new Intent(context, IncomingCallActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra("callId", callId);
intent.putExtra("callType", callType);
intent.putExtra("callerId", callerId);
intent.putExtra("callerName", callerName);
intent.putExtra("callerAvatar", callerAvatar);
android.app.PendingIntent pendingIntent = android.app.PendingIntent.getActivity(
context, 0, intent,
android.app.PendingIntent.FLAG_UPDATE_CURRENT | android.app.PendingIntent.FLAG_IMMUTABLE
);
// 构建通知
String callTypeText = "video".equals(callType) ? "视频通话" : "语音通话";
androidx.core.app.NotificationCompat.Builder builder =
new androidx.core.app.NotificationCompat.Builder(context, "incoming_call")
.setSmallIcon(android.R.drawable.ic_menu_call)
.setContentTitle(callerName + "" + callTypeText)
.setContentText("点击接听")
.setPriority(androidx.core.app.NotificationCompat.PRIORITY_HIGH)
.setCategory(androidx.core.app.NotificationCompat.CATEGORY_CALL)
.setFullScreenIntent(pendingIntent, true)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setOngoing(true);
notificationManager.notify(1001, builder.build());
Log.d(TAG, "来电通知已显示");
} catch (Exception e) {
Log.e(TAG, "显示来电通知失败: " + e.getMessage(), e);
}
}
@Override @Override
public void onCallAccepted(String callId) { public void onCallAccepted(String callId) {
@ -417,17 +485,56 @@ public class CallManager implements CallSignalingClient.SignalingListener {
@Override @Override
public void onOffer(String callId, String sdp) { public void onOffer(String callId, String sdp) {
// WebRTC offer处理 - 后续实现 Log.d(TAG, "收到 Offer");
// 通知 CallActivity 处理 Offer
if (stateListener instanceof CallActivity) {
((CallActivity) stateListener).handleRemoteOffer(sdp);
}
} }
@Override @Override
public void onAnswer(String callId, String sdp) { public void onAnswer(String callId, String sdp) {
// WebRTC answer处理 - 后续实现 Log.d(TAG, "收到 Answer");
// 通知 CallActivity 处理 Answer
if (stateListener instanceof CallActivity) {
((CallActivity) stateListener).handleRemoteAnswer(sdp);
}
} }
@Override @Override
public void onIceCandidate(String callId, JSONObject candidate) { public void onIceCandidate(String callId, JSONObject candidate) {
// WebRTC ICE candidate处理 - 后续实现 Log.d(TAG, "收到 ICE Candidate");
// 通知 CallActivity 处理 ICE Candidate
if (stateListener instanceof CallActivity) {
((CallActivity) stateListener).handleRemoteIceCandidate(candidate);
}
}
/**
* 发送 Offer
*/
public void sendOffer(String callId, String sdp) {
if (signalingClient != null) {
signalingClient.sendOffer(callId, sdp);
}
}
/**
* 发送 Answer
*/
public void sendAnswer(String callId, String sdp) {
if (signalingClient != null) {
signalingClient.sendAnswer(callId, sdp);
}
}
/**
* 发送 ICE Candidate
*/
public void sendIceCandidate(String callId, JSONObject candidate) {
if (signalingClient != null) {
signalingClient.sendIceCandidate(callId, candidate);
}
} }
public interface CallCallback { public interface CallCallback {

View File

@ -26,6 +26,12 @@ public class CallSignalingClient {
private String baseUrl; private String baseUrl;
private int userId; private int userId;
private boolean isConnected = false; private boolean isConnected = false;
private boolean isManualDisconnect = false;
private int reconnectAttempts = 0;
private static final int MAX_RECONNECT_ATTEMPTS = 5;
private static final long RECONNECT_DELAY_MS = 3000;
private static final long HEARTBEAT_INTERVAL_MS = 30000; // 30秒心跳
private Runnable heartbeatRunnable;
public interface SignalingListener { public interface SignalingListener {
void onConnected(); void onConnected();
@ -54,19 +60,26 @@ public class CallSignalingClient {
} }
public void connect() { public void connect() {
isManualDisconnect = false;
doConnect();
}
private void doConnect() {
String wsUrl = baseUrl.replace("http://", "ws://").replace("https://", "wss://"); String wsUrl = baseUrl.replace("http://", "ws://").replace("https://", "wss://");
if (!wsUrl.endsWith("/")) wsUrl += "/"; if (!wsUrl.endsWith("/")) wsUrl += "/";
wsUrl += "ws/call"; wsUrl += "ws/call";
Log.d(TAG, "Connecting to: " + wsUrl); Log.d(TAG, "Connecting to: " + wsUrl + " (attempt " + (reconnectAttempts + 1) + ")");
Request request = new Request.Builder().url(wsUrl).build(); Request request = new Request.Builder().url(wsUrl).build();
webSocket = client.newWebSocket(request, new WebSocketListener() { webSocket = client.newWebSocket(request, new WebSocketListener() {
@Override @Override
public void onOpen(WebSocket webSocket, Response response) { public void onOpen(WebSocket webSocket, Response response) {
Log.d(TAG, "WebSocket connected"); Log.d(TAG, "WebSocket connected successfully!");
isConnected = true; isConnected = true;
reconnectAttempts = 0; // 重置重连计数
register(); register();
startHeartbeat();
notifyConnected(); notifyConnected();
} }
@ -86,24 +99,83 @@ public class CallSignalingClient {
Log.d(TAG, "WebSocket closed: " + reason); Log.d(TAG, "WebSocket closed: " + reason);
isConnected = false; isConnected = false;
notifyDisconnected(); notifyDisconnected();
// 非手动断开时尝试重连
if (!isManualDisconnect) {
scheduleReconnect();
}
} }
@Override @Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) { public void onFailure(WebSocket webSocket, Throwable t, Response response) {
Log.e(TAG, "WebSocket error", t); Log.e(TAG, "WebSocket connection failed: " + t.getMessage(), t);
isConnected = false; isConnected = false;
notifyError(t.getMessage()); notifyError("连接失败: " + t.getMessage());
// 尝试重连
if (!isManualDisconnect) {
scheduleReconnect();
}
} }
}); });
} }
private void scheduleReconnect() {
if (isManualDisconnect) {
Log.d(TAG, "手动断开,不重连");
return;
}
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
Log.e(TAG, "达到最大重连次数(" + MAX_RECONNECT_ATTEMPTS + "),停止重连");
notifyError("WebSocket连接失败已达最大重试次数");
return;
}
reconnectAttempts++;
long delay = RECONNECT_DELAY_MS * reconnectAttempts;
Log.d(TAG, "将在 " + delay + "ms 后重连 (第" + reconnectAttempts + "次)");
mainHandler.postDelayed(() -> {
if (!isConnected && !isManualDisconnect) {
doConnect();
}
}, delay);
}
public void disconnect() { public void disconnect() {
isManualDisconnect = true;
reconnectAttempts = 0;
stopHeartbeat();
if (webSocket != null) { if (webSocket != null) {
webSocket.close(1000, "User disconnect"); webSocket.close(1000, "User disconnect");
webSocket = null; webSocket = null;
} }
isConnected = false; isConnected = false;
} }
private void startHeartbeat() {
stopHeartbeat();
heartbeatRunnable = new Runnable() {
@Override
public void run() {
if (isConnected && webSocket != null) {
try {
JSONObject ping = new JSONObject();
ping.put("type", "ping");
webSocket.send(ping.toString());
Log.d(TAG, "发送心跳 ping");
} catch (JSONException e) {
Log.e(TAG, "心跳发送失败", e);
}
mainHandler.postDelayed(this, HEARTBEAT_INTERVAL_MS);
}
}
};
mainHandler.postDelayed(heartbeatRunnable, HEARTBEAT_INTERVAL_MS);
}
private void stopHeartbeat() {
if (heartbeatRunnable != null) {
mainHandler.removeCallbacks(heartbeatRunnable);
heartbeatRunnable = null;
}
}
public boolean isConnected() { public boolean isConnected() {
return isConnected; return isConnected;

View File

@ -0,0 +1,729 @@
package com.example.livestreaming.call;
import android.content.Context;
import android.util.Log;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerator;
import org.webrtc.DataChannel;
import org.webrtc.DefaultVideoDecoderFactory;
import org.webrtc.DefaultVideoEncoderFactory;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.RtpReceiver;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import java.util.ArrayList;
import java.util.List;
/**
* WebRTC 客户端 - 处理音视频传输
*/
public class WebRTCClient {
private static final String TAG = "WebRTCClient";
private Context context;
private EglBase eglBase;
private PeerConnectionFactory peerConnectionFactory;
private PeerConnection peerConnection;
private AudioSource audioSource;
private AudioTrack localAudioTrack;
private VideoSource videoSource;
private VideoTrack localVideoTrack;
private VideoCapturer videoCapturer;
private SurfaceTextureHelper surfaceTextureHelper;
private SurfaceViewRenderer localRenderer;
private SurfaceViewRenderer remoteRenderer;
private WebRTCListener listener;
private boolean isVideoCall = false;
private boolean isMuted = false;
private boolean isVideoEnabled = true;
private boolean useFrontCamera = true;
// ICE 服务器配置 - 可通过 WebRTCConfig 修改
private static List<PeerConnection.IceServer> getIceServers() {
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
// STUN 服务器用于获取公网 IP
for (String stunServer : WebRTCConfig.STUN_SERVERS) {
iceServers.add(PeerConnection.IceServer.builder(stunServer).createIceServer());
}
// TURN 服务器用于 NAT 穿透失败时的中继
if (WebRTCConfig.TURN_SERVER_URL != null && !WebRTCConfig.TURN_SERVER_URL.isEmpty()) {
// UDP
iceServers.add(PeerConnection.IceServer.builder(WebRTCConfig.TURN_SERVER_URL)
.setUsername(WebRTCConfig.TURN_USERNAME)
.setPassword(WebRTCConfig.TURN_PASSWORD)
.createIceServer());
// TCP备用
iceServers.add(PeerConnection.IceServer.builder(WebRTCConfig.TURN_SERVER_URL + "?transport=tcp")
.setUsername(WebRTCConfig.TURN_USERNAME)
.setPassword(WebRTCConfig.TURN_PASSWORD)
.createIceServer());
}
return iceServers;
}
public interface WebRTCListener {
void onLocalStream(MediaStream stream);
void onRemoteStream(MediaStream stream);
void onIceCandidate(IceCandidate candidate);
void onIceConnectionChange(PeerConnection.IceConnectionState state);
void onOfferCreated(SessionDescription sdp);
void onAnswerCreated(SessionDescription sdp);
void onError(String error);
}
public WebRTCClient(Context context) {
this.context = context.getApplicationContext();
}
public void setListener(WebRTCListener listener) {
this.listener = listener;
}
/**
* 初始化 WebRTC
*/
public void initialize(boolean isVideoCall) {
this.isVideoCall = isVideoCall;
Log.d(TAG, "初始化 WebRTC, isVideoCall=" + isVideoCall);
// 在后台线程初始化避免阻塞UI
new Thread(() -> {
try {
// 初始化 EglBase
Log.d(TAG, "创建 EglBase...");
eglBase = EglBase.create();
Log.d(TAG, "EglBase 创建成功");
// 初始化 PeerConnectionFactory
Log.d(TAG, "初始化 PeerConnectionFactory...");
PeerConnectionFactory.InitializationOptions initOptions = PeerConnectionFactory.InitializationOptions.builder(context)
.setEnableInternalTracer(false)
.createInitializationOptions();
PeerConnectionFactory.initialize(initOptions);
Log.d(TAG, "PeerConnectionFactory 初始化成功");
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
Log.d(TAG, "创建视频编解码器...");
DefaultVideoEncoderFactory encoderFactory = new DefaultVideoEncoderFactory(
eglBase.getEglBaseContext(), true, true);
DefaultVideoDecoderFactory decoderFactory = new DefaultVideoDecoderFactory(eglBase.getEglBaseContext());
Log.d(TAG, "视频编解码器创建成功");
Log.d(TAG, "创建 PeerConnectionFactory...");
peerConnectionFactory = PeerConnectionFactory.builder()
.setOptions(options)
.setVideoEncoderFactory(encoderFactory)
.setVideoDecoderFactory(decoderFactory)
.createPeerConnectionFactory();
Log.d(TAG, "PeerConnectionFactory 创建成功");
} catch (Exception e) {
Log.e(TAG, "WebRTC 初始化异常", e);
if (listener != null) {
listener.onError("WebRTC初始化失败: " + e.getMessage());
}
}
}).start();
// 等待初始化完成最多5秒
int waitCount = 0;
while (peerConnectionFactory == null && waitCount < 50) {
try {
Thread.sleep(100);
waitCount++;
} catch (InterruptedException e) {
break;
}
}
if (peerConnectionFactory == null) {
Log.e(TAG, "WebRTC 初始化超时");
if (listener != null) {
listener.onError("WebRTC初始化超时");
}
} else {
Log.d(TAG, "WebRTC 初始化完成,耗时: " + (waitCount * 100) + "ms");
}
}
/**
* 设置本地视频渲染器
*/
public void setLocalRenderer(SurfaceViewRenderer renderer) {
this.localRenderer = renderer;
if (localRenderer != null) {
localRenderer.init(eglBase.getEglBaseContext(), null);
localRenderer.setMirror(true);
localRenderer.setEnableHardwareScaler(true);
}
}
/**
* 设置远程视频渲染器
*/
public void setRemoteRenderer(SurfaceViewRenderer renderer) {
this.remoteRenderer = renderer;
if (remoteRenderer != null) {
remoteRenderer.init(eglBase.getEglBaseContext(), null);
remoteRenderer.setMirror(false);
remoteRenderer.setEnableHardwareScaler(true);
}
}
/**
* 创建本地媒体流
*/
public void createLocalStream() {
Log.d(TAG, "========== 创建本地媒体流 ==========");
Log.d(TAG, "isVideoCall=" + isVideoCall);
try {
// 创建音频轨道
MediaConstraints audioConstraints = new MediaConstraints();
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));
if (peerConnectionFactory != null) {
audioSource = peerConnectionFactory.createAudioSource(audioConstraints);
if (audioSource != null) {
localAudioTrack = peerConnectionFactory.createAudioTrack("ARDAMSa0", audioSource);
if (localAudioTrack != null) {
localAudioTrack.setEnabled(true);
Log.d(TAG, "音频轨道创建成功");
} else {
Log.w(TAG, "音频轨道创建返回null继续执行");
}
} else {
Log.w(TAG, "音频源创建返回null继续执行");
}
} else {
Log.e(TAG, "peerConnectionFactory为null!");
if (listener != null) {
listener.onError("PeerConnectionFactory未初始化");
}
return;
}
} catch (Exception e) {
Log.e(TAG, "音频轨道创建失败,继续执行", e);
// 不返回继续尝试创建视频轨道
}
// 如果是视频通话创建视频轨道
if (isVideoCall) {
Log.d(TAG, "视频通话,开始创建视频轨道");
try {
createVideoTrack();
Log.d(TAG, "视频轨道创建结果: localVideoTrack=" + (localVideoTrack != null));
} catch (Exception e) {
Log.e(TAG, "视频轨道创建失败", e);
if (listener != null) {
listener.onError("视频轨道创建失败: " + e.getMessage());
}
}
} else {
Log.d(TAG, "语音通话,跳过视频轨道创建");
}
Log.d(TAG, "本地媒体流创建完成: audioTrack=" + (localAudioTrack != null) + ", videoTrack=" + (localVideoTrack != null));
}
/**
* 创建视频轨道
*/
private void createVideoTrack() {
Log.d(TAG, "创建视频轨道");
videoCapturer = createCameraCapturer();
if (videoCapturer == null) {
Log.e(TAG, "无法创建摄像头捕获器");
if (listener != null) {
listener.onError("无法访问摄像头");
}
return;
}
surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.getEglBaseContext());
videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
// 启动摄像头捕获 (720p, 30fps)
videoCapturer.startCapture(1280, 720, 30);
localVideoTrack = peerConnectionFactory.createVideoTrack("ARDAMSv0", videoSource);
localVideoTrack.setEnabled(true);
// 添加到本地渲染器
if (localRenderer != null) {
localVideoTrack.addSink(localRenderer);
}
Log.d(TAG, "视频轨道创建成功");
}
/**
* 创建摄像头捕获器
*/
private VideoCapturer createCameraCapturer() {
CameraEnumerator enumerator;
if (Camera2Enumerator.isSupported(context)) {
enumerator = new Camera2Enumerator(context);
} else {
enumerator = new Camera1Enumerator(true);
}
String[] deviceNames = enumerator.getDeviceNames();
// 优先使用前置摄像头
for (String deviceName : deviceNames) {
if (useFrontCamera && enumerator.isFrontFacing(deviceName)) {
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer != null) {
Log.d(TAG, "使用前置摄像头: " + deviceName);
return capturer;
}
}
}
// 如果没有前置摄像头使用后置
for (String deviceName : deviceNames) {
if (!enumerator.isFrontFacing(deviceName)) {
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer != null) {
Log.d(TAG, "使用后置摄像头: " + deviceName);
return capturer;
}
}
}
return null;
}
/**
* 创建 PeerConnection
*/
public void createPeerConnection() {
Log.d(TAG, "创建 PeerConnection");
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(getIceServers());
config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
config.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
peerConnection = peerConnectionFactory.createPeerConnection(config, new PeerConnection.Observer() {
@Override
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
Log.d(TAG, "onSignalingChange: " + signalingState);
}
@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
Log.d(TAG, "========== ICE 连接状态变化 ==========");
Log.d(TAG, "ICE状态: " + iceConnectionState);
// 打印详细的连接信息
if (peerConnection != null) {
Log.d(TAG, "信令状态: " + peerConnection.signalingState());
Log.d(TAG, "连接状态: " + peerConnection.connectionState());
}
if (listener != null) {
listener.onIceConnectionChange(iceConnectionState);
}
}
@Override
public void onIceConnectionReceivingChange(boolean b) {
Log.d(TAG, "onIceConnectionReceivingChange: " + b);
}
@Override
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
Log.d(TAG, "onIceGatheringChange: " + iceGatheringState);
}
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
Log.d(TAG, "========== 生成 ICE Candidate ==========");
Log.d(TAG, "sdpMid: " + iceCandidate.sdpMid);
Log.d(TAG, "sdpMLineIndex: " + iceCandidate.sdpMLineIndex);
Log.d(TAG, "candidate: " + iceCandidate.sdp);
if (listener != null) {
listener.onIceCandidate(iceCandidate);
}
}
@Override
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
Log.d(TAG, "onIceCandidatesRemoved");
}
@Override
public void onAddStream(MediaStream mediaStream) {
Log.d(TAG, "========== 收到远程媒体流 ==========");
Log.d(TAG, "streamId: " + mediaStream.getId());
Log.d(TAG, "音频轨道数: " + mediaStream.audioTracks.size());
Log.d(TAG, "视频轨道数: " + mediaStream.videoTracks.size());
if (listener != null) {
listener.onRemoteStream(mediaStream);
}
// 添加远程音频轨道
if (mediaStream.audioTracks.size() > 0) {
Log.d(TAG, "启用远程音频轨道");
mediaStream.audioTracks.get(0).setEnabled(true);
}
// 添加远程视频到渲染器
if (mediaStream.videoTracks.size() > 0 && remoteRenderer != null) {
Log.d(TAG, "添加远程视频到渲染器");
mediaStream.videoTracks.get(0).addSink(remoteRenderer);
}
}
@Override
public void onRemoveStream(MediaStream mediaStream) {
Log.d(TAG, "onRemoveStream: " + mediaStream.getId());
}
@Override
public void onDataChannel(DataChannel dataChannel) {
Log.d(TAG, "onDataChannel");
}
@Override
public void onRenegotiationNeeded() {
Log.d(TAG, "onRenegotiationNeeded");
}
@Override
public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
Log.d(TAG, "========== onAddTrack ==========");
if (rtpReceiver.track() != null) {
Log.d(TAG, "轨道类型: " + rtpReceiver.track().kind());
Log.d(TAG, "轨道ID: " + rtpReceiver.track().id());
// 处理远程视频轨道
if (rtpReceiver.track().kind().equals("video")) {
VideoTrack remoteVideoTrack = (VideoTrack) rtpReceiver.track();
Log.d(TAG, "收到远程视频轨道,添加到渲染器");
if (remoteRenderer != null) {
remoteVideoTrack.addSink(remoteRenderer);
Log.d(TAG, "远程视频已添加到渲染器");
} else {
Log.e(TAG, "remoteRenderer 为空,无法显示远程视频");
}
}
}
}
});
// 添加本地轨道到 PeerConnection
Log.d(TAG, "========== 添加本地轨道 ==========");
Log.d(TAG, "localAudioTrack=" + (localAudioTrack != null) + ", localVideoTrack=" + (localVideoTrack != null));
if (localAudioTrack != null) {
peerConnection.addTrack(localAudioTrack);
Log.d(TAG, "已添加本地音频轨道");
} else {
Log.w(TAG, "本地音频轨道为空,无法添加");
}
if (localVideoTrack != null) {
peerConnection.addTrack(localVideoTrack);
Log.d(TAG, "已添加本地视频轨道");
} else {
Log.w(TAG, "本地视频轨道为空,无法添加(如果是视频通话则有问题)");
}
Log.d(TAG, "PeerConnection 创建成功, isVideoCall=" + isVideoCall);
}
/**
* 创建 Offer (主叫方调用)
*/
public void createOffer() {
Log.d(TAG, "创建 Offer");
MediaConstraints constraints = new MediaConstraints();
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", isVideoCall ? "true" : "false"));
peerConnection.createOffer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(TAG, "Offer 创建成功");
peerConnection.setLocalDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {}
@Override
public void onSetSuccess() {
Log.d(TAG, "本地 SDP 设置成功");
if (listener != null) {
listener.onOfferCreated(sessionDescription);
}
}
@Override
public void onCreateFailure(String s) {}
@Override
public void onSetFailure(String s) {
Log.e(TAG, "设置本地 SDP 失败: " + s);
}
}, sessionDescription);
}
@Override
public void onSetSuccess() {}
@Override
public void onCreateFailure(String s) {
Log.e(TAG, "创建 Offer 失败: " + s);
if (listener != null) {
listener.onError("创建 Offer 失败: " + s);
}
}
@Override
public void onSetFailure(String s) {}
}, constraints);
}
/**
* 创建 Answer (被叫方调用)
*/
public void createAnswer() {
Log.d(TAG, "创建 Answer");
MediaConstraints constraints = new MediaConstraints();
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", isVideoCall ? "true" : "false"));
peerConnection.createAnswer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.d(TAG, "Answer 创建成功");
peerConnection.setLocalDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {}
@Override
public void onSetSuccess() {
Log.d(TAG, "本地 SDP 设置成功");
if (listener != null) {
listener.onAnswerCreated(sessionDescription);
}
}
@Override
public void onCreateFailure(String s) {}
@Override
public void onSetFailure(String s) {
Log.e(TAG, "设置本地 SDP 失败: " + s);
}
}, sessionDescription);
}
@Override
public void onSetSuccess() {}
@Override
public void onCreateFailure(String s) {
Log.e(TAG, "创建 Answer 失败: " + s);
if (listener != null) {
listener.onError("创建 Answer 失败: " + s);
}
}
@Override
public void onSetFailure(String s) {}
}, constraints);
}
/**
* 设置远程 SDP
*/
public void setRemoteDescription(SessionDescription sdp) {
Log.d(TAG, "设置远程 SDP, type=" + sdp.type);
peerConnection.setRemoteDescription(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {}
@Override
public void onSetSuccess() {
Log.d(TAG, "远程 SDP 设置成功");
// 如果是 Offer需要创建 Answer
if (sdp.type == SessionDescription.Type.OFFER) {
createAnswer();
}
}
@Override
public void onCreateFailure(String s) {}
@Override
public void onSetFailure(String s) {
Log.e(TAG, "设置远程 SDP 失败: " + s);
if (listener != null) {
listener.onError("设置远程 SDP 失败: " + s);
}
}
}, sdp);
}
/**
* 添加 ICE Candidate
*/
public void addIceCandidate(IceCandidate candidate) {
Log.d(TAG, "========== 添加远程 ICE Candidate ==========");
Log.d(TAG, "sdpMid: " + candidate.sdpMid);
Log.d(TAG, "sdpMLineIndex: " + candidate.sdpMLineIndex);
Log.d(TAG, "candidate: " + candidate.sdp);
if (peerConnection != null) {
boolean result = peerConnection.addIceCandidate(candidate);
Log.d(TAG, "添加结果: " + result);
} else {
Log.e(TAG, "peerConnection 为 null无法添加 ICE Candidate");
}
}
/**
* 切换静音
*/
public void setMuted(boolean muted) {
this.isMuted = muted;
if (localAudioTrack != null) {
localAudioTrack.setEnabled(!muted);
}
Log.d(TAG, "静音状态: " + muted);
}
/**
* 切换视频
*/
public void setVideoEnabled(boolean enabled) {
this.isVideoEnabled = enabled;
if (localVideoTrack != null) {
localVideoTrack.setEnabled(enabled);
}
Log.d(TAG, "视频状态: " + enabled);
}
/**
* 切换摄像头
*/
public void switchCamera() {
if (videoCapturer instanceof org.webrtc.CameraVideoCapturer) {
((org.webrtc.CameraVideoCapturer) videoCapturer).switchCamera(null);
useFrontCamera = !useFrontCamera;
if (localRenderer != null) {
localRenderer.setMirror(useFrontCamera);
}
Log.d(TAG, "切换摄像头, 使用前置: " + useFrontCamera);
}
}
/**
* 释放资源
*/
public void release() {
Log.d(TAG, "释放 WebRTC 资源");
if (videoCapturer != null) {
try {
videoCapturer.stopCapture();
} catch (InterruptedException e) {
Log.e(TAG, "停止摄像头捕获失败", e);
}
videoCapturer.dispose();
videoCapturer = null;
}
if (surfaceTextureHelper != null) {
surfaceTextureHelper.dispose();
surfaceTextureHelper = null;
}
if (localVideoTrack != null) {
localVideoTrack.dispose();
localVideoTrack = null;
}
if (videoSource != null) {
videoSource.dispose();
videoSource = null;
}
if (localAudioTrack != null) {
localAudioTrack.dispose();
localAudioTrack = null;
}
if (audioSource != null) {
audioSource.dispose();
audioSource = null;
}
if (peerConnection != null) {
peerConnection.close();
peerConnection = null;
}
if (peerConnectionFactory != null) {
peerConnectionFactory.dispose();
peerConnectionFactory = null;
}
if (localRenderer != null) {
localRenderer.release();
}
if (remoteRenderer != null) {
remoteRenderer.release();
}
if (eglBase != null) {
eglBase.release();
eglBase = null;
}
Log.d(TAG, "WebRTC 资源释放完成");
}
public boolean isMuted() {
return isMuted;
}
public boolean isVideoEnabled() {
return isVideoEnabled;
}
public EglBase getEglBase() {
return eglBase;
}
}

View File

@ -0,0 +1,52 @@
package com.example.livestreaming.call;
/**
* WebRTC 配置
* 部署时修改这里的服务器地址
*/
public class WebRTCConfig {
// ============ STUN 服务器 ============
// STUN 用于获取设备的公网IP帮助建立P2P连接
public static final String[] STUN_SERVERS = {
"stun:stun.l.google.com:19302", // Google STUN国内可能不稳定
"stun:stun.qq.com:3478", // 腾讯 STUN国内推荐
"stun:stun.miwifi.com:3478" // 小米 STUN国内推荐
};
// ============ TURN 服务器 ============
// TURN 用于在P2P连接失败时进行中继转发
// 你的服务器TURN地址
public static final String TURN_SERVER_URL = "turn:1.15.149.240:3478";
// TURN 服务器用户名
public static final String TURN_USERNAME = "turnuser";
// TURN 服务器密码
public static final String TURN_PASSWORD = "TurnPass123456";
// ============ 使用说明 ============
/*
* 局域网测试
* - 不需要修改当前配置即可使用
*
* 部署到公网服务器
* 1. 在服务器安装 coturn (TURN服务器)
* 2. 修改上面的配置
* TURN_SERVER_URL = "turn:你的服务器IP:3478"
* TURN_USERNAME = "你设置的用户名"
* TURN_PASSWORD = "你设置的密码"
*
* 宝塔安装 coturn 步骤
* 1. SSH执行: yum install -y coturn (CentOS) apt install -y coturn (Ubuntu)
* 2. 编辑 /etc/turnserver.conf:
* listening-port=3478
* external-ip=你的公网IP
* realm=你的公网IP
* lt-cred-mech
* user=用户名:密码
* 3. 宝塔放行端口: 3478(TCP/UDP), 49152-65535(UDP)
* 4. 启动: systemctl start coturn
*/
}

View File

@ -5,7 +5,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#1A1A2E"> android:background="#1A1A2E">
<!-- 背景模糊头像 --> <!-- 背景模糊头像(语音通话时显示) -->
<ImageView <ImageView
android:id="@+id/ivBackgroundAvatar" android:id="@+id/ivBackgroundAvatar"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -13,21 +13,22 @@
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:alpha="0.3" /> android:alpha="0.3" />
<!-- 视频通话时的远程视频 --> <!-- 视频通话时的远程视频容器 -->
<SurfaceView <FrameLayout
android:id="@+id/remoteVideoView" android:id="@+id/layoutRemoteVideo"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" /> android:visibility="gone" />
<!-- 视频通话时的本地视频(小窗口) --> <!-- 视频通话时的本地视频容器(小窗口) -->
<SurfaceView <FrameLayout
android:id="@+id/localVideoView" android:id="@+id/layoutLocalVideo"
android:layout_width="120dp" android:layout_width="120dp"
android:layout_height="160dp" android:layout_height="160dp"
android:layout_gravity="top|end" android:layout_gravity="top|end"
android:layout_marginTop="80dp" android:layout_marginTop="80dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:background="#333333"
android:visibility="gone" /> android:visibility="gone" />
<!-- 主内容区域 --> <!-- 主内容区域 -->

View File

@ -1,6 +1,8 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true android.useAndroidX=true
# 使用 Android Studio 自带的 JDK 17
org.gradle.java.home=C:/Program Files/Android/Android Studio/jbr
systemProp.gradle.wrapperUser=myuser systemProp.gradle.wrapperUser=myuser
systemProp.gradle.wrapperPassword=mypassword systemProp.gradle.wrapperPassword=mypassword