Jave+WebSocket实现视频异步转码并上传
需求:用户客户端上传视频后,服务端异步进行视频在线转码为MP4并保存,操作完成后,给客户端推送视频url进行在线播放。
1、WebSocket实现异步消息推送
1、webSocket和Http的区别
相同点
- 都是一样基于TCP的,都是可靠性传输协议。
- 都是应用层协议。
不同点
- WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息。HTTP是单向的。
- WebSocket是需要握手进行建立连接的。
联系
- WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。
2、为什么需要WebSocket
传统的消息推送都是通过轮询的方式进行。通过客户端不断询问服务端是否有新的消息。因为Http是非持久性的,所以每次都得发送新的请求,十分的消耗资源。而WebSocket只需要客户端和服务端建立一次连接,而该连接不会断掉,当服务端有消息需要发送时,才主动向客户端推送。
3、整合WebSocket
1、导入依赖
<!--WebSocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
2、新建配置文件 WebSocketConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
3、新建服务 WebSocketService
参数:userId
作用:是用来记录向哪个客户端发送消息,做到准确推送。
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/webSocket/{userId}")
@Slf4j
@Component
public class WebSocketService {
private static int onlineCount = 0;
private static Map<Long, WebSocketService> webSocketMap = new ConcurrentHashMap<>();
private Session session;
private Long userId;
@OnOpen
public void onOpen(@PathParam("userId") Long userId, Session session){
this.userId = userId;
this.session = session;
if(webSocketMap.containsKey(userId)){
webSocketMap.remove(userId);
webSocketMap.put(userId,this);
//加入set中
}else{
webSocketMap.put(userId,this);
//加入set中
addOnlineCount();
//在线数加1
}
webSocketMap.put(userId,this);
log.info("用户"+userId+"连接上webSocket,当前连接人数为"+getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if(webSocketMap.containsKey(userId)){
webSocketMap.remove(userId);
//从set中删除
subOnlineCount();
}
log.info("用户退出:"+userId+",当前在线人数为:" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:"+this.userId+",原因:"+error.getMessage());
error.printStackTrace();
}
/**
* 向执行某个任务的客户端发送消息
*/
public void sendMessage(String message){
this.session.getAsyncRemote().sendText(message);
}
/**
* 向用户id为userId的用户推送消息
* */
public static void sendInfo(String message,Long userId){
log.info("发送消息到:"+userId+",报文:"+message);
if(userId != null && webSocketMap.containsKey(userId)){
webSocketMap.get(userId).sendMessage(message);
}else{
log.error("用户"+userId+",不在线!");
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketService.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketService.onlineCount--;
}
}
4、测试
WebSocket测试网址:http://www.websocket-test.com/
1、填入服务器地址:ip+@ServerEndPoint的地址就是服务器地址
2、先断开(网址的测试地址),再点击连接
3、可以看到后台显示
10086为刚刚填入路径的用户id,@ServerEndPoint中的{userId}
再打开另一个浏览器,重复上面步骤,用户id为10087
2、@Async实现异步
1、开启异步,此注解会扫描带@Async的方法
@EnableAsync
@SpringBootApplication
public class MeidaAsnyApplication {
public static void main(String[] args) {
SpringApplication.run(MeidaAsnyApplication.class, args);
}
}
2、编写Service方法
这里有个细节部分,异步方法中,文件不能使用MultipartFile进行参数传递,否则会在读取的时候找不到该文件。
原因:multipartFile在本地文件目录中创建的一个临时文件的全路径+名称,主线程结束,导致本地目录的临时文件被清除,所以异步方法中的getInputStream方法报FileNotFoundException错误。
public interface MediaService {
/**
* 媒体上传解析的方法
* @param fileName 文件名
* @param inputStream 文件输入流
* @param userId 用户id
*/
void uploadMedia(String fileName, InputStream inputStream, Long userId);
}
3、给实现类添加@Async
@Service
public class MediaServiceImpl implements MediaService {
@Async
@Override
public void uploadMedia(String fileName, InputStream inputStream, Long userId) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
WebSocketService.sendInfo("消息推送测试",userId);
}
}
4、Controller层
@RestController
@RequestMapping("media")
public class MediaController {
@Autowired
MediaService mediaService;
@PostMapping
public String uploadMedia(MultipartFile file,Long userId) throws IOException {
String fileName = file.getOriginalFilename();
InputStream in = file.getInputStream();
mediaService.uploadMedia(fileName,in,userId);
return "用户"+userId+"正在对"+fileName+"文件进行在线解析";
}
}
5、测试效果
- webSocket连接
- postman模拟post请求
- 用户10086接收到消息,而10087没有,且消息是在前端收到消息,三秒后才推送到的,说明成功!
3、Jave实现视频解析
Jave是一款底层封装ffmepg的视频处理类库,想要了解更多参数,可以debug进encode方法
1、导入依赖
<!--jave 视频处理-->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-core</artifactId>
<version>2.4.5</version>
</dependency>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-native-win64</artifactId>
<version>2.4.5</version>
</dependency>
2、开发
-
配置文件中配置上传文件大小
#配置上传文件大小 spring.servlet.multipart.max-file-size=150MB spring.servlet.multipart.max-request-size=150MB
-
编写MeidaConstant用来保存编码常量
因为对视频这方面不是很了解,所以上面的注释不一定对,如果要转为其他格式就修改其中的编码等参数。
public class MediaConstant {
/**
* 音频编码
*/
public final static String AUDIO_CODE = "libmp3lame";
/**
* 音频比特率
*/
public final static Integer AUDIO_BITRATE = 800000;
/**
* 音频音道
*/
public final static Integer AUDIO_CHANNEL = 1;
/**
* 视频帧率
*/
public final static Integer VIDEO_FRAMERATE = 15;
/**
* 视频编码格式
*/
public final static String VIDEO_CODE = "libx264";
/**
* 视频比特率
*/
public final static Integer VIDEO_BITRATE = 3200000;
/**
* 视频格式
*/
public final static String VIDE_FORMAT="mp4";
}
-
编写工具类
1、媒体工具类
/**
*@Description 转码工具类
*@Verson v1.0.0
*@Date 2020/10/28
*/
public class MediaUtil {
/**
* 获取转码属性
* @return EncodingAttributes 转码属性
*/
public static EncodingAttributes getEncodingAttributes(){
//转码属性
EncodingAttributes attrs = new EncodingAttributes();
// 转码为MP4
AudioAttributes audio = new AudioAttributes();
// 音频编码格式
audio.setCodec(MediaConstant.AUDIO_CODE);
audio.setBitRate(MediaConstant.AUDIO_BITRATE);
audio.setChannels(MediaConstant.AUDIO_CHANNEL);
VideoAttributes video = new VideoAttributes();
// 视频编码格式
video.setCodec(MediaConstant.VIDEO_CODE);
video.setBitRate(MediaConstant.VIDEO_BITRATE);
// 数字设置小了,视频会卡顿
video.setFrameRate(MediaConstant.VIDEO_FRAMERATE);
attrs.setFormat(MediaConstant.VIDE_FORMAT);
attrs.setAudioAttributes(audio);
attrs.setVideoAttributes(video);
return attrs;
}
/**
* 后缀识别
* @param fileName 文件名
* @return
*/
public static boolean canConvert(String fileName) {
if (fileName == null || fileName.length() == 0) {
return false;
}
int extIndex = fileName.lastIndexOf(".");
if (extIndex == -1) {
return false;
}
final String ext = fileName.substring(extIndex);
return Arrays.stream(new String[] {".flv", ".avi", ".mp4"})
.anyMatch(p -> p.equalsIgnoreCase(ext));
}
}
2、文件工具类
/**
* 文件工具类
*/
public class FileUtil {
/**
* 获取文件后缀,带(.)
*
* @param fileName 文件名
* @return 后缀 extend
*/
public static String getExtend(String fileName) {
if (StringUtils.isEmpty(fileName)) {
return "";
}
int iPos = fileName.lastIndexOf('.');
if (iPos < 0) {
return "";
}
return fileName.substring(iPos).toLowerCase();
}
public static File createTempFile(InputStream inputStream, String fileName){
//防止多线程情况下重名
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddhhmmss");
String time = sdf.format(date);
//文件名上加上日期,防止多线程时文件被覆盖
try {
//创建临时文件temp
//将inputStream写进temp;
File temp = File.createTempFile("video"+time,getExtend(fileName));
OutputStream outputStream = new FileOutputStream(temp);
int index;
byte[] buffer = new byte[1024];
while ((index = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, index);
}
outputStream.flush();
inputStream.close();
outputStream.close();
return temp;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
3、编写方法
重写MeidaServerImpl
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import ws.schild.jave.Encoder;
import ws.schild.jave.EncoderException;
import ws.schild.jave.EncodingAttributes;
import ws.schild.jave.MultimediaObject;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
@Service
public class MediaServiceImpl implements MediaService {
@Async
@Override
public void uploadMedia(String fileName, InputStream inputStream, Long userId) {
//判断格式是否正确
if(MediaUtil.canConvert(fileName)){
//inputStream转本地文件
File source = FileUtil.createTempFile(inputStream,fileName);
if(source == null){
WebSocketService.sendInfo("文件"+fileName+"上传失败",userId);
}
//获取转码参数
EncodingAttributes attrs = MediaUtil.getEncodingAttributes();
//创建目标文件 路径是你要保存文件的路径,实际上应该是在配置文件中写好。这里为了方便直接写死。
// 防止重名
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String time = sdf.format(date);
File target = new File("E:\\video\\"+time+".mp4");
//转码部分
MultimediaObject multimediaObject = new MultimediaObject(source);
Encoder encoder = new Encoder();
try {
//转码并保存
encoder.encode(multimediaObject,target,attrs);
//结果可以可以用JSONUtil.toJsonStr(object)返回一个Json字符串
WebSocketService.sendInfo("视频解析成功,url:"+target.getAbsolutePath(), userId);
} catch (EncoderException e) {
e.printStackTrace();
WebSocketService.sendInfo("转码过程出现错误" + e.getMessage(), userId);
}finally {
//删除临时文件
if(source != null) {
source.deleteOnExit();
}
}
}else {
WebSocketService.sendInfo("文件"+fileName+"文件格式不正确",userId);
}
}
}
4、测试
postMan上传一个文件
在webSocket-test可以看到文件上传完后推送回文件路径
至此,感谢观看!
https://github.com/CaiCaiXian/media-async.git
更新
相同格式可以进行无损上传
更多的参数可以自己debug进encode方法,底层使用的还是ffmepg
/**
* 获取转码属性
* @return EncodingAttributes 转码属性
*/
public static EncodingAttributes getEncodingAttributes(String code){
//转码属性
EncodingAttributes attrs = new EncodingAttributes();
AudioAttributes audio = new AudioAttributes();
VideoAttributes video = new VideoAttributes();
if(!code.equals(MediaConstant.VIDE_FORMAT)){
// 转码为MP4
// 音频编码格式
audio.setBitRate(MediaConstant.AUDIO_BITRATE);
audio.setChannels(MediaConstant.AUDIO_CHANNEL);
audio.setCodec(MediaConstant.AUDIO_CODE);
// 视频编码格式
video.setCodec(MediaConstant.VIDEO_CODE);
video.setBitRate(MediaConstant.VIDEO_BITRATE);
// 数字设置小了,视频会卡顿
video.setFrameRate(MediaConstant.VIDEO_FRAMERATE);
}
else{
video.setCodec(VideoAttributes.DIRECT_STREAM_COPY);
audio.setCodec(AudioAttributes.DIRECT_STREAM_COPY);
}
attrs.setFormat(MediaConstant.VIDE_FORMAT);
attrs.setAudioAttributes(audio);
attrs.setVideoAttributes(video);
return attrs;
}