EasyPlayer RTSP Windows端(下文簡稱:EasyPlayer)播放器之前抓圖代碼主要通過OpenCV來實現,且數據格式轉換的效率過於低下;故而在當時的代碼中採用線程機制來解決抓圖導致視頻播放時卡頓的問題;而最新版的EasyPlayer爲了精簡代碼也爲了提高抓圖效率,我們採用ffmpeg進行抓圖,爲了保證視頻播放的流暢性,線程機制我們仍然保留。
採用ffmpeg進行抓圖代碼如下
// 抓圖函數實現
int take_snapshot(char *file, int w, int h, uint8_t *buffer, AVPixelFormat Format)
{
char *fileext = NULL;
enum AVCodecID codecid = AV_CODEC_ID_NONE;
struct SwsContext *sws_ctx = NULL;
AVPixelFormat swsofmt = AV_PIX_FMT_NONE;
AVFrame picture = {};
int ret = -1;
AVFormatContext *fmt_ctxt = NULL;
AVOutputFormat *out_fmt = NULL;
AVStream *stream = NULL;
AVCodecContext *codec_ctxt = NULL;
AVCodec *codec = NULL;
AVPacket packet = {};
int retry = 8;
int got = 0;
// init ffmpeg
av_register_all();
fileext = file + strlen(file) - 3;
if (_stricmp(fileext, "png") == 0) {
codecid = AV_CODEC_ID_APNG;
swsofmt = AV_PIX_FMT_RGB24;
}
else {
codecid = AV_CODEC_ID_MJPEG;
swsofmt = AV_PIX_FMT_YUVJ420P;
}
AVFrame video;
int numBytesIn;
numBytesIn = av_image_get_buffer_size(Format, w, h, 1);
av_image_fill_arrays(video.data, video.linesize, buffer, Format, w, h, 1);
video.width = w;
video.height = h;
video.format = Format;
// alloc picture
picture.format = swsofmt;
picture.width = w > 0 ? w : video.width;
picture.height = h > 0 ? h : video.height;
int numBytes = av_image_get_buffer_size(swsofmt, picture.width, picture.height , 1);
buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
av_image_fill_arrays(picture.data, picture.linesize, buffer, swsofmt, picture.width, picture.height, 1);
// scale picture
sws_ctx = sws_getContext(video.width, video.height, (AVPixelFormat)Format/*video->format*/,
picture.width, picture.height, swsofmt, SWS_FAST_BILINEAR, NULL, NULL, NULL);
if (!sws_ctx) {
//av_log(NULL, AV_LOG_ERROR, "could not initialize the conversion context jpg\n");
goto done;
}
sws_scale(sws_ctx, video.data, video.linesize, 0, video.height, picture.data, picture.linesize);
// do encoding
fmt_ctxt = avformat_alloc_context();
out_fmt = av_guess_format(codecid == AV_CODEC_ID_APNG ? "apng" : "mjpeg", NULL, NULL);
fmt_ctxt->oformat = out_fmt;
if (!out_fmt) {
//av_log(NULL, AV_LOG_ERROR, "failed to guess format !\n");
goto done;
}
if (avio_open(&fmt_ctxt->pb, file, AVIO_FLAG_READ_WRITE) < 0) {
//av_log(NULL, AV_LOG_ERROR, "failed to open output file: %s !\n", file);
goto done;
}
stream = avformat_new_stream(fmt_ctxt, 0);
if (!stream) {
//av_log(NULL, AV_LOG_ERROR, "failed to create a new stream !\n");
goto done;
}
codec_ctxt = stream->codec;
codec_ctxt->codec_id = out_fmt->video_codec;
codec_ctxt->codec_type = AVMEDIA_TYPE_VIDEO;
codec_ctxt->pix_fmt = swsofmt;
codec_ctxt->width = picture.width;
codec_ctxt->height = picture.height;
codec_ctxt->time_base.num = 1;
codec_ctxt->time_base.den = 25;
codec = avcodec_find_encoder(codec_ctxt->codec_id);
if (!codec) {
//av_log(NULL, AV_LOG_ERROR, "failed to find encoder !\n");
goto done;
}
if (avcodec_open2(codec_ctxt, codec, NULL) < 0) {
//av_log(NULL, AV_LOG_ERROR, "failed to open encoder !\n");
goto done;
}
while (retry-- && !got) {
if (avcodec_encode_video2(codec_ctxt, &packet, &picture, &got) < 0) {
//av_log(NULL, AV_LOG_ERROR, "failed to do picture encoding !\n");
goto done;
}
if (got) {
ret = avformat_write_header(fmt_ctxt, NULL);
if (ret < 0) {
//av_log(NULL, AV_LOG_ERROR, "error occurred when opening output file !\n");
goto done;
}
av_write_frame(fmt_ctxt, &packet);
av_write_trailer(fmt_ctxt);
}
}
// ok
ret = 0;
done:
avcodec_close(codec_ctxt);
if (fmt_ctxt)
{
avio_close(fmt_ctxt->pb);
}
avformat_free_context(fmt_ctxt);
av_packet_unref(&packet);
sws_freeContext(sws_ctx);
av_free(buffer);
return ret;
}
藉助ffmpeg強大的視頻處理和轉換功能,我們可以將一幀圖像轉換成任意格式的圖片,當然如代碼所示我們只選擇性地支持了“jpeg”和“png”兩種格式的圖片格式;
採用ffmpeg抓圖的步驟分兩步:
- 需要將圖像轉換成指定的格式,當然強大的格式轉換函數也支持圖像的縮放,且效率很高;
- 圖像編碼,細心的同學不難發現,ffmpeg的編碼和存文件/推送流的代碼是通用的,這套代碼可以用來抓圖也可以用來編碼H264、265等然後存文件(如MP4等)或者推送RTMP/RTSP等;
已經完成了抓圖代碼調用起來就很簡單了,只需替換掉舊的抓圖函數即可,需要注意的是之前的抓圖固定了格式爲YUY2,所以緩衝區大小隻有WidthHeight2的大小,而顯然RGB24格式的數據會導致緩衝區溢出,所以,我們需要重新定義緩衝區的大小,如下代碼所示:
//抓圖
if (pThread->manuScreenshot == 0x01 )//Just support jpeg,png
{
unsigned int timestamp = (unsigned int)time(NULL);
time_t tt = timestamp;
struct tm *_time = localtime(&tt);
char szTime[64] = {0,};
strftime(szTime, 32, "%Y%m%d-%H%M%S", _time);
// char strPath[512] = {0,};
// sprintf(strPath , "%sch%d_%s.jpg", pThread->strScreenCapturePath, pThread->channelId, szTime) ;
PhotoShotThreadInfo* pShotThreadInfo = new PhotoShotThreadInfo;
sprintf(pShotThreadInfo->strPath , "%sch%d_%s.jpg", pThread->strScreenCapturePath, pThread->channelId, szTime) ;
int nYuvBufLen = frameinfo.width*frameinfo.height*3;// most size = RGB24, we donot support RGBA Render type
pShotThreadInfo->pYuvBuf = new unsigned char[nYuvBufLen];
pShotThreadInfo->width = frameinfo.width;
pShotThreadInfo->height = frameinfo.height;
pShotThreadInfo->renderFormat = pThread->renderFormat ;
memcpy(pShotThreadInfo->pYuvBuf, pThread->yuvFrame[pThread->decodeYuvIdx].pYuvBuf, pThread->yuvFrame[pThread->decodeYuvIdx].Yuvsize-1);
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_lpPhotoShotThread, pShotThreadInfo, 0, NULL);
pThread->manuScreenshot = 0;
}
目前我們所支持的最大數據格式是RGB24,所以我們定義了WidthHeight3+1的最大緩衝區大小,其實這裏可以優化一下,就是根據具體的renderFormat來定義緩衝區的大小,從而避免不必要的內存資源浪費,這會在後續版本進行優化。