问题描述 近期线上环境出现视频封面链接异常的问题,视频封面链接地址视频封面地址后缀是 PNG格式,但是把文件保存在本地并把后缀改为MP4,可以用视频播放器正常打开。
原因分析 定位问题出现原因是安卓端调用阿里SDK上传视频后传到服务端的视频封面参数出错。
解决方案 由于线上已经产生了大量的异常数据,首先要将异常数据修复。同时,安卓端解决代码问题,在下一个发版的时间窗口修复此问题。
具体实施
服务端修复此问题的业务流程如下: 先给表增加一个fixed字段标识数据是否已修复。 alter table act_works add column FIXED int(11) NOT NULL DEFAULT ‘0’ COMMENT ‘数据修复标志:0 未修复 1 待修复 2 已修复’; update act_works t set t.FIXED=1; 把此表的线上数据导出到测试环境。 服务端通过跑定时任务修复异常的视频链接,业务流程如下:
定时任务代码
public void fixVedioUrl() throws IOException { //查询作品 List<ActWorksDO> list = actWorksDao.getActWorkToHandle("2019-08-26 02:10:00", 100); //保存至本地 if (!CollectionUtils.isEmpty(list)) { for (ActWorksDO bo : list) { if (!StringUtils.isEmpty(bo.getResourceUrl())) { String filePath = DOWNLOAD_FILE_DIR + bo.getId() + ".png"; downloadUtils.downloadFile(bo.getResourceUrl(), filePath); File file = new File(filePath); if (videoHandler.isImage(file)) { //判断文件格式,png格式文件删除 LOGGER.info(filePath + "文件格式正常"); if (file.exists()) { file.delete(); } actWorksDao.updateFixed(bo.getId(), 2, null); } else { try { LOGGER.info(filePath + "是mp4文件"); //拷贝文件修改视频后缀 String newFile = VIDEO_FILE_DIR + bo.getId() + ".mp4"; File destFile = new File(newFile); file.renameTo(destFile); String originName = bo.getId() + ".png"; String coverFile = COVER_FILE_DIR + originName; logger.info("coverFile-{}",coverFile); //截取视频封面 videoHandler.saveVideoCoverImage(newFile, coverFile); //上传视频封面图片 File coverVideoFile = new File(coverFile); String coverUrl = ""; if (coverVideoFile.exists()) { coverUrl = alyUploadUtil.uploadImg(originName, coverVideoFile); logger.info("alyUploadUtil uploadImg return-{}", coverUrl); } //替换原有视频视频封面 if (StringUtils.isNotEmpty(coverUrl)) { actWorksDao.updateFixed(bo.getId(), 2, coverUrl); } else { actWorksDao.updateFixed(bo.getId(), 1, null); } } catch (Exception e) { actWorksDao.updateFixed(bo.getId(), 1, null); } } } } } }在程序执行过程中报如下异常 问题分析:由于生成视频封面文件是调用外部程序ffmpeg生成,在多核服务器上是异步过程,需要待视频封面文件生成后才能把文件上传至阿里云对象存储,所以需要在上传文件前睡眠等待一段时间,通过在31行后添加如下代码片段即可。
Thread.sleep(2000);本案设置了2秒,可以根据自身的业务情况灵活调整。
下载网络资源工具类 DownloadUtils
public class DownloadUtils { private static final Logger LOGGER = LoggerFactory.getLogger(DownloadUtils.class); //链接url下载图片 public void downloadFile(String urlList,String fileName) { URL url = null; try { url = new URL(urlList); DataInputStream dataInputStream = new DataInputStream(url.openStream()); FileOutputStream fileOutputStream = new FileOutputStream(new File(fileName)); ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length; while ((length = dataInputStream.read(buffer)) > 0) { output.write(buffer, 0, length); } fileOutputStream.write(output.toByteArray()); dataInputStream.close(); fileOutputStream.close(); } catch (MalformedURLException e) { LOGGER.error("downloadFile error",e); e.printStackTrace(); } catch (IOException e) { LOGGER.error("downloadFile error",e); e.printStackTrace(); } } }视频文件处理工具类 VideoHandler
public class VideoHandler { private static final Logger LOGGER = LoggerFactory.getLogger(VideoHandler.class); // private final String ffmpegPath = "F:\\ffmpeg.exe"; private final String ffmpegPath = "ffmpeg"; public void saveVideoCoverImage(String upFilePath, String mediaPicPath) { List<String> cutpic = new ArrayList<String>(); cutpic.add(ffmpegPath); cutpic.add("-i"); cutpic.add(upFilePath); // 同上(指定的文件即可以是转换为flv格式之前的文件,也可以是转换的flv文件) cutpic.add("-y"); cutpic.add("-f"); cutpic.add("image2"); cutpic.add("-ss"); // 添加参数"-ss",该参数指定截取的起始时间 cutpic.add("0"); // 添加起始时间为第17秒 cutpic.add("-t"); // 添加参数"-t",该参数指定持续时间 cutpic.add("0.001"); // 添加持续时间为1毫秒 // cutpic.add("-s"); // 添加参数"-s",该参数指定截取的图片大小 // cutpic.add("500*400"); // 添加截取的图片大小为350*240 cutpic.add(mediaPicPath); // 添加截取的图片的保存路径 ProcessBuilder builder = new ProcessBuilder(); try { builder.command(cutpic); builder.redirectErrorStream(true); // 如果此属性为 true,则任何由通过此对象的 start() 方法启动的后续子进程生成的错误输出都将与标准输出合并, // 因此两者均可使用 Process.getInputStream() 方法读取。这使得关联错误消息和相应的输出变得更容易 builder.start(); } catch (Exception e) { LOGGER.error("saveVideoCoverImage error",e); } } /* * * @Author zhangs * @Description 判断文件是否为图片 * @Date 17:22 2019/10/21 **/ public static boolean isImage(File file) { if (!file.exists()) { return false; } BufferedImage image = null; try { image = ImageIO.read(file); if (image == null || image.getWidth() <= 0 || image.getHeight() <= 0) { return false; } return true; } catch (Exception e) { LOGGER.error("check isImage error",e); return false; } } }VideoHandler 工具类使用ffmpeg外部程序编辑视频生成视频封面,需要现在linux服务器上安装ffmpeg 软件,具体安装方法参考《CentOS 、Ubuntu安装ffmpeg》
https://blog.csdn.net/u013314786/article/details/89682800 笔者初次接触ffmpeg,感觉此工具在音视频处理方面功能非常强大,大家有兴趣可以深入去学习下。 https://blog.csdn.net/u014253332/article/details/86474950
上传文件至阿里云对象存储工具类 AlyUploadUtil
public class AlyUploadUtil { private static final Logger LOGGER = LoggerFactory.getLogger(AlyUploadUtil.class); private String endpoint = "http://oss-cn-shenzhen.aliyuncs.com"; private String bucketName = "*******"; private String accessKeyId="**********"; private String accessKeySecret="************"; public final String IMG_ACL_DOMAIN = "***************"; /* * * @Author zhangs * @Description 上传文件 * @Date 15:14 2019/5/8 **/ public String uploadImg(String originalFileName,File file) { String coverUrl = ""; try { String suffix = originalFileName.substring(originalFileName.lastIndexOf(".") + 1); String uuid = UUID.randomUUID().toString().replaceAll("-", ""); String fileName = uuid + "." + suffix; OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret); PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName,file); ossClient.putObject(putObjectRequest); coverUrl = IMG_ACL_DOMAIN + fileName; ossClient.shutdown(); } catch (Exception e) { LOGGER.error("uploadImg error",e); } return coverUrl; } }另外,AlyUploadUtil 工具类调用了阿里对象存储的sdk,项目中需要引入maven依赖 com.aliyun.oss aliyun-sdk-oss 3.5.0
方案验证采用此方案后可以100%识别并处理异常的视频封面,而且只要有用户上传新的短视频就会开启定时任务去检查修复视频封面,即使安卓没有发版修复逻辑问题,服务端也可以解决视频封面异常的问题,保证了App端的页面的正常显示。