[*] 本地录像
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package com.acgist.taoyao.signal.event.room;
|
||||
|
||||
import com.acgist.taoyao.signal.event.RoomEventAdapter;
|
||||
import com.acgist.taoyao.signal.party.media.Recorder;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 服务端录像关闭事件
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class RecorderCloseEvent extends RoomEventAdapter {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 媒体录像机
|
||||
*/
|
||||
private final Recorder recorder;
|
||||
|
||||
public RecorderCloseEvent(Recorder recorder) {
|
||||
super(recorder.getRoom());
|
||||
this.recorder = recorder;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 终端包装器:Peer
|
||||
* 视频房间使用
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@@ -123,7 +124,10 @@ public class ClientWrapper implements AutoCloseable {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// 注意:不要关闭终端
|
||||
// 注意:不要关闭终端(只是离开房间)
|
||||
if(this.recorder != null) {
|
||||
this.recorder.close();
|
||||
}
|
||||
this.consumers.values().forEach(Consumer::close);
|
||||
this.producers.values().forEach(Producer::close);
|
||||
this.dataConsumers.values().forEach(DataConsumer::close);
|
||||
|
||||
@@ -15,13 +15,19 @@ import com.acgist.taoyao.boot.utils.FileUtils;
|
||||
import com.acgist.taoyao.boot.utils.NetUtils;
|
||||
import com.acgist.taoyao.boot.utils.ScriptUtils;
|
||||
import com.acgist.taoyao.boot.utils.ScriptUtils.ScriptExecutor;
|
||||
import com.acgist.taoyao.signal.event.EventPublisher;
|
||||
import com.acgist.taoyao.signal.event.room.RecorderCloseEvent;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 媒体录像
|
||||
* 媒体录像机
|
||||
*
|
||||
* OPUS = 100
|
||||
* VP8 = 101
|
||||
* H264 = 107
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@@ -90,6 +96,14 @@ public class Recorder {
|
||||
* 命令执行器
|
||||
*/
|
||||
private ScriptExecutor scriptExecutor;
|
||||
/**
|
||||
* 房间
|
||||
*/
|
||||
private final Room room;
|
||||
/**
|
||||
* 终端
|
||||
*/
|
||||
private final ClientWrapper clientWrapper;
|
||||
/**
|
||||
* 文件路径
|
||||
*/
|
||||
@@ -113,16 +127,20 @@ public class Recorder {
|
||||
|
||||
/**
|
||||
* @param name 录像名称
|
||||
* @param room 房间
|
||||
* @param clientWrapper 终端
|
||||
* @param ffmpegProperties FFmpeg配置
|
||||
*/
|
||||
public Recorder(String name, FfmpegProperties ffmpegProperties) {
|
||||
public Recorder(String name, Room room, ClientWrapper clientWrapper, FfmpegProperties ffmpegProperties) {
|
||||
this.close = false;
|
||||
this.running = false;
|
||||
this.ffmpegProperties = ffmpegProperties;
|
||||
this.room = room;
|
||||
this.folder = Paths.get(ffmpegProperties.getStorageVideoPath(), name).toAbsolutePath().toString();
|
||||
this.sdpfile = Paths.get(this.folder, "taoyao.sdp").toAbsolutePath().toString();
|
||||
this.preview = Paths.get(this.folder, "taoyao.jpg").toAbsolutePath().toString();
|
||||
this.filepath = Paths.get(this.folder, "taoyao.mp4").toAbsolutePath().toString();
|
||||
this.clientWrapper = clientWrapper;
|
||||
this.ffmpegProperties = ffmpegProperties;
|
||||
FileUtils.mkdirs(this.folder);
|
||||
}
|
||||
|
||||
@@ -136,6 +154,7 @@ public class Recorder {
|
||||
}
|
||||
this.running = true;
|
||||
}
|
||||
this.buildSdpfile();
|
||||
this.thread = new Thread(this::record);
|
||||
this.thread.setDaemon(true);
|
||||
this.thread.setName("TaoyaoRecord");
|
||||
@@ -146,7 +165,6 @@ public class Recorder {
|
||||
* 录制视频
|
||||
*/
|
||||
private void record() {
|
||||
this.buildSdpfile();
|
||||
final String recordScript = String.format(this.ffmpegProperties.getRecord(), this.sdpfile, this.filepath);
|
||||
this.scriptExecutor = new ScriptExecutor(recordScript);
|
||||
try {
|
||||
@@ -172,11 +190,11 @@ public class Recorder {
|
||||
this.videoPort = NetUtils.scanPort(this.audioPort + 16, this.ffmpegProperties.getMaxPort());
|
||||
final String sdp = String.format(
|
||||
this.ffmpegProperties.getSdp(),
|
||||
this.ffmpegProperties.getHost(),
|
||||
// this.ffmpegProperties.getHost(),
|
||||
this.audioPort,
|
||||
this.ffmpegProperties.getHost(),
|
||||
this.videoPort,
|
||||
this.ffmpegProperties.getHost()
|
||||
// this.ffmpegProperties.getHost(),
|
||||
this.videoPort
|
||||
// this.ffmpegProperties.getHost()
|
||||
);
|
||||
Files.write(
|
||||
Paths.get(this.sdpfile),
|
||||
@@ -242,4 +260,11 @@ public class Recorder {
|
||||
this.duration();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭录像
|
||||
*/
|
||||
public void close() {
|
||||
EventPublisher.publishEvent(new RecorderCloseEvent(this));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import com.acgist.taoyao.signal.client.ClientType;
|
||||
import com.acgist.taoyao.signal.protocol.ProtocolControlAdapter;
|
||||
|
||||
/**
|
||||
* 录像信令
|
||||
* 终端录像信令
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@@ -36,12 +36,12 @@ import com.acgist.taoyao.signal.protocol.ProtocolControlAdapter;
|
||||
"终端=>信令服务->目标终端->信令服务->终端"
|
||||
}
|
||||
)
|
||||
public class ControlRecordProtocol extends ProtocolControlAdapter {
|
||||
public class ControlClientRecordProtocol extends ProtocolControlAdapter {
|
||||
|
||||
public static final String SIGNAL = "control::record";
|
||||
public static final String SIGNAL = "control::client::record";
|
||||
|
||||
public ControlRecordProtocol() {
|
||||
super("录像信令", SIGNAL);
|
||||
public ControlClientRecordProtocol() {
|
||||
super("终端录像信令", SIGNAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.acgist.taoyao.signal.protocol.media;
|
||||
package com.acgist.taoyao.signal.protocol.control;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.context.ApplicationListener;
|
||||
|
||||
import com.acgist.taoyao.boot.annotation.Description;
|
||||
import com.acgist.taoyao.boot.annotation.Protocol;
|
||||
import com.acgist.taoyao.boot.config.Constant;
|
||||
@@ -12,14 +14,15 @@ import com.acgist.taoyao.boot.model.Message;
|
||||
import com.acgist.taoyao.boot.utils.MapUtils;
|
||||
import com.acgist.taoyao.signal.client.Client;
|
||||
import com.acgist.taoyao.signal.client.ClientType;
|
||||
import com.acgist.taoyao.signal.event.room.RecorderCloseEvent;
|
||||
import com.acgist.taoyao.signal.party.media.ClientWrapper;
|
||||
import com.acgist.taoyao.signal.party.media.Kind;
|
||||
import com.acgist.taoyao.signal.party.media.Recorder;
|
||||
import com.acgist.taoyao.signal.party.media.Room;
|
||||
import com.acgist.taoyao.signal.protocol.ProtocolRoomAdapter;
|
||||
import com.acgist.taoyao.signal.protocol.ProtocolControlAdapter;
|
||||
|
||||
/**
|
||||
* 媒体录像
|
||||
* 服务端录像信令
|
||||
*
|
||||
* @author acgist
|
||||
*/
|
||||
@@ -28,12 +31,14 @@ import com.acgist.taoyao.signal.protocol.ProtocolRoomAdapter;
|
||||
body = {
|
||||
"""
|
||||
{
|
||||
"clientId": "目标终端ID",
|
||||
"to": "目标终端ID",
|
||||
"roomId": "房间ID",
|
||||
"enabled": 是否录像(true|false)
|
||||
}
|
||||
""",
|
||||
"""
|
||||
{
|
||||
"roomId": "房间ID",
|
||||
"enabled": 是否录像(true|false),
|
||||
"filepath": "视频文件路径"
|
||||
}
|
||||
@@ -41,23 +46,33 @@ import com.acgist.taoyao.signal.protocol.ProtocolRoomAdapter;
|
||||
},
|
||||
flow = "终端=>信令服务->终端"
|
||||
)
|
||||
public class MediaRecordProtocol extends ProtocolRoomAdapter {
|
||||
public class ControlServerRecordProtocol extends ProtocolControlAdapter implements ApplicationListener<RecorderCloseEvent> {
|
||||
|
||||
public static final String SIGNAL = "control::server::record";
|
||||
|
||||
private final FfmpegProperties ffmpegProperties;
|
||||
|
||||
public MediaRecordProtocol(FfmpegProperties ffmpegProperties) {
|
||||
super("媒体录像", "media::record");
|
||||
public ControlServerRecordProtocol(FfmpegProperties ffmpegProperties) {
|
||||
super("服务端录像信令", SIGNAL);
|
||||
this.ffmpegProperties = ffmpegProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(RecorderCloseEvent event) {
|
||||
final Recorder recorder = event.getRecorder();
|
||||
this.stop(recorder.getRoom(), recorder.getClientWrapper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(String clientId, ClientType clientType, Room room, Client client, Client mediaClient, Message message, Map<String, Object> body) {
|
||||
final Boolean enabled = MapUtils.get(body, Constant.ENABLED, Boolean.TRUE);
|
||||
public void execute(String clientId, ClientType clientType, Client client, Client targetClient, Message message, Map<String, Object> body) {
|
||||
String filepath;
|
||||
final String roomId = MapUtils.get(body, Constant.ROOM_ID);
|
||||
final Boolean enabled = MapUtils.get(body, Constant.ENABLED, Boolean.TRUE);
|
||||
final Room room = this.roomManager.room(roomId);
|
||||
if(enabled) {
|
||||
filepath = this.start(room, client, mediaClient);
|
||||
filepath = this.start(room, room.clientWrapper(client));
|
||||
} else {
|
||||
filepath = this.stop(room, client, mediaClient);
|
||||
filepath = this.stop(room, room.clientWrapper(client));
|
||||
}
|
||||
body.put(Constant.FILEPATH, filepath);
|
||||
client.push(message);
|
||||
@@ -71,16 +86,16 @@ public class MediaRecordProtocol extends ProtocolRoomAdapter {
|
||||
* @return 执行结果
|
||||
*/
|
||||
public Message execute(String roomId, String clientId, Boolean enabled) {
|
||||
String filepath;
|
||||
final Room room = this.roomManager.room(roomId);
|
||||
final Client client = this.clientManager.clients(clientId);
|
||||
final Client mediaClient = room.getMediaClient();
|
||||
String filepath;
|
||||
if(enabled) {
|
||||
filepath = this.start(room, client, mediaClient);
|
||||
filepath = this.start(room, room.clientWrapper(client));
|
||||
} else {
|
||||
filepath = this.stop(room, client, mediaClient);
|
||||
filepath = this.stop(room, room.clientWrapper(client));
|
||||
}
|
||||
return Message.success(Map.of(
|
||||
Constant.ROOM_ID, roomId,
|
||||
Constant.ENABLED, enabled,
|
||||
Constant.FILEPATH, filepath
|
||||
));
|
||||
@@ -89,22 +104,22 @@ public class MediaRecordProtocol extends ProtocolRoomAdapter {
|
||||
/**
|
||||
* 开始录制
|
||||
*
|
||||
* @param room 房间
|
||||
* @param client 终端
|
||||
* @param mediaClient 媒体终端
|
||||
* @param room 房间
|
||||
* @param clientWrapper 终端
|
||||
* @param mediaClient 媒体终端
|
||||
*
|
||||
* @return 文件地址
|
||||
*/
|
||||
private String start(Room room, Client client, Client mediaClient) {
|
||||
final ClientWrapper clientWrapper = room.clientWrapper(client);
|
||||
private String start(Room room, ClientWrapper clientWrapper) {
|
||||
synchronized (clientWrapper) {
|
||||
final Recorder recorder = clientWrapper.getRecorder();
|
||||
if(recorder != null) {
|
||||
return recorder.getFilepath();
|
||||
}
|
||||
}
|
||||
final String name = UUID.randomUUID().toString();
|
||||
// 打开录制线程
|
||||
final Recorder recorder = new Recorder(UUID.randomUUID().toString(), this.ffmpegProperties);
|
||||
final Recorder recorder = new Recorder(name, room, clientWrapper, this.ffmpegProperties);
|
||||
recorder.start();
|
||||
clientWrapper.setRecorder(recorder);
|
||||
// 打开媒体录制
|
||||
@@ -115,15 +130,15 @@ public class MediaRecordProtocol extends ProtocolRoomAdapter {
|
||||
body.put(Constant.HOST, this.ffmpegProperties.getHost());
|
||||
body.put(Constant.ROOM_ID, room.getRoomId());
|
||||
body.put(Constant.ENABLED, true);
|
||||
body.put(Constant.CLIENT_ID, client.clientId());
|
||||
body.put(Constant.CLIENT_ID, clientWrapper.getClientId());
|
||||
body.put(Constant.RTP_CAPABILITIES, clientWrapper.getRtpCapabilities());
|
||||
clientWrapper.getProducers().values().forEach(producer -> {
|
||||
if(producer.getKind() == Kind.AUDIO) {
|
||||
recorder.setAudioStreamId(Constant.STREAM_ID_CONSUMER.apply(producer.getStreamId(), client.clientId()));
|
||||
recorder.setAudioStreamId(Constant.STREAM_ID_CONSUMER.apply(producer.getStreamId(), clientWrapper.getClientId()));
|
||||
body.put("audioStreamId", recorder.getAudioStreamId());
|
||||
body.put("audioProducerId", producer.getProducerId());
|
||||
} else if(producer.getKind() == Kind.VIDEO) {
|
||||
recorder.setAudioStreamId(Constant.STREAM_ID_CONSUMER.apply(producer.getStreamId(), client.clientId()));
|
||||
recorder.setAudioStreamId(Constant.STREAM_ID_CONSUMER.apply(producer.getStreamId(), clientWrapper.getClientId()));
|
||||
body.put("videoStreamId", recorder.getVideoStreamId());
|
||||
body.put("videoProducerId", producer.getProducerId());
|
||||
} else {
|
||||
@@ -131,6 +146,7 @@ public class MediaRecordProtocol extends ProtocolRoomAdapter {
|
||||
}
|
||||
});
|
||||
message.setBody(body);
|
||||
final Client mediaClient = room.getMediaClient();
|
||||
mediaClient.request(message);
|
||||
return recorder.getFilepath();
|
||||
}
|
||||
@@ -138,15 +154,13 @@ public class MediaRecordProtocol extends ProtocolRoomAdapter {
|
||||
/**
|
||||
* 关闭录像
|
||||
*
|
||||
* @param room 房间
|
||||
* @param client 终端
|
||||
* @param mediaClient 媒体终端
|
||||
* @param room 房间
|
||||
* @param clientWrapper 终端
|
||||
*
|
||||
* @return 文件地址
|
||||
*/
|
||||
private String stop(Room room, Client client, Client mediaClient) {
|
||||
private String stop(Room room, ClientWrapper clientWrapper) {
|
||||
final Recorder recorder;
|
||||
final ClientWrapper clientWrapper = room.clientWrapper(client);
|
||||
synchronized (clientWrapper) {
|
||||
recorder = clientWrapper.getRecorder();
|
||||
if(recorder == null) {
|
||||
@@ -168,6 +182,7 @@ public class MediaRecordProtocol extends ProtocolRoomAdapter {
|
||||
body.put(Constant.ROOM_ID, room.getRoomId());
|
||||
body.put(Constant.ENABLED, false);
|
||||
message.setBody(body);
|
||||
final Client mediaClient = room.getMediaClient();
|
||||
mediaClient.request(message);
|
||||
return recorder.getFilepath();
|
||||
}
|
||||
Reference in New Issue
Block a user