公司的一个项目二次开发要用到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的朋友带来很大帮助