介紹:IOS下的RTMP播放器真心不多,好用的更少。本人找了一圈最后還是在GITHUB里使用了
ijkplayer 播放器。
簡(jiǎn)單介紹一下蹂随,ijkplayer 是基于ffmpeg 上封裝了一層的播放器支持IOS,android,寫這篇文章的時(shí)候ios 方面github上介紹還不多因惭,我這里說(shuō)下怎么來(lái)播放RTMP的視頻岳锁。
1、如何播放RTMP視頻
找到 JKDemoInputURLViewController.m 修改代碼如下
- (void)onClickPlayButton {
NSURL*url = [NSURLURLWithString:self.textView.text];
NSString*scheme = [[urlscheme]lowercaseString];
//if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"]) {
[IJKVideoViewControllerpresentFromViewController:selfwithTitle:[NSStringstringWithFormat:@"URL: %@", url]URL:urlcompletion:^{
//[self.navigationController popViewControllerAnimated:NO];
}];
//}
}
好了 你直接可以在輸入U(xiǎn)RL模式中輸入你的RTMP地址就好了
- 如何降低延遲的問(wèn)題
服務(wù)器:本人的流媒體服務(wù)器是自建的蹦魔,使用的是云主機(jī)激率,5MB帶寬2CPU 1.0GHz 2G內(nèi)存屬于比較爛配置,用的red5 1.0.7 最新的版本勿决,具體怎么安裝請(qǐng)查看Red5 GITHUB內(nèi)的WIKI部分即可乒躺,推流端:IOS 用的 LFLiveKit 可以自行GITHUB 內(nèi)搜索即可。
延遲主要存在于推流端和播放器端 LFLiveKit 不存在緩沖區(qū)沒(méi)什么好配置的低缩,如果使用OBS 推流請(qǐng)?jiān)?輸出-->高級(jí)-->緩沖大小填0即可嘉冒,
Red5 去conf 文件夾下里修改red5.properties 文件
rtmp.send_buffer_size=200
rtmp.receive_buffer_size=200
服務(wù)端就好了沒(méi)什么別的,重啟red5 服務(wù)即可
ijkplayer播放器默認(rèn)是帶緩沖的所以我們主要修改播放器部分的緩沖區(qū),原理就是去掉作者封裝的緩沖區(qū)那一層咆繁,激活ffplay 播放器的無(wú)緩沖標(biāo)志讳推。我們來(lái)操作吧
首頁(yè)我們?cè)?公共頭文件IJKMediaPlayer-Prefix.pch里增加一個(gè)宏定義
#define USE_IJK_BUFERING 0
在 IJKFFOptions.m 里修改
+ (IJKFFOptions*)optionsByDefault
{
IJKFFOptions*options = [[IJKFFOptionsalloc]init];
[optionssetPlayerOptionIntValue:30forKey:@"max-fps"];
[optionssetPlayerOptionIntValue:0forKey:@"framedrop"];
[optionssetPlayerOptionIntValue:3forKey:@"video-pictq-size"];
[optionssetPlayerOptionIntValue:0forKey:@"videotoolbox"];
[optionssetPlayerOptionIntValue:960forKey:@"videotoolbox-max-frame-width"];
[optionssetFormatOptionIntValue:0forKey:@"auto_convert"];
[optionssetFormatOptionIntValue:1forKey:@"reconnect"];
[optionssetFormatOptionIntValue:30*1000*1000forKey:@"timeout"];
[optionssetFormatOptionValue:@"ijkplayer"forKey:@"user-agent"];
[optionssetFormatOptionValue:@"2000000"forKey:@"analyzeduration"];//增加的
[optionssetFormatOptionValue:@"nobuffer"forKey:@"fflags"];//增加的
[optionssetFormatOptionValue:@"4096"forKey:@"probsize"]; //增加的
options.showHudView=NO;
returnoptions;
}
在 ff_ffplay_def.h 文件里修改
#ifdef USE_IJK_BUFERING
#define DEFAULT_HIGH_WATER_MARK_IN_BYTES (256*1024)
/*
* START: buffering after prepared/seeked
* NEXT:buffering for the second time after START
* MAX:...
*/
#define DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS (100)
#define DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS (1*1000)
#define DEFAULT_LAST_HIGH_WATER_MARK_IN_MS (5*1000)
#define BUFFERING_CHECK_PER_BYTES (512)
#define BUFFERING_CHECK_PER_MILLISECONDS (500)
#define MAX_QUEUE_SIZE (15*1024*1024)
#ifdef FFP_MERGE
#define MIN_FRAMES 25
#endif
#define DEFAULT_MIN_FRAMES 50000
#define MIN_MIN_FRAMES 5
#define MAX_MIN_FRAMES 50000
#define MIN_FRAMES (ffp->dcc.min_frames)
#else
#define MAX_QUEUE_SIZE (0)
#define MIN_FRAMES 5
#define MIN_MIN_FRAMES 5
#endif
簡(jiǎn)單說(shuō)明下就是去掉原作者的緩沖,使用我們自己定義的緩沖區(qū)大小 MAX_QUEUE_SIZE 為0 就是不緩沖了(這里的緩沖區(qū)是顯示層的玩般,不是網(wǎng)絡(luò)數(shù)據(jù)的緩沖)
接著ff_ffplay_options.h 文件里肯定要報(bào)錯(cuò)了
//{ "min-frames","minimal frames to stop pre-reading",
//OPTION_OFFSET(dcc.min_frames),OPTION_INT(DEFAULT_MIN_FRAMES, MIN_MIN_FRAMES, MAX_MIN_FRAMES) },
//{ "first-high-water-mark-ms","first chance to wakeup read_thread",
//OPTION_OFFSET(dcc.first_high_water_mark_in_ms),
//OPTION_INT(DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS,
//DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS,
//DEFAULT_LAST_HIGH_WATER_MARK_IN_MS) },
//{ "next-high-water-mark-ms","second chance to wakeup read_thread",
//OPTION_OFFSET(dcc.next_high_water_mark_in_ms),
//OPTION_INT(DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS,
//DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS,
//DEFAULT_LAST_HIGH_WATER_MARK_IN_MS) },
//{ "last-high-water-mark-ms","last chance to wakeup read_thread",
//OPTION_OFFSET(dcc.last_high_water_mark_in_ms),
//OPTION_INT(DEFAULT_LAST_HIGH_WATER_MARK_IN_MS,
//DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS,
//DEFAULT_LAST_HIGH_WATER_MARK_IN_MS) },
注釋掉相關(guān)報(bào)錯(cuò)的屬性即可
繼續(xù)在 ff_ffplay.c 文件里修改
staticintdecoder_decode_frame(FFPlayerffp,Decoderd,AVFrameframe,AVSubtitlesub)函數(shù)內(nèi)修改
do{
if(d->queue->nb_packets==0)
SDL_CondSignal(d->empty_queue_cond);
#ifdef USE_IJK_BUFERING
if(packet_queue_get_or_buffering(ffp, d->queue, &pkt, &d->pkt_serial, &d->finished) <0)
return-1;
#else
if(packet_queue_get(d->queue, &pkt,1, &d->pkt_serial) <0)
return-1;
#endif
if(pkt.data==flush_pkt.data) {
avcodec_flush_buffers(d->avctx);
d->finished=0;
d->next_pts= d->start_pts;
d->next_pts_tb= d->start_pts_tb;
}
}while(pkt.data==flush_pkt.data|| d->queue->serial!= d->pkt_serial);
staticintread_thread(void*arg) 函數(shù)最后修改
if(ffp->packet_buffering) {
#ifdef USE_IJK_BUFERING
io_tick_counter = SDL_GetTickHR();
if(abs((int)(io_tick_counter - prev_io_tick_counter)) > BUFFERING_CHECK_PER_MILLISECONDS) {
prev_io_tick_counter = io_tick_counter;
ffp_check_buffering_l(ffp);
}
#endif
}
最后 ffpipenode_ios_videotoolbox_vdec.m 文件里修改
intdecoder_decode_frame_videotoolbox(VideoToolBoxContext* context)函數(shù)里
do{
if(d->queue->nb_packets==0)
SDL_CondSignal(d->empty_queue_cond);
ffp_video_statistic_l(ffp);
#ifdef USE_IJK_BUFERING
if(ffp_packet_queue_get_or_buffering(ffp, d->queue, &pkt, &d->pkt_serial, &d->finished) <0)
return-1;
#else
if(ffp_packet_queue_get(d->queue, &pkt,1,&d->pkt_serial) <0)
return-1;
#endif
if(ffp_is_flush_packet(&pkt)) {
avcodec_flush_buffers(d->avctx);
context->refresh_request=true;
context->serial+=1;
d->finished=0;
ALOGI("flushed last keyframe pts %lld \n",d->pkt.pts);
d->next_pts= d->start_pts;
d->next_pts_tb= d->start_pts_tb;
}
}while(ffp_is_flush_packet(&pkt) || d->queue->serial!= d->pkt_serial);
好了跑一下银觅,帶寬足夠的情況下延遲應(yīng)該在0.5-1秒左右,基本是實(shí)時(shí)的了
3.ijkplayer 的BUG 解決
ijkplayer 確實(shí)已經(jīng)算很贊的播放器了坏为,畢竟是開(kāi)源的東東究驴,自然多少會(huì)有點(diǎn)BUG慨仿,說(shuō)下使用中發(fā)現(xiàn)的BUG問(wèn)題也節(jié)省各位看官時(shí)間。
現(xiàn)象:觀看一下直播然后點(diǎn)擊DONE按鈕 關(guān)閉后后會(huì)有一個(gè)超時(shí)提示纳胧,DEBUG時(shí)是在IJKSDLAudioQueueController.m 文件里的stop 函數(shù)里的 AudioQueueStop(_audioQueueRef,true); 這句話報(bào)出來(lái)的。查了下蘋果的資料說(shuō)明帘撰,這個(gè)函數(shù)默認(rèn)是和音頻的處理函數(shù)在同一個(gè)線程下執(zhí)行的所以會(huì)卡死報(bào)出超時(shí)警告跑慕。建議是用不同的線程去執(zhí)行這個(gè)函數(shù),并且需要判定kAudioQueueProperty_IsRunning 是否是1 才可以調(diào)用的那么我們就來(lái)解決下吧:
// do not lock AudioQueueStop, or may be run into deadlock
dispatch_queue_tconcurrentQueue =dispatch_queue_create("leon.audio.queue",DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
while(true) {
UInt32askRunning =0;
UInt32runsize =sizeof(askRunning);
AudioQueueGetProperty(_audioQueueRef,kAudioQueueProperty_IsRunning, &askRunning, &runsize);
if(askRunning) {
AudioQueueStop(_audioQueueRef,true);
AudioQueueDispose(_audioQueueRef,true);
_audioQueueRef=nil;
break;
}
}
});
再運(yùn)行下發(fā)現(xiàn)不再出現(xiàn)警告可以順利執(zhí)行,但是接著來(lái)了一個(gè)野指針訪問(wèn)摧找。核行。。程序直接CRASH 了(當(dāng)然不改的話等個(gè)5-10秒這個(gè)CRASH 也會(huì)出現(xiàn))
這個(gè)野指針?lè)治鱿聛?lái)小弟的看法是網(wǎng)絡(luò)數(shù)據(jù)包一次性給了10KB的數(shù)據(jù)蹬耘,audio 處理單元拿下來(lái)處理成了對(duì)應(yīng)的音頻包結(jié)構(gòu)后變成了10個(gè)音頻單元包芝雪,這10個(gè)音頻包是按順序處理的,數(shù)據(jù)源是在FFPlayer結(jié)構(gòu)體里的is分配的 ,當(dāng)我們點(diǎn)擊了DONE按鈕后FFPlayer 對(duì)象里的is 的對(duì)象釋放時(shí)综苔,那10個(gè)音頻包是在不同線程里處理的沒(méi)有處理完比如只處理4個(gè)還有6個(gè)惩系,從而造成了野指針訪問(wèn)導(dǎo)致程序CRASH,解釋了這么多其實(shí)小弟我也頭暈一開(kāi)始想的是如何做好線程間的調(diào)度如筛,想了半天沒(méi)什么思路為了方便就順著正常思路想下去那就是如果我把音頻包都處理完不就沒(méi)這個(gè)問(wèn)題了嘛堡牡,那就來(lái)動(dòng)手吧:
在FFPlayer 結(jié)構(gòu)體里增加一個(gè) int willclose; 屬性(我隨便起的)接著在FFPlayer*ffp_create()函數(shù)加一個(gè)初始化
FFPlayer*ffp_create()
{
av_log(NULL,AV_LOG_INFO,"av_version_info: %s\n",av_version_info());
FFPlayer* ffp = (FFPlayer*)av_mallocz(sizeof(FFPlayer));
if(!ffp)
returnNULL;
msg_queue_init(&ffp->msg_queue);
ffp->af_mutex=SDL_CreateMutex();
ffp->vf_mutex=SDL_CreateMutex();
ffp_reset_internal(ffp);
ffp->av_class= &ffp_context_class;
ffp->meta=ijkmeta_create();
ffp->willclose=10;
av_opt_set_defaults(ffp);
returnffp;
}
在 intffp_wait_stop_l(FFPlayer*ffp) 函數(shù)修改
intffp_wait_stop_l(FFPlayer*ffp)
{
assert(ffp);
if(ffp->is) {
ffp_stop_l(ffp);
ffp->willclose=90;
stream_close(ffp);
ffp->is=NULL;
}
return0;
}
最后 staticvoidsdl_audio_callback(voidopaque,Uint8stream,intlen) 函數(shù)做如下修改
while(len >0) {
if(is->audio_buf_index>= is->audio_buf_size) {
//增加部分(以下)
if(ffp->willclose==90) {
is->audio_buf_index= is->audio_buf_size;
memset(stream,0, len);
SDL_AoutFlushAudio(ffp->aout);
break;
}
//增加部分(以上)
audio_size =audio_decode_frame(ffp);
if(audio_size <0) {
/* if error, just output silence */
is->audio_buf= is->silence_buf;
is->audio_buf_size=sizeof(is->silence_buf) / is->audio_tgt.frame_size* is->audio_tgt.frame_size;
}else{
if(is->show_mode!=SHOW_MODE_VIDEO)
update_sample_display(is, (int16_t*)is->audio_buf, audio_size);
is->audio_buf_size= audio_size;
}
is->audio_buf_index=0;
}
if(is->auddec.pkt_serial!= is->audioq.serial) {
is->audio_buf_index= is->audio_buf_size;
memset(stream,0, len);
// stream += len;
// len = 0;
SDL_AoutFlushAudio(ffp->aout);
break;
}
len1 = is->audio_buf_size- is->audio_buf_index;
if(len1 > len)
len1 = len;
if(!is->muted&& is->audio_volume==SDL_MIX_MAXVOLUME)
memcpy(stream, (uint8_t*)is->audio_buf+ is->audio_buf_index, len1);
else{
memset(stream, is->silence_buf[0], len1);
if(!is->muted)
SDL_MixAudio(stream, (uint8_t*)is->audio_buf+ is->audio_buf_index, len1, is->audio_volume);
}
len -= len1;
stream += len1;
is->audio_buf_index+= len1;
}
運(yùn)行一下,咩嘿嘿順利釋放不再CRASH 了杨刨。就先寫到這里了后面有新的成果再來(lái)記錄