Android 音視頻深入 十五 FFmpeg 推流mp4文件(附源碼下載)

源碼地址
https://github.com/979451341/Rtmp

1.配置RTMP服務器

這個我不多說貼兩個博客分別是在mac和windows環境上的,大家跟着弄
MAC搭建RTMP服務器
https://www.jianshu.com/p/6fcec3b9d644
這個是在windows上的,RTMP服務器搭建(crtmpserver和nginx)

https://www.jianshu.com/p/c71cc39f72ec
2.關於推流輸出的ip地址我好好說說

我這裏是手機開啓熱點,電腦連接手機,這個RTMP服務器的推流地址有localhost,服務器在電腦上,對於電腦這個localhost是127.0.0.1,但是對於外界比如手機,你不能用localhost,而是用這個電腦的在這個熱點也就是局域網的ip地址,不是127.0.0.1這個只代表本設備節點的ip地址,這個你需要去手機設置——》更多——》移動網絡共享——》便攜式WLAN熱點——》管理設備列表,就可以看到電腦的局域網ip地址了

3.說說代碼

註冊組件,第二個如果不加的話就不能獲取網絡信息,比如類似url

av_register_all();
avformat_network_init();

獲取輸入視頻的信息,和創建輸出url地址的環境

    av_dump_format(ictx, 0, inUrl, 0);
    ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
    if (ret < 0) {
        avError(ret);
        throw ret;
    }

將輸入視頻流放入剛纔創建的輸出流裏

    for (i = 0; i < ictx->nb_streams; i++) {

        //獲取輸入視頻流
        AVStream *in_stream = ictx->streams[i];
        //爲輸出上下文添加音視頻流(初始化一個音視頻流容器)
        AVStream *out_stream = avformat_new_stream(octx, in_stream->codec->codec);
        if (!out_stream) {
            printf("未能成功添加音視頻流\n");
            ret = AVERROR_UNKNOWN;
        }
        if (octx->oformat->flags & AVFMT_GLOBALHEADER) {
            out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
        }
        ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
        if (ret < 0) {
            printf("copy 編解碼器上下文失敗\n");
        }
        out_stream->codecpar->codec_tag = 0;

// out_stream->codec->codec_tag = 0;
}

打開輸出url,並寫入頭部數據

    //打開IO
    ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
    if (ret < 0) {
        avError(ret);
        throw ret;
    }
    logd("avio_open success!");
    //寫入頭部信息
    ret = avformat_write_header(octx, 0);
    if (ret < 0) {
        avError(ret);
        throw ret;
    }

然後開始循環解碼並推流數據

首先獲取一幀的數據

ret = av_read_frame(ictx, &pkt);

然後給這一幀的數據配置參數,如果原有配置沒有時間就配置時間,我在這裏再提兩個概念

DTS(解碼時間戳)和PTS(顯示時間戳)分別是解碼器進行解碼和顯示幀時相對於SCR(系統參考)的時間戳。SCR可以理解爲解碼器應該開始從磁盤讀取數據時的時間。

        if (pkt.pts == AV_NOPTS_VALUE) {
            //AVRational time_base:時基。通過該值可以把PTS,DTS轉化爲真正的時間。
            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);
        }

調節播放時間,就是當初我們解碼視頻之前記錄了一個當前時間,然後在循環推流的時候又獲取一次當前時間,兩者的差值是我們視頻應該播放的時間,如果視頻播放太快就進程休眠 pkt.dts減去實際播放的時間的差值

        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;
            cout << avr.num << " " << avr.den << "  " << pkt.dts << "  " << pkt.pts << "   "
                 << pts_time << endl;
            if (pts_time > now_time) {
                //睡眠一段時間(目的是讓當前視頻記錄的播放時間與實際時間同步)
                av_usleep((unsigned int) (pts_time - now_time));
            }
        }

如果延時了,這一幀的配置所記錄的時間就應該改變

        //計算延時後,重新指定時間戳
        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));
        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));
        pkt.duration = (int) av_rescale_q(pkt.duration, in_stream->time_base,
                                          out_stream->time_base);

回調這一幀的時間參數,這裏在MainActivity裏實例化了接口,顯示播放時間

    int res = FFmpegHandle.setCallback(new PushCallback() {
        @Override
        public void videoCallback(final long pts, final long dts, final long duration, final long index) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if(pts == -1){
                        tvPushInfo.setText("播放結束");
                        return ;
                    }
                    tvPushInfo.setText("播放時間:"+dts/1000+"秒");
                }
            });
        }
    });

然後段代碼調用了c語言的setCallback函數,獲取了接口的實例,和接口的videoCallback函數引用,這裏還調用了一次這個函數初始化時間顯示

//轉換爲全局變量
pushCallback = env->NewGlobalRef(pushCallback1);
if (pushCallback == NULL) {
    return -3;
}
cls = env->GetObjectClass(pushCallback);
if (cls == NULL) {
    return -1;
}
mid = env->GetMethodID(cls, "videoCallback", "(JJJJ)V");
if (mid == NULL) {
    return -2;
}
env->CallVoidMethod(pushCallback, mid, (jlong) 0, (jlong) 0, (jlong) 0, (jlong) 0);

這個時候我們回到循環推流一幀幀數據的時候調用videoCallback函數

env->CallVoidMethod(pushCallback, mid, (jlong) pts, (jlong) dts, (jlong) duration,
                    (jlong) index);

然後就是向輸出url輸出數據,並釋放這一幀的數據

        ret = av_interleaved_write_frame(octx, &pkt);
        av_packet_unref(&pkt);

釋放資源

//關閉輸出上下文,這個很關鍵。
if (octx != NULL)
    avio_close(octx->pb);
//釋放輸出封裝上下文
if (octx != NULL)
    avformat_free_context(octx);
//關閉輸入上下文
if (ictx != NULL)
    avformat_close_input(&ictx);
octx = NULL;
ictx = NULL;
env->ReleaseStringUTFChars(path_, path);
env->ReleaseStringUTFChars(outUrl_, outUrl);

最後回調時間顯示,說播放結束

callback(env, -1, -1, -1, -1);

4.關於接收推流數據

我這裏使用的是VLC,這個mac和windows都有版本,FILE——》OPEN NETWORK,輸入之前的輸出url就可以了。這裏要注意首先在app上開啓推流再使用VLC打開url纔可以

效果如下

參考文章

https://www.jianshu.com/p/dcac5da8f1da

這個博主對於推流真的熟練,大家如果對推流還想輸入瞭解可以看看他的博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章