新增视频转码,进度显示功能

This commit is contained in:
wanggeng888 2021-06-14 00:10:39 +08:00
parent 15b0ec994a
commit 4c6c5d00c9
19 changed files with 1838 additions and 152 deletions

View File

@ -17,22 +17,40 @@ import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "media.video")
public class VideoProperties {
private Long backendDuration;
private Long appDuration;
private String types;
private Integer maxUploadCount;
private Long backendMaxDuration;
private Long appMaxDuration;
public Long getBackendDuration() {
return backendDuration;
public String getTypes() {
return types == null ? "mp4" : types;
}
public void setBackendDuration(Long backendDuration) {
this.backendDuration = backendDuration;
public void setTypes(String types) {
this.types = types;
}
public Long getAppDuration() {
return appDuration;
public Integer getMaxUploadCount() {
return maxUploadCount == null || maxUploadCount <= 1 ? 1 : maxUploadCount;
}
public void setAppDuration(Long appDuration) {
this.appDuration = appDuration;
public void setMaxUploadCount(Integer maxUploadCount) {
this.maxUploadCount = maxUploadCount;
}
public Long getBackendMaxDuration() {
return backendMaxDuration == null || backendMaxDuration <= 0 ? 0 : backendMaxDuration;
}
public void setBackendMaxDuration(Long backendMaxDuration) {
this.backendMaxDuration = backendMaxDuration;
}
public Long getAppMaxDuration() {
return appMaxDuration == null || appMaxDuration <= 0 ? 0 : appMaxDuration;
}
public void setAppMaxDuration(Long appMaxDuration) {
this.appMaxDuration = appMaxDuration;
}
}

View File

@ -3,19 +3,24 @@ package ink.wgink.module.file.media.controller.api.video;
import ink.wgink.common.base.DefaultBaseController;
import ink.wgink.exceptions.PropertiesException;
import ink.wgink.interfaces.consts.ISystemConstant;
import ink.wgink.module.file.media.pojo.dtos.video.VideoDTO;
import ink.wgink.module.file.media.service.video.IVideoService;
import ink.wgink.pojo.ListPage;
import ink.wgink.pojo.result.ErrorResult;
import ink.wgink.pojo.result.SuccessResult;
import ink.wgink.pojo.result.SuccessResultData;
import ink.wgink.pojo.result.SuccessResultList;
import ink.wgink.properties.media.MediaProperties;
import io.swagger.annotations.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
/**
@ -30,7 +35,7 @@ import java.util.concurrent.Callable;
**/
@Api(tags = ISystemConstant.API_TAGS_SYSTEM_PREFIX + "视频接口")
@RestController
@RequestMapping(ISystemConstant.API_PREFIX + "/video")
@RequestMapping(ISystemConstant.API_PREFIX + "/file/media/video")
public class VideoController extends DefaultBaseController {
@Autowired
@ -39,9 +44,6 @@ public class VideoController extends DefaultBaseController {
private IVideoService videoService;
@ApiOperation(value = "上传视频", notes = "上传视频接口")
@ApiImplicitParams({
@ApiImplicitParam(name = "video", value = "文件video", paramType = "query")
})
@ApiResponses({@ApiResponse(code = 400, message = "请求失败", response = ErrorResult.class)})
@PostMapping("upload")
public Callable<SuccessResultData<String>> upload(@RequestParam("video") MultipartFile video) {
@ -54,4 +56,53 @@ public class VideoController extends DefaultBaseController {
return () -> new SuccessResultData<>(videoService.upload(video));
}
@ApiOperation(value = "删除文件类别(id列表)", notes = "删除文件类别(id列表)接口")
@ApiImplicitParams({
@ApiImplicitParam(name = "ids", value = "ID列表用下划线分隔", paramType = "path", example = "1_2_3")
})
@ApiResponses({@ApiResponse(code = 400, message = "请求失败", response = ErrorResult.class)})
@DeleteMapping("remove/{isRemoveSource}/{ids}")
public synchronized SuccessResult remove(@PathVariable() Integer isRemoveSource, @PathVariable("ids") String ids) {
if (isRemoveSource == 0) {
videoService.remove(Arrays.asList(ids.split("\\_")));
}
if (isRemoveSource == 1) {
videoService.delete(Arrays.asList(ids.split("\\_")));
}
return new SuccessResult();
}
@ApiOperation(value = "视频转码", notes = "视频转码接口")
@ApiImplicitParams({
@ApiImplicitParam(name = "ids", value = "ID列表用下划线分隔", paramType = "path", example = "1_2_3")
})
@ApiResponses({@ApiResponse(code = 400, message = "请求失败", response = ErrorResult.class)})
@PutMapping("convert/{fileId}")
public synchronized SuccessResult updateConvert(@PathVariable("fileId") String fileId) {
videoService.updateConvert(fileId);
return new SuccessResult();
}
@ApiOperation(value = "视频转换进度", notes = "视频转换进度接口")
@GetMapping(value = "get-convert-progress", produces = "text/event-stream")
public SseEmitter getConvertProgress() {
return videoService.getConvertProgress();
}
@ApiOperation(value = "视频分页列表", notes = "视频分页列表接口")
@ApiImplicitParams({
@ApiImplicitParam(name = "page", value = "当前页码", paramType = "query", dataType = "int", defaultValue = "1"),
@ApiImplicitParam(name = "rows", value = "显示数量", paramType = "query", dataType = "int", defaultValue = "20"),
@ApiImplicitParam(name = "keywords", value = "关键字", paramType = "query", dataType = "String"),
@ApiImplicitParam(name = "startTime", value = "开始时间", paramType = "query", dataType = "String"),
@ApiImplicitParam(name = "endTime", value = "结束时间", paramType = "query", dataType = "String")
})
@ApiResponses({@ApiResponse(code = 400, message = "请求失败", response = ErrorResult.class)})
@GetMapping("listpage")
public SuccessResultList<List<VideoDTO>> listPageInfo(ListPage page) {
Map<String, Object> params = requestParams();
page.setParams(params);
return videoService.listPage(page);
}
}

View File

@ -0,0 +1,43 @@
package ink.wgink.module.file.media.controller.route.video;
import ink.wgink.interfaces.consts.ISystemConstant;
import ink.wgink.properties.media.video.VideoProperties;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
/**
* When you feel like quitting. Think about why you started
* 当你想要放弃的时候想想当初你为何开始
*
* @ClassName: VideoRouteController
* @Description: 视频路由
* @Author: wanggeng
* @Date: 2021/6/12 11:16 下午
* @Version: 1.0
*/
@Api(tags = ISystemConstant.API_TAGS_SYSTEM_PREFIX + "文件管理接口")
@Controller
@RequestMapping(ISystemConstant.ROUTE_PREFIX + "/file/media/video")
public class VideoRouteController {
@Autowired
private VideoProperties videoProperties;
@GetMapping("list")
public ModelAndView list() {
return new ModelAndView("file/media/video/list");
}
@GetMapping("upload")
public ModelAndView upload() {
ModelAndView modelAndView = new ModelAndView("file/media/video/upload");
modelAndView.addObject("types", videoProperties.getTypes());
modelAndView.addObject("maxUploadCount", videoProperties.getMaxUploadCount());
return modelAndView;
}
}

View File

@ -0,0 +1,91 @@
package ink.wgink.module.file.media.dao.video;
import ink.wgink.exceptions.RemoveException;
import ink.wgink.exceptions.SaveException;
import ink.wgink.exceptions.SearchException;
import ink.wgink.exceptions.UpdateException;
import ink.wgink.module.file.media.pojo.dtos.video.VideoDTO;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
/**
* When you feel like quitting. Think about why you started
* 当你想要放弃的时候想想当初你为何开始
*
* @ClassName: IVideoDao
* @Description: 视频
* @Author: wanggeng
* @Date: 2021/6/12 10:25 下午
* @Version: 1.0
*/
@Repository
public interface IVideoDao {
/**
* 建表
*
* @throws UpdateException
*/
void createTable() throws UpdateException;
/**
* 新增
*
* @param params
* @throws SaveException
*/
void save(Map<String, Object> params) throws SaveException;
/**
* 删除
*
* @param params
* @throws RemoveException
*/
void remove(Map<String, Object> params) throws RemoveException;
/**
* 删除物理
*
* @param params
* @throws RemoveException
*/
void delete(Map<String, Object> params) throws RemoveException;
/**
* 更新
*
* @param params
* @throws UpdateException
*/
void update(Map<String, Object> params) throws UpdateException;
/**
* 更新转码状态
*
* @param params
* @throws UpdateException
*/
void updateConvert(Map<String, Object> params) throws UpdateException;
/**
* 详情
*
* @param params
* @return
* @throws SearchException
*/
VideoDTO get(Map<String, Object> params) throws SearchException;
/**
* 列表
*
* @param params
* @return
* @throws SearchException
*/
List<VideoDTO> list(Map<String, Object> params) throws SearchException;
}

View File

@ -0,0 +1,34 @@
package ink.wgink.module.file.media.enums;
/**
* When you feel like quitting. Think about why you started
* 当你想要放弃的时候想想当初你为何开始
*
* @ClassName: ConvertStatusEnum
* @Description: 转码状态
* @Author: wanggeng
* @Date: 2021/6/13 5:06 下午
* @Version: 1.0
*/
public enum ConvertStatusEnum {
UN_CONVERTED("UN_CONVERTED", "未转码"),
CONVERTING("CONVERTING", "转码中"),
CONVERTED("CONVERTED", "已转码");
String value;
String summary;
ConvertStatusEnum(String value, String summary) {
this.value = value;
this.summary = summary;
}
public String getValue() {
return value == null ? "" : value;
}
public String getSummary() {
return summary == null ? "" : summary;
}
}

View File

@ -0,0 +1,33 @@
package ink.wgink.module.file.media.pojo.dtos;
/**
* When you feel like quitting. Think about why you started
* 当你想要放弃的时候想想当初你为何开始
*
* @ClassName: ConvertProcessDTO
* @Description: 转换进度
* @Author: wanggeng
* @Date: 2021/6/13 10:32 下午
* @Version: 1.0
*/
public class ConvertProgressDTO {
private String fileId;
private Double percent;
public String getFileId() {
return fileId == null ? "" : fileId;
}
public void setFileId(String fileId) {
this.fileId = fileId;
}
public Double getPercent() {
return percent == null ? 0 : percent;
}
public void setPercent(Double percent) {
this.percent = percent;
}
}

View File

@ -0,0 +1,117 @@
package ink.wgink.module.file.media.pojo.dtos;
import java.io.Serializable;
/**
* When you feel like quitting. Think about why you started
* 当你想要放弃的时候想想当初你为何开始
*
* @ClassName: MediaDTO
* @Description: 媒体
* @Author: wanggeng
* @Date: 2021/6/13 4:51 下午
* @Version: 1.0
*/
public class MediaDTO implements Serializable {
private static final long serialVersionUID = 4546445666757053123L;
private String fileId;
private String fileName;
private String filePath;
private String fileFullPath;
private String fileUrl;
private String fileType;
private Long fileSize;
private String fileSummary;
private String fileMd5;
private Integer isBack;
private String staticUrl;
public String getFileId() {
return fileId == null ? "" : fileId;
}
public void setFileId(String fileId) {
this.fileId = fileId;
}
public String getFileName() {
return fileName == null ? "" : fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFilePath() {
return filePath == null ? "" : filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getFileFullPath() {
return fileFullPath == null ? "" : fileFullPath;
}
public void setFileFullPath(String fileFullPath) {
this.fileFullPath = fileFullPath;
}
public String getFileUrl() {
return fileUrl == null ? "" : fileUrl;
}
public void setFileUrl(String fileUrl) {
this.fileUrl = fileUrl;
}
public String getFileType() {
return fileType == null ? "" : fileType;
}
public void setFileType(String fileType) {
this.fileType = fileType;
}
public Long getFileSize() {
return fileSize == null ? 0 : fileSize;
}
public void setFileSize(Long fileSize) {
this.fileSize = fileSize;
}
public String getFileSummary() {
return fileSummary == null ? "" : fileSummary;
}
public void setFileSummary(String fileSummary) {
this.fileSummary = fileSummary;
}
public String getFileMd5() {
return fileMd5 == null ? "" : fileMd5;
}
public void setFileMd5(String fileMd5) {
this.fileMd5 = fileMd5;
}
public Integer getIsBack() {
return isBack == null ? 0 : isBack;
}
public void setIsBack(Integer isBack) {
this.isBack = isBack;
}
public String getStaticUrl() {
return staticUrl == null ? "" : staticUrl;
}
public void setStaticUrl(String staticUrl) {
this.staticUrl = staticUrl;
}
}

View File

@ -0,0 +1,126 @@
package ink.wgink.module.file.media.pojo.dtos.video;
import ink.wgink.module.file.media.pojo.dtos.MediaDTO;
/**
* When you feel like quitting. Think about why you started
* 当你想要放弃的时候想想当初你为何开始
*
* @ClassName: VideoDTO
* @Description: 视频
* @Author: wanggeng
* @Date: 2021/6/13 4:53 下午
* @Version: 1.0
*/
public class VideoDTO extends MediaDTO {
private static final long serialVersionUID = -7694278158868881064L;
private String keyframe;
private Long videoDuration;
private Integer videoWidth;
private Integer videoHeight;
private Integer videoBitRate;
private String videoEncoder;
private Float videoFrameRate;
private Long audioDuration;
private Integer audioBitRate;
private Long audioSampleRate;
private String convertStatus;
private String gmtCreate;
public String getKeyframe() {
return keyframe == null ? "" : keyframe;
}
public void setKeyframe(String keyframe) {
this.keyframe = keyframe;
}
public Long getVideoDuration() {
return videoDuration == null ? 0 : videoDuration;
}
public void setVideoDuration(Long videoDuration) {
this.videoDuration = videoDuration;
}
public Integer getVideoWidth() {
return videoWidth == null ? 0 : videoWidth;
}
public void setVideoWidth(Integer videoWidth) {
this.videoWidth = videoWidth;
}
public Integer getVideoHeight() {
return videoHeight == null ? 0 : videoHeight;
}
public void setVideoHeight(Integer videoHeight) {
this.videoHeight = videoHeight;
}
public Integer getVideoBitRate() {
return videoBitRate == null ? 0 : videoBitRate;
}
public void setVideoBitRate(Integer videoBitRate) {
this.videoBitRate = videoBitRate;
}
public String getVideoEncoder() {
return videoEncoder == null ? "" : videoEncoder;
}
public void setVideoEncoder(String videoEncoder) {
this.videoEncoder = videoEncoder;
}
public Float getVideoFrameRate() {
return videoFrameRate == null ? 0 : videoFrameRate;
}
public void setVideoFrameRate(Float videoFrameRate) {
this.videoFrameRate = videoFrameRate;
}
public Long getAudioDuration() {
return audioDuration == null ? 0 : audioDuration;
}
public void setAudioDuration(Long audioDuration) {
this.audioDuration = audioDuration;
}
public Integer getAudioBitRate() {
return audioBitRate == null ? 0 : audioBitRate;
}
public void setAudioBitRate(Integer audioBitRate) {
this.audioBitRate = audioBitRate;
}
public Long getAudioSampleRate() {
return audioSampleRate == null ? 0 : audioSampleRate;
}
public void setAudioSampleRate(Long audioSampleRate) {
this.audioSampleRate = audioSampleRate;
}
public String getConvertStatus() {
return convertStatus == null ? "" : convertStatus;
}
public void setConvertStatus(String convertStatus) {
this.convertStatus = convertStatus;
}
public String getGmtCreate() {
return gmtCreate == null ? "" : gmtCreate;
}
public void setGmtCreate(String gmtCreate) {
this.gmtCreate = gmtCreate;
}
}

View File

@ -1,5 +1,9 @@
package ink.wgink.module.file.media.pojo.vos;
import io.swagger.annotations.ApiModel;
import java.io.Serializable;
/**
* When you feel like quitting. Think about why you started
* 当你想要放弃的时候想想当初你为何开始
@ -20,6 +24,8 @@ public class MediaVO {
private Long fileSize;
private String fileSummary;
private String fileMd5;
private Integer isBack;
private String staticUrl;
public String getFileName() {
return fileName == null ? "" : fileName.trim();
@ -84,4 +90,20 @@ public class MediaVO {
public void setFileMd5(String fileMd5) {
this.fileMd5 = fileMd5;
}
public Integer getIsBack() {
return isBack == null ? 0 : isBack;
}
public void setIsBack(Integer isBack) {
this.isBack = isBack;
}
public String getStaticUrl() {
return staticUrl == null ? "" : staticUrl;
}
public void setStaticUrl(String staticUrl) {
this.staticUrl = staticUrl;
}
}

View File

@ -15,74 +15,75 @@ import ink.wgink.module.file.media.pojo.vos.MediaVO;
public class VideoVO extends MediaVO {
private String keyframe;
private Long duration;
private Integer width;
private Integer height;
private Integer bitRate;
private String encoder;
private Float frameRate;
private Long videoDuration;
private Integer videoWidth;
private Integer videoHeight;
private Integer videoBitRate;
private String videoEncoder;
private Float videoFrameRate;
private Long audioDuration;
private Integer audioBitRate;
private Long audioSampleRate;
private String convertStatus;
public String getKeyframe() {
return keyframe == null ? "" : keyframe.trim();
return keyframe == null ? "" : keyframe;
}
public void setKeyframe(String keyframe) {
this.keyframe = keyframe;
}
public Long getDuration() {
return duration;
public Long getVideoDuration() {
return videoDuration == null ? 0 : videoDuration;
}
public void setDuration(Long duration) {
this.duration = duration;
public void setVideoDuration(Long videoDuration) {
this.videoDuration = videoDuration;
}
public Integer getWidth() {
return width;
public Integer getVideoWidth() {
return videoWidth == null ? 0 : videoWidth;
}
public void setWidth(Integer width) {
this.width = width;
public void setVideoWidth(Integer videoWidth) {
this.videoWidth = videoWidth;
}
public Integer getHeight() {
return height;
public Integer getVideoHeight() {
return videoHeight == null ? 0 : videoHeight;
}
public void setHeight(Integer height) {
this.height = height;
public void setVideoHeight(Integer videoHeight) {
this.videoHeight = videoHeight;
}
public Integer getBitRate() {
return bitRate;
public Integer getVideoBitRate() {
return videoBitRate == null ? 0 : videoBitRate;
}
public void setBitRate(Integer bitRate) {
this.bitRate = bitRate;
public void setVideoBitRate(Integer videoBitRate) {
this.videoBitRate = videoBitRate;
}
public String getEncoder() {
return encoder == null ? "" : encoder.trim();
public String getVideoEncoder() {
return videoEncoder == null ? "" : videoEncoder;
}
public void setEncoder(String encoder) {
this.encoder = encoder;
public void setVideoEncoder(String videoEncoder) {
this.videoEncoder = videoEncoder;
}
public Float getFrameRate() {
return frameRate;
public Float getVideoFrameRate() {
return videoFrameRate == null ? 0 : videoFrameRate;
}
public void setFrameRate(Float frameRate) {
this.frameRate = frameRate;
public void setVideoFrameRate(Float videoFrameRate) {
this.videoFrameRate = videoFrameRate;
}
public Long getAudioDuration() {
return audioDuration;
return audioDuration == null ? 0 : audioDuration;
}
public void setAudioDuration(Long audioDuration) {
@ -90,7 +91,7 @@ public class VideoVO extends MediaVO {
}
public Integer getAudioBitRate() {
return audioBitRate;
return audioBitRate == null ? 0 : audioBitRate;
}
public void setAudioBitRate(Integer audioBitRate) {
@ -98,10 +99,18 @@ public class VideoVO extends MediaVO {
}
public Long getAudioSampleRate() {
return audioSampleRate;
return audioSampleRate == null ? 0 : audioSampleRate;
}
public void setAudioSampleRate(Long audioSampleRate) {
this.audioSampleRate = audioSampleRate;
}
public String getConvertStatus() {
return convertStatus == null ? "" : convertStatus;
}
public void setConvertStatus(String convertStatus) {
this.convertStatus = convertStatus;
}
}

View File

@ -1,7 +1,17 @@
package ink.wgink.module.file.media.service.video;
import ink.wgink.exceptions.SearchException;
import ink.wgink.module.file.media.pojo.dtos.ConvertProgressDTO;
import ink.wgink.module.file.media.pojo.dtos.video.VideoDTO;
import ink.wgink.module.file.media.pojo.vos.video.VideoVO;
import ink.wgink.pojo.ListPage;
import ink.wgink.pojo.result.SuccessResultList;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.File;
import java.util.List;
import java.util.Map;
/**
* When you feel like quitting. Think about why you started
@ -34,6 +44,35 @@ public interface IVideoService {
*/
String save(String token, VideoVO videoVO);
/**
* 删除
*
* @param ids
*/
void remove(List<String> ids);
/**
* 删除物理
*
* @param ids
*/
void delete(List<String> ids);
/**
* 更新转码信息
*
* @param fileId
*/
void updateConvert(String fileId);
/**
* 更新转码信息
*
* @param fileId 文件ID
* @param convertFile 转码文件
*/
void updateConvert(String fileId, File convertFile);
/**
* 上传视频
*
@ -50,4 +89,52 @@ public interface IVideoService {
* @return
*/
String upload(String token, MultipartFile video);
/**
* 转换进度列表
*
* @return
*/
SseEmitter getConvertProgress();
/**
* 详情
*
* @param params
* @return
*/
VideoDTO get(Map<String, Object> params);
/**
* 详情
*
* @param fileId 文件ID
* @return
*/
VideoDTO get(String fileId);
/**
* 详情
*
* @param fileMd5 文件MD5值
* @return
*/
VideoDTO getByMd5(String fileMd5);
/**
* 列表
*
* @param params
* @return
*/
List<VideoDTO> list(Map<String, Object> params);
/**
* 分页
*
* @param page
* @return
*/
SuccessResultList<List<VideoDTO>> listPage(ListPage page);
}

View File

@ -1,20 +1,39 @@
package ink.wgink.module.file.media.service.video.impl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import ink.wgink.common.base.DefaultBaseService;
import ink.wgink.exceptions.FileException;
import ink.wgink.exceptions.SearchException;
import ink.wgink.module.file.media.dao.video.IVideoDao;
import ink.wgink.module.file.media.enums.ConvertStatusEnum;
import ink.wgink.module.file.media.manager.MediaManager;
import ink.wgink.module.file.media.manager.domain.MusicMetaInfo;
import ink.wgink.module.file.media.manager.domain.VideoMetaInfo;
import ink.wgink.module.file.media.pojo.dtos.ConvertProgressDTO;
import ink.wgink.module.file.media.pojo.dtos.video.VideoDTO;
import ink.wgink.module.file.media.pojo.vos.video.VideoVO;
import ink.wgink.module.file.media.service.IMediaService;
import ink.wgink.module.file.media.service.video.IVideoService;
import ink.wgink.module.file.media.task.transcoding.VideoConvertManager;
import ink.wgink.module.file.media.task.transcoding.runnable.VideoConvertRunnable;
import ink.wgink.pojo.ListPage;
import ink.wgink.pojo.result.SuccessResultList;
import ink.wgink.properties.media.MediaProperties;
import ink.wgink.util.UUIDUtil;
import ink.wgink.util.date.DateUtil;
import ink.wgink.util.map.HashMapUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* When you feel like quitting. Think about why you started
@ -33,17 +52,99 @@ public class VideoServiceImpl extends DefaultBaseService implements IVideoServic
private IMediaService mediaService;
@Autowired
private MediaProperties mediaProperties;
@Autowired
private IVideoDao videoDao;
@Override
public String save(VideoVO videoVO) {
return null;
return save(null, videoVO);
}
@Override
public String save(String token, VideoVO videoVO) {
String fileId = UUIDUtil.getUUID();
Map<String, Object> params = HashMapUtil.beanToMap(videoVO);
params.put("fileId", fileId);
if (StringUtils.isBlank(token)) {
setSaveInfo(params);
} else {
setAppSaveInfo(token, params);
}
videoDao.save(params);
return fileId;
}
return null;
@Override
public void remove(List<String> ids) {
Map<String, Object> params = getHashMap(2);
params.put("fileIds", ids);
List<VideoDTO> videoDTOs = videoDao.list(params);
checkConverting(videoDTOs);
setUpdateInfo(params);
videoDao.remove(params);
}
@Override
public void delete(List<String> ids) {
Map<String, Object> params = getHashMap(2);
params.put("fileIds", ids);
List<VideoDTO> videoDTOs = videoDao.list(params);
checkConverting(videoDTOs);
// 删除文件
for (VideoDTO videoDTO : videoDTOs) {
deleteSourceFile(videoDTO.getFileFullPath());
}
// 删除记录
videoDao.delete(params);
}
@Override
public void updateConvert(String fileId) {
VideoDTO videoDTO = get(fileId);
if (videoDTO == null) {
throw new SearchException("视频不存在");
}
VideoConvertManager.getInstance().convert(new VideoConvertRunnable(fileId, videoDTO.getFileFullPath(), this));
// 更新状态
Map<String, Object> params = getHashMap(8);
params.put("convertStatus", ConvertStatusEnum.CONVERTING.getValue());
params.put("fileId", fileId);
setUpdateInfo(params);
videoDao.updateConvert(params);
}
@Override
public void updateConvert(String fileId, File convertFile) {
VideoDTO videoDTO = get(fileId);
if (videoDTO == null) {
throw new SearchException("视频不存在");
}
Map<String, Object> params = getHashMap(10);
params.put("fileSize", convertFile.length());
params.put("fileType", "mp4");
params.put("fileName", convertFile.getName());
params.put("fileFullPath", convertFile.getAbsolutePath());
VideoMetaInfo videoMetaInfo = MediaManager.getInstance().getVideoMetaInfo(convertFile);
params.put("videoDuration", videoMetaInfo.getDuration());
params.put("videoWidth", videoMetaInfo.getWidth());
params.put("videoHeight", videoMetaInfo.getHeight());
params.put("videoBitRate", videoMetaInfo.getBitRate());
params.put("videoEncoder", videoMetaInfo.getEncoder());
params.put("videoFrameRate", videoMetaInfo.getFrameRate());
params.put("convertStatus", ConvertStatusEnum.CONVERTED.getValue());
MusicMetaInfo musicMetaInfo = videoMetaInfo.getMusicMetaInfo();
if (musicMetaInfo != null) {
params.put("audioDuration", musicMetaInfo.getDuration());
params.put("audioBitRate", musicMetaInfo.getBitRate());
params.put("audioSampleRate", musicMetaInfo.getSampleRate());
}
params.put("fileId", fileId);
setUpdateInfoByUserId(params, "1");
videoDao.update(params);
// 删除源文件
deleteSourceFile(videoDTO.getFileFullPath());
}
@Override
@ -53,32 +154,44 @@ public class VideoServiceImpl extends DefaultBaseService implements IVideoServic
@Override
public String upload(String token, MultipartFile video) {
String filePath = mediaProperties.getUploadPath() + File.separator + VIDEO_PATH;
String filePath = VIDEO_PATH + File.separator + DateUtil.getDays();
String uploadFolderPath = mediaProperties.getUploadPath() + File.separator + filePath;
String fileName = video.getOriginalFilename();
long fileSize = video.getSize();
String fileType = mediaService.getFileType(fileName);
String fileMd5 = mediaService.upload(video, filePath, fileName);
String fileFullPath = filePath;
String uuidFileName = UUIDUtil.getUUID() + "." + fileType;
String fileMd5 = mediaService.upload(video, uploadFolderPath, uuidFileName);
// 判断文件是否存在已经存在的不再新增直接返回结果删除临时文件
VideoDTO videoDTO = getByMd5(fileMd5);
String fileFullPath = uploadFolderPath + File.separator + uuidFileName;
File uploadFile = new File(fileFullPath);
if (videoDTO != null) {
LOG.error("上传的【{}】视频已经存在,原文件为【{}】返回原文件ID", videoDTO.getFileName(), videoDTO.getFileName());
uploadFile.delete();
return videoDTO.getFileId();
}
// 构建视频内容
VideoVO videoVO = new VideoVO();
videoVO.setFileName(fileName);
videoVO.setFileFullPath(fileFullPath);
videoVO.setFilePath(VIDEO_PATH);
videoVO.setFilePath(filePath);
videoVO.setFileSize(fileSize);
videoVO.setFileType(fileType);
videoVO.setFileMd5(fileMd5);
videoVO.setIsBack(0);
File uploadFile = new File(fileFullPath + File.separator + fileName);
VideoMetaInfo videoMetaInfo = MediaManager.getInstance().getVideoMetaInfo(uploadFile);
if (videoMetaInfo == null) {
throw new FileException("上传失败");
}
videoVO.setDuration(videoMetaInfo.getDuration());
videoVO.setWidth(videoMetaInfo.getWidth());
videoVO.setHeight(videoMetaInfo.getHeight());
videoVO.setBitRate(videoMetaInfo.getBitRate());
videoVO.setEncoder(videoMetaInfo.getEncoder());
videoVO.setFrameRate(videoMetaInfo.getFrameRate());
videoVO.setVideoDuration(videoMetaInfo.getDuration());
videoVO.setVideoWidth(videoMetaInfo.getWidth());
videoVO.setVideoHeight(videoMetaInfo.getHeight());
videoVO.setVideoBitRate(videoMetaInfo.getBitRate());
videoVO.setVideoEncoder(videoMetaInfo.getEncoder());
videoVO.setVideoFrameRate(videoMetaInfo.getFrameRate());
videoVO.setConvertStatus(ConvertStatusEnum.UN_CONVERTED.getValue());
MusicMetaInfo musicMetaInfo = videoMetaInfo.getMusicMetaInfo();
if (musicMetaInfo != null) {
@ -86,9 +199,84 @@ public class VideoServiceImpl extends DefaultBaseService implements IVideoServic
videoVO.setAudioBitRate(musicMetaInfo.getBitRate());
videoVO.setAudioSampleRate(musicMetaInfo.getSampleRate());
}
String fileId = save(token, videoVO);
// 转码
return fileId;
return save(token, videoVO);
}
@Override
public SseEmitter getConvertProgress() {
String threadId = String.valueOf(Thread.currentThread().getId());
// 30分钟超时
SseEmitter sseEmitter = new SseEmitter(1800000L);
sseEmitter.onTimeout(() -> {
VideoConvertManager.getInstance().removeConvertProgressSseEmitter(threadId);
});
sseEmitter.onCompletion(() -> {
});
sseEmitter.onError(throwable -> {
throwable.printStackTrace();
});
VideoConvertManager.getInstance().addConvertProgressSseEmitter(threadId, sseEmitter);
return sseEmitter;
}
@Override
public VideoDTO get(Map<String, Object> params) {
return videoDao.get(params);
}
@Override
public VideoDTO get(String fileId) {
Map<String, Object> params = getHashMap(2);
params.put("fileId", fileId);
return get(params);
}
@Override
public VideoDTO getByMd5(String fileMd5) {
Map<String, Object> params = getHashMap(2);
params.put("fileMd5", fileMd5);
return get(params);
}
@Override
public List<VideoDTO> list(Map<String, Object> params) {
return videoDao.list(params);
}
@Override
public SuccessResultList<List<VideoDTO>> listPage(ListPage page) {
PageHelper.startPage(page.getPage(), page.getRows());
List<VideoDTO> videoDTOs = list(page.getParams());
PageInfo<VideoDTO> pageInfo = new PageInfo<>(videoDTOs);
return new SuccessResultList<>(videoDTOs, pageInfo.getPageNum(), pageInfo.getTotal());
}
/**
* 检查转换中状态
*/
private void checkConverting(List<VideoDTO> videoDTOs) {
for (VideoDTO videoDTO : videoDTOs) {
if (StringUtils.equals(ConvertStatusEnum.CONVERTING.getValue(), videoDTO.getConvertStatus())) {
throw new SearchException("存在转码中的视频,删除失败");
}
}
}
/**
* 删除源文件
*
* @param sourceFilePath 源文件路径
*/
private void deleteSourceFile(String sourceFilePath) {
File file = new File(sourceFilePath);
if (file.exists()) {
boolean isDelete = file.delete();
if (isDelete) {
LOG.debug("源文件删除成功");
} else {
LOG.debug("源文件删除失败");
}
}
}
}

View File

@ -0,0 +1,62 @@
package ink.wgink.module.file.media.task.transcoding;
import ink.wgink.module.file.media.pojo.dtos.ConvertProgressDTO;
import ink.wgink.module.file.media.task.transcoding.runnable.VideoConvertRunnable;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Map;
import java.util.concurrent.*;
/**
* When you feel like quitting. Think about why you started
* 当你想要放弃的时候想想当初你为何开始
*
* @ClassName: VideoTransCodingManager
* @Description: 视频转码
* @Author: wanggeng
* @Date: 2021/6/10 9:46 上午
* @Version: 1.0
*/
public class VideoConvertManager {
private static VideoConvertManager VIDEO_TRANS_CODING_MANAGER = VideoTransCodingManagerBuilder.videoConvertManager;
private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 30L, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
private Map<String, SseEmitter> convertProgressSseEmitterMap = new ConcurrentHashMap<>();
private VideoConvertManager() {
}
public static VideoConvertManager getInstance() {
return VIDEO_TRANS_CODING_MANAGER;
}
public void convert(VideoConvertRunnable videoConvertRunnable) {
threadPoolExecutor.execute(videoConvertRunnable);
}
public void addConvertProgressSseEmitter(String threadId, SseEmitter sseEmitter) {
convertProgressSseEmitterMap.put(threadId, sseEmitter);
}
public void removeConvertProgressSseEmitter(String threadId) {
convertProgressSseEmitterMap.remove(threadId);
}
public void convertProgressSseEmitterNotice(String fileId, Double percent) {
ConvertProgressDTO convertProgressDTO = new ConvertProgressDTO();
convertProgressDTO.setFileId(fileId);
convertProgressDTO.setPercent(percent);
try {
for (Map.Entry<String, SseEmitter> kv : convertProgressSseEmitterMap.entrySet()) {
kv.getValue().send(convertProgressDTO);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static class VideoTransCodingManagerBuilder {
public static VideoConvertManager videoConvertManager = new VideoConvertManager();
}
}

View File

@ -1,31 +0,0 @@
package ink.wgink.module.file.media.task.transcoding;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* When you feel like quitting. Think about why you started
* 当你想要放弃的时候想想当初你为何开始
*
* @ClassName: VideoTransCodingManager
* @Description: 视频转码
* @Author: wanggeng
* @Date: 2021/6/10 9:46 上午
* @Version: 1.0
*/
public class VideoTransCodingManager {
private static VideoTransCodingManager VIDEO_TRANS_CODING_MANAGER = VideoTransCodingManagerBuilder.videoTransCodingManager;
private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 30L, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
private VideoTransCodingManager() {}
private static class VideoTransCodingManagerBuilder {
public static VideoTransCodingManager videoTransCodingManager = new VideoTransCodingManager();
}
}

View File

@ -0,0 +1,126 @@
package ink.wgink.module.file.media.task.transcoding.runnable;
import ink.wgink.module.file.media.manager.MediaManager;
import ink.wgink.module.file.media.manager.domain.enums.CrfValueEnum;
import ink.wgink.module.file.media.manager.domain.enums.PresetVauleEnum;
import ink.wgink.module.file.media.manager.process.IMediaStream;
import ink.wgink.module.file.media.service.video.IVideoService;
import ink.wgink.module.file.media.task.transcoding.VideoConvertManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* When you feel like quitting. Think about why you started
* 当你想要放弃的时候想想当初你为何开始
*
* @ClassName: VideoTransCodingRunnable
* @Description: 视频转码
* @Author: wanggeng
* @Date: 2021/6/10 10:04 上午
* @Version: 1.0
*/
public class VideoConvertRunnable implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(VideoConvertRunnable.class);
private static Pattern durationPattern = Pattern.compile("Duration: \\d{2}:\\d{2}:\\d{2}");
private static Pattern timePattern = Pattern.compile("time=\\d{2}:\\d{2}:\\d{2}");
private long fullTime = 0L;
private long currentTime = 0L;
private double convertPercent = 0D;
private boolean isUpdateConvertStatus = false;
private File outFile;
private String fileId;
private String sourceFile;
private IVideoService videoService;
public VideoConvertRunnable() {
}
public VideoConvertRunnable(String fileId, String sourceFile, IVideoService videoService) {
this.fileId = fileId;
this.sourceFile = sourceFile;
this.videoService = videoService;
}
@Override
public void run() {
File sourceFile = new File(this.sourceFile);
outFile = new File(this.sourceFile + ".mp4");
MediaManager.getInstance().convertVideo(sourceFile, outFile, true, CrfValueEnum.HIGH_QUALITY.getCode(), PresetVauleEnum.MAX_FAST_ZIP_SPEED.getPresetValue(), null, null, new IMediaStream() {
@Override
public void input(InputStream inputStream) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
for (String line; (line = bufferedReader.readLine()) != null; ) {
LOG.debug(line);
}
}
@Override
public void error(InputStream errorStream) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(errorStream));
for (String line; (line = bufferedReader.readLine()) != null; ) {
Matcher durationMatcher = durationPattern.matcher(line);
Matcher timeMatcher = timePattern.matcher(line);
if (durationMatcher.find()) {
String duration = durationMatcher.group();
LOG.debug("开始转码,时长:{}", duration);
String durationTime = duration.replace("Duration: ", "");
fullTime = durationToLongTime(durationTime);
}
if (timeMatcher.find()) {
String time = timeMatcher.group();
String timeTime = time.replace("time=", "");
currentTime = durationToLongTime(timeTime);
}
if (fullTime > 0L) {
convertPercent = new BigDecimal((currentTime / (fullTime * 1D) * 100D)).setScale(2, RoundingMode.HALF_UP).doubleValue();
}
if (convertPercent > 0D && convertPercent < 100D && (int) convertPercent % 5 == 0) {
VideoConvertManager.getInstance().convertProgressSseEmitterNotice(fileId, convertPercent);
}
if (convertPercent >= 100D) {
updateConvertStatus();
}
}
}
});
}
/**
* 更新转换状态
*/
private void updateConvertStatus() {
if (isUpdateConvertStatus) {
return;
}
VideoConvertManager.getInstance().convertProgressSseEmitterNotice(fileId, convertPercent);
isUpdateConvertStatus = true;
LOG.debug("转码结束,更新状态");
videoService.updateConvert(fileId, outFile);
}
/**
* 视频时长
*
* @param duration
* @return
*/
private long durationToLongTime(String duration) {
String[] durationArray = duration.split("\\:");
int hour = Integer.parseInt(durationArray[0]);
int minute = Integer.parseInt(durationArray[1]);
int second = Integer.parseInt(durationArray[2]);
return (hour * 3600 + minute * 60 + second);
}
}

View File

@ -1,53 +0,0 @@
package ink.wgink.module.file.media.task.transcoding.runnable;
import ink.wgink.module.file.media.manager.MediaManager;
import ink.wgink.module.file.media.manager.domain.enums.CrfValueEnum;
import ink.wgink.module.file.media.manager.domain.enums.PresetVauleEnum;
import ink.wgink.module.file.media.manager.process.IMediaStream;
import ink.wgink.module.file.media.service.video.IVideoService;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* When you feel like quitting. Think about why you started
* 当你想要放弃的时候想想当初你为何开始
*
* @ClassName: VideoTransCodingRunnable
* @Description: 视频转码
* @Author: wanggeng
* @Date: 2021/6/10 10:04 上午
* @Version: 1.0
*/
public class VideoTransCodingRunnable implements Runnable {
private String sourcePath;
private String sourceName;
private IVideoService videoService;
@Override
public void run() {
File sourceFile = new File(sourcePath + File.separator + sourceName);
File outFile = new File(sourcePath + File.separator + sourceName + ".mp4");
MediaManager.getInstance().convertVideo(sourceFile, outFile, true, CrfValueEnum.HIGH_QUALITY.getCode(), PresetVauleEnum.MAX_FAST_ZIP_SPEED.getPresetValue(), null, null, new IMediaStream() {
@Override
public void input(InputStream inputStream) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
for (String line; (line = bufferedReader.readLine()) != null; ) {
System.out.println("input-" + line);
}
}
@Override
public void error(InputStream errorStream) throws Exception {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(errorStream));
for (String line; (line = bufferedReader.readLine()) != null; ) {
System.out.println("error-" + line);
}
}
});
}
}

View File

@ -0,0 +1,298 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ink.wgink.module.file.media.dao.video.IVideoDao">
<cache/>
<resultMap id="videoDTO" type="ink.wgink.module.file.media.pojo.dtos.video.VideoDTO">
<id column="file_id" property="fileId"/>
<result column="file_name" property="fileName"/>
<result column="file_full_path" property="fileFullPath"/>
<result column="file_path" property="filePath"/>
<result column="file_size" property="fileSize"/>
<result column="file_type" property="fileType"/>
<result column="file_md5" property="fileMd5"/>
<result column="is_back" property="isBack"/>
<result column="static_url" property="staticUrl"/>
<result column="file_summary" property="fileSummary"/>
<result column="video_duration" property="videoDuration"/>
<result column="video_width" property="videoWidth"/>
<result column="video_height" property="videoHeight"/>
<result column="video_bit_rate" property="videoBitRate"/>
<result column="video_encoder" property="videoEncoder"/>
<result column="video_frame_rate" property="videoFrameRate"/>
<result column="audio_duration" property="audioDuration"/>
<result column="audio_bit_rate" property="audioBitRate"/>
<result column="audio_sample_rate" property="audioSampleRate"/>
<result column="convert_status" property="convertStatus"/>
<result column="gmt_create" property="gmtCreate"/>
</resultMap>
<!-- 建表 -->
<update id="createTable">
CREATE TABLE IF NOT EXISTS `media_video` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`file_id` char(36) NOT NULL COMMENT '主键',
`file_name` varchar(255) DEFAULT NULL COMMENT '名称',
`file_full_path` varchar(500) DEFAULT NULL COMMENT '全路径',
`file_path` varchar(255) DEFAULT NULL COMMENT '路径',
`file_size` varchar(100) DEFAULT NULL COMMENT '大小',
`file_type` varchar(255) DEFAULT NULL COMMENT '类型',
`file_md5` varchar(255) DEFAULT NULL COMMENT 'MD5',
`file_summary` varchar(255) DEFAULT NULL COMMENT '备注',
`video_duration` varchar(255) DEFAULT NULL COMMENT '时长',
`video_width` int(11) DEFAULT NULL COMMENT '宽度',
`video_height` int(11) DEFAULT NULL COMMENT '高度',
`video_bit_rate` int(11) DEFAULT NULL COMMENT '比特率',
`video_encoder` varchar(255) DEFAULT NULL COMMENT '编码',
`video_frame_rate` int(11) DEFAULT NULL COMMENT '帧率',
`audio_duration` varchar(255) DEFAULT NULL COMMENT '音频时长',
`audio_bit_rate` int(11) DEFAULT NULL COMMENT '音频比特率',
`audio_sample_rate` int(11) DEFAULT NULL COMMENT '音频采样率',
`convert_status` varchar(255) DEFAULT 'unConverted' COMMENT '转码状态',
`is_back` int(1) DEFAULT '0' COMMENT '是否备份',
`static_url` varchar(500) DEFAULT NULL COMMENT '静态url',
`creator` char(36) DEFAULT NULL COMMENT '创建人',
`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
`modifier` char(36) DEFAULT NULL COMMENT '修改人',
`gmt_modified` datetime DEFAULT NULL COMMENT '修改时间',
`is_delete` int(1) DEFAULT '0',
PRIMARY KEY (`id`,`file_id`) USING BTREE,
UNIQUE KEY `file_id` (`file_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
</update>
<!-- 保存文件 -->
<insert id="save" parameterType="map" flushCache="true">
INSERT INTO media_video(
file_id,
file_name,
file_full_path,
file_path,
file_size,
file_type,
file_md5,
file_summary,
video_duration,
video_width,
video_height,
video_bit_rate,
video_encoder,
video_frame_rate,
audio_duration,
audio_bit_rate,
audio_sample_rate,
convert_status,
is_back,
static_url,
creator,
gmt_create,
modifier,
gmt_modified,
is_delete
) VALUES(
#{fileId},
#{fileName},
#{fileFullPath},
#{filePath},
#{fileSize},
#{fileType},
#{fileMd5},
#{fileSummary},
#{videoDuration},
#{videoWidth},
#{videoHeight},
#{videoBitRate},
#{videoEncoder},
#{videoFrameRate},
#{audioDuration},
#{audioBitRate},
#{audioSampleRate},
#{convertStatus},
#{isBack},
#{staticUrl},
#{creator},
#{gmtCreate},
#{modifier},
#{gmtModified},
#{isDelete}
)
</insert>
<!-- 删除文件 -->
<update id="remove" parameterType="map" flushCache="true">
UPDATE
media_video
SET
is_delete = 1,
modifier = #{modifier},
gmt_modified = #{gmtModified}
WHERE
file_id IN
<foreach collection="fileIds" index="index" open="(" separator="," close=")">
#{fileIds[${index}]}
</foreach>
</update>
<!-- 删除文件(物理删除) -->
<delete id="delete" parameterType="map" flushCache="true">
DELETE FROM
media_video
WHERE
file_id IN
<foreach collection="fileIds" index="index" open="(" separator="," close=")">
#{fileIds[${index}]}
</foreach>
</delete>
<!-- 更新 -->
<update id="update" parameterType="map" flushCache="true">
UPDATE
media_video
SET
<if test="fileFullPath != null and fileFullPath != ''">
file_full_path = #{fileFullPath},
</if>
<if test="filePath != null and filePath != ''">
file_path = #{filePath},
</if>
<if test="fileSize != null and fileSize != ''">
file_size = #{fileSize},
</if>
<if test="fileType != null and fileType != ''">
file_type = #{fileType},
</if>
<if test="videoDuration != null and videoDuration != ''">
video_duration = #{videoDuration},
</if>
<if test="videoWidth != null">
video_width = #{videoWidth},
</if>
<if test="videoHeight != null">
video_height = #{videoHeight},
</if>
<if test="videoBitRate != null">
video_bit_rate = #{videoBitRate},
</if>
<if test="videoEncoder != null and videoEncoder != ''">
video_encoder = #{videoEncoder},
</if>
<if test="videoFrameRate != null">
video_frame_rate = #{videoFrameRate},
</if>
<if test="audioDuration != null and audioDuration != ''">
audio_duration = #{audioDuration},
</if>
<if test="audioBitRate != null">
audio_bit_rate = #{audioBitRate},
</if>
<if test="audioSampleRate != null">
audio_sample_rate = #{audioSampleRate},
</if>
<if test="convertStatus != null and convertStatus != ''">
convert_status = #{convertStatus},
</if>
modifier = #{modifier},
gmt_modified = #{gmtModified}
WHERE
file_id = #{fileId}
</update>
<!-- 更新转码状态 -->
<update id="updateConvert" parameterType="map" flushCache="true">
UPDATE
media_video
SET
convert_status = #{convertStatus},
modifier = #{modifier},
gmt_modified = #{gmtModified}
WHERE
file_id = #{fileId}
</update>
<!-- 详情 -->
<select id="get" parameterType="map" resultMap="videoDTO" useCache="true">
SELECT
file_id,
file_name,
file_full_path,
file_path,
file_size,
file_type,
file_md5,
file_summary,
video_duration,
video_width,
video_height,
video_bit_rate,
video_encoder,
video_frame_rate,
audio_duration,
audio_bit_rate,
audio_sample_rate,
convert_status,
is_back,
static_url
FROM
media_video
<where>
<if test="fileId != null and fileId != ''">
file_id = #{fileId}
</if>
<if test="fileMd5 != null and fileMd5 != ''">
AND file_md5 = #{fileMd5}
</if>
</where>
</select>
<!-- 列表 -->
<select id="list" parameterType="map" resultMap="videoDTO" useCache="true">
SELECT
file_id,
file_name,
file_full_path,
file_path,
file_size,
file_type,
file_md5,
file_summary,
video_duration,
video_width,
video_height,
video_bit_rate,
video_encoder,
video_frame_rate,
audio_duration,
audio_bit_rate,
audio_sample_rate,
convert_status,
is_back,
static_url,
LEFT(gmt_create, 19) gmt_create
FROM
media_video
<where>
is_delete = 0
<if test="keywords != null and keywords != ''">
AND
file_name LIKE CONCAT('%', #{keywords}, '%')
</if>
<if test="startTime != null and startTime != ''">
AND
LEFT(gmt_create, 10) <![CDATA[ >= ]]> #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND
LEFT(gmt_create, 10) <![CDATA[ <= ]]> #{endTime}
</if>
<if test="fileIds != null and fileIds.size > 0">
AND
file_id IN
<foreach collection="fileIds" index="index" open="(" separator="," close=")">
#{fileIds[${index}]}
</foreach>
</if>
</where>
</select>
</mapper>

View File

@ -0,0 +1,390 @@
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<base th:href="${#request.getContextPath() + '/'} ">
<meta charset="utf-8">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
<link rel="stylesheet" href="assets/fonts/font-awesome/css/font-awesome.css"/>
<link rel="stylesheet" href="assets/layuiadmin/layui/css/layui.css" media="all">
<link rel="stylesheet" href="assets/layuiadmin/style/admin.css" media="all">
</head>
<body>
<div class="layui-fluid layui-anim layui-anim-fadein">
<div class="layui-row">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-body" id="cardBody">
<div class="test-table-reload-btn" style="margin-bottom: 10px;">
<div class="layui-inline">
<input type="text" id="keywords" class="layui-input search-item search-item-width-100" placeholder="输入关键字">
</div>
<div class="layui-inline">
<input type="text" id="startTime" class="layui-input search-item search-item-width-100" placeholder="开始时间" readonly>
</div>
<div class="layui-inline">
<input type="text" id="endTime" class="layui-input search-item search-item-width-100" placeholder="结束时间" readonly>
</div>
<button type="button" id="search" class="layui-btn layui-btn-sm">
<i class="fa fa-lg fa-search"></i> 搜索
</button>
</div>
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
<!-- 表头按钮组 -->
<script type="text/html" id="headerToolBar">
<div class="layui-btn-group">
<button type="button" class="layui-btn layui-btn-sm" lay-event="saveEvent">
<i class="fa fa-lg fa-plus"></i> 新增
</button>
<button type="button" class="layui-btn layui-btn-danger layui-btn-sm" lay-event="removeEvent">
<i class="fa fa-lg fa-trash"></i> 删除
</button>
</div>
<span style="margin-left: 10px;"><i class="fa fa-warning"></i> 视频在线播放只支持MP4格式格式不符点击"转码状态"列的"转码"按钮进行转码</span>
</script>
</div>
</div>
</div>
</div>
<div id="videoBoxBackGround" style="position: fixed;z-index: 1000;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(0,0,0,0.6); display: none;">
<div role="button" class="viewer-button viewer-close video-close-btn"></div>
<div id="videoBox" style="width: 600px; height: 400px; position: absolute; left: 50%; top: 50%; margin-left: -300px; margin-top: -200px;">
<div id="video" style="width: 100%; height: 100%"></div>
</div>
</div>
</div>
<script src="assets/js/vendor/ckplayer/ckplayer/ckplayer.js"></script>
<script src="assets/layuiadmin/layui/layui.js"></script>
<script type="text/javascript">
layui.config({
base: 'assets/layuiadmin/'
}).extend({
index: 'lib/index'
}).use(['index', 'table', 'laydate', 'common'], function() {
var $ = layui.$;
var $win = $(window);
var table = layui.table;
var admin = layui.admin;
var laydate = layui.laydate;
var common = layui.common;
var resizeTimeout = null;
var tableUrl = 'api/file/media/video/listpage';
var ckPlayerFileId;
var ckPlayer = new ckplayer({
container: '#video',
variable: 'player',
flashplayer: false,
video: {
file: '',
type: 'video/mp4'
},
});
// 初始化表格
function initTable() {
table.render({
elem: '#dataTable',
id: 'dataTable',
url: top.restAjax.path(tableUrl, []),
width: admin.screen() > 1 ? '100%' : '',
height: $win.height() - 90,
limit: 20,
limits: [20, 40, 60, 80, 100, 200],
toolbar: '#headerToolBar',
request: {
pageName: 'page',
limitName: 'rows'
},
cols: [
[
{type:'checkbox', fixed: 'left'},
{field:'rowNum', width:80, title: '序号', fixed: 'left', align:'center', templet: '<span>{{d.LAY_INDEX}}</span>'},
{field: 'fileId', width: 140, title: '文件ID', align:'center',
templet: function(row) {
var rowData = row[this.field];
if(typeof(rowData) === 'undefined' || rowData == null || rowData == '') {
return '-';
}
return rowData;
}
},
{field: 'fileName', width: 140, title: '文件名称', align:'center',
templet: function(row) {
var rowData = row[this.field];
if(typeof(rowData) === 'undefined' || rowData == null || rowData == '') {
return '-';
}
return '<a href="javascript:void(0);" lay-event="searchFileEvent"><i class="fa fa-video-camera"></i> '+ rowData +'</a>'
}
},
{field: 'fileType', width: 100, title: '文件类型', align:'center',
templet: function(row) {
var rowData = row[this.field];
if(typeof(rowData) === 'undefined' || rowData == null || rowData == '') {
return '-';
}
return rowData;
}
},
{field: 'fileSize', width: 120, title: '文件大小', align:'center',
templet: function(row) {
var rowData = row[this.field];
if(typeof(rowData) === 'undefined' || rowData == null || rowData == '') {
return '-';
}
return parseInt(rowData / 1000000) +'MB';
}
},
{field: 'widthHeight', width: 120, title: '分辨率', align:'center',
templet: function(row) {
return row.videoWidth +' × '+ row.videoHeight;
}
},
{field: 'videoBitRate', width: 100, title: '比特率', align:'center',
templet: function(row) {
var rowData = row[this.field];
if(typeof(rowData) === 'undefined' || rowData == null || rowData == '') {
return '-';
}
return rowData;
}
},
{field: 'videoDuration', width: 100, title: '时长', align:'center',
templet: function(row) {
var rowData = row[this.field];
if(typeof(rowData) === 'undefined' || rowData == null || rowData == '') {
return '-';
}
var hour = parseInt(rowData / 3600000);
hour = hour < 10 ? '0'+ hour : hour;
var minute = parseInt(rowData % 3600000 / 60000);
minute = minute < 10 ? '0'+ minute : minute;
var second = parseInt(rowData % 3600000 % 60000 / 1000);
second = second < 10 ? '0'+ second : second;
return hour +':'+ minute +':'+ second;
}
},
{field: 'videoEncoder', width: 80, title: '编码', align:'center',
templet: function(row) {
var rowData = row[this.field];
if(typeof(rowData) === 'undefined' || rowData == null || rowData == '') {
return '-';
}
return rowData;
}
},
{field: 'videoFrameRate', width: 80, title: '帧率', align:'center',
templet: function(row) {
var rowData = row[this.field];
if(typeof(rowData) === 'undefined' || rowData == null || rowData == '') {
return '-';
}
return rowData;
}
},
{field: 'convertProgress', width: 100, title: '转换进度', align:'center', fixed: 'right',
templet: function(row) {
if(row.convertStatus === 'UN_CONVERTED') {
return '0%';
}
if(row.convertStatus === 'CONVERTED') {
return '100%';
}
return '<span id="progress_'+ row.fileId +'">0%</span>';
}
},
{field: 'convertStatus', width: 100, title: '转码状态', align:'center', fixed: 'right',
templet: function(row) {
var rowData = row[this.field];
if(typeof(rowData) === 'undefined' || rowData == null || rowData == '') {
return '-';
}
if(rowData === 'UN_CONVERTED') {
return '<button type="button" class="layui-btn layui-btn-xs" lay-event="convertEvent">转码</button>';
}
if(rowData === 'CONVERTING') {
return '转码中';
}
if(rowData === 'CONVERTED') {
return '已转码';
}
return rowData;
}
},
]
],
page: true,
parseData: function(data) {
return {
'code': 0,
'msg': '',
'count': data.total,
'data': data.rows
};
}
});
}
// 重载表格
function reloadTable(currentPage) {
table.reload('dataTable', {
url: top.restAjax.path(tableUrl, []),
where: {
keywords: $('#keywords').val(),
startTime: $('#startTime').val(),
endTime: $('#endTime').val()
},
page: {
curr: currentPage
},
height: $win.height() - 90,
});
}
// 初始化日期
function initDate() {
// 日期选择
laydate.render({
elem: '#startTime',
format: 'yyyy-MM-dd'
});
laydate.render({
elem: '#endTime',
format: 'yyyy-MM-dd'
});
}
// 删除
function removeFile(isRemoveSource, ids) {
var layIndex;
top.restAjax.delete(top.restAjax.path('api/file/media/video/remove/{isRemoveSource}/{ids}', [isRemoveSource, ids]), {}, null, function (code, data) {
top.dialog.msg(top.dataMessage.deleteSuccess, {time: 1000});
reloadTable();
}, function (code, data) {
top.dialog.msg(data.msg);
}, function () {
layIndex = top.dialog.msg(top.dataMessage.deleting, {icon: 16, time: 0, shade: 0.3});
}, function () {
top.dialog.close(layIndex);
});
}
function removeData(ids) {
top.dialog.msg(top.dataMessage.delete, {
time: 0,
btn: [top.dataMessage.button.yes, '同时删除文件(不可逆)', top.dataMessage.button.no],
shade: 0.3,
btn1: function (index) {
top.dialog.close(index);
removeFile(0, ids);
},
btn2: function(index) {
top.dialog.close(index);
removeFile(1, ids);
},
btn3: function(index) {
top.dialog.close(index);
}
});
}
function sseConvertProgress() {
var source = new EventSource(top.restAjax.path('api/file/media/video/get-convert-progress', []));
source.addEventListener('message', function(e) {
var data = JSON.parse(e.data);
$('#progress_'+ data.fileId).text(parseInt(data.percent) + '%');
if(data >= 100) {
setTimeout(function() {
reloadTable();
}, 2000);
}
});
source.addEventListener('open', function(e) {}, false);
// 响应finish事件主动关闭EventSource
source.addEventListener('finish', function(e) {
source.close();
}, false);
// 异常
source.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED) {
console.log("连接关闭");
}
}, false);
}
initTable();
initDate();
sseConvertProgress();
// 事件 - 页面变化
$win.on('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function() {
reloadTable();
}, 500);
});
// 事件 - 搜索
$(document).on('click', '#search', function() {
reloadTable(1);
});
// 事件 - 增删改
table.on('toolbar(dataTable)', function(obj) {
var layEvent = obj.event;
var checkStatus = table.checkStatus('dataTable');
var checkDatas = checkStatus.data;
if(layEvent === 'saveEvent') {
top.dialog.open({
url: top.restAjax.path('route/file/media/video/upload', []),
title: '上传视频',
width: '500px',
height: '380px',
onClose: function() {
reloadTable();
}
});
} else if(layEvent === 'removeEvent') {
if(checkDatas.length === 0) {
top.dialog.msg(top.dataMessage.table.selectDelete);
} else {
var ids = '';
for(var i = 0, item; item = checkDatas[i++];) {
if(i > 1) {
ids += '_';
}
ids += item['fileId'];
}
removeData(ids);
}
}
});
table.on('tool(dataTable)', function(obj) {
var data = obj.data;
var event = obj.event;
if(event === 'searchFileEvent') {
$('#videoBoxBackGround').show();
if(ckPlayerFileId == data.fileId) {
return;
}
ckPlayer.newVideo({
video: 'route/file/media/video/download/true/'+ data.fileId
});
ckPlayerFileId = data.fileId;
} else if(event === 'convertEvent') {
top.dialog.confirm('转码成功后,源文件将删除,确定转码吗?', function(index) {
top.dialog.close(index);
top.restAjax.put(top.restAjax.path('api/file/media/video/convert/{fileId}', [data.fileId]), {}, null, function (code, data) {
top.dialog.msg('正在转码...');
reloadTable();
}, function (code, data) {
top.dialog.msg(data.msg);
}, function () {
layIndex = top.dialog.msg(top.dataMessage.deleting, {icon: 16, time: 0, shade: 0.3});
}, function () {
top.dialog.close(layIndex);
});
});
}
});
$('.video-close-btn').on('click', function() {
ckPlayer.videoPause();
$('#videoBoxBackGround').hide();
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,75 @@
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<base th:href="${#request.getContextPath() + '/'} ">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8"/>
<link rel="stylesheet" type="text/css" href="assets/js/vendor/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="assets/js/vendor/bootstrap-fileupload/css/fileinput.min.css"/>
<style>
.krajee-default.file-preview-frame .kv-file-content {height: 150px !important;}
.krajee-default.file-preview-frame .file-thumbnail-footer {height: 70px !important;;}
.krajee-default .file-footer-caption {margin-bottom:30px !important;}
.krajee-default .file-thumb-progress .progress, .krajee-default .file-thumb-progress .progress-bar {height: 20px !important;}
</style>
</head>
<body>
<div id="content" class="edit-content" style="padding: 5px;">
<div class="tile color">
<form id="form" method="post" class="form-horizontal" role="form" onsubmit="return doSubmitForm()">
<input id="uploadFile" th:name="video" type="file" multiple data-theme="fas" data-preview-file-type="text">
</form>
</div>
</div>
<input type="hidden" id="types" th:value="${types}"/>
<input type="hidden" id="maxUploadCount" th:value="${maxUploadCount}"/>
<script type="text/javascript" src="assets/js/jquery-3.5.1.min.js"></script>
<script type="text/javascript" src="assets/js/vendor/bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="assets/js/vendor/bootstrap-fileupload/js/fileinput.js"></script>
<script type="text/javascript" src="assets/js/vendor/bootstrap-fileupload/js/locales/zh.js"></script>
<script type="text/javascript">
var customType = top.restAjax.params(window.location.href).customType;
var uploadFileArray = [];
function closeBox() {
top.dialog.closeBox();
}
function doSubmitForm() {
return false;
}
$(function () {
$('#uploadFile').fileinput({
language: 'zh', // 中文
uploadUrl: top.restAjax.path('api/file/media/video/upload', []),
allowedFileExtensions: customType ? customType.split(',') : $('#types').val().split(','), // 文件后缀
showUpload: false, // 显示上传按钮
showRemove: false, // 显示删除按钮
showPreview: true, // 是否预览
showCaption: false, // 是否标题
showClose: false, // 右上角关闭
showCancel: true, // 取消上传
dropZoneEnabled: true, // 是否拖拽区域
minFileCount: 0,
maxFileCount: $('#maxUploadCount').val(), // 表示允许同时上传的最大文件个数
enctype: 'multipart/form-data',
previewFileType: 'any' // 预览文件格式
}).on('fileuploaded', function (event, data, previewId, index) {
var id = data.response;
uploadFileArray.push(id);
top.dialog.dialogData.uploadFileArray = uploadFileArray;
if(uploadFileArray.length >= maxFileCount) {
$('.btn-file').hide();
}
}).on('filesuccessremove', function(event, previewId, index) {
uploadFileArray.splice(index, 1);
if(uploadFileArray.length < maxFileCount) {
$('.btn-file').show();
}
top.dialog.dialogData.uploadFileArray = uploadFileArray;
}).on('fileerror', function(event, data, msg) {
top.dialog.msg(data.response.msg);
});
})
</script>
</body>
</html>