源碼地址
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
這個博主對於推流真的熟練,大家如果對推流還想輸入瞭解可以看看他的博客