Day14_媒资管理
1 视频处理
1.1 需求分析
原始视频通常需要经过编码处理,生成m3u8和ts文件方可基于HLS协议播放视频。通常用户上传原始视频,系统 自动处理成标准格式,系统对用户上传的视频自动编码、转换,最终生成m3u8文件和ts文件,处理流程如下:
1、用户上传视频成功
2、系统对上传成功的视频自动开始编码处理
3、用户查看视频处理结果,没有处理成功的视频用户可在管理界面再次触发处理
4、视频处理完成将视频地址及处理结果保存到数据库
视频处理流程如下:
视频处理进程的任务是接收视频处理消息进行视频处理,业务流程如下:
1、监听MQ,接收视频处理消息。
2、进行视频处理。
3、向数据库写入视频处理结果。
视频处理进程属于媒资管理系统的一部分,考虑提高系统的扩展性,将视频处理单独定义视频处理工程。
1.2 视频处理开发
1.2.1 视频处理工程创建
1、导入“资料”下的视频处理工程:xc-service-manage-media-processor
2、RabbitMQ配置
使用rabbitMQ的routing交换机模式,视频处理程序监听视频处理队列,如下图:
RabbitMQ配置如下:
package com.xuecheng.manage_media_process.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Administrator
* @version 1.0
* @create 2018-07-12 9:04
**/
@Configuration
public class RabbitMQConfig {
public static final String EX_MEDIA_PROCESSTASK = "ex_media_processor";
//视频处理队列
@Value("${xc-service-manage-media.mq.queue-media-video-processor}")
public String queue_media_video_processtask;
//视频处理路由
@Value("${xc-service-manage-media.mq.routingkey-media-video}")
public String routingkey_media_video;
//消费者并发数量
public static final int DEFAULT_CONCURRENT = 10;
/**
* 交换机配置
* @return the exchange
*/
@Bean(EX_MEDIA_PROCESSTASK)
public Exchange EX_MEDIA_VIDEOTASK() {
return ExchangeBuilder.directExchange(EX_MEDIA_PROCESSTASK).durable(true).build();
}
//声明队列
@Bean("queue_media_video_processtask")
public Queue QUEUE_PROCESSTASK() {
Queue queue = new Queue(queue_media_video_processtask,true,false,true);
return queue;
}
/**
* 绑定队列到交换机 .
* @param queue the queue
* @param exchange the exchange
* @return the binding
*/
@Bean
public Binding binding_queue_media_processtask(@Qualifier("queue_media_video_processtask") Queue queue, @Qualifier(EX_MEDIA_PROCESSTASK) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(routingkey_media_video).noargs();
}
}
在application.yml中配置队列名称及routingkey
server:
port: 31450
spring:
application:
name: xc-service-manage-media-processor
data:
mongodb:
uri: mongodb://localhost:27017
database: xc_media
#rabbitmq配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
xc-service-manage-media:
mq:
queue-media-video-processor: queue_media_video_processor
routingkey-media-video: routingkey_media_video
video-location: /Users/XinxingWang/Development/Java/video
ffmpeg-path: /usr/local/Cellar/ffmpeg/4.3.1/bin/ffmpeg
1.2.2 视频处理技术方案
如何通过程序进行视频处理?
ffmpeg是一个可行的视频处理程序,可以通过Java调用ffmpeg.exe完成视频处理。
在java中可以使用Runtime类和Process Builder类两种方式来执行外部程序,工作中至少掌握一种。
本项目使用Process Builder的方式来调用ffmpeg完成视频处理。
关于Process Builder的测试如下:
package com.xuecheng.manage_media_process;
import com.xuecheng.framework.utils.Mp4VideoUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* @author Administrator
* @version 1.0
* @create 2018-07-12 9:11
**/
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestProcessBuilder {
@Test
public void testProcessBuilder() throws IOException {
//创建ProcessBuilder对象
ProcessBuilder processBuilder =new ProcessBuilder();
//设置执行的第三方程序(命令)
// processBuilder.command("ping","127.0.0.1");
processBuilder.command("ifconfig");
// processBuilder.command("java","-jar","f:/xc-service-manage-course.jar");
//将标准输入流和错误输入流合并,通过标准输入流读取信息就可以拿到第三方程序输出的错误信息、正常信息
processBuilder.redirectErrorStream(true);
//启动一个进程
Process process = processBuilder.start();
//由于前边将错误和正常信息合并在输入流,只读取输入流
InputStream inputStream = process.getInputStream();
//将字节流转成字符流
InputStreamReader reader = new InputStreamReader(inputStream,"gbk");
//字符缓冲区
char[] chars = new char[1024];
int len = -1;
while((len = reader.read(chars))!=-1){
String string = new String(chars,0,len);
System.out.println(string);
}
inputStream.close();
reader.close();
}
//测试使用工具类将avi转成mp4
@Test
public void testProcessMp4(){
//String ffmpeg_path, String video_path, String mp4_name, String mp4folder_path
//ffmpeg的路径
String ffmpeg_path = "/usr/local/Cellar/ffmpeg/4.3.1/bin/ffmpeg";
//video_path视频地址
String video_path = "/Users/XinxingWang/Development/Java/video/solr.avi";
//mp4_name mp4文件名称
String mp4_name ="1.mp4";
//mp4folder_path mp4文件目录路径
String mp4folder_path="/Users/XinxingWang/Development/Java/video/upload/";
Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4folder_path);
//开始编码,如果成功返回success,否则返回输出的日志
String result = mp4VideoUtil.generateMp4();
System.out.println(result);
}
}
转换视频工具类,参见:
上边的工具类中:
Mp4VideoUtil.java完成avi转mp4
HlsVideoUtil.java完成mp4转hls
1.2.3 视频处理实现
1.2.3.1 确定消息格式
MQ消息统一采用json格式,视频处理生产方会向MQ发送如下消息,视频处理消费方接收此消息后进行视频处 理:
{“mediaId”:XXX}
1.2.3.2 处理流程
1)接收视频处理消息
2)判断媒体文件是否需要处理(本视频处理程序目前只接收avi视频的处理)
当前只有avi文件需要处理,其它文件需要更新处理状态为“无需处理”。
3)处理前初始化处理状态为“未处理”
4)处理失败需要在数据库记录处理日志,及处理状态为“处理失败”
5)处理成功记录处理状态为“处理成功”
1.2.3.3 数据模型
在MediaFile类中添加mediaFileProcess_m3u8属性记录ts文件列表,代码如下:
package com.xuecheng.framework.domain.media;
import lombok.Data;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
/**
* @Author: mrt.
* @Description:
* @Date:Created in 2018/1/24 10:04.
* @Modified By:
*/
@Data
@ToString
@Document(collection = "media_file")
public class MediaFile {
/*
文件id、名称、大小、文件类型、文件状态(未上传、上传完成、上传失败)、上传时间、视频处理方式、视频处理状态、hls_m3u8,hls_ts_list、课程视频信息(课程id、章节id)
*/
@Id
//文件id
private String fileId;
//文件名称
private String fileName;
//文件原始名称
private String fileOriginalName;
//文件路径
private String filePath;
//文件url
private String fileUrl;
//文件类型
private String fileType;
//mimetype
private String mimeType;
//文件大小
private Long fileSize;
//文件状态
private String fileStatus;
//上传时间
private Date uploadTime;
//处理状态
private String processStatus;
//hls处理
private MediaFileProcess_m3u8 mediaFileProcess_m3u8;
//tag标签用于查询
private String tag;
}
package com.xuecheng.framework.domain.media;
import lombok.Data;
import lombok.ToString;
import java.util.List;
/**
* @Author: mrt.
* @Description:
* @Date:Created in 2018/1/24 10:04.
* @Modified By:
*/
@Data
@ToString
public class MediaFileProcess_m3u8 extends MediaFileProcess {
//ts列表
private List<String> tslist;
}
1.2.3.4 视频处理生成Mp4
1、创建Dao
视频处理结果需要保存到媒资数据库,创建dao如下:
package com.xuecheng.manage_media_process.dao;
import com.xuecheng.framework.domain.media.MediaFile;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface MediaFileRepository extends MongoRepository<MediaFile,String> {
}
2、在application.yml中配置ffmpeg的位置及视频目录的根目录:
server:
port: 31450
spring:
application:
name: xc-service-manage-media-processor
data:
mongodb:
uri: mongodb://localhost:27017
database: xc_media
#rabbitmq配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
xc-service-manage-media:
mq:
queue-media-video-processor: queue_media_video_processor
routingkey-media-video: routingkey_media_video
video-location: /Users/XinxingWang/Development/Java/video
ffmpeg-path: /usr/local/Cellar/ffmpeg/4.3.1/bin/ffmpeg
3、处理任务类
在mq包下创建MediaProcessTask类,此类负责监听视频处理队列,并进行视频处理。
整个视频处理内容较多,这里分两部分实现:生成Mp4和生成m3u8。
package com.xuecheng.manage_media_process.mq;
import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.MediaFileProcess_m3u8;
import com.xuecheng.framework.utils.HlsVideoUtil;
import com.xuecheng.framework.utils.Mp4VideoUtil;
import com.xuecheng.manage_media_process.dao.MediaFileRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* @author HackerStar
* @create 2020-08-29 15:29
*/
@Component
public class MediaProcessTask {
private static final Logger LOGGER = LoggerFactory.getLogger(MediaProcessTask.class);
//ffmpeg绝对路径
@Value("${xc‐service‐manage‐media.ffmpeg‐path}")
String ffmpeg_path;
//上传文件根目录
@Value("${xc‐service‐manage‐media.video-location}")
String serverPath;
@Autowired
MediaFileRepository mediaFileRepository;
@RabbitListener(queues="${xc-service-manage-media.mq.queue-media-video-processor}")
public void receiveMediaProcessTask(String msg) throws IOException {
Map msgMap = JSON.parseObject(msg, Map.class);
LOGGER.info("receive media process task msg :{} ", msgMap);
//解析消息
//媒资文件id
String mediaId = (String) msgMap.get("mediaId");
//获取媒资文件信息
Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);
if (!optional.isPresent()) {
return;
}
MediaFile mediaFile = optional.get();
//媒资文件类型
String fileType = mediaFile.getFileType();
if (fileType == null || !fileType.equals("avi")) {//目前只处理avi文件
mediaFile.setProcessStatus("303004");//处理状态为无需处理
mediaFileRepository.save(mediaFile);
return;
} else {
mediaFile.setProcessStatus("303001");//处理状态为未处理
mediaFileRepository.save(mediaFile);
}
//生成mp4
String video_path = serverPath + mediaFile.getFilePath() + mediaFile.getFileName();
String mp4_name = mediaFile.getFileId() + ".mp4";
String mp4folder_path = serverPath + mediaFile.getFilePath();
Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path, video_path, mp4_name, mp4folder_path);
String result = videoUtil.generateMp4();
if (result == null || !result.equals("success")) {
//操作失败写入处理日志
mediaFile.setProcessStatus("303003");//处理状态为处理失败
mediaFile.setProcessStatus("303003");//处理状态为处理失败
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
mediaFileProcess_m3u8.setErrormsg(result);
mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);
mediaFileRepository.save(mediaFile);
return;
}
//生成m3u8
video_path = serverPath + mediaFile.getFilePath() + mp4_name;//此地址为mp4的地址
String m3u8_name = mediaFile.getFileId() + ".m3u8";
String m3u8folder_path = serverPath + mediaFile.getFilePath() + "hls/";
HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path, video_path, m3u8_name, m3u8folder_path);
result = hlsVideoUtil.generateM3u8();
if (result == null || !result.equals("success")) {
//操作失败写入处理日志
mediaFile.setProcessStatus("303003");//处理状态为处理失败
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
mediaFileProcess_m3u8.setErrormsg(result);
mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);
mediaFileRepository.save(mediaFile);
return;
}
//获取m3u8列表
List<String> ts_list = hlsVideoUtil.get_ts_list();
//更新处理状态为成功
mediaFile.setProcessStatus("303002");//处理状态为处理成功
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
mediaFileProcess_m3u8.setTslist(ts_list);
mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);
//m3u8文件url
mediaFile.setFileUrl(mediaFile.getFilePath() + "hls/" + m3u8_name);
mediaFileRepository.save(mediaFile);
}
}
说明:
mp4转成m3u8如何判断转换成功?
第一、根据视频时长来判断,同mp4转换成功的判断方法。
第二、最后还要判断m3u8文件内容是否完整。
1.3 发送视频处理消息
当视频上传成功后向 MQ 发送视频处理消息。
修改媒资管理服务的文件上传代码,当文件上传成功向MQ发送视频处理消息。
1.3.1 RabbitMQ配置
1、将media-processor工程下的RabbitmqConfig配置类拷贝到media工程下
2、在media工程下配置mq队列等信息
修改application.yml
server:
port: 31400
spring:
application:
name: xc-service-manage-media
data:
mongodb:
uri: mongodb://localhost:27017
database: xc_media
eureka:
client:
registerWithEureka: true #服务注册开关
fetchRegistry: true #服务发现开关
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔
defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/}
instance:
prefer-ip-address: true #将自己的ip地址注册到Eureka服务中
ip-address: ${IP_ADDRESS:127.0.0.1}
instance-id: ${spring.application.name}:${server.port} #指定实例id
ribbon:
MaxAutoRetries: 2 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试,如果eureka中找不到服务则直接走断路器
MaxAutoRetriesNextServer: 3 #切换实例的重试次数
OkToRetryOnAllOperations: false #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为false
ConnectTimeout: 5000 #请求连接的超时时间
ReadTimeout: 6000 #请求处理的超时时间
xc-service-manage-media:
upload-location: /Users/XinxingWang/Development/Java/video/upload
mq:
queue‐media‐video‐processor: queue_media_video_processor
routingkey‐media‐video: routingkey_media_video
1.3.2 修改Service
在文件合并方法中添加向mq发送视频处理消息的代码:
在mergechunks方法最后调用sendProcessVideo方法。
//向MQ发送视频处理消息
public ResponseResult sendProcessVideoMsg(String mediaId) {
Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);
if (!optional.isPresent()) {
return new ResponseResult(CommonCode.FAIL);
}
MediaFile mediaFile = optional.get();
Map<String, String> msgMap = new HashMap<>();
msgMap.put("mediaId", mediaId);
//发送的消息
String msg = JSON.toJSONString(msgMap);
try {
this.rabbitTemplate.convertAndSend(RabbitMQConfig.EX_MEDIA_PROCESSTASK, routingkey_media_video, msg);
LOGGER.info("send media process task msg:{}", msg);
} catch (Exception e) {
e.printStackTrace();
LOGGER.info("send media process task error,msg is:{},error:{}", msg, e.getMessage());
return new ResponseResult(CommonCode.FAIL);
}
return new ResponseResult(CommonCode.SUCCESS);
}
......
//状态为上传成功
mediaFile.setFileStatus("301002");
mediaFileRepository.save(mediaFile);
String mediaId = mediaFile.getFileId();
//向MQ发送视频处理消息
sendProcessVideoMsg(mediaId);
......
完整代码:
package com.xuecheng.manage_media.service;
import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.domain.media.response.MediaCode;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_media.config.RabbitMQConfig;
import com.xuecheng.manage_media.controller.MediaUploadController;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.*;
/**
* @author HackerStar
* @create 2020-08-28 15:04
*/
@Service
public class MediaUploadService {
private final static Logger LOGGER = LoggerFactory.getLogger(MediaUploadController.class);
@Autowired
MediaFileRepository mediaFileRepository;
@Autowired
RabbitTemplate rabbitTemplate;
//上传文件根目录
@Value("${xc-service-manage-media.upload-location}")
String uploadPath;
@Value("${xc-service-manage-media.mq.routingkey‐media‐video}")
String routingkey_media_video;
/**
* 根据文件md5得到文件路径
* * 规则:
* * 一级目录:md5的第一个字符
* * 二级目录:md5的第二个字符
* * 三级目录:md5
* * 文件名:md5+文件扩展名
* * @param fileMd5 文件md5值
* * @param fileExt 文件扩展名
* * @return 文件路径
*/
private String getFilePath(String fileMd5, String fileExt) {
String filePath = uploadPath + "/" + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + "." + fileExt;
return filePath;
}
//得到文件目录相对路径,路径中去掉根目录
private String getFileFolderRelativePath(String fileMd5, String fileExt) {
String filePath = fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";
return filePath;
}
//得到文件所在目录
private String getFileFolderPath(String fileMd5) {
String fileFolderPath = uploadPath + "/" + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";
return fileFolderPath;
}
//创建文件目录
private boolean createFileFold(String fileMd5) {
//创建上传文件目录
String fileFolderPath = getFileFolderPath(fileMd5);
File fileFolder = new File(fileFolderPath);
if (!fileFolder.exists()) {
//创建文件夹
boolean mkdirs = fileFolder.mkdirs();
return mkdirs;
}
return true;
}
//文件上传注册
public ResponseResult register(String fileMd5, String fileName, String fileSize, String mimetype, String fileExt) {
//检查文件是否上传
//1、得到文件的路径
String filePath = getFilePath(fileMd5, fileExt);
File file = new File(filePath);
//2、查询数据库文件是否存在
Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5);
//文件存在直接返回
if (file.exists() && optional.isPresent()) {
ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
}
boolean fileFold = createFileFold(fileMd5);
if (!fileFold) {
//上传文件目录创建失败
ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_CREATEFOLDER_FAIL);
}
return new ResponseResult(CommonCode.SUCCESS);
}
//得到块文件所在目录
private String getChunkFileFolderPath(String fileMd5) {
String fileChunkFolderPath = getFileFolderPath(fileMd5) + "/" + "chunks" + "/";
return fileChunkFolderPath;
}
//检查块文件
public CheckChunkResult checkchunk(String fileMd5, String chunk, String chunkSize) {
//得到块文件所在路径
String chunkfileFolderPath = getChunkFileFolderPath(fileMd5);
//块文件的文件名称以1,2,3..序号命名,没有扩展名
File chunkFile = new File(chunkfileFolderPath + chunk);
if (chunkFile.exists()) {
return new CheckChunkResult(MediaCode.CHUNK_FILE_EXIST_CHECK, true);
} else {
return new CheckChunkResult(MediaCode.CHUNK_FILE_EXIST_CHECK, false);
}
}
//块文件上传
public ResponseResult uploadchunk(MultipartFile file, String fileMd5, String chunk) {
if (file == null) {
ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_ISNULL);
}
//创建块文件目录
boolean fileFold = createChunkFileFolder(fileMd5);
//块文件
File chunkfile = new File(getChunkFileFolderPath(fileMd5) + chunk);
//上传的块文件
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = file.getInputStream();
outputStream = new FileOutputStream(chunkfile);
IOUtils.copy(inputStream, outputStream);
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("upload chunk file fail:{}", e.getMessage());
ExceptionCast.cast(MediaCode.CHUNK_FILE_UPLOAD_FAIL);
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return new ResponseResult(CommonCode.SUCCESS);
}
//创建块文件目录
private boolean createChunkFileFolder(String fileMd5) {
//创建上传文件目录
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
File chunkFileFolder = new File(chunkFileFolderPath);
if (!chunkFileFolder.exists()) {
//创建文件夹
boolean mkdirs = chunkFileFolder.mkdirs();
return mkdirs;
}
return true;
}
//合并块文件
public ResponseResult mergechunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
//获取块文件的路径
String chunkfileFolderPath = getChunkFileFolderPath(fileMd5);
File chunkfileFolder = new File(chunkfileFolderPath);
if (!chunkfileFolder.exists()) {
chunkfileFolder.mkdirs();
}
//合并文件路径
File mergeFile = new File(getFilePath(fileMd5, fileExt));
//创建合并文件
//合并文件存在先删除再创建
if (mergeFile.exists()) {
mergeFile.delete();
}
boolean newFile = false;
try {
newFile = mergeFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
LOGGER.error("mergechunks..create mergeFile fail:{}", e.getMessage());
}
if (!newFile) {
ExceptionCast.cast(MediaCode.MERGE_FILE_CREATEFAIL);
}
//获取块文件,此列表是已经排好序的列表
List<File> chunkFiles = getChunkFiles(chunkfileFolder);
//合并文件
mergeFile = mergeFile(mergeFile, chunkFiles);
if (mergeFile == null) {
ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
}
//校验文件
boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);
if (!checkResult) {
ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);
}
//将文件信息保存到数据库
MediaFile mediaFile = new MediaFile();
mediaFile.setFileId(fileMd5);
mediaFile.setFileName(fileMd5 + "." + fileExt);
mediaFile.setFileOriginalName(fileName);
//文件路径保存相对路径
mediaFile.setFilePath(getFileFolderRelativePath(fileMd5, fileExt));
mediaFile.setFileSize(fileSize);
mediaFile.setUploadTime(new Date());
mediaFile.setMimeType(mimetype);
mediaFile.setFileType(fileExt);
//状态为上传成功
mediaFile.setFileStatus("301002");
MediaFile save = mediaFileRepository.save(mediaFile);
this.sendProcessVideoMsg(fileMd5);
return new ResponseResult(CommonCode.SUCCESS);
}
//校验文件的md5值
private boolean checkFileMd5(File mergeFile, String md5) {
if (mergeFile == null || StringUtils.isEmpty(md5)) {
return false;
}
//进行md5校验
FileInputStream mergeFileInputstream = null;
try {
mergeFileInputstream = new FileInputStream(mergeFile);
//得到文件的md5
String mergeFileMd5 = DigestUtils.md5Hex(mergeFileInputstream);
//比较md5
if (md5.equalsIgnoreCase(mergeFileMd5)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("checkFileMd5 error,file is:{},md5 is: {}", mergeFile.getAbsoluteFile(), md5);
} finally {
try {
mergeFileInputstream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
//获取所有块文件
private List<File> getChunkFiles(File chunkfileFolder) {
//获取路径下的所有块文件
File[] chunkFiles = chunkfileFolder.listFiles();
//将文件数组转成list,并排序
List<File> chunkFileList = new ArrayList<File>();
chunkFileList.addAll(Arrays.asList(chunkFiles));
//排序
Collections.sort(chunkFileList, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
if (Integer.parseInt(o1.getName()) > Integer.parseInt(o2.getName())) {
return 1;
}
return -1;
}
});
return chunkFileList;
}
//合并文件
private File mergeFile(File mergeFile, List<File> chunkFiles) {
try {
//创建写文件对象
RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");
//遍历分块文件开始合并
//读取文件缓冲区
byte[] b = new byte[1024];
for (File chunkFile : chunkFiles) {
RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "r");
int len = -1;
//读取分块文件
while ((len = raf_read.read(b)) != -1) {
//向合并文件中写数据
raf_write.write(b, 0, len);
}
raf_read.close();
}
raf_write.close();
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("merge file error:{}", e.getMessage());
return null;
}
return mergeFile;
}
//向MQ发送视频处理消息
public ResponseResult sendProcessVideoMsg(String mediaId) {
Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);
if (!optional.isPresent()) {
return new ResponseResult(CommonCode.FAIL);
}
MediaFile mediaFile = optional.get();
Map<String, String> msgMap = new HashMap<>();
msgMap.put("mediaId", mediaId);
//发送的消息
String msg = JSON.toJSONString(msgMap);
try {
this.rabbitTemplate.convertAndSend(RabbitMQConfig.EX_MEDIA_PROCESSTASK, routingkey_media_video, msg);
LOGGER.info("send media process task msg:{}", msg);
} catch (Exception e) {
e.printStackTrace();
LOGGER.info("send media process task error,msg is:{},error:{}", msg, e.getMessage());
return new ResponseResult(CommonCode.FAIL);
}
return new ResponseResult(CommonCode.SUCCESS);
}
}
1.4 视频处理测试
测试流程:
1、上传avi文件
2、观察日志是否发送消息
3、观察视频处理进程是否接收到消息进行处理
4、观察mp4文件是否生成
5、观察m3u8及ts文件是否生成
1.5 视频处理并发设置
代码中使用@RabbitListener注解指定消费方法,默认情况是单线程监听队列,可以观察当队列有多个任务时消费 端每次只消费一个消息,单线程处理消息容易引起消息处理缓慢,消息堆积,不能最大利用硬件资源。
可以配置mq的容器工厂参数,增加并发处理数量即可实现多线程处理监听队列,实现多线程处理消息。
1、在RabbitmqConfig.java中添加容器工厂配置:
//消费者并发数量
public static final int DEFAULT_CONCURRENT = 10;
@Bean("customContainerFactory")
public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConcurrentConsumers(DEFAULT_CONCURRENT);
factory.setMaxConcurrentConsumers(DEFAULT_CONCURRENT);
configurer.configure(factory, connectionFactory);
return factory;
}
2、在@RabbitListener注解中指定容器工厂
//视频处理方法
@RabbitListener(queues = {"${xc‐service‐manage‐media.mq.queue‐media‐video‐processor}"},containerFactory="customContainerFactory")
再次测试当队列有多个任务时消费端的并发处理能力。
2 我的媒资
2.1 需求分析
通过我的媒资可以查询本教育机构拥有的媒资文件,进行文件处理、删除文件、修改文件信息等操作,具体需求如 下:
1、分页查询我的媒资文件
2、删除媒资文件
3、处理媒资文件
4、修改媒资文件信息
2.2 API
本节讲解我的媒资文件分页查询、处理媒资文件,其它功能请学员自行实现。
package com.xuecheng.api.media;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.QueryResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
/**
* Created by Administrator.
*/
@Api(value = "媒体文件管理",description = "媒体文件管理接口",tags = {"媒体文件管理接口"})
public interface MediaFileControllerApi {
@ApiOperation("我的媒资文件查询列表")
public QueryResponseResult<MediaFile> findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest);
}
2.3 服务端开发
2.3.1 Dao
package com.xuecheng.manage_media_process.dao;
import com.xuecheng.framework.domain.media.MediaFile;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface MediaFileRepository extends MongoRepository<MediaFile,String> {
}
2.3.2 Service
package com.xuecheng.manage_media.service;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.QueryResult;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
/**
* @author HackerStar
* @create 2020-08-31 09:03
*/
@Service
public class MediaFileService {
private static Logger logger = LoggerFactory.getLogger(MediaFileService.class);
@Autowired
MediaFileRepository mediaFileRepository;
//文件列表分页查询
public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest) {
//查询条件
MediaFile mediaFile = new MediaFile();
if (queryMediaFileRequest == null) {
queryMediaFileRequest = new QueryMediaFileRequest();
}
//查询条件匹配器
ExampleMatcher matcher = ExampleMatcher.matching()
.withMatcher("tag", ExampleMatcher.GenericPropertyMatchers.contains())//tag字段模糊匹配
.withMatcher("fileOriginalName", ExampleMatcher.GenericPropertyMatchers.contains())//文件原始名称模糊匹配
.withMatcher("processStatus", ExampleMatcher.GenericPropertyMatchers.exact());//处理状态精确匹配(默认)
//查询条件对象
if (StringUtils.isNotEmpty(queryMediaFileRequest.getTag())) {
mediaFile.setTag(queryMediaFileRequest.getTag());
}
if (StringUtils.isNotEmpty(queryMediaFileRequest.getFileOriginalName())) {
mediaFile.setFileOriginalName(queryMediaFileRequest.getFileOriginalName());
}
if (StringUtils.isNotEmpty(queryMediaFileRequest.getProcessStatus())) {
mediaFile.setProcessStatus(queryMediaFileRequest.getProcessStatus());
}
//定义example实例
Example<MediaFile> ex = Example.of(mediaFile, matcher);
page = page - 1;
//分页参数
Pageable pageable = new PageRequest(page, size);
//分页查询
Page<MediaFile> all = mediaFileRepository.findAll(ex, pageable);
QueryResult<MediaFile> mediaFileQueryResult = new QueryResult<MediaFile>();
mediaFileQueryResult.setList(all.getContent());
mediaFileQueryResult.setTotal(all.getTotalElements());
return new QueryResponseResult(CommonCode.SUCCESS, mediaFileQueryResult);
}
}
2.3.3 Controller
package com.xuecheng.manage_media.controller;
import com.xuecheng.api.media.MediaFileControllerApi;
import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.manage_media.service.MediaFileService;
import com.xuecheng.manage_media.service.MediaUploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @author HackerStar
* @create 2020-08-31 09:09
*/
@RestController
@RequestMapping("/media/file")
public class MediaFileController implements MediaFileControllerApi {
@Autowired
MediaFileService mediaFileService;
@Autowired
MediaUploadService mediaUploadService;
@Override
@GetMapping("/list/{page}/{size}")
public QueryResponseResult findList(@PathVariable("page") int page, @PathVariable("size") int size, QueryMediaFileRequest queryMediaFileRequest) {
//媒资文件查询
return mediaFileService.findList(page, size, queryMediaFileRequest);
}
}
2.4 前端开发
2.4.1 API 方法
在media模块定义api方法如下:
import http from './../../../base/api/public'
import querystring from 'querystring'
let sysConfig = require('@/../config/sysConfig')
let apiUrl = sysConfig.xcApiUrlPre;
/*页面列表*/
export const media_list = (page, size, params) => {
//params为json格式
//使用querystring将json对象转成key/value串
let querys = querystring.stringify(params)
return http.requestQuickGet(apiUrl + '/media/file/list/' + page + '/' + size + '/?' + querys)
}
/*发送处理消息*/
export const media_process = (id) => {
return http.requestPost(apiUrl + '/media/file/process/' + id)
}
2.4.2 页面
在media模块创建media_list.vue,可参考cms系统的page_list.vue来编写此页面。
<template>
<div>
<!--查询表单-->
<el-form :model="params">
标签:
<el-input v-model="params.tag" style="width:160px"></el-input>
原始名称:
<el-input v-model="params.fileOriginalName" style="width:160px"></el-input>
处理状态:
<el-select v-model="params.processStatus" placeholder="请选择处理状态">
<el-option
v-for="item in processStatusList"
:key="item.id"
:label="item.name"
:value="item.id">
</el-option>
</el-select>
<br/>
<el-button type="primary" v-on:click="query" size="small">查询</el-button>
<router-link class="mui-tab-item" :to="{path:'/upload'}">
<el-button type="primary" size="small" v-if="ischoose != true">上传文件</el-button>
</router-link>
</el-form>
<!--列表-->
<el-table :data="list" highlight-current-row v-loading="listLoading" style="width: 100%;">
<el-table-column type="index" width="30">
</el-table-column>
<el-table-column prop="fileOriginalName" label="原始文件名称" width="220">
</el-table-column>
<el-table-column prop="fileName" label="文件名称" width="220">
</el-table-column>
<el-table-column prop="fileUrl" label="访问url" width="260">
</el-table-column>
<el-table-column prop="tag" label="标签" width="100">
</el-table-column>
<el-table-column prop="fileSize" label="文件大小" width="120">
</el-table-column>
<el-table-column prop="processStatus" label="处理状态" width="100" :formatter="formatProcessStatus">
</el-table-column>
<el-table-column prop="uploadTime" label="创建时间" width="110" :formatter="formatCreatetime">
</el-table-column>
<el-table-column label="开始处理" width="100" v-if="ischoose != true">
<template slot-scope="scope">
<el-button
size="small" type="primary" plain @click="process(scope.row.fileId)">开始处理
</el-button>
</template>
</el-table-column>
<el-table-column label="选择" width="80" v-if="ischoose == true">
<template slot-scope="scope">
<el-button
size="small" type="primary" plain @click="choose(scope.row)">选择</el-button>
</template>
</el-table-column>
</el-table>
<!--分页-->
<el-col :span="24" class="toolbar">
<el-pagination background layout="prev, pager, next" @current-change="changePage" :page-size="this.params.size"
:total="total" :current-page="this.params.page"
style="float:right;">
</el-pagination>
</el-col>
</div>
</template>
<script>
import * as mediaApi from '../api/media'
import utilApi from '@/common/utils';
export default{
props: ['ischoose'],
data(){
return {
params:{
page:1,//页码
size:10,//每页显示个数
tag:'',//标签
fileName:'',//文件名称
processStatus:''//处理状态
},
listLoading:false,
list:[],
total:0,
processStatusList:[]
}
},
methods:{
formatCreatetime(row, column){
var createTime = new Date(row.uploadTime);
if (createTime) {
return utilApi.formatDate(createTime, 'yyyy-MM-dd hh:mm:ss');
}
},
formatProcessStatus(row,column){
var processStatus = row.processStatus;
if (processStatus) {
if(processStatus == '303001'){
return "处理中";
}else if(processStatus == '303002'){
return "处理成功";
}else if(processStatus == '303003'){
return "处理失败";
}else if(processStatus == '303004'){
return "无需处理";
}
}
},
choose(mediaFile){
if(mediaFile.processStatus !='303002' && mediaFile.processStatus !='303004'){
this.$message.error('该文件未处理,不允许选择');
return ;
}
if(!mediaFile.fileUrl){
this.$message.error('该文件的访问url为空,不允许选择');
return ;
}
//调用父组件的choosemedia方法
this.$emit('choosemedia',mediaFile.fileId,mediaFile.fileOriginalName,mediaFile.fileUrl);
},
changePage(page){
this.params.page = page;
this.query()
},
process (id) {
// console.log(id)
mediaApi.media_process(id).then((res)=>{
console.log(res)
if(res.success){
this.$message.success('开始处理,请稍后查看处理结果');
}else{
this.$message.error('操作失败,请刷新页面重试');
}
})
},
query(){
mediaApi.media_list(this.params.page,this.params.size,this.params).then((res)=>{
console.log(res)
this.total = res.queryResult.total
this.list = res.queryResult.list
})
}
},
created(){
//默认第一页
this.params.page = Number.parseInt(this.$route.query.page||1);
},
mounted() {
//默认查询页面
this.query()
//初始化处理状态
this.processStatusList = [
{
id:'',
name:'全部'
},
{
id:'303001',
name:'处理中'
},
{
id:'303002',
name:'处理成功'
},
{
id:'303003',
name:'处理失败'
},
{
id:'303004',
name:'无需处理'
}
]
}
}
</script>
<style>
</style>
3 媒资与课程计划关联
3.1 需求分析
到目前为止,媒资管理已完成文件上传、视频处理、我的媒资功能等基本功能。其它模块已可以使用媒资管理功 能,本节要讲解课程计划在编辑时如何选择媒资文件。
操作的业务流程如下:
1、进入课程计划修改页面
2、选择视频
打开媒资文件查询窗口,找到该课程章节的视频,选择此视频。
点击“选择媒资文件”打开媒资文件列表
3、 选择成功后,将在课程管理数据库保存课程计划对应在的课程视频地址。
在课程管理数据库创建表 teachplan_media 存储课程计划与媒资关联信息,如下:
3.2 选择视频
3.2.1 Vue父子组件通信
上一章已实现了我的媒资页面,所以媒资查询窗口页面不需要再开发,将“我的媒资页面”作为一个组件在修改课程 计划页面中引用,如下图:
修改课程计划页面为父组件,我的媒资查询页面为子组件。
问题1:
我的媒资页面在选择媒资文件时不允许显示,比如“视频处理”按钮,该如何控制?
这时就需要父组件(修改课程计划页面)向子组件(我的媒资页面)传入一个变量,使用此变量来控制当前是否进 入选择媒资文件业务,从而控制哪些元素不显示,如下图:
问题2:
在我的媒资页面选择了媒资文件,如何将选择的媒资文件信息传到父组件?
这时就需要子组件调用父组件的方法来解决此问题,如下图:
3.2.2 父组件(修改课程计划)
本节实现功能:在课程计划页面打开我的媒资页面。(course/page/course_manage/course_summary.vue)
1、引入子组件
import mediaList from '@/module/media/page/media_list.vue';
export default {
components:{
mediaList
},
data() {
......
2、使用子组件
在父组件的视图中使用子组件,同时传入变量ischoose,并指定父组件的方法名为choosemedia
这里使用el-dialog 实现弹出窗口。
<el-dialog title="选择媒资文件" :visible.sync="mediaFormVisible">
<media-list v-bind:ischoose="true" @choosemedia="choosemedia"></media-list>
</el-dialog>
3、choosemedia方法
在父组件中定义choosemedia方法,接收子组件调用,参数包括:媒资文件id、媒资文件的原始名称、媒资文件 url
//保存选择的视频
choosemedia(mediaId,fileOriginalName,mediaUrl){
//保存视频到课程计划表中
let teachplanMedia ={}
teachplanMedia.mediaId =mediaId;
teachplanMedia.mediaFileOriginalName =fileOriginalName;
teachplanMedia.mediaUrl =mediaUrl;
teachplanMedia.courseId =this.courseid;
//课程计划
teachplanMedia.teachplanId=this.teachplanId
courseApi.savemedia(teachplanMedia).then(res=>{
if(res.success){
this.$message.success("选择视频成功")
//查询课程计划
this.findTeachplan()
}else{
this.$message.error(res.message)
}
})
},
4、打开子组件窗口
1)打开子组件窗口按钮定义
<el-button style="font-size: 12px;" type="text" on-click={ () => this.choosevideo(data) }>{data.mediaFileOriginalName} 选择视频</el-button>
效果如下:
- 打开子组件窗口方法
定义querymedia方法:
//选择视频,打开窗口
choosevideo(data){
//得到当前的课程计划
this.teachplanId = data.id
// alert(this.teachplanId)
this.mediaFormVisible = true;//打开窗口
},
3.2.3 子组件(我的媒资查询)
1、定义ischoose变量,接收父组件传入的ischoose(media_list.vue)
export default{
props: ['ischoose'],
data(){
return {
2、父组件传的ischoose变量为 true时表示当前是选择媒资文件业务,需要控制页面元素是否显示
1)ischoose=true,选择按钮显示
<el-table-column label="选择" width="80" v-if="ischoose == true">
<template slot-scope="scope">
<el-button
size="small" type="primary" plain @click="choose(scope.row)">选择</el-button>
</template>
</el-table-column>
2 )ischoose=false,视频处理按钮显示
<el-table-column label="开始处理" width="100" v-if="ischoose != true">
<template slot-scope="scope">
<el-button
size="small" type="primary" plain @click="process(scope.row.fileId)">开始处理
</el-button>
</template>
</el-table-column>
3、选择媒资文件方法
用户点击“选择”按钮将向父组件传递媒资文件信息
choose(mediaFile){
if(mediaFile.processStatus !='303002' && mediaFile.processStatus !='303004'){
this.$message.error('该文件未处理,不允许选择');
return ;
}
if(!mediaFile.fileUrl){
this.$message.error('该文件的访问url为空,不允许选择');
return ;
}
//调用父组件的choosemedia方法 this.$emit('choosemedia',mediaFile.fileId,mediaFile.fileOriginalName,mediaFile.fileUrl);
},
3.3 保存视频信息
3.3.1 需求分析
用户进入课程计划页面,选择视频,将课程计划与视频信息保存在课程管理数据库中。
用户操作流程:
1、进入课程计划,点击”选择视频“,打开我的媒资查询页面
2、为课程计划选择对应的视频,选择“选择”
3、前端请求课程管理服务保存课程计划与视频信息。
3.3.2 数据模型
在课程管理数据库创建表 teachplan_media 存储课程计划与媒资关联信息,如下:
创建teachplanMedia 模型类:
package com.xuecheng.framework.domain.course;
import lombok.Data;
import lombok.ToString;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.io.Serializable;
/**
* Created by admin on 2018/2/7.
*/
@Data
@ToString
@Entity
@Table(name="teachplan_media")
@GenericGenerator(name = "jpa-assigned", strategy = "assigned")
public class TeachplanMedia implements Serializable {
private static final long serialVersionUID = -916357110051689485L;
@Id
@GeneratedValue(generator = "jpa-assigned")
@Column(name="teachplan_id")
private String teachplanId;
@Column(name="media_id")
private String mediaId;
@Column(name="media_fileoriginalname")
private String mediaFileOriginalName;
@Column(name="media_url")
private String mediaUrl;
private String courseId;
}
3.3.3 API接口
此接口作为前端请求课程管理服务保存课程计划与视频信息的接口:
在课程管理服务增加接口:
@ApiOperation("保存媒资信息")
public ResponseResult savemedia(TeachplanMedia teachplanMedia);
3.3.4 服务端开发
3.3.3.1 DAO
创建 TeachplanMediaRepository用于对TeachplanMedia的操作。
package com.xuecheng.manage_course.dao;
import com.xuecheng.framework.domain.course.TeachplanMedia;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @author HackerStar
* @create 2020-08-31 11:34
*/
public interface TeachplanMediaRepository extends JpaRepository<TeachplanMedia, String> {
}
3.3.3.2 Service
//保存媒资信息
public ResponseResult savemedia(@RequestBody TeachplanMedia teachplanMedia) {
if (teachplanMedia == null) {
ExceptionCast.cast(CommonCode.INVALIDPARAM);
}
//课程计划
String teachplanId = teachplanMedia.getTeachplanId();
//查询课程计划
Optional<Teachplan> optional = teachplanRepository.findById(teachplanId);
if (!optional.isPresent()) {
ExceptionCast.cast(CourseCode.COURSE_MEDIA_TEACHPLAN_ISNULL);
}
Teachplan teachplan = optional.get(); //只允许为叶子结点课程计划选择视频
String grade = teachplan.getGrade();
if (StringUtils.isEmpty(grade) || !grade.equals("3")) {
ExceptionCast.cast(CourseCode.COURSE_MEDIA_TEACHPLAN_GRADEERROR);
}
TeachplanMedia one = null;
Optional<TeachplanMedia> teachplanMediaOptional = teachplanMediaRepository.findById(teachplanId);
if (!teachplanMediaOptional.isPresent()) {
one = new TeachplanMedia();
} else {
one = teachplanMediaOptional.get();
} //保存媒资信息与课程计划信息
one.setTeachplanId(teachplanId);
one.setCourseId(teachplanMedia.getCourseId());
one.setMediaFileOriginalName(teachplanMedia.getMediaFileOriginalName());
one.setMediaId(teachplanMedia.getMediaId());
one.setMediaUrl(teachplanMedia.getMediaUrl());
teachplanMediaRepository.save(one);
return new ResponseResult(CommonCode.SUCCESS);
}
3.3.3.3 Controller
@Override
@PostMapping("/savemedia")
public ResponseResult savemedia(TeachplanMedia teachplanMedia) {
return courseService.savemedia(teachplanMedia);
}
3.3.4 前端开发
定义api方法,调用课程管理服务保存媒资信息接口
/*保存媒资信息*/
export const savemedia = teachplanMedia => {
return http.requestPost(apiUrl + '/course/savemedia', teachplanMedia);
}
3.3.4.2 API调用
在课程视频方法中调用api:
//保存选择的视频
choosemedia(mediaId,fileOriginalName,mediaUrl){
this.mediaFormVisible = false;
//保存视频到课程计划表中
let teachplanMedia ={}
teachplanMedia.teachplanId = this.teachplanId;
teachplanMedia.mediaId = mediaId;
teachplanMedia.mediaFileOriginalName = fileOriginalName;
teachplanMedia.mediaUrl = mediaUrl;
teachplanMedia.courseId = this.courseid;
//课程计划
courseApi.savemedia(teachplanMedia).then(res=>{
if(res.success){
this.$message.success("选择视频成功")
}else{
this.$message.error(res.message)
}
})
},
3.3.4 测试
1、向叶子结点课程计划保存媒资信息
操作结果:保存成功
2、向非叶子结点课程计划保存媒资信息
操作结果:保存失败
如果报数据库的错,将数据库列mediaid改为media_id
3.4 查询视频信息
3.4.1 需求分析
课程计划的视频信息保存后在页面无法查看,本节解决课程计划页面显示相关联的媒资信息。
解决方案:
在获取课程计划树结点信息时将关联的媒资信息一并查询,并在前端显示,下图说明了课程计划显示的区域。
3.4.2 Dao
修改课程计划查询的Dao:
1、修改模型
在课程计划结果信息中添加媒资信息
package com.xuecheng.framework.domain.course.ext;
import com.xuecheng.framework.domain.course.Teachplan;
import lombok.Data;
import lombok.ToString;
import java.util.List;
/**
* Created by admin on 2018/2/7.
*/
@Data
@ToString
public class TeachplanNode extends Teachplan {
List<TeachplanNode> children;
//媒资信息
private String mediaId;
private String mediaFileOriginalName;
}
2、修改sql语句,添加关联查询媒资信息
添加mediaId、mediaFileOriginalName
<?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="com.xuecheng.manage_course.dao.TeachplanMapper">
<!-- <resultMap id="teachplanMap" type="com.xuecheng.framework.domain.course.ext.TeachplanNode">-->
<!-- <id column="one_id" property="id"></id>-->
<!-- <result column="one_pname" property="pname"></result>-->
<!-- <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">-->
<!-- <id column="two_id" property="id"></id>-->
<!-- <result column="two_pname" property="pname"></result>-->
<!-- <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">-->
<!-- <id column="three_id" property="id"></id>-->
<!-- <result column="three_pname" property="pname"></result>-->
<!-- </collection>-->
<!-- </collection>-->
<!-- </resultMap>-->
<!-- <select id="selectList" parameterType="java.lang.String" resultMap="teachplanMap">-->
<!-- SELECT-->
<!-- a.id one_id,-->
<!-- a.pname one_pname,-->
<!-- b.id two_id,-->
<!-- b.pname two_pname,-->
<!-- c.id three_id,-->
<!-- c.pname three_pname-->
<!-- FROM-->
<!-- teachplan a-->
<!-- LEFT JOIN teachplan b-->
<!-- ON b.parentid = a.id-->
<!-- LEFT JOIN teachplan c-->
<!-- ON c.parentid = b.id-->
<!-- WHERE a.parentid = '0'-->
<!-- <if test="_parameter !=null and _parameter!=''">-->
<!-- AND a.courseid = #{courseId}-->
<!-- </if>-->
<!-- ORDER BY a.orderby,-->
<!-- b.orderby,-->
<!-- c.orderby-->
<!-- </select>-->
<resultMap type="com.xuecheng.framework.domain.course.ext.TeachplanNode" id="teachplanMap">
<id property="id" column="one_id"/>
<result property="pname" column="one_name"/>
<result property="grade" column="one_grade"/>
<collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
<id property="id" column="two_id"/>
<result property="pname" column="two_name"/>
<result property="grade" column="two_grade"/>
<collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
<id property="id" column="three_id"/>
<result property="pname" column="three_name"/>
<result property="grade" column="three_grade"/>
<result property="mediaId" column="mediaId"/>
<result property="mediaFileOriginalName" column="mediaFileOriginalName"/>
</collection>
</collection>
</resultMap>
<select id="selectList" resultMap="teachplanMap" parameterType="java.lang.String">
SELECT a.id one_id, a.pname one_name, a.grade one_grade, a.orderby one_orderby, b.id two_id, b.pname two_name,
b.grade two_grade, b.orderby two_orderby, c.id three_id, c.pname three_name, c.grade three_grade, c.orderby
three_orderby, media.media_id mediaId, media.media_fileoriginalname mediaFileOriginalName FROM teachplan a LEFT
JOIN teachplan b ON a.id = b.parentid LEFT JOIN teachplan c ON b.id = c.parentid LEFT JOIN teachplan_media media
ON c.id = media.teachplan_id WHERE a.parentid = '0'
<if test="_parameter!=null and _parameter!=''">
and a.courseid=#{courseId}
</if>
ORDER BY a.orderby, b.orderby, c.orderby
</select>
</mapper>
3.4.3 页面查询视频
课程计划结点信息已包括媒资信息,可在页面获取信息后显示:
<el‐button style="font‐size: 12px;" type="text" on‐click={ () => this.querymedia(data.id) }>
{data.mediaFileOriginalName} 选择视频</el‐button>
效果如下:
选择视频后立即刷新课程计划树,在提交成功后,添加查询课程计划代码:this.findTeachplan(),完整代码如下:
//保存选择的视频
choosemedia(mediaId,fileOriginalName,mediaUrl){
//保存视频到课程计划表中
let teachplanMedia ={}
teachplanMedia.mediaId =mediaId;
teachplanMedia.mediaFileOriginalName =fileOriginalName;
teachplanMedia.mediaUrl =mediaUrl;
teachplanMedia.courseId =this.courseid;
//课程计划
teachplanMedia.teachplanId=this.teachplanId
courseApi.savemedia(teachplanMedia).then(res=>{
if(res.success){
this.$message.success("选择视频成功")
//查询课程计划
this.findTeachplan()
}else{
this.$message.error(res.message)
}
})
},