读书笔记-第六课

£可爱£侵袭症+ 提交于 2020-01-20 04:59:15

前言

查看了相关文章然后一笔一笔打代码再调试成功出结果,
eguid的博客
不保证代码能够原封不动就能运行,
这里做一下记录。
ps:代码内容有改动,原版的可以看原作者的。

代码

package net.w2p.JCVStudio.zhiboStudy;


import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.CanvasFrame;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Point;
import org.bytedeco.opencv.opencv_core.Scalar;
import org.bytedeco.opencv.opencv_videoio.VideoCapture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sound.sampled.*;
import javax.swing.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.text.DecimalFormat;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/***
 *https://blog.csdn.net/eguid_1/article/details/52804246
 * javaCV开发详解之6:本地音频(话筒设备)和视频(摄像头)抓取、混合并推送(录制)到服务器(本地)
 * **/
public class Lesson06 {

    private Logger logger = LoggerFactory.getLogger("[第六课]");

    private static final String key4Stop = "stop";
    private static final String key4AudioTask = "audioTask";
    private static final String key4VideoCapture = "videoCapture";
    private static final String key4Recorder = "recorder";
    private ConcurrentHashMap<String, Object> middleMap = new ConcurrentHashMap<>(5);

    /**
     * 推送/录制本机的音/视频(Webcam/Microphone)到流媒体服务器(Stream media server)
     *
     * @param WEBCAM_DEVICE_INDEX - 视频设备,本机默认是0
     * @param AUDIO_DEVICE_INDEX  - 音频设备,本机默认是4
     * @param outputFile          - 输出文件/地址(可以是本地文件,也可以是流媒体服务器地址)
     * @param captureWidth        - 摄像头宽
     * @param captureHeight       - 摄像头高
     * @param FRAME_RATE          - 视频帧率:最低 25(即每秒25张图片,低于25就会出现闪屏)
     * @throws org.bytedeco.javacv.FrameGrabber.Exception
     */
    public void recordWebcamAndMicrophone(
            int AUDIO_DEVICE_INDEX,
            String outputFile,
            int FRAME_RATE) throws Exception {

        final OpenCVFrameConverter.ToIplImage iplImageConverter = new OpenCVFrameConverter.ToIplImage();


        //--尝试读取摄像头。
        VideoCapture videoCapture = null;

        int videoCapIndex = -1;
        for (; videoCapIndex < 5; videoCapIndex++) {
            videoCapture = new VideoCapture(videoCapIndex);
            if (videoCapture.grab()) {
                logger.info("成功找到本机摄像头,摄像头当前序号:{}", videoCapIndex);
                break;
            }
            logger.info("摄像头{}初始化失败,关闭释放资源,继续尝试。", videoCapIndex);
            videoCapture.close();//--
        }

        if (videoCapture != null && !videoCapture.isOpened()) {
            logger.info("摄像头初始化失败,请检查是否有摄像头硬件。");
            return;
        }
        middleMap.put(key4VideoCapture, videoCapture);


        //--初始化相关显示用的frame框。
        CanvasFrame cFrame = new CanvasFrame("第六课,测试摄像头+耳麦混合输出");
        cFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        cFrame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                logger.info("关闭了ui窗口");
                middleMap.put(key4Stop, true);
                try{
                    ScheduledFuture tasker=(ScheduledFuture)middleMap.get(key4AudioTask);
                    if(tasker!=null){
                        tasker.cancel(true);
                    }
                }
                catch (Exception ed){
                    ed.printStackTrace();
                }

            }
        });

        //--frame初始化结束


        Mat mat = new Mat();
        /**用于保存帧的开始和结束时间**/
        double frameStartTime = System.currentTimeMillis();
        double frameEndTime = 0;
        /***用于计算写入帧的时间戳**/
        long videoTS = 0;
        long videoStartTime = 0;
        Frame ftmp = null;
        /**打水印相关参数**/
        String msg = "fps:";//水印文字
        // 水印文字位置
        Point point = new Point(10, 50);
        // 颜色,使用黄色
        Scalar scalar = new Scalar(0, 255, 255, 0);
        DecimalFormat df = new DecimalFormat(".##");//数字格式化

        FFmpegFrameRecorder recorder=null;



        /***视频数据采集 begin**/
        for (int i = 0; ; i++) {
            ftmp = null;
            if (middleMap.get(key4Stop) != null) {

                logger.info("接收到结束标志位,操作结束");
                videoCapture.close();
                videoCapture.release();
                break;
            }

            videoCapture.retrieve(mat);//--重新初始化mat

            /***采集摄像头数据***/
            if (videoCapture.grab()) {
                /***读取一帧mat图像**/
                if (videoCapture.read(mat)) {
                    frameEndTime = System.currentTimeMillis();//--获得两帧之间的时间。
                    //--打水印
                    String ftpTips = String.format("%s:%s", msg, df.format((1000.0 / (frameEndTime - frameStartTime))));

                    opencv_imgproc.putText(mat, ftpTips, point, opencv_imgproc.CV_FONT_VECTOR0
                            , 1.2, scalar
                    );

                    ftmp=iplImageConverter.convert(mat);
                    cFrame.showImage(ftmp);
                    if(recorder==null){
                        recorder=initRecorder(outputFile
                                ,ftmp.imageWidth
                                ,ftmp.imageHeight
                                ,25
                                );
                        try {
                            recorder.start();
                        } catch (org.bytedeco.javacv.FrameRecorder.Exception e2) {
                            if (recorder != null) {
                                logger.info("关闭失败,尝试重启");
                                try {
                                    recorder.stop();
                                    recorder.start();
                                } catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
                                    try {
                                        logger.info("开启失败,关闭录制");
                                        recorder.stop();
                                        return;
                                    } catch (org.bytedeco.javacv.FrameRecorder.Exception e1) {
                                        return;
                                    }
                                }
                            }

                        }

                        //--顺便初始化 音频录制线程。
//                        runNewThread4Audio(0,25);

                        final ScheduledThreadPoolExecutor exec=new ScheduledThreadPoolExecutor(1);
                        Runnable crabAudio=localAudioRecordTask(0);

                        if(crabAudio!=null){
                            ScheduledFuture tasker=exec.scheduleAtFixedRate(crabAudio,
                                    0,(long)1000/FRAME_RATE,
                                    TimeUnit.MILLISECONDS);

                            middleMap.put(key4AudioTask,tasker);
                        }

                    }

                    //定义我们的开始时间,当开始时需要先初始化时间戳
                    if(videoStartTime==0){
                        videoStartTime=System.currentTimeMillis();
                    }
                    //--创建一个timestamp用来写入帧中
                    videoTS=1000*(System.currentTimeMillis()-videoStartTime);
                    //--检查偏移量
                    if(videoTS>recorder.getTimestamp()){
                        logger.info("Lip-flap correction: videoTs is:{} and recorderTimeStamp is :{}",videoTS,recorder.getTimestamp());
                        //--告诉录制器写入这个timestamp
                        recorder.setTimestamp(videoTS);
                    }
                    //--发送帧
                    try{
                        recorder.record(ftmp);
                    }
                    catch (Exception ed){
                        ed.printStackTrace();
                        logger.info("录制帧发生异常:",ed.getMessage());
                    }


                    //--最后,保存上一帧的时间
                    frameStartTime = frameEndTime;
                }
                mat.release();
            } else {
                continue;
            }

        }

        /***视频数据采集 end**/


    }


    private FFmpegFrameRecorder initRecorder(String outputFile
            , int captureWidth, int captureHeight, int FRAME_RATE
    ) {
        /***recorder推流器初始化  begin**/
        /**
         * FFmpegFrameRecorder(String filename, int imageWidth, int imageHeight,
         * int audioChannels) fileName可以是本地文件(会自动创建),也可以是RTMP路径(发布到流媒体服务器)
         * imageWidth = width (为捕获器设置宽) imageHeight = height (为捕获器设置高)
         * audioChannels = 2(立体声);1(单声道);0(无音频)
         */
        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputFile
                , captureWidth, captureHeight
                , 2
        );
        recorder.setInterleaved(true);

        /**
         * 该参数用于降低延迟 参考FFMPEG官方文档:https://trac.ffmpeg.org/wiki/StreamingGuide
         * 官方原文参考:ffmpeg -f dshow -i video="Virtual-Camera" -vcodec libx264
         * -tune zerolatency -b 900k -f mpegts udp://10.1.0.102:1234
         */

        recorder.setVideoOption("tune", "zerolatency");

        /**
         * 权衡quality(视频质量)和encode speed(编码速度) values(值):
         * ultrafast(终极快),superfast(超级快), veryfast(非常快), faster(很快), fast(快),
         * medium(中等), slow(慢), slower(很慢), veryslow(非常慢)
         * ultrafast(终极快)提供最少的压缩(低编码器CPU)和最大的视频流大小;而veryslow(非常慢)提供最佳的压缩(高编码器CPU)的同时降低视频流的大小
         * 参考:https://trac.ffmpeg.org/wiki/Encode/H.264 官方原文参考:-preset ultrafast
         * as the name implies provides for the fastest possible encoding. If
         * some tradeoff between quality and encode speed, go for the speed.
         * This might be needed if you are going to be transcoding multiple
         * streams on one machine.
         */
        recorder.setVideoOption("preset", "ultrafast");

        /**
         * 参考转流命令: ffmpeg
         * -i'udp://localhost:5000?fifo_size=1000000&overrun_nonfatal=1' -crf 30
         * -preset ultrafast -acodec aac -strict experimental -ar 44100 -ac
         * 2-b:a 96k -vcodec libx264 -r 25 -b:v 500k -f flv 'rtmp://<wowza
         * serverIP>/live/cam0' -crf 30
         * -设置内容速率因子,这是一个x264的动态比特率参数,它能够在复杂场景下(使用不同比特率,即可变比特率)保持视频质量;
         * 可以设置更低的质量(quality)和比特率(bit rate),参考Encode/H.264 -preset ultrafast
         * -参考上面preset参数,与视频压缩率(视频大小)和速度有关,需要根据情况平衡两大点:压缩率(视频大小),编/解码速度 -acodec
         * aac -设置音频编/解码器 (内部AAC编码) -strict experimental
         * -允许使用一些实验的编解码器(比如上面的内部AAC属于实验编解码器) -ar 44100 设置音频采样率(audio sample
         * rate) -ac 2 指定双通道音频(即立体声) -b:a 96k 设置音频比特率(bit rate) -vcodec libx264
         * 设置视频编解码器(codec) -r 25 -设置帧率(frame rate) -b:v 500k -设置视频比特率(bit
         * rate),比特率越高视频越清晰,视频体积也会变大,需要根据实际选择合理范围 -f flv
         * -提供输出流封装格式(rtmp协议只支持flv封装格式) 'rtmp://<FMS server
         * IP>/live/cam0'-流媒体服务器地址
         */
        recorder.setVideoOption("crf", "25");
        // 2000 kb/s, 720P视频的合理比特率范围
        recorder.setVideoBitrate(2000000);
        // h264编/解码器
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        // 封装格式flv
        recorder.setFormat("flv");
        // 视频帧率(保证视频质量的情况下最低25,低于25会出现闪屏)
        recorder.setFrameRate(FRAME_RATE);
        // 关键帧间隔,一般与帧率相同或者是视频帧率的两倍
        recorder.setGopSize(FRAME_RATE * 2);
        // 不可变(固定)音频比特率
        recorder.setAudioOption("crf", "0");
        // 最高质量
        recorder.setAudioQuality(0);
        // 音频比特率
        recorder.setAudioBitrate(192000);
        // 音频采样率
        recorder.setSampleRate(44100);
        // 双通道(立体声)
        recorder.setAudioChannels(2);
        // 音频编/解码器
        recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);

        middleMap.put(key4Recorder,recorder);

        /***recorder推流器初始化  end**/

        return recorder;
    }

    /**
     * 设置音频编码器 最好是系统支持的格式,否则getLine() 会发生错误
     * 采样率:44.1k;采样率位数:16位;立体声(stereo);是否签名;true:
     * big-endian字节顺序,false:little-endian字节顺序(详见:ByteOrder类)
     */
    private Runnable localAudioRecordTask(final int AUDIO_DEVICE_INDEX) throws Exception {

        /**
         * 设置音频编码器 最好是系统支持的格式,否则getLine() 会发生错误
         * 采样率:44.1k;采样率位数:16位;立体声(stereo);是否签名;true:
         * big-endian字节顺序,false:little-endian字节顺序(详见:ByteOrder类)
         */
        AudioFormat audioFormat = new AudioFormat(44100.0F, 16, 2, true, false);

        // 通过AudioSystem获取本地音频混合器信息
        Mixer.Info[] minfoSet = AudioSystem.getMixerInfo();
        // 通过AudioSystem获取本地音频混合器
        Mixer mixer = AudioSystem.getMixer(minfoSet[AUDIO_DEVICE_INDEX]);
        // 通过设置好的音频编解码器获取数据线信息
        DataLine.Info dataLineInfo = new DataLine.Info(TargetDataLine.class, audioFormat);


        try {
            // 打开并开始捕获音频
            // 通过line可以获得更多控制权
            // 获取设备:TargetDataLine line
            // =(TargetDataLine)mixer.getLine(dataLineInfo);

            TargetDataLine line = (TargetDataLine) AudioSystem.getLine(dataLineInfo);
            line.open(audioFormat);
            line.start();

            //--


            // 获得当前音频采样率
            int sampleRate = (int) audioFormat.getSampleRate();
            // 获取当前音频通道数量
            int numChannels = audioFormat.getChannels();
            // 初始化音频缓冲区(size是音频采样率*通道数)
            int audioBufferSize = sampleRate * numChannels;
            byte[] audioBytes = new byte[audioBufferSize];


            Runnable crabAudio = new Runnable() {
                ShortBuffer sBuff = null;
                int nBytesRead;
                int nSamplesRead;

                @Override
                public void run() {
                    if (Thread.interrupted()) {
                        logger.info("线程已经关闭了,无须执行其他操作。");
                        return;
                    }
                    if (middleMap.get(key4Stop) != null) {
                        logger.info("录制声音时候发现已经停止了。");
                        try {

                        } catch (Exception ed) {
                            ed.printStackTrace();
                        }
                        return;
                    }
                    if (Thread.interrupted()) {
                        logger.info("线程已经被停止。");
                        return;
                    }

                    logger.info("读取音频数据...");

                    // 非阻塞方式读取
                    nBytesRead = line.read(audioBytes, 0, line.available());
                    // 因为我们设置的是16位音频格式,所以需要将byte[]转成short[]
                    nSamplesRead = nBytesRead / 2;
                    short[] samples = new short[nSamplesRead];
                    /**
                     * ByteBuffer.wrap(audioBytes)-将byte[]数组包装到缓冲区
                     * ByteBuffer.order(ByteOrder)-按little-endian修改字节顺序,解码器定义的
                     * ByteBuffer.asShortBuffer()-创建一个新的short[]缓冲区
                     * ShortBuffer.get(samples)-将缓冲区里short数据传输到short[]
                     */

                    ByteBuffer.wrap(audioBytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(samples);
                    sBuff = ShortBuffer.wrap(samples, 0, nSamplesRead);

                    // 按通道录制shortBuffer
                    FFmpegFrameRecorder recorder = (FFmpegFrameRecorder) middleMap.get(key4Recorder);
                    try {
                        if (recorder != null) {
                            recorder.recordSamples(sampleRate, numChannels, sBuff);
                        }

                    } catch (Exception ed) {
                        ed.printStackTrace();

                    }


                }

                @Override
                protected void finalize() throws Throwable {
                    sBuff.clear();
                    sBuff = null;
                    super.finalize();
                }

            };

            return crabAudio;
        }
        catch (Exception ed2){
            ed2.printStackTrace();
            return null;
        }

    }



    public static void main(String[] args) throws Exception {
        final String outFile = "/home/too-white/temp/lesson06_output.flv";
        final String rtmpUrl = "rtmp://localhost/live/livestream";
        Lesson06 test = new Lesson06();

        String strOutput = rtmpUrl;

        test.recordWebcamAndMicrophone(0, strOutput
                , 25
        );
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!