文章目录
1.使用命令行播放YUV数据
使用 FFmpeg 的命令行工具 ffplay
可以很方便地播放 YUV 文件。但是不同于MP4或MKV格式文件,YUV 存储的原始的信息,需要指定分辨率参数和数据格式。
以下是一个典型的命令及其参数解析:
ffplay -pix_fmt nv12 -s 1920*1080 input_test.yuv
1.1命令解析
ffplay
:这是 FFmpeg 的播放工具,用于播放音视频文件和流。-f rawvideo
:指定输入文件格式为原始视频数据(未压缩)。-pixel_format yuv420p
:指定像素格式为 YUV 4:2:0 平面格式(planar)。YUV420p 是一种常见的 YUV 格式,其中 Y、U、V 分量分别存储在独立的平面上。-video_size 1920x1080
:指定视频的宽度和高度。这个参数对于原始视频数据非常重要,因为它没有头文件来存储这些信息。input.yuv
:这是输入文件的名称。
1.2参数说明
-f rawvideo
:- 表示输入文件是原始视频数据,FFmpeg 不会自动检测文件格式,需要显式指定。
-pixel_format yuv420p
:yuv420p
表示 YUV 4:2:0 平面格式,Y、U、V 分量分别存储在独立的平面上。- 常见的像素格式还有
nv12
(UV 分量交织存储)等。
-video_size 1920x1080
:- 指定视频的分辨率(宽度 x 高度)。
- 必须准确指定,否则播放时会出现图像错位或播放失败。
2.使用C语言实现将YUV数据转为JPEG图片格式
2.1需求分析
项目需求:从camera sensor传出来的数据为一帧帧的NV12格式数据,需要将每帧数据转为JPEG图片。
分析转换流程:
其中如果一开始的YUV数据本身已经为平面格式的话,无需进行转换,直接可以在初始化完之后,进行编码
MJPEG支持主流的平面格式,如果是打包格式,则需要进行转换为平面格式
AV_PIX_FMT_YUVJ420P,
AV_PIX_FMT_YUVJ422P,
AV_PIX_FMT_YUVJ444P,
2.2读取YUV源文件
int readYUVFile(const char *filename, int width, int height, unsigned char *&originalData)
{
// 读取YUV 文件
FILE *file = fopen(filename, "rb");
if (!file)
{
cerr << "Failed to open file." << endl;
return -1;
}
size_t originalSize = width * height * 3 / 2; // YUV420 8-bit 1.5 bytes per pixel
originalData = new unsigned char[originalSize];
// 读取原始数据 这里是读取一帧, 如果要多帧数据,则使用循环
if (fread(originalData, sizeof(unsigned char), originalSize, file) != originalSize)
{
cerr << "Failed to read data." << endl;
delete[] originalData;
fclose(file);
return -1;
}
fclose(file);
return 0;
}
这里是读取了一帧,因为我的文件本身只有一帧数据,如果是多帧数据,那么需要考虑使用while循环
2.3将YUV数据封装为AVFrame
// 将一帧YUV数据 封装成AVFrame
int yuv_dataToAVFrame(unsigned char *originalData)
{
original_frame = av_frame_alloc();
if (!original_frame)
{
cerr << "Failed to allocate AVFrame." << std::endl;
return -1;
}
// 设置frame 参数
original_frame->width = 1920;
original_frame->height = 1080;
original_frame->format = AV_PIX_FMT_NV12;
if (av_frame_get_buffer(original_frame, 0) < 0)
{
cerr << "Failed to alloc frame data" << endl;
av_frame_free(&original_frame);
return -1;
}
// copy data to frame
int ret = av_image_fill_arrays(original_frame->data, original_frame->linesize, originalData, AV_PIX_FMT_NV12, 1920, 1080, 1);
if (ret < 0)
{
std::cerr << "Failed to fill frame data." << std::endl;
av_frame_free(&original_frame);
return -1;
}
}
Frame不仅仅存放了原数据,还需要指定一些参数,宽度和高度,编码格式,
2.4将NV12 转换为YUV420平面格式
int nv12_to_yuv420p( AVFrame *&frame, AVFrame *&nv12_frame,int width,int height,const unsigned char *nv12_data){
frame = av_frame_alloc();
if (!frame) {
std::cerr << "Failed to allocate AVFrame." << std::endl;
return -1;
}
frame->format = AV_PIX_FMT_YUV420P;
frame->width = width;
frame->height = height;
av_image_alloc(frame->data, frame->linesize, width, height, AV_PIX_FMT_YUV420P, 1);
nv12_frame = av_frame_alloc();
if (!nv12_frame) {
std::cerr << "Failed to allocate AVFrame for NV12." << std::endl;
av_frame_free(&frame);
return -1;
}
nv12_frame->format = AV_PIX_FMT_NV12;
nv12_frame->width = width;
nv12_frame->height = height;
av_image_fill_arrays(nv12_frame->data, nv12_frame->linesize, nv12_data, AV_PIX_FMT_NV12, width, height, 1);
struct SwsContext *sws_ctx = sws_getContext(width, height, AV_PIX_FMT_NV12, width, height, AV_PIX_FMT_YUV420P, 0, nullptr, nullptr, nullptr);
if (!sws_ctx) {
std::cerr << "Could not create SwsContext." << std::endl;
av_frame_free(&nv12_frame);
av_frame_free(&frame);
return -1;
}
cout<<"scale begin"<<endl;
sws_scale(sws_ctx, nv12_frame->data, nv12_frame->linesize, 0, height, frame->data, frame->linesize);
sws_freeContext(sws_ctx);
cout<<"scale endl"<<endl;
}
如果数据本身已经是平面格式,无需这个步骤
2.5初始化MJPEG编码器
int init_mjpeg_encoder(AVFrame *frame)
{
// 寻找编码器
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
if (!codec)
{
std::cerr << "Codec not found" << std::endl;
return -1;
}
// 分配编码器上下文
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx)
{
std::cerr << "Could not allocate video codec context" << std::endl;
return -1;
}
// 配置编码器参数
codec_ctx->bit_rate = 400000;
codec_ctx->width = frame->width;
codec_ctx->height = frame->height;
codec_ctx->time_base = {1, 25};
codec_ctx->pix_fmt = AV_PIX_FMT_YUVJ420P; //
// 打开编码器
if (avcodec_open2(codec_ctx, codec, nullptr) < 0)
{
std::cerr << "Could not open codec" << std::endl;
avcodec_free_context(&codec_ctx);
return -1;
}
return 0;
}
这里需注意:设定编码器上下文支持的格式,而且要与Frame的格式对应,为平面格式,否则会编码失败
2.6将YUV420P编码为JPEG
int frame_encode(AVFrame *frame)
{
//初始化数据包
packet = av_packet_alloc();
if (!packet)
{
std::cerr << "Could not allocate AVPacket" << std::endl;
avcodec_free_context(&codec_ctx);
return -1;
}
//发送原始数据帧
if(!codec_ctx){
cout<<"codec_ctx is null"<<endl;
return -1;
}
if(!frame){
cout<<"frame is null"<<endl;
return -1;
}
int ret = avcodec_send_frame(codec_ctx,frame);
if(ret<0){
std::cerr << "Error sending frame to codec context" << std::endl;
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
return -1;
}
// 接收编码后的数据帧
ret = avcodec_receive_packet(codec_ctx, packet);
if (ret < 0) {
std::cerr << "Error receiving packet from codec context" << std::endl;
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
return -1;
}
return 0;
}
这里仅针对一帧完成了编码,多帧请考虑使用循环
2.7将编码数据写入图片文件
int save_frame_as_jpeg(const char *filename){
std::ofstream outfile(filename, std::ios::out | std::ios::binary);
if (!outfile.is_open()) {
std::cerr << "Could not open output file" << std::endl;
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
return -1;
}
outfile.write(reinterpret_cast<const char *>(packet->data), packet->size);
outfile.close();
return 0;
}
2.8完整代码
头文件 hpp
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif
extern "C"
{
#include <stdio.h>
#include "libavutil/avutil.h"
#include "libavdevice/avdevice.h"
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include <libswscale/swscale.h>
#include "libswresample/swresample.h"
#include "libavutil/imgutils.h"
}
#include <fstream>
#include <iostream>
using namespace std;
AVFormatContext *format_ctx;
AVPacket *packet;
AVFrame *original_frame,*new_frame;
AVCodec *codec;
AVCodecContext *codec_ctx;
c++实现
#include "readYUV.hpp"
int readYUVFile(const char *filename, int width, int height, unsigned char *&originalData)
{
// 读取YUV 文件
FILE *file = fopen(filename, "rb");
if (!file)
{
cerr << "Failed to open file." << endl;
return -1;
}
size_t originalSize = width * height * 3 / 2; // YUV420 8-bit 1.5 bytes per pixel
originalData = new unsigned char[originalSize];
// 读取原始数据 这里是读取一帧, 如果要多帧数据,则使用循环
if (fread(originalData, sizeof(unsigned char), originalSize, file) != originalSize)
{
cerr << "Failed to read data." << endl;
delete[] originalData;
fclose(file);
return -1;
}
fclose(file);
return 0;
}
// 将一帧YUV数据 封装成AVFrame
int yuv_dataToAVFrame(unsigned char *originalData)
{
original_frame = av_frame_alloc();
if (!original_frame)
{
cerr << "Failed to allocate AVFrame." << std::endl;
return -1;
}
// 设置frame 参数
original_frame->width = 1920;
original_frame->height = 1080;
original_frame->format = AV_PIX_FMT_NV12;
if (av_frame_get_buffer(original_frame, 0) < 0)
{
cerr << "Failed to alloc frame data" << endl;
av_frame_free(&original_frame);
return -1;
}
// copy data to frame
int ret = av_image_fill_arrays(original_frame->data, original_frame->linesize, originalData, AV_PIX_FMT_NV12, 1920, 1080, 1);
if (ret < 0)
{
std::cerr << "Failed to fill frame data." << std::endl;
av_frame_free(&original_frame);
return -1;
}
}
// 初始化 MJPEG编码器
int init_mjpeg_encoder(AVFrame *frame)
{
// 寻找编码器
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
if (!codec)
{
std::cerr << "Codec not found" << std::endl;
return -1;
}
// 分配编码器上下文
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx)
{
std::cerr << "Could not allocate video codec context" << std::endl;
return -1;
}
// 配置编码器参数
codec_ctx->bit_rate = 400000;
codec_ctx->width = frame->width;
codec_ctx->height = frame->height;
codec_ctx->time_base = {1, 25};
codec_ctx->pix_fmt = AV_PIX_FMT_YUVJ420P; //
// 打开编码器
if (avcodec_open2(codec_ctx, codec, nullptr) < 0)
{
std::cerr << "Could not open codec" << std::endl;
avcodec_free_context(&codec_ctx);
return -1;
}
return 0;
}
// 对一帧 frame 进行编码
int frame_encode(AVFrame *frame)
{
//初始化数据包
packet = av_packet_alloc();
if (!packet)
{
std::cerr << "Could not allocate AVPacket" << std::endl;
avcodec_free_context(&codec_ctx);
return -1;
}
//发送原始数据帧
if(!codec_ctx){
cout<<"codec_ctx is null"<<endl;
return -1;
}
if(!frame){
cout<<"frame is null"<<endl;
return -1;
}
int ret = avcodec_send_frame(codec_ctx,frame);
if(ret<0){
std::cerr << "Error sending frame to codec context" << std::endl;
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
return -1;
}
// 接收编码后的数据帧
ret = avcodec_receive_packet(codec_ctx, packet);
if (ret < 0) {
std::cerr << "Error receiving packet from codec context" << std::endl;
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
return -1;
}
return 0;
}
// 将 NV12 数据帧 转换为 YUV420P 数据帧 yuv420sp ---->yuv420p 从打包格式转换为平面格式
int nv12_to_yuv420p( AVFrame *&frame, AVFrame *&nv12_frame,int width,int height,const unsigned char *nv12_data){
frame = av_frame_alloc();
if (!frame) {
std::cerr << "Failed to allocate AVFrame." << std::endl;
return -1;
}
frame->format = AV_PIX_FMT_YUV420P;
frame->width = width;
frame->height = height;
av_image_alloc(frame->data, frame->linesize, width, height, AV_PIX_FMT_YUV420P, 1);
nv12_frame = av_frame_alloc();
if (!nv12_frame) {
std::cerr << "Failed to allocate AVFrame for NV12." << std::endl;
av_frame_free(&frame);
return -1;
}
nv12_frame->format = AV_PIX_FMT_NV12;
nv12_frame->width = width;
nv12_frame->height = height;
av_image_fill_arrays(nv12_frame->data, nv12_frame->linesize, nv12_data, AV_PIX_FMT_NV12, width, height, 1);
struct SwsContext *sws_ctx = sws_getContext(width, height, AV_PIX_FMT_NV12, width, height, AV_PIX_FMT_YUV420P, 0, nullptr, nullptr, nullptr);
if (!sws_ctx) {
std::cerr << "Could not create SwsContext." << std::endl;
av_frame_free(&nv12_frame);
av_frame_free(&frame);
return -1;
}
cout<<"scale begin"<<endl;
sws_scale(sws_ctx, nv12_frame->data, nv12_frame->linesize, 0, height, frame->data, frame->linesize);
sws_freeContext(sws_ctx);
cout<<"scale endl"<<endl;
}
int save_frame_as_jpeg(const char *filename){
std::ofstream outfile(filename, std::ios::out | std::ios::binary);
if (!outfile.is_open()) {
std::cerr << "Could not open output file" << std::endl;
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
return -1;
}
outfile.write(reinterpret_cast<const char *>(packet->data), packet->size);
outfile.close();
return 0;
}
int main()
{
const char *filename = "input_test.yuv";
unsigned char *originalData = nullptr;
// 将读取的内存数据 赋值给originalData
int ret = readYUVFile(filename, 1920, 1080, originalData);
if (ret < 0)
{
cout << "open file fail " << endl;
return -1;
}
// 将yuvdata 转化为 frame
yuv_dataToAVFrame(originalData);
//originalFrame 转化为 yuv420p frame
nv12_to_yuv420p(new_frame,original_frame,1920,1080,originalData);
// 打开编码器
ret = init_mjpeg_encoder(new_frame);
if (ret < 0)
{
cout << "open mjpeg encoder fail" << endl;
return -1;
}
//编码
ret = frame_encode(new_frame);
if(ret <0){
cout <<"encode fail"<<endl;
return -1;
}
//输出文件
ret = save_frame_as_jpeg("output.jpeg");
if(ret <0){
cout <<"save jpeg fail"<<endl;
return -1;
}
cout<<"yuv data susccess convert jpeg!"<<endl;
//收尾工作
av_packet_free(&packet);
avcodec_free_context(&codec_ctx);
delete[] originalData;
av_frame_free(&original_frame);
av_frame_free(&new_frame);
return 0;
}