? ? ?公司的一個項目二次開發要用到RTSP解碼,對于我這個剛出道的彩筆工程師無疑是巨大的挑戰。。網上教程不算多,但是也不算少。第一步編譯ffmpeg 就卡了好久。。一個星期終于完工 ? 下面我把代碼貼出來吧 ? 每個方法基本都有注釋,有些是自己理解的,有些網上大神博客記載的 ,有錯誤的地方麻煩多見諒見諒,因為自己弄這個弄了很久,深知c是非人類語言=。=
- (id)initWithVideo:(NSString*)moviePath usesTcp:(BOOL)usesTcp{
if(!(self=[superinit]))returnnil;
AVCodec*pCodec;
//注冊編碼器(其實編碼器和解碼器用的注冊函數都是一樣的:avcodec_register())
avcodec_register_all();
//注冊所有容器的格式和codec (能夠自動選擇相應的文件格式和編碼器,只需要調用一次)
av_register_all();
//打開流媒體(或本地文件)的函數是avformat_open_input()其中打開網絡流的話,前面要加上函數avformat_network_init()
avformat_network_init();
/*
ps:在打開一些流媒體的時候可能需要附加一些參數。
如果直接進行打開是不會成功的:
ffplayrtsp://mms.cnr.cn/cnr003?MzE5MTg0IzEjIzI5NjgwOQ==
這時候我們需要指定其傳輸方式為TCP
ffplay -rtsp_transport tcprtsp://mms.cnr.cn/cnr003?MzE5MTg0IzEjIzI5NjgwOQ==
此外還可以附加一些參數
ffplay -rtsp_transport tcp -max_delay 5000000rtsp://mms.cnr.cn/cnr003?MzE5MTg0IzEjIzI5NjgwOQ==
在實際使用ffmpeg編程中,可以通過AVDictionary把參數傳給avformat_open_input()
轉化的代碼: (鍵值)
AVDictionary *avdic=NULL;
char option_key[]="rtsp_transport";
char option_value[]="tcp";
av_dict_set(&avdic,option_key,option_value,0);
char option_key2[]="max_delay";
char option_value2[]="5000000";
av_dict_set(&avdic,option_key2,option_value2,0);
char url[]="rtsp://mms.cnr.cn/cnr003?MzE5MTg0IzEjIzI5NjgwOQ==";
avformat_open_input(&pFormatCtx,url,NULL,&avdic);
*/
// Set the RTSP Options
AVDictionary*opts =0;
//usesTcp外部傳yes則添加參數
if(usesTcp)
av_dict_set(&opts,"rtsp_transport","tcp",0);
/*
ps:函數調用成功之后處理過的AVFormatContext結構體。
file:打開的視音頻流的URL。
fmt:強制指定AVFormatContext中AVInputFormat的。這個參數一般情況下可以設置為NULL,這樣FFmpeg可以自動檢測AVInputFormat。
dictionay:附加的一些選項參數,一般情況下可以設置為NULL。
avformat_open_input(ps, url, fmt, dict);
*/
if(avformat_open_input(&pFormatCtx, [moviePathUTF8String],NULL, &opts) !=0) {
av_log(NULL,AV_LOG_ERROR,"Couldn't open file\n");
NSLog(@"error not open");
gotoinitError;
}
NSLog(@"fileName = %@",moviePath);
//取出包含在文件內的流信息
/*
它其實已經實現了解碼器的查找,解碼器的打開,視音頻幀的讀取,視音頻幀的解碼等工作。換句話說,該函數實際上已經“走通”的解碼的整個流程
1.查找解碼器:find_decoder()
2.打開解碼器:avcodec_open2()
3.讀取完整的一幀壓縮編碼的數據:read_frame_internal()
注:av_read_frame()內部實際上就是調用的read_frame_internal()。
4.解碼一些壓縮編碼數據:try_decode_frame()
ps:
score變量是一個判決AVInputFormat的分數的門限值,如果最后得到的AVInputFormat的分數低于該門限值,就認為沒有找到合適的AVInputFormat。
FFmpeg內部判斷封裝格式的原理實際上是對每種AVInputFormat給出一個分數,滿分是100分,越有可能正確的AVInputFormat給出的分數就越高。最后選擇分數最高的AVInputFormat作為推測結果。
score的值是一個宏定義AVPROBE_SCORE_RETRY,我們可以看一下它的定義:
#define AVPROBE_SCORE_RETRY (AVPROBE_SCORE_MAX/4)
其中AVPROBE_SCORE_MAX是score的最大值,取值是100:
#define AVPROBE_SCORE_MAX100 ///< maximum score
由此我們可以得出score取值是25,即如果推測后得到的最佳AVInputFormat的分值低于25,就認為沒有找到合適的AVInputFormat。
*/
if(avformat_find_stream_info(pFormatCtx,NULL) <0) {//函數正常執行返回值是大于等于0的,這個函數只是檢測文件的頭部,所以接著我們需要檢查在晚間中的流信息
av_log(NULL,AV_LOG_ERROR,"Couldn't find stream information\n");
NSLog(@"error not find");
gotoinitError;
}
//函數為pFormatCtx->streams填充上正確的信息
//dump_format(pFormatCtx,0,argv[1],0);
// Find the first video stream
videoStream=-1;
audioStream=-1;
//區分音頻視屏及其他流(現在pFormatCtx->streams僅僅是一組大小為pFormatCtx->nb_streams的指針,所以讓我們先跳過它直到我們找到一個視頻流。)
for(inti=0; inb_streams; i++) {
NSLog(@"Stream");
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
NSLog(@"found video stream");
videoStream= i;
}elseif(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) {
audioStream= i;
NSLog(@"found audio stream");
}else{
NSLog(@"其他流");
}
}
//兩種流都沒找到返回錯誤
if(videoStream==-1&&audioStream==-1) {
NSLog(@"error not found stream");
gotoinitError;
}
//流中關于編解碼器的信息就是被我們叫做"codec context"(編解碼器上下文)的東西。這里面包含了流中所使用的關于編解碼器的所有信息,現在我們有了一個指向他的指針。但是我們必需要找到真正的編解碼器并且打開它
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
//尋找解碼器
pCodec =avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec ==NULL) {
av_log(NULL,AV_LOG_ERROR,"Unsupported codec!\n");
gotoinitError;
}
//打開解碼器
if(avcodec_open2(pCodecCtx, pCodec,NULL) <0) {
av_log(NULL,AV_LOG_ERROR,"Cannot open video decoder\n");
gotoinitError;
}
/*
ps:有些人可能會從舊的指導中記得有兩個關于這些代碼其它部分:添加CODEC_FLAG_TRUNCATED到pCodecCtx->flags和添加一個hack來粗糙的修正幀率。這兩個修正已經不在存在于ffplay.c中。
因此,我必需假設它們不再必要。我們移除了那些代碼后還有一個需要指出的不同點:pCodecCtx->time_base現在已經保存了幀率的信息。time_base是一個結構體,它里面有一個分子和分母(AVRational)。
我們使用分數的方式來表示幀率是因為很多編解碼器使用非整數的幀率(例如NTSC使用29.97fps)
*/
//表示有音頻輸入
if(audioStream> -1) {
NSLog(@"set up audiodecoder");
[selfsetupAudioDecoder];
}
//給視頻幀分配空間以便存儲解碼后的圖片
pFrame=av_frame_alloc();
//外部可以手動設置視屏寬高
outputWidth=pCodecCtx->width;
self.outputHeight=pCodecCtx->height;
returnself;
initError:
returnnil;
}
//對幀的操作?
- (void)seekTime:(double)seconds{
AVRationaltimeBase =pFormatCtx->streams[videoStream]->time_base;
int64_ttargetFrame = (int64_t)((double)timeBase.den/ timeBase.num* seconds);
avformat_seek_file(pFormatCtx,videoStream, targetFrame, targetFrame, targetFrame,AVSEEK_FLAG_FRAME);
avcodec_flush_buffers(pCodecCtx);
}
//what?
- (double)currentTime{
AVRationaltimeBase =pFormatCtx->streams[videoStream]->time_base;
returnpacket.pts* (double)timeBase.num/ timeBase.den;
}
//需要輸出的格式可以修改(這里是RGB)
- (void)setupScaler
{
// Release old picture and scaler
avpicture_free(&picture);
sws_freeContext(img_convert_ctx);
// Allocate RGB picture
avpicture_alloc(&picture,PIX_FMT_RGB24,outputWidth,outputHeight);
// S轉化(PIX_FMT_UYVY422PIX_FMT_YUV422PPIX_FMT_RGB24)
staticintsws_flags =SWS_FAST_BILINEAR;
img_convert_ctx=sws_getContext(pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt,
outputWidth,
outputHeight,
PIX_FMT_RGB24,
sws_flags,NULL,NULL,NULL);
//解碼后的視頻幀數據保存在pFrame變量中,然后經過swscale函數轉換后,將視頻幀數據保存在pFrameYUV變量中。最后將pFrameYUV中的數據寫入成文件。AVFrame *pFrameYUV
//sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
}
- (void)convertFrameToRGB{
//解碼后的視頻幀數據保存在pFrame變量中,然后經過swscale函數轉換后,將視頻幀數據保存在pFrameYUV變量中。最后將pFrameYUV中的數據寫入成文件。AVFrame *pFrameYUV
//這里是保存在avpicture(AVPicture的結成:AVPicture結構體是AVFrame結構體的子集――AVFrame結構體的開始部分與AVPicture結構體是一樣的)
sws_scale(img_convert_ctx,
(constuint8_t*const*)pFrame->data,
pFrame->linesize,
0,
pCodecCtx->height,
picture.data,
picture.linesize);
}
- (BOOL)stepFrame{
/*
av_read_frame()讀取一個包并且把它保存到AVPacket結構體中。注意我們僅僅申請了一個包的結構體――ffmpeg為我們申請了內部的數據的內存并通過packet.data指針來指向它。
這些數據可以在后面通過av_free_packet()來釋放。函數avcodec_decode_video()把包轉換為幀。然而當解碼一個包的時候,我們可能沒有得到我們需要的關于幀的信息。因此,
當我們得到下一幀的時候,avcodec_decode_video()為我們設置了幀結束標志frameFinished
*/
// AVPacket packet;
intframeFinished=0;
//NSLog(@"TS");
while(!frameFinished &&av_read_frame(pFormatCtx, &packet) >=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
avcodec_decode_video2(pCodecCtx,pFrame, &frameFinished, &packet);
}
if(packet.stream_index==audioStream) {
// NSLog(@"audio stream");
[audioPacketQueueLocklock];
audioPacketQueueSize+=packet.size;
[audioPacketQueueaddObject:[NSMutableDatadataWithBytes:&packetlength:sizeof(packet)]];
[audioPacketQueueLockunlock];
if(!primed) {
primed=YES;
[_audioController_startAudio];
}
if(emptyAudioBuffer) {
[_audioControllerenqueueBuffer:emptyAudioBuffer];
}
}
}
returnframeFinished!=0;
}
//文件打開動作,然后寫入RGB數據。我們一次向文件寫入一行數據
- (void)savePPMPicture:(AVPicture)pict width:(int)width height:(int)height index:(int)iFrame{
NSLog(@"WRITE?");
/*
ps:如果想將轉換后的原始數據存成文件,只需要將pFrameYUV的data指針指向的數據寫入文件就可以了。
1.保存YUV420P格式的數據
fwrite(pFrameYUV->data[0],(pCodecCtx->width)*(pCodecCtx->height),1,output);
fwrite(pFrameYUV->data[1],(pCodecCtx->width)*(pCodecCtx->height)/4,1,output);
fwrite(pFrameYUV->data[2],(pCodecCtx->width)*(pCodecCtx->height)/4,1,output);
2.保存RGB24格式的數據
fwrite(pFrameYUV->data[0],(pCodecCtx->width)*(pCodecCtx->height)*3,1,output);
3.保存UYVY格式的數據
fwrite(pFrameYUV->data[0],(pCodecCtx->width)*(pCodecCtx->height),2,output);
*/
FILE*pFile;
NSString*fileName;
inty;
fileName = [UtilitiesdocumentsPath:[NSStringstringWithFormat:@"image%04d.ppm",iFrame]];
// Open file
NSLog(@"write image file: %@",fileName);
//打開輸入文件
pFile =fopen([fileNamecStringUsingEncoding:NSASCIIStringEncoding],"wb");
if(pFile ==NULL) {
return;
}
// Write header
fprintf(pFile,"P6\n%d %d\n255\n", width, height);
// Write pixel data
for(y=0; y
fwrite(pict.data[0]+y*pict.linesize[0],1, width*3, pFile);
}
// Close file
fclose(pFile);
}
源碼鏈接https://pan.baidu.com/s/1jINgjI2 (如果百度云服務器太渣,留郵箱我發給你們) ? ?audioStream沒有實現,因為目前的需求只是完成視頻流的解析,有興趣的朋友自己去完善吧,相信它會給剛剛接觸到ffmpeg的朋友帶來很大幫助