效果
服务器的实现
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"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<org.webrtc.SurfaceViewRenderer
android:id="@+id/RemoteSurfaceView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<org.webrtc.SurfaceViewRenderer
android:id="@+id/LocalSurfaceView"
android:layout_width="120dp"
android:layout_height="160dp"
android:layout_gravity="top|end"
android:layout_margin="16dp"/>
<TextView
android:id="@+id/LogcatView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:textColor="@android:color/white"
android:layout_gravity="top|start" />
</FrameLayout>
- 视频交互实现(代码内有详细注释)
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.example.webrtcdemo.signal.SignalClient;
import org.json.JSONException;
import org.json.JSONObject;
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.Logging;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.MediaStreamTrack;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.RendererCommon;
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.VideoDecoderFactory;
import org.webrtc.VideoEncoderFactory;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class CallActivity extends AppCompatActivity implements SignalClient.OnSignalEventListener {
private String serverAddr;
private String roomName;
private TextView mLogcatView;
private SurfaceViewRenderer mLocalSurfaceView;
private SurfaceViewRenderer mRemoteSurfaceView;
private static final String TAG = "CallActivity----huang";
private static final int VIDEO_RESOLUTION_WIDTH = 1280;
private static final int VIDEO_RESOLUTION_HEIGHT = 720;
private static final int VIDEO_FPS = 30;
/**
* 状态机
*/
private String mState = "init";
public static final String VIDEO_TRACK_ID = "ARDAMSv0";//"ARDAMSv0";
public static final String AUDIO_TRACK_ID = "ARDAMSa0";//"ARDAMSa0";
//用于数据传输
private PeerConnection mPeerConnection;
private PeerConnectionFactory mPeerConnectionFactory;
//OpenGL ES
private EglBase mRootEglBase;
//纹理渲染
private SurfaceTextureHelper mSurfaceTextureHelper;
private VideoTrack mVideoTrack;
private AudioTrack mAudioTrack;
private VideoCapturer mVideoCapturer;
public void doLeave() {
logcatOnUI("Leave room, Wait ...");
hangup();
SignalClient.getInstance().leaveRoom();
}
@Override
protected void onDestroy() {
super.onDestroy();
doLeave();
mLocalSurfaceView.release();
mRemoteSurfaceView.release();
mVideoCapturer.dispose();
mSurfaceTextureHelper.dispose();
PeerConnectionFactory.stopInternalTracingCapture();
PeerConnectionFactory.shutdownInternalTracer();
mPeerConnectionFactory.dispose();
}
@Override
protected void onResume() {
super.onResume();
mVideoCapturer.startCapture(VIDEO_RESOLUTION_WIDTH, VIDEO_RESOLUTION_HEIGHT, VIDEO_FPS);
}
@Override
protected void onPause() {
super.onPause();
try {
mVideoCapturer.stopCapture();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_call);
mRootEglBase = EglBase.create();
initData();
initView();
//创建 factory, pc是从factory里获得的
mPeerConnectionFactory = createPeerConnectionFactory(this);
// NOTE: this _must_ happen while PeerConnectionFactory is alive!
Logging.enableLogToDebugOutput(Logging.Severity.LS_VERBOSE);
/**
* 捕捉视频
*/
mVideoCapturer = createVideoCapturer();
/**
* 纹理渲染
*/
mSurfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", mRootEglBase.getEglBaseContext());
/**
* 创建视频源,isScreencast:是否是投屏
*/
VideoSource videoSource = mPeerConnectionFactory.createVideoSource(false);
//传递视频信息
mVideoCapturer.initialize(mSurfaceTextureHelper, getApplicationContext(), videoSource.getCapturerObserver());
mVideoTrack = mPeerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
mVideoTrack.setEnabled(true);
mVideoTrack.addSink(mLocalSurfaceView);
/**
* 创建音屏源
*/
MediaConstraints mediaConstraints = new MediaConstraints();
// 添加所有现有的音频过滤器,以避免回声
//回声消除
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation2", "true"));
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googDAEchoCancellation", "true"));
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true"));
//自动增益
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl2", "true"));
//噪音处理
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression2", "true"));
//高音过滤
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAudioMirroring", "false"));
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true"));
AudioSource audioSource = mPeerConnectionFactory.createAudioSource(mediaConstraints);
mAudioTrack = mPeerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
mAudioTrack.setEnabled(true);
/**
* 添加监听
*/
SignalClient.getInstance().setSignalEventListener(this);
/**
* 加入房间
*/
SignalClient.getInstance().joinRoom(serverAddr, roomName);
}
private void initData() {
/**
* 服务器
*/
serverAddr = getIntent().getStringExtra("ServerAddr");
/**
* 房间名称
*/
roomName = getIntent().getStringExtra("RoomName");
}
private void initView() {
mLogcatView = findViewById(R.id.LogcatView);
mLocalSurfaceView = findViewById(R.id.LocalSurfaceView);
mRemoteSurfaceView = findViewById(R.id.RemoteSurfaceView);
/**
* 初始化,并传入openGL上下文
*/
mLocalSurfaceView.init(mRootEglBase.getEglBaseContext(), null);
/**
* 缩放类型,RendererCommon.ScalingType.SCALE_ASPECT_FILL:按照比例填充
*/
mLocalSurfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
/**
* 反转摄像头画面
*/
mLocalSurfaceView.setMirror(true);
/**
* 缩放时是否使用硬件
*/
mLocalSurfaceView.setEnableHardwareScaler(false /* enabled */);
mRemoteSurfaceView.init(mRootEglBase.getEglBaseContext(), null);
mRemoteSurfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
mRemoteSurfaceView.setMirror(true);
mRemoteSurfaceView.setEnableHardwareScaler(true /* enabled */);
/**
* 当两个view叠加时,谁在上面
*/
mLocalSurfaceView.setZOrderMediaOverlay(true);
}
/**
* 创建 factory, pc是从factory里获得的
*
* @param context
* @return
*/
public PeerConnectionFactory createPeerConnectionFactory(Context context) {
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();
}
/**
* 捕捉视频
* 使用Camera2
* Read more about Camera2 here
* https://developer.android.com/reference/android/hardware/camera2/package-summary.html
**/
private VideoCapturer createVideoCapturer() {
if (Camera2Enumerator.isSupported(this)) {
return createCameraCapturer(new Camera2Enumerator(this));
} else {
return createCameraCapturer(new Camera1Enumerator(true));
}
}
/**
* 捕捉视频
*
* @param enumerator
* @return
*/
private VideoCapturer createCameraCapturer(CameraEnumerator enumerator) {
final String[] deviceNames = enumerator.getDeviceNames();
// 使用前置摄像头
Log.d(TAG, "使用前置摄像头.");
for (String deviceName : deviceNames) {
if (enumerator.isFrontFacing(deviceName)) {
Logging.d(TAG, "创建前置摄像头捕获程序.");
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
if (videoCapturer != null) {
return videoCapturer;
}
}
}
// 使用后置摄像头
Log.d(TAG, "使用后置摄像头.");
for (String deviceName : deviceNames) {
if (!enumerator.isFrontFacing(deviceName)) {
Logging.d(TAG, "创建后置摄像头捕获程序.");
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
if (videoCapturer != null) {
return videoCapturer;
}
}
}
return null;
}
/**
* 连接服务器成功
*/
@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";
if (mLocalSurfaceView != null) {
mLocalSurfaceView.release();
mLocalSurfaceView = null;
}
if (mRemoteSurfaceView != null) {
mRemoteSurfaceView.release();
mRemoteSurfaceView = null;
}
if (mVideoCapturer != null) {
mVideoCapturer.dispose();
mVideoCapturer = null;
}
if (mSurfaceTextureHelper != null) {
mSurfaceTextureHelper.dispose();
mSurfaceTextureHelper = null;
}
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();
}
}
/**
* 创建通道连接
*
* @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);
if (connection == null) {
Log.e(TAG, "Failed to createPeerConnection !");
return null;
}
List<String> mediaStreamLabels = Collections.singletonList("ARDAMS");
connection.addTrack(mVideoTrack, mediaStreamLabels);
connection.addTrack(mAudioTrack, mediaStreamLabels);
return connection;
}
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);
}
@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("userId", RTCSignalClient.getInstance().getUserId());
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());
try {
/**
* 这里设置false,可以静音功能
*/
// mediaStream.audioTracks.get(0).setEnabled(false);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onRemoveStream(MediaStream mediaStream) {
Log.i(TAG, "onRemoveStream");
}
@Override
public void onDataChannel(DataChannel dataChannel) {
Log.i(TAG, "onDataChannel");
}
@Override
public void onRenegotiationNeeded() {
Log.i(TAG, "onRenegotiationNeeded");
}
@Override
public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
MediaStreamTrack track = rtpReceiver.track();
if (track instanceof VideoTrack) {
Log.i(TAG, "onAddVideoTrack");
VideoTrack remoteVideoTrack = (VideoTrack) track;
remoteVideoTrack.setEnabled(true);
remoteVideoTrack.addSink(mRemoteSurfaceView);
}
}
};
/**
* 开始进行媒体协商
*/
public void doStartCall() {
logcatOnUI("Start Call, Wait ...");
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
MediaConstraints mediaConstraints = new MediaConstraints();
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
//打开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 sessionDescription) {
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");
mPeerConnection.setRemoteDescription(
new SimpleSdpObserver(),
new SessionDescription(
SessionDescription.Type.OFFER,
description));
doAnswerCall();
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
* 创建回答
*/
public void doAnswerCall() {
logcatOnUI("Answer Call, Wait ...");
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
MediaConstraints sdpMediaConstraints = new MediaConstraints();
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);
updateCallState(false);
}
private void updateCallState(boolean idle) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (idle) {
mRemoteSurfaceView.setVisibility(View.GONE);
} else {
mRemoteSurfaceView.setVisibility(View.VISIBLE);
}
}
});
}
/**
* 接收远程回答
*
* @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();
}
updateCallState(false);
}
/**
* 接收远程候选人
*
* @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 onRemoteHangup() {
logcatOnUI("Receive Remote Hangup Event ...");
hangup();
}
/**
* 挂断
*/
private void hangup() {
logcatOnUI("Hangup Call, Wait ...");
if (mPeerConnection == null) {
return;
}
mPeerConnection.close();
mPeerConnection = null;
logcatOnUI("Hangup Done.");
updateCallState(true);
}
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);
}
});
}
}
web端测试地址:https://www.huangxiaoguo.club/textchat/room.html
服务器地址:https://www.huangxiaoguo.club
房间号:555555 (建议,因为web端写死为555555了)
这样web端和web端,web端和android端,android端和android端视频互通就完成了