最近工作中接触到了视频监控这块东西,最开始使用的是海康摄像头,因为有文档还有技术对接,做起来基本还算顺利,但是后来需求要求支持usb摄像头,最开始百度了一圈,用啥opencv javacv之类的技术,感觉都挺麻烦的,最终发现了FFMPEG可以很好的解决业务中的问题,在这里记录一下
(ps:请先切换到ffmpeg的bin目录下打开cmd命令)
命令:
ffmpeg -list_devices true -f dshow -i dummy
在java 项目中可以通过返回的errorStream进行业务的逻辑划分,注意,,如果是cmd命令执行,可以ffmpeg开头然后敲回车就可以,如果是java 可以带上exe后缀,会自动执行。
private static String ffmpegEXE = "C:/ffmpeg/bin/ffmpeg.exe";
public static Boolean haveUsbCamera() {
List command = new ArrayList();
command.add(ffmpegEXE);
command.add("-list_devices");
command.add("true");
command.add("-f");
command.add("dshow");
command.add("-i");
command.add("dummy");
ProcessBuilder p = new ProcessBuilder();
p.command(command);
Process start = null;
try {
start = p.start();
} catch (IOException e) {
log.error("启动进程错误", e);
}
try(BufferedReader readers = new BufferedReader(new InputStreamReader(start.getErrorStream()))) {
String lines = null;
while((lines = readers.readLine())!=null) {
if (lines.contains("Lanseyaoji Webcam")) {
log.info("查询到usb摄像头!");
return true;
}
}
}catch (Exception e) {
log.error("获取进程信息错误", e);
}
log.info("未查询到usb摄像头!");
return false;
}
2 调取摄像头并录屏
获取到设备信息后,拿到设备名称再执行命令进行录屏
命令:
ffmpeg -f dshow -i video="Lanseyaoji Webcam" -f dshow -i audio="麦克风 (Lanseyaoji USB2.0 MIC)" -vcodec libx264 -acodec aac -strict -2 output.mp4
执行命令后会使用指定的录屏和录音设备开始录制视频,输出的位置可以指定绝对路径,如果不是绝对路径会存放在ffmpeg的bin目录下
值得注意的是有时候是编码的速度太慢导致过多的数据保存在缓存内,缓存区满了就会报错,所以需要处理一下(百度来的,我也木有太理解这话啥意思。。。),使用exec.getOutputStream可以获取输出流,然后再执行对应的命令即可
public static Process exec = null;
@SneakyThrows
private static void doStartCamera(String fileName) {
String cmd = ffmpegEXE + " -f dshow -i video=\"Lanseyaoji Webcam\" -f dshow -i audio=\"麦克风 (Lanseyaoji USB2.0 MIC)\" -vcodec libx264 -acodec aac -strict -2 " + fileName;
List command = Arrays.asList(StringUtils.split(cmd, " "));
ProcessBuilder p = new ProcessBuilder();
p.command(command);
try {
exec = p.start();
} catch (IOException e) {
log.info("启动进程失败", e);
}
try (BufferedReader readers = new BufferedReader(new InputStreamReader(exec.getErrorStream()))){
String lines1 = null;
while((lines1 = readers.readLine())!=null) {
log.info("error lines:"+lines1);
// 有时候编码的速度太慢导致过多的图片数据保存在缓存内,缓存区空间太小或满了就会报错
// 需要手动确认一下才能继续
// 提示信息:xxxtoo full or near too fullxxx
if (lines1.contains("[y/N]")) {
executeCommand("y\n");
}
}
}catch (Exception e) {
log.error("获取进程信息错误", e);
}
}
private static boolean executeCommand(String command) {
try {
OutputStream outputStream = exec.getOutputStream();
outputStream.write(command.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
} catch (Exception e) {
log.error("执行命令{}错误", command, e);
return false;
}
return true;
}
这个方法时开始录制,后台会一直打印录制信息,包括时间、大小等信息,等待程序给出停止指令
public static boolean endCamera() {
boolean b = executeCommand("q\n");
try {
// 睡眠几秒,让视频流完全写入到视频文件,否则可能会导致文件不可用
Thread.sleep(3000);
} catch (Exception e) {
}
return b;
}
至此,一个视频文件就会产生在指定的路径中了
3 添加水印
现在应该已经下载好一个视频了,但是有时候有需求要添加水印,找过几个命令要么就产生的水印半天才走一秒,要么提示命令不正确,参数不正确,在疯狂百度下,终于找到了一个命令
public static void main(String[] args) {
Map map = new HashMap<>();
map.put(0, "000");
map.put(2, "222");
map.put(4, "444");
map.put(8, "888");
map.put(75, "");
Calendar instance = Calendar.getInstance();
instance.set(Calendar.YEAR, 2021);
instance.set(Calendar.MONTH, 8);
instance.set(Calendar.DAY_OF_MONTH, 23);
instance.set(Calendar.HOUR_OF_DAY, 15);
instance.set(Calendar.MINUTE, 23);
instance.set(Calendar.SECOND, 23);
addWatermark("C:/ffmpeg/bin/1.mp4", map, instance.getTime());
}
/**
* 添加水印
* @param fileName 原视频文件绝对路径
* @param secondsMap 时间轴和水印信息
* @param startTime 时间水印起始时间
* @return 新视频文件绝对路径
*/
public static String addWatermark(String fileName, Map secondsMap, Date startTime) {
//ffmpeg -i 1.mp4 -vf
// "drawtext=fontfile=simhei.ttf: text='I_CODE\:YT777':x='if(gte(t,3),if(lte(t,8),0,NAN),NAN)':y=35:fontsize=24:fontcolor=white:shadowy=2,
// drawtext=fontfile=simhei.ttf: text='I_CODE\:YT888':x='if(gte(t,0),if(lte(t,8),0,NAN),NAN)':y=15:fontsize=24:fontcolor=white:shadowy=2"
// output1.mp4
// 水印命令
StringBuilder command = new StringBuilder(ffmpegEXE + " -i " + fileName + " -vf \"");
// 垂直位置
int y = 10;
// 拼接水印命令
ArrayList integers = new ArrayList<>(secondsMap.keySet());
Collections.sort(integers);
// 所有水印都要持续到视频最后
Integer lastTime = integers.get(integers.size() - 1);
for (int i = 0; i < integers.size() - 1; i++) {
String s = secondsMap.get(integers.get(i));
if (i < integers.size() - 1) {
command.append("drawtext=fontfile=simhei.ttf:text='" + s + "':x='if(gte(t," + integers.get(i) + "),if(lte(t," + lastTime + "),0,NAN),NAN)':y=" + y + ":fontsize=24:fontcolor=white:shadowy=2");
}
if (i < integers.size() - 2) {
command.append(",");
}
y = y + 20;
}
// 模拟添加时间水印
// 注意第一个要添加逗号
Calendar calendar = Calendar.getInstance();
calendar.setTime(startTime);
long ll = calendar.getTimeInMillis() / 1000;
System.out.println(ll);
command.append(",drawtext='fontfile=simhei.ttf:fontsize=24:x=w-tw:y=10:fontcolor=white:text=%{pts\\: localtime\\:").append(ll).append("}'");
String newFileName = getNewFileName(fileName);
command.append("\" ").append(newFileName);
ProcessBuilder p = new ProcessBuilder();
p.command(command.toString().split(" "));
Process start = null;
try {
start = p.start();
} catch (IOException e) {
log.error("启动进程错误", e);
}
try(BufferedReader readers = new BufferedReader(new InputStreamReader(start.getErrorStream()))) {
String lines = null;
while((lines = readers.readLine())!=null) {
log.info("error lines:"+lines);
if (lines.contains("[y/N]")) {
executeCommand("y\n");
}
}
}catch (Exception e) {
log.error("获取进程信息错误", e);
}
return newFileName;
}
/**
* 重命名文件的时间戳
* @param fileName
* @return
*/
private static String getNewFileName(String fileName) {
String path = fileName.substring(0, fileName.lastIndexOf("/"));
String name = fileName.substring(fileName.lastIndexOf("/"), fileName.lastIndexOf("."));
String[] s = name.split("-");
return path + s[0] + "-" + System.currentTimeMillis() + ".mp4";
}
命令的格式是这样的:
ffmpeg -i 1.mp4 -vf
"drawtext=fontfile=simhei.ttf: text='I_CODE\:YT777':x='if(gte(t,3),if(lte(t,8),0,NAN),NAN)':y=35:fontsize=24:fontcolor=white:shadowy=2,
drawtext=fontfile=simhei.ttf: text='I_CODE\:YT888':x='if(gte(t,0),if(lte(t,8),0,NAN),NAN)':y=15:fontsize=24:fontcolor=white:shadowy=2" output1.mp4
解释一下参数map的格式,是一个时间轴信息,意思是第0秒开始显示000,第二秒开始显示222,以此类推,最后一个key表示这个视频的有多长,单位为秒(这个值需要在自己的业务逻辑中获取到,如果获取不到实际时长,应该也可以设置的非常大来达到同样的效果),这个值主要是在命令中控制水印可以显示多久
startTime是视频的时间水印从什么时候开始,最后需要转化为秒钟即可
添加水印是中间的drawtext命令,一次性添加多个水印使用逗号隔开,由于每个属性使用冒号隔开的,所以添加的文字水印中需要显示冒号的话要用反斜杠转义(\:)
text:文字水印的内容,记得要转义冒号
x:相对于左上角,显示的横坐标,if(gte(t,0),if(lte(t,3),w-tw,NAN),NAN)意思就是大于0秒和小于3秒时显示,大于3秒时消失,根据需求拼接多条水印命令即可
y:相对于左上角,显示的纵坐标
这里的属性代表的含义自己百度一下吧 ,最后执行一下就可以了