[*] 服务端录制

This commit is contained in:
acgist
2023-05-31 07:34:18 +08:00
parent b1aa1e4a7a
commit 441e99483b
37 changed files with 640 additions and 545 deletions

View File

@@ -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配置")

View File

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

View File

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

View File

@@ -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:
# SDPVP8 | 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

View File

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

View File

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