├── .gitignore ├── .idea ├── caches │ └── build_file_checksums.ser ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README-En.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ └── armeabi-v7a │ │ └── libspeex.so ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── proposeme │ │ └── seven │ │ └── phonecall │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ ├── gyz │ │ │ └── voipdemo_speex │ │ │ │ └── util │ │ │ │ └── Speex.java │ │ │ └── proposeme │ │ │ └── seven │ │ │ └── phonecall │ │ │ ├── MainActivity.java │ │ │ ├── MultiVoIpActivity.java │ │ │ ├── VoIpP2PActivity.java │ │ │ ├── audio │ │ │ ├── AudioConfig.java │ │ │ ├── AudioData.java │ │ │ ├── AudioDecoder.java │ │ │ ├── AudioEncoder.java │ │ │ ├── AudioPlayer.java │ │ │ └── AudioRecorder.java │ │ │ ├── net │ │ │ ├── BaseData.java │ │ │ ├── IPSave.java │ │ │ ├── Message.java │ │ │ ├── NettyClient.java │ │ │ └── NettyReceiverHandler.java │ │ │ ├── provider │ │ │ └── ApiProvider.java │ │ │ ├── service │ │ │ └── VoIPService.java │ │ │ └── utils │ │ │ ├── NetUtils.java │ │ │ ├── PermissionManager.java │ │ │ └── mixAduioUtils │ │ │ ├── AudioUtil.java │ │ │ ├── FileUtils.java │ │ │ └── MixAudioUtil.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── btn_style_white.xml │ │ ├── btn_style_white_normal.xml │ │ ├── btn_style_white_pressed.xml │ │ ├── ic_launcher_background.xml │ │ ├── icon_hangup.png │ │ ├── icon_mic_picup.png │ │ └── starfox_500.png │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_multi_voip.xml │ │ └── activity_voip_p2p.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── comments.png │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── phone.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── proposeme │ └── seven │ └── phonecall │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pictures ├── call.png ├── callsignal.png ├── errorsCall.png └── zhifubao.JPG └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/libraries 5 | /.idea/modules.xml 6 | /.idea/workspace.xml 7 | .DS_Store 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostStarTvT/PhoneCall/8663db491089ed91d9cbdf89b9451b18d3c876dd/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README-En.md: -------------------------------------------------------------------------------- 1 | # PhoneCall 2 | # 1. Introduce 3 | 4 | This is a LAN IP phone developed based on the Netty framework, the user can enter the IP address of the other party to make a phone call. In order to realize that the network connection is independent of Activity, the service is used to manage the netty object. At this stage, the call request can be monitored on any interface of the application and jump to the ringing interface. In addition, this project encapsulates the required operations into the ApiProvider class. 5 | 6 | PS: This project is improved on [VideoCalling](https://github.com/xmtggh/VideoCalling), thanks to the author’s contribution. 7 | 8 | ## Test Demo 9 | 10 | Preparation: Two mobile phones A and B. 11 | 12 | Releases v3 is the latest version. When two mobile phones are connected to the same LAN, one party can enter the other party's IP to make a phone call. The network monitoring thread is set in the Service, and when a call request is monitored, it will directly jump to the ringing interface. 13 | 14 | ## Implement 15 | 16 | Firstly, using `AudioRecord` for recording, and then use the `speex`for audio encoding to generate a byte stream and use `Netty` to send the byte stream. When receiver receive byte steam, the receiver first decodes it, and then uses `AudioTrack` to play it. Besides, the control logic uses number to control. 17 | 18 | ## Interface Display 19 | 20 | ![call.png](https://pic.tyzhang.top/images/2020/10/23/call.png) 21 | ## Project structure 22 | 23 | - audio package: Implement audio recording, encoding, decoding, and playback functions 24 | - net package: Implement network connection 25 | - CallSingal: Define phone control text 26 | - Message: Transmission of data, including bytes and audio streams and text 27 | - NettyClient: netty network connection client 28 | - NettyReceiverHandler: Process sending data and receiving data, use interface callback to return voice and control text 29 | - **ApiProvider: Provide API for sending text and audio streams. The core API file of the project.** 30 | - MultiVoIPActivity: for mixing audio. 31 | - VoipP2PActivity: The main interface of the PhoneCall 32 | 33 | ## control signaling logic diagram 34 | 35 | ![PhoneCallEn.png](https://pic.tyzhang.top/images/2020/10/20/PhoneCallEn.png) 36 | 37 | # 2. Code 38 | ### 0.API 39 | 40 | The logic of making a call is implemented based on the following API, including audio recording and play, voice streaming sending and receiving, connection disconnection and closing. 41 | 42 | ```java 43 | public class ApiProvider { 44 | 45 | public void registerFrameResultedCallback(NettyReceiverHandler.FrameResultedCallback callback){} 46 | 47 | public void sendAudioFrame(byte[] data) {} 48 | 49 | public void sentTextData(String msg) {} 50 | 51 | public void UserIPSentTextData(String targetIp, String msg) {} 52 | 53 | public void UserIpSendAudioFrame(String targetIp ,byte[] data) {} 54 | 55 | public void shutDownSocket(){} 56 | 57 | public boolean disConnect(){} 58 | 59 | public String getTargetIP() {} 60 | 61 | public void setTargetIP(String targetIP) {} 62 | 63 | public void startRecord(){} 64 | 65 | public void stopRecord(){} 66 | 67 | public boolean isRecording(){} 68 | 69 | public void startPlay(){} 70 | 71 | public void stopPlay(){} 72 | 73 | public boolean isPlaying(){} 74 | 75 | public void startRecordAndPlay(){} 76 | 77 | public void stopRecordAndPlay(){} 78 | } 79 | ``` 80 | 81 | ### 1. Listening Port 82 | 83 | Each client needs to monitor the call request and obtain the requester's IP. The following code is to initialize a netty instance. 84 | 85 | ```java 86 | Bootstrap b = new Bootstrap(); 87 | group = new NioEventLoopGroup(); 88 | try { 89 | b.group(group) 90 | .channel(NioDatagramChannel.class) 91 | .option(ChannelOption.SO_BROADCAST, true) 92 | .option(ChannelOption.SO_RCVBUF, 1024 * 1024) 93 | .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(65535)) 94 | .handler(handler); 95 | b.bind(localPort).sync().channel().closeFuture().await(); 96 | } catch (Exception e) { 97 | e.printStackTrace(); 98 | } finally { 99 | group.shutdownGracefully(); 100 | } 101 | ``` 102 | ### 2. Data Transmission 103 | 104 | It mainly includes two kinds of data: 105 | 106 | - Control Number (Integer): Connection control command, and converted to String when sending.(For compatibility with Handler, only numbers can be sent) 107 | - Audio data ( byte[ ] ): Audio binary stream 108 | 109 | Before data transmission, it is necessary to determine the type of data(`String or byte[]`) to be sent, and then encapsulate it into different carriers. Finally, use `ChannelhanlerContext` send to data to the target host (ip + port). 110 | 111 | ```java 112 | public void sendData(String ip, int port, Object data, String type) { 113 | Message message = null; 114 | if (data instanceof byte[]) { 115 | message = new Message(); 116 | message.setFrame((byte[]) data); 117 | message.setMsgtype(type); 118 | message.setTimestamp(System.currentTimeMillis()); 119 | }else if (data instanceof String){ 120 | message = new Message(); 121 | message.setMsgBody((String) data); 122 | message.setMsgtype(type); 123 | message.setTimestamp(System.currentTimeMillis()); 124 | message.setMsgIp(MLOC.localIpAddress); 125 | } 126 | if (channelHandlerContext != null) { 127 | channelHandlerContext.writeAndFlush(new DatagramPacket( 128 | Unpooled.copiedBuffer(JSON.toJSONString(message).getBytes()), 129 | new InetSocketAddress(ip, port))); 130 | } 131 | } 132 | ``` 133 | 134 | Similarly, the receiver needs to make the same judgment when receiving data. 135 | 136 | ```java 137 | ByteBuf buf = (ByteBuf) packet.copy().content(); 138 | byte[] req = new byte[buf.readableBytes()]; 139 | buf.readBytes(req); 140 | String str = new String(req, "UTF-8"); 141 | Message message = JSON.parseObject(str,Message.class); 142 | 143 | // interface callback 144 | if (message.getMsgtype().equals(Message.MES_TYPE_NOMAL)){ 145 | if (frameCallback !=null){ 146 | frameCallback.onTextMessage(message.getMsgBody()); 147 | frameCallback.onGetRemoteIP(message.getMsgIp()); 148 | } 149 | }else if (message.getMsgtype().equals(Message.MES_TYPE_AUDIO)){ 150 | if (frameCallback !=null){ 151 | frameCallback.onAudioData(message.getFrame()); 152 | } 153 | } 154 | ``` 155 | ### 3. Control Number 156 | 157 | There are three types of control number: 158 | 159 | ```java 160 | // control text 161 | public static final Integer PHONE_MAKE_CALL = 100; // make call 162 | public static final Integer PHONE_ANSWER_CALL = 200; // answer call 163 | public static final Integer PHONE_CALL_END = 300; // call end 164 | ``` 165 | 166 | Take A calling B as an example: 167 | 168 | 1. When B receives *PHONE_MAKE_CALL*, B needs to judge whether he is busy at this time, if he is not busy, he jumps to the ringing interface, otherwise the packet is lost directly. 169 | 2. When A receives the *PHONE_ANSWER_CALL* sent by B, A will directly display the dialogue interface, start recording and set the call-receiving flag to true. 170 | 3. When A or B receives *PHONE_CALL_END*, it needs to first determine whether the source of the message is the client that is on the call, so as to prevent the wrong hang-up when a third party makes a call. 171 | 172 | ```java 173 | public void handleMessage(Message msg) { 174 | if (msg.what == PHONE_MAKE_CALL) { // B receive 175 | if (!isBusy){ 176 | showRingView(); 177 | isBusy = true; 178 | } 179 | }else if (msg.what == PHONE_ANSWER_CALL){ // A receive 180 | showTalkingView(); 181 | provider.startRecordAndPlay(); 182 | isAnswer = true; 183 | }else if (msg.what == PHONE_CALL_END){ // Both A and B may receive 184 | if (newEndIp.equals(provider.getTargetIP())){ 185 | showBeginView(); 186 | isAnswer = false; 187 | isBusy = false; 188 | provider.stopRecordAndPlay(); 189 | timer.stop(); 190 | } 191 | } 192 | } 193 | ``` 194 | ### 4、Mixed audio 195 | 196 | The audio mixing algorithm uses a two-dimensional byte array to save two audio streams and then merge them. Need to pass in the name of the saved file: 197 | 198 | ```java 199 | public static byte[] averageMix(String file1,String file2) throws IOException { 200 | 201 | byte[][] bMulRoadAudioes = new byte[][]{ 202 | FileUtils.getContent(file1), // first file 203 | FileUtils.getContent(file2) // second file 204 | }; 205 | 206 | byte[] realMixAudio = bMulRoadAudioes[0]; //save the data after mixing. 207 | Log.e("ccc", " bMulRoadAudioes length " + bMulRoadAudioes.length); //2 208 | 209 | for (int rw = 0; rw < bMulRoadAudioes.length; ++rw) { 210 | if (bMulRoadAudioes[rw].length != realMixAudio.length) { 211 | Log.e("ccc", "column of the road of audio + " + rw + " is diffrent."); 212 | if (bMulRoadAudioes[rw].lengthrealMixAudio.length){ 216 | bMulRoadAudioes[rw] = subBytes(bMulRoadAudioes[rw],0,realMixAudio.length); 217 | } 218 | } 219 | } 220 | 221 | int row = bMulRoadAudioes.length; 222 | int column = realMixAudio.length / 2; 223 | short[][] sMulRoadAudioes = new short[row][column]; 224 | for (int r = 0; r < row; ++r) { 225 | for (int c = 0; c < column; ++c) { 226 | sMulRoadAudioes[r][c] = (short) ((bMulRoadAudioes[r][c * 2] & 0xff) | (bMulRoadAudioes[r][c * 2 + 1] & 0xff) << 8); 227 | } 228 | } 229 | short[] sMixAudio = new short[column]; 230 | int mixVal; 231 | int sr = 0; 232 | for (int sc = 0; sc < column; ++sc) { 233 | mixVal = 0; 234 | sr = 0; 235 | for (; sr < row; ++sr) { 236 | mixVal += sMulRoadAudioes[sr][sc]; 237 | } 238 | sMixAudio[sc] = (short) (mixVal / row); 239 | } 240 | 241 | 242 | for (sr = 0; sr < column; ++sr) { 243 | realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF); 244 | realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8); 245 | } 246 | 247 | FileOutputStream fos = null; 248 | 249 | File saveFile = new File(FileUtils.getFileBasePath()+ "averageMix.pcm" ); 250 | if (saveFile.exists()) { 251 | saveFile.delete(); 252 | } 253 | fos = new FileOutputStream(saveFile); 254 | fos.write(realMixAudio); 255 | fos.close(); 256 | return realMixAudio; 257 | } 258 | 259 | private static byte[] subBytes(byte[] src, int begin, int count) { 260 | byte[] bs = new byte[count]; 261 | System.arraycopy(src, begin, bs, 0, count); 262 | return bs; 263 | } 264 | ``` 265 | 266 | Input the file name and return the byte stream of the file content. 267 | ```java 268 | //Read the file stream into an array, 269 | public static byte[] getContent(String filePath) throws IOException { 270 | File file = new File(filePath); 271 | long fileSize = file.length(); 272 | if (fileSize > Integer.MAX_VALUE) { 273 | Log.d("ccc","file too big..."); 274 | return null; 275 | } 276 | FileInputStream fi = new FileInputStream(file); 277 | byte[] buffer = new byte[(int) fileSize]; 278 | int offset = 0; 279 | int numRead = 0; 280 | 281 | while (offset < buffer.length 282 | && (numRead = fi.read(buffer, offset, buffer.length - offset)) >= 0) { 283 | offset += numRead; 284 | } 285 | 286 | if (offset != buffer.length) { 287 | throw new IOException("Could not completely read file " 288 | + file.getName()); 289 | } 290 | fi.close(); 291 | return buffer; 292 | } 293 | ``` 294 | Thanks Star~ 295 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhoneCall 2 | 3 | [English](README-En.md) 4 | 5 | ## 一、介绍 6 | 基于netty框架开发的局域网IP电话,用户输入对方IP地址便能够进行语音通话。为了实现网络连接与Activity无关,使用service管理netty对象,现阶段可以在APP的任意界面监听到打电话请求,并跳转到响铃界面。另外,本项目将需要的操作封装到了ApiProvider类中。 7 | 8 | ps:本项目是在[VideoCalling]()上进行改进的,该项目是实现局域网的视频传输,我将其语音传输抽取出来进行更改实现语音通话,感谢作者的无私贡献。 9 | 10 | ## 测试 11 | 12 | Releases v3为最新版本,将两个手机连接到同一个局域网中,一方输入对方IP便可以进行语音通话。网络监听线程设置在Service中,当监听到打电话请求便会直接跳转到响铃界面。 13 | 14 | ## 实现思路 15 | 16 | 首先使用AudioRecord进行音频录制,使用speex进行降噪并编码生成语音流,然后使用socket发送给出去, 接受方收到语音数据后将进行解码,然后使用AudioTrack播放。其中,电话交互逻辑使用文本信令控制。 17 | 18 | 对于群组通话,假设有ABC三人进行通话,首先A开启一个聊天室,然后BC加入聊天室,此时BC只需将自己的语音流发送给A,然后在A进行语音的合成操作,将合成的语音在本地播放和发送给BC即可。 19 | 20 | ## 界面展示 21 | 22 | ![call.png](https://pic.tyzhang.top/images/2020/10/23/call.png) 23 | ## 项目架构 24 | 25 | - audio包:进行音频的录制、编码、解码、播放操作 26 | - net包:网络连接的包 27 | - CallSingal:定义电话信令,如拨打电话操作 28 | - Message: 传输数据,包括字节与音流和文字 29 | - NettyClient: netty网络连接代理 30 | - NettyReceiverHandler: 处理发送数据和接受数据,定义接口回调返回语音信息和电话信令信息 31 | - **ApiProvider: 提供网络发送API,音频播放和录制API,连接断开等API。整个项目的入口文件。** 32 | - mixAudioUtils: 混音用的工具类 33 | - MultiVoIPActivity:实现混音界面,需要录制两端音频然后点击混音按钮,之后点击输出混音即可播放 34 | - VoipP2PActivity:IP电话的主要界面,因为需要监听打电话的请求,但是不会写service进行后台监听,所以就在一个activity中写了五个界面进行切换..以后有机会可能会改 35 | 36 | ## 控制信令逻辑图 37 | 38 | ![PhoneCallCh.png](https://pic.tyzhang.top/images/2020/10/20/PhoneCallCh.png) 39 | 40 | ## 二、代码 41 | 42 | ### 0. API 43 | 44 | 打电话的逻辑是基于以下API实现,**包括音频的录制与播放,语音流的发送与接收,连接的断开与关闭,以上包括了实现PhoneCall所需要的全部功能。** 45 | 46 | ```java 47 | public class ApiProvider { 48 | /** 49 | * 注册回调,处理接收到的音频和文本。 50 | * @param callback 回调变量。 51 | */ 52 | public void registerFrameResultedCallback(NettyReceiverHandler.FrameResultedCallback callback){} 53 | 54 | /** 55 | * 发送音频数据 56 | * @param data 音频流 57 | */ 58 | public void sendAudioFrame(byte[] data) {} 59 | 60 | /** 61 | * 通过设置默认IP进行发送数据。 62 | * @param msg 消息 63 | */ 64 | public void sentTextData(String msg) {} 65 | /** 66 | * 通过指定IP发送文本信息 67 | * @param targetIp 目标IP 68 | * @param msg 文本消息。 69 | */ 70 | public void UserIPSentTextData(String targetIp, String msg) {} 71 | /** 72 | * 通过指定IP发送音频信息 73 | * @param targetIp 目标IP 74 | * @param data 数据流 75 | */ 76 | public void UserIpSendAudioFrame(String targetIp ,byte[] data) {} 77 | 78 | /** 79 | * 关闭Netty客户端, 80 | */ 81 | public void shutDownSocket(){} 82 | 83 | /** 84 | * 关闭连接,打电话结束 85 | * @return true or false 86 | */ 87 | public boolean disConnect(){} 88 | 89 | /** 90 | * 获取目标地址 91 | * @return 此时目标地址。 92 | */ 93 | public String getTargetIP() {} 94 | 95 | /** 96 | * 设置目标地址 97 | * @param targetIP 设置目标地址。 98 | */ 99 | public void setTargetIP(String targetIP) {} 100 | 101 | /** 102 | * 开始录音 在开始以下操作之前,必须先把目标IP设置对,否则会出现问题。 103 | */ 104 | public void startRecord(){} 105 | 106 | /** 107 | * 停止录音 108 | */ 109 | public void stopRecord(){} 110 | 111 | /** 112 | * 录音线程是否正在录音 113 | * @return true 正在录音 or false 没有在录音 114 | */ 115 | public boolean isRecording(){} 116 | 117 | /** 118 | * 开始播放音频 119 | */ 120 | public void startPlay(){} 121 | 122 | /** 123 | * 停止播放音频 124 | */ 125 | public void stopPlay(){} 126 | 127 | /** 128 | * 是否正在播放 129 | * @return true 正在播放; false 停止播放 130 | */ 131 | public boolean isPlaying(){} 132 | 133 | /** 134 | * 开启录音与播放 135 | */ 136 | public void startRecordAndPlay(){} 137 | 138 | /** 139 | * 关闭录音与播放 140 | */ 141 | public void stopRecordAndPlay(){} 142 | } 143 | ``` 144 | 145 | ### 1. 监听端口 146 | 147 | 每个客户端在启动时,都需要初始化一个Netty客户端进行监听请求,当收到请求以后,需要捕获发送方IP,然后进行主动的回复。 148 | 149 | ```java 150 | Bootstrap b = new Bootstrap(); 151 | group = new NioEventLoopGroup(); 152 | try { 153 | //设置netty的连接属性。 154 | b.group(group) 155 | .channel(NioDatagramChannel.class) //异步的 UDP 连接 156 | .option(ChannelOption.SO_BROADCAST, true) 157 | .option(ChannelOption.SO_RCVBUF, 1024 * 1024)//接收区2m缓存 158 | .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(65535))//加上这个,里面是最大接收、发送的长度 159 | .handler(handler); //设置数据的处理器 160 | b.bind(localPort).sync().channel().closeFuture().await(); 161 | } catch (Exception e) { 162 | e.printStackTrace(); 163 | } finally { 164 | group.shutdownGracefully(); 165 | } 166 | ``` 167 | ### 2. 数据传输 168 | 169 | 主要包括两种数据: 170 | 171 | 1. 数字信令(Integer):建立连接过程中的控制数字,发送的时候被转成String类型,(为了兼容Handler只能发送数字)。 172 | 2. 语音数据:通话中的语音数据 173 | 174 | 每次传输需要判断需要发送的是什么类型的数据,做相应的处理后装入运输载体Message对象中,最后用`ChannelHandlerContext`对象将转换为Json格式的Message对象发送至目标IP地址相应的端口。 175 | 176 | ```java 177 | //发送数据。 178 | public void sendData(String ip, int port, Object data, String type) { 179 | Message message = null; 180 | if (data instanceof byte[]) { 181 | message = new Message(); 182 | message.setFrame((byte[]) data); 183 | message.setMsgtype(type); 184 | message.setTimestamp(System.currentTimeMillis()); 185 | }else if (data instanceof String){ 186 | message = new Message(); 187 | message.setMsgBody((String) data); 188 | message.setMsgtype(type); 189 | message.setTimestamp(System.currentTimeMillis()); 190 | message.setMsgIp(MLOC.localIpAddress); 191 | } 192 | if (channelHandlerContext != null) { 193 | channelHandlerContext.writeAndFlush(new DatagramPacket( 194 | Unpooled.copiedBuffer(JSON.toJSONString(message).getBytes()), 195 | new InetSocketAddress(ip, port))); 196 | } 197 | } 198 | ``` 199 | 200 | 在进行接收数据的时候也是需要进行相同判断操作,然后进行数据的获取。 201 | 202 | ```java 203 | //接收数据。 204 | ByteBuf buf = (ByteBuf) packet.copy().content(); //字节缓冲区 205 | byte[] req = new byte[buf.readableBytes()]; 206 | buf.readBytes(req); 207 | String str = new String(req, "UTF-8"); 208 | Message message = JSON.parseObject(str,Message.class); 209 | 210 | Netty框架中有一个类SimpleChannelInboundHandler,主要是对监听的端口传来的数据进行处理的。自定义一个继承自它的类,并重写处理收到数据的方法channelRead0(),将数据写入Message对象。 211 | 212 | //发送文字类型信息回调 213 | 214 | if (message.getMsgtype().equals(Message.MES_TYPE_NOMAL)){ 215 | if (frameCallback !=null){ 216 | frameCallback.onTextMessage(message.getMsgBody()); 217 | frameCallback.onGetRemoteIP(message.getMsgIp()); 218 | } 219 | }else if (message.getMsgtype().equals(Message.MES_TYPE_AUDIO)){ 220 | 221 | //发送语音数据接口回调 222 | if (frameCallback !=null){ 223 | frameCallback.onAudioData(message.getFrame()); 224 | } 225 | } 226 | ``` 227 | ### 3. 数字信令 228 | 229 | 总共的控制代码有三种: 230 | 231 | ```java 232 | // control text 233 | public static final Integer PHONE_MAKE_CALL = 100; // make call 234 | public static final Integer PHONE_ANSWER_CALL = 200; // answer call 235 | public static final Integer PHONE_CALL_END = 300; // call end 236 | ``` 237 | 238 | 假设A向B进行打电话: 239 | 240 | 1. 当B收到*PHONE_MAKE_CALL*后,B需要先判断此时自己是否正忙(正在打电话),如果不忙则跳到响铃界面,否则直接丢包。 241 | 2. 当A收到B的PHONE_ANSWER_CALL后,则直接显示对话界面,开始录音并且将接电话标识设置为true。 242 | 3. 当A或者B收到PHONE_CALL_END后,此时需要判断发出此条结束消息的来源是否是正在通话的客户,防止在第三方进行呼叫是出现错误挂断的情形。 243 | 244 | ```java 245 | // 状态切换逻辑 246 | @SuppressLint("HandlerLeak") 247 | private Handler mHandler = new Handler() { 248 | public void handleMessage(Message msg) { 249 | //根据标志记性自定义的操作,这个操作可以操作主线程。 250 | if (msg.what == PHONE_MAKE_CALL) { //收到打电话的请求。 251 | if (!isBusy){ //如果不忙 则跳转到通话界面。 252 | showRingView(); //跳转到响铃界面。 253 | isBusy = true; 254 | } 255 | }else if (msg.what == PHONE_ANSWER_CALL){ //接听电话 256 | showTalkingView(); 257 | provider.startRecordAndPlay(); 258 | isAnswer = true; //接通电话为真 259 | }else if (msg.what == PHONE_CALL_END){ //收到通话结束的信息 260 | if (newEndIp.equals(provider.getTargetIP())){ 261 | showBeginView(); 262 | isAnswer = false; 263 | isBusy = false; 264 | provider.stopRecordAndPlay(); 265 | timer.stop(); 266 | } 267 | } 268 | } 269 | }; 270 | ``` 271 | ### 4.混音 272 | 273 | 混音采用的是平均混音算法,通过测试可以实现混音。主要涉及到安卓文件的创建和以字节流的方式进行文件的读取。 274 | 275 | ```java 276 | public static byte[] averageMix(String file1,String file2) throws IOException { 277 | 278 | byte[][] bMulRoadAudioes = new byte[][]{ 279 | FileUtils.getContent(file1), //第一个文件 280 | FileUtils.getContent(file2) //第二个文件 281 | }; 282 | 283 | byte[] realMixAudio = bMulRoadAudioes[0]; //保存混音之后的数据。 284 | Log.e("ccc", " bMulRoadAudioes length " + bMulRoadAudioes.length); //2 285 | //判断两个文件的大小是否相同,如果不同进行补齐操作 286 | for (int rw = 0; rw < bMulRoadAudioes.length; ++rw) { //length一直都是等于2.依次检测file长度和file2长度 287 | if (bMulRoadAudioes[rw].length != realMixAudio.length) { 288 | Log.e("ccc", "column of the road of audio + " + rw + " is diffrent."); 289 | if (bMulRoadAudioes[rw].lengthrealMixAudio.length){ 293 | bMulRoadAudioes[rw] = subBytes(bMulRoadAudioes[rw],0,realMixAudio.length); 294 | } 295 | } 296 | } 297 | 298 | int row = bMulRoadAudioes.length; //行 299 | int column = realMixAudio.length / 2; //列 300 | short[][] sMulRoadAudioes = new short[row][column]; 301 | for (int r = 0; r < row; ++r) { //前半部分 302 | for (int c = 0; c < column; ++c) { 303 | sMulRoadAudioes[r][c] = (short) ((bMulRoadAudioes[r][c * 2] & 0xff) | (bMulRoadAudioes[r][c * 2 + 1] & 0xff) << 8); 304 | } 305 | } 306 | short[] sMixAudio = new short[column]; 307 | int mixVal; 308 | int sr = 0; 309 | for (int sc = 0; sc < column; ++sc) { 310 | mixVal = 0; 311 | sr = 0; 312 | for (; sr < row; ++sr) { 313 | mixVal += sMulRoadAudioes[sr][sc]; 314 | } 315 | sMixAudio[sc] = (short) (mixVal / row); 316 | } 317 | 318 | //合成混音保存在realMixAudio 319 | for (sr = 0; sr < column; ++sr) { //后半部分 320 | realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF); 321 | realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8); 322 | } 323 | 324 | //保存混合之后的pcm 325 | FileOutputStream fos = null; 326 | //保存合成之后的文件。 327 | File saveFile = new File(FileUtils.getFileBasePath()+ "averageMix.pcm" ); 328 | if (saveFile.exists()) { 329 | saveFile.delete(); 330 | } 331 | fos = new FileOutputStream(saveFile);// 建立一个可存取字节的文件 332 | fos.write(realMixAudio); 333 | fos.close();// 关闭写入流 334 | return realMixAudio; //返回合成的混音。 335 | } 336 | 337 | //合并两个音轨。 338 | private static byte[] subBytes(byte[] src, int begin, int count) { 339 | byte[] bs = new byte[count]; 340 | System.arraycopy(src, begin, bs, 0, count); 341 | return bs; 342 | } 343 | ``` 344 | 345 | 传入文件名称,返回文件内容的字节流 346 | ```java 347 | //将文件流读取到数组中, 348 | public static byte[] getContent(String filePath) throws IOException { 349 | File file = new File(filePath); 350 | long fileSize = file.length(); 351 | if (fileSize > Integer.MAX_VALUE) { 352 | Log.d("ccc","file too big..."); 353 | return null; 354 | } 355 | FileInputStream fi = new FileInputStream(file); 356 | byte[] buffer = new byte[(int) fileSize]; 357 | int offset = 0; 358 | int numRead = 0; 359 | //while循环会使得read一直进行读取,fi.read()在读取完数据以后会返回-1 360 | while (offset < buffer.length 361 | && (numRead = fi.read(buffer, offset, buffer.length - offset)) >= 0) { 362 | offset += numRead; 363 | } 364 | //确保所有数据均被读取 365 | if (offset != buffer.length) { 366 | throw new IOException("Could not completely read file " 367 | + file.getName()); 368 | } 369 | fi.close(); 370 | return buffer; 371 | } 372 | ``` 373 | 万水千山总是情,点个star行不行~ 374 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | defaultConfig { 6 | applicationId "com.proposeme.seven.phonecall" 7 | minSdkVersion 19 8 | targetSdkVersion 27 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | 20 | 21 | sourceSets { 22 | main { 23 | jniLibs.srcDirs = ['libs'] 24 | } 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | implementation 'com.android.support:appcompat-v7:27.1.1' 31 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 32 | implementation 'com.android.support:design:27.1.1' 33 | implementation 'com.android.support:support-v4:27.1.1' 34 | testImplementation 'junit:junit:4.12' 35 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 36 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 37 | 38 | implementation 'org.greenrobot:eventbus:3.0.0' 39 | implementation 'io.netty:netty-all:4.1.17.Final' 40 | implementation 'com.google.code.gson:gson:2.8.5' 41 | implementation 'com.jakewharton:butterknife:8.8.1' 42 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' 43 | implementation 'org.ligboy.retrofit2:converter-fastjson-android:2.1.0' 44 | //权限申请库 45 | implementation 'com.yanzhenjie:permission:2.0.0-rc4' 46 | 47 | // log日志打印输出 48 | debugImplementation 'com.apkfuns.logutils:library:1.5.1.1' 49 | releaseImplementation 'com.apkfuns.logutils:logutils-no-op:1.5.1.1' 50 | implementation 'com.squareup.okhttp3:okhttp-ws:3.4.2' 51 | } 52 | -------------------------------------------------------------------------------- /app/libs/armeabi-v7a/libspeex.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostStarTvT/PhoneCall/8663db491089ed91d9cbdf89b9451b18d3c876dd/app/libs/armeabi-v7a/libspeex.so -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/proposeme/seven/phonecall/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.proposeme.seven.phonecall", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/gyz/voipdemo_speex/util/Speex.java: -------------------------------------------------------------------------------- 1 | package com.gyz.voipdemo_speex.util; 2 | 3 | public class Speex { 4 | private static final int DEFAULT_COMPRESSION = 8; 5 | //创建一个对象,单例模式 6 | private static final Speex speex = new Speex(); 7 | //打开编解码库 8 | public native int open(int compression); 9 | //获取帧的大小 10 | public native int getFrameSize(); 11 | //解码 12 | public native int decode(byte encoded[], short lin[], int size); 13 | //编码 14 | public native int encode(short lin[], int offset, byte encoded[], int size); 15 | //关闭编解码库 16 | public native void close(); 17 | 18 | private Speex() { 19 | 20 | } 21 | 22 | public static Speex getInstance() { 23 | return speex; 24 | } 25 | 26 | public void init() { 27 | load();//加载.so文件 28 | open(DEFAULT_COMPRESSION); 29 | } 30 | 31 | private void load() { 32 | try { 33 | System.loadLibrary("speex"); 34 | } catch (Throwable e) { 35 | e.printStackTrace(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall; 2 | 3 | import android.content.Intent; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.widget.TextView; 8 | 9 | import com.gyz.voipdemo_speex.util.Speex; 10 | import com.proposeme.seven.phonecall.net.BaseData; 11 | import com.proposeme.seven.phonecall.service.VoIPService; 12 | import com.proposeme.seven.phonecall.utils.PermissionManager; 13 | import com.yanzhenjie.permission.Permission; 14 | 15 | import butterknife.ButterKnife; 16 | 17 | 18 | import static com.proposeme.seven.phonecall.utils.NetUtils.getIPAddress; 19 | 20 | //p2p电话的主界面。 21 | public class MainActivity extends AppCompatActivity { 22 | 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_main); 28 | ButterKnife.bind(this); 29 | 30 | BaseData.LOCALHOST = getIPAddress(this); //获取本机ip地址 31 | Speex.getInstance().init(); 32 | 33 | 34 | findViewById(R.id.phoneCall).setOnClickListener(new View.OnClickListener() { 35 | @Override 36 | public void onClick(View v) { 37 | VoIpP2PActivity.newInstance(MainActivity.this); 38 | } 39 | }); 40 | 41 | //点击之后跳转到打电话。点击多人电话会议测试。 42 | findViewById(R.id.Multi_phoneCall).setOnClickListener(new View.OnClickListener() { 43 | @Override 44 | public void onClick(View v) { 45 | MultiVoIpActivity.newInstance(MainActivity.this); 46 | } 47 | }); 48 | 49 | 50 | ((TextView)findViewById(R.id.create_ip_addr)).setText(BaseData.LOCALHOST); 51 | initPermission(); 52 | 53 | // 启动service 54 | Intent intent = new Intent(this,VoIPService.class); 55 | startService(intent); 56 | } 57 | 58 | 59 | @Override 60 | protected void onResume() { 61 | super.onResume(); 62 | } 63 | 64 | /** 65 | * 初始化权限事件 66 | */ 67 | private void initPermission() { 68 | //检查权限 69 | PermissionManager.requestPermission(MainActivity.this, new PermissionManager.Callback() { 70 | @Override 71 | public void permissionSuccess() { 72 | PermissionManager.requestPermission(MainActivity.this, new PermissionManager.Callback() { 73 | @Override 74 | public void permissionSuccess() { 75 | PermissionManager.requestPermission(MainActivity.this, new PermissionManager.Callback() { 76 | @Override 77 | public void permissionSuccess() { 78 | 79 | 80 | } 81 | @Override 82 | public void permissionFailed() { 83 | } 84 | }, Permission.Group.STORAGE); 85 | } 86 | 87 | @Override 88 | public void permissionFailed() { 89 | 90 | } 91 | }, Permission.Group.MICROPHONE); 92 | } 93 | 94 | @Override 95 | public void permissionFailed() { 96 | 97 | } 98 | }, Permission.Group.CAMERA); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/MultiVoIpActivity.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.media.AudioFormat; 6 | import android.media.AudioManager; 7 | import android.media.AudioTrack; 8 | import android.os.Environment; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.os.Bundle; 11 | import android.util.Log; 12 | import android.view.View; 13 | 14 | import com.proposeme.seven.phonecall.utils.mixAduioUtils.AudioUtil; 15 | import com.proposeme.seven.phonecall.utils.mixAduioUtils.FileUtils; 16 | import com.proposeme.seven.phonecall.utils.mixAduioUtils.MixAudioUtil; 17 | 18 | import java.io.File; 19 | import java.io.FileInputStream; 20 | import java.io.IOException; 21 | import java.util.concurrent.ExecutorService; 22 | import java.util.concurrent.Executors; 23 | 24 | 25 | //实现多人语音通话的活动,现在只是实现两个音轨的合成。 26 | public class MultiVoIpActivity extends AppCompatActivity implements View.OnClickListener { 27 | 28 | private AudioUtil mAudioUtil; 29 | private static final int BUFFER_SIZE = 1024 * 2; 30 | private byte[] mBuffer; 31 | 32 | private ExecutorService mExecutorService; 33 | private static final String TAG = "MainActivity"; 34 | 35 | //跳转activity 36 | public static void newInstance(Context context) { 37 | context.startActivity(new Intent(context, MultiVoIpActivity.class)); 38 | } 39 | 40 | @Override 41 | protected void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | setContentView(R.layout.activity_multi_voip); 44 | 45 | findViewById(R.id.first_start_record_button).setOnClickListener(this); 46 | findViewById(R.id.first_stop_record_button).setOnClickListener(this); 47 | findViewById(R.id.first_play).setOnClickListener(this); 48 | 49 | findViewById(R.id.second_start_record_button).setOnClickListener(this); 50 | findViewById(R.id.second_stop_record_button).setOnClickListener(this); 51 | findViewById(R.id.second_play).setOnClickListener(this); 52 | 53 | findViewById(R.id.beginMix).setOnClickListener(this); 54 | findViewById(R.id.MixaudioPlayer).setOnClickListener(this); 55 | mBuffer = new byte[BUFFER_SIZE]; 56 | 57 | mExecutorService = Executors.newSingleThreadExecutor(); 58 | mAudioUtil = AudioUtil.getInstance(); 59 | } 60 | 61 | @Override 62 | public void onClick(View v) { 63 | switch (v.getId()){ 64 | //第一个录音操作 65 | case R.id.first_start_record_button: // 录音开始 66 | mAudioUtil.createFile("firstPcm.pcm"); 67 | mAudioUtil.startRecord(); 68 | mAudioUtil.recordData(); 69 | break; 70 | case R.id.first_stop_record_button: // 录音结束 71 | mAudioUtil.stopRecord(); 72 | break; 73 | case R.id.first_play: // 录音播放 74 | audioPlayer("firstPcm.pcm"); 75 | break; 76 | 77 | //第二个录音操作 78 | case R.id.second_start_record_button: 79 | mAudioUtil.createFile("secondPcm.pcm"); 80 | mAudioUtil.startRecord(); 81 | mAudioUtil.recordData(); 82 | break; 83 | case R.id.second_stop_record_button: 84 | mAudioUtil.stopRecord(); 85 | break; 86 | case R.id.second_play: 87 | audioPlayer("secondPcm.pcm"); 88 | break; 89 | 90 | //混音操作 91 | case R.id.beginMix: //混音开始 92 | try { 93 | byte[] realMixAudio = MixAudioUtil.averageMix(FileUtils.getFileBasePath()+"firstPcm.pcm",FileUtils.getFileBasePath()+"secondPcm.pcm"); 94 | } catch (IOException e) { 95 | e.printStackTrace(); 96 | } 97 | break; 98 | case R.id.MixaudioPlayer: //混音播放 99 | audioPlayer("averageMix.pcm"); 100 | break; 101 | } 102 | } 103 | 104 | private void audioPlayer(String fileName) { 105 | //在播放的时候需要提前设置好录音文件。 106 | final File mAudioFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() 107 | + "/record/"+ fileName); 108 | mExecutorService.submit(new Runnable() 109 | { 110 | @Override 111 | public void run() 112 | { 113 | playAudio(mAudioFile); //读入传入的文件。 114 | } 115 | }); 116 | } 117 | 118 | //将pcm文件读入并且进行播放。 119 | private void playAudio(File audioFile) //读入的是pcm文件。 120 | { 121 | Log.d("MainActivity" , "播放开始"); 122 | int streamType = AudioManager.STREAM_MUSIC; //按照音乐流进行播放 123 | int simpleRate = 44100; //播放的赫兹 124 | int channelConfig = AudioFormat.CHANNEL_OUT_STEREO; 125 | int audioFormat = AudioFormat.ENCODING_PCM_16BIT; 126 | int mode = AudioTrack.MODE_STREAM; 127 | 128 | int minBufferSize = AudioTrack.getMinBufferSize(simpleRate , channelConfig , audioFormat); 129 | AudioTrack audioTrack = new AudioTrack(streamType , simpleRate , channelConfig , audioFormat , 130 | Math.max(minBufferSize , BUFFER_SIZE) , mode); 131 | audioTrack.play(); 132 | Log.d(TAG , minBufferSize + " is the min buffer size , " + BUFFER_SIZE + " is the read buffer size"); 133 | 134 | FileInputStream inputStream = null; 135 | try 136 | { 137 | inputStream = new FileInputStream(audioFile); //读入pcm文件。 138 | int read; 139 | while ((read = inputStream.read(mBuffer)) > 0) 140 | { 141 | Log.d("MainActivity" , "录音开始 kaishi11111"); 142 | 143 | audioTrack.write(mBuffer , 0 , read); //将文件流添加播放 144 | } 145 | } 146 | catch (RuntimeException | IOException e) 147 | { 148 | e.printStackTrace(); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/VoIpP2PActivity.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.content.ComponentName; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.ServiceConnection; 9 | import android.os.CountDownTimer; 10 | import android.os.Handler; 11 | import android.os.IBinder; 12 | import android.os.Message; 13 | import android.os.SystemClock; 14 | import android.support.v7.app.AppCompatActivity; 15 | import android.os.Bundle; 16 | import android.util.Log; 17 | import android.view.View; 18 | import android.view.WindowManager; 19 | import android.view.inputmethod.InputMethodManager; 20 | import android.widget.Chronometer; 21 | import android.widget.EditText; 22 | import android.widget.TextView; 23 | import android.widget.Toast; 24 | 25 | import com.proposeme.seven.phonecall.audio.AudioDecoder; 26 | import com.proposeme.seven.phonecall.net.BaseData; 27 | import com.proposeme.seven.phonecall.net.IPSave; 28 | import com.proposeme.seven.phonecall.net.NettyReceiverHandler; 29 | import com.proposeme.seven.phonecall.provider.ApiProvider; 30 | import com.proposeme.seven.phonecall.service.VoIPService; 31 | import com.proposeme.seven.phonecall.utils.NetUtils; 32 | 33 | import static com.proposeme.seven.phonecall.net.BaseData.IFS; 34 | import static com.proposeme.seven.phonecall.net.BaseData.PHONE_ANSWER_CALL; 35 | import static com.proposeme.seven.phonecall.net.BaseData.PHONE_CALL_END; 36 | import static com.proposeme.seven.phonecall.net.BaseData.PHONE_MAKE_CALL; 37 | 38 | // 此界面就是进行呼叫、接听、响铃、正常的切换 39 | public class VoIpP2PActivity extends AppCompatActivity implements View.OnClickListener{ 40 | 41 | private Chronometer timer; // 通话计时器 42 | private CountDownTimer mCountDownTimer; //打电话超时计时器 43 | 44 | // API控制对象 45 | private ApiProvider provider; 46 | 47 | private boolean isAnswer = false; //是否接电话 48 | private boolean isBusy = false; //是否正在通话中。true 表示正忙 false 表示为不忙。 49 | private String newEndIp = null; //检测通话结束ip是否合法。 50 | 51 | private EditText mEditText; //记录用户输入ip地址 52 | 53 | 54 | private VoIPService IPService; 55 | // 获取到service对象引用,获取到provider 56 | private ServiceConnection mServiceConnection = new ServiceConnection() { 57 | @Override 58 | public void onServiceConnected(ComponentName name, IBinder service) { 59 | IPService = ((VoIPService.MyBinder) service).getService(); 60 | provider = IPService.getProvider(); 61 | // 只能在获取到provider 以后才能进行网络的初始化 62 | netInit(); 63 | } 64 | 65 | @Override 66 | public void onServiceDisconnected(ComponentName name) { 67 | } 68 | }; 69 | 70 | // 状态切换逻辑 71 | @SuppressLint("HandlerLeak") 72 | private Handler mHandler = new Handler() { 73 | public void handleMessage(Message msg) { 74 | //根据标志记性自定义的操作,这个操作可以操作主线程。 75 | if (msg.what == PHONE_MAKE_CALL) { // 接受方接收 76 | if (!isBusy){ //如果不忙 则跳转到通话界面。 77 | showRingView(); //跳转到响铃界面。 78 | isBusy = true; 79 | } 80 | }else if (msg.what == PHONE_ANSWER_CALL){ // 发送方接收 81 | showTalkingView(); 82 | provider.startRecordAndPlay(); 83 | isAnswer = true; //接通电话为真 84 | mCountDownTimer.cancel(); // 关闭倒计时定时器。 85 | }else if (msg.what == PHONE_CALL_END){ //收到通话结束的信息 接收方和发送方都可能接收。 86 | if (newEndIp.equals(provider.getTargetIP())){ 87 | showBeginView(); 88 | isAnswer = false; 89 | isBusy = false; 90 | provider.stopRecordAndPlay(); 91 | timer.stop(); 92 | } 93 | } 94 | } 95 | }; 96 | 97 | //跳转activity 98 | public static void newInstance(Context context) { 99 | context.startActivity(new Intent(context, VoIpP2PActivity.class)); 100 | } 101 | 102 | @Override 103 | protected void onCreate(Bundle savedInstanceState) { 104 | super.onCreate(savedInstanceState); 105 | 106 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 107 | getWindow().setFlags(WindowManager.LayoutParams. FLAG_FULLSCREEN , 108 | WindowManager.LayoutParams. FLAG_FULLSCREEN); 109 | 110 | //获取打电话ip,然后更新界面。 111 | setContentView(R.layout.activity_voip_p2p); 112 | findViewById(R.id.calling_view).setVisibility(View.GONE); 113 | findViewById(R.id.talking_view).setVisibility(View.GONE); 114 | findViewById(R.id.ring_view).setVisibility(View.GONE); 115 | findViewById(R.id.begin_view).setVisibility(View.GONE); 116 | findViewById(R.id.user_input_ip_view).setVisibility(View.GONE); 117 | 118 | ((TextView)findViewById(R.id.create_ip_addr)).setText(BaseData.LOCALHOST); 119 | timer = findViewById(R.id.timer); 120 | 121 | //设置挂断按钮 122 | findViewById(R.id.calling_hangup).setOnClickListener(this); 123 | findViewById(R.id.talking_hangup).setOnClickListener(this); 124 | findViewById(R.id.ring_pickup).setOnClickListener(this); 125 | findViewById(R.id.ring_hang_off).setOnClickListener(this); 126 | //设置手动输入ip 127 | findViewById(R.id.Create_button).setOnClickListener(this); 128 | findViewById(R.id.user_input_phoneCall).setOnClickListener(this); 129 | 130 | mEditText = findViewById(R.id.user_input_TargetIp); 131 | showBeginView();//显示初始的界面 132 | 133 | //拨打电话倒计时计时器。倒计时10s 134 | mCountDownTimer = new CountDownTimer(10000, 1000) { 135 | public void onTick(long millisUntilFinished) { 136 | } 137 | public void onFinish() { 138 | if (!isAnswer){ //如果没有人应答,则挂断 139 | hangupOperation(); 140 | Toast.makeText(VoIpP2PActivity.this,"打电话超时,请稍后再试!",Toast.LENGTH_SHORT).show(); 141 | } 142 | } 143 | }; 144 | //启动服务。 145 | Intent intent = new Intent(this,VoIPService.class); 146 | bindService(intent,mServiceConnection,BIND_AUTO_CREATE); 147 | 148 | // 检测是否是由service启动的activity。 149 | boolean isFromService = getIntent().getBooleanExtra(IFS,false); 150 | if (isFromService){ 151 | showRingView(); // 显示通话的界面。 152 | isBusy = true; 153 | } 154 | } 155 | 156 | /** 157 | * 自动关闭输入法 158 | * @param act 当前activity 159 | * @param v 绑定的控件。 160 | */ 161 | public void hideOneInputMethod(Activity act, View v) { 162 | InputMethodManager imm = (InputMethodManager) act.getSystemService(Context.INPUT_METHOD_SERVICE); 163 | assert imm != null; 164 | imm.hideSoftInputFromWindow(v.getWindowToken(), 0); 165 | } 166 | 167 | //网络初始化操作 168 | private void netInit(){ 169 | // 注册接口回调。 170 | provider.registerFrameResultedCallback(new NettyReceiverHandler.FrameResultedCallback() { 171 | //接受通话信令 172 | @Override 173 | public void onTextMessage(String msg) { 174 | 175 | // 发送来的信息总共有三种 100 200 300 176 | mHandler.sendEmptyMessage(Integer.parseInt(msg)); 177 | /* 178 | PHONE_MAKE_CALL = 100; //拨打电话 179 | PHONE_ANSWER_CALL = 200; //接听电话 //此时需要进行界面的切换。将打电话切成通话界面。 180 | PHONE_CALL_END = 300; //通话结束 181 | */ 182 | } 183 | 184 | // 对于录音来说,需要知道对方的IP将音频流发送出去,而对于播放而言,只需要开启线程进行播放即可。 185 | @Override 186 | public void onAudioData(byte[] data) { 187 | if (isAnswer){ 188 | AudioDecoder.getInstance().addData(data, data.length); 189 | } 190 | } 191 | 192 | //获得对方返回过过来的ip地址 193 | @Override 194 | public void onGetRemoteIP(String ip) { 195 | newEndIp = ip; //每次都会记录新的ip。 196 | Log.e("ccc", "收到对方ip" + ip); 197 | if ((!ip.equals("")) && (!isBusy)){ //如果正忙那么就不能够更改自己的ip。 只能做无应答操作。 198 | provider.setTargetIP(ip); 199 | } 200 | } 201 | }); 202 | } 203 | 204 | //点击后退键触发的方法 205 | @Override 206 | public void onBackPressed(){ 207 | 208 | // 这时候就需要重新进行注册监听。 209 | hangupOperation();// 这时候也会进行挂断。 210 | IPService.registerCallBack(); //重新注册监听打电话请求的监听。 211 | timer.stop(); 212 | finish(); //确定以后调用退出方法 213 | } 214 | 215 | // 显示初始界面 216 | private void showBeginView(){ 217 | findViewById(R.id.begin_view).setVisibility(View.VISIBLE); 218 | findViewById(R.id.talking_view).setVisibility(View.GONE); 219 | findViewById(R.id.ring_view).setVisibility(View.GONE); 220 | findViewById(R.id.calling_view).setVisibility(View.GONE); 221 | findViewById(R.id.user_input_ip_view).setVisibility(View.GONE); 222 | } 223 | 224 | // 显示用户输入ip界面 225 | private void showUserInputIpView(){ 226 | findViewById(R.id.user_input_ip_view).setVisibility(View.VISIBLE); 227 | findViewById(R.id.talking_view).setVisibility(View.GONE); 228 | findViewById(R.id.ring_view).setVisibility(View.GONE); 229 | findViewById(R.id.calling_view).setVisibility(View.GONE); 230 | findViewById(R.id.begin_view).setVisibility(View.GONE); 231 | // 从缓存中找到IP地址。 232 | mEditText.setText(IPSave.getIP(this)); 233 | } 234 | // 显示呼叫时候的view 235 | private void showCallingView(){ 236 | findViewById(R.id.calling_view).setVisibility(View.VISIBLE); 237 | findViewById(R.id.talking_view).setVisibility(View.GONE); 238 | findViewById(R.id.ring_view).setVisibility(View.GONE); 239 | findViewById(R.id.begin_view).setVisibility(View.GONE); 240 | findViewById(R.id.user_input_ip_view).setVisibility(View.GONE); 241 | 242 | //开启定时器。 243 | mCountDownTimer.start(); 244 | } 245 | //显示说话时候的view 246 | private void showTalkingView(){ 247 | 248 | findViewById(R.id.talking_view).setVisibility(View.VISIBLE); 249 | findViewById(R.id.calling_view).setVisibility(View.GONE); 250 | findViewById(R.id.ring_view).setVisibility(View.GONE); 251 | findViewById(R.id.begin_view).setVisibility(View.GONE); 252 | findViewById(R.id.user_input_ip_view).setVisibility(View.GONE); 253 | timer.setBase(SystemClock.elapsedRealtime()); 254 | timer.start(); 255 | } 256 | 257 | //显示响铃界面 258 | private void showRingView(){ 259 | findViewById(R.id.ring_view).setVisibility(View.VISIBLE); 260 | findViewById(R.id.calling_view).setVisibility(View.GONE); 261 | findViewById(R.id.talking_view).setVisibility(View.GONE); 262 | findViewById(R.id.begin_view).setVisibility(View.GONE); 263 | findViewById(R.id.user_input_ip_view).setVisibility(View.GONE); 264 | } 265 | 266 | @Override 267 | protected void onDestroy() { 268 | super.onDestroy(); 269 | provider.disConnect(); 270 | } 271 | 272 | //设置点击事件 273 | @Override 274 | public void onClick(View v) { 275 | switch (v.getId()){ 276 | case R.id.ring_pickup: //在响铃界面接电话 277 | showTalkingView(); 278 | provider.sentTextData(PHONE_ANSWER_CALL.toString()); 279 | // 开始发送语音信息 280 | provider.startRecordAndPlay(); 281 | isAnswer = true; //接通电话为真 282 | break; 283 | case R.id.calling_hangup: //正在拨打中挂断 284 | hangupOperation(); 285 | break; 286 | case R.id.talking_hangup: //通话中挂断 287 | hangupOperation(); 288 | break; 289 | case R.id.ring_hang_off: //响铃中挂断 290 | hangupOperation(); 291 | break; 292 | case R.id.Create_button: //手动输入ip地址 293 | showUserInputIpView(); 294 | break; 295 | case R.id.user_input_phoneCall: // 拨打电话的入口 296 | //获取ip地址 297 | String ip = mEditText.getText().toString(); 298 | // 检测是否为合法IP 299 | if (NetUtils.ipCheck(ip)){ 300 | provider.setTargetIP(ip); 301 | //1 显示拨打界面 302 | showCallingView(); 303 | isBusy = true; 304 | //2 发送一条拨打电话的信息。 305 | provider.sentTextData(PHONE_MAKE_CALL.toString()); 306 | IPSave.saveIP(this, ip); //保存IP 307 | hideOneInputMethod(this,mEditText); // 隐藏输入法 308 | }else { 309 | Toast.makeText(this,"IP格式不对,请重新输入~",Toast.LENGTH_SHORT).show(); 310 | } 311 | break; 312 | } 313 | } 314 | 315 | //进行挂断电话时候的逻辑 316 | private void hangupOperation(){ 317 | provider.sentTextData(PHONE_CALL_END.toString()); 318 | isBusy = false; 319 | showBeginView(); 320 | isAnswer = false; 321 | provider.stopRecordAndPlay(); 322 | timer.stop(); 323 | mCountDownTimer.cancel(); 324 | } 325 | 326 | } 327 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/audio/AudioConfig.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.audio; 2 | 3 | import android.media.AudioFormat; 4 | import android.media.MediaRecorder; 5 | 6 | /** 7 | * Describe: 音频的配置类 8 | */ 9 | public class AudioConfig { 10 | /** 11 | * Recorder Configure 12 | * 8KHZ 13 | */ 14 | public static final int SAMPLERATE = 8000; 15 | 16 | public static final int PLAYER_CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO; 17 | public static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; 18 | 19 | /** 20 | * Recorder Configure 21 | */ 22 | public static final int AUDIO_RESOURCE = MediaRecorder.AudioSource.VOICE_COMMUNICATION; 23 | public static final int RECORDER_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; 24 | 25 | /** 26 | * 27 | */ 28 | public static final int PLAYER_CHANNEL_CONFIG2 = AudioFormat.CHANNEL_CONFIGURATION_MONO; 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/audio/AudioData.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.audio; 2 | 3 | /** 4 | * Describe: 音频数据 5 | */ 6 | public class AudioData { 7 | int size; 8 | short[] realData; 9 | byte[] receiverdata; 10 | 11 | public int getSize() 12 | { 13 | return size; 14 | } 15 | 16 | public void setSize(int size) 17 | { 18 | this.size = size; 19 | } 20 | 21 | public short[] getRealData() 22 | { 23 | return realData; 24 | } 25 | 26 | public void setRealData(short[] realData) 27 | { 28 | this.realData = realData; 29 | } 30 | 31 | public byte[] getReceiverdata() { 32 | return receiverdata; 33 | } 34 | 35 | public void setReceiverdata(byte[] receiverdata) { 36 | this.receiverdata = receiverdata; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/audio/AudioDecoder.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.audio; 2 | 3 | import android.util.Log; 4 | 5 | import java.util.Collections; 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | import com.gyz.voipdemo_speex.util.Speex; 9 | /** 10 | * Describe: 根据接收到的音频流,进行解码和播放。 11 | */ 12 | public class AudioDecoder implements Runnable { 13 | String LOG = "AudioDecoder"; 14 | private static AudioDecoder decoder; 15 | 16 | private static final int MAX_BUFFER_SIZE = 2048; 17 | 18 | private short[] decodedData; 19 | private volatile boolean isDecoding = false; 20 | private List dataList = null; 21 | 22 | public static AudioDecoder getInstance() { 23 | if (decoder == null) { 24 | decoder = new AudioDecoder(); 25 | } 26 | return decoder; 27 | } 28 | 29 | private AudioDecoder() { 30 | this.dataList = Collections 31 | .synchronizedList(new LinkedList()); 32 | startDecoding(); 33 | } 34 | 35 | 36 | public void addData(byte[] data, int size) { 37 | AudioData adata = new AudioData(); 38 | adata.setSize(size); 39 | byte[] tempData = new byte[size]; 40 | System.arraycopy(data, 0, tempData, 0, size); 41 | adata.setReceiverdata(tempData); 42 | dataList.add(adata); 43 | } 44 | 45 | /** 46 | * startRecordAndPlay decode AMR data 47 | */ 48 | public void startDecoding() { 49 | System.out.println(LOG + "开始解码"); 50 | if (isDecoding) { 51 | return; 52 | } 53 | new Thread(this).start(); 54 | } 55 | 56 | @Override 57 | public void run() { 58 | 59 | //新建音频播放器 60 | AudioPlayer player = AudioPlayer.getInstance(); 61 | 62 | player.startPlaying(); 63 | this.isDecoding = true; 64 | Log.d(LOG, LOG + "初始化播放器"); 65 | int decodeSize = 0; 66 | while (isDecoding) { 67 | if (dataList.size() > 0) { 68 | AudioData encodedData = dataList.remove(0); 69 | decodedData = new short[Speex.getInstance().getFrameSize()]; 70 | byte[] data = encodedData.getReceiverdata(); //获取接收到的数据 71 | decodeSize = Speex.getInstance().decode(data, decodedData, data.length); 72 | if (decodeSize > 0) { 73 | // 将数据添加到播放器 74 | player.addData(decodedData, decodeSize); 75 | } 76 | } 77 | } 78 | System.out.println(LOG + "停止解码"); 79 | // stopRecordAndPlay playback audio 80 | player.stopPlaying(); 81 | } 82 | 83 | public void stopDecoding() { 84 | this.isDecoding = false; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/audio/AudioEncoder.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.audio; 2 | 3 | import android.util.Log; 4 | 5 | import com.gyz.voipdemo_speex.util.Speex; 6 | import com.proposeme.seven.phonecall.provider.ApiProvider; 7 | 8 | 9 | import java.util.Collections; 10 | import java.util.LinkedList; 11 | import java.util.List; 12 | 13 | /** 14 | * Describe: 监听收集到的音频流,并且进行编码,让后调用EncodeProvider 将数据发送出去 15 | */ 16 | public class AudioEncoder implements Runnable{ 17 | String LOG = "AudioEncoder"; 18 | //单例模式构造对象 19 | private static AudioEncoder encoder; 20 | //是否正在编码 21 | private volatile boolean isEncoding = false; 22 | 23 | //每一帧的音频数据的集合 24 | private List dataList = null; 25 | 26 | public static AudioEncoder getInstance() { 27 | if (encoder == null) { 28 | encoder = new AudioEncoder(); 29 | } 30 | return encoder; 31 | } 32 | 33 | private AudioEncoder() { 34 | dataList = Collections.synchronizedList(new LinkedList()); 35 | } 36 | 37 | //存放录音的数据 38 | public void addData(short[] data, int size) { 39 | AudioData rawData = new AudioData(); 40 | rawData.setSize(size); 41 | short[] tempData = new short[size]; 42 | System.arraycopy(data, 0, tempData, 0, size); 43 | rawData.setRealData(tempData); 44 | dataList.add(rawData); 45 | } 46 | 47 | /** 48 | * 开始编码。 49 | */ 50 | public void startEncoding() { 51 | 52 | Log.e("ccc", "编码子线程启动"); 53 | if (isEncoding) { 54 | Log.e(LOG, "encoder has been started !!!"); 55 | return; 56 | } 57 | //开子线程 58 | new Thread(this).start(); 59 | } 60 | 61 | /** 62 | * end encoding 停止编码 63 | */ 64 | public void stopEncoding() { 65 | this.isEncoding = false; 66 | } 67 | 68 | @Override 69 | public void run() { 70 | int encodeSize = 0; 71 | byte[] encodedData; 72 | isEncoding = true; 73 | while (isEncoding) { 74 | if (dataList.size() == 0) { //如果没有编码数据则进行等待并且释放线程 75 | try { 76 | Thread.sleep(20); 77 | } catch (InterruptedException e) { 78 | e.printStackTrace(); 79 | } 80 | continue; 81 | } 82 | if (isEncoding) { 83 | AudioData rawData = dataList.remove(0); 84 | encodedData = new byte[Speex.getInstance().getFrameSize()]; 85 | encodeSize = Speex.getInstance().encode(rawData.getRealData(), 86 | 0, encodedData, rawData.getSize()); 87 | if (encodeSize > 0) { 88 | //实现发送数据。 89 | if (ApiProvider.getProvider()!=null) 90 | ApiProvider.getProvider().sendAudioFrame(encodedData); //发送录音数据 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/audio/AudioPlayer.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.audio; 2 | 3 | 4 | import android.media.AudioManager; 5 | import android.media.AudioRecord; 6 | import android.media.AudioTrack; 7 | import android.os.Environment; 8 | import android.util.Log; 9 | 10 | import java.io.File; 11 | import java.io.FileNotFoundException; 12 | import java.io.FileOutputStream; 13 | import java.io.IOException; 14 | import java.util.Collections; 15 | import java.util.LinkedList; 16 | import java.util.List; 17 | 18 | /** 19 | * Describe: 接受解码音频流进行播放 20 | */ 21 | public class AudioPlayer implements Runnable{ 22 | String LOG = "AudioPlayer "; 23 | private static AudioPlayer player; 24 | 25 | private List dataList; 26 | private AudioData playData; 27 | private volatile boolean isPlaying = false; 28 | 29 | private AudioTrack audioTrack; 30 | 31 | //媒体文件保存与读取。 32 | private File file; 33 | private FileOutputStream fos; 34 | 35 | private AudioPlayer() { 36 | //建立双向链表 37 | dataList = Collections.synchronizedList(new LinkedList()); 38 | 39 | //文件播放测试。 40 | File mFile = new File(Environment.getExternalStorageDirectory(),"/audio"); 41 | if (!mFile.exists()){ 42 | if (mFile.mkdirs()){ 43 | Log.d("ggh","创建成功"); 44 | }else { 45 | Log.d("ggh","文件已存在!"); 46 | } 47 | } 48 | 49 | file = new File(Environment.getExternalStorageDirectory(),"/audio/decode.amr"); 50 | try { 51 | if (!file.exists()) 52 | try { 53 | file.createNewFile(); 54 | } catch (IOException e) { 55 | e.printStackTrace(); 56 | } 57 | fos = new FileOutputStream(file); 58 | } catch (FileNotFoundException e) { 59 | e.printStackTrace(); 60 | } 61 | } 62 | 63 | //获取播放音频实例 64 | public static AudioPlayer getInstance() { 65 | if (player == null) { 66 | player = new AudioPlayer(); 67 | } 68 | return player; 69 | } 70 | 71 | //向播放流添加数据 72 | public void addData(short[] rawData, int size) { 73 | AudioData decodedData = new AudioData(); 74 | decodedData.setSize(size); 75 | short[] tempData = new short[size]; 76 | System.arraycopy(rawData, 0, tempData, 0, size); 77 | decodedData.setRealData(tempData); 78 | dataList.add(decodedData); 79 | } 80 | 81 | /* 82 | * 初始化播放器参数 83 | */ 84 | private boolean initAudioTrack() { 85 | int bufferSize = AudioRecord.getMinBufferSize(AudioConfig.SAMPLERATE, 86 | AudioConfig.PLAYER_CHANNEL_CONFIG2, 87 | AudioConfig.AUDIO_FORMAT); 88 | if (bufferSize < 0) { 89 | Log.e(LOG, LOG + "initialize error!"); 90 | return false; 91 | } 92 | Log.i(LOG, "Player初始化的 buffersize大小" + bufferSize); 93 | 94 | //设置播放器参数 95 | //MODE_STREAM 边读边播 96 | // STREAM_VOICE_CALL 表示用听筒播放。 STREAM_MUSIC 表示用扬声器 97 | audioTrack = new AudioTrack(AudioManager.STREAM_VOICE_CALL, 98 | AudioConfig.SAMPLERATE, AudioConfig.PLAYER_CHANNEL_CONFIG2, 99 | AudioConfig.AUDIO_FORMAT, bufferSize, AudioTrack.MODE_STREAM); 100 | 101 | // set volume:设置播放音量 102 | audioTrack.setStereoVolume(1.0f, 1.0f); 103 | audioTrack.play(); 104 | return true; 105 | } 106 | 107 | //播放数据的添加 108 | private void playFromList() throws IOException { 109 | while (isPlaying) { 110 | while (dataList.size() > 0) { 111 | playData = dataList.remove(0); 112 | audioTrack.write(playData.getRealData(), 0, playData.getSize()); 113 | } 114 | try { 115 | Thread.sleep(20); 116 | } catch (InterruptedException e) { 117 | } 118 | } 119 | } 120 | 121 | public void startPlaying() { 122 | if (isPlaying) { 123 | Log.e(LOG, "验证播放器是否打开" + isPlaying); 124 | return; 125 | } 126 | new Thread(this).start(); 127 | } 128 | 129 | public void run() { 130 | this.isPlaying = true; 131 | //初始化播放器 132 | if (!initAudioTrack()) { 133 | Log.i(LOG, "播放器初始化失败"); 134 | return; 135 | } 136 | Log.e(LOG, "开始播放"); 137 | try { 138 | playFromList(); //开始播放 139 | } catch (IOException e) { 140 | e.printStackTrace(); 141 | } 142 | if (this.audioTrack != null) { 143 | if (this.audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { 144 | this.audioTrack.stop(); 145 | this.audioTrack.release(); 146 | } 147 | } 148 | Log.d(LOG, LOG + "end playing"); 149 | } 150 | 151 | public void stopPlaying() { 152 | this.isPlaying = false; 153 | } 154 | 155 | public boolean isPlaying(){ 156 | return this.isPlaying; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/audio/AudioRecorder.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.audio; 2 | 3 | 4 | import android.media.AudioRecord; 5 | import android.media.audiofx.AcousticEchoCanceler; 6 | import android.util.Log; 7 | 8 | import com.gyz.voipdemo_speex.util.Speex; 9 | 10 | /** 11 | * Describe: 进行音频录音 这个是音频录制的入口文件 12 | */ 13 | public class AudioRecorder implements Runnable { 14 | String LOG = "Recorder"; 15 | //音频录制对象 16 | private AudioRecord audioRecord; 17 | 18 | // 单例对象。 19 | private static AudioRecorder mAudioRecorder; 20 | //回声消除 21 | private AcousticEchoCanceler canceler; 22 | 23 | private AudioRecorder(){ 24 | } 25 | 26 | //是否正在录制 27 | private volatile boolean isRecording = false; 28 | 29 | // 获取单例模式实例。 30 | public static AudioRecorder getAudioRecorder(){ 31 | if (mAudioRecorder == null){ 32 | mAudioRecorder = new AudioRecorder(); 33 | } 34 | return mAudioRecorder; 35 | } 36 | 37 | //开始录音的逻辑 38 | public void startRecording() { 39 | Log.e("ccc", "开启录音"); 40 | //计算缓存大小 41 | int audioBufSize = AudioRecord.getMinBufferSize(AudioConfig.SAMPLERATE, AudioConfig.PLAYER_CHANNEL_CONFIG2, AudioConfig.AUDIO_FORMAT); 42 | //实例化录制对象 43 | if (null == audioRecord && audioBufSize != AudioRecord.ERROR_BAD_VALUE) { 44 | audioRecord = new AudioRecord(AudioConfig.AUDIO_RESOURCE, 45 | AudioConfig.SAMPLERATE, 46 | AudioConfig.PLAYER_CHANNEL_CONFIG2, 47 | AudioConfig.AUDIO_FORMAT, audioBufSize); 48 | } 49 | //消回音处理 50 | assert audioRecord != null; 51 | initAEC(audioRecord.getAudioSessionId()); 52 | new Thread(this).start(); 53 | } 54 | 55 | // 关闭录制 56 | public void stopRecording() { 57 | this.isRecording = false; 58 | } 59 | 60 | // 是否正在录制、 61 | public boolean isRecording() { 62 | return this.isRecording; 63 | } 64 | 65 | //消除回音 66 | public boolean initAEC(int audioSession) { 67 | if (canceler != null) { 68 | return false; 69 | } 70 | if (!AcousticEchoCanceler.isAvailable()){ 71 | return false; 72 | } 73 | canceler = AcousticEchoCanceler.create(audioSession); 74 | canceler.setEnabled(true); 75 | return canceler.getEnabled(); 76 | } 77 | 78 | @Override 79 | public void run() { 80 | 81 | if (audioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) { 82 | return; 83 | } 84 | // 在录音之前实例化一个编码类,在编码类中实现的数据的发送。 85 | AudioEncoder encoder = AudioEncoder.getInstance(); 86 | encoder.startEncoding(); 87 | audioRecord.startRecording(); 88 | 89 | this.isRecording = true; 90 | Log.e("ccc", "开始编码"); 91 | int size = Speex.getInstance().getFrameSize(); 92 | 93 | short[] samples = new short[size]; 94 | 95 | while (isRecording) { 96 | int bufferRead = audioRecord.read(samples, 0, size); 97 | if (bufferRead > 0) { 98 | encoder.addData(samples,bufferRead); 99 | } 100 | } 101 | encoder.stopEncoding(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/net/BaseData.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.net; 2 | 3 | /** 4 | * Describe: 记录打电话时候后的交互信令 5 | */ 6 | public class BaseData { 7 | 8 | // control text 9 | public static final Integer PHONE_MAKE_CALL = 100; //拨打电话 10 | public static final Integer PHONE_ANSWER_CALL = 200; //接听电话 11 | public static final Integer PHONE_CALL_END = 300; //通话结束 12 | 13 | // localhost 14 | public static String LOCALHOST = "127.0.0.1"; // 本机的IP地址,在发送文本数据的时候需要发送出去。 15 | 16 | // port 17 | public static final int PORT = 7777; // 监听端口。 18 | 19 | // isFromService 20 | public static final String IFS = "IFS"; 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/net/IPSave.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.net; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | /** 7 | * Describe: 保存IP地址信息 8 | */ 9 | public class IPSave { 10 | public static void saveIP(Context context, String ip){ 11 | SharedPreferences preferences = context.getSharedPreferences("user_ip",Context.MODE_PRIVATE); 12 | SharedPreferences.Editor editor = preferences.edit(); 13 | editor.putString("ip",ip); 14 | editor.apply(); 15 | } 16 | 17 | public static String getIP(Context context){ 18 | SharedPreferences preferences = context.getSharedPreferences("user_ip",Context.MODE_PRIVATE); 19 | return preferences.getString("ip",null); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/net/Message.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.net; 2 | 3 | /** 4 | * Describe: 存储产生的音频数据 5 | */ 6 | public class Message { 7 | 8 | public static final String MES_TYPE_AUDIO = "MES_TYPE_AUDIO"; //音频 9 | public static final String MES_TYPE_NORMAL = "MES_TYPE_NORMAL"; // 本 10 | private String msgtype; 11 | private String msgBody; 12 | private String msgIp; 13 | private long timestamp; 14 | private byte[] frame; 15 | private int sort; 16 | 17 | public String getMsgtype() { 18 | return msgtype; 19 | } 20 | 21 | public void setMsgtype(String msgtype) { 22 | this.msgtype = msgtype; 23 | } 24 | 25 | public String getMsgBody() { 26 | return msgBody; 27 | } 28 | 29 | public void setMsgBody(String msgBody) { 30 | this.msgBody = msgBody; 31 | } 32 | 33 | public long getTimestamp() { 34 | return timestamp; 35 | } 36 | 37 | public void setTimestamp(long timestamp) { 38 | this.timestamp = timestamp; 39 | } 40 | 41 | public byte[] getFrame() { 42 | return frame; 43 | } 44 | 45 | public void setFrame(byte[] frame) { 46 | this.frame = frame; 47 | } 48 | 49 | public int getSort() { 50 | return sort; 51 | } 52 | 53 | public void setSort(int sort) { 54 | this.sort = sort; 55 | } 56 | 57 | public String getMsgIp() { 58 | return msgIp; 59 | } 60 | 61 | public void setMsgIp(String msgIp) { 62 | this.msgIp = msgIp; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/net/NettyClient.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.net; 2 | 3 | 4 | import io.netty.bootstrap.Bootstrap; 5 | import io.netty.channel.ChannelOption; 6 | import io.netty.channel.EventLoopGroup; 7 | import io.netty.channel.FixedRecvByteBufAllocator; 8 | import io.netty.channel.nio.NioEventLoopGroup; 9 | import io.netty.channel.socket.nio.NioDatagramChannel; 10 | 11 | /** 12 | * Describe: 使用netty框架构建音频传输客户端 13 | * 此代理只是监听本地端口,然后在发送数据的时候需要指定对方ip和对方端口 14 | */ 15 | public class NettyClient { 16 | 17 | private NettyReceiverHandler handler; 18 | private int port = BaseData.PORT; // 监听端口 19 | 20 | private EventLoopGroup group; 21 | 22 | private static NettyClient sClient; 23 | 24 | 25 | private NettyClient() { 26 | init(); 27 | } 28 | 29 | /** 30 | * 获取Client 单例对象 31 | * @return NettyClient 32 | */ 33 | public static NettyClient getClient(){ 34 | if (sClient == null){ 35 | sClient = new NettyClient(); 36 | } 37 | 38 | return sClient; 39 | } 40 | 41 | /** 42 | * 注册回调 43 | * @param callback 回调变量。 44 | */ 45 | public void setFrameResultedCallback(NettyReceiverHandler.FrameResultedCallback callback) { 46 | if (handler != null){ 47 | handler.setOnFrameCallback(callback); 48 | } 49 | } 50 | 51 | /** 52 | * 初始化Netty对象。 53 | */ 54 | private void init() { 55 | //初始化receiverHandler. 56 | handler = new NettyReceiverHandler(); 57 | 58 | //启动客户端进行发送数据 59 | new Thread(new Runnable() { 60 | @Override 61 | public void run() { 62 | Bootstrap b = new Bootstrap(); 63 | group = new NioEventLoopGroup(); 64 | try { 65 | //设置netty的连接属性。 66 | b.group(group) 67 | .channel(NioDatagramChannel.class) //异步的 UDP 连接 68 | .option(ChannelOption.SO_BROADCAST, true) 69 | .option(ChannelOption.SO_RCVBUF, 1024 * 1024)//接收区2m缓存 70 | .option(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(65535))//加上这个,里面是最大接收、发送的长度 71 | .handler(handler); //设置数据的处理器 72 | 73 | b.bind(port).sync().channel().closeFuture().await(); 74 | //构造一个劲监听本地端口的netty代理 75 | } catch (Exception e) { 76 | e.printStackTrace(); 77 | } finally { 78 | group.shutdownGracefully(); 79 | } 80 | } 81 | }).start(); 82 | } 83 | 84 | 85 | /** 86 | * 发送数据到指定IP地址, 87 | * @param targetIp 目标IP 88 | * @param data 数据 89 | * @param msgType 数据类型 90 | */ 91 | public void UserIPSendData(String targetIp, Object data, String msgType) { 92 | //这个就是NettyReceiverHandler.sendData()方法的调用,也即是说,在NettyReceiverHandler这里面可以 93 | //即是处理接受数据的也是处理,发送数据的。 94 | handler.sendData(targetIp, port, data, msgType); 95 | } 96 | 97 | /** 98 | * 断开连接 99 | * @return true 成功断开 or false 断开失败 100 | */ 101 | public boolean DisConnect(){ 102 | return handler.DisConnect(); 103 | } 104 | 105 | /** 106 | * 优雅的关闭Netty对象。 107 | */ 108 | public void shutDownBootstrap(){ 109 | group.shutdownGracefully(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/net/NettyReceiverHandler.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.net; 2 | 3 | import android.util.Log; 4 | import com.alibaba.fastjson.JSON; 5 | import java.net.InetSocketAddress; 6 | 7 | import io.netty.buffer.ByteBuf; 8 | import io.netty.buffer.Unpooled; 9 | import io.netty.channel.ChannelFuture; 10 | import io.netty.channel.ChannelHandlerContext; 11 | import io.netty.channel.SimpleChannelInboundHandler; 12 | import io.netty.channel.socket.DatagramPacket; 13 | 14 | /** 15 | * Describe: 进行语音数据的发送和接收执行者。 16 | */ 17 | public class NettyReceiverHandler extends SimpleChannelInboundHandler { 18 | 19 | private ChannelHandlerContext channelHandlerContext; 20 | //数据返回接口注册 21 | private FrameResultedCallback frameCallback; 22 | 23 | public void setOnFrameCallback(FrameResultedCallback callback) { 24 | this.frameCallback = callback; 25 | } 26 | 27 | //收到数据时候进行开始触发 28 | @Override 29 | protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) 30 | throws Exception { 31 | //服务器推送对方IP和PORT 32 | ByteBuf buf = (ByteBuf) packet.copy().content(); //字节缓冲区 33 | byte[] req = new byte[buf.readableBytes()]; 34 | buf.readBytes(req); 35 | String str = new String(req, "UTF-8"); 36 | Message message = JSON.parseObject(str,Message.class); //同一类中不需要进行导包 37 | 38 | //只有发送文字信息的时候才会返回对方ip。 39 | //对应各自的回调。 40 | if (message.getMsgtype().equals(Message.MES_TYPE_NORMAL)){ 41 | if (frameCallback !=null){ 42 | frameCallback.onTextMessage(message.getMsgBody()); 43 | frameCallback.onGetRemoteIP(message.getMsgIp()); 44 | } 45 | }else if (message.getMsgtype().equals(Message.MES_TYPE_AUDIO)){ 46 | if (frameCallback !=null){ 47 | frameCallback.onAudioData(message.getFrame()); 48 | } 49 | } 50 | } 51 | 52 | //当通道激活时候进行触发, 53 | @Override 54 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 55 | super.channelActive(ctx); 56 | this.channelHandlerContext = ctx; 57 | Log.e("ccc", "nettyReceiver启动"); 58 | } 59 | 60 | //发生异常时候进行调用 61 | @Override 62 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 63 | super.exceptionCaught(ctx, cause); 64 | Log.e("ccc", "同道异常关闭 "); 65 | } 66 | 67 | //根据传递过来的ip和port 进行发送数据。 68 | public void sendData(String ip, int port, Object data, String type) { 69 | Message message = null; 70 | if (data instanceof byte[]) { 71 | message = new Message(); 72 | message.setFrame((byte[]) data); 73 | message.setMsgtype(type); 74 | message.setTimestamp(System.currentTimeMillis()); 75 | }else if (data instanceof String){ 76 | //在发送文本的时候也需要将本地ip地址发送过去。 77 | message = new Message(); 78 | message.setMsgBody((String) data); 79 | message.setMsgtype(type); 80 | message.setTimestamp(System.currentTimeMillis()); 81 | message.setMsgIp(BaseData.LOCALHOST); 82 | } 83 | 84 | //进行数据的发送 85 | if (channelHandlerContext != null) { 86 | channelHandlerContext.writeAndFlush(new DatagramPacket( 87 | Unpooled.copiedBuffer(JSON.toJSONString(message).getBytes()), 88 | new InetSocketAddress(ip, port))); 89 | } 90 | } 91 | 92 | // 断开连接。 93 | public boolean DisConnect(){ 94 | ChannelFuture disconnect = channelHandlerContext.disconnect(); 95 | return disconnect.isDone(); 96 | } 97 | 98 | 99 | // 数据回调。 100 | public interface FrameResultedCallback { 101 | void onTextMessage(String msg); //返回文本信息 102 | void onAudioData(byte[] data); //返回音频信息 103 | void onGetRemoteIP(String ip); //返回对方ip 只是在发送文字信息的时候接受到 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/provider/ApiProvider.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.provider; 2 | 3 | import android.util.Log; 4 | 5 | import com.proposeme.seven.phonecall.audio.AudioPlayer; 6 | import com.proposeme.seven.phonecall.audio.AudioRecorder; 7 | import com.proposeme.seven.phonecall.net.Message; 8 | import com.proposeme.seven.phonecall.net.NettyClient; 9 | import com.proposeme.seven.phonecall.net.NettyReceiverHandler; 10 | 11 | /** 12 | * Describe: 提供网络连接API 和控制逻辑API,这些都是在Service中进行依托。 13 | * 在进行录音的时需要先设置TargetIP的值。 14 | */ 15 | 16 | public class ApiProvider { 17 | 18 | private NettyClient nettyClient; //初始化网络发送代理 19 | private static ApiProvider provider; 20 | private String targetIP = null; // 目标地址。 21 | 22 | // 单例模式。 23 | public static ApiProvider getProvider() { 24 | if (provider == null) { 25 | provider = new ApiProvider(); 26 | } 27 | return provider; 28 | } 29 | 30 | private AudioRecorder mAudioRecorder; //录音机 31 | private AudioPlayer mAudioPlayer; // 播放器。 32 | 33 | //构造方法 34 | private ApiProvider() { 35 | // 1配置client的信息,目标ip和端口。 36 | nettyClient = NettyClient.getClient(); 37 | mAudioRecorder = AudioRecorder.getAudioRecorder(); 38 | mAudioPlayer = AudioPlayer.getInstance(); 39 | provider = this; 40 | } 41 | 42 | /** 43 | * 注册回调 44 | * @param callback 回调变量。 45 | */ 46 | public void registerFrameResultedCallback(NettyReceiverHandler.FrameResultedCallback callback){ 47 | nettyClient.setFrameResultedCallback(callback); 48 | } 49 | 50 | /** 51 | * 发送音频数据 52 | * @param data 音频流 53 | */ 54 | public void sendAudioFrame(byte[] data) { 55 | if (targetIP!= null) 56 | nettyClient.UserIPSendData(targetIP, data, Message.MES_TYPE_AUDIO); 57 | //需要处理为空的异常。 58 | } 59 | 60 | /** 61 | * 通过设置默认IP进行发送数据。 62 | * @param msg 消息 63 | */ 64 | public void sentTextData(String msg) { 65 | if (targetIP != null) 66 | nettyClient.UserIPSendData(targetIP, msg, Message.MES_TYPE_NORMAL); 67 | Log.e("ccc","发送一条信息" + msg ); 68 | } 69 | 70 | /** 71 | * 通过指定IP发送文本信息 72 | * @param targetIp 目标IP 73 | * @param msg 文本消息。 74 | */ 75 | public void UserIPSentTextData(String targetIp, String msg) { 76 | if (targetIp != null) 77 | nettyClient.UserIPSendData(targetIp, msg, Message.MES_TYPE_NORMAL); 78 | Log.e("ccc","发送一条信息" + msg ); 79 | } 80 | 81 | 82 | /** 83 | * 通过指定IP发送音频信息 84 | * @param targetIp 目标IP 85 | * @param data 数据流 86 | */ 87 | public void UserIpSendAudioFrame(String targetIp ,byte[] data) { 88 | if (targetIp != null) 89 | nettyClient.UserIPSendData(targetIp ,data, Message.MES_TYPE_AUDIO); 90 | } 91 | 92 | /** 93 | * 关闭Netty客户端, 94 | */ 95 | public void shutDownSocket(){ 96 | nettyClient.shutDownBootstrap(); 97 | } 98 | 99 | /** 100 | * 关闭连接,打电话结束 101 | * @return true or false 102 | */ 103 | public boolean disConnect(){ 104 | return nettyClient.DisConnect(); 105 | } 106 | 107 | /** 108 | * 获取目标地址 109 | * @return 此时目标地址。 110 | */ 111 | public String getTargetIP() { 112 | return targetIP; 113 | } 114 | 115 | /** 116 | * 设置目标地址 117 | * @param targetIP 设置目标地址。 118 | */ 119 | public void setTargetIP(String targetIP) { 120 | this.targetIP = targetIP; 121 | } 122 | 123 | /** 124 | * 开始录音 在开始以下操作之前,必须先把目标IP设置对,否则会出现问题。 125 | */ 126 | public void startRecord(){ 127 | mAudioRecorder.startRecording(); 128 | } 129 | 130 | /** 131 | * 停止录音 132 | */ 133 | public void stopRecord(){ 134 | mAudioRecorder.stopRecording(); 135 | } 136 | 137 | /** 138 | * 录音线程是否正在录音 139 | * @return true 正在录音 or false 没有在录音 140 | */ 141 | public boolean isRecording(){ 142 | return mAudioRecorder.isRecording(); 143 | } 144 | 145 | /** 146 | * 开始播放音频 147 | */ 148 | public void startPlay(){ 149 | mAudioPlayer.startPlaying(); 150 | } 151 | 152 | /** 153 | * 停止播放音频 154 | */ 155 | public void stopPlay(){ 156 | mAudioPlayer.stopPlaying(); 157 | } 158 | 159 | /** 160 | * 是否正在播放 161 | * @return true 正在播放; false 停止播放 162 | */ 163 | public boolean isPlaying(){ 164 | return mAudioPlayer.isPlaying(); 165 | } 166 | 167 | 168 | /** 169 | * 开启录音与播放 170 | */ 171 | public void startRecordAndPlay(){ 172 | startPlay(); 173 | startRecord(); 174 | } 175 | 176 | /** 177 | * 关闭录音与播放 178 | */ 179 | public void stopRecordAndPlay(){ 180 | stopRecord(); 181 | stopPlay(); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/service/VoIPService.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.service; 2 | 3 | import android.app.Service; 4 | import android.content.Intent; 5 | import android.os.Binder; 6 | import android.os.IBinder; 7 | import android.util.Log; 8 | 9 | import com.proposeme.seven.phonecall.VoIpP2PActivity; 10 | import com.proposeme.seven.phonecall.net.NettyReceiverHandler; 11 | import com.proposeme.seven.phonecall.provider.ApiProvider; 12 | 13 | import static com.proposeme.seven.phonecall.net.BaseData.IFS; 14 | import static com.proposeme.seven.phonecall.net.BaseData.PHONE_MAKE_CALL; 15 | 16 | /** 17 | * 语音后台支持,当有语音通话的时候,会直接的跳转到对应的界面 18 | */ 19 | public class VoIPService extends Service { 20 | 21 | //音频播放的变量 22 | private ApiProvider provider; // 唯一的作用就是将这个变量保存到这里。 23 | 24 | public VoIPService() { 25 | 26 | } 27 | 28 | // 创建一个netty进行监听端口。 29 | @Override 30 | public void onCreate() { 31 | super.onCreate(); 32 | provider = ApiProvider.getProvider(); 33 | registerCallBack(); 34 | } 35 | 36 | /** 37 | * 此接口只是单纯的监听PHONE_MAKE_CALL请求, 38 | */ 39 | public void registerCallBack(){ 40 | provider.registerFrameResultedCallback(new NettyReceiverHandler.FrameResultedCallback() { 41 | 42 | @Override 43 | public void onTextMessage(String msg) { 44 | Log.e("ccc", "收到消息" + msg); 45 | if (Integer.parseInt(msg) == PHONE_MAKE_CALL){ 46 | startActivity(); 47 | } 48 | } 49 | 50 | @Override 51 | public void onAudioData(byte[] data) { 52 | 53 | } 54 | 55 | @Override 56 | public void onGetRemoteIP(String ip) { 57 | if ((!ip.equals(""))){ // 当IP不为空 则更改provider中的IP地址。 58 | provider.setTargetIP(ip); 59 | } 60 | } 61 | }); 62 | } 63 | 64 | public ApiProvider getProvider() { 65 | return provider; 66 | } 67 | 68 | /** 69 | * 返回一个Binder对象 70 | */ 71 | @Override 72 | public IBinder onBind(Intent intent) { 73 | 74 | return new MyBinder(); 75 | } 76 | // 这样外部就可以直接的获取到Service对象,然后就可以直接的操作。 77 | 78 | //1.service中有个类部类继承Binder,然后提供一个公有方法,返回当前service的实例。 79 | public class MyBinder extends Binder { 80 | public VoIPService getService(){ 81 | return VoIPService.this; 82 | } 83 | } 84 | 85 | // 从service中启动一个服务。 86 | private void startActivity(){ 87 | Intent intent = new Intent(this,VoIpP2PActivity.class); 88 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 89 | intent.putExtra(IFS,true); 90 | startActivity(intent); 91 | } 92 | 93 | @Override 94 | public void onDestroy() { 95 | super.onDestroy(); 96 | // 关闭所有的组件。 97 | provider.shutDownSocket(); 98 | provider.stopRecordAndPlay(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/utils/NetUtils.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.utils; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.net.NetworkInfo; 6 | import android.net.wifi.WifiInfo; 7 | import android.net.wifi.WifiManager; 8 | 9 | import java.net.Inet4Address; 10 | import java.net.InetAddress; 11 | import java.net.NetworkInterface; 12 | import java.net.SocketException; 13 | import java.util.Enumeration; 14 | 15 | /** 16 | * Describe: 网络工具类,获取本机ip地址。 17 | */ 18 | public class NetUtils { 19 | 20 | 21 | /** 22 | * 获取本机IP地址 23 | * @param context 24 | * @return 25 | */ 26 | public static String getIPAddress(Context context) { 27 | NetworkInfo info = ((ConnectivityManager) context 28 | .getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo(); 29 | if (info != null && info.isConnected()) { 30 | if (info.getType() == ConnectivityManager.TYPE_MOBILE) {//当前使用2G/3G/4G网络 31 | try { 32 | //Enumeration en=NetworkInterface.getNetworkInterfaces(); 33 | for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { 34 | NetworkInterface intf = en.nextElement(); 35 | for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { 36 | InetAddress inetAddress = enumIpAddr.nextElement(); 37 | if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) { 38 | return inetAddress.getHostAddress(); 39 | } 40 | } 41 | } 42 | } catch (SocketException e) { 43 | e.printStackTrace(); 44 | } 45 | 46 | } else if (info.getType() == ConnectivityManager.TYPE_WIFI) {//当前使用无线网络 47 | WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); 48 | WifiInfo wifiInfo = wifiManager.getConnectionInfo(); 49 | String ipAddress = intIP2StringIP(wifiInfo.getIpAddress());//得到IPV4地址 50 | return ipAddress; 51 | } 52 | } else { 53 | //当前无网络连接,请在设置中打开网络 54 | } 55 | return null; 56 | } 57 | 58 | /** 59 | * 将得到的int类型的IP转换为String类型 60 | * 61 | * @param ip 62 | * @return 63 | */ 64 | public static String intIP2StringIP(int ip) { 65 | return (ip & 0xFF) + "." + 66 | ((ip >> 8) & 0xFF) + "." + 67 | ((ip >> 16) & 0xFF) + "." + 68 | (ip >> 24 & 0xFF); 69 | } 70 | 71 | 72 | /** 73 | * 检测输入的IP是否合法。 74 | * @param text 输入的IP 75 | * @return TRUE OR FALSE 76 | */ 77 | public static boolean ipCheck(String text) { 78 | if (text != null && !text.isEmpty()) { 79 | // 定义正则表达式 80 | String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\." 81 | + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\." +"(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\." 82 | + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$"; 83 | // 判断ip地址是否与正则表达式匹配 84 | // 返回判断信息 85 | // 返回判断信息 86 | return text.matches(regex); 87 | } 88 | return false; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/utils/PermissionManager.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.utils; 2 | 3 | import android.content.Context; 4 | import android.support.annotation.NonNull; 5 | 6 | import com.yanzhenjie.permission.Action; 7 | import com.yanzhenjie.permission.AndPermission; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Describe: 管理安卓权限申请。 13 | */ 14 | public class PermissionManager { 15 | 16 | public static void requestPermission(final Context context, final Callback callback, String... permissions) { 17 | AndPermission.with(context) 18 | .permission(permissions) 19 | // .rationale(mRationale) 20 | .onGranted(new Action() { 21 | @Override 22 | public void onAction(List permissions) { 23 | if (callback != null) { 24 | callback.permissionSuccess(); 25 | 26 | } 27 | } 28 | }) 29 | .onDenied(new Action() { 30 | @Override 31 | public void onAction(@NonNull List permissions) { 32 | //授权失败?是否弹出窗 33 | if (callback != null) 34 | callback.permissionFailed(); 35 | if (AndPermission.hasAlwaysDeniedPermission(context, permissions)) { 36 | } 37 | } 38 | }) 39 | .start(); 40 | } 41 | 42 | public interface Callback { 43 | void permissionSuccess(); 44 | void permissionFailed(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/utils/mixAduioUtils/AudioUtil.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.utils.mixAduioUtils; 2 | 3 | import android.media.AudioFormat; 4 | import android.media.AudioRecord; 5 | import android.media.MediaRecorder; 6 | import android.os.Environment; 7 | import android.util.Log; 8 | 9 | import java.io.BufferedOutputStream; 10 | import java.io.File; 11 | import java.io.FileNotFoundException; 12 | import java.io.FileOutputStream; 13 | import java.io.IOException; 14 | import java.io.OutputStream; 15 | 16 | /** 17 | * Describe: 此工具类进行混音的测试工具类,1 实现录音 2 创建文件和文件夹。 18 | */ 19 | public class AudioUtil { 20 | private static AudioUtil mInstance; 21 | private AudioRecord recorder; 22 | //声音源 23 | private static int audioSource = MediaRecorder.AudioSource.MIC; 24 | //录音的采样频率 25 | private static int audioRate = 44100; 26 | //录音的声道,单声道 27 | private static int audioChannel = AudioFormat.CHANNEL_IN_STEREO; 28 | //量化的精度 29 | private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT; 30 | //缓存的大小 31 | private static int bufferSize = AudioRecord.getMinBufferSize(audioRate , audioChannel , audioFormat); 32 | //记录播放状态 33 | private boolean isRecording = false; 34 | //数字信号数组 35 | private byte[] noteArray; 36 | //PCM文件 37 | private File pcmFile; 38 | //wav文件 39 | private File wavFile; 40 | //文件输出流 41 | private OutputStream os; 42 | //文件根目录 43 | private String basePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/record/"; 44 | 45 | private AudioUtil() 46 | { 47 | } 48 | 49 | //创建文件夹,首先创建目录,根据传递过来的文件名进行构建新的文件。 50 | public void createFile(String fileName) 51 | { 52 | 53 | File baseFile = new File(basePath); 54 | if (!baseFile.exists()) 55 | baseFile.mkdirs(); //创建一个目录。 56 | 57 | pcmFile = new File(basePath + fileName); 58 | 59 | if (pcmFile.exists()) //检测文件是否存在 60 | pcmFile.delete(); 61 | 62 | try 63 | { 64 | pcmFile.createNewFile(); //调用这个方法才会真实的进行文件的创建。 65 | } 66 | catch (IOException e) 67 | { 68 | e.printStackTrace(); 69 | } 70 | } 71 | 72 | //获取一个实例。 73 | public synchronized static AudioUtil getInstance() 74 | { 75 | if (mInstance == null) 76 | { 77 | mInstance = new AudioUtil(); 78 | } 79 | return mInstance; 80 | } 81 | 82 | //读取录音数字数据线程 83 | class WriteThread implements Runnable 84 | { 85 | @Override 86 | public void run() 87 | { 88 | writeData(); 89 | } 90 | } 91 | 92 | //录音线程执行体将录音信息写入文件。 93 | private void writeData() 94 | { 95 | noteArray = new byte[bufferSize]; 96 | //建立文件输出流 97 | try 98 | { 99 | //首先新建一个输入流的文件,然后将录音的数据 以字节的方式写入到pcm文件中去。 100 | os = new BufferedOutputStream(new FileOutputStream(pcmFile)); 101 | } 102 | catch (FileNotFoundException e) 103 | { 104 | e.printStackTrace(); 105 | } 106 | 107 | while (isRecording) 108 | { 109 | int recordSize = recorder.read(noteArray , 0 , bufferSize); 110 | if (recordSize > 0) 111 | { 112 | try 113 | { 114 | os.write(noteArray); 115 | } 116 | catch (IOException e) 117 | { 118 | e.printStackTrace(); 119 | } 120 | } 121 | } 122 | 123 | if (os != null) 124 | { 125 | try 126 | { 127 | os.close(); 128 | } 129 | catch (IOException e) 130 | { 131 | e.printStackTrace(); 132 | } 133 | } 134 | } 135 | 136 | //开始录音,设置录音参数 137 | public void startRecord() 138 | { 139 | recorder = new AudioRecord(audioSource , audioRate , 140 | audioChannel , audioFormat , bufferSize); 141 | isRecording = true; 142 | recorder.startRecording(); 143 | } 144 | 145 | //记录数据 146 | public void recordData() 147 | { 148 | new Thread(new WriteThread()).start(); 149 | } 150 | 151 | //停止录音 152 | public void stopRecord() 153 | { 154 | if (recorder != null) 155 | { 156 | isRecording = false; 157 | recorder.stop(); //释放资源 158 | recorder.release(); 159 | recorder = null; 160 | 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/utils/mixAduioUtils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.utils.mixAduioUtils; 2 | 3 | import android.os.Environment; 4 | import android.util.Log; 5 | 6 | import java.io.File; 7 | import java.io.FileInputStream; 8 | import java.io.IOException; 9 | 10 | /** 11 | * Describe: 文件工具类 12 | * 实现获取基础路径,实现将文件以字节的方式进行读取到byte数组中。 13 | */ 14 | public class FileUtils { 15 | 16 | private static String basePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/record/"; 17 | 18 | public static String getFileBasePath(){ 19 | return basePath; 20 | } 21 | 22 | //将文件流读取到数组中, 23 | public static byte[] getContent(String filePath) throws IOException { 24 | File file = new File(filePath); 25 | long fileSize = file.length(); 26 | if (fileSize > Integer.MAX_VALUE) { 27 | Log.d("ccc","file too big..."); 28 | return null; 29 | } 30 | FileInputStream fi = new FileInputStream(file); 31 | byte[] buffer = new byte[(int) fileSize]; 32 | int offset = 0; 33 | int numRead = 0; 34 | //while循环会使得read一直进行读取,fi.read()在读取完数据以后会返回-1 35 | while (offset < buffer.length 36 | && (numRead = fi.read(buffer, offset, buffer.length - offset)) >= 0) { 37 | offset += numRead; 38 | } 39 | //确保所有数据均被读取 40 | if (offset != buffer.length) { 41 | throw new IOException("Could not completely read file " 42 | + file.getName()); 43 | } 44 | fi.close(); 45 | return buffer; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/proposeme/seven/phonecall/utils/mixAduioUtils/MixAudioUtil.java: -------------------------------------------------------------------------------- 1 | package com.proposeme.seven.phonecall.utils.mixAduioUtils; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.File; 6 | 7 | import java.io.FileOutputStream; 8 | import java.io.IOException; 9 | 10 | /** 11 | * Describe: 混音工作类 12 | */ 13 | 14 | public class MixAudioUtil { 15 | /** 16 | * 采用简单的平均算法 average audio mixing algorithm 17 | * 测试发现这种算法会降低 录制的音量 18 | * 混合pcm的算法,并且作为文件进行保存 19 | * 原理:量化的语音信号的叠加等价于空气中声波的叠加,反应到音频数据上,也就是把同一个声道的数值进行简单的相加 20 | */ 21 | 22 | public static byte[] averageMix(String file1,String file2) throws IOException { 23 | 24 | byte[][] bMulRoadAudioes = new byte[][]{ 25 | FileUtils.getContent(file1), //第一个文件 26 | FileUtils.getContent(file2) //第二个文件 27 | }; 28 | 29 | 30 | byte[] realMixAudio = bMulRoadAudioes[0]; //保存混音之后的数据。 31 | Log.e("ccc", " bMulRoadAudioes length " + bMulRoadAudioes.length); //2 32 | //判断两个文件的大小是否相同,如果不同进行补齐操作 33 | for (int rw = 0; rw < bMulRoadAudioes.length; ++rw) { //length一直都是等于2.依次检测file长度和file2长度 34 | if (bMulRoadAudioes[rw].length != realMixAudio.length) { 35 | Log.e("ccc", "column of the road of audio + " + rw + " is diffrent."); 36 | if (bMulRoadAudioes[rw].lengthrealMixAudio.length){ 40 | bMulRoadAudioes[rw] = subBytes(bMulRoadAudioes[rw],0,realMixAudio.length); 41 | } 42 | } 43 | } 44 | 45 | int row = bMulRoadAudioes.length; //行 46 | int column = realMixAudio.length / 2; //列 47 | short[][] sMulRoadAudioes = new short[row][column]; 48 | for (int r = 0; r < row; ++r) { //前半部分 49 | for (int c = 0; c < column; ++c) { 50 | sMulRoadAudioes[r][c] = (short) ((bMulRoadAudioes[r][c * 2] & 0xff) | (bMulRoadAudioes[r][c * 2 + 1] & 0xff) << 8); 51 | } 52 | } 53 | short[] sMixAudio = new short[column]; 54 | int mixVal; 55 | int sr = 0; 56 | for (int sc = 0; sc < column; ++sc) { 57 | mixVal = 0; 58 | sr = 0; 59 | for (; sr < row; ++sr) { 60 | mixVal += sMulRoadAudioes[sr][sc]; 61 | } 62 | sMixAudio[sc] = (short) (mixVal / row); 63 | } 64 | 65 | //合成混音保存在realMixAudio 66 | for (sr = 0; sr < column; ++sr) { //后半部分 67 | realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF); 68 | realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8); 69 | } 70 | 71 | //保存混合之后的pcm 72 | FileOutputStream fos = null; 73 | //保存合成之后的文件。 74 | File saveFile = new File(FileUtils.getFileBasePath()+ "averageMix.pcm" ); 75 | if (saveFile.exists()) { 76 | saveFile.delete(); 77 | } 78 | fos = new FileOutputStream(saveFile);// 建立一个可存取字节的文件 79 | fos.write(realMixAudio); 80 | fos.close();// 关闭写入流 81 | return realMixAudio; //返回合成的混音。 82 | } 83 | 84 | //合并两个音轨。 85 | private static byte[] subBytes(byte[] src, int begin, int count) { 86 | byte[] bs = new byte[count]; 87 | System.arraycopy(src, begin, bs, 0, count); 88 | return bs; 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_style_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_style_white_normal.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/btn_style_white_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_hangup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostStarTvT/PhoneCall/8663db491089ed91d9cbdf89b9451b18d3c876dd/app/src/main/res/drawable/icon_hangup.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_mic_picup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostStarTvT/PhoneCall/8663db491089ed91d9cbdf89b9451b18d3c876dd/app/src/main/res/drawable/icon_mic_picup.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/starfox_500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostStarTvT/PhoneCall/8663db491089ed91d9cbdf89b9451b18d3c876dd/app/src/main/res/drawable/starfox_500.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 16 | 17 |