#include "xvideo_view.h"
#include "xsdl.h"
#include <iostream>
using namespace std;
extern "C"
{
#include <libavcodec/avcodec.h>
}
#pragma comment(lib, "avutil.lib")
XVideoView* XVideoView::Create(RenderType type)
{
switch (type)
{
case XVideoView::SDL:
return new XSDL();
break;
default:
break;
}
// 如果不支持的话,就直接返回nullptr
return nullptr;
}
XVideoView::~XVideoView()
{
if (cache_)
delete cache_;
cache_ = nullptr;
}
bool XVideoView::DrawFrame(AVFrame* frame)
{
if (!frame)
{
cout << "input frame can not be null" << endl;
return false;
}
// 累积显示次数
count_++;
if (beg_ms_ <= 0)
{
beg_ms_ = clock(); // 开始计时
}
else if ((clock() - beg_ms_) / (CLOCKS_PER_SEC / 1000) >= 1000) // 1s 计算一次fps
{
render_fps_ = count_;
count_ = 0;
beg_ms_ = clock(); // 重新计时
}
int linesize = 0;
switch (frame->format)
{
case AV_PIX_FMT_YUV420P:
return Draw(frame->data[0], frame->linesize[0], // Y
frame->data[1], frame->linesize[1], // U
frame->data[2], frame->linesize[2] // V
);
case AV_PIX_FMT_NV12:
if (!cache_)
{
// 若空间尚未分配,则申请一块较大的空间(4K的一幅图像的大小)
cache_ = new unsigned char[4096 * 2160 * 1.5];
}
// PS: 注意!这里面涉及到一个内存对齐的问题!
// 如果是400x300的YUV420P/NV12格式,它的linesize很可能会被FFmpeg自动进行字节对齐, 即当图像尺寸为400x300时,它的linesize可能
// 不是400,而是416(假设ffmpeg是以16字节进行对齐)
// 而在使用SDL进行渲染时,linesize若传递416,很可能导致渲染出现问题,为了避免出现这种情况,下面提供了一种逐行复制的策略。
// 下面的内存拷贝主要目的是为了让数据是连续的
linesize = frame->width;
if (frame->linesize[0] == frame->width)
{
// ---------------- 若linesize与图像的宽度一致,说明未发生字节对齐 ---------------
// 拷贝所有Y分量
memcpy(cache_, frame->data[0], frame->linesize[0] * frame->height);
// 拷贝所有的UV分量
memcpy(cache_ + frame->linesize[0] * frame->height, frame->data[1], frame->linesize[1] * frame->height / 2);
}
else
{
// ---------------- 若linesize与图像的宽度一致,说明发生了字节对齐 ---------------
// 使用逐行拷贝的方式
// 拷贝所有的Y分量
for (int i = 0; i < frame->height; i++)
{
memcpy(cache_ + i * frame->width,
frame->data[0] + i * frame->linesize[0],
frame->width // 注意只需要拷贝frame->width个数即可,丢弃因为对齐产生的多余数据
);
}
// 拷贝所有UV分量
for (int i = 0; i < frame->height / 2; i++)
{
// 将指针定位到所有Y分量数据的结尾处,从所有Y分量数据的结尾处开始拷贝UV分量
auto p = cache_ + frame->width * frame->height;
memcpy(p + i * frame->width,
frame->data[1] + i * frame->linesize[1],
frame->width // 注意只需要拷贝frame->width个数即可,丢弃因为对齐产生的多余数据
);
}
}
return Draw(cache_, linesize);
case AV_PIX_FMT_RGBA:
case AV_PIX_FMT_BGRA:
case AV_PIX_FMT_ARGB:
return Draw(frame->data[0], frame->linesize[0]);
default:
break;
}
return false;
}
/**
* @brief 返回当前时间戳
* @return 当前时间戳
*/
long long NowMs()
{
// 注意!!! 这里不要直接返回clock(), 这样的话只能在windows中当中使用,因为这个函数在windows当中返回的是毫秒数
// 但是在linux当中这个值返回的是微秒数,所以需要除以 (CLOCKS_PER_SEC / 1000)
// 因为在linux当中 CLOCKS_PER_SEC 这个值返回的是1000000, 但是在windows当中这个值返回的是1000.
// 这样可以最大限度地保证兼容性和跨平台
return clock() / (CLOCKS_PER_SEC / 1000);
}
AVFrame* XVideoView::Read()
{
// 如果宽度小于0,高度小于0,且文件未成功打开,则直接返回
if (width_ <= 0 || height_ <= 0 || !ifs_) return nullptr;
// 若AVFrame内存空间已申请,但参数发生变化,则需要释放空间
// 因为用户可能通过界面上的文本输入框重新设定图像大小
if (frame_)
{
if (frame_->width != width_ ||
frame_->height != height_ ||
frame_->format != fmt_)
{
// 说明用户重新选择了界面参数(宽,高,像素格式),需要释放AVFrame空间
av_frame_free(&frame_);
}
}
// 若frame为空,则重新申请
if (!frame_)
{
frame_ = av_frame_alloc();
frame_->width = width_;
frame_->height = height_;
frame_->format = fmt_;
frame_->linesize[0] = width_ * 4; // 默认像素格式为RGBA
if (frame_->format == AV_PIX_FMT_YUV420P)
{
frame_->linesize[0] = width_; // Y
frame_->linesize[1] = width_ / 2; // U
frame_->linesize[2] = width_ / 2; // V
}
// 生成AVFrame的buff空间,使用默认对齐方式
auto re = av_frame_get_buffer(frame_, 0);
if (re != 0)
{
char buf[1024] = { 0 };
av_strerror(re, buf, sizeof(buf) - 1);
cout << buf << endl;
av_frame_free(&frame_);
return nullptr;
}
}
if (!frame_) return nullptr;
// 读取一帧数据
if (frame_->format == AV_PIX_FMT_YUV420P)
{
ifs_.read((char*)frame_->data[0], frame_->linesize[0] * height_); // 读取Y
ifs_.read((char*)frame_->data[1], frame_->linesize[1] * height_ / 2); // 读取U
ifs_.read((char*)frame_->data[2], frame_->linesize[2] * height_ / 2); // 读取V
}
else
{
ifs_.read((char *)frame_->data[0], frame_->linesize[0] * height_);
}
// 如果读取到文件末尾,则直接返回
if (ifs_.gcount() == 0)
return nullptr;
// 否则返回读取后的AVFrame数据
return frame_;
}
bool XVideoView::Open(std::string filepath)
{
if (ifs_.is_open())
{
ifs_.close();
}
ifs_.open(filepath, ios::binary);
return ifs_.is_open();
}
bool XSDL::Draw(const unsigned char* data, int linesize)
{
if (!data)
{
cout << "input data is null" << endl;
return false;
}
// 保证线程同步
unique_lock<mutex> sdl_lock(mtx_);
if (!texture_ || !render_ || !win_ || width_ <= 0 || height_ <= 0)
{
cout << "draw failed, param error" << endl;
return false;
}
if (linesize <= 0)
{
switch (fmt_)
{
case XVideoView::RGBA:
case XVideoView::ARGB:
linesize = width_ * height_ * 4;
break;
case XVideoView::YUV420P:
linesize = width_;
break;
default:
break;
}
}
if (linesize <= 0)
{
cout << "linesize is error" << endl;
return false;
}
// 复制内存到显存
auto re = SDL_UpdateTexture(texture_, NULL, data, linesize);
if (re)
{
cout << "update texture failed" << endl;
return false;
}
// 清理渲染器
SDL_RenderClear(render_);
// 如果用户手动设置了缩放,就按照用户设置的大小显示
// 如果用户没有设置,就传递null, 采用默认的窗口大小
SDL_Rect *prect = nullptr;
if (scale_w_ > 0 || scale_h_ > 0)
{
SDL_Rect rect;
rect.x = 0;
rect.y = 0;
rect.w = scale_w_;
rect.h = scale_h_;
prect = ▭
}
// 拷贝材质到渲染器
re = SDL_RenderCopy(render_, texture_, NULL, prect);
if (re)
{
cout << "copy texture failed" << endl;
return false;
}
// 显示
SDL_RenderPresent(render_);
return true;
}
参考: