家里有个吃灰的树莓派,是为背景。

背景

偶然看到关于树莓派的玩法,发现了知乎树莓派玩法,简单来说就是利用ffmpeg把离线的视频推流到B站进行直播。直播的原理还是很简单的, 只需要把视频一个packet一个packet发送到直播服务器就行了。具体命令:
ffmpeg -re -i "1.mp4" -vcodec copy -acodec copy -f flv "你的rtmp地址/你的直播码" 如果你用的是ubuntu或者mac,只需要apt或brew即可快速安装ffmpeg。(windows也很快,只是需要手动安装)然后去B站就可以申请开播啦, 快去做主播把。这里有一个限制:如果播放的文件的视频编码格式不是h264(可自行百度容器和编码的区别),则需要指定**-vcodec libx264**。当然由于树莓派性能羸弱,最好可以在推流之前将文件先转换为 h264的编码格式,在推流时转码可能达不到实时的要求。

该命令存在问题就是不能无缝推流多个文件,在尝试了
Linux使用命令行调用ffmpeg尝试在b站无缝推流
后, 发现还是有点问题,另外还偶尔会报错: av_interleaved_write_frame(): end of file。谷歌很久也没发现解决方案,只有一个博客说到网络环境不好的时候出这个问题,换到云上执行就不出问题啦…可怜树莓派只有家庭宽带, 不能上云。

感觉这个错误也很像网络问题,就像往服务器写文件,但是网络波动导致写不进去所以就end of file了。

人生苦短,我用python,可怜python调ffmpeg的包都是把ffmpeg的命令包装了一下,也没搜到合适的ffmpeg推流源码仓库, 突发奇想自己实现一下。

目标

  1. 推流多个视频文件到B站直播, 中间不黑屏,流畅!
  2. 可以稳定运行, 避免出现av_interleaved_write_frame错误

实现

C++基础
  1. cmake
    具体也很简单 cmake就是类似于pip install 的功能?
    这里贴一下本项目CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(CXXDEMO)

set(CMAKE_CXX_STANDARD 14)

find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
        libavformat
        libavcodec
        libavutil
        )

add_executable(CXXDEMO main.cpp)

target_link_libraries(${PROJECT_NAME}
        PkgConfig::LIBAV
        )
  1. 推流功能
  • 读取视频文件名
  • 初始化推流的环境, 一个文件结束后直接推下一个视频文件的第一个packet
  • 遇到网络波动出现错误的时候直接 continue
  • 添加参数解析工具
    备注(由于多个视频用的一个推流环境, 需要多个视频的编码参数一致,最好提前转码设置好)

代码如下

#include <iostream>
#include <fstream>
#include <unistd.h>
#include "include/clipp.h"
#include <set>

extern "C"
{
#include "libavformat/avformat.h"
#include "libavutil/time.h"
#include <libavutil/opt.h>

}

using namespace std;
using namespace clipp; using std::cout; using std::string;
#pragma comment(lib, "avformat.lib")
#pragma comment(lib, "avutil.lib")
#pragma comment(lib, "avcodec.lib")

int avError(int errNum);

int push(char inUrls[][300], char *outUrl, bool debug);

int max(int a, int b);

int main(int argc, char *argv[]) {
    std::string input_mp4_file_s = "/Users/leeberli/Downloads/playlist.txt";
    std::string outUrl_s = "rtmp://localhost/live/livestream";
    bool debug = false;

    // 解析参数
    auto cli = (value("mp4 files", input_mp4_file_s),
            value("rtmp url", outUrl_s),
            option("-d", "--debug").set(debug));
    parse(argc, const_cast<char **>(argv), cli);

    char input_mp4_file[input_mp4_file_s.length() + 1];
    char outUrl[outUrl_s.length() + 1];
    strcpy(input_mp4_file, input_mp4_file_s.c_str());
    strcpy(outUrl, outUrl_s.c_str());

    // 解析视频列表文件
    int counter = 0;
    char inUrls[1000][300];
    ifstream inFile(input_mp4_file, ifstream::in);
    if (inFile.good()) {
        while (!inFile.eof() && (counter < 1000)) {
            inFile.getline(inUrls[counter], 300);
            counter++;
        }
    }

    push(inUrls, outUrl, debug);

    return 0;
}

int push(char inUrls[][300], char *outUrl, bool debug) {


    av_register_all();
    avformat_network_init();
    AVFormatContext *octx = nullptr;

    bool first = true;
    int his_pts = 0, his_dts = 0;
    int pkt_pts = 0, pkt_dts = 0;
    int retry_send = 0;
    int retry_send_file = 0;
    std::set<string> error_urls{};

    char *inUrl;
    string inUrl_s;
    for (int ii = 0; ii < 1000; ii++) {
        inUrl = inUrls[ii];
        inUrl_s = inUrl;
        if (error_urls.find(inUrl_s) != error_urls.end()) {
            cout << inUrl << " pass" << endl;
            break;
        }

        // 构建输入context 和 steams
        AVFormatContext *ictx = nullptr;
        int ret = avformat_open_input(&ictx, inUrl, NULL, NULL);
        if (ret < 0) { return avError(ret); }
        if (debug) { cout << "avformat_open_input success!" << endl; }
        ret = avformat_find_stream_info(ictx, 0);
        if (ret != 0) { return avError(ret); }
        if (debug) { av_dump_format(ictx, 0, inUrl, 0); }
        cout << inUrl << " start" << endl;

        // 构建输出context 和 steams
        if (first) {
            ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
            if (ret < 0) { return avError(ret); }
            if (debug) { cout << "avformat_alloc_output_context2 success!" << endl; }
        }
        for (int i = 0; i < ictx->nb_streams; i++) {
            AVStream *in_stream = ictx->streams[i];
            if (first) {
                AVStream *out_stream = avformat_new_stream(octx, in_stream->codec->codec);
                if (!out_stream) {
                    printf("Failed to add audio and video stream \n");
                    ret = AVERROR_UNKNOWN;
                }
                ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
                if (ret < 0) {
                    printf("copy codec context failed \n");
                }
                out_stream->codecpar->codec_tag = 0;
                out_stream->codec->codec_tag = 0;
                if (octx->oformat->flags & AVFMT_GLOBALHEADER) {
                    out_stream->codec->flags = out_stream->codec->flags | 0;
                }
            }
        }
        if (debug) { av_dump_format(octx, 0, outUrl, 1); }
        int videoindex = -1;
        for (int i = 0; i < ictx->nb_streams; i++) {
            if (ictx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
                videoindex = i;
                break;
            }
        }

        // 打开远程文件,设置header
        if (first) {
            ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
            if (ret < 0) { avError(ret); }
            ret = avformat_write_header(octx, 0);
            if (ret < 0) { avError(ret); }
            if (debug) { cout << "avformat_write_header Success!" << endl; }
        }

        // packet 循环
        AVPacket pkt;
        long long start_time = av_gettime();
        long long frame_index = 0;
        while (1) {
            AVStream *in_stream, *out_stream;
            // 读取packet
            ret = av_read_frame(ictx, &pkt);
            if (ret < 0) {
                cout << inUrl << " done" << endl;
                break;
            }

            if (pkt.pts == AV_NOPTS_VALUE) {
                if (debug) { cout << "Get pre-decode data AV_NOPTS_VALUE!" << endl; }
                //AVRational time_base: time base. This value can be used to convert PTS and DTS into real time.
                AVRational time_base1 = ictx->streams[videoindex]->time_base;
                int64_t calc_duration = (double) AV_TIME_BASE / av_q2d(ictx->streams[videoindex]->r_frame_rate);
                pkt.pts = (double) (frame_index * calc_duration) / (double) (av_q2d(time_base1) * AV_TIME_BASE);
                pkt.dts = pkt.pts;
                pkt.duration = (double) calc_duration / (double) (av_q2d(time_base1) * AV_TIME_BASE);
            }
            // 推的太快了 等待
            if (pkt.stream_index == videoindex) {
                AVRational time_base = ictx->streams[videoindex]->time_base;
                AVRational time_base_q = {1, AV_TIME_BASE};
                int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
                int64_t now_time = av_gettime() - start_time;
                AVRational avr = ictx->streams[videoindex]->time_base;
                if (pts_time > now_time) { av_usleep((unsigned int) (pts_time - now_time)); }
            }

            // copy 输入输出流
            in_stream = ictx->streams[pkt.stream_index];
            out_stream = octx->streams[pkt.stream_index];
            pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
                                       (AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX))
                      + his_pts;
            pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
                                       (AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX))
                      + his_dts;
            pkt.duration = (int) av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
            pkt.pos = -1;
            if (pkt.pts < his_pts) { continue; } // 跳过文件切换的时候前几帧
            pkt_pts = max(pkt.pts, pkt_pts);
            pkt_dts = max(pkt.dts, pkt_dts);
            if (pkt.stream_index == videoindex) { frame_index++; }

            // 实际写 在网络不好时直接跳过
            ret = av_interleaved_write_frame(octx, &pkt);
            if (ret < 0) {
                if (debug) { printf("send packet error "); }
                av_usleep(300000); //等待300ms
                retry_send += 1;
                if (retry_send > 20) { // 重试20次后退出该视频
                    error_urls.insert(inUrl_s);
                    cout << endl << inUrl << " !!! may be wrong !!!" << endl;
                    retry_send = 0;
                    retry_send_file += 1;
                    break;
                }
                if (retry_send_file > 10) { // 重试10次后退出该视频
                    cout << endl << " retry toomany times, exit !!!" << endl;
                    exit(1);
                }
            }
            av_packet_unref(&pkt);
        }

        avformat_free_context(ictx);
        first = false;
        his_pts = pkt_pts;
        his_dts = pkt_dts;
    }
}

int avError(int errNum) {
    char buf[1024];
    av_strerror(errNum, buf, sizeof(buf));
    cout << " failed! " << buf << endl;
    return -1;
}

int max(int a, int b) {
    if (a > b) return a;
    return b;
}

源码

https://github.com/lieberman94/ffmpeg-streaming-rtmp