Skip to main content
Version: 1.0.0

四、多媒体应用开发

4.1 概述

本文档所述的多媒体框架是指 TACO SDK 中提供的 TacoCV 库以及以 FFmpeg、OpenCV、OpenBLAS 为代表的开源框架/库。这些开源框架/库的部分核心功能已经在 EA65xx 平台上实现了硬件加速,主要功能覆盖:

  • 视频解码(H.264、H.265)
  • 视频编码(H.264)
  • 静态图像解码(JPEG)
  • 静态图像编码(JPEG)
  • 视频处理
  • 矩阵和数组运算

用户可以根据自己的习惯选择合适的框架/库,从而快速实现产品功能开发。

taCVtaFFmpegtaOpenCV 这几套接口在功能上互有交叉但各有侧重:

  1. taCV 库:提供了所有能用芯片硬件加速的功能接口,包括基于芯片硬件 IP 实现的 JPEG 编解码功能和常用视频处理功能。
  2. taFFmpeg 框架:提供了获取视频流和流转发的协议,除了原有的基于 CPU 算力实现的视频/图像编解码功能之外,还提供了基于芯片硬件能力实现的视频/图像编解码接口。
  3. taOpenCV 框架:除了原有的基于 CPU 算力提供的图像编解码和图像处理功能之外,还通过 taCV 提供了 JPEG 硬件编解码功能和部分视频处理功能。

4.2 视频流输入处理

4.2.1 硬件规格介绍

视频解码器

EA65xx 芯片提供的硬件解码器参数规格为:

  • 支持格式:AVC/H.264 Baseline/Main/High/High10 Profile,SVC-T,HEVC/H.265 Main 10 Profile
  • 分辨率范围:最小 128×128,最大 4096×2160(4K DCI)
  • 解码能力:支持 16 路 FullHD@30fps(或 4 路 4K@30fps)实时解码

视频编码器

EA65xx 芯片提供的硬件编码器参数规格为:

  • 支持格式:H.264 Baseline/Main/High/High 10 Profile
  • 分辨率范围:最小 128×128,最大 2048×1080(2K DCI)
  • 编码能力:支持 2 路 FullHD@60fps 实时编码

静态图像编解码

EA65xx 芯片支持 JPEG Baseline & Progressive 格式的硬件编/解码加速:

  • 最高分辨率:32768×32768
  • 解码能力:不低于 FullHD@480fps
  • 编码能力:不低于 2 路 FullHD@60fps

📝 注意:

  • 对于 JPEG Baseline & Progressive 以外的图片格式(包括 Bmp、Png、JPEG2000 等),OpenCV 接口使用 CPU 算力进行软件编解码。

地址对齐要求

视频编解码器、静态图像编解码器对输入输出 buffer 都有地址对齐要求,buffer 首地址应满足 4KB 对齐

4..2.2 使用 taFFmpeg 拉取视频流

taFFmpeg 支持多种协议和格式的推流和拉流操作。当使用 taFFmpeg 库进行拉流时,需要使用到 libavformatlibavcodec 库中提供的接口。

核心 API 接口

API 接口功能描述
avformat_network_init()初始化网络组件
avformat_open_input()打开输入 URL(如 RTMP、RTSP 地址),获取格式上下文
avformat_find_stream_info()读取数据并解析,获取输入流的详细信息
av_find_best_stream()在输入文件中查找最佳的音频和视频流
avcodec_find_decoder()根据流的编解码器 ID 查找适当的解码器
avcodec_alloc_context3()为解码器创建上下文
avcodec_parameters_to_context()将流参数复制到解码器上下文
avcodec_open2()打开解码器
av_frame_alloc()分配 AVFrame 结构体,用于存储解码后的帧
av_packet_alloc()分配 AVPacket 结构体,用于存储压缩码流数据
av_read_frame()从输入流中读取一个完整的数据包
avcodec_send_packet()将数据包发送到解码器
avcodec_receive_frame()从解码器接收解码后的帧
avcodec_free_context()释放解码器上下文
av_frame_free()释放 AVFrame
av_packet_free()释放 AVPacket
avformat_close_input()关闭输入流并释放相关资源

示例代码(使用 h264_taco 解码器)

以下示例代码使用 "h264_taco" 解码器,通过循环调用 avcodec_send_packet()avcodec_receive_frame() 接口完成对 MP4 视频文件的解码。

#include <iostream>
#include <unistd.h>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/avutil.h>
}

// RTMP 流地址示例
#define RTMP_ADDR "rtmp://127.0.0.1:1935/live/1234"

int main(int argc, char* argv[]) {
int res;
FILE *fp = fopen("result.yuv", "w+b");
if (!fp) {
std::cerr << "Failed to open output file" << std::endl;
return -1;
}

const char* input_file = "./input.mp4"; // 可替换为 RTMP_ADDR

AVFormatContext* format_ctx = nullptr;
res = avformat_open_input(&format_ctx, input_file, nullptr, nullptr);
if (res < 0) {
std::cerr << "Failed to open input file" << std::endl;
fclose(fp);
return -1;
}

res = avformat_find_stream_info(format_ctx, nullptr);
if (res < 0) {
std::cerr << "Failed to find stream info" << std::endl;
avformat_close_input(&format_ctx);
fclose(fp);
return -1;
}

// 检测视频流索引
int video_stream_index = -1;
for (unsigned int i = 0; i < format_ctx->nb_streams; i++) {
if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;
break;
}
}

if (video_stream_index == -1) {
std::cerr << "No video stream found" << std::endl;
avformat_close_input(&format_ctx);
fclose(fp);
return -1;
}

const AVCodec* codec = avcodec_find_decoder_by_name("h264_taco");
if (!codec) {
std::cerr << "Codec h264_taco not found" << std::endl;
avformat_close_input(&format_ctx);
fclose(fp);
return -1;
}

AVCodecContext *codec_ctx = format_ctx->streams[video_stream_index]->codec;
if (!codec_ctx) {
std::cerr << "Failed to get codec context" << std::endl;
avformat_close_input(&format_ctx);
fclose(fp);
return -1;
}

res = avcodec_open2(codec_ctx, codec, nullptr);
if (res < 0) {
std::cerr << "Failed to open codec" << std::endl;
avformat_close_input(&format_ctx);
fclose(fp);
return -1;
}

AVFrame* frame = av_frame_alloc();
AVPacket* packet = av_packet_alloc();
size_t y_size, uv_size;

while (true) {
// 释放未使用的 YUV buffer
if (frame) {
av_frame_free(&frame);
frame = NULL;
}

if (av_read_frame(format_ctx, packet) < 0)
break;

if (packet->stream_index == video_stream_index) {
int response = avcodec_send_packet(codec_ctx, packet);
if (response < 0) {
std::cerr << "Error sending packet for decoding" << std::endl;
break;
}

while (response >= 0) {
// 准备 AVFrame 结构体(不分配 YUV buffer)
if (frame == NULL) {
frame = av_frame_alloc();
}

response = avcodec_receive_frame(codec_ctx, frame);
if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
break;
} else if (response < 0) {
std::cerr << "Error during decoding" << std::endl;
break;
}

y_size = frame->width * frame->height;
uv_size = y_size / 2;
fwrite(frame->data[0], 1, y_size, fp);
fwrite(frame->data[1], 1, uv_size, fp);
}
}

av_packet_unref(packet);
av_free_packet(packet);
}

fclose(fp);
av_frame_free(&frame);
av_packet_free(&packet);
avformat_close_input(&format_ctx);

return 0;
}

代码要点说明

  1. av_read_frame() 接口:内部封装了 buffer 管理行为。当试图读取数据包时,如果 AVPacket 结构体未初始化或大小不足,接口内部会自动调用 av_new_packet() 重新分配数据缓存。SDK 对该接口进行了扩展,使其在媒体域上分配内存,能够同时被 CPU 和编解码硬件访问,每帧可节省一次拷贝操作。
  2. av_frame_alloc() 接口:仅创建结构体所需的存储空间,不分配 YUV buffer。正常情况下,avcodec_receive_frame() 会自动从内部缓存池获取 YUV buffer。特殊情况可显式调用 av_frame_get_buffer() 分配。
  3. API 演进:早期 FFmpeg 使用 avcodec_decode_video2() 接口,从 FFmpeg 3.x 开始推荐使用 avcodec_send_packet()avcodec_receive_frame() 这对新接口。

传统接口示例(已过时)

📝 注意:

  • 以下为使用旧版 avcodec_decode_video2() 接口的示例,仅作参考。
#include <iostream>
#include <unistd.h>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/avutil.h>
}

int main(int argc, char* argv[]) {
// ... 初始化代码与前面示例类似

AVFrame* frame = av_frame_alloc();
AVPacket packet;
int got_picture;
size_t y_size, uv_size;

while (av_read_frame(format_ctx, &packet) >= 0) {
if (packet.stream_index == video_stream_index) {
int len = avcodec_decode_video2(codec_ctx, frame, &got_picture, &packet);

if (len < 0) {
std::cerr << "Error decoding video frame" << std::endl;
break;
}

if (got_picture) {
y_size = frame->width * frame->height;
uv_size = y_size / 2;
fwrite(frame->data[0], 1, y_size, fp);
fwrite(frame->data[1], 1, uv_size, fp);
}
}
av_packet_unref(&packet);
}

// 刷新解码器缓冲区
packet.data = nullptr;
packet.size = 0;
res = avcodec_decode_video2(codec_ctx, frame, &got_picture, &packet);
while (res >= 0 && got_picture) {
y_size = frame->width * frame->height;
uv_size = y_size / 2;
fwrite(frame->data[0], 1, y_size, fp);
fwrite(frame->data[1], 1, uv_size, fp);
res = avcodec_decode_video2(codec_ctx, frame, &got_picture, &packet);
}

// 清理资源
fclose(fp);
av_frame_free(&frame);
avcodec_close(codec_ctx);
avformat_close_input(&format_ctx);

return 0;
}

4.3 图像处理与视觉计算

4.3.1 硬件规格

EA65xx 芯片支持专用的 SPP 硬件,可对图像进行"裁剪/缩放/补边"等视频处理操作。

硬件参数规格

  • 处理流水线:裁切 → 缩放 → 补边 → 输出格式调整(固定顺序)
  • 输入(In0)支持
    • 格式:YUV 类(NV12、I400)、RGB 类(RGB888、ARGB8888)
    • 分辨率:最小 48×48,最大 4096×2160(4K DCI)
    • 备注:RGB 输入时,输出只支持 YUV 类(固定 BT.601 公式)
  • 输出(Out0)支持
    • 格式:YUV 类(NV12、NV21、I400)
    • 分辨率:最小 48×48,最大 4096×2160(4K DCI)
  • 输出(Out1)支持
    • 格式:YUV 类(NV12、NV21、I400)、RGB 类(多种格式)
    • 分辨率:最小 48×48,最大 1920×1080(Full HD)
    • 备注:YUV 输出时支持多种 RGB2YUV 转换矩阵
  • 数据对齐要求
    • 首地址:输入输出 buffer 首地址按 4KB 对齐
    • 行跨距:行跨距按 128 字节对齐

用户可以通过 TacoCV 接口和 OpenCV 接口使用 SPP 提供的功能。

4.3.2 使用 taCV

taCV 提供图像处理 SPP 硬件加速接口,可对图像进行 crop、resize 和格式转换操作。详细描述参考 API 参考的「taCV 接口详解」章节

4.3.3 使用 taOpenCV

OpenCV 集成了硬件加速接口,以下是相关核心接口:

API接口功能描述
cv::resize()对图像进行down scale操作
cv::cvtColor()对图像进行格式转换操作
cv::imread()对jpeg图像进行解码操作
cv::imwrite()对jpeg图像进行编码操作

更多接口详见 API 参考的「taOpenCV 接口说明」章节

数据结构扩展说明

OpenCV内置标准处理的色彩空间为BGR格式,但是很多情况下,对于视频、图片源,直接在YUV色彩空间上处理,可以节省带宽和避免不必要的YUV和RGB之间的互相转换。因此tapencv对于Mat类进行了扩展。

  1. 在taopencv中,我们基于tasys提供了一个内存分配器,以便使用者保证获取连续的内存在物理地址上是连续的,内部接口也会判断Mat的数据存储内存是否连续,来决定是否调用硬件加速接口(硬件只能处理连续内存),如果没有连续内存,则调用CPU加速接口。在mat.create前,可以通过以下代码获取分配器。
mat.allocator = hal::getAllocator();
  1. 在Mat.UMatData中,引入了AVFrame成员,扩展支持各种YUV格式。其中AVFrame的格式定义与FFMPEG中的定义兼容

  2. 在Mat.UMatData中增加了addr和blk_id的定义,分别表示对应的物理内存地址和tasys的内存块ID

示例代码

#include "opencv2/opencv.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"

int main( int argc, const char** argv )
{
std::string filename = "./dog.image.jpg";
Mat image = cv::imread(filename , cv::IMREAD_COLOR_YUV);
if(image.empty())
{
printf("Cannot read image file: %s\n", filename.c_str());
return -1;
}
int out_height = 256;
int out_width = 256;
Mat img_out;
resize(image, image_out, Size(out_width,out_height));

cv::imwrite("./output.jpg", image_out);
return 0;
}

4.4 视频流输出处理

4.4.1 使用 taFFmpeg

FFmpeg 推流是指基于 FFmpeg 工具把本地产生的音视频数据流推送到流媒体服务器上。在生产端,通常会用 FFmpeg 对原始视频数据(如 RGB、YUV 等)进行压缩编码,然后封装成流媒体服务器支持的传输格式(如 RTMP、HLS 等)。

核心 API 接口

API 接口功能描述
avformat_network_init()初始化网络组件
avformat_alloc_output_context2()创建 AVFormatContext 并初始化
avformat_new_stream()为每种媒体类型创建新的输出流
avcodec_find_encoder()查找适当的编码器
avcodec_alloc_context3()为编码器创建上下文
avcodec_parameters_to_context()设置分辨率、帧率、比特率等参数
avcodec_open2()打开编码器并初始化
avcodec_parameters_from_context()从编码器上下文复制参数到流
avio_open()打开输出 URL(如 RTMP、RTSP 地址)
avformat_write_header()写入输出流的头部信息
av_frame_alloc()创建 AVFrame 结构体,用于存储原始帧数据
avcodec_send_frame()发送原始帧到编码器
avcodec_receive_packet()从编码器接收编码后的数据包
av_interleaved_write_frame()将编码后的数据包写入输出流
av_write_trailer()写入输出流的尾部信息
avcodec_free_context()释放编码器上下文
av_frame_free()释放 AVFrame
avio_closep()关闭输出
avformat_free_context()释放 AVFormatContext

示例代码

#include <iostream>
#include <unistd.h>

extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libavdevice/avdevice.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
}

int main() {
int res;

// 1. 初始化 FFmpeg
av_register_all();
avformat_network_init();

// 2. 打开输入媒体文件并读取流头信息
const char* input_url = "/sdcard/1080.mp4";
AVFormatContext *input_format_context = NULL;
res = avformat_open_input(&input_format_context, input_url, NULL, NULL);
if (res < 0) {
std::cerr << "Failed to open input file" << std::endl;
return -1;
}

// 3. 读取数据包以获取流信息(某些流可能没有头部)
res = avformat_find_stream_info(input_format_context, NULL);
if (res < 0) {
std::cerr << "Failed to find stream info" << std::endl;
avformat_close_input(&input_format_context);
return -1;
}

// 4. 打开输出流
const char* output_url = "rtmp://[RTMP服务器地址]/[应用名]/[流名]";
AVFormatContext *output_format_context = NULL;
res = avformat_alloc_output_context2(&output_format_context, NULL, NULL, output_url);
if (res < 0 || !output_format_context) {
std::cerr << "Failed to create output context" << std::endl;
avformat_close_input(&input_format_context);
return -1;
}

if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) {
res = avio_open(&output_format_context->pb, output_url, AVIO_FLAG_WRITE);
if (res < 0) {
std::cerr << "Failed to open output URL" << std::endl;
avformat_free_context(output_format_context);
avformat_close_input(&input_format_context);
return -1;
}
}

// 5. 写入头部信息
res = avformat_write_header(output_format_context, NULL);
if (res < 0) {
std::cerr << "Failed to write header" << std::endl;
avio_closep(&output_format_context->pb);
avformat_free_context(output_format_context);
avformat_close_input(&input_format_context);
return -1;
}

// 6. 循环读取和写入数据包
AVPacket packet;
av_init_packet(&packet);

// 获取输入输出流信息(需要根据实际情况调整)
AVStream* input_stream = input_format_context->streams[0]; // 示例:第一个流
AVStream* output_stream = output_format_context->streams[0]; // 示例:第一个流
int output_stream_index = 0;

while (av_read_frame(input_format_context, &packet) >= 0) {
// 转换时间戳
packet.pts = av_rescale_q(packet.pts, input_stream->time_base, output_stream->time_base);
packet.dts = av_rescale_q(packet.dts, input_stream->time_base, output_stream->time_base);
packet.duration = av_rescale_q(packet.duration, input_stream->time_base, output_stream->time_base);
packet.pos = -1;
packet.stream_index = output_stream_index;

// 写入数据包(压缩码流)到输出文件
res = av_interleaved_write_frame(output_format_context, &packet);
if (res < 0) {
std::cerr << "Error writing packet" << std::endl;
break;
}
av_packet_unref(&packet);
}

// 7. 写入尾部信息
av_write_trailer(output_format_context);

// 8. 清理资源
avformat_close_input(&input_format_context);
if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) {
avio_closep(&output_format_context->pb);
}
avformat_free_context(output_format_context);

return 0;
}