Jave+WebSocket实现视频异步解析

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;
    }
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值