效果
服务器的实现
请查看:https://huangxiaoguo.blog.csdn.net/article/details/104226077
web端实现
请查看:webRTC(十八):webrtc 实现web端对端的纯文本聊天互通
android端实现
- 引入第三方库
implementation 'org.webrtc:google-webrtc:1.0.30039'
implementation('io.socket:socket.io-client:1.0.0') {
exclude group: 'org.json', module: 'json'
}
implementation 'com.github.huangxiaoguo1:hxgpermissions:1.0.2'
android {
...
//处理打包出错
lintOptions {
checkReleaseBuilds false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
abortOnError false
}
//处理编译报错
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
...
}
- 申请权限
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- 动态权限处理
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
HxgPermissionHelper.requestPermissionsResult(this, requestCode, permissions);
}
// ------------------------------------
HxgPermissionHelper.with(this)
.requestCode(REQUESE_CODE)
.requestPermission(Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO)
.request();
// ------------------------------------
@HxgPermissionSuccess(requestCode = REQUESE_CODE)
private void success() {
}
@HxgPermissionFail(requestCode = REQUESE_CODE)
private void fail() {
Toast.makeText(this, "失败了", Toast.LENGTH_SHORT).show();
}
- 信令处理
import org.json.JSONObject;
import java.net.URISyntaxException;
import io.socket.client.IO;
import io.socket.client.Socket;
import io.socket.emitter.Emitter;
import okhttp3.OkHttpClient;
public class SignalClient {
private static final String TAG = "SignalClient";
private static SignalClient mInstance;
private OnSignalEventListener mOnSignalEventListener;
private Socket mSocket;
private String mRoomName;
private IO.Options opts;
public interface OnSignalEventListener {
void onConnected();
void onConnecting();
void onDisconnected();
void onUserJoined(String roomName, String userID);
void onUserLeaved(String roomName, String userID);
void onRemoteUserJoined(String roomName);
void onRemoteUserLeaved(String roomName, String userID);
void onRoomFull(String roomName, String userID);
void onMessage(JSONObject message);
}
public static SignalClient getInstance() {
synchronized (SignalClient.class) {
if (mInstance == null) {
mInstance = new SignalClient();
}
}
return mInstance;
}
public void setSignalEventListener(final OnSignalEventListener listener) {
mOnSignalEventListener = listener;
}
// private OkHttpClient okHttpClient = new OkHttpClient.Builder()
// .hostnameVerifier(new TrustAllCerts.TrustAllHostnameVerifier())
// .sslSocketFactory(TrustAllCerts.createSSLSocketFactory())
// .build();
/**
* 加入房间
*
* @param url
* @param roomName
*/
public void joinRoom(String url, String roomName) {
Log.i(TAG, "joinRoom: " + url + ", " + roomName);
try {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
//cloud_huang.pem证书
HTTPSCerUtils.setCertificate(UIUtils.getContext(), builder, R.raw.cloud_huang);
OkHttpClient okHttpClient = builder.build();
IO.setDefaultOkHttpWebSocketFactory(okHttpClient);
IO.setDefaultOkHttpCallFactory(okHttpClient);
opts = new IO.Options();
opts.callFactory = okHttpClient;
opts.webSocketFactory = okHttpClient;
mSocket = IO.socket(url,opts);
mSocket.connect();
} catch (URISyntaxException e) {
e.printStackTrace();
return;
}
//mUserId = userId;
mRoomName = roomName;
listenSignalEvents();
mSocket.emit("join", mRoomName);
}
public void leaveRoom() {
Log.i(TAG, "leaveRoom: " + mRoomName);
if (mSocket == null) {
return;
}
mSocket.emit("leave", mRoomName);
mSocket.close();
mSocket = null;
}
public void sendMessage(JSONObject message) {
Log.i(TAG, "broadcast: " + message);
if (mSocket == null) {
return;
}
mSocket.emit("message", mRoomName, message);
}
//侦听从服务器收到的消息
private void listenSignalEvents() {
if (mSocket == null) {
return;
}
mSocket.on(Socket.EVENT_CONNECT_ERROR, new Emitter.Listener() {
@Override
public void call(Object... args) {
Log.e(TAG, "onConnectError: " + args.getClass().getClasses());
}
});
mSocket.on(Socket.EVENT_ERROR, new Emitter.Listener() {
@Override
public void call(Object... args) {
Log.e(TAG, "onError: " + args);
}
});
mSocket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
@Override
public void call(Object... args) {
String sessionId = mSocket.id();
Log.i(TAG, "onConnected");
if (mOnSignalEventListener != null) {
mOnSignalEventListener.onConnected();
}
}
});
mSocket.on(Socket.EVENT_CONNECTING, new Emitter.Listener() {
@Override
public void call(Object... args) {
Log.i(TAG, "onConnecting");
if (mOnSignalEventListener != null) {
mOnSignalEventListener.onConnecting();
}
}
});
mSocket.on(Socket.EVENT_DISCONNECT, new Emitter.Listener() {
@Override
public void call(Object... args) {
Log.i(TAG, "onDisconnected");
if (mOnSignalEventListener != null) {
mOnSignalEventListener.onDisconnected();
}
}
});
mSocket.on("joined", new Emitter.Listener() {
@Override
public void call(Object... args) {
String roomName = (String) args[0];
String userId = (String) args[1];
if (/*!mUserId.equals(userId) &&*/ mOnSignalEventListener != null) {
//mOnSignalEventListener.onRemoteUserJoined(userId);
mOnSignalEventListener.onUserJoined(roomName, userId);
}
//Log.i(TAG, "onRemoteUserJoined: " + userId);
Log.i(TAG, "onUserJoined, room:" + roomName + "uid:" + userId);
}
});
mSocket.on("leaved", new Emitter.Listener() {
@Override
public void call(Object... args) {
String roomName = (String) args[0];
String userId = (String) args[1];
if (/*!mUserId.equals(userId) &&*/ mOnSignalEventListener != null) {
//mOnSignalEventListener.onRemoteUserLeft(userId);
mOnSignalEventListener.onUserLeaved(roomName, userId);
}
Log.i(TAG, "onUserLeaved, room:" + roomName + "uid:" + userId);
}
});
mSocket.on("otherjoin", new Emitter.Listener() {
@Override
public void call(Object... args) {
String roomName = (String) args[0];
String userId = (String) args[1];
if (mOnSignalEventListener != null) {
mOnSignalEventListener.onRemoteUserJoined(roomName);
}
Log.i(TAG, "onRemoteUserJoined, room:" + roomName + "uid:" + userId);
}
});
mSocket.on("bye", new Emitter.Listener() {
@Override
public void call(Object... args) {
String roomName = (String) args[0];
String userId = (String) args[1];
if (mOnSignalEventListener != null) {
mOnSignalEventListener.onRemoteUserLeaved(roomName, userId);
}
Log.i(TAG, "onRemoteUserLeaved, room:" + roomName + "uid:" + userId);
}
});
mSocket.on("full", new Emitter.Listener() {
@Override
public void call(Object... args) {
//释放资源
mSocket.disconnect();
mSocket.close();
mSocket = null;
String roomName = (String) args[0];
String userId = (String) args[1];
if (mOnSignalEventListener != null) {
mOnSignalEventListener.onRoomFull(roomName, userId);
}
Log.i(TAG, "onRoomFull, room:" + roomName + "uid:" + userId);
}
});
mSocket.on("message", new Emitter.Listener() {
@Override
public void call(Object... args) {
try {
String roomName = (String) args[0];
String userId = (String) args[1];
JSONObject msg = (JSONObject) args[2];
if (mOnSignalEventListener != null) {
mOnSignalEventListener.onMessage(msg);
}
Log.i(TAG, "onMessage, room:" + roomName +",userId:"+userId+ ",data:" + msg);
}catch (Exception e){
e.printStackTrace();
}
}
});
}
}
- https 信任证书
//只信任指定证书(传入raw资源ID)
public static void setCertificate(Context context, OkHttpClient.Builder okHttpClientBuilder, int cerResID) {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
InputStream inputStream = context.getResources().openRawResource(cerResID);
Certificate ca = certificateFactory.generateCertificate(inputStream);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
inputStream.close();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
okHttpClientBuilder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) tmf.getTrustManagers()[0]);
okHttpClientBuilder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
- 布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ChatActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/et_input"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_send"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发送" />
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:id="@+id/tv_context"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.core.widget.NestedScrollView>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:id="@+id/LogcatView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start" />
</androidx.core.widget.NestedScrollView>
</LinearLayout>
- 视频交互实现(代码内有详细注释)
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.example.webrtcdemo.signal.SignalClient;
import org.json.JSONException;
import org.json.JSONObject;
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.VideoDecoderFactory;
import org.webrtc.VideoEncoderFactory;
import java.nio.ByteBuffer;
import java.util.LinkedList;
public class OnlyTextActivity extends AppCompatActivity implements SignalClient.OnSignalEventListener {
private static final String TAG = "OnlyTextActivity--huang";
private String serverAddr;
private String roomName;
private TextView tvContext;
private EditText etInput;
private Button btnSend;
private TextView mLogcatView;
/**
* 状态机
*/
private String mState = "init";
//用于数据传输
private PeerConnection mPeerConnection;
private PeerConnectionFactory mPeerConnectionFactory;
private DataChannel mDataChannel;
public void doLeave() {
logcatOnUI("Leave room, Wait ...");
hangup();
SignalClient.getInstance().leaveRoom();
}
@Override
protected void onDestroy() {
super.onDestroy();
doLeave();
PeerConnectionFactory.stopInternalTracingCapture();
PeerConnectionFactory.shutdownInternalTracer();
if (mPeerConnectionFactory != null)
mPeerConnectionFactory.dispose();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_only_text);
mPeerConnectionFactory = createPeerConnectionFactory(this);
initData();
initView();
initListener();
}
private void initData() {
/**
* 服务器
*/
serverAddr = getIntent().getStringExtra("ServerAddr");
/**
* 房间名称
*/
roomName = getIntent().getStringExtra("RoomName");
/**
* 添加监听
*/
SignalClient.getInstance().setSignalEventListener(this);
/**
* 加入房间
*/
SignalClient.getInstance().joinRoom(serverAddr, roomName);
}
private void initView() {
mLogcatView = findViewById(R.id.LogcatView);
tvContext = findViewById(R.id.tv_context);
etInput = findViewById(R.id.et_input);
btnSend = findViewById(R.id.btn_send);
}
private void initListener() {
btnSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String message = etInput.getText().toString();
if (TextUtils.isEmpty(message)){
return;
}
String text = "我: " + message;
tvContext.setText(tvContext.getText().toString() + "\n" + text);
sendDataChannelMessage(message);
etInput.setText("");
}
});
this.setWebRtcListener(new WebRtcListener() {
@Override
public void onReceiveDataChannelMessage(String message) {
runOnUiThread(new Runnable() {
@Override
public void run() {
tvContext.setText(tvContext.getText().toString() + "\n" + "对方: "+message);
}
});
}
});
}
/**
* 连接服务器成功
*/
@Override
public void onConnected() {
logcatOnUI("连接服务器成功");
}
/**
* 正在连接服务器
*/
@Override
public void onConnecting() {
logcatOnUI("正在连接服务器");
}
/**
* 断开连接
*/
@Override
public void onDisconnected() {
logcatOnUI("断开连接");
}
/**
* 本地用户加入房间
*
* @param roomName
* @param userID
*/
@Override
public void onUserJoined(String roomName, String userID) {
logcatOnUI("本地用户加入房间");
mState = "joined";
//这里应该创建PeerConnection
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
}
/**
* 本地用户离开房间
*
* @param roomName
* @param userID
*/
@Override
public void onUserLeaved(String roomName, String userID) {
logcatOnUI("本地用户离开房间");
mState = "leaved";
}
/**
* 远端客户加入房间
*
* @param roomName
*/
@Override
public void onRemoteUserJoined(String roomName) {
logcatOnUI("远端客户加入房间, room: " + roomName);
if (mState.equals("joined_unbind")) {
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
}
mState = "joined_conn";
//调用call, 进行媒体协商
doStartCall();
}
/**
* 远端客户离开房间
*
* @param roomName
* @param userID
*/
@Override
public void onRemoteUserLeaved(String roomName, String userID) {
logcatOnUI("远端客户离开房间, room: " + roomName + "uid:" + userID);
mState = "joined_unbind";
if (mPeerConnection != null) {
mPeerConnection.close();
mPeerConnection = null;
}
}
/**
* 房间满员
*
* @param roomName
* @param userID
*/
@Override
public void onRoomFull(String roomName, String userID) {
logcatOnUI("房间满员, room: " + roomName + "uid:" + userID);
mState = "leaved";
PeerConnectionFactory.stopInternalTracingCapture();
PeerConnectionFactory.shutdownInternalTracer();
if (mPeerConnectionFactory != null) {
mPeerConnectionFactory.dispose();
mPeerConnectionFactory = null;
}
finish();
}
/**
* 消息通道
*
* @param message
*/
@Override
public void onMessage(JSONObject message) {
Log.i(TAG, "onMessage: " + message);
try {
String type = message.getString("type");
if (type.equals("offer")) {
onRemoteOfferReceived(message);
} else if (type.equals("answer")) {
onRemoteAnswerReceived(message);
} else if (type.equals("candidate")) {
onRemoteCandidateReceived(message);
} else {
Log.w(TAG, "the type is invalid: " + type);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
* 创建 factory, pc是从factory里获得的
*
* @param context
* @return
*/
public PeerConnectionFactory createPeerConnectionFactory(Context context) {
/**
* 这里音视频解码必须添加,因为对比发现web端转发过来端sdp 即使不进行媒体传输,也是含有音视频信息,
* 如果不添加,在setRemoteDescription时回奔溃,
*
* 注意如果单独进行,移动端的传输可以去掉
* .setVideoEncoderFactory(encoderFactory)
* .setVideoDecoderFactory(decoderFactory);
*
* */
EglBase mRootEglBase = EglBase.create();
final VideoEncoderFactory encoderFactory;
final VideoDecoderFactory decoderFactory;
/**
* 编码
*/
encoderFactory = new DefaultVideoEncoderFactory(
mRootEglBase.getEglBaseContext(),
false /* enableIntelVp8Encoder */,
true);//android H264 才能硬件加速
/**
*解码
*/
decoderFactory = new DefaultVideoDecoderFactory(mRootEglBase.getEglBaseContext());
PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(context)
.setEnableInternalTracer(true)//打开日志,线上关闭
.createInitializationOptions());
PeerConnectionFactory.Builder builder = PeerConnectionFactory.builder()
.setVideoEncoderFactory(encoderFactory)
.setVideoDecoderFactory(decoderFactory);
builder.setOptions(null);
return builder.createPeerConnectionFactory();
}
/**
* 创建通道连接
*
* @return
*/
public PeerConnection createPeerConnection() {
Log.i(TAG, "创建 PeerConnection ...");
LinkedList<PeerConnection.IceServer> iceServers = new LinkedList<PeerConnection.IceServer>();
PeerConnection.IceServer ice_server =
PeerConnection.IceServer.builder("turn:www.huangxiaoguo.club:3478")
.setPassword("123456")
.setUsername("huang")
.createIceServer();
iceServers.add(ice_server);
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
// TCP 候选者仅在连接到支持的服务器时有用
// candidates are only useful when connecting to a server that supports
// ICE-TCP.
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
//rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
//rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE;
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
// Use ECDSA encryption.
//rtcConfig.keyType = PeerConnection.KeyType.ECDSA;
// Enable DTLS for normal calls and disable for loopback calls.
rtcConfig.enableDtlsSrtp = true;
//rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
PeerConnection connection =
mPeerConnectionFactory.createPeerConnection(rtcConfig,
mPeerConnectionObserver);
/*
DataChannel.Init 可配参数说明:
ordered:是否保证顺序传输;
maxRetransmitTimeMs:重传允许的最长时间;
maxRetransmits:重传允许的最大次数;
*/
DataChannel.Init init = new DataChannel.Init();
init.ordered = true;
mDataChannel = connection.createDataChannel("dataChannel", init);
mDataChannel.registerObserver(mDataChannelObserver);
return connection;
}
/**
* PeerConnection的 观察者,监听回掉
*/
private PeerConnection.Observer mPeerConnectionObserver = new PeerConnection.Observer() {
@Override
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
Log.i(TAG, "onSignalingChange: " + signalingState);
}
@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
Log.i(TAG, "onIceConnectionChange: " + iceConnectionState.name());
}
@Override
public void onIceConnectionReceivingChange(boolean b) {
Log.i(TAG, "onIceConnectionChange: " + b);
}
@Override
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
Log.i(TAG, "onIceGatheringChange: " + iceGatheringState);
}
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
Log.i(TAG, "onIceCandidate: " + iceCandidate);
try {
JSONObject message = new JSONObject();
message.put("type", "candidate");
message.put("label", iceCandidate.sdpMLineIndex);
message.put("id", iceCandidate.sdpMid);
message.put("candidate", iceCandidate.sdp);
SignalClient.getInstance().sendMessage(message);
} catch (JSONException e) {
e.printStackTrace();
}
}
@Override
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
for (int i = 0; i < iceCandidates.length; i++) {
Log.i(TAG, "onIceCandidatesRemoved: " + iceCandidates[i]);
}
mPeerConnection.removeIceCandidates(iceCandidates);
}
@Override
public void onAddStream(MediaStream mediaStream) {
Log.i(TAG, "onAddStream: " + mediaStream.videoTracks.size());
}
@Override
public void onRemoveStream(MediaStream mediaStream) {
Log.i(TAG, "onRemoveStream");
}
@Override
public void onDataChannel(DataChannel dataChannel) {
Log.i(TAG, "onDataChannel");
dataChannel.registerObserver(mDataChannelObserver);
}
@Override
public void onRenegotiationNeeded() {
Log.i(TAG, "onRenegotiationNeeded");
}
@Override
public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
}
};
/**
* DataChannel 观察者,监听回掉
*/
private DataChannel.Observer mDataChannelObserver = new DataChannel.Observer() {
@Override
public void onBufferedAmountChange(long l) {
}
@Override
public void onStateChange() {
Log.d(TAG, "onDataChannel onStateChange:" + mDataChannel.state());
}
@Override
public void onMessage(DataChannel.Buffer buffer) {
Log.d(TAG, "onDataChannel onMessage : " + buffer);
ByteBuffer data = buffer.data;
byte[] bytes = new byte[data.capacity()];
data.get(bytes);
String msg = new String(bytes);
//TODO --------------------------------------------------------------------------------------------
Log.e(TAG, "msg============> : " + msg);
if (webRtcListener != null) {
webRtcListener.onReceiveDataChannelMessage(msg);
}
}
};
private void logcatOnUI(String msg) {
Log.i(TAG, msg);
runOnUiThread(new Runnable() {
@Override
public void run() {
String output = mLogcatView.getText() + "\n" + msg;
mLogcatView.setText(output);
}
});
}
private WebRtcListener webRtcListener;
public void setWebRtcListener(WebRtcListener webRtcListener) {
this.webRtcListener = webRtcListener;
}
public interface WebRtcListener {
void onReceiveDataChannelMessage(String message);
}
//---------------------------------------媒体协商--------------------------------------------
/**
* 开始进行媒体协商
*/
public void doStartCall() {
logcatOnUI("Start Call, Wait ...");
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
MediaConstraints mediaConstraints = new MediaConstraints();
//打开DTLS,不打开和浏览器之间不可以进行互通
mediaConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
mPeerConnection.createOffer(new SimpleSdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.i(TAG, "Create local offer success: \n" + sessionDescription.description);
mPeerConnection.setLocalDescription(new SimpleSdpObserver(), sessionDescription);
JSONObject message = new JSONObject();
try {
message.put("type", "offer");
message.put("sdp", sessionDescription.description);
SignalClient.getInstance().sendMessage(message);
} catch (JSONException e) {
e.printStackTrace();
}
}
}, mediaConstraints);
}
public static class SimpleSdpObserver implements SdpObserver {
@Override
public void onCreateSuccess(SessionDescription sdp) {
Log.i(TAG, "SdpObserver: onCreateSuccess !");
}
@Override
public void onSetSuccess() {
Log.i(TAG, "SdpObserver: onSetSuccess");
}
@Override
public void onCreateFailure(String msg) {
Log.e(TAG, "SdpObserver onCreateFailure: " + msg);
}
@Override
public void onSetFailure(String msg) {
Log.e(TAG, "SdpObserver onSetFailure: " + msg);
}
}
/**
* 与远端进行媒体协商
*
* @param message
*/
private void onRemoteOfferReceived(JSONObject message) {
logcatOnUI("Receive Remote Call ...");
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
try {
String description = message.getString("sdp");
SessionDescription sessionDescription = new SessionDescription(
SessionDescription.Type.OFFER,
description);
Log.e(TAG,"description=====>"+description);
mPeerConnection.setRemoteDescription(
new SimpleSdpObserver(),
sessionDescription);
doAnswerCall();
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
* 创建回答
*/
public void doAnswerCall() {
logcatOnUI("Answer Call, Wait ...");
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
MediaConstraints sdpMediaConstraints = new MediaConstraints();
//打开DTLS,不打开和浏览器之间不可以进行互通
sdpMediaConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
Log.i(TAG, "Create answer ...");
mPeerConnection.createAnswer(new SimpleSdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
Log.i(TAG, "Create answer success !");
mPeerConnection.setLocalDescription(new SimpleSdpObserver(),
sessionDescription);
JSONObject message = new JSONObject();
try {
message.put("type", "answer");
message.put("sdp", sessionDescription.description);
SignalClient.getInstance().sendMessage(message);
} catch (JSONException e) {
e.printStackTrace();
}
}
}, sdpMediaConstraints);
}
/**
* 接收远程回答
*
* @param message
*/
private void onRemoteAnswerReceived(JSONObject message) {
logcatOnUI("Receive Remote Answer ...");
try {
String description = message.getString("sdp");
mPeerConnection.setRemoteDescription(
new SimpleSdpObserver(),
new SessionDescription(
SessionDescription.Type.ANSWER,
description));
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
* 接收远程候选人
*
* @param message
*/
private void onRemoteCandidateReceived(JSONObject message) {
logcatOnUI("Receive Remote Candidate ...");
try {
IceCandidate remoteIceCandidate =
new IceCandidate(message.getString("id"),
message.getInt("label"),
message.getString("candidate"));
mPeerConnection.addIceCandidate(remoteIceCandidate);
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
* 挂断
*/
private void hangup() {
logcatOnUI("Hangup Call, Wait ...");
if (mPeerConnection == null) {
return;
}
mPeerConnection.close();
mPeerConnection = null;
logcatOnUI("Hangup Done.");
}
/********************************************************************/
/**
* 发送消息
*
* @param message
*/
public void sendDataChannelMessage(String message) {
byte[] msg = message.getBytes();
DataChannel.Buffer buffer = new DataChannel.Buffer(
ByteBuffer.wrap(msg),
false);
mDataChannel.send(buffer);
}
}
web端测试地址:https://www.huangxiaoguo.club/onlyText/index.html
服务器地址:https://www.huangxiaoguo.club
房间号:888888 (建议,因为web端写死为888888了)
这样web端和web端,web端和android端,android端和android端纯文本聊天互通就完成了
源码地址:https://gitee.com/huangxiaoguo/webRTCdemo