提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
使用
提示:这里可以添加本文要记录的大概内容:
当前我们是进行了手机点击翻译按钮进行连接WebSocket
开始录音进行转换发送
传输base64 、16000Hz、16bit、单声道、PCM 格式
后端接收发送阿里实时翻译将最后内容返回前端
前端不点击按钮即可断开WebSocket连接
提示:以下是本篇文章正文内容,下面案例可供参考
一、配置
1.配置项
- 首先我们需要配置启动类的@ServletComponentScan注解
- 然后还需要配置 WebSocketConfig 的Bean
java
package cn.chinaunicom.sdsi.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration("TranslationWebSocket")
public class WebSocketConfig {
/**
* 这个Bean会自动注册所有使用 @ServerEndpoint 注解的WebSocket端点
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2.pom文件
- 这里是关于阿里的大部分依赖
java
<!-- Alibaba FastJSON:高性能JSON解析和生成库,用于JSON数据的序列化与反序列化 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<!-- 阿里云内容安全(Green)2022版SDK:用于内容安全审核、违规内容检测等功能 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>green20220302</artifactId>
<version>3.2.1</version>
</dependency>
<!-- 阿里云媒体处理服务(MTS)SDK:用于音视频转码、处理、截图等媒体处理操作 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibabacloud-mts20140618</artifactId>
<version>4.0.2</version>
</dependency>
<!-- Apache HttpCore5 核心模块:HTTP协议基础实现,包含reactor包和ProtocolUpgradeHandler类 -->
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>5.2.5</version>
</dependency>
<!-- Apache HttpCore5 HTTP/2 支持模块:提供HTTP/2协议的实现和支持 -->
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5-h2</artifactId>
<version>5.2.5</version>
</dependency>
<!-- Apache HttpClient5:高性能的HTTP客户端工具,用于发送HTTP请求和处理响应 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<!-- 阿里云智能语音交互(NLS)TTS SDK:语音合成(文字转语音)功能开发包 -->
<dependency>
<groupId>com.alibaba.nls</groupId>
<artifactId>nls-sdk-tts</artifactId>
<version>2.2.14</version>
</dependency>
<!-- 阿里云智能语音交互(NLS)通用SDK:语音交互基础公共组件,提供通用的工具和接口 -->
<dependency>
<groupId>com.alibaba.nls</groupId>
<artifactId>nls-sdk-common</artifactId>
<version>2.2.14</version>
</dependency>
<!-- 阿里云Java SDK核心包:阿里云各产品SDK的基础依赖,提供身份认证、请求处理等核心功能 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.7.1</version>
</dependency>
<!-- 阿里云智能语音交互(NLS)识别SDK:语音识别(语音转文字)功能开发包 -->
<dependency>
<groupId>com.alibaba.nls</groupId>
<artifactId>nls-sdk-recognizer</artifactId>
<version>2.2.1</version>
</dependency>
二、使用步骤
1.代码部分利用Ai进行注释
java
package cn.chinaunicom.sdsi.picture.utils;
import com.alibaba.dashscope.audio.asr.translation.TranslationRecognizerParam;
import com.alibaba.dashscope.audio.asr.translation.TranslationRecognizerRealtime;
import com.alibaba.dashscope.audio.asr.translation.results.TranslationRecognizerResult;
import com.alibaba.dashscope.common.ResultCallback;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* 实时翻译WebSocket服务端组件
* <p>
* 核心定位:基于阿里云DashScope实时语音翻译API,提供WebSocket协议的实时音频翻译服务
* 适用场景:浏览器/客户端通过WebSocket推送音频流,服务端实时返回语音识别+翻译结果
* 技术栈:JSR-356 WebSocket + 阿里云DashScope SDK + 并发容器 + 定时任务
* </p>
* 核心功能:
* 1. 支持二进制PCM音频传输(推荐,低延迟/高性能)
* 2. 兼容Base64编码音频字符串(适配前端不同传输方式,含双引号包裹场景处理)
* 3. 支持JSON格式配置消息(动态切换源/目标语言)
* 4. 心跳检测机制(保障连接稳定性,超时自动清理)
* 5. 空闲连接清理(释放资源,防止内存泄漏)
* 6. 完整的异常防护(空消息/格式错误/网络异常等友好提示)
* 7. 音频缓存补发(翻译器初始化期间缓存音频,就绪后自动补发)
*/
@Slf4j // Lombok注解,自动生成日志对象
@Component // Spring组件注解,交由Spring容器管理
@ServerEndpoint(value = "/ws/translate") // WebSocket服务端点,客户端连接路径
public class TranslationWebSocket {
// ==================== 静态配置项(可抽取到配置文件) ====================
/** 阿里云DashScope API密钥(需替换为实际有效密钥) */
private static String apiKey = "sk-eb19********************3faf65";
/** 阿里云实时翻译模型(gummy-realtime-v1为通用实时语音翻译模型) */
private static String model = "gummy-realtime-v1";
/** 音频采样率(固定16000Hz,阿里云模型要求) */
private static Integer sampleRate = 16000;
/** 音频格式(固定pcm,阿里云模型要求) */
private static String audioFormat = "pcm";
/** 最大空闲时间(秒):无音频传输超过该时间自动清理连接 */
private static Integer maxIdleTime = 60;
/** 心跳超时时间(秒):未收到心跳包超过该时间判定为连接异常 */
private static Integer heartbeatTimeout = 30;
/** 最大音频缓存包数:限制缓存队列大小,避免内存溢出(约2秒音频数据) */
private static final int MAX_CACHE_SIZE = 100;
// ==================== 全局常量/容器 ====================
/** JSON序列化/反序列化工具(全局单例,避免重复创建) */
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/** 翻译上下文缓存:key=taskId,value=翻译会话上下文(并发安全) */
private static final Map<String, TranslationContext> CONTEXT_MAP = new ConcurrentHashMap<>();
/** Session与taskId映射:key=WebSocket会话,value=任务ID(用于快速查找上下文) */
private static final Map<Session, String> SESSION_TASK_ID_MAP = new ConcurrentHashMap<>();
/** 心跳检测线程池:固定4个线程,处理心跳检查任务 */
private static final ScheduledExecutorService HEARTBEAT_EXECUTOR = Executors.newScheduledThreadPool(4);
/** 空闲连接清理线程池:单线程,避免并发清理冲突 */
private static final ScheduledExecutorService CLEANUP_EXECUTOR = Executors.newScheduledThreadPool(1);
/** Base64解码器(全局单例,处理Base64编码音频) */
private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder();
// ==================== 内部上下文类 ====================
/**
* 翻译会话上下文(每个WebSocket连接对应一个上下文)
* <p>
* 职责:封装单个连接的所有状态信息,包括翻译器实例、音频缓存、时间戳、语言配置等
* 设计:使用Atomic原子类保证并发安全,避免多线程竞争问题
* </p>
*/
private static class TranslationContext {
/** 唯一任务ID(UUID生成,标识单次翻译会话) */
private String taskId;
/** WebSocket会话对象 */
private Session session;
/** 阿里云实时翻译器实例 */
private TranslationRecognizerRealtime translator;
/** 翻译器运行状态标记(true=运行中,false=未就绪) */
private AtomicBoolean isTranslating = new AtomicBoolean(false);
/** 累计接收音频字节数(统计用) */
private AtomicLong audioBytesReceived = new AtomicLong(0);
/** 累计接收音频包数(统计用) */
private AtomicLong packetsReceived = new AtomicLong(0);
/** 最后一次心跳时间戳(毫秒) */
private AtomicLong lastHeartbeatTime = new AtomicLong(System.currentTimeMillis());
/** 最后一次接收音频时间戳(毫秒) */
private AtomicLong lastAudioTime = new AtomicLong(System.currentTimeMillis());
/** 最后一次接收任意消息时间戳(毫秒) */
private AtomicLong lastMessageTime = new AtomicLong(System.currentTimeMillis());
/** 最后一次发送音频到阿里云时间戳(毫秒) */
private AtomicLong lastSendTime = new AtomicLong(System.currentTimeMillis());
/** 心跳失败次数(累计3次超时关闭连接) */
private AtomicInteger heartbeatFailCount = new AtomicInteger(0);
/** 源语言(默认auto=自动识别) */
private String sourceLanguage = "auto";
/** 目标语言(默认en=英语,支持zh=中文、ja=日语等) */
private String targetLanguage = "en";
/** 音频缓存队列(翻译器未就绪时缓存音频数据) */
private Queue<ByteBuffer> audioCache = new ConcurrentLinkedQueue<>();
/**
* 构造方法:初始化上下文基本信息
* @param taskId 任务ID
* @param session WebSocket会话
*/
public TranslationContext(String taskId, Session session) {
this.taskId = taskId;
this.session = session;
}
// 心跳重置:收到心跳包时更新时间戳并清零失败次数
public void updateHeartbeat() {
this.lastHeartbeatTime.set(System.currentTimeMillis());
this.heartbeatFailCount.set(0);
}
// 更新音频接收时间:收到音频时调用
public void updateAudioTime() {
this.lastAudioTime.set(System.currentTimeMillis());
this.lastMessageTime.set(System.currentTimeMillis());
}
// 更新消息接收时间:收到任意文本消息时调用
public void updateMessageTime() {
this.lastMessageTime.set(System.currentTimeMillis());
}
// 判断心跳是否超时:当前时间 - 最后心跳时间 > 心跳超时阈值
public boolean isHeartbeatTimeout() {
return System.currentTimeMillis() - lastHeartbeatTime.get() > heartbeatTimeout * 1000;
}
// 判断连接是否空闲:无音频传输超过最大空闲时间
public boolean isIdle() {
return System.currentTimeMillis() - lastAudioTime.get() > maxIdleTime * 1000;
}
// 心跳失败次数+1:心跳检查超时调用
public void incrementHeartbeatFail() {
heartbeatFailCount.incrementAndGet();
}
// Getters & Setters(仅暴露必要字段,保证封装性)
public int getHeartbeatFailCount() { return heartbeatFailCount.get(); }
public AtomicLong getLastSendTime() { return lastSendTime; }
public Queue<ByteBuffer> getAudioCache() { return audioCache; }
public String getTaskId() { return taskId; }
public Session getSession() { return session; }
public AtomicBoolean getIsTranslating() { return isTranslating; }
public TranslationRecognizerRealtime getTranslator() { return translator; }
public void setTranslator(TranslationRecognizerRealtime translator) { this.translator = translator; }
public long getAudioBytesReceived() { return audioBytesReceived.get(); }
public long getPacketsReceived() { return packetsReceived.get(); }
public void incrementPackets() { packetsReceived.incrementAndGet(); }
public void addBytes(long bytes) { audioBytesReceived.addAndGet(bytes); }
public String getSourceLanguage() { return sourceLanguage; }
public void setSourceLanguage(String sourceLanguage) { this.sourceLanguage = sourceLanguage; }
public String getTargetLanguage() { return targetLanguage; }
public void setTargetLanguage(String targetLanguage) { this.targetLanguage = targetLanguage; }
}
// ==================== 静态初始化(程序启动时执行) ====================
static {
// 心跳监控任务:延迟5秒启动,每5秒执行一次
HEARTBEAT_EXECUTOR.scheduleAtFixedRate(() -> {
// 遍历所有翻译上下文,逐个检查心跳状态
for (Map.Entry<String, TranslationContext> entry : CONTEXT_MAP.entrySet()) {
TranslationContext context = entry.getValue();
try {
checkHeartbeat(context);
} catch (Exception e) {
log.error("心跳检查异常 - taskId: {}", context.getTaskId(), e);
}
}
}, 5, 5, TimeUnit.SECONDS);
// 空闲连接清理任务:延迟30秒启动,每30秒执行一次
CLEANUP_EXECUTOR.scheduleAtFixedRate(() -> {
log.info("开始清理空闲连接,当前连接数: {}", CONTEXT_MAP.size());
int cleanedCount = 0;
// 遍历所有翻译上下文,清理空闲连接
for (Map.Entry<String, TranslationContext> entry : CONTEXT_MAP.entrySet()) {
TranslationContext context = entry.getValue();
if (context.isIdle()) {
log.info("清理空闲连接 - taskId: {}, 最后音频时间: {}",
context.getTaskId(), context.lastAudioTime.get());
stopTranslation(context); // 停止翻译服务
CONTEXT_MAP.remove(entry.getKey()); // 移除上下文缓存
SESSION_TASK_ID_MAP.remove(context.getSession()); // 移除Session映射
cleanedCount++;
}
}
if (cleanedCount > 0) {
log.info("清理完成,移除了 {} 个空闲连接", cleanedCount);
}
}, 30, 30, TimeUnit.SECONDS);
}
// ==================== 心跳检查核心方法 ====================
/**
* 心跳检查:校验连接状态,超时则关闭连接
* @param context 翻译会话上下文
*/
private static void checkHeartbeat(TranslationContext context) {
Session session = context.getSession();
// 会话已关闭:清理上下文
if (session == null || !session.isOpen()) {
log.info("会话已关闭,清理上下文 - taskId: {}", context.getTaskId());
stopTranslation(context);
CONTEXT_MAP.remove(context.getTaskId());
SESSION_TASK_ID_MAP.remove(session);
return;
}
// 新连接未接收过音频:跳过心跳检查(避免误判)
if (context.getPacketsReceived() == 0) {
return;
}
// 心跳超时:累计失败次数,超过3次则关闭连接
if (context.isHeartbeatTimeout()) {
context.incrementHeartbeatFail();
log.warn("心跳超时 - taskId: {}, 失败次数: {}, 最后心跳: {}",
context.getTaskId(), context.getHeartbeatFailCount(), context.lastHeartbeatTime.get());
if (context.getHeartbeatFailCount() >= 3) {
log.error("心跳连续失败3次,关闭连接 - taskId: {}", context.getTaskId());
try {
// 异常关闭连接,附带原因
session.close(new CloseReason(CloseReason.CloseCodes.CLOSED_ABNORMALLY, "心跳超时"));
} catch (IOException e) {
log.error("关闭会话失败 - taskId: {}", context.getTaskId(), e);
}
stopTranslation(context);
CONTEXT_MAP.remove(context.getTaskId());
SESSION_TASK_ID_MAP.remove(session);
}
}
}
// ==================== WebSocket生命周期方法 ====================
/**
* WebSocket连接建立时触发
* @param session 新建立的WebSocket会话
*/
@OnOpen
public void onOpen(Session session) {
// 生成唯一任务ID(去除横线,简化标识)
String taskId = UUID.randomUUID().toString().replace("-", "");
// 创建翻译上下文
TranslationContext context = new TranslationContext(taskId, session);
// 缓存上下文和Session映射
CONTEXT_MAP.put(taskId, context);
SESSION_TASK_ID_MAP.put(session, taskId);
// 设置WebSocket会话最大空闲超时时间(与业务层空闲清理逻辑双层保障)
session.setMaxIdleTimeout(maxIdleTime * 1000);
log.info("WebSocket连接建立 - taskId: {}, sessionId: {}", taskId, session.getId());
// 异步初始化翻译服务(避免阻塞WebSocket连接建立)
new Thread(() -> {
initTranslationService(context); // 初始化阿里云翻译器
sendReadyMessage(context); // 发送就绪消息
retryCachedAudio(context); // 补发缓存的音频数据
}).start();
// 同步发送欢迎消息
sendWelcomeMessage(context);
}
/**
* 接收二进制音频数据(优先推荐的传输方式)
* @param audioData 二进制PCM音频数据
* @param session WebSocket会话
*/
@OnMessage
public void onMessage(ByteBuffer audioData, Session session) {
// 根据Session获取上下文
TranslationContext context = getContextBySession(session);
if (context == null) {
log.warn("未找到会话上下文 - sessionId: {}", session.getId());
return;
}
// 统一处理音频数据
processAudioData(context, audioData);
}
/**
* 接收文本消息(处理心跳/配置/Base64音频/结束标记)
* @param message 文本消息内容
* @param session WebSocket会话
*/
@OnMessage
public void onMessage(String message, Session session) {
TranslationContext context = getContextBySession(session);
if (context == null) {
log.warn("未找到会话上下文 - sessionId: {}", session.getId());
return;
}
// 更新消息接收时间
context.updateMessageTime();
try {
// 空消息防护:直接返回错误提示
if (message == null || message.trim().isEmpty()) {
log.warn("收到空文本消息 - taskId: {}", context.getTaskId());
sendErrorMessage(context, "空消息不被处理");
return;
}
// 清理首尾空格
String cleanMsg = message.trim();
// 核心修复:去除首尾双引号(解决前端JSON.stringify导致的Base64字符串被包裹问题)
if (cleanMsg.startsWith("\"") && cleanMsg.endsWith("\"")) {
cleanMsg = cleanMsg.substring(1, cleanMsg.length() - 1);
log.debug("移除首尾双引号 - taskId: {}, 处理后长度: {}", context.getTaskId(), cleanMsg.length());
}
// 1. 处理心跳消息(ping -> 重置心跳)
if ("ping".equals(cleanMsg)) {
context.updateHeartbeat();
sendTipMessage(context, "心跳已确认");
return;
}
// 2. 处理音频结束标记(audio_end -> 停止翻译并重置)
if ("audio_end".equals(cleanMsg)) {
log.info("收到音频结束标记,停止翻译服务 - taskId: {}", context.getTaskId());
stopTranslation(context);
// 异步重置翻译器,准备下一次翻译
new Thread(() -> initTranslationService(context)).start();
sendTipMessage(context, "音频流已结束,翻译服务已重置");
return;
}
// 3. 尝试识别并处理Base64编码音频
if (isBase64Audio(cleanMsg)) {
log.info("收到Base64编码音频,解码处理 - taskId: {}", context.getTaskId());
try {
// Base64解码为字节数组
byte[] audioBytes = BASE64_DECODER.decode(cleanMsg);
// 封装为ByteBuffer,复用二进制音频处理逻辑
ByteBuffer audioData = ByteBuffer.wrap(audioBytes);
processAudioData(context, audioData);
return;
} catch (IllegalArgumentException e) {
log.error("Base64音频解码失败 - taskId: {}", context.getTaskId(), e);
sendErrorMessage(context, "Base64音频解码失败:" + e.getMessage());
}
}
// 4. 尝试解析JSON配置消息(语言切换等)
try {
Map<String, Object> config = OBJECT_MAPPER.readValue(cleanMsg, HashMap.class);
if (config.isEmpty()) {
log.warn("收到空配置消息 - taskId: {}", context.getTaskId());
sendErrorMessage(context, "配置消息为空!正确示例:{\"sourceLanguage\":\"zh\",\"targetLanguage\":\"en\"}");
return;
}
// 处理配置更新
handleConfigMessage(context, config);
} catch (Exception e) {
// JSON解析失败:提示支持的消息类型
log.warn("非JSON配置消息,无法解析 - taskId: {}, 消息开头: {}...",
context.getTaskId(), cleanMsg.substring(0, Math.min(50, cleanMsg.length())));
sendErrorMessage(context, "消息格式错误!支持:心跳(ping)、结束(audio_end)、JSON配置、Base64音频");
}
} catch (Exception e) {
log.error("解析文本消息失败 - taskId: {}", context.getTaskId(), e);
sendErrorMessage(context, "消息处理失败:" + e.getMessage());
}
}
/**
* WebSocket连接关闭时触发
* @param session 关闭的WebSocket会话
*/
@OnClose
public void onClose(Session session) {
TranslationContext context = getContextBySession(session);
if (context != null) {
log.info("WebSocket连接正常关闭 - taskId: {}, sessionId: {}",
context.getTaskId(), session.getId());
stopTranslation(context); // 停止翻译服务
CONTEXT_MAP.remove(context.getTaskId()); // 清理上下文
SESSION_TASK_ID_MAP.remove(session); // 清理Session映射
} else {
log.info("WebSocket连接关闭 - sessionId: {}", session.getId());
SESSION_TASK_ID_MAP.remove(session);
}
}
/**
* WebSocket连接异常时触发
* @param session 异常的WebSocket会话
* @param error 异常信息
*/
@OnError
public void onError(Session session, Throwable error) {
TranslationContext context = getContextBySession(session);
if (context != null) {
log.error("WebSocket异常 - taskId: {}, 错误: {}", context.getTaskId(), error.getMessage());
sendErrorMessage(context, "连接异常:" + error.getMessage());
stopTranslation(context); // 停止翻译服务
CONTEXT_MAP.remove(context.getTaskId()); // 清理上下文
SESSION_TASK_ID_MAP.remove(session); // 清理Session映射
} else {
log.error("WebSocket异常 - sessionId: {}, 错误: {}", session.getId(), error.getMessage());
SESSION_TASK_ID_MAP.remove(session);
}
}
// ==================== 核心业务处理方法 ====================
/**
* 判断字符串是否为Base64编码的音频数据
* @param message 待校验的文本消息
* @return true=是Base64音频,false=否
*/
private boolean isBase64Audio(String message) {
if (message == null || message.isEmpty()) {
return false;
}
// Base64特征校验:
// 1. 长度是4的倍数(允许末尾1-2个等号)
// 2. 长度不小于100(过滤短字符串,避免误判)
// 3. 仅包含Base64合法字符(A-Za-z0-9+/=)
if (message.length() % 4 > 2 || message.length() < 100) {
return false;
}
return message.matches("^[A-Za-z0-9+/=]+$");
}
/**
* 统一处理音频数据(二进制/Base64解码后)
* @param context 翻译会话上下文
* @param audioData 音频数据(ByteBuffer格式)
*/
private void processAudioData(TranslationContext context, ByteBuffer audioData) {
// 更新音频接收时间戳
context.updateAudioTime();
// 校验音频数据是否为空
int audioSize = audioData.remaining();
if (audioSize == 0) {
log.warn("收到空音频数据 - taskId: {}", context.getTaskId());
return;
}
// 校验音频包大小(16000Hz/16bit/单声道:20ms音频约640字节)
// 异常大小提示:避免前端传输错误格式的音频
int expectedSize = 16000 * 2 * 20 / 1000;
if (audioSize > expectedSize * 2 || audioSize < 10) {
log.warn("异常音频包大小 - taskId: {}, 大小: {} 字节(预期约640)",
context.getTaskId(), audioSize);
}
// 检查音频发送间隔(异常间隔提示:排查前端发送逻辑)
long currentTime = System.currentTimeMillis();
long interval = currentTime - context.getLastSendTime().get();
if (interval > 50) {
log.warn("音频发送间隔异常 - taskId: {}, 间隔: {}ms", context.getTaskId(), interval);
}
context.getLastSendTime().set(currentTime);
// 翻译器未就绪:缓存音频数据
if (!context.getIsTranslating().get() || context.getTranslator() == null) {
log.warn("翻译器未就绪,缓存音频数据 - taskId: {}, 当前缓存数: {}",
context.getTaskId(), context.getAudioCache().size());
// 缓存队列满:移除最早的音频包(避免内存溢出)
if (context.getAudioCache().size() >= MAX_CACHE_SIZE) {
log.warn("缓存队列已满,移除最早的音频包 - taskId: {}", context.getTaskId());
context.getAudioCache().poll();
}
// 添加到缓存队列
context.getAudioCache().add(audioData);
sendTipMessage(context, "翻译服务初始化中,已缓存音频数据(当前" + context.getAudioCache().size() + "包)");
return;
}
try {
// 更新统计数据
context.addBytes(audioSize);
context.incrementPackets();
// 发送音频数据到阿里云翻译器
context.getTranslator().sendAudioFrame(audioData);
// 每200包打印一次统计信息(便于监控传输状态)
long packets = context.getPacketsReceived();
if (packets % 200 == 0) {
log.info("音频传输统计 - taskId: {}, 总包数: {}, 总字节数: {} KB",
context.getTaskId(), packets, context.getAudioBytesReceived() / 1024);
}
} catch (Exception e) {
log.error("发送音频数据到阿里云失败 - taskId: {}", context.getTaskId(), e);
sendErrorMessage(context, "音频发送失败:" + e.getMessage());
}
}
/**
* 初始化阿里云实时翻译服务
* @param context 翻译会话上下文
*/
private void initTranslationService(TranslationContext context) {
// 翻译器已运行:直接返回
if (context.getIsTranslating().get()) {
sendTipMessage(context, "⚠️ 翻译服务已在运行中");
return;
}
try {
log.info("初始化翻译服务 - taskId: {}", context.getTaskId());
// 构建阿里云翻译参数
TranslationRecognizerParam param = TranslationRecognizerParam.builder()
.apiKey(apiKey) // API密钥
.model(model) // 翻译模型
.format(audioFormat) // 音频格式
.sampleRate(sampleRate) // 采样率
.sourceLanguage(context.getSourceLanguage()) // 源语言
.translationEnabled(true) // 启用翻译
.translationLanguages(new String[]{context.getTargetLanguage()}) // 目标语言
.build();
// 翻译结果回调处理
ResultCallback<TranslationRecognizerResult> callback = new ResultCallback<TranslationRecognizerResult>() {
/**
* 收到翻译结果时触发(仅处理句子结束的最终结果)
* @param result 翻译结果对象
*/
@Override
public void onEvent(TranslationRecognizerResult result) {
// 非句子结束的中间结果:忽略(减少前端数据量)
if (!result.isSentenceEnd()) {
return;
}
try {
// 构造翻译结果消息
Map<String, Object> resultMsg = new HashMap<>();
resultMsg.put("type", "translation_result"); // 消息类型
resultMsg.put("taskId", context.getTaskId()); // 任务ID
// 识别文本(语音转文字结果)
resultMsg.put("text", result.getTranscriptionResult() != null
? result.getTranscriptionResult().getText() : "");
// 翻译文本(目标语言结果)
resultMsg.put("translate", result.getTranslationResult() != null
? result.getTranslationResult().getTranslation(context.getTargetLanguage()).getText() : "");
resultMsg.put("isFinal", true); // 标记为最终结果
resultMsg.put("timestamp", System.currentTimeMillis()); // 时间戳
// 发送JSON格式结果到前端
sendJsonMessage(context, resultMsg);
log.info("[{}][最终结果] 识别:{} | 翻译:{}",
context.getTaskId(), resultMsg.get("text"), resultMsg.get("translate"));
} catch (Exception e) {
log.error("推送翻译结果失败 - taskId: {}", context.getTaskId(), e);
}
}
/**
* 翻译任务完成时触发
*/
@Override
public void onComplete() {
log.info("翻译任务完成 - taskId: {}", context.getTaskId());
sendTipMessage(context, "✅ 翻译任务已完成");
}
/**
* 翻译异常时触发
* @param e 异常信息
*/
@Override
public void onError(Exception e) {
log.error("翻译异常 - taskId: {}, 错误: {}", context.getTaskId(), e.getMessage());
sendErrorMessage(context, "❌ 翻译出错:" + e.getMessage());
}
};
// 初始化阿里云实时翻译器
TranslationRecognizerRealtime translator = new TranslationRecognizerRealtime();
// 启动翻译器(异步执行)
translator.call(param, callback);
// 更新上下文状态
context.setTranslator(translator);
context.getIsTranslating().set(true);
log.info("翻译服务初始化成功 - taskId: {}", context.getTaskId());
} catch (Exception e) {
log.error("初始化翻译服务失败 - taskId: {}, 错误: {}", context.getTaskId(), e);
sendErrorMessage(context, "❌ 初始化翻译服务失败:" + e.getMessage());
context.getIsTranslating().set(false);
}
}
/**
* 停止翻译服务(释放资源)
* @param context 翻译会话上下文
*/
private static void stopTranslation(TranslationContext context) {
// 翻译器未运行:直接返回
if (!context.getIsTranslating().get()) {
return;
}
log.info("停止翻译服务 - taskId: {}", context.getTaskId());
// 更新运行状态
context.getIsTranslating().set(false);
// 关闭翻译器并清理缓存
TranslationRecognizerRealtime translator = context.getTranslator();
if (translator != null) {
try {
translator.stop(); // 停止翻译器
context.getAudioCache().clear(); // 清空音频缓存
} catch (Exception e) {
log.error("关闭翻译器失败 - taskId: {}", context.getTaskId(), e);
}
context.setTranslator(null); // 置空翻译器引用
}
}
/**
* 翻译器就绪后,补发缓存的音频数据
* @param context 翻译会话上下文
*/
private void retryCachedAudio(TranslationContext context) {
// 翻译器未就绪:跳过补发
if (context.getTranslator() == null || !context.getIsTranslating().get()) {
log.warn("翻译器未就绪,跳过缓存音频补发 - taskId: {}", context.getTaskId());
return;
}
Queue<ByteBuffer> cache = context.getAudioCache();
// 缓存为空:直接返回
if (cache.isEmpty()) {
return;
}
log.info("开始补发缓存音频 - taskId: {}, 缓存数量: {}", context.getTaskId(), cache.size());
int successCount = 0;
int totalCount = cache.size();
// 遍历缓存队列,逐个补发音频包
while (!cache.isEmpty()) {
ByteBuffer audioData = cache.poll();
try {
context.getTranslator().sendAudioFrame(audioData);
successCount++;
} catch (Exception e) {
log.error("补发单包缓存音频失败 - taskId: {}", context.getTaskId(), e);
}
}
log.info("缓存音频补发完成 - taskId: {}, 成功: {} / {}", context.getTaskId(), successCount, totalCount);
sendTipMessage(context, "缓存音频补发完成:成功" + successCount + "/" + totalCount + "包");
}
/**
* 处理配置消息(动态切换源/目标语言)
* @param context 翻译会话上下文
* @param config 配置参数(sourceLanguage/targetLanguage)
*/
private void handleConfigMessage(TranslationContext context, Map<String, Object> config) {
log.info("收到配置消息 - taskId: {}, config: {}", context.getTaskId(), config);
// 更新目标语言
if (config.containsKey("targetLanguage")) {
String newTarget = (String) config.get("targetLanguage");
context.setTargetLanguage(newTarget);
sendTipMessage(context, "目标语言已切换为: " + newTarget);
}
// 更新源语言
if (config.containsKey("sourceLanguage")) {
String newSource = (String) config.get("sourceLanguage");
context.setSourceLanguage(newSource);
sendTipMessage(context, "源语言已切换为: " + newSource);
}
// 构造配置确认消息
Map<String, Object> ackMsg = new HashMap<>();
ackMsg.put("type", "config_ack"); // 消息类型:配置确认
ackMsg.put("taskId", context.getTaskId()); // 任务ID
// 当前配置信息
Map<String, Object> currentConfig = new HashMap<>();
currentConfig.put("sourceLanguage", context.getSourceLanguage());
currentConfig.put("targetLanguage", context.getTargetLanguage());
ackMsg.put("currentConfig", currentConfig);
// 发送确认消息到前端
sendJsonMessage(context, ackMsg);
}
// ==================== 消息发送工具方法 ====================
/**
* 发送欢迎消息(连接建立时)
* @param context 翻译会话上下文
*/
private void sendWelcomeMessage(TranslationContext context) {
Map<String, Object> msg = new HashMap<>();
msg.put("type", "welcome");
msg.put("taskId", context.getTaskId());
msg.put("message", "翻译服务已连接");
msg.put("timestamp", System.currentTimeMillis());
sendJsonMessage(context, msg);
}
/**
* 发送就绪消息(翻译器初始化完成时)
* @param context 翻译会话上下文
*/
private void sendReadyMessage(TranslationContext context) {
Map<String, Object> msg = new HashMap<>();
msg.put("type", "ready");
msg.put("taskId", context.getTaskId());
msg.put("message", "翻译服务已就绪,可发送音频数据");
msg.put("timestamp", System.currentTimeMillis());
sendJsonMessage(context, msg);
}
/**
* 发送提示消息(非错误类通知)
* @param context 翻译会话上下文
* @param message 提示内容
*/
private void sendTipMessage(TranslationContext context, String message) {
Map<String, Object> msg = new HashMap<>();
msg.put("type", "tip");
msg.put("taskId", context.getTaskId());
msg.put("message", message);
msg.put("timestamp", System.currentTimeMillis());
sendJsonMessage(context, msg);
}
/**
* 发送错误消息(异常提示)
* @param context 翻译会话上下文
* @param message 错误内容
*/
private void sendErrorMessage(TranslationContext context, String message) {
Map<String, Object> msg = new HashMap<>();
msg.put("type", "error");
msg.put("taskId", context.getTaskId());
msg.put("message", message);
msg.put("timestamp", System.currentTimeMillis());
sendJsonMessage(context, msg);
}
/**
* 发送JSON格式消息到前端
* @param context 翻译会话上下文
* @param data 消息内容
*/
private void sendJsonMessage(TranslationContext context, Map<String, Object> data) {
Session session = context.getSession();
// 会话已关闭:跳过发送
if (session == null || !session.isOpen()) {
log.debug("会话已关闭,跳过消息发送 - taskId: {}", context.getTaskId());
return;
}
try {
// 序列化为JSON字符串
String json = OBJECT_MAPPER.writeValueAsString(data);
// 同步发送文本消息
session.getBasicRemote().sendText(json);
} catch (JsonProcessingException e) {
log.error("JSON序列化失败 - taskId: {}", context.getTaskId(), e);
} catch (IOException e) {
log.error("消息发送失败 - taskId: {}", context.getTaskId(), e);
}
}
// ==================== 辅助工具方法 ====================
/**
* 根据WebSocket会话获取翻译上下文
* @param session WebSocket会话
* @return 翻译上下文(null=未找到)
*/
private TranslationContext getContextBySession(Session session) {
String taskId = SESSION_TASK_ID_MAP.get(session);
if (taskId == null) {
log.warn("未找到Session对应的TaskId - sessionId: {}", session.getId());
return null;
}
return CONTEXT_MAP.get(taskId);
}
}
2、文档级总结
- 组件核心定位
该组件是基于 WebSocket 协议和阿里云 DashScope 实时语音翻译 API 实现的实时音频翻译服务端,主要用于接收前端推送的音频流(二进制 PCM/Base64 编码),实时返回语音识别 + 翻译结果,适用于需要低延迟实时语音翻译的场景(如跨境会议、实时客服等)。
- 核心设计亮点
| 设计点 | 实现方式 | 解决的问题 |
|---|---|---|
| 多格式音频支持 | 二进制 PCM 优先 + Base64 兼容处理 | 适配前端不同音频传输方式,兼容 JSON 序列化导致的 Base64 字符串包裹问题 |
| 连接稳定性保障 | 心跳检测 + 空闲连接清理 | 防止无效连接占用资源,心跳超时自动关闭异常连接 |
| 并发安全设计 | ConcurrentHashMap/Atomic 原子类 / ConcurrentLinkedQueue | 解决多线程(WebSocket 会话 / 定时任务)并发访问数据的线程安全问题 |
| 异常防护机制 | 空消息校验 / 格式校验 / 异常捕获 / 友好提示 | 避免单个异常导致服务崩溃,提升用户体验 |
| 音频缓存补发 | 翻译器初始化期间缓存音频,就绪后自动补发 | 解决翻译器启动延迟导致的音频丢失问题 |
-
核心交互流程

-
关键配置与扩展建议
- 可配置化优化:当前静态配置(apiKey、超时时间等)建议抽取到 Spring 配置文件(application.yml),通过@Value注入,提升可维护性;
- API 密钥安全:生产环境需将 apiKey 存储在配置中心 / 环境变量,避免硬编码;
- 监控扩展:可增加连接数、音频传输量、翻译成功率等监控指标,便于运维排查问题;
- 容灾处理:增加阿里云 API 调用失败的重试机制,或降级为本地提示,提升服务可用性;
- 语言扩展:支持更多目标语言(阿里云支持的语言列表),可维护语言映射表。
三、前端
目前是
录音 + 转 Base64
前端操作(核心)
① 用浏览器MediaRecorder录制音频(配置:16000Hz、16bit、单声道、PCM 格式)
② 每 20ms 左右截取一段音频二进制数据
③ 将二进制转成 Base64 字符串
前端注意事项
① 音频格式必须和后端一致(16000Hz/PCM)
② 分片大小控制在 640 字节左右(20ms)
③ 避免连续发送间隔超过 50ms
- 等待更新。。。。。