[*] 服务端录制
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
|功能|是否支持|是否实现|描述|
|
||||
|:--|:--|:--|:--|
|
||||
|P2P|支持|完成|视频会话(监控)|
|
||||
|Mediasoup|支持|完成|视频房间(会话)|
|
||||
|Mediasoup|支持|完成|视频房间(会议)|
|
||||
|控制|支持|完成|完整控制信令|
|
||||
|
||||
## Media终端功能
|
||||
@@ -39,8 +39,8 @@
|
||||
|功能|是否支持|是否实现|描述|
|
||||
|:--|:--|:--|:--|
|
||||
|Mediasoup|支持|完成|视频房间(会议)|
|
||||
|控制|支持|完成|部分控制信令|
|
||||
|录像|支持|未完成|录像|
|
||||
|混音|支持|未完成|多路混音|
|
||||
|
||||
### Android终端功能
|
||||
|
||||
|
||||
@@ -32,14 +32,14 @@
|
||||
|
||||
* 发布版本:1.0.0
|
||||
* 开机自启
|
||||
* 录制底噪
|
||||
* 录像底噪
|
||||
* 性能测试
|
||||
* 稳定性测试
|
||||
* 分辨率调整
|
||||
* 服务端录制
|
||||
* 服务端录像
|
||||
* 安卓预览按钮
|
||||
* 安装内存抖动
|
||||
* 降低视频录制大小
|
||||
* 降低视频录像大小
|
||||
* 防止重复邀请拉取
|
||||
* 码率等等参数配置验证
|
||||
* 查询消费者生产者信息
|
||||
|
||||
@@ -239,7 +239,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
/**
|
||||
* 录制按钮
|
||||
* 录像按钮
|
||||
*
|
||||
* @param message 消息
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -786,7 +786,7 @@ public final class Taoyao implements ITaoyao {
|
||||
}
|
||||
|
||||
/**
|
||||
* 录制
|
||||
* 录像
|
||||
*
|
||||
* @param message 信令消息
|
||||
* @param body 信令主体
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- 是否预览视频 -->
|
||||
<bool name="preview">true</bool>
|
||||
<bool name="preview">false</bool>
|
||||
<!-- 是否开启音频播放 -->
|
||||
<bool name="playAudio">true</bool>
|
||||
<!-- 是否开启视频播放 -->
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -33,7 +33,7 @@ public class MixerProcesser extends Thread implements JavaAudioDeviceModule.Samp
|
||||
|
||||
/**
|
||||
* 音频数据来源
|
||||
* 其实可以不用切换可以两个同时录制,但是有点浪费资源。
|
||||
* 其实可以不用切换可以两个同时录像,但是有点浪费资源。
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -12,7 +12,7 @@ public class Config {
|
||||
*/
|
||||
public static final int WHAT_SCREEN_CAPTURE = 1000;
|
||||
/**
|
||||
* 视频录制
|
||||
* 视频录像
|
||||
*/
|
||||
public static final int WHAT_RECORD = 1001;
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -25,7 +25,7 @@ make -C worker
|
||||
|
||||
## 录像功能
|
||||
|
||||
录像直接将媒体流转发给`ffmpeg`实现录制,没有直接修改`mediasoup`代码,代码侵入较低更加方便升级。
|
||||
录像直接将媒体流转发给`ffmpeg`实现录像,没有直接修改`mediasoup`代码,代码侵入较低更加方便升级。
|
||||
|
||||
## 节点配置
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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或多个会话属性
|
||||
````
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
@@ -95,10 +97,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
})
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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配置")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -50,6 +50,10 @@ public class ClientWrapper implements AutoCloseable {
|
||||
* SCTP协商
|
||||
*/
|
||||
private Object sctpCapabilities;
|
||||
/**
|
||||
* 媒体录像
|
||||
*/
|
||||
private Recorder recorder;
|
||||
/**
|
||||
* 发送通道
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, Object> 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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, Object> 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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, Object> 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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, Object> 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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user