草庐IT

搭建Docker+SRS服务器实现推流拉流的效果

宝华的小岛 2024-01-30 原文

最初的一个想法,是针对当前的网络电视去的,很多网络电视买回家,还要充很多会员,甚至跌入连环坑。我想给妈妈买一台电视,想把我自己收集的电影电视剧做成一个影视库,通过搭建家庭影院服务器,然后在安卓终端上面点播。最初想得很简单,就是做一个文件服务器就可以了,但是安卓支持的解码器有限,就想着在服务器把各种格式的电影转换成流媒体,推向流媒体服务器。安卓软件直接从流媒体服务器拉流播放就可以了,不考虑解码的问题。

之前写过一个手机直播的模型,使用的rtmp服务器是nginx,这次我使用的是用Docker搭建的SRS服务器。

关于使用Docker搭建SRS服务器可以参照官网的文章:

http://ossrs.net/lts/zh-cn/docs/v4/doc/getting-started

首先我在一个虚拟机上面拉取镜像

docker pull registry.cn-hangzhou.aliyuncs.com/ossrs/srs:4

然后运行一个容器

docker run --rm -it -p 1935:1935 -p 1985:1985 -p 8080:8080 \
    registry.cn-hangzhou.aliyuncs.com/ossrs/srs:4 ./objs/srs -c conf/docker.conf

这是官网的指令,我一个字没改过。

就这样,一个SRS服务器就建好了。我的服务器host映射为srs.chris.com。下来就是推流了。

推流之前,要准备好一两段视频。还要在本地安装ffmpeg。安装方法参照:

https://blog.csdn.net/weixin_45947430/article/details/122509083

我是用的windows11-21h2,安装过后始终是找不到命令,反复确认了多次,也不知道是啥问题。没办法,只好使用绝对路径。

第一种推流,用命令行:

ffmpeg -re -stream_loop -1 -i ./v2.mp4 -c copy -f flv rtmp://srs.chris.com/live/livestream

因为视频比较短,所以添加了-stream -1进行循环推流。

srs提供了一个播放终端,在浏览器打开http://srs.chris.con:8080就可以打开页面,找到srs播放器,就能直接播放了。

 不过也可以下载一个vlc播放器,来打开一个串行流。

 这样基本推拉流都可以实现了。

我的最终目标是用Java写一个服务,所以最终需要使用Java代码来推流。

Java代码推流有两种方式。

第一种,在在Java代码中执行ffmpeg命令。

    @SneakyThrows
    public static void srsFile(String streamName,String filePath) {
            String command = "G:\\downs\\ffmpeg\\bin\\ffmpeg -re -stream_loop -1 " +
                    "-i " +
                    filePath +
                    " -c copy " +
                    "-f flv rtmp://srs.chris.com/live/" + streamName;
            System.out.println(command);
            Process process = Runtime.getRuntime().exec(command);
            BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String line = null;
            while ((line = br.readLine()) != null) {
                System.out.println("视频推流信息[" + line + "]");
        }
    }

第二种,是使用javacv库,通过抓取视频文件的帧,逐帧推送。

package com.chris.demo;

import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.avcodec.AVCodecParameters;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.avformat.AVStream;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.FFmpegLogCallback;
import org.bytedeco.javacv.Frame;

/**
 * @author willzhao
 * @version 1.0
 * @description 读取指定的mp4文件,推送到SRS服务器
 * @date 2021/11/19 8:49
 */
@Slf4j
public class PushMp4 {
    /**
     * 本地MP4文件的完整路径(两分零五秒的视频)
     */
    private static final String MP4_FILE_PATH = "G:\\downs\\video\\v2.mp4";

    /**
     * SRS的推流地址
     */
    private static final String SRS_PUSH_ADDRESS = "rtmp://192.168.2.61/live/livestream";

    /**
     * 读取指定的mp4文件,推送到SRS服务器
     *
     * @param sourceFilePath 视频文件的绝对路径
     * @param PUSH_ADDRESS   推流地址
     * @throws Exception
     */
    public static void grabAndPush(String sourceFilePath, String PUSH_ADDRESS) throws Exception {
        // ffmepg日志级别
        avutil.av_log_set_level(avutil.AV_LOG_ERROR);
        FFmpegLogCallback.set();

        // 实例化帧抓取器对象,将文件路径传入
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(MP4_FILE_PATH);

        long startTime = System.currentTimeMillis();
        log.info("开始初始化帧抓取器");

        // 初始化帧抓取器,例如数据结构(时间戳、编码器上下文、帧对象等),
        // 如果入参等于true,还会调用avformat_find_stream_info方法获取流的信息,放入AVFormatContext类型的成员变量oc中
        grabber.start(true);

        log.info("帧抓取器初始化完成,耗时[{}]毫秒", System.currentTimeMillis() - startTime);

        // grabber.start方法中,初始化的解码器信息存在放在grabber的成员变量oc中
        AVFormatContext avFormatContext = grabber.getFormatContext();

        // 文件内有几个媒体流(一般是视频流+音频流)
        int streamNum = avFormatContext.nb_streams();

        // 没有媒体流就不用继续了
        if (streamNum < 1) {
            log.error("文件内不存在媒体流");
            return;
        }

        // 取得视频的帧率
        int frameRate = (int) grabber.getVideoFrameRate();

        log.info("视频帧率[{}],视频时长[{}]秒,媒体流数量[{}]",
                frameRate,
                avFormatContext.duration() / 1000000,
                avFormatContext.nb_streams());

        // 遍历每一个流,检查其类型
        for (int i = 0; i < streamNum; i++) {
            AVStream avStream = avFormatContext.streams(i);
            AVCodecParameters avCodecParameters = avStream.codecpar();
            log.info("流的索引[{}],编码器类型[{}],编码器ID[{}]", i, avCodecParameters.codec_type(), avCodecParameters.codec_id());
        }

        // 视频宽度
        int frameWidth = grabber.getImageWidth();
        // 视频高度
        int frameHeight = grabber.getImageHeight();
        // 音频通道数量
        int audioChannels = grabber.getAudioChannels();

        log.info("视频宽度[{}],视频高度[{}],音频通道数[{}]",
                frameWidth,
                frameHeight,
                audioChannels);

        // 实例化FFmpegFrameRecorder,将SRS的推送地址传入
        FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(SRS_PUSH_ADDRESS,
                320,
                (int) (frameHeight / (frameWidth * 1.0) * 320),
                audioChannels);

        // 设置编码格式
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);

        // 设置封装格式
        recorder.setFormat("flv");

        // 一秒内的帧数
        recorder.setFrameRate(frameRate);

        // 两个关键帧之间的帧数
        recorder.setGopSize(frameRate);

        // 设置音频通道数,与视频源的通道数相等
        recorder.setAudioChannels(grabber.getAudioChannels());

        startTime = System.currentTimeMillis();
        log.info("开始初始化帧抓取器");

        // 初始化帧录制器,例如数据结构(音频流、视频流指针,编码器),
        // 调用av_guess_format方法,确定视频输出时的封装方式,
        // 媒体上下文对象的内存分配,
        // 编码器的各项参数设置
        recorder.start();

        log.info("帧录制初始化完成,耗时[{}]毫秒", System.currentTimeMillis() - startTime);

        Frame frame;

        startTime = System.currentTimeMillis();

        log.info("开始推流");

        long videoTS = 0;

        int videoFrameNum = 0;
        int audioFrameNum = 0;
        int dataFrameNum = 0;

        // 假设一秒钟15帧,那么两帧间隔就是(1000/15)毫秒
        int interVal = 1000 / frameRate;
        // 发送完一帧后sleep的时间,不能完全等于(1000/frameRate),不然会卡顿,
        // 要更小一些,这里取八分之一
        interVal /= 8;

        // 持续从视频源取帧
        while (null != (frame = grabber.grab())) {
            videoTS = 1000 * (System.currentTimeMillis() - startTime);

            // 时间戳
            recorder.setTimestamp(videoTS);

            // 有图像,就把视频帧加一
            if (null != frame.image) {
                videoFrameNum++;
            }

            // 有声音,就把音频帧加一
            if (null != frame.samples) {
                audioFrameNum++;
            }

            // 有数据,就把数据帧加一
            if (null != frame.data) {
                dataFrameNum++;
            }

            // 取出的每一帧,都推送到SRS
            recorder.record(frame);

            // 停顿一下再推送
            Thread.sleep(interVal);
        }

        log.info("推送完成,视频帧[{}],音频帧[{}],数据帧[{}],耗时[{}]秒",
                videoFrameNum,
                audioFrameNum,
                dataFrameNum,
                (System.currentTimeMillis() - startTime) / 1000);

        // 关闭帧录制器
        recorder.close();
        // 关闭帧抓取器
        grabber.close();
    }

    public static void main(String[] args) throws Exception {
        grabAndPush(MP4_FILE_PATH, SRS_PUSH_ADDRESS);
    }
}

上面这是一段测试成功的代码,也没有优化。看起来要复杂一些,但是可能更灵活。不过目前没有仔细研究,拉流效果感觉有些问题,也没有找到循环推流的尝试。

不过我想如果要实现我最初的想法,第一要多个视频无缝衔接,因为一个视频推流结束之后这个流 就结束了。后面要连续播放就只能重新打开。还有一个问题就是这种模式应该属于直播模式,如果我想快进跳跃式观影不知道有没有办法实现。还得继续研究。

有关搭建Docker+SRS服务器实现推流拉流的效果的更多相关文章

  1. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  2. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

    我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

  3. ruby-on-rails - 启动 Rails 服务器时 ImageMagick 的警告 - 2

    最近,当我启动我的Rails服务器时,我收到了一长串警告。虽然它不影响我的应用程序,但我想知道如何解决这些警告。我的估计是imagemagick以某种方式被调用了两次?当我在警告前后检查我的git日志时。我想知道如何解决这个问题。-bcrypt-ruby(3.1.2)-better_errors(1.0.1)+bcrypt(3.1.7)+bcrypt-ruby(3.1.5)-bcrypt(>=3.1.3)+better_errors(1.1.0)bcrypt和imagemagick有关系吗?/Users/rbchris/.rbenv/versions/2.0.0-p247/lib/ru

  4. ruby-on-rails - s3_direct_upload 在生产服务器中不工作 - 2

    在Rails4.0.2中,我使用s3_direct_upload和aws-sdkgems直接为s3存储桶上传文件。在开发环境中它工作正常,但在生产环境中它会抛出如下错误,ActionView::Template::Error(noimplicitconversionofnilintoString)在View中,create_cv_url,:id=>"s3_uploader",:key=>"cv_uploads/{unique_id}/${filename}",:key_starts_with=>"cv_uploads/",:callback_param=>"cv[direct_uplo

  5. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  6. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  7. ruby-on-rails - 在 Rails 中调试生产服务器 - 2

    您如何在Rails中的实时服务器上进行有效调试,无论是在测试版/生产服务器上?我试过直接在服务器上修改文件,然后重启应用,但是修改好像没有生效,或者需要很长时间(缓存?)我也试过在本地做“脚本/服务器生产”,但是那很慢另一种选择是编码和部署,但效率很低。有人对他们如何有效地做到这一点有任何见解吗? 最佳答案 我会回答你的问题,即使我不同意这种热修补服务器代码的方式:)首先,你真的确定你已经重启了服务器吗?您可以通过跟踪日志文件来检查它。您更改的代码显示的View可能会被缓存。缓存页面位于tmp/cache文件夹下。您可以尝试手动删除

  8. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

随机推荐