c++程序使用ffmpeg和drm播放MP4
时间: 2025-07-14 14:18:00 浏览: 6
<think>我们将使用FFmpeg进行视频解码,然后使用DRM(Direct Rendering Manager)在Linux上直接渲染视频帧。这避免了通过X11或Wayland的开销,通常用于嵌入式系统或需要高性能的场景。
步骤:
1. 初始化FFmpeg:打开视频文件,查找视频流,创建解码器。
2. 初始化DRM:打开DRM设备,设置显示模式,创建FrameBuffer。
3. 解码循环:从视频文件中读取帧,解码,将解码后的帧转换为DRM支持的格式(如NV12或RGB)。
4. 将帧数据导入DRM的FrameBuffer并显示。
5. 清理资源。
注意:我们假设系统已经安装了FFmpeg和libdrm的开发包。编译时需要链接这些库。
代码结构:
- 打开视频文件,获取视频流信息
- 初始化FFmpeg解码器(H264/H265)
- 初始化DRM设备,设置显示模式
- 为解码帧分配DRM FrameBuffer
- 循环:解码帧 -> 转换帧格式(如果需要) -> 显示
- 退出时释放资源
我们将使用FFmpeg的硬件加速解码(例如通过VAAPI)来减少CPU负载,但为了简化,我们先使用软解码。DRM部分我们将使用drmModeSetPlane直接设置平面。
注意:DRM显示需要root权限或视频组权限。
代码示例:</think>以下是一个完整的 C++ 程序,使用 FFmpeg 进行视频解码,并通过 DRM (Direct Rendering Manager) 直接在 Linux 帧缓冲上显示 MP4 文件内容:
```cpp
#include <iostream>
#include <thread>
#include <chrono>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
// DRM显示结构体
struct DrmDisplay {
int fd;
uint32_t conn_id;
uint32_t crtc_id;
drmModeModeInfo mode;
uint32_t plane_id;
uint32_t fb_id;
uint32_t width;
uint32_t height;
uint32_t pitch;
uint8_t* map;
};
// 初始化DRM显示
bool init_drm(DrmDisplay* disp, const char* device = "/dev/dri/card0") {
disp->fd = open(device, O_RDWR | O_CLOEXEC);
if (disp->fd < 0) {
perror("Failed to open DRM device");
return false;
}
drmModeRes* res = drmModeGetResources(disp->fd);
if (!res) {
perror("drmModeGetResources failed");
return false;
}
// 查找第一个连接的显示器
bool connector_found = false;
for (int i = 0; i < res->count_connectors; i++) {
drmModeConnector* conn = drmModeGetConnector(disp->fd, res->connectors[i]);
if (conn && conn->connection == DRM_MODE_CONNECTED) {
disp->conn_id = conn->connector_id;
disp->mode = conn->modes[0]; // 使用第一个可用模式
connector_found = true;
drmModeFreeConnector(conn);
break;
}
if (conn) drmModeFreeConnector(conn);
}
if (!connector_found) {
fprintf(stderr, "No connected display found\n");
drmModeFreeResources(res);
return false;
}
// 查找CRTC
disp->crtc_id = res->crtcs[0]; // 通常使用第一个CRTC
// 查找Overlay平面
drmModePlaneRes* planes = drmModeGetPlaneResources(disp->fd);
if (!planes) {
perror("drmModeGetPlaneResources failed");
drmModeFreeResources(res);
return false;
}
bool plane_found = false;
for (uint32_t i = 0; i < planes->count_planes; i++) {
drmModePlane* plane = drmModeGetPlane(disp->fd, planes->planes[i]);
if (plane) {
// 检查平面类型(OVERLAY或PRIMARY)
if (plane->possible_crtcs & (1 << 0)) { // 假设CRTC索引为0
disp->plane_id = plane->plane_id;
plane_found = true;
drmModeFreePlane(plane);
break;
}
drmModeFreePlane(plane);
}
}
drmModeFreePlaneResources(planes);
drmModeFreeResources(res);
if (!plane_found) {
fprintf(stderr, "No suitable plane found\n");
return false;
}
return true;
}
// 创建DRM FrameBuffer
bool create_drm_fb(DrmDisplay* disp, uint32_t width, uint32_t height, uint32_t format) {
disp->width = width;
disp->height = height;
// 创建FrameBuffer对象
struct drm_mode_create_dumb create_dumb = {0};
create_dumb.width = width;
create_dumb.height = height;
create_dumb.bpp = 32; // 32位像素格式
if (drmIoctl(disp->fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_dumb) < 0) {
perror("DRM_IOCTL_MODE_CREATE_DUMB failed");
return false;
}
disp->pitch = create_dumb.pitch;
// 映射内存
struct drm_mode_map_dumb map_dumb = {0};
map_dumb.handle = create_dumb.handle;
if (drmIoctl(disp->fd, DRM_IOCTL_MODE_MAP_DUMB, &map_dumb) < 0) {
perror("DRM_IOCTL_MODE_MAP_DUMB failed");
return false;
}
disp->map = static_cast<uint8_t*>(mmap(0, create_dumb.size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
disp->fd,
map_dumb.offset));
if (disp->map == MAP_FAILED) {
perror("mmap failed");
return false;
}
// 添加FrameBuffer
uint32_t handles[4] = {create_dumb.handle, 0, 0, 0};
uint32_t pitches[4] = {disp->pitch, 0, 0, 0};
uint32_t offsets[4] = {0, 0, 0, 0};
if (drmModeAddFB2(disp->fd, width, height, format,
handles, pitches, offsets, &disp->fb_id, 0)) {
perror("drmModeAddFB2 failed");
return false;
}
return true;
}
// 显示帧到屏幕
void display_frame(DrmDisplay* disp, uint8_t* data, int width, int height) {
// 计算居中位置
int x = (disp->mode.hdisplay - width) / 2;
int y = (disp->mode.vdisplay - height) / 2;
// 复制数据到DRM缓冲区
for (int i = 0; i < height; i++) {
memcpy(disp->map + i * disp->pitch,
data + i * width * 4,
width * 4);
}
// 设置平面显示
drmModeSetPlane(disp->fd, disp->plane_id, disp->crtc_id, disp->fb_id, 0,
x, y, width, height,
0, 0, width << 16, height << 16);
}
// FFmpeg解码和播放函数
void play_mp4(const char* filename, DrmDisplay* disp) {
AVFormatContext* fmt_ctx = nullptr;
AVCodecContext* codec_ctx = nullptr;
AVCodec* codec = nullptr;
AVFrame* frame = nullptr;
AVFrame* rgb_frame = nullptr;
AVPacket pkt;
SwsContext* sws_ctx = nullptr;
// 打开视频文件
if (avformat_open_input(&fmt_ctx, filename, nullptr, nullptr) != 0) {
std::cerr << "Could not open video file" << std::endl;
return;
}
// 查找流信息
if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
std::cerr << "Could not find stream information" << std::endl;
avformat_close_input(&fmt_ctx);
return;
}
// 查找视频流
int video_stream = -1;
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream = i;
break;
}
}
if (video_stream == -1) {
std::cerr << "No video stream found" << std::endl;
avformat_close_input(&fmt_ctx);
return;
}
// 获取解码器
AVCodecParameters* codec_params = fmt_ctx->streams[video_stream]->codecpar;
codec = avcodec_find_decoder(codec_params->codec_id);
if (!codec) {
std::cerr << "Unsupported codec" << std::endl;
avformat_close_input(&fmt_ctx);
return;
}
// 创建解码器上下文
codec_ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(codec_ctx, codec_params);
// 打开解码器
if (avcodec_open2(codec_ctx, codec, nullptr) < 0) {
std::cerr << "Could not open codec" << std::endl;
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
return;
}
// 分配帧
frame = av_frame_alloc();
rgb_frame = av_frame_alloc();
if (!frame || !rgb_frame) {
std::cerr << "Could not allocate frames" << std::endl;
goto cleanup;
}
// 准备RGB转换
rgb_frame->format = AV_PIX_FMT_RGBA;
rgb_frame->width = codec_ctx->width;
rgb_frame->height = codec_ctx->height;
av_frame_get_buffer(rgb_frame, 0);
// 创建DRM FrameBuffer
if (!create_drm_fb(disp, codec_ctx->width, codec_ctx->height, DRM_FORMAT_XRGB8888)) {
std::cerr << "Failed to create DRM framebuffer" << std::endl;
goto cleanup;
}
// 主解码循环
while (av_read_frame(fmt_ctx, &pkt) >= 0) {
if (pkt.stream_index == video_stream) {
// 发送包到解码器
int ret = avcodec_send_packet(codec_ctx, &pkt);
if (ret < 0) {
std::cerr << "Error sending packet to decoder" << std::endl;
break;
}
// 接收解码后的帧
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
std::cerr << "Error during decoding" << std::endl;
break;
}
// 创建缩放上下文
sws_ctx = sws_getCachedContext(sws_ctx,
frame->width, frame->height,
static_cast<AVPixelFormat>(frame->format),
codec_ctx->width, codec_ctx->height,
AV_PIX_FMT_RGBA,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!sws_ctx) {
std::cerr << "Could not create scale context" << std::endl;
break;
}
// 转换到RGB
sws_scale(sws_ctx,
frame->data, frame->linesize,
0, frame->height,
rgb_frame->data, rgb_frame->linesize);
// 显示帧
display_frame(disp, rgb_frame->data[0],
codec_ctx->width, codec_ctx->height);
// 控制帧率
double fps = av_q2d(fmt_ctx->streams[video_stream]->avg_frame_rate);
if (fps > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(
static_cast<int>(1000 / fps)));
}
}
}
av_packet_unref(&pkt);
}
// 刷新解码器
pkt.data = nullptr;
pkt.size = 0;
avcodec_send_packet(codec_ctx, &pkt);
cleanup:
// 清理资源
if (sws_ctx) sws_freeContext(sws_ctx);
if (frame) av_frame_free(&frame);
if (rgb_frame) av_frame_free(&rgb_frame);
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
}
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <video.mp4>" << std::endl;
return 1;
}
// 初始化DRM
DrmDisplay disp;
if (!init_drm(&disp)) {
std::cerr << "DRM initialization failed" << std::endl;
return 1;
}
// 注册FFmpeg组件
av_register_all();
avformat_network_init();
// 播放视频
play_mp4(argv[1], &disp);
// 清理DRM资源
if (disp.fd >= 0) {
if (disp.fb_id) drmModeRmFB(disp.fd, disp.fb_id);
close(disp.fd);
}
return 0;
}
```
### 代码详解
#### 1. DRM 初始化与帧缓冲管理
```cpp
bool init_drm(DrmDisplay* disp) {
// 打开DRM设备文件
disp->fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
// 获取显示资源
drmModeRes* res = drmModeGetResources(disp->fd);
// 查找连接的显示器
for (...) {
if (conn->connection == DRM_MODE_CONNECTED) {
disp->conn_id = conn->connector_id;
disp->mode = conn->modes[0]; // 使用第一个可用模式
}
}
}
```
- **关键点**:
- 使用 `O_CLOEXEC` 标志防止文件描述符泄漏
- 遍历所有连接器(connector)查找已连接的显示器
- 使用第一个可用的显示模式
#### 2. FFmpeg 解码流程
```cpp
// 打开视频文件
avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
// 查找流信息
avformat_find_stream_info(fmt_ctx, nullptr);
// 查找视频流
for (...) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream = i;
}
}
// 创建解码器上下文
codec_ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(codec_ctx, codec_params);
avcodec_open2(codec_ctx, codec, nullptr);
```
- **解码循环**:
```cpp
while (av_read_frame(fmt_ctx, &pkt) >= 0) {
avcodec_send_packet(codec_ctx, &pkt);
while (avcodec_receive_frame(codec_ctx, frame) >= 0) {
// 处理解码后的帧
}
}
```
#### 3. 像素格式转换与显示
```cpp
// 创建缩放上下文
sws_ctx = sws_getCachedContext(...);
// 转换到RGBA格式
sws_scale(sws_ctx, frame->data, frame->linesize,
0, frame->height,
rgb_frame->data, rgb_frame->linesize);
// 显示帧
display_frame(disp, rgb_frame->data[0], width, height);
```
#### 4. DRM 帧缓冲管理
```cpp
// 创建DUMB缓冲区
drmIoctl(disp->fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_dumb);
// 映射内存
disp->map = mmap(0, create_dumb.size,
PROT_READ | PROT_WRITE,
MAP_SHARED,
disp->fd, map_dumb.offset);
// 添加FrameBuffer
drmModeAddFB2(disp->fd, width, height, DRM_FORMAT_XRGB8888,
handles, pitches, offsets, &disp->fb_id, 0);
```
#### 5. 帧率控制
```cpp
// 计算帧间隔
double fps = av_q2d(fmt_ctx->streams[video_stream]->avg_frame_rate);
if (fps > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(
static_cast<int>(1000 / fps)));
}
```
### 编译与运行
#### 编译命令
```bash
g++ -o drm_player main.cpp \
-ldrm \
-lavformat -lavcodec -lswscale -lavutil -lavdevice -lavfilter -lswresample \
-lpthread
```
#### 运行命令(需要root权限)
```bash
sudo ./drm_player video.mp4
```
### 性能优化技巧
1. **硬件加速解码**:
```cpp
// 设置硬件解码器
codec_ctx->get_format = get_hw_format;
codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
// 在回调函数中选择硬件格式
static AVPixelFormat get_hw_format(AVCodecContext* ctx, const AVPixelFormat* pix_fmts) {
for (const AVPixelFormat* p = pix_fmts; *p != AV_PIX_FMT_NONE; p++) {
if (*p == AV_PIX_FMT_DRM_PRIME)
return AV_PIX_FMT_DRM_PRIME;
}
return AV_PIX_FMT_NONE;
}
```
2. **零拷贝显示**:
```cpp
// 使用DRM PRIME直接显示
if (frame->format == AV_PIX_FMT_DRM_PRIME) {
AVDRMFrameDescriptor* desc = (AVDRMFrameDescriptor*)frame->data[0];
uint32_t handles[4] = {0};
uint32_t pitches[4] = {0};
uint32_t offsets[4] = {0};
// 直接从DRM描述符创建FrameBuffer
drmModeAddFB2(disp->fd, frame->width, frame->height,
DRM_FORMAT_NV12, handles, pitches, offsets, &fb_id, 0);
}
```
3. **异步显示**:
```cpp
// 使用drmModePageFlip实现异步翻转
drmModePageFlip(disp->fd, disp->crtc_id, fb_id,
DRM_MODE_PAGE_FLIP_EVENT, &user_data);
// 事件处理循环
drmEventContext evctx = {.version = 2};
drmHandleEvent(disp->fd, &evctx);
```
### 常见问题解决
**问题:权限不足**
```bash
sudo usermod -aG video $USER
# 然后重新登录
```
**问题:不支持的分辨率**
```cpp
// 在create_drm_fb中检查最大分辨率
drmModeCrtcPtr crtc = drmModeGetCrtc(disp->fd, disp->crtc_id);
if (width > crtc->width || height > crtc->height) {
// 缩放视频
}
```
**问题:颜色不正确**
```cpp
// 尝试不同的DRM格式
const uint32_t formats[] = {
DRM_FORMAT_XRGB8888,
DRM_FORMAT_ARGB8888,
DRM_FORMAT_RGB888
};
```
阅读全文
相关推荐



















