精华内容
下载资源
问答
  • 局域网通话实现了

    2013-09-24 17:07:47
    这个比较适合新手学习,实现了在局域网通话的功能,
  • 局域网视频通话

    2014-11-18 15:41:56
    c# 局域网视频通话小软件,本人亲自写的 ,绝对测试可用,主要使用的是tcp协议。在xp系统上需要开启点对点服务。可实现视频通话,文字发送,可录像。录像是录得当前程序运行的窗口。
  • 局域网语音通话

    2017-07-05 17:39:40
    局域网点对点通话,无噪音效果良好
  • 局域网语音通话demo

    千次阅读 2018-04-30 10:49:00
    https://github.com/xj913492952/SocketClientVoicePhone https://github.com/eltld/SocketClientVoicePhone  
    展开全文
  • 局域网语音通话,单方向的,改的别人的代码,G711音频编码,udp传输,开始后一直是个死循环,不想写多线程了
  • vc++ G726局域网语音通话源代码。
  • Qt局域网语音实现,目前测试系统在windows下面测试通过,在linux下面也能运行,没试通话,实现原理:QAudioInput采集音频数据,通过udp发送至客户端,客户端收到数据后写入Output进行播放,完成实时通话功能。
  • G726局域网语音通话源代码 有研究或探讨或开源的请加群:37424970 或联系本人MSN或邮箱:zhuseahui@yahoo.com.cn
  • 请问,这个模块可以基于什么开发包做? 一般开发周期多长时间? 工程量有多大? 希望做过类似的大虾能指导!!!
  • wifi局域网内双向语音实时通话

    热门讨论 2013-06-20 22:49:48
    wifi局域网内双向语音实时通话
  • 基于Java实现语音通话,可实现 1对1通话(明确对方IP地址)或者群通话(不需要知道对方IP地址),希望能帮到更多小伙伴,直接运行main方法就可以,总共三个文件,有问题留言交流。
  • 搭建SIP局域网语音通话个工具合集 mini SIP Server V37 windows服务器、 Sip Serve 2008 windows服务器、 X-Lite windows客户端 sip droid 安卓客户端
  • C# Webapi+Webrtc 局域网音视频通话示例

    千次阅读 2020-06-19 10:37:15
    C# WebApi+Webrtc 局域网音视频通话示例 本示例通过IIS部署webapi,利用websocket进行webrtc消息交换,通过Chrome浏览器访问,可实现局域网内webrtc 音视频通话。 通过Chrome浏览器打开localhost/live.html本地网址...

    C# WebApi+Webrtc 局域网音视频通话示例

    本示例通过IIS部署webapi,利用websocket进行webrtc消息交换,通过Chrome浏览器访问,可实现局域网内webrtc 音视频通话。

    通过Chrome浏览器打开localhost/live.html本地网址,打开两个本地网,点击任意页面连接按钮即联通。

    本示例未实现NAT穿透处理,互联网无法联通,如需NAT穿透请自行查阅相关资料。

    关于webrtc、webapi相关技术说明请自行查阅相关资料,本文不做赘述说明。

    运行效果如下图:

    在这里插入图片描述
    webapi端Handler1.ashx代码如下:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net.WebSockets;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web;
    using System.Web.WebSockets;
    
    namespace webrtclan
    {
        /// <summary>
        /// 离线消息
        /// </summary>
        public class MessageInfo
        {
            public MessageInfo(DateTime _MsgTime, ArraySegment<byte> _MsgContent)
            {
                MsgTime = _MsgTime;
                MsgContent = _MsgContent;
            }
            public DateTime MsgTime { get; set; }
            public ArraySegment<byte> MsgContent { get; set; }
            
        }
     
    
        /// <summary>
        /// Handler1 的摘要说明
        /// </summary>
        public class Handler1 : IHttpHandler
        {
            private static Dictionary<string, WebSocket> CONNECT_POOL = new Dictionary<string, WebSocket>();//用户连接池
            private static Dictionary<string, List<MessageInfo>> MESSAGE_POOL = new Dictionary<string, List<MessageInfo>>();//离线消息池
            
    
            public void ProcessRequest(HttpContext context)
            {
                
                if (context.IsWebSocketRequest)
                {
                    context.Response.ContentType = "application/json";
                    context.Response.Charset = "utf-8";
                    context.AcceptWebSocketRequest(ProcessMsg);
                }
            }
            
    
            private async Task ProcessMsg(AspNetWebSocketContext context)
            {
                WebSocket socket = context.WebSocket;
                string user = context.QueryString["user"].ToString();
                
                try
                {
                    #region 用户添加连接池
                    //第一次open时,添加到连接池中
                    if (!CONNECT_POOL.ContainsKey(user))
                    {
                        CONNECT_POOL.Add(user, socket);//不存在,添加
                    }
                    else
                    {
                        if (socket != CONNECT_POOL[user])//当前对象不一致,更新
                        {
                            CONNECT_POOL[user] = socket;
                        }
                    }
                    #endregion
    
                    //#region 连线成功
                    //for (int cp = 0; cp < CONNECT_POOL.Count; cp++)
                    //{
                    //    if (CONNECT_POOL.ElementAt(cp).Key != user)
                    //    {
                    //        string joinedmsg = "{\"FROM\":\"" + user + "\",\"event\":\"joined\"}";
                    //        ArraySegment<byte> joinedmsgbuffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(joinedmsg));
                    //        WebSocket destSocket = CONNECT_POOL.ElementAt(cp).Value;//目的客户端
                    //        await destSocket.SendAsync(joinedmsgbuffer, WebSocketMessageType.Text, true, CancellationToken.None);
                    //    }
                    //}
                    //#endregion
    
                    #region 离线消息处理
                    if (MESSAGE_POOL.ContainsKey(user))
                    {
                        List<MessageInfo> msgs = MESSAGE_POOL[user];
                        foreach (MessageInfo item in msgs)
                        {
                            await socket.SendAsync(item.MsgContent, WebSocketMessageType.Text, true, CancellationToken.None);
                        }
                        MESSAGE_POOL.Remove(user);//移除离线消息
                    }
                    #endregion
                    
                    while (true)
                    {
                        if (socket.State == WebSocketState.Open)
                        {
                            ArraySegment<byte> wholemessage= new ArraySegment<byte>(new byte[10240]);
    
                            
                            int i = 0;
    
                           
                            WebSocketReceiveResult dresult;
                            do
                            { 
                                //因为websocket每一次发送的数据会被tcp分包
                                //所以必须判断接收到的消息是否完整
                                //不完整就要继续接收并拼接数据包
                                ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[2048]);
                                dresult = await socket.ReceiveAsync(buffer, CancellationToken.None);
                                string message1 = Encoding.UTF8.GetString(buffer.Array);                            
                                buffer.Array.CopyTo(wholemessage.Array,i);
                                i += 2048;
                            } while (false == dresult.EndOfMessage);                        
    
                            //string message = Encoding.UTF8.GetString(wholemessage.Array);
                            //message = message.Replace("\0", "").Trim();
                            //JavaScriptSerializer serializer = new JavaScriptSerializer();
                            //Dictionary<string, object> json = (Dictionary<string, object>)serializer.DeserializeObject(message);
                            //string target = (string)json.ElementAt(1).Value;
    
                            #region 消息处理(字符截取、消息转发)
                            try
                            {
                                #region 关闭Socket处理,删除连接池
                                if (socket.State != WebSocketState.Open)//连接关闭
                                {
                                    if (CONNECT_POOL.ContainsKey(user)) CONNECT_POOL.Remove(user);//删除连接池
                                    break;
                                }
                                #endregion
                                
                                for (int cp = 0; cp < CONNECT_POOL.Count; cp++)
                                {
                                     //if (CONNECT_POOL.ElementAt(cp).Key!=target)
                                     //   {
                                            WebSocket destSocket = CONNECT_POOL.ElementAt(cp).Value;//目的客户端
                                            await destSocket.SendAsync(wholemessage, WebSocketMessageType.Text, true, CancellationToken.None);
                                     //  }
                                 }
                                
    
                                //if (CONNECT_POOL.ContainsKey(descUser))//判断客户端是否在线
                                //{
                                //    WebSocket destSocket = CONNECT_POOL[descUser];//目的客户端
                                //    if (destSocket != null && destSocket.State == WebSocketState.Open)
                                //        await destSocket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
                                //}
                                //else
                                //{
                                //    _ = Task.Run(() =>
                                //      {
                                //          if (!MESSAGE_POOL.ContainsKey(descUser))//将用户添加至离线消息池中
                                //            MESSAGE_POOL.Add(descUser, new List<MessageInfo>());
                                //          MESSAGE_POOL[descUser].Add(new MessageInfo(DateTime.Now, buffer));//添加离线消息
                                //    });
                                //}
                            }
                            catch (Exception exs)
                            {
                                //消息转发异常处理,本次消息忽略 继续监听接下来的消息
                            }
                            #endregion
                        }
                        else
                        {
                            if (CONNECT_POOL.ContainsKey(user)) CONNECT_POOL.Remove(user);//删除连接池                        
                            break;
                        }
                    }//while end
                }
                catch (Exception ex)
                {
                    //整体异常处理
                    if (CONNECT_POOL.ContainsKey(user)) CONNECT_POOL.Remove(user);
                    
                }
            }
    
         
    
            public bool IsReusable
            {
                get
                {
                    return false;
                }
            }
    
    
        }
    }
    

    live.html客户端代码如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>webrtc</title>
        <style>
            #yours {
                width: 200px;
                position: absolute;
                top: 50px;
                left: 100px;
            }
            #theirs {
                width: 600px;
                position: absolute;
                top: 50px;
                left: 400px;
            }
        </style>
    </head>
    <body>
        <button onclick="createOffer()">建立连接</button>
        <video id="yours" autoplay controls="controls" ></video>
        <video id="theirs" autoplay controls="controls"></video>
    
    </body>
    
    <script src="webrtc.js"></script>
    
    </html>
    

    webrtc.js脚本代码如下:

    var websocket;
    
    function randomNum(minNum, maxNum) {
        switch (arguments.length) {
            case 1:
                return parseInt(Math.random() * minNum + 1, 10);
                break;
            case 2:
                return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
                break;
            default:
                return 0;
                break;
        }
    }
    const userid = 'user' + randomNum(0, 100000);
    
    function hasUserMedia() {
        navigator.getUserMedia = navigator.getUserMedia || navigator.msGetUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
        return !!navigator.getUserMedia;
    }
    function hasRTCPeerConnection() {
        window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerConnection;
        return !!window.RTCPeerConnection;
    }
    
    var yourVideo = document.getElementById("yours");
    var theirVideo = document.getElementById("theirs");
    var Connection;
    
    
    function startPeerConnection() {
        //return;
        var config = {
            'iceServers': [
                //{ 'urls': 'stun:stun.xten.com:3478' },
                //{ 'urls': 'stun:stun.voxgratia.org:3478' },
    
                //{ 'url': 'stun:stun.l.google.com:19302' }
            ]
        };
        config = {
            iceServers: [
                //{ urls: 'stun:stun.l.google.com:19302' },
                //{ urls: 'stun:global.stun.twilio.com:3478?transport=udp' }
            ]
            //sdpSemantics: 'unified-plan'
        };
        // {
        //     "iceServers": [{
        //         "url": "stun:stun.1.google.com:19302"
        //     }]
        // };
        Connection = new RTCPeerConnection(config);
        Connection.onicecandidate = function (e) {
            console.log('onicecandidate');
            if (e.candidate) {
                websocket.send(JSON.stringify({
                    "userid": userid,
                    "event": "_ice_candidate",
                    "data": {
                        "candidate": e.candidate
                    }
                }));
            }
        };
        Connection.onaddstream = function (e) {
            console.log('onaddstream');
            //theirVideo.src = window.URL.createObjectURL(e.stream);
            theirVideo.srcObject = e.stream;
        };
        Connection.onclose = function (e) {
            console.log('RTCPeerConnection close'+e);
        };
    }
    
    createSocket();
    startPeerConnection();
    
    if (hasUserMedia()) {
        navigator.getUserMedia({ video: true, audio: true },
            stream => {
                yourVideo.srcObject = stream;
                window.stream = stream;
                yourVideo.muted = true;
                Connection.addStream(stream)
            },
            err => {
                console.log(err);
            })
    }
    
    
    function createOffer() {
        //发送offer和answer的函数,发送本地session描述
        Connection.createOffer().then(offer => {
            Connection.setLocalDescription(offer);
            websocket.send(JSON.stringify({            
                "userid": userid,
                "event": "offer",
                "data": {
                    "sdp": offer
                }
            }));
        });
    }
    
    
    
    function createSocket() {
        //websocket = null;
        websocket = new WebSocket('ws://localhost:80/Handler1.ashx?user='+userid);//('wss://www.ecoblog.online/wss');
        eventBind();
    };
    
    function eventBind() {
        //连接成功
        websocket.onopen = function (e) {
            console.log('open:' + e);
        };
        //server端请求关闭
        websocket.onclose = function (e) {
            console.log('close:' + e);
        };
        //error
        websocket.onerror = function (e) {
            console.log('error:' + e.data);
        };
        //收到消息
        websocket.onmessage = (event) => {
            if (event.data == "new user") {
                location.reload();
            } else {
                var js = event.data.replace(/[\u0000-\u0019]+/g, ""); 
                var json = JSON.parse(js);
                
                if (json.userid != userid) {
                    //如果是一个ICE的候选,则将其加入到PeerConnection中,否则设定对方的session描述为传递过来的描述
                    if (json.event === "_ice_candidate" && json.data.candidate) {
                        Connection.addIceCandidate(new RTCIceCandidate(json.data.candidate));
                    }
                    else if (json.event === 'offer') {
                        Connection.setRemoteDescription(json.data.sdp);
                        Connection.createAnswer().then(answer => {
                            Connection.setLocalDescription(answer);
                            //console.log(window.stream)
                            websocket.send(JSON.stringify({                            
                                "userid": userid,
                                "event": "answer",
                                "data": {
                                    "sdp": answer
                                }
                            }));
                        })
                    }
                    else if (json.event === 'answer') {
                        Connection.setRemoteDescription(json.data.sdp);
                        //console.log(window.stream)
    
                    }
                }
            }
        };
    }
    
    展开全文
  • 这里是iOS 下WebRTC音视频通话开发的第二篇,在这一篇会利用一个局域网内音视频通话的例子介绍WebRTC中常用的API。 如果你下载并编译完成之后,会看到一个iOS 版的WebRTC Demo。但是那个demo涉及到外网的通讯需要...

    这里是iOS 下WebRTC音视频通话开发的第二篇,在这一篇会利用一个局域网内音视频通话的例子介绍WebRTC中常用的API。
    如果你下载并编译完成之后,会看到一个iOS 版的WebRTC Demo。但是那个demo涉及到外网的通讯需要翻墙,而且还有对信令消息的封装理解起来非常的困难。
    但是,我将要写的这个demo去掉了STUN服务器、TURN服务器配置,以及信令的包装,基本上是用WebRTC进行音视频通话的最精简主干了,非常容易理解。

    准备

    因为这个Demo用到了我之前写的另外两个工程:
    一个XMPP聊天的Demo
    音视频通话的UI效果视图
    如果你对在本地搭建OpenFire服务以及开发一个基于XMPP的聊天小程序感兴趣
    教程在这里:
    XMPP系列(一):OpenFire环境搭建
    XMPP系列(二)—-用户注册和用户登录功能
    XMPP系列(三)—获取好友列表、添加好友
    XMPP系列(四)—发送和接收文字消息,获取历史消息功能
    XMPP系列(五)—文件传输

    所以只需要下载上面两个工程,然后把一些控件合并下,然后配置好你的XMPP服务器的IP和端口号,就可以继续做音视频功能的开发了。

    开始着手开发

    首先我在聊天介绍导航栏上加了两个按钮【视频】【语音】(主要是太懒,不想在输入框做更多功能)。如下图:

    图1.png
    然后为视频按钮添加点击事件,在这个点击事件里需要做几件事:
    1、弹出一个拨打的界面。
    2、播放拨打视频通话的声音。
    3、做WebRTC的配置。

    - (void)videoAction
    {
        NSLog(@"%s",__func__);
        [self startCommunication:YES];
    }
    
    - (void)startCommunication:(BOOL)isVideo
    {
        WebRTCClient *client = [WebRTCClient sharedInstance];
        [client startEngine];
        client.myJID = [HLIMCenter sharedInstance].xmppStream.myJID.full;
        client.remoteJID = self.chatJID.full;
    
        [client showRTCViewByRemoteName:self.chatJID.full isVideo:isVideo isCaller:YES];
    
    }

    所有与WebRTC相关的操作都先封装在WebRTCClient内,然后再根据功能做拆分,后面你可以拆分到不同的类里。
    下面开始介绍WebRTC的相关配置:

    - (void)startEngine
    {
      //如果你需要安全一点,用到SSL验证,那就加上这句话。
        [RTCPeerConnectionFactory initializeSSL];
    
        //set RTCPeerConnection's constraints
        self.peerConnectionFactory = [[RTCPeerConnectionFactory alloc] init];
        NSArray *mandatoryConstraints = @[[[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"],
                                          [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]
                                          ];
        NSArray *optionalConstraints = @[[[RTCPair alloc] initWithKey:@"DtlsSrtpKeyAgreement" value:@"false"]];
        self.pcConstraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:mandatoryConstraints optionalConstraints:optionalConstraints];
    
        //set SDP's Constraints in order to (offer/answer)
        NSArray *sdpMandatoryConstraints = @[[[RTCPair alloc] initWithKey:@"OfferToReceiveAudio" value:@"true"],
                                             [[RTCPair alloc] initWithKey:@"OfferToReceiveVideo" value:@"true"]
                                             ];
        self.sdpConstraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:sdpMandatoryConstraints optionalConstraints:nil];
    
        //set RTCVideoSource's(localVideoSource) constraints
        self.videoConstraints = [[RTCMediaConstraints alloc] initWithMandatoryConstraints:nil optionalConstraints:nil];
    }

    上面的三个约束,只有pcConstraints是必须的,其他的都不是必须的。这些约束主要是控制音视频的采集,以及PeerConnection的设置。
    其他RTC相关的配置是在显示拨打界面后做的操作:

    - (void)showRTCViewByRemoteName:(NSString *)remoteName isVideo:(BOOL)isVideo isCaller:(BOOL)isCaller
    {
        // 1.显示视图
        self.rtcView = [[RTCView alloc] initWithIsVideo:isVideo isCallee:!isCaller];
        self.rtcView.nickName = remoteName;
        self.rtcView.connectText = @"等待对方接听";
        self.rtcView.netTipText = @"网络状况良好";
        [self.rtcView show];
    
        // 2.播放声音
        NSURL *audioURL;
        if (isCaller) {
            audioURL = [[NSBundle mainBundle] URLForResource:@"AVChat_waitingForAnswer.mp3" withExtension:nil];
        } else {
            audioURL = [[NSBundle mainBundle] URLForResource:@"AVChat_incoming.mp3" withExtension:nil];
        }
        _audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:audioURL error:nil];
        _audioPlayer.numberOfLoops = -1;
        [_audioPlayer prepareToPlay];
        [_audioPlayer play];
    
        // 3.拨打时,禁止黑屏
        [UIApplication sharedApplication].idleTimerDisabled = YES;
    
        // 4.监听系统电话
        [self listenSystemCall];
    
        // 5.做RTC必要设置
        if (isCaller) {
            [self initRTCSetting];
            // 如果是发起者,创建一个offer信令
            [self.peerConnection createOfferWithDelegate:self constraints:self.sdpConstraints];
        } else {
            // 如果是接收者,就要处理信令信息,创建一个answer,但是设置和创建answer应该在点击接听后才开始
            NSLog(@"如果是接收者,就要处理信令信息");
            self.rtcView.connectText = isVideo ? @"视频通话":@"语音通话";
        }
    }

    上面的注释已经很明白了。主要内容在[initRTCSetting]中。
    * 1.已ICE服务器地址、pc约束、代理作为参数创建RTCPeerConnection对象。

    self.peerConnection = [self.peerConnectionFactory peerConnectionWithICEServers:_ICEServers constraints:self.pcConstraints delegate:self];
    • 2.创建本地多媒体流
    RTCMediaStream *mediaStream = [self.peerConnectionFactory mediaStreamWithLabel:@"ARDAMS"];
    • 3.为多媒体流添加音频轨迹
    RTCAudioTrack *localAudioTrack = [self.peerConnectionFactory audioTrackWithID:@"ARDAMSa0"];
     [mediaStream addAudioTrack:localAudioTrack];

    音频的采集,已经封装在peerConnectionFactory工厂内。
    * 4.为多媒体流添加视频轨迹

    RTCAVFoundationVideoSource *source = [[RTCAVFoundationVideoSource alloc] initWithFactory:self.peerConnectionFactory constraints:self.videoConstraints];
    RTCVideoTrack *localVideoTrack = [[RTCVideoTrack alloc] initWithFactory:self.peerConnectionFactory source:source trackId:@"AVAMSv0"];
     [mediaStream addVideoTrack:localVideoTrack];

    随着WebRTC的更新,API也被替换了很多,现在视频的采集多了一个新的类RTCAVFoundationVideoSource
    * 5.为视频流添加渲染视图

        RTCEAGLVideoView *localVideoView = [[RTCEAGLVideoView alloc] initWithFrame:self.rtcView.ownImageView.bounds];
        localVideoView.transform = CGAffineTransformMakeScale(-1, 1);
        localVideoView.delegate = self;
        [self.rtcView.ownImageView addSubview:localVideoView];
        self.localVideoView = localVideoView;
        // 添加渲染视图
        [self.localVideoTrack addRenderer:self.localVideoView];

    RTCEAGLVideoView也是新增的一个视图类,我们可以直接将这个视图添加到某个视图上。
    * 6.将多媒体流绑定到peerConnection上

    [self.peerConnection addStream:mediaStream];

    至此发起方的RTC 设置完毕,只用在创建一个Offer,然后将Offer发送给对方。
    * 7.创建Offer

    [self.peerConnection createOfferWithDelegate:self constraints:self.sdpConstraints];
    • 8.在createSession的回调里,为peerConnection设置localDescription,并发送信令给对方—(其实在setSession的代理方法中发送信令更合适,但是那样就得保存sdp,所以这里偷了个懒)。
     - (void)peerConnection:(RTCPeerConnection *)peerConnection
    didCreateSessionDescription:(RTCSessionDescription *)sdp
                     error:(NSError *)error
    {
        if (error) {
            NSLog(@"创建SessionDescription 失败");
    #warning 这里创建 创建SessionDescription 失败
        } else {
            NSLog(@"创建SessionDescription 成功");
            // 这里将SessionDescription转换成H264的sdp,因为默认的是V8格式的视频。
            RTCSessionDescription *sdpH264 = [self descriptionWithDescription:sdp videoFormat:@"H264"];
            [self.peerConnection setLocalDescriptionWithDelegate:self sessionDescription:sdpH264];
            NSDictionary *jsonDict = @{ @"type" : sdp.type, @"sdp" : sdp.description };
            NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:0 error:nil];
            NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
            // 将offer信令消息通过XMPP发送给对方
            [[HLIMClient shareClient] sendSignalingMessage:jsonStr toUser:self.remoteJID];
        }
    }
    • 9.等待对方返回Answer信令消息,当接听后,发送Answer信令消息回来后,将其设置为peerConnection的RemoteDescription即可。然后RTC在处理完成后就开始像对方发送多媒体流啦。

      补充:
      RTCPeerConnection有很多个回调,他们分别是在不同的时机触发
      图3.png

    在为peerConnection添加RTCMediaStream之后就会触发下面这个代理方法:

    - (void)peerConnectionOnRenegotiationNeeded:(RTCPeerConnection *)peerConnection

    设置完LocalDescription之后,ICE框架才会开始去进行流数据传输,才会触发下面这几个方法

     - (void)peerConnection:(RTCPeerConnection *)peerConnection
     signalingStateChanged:(RTCSignalingState)stateChanged
    
     - (void)peerConnection:(RTCPeerConnection *)peerConnection
       iceGatheringChanged:(RTCICEGatheringState)newState
    
    // 而局域网内,因为没有涉及到穿墙,这个代理方法并没有回调。
     - (void)peerConnection:(RTCPeerConnection *)peerConnection
      iceConnectionChanged:(RTCICEConnectionState)newState
    

    其中各种不同的状态的枚举值含义,在这篇文中里有英文解释:中间部分有各种枚举值的解释
    而搜索到ICECandidate之后,会回调:

    - (void)peerConnection:(RTCPeerConnection *)peerConnection
           gotICECandidate:(RTCICECandidate *)candidate

    我们需要在上面这个回调中,将候选信息发送给对方,然后对方讲接收到的候选添加到peerConnection中。
    完整代码:

    - (void)peerConnection:(RTCPeerConnection *)peerConnection
           gotICECandidate:(RTCICECandidate *)candidate
    {
        if (self.HaveSentCandidate) {
            return;
        }
        NSLog(@"新的 Ice candidate 被发现.");
    
        NSDictionary *jsonDict = @{@"type":@"candidate",
                                   @"label":[NSNumber numberWithInteger:candidate.sdpMLineIndex],
                                   @"id":candidate.sdpMid,
                                   @"sdp":candidate.sdp
                                   };
        NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:0 error:nil];
        if (jsonData.length > 0) {
            NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
            [[HLIMClient shareClient] sendSignalingMessage:jsonStr toUser:self.remoteJID];
            self.HaveSentCandidate = YES;
        }
    }

    接收方

    接收方在收到发起方通过XMPP发送过来的信令(可能会有Offer信令,Candidate信令,bye信令)后,先将其保存到数组中,同时展示音视频通话界面,并播放声音。

    这里需要注意:要将收到的Offer信令消息插入到第一个,Offer信令消息必须先处理。

    当点击接听按钮时,初始化RTC的设置,即上面的[initRTCSetting]方法。然后处理之前保存的信令消息。

    - (void)acceptAction
    {
        [self.audioPlayer stop];
    
        [self initRTCSetting];
    
        for (NSDictionary *dict in self.messages) {
            [self processMessageDict:dict];
        }
    
        [self.messages removeAllObjects];
    }

    如何处理之前的信令消息呢?
    处理Offer信令消息:
    将收到的Offer信令设置为peerConnection的RemoteDescription,并创建一个Answer信令发送给对方。
    处理Candidate信令消息
    将收到的信令消息包装成RTCICECandidate对象,然后添加到peerConnection上。
    具体代码:

    - (void)processMessageDict:(NSDictionary *)dict
    {
        NSString *type = dict[@"type"];
        if ([type isEqualToString:@"offer"]) {
            RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:dict[@"sdp"]];
    
            [self.peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:remoteSdp];
    
            [self.peerConnection createAnswerWithDelegate:self constraints:self.sdpConstraints];
        } else if ([type isEqualToString:@"answer"]) {
            RTCSessionDescription *remoteSdp = [[RTCSessionDescription alloc] initWithType:type sdp:dict[@"sdp"]];
    
            [self.peerConnection setRemoteDescriptionWithDelegate:self sessionDescription:remoteSdp];
    
        } else if ([type isEqualToString:@"candidate"]) {
            NSString *mid = [dict objectForKey:@"id"];
            NSNumber *sdpLineIndex = [dict objectForKey:@"label"];
            NSString *sdp = [dict objectForKey:@"sdp"];
            RTCICECandidate *candidate = [[RTCICECandidate alloc] initWithMid:mid index:sdpLineIndex.intValue sdp:sdp];
    
            [self.peerConnection addICECandidate:candidate];
        } else if ([type isEqualToString:@"bye"]) {
    
            if (self.rtcView) {
                NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
                NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
                if (jsonStr.length > 0) {
                    [[HLIMClient shareClient] sendSignalingMessage:jsonStr toUser:self.remoteJID];
                }
    
                [self.rtcView dismiss];
    
                [self cleanCache];
            }
        }
    }

    需要注意的是因为没有用到ICE穿墙,所以必须在同一个路由器下,否则可能无法进行点对点传输多媒体流。至此,局域网内音视频通话的小程序就完成了。

    完整的Demo地址:局域网内WebRTC音视频通话
    Demo中用到的WebRTC静态库已放到:百度网盘

    展开全文
  • 基于Unity与C#控制台的局域网音视频通话解决方案 前段时间由于工作上的需要,研究了一下局域网内音视频通话的需求,思路主要有以下几个点: Socket UDP协议 分包组包 RawImage加WebCamTexture Microphone加...

    前段时间由于工作上的需要,研究了一下局域网内音视频通话的需求,思路主要有以下几个点:

    1. Socket  UDP协议
    2. 分包组包
    3. RawImage加WebCamTexture
    4. Microphone加AudioClip
    5. 数据压缩

    先说一下需求:

    多人在局域网下,在不同设备\平台上进行音视频通话,通话质量要求不高。

    平台用了Unity,到时候如果需要的话改该发布平台就好了,版本是2018.2.2f1,VS2017。

    相关代码贴一部分在最后,源码链接稍后也会贴在文章末尾,如有错误,希望大家不吝指正。

    、Socket   UDP协议

    由于需求内不要求通话质量很高,并且丢包影响不会太大,所以优先选择了UDP协议,图像数据每70毫秒发送一次,音频每500毫秒发送一次

    暂时没有音画同步逻辑。

     

    服务器只有转发功能,记录各个客户端的IP,接收到某个客户端的信息之后,给其他IP转发。

    客户端负责信息传输的部分,包含发送、接收、分包组包等功能。

     

    二、分包组包

    每个包长度固定65045,前45个byte是包头,包头包含用户ID、包数量-完整数据被分成了几个包、包索引

     

    三、RawImage加WebCamTexture

    RawImage用来显示相机内容,WebCamTexture用来获取

    WebCamTexture的像素可以自定义,requestedHeight和requestWidth

    WebCamDevice有一个属性isFrontFacing,是否是前置摄像头

     

    四、Microphone加AudioClip

    Microphone用来获取麦克风,AudioClip存放音频数据float[],AudioSource播放

    麦克风设备启动需要时间,所以这里我启动后等待了2.5秒

    麦克风开始录音:

    AudioClip = Microphone.Start(设备名, 是否循环录制, 单次录制时长(秒), 采样率(我用的是8000Hz,就是一般电话的采样率,人声清晰))

     

    五、数据压缩

    这个算法适用于重复率高的数据,然而这里的数据重复率并不高,所以压缩率大概80%左右,用的是GZip的dll

     

    以下是部分源码

    服务器:

    const int Port = 8010;
    static Socket socket;
    static List<string> connected = new List<string>();
    
    static void Main(string[] args)
    {
        try{
            socket = new Socket(AddressFamiy.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            IPAddress IP = IPAddress.Any;
            IPEndPoint endPoint = new IPEndPoint(IP, Port);
            Socket.Bind(endPoint);
            Console.WriteLine(“服务器已开启”);
            Thread t = new Thread(ReciveMsg);
            t.IsBackground = true;
            t.Start();
        }
        catch(Exception e)
        {
            Console.WriteLine(“服务器开启失败!e:” + e);
        }
    }
    
    /// <summary>
    /// 接收发送给本机ip对应端口号的数据
    /// </summary>
    static void ReciveMsg()
    {
        while (true)
        {
            try
            {
                EndPoint point = new IPEndPoint(IPAddress.Any, 0);//用来保存发送方的ip和端口号
                byte[] buffer = new byte[1024 * 1024];
                int length = server.ReceiveFrom(buffer, ref point);//接收数据报
                byte[] useBytes = new byte[length];
                Array.Copy(buffer, 0, useBytes, 0, length);
                if (!connected.Contains(point.ToString()))
                {
                    connected.Add(point.ToString());
                    Console.WriteLine(point.ToString() + "连接成功!");
                }
                for (int i = connected.Count - 1; i >= 0; i--)
                {
                    string ip = connected[i];
                    if (!ip.Equals(point.ToString()))
                    {
                        try
                        {
                            string[] split = ip.Split(':');
                            IPAddress IP = IPAddress.Parse(split[0]);
                            int port = int.Parse(split[1]);
                            EndPoint otherEndPoint = new IPEndPoint(IP, port);
                            server.SendTo(useBytes, otherEndPoint);
                            Console.WriteLine("转发成功, IP:" + ip);
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine("转发失败,e:" + e);
                            connected.RemoveAt(i);
                        }
                    }
                }
            }
            catch(Exception e)
            {
                Console.WriteLine("接收失败! e:" + e);
            }
        }
    }

    服务器和客户端参考引用了https://blog.csdn.net/dl962454/article/details/78990735这位大佬的代码

     

    客户端:

    接收数据:

    ///用来保存发送方的ip和端口号
    
    EndPoint point = new IPEndPoint(IPAddress.Any, 0);
    
    ///定义客户端接收到的信息大小
    
    byte[] buffer = new byte[1024 * 1024];
    
    Debug.Log("Before---ReceiveFrom");
    
    ///接收到的信息大小(所占字节数)
    
    int length = client.ReceiveFrom(buffer, ref point);
    
    byte[] singlePackage = new byte[length];
    
    Array.Copy(buffer, 0, singlePackage, 0, length);

    发送数据:

    // 分包
    List<byte[]> list = SocketBytesUtils.GetByetsList(buffer, id);
    
    // 分包后的数据,依次发送
    for (int i = 0; i < list.Count; i++)
    {
        client.SendTo(list[i], serverEndPoint);
        // 等1毫秒
        await Task.Delay(1);
    }

    检查单个包,组成完整的一条数据,就组包存起来,使用后回收

    这里是检查的代码:

        private void CheckSinglePackage(byte[] singlePackage)
        {
            if (singlePackage.Length <= SocketBytesUtils.GetHeadLength())
                return;
    
            int id = SocketBytesUtils.GetReceiveByteID(singlePackage);
            int count = SocketBytesUtils.GetReceivePackageCount(singlePackage);
            int index = SocketBytesUtils.GetReceivePackageIndex(singlePackage);
            SocketPackage socketPackage;
            if (packageCacha.ContainsKey(id))
            {
                socketPackage = packageCacha[id];
            }
            else
            {
                socketPackage = new SocketPackage();
                socketPackage.ID = id;
                socketPackage.count = count;
                socketPackage.list = new List<byte[]>();
                socketPackage.useByteLength = 0;
                packageCacha.Add(id, socketPackage);
            }
            socketPackage.AddBytes(singlePackage);
            // 是否是结束数据,即没有丢包,正常结束的完整数据
            bool endBytes = false;
            // 是否是错误数据
            bool errorBytes = false;
    
            // 本次发送的数据包量和之前的不一致,是错误数据
            if (socketPackage.count != count)
            {
                errorBytes = true;
            }
            else
            {
                // 是最后一条
                if (index == count - 1)
                {
                    // list长度和count一致
                    if (socketPackage.list.Count == count)
                    {
                        // 有效数据
                        receivedBuffer = SocketBytesUtils.GetReceiveUseBytes(socketPackage.list, socketPackage.useByteLength);
                        endBytes = true;
                    }
                    else
                    {
                        // 错误数据
                        errorBytes = true;
                    }
                }
            }
    
            // 是结束的那个包 或 发现了丢包、错误数据
            if (endBytes || errorBytes)
            {
                // 删除此ID的缓存包
                packageCacha.Remove(id);
            }
    
            Debug.Log("CheckSinglePackage----End");
        }

    单个数据包的信息:

    public class SocketPackage
    {
        public int ID;
        public int count;
        public int useByteLength;
        public List<byte[]> list;
        public void AddBytes(byte[] bytes)
        {
            list.Add(bytes);
            useByteLength += bytes.Length - SocketBytesUtils.GetHeadLength();
        }
    }

     

    分包组包:

    using System;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class SocketBytesUtils
    {
        public static int MAX_BUFFER_LENGTH = 65045;
        public static int ID_LENGTH = 15;
        public static int LIST_COUNT = 15;
        public static int LIST_INDEX = 15;
    
        /// <summary>
        /// 分包,每个数据包包括:
        /// 用户ID + 包数量 + 此包索引 + 本包内有效数据
        /// </summary>
        /// <param name="buffer"></param>
        /// <returns></returns>
        public static List<byte[]> GetByetsList(byte[] buffer, int id)
        {
            // 有效数据长度    = 最大传输长度      - ID占位    - 包数量     - 包索引
            int singleUseBytes = MAX_BUFFER_LENGTH - ID_LENGTH - LIST_COUNT - LIST_INDEX;
    
            // 包数量
            int listCount = buffer.Length / singleUseBytes;
            listCount = buffer.Length % singleUseBytes <= 0 ? listCount : listCount + 1;
    
            // ID -> bytes
            byte[] idFullBytes = new byte[ID_LENGTH];
            IntToBytes(id, idFullBytes);
    
            // 包数量 -> bytes
            byte[] listCountFullBytes = new byte[LIST_COUNT];
            IntToBytes(listCount, listCountFullBytes);
    
            // 初始化list
            List<byte[]> bytesList = new List<byte[]>();
            for (int i = 0; i < listCount; i++)
            {
                // 包索引 -> bytes
                byte[] listIndexFullBytes = new byte[LIST_INDEX];
                IntToBytes(i, listIndexFullBytes);
    
                // 拼包
                // 单个包所有数据
                byte[] singleFullBytes = new byte[MAX_BUFFER_LENGTH];
    
                // 最后一条,长度为剩余的所有byte
                if (i == listCount - 1)
                {
                    singleUseBytes = buffer.Length - singleUseBytes * i;
                    singleFullBytes = new byte[singleUseBytes + GetHeadLength()];
                }
    
                // 复制id数据
                int startCopyIndex = 0;
                Array.Copy(idFullBytes, 0, singleFullBytes, startCopyIndex, idFullBytes.Length);
    
                // 复制包数量数据
                startCopyIndex += idFullBytes.Length;
                Array.Copy(listCountFullBytes, 0, singleFullBytes, startCopyIndex, listCountFullBytes.Length);
    
                // 复制包索引数据
                startCopyIndex += listCountFullBytes.Length;
                Array.Copy(listIndexFullBytes, 0, singleFullBytes, startCopyIndex, listIndexFullBytes.Length);
    
                // 复制本包有效数据
                startCopyIndex += listIndexFullBytes.Length;
    
                Array.Copy(buffer, (MAX_BUFFER_LENGTH - GetHeadLength()) * i, singleFullBytes, startCopyIndex, singleUseBytes);
    
                // 添加到list内
                bytesList.Add(singleFullBytes);
            }
    
            return bytesList;
        }
    
        /// <summary>
        /// 获取接收到数据的用户ID
        /// </summary>
        /// <param name="buffer"></param>
        /// <returns></returns>
        public static int GetReceiveByteID(byte[] buffer)
        {
            byte[] idBytes = new byte[ID_LENGTH];
            Array.Copy(buffer, 0, idBytes, 0, ID_LENGTH);
            return BytesToInt32(idBytes);
        }
    
        /// <summary>
        /// 获取接收到数据的包数量
        /// </summary>
        /// <param name="buffer"></param>
        /// <returns></returns>
        public static int GetReceivePackageCount(byte[] buffer)
        {
            byte[] countBytes = new byte[LIST_COUNT];
            Array.Copy(buffer, ID_LENGTH, countBytes, 0, LIST_COUNT);
            return BytesToInt32(countBytes);
        }
    
        /// <summary>
        /// 获取接收到数据的包索引
        /// </summary>
        /// <param name="buffer"></param>
        /// <returns></returns>
        public static int GetReceivePackageIndex(byte[] buffer)
        {
            byte[] indexBytes = new byte[LIST_INDEX];
            Array.Copy(buffer, ID_LENGTH + LIST_COUNT, indexBytes, 0, LIST_INDEX);
            return BytesToInt32(indexBytes);
        }
    
        /// <summary>
        /// 组包
        /// </summary>
        /// <param name="packageList"></param>
        /// <param name="useByteLength"></param>
        /// <returns></returns>
        public static byte[] GetReceiveUseBytes(List<byte[]> packageList, int useByteLength)
        {
            //Debug.LogError("GetReceiveUseBytes: listCount(" + packageList.Count + "), useByteLength(" + useByteLength + ")");
            //Debug.LogError("GetReceiveUseBytes: lastBytesLength(" + packageList[packageList.Count - 1].Length + ")");
            byte[] useBytes = new byte[useByteLength];
            // 单个包有效数据起点
            int headLength = GetHeadLength();
            for (int i = 0; i < packageList.Count; i++)
            {
                byte[] singlePackage = packageList[i];
                // 单个包内有效数据长度
                int singleUseLength = singlePackage.Length - headLength;
                // 此包的索引,一般情况下与i相等
                int singleIndex = GetReceivePackageIndex(singlePackage);
                // 单个包内有效数据最大长度
                int singleMaxUseLength = MAX_BUFFER_LENGTH - headLength;
                // 最后一个包
                if (i == packageList.Count - 1)
                {
                    // 有效数据 = 接收数组长度 - 之前所有数据长度
                    singleUseLength = useBytes.Length - singleIndex * singleMaxUseLength;
                }
    
                if (singleUseLength > useBytes.Length - singleIndex * singleMaxUseLength || singleUseLength <= 0)
                {
                    Debug.LogError("数据接收错误");
                }
                // 依次拷贝进useBytes
                Array.Copy(singlePackage, headLength, useBytes, singleIndex * singleMaxUseLength, singleUseLength);
            }
            return useBytes;
        }
    
        /// <summary>
        /// bytes转int
        /// </summary>
        /// <returns></returns>
        public static int BytesToInt32(byte[] bytes)
        {
            int id = BitConverter.ToInt32(bytes, 0);
            return id;
        }
    
        /// <summary>
        /// 获取包头长度
        /// </summary>
        /// <returns></returns>
        public static int GetHeadLength()
        {
            return ID_LENGTH + LIST_COUNT + LIST_INDEX;
        }
    
        /// <summary>
        /// Int转Bytes
        /// </summary>
        /// <param name="value"></param>
        /// <param name="fullBytes"></param>
        public static void IntToBytes(int value, byte[] fullBytes)
        {
            // 清空数据
            Array.Clear(fullBytes, 0, fullBytes.Length);
            //Convert int to bytes
            byte[] bytesToSendCount = BitConverter.GetBytes(value);
            //Copy result to fullBytes
            bytesToSendCount.CopyTo(fullBytes, 0);
        }
    }

    这个没有参考,按自己需求写的,转载的话请标注出处。

     

    RawImage加WebCamTexture

    对象:

    WebCamTexture webCam;
    Texture2D currTexture;
    RawImage mSelfCamera;

    初始化:

    webCam = new WebCamTexture();
    webCam.deviceName = GetFrontFacingName();
        
    webCam.requestedHeight = 300;
    webCam.requestedWidth = 300;
    
    mSelfCamera.texture = webCam;
        
    webCam.Play();
        
    currTexture = new Texture2D(webCam.width, webCam.height);

    获取图像数据:

    currTexture.SetPixels(webCam.GetPixels());
    byte[] pngBytes = currTexture.EncodeToPNG();

    显示图像数据:

    bool result = tex.LoadImage(pngBytes);
    if (result)
    {
        targetImage.texture = tex;
    }
    else
    {
        Debug.LogWarning("此图片数据错误,无法加载,不做显示-----------");
    }

     

    Microphone加AudioClip

    对象:

    AudioClip mAudioClip;
    string mDevice;

    初始化:

    mDevice = Microphone.devices[0];
    mAudioClip = Microphone.Start(mDevice, true, 60, AudioUtils.FREQUENCY);
    // 这里等待2.5秒,启动麦克风,源码不贴,可以用协程、异步、Sleep啥的

    获取音频数据:

    float[] audioFloats = new float[point - lastPoint];
    mAudioClip.GetData(audioFloats, lastPoint);
    lastPoint = point;
    // AudioClip数据读出来是float[],转换成byte[]
    byte[] audioBytes = AudioUtils.FloatToByte(audioFloats);
    audioBytes = CompressedUtils.GZipCompress(audioBytes);

    播放音频:

    AudioSource mAudioSource;
    audioBytes = CompressedUtils.GZipDeCompress(audioBytes);
    AudioClip audioClip = AudioUtils.AudioByteToClip(audioBytes, 1, AudioUtils.FREQUENCY);
    mAudioSource.clip = audioClip;
    mAudioSource.loop = false;
    mAudioSource.Play();

    下面是AudioUtils,操作音频数据:

    using System;
    using UnityEngine;
    
    public class AudioUtils
    {
        public static int FREQUENCY = 8000;
    
        /// <summary>
        /// 音频转bytes
        /// </summary>
        /// <param name="clip"></param>
        /// <returns></returns>
        public static byte[] FloatToByte(float[] data)
        {
            int rescaleFactor = 32767; //to convert float to Int16
            byte[] outData = new byte[data.Length * 2];
            for (int i = 0; i < data.Length; i++)
            {
                short temshort = (short) (data[i] * rescaleFactor);
                byte[] temdata = BitConverter.GetBytes(temshort);
                outData[i * 2] = temdata[0];
                outData[i * 2 + 1] = temdata[1];
            }
            return outData;
        }
    
        /// <summary>
        /// 音频转bytes
        /// </summary>
        /// <param name="clip"></param>
        /// <returns></returns>
        public static byte[] AudioClipToByte(AudioClip clip)
        {
            float[] data = new float[clip.samples];
            clip.GetData(data, 0);
            return FloatToByte(data);
        }
    
        /// <summary>
        /// bytes转音频
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public static AudioClip AudioByteToClip(byte[] bytes, int channels, int frequency)
        {
            if (bytes.Length <= 0)
            {
                return null;
            }
    
            float[] data = new float[bytes.Length / 2];
            for (int i = 0; i < data.Length; i++)
            {
                data[i] = ByteToFloat(bytes[i * 2], bytes[i * 2 + 1]);
            }
            //data = NoiseReduction(data);
            AudioClip clip = AudioClip.Create("Clip", data.Length, channels, frequency, false);
            clip.SetData(data, 0);
            return clip;
        }
    
        /// <summary>
        /// byte转float
        /// </summary>
        /// <param name="firstByte"></param>
        /// <param name="secondByte"></param>
        /// <returns></returns>
        private static float ByteToFloat(byte firstByte, byte secondByte)
        {
            // convert two bytes to one short (little endian)
            //小端和大端顺序要调整
            short s;
            if (BitConverter.IsLittleEndian)
                s = (short) ((secondByte << 8) | firstByte);
            else
                s = (short) ((firstByte << 8) | secondByte);
            // convert to range from -1 to (just below) 1
            return s / 32768.0F;
        }
    }

    复制了这里大佬的代码:https://blog.csdn.net/BDDNH/article/details/103381518

    压缩:

    using ICSharpCode.SharpZipLib.GZip;
    public static byte[] GZipCompress(byte[] bytesToCompress)
    {
        // 其他地方有源码,这里就不贴了
    }
        public static byte[] GZipDeCompress(byte[] data)
    {
        // 其他地方有源码,这里就不贴了
    }

    本文作于2020年10月27日。

    本文链接:https://blog.csdn.net/qq_28873519/article/details/109323470

    源码链接:https://download.csdn.net/download/qq_28873519/14919343

    如转载请注明出处。

    展开全文
  • 在日常生活中,某些没法实现联网通话的场所,很多物联网设备都会采用局域网通信的方式构件音视频通话场景,就是不通过云端,通过内网的形式实现沟通。 常见的应用场景 最普遍的应用是在楼宇对讲上。市面上很多楼宇...
  • 利用labview的DataSocket实现局域网内多人同时语音通话,使用前需求先启动DataSocket Server
  • 有的人想实现局域网内实时语音通话的功能,如果电脑可以连接互联网的话,可安装QQ,电脑版微信等进行实时语音聊天,如果电脑只连接了内网,并未连接因特网,该怎么实现呢?其实不难,只需要通过局域网声音传输软件,...
  • //将文件流读取到数组中, public static byte[] getContent(String filePath) throws IOException { File file = new File(filePath); long fileSize = file.length(); if (fileSize > Integer.MAX_VALUE) {...
  • Java实现的简单网络电话,可以实现局域网内的通话
  • 局域网内的Android系统两个apk可视通话,只要把apk安装到两个手机中,两个手机wifi连接同一个局域网,互相输入对方IP即可通信
  • userId 和 password 为上一步中的分机(局域网内使用的电话)都是101,点击OK之后 如果你的miniSIPServer中的分机101变为激活状态,那么恭喜你! 五,使用HTC和华为手机进行局域网内通讯      ...

空空如也

空空如也

1 2 3 4 5 ... 14
收藏数 262
精华内容 104
关键字:

局域网通话