源碼地址https://github.com/979451341/Rtmp 1.配置RTMP伺服器 這個我不多說貼兩個博客分別是在mac和windows環境上的,大家跟著弄 MAC搭建RTMP伺服器https://www.jianshu.com/p/6fcec3b9d644這個是在windows上的 ...
源碼地址
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
這個博主對於推流真的熟練,大家如果對推流還想輸入瞭解可以看看他的博客