PhoneCall

此项目为基于netty框架实现的局域网内的ip电话,实现了先录音然后进行混音的功能,并且在github上传了demo可以进行测试。

一、介绍

基于netty框架开发的局域网IP电话,用户输入对方IP地址便能够进行语音通话。为了实现网络连接与Activity无关,使用service管理netty对象,现阶段可以在APP的任意界面监听到打电话请求,并跳转到响铃界面。另外,本项目将需要的操作封装到了ApiProvider类中。 本来打算做个群组通话,但是只实现了音频合成功能。细节参照github项目PhoneCall

ps:本项目是在VideoCalling上进行改进的,该项目是实现局域网的视频传输,我将其语音传输抽取出来进行更改实现语音通话,感谢作者的无私贡献。

测试

Releases v3为最新版本,将两个手机连接到同一个局域网中,一方输入对方IP便可以进行语音通话。网络监听线程设置在Service中,当监听到打电话请求便会直接跳转到响铃界面。

实现思路

首先使用AudioRecord进行音频录制,使用speex进行降噪并编码生成语音流,然后使用socket发送给出去, 接受方收到语音数据后将进行解码,然后使用AudioTrack播放。其中,电话交互逻辑使用文本信令控制。

对于群组通话,假设有ABC三人进行通话,首先A开启一个聊天室,然后BC加入聊天室,此时BC只需将自己的语音流发送给A,然后在A进行语音的合成操作,将合成的语音在本地播放和发送给BC即可。

界面展示

项目架构

  • audio包:进行音频的录制、编码、解码、播放操作
  • net包:网络连接的包
    • CallSingal:定义电话信令,如拨打电话操作
    • Message: 传输数据,包括字节与音流和文字
    • NettyClient: netty网络连接代理
    • NettyReceiverHandler: 处理发送数据和接受数据,定义接口回调返回语音信息和电话信令信息
  • ApiProvider: 提供网络发送API,音频播放和录制API,连接断开等API。整个项目的入口文件。
  • mixAudioUtils: 混音用的工具类
  • MultiVoIPActivity:实现混音界面,需要录制两端音频然后点击混音按钮,之后点击输出混音即可播放
  • VoipP2PActivity:IP电话的主要界面,因为需要监听打电话的请求,但是不会写service进行后台监听,所以就在一个activity中写了五个界面进行切换…以后有机会可能会改

控制信令逻辑图

二、代码

0. API

打电话的逻辑是基于以下API实现,包括音频的录制与播放,语音流的发送与接收,连接的断开与关闭,以上包括了实现PhoneCall所需要的全部功能。

public class ApiProvider {/***  注册回调,处理接收到的音频和文本。* @param callback 回调变量。*/public void registerFrameResultedCallback(NettyReceiverHandler.FrameResultedCallback callback){}/*** 发送音频数据* @param data 音频流*/public void sendAudioFrame(byte[] data) {}/***  通过设置默认IP进行发送数据。* @param msg 消息*/public void sentTextData(String msg) {}/*** 通过指定IP发送文本信息* @param targetIp 目标IP* @param msg 文本消息。*/public void UserIPSentTextData(String targetIp, String msg) {}/*** 通过指定IP发送音频信息* @param targetIp 目标IP* @param data 数据流*/public void UserIpSendAudioFrame(String targetIp ,byte[] data) {}/*** 关闭Netty客户端,*/public void shutDownSocket(){}/***  关闭连接,打电话结束* @return true or false*/public boolean disConnect(){}/***  获取目标地址* @return 此时目标地址。*/public String getTargetIP() {}/***  设置目标地址* @param targetIP 设置目标地址。*/public void setTargetIP(String targetIP) {}/*** 开始录音 在开始以下操作之前,必须先把目标IP设置对,否则会出现问题。*/public void startRecord(){}/*** 停止录音*/public void  stopRecord(){}/***  录音线程是否正在录音* @return true 正在录音 or false 没有在录音*/public boolean isRecording(){}/*** 开始播放音频*/public void startPlay(){}/*** 停止播放音频*/public void stopPlay(){}/***  是否正在播放* @return true 正在播放;  false 停止播放*/public boolean isPlaying(){}/***  开启录音与播放*/public void startRecordAndPlay(){}/*** 关闭录音与播放*/public void stopRecordAndPlay(){}
}

1. 监听端口

每个客户端在启动时,都需要初始化一个Netty客户端进行监听请求,当收到请求以后,需要捕获发送方IP,然后进行主动的回复。

Bootstrap b = new Bootstrap();
group = new NioEventLoopGroup();
try {//设置netty的连接属性。b.group(group).channel(NioDatagramChannel.class) //异步的 UDP 连接.option(ChannelOption.SO_BROADCAST, true).option(ChannelOption.SO_RCVBUF, 1024 * 1024)//接收区2m缓存.option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(65535))//加上这个,里面是最大接收、发送的长度.handler(handler); //设置数据的处理器b.bind(localPort).sync().channel().closeFuture().await();
} catch (Exception e) {e.printStackTrace();
} finally {group.shutdownGracefully();
}

2. 数据传输

主要包括两种数据:

  1. 数字信令(Integer):建立连接过程中的控制数字,发送的时候被转成String类型,(为了兼容Handler只能发送数字)。
  2. 语音数据:通话中的语音数据

每次传输需要判断需要发送的是什么类型的数据,做相应的处理后装入运输载体Message对象中,最后用ChannelHandlerContext对象将转换为Json格式的Message对象发送至目标IP地址相应的端口。

//发送数据。
public void sendData(String ip, int port, Object data, String type) {Message message = null;if (data instanceof byte[]) {message = new Message();message.setFrame((byte[]) data);message.setMsgtype(type);message.setTimestamp(System.currentTimeMillis());}else if (data instanceof String){message = new Message();message.setMsgBody((String) data);message.setMsgtype(type);message.setTimestamp(System.currentTimeMillis());message.setMsgIp(MLOC.localIpAddress);}if (channelHandlerContext != null) {channelHandlerContext.writeAndFlush(new DatagramPacket(Unpooled.copiedBuffer(JSON.toJSONString(message).getBytes()),new InetSocketAddress(ip, port)));}}

在进行接收数据的时候也是需要进行相同判断操作,然后进行数据的获取。

//接收数据。ByteBuf buf = (ByteBuf) packet.copy().content(); //字节缓冲区byte[] req = new byte[buf.readableBytes()];buf.readBytes(req);String str = new String(req, "UTF-8");Message message = JSON.parseObject(str,Message.class);Netty框架中有一个类SimpleChannelInboundHandler,主要是对监听的端口传来的数据进行处理的。自定义一个继承自它的类,并重写处理收到数据的方法channelRead0(),将数据写入Message对象。//发送文字类型信息回调if (message.getMsgtype().equals(Message.MES_TYPE_NOMAL)){if (frameCallback !=null){frameCallback.onTextMessage(message.getMsgBody());frameCallback.onGetRemoteIP(message.getMsgIp());}}else if (message.getMsgtype().equals(Message.MES_TYPE_AUDIO)){//发送语音数据接口回调if (frameCallback !=null){frameCallback.onAudioData(message.getFrame());}}

3. 数字信令

总共的控制代码有三种:

// control text
public static final Integer PHONE_MAKE_CALL = 100; // make call
public static final Integer PHONE_ANSWER_CALL = 200; // answer call
public static final Integer PHONE_CALL_END = 300; //  call end

假设A向B进行打电话:

  1. 当B收到PHONE_MAKE_CALL后,B需要先判断此时自己是否正忙(正在打电话),如果不忙则跳到响铃界面,否则直接丢包。
  2. 当A收到B的PHONE_ANSWER_CALL后,则直接显示对话界面,开始录音并且将接电话标识设置为true。
  3. 当A或者B收到PHONE_CALL_END后,此时需要判断发出此条结束消息的来源是否是正在通话的客户,防止在第三方进行呼叫是出现错误挂断的情形。
// 状态切换逻辑
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {public void handleMessage(Message msg) {//根据标志记性自定义的操作,这个操作可以操作主线程。if (msg.what == PHONE_MAKE_CALL) { //收到打电话的请求。if (!isBusy){ //如果不忙 则跳转到通话界面。showRingView(); //跳转到响铃界面。isBusy = true;}}else if (msg.what == PHONE_ANSWER_CALL){ //接听电话showTalkingView();provider.startRecordAndPlay();isAnswer = true; //接通电话为真}else if (msg.what == PHONE_CALL_END){ //收到通话结束的信息if (newEndIp.equals(provider.getTargetIP())){showBeginView();isAnswer = false;isBusy = false;provider.stopRecordAndPlay();timer.stop();}}}
};

4.混音

混音采用的是平均混音算法,通过测试可以实现混音。主要涉及到安卓文件的创建和以字节流的方式进行文件的读取。

public static byte[] averageMix(String file1,String file2) throws IOException {byte[][] bMulRoadAudioes =  new byte[][]{FileUtils.getContent(file1),    //第一个文件FileUtils.getContent(file2)     //第二个文件};byte[] realMixAudio = bMulRoadAudioes[0]; //保存混音之后的数据。Log.e("ccc", " bMulRoadAudioes length " + bMulRoadAudioes.length); //2//判断两个文件的大小是否相同,如果不同进行补齐操作for (int rw = 0; rw < bMulRoadAudioes.length; ++rw) { //length一直都是等于2.依次检测file长度和file2长度if (bMulRoadAudioes[rw].length != realMixAudio.length) {Log.e("ccc", "column of the road of audio + " + rw + " is diffrent.");if (bMulRoadAudioes[rw].length<realMixAudio.length){realMixAudio = subBytes(realMixAudio,0,bMulRoadAudioes[rw].length); //进行数组的扩展}else if (bMulRoadAudioes[rw].length>realMixAudio.length){bMulRoadAudioes[rw] = subBytes(bMulRoadAudioes[rw],0,realMixAudio.length);}}}int row = bMulRoadAudioes.length;       //行int column = realMixAudio.length / 2;   //列short[][] sMulRoadAudioes = new short[row][column];for (int r = 0; r < row; ++r) {         //前半部分for (int c = 0; c < column; ++c) {sMulRoadAudioes[r][c] = (short) ((bMulRoadAudioes[r][c * 2] & 0xff) | (bMulRoadAudioes[r][c * 2 + 1] & 0xff) << 8);}}short[] sMixAudio = new short[column];int mixVal;int sr = 0;for (int sc = 0; sc < column; ++sc) {mixVal = 0;sr = 0;for (; sr < row; ++sr) {mixVal += sMulRoadAudioes[sr][sc];}sMixAudio[sc] = (short) (mixVal / row);}//合成混音保存在realMixAudiofor (sr = 0; sr < column; ++sr) { //后半部分realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF);realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8);}//保存混合之后的pcmFileOutputStream fos = null;//保存合成之后的文件。File saveFile = new File(FileUtils.getFileBasePath()+ "averageMix.pcm" );if (saveFile.exists()) {saveFile.delete();}fos = new FileOutputStream(saveFile);// 建立一个可存取字节的文件fos.write(realMixAudio);fos.close();// 关闭写入流return realMixAudio; //返回合成的混音。}//合并两个音轨。private static byte[] subBytes(byte[] src, int begin, int count) {byte[] bs = new byte[count];System.arraycopy(src, begin, bs, 0, count);return bs;}

传入文件名称,返回文件内容的字节流

    //将文件流读取到数组中,public static byte[] getContent(String filePath) throws IOException {File file = new File(filePath);long fileSize = file.length();if (fileSize > Integer.MAX_VALUE) {Log.d("ccc","file too big...");return null;}FileInputStream fi = new FileInputStream(file);byte[] buffer = new byte[(int) fileSize];int offset = 0;int numRead = 0;//while循环会使得read一直进行读取,fi.read()在读取完数据以后会返回-1while (offset < buffer.length&& (numRead = fi.read(buffer, offset, buffer.length - offset)) >= 0) {offset += numRead;}//确保所有数据均被读取if (offset != buffer.length) {throw new IOException("Could not completely read file "+ file.getName());}fi.close();return buffer;}

万水千山总是情,点个star行不行~

具体细节请参照我的github项目PhoneCall,千山万水总是情,点个star行不行。
可以转载,转载请注明出处谢谢~

基于Netty的Android局域网IP电话相关推荐

  1. 基于智能移动设备的IP电话软件的设计与实现

     摘  要 介绍了以Pocket PC2003为操作系统,带WiFi功能的智能移动设备平台下IP电话软件的设计方案与实现方法.实践证明,本软件具有实时性好.移植性强等优点.     关键字 智能移动设 ...

  2. 基于SIP协议的IP电话系统设计与实现

    网络IP电话不仅具有成本低廉.网络资源利用率高等诸多优点,而且还可以进一步集成多媒体信息(包括语音.图像.数据等),以实现交互式的实时通信等,具有很大的发展潜力,且有逐渐取代传统PSTN电话的趋势,成 ...

  3. 基于RTP协议的IP电话QoS监测及提高策略

    基于RTP协议的IP电话QoS监测及提高策略 本文转自 http://jxic.jiangxi.gov.cn/Html/2008321143656-1.html 1. 概述  随着Internet和多 ...

  4. 基于局域网IP的考勤系统设计与实现

    基于局域网IP的考勤系统设计与实现 作者:不染心 时间:2022/7/2 项目地址: https://mbd.pub/o/author-aWaVlmpkYw==/work 文章目录 基于局域网IP的考 ...

  5. 基于 P2P 技术的 Android 局域网内设备通信实践

    Android 局域网内的多设备通信方式有多种,其中常见的方式有: 基于 TCP/UDP 的 Socket 通信 基于 Bluetooth 的近场通信 基于 Wifi 的 Wi-Fi Direct 连 ...

  6. 基于SIP协议的IP电话增值业务实现技术

    基于SIP协议的IP电话增值业务实现技术 王瑜,乐正友 (清华大学电子工程系,北京 100084)    摘  要:讨论了SIP协议以及基于SIP协议的IP电话增值业务实现技术,并对SIP CGI.C ...

  7. 如何利用局域网的资源打内线IP电话

    摘要:本文系统介绍局域网中IP电话的实现原理:在MCU中如何实现Lean TCP/IP协议;对电话的信令和语音信号怎样打成IP包,进行了具体阐述:并给出在实际中如何选出相应的支持芯片及完整的硬件设计模 ...

  8. android局域网聊天毕业设计,Android基于wifi模块的局域网聊天以及文件传输app

    [实例简介] 一款基于wifi模块的局域网实时聊天以及文件互传的安卓app,能实现热点创建,热点连接,文件传输,实时通讯等功能. [实例截图] [核心代码] MyFeiGe2.0 └── MyFeiG ...

  9. 为什么局域网IP通常以192.168开头而不是1.2或者193.169?

    点击上方"Java基基",选择"设为星标" 做积极的人,而不是积极废人! 每天 14:00 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | Java ...

最新文章

  1. MATLAB 的条件分支语句
  2. XSS攻击之窃取Cookie
  3. patricia tree_前5名:专访Patricia Torvalds和Ada Initiative,印度采用开源,等等
  4. [多重背包+二进制优化]HDU1059 Dividing
  5. 英雄无敌6服务器在哪个文件夹,Win7系统无法运行英雄无敌6的两种原因和解决方法...
  6. linux内网发现登录设备,LINUX 内网设备将服务映射到公网地址
  7. kind富文本编辑器_kind富文本编辑器
  8. 《Essential C++》笔记之return;分析
  9. 常见危险函数和特殊函数(二)----变量覆盖
  10. Windows 8和CentOS 6.4(64)双系统硬盘安装教程
  11. 如何使用notepad++查看二进制bin文件
  12. 智能合约的形式化描述、分析和验证
  13. 博图如何上载wincc程序_博途Wincc:新手4分钟学会两种VB语句,实现西门子Wincc V14 判断功能...
  14. google linux桌面快捷方式,centos7 rhel7 linux下怎么安装google chrome 设置谷歌浏览器桌面快捷方式...
  15. win10系统CAJViewer 绿色提示缺少由于找不到 MSVCR71.dll
  16. 圆的半径java_计算圆的半径
  17. 三维马氏距离_马氏距离2
  18. TeXStudio 中如何调用Gnuplot
  19. 2018中国开源开发者调查问卷
  20. 图像处理----形态学滤波

热门文章

  1. 和老外聊天的几个网站
  2. Java for Web学习笔记(三五):自定义tag(3)TLDS和Tag Handler
  3. udk开发-稀里糊涂
  4. 面试-Java【之】(revers)递归实现字符串倒序排列(详解)
  5. 2023 云海Chatgtp个人商业源码
  6. android界面设计的解剖,ps cc 2017启动画面的制作解剖
  7. 清晰明了,什么是贝叶斯定理?朴素贝叶斯又是什么?
  8. 阿里云 RAM 企业上云实战
  9. Linux那些事儿之我是Hub(4)
  10. 高德地图放大Marker icon