Google WebRtc Android 使用詳解(包括客戶端和服務端代碼)


轉自:https://zhuanlan.zhihu.com/p/82446482

 

1、Google Webrtc介紹

WebRTC(Web Real-Time Communication)實現了基於網頁的視頻會議,標准是WHATWG 協議,目的是通過瀏覽器提供簡單的javascript就可以達到實時通訊(Real-Time Communications (RTC))能力。

提供了視頻會議的核心技術,包括音視頻的采集、編解碼、網絡傳輸、顯示等功能,並且還支持跨平台:windows,linux,mac,android,音視頻通訊技術是基於p2p實現的

2、Google Webrtc服務器配置

信令服務器(signaling),轉發服務器(TURN),穿透服務器(STUN)(自行百度,配置),更詳細的介紹請看書籍 WebRtc權威指南

2.1 信令服務器(signaling)

WebRTC 之間使用PeerConnection交流數據,但還需要一種機制來協調溝通和發送控制消息,這一過程稱之為信令傳輸

2.2 轉發服務器(TURN) & 穿透服務器(STUN)

WebRTC 被設計為點對點工作模式,所以用戶之間是盡可能地通過最短路線進行連接,然而在現實世界當中:客戶端應用需要穿透 NAT 網關 和防火牆,並且點對點網絡需要握手來防止直接連接失敗,在這一過程中,WebRTC使用STUN服務器和TURN服務器來獲取計算機IP地址來保證點對點連接成功

3、ICE框架

基於信令協議的多媒體傳輸是一個兩段式傳輸,首先通過信令協議(如WebSocket)建立一個連接,通過該連接,雙方交換傳輸媒體時所必須的信息。基於傳輸效率的考慮,在完成第一階段的交互之后,通信雙方會建立一條通道來實現媒體傳輸,以減少傳輸時延、降低丟包率並減少開銷。由於使用新的鏈路,當傳輸雙方有任意一方位於NAT之后,新的傳輸鏈路就需要考慮NAT穿越的問題了通常有四種形式的NAT,每一種NAT都有相應的解決方案,然而在復雜的網絡環境中,由於每一種NAT穿越方案都局限於對應的NAT方式,這些方案就給整個系統帶來了一定的復雜性。在這種背景下,交互式連通建立方式(Interactive Connectivity Establishment)即ICE解決方案應運而生,ICE能夠在不增加整個系統的復雜性和脆弱性的情況下,實現對各種形式的NAT的穿越。

官方提供的實現原理 基於下圖

 

4、Android Webrtc的配置

客戶端和服務端通信 采用WebSocket
攝像頭,錄音,SD卡讀取等其他權限加入,有些需要在運行時調用

4.1 在build.gradle 引入websocket 和 webrtc

    dependencies {
            implementation 'org.webrtc:google-webrtc:1.0.28513'
            implementation 'org.java-websocket:Java-WebSocket:1.4.0'
            implementation 'com.google.code.gson:gson:2.8.5'
        }

5、Android Webrtc的使用

5.1 創建websocket

     private void connectionWebsocket() {
            try {
                webSocketClient = new WebSocketClient(URI.create(Constant.URL)) {
                    @Override
                    public void onOpen(ServerHandshake handshakedata) {
                        setText("已連接");
                        Log.e(TAG, "onOpen == Status == " + handshakedata.getHttpStatus() + " StatusMessage == " + handshakedata.getHttpStatusMessage());
                        Model model = new Model(Constant.REGISTER, getFromName(), getFrom(), getToName(), getTo());
                        webSocketClient.send(new Gson().toJson(model));
                    }

                    @Override
                    public void onMessage(String message) {
                        Log.e(TAG, "onMessage == " + message);
                        if (!TextUtils.isEmpty(message)) {
                            Model model = new Gson().fromJson(message, Model.class);
                            if (model != null) {
                                String id = model.getId();
                                if (!TextUtils.isEmpty(id)) {
                                    int isSucceed = model.getIsSucceed();
                                    switch (id) {
                                        case Constant.REGISTER_RESPONSE:
                                            if (isSucceed == Constant.RESPONSE_SUCCEED) {
                                                Message msg = new Message();
                                                msg.obj = Constant.OPEN;
                                                handler.sendMessage(msg);
                                                Log.e(TAG, "連接成功");
                                            } else if (isSucceed == Constant.RESPONSE_FAILURE) {
                                                Log.e(TAG, "注冊失敗,已經注冊");
                                            }
                                            break;
                                        case Constant.CALL_RESPONSE:
                                            if (isSucceed == Constant.RESPONSE_SUCCEED) {
                                                Log.e(TAG, "對方在線,創建sdp offer");
                                                createOffer();
                                            } else if (isSucceed == Constant.RESPONSE_FAILURE) {
                                                Log.e(TAG, "對方不在線,連接失敗");
                                            }
                                            break;
                                        case Constant.INCALL:
                                            isIncall();
                                            break;
                                        case Constant.INCALL_RESPONSE:
                                            if (isSucceed == Constant.RESPONSE_SUCCEED) {
                                                createOffer();
                                                Log.e(TAG, "對方同意接聽");
                                            } else if (isSucceed == Constant.RESPONSE_FAILURE) {
                                                Log.e(TAG, "對方拒絕接聽");
                                            }
                                            break;
                                        case Constant.OFFER:
                                            //收到對方offer sdp
                                            SessionDescription sessionDescription1 = model.getSessionDescription();
                                            peerConnection.setRemoteDescription(observer, sessionDescription1);
                                            createAnswer();
                                            break;
                                        case Constant.CANDIDATE:
                                            //服務端 發送 接收方sdpAnswer
                                            IceCandidate iceCandidate = model.getIceCandidate();
                                            if (iceCandidate != null) {
                                                peerConnection.addIceCandidate(iceCandidate);
                                            }
                                            break;
                                    }
                                }
                            }
                        }
                    }

                    @Override
                    public void onClose(int code, String reason, boolean remote) {
                        setText("已關閉");
                        Log.e(TAG, "onClose == code " + code + " reason == " + reason + " remote == " + remote);
                    }

                    @Override
                    public void onError(Exception ex) {
                        setText("onError == " + ex.getMessage());
                        Log.e(TAG, "onError== " + ex.getMessage());
                    }
                };
                webSocketClient.connect();
            } catch (Exception e) {
                Log.d(TAG, "socket Exception : " + e.getMessage());
            }
        }

5.2 創建websocket成功后開始創建PeerConnection,穿透服務器采用Google提供的("stun:")

    private void createPeerConnection() {
            //Initialising PeerConnectionFactory
            InitializationOptions initializationOptions = InitializationOptions.builder(this)
                    .setEnableInternalTracer(true)
                    .setFieldTrials("WebRTC-H264HighProfile/Enabled/")
                    .createInitializationOptions();
            PeerConnectionFactory.initialize(initializationOptions);
            //創建EglBase對象
            eglBase = EglBase.create();
            PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
            options.disableEncryption = true;
            options.disableNetworkMonitor = true;
            peerConnectionFactory = PeerConnectionFactory.builder()
                    .setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBase.getEglBaseContext()))
                    .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true))
                    .setOptions(options)
                    .createPeerConnectionFactory();
            // 配置STUN穿透服務器  轉發服務器
            iceServers = new ArrayList<>();
            PeerConnection.IceServer iceServer = PeerConnection.IceServer.builder(Constant.STUN).createIceServer();
            iceServers.add(iceServer);

            streamList = new ArrayList<>();

            PeerConnection.RTCConfiguration configuration = new PeerConnection.RTCConfiguration(iceServers);

            PeerConnectionObserver connectionObserver = getObserver();
            peerConnection = peerConnectionFactory.createPeerConnection(configuration, connectionObserver);


            /*
            DataChannel.Init 可配參數說明:
            ordered:是否保證順序傳輸;
            maxRetransmitTimeMs:重傳允許的最長時間;
            maxRetransmits:重傳允許的最大次數;
            */
            DataChannel.Init init = new DataChannel.Init();
            if (peerConnection != null) {
                channel = peerConnection.createDataChannel(Constant.CHANNEL, init);
            }
            DateChannelObserver channelObserver = new DateChannelObserver();
            connectionObserver.setObserver(channelObserver);
            initView();
            initObserver();
        }

 

  • 5.2.1 初始化view
    private void initSurfaceview(SurfaceViewRenderer localSurfaceView) {
            localSurfaceView.init(eglBase.getEglBaseContext(), null);
            localSurfaceView.setMirror(true);
            localSurfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
            localSurfaceView.setKeepScreenOn(true);
            localSurfaceView.setZOrderMediaOverlay(true);
            localSurfaceView.setEnableHardwareScaler(false);
    }
  • 5.2.2 初始化音頻和視頻
    /**
     * 創建本地視頻
     *
     * @param localSurfaceView
     */
    private void startLocalVideoCapture(SurfaceViewRenderer localSurfaceView) {
        VideoSource videoSource = peerConnectionFactory.createVideoSource(true);
        SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(Thread.currentThread().getName(), eglBase.getEglBaseContext());
        VideoCapturer videoCapturer = createVideoCapturer();
        videoCapturer.initialize(surfaceTextureHelper, this, videoSource.getCapturerObserver());
        videoCapturer.startCapture(Constant.VIDEO_RESOLUTION_WIDTH, Constant.VIDEO_RESOLUTION_HEIGHT, Constant.VIDEO_FPS); // width, height, frame per second
        videoTrack = peerConnectionFactory.createVideoTrack(Constant.VIDEO_TRACK_ID, videoSource);
        videoTrack.addSink(localSurfaceView);
        MediaStream localMediaStream = peerConnectionFactory.createLocalMediaStream(Constant.LOCAL_VIDEO_STREAM);
        localMediaStream.addTrack(videoTrack);
        peerConnection.addTrack(videoTrack, streamList);
        peerConnection.addStream(localMediaStream);
    }

    /**
     * 創建本地音頻
     */
    private void startLocalAudioCapture() {
        //語音
        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 audioSource = peerConnectionFactory.createAudioSource(audioConstraints);
        audioTrack = peerConnectionFactory.createAudioTrack(Constant.AUDIO_TRACK_ID, audioSource);
        MediaStream localMediaStream = peerConnectionFactory.createLocalMediaStream(Constant.LOCAL_AUDIO_STREAM);
        localMediaStream.addTrack(audioTrack);
        audioTrack.setVolume(Constant.VOLUME);
        peerConnection.addTrack(audioTrack, streamList);
        peerConnection.addStream(localMediaStream);
    }

5.3 創建PeerConnection成功后通過信令傳輸交換各自信息,實現視頻通話,調整后的流程如下圖

信令
    public static final String REGISTER = "register";//注冊
    public static final String REGISTER_RESPONSE = "register_response";//注冊回復
    public static final int RESPONSE_SUCCEED = 1;//1成功
    public static final int RESPONSE_FAILURE = 2;//2失敗
    public static final String CALL = "call";//撥打
    public static final String CALL_RESPONSE = "call_response";//撥打回復
    public static final String INCALL = "incall";//接聽
    public static final String INCALL_RESPONSE = "incall_response";//接聽回復
    public static final String OFFER = "offer";//發送sdp信息
    public static final String CANDIDATE = "candidate";//ice互傳
流程內容: 客戶端A,客戶端B
  • 5.3.1 -->> A 和 B 都注冊成功后,A 發起 call,服務端收到A的指令后轉發給B, B接受后發給服務器,服務器轉發給A
  • 5.3.2 -->> A createOffer 將自己的SessionDescription 發給服務器,服務器轉發給B
  • 5.3.3 -->> B 收到后將A的SessionDescription 設置到自己的setRemoteDescription,同時createAnswer 發給服務器,服務器在轉發A
  • 5.3.4 -->> A 收到后將B的SessionDescription 設置到自己的setRemoteDescription
  • 5.3.5 -->> A 在PeerConnectionObserver 的 onIceCandidate方法中,將收到的IceCandidate 發給服務端,服務器轉發給B
  • 5.3.6 -->> B 收到后在將IceCandidate 設置到自己的addIceCandidate
  • 5.3.7 -->> B 在PeerConnectionObserver 的 onIceCandidate方法中,將收到的IceCandidate 發給服務端,服務器轉發給A
  • 5.3.8 -->> A 收到后在將IceCandidate 設置到自己的addIceCandidate
  • 5.3.9 -->> A 和 B 在PeerConnectionObserver 的 onAddStream方法中,將遠端的流媒體加入到本地的View
  • 5.3.10 -->> 交互已經結束,可以正常視頻通話
詳細的流程,json 格式
       isSucceed 1成功 2失敗
	
	"from": "11",  "fromName": "王安", "to": "12", "toName": "往事"
		
	1.注冊 
	from 發送服務端:register 
	{
	"from": "11",
	"fromName": "王安",
	"id": "register",
	"isSucceed": 0
	}
	
	from接收:register_response 
	{
	"id": "register_response",
	"isSucceed": 1
	}
	
	2.呼叫
	from 發送服務端:call 
	{
	"from": "11",
	"fromName": "王安",
	"id": "call",
	"isSucceed": 0,
	"to": "12",
	"toName": "往事"
	}
	
	2.1服務端做判斷
	
	如果不在線 直接返回 "isSucceed": 1
	{
	"id": "call_response",
	"isSucceed": 1
	}
	 
	在線 返回給to做判斷
	
	to 接收:incall 
	改變本地狀態
	{
	"id": "incall",
	"isSucceed": 0
	}
		to 發送服務端 拒絕:"isSucceed": 2
		{
			"from": "12",
			"fromName": "往事",
			"id": "incall",
			"isSucceed": 2,
			"to": "11",
			"toName": "王安"
		}
			from接收:incall_response
			做拒絕的操作
			{
				"id": "incall_response",
				"from": "12",
				"fromName": "往事",
				"to": "11",
				"toName": "王安",
				"isSucceed": 2
			}
			
	-------------------------------------------------
		
		to 發送服務端 同意:"isSucceed": 1
		{
			"from": "12",
			"fromName": "往事",
			"id": "incall",
			"isSucceed": 1,
			"to": "11",
			"toName": "王安"
		}
			from接收 同意:incall_response
			  
			{
				"id": "incall_response",
				"from": "12",
				"fromName": "往事",
				"to": "11",
				"toName": "王安",
				"isSucceed": 1
			}
	
	3.to同意后 from發送流
	from 發送服務端:offer
	{
	"from": "11",
	"fromName": "王安",
	"id": "offer",
	"isSucceed": 0,
	"sessionDescription": {
	"description": "v=0\r\no=- 3285195603599485281 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE video\r\na=msid-semantic: WMS localstream\r\nm=video 9 RTP/AVPF 96 97 98 99 100 101 127 124 125\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:9Jqg\r\na=ice-pwd:lM34EvVxjaPGBislEH9VccVu\r\na=ice-options:trickle renomination\r\na=mid:video\r\na=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:13 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 urn:3gpp:video-orientation\r\na=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07\r\na=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=sendrecv\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:98 VP9/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:100 H264/90000\r\na=rtcp-fb:100 goog-remb\r\na=rtcp-fb:100 transport-cc\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 nack pli\r\na=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\na=rtpmap:127 red/90000\r\na=rtpmap:124 rtx/90000\r\na=fmtp:124 apt=127\r\na=rtpmap:125 ulpfec/90000\r\na=ssrc-group:FID 1791802911 305163890\r\na=ssrc:1791802911 cname:8uTWIc2YbUNSY02u\r\na=ssrc:1791802911 msid:localstream videtrack\r\na=ssrc:1791802911 mslabel:localstream\r\na=ssrc:1791802911 label:videtrack\r\na=ssrc:305163890 cname:8uTWIc2YbUNSY02u\r\na=ssrc:305163890 msid:localstream videtrack\r\na=ssrc:305163890 mslabel:localstream\r\na=ssrc:305163890 label:videtrack\r\n",
	"type": "OFFER"
	},
	"to": "12",
	"toName": "往事"
	}
	
	to接收:offer
	{
	"id": "offer",
	"from": "11",
	"fromName": "王安",
	"to": "12",
	"toName": "往事",
	"sessionDescription": {
	"type": "OFFER",
	"description": "v=0\r\no=- 3285195603599485281 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE video\r\na=msid-semantic: WMS localstream\r\nm=video 9 RTP/AVPF 96 97 98 99 100 101 127 124 125\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:9Jqg\r\na=ice-pwd:lM34EvVxjaPGBislEH9VccVu\r\na=ice-options:trickle renomination\r\na=mid:video\r\na=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:13 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 urn:3gpp:video-orientation\r\na=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07\r\na=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=sendrecv\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:98 VP9/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:100 H264/90000\r\na=rtcp-fb:100 goog-remb\r\na=rtcp-fb:100 transport-cc\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 nack pli\r\na=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\na=rtpmap:127 red/90000\r\na=rtpmap:124 rtx/90000\r\na=fmtp:124 apt=127\r\na=rtpmap:125 ulpfec/90000\r\na=ssrc-group:FID 1791802911 305163890\r\na=ssrc:1791802911 cname:8uTWIc2YbUNSY02u\r\na=ssrc:1791802911 msid:localstream videtrack\r\na=ssrc:1791802911 mslabel:localstream\r\na=ssrc:1791802911 label:videtrack\r\na=ssrc:305163890 cname:8uTWIc2YbUNSY02u\r\na=ssrc:305163890 msid:localstream videtrack\r\na=ssrc:305163890 mslabel:localstream\r\na=ssrc:305163890 label:videtrack\r\n"
	},
	"isSucceed": 0
	}
	
	to接收到 offer 創建answer 創建完 to 發送服務端:offer
	{
	"from": "12",
	"fromName": "往事",
	"id": "offer",
	"isSucceed": 0,
	"sessionDescription": {
	"description": "v=0\r\no=- 8665372075017263288 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE video\r\na=msid-semantic: WMS localstream\r\nm=video 9 RTP/AVPF 96 97 98 99 100 101 127 124 125\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:zoQn\r\na=ice-pwd:oBePBH1X7SehiT6Tb83SZV2U\r\na=ice-options:trickle renomination\r\na=mid:video\r\na=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:13 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 urn:3gpp:video-orientation\r\na=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07\r\na=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=sendrecv\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:98 VP9/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:100 H264/90000\r\na=rtcp-fb:100 goog-remb\r\na=rtcp-fb:100 transport-cc\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 nack pli\r\na=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\na=rtpmap:127 red/90000\r\na=rtpmap:124 rtx/90000\r\na=fmtp:124 apt=127\r\na=rtpmap:125 ulpfec/90000\r\na=ssrc-group:FID 330040035 1917181902\r\na=ssrc:330040035 cname:uxoKaX6C0J4LHdZ2\r\na=ssrc:330040035 msid:localstream videtrack\r\na=ssrc:330040035 mslabel:localstream\r\na=ssrc:330040035 label:videtrack\r\na=ssrc:1917181902 cname:uxoKaX6C0J4LHdZ2\r\na=ssrc:1917181902 msid:localstream videtrack\r\na=ssrc:1917181902 mslabel:localstream\r\na=ssrc:1917181902 label:videtrack\r\n",
	"type": "ANSWER"
	},
	"to": "11",
	"toName": "王安"
	}
	
	4.ice交換
	from 發送服務端:candidate
	{
	"from": "11",
	"fromName": "王安",
	"iceCandidate": {
	"sdp": "candidate:3575964346 1 udp 2122260223 192.168.0.166 47369 typ host generation 0 ufrag 9Jqg network-id 3 network-cost 10",
	"sdpMLineIndex": 0,
	"sdpMid": "video",
	"serverUrl": ""
	},
	"id": "candidate",
	"isSucceed": 0,
	"to": "12",
	"toName": "往事"
	}
	
	to 接收:candidate
	
	{
	"from": "11",
	"fromName": "王安",
	"iceCandidate": {
	"sdp": "candidate:3575964346 1 udp 2122260223 192.168.0.166 47369 typ host generation 0 ufrag 9Jqg network-id 3 network-cost 10",
	"sdpMLineIndex": 0,
	"sdpMid": "video",
	"serverUrl": ""
	},
	"id": "candidate",
	"isSucceed": 0,
	"to": "12",
	"toName": "往事"
	}
	
	to 發送服務端:candidate
	{
	"from": "12",
	"fromName": "往事",
	"iceCandidate": {
	"sdp": "candidate:559267639 1 udp 2122202367 ::1 55843 typ host generation 0 ufrag zoQn network-id 2",
	"sdpMLineIndex": 0,
	"sdpMid": "video",
	"serverUrl": ""
	},
	"id": "candidate",
	"isSucceed": 0,
	"to": "11",
	"toName": "王安"
	}
	
	from接收:candidate
	{
	"from": "12",
	"fromName": "往事",
	"iceCandidate": {
	"sdp": "candidate:559267639 1 udp 2122202367 ::1 55843 typ host generation 0 ufrag zoQn network-id 2",
	"sdpMLineIndex": 0,
	"sdpMid": "video",
	"serverUrl": ""
	},
	"id": "candidate",
	"isSucceed": 0,
	"to": "11",
	"toName": "王安"
	}

5.4 實現視頻通話,部分截圖

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM