feat(audio): 实现实时语音会话功能支持设备分离和音频缓冲优化
- 配置Tomcat支持20MB大JSON请求处理PPE报警中的Base64图片 - 扩展AudioService支持独立的扬声器和麦克风设备ID参数处理 - 实现浏览器音频推送器,提供音频帧缓冲和定时推送机制 - 添加音频数据protobuf定义,支持多种音频格式和编解码参数 - 重构音频会话管理,实现音频队列、统计监控和性能优化 - 增强心跳检测机制,改进机器人离线状态处理逻辑
This commit is contained in:
parent
152cae0323
commit
ecda2b0ebc
@ -0,0 +1,123 @@
|
||||
package com.cmvr.web.controller.inspection;
|
||||
|
||||
import com.cmvr.common.annotation.Anonymous;
|
||||
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
|
||||
import com.cmvr.inspection.exception.DetectionAlertBadRequestException;
|
||||
import com.cmvr.inspection.service.IInspectionDetectionAlertService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* cmvr_edge_ai 检测报警推送入口。
|
||||
*
|
||||
* 该接口由边缘服务主动调用,使用HTTP状态码表达处理结果,不使用项目通用AjaxResult包装。
|
||||
*/
|
||||
@Slf4j
|
||||
@Anonymous
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@Api(tags = "智能巡检--PPE报警接收")
|
||||
public class InspectionDetectionAlertController
|
||||
{
|
||||
/** 包含Base64图片的JSON请求最大允许20MB,防止异常请求耗尽服务端内存。 */
|
||||
private static final int MAX_REQUEST_BYTES = 20 * 1024 * 1024;
|
||||
|
||||
private final IInspectionDetectionAlertService detectionAlertService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 接收PPE违规报警。重复事件视为已处理,仍返回204,避免边缘端持续重试。
|
||||
*/
|
||||
@ApiOperation("接收边缘AI的PPE违规报警")
|
||||
@PostMapping(value = "/v1/detection-alerts", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Void> receive(
|
||||
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
|
||||
HttpServletRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
String requestBody = readRequestBody(request);
|
||||
AlertEnvelope envelope = objectMapper.readValue(requestBody, AlertEnvelope.class);
|
||||
detectionAlertService.receive(envelope, idempotencyKey);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
catch (JsonProcessingException | DetectionAlertBadRequestException ex)
|
||||
{
|
||||
log.warn("拒绝不合法的PPE报警请求,idempotencyKey={},原因={}", idempotencyKey, ex.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
catch (DuplicateKeyException ex)
|
||||
{
|
||||
// 预查与插入之间仍可能发生并发,由数据库唯一索引完成最终幂等。
|
||||
log.info("忽略并发重复的PPE报警,idempotencyKey={}", idempotencyKey);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
log.warn("读取PPE报警请求失败,idempotencyKey={},原因={}", idempotencyKey, ex.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.error("PPE报警处理暂时失败,idempotencyKey={}", idempotencyKey, ex);
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
|
||||
}
|
||||
}
|
||||
|
||||
private String readRequestBody(HttpServletRequest request) throws IOException
|
||||
{
|
||||
long contentLength = request.getContentLengthLong();
|
||||
if (contentLength > MAX_REQUEST_BYTES)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("请求体不能超过20MB");
|
||||
}
|
||||
|
||||
try (ServletInputStream inputStream = request.getInputStream();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(initialCapacity(contentLength)))
|
||||
{
|
||||
byte[] buffer = new byte[8192];
|
||||
int total = 0;
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) != -1)
|
||||
{
|
||||
total += length;
|
||||
if (total > MAX_REQUEST_BYTES)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("请求体不能超过20MB");
|
||||
}
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
if (total == 0)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("请求体不能为空");
|
||||
}
|
||||
return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private int initialCapacity(long contentLength)
|
||||
{
|
||||
if (contentLength <= 0 || contentLength > MAX_REQUEST_BYTES)
|
||||
{
|
||||
return 8192;
|
||||
}
|
||||
return (int) contentLength;
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,9 @@ server:
|
||||
tomcat:
|
||||
# tomcat的URI编码
|
||||
uri-encoding: UTF-8
|
||||
# PPE报警包含Base64图片,允许读取较大的JSON请求,同时限制异常请求的吞入大小
|
||||
max-http-form-post-size: 20MB
|
||||
max-swallow-size: 20MB
|
||||
# 连接数满后的排队数,默认为100
|
||||
accept-count: 1000
|
||||
threads:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -47,8 +47,10 @@ public class GrpcServiceManager {
|
||||
clientFactories.put(SystemServiceGrpc.SystemServiceBlockingStub.class, new GrpcClientFactory<>(SystemServiceGrpc::newBlockingStub));
|
||||
// 注册扬声器服务的 Stub
|
||||
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceBlockingStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newBlockingStub));
|
||||
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newStub));
|
||||
// 注册麦克风服务的 Stub
|
||||
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceBlockingStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newBlockingStub));
|
||||
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newStub));
|
||||
// 注册机器人头部服务的 Stub
|
||||
clientFactories.put(BioHeadServiceGrpc.BioHeadServiceBlockingStub.class, new GrpcClientFactory<>(BioHeadServiceGrpc::newBlockingStub));
|
||||
// 注册机器人灵巧手服务的 Stub
|
||||
@ -60,7 +62,6 @@ public class GrpcServiceManager {
|
||||
// 注册agv服务的stub
|
||||
clientFactories.put(AgvServiceGrpc.AgvServiceBlockingStub.class, new GrpcClientFactory<>(AgvServiceGrpc::newBlockingStub));
|
||||
// 注册机器人实时语音双向流服务的异步Stub
|
||||
clientFactories.put(RobotAudioServiceGrpc.RobotAudioServiceStub.class, new GrpcClientFactory<>(RobotAudioServiceGrpc::newStub));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -21,6 +21,12 @@ import java.nio.ByteBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
@ -40,6 +46,12 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
|
||||
/** 机器人心跳超时时间:机器人每 1 秒上报一次,超过 5 秒判定离线。 */
|
||||
private static final long ROBOT_HEARTBEAT_TIMEOUT_MS = 5000L;
|
||||
private static final int BROWSER_AUDIO_FRAME_BYTES = 960;
|
||||
private static final int BROWSER_AUDIO_FRAME_MS = 10;
|
||||
private static final int TARGET_BROWSER_AUDIO_QUEUE_FRAMES = 4;
|
||||
private static final int MAX_BROWSER_AUDIO_CATCH_UP_FRAMES = 4;
|
||||
private static final int WARN_BROWSER_AUDIO_QUEUE_FRAMES = 100;
|
||||
private static final long BROWSER_AUDIO_PUSH_INTERVAL_MS = 5L;
|
||||
|
||||
private final RobotAudioGrpcAdapter robotAudioGrpcAdapter;
|
||||
|
||||
@ -166,13 +178,25 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
private void handleJoin(WebSocketSession webSocketSession, JsonNode jsonNode) {
|
||||
String terminalId = getText(jsonNode, "terminalId");
|
||||
String deviceId = getText(jsonNode, "deviceId");
|
||||
String speakerDeviceId = getText(jsonNode, "speakerDeviceId");
|
||||
String micDeviceId = getText(jsonNode, "micDeviceId");
|
||||
String operatorId = getText(jsonNode, "operatorId");
|
||||
String sessionId = getText(jsonNode, "sessionId");
|
||||
|
||||
if (terminalId == null || deviceId == null) {
|
||||
sendError(webSocketSession, sessionId, "terminalId和deviceId不能为空");
|
||||
if (speakerDeviceId == null) {
|
||||
speakerDeviceId = deviceId;
|
||||
}
|
||||
if (micDeviceId == null) {
|
||||
micDeviceId = deviceId;
|
||||
}
|
||||
|
||||
if (terminalId == null || speakerDeviceId == null || micDeviceId == null) {
|
||||
sendError(webSocketSession, sessionId, "terminalId、speakerDeviceId、micDeviceId不能为空");
|
||||
return;
|
||||
}
|
||||
if (deviceId == null) {
|
||||
deviceId = speakerDeviceId;
|
||||
}
|
||||
if (sessionId == null) {
|
||||
sessionId = IdUtils.fastSimpleUUID();
|
||||
}
|
||||
@ -191,24 +215,28 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
session.sessionId = sessionId;
|
||||
session.terminalId = terminalId;
|
||||
session.deviceId = deviceId;
|
||||
session.speakerDeviceId = speakerDeviceId;
|
||||
session.micDeviceId = micDeviceId;
|
||||
session.operatorId = operatorId;
|
||||
session.webSocketSession = webSocketSession;
|
||||
session.state = AudioSessionState.CONNECTING;
|
||||
session.createdAt = System.currentTimeMillis();
|
||||
session.lastBrowserSignalAt = session.createdAt;
|
||||
session.lastRobotHeartbeatAt = session.createdAt;
|
||||
startBrowserAudioPusher(session);
|
||||
|
||||
sessionMap.put(sessionId, session);
|
||||
webSocketSessionMap.put(webSocketSession.getId(), sessionId);
|
||||
try {
|
||||
session.robotStream = robotAudioGrpcAdapter.openStream(terminalId, deviceId, sessionId, operatorId, this);
|
||||
session.robotStream = robotAudioGrpcAdapter.openStream(terminalId, speakerDeviceId, micDeviceId, sessionId, operatorId, this);
|
||||
} catch (Exception e) {
|
||||
sessionMap.remove(sessionId);
|
||||
webSocketSessionMap.remove(webSocketSession.getId());
|
||||
stopBrowserAudioPusher(session);
|
||||
throw e;
|
||||
}
|
||||
sendState(session, "joined");
|
||||
log.info("实时语音会话创建成功,sessionId={},terminalId={},deviceId={}", sessionId, terminalId, deviceId);
|
||||
log.info("实时语音会话创建成功,sessionId={},terminalId={},speakerDeviceId={},micDeviceId={}", sessionId, terminalId, speakerDeviceId, micDeviceId);
|
||||
} catch (Exception e) {
|
||||
log.error("创建实时语音会话失败,sessionId={}", sessionId, e);
|
||||
sendError(webSocketSession, sessionId, "创建实时语音会话失败:" + e.getMessage());
|
||||
@ -294,6 +322,7 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
try {
|
||||
session.state = AudioSessionState.CLOSED;
|
||||
removeWebSocketMapping(session);
|
||||
stopBrowserAudioPusher(session);
|
||||
if (session.robotStream != null) {
|
||||
session.robotStream.sendStop(reason);
|
||||
}
|
||||
@ -318,12 +347,13 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
continue;
|
||||
}
|
||||
if (now - session.lastRobotHeartbeatAt > ROBOT_HEARTBEAT_TIMEOUT_MS) {
|
||||
log.warn("机器人心跳超时,sessionId={},terminalId={},deviceId={}",
|
||||
session.sessionId, session.terminalId, session.deviceId);
|
||||
log.warn("机器人心跳超时,sessionId={},terminalId={},speakerDeviceId={},micDeviceId={}",
|
||||
session.sessionId, session.terminalId, session.speakerDeviceId, session.micDeviceId);
|
||||
// 先从业务会话表摘除,避免 cancel/close 触发的异步回调再次进入同一个会话并重复关闭 native 对象。
|
||||
iterator.remove();
|
||||
session.state = AudioSessionState.ROBOT_OFFLINE;
|
||||
sendState(session, "robotOffline");
|
||||
stopBrowserAudioPusher(session);
|
||||
if (session.robotStream != null) {
|
||||
session.robotStream.cancel("robot heartbeat timeout");
|
||||
}
|
||||
@ -359,7 +389,7 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
}
|
||||
session.lastRobotAudioAt = System.currentTimeMillis();
|
||||
session.lastRobotHeartbeatAt = session.lastRobotAudioAt;
|
||||
sendBinary(session.webSocketSession, pcm);
|
||||
enqueueRobotAudioForBrowser(session, pcm);
|
||||
Consumer<byte[]> consumer = session.robotAudioConsumer;
|
||||
if (consumer != null) {
|
||||
try {
|
||||
@ -412,12 +442,14 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
sendState(session, "grpcCompleted");
|
||||
}
|
||||
sessionMap.remove(sessionId);
|
||||
stopBrowserAudioPusher(session);
|
||||
removeWebSocketMapping(session);
|
||||
}
|
||||
|
||||
private void failSession(AudioSession session, String reason, boolean cancelRobotStream) {
|
||||
sessionMap.remove(session.sessionId, session);
|
||||
session.state = AudioSessionState.FAILED;
|
||||
stopBrowserAudioPusher(session);
|
||||
ObjectNode event = baseMessage("error", session.sessionId);
|
||||
event.put("message", reason == null ? "未知错误" : reason);
|
||||
event.put("state", session.state.name());
|
||||
@ -436,6 +468,8 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
ObjectNode event = baseMessage(type, session.sessionId);
|
||||
event.put("terminalId", session.terminalId);
|
||||
event.put("deviceId", session.deviceId);
|
||||
event.put("speakerDeviceId", session.speakerDeviceId);
|
||||
event.put("micDeviceId", session.micDeviceId);
|
||||
event.put("state", session.state.name());
|
||||
event.put("createdAt", session.createdAt);
|
||||
event.put("lastRobotHeartbeatAt", session.lastRobotHeartbeatAt);
|
||||
@ -462,6 +496,108 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
sendText(session, toJson(event));
|
||||
}
|
||||
|
||||
private void startBrowserAudioPusher(AudioSession session) {
|
||||
if (session == null || session.robotBrowserAudioExecutor != null) {
|
||||
return;
|
||||
}
|
||||
session.robotBrowserAudioExecutor = Executors.newSingleThreadScheduledExecutor(
|
||||
new NamedThreadFactory("robot-browser-audio-" + session.sessionId));
|
||||
session.robotBrowserAudioFuture = session.robotBrowserAudioExecutor.scheduleAtFixedRate(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
int queued = session.robotBrowserAudioQueue.size();
|
||||
int framesToSend = queued > TARGET_BROWSER_AUDIO_QUEUE_FRAMES
|
||||
? Math.min(MAX_BROWSER_AUDIO_CATCH_UP_FRAMES, queued - TARGET_BROWSER_AUDIO_QUEUE_FRAMES + 1)
|
||||
: 1;
|
||||
for (int i = 0; i < framesToSend; i++) {
|
||||
byte[] frame = session.robotBrowserAudioQueue.poll();
|
||||
if (frame == null) {
|
||||
break;
|
||||
}
|
||||
sendBinary(session.webSocketSession, frame);
|
||||
session.robotBrowserAudioSent++;
|
||||
}
|
||||
logRobotBrowserAudioStatsIfNeeded(session, "robotAudioPush");
|
||||
} catch (Exception e) {
|
||||
log.error("推送机器人音频到浏览器失败,sessionId={}", session.sessionId, e);
|
||||
}
|
||||
}
|
||||
}, 0L, BROWSER_AUDIO_PUSH_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void enqueueRobotAudioForBrowser(AudioSession session, byte[] pcm) {
|
||||
if (session == null || pcm == null || pcm.length == 0) {
|
||||
return;
|
||||
}
|
||||
synchronized (session.robotBrowserAudioLock) {
|
||||
byte[] merged = new byte[session.robotBrowserRemainder.length + pcm.length];
|
||||
System.arraycopy(session.robotBrowserRemainder, 0, merged, 0, session.robotBrowserRemainder.length);
|
||||
System.arraycopy(pcm, 0, merged, session.robotBrowserRemainder.length, pcm.length);
|
||||
|
||||
int offset = 0;
|
||||
while (merged.length - offset >= BROWSER_AUDIO_FRAME_BYTES) {
|
||||
byte[] frame = new byte[BROWSER_AUDIO_FRAME_BYTES];
|
||||
System.arraycopy(merged, offset, frame, 0, BROWSER_AUDIO_FRAME_BYTES);
|
||||
offerRobotAudioFrame(session, frame);
|
||||
offset += BROWSER_AUDIO_FRAME_BYTES;
|
||||
}
|
||||
|
||||
int remain = merged.length - offset;
|
||||
session.robotBrowserRemainder = new byte[remain];
|
||||
if (remain > 0) {
|
||||
System.arraycopy(merged, offset, session.robotBrowserRemainder, 0, remain);
|
||||
}
|
||||
}
|
||||
logRobotBrowserAudioStatsIfNeeded(session, "robotAudioEnqueue");
|
||||
}
|
||||
|
||||
private void offerRobotAudioFrame(AudioSession session, byte[] frame) {
|
||||
session.robotBrowserAudioQueue.offer(frame);
|
||||
session.robotBrowserAudioEnqueued++;
|
||||
}
|
||||
|
||||
private void stopBrowserAudioPusher(AudioSession session) {
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
ScheduledFuture<?> future = session.robotBrowserAudioFuture;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
session.robotBrowserAudioFuture = null;
|
||||
}
|
||||
ScheduledExecutorService executor = session.robotBrowserAudioExecutor;
|
||||
if (executor != null) {
|
||||
executor.shutdownNow();
|
||||
session.robotBrowserAudioExecutor = null;
|
||||
}
|
||||
synchronized (session.robotBrowserAudioLock) {
|
||||
session.robotBrowserAudioQueue.clear();
|
||||
session.robotBrowserRemainder = new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
private void logRobotBrowserAudioStatsIfNeeded(AudioSession session, String source) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - session.lastRobotBrowserAudioLogAt < 5000L) {
|
||||
return;
|
||||
}
|
||||
session.lastRobotBrowserAudioLogAt = now;
|
||||
int queued = session.robotBrowserAudioQueue.size();
|
||||
log.info("机器人到浏览器音频统计,source={},sessionId={},enqueued={},sent={},dropped={},queued={},remainderBytes={}",
|
||||
source,
|
||||
session.sessionId,
|
||||
session.robotBrowserAudioEnqueued,
|
||||
session.robotBrowserAudioSent,
|
||||
session.robotBrowserAudioDropped,
|
||||
queued,
|
||||
session.robotBrowserRemainder.length);
|
||||
if (queued > WARN_BROWSER_AUDIO_QUEUE_FRAMES) {
|
||||
log.warn("机器人到浏览器音频队列积压较高,sessionId={},queued={},estimatedDelayMs={},请检查前端播放消费或网络发送是否跟不上",
|
||||
session.sessionId, queued, queued * BROWSER_AUDIO_FRAME_MS);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendBinary(WebSocketSession session, byte[] pcm) {
|
||||
if (session == null || !session.isOpen() || pcm == null || pcm.length == 0) {
|
||||
return;
|
||||
@ -571,10 +707,21 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
private String sessionId;
|
||||
private String terminalId;
|
||||
private String deviceId;
|
||||
private String speakerDeviceId;
|
||||
private String micDeviceId;
|
||||
private String operatorId;
|
||||
private volatile WebSocketSession webSocketSession;
|
||||
private volatile RobotAudioStream robotStream;
|
||||
private volatile Consumer<byte[]> robotAudioConsumer;
|
||||
private volatile ScheduledExecutorService robotBrowserAudioExecutor;
|
||||
private volatile ScheduledFuture<?> robotBrowserAudioFuture;
|
||||
private final LinkedBlockingQueue<byte[]> robotBrowserAudioQueue = new LinkedBlockingQueue<>();
|
||||
private final Object robotBrowserAudioLock = new Object();
|
||||
private byte[] robotBrowserRemainder = new byte[0];
|
||||
private volatile long robotBrowserAudioEnqueued;
|
||||
private volatile long robotBrowserAudioSent;
|
||||
private volatile long robotBrowserAudioDropped;
|
||||
private volatile long lastRobotBrowserAudioLogAt;
|
||||
private volatile AudioSessionState state;
|
||||
private volatile long createdAt;
|
||||
private volatile long lastBrowserSignalAt;
|
||||
@ -588,4 +735,19 @@ public class AudioService implements RobotAudioStreamListener {
|
||||
private String sdpMid;
|
||||
private int sdpMLineIndex;
|
||||
}
|
||||
|
||||
private static class NamedThreadFactory implements ThreadFactory {
|
||||
private final String name;
|
||||
|
||||
private NamedThreadFactory(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = new Thread(runnable, name);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -170,6 +170,37 @@ public final class MicPhoneServiceGrpc {
|
||||
return getResumeRecordMethod;
|
||||
}
|
||||
|
||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request,
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> getStreamAudioMethod;
|
||||
|
||||
@io.grpc.stub.annotations.RpcMethod(
|
||||
fullMethodName = SERVICE_NAME + '/' + "StreamAudio",
|
||||
requestType = cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request.class,
|
||||
responseType = cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback.class,
|
||||
methodType = io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
|
||||
public static io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request,
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> getStreamAudioMethod() {
|
||||
io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request, cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> getStreamAudioMethod;
|
||||
if ((getStreamAudioMethod = MicPhoneServiceGrpc.getStreamAudioMethod) == null) {
|
||||
synchronized (MicPhoneServiceGrpc.class) {
|
||||
if ((getStreamAudioMethod = MicPhoneServiceGrpc.getStreamAudioMethod) == null) {
|
||||
MicPhoneServiceGrpc.getStreamAudioMethod = getStreamAudioMethod =
|
||||
io.grpc.MethodDescriptor.<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request, cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback>newBuilder()
|
||||
.setType(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
|
||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "StreamAudio"))
|
||||
.setSampledToLocalTracing(true)
|
||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request.getDefaultInstance()))
|
||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback.getDefaultInstance()))
|
||||
.setSchemaDescriptor(new MicPhoneServiceMethodDescriptorSupplier("StreamAudio"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return getStreamAudioMethod;
|
||||
}
|
||||
|
||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request,
|
||||
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> getSetVolumeMethod;
|
||||
|
||||
@ -281,9 +312,6 @@ public final class MicPhoneServiceGrpc {
|
||||
public static abstract class MicPhoneServiceImplBase implements io.grpc.BindableService {
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
||||
@ -319,9 +347,13 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public void streamAudio(cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> responseObserver) {
|
||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getStreamAudioMethod(), responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
||||
@ -372,6 +404,13 @@ public final class MicPhoneServiceGrpc {
|
||||
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request,
|
||||
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>(
|
||||
this, METHODID_RESUME_RECORD)))
|
||||
.addMethod(
|
||||
getStreamAudioMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncServerStreamingCall(
|
||||
new MethodHandlers<
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request,
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback>(
|
||||
this, METHODID_STREAM_AUDIO)))
|
||||
.addMethod(
|
||||
getSetVolumeMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
||||
@ -405,9 +444,6 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
||||
@ -448,9 +484,14 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public void streamAudio(cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> responseObserver) {
|
||||
io.grpc.stub.ClientCalls.asyncServerStreamingCall(
|
||||
getChannel().newCall(getStreamAudioMethod(), getCallOptions()), request, responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
||||
@ -482,9 +523,6 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||
@ -520,9 +558,14 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public java.util.Iterator<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> streamAudio(
|
||||
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request) {
|
||||
return io.grpc.stub.ClientCalls.blockingServerStreamingCall(
|
||||
getChannel(), getStreamAudioMethod(), getCallOptions(), request);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||
@ -552,9 +595,6 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> getStatus(
|
||||
cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
||||
@ -595,9 +635,6 @@ public final class MicPhoneServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> setVolume(
|
||||
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
||||
@ -619,8 +656,9 @@ public final class MicPhoneServiceGrpc {
|
||||
private static final int METHODID_STOP_RECORD = 2;
|
||||
private static final int METHODID_PAUSE_RECORD = 3;
|
||||
private static final int METHODID_RESUME_RECORD = 4;
|
||||
private static final int METHODID_SET_VOLUME = 5;
|
||||
private static final int METHODID_GET_VOLUME = 6;
|
||||
private static final int METHODID_STREAM_AUDIO = 5;
|
||||
private static final int METHODID_SET_VOLUME = 6;
|
||||
private static final int METHODID_GET_VOLUME = 7;
|
||||
|
||||
private static final class MethodHandlers<Req, Resp> implements
|
||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||
@ -659,6 +697,10 @@ public final class MicPhoneServiceGrpc {
|
||||
serviceImpl.resumeRecord((cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request) request,
|
||||
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>) responseObserver);
|
||||
break;
|
||||
case METHODID_STREAM_AUDIO:
|
||||
serviceImpl.streamAudio((cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request) request,
|
||||
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback>) responseObserver);
|
||||
break;
|
||||
case METHODID_SET_VOLUME:
|
||||
serviceImpl.setVolume((cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request) request,
|
||||
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback>) responseObserver);
|
||||
@ -733,6 +775,7 @@ public final class MicPhoneServiceGrpc {
|
||||
.addMethod(getStopRecordMethod())
|
||||
.addMethod(getPauseRecordMethod())
|
||||
.addMethod(getResumeRecordMethod())
|
||||
.addMethod(getStreamAudioMethod())
|
||||
.addMethod(getSetVolumeMethod())
|
||||
.addMethod(getGetVolumeMethod())
|
||||
.build();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,7 @@ public final class MicrophoneService {
|
||||
java.lang.String[] descriptorData = {
|
||||
"\n!cmvr/api/microphone_service.proto\022\010cmv" +
|
||||
"r.api\032!cmvr/api/microphone_command.proto" +
|
||||
"2\327\005\n\017MicPhoneService\022X\n\tGetStatus\022$.cmvr" +
|
||||
"2\273\006\n\017MicPhoneService\022X\n\tGetStatus\022$.cmvr" +
|
||||
".api.GetMicStateCommand.Request\032%.cmvr.a" +
|
||||
"pi.GetMicStateCommand.Feedback\022f\n\013StartR" +
|
||||
"ecord\022*.cmvr.api.StartMicRecordingComman" +
|
||||
@ -38,12 +38,14 @@ public final class MicrophoneService {
|
||||
"ingCommand.Feedback\022i\n\014ResumeRecord\022+.cm" +
|
||||
"vr.api.ResumeMicRecordingCommand.Request" +
|
||||
"\032,.cmvr.api.ResumeMicRecordingCommand.Fe" +
|
||||
"edback\022d\n\tSetVolume\022*.cmvr.api.SetMicPho" +
|
||||
"neVolumeCommand.Request\032+.cmvr.api.SetMi" +
|
||||
"cPhoneVolumeCommand.Feedback\022d\n\tGetVolum" +
|
||||
"e\022*.cmvr.api.GetMicPhoneVolumeCommand.Re" +
|
||||
"quest\032+.cmvr.api.GetMicPhoneVolumeComman" +
|
||||
"d.Feedbackb\006proto3"
|
||||
"edback\022b\n\013StreamAudio\022\'.cmvr.api.StreamM" +
|
||||
"icAudioCommand.Request\032(.cmvr.api.Stream" +
|
||||
"MicAudioCommand.Feedback0\001\022d\n\tSetVolume\022" +
|
||||
"*.cmvr.api.SetMicPhoneVolumeCommand.Requ" +
|
||||
"est\032+.cmvr.api.SetMicPhoneVolumeCommand." +
|
||||
"Feedback\022d\n\tGetVolume\022*.cmvr.api.GetMicP" +
|
||||
"honeVolumeCommand.Request\032+.cmvr.api.Get" +
|
||||
"MicPhoneVolumeCommand.Feedbackb\006proto3"
|
||||
};
|
||||
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
||||
.internalBuildGeneratedFileFrom(descriptorData,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,280 +0,0 @@
|
||||
package cmvr.api;
|
||||
|
||||
import static io.grpc.MethodDescriptor.generateFullMethodName;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Real-time bidirectional robot audio service.
|
||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
* </pre>
|
||||
*/
|
||||
@javax.annotation.Generated(
|
||||
value = "by gRPC proto compiler (version 1.52.0)",
|
||||
comments = "Source: cmvr/api/robot_audio.proto")
|
||||
@io.grpc.stub.annotations.GrpcGenerated
|
||||
public final class RobotAudioServiceGrpc {
|
||||
|
||||
private RobotAudioServiceGrpc() {}
|
||||
|
||||
public static final String SERVICE_NAME = "cmvr.api.RobotAudioService";
|
||||
|
||||
// Static method descriptors that strictly reflect the proto.
|
||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.RobotAudio.RobotAudioRequest,
|
||||
cmvr.api.RobotAudio.RobotAudioResponse> getAudioTalkMethod;
|
||||
|
||||
@io.grpc.stub.annotations.RpcMethod(
|
||||
fullMethodName = SERVICE_NAME + '/' + "AudioTalk",
|
||||
requestType = cmvr.api.RobotAudio.RobotAudioRequest.class,
|
||||
responseType = cmvr.api.RobotAudio.RobotAudioResponse.class,
|
||||
methodType = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)
|
||||
public static io.grpc.MethodDescriptor<cmvr.api.RobotAudio.RobotAudioRequest,
|
||||
cmvr.api.RobotAudio.RobotAudioResponse> getAudioTalkMethod() {
|
||||
io.grpc.MethodDescriptor<cmvr.api.RobotAudio.RobotAudioRequest, cmvr.api.RobotAudio.RobotAudioResponse> getAudioTalkMethod;
|
||||
if ((getAudioTalkMethod = RobotAudioServiceGrpc.getAudioTalkMethod) == null) {
|
||||
synchronized (RobotAudioServiceGrpc.class) {
|
||||
if ((getAudioTalkMethod = RobotAudioServiceGrpc.getAudioTalkMethod) == null) {
|
||||
RobotAudioServiceGrpc.getAudioTalkMethod = getAudioTalkMethod =
|
||||
io.grpc.MethodDescriptor.<cmvr.api.RobotAudio.RobotAudioRequest, cmvr.api.RobotAudio.RobotAudioResponse>newBuilder()
|
||||
.setType(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)
|
||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "AudioTalk"))
|
||||
.setSampledToLocalTracing(true)
|
||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.RobotAudio.RobotAudioRequest.getDefaultInstance()))
|
||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.RobotAudio.RobotAudioResponse.getDefaultInstance()))
|
||||
.setSchemaDescriptor(new RobotAudioServiceMethodDescriptorSupplier("AudioTalk"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return getAudioTalkMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new async stub that supports all call types for the service
|
||||
*/
|
||||
public static RobotAudioServiceStub newStub(io.grpc.Channel channel) {
|
||||
io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceStub> factory =
|
||||
new io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceStub>() {
|
||||
@java.lang.Override
|
||||
public RobotAudioServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceStub(channel, callOptions);
|
||||
}
|
||||
};
|
||||
return RobotAudioServiceStub.newStub(factory, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new blocking-style stub that supports unary and streaming output calls on the service
|
||||
*/
|
||||
public static RobotAudioServiceBlockingStub newBlockingStub(
|
||||
io.grpc.Channel channel) {
|
||||
io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceBlockingStub> factory =
|
||||
new io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceBlockingStub>() {
|
||||
@java.lang.Override
|
||||
public RobotAudioServiceBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceBlockingStub(channel, callOptions);
|
||||
}
|
||||
};
|
||||
return RobotAudioServiceBlockingStub.newStub(factory, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new ListenableFuture-style stub that supports unary calls on the service
|
||||
*/
|
||||
public static RobotAudioServiceFutureStub newFutureStub(
|
||||
io.grpc.Channel channel) {
|
||||
io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceFutureStub> factory =
|
||||
new io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceFutureStub>() {
|
||||
@java.lang.Override
|
||||
public RobotAudioServiceFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceFutureStub(channel, callOptions);
|
||||
}
|
||||
};
|
||||
return RobotAudioServiceFutureStub.newStub(factory, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Real-time bidirectional robot audio service.
|
||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
* </pre>
|
||||
*/
|
||||
public static abstract class RobotAudioServiceImplBase implements io.grpc.BindableService {
|
||||
|
||||
/**
|
||||
*/
|
||||
public io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioRequest> audioTalk(
|
||||
io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioResponse> responseObserver) {
|
||||
return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getAudioTalkMethod(), responseObserver);
|
||||
}
|
||||
|
||||
@java.lang.Override public final io.grpc.ServerServiceDefinition bindService() {
|
||||
return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor())
|
||||
.addMethod(
|
||||
getAudioTalkMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncBidiStreamingCall(
|
||||
new MethodHandlers<
|
||||
cmvr.api.RobotAudio.RobotAudioRequest,
|
||||
cmvr.api.RobotAudio.RobotAudioResponse>(
|
||||
this, METHODID_AUDIO_TALK)))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Real-time bidirectional robot audio service.
|
||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
* </pre>
|
||||
*/
|
||||
public static final class RobotAudioServiceStub extends io.grpc.stub.AbstractAsyncStub<RobotAudioServiceStub> {
|
||||
private RobotAudioServiceStub(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
super(channel, callOptions);
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
protected RobotAudioServiceStub build(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceStub(channel, callOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioRequest> audioTalk(
|
||||
io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioResponse> responseObserver) {
|
||||
return io.grpc.stub.ClientCalls.asyncBidiStreamingCall(
|
||||
getChannel().newCall(getAudioTalkMethod(), getCallOptions()), responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Real-time bidirectional robot audio service.
|
||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
* </pre>
|
||||
*/
|
||||
public static final class RobotAudioServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub<RobotAudioServiceBlockingStub> {
|
||||
private RobotAudioServiceBlockingStub(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
super(channel, callOptions);
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
protected RobotAudioServiceBlockingStub build(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceBlockingStub(channel, callOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Real-time bidirectional robot audio service.
|
||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
* </pre>
|
||||
*/
|
||||
public static final class RobotAudioServiceFutureStub extends io.grpc.stub.AbstractFutureStub<RobotAudioServiceFutureStub> {
|
||||
private RobotAudioServiceFutureStub(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
super(channel, callOptions);
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
protected RobotAudioServiceFutureStub build(
|
||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
||||
return new RobotAudioServiceFutureStub(channel, callOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int METHODID_AUDIO_TALK = 0;
|
||||
|
||||
private static final class MethodHandlers<Req, Resp> implements
|
||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.ServerStreamingMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.ClientStreamingMethod<Req, Resp>,
|
||||
io.grpc.stub.ServerCalls.BidiStreamingMethod<Req, Resp> {
|
||||
private final RobotAudioServiceImplBase serviceImpl;
|
||||
private final int methodId;
|
||||
|
||||
MethodHandlers(RobotAudioServiceImplBase serviceImpl, int methodId) {
|
||||
this.serviceImpl = serviceImpl;
|
||||
this.methodId = methodId;
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
@java.lang.SuppressWarnings("unchecked")
|
||||
public void invoke(Req request, io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||
switch (methodId) {
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
@java.lang.SuppressWarnings("unchecked")
|
||||
public io.grpc.stub.StreamObserver<Req> invoke(
|
||||
io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||
switch (methodId) {
|
||||
case METHODID_AUDIO_TALK:
|
||||
return (io.grpc.stub.StreamObserver<Req>) serviceImpl.audioTalk(
|
||||
(io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioResponse>) responseObserver);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class RobotAudioServiceBaseDescriptorSupplier
|
||||
implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier {
|
||||
RobotAudioServiceBaseDescriptorSupplier() {}
|
||||
|
||||
@java.lang.Override
|
||||
public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() {
|
||||
return cmvr.api.RobotAudio.getDescriptor();
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() {
|
||||
return getFileDescriptor().findServiceByName("RobotAudioService");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RobotAudioServiceFileDescriptorSupplier
|
||||
extends RobotAudioServiceBaseDescriptorSupplier {
|
||||
RobotAudioServiceFileDescriptorSupplier() {}
|
||||
}
|
||||
|
||||
private static final class RobotAudioServiceMethodDescriptorSupplier
|
||||
extends RobotAudioServiceBaseDescriptorSupplier
|
||||
implements io.grpc.protobuf.ProtoMethodDescriptorSupplier {
|
||||
private final String methodName;
|
||||
|
||||
RobotAudioServiceMethodDescriptorSupplier(String methodName) {
|
||||
this.methodName = methodName;
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
public com.google.protobuf.Descriptors.MethodDescriptor getMethodDescriptor() {
|
||||
return getServiceDescriptor().findMethodByName(methodName);
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile io.grpc.ServiceDescriptor serviceDescriptor;
|
||||
|
||||
public static io.grpc.ServiceDescriptor getServiceDescriptor() {
|
||||
io.grpc.ServiceDescriptor result = serviceDescriptor;
|
||||
if (result == null) {
|
||||
synchronized (RobotAudioServiceGrpc.class) {
|
||||
result = serviceDescriptor;
|
||||
if (result == null) {
|
||||
serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME)
|
||||
.setSchemaDescriptor(new RobotAudioServiceFileDescriptorSupplier())
|
||||
.addMethod(getAudioTalkMethod())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -77,6 +77,37 @@ public final class SpeakerServiceGrpc {
|
||||
return getPlayAudioMethod;
|
||||
}
|
||||
|
||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request,
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> getStreamAudioMethod;
|
||||
|
||||
@io.grpc.stub.annotations.RpcMethod(
|
||||
fullMethodName = SERVICE_NAME + '/' + "StreamAudio",
|
||||
requestType = cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request.class,
|
||||
responseType = cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback.class,
|
||||
methodType = io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING)
|
||||
public static io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request,
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> getStreamAudioMethod() {
|
||||
io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request, cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> getStreamAudioMethod;
|
||||
if ((getStreamAudioMethod = SpeakerServiceGrpc.getStreamAudioMethod) == null) {
|
||||
synchronized (SpeakerServiceGrpc.class) {
|
||||
if ((getStreamAudioMethod = SpeakerServiceGrpc.getStreamAudioMethod) == null) {
|
||||
SpeakerServiceGrpc.getStreamAudioMethod = getStreamAudioMethod =
|
||||
io.grpc.MethodDescriptor.<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request, cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback>newBuilder()
|
||||
.setType(io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING)
|
||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "StreamAudio"))
|
||||
.setSampledToLocalTracing(true)
|
||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request.getDefaultInstance()))
|
||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback.getDefaultInstance()))
|
||||
.setSchemaDescriptor(new SpeakerServiceMethodDescriptorSupplier("StreamAudio"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return getStreamAudioMethod;
|
||||
}
|
||||
|
||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StopSpeakerCommand.Request,
|
||||
cmvr.api.SpeakerCommand.StopSpeakerCommand.Feedback> getStopPlaybackMethod;
|
||||
|
||||
@ -281,9 +312,6 @@ public final class SpeakerServiceGrpc {
|
||||
public static abstract class SpeakerServiceImplBase implements io.grpc.BindableService {
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
||||
@ -297,6 +325,13 @@ public final class SpeakerServiceGrpc {
|
||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getPlayAudioMethod(), responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request> streamAudio(
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> responseObserver) {
|
||||
return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getStreamAudioMethod(), responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public void stopPlayback(cmvr.api.SpeakerCommand.StopSpeakerCommand.Request request,
|
||||
@ -319,9 +354,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
||||
@ -351,6 +383,13 @@ public final class SpeakerServiceGrpc {
|
||||
cmvr.api.SpeakerCommand.PlayAudioCommand.Request,
|
||||
cmvr.api.SpeakerCommand.PlayAudioCommand.Feedback>(
|
||||
this, METHODID_PLAY_AUDIO)))
|
||||
.addMethod(
|
||||
getStreamAudioMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncClientStreamingCall(
|
||||
new MethodHandlers<
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request,
|
||||
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback>(
|
||||
this, METHODID_STREAM_AUDIO)))
|
||||
.addMethod(
|
||||
getStopPlaybackMethod(),
|
||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
||||
@ -405,9 +444,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
||||
@ -423,6 +459,14 @@ public final class SpeakerServiceGrpc {
|
||||
getChannel().newCall(getPlayAudioMethod(), getCallOptions()), request, responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request> streamAudio(
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> responseObserver) {
|
||||
return io.grpc.stub.ClientCalls.asyncClientStreamingCall(
|
||||
getChannel().newCall(getStreamAudioMethod(), getCallOptions()), responseObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public void stopPlayback(cmvr.api.SpeakerCommand.StopSpeakerCommand.Request request,
|
||||
@ -448,9 +492,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
||||
@ -482,9 +523,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||
@ -520,9 +558,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||
@ -552,9 +587,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 基本控制
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> getStatus(
|
||||
cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
||||
@ -595,9 +627,6 @@ public final class SpeakerServiceGrpc {
|
||||
}
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 音量控制
|
||||
* </pre>
|
||||
*/
|
||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> setVolume(
|
||||
cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
||||
@ -621,6 +650,7 @@ public final class SpeakerServiceGrpc {
|
||||
private static final int METHODID_RESUME_PLAYBACK = 4;
|
||||
private static final int METHODID_SET_VOLUME = 5;
|
||||
private static final int METHODID_GET_VOLUME = 6;
|
||||
private static final int METHODID_STREAM_AUDIO = 7;
|
||||
|
||||
private static final class MethodHandlers<Req, Resp> implements
|
||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||
@ -677,6 +707,9 @@ public final class SpeakerServiceGrpc {
|
||||
public io.grpc.stub.StreamObserver<Req> invoke(
|
||||
io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||
switch (methodId) {
|
||||
case METHODID_STREAM_AUDIO:
|
||||
return (io.grpc.stub.StreamObserver<Req>) serviceImpl.streamAudio(
|
||||
(io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback>) responseObserver);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
@ -730,6 +763,7 @@ public final class SpeakerServiceGrpc {
|
||||
.setSchemaDescriptor(new SpeakerServiceFileDescriptorSupplier())
|
||||
.addMethod(getGetStatusMethod())
|
||||
.addMethod(getPlayAudioMethod())
|
||||
.addMethod(getStreamAudioMethod())
|
||||
.addMethod(getStopPlaybackMethod())
|
||||
.addMethod(getPausePlaybackMethod())
|
||||
.addMethod(getResumePlaybackMethod())
|
||||
|
||||
@ -24,25 +24,28 @@ public final class SpeakerServiceOuterClass {
|
||||
static {
|
||||
java.lang.String[] descriptorData = {
|
||||
"\n\036cmvr/api/speaker_service.proto\022\010cmvr.a" +
|
||||
"pi\032\036cmvr/api/speaker_command.proto2\260\005\n\016S" +
|
||||
"pi\032\036cmvr/api/speaker_command.proto2\234\006\n\016S" +
|
||||
"peakerService\022`\n\tGetStatus\022(.cmvr.api.Ge" +
|
||||
"tSpeakerStateCommand.Request\032).cmvr.api." +
|
||||
"GetSpeakerStateCommand.Feedback\022T\n\tPlayA" +
|
||||
"udio\022\".cmvr.api.PlayAudioCommand.Request" +
|
||||
"\032#.cmvr.api.PlayAudioCommand.Feedback\022[\n" +
|
||||
"\014StopPlayback\022$.cmvr.api.StopSpeakerComm" +
|
||||
"and.Request\032%.cmvr.api.StopSpeakerComman" +
|
||||
"d.Feedback\022^\n\rPausePlayback\022%.cmvr.api.P" +
|
||||
"auseSpeakerCommand.Request\032&.cmvr.api.Pa" +
|
||||
"useSpeakerCommand.Feedback\022a\n\016ResumePlay" +
|
||||
"back\022&.cmvr.api.ResumeSpeakerCommand.Req" +
|
||||
"uest\032\'.cmvr.api.ResumeSpeakerCommand.Fee" +
|
||||
"dback\022b\n\tSetVolume\022).cmvr.api.SetSpeaker" +
|
||||
"VolumeCommand.Request\032*.cmvr.api.SetSpea" +
|
||||
"kerVolumeCommand.Feedback\022b\n\tGetVolume\022)" +
|
||||
".cmvr.api.GetSpeakerVolumeCommand.Reques" +
|
||||
"t\032*.cmvr.api.GetSpeakerVolumeCommand.Fee" +
|
||||
"dbackb\006proto3"
|
||||
"\032#.cmvr.api.PlayAudioCommand.Feedback\022j\n" +
|
||||
"\013StreamAudio\022+.cmvr.api.StreamSpeakerAud" +
|
||||
"ioCommand.Request\032,.cmvr.api.StreamSpeak" +
|
||||
"erAudioCommand.Feedback(\001\022[\n\014StopPlaybac" +
|
||||
"k\022$.cmvr.api.StopSpeakerCommand.Request\032" +
|
||||
"%.cmvr.api.StopSpeakerCommand.Feedback\022^" +
|
||||
"\n\rPausePlayback\022%.cmvr.api.PauseSpeakerC" +
|
||||
"ommand.Request\032&.cmvr.api.PauseSpeakerCo" +
|
||||
"mmand.Feedback\022a\n\016ResumePlayback\022&.cmvr." +
|
||||
"api.ResumeSpeakerCommand.Request\032\'.cmvr." +
|
||||
"api.ResumeSpeakerCommand.Feedback\022b\n\tSet" +
|
||||
"Volume\022).cmvr.api.SetSpeakerVolumeComman" +
|
||||
"d.Request\032*.cmvr.api.SetSpeakerVolumeCom" +
|
||||
"mand.Feedback\022b\n\tGetVolume\022).cmvr.api.Ge" +
|
||||
"tSpeakerVolumeCommand.Request\032*.cmvr.api" +
|
||||
".GetSpeakerVolumeCommand.Feedbackb\006proto" +
|
||||
"3"
|
||||
};
|
||||
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
||||
.internalBuildGeneratedFileFrom(descriptorData,
|
||||
|
||||
@ -29,6 +29,22 @@ message CommandHeader {
|
||||
}
|
||||
}
|
||||
|
||||
message AudioData {
|
||||
enum AudioFormat {
|
||||
PCM = 0;
|
||||
MP3 = 1;
|
||||
AAC = 2;
|
||||
WAV = 3;
|
||||
}
|
||||
bytes data = 1;
|
||||
int32 sample_rate = 2;
|
||||
int32 channels = 3;
|
||||
AudioFormat format = 4;
|
||||
string codec = 5;
|
||||
int64 pts = 6;
|
||||
int32 nb_samples = 7;
|
||||
}
|
||||
|
||||
message ConfigParam {
|
||||
|
||||
string param_name = 1;
|
||||
|
||||
@ -11,6 +11,7 @@ message MicState {
|
||||
int32 volume = 4;
|
||||
string error_message = 5;
|
||||
}
|
||||
|
||||
message GetMicStateCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
@ -21,6 +22,7 @@ message GetMicStateCommand {
|
||||
MicState state = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message StartMicRecordingCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
@ -59,10 +61,20 @@ message ResumeMicRecordingCommand {
|
||||
}
|
||||
}
|
||||
|
||||
message StreamMicAudioCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
AudioData audio = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message SetMicPhoneVolumeCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
int32 volume = 2; // 音量值(0 ~ 100)
|
||||
int32 volume = 2;
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
@ -75,7 +87,6 @@ message GetMicPhoneVolumeCommand {
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
int32 volume = 2; // 当前音量值
|
||||
int32 volume = 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,17 +4,14 @@ import "cmvr/api/microphone_command.proto";
|
||||
|
||||
package cmvr.api;
|
||||
|
||||
|
||||
service MicPhoneService {
|
||||
// 基本控制
|
||||
rpc GetStatus(GetMicStateCommand.Request) returns (GetMicStateCommand.Feedback);
|
||||
rpc StartRecord(StartMicRecordingCommand.Request) returns (StartMicRecordingCommand.Feedback);
|
||||
rpc StopRecord(StopMicRecordingCommand.Request) returns (StopMicRecordingCommand.Feedback);
|
||||
rpc PauseRecord(PauseMicRecordingCommand.Request) returns (PauseMicRecordingCommand.Feedback);
|
||||
rpc ResumeRecord(ResumeMicRecordingCommand.Request) returns (ResumeMicRecordingCommand.Feedback);
|
||||
rpc StreamAudio(StreamMicAudioCommand.Request) returns (stream StreamMicAudioCommand.Feedback);
|
||||
|
||||
// 音量控制
|
||||
rpc SetVolume(SetMicPhoneVolumeCommand.Request) returns (SetMicPhoneVolumeCommand.Feedback);
|
||||
rpc GetVolume(GetMicPhoneVolumeCommand.Request) returns (GetMicPhoneVolumeCommand.Feedback);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "cmvr/api/common.proto";
|
||||
|
||||
package cmvr.api;
|
||||
|
||||
// Real-time bidirectional robot audio service.
|
||||
// Java backend is the gRPC client; robot terminal is the gRPC server.
|
||||
service RobotAudioService {
|
||||
rpc AudioTalk(stream RobotAudioRequest) returns (stream RobotAudioResponse);
|
||||
}
|
||||
|
||||
// Fixed audio format: PCM_S16LE, 48 kHz, 16-bit, mono.
|
||||
// Both directions use 10 ms frames: 480 samples, 960 bytes.
|
||||
message RobotAudioFormat {
|
||||
int32 sample_rate = 1;
|
||||
int32 channels = 2;
|
||||
int32 bits_per_sample = 3;
|
||||
string encoding = 4;
|
||||
}
|
||||
|
||||
message RobotAudioStart {
|
||||
string session_id = 1;
|
||||
string operator_id = 2;
|
||||
RobotAudioFormat format = 3;
|
||||
}
|
||||
|
||||
message RobotAudioFrame {
|
||||
string session_id = 1;
|
||||
int64 sequence = 2;
|
||||
// Robot upload: capture timestamp. Browser downlink: backend send timestamp.
|
||||
int64 timestamp_ms = 3;
|
||||
// PCM_S16LE. Fixed 10 ms / 960 bytes at 48 kHz mono 16-bit.
|
||||
bytes pcm = 4;
|
||||
}
|
||||
|
||||
message RobotAudioHeartbeat {
|
||||
string session_id = 1;
|
||||
int64 timestamp_ms = 2;
|
||||
}
|
||||
|
||||
message RobotAudioStop {
|
||||
string session_id = 1;
|
||||
string reason = 2;
|
||||
}
|
||||
|
||||
message RobotAudioError {
|
||||
string session_id = 1;
|
||||
int32 code = 2;
|
||||
string message = 3;
|
||||
}
|
||||
|
||||
message RobotAudioStartAck {
|
||||
string session_id = 1;
|
||||
bool accepted = 2;
|
||||
string message = 3;
|
||||
}
|
||||
|
||||
message RobotAudioRequest {
|
||||
CommandHeader.Request header = 1;
|
||||
|
||||
oneof payload {
|
||||
RobotAudioStart start = 2;
|
||||
RobotAudioFrame audio_frame = 3;
|
||||
RobotAudioHeartbeat heartbeat = 4;
|
||||
RobotAudioStop stop = 5;
|
||||
RobotAudioError error = 6;
|
||||
}
|
||||
}
|
||||
|
||||
message RobotAudioResponse {
|
||||
CommandHeader.Feedback header = 1;
|
||||
|
||||
oneof payload {
|
||||
RobotAudioStartAck start_ack = 2;
|
||||
RobotAudioFrame audio_frame = 3;
|
||||
RobotAudioHeartbeat heartbeat = 4;
|
||||
RobotAudioStop stop = 5;
|
||||
RobotAudioError error = 6;
|
||||
}
|
||||
}
|
||||
@ -4,33 +4,15 @@ import "cmvr/api/common.proto";
|
||||
|
||||
package cmvr.api;
|
||||
|
||||
|
||||
// 音频数据格式(与麦克风类似,但用于输出)
|
||||
message AudioData {
|
||||
enum AudioFormat {
|
||||
PCM = 0;
|
||||
MP3 = 1;
|
||||
AAC = 2;
|
||||
WAV = 3;
|
||||
}
|
||||
bytes data = 1; // 音频二进制数据
|
||||
int32 sample_rate = 2; // 采样率(Hz)
|
||||
int32 channels = 3; // 声道数
|
||||
AudioFormat format = 4; // 音频格式
|
||||
string codec = 5; // 编码方式
|
||||
}
|
||||
|
||||
// 扬声器状态
|
||||
message SpeakerState {
|
||||
bool is_initialized = 1; // 是否已初始化
|
||||
bool is_running = 2; // 是否正在播放
|
||||
bool is_initialized = 1;
|
||||
bool is_running = 2;
|
||||
bool is_decoding = 3;
|
||||
bool is_paused = 5;
|
||||
int32 volume = 6; // 当前音量(0 ~ 100)
|
||||
string error_message = 7; // 错误信息
|
||||
int32 volume = 6;
|
||||
string error_message = 7;
|
||||
}
|
||||
|
||||
// 各种扬声器命令
|
||||
message GetSpeakerStateCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
@ -47,7 +29,19 @@ message PlayAudioCommand {
|
||||
CommandHeader.Request header = 1;
|
||||
string audio_path = 2;
|
||||
}
|
||||
message Feedback { CommandHeader.Feedback header = 1; }
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message StreamSpeakerAudioCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
AudioData audio = 2;
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message StopSpeakerCommand {
|
||||
@ -80,7 +74,7 @@ message ResumeSpeakerCommand {
|
||||
message SetSpeakerVolumeCommand {
|
||||
message Request {
|
||||
CommandHeader.Request header = 1;
|
||||
int32 volume = 2; // 音量值(0 ~ 100)
|
||||
int32 volume = 2;
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
@ -93,7 +87,6 @@ message GetSpeakerVolumeCommand {
|
||||
}
|
||||
message Feedback {
|
||||
CommandHeader.Feedback header = 1;
|
||||
int32 volume = 2; // 当前音量值
|
||||
int32 volume = 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,17 +4,14 @@ import "cmvr/api/speaker_command.proto";
|
||||
|
||||
package cmvr.api;
|
||||
|
||||
|
||||
service SpeakerService {
|
||||
// 基本控制
|
||||
rpc GetStatus(GetSpeakerStateCommand.Request) returns (GetSpeakerStateCommand.Feedback);
|
||||
rpc PlayAudio(PlayAudioCommand.Request) returns (PlayAudioCommand.Feedback);
|
||||
rpc StreamAudio(stream StreamSpeakerAudioCommand.Request) returns (StreamSpeakerAudioCommand.Feedback);
|
||||
rpc StopPlayback(StopSpeakerCommand.Request) returns (StopSpeakerCommand.Feedback);
|
||||
rpc PausePlayback(PauseSpeakerCommand.Request) returns (PauseSpeakerCommand.Feedback);
|
||||
rpc ResumePlayback(ResumeSpeakerCommand.Request) returns (ResumeSpeakerCommand.Feedback);
|
||||
|
||||
// 音量控制
|
||||
rpc SetVolume(SetSpeakerVolumeCommand.Request) returns (SetSpeakerVolumeCommand.Feedback);
|
||||
rpc GetVolume(GetSpeakerVolumeCommand.Request) returns (GetSpeakerVolumeCommand.Feedback);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
package com.cmvr.inspection.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 边缘 AI 推送的 PPE 违规报警主表实体。
|
||||
*/
|
||||
@Data
|
||||
@TableName("inspection_detection_alert")
|
||||
public class InspectionDetectionAlert implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.INPUT)
|
||||
private String id;
|
||||
|
||||
private String eventId;
|
||||
private String idempotencyKey;
|
||||
@TableField("schema_version")
|
||||
private String schema;
|
||||
private String sourceId;
|
||||
@TableField("source_sequence")
|
||||
private Long sequence;
|
||||
private Long capturedAtNs;
|
||||
private Long receivedAtNs;
|
||||
private String traceId;
|
||||
private String sessionId;
|
||||
private String inputPort;
|
||||
private String ruleId;
|
||||
private String modelId;
|
||||
private String modelName;
|
||||
private String labelsJson;
|
||||
@TableField("detection_scope")
|
||||
private String scope;
|
||||
private String scopeId;
|
||||
private Integer hitCount;
|
||||
private Double windowMs;
|
||||
private Long firstSeenNs;
|
||||
private Long lastSeenNs;
|
||||
private Long triggeredAtNs;
|
||||
private Double maxConfidence;
|
||||
private String imagePath;
|
||||
private String rawJson;
|
||||
private Date createdAt;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package com.cmvr.inspection.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* PPE 报警检测框明细实体。
|
||||
*/
|
||||
@Data
|
||||
@TableName("inspection_detection_detail")
|
||||
public class InspectionDetectionDetail implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.INPUT)
|
||||
private String id;
|
||||
|
||||
private String alertId;
|
||||
private String label;
|
||||
private Double confidence;
|
||||
private Double xMin;
|
||||
private Double yMin;
|
||||
private Double xMax;
|
||||
private Double yMax;
|
||||
private Long trackId;
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package com.cmvr.inspection.domain.dto.alert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 边缘 AI 推送消息的统一信封。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class AlertEnvelope
|
||||
{
|
||||
/** 消息结构版本,当前固定为 DetectionAlert/v1。 */
|
||||
@JsonProperty("schema")
|
||||
private String schema;
|
||||
|
||||
/** 产生消息的视频源标识。 */
|
||||
@JsonProperty("source_id")
|
||||
private String sourceId;
|
||||
|
||||
/** 视频源内递增序号。 */
|
||||
@JsonProperty("sequence")
|
||||
private Long sequence;
|
||||
|
||||
/** 图像采集时间,Unix 纳秒时间戳。 */
|
||||
@JsonProperty("captured_at_ns")
|
||||
private Long capturedAtNs;
|
||||
|
||||
/** 边缘端接收时间,Unix 纳秒时间戳。 */
|
||||
@JsonProperty("received_at_ns")
|
||||
private Long receivedAtNs;
|
||||
|
||||
/** 全链路追踪标识。 */
|
||||
@JsonProperty("trace_id")
|
||||
private String traceId;
|
||||
|
||||
/** 边缘推理会话标识。 */
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
/** 产生消息的输入端口。 */
|
||||
@JsonProperty("input_port")
|
||||
private String inputPort;
|
||||
|
||||
/** 扩展属性。 */
|
||||
@JsonProperty("attributes")
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
/** PPE 违规报警内容。 */
|
||||
@JsonProperty("payload")
|
||||
private DetectionAlert payload;
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.cmvr.inspection.domain.dto.alert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* 报警现场图片。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class AlertImage
|
||||
{
|
||||
/** 图片媒体类型,例如 image/jpeg。 */
|
||||
@JsonProperty("media_type")
|
||||
private String mediaType;
|
||||
|
||||
@JsonProperty("width")
|
||||
private Integer width;
|
||||
|
||||
@JsonProperty("height")
|
||||
private Integer height;
|
||||
|
||||
/** 编码方式,当前协议固定为 base64。 */
|
||||
@JsonProperty("encoding")
|
||||
private String encoding;
|
||||
|
||||
/** Base64 编码后的图片内容。 */
|
||||
@JsonProperty("data")
|
||||
@ToString.Exclude
|
||||
private String data;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.cmvr.inspection.domain.dto.alert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 检测目标的像素坐标框。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class BoundingBox
|
||||
{
|
||||
@JsonProperty("x_min")
|
||||
private Double xMin;
|
||||
|
||||
@JsonProperty("y_min")
|
||||
private Double yMin;
|
||||
|
||||
@JsonProperty("x_max")
|
||||
private Double xMax;
|
||||
|
||||
@JsonProperty("y_max")
|
||||
private Double yMax;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.cmvr.inspection.domain.dto.alert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 单个 PPE 检测结果。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class Detection
|
||||
{
|
||||
/** 检测标签。 */
|
||||
@JsonProperty("label")
|
||||
private String label;
|
||||
|
||||
/** 检测置信度。 */
|
||||
@JsonProperty("confidence")
|
||||
private Double confidence;
|
||||
|
||||
/** 目标框坐标。 */
|
||||
@JsonProperty("box")
|
||||
private BoundingBox box;
|
||||
|
||||
/** 跟踪目标标识。 */
|
||||
@JsonProperty("track_id")
|
||||
private Long trackId;
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package com.cmvr.inspection.domain.dto.alert;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* PPE 违规报警业务数据。
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DetectionAlert
|
||||
{
|
||||
/** 边缘端生成的全局事件标识。 */
|
||||
@JsonProperty("event_id")
|
||||
private String eventId;
|
||||
|
||||
/** 命中的规则标识。 */
|
||||
@JsonProperty("rule_id")
|
||||
private String ruleId;
|
||||
|
||||
/** 模型版本标识。 */
|
||||
@JsonProperty("model_id")
|
||||
private String modelId;
|
||||
|
||||
/** 模型显示名称。 */
|
||||
@JsonProperty("model_name")
|
||||
private String modelName;
|
||||
|
||||
/** 命中的标签集合。 */
|
||||
@JsonProperty("labels")
|
||||
private List<String> labels;
|
||||
|
||||
/** 规则统计范围。 */
|
||||
@JsonProperty("scope")
|
||||
private String scope;
|
||||
|
||||
/** 规则统计范围对应的对象标识。 */
|
||||
@JsonProperty("scope_id")
|
||||
private String scopeId;
|
||||
|
||||
/** 时间窗口内命中次数。 */
|
||||
@JsonProperty("hit_count")
|
||||
private Integer hitCount;
|
||||
|
||||
/** 规则统计窗口,单位毫秒。 */
|
||||
@JsonProperty("window_ms")
|
||||
private Double windowMs;
|
||||
|
||||
/** 首次命中时间,Unix 纳秒时间戳。 */
|
||||
@JsonProperty("first_seen_ns")
|
||||
private Long firstSeenNs;
|
||||
|
||||
/** 最后命中时间,Unix 纳秒时间戳。 */
|
||||
@JsonProperty("last_seen_ns")
|
||||
private Long lastSeenNs;
|
||||
|
||||
/** 触发报警时间,Unix 纳秒时间戳。 */
|
||||
@JsonProperty("triggered_at_ns")
|
||||
private Long triggeredAtNs;
|
||||
|
||||
/** 本次报警中的最大置信度。 */
|
||||
@JsonProperty("max_confidence")
|
||||
private Double maxConfidence;
|
||||
|
||||
/** 报警关联的检测框。 */
|
||||
@JsonProperty("detections")
|
||||
private List<Detection> detections;
|
||||
|
||||
/** 报警现场图片,允许为空。 */
|
||||
@JsonProperty("image")
|
||||
private AlertImage image;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.cmvr.inspection.exception;
|
||||
|
||||
/**
|
||||
* 边缘检测报警请求参数不符合协议。
|
||||
*/
|
||||
public class DetectionAlertBadRequestException extends RuntimeException
|
||||
{
|
||||
public DetectionAlertBadRequestException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DetectionAlertBadRequestException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.cmvr.inspection.exception;
|
||||
|
||||
/**
|
||||
* 数据库或对象存储暂时不可用,边缘端可稍后重试。
|
||||
*/
|
||||
public class DetectionAlertTemporaryException extends RuntimeException
|
||||
{
|
||||
public DetectionAlertTemporaryException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DetectionAlertTemporaryException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.cmvr.inspection.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.cmvr.inspection.domain.InspectionDetectionAlert;
|
||||
|
||||
/**
|
||||
* PPE 违规报警主表 Mapper。
|
||||
*/
|
||||
public interface InspectionDetectionAlertMapper extends BaseMapper<InspectionDetectionAlert>
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.cmvr.inspection.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.cmvr.inspection.domain.InspectionDetectionDetail;
|
||||
|
||||
/**
|
||||
* PPE 报警检测框明细 Mapper。
|
||||
*/
|
||||
public interface InspectionDetectionDetailMapper extends BaseMapper<InspectionDetectionDetail>
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.cmvr.inspection.service;
|
||||
|
||||
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
|
||||
|
||||
/**
|
||||
* 边缘 AI 检测报警接收服务。
|
||||
*/
|
||||
public interface IInspectionDetectionAlertService
|
||||
{
|
||||
/**
|
||||
* 接收并持久化一条 PPE 违规报警。
|
||||
*
|
||||
* @param envelope 报警信封
|
||||
* @param requestIdempotencyKey HTTP 请求头中的幂等键,允许为空
|
||||
* @return true 表示本次新增,false 表示重复事件
|
||||
*/
|
||||
boolean receive(AlertEnvelope envelope, String requestIdempotencyKey);
|
||||
}
|
||||
@ -0,0 +1,391 @@
|
||||
package com.cmvr.inspection.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.cmvr.common.config.properties.MinioProperties;
|
||||
import com.cmvr.common.core.minio.MinioService;
|
||||
import com.cmvr.inspection.domain.InspectionDetectionAlert;
|
||||
import com.cmvr.inspection.domain.InspectionDetectionDetail;
|
||||
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
|
||||
import com.cmvr.inspection.domain.dto.alert.AlertImage;
|
||||
import com.cmvr.inspection.domain.dto.alert.BoundingBox;
|
||||
import com.cmvr.inspection.domain.dto.alert.Detection;
|
||||
import com.cmvr.inspection.exception.DetectionAlertBadRequestException;
|
||||
import com.cmvr.inspection.exception.DetectionAlertTemporaryException;
|
||||
import com.cmvr.inspection.mapper.InspectionDetectionAlertMapper;
|
||||
import com.cmvr.inspection.mapper.InspectionDetectionDetailMapper;
|
||||
import com.cmvr.inspection.service.IInspectionDetectionAlertService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* PPE 违规报警接收业务实现。
|
||||
*
|
||||
* 数据库唯一索引是并发幂等的最终保障;业务预查用于减少重复请求的无效写入。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InspectionDetectionAlertServiceImpl implements IInspectionDetectionAlertService
|
||||
{
|
||||
private static final String SUPPORTED_SCHEMA = "DetectionAlert/v1";
|
||||
private static final String IMAGE_ENCODING_BASE64 = "base64";
|
||||
private static final String IMAGE_OBJECT_PREFIX = "inspection/detection-alerts";
|
||||
private static final int MAX_IDEMPOTENCY_KEY_LENGTH = 191;
|
||||
private static final int MAX_EVENT_ID_LENGTH = 128;
|
||||
private static final int MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
||||
private static final int MAX_BASE64_LENGTH = (MAX_IMAGE_BYTES * 4 / 3) + 8;
|
||||
|
||||
private final InspectionDetectionAlertMapper alertMapper;
|
||||
private final InspectionDetectionDetailMapper detailMapper;
|
||||
private final MinioService minioService;
|
||||
private final MinioProperties minioProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean receive(AlertEnvelope envelope, String requestIdempotencyKey)
|
||||
{
|
||||
String idempotencyKey = validateAndResolveIdempotencyKey(envelope, requestIdempotencyKey);
|
||||
String eventId = trimToNull(envelope.getPayload().getEventId());
|
||||
|
||||
if (exists(idempotencyKey, eventId))
|
||||
{
|
||||
log.info("忽略重复PPE报警,idempotencyKey={},eventId={}", idempotencyKey, eventId);
|
||||
return false;
|
||||
}
|
||||
|
||||
InspectionDetectionAlert alert = buildAlert(envelope, idempotencyKey);
|
||||
StoredImage storedImage = null;
|
||||
try
|
||||
{
|
||||
alertMapper.insert(alert);
|
||||
|
||||
AlertImage image = envelope.getPayload().getImage();
|
||||
if (image != null)
|
||||
{
|
||||
storedImage = uploadImage(alert.getId(), image);
|
||||
alert.setImagePath(storedImage.url);
|
||||
if (alertMapper.updateById(alert) != 1)
|
||||
{
|
||||
throw new DetectionAlertTemporaryException("更新报警图片地址失败");
|
||||
}
|
||||
}
|
||||
|
||||
saveDetectionDetails(alert.getId(), envelope.getPayload().getDetections());
|
||||
log.info("PPE报警接收成功,alertId={},eventId={},sourceId={}",
|
||||
alert.getId(), eventId, envelope.getSourceId());
|
||||
return true;
|
||||
}
|
||||
catch (RuntimeException ex)
|
||||
{
|
||||
// 数据库事务无法回滚 MinIO,对后续步骤失败时主动删除已上传对象。
|
||||
if (storedImage != null)
|
||||
{
|
||||
deleteUploadedImageQuietly(storedImage.objectName);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验协议关键字段,并按请求头、event_id、trace_id 的顺序生成幂等键。
|
||||
*/
|
||||
private String validateAndResolveIdempotencyKey(AlertEnvelope envelope, String requestIdempotencyKey)
|
||||
{
|
||||
if (envelope == null)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("请求体不能为空");
|
||||
}
|
||||
if (!SUPPORTED_SCHEMA.equals(envelope.getSchema()))
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("schema必须为" + SUPPORTED_SCHEMA);
|
||||
}
|
||||
if (envelope.getPayload() == null)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload不能为空");
|
||||
}
|
||||
|
||||
String eventId = trimToNull(envelope.getPayload().getEventId());
|
||||
if (eventId != null && eventId.length() > MAX_EVENT_ID_LENGTH)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.event_id长度不能超过" + MAX_EVENT_ID_LENGTH);
|
||||
}
|
||||
|
||||
String idempotencyKey = trimToNull(requestIdempotencyKey);
|
||||
if (idempotencyKey == null)
|
||||
{
|
||||
idempotencyKey = eventId;
|
||||
}
|
||||
if (idempotencyKey == null)
|
||||
{
|
||||
idempotencyKey = trimToNull(envelope.getTraceId());
|
||||
}
|
||||
if (idempotencyKey == null)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("Idempotency-Key、payload.event_id和trace_id不能同时为空");
|
||||
}
|
||||
if (idempotencyKey.length() > MAX_IDEMPOTENCY_KEY_LENGTH)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("幂等键长度不能超过" + MAX_IDEMPOTENCY_KEY_LENGTH);
|
||||
}
|
||||
|
||||
validateImage(envelope.getPayload().getImage());
|
||||
return idempotencyKey;
|
||||
}
|
||||
|
||||
private void validateImage(AlertImage image)
|
||||
{
|
||||
if (image == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!IMAGE_ENCODING_BASE64.equalsIgnoreCase(trimToEmpty(image.getEncoding())))
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.image.encoding必须为base64");
|
||||
}
|
||||
if (StringUtils.isBlank(image.getData()))
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.image.data不能为空");
|
||||
}
|
||||
if (image.getData().length() > MAX_BASE64_LENGTH)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("报警图片不能超过10MB");
|
||||
}
|
||||
String mediaType = trimToNull(image.getMediaType());
|
||||
if (mediaType == null || !mediaType.toLowerCase(Locale.ROOT).startsWith("image/"))
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.image.media_type必须是图片类型");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean exists(String idempotencyKey, String eventId)
|
||||
{
|
||||
LambdaQueryWrapper<InspectionDetectionAlert> query = new LambdaQueryWrapper<>();
|
||||
query.eq(InspectionDetectionAlert::getIdempotencyKey, idempotencyKey);
|
||||
if (eventId != null)
|
||||
{
|
||||
query.or().eq(InspectionDetectionAlert::getEventId, eventId);
|
||||
}
|
||||
return alertMapper.selectCount(query) > 0;
|
||||
}
|
||||
|
||||
private InspectionDetectionAlert buildAlert(AlertEnvelope envelope, String idempotencyKey)
|
||||
{
|
||||
com.cmvr.inspection.domain.dto.alert.DetectionAlert payload = envelope.getPayload();
|
||||
InspectionDetectionAlert alert = new InspectionDetectionAlert();
|
||||
alert.setId(newId());
|
||||
alert.setEventId(trimToNull(payload.getEventId()));
|
||||
alert.setIdempotencyKey(idempotencyKey);
|
||||
alert.setSchema(envelope.getSchema());
|
||||
alert.setSourceId(envelope.getSourceId());
|
||||
alert.setSequence(envelope.getSequence());
|
||||
alert.setCapturedAtNs(envelope.getCapturedAtNs());
|
||||
alert.setReceivedAtNs(envelope.getReceivedAtNs());
|
||||
alert.setTraceId(envelope.getTraceId());
|
||||
alert.setSessionId(envelope.getSessionId());
|
||||
alert.setInputPort(envelope.getInputPort());
|
||||
alert.setRuleId(payload.getRuleId());
|
||||
alert.setModelId(payload.getModelId());
|
||||
alert.setModelName(payload.getModelName());
|
||||
alert.setLabelsJson(writeJson(payload.getLabels(), "序列化labels失败"));
|
||||
alert.setScope(payload.getScope());
|
||||
alert.setScopeId(payload.getScopeId());
|
||||
alert.setHitCount(payload.getHitCount());
|
||||
alert.setWindowMs(payload.getWindowMs());
|
||||
alert.setFirstSeenNs(payload.getFirstSeenNs());
|
||||
alert.setLastSeenNs(payload.getLastSeenNs());
|
||||
alert.setTriggeredAtNs(payload.getTriggeredAtNs());
|
||||
alert.setMaxConfidence(payload.getMaxConfidence());
|
||||
alert.setRawJson(buildSanitizedRawJson(envelope));
|
||||
alert.setCreatedAt(new Date());
|
||||
return alert;
|
||||
}
|
||||
|
||||
/**
|
||||
* raw_json保留协议元数据,但明确移除Base64图片,避免图片以另一种形式进入数据库。
|
||||
*/
|
||||
private String buildSanitizedRawJson(AlertEnvelope envelope)
|
||||
{
|
||||
AlertImage image = envelope.getPayload().getImage();
|
||||
String imageData = image == null ? null : image.getData();
|
||||
try
|
||||
{
|
||||
// 避免构建JSON树时复制一份大Base64字符串;finally中恢复仅供随后上传MinIO。
|
||||
if (image != null)
|
||||
{
|
||||
image.setData(null);
|
||||
}
|
||||
return objectMapper.writeValueAsString(envelope);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new DetectionAlertTemporaryException("序列化报警原始数据失败", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (image != null)
|
||||
{
|
||||
image.setData(imageData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String writeJson(Object value, String errorMessage)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
try
|
||||
{
|
||||
return objectMapper.writeValueAsString(value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new DetectionAlertTemporaryException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private StoredImage uploadImage(String alertId, AlertImage image)
|
||||
{
|
||||
final byte[] imageBytes;
|
||||
try
|
||||
{
|
||||
imageBytes = Base64.getDecoder().decode(image.getData().getBytes(StandardCharsets.US_ASCII));
|
||||
}
|
||||
catch (IllegalArgumentException ex)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.image.data不是有效的Base64", ex);
|
||||
}
|
||||
if (imageBytes.length > MAX_IMAGE_BYTES)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("报警图片不能超过10MB");
|
||||
}
|
||||
|
||||
String extension = extensionFor(image.getMediaType());
|
||||
String datePath = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
|
||||
String objectName = IMAGE_OBJECT_PREFIX + "/" + datePath + "/" + alertId + extension;
|
||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes))
|
||||
{
|
||||
minioService.uploadStream(minioProperties.getBucketName(), objectName, inputStream,
|
||||
imageBytes.length, image.getMediaType());
|
||||
return new StoredImage(objectName, buildMinioUrl(objectName));
|
||||
}
|
||||
catch (DetectionAlertBadRequestException ex)
|
||||
{
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new DetectionAlertTemporaryException("上传报警图片到MinIO失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDetectionDetails(String alertId, List<Detection> detections)
|
||||
{
|
||||
if (detections == null || detections.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (Detection detection : detections)
|
||||
{
|
||||
if (detection == null)
|
||||
{
|
||||
throw new DetectionAlertBadRequestException("payload.detections不能包含null元素");
|
||||
}
|
||||
InspectionDetectionDetail detail = new InspectionDetectionDetail();
|
||||
detail.setId(newId());
|
||||
detail.setAlertId(alertId);
|
||||
detail.setLabel(detection.getLabel());
|
||||
detail.setConfidence(detection.getConfidence());
|
||||
detail.setTrackId(detection.getTrackId());
|
||||
BoundingBox box = detection.getBox();
|
||||
if (box != null)
|
||||
{
|
||||
detail.setXMin(box.getXMin());
|
||||
detail.setYMin(box.getYMin());
|
||||
detail.setXMax(box.getXMax());
|
||||
detail.setYMax(box.getYMax());
|
||||
}
|
||||
detailMapper.insert(detail);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildMinioUrl(String objectName)
|
||||
{
|
||||
String baseUrl = StringUtils.removeEnd(minioProperties.getUrl(), "/");
|
||||
return baseUrl + "/" + minioProperties.getBucketName() + "/" + objectName;
|
||||
}
|
||||
|
||||
private void deleteUploadedImageQuietly(String objectName)
|
||||
{
|
||||
try
|
||||
{
|
||||
minioService.deleteFile(minioProperties.getBucketName(), objectName);
|
||||
}
|
||||
catch (Exception cleanupEx)
|
||||
{
|
||||
log.error("回滚PPE报警时删除MinIO对象失败,objectName={}", objectName, cleanupEx);
|
||||
}
|
||||
}
|
||||
|
||||
private String extensionFor(String mediaType)
|
||||
{
|
||||
String normalized = trimToEmpty(mediaType).toLowerCase(Locale.ROOT);
|
||||
if ("image/jpeg".equals(normalized) || "image/jpg".equals(normalized))
|
||||
{
|
||||
return ".jpg";
|
||||
}
|
||||
if ("image/png".equals(normalized))
|
||||
{
|
||||
return ".png";
|
||||
}
|
||||
if ("image/webp".equals(normalized))
|
||||
{
|
||||
return ".webp";
|
||||
}
|
||||
// 未知图片类型使用无执行语义的后缀,实际Content-Type仍按协议上传。
|
||||
return ".img";
|
||||
}
|
||||
|
||||
private String trimToNull(String value)
|
||||
{
|
||||
return StringUtils.trimToNull(value);
|
||||
}
|
||||
|
||||
private String trimToEmpty(String value)
|
||||
{
|
||||
return StringUtils.trimToEmpty(value);
|
||||
}
|
||||
|
||||
private String newId()
|
||||
{
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
private static final class StoredImage
|
||||
{
|
||||
private final String objectName;
|
||||
private final String url;
|
||||
|
||||
private StoredImage(String objectName, String url)
|
||||
{
|
||||
this.objectName = objectName;
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
54
sql/inspection_detection_alert.sql
Normal file
54
sql/inspection_detection_alert.sql
Normal file
@ -0,0 +1,54 @@
|
||||
-- PPE违规报警接收表。
|
||||
-- 本脚本用于已有数据库增量部署,不会删除现有巡检数据。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `inspection_detection_alert` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键ID',
|
||||
`event_id` varchar(128) DEFAULT NULL COMMENT '边缘端事件ID',
|
||||
`idempotency_key` varchar(191) NOT NULL COMMENT 'HTTP幂等键',
|
||||
`schema_version` varchar(64) NOT NULL COMMENT '消息结构版本',
|
||||
`source_id` varchar(128) DEFAULT NULL COMMENT '视频源标识',
|
||||
`source_sequence` bigint DEFAULT NULL COMMENT '视频源内消息序号',
|
||||
`captured_at_ns` bigint DEFAULT NULL COMMENT '采集时间Unix纳秒',
|
||||
`received_at_ns` bigint DEFAULT NULL COMMENT '边缘端接收时间Unix纳秒',
|
||||
`trace_id` varchar(128) DEFAULT NULL COMMENT '链路追踪ID',
|
||||
`session_id` varchar(128) DEFAULT NULL COMMENT '边缘推理会话ID',
|
||||
`input_port` varchar(128) DEFAULT NULL COMMENT '消息输入端口',
|
||||
`rule_id` varchar(128) DEFAULT NULL COMMENT '命中的规则ID',
|
||||
`model_id` varchar(255) DEFAULT NULL COMMENT '模型版本ID',
|
||||
`model_name` varchar(255) DEFAULT NULL COMMENT '模型名称',
|
||||
`labels_json` text COMMENT '命中标签JSON',
|
||||
`detection_scope` varchar(64) DEFAULT NULL COMMENT '规则统计范围',
|
||||
`scope_id` varchar(128) DEFAULT NULL COMMENT '统计范围对象ID',
|
||||
`hit_count` int DEFAULT NULL COMMENT '窗口内命中次数',
|
||||
`window_ms` decimal(12,3) DEFAULT NULL COMMENT '统计窗口毫秒',
|
||||
`first_seen_ns` bigint DEFAULT NULL COMMENT '首次命中时间Unix纳秒',
|
||||
`last_seen_ns` bigint DEFAULT NULL COMMENT '最后命中时间Unix纳秒',
|
||||
`triggered_at_ns` bigint DEFAULT NULL COMMENT '报警触发时间Unix纳秒',
|
||||
`max_confidence` decimal(8,6) DEFAULT NULL COMMENT '最大置信度',
|
||||
`image_path` varchar(1000) DEFAULT NULL COMMENT 'MinIO图片URL',
|
||||
`raw_json` mediumtext COMMENT '已移除Base64图片数据的原始协议JSON',
|
||||
`created_at` datetime(3) NOT NULL COMMENT '平台接收时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_detection_alert_event_id` (`event_id`),
|
||||
UNIQUE KEY `uk_detection_alert_idempotency_key` (`idempotency_key`),
|
||||
KEY `idx_detection_alert_source_id` (`source_id`),
|
||||
KEY `idx_detection_alert_rule_id` (`rule_id`),
|
||||
KEY `idx_detection_alert_triggered_at` (`triggered_at_ns`),
|
||||
KEY `idx_detection_alert_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检PPE违规报警主表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `inspection_detection_detail` (
|
||||
`id` varchar(32) NOT NULL COMMENT '主键ID',
|
||||
`alert_id` varchar(32) NOT NULL COMMENT '报警主表ID',
|
||||
`label` varchar(128) DEFAULT NULL COMMENT '检测标签',
|
||||
`confidence` decimal(8,6) DEFAULT NULL COMMENT '检测置信度',
|
||||
`x_min` decimal(12,4) DEFAULT NULL COMMENT '目标框左上X坐标',
|
||||
`y_min` decimal(12,4) DEFAULT NULL COMMENT '目标框左上Y坐标',
|
||||
`x_max` decimal(12,4) DEFAULT NULL COMMENT '目标框右下X坐标',
|
||||
`y_max` decimal(12,4) DEFAULT NULL COMMENT '目标框右下Y坐标',
|
||||
`track_id` bigint DEFAULT NULL COMMENT '目标跟踪ID',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_detection_detail_alert_id` (`alert_id`),
|
||||
CONSTRAINT `fk_detection_detail_alert` FOREIGN KEY (`alert_id`)
|
||||
REFERENCES `inspection_detection_alert` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检PPE报警检测框明细表';
|
||||
Loading…
Reference in New Issue
Block a user