基于Kurento的WebRTC移动视频群聊解决方案

说在前面的话:视频实时群聊天有三种架构:

       Mesh架构:终端之间互相连接,没有中心服务器,产生的问题,每个终端都要连接n-1个终端,每个终端的编码和网络压力都很大。群聊人数N不可能太大。

       Router架构:终端之间引入中心服务器,学名MCU(Multi Point Control Unit),每个终端的视频流都发布到MCU服务器上,然后服务器负责编码发布多视频流的工作,减轻客户端的压力。

       Mix架构:在Router架构基础上,多个视频流在服务器端被合为一个视频流,减轻网络压力。

       下面讲我们的选择,在MCU方面有licode、kurento等解决方案。kurento在视频群聊领域有专门的kurento Room解决方案,官方还提供一个kurento room server的样例实现。

       首先可以考虑不是一个Kurento Room Demo作为搭建方案原型的MCU组件。

       Room Demo的部署可见:http://doc-kurento-room.readthedocs.io/en/stable/demo_deployment.html

      其中碰到一些Maven编译问题:

[plain]  view plain  copy
 print ?
  1. Unable to initialise extensions Component descriptor role: 'com.jcraft.jsch.UIKeyboardInteractive', implementation: 'org.apache.maven.wagon.providers.ssh.jsch.interactive.PrompterUIKeyboardInteractive', role hint: 'default' has a hint, but there are other implementations that don't  

      Maven的安装版本需要时3.0以上

      还有碰到找不到bower命令行问题。bower是Node.js下面的一个包管理工具,安装node.js以后用npm安装即可

      最后按照部署指南网页中的命令启动服务器即可。

      Demo服务器有两部分,一部分是Demo Web服务器,二是把官方的kurento room server也集成到了这个demo中。不用再架设独立的kurento room server

      说说Android段的实施:再说一个公司:http://www.nubomedia.eu/,这家公司提供实时媒体通信开源云服务,核心组件可能是kurento media server,它的官网和kurento官网用一个模板,about里面显示两家组织有联系,kurento官方提供的Java Client因为底层API原因在Android上不肯用,这个nubomedia组织提供了一个kurento android client的实现,同时还提供了一个kurento room client的实现以及room使用案例:https://github.com/nubomedia-vtt/nubo-test,这家公司对其开发的开源方案管理非常及时,早晨提个接口的issue,下午已经commit了代码修改。

       这个案例虽然支持room沟通,但视频沟通是基于room发布订阅机制做的双人聊天。略改一下代码应该就可以实现多人聊天不过这家组织提供的两个client实现和官方的接口高度相似。主要改的是PeerVideoActivity这个类,下面我share一个基本走通多端通信的这个类的代码,供大家参考:

[java]  view plain  copy
 print ?
  1. package fi.vtt.nubotest;  
  2.   
  3. import android.app.ListActivity;  
  4. import android.content.SharedPreferences;  
  5. import android.graphics.PixelFormat;  
  6. import android.opengl.GLSurfaceView;  
  7. import android.os.Bundle;  
  8. import android.os.Handler;  
  9. import android.util.Log;  
  10. import android.view.Menu;  
  11. import android.view.MenuItem;  
  12. import android.view.View;  
  13. import android.view.WindowManager;  
  14. import android.widget.TextView;  
  15. import android.widget.Toast;  
  16.   
  17. import org.webrtc.IceCandidate;  
  18. import org.webrtc.MediaStream;  
  19. import org.webrtc.PeerConnection;  
  20. import org.webrtc.RendererCommon;  
  21. import org.webrtc.SessionDescription;  
  22. import org.webrtc.VideoRenderer;  
  23. import org.webrtc.VideoRendererGui;  
  24.   
  25. import java.util.Map;  
  26.   
  27. import fi.vtt.nubomedia.kurentoroomclientandroid.RoomError;  
  28. import fi.vtt.nubomedia.kurentoroomclientandroid.RoomListener;  
  29. import fi.vtt.nubomedia.kurentoroomclientandroid.RoomNotification;  
  30. import fi.vtt.nubomedia.kurentoroomclientandroid.RoomResponse;  
  31. import fi.vtt.nubomedia.webrtcpeerandroid.NBMMediaConfiguration;  
  32. import fi.vtt.nubomedia.webrtcpeerandroid.NBMPeerConnection;  
  33. import fi.vtt.nubomedia.webrtcpeerandroid.NBMWebRTCPeer;  
  34.   
  35. import fi.vtt.nubotest.util.Constants;  
  36.   
  37. /** 
  38.  * Activity for receiving the video stream of a peer 
  39.  * (based on PeerVideoActivity of Pubnub's video chat tutorial example. 
  40.  */  
  41. public class PeerVideoActivity extends ListActivity implements NBMWebRTCPeer.Observer, RoomListener {  
  42.     private static final String TAG = "PeerVideoActivity";  
  43.   
  44.     private NBMMediaConfiguration peerConnectionParameters;  
  45.     private NBMWebRTCPeer nbmWebRTCPeer;  
  46.   
  47.     private SessionDescription localSdp;  
  48.     private SessionDescription remoteSdp;  
  49.     private String PaticipantID;  
  50.   
  51.     private VideoRenderer.Callbacks localRender;  
  52.     private VideoRenderer.Callbacks remoteRender;  
  53.     private GLSurfaceView videoView;  
  54.   
  55.     private SharedPreferences mSharedPreferences;  
  56.   
  57.     private int publishVideoRequestId;  
  58.     private int sendIceCandidateRequestId;  
  59.   
  60.     private TextView mCallStatus;  
  61.   
  62.     private String  username, calluser;  
  63.     private boolean backPressed = false;  
  64.     private Thread  backPressedThread = null;  
  65.   
  66.     private static final int LOCAL_X_CONNECTED = 72;  
  67.     private static final int LOCAL_Y_CONNECTED = 72;  
  68.     private static final int LOCAL_WIDTH_CONNECTED = 25;  
  69.     private static final int LOCAL_HEIGHT_CONNECTED = 25;  
  70.     // Remote video screen position  
  71.     private static final int REMOTE_X = 0;  
  72.     private static  int REMOTE_Y = 0;  
  73.     private static final int REMOTE_WIDTH = 25;  
  74.     private static final int REMOTE_HEIGHT = 25;  
  75.   
  76.     private Handler mHandler;  
  77.     private CallState callState;  
  78.   
  79.     private enum CallState{  
  80.         IDLE, PUBLISHING, PUBLISHED, WAITING_REMOTE_USER, RECEIVING_REMOTE_USER,PATICIPANT_JOINED,RECEIVING_PATICIPANT,  
  81.     }  
  82.   
  83.     @Override  
  84.     public void onCreate(Bundle savedInstanceState) {  
  85.         super.onCreate(savedInstanceState);  
  86.         callState = CallState.IDLE;  
  87.   
  88.         setContentView(R.layout.activity_video_chat);  
  89.         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);  
  90.         mHandler = new Handler();  
  91.         Bundle extras = getIntent().getExtras();  
  92.         if (extras == null || !extras.containsKey(Constants.USER_NAME)) {  
  93.             ;  
  94.             Toast.makeText(this"Need to pass username to PeerVideoActivity in intent extras (Constants.USER_NAME).",  
  95.                     Toast.LENGTH_SHORT).show();  
  96.             finish();  
  97.             return;  
  98.         }  
  99.         this.username      = extras.getString(Constants.USER_NAME, "");  
  100.         Log.i(TAG, "username: " + username);  
  101.   
  102.         if (extras.containsKey(Constants.CALL_USER)) {  
  103.             this.calluser      = extras.getString(Constants.CALL_USER, "");  
  104.             Log.i(TAG, "callUser: " + calluser);  
  105.         }  
  106.   
  107.         this.mCallStatus   = (TextView) findViewById(R.id.call_status);  
  108.         TextView prompt   = (TextView) findViewById(R.id.receive_prompt);  
  109.         prompt.setText("Receive from " + calluser);  
  110.   
  111.         this.videoView = (GLSurfaceView) findViewById(R.id.gl_surface);  
  112.         // Set up the List View for chatting  
  113.         RendererCommon.ScalingType scalingType = RendererCommon.ScalingType.SCALE_ASPECT_FILL;  
  114.         VideoRendererGui.setView(videoView, null);  
  115.   
  116.   
  117.         localRender = VideoRendererGui.create(  LOCAL_X_CONNECTED, LOCAL_Y_CONNECTED,  
  118.                 LOCAL_WIDTH_CONNECTED, LOCAL_HEIGHT_CONNECTED,  
  119.                 scalingType, true);  
  120.         NBMMediaConfiguration.NBMVideoFormat receiverVideoFormat = new NBMMediaConfiguration.NBMVideoFormat(352288, PixelFormat.RGB_888, 20);  
  121.         peerConnectionParameters = new NBMMediaConfiguration(   NBMMediaConfiguration.NBMRendererType.OPENGLES,  
  122.                 NBMMediaConfiguration.NBMAudioCodec.OPUS, 0,  
  123.                 NBMMediaConfiguration.NBMVideoCodec.VP8, 0,  
  124.                 receiverVideoFormat,  
  125.                 NBMMediaConfiguration.NBMCameraPosition.FRONT);  
  126.         nbmWebRTCPeer = new NBMWebRTCPeer(peerConnectionParameters, this, localRender, this);  
  127.         nbmWebRTCPeer.initialize();  
  128.         Log.i(TAG, "PeerVideoActivity initialized");  
  129.         mHandler.postDelayed(publishDelayed, 4000);  
  130.   
  131.         MainActivity.getKurentoRoomAPIInstance().addObserver(this);  
  132.   
  133.   
  134.   
  135.         callState = CallState.PUBLISHING;  
  136.         mCallStatus.setText("Publishing...");  
  137.   
  138.     }  
  139.   
  140.     private Runnable publishDelayed = new Runnable() {  
  141.         @Override  
  142.         public void run() {  
  143.             nbmWebRTCPeer.generateOffer("derp"true);  
  144.         }  
  145.     };  
  146.   
  147.     @Override  
  148.     public boolean onCreateOptionsMenu(Menu menu) {  
  149.         // Inflate the menu; this adds items to the action bar if it is present.  
  150.         getMenuInflater().inflate(R.menu.menu_video_chat, menu);  
  151.         return true;  
  152.     }  
  153.   
  154.     @Override  
  155.     public boolean onOptionsItemSelected(MenuItem item) {  
  156.         // Handle action bar item clicks here. The action bar will  
  157.         // automatically handle clicks on the Home/Up button, so long  
  158.         // as you specify a parent activity in AndroidManifest.xml.  
  159.         int id = item.getItemId();  
  160.   
  161.         //noinspection SimplifiableIfStatement  
  162.         if (id == R.id.action_settings) {  
  163.             return true;  
  164.         }  
  165.   
  166.         return super.onOptionsItemSelected(item);  
  167.     }  
  168.   
  169.     @Override  
  170.     protected void onStart() {  
  171.         super.onStart();  
  172.   
  173.     }  
  174.   
  175.     @Override  
  176.     protected void onPause() {  
  177.         nbmWebRTCPeer.stopLocalMedia();  
  178.         super.onPause();  
  179.     }  
  180.   
  181.     @Override  
  182.     protected void onResume() {  
  183.         super.onResume();  
  184.         nbmWebRTCPeer.startLocalMedia();  
  185.     }  
  186.   
  187.     @Override  
  188.     protected void onStop() {  
  189.         endCall();  
  190.         super.onStop();  
  191.     }  
  192.   
  193.     @Override  
  194.     protected void onDestroy() {  
  195.         super.onDestroy();  
  196.     }  
  197.   
  198.     @Override  
  199.     public void onBackPressed() {  
  200.         // If back button has not been pressed in a while then trigger thread and toast notification  
  201.         if (!this.backPressed){  
  202.             this.backPressed = true;  
  203.             Toast.makeText(this,"Press back again to end.",Toast.LENGTH_SHORT).show();  
  204.             this.backPressedThread = new Thread(new Runnable() {  
  205.                 @Override  
  206.                 public void run() {  
  207.                     try {  
  208.                         Thread.sleep(5000);  
  209.                         backPressed = false;  
  210.                     } catch (InterruptedException e){ Log.d("VCA-oBP","Successfully interrupted"); }  
  211.                 }  
  212.             });  
  213.             this.backPressedThread.start();  
  214.         }  
  215.         // If button pressed the second time then call super back pressed  
  216.         // (eventually calls onDestroy)  
  217.         else {  
  218.             if (this.backPressedThread != null)  
  219.                 this.backPressedThread.interrupt();  
  220.             super.onBackPressed();  
  221.         }  
  222.     }  
  223.   
  224.     public void hangup(View view) {  
  225.         finish();  
  226.     }  
  227.   
  228.     public void receiveFromRemote(View view){  
  229.         Log.e(TAG,"--->receiveFromRemote");  
  230.         if (callState == CallState.PUBLISHED){  
  231.             callState = CallState.WAITING_REMOTE_USER;  
  232.             nbmWebRTCPeer.generateOffer("remote"false);  
  233.             runOnUiThread(new Runnable() {  
  234.                 @Override  
  235.                 public void run() {  
  236.                     mCallStatus.setText("Waiting remote stream...");  
  237.                 }  
  238.             });  
  239.         }  
  240.     }  
  241.   
  242.     /** 
  243.      * Terminates the current call and ends activity 
  244.      */  
  245.     private void endCall() {  
  246.         callState = CallState.IDLE;  
  247.         try  
  248.         {  
  249.             if (nbmWebRTCPeer != null) {  
  250.                 nbmWebRTCPeer.close();  
  251.                 nbmWebRTCPeer = null;  
  252.             }  
  253.         }  
  254.         catch (Exception e){e.printStackTrace();}  
  255.     }  
  256.   
  257.     @Override  
  258.     public void onLocalSdpOfferGenerated(final SessionDescription sessionDescription, NBMPeerConnection nbmPeerConnection) {  
  259.         Log.e(TAG,"--->onLocalSdpOfferGenerated");  
  260.         if (callState == CallState.PUBLISHING || callState == CallState.PUBLISHED) {  
  261.             localSdp = sessionDescription;  
  262.             Log.e(TAG,"--->onLocalSdpOfferGenerated:publish");  
  263.             runOnUiThread(new Runnable() {  
  264.                 @Override  
  265.                 public void run() {  
  266.                     if (MainActivity.getKurentoRoomAPIInstance() != null) {  
  267.                         Log.d(TAG, "Sending " + sessionDescription.type);  
  268.                         publishVideoRequestId = ++Constants.id;  
  269.   
  270. //                    String sender = calluser + "_webcam";  
  271. //                    MainActivity.getKurentoRoomAPIInstance().sendReceiveVideoFrom(sender, localSdp.description, publishVideoRequestId);  
  272.   
  273.                         MainActivity.getKurentoRoomAPIInstance().sendPublishVideo(localSdp.description, false, publishVideoRequestId);  
  274.                     }  
  275.                 }  
  276.             });  
  277.         } else { // Asking for remote user video  
  278.             Log.e(TAG,"--->onLocalSdpOfferGenerated:remote");  
  279.             remoteSdp = sessionDescription;  
  280. //            nbmWebRTCPeer.selectCameraPosition(NBMMediaConfiguration.NBMCameraPosition.BACK);  
  281.             runOnUiThread(new Runnable() {  
  282.                 @Override  
  283.                 public void run() {  
  284.                     if (MainActivity.getKurentoRoomAPIInstance() != null) {  
  285.                         Log.e(TAG, "Sending--> " +calluser+ sessionDescription.type);  
  286.                         publishVideoRequestId = ++Constants.id;  
  287.   
  288.                         String sender = calluser + "_webcam";  
  289.                         MainActivity.getKurentoRoomAPIInstance().sendReceiveVideoFrom(sender, remoteSdp.description, publishVideoRequestId);  
  290.                     }  
  291.                 }  
  292.             });  
  293.         }  
  294.     }  
  295.   
  296.     @Override  
  297.     public void onLocalSdpAnswerGenerated(SessionDescription sessionDescription, NBMPeerConnection nbmPeerConnection) {  
  298.     }  
  299.   
  300.     @Override  
  301.     public void onIceCandidate(IceCandidate iceCandidate, NBMPeerConnection nbmPeerConnection) {  
  302.         Log.e(TAG,"--->onIceCandidate");  
  303.         sendIceCandidateRequestId = ++Constants.id;  
  304.         if (callState == CallState.PUBLISHING || callState == CallState.PUBLISHED){  
  305.             Log.e(TAG,"--->onIceCandidate:publish");  
  306.             MainActivity.getKurentoRoomAPIInstance().sendOnIceCandidate(this.username, iceCandidate.sdp,  
  307.                     iceCandidate.sdpMid, Integer.toString(iceCandidate.sdpMLineIndex), sendIceCandidateRequestId);  
  308.         } else{  
  309.             Log.e(TAG,"--->onIceCandidate:"+this.calluser);  
  310.             MainActivity.getKurentoRoomAPIInstance().sendOnIceCandidate(this.calluser, iceCandidate.sdp,  
  311.                     iceCandidate.sdpMid, Integer.toString(iceCandidate.sdpMLineIndex), sendIceCandidateRequestId);  
  312.         }  
  313.     }  
  314.   
  315.     @Override  
  316.     public void onIceStatusChanged(PeerConnection.IceConnectionState iceConnectionState, NBMPeerConnection nbmPeerConnection) {  
  317.         Log.i(TAG, "onIceStatusChanged");  
  318.     }  
  319.   
  320.     @Override  
  321.     public void onRemoteStreamAdded(MediaStream mediaStream, NBMPeerConnection nbmPeerConnection) {  
  322.         if (callState == CallState.PUBLISHING || callState == CallState.PUBLISHED) {  
  323.             Log.e(TAG, "-->onRemoteStreamAdded-->no");  
  324.             return;  
  325.   
  326.   
  327.         }  
  328.         Log.e(TAG, "-->onRemoteStreamAdded");  
  329.         RendererCommon.ScalingType scalingType = RendererCommon.ScalingType.SCALE_ASPECT_FILL;  
  330.         remoteRender = VideoRendererGui.create( REMOTE_X, REMOTE_Y,  
  331.                 REMOTE_WIDTH, REMOTE_HEIGHT,  
  332.                 scalingType, false);  
  333.         REMOTE_Y = REMOTE_Y+25;  
  334.         nbmWebRTCPeer.attachRendererToRemoteStream(remoteRender, mediaStream);  
  335.         runOnUiThread(new Runnable() {  
  336.             @Override  
  337.             public void run() {  
  338.                 mCallStatus.setText("");  
  339.             }  
  340.         });  
  341.     }  
  342.   
  343.     @Override  
  344.     public void onRemoteStreamRemoved(MediaStream mediaStream, NBMPeerConnection nbmPeerConnection) {  
  345.         Log.i(TAG, "onRemoteStreamRemoved");  
  346.     }  
  347.   
  348.     @Override  
  349.     public void onPeerConnectionError(String s) {  
  350.         Log.e(TAG, "onPeerConnectionError:" + s);  
  351.     }  
  352.   
  353.     @Override  
  354.     public void onRoomResponse(RoomResponse response) {  
  355.         Log.e(TAG, "-->OnRoomResponse:" + response);  
  356.         if (Integer.valueOf(response.getId()) == publishVideoRequestId){  
  357.             SessionDescription sd = new SessionDescription(SessionDescription.Type.ANSWER,  
  358.                     response.getValue("sdpAnswer").get(0));  
  359.             if (callState == CallState.PUBLISHING){  
  360.                 callState = CallState.PUBLISHED;  
  361.                 nbmWebRTCPeer.processAnswer(sd, "derp");  
  362.             } else if (callState == CallState.WAITING_REMOTE_USER){  
  363.                 callState = CallState.RECEIVING_REMOTE_USER;  
  364.                 nbmWebRTCPeer.processAnswer(sd, "remote");  
  365.             } else if (callState == CallState.PATICIPANT_JOINED){  
  366.   
  367.                 callState = CallState.RECEIVING_PATICIPANT;  
  368.                 nbmWebRTCPeer.processAnswer(sd, this.PaticipantID);  
  369.                 //NOP  
  370.             }  
  371.         }  
  372.     }  
  373.   
  374.     @Override  
  375.     public void onRoomError(RoomError error) {  
  376.         Log.e(TAG, "OnRoomError:" + error);  
  377.     }  
  378.   
  379.     @Override  
  380.     public void onRoomNotification(RoomNotification notification) {  
  381.         Log.e(TAG, "OnRoomNotification--> (state=" + callState.toString() + "):" + notification);  
  382.   
  383.         if(notification.getMethod().equals("iceCandidate")) {  
  384.             Map map = notification.getParams();  
  385.   
  386.             String sdpMid = map.get("sdpMid").toString();  
  387.             int sdpMLineIndex = Integer.valueOf(map.get("sdpMLineIndex").toString());  
  388.             String sdp = map.get("candidate").toString();  
  389.   
  390.             IceCandidate ic = new IceCandidate(sdpMid, sdpMLineIndex, sdp);  
  391.             Log.e(TAG, "callState-->" + callState);  
  392.             if (callState == CallState.PUBLISHING || callState == CallState.PUBLISHED) {  
  393.                 nbmWebRTCPeer.addRemoteIceCandidate(ic, "derp");  
  394.             }else if(callState==CallState.PATICIPANT_JOINED ||  callState== CallState.RECEIVING_PATICIPANT){  
  395.                 nbmWebRTCPeer.addRemoteIceCandidate(ic,this.PaticipantID);  
  396.             }else {  
  397.                 nbmWebRTCPeer.addRemoteIceCandidate(ic, "remote");  
  398.             }  
  399.         }  
  400.         if(notification.getMethod().equals("participantPublished"))  
  401.         {  
  402.             Map map = notification.getParams();  
  403.             final String user = map.get("id").toString();  
  404.             this.calluser = user;  
  405.             this.PaticipantID = "pt_"+this.calluser;  
  406.   
  407.             PeerVideoActivity.this.runOnUiThread(new Runnable() {  
  408.                 @Override  
  409.                 public void run() {  
  410.                     callState = CallState.PATICIPANT_JOINED;  
  411.                     nbmWebRTCPeer.generateOffer(PaticipantID, false);  
  412.   
  413.                 }  
  414.             });  
  415.         }  
  416.     }  
  417.   
  418.     @Override  
  419.     public void onRoomConnected() {  
  420.   
  421.     }  
  422.   
  423.     @Override  
  424.     public void onRoomDisconnected() {  
  425.   
  426.     }  
  427. }  


       再就是android room demo中的MainActivity的添加cert的代码要去掉注释,让这段代码生效,就可以连通服务器了。

       在iOS的实施方面,上面这家公司也提供了一个工具包:https://github.com/nubomediaTI/Kurento-iOS ,工具包里面也有demo

       Web方面,最上面官方的哪个demo就足够参考了

你可能感兴趣的:(基于Kurento的WebRTC移动视频群聊解决方案)