├── LICENSE
├── README.md
├── pom.xml
└── src
└── main
├── java
└── cn
│ └── zack
│ ├── Application.java
│ ├── camera
│ └── VideoService.java
│ ├── config
│ └── RestTemplateConfig.java
│ └── utils
│ ├── GPIO_Utils.java
│ └── MoveUtil.java
└── resources
├── application-dev.yml
└── haarcascade_frontalface_alt.xml
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### 基于Raspberry开发板构建监控平台
2 |
3 | ---
4 |
5 | ##### 已实现功能:
6 | - 音视频直播 ✔
7 | - 人脸识别 ✔
8 | - 运动追踪 ✔
9 | - 告警通知 ✔
10 |
11 | ##### 待实现功能:
12 | - 双向通话 ✖
13 |
14 | ---
15 |
16 | ##### 硬件相关:
17 | - 主板采用Raspberry 4B开发板
18 | - 运动控制采用SG90舵机(或者5线4相步进电机), 支持2自由度运动
19 | - 相机采用CSI相机模块(也可采用USB相机模块)
20 | - 红外成像采用850nm不可见红外光以及光敏二极管切换红外模式
21 |
22 | ---
23 |
24 | ##### 软件相关:
25 | - 推流端系统采用Raspbian, 服务端系统采用Ubuntu, 客户端可基于flv.js在线播放或者支持rtmp协议的播放器
26 | - SpringBoot开源框架
27 | - JavaCV开源框架
28 | - Pi4j开源框架
29 | - Nginx开源框架, 以及nginx-rtmp模块提供流媒体支持
30 | - Frp开源软件, 提供局域网对公网穿透能力
31 | - 阿里云AAAA域名解析以及自建IPv6DDNS对IPV6网络支持
32 | - 参考了motion软件的一些实现
33 |
34 | ---
35 |
36 | ##### 实现原理
37 | - 推流端运行在Raspberry 4B开发板, 基于javacv实时捕获相机和麦克风数据混合为视频流, 使用opencv官方训练模型对每一帧图像进行分析, 判定是否存在人脸信息(通知流媒体服务器开始/停止录制)以及目标偏离画面中心的方位(通过GPIO接口控制舵机转向目标方位), 同时通过ffmpeg推流到流媒体服务端
38 | - 流媒体服务端运行在X86主机(或者ECS), 基于nginx以及nginx-rtmp模块搭建流媒体服务器, 同时暴露直播开始/停止录制接口, 此处不进行转码
39 | - 播放端运行在web端(基于flv.js的播放器)或者app(支持rtmp协议)
40 | - ~~关于双向通话, 需要播放端支持音频录制(暂未完成), 将录制的实时音频推流到流媒体服务器另一个直播流, 同时通知原推流端(Raspberry 4B)拉流播放(ffplay)(本条不做了, 问就是懒)~~
41 |
42 | ---
43 |
44 | ##### GPIO和PWM
45 | - GPIO
46 | 通用型之输入输出, 其接脚可以供使用者由程控自由使用,PIN脚依现实考量可作为通用输入(GPI)或通用输出(GPO)或通用输入与输出(GPIO)
47 |
48 | - PWM
49 | 脉冲宽度调制是一种模拟控制方式, 分为相电压控制PWM、脉宽PWM法、随机PWM、SPWM法、线电压控制PWM等
50 | 脉宽PWM法: 把每一脉冲宽度均相等的脉冲列作为PWM波形,通过调整PWM的周期、PWM的占空比达到控制输出电平信号的目的
51 |
52 | 在本系统中使用Raspberry 4B板载GPIO, 通过模拟输出PWM脉冲占空比控制舵机, 其中SG90舵机标准控制频率为50HZ, 脉冲占空比为2.5%-12.5%, 则对应脉宽为0.5ms-2.5ms
53 |
54 | ---
55 |
56 | #### 编译与运行
57 | - 修正配置文件
58 | 检查相机驱动编号(本机相机驱动号默认为0, 根据需要修改), 视频推流/输出地址, 流媒体服务器开始/停止录制接口地址, 历史回看资源服务器地址, SMTP邮件配置
59 |
60 | - 编译
61 | ###### pi4j框架为arm平台Raspberry定制化封装, 如果在X86平台运行, 需要移除pi4j依赖和舵机控制相关代码
62 | 由于JavaCV基于JavaCPP封装了OpenCV在各个平台的实现以及对应的平台库, 与OpenCV一样, 在不同的平台需要指定不同编译环境
63 | 在X86平台编译:
64 | ```shell
65 | mvn clean install -DskipTests
66 | ```
67 | 在arm平台编译:
68 | ```shell
69 | mvn clean install -Dplatform.name=linux-arm
70 | ```
71 |
72 | - 运行
73 | ```shell
74 | java -jar XXX.jar
75 | ```
76 |
77 | ---
78 | 感谢以上开源社区做出的贡献!!!
79 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 |
8 | org.springframework.boot
9 | spring-boot-starter-parent
10 | 2.0.2.RELEASE
11 |
12 |
13 | cn.zack
14 | RaspberryCameraAutoTrackLive
15 | 1.0-SNAPSHOT
16 | jar
17 |
18 |
19 | 8
20 | 8
21 |
22 | 1.5.4
23 |
24 |
25 |
26 |
27 | org.springframework.boot
28 | spring-boot-starter-web
29 |
30 |
31 |
32 | org.bytedeco
33 | javacv-platform
34 | ${javacv.version}
35 |
36 |
37 | org.hibernate.validator
38 | hibernate-validator-parent
39 |
40 |
41 |
42 |
43 |
44 | org.springframework.boot
45 | spring-boot-starter-mail
46 |
47 |
48 |
49 | com.pi4j
50 | pi4j-core
51 | 1.3
52 |
53 |
54 |
55 |
56 |
57 |
58 | org.springframework.boot
59 | spring-boot-maven-plugin
60 |
61 |
62 | CameraVideoLive
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/main/java/cn/zack/Application.java:
--------------------------------------------------------------------------------
1 | package cn.zack;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.boot.builder.SpringApplicationBuilder;
6 |
7 | @SpringBootApplication
8 | public class Application {
9 | public static void main(String[] args) {
10 | /**
11 | * 初始化spring
12 | * 当需要屏幕预览等操作时, 设置headless为false, 否则会报java.awt.HeadlessException
13 | * java.awt.headless是J2SE的一种模式, 用于在缺失显示屏、鼠标或者键盘时的系统配置, springboot默认将这个属性设置为true
14 | */
15 | new SpringApplicationBuilder(Application.class).headless(false).run(args);
16 | // SpringApplication.run(Application.class, args);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/cn/zack/camera/VideoService.java:
--------------------------------------------------------------------------------
1 | package cn.zack.camera;
2 |
3 | import cn.zack.utils.MoveUtil;
4 | import org.bytedeco.ffmpeg.global.avcodec;
5 | import org.bytedeco.javacv.*;
6 | import org.bytedeco.opencv.global.opencv_imgcodecs;
7 | import org.bytedeco.opencv.global.opencv_imgproc;
8 | import org.bytedeco.opencv.opencv_core.*;
9 | import org.bytedeco.opencv.opencv_objdetect.CascadeClassifier;
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 | import org.springframework.beans.factory.annotation.Autowired;
13 | import org.springframework.beans.factory.annotation.Value;
14 | import org.springframework.core.io.FileSystemResource;
15 | import org.springframework.mail.javamail.JavaMailSender;
16 | import org.springframework.mail.javamail.MimeMessageHelper;
17 | import org.springframework.stereotype.Service;
18 | import org.springframework.web.client.RestTemplate;
19 |
20 | import static org.bytedeco.opencv.global.opencv_imgproc.*;
21 |
22 | import javax.annotation.PostConstruct;
23 | import javax.mail.MessagingException;
24 | import javax.mail.internet.MimeMessage;
25 | import javax.sound.sampled.*;
26 | import java.nio.ByteBuffer;
27 | import java.nio.ByteOrder;
28 | import java.nio.ShortBuffer;
29 | import java.text.SimpleDateFormat;
30 | import java.util.Date;
31 | import java.util.concurrent.ScheduledThreadPoolExecutor;
32 | import java.util.concurrent.TimeUnit;
33 |
34 | @Service
35 | public class VideoService {
36 |
37 | private static final Logger logger = LoggerFactory.getLogger(VideoService.class);
38 |
39 | /**
40 | * 音频设备驱动号
41 | */
42 | @Value("${camera.audio.device}")
43 | private int audioDevice;
44 |
45 | /**
46 | * 相机驱动号
47 | */
48 | @Value("${camera.video.device}")
49 | private int cameraDevice;
50 |
51 | /**
52 | * 录制帧率, 最低25(低于25帧会闪屏)
53 | */
54 | @Value("${camera.video.rate}")
55 | private int rate;
56 |
57 | /**
58 | * 录制画面宽度
59 | */
60 | @Value("${camera.video.width}")
61 | private int width;
62 |
63 | /**
64 | * 录制画面高度
65 | */
66 | @Value("${camera.video.height}")
67 | private int height;
68 |
69 | /**
70 | * 画面中心矩形的四条边界线
71 | * 左右两条边界线为X轴坐标值
72 | * 上下两条边界线为Y轴坐标值
73 | */
74 | @Value("${camera.video.center.leftLine}")
75 | private int leftLine;
76 | @Value("${camera.video.center.rightLine}")
77 | private int rightLine;
78 | @Value("${camera.video.center.topLine}")
79 | private int topLine;
80 | @Value("${camera.video.center.downLine}")
81 | private int downLine;
82 |
83 | /**
84 | * 画面中心点坐标
85 | */
86 | @Value("${camera.video.center.pointX}")
87 | private int pointX;
88 | @Value("${camera.video.center.pointY}")
89 | private int pointY;
90 |
91 | /**
92 | * 帧截图临时保存位置
93 | */
94 | @Value("${camera.screen.path}")
95 | private String screenPath;
96 |
97 | /**
98 | * rtmp协议推流地址
99 | */
100 | @Value("${rtmp.push.url}")
101 | private String rtmpUrl;
102 |
103 | /**
104 | * 通知rtmp服务端开启录制接口
105 | */
106 | @Value("${rtmp.record.start}")
107 | private String recordStartUrl;
108 |
109 | /**
110 | * 通知rtmp服务端开启录制接口
111 | */
112 | @Value("${rtmp.history.prefix}")
113 | private String historyLivePrefix;
114 |
115 | /**
116 | * 通知rtmp服务端停止录制接口
117 | */
118 | @Value("${rtmp.record.stop}")
119 | private String recordStoptUrl;
120 |
121 | /**
122 | * 注入restTemplate, 用于通知rtmp服务端开始或者停止录制
123 | */
124 | @Autowired
125 | private RestTemplate restTemplate;
126 |
127 | /**
128 | * 注入邮件发送器
129 | */
130 | @Autowired
131 | private JavaMailSender javaMailSender;
132 |
133 | /**
134 | * 舵机运动控制
135 | */
136 | @Autowired
137 | private MoveUtil moveUtil;
138 |
139 | /**
140 | * rtmp服务端的录制状态
141 | */
142 | private boolean recordStatus = false;
143 |
144 | /**
145 | * 本次录制最后一次检测到人脸的时间, 毫秒值
146 | */
147 | private long recordLastFaceTime = 0;
148 |
149 | /**
150 | * 推送/录制本机的音/视频(Webcam/Microphone)到流媒体服务器(Stream media server)
151 | */
152 | @PostConstruct
153 | public void recordWebcamAndMicrophone() throws FrameGrabber.Exception {
154 | /**
155 | * FrameGrabber 类包含:OpenCVFrameGrabber
156 | * (opencv_videoio),C1394FrameGrabber, FlyCaptureFrameGrabber,
157 | * OpenKinectFrameGrabber,PS3EyeFrameGrabber,VideoInputFrameGrabber,
158 | * FFmpegFrameGrabber.
159 | */
160 | FrameGrabber grabber = new OpenCVFrameGrabber(cameraDevice);
161 | grabber.setImageWidth(width);
162 | grabber.setImageHeight(height);
163 | logger.info("开始获取摄像头...");
164 | try {
165 | grabber.start();
166 | logger.info("开启摄像头成功...");
167 | } catch (Exception e) {
168 | try {
169 | logger.error("摄像头开启失败, 尝试重启摄像头...");
170 | grabber.restart();
171 | } catch (Exception ex) {
172 | logger.error("重启摄像头失败...");
173 | }
174 | }
175 |
176 | /**
177 | * FFmpegFrameRecorder(String filename, int imageWidth, int imageHeight,
178 | * int audioChannels) fileName可以是本地文件(会自动创建),也可以是RTMP路径(发布到流媒体服务器)
179 | * imageWidth = width (为捕获器设置宽) imageHeight = height (为捕获器设置高)
180 | * audioChannels = 2(立体声);1(单声道);0(无音频)
181 | */
182 | FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(rtmpUrl, width, height, 2);
183 | recorder.setInterleaved(true);
184 |
185 | /**
186 | * 该参数用于降低延迟 参考FFMPEG官方文档:https://trac.ffmpeg.org/wiki/StreamingGuide
187 | * 官方原文参考:ffmpeg -f dshow -i video="Virtual-Camera" -vcodec libx264
188 | * -tune zerolatency -b 900k -f mpegts udp://10.1.0.102:1234
189 | */
190 | recorder.setVideoOption("tune", "zerolatency");
191 |
192 | /**
193 | * 权衡quality(视频质量)和encode speed(编码速度) values(值):
194 | * ultrafast(终极快),superfast(超级快), veryfast(非常快), faster(很快), fast(快),
195 | * medium(中等), slow(慢), slower(很慢), veryslow(非常慢)
196 | * ultrafast(终极快)提供最少的压缩(低编码器CPU)和最大的视频流大小;而veryslow(非常慢)提供最佳的压缩(高编码器CPU)的同时降低视频流的大小
197 | * 参考:https://trac.ffmpeg.org/wiki/Encode/H.264 官方原文参考:-preset ultrafast
198 | * as the name implies provides for the fastest possible encoding. If
199 | * some tradeoff between quality and encode speed, go for the speed.
200 | * This might be needed if you are going to be transcoding multiple
201 | * streams on one machine.
202 | */
203 | recorder.setVideoOption("preset", "fast");
204 |
205 | /**
206 | * 参考转流命令: ffmpeg
207 | * -i'udp://localhost:5000?fifo_size=1000000&overrun_nonfatal=1' -crf 30
208 | * -preset ultrafast -acodec aac -strict experimental -ar 44100 -ac
209 | * 2-b:a 96k -vcodec libx264 -r 25 -b:v 500k -f flv 'rtmp:///live/cam0' -crf 30
211 | * -设置内容速率因子,这是一个x264的动态比特率参数,它能够在复杂场景下(使用不同比特率,即可变比特率, 数值越大, 画质越差, 一般18认为视觉无损)保持视频质量;
212 | * 可以设置更低的质量(quality)和比特率(bit rate),参考Encode/H.264 -preset ultrafast
213 | * -参考上面preset参数,与视频压缩率(视频大小)和速度有关,需要根据情况平衡两大点:压缩率(视频大小),编/解码速度 -acodec
214 | * aac -设置音频编/解码器 (内部AAC编码) -strict experimental
215 | * -允许使用一些实验的编解码器(比如上面的内部AAC属于实验编解码器) -ar 44100 设置音频采样率(audio sample
216 | * rate) -ac 2 指定双通道音频(即立体声) -b:a 96k 设置音频比特率(bit rate) -vcodec libx264
217 | * 设置视频编解码器(codec) -r 25 -设置帧率(frame rate) -b:v 500k -设置视频比特率(bit
218 | * rate),比特率越高视频越清晰,视频体积也会变大,需要根据实际选择合理范围 -f flv
219 | * -提供输出流封装格式(rtmp协议只支持flv封装格式) 'rtmp:///live/cam0'-流媒体服务器地址
221 | */
222 | recorder.setVideoOption("crf", "18");
223 |
224 | /**
225 | * 使用硬编解码
226 | * 参考ffmpeg -f alsa -ar 16000 -ac 1 -i hw:1 -f video4linux2 -s 640x480 -r 25 -i /dev/video0 -c:v h264_omx -f flv "rtmp地址"
227 | * -f 指定输入或输出文件格式
228 | * alsa 高级Linux声音架构的简称
229 | * video4linux2 为linux中视频设备的内核驱动
230 | * -ar 16000 指定音频采样率为16kHz
231 | * -ac 1 指定音频声数
232 | * -i hw:1 指定输入来源
233 | * -s 640x480 指定帧大小(不指定就为原画尺寸)
234 | * -r 25 指定帧率fps
235 | * -c:v h264_omx 指定用h264_omx解码(树莓派的硬件解码,速度快得多)
236 | */
237 | // recorder.setVideoOption("c:v", "h264_omx");
238 | // h264编/解码器
239 | recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
240 | // 4000 kb/s, 1080P视频的合理比特率范围
241 | recorder.setVideoBitrate(2000000);
242 | // 封装格式flv
243 | recorder.setFormat("flv");
244 | // 视频帧率(保证视频质量的情况下最低25,低于25会出现闪屏)
245 | recorder.setFrameRate(rate);
246 | /**
247 | * 视频帧通常分为B,P,I
248 | * 其中I为关键帧, 实际上是一张完整的静态图像
249 | * B帧和P帧是用来记录的运动矢量等非图像数据, 需要依赖I帧才能够解码出完整图像(有损图像)
250 | * 其中大量使用B帧可以提高压缩率, 但是会消耗更多硬件性能(CPU或GPU编码)
251 | * 大部分情况下都以I帧和大量P帧为主
252 | * 设置关键帧间隔, 间隔越短则拉流端显示首帧画面越快(B,P帧需要I帧才能解码), 但同时增加了传输I帧的数据量
253 | */
254 | recorder.setGopSize(rate);
255 | // 不可变(固定)音频比特率
256 | recorder.setAudioOption("crf", "0");
257 | // 最高质量
258 | recorder.setAudioQuality(0);
259 | // 音频比特率
260 | recorder.setAudioBitrate(192000);
261 | // 音频采样率
262 | recorder.setSampleRate(44100);
263 | // 双通道(立体声)
264 | recorder.setAudioChannels(2);
265 | // 音频编/解码器
266 | recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
267 |
268 | try {
269 | // 开启录制器
270 | logger.info("开始录制...");
271 | recorder.start();
272 | } catch (Exception e) {
273 | try {
274 | logger.error("录制失败,尝试重启录制...");
275 | recorder.stop();
276 | recorder.start();
277 | } catch (Exception ex) {
278 | logger.error("重启录制失败...");
279 | }
280 | }
281 | // 异步线程捕获音频
282 | ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(1);
283 | new Thread(() -> {
284 | /**
285 | * 设置音频编码器 最好是系统支持的格式,否则getLine() 会发生错误
286 | * 采样率:44.1k;采样率位数:16位;立体声(stereo);是否签名;true:
287 | * big-endian字节顺序,false:little-endian字节顺序(详见:ByteOrder类)
288 | */
289 | AudioFormat audioFormat = new AudioFormat(44100.0F, 16, 2, true, false);
290 | DataLine.Info dataLineInfo = new DataLine.Info(TargetDataLine.class, audioFormat);
291 | try {
292 | // 打开并开始捕获音频
293 | TargetDataLine line = (TargetDataLine) AudioSystem.getLine(dataLineInfo);
294 | line.open(audioFormat);
295 | line.start();
296 | // 获得当前音频采样率
297 | int sampleRate = (int) audioFormat.getSampleRate();
298 | // 获取当前音频通道数量
299 | int numChannels = audioFormat.getChannels();
300 | // 初始化音频缓冲区(size是音频采样率*通道数)
301 | int audioBufferSize = sampleRate * numChannels;
302 | byte[] audioBytes = new byte[audioBufferSize];
303 |
304 | // 使用延时线程逐帧写入音频, 延时时间为1秒/帧率
305 | exec.scheduleAtFixedRate(() -> {
306 | try {
307 | // 非阻塞方式读取
308 | int nBytesRead = line.read(audioBytes, 0, line.available());
309 | // 因为我们设置的是16位音频格式,所以需要将byte[]转成short[]
310 | int nSamplesRead = nBytesRead / 2;
311 | short[] samples = new short[nSamplesRead];
312 | /**
313 | * ByteBuffer.wrap(audioBytes)-将byte[]数组包装到缓冲区
314 | * ByteBuffer.order(ByteOrder)-按little-endian修改字节顺序,解码器定义的
315 | * ByteBuffer.asShortBuffer()-创建一个新的short[]缓冲区
316 | * ShortBuffer.get(samples)-将缓冲区里short数据传输到short[]
317 | */
318 | ByteBuffer.wrap(audioBytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(samples);
319 | // 将short[]包装到ShortBuffer
320 | ShortBuffer sBuff = ShortBuffer.wrap(samples, 0, nSamplesRead);
321 | // 按通道录制shortBuffer
322 | recorder.recordSamples(sampleRate, numChannels, sBuff);
323 | } catch (Exception e) {
324 | logger.error("读取音频缓冲流失败...");
325 | }
326 | }, 0, (long) 800 / rate, TimeUnit.MILLISECONDS);
327 | } catch (Exception ex) {
328 | logger.error("获取音频失败...");
329 | }
330 | }).start();
331 |
332 | // javaCV提供了优化非常好的硬件加速组件来帮助显示我们抓取的摄像头视频
333 | CanvasFrame cFrame = new CanvasFrame("Capture Preview", CanvasFrame.getDefaultGamma() / grabber.getGamma());
334 |
335 | /**
336 | * 读取opencv人脸训练模型
337 | * 如果读取不到, 则error: (-215:Assertion failed) !empty() in function 'cv::CascadeClassifier::detectMultiScale'
338 | * 注意, 209错误可能为模型文件格式错误或者解析出错
339 | */
340 | String path = Thread.currentThread().getContextClassLoader().getResource("haarcascade_frontalface_alt.xml").getPath();
341 | if (path.contains("BOOT-INF")) {
342 | path = path.replace("!", "").split("BOOT-INF/")[1];
343 | } else {
344 | path = path.substring(1);
345 | }
346 | logger.info(path);
347 | // 加载模型配置
348 | CascadeClassifier cascade = new CascadeClassifier(path);
349 |
350 | // 人脸框颜色
351 | Scalar faceScalar = new Scalar(0, 255, 0, 1);
352 | // 转换器,用于Frame/Mat/IplImage相互转换
353 | OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage();
354 | // 时间戳水印位置
355 | Point timePoint = new Point(920, 50);
356 | // 时间戳水印颜色
357 | Scalar timeScalar = new Scalar(0, 255, 255, 0);
358 | // 时间戳格式
359 | SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
360 |
361 | // 执行抓取(capture)过程
362 | Frame capturedFrame;
363 | long videoTS;
364 | long startTime = System.currentTimeMillis();
365 | while ((capturedFrame = grabber.grab()) != null) {
366 | // 创建一个 timestamp用来同步音频
367 | videoTS = 1000 * (System.currentTimeMillis() - startTime);
368 | //检查偏移量
369 | recorder.setTimestamp(videoTS);
370 | // 获取当前帧彩色图像
371 | Mat mat = (Mat) capturedFrame.opaque;
372 | // 存放灰度图
373 | Mat grayImg = new Mat();
374 | /**
375 | * 摄像头获取的是彩色图像转灰度
376 | * 如果要获取摄像头灰度图,可以直接对FrameGrabber进行设置grabber.setImageMode(ImageMode.GRAY);,grabber.grab()获取的都是灰度图
377 | */
378 | cvtColor(mat, grayImg, COLOR_BGRA2GRAY);
379 | // 均衡化直方图
380 | equalizeHist(grayImg, grayImg);
381 | // 检测到人脸
382 | RectVector faces = new RectVector();
383 | // 给人脸绘制矩形
384 | cascade.detectMultiScale(grayImg, faces);
385 | // 收集当前帧中的矩形
386 | int[][] rectangles = new int[(int) faces.size()][2];
387 | // 遍历人脸
388 | for (int i = 0; i < faces.size(); i++) {
389 | Rect face_i = faces.get(i);
390 | //绘制人脸矩形区域,scalar色彩顺序:BGR(蓝绿红)
391 | rectangle(mat, face_i, faceScalar);
392 | // 计算此矩形中心点坐标
393 | rectangles[i][0] = face_i.x() + (face_i.width() / 2);
394 | rectangles[i][1] = face_i.y() + (face_i.height() / 2);
395 | }
396 | // 如果当前帧画面中检测到人脸
397 | if (rectangles.length > 0) {
398 | // 最后一次检测到人脸的时间重置为当前时间
399 | recordLastFaceTime = System.currentTimeMillis();
400 | // 计算当前帧中矩形的总体偏向方位, 开启目标追踪
401 | try {
402 | getPosition(rectangles);
403 | } catch (InterruptedException e) {
404 | logger.info("运动追踪发生异常");
405 | }
406 | // 如果当前不在录制中
407 | if (!recordStatus) {
408 | // 通知录制线程开始录制
409 | recordStart();
410 | // 同时将当前帧的图片临时保存
411 | opencv_imgcodecs.imwrite(screenPath, mat);
412 | }
413 | } else {
414 | // 如果当前正在录制中, 并且连续5秒检测不到人脸, 停止录制
415 | if (recordStatus && System.currentTimeMillis() - recordLastFaceTime > 10000) {
416 | recordStop();
417 | // 舵机回正
418 | try {
419 | moveUtil.turnPointX();
420 | // todo 上下回正待完成
421 | } catch (InterruptedException e) {
422 | logger.info("舵机回正发生异常");
423 | }
424 | }
425 | }
426 |
427 | // 开启本地预览
428 | if (cFrame.isVisible()) {
429 | //本机预览要发送的帧
430 | cFrame.showImage(capturedFrame);
431 | }
432 |
433 | try {
434 | // 每一帧添加时间戳水印
435 | opencv_imgproc.putText(mat,
436 | format.format(new Date()),
437 | timePoint,
438 | opencv_imgproc.CV_FONT_ITALIC,
439 | 0.8,
440 | timeScalar,
441 | 2,
442 | 20,
443 | false);
444 | // 发送帧
445 | recorder.record(capturedFrame);
446 | } catch (Exception e) {
447 | logger.error("录制帧发生异常");
448 | }
449 | }
450 | // 本地预览
451 | cFrame.dispose();
452 | try {
453 | recorder.stop();
454 | recorder.release();
455 | } catch (FrameRecorder.Exception e) {
456 | logger.error("关闭录制器失败");
457 | }
458 | try {
459 | grabber.stop();
460 | } catch (Exception e) {
461 | logger.error("关闭摄像头失败");
462 | }
463 | }
464 |
465 | /**
466 | * 计算画面中多个矩形总体偏向画面的方位
467 | *
468 | * @param rectangles 二维矩形数组, 第一维保存矩形的宽, 第二维保存矩形的高
469 | * @return 偏向方位, 0为中央, 1为左上, -1为左下, 2为右上, -2为右下
470 | */
471 | public void getPosition(int[][] rectangles) throws InterruptedException {
472 | // 初始化多个矩形整体中心点坐标
473 | int centerX = 0;
474 | int centerY = 0;
475 | for (int i = 0; i < rectangles.length; i++) {
476 | // 取当前点的坐标
477 | int thisPointX = rectangles[i][0];
478 | int thisPointY = rectangles[i][1];
479 | centerX += thisPointX;
480 | centerY += thisPointY;
481 | }
482 | // 得到中心点坐标
483 | centerX = centerX / rectangles.length;
484 | centerY = centerY / rectangles.length;
485 |
486 | // 位于中心区域
487 | if (centerX >= leftLine && centerX <= rightLine && centerY >= topLine && centerY <= downLine) {
488 | // 中心区域, 无需不追踪, 暂时不作处理
489 | } else {
490 | // 不在中心区域, 继续判定具体方位
491 | // 偏左
492 | if (centerX < pointX) {
493 | moveUtil.turnLeft();
494 | // 继续判定左上或者左下
495 | if (centerY < pointY) {
496 | moveUtil.turnUp();
497 | } else {
498 | moveUtil.turnDown();
499 | }
500 | } else {
501 | moveUtil.turnRight();
502 | // 继续判定右上或者右下
503 | if (centerY < pointY) {
504 | moveUtil.turnUp();
505 | } else {
506 | moveUtil.turnDown();
507 | }
508 | }
509 | }
510 | }
511 |
512 | /**
513 | * 异步通知流媒体服务端开始录制
514 | */
515 | public void recordStart() {
516 | // 如果当前是非录制状态, 通知rtmp服务端开始录制
517 | if (!recordStatus) {
518 | // 录制状态改为开启
519 | recordStatus = true;
520 | new Thread(() -> {
521 | String startResponse = restTemplate.getForObject(recordStartUrl, String.class);
522 | logger.info("通知rtmp服务端开始录制, 响应报文: {}", startResponse);
523 | // 发邮件通知
524 | if (startResponse != null) {
525 | sendMail(historyLivePrefix + startResponse.substring(11));
526 | }
527 | }).start();
528 | }
529 | }
530 |
531 | /**
532 | * 异步通知流媒体服务端停止录制
533 | */
534 | public void recordStop() {
535 | // 如果当前是录制状态, 通知rtmp服务端停止录制
536 | if (recordStatus) {
537 | // 录制状态改为停止
538 | recordStatus = false;
539 | new Thread(() -> {
540 | String startResponse = restTemplate.getForObject(recordStoptUrl, String.class);
541 | logger.info("通知rtmp服务端停止录制, 响应报文: {}", startResponse);
542 | }).start();
543 | }
544 | }
545 |
546 | /**
547 | * 发送邮件
548 | */
549 | public void sendMail(String mailText) {
550 | MimeMessage mimeMessage = javaMailSender.createMimeMessage();
551 | MimeMessageHelper messageHelper;
552 | try {
553 | messageHelper = new MimeMessageHelper(mimeMessage, true);
554 | // 设置邮件主题
555 | messageHelper.setSubject("检测到人脸");
556 | // 设置邮件发送者,这个跟application.yml中设置的要一致
557 | messageHelper.setFrom("xxxxxxxxx@qq.com");
558 | // 设置邮件接收者,可以有多个接收者,中间用逗号隔开,以下类似
559 | messageHelper.setTo("xxxxxxxxx@qq.com");
560 | // 设置邮件抄送人,可以有多个抄送人
561 | messageHelper.setCc("xxxxxxxxx@qq.com");
562 | // 设置邮件发送日期
563 | messageHelper.setSentDate(new Date());
564 | // 设置邮件的正文
565 | messageHelper.setText(mailText);
566 | // 设置附件
567 | FileSystemResource file = new FileSystemResource(screenPath);
568 | messageHelper.addAttachment("temp.jpg", file);
569 | // 发送邮件
570 | javaMailSender.send(mimeMessage);
571 | } catch (MessagingException e) {
572 | e.printStackTrace();
573 | }
574 | }
575 | }
576 |
--------------------------------------------------------------------------------
/src/main/java/cn/zack/config/RestTemplateConfig.java:
--------------------------------------------------------------------------------
1 | package cn.zack.config;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.http.client.SimpleClientHttpRequestFactory;
6 | import org.springframework.web.client.RestTemplate;
7 |
8 | /**
9 | * 注入restTemplate配置
10 | */
11 | @Configuration
12 | public class RestTemplateConfig {
13 | @Bean
14 | public RestTemplate restTemplate() {
15 | SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
16 | // 5秒连接超时
17 | factory.setConnectTimeout(50000);
18 | // 2秒读取超时
19 | factory.setReadTimeout(20000);
20 | return new RestTemplate(factory);
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/cn/zack/utils/GPIO_Utils.java:
--------------------------------------------------------------------------------
1 | package cn.zack.utils;
2 |
3 | import com.pi4j.io.gpio.GpioController;
4 | import com.pi4j.io.gpio.GpioFactory;
5 |
6 | /**
7 | * @author cn.zack
8 | * GPIO控制器
9 | */
10 | public class GPIO_Utils {
11 |
12 | private static GpioController gpioController;
13 |
14 | /**
15 | * 双锁检查 创建全局唯一GPIO控制器, 防止多线程操作GPIO损坏主板
16 | *
17 | * @return
18 | */
19 | public static GpioController getGpioController() {
20 | if (gpioController == null) {
21 | synchronized (GPIO_Utils.class) {
22 | if (gpioController == null) {
23 | gpioController = GpioFactory.getInstance();
24 | }
25 | }
26 | }
27 | return gpioController;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/cn/zack/utils/MoveUtil.java:
--------------------------------------------------------------------------------
1 | package cn.zack.utils;
2 |
3 | import com.pi4j.io.gpio.GpioController;
4 | import com.pi4j.io.gpio.GpioPinDigitalOutput;
5 | import com.pi4j.io.gpio.PinState;
6 | import com.pi4j.io.gpio.RaspiPin;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 | import org.springframework.stereotype.Service;
10 |
11 | @Service
12 | public class MoveUtil {
13 | private static final Logger logger = LoggerFactory.getLogger(MoveUtil.class);
14 |
15 | // 获取gpio控制器
16 | GpioController gpioController = GPIO_Utils.getGpioController();
17 | // 定义pin, 其中00针脚接入舵机为上下方位180°运动控制, 01针脚为左右方位180°运动控制
18 | GpioPinDigitalOutput gpio00 = gpioController.provisionDigitalOutputPin(RaspiPin.GPIO_00, "00", PinState.LOW);
19 | GpioPinDigitalOutput gpio01 = gpioController.provisionDigitalOutputPin(RaspiPin.GPIO_01, "01", PinState.LOW);
20 |
21 | private int pointX = 0;
22 |
23 | /**
24 | * 相机向左
25 | * SG90 脉冲周期为20ms,脉宽0.5ms-2.5ms对应的角度-90到+90,对应的占空比为2.5%-12。
26 | *
27 | * @throws InterruptedException
28 | */
29 | public void turnLeft() throws InterruptedException {
30 | logger.info("相机正在向左...");
31 |
32 | // 如果当前在右侧, 应回正
33 | if (pointX == 1) {
34 | turnPointX();
35 | // 如果当前不在右侧(左或者已回正, 开始左转)
36 | } else {
37 | for (int i = 0; i < 20; i++) {
38 | gpio01.high();
39 |
40 | long highStart = System.nanoTime();
41 | long highEnd;
42 | do {
43 | highEnd = System.nanoTime();
44 | } while (highEnd - highStart < 50000);
45 |
46 | gpio01.low();
47 |
48 | long lowStart = System.nanoTime();
49 | long lowEnd;
50 | do {
51 | lowEnd = System.nanoTime();
52 | } while (lowEnd - lowStart < 1870000);
53 | }
54 | pointX = -1;
55 | }
56 | }
57 |
58 | /**
59 | * 相机向右
60 | *
61 | * @throws InterruptedException
62 | */
63 | public void turnRight() throws InterruptedException {
64 | logger.info("相机正在向右...");
65 | // 如果当前在左侧, 应回正
66 | if (pointX == -1) {
67 | turnPointX();
68 | // 如果当前不在左侧(已回正, 或者在右侧, 开始右转)
69 | } else {
70 | for (int i = 0; i < 20; i++) {
71 | gpio01.high();
72 |
73 | long highStart = System.nanoTime();
74 | long highEnd;
75 | do {
76 | highEnd = System.nanoTime();
77 | } while (highEnd - highStart < 2100000);
78 |
79 | gpio01.low();
80 |
81 | long lowStart = System.nanoTime();
82 | long lowEnd;
83 | do {
84 | lowEnd = System.nanoTime();
85 | } while (lowEnd - lowStart < 1700000);
86 | }
87 | pointX = 1;
88 | }
89 | }
90 |
91 | /**
92 | * 左右回正
93 | *
94 | * @throws InterruptedException
95 | */
96 | public void turnPointX() throws InterruptedException {
97 | logger.info("相机回正...");
98 | for (int i = 0; i < 20; i++) {
99 | gpio01.high();
100 |
101 | long highStart = System.nanoTime();
102 | long highEnd;
103 | do {
104 | highEnd = System.nanoTime();
105 | } while (highEnd - highStart < 1400000);
106 |
107 | gpio01.low();
108 |
109 | long lowStart = System.nanoTime();
110 | long lowEnd;
111 | do {
112 | lowEnd = System.nanoTime();
113 | } while (lowEnd - lowStart < 1800000);
114 | }
115 | pointX = 0;
116 | }
117 |
118 |
119 | /**
120 | * 相机向上
121 | *
122 | * @throws InterruptedException
123 | */
124 | public void turnUp() throws InterruptedException {
125 | logger.info("相机正在向上...");
126 | for (int i = 0; i < 20; i++) {
127 | gpio00.high();
128 |
129 | long highStart = System.nanoTime();
130 | long highEnd;
131 | do {
132 | highEnd = System.nanoTime();
133 | } while (highEnd - highStart < 50000);
134 |
135 | gpio00.low();
136 |
137 | long lowStart = System.nanoTime();
138 | long lowEnd;
139 | do {
140 | lowEnd = System.nanoTime();
141 | } while (lowEnd - lowStart < 1860000);
142 | }
143 | }
144 |
145 | /**
146 | * 相机向下
147 | *
148 | * @throws InterruptedException
149 | */
150 | public void turnDown() throws InterruptedException {
151 | logger.info("相机正在向下...");
152 | for (int i = 0; i < 20; i++) {
153 | gpio00.high();
154 |
155 | long highStart = System.nanoTime();
156 | long highEnd;
157 | do {
158 | highEnd = System.nanoTime();
159 | } while (highEnd - highStart < 2100000);
160 |
161 | gpio00.low();
162 |
163 | long lowStart = System.nanoTime();
164 | long lowEnd;
165 | do {
166 | lowEnd = System.nanoTime();
167 | } while (lowEnd - lowStart < 1700000);
168 | }
169 | }
170 |
171 | }
172 |
--------------------------------------------------------------------------------
/src/main/resources/application-dev.yml:
--------------------------------------------------------------------------------
1 | server:
2 | port: 8080
3 |
4 | # 相机相关配置: 音视频设备驱动号, 录制帧率, 录制画面宽高, 录制画面中心区域坐标以及边界线, 屏幕截图保存位置
5 | camera:
6 | audio:
7 | device: 4
8 | video:
9 | device: 0
10 | rate: 30
11 | width: 1280
12 | height: 720
13 | center:
14 | leftLine: 400
15 | rightLine: 880
16 | topLine: 225
17 | downLine: 495
18 | pointX: 640
19 | pointY: 360
20 | screen:
21 | path: /temp.jpg
22 | # 流媒体服务器配置: 推流地址, 通知流媒体开始/停止录制接口, 录制视频回放地址
23 | rtmp:
24 | push:
25 | # rtmp://ip:port/rtmpApplicationName/streamName 或者 本地文件
26 | url: C:\output.mp4
27 | record:
28 | # rtmp开始/停止录制api
29 | start: http://ip:port/path?app=rtmpApplicationName&name=streamName&rec=all
30 | stop: http://ip:port/path?app=rtmpApplicationName&name=streamName&rec=all
31 | history:
32 | prefix: http://ip:port/path
33 |
34 | # SMTP服务器配置(此处配置为QQ邮箱)
35 | spring:
36 | mail:
37 | host: smtp.qq.com
38 | username: xxxxxxxxx@qq.com
39 | password: password
40 | port: 587
41 | default-encoding: UTF-8
42 | properties:
43 | mail:
44 | smtp:
45 | socketFactoryClass: javax.net.ssl.SSLSocketFactory
46 | debug: false
--------------------------------------------------------------------------------