Index: ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/imagefilemanager/action/UploadImageAndVideoAction.java =================================================================== diff -u -r32868 -r33517 --- ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/imagefilemanager/action/UploadImageAndVideoAction.java (.../UploadImageAndVideoAction.java) (revision 32868) +++ ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/imagefilemanager/action/UploadImageAndVideoAction.java (.../UploadImageAndVideoAction.java) (revision 33517) @@ -31,7 +31,9 @@ import com.forgon.disinfectsystem.basedatamanager.supplyroomconfig.service.SupplyRoomConfigManager; import com.forgon.disinfectsystem.entity.assestmanagement.DisposableGoods; import com.forgon.disinfectsystem.entity.basedatamanager.imagefilemanager.ImageFile; +import com.forgon.disinfectsystem.entity.basedatamanager.materialdefinition.MaterialDefinition; import com.forgon.disinfectsystem.entity.basedatamanager.supplyroomconfig.SupplyRoomConfig; +import com.forgon.disinfectsystem.entity.basedatamanager.toussedefinition.TousseDefinition; import com.forgon.disinfectsystem.entity.basedatamanager.videomanager.VideoFile; import com.forgon.disinfectsystem.tousse.imagefilemanager.service.ImageFileManager; import com.forgon.disinfectsystem.tousse.videomanager.service.VideoFileManager; @@ -169,6 +171,9 @@ String uuidName = UUID.randomUUID().toString().replace("-", "") + "_" + videoFile.getVideoName(); videoFile.setUuid_videoName(uuidName); videoFile.setVideoType(videoType); + if(StringUtils.equals(videoType, VideoFile.VIDEO_TYPE_TOUSSE)){ + videoFile.setTousseDefinition_id(Long.valueOf(objectId)); + } uploadVideoList.add(videoFile); File uploadFileDir = new File(savePath); @@ -205,26 +210,12 @@ startIndexInclusive = endIndexExclusive; } - //一次性物品图片的特殊处理: - //图片名称为:一次性物品的名称+规格+日期+流水号; - if(StringUtils.equals(ImageFile.IMAGE_TYPE_DISPOSABLEGOODS, imageType) && CollectionUtils.isNotEmpty(uploadImageList)){ - //查找一次性物品定义 - DisposableGoods disposableGoods = (DisposableGoods) objectDao.getById(DisposableGoods.class.getSimpleName(), objectId); - if(disposableGoods != null){ - String showName = disposableGoods.getName(); - if(disposableGoods.getSpecification() != null){ - showName += "[" + disposableGoods.getSpecification() + "]"; - } - for (ImageFile imageFile : uploadImageList) { - String imageName = showName - + DateTools.getFormatDateStr(new Date(), "yyyyMMddHHmmss") - + imageFile.getSerialNum() - + ".png"; - imageName = replaceIllegalCharacter(imageName); - imageFile.setImageName(imageName); - } - } - } + //生成视频预览图 + renderPreviewPic(uploadVideoList, savePath); + //生成图片名称:关联对象的名称+日期+时间(精确到秒)+流水号,例如:开胸包20211220812211.png、直剪【大】20211220812213.png + generateImageFileName(uploadImageList, imageType, objectId); + //【配包教学视频】生成视频名称 + generateVideoFileName(uploadVideoList, videoType, objectId); // 保存图片记录到数据库 String imageIds = ""; @@ -270,6 +261,111 @@ } /** + * 生成视频预览图 + * @param uploadVideoList + * @param savePath + * @throws Exception + */ + private void renderPreviewPic(List uploadVideoList, String savePath){ + if(CollectionUtils.isEmpty(uploadVideoList)){ + return; + } + for (VideoFile videoFile : uploadVideoList) { + try { + String previewPicName = videoFileManager.generatePreviewPic(savePath, videoFile.getUuid_videoName(), 640, 480, 1); + videoFile.setPreviewPicName(previewPicName); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * 生成视频名称,例如:开胸包20211220812211.mp4 + * @param uploadVideoList + * @param videoType + * @param objectId + */ + private void generateVideoFileName(List uploadVideoList, String videoType, String objectId) { + if(CollectionUtils.isNotEmpty(uploadVideoList)){ + String showName = ""; + if(StringUtils.equals(videoType, VideoFile.VIDEO_TYPE_TOUSSE)){ + TousseDefinition tousseDefinition = (TousseDefinition) objectDao.getById(TousseDefinition.class.getSimpleName(), objectId); + if(tousseDefinition != null){ + showName = tousseDefinition.getName(); + } + } + if(StringUtils.isNotBlank(showName)){ + for (VideoFile videoFile : uploadVideoList) { + String videoName = showName + + DateTools.getFormatDateStr(new Date(), "yyyyMMddHHmmss") + + videoFile.getSerialNum() + + ".mp4"; + videoName = replaceIllegalCharacter(videoName); + videoFile.setVideoName(videoName); + } + } + } + } + + /** + * 生成图片名称:关联对象的名称+日期+时间(精确到秒)+流水号,例如:开胸包20211220812211.png、直剪【大】20211220812213.png + * @param uploadImageList + * @param imageType + * @param objectId + */ + private void generateImageFileName(List uploadImageList, + String imageType, String objectId) { + if(CollectionUtils.isNotEmpty(uploadImageList)){ + String showName = ""; + if(StringUtils.equals(ImageFile.IMAGE_TYPE_DISPOSABLEGOODS, imageType)){ + //一次性物品图片的特殊处理: + //图片名称为:一次性物品的名称+规格+日期+流水号; + //查找一次性物品定义 + DisposableGoods disposableGoods = (DisposableGoods) objectDao.getById(DisposableGoods.class.getSimpleName(), objectId); + if(disposableGoods != null){ + showName = disposableGoods.getName(); + if(disposableGoods.getSpecification() != null){ + showName += "[" + disposableGoods.getSpecification() + "]"; + } + } + } + if(StringUtils.equals(ImageFile.IMAGE_TYPE_TOUSSE, imageType) + ||StringUtils.equals(ImageFile.IMAGE_TYPE_SPECIFICATION, imageType) + ||StringUtils.equals(ImageFile.IMAGE_TYPE_TOUSSE_WASHGUIDE, imageType) + ||StringUtils.equals(ImageFile.IMAGE_TYPE_TOUSSEPACKING, imageType)){ + //器械包定义里的【器械包图片】、【说明书】、【清洗操作指引图片】、【装配教学图片】 + TousseDefinition tousseDefinition = (TousseDefinition) objectDao.getById(TousseDefinition.class.getSimpleName(), objectId); + if(tousseDefinition != null){ + showName = tousseDefinition.getName(); + } + } + + if(StringUtils.equals(ImageFile.IMAGE_TYPE_MATERIAL, imageType)){ + //材料定义 + MaterialDefinition materialDefinition = (MaterialDefinition) objectDao.getById(MaterialDefinition.class.getSimpleName(), objectId); + if(materialDefinition != null){ + showName = materialDefinition.getName(); + if(materialDefinition.getSpecification() != null){ + showName += "[" + materialDefinition.getSpecification() + "]"; + } + } + } + + if(StringUtils.isNotBlank(showName)){ + for (ImageFile imageFile : uploadImageList) { + String imageName = showName + + DateTools.getFormatDateStr(new Date(), "yyyyMMddHHmmss") + + imageFile.getSerialNum() + + ".png"; + imageName = replaceIllegalCharacter(imageName); + imageFile.setImageName(imageName); + } + } + } + } + + /** * 去除文件名非法字符 * @param imageName * @return @@ -298,14 +394,6 @@ String imageIds = StrutsParamUtils.getPraramValue("imageIds", null); String videoIds = StrutsParamUtils.getPraramValue("videoIds", null); - if((!DatabaseUtil.isPoIdValid(objectId) && StringUtils.isBlank(imageIds) && StringUtils.isBlank(videoIds) && StringUtils.isBlank(packingTaskId)) - || StringUtils.isBlank(imageType) || StringUtils.isBlank(videoType)){ - // 提示参数异常 - JSONObject json = JSONUtil.buildJsonObject(false, "参数异常!"); - StrutsResponseUtils.output(json); - return; - } - JSONObject json = imageFileManager.getImageAndVideoInfo(objectId, packingTaskId, imageType, videoType, imageIds, videoIds, barcode); StrutsResponseUtils.output(json); @@ -387,7 +475,9 @@ } else { throw new RuntimeException("视频类型错误"); } - + //视频文件编码(如果文件是标准的视频文件,就不会再编码) + videoFileManager.encodeVideo(savePath, video); + File downloadFile = new File(savePath + "/" + video.getUuid_videoName()); HttpServletResponse response = StrutsParamUtils.getResponse(); @@ -540,7 +630,7 @@ /** * 根据关联对象id获取预览视频或完整视频 * @param videoId : 视频id - * @param original : true--查看完整视频; false--查看预览视频(预览视频大小不超过250kB) + * @param original : true--查看完整视频; false--查看预览 */ public void getVideoById() { String videoId = StrutsParamUtils.getPraramValue("videoId", null); @@ -551,18 +641,21 @@ StrutsResponseUtils.output(json); return; } - byte[] img = videoFileManager.getVideoFileById(videoId, original); + byte[] img = null; + if(original){ + img = videoFileManager.getVideoFileById(videoId, original); + }else{ + //查看视频预览图 + img = videoFileManager.getVideoFilePreviewPic(videoId); + } img = ImageUtils.defaultIfEmpty(img); - OutputStream outputStream = null; + BufferedOutputStream outputStream = null; try { HttpServletResponse response = StrutsParamUtils.getResponse(); response.setContentType("image/jpeg"); - outputStream = response.getOutputStream(); - if (original || img.length < 250 * 1024) { - outputStream.write(img, 0, img.length); - } else { - outputStream.write(img, 0, 250 * 1024); - } + outputStream = new BufferedOutputStream(response.getOutputStream()); + outputStream.write(img); + outputStream.flush(); } catch (Exception e) { e.printStackTrace(); } finally { Index: build.gradle =================================================================== diff -u -r33202 -r33517 --- build.gradle (.../build.gradle) (revision 33202) +++ build.gradle (.../build.gradle) (revision 33517) @@ -574,6 +574,13 @@ compile group: 'com.intersys', name: 'cache-jdbc', version: '2016.2' //compile 'org.java-websocket:Java-WebSocket:1.5.1' + + //ffmpeg视频编码解码 + compile group: 'ws.schild', name: 'jave-all-deps', version:'2.7.1' + compile group: 'ws.schild', name: 'jave-core', version:'2.7.1' + compile group: 'ws.schild', name: 'jave-nativebin-win64', version:'2.7.1' + compile group: 'ws.schild', name: 'jave-nativebin-linux64', version:'2.7.1' + compile group: 'ws.schild', name: 'jave-nativebin-osx64', version:'2.7.1' } Index: ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/videomanager/service/VideoFileManagerImpl.java =================================================================== diff -u -r33302 -r33517 --- ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/videomanager/service/VideoFileManagerImpl.java (.../VideoFileManagerImpl.java) (revision 33302) +++ ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/videomanager/service/VideoFileManagerImpl.java (.../VideoFileManagerImpl.java) (revision 33517) @@ -8,6 +8,7 @@ import java.io.OutputStream; import java.net.URLEncoder; import java.sql.ResultSet; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; @@ -29,6 +30,13 @@ import org.hibernate.Query; import org.hibernate.Session; +import ws.schild.jave.Encoder; +import ws.schild.jave.EncodingAttributes; +import ws.schild.jave.MultimediaInfo; +import ws.schild.jave.MultimediaObject; +import ws.schild.jave.ScreenExtractor; +import ws.schild.jave.VideoAttributes; + import com.forgon.directory.acegi.tools.AcegiHelper; import com.forgon.disinfectsystem.basedatamanager.supplyroomconfig.service.SupplyRoomConfigManager; import com.forgon.disinfectsystem.datamodifyrecord.service.DataModifyRecordManager; @@ -109,6 +117,8 @@ } boolean flag = objectDao.executeHQL("delete from VideoFile po where po.uuid_videoName =?", uuidName); + //删除视频预览图 + deleteLocalFile(uuidName.split("\\.")[0] + ".jpg", realPath); return flag?deleteLocalFile(uuidName,realPath):null; } /** @@ -460,24 +470,161 @@ return null; } String videoType = video.getVideoType(); - SupplyRoomConfig supplyRoomConfig = supplyRoomConfigManager.getSystemParamsObj(); - String videoDir = supplyRoomConfig.getSaveImagePath(); String savePath = ""; - if (VideoFile.VIDEO_TYPE_TOUSSE.equals(videoType)) { - savePath = videoDir + VideoFile.VIDEO_PATH_TOUSSE; - } else if (VideoFile.VIDEO_TYPE_CAMERA_APPLICATION.equals(videoType)) { - savePath = videoDir + VideoFile.VIDEO_PATH_CAMERA_APPLICATION; - } else if (VideoFile.VIDEO_TYPE_CAMERA_PACKING.equals(videoType)) { - savePath = videoDir + VideoFile.VIDEO_PATH_CAMERA_PACKING; - } else if(VideoFile.VIDEO_TYPE_CAMERA_RECYCLING.equals(videoType)){ - savePath = videoDir + VideoFile.VIDEO_PATH_CAMERA_RECYCLING; - } else { + try { + savePath = this.videoSavePath_TRANS_SUPPORTS(videoType); + } catch (Exception e) { + e.printStackTrace(); + } + if(StringUtils.isBlank(savePath)){ return null; } byte[] img = com.forgon.tools.util.FileUtils.readAllContentAsBytes(savePath + "/" + video.getUuid_videoName()); return img; } + /** + * 获取文件预览图片,如果没有预览图,就生成预览图 + * @param videoId + * @return + */ + @Override + public byte[] getVideoFilePreviewPic(String videoId){ + VideoFile video = (VideoFile) objectDao.getById(VideoFile.class.getSimpleName(), videoId); + if (video == null) { + return null; + } + String videoType = video.getVideoType(); + String savePath = this.videoSavePath_TRANS_SUPPORTS(videoType); + + byte[] img = null; + try { + String imageFileName = video.getUuid_videoName().split("\\.")[0] + ".jpg"; + File imageFile = new File(savePath + "/" + imageFileName); + if(!imageFile.exists()){ + //视频文件不存在预览图时,生成预览图 + String previewPicName = this.generatePreviewPic(savePath, video.getUuid_videoName(), 640, 480, 1); + video.setPreviewPicName(previewPicName); + objectDao.saveOrUpdate(video); + } + img = com.forgon.tools.util.FileUtils.readAllContentAsBytes(savePath + "/" + imageFileName); + } catch (Exception e) { + e.printStackTrace(); + } + return img; + } + + /** + * 视频文件编码 + * @param savePath + * @param videoFile + * @return + */ + @Override + public void encodeVideo(String savePath, VideoFile videoFile) { + + String sourcePath = savePath + "/" + videoFile.getUuid_videoName(); + MultimediaInfo multimediaInfo = null; + try { + MultimediaObject multimediaObject = new MultimediaObject(new File(sourcePath)); + multimediaInfo = multimediaObject.getInfo(); + } catch (Exception e1) { + e1.printStackTrace(); + } + + if(multimediaInfo == null || multimediaInfo.getDuration() == -1){ + String targetPath = sourcePath.split("\\.")[0] + "_zip.mp4"; + File source = new File(sourcePath); + File target = new File(targetPath); + try { + //复制不需要解码再编码,速度较快;按照"mp4"、"webm"的格式对视频文件进行复制(浏览器拍摄的视频格式为"mp4"或者"webm") + boolean result = encodeVideo(source, target, "copy", ""); + if(!result){ + //h264编码,需要解码再编码,速度较慢 + result = encodeVideo(source, target, "h264", "mp4"); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * 视频编码 + * @param source 视频源文件 + * @param target 编码后的文件 + * @param codec 编码方式(copy表示保留源文件的编码格式,不需要重新编解码,效率高) + * @param format 视频格式(format为空时,按照"mp4"、"webm"的格式对视频文件进行编码) + * @return + */ + private boolean encodeVideo(File source, File target, String codec, String format){ + List videoFormatList = new ArrayList(); + if(StringUtils.isNotBlank(format)){ + videoFormatList.add(format); + }else{ + //h264编码生成mp4格式的视频 + videoFormatList.add("mp4"); + //Only VP8 or VP9 or AV1 video and Vorbis or Opus audio and WebVTT subtitles are supported for WebM. + videoFormatList.add("webm"); + } + long beginTime = System.currentTimeMillis(); + codec = StringUtils.defaultIfEmpty(codec, "copy"); + //视频编码 + VideoAttributes video = new VideoAttributes(); + video.setCodec(codec); + if(!StringUtils.equals(codec, "copy")){ + video.setBitRate(921600); + video.setFrameRate(60); + } + EncodingAttributes attrs = new EncodingAttributes(); + attrs.setVideoAttributes(video); + attrs.setDecodingThreads(4); + attrs.setEncodingThreads(4); + for (String vFormat : videoFormatList) { + try { + attrs.setFormat(vFormat); + Encoder encoder = new Encoder(); + encoder.encode(new MultimediaObject(source), target, attrs); + System.out.println("视频编码花费时间是:" + ((System.currentTimeMillis() - beginTime))/1000 + "秒,format = " + vFormat); + source.delete(); + target.renameTo(source); + return true; + } catch (Exception e) { + e.printStackTrace(); + } + } + return false; + } + + /** + * 获取视频的预览图片 + * @param savePath 视频文件目录 + * @param uuidName 视频文件名称 + * @param width 图片宽度 + * @param height 图片高度 + * @param millis 截取视频第几秒的图片 + * @return + */ + @Override + public String generatePreviewPic(String savePath, String uuidName, int width, int height, long millis){ + String videoFilePath = savePath + "/" + uuidName; + String imageFilePath = videoFilePath.split("\\.")[0] + ".jpg"; + File source = new File(videoFilePath); + File target = new File(imageFilePath); + long beginTime = System.currentTimeMillis(); + if(!target.exists()){ + try { + ScreenExtractor screenExtractor = new ScreenExtractor(); + MultimediaObject multimediaObject = new MultimediaObject(source); + screenExtractor.renderOneImage(multimediaObject, width, height, millis, target, 4); + } catch (Exception e) { + e.printStackTrace(); + } + } + System.out.println("生成预览图花费时间是:" + ((System.currentTimeMillis() - beginTime))/1000 + "秒"); + return uuidName.split("\\.")[0] + ".jpg"; + } + @SuppressWarnings("unchecked") @Override public void bindingVideoFile(String objectId, String videoFileIds) { Index: ssts-basedata/src/main/java/com/forgon/disinfectsystem/entity/basedatamanager/videomanager/VideoFile.java =================================================================== diff -u -r32119 -r33517 --- ssts-basedata/src/main/java/com/forgon/disinfectsystem/entity/basedatamanager/videomanager/VideoFile.java (.../VideoFile.java) (revision 32119) +++ ssts-basedata/src/main/java/com/forgon/disinfectsystem/entity/basedatamanager/videomanager/VideoFile.java (.../VideoFile.java) (revision 33517) @@ -52,6 +52,10 @@ //视频名称 private String videoName; private String uuid_videoName;//上传后的视频名称(uuid+原来视频的名称) + /** + * 视频的预览图(上传视频文件时,生成预览图;旧数据没有预览图,会在查看视频预览时,生成预览图) + */ + private String previewPicName; //视频类型 private String videoType; //视频关联对象ID @@ -154,5 +158,13 @@ public void setBarcode(String barcode) { this.barcode = barcode; } + + public String getPreviewPicName() { + return previewPicName; + } + + public void setPreviewPicName(String previewPicName) { + this.previewPicName = previewPicName; + } } Index: ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/imagefilemanager/service/ImageFileManagerImpl.java =================================================================== diff -u -r33302 -r33517 --- ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/imagefilemanager/service/ImageFileManagerImpl.java (.../ImageFileManagerImpl.java) (revision 33302) +++ ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/imagefilemanager/service/ImageFileManagerImpl.java (.../ImageFileManagerImpl.java) (revision 33517) @@ -38,6 +38,9 @@ import org.hibernate.Query; import org.hibernate.Session; +import ws.schild.jave.MultimediaInfo; +import ws.schild.jave.MultimediaObject; + import com.forgon.directory.acegi.tools.AcegiHelper; import com.forgon.disinfectsystem.basedatamanager.supplyroomconfig.service.SupplyRoomConfigManager; import com.forgon.disinfectsystem.common.geom.SizeAndPosition; @@ -1210,25 +1213,38 @@ * @param videoPath 视频文件的路径 * @return 时长(例如:01:30) */ - public static String getVideoDurationByRate(String videoPath){ - String result = null; + public String getVideoDurationByRate(String videoPath){ + try { + MultimediaObject multimediaObject = new MultimediaObject(new File(videoPath)); + MultimediaInfo multimediaInfo = multimediaObject.getInfo(); + + if(multimediaInfo != null && multimediaInfo.getDuration() != -1){ + long duration = multimediaInfo.getDuration(); + + //long hour = (duration % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60); + long minute = (duration % (1000 * 60 * 60)) / (1000 * 60); + long second = (duration % (1000 * 60)) / 1000; + return (minute > 9 ? minute : "0" + minute) + ":" + (second > 9 ? second : "0" + second); + } + } catch (Exception e) { + e.printStackTrace(); + } + byte[] img = FileUtils.readAllContentAsBytes(videoPath); if (img == null || img.length == 0) { - return result; + return ""; } + //非标准视频文件计算时长的方式: double length = img.length; // 视频文件大小计算公式:(音频码率+视频码率) x 时长 / 8 // 128kbps = 128/8*1024B/s double videoRate = 921.6; - //double audioRate = 128; - double audioRate = 0; + double audioRate = 128; BigDecimal duration = MathTools.div(length * 8, videoRate + audioRate, 20); - long minute = duration.longValue() / 60000; - double temp = Math.ceil((duration.longValue()) / 1000.0); - long second = (long) (temp % 60); - result = (minute > 9 ? minute : "0" + minute) + ":" + (second > 9 ? second : "0" + second); - return result; + long minute = (duration.longValue() % (1000 * 60 * 60)) / (1000 * 60); + long second = (duration.longValue() % (1000 * 60)) / 1000; + return (minute > 9 ? minute : "0" + minute) + ":" + (second > 9 ? second : "0" + second); } // 删除图片文件存储在文件夹里面的图片 Index: ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/videomanager/service/VideoFileManager.java =================================================================== diff -u -r32119 -r33517 --- ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/videomanager/service/VideoFileManager.java (.../VideoFileManager.java) (revision 32119) +++ ssts-tousse/src/main/java/com/forgon/disinfectsystem/tousse/videomanager/service/VideoFileManager.java (.../VideoFileManager.java) (revision 33517) @@ -1,6 +1,5 @@ package com.forgon.disinfectsystem.tousse.videomanager.service; -import java.util.Collection; import java.util.List; import java.util.Set; @@ -122,4 +121,29 @@ * @return */ public JSONArray findRecyclingVideoFilesInfo(Set objectIds); + + /** + * 视频文件编码(如果文件是标准的视频文件,就不会再编码) + * @param savePath 视频文件目录 + * @param videoFile 视频文件名称 + */ + public void encodeVideo(String savePath, VideoFile videoFile); + + /** + * 生成视频的预览图片 + * @param savePath 视频文件目录 + * @param uuidName 视频文件名称 + * @param width 图片宽度 + * @param height 图片高度 + * @param millis 截取视频第几秒的图片 + * @return + */ + public String generatePreviewPic(String savePath, String uuidName, int width, int height, long millis); + + /** + * 查看视频预览图,如果没有视频还没有预览图,就生成预览图 + * @param videoId 视频ID + * @return + */ + public byte[] getVideoFilePreviewPic(String videoId); }