最近由于项目需求,要在界面中内嵌一个简单的视频播放器,能够打开视频,逐帧播放,进度条拖动等功能。
因此,首先尝试使用opencv编写。原因:1、便于后续处理;2、opencv提高的接口较完善。很快就动手写出了一个;基于OpenCV的视频播放器。
#include "stdafx.h" #include <opencv2/opencv.hpp> #include <time.h> #include <Windows.h> #include <iostream> #include <stdlib.h> using namespace std; using namespace cv; int g_slider_position = 0; int n_position = 0; VideoCapture pCap; // 回调函数 void onTrackbarSlide( int pos, void * ) { if (pos != n_position) { pCap.set(CV_CAP_PROP_POS_FRAMES, pos); n_position = pos; g_slider_position = pos; } } int main( int argc, char *argv[] ) { Mat pFrame; int nFrames = 0; double fps = 0; // 帧率 int spf = 0; // 1/fps //char filename[200] = "768x576.avi"; if (argc == 2) { sprintf(filename, "%s", argv[1]); } namedWindow("VideoPlayer", 0); // 可改变窗口大小 pCap.open(filename); if (!pCap.isOpened()) { printf("Count not open video file/n"); exit(1); } // 获取视频帧率 fps = pCap.get(CV_CAP_PROP_FPS); spf = (int)(1000/fps); printf("fps = %.f\nspf = %d\n", fps, spf); // 获取视频总帧数 nFrames = (int)(pCap.get(CV_CAP_PROP_FRAME_COUNT)); printf("Total frames = %d\n", nFrames); // 如果由于编码方式问题获取不到帧数,则不创建滚动条 if (nFrames != 0) { createTrackbar( "Position", "VideoPlayer", &g_slider_position, nFrames, onTrackbarSlide ); } while (1) { pCap >> pFrame; if (pFrame.empty()) // 播放结束退出 { break; } imshow("VideoPlayer", pFrame); // 播放的过程中移动滚动条 printf("current pos %d\n", n_position); n_position++; g_slider_position = n_position; setTrackbarPos("Position", "VideoPlayer", g_slider_position); char c = waitKey((int)(spf)); // 按原来的帧率播放视频 if (c == 27) // 按下Esc退出播放 { break; } } pCap.release(); pFrame.release(); destroyWindow("VideoPlayer"); destroyWindow("VideoPlayerControl"); return 0; }
在播放部分avi格式的视频时,拖动进度条会出现问题帧定位不准的问题,可能是定位到了关键帧。
google了一番,大家认为是从opencv2.x开始,开始使用ffmpeg。ffmpeg是一个开源的视频库,能够打开视频,转码等等。KMPlayer等等播放器都使用过这个库,只是因为违反了LGPL,被ffmpeg社区拉黑了。
下面给出的是X:\OpenCV\OpenCV231\modules\highgui\src\cap_ffmpeg_impl.hpp
bool CvCapture_FFMPEG::open( const char* _filename ) { unsigned i; bool valid = false; close(); /* register all codecs, demux and protocols */ av_register_all(); #ifndef _DEBUG // av_log_level = AV_LOG_QUIET; #endif int err = av_open_input_file(&ic, _filename, NULL, 0, NULL); if (err < 0) { CV_WARN("Error opening file"); goto exit_func; } err = av_find_stream_info(ic); if (err < 0) { CV_WARN("Could not find codec parameters"); goto exit_func; } for(i = 0; i < ic->nb_streams; i++) { #if LIBAVFORMAT_BUILD > 4628 AVCodecContext *enc = ic->streams[i]->codec; #else AVCodecContext *enc = &ic->streams[i]->codec; #endif avcodec_thread_init(enc, get_number_of_cpus()); #if LIBAVFORMAT_BUILD < CALC_FFMPEG_VERSION(53, 4, 0) #define AVMEDIA_TYPE_VIDEO CODEC_TYPE_VIDEO #endif if( AVMEDIA_TYPE_VIDEO == enc->codec_type && video_stream < 0) { AVCodec *codec = avcodec_find_decoder(enc->codec_id); if (!codec || avcodec_open(enc, codec) < 0) goto exit_func; video_stream = i; video_st = ic->streams[i]; picture = avcodec_alloc_frame(); rgb_picture.data[0] = (uint8_t*)malloc( avpicture_get_size( PIX_FMT_BGR24, enc->width, enc->height )); avpicture_fill( (AVPicture*)&rgb_picture, rgb_picture.data[0], PIX_FMT_BGR24, enc->width, enc->height ); frame.width = enc->width; frame.height = enc->height; frame.cn = 3; frame.step = rgb_picture.linesize[0]; frame.data = rgb_picture.data[0]; break; } } if(video_stream >= 0) valid = true; // perform check if source is seekable via ffmpeg's seek function av_seek_frame(...) err = av_seek_frame(ic, video_stream, 10, 0); if (err < 0) { filename=(char*)malloc(strlen(_filename)+1); strcpy(filename, _filename); // reopen videofile to 'seek' back to first frame reopen(); } else { // seek seems to work, so we don't need the filename, // but we still need to seek back to filestart filename=NULL; int64_t ts = video_st->first_dts; int flags = AVSEEK_FLAG_FRAME | AVSEEK_FLAG_BACKWARD; av_seek_frame(ic, video_stream, ts, flags); } exit_func: if( !valid ) close(); return valid; }
err = av_seek_frame(ic, video_stream, 10, 0);
是大家认为opencv只能定位关键帧的原因所在。因此,大家推荐的做法是将0改成AVSEEK_FLAG_ANY,然后重新编译opencv。
第二个问题是,opencv能够打开并播放mp4,ts等格式的视频文件,但是不能提供帧定位。只有拖动进度条重新定位,视频就会从头开始播放。面对这个问题甚是苦恼。
既然opencv使用的是ffmpeg,那么我可以直接用ffmpeg的库进行视频文件的操作。因此从www.ffmpeg.org下载最新的static、shared和dev文件,解压出dev和shared,将shared中的bin文件夹拷贝到dev中。(我只是为了用起来方便,请原谅我的轻度强迫症)
我把dev放在了c盘根目录下,命名为ffmpeg。将c:\ffmpeg\bin加入到环境变量的path。然后注销一下。
在工程中加入ffmpeg\include和ffmpeg\lib,并将ffmpeg\lib中的*.lib加入到链接库中。在代码中加入对应的头文件就可以了。
#include "stdafx.h" #include <opencv2/opencv.hpp> #include <time.h> #include <Windows.h> #include <iostream> #include <stdlib.h> #include <math.h> #ifdef __cplusplus extern "C" { #endif #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libavutil/opt.h> #include <libavutil/channel_layout.h> #include <libavutil/common.h> #include <libavutil/imgutils.h> #include <libavutil/mathematics.h> #include <libavutil/samplefmt.h> #ifdef __cplusplus } #endif #define N 3 #define INBUF_SIZE 4096 #define AUDIO_INBUF_SIZE 20480 #define AUDIO_REFILL_THRESH 4096 using namespace std; using namespace cv; int main( int argc, char *argv[] ) { AVFormatContext *pFormatCtx; int codec_id = CODEC_ID_MPEG4; char filename[] = "Locust_Inspired_Jumping_Robot.flv"; // Open video file AVCodec *codec; AVCodecContext *c= NULL; int i, out_size, x, y, outbuf_size; FILE *f; AVFrame *picture; uint8_t *outbuf; int had_output = 0; av_register_all(); printf("Encode video file %s\n", filename); codec = avcodec_find_encoder(CODEC_ID_H264); if (!codec) { fprintf(stderr, "codec not found\n"); exit(1); } c = avcodec_alloc_context3(codec); picture = avcodec_alloc_frame(); /* put sample parameters */ c->bit_rate = 40000; //c->bit_rate_tolerance=30; /* resolution must be a multiple of two */ c->width = 352; c->height = 288; /* frames per second */ c->time_base.den= 25; c->time_base.num= 1; c->gop_size = 10; /* emit one intra frame every ten frames */ c->max_b_frames=1; c->pix_fmt = PIX_FMT_YUV420P; if(codec_id == CODEC_ID_H264) av_opt_set(c->priv_data, "preset", "slow", 0); /* open it */ if (avcodec_open2(c, codec, NULL) < 0) { fprintf(stderr, "could not open codec\n"); exit(1); } f = fopen(filename, "wb"); if (!f) { fprintf(stderr, "could not open %s\n", filename); exit(1); } /* alloc image and output buffer */ outbuf_size = 100000 + 12*c->width*c->height; outbuf = (uint8_t*)malloc(outbuf_size); //CHANGED /* the image can be allocated by any means and av_image_alloc() is * just the most convenient way if av_malloc() is to be used */ av_image_alloc(picture->data, picture->linesize, c->width, c->height, c->pix_fmt, 1); /* encode 1 second of video */ for(i = 0; i < 25; i++) { fflush(stdout); /* prepare a dummy image */ /* Y */ for(y = 0; y < c->height; y++) { for(x = 0; x < c->width; x++) { picture->data[0][y * picture->linesize[0] + x] = x + y + i * 3; } } /* Cb and Cr */ for(y = 0; y < c->height / 2; y++) { for(x = 0; x < c->width / 2; x++) { picture->data[1][y * picture->linesize[1] + x] = 128 + y + i * 2; picture->data[2][y * picture->linesize[2] + x] = 64 + x + i * 5; } } /* encode the image */ out_size = avcodec_encode_video(c, outbuf, outbuf_size, picture); had_output |= out_size; printf("encoding frame %3d (size=%5d)\n", i, out_size); fwrite(outbuf, 1, out_size, f); } /* get the delayed frames */ for(; out_size || !had_output; i++) { fflush(stdout); out_size = avcodec_encode_video(c, outbuf, outbuf_size, NULL); had_output |= out_size; printf("write frame %3d (size=%5d)\n", i, out_size); fwrite(outbuf, 1, out_size, f); } /* add sequence end code to have a real mpeg file */ outbuf[0] = 0x00; outbuf[1] = 0x00; outbuf[2] = 0x01; outbuf[3] = 0xb7; fwrite(outbuf, 1, 4, f); fclose(f); free(outbuf); avcodec_close(c); av_free(c); av_free(picture->data[0]); av_free(picture); printf("\n"); return 0; }
值得注意的是,在包含ffmpeg的头文件时一定要定义在extern “C”中,同时这些头文件必须在#include <math.h>之后。
然后编译一下,就可以运行了。目前做到这一步了,现挖个坑,等把如何用ffmpeg读取编码输出视频的问题解决了再继续写。
未完待续