基於Java開發客戶端音頻採集播放、UDP協議轉發程序

一、綜述

學習使用Java開發語言做計算機音頻數據採集、壓縮、轉發功能,從而實現雙向通話功能。採集數據頻率爲8KHz、16bit、單通道、小端格式,數據轉發採用G711A壓縮傳輸。

二、音頻採樣率

1. 參考百度百科

爲了測試語音通話,音頻採樣率爲8KHz即可滿足要求。

2. 在數字音頻領域,常用的採樣率有:

  • 8,000 Hz - 電話所用採樣率, 對於人的說話已經足夠
  • 11,025 Hz-AM調幅廣播所用採樣率
  • 22,050 Hz和24,000 Hz- FM調頻廣播所用採樣率
  • 32,000 Hz - miniDV 數碼視頻 camcorder、DAT (LP mode)所用採樣率
  • 44,100 Hz - 音頻 CD, 也常用於 MPEG-1 音頻(VCD, SVCD, MP3)所用採樣率
  • 47,250 Hz - 商用 PCM 錄音機所用採樣率
  • 48,000 Hz - miniDV、數字電視、DVD、DAT、電影和專業音頻所用的數字聲音所用採樣率
  • 50,000 Hz - 商用數字錄音機所用採樣率
  • 96,000 或者 192,000 Hz - DVD-Audio、一些 LPCM DVD 音軌、BD-ROM(藍光盤)音軌、和 HD-DVD (高清晰度 DVD)音軌所用所用採樣率
  • 2.8224 MHz - Direct Stream Digital 的 1 位 sigma-delta modulation 過程所用採樣率。

三、開發環境

  • JDK: 1.8
  • MINA: 2.1.3
  • SpringBoot: 1.3.1.RELEASE

四、核心代碼

爲了記錄學習流程,接下來僅介紹核心代碼部分,具體完整程序參考源碼
本項目主要的業務邏輯分爲兩個部分:一是音頻採集部分,二是數據通訊轉發和接收部分。爲了保證兩部分的業務鬆耦合,需要對通訊和採集播放做程序設計上分離。

1. Socket通訊組件開發

爲了保證底層通訊的和上層業務的鬆耦合,通訊部分做以下程序設計:

  • (1)將通訊框架的數據統一轉爲ByteBuffer做進一步的業務處理。例如將Mina的IoBuffer數據轉爲ByteBuffer交付於上層業務處理,從而保證對Mina通訊框架的鬆耦合,爲以後切換其他的通訊框架做準備(比如Netty)
  • (2)統一定義Socket connector的接口標準,方便擴展。
  • (3)爲了減少音頻採集部分對通訊Connector的過渡依賴,採用單例工廠模式保證對外提供的通訊服務,管理所有啓動的socket服務及釋放資源等

1.1 SocketMessageHandler

定義消息上層業務轉發統一接口,需要音頻採集處理部分傳遞該處理部分。

package com.david.test.socket;

import java.nio.ByteBuffer;

public interface SocketMessageHandler {
	
	/**
	 * 獲取消息
	 * @param byteBuffer
	 */
	public void onMessage(ByteBuffer byteBuffer);
}

1.2 SocketConnector

該類定義一個底層通訊的Connector的一些對外統一接口。

package com.david.test.socket;

import java.nio.ByteBuffer;

public interface SocketConnector {
	/**
	 * 獲取Connector ID
	 * @return
	 */
	public String getId();
	/**
	 * 啓動一個客戶端
	 * @throws Exception
	 */
	public void startClient(SocketConnectorParams socketConnectorParams,SocketMessageHandler socketMessageHandler) throws Exception;
	
	/**
	 * 關閉客戶端
	 * @throws Exception
	 */
	public void stopClient() throws Exception;
	
	/**
	 * 發送消息
	 * @param byteBuffer
	 * @throws Exception
	 */
	public void sendMessage(ByteBuffer byteBuffer) throws Exception;
	
	/**
	 * 連接是否存在
	 * @throws Exception
	 */
	public boolean isActive() throws Exception;
}

1.3 DefaultMinaUDPSocketConnector

基於Mina實現的缺省的UDP通訊Connector,源碼如下:

package com.david.test.socket.connector;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.Date;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.proxy.utils.ByteUtilities;
import org.apache.mina.transport.socket.nio.NioDatagramConnector;
import org.springframework.util.StringUtils;

import com.david.test.socket.SocketConnector;
import com.david.test.socket.SocketConnectorParams;
import com.david.test.socket.SocketMessageHandler;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class DefaultMinaUDPSocketConnector implements SocketConnector{
	private IoConnector connector;
	private IoSession ioSession;
	private String connectorId;
	
	@Override
	public String getId() {
		// TODO Auto-generated method stub
		return this.connectorId;
	}

	@Override
	public boolean isActive() throws Exception {
		// TODO Auto-generated method stub
		return null == ioSession?false:ioSession.isActive();
	}

	@Override
	public void stopClient() throws Exception {
		// TODO Auto-generated method stub
		try {
			if(null != ioSession){
				ioSession.closeNow();
				ioSession = null;
			}
			if(null != connector){
				connector.dispose();
				connector = null;
			}
			log.info("STOP UDP CLIENT SUCCESS, ID:{}",this.connectorId);
		} catch (Exception e) {
			log.error("Stop UDP Client Error", e);
		}
	}

	@Override
	public void startClient(SocketConnectorParams socketConnectorParams,SocketMessageHandler socketMessageHandler) throws Exception {
		// TODO Auto-generated method stub
		if(null == socketConnectorParams || StringUtils.isEmpty(socketConnectorParams.getHost()) 
				|| socketConnectorParams.getPort() < 1){
			throw new Exception("參數配置有誤");
		}
		if(null == socketMessageHandler){
			throw new Exception("Socket Message Handler 消息解析器配置異常");
		}
		//connector Id
		this.connectorId = Thread.currentThread().getName()+"_"+new Date().getTime();
		try {
			InetSocketAddress inetSocketAddress = new InetSocketAddress(socketConnectorParams.getHost(), socketConnectorParams.getPort());
			connector = new NioDatagramConnector();
			connector.getFilterChain().addLast("logger", new LoggingFilter());
			connector.setHandler(new MinaUDPIoHandler(socketConnectorParams,socketMessageHandler));
			ConnectFuture connectFuture = connector.connect(inetSocketAddress);
			// 等待是否連接成功,相當於是轉異步執行爲同步執行。
			connectFuture.awaitUninterruptibly();
			log.info("Mina start UDP Client SUCCESS, ID:{}",this.connectorId);
		} catch (Exception e) {
			log.error("Mina start UDP Client Error", e);
		}
	}
	
	@Override
	public void sendMessage(ByteBuffer byteBuffer) throws Exception {
		// TODO Auto-generated method stub
		if(null == ioSession){
			throw new Exception("UDP session is released");
		}
		ioSession.write(IoBuffer.wrap(byteBuffer.array()));
		log.info("ID:{},SOCKET SEND:{}",this.connectorId,ByteUtilities.asHex(byteBuffer.array()).toUpperCase());
	}
	
	// Mina UDP 數據處理
	class MinaUDPIoHandler extends IoHandlerAdapter{
		private SocketConnectorParams socketConnectorParams;
		private SocketMessageHandler socketMessageHandler;
		
		public MinaUDPIoHandler(SocketConnectorParams socketConnectorParams, SocketMessageHandler socketMessageHandler) {
			this.socketConnectorParams = socketConnectorParams;
			this.socketMessageHandler = socketMessageHandler;
		}

		@Override
		public void sessionOpened(IoSession session) throws Exception {
			// TODO Auto-generated method stub
			super.sessionOpened(session);
			ioSession = session;
		}

		@Override
		public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
			// TODO Auto-generated method stub
			super.sessionIdle(session, status);
		}

		@Override
		public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
			// TODO Auto-generated method stub
			super.exceptionCaught(session, cause);
		}

		@Override
		public void messageReceived(IoSession session, Object message) throws Exception {
			// TODO Auto-generated method stub
			super.messageReceived(session, message);
			try {
				IoBuffer ioBuffer = (IoBuffer)message;
				int capacity = ioBuffer.capacity();
				int limit = ioBuffer.limit();
				byte[] data = new byte[limit];
				ioBuffer.get(data, 0, limit);
				log.info("RECV DATA: capacity[{}] limit[{}] data:{}",capacity,limit,ByteUtilities.asHex(data).toUpperCase());
				if(null != this.socketMessageHandler){
					this.socketMessageHandler.onMessage(ByteBuffer.wrap(data));
				}
			} catch (Exception e) {
				log.error("Mina UDP RECV Message Error", e);
			}
		}

		@Override
		public void messageSent(IoSession session, Object message) throws Exception {
			// TODO Auto-generated method stub
			super.messageSent(session, message);
		}
	}
}

1.4 SocketConnectorFactory

統一管理系統中的所有的底層通訊Connector,提供服務。

package com.david.test.socket;
import java.util.LinkedList;
import java.util.List;

import com.david.test.socket.connector.DefaultMinaUDPSocketConnector;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SocketConnectorFactory {
	private static SocketConnectorFactory socketConnectorFactory = new SocketConnectorFactory();
	private static List<SocketConnector> socketConnectors = new LinkedList<>();
	
	public static SocketConnectorFactory getInstance(){
		return socketConnectorFactory;
	}
	
	/**
	 * 創建一個UDPClient
	 * @return
	 */
	public SocketConnector createUDPClient(){
		SocketConnector socketConnector = new DefaultMinaUDPSocketConnector();
		socketConnectors.add(socketConnector);
		return socketConnector;
	}
	
	/**
	 * 啓動一個UDPClient
	 * @param socketConnectorParams
	 * @param socketMessageHandler
	 * @return
	 * @throws Exception
	 */
	public SocketConnector startUDPClient(SocketConnectorParams socketConnectorParams,
			SocketMessageHandler socketMessageHandler) throws Exception{
		SocketConnector socketConnector = new DefaultMinaUDPSocketConnector();
		socketConnector.startClient(socketConnectorParams, socketMessageHandler);
		socketConnectors.add(socketConnector);
		return socketConnector;
	}
	
	/**
	 * 根據Connector ID移除Socket連接
	 * @param id
	 * @throws Exception
	 */
	public void releaseSocketConnector(String id) throws Exception{
		for(SocketConnector socketConnector:socketConnectors){
			try {
				if(socketConnector.getId().equals(id)){
					socketConnector.stopClient();
					socketConnectors.remove(socketConnector);
					break;
				}
			} catch (Exception e) {
				log.error("關閉客戶端異常", e);
			}
		}
		log.info("移除Socket監聽器,剩餘socket監聽器數量:{}",socketConnectors.size());
	}
	
	/**
	 * 釋放所有的socket連接
	 * @throws Exception
	 */
	public void releaseAllSocketConnectors() throws Exception{
		List<SocketConnector> toRemovedSocketConnectors = new LinkedList<>();
		for(SocketConnector socketConnector:socketConnectors){
			try {
				socketConnector.stopClient();
				toRemovedSocketConnectors.add(socketConnector);
			} catch (Exception e) {
				log.error("關閉客戶端異常", e);
			}
		}
		socketConnectors.removeAll(toRemovedSocketConnectors);
		log.info("移除Socket監聽器數量:{},剩餘socket監聽器數量:{}",
				toRemovedSocketConnectors.size(),socketConnectors.size());
	}
}

1.5 小結

以上1.1-1.4部分即完成了通訊服務的業務處理規則,此時還不涉及到業務開發,僅爲通訊部分數據傳輸的統一標準。方便以後的底層通訊框架的切換等。

2. 音頻數據採集轉發和接收播放

音頻採集是一個持續的過程,需要在子線程中處理。整體的思路如下:採集轉發在子線程中處理,接收播放在主線程中處理即可(因爲接收播放是異步通訊框架傳遞過來的數據)

2.1 UserAudioMedia

定義音頻採集部分的規範,因爲音頻的採樣率不同及壓縮方式不同會存在不同的實現方式,定義統一的接口,便於擴展。

  • SocketMessageHandler: 該接口爲和通訊框架之間的數據傳遞規範
  • Runnable: 該接口實現線程的Runnable接口,用於採集計算機音頻數據轉發
package com.david.test.audio;

import com.david.test.socket.SocketMessageHandler;

public interface UserAudioMedia extends SocketMessageHandler,Runnable{
	/**
	 * 開始
	 */
	public void start();
	
	/**
	 * 停止
	 */
	public void stop();
	
}

2.2 DefaultUserAudioMedia

PCM原始音頻流數據採集轉發及接收播放

package com.david.test.audio.media;

import java.io.IOException;
import java.nio.ByteBuffer;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.TargetDataLine;

import com.david.test.audio.UserAudioMedia;
import com.david.test.socket.SocketConnector;
import com.david.test.socket.SocketConnectorFactory;
import com.david.test.socket.SocketConnectorParams;
import com.david.test.utils.GsonUtils;

import lombok.extern.slf4j.Slf4j;
/**
 * 傳輸:PCM傳輸
 * 播放:PCM原始流播放
* @author 作者 :david E-mail:[email protected] 
* @date 創建時間:2020年4月18日 上午10:24:37 
* @version 1.0 
* @since 2020年4月18日 上午10:24:37
 */
@Slf4j
public class DefaultUserAudioMedia implements UserAudioMedia{
	private SocketConnectorParams socketConnectorParams;
	private RegistParam registParam;
	private SocketConnector socketConnector;
	private boolean isStop;
	private SourceDataLine sourceDataLine;
	private TargetDataLine targetDataLine;
	private Thread curThread = null;

	public DefaultUserAudioMedia(SocketConnectorParams socketConnectorParams,RegistParam registParam) throws Exception{
		this.socketConnectorParams = socketConnectorParams;
		this.registParam = registParam;
		this.socketConnector = SocketConnectorFactory.getInstance().startUDPClient(socketConnectorParams, this);
		
	}
	
	/**
	 * 停止
	 */
	@Override
	public void stop(){
		this.isStop = true;
	}
	
	/**
	 * 發起註冊監聽
	 * @param registParam 
	 */
	@Override
	public void start() {
		// TODO Auto-generated method stub
		try {
			//發起註冊
			String paramStr = GsonUtils.toJson(registParam);
			log.info("SocketConnector Status:{},Info:{},REGIST:{}",
					this.socketConnector.isActive(),registParam.getContent(),paramStr);
			this.socketConnector.sendMessage(ByteBuffer.wrap(paramStr.getBytes()));
			//啓動音頻獲取
			curThread = new Thread(this);
			curThread.start();
		} catch (Exception e) {
			log.error("發起註冊請求異常", e);
		}
	}

	@Override
	public void onMessage(ByteBuffer byteBuffer) {
		// TODO Auto-generated method stub
		try {
			if(null == sourceDataLine){
				AudioFormat audioFormat = new AudioFormat(8000, 16, 1, true ,false);
				DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
				sourceDataLine = (SourceDataLine) AudioSystem.getLine(info);
				sourceDataLine.open(audioFormat);
				sourceDataLine.start();
				//set voice
				FloatControl fc = (FloatControl) sourceDataLine.getControl(FloatControl.Type.MASTER_GAIN);
				double value = 2;
				float dB = (float) (Math.log(value == 0.0 ? 0.0001 : value) / Math.log(10.0) * 20.0);
				fc.setValue(dB);
			}
			//play
			byte[] data = byteBuffer.array();
			sourceDataLine.write(data, 0, data.length);
		} catch (Exception e) {
			log.error("播放音頻異常", e);
		}
	}

	@Override
	public void run() {
		AudioInputStream audioInputStream = null;
		try {
			AudioFormat audioFormat = new AudioFormat(8000, 16, 1, true ,false);
			DataLine.Info info = new DataLine.Info(TargetDataLine.class, audioFormat);
			targetDataLine = (TargetDataLine) AudioSystem.getLine(info);
			targetDataLine.open(audioFormat);
			targetDataLine.start();
			
			audioInputStream = new AudioInputStream(targetDataLine);
			int len = 0;
			byte[] buffer = new byte[1024];
			while(!isStop && -1 != (len = audioInputStream.read(buffer))){
				ByteBuffer byteBuffer = ByteBuffer.wrap(buffer,0,len);
				this.socketConnector.sendMessage(byteBuffer);
			}
		} catch (Exception e) {
			log.error("採集音頻異常",e);
		} finally {
			if(null != sourceDataLine){
				sourceDataLine.close();
				sourceDataLine.stop();
			}
			if(null != audioInputStream){
				try {
					audioInputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if(null != targetDataLine){
				targetDataLine.close();
				targetDataLine.stop();
			}
			if(null != socketConnector){
				try {
					socketConnector.stopClient();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("UserAudioMedia [socketConnectorParams=");
		builder.append(socketConnectorParams);
		builder.append(", registParam=");
		builder.append(registParam);
		builder.append(", socketConnector=");
		builder.append(socketConnector);
		builder.append(", isStop=");
		builder.append(isStop);
		builder.append("]");
		return builder.toString();
	}
}

2.3 G711AUserAudioMedia

基於G711A編碼的PCM音頻流數據採集轉發及播放,G711A的音頻數據壓縮率在50%,傳輸播放效率較好。

package com.david.test.audio.media;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.TargetDataLine;

import com.david.test.audio.UserAudioMedia;
import com.david.test.socket.SocketConnector;
import com.david.test.socket.SocketConnectorFactory;
import com.david.test.socket.SocketConnectorParams;
import com.david.test.utils.G711Code;
import com.david.test.utils.GsonUtils;

import lombok.extern.slf4j.Slf4j;

/**
 * 傳輸:PCM流壓縮轉爲G711A傳輸
 * 播放:G711A流轉爲PCM原始流播放
* @author 作者 :david E-mail:[email protected] 
* @date 創建時間:2020年4月18日 上午10:22:53 
* @version 1.0 
* @since 2020年4月18日 上午10:22:53
 */
@Slf4j
public class G711AUserAudioMedia implements UserAudioMedia{
	private SocketConnectorParams socketConnectorParams;
	private RegistParam registParam;
	private SocketConnector socketConnector;
	private boolean isStop;
	private SourceDataLine sourceDataLine;
	private TargetDataLine targetDataLine;
	private Thread curThread = null;

	public G711AUserAudioMedia(SocketConnectorParams socketConnectorParams,RegistParam registParam) throws Exception{
		this.socketConnectorParams = socketConnectorParams;
		this.registParam = registParam;
		this.socketConnector = SocketConnectorFactory.getInstance().startUDPClient(socketConnectorParams, this);
	}
	
	/**
	 * 停止
	 */
	@Override
	public void stop(){
		this.isStop = true;
	}
	
	/**
	 * 發起註冊監聽
	 * @param registParam 
	 */
	@Override
	public void start() {
		// TODO Auto-generated method stub
		try {
			//發起註冊
			String paramStr = GsonUtils.toJson(registParam);
			log.info("SocketConnector Status:{},Info:{},REGIST:{}",
					this.socketConnector.isActive(),registParam.getContent(),paramStr);
			this.socketConnector.sendMessage(ByteBuffer.wrap(paramStr.getBytes()));
			//啓動音頻獲取
			curThread = new Thread(this);
			curThread.start();
		} catch (Exception e) {
			log.error("發起註冊請求異常", e);
		}
	}

	@Override
	public void onMessage(ByteBuffer byteBuffer) {
		// TODO Auto-generated method stub
		try {
			if(null == sourceDataLine){
				AudioFormat audioFormat = new AudioFormat(8000, 16, 1, true ,false);
				DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
				sourceDataLine = (SourceDataLine) AudioSystem.getLine(info);
				sourceDataLine.open(audioFormat);
				sourceDataLine.start();
				//set voice
				FloatControl fc = (FloatControl) sourceDataLine.getControl(FloatControl.Type.MASTER_GAIN);
				double value = 2;
				float dB = (float) (Math.log(value == 0.0 ? 0.0001 : value) / Math.log(10.0) * 20.0);
				fc.setValue(dB);
			}
			//play
			byte[] data = byteBuffer.array();
			//g711a -> pcm
			short[] pcm = new short[data.length];
			G711Code.G711aDecoder(pcm, data, data.length);
			byte[] orginPcm = new byte[data.length*2];
			ByteBuffer.wrap(orginPcm).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(pcm);
			sourceDataLine.write(orginPcm, 0, orginPcm.length);
		} catch (Exception e) {
			log.error("播放音頻異常", e);
		}
	}

	@Override
	public void run() {
		AudioInputStream audioInputStream = null;
		try {
			AudioFormat audioFormat = new AudioFormat(8000, 16, 1, true ,false);
			DataLine.Info info = new DataLine.Info(TargetDataLine.class, audioFormat);
			targetDataLine = (TargetDataLine) AudioSystem.getLine(info);
			targetDataLine.open(audioFormat);
			targetDataLine.start();
			
			audioInputStream = new AudioInputStream(targetDataLine);
			int len = 0;
			byte[] buffer = new byte[1024];
			while(!isStop && -1 != (len = audioInputStream.read(buffer))){
				//pcm -> g711a
				short[] shortBytes = new short[len/2];
				ByteBuffer.wrap(buffer,0,len)
						.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer()
						.get(shortBytes);
				byte[] g711a = new byte[shortBytes.length];
				G711Code.G711aEncoder(shortBytes, g711a, shortBytes.length);
				this.socketConnector.sendMessage(ByteBuffer.wrap(g711a));
			}
		} catch (Exception e) {
			log.error("採集音頻異常",e);
		} finally {
			if(null != sourceDataLine){
				sourceDataLine.close();
				sourceDataLine.stop();
			}
			if(null != audioInputStream){
				try {
					audioInputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if(null != targetDataLine){
				targetDataLine.close();
				targetDataLine.stop();
			}
			if(null != socketConnector){
				try {
					socketConnector.stopClient();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("UserAudioMedia [socketConnectorParams=");
		builder.append(socketConnectorParams);
		builder.append(", registParam=");
		builder.append(registParam);
		builder.append(", socketConnector=");
		builder.append(socketConnector);
		builder.append(", isStop=");
		builder.append(isStop);
		builder.append("]");
		return builder.toString();
	}
}

2.4 AudioControl

對外提供統一的業務接口管理服務,設計爲單例模式,用於控制服務的音頻採集實例。提供創建一個音頻採集實例(默認爲G711A)及停止採集等。

package com.david.test.audio;

import java.util.LinkedList;
import java.util.List;

import com.david.test.audio.media.G711AUserAudioMedia;
import com.david.test.audio.media.RegistParam;
import com.david.test.socket.SocketConnectorParams;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class AudioControl {
	private static AudioControl instance = new AudioControl();
	private static List<UserAudioMedia> userAudioMedias = new LinkedList<UserAudioMedia>();
	
	public static AudioControl getInstance(){
		return instance;
	}
	
	/**
	 * 獲取啓動的設備信息
	 * @return
	 */
	public List<String> getUserAudioMedias() {
		List<String> infos = new LinkedList<>();
		for(UserAudioMedia userAudioMedia:userAudioMedias){
			infos.add(userAudioMedia.toString());
		}
		return infos;
	}
	
	/**
	 * 清除數據
	 */
	public void stopUserAudioMedias(){
		for(UserAudioMedia userAudioMedia:userAudioMedias){
			userAudioMedia.stop();
		}
		userAudioMedias.clear();
	}

	/**
	 * 
	 * @return
	 */
	public void startUserAudioMedia(SocketConnectorParams socketConnectorParams,RegistParam registParam) throws Exception{
		UserAudioMedia userAudioMedia = new G711AUserAudioMedia(socketConnectorParams,registParam);
		userAudioMedia.start();
		userAudioMedias.add(userAudioMedia);
	}
}

3. Web提供接口啓動服務

提供簡單的web接口啓動服務、關閉服務等。

package com.david.test.web;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.david.test.audio.AudioControl;
import com.david.test.audio.media.RegistParam;
import com.david.test.audio.media.RegistParamClientContent;
import com.david.test.audio.media.RegistParamDeviceContent;
import com.david.test.socket.SocketConnectorParams;
import com.david.test.web.common.WebRetCodes;
import com.david.test.web.common.WebRetUtils;

/**
 * 通話web接口
* @author 作者 :david E-mail:[email protected] 
* @date 創建時間:2020年4月16日 下午4:57:05 
* @version 1.0 
* @since 2020年4月16日 下午4:57:05
* --start
* http://localhost/start?host=192.168.0.80&port=9898&company=&lineCode=&binddevice=123&ignoredevices=
* 
* --startDevice
* http://localhost/startDevice?host=192.168.0.80&port=9898&company=&lineCode=&id=123&password=
* 
* --stop
* http://localhost/stop
* 
* --connectors
* http://localhost/connectors
 */
@RestController
public class AudioToolWeb {
	
	/**
	 * PC端開啓通話
	 * @param request
	 * @param host
	 * @param port
	 * @param company
	 * @param lineCode
	 * @param binddevice
	 * @param ignoredevices
	 * @return
	 * @throws Exception
	 */
	@RequestMapping(value = "start", method = { RequestMethod.GET, RequestMethod.POST })
	public @ResponseBody Map<String, ? extends Object> start(HttpServletRequest request,
			@RequestParam String host,@RequestParam Integer port,@RequestParam String company,
			@RequestParam String lineCode,@RequestParam String binddevice,@RequestParam String ignoredevices)
			throws Exception {
		try {
			SocketConnectorParams socketConnectorParams = new SocketConnectorParams();
			socketConnectorParams.setHost(host);
			socketConnectorParams.setPort(port);
			RegistParam registParam = new RegistParam();
			RegistParamClientContent content = new RegistParamClientContent();
			content.setCompany(company);
			content.setLineCode(lineCode);
			content.setBinddevice(binddevice);
			content.setIgnoredevices(ignoredevices);
			registParam.setContent(content);
			AudioControl.getInstance().startUserAudioMedia(socketConnectorParams,registParam);
			return WebRetUtils.mapRet(WebRetCodes.OK);
		} catch (Exception e) {
			return WebRetUtils.mapRet(WebRetCodes.INNER_ERROR);
		}
	}
	
	/**
	 * 設備開啓通話
	 * @param request
	 * @param host
	 * @param port
	 * @param company
	 * @param lineCode
	 * @param id
	 * @param password
	 * @return
	 * @throws Exception
	 */
	@RequestMapping(value = "startDevice", method = { RequestMethod.GET, RequestMethod.POST })
	public @ResponseBody Map<String, ? extends Object> startDevice(HttpServletRequest request,
			@RequestParam String host,@RequestParam Integer port,@RequestParam String company,
			@RequestParam String lineCode,@RequestParam String id,@RequestParam String password)
			throws Exception {
		try {
			SocketConnectorParams socketConnectorParams = new SocketConnectorParams();
			socketConnectorParams.setHost(host);
			socketConnectorParams.setPort(port);
			RegistParam registParam = new RegistParam();
			RegistParamDeviceContent content = new RegistParamDeviceContent();
			content.setCompany(company);
			content.setLineCode(lineCode);
			content.setPassword(password);
			content.setId(id);
			registParam.setContent(content);
			AudioControl.getInstance().startUserAudioMedia(socketConnectorParams,registParam);
			return WebRetUtils.mapRet(WebRetCodes.OK);
		} catch (Exception e) {
			return WebRetUtils.mapRet(WebRetCodes.INNER_ERROR);
		}
	}
	
	/**
	 * 停止所有通話
	 * @param request
	 * @return
	 * @throws Exception
	 */
	@RequestMapping(value = "stop", method = { RequestMethod.GET, RequestMethod.POST })
	public @ResponseBody Map<String, ? extends Object> stop(HttpServletRequest request)
			throws Exception {
		try {
			AudioControl.getInstance().stopUserAudioMedias();
			return WebRetUtils.mapRet(WebRetCodes.OK);
		} catch (Exception e) {
			return WebRetUtils.mapRet(WebRetCodes.INNER_ERROR);
		}
	}
	
	/**
	 * 所有通話Socket連接
	 * @param request
	 * @return
	 * @throws Exception
	 */
	@RequestMapping(value = "connectors", method = { RequestMethod.GET, RequestMethod.POST })
	public @ResponseBody Map<String, ? extends Object> connectors(HttpServletRequest request)
			throws Exception {
		try {
			return WebRetUtils.mapRet(WebRetCodes.OK,AudioControl.getInstance().getUserAudioMedias());
		} catch (Exception e) {
			return WebRetUtils.mapRet(WebRetCodes.INNER_ERROR);
		}
	}

}

3.1 瀏覽器請求

  • 發起採集:http://localhost/start?host=192.168.0.80&port=9898&company=&lineCode=&binddevice=123&ignoredevices=
  • 停止採集:http://localhost/stop

五、總結

本次因項目需要做了音頻採集播放相關的測試項目,同時也對相關的業務做了封裝處理,便於擴展等,僅供學習參考!項目源碼
本項目僅爲客戶端程序,爲了項目完整運行還需要有個轉發的Server,如需可私聊

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章