javacv实现直播流

javacv实现直播流

javacv从入门到入土系列,音视频入门有一点门槛的延迟大概是2~4秒之间,

依赖

        <!-- 需要注意,javacv主要是一组API为主,还需要加入对应的实现 -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv</artifactId>
            <version>1.5.6</version>
        </dependency>

        <!-- 用到了 ffmpeg 需要把 ffmpeg 的平台实现依赖引入 -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>ffmpeg-platform</artifactId>
            <version>4.4-1.5.6</version>
        </dependency>

        <!--所有平台实现,依赖非常大,几百MB吧-->
        <!--<dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>1.5.6</version>
        </dependency>-->

视频采集可以使用摄像头或者什么的,我这里用了桌面录像

package top.lingkang.test.gui;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.*;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.TargetDataLine;
import java.io.File;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.Timer;
import java.util.TimerTask;
/**
 * @author lingkang
 * Created by 2022/5/10
 */
public class MyLive extends Application {
    private static final int frameRate = 24;// 录制的帧率
    private static boolean isStop = false;

    private static TargetDataLine line;

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("lingkang-桌面录屏大师");
        ImageView imageVideo = new ImageView();// 用于软件录制显示
        imageVideo.setFitWidth(800);
        imageVideo.setFitHeight(600);
        Button button = new Button("停止录制");
        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                isStop = true;
                if (line != null) {// 马上停止声音录入
                    try {
                        line.close();
                    } catch (Exception e) {
                    }
                }
                Alert alert = new Alert(Alert.AlertType.INFORMATION);
                alert.setTitle("info");
                alert.setHeaderText("已经停止录制");
                alert.setOnCloseRequest(event1 -> alert.hide());
                alert.showAndWait();
            }
        });

        VBox box = new VBox();
        box.getChildren().addAll(button, imageVideo);
        primaryStage.setScene(new Scene(box));
        primaryStage.setHeight(600);
        primaryStage.setWidth(800);
        primaryStage.show();
        primaryStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
            @Override
            public void handle(WindowEvent event) {// 退出时停止
                isStop = true;
                System.exit(0);
            }
        });


        // 帧记录
        // window 建议使用 FFmpegFrameGrabber("desktop") 进行屏幕捕捉
        FrameGrabber grabber = new FFmpegFrameGrabber("desktop");
        grabber.setFormat("gdigrab");
        grabber.setFrameRate(frameRate);// 帧获取间隔
        // 捕获指定区域,不设置则为全屏
        grabber.setImageHeight(600);
        grabber.setImageWidth(800);
        // grabber.setOption("offset_x", "200");
        // grabber.setOption("offset_y", "200");//必须设置了大小才能指定区域起点,参数可参考 FFmpeg 入参
        grabber.start();

        File file = new File("D://output.avi");
        if (file.exists())
            file.delete();

        // 直播推流
        final FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(
                "rtmp://10.8.4.191/live/livestream",
                grabber.getImageWidth(), grabber.getImageHeight(), 2);

        // 用于存储视频 , 调用stop后,需要释放,就会在指定位置输出文件,,这里我保存到D盘
        //FFmpegFrameRecorder recorder = FFmpegFrameRecorder.createDefault(file, grabber.getImageWidth(), grabber.getImageHeight());
        recorder.setInterleaved(true);
        // https://trac.ffmpeg.org/wiki/StreamingGuide
        recorder.setVideoOption("tune", "zerolatency");// 加速
        // https://trac.ffmpeg.org/wiki/Encode/H.264
        recorder.setVideoOption("preset", "ultrafast");
        recorder.setFrameRate(frameRate);// 设置帧率,重要!
        // Key frame interval, in our case every 2 seconds -> 30 (fps) * 2 = 60
        recorder.setGopSize(frameRate * 2);
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);// 编码,使用编码能让视频占用内存更小,根据实际自行选择
        // https://trac.ffmpeg.org/wiki/Encode/H.264
        recorder.setVideoOption("crf", "28");
        // 2000 kb/s  720P
        recorder.setVideoBitrate(2000000);
        recorder.setFormat("flv");


        // 添加音频录制
        // 不可变音频
        recorder.setAudioOption("crf", "0");
        // 最高音质
        recorder.setAudioQuality(0);
        // 192 Kbps
        recorder.setAudioBitrate(192000);
        recorder.setSampleRate(44100);
        recorder.setAudioChannels(2);
        recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);

        recorder.start();

        // 44100  16声道
        AudioFormat audioFormat = new AudioFormat(44100.0F, 16, 2, true, false);
        DataLine.Info dataLineInfo = new DataLine.Info(TargetDataLine.class, audioFormat);
        // 可以捕捉不同声道
        line = (TargetDataLine) AudioSystem.getLine(dataLineInfo);
        // 录制声音
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    line.open(audioFormat);
                    line.start();

                    final int sampleRate = (int) audioFormat.getSampleRate();
                    final int numChannels = audioFormat.getChannels();

                    // 缓冲区
                    final int audioBufferSize = sampleRate * numChannels;
                    final byte[] audioBytes = new byte[audioBufferSize];
                    Timer timer = new Timer();
                    timer.schedule(new TimerTask() {
                        @Override
                        public void run() {
                            try {
                                if (isStop) {// 停止录音
                                    line.stop();
                                    line.close();
                                    System.out.println("已经停止!");
                                    timer.cancel();
                                }

                                // 读取音频
                                // read会阻塞
                                int readLenth = 0;
                                while (readLenth == 0)
                                    readLenth = line.read(audioBytes, 0, line.available());

                                // audioFormat 定义了音频输入为16进制,需要将字节[]转为短字节[]
                                // FFmpegFrameRecorder.recordSamples 源码中的 AV_SAMPLE_FMT_S16
                                int rl = readLenth / 2;
                                short[] samples = new short[rl];

                                // short[] 转换为 ShortBuffer
                                ByteBuffer.wrap(audioBytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(samples);
                                ShortBuffer sBuff = ShortBuffer.wrap(samples, 0, rl);

                                // 记录
                                recorder.recordSamples(sampleRate, numChannels, sBuff);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }, 1000, 1000 / frameRate);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 获取屏幕捕捉的一帧
                    Frame frame = null;
                    // 屏幕录制,由于已经对音频进行了记录,需要对记录时间进行调整即可
                    // 即上面调用了 recorder.recordSamples 需要重新分配时间,否则视频输出时长等于实际 的2倍
                    while ((frame = grabber.grab()) != null) {
                        if (isStop) {
                            try {
                                // 停止
                                recorder.stop();
                                grabber.stop();
                                // 释放内存,我们都知道c/c++需要手动释放资源
                                recorder.release();
                                grabber.release();
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                            break;
                        }

                        // 将这帧放到录制
                        recorder.record(frame);
                        Image convert = new JavaFXFrameConverter().convert(frame);
                        imageVideo.setImage(convert);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

可以用docker起一个srs进行推流播放。

# 先启动
docker run -p 1935:1935 -p 1985:1985 -p 8080:8080 \
    ccr.ccs.tencentyun.com/ossrs/srs:4
646e0c29325051d1b1c87a4c727d04be_ca0e5683c92344efb0c017940e3f968b.png

再启动推流:
http://10.8.4.191:8080/players/srs_player.html?schema=http

e33215f30d3128c0dba84116e72160d5_74fdeb2737664ba4a7f8d3837fb730b1.png

355180a38181a4e7555db5b9ce735900_96f3076796114dc481b3156e162ea022.png
a12856f0251f76e79f3106c49a93d86d_526fdc1c1c324c72b7c5111bd1739be2.png
51e58a383f42dab0f9029a7301b97b36_ae64582358a4466c9ad428f09b6476e8.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容