功能:增加语音和视频通话的功能
This commit is contained in:
parent
b97c82899f
commit
c37cf4884b
|
|
@ -229,4 +229,3 @@ public class CallController {
|
|||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -259,10 +259,7 @@ public class LiveRoomController {
|
|||
}
|
||||
}
|
||||
|
||||
// ========== 关注主播接口 ==========
|
||||
|
||||
@Autowired
|
||||
private com.zbkj.service.service.FollowRecordService followRecordService;
|
||||
// ========== 直播控制接口 ==========
|
||||
|
||||
@ApiOperation(value = "开始直播")
|
||||
@PostMapping("/room/{id}/start")
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
System.out.println("[CallSignaling] ========== 连接建立 ==========");
|
||||
System.out.println("[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() : "";
|
||||
|
||||
switch (type) {
|
||||
case "ping":
|
||||
handlePing(session);
|
||||
break;
|
||||
case "register":
|
||||
handleRegister(session, json);
|
||||
break;
|
||||
|
|
@ -97,6 +102,9 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
|
||||
private void handleRegister(WebSocketSession session, JsonNode json) throws IOException {
|
||||
Integer userId = json.has("userId") ? json.get("userId").asInt() : null;
|
||||
System.out.println("[CallSignaling] ========== 用户注册 ==========");
|
||||
System.out.println("[CallSignaling] userId=" + userId);
|
||||
|
||||
if (userId == null) {
|
||||
sendError(session, "userId不能为空");
|
||||
return;
|
||||
|
|
@ -105,6 +113,7 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
// 关闭旧连接
|
||||
WebSocketSession oldSession = userCallSessions.get(userId);
|
||||
if (oldSession != null && oldSession.isOpen() && !oldSession.getId().equals(session.getId())) {
|
||||
System.out.println("[CallSignaling] 关闭旧连接: userId=" + userId);
|
||||
logger.info("[CallSignaling] 关闭旧连接: userId={}, oldSessionId={}", userId, oldSession.getId());
|
||||
try {
|
||||
oldSession.close();
|
||||
|
|
@ -118,6 +127,8 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
response.put("type", "registered");
|
||||
response.put("userId", userId);
|
||||
session.sendMessage(new TextMessage(response.toString()));
|
||||
|
||||
System.out.println("[CallSignaling] 用户注册成功: userId=" + userId + ", 当前在线用户=" + userCallSessions.keySet());
|
||||
logger.info("[CallSignaling] 用户注册成功: userId={}, sessionId={}, 当前在线用户数={}",
|
||||
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 {
|
||||
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) {
|
||||
sendError(session, "callId不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保通话会话存在
|
||||
Set<WebSocketSession> sessions = callSessions.get(callId);
|
||||
if (sessions == null) {
|
||||
sendError(session, "通话不存在");
|
||||
return;
|
||||
// 尝试创建会话(可能是REST API发起的通话)
|
||||
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();
|
||||
int forwardCount = 0;
|
||||
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));
|
||||
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
|
||||
|
|
@ -461,11 +504,49 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
return "calling".equals(status) || "ringing".equals(status) || "connected".equals(status);
|
||||
}
|
||||
|
||||
private void sendError(WebSocketSession session, String message) throws IOException {
|
||||
ObjectNode error = objectMapper.createObjectNode();
|
||||
error.put("type", "error");
|
||||
error.put("message", message);
|
||||
session.sendMessage(new TextMessage(error.toString()));
|
||||
/**
|
||||
* 处理心跳 ping 消息
|
||||
*/
|
||||
private void handlePing(WebSocketSession session) {
|
||||
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,10 +554,28 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
*/
|
||||
public void notifyIncomingCall(String callId, Integer callerId, String callerName, String callerAvatar,
|
||||
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 {
|
||||
// 记录通话创建时间
|
||||
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);
|
||||
if (calleeSession != null && calleeSession.isOpen()) {
|
||||
|
|
@ -488,11 +587,14 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
incoming.put("callerAvatar", callerAvatar != null ? callerAvatar : "");
|
||||
incoming.put("callType", callType);
|
||||
calleeSession.sendMessage(new TextMessage(incoming.toString()));
|
||||
System.out.println("[CallSignaling] 已发送来电通知给被叫方: calleeId=" + calleeId);
|
||||
logger.info("[CallSignaling] 通知来电: callId={}, callee={}", callId, calleeId);
|
||||
} else {
|
||||
logger.warn("[CallSignaling] 被叫方未在线: calleeId={}", calleeId);
|
||||
System.out.println("[CallSignaling] !!!!! 被叫方未在线 !!!!! calleeId=" + calleeId);
|
||||
logger.warn("[CallSignaling] 被叫方未在线: calleeId={}, 当前在线用户={}", calleeId, userCallSessions.keySet());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("[CallSignaling] 通知来电异常: " + e.getMessage());
|
||||
logger.error("[CallSignaling] 通知来电异常: callId={}", callId, e);
|
||||
}
|
||||
}
|
||||
|
|
@ -512,8 +614,15 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
logger.info("[CallSignaling] REST API调用notifyCallAccepted: callId={}, callerId={}, 当前在线用户={}",
|
||||
callId, callerId, userCallSessions.keySet());
|
||||
try {
|
||||
// 确保通话会话存在
|
||||
callSessions.computeIfAbsent(callId, k -> new CopyOnWriteArraySet<>());
|
||||
|
||||
WebSocketSession callerSession = userCallSessions.get(callerId);
|
||||
if (callerSession != null && callerSession.isOpen()) {
|
||||
// 确保主叫方在通话会话中
|
||||
joinCallSession(callId, callerSession);
|
||||
sessionCallMap.put(callerSession.getId(), callId);
|
||||
|
||||
ObjectNode notify = objectMapper.createObjectNode();
|
||||
notify.put("type", "call_accepted");
|
||||
notify.put("callId", callId);
|
||||
|
|
|
|||
|
|
@ -84,7 +84,8 @@ debug: true
|
|||
logging:
|
||||
level:
|
||||
io.swagger.*: error
|
||||
com.zbjk.crmeb: debug
|
||||
com.zbkj: debug
|
||||
com.zbkj.front.websocket: info
|
||||
org.springframework.boot.autoconfigure: ERROR
|
||||
config: classpath:logback-spring.xml
|
||||
file:
|
||||
|
|
|
|||
|
|
@ -113,4 +113,8 @@ dependencies {
|
|||
implementation("com.github.andnux:ijkplayer:0.0.1") {
|
||||
exclude("com.google.android.exoplayer", "exoplayer")
|
||||
}
|
||||
|
||||
// WebRTC for voice/video calls
|
||||
// 使用 Google 官方 WebRTC 库
|
||||
implementation("io.getstream:stream-webrtc-android:1.1.1")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@
|
|||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<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
|
||||
android:name=".LiveStreamingApplication"
|
||||
|
|
|
|||
|
|
@ -832,6 +832,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
}
|
||||
|
||||
// 确保通话信令 WebSocket 保持连接(用于接收来电通知)
|
||||
LiveStreamingApplication app = (LiveStreamingApplication) getApplication();
|
||||
app.connectCallSignalingIfLoggedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,27 +1,51 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.R;
|
||||
|
||||
/**
|
||||
* 通话界面
|
||||
*/
|
||||
public class CallActivity extends AppCompatActivity implements CallManager.CallStateListener {
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.webrtc.IceCandidate;
|
||||
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 ivAvatar;
|
||||
private TextView tvUserName;
|
||||
|
|
@ -36,11 +60,20 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
private ImageButton btnSwitchCamera;
|
||||
private LinearLayout layoutCallControls;
|
||||
private LinearLayout layoutVideoToggle;
|
||||
private FrameLayout layoutLocalVideo;
|
||||
private FrameLayout layoutRemoteVideo;
|
||||
|
||||
// WebRTC 视频渲染器
|
||||
private SurfaceViewRenderer localRenderer;
|
||||
private SurfaceViewRenderer remoteRenderer;
|
||||
|
||||
// 管理器
|
||||
private CallManager callManager;
|
||||
private WebRTCClient webRTCClient;
|
||||
private AudioManager audioManager;
|
||||
private Handler handler;
|
||||
|
||||
// 通话信息
|
||||
private String callId;
|
||||
private String callType;
|
||||
private boolean isCaller;
|
||||
|
|
@ -48,12 +81,18 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
private String otherUserName;
|
||||
private String otherUserAvatar;
|
||||
|
||||
// 状态
|
||||
private boolean isMuted = false;
|
||||
private boolean isSpeakerOn = false;
|
||||
private boolean isVideoEnabled = true;
|
||||
private boolean isConnected = false;
|
||||
private boolean isWebRTCInitialized = false;
|
||||
private long callStartTime = 0;
|
||||
|
||||
// ICE Candidate 缓存(在远程 SDP 设置前收到的)
|
||||
private List<IceCandidate> pendingIceCandidates = new ArrayList<>();
|
||||
private boolean remoteDescriptionSet = false;
|
||||
|
||||
private Runnable durationRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
|
@ -70,6 +109,8 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Log.d(TAG, "========== CallActivity onCreate ==========");
|
||||
Toast.makeText(this, "通话界面已打开", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 保持屏幕常亮,显示在锁屏上方
|
||||
getWindow().addFlags(
|
||||
|
|
@ -82,9 +123,20 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
|
||||
initViews();
|
||||
initData();
|
||||
initCallManager();
|
||||
setupListeners();
|
||||
updateUI();
|
||||
|
||||
Log.d(TAG, "callId=" + callId + ", callType=" + callType + ", isCaller=" + isCaller);
|
||||
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() {
|
||||
|
|
@ -102,9 +154,62 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
btnSwitchCamera = findViewById(R.id.btnSwitchCamera);
|
||||
layoutCallControls = findViewById(R.id.layoutCallControls);
|
||||
layoutVideoToggle = findViewById(R.id.layoutVideoToggle);
|
||||
layoutLocalVideo = findViewById(R.id.layoutLocalVideo);
|
||||
layoutRemoteVideo = findViewById(R.id.layoutRemoteVideo);
|
||||
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
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() {
|
||||
|
|
@ -118,36 +223,175 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
// 如果是被叫方接听,直接进入通话状态
|
||||
boolean alreadyConnected = getIntent().getBooleanExtra("isConnected", false);
|
||||
if (alreadyConnected) {
|
||||
android.util.Log.d("CallActivity", "被叫方接听,直接进入通话状态");
|
||||
Log.d(TAG, "被叫方接听,直接进入通话状态");
|
||||
isConnected = true;
|
||||
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() {
|
||||
callManager = CallManager.getInstance(this);
|
||||
callManager.setStateListener(this);
|
||||
|
||||
// 确保WebSocket已连接(主叫方需要接收接听/拒绝通知)
|
||||
// 确保WebSocket已连接
|
||||
String userId = com.example.livestreaming.net.AuthStore.getUserId(this);
|
||||
if (userId != null && !userId.isEmpty()) {
|
||||
try {
|
||||
int uid = (int) Double.parseDouble(userId);
|
||||
if (uid > 0) {
|
||||
android.util.Log.d("CallActivity", "确保WebSocket连接,userId: " + uid);
|
||||
Log.d(TAG, "确保WebSocket连接,userId: " + uid);
|
||||
callManager.connect(uid);
|
||||
}
|
||||
} 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() {
|
||||
btnMinimize.setOnClickListener(v -> {
|
||||
// 最小化通话(后台运行)
|
||||
moveTaskToBack(true);
|
||||
});
|
||||
btnMinimize.setOnClickListener(v -> moveTaskToBack(true));
|
||||
|
||||
btnMute.setOnClickListener(v -> toggleMute());
|
||||
btnSpeaker.setOnClickListener(v -> toggleSpeaker());
|
||||
|
|
@ -162,7 +406,7 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
} else {
|
||||
callManager.rejectCall(callId);
|
||||
}
|
||||
finish();
|
||||
releaseAndFinish();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -183,14 +427,11 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
|
||||
// 根据连接状态设置界面
|
||||
if (isConnected) {
|
||||
// 已接通,显示通话中界面
|
||||
tvCallStatus.setVisibility(View.GONE);
|
||||
tvCallDuration.setVisibility(View.VISIBLE);
|
||||
layoutCallControls.setVisibility(View.VISIBLE);
|
||||
handler.post(durationRunnable);
|
||||
android.util.Log.d("CallActivity", "updateUI: 已接通状态,显示计时器");
|
||||
} else {
|
||||
// 未接通,显示等待状态
|
||||
if (isCaller) {
|
||||
tvCallStatus.setText("正在呼叫...");
|
||||
} else {
|
||||
|
|
@ -203,7 +444,10 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
isMuted = !isMuted;
|
||||
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);
|
||||
// TODO: 实际静音控制
|
||||
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.setMuted(isMuted);
|
||||
}
|
||||
Toast.makeText(this, isMuted ? "已静音" : "已取消静音", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
|
|
@ -217,17 +461,35 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
private void toggleVideo() {
|
||||
isVideoEnabled = !isVideoEnabled;
|
||||
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();
|
||||
}
|
||||
|
||||
private void switchCamera() {
|
||||
// TODO: 切换前后摄像头
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.switchCamera();
|
||||
}
|
||||
Toast.makeText(this, "切换摄像头", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
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;
|
||||
callStartTime = System.currentTimeMillis();
|
||||
|
||||
|
|
@ -236,14 +498,22 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
layoutCallControls.setVisibility(View.VISIBLE);
|
||||
|
||||
handler.post(durationRunnable);
|
||||
android.util.Log.d("CallActivity", "onCallConnected() 执行完成,通话已连接");
|
||||
Toast.makeText(this, "通话已接通", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
// CallStateListener 实现
|
||||
private void releaseAndFinish() {
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.release();
|
||||
webRTCClient = null;
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
// ==================== CallStateListener 实现 ====================
|
||||
|
||||
@Override
|
||||
public void onCallStateChanged(String state, String callId) {
|
||||
// 状态变化处理
|
||||
Log.d(TAG, "onCallStateChanged: " + state);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -253,13 +523,8 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
|
||||
@Override
|
||||
public void onCallConnected(String callId) {
|
||||
android.util.Log.d("CallActivity", "========== onCallConnected 被调用 ==========");
|
||||
android.util.Log.d("CallActivity", "callId: " + callId);
|
||||
android.util.Log.d("CallActivity", "this.callId: " + this.callId);
|
||||
runOnUiThread(() -> {
|
||||
android.util.Log.d("CallActivity", "执行 onCallConnected UI更新");
|
||||
onCallConnected();
|
||||
});
|
||||
Log.d(TAG, "onCallConnected: " + callId);
|
||||
runOnUiThread(this::onCallConnected);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -286,7 +551,7 @@ public class CallActivity extends AppCompatActivity implements CallManager.CallS
|
|||
message = "通话已结束";
|
||||
}
|
||||
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
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
handler.removeCallbacks(durationRunnable);
|
||||
|
||||
// 恢复音频模式
|
||||
if (audioManager != null) {
|
||||
audioManager.setMode(AudioManager.MODE_NORMAL);
|
||||
audioManager.setSpeakerphoneOn(false);
|
||||
Log.d(TAG, "音频模式已恢复");
|
||||
}
|
||||
|
||||
if (callManager != null) {
|
||||
callManager.setStateListener(null);
|
||||
}
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.release();
|
||||
webRTCClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// 禁止返回键退出,需要点击挂断
|
||||
moveTaskToBack(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -351,19 +351,87 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
|||
// 启动来电界面
|
||||
Log.d(TAG, "启动来电界面 IncomingCallActivity");
|
||||
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("callType", callType);
|
||||
intent.putExtra("callerId", callerId);
|
||||
intent.putExtra("callerName", callerName);
|
||||
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) {
|
||||
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
|
||||
public void onCallAccepted(String callId) {
|
||||
Log.d(TAG, "========== 收到通话接听通知 ==========");
|
||||
|
|
@ -417,17 +485,56 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
|||
|
||||
@Override
|
||||
public void onOffer(String callId, String sdp) {
|
||||
// WebRTC offer处理 - 后续实现
|
||||
Log.d(TAG, "收到 Offer");
|
||||
// 通知 CallActivity 处理 Offer
|
||||
if (stateListener instanceof CallActivity) {
|
||||
((CallActivity) stateListener).handleRemoteOffer(sdp);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnswer(String callId, String sdp) {
|
||||
// WebRTC answer处理 - 后续实现
|
||||
Log.d(TAG, "收到 Answer");
|
||||
// 通知 CallActivity 处理 Answer
|
||||
if (stateListener instanceof CallActivity) {
|
||||
((CallActivity) stateListener).handleRemoteAnswer(sdp);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ public class CallSignalingClient {
|
|||
private String baseUrl;
|
||||
private int userId;
|
||||
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 {
|
||||
void onConnected();
|
||||
|
|
@ -54,19 +60,26 @@ public class CallSignalingClient {
|
|||
}
|
||||
|
||||
public void connect() {
|
||||
isManualDisconnect = false;
|
||||
doConnect();
|
||||
}
|
||||
|
||||
private void doConnect() {
|
||||
String wsUrl = baseUrl.replace("http://", "ws://").replace("https://", "wss://");
|
||||
if (!wsUrl.endsWith("/")) wsUrl += "/";
|
||||
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();
|
||||
webSocket = client.newWebSocket(request, new WebSocketListener() {
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
Log.d(TAG, "WebSocket connected");
|
||||
Log.d(TAG, "WebSocket connected successfully!");
|
||||
isConnected = true;
|
||||
reconnectAttempts = 0; // 重置重连计数
|
||||
register();
|
||||
startHeartbeat();
|
||||
notifyConnected();
|
||||
}
|
||||
|
||||
|
|
@ -86,18 +99,49 @@ public class CallSignalingClient {
|
|||
Log.d(TAG, "WebSocket closed: " + reason);
|
||||
isConnected = false;
|
||||
notifyDisconnected();
|
||||
// 非手动断开时尝试重连
|
||||
if (!isManualDisconnect) {
|
||||
scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
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() {
|
||||
isManualDisconnect = true;
|
||||
reconnectAttempts = 0;
|
||||
stopHeartbeat();
|
||||
if (webSocket != null) {
|
||||
webSocket.close(1000, "User disconnect");
|
||||
webSocket = null;
|
||||
|
|
@ -105,6 +149,34 @@ public class CallSignalingClient {
|
|||
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() {
|
||||
return isConnected;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
*/
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:background="#1A1A2E">
|
||||
|
||||
<!-- 背景模糊头像 -->
|
||||
<!-- 背景模糊头像(语音通话时显示) -->
|
||||
<ImageView
|
||||
android:id="@+id/ivBackgroundAvatar"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -13,21 +13,22 @@
|
|||
android:scaleType="centerCrop"
|
||||
android:alpha="0.3" />
|
||||
|
||||
<!-- 视频通话时的远程视频 -->
|
||||
<SurfaceView
|
||||
android:id="@+id/remoteVideoView"
|
||||
<!-- 视频通话时的远程视频容器 -->
|
||||
<FrameLayout
|
||||
android:id="@+id/layoutRemoteVideo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 视频通话时的本地视频(小窗口) -->
|
||||
<SurfaceView
|
||||
android:id="@+id/localVideoView"
|
||||
<!-- 视频通话时的本地视频容器(小窗口) -->
|
||||
<FrameLayout
|
||||
android:id="@+id/layoutLocalVideo"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_marginTop="80dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="#333333"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
|
||||
# 使用 Android Studio 自带的 JDK 17
|
||||
org.gradle.java.home=C:/Program Files/Android/Android Studio/jbr
|
||||
|
||||
systemProp.gradle.wrapperUser=myuser
|
||||
systemProp.gradle.wrapperPassword=mypassword
|
||||
Loading…
Reference in New Issue
Block a user