在工作之中,遇到这样的一个场景,在录制教学视频(只有系统声音)时,由于其他原因无法录制音频,后期需要进行视频音频的合成,当时的想法是调用各大厂的语音合成接口进行文字转音频的合成。音频有了,接下来就是把音频以及在视频中的时间点发给视频剪辑的同事,一个一个音频进行加入,最后发现这种重复的工作效率很低,就直接使用ffmpeg进行音视频处理,要求:视频按照指定时间点插入音频,添加片头片尾,添加背景音乐
首先通过excel将所有的要合成的文本以及其对应的时间点读取,我的excel文件类似这样的。
public HashMap<String,List<String>> readXls(String path) throws Exception {
InputStream is = new FileInputStream(path);
// HSSFWorkbook 标识整个excel
HSSFWorkbook hssfWorkbook = new HSSFWorkbook(is);
HashMap<String,List<String>> result = new HashMap<String, List<String>>();
int size = hssfWorkbook.getNumberOfSheets();
// 循环每一页,并处理当前循环页
HSSFSheet hssfSheet = hssfWorkbook.getSheetAt(0);
List<String> rowList = new ArrayList<String>();
List<String> rowList2 = new ArrayList<String>();
List<String> rowList3 = new ArrayList<String>();
// 处理当前页,循环读取每一行
for (int rowNum = 1; rowNum <= hssfSheet.getLastRowNum(); rowNum++) {
// HSSFRow表示行
HSSFRow hssfRow = hssfSheet.getRow(rowNum);
int minColIx = hssfRow.getFirstCellNum();
int maxColIx = hssfRow.getLastCellNum();
// 遍历改行,获取处理每个cell元素
HSSFCell cell = hssfRow.getCell(0);
if (cell != null) {
rowList.add(getStringVal(cell));
}
HSSFCell cell2 = hssfRow.getCell(1);
if (cell2 != null) {
rowList2.add(getStringVal(cell2));
}
HSSFCell cell3 = hssfRow.getCell(2);
if (cell3 != null) {
rowList3.add(getStringVal(cell3));
}
}
result.put("time",rowList);
result.put("text",rowList2);
result.put("name",rowList3);
return result;
}
在读取数据后,使用阿里语音合成接口将这些文本,转换为音频文件,并且以时间点命名;
/**
* 通过阿里巴巴语音合成进行单线程语音合成
* @param list_time
* @param list_text
* @param list_name
* @throws InterruptedException
*/
public static void buildWavByAlibaba(List<String> list_time,List<String> list_text,List<String> list_name,String title) throws InterruptedException {
String appKey = ""; //在阿里巴巴控制台中输入自己的appKey
String id = ""; //自己的id
String secret = ""; //自己的秘钥
String url = ""; // 默认即可,默认值:wss://nls-gateway.cn-shanghai.aliyuncs.com/ws/v1
AlibabaTts alibabaTts = new AlibabaTts(appKey, id, secret, url);
for(int i=0;i<list_time.size();i++){
alibabaTts.process(list_name.get(i).toString(),list_time.get(i).toString(),list_text.get(i).toString());
Thread.sleep(1000);
}
alibabaTts.process(list_name.get(0).toString(),"title-",title);
alibabaTts.shutdown();
}
上面的是单线程生成音频文件,当然也可以多线程,只是阿里语音合成非付费版的最高并发只有2,多线程会出现合成失败的情况;
首先需要安装ffmpeg,安装过程可自行百度。现在我们的需求就是根据第一步获取的excel中的数据以及合成的音频文件对视频进行处理,同时加上片头片尾;
在进行插入之前,我先将所有的音频文件进行了音量的增强;
// 传入的是音频文件存放的绝对路径的集合
public static void addVoiceToWav(List<String> paths){
for(String path:paths){
if(path.indexOf("title")>=0){
continue;
}
String cmd = "-i "+path+" -vcodec copy -af \"volume=23dB\" "+path.replaceAll("_","-");
System.out.println(cmd);
try {
Runtime run = Runtime.getRuntime();
// run.exec("cmd /k start cmd.exe");
Process process = run.exec("cmd /c start D:\\ffmpeg\\bin\\ffmpeg.exe "+cmd.toString());
int rsource = process.waitFor();
InputStream stderr=process.getInputStream();
InputStreamReader isr=new InputStreamReader(stderr,"GBK");
BufferedReader br=new BufferedReader(isr);
String line=null;
System.out.println("-------------正在进行音频增强----------"+path);
while((line=br.readLine())!=null){
System.out.println(line);
}
System.out.println("cmd执行结果:"+rsource);
}catch (Exception e){
e.printStackTrace();
}
}
}
紧接着是将处理过的音频文件按照指定的时间点加入到视频中;其中具体的插入时间点是以音频的文件名来辨别;
/**
* 将文件名转换为时间,毫秒为单位
* @param path
* @return
*/
public static int converTime(String path){
String fileName = path.substring(path.length()-12,path.length()-4);
System.out.println(fileName);
if(path.indexOf("title")>=0){
return 0;
}else {
String[] times = fileName.split("-");
int hour = Integer.parseInt(times[0]);
int minute = Integer.parseInt(times[1]);
int second = Integer.parseInt(times[2]);
return (hour*60*60+minute*60+second)*1000;
}
}
接下来就可以进行音频和视频的合并;
public static String videoAddWav(String mp4Path, List<String> wav_list) throws UnsupportedEncodingException {
addVoiceToWav(wav_list);
//拼接ffmpeg的执行命令
StringBuffer cmd = new StringBuffer(" -i "+mp4Path);
StringBuffer cmd2 = new StringBuffer(" -filter_complex \"");
StringBuffer cmd3 = new StringBuffer("[0]");
for(int i=0;i<wav_list.size();i++){
String wav = wav_list.get(i).replaceAll("_","-");
cmd.append(" -i "+wav);
int timeOut = converTime(wav);
cmd2.append("["+(i+1)+"]adelay="+timeOut+"|"+timeOut+"[aud"+(i+1)+"];");
cmd3.append("[aud"+(i+1)+"]");
}
cmd3.append("amix="+(wav_list.size()+1)+"\"");
cmd2.append(cmd3);
cmd.append(cmd2);
String newFile = mp4Path.substring(0,mp4Path.lastIndexOf("."))+"(finish).mp4";
cmd.append(" -c:v copy "+newFile);
try {
Runtime run = Runtime.getRuntime();
// run.exec("cmd /k start cmd.exe");
Process process = run.exec("cmd /c start D:\\ffmpeg\\bin\\ffmpeg.exe "+cmd.toString());
int rsource = process.waitFor();
InputStream stderr=process.getInputStream();
InputStreamReader isr=new InputStreamReader(stderr,"GBK");
BufferedReader br=new BufferedReader(isr);
String line=null;
System.out.println("-------------正在进行主体视频与音频融合----------");
while((line=br.readLine())!=null){
System.out.println(line);
}
System.out.println("cmd执行结果:"+rsource);
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
for(int i=0;i<wav_list.size();i++){
String wav = wav_list.get(i).replaceAll("_","-");
if(new File(wav_list.get(i)).exists()){
new File(wav_list.get(i)).delete();
}
if(new File(wav).exists()){
new File(wav).delete();
}
}
if(mp4Path.indexOf("temp")>=0){
new File(mp4Path).delete();
}
return mp4Path.substring(0,mp4Path.lastIndexOf("."))+"(finish).mp4";
}
现在音频已经合成到视频之中,接下来就是合成片头片尾,添加背景音乐。
首先片头片尾文件时制作好的,我们需要将他们转换为ts格式,可以使用ffmpeg命令:
ffmpeg -i start.mp4 -c:v libx264 -vf scale=1920:1080 -r 24 -c:a aac -ar 48000 -b:a 160k -strict experimental -f mpegts start.ts
同时需要根据此视频的对应的标题生成一张图片转换为视频,加入到片头后面(我这里的标题是excel文件的文件名)
//这里的text是标题内容,name是excel中的任务名称
public static String createImgVideo(String text,String name) throws Exception {
String name_temp = System.currentTimeMillis()+"";
CreateImgUtil.createImage(text, new Font("宋体", Font.BOLD, 50), new File(
workSpace+"img\\"+name_temp+".png"), 1920, 1078);
Runtime run = Runtime.getRuntime();
String cmd =" -f image2 -loop 1 -i "+workSpace+"img\\"+name_temp+".png"+" -i "+workSpace+"\\backgroundWav\\1.wav -pix_fmt yuv420p -vcodec libx264 -b:v 600k -r:v 15 -preset medium -crf 30 -s 1920x1078 -vframes 250 -r 15 -t 9 "+workSpace+"img\\"+name_temp+"temp.mp4";
System.out.println(cmd);
Process process = run.exec("cmd /c start D:\\ffmpeg\\bin\\ffmpeg.exe "+cmd.toString());
int rsource = process.waitFor();
InputStream stderr=process.getInputStream();
InputStreamReader isr=new InputStreamReader(stderr,"GBK");
BufferedReader br=new BufferedReader(isr);
String line=null;
System.out.println("-------------正在生成由标题图片转为的视频----------");
while((line=br.readLine())!=null){
System.out.println(line);
}
System.out.println("cmd执行结果:"+rsource);
Thread.sleep(1000);
List<String> filePaths = new ArrayList<String>();
filePaths.add(wavPath_base+"\\"+name+"\\title-.wav");
new File(workSpace+"img\\"+name_temp+".png").delete();
return VideoUtil.convertMp4ToTs(VideoAddWavUtil.videoAddWav(workSpace+"img\\"+name_temp+"temp.mp4",filePaths),workSpace);
}
通过多次测试,将MP4视频进行合并时,由于视频的分辨率不同,播放时会出现下方有黑色区域,所有将所有的视频文件全部转换为分辨率一样的ts格式,正如上面的**VideoUtil.convertMp4ToTs()**方法,
//mp4Path--待转换的视频文件绝对路径,workSpace--自定义的工作路径
public static String convertMp4ToTs(String mp4Path,String workSpace){
String time_now = System.currentTimeMillis()+"";
String cmd = " -i "+mp4Path+" -c:v libx264 -vf scale=1920:1080 -r 24 -c:a aac -ar 48000 -b:a 160k -strict experimental -f mpegts "+workSpace+"ts\\"+time_now+".ts";
Runtime run = Runtime.getRuntime();
try {
Process process = run.exec("cmd /c start D:\\ffmpeg\\bin\\ffmpeg.exe "+cmd.toString());
int rsource = process.waitFor();
InputStream stderr=process.getInputStream();
InputStreamReader isr=new InputStreamReader(stderr,"GBK");
BufferedReader br=new BufferedReader(isr);
String line=null;
System.out.println("-------------正在进行视频转为TS格式视频流文件----------");
while((line=br.readLine())!=null){
// String encoded=new String(line.getBytes("gbk"),"GB18030");//GB2312/CP936/GB18030
// System.out.println(encoded);
System.out.println(line);
}
System.out.println("cmd执行结果:"+rsource);
} catch (Exception e) {
e.printStackTrace();
}
new File(mp4Path).delete();
return workSpace+"ts\\"+time_now+".ts";
}
接下来就是讲片头、标题片头、主体视频、片尾进行合并,
public static String mergyTsToMp4(String startPath,String imgMp4Path,String bodyPath,String endPath,String name,String workSpace){
String body_ts = convertMp4ToTs(bodyPath,workSpace);
String cmd = " -i \"concat:"+startPath+"|"+imgMp4Path+"|"+body_ts+"|"+endPath+"\" -c copy -bsf:a aac_adtstoasc -movflags +faststart "+workSpace+"finish\\"+name+".mp4";
System.out.println(cmd);
Runtime run = Runtime.getRuntime();
try {
Process process = run.exec("cmd /c start D:\\ffmpeg\\bin\\ffmpeg.exe "+cmd.toString());
int rsource = process.waitFor();
InputStream stderr=process.getInputStream();
InputStreamReader isr=new InputStreamReader(stderr,"GBK");
BufferedReader br=new BufferedReader(isr);
String line=null;
System.out.println("-------------正在进行4个视频片段的合并----------");
while((line=br.readLine())!=null){
// String encoded=new String(line.getBytes("gbk"),"GB18030");//GB2312/CP936/GB18030
// System.out.println(encoded);
System.out.println(line);
}
System.out.println("cmd执行结果:"+rsource);
} catch (Exception e) {
e.printStackTrace();
}
new File(imgMp4Path).delete();
new File(body_ts).delete();
return workSpace+"finish\\"+name+".mp4";
}
4个视频合成后,需要添加上背景音乐
public static String addBackgroundWavToMp4(String mp4Path,String backgroundWavPath){
String cmd = " -i "+mp4Path+" -i "+backgroundWavPath+" -filter_complex amix=inputs=2:duration=first:dropout_transition=2 "+mp4Path.substring(0,mp4Path.lastIndexOf("."))+"(havedBackground).mp4";
System.out.println(cmd);
Runtime run = Runtime.getRuntime();
try {
Process process = run.exec("cmd /c start D:\\ffmpeg\\bin\\ffmpeg.exe "+cmd.toString());
int rsource = process.waitFor();
InputStream stderr=process.getInputStream();
InputStreamReader isr=new InputStreamReader(stderr,"GBK");
BufferedReader br=new BufferedReader(isr);
String line=null;
System.out.println("-------------正在进行视频的背景音乐添加----------");
while((line=br.readLine())!=null){
// String encoded=new String(line.getBytes("gbk"),"GB18030");//GB2312/CP936/GB18030
// System.out.println(encoded);
System.out.println(line);
}
System.out.println("cmd执行结果:"+rsource);
} catch (Exception e) {
e.printStackTrace();
}
new File(mp4Path).delete();
return mp4Path.substring(0,mp4Path.lastIndexOf("."))+"(havedBackground).mp4";
}
在视频进行合并时,有音频的视频和无音频的视频会合并失败,所以图片转为视频时我也加入了音频;
在java调用ffmpeg时,命令中的文件名带有中文时也会报错;