├── doc └── images │ └── qun.jpg ├── src └── main │ ├── resources │ ├── SimHei.ttf │ ├── static │ │ ├── bg.jpg │ │ ├── decoder.wasm │ │ ├── play.html │ │ ├── webrtc.html │ │ └── index.html │ ├── application.yml │ └── conf.ini │ └── java │ └── com │ └── ldf │ └── media │ ├── api │ ├── service │ │ ├── ISnapService.java │ │ ├── ITestVideoService.java │ │ ├── ITranscodeService.java │ │ ├── IVideoStackService.java │ │ ├── IApiService.java │ │ └── impl │ │ │ ├── TestVideoService.java │ │ │ ├── TranscodeService.java │ │ │ ├── VideoStackService.java │ │ │ └── SnapServiceImpl.java │ ├── model │ │ ├── param │ │ │ ├── CloseTestVideoParam.java │ │ │ ├── VideoStackWindowParam.java │ │ │ ├── GetMediaListParam.java │ │ │ ├── CloseStreamsParam.java │ │ │ ├── MediaQueryParam.java │ │ │ ├── StopRecordParam.java │ │ │ ├── RecordStatusParam.java │ │ │ ├── OpenRtpServerParam.java │ │ │ ├── CloseStreamParam.java │ │ │ ├── StreamPushProxyParam.java │ │ │ ├── TranscodeParam.java │ │ │ ├── StartRecordParam.java │ │ │ ├── TestVideoParam.java │ │ │ ├── VideoStackParam.java │ │ │ └── StreamProxyParam.java │ │ └── result │ │ │ ├── Track.java │ │ │ ├── Statistic.java │ │ │ ├── RtpServerResult.java │ │ │ ├── Result.java │ │ │ ├── MediaInfoResult.java │ │ │ └── StreamUrlResult.java │ └── controller │ │ ├── WebRTCController.java │ │ └── ApiController.java │ ├── callback │ ├── IMKSourceHandleCallBack.java │ ├── MKProxyPlayCloseCallBack.java │ ├── MKFlowReportCallBack.java │ ├── MKSourceFindCallBack.java │ ├── MKHttpBeforeAccessCallBack.java │ ├── MKHttpRequestCallBack.java │ ├── MKHttpAccessCallBack.java │ ├── MKNoFoundCallBack.java │ ├── MKRecordTsCallBack.java │ ├── MKRecordMp4CallBack.java │ ├── MKPlayCallBack.java │ ├── MKNoReaderCallBack.java │ ├── MKLogCallBack.java │ ├── MKStreamChangeCallBack.java │ └── MKPublishCallBack.java │ ├── constants │ └── MediaServerConstants.java │ ├── JMediaKitApplication.java │ ├── enums │ └── ResultEnum.java │ ├── pool │ └── MediaServerThreadPool.java │ ├── module │ ├── test │ │ ├── SimHeiFontLoader.java │ │ ├── SystemMonitor.java │ │ ├── YUV420PBarGenerator.java │ │ ├── TestVideo.java │ │ └── YuvImageGenerator.java │ ├── stack │ │ └── VideoStackWindow.java │ └── transcode │ │ └── Transcode.java │ ├── config │ ├── MediaServerConfig.java │ └── SwaggerConfiguration.java │ ├── exception │ └── GlobalExceptionHandler.java │ └── context │ └── MediaServerContext.java ├── webrtc.md ├── .gitignore ├── LICENSE ├── README.md └── pom.xml /doc/images/qun.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidaofu-hub/j_media_server/HEAD/doc/images/qun.jpg -------------------------------------------------------------------------------- /src/main/resources/SimHei.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidaofu-hub/j_media_server/HEAD/src/main/resources/SimHei.ttf -------------------------------------------------------------------------------- /src/main/resources/static/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidaofu-hub/j_media_server/HEAD/src/main/resources/static/bg.jpg -------------------------------------------------------------------------------- /src/main/resources/static/decoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lidaofu-hub/j_media_server/HEAD/src/main/resources/static/decoder.wasm -------------------------------------------------------------------------------- /webrtc.md: -------------------------------------------------------------------------------- 1 | > webrtc支持推流和拉流,推理需要开启https,如果只是拉流http协议就可以支持。 2 | webrtc测试步骤如下: 3 | 1. 启动服务 4 | 2. 通过rtsp代理拉流添加一个媒体输入源,确保`enable_rtsp=1 app=live stream=test` 5 | 3. 在浏览器请求 `http://127.0.0.1:8899/index.html` 6 | 4. 直接点击**开始**,既可以看到媒体画面 7 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/service/ISnapService.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.service; 2 | 3 | import javax.servlet.http.HttpServletResponse; 4 | 5 | public interface ISnapService { 6 | 7 | /** 8 | * 获取截图 9 | * @param url 10 | * @param response 11 | */ 12 | void getSnap(String url, HttpServletResponse response); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/IMKSourceHandleCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.structure.MK_MEDIA_SOURCE; 4 | 5 | /** 6 | * 媒体资源回调接口 7 | * 8 | * @author lidaofu 9 | * @since 2023/11/30 10 | **/ 11 | public interface IMKSourceHandleCallBack { 12 | 13 | void invoke(MK_MEDIA_SOURCE ctx); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/service/ITestVideoService.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.service; 2 | 3 | import com.ldf.media.api.model.param.TestVideoParam; 4 | import com.ldf.media.api.model.result.StreamUrlResult; 5 | 6 | public interface ITestVideoService { 7 | StreamUrlResult createTestVideo(TestVideoParam param); 8 | 9 | Boolean stopTestVideo(String app,String stream); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/constants/MediaServerConstants.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.constants; 2 | 3 | /** 4 | * 常量 5 | * 6 | * @author lidaofu 7 | * @since 2023/11/30 8 | **/ 9 | public interface MediaServerConstants { 10 | 11 | /** 12 | * 默认vhost 13 | */ 14 | String DEFAULT_VHOST="__defaultVhost__"; 15 | 16 | /** 17 | * 默认app 18 | */ 19 | String DEFAULT_APP="live"; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/service/ITranscodeService.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.service; 2 | 3 | import com.ldf.media.api.model.param.TranscodeParam; 4 | 5 | public interface ITranscodeService { 6 | 7 | /** 8 | * 转码 9 | * @param param 10 | * @return 11 | */ 12 | void transcode(TranscodeParam param); 13 | 14 | /** 15 | * 停止转码 16 | * @param stream 17 | */ 18 | void stopTranscode(String stream); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/JMediaKitApplication.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * 流媒体服务器 8 | * 9 | * @author lidaofu 10 | * @since 2023/11/29 11 | **/ 12 | @SpringBootApplication 13 | public class JMediaKitApplication { 14 | public static void main(String[] args) { 15 | SpringApplication.run(JMediaKitApplication.class, args); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea 8 | *.iws 9 | *.iml 10 | *.ipr 11 | 12 | ### Eclipse ### 13 | .apt_generated 14 | .classpath 15 | .factorypath 16 | .project 17 | .settings 18 | .springBeans 19 | .sts4-cache 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### Mac OS ### 35 | .DS_Store -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/service/IVideoStackService.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.service; 2 | 3 | import com.ldf.media.api.model.param.VideoStackParam; 4 | 5 | public interface IVideoStackService { 6 | 7 | /** 8 | * 开启拼接屏任务 9 | * @param param 10 | * @return 11 | */ 12 | void startStack(VideoStackParam param); 13 | 14 | /** 15 | * 重设拼接屏任务 16 | * @param param 17 | * @return 18 | */ 19 | void resetStack(VideoStackParam param); 20 | 21 | /** 22 | * 停止拼接屏任务 23 | * @param id 24 | * @return 25 | */ 26 | void stopStack(String id); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKProxyPlayCloseCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.callback.IMKProxyPlayerCallBack; 4 | import com.sun.jna.Pointer; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * MediaSource.close()回调事件 9 | */ 10 | @Component 11 | public class MKProxyPlayCloseCallBack implements IMKProxyPlayerCallBack { 12 | /** 13 | * MediaSource.close()回调事件 14 | * 在选择关闭一个关联的MediaSource时,将会最终触发到该回调 15 | * 你应该通过该事件调用mk_proxy_player_release函数并且释放其他资源 16 | * 如果你不调用mk_proxy_player_release函数,那么MediaSource.close()操作将无效 17 | * 18 | * @param pUser 用户数据指针,通过mk_proxy_player_set_on_close函数设置 19 | */ 20 | public void invoke(Pointer pUser, int err, String what, int sys_err) { 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/CloseTestVideoParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import java.io.Serializable; 9 | 10 | @Data 11 | @ApiModel(value = "CloseTestVideoParam对象", description = "关闭测试流参数") 12 | public class CloseTestVideoParam implements Serializable { 13 | 14 | private static final long serialVersionUID = 1; 15 | 16 | 17 | @NotBlank(message = "app不为空") 18 | @ApiModelProperty(value = "app", required = true) 19 | private String app; 20 | 21 | @NotBlank(message = "流id不为空") 22 | @ApiModelProperty(value = "流id", required = true) 23 | private String stream; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKFlowReportCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.callback.IMKFlowReportCallBack; 4 | import com.aizuda.zlm4j.structure.MK_MEDIA_INFO; 5 | import com.aizuda.zlm4j.structure.MK_SOCK_INFO; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * 停止rtsp/rtmp/http-flv会话后流量汇报事件广播 10 | */ 11 | @Component 12 | public class MKFlowReportCallBack implements IMKFlowReportCallBack { 13 | /** 14 | * 停止rtsp/rtmp/http-flv会话后流量汇报事件广播 15 | * 16 | * @param url_info 播放url相关信息 17 | * @param total_bytes 耗费上下行总流量,单位字节数 18 | * @param total_seconds 本次tcp会话时长,单位秒 19 | * @param is_player 客户端是否为播放器 20 | */ 21 | public void invoke(MK_MEDIA_INFO url_info, long total_bytes, long total_seconds, int is_player, MK_SOCK_INFO sender) { 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/VideoStackWindowParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | import java.util.List; 9 | 10 | @Data 11 | @ApiModel(value = "VideoStackWindowParam对象", description = "拼接屏幕地址参数") 12 | public class VideoStackWindowParam implements Serializable { 13 | 14 | private static final long serialVersionUID = 1; 15 | 16 | @ApiModelProperty(value = "拼接视频地址") 17 | private String videoUrl; 18 | 19 | @ApiModelProperty(value = "拼接图片地址,和上面二选一") 20 | private String imgUrl; 21 | 22 | @ApiModelProperty(value = "默认填充颜色") 23 | private String fillColor = "BFBFBF"; 24 | 25 | @ApiModelProperty(value = "所占的格子") 26 | private List span; 27 | } -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/result/Track.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.result; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | @Data 8 | public class Track implements Serializable { 9 | private static final long serialVersionUID = 1; 10 | 11 | private Integer is_video; 12 | private Integer codec_id; 13 | private String codec_id_name; 14 | private Integer codec_type; 15 | private Integer fps; 16 | private Long frames; 17 | private Long duration; 18 | private Integer bit_rate; 19 | private Integer gop_interval_ms; 20 | private Integer gop_size; 21 | private Integer height; 22 | private Long key_frames; 23 | private Float loss; 24 | private Boolean ready; 25 | private Integer width; 26 | private Integer sample_rate; 27 | private Integer audio_channel; 28 | private Integer audio_sample_bit; 29 | 30 | } -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/result/Statistic.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.result; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | /** 8 | * 内存占用信息 9 | * 10 | * @author lidaofu 11 | * @since 2024/5/20 12 | **/ 13 | @Data 14 | public class Statistic implements Serializable { 15 | private static final long serialVersionUID = 1; 16 | 17 | private Long mediaSource; 18 | private Long multiMediaSourceMuxer; 19 | private Long tcpServer; 20 | private Long tcpSession; 21 | private Long udpServer; 22 | private Long udpSession; 23 | private Long tcpClient; 24 | private Long socket; 25 | private Long frameImp; 26 | private Long frame; 27 | private Long buffer; 28 | private Long bufferRaw; 29 | private Long bufferLikeString; 30 | private Long bufferList; 31 | private Long rtpPacket; 32 | private Long rtmpPacket; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKSourceFindCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.callback.IMKSourceFindCallBack; 4 | import com.aizuda.zlm4j.structure.MK_MEDIA_SOURCE; 5 | import com.sun.jna.CallbackThreadInitializer; 6 | import com.sun.jna.Native; 7 | import com.sun.jna.Pointer; 8 | 9 | /** 10 | * 寻找流回调 11 | */ 12 | public class MKSourceFindCallBack implements IMKSourceFindCallBack { 13 | private IMKSourceHandleCallBack imkSourceHandleCallBack; 14 | 15 | public MKSourceFindCallBack(IMKSourceHandleCallBack imkSourceHandleCallBack) { 16 | Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "MediaSourceFindThread")); 17 | this.imkSourceHandleCallBack=imkSourceHandleCallBack; 18 | } 19 | 20 | public void invoke(Pointer user_data, MK_MEDIA_SOURCE ctx) { 21 | imkSourceHandleCallBack.invoke(ctx); 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKHttpBeforeAccessCallBack.java: -------------------------------------------------------------------------------- 1 | 2 | package com.ldf.media.callback; 3 | 4 | import com.aizuda.zlm4j.callback.IMKHttpBeforeAccessCallBack; 5 | import com.aizuda.zlm4j.structure.MK_PARSER; 6 | import com.aizuda.zlm4j.structure.MK_SOCK_INFO; 7 | import com.sun.jna.Callback; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * 在http文件服务器中,收到http访问文件或目录前的广播,通过该事件可以控制http url到文件路径的映射 12 | */ 13 | @Component 14 | public class MKHttpBeforeAccessCallBack implements IMKHttpBeforeAccessCallBack { 15 | /** 16 | * 在http文件服务器中,收到http访问文件或目录前的广播,通过该事件可以控制http url到文件路径的映射 17 | * 在该事件中通过自行覆盖path参数,可以做到譬如根据虚拟主机或者app选择不同http根目录的目的 18 | * 19 | * @param parser http请求内容对象 20 | * @param path 文件绝对路径,覆盖之可以重定向到其他文件 21 | * @param sender http客户端相关信息 22 | */ 23 | public void invoke(MK_PARSER parser, String path, MK_SOCK_INFO sender){ 24 | 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8899 3 | media: ##目前只增加部分重要配置,更多配置请自行配置 4 | media_ip: 127.0.0.1 #播放流ip 5 | thread_num: 8 6 | rtmp_port: 7935 7 | rtsp_port: 7554 8 | http_port: 7080 9 | rtc_port: 8000 10 | rtc_host: 127.0.0.1 11 | auto_close: 0 12 | streamNoneReaderDelayMS: 30000 13 | maxStreamWaitMS: 1500 14 | enable_ts: 1 15 | enable_hls: 1 16 | enable_fmp4: 0 17 | enable_rtsp: 1 18 | enable_rtmp: 1 19 | enable_mp4: 0 20 | enable_hls_fmp4: 1 21 | enable_audio: 1 22 | mp4_as_player: 1 23 | mp4_max_second: 3600 24 | mp4_save_path: D:/media 25 | hls_save_path: D:/media 26 | rootPath: D:/media 27 | hls_demand: 0 28 | broadcastRecordTs: 1 29 | segDur: 3 30 | segNum: 3 31 | rtsp_demand: 0 32 | rtmp_demand: 0 33 | ts_demand: 1 34 | fmp4_demand: 1 35 | log_level: 2 36 | log_mask: 1 #1console 2file 4fun 37 | log_file_days: 1 38 | log_path: E:/media 39 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/result/RtpServerResult.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.result; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * rtp服务 13 | * 14 | * @author lidaofu 15 | * @since 2023/3/30 16 | **/ 17 | @Data 18 | @ApiModel(value = "RtpServerResult对象", description = "rtp服务") 19 | public class RtpServerResult implements Serializable { 20 | 21 | private static final long serialVersionUID = 1; 22 | 23 | 24 | @NotNull(message = "接收端口,0则为随机端口") 25 | @ApiModelProperty(value = "接收端口",required = true) 26 | private Integer port; 27 | 28 | @NotBlank(message = "流id不为空") 29 | @ApiModelProperty(value = "流id",required = true) 30 | private String stream; 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/GetMediaListParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | import lombok.experimental.SuperBuilder; 7 | 8 | import javax.validation.constraints.NotBlank; 9 | import javax.validation.constraints.NotNull; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 获取流列表 14 | * 15 | * @author lidaofu 16 | * @since 2023/3/30 17 | **/ 18 | @Data 19 | @ApiModel(value = "GetMediaListParam对象", description = "获取流列表") 20 | public class GetMediaListParam implements Serializable { 21 | 22 | private static final long serialVersionUID = 1; 23 | 24 | 25 | @ApiModelProperty(value = "app") 26 | private String app; 27 | 28 | @ApiModelProperty(value = "流id") 29 | private String stream; 30 | 31 | @ApiModelProperty(value = "流的协议") 32 | private String schema; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/enums/ResultEnum.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.enums; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 返回枚举 8 | * 9 | * @author lidaofu 10 | * @since 2023/4/4 11 | **/ 12 | @Getter 13 | @AllArgsConstructor 14 | public enum ResultEnum { 15 | /** 16 | * 请求成功 17 | */ 18 | SUCCESS(0, "请求成功"), 19 | 20 | /** 21 | * 请求失败 22 | */ 23 | ERROR(-1, "请求成功"), 24 | /** 25 | * 未登录 26 | */ 27 | NOT_LOGIN(-100, "未提供有效凭据"), 28 | 29 | /** 30 | * 无权限 31 | */ 32 | NOT_POWER(-100, "无权限"), 33 | 34 | /** 35 | * 提交限制 36 | */ 37 | INVALID_ARGS (-300, "请求过快"), 38 | 39 | /** 40 | * 请求失败 41 | */ 42 | EXCEPTION(-400, "请求或操作失败"), 43 | 44 | 45 | ; 46 | 47 | /** 48 | * 代码 49 | */ 50 | private final Integer code; 51 | 52 | /** 53 | * 信息 54 | */ 55 | private final String msg; 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKHttpRequestCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.callback.IMKHttpRequestCallBack; 4 | import com.aizuda.zlm4j.structure.MK_HTTP_RESPONSE_INVOKER; 5 | import com.aizuda.zlm4j.structure.MK_PARSER; 6 | import com.aizuda.zlm4j.structure.MK_SOCK_INFO; 7 | import com.sun.jna.ptr.IntByReference; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * 收到http api请求广播(包括GET/POST) 12 | */ 13 | @Component 14 | public class MKHttpRequestCallBack implements IMKHttpRequestCallBack { 15 | /** 16 | * 收到http api请求广播(包括GET/POST) 17 | * 18 | * @param parser http请求内容对象 19 | * @param invoker 执行该invoker返回http回复 20 | * @param consumed 置1则说明我们要处理该事件 21 | * @param sender http客户端相关信息 22 | */ 23 | public void invoke(MK_PARSER parser, MK_HTTP_RESPONSE_INVOKER invoker, IntByReference consumed, MK_SOCK_INFO sender) { 24 | consumed.setValue(0); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKHttpAccessCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.callback.IMKHttpAccessCallBack; 4 | import com.aizuda.zlm4j.structure.MK_HTTP_ACCESS_PATH_INVOKER; 5 | import com.aizuda.zlm4j.structure.MK_PARSER; 6 | import com.aizuda.zlm4j.structure.MK_SOCK_INFO; 7 | import com.sun.jna.Callback; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * 在http文件服务器中,收到http访问文件或目录的广播,通过该事件控制访问http目录的权限 12 | */ 13 | @Component 14 | public class MKHttpAccessCallBack implements IMKHttpAccessCallBack { 15 | /** 16 | * 在http文件服务器中,收到http访问文件或目录的广播,通过该事件控制访问http目录的权限 17 | * @param parser http请求内容对象 18 | * @param path 文件绝对路径 19 | * @param is_dir path是否为文件夹 20 | * @param invoker 执行invoker返回本次访问文件的结果 21 | * @param sender http客户端相关信息 22 | */ 23 | public void invoke(MK_PARSER parser, String path, int is_dir, MK_HTTP_ACCESS_PATH_INVOKER invoker, MK_SOCK_INFO sender){ 24 | 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/CloseStreamsParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 关闭流请求参数 13 | * 14 | * @author lidaofu 15 | * @since 2023/3/30 16 | **/ 17 | @Data 18 | @ApiModel(value = "CloseStreamsParam对象", description = "关闭流请求参数") 19 | public class CloseStreamsParam implements Serializable { 20 | 21 | private static final long serialVersionUID = 1; 22 | 23 | @ApiModelProperty(value = "app") 24 | private String app; 25 | 26 | @ApiModelProperty(value = "流id") 27 | private String stream; 28 | 29 | @ApiModelProperty(value = "是否强制关闭") 30 | private Integer force=1; 31 | 32 | @ApiModelProperty(value = "流的协议") 33 | private String schema; 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/MediaQueryParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 流查询参数 13 | * 14 | * @author lidaofu 15 | * @since 2023/3/30 16 | **/ 17 | @Data 18 | @ApiModel(value = "MediaQueryParam对象", description = "流查询参数") 19 | public class MediaQueryParam implements Serializable { 20 | 21 | private static final long serialVersionUID = 1; 22 | 23 | 24 | @NotBlank(message = "app不为空") 25 | @ApiModelProperty(value = "app",required = true) 26 | private String app; 27 | 28 | @NotBlank(message = "流id不为空") 29 | @ApiModelProperty(value = "流id",required = true) 30 | private String stream; 31 | 32 | @NotBlank(message = "流的协议不为空") 33 | @ApiModelProperty(value = "流的协议",required = true) 34 | private String schema; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/StopRecordParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 停止录像 13 | * 14 | * @author lidaofu 15 | * @since 2023/3/30 16 | **/ 17 | @Data 18 | @ApiModel(value = "StopRecordParam对象", description = "停止录像") 19 | public class StopRecordParam implements Serializable { 20 | 21 | private static final long serialVersionUID = 1; 22 | 23 | @NotBlank(message = "app不为空") 24 | @ApiModelProperty(value = "app",required = true) 25 | private String app; 26 | 27 | @NotBlank(message = "流id不为空") 28 | @ApiModelProperty(value = "流id",required = true) 29 | private String stream; 30 | 31 | @NotNull(message = "录像类型不为空") 32 | @ApiModelProperty(value = "0为hls,1为mp4,2:hls-fmp4,3:http-fmp4,4:http-ts 当0时需要开启配置分片持久化",required = true) 33 | private Integer type; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/RecordStatusParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 录像状态 13 | * 14 | * @author lidaofu 15 | * @since 2023/3/30 16 | **/ 17 | @Data 18 | @ApiModel(value = "RecordStatusParam对象", description = "录像状态") 19 | public class RecordStatusParam implements Serializable { 20 | 21 | private static final long serialVersionUID = 1; 22 | 23 | @NotBlank(message = "app不为空") 24 | @ApiModelProperty(value = "app",required = true) 25 | private String app; 26 | 27 | @NotBlank(message = "流id不为空") 28 | @ApiModelProperty(value = "流id",required = true) 29 | private String stream; 30 | 31 | @NotNull(message = "录像类型不为空") 32 | @ApiModelProperty(value = "0为hls,1为mp4,2:hls-fmp4,3:http-fmp4,4:http-ts 当0时需要开启配置分片持久化",required = true) 33 | private Integer type; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/OpenRtpServerParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | /** 12 | * 开启rtp服务 13 | * 14 | * @author lidaofu 15 | * @since 2023/3/30 16 | **/ 17 | @Data 18 | @ApiModel(value = "OpenRtpServerParam对象", description = "开启rtp服务参数") 19 | public class OpenRtpServerParam implements Serializable { 20 | 21 | private static final long serialVersionUID = 1; 22 | 23 | 24 | @NotNull(message = "接收端口,0则为随机端口") 25 | @ApiModelProperty(value = "接收端口",required = true) 26 | private Integer port; 27 | 28 | @NotNull(message = "0 udp 模式,1 tcp 被动模式, 2 tcp 主动模式。 (兼容enable_tcp 为0/1)") 29 | @ApiModelProperty(value = "tcp_mode",required = true) 30 | private Integer tcp_mode; 31 | 32 | @NotBlank(message = "流id不为空") 33 | @ApiModelProperty(value = "流id",required = true) 34 | private String stream; 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 胖虎 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKNoFoundCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.callback.IMKNoFoundCallBack; 4 | import com.aizuda.zlm4j.structure.MK_MEDIA_INFO; 5 | import com.aizuda.zlm4j.structure.MK_SOCK_INFO; 6 | import com.sun.jna.Callback; 7 | import com.sun.jna.CallbackThreadInitializer; 8 | import com.sun.jna.Native; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * 未找到流后会广播该事件,请在监听该事件后去拉流或其他方式产生流,这样就能按需拉流了 13 | * 14 | * @author lidaofu 15 | * @since 2023/11/23 16 | **/ 17 | @Component 18 | public class MKNoFoundCallBack implements IMKNoFoundCallBack { 19 | 20 | public MKNoFoundCallBack() { 21 | //回调使用同一个线程 22 | Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "MediaNoFoundThread")); 23 | } 24 | /** 25 | * 未找到流后会广播该事件,请在监听该事件后去拉流或其他方式产生流,这样就能按需拉流了 26 | * 27 | * @param url_info 播放url相关信息 28 | * @param sender 播放客户端相关信息 29 | * @return 1 直接关闭 30 | * 0 等待流注册 31 | */ 32 | public int invoke(MK_MEDIA_INFO url_info, MK_SOCK_INFO sender){ 33 | //这里可以实现按需拉流,这里面新起个线程去操作拉起流 34 | return 1; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKRecordTsCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.callback.IMKRecordTsCallBack; 4 | import com.aizuda.zlm4j.structure.MK_RECORD_INFO; 5 | import com.ldf.media.context.MediaServerContext; 6 | import com.sun.jna.CallbackThreadInitializer; 7 | import com.sun.jna.Native; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * 录制ts分片文件成功后广播 12 | */ 13 | @Component 14 | public class MKRecordTsCallBack implements IMKRecordTsCallBack { 15 | public MKRecordTsCallBack() { 16 | //回调使用同一个线程 17 | Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "RecordTsThread")); 18 | } 19 | 20 | /** 21 | * 录制ts分片文件成功后广播 22 | */ 23 | public void invoke(MK_RECORD_INFO info) { 24 | //录制ts成功回调 通过mk_ts_info_get_*获取各种信息 25 | String path = MediaServerContext.ZLM_API.mk_record_info_get_file_path(info); 26 | String folder = MediaServerContext.ZLM_API.mk_record_info_get_folder(info); 27 | float timeLen = MediaServerContext.ZLM_API.mk_record_info_get_time_len(info); 28 | long startTime = MediaServerContext.ZLM_API.mk_record_info_get_start_time(info); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKRecordMp4CallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.callback.IMKRecordMp4CallBack; 4 | import com.aizuda.zlm4j.structure.MK_RECORD_INFO; 5 | import com.ldf.media.context.MediaServerContext; 6 | import com.sun.jna.CallbackThreadInitializer; 7 | import com.sun.jna.Native; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * 录制mp4分片文件成功后广播 12 | */ 13 | @Component 14 | public class MKRecordMp4CallBack implements IMKRecordMp4CallBack { 15 | public MKRecordMp4CallBack() { 16 | //回调使用同一个线程 17 | Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "RecordMp4Thread")); 18 | } 19 | 20 | /** 21 | * 录制mp4分片文件成功后广播 22 | */ 23 | public void invoke(MK_RECORD_INFO info) { 24 | //录制mp4成功回调 通过mk_record_info_get_*获取各种信息 25 | String path = MediaServerContext.ZLM_API.mk_record_info_get_file_path(info); 26 | String folder = MediaServerContext.ZLM_API.mk_record_info_get_folder(info); 27 | float timeLen = MediaServerContext.ZLM_API.mk_record_info_get_time_len(info); 28 | long startTime = MediaServerContext.ZLM_API.mk_record_info_get_start_time(info); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/CloseStreamParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | import lombok.experimental.SuperBuilder; 7 | 8 | import javax.validation.constraints.NotBlank; 9 | import javax.validation.constraints.NotNull; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 关闭流请求参数 14 | * 15 | * @author lidaofu 16 | * @since 2023/3/30 17 | **/ 18 | @Data 19 | @ApiModel(value = "CloseStreamParam对象", description = "关闭流请求参数") 20 | public class CloseStreamParam implements Serializable { 21 | 22 | private static final long serialVersionUID = 1; 23 | 24 | @NotBlank(message = "app不为空") 25 | @ApiModelProperty(value = "app",required = true) 26 | private String app; 27 | 28 | @NotBlank(message = "流id不为空") 29 | @ApiModelProperty(value = "流id",required = true) 30 | private String stream; 31 | 32 | @NotNull(message = "是否强制关闭不为空") 33 | @ApiModelProperty(value = "是否强制关闭",required = true) 34 | private Integer force; 35 | 36 | @NotBlank(message = "流的协议不为空") 37 | @ApiModelProperty(value = "流的协议",required = true) 38 | private String schema; 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/StreamPushProxyParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import java.math.BigDecimal; 9 | 10 | /** 11 | * 推流代理参数 12 | * 13 | * @author lidaofu 14 | * @since 2023/11/29 15 | **/ 16 | @Data 17 | @ApiModel(value = "StreamPushProxyParam对象", description = "推流代理参数") 18 | public class StreamPushProxyParam { 19 | 20 | 21 | @NotBlank(message = "app不为空") 22 | @ApiModelProperty(value = "app",required = true) 23 | private String app; 24 | 25 | @NotBlank(message = "流id不为空") 26 | @ApiModelProperty(value = "流id",required = true) 27 | private String stream; 28 | 29 | @NotBlank(message = "流的协议不为空") 30 | @ApiModelProperty(value = "流的协议",required = true) 31 | private String schema; 32 | 33 | @NotBlank(message = "推流代理流地址不为空") 34 | @ApiModelProperty(value = "推流代理流地址",required = true) 35 | private String url; 36 | 37 | @ApiModelProperty(value = "rtsp推流时,推流方式,0:tcp,1:udp,2:组播") 38 | private Integer rtpType=0; 39 | 40 | @ApiModelProperty(value = "推流代理超时时间,单位秒") 41 | private Integer timeoutSec; 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/TranscodeParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | @Data 11 | @ApiModel(value = "TranscodeParam对象", description = "转码参数") 12 | public class TranscodeParam implements Serializable { 13 | 14 | private static final long serialVersionUID = 1; 15 | 16 | @NotBlank(message = "url不为空") 17 | @ApiModelProperty(value = "url(rtmp协议只支持H264)",required = true) 18 | private String url; 19 | 20 | @NotBlank(message = "转码后推的app不为空") 21 | @ApiModelProperty(value = "转码后推的app",required = true) 22 | private String app; 23 | 24 | @ApiModelProperty(value = "是否开启音频",required = true) 25 | private Boolean enableAudio=true; 26 | 27 | @NotBlank(message = "转码后推的stream不为空") 28 | @ApiModelProperty(value = "转码后推的stream",required = true) 29 | private String stream; 30 | 31 | @ApiModelProperty(value = "修改分辨率宽",notes = "不需要则置为空") 32 | private Integer scaleWidth=0; 33 | 34 | @ApiModelProperty(value = "修改分辨率高",notes = "不需要则置为空") 35 | private Integer scaleHeight=0; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/StartRecordParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import io.swagger.models.auth.In; 6 | import lombok.Data; 7 | 8 | import javax.validation.constraints.NotBlank; 9 | import javax.validation.constraints.NotNull; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 开始录像 14 | * 15 | * @author lidaofu 16 | * @since 2023/3/30 17 | **/ 18 | @Data 19 | @ApiModel(value = "StartRecordParam对象", description = "开始录像参数") 20 | public class StartRecordParam implements Serializable { 21 | 22 | private static final long serialVersionUID = 1; 23 | 24 | @NotBlank(message = "app不为空") 25 | @ApiModelProperty(value = "app",required = true) 26 | private String app; 27 | 28 | @NotBlank(message = "流id不为空") 29 | @ApiModelProperty(value = "流id",required = true) 30 | private String stream; 31 | 32 | @NotNull(message = "录像类型不为空") 33 | @ApiModelProperty(value = "0为hls,1为mp4,2:hls-fmp4,3:http-fmp4,4:http-ts 当0时需要开启配置分片持久化",required = true) 34 | private Integer type; 35 | 36 | @ApiModelProperty(value = "录像保存目录") 37 | private String customized_path; 38 | 39 | @ApiModelProperty(value = "mp4录像切片时间大小,单位秒,置0则采用配置项") 40 | private Long max_second=1L; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/pool/MediaServerThreadPool.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.pool; 2 | 3 | 4 | import java.util.concurrent.LinkedBlockingDeque; 5 | import java.util.concurrent.ThreadFactory; 6 | import java.util.concurrent.ThreadPoolExecutor; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | /** 11 | * MediaServer模块线程池配置 12 | * 13 | * @author lidaofu 14 | * @since 2023/4/11 15 | **/ 16 | public class MediaServerThreadPool { 17 | 18 | /** 19 | * MediaServer模块线程池 20 | */ 21 | private final static ThreadPoolExecutor SYSTEM_THREAD_POOL = new ThreadPoolExecutor( 22 | 32, 64, 60, TimeUnit.SECONDS 23 | , new LinkedBlockingDeque<>(256), new ThreadFactory() { 24 | private final AtomicInteger threadNumber = new AtomicInteger(1); 25 | 26 | @Override 27 | public Thread newThread(Runnable r) { 28 | return new Thread(r, "MediaServerThread-" + threadNumber.getAndIncrement()); 29 | } 30 | }); 31 | 32 | /** 33 | * 执行 34 | * 35 | * @param runnable 36 | */ 37 | public static void execute(Runnable runnable) { 38 | SYSTEM_THREAD_POOL.execute(runnable); 39 | } 40 | 41 | /** 42 | * 停止 43 | */ 44 | public static void shutdown() { 45 | SYSTEM_THREAD_POOL.shutdown(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/module/test/SimHeiFontLoader.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.module.test; 2 | 3 | import java.awt.*; 4 | import java.io.InputStream; 5 | 6 | public class SimHeiFontLoader { 7 | 8 | /** 9 | * 从src/main/resources加载SimHei.ttf字体 10 | * 11 | * @param fontSize 字体大小 12 | * @return 加载成功的Font对象,失败返回默认字体 13 | */ 14 | public static Font loadSimHeiFont(float fontSize) { 15 | // 字体文件在resources中的路径 16 | String fontPath = "/SimHei.ttf"; // 若放在子目录如fonts下,则改为"/fonts/SimHei.ttf" 17 | 18 | try (InputStream is = SimHeiFontLoader.class.getResourceAsStream(fontPath)) { 19 | if (is == null) { 20 | throw new RuntimeException("未找到字体文件: " + fontPath); 21 | } 22 | 23 | // 从输入流创建基础字体 24 | Font baseFont = Font.createFont(Font.TRUETYPE_FONT, is); 25 | 26 | // 注册字体到系统(可选,注册后可通过字体名全局使用) 27 | GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); 28 | ge.registerFont(baseFont); 29 | 30 | // 派生指定大小的字体并返回 31 | return baseFont.deriveFont(fontSize); 32 | 33 | } catch (Exception e) { 34 | System.err.println("加载SimHei字体失败,将使用默认字体: " + e.getMessage()); 35 | // 返回默认字体作为降级方案 36 | return new Font("SansSerif", Font.PLAIN, (int) fontSize); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/result/Result.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.result; 2 | 3 | import com.ldf.media.enums.ResultEnum; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 返回类 11 | * 12 | * @author lidaofu 13 | * @since 2023/4/10 14 | **/ 15 | @Data 16 | @AllArgsConstructor 17 | public class Result implements Serializable { 18 | private static final long serialVersionUID = 1; 19 | /** 20 | * 状态码 21 | */ 22 | private Integer code; 23 | 24 | /** 25 | * 信息 26 | */ 27 | private String msg; 28 | 29 | /** 30 | * 数据 31 | */ 32 | private T data; 33 | 34 | public Result() { 35 | this.code = ResultEnum.SUCCESS.getCode(); 36 | this.msg = ResultEnum.SUCCESS.getMsg(); 37 | } 38 | 39 | public Result(T t) { 40 | this.code = ResultEnum.SUCCESS.getCode(); 41 | this.msg = ResultEnum.SUCCESS.getMsg(); 42 | this.data = t; 43 | } 44 | 45 | public Result(ResultEnum resultEnum) { 46 | this.code = resultEnum.getCode(); 47 | this.msg = resultEnum.getMsg(); 48 | } 49 | 50 | public Result(ResultEnum resultEnum, T t) { 51 | this.code = resultEnum.getCode(); 52 | this.msg = resultEnum.getMsg(); 53 | this.data = t; 54 | } 55 | 56 | public Result(Integer code, String msg) { 57 | this.code = code; 58 | this.msg = msg; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKPlayCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.callback.IMKPlayCallBack; 4 | import com.aizuda.zlm4j.structure.MK_AUTH_INVOKER; 5 | import com.aizuda.zlm4j.structure.MK_MEDIA_INFO; 6 | import com.aizuda.zlm4j.structure.MK_SOCK_INFO; 7 | import com.ldf.media.context.MediaServerContext; 8 | import com.sun.jna.CallbackThreadInitializer; 9 | import com.sun.jna.Native; 10 | import org.springframework.stereotype.Component; 11 | 12 | /** 13 | * 播放rtsp/rtmp/http-flv/hls事件广播,通过该事件控制播放鉴权 14 | * 15 | * @author lidaofu 16 | * @since 2023/11/23 17 | **/ 18 | @Component 19 | public class MKPlayCallBack implements IMKPlayCallBack { 20 | public MKPlayCallBack() { 21 | //回调使用同一个线程 22 | Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "MediaPlayThread")); 23 | } 24 | 25 | /** 26 | * 播放rtsp/rtmp/http-flv/hls事件广播,通过该事件控制播放鉴权 27 | * 28 | * @param url_info 播放url相关信息 29 | * @param invoker 执行invoker返回鉴权结果 30 | * @param sender 播放客户端相关信息 31 | * @see mk_auth_invoker_do 32 | */ 33 | public void invoke(MK_MEDIA_INFO url_info, MK_AUTH_INVOKER invoker, MK_SOCK_INFO sender) { 34 | //这里拿到访问路径后(例如http://xxxx/xxx/xxx.live.flv?token=xxxx其中?后面就是拿到的参数)的参数 35 | // err_msg返回 空字符串表示鉴权成功 否则鉴权失败提示 36 | //String param = ZLM_API.mk_media_info_get_params(url_info); 37 | MediaServerContext.ZLM_API.mk_auth_invoker_do(invoker, ""); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKNoReaderCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.ldf.media.context.MediaServerContext; 4 | import com.ldf.media.pool.MediaServerThreadPool; 5 | import com.aizuda.zlm4j.callback.IMKNoReaderCallBack; 6 | import com.aizuda.zlm4j.structure.MK_MEDIA_SOURCE; 7 | import com.sun.jna.CallbackThreadInitializer; 8 | import com.sun.jna.Native; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * 无人观看回调 13 | * 14 | * @author lidaofu 15 | * @since 2023/11/23 16 | **/ 17 | @Component 18 | public class MKNoReaderCallBack implements IMKNoReaderCallBack { 19 | public MKNoReaderCallBack() { 20 | //回调使用同一个线程 21 | Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "MediaNoReaderThread")); 22 | } 23 | 24 | /** 25 | * 某个流无人消费时触发,目的为了实现无人观看时主动断开拉流等业务逻辑 26 | * 27 | * @param sender 该MediaSource对象 28 | */ 29 | public void invoke(MK_MEDIA_SOURCE sender) { 30 | String appValue = MediaServerContext.ZLM_API.mk_media_source_get_app(sender); 31 | String streamValue = MediaServerContext.ZLM_API.mk_media_source_get_stream(sender); 32 | String schemaValue = MediaServerContext.ZLM_API.mk_media_source_get_schema(sender); 33 | //无人观看时候可以调用下面的实现关流 不调用就代表不关流 需要配置protocol.auto_close 为 0 这里才会有回调 34 | //关流操作 只关闭一种协议流 35 | //if (schemaValue.equals("rtmp")){ 36 | //MediaServerContext.ZLM_API.mk_media_source_close(sender, 0) 37 | //} 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKLogCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.callback.IMKLogCallBack; 4 | import com.sun.jna.CallbackThreadInitializer; 5 | import com.sun.jna.Native; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * 日志输出广播 11 | */ 12 | @Slf4j 13 | @Component 14 | public class MKLogCallBack implements IMKLogCallBack { 15 | 16 | public MKLogCallBack() { 17 | //回调使用同一个线程 18 | Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "MediaServerLogThread")); 19 | } 20 | 21 | /** 22 | * 日志输出广播 23 | * 24 | * @param level 日志级别 25 | * @param file 源文件名 26 | * @param line 源文件行 27 | * @param function 源文件函数名 28 | * @param message 日志内容 29 | */ 30 | public void invoke(int level, String file, int line, String function, String message) { 31 | switch (level) { 32 | case 0: 33 | log.trace("【MediaServer】{}", message); 34 | break; 35 | case 1: 36 | log.debug("【MediaServer】{}", message); 37 | break; 38 | case 2: 39 | log.info("【MediaServer】{}", message); 40 | break; 41 | case 3: 42 | log.warn("【MediaServer】{}", message); 43 | break; 44 | case 4: 45 | log.error("【MediaServer】{}", message); 46 | break; 47 | default: 48 | log.info("【MediaServer】{}", message); 49 | break; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/service/IApiService.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.service; 2 | 3 | import com.ldf.media.api.model.param.*; 4 | import com.ldf.media.api.model.result.MediaInfoResult; 5 | import com.ldf.media.api.model.result.RtpServerResult; 6 | import com.ldf.media.api.model.result.Statistic; 7 | import com.ldf.media.api.model.result.StreamUrlResult; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | /** 13 | * 接口服务 14 | * 15 | * @author lidaofu 16 | * @since 2023/11/29 17 | **/ 18 | public interface IApiService { 19 | 20 | StreamUrlResult addStreamProxy(StreamProxyParam param); 21 | 22 | Boolean delStreamProxy(String key); 23 | 24 | String addStreamPusherProxy(StreamPushProxyParam param); 25 | 26 | Boolean delStreamPusherProxy(String key); 27 | 28 | Integer closeStream(CloseStreamParam param); 29 | 30 | Integer closeStreams(CloseStreamsParam param); 31 | 32 | List getMediaList(GetMediaListParam param); 33 | 34 | Boolean isMediaOnline(MediaQueryParam param); 35 | 36 | MediaInfoResult getMediaInfo(MediaQueryParam param); 37 | 38 | Boolean startRecord(StartRecordParam param); 39 | 40 | Boolean stopRecord(StopRecordParam param); 41 | 42 | Boolean isRecording(RecordStatusParam param); 43 | 44 | Statistic getStatistic(); 45 | 46 | String getServerConfig(); 47 | 48 | Boolean restartServer(); 49 | 50 | Integer setServerConfig(Map parameterMap); 51 | 52 | 53 | Integer openRtpServer(OpenRtpServerParam param); 54 | 55 | Integer closeRtpServer(String stream); 56 | 57 | List listRtpServer(); 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/TestVideoParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | 11 | @Data 12 | @ApiModel(value = "TestVideoParam对象", description = "测试流参数") 13 | public class TestVideoParam implements Serializable { 14 | 15 | private static final long serialVersionUID = 1; 16 | 17 | 18 | @NotBlank(message = "app不为空") 19 | @ApiModelProperty(value = "app", required = true) 20 | private String app; 21 | 22 | @NotBlank(message = "流id不为空") 23 | @ApiModelProperty(value = "流id", required = true) 24 | private String stream; 25 | 26 | @ApiModelProperty(value ="视频宽") 27 | private Integer width = 1920; 28 | 29 | @ApiModelProperty(value ="视频高") 30 | private Integer height = 1080; 31 | 32 | @ApiModelProperty(value = "帧率") 33 | private Integer fps =25; 34 | 35 | @ApiModelProperty(value = "比特率") 36 | private Integer bitRate=5000000; 37 | 38 | @ApiModelProperty(value = "自动关流") 39 | private Integer autoClose = 1; 40 | 41 | @ApiModelProperty(value = "开启hls转码") 42 | private Integer enableHls = 1; 43 | 44 | @ApiModelProperty(value = "开启rtsp/webrtc转码") 45 | private Integer enableRtsp = 1; 46 | 47 | @ApiModelProperty(value = "开启rtmp/flv转码") 48 | private Integer enableRtmp = 1; 49 | 50 | @ApiModelProperty(value = "开启ts/ws转码") 51 | private Integer enableTs = 0; 52 | 53 | @ApiModelProperty(value = "开启转fmp4") 54 | private Integer enableFmp4 = 0; 55 | 56 | @ApiModelProperty(value = "开启mp4录制") 57 | private Integer enableMp4 = 0; 58 | 59 | @ApiModelProperty(value = "mp4录制切片大小") 60 | private Integer mp4MaxSecond = 3600; 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/config/MediaServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * 媒体服务器配置 9 | * 10 | * @author lidaofu 11 | * @since 2023/11/29 12 | **/ 13 | @Data 14 | @Configuration 15 | @ConfigurationProperties(prefix = "media") 16 | public class MediaServerConfig { 17 | 18 | private String media_ip; 19 | 20 | private Integer thread_num; 21 | 22 | private Integer rtmp_port; 23 | 24 | private Integer rtsp_port; 25 | 26 | private Integer http_port; 27 | 28 | private Integer rtc_port; 29 | 30 | private Integer auto_close; 31 | 32 | private Integer streamNoneReaderDelayMS; 33 | 34 | private Integer maxStreamWaitMS; 35 | 36 | private Integer enable_ts; 37 | 38 | private Integer enable_hls; 39 | 40 | private Integer enable_fmp4; 41 | 42 | private Integer enable_rtsp; 43 | 44 | private Integer enable_rtmp; 45 | 46 | private Integer enable_mp4; 47 | 48 | private Integer enable_hls_fmp4; 49 | 50 | private Integer enable_audio; 51 | 52 | private Integer mp4_as_player; 53 | 54 | private Integer mp4_max_second; 55 | 56 | private String mp4_save_path; 57 | 58 | private String hls_save_path; 59 | 60 | private String rootPath; 61 | 62 | private Integer hls_demand; 63 | 64 | private Integer rtsp_demand; 65 | 66 | private Integer rtmp_demand; 67 | 68 | private Integer ts_demand; 69 | 70 | private Integer fmp4_demand; 71 | 72 | private Integer log_level; 73 | 74 | private Integer log_mask; 75 | 76 | private Integer log_file_days; 77 | 78 | private String log_path; 79 | 80 | private String rtc_host; 81 | 82 | private Integer broadcastRecordTs; 83 | 84 | private Integer segDur; 85 | 86 | private Integer segNum; 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/result/MediaInfoResult.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.result; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import java.io.Serializable; 8 | import java.util.List; 9 | 10 | /** 11 | * MediaInfoResult 12 | * 13 | * @author lidaofu 14 | * @since 2023/3/30 15 | **/ 16 | @Data 17 | @ApiModel(value = "GetMediaListParam对象", description = "流信息") 18 | public class MediaInfoResult implements Serializable { 19 | 20 | private static final long serialVersionUID = 1; 21 | 22 | @ApiModelProperty(value = "app") 23 | private String app; 24 | 25 | @ApiModelProperty(value = "流id") 26 | private String stream; 27 | 28 | @ApiModelProperty(value = "本协议观看人数") 29 | private Integer readerCount; 30 | 31 | @ApiModelProperty(value = "产生源类型,包括 unknown = 0,rtmp_push=1,rtsp_push=2,rtp_push=3,pull=4,ffmpeg_pull=5,mp4_vod=6,device_chn=7") 32 | private Integer originType; 33 | 34 | @ApiModelProperty(value = "产生源的url") 35 | private String originUrl; 36 | 37 | @ApiModelProperty(value = "产生源的url的类型") 38 | private String originTypeStr; 39 | 40 | @ApiModelProperty(value = "观看总数 包括hls/rtsp/rtmp/http-flv/ws-flv") 41 | private Integer totalReaderCount; 42 | 43 | @ApiModelProperty(value = "schema") 44 | private String schema; 45 | 46 | @ApiModelProperty(value = "存活时间,单位秒") 47 | private Long aliveSecond; 48 | 49 | @ApiModelProperty(value = "数据产生速度,单位byte/s") 50 | private Integer bytesSpeed; 51 | 52 | @ApiModelProperty(value = "GMT unix系统时间戳,单位秒") 53 | private Long createStamp; 54 | 55 | @ApiModelProperty(value = "是否录制Hls") 56 | private Boolean isRecordingHLS; 57 | 58 | @ApiModelProperty(value = "是否录制mp4") 59 | private Boolean isRecordingMP4; 60 | 61 | @ApiModelProperty(value = "虚拟地址") 62 | private String vhost; 63 | 64 | private List tracks; 65 | 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/VideoStackParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import io.swagger.annotations.ApiModel; 4 | import io.swagger.annotations.ApiModelProperty; 5 | import lombok.Data; 6 | 7 | import javax.validation.constraints.NotBlank; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | import java.util.List; 11 | 12 | @Data 13 | @ApiModel(value = "VideoStackParam对象", description = "拼接屏幕参数") 14 | public class VideoStackParam implements Serializable { 15 | 16 | private static final long serialVersionUID = 1; 17 | 18 | @NotBlank(message = "拼接屏任务id不为空") 19 | @ApiModelProperty(value = "拼接屏任务id(流id)", required = true) 20 | private String id; 21 | 22 | @ApiModelProperty(value = "拼接屏任务流app") 23 | private String app = "live"; 24 | 25 | @ApiModelProperty(value = "推流地址",notes = "如果传了pushUrl将不在本地产生流") 26 | private String pushUrl; 27 | 28 | @NotNull(message = "拼接屏行数不为空") 29 | @ApiModelProperty(value = "拼接屏行数", required = true) 30 | private Integer row; 31 | 32 | @NotNull(message = "拼接屏列行数不为空") 33 | @ApiModelProperty(value = "拼接屏列行数", required = true) 34 | private Integer col; 35 | 36 | @NotNull(message = "拼接屏宽度不为空") 37 | @ApiModelProperty(value = "拼接屏宽度", required = true) 38 | private Integer width; 39 | 40 | @NotNull(message = "拼接屏高度不为空") 41 | @ApiModelProperty(value = "拼接屏高度", required = true) 42 | private Integer height; 43 | 44 | @ApiModelProperty(value = "图片链接,为空则填灰色") 45 | private String fillImgUrl; 46 | 47 | @ApiModelProperty(value = "默认填充颜色") 48 | private String fillColor = "BFBFBF"; 49 | 50 | @ApiModelProperty(value = "是否存在分割线") 51 | private Boolean gridLineEnable = false; 52 | 53 | @ApiModelProperty(value = "分割线颜色") 54 | private String gridLineColor = "000000"; 55 | 56 | @ApiModelProperty(value = "分割线宽度") 57 | private Integer gridLineWidth = 1; 58 | 59 | @ApiModelProperty(value = "拼接屏内容") 60 | private List windowList; 61 | 62 | } -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/service/impl/TestVideoService.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.service.impl; 2 | 3 | import cn.hutool.core.lang.Assert; 4 | import cn.hutool.core.util.StrUtil; 5 | import com.aizuda.zlm4j.structure.MK_MEDIA_SOURCE; 6 | import com.ldf.media.api.model.param.TestVideoParam; 7 | import com.ldf.media.api.model.param.TranscodeParam; 8 | import com.ldf.media.api.model.result.StreamUrlResult; 9 | import com.ldf.media.api.service.ITestVideoService; 10 | import com.ldf.media.api.service.ITranscodeService; 11 | import com.ldf.media.config.MediaServerConfig; 12 | import com.ldf.media.constants.MediaServerConstants; 13 | import com.ldf.media.module.test.TestVideo; 14 | import com.ldf.media.module.transcode.Transcode; 15 | import com.ldf.media.pool.MediaServerThreadPool; 16 | import lombok.extern.slf4j.Slf4j; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.stereotype.Service; 19 | 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | 23 | import static com.ldf.media.context.MediaServerContext.ZLM_API; 24 | 25 | @Slf4j 26 | @Service 27 | public class TestVideoService implements ITestVideoService { 28 | @Autowired 29 | private MediaServerConfig config; 30 | /** 31 | * 测试视频 32 | */ 33 | private static final Map TEST_VIDEO_MAP = new HashMap<>(); 34 | 35 | @Override 36 | public StreamUrlResult createTestVideo(TestVideoParam param) { 37 | TestVideo testVideo = new TestVideo(param); 38 | testVideo.initVideo(); 39 | testVideo.startTestVideo(); 40 | TEST_VIDEO_MAP.put(param.getApp() + param.getStream(), testVideo); 41 | return new StreamUrlResult(config, param); 42 | } 43 | 44 | @Override 45 | public Boolean stopTestVideo(String app, String stream) { 46 | String key = app + stream; 47 | TestVideo testVideo = TEST_VIDEO_MAP.get(key); 48 | if (testVideo != null) { 49 | testVideo.closeVideo(); 50 | TEST_VIDEO_MAP.remove(key); 51 | return true; 52 | } 53 | return false; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/service/impl/TranscodeService.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.service.impl; 2 | 3 | import cn.hutool.core.lang.Assert; 4 | import cn.hutool.core.util.StrUtil; 5 | import com.aizuda.zlm4j.structure.MK_MEDIA_SOURCE; 6 | import com.ldf.media.api.model.param.TranscodeParam; 7 | import com.ldf.media.api.service.ITranscodeService; 8 | import com.ldf.media.config.MediaServerConfig; 9 | import com.ldf.media.constants.MediaServerConstants; 10 | import com.ldf.media.module.transcode.Transcode; 11 | import com.ldf.media.pool.MediaServerThreadPool; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | import static com.ldf.media.context.MediaServerContext.ZLM_API; 20 | 21 | @Slf4j 22 | @Service 23 | public class TranscodeService implements ITranscodeService { 24 | @Autowired 25 | private MediaServerConfig mediaServerConfig; 26 | private Map TRANSCODE_MAP = new HashMap<>(); 27 | 28 | @Override 29 | public void transcode(TranscodeParam param) { 30 | MK_MEDIA_SOURCE mkMediaSource = ZLM_API.mk_media_source_find2("rtmp", MediaServerConstants.DEFAULT_VHOST, param.getApp(), param.getStream(), 0); 31 | Assert.isNull(mkMediaSource, "当前流已在线"); 32 | Assert.isFalse(TRANSCODE_MAP.containsKey(param.getStream()), "转码任务已存在"); 33 | String pushUrl = StrUtil.format("rtmp://127.0.0.1:{}/{}/{}", mediaServerConfig.getRtmp_port(), param.getApp(), param.getStream()); 34 | Transcode transcode = new Transcode(param, pushUrl); 35 | MediaServerThreadPool.execute(() -> { 36 | transcode.start(); 37 | }); 38 | TRANSCODE_MAP.put(param.getStream(), transcode); 39 | } 40 | 41 | @Override 42 | public void stopTranscode(String stream) { 43 | Transcode transcode = TRANSCODE_MAP.get(stream); 44 | if (transcode!=null){ 45 | transcode.stop(); 46 | TRANSCODE_MAP.remove(stream); 47 | } 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/config/SwaggerConfiguration.java: -------------------------------------------------------------------------------- 1 | // 2 | package com.ldf.media.config; 3 | 4 | import io.swagger.annotations.ApiOperation; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Import; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration; 11 | import springfox.documentation.builders.ApiInfoBuilder; 12 | import springfox.documentation.builders.PathSelectors; 13 | import springfox.documentation.builders.RequestHandlerSelectors; 14 | import springfox.documentation.service.ApiInfo; 15 | import springfox.documentation.service.Contact; 16 | import springfox.documentation.spi.DocumentationType; 17 | import springfox.documentation.spring.web.plugins.Docket; 18 | import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc; 19 | 20 | @Configuration 21 | @Import(BeanValidatorPluginsConfiguration.class) 22 | @EnableSwagger2WebMvc 23 | public class SwaggerConfiguration implements WebMvcConfigurer { 24 | @Value("${swagger.enable:true}") 25 | private Boolean enable; 26 | 27 | @Bean(value = "defaultApi") 28 | public Docket defaultApi() { 29 | Docket docket = new Docket(DocumentationType.SWAGGER_2) 30 | .enable(enable) 31 | .apiInfo(apiInfo()) 32 | .select() 33 | .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) 34 | .paths(PathSelectors.any()) 35 | .build(); 36 | return docket; 37 | } 38 | 39 | private ApiInfo apiInfo() { 40 | return new ApiInfoBuilder() 41 | .title("JMediaServer接口文档") 42 | .description("# JMediaServer接口文档") 43 | .termsOfServiceUrl("http://127.0.0.1:8899/doc.html") 44 | .version("v1.0.0") 45 | .contact(new Contact("李道甫", "", "746101210@qq.com")) 46 | .build(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKStreamChangeCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.callback.IMKStreamChangeCallBack; 4 | import com.aizuda.zlm4j.structure.MK_MEDIA_SOURCE; 5 | import com.ldf.media.api.service.ITestVideoService; 6 | import com.ldf.media.api.service.ITranscodeService; 7 | import com.ldf.media.api.service.impl.ApiServiceImpl; 8 | import com.ldf.media.context.MediaServerContext; 9 | import com.ldf.media.module.test.TestVideo; 10 | import com.sun.jna.CallbackThreadInitializer; 11 | import com.sun.jna.Native; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Component; 15 | 16 | /** 17 | * 注册或反注册MediaSource事件广播 18 | * 19 | * @author lidaofu 20 | * @since 2023/11/23 21 | **/ 22 | @Component 23 | @Slf4j 24 | public class MKStreamChangeCallBack implements IMKStreamChangeCallBack { 25 | @Autowired 26 | private ITranscodeService transcodeService; 27 | @Autowired 28 | private ITestVideoService iTestVideoService; 29 | 30 | public MKStreamChangeCallBack() { 31 | Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "MediaStreamChangeThread")); 32 | } 33 | 34 | /** 35 | * 注册或反注册MediaSource事件广播 36 | * 37 | * @param regist 注册为1,注销为0 38 | * @param sender 该MediaSource对象 39 | */ 40 | public void invoke(int regist, MK_MEDIA_SOURCE sender) { 41 | //这里进行流状态处理 42 | String stream = MediaServerContext.ZLM_API.mk_media_source_get_stream(sender); 43 | String app = MediaServerContext.ZLM_API.mk_media_source_get_app(sender); 44 | String schema = MediaServerContext.ZLM_API.mk_media_source_get_schema(sender); 45 | //如果是regist是注销情况下无法获取流详细信息如观看人数等 46 | log.info("【MediaServer】APP:{} 流:{} 协议:{} {}", app, stream, schema, regist == 1 ? "注册" : "注销"); 47 | //停止转码 48 | if (schema.equals("rtmp") && regist == 0) { 49 | transcodeService.stopTranscode(stream); 50 | } 51 | //停止测试流 52 | if (regist == 0) { 53 | iTestVideoService.stopTestVideo(app,stream); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/param/StreamProxyParam.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.param; 2 | 3 | import cn.hutool.core.annotation.Alias; 4 | import io.swagger.annotations.ApiModel; 5 | import io.swagger.annotations.ApiModelProperty; 6 | import lombok.Data; 7 | 8 | import javax.validation.constraints.NotBlank; 9 | import javax.validation.constraints.NotNull; 10 | import java.math.BigDecimal; 11 | 12 | /** 13 | * 拉流代理参数 14 | * 15 | * @author lidaofu 16 | * @since 2023/11/29 17 | **/ 18 | @Data 19 | @ApiModel(value = "StreamProxyParam对象", description = "拉流代理参数") 20 | public class StreamProxyParam { 21 | 22 | 23 | @NotBlank(message = "app不为空") 24 | @ApiModelProperty(value = "app",required = true) 25 | private String app; 26 | 27 | @NotBlank(message = "流id不为空") 28 | @ApiModelProperty(value = "流id",required = true) 29 | private String stream; 30 | 31 | @NotBlank(message = "代理流地址不为空") 32 | @ApiModelProperty(value = "代理流地址",required = true) 33 | private String url; 34 | 35 | @ApiModelProperty(value = "rtsp拉流时,拉流方式,0:tcp,1:udp,2:组播") 36 | private Integer rtpType=0; 37 | 38 | @ApiModelProperty(value = "拉流重试次数,不传此参数或传值<=0时,则无限重试") 39 | private Integer retryCount=3; 40 | 41 | @ApiModelProperty(value = "拉流超时时间,单位秒型") 42 | private Integer timeoutSec; 43 | 44 | @ApiModelProperty(value = "开启hls转码") 45 | private Integer enableHls=1; 46 | 47 | @ApiModelProperty(value = "开启rtsp/webrtc转码") 48 | private Integer enableRtsp=1; 49 | 50 | @ApiModelProperty(value = "开启rtmp/flv转码") 51 | private Integer enableRtmp=1; 52 | 53 | @ApiModelProperty(value = "开启ts/ws转码") 54 | private Integer enableTs=0; 55 | 56 | @ApiModelProperty(value = "转协议是否开启音频") 57 | private Integer enableAudio=1; 58 | 59 | @ApiModelProperty(value = "开启转fmp4") 60 | private Integer enableFmp4=0; 61 | 62 | @ApiModelProperty(value = "开启mp4录制") 63 | private Integer enableMp4=0; 64 | 65 | @ApiModelProperty(value = "mp4录制切片大小") 66 | private Integer mp4MaxSecond=3600; 67 | 68 | @ApiModelProperty(value = "rtsp倍速") 69 | private BigDecimal rtspSpeed; 70 | 71 | @ApiModelProperty(value = "自动关流") 72 | private Integer autoClose = 1; 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/service/impl/VideoStackService.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.service.impl; 2 | 3 | import cn.hutool.core.lang.Assert; 4 | import cn.hutool.core.util.StrUtil; 5 | import com.aizuda.zlm4j.structure.MK_MEDIA_SOURCE; 6 | import com.ldf.media.api.model.param.VideoStackParam; 7 | import com.ldf.media.api.service.IVideoStackService; 8 | import com.ldf.media.config.MediaServerConfig; 9 | import com.ldf.media.constants.MediaServerConstants; 10 | import com.ldf.media.module.stack.VideoStack; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | import static com.ldf.media.context.MediaServerContext.ZLM_API; 19 | 20 | @Slf4j 21 | @Service 22 | public class VideoStackService implements IVideoStackService { 23 | @Autowired 24 | private MediaServerConfig mediaServerConfig; 25 | private final static Map VIDEO_STACK_MAP = new HashMap<>(); 26 | 27 | 28 | @Override 29 | public void startStack(VideoStackParam param) { 30 | if (StrUtil.isBlank(param.getPushUrl())) { 31 | MK_MEDIA_SOURCE mkMediaSource = ZLM_API.mk_media_source_find2("rtmp", MediaServerConstants.DEFAULT_VHOST, param.getApp(), param.getId(), 0); 32 | Assert.isNull(mkMediaSource, "当前流已在线"); 33 | } 34 | Assert.isFalse(VIDEO_STACK_MAP.containsKey(param.getId()), "拼接屏任务已存在"); 35 | //String pushUrl = StrUtil.format("rtmp://127.0.0.1:{}/{}/{}", mediaServerConfig.getRtmp_port(), param.getApp(), param.getId()); 36 | VideoStack videoStack = new VideoStack(param); 37 | videoStack.init(); 38 | VIDEO_STACK_MAP.put(param.getId(), videoStack); 39 | } 40 | 41 | 42 | @Override 43 | public void resetStack(VideoStackParam param) { 44 | VideoStack videoStack = VIDEO_STACK_MAP.get(param.getId()); 45 | Assert.isTrue(VIDEO_STACK_MAP.containsKey(param.getId()), "拼接屏任务不存在"); 46 | videoStack.reset(param); 47 | } 48 | 49 | @Override 50 | public void stopStack(String id) { 51 | VideoStack videoStack = VIDEO_STACK_MAP.get(id); 52 | Assert.isTrue(VIDEO_STACK_MAP.containsKey(id), "拼接屏任务不存在"); 53 | videoStack.stop(); 54 | VIDEO_STACK_MAP.remove(id); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/module/test/SystemMonitor.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.module.test; 2 | 3 | import com.sun.management.OperatingSystemMXBean; 4 | 5 | import java.lang.management.ManagementFactory; 6 | 7 | public class SystemMonitor { 8 | 9 | // 保存上一次CPU时间用于计算CPU占用率 10 | private static long lastSystemTime = 0; 11 | private static long lastProcessCpuTime = 0; 12 | private static float lastCpuUsage = 0.0f; 13 | private static float lastRamUsage = 0.0f; 14 | private static long lastCpuReadTime = 0; 15 | private static long lastRamReadTime = 0; 16 | 17 | /** 18 | * 获取当前JVM进程的CPU占用率 19 | * 20 | * @return CPU占用率,范围0.0-100.0 21 | */ 22 | public static float getProcessCpuUsage(long now) { 23 | if ((now - lastCpuReadTime) <= 1000) { 24 | return lastCpuUsage; 25 | } 26 | OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); 27 | long currentProcessCpuTime = osBean.getProcessCpuTime(); 28 | long currentSystemTime = System.nanoTime(); 29 | 30 | // 初始情况,返回0 31 | if (lastSystemTime == 0) { 32 | lastSystemTime = currentSystemTime; 33 | lastProcessCpuTime = currentProcessCpuTime; 34 | lastCpuReadTime = now; 35 | lastCpuUsage = 0.0f; 36 | return 0.0f; 37 | } 38 | long elapsedCpu = currentProcessCpuTime - lastProcessCpuTime; 39 | long elapsedTime = currentSystemTime - lastSystemTime; 40 | 41 | 42 | // 更新上一次的时间 43 | lastSystemTime = currentSystemTime; 44 | lastProcessCpuTime = currentProcessCpuTime; 45 | 46 | // 获取CPU核心数 47 | int availableProcessors = Runtime.getRuntime().availableProcessors(); 48 | // 计算单核心视角的CPU占用率(限制在100%以内) 49 | float cpuUsage = (elapsedCpu / (float) elapsedTime / availableProcessors) * 100; 50 | lastCpuUsage = cpuUsage; 51 | lastCpuReadTime = now; 52 | return cpuUsage; 53 | } 54 | 55 | /** 56 | * 获取系统RAM内存占用率 57 | * 58 | * @return 内存占用率,范围0.0-100.0 59 | */ 60 | public static float getMemoryUsage(long now) { 61 | if ((now - lastRamReadTime) <= 1000) { 62 | return lastRamUsage; 63 | } 64 | OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); 65 | 66 | // 总物理内存 67 | long totalMemory = osBean.getTotalPhysicalMemorySize(); 68 | // 可用物理内存 69 | long freeMemory = osBean.getFreePhysicalMemorySize(); 70 | // 已使用内存 71 | long usedMemory = totalMemory - freeMemory; 72 | 73 | // 计算内存占用率 74 | float memoryUsage = (usedMemory / (float) totalMemory) * 100; 75 | lastRamUsage = memoryUsage; 76 | lastRamReadTime = now; 77 | return memoryUsage; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/callback/MKPublishCallBack.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.callback; 2 | 3 | import com.aizuda.zlm4j.structure.MK_INI; 4 | import com.ldf.media.config.MediaServerConfig; 5 | import com.ldf.media.context.MediaServerContext; 6 | import com.aizuda.zlm4j.callback.IMKPublishCallBack; 7 | import com.aizuda.zlm4j.structure.MK_MEDIA_INFO; 8 | import com.aizuda.zlm4j.structure.MK_PUBLISH_AUTH_INVOKER; 9 | import com.aizuda.zlm4j.structure.MK_SOCK_INFO; 10 | import com.sun.jna.CallbackThreadInitializer; 11 | import com.sun.jna.Native; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Component; 14 | 15 | import static com.ldf.media.context.MediaServerContext.ZLM_API; 16 | 17 | /** 18 | * 推流回调 19 | * 20 | * @author lidaofu 21 | * @since 2023/11/29 22 | **/ 23 | @Component 24 | public class MKPublishCallBack implements IMKPublishCallBack { 25 | @Autowired 26 | private MediaServerConfig config; 27 | 28 | public MKPublishCallBack() { 29 | //回调使用同一个线程 30 | Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "MediaPublishThread")); 31 | } 32 | 33 | /** 34 | * 收到rtsp/rtmp推流事件广播,通过该事件控制推流鉴权 35 | * 36 | * @param url_info 推流url相关信息 37 | * @param invoker 执行invoker返回鉴权结果 38 | * @param sender 该tcp客户端相关信息 39 | * @see mk_publish_auth_invoker_do 40 | */ 41 | @Override 42 | public void invoke(MK_MEDIA_INFO url_info, MK_PUBLISH_AUTH_INVOKER invoker, MK_SOCK_INFO sender) { 43 | //这里拿到访问路径后(例如rtmp://xxxx/xxx/xxx?token=xxxx其中?后面就是拿到的参数)的参数 44 | // err_msg返回 空字符串表示鉴权成功 否则鉴权失败提示 45 | //String param = ZLM_API.mk_media_info_get_params(url_info); 46 | //MediaServerContext.ZLM_API.mk_publish_auth_invoker_do(invoker,"",1,0); 47 | MK_INI option = ZLM_API.mk_ini_create(); 48 | ZLM_API.mk_ini_set_option_int(option, "enable_mp4", config.getEnable_mp4()); 49 | ZLM_API.mk_ini_set_option_int(option, "enable_audio", config.getEnable_audio()); 50 | ZLM_API.mk_ini_set_option_int(option, "enable_fmp4",config.getEnable_fmp4()); 51 | ZLM_API.mk_ini_set_option_int(option, "enable_hls_fmp4",config.getEnable_hls_fmp4()); 52 | ZLM_API.mk_ini_set_option_int(option, "enable_ts", config.getEnable_ts()); 53 | ZLM_API.mk_ini_set_option_int(option, "enable_hls",config.getEnable_hls()); 54 | ZLM_API.mk_ini_set_option_int(option, "enable_rtsp", config.getEnable_rtsp()); 55 | ZLM_API.mk_ini_set_option_int(option, "enable_rtmp", config.getEnable_rtmp()); 56 | ZLM_API.mk_ini_set_option_int(option, "auto_close", config.getAuto_close()); 57 | ZLM_API.mk_ini_set_option_int(option, "mp4_max_second", config.getMp4_max_second()); 58 | ZLM_API.mk_ini_set_option_int(option, "segNum", config.getSegNum()); 59 | //流名称替换 60 | //ZLM_API.mk_ini_set_option(option, "stream_replace", "test1"); 61 | ZLM_API.mk_publish_auth_invoker_do2(invoker, "", option); 62 | ZLM_API.mk_ini_release(option); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 😁项目简介 2 | 3 | 开源流媒体服务器ZLMediaKit 的Java Api实现的Java版ZLMediaKit流媒体服务器 4 | 5 | 本项目可以作为[ZLM4J](https://gitee.com/aizuda/zlm4j)使用示例代码。 6 | 7 | 本项目接口风格部分兼容ZLMediaKit REST API 8 | 9 | ## 😁项目使用 10 | 运行程序并打开 http://127.0.0.1:8899 进行功能测试及文档查看 11 | 12 | ## 😁项目功能 13 | 14 | - **接口**(可以使用knife4j): 15 | - 拉流代理接口:/index/api/addStreamProxy 16 | - 关闭拉流代理接口:/index/api/delStreamProxy 17 | - 推流代理接口:/index/api/addStreamPusherProxy 18 | - 关闭推流代理接口:/index/api/delStreamPusherProxy 19 | - 关闭流接口:/index/api/close_stream&/index/api/close_streams 20 | - 在线流列表接口:/index/api/getMediaList 21 | - 流详情:/index/api/getMediaInfo 22 | - 流是否在线:/index/api/isMediaOnline 23 | - 开始录像接口:/index/api/startRecord 24 | - 停止录像接口:/index/api/stopRecord 25 | - 获取录像状态接口:/index/api/isRecording 26 | - 获取内存资源信息:/index/api/getStatistic 27 | - 获取服务器配置:/index/api/getServerConfig 28 | - 设置服务器配置:/index/api/setServerConfig 29 | - 开启rtp服务:/index/api/openRtpServer 30 | - 关闭rtp服务:/index/api/closeRtpServer 31 | - 获取rtp服务列表:/index/api/listRtpServer 32 | - 截图:/index/api/getSnap 33 | - 转码(beta) :/index/api/transcode 34 | - 开始拼接屏任务(beta) :/index/api/stack/start 35 | - 重设拼接屏任务(beta) :/index/api/stack/rest 36 | - 停止拼接屏任务(beta) :/index/api/stack/stop 37 | - 开始测试视频流 :/index/api/createTestVideo 38 | - 停止测试视频流 :/index/api/stopTestVideo 39 | - 开发中:😁 40 | - **回调实现**: 41 | - MKHttpAccessCallBack:http鉴权回调 42 | - MKHttpBeforeAccessCallBack:http前置鉴权回调 43 | - MKHttpFlowReportCallBack:码流数据统计回调 44 | - MKHttpRequestCallBack:http请求回调 45 | - MKLogCallBack:日志回调 46 | - MKNoFoundCallBack:未找到流回调 47 | - MKNoReaderCallBack:无人观看回调 48 | - MKPlayCallBack:播放回调 49 | - MKProxyPlayCloseCallBack:流代理关闭回调 50 | - MKPublishCallBack:推流回调 51 | - MKRecordMp4CallBack:录制回调 52 | - MKSourceFindCallBack:找不到流回调 53 | - MKStreamChangeCallBack:流上下回调 54 | - **流相关(注意rtmp_port、rtsp_port、http_port(非Spring Mvc端口)等参见application.yml,流APP、流名称可自定义)**: 55 | - RTMP推流:rtmp://ip:rtmp_port/流APP/流名称 56 | - FLV拉流:http://ip:http_port/流APP/流名称.live.flv 57 | - WS-FLV拉流:ws://ip:http_port/流APP/流名称.live.flv 58 | - HLS拉流:http://ip:http_port/流APP/流名称/hls.m3u8 59 | - RTMP拉流:rtmp://ip:rtmp_port/流APP/流名称 60 | - RTSP拉流:rtsp://ip:rtsp_port/流APP/流名称 61 | 62 | ## 😁项目组成 63 | 64 | 1. 本项目基于Spring Boot 2.7.12版本,使用undertow作为web容器,使用knife4j作为接口文档, 65 | 2. 本项目基于最新ZLM4J开发完成 66 | 67 | ## 😁拼接屏使用说明 68 | 69 | ### 参数说明 70 | 71 | ``` 72 | 以row:4 col:4 拼接16宫格为例 73 | 视频被分为下面的块 74 | 75 | | ----1--- | -----2---- | ----3--- | ----4---- | 76 | | ----5--- | -----6---- | ----7--- | ----8---- | 77 | | ----9--- | ----10---- | ----11---| ----12----| 78 | | ----13---| ----14---- | ----15---| ----16----| 79 | 80 | ``` 81 | 82 | ``` 83 | { 84 | "app": "live", //生成流的app 85 | "id": "test", //生成流的stream 也是任务id 86 | "pushUrl":"rtmp://127.0.0.1/live/test11",//如果填了这个上面的流不会在系统生效,直接推这个地址 87 | "height": 1920, //生成视频的宽 88 | "width": 1080, //生成视频的高 89 | "row": 4, //生成多少列 90 | "col": 4, //生成多少行 91 | "fillColor": "00ff00", //空块填充颜色 RGB hex字符串 92 | "fillImgUrl": "" //空块填充的图片 如果设置这个填充图片 则上面的fillColor失效, 需要图片像素格式为BGR格式的png或者jpg如果不是则默认填充颜色 93 | "gridLineEnable": true, //是否开启分割线 94 | "gridLineColor": "000000", //分割线颜色 RGB hex字符串 95 | "gridLineWidth": 1, //分割线宽度 96 | "windowList": [ //块配置数组,可为空 97 | { 98 | "imgUrl": "", //块内填充的图片地址 需要图片像素格式为BGR格式的png或者jpg如果不是则默认填充颜色 99 | "span": [1,2,5,6], //块所占的位置 多个则合并单元格 1 2 5 6 则代表占用 1 2 5 6四个单元格组成一个单元格 100 | "videoUrl": "rtsp://admin:Hk@123456@192.168.1.64/streaming/tracks/101", //块内填充的视频地址 如果设置这个地址 则块内填充的图片地址imageUrl失效 101 | "fillColor: "FF0000" //块内填充颜色 优先级最低 102 | }, 103 | { 104 | "imgUrl": "http://127.0.0.1/upload/test.jpg", //块内填充的图片地址 需要图片像素格式为BGR格式的png或者jpg如果不是则默认填充颜色 105 | "span": [13], //块所占的位置 多个则合并单元格 1 意思就是这个占用编号为13 的单元格 对应上面表 106 | "videoUrl": "", //块内填充的视频地址 如果设置这个地址 则块内填充的图片地址imageUrl失效 107 | "fillColor: "FF0000" //块内填充颜色 优先级最低 108 | } 109 | ] 110 | } 111 | ``` 112 | 113 | ## 😁常见问题 114 | 115 | 1. 参见[ZLM4J常见问题 ](https://ux5phie02ut.feishu.cn/wiki/SzIAwyxnpilVMlkccS4cfJFGn1g) 116 | 117 | ## 😁学习探讨 118 | 119 |

120 | 121 | zlm4j-qun 122 | 123 |

124 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/module/test/YUV420PBarGenerator.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.module.test; 2 | 3 | import java.util.Random; 4 | 5 | public class YUV420PBarGenerator { 6 | private static final int ALIGNMENT = 16; // 16字节对齐,可根据需要调整 7 | private static int frameCounter = 0; // 帧计数器用于实现滚动效果 8 | 9 | public static YUV420PFrame generateBarFrame(int width, int height) { 10 | // 计算对齐后的行跨度 11 | int yStride = align(width, ALIGNMENT); 12 | int uvStride = align(width / 2, ALIGNMENT); 13 | 14 | // 计算各平面大小 15 | int ySize = yStride * height; 16 | int uvSize = uvStride * height / 2; 17 | 18 | // 创建数据数组 19 | byte[] yData = new byte[ySize]; 20 | byte[] uData = new byte[uvSize]; 21 | byte[] vData = new byte[uvSize]; 22 | int[] linesize = new int[]{yStride, uvStride, uvStride}; 23 | 24 | // 生成条状图案 25 | generateBars(yData, uData, vData, linesize, width, height); 26 | 27 | // 增加帧计数器以实现滚动效果 28 | frameCounter = (frameCounter + 1) % width; 29 | 30 | return new YUV420PFrame(yData, uData, vData, linesize, width, height); 31 | } 32 | 33 | // 生成条状图案 34 | private static void generateBars(byte[] yData, byte[] uData, byte[] vData, 35 | int[] linesize, int width, int height) { 36 | int yStride = linesize[0]; 37 | int uvStride = linesize[1]; 38 | 39 | // 定义几种不同颜色的条状图案 40 | BarPattern[] patterns = { 41 | new BarPattern(100, 50, 50), // 灰色 42 | new BarPattern(180, 100, 100), // 亮黄色 43 | new BarPattern(80, 90, 200), // 蓝色 44 | new BarPattern(150, 40, 100), // 紫色 45 | new BarPattern(60, 200, 60) // 绿色 46 | }; 47 | 48 | Random random = new Random(); 49 | 50 | // 填充Y平面 51 | for (int y = 0; y < height; y++) { 52 | for (int x = 0; x < width; x++) { 53 | // 计算滚动位置 54 | int scrollX = (x + frameCounter) % width; 55 | 56 | // 根据位置选择图案 57 | int patternIndex = (scrollX / (width / patterns.length)) % patterns.length; 58 | BarPattern pattern = patterns[patternIndex]; 59 | 60 | // 设置Y值 61 | yData[y * yStride + x] = (byte) pattern.y; 62 | } 63 | } 64 | 65 | // 填充U和V平面 66 | for (int y = 0; y < height / 2; y++) { 67 | for (int x = 0; x < width / 2; x++) { 68 | // 计算滚动位置 69 | int scrollX = (x * 2 + frameCounter) % width; 70 | 71 | // 根据位置选择图案 72 | int patternIndex = (scrollX / (width / patterns.length)) % patterns.length; 73 | BarPattern pattern = patterns[patternIndex]; 74 | 75 | // 设置U和V值 76 | uData[y * uvStride + x] = (byte) pattern.u; 77 | vData[y * uvStride + x] = (byte) pattern.v; 78 | } 79 | } 80 | } 81 | 82 | // 字节对齐计算 83 | private static int align(int value, int alignment) { 84 | return (value + alignment - 1) & ~(alignment - 1); 85 | } 86 | 87 | // 条状图案定义 88 | private static class BarPattern { 89 | int y; 90 | int u; 91 | int v; 92 | 93 | BarPattern(int y, int u, int v) { 94 | this.y = y; 95 | this.u = u; 96 | this.v = v; 97 | } 98 | } 99 | 100 | // 帧数据容器类 101 | public static class YUV420PFrame { 102 | private final byte[] yData; 103 | private final byte[] uData; 104 | private final byte[] vData; 105 | private final int[] linesize; 106 | private final int width; 107 | private final int height; 108 | 109 | public YUV420PFrame(byte[] yData, byte[] uData, byte[] vData, int[] linesize, int width, int height) { 110 | this.yData = yData; 111 | this.uData = uData; 112 | this.vData = vData; 113 | this.linesize = linesize; 114 | this.width = width; 115 | this.height = height; 116 | } 117 | 118 | public byte[] getYData() { 119 | return yData; 120 | } 121 | 122 | public byte[] getUData() { 123 | return uData; 124 | } 125 | 126 | public byte[] getVData() { 127 | return vData; 128 | } 129 | 130 | public int[] getLinesize() { 131 | return linesize; 132 | } 133 | 134 | public int getWidth() { 135 | return width; 136 | } 137 | 138 | public int getHeight() { 139 | return height; 140 | } 141 | 142 | // 打印帧信息(用于调试) 143 | public void printInfo() { 144 | System.out.println("Frame size: " + width + "x" + height); 145 | System.out.println("Y plane: " + yData.length + " bytes, stride: " + linesize[0]); 146 | System.out.println("U plane: " + uData.length + " bytes, stride: " + linesize[1]); 147 | System.out.println("V plane: " + vData.length + " bytes, stride: " + linesize[2]); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.7.12 10 | 11 | 12 | com.ldf 13 | j-media-server 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 8 18 | 8 19 | UTF-8 20 | 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-web 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-tomcat 31 | 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-undertow 37 | 38 | 39 | 40 | org.jboss.xnio 41 | xnio-nio 42 | 43 | 44 | 45 | 46 | org.jboss.xnio 47 | xnio-nio 48 | 3.8.9.Final 49 | 50 | 51 | 52 | org.projectlombok 53 | lombok 54 | true 55 | 56 | 57 | cn.hutool 58 | hutool-core 59 | 5.8.25 60 | 61 | 62 | cn.hutool 63 | hutool-json 64 | 5.8.25 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-starter-validation 69 | 70 | 71 | 72 | com.github.xiaoymin 73 | knife4j-openapi2-spring-boot-starter 74 | 4.5.0 75 | 76 | 77 | 78 | com.aizuda 79 | zlm4j 80 | 1.6.1 81 | 82 | 83 | 84 | org.bytedeco 85 | javacv 86 | 1.5.10 87 | 88 | 89 | org.bytedeco 90 | ffmpeg 91 | 6.1.1-1.5.10 92 | windows-x86_64-gpl 93 | 94 | 95 | org.bytedeco 96 | ffmpeg 97 | 6.1.1-1.5.10 98 | linux-x86_64-gpl 99 | 100 | 101 | 102 | 103 | 104 | nexus-tencentyun 105 | Nexus tencentyun 106 | https://mirrors.cloud.tencent.com/nexus/repository/maven-public/ 107 | 108 | 109 | 110 | 111 | 112 | 113 | org.springframework.boot 114 | spring-boot-maven-plugin 115 | 116 | j-media-server 117 | true 118 | 119 | 120 | 121 | 122 | repackage 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/main/resources/static/play.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 88 | 89 | 90 |
91 |
92 |
93 |
94 |
输入URL:
95 | 100 | 101 | 102 |
103 |
104 | 105 |
106 |
107 |
108 | 109 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/model/result/StreamUrlResult.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.model.result; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.ldf.media.api.model.param.StreamProxyParam; 5 | import com.ldf.media.api.model.param.TestVideoParam; 6 | import com.ldf.media.config.MediaServerConfig; 7 | import io.swagger.annotations.ApiModelProperty; 8 | import lombok.Data; 9 | 10 | import java.io.Serializable; 11 | 12 | @Data 13 | public class StreamUrlResult implements Serializable { 14 | private static final long serialVersionUID = 1; 15 | 16 | @ApiModelProperty(value = "拉流代理key") 17 | private String key; 18 | 19 | @ApiModelProperty(value = "app") 20 | private String app; 21 | 22 | @ApiModelProperty(value = "流id") 23 | private String stream; 24 | 25 | @ApiModelProperty("hlsUrl播放地址") 26 | private String hlsUrl; 27 | 28 | @ApiModelProperty("rtspUrl播放地址") 29 | private String rtspUrl; 30 | 31 | @ApiModelProperty("rtmpUrl播放地址") 32 | private String rtmpUrl; 33 | 34 | @ApiModelProperty("wsflv播放地址") 35 | private String wsFlvUrl; 36 | 37 | @ApiModelProperty("httpflv播放地址") 38 | private String httpFlvUrl; 39 | 40 | @ApiModelProperty("httpFmp4Url播放地址") 41 | private String httpFmp4Url; 42 | 43 | @ApiModelProperty("wsFmp4Url播放地址") 44 | private String wsFmp4Url; 45 | 46 | @ApiModelProperty("httpTsUrl播放地址") 47 | private String httpTsUrl; 48 | 49 | @ApiModelProperty("wsTsUrl播放地址") 50 | private String wsTsUrl; 51 | 52 | public StreamUrlResult(MediaServerConfig config, StreamProxyParam param, String key) { 53 | this.app = param.getApp(); 54 | this.key = key; 55 | this.stream = param.getStream(); 56 | if (param.getEnableRtmp() == 1) { 57 | this.wsFlvUrl = StrUtil.format("ws://{}:{}/{}/{}.live.flv", config.getMedia_ip(), config.getHttp_port(), app, stream); 58 | this.httpFlvUrl = StrUtil.format("http://{}:{}/{}/{}.live.flv", config.getMedia_ip(), config.getHttp_port(), app, stream); 59 | if (config.getRtmp_port() == 1935) { 60 | this.rtmpUrl = StrUtil.format("rtmp://{}/{}/{}", config.getMedia_ip(), app, stream); 61 | } else { 62 | this.rtmpUrl = StrUtil.format("rtmp://{}:{}/{}/{}", config.getMedia_ip(), config.getRtmp_port(), app, stream); 63 | } 64 | } 65 | if (param.getEnableHls() == 1) { 66 | this.hlsUrl = StrUtil.format("http://{}:{}/{}/{}/hls.m3u8", config.getMedia_ip(), config.getHttp_port(), app, stream); 67 | } 68 | if (param.getEnableRtsp() == 1) { 69 | if (config.getRtsp_port() == 554) { 70 | this.rtspUrl = StrUtil.format("rtsp://{}/{}/{}", config.getMedia_ip(), app, stream); 71 | } else { 72 | this.rtspUrl = StrUtil.format("rtsp://{}:{}/{}/{}", config.getMedia_ip(), config.getRtsp_port(), app, stream); 73 | } 74 | } 75 | if (param.getEnableFmp4() == 1) { 76 | this.wsFmp4Url = StrUtil.format("ws://{}:{}/{}/{}.live.mp4", config.getMedia_ip(), config.getHttp_port(), app, stream); 77 | this.httpFmp4Url = StrUtil.format("http://{}:{}/{}/{}.live.mp4", config.getMedia_ip(), config.getHttp_port(), app, stream); 78 | } 79 | 80 | if (param.getEnableTs() == 1) { 81 | this.wsTsUrl = StrUtil.format("ws://{}:{}/{}/{}.live.ts", config.getMedia_ip(), config.getHttp_port(), app, stream); 82 | this.httpTsUrl = StrUtil.format("http://{}:{}/{}/{}.live.ts", config.getMedia_ip(), config.getHttp_port(), app, stream); 83 | } 84 | 85 | } 86 | 87 | public StreamUrlResult(MediaServerConfig config, TestVideoParam param) { 88 | this.app = param.getApp(); 89 | this.stream = param.getStream(); 90 | if (param.getEnableRtmp() == 1) { 91 | this.wsFlvUrl = StrUtil.format("ws://{}:{}/{}/{}.live.flv", config.getMedia_ip(), config.getHttp_port(), app, stream); 92 | this.httpFlvUrl = StrUtil.format("http://{}:{}/{}/{}.live.flv", config.getMedia_ip(), config.getHttp_port(), app, stream); 93 | if (config.getRtmp_port() == 1935) { 94 | this.rtmpUrl = StrUtil.format("rtmp://{}/{}/{}", config.getMedia_ip(), app, stream); 95 | } else { 96 | this.rtmpUrl = StrUtil.format("rtmp://{}:{}/{}/{}", config.getMedia_ip(), config.getRtmp_port(), app, stream); 97 | } 98 | } 99 | if (param.getEnableHls() == 1) { 100 | this.hlsUrl = StrUtil.format("http://{}:{}/{}/{}/hls.m3u8", config.getMedia_ip(), config.getHttp_port(), app, stream); 101 | } 102 | if (param.getEnableRtsp() == 1) { 103 | if (config.getRtsp_port() == 554) { 104 | this.rtspUrl = StrUtil.format("rtsp://{}/{}/{}", config.getMedia_ip(), app, stream); 105 | } else { 106 | this.rtspUrl = StrUtil.format("rtsp://{}:{}/{}/{}", config.getMedia_ip(), config.getRtsp_port(), app, stream); 107 | } 108 | } 109 | if (param.getEnableFmp4() == 1) { 110 | this.wsFmp4Url = StrUtil.format("ws://{}:{}/{}/{}.live.mp4", config.getMedia_ip(), config.getHttp_port(), app, stream); 111 | this.httpFmp4Url = StrUtil.format("http://{}:{}/{}/{}.live.mp4", config.getMedia_ip(), config.getHttp_port(), app, stream); 112 | } 113 | 114 | if (param.getEnableTs() == 1) { 115 | this.wsTsUrl = StrUtil.format("ws://{}:{}/{}/{}.live.ts", config.getMedia_ip(), config.getHttp_port(), app, stream); 116 | this.httpTsUrl = StrUtil.format("http://{}:{}/{}/{}.live.ts", config.getMedia_ip(), config.getHttp_port(), app, stream); 117 | } 118 | 119 | } 120 | 121 | public StreamUrlResult() { 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/module/test/TestVideo.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.module.test; 2 | 3 | import cn.hutool.core.date.DateUtil; 4 | import cn.hutool.core.util.StrUtil; 5 | import com.aizuda.zlm4j.structure.MK_INI; 6 | import com.aizuda.zlm4j.structure.MK_MEDIA; 7 | import com.ldf.media.api.model.param.TestVideoParam; 8 | import com.sun.jna.Memory; 9 | import com.sun.jna.Pointer; 10 | 11 | import java.awt.*; 12 | import java.util.concurrent.LinkedBlockingQueue; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | import static com.ldf.media.context.MediaServerContext.ZLM_API; 16 | 17 | public class TestVideo { 18 | private TestVideoParam param; 19 | private MK_MEDIA mkMedia; 20 | private Pointer yPointer; 21 | private Pointer uPointer; 22 | private Pointer vPointer; 23 | private int[] linesize; 24 | private LinkedBlockingQueue frameQueue; 25 | private long pts = 0; 26 | 27 | public TestVideo(TestVideoParam param) { 28 | this.param = param; 29 | this.frameQueue = new LinkedBlockingQueue<>(25); 30 | } 31 | 32 | /** 33 | * 初始化视频 34 | */ 35 | public void initVideo() { 36 | //创建媒体流 支持配置参数 37 | MK_INI mkIni = ZLM_API.mk_ini_create(); 38 | ZLM_API.mk_ini_set_option_int(mkIni, "enable_rtsp", param.getEnableRtsp()); 39 | ZLM_API.mk_ini_set_option_int(mkIni, "enable_rtmp", param.getEnableRtmp()); 40 | ZLM_API.mk_ini_set_option_int(mkIni, "enable_fmp4", param.getEnableFmp4()); 41 | ZLM_API.mk_ini_set_option_int(mkIni, "enable_hls", param.getEnableHls()); 42 | ZLM_API.mk_ini_set_option_int(mkIni, "enable_ts", param.getEnableTs()); 43 | ZLM_API.mk_ini_set_option_int(mkIni, "enable_mp4", param.getEnableMp4()); 44 | ZLM_API.mk_ini_set_option_int(mkIni, "mp4_max_second", param.getMp4MaxSecond()); 45 | ZLM_API.mk_ini_set_option_int(mkIni, "auto_close", param.getAutoClose()); 46 | mkMedia = ZLM_API.mk_media_create2("__defaultVhost__", param.getApp(), param.getStream(), 0, mkIni); 47 | ZLM_API.mk_ini_release(mkIni); 48 | ZLM_API.mk_media_init_video(mkMedia, 0, param.getWidth(), param.getHeight(), param.getFps(), param.getBitRate()); 49 | ZLM_API.mk_media_init_complete(mkMedia); 50 | } 51 | 52 | /** 53 | * 开始测试视频 54 | */ 55 | public void startTestVideo() { 56 | new Thread(() -> { 57 | createVideoFrame(); 58 | }).start(); 59 | new Thread(() -> { 60 | sendVideoFrame(); 61 | }).start(); 62 | } 63 | 64 | /** 65 | * 创建视频帧 66 | */ 67 | private void createVideoFrame() { 68 | int timebase = 1000 / param.getFps(); 69 | while (mkMedia != null) { 70 | long startTime = System.currentTimeMillis(); 71 | //YUV420PBarGenerator.YUV420PFrame yuv420PFrame = YUV420PBarGenerator.generateBarFrame(param.getWidth(), param.getHeight()); 72 | Color bgColor = new Color(200, 255, 255); 73 | Color textColor = new Color(255, 120, 0); 74 | String text= StrUtil.format("欢迎使用ZLM4J!这是一个演示视频,当前时间是:{}", DateUtil.now()); 75 | YuvImageGenerator.YUV420PFrame yuv420PFrame = YuvImageGenerator.generateYuv420pImage(param.getWidth(), param.getHeight(), bgColor, text, 56, textColor); 76 | if (yPointer == null) { 77 | yPointer = new Memory((long) yuv420PFrame.getHeight() * yuv420PFrame.getLinesize()[0]); 78 | } 79 | if (uPointer == null) { 80 | uPointer = new Memory((long) yuv420PFrame.getHeight() * yuv420PFrame.getLinesize()[1] / 2); 81 | } 82 | if (vPointer == null) { 83 | vPointer = new Memory((long) yuv420PFrame.getHeight() * yuv420PFrame.getLinesize()[2] / 2); 84 | } 85 | yPointer.write(0, yuv420PFrame.getYData(), 0, yuv420PFrame.getHeight() * yuv420PFrame.getLinesize()[0]); 86 | uPointer.write(0, yuv420PFrame.getUData(), 0, yuv420PFrame.getHeight() * yuv420PFrame.getLinesize()[1] / 2); 87 | vPointer.write(0, yuv420PFrame.getVData(), 0, yuv420PFrame.getHeight() * yuv420PFrame.getLinesize()[2] / 2); 88 | linesize = yuv420PFrame.getLinesize(); 89 | while (true) { 90 | long diffTime = System.currentTimeMillis() - startTime; 91 | if (diffTime < timebase) { 92 | try { 93 | Thread.sleep(2); 94 | } catch (InterruptedException e) { 95 | } 96 | } else { 97 | break; 98 | } 99 | } 100 | frameQueue.offer(pts); 101 | pts += timebase; 102 | } 103 | } 104 | 105 | /** 106 | * 发送帧 107 | */ 108 | private void sendVideoFrame() { 109 | while (mkMedia != null) { 110 | try { 111 | Long ptsNow = frameQueue.poll(5, TimeUnit.SECONDS); 112 | if (ptsNow!=null) { 113 | synchronized (this) { 114 | if (mkMedia != null) { 115 | ZLM_API.mk_media_input_yuv(mkMedia, new Pointer[]{yPointer, uPointer, vPointer}, linesize, ptsNow); 116 | } 117 | } 118 | } 119 | } catch (InterruptedException e) { 120 | 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * 关闭视频 127 | */ 128 | public void closeVideo() { 129 | synchronized (this) { 130 | if (mkMedia != null) { 131 | ZLM_API.mk_media_release(mkMedia); 132 | mkMedia = null; 133 | } 134 | if (yPointer != null) { 135 | ((Memory) yPointer).close(); 136 | } 137 | if (uPointer != null) { 138 | ((Memory) uPointer).close(); 139 | } 140 | if (vPointer != null) { 141 | ((Memory) vPointer).close(); 142 | } 143 | } 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/module/test/YuvImageGenerator.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.module.test; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | 5 | import java.awt.*; 6 | import java.awt.image.BufferedImage; 7 | import java.util.Arrays; 8 | 9 | public class YuvImageGenerator { 10 | static { 11 | SimHeiFontLoader.loadSimHeiFont(24); 12 | } 13 | 14 | // 用于包装YUV420P数据的类 15 | public static class YUV420PFrame { 16 | private final byte[] yData; 17 | private final byte[] uData; 18 | private final byte[] vData; 19 | private final int[] linesize; 20 | private final int width; 21 | private final int height; 22 | 23 | public YUV420PFrame(byte[] yData, byte[] uData, byte[] vData, int[] linesize, int width, int height) { 24 | this.yData = yData; 25 | this.uData = uData; 26 | this.vData = vData; 27 | this.linesize = linesize; 28 | this.width = width; 29 | this.height = height; 30 | } 31 | 32 | // getter方法 33 | public byte[] getYData() { 34 | return yData; 35 | } 36 | 37 | public byte[] getUData() { 38 | return uData; 39 | } 40 | 41 | public byte[] getVData() { 42 | return vData; 43 | } 44 | 45 | public int[] getLinesize() { 46 | return linesize; 47 | } 48 | 49 | public int getWidth() { 50 | return width; 51 | } 52 | 53 | public int getHeight() { 54 | return height; 55 | } 56 | } 57 | 58 | /** 59 | * 生成包含指定文字的YUV420P格式图像 60 | * 61 | * @param width 图像宽度 62 | * @param height 图像高度 63 | * @param backgroundColor 背景色 64 | * @param text 需要显示的文字 65 | * @param textSize 文字大小 66 | * @param textColor 文字颜色(新增参数) 67 | * @return 包装了YUV420P数据的YUV420PFrame对象 68 | */ 69 | public static YUV420PFrame generateYuv420pImage(int width, int height, Color backgroundColor, 70 | String text, int textSize, Color textColor) { 71 | // 创建BufferedImage绘制文字 72 | BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); 73 | Graphics2D g2d = bufferedImage.createGraphics(); 74 | 75 | // 设置背景色 76 | g2d.setColor(backgroundColor); 77 | g2d.fillRect(0, 0, width, height); 78 | 79 | // 设置文字属性并绘制文字 80 | if (text != null && !text.isEmpty()) { 81 | // 使用指定的文字颜色 82 | g2d.setColor(textColor); 83 | g2d.setFont(new Font("SimHei",Font.PLAIN,textSize)); 84 | 85 | // 计算文字位置使其居中 86 | FontMetrics metrics = g2d.getFontMetrics(); 87 | int x = (width - metrics.stringWidth(text)) / 2; 88 | int y = (height - metrics.getHeight()) / 2 + metrics.getAscent(); 89 | g2d.drawString(text, x, y - 64); 90 | g2d.drawString(StrUtil.format("当前视频分辨率为:{}x{}", width, height), x, y); 91 | long now = System.currentTimeMillis(); 92 | g2d.drawString(StrUtil.format("当前系统CPU占用: {}%", SystemMonitor.getProcessCpuUsage(now)), x, y + 64); 93 | g2d.drawString(StrUtil.format("当前系统RAM占用: {}%", SystemMonitor.getMemoryUsage(now)), x, y + 128); 94 | } 95 | g2d.dispose(); 96 | 97 | // 转换为YUV420P格式并分离Y、U、V分量 98 | byte[][] yuvPlanes = rgbToYuv420pPlanes(bufferedImage, width, height); 99 | byte[] yData = yuvPlanes[0]; 100 | byte[] uData = yuvPlanes[1]; 101 | byte[] vData = yuvPlanes[2]; 102 | 103 | // 计算行大小 104 | int[] lineSizes = new int[3]; 105 | lineSizes[0] = width; // Y分量行大小 106 | lineSizes[1] = width / 2; // U分量行大小 107 | lineSizes[2] = width / 2; // V分量行大小 108 | 109 | return new YUV420PFrame(yData, uData, vData, lineSizes, width, height); 110 | } 111 | 112 | /** 113 | * 将RGB图像转换为YUV420P格式并分离为Y、U、V三个平面 114 | */ 115 | private static byte[][] rgbToYuv420pPlanes(BufferedImage image, int width, int height) { 116 | int ySize = width * height; 117 | int uvSize = (width / 2) * (height / 2); 118 | 119 | // 初始化Y、U、V三个分量数组 120 | byte[] yData = new byte[ySize]; 121 | byte[] uData = new byte[uvSize]; 122 | byte[] vData = new byte[uvSize]; 123 | 124 | // 提取RGB数据 125 | int[] rgb = new int[ySize]; 126 | image.getRGB(0, 0, width, height, rgb, 0, width); 127 | 128 | // 转换为YUV并填充到相应数组 129 | int yIndex = 0; 130 | int uvIndex = 0; 131 | 132 | for (int i = 0; i < height; i++) { 133 | for (int j = 0; j < width; j++) { 134 | int pixel = rgb[i * width + j]; 135 | 136 | // 提取RGB分量 137 | int r = (pixel >> 16) & 0xFF; 138 | int g = (pixel >> 8) & 0xFF; 139 | int b = pixel & 0xFF; 140 | 141 | // 计算Y分量 142 | yData[yIndex++] = (byte) (0.299 * r + 0.587 * g + 0.114 * b + 0.5); 143 | 144 | // 每2x2像素计算一次UV分量 145 | if (i % 2 == 0 && j % 2 == 0) { 146 | // 计算U和V分量 147 | uData[uvIndex] = (byte) (-0.14713 * r - 0.28886 * g + 0.436 * b + 128 + 0.5); 148 | vData[uvIndex] = (byte) (0.615 * r - 0.51499 * g - 0.10001 * b + 128 + 0.5); 149 | uvIndex++; 150 | } 151 | } 152 | } 153 | 154 | return new byte[][]{yData, uData, vData}; 155 | } 156 | 157 | // 使用示例 158 | public static void main(String[] args) { 159 | int width = 640; 160 | int height = 480; 161 | Color bgColor = new Color(255, 255, 255); // 白色背景 162 | String text = "Hello, YUV420P!"; 163 | int textSize = 24; 164 | Color textColor = new Color(255, 0, 0); // 红色文字(新增参数示例) 165 | 166 | YUV420PFrame frame = generateYuv420pImage(width, height, bgColor, text, textSize, textColor); 167 | 168 | System.out.println("图像宽度: " + frame.getWidth()); 169 | System.out.println("图像高度: " + frame.getHeight()); 170 | System.out.println("Y分量长度: " + frame.getYData().length); 171 | System.out.println("U分量长度: " + frame.getUData().length); 172 | System.out.println("V分量长度: " + frame.getVData().length); 173 | System.out.println("行大小: " + Arrays.toString(frame.getLinesize())); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/controller/WebRTCController.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.controller; 2 | 3 | import cn.hutool.core.util.RandomUtil; 4 | import cn.hutool.core.util.StrUtil; 5 | import cn.hutool.json.JSONObject; 6 | import com.aizuda.zlm4j.callback.IMKWebRtcGetAnwerSdpCallBack; 7 | import com.ldf.media.config.MediaServerConfig; 8 | import io.swagger.annotations.Api; 9 | import io.swagger.annotations.ApiOperation; 10 | import io.swagger.annotations.ApiParam; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.web.bind.annotation.*; 17 | import org.springframework.web.context.request.async.DeferredResult; 18 | 19 | import javax.servlet.http.HttpServletRequest; 20 | import javax.servlet.http.HttpServletResponse; 21 | import java.io.IOException; 22 | 23 | import static com.ldf.media.context.MediaServerContext.ZLM_API; 24 | 25 | /** 26 | * WebRTC信令模块 27 | * 28 | * @author lidaofu 29 | * @since 2024/4/19 30 | **/ 31 | @Api(tags = "【API】webrtc Api") 32 | @RestController 33 | @Slf4j 34 | @CrossOrigin("*") 35 | public class WebRTCController { 36 | @Autowired 37 | private MediaServerConfig config; 38 | @Autowired 39 | private HttpServletRequest request; 40 | @Autowired 41 | private HttpServletResponse response; 42 | @Value("${server.port}") 43 | private int port; 44 | 45 | /** 46 | * webrtc sdp协议交互 47 | * 48 | * @param app 应用名称 49 | * @param stream 流标识 50 | * @param type sdp协议动作类型:play、push、echo 51 | * @param pcSdp 前端请求的sdp协议 52 | * @return 服务端响应的sdp协议 53 | * @throws IOException 54 | */ 55 | @ApiOperation(value = "【webrtc】sdp协议交换") 56 | @PostMapping("/index/api/webrtc") 57 | public DeferredResult> webrtc(@ApiParam("应用名称") String app, 58 | @ApiParam("流标识") String stream, 59 | @ApiParam("sdp协议动作类型:play、push、echo") String type, 60 | @RequestBody String pcSdp) throws IOException { 61 | DeferredResult> out = new DeferredResult<>(); 62 | //webrtc使用的是udp,默认监听8000,不需要设置端口号 63 | String rtcUrl = StrUtil.format("rtc://{}:{}/{}/{}", config.getRtc_host(), config.getRtc_port(), app, stream); 64 | IMKWebRtcGetAnwerSdpCallBack imkWebRtcGetAnwerSdpCallBack = createWebrtcAnswerSdpCallback(out); 65 | ZLM_API.mk_webrtc_get_answer_sdp(null, imkWebRtcGetAnwerSdpCallBack, type, pcSdp, rtcUrl); 66 | return out; 67 | } 68 | 69 | /** 70 | * 构建服务端SDP协议回调对象 71 | * 72 | * @param out 异步接受对象 73 | * @return 74 | */ 75 | private static IMKWebRtcGetAnwerSdpCallBack createWebrtcAnswerSdpCallback(DeferredResult> out) { 76 | return (pointer, sevSdp, error) -> { 77 | JSONObject result = new JSONObject(); 78 | if (StrUtil.isNotBlank(error)) { 79 | log.error("zkMediaKit 交互 webrtc 协议失败!"); 80 | result.putOnce("code", -1).putOnce("sdp", null); 81 | } else { 82 | log.info("zkMediaKit 交互 webrtc 协议成功!"); 83 | result.putOnce("code", 0).putOnce("sdp", sevSdp); 84 | } 85 | ResponseEntity response = new ResponseEntity<>(result.toString(), HttpStatus.OK); 86 | out.setResult(response); 87 | }; 88 | } 89 | 90 | 91 | /** 92 | * 推流sdp交互 93 | * 94 | * @param app 95 | * @param stream 96 | */ 97 | @PostMapping("/index/api/whip") 98 | public void whip(String app, String stream, @RequestBody String offerSdp) { 99 | //这里可以做鉴权 100 | String authorization = request.getHeader("Authorization"); 101 | //webrtc使用的是udp,默认监听8000,不需要设置端口号 102 | String rtcUrl = StrUtil.format("rtc://{}:{}/{}/{}", config.getRtc_host(), config.getRtc_port(), app, stream); 103 | IMKWebRtcGetAnwerSdpCallBack imkWebRtcGetAnwerSdpCallBack = (pointer, sdp, s1) -> { 104 | try { 105 | response.setContentType("application/sdp"); 106 | //todo 如果是https请换为https 107 | String location = StrUtil.format("http://{}:{}/index/api/delete_webrtc?id={}&token={}", config.getRtc_host(), port, "whip_" + stream, RandomUtil.randomString(8)); 108 | response.setHeader("Location", location); 109 | response.setStatus(201); 110 | response.getWriter().write(sdp); 111 | } catch (IOException e) { 112 | throw new RuntimeException(e); 113 | } 114 | }; 115 | ZLM_API.mk_webrtc_get_answer_sdp(null, imkWebRtcGetAnwerSdpCallBack, "push", offerSdp, rtcUrl); 116 | } 117 | 118 | /** 119 | * 拉流sdp交互 120 | * 121 | * @param app 122 | * @param stream 123 | */ 124 | @PostMapping("/index/api/whep") 125 | public void whep(String app, String stream, @RequestBody String offerSdp) { 126 | String rtcUrl = StrUtil.format("rtc://{}:{}/{}/{}", config.getRtc_host(), config.getRtc_port(), app, stream); 127 | IMKWebRtcGetAnwerSdpCallBack imkWebRtcGetAnwerSdpCallBack = (pointer, sdp, s1) -> { 128 | try { 129 | response.setContentType("application/sdp"); 130 | //todo 如果是https请换为https 131 | String location = StrUtil.format("http://{}:{}/index/api/delete_webrtc?id={}&token={}", config.getRtc_host(), port, "whip_" + stream, RandomUtil.randomString(8)); 132 | response.setHeader("Location", location); 133 | response.setStatus(201); 134 | response.getWriter().write(sdp); 135 | } catch (IOException e) { 136 | throw new RuntimeException(e); 137 | } 138 | }; 139 | ZLM_API.mk_webrtc_get_answer_sdp(null, imkWebRtcGetAnwerSdpCallBack, "play", offerSdp, rtcUrl); 140 | } 141 | 142 | /** 143 | * 删除webrtc 144 | * 要显式终止会话,WHEP 播放器必须对初始HTTP POST的Location头字段中返回的资源URL执行HTTP DELETE请求。收到HTTP DELETE请求后,WHEP资源将被删除,并在媒体服务器上释放资源 145 | * 146 | * @param id 147 | * @param token 148 | * @throws IOException 149 | */ 150 | @DeleteMapping("/index/api/delete_webrtc") 151 | public void deleteWebrtc(String id, String token) throws IOException { 152 | //todo 校验token后删除资源 153 | response.setStatus(200); 154 | response.getWriter().write(""); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/main/resources/conf.ini: -------------------------------------------------------------------------------- 1 | ; auto-generated by mINI class { 2 | 3 | [general] 4 | check_nvidia_dev=1 5 | #是否启用虚拟主机 6 | enableVhost=0 7 | enable_ffmpeg_log=0 8 | #播放器或推流器在断开后会触发hook.on_flow_report事件(使用多少流量事件), 9 | #flowThreshold参数控制触发hook.on_flow_report事件阈值,使用流量超过该阈值后才触发,单位KB 10 | flowThreshold=1024 11 | #播放最多等待时间,单位毫秒 12 | #播放在播放某个流时,如果该流不存在, 13 | #ZLMediaKit会最多让播放器等待maxStreamWaitMS毫秒 14 | #如果在这个时间内,该流注册成功,那么会立即返回播放器播放成功 15 | #否则返回播放器未找到该流,该机制的目的是可以先播放再推流 16 | maxStreamWaitMS=15000 17 | #服务器唯一id,用于触发hook时区别是哪台服务器 18 | mediaServerId=your_server_id 19 | #合并写缓存大小(单位毫秒),合并写指服务器缓存一定的数据后才会一次性写入socket,这样能提高性能,但是会提高延时 20 | #开启后会同时关闭TCP_NODELAY并开启MSG_MORE 21 | mergeWriteMS=0 22 | #拉流代理时如果断流再重连成功是否删除前一次的媒体流数据,如果删除将重新开始, 23 | #如果不删除将会接着上一次的数据继续写(录制hls/mp4时会继续在前一个文件后面写) 24 | resetWhenRePlay=1 25 | #某个流无人观看时,触发hook.on_stream_none_reader事件的最大等待时间,单位毫秒 26 | #在配合hook.on_stream_none_reader事件时,可以做到无人观看自动停止拉流或停止接收推流 27 | streamNoneReaderDelayMS=20000 28 | #如果track未就绪,我们先缓存帧数据,但是有最大个数限制,防止内存溢出 29 | unready_frame_cache=100 30 | #最多等待未初始化的Track时间,单位毫秒,超时之后会忽略未初始化的Track 31 | wait_track_ready_ms=10000 32 | #如果流只有单Track,最多等待若干毫秒,超时后未收到其他Track的数据,则认为是单Track 33 | #如果协议元数据有声明特定track数,那么无此等待时间 34 | wait_add_track_ms=3000 35 | 36 | 37 | [hls] 38 | #是否广播 hls切片(ts/fmp4)完成通知(on_record_ts) 39 | broadcastRecordTs=0 40 | #直播hls文件删除延时,单位秒,issue: #913 41 | deleteDelaySec=10 42 | #hls写文件的buf大小,调整参数可以提高文件io性能 43 | fileBufSize=65536 44 | #hls最大切片时间 45 | segDur=2 46 | #是否保留hls文件,此功能部分等效于segNum=0的情况 47 | #不同的是这个保留不会在m3u8文件中体现 48 | #0为不保留,不起作用 49 | #1为保留,则不删除hls文件,如果开启此功能,注意磁盘大小,或者定期手动清理hls文件 50 | segKeep=0 51 | #m3u8索引中,hls保留切片个数(实际保留切片个数大2~3个) 52 | #如果设置为0,则不删除切片,而是保存为点播 53 | segNum=3 54 | #HLS切片从m3u8文件中移除后,继续保留在磁盘上的个数 55 | segRetain=5 56 | 57 | 58 | [http] 59 | #默认允许所有跨域请求 60 | allow_cross_domains=1 61 | #允许访问http api和http文件索引的ip地址范围白名单,置空情况下不做限制 62 | allow_ip_range=127.0.0.1,172.16.0.0-172.31.255.255,192.168.0.0-192.168.255.255,10.0.0.0-10.255.255.255 63 | #http服务器字符编码,windows上默认gb2312 64 | charSet=utf-8 65 | #是否显示文件夹菜单,开启后可以浏览文件夹 66 | dirMenu=1 67 | #禁止后缀的文件使用mmap缓存,使用“,”隔开 68 | #例如赋值为 .mp4,.flv 69 | #那么访问后缀为.mp4与.flv 的文件不缓存 70 | forbidCacheSuffix= 71 | #可以把http代理前真实客户端ip放在http头中:https://github.com/ZLMediaKit/ZLMediaKit/issues/1388 72 | #切勿暴露此key,否则可能导致伪造客户端ip 73 | forwarded_ip_header= 74 | #http链接超时时间 75 | keepAliveSecond=15 76 | #http请求体最大字节数,如果post的body太大,则不适合缓存body在内存 77 | maxReqSize=40960 78 | #404网页内容,用户可以自定义404网页 79 | notFound=404 Not Found

您访问的资源不存在!


ZLMediaKit(git hash:2628690/2023-11-05T13:26:42+08:00,branch:master,build time:2023-11-07T18:04:59)
80 | #可以为相对(相对于本可执行程序目录)或绝对路径 81 | rootPath=./www 82 | #http文件服务器读文件缓存大小,单位BYTE,调整该参数可以优化文件io性能 83 | sendBufSize=65536 84 | #访问其他http路径,对应的文件路径还是在rootPath内 85 | virtualPath= 86 | 87 | 88 | [multicast] 89 | #rtp组播截止组播ip地址 90 | addrMax=239.255.255.255 91 | #rtp组播起始组播ip地址 92 | addrMin=239.0.0.0 93 | #组播udp ttl 94 | udpTTL=64 95 | 96 | 97 | [protocol] 98 | #添加acc静音音频,在关闭音频时,此开关无效 99 | add_mute_audio=1 100 | #无人观看时,是否直接关闭(而不是通过on_none_reader hook返回close) 101 | #此配置置1时,此流如果无人观看,将不触发on_none_reader hook回调, 102 | #而是将直接关闭流 103 | auto_close=0 104 | #推流断开后可以在超时时间内重新连接上继续推流,这样播放器会接着播放。 105 | #置0关闭此特性(推流断开会导致立即断开播放器) 106 | #此参数不应大于播放器超时时间;单位毫秒 107 | continue_push_ms=15000 108 | #转协议是否开启音频 109 | enable_audio=1 110 | #是否开启转换为http-fmp4/ws-fmp4 111 | enable_fmp4=1 112 | #是否开启转换为hls(mpegts) 113 | enable_hls=1 114 | #是否开启转换为hls(fmp4) 115 | enable_hls_fmp4=0 116 | #是否开启MP4录制 117 | enable_mp4=0 118 | #是否开启转换为rtmp/flv 119 | enable_rtmp=1 120 | #是否开启转换为rtsp/webrtc 121 | enable_rtsp=1 122 | #是否开启转换为http-ts/ws-ts 123 | enable_ts=1 124 | #http[s]-fmp4、ws[s]-fmp4协议是否按需生成 125 | fmp4_demand=0 126 | ###### 以下是按需转协议的开关,在测试ZLMediaKit的接收推流性能时,请把下面开关置1 127 | ###### 如果某种协议你用不到,你可以把以下开关置1以便节省资源(但是还是可以播放,只是第一个播放者体验稍微差点), 128 | ###### 如果某种协议你想获取最好的用户体验,请置0(第一个播放者可以秒开,且不花屏) 129 | #hls协议是否按需生成,如果hls.segNum配置为0(意味着hls录制),那么hls将一直生成(不管此开关) 130 | hls_demand=0 131 | #hls录制保存路径 132 | hls_save_path=./www 133 | #转协议时,是否开启帧级时间戳覆盖 134 | # 0:采用源视频流绝对时间戳,不做任何改变 135 | # 1:采用zlmediakit接收数据时的系统时间戳(有平滑处理) 136 | # 2:采用源视频流时间戳相对时间戳(增长量),有做时间戳跳跃和回退矫正 137 | modify_stamp=2 138 | #是否将mp4录制当做观看者 139 | mp4_as_player=0 140 | #mp4切片大小,单位秒 141 | mp4_max_second=3600 142 | #mp4录制保存路径 143 | mp4_save_path=./www 144 | #rtmp[s]、http[s]-flv、ws[s]-flv协议是否按需生成 145 | rtmp_demand=0 146 | #rtsp[s]协议是否按需生成 147 | rtsp_demand=0 148 | #http[s]-ts协议是否按需生成 149 | ts_demand=0 150 | 151 | [record] 152 | #mp4录制或mp4点播的应用名,通过限制应用名,可以防止随意点播 153 | #点播的文件必须放置在此文件夹下 154 | appName=record 155 | #mp4录制完成后是否进行二次关键帧索引写入头部 156 | fastStart=0 157 | #mp4录制写文件缓存,单位BYTE,调整参数可以提高文件io性能 158 | fileBufSize=65536 159 | #MP4点播(rtsp/rtmp/http-flv/ws-flv)是否循环播放文件 160 | fileRepeat=0 161 | #减少该值可以让点播数据发送量更平滑,增大该值则更节省cpu资源 162 | sampleMS=500 163 | 164 | [rtmp] 165 | #rtmp必须在此时间内完成握手,否则服务器会断开链接,单位秒 166 | handshakeSecond=15 167 | #rtmp超时时间,如果该时间内未收到客户端的数据, 168 | #或者tcp发送缓存超过这个时间,则会断开连接,单位秒 169 | keepAliveSecond=15 170 | 171 | [rtp] 172 | #音频mtu大小,该参数限制rtp最大字节数,推荐不要超过1400 173 | #加大该值会明显增加直播延时 174 | audioMtuSize=600 175 | # H264 rtp打包模式是否采用stap-a模式(为了在老版本浏览器上兼容webrtc)还是采用Single NAL unit packet per H.264 模式 176 | # 有些老的rtsp设备不支持stap-a rtp,设置此配置为0可提高兼容性 177 | h264_stap_a=1 178 | # rtp 打包时,低延迟开关,默认关闭(为0),h264存在一帧多个slice(NAL)的情况,在这种情况下,如果开启可能会导致画面花屏 179 | lowLatency=0 180 | #rtp包最大长度限制,单位KB,主要用于识别TCP上下文破坏时,获取到错误的rtp 181 | rtpMaxSize=10 182 | #视频mtu大小,该参数限制rtp最大字节数,推荐不要超过1400 183 | videoMtuSize=1400 184 | 185 | [rtp_proxy] 186 | #导出调试数据(包括rtp/ps/h264)至该目录,置空则关闭数据导出 187 | dumpDir= 188 | #RtpSender相关功能是否提前开启gop缓存优化级联秒开体验,默认开启 189 | #如果不调用startSendRtp相关接口,可以置0节省内存 190 | #rtp h264 负载的pt 191 | h264_pt=98 192 | #rtp h265 负载的pt 193 | h265_pt=99 194 | #rtp ps 负载的pt 195 | ps_pt=96 196 | #rtp opus 负载的pt 197 | opus_pt=100 198 | #随机端口范围,最少确保36个端口 199 | #该范围同时限制rtsp服务器udp端口范围 200 | port_range=30000-35000 201 | #rtp超时时间,单位秒 202 | timeoutSec=15 203 | 204 | [rtsp] 205 | #rtsp专有鉴权方式是采用base64还是md5方式 206 | authBasic=0 207 | #rtsp拉流、推流代理是否是直接代理模式 208 | #直接代理后支持任意编码格式,但是会导致GOP缓存无法定位到I帧,可能会导致开播花屏 209 | #并且如果是tcp方式拉流,如果rtp大于mtu会导致无法使用udp方式代理 210 | #假定您的拉流源地址不是264或265或AAC,那么你可以使用直接代理的方式来支持rtsp代理 211 | #如果你是rtsp推拉流,但是webrtc播放,也建议关闭直接代理模式, 212 | #因为直接代理时,rtp中可能没有sps pps,会导致webrtc无法播放; 另外webrtc也不支持Single NAL Unit Packets类型rtp 213 | #默认开启rtsp直接代理,rtmp由于没有这些问题,是强制开启直接代理的 214 | directProxy=1 215 | #rtsp必须在此时间内完成握手,否则服务器会断开链接,单位秒 216 | handshakeSecond=15 217 | #或者tcp发送缓存超过这个时间,则会断开连接,单位秒 218 | keepAliveSecond=15 219 | #rtsp 转发是否使用低延迟模式,当开启时,不会缓存rtp包,来提高并发,可以降低一帧的延迟 220 | lowLatency=0 221 | #迫使客户端重新SETUP并切换到对应协议。目前支持FFMPEG和VLC 222 | rtpTransportType=-1 223 | 224 | [shell] 225 | #调试telnet服务器接受最大bufffer大小 226 | maxReqSize=1024 227 | 228 | [srt] 229 | #srt 协议中延迟缓存的估算参数,在握手阶段估算rtt ,然后latencyMul*rtt 为最大缓存时长,此参数越大,表示等待重传的时长就越大 230 | latencyMul=4 231 | #包缓存的大小 232 | pktBufSize=8192 233 | #srt udp服务器监听端口号,所有srt客户端将通过该端口传输srt数据, 234 | #该端口是多线程的,同时支持客户端网络切换导致的连接迁移 235 | port=9000 236 | #srt播放推流、播放超时时间,单位秒 237 | timeoutSec=5 238 | 239 | ; } --- 240 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.exception; 2 | 3 | 4 | import cn.hutool.core.util.StrUtil; 5 | import com.ldf.media.api.model.result.Result; 6 | import com.ldf.media.enums.ResultEnum; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.converter.HttpMessageNotReadableException; 10 | import org.springframework.validation.BindException; 11 | import org.springframework.validation.BindingResult; 12 | import org.springframework.validation.ObjectError; 13 | import org.springframework.web.HttpRequestMethodNotSupportedException; 14 | import org.springframework.web.bind.MethodArgumentNotValidException; 15 | import org.springframework.web.bind.MissingServletRequestParameterException; 16 | import org.springframework.web.bind.annotation.ExceptionHandler; 17 | import org.springframework.web.bind.annotation.ResponseStatus; 18 | import org.springframework.web.bind.annotation.RestControllerAdvice; 19 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 20 | 21 | import javax.servlet.http.HttpServletRequest; 22 | import javax.servlet.http.HttpServletResponse; 23 | import javax.validation.ConstraintViolation; 24 | import javax.validation.ConstraintViolationException; 25 | import java.util.List; 26 | 27 | /** 28 | * 全局异常处理 29 | * 30 | * @author lidaofu 31 | * @since 2023/11/29 32 | **/ 33 | @Slf4j 34 | @RestControllerAdvice 35 | public class GlobalExceptionHandler { 36 | /** 37 | * 默认全局异常处理 38 | */ 39 | @ResponseStatus(HttpStatus.OK) 40 | @ExceptionHandler(Exception.class) 41 | public Result handleException(Exception ex, HttpServletRequest request) { 42 | log.error("【业务】接口异常 接口接口地址:{} 错误信息:{}", request.getRequestURI(), ex.getMessage(), ex); 43 | return new Result<>(ResultEnum.EXCEPTION); 44 | } 45 | 46 | /** 47 | * 接口缺少参数 48 | * 49 | * @param ex 50 | * @param request 51 | * @return 52 | */ 53 | @ResponseStatus(HttpStatus.OK) 54 | @ExceptionHandler(value = MissingServletRequestParameterException.class) 55 | public Result handleMissingServletRequestParameterException(MissingServletRequestParameterException ex, HttpServletResponse response, HttpServletRequest request) { 56 | String parameterName = ex.getParameterName(); 57 | String parameterType = ex.getParameterType(); 58 | String format = StrUtil.format("请求的接口缺少参数类型为:{} 参数名为:{} 的参数", parameterType, parameterName); 59 | log.error("【业务】接口缺少参数异常 接口接口地址:{} 错误信息:{}", request.getRequestURI(), format); 60 | return new Result<>(ResultEnum.INVALID_ARGS.getCode(), format); 61 | } 62 | 63 | /** 64 | * 接口参数类型错误 65 | * 66 | * @param ex 67 | * @param request 68 | * @return 69 | */ 70 | @ResponseStatus(HttpStatus.OK) 71 | @ExceptionHandler(value = MethodArgumentTypeMismatchException.class) 72 | public Result handleMethodArgumentTypeMismatchExceptions(MethodArgumentTypeMismatchException ex, HttpServletRequest request) { 73 | String name = ex.getName(); 74 | String typename = ex.getRequiredType().getName(); 75 | Object value = ex.getValue(); 76 | String format = StrUtil.format("请求的接口需要的参数类型为:{} 参数名为:{} 的参数 错误值为 :{}", typename, name, value.toString()); 77 | log.error("【业务】接口参数类型不匹配异常 接口接口地址:{} 错误信息:{}", request.getRequestURI(), format); 78 | return new Result<>(ResultEnum.INVALID_ARGS.getCode(), format); 79 | } 80 | 81 | 82 | /** 83 | * Json参数异常 84 | * 85 | * @param ex 86 | * @return 87 | */ 88 | @ResponseStatus(HttpStatus.OK) 89 | @ExceptionHandler(value = HttpMessageNotReadableException.class) 90 | public Result handleHttpMessageNotReadableException(HttpMessageNotReadableException ex, HttpServletRequest request) { 91 | log.error("【业务】接口Json参数异常 接口接口地址:{} 错误信息:{}", request.getRequestURI(), ex.getMessage(), ex); 92 | return new Result<>(ResultEnum.INVALID_ARGS.getCode(), "JSON对象中的参数类型不正确"); 93 | } 94 | 95 | /** 96 | * 一般的参数绑定时候抛出的异常 97 | * 98 | * @param ex 99 | * @return 100 | */ 101 | @ExceptionHandler(value = BindException.class) 102 | @ResponseStatus(HttpStatus.OK) 103 | public Result handleBindException(BindException ex, HttpServletRequest request) { 104 | log.error("【业务】接口Json绑定异常 接口接口地址:{} 错误信息:{}", request.getRequestURI(), ex.getMessage(), ex); 105 | return new Result<>(ResultEnum.INVALID_ARGS.getCode(), "JSON对象中的参数数据不符合规范或绑定异常"); 106 | } 107 | 108 | /** 109 | * json参数绑定 110 | * 111 | * @param ex 112 | * @return 113 | */ 114 | @ExceptionHandler(value = MethodArgumentNotValidException.class) 115 | @ResponseStatus(HttpStatus.OK) 116 | public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request) { 117 | BindingResult bindingResult = ex.getBindingResult(); 118 | List allErrors = bindingResult.getAllErrors(); 119 | log.error("【业务】接口Json参数绑定异常 接口地址:{} 错误信息:{}", request.getRequestURI(), allErrors.get(0).getDefaultMessage(), ex); 120 | return new Result<>(ResultEnum.INVALID_ARGS.getCode(), allErrors.get(0).getDefaultMessage()); 121 | } 122 | 123 | /** 124 | * 接口参数校验异常 125 | * 126 | * @param ex 127 | * @return 128 | */ 129 | @ResponseStatus(HttpStatus.OK) 130 | @ExceptionHandler(value = ConstraintViolationException.class) 131 | public Result handleConstraintViolationException(ConstraintViolationException ex, HttpServletRequest request) { 132 | for (ConstraintViolation constraintViolation : ex.getConstraintViolations()) { 133 | //只提示第一个 134 | log.error("【业务】接口参数校验异常 接口地址:{} 错误信息:{}", request.getRequestURI(), constraintViolation.getMessage(), ex); 135 | return new Result<>(ResultEnum.ERROR.getCode(), constraintViolation.getMessage()); 136 | } 137 | return new Result<>(ResultEnum.INVALID_ARGS.getCode(), "参数为空或错误"); 138 | } 139 | 140 | 141 | /** 142 | * 接口访问方法不支持 143 | */ 144 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 145 | @ExceptionHandler(HttpRequestMethodNotSupportedException.class) 146 | public Result handleHttpRequestMethodNotSupportedException(HttpServletRequest request, HttpRequestMethodNotSupportedException ex) { 147 | String format = StrUtil.format("请求方法不支持! 当前方法请求方式为:{} 支持的请求方法为:{}", ex.getMethod(), ex.getSupportedMethods()[0], ex); 148 | log.error("【系统】接口方法错误异常 接口地址:{} 错误信息:{}", request.getRequestURI(), format); 149 | return new Result<>(ResultEnum.ERROR.getCode(), format); 150 | } 151 | 152 | 153 | /** 154 | * 全局断言异常 155 | */ 156 | @ResponseStatus(HttpStatus.OK) 157 | @ExceptionHandler(IllegalArgumentException.class) 158 | public Result handleIllegalArgumentException(IllegalArgumentException ex, HttpServletRequest request) { 159 | log.error("【业务】业务断言异常异常 接口地址:{} 错误信息:{}", request.getRequestURI(), ex.getMessage()); 160 | return new Result<>(ResultEnum.ERROR.getCode(), ex.getMessage()); 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/context/MediaServerContext.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.context; 2 | 3 | import com.aizuda.zlm4j.callback.*; 4 | import com.aizuda.zlm4j.core.ZLMApi; 5 | import com.aizuda.zlm4j.structure.MK_EVENTS; 6 | import com.aizuda.zlm4j.structure.MK_INI; 7 | import com.ldf.media.config.MediaServerConfig; 8 | import com.sun.jna.Native; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | 13 | import javax.annotation.PostConstruct; 14 | import javax.annotation.PreDestroy; 15 | 16 | /** 17 | * 流媒体上下文 18 | * 19 | * @author lidaofu 20 | * @since 2023/11/28 21 | **/ 22 | @Slf4j 23 | @Component 24 | public class MediaServerContext { 25 | @Autowired 26 | private MediaServerConfig config; 27 | @Autowired 28 | private IMKStreamChangeCallBack iMKStreamChangeCallBack; 29 | @Autowired 30 | private IMKNoReaderCallBack iMKNoReaderCallBack; 31 | @Autowired 32 | private IMKPublishCallBack iMKPublishCallBack; 33 | @Autowired 34 | private IMKNoFoundCallBack imKNoFoundCallBack; 35 | @Autowired 36 | private IMKFlowReportCallBack iMKFlowReportCallBack; 37 | @Autowired 38 | private IMKPlayCallBack iMKPlayCallBack; 39 | @Autowired 40 | private IMKRecordMp4CallBack iMKRecordMp4CallBack; 41 | @Autowired 42 | private IMKRecordTsCallBack iMKRecordTsCallBack; 43 | @Autowired 44 | private IMKLogCallBack iMKLogCallBack; 45 | public static ZLMApi ZLM_API = null; 46 | public static MK_EVENTS MK_EVENT = null; 47 | public static MK_INI MK_INI = null; 48 | 49 | @PostConstruct 50 | public void initMediaServer() { 51 | ZLM_API = Native.load("mk_api", ZLMApi.class); 52 | this.initMediaServerConf(); 53 | this.initEvents(); 54 | this.startMediaServer(); 55 | log.info("【MediaServer】初始化MediaServer程序成功"); 56 | } 57 | 58 | 59 | /** 60 | * 初始化服务器配置 61 | */ 62 | public void initMediaServerConf() { 63 | //初始化环境配置 64 | MK_INI = ZLM_API.mk_ini_default(); 65 | ZLM_API.mk_ini_set_option(MK_INI, "general.mediaServerId", "JMediaServer"); 66 | ZLM_API.mk_ini_set_option(MK_INI, "http.notFound", "

Media Server V1.0 By LiDaoFu

"); 67 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.auto_close", config.getAuto_close()); 68 | ZLM_API.mk_ini_set_option_int(MK_INI, "general.streamNoneReaderDelayMS", config.getStreamNoneReaderDelayMS()); 69 | ZLM_API.mk_ini_set_option_int(MK_INI, "general.maxStreamWaitMS", config.getMaxStreamWaitMS()); 70 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.enable_ts", config.getEnable_ts()); 71 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.enable_hls", config.getEnable_hls()); 72 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.enable_fmp4", config.getEnable_fmp4()); 73 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.enable_rtsp", config.getEnable_rtsp()); 74 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.enable_rtmp", config.getEnable_rtmp()); 75 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.enable_mp4", config.getEnable_mp4()); 76 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.enable_hls_fmp4", config.getEnable_hls_fmp4()); 77 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.enable_audio", config.getEnable_audio()); 78 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.mp4_as_player", config.getMp4_as_player()); 79 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.mp4_max_second", config.getMp4_max_second()); 80 | ZLM_API.mk_ini_set_option(MK_INI, "http.rootPath", config.getRootPath()); 81 | ZLM_API.mk_ini_set_option(MK_INI, "protocol.mp4_save_path", config.getMp4_save_path()); 82 | ZLM_API.mk_ini_set_option(MK_INI, "protocol.hls_save_path", config.getHls_save_path()); 83 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.hls_demand", config.getHls_demand()); 84 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.rtsp_demand", config.getRtsp_demand()); 85 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.rtmp_demand", config.getRtmp_demand()); 86 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.ts_demand", config.getTs_demand()); 87 | ZLM_API.mk_ini_set_option_int(MK_INI, "protocol.fmp4_demand", config.getFmp4_demand()); 88 | ZLM_API.mk_ini_set_option_int(MK_INI, "hls.broadcastRecordTs", config.getBroadcastRecordTs()); 89 | ZLM_API.mk_ini_set_option_int(MK_INI, "hls.segNum", config.getSegNum()); 90 | ZLM_API.mk_ini_set_option_int(MK_INI, "hls.segDur", config.getSegDur()); 91 | ZLM_API.mk_ini_set_option(MK_INI, "rtc.externIP", config.getRtc_host()); 92 | ZLM_API.mk_ini_set_option_int(MK_INI, "rtc.port", config.getRtc_port()); 93 | ZLM_API.mk_ini_set_option_int(MK_INI, "rtc.tcpPort", config.getRtc_port()); 94 | //初始化zmk服务器 95 | ZLM_API.mk_env_init1(config.getThread_num(), config.getLog_level(), config.getLog_mask(), config.getLog_path(), config.getLog_file_days(), 0, null, 0, null, null); 96 | } 97 | 98 | /** 99 | * 初始化事件回调 100 | */ 101 | public void initEvents() { 102 | //全局回调 103 | MK_EVENT = new MK_EVENTS(); 104 | MK_EVENT.on_mk_media_changed = iMKStreamChangeCallBack; 105 | MK_EVENT.on_mk_media_no_reader = iMKNoReaderCallBack; 106 | MK_EVENT.on_mk_media_publish = iMKPublishCallBack; 107 | MK_EVENT.on_mk_media_not_found = imKNoFoundCallBack; 108 | MK_EVENT.on_mk_flow_report = iMKFlowReportCallBack; 109 | MK_EVENT.on_mk_media_play = iMKPlayCallBack; 110 | /* MK_EVENT.on_mk_http_access = new MKHttpAccessCallBack(); 111 | MK_EVENT.on_mk_http_request = new MKHttpRequestCallBack(); 112 | MK_EVENT.on_mk_http_access = new MKHttpAccessCallBack(); 113 | MK_EVENT.on_mk_http_before_access = new MKHttpBeforeAccessCallBack();*/ 114 | MK_EVENT.on_mk_record_mp4 = iMKRecordMp4CallBack; 115 | MK_EVENT.on_mk_record_ts = iMKRecordTsCallBack; 116 | MK_EVENT.on_mk_log = iMKLogCallBack; 117 | //添加全局回调 118 | ZLM_API.mk_events_listen(MK_EVENT); 119 | } 120 | 121 | /** 122 | * 启动流媒体服务器 123 | */ 124 | public boolean startMediaServer() { 125 | boolean result = false; 126 | //创建http服务器 0:失败,非0:端口号 127 | short http_server_port = ZLM_API.mk_http_server_start(config.getHttp_port().shortValue(), 0); 128 | result=http_server_port == 0; 129 | log.info("【MediaServer】HTTP流媒体服务启动:{}", result ? "失败" : "成功,端口:" + http_server_port); 130 | //创建rtsp服务器 0:失败,非0:端口号 131 | short rtsp_server_port = ZLM_API.mk_rtsp_server_start(config.getRtsp_port().shortValue(), 0); 132 | result=rtsp_server_port == 0; 133 | log.info("【MediaServer】RTSP流媒体服务启动:{}", result ? "失败" : "成功,端口:" + rtsp_server_port); 134 | //创建rtmp服务器 0:失败,非0:端口号 135 | short rtmp_server_port = ZLM_API.mk_rtmp_server_start(config.getRtmp_port().shortValue(), 0); 136 | result=rtmp_server_port == 0; 137 | log.info("【MediaServer】RTMP流媒体服务启动:{}",result ? "失败" : "成功,端口:" + rtmp_server_port); 138 | //创建rtc服务器 0:失败,非0:端口号 139 | short rtc_server_port = ZLM_API.mk_rtc_server_start(config.getRtc_port().shortValue()); 140 | result=rtc_server_port == 0; 141 | log.info("【MediaServer】RTC流媒体服务启动:{}", result ? "失败" : "成功,端口:" + rtc_server_port); 142 | return !result; 143 | } 144 | 145 | /** 146 | * 关闭流媒体服务器 147 | */ 148 | public void stopMediaServer() { 149 | ZLM_API.mk_stop_all_server(); 150 | log.info("【MediaServer】关闭所有流媒体服务"); 151 | } 152 | 153 | /** 154 | * 释放资源 155 | */ 156 | @PreDestroy 157 | public void release() { 158 | this.stopMediaServer(); 159 | } 160 | 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/controller/ApiController.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.controller; 2 | 3 | import com.ldf.media.api.model.param.*; 4 | import com.ldf.media.api.model.result.*; 5 | import com.ldf.media.api.service.*; 6 | import io.swagger.annotations.Api; 7 | import io.swagger.annotations.ApiImplicitParam; 8 | import io.swagger.annotations.ApiOperation; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.validation.annotation.Validated; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import javax.validation.constraints.NotBlank; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * Api接口 21 | * 22 | * @author lidaofu 23 | * @since 2023/11/29 24 | **/ 25 | @Api(tags = "【API】流媒体Api") 26 | @RequestMapping("/index/api") 27 | @RestController 28 | @Validated 29 | @RequiredArgsConstructor 30 | public class ApiController { 31 | private final IApiService iApiService; 32 | private final ISnapService iSnapService; 33 | private final ITranscodeService iTranscodeService; 34 | private final IVideoStackService iVideoStackService; 35 | private final ITestVideoService iTestVideoService; 36 | 37 | @ApiOperation(value = "【拉流代理】添加拉流代理", notes = "已支持webrtc代理 url格式 webrtc://127.0.0.1:8899/live/test") 38 | @PostMapping(value = "/addStreamProxy") 39 | public Result addStreamProxy(@Validated @RequestBody StreamProxyParam param) { 40 | StreamUrlResult result = iApiService.addStreamProxy(param); 41 | return new Result<>(result); 42 | } 43 | 44 | @ApiOperation(value = "【拉流代理】关闭拉流代理", notes = "流注册成功后,也可以使用close_streams接口替代") 45 | @PostMapping(value = "/delStreamProxy") 46 | public Result delStreamProxy(String key) { 47 | Boolean flag = iApiService.delStreamProxy(key); 48 | return new Result<>(flag); 49 | } 50 | 51 | @ApiOperation(value = "【推流代理】添加推流代理",notes = "已支持webrtc代理 url格式 webrtc://127.0.0.1:8899/live/test") 52 | @PostMapping(value = "/addStreamPusherProxy") 53 | public Result addStreamPusherProxy(@Validated @RequestBody StreamPushProxyParam param) { 54 | String error = iApiService.addStreamPusherProxy(param); 55 | return new Result<>(error); 56 | } 57 | 58 | 59 | @ApiOperation(value = "【推流代理】删除推流代理") 60 | @PostMapping(value = "/delStreamPusherProxy") 61 | public Result delStreamPusherProxy(String key) { 62 | Boolean flag = iApiService.delStreamPusherProxy(key); 63 | return new Result<>(flag); 64 | } 65 | 66 | 67 | @ApiOperation(value = "【流操作】关闭流") 68 | @PostMapping(value = "/close_stream") 69 | public Result closeStream(@Validated @RequestBody CloseStreamParam param) { 70 | Integer status = iApiService.closeStream(param); 71 | return new Result<>(status); 72 | } 73 | 74 | @ApiOperation(value = "【流操作】关闭流(批量关)") 75 | @PostMapping(value = "/close_streams") 76 | public Result closeStreams(@Validated @RequestBody CloseStreamsParam param) { 77 | Integer status = iApiService.closeStreams(param); 78 | return new Result<>(status); 79 | } 80 | 81 | @ApiOperation(value = "【流操作】获取流列表") 82 | @GetMapping(value = "/getMediaList") 83 | public Result> getMediaList(GetMediaListParam param) { 84 | List list = iApiService.getMediaList(param); 85 | return new Result<>(list); 86 | } 87 | 88 | @ApiOperation(value = "【流操作】获取流信息") 89 | @GetMapping(value = "/getMediaInfo") 90 | public Result getMediaInfo(@Validated MediaQueryParam param) { 91 | MediaInfoResult info = iApiService.getMediaInfo(param); 92 | return new Result<>(info); 93 | } 94 | 95 | 96 | @ApiOperation(value = "【流操作】流是否在线") 97 | @GetMapping(value = "/isMediaOnline") 98 | public Result isMediaOnline(@Validated MediaQueryParam param) { 99 | Boolean online = iApiService.isMediaOnline(param); 100 | return new Result<>(online); 101 | } 102 | 103 | 104 | @ApiOperation(value = "【录像】开始录像") 105 | @PostMapping(value = "/startRecord") 106 | public Result startRecord(@Validated @RequestBody StartRecordParam param) { 107 | Boolean flag = iApiService.startRecord(param); 108 | return new Result<>(flag); 109 | } 110 | 111 | 112 | @ApiOperation(value = "【录像】停止录像") 113 | @PostMapping(value = "/stopRecord") 114 | public Result stopRecord(@Validated @RequestBody StopRecordParam param) { 115 | Boolean flag = iApiService.stopRecord(param); 116 | return new Result<>(flag); 117 | } 118 | 119 | @ApiOperation(value = "【录像】是否录像") 120 | @GetMapping(value = "/isRecording") 121 | public Result isRecording(@Validated RecordStatusParam param) { 122 | Boolean flag = iApiService.isRecording(param); 123 | return new Result<>(flag); 124 | } 125 | 126 | @ApiOperation(value = "【系统】获取内存资源信息") 127 | @GetMapping(value = "/getStatistic") 128 | public Result getStatistic() { 129 | Statistic statistic = iApiService.getStatistic(); 130 | return new Result<>(statistic); 131 | } 132 | 133 | 134 | @ApiOperation(value = "【系统】获取服务器配置") 135 | @GetMapping(value = "/getServerConfig") 136 | public Result getServerConfig() { 137 | String confStr = iApiService.getServerConfig(); 138 | return new Result<>(confStr); 139 | } 140 | 141 | @ApiOperation(value = "【系统】重启流媒体服务") 142 | @PostMapping(value = "/restartServer") 143 | public Result restartServer() { 144 | Boolean status = iApiService.restartServer(); 145 | return new Result<>(status); 146 | } 147 | 148 | @ApiOperation(value = "【系统】设置服务器配置") 149 | @PostMapping(value = "/setServerConfig") 150 | public Result setServerConfig(HttpServletRequest request) { 151 | Map parameterMap = request.getParameterMap(); 152 | Integer size = iApiService.setServerConfig(parameterMap); 153 | return new Result<>(size); 154 | } 155 | 156 | 157 | @ApiOperation(value = "【RTP服务】开启rtp服务") 158 | @PostMapping(value = "/openRtpServer") 159 | public Result openRtpServer(@Validated @RequestBody OpenRtpServerParam param) { 160 | Integer port = iApiService.openRtpServer(param); 161 | return new Result<>(port); 162 | } 163 | 164 | @ApiOperation(value = "【RTP服务】关闭rtp服务") 165 | @ApiImplicitParam(name = "stream", value = "流id", required = true) 166 | @PostMapping(value = "/closeRtpServer") 167 | public Result closeRtpServer(@NotBlank(message = "流id不为空") @RequestParam(value = "stream") String stream) { 168 | Integer status = iApiService.closeRtpServer(stream); 169 | return new Result<>(status); 170 | } 171 | 172 | @ApiOperation(value = "【RTP服务】获取所有RTP服务器") 173 | @GetMapping(value = "/listRtpServer") 174 | public Result> listRtpServer() { 175 | List results = iApiService.listRtpServer(); 176 | return new Result<>(results); 177 | } 178 | 179 | @ApiOperation(value = "【截图】获取截图",notes = "rtsp地址支持H264、H265,rtmp/http-flv只支持H264") 180 | @ApiImplicitParam(name = "url", value = "截图流地址", required = true) 181 | @GetMapping(value = "/getSnap") 182 | public void getSnap(String url, HttpServletResponse response) { 183 | iSnapService.getSnap(url, response); 184 | 185 | } 186 | 187 | @ApiOperation(value = "【转码】拉流代理转码(beta)", notes = "默认H265转H264 支持分辨率调整 暂时只支持视频转码") 188 | @PostMapping(value = "/transcode") 189 | public Result transcode(@Validated @RequestBody TranscodeParam param) { 190 | iTranscodeService.transcode(param); 191 | return new Result<>(); 192 | } 193 | 194 | @ApiOperation(value = "【拼接屏】开启拼接屏(beta)") 195 | @PostMapping(value = "/stack/start") 196 | public Result startStack(@RequestBody @Validated VideoStackParam param) { 197 | iVideoStackService.startStack(param); 198 | return new Result<>(); 199 | } 200 | 201 | @ApiOperation(value = "【拼接屏】重新设置拼接屏(beta)") 202 | @PostMapping(value = "/stack/reset") 203 | public Result resetStack(@RequestBody @Validated VideoStackParam param) { 204 | iVideoStackService.resetStack(param); 205 | return new Result<>(); 206 | } 207 | 208 | @ApiOperation(value = "【拼接屏】关闭拼接屏(beta)") 209 | @ApiImplicitParam(name = "id", value = "拼接屏任务id", required = true) 210 | @PostMapping(value = "/stack/stop") 211 | public Result stopStack(@NotBlank(message = "拼接屏任务id不为空") @RequestParam(value = "id") String id) { 212 | iVideoStackService.stopStack(id); 213 | return new Result<>(); 214 | } 215 | 216 | @ApiOperation(value = "【测试视频流】生成一路测试视频流", notes = "生成一路测试视频流") 217 | @PostMapping(value = "/createTestVideo") 218 | public Result createTestVideo(@Validated @RequestBody TestVideoParam param) { 219 | StreamUrlResult result = iTestVideoService.createTestVideo(param); 220 | return new Result<>(result); 221 | } 222 | 223 | @ApiOperation(value = "【测试视频流】停止一路测试视频流", notes = "停止一路测试视频流") 224 | @PostMapping(value = "/stopTestVideo") 225 | public Result stopTestVideo(@Validated @RequestBody CloseTestVideoParam param) { 226 | Boolean result = iTestVideoService.stopTestVideo(param.getApp(), param.getStream()); 227 | return new Result<>(result); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/main/resources/static/webrtc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ZLM RTC demo 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 15 | 16 | 19 |
20 | 21 |
22 | 23 |

24 | 25 | 26 |

27 | 28 |

29 | 30 | 31 |

32 |

33 | 34 | 35 |

36 | 37 | 38 |

39 | 40 | 41 |

42 | 43 |

44 | 45 | 46 |

47 | 48 |

49 | 50 | echo 51 | push 52 | play 53 |

54 |

55 | 56 | 58 |

59 |

60 | 61 | 62 |

63 | 64 | 65 | 66 |

67 | 68 | 69 |

70 |

71 | 72 | 73 |

74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 |
82 | 83 | 256 | 257 | 258 | 259 | 262 | 263 | 264 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/api/service/impl/SnapServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.api.service.impl; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import cn.hutool.core.io.IoUtil; 5 | import cn.hutool.core.util.RandomUtil; 6 | import com.ldf.media.api.service.ISnapService; 7 | import com.ldf.media.config.MediaServerConfig; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.bytedeco.ffmpeg.avcodec.AVCodec; 10 | import org.bytedeco.ffmpeg.avcodec.AVCodecContext; 11 | import org.bytedeco.ffmpeg.avcodec.AVPacket; 12 | import org.bytedeco.ffmpeg.avformat.AVFormatContext; 13 | import org.bytedeco.ffmpeg.avformat.AVIOContext; 14 | import org.bytedeco.ffmpeg.avformat.AVStream; 15 | import org.bytedeco.ffmpeg.avutil.AVDictionary; 16 | import org.bytedeco.ffmpeg.avutil.AVFrame; 17 | import org.bytedeco.ffmpeg.global.avcodec; 18 | import org.bytedeco.ffmpeg.global.avformat; 19 | import org.bytedeco.ffmpeg.global.avutil; 20 | import org.bytedeco.javacpp.Pointer; 21 | import org.bytedeco.javacpp.PointerPointer; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.stereotype.Service; 24 | 25 | import javax.servlet.ServletOutputStream; 26 | import javax.servlet.http.HttpServletResponse; 27 | import java.io.File; 28 | import java.io.IOException; 29 | import java.io.InputStream; 30 | import java.nio.charset.StandardCharsets; 31 | import java.nio.file.Files; 32 | 33 | import static org.bytedeco.ffmpeg.global.avutil.*; 34 | import static org.bytedeco.ffmpeg.presets.avutil.AVERROR_EAGAIN; 35 | 36 | @Slf4j 37 | @Service 38 | public class SnapServiceImpl implements ISnapService { 39 | @Autowired 40 | private MediaServerConfig mediaServerConfig; 41 | 42 | /** 43 | * 获取截图 44 | * 45 | * @param url 46 | * @param response 47 | */ 48 | @Override 49 | public void getSnap(String url, HttpServletResponse response) { 50 | int ret = 0; 51 | int videoIndex = -1; 52 | AVFormatContext oFmtCtx = null; 53 | AVFormatContext iFmtCtx = null; 54 | AVCodecContext deCodecCtx = null; 55 | AVCodecContext enCodecCtx = null; 56 | AVPacket srcPacket = null; 57 | AVFrame frame = null; 58 | AVPacket packet = null; 59 | AVStream vStream = null; 60 | String imgPath = null; 61 | File snapPath = new File(mediaServerConfig.getRootPath() + "/snap"); 62 | if (!snapPath.exists()) { 63 | snapPath.mkdirs(); 64 | } 65 | imgPath = snapPath.getAbsolutePath() + "/" + RandomUtil.randomString(10) + ".jpg"; 66 | iFmtCtx = new AVFormatContext(null); 67 | boolean isRtsp = url.startsWith("rtsp"); 68 | if (isRtsp) { 69 | AVDictionary rtspOptions = new AVDictionary(null); 70 | avutil.av_dict_set(rtspOptions, "rtsp_transport", "tcp", 0); 71 | ret = avformat.avformat_open_input(iFmtCtx, url, null, rtspOptions); 72 | avutil.av_dict_free(rtspOptions); 73 | } else { 74 | ret = avformat.avformat_open_input(iFmtCtx, url, null, null); 75 | } 76 | if (ret < 0) { 77 | avutil.av_log(iFmtCtx, AV_LOG_ERROR, "avformat_open_input error \n"); 78 | writeError(response); 79 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 80 | return; 81 | } 82 | ret = avformat.avformat_find_stream_info(iFmtCtx, (PointerPointer) null); 83 | if (ret < 0) { 84 | avutil.av_log(iFmtCtx, AV_LOG_ERROR, "avformat_find_stream_info error \n"); 85 | writeError(response); 86 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 87 | return; 88 | } 89 | videoIndex = avformat.av_find_best_stream(iFmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, (PointerPointer) null, 0); 90 | if (videoIndex < 0) { 91 | avutil.av_log(iFmtCtx, AV_LOG_ERROR, "av_find_best_stream error \n"); 92 | writeError(response); 93 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 94 | return; 95 | } 96 | vStream = iFmtCtx.streams(videoIndex); 97 | srcPacket = avcodec.av_packet_alloc(); 98 | while ((ret = avformat.av_read_frame(iFmtCtx, srcPacket)) == 0) { 99 | if (srcPacket.stream_index() == videoIndex) { 100 | if (srcPacket.flags() == avcodec.AV_PKT_FLAG_KEY) { 101 | break; 102 | } 103 | } 104 | avcodec.av_packet_unref(srcPacket); 105 | } 106 | if (ret < 0) { 107 | avutil.av_log(iFmtCtx, AV_LOG_ERROR, "av_read_frame error \n"); 108 | writeError(response); 109 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 110 | return; 111 | } 112 | AVCodec deCodec = avcodec.avcodec_find_decoder(vStream.codecpar().codec_id()); 113 | deCodecCtx = avcodec.avcodec_alloc_context3(deCodec); 114 | AVCodec enCodec = avcodec.avcodec_find_encoder(avcodec.AV_CODEC_ID_MJPEG); 115 | enCodecCtx = avcodec.avcodec_alloc_context3(enCodec); 116 | ret = avcodec.avcodec_parameters_to_context(deCodecCtx, vStream.codecpar()); 117 | if (ret < 0) { 118 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "avcodec_parameters_to_context error \n"); 119 | writeError(response); 120 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 121 | return; 122 | } 123 | ret = avcodec.avcodec_open2(deCodecCtx, deCodec, (PointerPointer) null); 124 | if (ret < 0) { 125 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "avcodec_open2 error \n"); 126 | writeError(response); 127 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 128 | return; 129 | } 130 | oFmtCtx = new AVFormatContext(null); 131 | ret = avformat.avformat_alloc_output_context2(oFmtCtx, null, "mjpeg", imgPath); 132 | if (ret < 0) { 133 | return; 134 | } 135 | AVStream oStream = avformat.avformat_new_stream(oFmtCtx, null); 136 | enCodecCtx.width(deCodecCtx.width()); 137 | enCodecCtx.height(deCodecCtx.height()); 138 | enCodecCtx.time_base(avutil.av_make_q(1, 1)); 139 | enCodecCtx.pix_fmt(AV_PIX_FMT_YUVJ420P); 140 | avcodec.avcodec_parameters_from_context(oStream.codecpar(), enCodecCtx); 141 | ret = avcodec.avcodec_open2(enCodecCtx, enCodec, (PointerPointer) null); 142 | if (ret < 0) { 143 | avutil.av_log(enCodecCtx, AV_LOG_ERROR, "avcodec_open2 error \n"); 144 | writeError(response); 145 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 146 | return; 147 | } 148 | frame = avutil.av_frame_alloc(); 149 | frame.width(enCodecCtx.width()); 150 | frame.height(enCodecCtx.height()); 151 | frame.format(enCodecCtx.pix_fmt()); 152 | ret = av_frame_get_buffer(frame, 0); 153 | if (ret < 0) { 154 | avutil.av_log(enCodecCtx, AV_LOG_ERROR, "av_frame_get_buffer error \n"); 155 | writeError(response); 156 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 157 | return; 158 | } 159 | if (avutil.av_frame_is_writable(frame) != 1) { 160 | avutil.av_frame_make_writable(frame); 161 | } 162 | packet = avcodec.av_packet_alloc(); 163 | packet.stream_index(oStream.index()); 164 | avcodec.avcodec_send_packet(deCodecCtx, srcPacket); 165 | avcodec.avcodec_send_packet(deCodecCtx, null); 166 | ret = avcodec.avcodec_receive_frame(deCodecCtx, frame); 167 | if (ret < 0 && ret != AVERROR_EOF() && ret != AVERROR_EAGAIN()) { 168 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "avcodec_receive_frame error \n"); 169 | writeError(response); 170 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 171 | return; 172 | } 173 | avcodec.avcodec_send_frame(enCodecCtx, frame); 174 | avcodec.avcodec_send_frame(enCodecCtx, null); 175 | ret = avcodec.avcodec_receive_packet(enCodecCtx, packet); 176 | if (ret < 0 && ret != AVERROR_EOF() && ret != AVERROR_EAGAIN()) { 177 | avutil.av_log(enCodecCtx, AV_LOG_ERROR, "avcodec_receive_packet error \n"); 178 | writeError(response); 179 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 180 | return; 181 | } 182 | AVIOContext pb = new AVIOContext((Pointer) null); 183 | ret = avformat.avio_open(pb, imgPath, avformat.AVIO_FLAG_WRITE); 184 | if (ret < 0) { 185 | avutil.av_log(oFmtCtx, AV_LOG_ERROR, "avio_open error \n"); 186 | writeError(response); 187 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 188 | return; 189 | } 190 | oFmtCtx.pb(pb); 191 | ret = avformat.avformat_write_header(oFmtCtx, (PointerPointer) null); 192 | if (ret < 0) { 193 | avutil.av_log(oFmtCtx, AV_LOG_ERROR, "avformat_write_header error \n"); 194 | writeError(response); 195 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 196 | return; 197 | } 198 | ret = avformat.av_write_frame(oFmtCtx, packet); 199 | if (ret < 0) { 200 | avutil.av_log(oFmtCtx, AV_LOG_ERROR, "av_write_frame error \n"); 201 | writeError(response); 202 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 203 | return; 204 | } 205 | avcodec.av_packet_unref(packet); 206 | avcodec.av_packet_unref(srcPacket); 207 | avformat.av_write_trailer(oFmtCtx); 208 | avformat.avio_close(pb); 209 | free(url, deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, frame, srcPacket, packet); 210 | writeImg(imgPath, response); 211 | } 212 | 213 | /** 214 | * 写图片 215 | * 216 | * @param imgPath 217 | * @param response 218 | */ 219 | private void writeImg(String imgPath, HttpServletResponse response) { 220 | File file = new File(imgPath); 221 | if (!file.exists()) { 222 | writeError(response); 223 | } 224 | Long fileLength = file.length(); 225 | try (InputStream ins = Files.newInputStream(file.toPath())) { 226 | response.setHeader("Access-Control-Allow-Origin", "*"); 227 | response.setHeader("Access-Control-Allow-Methods", "OPTIONS,GET, POST, PUT, DELETE"); 228 | response.setHeader("Content-Disposition", "inline;filename=" + imgPath.substring(imgPath.lastIndexOf("/") + 1, imgPath.length())); 229 | response.setHeader("Content-Length", String.valueOf(fileLength)); 230 | response.setContentType("image/jpg"); 231 | IoUtil.copy(ins, response.getOutputStream()); 232 | } catch (Exception e) { 233 | log.error("【Response】写入图片失败,原因:{}", e.getMessage()); 234 | writeError(response); 235 | } finally { 236 | //主动删除录像 237 | FileUtil.del(imgPath); 238 | } 239 | } 240 | 241 | /** 242 | * 写错误 243 | * 244 | * @param response 245 | */ 246 | private void writeError(HttpServletResponse response) { 247 | try { 248 | ServletOutputStream outputStream = response.getOutputStream(); 249 | outputStream.write("截图失败".getBytes(StandardCharsets.UTF_8)); 250 | } catch (IOException e) { 251 | } 252 | } 253 | 254 | 255 | /** 256 | * 释放资源 257 | * 258 | * @param deCodecCtx 259 | * @param enCodecCtx 260 | * @param iFmtCtx 261 | * @param oFmtCtx 262 | * @param frame 263 | * @param srcPacket 264 | * @param packet 265 | */ 266 | private void free(String url, AVCodecContext deCodecCtx, AVCodecContext enCodecCtx, AVFormatContext iFmtCtx, AVFormatContext oFmtCtx, AVFrame frame, AVPacket srcPacket, AVPacket packet) { 267 | log.info("【截图】释放流地址:{} 资源", url); 268 | if (deCodecCtx != null) { 269 | avcodec.avcodec_free_context(deCodecCtx); 270 | } 271 | if (enCodecCtx != null) { 272 | avcodec.avcodec_free_context(enCodecCtx); 273 | } 274 | if (iFmtCtx != null) { 275 | avformat.avformat_close_input(iFmtCtx); 276 | } 277 | if (oFmtCtx != null) { 278 | avformat.avformat_free_context(oFmtCtx); 279 | } 280 | if (frame != null) { 281 | avutil.av_frame_free(frame); 282 | } 283 | if (srcPacket != null) { 284 | avcodec.av_packet_free(srcPacket); 285 | } 286 | if (packet != null) { 287 | avcodec.av_packet_free(packet); 288 | } 289 | } 290 | 291 | 292 | } 293 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/module/stack/VideoStackWindow.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.module.stack; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.ldf.media.api.model.param.VideoStackWindowParam; 5 | import com.ldf.media.pool.MediaServerThreadPool; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.bytedeco.ffmpeg.avcodec.AVCodec; 8 | import org.bytedeco.ffmpeg.avcodec.AVCodecContext; 9 | import org.bytedeco.ffmpeg.avcodec.AVPacket; 10 | import org.bytedeco.ffmpeg.avformat.AVFormatContext; 11 | import org.bytedeco.ffmpeg.avformat.AVStream; 12 | import org.bytedeco.ffmpeg.avutil.AVBufferRef; 13 | import org.bytedeco.ffmpeg.avutil.AVDictionary; 14 | import org.bytedeco.ffmpeg.avutil.AVFrame; 15 | import org.bytedeco.ffmpeg.global.avcodec; 16 | import org.bytedeco.ffmpeg.global.avformat; 17 | import org.bytedeco.ffmpeg.global.avutil; 18 | import org.bytedeco.ffmpeg.global.swscale; 19 | import org.bytedeco.ffmpeg.swscale.SwsContext; 20 | import org.bytedeco.javacpp.*; 21 | 22 | import static com.ldf.media.module.stack.VideoStack.getImageBGRData; 23 | import static org.bytedeco.ffmpeg.global.avutil.AVERROR_EOF; 24 | import static org.bytedeco.ffmpeg.global.avutil.AV_LOG_ERROR; 25 | import static org.bytedeco.ffmpeg.presets.avutil.AVERROR_EAGAIN; 26 | 27 | @Slf4j 28 | public class VideoStackWindow { 29 | 30 | private VideoStackWindowParam param; 31 | 32 | private String fillImgUrl; 33 | 34 | private Integer width; 35 | 36 | private Integer height; 37 | 38 | private Integer yPos; 39 | 40 | private Integer vIndex; 41 | 42 | private Integer lineWidth; 43 | 44 | private PointerPointer dataPointer; 45 | 46 | private IntPointer linePointer; 47 | 48 | private AVFormatContext iFmtCtx = null; 49 | 50 | private AVBufferRef hwDeviceCtx = null; 51 | 52 | private AVStream avStream = null; 53 | 54 | private AVFrame avFrame = null; 55 | 56 | private AVFrame localFrame = null; 57 | 58 | private AVFrame rgbFrame = null; 59 | 60 | private AVPacket avPacket = null; 61 | 62 | private AVCodecContext deCodecCtx = null; 63 | 64 | private SwsContext avSwsCtx = null; 65 | 66 | private SwsContext imgSwsCtx = null; 67 | 68 | private Boolean isStop = false; 69 | 70 | 71 | public VideoStackWindow(VideoStackWindowParam param, String fillImgUrl, int width, int height, int yPos, PointerPointer dataPointer, IntPointer linePointer) { 72 | this.param = param; 73 | this.fillImgUrl = fillImgUrl; 74 | this.width = width; 75 | this.height = height; 76 | this.yPos = yPos; 77 | this.dataPointer = dataPointer; 78 | this.linePointer = linePointer; 79 | } 80 | 81 | 82 | public void init() { 83 | if (StrUtil.isNotBlank(param.getVideoUrl())) { 84 | MediaServerThreadPool.execute(() -> { 85 | initFillVideo(); 86 | }); 87 | } else if (StrUtil.isNotBlank(param.getImgUrl())) { 88 | initFillImg(param.getImgUrl()); 89 | } else if (StrUtil.isNotBlank(param.getFillColor())) { 90 | initFillColor(param.getFillColor()); 91 | } else if (StrUtil.isNotBlank(fillImgUrl)) { 92 | initFillImg(fillImgUrl); 93 | } 94 | } 95 | 96 | /** 97 | * 填充颜色 98 | * 99 | * @param fillColor 100 | */ 101 | private void initFillColor(String fillColor) { 102 | int[] bgr = VideoStack.convertRGBHex(fillColor); 103 | BytePointer bytePointer = new BytePointer(dataPointer.get(0).getPointer(yPos)); 104 | long yDiff = linePointer.get(0); 105 | long yImgPos = 0L; 106 | for (int y = 0; y < height; y++) { 107 | for (int x = 0; x < width; x++) { 108 | long xImgPos = x * 3L; 109 | bytePointer.put(yImgPos + xImgPos, (byte) bgr[0]); 110 | bytePointer.put(yImgPos + xImgPos + 1, (byte) bgr[1]); 111 | bytePointer.put(yImgPos + xImgPos + 2, (byte) bgr[2]); 112 | } 113 | yImgPos = yImgPos + yDiff; 114 | } 115 | } 116 | 117 | 118 | /** 119 | * 填充视频 120 | */ 121 | private void initFillVideo() { 122 | int ret = 0; 123 | System.out.println(avutil.avutil_configuration()); 124 | iFmtCtx = new AVFormatContext(null); 125 | boolean isRtsp = param.getVideoUrl().startsWith("rtsp"); 126 | if (isRtsp) { 127 | AVDictionary rtspOptions = new AVDictionary(null); 128 | avutil.av_dict_set(rtspOptions, "rtsp_transport", "tcp", 0); 129 | ret = avformat.avformat_open_input(iFmtCtx, param.getVideoUrl(), null, rtspOptions); 130 | avutil.av_dict_free(rtspOptions); 131 | } else { 132 | ret = avformat.avformat_open_input(iFmtCtx, param.getVideoUrl(), null, null); 133 | } 134 | if (ret < 0) { 135 | avutil.av_log(iFmtCtx, AV_LOG_ERROR, "avformat_open_input error \n"); 136 | free(); 137 | return; 138 | } 139 | ret = avformat.avformat_find_stream_info(iFmtCtx, (PointerPointer) null); 140 | if (ret < 0) { 141 | avutil.av_log(iFmtCtx, AV_LOG_ERROR, "avformat_find_stream_info error \n"); 142 | free(); 143 | return; 144 | } 145 | vIndex = avformat.av_find_best_stream(iFmtCtx, avutil.AVMEDIA_TYPE_VIDEO, -1, -1, (PointerPointer) null, 0); 146 | if (vIndex < 0) { 147 | avutil.av_log(iFmtCtx, AV_LOG_ERROR, "av_find_best_stream error \n"); 148 | free(); 149 | return; 150 | } 151 | avStream = iFmtCtx.streams(vIndex); 152 | AVCodec deCodec = null; 153 | deCodec = avcodec.avcodec_find_decoder(avStream.codecpar().codec_id()); 154 | deCodecCtx = avcodec.avcodec_alloc_context3(deCodec); 155 | if (deCodecCtx == null) { 156 | avutil.av_log(iFmtCtx, AV_LOG_ERROR, "avcodec_alloc_context3 error \n"); 157 | free(); 158 | return; 159 | } 160 | ret = avcodec.avcodec_parameters_to_context(deCodecCtx, avStream.codecpar()); 161 | if (vIndex < 0) { 162 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "avcodec_parameters_to_context error \n"); 163 | free(); 164 | return; 165 | } 166 | if (hwDeviceCtx != null) { 167 | deCodecCtx.hw_device_ctx(avutil.av_buffer_ref(hwDeviceCtx)); 168 | } 169 | ret = avcodec.avcodec_open2(deCodecCtx, deCodec, (PointerPointer) null); 170 | if (ret < 0) { 171 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "avcodec_open2 error \n"); 172 | free(); 173 | return; 174 | } 175 | avPacket = avcodec.av_packet_alloc(); 176 | avFrame = avutil.av_frame_alloc(); 177 | if (hwDeviceCtx == null) { 178 | avFrame.width(deCodecCtx.width()); 179 | avFrame.height(deCodecCtx.height()); 180 | avFrame.format(deCodecCtx.pix_fmt()); 181 | } else { 182 | localFrame = avutil.av_frame_alloc(); 183 | } 184 | avutil.av_frame_get_buffer(avFrame, 1); 185 | rgbFrame = avutil.av_frame_alloc(); 186 | rgbFrame.width(width); 187 | rgbFrame.height(height); 188 | rgbFrame.format(avutil.AV_PIX_FMT_BGR24); 189 | avutil.av_frame_get_buffer(rgbFrame, 1); 190 | avSwsCtx = swscale.sws_getContext(avFrame.width(), avFrame.height(), avFrame.format(), width, height, avutil.AV_PIX_FMT_BGR24, swscale.SWS_BICUBIC, null, null, (DoublePointer) null); 191 | if (avSwsCtx == null) { 192 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "sws_getContext error \n"); 193 | free(); 194 | return; 195 | } 196 | while (!isStop && (ret = avformat.av_read_frame(iFmtCtx, avPacket)) == 0) { 197 | if (avPacket.stream_index() == vIndex) { 198 | ret = avcodec.avcodec_send_packet(deCodecCtx, avPacket); 199 | if (ret < 0) { 200 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "avcodec_send_packet error \n"); 201 | free(); 202 | return; 203 | } 204 | while (true) { 205 | if (avutil.av_frame_is_writable(avFrame) != 1) { 206 | avutil.av_frame_make_writable(avFrame); 207 | } 208 | ret = avcodec.avcodec_receive_frame(deCodecCtx, avFrame); 209 | if (ret == AVERROR_EAGAIN() || ret == AVERROR_EOF()) { 210 | break; 211 | } 212 | if (ret < 0) { 213 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "avcodec_receive_frame error \n"); 214 | free(); 215 | return; 216 | } 217 | if (avutil.av_frame_is_writable(rgbFrame) != 1) { 218 | avutil.av_frame_make_writable(rgbFrame); 219 | } 220 | swscale.sws_scale(avSwsCtx, avFrame.data(), avFrame.linesize(), 0, avFrame.height(), rgbFrame.data(), rgbFrame.linesize()); 221 | int yRgbPos = 0; 222 | int destPos = yPos; 223 | int diffW = linePointer.get(0); 224 | int destW = rgbFrame.linesize().get(0); 225 | for (int i = 0; i < rgbFrame.height(); i++) { 226 | Pointer.memcpy(dataPointer.get(0).getPointer(destPos), rgbFrame.data().get(0).getPointer(yRgbPos), rgbFrame.width() * 3L); 227 | yRgbPos = yRgbPos + destW; 228 | destPos = destPos + diffW; 229 | } 230 | avutil.av_frame_unref(avFrame); 231 | } 232 | } 233 | avcodec.av_packet_unref(avPacket); 234 | } 235 | 236 | free(); 237 | } 238 | 239 | /** 240 | * 填充图像 241 | */ 242 | private void initFillImg(String imgUrl) { 243 | VideoStack.ImageData imageData = getImageBGRData(imgUrl); 244 | if (imageData != null) { 245 | imgSwsCtx = swscale.sws_getContext(imageData.width, imageData.height, avutil.AV_PIX_FMT_BGR24, width, height, avutil.AV_PIX_FMT_BGR24, swscale.SWS_BICUBIC, null, null, (DoublePointer) null); 246 | if (imgSwsCtx != null) { 247 | PointerPointer srcDataPointerPointer = new PointerPointer<>(4); 248 | IntPointer srcLineSize = new IntPointer(4); 249 | BytePointer rgbImgPointer = new BytePointer(imageData.bgrData); 250 | avutil.av_image_fill_arrays(srcDataPointerPointer, srcLineSize, rgbImgPointer, avutil.AV_PIX_FMT_BGR24, imageData.width, imageData.height, 1); 251 | int destDataSize = avutil.av_image_get_buffer_size(avutil.AV_PIX_FMT_BGR24, width, height, 1); 252 | Pointer destPointer = avutil.av_mallocz(destDataSize); 253 | PointerPointer destDataPointerPointer = new PointerPointer<>(4); 254 | IntPointer destLineSize = new IntPointer(4); 255 | avutil.av_image_fill_arrays(destDataPointerPointer, destLineSize, new BytePointer(destPointer), avutil.AV_PIX_FMT_BGR24, width, height, 1); 256 | swscale.sws_scale(imgSwsCtx, srcDataPointerPointer, srcLineSize, 0, imageData.height, destDataPointerPointer, destLineSize); 257 | int yImgPos = 0; 258 | int diffW = linePointer.get(0); 259 | int destW = destLineSize.get(0); 260 | int destPos = yPos; 261 | for (int i = 0; i < height; i++) { 262 | Pointer.memcpy(dataPointer.get(0).getPointer(destPos), destPointer.getPointer(yImgPos), width * 3); 263 | yImgPos = yImgPos + destW; 264 | destPos = destPos + diffW; 265 | } 266 | swscale.sws_freeContext(imgSwsCtx); 267 | avutil.av_free(destPointer); 268 | imgSwsCtx = null; 269 | } 270 | } 271 | } 272 | 273 | 274 | /** 275 | * 停止 276 | */ 277 | public void stop() { 278 | isStop = true; 279 | } 280 | 281 | 282 | /** 283 | * 释放资源 284 | */ 285 | private void free() { 286 | if (StrUtil.isNotBlank(param.getVideoUrl())) { 287 | log.info("【拼接屏窗口】 释放区域:{} 资源", param.getVideoUrl()); 288 | } 289 | if (iFmtCtx != null) { 290 | avformat.avformat_close_input(iFmtCtx); 291 | iFmtCtx = null; 292 | } 293 | if (deCodecCtx != null) { 294 | avcodec.avcodec_free_context(deCodecCtx); 295 | deCodecCtx = null; 296 | } 297 | if (avFrame != null) { 298 | avutil.av_frame_free(avFrame); 299 | avFrame = null; 300 | } 301 | if (localFrame != null) { 302 | avutil.av_frame_free(localFrame); 303 | localFrame = null; 304 | } 305 | if (hwDeviceCtx != null) { 306 | avutil.av_buffer_unref(hwDeviceCtx); 307 | } 308 | if (rgbFrame != null) { 309 | avutil.av_frame_free(rgbFrame); 310 | rgbFrame = null; 311 | } 312 | if (avPacket != null) { 313 | avcodec.av_packet_free(avPacket); 314 | avPacket = null; 315 | } 316 | if (avSwsCtx != null) { 317 | swscale.sws_freeContext(avSwsCtx); 318 | avSwsCtx = null; 319 | } 320 | } 321 | 322 | } 323 | -------------------------------------------------------------------------------- /src/main/java/com/ldf/media/module/transcode/Transcode.java: -------------------------------------------------------------------------------- 1 | package com.ldf.media.module.transcode; 2 | 3 | import com.ldf.media.api.model.param.TranscodeParam; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.bytedeco.ffmpeg.avcodec.AVCodec; 6 | import org.bytedeco.ffmpeg.avcodec.AVCodecContext; 7 | import org.bytedeco.ffmpeg.avcodec.AVPacket; 8 | import org.bytedeco.ffmpeg.avformat.AVFormatContext; 9 | import org.bytedeco.ffmpeg.avformat.AVIOContext; 10 | import org.bytedeco.ffmpeg.avformat.AVStream; 11 | import org.bytedeco.ffmpeg.avutil.AVDictionary; 12 | import org.bytedeco.ffmpeg.avutil.AVFrame; 13 | import org.bytedeco.ffmpeg.global.avcodec; 14 | import org.bytedeco.ffmpeg.global.avformat; 15 | import org.bytedeco.ffmpeg.global.avutil; 16 | import org.bytedeco.ffmpeg.global.swscale; 17 | import org.bytedeco.ffmpeg.swscale.SwsContext; 18 | import org.bytedeco.javacpp.DoublePointer; 19 | import org.bytedeco.javacpp.Pointer; 20 | import org.bytedeco.javacpp.PointerPointer; 21 | 22 | import static org.bytedeco.ffmpeg.global.avformat.AVFMT_NOFILE; 23 | import static org.bytedeco.ffmpeg.global.avutil.*; 24 | import static org.bytedeco.ffmpeg.presets.avutil.AVERROR_EAGAIN; 25 | 26 | @Slf4j 27 | public class Transcode { 28 | private TranscodeParam param; 29 | 30 | private String pushUrl; 31 | 32 | private Boolean isStop = false; 33 | 34 | public Transcode(TranscodeParam param, String pushUrl) { 35 | this.param = param; 36 | this.pushUrl = pushUrl; 37 | } 38 | 39 | /** 40 | * 开始转码 41 | */ 42 | public void start() { 43 | int ret = 0; 44 | int videoIndex = -1; 45 | int audioIndex = -1; 46 | AVFormatContext oFmtCtx = null; 47 | AVFormatContext iFmtCtx = null; 48 | AVCodecContext deCodecCtx = null; 49 | AVCodecContext enCodecCtx = null; 50 | SwsContext swsCtx = null; 51 | AVPacket srcPacket = null; 52 | AVFrame frame = null; 53 | AVFrame swsFrame = null; 54 | AVPacket packet = null; 55 | AVStream vStream = null; 56 | AVStream aStream = null; 57 | AVStream oVStream = null; 58 | AVStream oAStream = null; 59 | boolean needScale = false; 60 | iFmtCtx = new AVFormatContext(null); 61 | boolean isRtsp = param.getUrl().startsWith("rtsp"); 62 | if (isRtsp) { 63 | AVDictionary rtspOptions = new AVDictionary(null); 64 | avutil.av_dict_set(rtspOptions, "rtsp_transport", "tcp", 0); 65 | ret = avformat.avformat_open_input(iFmtCtx, param.getUrl(), null, rtspOptions); 66 | avutil.av_dict_free(rtspOptions); 67 | } else { 68 | ret = avformat.avformat_open_input(iFmtCtx, param.getUrl(), null, null); 69 | } 70 | if (ret < 0) { 71 | avutil.av_log(iFmtCtx, AV_LOG_ERROR, "avformat_open_input error \n"); 72 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 73 | return; 74 | } 75 | ret = avformat.avformat_find_stream_info(iFmtCtx, (PointerPointer) null); 76 | if (ret < 0) { 77 | avutil.av_log(iFmtCtx, AV_LOG_ERROR, "avformat_find_stream_info error \n"); 78 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 79 | return; 80 | } 81 | oFmtCtx = new AVFormatContext(null); 82 | ret = avformat.avformat_alloc_output_context2(oFmtCtx, null, "flv", pushUrl); 83 | if (ret < 0) { 84 | return; 85 | } 86 | for (int i = 0; i < iFmtCtx.nb_streams(); i++) { 87 | AVStream stream = iFmtCtx.streams(i); 88 | if (stream.codecpar().codec_type() == AVMEDIA_TYPE_VIDEO) { 89 | oVStream = avformat.avformat_new_stream(oFmtCtx, null); 90 | vStream = stream; 91 | videoIndex = i; 92 | } else if (stream.codecpar().codec_type() == AVMEDIA_TYPE_AUDIO && param.getEnableAudio()) { 93 | oAStream = avformat.avformat_new_stream(oFmtCtx, null); 94 | avcodec.avcodec_parameters_copy(oAStream.codecpar(), stream.codecpar()); 95 | aStream = stream; 96 | audioIndex = i; 97 | } 98 | } 99 | AVCodec deCodec = avcodec.avcodec_find_decoder(vStream.codecpar().codec_id()); 100 | deCodecCtx = avcodec.avcodec_alloc_context3(deCodec); 101 | AVCodec enCodec = avcodec.avcodec_find_encoder_by_name("libx264"); 102 | enCodecCtx = avcodec.avcodec_alloc_context3(enCodec); 103 | ret = avcodec.avcodec_parameters_to_context(deCodecCtx, vStream.codecpar()); 104 | if (ret < 0) { 105 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "avcodec_parameters_to_context error \n"); 106 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 107 | return; 108 | } 109 | ret = avcodec.avcodec_open2(deCodecCtx, deCodec, (PointerPointer) null); 110 | if (ret < 0) { 111 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "avcodec_open2 error \n"); 112 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 113 | return; 114 | } 115 | if (param.getScaleWidth() != 0) { 116 | enCodecCtx.width(param.getScaleWidth()); 117 | needScale = true; 118 | } else { 119 | enCodecCtx.width(deCodecCtx.width()); 120 | } 121 | if (param.getScaleHeight() != 0) { 122 | enCodecCtx.height(param.getScaleHeight()); 123 | needScale = true; 124 | } else { 125 | enCodecCtx.height(deCodecCtx.height()); 126 | } 127 | enCodecCtx.gop_size(avutil.av_q2intfloat(vStream.avg_frame_rate())); 128 | enCodecCtx.has_b_frames(0); 129 | enCodecCtx.max_b_frames(0); 130 | enCodecCtx.profile(AVCodecContext.FF_PROFILE_H264_BASELINE); 131 | enCodecCtx.framerate(vStream.avg_frame_rate()); 132 | enCodecCtx.time_base(avutil.av_make_q(vStream.avg_frame_rate().den(), vStream.avg_frame_rate().num())); 133 | enCodecCtx.pix_fmt(AV_PIX_FMT_YUV420P); 134 | avcodec.avcodec_parameters_from_context(oVStream.codecpar(), enCodecCtx); 135 | oVStream.codecpar().codec_tag(0); 136 | ret = avcodec.avcodec_open2(enCodecCtx, enCodec, (PointerPointer) null); 137 | if (ret < 0) { 138 | avutil.av_log(enCodecCtx, AV_LOG_ERROR, "avcodec_open2 error \n"); 139 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 140 | return; 141 | } 142 | if (needScale) { 143 | swsCtx = swscale.sws_getContext(deCodecCtx.width(), deCodecCtx.height(), deCodecCtx.pix_fmt(), enCodecCtx.width(), enCodecCtx.height(), enCodecCtx.pix_fmt(), swscale.SWS_BICUBIC, null, null, (DoublePointer) null); 144 | if (swsCtx == null) { 145 | avutil.av_log(swsCtx, AV_LOG_ERROR, "sws_getContext error \n"); 146 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 147 | return; 148 | } 149 | } 150 | frame = avutil.av_frame_alloc(); 151 | frame.width(deCodecCtx.width()); 152 | frame.height(deCodecCtx.height()); 153 | frame.format(deCodecCtx.pix_fmt()); 154 | ret = av_frame_get_buffer(frame, 0); 155 | if (ret < 0) { 156 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "av_frame_get_buffer error \n"); 157 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 158 | return; 159 | } 160 | if (avutil.av_frame_is_writable(frame) != 1) { 161 | avutil.av_frame_make_writable(frame); 162 | } 163 | if (needScale) { 164 | swsFrame = avutil.av_frame_alloc(); 165 | swsFrame.width(enCodecCtx.width()); 166 | swsFrame.height(enCodecCtx.height()); 167 | swsFrame.format(enCodecCtx.pix_fmt()); 168 | ret = av_frame_get_buffer(swsFrame, 0); 169 | if (ret < 0) { 170 | avutil.av_log(swsCtx, AV_LOG_ERROR, "av_frame_get_buffer error \n"); 171 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 172 | return; 173 | } 174 | if (avutil.av_frame_is_writable(swsFrame) != 1) { 175 | avutil.av_frame_make_writable(swsFrame); 176 | } 177 | } 178 | packet = avcodec.av_packet_alloc(); 179 | packet.stream_index(oVStream.index()); 180 | srcPacket = avcodec.av_packet_alloc(); 181 | AVIOContext pb = new AVIOContext((Pointer) null); 182 | ret = avformat.avio_open(pb, pushUrl, avformat.AVIO_FLAG_WRITE); 183 | if (ret < 0) { 184 | avutil.av_log(oFmtCtx, AV_LOG_ERROR, "avio_open error \n"); 185 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 186 | return; 187 | } 188 | oFmtCtx.pb(pb); 189 | ret = avformat.avformat_write_header(oFmtCtx, (PointerPointer) null); 190 | if (ret < 0) { 191 | avutil.av_log(oFmtCtx, AV_LOG_ERROR, "avformat_write_header error \n"); 192 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 193 | return; 194 | } 195 | while (!isStop && (ret = avformat.av_read_frame(iFmtCtx, srcPacket)) == 0) { 196 | if (srcPacket.stream_index() == videoIndex) { 197 | ret = avcodec.avcodec_send_packet(deCodecCtx, srcPacket); 198 | if (ret < 0) { 199 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "avcodec_send_packet error \n"); 200 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 201 | return; 202 | } 203 | while (true) { 204 | ret = avcodec.avcodec_receive_frame(deCodecCtx, frame); 205 | if (ret == AVERROR_EAGAIN() || ret == AVERROR_EOF()) { 206 | break; 207 | } 208 | if (ret < 0) { 209 | avutil.av_log(deCodecCtx, AV_LOG_ERROR, "avcodec_receive_frame error \n"); 210 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 211 | return; 212 | } 213 | AVFrame tempFrame = null; 214 | if (needScale) { 215 | swscale.sws_scale(swsCtx, frame.data(), frame.linesize(), 0, frame.height(), swsFrame.data(), swsFrame.linesize()); 216 | swsFrame.pts(frame.pts()); 217 | swsFrame.pkt_dts(frame.pkt_dts()); 218 | swsFrame.duration(frame.duration()); 219 | tempFrame = swsFrame; 220 | } else { 221 | tempFrame = frame; 222 | } 223 | ret = avcodec.avcodec_send_frame(enCodecCtx, tempFrame); 224 | if (ret < 0) { 225 | avutil.av_log(enCodecCtx, AV_LOG_ERROR, "avcodec_send_frame error \n"); 226 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 227 | return; 228 | } 229 | while (true) { 230 | ret = avcodec.avcodec_receive_packet(enCodecCtx, packet); 231 | if (ret == AVERROR_EAGAIN() || ret == AVERROR_EOF()) { 232 | break; 233 | } 234 | if (ret < 0) { 235 | avutil.av_log(enCodecCtx, AV_LOG_ERROR, "avcodec_receive_packet error \n"); 236 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 237 | return; 238 | } 239 | avcodec.av_packet_rescale_ts(packet, vStream.time_base(), oVStream.time_base()); 240 | packet.dts(packet.pts()); 241 | avformat.av_interleaved_write_frame(oFmtCtx, packet); 242 | if (ret < 0) { 243 | avutil.av_log(oFmtCtx, AV_LOG_ERROR, "av_interleaved_write_frame error \n"); 244 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 245 | return; 246 | } 247 | avutil.av_frame_unref(frame); 248 | avcodec.av_packet_unref(packet); 249 | } 250 | } 251 | } else if (srcPacket.stream_index() == audioIndex && param.getEnableAudio()) { 252 | avcodec.av_packet_rescale_ts(srcPacket, aStream.time_base(), oAStream.time_base()); 253 | srcPacket.stream_index(oAStream.index()); 254 | ret = avformat.av_interleaved_write_frame(oFmtCtx, srcPacket); 255 | if (ret < 0) { 256 | avutil.av_log(oFmtCtx, AV_LOG_ERROR, "av_interleaved_write_frame error \n"); 257 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 258 | return; 259 | } 260 | } 261 | avcodec.av_packet_unref(srcPacket); 262 | } 263 | if (ret < 0) { 264 | avutil.av_log(iFmtCtx, AV_LOG_ERROR, "av_read_frame error \n"); 265 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 266 | return; 267 | } 268 | avformat.av_write_trailer(oFmtCtx); 269 | free(deCodecCtx, enCodecCtx, iFmtCtx, oFmtCtx, swsCtx, frame, swsFrame, srcPacket, packet); 270 | } 271 | 272 | /** 273 | * 停止 274 | */ 275 | public void stop() { 276 | this.isStop=true; 277 | } 278 | 279 | /** 280 | * 释放资源 281 | * 282 | * @param deCodecCtx 283 | * @param enCodecCtx 284 | * @param iFmtCtx 285 | * @param oFmtCtx 286 | * @param frame 287 | * @param srcPacket 288 | * @param packet 289 | */ 290 | private void free(AVCodecContext deCodecCtx, AVCodecContext enCodecCtx, AVFormatContext iFmtCtx, AVFormatContext oFmtCtx, SwsContext swsCtx, AVFrame frame, AVFrame swsFrame, AVPacket srcPacket, AVPacket packet) { 291 | log.info("【转码】释放流:{} 资源",param.getStream()); 292 | if (deCodecCtx != null) { 293 | avcodec.avcodec_free_context(deCodecCtx); 294 | deCodecCtx = null; 295 | } 296 | if (enCodecCtx != null) { 297 | avcodec.avcodec_free_context(enCodecCtx); 298 | enCodecCtx = null; 299 | } 300 | if (iFmtCtx != null) { 301 | avformat.avformat_close_input(iFmtCtx); 302 | iFmtCtx = null; 303 | } 304 | if (oFmtCtx!=null && (oFmtCtx.flags() & AVFMT_NOFILE) == 0){ 305 | avformat.avio_closep(oFmtCtx.pb()); 306 | } 307 | if (oFmtCtx != null) { 308 | avformat.avformat_free_context(oFmtCtx); 309 | oFmtCtx = null; 310 | } 311 | if (swsCtx != null) { 312 | swscale.sws_freeContext(swsCtx); 313 | swsCtx = null; 314 | } 315 | if (frame != null) { 316 | avutil.av_frame_free(frame); 317 | frame = null; 318 | } 319 | if (swsFrame != null) { 320 | avutil.av_frame_free(swsFrame); 321 | swsFrame = null; 322 | } 323 | if (srcPacket != null) { 324 | avcodec.av_packet_free(srcPacket); 325 | srcPacket = null; 326 | } 327 | if (packet != null) { 328 | avcodec.av_packet_free(packet); 329 | packet = null; 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 地址列表 7 | 360 | 361 | 362 |
363 | 364 |
365 |

JMediaServer测试页

366 |

点击下方链接跳转到相应页面,便捷访问各项功能

367 |
368 | 369 | 370 |
371 |
372 |
373 | 374 | 375 |
376 |
377 | 378 | 379 |
380 | 383 |
384 | 385 |
386 | 387 | 388 | 428 | 429 | 430 |
431 |

© 2025 JMediaServer

432 |
433 |
434 | 435 | 436 | 447 | 448 | 621 | 622 | 623 | --------------------------------------------------------------------------------