java实现直播(推拉流)和保存直播视频

首先墨迹一下前提:
公司接了个电商的项目,然后甲方要求有类似于淘宝的直播的功能。重点是!!!甲方爸爸不愿意除阿里云直播,腾讯云直播的钱。所以问题就来了,这个直播的实现要免费。
然后这个项目是java+vue实现的。前后端分离的,至于用不用分布式啥的暂时先没定下来,不过因为确定有直播模块,所以这块是先测试实现了的。这个帖子就是对实现直播这一需求的整理的记录。

互联网直播服务管理规定

2016年4月13日,百度,新浪,搜狐等20余家直播平台共同发布《北京网络直播行业自律公约》,承诺网络直播房间必须标识水印;内容存储时间不少于15天备查;所有主播必须实名认证;对于播出涉政、涉枪、涉毒、涉暴、涉黄内容的主播,情节严重的将列入黑名单;审核人员对平台上的直播内容进行24小时实时监管。
咳咳,想来想去我决定把这个放在最开始。上面的规定其实不算多:

  • 实名认证是能直播的用户要经过实名认证,代码里就可以搞定。
  • 直播间房间要水印其实这个可以和点赞等做一个挂在视频前面的“画布”,所以我们这个项目最终决定前端搞定。
  • 至于内容存储时间不小于15天,因为我做了保存直播的功能,虽然还没做定时清理,不过技术上不是难点,所以也算是完成了。
  • 最后这个涉黄,涉政,涉爆啥的人工审核我也没办法了,只能到时候甲方将项目上线自己去找人看着了。

如上,反正这个直播功能是理清楚了,大概做出来也是合法的了。接下来开始说技术吧(因为我也是第一次做直播,所以可能走了一些冤路,不过我觉得稍微有用的都会写出来,毕竟前车之鉴)

直播技术的实现

其实这一块没想的那么难,但是也没想的那么简单。我一点点分析。

首先,要理解直播是什么:

  • 图片可以有base64编码,这一串码就是代表这个图片。
  • 视频我们可以理解成是一张张图片的编码的和,展现出来的动图也不过是一张张图片连续播放的结果(我这么说并不准确,我只是为了方便理解)
  • 因为直播是一个个图片传输的,在你刚开播的一秒是没有下一秒的图片的,所以这个视频的编码是要一直传输的,而这种一直传输编码的情况,我们用一个专业术语表示:

我不知道别的语言怎么理解,但是java中有字节流,字符流等等,所以这里的流一般是音频+视频。所以具体是什么不深究,只要知道这里的流是用来传输视频的就行。

简单直播流程

然后这里有一张图很好的说明了直播的流程:


直播流程

如图,直播的流程其实主要就这么多(也可以在中间有别的操作,我说的是最简单的操作),又因为我们决定直播的形式是服务器作为中间站。所以大概流程是直播用户推流到服务器,然后有想要看直播的去服务器拉流。接下来一点点说:

  • 采集。其实这个很好理解,打开语音,摄像头,采集数据。这里也说了,需要终端音视频引擎解决。(终端是指用户直播的设备。比如手机,pc等)

  • 前处理。这个是已经试过的,比如我们现在手机拍照片自带美颜,你以为是你摄像头给你美颜的么?不是的,是摄像头拍出来的是正常照片,手机拍照后对这个照片进行了处理。而这里的前处理也是类似的道理,前端调用麦克风摄像头,拍/录视频的同时对这个视频进行一些加工(我们已经测试完了,反正是可以美颜,磨皮等。刚刚顺口问了下我们前端,说是用的h5 plus中一些自带的api,我是前端小白,不是很理解,反正是能实现就对了。)

  • 编码。这个其实就类似于图片的base 64 一样,视频流传输之前肯定是要经过编码的,应该可能也有很多种设置,反正我们调试用的H264。(关于这个编码我在网上看了下,好像直接影响编码速度解码速度什么的,也就是影响延迟,卡顿,清晰度等。不过我们现在真的是只要能实现就行了,所以暂定h264.没做什么多的尝试)

  • 推流。这个其实刚刚我解释了流是什么,推流应该不难理解,就是把自己这个视频直播的编码以流的形式传给谁。

  • 拉流。和推流对应,就是去有流的地方,把这个直播流拉倒本地。这两个行为其实很容易理解:我把这篇图文上传到简书,也就是推到简书。你们来简书看这篇图文,也就是从简书拉到你的显示器。只不过因为直播推来的都是流而已。

  • 解码。这个其实就不用说了吧,你当时传输的时候是按照一定编码编译的,肯定想要复原要按照这个编码方式解码。

  • 渲染。属于后期显示问题了,就是这个视频已经传到你观看直播的画面,不同的手机会有不同的显示,同一个视频有的手机看颜色发紫(因为我手机防辐射蓝光膜),有的手机看颜色艳丽,有的手机看颜色昏黄,这个就是渲染吧。

如上,其实一个简单的直播就是这么个流程。
然后注意一下,我这里说的只是一个最简单的实现直播的流程。复杂的比如多人连麦啊,甚至多人视频直播啊这种,推流是差不多的,但是拉流要众多流合在一起展示,这个属于复杂操作了,我也没做,有兴趣的可以自己做。

本地搭建直播平台

其实刚刚我也说了,我们打算是流程是直播用户把自己的直播流推给服务器,然后想要看直播的用户来服务器拉取对应的流。所以重点就是怎么把一台电脑作为服务器。
其实现在已经有现成的框架了,再次感谢共享技术的大大。然后我一点点说:
其实本地作为服务器是多多种方式的,我也是在网上看到的教程(最后会附上写这个文章参考的连接)。我这里只写我自己用的方法啦。
我这里用到的比较简单,是ffmpeg+nginx实现的。
其实主要就三步:

  1. 下载nginx并启动nginx(以后的所有推拉工作都要在nginx启动的情况下实现);
  2. 下载ffmpeg并设置环境变量(说实话我不知道如果没有这个会怎么样,不过三台电脑我都按照要求配置了。这个应该是推流的时候用的,不过demo中是本地推才用到这个,我感觉我和前端联调的时候好像是没用到这个啊)
  3. 自推自拉做个demo

然后感谢之前提供网盘压缩包的大佬。我这里借花献佛贴在这里:
链接: https: //pan.baidu.com/s/1lN1ps0ZhCb-1A56ycNR88g
密码: 2t88
点进这个链接可以下载需要的ffmpeg和nginx的压缩包(我在网址的https:后面加了个空格,复制粘贴的时候注意删除啊,不然链接格式发不出来)。

然后打开压缩包里面两个小压缩包,一个视频。视频是用来做实验的,用不用都行。


网盘下载来的

我解压到d盘demo文件夹中,并把ffmpeg和nginx都解压到当前文件夹了。


解压后

接下来按照步骤:

  1. 启动nginx:
    cmd打开控制台,进入到nginx-1.7.11.3-Gryphon目录,然后启动。
    比如我的是放在d盘demo下,然后命令就是:

第一步,跳到d盘:

d:

第二步,cd到nginx-1.7.11.3-Gryphon目录下:

cd demo/nginx-1.7.11.3-Gryphon

第三步,启动nginx:

nginx.exe -c conf\nginx-win-rtmp.conf

然后正常启动是不会显示内容的,但是一直在运行,这个控制台页面不要关,就这么放着。


我控制台启动nginx页面

然后可以访问ip:80直接访问,查看自己nginx是否正常启动了


nginx正常启动页面
  1. 下载ffmpeg并设置环境变量
    第一步:设置环境变量(步骤就不重复了,这个比较初级了。)
    我直接贴我的截图了:


    image.png
  2. 其实这个时候前台推来的流我就已经可以正常接收并且被同局域网的设备拉取了,不过如果自己做没有这个前端配合,或者说想确定一下是不是前面都做对了,可以自己模拟一下推流,再自己模拟拉流。
    因为我这块完全按教程做的,好像模拟拉流只能用VLC播放器,反正我是只用了这个测试。


    vlc播放器,自己去百度下载就行了

    然后这里就用到了之前下载顺便带的测试视频了,当然你电脑里有别的视频也可以用,新打开一个控制台,然后下面这串命令:

ffmpeg -re -i d:/demo/orange.mp4 -vcodec libx264 -acodec aac -f flv rtmp://ip:1935/live/home

重点来了!!首先,之前我看教程,人家是直接-i orange.mp4,我复制粘贴运行一直报找不到文件,后来我改成绝对路径就ok了。 其次这个ip+端口ip写你自己的ip就行,端口1935的设置是在nginx的配置文件中配置的,是可以改的。要酌情使用
然后如上命令中,大概意思是把orange.mp4这个视频以流的形式用h264编码,flv的格式 以rtmp的形式推给ip指向的电脑的/live/home这(自我理解,错了勿喷,欢迎指点)。
同理如果拉流也要来这个地方拉。所以可以直接复制这个路径,直接在vlc上ctrl+v可以贴网址,把这个网址贴上就能看到你之前推的视频了。

输入你之前推的网址

如果你能看到你传的视频,说明你成功了!直播平台已经搭建完了!

视频传输和直播的区别

可能看到这有人不懂了,这不就是视频传输么?凭什么叫直播啊?这不是忽悠人么?其实不是的,这个在细节上可以看到。如果是视频传输,那么接收的人只能从视频的一开始看,而直播的区别是传输到哪里就应该从哪里看(不要和我抬杠延迟)、还有就是应该是直播一结束立刻观看直播的也会结束。
因为上面的demo我们用的视频,所以这点可能让很多人误会,不过好一点的是这个视频一分多钟,而推流的过程不是一秒钟一秒钟推的,所以速度比较快,好几秒好几秒推的,但是我们可以稍微晚几秒拉流,明显可以看到每次显示的视频开始的内容是不一样的。
同样,如果到这还不能理解或者有疑问,我建议大家不要用传视频的方式证明平台搭建成功。可以直接调用本地视频音频,然后推流到本地,再用vlc拉流,这样明显看到开始直播则能拉倒,关闭直播则立刻拉不到。
做法我觉得比较麻烦,但是不难,最后我列出来的技术贴上会有教程。大家可以去看看。

保存直播视频

最开始就说了,保存直播视频15天是一个规范。然后15天其实可以多种方式实现,重点是保存。
因为我推拉流都没经过代码,我还以为保存会很复杂, 但是也比想的简单的多。我就不多说心路历程了,直接上结果(我要重复一下,实现的方式可能是多种多样的,我只不过选了一种实现了,不代表性能是最好的或者技术最新的):

  • 导包(因为用到了javaCV的FFmpegFrameGrabber帧捕捉器捕捉流的音频帧和视频帧,所以要导相应的包)
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv</artifactId>
    <version>1.4.3</version>
</dependency>
  • 复制粘贴代码
    这里因为有些不好解释,中间遇到问题FFmpegFrameGrabber类也不说清楚,还是比较坑的,我是调了很多细微的东西才实现的,我先贴出来,咱们再一点点说需要注意什么。
package io.renren.modules.other.tool;

import java.io.File;
import java.io.IOException;

import org.bytedeco.javacpp.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.FrameGrabber;
import org.bytedeco.javacv.FrameRecorder;

public class RecordVideoThread extends Thread {

    public String streamURL;// 流地址 网上有自行百度
    public String filePath;// 文件路径
    public Integer id;// 案件id
    
    public void setStreamURL(String streamURL) {
        this.streamURL = streamURL;
    }
    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }
    @Override
    public void run() {
        System.out.println(streamURL);
        // 获取视频源
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(streamURL);
        FFmpegFrameRecorder recorder = null;
        try {
            grabber.start();
            Frame frame = grabber.grabFrame();
            if (frame != null) {
                File outFile = new File(filePath);
                if (!outFile.isFile()) {
                    try {
                        outFile.createNewFile();
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                // 流媒体输出地址,分辨率(长,高),是否录制音频(0:不录制/1:录制)
                recorder = new FFmpegFrameRecorder(filePath, 1080, 1440, 1);
                recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);// 直播流格式
                recorder.setFormat("flv");// 录制的视频格式
                recorder.setFrameRate(25);// 帧数
                //百度翻译的比特率,默认400000,但是我400000贼模糊,调成800000比较合适
                recorder.setVideoBitrate(800000);
                recorder.start();
                while ((frame != null)) {
                    recorder.record(frame);// 录制
                    frame = grabber.grabFrame();// 获取下一帧
                }
                recorder.record(frame);
                // 停止录制
                recorder.stop();
                grabber.stop();
            }
        } catch (FrameGrabber.Exception e) {
            e.printStackTrace();
        } catch (FrameRecorder.Exception e) {
            e.printStackTrace();
        } finally {
            if (null != grabber) {
                try {
                    grabber.stop();
                } catch (FrameGrabber.Exception e) {
                    e.printStackTrace();
                }
            }
            if (recorder != null) {
                try {
                    recorder.stop();
                } catch (FrameRecorder.Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        RecordVideoThread thread = new RecordVideoThread();
        thread.setFilePath("D:/testOne.flv");
        thread.setStreamURL("rtmp://ip:端口/live/home");
        thread.start();        
    }
}

如下代码,推流的地址和要保存的文件的文件名我是写活了的。而且因为这个保存是个长时间执行的方法,所以我专门启线程来实现的。
然后注意顺序:
!!!必须必须按照顺序来:首先先启动方法,再启动直播,然后先结束直播,再结束方法
如果不这样做会保存不到视频。具体原因我也没深究,反正我这个是一次次测试得出来的结论,并且这样也能让直播视频完整的保存下来,这样逻辑也符合,所以就这样按照流程操作就行了。
我代码中是存到D:/testOne.flv中,给大家看下:

保存的视频

播放保存的视频

其实我这个画质也就这样了,把代码中的

ecorder.setVideoBitrate(800000);

时间越大越清楚,但是同样耗费空间也越大, 之前试过400000的时候。43s视频才5兆不到。 然后1800000的时候不到二十兆,差别就来了,所以这个值挑一个平衡值就好。

然后说最大的一个坑:格式问题
现在我也不知道是我看的所有的技术贴都过时了还是推流的方式不一样,反正好多帖子上都是mp4格式,我就傻了吧唧照着cv了,然后能保存下东西播放不了。为此还特意下载了暴风影音和迅雷看看。。。
昨天整整调了一下午都没弄好,今天上午在群友的建议下改成了avi还是不行,最后改成了flv才可以正常播放。
还有上面代码中有一行:

                // 流媒体输出地址,分辨率(长,高),是否录制音频(0:不录制/1:录制)
                recorder = new FFmpegFrameRecorder(filePath, 1080, 1440, 1);

这行代码的1080和1440也是我自己随手改的,这个长宽应该是可以自定义(其实大多数属性都可以自定义吧,只不过我没用到的就没那么仔细看)
其实这个我主要是卡在了视频格式上,但是我看百度上说遇到的问题千奇百怪。反正我大概只遇到这两个:一个是推流顺序不对会保存不到文件,还有一个就是格式问题打不开。
以后如果随着使用遇到问题了再说。
然后这次直播的实现差不多就这样了。
还有一些小知识点:

路径问题

你怎么推的怎么拉就行了,这个/live/home不是写死的。比如我们这个项目因为每个用户都可以直播,所以打算是/live/user_id这种形式,可以保证每个用户的推拉流都是不同的。顺便分享一下我们前端实现的页面:


image.png

image.png

直播页面,可以在这或者写推流地址,正常应该写死的

看直播的页面
端口问题

刚刚我就说了1935是nginx默认的rtmp端口。这个是可以改的,只不过默认配置是1935(不过我没改过,有兴趣的可以试试)如下图:


配置文件的位置

配置文件中的配置
细节问题

之前我们直播的时候发现直播效果贼卡,各种马赛克,中间经过了怀疑,调试等功能,发现应该是前端可以设置推流的一些配置,除了特别的情况,剩下好多画面明亮度,清晰度啥的,都是前端调的,反正上面这种做法后端是没啥调的能力。如果都打算这么偷懒了,就别浪费时间瞎找了,直接甩给前端ok了。

拓展问题

刚刚我也说了,这是最简单的一种方法,也是偷懒的方法,甚至最开始的流程也是最简流程。其实可以做的还很多。
就比如我之前说的美颜,微调,清晰度啥的,后端其实是可以做的,但是那就相当于前端推流给后端,后端解析成画面,处理,然后再编码成流,等着别人拉。
这个过程应该比较好理解,多了几个步骤,肯定是性能不如现在。而且需要的难度比较高,虽然有现有的框架但是也没现在一行代码不写方便啊!
顺便推荐三个后端处理的框架:

  • 大而全的Kurento

  • 务实主义的Licode

  • 小而美的Mediasoup

因为我也没用过,所以只是给个提议,有兴趣的自己去看。最后会列上关于这三个框架的详细介绍的帖子地址。

外网问题

额,因为是要用在项目里,所以一直局域网测试肯定是不太合适。所以我是用内网穿透的形式把我自己的工作电脑映射到我们的服务器上了,由此实现了外网直播,外网观看。反正截止到目前为止,四五台电脑四五个手机,4个推流5个拉流是保证延迟在5s以内的。另外因为我是内网穿透实现的,所以可能还有一部分时间是这么消耗的,反正是在能承受的范围内。
再多的就没办法了,起码说明小范围的推拉流是完全没问题的。


如图,手机推流给外网,电脑外网拉流

补充下之前说的好多技术贴的链接:
使用ffmpeg将直播流保存到本地
rtmp和rtsp的区别和适用范围
http://javadox.com/org.bytedeco/javacv/0.8/org/bytedeco/javacv/FFmpegFrameGrabber.html
本地直播平台搭建的四种方式
通过MediaRecorder+MediaSource实现H5直播,以及关于WebRTC直播的问题
如何真正让小程序,WebRTC和APP互通连麦直播
HTML5实现视频直播功能思路详解
如何打造自己的WebRTC 服务器

目前为止,我也只不过是调通了功能,实现了部分应该实现的,多推流多拉流还有保存直播视频,然后几乎没遇到什么问题(性能问题不算)。感觉就是比想的要简单,但是也比想的要复杂,因为我做的太简洁了。这个帖子就算是对这几天整理的一个记录,如果以后这块功能在项目中实现了,再遇到什么问题我会追更的。
不得不承认,编写程序的魅力就在于自己从无到有做出了一些看起来很神奇的功能。我热爱我的职业,现在,依然,永远。

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

推荐阅读更多精彩内容

  • 一.创建接口或内部接口 二.在Adapter 1.设置全局变量 2.添加set方法 3.在onBindViewHo...
    我叫杨毅阅读 179评论 0 0
  • 现在的我比以前多了丝忧虑 为了生活 为了自己存在的状态 当然,不是海子那样的抑郁 也不是徐志摩为林徽因那样的冲动 ...
    右右知我心阅读 263评论 1 4
  • 姓名:王丽 组别:第377期六项精进努力二组组员 【日精进打卡第109天】 【知-学习】 背诵《大学》开篇5遍共1...
    天黑黑_e3af阅读 113评论 0 0