Java Socket实时音频流:实现服务器到客户端音频传输反转
实现服务器采集音频并推送至客户端播放的方案
没问题!我来帮你把现有的音频流逻辑完全反转,让服务器负责采集本地音频并通过UDP发送给客户端,客户端接收后直接播放。咱们基于你现有的代码做针对性修改:
核心逻辑反转说明
原来的流程是:客户端采集 → UDP发送 → 服务器接收 → 服务器播放
现在要改成:服务器采集 → UDP发送 → 客户端接收 → 客户端播放
核心改动就是把「采集逻辑」从客户端移到服务器,把「播放逻辑」从服务器移到客户端,同时调整UDP的发送/接收方向。
修改后的服务器代码(负责采集+发送)
import java.io.ByteArrayOutputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.TargetDataLine; public class VUServer { boolean stopAudioCapture = false; ByteArrayOutputStream byteOutputStream; AudioFormat adFormat; TargetDataLine targetDataLine; // 记录客户端的地址和端口,用于推送音频 private InetAddress clientAddress; private int clientPort; private AudioFormat getAudioFormat() { float sampleRate = 16000.0F; int sampleInbits = 16; int channels = 1; boolean signed = true; boolean bigEndian = false; return new AudioFormat(sampleRate, sampleInbits, channels, signed, bigEndian); } public static void main(String args[]) { new VUServer().runVOIP(); } public void runVOIP() { try { // 先创建UDP Socket监听客户端的连接请求 DatagramSocket serverSocket = new DatagramSocket(9786); byte[] handshakeBuffer = new byte[10]; DatagramPacket handshakePacket = new DatagramPacket(handshakeBuffer, handshakeBuffer.length); // 等待客户端发送握手包,获取客户端地址 serverSocket.receive(handshakePacket); clientAddress = handshakePacket.getAddress(); clientPort = handshakePacket.getPort(); System.out.println("已连接客户端:" + clientAddress.getHostAddress() + ":" + clientPort); // 启动音频采集线程 captureAudio(serverSocket); } catch (Exception e) { e.printStackTrace(); } } private void captureAudio(DatagramSocket serverSocket) { try { adFormat = getAudioFormat(); DataLine.Info dataLineInfo = new DataLine.Info(TargetDataLine.class, adFormat); targetDataLine = (TargetDataLine) AudioSystem.getLine(dataLineInfo); targetDataLine.open(adFormat); targetDataLine.start(); Thread captureThread = new Thread(new CaptureThread(serverSocket)); captureThread.start(); } catch (Exception e) { e.printStackTrace(); System.exit(0); } } class CaptureThread extends Thread { private DatagramSocket socket; byte tempBuffer[] = new byte[10000]; public CaptureThread(DatagramSocket socket) { this.socket = socket; } public void run() { stopAudioCapture = false; try { while (!stopAudioCapture) { int cnt = targetDataLine.read(tempBuffer, 0, tempBuffer.length); if (cnt > 0) { // 把采集到的音频发送给客户端 DatagramPacket sendPacket = new DatagramPacket(tempBuffer, cnt, clientAddress, clientPort); socket.send(sendPacket); } } targetDataLine.close(); socket.close(); } catch (Exception e) { System.out.println("CaptureThread::run()" + e); System.exit(0); } } } }
修改后的客户端代码(负责接收+播放)
import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.net.*; import javax.sound.sampled.*; public class VUClient extends JFrame { boolean stopAudioPlay = false; AudioFormat adFormat; SourceDataLine sourceLine; DatagramSocket clientSocket; public static void main(String args[]) { new VUClient(); } public VUClient() { final JButton connect = new JButton("连接服务器"); final JButton stop = new JButton("停止播放"); connect.setEnabled(true); stop.setEnabled(false); connect.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { connect.setEnabled(false); stop.setEnabled(true); connectAndPlayAudio(); } }); getContentPane().add(connect); stop.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { connect.setEnabled(true); stop.setEnabled(false); stopAudioPlay = true; if (sourceLine != null) { sourceLine.stop(); sourceLine.close(); } if (clientSocket != null) { clientSocket.close(); } } }); getContentPane().add(stop); getContentPane().setLayout(new FlowLayout()); setTitle("服务器音频播放客户端"); setDefaultCloseOperation(EXIT_ON_CLOSE); setSize(400, 100); getContentPane().setBackground(Color.white); setVisible(true); } private void connectAndPlayAudio() { try { adFormat = getAudioFormat(); // 初始化播放线路 DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, adFormat); sourceLine = (SourceDataLine) AudioSystem.getLine(dataLineInfo); sourceLine.open(adFormat); sourceLine.start(); // 创建UDP Socket并发送握手包给服务器 clientSocket = new DatagramSocket(); InetAddress serverAddress = InetAddress.getByName("127.0.0.1"); byte[] handshakeData = "CONNECT".getBytes(); DatagramPacket handshakePacket = new DatagramPacket(handshakeData, handshakeData.length, serverAddress, 9786); clientSocket.send(handshakePacket); // 启动接收音频线程 Thread playThread = new Thread(new PlayThread()); playThread.start(); } catch (Exception e) { e.printStackTrace(); System.exit(0); } } private AudioFormat getAudioFormat() { float sampleRate = 16000.0F; int sampleInbits = 16; int channels = 1; boolean signed = true; boolean bigEndian = false; return new AudioFormat(sampleRate, sampleInbits, channels, signed, bigEndian); } class PlayThread extends Thread { byte tempBuffer[] = new byte[10000]; public void run() { stopAudioPlay = false; try { while (!stopAudioPlay) { DatagramPacket receivePacket = new DatagramPacket(tempBuffer, tempBuffer.length); clientSocket.receive(receivePacket); // 直接播放接收到的音频数据 sourceLine.write(receivePacket.getData(), 0, receivePacket.getLength()); } } catch (Exception e) { System.out.println("PlayThread::run()" + e); } } } }
关键改动点说明
服务器侧:
- 新增了握手逻辑,先接收客户端的连接请求以获取客户端的IP和端口,确保音频能精准推送
- 把原来客户端的
CaptureThread移到服务器,负责采集本地音频并通过UDP发送给客户端 - 移除了原来服务器的播放逻辑,专注于采集和发送
客户端侧:
- 移除了原来的采集逻辑,新增了UDP接收线程,负责接收服务器推送的音频
- 把原来服务器的播放逻辑移到客户端,用
SourceDataLine直接播放接收到的音频数据 - 新增了连接/停止按钮,替代原来的采集/播放按钮,符合新的业务逻辑
通用优化:
- 避免了每次接收音频都创建新的
SourceDataLine,复用同一条线路播放,减少资源浪费 - 修正了UDP数据包的长度问题,发送时只发送实际采集到的字节数(
cnt),避免发送空数据
- 避免了每次接收音频都创建新的
现在你可以先启动服务器,再启动客户端点击「连接服务器」,就能听到服务器采集的音频在客户端播放啦!
内容的提问来源于stack exchange,提问作者yitow53
相关产品推荐
相关产品推荐

