Java实现实时视频转播的代码示例

 更新时间:2023年09月18日 10:55:50   作者:Ha_Ha_Wu  
这篇文章主要给大家详细介绍了Java如何实现实时视频转播,文中通过代码实例介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴可以自己动手试一试

实现功能简述

最开始是想做一个在线的美妆功能,就像抖音上录制视频时增加特效一样。由于是后端程序员,我下意识的认为对图像/视频的处理都应该在后台完成,通过某种协议传给前端进行展示,我的项目也是基于此写成的。 主要用到的轮子:spring-boot,javaCV,Libjitsi,Webcam

第一步:后台启动摄像头,抓拍图像

while (isOpened) {
            synchronized (lock) {
                System.out.println("正在添加FFmpag");
                BufferedImage image = webcam.getImage();
                Frame frame = converter.getFrame(image);
                recorder.record(frame);
                if (!isFileStreamOn) {
                    isFileStreamOn = true;
                }
            }
            Thread.yield();
        }

此处用到Webcam包和JavaCV包

Webcam可以通过电脑摄像头获取BufferedImage图片(当然javaCV中也有类似的工具) JavaCV包是用Java封装音视频处理的库,主要逻辑是将图片/视频/音频数据转化为Frame类,基于Frame对象进行操作。 上面代码中我将每张图片用转化器转化成Frame对象(很明显转化器类源码中的逻辑很值得研究),用FFmpegFrameRecorder将传入的图片转化成视频(文件/流)。

其中webcam,recorder和转化器在config中定义为bean:

@Bean
    public Webcam getCam() {
        Webcam webcam = Webcam.getDefault();
        webcam.setViewSize(new Dimension(640, 480));
        return webcam;
    }
@Bean
    public FFmpegFrameRecorder getRecorder(Webcam webcam){
        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder("src/main/resources/static/output.avi",webcam.getViewSize().width,webcam.getViewSize().height);
        recorder.setVideoCodecName("lib264");
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_MPEG4);
        recorder.setFormat("avi");
        recorder.setFrameRate(24);
        return recorder;
    }
@Bean
    public Java2DFrameConverter getConverter(){
        return new Java2DFrameConverter();
    }

第二步:后台获取的视频发送RTP包到前端

while (isFileStreamOn) {
   InputStream stream = new FileInputStream(file);
   synchronized (lock) {
       while (stream.available() > 0) {
           byte[] bytes = new byte[1024];
           int length = stream.read(bytes);
           RawPacket rawPacket = new RawPacket(bytes, 0, length);
           DatagramPacket udpPacket = new DatagramPacket(rawPacket.getBuffer(), rawPacket.getLength(), targetHost, targetPort);
           udpSocket.send(udpPacket);
           System.out.println("发送数据包 " + Arrays.toString(udpPacket.getData()))             }
//         file = new File(file.getAbsoluteFile());
           try (FileWriter writer = new FileWriter(file)) {
                writer.write(""); // 写入空字符串,清空文件内容
           } catch (IOException e) {
                e.printStackTrace();
                }
           stream = new FileInputStream(file);
           }
	}

注意点:

  • 我将由FFmpegFrameRecorder写入文件的视频再次读取出来,并将此文件流分段,用基于webRTC的jitsi库进行打包发送。 jitsi是Java支持WebRTC(实时通信)的一个库,主要能够将数据以很小的时间间隔发送(据说视频可以做到每帧一个包,我没试也没必要) webRTC中,RTP协议是基于UDP的,也就是说一个RTP会被拆分成多个UDP包。

  • 问题1:为什么要先写入文件再读取文件? 其实FFmpegFrameRecorder用于图片转视频时,视频数据的写入方式其实可以是文件,也可以是输出流,假如可以获取一段时间的视频输出流,无论是切片转化还是加锁重置都很方便,但唯独是我用这个API有bug,底层的源码绕来绕去到达JNI,就完全不懂了。 关于用文件缓存,我有另一个解决方法是:写入文件到一定大小,主动切换写另一个文件,将写好的文件存入队列中等待读取,但由于JavaCV的API我还不太熟练,如何给一个recorder对象切换输出流暂不明,此法仍需考虑。

  • 问题2:写入文件和读取文件怎么做到异步? 出于实时性和用户使用效果的考虑,程序的设计一定是一个线程写,一个线程异步读取,以下是我的程序设计以及其中出现的问题和解决方式

    用于获取视频帧的线程B先执行,启动摄像头并将视频帧加入recorder写入缓存(文件),写入一段时间后用于发送数据包的线程A开始执行,获取到缓存中的数据,分包,以UDP的形式发送到目标主机

    为了避免两个线程对同一缓存读写冲突,他们的操作分别需要加锁。

    怎么实现一个线程先于另一线程执行呢?sleep基于时间设置延迟但并不是根据逻辑;wait和notify是基于逻辑的但也有要求——由于两个方法是基于锁的,所以只能在有锁的同步代码块中执行,用起来没那么丝滑。

    关于将输出流分包转储到package中,我考虑了两种方式:

    持久读取一个流中的数据:用锁将此流锁住,然后就可以用固定长度的byte[]慢慢拆解。

    立马读取此流的全部数据:不需要锁住流,只要将流中数据转入超大byte[]中,再慢慢拆分大byte[]

    第一种在时间上性能消耗较大,对流的阻塞时间较长,第二种在内存上消耗较大,可能导致GC的STW较频繁。

    为了防止中间缓存过大,需要一定时间将其进行重置,也就是对文件进行清空 有权利清空文件的一定是能确保文件读取完毕的线程,所以什么时候清空?我的想法是当读线程发现没有新数据可读时就进行清空,那有没有可能写线程一直在写导致不会有空隙没有新数据?不会,因为我的程序加锁的粒度比较大,当读线程全部读完后才会释放锁给写线程。也就是说读线程每次完整读完缓存就会清空。

完整代码:

import com.example.meitu2.utils.bfiOps;
import com.github.sarxos.webcam.Webcam;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.jitsi.service.neomedia.RawPacket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@RestController
public class video3Controller {
    @Autowired
    Webcam webcam;
    @Autowired
    List<Webcam> webcams;
    @Autowired
    com.example.meitu2.pojos.websocket websocket;
    @Autowired
    Java2DFrameConverter converter;
    boolean isOpened = false;
    boolean isFileStreamOn = false;
    boolean needsLight = false;
    int lightIndex = 0;
    boolean needDuibidu = false;
    int duibiduIndex = 0;
    @GetMapping("RTPThread")
    public void RTPThread() throws SocketException, UnknownHostException, FFmpegFrameRecorder.Exception, InterruptedException {
        if (!webcam.isOpen()) {
            webcam.open();
        }
        isOpened = true;
        DatagramSocket udpSocket = new DatagramSocket();
        InetAddress targetHost = InetAddress.getByName("localhost");
        int targetPort = 2244;
//        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        File file = new File("Demo.mp4");
        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(file.getAbsoluteFile(), webcam.getViewSize().width, webcam.getViewSize().height);
        int a = 0;
        recorder.setVideoCodecName("lib264");
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        recorder.setFormat("mp4");
        recorder.setFrameRate(24);
        System.out.println("recorder: " + recorder);
        recorder.start();
        Object lock = new Object();
        new Thread(() -> {
   if(outputStream.size()==1024){  
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            try {
                while (isFileStreamOn) {
                    InputStream stream = new FileInputStream(file);
                    synchronized (lock) {
                        while (stream.available() > 0) {
                            byte[] bytes = new byte[1024];
                            int length = stream.read(bytes);
                            RawPacket rawPacket = new RawPacket(bytes, 0, length);
                            DatagramPacket udpPacket = new DatagramPacket(rawPacket.getBuffer(), rawPacket.getLength(), targetHost, targetPort);
                            udpSocket.send(udpPacket);
                            System.out.println("发送数据包 " + Arrays.toString(udpPacket.getData()));
                        }
                        System.out.println("一份file读完,更新file");
//                        file = new File(file.getAbsoluteFile());
                        try (FileWriter writer = new FileWriter(file)) {
                            writer.write(""); // 写入空字符串,清空文件内容
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        stream = new FileInputStream(file);
                    }
                }
                System.out.println("一号外层while结束");
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }).start();
        while (isOpened) {
            synchronized (lock) {
                System.out.println("正在添加FFmpag");
                BufferedImage image = webcam.getImage();
                Frame frame = converter.getFrame(image);
                recorder.record(frame);
                if (!isFileStreamOn) {
                    isFileStreamOn = true;
                }
            }
            Thread.yield();
        }
        recorder.stop();
        recorder.release();
    }

第三步:如何接收RTP包

由于前端代码不太熟练,我决定用Java写一个后台接收这些文件进行验证

public static void main(String[] args) throws IOException {
        DatagramSocket ds = new DatagramSocket(2244);
        byte[] bytes = new byte[1024];
        int length = bytes.length;
        Object lock = new Object();  //锁似乎没有必要
        Queue<DatagramPacket> dps = new ArrayDeque<>();
        new Thread() {
            @Override
            public void run() {
                try {
                    FileOutputStream outputStream = new FileOutputStream("Demo.mp4");
                    while (!dps.isEmpty()) {
                        byte[] data = dps.poll().getData();
                        outputStream.write(data);
                        System.out.println(data);
                        if (data.length == 0) {
                            break;
                        }
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }.start();
        while (true) {
            DatagramPacket dp = new DatagramPacket(bytes, length);
            System.out.println("接收到dp"+ Arrays.toString(dp.getData()));
            dps.add(dp);
            ds.receive(dp);
        }
    }

这里也是两个线程异步操作,一个用来接收,一个用来解读

值得一提的是:由于此时数据是分批来的,天然可以用队列有序操作。 而像之前数据是基于流进行传输,对一整个长时间的流数据的操作首先就需要合理的分片,这就要求涉及的线程通过加锁避免冲突。

关于之前几个版本的记录:

1. 后端之间存储mp4文件到静态资源,前端访问到即播放

后台代码:

 @GetMapping("/setVideo")
    public synchronized Result setVideo() throws FFmpegFrameRecorder.Exception {  //默认获取三十秒的视频
        //加锁是为了防止webcam被多个线程调用
        if(!webcam.isOpen()){
            webcam.open();
        }
        num++;
        List<BufferedImage> list = new ArrayList<>();
        long start = System.currentTimeMillis();
        while (System.currentTimeMillis()-start<=10000){
            BufferedImage bfi = webcam.getImage();
            list.add(bfi);
        }
        System.out.println("鹿丸!,开存!");
        webcam.close();
        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder("src/main/resources/static/output.mp4",webcam.getViewSize().width,webcam.getViewSize().height);
        recorder.setVideoCodecName("lib264");
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        recorder.setFormat("mp4");
        recorder.setFrameRate(24);
        System.out.println("recorder: "+recorder);
        recorder.start();
        for (int i = 0; i < list.size(); i++) {
            Frame frame = converter.getFrame(list.get(i));
            recorder.record(frame);
        }
        recorder.stop();
        recorder.release();
        System.out.println("存完");
        return Result.ok("down");
    }

前端代码: 由于前端写的很乱,很多东西都挤在一个vue页面里,不太方面拆分 以下是核心实现代码:

const getVideo = function () {
  axios.get("http://localhost:8080/output.mp4?t="+(new Date()).getTime(), { responseType: 'blob'}).then((result) => {
    console.log("getVideo的result: ", result);
    const blob = new Blob([result.data], { type: 'video/mp4' });
    const videoURL = URL.createObjectURL(blob);
    console.log("videoValue", videoURL);
    video.value = videoURL;
  })
}
const setVideo = function () {
  console.log('发起setVideo,后台开始录制');
  axios.get("http://localhost:8080/setVideo").then((result) => {  //由于是axios异步请求,axios还没把videoURL填充
    console.log("setVideo的result", result);
  })
  setTimeout(function () {
    console.log("发起getVideo,获取后台录制好的avi");
    getVideo();
    setVideo();
  }, 12000);

主要的思路是:

后端循环录制一定时间的视频(比如说是10s),一旦录制好就交给前端进行播放,类似在后端用mp4文件作为缓存一样

  • 前端其实没办法知道后端什么时候视频更新了,于是要么轮询,要么长链接,要么前后端约定一个时间,我是基于最后一种办法,但很明显网络的传输和程序的不确定耗时必然导致时间的不准确——后端限制10s的视频常常录出12s...
  • 还有一个我遇到的最大的问题是:浏览器的缓存问题: 明明后端的静态文件更新了,明明我发的AJAX里有时间戳,明明我在开发者工具中设置了禁止缓存...但无论是AJAX还是之间localhost:8080/output.mp4都是老视频 由于是直接访问后台静态资源,没法用MVC模板给response设置缓存,但可以加过滤器强行拦截;或者也有在前端增加增加配置来禁止缓存的,但也没用...

还有一些额外的问题,比如,浏览器有时将访问到的视频是播放还是下载,取决于响应头Content-Disposition,可以setHeader强行更改为attachment表示为播放;通过工具将视频编码格式转化成支持浏览器的格式......

2. 用webSocket优化

webSocket的作用是在前后端之间建立平等互通的通道,主要是后台也可以给前端主动发起数据,用这个可以优化之前由于前后端规定时间访问而出现的错位问题

关于webSocket,它的设计还给前后端连接的建立提供了钩子,这个思想在Java中感觉不常见;在vue这种很强调生命周期的语法中很常见 如下:可以在建立连接,关闭连接,接收信息,发送消息时都写逻辑,就和前端能够更好地,平等的完成一致的功能了。

后端代码:

@GetMapping("openCam")
    public void openCam() throws IOException {
        if (!webcam.isOpen()) {
            webcam.open();
        }
        isOpened = true;
        DatagramSocket udpSocket = new DatagramSocket();
        InetAddress targetHost = InetAddress.getByName("localhost");
        int targetPort = 80;
        while (isOpened) {
            List<BufferedImage> list = new ArrayList<>();
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis() - start <= 10000) {
                BufferedImage bfi = webcam.getImage();
                if (needsLight) {
                    bfi = bfiOps.light(bfi, lightIndex);
                }
                if (needDuibidu) {
                    bfi = bfiOps.duibidu(bfi, duibiduIndex);
                }
                list.add(bfi);
            }
            System.out.println("鹿丸!,开存!小节视频长度:" + (System.currentTimeMillis() - start));
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            byte[] byteArray = outputStream.toByteArray();
            FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputStream, webcam.getViewSize().width, webcam.getViewSize().height);
            recorder.setVideoCodecName("lib264");
            recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
            recorder.setFormat("mp4");
            recorder.setFrameRate(24);
            System.out.println("recorder: " + recorder);
            recorder.start();
            for (int i = 0; i < list.size(); i++) {
                Frame frame = converter.getFrame(list.get(i));
                recorder.record(frame);
            }
            recorder.stop();
            recorder.release();
            System.out.println("后台保存完毕");
            RawPacket rtpPacket = new RawPacket(byteArray, 0, byteArray.length);//构造中需要传入起始终结,可能表明不是让一遍传完
            DatagramPacket udpPacket = new DatagramPacket(rtpPacket.getBuffer(), rtpPacket.getLength(), targetHost, targetPort);
            udpSocket.send(udpPacket);
            udpSocket.close();
            websocket.sendOneMessage("0", "down");
        }
        webcam.close();
    }

前端代码:

const openCam = function() {
  socket = new WebSocket("ws://localhost:8080/websocket/"+userId);
  socket.onopen = function(){
    console.log("开启");
    axios.get("http://localhost:8080/openCam").then((result)=>{
      console.log(result);
    })
  }
  socket.onmessage = function(msg){
    console.log("收到消息",msg.data);
    if(msg.data === "down"){
        getVideo();
    }
  }
}

以上就是Java实现实时视频转播的代码示例的详细内容,更多关于Java实时视频转播的资料请关注脚本之家其它相关文章!

相关文章

  • Java8 Stream 流常用方法合集

    Java8 Stream 流常用方法合集

    这篇文章主要介绍了 Java8 Stream 流常用方法合集,Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作,下文相关资料,需要的朋友可以参考一下
    2022-04-04
  • eclipse+myeclipse 环境配置方法

    eclipse+myeclipse 环境配置方法

    eclipse+myeclipse配置环境
    2009-07-07
  • Java复制(拷贝)数组的4种方法:arraycopy()方法、clone() 方法、copyOf()和copyOfRan

    Java复制(拷贝)数组的4种方法:arraycopy()方法、clone() 方法、copyOf()和copyOfRa

    这篇文章主要介绍了Java复制(拷贝)数组的4种方法:arraycopy()方法、clone() 方法、copyOf()和copyOfRan,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • Java 编程中十个处理异常的建议

    Java 编程中十个处理异常的建议

    这篇文章主要介绍了Java 编程中十个处理异常的建议,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • Java序列化(Serialization) 机制

    Java序列化(Serialization) 机制

    本篇文章是对Java中对象的序列化(Serialization) 机制进行了详细的分析介绍,并附实例,需要的朋友可以参考下
    2016-07-07
  • 修改request的parameter的几种方式总结

    修改request的parameter的几种方式总结

    这篇文章主要介绍了修改request的parameter的几种方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • 浅谈java 中equals和==的区别

    浅谈java 中equals和==的区别

    这篇文章主要介绍了java 中equals和==的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03
  • SpringBoot自动装配之Condition深入讲解

    SpringBoot自动装配之Condition深入讲解

    @Conditional表示仅当所有指定条件都匹配时,组件才有资格注册。该@Conditional注释可以在以下任一方式使用:作为任何@Bean方法的方法级注释、作为任何类的直接或间接注释的类型级别注释@Component,包括@Configuration类、作为元注释,目的是组成自定义构造型注释
    2023-01-01
  • Spring IOC容器使用详细讲解

    Spring IOC容器使用详细讲解

    IOC-Inversion of Control,即控制反转。它不是什么技术,而是一种设计思想。这篇文章将为大家介绍一下Spring控制反转IOC的原理,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-12-12
  • 解析Java中PriorityQueue优先级队列结构的源码及用法

    解析Java中PriorityQueue优先级队列结构的源码及用法

    优先级队列是一种队列结构,是0个或多个元素的集合,每个元素都有一个优先权,PriorityQueue被内置于JDK中,本文就来解析Java中PriorityQueue优先级队列结构的源码及用法.
    2016-05-05

最新评论