[*] 本地录像

This commit is contained in:
acgist
2023-06-02 07:17:10 +08:00
parent a06f6a251f
commit becc68b05f
19 changed files with 404 additions and 179 deletions

View File

@@ -10,10 +10,11 @@ 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.protocol.control.ControlBellProtocol;
import com.acgist.taoyao.signal.protocol.control.ControlClientRecordProtocol;
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.ControlRecordProtocol;
import com.acgist.taoyao.signal.protocol.control.ControlServerRecordProtocol;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -34,10 +35,11 @@ import lombok.RequiredArgsConstructor;
public class ControlController {
private final ControlBellProtocol controlBellProtocol;
private final ControlRecordProtocol controlRecordProtocol;
private final ControlPhotographProtocol controlPhotographProtocol;
private final ControlConfigAudioProtocol controlConfigAudioProtocol;
private final ControlConfigVideoProtocol controlConfigVideoProtocol;
private final ControlClientRecordProtocol controlClientRecordProtocol;
private final ControlServerRecordProtocol controlServerRecordProtocol;
@Operation(summary = "响铃", description = "响铃控制")
@GetMapping("/bell/{clientId}")
@@ -45,10 +47,16 @@ public class ControlController {
return this.controlBellProtocol.execute(clientId, enabled);
}
@Operation(summary = "录像", description = "录像控制")
@GetMapping("/record/{clientId}")
@Operation(summary = "录像", description = "终端录像控制")
@GetMapping("/client/record/{clientId}")
public Message record(@PathVariable String clientId, @NotNull(message = "没有指定操作状态") Boolean enabled) {
return this.controlRecordProtocol.execute(clientId, enabled);
return this.controlClientRecordProtocol.execute(clientId, enabled);
}
@Operation(summary = "录像", description = "服务端录像控制")
@GetMapping("/server/record/{roomId}/{clientId}")
public Message record(@PathVariable String roomId, @PathVariable String clientId, @NotNull(message = "没有指定操作状态") Boolean enabled) {
return this.controlServerRecordProtocol.execute(roomId, clientId, enabled);
}
@Operation(summary = "拍照", description = "拍照控制")

View File

@@ -1,37 +0,0 @@
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);
}
}

View File

@@ -228,17 +228,17 @@ taoyao:
# SDPVP8 | H264
sdp: |
v=0
o=- 0 0 IN IP4 %s
o=- 0 0 IN IP4 127.0.0.1
s=TaoyaoRecord
t=0 0
m=audio %d RTP/AVP 97
c=IN IP4 %s
a=rtpmap:97 OPUS/48000/2
a=fmtp:97 sprop-stereo=1
m=video %d RTP/AVP 96
c=IN IP4 %s
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1
m=audio %d RTP/AVP 100
c=IN IP4 127.0.0.1
a=rtpmap:100 OPUS/48000/2
a=fmtp:100 sprop-stereo=1
m=video %d RTP/AVP 107
c=IN IP4 127.0.0.1
a=rtpmap:107 H264/90000
a=fmtp:107 packetization-mode=1
# 录像命令
record: ffmpeg -protocol_whitelist "file,rtp,udp" -y -i %s %s
# 截图命令
@@ -252,7 +252,8 @@ taoyao:
# 视频存储目录
storage-video-path: /data/taoyao/storage/video
# 录像地址
host: 127.0.0.1
#host: 127.0.0.1
host: 192.168.8.40
# 端口范围
min-port: 50000
max-port: 59999

View File

@@ -25,13 +25,27 @@ public class RecorderTest {
a=fmtp:97 sprop-stereo=1
m=video %d RTP/AVP 96
c=IN IP4 %s
a=rtpmap:96 H264/90000
a=rtpmap:96 VP8/90000
a=fmtp:96 packetization-mode=1
""");
// ffmpegProperties.setSdp("""
// v=0
// o=- 0 0 IN IP4 %s
// s=TaoyaoRecord
// t=0 0
// m=audio %d RTP/AVP 97
// c=IN IP4 %s
// a=rtpmap:97 OPUS/48000/2
// a=fmtp:97 sprop-stereo=1
// m=video %d RTP/AVP 96
// c=IN IP4 %s
// a=rtpmap:96 H264/90000
// a=fmtp:96 packetization-mode=1
// """);
ffmpegProperties.setRecord("ffmpeg -protocol_whitelist \"file,rtp,udp\" -y -i %s %s");
ffmpegProperties.setPreview("ffmpeg -y -i %s -ss %d -vframes 1 -f image2 %s");
ffmpegProperties.setDuration("ffprobe -i %s -show_entries format=duration");
final Recorder recorder = new Recorder("taoyao", ffmpegProperties);
final Recorder recorder = new Recorder("taoyao", null, null, ffmpegProperties);
recorder.start();
Thread.sleep(20 * 1000);
recorder.stop();

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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));
}
}

View File

@@ -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

View File

@@ -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();
}