From 441e99483b106bdd05fdca48f037f0e0ecd1b18b Mon Sep 17 00:00:00 2001 From: acgist <289547414@qq.com> Date: Wed, 31 May 2023 07:34:18 +0800 Subject: [PATCH] =?UTF-8?q?[*]=20=E6=9C=8D=E5=8A=A1=E7=AB=AF=E5=BD=95?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- docs/TODO.md | 6 +- .../acgist/taoyao/client/MainActivity.java | 2 +- .../acgist/taoyao/client/MediaService.java | 13 +- .../acgist/taoyao/client/signal/Taoyao.java | 2 +- .../client/src/main/res/values/settings.xml | 2 +- .../com/acgist/taoyao/media/MediaManager.java | 16 +- .../taoyao/media/audio/MixerProcesser.java | 2 +- .../taoyao/media/client/RecordClient.java | 48 ++-- .../acgist/taoyao/media/config/Config.java | 2 +- .../webrtc/audio/JavaAudioDeviceModule.java | 4 +- .../org/webrtc/audio/WebRtcAudioRecord.java | 4 +- taoyao-client-media/README.md | 2 +- taoyao-client-media/src/Taoyao.js | 67 ++++++ taoyao-signal-server/README.md | 27 +++ .../taoyao/boot/config/FfmpegProperties.java | 39 +++ .../configuration/BootAutoConfiguration.java | 10 +- .../acgist/taoyao/boot/utils/FileUtils.java | 21 ++ .../taoyao/controller/ConfigController.java | 14 +- .../taoyao/controller/ControlController.java | 46 +--- .../taoyao/controller/MediaController.java | 37 +++ .../src/main/resources/application.yml | 63 ++--- .../java/com/acgist/taoyao/RecorderTest.java | 36 +++ .../java/com/acgist/taoyao/rtp/RtpTest.java | 11 +- .../signal/config/camera/AiProperties.java | 36 --- .../config/camera/BeautyProperties.java | 24 -- .../config/camera/CameraProperties.java | 50 ---- .../config/camera/WatermarkProperties.java | 30 --- .../SignalAutoConfiguration.java | 5 - .../taoyao/signal/model/control/PtzModel.java | 37 --- .../signal/party/media/ClientWrapper.java | 4 + .../taoyao/signal/party/media/Recorder.java | 224 ++++++++++++++++++ .../protocol/control/ControlAiProtocol.java | 55 ----- .../control/ControlBeautyProtocol.java | 54 ----- .../protocol/control/ControlPtzProtocol.java | 54 ----- .../control/ControlWatermarkProtocol.java | 55 ----- .../protocol/media/MediaRecordProtocol.java | 79 +++++- 37 files changed, 640 insertions(+), 545 deletions(-) create mode 100644 taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/FfmpegProperties.java create mode 100644 taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/MediaController.java create mode 100644 taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/RecorderTest.java delete mode 100644 taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/AiProperties.java delete mode 100644 taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/BeautyProperties.java delete mode 100644 taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/CameraProperties.java delete mode 100644 taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/WatermarkProperties.java delete mode 100644 taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/model/control/PtzModel.java create mode 100644 taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/party/media/Recorder.java delete mode 100644 taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlAiProtocol.java delete mode 100644 taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlBeautyProtocol.java delete mode 100644 taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlPtzProtocol.java delete mode 100644 taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlWatermarkProtocol.java diff --git a/README.md b/README.md index fa78a6f..2846ea1 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ |功能|是否支持|是否实现|描述| |:--|:--|:--|:--| |P2P|支持|完成|视频会话(监控)| -|Mediasoup|支持|完成|视频房间(会话)| +|Mediasoup|支持|完成|视频房间(会议)| |控制|支持|完成|完整控制信令| ## Media终端功能 @@ -39,8 +39,8 @@ |功能|是否支持|是否实现|描述| |:--|:--|:--|:--| |Mediasoup|支持|完成|视频房间(会议)| +|控制|支持|完成|部分控制信令| |录像|支持|未完成|录像| -|混音|支持|未完成|多路混音| ### Android终端功能 diff --git a/docs/TODO.md b/docs/TODO.md index 13e5be7..53ec7a9 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -32,14 +32,14 @@ * 发布版本:1.0.0 * 开机自启 -* 录制底噪 +* 录像底噪 * 性能测试 * 稳定性测试 * 分辨率调整 -* 服务端录制 +* 服务端录像 * 安卓预览按钮 * 安装内存抖动 -* 降低视频录制大小 +* 降低视频录像大小 * 防止重复邀请拉取 * 码率等等参数配置验证 * 查询消费者生产者信息 diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java index ebb80d7..d753167 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MainActivity.java @@ -239,7 +239,7 @@ public class MainActivity extends AppCompatActivity { } /** - * 录制按钮 + * 录像按钮 * * @param message 消息 */ diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java index bc2f6ad..451e6a2 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/MediaService.java @@ -63,7 +63,7 @@ public class MediaService extends Service { */ RECONNECT, /** - * 屏幕录制 + * 屏幕录像 */ SCREEN_CAPTURE; @@ -222,7 +222,7 @@ public class MediaService extends Service { } /** - * 屏幕录制 + * 屏幕录像 * * @param intent Intent */ @@ -232,8 +232,8 @@ public class MediaService extends Service { final NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, TAOYAO) .setSmallIcon(R.mipmap.ic_launcher_foreground) .setLargeIcon(BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher_foreground)) - .setContentTitle("录制屏幕") - .setContentText("桃夭正在录制屏幕") + .setContentTitle("屏幕录像") + .setContentText("桃夭正在屏幕录像") .setContentIntent(pendingIntent); final Notification notification = notificationBuilder.build(); this.startForeground((int) System.currentTimeMillis(), notification); @@ -279,13 +279,16 @@ public class MediaService extends Service { private void settingAudio() { final AudioManager audioManager = this.getApplicationContext().getSystemService(AudioManager.class); audioManager.setSpeakerphoneOn(true); +// audioManager.setMicrophoneMute(true); + audioManager.setMode(AudioManager.MODE_IN_CALL); Log.d(MediaService.class.getSimpleName(), "当前音频模式:" + audioManager.getMode()); Log.d(MediaService.class.getSimpleName(), "当前音频音量:" + audioManager.getStreamVolume(audioManager.getMode())); Log.d(MediaService.class.getSimpleName(), "当前最大音频音量:" + audioManager.getStreamMaxVolume(audioManager.getMode())); Log.d(MediaService.class.getSimpleName(), "当前蓝牙是否打开:" + audioManager.isBluetoothScoOn()); // Log.d(MediaService.class.getSimpleName(), "当前耳机是否打开:" + audioManager.isWiredHeadsetOn()); Log.d(MediaService.class.getSimpleName(), "当前扬声器是否打开:" + audioManager.isSpeakerphoneOn()); - audioManager.setStreamVolume(AudioManager.MODE_IN_COMMUNICATION, audioManager.getStreamMaxVolume(AudioManager.MODE_IN_COMMUNICATION), AudioManager.FLAG_PLAY_SOUND); + audioManager.setStreamVolume(AudioManager.MODE_IN_CALL, audioManager.getStreamMaxVolume(AudioManager.MODE_IN_CALL), AudioManager.FLAG_PLAY_SOUND); +// audioManager.setStreamVolume(AudioManager.MODE_IN_COMMUNICATION, audioManager.getStreamMaxVolume(AudioManager.MODE_IN_COMMUNICATION), AudioManager.FLAG_PLAY_SOUND); } /** diff --git a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java index 6da66f9..0828d1d 100644 --- a/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java +++ b/taoyao-client-android/taoyao/client/src/main/java/com/acgist/taoyao/client/signal/Taoyao.java @@ -786,7 +786,7 @@ public final class Taoyao implements ITaoyao { } /** - * 录制 + * 录像 * * @param message 信令消息 * @param body 信令主体 diff --git a/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml b/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml index dc7d529..ce3ea1a 100644 --- a/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml +++ b/taoyao-client-android/taoyao/client/src/main/res/values/settings.xml @@ -1,7 +1,7 @@ - true + false true diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java index 655b233..65d81b9 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/MediaManager.java @@ -148,7 +148,7 @@ public final class MediaManager { */ private VideoSource shareVideoSource; /** - * 录制终端 + * 录像终端 */ private RecordClient recordClient; /** @@ -217,7 +217,7 @@ public final class MediaManager { } /** - * @return 是否正在录制 + * @return 是否正在录像 */ public boolean isRecording() { return this.recordClient != null; @@ -440,25 +440,25 @@ public final class MediaManager { .setAudioRecordErrorCallback(new JavaAudioDeviceModule.AudioRecordErrorCallback() { @Override public void onWebRtcAudioRecordInitError(String errorMessage) { - Log.e(MediaManager.class.getSimpleName(), "WebRTC本地音频录制加载异常:" + errorMessage); + Log.e(MediaManager.class.getSimpleName(), "WebRTC本地音频录像加载异常:" + errorMessage); } @Override public void onWebRtcAudioRecordStartError(JavaAudioDeviceModule.AudioRecordStartErrorCode errorCode, String errorMessage) { - Log.e(MediaManager.class.getSimpleName(), "WebRTC本地音频录制开始异常:" + errorMessage); + Log.e(MediaManager.class.getSimpleName(), "WebRTC本地音频录像开始异常:" + errorMessage); } @Override public void onWebRtcAudioRecordError(String errorMessage) { - Log.e(MediaManager.class.getSimpleName(), "WebRTC本地音频录制异常:" + errorMessage); + Log.e(MediaManager.class.getSimpleName(), "WebRTC本地音频录像异常:" + errorMessage); } }) .setAudioRecordStateCallback(new JavaAudioDeviceModule.AudioRecordStateCallback() { @Override public void onWebRtcAudioRecordStart() { - Log.i(MediaManager.class.getSimpleName(), "WebRTC本地音频录制开始"); + Log.i(MediaManager.class.getSimpleName(), "WebRTC本地音频录像开始"); } @Override public void onWebRtcAudioRecordStop() { - Log.i(MediaManager.class.getSimpleName(), "WebRTC本地音频录制结束"); + Log.i(MediaManager.class.getSimpleName(), "WebRTC本地音频录像结束"); } }) // .setUseHardwareNoiseSuppressor(true) @@ -942,7 +942,7 @@ public final class MediaManager { } /** - * 屏幕录制回调 + * 屏幕录像回调 * * @author acgist */ diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/MixerProcesser.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/MixerProcesser.java index 2d7fa95..b9ca4b0 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/MixerProcesser.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/audio/MixerProcesser.java @@ -33,7 +33,7 @@ public class MixerProcesser extends Thread implements JavaAudioDeviceModule.Samp /** * 音频数据来源 - * 其实可以不用切换可以两个同时录制,但是有点浪费资源。 + * 其实可以不用切换可以两个同时录像,但是有点浪费资源。 * * @author acgist */ diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java index 3ddf798..29c6313 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/client/RecordClient.java @@ -45,19 +45,19 @@ public class RecordClient extends Client implements VideoSink { private static final long WAIT_TIME_US = WAIT_TIME_MS * 1000; /** - * 音频录制准备完成 + * 音频录像准备完成 */ private volatile boolean audioActive; /** - * 视频录制准备完成 + * 视频录像准备完成 */ private volatile boolean videoActive; /** - * 录制文件名称 + * 录像文件名称 */ private final String filename; /** - * 录制文件路径 + * 录像文件路径 */ private final String filepath; /** @@ -123,8 +123,8 @@ public class RecordClient extends Client implements VideoSink { */ private Handler videoHandler; /** - * 是否已经开始录制 - * 不能使用多线程wait/notify录制音频没有结束 + * 是否已经开始录像 + * 不能使用多线程wait/notify录像音频没有结束 */ private boolean muxerActive = false; /** @@ -180,16 +180,16 @@ public class RecordClient extends Client implements VideoSink { } /** - * 开始录制 + * 开始录像 * - * @return 录制文件路径 + * @return 录像文件路径 */ public String start() { synchronized (this) { if(this.init) { return this.filepath; } - Log.i(RecordClient.class.getSimpleName(), "录制视频文件:" + this.filepath); + Log.i(RecordClient.class.getSimpleName(), "录像视频文件:" + this.filepath); super.init(); this.mediaManager.newClient(); this.initMediaMuxer(); @@ -235,7 +235,7 @@ public class RecordClient extends Client implements VideoSink { this.audioCodec = MediaCodec.createEncoderByType(audioType); this.audioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); } catch (Exception e) { - Log.e(RecordClient.class.getSimpleName(), "加载音频录制线程异常", e); + Log.e(RecordClient.class.getSimpleName(), "加载音频录像线程异常", e); } this.audioThread = new HandlerThread("AudioRecordThread"); this.audioThread.start(); @@ -260,9 +260,9 @@ public class RecordClient extends Client implements VideoSink { } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { synchronized (this) { trackIndex = this.mediaMuxer.addTrack(this.audioCodec.getOutputFormat()); - Log.i(RecordClient.class.getSimpleName(), "开始录制音频:" + trackIndex); + Log.i(RecordClient.class.getSimpleName(), "开始录像音频:" + trackIndex); if (!this.close && this.videoActive) { - Log.i(RecordClient.class.getSimpleName(), "开始录制文件:" + this.filename); + Log.i(RecordClient.class.getSimpleName(), "开始录像文件:" + this.filename); this.mediaMuxer.start(); this.muxerActive = true; } else { @@ -283,7 +283,7 @@ public class RecordClient extends Client implements VideoSink { bufferInfo.presentationTimeUs -= pts; this.mediaMuxer.writeSampleData(trackIndex, outputBuffer, bufferInfo); this.audioCodec.releaseOutputBuffer(outputIndex, false); -// Log.d(RecordClient.class.getSimpleName(), "录制音频帧(时间戳):" + (bufferInfo.presentationTimeUs / 1_000_000F)); +// Log.d(RecordClient.class.getSimpleName(), "录像音频帧(时间戳):" + (bufferInfo.presentationTimeUs / 1_000_000F)); // if (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME == MediaCodec.BUFFER_FLAG_KEY_FRAME) { // } else if (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { // } else if (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM == MediaCodec.BUFFER_FLAG_END_OF_STREAM) { @@ -298,14 +298,14 @@ public class RecordClient extends Client implements VideoSink { } synchronized (this) { if (this.audioCodec != null && this.audioActive) { - Log.i(RecordClient.class.getSimpleName(), "结束录制音频:" + this.filename); + Log.i(RecordClient.class.getSimpleName(), "结束录像音频:" + this.filename); this.audioCodec.stop(); this.audioCodec.release(); this.audioCodec = null; } this.audioActive = false; if (this.mediaMuxer != null && !this.videoActive) { - Log.i(RecordClient.class.getSimpleName(), "结束录制文件:" + this.filename); + Log.i(RecordClient.class.getSimpleName(), "结束录像文件:" + this.filename); this.muxerActive = false; // this.mediaMuxer.stop(); this.mediaMuxer.release(); @@ -330,7 +330,7 @@ public class RecordClient extends Client implements VideoSink { this.videoCodec = MediaCodec.createEncoderByType(videoType); this.videoCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); } catch (Exception e) { - Log.e(RecordClient.class.getSimpleName(), "加载视频录制线程异常", e); + Log.e(RecordClient.class.getSimpleName(), "加载视频录像线程异常", e); } this.videoThread = new HandlerThread("VideoRecordThread"); this.videoThread.start(); @@ -355,9 +355,9 @@ public class RecordClient extends Client implements VideoSink { } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { synchronized (this) { trackIndex = this.mediaMuxer.addTrack(this.videoCodec.getOutputFormat()); - Log.i(RecordClient.class.getSimpleName(), "开始录制视频:" + trackIndex); + Log.i(RecordClient.class.getSimpleName(), "开始录像视频:" + trackIndex); if (!this.close && this.audioActive) { - Log.i(RecordClient.class.getSimpleName(), "开始录制文件:" + this.filename); + Log.i(RecordClient.class.getSimpleName(), "开始录像文件:" + this.filename); this.mediaMuxer.start(); this.muxerActive = true; } else { @@ -379,7 +379,7 @@ public class RecordClient extends Client implements VideoSink { bufferInfo.presentationTimeUs -= pts; this.mediaMuxer.writeSampleData(trackIndex, outputBuffer, bufferInfo); this.videoCodec.releaseOutputBuffer(outputIndex, false); -// Log.d(RecordClient.class.getSimpleName(), "录制视频帧(时间戳):" + (bufferInfo.presentationTimeUs / 1_000_000F)); +// Log.d(RecordClient.class.getSimpleName(), "录像视频帧(时间戳):" + (bufferInfo.presentationTimeUs / 1_000_000F)); // if (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME == MediaCodec.BUFFER_FLAG_KEY_FRAME) { // } else if (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { // } else if (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM == MediaCodec.BUFFER_FLAG_END_OF_STREAM) { @@ -394,14 +394,14 @@ public class RecordClient extends Client implements VideoSink { } synchronized (this) { if (this.videoCodec != null && this.videoActive) { - Log.i(RecordClient.class.getSimpleName(), "结束录制视频:" + this.filename); + Log.i(RecordClient.class.getSimpleName(), "结束录像视频:" + this.filename); this.videoCodec.stop(); this.videoCodec.release(); this.videoCodec = null; } this.videoActive = false; if (this.mediaMuxer != null && !this.audioActive) { - Log.i(RecordClient.class.getSimpleName(), "结束录制文件:" + this.filename); + Log.i(RecordClient.class.getSimpleName(), "结束录像文件:" + this.filename); this.muxerActive = false; // this.mediaMuxer.stop(); this.mediaMuxer.release(); @@ -411,7 +411,7 @@ public class RecordClient extends Client implements VideoSink { } /** - * 加载录制来源 + * 加载录像来源 * * @param videoSource 视频来源 * @param javaAudioDeviceModule 音频设备模块 @@ -440,7 +440,7 @@ public class RecordClient extends Client implements VideoSink { return; } super.close(); - Log.i(RecordClient.class.getSimpleName(), "结束录制:" + this.filepath); + Log.i(RecordClient.class.getSimpleName(), "结束录像:" + this.filepath); if(this.javaAudioDeviceModule != null) { this.javaAudioDeviceModule.removeMixerProcesser(); this.javaAudioDeviceModule = null; @@ -464,7 +464,7 @@ public class RecordClient extends Client implements VideoSink { } final File file = new File(this.filepath); if(file.length() <= 0) { - Log.i(RecordClient.class.getSimpleName(), "删除没有录制数据文件:" + this.filepath); + Log.i(RecordClient.class.getSimpleName(), "删除没有录像数据文件:" + this.filepath); file.delete(); } this.mediaManager.closeClient(); diff --git a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/Config.java b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/Config.java index 6cc4fc0..fd4e436 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/Config.java +++ b/taoyao-client-android/taoyao/media/src/main/java/com/acgist/taoyao/media/config/Config.java @@ -12,7 +12,7 @@ public class Config { */ public static final int WHAT_SCREEN_CAPTURE = 1000; /** - * 视频录制 + * 视频录像 */ public static final int WHAT_RECORD = 1001; /** diff --git a/taoyao-client-android/taoyao/media/src/main/java/org/webrtc/audio/JavaAudioDeviceModule.java b/taoyao-client-android/taoyao/media/src/main/java/org/webrtc/audio/JavaAudioDeviceModule.java index a165268..571b1a9 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/org/webrtc/audio/JavaAudioDeviceModule.java +++ b/taoyao-client-android/taoyao/media/src/main/java/org/webrtc/audio/JavaAudioDeviceModule.java @@ -318,11 +318,11 @@ public class JavaAudioDeviceModule implements AudioDeviceModule { /** Called when new audio samples are ready. This should only be set for debug purposes */ public static interface SamplesReadyCallback { /** - * 本地录制 + * 本地录像 */ void startNative(); /** - * 远程录制 + * 远程录像 */ void startWebRTC(); /** diff --git a/taoyao-client-android/taoyao/media/src/main/java/org/webrtc/audio/WebRtcAudioRecord.java b/taoyao-client-android/taoyao/media/src/main/java/org/webrtc/audio/WebRtcAudioRecord.java index 36aefcb..1adf0af 100644 --- a/taoyao-client-android/taoyao/media/src/main/java/org/webrtc/audio/WebRtcAudioRecord.java +++ b/taoyao-client-android/taoyao/media/src/main/java/org/webrtc/audio/WebRtcAudioRecord.java @@ -117,12 +117,12 @@ class WebRtcAudioRecord { * @Taoyao */ public void setMixerProcesser(SamplesReadyCallback samplesReadyCallback) { - // 不用处理这个逻辑设置为空表示关闭录制 + // 不用处理这个逻辑设置为空表示关闭录像 // if(this.audioSamplesReadyCallback != null && samplesReadyCallback == null) { // this.audioSamplesReadyCallback.startNative(); // } this.audioSamplesReadyCallback = samplesReadyCallback; - // 下面逻辑最好加锁防止关闭录制导致异常 + // 下面逻辑最好加锁防止关闭录像导致异常 if(this.audioSamplesReadyCallback != null) { if(this.audioThread == null) { this.audioSamplesReadyCallback.startNative(); diff --git a/taoyao-client-media/README.md b/taoyao-client-media/README.md index db8efc5..981c137 100644 --- a/taoyao-client-media/README.md +++ b/taoyao-client-media/README.md @@ -25,7 +25,7 @@ make -C worker ## 录像功能 -录像直接将媒体流转发给`ffmpeg`实现录制,没有直接修改`mediasoup`代码,代码侵入较低更加方便升级。 +录像直接将媒体流转发给`ffmpeg`实现录像,没有直接修改`mediasoup`代码,代码侵入较低更加方便升级。 ## 节点配置 diff --git a/taoyao-client-media/src/Taoyao.js b/taoyao-client-media/src/Taoyao.js index 245a7fd..45e4d32 100644 --- a/taoyao-client-media/src/Taoyao.js +++ b/taoyao-client-media/src/Taoyao.js @@ -455,6 +455,9 @@ class Taoyao { case "media::producer::resume": me.mediaProducerResume(message, body); break; + case "media::record": + me.mediaRecord(message, body); + break; case "media::router::rtp::capabilities": me.mediaRouterRtpCapabilities(message, body); break; @@ -767,6 +770,70 @@ class Taoyao { } } + /** + * 媒体录像 + * + * @param {*} message 消息 + * @param {*} body 消息主体 + */ + async mediaRecord(message, body) { + const me = this; + const { roomId, rtcpMux, comedia, clientId, host, audioPort, videoProt, rtpCapabilities, audioProducerId, audioStreamId, videoProducerId, videoStreamId } = body; + const plainTransportOptions = { + ...config.mediasoup.plainTransportOptions, + rtcpMux: rtcpMux, + comedia: comedia + }; + const room = this.rooms.get(roomId); + const transport = await room.mediasoupRouter.createPlainTransport(plainTransportOptions); + me.transportEvent("plain", roomId, transport); + transport.clientId = clientId; + room.transports.set(transport.id, transport); + let audioConsumerId; + let videoConsumerId; + await transport.connect({ + ip: '127.0.0.1', + port: remoteRtpPort, + rtcpPort: remoteRtcpPort + }); + if(audioProducerId) { + const audioConsumer = await transport.consume({ + producerId: audioProducerId, + rtpCapabilities, + paused: true + }); + audioConsumerId = audioConsumer.id; + await audioConsumer.resume(); + audioConsumer.clientId = clientId; + audioConsumer.streamId = videoStreamId; + room.consumers.set(audioConsumer.id, audioConsumer); + } + if(videoProducerId) { + const videoConsumer = await transport.consume({ + producerId: videoProducerId, + rtpCapabilities, + paused: true + }); + videoConsumerId = videoConsumer.id; + await videoConsumer.resume(); + videoConsumer.clientId = clientId; + videoConsumer.streamId = videoStreamId; + room.consumers.set(videoConsumer.id, videoConsumer); + } + console.info(transport.tuple) + console.info(transport.rtcpTuple) + message.body = { + ip : transport.tuple.localIp, + port : transport.tuple.localPort, + roomId : roomId, + rtcpPort : transport.rtcpTuple ? transport.rtcpTuple.localPort : undefined, + transportId : transport.id, + audioConsumerId : audioConsumerId, + videoConsumerId : videoConsumerId, + }; + me.push(message); + } + async mediaConsume(message, body) { const { roomId, diff --git a/taoyao-signal-server/README.md b/taoyao-signal-server/README.md index b011e1f..71d290b 100644 --- a/taoyao-signal-server/README.md +++ b/taoyao-signal-server/README.md @@ -16,3 +16,30 @@ ## STUN/TURN 视频房间不用`STUN/TURN`服务,视频会话需要自己搭建`coturn`服务。 + +## SDP格式 + +``` +v= (protocol version) #协议版本 +o= (owner/creator and session identifier) #所有者创建者和会话标识符 +s= (session name) #会话名称 +i=* (session information) #会话信息 +u=* (URI of description) #URI描述 +e=* (email address) #Email地址 +p=* (phone number) #电话号码 +c=* (connection information - not required if included in all media) #连接信息(如果包含在所有媒体中则不需要该字段) +b=* (zero or more bandwidth information lines) #带宽信息 +z=* (time zone adjustments) #时区调整 +k=* (encryption key) #加密密钥 +a=* (zero or more session attribute lines) #0或多次会话属性 +Time description #时间描述 +t= (time the session is active) #会话活动时间 +r=* (zero or more repeat times) #0或多次重复次数 +​Media description #媒体描述 +m= (media name and transport address) #媒体名称和传输地址 +i=* (media title) #媒体标题 +c=* (connection information - optional if included at session-level) #连接信息(如果包含在会话层则该字段可选) +b=* (zero or more bandwidth information lines) #带宽信息 +k=* (encryption key) #加密密钥 +a=* (zero or more media attribute lines) #0或多个会话属性 +```` diff --git a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/FfmpegProperties.java b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/FfmpegProperties.java new file mode 100644 index 0000000..0a776f6 --- /dev/null +++ b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/config/FfmpegProperties.java @@ -0,0 +1,39 @@ +package com.acgist.taoyao.boot.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * FFmpeg配置 + * + * @author acgist + */ +@Getter +@Setter +@Schema(title = "FFmpeg配置", description = "FFmpeg配置") +@ConfigurationProperties(prefix = "taoyao.ffmpeg") +public class FfmpegProperties { + + @Schema(title = "SDP模板", description = "SDP模板") + private String sdp; + @Schema(title = "媒体录像", description = "媒体录像") + private String record; + @Schema(title = "预览截图", description = "预览截图") + private String preview; + @Schema(title = "视频时长", description = "视频时长") + private String duration; + @Schema(title = "存储目录", description = "存储目录") + private String storagePath; + @Schema(title = "图片存储目录", description = "图片存储目录") + private String storageImagePath; + @Schema(title = "视频存储目录", description = "视频存储目录") + private String storageVideoPath; + @Schema(title = "录像最小端口", description = "录像最小端口") + private Integer minPort; + @Schema(title = "录像最大端口", description = "录像最大端口") + private Integer maxPort; + +} diff --git a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/configuration/BootAutoConfiguration.java b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/configuration/BootAutoConfiguration.java index 4915ce9..df13bbe 100644 --- a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/configuration/BootAutoConfiguration.java +++ b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/configuration/BootAutoConfiguration.java @@ -43,6 +43,7 @@ import org.springframework.web.context.request.async.AsyncRequestTimeoutExceptio import org.springframework.web.multipart.support.MissingServletRequestPartException; import org.springframework.web.servlet.NoHandlerFoundException; +import com.acgist.taoyao.boot.config.FfmpegProperties; import com.acgist.taoyao.boot.config.IdProperties; import com.acgist.taoyao.boot.config.IpRewriteProperties; import com.acgist.taoyao.boot.config.MediaProperties; @@ -86,6 +87,7 @@ import lombok.extern.slf4j.Slf4j; @EnableConfigurationProperties({ IdProperties.class, MediaProperties.class, + FfmpegProperties.class, ScriptProperties.class, SocketProperties.class, TaoyaoProperties.class, @@ -94,11 +96,13 @@ import lombok.extern.slf4j.Slf4j; IpRewriteProperties.class }) public class BootAutoConfiguration { - + + private final FfmpegProperties ffmpegProperties; private final TaoyaoProperties taoyaoProperties; private final ApplicationContext applicationContext; - public BootAutoConfiguration(TaoyaoProperties taoyaoProperties, ApplicationContext applicationContext) { + public BootAutoConfiguration(FfmpegProperties ffmpegProperties, TaoyaoProperties taoyaoProperties, ApplicationContext applicationContext) { + this.ffmpegProperties = ffmpegProperties; this.taoyaoProperties = taoyaoProperties; this.applicationContext = applicationContext; } @@ -188,6 +192,8 @@ public class BootAutoConfiguration { this.applicationContext.getBeansOfType(TaskScheduler.class).forEach((k, v) -> { log.info("系统定时任务线程池:{} - {}", k, v); }); + FileUtils.mkdirs(this.ffmpegProperties.getStorageImagePath()); + FileUtils.mkdirs(this.ffmpegProperties.getStorageVideoPath()); } /** diff --git a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/utils/FileUtils.java b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/utils/FileUtils.java index a241ec5..7b6f52f 100644 --- a/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/utils/FileUtils.java +++ b/taoyao-signal-server/taoyao-boot/src/main/java/com/acgist/taoyao/boot/utils/FileUtils.java @@ -1,5 +1,6 @@ package com.acgist.taoyao.boot.utils; +import java.io.File; import java.math.BigDecimal; import java.math.RoundingMode; @@ -55,4 +56,24 @@ public final class FileUtils { return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_EVEN) + UNITS[index]; } + /** + * @return 是否Linux平台 + */ + public static final boolean linux() { + return File.separatorChar == '/'; + } + + /** + * 创建目录 + * + * @param path + */ + public static final void mkdirs(String path) { + final File file = new File(path); + if(file.exists()) { + return; + } + file.mkdirs(); + } + } diff --git a/taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/ConfigController.java b/taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/ConfigController.java index 05794ef..6887084 100644 --- a/taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/ConfigController.java +++ b/taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/ConfigController.java @@ -4,11 +4,11 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.acgist.taoyao.boot.config.FfmpegProperties; import com.acgist.taoyao.boot.config.MediaProperties; import com.acgist.taoyao.boot.config.SocketProperties; import com.acgist.taoyao.boot.config.WebrtcProperties; import com.acgist.taoyao.boot.model.Message; -import com.acgist.taoyao.signal.config.camera.CameraProperties; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -29,7 +29,7 @@ import lombok.RequiredArgsConstructor; public class ConfigController { private final MediaProperties mediaProperties; - private final CameraProperties cameraProperties; + private final FfmpegProperties ffmpegProperties; private final SocketProperties socketProperties; private final WebrtcProperties webrtcProperties; @@ -40,11 +40,11 @@ public class ConfigController { return Message.success(this.mediaProperties); } - @Operation(summary = "摄像头配置", description = "摄像头配置") - @GetMapping("/camera") - @ApiResponse(content = @Content(schema = @Schema(implementation = CameraProperties.class))) - private Message camera() { - return Message.success(this.cameraProperties); + @Operation(summary = "FFmpeg配置", description = "FFmpeg配置") + @GetMapping("/ffmpeg") + @ApiResponse(content = @Content(schema = @Schema(implementation = FfmpegProperties.class))) + public Message ffmpeg() { + return Message.success(this.ffmpegProperties); } @Operation(summary = "Socket配置", description = "Socket配置") diff --git a/taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/ControlController.java b/taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/ControlController.java index fc06029..595b65a 100644 --- a/taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/ControlController.java +++ b/taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/ControlController.java @@ -9,19 +9,11 @@ import org.springframework.web.bind.annotation.RestController; import com.acgist.taoyao.boot.config.MediaAudioProperties; import com.acgist.taoyao.boot.config.MediaVideoProperties; import com.acgist.taoyao.boot.model.Message; -import com.acgist.taoyao.signal.config.camera.AiProperties; -import com.acgist.taoyao.signal.config.camera.BeautyProperties; -import com.acgist.taoyao.signal.config.camera.WatermarkProperties; -import com.acgist.taoyao.signal.model.control.PtzModel; -import com.acgist.taoyao.signal.protocol.control.ControlAiProtocol; -import com.acgist.taoyao.signal.protocol.control.ControlBeautyProtocol; import com.acgist.taoyao.signal.protocol.control.ControlBellProtocol; import com.acgist.taoyao.signal.protocol.control.ControlConfigAudioProtocol; import com.acgist.taoyao.signal.protocol.control.ControlConfigVideoProtocol; import com.acgist.taoyao.signal.protocol.control.ControlPhotographProtocol; -import com.acgist.taoyao.signal.protocol.control.ControlPtzProtocol; import com.acgist.taoyao.signal.protocol.control.ControlRecordProtocol; -import com.acgist.taoyao.signal.protocol.control.ControlWatermarkProtocol; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -41,68 +33,40 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class ControlController { - private final ControlAiProtocol controlAiProtocol; - private final ControlPtzProtocol controlPtzProtocol; private final ControlBellProtocol controlBellProtocol; - private final ControlBeautyProtocol controlBeautyProtocol; private final ControlRecordProtocol controlRecordProtocol; - private final ControlWatermarkProtocol controlWatermarkProtocol; private final ControlPhotographProtocol controlPhotographProtocol; private final ControlConfigAudioProtocol controlConfigAudioProtocol; private final ControlConfigVideoProtocol controlConfigVideoProtocol; - @Operation(summary = "AI识别", description = "AI识别控制") - @GetMapping("/ai/{clientId}") - public Message ai(@PathVariable String clientId, @Valid AiProperties aiProperties) { - return Message.success(this.controlAiProtocol.execute(clientId, aiProperties)); - } - - @Operation(summary = "PTZ", description = "PTZ控制") - @GetMapping("/ptz/{clientId}") - public Message ptz(@PathVariable String clientId, @Valid PtzModel ptzModel) { - return Message.success(this.controlPtzProtocol.execute(clientId, ptzModel)); - } - @Operation(summary = "响铃", description = "响铃控制") @GetMapping("/bell/{clientId}") public Message bell(@PathVariable String clientId, @NotNull(message = "没有指定操作状态") Boolean enabled) { - return Message.success(this.controlBellProtocol.execute(clientId, enabled)); - } - - @Operation(summary = "美颜", description = "美颜控制") - @GetMapping("/beauty/{clientId}") - public Message beauty(@PathVariable String clientId, @Valid BeautyProperties beautyProperties) { - return Message.success(this.controlBeautyProtocol.execute(clientId, beautyProperties)); + return this.controlBellProtocol.execute(clientId, enabled); } @Operation(summary = "录像", description = "录像控制") @GetMapping("/record/{clientId}") public Message record(@PathVariable String clientId, @NotNull(message = "没有指定操作状态") Boolean enabled) { - return Message.success(this.controlRecordProtocol.execute(clientId, enabled)); - } - - @Operation(summary = "水印", description = "水印控制") - @GetMapping("/watermark/{clientId}") - public Message watermark(@PathVariable String clientId, @Valid WatermarkProperties watermarkProperties) { - return Message.success(this.controlWatermarkProtocol.execute(clientId, watermarkProperties)); + return this.controlRecordProtocol.execute(clientId, enabled); } @Operation(summary = "拍照", description = "拍照控制") @GetMapping("/photograph/{clientId}") public Message photograph(@PathVariable String clientId) { - return Message.success(this.controlPhotographProtocol.execute(clientId)); + return this.controlPhotographProtocol.execute(clientId); } @Operation(summary = "配置音频", description = "配置音频") @GetMapping("/config/audio/{clientId}") public Message configAudio(@PathVariable String clientId, @Valid MediaAudioProperties mediaAudioProperties) { - return Message.success(this.controlConfigAudioProtocol.execute(clientId, mediaAudioProperties)); + return this.controlConfigAudioProtocol.execute(clientId, mediaAudioProperties); } @Operation(summary = "配置视频", description = "配置视频") @GetMapping("/config/video/{clientId}") public Message configVideo(@PathVariable String clientId, @Valid MediaVideoProperties mediaVideoProperties) { - return Message.success(this.controlConfigVideoProtocol.execute(clientId, mediaVideoProperties)); + return this.controlConfigVideoProtocol.execute(clientId, mediaVideoProperties); } } diff --git a/taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/MediaController.java b/taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/MediaController.java new file mode 100644 index 0000000..e6f9a28 --- /dev/null +++ b/taoyao-signal-server/taoyao-server/src/main/java/com/acgist/taoyao/controller/MediaController.java @@ -0,0 +1,37 @@ +package com.acgist.taoyao.controller; + +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.acgist.taoyao.boot.model.Message; +import com.acgist.taoyao.signal.protocol.media.MediaRecordProtocol; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; + +/** + * 媒体 + * + * @author acgist + */ +@Tag(name = "媒体", description = "媒体管理") +@Validated +@RestController +@RequestMapping("/media") +@RequiredArgsConstructor +public class MediaController { + + private final MediaRecordProtocol mediaRecordProtocol; + + @Operation(summary = "录像", description = "媒体录像") + @GetMapping("/record/{roomId}/{clientId}") + public Message record(@PathVariable String roomId, @PathVariable String clientId, @NotNull(message = "没有指定操作状态") Boolean enabled) { + return this.mediaRecordProtocol.execute(roomId, clientId, enabled); + } + +} diff --git a/taoyao-signal-server/taoyao-server/src/main/resources/application.yml b/taoyao-signal-server/taoyao-server/src/main/resources/application.yml index 1004e1d..d66f6d2 100644 --- a/taoyao-signal-server/taoyao-server/src/main/resources/application.yml +++ b/taoyao-signal-server/taoyao-server/src/main/resources/application.yml @@ -67,7 +67,7 @@ taoyao: max-client-index: 99999 # 媒体配置 media: - # =============== 视频配置 =============== + # 视频配置 # 宽度 min-width: 720 max-width: 4096 @@ -80,7 +80,7 @@ taoyao: # 视频码率 min-video-bitrate: 800 max-video-bitrate: 1600 - # =============== 音频配置 =============== + # 音频配置 # 采样位数 min-sample-size: 8 max-sample-size: 32 @@ -196,35 +196,6 @@ taoyao: # port: 3478 # username: taoyao # password: taoyao - # 摄像头配置 - camera: - # 混音 - audio-mixer: false - # 变声 - audio-changer: false - # 降噪 - audio-denoise: true - # 存储目录 - storage-path: /data/taoyao/storage - # 图片存储目录 - storage-image-path: /data/taoyao/storage/image - # 视频存储目录 - storage-video-path: /data/taoyao/storage/video - # AI识别 - ai: - enabled: false - type: PERSON - # 美颜 - beauty: - enabled: false - level: 10 - # 水印 - watermark: - enabled: false - text: taoyao - posx: 10 - posy: 10 - opacity: 0.8 # 安全配置 security: enabled: true @@ -253,6 +224,36 @@ taoyao: - network: 192.168.8.0 inner-host: outer-host: + ffmpeg: + # SDP:VP8 | H264 + sdp: | + v=0 + o=- 0 0 IN IP4 127.0.0.1 + s=TaoyaoRecord + t=0 0 + m=audio %d RTP/AVP 97 + c=IN IP4 127.0.0.1 + a=rtpmap:97 opus/48000/2 + a=fmtp:97 sprop-stereo=1 + m=video %d RTP/AVP 96 + c=IN IP4 127.0.0.1 + a=rtpmap:96 H264/90000 + a=fmtp:96 packetization-mode=1 + # 录像命令 + record: ffmpeg -y -protocol_whitelist "file,rtp,udp" -i %s %s + # 截图命令 + preview: ffmpeg -y -i %s -t %d -f image2 %s + # 时长命令 + duration: ffprobe -i %s -show_entries format=duration + # 存储目录 + storage-path: /data/taoyao/storage + # 图片存储目录 + storage-image-path: /data/taoyao/storage/image + # 视频存储目录 + storage-video-path: /data/taoyao/storage/video + # 端口范围 + min-port: 50000 + max-port: 59999 # 脚本配置 script: enabled: true diff --git a/taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/RecorderTest.java b/taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/RecorderTest.java new file mode 100644 index 0000000..7f7f732 --- /dev/null +++ b/taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/RecorderTest.java @@ -0,0 +1,36 @@ +package com.acgist.taoyao; + +import org.junit.jupiter.api.Test; + +import com.acgist.taoyao.boot.config.FfmpegProperties; +import com.acgist.taoyao.signal.party.media.Recorder; + +public class RecorderTest { + + @Test + public void testStart() throws InterruptedException { + final FfmpegProperties ffmpegProperties = new FfmpegProperties(); + ffmpegProperties.setStorageVideoPath("D:\\tmp\\video"); + ffmpegProperties.setSdp(""" + v=0 + o=- 0 0 IN IP4 127.0.0.1 + s=TaoyaoRecord + t=0 0 + m=audio %d RTP/AVP 97 + c=IN IP4 127.0.0.1 + a=rtpmap:97 opus/48000/2 + a=fmtp:97 sprop-stereo=1 + m=video %d RTP/AVP 96 + c=IN IP4 127.0.0.1 + a=rtpmap:96 H264/90000 + a=fmtp:96 packetization-mode=1 + """); + ffmpegProperties.setRecord("ffmpeg -y -protocol_whitelist \"file,rtp,udp\" -i %s %s"); + final Recorder recorder = new Recorder(ffmpegProperties); + recorder.start(); + Thread.sleep(20 * 1000); + recorder.stop(); + Thread.sleep(20 * 1000); + } + +} diff --git a/taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/rtp/RtpTest.java b/taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/rtp/RtpTest.java index 1b2544e..83231d9 100644 --- a/taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/rtp/RtpTest.java +++ b/taoyao-signal-server/taoyao-server/src/test/java/com/acgist/taoyao/rtp/RtpTest.java @@ -82,20 +82,13 @@ public class RtpTest { // {"header":{"v":"1.0.0","id":1215310510002009,"signal":"room::enter"},"body":{"roomId":"8260e615-3081-4bfc-96a8-574f4dd780d9"}} // {"header":{"v":"1.0.0","id":1215310510002010,"signal":"media::transport::plain"},"body":{"roomId":"8260e615-3081-4bfc-96a8-574f4dd780d9","rtcpMux":false,"comedia":true}} // {"header":{"v":"1.0.0","id":1215375110006012,"signal":"media::produce"},"body":{"kind":"video","roomId":"8260e615-3081-4bfc-96a8-574f4dd780d9","transportId":"14dc9307-bf9c-4442-a9ad-ce6a97623ef4","appData":{},"rtpParameters":{"codecs":[{"mimeType":"video/vp8","clockRate":90000,"payloadType":102,"rtcpFeedback":[]}],"encodings":[{"ssrc":123123}]}}} - // ffmpeg直接播放 -// ffmpeg -re -i video.mp4 -c:a copy -vn -map 0:1 -f rtp rtp://localhost:6666 > taoyao.sdp -// ffmpeg -re -i video.mp4 -c:v copy -an -map 0:0 -f rtp rtp://localhost:6666 > taoyao.sdp -// ffplay -protocol_whitelist "file,udp,rtp" taoyao.sdp // ffmpeg不支持rtcpMux -// ffmpeg -re -i video.mp4 -c:v libvpx -map 0:0 -f tee "[select=v:f=rtp:ssrc=123123:payload_type=102]rtp://192.168.1.110:40793?rtcpport=47218" // ffmpeg -re -i video.mp4 -c:v vp8 -map 0:0 -f tee "[select=v:f=rtp:ssrc=123123:payload_type=102]rtp://192.168.1.110:40793?rtcpport=47218" +// ffmpeg -re -i video.mp4 -c:v libvpx -map 0:0 -f tee "[select=v:f=rtp:ssrc=123123:payload_type=102]rtp://192.168.1.110:40793?rtcpport=47218" // 音频视频同时传输 -// ffmpeg -re -i video.mp4 -c:v copy -an -f rtp rtp://192.168.8.122:6666 -c:a copy -vn -f rtp rtp://192.168.8.122:7777 -sdp_file taoyao.sdp +// ffmpeg -re -i video.mp4 -c:a libopus -vn -f rtp rtp://192.168.1.110:8888 -c:v libx264 -an -f rtp rtp://192.168.1.110:9999 -sdp_file taoyao.sdp // ffplay -protocol_whitelist "file,rtp,udp" -i taoyao.sdp // ffmpeg -protocol_whitelist "file,rtp,udp" -i taoyao.sdp taoyao.mp4 - // 混音 -// ffmpeg -re -i video.mp4 -i music.mp3 -map 0 -vn -c:a copy -f rtp rtp://192.168.8.122:6666 -map 0 -an -c:v copy -f rtp rtp://192.168.8.122:7777 -map 1 -c:a aac -f rtp rtp://192.168.8.122:8888 -sdp_file taoyao.sdp -// ffmpeg -protocol_whitelist "file,rtp,udp" -i taoyao.sdp -filter_complex "[0:0]aresample=8000[a0];[0:2]aresample=8000[a2];[a0][a2]amix=inputs=2" taoyao.mp4 final Scanner scanner = new Scanner(System.in); do { if(StringUtils.isEmpty(line)) { diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/AiProperties.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/AiProperties.java deleted file mode 100644 index 76b635a..0000000 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/AiProperties.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.acgist.taoyao.signal.config.camera; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; - -/** - * AI识别配置 - * - * @author acgist - */ -@Getter -@Setter -@Schema(title = "AI识别配置", description = "AI识别配置") -public class AiProperties { - - /** - * 识别类型 - * - * @author acgist - */ - public enum Type { - - // 人 - PERSON; - - } - - @Schema(title = "是否开启", description = "是否开启") - @NotNull(message = "没有指定操作状态") - private Boolean enabled; - @Schema(title = "识别类型", description = "识别类型") - private Type type; - -} diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/BeautyProperties.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/BeautyProperties.java deleted file mode 100644 index 99cc00c..0000000 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/BeautyProperties.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.acgist.taoyao.signal.config.camera; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; - -/** - * 美颜配置 - * - * @author acgist - */ -@Getter -@Setter -@Schema(title = "美颜配置", description = "美颜配置") -public class BeautyProperties { - - @Schema(title = "是否开启", description = "是否开启") - @NotNull(message = "没有指定操作状态") - private Boolean enabled; - @Schema(title = "美颜级别", description = "美颜级别") - private Integer level; - -} diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/CameraProperties.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/CameraProperties.java deleted file mode 100644 index 4c9c094..0000000 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/CameraProperties.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.acgist.taoyao.signal.config.camera; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.Setter; - -/** - * 摄像头配置 - * - * 音频: - * 混音:不用混音 - * 变声: - * 降噪: - * - * 视频: - * 录制: - * 水印: - * 美颜: - * AI识别: - * - * @author acgist - */ -@Getter -@Setter -@Schema(title = "摄像头配置", description = "摄像头配置") -@ConfigurationProperties(prefix = "taoyao.camera") -public class CameraProperties { - - @Schema(title = "混音", description = "混音") - private Boolean audioMixer = Boolean.FALSE; - @Schema(title = "变声", description = "变声") - private Boolean audioChanger; - @Schema(title = "降噪", description = "降噪") - private Boolean audioDenoise; - @Schema(title = "存储目录", description = "存储目录") - private String storagePath; - @Schema(title = "图片存储目录", description = "图片存储目录") - private String storageImagePath; - @Schema(title = "视频存储目录", description = "视频存储目录") - private String storageVideoPath; - @Schema(title = "AI识别配置", description = "AI识别配置") - private AiProperties ai; - @Schema(title = "美颜配置", description = "美颜配置") - private BeautyProperties beauty; - @Schema(title = "水印配置", description = "水印配置") - private WatermarkProperties watermark; - -} diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/WatermarkProperties.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/WatermarkProperties.java deleted file mode 100644 index 6d0e7fe..0000000 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/config/camera/WatermarkProperties.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.acgist.taoyao.signal.config.camera; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; - -/** - * 水印配置 - * - * @author acgist - */ -@Getter -@Setter -@Schema(title = "水印配置", description = "水印配置") -public class WatermarkProperties { - - @Schema(title = "是否开启", description = "是否开启") - @NotNull(message = "没有指定操作状态") - private Boolean enabled; - @Schema(title = "水印内容", description = "水印内容") - private String text; - @Schema(title = "X坐标", description = "X坐标") - private Integer posx; - @Schema(title = "Y坐标", description = "Y坐标") - private Integer posy; - @Schema(title = "透明度", description = "透明度") - private Double opacity; - -} diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/configuration/SignalAutoConfiguration.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/configuration/SignalAutoConfiguration.java index 74413a7..2e51f44 100644 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/configuration/SignalAutoConfiguration.java +++ b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/configuration/SignalAutoConfiguration.java @@ -2,12 +2,10 @@ package com.acgist.taoyao.signal.configuration; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import com.acgist.taoyao.boot.runner.OrderedCommandLineRunner; -import com.acgist.taoyao.signal.config.camera.CameraProperties; import com.acgist.taoyao.signal.event.EventPublisher; import com.acgist.taoyao.signal.protocol.ProtocolManager; @@ -21,9 +19,6 @@ import lombok.RequiredArgsConstructor; */ @AutoConfiguration @RequiredArgsConstructor -@EnableConfigurationProperties({ - CameraProperties.class -}) public class SignalAutoConfiguration { private final ApplicationContext applicationContext; diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/model/control/PtzModel.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/model/control/PtzModel.java deleted file mode 100644 index ac39b96..0000000 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/model/control/PtzModel.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.acgist.taoyao.signal.model.control; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; - -/** - * PTZ控制参数 - * - * @author acgist - */ -@Schema(title = "PTZ控制参数", description = "PTZ控制参数") -public class PtzModel { - - /** - * PTZ类型 - * - * @author acgist - */ - public enum Type { - - // 水平 - PAN, - // 垂直 - TILT, - // 变焦 - ZOOM; - - } - - @Schema(title = "PTZ类型", description = "PTZ类型") - @NotNull(message = "PTZ类型不能为空") - private Type type; - @Schema(title = "PTZ参数", description = "PTZ参数") - @NotNull(message = "PTZ参数不能为空") - private Double value; - -} diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/party/media/ClientWrapper.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/party/media/ClientWrapper.java index 2e78886..5e23b96 100644 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/party/media/ClientWrapper.java +++ b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/party/media/ClientWrapper.java @@ -50,6 +50,10 @@ public class ClientWrapper implements AutoCloseable { * SCTP协商 */ private Object sctpCapabilities; + /** + * 媒体录像 + */ + private Recorder recorder; /** * 发送通道 */ diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/party/media/Recorder.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/party/media/Recorder.java new file mode 100644 index 0000000..bbe90ce --- /dev/null +++ b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/party/media/Recorder.java @@ -0,0 +1,224 @@ +package com.acgist.taoyao.signal.party.media; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.UUID; + +import com.acgist.taoyao.boot.config.FfmpegProperties; +import com.acgist.taoyao.boot.utils.FileUtils; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +/** + * 媒体录像 + * + * @author acgist + */ +@Slf4j +@Getter +@Setter +public class Recorder { + + /** + * 是否关闭 + */ + private boolean close; + /** + * 是否正在运行 + */ + private boolean running; + /** + * 音频端口 + */ + private Integer audioPort; + /** + * 视频端口 + */ + private Integer videoPort; + /** + * 传输通道 + */ + private Transport transport; + /** + * 音频消费者 + */ + private Consumer audioConsumer; + /** + * 视频消费者 + */ + private Consumer videoConsumer; + /** + * 录像进程 + */ + private Process process; + /** + * 进程Builder + */ + private ProcessBuilder processBuilder; + /** + * 录制线程 + */ + private Thread thread; + /** + * 日志线程 + */ + private Thread inputThread; + /** + * 异常线程 + */ + private Thread errorThread; + /** + * 命令 + */ + private String command; + /** + * 文件路径 + */ + private final String folder; + /** + * SDP路径 + */ + private final String sdpfile; + /** + * 文件路径 + */ + private final String filepath; + /** + * FFmpeg配置 + */ + private final FfmpegProperties ffmpegProperties; + + public Recorder(FfmpegProperties ffmpegProperties) { + this.close = false; + this.running = false; + this.ffmpegProperties = ffmpegProperties; + final String id = UUID.randomUUID().toString(); + this.folder = Paths.get(ffmpegProperties.getStorageVideoPath(), id).toAbsolutePath().toString(); + this.sdpfile = Paths.get(this.folder, "taoyao.sdp").toAbsolutePath().toString(); + this.filepath = Paths.get(this.folder, "taoyao.mp4").toAbsolutePath().toString(); + this.command = String.format(this.ffmpegProperties.getRecord(), this.sdpfile, this.filepath); + FileUtils.mkdirs(this.folder); + } + + /** + * 开始录像 + */ + public void start() { + synchronized (this) { + if(this.running) { + return; + } + this.running = true; + this.thread = new Thread(this::record); + this.thread.setDaemon(true); + this.thread.setName("TaoyaoRecord"); + this.thread.start(); + } + } + + /** + * 录制视频 + */ + private void record() { + this.buildSdpfile(); + int status = 0; + final StringBuilder input = new StringBuilder(); + final StringBuilder error = new StringBuilder(); + try { + final boolean linux = FileUtils.linux(); + if(linux) { + this.processBuilder = new ProcessBuilder("/bin/bash", "-c", this.command); + this.process = processBuilder.start(); + } else { + this.processBuilder = new ProcessBuilder("cmd", "/c", this.command); + this.process = processBuilder.start(); + } + log.debug(""" + 开始录像:{} + 录像命令:{} + """, this.filepath, this.command); + this.inputThread = new Thread(() -> { + try (final InputStream inputStream = this.process.getInputStream()) { + int length; + final byte[] bytes = new byte[1024]; + while(this.running && !this.close && (length = inputStream.read(bytes)) >= 0) { + input.append(linux ? new String(bytes, 0, length) : new String(bytes, 0, length, "GBK")); + } + } catch (Exception e) { + log.error("读取录像日志异常", e); + } + }); + this.inputThread.setDaemon(true); + this.inputThread.setName("TaoyaoRecordInput"); + this.inputThread.start(); + this.errorThread = new Thread(() -> { + try (final InputStream inputStream = this.process.getErrorStream();) { + int length; + final byte[] bytes = new byte[1024]; + while(this.running && !this.close && (length = inputStream.read(bytes)) >= 0) { + error.append(linux ? new String(bytes, 0, length) : new String(bytes, 0, length, "GBK")); + } + } catch (Exception e) { + log.error("读取录像错误异常", e); + } + }); + this.errorThread.setDaemon(true); + this.errorThread.setName("TaoyaoRecordError"); + this.errorThread.start(); + status = this.process.waitFor(); + } catch (Exception e) { + log.error("录像异常:{}", this.command, e); + } finally { + this.stop(); + } + log.debug(""" + 结束录像:{} + 结束状态:{} + 录像日志:{} + 异常日志:{} + """, this.filepath, status, input, error); + } + + /** + * 创建SDP文件 + */ + private void buildSdpfile() { + try { + Files.write( + Paths.get(this.sdpfile), + String.format(this.ffmpegProperties.getSdp(), 8888, 9999).getBytes(), + StandardOpenOption.WRITE, StandardOpenOption.CREATE + ); + } catch (IOException e) { + log.error("创建SDP文件异常:{}", this.sdpfile, e); + } + } + + /** + * 结束录像 + */ + public void stop() { + synchronized (this) { + if(this.close) { + return; + } + this.close = true; + } + if(this.process == null) { + return; + } + log.debug("结束媒体录像:{}", this.filepath); + // 所有子进程 + this.process.children().forEach(process -> { + process.destroy(); + }); + // 当前父进程 + this.process.destroy(); + } + +} diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlAiProtocol.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlAiProtocol.java deleted file mode 100644 index 6c30a6a..0000000 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlAiProtocol.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.acgist.taoyao.signal.protocol.control; - -import java.util.Map; - -import com.acgist.taoyao.boot.annotation.Description; -import com.acgist.taoyao.boot.annotation.Protocol; -import com.acgist.taoyao.boot.model.Message; -import com.acgist.taoyao.signal.client.Client; -import com.acgist.taoyao.signal.client.ClientType; -import com.acgist.taoyao.signal.config.camera.AiProperties; -import com.acgist.taoyao.signal.protocol.ProtocolControlAdapter; - -/** - * 打开AI识别信令 - * - * @author acgist - */ -@Protocol -@Description( - body = """ - { - "to": "目标终端ID", - ...AiProperties - } - """, - flow = { - "信令服务->终端", - "终端=>信令服务->终端" - } -) -public class ControlAiProtocol extends ProtocolControlAdapter { - - public static final String SIGNAL = "control::ai"; - - public ControlAiProtocol() { - super("打开AI识别信令", SIGNAL); - } - - - @Override - public void execute(String clientId, ClientType clientType, Client client, Client targetClient, Message message, Map body) { - client.push(targetClient.request(message)); - } - - /** - * @param clientId 终端标识 - * @param aiProperties AI识别配置 - * - * @return 执行结果 - */ - public Message execute(String clientId, AiProperties aiProperties) { - return this.request(clientId, this.build(aiProperties)); - } - -} diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlBeautyProtocol.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlBeautyProtocol.java deleted file mode 100644 index 6f3e71d..0000000 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlBeautyProtocol.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.acgist.taoyao.signal.protocol.control; - -import java.util.Map; - -import com.acgist.taoyao.boot.annotation.Description; -import com.acgist.taoyao.boot.annotation.Protocol; -import com.acgist.taoyao.boot.model.Message; -import com.acgist.taoyao.signal.client.Client; -import com.acgist.taoyao.signal.client.ClientType; -import com.acgist.taoyao.signal.config.camera.BeautyProperties; -import com.acgist.taoyao.signal.protocol.ProtocolControlAdapter; - -/** - * 打开美颜信令 - * - * @author acgist - */ -@Protocol -@Description( - body = """ - { - "to": "目标终端ID", - ...BeautyProperties - } - """, - flow = { - "信令服务->终端", - "终端=>信令服务->终端" - } -) -public class ControlBeautyProtocol extends ProtocolControlAdapter { - - public static final String SIGNAL = "control::beauty"; - - public ControlBeautyProtocol() { - super("打开美颜信令", SIGNAL); - } - - @Override - public void execute(String clientId, ClientType clientType, Client client, Client targetClient, Message message, Map body) { - client.push(targetClient.request(message)); - } - - /** - * @param clientId 终端ID - * @param beautyProperties 美颜配置 - * - * @return 执行结果 - */ - public Message execute(String clientId, BeautyProperties beautyProperties) { - return this.request(clientId, this.build(beautyProperties)); - } - -} diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlPtzProtocol.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlPtzProtocol.java deleted file mode 100644 index 1b5d8fc..0000000 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlPtzProtocol.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.acgist.taoyao.signal.protocol.control; - -import java.util.Map; - -import com.acgist.taoyao.boot.annotation.Description; -import com.acgist.taoyao.boot.annotation.Protocol; -import com.acgist.taoyao.boot.model.Message; -import com.acgist.taoyao.signal.client.Client; -import com.acgist.taoyao.signal.client.ClientType; -import com.acgist.taoyao.signal.model.control.PtzModel; -import com.acgist.taoyao.signal.protocol.ProtocolControlAdapter; - -/** - * PTZ信令 - * - * @author acgist - */ -@Protocol -@Description( - body = """ - { - "to": "目标终端ID", - ...PtzControl - } - """, - flow = { - "信令服务->终端", - "终端->信令服务->终端" - } -) -public class ControlPtzProtocol extends ProtocolControlAdapter { - - public static final String SIGNAL = "control::ptz"; - - public ControlPtzProtocol() { - super("PTZ信令", SIGNAL); - } - - @Override - public void execute(String clientId, ClientType clientType, Client client, Client targetClient, Message message, Map body) { - client.push(targetClient.request(message)); - } - - /** - * @param clientId 终端标识 - * @param ptzModel PTZ控制参数 - * - * @return 执行结果 - */ - public Message execute(String clientId, PtzModel ptzModel) { - return this.request(clientId, this.build(ptzModel)); - } - -} diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlWatermarkProtocol.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlWatermarkProtocol.java deleted file mode 100644 index 1e1173b..0000000 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/control/ControlWatermarkProtocol.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.acgist.taoyao.signal.protocol.control; - -import java.util.Map; - -import com.acgist.taoyao.boot.annotation.Description; -import com.acgist.taoyao.boot.annotation.Protocol; -import com.acgist.taoyao.boot.model.Message; -import com.acgist.taoyao.signal.client.Client; -import com.acgist.taoyao.signal.client.ClientType; -import com.acgist.taoyao.signal.config.camera.WatermarkProperties; -import com.acgist.taoyao.signal.protocol.ProtocolControlAdapter; - -/** - * 配置水印信令 - * - * @author acgist - */ -@Protocol -@Description( - memo = "如果没有指定参数使用默认参数配置", - body = """ - { - "to": "目标终端ID", - ...WatermarkProperties - } - """, - flow = { - "信令服务->终端", - "终端=>信令服务->终端" - } -) -public class ControlWatermarkProtocol extends ProtocolControlAdapter { - - public static final String SIGNAL = "control::watermark"; - - public ControlWatermarkProtocol() { - super("配置水印信令", SIGNAL); - } - - @Override - public void execute(String clientId, ClientType clientType, Client client, Client targetClient, Message message, Map body) { - client.push(targetClient.request(message)); - } - - /** - * @param clientId 终端ID - * @param watermarkProperties 水印配置 - * - * @return 执行结果 - */ - public Message execute(String clientId, WatermarkProperties watermarkProperties) { - return this.request(clientId, this.build(watermarkProperties)); - } - -} diff --git a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaRecordProtocol.java b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaRecordProtocol.java index 2d9e12f..107e7cc 100644 --- a/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaRecordProtocol.java +++ b/taoyao-signal-server/taoyao-signal/src/main/java/com/acgist/taoyao/signal/protocol/media/MediaRecordProtocol.java @@ -1,11 +1,84 @@ package com.acgist.taoyao.signal.protocol.media; +import java.util.Map; + +import com.acgist.taoyao.boot.annotation.Description; +import com.acgist.taoyao.boot.annotation.Protocol; +import com.acgist.taoyao.boot.config.FfmpegProperties; +import com.acgist.taoyao.boot.model.Message; +import com.acgist.taoyao.signal.client.Client; +import com.acgist.taoyao.signal.client.ClientType; +import com.acgist.taoyao.signal.party.media.ClientWrapper; +import com.acgist.taoyao.signal.party.media.Recorder; +import com.acgist.taoyao.signal.party.media.Room; +import com.acgist.taoyao.signal.protocol.ProtocolRoomAdapter; + /** - * 媒体录制 - * 使用ffmpeg进行录制代码侵入较低 + * 媒体录像 * * @author acgist */ -public class MediaRecordProtocol { +@Protocol +@Description( + body = { + """ + { + "clientId": "目标终端ID", + "enabled": 是否录像(true|false) + } + """, + """ + { + "enabled": 是否录像(true|false), + "filepath": "视频文件路径" + } + """ + }, + flow = "终端=>信令服务->终端" +) +public class MediaRecordProtocol extends ProtocolRoomAdapter { + private final FfmpegProperties ffmpegProperties; + + public MediaRecordProtocol(FfmpegProperties ffmpegProperties) { + super("媒体录像", "media::record"); + this.ffmpegProperties = ffmpegProperties; + } + + @Override + public void execute(String clientId, ClientType clientType, Room room, Client client, Client mediaClient, Message message, Map body) { + + } + + /** + * @param roomId 房间ID + * @param clientId 终端ID + * @param enabled 状态 + * + * @return 执行结果 + */ + public Message execute(String roomId, String clientId, Boolean enabled) { + final Room room = this.roomManager.room(roomId); + final Client client = this.clientManager.clients(clientId); + if(enabled) { + this.record(room, client); + } + return null; + } + + /** + * 开始录制 + */ + private void record(Room room, Client client) { + final ClientWrapper clientWrapper = room.clientWrapper(client); + synchronized (clientWrapper) { + if(clientWrapper.getRecorder() != null) { + return; + } + final Recorder recorder = new Recorder(this.ffmpegProperties); + recorder.start(); + clientWrapper.setRecorder(recorder); + } + } + }