webrtc Android入门非常的简单不要被那些博客给带乱了,我看了几篇这方面的博客都是给你零散的代码更本就不能实现通话,学这个要先从全局流程再到详细步骤来学习。
简单介绍下实现webrtc通话总体流程并且给出全部代码,复制粘贴即可
本文福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓
1、用户A和用户B实现通信双方间建立链接最重要的是双方获取彼此的sdp信息和ice信息。
sdp就是一段文本描述,里面包含了当前本地设备所支持的一些信息,比如设备是否支持h264编码,传输协议是什么。
ice也是一段文本,是配合p2p打洞服务器stun/turn让双方知道各自的公网ip和端口,从而实现端对端通信。
对于实现一个简单入门通信案例来说,不要太去深入的理解每个名词的具体意思,只要能总的明白是做什么就行了,等入门后再去深入理解每个知识点。
简单理解就是sdp是设备描述文本,ice就是ip端口描述文本
2、sdp,ice的创建和使用
用户A,B都需要设置setLocationDescription和setRemoteDescription。
呼叫者调用createOffer创建sdp,被呼叫者调用createAnswer创建sdp
ice信息是在创建peerConnection后会自动从stun/turn服务器请求回调。
下面来具体实现
xml布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#000"tools:context=".ui.SimpleMeetingActivity"><!--播放远端视频--><org.webrtc.SurfaceViewRendererandroid:layout_marginBottom="200dp"android:id="@+id/ivRemoteRender"android:layout_width="match_parent"android:layout_height="match_parent"/><!--播放本地视频--><org.webrtc.SurfaceViewRendererandroid:id="@+id/ivLocalRender"app:layout_constraintTop_toTopOf="parent"app:layout_constraintRight_toRightOf="parent"android:layout_width="150dp"android:layout_height="200dp"/><Viewandroid:id="@+id/ivJoin"android:layout_marginBottom="20dp"app:layout_constraintRight_toRightOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintBottom_toBottomOf="parent"android:background="#4CAF50"android:layout_width="100dp"android:layout_height="100dp"/></androidx.constraintlayout.widget.ConstraintLayout>
1.引入webrtc Android 和websocket
implementation 'org.webrtc:google-webrtc:1.0.32006'
//引入socket依赖
implementation 'org.java-websocket:Java-WebSocket:1.4.0'
2.全局初始化
//在初次使用PeerConnectionFactory之前,必须调用静态方法initialize()对其进行全局的初始化与资源加载PeerConnectionFactory.InitializationOptions initializationOptions =PeerConnectionFactory.InitializationOptions.builder(this).setEnableInternalTracer(true)// 启用内部追踪器,用来记录一些相关数据.createInitializationOptions();PeerConnectionFactory.initialize(initializationOptions);
3.创建PeerConnectionFactory
//-----------创建视频编码和解码器VideoEncoderFactory encoderFactory = new DefaultVideoEncoderFactory(mRootEGL.getEglBaseContext(), true, true);VideoDecoderFactory decoderFactory = new DefaultVideoDecoderFactory(mRootEGL.getEglBaseContext());//-----------创建PeerConnectionFactoryAudioDeviceModule adm = JavaAudioDeviceModule.builder(this).createAudioDeviceModule();//音频配置当前JAVA实现,还有nativePeerConnectionFactory.Options options = new PeerConnectionFactory.Options();//options.disableEncryption : true表示不用数据加密//options.disableNetworkMonitor : true表示禁用网络监视器factory = PeerConnectionFactory.builder().setOptions(options)//设置网络配置,使用默认.setAudioDeviceModule(adm)//设置音频采集和播放使用的配置,当前使用java中的audioTrack 和audioRecord.setVideoEncoderFactory(encoderFactory).setVideoDecoderFactory(decoderFactory).createPeerConnectionFactory();
4.创建声音源
//配置音频约束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"));audioSource = factory.createAudioSource(audioConstraints);
//创建音频轨道localAudioTrack = factory.createAudioTrack("102", audioSource);
5.创建视频源
//ScreenCapturerAndroid 录屏 ;FileVideoCapturer文件//从相机里获取视频流CameraEnumerator enumerator = new Camera2Enumerator(this);String[] deviceNames = enumerator.getDeviceNames();//遍历所有摄像头找到前置摄像头for (String deviceName : deviceNames) {if (enumerator.isFrontFacing(deviceName)) {videoCapturer = enumerator.createCapturer(deviceName, null);}}assert videoCapturer != null;videoSource = factory.createVideoSource(videoCapturer.isScreencast());//创建视频轨道videoTrack = factory.createVideoTrack("103", videoSource);
6.播放本地视频
//因为使用的是opengl 渲染surfaceTextureHelper = SurfaceTextureHelper.create("capture-thread", mRootEGL.getEglBaseContext());videoCapturer.initialize(surfaceTextureHelper, this, videoSource.getCapturerObserver());//开始录制videoCapturer.startCapture(720,//宽1080,//高25//fps 帧率);//播放本地视频localRender.init(mRootEGL.getEglBaseContext(), null);//SCALE_ASPECT_FILL 自动适应屏幕比例, 画面存在被裁剪的可能//SCALE_ASPECT_FIT 自动适应画面比例,屏幕上可能存在黑边//SCALE_ASPECT_BALANCED 视频尺寸非等比缩放。保证视频内容全部显示,且填满视窗。localRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);localRender.setMirror(true);//启用镜像videoTrack.addSink(localRender);//最后播放
7.初始化远端render
//初始化远端render ,因为使用opengl渲染所以 必须在在主线程初始化remoteRender.init(mRootEGL.getEglBaseContext(), null);remoteRender.setMirror(true);//启用镜像remoteRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
8.安装ice服务器
//在ubunto服务器上安装coturn软件即可实现ice服务器
sudo apt-get install libssl-dev
sudo apt-get install libevent-dev
git clone https://github.com/coturn/coturn
cd coturn
./configure
make
sudo make install
//启动服务器, wzp:123456 表示用户名:密码也可用通过配置文件来设置
sudo nohup turnserver -L 0.0.0.0 -a -u wzp:123456 -v -f -r nort.text &
//查看端口号,默认3478端口,
sudo lsof -i:3478
9.创建peerConntion
//ice服务器列表
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
//添加一个turn服务器,turn服务器主要用户下面的stun服务器打洞失败的时候使用这个turn服务器转发数据流,可以添加多个
iceServers.add(PeerConnection.IceServer.builder("turn:**.**.**.**3478")//这是你服务器的地址.setUsername("wzp")//用户名.setPassword("123456")//密码.createIceServer());//添加一个stun服务器,
iceServers.add(PeerConnection.IceServer.builder("stun:**.**.**.**:3478").createIceServer());peerConnection = factory.createPeerConnection(iceServers, new PeerConnection.Observer() {@Overridepublic void onSignalingChange(PeerConnection.SignalingState signalingState) {}@Overridepublic void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {//ICE 连接状态变化后回调}@Overridepublic void onIceConnectionReceivingChange(boolean b) {}@Overridepublic void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {}@Overridepublic void onIceCandidate(IceCandidate iceCandidate) {//自动请求stun/turn服务器后回调这个方法//发送Ice信息给对端用户 ,下面的代码只是用于发送信息给远端用户,我使用的是websocket,自己可以用其他方式实现。最后结尾我会给出服务器端的代码。JSONObject sendObj = new JSONObject();try {sendObj.put("cmd", cmd_ice);sendObj.put("uid", uid);sendObj.put("remoteUid", remoteUid);sendObj.put("roomId",roomId);JSONObject msgObj = new JSONObject();msgObj.put("sdpMid", iceCandidate.sdpMid);msgObj.put("sdpMLineIndex", iceCandidate.sdpMLineIndex);msgObj.put("sdp", iceCandidate.sdp);sendObj.put("msg", msgObj);socket.send(sendObj.toString());} catch (JSONException e) {throw new RuntimeException(e);}}@Overridepublic void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {}@Overridepublic void onAddStream(MediaStream mediaStream) {//收到远端数据流信息mediaStream.videoTracks.get(0).addSink(remoteRender);mediaStream.audioTracks.get(0).setEnabled(true);}@Overridepublic void onRemoveStream(MediaStream mediaStream) {}@Overridepublic void onDataChannel(DataChannel dataChannel) {}@Overridepublic void onRenegotiationNeeded() {}@Overridepublic void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {}});// List<String> mediaStreamLabels = Collections.singletonList("ARDAMS");
// peerConnection.addTrack(videoTrack,mediaStreamLabels);
// peerConnection.addTrack(localAudioTrack,mediaStreamLabels);//将本地流添加到peerConnection,远端的onAddStream回调将接受该数据流MediaStream stream = factory.createLocalMediaStream("110");stream.addTrack(videoTrack);stream.addTrack(localAudioTrack);peerConnection.addStream(stream);
10.建立websocket链接,只是用于演示效果,所以这里只是简单的实现。重要的是如何简单的去理解流程
URI uri = null;//信令服务器地址try {uri = new URI("ws://192.168.2.134:8090");} catch (Exception e) {}socket = new WebSocketClient(uri) {@Overridepublic void onOpen(ServerHandshake handshakedata) {if (isDestroyed()) {return;}Log.e(tag,"链接socket成功");}@Overridepublic void onMessage(String message) {if (isDestroyed()) {return;}try {JSONObject msgObj = new JSONObject(message);String cmd = msgObj.getString("cmd");Log.e(tag,"收到消息:" + message);if (cmd.equals(cmd_new_peer)) {//有新人加入房间handleNewPeer(msgObj);return;}if (cmd.equals(cmd_offer)) {//收到offer请求handleOffer(msgObj);return;}if (cmd.equals(cmd_answer)) {//收到answer请求handleAnswer(msgObj);return;}if (cmd.equals(cmd_ice)) {//收到ice信息handleIce(msgObj);}} catch (JSONException e) {throw new RuntimeException(e);}}@Overridepublic void onClose(int code, String reason, boolean remote) {if (isDestroyed()) {return;}}@Overridepublic void onError(Exception ex) {if (isDestroyed()) {return;}Log.e(tag,"socket错误" + ex.toString());}};socket.connect();
11.发起通话者创建offer并且设置本地setLocalDescription,最后发送给被呼叫者
private void handleNewPeer(JSONObject msgObj) {/*新人加入房间,创建offer,发起通话*/try {//被呼叫者的uidremoteUid = msgObj.getString("uid");MediaConstraints constraints = new MediaConstraints();constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));peerConnection.createOffer(new SdpObserver() {private SessionDescription localSdp;@Overridepublic void onCreateSuccess(SessionDescription sessionDescription) {//创建offer成功即成功localSdp = sessionDescription;peerConnection.setLocalDescription(this, sessionDescription);}@Overridepublic void onSetSuccess() {//setLocalDescription 成功后回调JSONObject sendObj = new JSONObject();try {sendObj.put("cmd", cmd_offer);sendObj.put("uid", uid);sendObj.put("remoteUid", remoteUid);sendObj.put("roomId",roomId);sendObj.put("msg", localSdp.description);socket.send(sendObj.toString());} catch (JSONException e) {throw new RuntimeException(e);}}@Overridepublic void onCreateFailure(String s) {}@Overridepublic void onSetFailure(String s) {}}, constraints);} catch (JSONException e) {throw new RuntimeException(e);}}
12.被呼叫者收到offer后设置setRemoteDescription ,创建answer并设置setLocalDescription,最后发送给呼叫者
private void handleOffer(JSONObject msgObj) {//收到offer, 当前身份是被呼叫者try {//发起通话者的uidremoteUid = msgObj.getString("uid");String sdpDescription = msgObj.getString("msg");SessionDescription sdp = new SessionDescription(SessionDescription.Type.OFFER, sdpDescription);peerConnection.setRemoteDescription(new SdpObserver() {private boolean isCreateAnswer;private String sdpDescription;@Overridepublic void onCreateSuccess(SessionDescription sessionDescription) {//createAnswer 创建成功时候回调Log.e(tag,"创建answer成功");sdpDescription = sessionDescription.description;peerConnection.setLocalDescription(this, sessionDescription);}@Overridepublic void onSetSuccess() {//setRemoteDescription setLocalDescription 成功时候的回调if (!isCreateAnswer) {Log.e(tag,"onSetSuccess1");//还未创建answer 说明是setRemoteDescription回调isCreateAnswer = true;MediaConstraints constraints = new MediaConstraints();constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));peerConnection.createAnswer(this, constraints);} else {Log.e(tag,"onSetSuccess2");//是setLocalDescription回调JSONObject sendObj = new JSONObject();try {sendObj.put("cmd", cmd_answer);sendObj.put("uid", uid);sendObj.put("remoteUid", remoteUid);sendObj.put("roomId",roomId);sendObj.put("msg", sdpDescription);socket.send(sendObj.toString());} catch (JSONException e) {throw new RuntimeException(e);}}}@Overridepublic void onCreateFailure(String s) {}@Overridepublic void onSetFailure(String s) {}}, sdp);} catch (JSONException e) {e.printStackTrace();throw new RuntimeException(e);}}
13.发起通话者收到answer后设置setRemoteDescription
private void handleAnswer(JSONObject msgObj) {//收到answer,当前是发起通话者try {String sdpDescription = msgObj.getString("msg");SessionDescription sdp = new SessionDescription(SessionDescription.Type.ANSWER, sdpDescription);peerConnection.setRemoteDescription(new SdpObserver() {@Overridepublic void onCreateSuccess(SessionDescription sessionDescription) {}@Overridepublic void onSetSuccess() {// setRemoteDescription 设置成功}@Overridepublic void onCreateFailure(String s) {}@Overridepublic void onSetFailure(String s) {}}, sdp);} catch (JSONException e) {throw new RuntimeException(e);}}
14,各自收到对方发来的ice信息时候addIceCandidate
private void handleIce(JSONObject msgObj) {//收到对方的ice信息try {Log.e(tag,"设置ICE信息");JSONObject iceObj = msgObj.getJSONObject("msg");IceCandidate iceCandidate = new IceCandidate(iceObj.getString("sdpMid"),iceObj.getInt("sdpMLineIndex"),iceObj.getString("sdp"));peerConnection.addIceCandidate(iceCandidate);} catch (JSONException e) {Log.e(tag,"ice设置失败:" + e.getMessage());throw new RuntimeException(e);}}
15.销毁
@Overrideprotected void onDestroy() {if (socket != null && socket.isOpen()) {socket.close();}socket = null;if(peerConnection != null){peerConnection.close();peerConnection = null;}if(videoSource != null){videoSource.dispose();videoSource = null;}if (audioSource != null) {audioSource.dispose();audioSource = null;}if(videoCapturer != null){try {videoCapturer.stopCapture();} catch (InterruptedException e) {throw new RuntimeException(e);}videoCapturer.dispose();videoCapturer = null;}if(surfaceTextureHelper != null){surfaceTextureHelper.dispose();surfaceTextureHelper = null;}localRender.release();remoteRender.release();if(factory != null){factory.dispose();factory = null;}if(mRootEGL != null){mRootEGL.release();mRootEGL = null;}super.onDestroy();}
最后给出java全部源码
package com.example.rtcmy.ui;import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.BindingConversion;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ObservableField;import android.database.Observable;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;import com.example.rtcmy.R;import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
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.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.RendererCommon;
import org.webrtc.RtpReceiver;
import org.webrtc.ScreenCapturerAndroid;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoDecoder;
import org.webrtc.VideoDecoderFactory;
import org.webrtc.VideoEncoderFactory;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import org.webrtc.audio.AudioDeviceModule;
import org.webrtc.audio.JavaAudioDeviceModule;import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;public class SimpleMeetingActivity extends AppCompatActivity {private WebSocketClient socket;private String roomId = "1";//房间号private String cmd_join_room = "cmd_join_room";//加入房间命令private String cmd_new_peer = "cmd_new_peer";//有新人加入房间private String cmd_offer = "cmd_offer";private String cmd_answer = "cmd_answer";private String cmd_ice = "cmd_ice";private SurfaceViewRenderer remoteRender;private SurfaceViewRenderer localRender;private TextView ivStatus;private View ivJoin;private EglBase mRootEGL;private PeerConnectionFactory factory;private VideoCapturer videoCapturer;private AudioSource audioSource;private AudioTrack localAudioTrack;private VideoSource videoSource;private VideoTrack videoTrack;private List<PeerConnection.IceServer> iceServers = new ArrayList<>();private SurfaceTextureHelper surfaceTextureHelper;private PeerConnection peerConnection;private String remoteUid;private String uid = UUID.randomUUID().toString();private String tag = "simpleWebrtc";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_simple_metting);ivStatus = findViewById(R.id.ivStatus);ivJoin = findViewById(R.id.ivJoin);remoteRender = findViewById(R.id.ivRemoteRender);localRender = findViewById(R.id.ivLocalRender);//开启日志iceServers.add(PeerConnection.IceServer.builder("turn:123.60.151.37:3478").setUsername("wzp").setPassword("123456").createIceServer());iceServers.add(PeerConnection.IceServer.builder("stun:123.60.151.37:3478").createIceServer());mRootEGL = EglBase.create();//在初次使用PeerConnectionFactory之前,必须调用静态方法initialize()对其进行全局的初始化与资源加载PeerConnectionFactory.InitializationOptions initializationOptions =PeerConnectionFactory.InitializationOptions.builder(this).setEnableInternalTracer(true)// 启用内部追踪器,用来记录一些相关数据.createInitializationOptions();PeerConnectionFactory.initialize(initializationOptions);//-----------创建视频编码和解码器VideoEncoderFactory encoderFactory = new DefaultVideoEncoderFactory(mRootEGL.getEglBaseContext(),true, true);VideoDecoderFactory decoderFactory = new DefaultVideoDecoderFactory(mRootEGL.getEglBaseContext());//-----------创建PeerConnectionFactoryAudioDeviceModule adm = JavaAudioDeviceModule.builder(this).createAudioDeviceModule();//音频配置当前JAVA实现,还有nativePeerConnectionFactory.Options options = new PeerConnectionFactory.Options();//options.disableEncryption : true表示不用数据加密//options.disableNetworkMonitor : true表示禁用网络监视器factory = PeerConnectionFactory.builder().setOptions(options)//设置网络配置,使用默认.setAudioDeviceModule(adm)//设置音频采集和播放使用的配置,当前使用java中的audioTrack 和audioRecord.setVideoEncoderFactory(encoderFactory).setVideoDecoderFactory(decoderFactory).createPeerConnectionFactory();//-----------创建声音源//配置音频约束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"));audioSource = factory.createAudioSource(audioConstraints);localAudioTrack = factory.createAudioTrack("102", audioSource);//----------创建视频源//ScreenCapturerAndroid 录屏 ;FileVideoCapturer文件//从相机里获取视频流CameraEnumerator enumerator = new Camera2Enumerator(this);String[] deviceNames = enumerator.getDeviceNames();//遍历所有摄像头找到前置摄像头for (String deviceName : deviceNames) {if (enumerator.isFrontFacing(deviceName)) {videoCapturer = enumerator.createCapturer(deviceName, null);}}assert videoCapturer != null;videoSource = factory.createVideoSource(videoCapturer.isScreencast());//创建视频轨道videoTrack = factory.createVideoTrack("103", videoSource);//因为使用的是opengl 渲染surfaceTextureHelper = SurfaceTextureHelper.create("capture-thread", mRootEGL.getEglBaseContext());videoCapturer.initialize(surfaceTextureHelper, this, videoSource.getCapturerObserver());//开始录制videoCapturer.startCapture(720,//宽1080,//高25//fps 帧率);//播放本地视频localRender.init(mRootEGL.getEglBaseContext(), null);//SCALE_ASPECT_FILL 自动适应屏幕比例, 画面存在被裁剪的可能//SCALE_ASPECT_FIT 自动适应画面比例,屏幕上可能存在黑边//SCALE_ASPECT_BALANCED 视频尺寸非等比缩放。保证视频内容全部显示,且填满视窗。localRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);localRender.setMirror(true);//启用镜像videoTrack.addSink(localRender);//最后播放//初始化远端render ,因为使用opengl渲染所以 必须在在主线程初始化remoteRender.init(mRootEGL.getEglBaseContext(), null);remoteRender.setMirror(true);//启用镜像remoteRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);//创建peerConnectionpeerConnection = factory.createPeerConnection(iceServers, new PeerConnection.Observer() {@Overridepublic void onSignalingChange(PeerConnection.SignalingState signalingState) {}@Overridepublic void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {//ICE 连接状态变化后回调}@Overridepublic void onIceConnectionReceivingChange(boolean b) {}@Overridepublic void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {}@Overridepublic void onIceCandidate(IceCandidate iceCandidate) {//自动请求stun/turn服务器后回调这个方法//发送Ice信息给对端用户JSONObject sendObj = new JSONObject();try {sendObj.put("cmd", cmd_ice);sendObj.put("uid", uid);sendObj.put("remoteUid", remoteUid);sendObj.put("roomId",roomId);JSONObject msgObj = new JSONObject();msgObj.put("sdpMid", iceCandidate.sdpMid);msgObj.put("sdpMLineIndex", iceCandidate.sdpMLineIndex);msgObj.put("sdp", iceCandidate.sdp);sendObj.put("msg", msgObj);socket.send(sendObj.toString());} catch (JSONException e) {throw new RuntimeException(e);}}@Overridepublic void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {}@Overridepublic void onAddStream(MediaStream mediaStream) {//收到远端数据流信息mediaStream.videoTracks.get(0).addSink(remoteRender);mediaStream.audioTracks.get(0).setEnabled(true);}@Overridepublic void onRemoveStream(MediaStream mediaStream) {}@Overridepublic void onDataChannel(DataChannel dataChannel) {}@Overridepublic void onRenegotiationNeeded() {}@Overridepublic void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {}});// List<String> mediaStreamLabels = Collections.singletonList("ARDAMS");
// peerConnection.addTrack(videoTrack,mediaStreamLabels);
// peerConnection.addTrack(localAudioTrack,mediaStreamLabels);//将本地流添加到peerConnection,远端的onAddStream回调将接受该数据流MediaStream stream = factory.createLocalMediaStream("110");stream.addTrack(videoTrack);stream.addTrack(localAudioTrack);peerConnection.addStream(stream);createSocket();ivJoin.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {if (!socket.isOpen()) {return;}ivStatus.setText("正在加入房间...");JSONObject sendObj = new JSONObject();try {sendObj.put("cmd", cmd_join_room);sendObj.put("uid", uid);sendObj.put("roomId", roomId);socket.send(sendObj.toString());} catch (JSONException e) {throw new RuntimeException(e);}}});}private void createSocket() {if (socket != null) {socket.close();}URI uri = null;//信令服务器地址try {uri = new URI("ws://192.168.2.134:8090");} catch (Exception e) {}socket = new WebSocketClient(uri) {@Overridepublic void onOpen(ServerHandshake handshakedata) {if (isDestroyed()) {return;}Log.e(tag,"链接socket成功");}@Overridepublic void onMessage(String message) {if (isDestroyed()) {return;}try {JSONObject msgObj = new JSONObject(message);String cmd = msgObj.getString("cmd");Log.e(tag,"收到消息:" + message);if (cmd.equals(cmd_new_peer)) {//有新人加入房间handleNewPeer(msgObj);return;}if (cmd.equals(cmd_offer)) {//收到offer请求handleOffer(msgObj);return;}if (cmd.equals(cmd_answer)) {//收到answer请求handleAnswer(msgObj);return;}if (cmd.equals(cmd_ice)) {//收到ice信息handleIce(msgObj);}} catch (JSONException e) {throw new RuntimeException(e);}}@Overridepublic void onClose(int code, String reason, boolean remote) {if (isDestroyed()) {return;}}@Overridepublic void onError(Exception ex) {if (isDestroyed()) {return;}Log.e(tag,"socket错误" + ex.toString());}};socket.connect();}private void handleIce(JSONObject msgObj) {//收到对方的ice信息try {Log.e(tag,"设置ICE信息");JSONObject iceObj = msgObj.getJSONObject("msg");IceCandidate iceCandidate = new IceCandidate(iceObj.getString("sdpMid"),iceObj.getInt("sdpMLineIndex"),iceObj.getString("sdp"));peerConnection.addIceCandidate(iceCandidate);} catch (JSONException e) {Log.e(tag,"ice设置失败:" + e.getMessage());throw new RuntimeException(e);}}private void handleAnswer(JSONObject msgObj) {//收到answer,当前是发起通话者try {String sdpDescription = msgObj.getString("msg");SessionDescription sdp = new SessionDescription(SessionDescription.Type.ANSWER, sdpDescription);peerConnection.setRemoteDescription(new SdpObserver() {@Overridepublic void onCreateSuccess(SessionDescription sessionDescription) {}@Overridepublic void onSetSuccess() {// setRemoteDescription 设置成功}@Overridepublic void onCreateFailure(String s) {}@Overridepublic void onSetFailure(String s) {}}, sdp);} catch (JSONException e) {throw new RuntimeException(e);}}private void handleOffer(JSONObject msgObj) {//收到offer, 当前身份是被呼叫者try {//发起通话者的uidremoteUid = msgObj.getString("uid");String sdpDescription = msgObj.getString("msg");SessionDescription sdp = new SessionDescription(SessionDescription.Type.OFFER, sdpDescription);peerConnection.setRemoteDescription(new SdpObserver() {private boolean isCreateAnswer;private String sdpDescription;@Overridepublic void onCreateSuccess(SessionDescription sessionDescription) {//createAnswer 创建成功时候回调Log.e(tag,"创建answer成功");sdpDescription = sessionDescription.description;peerConnection.setLocalDescription(this, sessionDescription);}@Overridepublic void onSetSuccess() {//setRemoteDescription setLocalDescription 成功时候的回调if (!isCreateAnswer) {Log.e(tag,"onSetSuccess1");//还未创建answer 说明是setRemoteDescription回调isCreateAnswer = true;MediaConstraints constraints = new MediaConstraints();constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));peerConnection.createAnswer(this, constraints);} else {Log.e(tag,"onSetSuccess2");//是setLocalDescription回调JSONObject sendObj = new JSONObject();try {sendObj.put("cmd", cmd_answer);sendObj.put("uid", uid);sendObj.put("remoteUid", remoteUid);sendObj.put("roomId",roomId);sendObj.put("msg", sdpDescription);socket.send(sendObj.toString());} catch (JSONException e) {throw new RuntimeException(e);}}}@Overridepublic void onCreateFailure(String s) {}@Overridepublic void onSetFailure(String s) {}}, sdp);} catch (JSONException e) {e.printStackTrace();throw new RuntimeException(e);}}private void handleNewPeer(JSONObject msgObj) {/*新人加入房间,创建offer,发起通话*/try {//被呼叫者的uidremoteUid = msgObj.getString("uid");MediaConstraints constraints = new MediaConstraints();constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));peerConnection.createOffer(new SdpObserver() {private SessionDescription localSdp;@Overridepublic void onCreateSuccess(SessionDescription sessionDescription) {//创建offer成功即成功localSdp = sessionDescription;peerConnection.setLocalDescription(this, sessionDescription);}@Overridepublic void onSetSuccess() {//setLocalDescription 成功后回调JSONObject sendObj = new JSONObject();try {sendObj.put("cmd", cmd_offer);sendObj.put("uid", uid);sendObj.put("remoteUid", remoteUid);sendObj.put("roomId",roomId);sendObj.put("msg", localSdp.description);socket.send(sendObj.toString());} catch (JSONException e) {throw new RuntimeException(e);}}@Overridepublic void onCreateFailure(String s) {}@Overridepublic void onSetFailure(String s) {}}, constraints);} catch (JSONException e) {throw new RuntimeException(e);}}@Overrideprotected void onDestroy() {if (socket != null && socket.isOpen()) {socket.close();}socket = null;if(peerConnection != null){peerConnection.close();peerConnection = null;}if(videoSource != null){videoSource.dispose();videoSource = null;}if (audioSource != null) {audioSource.dispose();audioSource = null;}if(videoCapturer != null){try {videoCapturer.stopCapture();} catch (InterruptedException e) {throw new RuntimeException(e);}videoCapturer.dispose();videoCapturer = null;}if(surfaceTextureHelper != null){surfaceTextureHelper.dispose();surfaceTextureHelper = null;}localRender.release();remoteRender.release();if(factory != null){factory.dispose();factory = null;}if(mRootEGL != null){mRootEGL.release();mRootEGL = null;}super.onDestroy();}
}
服务器端node js全部源码
//简单信令服务器 npm install nodejs-websocket
let ws = require("nodejs-websocket")
class Client {constructor(uid,conn,roomId) {this.uid = uid;this.conn = conn;this.conn.uid = this.uid;this.roomId = roomId;this.conn.roomId = roomId;}
}let roomMaps = new Mapfunction handleJoinRoom(receiveObj, conn) {//有人加入房间let uid = receiveObj.uid;let roomId = receiveObj.roomId;let room = roomMaps.get(roomId);let client = new Client(uid,conn,roomId)if(!room){//创建房间room = new Map}if(room.get(uid)){//已经在房间了console.log("已经在房间里了")return}room.set(uid,client)roomMaps.set(roomId,room);console.log("加入房间了");if(room.size > 1){let clients = Array.from(room.keys())clients.forEach(remoteUid => {if(remoteUid !== uid){//通知房间其他人有新人加入let sendObj = {cmd: "cmd_new_peer",uid: uid,remoteUid}let remoteClient = room.get(remoteUid)remoteClient.conn.sendText(JSON.stringify(sendObj))console.log("新人发送成功");}})}}function handleOffer(receiveObj) {//转发offerlet remoteUid = receiveObj.remoteUidlet roomId = receiveObj.roomIdlet room = roomMaps.get(roomId)let client = room.get(remoteUid)client.conn.sendText(JSON.stringify(receiveObj))}function handleAnswer(receiveObj) {//转发answer;let remoteUid = receiveObj.remoteUidlet roomId = receiveObj.roomIdlet room = roomMaps.get(roomId)let client = room.get(remoteUid)client.conn.sendText(JSON.stringify(receiveObj))
}function handleIce(receiveObj) {//转发icelet remoteUid = receiveObj.remoteUidlet roomId = receiveObj.roomIdlet room = roomMaps.get(roomId)let client = room.get(remoteUid)client.conn.sendText(JSON.stringify(receiveObj))
}function handleClose(conn) {let uid = conn.uidlet roomId = conn.roomIdlet room = roomMaps.get(roomId)room.delete(uid)
}ws.createServer(function (conn) {//有客服端链接conn.on("text",function (str) {//收到消息let receiveObj = JSON.parse(str);console.log(str)switch (receiveObj.cmd) {case "cmd_join_room"://有人加入handleJoinRoom(receiveObj,conn)breakcase "cmd_offer"://转发offerhandleOffer(receiveObj)breakcase "cmd_answer":handleAnswer(receiveObj);breakcase "cmd_ice":handleIce(receiveObj);break}})conn.on("close",function (code,reason) {console.log("链接关闭")handleClose(conn);});conn.on("error",function (err){console.log(err);})}).listen(8090)
本文福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓