Java使用Vlcj进行屏幕录制

需求

使用Java编写GUI程序完成简单的屏幕录制操作,支持开始录屏、结束录屏、销毁本次录制等操作。

解决

添加主要依赖

1
2
3
4
5
<dependency>
<groupId>uk.co.caprica</groupId>
<artifactId>vlcj</artifactId>
<version>4.2.0</version>
</dependency>

关键代码

1、Application.java(项目的入口)

1
2
3
4
5
6
public class Application {
public static void main(String[] args) {
// 其中 destination 为 录制完成后视频文件存放的目录
SwingUtilities.invokeLater(() -> new ScreenRecorder(destination));
}
}

2、ScreenRecorder.java(GUI 及 录屏 主要逻辑)

程序 包含3个按钮,开始、停止、销毁;2个操作目录,录制完成后视频文件存放的目录A(vlcj使用) 及 待处理业务目录B(自身业务使用)。

一般流程是 先点击 开始,此时 目录A下会 多一个视频文件,录制一段视频,点击停止,程序会将目录A下的视频移至 目录B下,此时可进行后续业务处理。因为 开始录制时目录A就会创建文件,导致目录A下文件可能不是最终文件(录制未完成),所以目录B就有存在的意义,程序关闭再启动后也可遍历 目录B下的文件 处理未完成的业务。

销毁按钮用于 开始录制但未停止时想放弃本次录制,注意停止后再销毁无效。

录制中关闭程序,则保存本次录制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
51
52
53
54
55
56
57
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class ScreenRecorder {
private static final String[] OPTIONS = {
"--quiet",
"--quiet-synchro",
"--intf",
"dummy"
};
private static String filePath = "";
private static final String MRL = "screen://";
private static final String SOUT = ":sout=#transcode{vcodec=mp4v,acodec=mpga,vb=4096,ab=512}:duplicate{dst=file{dst=%s}}";
private static final String FPS = ":screen-fps=20";
private static final String CACHING = ":screen-caching=500";
private final MediaPlayerFactory mediaPlayerFactory;
private final MediaPlayer mediaPlayer;
private JFrame frame;
private static JLabel countLabel;
private static JTextField nameField;
public ScreenRecorder(final String destination) {
mediaPlayerFactory = new MediaPlayerFactory(OPTIONS);
mediaPlayer = mediaPlayerFactory.mediaPlayers().newMediaPlayer();
JPanel cp = new JPanel();
nameField = new JTextField(5);
JButton recordButton = new JButton("开始");
JButton stopButton = new JButton("停止");
JButton delButton = new JButton("销毁");
cp.add(recordButton);
cp.add(stopButton);
cp.add(delButton);
frame = new JFrame("录屏程序");
// 图标化
// frame.setExtendedState(JFrame.ICONIFIED);
frame.setContentPane(cp);
frame.setLocation(10, 10);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
release();
}
});
recordButton.addActionListener(e -> go(destination));
stopButton.addActionListener(e -> stop());
delButton.addActionListener(e -> delete());
frame.setVisible(true);
}
private void go(String destination) {
State state = mediaPlayer.status().state();
filePath = destination + getUUID() + ".mp4";
mediaPlayer.media().play(MRL, getMediaOptions(filePath));
updateTitle("录屏中");
}
private void stop() {
State state = mediaPlayer.status().state();
mediaPlayer.controls().stop();
if (!state.equals(State.STOPPED) && !state.equals(State.NOTHING_SPECIAL)) {
// 将 目录A下的文件 移动到 目录B
FileUtil.moveFile(filePath, waitSendPath);
filePath = "";
}
updateTitle("停止");
}
private void delete() {
mediaPlayer.controls().stop();
if (!"".equals(filePath)) {
FileUtil.delFile(filePath);
}
updateTitle("停止");
}
private void release() {
State state = mediaPlayer.status().state();
if (state.equals(State.PLAYING)) {
// 如果关闭时程序录制中,则保存本次录制
stop();
}
log.info("status:" + state);
mediaPlayer.release();
mediaPlayerFactory.release();
}
private void updateTitle(String title) {
frame.setTitle("录屏程序:" + title);
}
private String[] getMediaOptions(String destination) {
String result = String.format(SOUT, destination);
return new String[] {
result,
FPS,
CACHING
};
}
}
注意
  1. vlcj好像不能处理好windows下的中文文件名乱码的问题(创建的中文文件名乱码),mac下没问题,可以定义map存放 目录A下的随机非中文文件名 及 想要的中文文件名,在将文件从目录A移动到目录B后使用中文文件名。
  2. 虽然视频文件是以.mp4结尾,且视频文件能用一般的播放器打开,但文件应该不是标准的mp4,使用浏览器无法直接播放。我对视频编码这里不了解,因此也没有进行后续研究(有更好的解决办法欢迎留言交流),后续业务采用阿里云进行视频转码,转换后的mp4可在浏览器中直接打开。
  3. 暂不支持声音录制。
  4. 可使用Java监听 目录B 文件变化来实现后续业务 Java监听文件变化

界面效果

Java Vlcj录屏程序最终效果

vue nuxt window is not defined 或 document is not defined错误

问题

vue nuxt 中出现 window is not defined 或 document is not defined 错误。

原因

vue服务端渲染ssr使用了客户端的一些对象,即代码中使用了window对象或document对象,可能是由于引入的第三方组件中操作了这些对象,也可能是自己的代码使用了它们。

解决

1、由于引入部分第三方组件导致的错误可以通过配置插件ssr为false来解决。

1
2
3
4
5
6
7
...
plugins: [
...
{ src: '~/plugins/xxx', ssr: false },
...
]
...

2、由于自己操作window对象在网上找了半天都没有解决办法
基本都是第三方组件的方案或者是官网上下面的无效方案

1
2
3
4
// 截止2019-06-02 亲测无效
if (process.client) {
require('external_library')
}

终于在缩小搜索时间后找到了别人的解决方案。
在.vue文件中的window上操作的代码外加一层判断,和官网的方案其实差不多,就是官网的require是什么鬼,容易误解!!!也是我自己笨!!

1
2
3
4
// 正确的使用办法
if (process.client) {
window.xxx
}

特别小的问题,花了较多时间,遂记录一下。

参考:
Nuxt中关于window or document is not defined的问题总结

npm检查依赖最新版本并升级

使用 npm-check-updates 检查依赖并升级。

1.安装 npm-check-updates

1
$ npm install npm-check-updates -g

2.查看package.json中依赖的最新版本

1
$ ncu

执行结果:

1
2
3
4
5
6
7
8
9
10
11
Using /Users/xxx/package.json
⸨░░░░░░░░░░░░░░░░░░⸩ ⠸ :
mobx ^3.3.1 → ^5.5.0
mobx-react ^4.2.2 → ^5.2.8
The following dependencies are satisfied by their declared version range, but the installed versions are behind. You can install the latest versions without modifying your package file by using npm update. If you want to update the dependencies in your package file anyway, run ncu -a.
antd ^3.1.2 → ^3.10.1
node-sass ^4.6.1 → ^4.9.3
Run ncu with -u to upgrade package.json

3.更新package.json依赖到最新版本

1
$ ncu -a

4.执行npm install 完成新依赖安装升级

1
$ npm install

5.完成升级

注意前端发展日新月异,版本间有时差距较大,不建议做无准备的升级。

Linux系统用户权限命令小记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 创建用户组 dev
$ groupadd dev
# root用户 添加 附加组dev
$ usermod -G dev root
# 指定目录或文件 删除其他人read权限
$ sudo chmod o-r config
# 指定目录或文件拥有组改为 dev
$ sudo chown dev account/
# 创建test用户并添加到 dev用户组
$ useradd -g dev test
# 更新test用户密码
$ passwd test

err_incomplete_chunked_encoding错误

问题

项目使用ajax请求json数据大量返回值下,浏览器可能会报 err_incomplete_chunked_encoding 错误。

解决

项目使用了nginx,在大量返回值的情况下,nginx需要使用到临时文件,有的时候上述错误是由 nginx权限不足导致的,可参考nginx日志配置对应目录权限。

本项目由于返回值过大,需更新nginx配置,proxy_max_temp_file_size设个合适的较大的值,或直接设为0,关闭硬盘缓冲。

1
2
3
server {
proxy_max_temp_file_size 0;
}

问题解决。

禁止Chrome强制Https跳转

问题

场景1:
网站原先使用了https证书,现在不再使用,chrome打开网站会进行https跳转,导致异常。

场景2:
主域名配置单个https证书,其二级域名未配置正式,chrome可能会对二级域名进行https跳转。

此时chrome会有如下提示:

https跳转提示您的连接不上私密连接

解决

chrome的地址栏输入:

1
chrome://net-internals/#hsts

Delete domain security policies 中输入 不想自动https跳转的地址,然后点击“delete”按钮,即可完成配置。

chrome删除https跳转

当然这个配置的前提是 网站原来就支持http且未开启https强制跳转,主要可以解决主域名有https证书但二级域名没有,访问二级域名自动跳转https的问题,像本站 blog.gelu.me,做了https强制跳转,就算删除后http请求也会再次重定向到https。

mysql性能优化-limit分页

问题

平台在主数据量不大的情况下遇到了MySQL limit较大偏移量分页查询极慢的情况。

分析解决

该查询语句存在的一些问题

问题1:

查询使用的 select ,虽然知道从性能角度来说应该用什么查什么,但项目非大数据类的项目,可见项目生命周期内也不会有超大量数据,因此为加快开发效率还是使用了select

问题2:

表中存在部分大字段,limit m, n 使得结果集查询了 m + n 的数据,并将 m 以前的数据抛弃。配合第一个问题,导致了 m 较大时查询非常慢。

实际项目中,这些字段当前也没有了使用需求,因此予以删除,基本解决了查询慢的问题。同时该表字段过多,近100个,会在后续的改版优化中进行拆分。

因为遇到了分页查询慢的问题,顺便整理收集了一下真正大量数据下的分页优化方案。

解决方案

1
select * from table_name inner join ( select id from table_name where xxx=yyy limit 200000,10) b using (id)

还有一些缩减查询范围 和 使用子查询的一些方法,有诸多限制,因此就没有记录。

当然有些时候大量数据查询场景可以避免,上家公司千万级的数据就通过归档数据减少数据库压力,也可以缩减业务查询范围避免大量数据查询。

Gitlab升级迁移小记

由于历史操作原因,Gitlab服务器与Jenkins服务器位于不同大区的ECS服务器中,拉取代码较慢,新服务器采购时正好做个迁移,将2个服务器配置在一个内网中。

原服务器系统CentOS,Gitlab版本 10.2.4,新服务器系统Ubuntu,Gitlab版本。

登录新服务器配置SSH,方便以后的登录。

可参考 SSH登录配置

在新服务器上安装Gitlab

参考 官网命令进行 新服务器Gitlab安装 https://about.gitlab.com/installation/#ubuntu?version=ce

1
2
3
4
5
6
7
8
9
apt update
sudo apt-get install -y curl openssh-server ca-certificates
sudo apt-get install -y postfix
curl -sS https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash
sudo EXTERNAL_URL="https://xxx.xxxx.com" apt-get install gitlab-ce

升级原服务器Gitlab版本。

由于Gitlab迁移要求 版本号相同,因此对于原服务器Gitlab需要做个升级。

下载新包

https://packages.gitlab.com/gitlab/gitlab-ce
https://packages.gitlab.com/gitlab/gitlab-ce/packages/el/7/gitlab-ce-11.0.3-ce.0.el7.x86_64.rpm

关闭部分gitlab服务

1
2
3
gitlab-ctl stop unicorn
gitlab-ctl stop sidekiq
gitlab-ctl stop nginx

Gitlab备份

1
gitlab-rake gitlab:backup:create

升级

1
2
3
4
5
6
7
8
rpm -Uvh gitlab-ce-11.0.3-ce.0.el7.x86_64.rpm
# 参考
# Ubuntu/Debian系统:
# sudo dpkg -i gitlab_x.x.x-omnibus.xxx.deb
# CentOS系统:
# sudo rpm -Uvh gitlab-x.x.x_xxx.rpm

gitlab跨版本升级错误

提示版本跨度过大无法升级,需按路径依次升级。

原Gitlab版本10.x,找了10版本下最大版本10.8.4,按照上面的升级流程重新走了一遍。

升级10成功。

Gitlab升级10.8.4成功

重新配置gitlab

1
gitlab-ctl reconfigure

重启gitlab

1
gitlab-ctl restart

启动以后500错误。

10.8.4 500错误

查看 日志 /var/log/gitlab/gitlab-rails/production.log

500日志错误

这个错误没有找到好的解决办法。

1
2
3
4
5
6
7
8
# 尝试直接修改表结构
$ gitlab-rails dbconsole
$ alter table issues add column deleted_at timestamp;
# 后续错误依次修改
$ alter table merge_requests add column deleted_at timestamp;
$ alter table namespaces add column deleted_at timestamp;

然后一般页面500错误消失,但退出登录会出现500错误,简直奔溃。

迁移还原

原服务器弄的不完美,直接将升完级的后再备份的文件转移至新服务器备份文件夹下,Gitlab备份文件夹目录/var/opt/gitlab/backups/,还原成功。

1
2
3
4
5
6
7
8
# 备份文件名 1531211711_2018_07_10_11.0.3_gitlab_backup.tar
$ cd /var/opt/gitlab/backups/
# 还原时出现 这个错误 tar: Error is not recoverable: exiting now,权限不足
$ chown git 1531211711_2018_07_10_11.0.3_gitlab_backup.tar
# 还原,还原成功。
$ sudo gitlab-rake gitlab:backup:restore BACKUP=1531211711_2018_07_10_11.0.3

建议谨慎升级,做好备份,实在不行还原到原版本,迁移的话也可以到同版本升级。

本次迁移的下下策是重新创建仓库、用户。因为用户及仓库数量也就十来个,不算多,也算有恃无恐。

Thymeleaf List指定数量循环

问题

项目使用Thymeleaf模板,后端查询10条数据,但根据不同页面可能会显示其中3条或5条。因此有了List指定数量循环的需求,数量由模板决定,而非后端。

解决

不知道是不是搜索关键字的问题还是问题太简单的原因,网上找到一些都是这种

1
${#numbers.sequence(0,3)}

通过Thymeleaf numbers函数进行截取的解决方案。此方案对本项目不适用,因为它要求传入的数据模型就是List,函数会直接截取该List,而项目使用的是更复杂的Map对象。
看了一下Thymeleaf文档,可以使用List的状态变量加以判断来解决这种问题。

1
2
3
4
<div th:text="${title}">标题</div>
<div class="column" th:each="item,itemStat : ${testList}" th:if="${itemStat.count}<=3">
<div th:text="${item.name}"></div>
</div>

以上代码使得无论testList实际有多长,最多只有前3个会被使用到。

其中th:each第二个变量就是迭代的状态变量,本文命名的是itemStat。状态变量有如下属性:
index: 当前迭代索引,从0开始。
count: 当前迭代索引,从1开始。
size: 迭代变量中的元素总数。
current: 每个迭代的iter变量, 和本文中的item一样。
even/odd: 当前迭代是偶数还是奇数,布尔值。
first: 在当前的迭代中是否是第一个,布尔值。
last: 在当前的迭代中是否是最后一个,布尔值。

使用 th:if 加 count比较 即可约束后续内容显示。

Thymeleaf相关链接:

Thymeleaf SAXParseException
Thymeleaf实现页面静态化
Thymeleaf TemplateProcessingException
Thymeleaf比较判断枚举类型

MyBatis查询无数据

问题

项目使用了Mybatis-PageHelper,项目有业务需要在指定范围(指定时间范围指定条数)内的数据里做查询,因此有了下面的Mybatis代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<select id="list" resultMap="TotalResultMap" parameterType="com.yl.dao.query.YlCaseListQuery">
select allData.* from (
select yce.*,yc.patient_id
from yl_a yce left join yl_c yc on yc.id = yce.case_id where 1=1
<if test="ctimeStart!=null">
and DATE_FORMAT(yce.ctime, '%Y-%m-%d') <![CDATA[>=]]> DATE_FORMAT(#{ctimeStart}, '%Y-%m-%d')
</if>
<if test="ctimeEnd!=null">
and DATE_FORMAT(yce.ctime, '%Y-%m-%d') <![CDATA[<=]]> DATE_FORMAT(#{ctimeEnd}, '%Y-%m-%d')
</if>
order by yce.ctime desc limit 200) as allData
<where>
<if test="patientId!=null">
and allData.patient_id = #{patientId}
</if>
</where>
</select>

当不使用patientId查询条件时没有问题,查询能正常查询出200条记录。
但添加查询条件后数据可能查询不出来。当时的情况是测试环境测试了一下可以查询,生产环境没有结果。

解决

当前看了好久没找到问题,后来打开生产环境的sql打印。发现mybatis-pagehelper在select count(0)查询时会默认将sql语句中的order by去掉,也就是count查询时上面的子查询没有order by排序但有limit 200, 有无排序的数据集是不一样的,此时查询出来的新的数据集确实没有包含对应patientId记录的话,count(0)值就是0。

因为查询前会count查询一下判断是否存在数据,再决定是否进行实际查询,count结果为0就直接导致了查询无数据。

查看Mybatis-PageHelper文档,在SQL中包含

1
/*keep orderby*/

注释时,count查询时不会移出order by。

因此更新代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<select id="list" resultMap="TotalResultMap" parameterType="com.yl.dao.query.YlCaseListQuery">
select allData.* from (
select yce.*,yc.patient_id
from yl_a yce left join yl_c yc on yc.id = yce.case_id where 1=1
<if test="ctimeStart!=null">
and DATE_FORMAT(yce.ctime, '%Y-%m-%d') <![CDATA[>=]]> DATE_FORMAT(#{ctimeStart}, '%Y-%m-%d')
</if>
<if test="ctimeEnd!=null">
and DATE_FORMAT(yce.ctime, '%Y-%m-%d') <![CDATA[<=]]> DATE_FORMAT(#{ctimeEnd}, '%Y-%m-%d')
</if>
order by yce.ctime desc limit 200) as allData /*keep orderby*/
<where>
<if test="patientId!=null">
and allData.patient_id = #{patientId}
</if>
</where>
</select>

此时,count查询是order by没被删除,根据patientId查询成功。