Android手把手编写儿童手机远程监控App之webrtc聊天数据通道

概述

上节完成webrtc音视频通信以及sdp、ice详细讲解。

  • sdp描述多媒体通信会话的各项参数,如视频参数、音频参数
  • ice是用于点对点连接的网络信息 webrtc正式通信之前,首先交互sdp,再交互ice。交互完成后开始创建通道建立。webrtc建立通道可以分成三类:
  • 视频通道
  • 音频通道
  • 数据通道 webrtc通信,是点对点通信。嘟宝与嘟妈是直接相互连接,而不通过服务器,这是去中心化。而如今我们的网络设备都处在NAT,是无法直接相互连接的。这就需要一个工具,coturn打通一个通道。而打通通道在不同网络中会有失败的可能。所有连接的通道合在一个,这个通道同时传输视频通道、音频通道、数据通道。

数据通道

WebRTC 数据通道(Data Channel)是一个强大的功能,允许你在两个 PeerConnection 之间直接传输任意数据,无需经过服务器中转(理论上),延迟极低,非常适合实时游戏、文件传输、聊天等场景。

数据通道的创建

  • 创建RTCPeerConnection对象pc
  • 创建数据通道createDataChannel对象
  • 创建数据通道再创建offer交互sdp

嘟妈创建数据通道

clike 复制代码
 const configuration = {
    iceServers: [
      { urls: 'stun:192.168.1.20:3478' },
    ]
  }
peerConnection = new RTCPeerConnection(configuration)
// ✅ 创建数据通道(必须在创建 Offer 之前!)
  dataChannel = peerConnection.createDataChannel('chat', {
    ordered: true,        // 保证顺序
    maxRetransmits: 3,    // 最大重传次数(不设置则可靠传输)
    // 或者使用 maxPacketLifeTime: 1000  // 最大存活时间(毫秒)
  });

  // 监听数据通道事件
  dataChannel.onopen = () => {
    console.log(' 数据通道已打开');
    dataChannel.send('Hello from offerer!');
  };

  dataChannel.onmessage = (event:any) => {
    console.log('收到消息:', event.data);
  };

  dataChannel.onclose = () => {
    console.log('数据通道已关闭');
  };

在交互完sdp与iec建立连接后,数据通道正式建立,回调onopen 。

发送二进制数据

clike 复制代码
// 发送 ArrayBuffer
const buffer = new ArrayBuffer(1024);
dataChannel.send(buffer);

// 发送 Blob
const blob = new Blob(['hello'], { type: 'text/plain' });
dataChannel.send(blob);

// 发送 TypedArray
const int16Array = new Int16Array([1, 2, 3, 4]);
dataChannel.send(int16Array.buffer);

注意事项

  • 必须在创建 Offer 前创建数据通道(作为发起方)
  • 数据大小限制:单个消息通常限制在 16KB-256KB,大文件需要分块
  • 信令交换:数据通道的协商随 SDP 一起完成,不需要额外信令
  • ICE 重启:数据通道会在 ICE 重启后自动恢复

嘟宝创建数据通道

clike 复制代码
 List<PeerConnection.IceServer> iceServers=new ArrayList <>();
       iceServers.add(PeerConnection.IceServer.builder("stun:192.168.1.20:3478").createIceServer());
        PeerConnection.RTCConfiguration config =new PeerConnection.RTCConfiguration(iceServers);
        peerConnection=factory.createPeerConnection(config, new PeerConnection.Observer() {
        
            @Override
            public void onDataChannel(DataChannel dataChannel) {
                mdataChannel=dataChannel;
                setupDataChannelObserver();
                print("onDataChannel--------------------------");

            }

            @Override
            public void onRenegotiationNeeded() {

            }

            @Override
            public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
                print("onAddTrack!!!!!!!!!!!!!!!!!!!!");

            }
        });

Android sdp在创建PeerConnection时,通过回调自动创建数据通道DataChannel 。 在onDataChannel函数中获取dataChannel对象,调用setupDataChannelObserver创建dataChannel事件信息

clike 复制代码
 if (mdataChannel == null) {

            return;
        }
        mdataChannel.registerObserver(new DataChannel.Observer() {
            @Override
            public void onBufferedAmountChange(long l) {
                print("webrtc缓冲区变化:"+l);

            }

            @Override
            public void onStateChange() {
                DataChannel.State state = mdataChannel.state();
                Log.d("WebRTC", ": " + state);
                print("WebRTC 数据通道状态:"+state);
                if (state == DataChannel.State.OPEN) {
                    Log.d("mqtt", "数据通道已打开");
                    // 可以发送初始消息
                } else if (state == DataChannel.State.CLOSED) {
                    Log.d("mqtt", " 数据通道已关闭");
                }

            }

            @Override
            public void onMessage(DataChannel.Buffer buffer) {
                if (buffer.binary) {
                    // 二进制数据
                    ByteBuffer byteBuffer = buffer.data;
                    byte[] bytes = new byte[byteBuffer.remaining()];
                    byteBuffer.get(bytes);
                } else {
                    // 文本消息
                    ByteBuffer byteBuffer = buffer.data;
                    byte[] bytes = new byte[byteBuffer.remaining()];
                    byteBuffer.get(bytes);
                    String message = new String(bytes);
                    sendMessage("hello !!!!!!!");
                }

            }
        });
    }

通过sendMessage发送数据

clike 复制代码
 // 发送文本消息
    public void sendMessage(String message) {
        if (mdataChannel != null && mdataChannel.state() == DataChannel.State.OPEN) {
            byte[] bytes = message.getBytes();
            ByteBuffer buffer = ByteBuffer.wrap(bytes);
            DataChannel.Buffer dataBuffer = new DataChannel.Buffer(buffer, false);
            boolean success = mdataChannel.send(dataBuffer);
            Log.d("WebRTC", "发送消息: " + message + ", 结果: " + success);
        } else {
            Log.e("WebRTC", "数据通道未打开");
        }
    }

发送二进制数据

clike 复制代码
   // 发送二进制数据
    public void sendBinaryData(byte[] data) {
        if (mdataChannel != null && mdataChannel.state() == DataChannel.State.OPEN) {
            ByteBuffer buffer = ByteBuffer.wrap(data);
            DataChannel.Buffer dataBuffer = new DataChannel.Buffer(buffer, true);
            boolean success = mdataChannel.send(dataBuffer);
            Log.d("WebRTC", "发送二进制数据长度: " + data.length + ", 结果: " + success);
        }
    }

嘟宝完成类设计

clike 复制代码
package com.zilong.dubao;

import android.content.Context;
import android.content.Intent;
import android.media.projection.MediaProjection;
import android.util.Log;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

import org.json.JSONObject;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraVideoCapturer;
import org.webrtc.DataChannel;
import org.webrtc.DefaultVideoDecoderFactory;
import org.webrtc.DefaultVideoEncoderFactory;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.RtpReceiver;
import org.webrtc.RtpSender;
import org.webrtc.ScreenCapturerAndroid;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;


public class MyWebRtc {
    private EglBase eglBase;
    private PeerConnectionFactory factory;
    PeerConnection peerConnection;
    Context context;
    DataChannel mdataChannel;
    MyWebRtc(){
        this.context=app.getContext();
        init();

    }
    private void print(String s){
        Log.d("mqtt",s);

    }
    private void init() {
        eglBase = EglBase.create();
        // 初始化 WebRTC
        PeerConnectionFactory.InitializationOptions options =
                PeerConnectionFactory.InitializationOptions
                        .builder(context)
                        .createInitializationOptions();

        PeerConnectionFactory.initialize(options);
        factory = PeerConnectionFactory.builder()
                .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(),true,true))
                .setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBase.getEglBaseContext()))
                .createPeerConnectionFactory();

    }
    public void createPeerConnection(String remoteSdp) {
        List<PeerConnection.IceServer> iceServers=new ArrayList <>();
        iceServers.add(PeerConnection.IceServer.builder("stun:192.168.1.20:3478").createIceServer());
        PeerConnection.RTCConfiguration config =new PeerConnection.RTCConfiguration(iceServers);
        peerConnection=factory.createPeerConnection(config, new PeerConnection.Observer() {
            @Override
            public void onSignalingChange(PeerConnection.SignalingState signalingState) {

            }

            @Override
            public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
                print("onIceConnectionChange"+iceConnectionState.name());
                if(iceConnectionState==PeerConnection.IceConnectionState.FAILED){

                }

            }

            @Override
            public void onIceConnectionReceivingChange(boolean b) {

            }

            @Override
            public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {

            }

            @Override
            public void onIceCandidate(IceCandidate iceCandidate) {
                js_IceCandidate js=new js_IceCandidate();
                js.candidate=iceCandidate.sdp;
                js.sdpMid=iceCandidate.sdpMid;
                js.sdpMLineIndex=iceCandidate.sdpMLineIndex;
                Gson gson = new Gson();
                String s = gson.toJson(js);
                pushmsg("ice",s);
            }

            @Override
            public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {

            }

            @Override
            public void onAddStream(MediaStream mediaStream) {

            }

            @Override
            public void onRemoveStream(MediaStream mediaStream) {

            }

            @Override
            public void onDataChannel(DataChannel dataChannel) {
                mdataChannel=dataChannel;
                setupDataChannelObserver();
                print("onDataChannel--------------------------");

            }

            @Override
            public void onRenegotiationNeeded() {

            }

            @Override
            public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
                print("onAddTrack!!!!!!!!!!!!!!!!!!!!");

            }
        });
        peerConnection.addTrack(getAudioTrack());
        localVideoSender =peerConnection.addTrack(getVideoTrack());
        SessionDescription offer=JSONSessionDescription(remoteSdp);
        peerConnection.setRemoteDescription(new SdpObserver() {
            @Override
            public void onCreateSuccess(SessionDescription sessionDescription) {
                print("onCreateSuccess");

            }

            @Override
            public void onSetSuccess() {
                print("onSetSuccess");
                createAnswer();
            }

            @Override
            public void onCreateFailure(String s) {
                print(s);

            }

            @Override
            public void onSetFailure(String s) {
                print(s);

            }
        }, offer);
    }

    private AudioTrack getAudioTrack(){
        AudioSource audioSource =
                factory.createAudioSource(
                        new MediaConstraints());

        AudioTrack audioTrack =
                factory.createAudioTrack(
                        "audio_track",
                        audioSource);
        audioTrack.enabled();
        return audioTrack;
    }
    private CameraVideoCapturer mCamCapture;
    private SurfaceTextureHelper surfaceTextureHelper;
    private SurfaceTextureHelper surfaceTextureHelperscreen;

    private RtpSender localVideoSender;
    static Intent i;
    private VideoTrack getVideoTrack(){
        surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.getEglBaseContext());
        VideoCapturer videoCapturer = createCameraCapturer(true);
        VideoSource videoSource = factory.createVideoSource(videoCapturer.isScreencast());
        videoCapturer.initialize(surfaceTextureHelper, context.getApplicationContext(), videoSource.getCapturerObserver());
        videoCapturer.startCapture(480, 640, 30);
        VideoTrack videoTrack=   factory.createVideoTrack("100", videoSource);
        videoTrack.enabled();
        return videoTrack;
    }


    private void setupDataChannelObserver() {
        if (mdataChannel == null) {

            return;
        }
        mdataChannel.registerObserver(new DataChannel.Observer() {
            @Override
            public void onBufferedAmountChange(long l) {
                print("webrtc缓冲区变化:"+l);

            }

            @Override
            public void onStateChange() {
                DataChannel.State state = mdataChannel.state();
                Log.d("WebRTC", ": " + state);
                print("WebRTC 数据通道状态:"+state);
                if (state == DataChannel.State.OPEN) {
                    Log.d("mqtt", "✅ 数据通道已打开");
                    // 可以发送初始消息
                } else if (state == DataChannel.State.CLOSED) {
                    Log.d("mqtt", "❌ 数据通道已关闭");
                }

            }

            @Override
            public void onMessage(DataChannel.Buffer buffer) {
                if (buffer.binary) {
                    // 二进制数据
                    ByteBuffer byteBuffer = buffer.data;
                    byte[] bytes = new byte[byteBuffer.remaining()];
                    byteBuffer.get(bytes);
//                    handleBinaryMessage(bytes);
                } else {
                    // 文本消息
                    ByteBuffer byteBuffer = buffer.data;
                    byte[] bytes = new byte[byteBuffer.remaining()];
                    byteBuffer.get(bytes);
                    String message = new String(bytes);
//                    handleTextMessage(message);
                    print(message);
                    sendMessage("shoudaoshsdasdasd");
                }

            }
        });



    }

    // 发送文本消息
    public void sendMessage(String message) {
        if (mdataChannel != null && mdataChannel.state() == DataChannel.State.OPEN) {
            byte[] bytes = message.getBytes();
            ByteBuffer buffer = ByteBuffer.wrap(bytes);
            DataChannel.Buffer dataBuffer = new DataChannel.Buffer(buffer, false);
            boolean success = mdataChannel.send(dataBuffer);
            Log.d("WebRTC", "发送消息: " + message + ", 结果: " + success);
        } else {
            Log.e("WebRTC", "数据通道未打开");
        }
    }
    // 发送二进制数据
    public void sendBinaryData(byte[] data) {
        if (mdataChannel != null && mdataChannel.state() == DataChannel.State.OPEN) {
            ByteBuffer buffer = ByteBuffer.wrap(data);
            DataChannel.Buffer dataBuffer = new DataChannel.Buffer(buffer, true);
            boolean success = mdataChannel.send(dataBuffer);
            Log.d("WebRTC", "发送二进制数据长度: " + data.length + ", 结果: " + success);
        }
    }

    public void getScreenVideo() {
        surfaceTextureHelperscreen = SurfaceTextureHelper.create("CaptureThread1", eglBase.getEglBaseContext());

        VideoCapturer screenCapturer=new ScreenCapturerAndroid(i, new MediaProjection.Callback() {
            @Override
            public void onStop() {
                super.onStop();
            }
        }); // eglBaseContext 是你之前初始化的 EGL 上下文
        VideoSource screenVideoSource = factory.createVideoSource(screenCapturer.isScreencast());
        VideoTrack screenVideoTrack = factory.createVideoTrack("102", screenVideoSource);

        // 2. 初始化并启动 ScreenCapturer
        // 注意:需要提前准备好 SurfaceTextureHelper
        screenCapturer.initialize(surfaceTextureHelperscreen, context.getApplicationContext(), screenVideoSource.getCapturerObserver());
        screenCapturer.startCapture(/* width */ 640, /* height */ 480, /* fps */ 25);

        // 3. 执行替换的关键操作
//        localVideoSender.track().setEnabled(false);
        localVideoSender.setTrack(screenVideoTrack, /* takeOwnership= */ true);
    }

    public void changeCam(){
        if (mCamCapture != null) {
            mCamCapture.switchCamera(null);
        }

    }
    private VideoCapturer createCameraCapturer(boolean isFront) {
        Camera2Enumerator enumerator = new Camera2Enumerator(context.getApplicationContext());
        final String[] deviceNames = enumerator.getDeviceNames();
        for (String deviceName : deviceNames) {
            if (isFront ? enumerator.isFrontFacing(deviceName) : enumerator.isBackFacing(deviceName)) {
                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
                mCamCapture = (CameraVideoCapturer) videoCapturer;
                if (videoCapturer != null) {
                    return videoCapturer;
                }
            }
        }
        return null;
    }
    private void createAnswer(){
        peerConnection.createAnswer(new SdpObserver() {
            @Override
            public void onCreateSuccess(SessionDescription sessionDescription) {
                peerConnection.setLocalDescription(this,sessionDescription);
                pushmsg("answer",(SessionDescriptionJSON(sessionDescription)));


            }

            @Override
            public void onSetSuccess() {
                print("answer onSetSuccess");

            }

            @Override
            public void onCreateFailure(String s) {

            }

            @Override
            public void onSetFailure(String s) {

            }
        }, new MediaConstraints());
    }
    public String SessionDescriptionJSON(SessionDescription description){
        try {
            JS_SessionDescription s=new JS_SessionDescription();
            switch (description.type){
                case ANSWER:
                    s.type="answer";
                    break;
                default:break;

            }
            s.sdp=description.description;
            Gson gson = new Gson();
            return  gson.toJson(s);
        } catch (JsonSyntaxException e) {
            e.printStackTrace();
            return "";
        }

    }
    public void onRemoteIceCandidateReceived(String s) {
//        print("发起者收到 onRemoteIceCandidateReceived10"+s);
        peerConnection.addIceCandidate(jsonToIceCandidate(s));
    }
    class JS_SessionDescription{
        String sdp="";
        String type="";
    }
    class js_IceCandidate{
        public  String sdpMid;
        public  int sdpMLineIndex;
        public  String candidate;
        public  String usernameFragment="";
    }
    public IceCandidate jsonToIceCandidate(String s) {
        Gson gson = new Gson();
        js_IceCandidate c = gson.fromJson(s, js_IceCandidate.class);
        return new IceCandidate(c.sdpMid, c.sdpMLineIndex, c.candidate);
    }
    public SessionDescription JSONSessionDescription(String s){
        try {
            Gson gson = new Gson();
            JS_SessionDescription offer = gson.fromJson(s, JS_SessionDescription.class);
            SessionDescription.Type type= SessionDescription.Type.OFFER;
            switch (offer.type) {
                case "offer":
                    type = SessionDescription.Type.OFFER;
                    break;
                case "answer":
                    type = SessionDescription.Type.ANSWER;
                    break;
                case "pranswer":
                    type = SessionDescription.Type.PRANSWER;
                    break;
            }
            return new SessionDescription(type, offer.sdp);
        } catch (JsonSyntaxException e) {
            e.printStackTrace();
            return null;
        }
    }
    private void pushmsg(String code,String data){
        MyMqttClient.Msg msg=new MyMqttClient.Msg();
        msg.dubaoId=MyService.dubaoId;
        msg.dumaId="f1122aeb-f2b0-400d-9919-eddd2eaebaa2";
        msg.dumaName="天使嘟妈";
        msg.code=code;
        msg.data=data;
        Gson gson=new Gson();
        String json=gson.toJson(msg);
        MyService.myMqttClient.publish("/duma/"+ msg.dumaId,json);
    }

}
相关推荐
浩风祭月1 小时前
受够了每次切分支都要重装依赖:一份 Git 工作流优化指南
前端·ai编程
谭光志1 小时前
如何从零开始实现一个 AI Agent CLI
前端·javascript·ai编程
半个落月2 小时前
彻底搞懂 JavaScript 变量提升(Hoisting)—— 从现象到底层原理
前端·javascript
零度晚风2 小时前
React 底层原理 & 新特性
前端
用户61848240219512 小时前
我受够了 Electron 的 IPC 样板代码,于是写了 electron-ipc-auto-import
前端
梦想的颜色2 小时前
TypeScript 完全指南(中):函数、接口、类与高级类型
前端·typescript
鹏多多2 小时前
OpenSpec+SDD规范驱动AI Agent开发项目实战指南
前端·vue.js·react.js
叶小树咯2 小时前
React 为什么不能像 Vue 那样 state.count++
前端·react.js
ricardo19732 小时前
防抖节流进阶 + requestAnimationFrame:滚动与输入场景的性能优化
前端·面试