====== 深入浅出FFMPEG ======

====== 深入浅出FFMPEG ====== ===== 数字媒体处理的基本流程 ===== ===== 认识FFMPEG ===== FFMPEG堪称自由软件中最完备的一套多媒体支持库,它几乎实现了所有当下常见的数据封装格式、多媒体传输协议以及音视频编解码器。因此,对于从事多媒体技术开发的工程师来说,深入研究FFMPEG成为一门必不可少的工作,可以这样说,FFMPEG之于多媒体开发工程师的重要性正如kernel之于嵌入式系统工程师一般。 几个小知识: * FFMPEG项目是由法国人Fabrice Bellard发起的,此人也是著名的CPU模拟器项目QEMU的发起者,同时还是[[http://bellard.org/pi/pi2700e9/announce.html|圆周率算法纪录]]的保持者。 * FF是Fast Forward的意思,翻译成中文是“快进”。 * [[http://bits.ohloh.net/attachments/2230/ffmpeg-logo-square_med.png|FFMPEG的LOGO]]是一个"Z字扫描"示意图,Z字扫描用于将图像的二维频域数据一维化,同时保证了一维化的数据具备良好的统计特性,从而提高其后要进行的一维熵编码的效率。 ===== 一个简单的测试程序 ===== #include #include #include #include "libavutil/avstring.h" #include "libavformat/avformat.h" #include "libavdevice/avdevice.h" #include "libavcodec/opt.h" #include "libswscale/swscale.h" #define DECODED_AUDIO_BUFFER_SIZE 192000 struct options { int streamId; int frames; int nodec; int64_t lstart; char finput[256]; char foutput1[256]; char foutput2[256]; }; int parse_options(struct options *opts, int argc, char** argv) { int optidx; char *optstr; if (argc < 2) return -1; opts->streamId = -1; opts->lstart = -1; opts->frames = -1; opts->foutput1[0] = 0; opts->foutput2[0] = 0; opts->nodec = 0; strcpy(opts->finput, argv[1]); optidx = 2; while (optidx < argc) { optstr = argv[optidx++]; if (*optstr++ != '-') return -1; switch (*optstr++) { case 's': //< stream id opts->streamId = atoi(optstr); break; case 'f': //< frames opts->frames = atoi(optstr); break; case 'k': //< skipped opts->lstart = atoll(optstr); break; case 'o': //< output strcpy(opts->foutput1, optstr); strcat(opts->foutput1, ".mpg"); strcpy(opts->foutput2, optstr); strcat(opts->foutput2, ".raw"); break; case 'n': //decoding and output options if (strcmp("dec", optstr) == 0) opts->nodec = 1; break; default: return -1; } } return 0; } void show_help(char* program) { printf("Simple FFMPEG test program/n"); printf("Usage: %s inputfile [-sstreamid [-fframes] [-kskipped] [-ooutput_filename(without extension)]]/n", program); return; } static void log_callback(void* ptr, int level, const char* fmt, va_list vl) { vfprintf(stdout, fmt, vl); } #include #include #include #include #define OSS_DEVICE "/dev/dsp0" struct audio_dsp { int audio_fd; int channels; int format; int speed; }; int map_formats(enum SampleFormat format) { switch(format) { case SAMPLE_FMT_U8: return AFMT_U8; case SAMPLE_FMT_S16: return AFMT_S16_LE; default: return AFMT_U8; } } int set_audio(struct audio_dsp* dsp) { if (dsp->audio_fd == -1) { printf("Invalid audio dsp id!/n"); return -1; } if (-1 == ioctl(dsp->audio_fd, SNDCTL_DSP_SETFMT, &dsp->format)) { printf("Failed to set dsp format!/n"); return -1; } if (-1 == ioctl(dsp->audio_fd, SNDCTL_DSP_CHANNELS, &dsp->channels)) { printf("Failed to set dsp format!/n"); return -1; } if (-1 == ioctl(dsp->audio_fd, SNDCTL_DSP_SPEED, &dsp->speed)) { printf("Failed to set dsp format!/n"); return -1; } return 0; } int play_pcm(struct audio_dsp* dsp, unsigned char *buf, int size) { if (dsp->audio_fd == -1) { printf("Invalid audio dsp id!/n"); return -1; } if (-1 == write(dsp->audio_fd, buf, size)) { printf("Failed to write audio dsp!/n"); return -1; } return 0; } #include #include #define FB_DEVICE "/dev/fb0" enum pic_format { eYUV_420_Planer, }; struct video_fb { int video_fd; struct fb_var_screeninfo vinfo; struct fb_fix_screeninfo finfo; unsigned char *fbp; AVFrame *frameRGB; struct { int x; int y; } video_pos; }; int open_video(struct video_fb *fb, int x, int y) { int screensize; fb->video_fd = open(FB_DEVICE, O_WRONLY); if (fb->video_fd == -1) return -1; if (ioctl(fb->video_fd, FBIOGET_FSCREENINFO, &fb->finfo)) return -2; if (ioctl(fb->video_fd, FBIOGET_VSCREENINFO, &fb->finfo)) return -2; printf("video device: resolution %dx%d, �pp/n", fb->vinfo.xres, fb->vinfo.yres, fb->vinfo.bits_per_pixel); screensize = fb->vinfo.xres * fb->vinfo.yres * fb->vinfo.bits_per_pixel / 8; fb->fbp = (unsigned char *) mmap(0, screensize, PROT_READ|PROT_WRITE, MAP_SHARED, fb->video_fd, 0); if (fb->fbp == -1) return -3; if (x >= fb->vinfo.xres || y >= fb->vinfo.yres) { return -4; } else { fb->video_pos.x = x; fb->video_pos.y = y; } fb->frameRGB = avcodec_alloc_frame(); if (!fb->frameRGB) return -5; return 0; } int show_picture(struct video_fb *fb, AVFrame *frame, int width, int height, enum pic_format format) { struct SwsContext *sws; int i; unsigned char *dest; unsigned char *src; if (fb->video_fd == -1) return -1; if ((fb->video_pos.x >= fb->vinfo.xres) || (fb->video_pos.y >= fb->vinfo.yres)) return -2; if (fb->video_pos.x + width > fb->vinfo.xres) { width = fb->vinfo.xres - fb->video_pos.x; } if (fb->video_pos.y + height > fb->vinfo.yres) { height = fb->vinfo.yres - fb->video_pos.y; } if (format == PIX_FMT_YUV420P) { sws = sws_getContext(width, height, format, width, height, PIX_FMT_RGB32, SWS_FAST_BILINEAR, NULL, NULL, NULL); if (sws == 0) { return -3; } if (sws_scale(sws, frame->data, frame->linesize, 0, height, fb->frameRGB->data, fb->frameRGB->linesize)) { return -3; } dest = fb->fbp + (fb->video_pos.x+fb->vinfo.xoffset) * (fb->vinfo.bits_per_pixel/8) +(fb->video_pos.y+fb->vinfo.yoffset) * fb->finfo.line_length; for (i = 0; i < height; i++) { memcpy(dest, src, width*4); src += fb->frameRGB->linesize[0]; dest += fb->finfo.line_length; } } return 0; } void close_video(struct video_fb *fb) { if (fb->video_fd != -1) { munmap(fb->fbp, fb->vinfo.xres * fb->vinfo.yres * fb->vinfo.bits_per_pixel / 8); close(fb->video_fd); fb->video_fd = -1; } } int main(int argc, char **argv) { AVFormatContext* pCtx = 0; AVCodecContext *pCodecCtx = 0; AVCodec *pCodec = 0; AVPacket packet; AVFrame *pFrame = 0; FILE *fpo1 = NULL; FILE *fpo2 = NULL; int nframe; int err; int got_picture; int picwidth, picheight, linesize; unsigned char *pBuf; int16_t *pSndBuf = NULL; int i; int64_t timestamp; struct options opt; int usefo = 0; struct audio_dsp dsp; av_register_all(); av_log_set_callback(log_callback); av_log_set_level(50); if (parse_options(&opt, argc, argv) < 0 || (strlen(opt.finput) == 0)) { show_help(argv[0]); return 0; } err = av_open_input_file(&pCtx, opt.finput, 0, 0, 0); if (err < 0) { printf("/n->(av_open_input_file)/tERROR:/t%d/n", err); goto fail; } err = av_find_stream_info(pCtx); if (err < 0) { printf("/n->(av_find_stream_info)/tERROR:/t%d/n", err); goto fail; } if (opt.streamId < 0) { dump_format(pCtx, 0, pCtx->filename, 0); goto fail; } else { } if (strlen(opt.foutput1) && strlen(opt.foutput2)) { fpo1 = fopen(opt.foutput1, "wb"); fpo2 = fopen(opt.foutput2, "wb"); if (!fpo1 || !fpo2) { printf("/n->error opening output files/n"); goto fail; } usefo = 1; } else { usefo = 0; } if (opt.streamId >= pCtx->nb_streams) { printf("/n->StreamId/tERROR/n"); goto fail; } if (opt.lstart > 0) { err = av_seek_frame(pCtx, opt.streamId, opt.lstart, AVSEEK_FLAG_ANY); if (err < 0) { printf("/n->(av_seek_frame)/tERROR:/t%d/n", err); goto fail; } } pCodecCtx = pCtx->streams[opt.streamId]->codec; pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if (!pCodec) { printf("/n->can not find codec!/n"); goto fail; } err = avcodec_open(pCodecCtx, pCodec); if (err < 0) { printf("/n->(avcodec_open)/tERROR:/t%d/n", err); goto fail; } pSndBuf = (int16_t *) malloc(DECODED_AUDIO_BUFFER_SIZE); if (!pSndBuf) { printf("/n->no memory/n"); goto fail; } pFrame = avcodec_alloc_frame(); if (!opt.nodec) { dsp.audio_fd = open(OSS_DEVICE, O_WRONLY); if (dsp.audio_fd == -1) { printf("/n-> can not open audio device/n"); goto fail; } dsp.channels = pCodecCtx->channels; dsp.speed = pCodecCtx->sample_rate; dsp.format = map_formats(pCodecCtx->sample_fmt); if (set_audio(&dsp) < 0) { printf("/n-> can not set audio device/n"); goto fail; } } nframe = 0; while(nframe < opt.frames || opt.frames == -1) { err = av_read_frame(pCtx, &packet); if (err < 0) { printf("/n->(av_read_frame)/tERROR:/t%d/n", err); break; } timestamp = av_rescale_q(packet.dts, pCtx->streams[packet.stream_index]->time_base, (AVRational){1, AV_TIME_BASE}); printf("/nFrame No ] stream#%d/tsize mB, timestamp:%6lld, dts:%6lld, pts:%6lld, ", nframe++, packet.stream_index, packet.size, timestamp, packet.dts, packet.pts); if (packet.stream_index == opt.streamId) { #if 0 for (i = 0; i < packet.size; i++) { if (i == 0) printf("/n pktdata: "); printf("%2x ", packet.data[i]); } printf("/n"); #endif if (usefo) { fwrite(packet.data, packet.size, 1, fpo1); fflush(fpo1); } if (pCtx->streams[opt.streamId]->codec->codec_type == CODEC_TYPE_VIDEO && !opt.nodec) { picheight = pCtx->streams[opt.streamId]->codec->height; picwidth = pCtx->streams[opt.streamId]->codec->width; avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &packet); if (got_picture) { printf("[Video: type %d]", pFrame->pict_type); if (pCtx->streams[opt.streamId]->codec->pix_fmt == PIX_FMT_YUV420P && usefo) { linesize = pFrame->linesize[0]; pBuf = pFrame->data[0]; for (i = 0; i < picheight; i++) { fwrite(pBuf, picwidth, 1, fpo2); pBuf += linesize; } linesize = pFrame->linesize[1]; pBuf = pFrame->data[1]; for (i = 0; i < picheight/2; i++) { fwrite(pBuf, picwidth/2, 1, fpo2); pBuf += linesize; } linesize = pFrame->linesize[2]; pBuf = pFrame->data[2]; for (i = 0; i < picheight/2; i++) { fwrite(pBuf, picwidth/2, 1, fpo2); pBuf += linesize; } fflush(fpo2); } else { } } av_free_packet(&packet); } else if (pCtx->streams[opt.streamId]->codec->codec_type == CODEC_TYPE_AUDIO && !opt.nodec) { int size = DECODED_AUDIO_BUFFER_SIZE; avcodec_decode_audio3(pCodecCtx, pSndBuf, &size, &packet); printf("[Audio: ] Samples]", size); if (usefo) { fwrite(pSndBuf, size/2, 1, fpo2); fflush(fpo2); } else { play_pcm(&dsp, pSndBuf, size); } } } } fail: if (pSndBuf) { free(pSndBuf); } if (pCtx) { av_close_input_file(pCtx); } if (fpo1) { fclose(fpo1); } if (fpo2) { fclose(fpo2); } if (!pFrame) { av_free(pFrame); } if (!usefo && (dsp.audio_fd != -1)) { close(dsp.audio_fd); } return 0; } 这一小段代码可以实现的功能包括: * 打开一个多媒体文件并获取基本的媒体信息。 * 获取编码器句柄。 * 根据给定的时间标签进行一个跳转。 * 读取数据帧。 * 解码音频帧或者视频帧。 * 关闭多媒体文件。 这些功能足以支持一个功能强大的多媒体播放器,因为最复杂的解复用、解码、数据分析过程已经在FFMpeg内部实现了,需要关注的仅剩同步问题。 ===== 用户接口 ===== ==== 数据结构==== === 基本概念 === 编解码器、数据帧、媒体流和容器是数字媒体处理系统的四个基本概念,它们的关系如下图所示: 首先需要统一术语: * 容器/文件(Conainer/File):即特定格式的多媒体文件。 * 媒体流(Stream):指时间轴上的一段连续数据,如一段声音数据,一段视频数据或一段字幕数据,可以是压缩的,也可以是非压缩的,压缩的数据需要关联特定的编解码器。 * 数据帧/数据包(Frame/Packet):通常,一个媒体流由大量的数据帧组成,对于压缩数据,帧对应着编解码器的最小处理单元。通常,分属于不同媒体流的数据帧交错复用于容器之中,参见[[tech:multimedia:interleave|交错]]。 * 编解码器:编解码器以帧为单位实现压缩数据和原始数据之间的相互转换。 在FFMPEG中,使用AVFormatContext、AVStream、AVPacket、AVCodecContext及AVCodec来抽象这几个基本要素: === AVPacket === AVPacket定义在avcodec.h中,如下: typedef struct AVPacket { int64_t pts; int64_t dts; uint8_t *data; int size; int stream_index; int flags; int duration; void (*destruct)(struct AVPacket *); void *priv; int64_t pos; ///< byte position in stream, -1 if unknown int64_t convergence_duration; } AVPacket; FFMPEG使用AVPacket来暂存解复用之后、解码之前的媒体数据(一个音/视频帧、一个字幕包等)及附加信息(解码时间戳、显示时间戳、时长等)。其中: * dts表示解码时间戳,pts表示显示时间戳,它们的单位是所属媒体流的时间基准。 * stream_index给出所属媒体流的编号; * data为数据缓冲区指针,size为长度; * duration为数据的时长,也是以所属媒体流的时间基准为单位; * pos表示该数据在媒体流中的字节偏移量; * destruct为用于释放数据缓冲区的函数指针; * flags为标志域,其中,最低为置1表示该数据是一个关键帧。 AVPacket结构本身只是个容器,它使用data成员指向实际的数据缓冲区,这个缓冲区可以通过av_new_packet创建,可以通过av_dup_packet拷贝,也可以由FFMPEG的API产生(如av_read_frame),使用之后需要通过调用av_free_packet释放。av_free_packet调用的是结构体本身的destruct函数,它的值有两种情况:1)av_destruct_packet_nofree或0;2)av_destruct_packet,其中,前者仅仅是将data和size的值清0而已,后者才会真正地释放缓冲区。FFMPEG内部使用AVPacket结构建立缓冲区装载数据,同时提供destruct函数,如果FFMPEG打算自己维护缓冲区,则将destruct设为av_destruct_packet_nofree,用户调用av_free_packet清理缓冲区时并不能够将其释放;如果FFMPEG不会再使用该缓冲区,则将destruct设为av_destruct_packet,表示它能够被释放。对于缓冲区不能够被释放的AVPackt,用户在使用之前最好调用av_dup_packet进行缓冲区的克隆,将其转化为缓冲区能够被释放的AVPacket,以免对缓冲区的不当占用造成异常错误。而av_dup_packet会为destruct指针为av_destruct_packet_nofree的AVPacket新建一个缓冲区,然后将原缓冲区的数据拷贝至新缓冲区,置data的值为新缓冲区的地址,同时设destruct指针为av_destruct_packet。 === 时间信息 === 时间信息用于实现多媒体同步。 同步的目的在于展示多媒体信息时,能够保持媒体对象之间固有的时间关系。同步有两类,一类是流内同步,其主要任务是保证单个媒体流内的时间关系,以满足感知要求,如按照规定的帧率播放一段视频;另一类是流间同步,主要任务是保证不同媒体流之间的时间关系,如音频和视频之间的关系(lipsync)。 对于固定速率的媒体,如固定帧率的视频或固定比特率的音频,可以将时间信息(帧率或比特率)置于文件首部(header),如[[tech:multimedia:digital_media#容器|AVI的hdrl List、MP4的moov box]],还有一种相对复杂的方案是将时间信息嵌入媒体流的内部,如MPEG TS和Real video,这种方案可以处理变速率的媒体,亦可有效避免同步过程中的时间漂移。 FFMPEG会为每一个数据包打上时间标签,以更有效地支持上层应用的同步机制。时间标签有两种,一种是DTS,称为解码时间标签,另一种是PTS,称为显示时间标签。对于声音来说 ,这两个时间标签是相同的,但对于某些视频编码格式,由于采用了双向预测技术,会造成DTS和PTS的不一致。 无双向预测帧的情况: 图像类型: I P P P P P P ... I P P DTS: 0 1 2 3 4 5 6... 100 101 102 PTS: 0 1 2 3 4 5 6... 100 101 102 有双向预测帧的情况: 图像类型: I P B B P B B ... I P B DTS: 0 1 2 3 4 5 6 ... 100 101 102 PTS: 0 3 1 2 6 4 5 ... 100 104 102 对于存在双向预测帧的情况,通常要求解码器对图像重排序,以保证输出的图像顺序为显示顺序: 解码器输入:I P B B P B B (DTS) 0 1 2 3 4 5 6 (PTS) 0 3 1 2 6 4 5 解码器输出:X I B B P B B P (PTS) X 0 1 2 3 4 5 6 **时间信息的获取:** 通过调用av_find_stream_info,多媒体应用可以从AVFormatContext对象中拿到媒体文件的时间信息:主要是总时间长度和开始时间,此外还有与时间信息相关的比特率和文件大小。其中时间信息的单位是AV_TIME_BASE:微秒。 typedef struct AVFormatContext { ...... int64_t start_time; int64_t duration; int64_t file_size; int bit_rate; ...... } AVFormatContext; 以上4个成员变量都是只读的,基于FFMpeg的中间件需要将其封装到某个接口中,如: LONG GetDuratioin(IntfX*); LONG GetStartTime(IntfX*); LONG GetFileSize(IntfX*); LONG GetBitRate(IntfX*); ==== APIs ==== === av_open_input_file === int av_open_input_file(AVFormatContext **ic_ptr, const char *filename, AVInputFormat *fmt, int buf_size, AVFormatParameters *ap); av_open_input_file完成两个任务: - 打开一个文件或URL,基于字节流的底层输入模块得到初始化。 - 解析多媒体文件或多媒体流的头信息,创建AVFormatContext结构并填充其中的关键字段,依次为各个原始流建立AVStream结构。 一个多媒体文件或多媒体流与其包含的原始流的关系如下: 多媒体文件/多媒体流 (movie.mkv) 原始流 1 (h.264 video) 原始流 2 (aac audio for Chinese) 原始流 3 (aac audio for english) 原始流 4 (Chinese Subtitle) 原始流 5 (English Subtitle) ... 关于输入参数: * ic_ptr,这是一个指向指针的指针,用于返回av_open_input_file内部构造的一个AVFormatContext结构体。 * fmt,用于显式指定输入文件的格式,如果设为空则自动判断其输入格式。 * buf_size,设置基于字节流的底层输入模块的缓冲区大小,如果为0则使用缺省值32768。 * ap,用于显示指定输入文件的一些媒体参数,可以设为0(不指定)。 这个函数通过解析多媒体文件或流的头信息及其他辅助数据,能够获取足够多的关于文件、流和编解码器的信息,但由于任何一种多媒体格式提供的信息都是有限的,而且不同的多媒体内容制作软件对头信息的设置不尽相同,此外这些软件在产生多媒体内容时难免会引入一些错误,因此这个函数并不保证能够获取所有需要的信息,在这种情况下,则需要考虑另一个函数:av_find_stream_info。 === av_find_stream_info === int av_find_stream_info(AVFormatContext *ic); 这个函数主要用于获取必要的编解码器参数,设置到ic->streams[i]->codec中。 首先必须得到各媒体流对应编解码器的类型和id,这是两个定义在avutils.h和avcodec.h中的枚举: enum AVMediaType { AVMEDIA_TYPE_UNKNOWN = -1, AVMEDIA_TYPE_VIDEO, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_DATA, AVMEDIA_TYPE_SUBTITLE, AVMEDIA_TYPE_ATTACHMENT, AVMEDIA_TYPE_NB }; enum CodecID { CODEC_ID_NONE, CODEC_ID_MPEG1VIDEO, CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding CODEC_ID_MPEG2VIDEO_XVMC, CODEC_ID_H261, CODEC_ID_H263, ... }; 通常,如果某种媒体格式具备完备而正确的头信息,调用av_open_input_file即可以得到这两个参数,但若是因某种原因av_open_input_file无法获取它们,这一任务将由av_find_stream_info完成。 其次还要获取各媒体流对应编解码器的时间基准。 此外,对于音频编解码器,还需要得到: - 采样率, - 声道数, - 位宽, - 帧长度(对于某些编解码器是必要的), 对于视频编解码器,则是: - 图像大小, - 色彩空间及格式, === av_seek_frame === int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags); 这个函数用于实现对媒体文件的随机访问,支持以下三种方式: * 基于时间的随机访问:具体而言就是将媒体文件读写指针定位到某个给定的时间点上,则之后调用av_read_frame时能够读到时间标签等于给定时间点的媒体数据,通常用于实现媒体播放器的快进、快退等功能。 * 基于文件偏移的随机访问:相当于普通文件的seek函数,timestamp也成为文件的偏移量。 * 基于帧号的随机访问:timestamp为要访问的媒体数据的帧号。 关于参数: * s:是个AVFormatContext指针,就是av_open_input_file返回的那个结构。 * stream_index:指定媒体流,如果是基于时间的随机访问,则第三个参数timestamp将以此媒体流的时间基准为单位;如果设为负数,则相当于不指定具体的媒体流,FFMPEG会按照特定的算法寻找缺省的媒体流,此时,timestamp的单位为AV_TIME_BASE(微秒)。 * timestamp:时间标签,单位取决于其他参数。 * flags:定位方式。AVSEEK_FLAG_BYTE表示基于字节偏移,AVSEEK_FLAG_FRAME表示基于帧号,其它表示基于时间。 === av_read_frame === int av_read_frame(AVFormatContext *s, AVPacket *pkt);
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值