WebRTC提供了点对点之间的通信,但并不意味着WebRTC不需要服务器。暂且不说基于服务器的一些扩展业务,WebRTC至少有两件事必须要用到服务器:
1. 浏览器之间交换建立通信的元数据(信令)必须通过服务器
2. 为了穿越NAT和防火墙
此处,我们使用XMPP协议实现信令,采用openfire当做服务器,通过openfire服务器+Smack API实现信令的传递。
因此,在建立PeerConnection实例之后,想要使用其建立一个点对点的信道,我们需要做两件事:
1. 确定本机上的媒体流的特性,比如分辨率、编解码能力啥的(SDP描述符)
2. 连接两端的主机的网络地址(ICE Candidate)
通过offer和answer交换SDP描述符
大致上在两个用户(甲和乙)之间建立点对点连接流程应该是这个样子(这里不考虑错误的情况,PeerConnection简称PC):
甲和乙各自建立一个PC实例
甲通过PC所提供的createOffer()方法建立一个包含甲的SDP描述符的offer信令
甲通过PC所提供的setLocalDescription()方法,将甲的SDP描述符交给甲的PC实例
甲将offer信令通过服务器发送给乙
乙将甲的offer信令中所包含的的SDP描述符提取出来,通过PC所提供的setRemoteDescription()方法交给乙的PC实例
乙通过PC所提供的createAnswer()方法建立一个包含乙的SDP描述符answer信令
乙通过PC所提供的setLocalDescription()方法,将乙的SDP描述符交给乙的PC实例
乙将answer信令通过服务器发送给甲
甲接收到乙的answer信令后,将其中乙的SDP描述符提取出来,调用setRemoteDescripttion()方法交给甲自己的PC实例
通过在这一系列的信令交换之后,甲和乙所创建的PC实例都包含甲和乙的SDP描述符了,完成了两件事的第一件。我们还需要完成第二件事——获取连接两端主机的网络地址
通过ICE框架建立NAT/防火墙穿越的连接
这个网络地址应该是能从外界直接访问,WebRTC使用ICE框架来获得这个地址。PeerConnection在创立的时候可以将ICE服务器的地址传递进去,如:
private List<PeerConnection.IceServer> getIceServers(String url,String user,String credential)
{
PeerConnection.IceServer turn = new PeerConnection.IceServer(
url,user,credential);
LinkedList<PeerConnection.IceServer> iceServers = new LinkedList<PeerConnection.IceServer>();
iceServers.add(turn);
return iceServers;
}
//iceServer List对象获取
List<PeerConnection.IceServer> iceServers = getIceServers(CoturnData.url,
CoturnData.userName,CoturnData.credential);
pcConstraints = new MediaConstraints();
pcConstraints.optional.add(new MediaConstraints.KeyValuePair(
"DtlsSrtpKeyAgreement", "true"));
pcConstraints.mandatory.add(new
MediaConstraints.KeyValuePair("VoiceActivityDetection", "false"));
pc = factory.createPeerConnection(iceServers,pcConstraints,pcObserver);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
当然这个地址也需要交换,还是以甲乙两位为例,交换的流程如下(PeerConnection简称PC):
甲、乙各创建配置了ICE服务器的PC实例,并为其添加onicecandidate事件回调
当网络候选可用时,将会调用onicecandidate函数
在回调函数内部,甲或乙将网络候选的消息封装在ICE Candidate信令中,通过服务器中转,传递给对方
甲或乙接收到对方通过服务器中转所发送过来ICE Candidate信令时,将其解析并获得网络候选,将其通过PC实例的addIceCandidate()方法加入到PC实例中
这样连接就创立完成了,可以向RTCPeerConnection中通过addStream()加入流来传输媒体流数据。将流加入到RTCPeerConnection实例中后,对方就可以通过onaddstream所绑定的回调函数监听到了。调用addStream()可以在连接完成之前,在连接建立之后,对方一样能监听到媒体流
代码实现:
public class VideoCallActivity extends ParentActivity{
public static final String VIDEO_TRACK_ID = "video_track_id";
public static final String AUDIO_TRACK_ID = "audio_track_id";
public static final String LOCAL_MEDIA_STREAM_ID = "local_media_stream_id";
private String mServiceName; //XMPP服务器名称
private GLSurfaceView mGLSurfaceView;
private GLSurfaceView mGLSurfaceViewRemote;
private PeerConnection pc;
private final PCObserver pcObserver = new PCObserver();
private final SDPObserver sdpObserver = new SDPObserver();
private MediaConstraints sdpMediaConstraints;
private MediaConstraints pcConstraints;
private String remoteName;
IceCandidate remoteIceCandidate;
private boolean mIsInited;
private boolean mIsCalled;
PeerConnectionFactory factory;
VideoCapturer videoCapturer;
VideoSource videoSource;
VideoRenderer localVideoRenderer;
VideoRenderer remoteVideoRenderer;
AudioManager audioManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_call);
//打开扬声器
audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
audioManager.setSpeakerphoneOn(true);
mServiceName = connection.getServiceName();
mGLSurfaceView = (GLSurfaceView) findViewById(R.id.glsurfaceview);
// mGLSurfaceViewRemote = (GLSurfaceView) findViewById(R.id.glsurfaceview_remote);
//检查初始化音视频设备是否成功
if (!PeerConnectionFactory.initializeAndroidGlobals(this,true,true,true,null))
{
Log.e("init","PeerConnectionFactory init fail!");
return;
}
// Intent intent = getIntent().getBundleExtra()
//Media条件信息SDP接口
sdpMediaConstraints = new MediaConstraints();
//接受远程音频
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveAudio", "true"));
//接受远程视频
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveVideo", "true"));
factory = new PeerConnectionFactory();
//iceServer List对象获取
List<PeerConnection.IceServer> iceServers = getIceServers(CoturnData.url,
CoturnData.userName,CoturnData.credential);
pcConstraints = new MediaConstraints();
pcConstraints.optional.add(new MediaConstraints.KeyValuePair(
"DtlsSrtpKeyAgreement", "true"));
pcConstraints.mandatory.add(new
MediaConstraints.KeyValuePair("VoiceActivityDetection", "false"));
pc = factory.createPeerConnection(iceServers,pcConstraints,pcObserver);
mIsInited = false;
mIsCalled=false;
boolean offer=getIntent().getBooleanExtra("createOffer",false);
//offer:如果offer为true表示主叫方初始化,如果为false表示被叫方初始化。
remoteName = getIntent().getStringExtra("remoteName");
if(!offer)
{
initialSystem();
}
else {
callRemote(remoteName);
}
//当VideoActivity已经打开时,处理后续的intent传过来的数据。
processExtraData();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
processExtraData();
}
@Override
protected void onDestroy() {
super.onDestroy();
//通话结束,发送通话结束消息
videoCallEnded();
//释放资源
videoCapturer.dispose();
videoSource.stop();
if (pc != null) {
pc.dispose();
pc = null;
}
audioManager.setSpeakerphoneOn(false);
}
private void videoCallEnded() {
String chatJid = remoteName+"@"+mServiceName;
Message message = new Message();
VideoInvitation videoInvitation = new VideoInvitation();
videoInvitation.setTypeText("video-ended");
message.addExtension(videoInvitation);
Chat chat = createChat(chatJid);
try {
chat.sendMessage(message);
} catch (SmackException.NotConnectedException e) {
e.printStackTrace();
}
}
private void processExtraData() {
Intent intent = getIntent();
//获取SDP数据
String sdpType = intent.getStringExtra("type");
String sdpDescription = intent.getStringExtra("description");
if (sdpType != null)
{
SessionDescription.Type type = SessionDescription.Type.fromCanonicalForm(sdpType);
SessionDescription sdp = new SessionDescription(type,sdpDescription);
if (pc == null)
{
Log.e("pc","pc == null");
}
pc.setRemoteDescription(sdpObserver,sdp);
//如果是offer,则被叫方createAnswer
if (sdpType.equals("offer"))
{
mIsCalled = true;
pc.createAnswer(sdpObserver,sdpMediaConstraints);
}
}
//获取ICE Candidate数据
String iceSdpMid = intent.getStringExtra("sdpMid");
int iceSdpMLineIndex = intent.getIntExtra("sdpMLineIndex",-1);
String iceSdp = intent.getStringExtra("sdp");
if (iceSdpMid != null)
{
IceCandidate iceCandidate = new IceCandidate(iceSdpMid,iceSdpMLineIndex,iceSdp);
if (remoteIceCandidate == null)
{
remoteIceCandidate = iceCandidate;
}
//下面这步放到函数drainRemoteCandidates()中
/*//添加远端的IceCandidate到pc
pc.addIceCandidate(iceCandidate);*/
}
//结束activity
boolean videoEnded = intent.getBooleanExtra("videoEnded",false);
if (videoEnded)
{
finish();
}
}
private void callRemote(String remoteName) {
initialSystem();
//createOffer
pc.createOffer(sdpObserver,sdpMediaConstraints);
}
private void initialSystem() {
if (mIsInited)
{
return;
}
//获取前置摄像头本地视频流
String frontDeviceName = VideoCapturerAndroid.getNameOfFrontFacingDevice();
// String frontDeviceName = "Camera 1, Facing front, Orientation 0";
Log.e("CameraName","CameraName: "+frontDeviceName);
videoCapturer = VideoCapturerAndroid.create(frontDeviceName);
if (videoCapturer == null)
{
Log.e("open","fail to open camera");
return;
}
//视频
MediaConstraints mediaConstraints = new MediaConstraints();
videoSource = factory.createVideoSource(videoCapturer, mediaConstraints);
VideoTrack localVideoTrack = factory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
//音频
MediaConstraints audioConstraints = new MediaConstraints();
AudioSource audioSource = factory.createAudioSource(audioConstraints);
AudioTrack localAudioTrack = factory.createAudioTrack(AUDIO_TRACK_ID,audioSource);
Runnable runnable = new Runnable() {
@Override
public void run() {
}
};
VideoRendererGui.setView(mGLSurfaceView,runnable);
try {
//改成ScalingType.SCALE_ASPECT_FILL可以显示双方视频,但是显示比例不美观,并且不知道最后一个参数true和false的含义。
localVideoRenderer = VideoRendererGui.createGui(0,0,30,30, VideoRendererGui.ScalingType.SCALE_ASPECT_FILL,true);
remoteVideoRenderer = VideoRendererGui.createGui(0,0,100,100, VideoRendererGui.ScalingType.SCALE_FILL,true);
localVideoTrack.addRenderer(localVideoRenderer);
} catch (Exception e) {
e.printStackTrace();
}
MediaStream localMediaStream = factory.createLocalMediaStream(LOCAL_MEDIA_STREAM_ID);
localMediaStream.addTrack(localAudioTrack);
localMediaStream.addTrack(localVideoTrack);
pc.addStream(localMediaStream);
}
private List<PeerConnection.IceServer> getIceServers(String url,String user,String credential)
{
PeerConnection.IceServer turn = new PeerConnection.IceServer(
url,user,credential);
LinkedList<PeerConnection.IceServer> iceServers = new LinkedList<PeerConnection.IceServer>();
iceServers.add(turn);
return iceServers;
}
private class PCObserver implements PeerConnection.Observer
{
@Override
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
}
@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
}
@Override
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
}
//发送ICE候选到其他客户端
@Override
public void onIceCandidate(final IceCandidate iceCandidate) {
//利用XMPP发送iceCandidate到其他客户端
runOnUiThread(new Runnable() {
@Override
public void run() {
String chatJid = remoteName+"@"+mServiceName;
Message message = new Message();
IceCandidateExtensionElement iceCandidateExtensionElement=
new IceCandidateExtensionElement();
iceCandidateExtensionElement.setSdpMidText(iceCandidate.sdpMid);
iceCandidateExtensionElement.setSdpMLineIndexText(iceCandidate.sdpMLineIndex);
iceCandidateExtensionElement.setSdpText(iceCandidate.sdp);
message.addExtension(iceCandidateExtensionElement);
Chat chat = createChat(chatJid);
try {
chat.sendMessage(message);
} catch (SmackException.NotConnectedException e) {
e.printStackTrace();
}
}
});
}
//Display a media stream from remote
@Override
public void onAddStream(final MediaStream mediaStream) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (pc == null)
{
Log.e("onAddStream","pc == null");
return;
}
if (mediaStream.videoTracks.size()>1 || mediaStream.audioTracks.size()>1)
{
Log.e("onAddStream","size > 1");
return;
}
if (mediaStream.videoTracks.size() == 1)
{
VideoTrack videoTrack = mediaStream.videoTracks.get(0);
videoTrack.addRenderer(remoteVideoRenderer);
}
}
});
}
@Override
public void onRemoveStream(final MediaStream mediaStream) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mediaStream.videoTracks.get(0).dispose();
}
});
}
@Override
public void onDataChannel(DataChannel dataChannel) {
}
@Override
public void onRenegotiationNeeded() {
}
}
private class SDPObserver implements SdpObserver
{
@Override
public void onCreateSuccess(final SessionDescription sessionDescription) {
//sendMessage(offer);
runOnUiThread(new Runnable() {
@Override
public void run() {
String chatJid = remoteName+"@"+mServiceName;
Message message = new Message();
SDPExtensionElement sdpExtensionElement = new SDPExtensionElement();
sdpExtensionElement.setTypeText(sessionDescription.type.canonicalForm());
sdpExtensionElement.setDescriptionText(sessionDescription.description);
message.addExtension(sdpExtensionElement);
Chat chat = createChat(chatJid);
try {
chat.sendMessage(message);
} catch (SmackException.NotConnectedException e) {
e.printStackTrace();
}
pc.setLocalDescription(sdpObserver,sessionDescription);
}
});
}
@Override
public void onSetSuccess() {
runOnUiThread(new Runnable() {
@Override
public void run() {
//主叫方
if (!mIsCalled)
{
if (pc.getRemoteDescription() != null)
{
drainRemoteCandidates();
}
}
//被叫方
else
{
//如果被叫方还没有createAnswer
if (pc.getLocalDescription() == null)
{
Log.e("SDPObserver", "SDPObserver create answer");
}
else
{
drainRemoteCandidates();
}
}
}
});
}
private void drainRemoteCandidates() {
if (remoteIceCandidate == null)
{
Log.e("SDPObserver","remoteIceCandidate == null");
return;
}
pc.addIceCandidate(remoteIceCandidate);
Log.e("IceCanditate","添加IceCandidate成功");
remoteIceCandidate = null;
}
@Override
public void onCreateFailure(String s) {
Log.e("SDPObserver","onCreateFailure");
}
@Override
public void onSetFailure(String s) {
Log.e("SDPObserver","onSetFailure");
}
}
}
————————————————
版权声明:本文为CSDN博主「程序员的自我救赎」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u011026329/article/details/50582690
- Android之WebRTC介绍
参考自:Introduction to WebRTC on AndroidAndroid之WebRTC介绍 WebRTC被誉为是web长期开源开发的一个新启元,是近年来web开发的最重要创新.WebR ...
- Android端WebRTC点对点互连
项目准备 信令服务器代码:https://github.com/matthewYang92/WebRtcServer(代码改自ProjectRTC) 安装Node.js 进入项目根目录,命令行:npm ...
- Android IOS WebRTC 音视频开发总结(四六)-- 从另一个角度看国内首届WebRTC大会
文章主要从开发者角度谈国内首届WebRTC大会,支持原创,文章来自博客园RTC.Blacker,支持原创,转载必须说明出处,更多详见www.rtc.help. -------------------- ...
- 转:Android IOS WebRTC 音视频开发总结 (系列文章集合)
随笔分类 - webrtc Android IOS WebRTC 音视频开发总结(七八)-- 为什么WebRTC端到端监控很关键? 摘要: 本文主要介绍WebRTC端到端监控(我们翻译和整理的,译 ...
- Android APP压力测试(二)之Monkey信息自动收集脚本
Android APP压力测试(二) 之Monkey信息自动收集脚本 前言: 上一篇Monkey介绍基本搬抄官方介绍,主要是为了自己查阅方便.本文重点介绍我在进行Monkey时如何自动收集相关信息 ...
- Android项目实战(二十八):Zxing二维码实现及优化
前言: 多年之前接触过zxing实现二维码,没想到今日项目中再此使用竟然使用的还是zxing,百度之,竟是如此牛的玩意. 当然,项目中我们也许只会用到二维码的扫描和生成两个功能,所以不必下载完整的ja ...
- Android IOS WebRTC 音视频开发总结(八十五)-- 使用WebRTC广播网络摄像头视频(下)
本文主要介绍WebRTC (我们翻译和整理的,译者:weizhenwei,校验:blacker),最早发表在[编风网] 支持原创,转载必须注明出处,欢迎关注我的微信公众号blacker(微信ID:bl ...
- Android IOS WebRTC 音视频开发总结(八十三)-- 使用WebRTC广播网络摄像头视频(上)
本文主要介绍WebRTC (我们翻译和整理的,译者:weizhenwei,校验:blacker),最早发表在[编风网] 支持原创,转载必须注明出处,欢迎关注我的微信公众号blacker(微信ID:bl ...
- Android多线程分析之二:Thread的实现
Android多线程分析之二:Thread的实现 罗朝辉 (http://www.cnblogs.com/kesalin/) CC 许可,转载请注明出处 在前文<Android多线程分析之一 ...
随机推荐
- 使用DocumentFormat.OpenXml操作Excel文件.xlsx
1.开始 DocumentFormat.OpenXml是ms官方给一个操作office三大件新版文件格式(.xlsx,.docx,.pptx)的组件:特色是它定义了OpenXml所包含的所有对象(たぶ ...
- ZooKeeper基础:快速部署
本文主要介绍ZooKeeper的快速部署安装,更多信息请参考ZooKeeper 概述 ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是 ...
- 泛微e-cology OA系统某接口存在数据库配置信息泄露漏洞复现
1.简介(开场废话) 攻击者可通过存在漏洞的页面直接获取到数据库配置信息.如果攻击者可直接访问数据库,则可直接获取用户数据,甚至可以直接控制数据库服务器. 2.影响范围 漏洞涉及范围包括不限于8.0. ...
- 图论 - 二分图的判断(dfs染色法)
二分图的判断(dfs染色法) 如何判断一个图是否为二分图 普通染色法模板 C++ 代码模板如下 思想:先将当前点染色,然后再将该点相连的结点进行染另外一种颜色 下面附上自己画的一张图假设我们从第一个点 ...
- Solr的集群搭建(索引库)
Solr的集群的搭建 Solr集群原理 SolrCloud概念以及结构 概念: SolrCloud(Solr云)是Solr提供的分布式搜索方案,当你需要大规模,容错,分布式索引和检索能力时使用Solr ...
- .net反射机制的简单介绍
1.什么是反射 Reflection,中文翻译为反射. 这是.Net中获取运行时类型信息的方式,.Net的应用程序由几个部分: ‘程序集(Assembly)’.‘模块(Module)’.‘类型(cl ...
- Spark常规性能调优
1.1.1 常规性能调优一:最优资源配置 Spark性能调优的第一步,就是为任务分配更多的资源,在一定范围内,增加资源的分配与性能的提升是成正比的,实现了最优的资源配置后,在此基础上再考虑进行 ...
- Arthas - 开源的java诊断工具,非常有用
常用命令 help 查看帮助 help COMMAND 查看指定命令的详细帮助 COMMAND -h 查看指定命令的详细帮助 double tab 查看支持的所有命令 dashboard 查看线程JV ...
- web前端如何性能优化提高加载速度
前端优化有以下几种途径: 一.减少HTTP请求数量和次数: 二.使用CDN: 三.添加Expires头: 四.压缩组件: 五.将样式表放在头部: 六.将脚本放在底部: 七.避免CSS表达式: 八.使用 ...
- RabbitMQ简单介绍+Windows环境安装
文章目录 1.RabbitMQ简介2.RabbitMQ与其他MQ有什么不同3.RabbitMQ环境安装3.1 安装erlang3.2 安装rabbitmq-server4. RabbitMQ管理平台介 ...