? 知识点概览
为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。
本章节为【学成在线】项目的 day14
的内容
- 视频上传成功后通过
RabbitMQ
进行消息发送,再通过视频处理服务
对视频进行格式转换,以及m3u8
视频文件的生成。 - 实现媒资信息的浏览
-
Vue
跨组件间的通讯实战,实现课程计划与已上传的媒资文件的关联一、视频处理
0x01 需求分析
原始视频通常需要经过编码处理,生成 m3u8
和 ts
文件方可基于 HLS
协议播放视频。通常用户上传原始视频,系统自动处理成标准格式,系统对用户上传的视频自动编码、转换,最终生成m3u8
文件和 ts
文件,处理流程如下:
1、用户上传视频成功
2、系统对上传成功的视频自动开始编码处理
3、用户查看视频处理结果,没有处理成功的视频用户可在管理界面再次触发处理
4、视频处理完成将视频地址及处理结果保存到数据库
视频处理流程如下:
视频处理进程的任务是接收视频处理消息进行视频处理,业务流程如下:
1、监听 MQ
,接收视频处理消息。
2、进行视频处理。
3、向数据库写入视频处理结果。
视频处理进程属于媒资管理系统的一部分,考虑提高系统的扩展性,将视频处理单独定义视频处理工程。
0x02 视频处理开发
视频处理工程创建
1、导入“资料” 下的视频处理工程:xc-service-manage-media-processor
2、RabbitMQ
配置
使用 rabbitMQ
的 routing
交换机模式,视频处理程序监听视频处理队列,如下图:
RabbitMQ配置如下:
代码语言:javascript复制@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;
/**
* 交换机配置
* @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
xc‐service‐manage‐media:
mq:
queue‐media‐video‐processor: queue_media_video_processor
routingkey‐media‐video: routingkey_media_video
视频处理技术方案
如何通过程序进行视频处理?
ffmpeg
是一个可行的视频处理程序,可以通过 Java
调用 ffmpeg.exe
完成视频处理。
在 java
中可以使用 Runtime
类和 Process Builder
类两种方式来执行外部程序,工作中至少掌握一种。
本项目使用 Process Builder
的方式来调用 ffmpeg
完成视频处理。
关于 Process Builder
的测试如下:
//测试ping命令
@Test
public void testProcessBuilder() throws IOException {
//创建ProcessBuilder对象
ProcessBuilder processBuilder = new ProcessBuilder();
//设置执行的第三方程序(命令)
List<String> cmds = new ArrayList<>();
cmds.add("ping");
cmds.add("127.0.0.1");
processBuilder.command(cmds);
//合并标准输入流和错误输出
processBuilder.redirectErrorStream(true);
Process start = processBuilder.start();
//获取输入流
InputStream inputStream = start.getInputStream();
//将输入流转换为字符输入流
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "gbk");
//获取流的数据
int len = -1;
//数据缓冲区
char[] cache = new char[1024];
StringBuffer stringBuffer = new StringBuffer();
while ((len = inputStreamReader.read(cache)) != -1) {
//获取缓冲区内的数据
String outStr = new String(cache, 0, len);
System.out.println(outStr);
stringBuffer.append(outStr);
}
inputStream.close();
}
//测试使用工具类将avi转成mp4
@Test
public void testProcessMp4() throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder();
//定义命令内容
List<String> command = new ArrayList<>();
command.add("D:/soft/ffmpeg-20200315-c467328-win64-static/bin/ffmpeg.exe");
command.add("-i");
command.add("E:/temp/1.avi");
command.add("-y"); //覆盖输出文件
command.add("-c:v");
command.add("libx264");
command.add("-s");
command.add("1280x720");
command.add("-pix_fmt");
command.add("yuv420p");
command.add("-b:a");
command.add("63k");
command.add("-b:v");
command.add("753k");
command.add("-r");
command.add("18");
command.add("E:/temp/1.mp4");
processBuilder.command(command);
//将标准输入流和错误输入流合并,通过标准输入流读取信息
processBuilder.redirectErrorStream(true);
Process start = processBuilder.start();
InputStream inputStream = start.getInputStream();
InputStreamReader streamReader = new InputStreamReader(inputStream, "gbk");
//获取输入流数据
int len = -1;
//数据缓冲区
char[] cache = new char[1024];
StringBuffer stringBuffer = new StringBuffer();
while ((len=streamReader.read(cache)) != -1){
//从缓冲区获取数据
String out = new String(cache, 0, len);
System.out.println(out);
stringBuffer.append(out);
}
inputStream.close();
}
上边的代码已经封装成工具类,参见:
上边的工具类中:
Mp4VideoUtil.java
完成 avi
转 mp4
HlsVideoUtil.java
完成 mp4
转 hls
分别测试每个工具类的使用方法。
代码语言:javascript复制public static void main(String[] args) throws IOException {
String ffmpeg_path = "D:/soft/ffmpeg-20200315-c467328-win64-static/bin/ffmpeg.exe";//ffmpeg的安装位置
String video_path = "E:\temp\1.avi";
String mp4_name = "2.mp4";
String mp4_path = "E:\temp\";
Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path);
String s = videoUtil.generateMp4();
System.out.println(s);
}
视频处理实现
1、确定消息格式
MQ
消息统一采用 json
格式,视频处理生产方会向 MQ
发送如下消息,视频处理消费方接收此消息后进行视频处
理:
{"mediaId":XXX}
2、处理流程
1)接收视频处理消息
2)判断媒体文件是否需要处理(本视频处理程序目前只接收avi
视频的处理)当前只有 avi
文件需要处理,其它文件需要更新处理状态为 “无需处理
”。
3)处理前初始化处理状态为 “未处理
”
4)处理失败需要在数据库记录处理日志,及处理状态为 “处理失败
”
5)处理成功记录处理状态为 “处理成功
“
3、数据模型
在 MediaFile
类中添加 mediaFileProcess_m3u8
属性记录 ts
文件列表,代码如下 :
//处理状态
private String processStatus;
//hls处理
private MediaFileProcess_m3u8 mediaFileProcess_m3u8;
MediaFileProcess_m3u8
如下
@Data
@ToString
public class MediaFileProcess_m3u8 extends MediaFileProcess {
//ts列表
private List<String> tslist;
}
4、视频处理生成 MP4
1)创建 dao
视频处理结果需要保存到媒资数据库,创建 dao
如下:
public interface MediaFileRepository extends MongoRepository<MediaFile,String> {
}
2)在 application.yml
中配置 ffmpeg
的位置及视频目录的根目录
xc‐service‐manage‐media:
video‐location: F:/develop/video/
ffmpeg‐path: D:/Program Files/ffmpeg‐20180227‐fa0c9d6‐win64‐static/bin/ffmpeg.exe
3)处理任务类
在 mq
包下创建 MediaProcessTask
类,此类负责监听视频处理队列,并进行视频处理。
整个视频处理内容较多,这里分两部分实现:生成 Mp4
和生成 m3u8
,下边代码实现了生成 mp4
。
@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){
//将接收到的消息转换为json数据
Map msgMap = JSON.parseObject(msg);
LOGGER.info("receive media process task msg :{} ",msgMap);
//解析消息
//媒资文件id
String mediaId = (String) msgMap.get("mediaId");
//获取媒资文件信息
Optional<MediaFile> byId = mediaFileRepository.findById(mediaId);
if(!byId.isPresent()){
return;
}
MediaFile mediaFile = byId.get();
//媒资文件类型
String fileType = mediaFile.getFileType();
//目前只处理avi文件
if(fileType == null || !fileType.equals("avi")){
mediaFile.setProcessStatus("303004"); // 处理状态为无需处理
mediaFileRepository.save(mediaFile);
}else{
mediaFile.setProcessStatus("303001"); //处理状态为未处理
}
//生成MP4
String videoPath = serverPath mediaFile.getFilePath() mediaFile.getFileName();
String mp4Name = mediaFile.getFileId() ".mp4";
String mp4FloderPath = serverPath mediaFile.getFilePath();
Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, videoPath, mp4Name, mp4FloderPath);
String result = mp4VideoUtil.generateMp4();
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...
}
}
说明:
1、原始视频转成 mp4
如何判断转换成功?
根据视频时长来判断,取原视频和转换成功视频的时长(时分秒),如果相等则相同。
5、视频处理生成 m3u8
下边是完整的视频处理任务类代码,包括了生成 m3u8
及生成 mp4
的代码
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.util.List;
import java.util.Map;
import java.util.Optional;
@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){
//将接收到的消息转换为json数据
Map msgMap = JSON.parseObject(msg);
LOGGER.info("receive media process task msg :{} ",msgMap);
//解析消息
//媒资文件id
String mediaId = (String) msgMap.get("mediaId");
//获取媒资文件信息
Optional<MediaFile> byId = mediaFileRepository.findById(mediaId);
if(!byId.isPresent()){
return;
}
MediaFile mediaFile = byId.get();
//媒资文件类型
String fileType = mediaFile.getFileType();
//目前只处理avi文件
if(fileType == null || !fileType.equals("avi")){
mediaFile.setProcessStatus("303004"); // 处理状态为无需处理
mediaFileRepository.save(mediaFile);
}else{
mediaFile.setProcessStatus("303001"); //处理状态为未处理
}
//生成MP4
String videoPath = serverPath mediaFile.getFilePath() mediaFile.getFileName();
String mp4Name = mediaFile.getFileId() ".mp4";
String mp4FloderPath = serverPath mediaFile.getFilePath();
Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, videoPath, mp4Name, mp4FloderPath);
String result = mp4VideoUtil.generateMp4();
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列表
//生成m3u8
String mp4VideoPath = serverPath mediaFile.getFilePath() mp4Name;//此地址为mp4的地址
String m3u8Name = mediaFile.getFileId() ".m3u8";
String m3u8FolderPath = serverPath mediaFile.getFilePath() "hls/";
//调用工具类进行生成m3u8
HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path, mp4VideoPath, m3u8Name, m3u8FolderPath);
String m3u8Result = hlsVideoUtil.generateM3u8();
if(m3u8Result==null || !m3u8Result.equals("success")){
//操作失败写入处理日志
mediaFile.setProcessStatus("303003");//处理状态为处理失败
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
mediaFileProcess_m3u8.setErrormsg(m3u8Result);
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/" m3u8Name);
mediaFileRepository.save(mediaFile);
}
}
0x03 发送视频处理消息
当视频上传成功后向 MQ
发送视频 处理消息。
修改媒资管理服务的文件上传代码,当文件上传成功向 MQ
发送视频处理消息。
配置RabbitMQ
1、将media-processor
工程下的 RabbitmqConfig
配置类拷贝到 media
工程下。
2、在 media
工程下配置 mq
队列等信息
修改 application.yml
xc-service-manage-media:
mq:
queue-media-video-processor: queue_media_video_processor
routingkey-media-video: routingkey_media_video
配置Service
在文件合并方法中添加向 mq
发送视频处理消息的代码:
//视频处理路由
@Value("${xc-service-manage-media.mq.routingkey-media-video}")
public String routingkey_media_video;
@Autowired
RabbitTemplate rabbitTemplate;
//向MQ发送视频处理消息
private 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);
}
在 mergechunks
方法最后调用 sendProcessVideo
方法。
......
//状态为上传成功
mediaFile.setFileStatus("301002");
mediaFileRepository.save(mediaFile);
String mediaId = mediaFile.getFileId();
//向MQ发送视频处理消息
sendProcessVideoMsg(mediaId);
......
0x04 视频处理测试
测试流程:
1、上传avi文件
2、观察日志是否发送消息
3、观察视频处理进程是否接收到消息进行处理
4、观察 mp4
文件是否生成
5、观察 m3u8
及 ts
文件是否生成
0x05 视频处理并发设置
代码中使用 @RabbitListener
注解指定消费方法,默认情况是单线程监听队列,可以观察当队列有多个任务时消费端每次只消费一个消息,单线程处理消息容易引起消息处理缓慢,消息堆积,不能最大利用硬件资源。
可以配置 mq
的容器工厂参数,增加并发处理数量即可实现多线程处理监听队列,实现多线程处理消息。
1、在 RabbitmqConfig.java
中添加容器工厂配置:
/**
* 多线程处理消息
* @param configurer
* @param connectionFactory
* @return
*/
@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")
再次测试当队列有多个任务时消费端的并发处理能力。
0x06 完整代码
RabbitMQConfig
代码语言:javascript复制package com.xuecheng.manage_media_process.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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();
}
/**
* 多线程处理消息
* @param configurer
* @param connectionFactory
* @return
*/
@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;
}
}
MediaProcessTask
代码语言:javascript复制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.util.List;
import java.util.Map;
import java.util.Optional;
@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}" , containerFactory="customContainerFactory")
public void receiveMediaProcessTask(String msg){
//将接收到的消息转换为json数据
Map msgMap = JSON.parseObject(msg);
LOGGER.info("receive media process task msg :{} ",msgMap);
//解析消息
//媒资文件id
String mediaId = (String) msgMap.get("mediaId");
//获取媒资文件信息
Optional<MediaFile> byId = mediaFileRepository.findById(mediaId);
if(!byId.isPresent()){
return;
}
MediaFile mediaFile = byId.get();
//媒资文件类型
String fileType = mediaFile.getFileType();
//目前只处理avi文件
if(fileType == null || !fileType.equals("avi")){
mediaFile.setProcessStatus("303004"); // 处理状态为无需处理
mediaFileRepository.save(mediaFile);
}else{
mediaFile.setProcessStatus("303001"); //处理状态为未处理
}
//生成MP4
String videoPath = serverPath mediaFile.getFilePath() mediaFile.getFileName();
String mp4Name = mediaFile.getFileId() ".mp4";
String mp4FloderPath = serverPath mediaFile.getFilePath();
Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, videoPath, mp4Name, mp4FloderPath);
String result = mp4VideoUtil.generateMp4();
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列表
//生成m3u8
String mp4VideoPath = serverPath mediaFile.getFilePath() mp4Name;//此地址为mp4的地址
String m3u8Name = mediaFile.getFileId() ".m3u8";
String m3u8FolderPath = serverPath mediaFile.getFilePath() "hls/";
//调用工具类进行生成m3u8
HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path, mp4VideoPath, m3u8Name, m3u8FolderPath);
String m3u8Result = hlsVideoUtil.generateM3u8();
if(m3u8Result==null || !m3u8Result.equals("success")){
//操作失败写入处理日志
mediaFile.setProcessStatus("303003");//处理状态为处理失败
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
mediaFileProcess_m3u8.setErrormsg(m3u8Result);
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/" m3u8Name);
mediaFileRepository.save(mediaFile);
}
}
MediaUploadServiceImpl
代码语言:javascript复制package com.xuecheng.manage_media.service.impl;
import com.alibaba.fastjson.JSON;
import com.netflix.discovery.converters.Auto;
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 com.xuecheng.manage_media.service.MediaUploadService;
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.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import javax.jws.Oneway;
import java.io.*;
import java.util.*;
@Service
class MediaUploadServiceImpl implements MediaUploadService {
private final static Logger LOGGER = LoggerFactory.getLogger(MediaUploadController.class);
@Autowired
MediaFileRepository mediaFileRepository;
//上传文件根目录
@Value("${xc-service-manage-media.upload-location}")
String uploadPath;
/**
* 检查文件信息是否已经存在本地以及mongodb内,其中一者不存在则重新注册
* @param fileMd5 文件md5值
* @param fileExt 文件扩展名
* @return 文件路径
*/
@Override
public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
//1.检查文件在磁盘上是否存在
//2.检查文件信息在mongodb上是否存在
//获取文件所属目录以及文件路径
String fileFloderPath = this.getFileFloderPath(fileMd5);
String filePath = this.getFileFullPath(fileMd5, fileExt);
File file = new File(filePath);
boolean exists = file.exists();
//查询mongodb上的文件信息
Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5);
if(exists && optional.isPresent()){
ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
}
//其中一者不存在则重新注册文件信息
File fileFloder = new File(fileFloderPath);
if(!fileFloder.exists()){
//创建文件目录
fileFloder.mkdirs();
}
return new ResponseResult(CommonCode.SUCCESS);
}
/**
* 检查文件块是否存在
* @param fileMd5 文件md5
* @param chunk 块编号
* @param chunkSize 块大小
* @return CheckChunkResult
*/
@Override
public CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {
//获取文件块路径
String chunkFloder = this.getChunkFloderPath(fileMd5);
File chunkFile = new File(chunkFloder chunk);
if(chunkFile.exists()){
return new CheckChunkResult(CommonCode.SUCCESS, true);
}
return new CheckChunkResult(CommonCode.SUCCESS, false);
}
/**
* 上传分块文件
* @param file 上传的文件
* @param chunk 分块号
* @param fileMd5 文件MD5
* @return
*/
@Override
public ResponseResult uploadChunk(MultipartFile file, Integer chunk, String fileMd5) {
//获取分块文件所属目录
String chunkFloder = this.getChunkFloderPath(fileMd5);
InputStream inputStream = null;
FileOutputStream fileOutputStream = null;
try {
inputStream = file.getInputStream();
fileOutputStream = new FileOutputStream(chunkFloder chunk);
IOUtils.copy(inputStream,fileOutputStream);
} catch (IOException e) {
//文件保存失败
e.printStackTrace();
LOGGER.error("upload chunk file fail:{}",e.getMessage());
ExceptionCast.cast(MediaCode.CHUNK_FILE_UPLOAD_FAIL);
}
return new ResponseResult(CommonCode.SUCCESS);
}
/**
* 合并文件块信息
* @param fileMd5 文件MD5
* @param fileName 文件名称
* @param fileSize 文件大小
* @param mimetype 文件类型
* @param fileExt 文件拓展名
* @return ResponseResult
*/
@Override
public ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
//获取文件块路径
String chunkFloderPath = getChunkFloderPath(fileMd5);
//合并文件路径
String fileFullPath = this.getFileFullPath(fileMd5, fileExt);
File mergeFile = new File(fileFullPath);
//创建合并文件,如果存在则先删除再创建
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 = this.getChunkFiles(chunkFloderPath);
//合并文件
mergeFile = this.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);
//文件路径保存相对路径
String filePath = this.getFilePath(fileMd5,fileExt);
mediaFile.setFilePath(this.getFilePath(fileMd5,fileExt));
mediaFile.setFileUrl(filePath fileName "." fileExt);
mediaFile.setFileSize(fileSize);
mediaFile.setUploadTime(new Date());
mediaFile.setMimeType(mimetype);
mediaFile.setFileType(fileExt);
//状态为上传成功
mediaFile.setFileStatus("301002");
MediaFile save = mediaFileRepository.save(mediaFile);
//向MQ发送视频处理消息
this.sendProcessVideoMsg(fileMd5);
return new ResponseResult(CommonCode.SUCCESS);
}
//视频处理路由
@Value("${xc-service-manage-media.mq.routingkey-media-video}")
public String routingkey_media_video;
@Autowired
RabbitTemplate rabbitTemplate;
//向MQ发送视频处理消息
private 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);
}
//校验文件MD5
private boolean checkFileMd5(File mergeFile, String fileMd5) {
if(mergeFile == null || StringUtils.isEmpty(fileMd5)){
return false;
}
//进行md5校验
try {
FileInputStream fileInputStream = new FileInputStream(mergeFile);
//得到文件的MD5
String md5Hex = DigestUtils.md5Hex(fileInputStream);
//比较两个MD5值
if(md5Hex.equalsIgnoreCase(fileMd5)){
return true;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
LOGGER.error("未找到该文件 {}",e.getMessage());
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
//合并文件
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;
}
//获取块文件列表
private List<File> getChunkFiles(String chunkFloderPath) {
//块文件目录
File chunkFolder = new File(chunkFloderPath);
//分块文件列表
File[] fileArray = chunkFolder.listFiles();
//将分块列表转为集合,便于排序
ArrayList<File> fileList = new ArrayList<>(Arrays.asList(fileArray));
//从小到大排序,按名称升序
Collections.sort(fileList, 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 fileList;
}
//获取文件块路径
private String getChunkFloderPath(String fileMd5) {
//获取分块文件所属目录
String fileFloderPath = this.getFileFloderPath(fileMd5);
String chunkFloder = fileFloderPath "chunk/";
File fileChunkFloder = new File(chunkFloder);
//如果分块所属目录不存在则创建
if(!fileChunkFloder.exists()){
fileChunkFloder.mkdirs();
}
return chunkFloder;
}
/**
* 根据文件md5得到文件的所属目录
* 规则:
* 一级目录:md5的第一个字符
* 二级目录:md5的第二个字符
* 三级目录:md5
*/
private String getFileFloderPath(String fileMd5){
String floderPath = uploadPath "/" fileMd5.substring(0,1) "/" fileMd5.substring(1,2) "/" fileMd5 "/";
return floderPath;
}
/**
* 获取全文件路径
* 文件名:md5 文件扩展名
*/
private String getFileFullPath(String fileMd5, String fileExt){
String floderPath = this.getFileFloderPath(fileMd5);
String filePath = floderPath fileMd5 "." fileExt;
return filePath;
}
/**
* 获取文件路径
* 文件名:md5 文件扩展名
*/
private String getFilePath(String fileMd5, String fileExt){
String filePath = "/" fileMd5.substring(0,1) "/" fileMd5.substring(1,2) "/" fileMd5 "/";
return filePath;
}
}
二、我的媒资
0x01 需求分析
通过我的媒资可以查询本教育机构拥有的媒资文件,进行文件处理、删除文件、修改文件信息等操作,具体需求如 下:
1、分页查询我的媒资文件
2、删除媒资文件
3、处理媒资文件
4、修改媒资文件信息
0x02 API
本节讲解我的媒资文件分页查询、处理媒资文件,其它功能请学员自行实现
代码语言:javascript复制@Api(value = "媒体文件管理",description = "媒体文件管理接口",tags = {"媒体文件管理接口"})
public interface MediaFileControllerApi {
@ApiOperation("查询文件列表")
public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest);
}
0x03 服务端开发
Dao
代码语言:javascript复制public interface MediaFileRepository extends MongoRepository<MediaFile,String> {
}
Service
定义 findList
方法实现媒资文件查询列表
package com.xuecheng.manage_media.service;
import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.QueryResponseResult;
public interface MediaFileService {
/**
* 查询媒体问价内信息
* @param page 页码
* @param size 每页数量
* @param queryMediaFileRequest 查询条件
* @return QueryResponseResult
*/
public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest);
}
实现
代码语言:javascript复制package com.xuecheng.manage_media.service.impl;
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 com.xuecheng.manage_media.service.MediaFileService;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
@Service
public class MediaFileServiceImpl implements MediaFileService {
private static Logger logger = LoggerFactory.getLogger(MediaFileService.class);
@Autowired
MediaFileRepository mediaFileRepository;
/**
* 分页查询文件信息
* @param page 页码
* @param size 每页数量
* @param queryMediaFileRequest 查询条件
* @return QueryResponseResult
*/
@Override
public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest) {
MediaFile mediaFile = new MediaFile();
//查询条件
if(queryMediaFileRequest == null){
queryMediaFileRequest = new QueryMediaFileRequest();
}
//查询条件匹配器
ExampleMatcher exampleMatcher = ExampleMatcher.matching()
.withMatcher("tag", ExampleMatcher.GenericPropertyMatchers.contains()) //模糊匹配
.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> example = Example.of(mediaFile, exampleMatcher);
//校验page和size参数的合法性,并设置默认值
if(page <=0){
page = 0;
}else{
page = page -1;
}
if(size <=0){
size = 10;
}
//分页对象
PageRequest pageRequest = new PageRequest(page, size);
//分页查询
Page<MediaFile> all = mediaFileRepository.findAll(example, pageRequest);
//设置响应对象属性
QueryResult<MediaFile> mediaFileQueryResult = new QueryResult<MediaFile>();
mediaFileQueryResult.setList(all.getContent());
mediaFileQueryResult.setTotal(all.getTotalElements());
return new QueryResponseResult(CommonCode.SUCCESS,mediaFileQueryResult);
}
}
Controller
代码语言:javascript复制@RestController
@RequestMapping("/media/file")
public class MediaFileController implements MediaFileControllerApi {
@Autowired
MediaFileService mediaFileService;
@GetMapping("/list/{page}/{size}")
@Override
public QueryResponseResult findList(@PathVariable("page") int page,@PathVariable("size") int size, QueryMediaFileRequest queryMediaFileRequest) {
//媒资文件信息查询
return mediaFileService.findList(page,size,queryMediaFileRequest);
}
}
接口测试
使用 swagger
进行接口测试
0x04 前端开发
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)
}
页面
在 media
模块创建 media_list.vue
,可参考 cms
系统的 page_list.vue
来编写此页面。
1、视图
代码语言:javascript复制<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="" 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>
2、数据对象、方法、钩子函数
代码语言:javascript复制<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>
三、媒资与课程计划关联
0x01 需求分析
到目前为止,媒资管理已完成文件上传、视频处理、我的媒资功能等基本功能。其它模块已可以使用媒资管理功 能,本节要讲解课程计划在编辑时如何选择媒资文件。
操作的业务流程如下:
1、进入课程计划修改页面
2、选择视频
打开媒资文件查询窗口,找到该课程章节的视频,选择此视频。
点击 “选择媒资文件
” 打开媒资文件列表
3、 选择成功后,将在课程管理数据库保存课程计划对应在的课程视频地址。
在课程管理数据库创建表 teachplan_media
存储课程计划与媒资关联信息,表结构如下:
0x02 选择视频
Vue 父子组件通信
上一章已实现了我的媒资页面,所以媒资查询窗口页面不需要再开发,将 “我的媒资页面
” 作为一个组件在修改课程
计划页面中引用,如下图:
修改课程计划页面为父组件,我的媒资查询页面为子组件。
问题1:
我的媒资页面在选择媒资文件时不允许显示,比如 视频处理
按钮,该如何控制?
这时就需要父组件(修改课程计划页面
)向子组件(我的媒资页面
)传入一个变量,使用此变量来控制当前是否进入选择媒资文件业务,从而控制哪些元素不显示,如下图:
问题2:
在我的媒资页面选择了媒资文件,如何将选择的媒资文件信息传到父组件?
这时就需要子组件调用父组件的方法来解决此问题,如下图:
父组件:修改课程计划
本节实现功能:在课程计划页面打开我的媒资页面。
1、引入子组件
代码语言:javascript复制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){
}
4、打开子组件窗口
1)打开子组件窗口按钮定义
代码语言:javascript复制<el‐button style="font‐size: 12px;" type="text" on‐click={ () => this.choosevideo(data.id) }>选择视频</el‐button>
效果如下:
2)打开子组件窗口方法
定义 querymedia
方法:
methods: {
//打开查询媒资文件窗口,传入课程计划id
choosevideo(teachplanId){
this.activeTeachplanId = teachplanId;
this.mediaFormVisible = true;
},
...
}
子组件:我的媒资查询
1、定义 ischoose
变量,接收父组件传入的 ischoose
export default{
props: ['ischoose'],
data(){
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)选择媒资文件方法
用户点击“选择”按钮将向父组件传递媒资文件信息
代码语言:javascript复制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);
}
页面效果
全部代码
course_plan.vue
代码语言:javascript复制<template>
<div>
<el-button type="primary" @click="teachplayFormVisible = true">添加课程计划</el-button>
<el-tree
:data="teachplanList"
:props="defaultProps"
node-key="id"
default-expand-all
:expand-on-click-node="false"
:render-content="renderContent">
</el-tree>
<el-dialog title="添加课程计划" :visible.sync="teachplayFormVisible" >
<el-form ref="teachplanForm" :model="teachplanActive" label-width="140px" style="width:600px;" :rules="teachplanRules" >
<el-form-item label="上级结点" >
<el-select v-model="teachplanActive.parentid" placeholder="不填表示根结点">
<el-option
v-for="item in teachplanList"
:key="item.id"
:label="item.pname"
:value="item.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="章节/课时名称" prop="pname">
<el-input v-model="teachplanActive.pname" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="课程类型" >
<el-radio-group v-model="teachplanActive.ptype">
<el-radio class="radio" label='1'>视频</el-radio>
<el-radio class="radio" label='2'>文档</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="学习时长(分钟) 请输入数字" >
<el-input type="number" v-model="teachplanActive.timelength" auto-complete="off" ></el-input>
</el-form-item>
<el-form-item label="排序字段" >
<el-input v-model="teachplanActive.orderby" auto-complete="off" ></el-input>
</el-form-item>
<el-form-item label="章节/课时介绍" prop="description">
<el-input type="textarea" v-model="teachplanActive.description" ></el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="teachplanActive.status" >
<el-radio class="radio" label="0" >未发布</el-radio>
<el-radio class="radio" label='1'>已发布</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item >
<el-button type="primary" v-on:click="addTeachplan">提交</el-button>
<el-button type="primary" v-on:click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-dialog>
<el-dialog title="选择媒资文件" :visible.sync="mediaFormVisible">
<media-list v-bind:ischoose="true" @choosemedia="choosemedia"></media-list>
</el-dialog>
</div>
</template>
<script>
let id = 1000;
import * as courseApi from '../../api/course';
import utilApi from '../../../../common/utils';
import * as systemApi from '../../../../base/api/system';
import mediaList from '@/module/media/page/media_list.vue';
export default {
components:{
mediaList
},
data() {
return {
mediaFormVisible:false,
teachplayFormVisible:false,//控制添加窗口是否显示
teachplanList : [{
id: 1,
pname: '一级 1',
children: [{
id: 4,
pname: '二级 1-1',
children: [{
id: 9,
pname: '三级 1-1-1'
}, {
id: 10,
pname: '三级 1-1-2'
}]
}]
}],
defaultProps:{
children: 'children',
label: 'pname'
},
teachplanRules: {
pname: [
{required: true, message: '请输入课程计划名称', trigger: 'blur'}
],
status: [
{required: true, message: '请选择状态', trigger: 'blur'}
]
},
teachplanActive:{},
teachplanId:''
}
},
methods: {
//选择视频,打开窗口
choosevideo(data){
//得到当前的课程计划
this.teachplanId = data.id
// alert(this.teachplanId)
this.mediaFormVisible = true;//打开窗口
},
//保存选择的视频
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)
}
})
},
//提交课程计划
addTeachplan(){
//校验表单
this.$refs.teachplanForm.validate((valid) => {
if (valid) {
//调用api方法
//将课程id设置到teachplanActive
this.teachplanActive.courseid = this.courseid
courseApi.addTeachplan(this.teachplanActive).then(res=>{
if(res.success){
this.$message.success("添加成功")
//刷新树
this.findTeachplan()
}else{
this.$message.error(res.message)
}
})
}
})
},
//重置表单
resetForm(){
this.teachplanActive = {}
},
append(data) {
const newChild = { id: id , label: 'testtest', children: [] };
if (!data.children) {
this.$set(data, 'children', []);
}
data.children.push(newChild);
},
edit(data){
//alert(data.id);
},
remove(node, data) {
const parent = node.parent;
const children = parent.data.children || parent.data;
const index = children.findIndex(d => d.id === data.id);
children.splice(index, 1);
},
renderContent(h, { node, data, store }) {
return (
<span style="flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px;">
<span>
<span>{node.label}</span>
</span>
<span>
<el-button style="font-size: 12px;" type="text" on-click={ () => this.choosevideo(data) }>{data.mediaFileOriginalName} 选择视频</el-button>
<el-button style="font-size: 12px;" type="text" on-click={ () => this.edit(data) }>修改</el-button>
<el-button style="font-size: 12px;" type="text" on-click={ () => this.remove(node, data) }>删除</el-button>
</span>
</span>);
},
findTeachplan(){
this.teachplanList = []
//查询课程计划
courseApi.findTeachplanList(this.courseid).then(res=>{
if(res && res.children){
this.teachplanList = res.children;
}else {
this.$message.error("课程计划查询失败")
console.log(res)
}
})
}
},
mounted(){
//课程id
this.courseid = this.$route.params.courseid;
//查询课程计划
this.findTeachplan()
}
}
</script>
<style>
</style>
media_list.vue
代码语言:javascript复制<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="" 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>
0x03 保存视频信息
需求分析
用户进入课程计划页面,选择视频,将课程计划与视频信息保存在课程管理数据库中。
用户操作流程:
1、进入课程计划,点击”选择视频“,打开我的媒资查询页面
2、为课程计划选择对应的视频,选择“选择”
3、前端请求课程管理服务保存课程计划与视频信息
数据模型
在课程管理数据库创建表 teachplan_media
存储课程计划与媒资关联信息,如下:
创建 teachplanMedia
模型类:
@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;
@Column(name="courseid")
private String courseId;
}
API接口
此接口作为前端请求课程管理服务保存课程计划与视频信息的接口:
在 TeachplanControllerApi
增加接口:
@ApiOperation("保存媒资信息")
public ResponseResult saveTeachplanMedia(TeachplanMedia teachplanMedia);
服务端开发
1、Controller
在 TeachplanController
下添加该方法
@Override
@PostMapping("/savemedia")
public ResponseResult saveTeachplanMedia(@RequestBody TeachplanMedia teachplanMedia) {
return teachplanService.saveTeachplanMedia(teachplanMedia);
}
2、Dao
创建 TeachplanMediaRepository
用于对 TeachplanMedia
的操作。
public interface TeachplanMediaRepository extends JpaRepository<TeachplanMedia, String> {}
3、Service
代码语言:javascript复制//保存媒资信息
public ResponseResult saveTeachplanMedia(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);
}
前端开发
1、API方法
代码语言:javascript复制/*保存媒资信息*/
export const savemedia = teachplanMedia => {
return http.requestPost(apiUrl '/course/savemedia',teachplanMedia);
}
2、API调用
在课程视频方法中调用 api
:
choosemedia(mediaId,fileOriginalName,mediaUrl){
this.mediaFormVisible = false;
//保存课程计划与视频对应关系
let teachplanMedia = {};
teachplanMedia.teachplanId = this.activeTeachplanId;
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、测试
1、向叶子结点课程计划保存媒资信息
操作结果:保存成功
2、向非叶子结点课程计划保存媒资信息
操作结果:保存失败
0x04 查询视频信息
需求分析
课程计划的视频信息保存后在页面无法查看,本节解决课程计划页面显示相关联的媒资信息。
解决方案:
在获取课程计划树结点信息时将关联的媒资信息一并查询,并在前端显示,下图说明了课程计划显示的区域。
Dao
修改课程计划查询的 Dao
:
1、修改模型
在课程计划结果信息中添加媒资信息
代码语言:javascript复制package com.xuecheng.framework.domain.course.ext;
import com.xuecheng.framework.domain.course.Teachplan;
import lombok.Data;
import lombok.ToString;
import java.util.List;
@Data
@ToString
public class TeachplanNode extends Teachplan {
List<TeachplanNode> children;
//媒资信息
private String media_id;
private String media_fileoriginalname;
}
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 property="id" column="one_id"/>
<result property="pname" column="one_pname"/>
<collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
<!--二级节点-->
<id property="id" column="two_id"/>
<result property="pname" column="two_pname"/>
<collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode">
<!--三级节点-->
<id property="id" column="three_id"/>
<result property="pname" column="three_pname"/>
<result property="media_id" column="media_id"/>
<result property="media_fileoriginalname" column="media_fileoriginalname"/>
</collection>
</collection>
</resultMap>
<!--三级菜单查询-->
<select id="selectList" resultMap="teachplanMap" parameterType="java.lang.String">
SELECT
a.id one_id,
a.pname one_pname,
a.courseid one_course,
b.id two_id,
b.pname two_pname,
c.id three_id,
c.pname three_pname,
media.media_id media_id,
media.media_fileoriginalname media_fileoriginalname
FROM
teachplan a
LEFT JOIN teachplan b
ON b.parentid = a.id
LEFT JOIN teachplan c
ON c.parentid = b.id
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>
这里的核心代码是使用 LEFT JOIN
关联 teachplan_media
表中的数据,再获取该课程计划下的 mediaId
与 mediaFileOriginalName
代码如下
LEFT JOIN teachplan_media media ON c.id = media.teachplan_id
WHERE
代码语言:javascript复制<!--三级节点-->
<id property="id" column="three_id"/>
<result property="pname" column="three_pname"/>
<result property="media_id" column="media_id"/>
<result property="media_fileoriginalname"
使用swagger进行接口测试
从结果中成功的查询到了课程计划所关联的媒资信息。
页面查询视频
课程计划结点信息已包括媒资信息,可在页面获取信息后显示。
通过 data.media_fileoriginalname
获取媒资视频的原始名称
<el‐button style="font‐size: 12px;" type="text" on‐click={ () => this.querymedia(data.id) }>
{data.media_fileoriginalname} 选择视频</el‐button>
效果如下:
选择视频后立即刷新课程计划树,在提交成功后,添加查询课程计划代码:this.findTeachplan()
,完整代码如下:
choosemedia(mediaId,fileOriginalName,mediaUrl){
this.mediaFormVisible = false;
//保存课程计划与视频对应关系
let teachplanMedia = {};
teachplanMedia.teachplanId = this.activeTeachplanId;
teachplanMedia.mediaId = mediaId;
teachplanMedia.mediaFileOriginalName = fileOriginalName;
teachplanMedia.mediaUrl = mediaUrl;
teachplanMedia.courseId = this.courseid;
//保存媒资信息到课程数据库
courseApi.savemedia(teachplanMedia).then(res=>{
if(res.success){
this.$message.success("选择视频成功")
//查询课程计划
this.findTeachplan()
}else{
this.$message.error(res.message)
}
})
},