前言
最近实习的时候突然leader让写一个可以提取视频的时长,宽高信息和某一帧的小工具,就开始了解音视频的流媒体编程。主要用到的是
FFmpeg,看了好多好多的文章,尤其是雷霄骅的文章,真的是个好厉害的人啊。
这篇文章主要就记录我在写这个小工具的时候遇到的问题,以及收集到的一些信息。
1 FFmpeg
FFmpeg是一个开源的项目,很多的播放器之类的都是基于这个框架搭建;也可以直接下载使用一些简单的功能,比如Mac可以直接 brew install ffmpeg
安装后,命令行可以使用ffmpeg
ffmplay
和 ffprobe
这三种命令,ffprobe input.mov
就可以直接查看视频的信息,如下图。
当然也提供了很多的API供使用,一些经常要用到的编码解码在Example里有很详细的例子,入门的话我觉得仔细分析decode_video.c
那个例子就可以上手完成很基础的东西了!
2 音视频处理流程
关于音视频的处理流程网上有很多大神都有详细的介绍,但是也要注意FFmpeg的版本问题,有不少的API已经deprecated,所以还是要具体情况具体分析,可以多查看官方的文档,很多新的API都有特别详尽的说明和例子的。
有一个写的特别好的知乎文章,一开始我也是一知半解,东边抄抄西边凑凑,但是看完这个之后就突然感觉思路特别清晰,这感觉约等于是修仙小说里头一路靠磕丹药基础薄弱的炮灰突然遇到主角那种稳扎稳打一路杀上来的时候的降维打击感。链接点一下,不看会后悔,绝佳入门,从不骗人!
还有一个tutorial也特别的好,但是里面用到的API是之前老版本的,但是对于入门而言也是特别好的资料。
2.1 用到的函数分析
拿到一个视频文件,首先我们一定是要打开它!这里有一个贯穿整个FFmpeg里的一个结构体AVFormatContext
, 这个结构体是对音视频文件的一种抽象和封装,里面包含了很多的信息,像音频流、视频流、字幕流等等,具体可以看官方文档。avformat_open_input()
打开了输入文件后,读取文件的header并将这些信息存储在分配的AVFormatContext
中;但是有一些文件没有header或者是header中没有存储足够的信息,所以通常还需要再使用一个avformat_find_stream_info()
,通过尝试读并解码个帧来获取一些补充信息。
if ((ret = avformat_open_input(&fmt_ctx, inputfile, NULL, NULL)) < 0) {
fprintf(stderr, "Unable to open file %s\n", inputfile);
return EXIT_FAILURE;
}
if ((ret = avformat_find_stream_info(fmt_ctx, NULL)) < 0) {
fprintf(stderr, "Cannot find stream information\n");
return EXIT_FAILURE;
}
av_dump_format(fmt_ctx, 0, inputfile, 0); // output the stream information
刚才有讲一个视频文件中通常有多个流也就是stream,所以我们接下来我们需要根据我们需要的类型找到我们需要的流。也可以使用FFmpeg提供的一个API: av_find_best_stream
for (int i = 0; i < fmt_ctx->nb_streams; i++)
if (fmt_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && videoStream < 0) {
videoStream = i;
break;
}
if (videoStream == -1)
return EXIT_FAILURE;
接下来就可以根据我们的frameNum
去找到我们想要的那一帧了!一开始的需求是只需要提取第一帧所以直接用了一个while循环去一个一个读,但是后来Leader又说想要获取任意一个关键帧,就想到了可以用av_seek_frame
来获取任意帧的位置,之后只要读取并将这一帧保存就可以了。同时这里还需要将关键帧保存为任意的宽高,所以用到了sws_scale
. 注意这里使用avcodec_receive_frame
的时候一定要注意判断这一帧是不是读完了!!一定!!
ret = av_seek_frame(fmt_ctx, -1, frameNum * AV_TIME_BASE, AVSEEK_FLAG_BACKWARD);
if (ret < 0) {
fprintf(stderr, "Cannot find the frame, check the frame number\n");
return EXIT_FAILURE;
}
if (fixed)
nHeight = nWidth * dec_ctx->height / dec_ctx->width;
numBytes = avpicture_get_size(dec_ctx->pix_fmt, nWidth, nHeight); // required buffer size
buffer = (uint8_t*)av_malloc(numBytes * sizeof(uint8_t));
avpicture_fill((AVPicture*)avFrameScale, buffer, dec_ctx->pix_fmt, nWidth, nHeight);
sws_ctx = sws_getContext(dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt,
nWidth, nHeight, dec_ctx->pix_fmt, SWS_BILINEAR, NULL, NULL, NULL);
while (true) {
av_read_frame(fmt_ctx, &avPacket);
if (avPacket.stream_index == videoStream) {
ret = avcodec_send_packet(dec_ctx, &avPacket);
frameFinished = avcodec_receive_frame(dec_ctx, avFrame);
if (frameFinished == 0) {
sws_scale(sws_ctx, (uint8_t const * const *)avFrame->data, avFrame->linesize,
0, dec_ctx->height, avFrameScale->data, avFrameScale->linesize);
avFrameScale->format = avFrame->format;
avFrameScale->width = nWidth;
avFrameScale->height = nHeight;
if ((ret = saveFrameToIMG(outputfile, dec_ctx, avFrameScale, suffixStr)) == EXIT_FAILURE)
return EXIT_FAILURE;
break;
}
}
av_free_packet(