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


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

    之前写过一个手机直播的模型,使用的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

    然后运行一个容器

    1. docker run --rm -it -p 1935:1935 -p 1985:1985 -p 8080:8080 \
    2. 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命令。

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

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

    1. package com.chris.demo;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.bytedeco.ffmpeg.avcodec.AVCodecParameters;
    4. import org.bytedeco.ffmpeg.avformat.AVFormatContext;
    5. import org.bytedeco.ffmpeg.avformat.AVStream;
    6. import org.bytedeco.ffmpeg.global.avcodec;
    7. import org.bytedeco.ffmpeg.global.avutil;
    8. import org.bytedeco.javacv.FFmpegFrameGrabber;
    9. import org.bytedeco.javacv.FFmpegFrameRecorder;
    10. import org.bytedeco.javacv.FFmpegLogCallback;
    11. import org.bytedeco.javacv.Frame;
    12. /**
    13. * @author willzhao
    14. * @version 1.0
    15. * @description 读取指定的mp4文件,推送到SRS服务器
    16. * @date 2021/11/19 8:49
    17. */
    18. @Slf4j
    19. public class PushMp4 {
    20. /**
    21. * 本地MP4文件的完整路径(两分零五秒的视频)
    22. */
    23. private static final String MP4_FILE_PATH = "G:\\downs\\video\\v2.mp4";
    24. /**
    25. * SRS的推流地址
    26. */
    27. private static final String SRS_PUSH_ADDRESS = "rtmp://192.168.2.61/live/livestream";
    28. /**
    29. * 读取指定的mp4文件,推送到SRS服务器
    30. *
    31. * @param sourceFilePath 视频文件的绝对路径
    32. * @param PUSH_ADDRESS 推流地址
    33. * @throws Exception
    34. */
    35. public static void grabAndPush(String sourceFilePath, String PUSH_ADDRESS) throws Exception {
    36. // ffmepg日志级别
    37. avutil.av_log_set_level(avutil.AV_LOG_ERROR);
    38. FFmpegLogCallback.set();
    39. // 实例化帧抓取器对象,将文件路径传入
    40. FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(MP4_FILE_PATH);
    41. long startTime = System.currentTimeMillis();
    42. log.info("开始初始化帧抓取器");
    43. // 初始化帧抓取器,例如数据结构(时间戳、编码器上下文、帧对象等),
    44. // 如果入参等于true,还会调用avformat_find_stream_info方法获取流的信息,放入AVFormatContext类型的成员变量oc中
    45. grabber.start(true);
    46. log.info("帧抓取器初始化完成,耗时[{}]毫秒", System.currentTimeMillis() - startTime);
    47. // grabber.start方法中,初始化的解码器信息存在放在grabber的成员变量oc中
    48. AVFormatContext avFormatContext = grabber.getFormatContext();
    49. // 文件内有几个媒体流(一般是视频流+音频流)
    50. int streamNum = avFormatContext.nb_streams();
    51. // 没有媒体流就不用继续了
    52. if (streamNum < 1) {
    53. log.error("文件内不存在媒体流");
    54. return;
    55. }
    56. // 取得视频的帧率
    57. int frameRate = (int) grabber.getVideoFrameRate();
    58. log.info("视频帧率[{}],视频时长[{}]秒,媒体流数量[{}]",
    59. frameRate,
    60. avFormatContext.duration() / 1000000,
    61. avFormatContext.nb_streams());
    62. // 遍历每一个流,检查其类型
    63. for (int i = 0; i < streamNum; i++) {
    64. AVStream avStream = avFormatContext.streams(i);
    65. AVCodecParameters avCodecParameters = avStream.codecpar();
    66. log.info("流的索引[{}],编码器类型[{}],编码器ID[{}]", i, avCodecParameters.codec_type(), avCodecParameters.codec_id());
    67. }
    68. // 视频宽度
    69. int frameWidth = grabber.getImageWidth();
    70. // 视频高度
    71. int frameHeight = grabber.getImageHeight();
    72. // 音频通道数量
    73. int audioChannels = grabber.getAudioChannels();
    74. log.info("视频宽度[{}],视频高度[{}],音频通道数[{}]",
    75. frameWidth,
    76. frameHeight,
    77. audioChannels);
    78. // 实例化FFmpegFrameRecorder,将SRS的推送地址传入
    79. FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(SRS_PUSH_ADDRESS,
    80. 320,
    81. (int) (frameHeight / (frameWidth * 1.0) * 320),
    82. audioChannels);
    83. // 设置编码格式
    84. recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
    85. // 设置封装格式
    86. recorder.setFormat("flv");
    87. // 一秒内的帧数
    88. recorder.setFrameRate(frameRate);
    89. // 两个关键帧之间的帧数
    90. recorder.setGopSize(frameRate);
    91. // 设置音频通道数,与视频源的通道数相等
    92. recorder.setAudioChannels(grabber.getAudioChannels());
    93. startTime = System.currentTimeMillis();
    94. log.info("开始初始化帧抓取器");
    95. // 初始化帧录制器,例如数据结构(音频流、视频流指针,编码器),
    96. // 调用av_guess_format方法,确定视频输出时的封装方式,
    97. // 媒体上下文对象的内存分配,
    98. // 编码器的各项参数设置
    99. recorder.start();
    100. log.info("帧录制初始化完成,耗时[{}]毫秒", System.currentTimeMillis() - startTime);
    101. Frame frame;
    102. startTime = System.currentTimeMillis();
    103. log.info("开始推流");
    104. long videoTS = 0;
    105. int videoFrameNum = 0;
    106. int audioFrameNum = 0;
    107. int dataFrameNum = 0;
    108. // 假设一秒钟15帧,那么两帧间隔就是(1000/15)毫秒
    109. int interVal = 1000 / frameRate;
    110. // 发送完一帧后sleep的时间,不能完全等于(1000/frameRate),不然会卡顿,
    111. // 要更小一些,这里取八分之一
    112. interVal /= 8;
    113. // 持续从视频源取帧
    114. while (null != (frame = grabber.grab())) {
    115. videoTS = 1000 * (System.currentTimeMillis() - startTime);
    116. // 时间戳
    117. recorder.setTimestamp(videoTS);
    118. // 有图像,就把视频帧加一
    119. if (null != frame.image) {
    120. videoFrameNum++;
    121. }
    122. // 有声音,就把音频帧加一
    123. if (null != frame.samples) {
    124. audioFrameNum++;
    125. }
    126. // 有数据,就把数据帧加一
    127. if (null != frame.data) {
    128. dataFrameNum++;
    129. }
    130. // 取出的每一帧,都推送到SRS
    131. recorder.record(frame);
    132. // 停顿一下再推送
    133. Thread.sleep(interVal);
    134. }
    135. log.info("推送完成,视频帧[{}],音频帧[{}],数据帧[{}],耗时[{}]秒",
    136. videoFrameNum,
    137. audioFrameNum,
    138. dataFrameNum,
    139. (System.currentTimeMillis() - startTime) / 1000);
    140. // 关闭帧录制器
    141. recorder.close();
    142. // 关闭帧抓取器
    143. grabber.close();
    144. }
    145. public static void main(String[] args) throws Exception {
    146. grabAndPush(MP4_FILE_PATH, SRS_PUSH_ADDRESS);
    147. }
    148. }

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

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

  • 相关阅读:
    不得不会的MySQL数据库知识点(七)
    Matlab多维数组漫谈教程
    金融科技赋能 互融云手机回租系统 实现资产全流程在线运营管理
    如何优化前端性能?
    设计师设计相关图表时,如何运用设计技巧与合理的用户体验?【大屏可视化(PC端、移动端)】
    PreparedStatement vs Statement 不同及其使用
    大数据之Spark案例实操完整使用(第六章)
    【上传vip专享资源,瓜分奖金池】第一期获奖名单
    基于Qt的旅行最优时间费用模拟系统
    热量传递总复习
  • 原文地址:https://blog.csdn.net/xxkalychen/article/details/128130608