天天看點

android全平台基于ffmpeg解碼本地MP4視訊推流到RTMP伺服器

音視訊實踐學習

  • android全平台編譯ffmpeg以及x264與fdk-aac實踐
  • ubuntu下使用nginx和nginx-rtmp-module配置直播推流伺服器
  • android全平台編譯ffmpeg合并為單個庫實踐
  • android-studio使用cmake編譯ffmpeg實踐
  • android全平台下基于ffmpeg解碼MP4視訊檔案為YUV檔案
  • android全平台編譯ffmpeg支援指令行實踐
  • android全平台基于ffmpeg解碼本地MP4視訊推流到RTMP伺服器
  • android平台下音頻編碼之編譯LAME庫轉碼PCM為MP3
  • ubuntu平台下編譯vlc-android視訊播放器實踐
  • 圖解YU12、I420、YV12、NV12、NV21、YUV420P、YUV420SP、YUV422P、YUV444P的差別
  • 圖解RGB565、RGB555、RGB16、RGB24、RGB32、ARGB32等格式的差別
  • YUV420P、YUV420SP、NV12、NV21和RGB互相轉換并存儲為JPEG以及PNG圖檔
  • android全平台編譯libyuv庫實作YUV和RGB的轉換

概述

整個音視訊設計到的子產品是很龐大的,之前按照

雷神

的部落格,操作了推流,也成功了,但是沒有深入了解這個過程,最近一段時間看了下很多相關的部落格,對這個過程有了一點了解,現在重新整理一下這個部落格内容,先從最基本的視訊推流開始,我們在電腦上使用

ffmpeg

完成對

視訊檔案

推流,很簡單,直接使用

ffmpeg

的推流指令即可。

ffmpeg -re -i input.mp4 -vcodec copy -f flv rtmp://192.168.1.102:1935/onzhou/live
           

下面會結合一個完整的

推流

執行個體,來記錄一下這個過程

推流流程分析

如果你和我一樣,剛開始接觸音視訊的相關處理,雲裡霧裡很正常,建議多去看看

雷神的部落格

,再加上自己的

了解和實踐

,會更深刻一些

android全平台基于ffmpeg解碼本地MP4視訊推流到RTMP伺服器

不太喜歡直接拿别人的原文,無論如何自己都要操作一遍,上圖是筆者結合新版本的

ffmpeg-3.3.8版本

中部分

API

,重新整理的一個流程圖

//注冊FFmpeg所有編解碼器。

av_register_all()

//初始化網絡元件。

avformat_network_init()

//打開一個輸入流。

avformat_open_input()

//擷取媒體的資訊。

avformat_find_stream_info()

//推薦API用來代替

avcodec_copy_context()

avcodec_parameters_to_context()

//輸出RTMP

avformat_alloc_output_context2()

//申請AVCodecContext空間

avcodec_alloc_context3()

//初始化一個視音頻編解碼器的AVCodecContext

avcodec_open2()

配置環境

作業系統:

ubuntu 16.05

ndk版本:

android-ndk-r16b版本

ffmpeg版本:

ffmpeg-3.3.8

(使用

android-ndk-r10e版本

編譯)

android全平台基于ffmpeg解碼本地MP4視訊推流到RTMP伺服器

工程實踐

建立個子工程:

ffmpeg-stream-mp4

配置CMakeLists.txt檔案和build.gradle檔案比較簡單,不多贅述

  • 定義好

    java層的類檔案

    :主要傳遞

    MP4視訊檔案的路徑到native層中處理,第二個參數是目标流位址

package com.onzhou.ffmpeg.streamer;

public class NativeStreamer {

    static {
        System.loadLibrary("native-stream");
    }

    public native int startPublish(String mp4Path, String stream);

    public native void stopPublish();

}
           

與之對于的

native層的類實作:

AVOutputFormat *ofmt = NULL;
AVCodecContext *codec_ctx = NULL;
AVFormatContext *in_fmt = NULL, *out_fmt = NULL;
AVPacket avPacket;

//退出标記
int exit_flag = 1;

int start_publish(const char *mp4Path, const char *stream) {
    //記錄幀下标
    int frame_index = 0;
    //退出标記
    exit_flag = 1;
    //1.注冊所有元件
    av_register_all();
    //2.初始化網絡
    avformat_network_init();
    //3.打開檔案輸入
    if (avformat_open_input(&in_fmt, mp4Path, 0, 0) < 0) {
        LOGE("Could not open input file.");
        goto end_line;
    }
    //4.查找相關流資訊
    if (avformat_find_stream_info(in_fmt, 0) < 0) {
        LOGE("Failed to retrieve input stream information");
        goto end_line;
    }
    //周遊視訊軌
    int videoIndex = -1;
    for (int index = 0; index < in_fmt->nb_streams; index++)
        if (in_fmt->streams[index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoIndex = index;
            break;
        }
    //5.初始化輸出碼流的AVFormatContext
    avformat_alloc_output_context2(&out_fmt, NULL, "flv", stream); //RTMP
    if (!out_fmt) {
        LOGE("Could not create output context");
        goto end_line;
    }
    ofmt = out_fmt->oformat;
    for (int index = 0; index < in_fmt->nb_streams; index++) {
        //6. 根據輸入流建立一個輸出流
        AVStream *in_stream = in_fmt->streams[index];
        codec_ctx = avcodec_alloc_context3(NULL);
        avcodec_parameters_to_context(codec_ctx, in_stream->codecpar);
        AVStream *out_stream = avformat_new_stream(out_fmt, codec_ctx->codec);
        if (!out_stream) {
            LOGE("Failed allocating output stream");
            goto end_line;
        }
        codec_ctx->codec_tag = 0;
        if (out_fmt->oformat->flags & AVFMT_GLOBALHEADER) {
            codec_ctx->flags |= CODEC_FLAG_GLOBAL_HEADER;
        }
        if (avcodec_parameters_from_context(out_stream->codecpar, codec_ctx) < 0) {
            goto end_line;
        }
    }
    //7.打開網絡輸出流
    if (!(ofmt->flags & AVFMT_NOFILE)) {
        if (avio_open(&out_fmt->pb, stream, AVIO_FLAG_WRITE) < 0) {
            LOGE("Could not open output URL '%s'", stream);
            goto end_line;
        }
    }
    //8.寫檔案頭部
    if (avformat_write_header(out_fmt, NULL) < 0) {
        LOGE("Error occurred when opening output URL");
        goto end_line;
    }

    AVStream *in_stream = NULL, *out_stream = NULL;
    //記錄開始時間
    int64_t start_time = av_gettime();
    //讀取幀資料AVPacket
    while (exit_flag && av_read_frame(in_fmt, &avPacket) >= 0) {
        if (avPacket.stream_index == videoIndex) {
            //時間基
            AVRational time_base = in_fmt->streams[videoIndex]->time_base;
            AVRational time_base_q = {1, AV_TIME_BASE};
            int64_t pts_time = av_rescale_q(avPacket.dts, time_base, time_base_q);
            int64_t now_time = av_gettime() - start_time;
            if (pts_time > now_time) {
                av_usleep(pts_time - now_time);
            }
        }
        in_stream = in_fmt->streams[avPacket.stream_index];
        out_stream = out_fmt->streams[avPacket.stream_index];

        //PTS主要用于度量解碼後的視訊幀什麼時候被顯示出來
        avPacket.pts = av_rescale_q_rnd(avPacket.pts, in_stream->time_base, out_stream->time_base,
                                        AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
        //DTS主要是辨別讀入記憶體中的位元組流在什麼時候開始送入解碼器中進行解碼
        avPacket.dts = av_rescale_q_rnd(avPacket.dts, in_stream->time_base, out_stream->time_base,
                                        AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
        avPacket.duration = av_rescale_q(avPacket.duration, in_stream->time_base,
                                         out_stream->time_base);
        avPacket.pos = -1;

        if (avPacket.stream_index == videoIndex) {
            LOGI("Send %8d video frames to output URL", frame_index);
            frame_index++;
        }
        if (av_interleaved_write_frame(out_fmt, &avPacket) < 0) {
            LOGE("Error write frame");
            break;
        }
        av_packet_unref(&avPacket);
    }
    //9.收尾工作
    av_write_trailer(out_fmt);

    end_line:

    //10.關閉
    avformat_close_input(&in_fmt);
    if (out_fmt && !(ofmt->flags & AVFMT_NOFILE)) {
        avio_close(out_fmt->pb);
    }
    avformat_free_context(out_fmt);
    return 0;
}

/**
 * 停止推流
 */
void stop_publish() {
    exit_flag = 0;
}
           

停止推流的函數比較簡單,直接标記

exit_flag=0

,推流伺服器的搭建,可以參考之前的文章ubuntu下使用nginx和nginx-rtmp-module配置直播推流伺服器

最後是應用層的調用

public void onStartClick(View view) {
  mBtnStartPublish.setEnabled(false);
  mBtnStopPublish.setEnabled(true);
  if (nowStreamer == null) {
      nowStreamer = new NativeStreamer();
  }
  if (publishDisposable == null) {
      publishDisposable = Schedulers.newThread().scheduleDirect(new Runnable() {
          @Override
          public void run() {
              final File inputVideo = new File(getExternalFilesDir(null), "input.mp4");
              nowStreamer.startPublish(inputVideo.getAbsolutePath(), PUBLISH_ADDRESS);
          }
      });
  }
}
           

運作應用,開始推流

android全平台基于ffmpeg解碼本地MP4視訊推流到RTMP伺服器

```

我們在區域網路中使用

vlc播放器

,打開網絡串流

rtmp://192.168.1.102:1935/onzhou/live

android全平台基于ffmpeg解碼本地MP4視訊推流到RTMP伺服器

PTS/DTS問題

PTS

:主要用于度量解碼後的視訊幀什麼時候被顯示出來

DTS

:主要是辨別讀入記憶體中的位元組流在什麼時候開始送入解碼器中進行解碼

通常談論到

PTS和DTS

的時候,一般都是跟

time_base相關聯的

time_base

使用來度量時間概念的,如果把1秒分為25等份,你可以了解就是一把尺,那麼每一格表示的就是1/25秒。此時的

time_base={1,25}

如果你是把1秒分成90000份,每一個刻度就是1/90000秒,此時的

time_base={1,90000}

time_base

表示的就是每個刻度是多少秒

注意:

正常情況下,一個視訊檔案都會有幀率資訊,這個幀率影響畫面流暢度(你可以了解為機關時間内出現的視訊畫面),那麼我們在發送資料的時候就需要控制資料的發送間隔,過快和過慢都會導緻畫面顯示不正常,計算

PTS

DTS

間隔時間

項目位址:

ffmpeg-stream-mp4

https://github.com/byhook/ffmpeg4android

參考:

https://blog.csdn.net/leixiaohua1020/article/details/47056051

https://blog.csdn.net/leixiaohua1020/article/details/39803457

https://blog.csdn.net/bixinwei22/article/details/78770090