NV12数据格式转H265编码格式实现过程

一、需求

在视频处理和传输应用中,将视频数据编码为高效的格式是非常重要的。H.265(也称为HEVC)是一种先进的视频编码标准,具有更好的压缩性能和图像质量,相比于传统的编码标准(如H.264),可以显著减少视频的带宽和存储需求。

NV12是一种常见的视频格式,用于表示YUV图像数据,尤其在实时视频处理中广泛使用。它将亮度(Y)和色度(UV)分量分开存储,其中Y分量占据连续的内存块,而UV分量交错存储在另一个连续的内存块中。

本需求实现将NV12格式的视频数据转换为H.265格式的数据,并将其保存在内存中。这样可以方便地对视频数据进行后续处理,如网络传输、存储或实时流媒体传输等。

为了实现这一需求,使用了C语言和FFmpeg库。FFmpeg是一个强大的开源多媒体处理库,提供了丰富的功能和编解码器,包括H.265编码器。

下面代码实现了如何使用FFmpeg库将NV12格式的视频数据编码为H.265格式的数据,并将其保存在内存中。函数接受NV12数据、宽度和高度作为输入,并返回编码后的H.265数据和数据大小。这样,用户可以方便地将NV12格式的视频数据转换为H.265格式,并在内存中进行进一步处理或传输。同时也提供了文件的方式。

这个功能可以在各种视频处理应用中使用,如视频编辑软件、实时视频流处理系统、视频通信应用等。通过使用H.265编码,可以提高视频传输的效率和质量,减少带宽和存储需求,同时保持良好的视觉体验。

二、NV12和H265格式详细介绍

NV12和H265都是视频编码中经常使用的像素格式,下面分别介绍这两种格式的特点和使用场景。

【1】NV12像素格式

NV12是一种YUV像素格式,常用于视频编码和解码过程中。它是一种planar格式,即Y和UV分量分别存储在不同的平面中。其中,Y分量表示亮度信息,UV分量表示色度信息。在NV12格式中,UV分量的采样率为4:2:0,即每4个Y像素共用一个U和一个V像素。这种采样方式可以有效地减小数据量,同时保持视频质量。

NV12格式的存储顺序为:先存储所有的Y分量,然后存储所有的U和V分量,U和V交错存储。因此,NV12格式的数据大小为(1.5 x 图像宽度 x 图像高度)字节。

NV12格式常用于视频流传输和视频编解码器中,例如在H.264视频编解码器和DirectShow视频开发中都广泛使用。

【2】H265像素格式

H265(又称HEVC)是一种高效的视频编码标准,它可以在相同视频质量的情况下大幅度减小视频文件的大小。H265支持多种像素格式,其中最常用的是YUV 4:2:0和YUV 4:2:2。

YUV 4:2:0格式与NV12类似,也是一种planar格式,其中Y分量存储亮度信息,UV分量采用4:2:0采样方式存储色度信息。YUV 4:2:2格式则采用4:2:2的采样方式存储UV分量,即每2个Y像素共用一个U和一个V像素。

与H264相比,H265的主要改进在于更高的压缩率和更低的比特率,同时保持相同质量的视频输出。因此,H265格式可以在同样的视频质量下使用更低的比特率进行编码,达到更小的文件大小。H265格式常用于网络视频流媒体传输、4K和8K高清视频等领域。

三、代码实现

【1】内存数据处理

要将NV12格式的数据转换为H.265格式的数据并保存在内存中,可以使用FFmpeg库来实现编码操作。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <libavcodec/avcodec.h>
​
// 将NV12数据编码为H.265并保存在内存中
int encodeNV12toH265(uint8_t* nv12Data, int width, int height, uint8_t** h265Data, int* h265Size) {
    int ret = 0;
    AVCodec* codec = NULL;
    AVCodecContext* codecContext = NULL;
    AVFrame* frame = NULL;
    AVPacket* packet = NULL;
​
    // 注册所有的编解码器
    avcodec_register_all();
​
    // 查找H.265编码器
    codec = avcodec_find_encoder(AV_CODEC_ID_H265);
    if (!codec) {
        fprintf(stderr, "找不到H.265编码器\n");
        ret = -1;
        goto cleanup;
    }
​
    // 创建编码器上下文
    codecContext = avcodec_alloc_context3(codec);
    if (!codecContext) {
        fprintf(stderr, "无法分配编码器上下文\n");
        ret = -1;
        goto cleanup;
    }
​
    // 配置编码器参数
    codecContext->width = width;
    codecContext->height = height;
    codecContext->pix_fmt = AV_PIX_FMT_NV12;
    codecContext->codec_id = AV_CODEC_ID_H265;
    codecContext->codec_type = AVMEDIA_TYPE_VIDEO;
    codecContext->time_base.num = 1;
    codecContext->time_base.den = 25; // 假设帧率为25fps
​
    // 打开编码器
    if (avcodec_open2(codecContext, codec, NULL) < 0) {
        fprintf(stderr, "无法打开编码器\n");
        ret = -1;
        goto cleanup;
    }
​
    // 创建输入帧
    frame = av_frame_alloc();
    if (!frame) {
        fprintf(stderr, "无法分配输入帧\n");
        ret = -1;
        goto cleanup;
    }
​
    // 设置输入帧的属性
    frame->format = codecContext->pix_fmt;
    frame->width = codecContext->width;
    frame->height = codecContext->height;
​
    // 分配输入帧的数据缓冲区
    ret = av_frame_get_buffer(frame, 32);
    if (ret < 0) {
        fprintf(stderr, "无法分配输入帧的数据缓冲区\n");
        ret = -1;
        goto cleanup;
    }
​
    // 将NV12数据复制到输入帧的数据缓冲区
    memcpy(frame->data[0], nv12Data, width * height); // Y分量
    memcpy(frame->data[1], nv12Data + width * height, width * height / 2); // UV分量
​
    // 创建输出数据包
    packet = av_packet_alloc();
    if (!packet) {
        fprintf(stderr, "无法分配输出数据包\n");
        ret = -1;
        goto cleanup;
    }
​
    // 发送输入帧给编码器
    ret = avcodec_send_frame(codecContext, frame);
    if (ret < 0) {
        fprintf(stderr, "无法发送输入帧给编码器\n");
        ret = -1;
        goto cleanup;
    }
​
    // 循环从编码器接收输出数据包
    while (ret >= 0) {
        ret = avcodec_receive_packet(codecContext, packet);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            break;
        else if (ret < 0) {
            fprintf(stderr, "从编码器接收输出数据包时发生错误\n");
            ret = -1;
            goto cleanup;
        }
​
        // 分配内存并复制输出数据包的数据
        *h265Data = (uint8_t*)malloc(packet->size);
        if (!*h265Data) {
            fprintf(stderr, "无法分配内存\n");
            ret = -1;
            goto cleanup;
        }
        memcpy(*h265Data, packet->data, packet->size);
        *h265Size = packet->size;
​
        // 释放输出数据包
        av_packet_unref(packet);
    }
​
cleanup:
    // 释放资源
    if (packet)
        av_packet_free(&packet);
    if (frame)
        av_frame_free(&frame);
    if (codecContext)
        avcodec_free_context(&codecContext);
​
    return ret;
}
​

使用以上的封装函数,可以将NV12格式的数据传入函数中,函数会将其编码为H.265格式的数据并保存在内存中。编码后的H.265数据存储在h265Data指针指向的内存中,数据的大小保存在h265Size中。

【2】文件数据处理

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
​
// 将NV12数据编码为H.265格式
int encodeNV12ToH265(const char* nv12FilePath, const char* h265FilePath, int width, int height)
{
    // 注册所有的编解码器
    avcodec_register_all();
​
    // 打开编码器
    AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H265);
    if (!codec) {
        fprintf(stderr, "Failed to find H.265 encoder\n");
        return -1;
    }
​
    AVCodecContext* codecContext = avcodec_alloc_context3(codec);
    if (!codecContext) {
        fprintf(stderr, "Failed to allocate codec context\n");
        return -1;
    }
​
    // 配置编码器参数
    codecContext->width = width;
    codecContext->height = height;
    codecContext->time_base = (AVRational){1, 25};  // 帧率为25fps
    codecContext->framerate = (AVRational){25, 1};
    codecContext->pix_fmt = AV_PIX_FMT_NV12;
​
    // 打开编码器上下文
    if (avcodec_open2(codecContext, codec, NULL) < 0) {
        fprintf(stderr, "Failed to open codec\n");
        return -1;
    }
​
    // 创建输入文件的AVFrame
    AVFrame* frame = av_frame_alloc();
    if (!frame) {
        fprintf(stderr, "Failed to allocate frame\n");
        return -1;
    }
​
    frame->format = codecContext->pix_fmt;
    frame->width = codecContext->width;
    frame->height = codecContext->height;
​
    // 分配输入帧缓冲区
    int ret = av_frame_get_buffer(frame, 32);
    if (ret < 0) {
        fprintf(stderr, "Failed to allocate frame buffer\n");
        return -1;
    }
​
    // 打开输入文件
    FILE* nv12File = fopen(nv12FilePath, "rb");
    if (!nv12File) {
        fprintf(stderr, "Failed to open NV12 file\n");
        return -1;
    }
​
    // 打开输出文件
    FILE* h265File = fopen(h265FilePath, "wb");
    if (!h265File) {
        fprintf(stderr, "Failed to open H.265 file\n");
        return -1;
    }
​
    // 创建编码用的AVPacket
    AVPacket* packet = av_packet_alloc();
    if (!packet) {
        fprintf(stderr, "Failed to allocate packet\n");
        return -1;
    }
​
    int frameCount = 0;
    while (1) {
        // 从输入文件读取NV12数据到AVFrame
        if (fread(frame->data[0], 1, width * height, nv12File) != width * height) {
            break;
        }
        if (fread(frame->data[1], 1, width * height / 2, nv12File) != width * height / 2) {
            break;
        }
​
        frame->pts = frameCount++;  // 设置帧的显示时间戳
​
        // 发送AVFrame到编码器
        ret = avcodec_send_frame(codecContext, frame);
        if (ret < 0) {
            fprintf(stderr, "Error sending frame to codec: %s\n", av_err2str(ret));
            return -1;
        }
​
        // 从编码器接收编码后的数据包
        while (ret >= 0) {
            ret = avcodec_receive_packet(codecContext, packet);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                break;
            } else if (ret < 0) {
                fprintf(stderr, "Error receiving packet from codec: %s\n", av_err2str(ret));
                return -1;
            }
​
            // 将编码后的数据包写入输出文件
            fwrite(packet->data, 1, packet->size, h265File);
            av_packet_unref(packet);
        }
    }
​
    // 刷新编码器
    ret = avcodec_send_frame(codecContext, NULL);
    if (ret < 0) {
        fprintf(stderr, "Error sending flush frame to codec: %s\n", av_err2str(ret));
        return -1;
    }
​
    while (ret >= 0) {
        ret = avcodec_receive_packet(codecContext, packet);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            fprintf(stderr, "Error receiving packet from codec: %s\n", av_err2str(ret));
            return -1;
        }
​
        // 将编码后的数据包写入输出文件
        fwrite(packet->data, 1, packet->size, h265File);
        av_packet_unref(packet);
    }
​
    // 释放资源
    fclose(nv12File);
    fclose(h265File);
    av_frame_free(&frame);
    av_packet_free(&packet);
    avcodec_free_context(&codecContext);
​
    return 0;
}
​
int main()
{
    const char* nv12FilePath = "input.nv12";
    const char* h265FilePath = "output.h265";
    int width = 1920;
    int height = 1080;
​
    int ret = encodeNV12ToH265(nv12FilePath, h265FilePath, width, height);
    if (ret < 0) {
        fprintf(stderr, "Failed to encode NV12 to H.265\n");
        return -1;
    }
​
    printf("Encoding complete\n");
​
    return 0;
}
​

要确保已经正确安装了FFmpeg库,并在编译选项中包含了FFmpeg的头文件和库文件。

需要提供NV12格式的输入文件路径、输出H.265格式文件路径以及图像的宽度和高度。

相关推荐
希望永不加班40 分钟前
Spring AOP 代理模式:CGLIB 与 JDK 动态代理区别
java·开发语言·后端·spring·代理模式
浮游本尊2 小时前
一次合同同步背后的多阶段流水线:从外部主数据到本地歧义消解
后端
lv__pf2 小时前
springboot原理
java·spring boot·后端
段小二2 小时前
服务一重启全丢了——Spring AI Alibaba Agent 三层持久化完整方案
java·后端
UIUV3 小时前
Go语言入门到精通学习笔记
后端·go·编程语言
lizhongxuan3 小时前
开发 Agent 的坑
后端
段小二3 小时前
Agent 自动把机票改错了,推理完全正确——这才是真正的风险
java·后端
itjinyin3 小时前
ShardingSphere-jdbc 5.5.0 + spring boot 基础配置 - 实战篇
java·spring boot·后端
Victor3563 小时前
MongoDB(91)如何在MongoDB中使用TTL索引?
后端