让我先深呼吸一下。其实从很早就开始看视频通话和直播相关的一些内容,但由于这些东西太深奥了,涉及到很多编解码的知识,让我们很多程序员都是可望而不可即。后来我看了一些关于WebRTC的资料,也了解了一些相关的用发,暂时感觉这方面的门槛稍微低了一些。所以我不自量力的搭建了一套视频通话的Demo,算是一个小小的入门吧 ,为今后如果更加深入的研究Android算是立一个Flag。好了,言归正传。今天我搭建的这套通话方案,是超超超级简化版的。为什么这么说,因为没有后台支撑,没有信令传递。说白了,就是单纯整P2P视频传输。有人就要说了,那我看你这个能得到什么。我觉得这是一个单纯学习的博客,我们还是要真正自己学习会了,从而结和自己应用的已有功能来实现相关的业务。比如,我们已经做了文字的聊天,要接入一个点对点视频通话,就没有必要再做对应的信令服务器了。
言归正传,开始WebRTC点对点视频直播方案的讲解。
再开始代码之前,我要先说一下关于信令的传递。第一个要传递的信令就是Candidate,这个是在我们创建PeerConnection的时候生成的。第二个要传递的信令是sdp,这个是在CreateOffer和CreateAnswer时生成的。这两个数据由于没有信令服务器,我们暂时使用生成后放入EditText,通过微信或其他通信工具发到另一方,另一方设置给自己的软件方式,实现这个功能。至于生产环境中要用的时候,我会在后面说明需要什么改造。
这里不一步一步说了,直接上代码。
1.导入依赖
implementation 'org.webrtc:google-webrtc:1.0.+'
implementation 'com.google.code.gson:gson:2.6.2'
这里第一个依赖是WebRTC的依赖,是必须的。第二个是Gson的依赖,这个不是必须的,主要是方便我这里数据的传递。
2.最低支持版本修改
minSdkVersion 21
3.权限设置
清单文件中加入如下权限
<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" />
<uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.NFC"/>
<!-- 局域网通信临时用-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.READ_LOGS"
tools:ignore="ProtectedPermissions" />
别忘了在Manifest节点下加入
xmlns:tools="http://schemas.android.com/tools"
4.接着我们搭建一下布局文件
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
android:background="@android:color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/white">
<org.webrtc.SurfaceViewRenderer
android:id="@+id/LocalSurfaceView"
android:layout_width="match_parent"
android:layout_height="400dp"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/white" />
<org.webrtc.SurfaceViewRenderer
android:id="@+id/RemoteSurfaceView"
android:layout_width="match_parent"
android:layout_height="400dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<EditText
android:id="@+id/et_offer"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<Button
android:id="@+id/bt_offer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置offer" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<EditText
android:id="@+id/et_answer"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<Button
android:id="@+id/bt_answer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置answer" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<EditText
android:id="@+id/tv_candicate1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<Button
android:id="@+id/bt_candidate1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置candidate" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<EditText
android:id="@+id/tv_candicate2"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<Button
android:id="@+id/bt_candidate2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置candidate" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<EditText
android:id="@+id/tv_candicate3"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<Button
android:id="@+id/bt_candidate3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置candidate" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<EditText
android:id="@+id/tv_candicate4"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<Button
android:id="@+id/bt_candidate4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置candidate" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<EditText
android:id="@+id/tv_candicate5"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<Button
android:id="@+id/bt_candidate5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置candidate" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<EditText
android:id="@+id/tv_candicate6"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<Button
android:id="@+id/bt_candidate6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置candidate" />
</LinearLayout>
</LinearLayout>
</ScrollView>
这里有很多输入框EditText和Button,大家可以不要理会,是我设置Candidate和sdp用的。真实项目中不需要。大家主要注意一下org.webrtc.SurfaceViewRenderer控件。这里我写了两个。这个控件就是完了要承载我们本地视频和远端视频的帧布局控件了。视频布局具体什么样子,只要改变这两个控件大小就好。
5.MainActivity即通话页面的全局变量
private EglBase mRootEglBase;
private SurfaceViewRenderer mLocalSurfaceView;
private SurfaceViewRenderer mRemoteSurfaceView;
private PeerConnectionFactory mPeerConnectionFactory;
private VideoCapturer mVideoCapturer;
private SurfaceTextureHelper mSurfaceTextureHelper;
private VideoTrack mVideoTrack;
private AudioTrack mAudioTrack;
public static final String VIDEO_TRACK_ID = "1";//"ARDAMSv0";
public static final String AUDIO_TRACK_ID = "2";//"ARDAMSa0";
private static final int VIDEO_RESOLUTION_WIDTH = 1280;
private static final int VIDEO_RESOLUTION_HEIGHT = 720;
private static final int VIDEO_FPS = 30;
private PeerConnection mPeerConnection;
private AudioManager audioManager;
private static String TAG = "aaaaaaaaaaaaaaaaaaaa";
private EditText et_offer;
private Button bt_offer;
private EditText et_answer;
private Button bt_answer;
private TextView tv_candicate1;
private TextView tv_candicate2;
private TextView tv_candicate3;
private TextView tv_candicate4;
private TextView tv_candicate5;
private TextView tv_candicate6;
private int count = 0;
private Button bt_candidate1;
private Button bt_candidate2;
private Button bt_candidate3;
private Button bt_candidate4;
private Button bt_candidate5;
private Button bt_candidate6;
空行上方是必备的WebRTC搭建相关的属性,空行下方是该Demo使用的一些属性,和视频通话没有关系。
6.接着我们要在该页面用自己的方法申请动态权限,这里我提供用郭霖大神的框架请求动态权限的一个方案,大家可以用自己项目里自带的 。
添加框架依赖
implementation 'com.permissionx.guolindev:permission-support:1.4.0'
在OnCreate中调用
PermissionX.init(this)
.permissions(Manifest.permission.READ_PHONE_STATE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
.onExplainRequestReason(new ExplainReasonCallbackWithBeforeParam() {
@Override
public void onExplainReason(ExplainScope scope, List<String> deniedList, boolean beforeRequest) {
scope.showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白");
}
})
.onForwardToSettings(new ForwardToSettingsCallback() {
@Override
public void onForwardToSettings(ForwardScope scope, List<String> deniedList) {
scope.showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白");
}
})
.request(new RequestCallback() {
@Override
public void onResult(boolean allGranted, List<String> grantedList, List<String> deniedList) {
if (allGranted) {
// TODO: 2020/10/14 权限申请成功
} else {
Toast.makeText(MainActivity.this, "您拒绝了如下权限:" + deniedList, Toast.LENGTH_SHORT).show();
}
}
});
TODO的地方是一会儿我们要进行下一步的代码编写的地方。
这里主要申请了五个动态权限。
7.在Activity中添加如下代码
/**
* 开始webRtc的初始化
*/
private void init() {
//初始化视频必要对象
mRootEglBase = EglBase.create();
//两个视频画面
mLocalSurfaceView = findViewById(R.id.LocalSurfaceView);
mRemoteSurfaceView = findViewById(R.id.RemoteSurfaceView);
et_offer = findViewById(R.id.et_offer);
bt_offer = findViewById(R.id.bt_offer);
et_answer = findViewById(R.id.et_answer);
bt_answer = findViewById(R.id.bt_answer);
tv_candicate1 = findViewById(R.id.tv_candicate1);
tv_candicate2 = findViewById(R.id.tv_candicate2);
tv_candicate3 = findViewById(R.id.tv_candicate3);
tv_candicate4 = findViewById(R.id.tv_candicate4);
tv_candicate5 = findViewById(R.id.tv_candicate5);
tv_candicate6 = findViewById(R.id.tv_candicate6);
bt_candidate1 = findViewById(R.id.bt_candidate1);
bt_candidate2 = findViewById(R.id.bt_candidate2);
bt_candidate3 = findViewById(R.id.bt_candidate3);
bt_candidate4 = findViewById(R.id.bt_candidate4);
bt_candidate5 = findViewById(R.id.bt_candidate5);
bt_candidate6 = findViewById(R.id.bt_candidate6);
//初始化本地视频界面
mLocalSurfaceView.init(mRootEglBase.getEglBaseContext(), null);
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(false /* enabled */);
mRemoteSurfaceView.setZOrderMediaOverlay(true);
//创建PeerConnectionFactory对象
mPeerConnectionFactory = createPeerConnectionFactory(this);
//日志输出设置
Logging.enableLogToDebugOutput(Logging.Severity.LS_VERBOSE);
//VideoCapturer对象初始化
mVideoCapturer = createVideoCapturer();
//相关视频源初始化,必备
mSurfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", mRootEglBase.getEglBaseContext());
VideoSource videoSource = mPeerConnectionFactory.createVideoSource(false);
mVideoCapturer.initialize(mSurfaceTextureHelper, getApplicationContext(), videoSource.getCapturerObserver());
//视频轨初始化,并设置到本地画布上
mVideoTrack = mPeerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
mVideoTrack.setEnabled(true);
mVideoTrack.addSink(mLocalSurfaceView);
//音频轨初始化
AudioSource audioSource = mPeerConnectionFactory.createAudioSource(new MediaConstraints());
mAudioTrack = mPeerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
mAudioTrack.setEnabled(true);
//初始化音频控制器
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
setSpeakerphoneOn(true);
bt_offer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
//接收到对放信令后的一个设置
try {
String description = et_offer.getText().toString();
et_offer.setText("");
mPeerConnection.setRemoteDescription(
new SimpleSdpObserver() {
@Override
public void onSetSuccess() {
Log.i(TAG, "set remote Description ok");
doAnswerCall();
}
@Override
public void onSetFailure(String msg) {
Log.i(TAG, "set remote Description unok");
}
},
new SessionDescription(
SessionDescription.Type.OFFER,
description));
} catch (Exception e) {
e.printStackTrace();
}
}
});
bt_answer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String answer = et_answer.getText().toString();
et_answer.setText("");
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
mPeerConnection.setRemoteDescription(
new SimpleSdpObserver() {
@Override
public void onSetSuccess() {
}
@Override
public void onSetFailure(String msg) {
}
},
new SessionDescription(
SessionDescription.Type.ANSWER,
answer));
}
});
bt_candidate1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
String json = tv_candicate1.getText().toString().trim();
tv_candicate1.setText("");
CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
IceCandidate remoteIceCandidate =
new IceCandidate(candidateBean.getId(),
candidateBean.getLabel(),
candidateBean.getCandidate());
mPeerConnection.addIceCandidate(remoteIceCandidate);
}
});
bt_candidate2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
String json = tv_candicate2.getText().toString().trim();
tv_candicate2.setText("");
CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
IceCandidate remoteIceCandidate =
new IceCandidate(candidateBean.getId(),
candidateBean.getLabel(),
candidateBean.getCandidate());
mPeerConnection.addIceCandidate(remoteIceCandidate);
}
});
bt_candidate3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
String json = tv_candicate3.getText().toString().trim();
tv_candicate3.setText("");
CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
IceCandidate remoteIceCandidate =
new IceCandidate(candidateBean.getId(),
candidateBean.getLabel(),
candidateBean.getCandidate());
mPeerConnection.addIceCandidate(remoteIceCandidate);
}
});
bt_candidate4.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
String json = tv_candicate4.getText().toString().trim();
tv_candicate4.setText("");
CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
IceCandidate remoteIceCandidate =
new IceCandidate(candidateBean.getId(),
candidateBean.getLabel(),
candidateBean.getCandidate());
mPeerConnection.addIceCandidate(remoteIceCandidate);
}
});
bt_candidate5.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
String json = tv_candicate5.getText().toString().trim();
tv_candicate5.setText("");
CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
IceCandidate remoteIceCandidate =
new IceCandidate(candidateBean.getId(),
candidateBean.getLabel(),
candidateBean.getCandidate());
mPeerConnection.addIceCandidate(remoteIceCandidate);
}
});
bt_candidate6.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
String json = tv_candicate6.getText().toString().trim();
tv_candicate6.setText("");
CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
IceCandidate remoteIceCandidate =
new IceCandidate(candidateBean.getId(),
candidateBean.getLabel(),
candidateBean.getCandidate());
mPeerConnection.addIceCandidate(remoteIceCandidate);
}
});
mVideoCapturer.startCapture(VIDEO_RESOLUTION_WIDTH, VIDEO_RESOLUTION_HEIGHT, VIDEO_FPS);
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
// doStartCall();
}
/**
* 回复
*/
public void doAnswerCall() {
if (mPeerConnection == null) {
mPeerConnection = createPeerConnection();
}
MediaConstraints sdpMediaConstraints = new MediaConstraints();
mPeerConnection.createAnswer(new SimpleSdpObserver() {
@Override
public void onCreateSuccess(final SessionDescription sessionDescription) {
mPeerConnection.setLocalDescription(new SimpleSdpObserver() {
@Override
public void onSetFailure(String msg) {
}
@Override
public void onSetSuccess() {
runOnUiThread(new Runnable() {
@Override
public void run() {
et_answer.setText(sessionDescription.description);
}
});
}
},
sessionDescription);
}
@Override
public void onCreateFailure(String msg) {
}
}, sdpMediaConstraints);
}
/**
* 创建PeerConnectionFactory对象
*
* @param context
* @return
*/
public PeerConnectionFactory createPeerConnectionFactory(Context context) {
final VideoEncoderFactory encoderFactory;
final VideoDecoderFactory decoderFactory;
encoderFactory = new DefaultVideoEncoderFactory(
mRootEglBase.getEglBaseContext(),
false /* enableIntelVp8Encoder */,
true);
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();
}
/**
* 创建VideoCapturer对象
*
* @return
*/
private VideoCapturer createVideoCapturer() {
if (Camera2Enumerator.isSupported(this)) {
return createCameraCapturer(new Camera2Enumerator(this));
} else {
return createCameraCapturer(new Camera1Enumerator(true));
}
}
/**
* 根据不同摄像头初始化VideoCapturer对象
*
* @param enumerator
* @return
*/
private VideoCapturer createCameraCapturer(CameraEnumerator enumerator) {
final String[] deviceNames = enumerator.getDeviceNames();
for (String deviceName : deviceNames) {
if (enumerator.isFrontFacing(deviceName)) {
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
if (videoCapturer != null) {
return videoCapturer;
}
}
}
for (String deviceName : deviceNames) {
if (!enumerator.isFrontFacing(deviceName)) {
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
if (videoCapturer != null) {
return videoCapturer;
}
}
}
return null;
}
@Override
protected void onDestroy() {
super.onDestroy();
// TODO: 2021/1/6 缺一个挂电话的动作
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);
// //开始创建PeerConnection
// //默认开启本地视频后就创建这个PeerConnection
// if (mPeerConnection == null) {
// mPeerConnection = createPeerConnection();
// }
// doStartCall();
}
/**
* 创建PeerConnection
*
* @return
*/
public PeerConnection createPeerConnection() {
LinkedList<PeerConnection.IceServer> iceServers = new LinkedList<PeerConnection.IceServer>();
// PeerConnection.IceServer ice_server =
// PeerConnection.IceServer.builder("turn:xxxx:3478")
// .setPassword("xxx")
// .setUsername("xxx")
// .createIceServer();
PeerConnection.IceServer ice_server = PeerConnection
.IceServer
.builder("stun:stun.l.google.com:19302")
.createIceServer();
iceServers.add(ice_server);
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
//TCP候选策略控制开关
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
//只使用TCP中转模式,不使用p2p直连
//rtcConfig.iceTransportsType = PeerConnection.IceTransportsType.RELAY;
//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) {
return null;
}
List<String> mediaStreamLabels = Collections.singletonList("ARDAMS");
connection.addTrack(mVideoTrack, mediaStreamLabels);
connection.addTrack(mAudioTrack, mediaStreamLabels);
connection.setAudioPlayout(true);
return connection;
}
/**
* 创建PeerConnection的回调
*/
private PeerConnection.Observer mPeerConnectionObserver = new PeerConnection.Observer() {
@Override
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
}
@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
}
@Override
public void onIceConnectionReceivingChange(boolean b) {
}
@Override
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
}
/**
* 获取到当前设备的Candidate
* @param iceCandidate
*/
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
JSONObject message = new JSONObject();
try {
message.put("type", "candidate");
message.put("label", iceCandidate.sdpMLineIndex);
message.put("id", iceCandidate.sdpMid);
message.put("candidate", iceCandidate.sdp);
Log.i(TAG, "candidate:" + message.toString());
runOnUiThread(new Runnable() {
@Override
public void run() {
count++;
if (count == 1) {
tv_candicate1.setText(message.toString());
} else if (count == 2) {
tv_candicate2.setText(message.toString());
} else if (count == 3) {
tv_candicate3.setText(message.toString());
} else if (count == 4) {
tv_candicate4.setText(message.toString());
} else if (count == 5) {
tv_candicate5.setText(message.toString());
} else if (count == 6) {
tv_candicate6.setText(message.toString());
}
}
});
} catch (JSONException e) {
e.printStackTrace();
}
}
@Override
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
if (iceCandidates != null) {
for (int i = 0; i < iceCandidates.length; i++) {
}
mPeerConnection.removeIceCandidates(iceCandidates);
}
}
@Override
public void onAddStream(MediaStream mediaStream) {
// VideoTrack remoteVideoTrack = mediaStream.videoTracks.get(0);
// remoteVideoTrack.setEnabled(true);
// remoteVideoTrack.addSink(mRemoteSurfaceView);
// VideoTrack videoTrack = mediaStream.videoTracks.get(0);
// videoTrack.setEnabled(true);
// videoTrack.addSink(mRemoteSurfaceView);
}
@Override
public void onRemoveStream(MediaStream mediaStream) {
}
@Override
public void onDataChannel(DataChannel dataChannel) {
}
@Override
public void onRenegotiationNeeded() {
}
/**
* 接收到对方的视频
* @param rtpReceiver
* @param mediaStreams
*/
@Override
public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
MediaStreamTrack track = rtpReceiver.track();
if (track instanceof VideoTrack) {
Log.i(TAG, "有视频");
VideoTrack remoteVideoTrack = (VideoTrack) track;
remoteVideoTrack.setEnabled(true);
remoteVideoTrack.addSink(mRemoteSurfaceView);
}
}
};
@Override
protected void onPause() {
super.onPause();
// try {
// mVideoCapturer.stopCapture();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
/**
* 开始呼叫
*/
public void doStartCall() {
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"));
mediaConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
mPeerConnection.createOffer(new SimpleSdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
mPeerConnection.setLocalDescription(new SimpleSdpObserver() {
@Override
public void onSetSuccess() {
runOnUiThread(new Runnable() {
@Override
public void run() {
et_offer.setText(sessionDescription.description);
}
});
}
@Override
public void onSetFailure(String msg) {
}
}, sessionDescription);
}
}, mediaConstraints);
}
public static class SimpleSdpObserver implements SdpObserver {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
}
@Override
public void onSetSuccess() {
}
@Override
public void onCreateFailure(String msg) {
}
@Override
public void onSetFailure(String msg) {
}
}
private void setSpeakerphoneOn(boolean on) {
if (on) {
audioManager.setSpeakerphoneOn(true);
} else {
audioManager.setSpeakerphoneOn(false);//关闭扬声器
//把声音设定成Earpiece(听筒)出来,设定为正在通话中
audioManager.setMode(AudioManager.MODE_IN_CALL);
}
}
8.大家可能发现少一个CandidateBean,我们加入,代码如下
public class CandidateBean {
private String type;
private int label;
private String id;
private String candidate;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public int getLabel() {
return label;
}
public void setLabel(int label) {
this.label = label;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCandidate() {
return candidate;
}
public void setCandidate(String candidate) {
this.candidate = candidate;
}
}
9.在app的build.gradle中android节点下添加java 8依赖
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
10.到这里就没有报错了,我们最后在动态权限全部申请完成的地方,调用init()方法,我们这里是第6步中TODO的位置
11.到这里我们的代码就写完了。我们运行的时候,一个调用doStartCall,一个不调用。调用的为呼叫方。如果用了我的Demo,你也没有做信令服务器,那么要注意一下两点,第一,主叫一打开生成Candidate和sdp,发给被叫方后,先设置candidate再设置sdp;生成被叫方candidate和sdp后,发给主叫方,先设置sdp再设置candidate。第二,我们sdp通过任何聊天工具发到另一方后要注意,最后都是回车结尾的,没有的话自己回车,否则会报错。
到此我们的WebRTC点对点视频就搭建完成了。这里我们使用了谷歌提供的stun服务器。如果想在生产环境中应用,一个是要修改我们的信令传递方式,用聊天的方式通过长连接传递。一个是我们要有一个中继服务器,配置给我们的IceService。
小伙伴们有什么心得和问题都可以给我评论区留言。