Compare commits
184 Commits
Author | SHA1 | Date | |
---|---|---|---|
4799d83014 | |||
d1829901bf | |||
dda2d2fc07 | |||
b4be3825bc | |||
5eed5e0eca | |||
bc5ff7505a | |||
5003b1268b | |||
ddd13510a8 | |||
db293c8430 | |||
33185bdc1f | |||
86f056a01b | |||
daccfb48ca | |||
279ae44a25 | |||
5061c3fdda | |||
41c6c887f5 | |||
b4423f9084 | |||
b699f1fb3a | |||
6c52ede51b | |||
c69ee9af05 | |||
f8ce65d15b | |||
04cfcfd22a | |||
054ab25205 | |||
8c5b6f6021 | |||
c9af1a4dc3 | |||
87423c734f | |||
0b3a80bce1 | |||
e1d0e355a0 | |||
79fc31dd6e | |||
5152136f97 | |||
5611bbbe38 | |||
ccbb1ad186 | |||
ef13720181 | |||
3ec2f90961 | |||
ac10573dc8 | |||
d4cf7e076a | |||
e37e9b2677 | |||
fd8a711662 | |||
2dc1f43401 | |||
fdc826e246 | |||
9ac5a2adad | |||
8a907dd68a | |||
1a1fe4e89b | |||
c456d2e1ea | |||
0feb5ad242 | |||
205c903294 | |||
071e2a2491 | |||
028c178ef5 | |||
d3a828fa13 | |||
9c8883823a | |||
1c2b05b0ef | |||
8c68d9dfa1 | |||
e6819b2a15 | |||
e36befe7ef | |||
440b316c4c | |||
b35db44aeb | |||
21f2a50952 | |||
f1db3e3d1d | |||
b5068617bb | |||
da98ec41c3 | |||
b2d6a5fe4c | |||
c8bfdfb67f | |||
7ccb3db8bc | |||
091776c30e | |||
00a5ab9dde | |||
b0733e985b | |||
4e0b1d6c31 | |||
92dee06429 | |||
7528fdbb4b | |||
2bb56f5bbc | |||
a5faf10138 | |||
cbba1c7be7 | |||
9c5a2c7b26 | |||
db58ea8b95 | |||
5ae7fc9972 | |||
ff690aaaef | |||
613be42216 | |||
4c84da13bc | |||
e55bd2aea5 | |||
d7837a1975 | |||
c1655bdeb5 | |||
81822ff553 | |||
9bcade0b2e | |||
71b75038e2 | |||
37cfd012e8 | |||
a66c66119b | |||
44470dddb7 | |||
3ea5ac5deb | |||
765b1f6fe8 | |||
5a35e37687 | |||
0bf1eb6615 | |||
a9eab491d5 | |||
6d1dfc6445 | |||
845d049ad5 | |||
4fc4d3a863 | |||
c2dcf6d931 | |||
91b488d9a6 | |||
f36ed68607 | |||
410de78a1c | |||
53c4b69f0e | |||
a24e9fc910 | |||
9586f05a6d | |||
c20a1b4534 | |||
510319cd65 | |||
2ec9dad3c8 | |||
94df487a93 | |||
60afe2aaed | |||
0de97d39a8 | |||
d847e06ec3 | |||
c11f5c9ef0 | |||
3e422e384b | |||
6ecbf83cdc | |||
bff7ad1b37 | |||
6c619f6605 | |||
efdb6b1e02 | |||
51baef0318 | |||
0ce07bbcfe | |||
edf174c2f5 | |||
f5da6cf084 | |||
b12cdba22f | |||
8a7c1e7edd | |||
0e586a75bb | |||
b372fd8d8e | |||
6fdc9c5c3b | |||
a5ef517477 | |||
a3a23db8df | |||
5d7e30eec9 | |||
506f67cb0c | |||
387451afb2 | |||
e2aa0a5b0c | |||
e0974681f9 | |||
f2835262fd | |||
65b492f8e7 | |||
39b65a2aee | |||
7c6f0a612c | |||
00b2d42a68 | |||
7d71478baf | |||
704e9eb9a0 | |||
bbabf0d695 | |||
b98acc9cd4 | |||
b0cdba12f7 | |||
5cbd6a6b49 | |||
2e42490440 | |||
6a86ab806f | |||
e5e2a65c18 | |||
30e96ed6cb | |||
f7b8960a98 | |||
37fc276f3d | |||
8d1bc71170 | |||
ffac3458fd | |||
84e0addf53 | |||
34a6ba040d | |||
b625dcff7d | |||
dfec2e8754 | |||
330e279347 | |||
5f16ba0371 | |||
c7d79b48ab | |||
707c38da72 | |||
ef6c164c0f | |||
62cd5e2016 | |||
b7b70fe42f | |||
9d1da22aa0 | |||
6a25f48852 | |||
45ac5d5d56 | |||
85962d1373 | |||
66dc724317 | |||
2641b866be | |||
55de64069b | |||
7666446ed8 | |||
0a7fefaead | |||
d2d278ae76 | |||
cd43cc9fa8 | |||
d4ae48d963 | |||
8db1726e10 | |||
a7e453fbda | |||
bd9e15f232 | |||
dc56cbd65e | |||
af40be890e | |||
951bffea4e | |||
0bcebc8c30 | |||
c4ebda0713 | |||
67a891ada8 | |||
310ec451fb | |||
0ece1e2915 | |||
c1e9ce147f |
11
README.md
11
README.md
@ -45,3 +45,14 @@ docker run --name gb28181-wvp-proxy --rm \
|
||||
mvn deploy -DaltDeploymentRepository=amleixun-mvn-reop::default::file:H:/Repository/skcks.cn/gb28181-docking-platform-mvn-repo
|
||||
```
|
||||
git push 推送即可
|
||||
|
||||
### docker ffmpeg 推流
|
||||
```shell
|
||||
docker run --rm jrottenberg/ffmpeg \
|
||||
-re -i \
|
||||
"http://192.168.1.241:5080/rtp/44050100001180000001_44050100001310000001.live.flv" \
|
||||
-vcodec h264 \
|
||||
-acodec aac \
|
||||
-f flv \
|
||||
rtmp://192.168.1.241:1935/live/test?secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc
|
||||
```
|
BIN
ffmpeg/ffmpeg-git-amd64-static.tar.xz
Normal file
BIN
ffmpeg/ffmpeg-git-amd64-static.tar.xz
Normal file
Binary file not shown.
@ -5,6 +5,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 全局异常处理类
|
||||
*
|
||||
@ -13,7 +15,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
@Slf4j
|
||||
@ControllerAdvice
|
||||
public class BasicExceptionAdvice {
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ExceptionHandler(IOException.class)
|
||||
public void exception(HttpServletRequest request, Exception e) {
|
||||
if(request.getRequestURI().equals("/video")){
|
||||
return;
|
||||
|
@ -1,5 +1,6 @@
|
||||
package cn.skcks.docking.gb28181.wvp.advice;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.JsonException;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -72,6 +73,11 @@ public class ExceptionAdvice {
|
||||
return JsonResponse.error("参数异常");
|
||||
}
|
||||
|
||||
@ExceptionHandler(JsonException.class)
|
||||
public JsonResponse<String> handleJsonException(JsonException e){
|
||||
return JsonResponse.error(e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public JsonResponse<String> exception(Exception e) {
|
||||
e.printStackTrace();
|
||||
|
@ -1,91 +0,0 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.skcks.docking.gb28181.media.config.ZlmMediaConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.proxy.ZlmProxyClient;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bytedeco.ffmpeg.global.avcodec;
|
||||
import org.bytedeco.javacv.*;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.bytedeco.ffmpeg.global.avutil.AVMEDIA_TYPE_VIDEO;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "视频api")
|
||||
@Controller
|
||||
@RequestMapping("/video")
|
||||
@RequiredArgsConstructor
|
||||
public class VideoController {
|
||||
private final ZlmProxyClient zlmProxyClient;
|
||||
private final ZlmMediaConfig config;
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi videoApi() {
|
||||
return SwaggerConfig.api("VideoApi", "/video");
|
||||
}
|
||||
|
||||
@Operation(summary = "获取视频")
|
||||
@GetMapping(produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
@ResponseBody
|
||||
public void video(HttpServletResponse response) throws IOException {
|
||||
response.reset();
|
||||
// response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
response.setContentType("video/mp4");
|
||||
|
||||
Path tmp = Path.of(System.getProperty("java.io.tmpdir"), IdUtil.getSnowflakeNextIdStr()).toAbsolutePath();
|
||||
File file = new File(tmp + ".mp4");
|
||||
log.info("创建文件 {}, {}", file, file.createNewFile());
|
||||
|
||||
String url = StringUtils.joinWith("/", config.getUrl(), "live", "test.live.mp4");
|
||||
log.info("url {}", url);
|
||||
try (
|
||||
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(url);
|
||||
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(file, 1920, 1080, 0)
|
||||
) {
|
||||
grabber.start();
|
||||
recorder.start();
|
||||
log.info("开始录像");
|
||||
log.info("{}", file);
|
||||
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
|
||||
// recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); //视频源数据yuv
|
||||
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); //设置音频压缩方式
|
||||
recorder.setFormat("mp4");
|
||||
recorder.setVideoOption("threads", String.valueOf(Runtime.getRuntime().availableProcessors())); //解码线程数
|
||||
try {
|
||||
Frame frame;
|
||||
while ((frame = grabber.grab()) != null) {
|
||||
if (frame.streamIndex == AVMEDIA_TYPE_VIDEO) {
|
||||
recorder.record(frame);
|
||||
}
|
||||
}
|
||||
grabber.stop();
|
||||
grabber.release();
|
||||
recorder.stop();
|
||||
} catch (FFmpegFrameRecorder.Exception | FrameGrabber.Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
log.info("录像结束");
|
||||
InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
|
||||
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
|
||||
IoUtil.copy(inputStream, outputStream);
|
||||
log.info("临时文件 {} 写入 响应 完成", file);
|
||||
log.info("删除临时文件 {} {}", file, file.delete());
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.device;
|
||||
|
||||
import cn.skcks.docking.gb28181.annotation.web.JsonMapping;
|
||||
import cn.skcks.docking.gb28181.annotation.web.methods.GetJson;
|
||||
import cn.skcks.docking.gb28181.annotation.web.methods.PostJson;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import cn.skcks.docking.gb28181.common.page.PageWrapper;
|
||||
import cn.skcks.docking.gb28181.wvp.api.device.convertor.DeviceDTOConvertor;
|
||||
import cn.skcks.docking.gb28181.wvp.api.device.dto.AddDeviceDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.api.device.dto.DevicePageDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "设备信息")
|
||||
@RestController
|
||||
@RequestMapping("/device")
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceController {
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi deviceApi() {
|
||||
return SwaggerConfig.api("DeviceApi", "/device");
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询设备列表")
|
||||
@GetJson("/page")
|
||||
public JsonResponse<PageWrapper<WvpProxyDevice>> getDevicesWithPagePage(@ParameterObject @Validated DevicePageDTO dto) {
|
||||
return JsonResponse.success(PageWrapper.of(deviceService.getDevicesWithPage(dto.getPage(), dto.getSize())));
|
||||
}
|
||||
|
||||
@Operation(summary = "添加设备")
|
||||
@PostJson("/add")
|
||||
public JsonResponse<Boolean> addDevice(@RequestBody AddDeviceDTO dto) {
|
||||
return JsonResponse.success(deviceService.addDevice(DeviceDTOConvertor.INSTANCE.dto2dao(dto)));
|
||||
}
|
||||
|
||||
@Operation(summary = "根据设备编码(21位) 查询指定设备信息")
|
||||
@GetJson("/info/deviceCode")
|
||||
public JsonResponse<WvpProxyDevice> infoByDeviceCode(@RequestParam String deviceCode) {
|
||||
Optional<WvpProxyDevice> wvpProxyDevice = deviceService.getDeviceByDeviceCode(deviceCode);
|
||||
return JsonResponse.success(wvpProxyDevice.orElse(null));
|
||||
}
|
||||
|
||||
@Operation(summary = "根据国标id(20位) 查询指定设备信息")
|
||||
@GetJson("/info/gbDeviceId")
|
||||
public JsonResponse<List<WvpProxyDevice>> infoByGbDeviceId(@RequestParam String gbDeviceId) {
|
||||
List<WvpProxyDevice> wvpProxyDevice = deviceService.getDeviceByGbDeviceId(gbDeviceId);
|
||||
return JsonResponse.success(wvpProxyDevice);
|
||||
}
|
||||
|
||||
@Operation(summary = "根据设备编码(21位) 删除指定设备")
|
||||
@JsonMapping(value = "/delete/deviceCode",method = {RequestMethod.GET,RequestMethod.DELETE})
|
||||
public JsonResponse<Boolean> deleteByDeviceCode(@RequestParam String deviceCode){
|
||||
WvpProxyDevice wvpProxyDevice = new WvpProxyDevice();
|
||||
wvpProxyDevice.setDeviceCode(deviceCode);
|
||||
return JsonResponse.success(deviceService.deleteDevice(wvpProxyDevice));
|
||||
}
|
||||
|
||||
@Operation(summary = "根据国标id(20位) 删除指定设备")
|
||||
@JsonMapping(value = "/delete/gbDeviceId",method = {RequestMethod.GET,RequestMethod.DELETE})
|
||||
public JsonResponse<Boolean> deleteByGbDeviceId(@RequestParam String gbDeviceId){
|
||||
WvpProxyDevice wvpProxyDevice = new WvpProxyDevice();
|
||||
wvpProxyDevice.setGbDeviceId(gbDeviceId);
|
||||
return JsonResponse.success(deviceService.deleteDevice(wvpProxyDevice));
|
||||
}
|
||||
|
||||
@Operation(summary = "根据主键 id 删除指定设备")
|
||||
@JsonMapping(value = "/delete/id",method = {RequestMethod.GET,RequestMethod.DELETE})
|
||||
public JsonResponse<Boolean> deleteById(@RequestParam Long id){
|
||||
WvpProxyDevice wvpProxyDevice = new WvpProxyDevice();
|
||||
wvpProxyDevice.setId(id);
|
||||
return JsonResponse.success(deviceService.deleteDevice(wvpProxyDevice));
|
||||
}
|
||||
|
||||
@Operation(summary = "根据主键id修改设备信息")
|
||||
@PostJson("/modify")
|
||||
public JsonResponse<Boolean> updateById(@RequestBody WvpProxyDevice device){
|
||||
return JsonResponse.success(deviceService.modifyDevice(device));
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.device.convertor;
|
||||
|
||||
import cn.skcks.docking.gb28181.wvp.api.device.dto.AddDeviceDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
@Mapper
|
||||
public abstract class DeviceDTOConvertor {
|
||||
public static final DeviceDTOConvertor INSTANCE = Mappers.getMapper(DeviceDTOConvertor.class);
|
||||
|
||||
abstract public WvpProxyDevice dto2dao(AddDeviceDTO dto);
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.device.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AddDeviceDTO {
|
||||
@NotBlank(message = "设备编码 不能为空")
|
||||
@Schema(description = "设备编码")
|
||||
private String deviceCode;
|
||||
@NotBlank(message = "国标编码 不能为空")
|
||||
@Schema(description = "国标编码")
|
||||
private String gbDeviceId;
|
||||
@NotBlank(message = "国标通道id 不能为空")
|
||||
@Schema(description = "国标通道id")
|
||||
private String gbDeviceChannelId;
|
||||
@NotBlank(message = "设备名称 不能为空")
|
||||
@Schema(description = "设备名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "地址")
|
||||
private String address;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.device.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DevicePageDTO {
|
||||
@Schema(description = "页数", example = "1")
|
||||
@NotNull(message = "page 不能为空")
|
||||
@Min(value = 1, message = "page 必须为正整数")
|
||||
Integer page;
|
||||
|
||||
@Schema(description = "每页条数", example = "10")
|
||||
@NotNull(message = "size 不能为空")
|
||||
@Min(value = 1, message = "size 必须为正整数")
|
||||
Integer size;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.docking;
|
||||
|
||||
import cn.skcks.docking.gb28181.annotation.web.JsonMapping;
|
||||
import cn.skcks.docking.gb28181.annotation.web.methods.GetJson;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import cn.skcks.docking.gb28181.common.page.PageWrapper;
|
||||
import cn.skcks.docking.gb28181.wvp.api.docking.dto.DockingPageDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "设备/平台对接")
|
||||
@RestController
|
||||
@JsonMapping("/docking")
|
||||
@RequiredArgsConstructor
|
||||
public class DockingController {
|
||||
private final DockingService dockingService;
|
||||
@Bean
|
||||
public GroupedOpenApi dockingApi() {
|
||||
return SwaggerConfig.api("Docking", "/docking");
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询对接的设备/平台列表")
|
||||
@GetJson("/page")
|
||||
private JsonResponse<PageWrapper<WvpProxyDocking>> page(@ParameterObject @Validated DockingPageDTO dto){
|
||||
return JsonResponse.success(PageWrapper.of(dockingService.getDockingWithPage(dto.getPage(), dto.getSize())));
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.docking.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DockingPageDTO {
|
||||
@Schema(description = "页数", example = "1")
|
||||
@NotNull(message = "page 不能为空")
|
||||
@Min(value = 1, message = "page 必须为正整数")
|
||||
Integer page;
|
||||
|
||||
@Schema(description = "每页条数", example = "10")
|
||||
@NotNull(message = "size 不能为空")
|
||||
@Min(value = 1, message = "size 必须为正整数")
|
||||
Integer size;
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.download;
|
||||
|
||||
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.service.download.DownloadService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.gb28181.Gb28181DownloadService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "下载")
|
||||
@RestController
|
||||
@RequestMapping("/download")
|
||||
public class DownloadController {
|
||||
private final DownloadService downloadService;
|
||||
private final Gb28181DownloadService gb28181DownloadService;
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi downloadApi() {
|
||||
return SwaggerConfig.api("DownloadApi", "/download");
|
||||
}
|
||||
|
||||
@Operation(summary = "下载代理")
|
||||
@RequestMapping(method = RequestMethod.HEAD, value = "/proxy")
|
||||
public void downloadProxyHeader(HttpServletRequest request, HttpServletResponse response, @RequestParam String url) {
|
||||
downloadService.header(request, response, url);
|
||||
}
|
||||
|
||||
@Operation(summary = "下载代理")
|
||||
@GetMapping("/proxy")
|
||||
public void downloadProxy(HttpServletRequest request, HttpServletResponse response, @RequestParam String url) {
|
||||
downloadService.download(request, response, url);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.gb28181;
|
||||
|
||||
import cn.skcks.docking.gb28181.annotation.web.JsonMapping;
|
||||
import cn.skcks.docking.gb28181.annotation.web.methods.GetJson;
|
||||
import cn.skcks.docking.gb28181.annotation.web.methods.PostJson;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import cn.skcks.docking.gb28181.service.record.vo.RecordInfoItemVO;
|
||||
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.service.catalog.CatalogService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.device.control.DeviceControlService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.record.RecordInfoService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.record.dto.RecordInfoDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogItemDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogResponseDTO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.request.async.DeferredResult;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@JsonMapping("/gb28181")
|
||||
public class Gb28181Controller {
|
||||
private final CatalogService catalogService;
|
||||
private final DeviceControlService deviceControlService;
|
||||
private final RecordInfoService recordInfoService;
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi gb28181Api() {
|
||||
return SwaggerConfig.api("Gb28181Api", "/gb28181");
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@GetJson("/catalog")
|
||||
public DeferredResult<JsonResponse<List<CatalogItemDTO>>> catalog(@RequestParam("gbDeviceId") String id){
|
||||
DeferredResult<JsonResponse<List<CatalogItemDTO>>> result = new DeferredResult<>();
|
||||
CompletableFuture<CatalogResponseDTO> catalog = catalogService.getCatalog(id);
|
||||
catalog.thenApplyAsync((dto)->{
|
||||
List<CatalogItemDTO> deviceList = dto.getDeviceList().getDeviceList();
|
||||
result.setResult(JsonResponse.success(deviceList));
|
||||
return null;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@GetJson("/deviceControl/recordCmd")
|
||||
public JsonResponse<Void> recordCmd(@RequestParam String deviceCode,@RequestParam String cmd){
|
||||
deviceControlService.sendRecordControl(deviceCode, cmd);
|
||||
return JsonResponse.success(null);
|
||||
}
|
||||
|
||||
@PostJson("/recordInfo")
|
||||
public DeferredResult<JsonResponse<List<RecordInfoItemVO>>> recordInfo(RecordInfoDTO dto){
|
||||
return recordInfoService.requestRecordInfo(dto);
|
||||
}
|
||||
}
|
@ -1,25 +1,31 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.video;
|
||||
|
||||
import cn.skcks.docking.gb28181.wvp.service.video.RecordService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.video.VideoService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "录像api")
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/video/record")
|
||||
public class RecordController {
|
||||
private final RecordService recordService;
|
||||
private final VideoService videoService;
|
||||
|
||||
@Operation(summary = "返回文件下载 http 头信息",description = "禁止多线程下载, 默认文件名为 record.mp4")
|
||||
@RequestMapping(method = {RequestMethod.HEAD,RequestMethod.OPTIONS})
|
||||
public void record(HttpServletResponse response){
|
||||
recordService.header(response);
|
||||
videoService.header(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "录制flv视频流, 并下载为flv文件")
|
||||
@GetMapping
|
||||
public void record(HttpServletResponse response, @RequestParam String url,@RequestParam long time){
|
||||
recordService.record(response,url,time);
|
||||
public void record(HttpServletRequest request, HttpServletResponse response, @RequestParam String url, @RequestParam long time){
|
||||
videoService.record(request, response,url,time);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,95 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.video;
|
||||
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import cn.skcks.docking.gb28181.media.config.ZlmMediaConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.api.video.dto.RealtimeVideoReq;
|
||||
import cn.skcks.docking.gb28181.wvp.api.video.dto.VideoMp4Req;
|
||||
import cn.skcks.docking.gb28181.wvp.api.video.dto.VideoReq;
|
||||
import cn.skcks.docking.gb28181.wvp.config.Gb28181DeviceVideoApiConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.config.SwaggerConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.service.gb28181.Gb28181DownloadService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.video.VideoService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.wvp.WvpService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.context.request.async.DeferredResult;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "视频api")
|
||||
@Controller
|
||||
@RequestMapping("/video")
|
||||
@RequiredArgsConstructor
|
||||
public class VideoController {
|
||||
private final ZlmMediaConfig config;
|
||||
private final VideoService videoService;
|
||||
private final WvpService wvpService;
|
||||
|
||||
private final WvpProxyConfig proxyConfig;
|
||||
|
||||
private final Gb28181DownloadService gb28181DownloadService;
|
||||
|
||||
private final Gb28181DeviceVideoApiConfig gb28181DeviceVideoApiConfig;
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi videoApi() {
|
||||
return SwaggerConfig.api("VideoApi", "/video");
|
||||
}
|
||||
|
||||
@Operation(summary = "获取视频 (返回视频流)")
|
||||
@GetMapping(produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
@ResponseBody
|
||||
public void video(HttpServletRequest request, HttpServletResponse response, @ParameterObject VideoReq req) {
|
||||
if(proxyConfig.getEnable()){
|
||||
wvpService.video(request,response,req.getDeviceCode(), req.getStartTime(), req.getEndTime());
|
||||
} else {
|
||||
gb28181DownloadService.video(request,response,req.getDeviceCode(), req.getStartTime(), req.getEndTime(), req.getFileHeader(), req.getUseDownload());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取视频 (返回视频url)")
|
||||
@GetMapping(value = "/device/video.mp4")
|
||||
@ResponseBody
|
||||
public DeferredResult<JsonResponse<String>> video(@ParameterObject VideoMp4Req req) {
|
||||
long forward = gb28181DeviceVideoApiConfig.getOffset().getForward().toMillis();
|
||||
long back = gb28181DeviceVideoApiConfig.getOffset().getBack().toMillis();
|
||||
DateTime reqStartTime = DateUtil.date(req.getStartTime());
|
||||
DateTime reqEndTime = DateUtil.date(req.getEndTime());
|
||||
Date startTime = DateUtil.offsetMillisecond(reqStartTime, (int) -forward);
|
||||
Date endTime = DateUtil.offsetMillisecond(reqEndTime, (int) back);
|
||||
log.info("请求的时间范围 {} ~ {}", reqStartTime, reqEndTime);
|
||||
log.info("偏移后的时间范围 {} ~ {}", startTime, endTime);
|
||||
return gb28181DownloadService.videoUrl(req.getDeviceCode(), startTime, endTime);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取实时视频 (返回视频url)")
|
||||
@GetMapping(value = "/device/realtime")
|
||||
@ResponseBody
|
||||
public DeferredResult<JsonResponse<String>> realtime(@ParameterObject RealtimeVideoReq req) {
|
||||
return wvpService.realtimeVideoUrl(req.getDeviceCode());
|
||||
}
|
||||
|
||||
@Operation(summary = "关闭实时视频")
|
||||
@GetMapping(value = "/device/realtime/close")
|
||||
@ResponseBody
|
||||
public JsonResponse<Void> close(@ParameterObject RealtimeVideoReq req) {
|
||||
wvpService.closeRealtimeVideo(req.getDeviceCode());
|
||||
return JsonResponse.success(null);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.video.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class RealtimeVideoReq {
|
||||
@NotBlank(message = "设备编码 不能为空")
|
||||
@Schema(description = "设备编码")
|
||||
private String deviceCode;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.video.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class VideoMp4Req {
|
||||
|
||||
@NotBlank(message = "设备编码 不能为空")
|
||||
@Schema(description = "设备编码")
|
||||
private String deviceCode;
|
||||
|
||||
@Schema(description = "开始时间 (毫秒) (proxy.gb28181.device-api.offset 可额外设置偏移量)",example = "1695593100207")
|
||||
@NotBlank(message = "开始时间 不能为空")
|
||||
private Long startTime;
|
||||
|
||||
@Schema(description = "结束时间 (毫秒) (proxy.gb28181.device-api.offset 可额外设置偏移量)",example = "1695593190207")
|
||||
@NotBlank(message = "结束时间 不能为空")
|
||||
private Long endTime;
|
||||
|
||||
@Schema(description = "http 头是否需要文件名 (没有文件名时浏览器会试图直接播放,会导致短时间内重复访问同一设备,导致失败)")
|
||||
private Boolean fileHeader = true;
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package cn.skcks.docking.gb28181.wvp.api.video.dto;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class VideoReq {
|
||||
|
||||
@NotBlank(message = "设备编码 不能为空")
|
||||
@Schema(description = "设备编码 (支持参数名: deviceCode device_code deviceId device_id)")
|
||||
private String deviceCode;
|
||||
|
||||
@Schema(description = "开始时间 (支持参数名: startTime start_time beginTime begin_time)",example = "20230909000000")
|
||||
@NotBlank(message = "开始时间 不能为空")
|
||||
@DateTimeFormat(pattern= DatePattern.PURE_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.PURE_DATETIME_PATTERN)
|
||||
private Date startTime;
|
||||
|
||||
@Schema(description = "结束时间 (支持参数名: endTime end_time endTime end_time)",example = "20230909000500")
|
||||
@NotBlank(message = "结束时间 不能为空")
|
||||
@DateTimeFormat(pattern= DatePattern.PURE_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.PURE_DATETIME_PATTERN)
|
||||
private Date endTime;
|
||||
|
||||
@Schema(description = "http 头是否需要文件名 (没有文件名时浏览器会试图直接播放,会导致短时间内重复访问同一设备,导致失败)")
|
||||
private Boolean fileHeader = true;
|
||||
|
||||
@Schema(description = "使用哪种方式拉取历史视频 (true 为 使用 Download 方式拉取 4倍速流, false 为 使用 Playback 原始速率拉取 视频回放)")
|
||||
private Boolean useDownload = true;
|
||||
|
||||
public void setDevice_id(String deviceCode){
|
||||
this.deviceCode = deviceCode;
|
||||
}
|
||||
|
||||
public void setDevice_code(String deviceCode){
|
||||
this.deviceCode = deviceCode;
|
||||
}
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.PURE_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.PURE_DATETIME_PATTERN)
|
||||
public void setBegin_time(Date startTime){
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.PURE_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.PURE_DATETIME_PATTERN)
|
||||
public void setStart_time(Date startTime){
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.PURE_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.PURE_DATETIME_PATTERN)
|
||||
public void setEnd_time(Date endTime){
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public void setUse_download(Boolean useDownload){
|
||||
this.useDownload = useDownload;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package cn.skcks.docking.gb28181.wvp.common.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public class PageDTO {
|
||||
@Schema(description = "页数", example = "10")
|
||||
@NotNull(message = "page 不能为空")
|
||||
@Min(value = 1, message = "page 必须为正整数")
|
||||
int page;
|
||||
|
||||
@Schema(description = "每页条数", example = "10")
|
||||
@NotNull(message = "size 不能为空")
|
||||
@Min(value = 1, message = "size 必须为正整数")
|
||||
int size;
|
||||
}
|
@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@ -19,4 +20,14 @@ public class WebConfig implements WebMvcConfigurer {
|
||||
.excludePathPatterns("/swagger-ui/**","/v3/api-docs/**")
|
||||
.addPathPatterns("/**");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry corsRegistry){
|
||||
corsRegistry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowCredentials(true)
|
||||
.allowedMethods("GET","POST","HEAD","DELETE","PUT")
|
||||
.allowedHeaders("*")
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper;
|
||||
|
||||
import static cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDeviceDynamicSqlSupport.*;
|
||||
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
|
||||
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
@ -31,7 +32,7 @@ import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils;
|
||||
@Mapper
|
||||
public interface WvpProxyDeviceMapper extends CommonCountMapper, CommonDeleteMapper, CommonInsertMapper<WvpProxyDevice>, CommonUpdateMapper {
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
BasicColumn[] selectList = BasicColumn.columnList(WvpProxyDeviceDynamicSqlSupport.id, WvpProxyDeviceDynamicSqlSupport.deviceCode, WvpProxyDeviceDynamicSqlSupport.gbDeviceId, WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId, WvpProxyDeviceDynamicSqlSupport.name, WvpProxyDeviceDynamicSqlSupport.address);
|
||||
BasicColumn[] selectList = BasicColumn.columnList(id, deviceCode, gbDeviceId, gbDeviceChannelId, name, address);
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
@SelectProvider(type=SqlProviderAdapter.class, method="select")
|
||||
@ -52,125 +53,125 @@ public interface WvpProxyDeviceMapper extends CommonCountMapper, CommonDeleteMap
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default long count(CountDSLCompleter completer) {
|
||||
return MyBatis3Utils.countFrom(this::count, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
|
||||
return MyBatis3Utils.countFrom(this::count, wvpProxyDevice, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default int delete(DeleteDSLCompleter completer) {
|
||||
return MyBatis3Utils.deleteFrom(this::delete, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
|
||||
return MyBatis3Utils.deleteFrom(this::delete, wvpProxyDevice, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default int deleteByPrimaryKey(Long id_) {
|
||||
return delete(c ->
|
||||
c.where(WvpProxyDeviceDynamicSqlSupport.id, isEqualTo(id_))
|
||||
c.where(id, isEqualTo(id_))
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default int insert(WvpProxyDevice row) {
|
||||
return MyBatis3Utils.insert(this::insert, row, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, c ->
|
||||
c.map(WvpProxyDeviceDynamicSqlSupport.id).toProperty("id")
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.deviceCode).toProperty("deviceCode")
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).toProperty("gbDeviceId")
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).toProperty("gbDeviceChannelId")
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.name).toProperty("name")
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.address).toProperty("address")
|
||||
return MyBatis3Utils.insert(this::insert, row, wvpProxyDevice, c ->
|
||||
c.map(id).toProperty("id")
|
||||
.map(deviceCode).toProperty("deviceCode")
|
||||
.map(gbDeviceId).toProperty("gbDeviceId")
|
||||
.map(gbDeviceChannelId).toProperty("gbDeviceChannelId")
|
||||
.map(name).toProperty("name")
|
||||
.map(address).toProperty("address")
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default int insertMultiple(Collection<WvpProxyDevice> records) {
|
||||
return MyBatis3Utils.insertMultiple(this::insertMultiple, records, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, c ->
|
||||
c.map(WvpProxyDeviceDynamicSqlSupport.id).toProperty("id")
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.deviceCode).toProperty("deviceCode")
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).toProperty("gbDeviceId")
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).toProperty("gbDeviceChannelId")
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.name).toProperty("name")
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.address).toProperty("address")
|
||||
return MyBatis3Utils.insertMultiple(this::insertMultiple, records, wvpProxyDevice, c ->
|
||||
c.map(id).toProperty("id")
|
||||
.map(deviceCode).toProperty("deviceCode")
|
||||
.map(gbDeviceId).toProperty("gbDeviceId")
|
||||
.map(gbDeviceChannelId).toProperty("gbDeviceChannelId")
|
||||
.map(name).toProperty("name")
|
||||
.map(address).toProperty("address")
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default int insertSelective(WvpProxyDevice row) {
|
||||
return MyBatis3Utils.insert(this::insert, row, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, c ->
|
||||
c.map(WvpProxyDeviceDynamicSqlSupport.id).toPropertyWhenPresent("id", row::getId)
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.deviceCode).toPropertyWhenPresent("deviceCode", row::getDeviceCode)
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).toPropertyWhenPresent("gbDeviceId", row::getGbDeviceId)
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).toPropertyWhenPresent("gbDeviceChannelId", row::getGbDeviceChannelId)
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.name).toPropertyWhenPresent("name", row::getName)
|
||||
.map(WvpProxyDeviceDynamicSqlSupport.address).toPropertyWhenPresent("address", row::getAddress)
|
||||
return MyBatis3Utils.insert(this::insert, row, wvpProxyDevice, c ->
|
||||
c.map(id).toPropertyWhenPresent("id", row::getId)
|
||||
.map(deviceCode).toPropertyWhenPresent("deviceCode", row::getDeviceCode)
|
||||
.map(gbDeviceId).toPropertyWhenPresent("gbDeviceId", row::getGbDeviceId)
|
||||
.map(gbDeviceChannelId).toPropertyWhenPresent("gbDeviceChannelId", row::getGbDeviceChannelId)
|
||||
.map(name).toPropertyWhenPresent("name", row::getName)
|
||||
.map(address).toPropertyWhenPresent("address", row::getAddress)
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default Optional<WvpProxyDevice> selectOne(SelectDSLCompleter completer) {
|
||||
return MyBatis3Utils.selectOne(this::selectOne, selectList, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
|
||||
return MyBatis3Utils.selectOne(this::selectOne, selectList, wvpProxyDevice, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default List<WvpProxyDevice> select(SelectDSLCompleter completer) {
|
||||
return MyBatis3Utils.selectList(this::selectMany, selectList, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
|
||||
return MyBatis3Utils.selectList(this::selectMany, selectList, wvpProxyDevice, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default List<WvpProxyDevice> selectDistinct(SelectDSLCompleter completer) {
|
||||
return MyBatis3Utils.selectDistinct(this::selectMany, selectList, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
|
||||
return MyBatis3Utils.selectDistinct(this::selectMany, selectList, wvpProxyDevice, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default Optional<WvpProxyDevice> selectByPrimaryKey(Long id_) {
|
||||
return selectOne(c ->
|
||||
c.where(WvpProxyDeviceDynamicSqlSupport.id, isEqualTo(id_))
|
||||
c.where(id, isEqualTo(id_))
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default int update(UpdateDSLCompleter completer) {
|
||||
return MyBatis3Utils.update(this::update, WvpProxyDeviceDynamicSqlSupport.wvpProxyDevice, completer);
|
||||
return MyBatis3Utils.update(this::update, wvpProxyDevice, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
static UpdateDSL<UpdateModel> updateAllColumns(WvpProxyDevice row, UpdateDSL<UpdateModel> dsl) {
|
||||
return dsl.set(WvpProxyDeviceDynamicSqlSupport.id).equalTo(row::getId)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.deviceCode).equalTo(row::getDeviceCode)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).equalTo(row::getGbDeviceId)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).equalTo(row::getGbDeviceChannelId)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.name).equalTo(row::getName)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.address).equalTo(row::getAddress);
|
||||
return dsl.set(id).equalTo(row::getId)
|
||||
.set(deviceCode).equalTo(row::getDeviceCode)
|
||||
.set(gbDeviceId).equalTo(row::getGbDeviceId)
|
||||
.set(gbDeviceChannelId).equalTo(row::getGbDeviceChannelId)
|
||||
.set(name).equalTo(row::getName)
|
||||
.set(address).equalTo(row::getAddress);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
static UpdateDSL<UpdateModel> updateSelectiveColumns(WvpProxyDevice row, UpdateDSL<UpdateModel> dsl) {
|
||||
return dsl.set(WvpProxyDeviceDynamicSqlSupport.id).equalToWhenPresent(row::getId)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.deviceCode).equalToWhenPresent(row::getDeviceCode)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).equalToWhenPresent(row::getGbDeviceChannelId)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.name).equalToWhenPresent(row::getName)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.address).equalToWhenPresent(row::getAddress);
|
||||
return dsl.set(id).equalToWhenPresent(row::getId)
|
||||
.set(deviceCode).equalToWhenPresent(row::getDeviceCode)
|
||||
.set(gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
|
||||
.set(gbDeviceChannelId).equalToWhenPresent(row::getGbDeviceChannelId)
|
||||
.set(name).equalToWhenPresent(row::getName)
|
||||
.set(address).equalToWhenPresent(row::getAddress);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default int updateByPrimaryKey(WvpProxyDevice row) {
|
||||
return update(c ->
|
||||
c.set(WvpProxyDeviceDynamicSqlSupport.deviceCode).equalTo(row::getDeviceCode)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).equalTo(row::getGbDeviceId)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).equalTo(row::getGbDeviceChannelId)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.name).equalTo(row::getName)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.address).equalTo(row::getAddress)
|
||||
.where(WvpProxyDeviceDynamicSqlSupport.id, isEqualTo(row::getId))
|
||||
c.set(deviceCode).equalTo(row::getDeviceCode)
|
||||
.set(gbDeviceId).equalTo(row::getGbDeviceId)
|
||||
.set(gbDeviceChannelId).equalTo(row::getGbDeviceChannelId)
|
||||
.set(name).equalTo(row::getName)
|
||||
.set(address).equalTo(row::getAddress)
|
||||
.where(id, isEqualTo(row::getId))
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_device")
|
||||
default int updateByPrimaryKeySelective(WvpProxyDevice row) {
|
||||
return update(c ->
|
||||
c.set(WvpProxyDeviceDynamicSqlSupport.deviceCode).equalToWhenPresent(row::getDeviceCode)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).equalToWhenPresent(row::getGbDeviceChannelId)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.name).equalToWhenPresent(row::getName)
|
||||
.set(WvpProxyDeviceDynamicSqlSupport.address).equalToWhenPresent(row::getAddress)
|
||||
.where(WvpProxyDeviceDynamicSqlSupport.id, isEqualTo(row::getId))
|
||||
c.set(deviceCode).equalToWhenPresent(row::getDeviceCode)
|
||||
.set(gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
|
||||
.set(gbDeviceChannelId).equalToWhenPresent(row::getGbDeviceChannelId)
|
||||
.set(name).equalToWhenPresent(row::getName)
|
||||
.set(address).equalToWhenPresent(row::getAddress)
|
||||
.where(id, isEqualTo(row::getId))
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper;
|
||||
|
||||
import jakarta.annotation.Generated;
|
||||
import java.sql.JDBCType;
|
||||
import org.mybatis.dynamic.sql.AliasableSqlTable;
|
||||
import org.mybatis.dynamic.sql.SqlColumn;
|
||||
|
||||
public final class WvpProxyDockingDynamicSqlSupport {
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
public static final WvpProxyDocking wvpProxyDocking = new WvpProxyDocking();
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.id")
|
||||
public static final SqlColumn<Long> id = wvpProxyDocking.id;
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.gb_device_id")
|
||||
public static final SqlColumn<String> gbDeviceId = wvpProxyDocking.gbDeviceId;
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.ip")
|
||||
public static final SqlColumn<String> ip = wvpProxyDocking.ip;
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.port")
|
||||
public static final SqlColumn<String> port = wvpProxyDocking.port;
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
public static final class WvpProxyDocking extends AliasableSqlTable<WvpProxyDocking> {
|
||||
public final SqlColumn<Long> id = column("id", JDBCType.BIGINT);
|
||||
|
||||
public final SqlColumn<String> gbDeviceId = column("gb_device_id", JDBCType.VARCHAR);
|
||||
|
||||
public final SqlColumn<String> ip = column("ip", JDBCType.VARCHAR);
|
||||
|
||||
public final SqlColumn<String> port = column("port", JDBCType.VARCHAR);
|
||||
|
||||
public WvpProxyDocking() {
|
||||
super("wvp_proxy_docking", WvpProxyDocking::new);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
package cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper;
|
||||
|
||||
import static cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDockingDynamicSqlSupport.*;
|
||||
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
|
||||
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import jakarta.annotation.Generated;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Result;
|
||||
import org.apache.ibatis.annotations.ResultMap;
|
||||
import org.apache.ibatis.annotations.Results;
|
||||
import org.apache.ibatis.annotations.SelectProvider;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.mybatis.dynamic.sql.BasicColumn;
|
||||
import org.mybatis.dynamic.sql.delete.DeleteDSLCompleter;
|
||||
import org.mybatis.dynamic.sql.select.CountDSLCompleter;
|
||||
import org.mybatis.dynamic.sql.select.SelectDSLCompleter;
|
||||
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
|
||||
import org.mybatis.dynamic.sql.update.UpdateDSL;
|
||||
import org.mybatis.dynamic.sql.update.UpdateDSLCompleter;
|
||||
import org.mybatis.dynamic.sql.update.UpdateModel;
|
||||
import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
|
||||
import org.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper;
|
||||
import org.mybatis.dynamic.sql.util.mybatis3.CommonDeleteMapper;
|
||||
import org.mybatis.dynamic.sql.util.mybatis3.CommonInsertMapper;
|
||||
import org.mybatis.dynamic.sql.util.mybatis3.CommonUpdateMapper;
|
||||
import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils;
|
||||
|
||||
@Mapper
|
||||
public interface WvpProxyDockingMapper extends CommonCountMapper, CommonDeleteMapper, CommonInsertMapper<WvpProxyDocking>, CommonUpdateMapper {
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
BasicColumn[] selectList = BasicColumn.columnList(id, gbDeviceId, ip, port);
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
@SelectProvider(type=SqlProviderAdapter.class, method="select")
|
||||
@Results(id="WvpProxyDockingResult", value = {
|
||||
@Result(column="id", property="id", jdbcType=JdbcType.BIGINT, id=true),
|
||||
@Result(column="gb_device_id", property="gbDeviceId", jdbcType=JdbcType.VARCHAR),
|
||||
@Result(column="ip", property="ip", jdbcType=JdbcType.VARCHAR),
|
||||
@Result(column="port", property="port", jdbcType=JdbcType.VARCHAR)
|
||||
})
|
||||
List<WvpProxyDocking> selectMany(SelectStatementProvider selectStatement);
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
@SelectProvider(type=SqlProviderAdapter.class, method="select")
|
||||
@ResultMap("WvpProxyDockingResult")
|
||||
Optional<WvpProxyDocking> selectOne(SelectStatementProvider selectStatement);
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default long count(CountDSLCompleter completer) {
|
||||
return MyBatis3Utils.countFrom(this::count, wvpProxyDocking, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default int delete(DeleteDSLCompleter completer) {
|
||||
return MyBatis3Utils.deleteFrom(this::delete, wvpProxyDocking, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default int deleteByPrimaryKey(Long id_) {
|
||||
return delete(c ->
|
||||
c.where(id, isEqualTo(id_))
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default int insert(WvpProxyDocking row) {
|
||||
return MyBatis3Utils.insert(this::insert, row, wvpProxyDocking, c ->
|
||||
c.map(id).toProperty("id")
|
||||
.map(gbDeviceId).toProperty("gbDeviceId")
|
||||
.map(ip).toProperty("ip")
|
||||
.map(port).toProperty("port")
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default int insertMultiple(Collection<WvpProxyDocking> records) {
|
||||
return MyBatis3Utils.insertMultiple(this::insertMultiple, records, wvpProxyDocking, c ->
|
||||
c.map(id).toProperty("id")
|
||||
.map(gbDeviceId).toProperty("gbDeviceId")
|
||||
.map(ip).toProperty("ip")
|
||||
.map(port).toProperty("port")
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default int insertSelective(WvpProxyDocking row) {
|
||||
return MyBatis3Utils.insert(this::insert, row, wvpProxyDocking, c ->
|
||||
c.map(id).toPropertyWhenPresent("id", row::getId)
|
||||
.map(gbDeviceId).toPropertyWhenPresent("gbDeviceId", row::getGbDeviceId)
|
||||
.map(ip).toPropertyWhenPresent("ip", row::getIp)
|
||||
.map(port).toPropertyWhenPresent("port", row::getPort)
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default Optional<WvpProxyDocking> selectOne(SelectDSLCompleter completer) {
|
||||
return MyBatis3Utils.selectOne(this::selectOne, selectList, wvpProxyDocking, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default List<WvpProxyDocking> select(SelectDSLCompleter completer) {
|
||||
return MyBatis3Utils.selectList(this::selectMany, selectList, wvpProxyDocking, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default List<WvpProxyDocking> selectDistinct(SelectDSLCompleter completer) {
|
||||
return MyBatis3Utils.selectDistinct(this::selectMany, selectList, wvpProxyDocking, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default Optional<WvpProxyDocking> selectByPrimaryKey(Long id_) {
|
||||
return selectOne(c ->
|
||||
c.where(id, isEqualTo(id_))
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default int update(UpdateDSLCompleter completer) {
|
||||
return MyBatis3Utils.update(this::update, wvpProxyDocking, completer);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
static UpdateDSL<UpdateModel> updateAllColumns(WvpProxyDocking row, UpdateDSL<UpdateModel> dsl) {
|
||||
return dsl.set(id).equalTo(row::getId)
|
||||
.set(gbDeviceId).equalTo(row::getGbDeviceId)
|
||||
.set(ip).equalTo(row::getIp)
|
||||
.set(port).equalTo(row::getPort);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
static UpdateDSL<UpdateModel> updateSelectiveColumns(WvpProxyDocking row, UpdateDSL<UpdateModel> dsl) {
|
||||
return dsl.set(id).equalToWhenPresent(row::getId)
|
||||
.set(gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
|
||||
.set(ip).equalToWhenPresent(row::getIp)
|
||||
.set(port).equalToWhenPresent(row::getPort);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default int updateByPrimaryKey(WvpProxyDocking row) {
|
||||
return update(c ->
|
||||
c.set(gbDeviceId).equalTo(row::getGbDeviceId)
|
||||
.set(ip).equalTo(row::getIp)
|
||||
.set(port).equalTo(row::getPort)
|
||||
.where(id, isEqualTo(row::getId))
|
||||
);
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source Table: wvp_proxy_docking")
|
||||
default int updateByPrimaryKeySelective(WvpProxyDocking row) {
|
||||
return update(c ->
|
||||
c.set(gbDeviceId).equalToWhenPresent(row::getGbDeviceId)
|
||||
.set(ip).equalToWhenPresent(row::getIp)
|
||||
.set(port).equalToWhenPresent(row::getPort)
|
||||
.where(id, isEqualTo(row::getId))
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model;
|
||||
|
||||
import jakarta.annotation.Generated;
|
||||
|
||||
/**
|
||||
*
|
||||
* This class was generated by MyBatis Generator.
|
||||
* This class corresponds to the database table wvp_proxy_docking
|
||||
*/
|
||||
public class WvpProxyDocking {
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.id")
|
||||
private Long id;
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.gb_device_id")
|
||||
private String gbDeviceId;
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.ip")
|
||||
private String ip;
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.port")
|
||||
private String port;
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.id")
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.id")
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.gb_device_id")
|
||||
public String getGbDeviceId() {
|
||||
return gbDeviceId;
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.gb_device_id")
|
||||
public void setGbDeviceId(String gbDeviceId) {
|
||||
this.gbDeviceId = gbDeviceId == null ? null : gbDeviceId.trim();
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.ip")
|
||||
public String getIp() {
|
||||
return ip;
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.ip")
|
||||
public void setIp(String ip) {
|
||||
this.ip = ip == null ? null : ip.trim();
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.port")
|
||||
public String getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@Generated(value="org.mybatis.generator.api.MyBatisGenerator", comments="Source field: wvp_proxy_docking.port")
|
||||
public void setPort(String port) {
|
||||
this.port = port == null ? null : port.trim();
|
||||
}
|
||||
}
|
@ -6,4 +6,6 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
public interface WvpProxyOperateTableMapper {
|
||||
// int createNewTable(@Param("tableName")String tableName);
|
||||
void createDeviceTable();
|
||||
|
||||
void createDockingTable();
|
||||
}
|
||||
|
@ -6,18 +6,28 @@
|
||||
<update id="createDeviceTable">
|
||||
CREATE TABLE IF NOT EXISTS `wvp_proxy_device`
|
||||
(
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`device_code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`gb_device_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`gb_device_channel_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `gb_device_id` (`gb_device_id`),
|
||||
UNIQUE KEY `device_code` (`device_code`),
|
||||
UNIQUE KEY `gb_device_id_2` (`gb_device_id`, `gb_device_channel_id`)
|
||||
UNIQUE KEY `device_code_2` (`device_code`, `gb_device_id`,
|
||||
`gb_device_channel_id`),
|
||||
KEY `device_code` (`device_code`) USING BTREE
|
||||
) ENGINE = InnoDB
|
||||
AUTO_INCREMENT = 6
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
</update>
|
||||
<update id="createDockingTable">
|
||||
CREATE TABLE IF NOT EXISTS `wvp_proxy_docking` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`gb_device_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`ip` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`port` varchar(5) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
</update>
|
||||
</mapper>
|
||||
|
@ -29,6 +29,11 @@
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.skcks.docking.gb28181</groupId>
|
||||
<artifactId>gb28181-service</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.skcks.docking</groupId>
|
||||
<artifactId>zlmediakit-service</artifactId>
|
||||
@ -168,6 +173,47 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-httpclient</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>32.1.2-jre</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.rholder</groupId>
|
||||
<artifactId>guava-retrying</artifactId>
|
||||
<version>2.0.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-exec -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-exec</artifactId>
|
||||
<version>1.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
<version>4.4</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -0,0 +1,36 @@
|
||||
package cn.skcks.docking.gb28181.wvp.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@ConfigurationProperties(prefix = "ffmpeg-support")
|
||||
@Configuration
|
||||
@Data
|
||||
public class FfmpegConfig {
|
||||
private String ffmpeg;
|
||||
private String ffprobe;
|
||||
|
||||
private Rtp rtp;
|
||||
|
||||
@Data
|
||||
public static class Rtp {
|
||||
private String download = "-i";
|
||||
private String input = "-re -i";
|
||||
private String output = "-vcodec h264 -acodec aac -f mp4";
|
||||
private String logLevel = "fatal";
|
||||
}
|
||||
|
||||
|
||||
private Debug debug;
|
||||
|
||||
@Data
|
||||
public static class Debug {
|
||||
private Boolean download = false;
|
||||
private Boolean input = false;
|
||||
private Boolean output = false;
|
||||
}
|
||||
|
||||
private Boolean useTmpFile = true;
|
||||
private String tmpDir = System.getProperty("java.io.tmpdir");
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package cn.skcks.docking.gb28181.wvp.config;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "proxy.gb28181.device-api", ignoreInvalidFields = true)
|
||||
@Order(0)
|
||||
@Data
|
||||
public class Gb28181DeviceVideoApiConfig {
|
||||
private Offset offset = new Offset();
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Offset {
|
||||
private Duration forward = Duration.of(0, ChronoUnit.SECONDS);
|
||||
private Duration back= Duration.of(0, ChronoUnit.SECONDS);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package cn.skcks.docking.gb28181.wvp.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "media")
|
||||
public class MediaRtmpConfig {
|
||||
private int rtmpPort = 1935;
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package cn.skcks.docking.gb28181.wvp.config;
|
||||
|
||||
import cn.skcks.docking.gb28181.config.sip.SipConfig;
|
||||
import cn.skcks.docking.gb28181.sdp.media.MediaStreamMode;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sip.ListeningPoint;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "proxy.gb28181.sip", ignoreInvalidFields = true)
|
||||
@Order(0)
|
||||
@Data
|
||||
public class ProxySipConfig {
|
||||
private List<String> ip;
|
||||
|
||||
private List<String> showIp;
|
||||
|
||||
private Integer port;
|
||||
|
||||
private String domain;
|
||||
|
||||
private String id;
|
||||
|
||||
private String password;
|
||||
|
||||
private String transport = ListeningPoint.UDP;
|
||||
|
||||
private MediaStreamMode streamMode = MediaStreamMode.TCP_PASSIVE;
|
||||
|
||||
/**
|
||||
* 某些特殊情况下 使用 视频回放点播 代替 下载
|
||||
*/
|
||||
private boolean usePlaybackToDownload = false;
|
||||
|
||||
/**
|
||||
* 代理 zlm 地址, 用于 /video/device/video.mp4 替换返回值地址
|
||||
* <p>例: http://127.0.0.1:5080</p>
|
||||
*/
|
||||
private String proxyMediaUrl = "";
|
||||
|
||||
/**
|
||||
* 调用 视频下载之前 是否使用 recordInfo 查询
|
||||
*/
|
||||
private boolean useRecordInfoQueryBeforeDownload = true;
|
||||
private int retryRecordInfoQueryBeforeDownloadTimes = 20;
|
||||
private long retryRecordInfoQueryBeforeDownloadInterval = 3;
|
||||
private TimeUnit retryRecordInfoQueryBeforeDownloadIntervalUnit = TimeUnit.SECONDS;
|
||||
|
||||
@Bean
|
||||
public SipConfig sipConfig(){
|
||||
SipConfig sipConfig = new SipConfig();
|
||||
sipConfig.setIp(ip);
|
||||
sipConfig.setShowIp(showIp);
|
||||
sipConfig.setPort(port);
|
||||
sipConfig.setDomain(domain);
|
||||
sipConfig.setId(id);
|
||||
sipConfig.setPassword(password);
|
||||
return sipConfig;
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package cn.skcks.docking.gb28181.wvp.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "report")
|
||||
public class ReportConfig {
|
||||
private Boolean enabled = false;
|
||||
private String url;
|
||||
private Map<String, String> customHeaders = new HashMap<>();
|
||||
}
|
@ -4,11 +4,40 @@ import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "proxy.wvp")
|
||||
@Data
|
||||
public class WvpProxyConfig {
|
||||
private Boolean enable = true;
|
||||
private String url;
|
||||
private String user;
|
||||
private String passwd;
|
||||
/**
|
||||
* 是否尝试通过 wvp-assist 服务下载
|
||||
*/
|
||||
private Boolean useWvpAssist = true;
|
||||
|
||||
/**
|
||||
* 是否使用 ffmpeg 编/解码, 否则使用内置 javacv
|
||||
*/
|
||||
private Boolean useFfmpeg = false;
|
||||
|
||||
/**
|
||||
* 需要通过 wvp 代理的 (wvp的上级) 上级平台
|
||||
*/
|
||||
private List<String> parents = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 用于生成 代理 wvp 的 视频流 ws-flv 地址
|
||||
*/
|
||||
private String proxyMediaUrl = "";
|
||||
|
||||
/**
|
||||
* 实时视频单次点播持续时间 (默认: 15分钟)
|
||||
*/
|
||||
private Duration realtimeVideoDuration = Duration.ofMinutes(15);
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
package cn.skcks.docking.gb28181.wvp.dto.common;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class GeneralTimeReq {
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String startTime;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String endTime;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package cn.skcks.docking.gb28181.wvp.dto.download;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class DownloadStartReq {
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String startTime;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String endTime;
|
||||
|
||||
@Builder.Default
|
||||
private int downloadSpeed = 4;
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package cn.skcks.docking.gb28181.wvp.dto.download;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class DownloadStartResp {
|
||||
private String app;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String endTime;
|
||||
|
||||
private String flv;
|
||||
private String fmp4;
|
||||
private String hls;
|
||||
private String httpsFlv;
|
||||
private String httpsFmp4;
|
||||
private String httpsHls;
|
||||
private String httpsTs;
|
||||
private String mediaServerId;
|
||||
private Double progress;
|
||||
private String rtc;
|
||||
private String rtcs;
|
||||
private String rtmp;
|
||||
private String rtsp;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String startTime;
|
||||
private String stream;
|
||||
private List<TracksDTO> tracks;
|
||||
private String ts;
|
||||
private String wsFlv;
|
||||
private String wsFmp4;
|
||||
private String wsHls;
|
||||
private String wsTs;
|
||||
private String wssFlv;
|
||||
private String wssFmp4;
|
||||
private String wssHls;
|
||||
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public static class TracksDTO {
|
||||
private Integer channels;
|
||||
private Integer codecId;
|
||||
private Integer codecType;
|
||||
private Integer fps;
|
||||
private Integer height;
|
||||
private Boolean ready;
|
||||
private Integer sampleBit;
|
||||
private Integer sampleRate;
|
||||
private Integer width;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package cn.skcks.docking.gb28181.wvp.dto.media.proxy;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class AddDownloadTaskReq {
|
||||
/**
|
||||
* 应用id
|
||||
* <p>WvpProxyClient.downloadStart 或 WvpProxyClient.downloadProgress 返回值中获取的 app 字段</p>
|
||||
*/
|
||||
private String app;
|
||||
|
||||
/**
|
||||
* 流id
|
||||
* <p>WvpProxyClient.downloadStart 或 WvpProxyClient.downloadProgress 返回值中获取的 stream 字段</p>
|
||||
*/
|
||||
private String stream;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package cn.skcks.docking.gb28181.wvp.dto.media.proxy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class GetDownloadTaskReq {
|
||||
/**
|
||||
* 应用id
|
||||
* <p>WvpProxyClient.downloadStart 或 WvpProxyClient.downloadProgress 返回值中获取的 app 字段</p>
|
||||
*/
|
||||
private String app;
|
||||
|
||||
/**
|
||||
* 流id
|
||||
* <p>WvpProxyClient.downloadStart 或 WvpProxyClient.downloadProgress 返回值中获取的 stream 字段</p>
|
||||
*/
|
||||
private String stream;
|
||||
|
||||
private String taskId;
|
||||
|
||||
@JsonProperty("isEnd")
|
||||
private Boolean isEnd;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package cn.skcks.docking.gb28181.wvp.dto.media.proxy;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class GetDownloadTaskResp {
|
||||
private String id;
|
||||
private String app;
|
||||
private String stream;
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String startTime;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String endTime;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String createTime;
|
||||
private String percentage;
|
||||
private String recordFile;
|
||||
private String downloadFile;
|
||||
private String playFile;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package cn.skcks.docking.gb28181.wvp.dto.record;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class QueryRecordReq {
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String startTime;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String endTime;
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package cn.skcks.docking.gb28181.wvp.dto.record;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class QueryRecordResp {
|
||||
|
||||
private String channelId;
|
||||
private Integer count;
|
||||
private String deviceId;
|
||||
private String name;
|
||||
private List<RecordListDTO> recordList;
|
||||
private String sn;
|
||||
private Integer sumNum;
|
||||
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public static class RecordListDTO {
|
||||
private String deviceId;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String endTime;
|
||||
private String filePath;
|
||||
private String name;
|
||||
private Integer secrecy;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String startTime;
|
||||
private String type;
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package cn.skcks.docking.gb28181.wvp.dto.report;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Schema(description = "上报信息")
|
||||
public class ReportReq {
|
||||
@Schema(description = "上报消息id")
|
||||
private String id;
|
||||
@Schema(description = "设备编码")
|
||||
private String deviceCode;
|
||||
@Schema(description = "设备id/通道id")
|
||||
private String deviceId;
|
||||
@Schema(description = "点播时长")
|
||||
private String durationTime;
|
||||
@Schema(description = "点播时长")
|
||||
private TimeRange timeRange;
|
||||
|
||||
@Schema(description = "日志记录时间")
|
||||
private String logTime;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public static class TimeRange {
|
||||
@Schema(description = "开始时间")
|
||||
private String startTime;
|
||||
|
||||
@Schema(description = "结束时间")
|
||||
private String endTime;
|
||||
}
|
||||
|
||||
@Schema(description = "文件大小, 未知大小为 -1")
|
||||
private String fileSize;
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package cn.skcks.docking.gb28181.wvp.dto.stream;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class StreamContent {
|
||||
private String app;
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String endTime;
|
||||
private String flv;
|
||||
private String fmp4;
|
||||
private String hls;
|
||||
private String httpsFlv;
|
||||
private String httpsFmp4;
|
||||
private String httpsHls;
|
||||
private String httpsTs;
|
||||
private String mediaServerId;
|
||||
private Double progress;
|
||||
private String rtc;
|
||||
private String rtcs;
|
||||
private String rtmp;
|
||||
private String rtsp;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN)
|
||||
private String startTime;
|
||||
private String stream;
|
||||
private List<TracksDTO> tracks;
|
||||
private String ts;
|
||||
private String wsFlv;
|
||||
private String wsFmp4;
|
||||
private String wsHls;
|
||||
private String wsTs;
|
||||
private String wssFlv;
|
||||
private String wssFmp4;
|
||||
private String wssHls;
|
||||
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public static class TracksDTO {
|
||||
private Integer channels;
|
||||
private Integer codecId;
|
||||
private Integer codecType;
|
||||
private Integer fps;
|
||||
private Integer height;
|
||||
private Boolean ready;
|
||||
private Integer sampleBit;
|
||||
private Integer sampleRate;
|
||||
private Integer width;
|
||||
}
|
||||
}
|
@ -24,5 +24,6 @@ public class WvpProxyOrmInitService {
|
||||
public void init(){
|
||||
log.info("[orm] 自动建表");
|
||||
mapper.createDeviceTable();
|
||||
mapper.createDockingTable();
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,126 @@
|
||||
package cn.skcks.docking.gb28181.wvp.proxy;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.common.GeneralTimeReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsResp;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceTreeReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceTreeResp;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.download.DownloadStartReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginResp;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.AddDownloadTaskReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.GetDownloadTaskReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.GetDownloadTaskResp;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.record.QueryRecordReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.record.QueryRecordResp;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.stream.StreamContent;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.SpringQueryMap;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@FeignClient(name="wvpProxyClient", url = "${proxy.wvp.url}")
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name = "wvpProxyClient", url = "${proxy.wvp.url}")
|
||||
public interface WvpProxyClient {
|
||||
@GetMapping(value = "/api/user/login", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
JsonResponse<WvpLoginResp> login(@SpringQueryMap WvpLoginReq req);
|
||||
|
||||
@GetMapping(value = "/api/device/query/tree/channel/{deviceId}")
|
||||
JsonResponse<GetDeviceTreeResp> getDeviceTree(@RequestHeader("access-token") String token, @PathVariable String deviceId, @SpringQueryMap GetDeviceTreeReq req);
|
||||
@GetMapping("/api/device/query/tree/channel/{deviceId}")
|
||||
JsonResponse<GetDeviceTreeResp> getDeviceTree(@RequestHeader("access-token") String token,
|
||||
@PathVariable String deviceId,
|
||||
@SpringQueryMap GetDeviceTreeReq req);
|
||||
|
||||
@GetMapping(value = "/api/device/query/devices/{deviceId}/channels")
|
||||
JsonResponse<GetDeviceChannelsResp> getDeviceChannels(@RequestHeader("access-token") String token, @PathVariable String deviceId, @SpringQueryMap GetDeviceChannelsReq req);
|
||||
@GetMapping("/api/device/query/devices/{deviceId}/channels")
|
||||
JsonResponse<GetDeviceChannelsResp> getDeviceChannels(@RequestHeader("access-token") String token,
|
||||
@PathVariable String deviceId,
|
||||
@SpringQueryMap GetDeviceChannelsReq req);
|
||||
|
||||
@GetMapping("/api/gb_record/query/{deviceId}/{channelId}")
|
||||
JsonResponse<QueryRecordResp> queryRecord(@RequestHeader("access-token") String token,
|
||||
@PathVariable String deviceId,
|
||||
@PathVariable String channelId,
|
||||
@SpringQueryMap QueryRecordReq req);
|
||||
|
||||
// /api/playback/start/44050100001180000001/44050100001310000001?startTime=2023-09-07%2000:20:01&endTime=2023-09-07%2000:25:01
|
||||
@GetMapping("/api/playback/start/{deviceId}/{channelId}")
|
||||
JsonResponse<StreamContent> playbackStart(@RequestHeader("access-token") String token,
|
||||
@PathVariable String deviceId,
|
||||
@PathVariable String channelId,
|
||||
@SpringQueryMap GeneralTimeReq req);
|
||||
|
||||
@GetMapping("/api/playback/stop/{deviceId}/{channelId}/{stream}")
|
||||
JsonResponse<StreamContent> playbackStop(@RequestHeader("access-token") String token,
|
||||
@PathVariable String deviceId,
|
||||
@PathVariable String channelId,
|
||||
@PathVariable String stream);
|
||||
|
||||
// /api/playback/stop/44050100001180000001/44050100001310000001/44050100001180000001_44050100001310000001_20230908083001_20230908083501
|
||||
@GetMapping("/api/gb_record/download/start/{deviceId}/{channelId}")
|
||||
JsonResponse<StreamContent> downloadStart(@RequestHeader("access-token") String token,
|
||||
@PathVariable String deviceId,
|
||||
@PathVariable String channelId,
|
||||
@SpringQueryMap DownloadStartReq req);
|
||||
|
||||
// /api/gb_record/download/progress/44050100001180000001/44050100001310000001/59777645
|
||||
@GetMapping("/api/gb_record/download/progress/{deviceId}/{channelId}/{stream}")
|
||||
JsonResponse<StreamContent> downloadProgress(@RequestHeader("access-token") String token,
|
||||
@PathVariable String deviceId,
|
||||
@PathVariable String channelId,
|
||||
@PathVariable String stream);
|
||||
|
||||
/**
|
||||
* 停止下载, 部分设备会出现长时间卡住不动 且 wvp 会出现 100 错误, 故调用即可无需理会返回值
|
||||
* @param token 认证token
|
||||
* @param deviceId 国标设备id/下级平台id
|
||||
* @param channelId 通道id
|
||||
* @param stream downloadStart中的 stream
|
||||
*/
|
||||
// /api/gb_record/download/stop/44050100001180000001/44050100001310000001/59777645
|
||||
@GetMapping("/api/gb_record/download/stop/{deviceId}/{channelId}/{stream}")
|
||||
JsonResponse<Void> downloadStop(@RequestHeader("access-token") String token,
|
||||
@PathVariable String deviceId,
|
||||
@PathVariable String channelId,
|
||||
@PathVariable String stream);
|
||||
|
||||
/**
|
||||
* 添加视频裁剪合并 任务 到 wvp-assist 服务, 返回 taskId 用于后续操作
|
||||
* @param token 认证token
|
||||
* @param mediaServerId 流媒体服务id, 从 downloadStart/downloadProgress 获得
|
||||
* @return 返回 taskId 用于后续操作
|
||||
*/
|
||||
// /record_proxy/your_server_id/api/record/file/download/task/add?app=rtp&stream=59777645
|
||||
@GetMapping("/record_proxy/{mediaServerId}/api/record/file/download/task/add")
|
||||
JsonResponse<String> addDownloadTask2MediaServer(@RequestHeader("access-token") String token,
|
||||
@PathVariable String mediaServerId,
|
||||
@SpringQueryMap AddDownloadTaskReq req);
|
||||
|
||||
/**
|
||||
* 获取视频裁剪合并任务列表
|
||||
* <p>如果 GetDownloadTaskResp.isEnd = true, 就是已经处理完成, 可以直接用里面的 playUrl / downloadUrl下载</p>
|
||||
* @param token 认证token
|
||||
* @param mediaServerId 流媒体服务id, 从 downloadStart/downloadProgress 获得
|
||||
* @param req GetDownloadTaskReq 请求参数
|
||||
* @return GetDownloadTaskResp 任务列表
|
||||
*/
|
||||
// /record_proxy/your_server_id/api/record/file/download/task/list?app=rtp&stream=59777645&taskId=0490d767d94ce20aedce57c862b6bfe9&isEnd=true
|
||||
@GetMapping("/record_proxy/{mediaServerId}/api/record/file/download/task/list")
|
||||
JsonResponse<List<GetDownloadTaskResp>> getDownloadTask4MediaServer(@RequestHeader("access-token") String token,
|
||||
@PathVariable String mediaServerId,
|
||||
@SpringQueryMap GetDownloadTaskReq req);
|
||||
|
||||
@GetMapping("/api/play/start/{deviceId}/{channelId}")
|
||||
JsonResponse<StreamContent> playStart(@RequestHeader("access-token") String token,
|
||||
@PathVariable String deviceId,
|
||||
@PathVariable String channelId,
|
||||
@RequestParam boolean isSubStream);
|
||||
@GetMapping("/api/play/stop/{deviceId}/{channelId}")
|
||||
JsonResponse<Void> playStop(@RequestHeader("access-token") String token,
|
||||
@PathVariable String deviceId,
|
||||
@PathVariable String channelId,
|
||||
@RequestParam boolean isSubStream);
|
||||
}
|
||||
|
@ -0,0 +1,129 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.catalog;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.JsonException;
|
||||
import cn.skcks.docking.gb28181.common.xml.XmlUtils;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
|
||||
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogItemDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogRequestDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogResponseDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
|
||||
import gov.nist.javax.sip.message.SIPRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.ListUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CatalogService {
|
||||
private final SipSender sipSender;
|
||||
private final SipSubscribe sipSubscribe;
|
||||
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
private final DockingService dockingService;
|
||||
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@SneakyThrows
|
||||
public CompletableFuture<CatalogResponseDTO> getCatalog(String deviceId){
|
||||
CompletableFuture<CatalogResponseDTO> result = new CompletableFuture<>();
|
||||
WvpProxyDocking device = dockingService.getDeviceByDeviceCode(deviceId).orElse(null);
|
||||
if (device == null){
|
||||
throw new JsonException("设备不存在");
|
||||
}
|
||||
CatalogRequestDTO catalogRequestDTO = new CatalogRequestDTO();
|
||||
catalogRequestDTO.setDeviceId(deviceId);
|
||||
String sn = String.valueOf((int) (Math.random() * 9 + 1) * 100000);
|
||||
catalogRequestDTO.setSn(sn);
|
||||
String key = GenericSubscribe.Helper.getKey(CmdType.CATALOG, deviceId);
|
||||
sipSubscribe.getCatalogSubscribe().addPublisher(key);
|
||||
final ScheduledFuture<?>[] schedule = new ScheduledFuture<?>[1];
|
||||
Flow.Subscriber<SIPRequest> subscriber = catalog(key, device, schedule, result);
|
||||
// 60秒超时计时器
|
||||
schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, 60 , TimeUnit.SECONDS);
|
||||
// 添加订阅
|
||||
sipSubscribe.getCatalogSubscribe().addSubscribe(key, subscriber);
|
||||
sipSender.sendRequest((provider, ip, port)-> SipRequestBuilder.createMessageRequest(device,ip,port,SipRequestBuilder.getCSeq(), XmlUtils.toXml(catalogRequestDTO), SipUtil.generateViaTag(),
|
||||
SipUtil.generateFromTag(), provider.getNewCallId()));
|
||||
return result;
|
||||
}
|
||||
|
||||
private Flow.Subscriber<SIPRequest> catalog(String key, WvpProxyDocking device, ScheduledFuture<?>[] schedule,CompletableFuture<CatalogResponseDTO> result){
|
||||
List<CatalogItemDTO> deviceList = new ArrayList<>();
|
||||
return new Flow.Subscriber<>() {
|
||||
Flow.Subscription subscription;
|
||||
CatalogResponseDTO dto;
|
||||
|
||||
final AtomicLong getNum = new AtomicLong(0);
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Flow.Subscription subscription) {
|
||||
log.info("开始订阅 {}", key);
|
||||
this.subscription = subscription;
|
||||
subscription.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(SIPRequest item) {
|
||||
CatalogResponseDTO responseDTO = XmlUtils.parse(item.getRawContent(), CatalogResponseDTO.class, GB28181Constant.CHARSET);
|
||||
dto = responseDTO;
|
||||
Long sumNum = responseDTO.getSumNum();
|
||||
log.info("{}",responseDTO);
|
||||
getNum.getAndAdd(responseDTO.getDeviceList().getDeviceList().size());
|
||||
deviceList.addAll(responseDTO.getDeviceList().getDeviceList());
|
||||
if(getNum.get() < sumNum){
|
||||
subscription.request(1);
|
||||
} else{
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
onComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
sipSubscribe.getCatalogSubscribe().delPublisher(key);
|
||||
schedule[0].cancel(true);
|
||||
log.info("订阅结束 {}", key);
|
||||
if(dto != null){
|
||||
dto.getDeviceList().setDeviceList(deviceList);
|
||||
scheduledExecutorService.execute(()->{
|
||||
updateProxyDevice(dto);
|
||||
});
|
||||
}
|
||||
result.complete(dto);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void updateProxyDevice(CatalogResponseDTO dto){
|
||||
String gbDeviceId = dto.getDeviceId();
|
||||
List<WvpProxyDevice> deviceByGbDeviceId = deviceService.getDeviceByGbDeviceId(gbDeviceId);
|
||||
List<String> existChannels = deviceByGbDeviceId.stream().map(WvpProxyDevice::getGbDeviceChannelId).toList();
|
||||
List<CatalogItemDTO> deviceList = dto.getDeviceList().getDeviceList();
|
||||
List<String> catalogChannels = deviceList.stream().map(CatalogItemDTO::getDeviceId).toList();
|
||||
List<String> noexist = ListUtils.subtract(catalogChannels, existChannels);
|
||||
noexist.forEach(channel->{
|
||||
log.info("更新 设备 {}, 通道 {} 信息", gbDeviceId, channel);
|
||||
deviceService.autoUpdateDeviceByGbDeviceIdAndChannel(gbDeviceId,channel);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.device;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.JsonException;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDeviceDynamicSqlSupport;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDeviceMapper;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
import com.github.pagehelper.ISelect;
|
||||
import com.github.pagehelper.Page;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class DeviceService {
|
||||
private final WvpProxyDeviceMapper deviceMapper;
|
||||
|
||||
public Optional<WvpProxyDevice> getDeviceById(Long id){
|
||||
return deviceMapper.selectOne(s->
|
||||
s.where(WvpProxyDeviceDynamicSqlSupport.id, isEqualTo(id)));
|
||||
}
|
||||
|
||||
public Optional<WvpProxyDevice> getDeviceByDeviceCode(String deviceCode){
|
||||
return deviceMapper.selectOne(s->s.where(WvpProxyDeviceDynamicSqlSupport.deviceCode,isEqualTo(deviceCode)).limit(1));
|
||||
}
|
||||
|
||||
public List<WvpProxyDevice> getDeviceByGbDeviceId(String gbDeviceId){
|
||||
return deviceMapper.select(s->
|
||||
s.where(WvpProxyDeviceDynamicSqlSupport.gbDeviceId,isEqualTo(gbDeviceId)));
|
||||
}
|
||||
|
||||
public Optional<WvpProxyDevice> getDeviceByGbDeviceIdAndChannel(String gbDeviceId,String channel){
|
||||
return deviceMapper.selectOne(s->
|
||||
s.where(WvpProxyDeviceDynamicSqlSupport.gbDeviceId,isEqualTo(gbDeviceId))
|
||||
.and(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId,isEqualTo(channel)).limit(1));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public boolean autoUpdateDeviceByGbDeviceIdAndChannel(String gbDeviceId, String channel){
|
||||
List<WvpProxyDevice> deviceByGbDeviceId = getDeviceByGbDeviceId(gbDeviceId);
|
||||
if(deviceByGbDeviceId.isEmpty()){
|
||||
WvpProxyDevice device = new WvpProxyDevice();
|
||||
device.setDeviceCode("");
|
||||
device.setGbDeviceId(gbDeviceId);
|
||||
device.setGbDeviceChannelId(channel);
|
||||
return deviceMapper.insert(device) > 0;
|
||||
}
|
||||
|
||||
Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = getDeviceByGbDeviceIdAndChannel(gbDeviceId,channel);
|
||||
if(deviceByGbDeviceIdAndChannel.isPresent()) {
|
||||
return deviceMapper.update(u->
|
||||
u.set(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId).equalTo(channel)
|
||||
.where(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId, isEqualTo(gbDeviceId))
|
||||
.and(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId, isEqualTo(channel))) > 0;
|
||||
} else {
|
||||
WvpProxyDevice device = new WvpProxyDevice();
|
||||
device.setDeviceCode("");
|
||||
device.setGbDeviceId(gbDeviceId);
|
||||
device.setGbDeviceChannelId(channel);
|
||||
return deviceMapper.insert(device) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加设备
|
||||
* @param device 设备
|
||||
* @return 是否成功
|
||||
*/
|
||||
@SneakyThrows
|
||||
public boolean addDevice(WvpProxyDevice device) {
|
||||
if(device == null){
|
||||
return false;
|
||||
}
|
||||
|
||||
String deviceCode = device.getDeviceCode();
|
||||
if(StringUtils.isBlank(deviceCode)){
|
||||
throw new JsonException("设备编码不能为空");
|
||||
}
|
||||
if(getDeviceByDeviceCode(deviceCode).isPresent()){
|
||||
throw new JsonException(MessageFormat.format("设备编码 {0} 已存在" ,deviceCode));
|
||||
}
|
||||
|
||||
String gbDeviceId = device.getGbDeviceId();
|
||||
String channel = device.getGbDeviceChannelId();
|
||||
if(StringUtils.isBlank(gbDeviceId)){
|
||||
throw new JsonException("国标编码不能为空");
|
||||
}
|
||||
if(StringUtils.isBlank(channel)){
|
||||
throw new JsonException("国标通道不能为空");
|
||||
}
|
||||
if(getDeviceByGbDeviceIdAndChannel(gbDeviceId,channel).isPresent()){
|
||||
throw new JsonException(MessageFormat.format("国标编码 {0}, 通道 {1} 已存在" ,gbDeviceId, channel));
|
||||
}
|
||||
|
||||
return deviceMapper.insert(device) > 0;
|
||||
}
|
||||
|
||||
public boolean deleteDeviceByGbDeviceIdAndChannel(String gbDeviceId, String channel){
|
||||
return deviceMapper.delete(d->
|
||||
d.where(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId, isEqualTo(gbDeviceId))
|
||||
.and(WvpProxyDeviceDynamicSqlSupport.gbDeviceChannelId, isEqualTo(channel))) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 依据 id 或 deviceCode 或 gbDeviceId 删除设备信息
|
||||
* @param device 设备
|
||||
* @return 是否成功
|
||||
*/
|
||||
public boolean deleteDevice(WvpProxyDevice device){
|
||||
if(device == null){
|
||||
return false;
|
||||
}
|
||||
|
||||
Long id = device.getId();
|
||||
String deviceCode = device.getDeviceCode();
|
||||
String gbDeviceId = device.getGbDeviceId();
|
||||
if(id != null){
|
||||
return deviceMapper.deleteByPrimaryKey(id) > 0;
|
||||
} else if(StringUtils.isNotBlank(deviceCode)){
|
||||
return deviceMapper.delete(d->d.where(WvpProxyDeviceDynamicSqlSupport.deviceCode,isEqualTo(deviceCode))) > 0;
|
||||
} else if(StringUtils.isNotBlank(gbDeviceId)){
|
||||
return deviceMapper.delete(d->d.where(WvpProxyDeviceDynamicSqlSupport.gbDeviceId,isEqualTo(gbDeviceId))) > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询设备
|
||||
* @param page 页数
|
||||
* @param size 数量
|
||||
* @return 分页设备
|
||||
*/
|
||||
public PageInfo<WvpProxyDevice> getDevicesWithPage(int page, int size){
|
||||
ISelect select = () -> deviceMapper.select(u -> u.orderBy(WvpProxyDeviceDynamicSqlSupport.id.descending()));
|
||||
return getDevicesWithPage(page,size, select);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询设备
|
||||
* @param page 页数
|
||||
* @param size 数量
|
||||
* @param select 查询语句
|
||||
* @return 分页设备
|
||||
*/
|
||||
public PageInfo<WvpProxyDevice> getDevicesWithPage(int page, int size, ISelect select){
|
||||
PageInfo<WvpProxyDevice> pageInfo;
|
||||
try (Page<WvpProxyDevice> startPage = PageHelper.startPage(page, size)) {
|
||||
pageInfo = startPage.doSelectPageInfo(select);
|
||||
}
|
||||
return pageInfo;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public boolean modifyDevice(WvpProxyDevice device){
|
||||
if(device == null){
|
||||
return false;
|
||||
}
|
||||
Long id = device.getId();
|
||||
if(id == null){
|
||||
throw new JsonException("id 不能为空");
|
||||
}
|
||||
|
||||
String deviceCode = device.getDeviceCode();
|
||||
if(StringUtils.isNotBlank(deviceCode)){
|
||||
WvpProxyDevice existDevice = getDeviceByDeviceCode(deviceCode).orElse(null);
|
||||
if(existDevice != null && !Objects.equals(existDevice.getId(), device.getId())){
|
||||
throw new JsonException(MessageFormat.format("设备编码 {0} 已存在", deviceCode));
|
||||
}
|
||||
}
|
||||
|
||||
String gbDeviceId = device.getGbDeviceId();
|
||||
String gbDeviceChannelId = device.getGbDeviceChannelId();
|
||||
if(gbDeviceId != null && StringUtils.isBlank(gbDeviceId)){
|
||||
throw new JsonException("国标id 不能为空");
|
||||
}
|
||||
if(gbDeviceChannelId != null && StringUtils.isBlank(gbDeviceChannelId)){
|
||||
throw new JsonException("通道 不能为空");
|
||||
}
|
||||
if(StringUtils.isNotBlank(gbDeviceId) && StringUtils.isNotBlank(gbDeviceChannelId)){
|
||||
WvpProxyDevice existDevice = getDeviceByGbDeviceIdAndChannel(gbDeviceId, gbDeviceChannelId).orElse(null);
|
||||
if(existDevice != null && !Objects.equals(existDevice.getId(), device.getId())){
|
||||
throw new JsonException(MessageFormat.format("国标id {0} ,通道 {1} 已存在", gbDeviceId, gbDeviceChannelId));
|
||||
}
|
||||
}
|
||||
return deviceMapper.updateByPrimaryKeySelective(device) > 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.device.control;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.JsonException;
|
||||
import cn.skcks.docking.gb28181.common.xml.XmlUtils;
|
||||
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.message.message.device.control.DeviceControlDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceControlService {
|
||||
private final SipSender sipSender;
|
||||
private final SipSubscribe sipSubscribe;
|
||||
private final DockingService dockingService;
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@SneakyThrows
|
||||
public void sendRecordControl(String deviceCode, String recordCmd){
|
||||
Optional<WvpProxyDevice> deviceByDeviceCode = deviceService.getDeviceByDeviceCode(deviceCode);
|
||||
if (deviceByDeviceCode.isEmpty()) {
|
||||
String reason = MessageFormat.format("未能找到 设备编码 为 {0} 的设备", deviceCode);
|
||||
log.error("{}",reason);
|
||||
throw new JsonException(reason);
|
||||
} else {
|
||||
WvpProxyDevice device = deviceByDeviceCode.get();
|
||||
sendRecordControl(device.getGbDeviceId(), device.getGbDeviceChannelId(), recordCmd);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendRecordControl(String gbDeviceId, String channel, String recordCmd){
|
||||
Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(gbDeviceId);
|
||||
if(deviceByGbDeviceId.isEmpty()){
|
||||
log.info("未能找到 国标编码 {} 的注册信息", gbDeviceId);
|
||||
return;
|
||||
}
|
||||
Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = deviceService.getDeviceByGbDeviceIdAndChannel(gbDeviceId, channel);
|
||||
if (deviceByGbDeviceIdAndChannel.isEmpty()) {
|
||||
log.info("未能找到 编码 {}, 通道 {} 的设备", gbDeviceId, channel);
|
||||
return;
|
||||
}
|
||||
WvpProxyDocking device = deviceByGbDeviceId.get();
|
||||
|
||||
String sn = String.valueOf((int) (Math.random() * 9 + 1) * 100000);
|
||||
DeviceControlDTO deviceControlDTO = DeviceControlDTO.builder()
|
||||
.sn(sn)
|
||||
.deviceId(channel)
|
||||
.recordCmd(recordCmd)
|
||||
.build();
|
||||
sipSender.sendRequest((provider, ip, port)-> SipRequestBuilder.createMessageRequest(device,ip,port,SipRequestBuilder.getCSeq(), XmlUtils.toXml(deviceControlDTO), SipUtil.generateViaTag(),
|
||||
SipUtil.generateFromTag(), provider.getNewCallId()));
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.docking;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.JsonException;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDockingDynamicSqlSupport;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.mapper.WvpProxyDockingMapper;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import com.github.pagehelper.ISelect;
|
||||
import com.github.pagehelper.Page;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DockingService {
|
||||
private final WvpProxyDockingMapper wvpProxyDockingMapper;
|
||||
|
||||
public Optional<WvpProxyDocking> getDeviceById(Long id){
|
||||
return wvpProxyDockingMapper.selectOne(s->
|
||||
s.where(WvpProxyDockingDynamicSqlSupport.id, isEqualTo(id)));
|
||||
}
|
||||
|
||||
public Optional<WvpProxyDocking> getDeviceByDeviceCode(String deviceCode){
|
||||
return wvpProxyDockingMapper.selectOne(s->
|
||||
s.where(WvpProxyDockingDynamicSqlSupport.gbDeviceId,isEqualTo(deviceCode)));
|
||||
}
|
||||
|
||||
public Boolean hasDeviceByDeviceCode(String deviceCode){
|
||||
return getDeviceByDeviceCode(deviceCode).orElse(null) != null;
|
||||
}
|
||||
|
||||
public Optional<WvpProxyDocking> getDeviceByGbDeviceId(String gbDeviceId){
|
||||
return wvpProxyDockingMapper.selectOne(s->
|
||||
s.where(WvpProxyDockingDynamicSqlSupport.gbDeviceId, isEqualTo(gbDeviceId)));
|
||||
}
|
||||
|
||||
public Boolean hasDeviceByGbDeviceId(String deviceCode){
|
||||
return getDeviceByGbDeviceId(deviceCode).orElse(null) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加设备
|
||||
* @param device 设备
|
||||
* @return 是否成功
|
||||
*/
|
||||
@SneakyThrows
|
||||
@Transactional
|
||||
public boolean addDevice(WvpProxyDocking device) {
|
||||
if(device == null){
|
||||
return false;
|
||||
}
|
||||
|
||||
String deviceCode = device.getGbDeviceId();
|
||||
if(StringUtils.isBlank(deviceCode)){
|
||||
throw new JsonException("设备编码不能为空");
|
||||
}
|
||||
if(getDeviceByDeviceCode(deviceCode).isPresent()){
|
||||
wvpProxyDockingMapper.delete(d->d.where(WvpProxyDockingDynamicSqlSupport.gbDeviceId,isEqualTo(deviceCode)));
|
||||
}
|
||||
|
||||
return wvpProxyDockingMapper.insert(device) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询对接设备/平台
|
||||
* @param page 页数
|
||||
* @param size 数量
|
||||
* @return 分页设备/平台
|
||||
*/
|
||||
public PageInfo<WvpProxyDocking> getDockingWithPage(int page, int size){
|
||||
ISelect select = () -> wvpProxyDockingMapper.select(s -> s.orderBy(WvpProxyDockingDynamicSqlSupport.id.descending()));
|
||||
return getDockingWithPage(page,size, select);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询对接设备/平台
|
||||
* @param page 页数
|
||||
* @param size 数量
|
||||
* @param select 查询语句
|
||||
* @return 分页设备/平台
|
||||
*/
|
||||
public PageInfo<WvpProxyDocking> getDockingWithPage(int page, int size, ISelect select){
|
||||
PageInfo<WvpProxyDocking> pageInfo;
|
||||
try (Page<WvpProxyDocking> startPage = PageHelper.startPage(page, size)) {
|
||||
pageInfo = startPage.doSelectPageInfo(select);
|
||||
}
|
||||
return pageInfo;
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.download;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpHead;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.core5.http.ClassicHttpResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DownloadService {
|
||||
@SneakyThrows
|
||||
public void header(HttpServletRequest request, HttpServletResponse response, String url) {
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
asyncContext.setTimeout(0);
|
||||
asyncContext.start(() -> {
|
||||
try {
|
||||
response.setHeader("Accept-Ranges", "none");
|
||||
response.setHeader("Connection", "close");
|
||||
header((HttpServletResponse) asyncContext.getResponse(), url);
|
||||
} finally {
|
||||
log.info("record 结束");
|
||||
asyncContext.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void header(HttpServletResponse response, String url) {
|
||||
try (CloseableHttpClient client = HttpClients.custom().build()) {
|
||||
HttpHead httpHead = new HttpHead(url);
|
||||
client.execute(httpHead, resp -> {
|
||||
setHeaderFromProxy(resp,response);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void setHeaderFromProxy(ClassicHttpResponse resp, HttpServletResponse response){
|
||||
response.setContentType(resp.getEntity().getContentType());
|
||||
Optional.ofNullable(resp.getHeader("Content-Disposition")).ifPresent((header)->{
|
||||
response.setHeader(header.getName(), header.getValue());
|
||||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void download(AsyncContext asyncContext , HttpServletResponse response, String url) {
|
||||
asyncContext.start(() -> {
|
||||
try {
|
||||
response.setHeader("Accept-Ranges", "none");
|
||||
response.setHeader("Connection", "close");
|
||||
download((HttpServletResponse) asyncContext.getResponse(), url);
|
||||
} finally {
|
||||
log.info("record 结束");
|
||||
asyncContext.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void download(HttpServletRequest request, HttpServletResponse response, String url) {
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
asyncContext.setTimeout(0);
|
||||
download(asyncContext, response, url);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void download(HttpServletResponse response, String url) {
|
||||
OutputStream outputStream = response.getOutputStream();
|
||||
|
||||
try (CloseableHttpClient client = HttpClients.custom().build()) {
|
||||
HttpGet httpGet = new HttpGet(url);
|
||||
client.execute(httpGet, resp -> {
|
||||
setHeaderFromProxy(resp,response);
|
||||
InputStream stream = resp.getEntity().getContent();
|
||||
IoUtil.copy(stream, outputStream);
|
||||
return stream;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.ffmpeg;
|
||||
|
||||
|
||||
import cn.skcks.docking.gb28181.wvp.config.FfmpegConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.exec.*;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FfmpegSupportService {
|
||||
private final FfmpegConfig ffmpegConfig;
|
||||
|
||||
@SneakyThrows
|
||||
public Executor downloadToStream(String input, String out, long time, TimeUnit unit, ExecuteStreamHandler streamHandler, ExecuteResultHandler executeResultHandler) {
|
||||
FfmpegConfig.Rtp rtp = ffmpegConfig.getRtp();
|
||||
FfmpegConfig.Debug debug = ffmpegConfig.getDebug();
|
||||
String inputParam = debug.getDownload() ? rtp.getDownload() : StringUtils.joinWith(" ", "-y", rtp.getDownload(), input);
|
||||
log.info("视频输入参数 {}", inputParam);
|
||||
|
||||
String outputParam = debug.getOutput() ? rtp.getOutput() : StringUtils.joinWith(" ", "-t", unit.toSeconds(time), rtp.getOutput(), out);
|
||||
log.info("视频输出参数 {}", outputParam);
|
||||
|
||||
return ffmpegExecutor(inputParam, outputParam, unit.toSeconds(time) + 60, TimeUnit.SECONDS, streamHandler, executeResultHandler);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Executor downloadToStream(String input, long time, TimeUnit unit, ExecuteStreamHandler streamHandler, ExecuteResultHandler executeResultHandler) {
|
||||
return downloadToStream(input, "-", time, unit, streamHandler, executeResultHandler);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Executor playbackToStream(String input, String out, long time, TimeUnit unit, ExecuteStreamHandler streamHandler, ExecuteResultHandler executeResultHandler){
|
||||
FfmpegConfig.Rtp rtp = ffmpegConfig.getRtp();
|
||||
FfmpegConfig.Debug debug = ffmpegConfig.getDebug();
|
||||
String inputParam = debug.getInput() ? rtp.getInput() : StringUtils.joinWith(" ", "-y", rtp.getInput(), input);
|
||||
log.info("视频输入参数 {}", inputParam);
|
||||
|
||||
String outputParam = debug.getOutput() ? rtp.getOutput() : StringUtils.joinWith(" ", rtp.getOutput(), out);
|
||||
log.info("视频输出参数 {}", outputParam);
|
||||
|
||||
return ffmpegExecutor(inputParam, outputParam, unit.toSeconds(time) + 60, TimeUnit.SECONDS, streamHandler, executeResultHandler);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Executor playbackToStream(String input, long time, TimeUnit unit, ExecuteStreamHandler streamHandler, ExecuteResultHandler executeResultHandler) {
|
||||
return playbackToStream(input, "-", time, unit, streamHandler, executeResultHandler);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Executor ffmpegExecutor(String inputParam,String outputParam, long time, TimeUnit unit,ExecuteStreamHandler streamHandler,ExecuteResultHandler executeResultHandler){
|
||||
FfmpegConfig.Rtp rtp = ffmpegConfig.getRtp();
|
||||
String logLevelParam = StringUtils.joinWith(" ","-loglevel", rtp.getLogLevel());
|
||||
String command = StringUtils.joinWith(" ", ffmpegConfig.getFfmpeg(), logLevelParam, inputParam, outputParam);
|
||||
CommandLine commandLine = CommandLine.parse(command);
|
||||
log.info("{}", commandLine);
|
||||
Executor executor = new DefaultExecutor();
|
||||
ExecuteWatchdog watchdog = new ExecuteWatchdog(unit.toMillis(time));
|
||||
executor.setStreamHandler(streamHandler);
|
||||
executor.setExitValues(null);
|
||||
executor.setWatchdog(watchdog);
|
||||
if(executeResultHandler == null){
|
||||
executor.execute(commandLine);
|
||||
} else {
|
||||
executor.execute(commandLine, executeResultHandler);
|
||||
}
|
||||
return executor;
|
||||
}
|
||||
}
|
@ -0,0 +1,717 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.gb28181;
|
||||
|
||||
import cn.hutool.core.date.*;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonException;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonUtils;
|
||||
import cn.skcks.docking.gb28181.common.redis.RedisUtil;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.cache.CacheUtil;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
|
||||
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
|
||||
import cn.skcks.docking.gb28181.media.config.ZlmMediaConfig;
|
||||
import cn.skcks.docking.gb28181.media.dto.rtp.CloseRtpServer;
|
||||
import cn.skcks.docking.gb28181.media.dto.rtp.GetRtpInfoResp;
|
||||
import cn.skcks.docking.gb28181.media.dto.rtp.OpenRtpServer;
|
||||
import cn.skcks.docking.gb28181.media.dto.rtp.OpenRtpServerResp;
|
||||
import cn.skcks.docking.gb28181.media.dto.status.ResponseStatus;
|
||||
import cn.skcks.docking.gb28181.media.proxy.ZlmMediaService;
|
||||
import cn.skcks.docking.gb28181.sdp.GB28181Description;
|
||||
import cn.skcks.docking.gb28181.sdp.GB28181SDPBuilder;
|
||||
import cn.skcks.docking.gb28181.sdp.media.MediaStreamMode;
|
||||
import cn.skcks.docking.gb28181.service.record.vo.RecordInfoItemVO;
|
||||
import cn.skcks.docking.gb28181.service.ssrc.SsrcService;
|
||||
import cn.skcks.docking.gb28181.wvp.config.MediaRtmpConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.executor.DefaultVideoExecutor;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.record.RecordInfoService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.record.dto.RecordInfoDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.service.video.VideoService;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.response.SipResponseBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
|
||||
import cn.skcks.docking.gb28181.wvp.utils.RetryUtil;
|
||||
import com.github.rholder.retry.Retryer;
|
||||
import com.github.rholder.retry.RetryerBuilder;
|
||||
import com.github.rholder.retry.StopStrategies;
|
||||
import com.github.rholder.retry.WaitStrategies;
|
||||
import gov.nist.javax.sdp.MediaDescriptionImpl;
|
||||
import gov.nist.javax.sdp.fields.TimeField;
|
||||
import gov.nist.javax.sdp.fields.URIField;
|
||||
import gov.nist.javax.sip.message.SIPRequest;
|
||||
import gov.nist.javax.sip.message.SIPResponse;
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.async.DeferredResult;
|
||||
|
||||
import javax.sdp.Connection;
|
||||
import javax.sdp.SdpFactory;
|
||||
import javax.sdp.TimeDescription;
|
||||
import javax.sip.header.CallIdHeader;
|
||||
import javax.sip.message.Request;
|
||||
import javax.sip.message.Response;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.MessageFormat;
|
||||
import java.time.Duration;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class Gb28181DownloadService {
|
||||
private final ZlmMediaService zlmMediaService;
|
||||
private final ZlmMediaConfig zlmMediaConfig;
|
||||
private final SsrcService ssrcService;
|
||||
private final DeviceService deviceService;
|
||||
private final DockingService dockingService;
|
||||
private final ProxySipConfig proxySipConfig;
|
||||
private final SipSender sender;
|
||||
private final SipSubscribe subscribe;
|
||||
private final VideoService videoService;
|
||||
private final WvpProxyConfig wvpProxyConfig;
|
||||
private final RecordInfoService recordInfoService;
|
||||
private final MediaRtmpConfig mediaRtmpConfig;
|
||||
|
||||
@Qualifier(DefaultVideoExecutor.EXECUTOR_BEAN_NAME)
|
||||
private final Executor executor;
|
||||
|
||||
private final ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(64);
|
||||
|
||||
private final ConcurrentMap<String, DeferredResult<JsonResponse<String>>> requestMap = new ConcurrentHashMap<>();
|
||||
|
||||
private final RealtimeManager realtimeManager;
|
||||
|
||||
@Getter
|
||||
private final RealTime realtime = new RealTime();
|
||||
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public static class VideoInfo {
|
||||
private String streamId;
|
||||
private String url;
|
||||
private String callId;
|
||||
private WvpProxyDevice device;
|
||||
}
|
||||
|
||||
public void header(HttpServletResponse response) {
|
||||
response.setContentType("video/mp4");
|
||||
response.setHeader("Accept-Ranges", "none");
|
||||
response.setHeader("Connection", "close");
|
||||
}
|
||||
|
||||
public void streamHeader(HttpServletResponse response) {
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
|
||||
response.setHeader("Accept-Ranges", "none");
|
||||
response.setHeader("Connection", "close");
|
||||
}
|
||||
|
||||
public void header(HttpServletResponse response, String fileName) {
|
||||
header(response);
|
||||
response.setHeader("Content-Disposition",
|
||||
MessageFormat.format("attachment; filename=\"{0}.mp4\"",fileName));
|
||||
}
|
||||
|
||||
private String videoRtmpUrl(String streamId) {
|
||||
String rtmpSchema = "rtmp://" + zlmMediaConfig.getIp() + ":" + mediaRtmpConfig.getRtmpPort();
|
||||
return StringUtils.joinWith("/", rtmpSchema, "rtp", streamId);
|
||||
}
|
||||
|
||||
private String videoWsUrl(String rtmpUrl){
|
||||
String rtmpSchema = "rtmp://" + zlmMediaConfig.getIp() + ":" + mediaRtmpConfig.getRtmpPort();
|
||||
if(StringUtils.isNotBlank(proxySipConfig.getProxyMediaUrl())){
|
||||
return StringUtils.replace(rtmpUrl + ".live.flv", rtmpSchema, proxySipConfig.getProxyMediaUrl());
|
||||
} else {
|
||||
String wsSchema = StringUtils.replace(zlmMediaConfig.getUrl(), "http://", "ws://");
|
||||
return StringUtils.replace(rtmpUrl + ".live.flv", rtmpSchema, wsSchema);
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void writeErrorToResponse(HttpServletResponse response, JsonResponse<?> json) {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
IoUtil.writeUtf8(response.getOutputStream(), false, json);
|
||||
}
|
||||
|
||||
private int openRtpServer(String streamId, int streamMode) {
|
||||
GetRtpInfoResp rtpInfo = zlmMediaService.getRtpInfo(streamId);
|
||||
if (rtpInfo.getExist()) {
|
||||
log.warn("流 {} 已存在", streamId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
OpenRtpServer openRtpServer = new OpenRtpServer();
|
||||
openRtpServer.setPort(0);
|
||||
openRtpServer.setStreamId(streamId);
|
||||
openRtpServer.setTcpMode(streamMode);
|
||||
OpenRtpServerResp openRtpServerResp = zlmMediaService.openRtpServer(openRtpServer);
|
||||
log.info("openRtpServerResp => {} => {}", streamId, openRtpServerResp);
|
||||
if (!openRtpServerResp.getCode().equals(ResponseStatus.Success)) {
|
||||
log.error("{}", openRtpServerResp.getCode().getMsg());
|
||||
return -1;
|
||||
}
|
||||
return openRtpServerResp.getPort();
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@SuppressWarnings({"UnstableApiUsage", "unchecked"})
|
||||
public void video(HttpServletRequest request, HttpServletResponse response, String deviceCode, Date startTime, Date endTime, Boolean fileHeader, Boolean useDownload) {
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
asyncContext.setTimeout(DateUtil.between(startTime, DateUtil.offsetSecond(endTime, 60), DateUnit.MS));
|
||||
asyncContext.start(()->{
|
||||
DateTime start = DateUtil.date();
|
||||
HttpServletResponse asyncResponse = (HttpServletResponse)asyncContext.getResponse();
|
||||
try{
|
||||
if(proxySipConfig.isUseRecordInfoQueryBeforeDownload()){
|
||||
String name = MessageFormat.format("{0} {1}-{2}", deviceCode, startTime, endTime);
|
||||
Retryer<JsonResponse<List<RecordInfoItemVO>>> retryer = RetryerBuilder.<JsonResponse<List<RecordInfoItemVO>>>newBuilder()
|
||||
// 异常就重试
|
||||
.retryIfException()
|
||||
.retryIfRuntimeException()
|
||||
// 重试间隔
|
||||
.withWaitStrategy(WaitStrategies.fixedWait(proxySipConfig.getRetryRecordInfoQueryBeforeDownloadInterval(), proxySipConfig.getRetryRecordInfoQueryBeforeDownloadIntervalUnit()))
|
||||
// 重试次数
|
||||
.withStopStrategy(StopStrategies.stopAfterAttempt(proxySipConfig.getRetryRecordInfoQueryBeforeDownloadTimes()))
|
||||
.retryIfResult((result) -> {
|
||||
log.info("{}", result);
|
||||
return result == null ||
|
||||
result.getCode() != Response.OK ||
|
||||
result.getData() == null ||
|
||||
result.getData().isEmpty();
|
||||
})
|
||||
.withRetryListener(RetryUtil.defaultRetryListener(name)).build();
|
||||
|
||||
retryer.call(()->{
|
||||
CompletableFuture<JsonResponse<List<RecordInfoItemVO>>> future = new CompletableFuture<>();
|
||||
future.completeOnTimeout(JsonResponse.error("录像查询超时"),1,TimeUnit.MINUTES);
|
||||
// 发起设备录像查询
|
||||
DeferredResult<JsonResponse<List<RecordInfoItemVO>>> requestedRecordInfo =
|
||||
recordInfoService.requestRecordInfo(new RecordInfoDTO(deviceCode, startTime, endTime, "", 0, "all"));
|
||||
|
||||
requestedRecordInfo.setResultHandler(result -> {
|
||||
future.complete((JsonResponse<List<RecordInfoItemVO>>) result);
|
||||
});
|
||||
requestedRecordInfo.onError((throwable)->{
|
||||
future.complete(JsonResponse.error(throwable.getMessage()));
|
||||
});
|
||||
|
||||
return future.get();
|
||||
});
|
||||
}
|
||||
|
||||
download(deviceCode, startTime, endTime, useDownload, true).whenComplete((videoInfo, e) -> {
|
||||
writeFileHeader(response, deviceCode, startTime, endTime, fileHeader);
|
||||
log.info("videoInfo {}", videoInfo);
|
||||
if (e != null) {
|
||||
writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
|
||||
} else if (videoInfo == null) {
|
||||
writeErrorToResponse(asyncResponse, JsonResponse.error("下载失败"));
|
||||
} else if (wvpProxyConfig.getUseFfmpeg()) {
|
||||
log.info("开始 ffmpeg 录制, deviceCode {}, startTime {}, endTime {}", deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
|
||||
videoService.ffmpegRecord(request, asyncResponse, videoInfo.getUrl(), startTime, endTime, DateUtil.between(startTime, endTime, DateUnit.SECOND), videoInfo.getDevice(), videoInfo.getCallId());
|
||||
DateTime end = DateUtil.date();
|
||||
asyncContext.complete();
|
||||
log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start, end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
|
||||
} else {
|
||||
videoService.javaCVrecord(asyncResponse, videoInfo.getUrl(), DateUtil.between(startTime, endTime, DateUnit.SECOND) + 15);
|
||||
}
|
||||
});
|
||||
} catch(Exception e) {
|
||||
writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
|
||||
} finally {
|
||||
if(!wvpProxyConfig.getUseFfmpeg()){
|
||||
DateTime end = DateUtil.date();
|
||||
asyncContext.complete();
|
||||
log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start,end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public DeferredResult<JsonResponse<String>> videoUrl(String deviceCode, Date startTime, Date endTime) {
|
||||
long time = DateUtil.between(startTime, endTime, DateUnit.MS);
|
||||
DeferredResult<JsonResponse<String>> result = new DeferredResult<>(time);
|
||||
result.onTimeout(()->{
|
||||
result.setResult(JsonResponse.error("请求超时"));
|
||||
});
|
||||
WvpProxyDevice device = deviceService.getDeviceByDeviceCode(deviceCode).orElse(null);
|
||||
if(device == null){
|
||||
result.setResult(JsonResponse.error("设备不存在"));
|
||||
return result;
|
||||
}
|
||||
WvpProxyDocking docking = dockingService.getDeviceByDeviceCode(device.getGbDeviceId()).orElse(null);
|
||||
if(docking == null){
|
||||
result.setResult(JsonResponse.error("设备(通道)不存在"));
|
||||
return result;
|
||||
}
|
||||
|
||||
closeExistRequest(deviceCode);
|
||||
requestMap.put(deviceCode, result);
|
||||
|
||||
// 间隔一定时间(200ms) 给设备足够的时间结束前次请求
|
||||
scheduledExecutorService.schedule(()->{
|
||||
download(deviceCode, startTime, endTime, false).whenComplete((videoInfo, e)->{
|
||||
log.info("获取媒体信息 {}", videoInfo);
|
||||
String cacheKey = CacheUtil.getKey(docking.getGbDeviceId(), device.getGbDeviceChannelId());
|
||||
String existCallId = RedisUtil.StringOps.get(cacheKey);
|
||||
// 到达时间后 延迟 10秒 主动结束, 防止某些设备不会主动结束
|
||||
scheduledExecutorService.schedule(()->{
|
||||
log.info("到达结束时间 发送 bye 关闭 {} {}", videoInfo.getDevice().getGbDeviceChannelId(), videoInfo.getCallId());
|
||||
String deviceIp = docking.getIp();
|
||||
int devicePort = Integer.parseInt(docking.getPort());
|
||||
if(StringUtils.isNotBlank(existCallId)) {
|
||||
sender.sendRequest((provider, localIp, localPort) ->
|
||||
SipRequestBuilder.createByeRequest(deviceIp, devicePort, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, existCallId));
|
||||
}
|
||||
RedisUtil.KeyOps.delete(cacheKey);
|
||||
zlmMediaService.closeRtpServer(CloseRtpServer.builder()
|
||||
.streamId(videoInfo.streamId)
|
||||
.build());
|
||||
}, time + Duration.ofSeconds(10).toMillis(), TimeUnit.MILLISECONDS);
|
||||
|
||||
String url = videoWsUrl(videoInfo.getUrl());
|
||||
url = StringUtils.replaceOnce(url, ".live.flv", ".live.mp4");
|
||||
result.setResult(JsonResponse.success(url));
|
||||
});
|
||||
}, 200, TimeUnit.MILLISECONDS);
|
||||
return result;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public synchronized DeferredResult<JsonResponse<String>> realtimeVideoUrl(String deviceCode){
|
||||
DeferredResult<JsonResponse<String>> result = new DeferredResult<>(TimeUnit.SECONDS.toMillis(60));
|
||||
result.onTimeout(()->{
|
||||
result.setResult(JsonResponse.error("请求超时"));
|
||||
});
|
||||
WvpProxyDevice device = deviceService.getDeviceByDeviceCode(deviceCode).orElse(null);
|
||||
if(device == null){
|
||||
result.setResult(JsonResponse.error("设备不存在"));
|
||||
return result;
|
||||
}
|
||||
WvpProxyDocking docking = dockingService.getDeviceByDeviceCode(device.getGbDeviceId()).orElse(null);
|
||||
if(docking == null){
|
||||
result.setResult(JsonResponse.error("设备(通道)不存在"));
|
||||
return result;
|
||||
}
|
||||
|
||||
String existUrl = RedisUtil.StringOps.get(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), deviceCode));
|
||||
if(Optional.ofNullable(existUrl).isPresent()){
|
||||
result.setResult(JsonResponse.success(existUrl));
|
||||
return result;
|
||||
}
|
||||
// 间隔一定时间(200ms) 给设备足够的时间结束前次请求
|
||||
scheduledExecutorService.schedule(()->{
|
||||
realtime.realtime(deviceCode).whenComplete((videoInfo, e)->{
|
||||
if(videoInfo == null){
|
||||
result.setResult(JsonResponse.error("媒体信息获取失败"));
|
||||
return;
|
||||
}
|
||||
log.info("获取媒体信息 {}", videoInfo);
|
||||
|
||||
// 原始链接转换为前端可用的链接
|
||||
RedisUtil.StringOps.set(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), videoInfo.getCallId()), JsonUtils.toJson(videoInfo));
|
||||
String url = videoWsUrl(videoInfo.getUrl());
|
||||
videoInfo.setUrl(url);
|
||||
|
||||
realtimeManager.addPlaying(deviceCode, videoInfo);
|
||||
result.setResult(JsonResponse.success(url));
|
||||
});
|
||||
}, 200, TimeUnit.MILLISECONDS);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void closeExistRequest(String deviceCode) {
|
||||
requestMap.computeIfPresent(deviceCode,(key,requestResult)->{
|
||||
log.info("关闭已存在的视频请求 {}", deviceCode);
|
||||
if(!requestResult.hasResult()){
|
||||
requestResult.setResult(JsonResponse.error("同一设备重复请求, 本次请求结束"));
|
||||
realtimeManager.removePlaying(deviceCode);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public void closeRealtimeVideoNow(String deviceCode){
|
||||
realtimeManager.removePlaying(deviceCode);
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public void videoStream(HttpServletRequest request, HttpServletResponse response, String deviceCode, Date startTime, Date endTime) {
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
asyncContext.setTimeout(0);
|
||||
asyncContext.start(()->{
|
||||
DateTime start = DateUtil.date();
|
||||
HttpServletResponse asyncResponse = (HttpServletResponse)asyncContext.getResponse();
|
||||
try{
|
||||
download(deviceCode, startTime,endTime, true).whenComplete((videoInfo, e)->{
|
||||
streamHeader(asyncResponse);
|
||||
if(e != null){
|
||||
writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
|
||||
} else if(videoInfo == null){
|
||||
writeErrorToResponse(asyncResponse, JsonResponse.error("下载失败"));
|
||||
} else if(wvpProxyConfig.getUseFfmpeg()){
|
||||
log.info("开始 ffmpeg 录制, deviceCode {}, startTime {}, endTime {}", deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
|
||||
videoService.ffmpegRecord(request, asyncResponse, videoInfo.getUrl(), startTime, endTime, DateUtil.between(startTime, endTime, DateUnit.SECOND), videoInfo.getDevice(), videoInfo.getCallId());
|
||||
DateTime end = DateUtil.date();
|
||||
asyncContext.complete();
|
||||
log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start, end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
|
||||
|
||||
} else {
|
||||
videoService.javaCVrecord(asyncResponse, videoInfo.getUrl(), DateUtil.between(startTime,endTime,DateUnit.SECOND) + 15);
|
||||
}
|
||||
});
|
||||
} catch(Exception e) {
|
||||
writeErrorToResponse(asyncResponse, JsonResponse.error(e.getMessage()));
|
||||
|
||||
} finally {
|
||||
if(!wvpProxyConfig.getUseFfmpeg()){
|
||||
DateTime end = DateUtil.date();
|
||||
asyncContext.complete();
|
||||
log.info("下载总耗时: {}, deviceCode {}, startTime {}, endTime {}", DateUtil.between(start,end, DateUnit.SECOND), deviceCode, DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void writeFileHeader(HttpServletResponse response, String deviceCode, Date startTime, Date endTime, Boolean fileHeader){
|
||||
if(fileHeader){
|
||||
header(response, StringUtils.joinWith("_",
|
||||
deviceCode,
|
||||
DateUtil.format(startTime, DatePattern.PURE_DATETIME_FORMAT),
|
||||
DateUtil.format(endTime, DatePattern.PURE_DATETIME_FORMAT)));
|
||||
} else {
|
||||
header(response);
|
||||
}
|
||||
}
|
||||
|
||||
private class RealTime {
|
||||
@SneakyThrows
|
||||
public CompletableFuture<VideoInfo> realtime(String deviceCode) {
|
||||
Optional<WvpProxyDevice> deviceByDeviceCode = deviceService.getDeviceByDeviceCode(deviceCode);
|
||||
if (deviceByDeviceCode.isEmpty()) {
|
||||
String reason = MessageFormat.format("未能找到 设备编码 为 {0} 的设备", deviceCode);
|
||||
log.error("{}",reason);
|
||||
throw new JsonException(reason);
|
||||
} else {
|
||||
WvpProxyDevice device = deviceByDeviceCode.get();
|
||||
return realtime(device.getGbDeviceId(), device.getGbDeviceChannelId());
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public CompletableFuture<VideoInfo> realtime(String gbDeviceId, String channel){
|
||||
CompletableFuture<VideoInfo> result = new CompletableFuture<>();
|
||||
Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(gbDeviceId);
|
||||
if(deviceByGbDeviceId.isEmpty()){
|
||||
log.info("未能找到 国标编码 {} 的注册信息", gbDeviceId);
|
||||
result.complete(null);
|
||||
return result;
|
||||
}
|
||||
Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = deviceService.getDeviceByGbDeviceIdAndChannel(gbDeviceId, channel);
|
||||
if (deviceByGbDeviceIdAndChannel.isEmpty()) {
|
||||
log.info("未能找到 编码 {}, 通道 {} 的设备", gbDeviceId, channel);
|
||||
result.complete(null);
|
||||
return result;
|
||||
}
|
||||
|
||||
WvpProxyDevice device = deviceByGbDeviceIdAndChannel.get();
|
||||
WvpProxyDocking docking = deviceByGbDeviceId.get();
|
||||
|
||||
String streamId = GB28181SDPBuilder.getStreamId(gbDeviceId, channel, IdUtil.getSnowflakeNextIdStr());
|
||||
int isTcp = proxySipConfig.getStreamMode() == MediaStreamMode.UDP ? 0 : 1;
|
||||
MediaStreamMode streamMode = proxySipConfig.getStreamMode();
|
||||
String ip = zlmMediaConfig.getIp();
|
||||
int port = openRtpServer(streamId, isTcp);
|
||||
if(port <= 0){
|
||||
log.error("zlm 暂无可用端口");
|
||||
result.complete(null);
|
||||
return result;
|
||||
}
|
||||
String ssrc = ssrcService.getPlaySsrc();
|
||||
GB28181Description gb28181Description = GB28181SDPBuilder.Receiver.play(gbDeviceId, channel, Connection.IP4, ip, port, ssrc, streamMode);
|
||||
sender.sendRequest(inviteRequest(docking, device, gb28181Description, ssrc, streamId, result, false));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@SneakyThrows
|
||||
public SipSender.SendRequest inviteRequest(WvpProxyDocking docking, WvpProxyDevice device, GB28181Description description, String ssrc, String streamId, CompletableFuture<VideoInfo> result, Boolean prefetch) {
|
||||
String cacheKey = CacheUtil.getKey(docking.getGbDeviceId(), device.getGbDeviceChannelId());
|
||||
String existCallId = RedisUtil.StringOps.get(cacheKey);
|
||||
|
||||
// 限制单个设备/通道 只能 点播 一路
|
||||
if(StringUtils.isNotBlank(existCallId)){
|
||||
String deviceIp = docking.getIp();
|
||||
int devicePort = Integer.parseInt(docking.getPort());
|
||||
sender.sendRequest((provider,localIp,localPort)->
|
||||
SipRequestBuilder.createByeRequest(deviceIp, devicePort, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, existCallId));
|
||||
RedisUtil.KeyOps.delete(cacheKey);
|
||||
log.info("关闭已存在的 点播请求 {} {}", cacheKey, existCallId);
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
return (provider, ip, port) -> {
|
||||
CallIdHeader callId = provider.getNewCallId();
|
||||
String subscribeKey = GenericSubscribe.Helper.getKey(Request.INVITE, callId.getCallId());
|
||||
subscribe.getInviteSubscribe().addPublisher(subscribeKey);
|
||||
Flow.Subscriber<SIPResponse> subscriber = inviteSubscriber(docking,device,subscribeKey,cacheKey, ssrc, streamId, result, 0, TimeUnit.SECONDS, prefetch);
|
||||
subscribe.getInviteSubscribe().addSubscribe(subscribeKey, subscriber);
|
||||
RedisUtil.StringOps.set(cacheKey, callId.getCallId());
|
||||
return SipRequestBuilder.createInviteRequest(ip, port, docking, device.getGbDeviceChannelId(), description.toString(), SipUtil.generateViaTag(), SipUtil.generateFromTag(), null, ssrc, callId);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public CompletableFuture<VideoInfo> download(String deviceCode, Date startTime, Date endTime, Boolean prefetch) {
|
||||
return download(deviceCode,startTime,endTime, proxySipConfig.isUsePlaybackToDownload(), prefetch);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public CompletableFuture<VideoInfo> download(String deviceCode, Date startTime, Date endTime, Boolean useDownload, Boolean prefetch) {
|
||||
Optional<WvpProxyDevice> deviceByDeviceCode = deviceService.getDeviceByDeviceCode(deviceCode);
|
||||
if (deviceByDeviceCode.isEmpty()) {
|
||||
String reason = MessageFormat.format("未能找到 设备编码 为 {0} 的设备", deviceCode);
|
||||
log.error("{}",reason);
|
||||
throw new JsonException(reason);
|
||||
} else {
|
||||
WvpProxyDevice device = deviceByDeviceCode.get();
|
||||
return download(device.getGbDeviceId(), device.getGbDeviceChannelId(), startTime, endTime, useDownload, prefetch);
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public CompletableFuture<VideoInfo> download(String gbDeviceId, String channel, Date startTime, Date endTime, Boolean useDownload, Boolean prefetch){
|
||||
CompletableFuture<VideoInfo> result = new CompletableFuture<>();
|
||||
Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(gbDeviceId);
|
||||
long time = DateUtil.between(startTime, endTime, DateUnit.SECOND);
|
||||
if(deviceByGbDeviceId.isEmpty()){
|
||||
log.info("未能找到 国标编码 {} 的注册信息", gbDeviceId);
|
||||
result.complete(null);
|
||||
return result;
|
||||
}
|
||||
Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = deviceService.getDeviceByGbDeviceIdAndChannel(gbDeviceId, channel);
|
||||
if (deviceByGbDeviceIdAndChannel.isEmpty()) {
|
||||
log.info("未能找到 编码 {}, 通道 {} 的设备", gbDeviceId, channel);
|
||||
result.complete(null);
|
||||
return result;
|
||||
}
|
||||
|
||||
WvpProxyDevice device = deviceByGbDeviceIdAndChannel.get();
|
||||
WvpProxyDocking docking = deviceByGbDeviceId.get();
|
||||
|
||||
ZoneId zoneId = ZoneId.of(GB28181Constant.TIME_ZONE);
|
||||
long start = LocalDateTimeUtil.of(startTime.toInstant(), zoneId).atZone(zoneId).toEpochSecond();
|
||||
long end = LocalDateTimeUtil.of(endTime.toInstant(), zoneId).atZone(zoneId).toEpochSecond();
|
||||
String streamId = GB28181SDPBuilder.getStreamId(gbDeviceId, channel, String.valueOf(start), String.valueOf(end), IdUtil.getSnowflakeNextIdStr());
|
||||
int isTcp = proxySipConfig.getStreamMode() == MediaStreamMode.UDP ? 0 : 1;
|
||||
MediaStreamMode streamMode = proxySipConfig.getStreamMode();
|
||||
String ip = zlmMediaConfig.getIp();
|
||||
int port = openRtpServer(streamId, isTcp);
|
||||
if(port <= 0){
|
||||
log.error("zlm 暂无可用端口");
|
||||
result.complete(null);
|
||||
return result;
|
||||
}
|
||||
String ssrc = ssrcService.getPlaySsrc();
|
||||
TimeField timeField = new TimeField();
|
||||
timeField.setStartTime(start);
|
||||
timeField.setStopTime(end);
|
||||
TimeDescription timeDescription = SdpFactory.getInstance().createTimeDescription(timeField);
|
||||
GB28181SDPBuilder.Action action = GB28181SDPBuilder.Action.DOWNLOAD;
|
||||
if(useDownload == null ? proxySipConfig.isUsePlaybackToDownload(): !useDownload){
|
||||
action = GB28181SDPBuilder.Action.PLAY_BACK;
|
||||
}
|
||||
GB28181Description gb28181Description = GB28181SDPBuilder.Receiver.build(action, gbDeviceId, channel, Connection.IP4, ip, port, ssrc, streamMode, timeDescription);
|
||||
gb28181Description.setSessionName(SdpFactory.getInstance().createSessionName(action.getAction()));
|
||||
gb28181Description.setTimeDescriptions(new Vector<>(){{add(timeDescription);}});
|
||||
MediaDescriptionImpl media = (MediaDescriptionImpl) gb28181Description.getMediaDescriptions(true).get(0);
|
||||
if(proxySipConfig.getStreamMode() != MediaStreamMode.UDP){
|
||||
media.getMedia().setProtocol("RTP/AVP/TCP");
|
||||
}
|
||||
if(useDownload == null ? !proxySipConfig.isUsePlaybackToDownload(): useDownload){
|
||||
media.setAttribute("downloadspeed", String.valueOf(4));
|
||||
}
|
||||
URIField uriField = new URIField();
|
||||
uriField.setURI(StringUtils.joinWith(":", channel, "0"));
|
||||
gb28181Description.setURI(uriField);
|
||||
sender.sendRequest(inviteRequest(docking, device, gb28181Description, action, ssrc, streamId, result, time, prefetch));
|
||||
return result;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public SipSender.SendRequest inviteRequest(WvpProxyDocking docking, WvpProxyDevice device, GB28181Description description, GB28181SDPBuilder.Action action, String ssrc, String streamId, CompletableFuture<VideoInfo> result, long time, Boolean prefetch) {
|
||||
String cacheKey = CacheUtil.getKey(docking.getGbDeviceId(), device.getGbDeviceChannelId());
|
||||
String existCallId = RedisUtil.StringOps.get(cacheKey);
|
||||
|
||||
// 限制单个设备/通道 只能 点播 一路
|
||||
if(StringUtils.isNotBlank(existCallId)){
|
||||
String deviceIp = docking.getIp();
|
||||
int devicePort = Integer.parseInt(docking.getPort());
|
||||
sender.sendRequest((provider,localIp,localPort)->
|
||||
SipRequestBuilder.createByeRequest(deviceIp, devicePort, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, existCallId));
|
||||
RedisUtil.KeyOps.delete(cacheKey);
|
||||
log.info("关闭已存在的 点播请求 {} {}", cacheKey, existCallId);
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
return (provider, ip, port) -> {
|
||||
CallIdHeader callId = provider.getNewCallId();
|
||||
String subscribeKey = GenericSubscribe.Helper.getKey(Request.INVITE, callId.getCallId());
|
||||
subscribe.getInviteSubscribe().addPublisher(subscribeKey);
|
||||
Flow.Subscriber<SIPResponse> subscriber = inviteSubscriber(docking,device,subscribeKey,cacheKey, ssrc, streamId, result, time + 60, TimeUnit.SECONDS, prefetch);
|
||||
subscribe.getInviteSubscribe().addSubscribe(subscribeKey, subscriber);
|
||||
RedisUtil.StringOps.set(cacheKey, callId.getCallId());
|
||||
if(prefetch){
|
||||
// 用以 提前 启动 ffmpeg 预备录制, 需要配置 ffmpeg rw_timeout 时长 避免收不到流
|
||||
result.complete(new VideoInfo(streamId, videoRtmpUrl(streamId), callId.getCallId(), device));
|
||||
}
|
||||
return SipRequestBuilder.createInviteRequest(ip, port, docking, device.getGbDeviceChannelId(), description.toString(), SipUtil.generateViaTag(), SipUtil.generateFromTag(), null, ssrc, callId);
|
||||
};
|
||||
}
|
||||
|
||||
public Flow.Subscriber<SIPResponse> inviteSubscriber(WvpProxyDocking docking, WvpProxyDevice device, String subscribeKey, String cacheKey, String ssrc,String streamId,CompletableFuture<VideoInfo> result, long time, TimeUnit unit, Boolean prefetch){
|
||||
ScheduledFuture<?>[] schedule = new ScheduledFuture<?>[1];
|
||||
Flow.Subscriber<SIPResponse> subscriber = new Flow.Subscriber<>() {
|
||||
private Flow.Subscription subscription;
|
||||
@Override
|
||||
public void onSubscribe(Flow.Subscription subscription) {
|
||||
this.subscription = subscription;
|
||||
log.info("订阅 {} {}", MessageProcessor.Method.INVITE, subscribeKey);
|
||||
subscription.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(SIPResponse item) {
|
||||
int statusCode = item.getStatusCode();
|
||||
log.debug("{} 收到订阅消息 {}", subscribeKey, item);
|
||||
if (statusCode == Response.TRYING) {
|
||||
log.info("订阅 {} {} 尝试连接流媒体服务", MessageProcessor.Method.INVITE, subscribeKey);
|
||||
subscription.request(1);
|
||||
} else if (statusCode >= Response.OK && statusCode < Response.MULTIPLE_CHOICES) {
|
||||
log.info("订阅 {} {} 流媒体服务连接成功, 开始推送视频流", MessageProcessor.Method.INVITE, subscribeKey);
|
||||
log.info("收到响应状态 {}", statusCode);
|
||||
String callId = item.getCallId().getCallId();
|
||||
if(!prefetch){
|
||||
// 待 相应 200OK 后再返回, 用于对延迟不敏感的实时请求
|
||||
result.complete(new VideoInfo(streamId, videoRtmpUrl(streamId), item.getCallId().getCallId(), device));
|
||||
}
|
||||
|
||||
scheduledExecutorService.schedule(()->{
|
||||
sender.sendRequest(((provider, ip, port) -> {
|
||||
String fromTag = item.getFromTag();
|
||||
String toTag = item.getToTag();
|
||||
String key = GenericSubscribe.Helper.getKey(Request.BYE, callId);
|
||||
subscribe.getByeSubscribe().addPublisher(key);
|
||||
subscribe.getByeSubscribe().addSubscribe(key, byeSubscriber(key, device, cacheKey, streamId, time, unit));
|
||||
return SipRequestBuilder.createAckRequest(Response.OK, ip, port, docking, device.getGbDeviceChannelId(), fromTag, toTag, callId);
|
||||
}));
|
||||
},200,TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
log.info("订阅 {} {} 连接流媒体服务时出现异常, 终止订阅", MessageProcessor.Method.INVITE, subscribeKey);
|
||||
zlmMediaService.closeRtpServer(new CloseRtpServer(streamId));
|
||||
result.complete(null);
|
||||
ssrcService.releaseSsrc(zlmMediaConfig.getId(), ssrc);
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
onComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
subscribe.getInviteSubscribe().delPublisher(subscribeKey);
|
||||
schedule[0].cancel(true);
|
||||
}
|
||||
};
|
||||
|
||||
if(time == 0){
|
||||
schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, 60, unit);
|
||||
} else {
|
||||
schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, time, unit);
|
||||
}
|
||||
return subscriber;
|
||||
}
|
||||
|
||||
public Flow.Subscriber<SIPRequest> byeSubscriber(String key,WvpProxyDevice device, String cacheKey,String streamId, long time, TimeUnit unit){
|
||||
ScheduledFuture<?>[] schedule = new ScheduledFuture<?>[1];
|
||||
Flow.Subscriber<SIPRequest> subscriber = new Flow.Subscriber<>() {
|
||||
@Override
|
||||
public void onSubscribe(Flow.Subscription subscription) {
|
||||
log.info("创建订阅 {}", key);
|
||||
subscription.request(1);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void onNext(SIPRequest request) {
|
||||
RedisUtil.KeyOps.delete(cacheKey);
|
||||
String transport = request.getTopmostViaHeader().getTransport();
|
||||
String ip = request.getLocalAddress().getHostAddress();
|
||||
if(time <= 0) {
|
||||
String callId = request.getCallId().getCallId();
|
||||
String infoKey = CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), callId);
|
||||
VideoInfo parse = JsonUtils.parse(RedisUtil.StringOps.get(infoKey), VideoInfo.class);
|
||||
Optional.ofNullable(parse).ifPresent((info)->{
|
||||
if(info.getCallId().equalsIgnoreCase(callId)){
|
||||
realtimeManager.removePlaying(device.getDeviceCode());
|
||||
}
|
||||
});
|
||||
RedisUtil.KeyOps.delete(infoKey);
|
||||
}
|
||||
|
||||
sender.getProvider(transport,ip)
|
||||
.sendResponse(SipResponseBuilder.response(request, Response.OK, "OK"));
|
||||
onComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
onComplete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
subscribe.getByeSubscribe().delPublisher(key);
|
||||
if(time > 0){
|
||||
schedule[0].cancel(true);
|
||||
}
|
||||
zlmMediaService.closeRtpServer(new CloseRtpServer(streamId));
|
||||
}
|
||||
};
|
||||
|
||||
if(time > 0){
|
||||
schedule[0] = scheduledExecutorService.schedule(subscriber::onComplete, time, unit);
|
||||
}
|
||||
return subscriber;
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.gb28181;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.JsonUtils;
|
||||
import cn.skcks.docking.gb28181.common.redis.RedisUtil;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.cache.CacheUtil;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
|
||||
import cn.skcks.docking.gb28181.media.dto.rtp.CloseRtpServer;
|
||||
import cn.skcks.docking.gb28181.media.proxy.ZlmMediaService;
|
||||
import cn.skcks.docking.gb28181.sdp.GB28181SDPBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RealtimeManager {
|
||||
public final static String REALTIME_KEY = "realtime:";
|
||||
private final ZlmMediaService zlmMediaService;
|
||||
private final DeviceService deviceService;
|
||||
private final DockingService dockingService;
|
||||
private final SipSender sender;
|
||||
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ConcurrentMap<String, ScheduledFuture<?>> playing = new ConcurrentHashMap<>();
|
||||
|
||||
private String getRealtimeKey(String deviceCode){
|
||||
return REALTIME_KEY + deviceCode;
|
||||
}
|
||||
|
||||
public void addPlaying(String deviceCode, Gb28181DownloadService.VideoInfo videoInfo) {
|
||||
RedisUtil.StringOps.set(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), deviceCode), videoInfo.getUrl());
|
||||
|
||||
ScheduledFuture<?> schedule = playing.get(deviceCode);
|
||||
if(schedule != null){
|
||||
schedule.cancel(true);
|
||||
}
|
||||
|
||||
// 定时任务
|
||||
schedule = scheduledExecutorService.schedule(()->{
|
||||
log.info("[定时任务] 关闭设备(deviceCode => {}) 视频", deviceCode);
|
||||
playing.remove(deviceCode);
|
||||
Gb28181DownloadService.VideoInfo _videoInfo = JsonUtils.parse(RedisUtil.StringOps.get(getRealtimeKey(deviceCode)), Gb28181DownloadService.VideoInfo.class);
|
||||
RedisUtil.KeyOps.delete(getRealtimeKey(deviceCode));
|
||||
close(deviceCode, _videoInfo);
|
||||
}, 3, TimeUnit.MINUTES);
|
||||
|
||||
// 缓存
|
||||
RedisUtil.StringOps.set(getRealtimeKey(deviceCode), JsonUtils.toJson(videoInfo));
|
||||
playing.put(deviceCode, schedule);
|
||||
}
|
||||
|
||||
public void removePlaying(String deviceCode) {
|
||||
ScheduledFuture<?> schedule = playing.get(deviceCode);
|
||||
if(schedule != null){
|
||||
schedule.cancel(true);
|
||||
playing.remove(deviceCode);
|
||||
}
|
||||
|
||||
Gb28181DownloadService.VideoInfo videoInfo = JsonUtils.parse(RedisUtil.StringOps.get(getRealtimeKey(deviceCode)), Gb28181DownloadService.VideoInfo.class);
|
||||
if(videoInfo == null) {
|
||||
log.warn("未找到 设备(deviceCode => {}) 视频信息", deviceCode);
|
||||
return;
|
||||
}
|
||||
|
||||
close(deviceCode, videoInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭视频连接
|
||||
* @param deviceCode 设备编码
|
||||
* @param videoInfo 视频信息
|
||||
*/
|
||||
private void close(String deviceCode, Gb28181DownloadService.VideoInfo videoInfo){
|
||||
RedisUtil.KeyOps.delete(getRealtimeKey(deviceCode));
|
||||
RedisUtil.KeyOps.delete(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), deviceCode));
|
||||
|
||||
WvpProxyDevice device = deviceService.getDeviceByDeviceCode(deviceCode).orElse(null);
|
||||
if(device == null){
|
||||
return;
|
||||
}
|
||||
WvpProxyDocking docking = dockingService.getDeviceByDeviceCode(device.getGbDeviceId()).orElse(null);
|
||||
if(docking == null){
|
||||
return;
|
||||
}
|
||||
|
||||
String cacheKey = CacheUtil.getKey(docking.getGbDeviceId(), device.getGbDeviceChannelId());
|
||||
String existCallId = RedisUtil.StringOps.get(cacheKey);
|
||||
|
||||
String deviceIp = docking.getIp();
|
||||
int devicePort = Integer.parseInt(docking.getPort());
|
||||
|
||||
// 判断缓存的 视频信息 与 缓存的 callId 是否相同 避免 下级点播出现信息不一致
|
||||
if(videoInfo.getCallId().equalsIgnoreCase(existCallId)){
|
||||
log.info("关闭视频连接, {} => {}",deviceCode, existCallId);
|
||||
sender.sendRequest((provider, localIp, localPort) ->
|
||||
SipRequestBuilder.createByeRequest(deviceIp, devicePort, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, existCallId));
|
||||
}
|
||||
|
||||
// 无论是否存在都调用一次 zlm 关闭流, 避免异常情况
|
||||
zlmMediaService.closeRtpServer(CloseRtpServer.builder()
|
||||
.streamId(videoInfo.getStreamId())
|
||||
.build());
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.record;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.JsonException;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import cn.skcks.docking.gb28181.common.xml.XmlUtils;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
|
||||
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
|
||||
import cn.skcks.docking.gb28181.service.record.vo.RecordInfoItemVO;
|
||||
import cn.skcks.docking.gb28181.sip.manscdp.recordinfo.request.RecordInfoRequestDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.record.dto.RecordInfoDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.subscribe.RecordSubscribe;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.async.DeferredResult;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RecordInfoService {
|
||||
private final SipSender sipSender;
|
||||
private final SipSubscribe sipSubscribe;
|
||||
private final DockingService dockingService;
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@SneakyThrows
|
||||
public DeferredResult<JsonResponse<List<RecordInfoItemVO>>> requestRecordInfo(RecordInfoDTO dto){
|
||||
String deviceCode = dto.getDeviceCode();
|
||||
Optional<WvpProxyDevice> deviceByDeviceCode = deviceService.getDeviceByDeviceCode(deviceCode);
|
||||
if (deviceByDeviceCode.isEmpty()) {
|
||||
String reason = MessageFormat.format("未能找到 设备编码 为 {0} 的设备", deviceCode);
|
||||
log.error("{}",reason);
|
||||
throw new JsonException(reason);
|
||||
} else {
|
||||
WvpProxyDevice device = deviceByDeviceCode.get();
|
||||
return requestRecordInfo(device.getGbDeviceId(), device.getGbDeviceChannelId(), dto);
|
||||
}
|
||||
}
|
||||
|
||||
public DeferredResult<JsonResponse<List<RecordInfoItemVO>>> requestRecordInfo(String gbDeviceId, String channel, RecordInfoDTO dto){
|
||||
DeferredResult<JsonResponse<List<RecordInfoItemVO>>> result = new DeferredResult<>();
|
||||
Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(gbDeviceId);
|
||||
if(deviceByGbDeviceId.isEmpty()){
|
||||
log.info("未能找到 国标编码 {} 的注册信息", gbDeviceId);
|
||||
result.setResult(JsonResponse.error(MessageFormat.format("未能找到 设备编码 为 {0} 的设备", gbDeviceId)));
|
||||
return result;
|
||||
}
|
||||
Optional<WvpProxyDevice> deviceByGbDeviceIdAndChannel = deviceService.getDeviceByGbDeviceIdAndChannel(gbDeviceId, channel);
|
||||
if (deviceByGbDeviceIdAndChannel.isEmpty()) {
|
||||
log.info("未能找到 编码 {}, 通道 {} 的设备", gbDeviceId, channel);
|
||||
result.setResult(JsonResponse.error(MessageFormat.format("未能找到 编码 {0}, 通道 {1} 的设备", gbDeviceId, channel)));
|
||||
return result;
|
||||
}
|
||||
WvpProxyDocking device = deviceByGbDeviceId.get();
|
||||
String sn = String.valueOf((int) (Math.random() * 9 + 1) * 100000);
|
||||
RecordInfoRequestDTO recordInfoRequestDTO = RecordInfoRequestDTO.builder()
|
||||
.deviceId(channel)
|
||||
.sn(sn)
|
||||
.startTime(dto.getStartTime())
|
||||
.endTime(dto.getEndTime())
|
||||
.type(dto.getType())
|
||||
.secrecy(dto.getSecrecy())
|
||||
.filePath(dto.getFilePath())
|
||||
.indistinctQuery(0)
|
||||
.build();
|
||||
|
||||
String key = GenericSubscribe.Helper.getKey(CmdType.RECORD_INFO, channel, sn);
|
||||
sipSubscribe.getMessageSubscribe().addPublisher(key);
|
||||
sipSubscribe.getMessageSubscribe().addSubscribe(key, new RecordSubscribe(sipSubscribe, key, result, gbDeviceId));
|
||||
sipSender.sendRequest((provider, ip, port)-> SipRequestBuilder.createMessageRequest(device,ip,port,SipRequestBuilder.getCSeq(), XmlUtils.toXml(recordInfoRequestDTO), SipUtil.generateViaTag(),
|
||||
SipUtil.generateFromTag(), provider.getNewCallId()));
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.record.dto;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.skcks.docking.gb28181.constant.GB28181Constant;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class RecordInfoDTO {
|
||||
/**
|
||||
* 目标设备的设备编码(必选)
|
||||
*/
|
||||
private String deviceCode;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN, timezone = GB28181Constant.TIME_ZONE)
|
||||
private Date startTime;
|
||||
|
||||
@DateTimeFormat(pattern= DatePattern.NORM_DATETIME_PATTERN)
|
||||
@JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN, timezone = GB28181Constant.TIME_ZONE)
|
||||
private Date endTime;
|
||||
|
||||
private String filePath;
|
||||
|
||||
private Integer Secrecy = 0;
|
||||
|
||||
private String type = "all";
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.report;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import cn.skcks.docking.gb28181.media.feign.IgnoreSSLFeignClientConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.report.ReportReq;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
@FeignClient(
|
||||
name = "ReportServiceProxy",
|
||||
url = "${report.url}",
|
||||
configuration = {IgnoreSSLFeignClientConfig.class}
|
||||
)
|
||||
public interface ReportApi {
|
||||
@PostMapping
|
||||
JsonResponse<?> report(@RequestHeader MultiValueMap<String, String> headers, @RequestBody ReportReq body);
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.report;
|
||||
|
||||
import cn.hutool.core.date.BetweenFormatter;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.unit.DataSizeUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.skcks.docking.gb28181.wvp.config.ReportConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.report.ReportReq;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ReportService {
|
||||
private final ReportApi reportApi;
|
||||
private final ReportConfig reportConfig;
|
||||
|
||||
public void report(HttpServletRequest request, WvpProxyDevice device, Date startTime, Date endTime, long fileSize) {
|
||||
if(!reportConfig.getEnabled()){
|
||||
return;
|
||||
}
|
||||
|
||||
ReportReq reportReq = new ReportReq(IdUtil.fastUUID(),
|
||||
device.getDeviceCode(),
|
||||
device.getGbDeviceChannelId(),
|
||||
DateUtil.formatBetween(startTime, endTime, BetweenFormatter.Level.SECOND),
|
||||
new ReportReq.TimeRange(DateUtil.formatDateTime(startTime), DateUtil.formatDateTime(endTime)),
|
||||
DateUtil.now(),
|
||||
DataSizeUtil.format(fileSize));
|
||||
LinkedMultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
|
||||
reportConfig.getCustomHeaders().forEach(headers::add);
|
||||
request.getHeaderNames().asIterator().forEachRemaining(headerKey -> {
|
||||
String header = request.getHeader(headerKey);
|
||||
headers.add(headerKey, header);
|
||||
});
|
||||
headers.add("X-Client-Ip", request.getRemoteAddr());
|
||||
log.info("上报调用信息 {}", reportReq);
|
||||
|
||||
try{
|
||||
reportApi.report(headers, reportReq);
|
||||
} catch (Exception e){
|
||||
log.error("上报调用信息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.video;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bytedeco.ffmpeg.global.avcodec;
|
||||
import org.bytedeco.ffmpeg.global.avutil;
|
||||
import org.bytedeco.javacv.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class RecordService {
|
||||
public void header(HttpServletResponse response){
|
||||
response.setContentType("video/mp4");
|
||||
response.setHeader("Accept-Ranges","none");
|
||||
response.setHeader("Connection","close");
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void record(HttpServletResponse response, String url, long timeout){
|
||||
response.reset();
|
||||
// response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
header(response);
|
||||
|
||||
Path tmp = Path.of(System.getProperty("java.io.tmpdir"), IdUtil.getSnowflakeNextIdStr()).toAbsolutePath();
|
||||
File file = new File(tmp + ".mp4");
|
||||
log.info("创建文件 {}, {}", file, file.createNewFile());
|
||||
|
||||
log.info("url {}", url);
|
||||
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(url)) {
|
||||
grabber.start();
|
||||
try(FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(file, grabber.getImageWidth(), grabber.getImageHeight(),grabber.getAudioChannels())){
|
||||
recorder.start();
|
||||
log.info("开始录像");
|
||||
log.info("{}", file);
|
||||
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
|
||||
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); //视频源数据yuv
|
||||
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); //设置音频压缩方式
|
||||
recorder.setFormat("mp4");
|
||||
recorder.setVideoOption("threads", String.valueOf(Runtime.getRuntime().availableProcessors())); //解码线程数
|
||||
|
||||
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
AtomicBoolean record = new AtomicBoolean(true);
|
||||
scheduledExecutorService.schedule(()->{
|
||||
log.info("到达超时时间, 结束录制");
|
||||
record.set(false);
|
||||
}, timeout, TimeUnit.SECONDS);
|
||||
|
||||
try {
|
||||
Frame frame;
|
||||
while (record.get() && (frame = grabber.grab()) != null) {
|
||||
recorder.record(frame);
|
||||
}
|
||||
grabber.stop();
|
||||
recorder.stop();
|
||||
} catch (FFmpegFrameRecorder.Exception | FrameGrabber.Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
log.info("结束录制");
|
||||
InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
|
||||
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
|
||||
try{
|
||||
IoUtil.copy(inputStream, outputStream);
|
||||
} catch (Exception ignore){}
|
||||
log.info("临时文件 {} 写入 响应 完成", file);
|
||||
log.info("删除临时文件 {} {}", file, file.delete());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,325 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.video;
|
||||
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import cn.hutool.core.date.DateUnit;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
|
||||
import cn.skcks.docking.gb28181.wvp.config.FfmpegConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.ffmpeg.FfmpegSupportService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.report.ReportService;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.exec.*;
|
||||
import org.bytedeco.ffmpeg.avcodec.AVPacket;
|
||||
import org.bytedeco.ffmpeg.global.avcodec;
|
||||
import org.bytedeco.ffmpeg.global.avutil;
|
||||
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||
import org.bytedeco.javacv.FFmpegFrameRecorder;
|
||||
import org.bytedeco.javacv.FrameGrabber;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VideoService {
|
||||
private final FfmpegSupportService ffmpegSupportService;
|
||||
private final WvpProxyConfig wvpProxyConfig;
|
||||
private final ProxySipConfig proxySipConfig;
|
||||
private final DockingService dockingService;
|
||||
private final SipSender sender;
|
||||
private final FfmpegConfig ffmpegConfig;
|
||||
private final ReportService reportService;
|
||||
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
/**
|
||||
* 写入 flv 响应头信息
|
||||
* @param response HttpServletResponse 响应
|
||||
*/
|
||||
public void header(HttpServletResponse response) {
|
||||
response.setContentType("video/mp4");
|
||||
response.setHeader("Accept-Ranges", "none");
|
||||
response.setHeader("Connection", "close");
|
||||
response.setHeader("Content-Disposition","attachment; filename=\"record.mp4\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* 录制视频 并写入 响应
|
||||
* @param request HttpServletRequest 请求
|
||||
* @param response HttpServletResponse 同步响应
|
||||
* @param url 要录制的视频地址
|
||||
* @param time 录制时长 (单位: 秒)
|
||||
*/
|
||||
@SneakyThrows
|
||||
public void record(HttpServletRequest request, HttpServletResponse response, String url, long time) {
|
||||
AsyncContext asyncContext = request.startAsync();
|
||||
asyncContext.setTimeout(0);
|
||||
asyncContext.start(() -> {
|
||||
header(response);
|
||||
try{
|
||||
record(asyncContext.getResponse(), url, time);
|
||||
} finally {
|
||||
log.info("record 结束");
|
||||
asyncContext.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 录制视频 并写入 异步响应
|
||||
* @param response AsyncContext.getResponse 异步响应
|
||||
* @param url 要录制的视频地址
|
||||
* @param time 录制时长 (单位: 秒)
|
||||
*/
|
||||
@SneakyThrows
|
||||
public void record(ServletResponse response, String url, long time) {
|
||||
if (wvpProxyConfig.getUseFfmpeg()) {
|
||||
ffmpegRecord(response, url, time);
|
||||
} else {
|
||||
javaCVrecord(response, url, time);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class FfmpegExecuteResultHandler extends DefaultExecuteResultHandler implements ExecuteResultHandler {
|
||||
private final static long SLEEP_TIME_MS = 50;
|
||||
@Setter(AccessLevel.PRIVATE)
|
||||
private boolean hasResult = false;
|
||||
private final WvpProxyDevice device;
|
||||
private final String callId;
|
||||
private final SipSender sender;
|
||||
|
||||
private void mediaStatus(){
|
||||
String deviceId = device.getGbDeviceId();
|
||||
Optional<WvpProxyDocking> deviceByGbDeviceId = dockingService.getDeviceByGbDeviceId(deviceId);
|
||||
if(deviceByGbDeviceId.isEmpty()){
|
||||
return;
|
||||
}
|
||||
WvpProxyDocking wvpProxyDocking = deviceByGbDeviceId.get();
|
||||
String ip = wvpProxyDocking.getIp();
|
||||
int port = Integer.parseInt(wvpProxyDocking.getPort());
|
||||
try{
|
||||
sender.sendRequest((provider,localIp,localPort)->
|
||||
SipRequestBuilder.createByeRequest(ip, port, device.getGbDeviceChannelId(), SipUtil.generateFromTag(), null, callId));
|
||||
}catch (Exception e){
|
||||
log.error("bye 请求发送失败 {}",e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasResult() {
|
||||
return hasResult;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void waitFor() {
|
||||
while (!hasResult()) {
|
||||
Thread.sleep(SLEEP_TIME_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProcessComplete(int exitValue) {
|
||||
hasResult = true;
|
||||
mediaStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProcessFailed(ExecuteException e) {
|
||||
hasResult = true;
|
||||
mediaStatus();
|
||||
}
|
||||
}
|
||||
|
||||
public FfmpegExecuteResultHandler mediaStatus(WvpProxyDevice device, String key){
|
||||
return new FfmpegExecuteResultHandler(device,key,sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* 录制视频 并写入 异步响应
|
||||
* @param response AsyncContext.getResponse 异步响应
|
||||
* @param url 要录制的视频地址
|
||||
* @param time 录制时长 (单位: 秒)
|
||||
*/
|
||||
@SneakyThrows
|
||||
public void javaCVrecord(ServletResponse response, String url, long time) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
OutputStream outputStream = response.getOutputStream();
|
||||
log.info("准备录制 url {}, time: {}", url, time);
|
||||
// FFmpeg 调试日志
|
||||
// FFmpegLogCallback.set();
|
||||
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(url);
|
||||
grabber.start();
|
||||
|
||||
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(stream, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
|
||||
recorder.setInterleaved(true);
|
||||
recorder.setVideoOption("preset", "ultrafast");
|
||||
recorder.setVideoOption("tune", "zerolatency");
|
||||
recorder.setVideoOption("crf", "25");
|
||||
recorder.setFrameRate(grabber.getFrameRate());
|
||||
recorder.setSampleRate(grabber.getSampleRate());
|
||||
recorder.setOption("flvflags", "no_duration_filesize");
|
||||
recorder.setOption("movflags","frag_keyframe+empty_moov");
|
||||
if (grabber.getAudioChannels() > 0) {
|
||||
recorder.setAudioChannels(grabber.getAudioChannels());
|
||||
recorder.setAudioBitrate(grabber.getAudioBitrate());
|
||||
recorder.setAudioCodec(grabber.getAudioCodec());
|
||||
}
|
||||
recorder.setVideoBitrate(grabber.getVideoBitrate());
|
||||
// recorder.setVideoCodec(grabber.getVideoCodec());
|
||||
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
|
||||
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); // 视频源数据yuv
|
||||
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); // 设置音频压缩方式
|
||||
recorder.setFormat("mp4");
|
||||
recorder.setVideoOption("threads", String.valueOf(Runtime.getRuntime().availableProcessors())); // 解码线程数
|
||||
recorder.start(grabber.getFormatContext());
|
||||
log.info("开始录制 {}", url);
|
||||
|
||||
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
AtomicBoolean record = new AtomicBoolean(true);
|
||||
scheduledExecutorService.schedule(() -> {
|
||||
log.info("到达结束时间, 结束录制 {}", url);
|
||||
record.set(false);
|
||||
}, time, TimeUnit.SECONDS);
|
||||
try {
|
||||
AVPacket k;
|
||||
while (record.get() && (k = grabber.grabPacket()) != null) {
|
||||
recorder.recordPacket(k);
|
||||
if(stream.size() > 0){
|
||||
outputStream.write(stream.toByteArray());
|
||||
outputStream.flush();
|
||||
stream.reset();
|
||||
}
|
||||
avcodec.av_packet_unref(k);
|
||||
}
|
||||
grabber.close();
|
||||
recorder.close();
|
||||
} catch (FFmpegFrameRecorder.Exception | FrameGrabber.Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException ignore){}
|
||||
finally {
|
||||
log.info("结束录制 {}", url);
|
||||
stream.close();
|
||||
outputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 录制视频 并写入 异步响应
|
||||
* @param response AsyncContext.getResponse 异步响应
|
||||
* @param url 要录制的视频地址
|
||||
* @param time 录制时长 (单位: 秒)
|
||||
*/
|
||||
@SneakyThrows
|
||||
public void ffmpegRecord(HttpServletRequest request, ServletResponse response, String url, Date startTime, Date endTime, long time, WvpProxyDevice device, String callId){
|
||||
String tmpDir = ffmpegConfig.getTmpDir();
|
||||
String fileName = callId + ".mp4";
|
||||
File file = new File(tmpDir, fileName);
|
||||
Executor executor;
|
||||
DefaultExecuteResultHandler executeResultHandler = mediaStatus(device,callId);
|
||||
if(ffmpegConfig.getUseTmpFile()) {
|
||||
OutputStream outputStream = new PipedOutputStream();
|
||||
String filePath = file.getAbsolutePath();
|
||||
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, System.err);
|
||||
if(proxySipConfig.isUsePlaybackToDownload()){
|
||||
executor = ffmpegSupportService.playbackToStream(url, filePath, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
|
||||
} else {
|
||||
executor = ffmpegSupportService.downloadToStream(url, filePath, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
|
||||
}
|
||||
} else {
|
||||
OutputStream outputStream = response.getOutputStream();
|
||||
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, System.err);
|
||||
if(proxySipConfig.isUsePlaybackToDownload()){
|
||||
executor = ffmpegSupportService.playbackToStream(url, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
|
||||
} else {
|
||||
executor = ffmpegSupportService.downloadToStream(url, time, TimeUnit.SECONDS,streamHandler,executeResultHandler);
|
||||
}
|
||||
}
|
||||
|
||||
DateTime start = DateUtil.date();
|
||||
log.info("开始录制 {}", url);
|
||||
ScheduledFuture<?> schedule = scheduledExecutorService.schedule(() -> {
|
||||
log.info("到达结束时间, 结束录制 {}", url);
|
||||
executor.getWatchdog().destroyProcess();
|
||||
log.info("结束录制 {}", url);
|
||||
}, time + 60, TimeUnit.SECONDS);
|
||||
executeResultHandler.waitFor();
|
||||
schedule.cancel(true);
|
||||
DateTime end = DateUtil.date();
|
||||
log.info("录制进程结束 {}, 录制耗时: {}", url, DateUtil.between(start,end, DateUnit.SECOND));
|
||||
|
||||
if(ffmpegConfig.getUseTmpFile()) {
|
||||
ServletOutputStream servletOutputStream = response.getOutputStream();
|
||||
try{
|
||||
log.info("临时文件 {}(大小 {})", file.getAbsolutePath(), file.length());
|
||||
IoUtil.copy(new FileInputStream(file), servletOutputStream);
|
||||
response.flushBuffer();
|
||||
reportService.report(request, device, startTime, endTime, file.length());
|
||||
} catch (Exception e){
|
||||
reportService.report(request, device, startTime, endTime, -1);
|
||||
log.error("写入 http 响应异常: {}", e.getMessage());
|
||||
} finally {
|
||||
System.gc();
|
||||
boolean delete = file.delete();
|
||||
log.info("删除临时文件 {} => {}", file, delete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 录制视频 并写入 异步响应
|
||||
* @param response AsyncContext.getResponse 异步响应
|
||||
* @param url 要录制的视频地址
|
||||
* @param time 录制时长 (单位: 秒)
|
||||
*/
|
||||
@SneakyThrows
|
||||
public void ffmpegRecord(ServletResponse response, String url, long time){
|
||||
ServletOutputStream outputStream = response.getOutputStream();
|
||||
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
|
||||
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, errorStream);
|
||||
DefaultExecuteResultHandler defaultExecuteResultHandler = new DefaultExecuteResultHandler();
|
||||
DateTime startTime = DateUtil.date();
|
||||
Executor executor;
|
||||
if(proxySipConfig.isUsePlaybackToDownload()){
|
||||
executor = ffmpegSupportService.playbackToStream(url, time, TimeUnit.SECONDS,streamHandler,defaultExecuteResultHandler);
|
||||
} else {
|
||||
executor = ffmpegSupportService.downloadToStream(url, time, TimeUnit.SECONDS,streamHandler,defaultExecuteResultHandler);
|
||||
}
|
||||
log.info("开始录制 {}", url);
|
||||
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
ScheduledFuture<?> schedule = scheduledExecutorService.schedule(() -> {
|
||||
log.info("到达结束时间, 结束录制 {}", url);
|
||||
executor.getWatchdog().destroyProcess();
|
||||
log.info("结束录制 {}", url);
|
||||
}, time, TimeUnit.SECONDS);
|
||||
defaultExecuteResultHandler.waitFor();
|
||||
schedule.cancel(true);
|
||||
DateTime endTime = DateUtil.date();
|
||||
log.info("录制进程结束 {}, 录制耗时: {}", url, DateUtil.between(startTime,endTime, DateUnit.SECOND));
|
||||
}
|
||||
}
|
@ -0,0 +1,444 @@
|
||||
package cn.skcks.docking.gb28181.wvp.service.wvp;
|
||||
|
||||
import cn.hutool.cache.CacheUtil;
|
||||
import cn.hutool.cache.impl.TimedCache;
|
||||
import cn.hutool.core.date.DateUnit;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.crypto.digest.MD5;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonException;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.common.GeneralTimeReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.device.DeviceChannel;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsResp;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.download.DownloadStartReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginResp;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.AddDownloadTaskReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.GetDownloadTaskReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.media.proxy.GetDownloadTaskResp;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.record.QueryRecordReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.record.QueryRecordResp;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.stream.StreamContent;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDevice;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import cn.skcks.docking.gb28181.wvp.proxy.WvpProxyClient;
|
||||
import cn.skcks.docking.gb28181.wvp.service.device.DeviceService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.download.DownloadService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.report.ReportService;
|
||||
import cn.skcks.docking.gb28181.wvp.service.video.VideoService;
|
||||
import cn.skcks.docking.gb28181.wvp.utils.RetryUtil;
|
||||
import com.github.rholder.retry.*;
|
||||
import jakarta.servlet.AsyncContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.context.request.async.DeferredResult;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WvpService {
|
||||
private final WvpProxyClient wvpProxyClient;
|
||||
private final WvpProxyConfig wvpProxyConfig;
|
||||
private final DeviceService deviceService;
|
||||
private final DownloadService downloadService;
|
||||
private final VideoService videoService;
|
||||
private final DockingService dockingService;
|
||||
private final TimedCache<String, String> cache = CacheUtil.newTimedCache(TimeUnit.HOURS.toMillis(1));
|
||||
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ConcurrentMap<String, ScheduledFuture<?>> playing = new ConcurrentHashMap<>();
|
||||
|
||||
private final ReportService reportService;
|
||||
|
||||
public void header(HttpServletResponse response) {
|
||||
response.setContentType("video/mp4");
|
||||
response.setHeader("Accept-Ranges", "none");
|
||||
response.setHeader("Connection", "close");
|
||||
// response.setHeader("Content-Disposition","attachment; filename=\"record.mp4\"");
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void writeErrorToResponse(HttpServletResponse response, JsonResponse<?> json) {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
IoUtil.writeUtf8(response.getOutputStream(), false, json);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void video(HttpServletRequest request, HttpServletResponse response, String deviceCode, Date startTime, Date endTime) {
|
||||
Optional<WvpProxyDevice> wvpProxyDeviceList = deviceService.getDeviceByDeviceCode(deviceCode);
|
||||
if (wvpProxyDeviceList.isEmpty()) {
|
||||
writeErrorToResponse(response, JsonResponse.error("设备不存在"));
|
||||
return;
|
||||
}
|
||||
WvpProxyDevice wvpProxyDevice = wvpProxyDeviceList.get();
|
||||
String deviceId = wvpProxyDevice.getGbDeviceId();
|
||||
String channelId = wvpProxyDevice.getGbDeviceChannelId();
|
||||
log.info("设备编码 (deviceCode=>{}) 查询到的设备信息 国标id(gbDeviceId => {}), 通道(channelId => {})", deviceCode, deviceId, channelId);
|
||||
request.startAsync();
|
||||
AsyncContext asyncContext = request.getAsyncContext();
|
||||
asyncContext.setTimeout(0);
|
||||
asyncContext.start(() -> {
|
||||
HttpServletResponse asyncResponse = (HttpServletResponse) asyncContext.getResponse();
|
||||
try {
|
||||
Retryer<JsonResponse<?>> genericRetryer = RetryUtil.getDefaultGenericRetryer("调用 wvp api 查询设备历史");
|
||||
genericRetryer.call(() -> video(asyncResponse, deviceCode, deviceId, channelId, startTime, endTime));
|
||||
} catch (RetryException e) {
|
||||
Attempt<?> failedAttempt = e.getLastFailedAttempt();
|
||||
String reason;
|
||||
if (failedAttempt.hasException()) {
|
||||
reason = MessageFormat.format("调用 wvp api 查询设备: {0} 历史失败, 已重试 {1} 次, 异常: {2}", deviceCode, e.getNumberOfFailedAttempts(), failedAttempt.getExceptionCause().getMessage());
|
||||
} else {
|
||||
reason = MessageFormat.format("调用 wvp api 查询设备: {0} 历史失败, 已重试 {1} 次", deviceCode, e.getNumberOfFailedAttempts());
|
||||
}
|
||||
log.error(reason);
|
||||
writeErrorToResponse(asyncResponse, JsonResponse.error(reason));
|
||||
} catch (Exception e) {
|
||||
String reason = MessageFormat.format("调用 wvp api 查询设备({0})历史失败, 异常: {1}", deviceCode, e.getMessage());
|
||||
writeErrorToResponse(asyncResponse, JsonResponse.error(reason));
|
||||
} finally {
|
||||
reportService.report(request,wvpProxyDevice, startTime, endTime,-1);
|
||||
log.info("asyncContext 结束");
|
||||
asyncContext.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 视频查询
|
||||
* <p>返回一个 JsonResponse 类型的执行结果 <p>
|
||||
* <p>如果返回值为 null 或 code 不为 0 或 200 则视为执行失败</p>
|
||||
* <p>如果排除异常 也视为执行失败</p>
|
||||
*
|
||||
* @param response 异步响应
|
||||
* @param deviceCode 设备编码 21位
|
||||
* @param deviceId 国标设备编码 20位
|
||||
* @param channelId 通道id
|
||||
* @param startDateTime 开始时间
|
||||
* @param endDateTime 结束时间
|
||||
* @return JsonResponse 类型的执行结果 如果 为 null 或 code 不为 0 或 200 则视为执行失败
|
||||
*/
|
||||
@SneakyThrows
|
||||
public JsonResponse<?> video(HttpServletResponse response, String deviceCode, String deviceId, String channelId, Date startDateTime, Date endDateTime) {
|
||||
String token = login();
|
||||
|
||||
log.debug("通过 wvp 查询设备 国标id(gbDeviceId => {}) 通道信息", deviceId);
|
||||
JsonResponse<GetDeviceChannelsResp> deviceChannels = wvpProxyClient.getDeviceChannels(
|
||||
token,
|
||||
deviceId,
|
||||
GetDeviceChannelsReq.builder().query(channelId).build());
|
||||
if (deviceChannels.getData() == null || deviceChannels.getData().getTotal() == 0) {
|
||||
writeErrorToResponse(response, JsonResponse.error(MessageFormat.format("未能获取 设备: {0}, 国标id: {1}, 的通道信息", deviceCode, deviceId)));
|
||||
return JsonResponse.success(null);
|
||||
}
|
||||
List<DeviceChannel> list = deviceChannels.getData().getList();
|
||||
log.info("通过 wvp 获取到 查询设备 国标id(gbDeviceId => {}), 通道数量 => {}", deviceId, list.size());
|
||||
DeviceChannel deviceChannel = list.parallelStream().filter(item -> item.getChannelId().equalsIgnoreCase(channelId)).findFirst().orElse(null);
|
||||
if (deviceChannel == null) {
|
||||
writeErrorToResponse(response, JsonResponse.error(MessageFormat.format("未查询到 设备: {0}, 国标id: {1}, 通道: {2} 信息", deviceCode, deviceId, channelId)));
|
||||
return JsonResponse.success(null);
|
||||
}
|
||||
|
||||
String startTime = DateUtil.formatDateTime(startDateTime);
|
||||
String endTime = DateUtil.formatDateTime(endDateTime);
|
||||
Retryer<JsonResponse<List<QueryRecordResp.RecordListDTO>>> queryRecordRetryer = RetryUtil.getDefaultRetryer("调用 wvp 设备历史查询 api");
|
||||
JsonResponse<List<QueryRecordResp.RecordListDTO>> recordListResponse = queryRecordRetryer.call(() -> {
|
||||
JsonResponse<QueryRecordResp> queryRecord = wvpProxyClient.queryRecord(token, deviceId, channelId, new QueryRecordReq(startTime, endTime));
|
||||
QueryRecordResp queryRecordData = queryRecord.getData();
|
||||
if (queryRecordData == null) {
|
||||
String reason = MessageFormat.format("通过 wvp 查询历史录像 失败 设备: {0}, 国标id: {1}, 通道: {2}, 错误信息: {3}", deviceCode, deviceId, channelId, queryRecord.getMsg());
|
||||
log.error(reason);
|
||||
log.error("查询历史录像 返回结果 => {}", queryRecord);
|
||||
throw new JsonException(reason);
|
||||
}
|
||||
List<QueryRecordResp.RecordListDTO> recordList = queryRecordData.getRecordList();
|
||||
if (CollectionUtils.isEmpty(recordList)) {
|
||||
String reason = MessageFormat.format("通过 wvp 查询历史录像 失败 设备: {0}, 国标id: {1}, 通道: {2}, 查询时间范围 开始时间: {3}, 结束时间: {4}, 录像数量为 0", deviceCode, deviceId, channelId, startTime, endTime);
|
||||
throw new JsonException(reason);
|
||||
}
|
||||
log.info("通过 wvp 查询到 {} 条历史录像 设备: {}, 国标id: {}, 通道: {}, 开始时间: {}, 结束时间: {}", recordList.size(), deviceCode, deviceId, channelId, startTime, endTime);
|
||||
return JsonResponse.success(recordList);
|
||||
});
|
||||
|
||||
List<QueryRecordResp.RecordListDTO> recordList = recordListResponse.getData();
|
||||
recordList.forEach(record -> {
|
||||
log.debug("{}", record);
|
||||
});
|
||||
|
||||
Boolean useWvpAssist = wvpProxyConfig.getUseWvpAssist();
|
||||
log.info("准备下载 deviceCode: {}, deviceId: {}, channelId:{}, ({}~{}) 历史视频, 通过 wvp-assist: {}", deviceCode, deviceId, channelId, startTime, endTime, useWvpAssist);
|
||||
if (useWvpAssist) {
|
||||
try {
|
||||
downloadFromWvpAssist(response, token, deviceCode, deviceId, channelId, startTime, endTime);
|
||||
} catch (Exception e) {
|
||||
log.warn("尝试通过 wvp-assist 下载视频失败, 尝试通过 视频回放 拉取视频");
|
||||
downloadFromPlayback(response, token, deviceId, channelId, startTime, endTime);
|
||||
}
|
||||
} else {
|
||||
downloadFromPlayback(response, token, deviceId, channelId, startTime, endTime);
|
||||
}
|
||||
|
||||
return JsonResponse.success(null);
|
||||
}
|
||||
|
||||
private void downloadFromWvpAssist(HttpServletResponse response, String token, String deviceCode, String deviceId, String channelId, String startTime, String endTime) {
|
||||
JsonResponse<String> videoPathResponse = downloadFromWvpAssist(deviceCode, deviceId, channelId, startTime, endTime, token);
|
||||
String videoUrl = videoPathResponse.getData();
|
||||
log.info("设备(deviceCode {}) (deviceId {}, channel{}) ({} ~ {}) 视频下载地址 {}", deviceCode, deviceId, channelId, startTime, endTime, videoUrl);
|
||||
downloadService.download(response, videoUrl);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void downloadFromPlayback(HttpServletResponse response, String token, String deviceId, String channelId, String startTime, String endTime) {
|
||||
Retryer<JsonResponse<StreamContent>> playBackRetryer = RetryUtil
|
||||
.<StreamContent>getDefaultRetryerBuilder("通过回放获取实时视频流下载", 100, TimeUnit.MILLISECONDS, 5)
|
||||
.build();
|
||||
JsonResponse<StreamContent> videoStreamResponse = playBackRetryer.call(() -> {
|
||||
JsonResponse<StreamContent> streamContentJsonResponse = wvpProxyClient.playbackStart(token, deviceId, channelId, new GeneralTimeReq(startTime, endTime));
|
||||
log.info("实时视频流下载 响应:{} ", streamContentJsonResponse);
|
||||
return streamContentJsonResponse;
|
||||
});
|
||||
StreamContent streamContent = videoStreamResponse.getData();
|
||||
String stream = streamContent.getStream();
|
||||
String streamUrl = streamContent.getFlv();
|
||||
try {
|
||||
header(response);
|
||||
videoService.record(response, streamUrl, DateUtil.between(DateUtil.parseDateTime(startTime), DateUtil.parseDateTime(endTime), DateUnit.SECOND));
|
||||
} finally {
|
||||
wvpProxyClient.playbackStop(token, deviceId, channelId, stream);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
@SneakyThrows
|
||||
private JsonResponse<String> downloadFromWvpAssist(String deviceCode, String deviceId, String channelId, String startTime, String endTime, String token) {
|
||||
Retryer<JsonResponse<String>> downloadRetryer = RetryUtil.getDefaultRetryer("调用 wvp 设备历史视频下载 api");
|
||||
return downloadRetryer.call(() -> {
|
||||
JsonResponse<StreamContent> downloadStart = wvpProxyClient.downloadStart(token, deviceId, channelId, DownloadStartReq.builder()
|
||||
.startTime(startTime)
|
||||
.endTime(endTime)
|
||||
.build());
|
||||
if (downloadStart.getData() == null) {
|
||||
throw new JsonException(downloadStart.getMsg());
|
||||
}
|
||||
StreamContent downloadStartData = downloadStart.getData();
|
||||
String mediaServerId = downloadStartData.getMediaServerId();
|
||||
String app = downloadStartData.getApp();
|
||||
String stream = downloadStartData.getStream();
|
||||
log.info("开始下载 mediaServerId: {}, app: {}, stream: {}", mediaServerId, app, stream);
|
||||
|
||||
AtomicLong keepTime = new AtomicLong();
|
||||
Double downloadProgress = 0D;
|
||||
Retryer<JsonResponse<StreamContent>> downloadProgressRetryer = RetryUtil.<StreamContent>getDefaultRetryerBuilder(MessageFormat.format("查询设备(deviceCode {0}) (deviceId {1}, channel{2}) ({3} ~ {4}) 历史视频下载进度", deviceCode, deviceId, channelId, startTime, endTime))
|
||||
// 重试间隔
|
||||
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
|
||||
.retryIfResult(result -> {
|
||||
keepTime.getAndIncrement();
|
||||
if (keepTime.get() > 60) {
|
||||
return false;
|
||||
}
|
||||
if (result == null || result.getData() == null) {
|
||||
return true;
|
||||
}
|
||||
if (!result.getData().getProgress().equals(downloadProgress)) {
|
||||
keepTime.set(0);
|
||||
return true;
|
||||
}
|
||||
return result.getData().getProgress().equals(downloadProgress) && keepTime.get() <= 60;
|
||||
})
|
||||
.withRetryListener(new RetryListener() {
|
||||
|
||||
@Override
|
||||
public <V> void onRetry(Attempt<V> attempt) {
|
||||
if (attempt.hasResult()) {
|
||||
log.debug("第 {} 次 获取 设备(deviceCode {}) (deviceId {}, channel{}) ({} ~ {}) 历史视频下载进度 mediaServerId: {}, app: {}, stream: {}", attempt.getAttemptNumber(), deviceCode, deviceId, channelId, startTime, endTime, mediaServerId, app, stream);
|
||||
} else {
|
||||
log.debug("第 {} 次 获取 设备(deviceCode {}) (deviceId {}, channel{}) ({} ~ {}) 历史视频下载进度失败 mediaServerId: {}, app: {}, stream: {}", attempt.getAttemptNumber(), deviceCode, deviceId, channelId, startTime, endTime, mediaServerId, app, stream);
|
||||
}
|
||||
}
|
||||
})
|
||||
.withStopStrategy(StopStrategies.neverStop())
|
||||
.build();
|
||||
downloadProgressRetryer.call(() -> {
|
||||
JsonResponse<StreamContent> downloadProgressResp = wvpProxyClient.downloadProgress(token, deviceId, channelId, stream);
|
||||
StreamContent data = downloadProgressResp.getData();
|
||||
log.debug("设备(deviceCode {}) (deviceId {}, channel{}) ({} ~ {}) 历史视频下载进度 {}% mediaServerId: {}, app: {}, stream: {}", deviceCode, deviceId, channelId, startTime, endTime, data.getProgress(), mediaServerId, app, stream);
|
||||
return downloadProgressResp;
|
||||
});
|
||||
wvpProxyClient.downloadStop(token, deviceId, channelId, stream);
|
||||
|
||||
JsonResponse<String> addDownloadTask2MediaServer = wvpProxyClient.addDownloadTask2MediaServer(token, mediaServerId, new AddDownloadTaskReq(app, stream));
|
||||
String taskId = addDownloadTask2MediaServer.getData();
|
||||
Retryer<JsonResponse<String>> mediaServerRetryer = RetryUtil.<String>getDefaultRetryerBuilder("从 mediaServer 获取视频")
|
||||
// 重试间隔
|
||||
.withWaitStrategy(WaitStrategies.fixedWait(RetryUtil.DEFAULT_RETRY_INTERVAL, TimeUnit.SECONDS))
|
||||
// 重试次数
|
||||
.withStopStrategy(StopStrategies.stopAfterAttempt(RetryUtil.DEFAULT_RETRY_TIME))
|
||||
.retryIfResult(RetryUtil.defaultRetryIf())
|
||||
.withRetryListener(RetryUtil.defaultRetryListener("从 mediaServer 获取视频"))
|
||||
.withStopStrategy(StopStrategies.stopAfterAttempt(10))
|
||||
.build();
|
||||
return mediaServerRetryer.call(() -> {
|
||||
JsonResponse<List<GetDownloadTaskResp>> downloadTask4MediaServer = wvpProxyClient.getDownloadTask4MediaServer(token, mediaServerId, new GetDownloadTaskReq(app, stream, taskId, true));
|
||||
if (downloadTask4MediaServer.getData().size() > 0) {
|
||||
return JsonResponse.success(downloadTask4MediaServer.getData().get(0).getPlayFile());
|
||||
} else {
|
||||
return JsonResponse.error(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Optional<DeviceChannel> getDeviceChannelByDeviceCode(String token, String deviceCode) {
|
||||
WvpProxyDevice device = deviceService.getDeviceByDeviceCode(deviceCode).orElse(null);
|
||||
if (device == null) {
|
||||
throw new JsonException("设备不存在");
|
||||
}
|
||||
String channelId = device.getGbDeviceChannelId();
|
||||
|
||||
WvpProxyDocking docking = dockingService.getDeviceByDeviceCode(device.getGbDeviceId()).orElse(null);
|
||||
if (docking == null) {
|
||||
throw new JsonException("设备(通道)不存在");
|
||||
}
|
||||
|
||||
Optional<DeviceChannel> deviceChannel = Optional.empty();
|
||||
for (String deviceId : wvpProxyConfig.getParents()) {
|
||||
log.debug("通过 wvp 查询设备 国标id(gbDeviceId => {}) 通道信息", deviceId);
|
||||
JsonResponse<GetDeviceChannelsResp> deviceChannels = wvpProxyClient.getDeviceChannels(
|
||||
token,
|
||||
deviceId,
|
||||
GetDeviceChannelsReq.builder()
|
||||
.query(channelId)
|
||||
.count(Integer.MAX_VALUE)
|
||||
.build());
|
||||
if (deviceChannels.getData() != null && deviceChannels.getData().getTotal() > 0) {
|
||||
deviceChannel = deviceChannels.getData()
|
||||
.getList()
|
||||
.parallelStream()
|
||||
.filter(item -> item.getChannelId().equalsIgnoreCase(channelId))
|
||||
.findFirst();
|
||||
if (deviceChannel.isPresent()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return deviceChannel;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public DeferredResult<JsonResponse<String>> realtimeVideoUrl(String deviceCode) {
|
||||
DeferredResult<JsonResponse<String>> result = new DeferredResult<>(TimeUnit.SECONDS.toMillis(60));
|
||||
result.onTimeout(() -> {
|
||||
log.error("timeout");
|
||||
result.setResult(JsonResponse.error("请求超时"));
|
||||
});
|
||||
|
||||
ScheduledFuture<?> schedule = playing.get(deviceCode);
|
||||
if (schedule != null) {
|
||||
schedule.cancel(true);
|
||||
}
|
||||
|
||||
final String[] token = {login()};
|
||||
Retryer<Optional<DeviceChannel>> defaultGenericRetryer = RetryerBuilder.<Optional<DeviceChannel>>newBuilder()
|
||||
// 异常就重试
|
||||
.retryIfException()
|
||||
.retryIfRuntimeException()
|
||||
// 重试间隔
|
||||
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
|
||||
// 重试次数
|
||||
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
|
||||
.withRetryListener(new RetryListener() {
|
||||
@Override
|
||||
public <V> void onRetry(Attempt<V> attempt) {
|
||||
if (attempt.hasException()) {
|
||||
log.info("异常 {}", attempt.getExceptionCause().getMessage());
|
||||
cache.remove("token");
|
||||
token[0] = login();
|
||||
}
|
||||
}
|
||||
}).build();
|
||||
Optional<DeviceChannel> deviceChannel = defaultGenericRetryer.call(() -> getDeviceChannelByDeviceCode(token[0], deviceCode));
|
||||
|
||||
if (deviceChannel.isEmpty()) {
|
||||
result.setResult(JsonResponse.error(MessageFormat.format("未能获取 设备: {0} 的通道信息", deviceCode)));
|
||||
return result;
|
||||
}
|
||||
|
||||
DeviceChannel dc = deviceChannel.get();
|
||||
log.info("设备通道信息 => {}", dc);
|
||||
|
||||
try {
|
||||
StreamContent data = wvpProxyClient.playStart(token[0], dc.getDeviceId(), dc.getChannelId(), false).getData();
|
||||
log.info("实时流信息 {}", data);
|
||||
|
||||
String url = StringUtils.joinWith("/",
|
||||
wvpProxyConfig.getProxyMediaUrl(), data.getApp(),
|
||||
StringUtils.joinWith(".", data.getStream(), "live", "flv"));
|
||||
|
||||
|
||||
// 定时任务
|
||||
schedule = scheduledExecutorService.schedule(() -> {
|
||||
log.info("[定时任务] 关闭设备(deviceCode => {}) 视频", deviceCode);
|
||||
playing.remove(deviceCode);
|
||||
closeRealtimeVideo(deviceCode);
|
||||
}, wvpProxyConfig.getRealtimeVideoDuration().getSeconds(), TimeUnit.SECONDS);
|
||||
playing.put(deviceCode, schedule);
|
||||
|
||||
result.setResult(JsonResponse.success(url));
|
||||
} catch (Exception e) {
|
||||
log.error("点播失败", e);
|
||||
result.setResult(JsonResponse.error("点播失败"));
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void closeRealtimeVideo(String deviceCode) {
|
||||
String token = login();
|
||||
Optional<DeviceChannel> deviceChannel = getDeviceChannelByDeviceCode(token, deviceCode);
|
||||
if (deviceChannel.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
DeviceChannel dc = deviceChannel.get();
|
||||
log.info("设备通道信息 => {}", dc);
|
||||
|
||||
wvpProxyClient.playStop(token, dc.getDeviceId(), dc.getChannelId(), false);
|
||||
}
|
||||
|
||||
private String login() {
|
||||
return cache.get("token", false, () -> {
|
||||
String passwdMd5 = MD5.create().digestHex(wvpProxyConfig.getPasswd());
|
||||
WvpLoginReq loginReq = WvpLoginReq.builder()
|
||||
.username(wvpProxyConfig.getUser())
|
||||
.password(passwdMd5)
|
||||
.build();
|
||||
JsonResponse<WvpLoginResp> login = wvpProxyClient.login(loginReq);
|
||||
String token = login.getData().getAccessToken();
|
||||
log.info("wvp 登录成功 token => {}", token);
|
||||
return token;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.JsonUtils;
|
||||
import cn.skcks.docking.gb28181.core.sip.service.SipService;
|
||||
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@Order(0)
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@DependsOn("wvpProxyOrmInitService")
|
||||
public class SipStarter implements SmartLifecycle {
|
||||
private final ProxySipConfig proxySipConfig;
|
||||
private final SipService sipService;
|
||||
|
||||
private boolean isRunning;
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if(checkConfig()){
|
||||
isRunning = true;
|
||||
log.debug("sip 服务 启动");
|
||||
sipService.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
log.debug("sip 服务 关闭");
|
||||
sipService.stop();
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
public boolean checkConfig(){
|
||||
log.debug("sip 配置信息 => \n{}", JsonUtils.toJson(proxySipConfig));
|
||||
if(CollectionUtils.isEmpty(proxySipConfig.getIp())){
|
||||
log.error("sip ip 配置错误, 请检查配置是否正确");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.listener;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.executor.DefaultSipExecutor;
|
||||
import cn.skcks.docking.gb28181.core.sip.listener.SipListener;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sip.*;
|
||||
import javax.sip.header.CSeqHeader;
|
||||
import javax.sip.header.CallIdHeader;
|
||||
import javax.sip.message.Request;
|
||||
import javax.sip.message.Response;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SipListenerImpl implements SipListener {
|
||||
private final ConcurrentMap<String, MessageProcessor> requestProcessor = new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<String, MessageProcessor> responseProcessor = new ConcurrentHashMap<>();
|
||||
|
||||
public void addRequestProcessor(String method, MessageProcessor messageProcessor) {
|
||||
log.debug("[SipListener] 注册 {} 请求处理器", method);
|
||||
requestProcessor.put(method, messageProcessor);
|
||||
}
|
||||
|
||||
public void addResponseProcessor(String method, MessageProcessor messageProcessor) {
|
||||
log.debug("[SipListener] 注册 {} 响应处理器", method);
|
||||
responseProcessor.put(method, messageProcessor);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Async(DefaultSipExecutor.EXECUTOR_BEAN_NAME)
|
||||
public void processRequest(RequestEvent requestEvent) {
|
||||
String method = requestEvent.getRequest().getMethod();
|
||||
log.debug("传入请求 method => {}", method);
|
||||
Optional.ofNullable(requestProcessor.get(method)).ifPresent(processor -> {
|
||||
processor.process(requestEvent);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async(DefaultSipExecutor.EXECUTOR_BEAN_NAME)
|
||||
public void processResponse(ResponseEvent responseEvent) {
|
||||
Response response = responseEvent.getResponse();
|
||||
int status = response.getStatusCode();
|
||||
CSeqHeader cseqHeader = (CSeqHeader) response.getHeader(CSeqHeader.NAME);
|
||||
String method = cseqHeader.getMethod();
|
||||
log.debug("{} {}", method, response);
|
||||
|
||||
|
||||
// Success
|
||||
if (((status >= Response.OK) && (status < Response.MULTIPLE_CHOICES)) || status == Response.UNAUTHORIZED) {
|
||||
log.debug("传入响应 method => {}", method);
|
||||
Optional.ofNullable(responseProcessor.get(method)).ifPresent(processor -> {
|
||||
processor.process(responseEvent);
|
||||
});
|
||||
} else if ((status >= Response.TRYING) && (status < Response.OK)) {
|
||||
// 增加其它无需回复的响应,如101、180等
|
||||
Optional.ofNullable(responseProcessor.get(method)).ifPresent(processor -> {
|
||||
processor.process(responseEvent);
|
||||
});
|
||||
} else {
|
||||
log.warn("接收到失败的response响应!status:" + status + ",message:" + response.getReasonPhrase());
|
||||
if (responseEvent.getDialog() != null) {
|
||||
responseEvent.getDialog().delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processTimeout(TimeoutEvent timeoutEvent) {
|
||||
ClientTransaction clientTransaction = timeoutEvent.getClientTransaction();
|
||||
if (clientTransaction != null) {
|
||||
Request request = clientTransaction.getRequest();
|
||||
if (request != null) {
|
||||
CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME);
|
||||
if (callIdHeader != null) {
|
||||
log.debug("会话超时 callId => {}", callIdHeader.getCallId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processIOException(IOExceptionEvent exceptionEvent) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processTransactionTerminated(TransactionTerminatedEvent transactionTerminatedEvent) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processDialogTerminated(DialogTerminatedEvent dialogTerminatedEvent) {
|
||||
CallIdHeader callIdHeader = dialogTerminatedEvent.getDialog().getCallId();
|
||||
log.debug("会话终止 callId => {}", callIdHeader.getCallId());
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.message.bye.request.request;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.redis.RedisUtil;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.cache.CacheUtil;
|
||||
import cn.skcks.docking.gb28181.core.sip.listener.SipListener;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
|
||||
import cn.skcks.docking.gb28181.sdp.GB28181SDPBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.response.SipResponseBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
|
||||
import gov.nist.javax.sip.message.SIPRequest;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sip.RequestEvent;
|
||||
import javax.sip.message.Request;
|
||||
import javax.sip.message.Response;
|
||||
import java.util.EventObject;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class ByeRequestProcessor implements MessageProcessor {
|
||||
private final SipListener sipListener;
|
||||
|
||||
private final SipSubscribe subscribe;
|
||||
|
||||
private final SipSender sender;
|
||||
|
||||
@PostConstruct
|
||||
@Override
|
||||
public void init() {
|
||||
sipListener.addRequestProcessor(Request.BYE, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(EventObject eventObject) {
|
||||
RequestEvent requestEvent = (RequestEvent) eventObject;
|
||||
SIPRequest request = (SIPRequest) requestEvent.getRequest();
|
||||
String callId = request.getCallId().getCallId();
|
||||
String key = GenericSubscribe.Helper.getKey(Request.BYE, callId);
|
||||
log.info("key {}", key);
|
||||
String ip = request.getLocalAddress().getHostAddress();
|
||||
String transport = request.getTopmostViaHeader().getTransport();
|
||||
Optional.ofNullable(subscribe.getByeSubscribe().getPublisher(key))
|
||||
.ifPresentOrElse(
|
||||
publisher -> publisher.submit(request),
|
||||
() -> {
|
||||
RedisUtil.KeyOps.delete(CacheUtil.getKey(GB28181SDPBuilder.Action.PLAY.getAction(), callId));
|
||||
sender.sendResponse(ip, transport, ((provider, ip1, port) ->
|
||||
SipResponseBuilder.response(request, Response.OK, "OK")));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.message.invite.response;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.listener.SipListener;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
|
||||
import gov.nist.javax.sip.message.SIPResponse;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sip.ResponseEvent;
|
||||
import javax.sip.message.Request;
|
||||
import java.util.EventObject;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class InviteResponseProcessor implements MessageProcessor {
|
||||
private final SipListener sipListener;
|
||||
private final SipSubscribe subscribe;
|
||||
|
||||
@PostConstruct
|
||||
@Override
|
||||
public void init() {
|
||||
sipListener.addResponseProcessor(Request.INVITE, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(EventObject eventObject) {
|
||||
ResponseEvent requestEvent = (ResponseEvent) eventObject;
|
||||
SIPResponse response = (SIPResponse) requestEvent.getResponse();
|
||||
String callId = response.getCallId().getCallId();
|
||||
String subscribeKey = GenericSubscribe.Helper.getKey(Request.INVITE, callId);
|
||||
log.info("收到 INVITE 响应, key {}", subscribeKey);
|
||||
Optional.ofNullable(subscribe.getInviteSubscribe().getPublisher(subscribeKey))
|
||||
.ifPresent(publisher->publisher.submit(response));
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JacksonXmlRootElement(localName = "DeviceList")
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class CatalogDeviceListDTO {
|
||||
@JacksonXmlProperty(isAttribute = true)
|
||||
private Integer num;
|
||||
@JacksonXmlProperty(localName = "Item")
|
||||
@JacksonXmlElementWrapper(useWrapping = false)
|
||||
private List<CatalogItemDTO> deviceList;
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto;
|
||||
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
@JacksonXmlRootElement(localName = "Item")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class CatalogItemDTO {
|
||||
/**
|
||||
* 设备/区域/系统编码(必选)
|
||||
*/
|
||||
@JacksonXmlProperty(localName = "DeviceID")
|
||||
private String deviceId;
|
||||
|
||||
/**
|
||||
* 设备/区域/系统名称(必选)
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 当为设备时,设备厂商(必选)
|
||||
*/
|
||||
private String manufacturer;
|
||||
|
||||
/**
|
||||
* 当为设备时,设备型号(必选)
|
||||
*/
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* 当为设备时,设备归属(必选)
|
||||
*/
|
||||
private String owner;
|
||||
|
||||
/**
|
||||
* 行政区域(必选)
|
||||
*/
|
||||
@JacksonXmlProperty(localName = "CivilCode")
|
||||
private String civilCode;
|
||||
|
||||
/**
|
||||
* 警区(可选)
|
||||
*/
|
||||
private String block;
|
||||
|
||||
/**
|
||||
* 当为设备时,安装地址(必选)
|
||||
*/
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 当为设备时,是否有子设备(必选)1有, 0没有
|
||||
*/
|
||||
@Builder.Default
|
||||
private Integer parental = 0;
|
||||
|
||||
/**
|
||||
* 父设备/区域/系统ID(必选)
|
||||
*/
|
||||
@JacksonXmlProperty(localName = "ParentID")
|
||||
private String parentId;
|
||||
|
||||
/**
|
||||
* 信令安全模式(可选)缺省为0; 0:不采用;2:S/MIME 签名方式;3:S/ MIME加密签名同时采用方式;4:数字摘要方式
|
||||
*/
|
||||
@Builder.Default
|
||||
private Integer safetyWay = 0;
|
||||
|
||||
/**
|
||||
* 注册方式(必选)缺省为1;1:符合IETF RFC3261标准的认证注册模 式;2:基于口令的双向认证注册模式;3:基于数字证书的双向认证注册模式
|
||||
*/
|
||||
@Builder.Default
|
||||
private Integer registerWay = 1;
|
||||
|
||||
/**
|
||||
* 证书序列号(有证书的设备必选)
|
||||
*/
|
||||
private String certNum;
|
||||
|
||||
/**
|
||||
* 证书有效标识(有证书的设备必选)缺省为0;证书有效标识:0:无效 1: 有效
|
||||
*/
|
||||
@Builder.Default
|
||||
private Integer certifiable = 0;
|
||||
|
||||
/**
|
||||
* 无效原因码(有证书且证书无效的设备必选)
|
||||
*/
|
||||
@Builder.Default
|
||||
private Integer errCode = 0;
|
||||
|
||||
/**
|
||||
* 证书终止有效期(有证书的设备必选)
|
||||
*/
|
||||
@JsonFormat(pattern = DatePattern.UTC_SIMPLE_PATTERN, timezone = GB28181Constant.TIME_ZONE)
|
||||
private Date endTime;
|
||||
|
||||
/**
|
||||
* 保密属性(必选)缺省为0;0:不涉密,1:涉密
|
||||
*/
|
||||
@Builder.Default
|
||||
private Integer secrecy = 0;
|
||||
|
||||
/**
|
||||
* 设备/区域/系统IP地址(可选)
|
||||
*/
|
||||
@JacksonXmlProperty(localName = "IPAddress")
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* 设备/区域/系统端口(可选)
|
||||
*/
|
||||
private Integer port;
|
||||
|
||||
/**
|
||||
* 设备口令(可选)
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 设备状态(必选)
|
||||
*/
|
||||
@Builder.Default
|
||||
private String status = "ON";
|
||||
|
||||
/**
|
||||
* 经度(可选)
|
||||
*/
|
||||
@Builder.Default
|
||||
private String longitude = "0.0";
|
||||
|
||||
/**
|
||||
* 纬度(可选)
|
||||
*/
|
||||
@Builder.Default
|
||||
private String latitude = "0.0";
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@JacksonXmlRootElement(localName = "Query")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class CatalogRequestDTO {
|
||||
@Builder.Default
|
||||
private String cmdType = CmdType.CATALOG;
|
||||
@JacksonXmlProperty(localName = "SN")
|
||||
private String sn;
|
||||
|
||||
/**
|
||||
* 目标设备的设备编码(必选)
|
||||
*/
|
||||
@JacksonXmlProperty(localName = "DeviceID")
|
||||
private String deviceId;
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@JacksonXmlRootElement(localName = "Response")
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
@Data
|
||||
public class CatalogResponseDTO {
|
||||
@Builder.Default
|
||||
private String cmdType = CmdType.CATALOG;
|
||||
@JacksonXmlProperty(localName = "SN")
|
||||
private String sn;
|
||||
|
||||
/**
|
||||
* 目标设备的设备编码(必选)
|
||||
*/
|
||||
@JacksonXmlProperty(localName = "DeviceID")
|
||||
private String deviceId;
|
||||
|
||||
private Long sumNum;
|
||||
|
||||
private CatalogDeviceListDTO deviceList;
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.message.message.device.control;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@JacksonXmlRootElement(localName = "Control")
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class DeviceControlDTO {
|
||||
@Builder.Default
|
||||
private String cmdType = CmdType.DEVICE_CONTROL;
|
||||
|
||||
@JacksonXmlProperty(localName = "SN")
|
||||
private String sn;
|
||||
|
||||
/**
|
||||
* 目标设备的设备编码(必选)
|
||||
*/
|
||||
@JacksonXmlProperty(localName = "DeviceID")
|
||||
private String deviceId;
|
||||
|
||||
/**
|
||||
* 录像控制命令
|
||||
*/
|
||||
private String recordCmd;
|
||||
|
||||
/**
|
||||
* 云台控制命令
|
||||
*/
|
||||
@JacksonXmlProperty(localName = "PTZCmd")
|
||||
private String ptzCmd;
|
||||
|
||||
/**
|
||||
* 远程启动
|
||||
*/
|
||||
private String teleBoot;
|
||||
|
||||
/**
|
||||
* 布防撤防
|
||||
*/
|
||||
private String guardCmd;
|
||||
|
||||
/**
|
||||
* 告警控制
|
||||
*/
|
||||
private String alarmCmd;
|
||||
|
||||
/**
|
||||
* 强制关键帧
|
||||
*/
|
||||
@JacksonXmlProperty(localName = "IFameCmd")
|
||||
private String iFameCmd;
|
||||
|
||||
/**
|
||||
* 拉框放大
|
||||
*/
|
||||
private String dragZoomIn;
|
||||
|
||||
/**
|
||||
* 拉框缩小
|
||||
*/
|
||||
private String dragZoomOut;
|
||||
|
||||
/**
|
||||
* 看守位
|
||||
*/
|
||||
private String homePosition;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.message.message.notify;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@JacksonXmlRootElement(localName = "Notify")
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
@Data
|
||||
public class MediaStatusRequestDTO {
|
||||
@Builder.Default
|
||||
private String cmdType = CmdType.MEDIA_STATUS;
|
||||
@JacksonXmlProperty(localName = "SN")
|
||||
private String sn;
|
||||
|
||||
/**
|
||||
* 目标设备的设备编码(必选)
|
||||
*/
|
||||
@JacksonXmlProperty(localName = "DeviceID")
|
||||
private String deviceId;
|
||||
|
||||
@JacksonXmlProperty(localName = "NotifyType")
|
||||
private String notifyType = "121";
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.message.message.processor.request;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.ResponseStatus;
|
||||
import cn.skcks.docking.gb28181.common.xml.XmlUtils;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.CmdType;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
|
||||
import cn.skcks.docking.gb28181.core.sip.listener.SipListener;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.processor.message.request.dto.MessageDTO;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.sender.SipMessageSender;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
|
||||
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
|
||||
import cn.skcks.docking.gb28181.sip.manscdp.recordinfo.response.RecordInfoResponseDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.message.message.catalog.dto.CatalogResponseDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.message.message.notify.MediaStatusRequestDTO;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.request.SipRequestBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.response.SipResponseBuilder;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.sender.SipSender;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.subscribe.SipSubscribe;
|
||||
import gov.nist.javax.sip.message.SIPRequest;
|
||||
import gov.nist.javax.sip.message.SIPResponse;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sip.RequestEvent;
|
||||
import javax.sip.address.SipURI;
|
||||
import javax.sip.header.CallIdHeader;
|
||||
import javax.sip.message.Request;
|
||||
import javax.sip.message.Response;
|
||||
import java.util.EventObject;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class MessageRequestProcessor implements MessageProcessor {
|
||||
private final SipListener sipListener;
|
||||
private final SipMessageSender sender;
|
||||
private final SipSender sipSender;
|
||||
private final SipSubscribe subscribe;
|
||||
private final DockingService dockingService;
|
||||
|
||||
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
@PostConstruct
|
||||
@Override
|
||||
public void init() {
|
||||
sipListener.addRequestProcessor(Request.MESSAGE,this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(EventObject eventObject) {
|
||||
RequestEvent requestEvent = (RequestEvent) eventObject;
|
||||
SIPRequest request = (SIPRequest)requestEvent.getRequest();
|
||||
String deviceId = SipUtil.getUserIdFromFromHeader(request);
|
||||
CallIdHeader callIdHeader = request.getCallIdHeader();
|
||||
|
||||
byte[] content = request.getRawContent();
|
||||
MessageDTO messageDto = XmlUtils.parse(content, MessageDTO.class, GB28181Constant.CHARSET);
|
||||
log.debug("接收到的消息 => {}", messageDto);
|
||||
|
||||
String senderIp = request.getLocalAddress().getHostAddress();
|
||||
|
||||
if(!dockingService.hasDeviceByDeviceCode(deviceId)){
|
||||
log.info("未找到相关设备信息 => {}", deviceId);
|
||||
Response response = response(request,Response.NOT_FOUND,"设备未注册");
|
||||
sender.send(senderIp,response);
|
||||
return;
|
||||
}
|
||||
|
||||
Response ok = response(request, Response.OK, "OK");
|
||||
Response response;
|
||||
|
||||
if(StringUtils.equalsAnyIgnoreCase(messageDto.getCmdType(), CmdType.KEEPALIVE)){
|
||||
response = ok;
|
||||
// 更新设备在线状态
|
||||
} else if(messageDto.getCmdType().equalsIgnoreCase(cn.skcks.docking.gb28181.constant.CmdType.RECORD_INFO)) {
|
||||
response = ok;
|
||||
RecordInfoResponseDTO dto = XmlUtils.parse(content, RecordInfoResponseDTO.class, GB28181Constant.CHARSET);
|
||||
String key = GenericSubscribe.Helper.getKey(cn.skcks.docking.gb28181.constant.CmdType.RECORD_INFO, dto.getDeviceId(), dto.getSn());
|
||||
Optional.ofNullable(subscribe.getMessageSubscribe().getPublisher(key))
|
||||
.ifPresentOrElse(publisher -> publisher.submit(request),
|
||||
() -> log.warn("对应订阅 {} 已结束, 异常数据 => {}", key, dto));
|
||||
} else if(messageDto.getCmdType().equalsIgnoreCase(CmdType.CATALOG)){
|
||||
response = ok;
|
||||
CatalogResponseDTO dto = XmlUtils.parse(content, CatalogResponseDTO.class, GB28181Constant.CHARSET);
|
||||
String key = GenericSubscribe.Helper.getKey(CmdType.CATALOG, dto.getDeviceId());
|
||||
Optional.ofNullable(subscribe.getCatalogSubscribe().getPublisher(key))
|
||||
.ifPresentOrElse(publisher-> publisher.submit(request),
|
||||
()-> log.warn("对应订阅 {} 已结束, 异常数据 => {}",key, dto));
|
||||
} else if(messageDto.getCmdType().equalsIgnoreCase(CmdType.MEDIA_STATUS)){
|
||||
response = ok;
|
||||
|
||||
MediaStatusRequestDTO parse = XmlUtils.parse(content, MediaStatusRequestDTO.class, GB28181Constant.CHARSET);
|
||||
if(StringUtils.equalsIgnoreCase(parse.getNotifyType(),"121")){
|
||||
String ip = request.getLocalAddress().getHostAddress();
|
||||
String transport = request.getTopmostViaHeader().getTransport();
|
||||
scheduledExecutorService.schedule(()->{
|
||||
sipSender.sendResponse(ip, transport, ((provider, ip1, port) ->
|
||||
SipResponseBuilder.response(request, Response.OK, "OK")));
|
||||
|
||||
String targetIp = request.getRemoteAddress().getHostAddress();
|
||||
int targetPort = request.getRemotePort();
|
||||
String targetId = ((SipURI)request.getFromHeader().getAddress().getURI()).getUser();
|
||||
String callId = request.getCallIdHeader().getCallId();
|
||||
Request byeRequest = SipRequestBuilder.createByeRequest(targetIp, targetPort, targetId, SipUtil.generateFromTag(), null, callId);
|
||||
sipSender.sendRequest(((provider, ip1, port) -> byeRequest));
|
||||
},100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}else {
|
||||
response = response(request, Response.NOT_IMPLEMENTED, ResponseStatus.NOT_IMPLEMENTED.getMessage());
|
||||
}
|
||||
sender.send(senderIp, response);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public Response response(SIPRequest request, int status, String message){
|
||||
if (request.getToHeader().getTag() == null) {
|
||||
request.getToHeader().setTag(SipUtil.generateTag());
|
||||
}
|
||||
SIPResponse response = (SIPResponse)getMessageFactory().createResponse(status, request);
|
||||
if (message != null) {
|
||||
response.setReasonPhrase(message);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.message.register.request;
|
||||
|
||||
import cn.skcks.docking.gb28181.config.sip.SipConfig;
|
||||
import cn.skcks.docking.gb28181.core.sip.dto.RemoteInfo;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.sip.GbSipDate;
|
||||
import cn.skcks.docking.gb28181.core.sip.listener.SipListener;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.auth.DigestServerAuthenticationHelper;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.processor.MessageProcessor;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.sender.SipMessageSender;
|
||||
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import cn.skcks.docking.gb28181.wvp.service.docking.DockingService;
|
||||
import gov.nist.javax.sip.address.SipUri;
|
||||
import gov.nist.javax.sip.header.Authorization;
|
||||
import gov.nist.javax.sip.header.SIPDateHeader;
|
||||
import gov.nist.javax.sip.message.SIPRequest;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sip.RequestEvent;
|
||||
import javax.sip.address.Address;
|
||||
import javax.sip.header.ExpiresHeader;
|
||||
import javax.sip.header.FromHeader;
|
||||
import javax.sip.header.ViaHeader;
|
||||
import javax.sip.message.Request;
|
||||
import javax.sip.message.Response;
|
||||
import java.util.Calendar;
|
||||
import java.util.EventObject;
|
||||
import java.util.Locale;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class RegisterRequestProcessor implements MessageProcessor {
|
||||
private final SipListener sipListener;
|
||||
private final SipMessageSender sender;
|
||||
|
||||
private final SipConfig sipConfig;
|
||||
private final DockingService dockingService;
|
||||
|
||||
|
||||
@PostConstruct
|
||||
@Override
|
||||
public void init(){
|
||||
sipListener.addRequestProcessor(Method.REGISTER,this);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void process(EventObject eventObject) {
|
||||
RequestEvent requestEvent = (RequestEvent) eventObject;
|
||||
SIPRequest request = (SIPRequest)requestEvent.getRequest();
|
||||
FromHeader fromHeader = request.getFrom();
|
||||
Address address = fromHeader.getAddress();
|
||||
log.debug("From {}",address);
|
||||
|
||||
SipUri uri = (SipUri)address.getURI();
|
||||
String deviceId = uri.getUser();
|
||||
log.debug("请求注册 设备id => {}", deviceId);
|
||||
|
||||
|
||||
Boolean hasDevice = dockingService.hasDeviceByDeviceCode(deviceId);
|
||||
String senderIp = request.getLocalAddress().getHostAddress();
|
||||
RemoteInfo remoteInfo = SipUtil.getRemoteInfoFromRequest(request, false);
|
||||
log.debug("远程连接信息 => {}", remoteInfo);
|
||||
if(!hasDevice){
|
||||
log.info("新注册的设备 deviceId => {}", deviceId);
|
||||
}
|
||||
|
||||
String password = sipConfig.getPassword();
|
||||
Authorization authorization = request.getAuthorization();
|
||||
if(authorization == null && StringUtils.isNotBlank(password)){
|
||||
Response response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request);
|
||||
DigestServerAuthenticationHelper.generateChallenge(getHeaderFactory(),response,sipConfig.getDomain());
|
||||
sender.send(senderIp,response);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("认证信息 => {}", authorization);
|
||||
boolean authPass = StringUtils.isBlank(password) ||
|
||||
DigestServerAuthenticationHelper.doAuthenticatePlainTextPassword(request,password);
|
||||
if(!authPass){
|
||||
Response response = getMessageFactory().createResponse(Response.FORBIDDEN, request);
|
||||
response.setReasonPhrase("Forbidden");
|
||||
log.info("设备注册信息认证失败 deviceId => {}", deviceId);
|
||||
sender.send(senderIp,response);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
log.debug("设备 deviceId => {}, 认证通过", deviceId);
|
||||
registerDevice(deviceId, request, senderIp, remoteInfo);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private Response generateRegisterResponse(Request request){
|
||||
SIPRequest sipRequest = (SIPRequest) request;
|
||||
ExpiresHeader expires = sipRequest.getExpires();
|
||||
if(expires == null){
|
||||
return getMessageFactory().createResponse(Response.BAD_REQUEST, request);
|
||||
}
|
||||
|
||||
Response response = getMessageFactory().createResponse(Response.OK, request);
|
||||
// 添加date头
|
||||
SIPDateHeader dateHeader = new SIPDateHeader();
|
||||
// GB28181 日期
|
||||
GbSipDate gbSipDate = new GbSipDate(Calendar.getInstance(Locale.ENGLISH).getTimeInMillis());
|
||||
dateHeader.setDate(gbSipDate);
|
||||
|
||||
response.addHeader(dateHeader);
|
||||
response.addHeader(sipRequest.getContactHeader());
|
||||
response.addHeader(expires);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private void registerDevice(String deviceId, SIPRequest request, String senderIp, RemoteInfo remoteInfo) {
|
||||
WvpProxyDocking device = new WvpProxyDocking();
|
||||
device.setGbDeviceId(deviceId);
|
||||
device.setIp(remoteInfo.getIp());
|
||||
device.setPort(String.valueOf(remoteInfo.getPort()));
|
||||
dockingService.addDevice(device);
|
||||
|
||||
Response response = generateRegisterResponse(request);
|
||||
sender.send(senderIp, response);
|
||||
|
||||
|
||||
ViaHeader viaHeader = request.getTopmostViaHeader();
|
||||
String transport = viaHeader.getTransport();
|
||||
|
||||
int expires = request.getExpires().getExpires();
|
||||
|
||||
// expires == 0 时 注销
|
||||
if (expires == 0) {
|
||||
log.info("设备注销 deviceId => {}", deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,329 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.request;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.redis.RedisUtil;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.cache.CacheUtil;
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.MessageHelper;
|
||||
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
|
||||
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.orm.mybatis.dynamic.model.WvpProxyDocking;
|
||||
import gov.nist.javax.sip.message.MessageFactoryImpl;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sip.SipFactory;
|
||||
import javax.sip.address.Address;
|
||||
import javax.sip.address.SipURI;
|
||||
import javax.sip.header.*;
|
||||
import javax.sip.message.Request;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
@DependsOn("proxySipConfig")
|
||||
@Component
|
||||
public class SipRequestBuilder implements ApplicationContextAware {
|
||||
private static ProxySipConfig sipConfig;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
sipConfig = applicationContext.getBean(ProxySipConfig.class);
|
||||
}
|
||||
|
||||
private static SipFactory getSipFactory(){
|
||||
return SipFactory.getInstance();
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private static List<ViaHeader> getViaHeaders(String ip,int port, String transport, String viaTag){
|
||||
ViaHeader viaHeader = getSipFactory().createHeaderFactory().createViaHeader(ip, port, transport, viaTag);
|
||||
viaHeader.setRPort();
|
||||
return Collections.singletonList(viaHeader);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private static CSeqHeader getCSeqHeader(long cSeq, String method){
|
||||
return getSipFactory().createHeaderFactory().createCSeqHeader(cSeq, method);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static Request createMessageRequest(WvpProxyDocking device, String ip, int port, long cSeq, String content, String fromTag, CallIdHeader callIdHeader) {
|
||||
Request request;
|
||||
String target = StringUtils.joinWith(":", device.getIp(), device.getPort());
|
||||
// sip uri
|
||||
SipURI requestURI = MessageHelper.createSipURI(device.getGbDeviceId(), target);
|
||||
|
||||
// via
|
||||
List<ViaHeader> viaHeaders = getViaHeaders(ip, port, sipConfig.getTransport(), null );
|
||||
|
||||
String from = StringUtils.joinWith(":", ip, port);
|
||||
// from
|
||||
SipURI fromSipURI = MessageHelper.createSipURI(device.getGbDeviceId(), from);
|
||||
Address fromAddress = MessageHelper.createAddress(fromSipURI);
|
||||
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
|
||||
// to
|
||||
SipURI toSipURI = MessageHelper.createSipURI(device.getGbDeviceId(), target);
|
||||
Address toAddress = MessageHelper.createAddress(toSipURI);
|
||||
ToHeader toHeader = MessageHelper.createToHeader(toAddress, null);
|
||||
|
||||
// Forwards
|
||||
MaxForwardsHeader maxForwards = MessageHelper.createMaxForwardsHeader(70);
|
||||
// ceq
|
||||
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(cSeq, Request.MESSAGE);
|
||||
|
||||
// 使用 GB28181 默认编码 否则中文将会乱码
|
||||
MessageFactoryImpl messageFactory = (MessageFactoryImpl) getSipFactory().createMessageFactory();
|
||||
messageFactory.setDefaultContentEncodingCharset(GB28181Constant.CHARSET);
|
||||
request = messageFactory.createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader,
|
||||
toHeader, viaHeaders, maxForwards);
|
||||
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
ContentTypeHeader contentTypeHeader = getSipFactory().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml");
|
||||
request.setContent(content, contentTypeHeader);
|
||||
return request;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static Request createMessageRequest(WvpProxyDocking device, String ip, int port,long cSeq,String content, String viaTag, String fromTag, CallIdHeader callIdHeader) {
|
||||
Request request;
|
||||
String target = StringUtils.joinWith(":", device.getIp(), device.getPort());
|
||||
// sip uri
|
||||
SipURI requestURI = MessageHelper.createSipURI(device.getGbDeviceId(), target);
|
||||
|
||||
// via
|
||||
List<ViaHeader> viaHeaders = getViaHeaders(ip, port, sipConfig.getTransport(), viaTag );
|
||||
|
||||
String from = StringUtils.joinWith(":", ip, port);
|
||||
// from
|
||||
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), from);
|
||||
Address fromAddress = MessageHelper.createAddress(fromSipURI);
|
||||
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
|
||||
// to
|
||||
SipURI toSipURI = MessageHelper.createSipURI(device.getGbDeviceId(), target);
|
||||
Address toAddress = MessageHelper.createAddress(toSipURI);
|
||||
ToHeader toHeader = MessageHelper.createToHeader(toAddress, null);
|
||||
|
||||
// Forwards
|
||||
MaxForwardsHeader maxForwards = MessageHelper.createMaxForwardsHeader(70);
|
||||
// ceq
|
||||
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(cSeq, Request.MESSAGE);
|
||||
|
||||
// 使用 GB28181 默认编码 否则中文将会乱码
|
||||
MessageFactoryImpl messageFactory = (MessageFactoryImpl) getSipFactory().createMessageFactory();
|
||||
messageFactory.setDefaultContentEncodingCharset(GB28181Constant.CHARSET);
|
||||
request = messageFactory.createRequest(requestURI, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader,
|
||||
toHeader, viaHeaders, maxForwards);
|
||||
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
ContentTypeHeader contentTypeHeader = getSipFactory().createHeaderFactory().createContentTypeHeader("Application", "MANSCDP+xml");
|
||||
request.setContent(content, contentTypeHeader);
|
||||
return request;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static Request createInviteRequest(String ip, int port, WvpProxyDocking device, String channelId, String content, String viaTag, String fromTag, String toTag, String ssrc, CallIdHeader callIdHeader){
|
||||
Request request;
|
||||
String target = StringUtils.joinWith(":", device.getIp(), device.getPort());
|
||||
SipURI requestLine = MessageHelper.createSipURI(channelId, target);
|
||||
// via
|
||||
List<ViaHeader> viaHeaders = getViaHeaders(ip, port, sipConfig.getTransport(), viaTag );
|
||||
// from
|
||||
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), sipConfig.getDomain());
|
||||
Address fromAddress = MessageHelper.createAddress(fromSipURI);
|
||||
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
|
||||
// to
|
||||
SipURI toSipURI = MessageHelper.createSipURI(channelId, target);
|
||||
Address toAddress = MessageHelper.createAddress(toSipURI);
|
||||
ToHeader toHeader = MessageHelper.createToHeader(toAddress, null);
|
||||
// Forwards
|
||||
MaxForwardsHeader maxForwards = getSipFactory().createHeaderFactory().createMaxForwardsHeader(70);
|
||||
// cSeq
|
||||
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(getCSeq(), Request.INVITE);
|
||||
request = getSipFactory().createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
Address concatAddress = MessageHelper.createAddress(MessageHelper.createSipURI(sipConfig.getId(), ip + ":" + port));
|
||||
|
||||
request.addHeader(getSipFactory().createHeaderFactory().createContactHeader(concatAddress));
|
||||
// Subject
|
||||
SubjectHeader subjectHeader = getSipFactory().createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0));
|
||||
request.addHeader(subjectHeader);
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
ContentTypeHeader contentTypeHeader = getSipFactory().createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP");
|
||||
request.setContent(content, contentTypeHeader);
|
||||
return request;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static Request createAckRequest(int status,String ip, int port, WvpProxyDocking device, String channelId, String fromTag, String toTag, String callId) {
|
||||
Request request;
|
||||
// 请求行
|
||||
String target = StringUtils.joinWith(":", device.getIp(), device.getPort());
|
||||
SipURI requestLine = MessageHelper.createSipURI(channelId, target);
|
||||
// via
|
||||
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
|
||||
ViaHeader viaHeader = getSipFactory().createHeaderFactory().createViaHeader(ip, port, sipConfig.getTransport(), SipUtil.generateViaTag());
|
||||
viaHeaders.add(viaHeader);
|
||||
// from
|
||||
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), sipConfig.getDomain());
|
||||
Address fromAddress = MessageHelper.createAddress(fromSipURI);
|
||||
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
|
||||
// to
|
||||
SipURI toSipURI = MessageHelper.createSipURI(channelId, target);
|
||||
Address toAddress = MessageHelper.createAddress(toSipURI);
|
||||
ToHeader toHeader = MessageHelper.createToHeader(toAddress, toTag);
|
||||
|
||||
// Forwards
|
||||
MaxForwardsHeader maxForwards = getSipFactory().createHeaderFactory().createMaxForwardsHeader(70);
|
||||
|
||||
// ceq
|
||||
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(getCSeq(), Request.ACK);
|
||||
CallIdHeader callIdHeader = getSipFactory().createHeaderFactory().createCallIdHeader(callId);
|
||||
request = getSipFactory().createMessageFactory().createRequest(requestLine, Request.ACK, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
|
||||
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
Address concatAddress = MessageHelper.createAddress(MessageHelper.createSipURI(sipConfig.getId(), ip + ":" + port));
|
||||
request.addHeader(getSipFactory().createHeaderFactory().createContactHeader(concatAddress));
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static Request createByeRequest(String ip, int port, WvpProxyDocking device, String channelId, String fromTag, String toTag, String callId) {
|
||||
Request request;
|
||||
// 请求行
|
||||
String target = StringUtils.joinWith(":", device.getIp(), device.getPort());
|
||||
SipURI requestLine = MessageHelper.createSipURI(channelId, target);
|
||||
// via
|
||||
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
|
||||
ViaHeader viaHeader = getSipFactory().createHeaderFactory().createViaHeader(ip, port, sipConfig.getTransport(), SipUtil.generateViaTag());
|
||||
viaHeaders.add(viaHeader);
|
||||
// from
|
||||
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), sipConfig.getDomain());
|
||||
Address fromAddress = MessageHelper.createAddress(fromSipURI);
|
||||
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
|
||||
// to
|
||||
SipURI toSipURI = MessageHelper.createSipURI(channelId, target);
|
||||
Address toAddress = MessageHelper.createAddress(toSipURI);
|
||||
ToHeader toHeader = MessageHelper.createToHeader(toAddress, toTag);
|
||||
|
||||
// Forwards
|
||||
MaxForwardsHeader maxForwards = getSipFactory().createHeaderFactory().createMaxForwardsHeader(70);
|
||||
|
||||
// ceq
|
||||
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(getCSeq(), Request.BYE);
|
||||
CallIdHeader callIdHeader = getSipFactory().createHeaderFactory().createCallIdHeader(callId);
|
||||
request = getSipFactory().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
|
||||
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
Address concatAddress = MessageHelper.createAddress(MessageHelper.createSipURI(sipConfig.getId(), ip + ":" + port));
|
||||
request.addHeader(getSipFactory().createHeaderFactory().createContactHeader(concatAddress));
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static Request createByeRequest(String ip, int port, long cSeq, String targetId, String fromTag, String toTag, String callId) {
|
||||
Request request;
|
||||
// 请求行
|
||||
String target = StringUtils.joinWith(":", ip, port);
|
||||
SipURI requestLine = MessageHelper.createSipURI(targetId, target);
|
||||
// via
|
||||
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
|
||||
ViaHeader viaHeader = getSipFactory().createHeaderFactory().createViaHeader(ip, port, sipConfig.getTransport(), SipUtil.generateViaTag());
|
||||
viaHeaders.add(viaHeader);
|
||||
// from
|
||||
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), sipConfig.getDomain());
|
||||
Address fromAddress = MessageHelper.createAddress(fromSipURI);
|
||||
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
|
||||
// to
|
||||
SipURI toSipURI = MessageHelper.createSipURI(targetId, target);
|
||||
Address toAddress = MessageHelper.createAddress(toSipURI);
|
||||
ToHeader toHeader = MessageHelper.createToHeader(toAddress, toTag);
|
||||
|
||||
// Forwards
|
||||
MaxForwardsHeader maxForwards = getSipFactory().createHeaderFactory().createMaxForwardsHeader(70);
|
||||
|
||||
// ceq
|
||||
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(cSeq, Request.BYE);
|
||||
CallIdHeader callIdHeader = getSipFactory().createHeaderFactory().createCallIdHeader(callId);
|
||||
request = getSipFactory().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
|
||||
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
Address concatAddress = MessageHelper.createAddress(MessageHelper.createSipURI(sipConfig.getId(), ip + ":" + port));
|
||||
request.addHeader(getSipFactory().createHeaderFactory().createContactHeader(concatAddress));
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static Request createByeRequest(String ip, int port, String targetId, String fromTag, String toTag, String callId) {
|
||||
Request request;
|
||||
// 请求行
|
||||
String target = StringUtils.joinWith(":", ip, port);
|
||||
SipURI requestLine = MessageHelper.createSipURI(targetId, target);
|
||||
// via
|
||||
ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
|
||||
ViaHeader viaHeader = getSipFactory().createHeaderFactory().createViaHeader(ip, port, sipConfig.getTransport(), SipUtil.generateViaTag());
|
||||
viaHeaders.add(viaHeader);
|
||||
// from
|
||||
SipURI fromSipURI = MessageHelper.createSipURI(sipConfig.getId(), sipConfig.getDomain());
|
||||
Address fromAddress = MessageHelper.createAddress(fromSipURI);
|
||||
FromHeader fromHeader = MessageHelper.createFromHeader(fromAddress, fromTag);
|
||||
// to
|
||||
SipURI toSipURI = MessageHelper.createSipURI(targetId, target);
|
||||
Address toAddress = MessageHelper.createAddress(toSipURI);
|
||||
ToHeader toHeader = MessageHelper.createToHeader(toAddress, toTag);
|
||||
|
||||
// Forwards
|
||||
MaxForwardsHeader maxForwards = getSipFactory().createHeaderFactory().createMaxForwardsHeader(70);
|
||||
|
||||
// ceq
|
||||
CSeqHeader cSeqHeader = getSipFactory().createHeaderFactory().createCSeqHeader(getCSeq(), Request.BYE);
|
||||
CallIdHeader callIdHeader = getSipFactory().createHeaderFactory().createCallIdHeader(callId);
|
||||
request = getSipFactory().createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader, fromHeader, toHeader, viaHeaders, maxForwards);
|
||||
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
Address concatAddress = MessageHelper.createAddress(MessageHelper.createSipURI(sipConfig.getId(), ip + ":" + port));
|
||||
request.addHeader(getSipFactory().createHeaderFactory().createContactHeader(concatAddress));
|
||||
request.addHeader(SipUtil.createUserAgentHeader());
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
public static long getCSeq() {
|
||||
String key = CacheUtil.getKey(CacheUtil.SIP_C_SEQ_PREFIX,sipConfig.getId());
|
||||
|
||||
long result = 1L;
|
||||
if(RedisUtil.KeyOps.hasKey(key)){
|
||||
try {
|
||||
result = RedisUtil.StringOps.incrBy(key,1L);
|
||||
} finally {
|
||||
if (result > Integer.MAX_VALUE) {
|
||||
RedisUtil.StringOps.set(key, String.valueOf(1L));
|
||||
result = 1L;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
RedisUtil.StringOps.set(key, String.valueOf(result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.response;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.gb28181.constant.GB28181Constant;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.MessageHelper;
|
||||
import cn.skcks.docking.gb28181.core.sip.utils.SipUtil;
|
||||
import cn.skcks.docking.gb28181.sdp.GB28181Description;
|
||||
import gov.nist.javax.sip.message.MessageFactoryImpl;
|
||||
import gov.nist.javax.sip.message.SIPRequest;
|
||||
import gov.nist.javax.sip.message.SIPResponse;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.sip.SipFactory;
|
||||
import javax.sip.address.Address;
|
||||
import javax.sip.address.SipURI;
|
||||
import javax.sip.header.ContentTypeHeader;
|
||||
import javax.sip.header.MaxForwardsHeader;
|
||||
import javax.sip.message.Response;
|
||||
|
||||
@Slf4j
|
||||
public class SipResponseBuilder {
|
||||
@SneakyThrows
|
||||
public static Response response(SIPRequest request, int status, String message){
|
||||
if (request.getToHeader().getTag() == null) {
|
||||
request.getToHeader().setTag(SipUtil.generateTag());
|
||||
}
|
||||
|
||||
MessageFactoryImpl messageFactory = (MessageFactoryImpl)MessageHelper.getSipFactory().createMessageFactory();
|
||||
// 使用 GB28181 默认编码 否则中文将会乱码
|
||||
messageFactory.setDefaultContentEncodingCharset(GB28181Constant.CHARSET);
|
||||
SIPResponse response = (SIPResponse)messageFactory.createResponse(status, request);
|
||||
if (message != null) {
|
||||
response.setReasonPhrase(message);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static Response responseSdp(SIPRequest request, GB28181Description sdp) {
|
||||
MessageFactoryImpl messageFactory = (MessageFactoryImpl)MessageHelper.getSipFactory().createMessageFactory();
|
||||
// 使用 GB28181 默认编码 否则中文将会乱码
|
||||
messageFactory.setDefaultContentEncodingCharset(GB28181Constant.CHARSET);
|
||||
SIPResponse response = (SIPResponse)messageFactory.createResponse(Response.OK, request);
|
||||
SipFactory sipFactory = SipFactory.getInstance();
|
||||
ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("application", "sdp");
|
||||
response.setContent(sdp.toString(), contentTypeHeader);
|
||||
SipURI sipURI = (SipURI) request.getRequestURI();
|
||||
SipURI uri = MessageHelper.createSipURI(sipURI.getUser(), StringUtils.joinWith(":", sipURI.getHost() + ":" + sipURI.getPort()));
|
||||
Address concatAddress = sipFactory.createAddressFactory().createAddress(uri);
|
||||
MaxForwardsHeader maxForwardsHeader = MessageHelper.createMaxForwardsHeader(70);
|
||||
response.setMaxForwards(maxForwardsHeader);
|
||||
response.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
|
||||
return response;
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.sender;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.service.SipService;
|
||||
import cn.skcks.docking.gb28181.wvp.config.ProxySipConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sip.ListeningPoint;
|
||||
import javax.sip.SipException;
|
||||
import javax.sip.SipProvider;
|
||||
import javax.sip.message.Request;
|
||||
import javax.sip.message.Response;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
|
||||
@Component
|
||||
public class SipSender {
|
||||
private final SipService sipService;
|
||||
private final ProxySipConfig sipConfig;
|
||||
|
||||
public SipProvider getProvider(String transport, String ip) {
|
||||
return sipService.getProvider(transport, ip);
|
||||
}
|
||||
|
||||
public List<SipProvider> getProviders() {
|
||||
return sipConfig.getIp().stream().map(item -> getProvider(sipConfig.getTransport(), item))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public void sendResponse(SipProvider sipProvider, SendResponse response) {
|
||||
log.info("{}", sipProvider);
|
||||
ListeningPoint[] listeningPoints = sipProvider.getListeningPoints();
|
||||
if (listeningPoints == null || listeningPoints.length == 0) {
|
||||
log.error("发送响应失败, 未找到有效的监听地址");
|
||||
return;
|
||||
}
|
||||
ListeningPoint listeningPoint = listeningPoints[0];
|
||||
String ip = listeningPoint.getIPAddress();
|
||||
int port = listeningPoint.getPort();
|
||||
try {
|
||||
sipProvider.sendResponse(response.build(sipProvider, ip, port));
|
||||
} catch (SipException e) {
|
||||
log.error("向{} {}:{} 发送响应失败, 异常: {}", ip, listeningPoint.getPort(), listeningPoint.getTransport(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void sendResponse(String senderIp,String transport, SendResponse response) {
|
||||
SipProvider sipProvider = getProvider(transport, senderIp);
|
||||
sendResponse(sipProvider, response);
|
||||
}
|
||||
|
||||
public void sendRequest(SendRequest request) {
|
||||
getProviders().parallelStream().forEach(sipProvider -> {
|
||||
log.info("{}", sipProvider);
|
||||
ListeningPoint[] listeningPoints = sipProvider.getListeningPoints();
|
||||
if (listeningPoints == null || listeningPoints.length == 0) {
|
||||
log.error("发送请求失败, 未找到有效的监听地址");
|
||||
return;
|
||||
}
|
||||
ListeningPoint listeningPoint = listeningPoints[0];
|
||||
String ip = listeningPoint.getIPAddress();
|
||||
int port = listeningPoint.getPort();
|
||||
try {
|
||||
sipProvider.sendRequest(request.build(sipProvider, ip, port));
|
||||
} catch (SipException e) {
|
||||
log.error("向{} {}:{} 发送请求失败, 异常: {}", ip, listeningPoint.getPort(), listeningPoint.getTransport(), e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface SendRequest {
|
||||
Request build(SipProvider provider, String ip, int port);
|
||||
}
|
||||
|
||||
public interface SendResponse {
|
||||
Response build(SipProvider provider, String ip, int port);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.subscribe;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
|
||||
import gov.nist.javax.sip.message.SIPRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Flow;
|
||||
import java.util.concurrent.SubmissionPublisher;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class ByeSubscribe implements GenericSubscribe<SIPRequest> {
|
||||
private final Executor executor;
|
||||
|
||||
private static final Map<String, SubmissionPublisher<SIPRequest>> publishers = new ConcurrentHashMap<>();
|
||||
|
||||
public void close() {
|
||||
Helper.close(publishers);
|
||||
}
|
||||
|
||||
public void addPublisher(String key) {
|
||||
Helper.addPublisher(executor, publishers, key);
|
||||
}
|
||||
|
||||
public SubmissionPublisher<SIPRequest> getPublisher(String key) {
|
||||
return Helper.getPublisher(publishers, key);
|
||||
}
|
||||
|
||||
public void addSubscribe(String key, Flow.Subscriber<SIPRequest> subscribe) {
|
||||
Helper.addSubscribe(publishers, key, subscribe);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delPublisher(String key) {
|
||||
Helper.delPublisher(publishers, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void complete(String key) {
|
||||
delPublisher(key);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.subscribe;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
|
||||
import gov.nist.javax.sip.message.SIPRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Flow;
|
||||
import java.util.concurrent.SubmissionPublisher;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class CatalogSubscribe implements GenericSubscribe<SIPRequest> {
|
||||
private final Executor executor;
|
||||
|
||||
private static final Map<String, SubmissionPublisher<SIPRequest>> publishers = new ConcurrentHashMap<>();
|
||||
|
||||
public void close() {
|
||||
Helper.close(publishers);
|
||||
}
|
||||
|
||||
public void addPublisher(String key) {
|
||||
Helper.addPublisher(executor, publishers, key);
|
||||
}
|
||||
|
||||
public SubmissionPublisher<SIPRequest> getPublisher(String key) {
|
||||
return Helper.getPublisher(publishers, key);
|
||||
}
|
||||
|
||||
public void addSubscribe(String key, Flow.Subscriber<SIPRequest> subscribe) {
|
||||
Helper.addSubscribe(publishers, key, subscribe);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delPublisher(String key) {
|
||||
Helper.delPublisher(publishers, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void complete(String key) {
|
||||
delPublisher(key);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.subscribe;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import cn.skcks.docking.gb28181.service.record.convertor.RecordConvertor;
|
||||
import cn.skcks.docking.gb28181.service.record.vo.RecordInfoItemVO;
|
||||
import cn.skcks.docking.gb28181.sip.manscdp.recordinfo.response.RecordInfoItemDTO;
|
||||
import cn.skcks.docking.gb28181.sip.manscdp.recordinfo.response.RecordInfoResponseDTO;
|
||||
import cn.skcks.docking.gb28181.sip.utils.MANSCDPUtils;
|
||||
import gov.nist.javax.sip.message.SIPRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.context.request.async.DeferredResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Flow;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class RecordSubscribe implements Flow.Subscriber<SIPRequest>{
|
||||
private final SipSubscribe subscribe;
|
||||
private final String key;
|
||||
private final DeferredResult<JsonResponse<List<RecordInfoItemVO>>> result;
|
||||
private final String deviceId;
|
||||
|
||||
private final List<RecordInfoItemDTO> list = new ArrayList<>();
|
||||
private final AtomicLong atomicSum = new AtomicLong(0);
|
||||
private final AtomicLong atomicNum = new AtomicLong(0);
|
||||
private Flow.Subscription subscription;
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Flow.Subscription subscription) {
|
||||
this.subscription = subscription;
|
||||
log.debug("建立订阅 => {}", key);
|
||||
subscription.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(SIPRequest item) {
|
||||
RecordInfoResponseDTO data = MANSCDPUtils.parse(item.getRawContent(), RecordInfoResponseDTO.class);
|
||||
atomicSum.set(Math.max(data.getSumNum(), atomicNum.get()));
|
||||
atomicNum.addAndGet(data.getRecordList().getNum());
|
||||
list.addAll(data.getRecordList().getRecordList());
|
||||
long num = atomicNum.get();
|
||||
long sum = atomicSum.get();
|
||||
if(num > sum){
|
||||
log.warn("检测到 设备 => {}, 未按规范实现, 订阅 => {}, 期望总数为 => {}, 已接收数量 => {}", deviceId, key, atomicSum.get(), atomicNum.get());
|
||||
} else {
|
||||
log.info("获取订阅 => {}, {}/{}", key, atomicNum.get(), atomicSum.get());
|
||||
}
|
||||
|
||||
if (num >= sum) {
|
||||
// 针对某些不按规范的设备
|
||||
// 如果已获取数量 >= 约定的总数
|
||||
// 就执行定时任务, 若 500ms 内未收到新的数据视为已结束
|
||||
subscribe.getMessageSubscribe().refreshPublisher(key,500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
subscription.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
result.setResult(JsonResponse.success(RecordConvertor.INSTANCE.dto2Vo(sortedRecordList(list))));
|
||||
log.debug("订阅结束 => {}", key);
|
||||
subscribe.getMessageSubscribe().delPublisher(key);
|
||||
}
|
||||
|
||||
private List<RecordInfoItemDTO> sortedRecordList(List<RecordInfoItemDTO> list){
|
||||
return list.stream().sorted((a,b)-> DateUtil.compare(a.getStartTime(),b.getStartTime())).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
package cn.skcks.docking.gb28181.wvp.sip.subscribe;
|
||||
|
||||
import cn.skcks.docking.gb28181.core.sip.executor.DefaultSipExecutor;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericSubscribe;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.GenericTimeoutSubscribe;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.InviteSubscribe;
|
||||
import cn.skcks.docking.gb28181.core.sip.message.subscribe.SipRequestSubscribe;
|
||||
import gov.nist.javax.sip.message.SIPRequest;
|
||||
import gov.nist.javax.sip.message.SIPResponse;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
@Slf4j
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class SipSubscribe {
|
||||
@Qualifier(DefaultSipExecutor.EXECUTOR_BEAN_NAME)
|
||||
private final Executor executor;
|
||||
private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());
|
||||
private GenericSubscribe<SIPRequest> catalogSubscribe;
|
||||
private GenericSubscribe<SIPResponse> inviteSubscribe;
|
||||
private GenericSubscribe<SIPRequest> byeSubscribe;
|
||||
private GenericTimeoutSubscribe<SIPRequest> messageSubscribe;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
catalogSubscribe = new CatalogSubscribe(executor);
|
||||
inviteSubscribe = new InviteSubscribe(executor);
|
||||
byeSubscribe = new ByeSubscribe(executor);
|
||||
messageSubscribe = new SipRequestSubscribe(executor, scheduledExecutorService);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
private void destroy() {
|
||||
catalogSubscribe.close();
|
||||
inviteSubscribe.close();
|
||||
byeSubscribe.close();
|
||||
messageSubscribe.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
package cn.skcks.docking.gb28181.wvp.utils;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import com.github.rholder.retry.*;
|
||||
import com.google.common.base.Predicate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
public class RetryUtil {
|
||||
/**
|
||||
* 默认重试次数
|
||||
*/
|
||||
public final static int DEFAULT_RETRY_TIME = 3;
|
||||
/**
|
||||
* 默认每次重试等待时间
|
||||
*/
|
||||
public final static long DEFAULT_RETRY_INTERVAL = 3;
|
||||
public final static TimeUnit DEFAULT_RETRY_INTERVAL_UNIT = TimeUnit.SECONDS;
|
||||
|
||||
public static RetryListener defaultRetryListener(String name) {
|
||||
return new RetryListener() {
|
||||
@Override
|
||||
public <V> void onRetry(Attempt<V> attempt) {
|
||||
log.info("第 {} 次 执行 {} 结束", attempt.getAttemptNumber(), name);
|
||||
if (attempt.hasException()) {
|
||||
log.info("执行 {} 异常 {}", name, attempt.getExceptionCause().getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static <T> Predicate<JsonResponse<T>> defaultRetryIf(){
|
||||
return result -> result == null || (result.getCode() != 0 && result.getCode() != 200);
|
||||
}
|
||||
|
||||
public static Predicate<JsonResponse<?>> defaultGenericRetryIf(){
|
||||
return result -> result == null || (result.getCode() != 0 && result.getCode() != 200);
|
||||
}
|
||||
|
||||
public static <T> RetryerBuilder<JsonResponse<T>> getDefaultRetryerBuilder(String name, int waitTime, TimeUnit waitUnit, int retryTime) {
|
||||
return RetryerBuilder.<JsonResponse<T>>newBuilder()
|
||||
// 异常就重试
|
||||
.retryIfException()
|
||||
.retryIfRuntimeException()
|
||||
// 重试间隔
|
||||
.withWaitStrategy(WaitStrategies.fixedWait(waitTime, waitUnit))
|
||||
// 重试次数
|
||||
.withStopStrategy(StopStrategies.stopAfterAttempt(retryTime))
|
||||
.retryIfResult(defaultRetryIf())
|
||||
.withRetryListener(defaultRetryListener(name));
|
||||
}
|
||||
|
||||
public static <T> RetryerBuilder<JsonResponse<T>> getDefaultRetryerBuilder(String name) {
|
||||
return RetryerBuilder.<JsonResponse<T>>newBuilder()
|
||||
// 异常就重试
|
||||
.retryIfException()
|
||||
.retryIfRuntimeException();
|
||||
}
|
||||
|
||||
|
||||
public static <T> Retryer<JsonResponse<T>> getDefaultRetryer(String name) {
|
||||
return RetryUtil.<T>getDefaultRetryerBuilder(name)
|
||||
// 重试间隔
|
||||
.withWaitStrategy(WaitStrategies.fixedWait(DEFAULT_RETRY_INTERVAL, TimeUnit.SECONDS))
|
||||
// 重试次数
|
||||
.withStopStrategy(StopStrategies.stopAfterAttempt(DEFAULT_RETRY_TIME))
|
||||
.retryIfResult(defaultRetryIf())
|
||||
.withRetryListener(defaultRetryListener(name))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static RetryerBuilder<JsonResponse<?>> getDefaultGenericRetryerBuilder(String name, int retryTime, long retryInterval, TimeUnit retryTimeUnit,RetryListener retryListener) {
|
||||
return RetryerBuilder.<JsonResponse<?>>newBuilder()
|
||||
// 异常就重试
|
||||
.retryIfException()
|
||||
.retryIfRuntimeException()
|
||||
// 重试间隔
|
||||
.withWaitStrategy(WaitStrategies.fixedWait(retryInterval, retryTimeUnit))
|
||||
// 重试次数
|
||||
.withStopStrategy(StopStrategies.stopAfterAttempt(retryTime))
|
||||
.retryIfResult(defaultGenericRetryIf())
|
||||
.withRetryListener(retryListener);
|
||||
}
|
||||
|
||||
public static RetryerBuilder<JsonResponse<?>> getDefaultGenericRetryerBuilder(String name) {
|
||||
return getDefaultGenericRetryerBuilder(name, DEFAULT_RETRY_TIME, DEFAULT_RETRY_INTERVAL, DEFAULT_RETRY_INTERVAL_UNIT, defaultRetryListener(name));
|
||||
}
|
||||
|
||||
public static Retryer<JsonResponse<?>> getDefaultGenericRetryer(String name, int retryTime, long retryInterval, TimeUnit retryTimeUnit, RetryListener retryListener) {
|
||||
return getDefaultGenericRetryerBuilder(name, retryTime, retryInterval, retryTimeUnit, retryListener).build();
|
||||
}
|
||||
|
||||
public static Retryer<JsonResponse<?>> getDefaultGenericRetryer(String name, int retryTime, long retryInterval, TimeUnit retryTimeUnit) {
|
||||
return getDefaultGenericRetryer(name, retryTime, retryInterval, retryTimeUnit, defaultRetryListener(name));
|
||||
}
|
||||
|
||||
public static Retryer<JsonResponse<?>> getDefaultGenericRetryer(String name, int retryTime, long retryInterval) {
|
||||
return getDefaultGenericRetryer(name, retryTime, retryInterval, DEFAULT_RETRY_INTERVAL_UNIT);
|
||||
}
|
||||
|
||||
public static Retryer<JsonResponse<?>> getDefaultGenericRetryer(String name) {
|
||||
return getDefaultGenericRetryer(name, DEFAULT_RETRY_TIME, DEFAULT_RETRY_INTERVAL);
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
proxy:
|
||||
wvp:
|
||||
url: http://192.168.3.13:18978
|
||||
user: admin
|
||||
passwd: admin
|
43
gb28181-wvp-proxy-service/src/main/resources/application.yml
Normal file
43
gb28181-wvp-proxy-service/src/main/resources/application.yml
Normal file
@ -0,0 +1,43 @@
|
||||
server:
|
||||
port: 18183
|
||||
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
# [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
|
||||
# host: 192.168.1.241
|
||||
host: 10.10.10.200
|
||||
# [必须修改] 端口号
|
||||
port: 16379
|
||||
# [可选] 数据库 DB
|
||||
database: 15
|
||||
# [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
|
||||
password: 12341234
|
||||
# [可选] 超时时间
|
||||
timeout: 10000
|
||||
datasource:
|
||||
type: com.zaxxer.hikari.HikariDataSource
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
username: root
|
||||
password: 12341234
|
||||
url: jdbc:mysql://10.10.10.200:3306/gb28181_docking_platform?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
|
||||
cloud:
|
||||
openfeign:
|
||||
httpclient:
|
||||
connection-timeout: 0
|
||||
ok-http:
|
||||
read-timeout: 0
|
||||
|
||||
media:
|
||||
ip: 10.10.10.200
|
||||
url: 'http://10.10.10.200:5080'
|
||||
# url: 'http://10.10.10.200:12580/anything/'
|
||||
id: amrWMKmbKqoBjRQ9
|
||||
# secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
|
||||
secret: 4155cca6-2f9f-11ee-85e6-8de4ce2e7333
|
||||
|
||||
proxy:
|
||||
wvp:
|
||||
url: http://192.168.3.13:18978
|
||||
user: admin
|
||||
passwd: admin
|
@ -1,7 +1,9 @@
|
||||
package cn.skcks.docking.gb28181.wvp.test;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.crypto.digest.MD5;
|
||||
import cn.skcks.docking.gb28181.common.json.JsonResponse;
|
||||
import cn.skcks.docking.gb28181.wvp.WvpProxyTestApplication;
|
||||
import cn.skcks.docking.gb28181.wvp.config.WvpProxyConfig;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.device.GetDeviceChannelsResp;
|
||||
@ -11,17 +13,24 @@ import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginReq;
|
||||
import cn.skcks.docking.gb28181.wvp.dto.login.WvpLoginResp;
|
||||
import cn.skcks.docking.gb28181.wvp.proxy.WvpProxyClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.core5.io.CloseMode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootTest
|
||||
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
|
||||
@SpringBootTest(classes = WvpProxyTestApplication.class)
|
||||
@ExtendWith(SpringExtension.class)
|
||||
public class WvpProxyTest {
|
||||
@Autowired
|
||||
@ -55,4 +64,24 @@ public class WvpProxyTest {
|
||||
log.info("{}", item);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadTest() throws IOException {
|
||||
final CloseableHttpClient client = HttpClients.custom()
|
||||
.build();
|
||||
|
||||
String tmpDir = new File(System.getProperty("java.io.tmpdir")).getAbsolutePath();
|
||||
File file = new File(Paths.get(tmpDir,"test.mp4").toUri());
|
||||
log.info("临时文件 路径 => {}",file.getAbsolutePath());
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
|
||||
String url = "http://192.168.1.241:18979/download/recordTemp/0490d767d94ce20aedce57c862b6bfe9/rtp/59777645.mp4";
|
||||
HttpGet httpGet = new HttpGet(url);
|
||||
client.execute(httpGet, response -> {
|
||||
InputStream stream = response.getEntity().getContent();
|
||||
IoUtil.copy(stream,outputStream);
|
||||
return stream;
|
||||
});
|
||||
client.close(CloseMode.GRACEFUL);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
package cn.skcks.docking.gb28181.wvp.test;
|
||||
|
||||
import cn.skcks.docking.gb28181.common.xml.XmlUtils;
|
||||
import cn.skcks.docking.gb28181.wvp.sip.message.message.device.control.DeviceControlDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class XmlTest {
|
||||
@Test
|
||||
public void test(){
|
||||
DeviceControlDTO deviceControlDTO = DeviceControlDTO.builder()
|
||||
.sn("100")
|
||||
.deviceId("123456")
|
||||
.recordCmd("PreRecord")
|
||||
.build();
|
||||
log.info("\n{}", XmlUtils.toXml(deviceControlDTO));
|
||||
}
|
||||
}
|
@ -3,11 +3,17 @@ FROM eclipse-temurin:17-jre-focal
|
||||
MAINTAINER Shikong <919411476@qq.com>
|
||||
|
||||
ENV HOME_PATH /opt/gb28181-docking-platform-wvp-proxy/
|
||||
ENV TZ Asia/Shanghai
|
||||
RUN mkdir -p $HOME_PATH
|
||||
WORKDIR $HOME_PATH
|
||||
|
||||
RUN mkdir -p /usr/bin/ffmpeg
|
||||
ADD ./ffmpeg/ffmpeg /usr/bin/ffmpeg/ffmpeg
|
||||
ADD ./ffmpeg/ffprobe /usr/bin/ffmpeg/ffprobe
|
||||
ADD ./ffmpeg/model /usr/bin/ffmpeg/model
|
||||
|
||||
ADD target/gb28181-wvp-proxy-starter.jar /opt/gb28181-docking-platform-wvp-proxy/starter.jar
|
||||
|
||||
EXPOSE 18182
|
||||
EXPOSE 18183
|
||||
|
||||
ENTRYPOINT ["java", "-jar","/opt/gb28181-docking-platform-wvp-proxy/starter.jar"]
|
||||
|
@ -4,11 +4,13 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@EnableFeignClients(basePackages = {
|
||||
"cn.skcks.docking.gb28181.media",
|
||||
"cn.skcks.docking.gb28181.wvp.proxy"
|
||||
"cn.skcks.docking.gb28181.wvp.proxy",
|
||||
"cn.skcks.docking.gb28181.wvp.service.report"
|
||||
})
|
||||
@SpringBootApplication
|
||||
@ComponentScan(basePackages = {
|
||||
@ -16,6 +18,25 @@ import org.springframework.scheduling.annotation.EnableAsync;
|
||||
"cn.skcks.docking.gb28181.common",
|
||||
"cn.skcks.docking.gb28181.wvp",
|
||||
"cn.skcks.docking.gb28181.media",
|
||||
"cn.skcks.docking.gb28181"
|
||||
}, includeFilters = {
|
||||
@ComponentScan.Filter(type = FilterType.REGEX, pattern = {
|
||||
"cn.skcks.docking.gb28181.service.ssrc.*",
|
||||
})
|
||||
},
|
||||
excludeFilters = {
|
||||
@ComponentScan.Filter(type = FilterType.REGEX, pattern = {
|
||||
"cn.skcks.docking.gb28181.starter.*",
|
||||
"cn.skcks.docking.gb28181.config.sip.SipConfig",
|
||||
"cn.skcks.docking.gb28181.core.sip.listener.*",
|
||||
"cn.skcks.docking.gb28181.core.sip.message.processor.*",
|
||||
"cn.skcks.docking.gb28181.core.sip.message.subscribe.*",
|
||||
"cn.skcks.docking.gb28181.service.play.*",
|
||||
"cn.skcks.docking.gb28181.service.record.*",
|
||||
"cn.skcks.docking.gb28181.core.sip.message.request.*",
|
||||
"cn.skcks.docking.gb28181.service.catalog.*",
|
||||
"cn.skcks.docking.gb28181.service.notify.*"
|
||||
})
|
||||
})
|
||||
@EnableAsync
|
||||
public class Gb28181WvpProxyStarter {
|
||||
|
@ -1,5 +1,5 @@
|
||||
server:
|
||||
port: 18186
|
||||
port: 18183
|
||||
project:
|
||||
version: @project.version@
|
||||
|
||||
@ -32,16 +32,88 @@ spring:
|
||||
connection-timeout: 0
|
||||
ok-http:
|
||||
read-timeout: 0
|
||||
|
||||
media:
|
||||
ip: 10.10.10.200
|
||||
url: 'http://10.10.10.200:5080'
|
||||
url: 'https://10.10.10.200:5444'
|
||||
# url: 'http://10.10.10.200:12580/anything/'
|
||||
id: amrWMKmbKqoBjRQ9
|
||||
# secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
|
||||
secret: 4155cca6-2f9f-11ee-85e6-8de4ce2e7333
|
||||
rtmp-port: 1936
|
||||
|
||||
proxy:
|
||||
wvp:
|
||||
url: http://192.168.3.13:18978
|
||||
url: http://127.0.0.1:18978
|
||||
# url: http://192.168.3.12:18978
|
||||
user: admin
|
||||
passwd: admin
|
||||
use-ffmpeg: true
|
||||
# 是否使用 wvp 的 api(wvp 的 并发有问题,仅保留用于兼容), 否则使用sip 信令直接操作设备
|
||||
enable: false
|
||||
# 是否使用 ffmpeg 编/解码, 否则使用内置 javacv
|
||||
parents:
|
||||
- 44050100002000000002
|
||||
- 44050100002000000003
|
||||
- 44050100001180000001
|
||||
- 44050100001320000001
|
||||
- 44050100001110000010
|
||||
# 用于生成 代理 wvp 的 视频流 ws-flv 地址
|
||||
#proxy-media-url: 'wss://192.168.1.241:9022/mf-config/media'
|
||||
proxy-media-url: 'ws://10.10.10.200:5080'
|
||||
# 实时视频单次点播持续时间 (默认: 15分钟)
|
||||
realtime-video-duration: 15m
|
||||
gb28181:
|
||||
sip:
|
||||
id: 44050100002000000003
|
||||
# id: 44050100002000000005
|
||||
domain: 4405010000
|
||||
password: 123456
|
||||
port: 5063
|
||||
ip:
|
||||
- 10.10.10.20
|
||||
# - 192.168.0.195
|
||||
stream-mode: udp
|
||||
use-playback-to-download: false
|
||||
# proxy-media-url: 'https://10.10.10.200:18181/media'
|
||||
proxy-media-url: 'https://10.10.10.200:5444'
|
||||
use-record-info-query-before-download: true
|
||||
retry-record-info-query-before-download-interval: 3
|
||||
retry-record-info-query-before-download-times: 20
|
||||
retry-record-info-query-before-download-interval-unit: seconds
|
||||
# - 192.168.1.241
|
||||
device-api:
|
||||
offset:
|
||||
forward: 0s
|
||||
back: 0s
|
||||
|
||||
ffmpeg-support:
|
||||
ffmpeg: D:\Soft\Captura\ffmpeg\ffmpeg.exe
|
||||
ffprobe: D:\Soft\Captura\ffmpeg\ffprobe.exe
|
||||
rtp:
|
||||
# input: -i http://10.10.10.200:5080/live/test.live.flv
|
||||
input: -re -i
|
||||
# output: -preset ultrafast -vcodec libx264 -acodec aac -movflags empty_moov+frag_keyframe+default_base_moof -vsync 2 -copyts -f flv # -rtsp_transport tcp
|
||||
# output: -enc_time_base -1 -preset ultrafast -tune zerolatency -vcodec libx264 -an -movflags faststart -f flv # -rtsp_transport tcp
|
||||
#output: -c:v libx264 -an -f flv # -rtsp_transport tcp
|
||||
output: -c:v copy -an -f flv
|
||||
#download: -rw_timeout 30000000 -rtmp_live recorded -tcp_nodelay 1 -thread_queue_size 1 -i
|
||||
download: -rw_timeout 30000000 -rtmp_live recorded -tcp_nodelay 1 -thread_queue_size 1 -i
|
||||
log-level: error
|
||||
# download: -rtmp_live recorded -tcp_nodelay 1 -thread_queue_size 128 -i
|
||||
debug:
|
||||
download: false
|
||||
input: false
|
||||
output: false
|
||||
tmp-dir: G:\Temp\record\download-proxy
|
||||
use-tmp-file: true
|
||||
|
||||
# [可选] 日志配置, 一般不需要改
|
||||
logging:
|
||||
config: classpath:logback.xml
|
||||
|
||||
report:
|
||||
enabled: false
|
||||
url: http://127.0.0.1:8080/api/report
|
||||
custom-headers:
|
||||
agent: gb28181-proxy
|
||||
|
@ -1,10 +1,9 @@
|
||||
server:
|
||||
port: 18186
|
||||
port: 18183
|
||||
|
||||
project:
|
||||
version: @project.version@
|
||||
|
||||
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
@ -24,7 +23,7 @@ spring:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
username: root
|
||||
password: 123456a
|
||||
url: jdbc:mysql://192.168.1.241:3306/gb28181_docking_platform?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
|
||||
url: jdbc:mysql://192.168.1.241:3306/gb28181_docking_platform_dev?createDatabaseIfNotExist=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
|
||||
profiles:
|
||||
active: local
|
||||
cloud:
|
||||
@ -33,6 +32,9 @@ spring:
|
||||
connection-timeout: 0
|
||||
ok-http:
|
||||
read-timeout: 0
|
||||
hc5:
|
||||
connection-request-timeout: 60
|
||||
connection-request-timeout-unit: seconds
|
||||
|
||||
media:
|
||||
ip: 192.168.1.241
|
||||
@ -44,6 +46,63 @@ media:
|
||||
|
||||
proxy:
|
||||
wvp:
|
||||
url: http://192.168.3.13:18978
|
||||
url: http://192.168.3.12:18978
|
||||
user: admin
|
||||
passwd: admin
|
||||
use-wvp-assist: false
|
||||
# 是否使用 wvp 的 api(wvp 的 并发有问题,仅保留用于兼容), 否则使用sip 信令直接操作设备
|
||||
enable: false
|
||||
# 是否使用 ffmpeg 编/解码, 否则使用内置 javacv
|
||||
use-ffmpeg: false
|
||||
parents:
|
||||
- 44050100002000000003
|
||||
- 44050100001180000001
|
||||
# 用于生成 代理 wvp 的 视频流 ws-flv 地址
|
||||
proxy-media-url: 'wss://192.168.1.241:9022/mf-config/media'
|
||||
# 实时视频单次点播持续时间 (默认: 15分钟)
|
||||
realtime-video-duration: 15m
|
||||
gb28181:
|
||||
sip:
|
||||
id: 44050100002000000005
|
||||
domain: 4405010000
|
||||
password: 123456
|
||||
port: 5063
|
||||
ip:
|
||||
- 192.168.0.195
|
||||
# - 192.168.3.10
|
||||
# - 192.168.1.241
|
||||
stream-mode: tcp_passive
|
||||
use-playback-to-download: false
|
||||
# 用于替换 返回的 url 值, 可用 nginx 或 caddy 代理 zlm
|
||||
# proxy-media-url: 'http://10.10.10.200/media'
|
||||
proxy-media-url: 'https://192.168.1.241:9022/mf-config/media'
|
||||
device-api:
|
||||
offset:
|
||||
forward: 0s
|
||||
back: 0s
|
||||
|
||||
ffmpeg-support:
|
||||
ffmpeg: /usr/bin/ffmpeg/ffmpeg
|
||||
# ffmpeg: C:\ffmpeg\bin\ffmpeg.exe
|
||||
ffprobe: /usr/bin/ffmpeg/ffprobe
|
||||
rtp:
|
||||
input: -re -i
|
||||
#output: -preset ultrafast -tune zerolatency -vcodec libx264 -acodec aac -movflags empty_moov+frag_keyframe+default_base_moof -f mp4 # -rtsp_transport tcp
|
||||
# output: -enc_time_base -1 -preset ultrafast -tune zerolatency -vcodec libx264 -an -movflags empty_moov+frag_keyframe+default_base_moof -f mp4 # -rtsp_transport tcp
|
||||
output: -enc_time_base -1 -preset ultrafast -tune zerolatency -vcodec libx264 -an -movflags faststart -f mp4 # -rtsp_transport tcp
|
||||
download: -thread_queue_size 128 -fflags +genpts -enc_time_base -1 -i
|
||||
debug:
|
||||
download: false
|
||||
input: false
|
||||
output: false
|
||||
|
||||
# [可选] 日志配置, 一般不需要改
|
||||
logging:
|
||||
config: classpath:logback.xml
|
||||
|
||||
|
||||
report:
|
||||
enabled: false
|
||||
url: http://127.0.0.1:8080/api/report
|
||||
custom-headers:
|
||||
agent: gb28181-proxy
|
||||
|
54
gb28181-wvp-proxy-starter/src/main/resources/logback.xml
Normal file
54
gb28181-wvp-proxy-starter/src/main/resources/logback.xml
Normal file
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<configuration scan="true" scanPeriod="60 seconds" debug="false">
|
||||
|
||||
<contextName>logback</contextName>
|
||||
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
|
||||
<!--<property name="log.path" value="./log/business_Log" />-->
|
||||
|
||||
<!--输出到控制台-->
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter">-->
|
||||
<!-- <level>INFO</level>-->
|
||||
<!-- </filter>-->
|
||||
<!-- <withJansi>true</withJansi>-->
|
||||
<encoder>
|
||||
<!--<pattern>%d %p (%file:%line\)- %m%n</pattern>-->
|
||||
<!--格式化输出:%d:表示日期 %thread:表示线程名 %-5level:级别从左显示5个字符宽度 %msg:日志消息 %n:是换行符-->
|
||||
<pattern>%red(%d{yyyy-MM-dd HH:mm:ss.SSS}) %green([%thread]) %highlight(%-5level) %yellow(at %class.%method) (%file:%line\) - %cyan(%msg%n)</pattern>
|
||||
<!--<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %magenta(%-5level) %green([%-50.50class]) >>> %cyan(%msg) %n</pattern>-->
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!--<!–输出到文件–>-->
|
||||
<!--<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">-->
|
||||
<!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter">-->
|
||||
<!-- <level>INFO</level>-->
|
||||
<!-- </filter>-->
|
||||
<!-- <file>${log.path}/logback.log</file>-->
|
||||
<!-- <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">-->
|
||||
<!-- <fileNamePattern>${log.path}/logback-%d{yyyy-MM-dd-HH-mm}.log</fileNamePattern>-->
|
||||
<!-- <maxHistory>365</maxHistory>-->
|
||||
<!-- <!– <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">–>-->
|
||||
<!-- <!– <maxFileSize>100kB</maxFileSize>–>-->
|
||||
<!-- <!– </timeBasedFileNamingAndTriggeringPolicy>–>-->
|
||||
<!-- </rollingPolicy>-->
|
||||
<!-- <encoder>-->
|
||||
<!-- <!–格式化输出:%d:表示日期 %thread:表示线程名 %-5level:级别从左显示5个字符宽度 %msg:日志消息 %n:是换行符–>-->
|
||||
<!-- <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>-->
|
||||
<!-- <charset>UTF-8</charset>-->
|
||||
<!-- </encoder>-->
|
||||
<!--</appender>-->
|
||||
|
||||
<!-- 如果appender里没有限定日志级别,那么root可以统一设置,如果没有配置那么控制台和文件不会输出任何日志,这里root的level不做限制-->
|
||||
<root level="INFO">
|
||||
<!-- 允许控制台输出-->
|
||||
<appender-ref ref="console" />
|
||||
<!--<!– 允许文件输出–>-->
|
||||
<!--<appender-ref ref="file" />-->
|
||||
</root>
|
||||
|
||||
<logger name="cn.skcks.docking.gb28181.core.sip.logger" level="INFO" />
|
||||
<logger name="cn.skcks.docking.gb28181" level="DEBUG" />
|
||||
</configuration>
|
@ -2,9 +2,22 @@
|
||||
# 用于缓存打包过程下载的依赖
|
||||
mkdir repository
|
||||
curDir=`pwd`
|
||||
repository=${curDir}/repository
|
||||
|
||||
if ! test -e ${curDir}/gb28181-wvp-proxy-starter/ffmpeg;then
|
||||
xz -d ${curDir}/ffmpeg/ffmpeg-git-amd64-static.tar.xz
|
||||
tar -xvf ${curDir}/ffmpeg/ffmpeg-git-amd64-static.tar -C ${curDir}/ffmpeg/
|
||||
mv ${curDir}/ffmpeg/ffmpeg-git*-static/* ${curDir}/ffmpeg
|
||||
rm -rf ${curDir}/ffmpeg/ffmpeg-git*-static
|
||||
mkdir -p ${curDir}/gb28181-wvp-proxy-starter/ffmpeg
|
||||
cp ${curDir}/ffmpeg/ffmpeg ${curDir}/gb28181-wvp-proxy-starter/ffmpeg
|
||||
cp ${curDir}/ffmpeg/ffprobe ${curDir}/gb28181-wvp-proxy-starter/ffmpeg
|
||||
cp -r ${curDir}/ffmpeg/model ${curDir}/gb28181-wvp-proxy-starter/ffmpeg
|
||||
fi
|
||||
|
||||
docker run --name maven --rm \
|
||||
-v ${curDir}:/usr/src/mymaven \
|
||||
-v ${curDir}/repository:/root/.m2/repository \
|
||||
-v ${repository}:/root/.m2/repository \
|
||||
-v ${curDir}/settings.xml:/usr/share/maven/ref/settings.xml \
|
||||
-v /etc/docker/daemon.json:/etc/docker/daemon.json -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker \
|
||||
-w /usr/src/mymaven \
|
||||
|
8
pom.xml
8
pom.xml
@ -57,7 +57,7 @@
|
||||
<!-- <docker.registry.password>XXX</docker.registry.password>-->
|
||||
<docker.maven.plugin.version>1.4.13</docker.maven.plugin.version>
|
||||
|
||||
<gb28181.docking.version>0.0.1-SNAPSHOT</gb28181.docking.version>
|
||||
<gb28181.docking.version>0.1.0</gb28181.docking.version>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
@ -126,6 +126,12 @@
|
||||
<version>${gb28181.docking.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.skcks.docking.gb28181</groupId>
|
||||
<artifactId>gb28181-service</artifactId>
|
||||
<version>${gb28181.docking.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!--MapStruct-->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
|
97
test/main.py
Normal file
97
test/main.py
Normal file
@ -0,0 +1,97 @@
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import os
|
||||
import os.path as path
|
||||
import sys
|
||||
import datetime
|
||||
import urllib
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
work_path = sys.path[0]
|
||||
# 设备列表
|
||||
devices_file = "devices.txt"
|
||||
devices_file_path = path.join(work_path, devices_file)
|
||||
# 下载目录
|
||||
tmp_dir = "download"
|
||||
tmp_path = path.join(work_path, tmp_dir)
|
||||
|
||||
server = "http://127.0.0.1:18183/video"
|
||||
|
||||
|
||||
# server = "http://httpbin.org/anything/video"
|
||||
|
||||
def check_or_mk_tmp_dir():
|
||||
if not path.exists(tmp_path):
|
||||
os.mkdir(tmp_path)
|
||||
|
||||
|
||||
def check_or_create_devices_file():
|
||||
if not path.exists(devices_file_path):
|
||||
with open(devices_file_path, mode="w", encoding="utf8") as f:
|
||||
f.write("# 设备编码 一行一个\n")
|
||||
|
||||
|
||||
def read_devices_file():
|
||||
check_or_create_devices_file()
|
||||
devices = []
|
||||
with open(devices_file_path, mode="r", encoding="utf8") as f:
|
||||
while True:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.strip()
|
||||
if line.startswith("#") or len(line) == 0:
|
||||
continue
|
||||
else:
|
||||
devices.append(line)
|
||||
|
||||
print(f"读取设备数量: {len(devices)}")
|
||||
return devices
|
||||
|
||||
|
||||
def tasks(device: str, start_time: str, end_time: str):
|
||||
params = {
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"device_id": device,
|
||||
"useDownload": True,
|
||||
}
|
||||
url_params = urllib.parse.urlencode(params)
|
||||
url = urllib.parse.urljoin(server, f"?{url_params}")
|
||||
start = datetime.datetime.now()
|
||||
start_str = start.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
print(f"{start_str} 开始下载: {url}")
|
||||
file_path = f"{tmp_path}/{device}_{start_time}_{end_time}.mp4"
|
||||
urllib.request.urlretrieve(url, file_path)
|
||||
end = datetime.datetime.now()
|
||||
print(
|
||||
f"{device} {start_time}-{end_time}: 下载用时: {(end - start).total_seconds()} 秒")
|
||||
stats = os.stat(file_path)
|
||||
print(f"文件 {file_path} 大小: {stats.st_size}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
check_or_mk_tmp_dir()
|
||||
check_or_create_devices_file()
|
||||
|
||||
print(work_path)
|
||||
# workers = os.cpu_count()
|
||||
workers = 32
|
||||
|
||||
print(f"最大并发数: {workers}")
|
||||
|
||||
with ThreadPoolExecutor(max_workers=workers) as worker:
|
||||
devices = read_devices_file()
|
||||
|
||||
# day = datetime.datetime.today()
|
||||
day = datetime.date(year=2024, month=3, day=11)
|
||||
# 开始时间
|
||||
start = datetime.time(hour=8, minute=11, second=15)
|
||||
# 结束时间
|
||||
end = datetime.time(hour=8, minute=11, second=30)
|
||||
|
||||
start_time = datetime.datetime.combine(day, start).strftime(
|
||||
"%Y%m%d%H%M%S")
|
||||
end_time = datetime.datetime.combine(day, end).strftime("%Y%m%d%H%M%S")
|
||||
for device in devices:
|
||||
worker.submit(tasks, device, start_time, end_time)
|
66
test/merge.py
Normal file
66
test/merge.py
Normal file
@ -0,0 +1,66 @@
|
||||
import re
|
||||
import subprocess
|
||||
import os
|
||||
import os.path as path
|
||||
import sys
|
||||
|
||||
work_path = sys.path[0]
|
||||
|
||||
ffmpeg_path = "ffmpeg"
|
||||
ffprobe_path = "ffprobe"
|
||||
|
||||
record_dir = "record"
|
||||
record_path = path.join(work_path, record_dir)
|
||||
|
||||
if not path.exists(record_path):
|
||||
os.mkdir(record_path)
|
||||
|
||||
merge_file = path.join(work_path, "test.mp4")
|
||||
cmd = f"{ffmpeg_path} -y -loglevel error -f concat -safe 0 -i %s -c copy {merge_file}"
|
||||
|
||||
|
||||
def natural_sort_key(s):
|
||||
"""
|
||||
按文件名中的自然数排序
|
||||
"""
|
||||
# 将字符串按照数字和非数字部分分割,返回分割后的子串列表
|
||||
sub_strings = re.split(r'(\d+)', s)
|
||||
# 如果当前子串由数字组成,则将它转换为整数;否则将其替换成空字符串
|
||||
sub_strings = [int(c) if c.isdigit() else '' for c in sub_strings]
|
||||
# 返回子串列表
|
||||
return sub_strings
|
||||
|
||||
file_list = []
|
||||
for item in os.listdir(record_path):
|
||||
p = path.join(record_path, item)
|
||||
if not path.isfile(p) or (not p.endswith(".mp4") and not p.endswith(".rec")):
|
||||
continue
|
||||
else:
|
||||
file_list.append(p)
|
||||
|
||||
sorted_file_list = sorted(file_list, key=natural_sort_key)
|
||||
|
||||
tmp_merge_file = path.join(work_path, "merge.tmp")
|
||||
with open(tmp_merge_file, mode="w", encoding="utf8") as f:
|
||||
for record in sorted_file_list:
|
||||
f.write(f"file '{record}'\n")
|
||||
|
||||
proc = subprocess.Popen(
|
||||
cmd % tmp_merge_file,
|
||||
stdin=None,
|
||||
stdout=None,
|
||||
shell=True
|
||||
)
|
||||
|
||||
proc.communicate()
|
||||
os.remove(tmp_merge_file)
|
||||
|
||||
meta_cmd = f"{ffprobe_path} -v error -i {merge_file} -print_format json -show_format -show_streams -pretty > {merge_file}.meta.json"
|
||||
|
||||
proc = subprocess.Popen(
|
||||
meta_cmd,
|
||||
stdin=None,
|
||||
stdout=None,
|
||||
shell=True
|
||||
)
|
||||
proc.communicate()
|
84
test/push_zlm.py
Normal file
84
test/push_zlm.py
Normal file
@ -0,0 +1,84 @@
|
||||
import os
|
||||
import os.path as path
|
||||
import select
|
||||
import sys
|
||||
import urllib.parse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import subprocess
|
||||
import platform
|
||||
|
||||
work_path = sys.path[0]
|
||||
|
||||
# 下载目录
|
||||
tmp_dir = "download"
|
||||
tmp_path = path.join(work_path, tmp_dir)
|
||||
|
||||
zlm_server = "192.168.3.12"
|
||||
zlm_rtmp_port = 1936
|
||||
zlm_auth_params = {
|
||||
"sign": "41db35390ddad33f83944f44b8b75ded"
|
||||
}
|
||||
|
||||
ffmpeg_path = "ffmpeg"
|
||||
ffmpeg_read_rate = 1
|
||||
|
||||
ffplay_path = "ffplay"
|
||||
enable_preview = True
|
||||
|
||||
workers = os.cpu_count()
|
||||
|
||||
|
||||
def get_rtmp_url(app: str, stream_id: str):
|
||||
params = urllib.parse.urlencode(zlm_auth_params)
|
||||
return "rtmp://{}:{}/{}/{}?{}".format(zlm_server, zlm_rtmp_port, app,
|
||||
stream_id, params)
|
||||
|
||||
|
||||
def check_or_mk_tmp_dir():
|
||||
if not path.exists(tmp_path):
|
||||
os.mkdir(tmp_path)
|
||||
|
||||
|
||||
def push_stream(file: str):
|
||||
stream_id = path.basename(file)
|
||||
target = get_rtmp_url("ffmpeg", stream_id)
|
||||
print("开始 ffmpeg 推流 {} => {}", file, target)
|
||||
cmd = "{} -loglevel error -stream_loop -1 -readrate {} -i {} -t 60 -c copy -f flv {}".format(
|
||||
ffmpeg_path, ffmpeg_read_rate, file, target)
|
||||
print(cmd)
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=None,
|
||||
stdout=None,
|
||||
shell=True
|
||||
)
|
||||
|
||||
if enable_preview and len(
|
||||
ffplay_path) > 0 and platform.system() == "Windows":
|
||||
subprocess.Popen(
|
||||
"ffplay {} -x 400 -y 300 -autoexit".format(target),
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
shell=True
|
||||
)
|
||||
|
||||
proc.communicate()
|
||||
print("ffmpeg 结束 {} =/= {}", file, target)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
try:
|
||||
workers = int(sys.argv[1])
|
||||
except:
|
||||
print("参数解析错误, 将使用默认线程数 {} ".format(workers))
|
||||
|
||||
print("最大并发数: {}".format(workers))
|
||||
with ThreadPoolExecutor(max_workers=workers) as worker:
|
||||
for item in os.listdir(tmp_path):
|
||||
p = path.join(tmp_path, item)
|
||||
if not path.isfile(p) or not p.endswith(".mp4"):
|
||||
continue
|
||||
else:
|
||||
worker.submit(push_stream, p)
|
44
test/video.py
Normal file
44
test/video.py
Normal file
@ -0,0 +1,44 @@
|
||||
import os.path as path
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
work_path = sys.path[0]
|
||||
# 设备列表
|
||||
devices_file = "devices.txt"
|
||||
devices_file_path = path.join(work_path, devices_file)
|
||||
# 源文件
|
||||
source_video_file = "20240311081115_20240311081130.mp4"
|
||||
|
||||
|
||||
def check_or_create_devices_file():
|
||||
if not path.exists(devices_file_path):
|
||||
with open(devices_file_path, mode="w", encoding="utf8") as f:
|
||||
f.write("# 设备编码 一行一个\n")
|
||||
|
||||
|
||||
def read_devices_file():
|
||||
check_or_create_devices_file()
|
||||
devices = []
|
||||
with open(devices_file_path, mode="r", encoding="utf8") as f:
|
||||
while True:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.strip()
|
||||
if line.startswith("#") or len(line) == 0:
|
||||
continue
|
||||
else:
|
||||
devices.append(line)
|
||||
|
||||
print("读取设备数量: {}".format(len(devices)))
|
||||
return devices
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
check_or_create_devices_file()
|
||||
devices = read_devices_file()
|
||||
|
||||
src = path.join(work_path, source_video_file)
|
||||
for device in devices:
|
||||
dst = path.join(work_path, "{}_{}".format(device, source_video_file))
|
||||
shutil.copyfile(src, dst)
|
Loading…
Reference in New Issue
Block a user