788 lines
26 KiB
C++
788 lines
26 KiB
C++
#include "CVrFFMediaPusher.h"
|
||
#include "VrError.h"
|
||
#include "VrLog.h"
|
||
|
||
#include <algorithm>
|
||
#include <chrono>
|
||
#include <cstring>
|
||
#include <limits>
|
||
|
||
#if !defined(FFMEDIA_PLATFORM_DUMMY)
|
||
#include <linux/videodev2.h>
|
||
#endif
|
||
|
||
// Rockchip FFmpeg 分支要求应用层提供 ff_log_level 全局符号(定义于此)
|
||
// main.cpp 中 extern 声明并运行时设 0(仅 ERROR),此处提供定义默认 0
|
||
#if !defined(FFMEDIA_PLATFORM_DUMMY)
|
||
extern "C" {
|
||
unsigned int ff_log_level = 0;
|
||
|
||
// rkmppdec.c 中引用的 MPP 内部辅助函数,未随 .a 提供
|
||
// 返回编码类型对应的字符串名称,此处返回占位值
|
||
const char* querystring(MppCodingType /*coding*/)
|
||
{
|
||
return "unknown";
|
||
}
|
||
}
|
||
#endif
|
||
|
||
bool IVrFFMediaPusher::CreateObject(IVrFFMediaPusher** ppPusher)
|
||
{
|
||
if (!ppPusher) return false;
|
||
try { *ppPusher = new CVrFFMediaPusher(); return true; }
|
||
catch (...) { return false; }
|
||
}
|
||
|
||
CVrFFMediaPusher::CVrFFMediaPusher() = default;
|
||
|
||
CVrFFMediaPusher::~CVrFFMediaPusher()
|
||
{
|
||
try { UnInit(); } catch (...) {}
|
||
}
|
||
|
||
#if defined(FFMEDIA_PLATFORM_DUMMY)
|
||
|
||
// =================== Windows / 非 RK3588 占位实现 ===================
|
||
// 实际编码/推流仅在 RK3588 + MPP + FFmpeg-rockchip 下可用
|
||
// Windows 仅保证编译通过和接口存在
|
||
|
||
int CVrFFMediaPusher::Init(const VrFFPushConfig& config)
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_mutex);
|
||
m_config = config;
|
||
m_bInited = true;
|
||
LOG_INFO("FFMediaPusher: Init (dummy on this platform)\n");
|
||
return SUCCESS;
|
||
}
|
||
|
||
int CVrFFMediaPusher::Start()
|
||
{
|
||
if (!m_bInited.load()) return ERR_CODE(DEV_NO_OPEN);
|
||
m_bStarted = true;
|
||
LOG_INFO("FFMediaPusher: Start (dummy)\n");
|
||
return SUCCESS;
|
||
}
|
||
|
||
int CVrFFMediaPusher::PushFrame(const void* /*data*/, size_t /*size*/, int64_t /*ptsUs*/)
|
||
{
|
||
return m_bStarted.load() ? SUCCESS : ERR_CODE(DEV_NO_OPEN);
|
||
}
|
||
|
||
int CVrFFMediaPusher::Stop()
|
||
{
|
||
m_bStarted = false;
|
||
return SUCCESS;
|
||
}
|
||
|
||
int CVrFFMediaPusher::UnInit()
|
||
{
|
||
Stop();
|
||
m_bInited = false;
|
||
return SUCCESS;
|
||
}
|
||
|
||
#else
|
||
|
||
// =================== RK3588 真正实现 MPP + FFmpeg-rockchip ===================
|
||
|
||
// ---- 辅助函数 ----
|
||
|
||
uint32_t CVrFFMediaPusher::AlignTo(uint32_t value, uint32_t align)
|
||
{
|
||
return (value + align - 1) & ~(align - 1);
|
||
}
|
||
|
||
MppCodingType CVrFFMediaPusher::ToMppCoding(EFFEncodeType t)
|
||
{
|
||
switch (t)
|
||
{
|
||
case EFFEncodeType::H264: return MPP_VIDEO_CodingAVC;
|
||
case EFFEncodeType::H265: return MPP_VIDEO_CodingHEVC;
|
||
case EFFEncodeType::MJPEG: return MPP_VIDEO_CodingMJPEG;
|
||
}
|
||
return MPP_VIDEO_CodingAVC;
|
||
}
|
||
|
||
int CVrFFMediaPusher::ToAvPixelFormat(EFFPushPixelFormat fmt)
|
||
{
|
||
switch (fmt)
|
||
{
|
||
case EFFPushPixelFormat::NV12: return AV_PIX_FMT_NV12;
|
||
case EFFPushPixelFormat::YUV420P: return AV_PIX_FMT_YUV420P;
|
||
case EFFPushPixelFormat::BGR24: return AV_PIX_FMT_BGR24;
|
||
case EFFPushPixelFormat::RGB24: return AV_PIX_FMT_RGB24;
|
||
case EFFPushPixelFormat::BGRA32: return AV_PIX_FMT_BGRA;
|
||
case EFFPushPixelFormat::RGBA32: return AV_PIX_FMT_RGBA;
|
||
}
|
||
return AV_PIX_FMT_BGR24;
|
||
}
|
||
|
||
// ---- MPP 编码器初始化 ----
|
||
|
||
int CVrFFMediaPusher::initMppEncoder()
|
||
{
|
||
// 1) 创建 MPP 上下文
|
||
MPP_RET ret = mpp_create(&m_mppCtx, &m_mppApi);
|
||
if (ret != MPP_SUCCESS || !m_mppCtx || !m_mppApi)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: mpp_create fail %d\n", ret);
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
}
|
||
|
||
MppCodingType coding = ToMppCoding(m_config.encodeType);
|
||
|
||
RK_S64 timeout = 1000;
|
||
ret = m_mppApi->control(m_mppCtx, MPP_SET_INPUT_TIMEOUT, &timeout);
|
||
if (ret != MPP_SUCCESS)
|
||
LOG_WARN("FFMediaPusher: MPP_SET_INPUT_TIMEOUT fail %d\n", ret);
|
||
ret = m_mppApi->control(m_mppCtx, MPP_SET_OUTPUT_TIMEOUT, &timeout);
|
||
if (ret != MPP_SUCCESS)
|
||
LOG_WARN("FFMediaPusher: MPP_SET_OUTPUT_TIMEOUT fail %d\n", ret);
|
||
|
||
ret = mpp_init(m_mppCtx, MPP_CTX_ENC, coding);
|
||
if (ret != MPP_SUCCESS)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: mpp_init fail %d\n", ret);
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
}
|
||
|
||
// 2) 创建编码器配置
|
||
MppEncCfg cfg = nullptr;
|
||
ret = mpp_enc_cfg_init(&cfg);
|
||
if (ret != MPP_SUCCESS || !cfg)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: mpp_enc_cfg_init fail %d\n", ret);
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
}
|
||
|
||
// prep 参数(输入帧格式)
|
||
mpp_enc_cfg_set_s32(cfg, "prep:width", static_cast<RK_S32>(m_encWidth));
|
||
mpp_enc_cfg_set_s32(cfg, "prep:height", static_cast<RK_S32>(m_encHeight));
|
||
mpp_enc_cfg_set_s32(cfg, "prep:hor_stride", static_cast<RK_S32>(m_encHorStride));
|
||
mpp_enc_cfg_set_s32(cfg, "prep:ver_stride", static_cast<RK_S32>(m_encVerStride));
|
||
mpp_enc_cfg_set_s32(cfg, "prep:format", static_cast<RK_S32>(MPP_FMT_YUV420SP));
|
||
|
||
// rc 参数(码率控制)
|
||
MppEncRcMode rcMode = (m_config.rcMode == EFFRcMode::CBR)
|
||
? MPP_ENC_RC_MODE_CBR : MPP_ENC_RC_MODE_VBR;
|
||
const RK_S32 bpsTarget = static_cast<RK_S32>(m_config.bitrateKbps) * 1000;
|
||
// 码率上下界:尽量减少网络带宽 —— CBR 收紧到 ±6%(严格守住预算,避免超发);
|
||
// VBR 放开下界(简单画面省码率),上界仍限制在 +6% 防止突发占满带宽。
|
||
RK_S32 bpsMax, bpsMin;
|
||
if (rcMode == MPP_ENC_RC_MODE_CBR)
|
||
{
|
||
bpsMax = bpsTarget * 17 / 16; // +6.25%
|
||
bpsMin = bpsTarget * 15 / 16; // -6.25%
|
||
}
|
||
else // VBR
|
||
{
|
||
bpsMax = bpsTarget * 17 / 16; // +6.25%
|
||
bpsMin = bpsTarget * 1 / 16; // 简单画面大幅降码率
|
||
}
|
||
mpp_enc_cfg_set_s32(cfg, "rc:mode", static_cast<RK_S32>(rcMode));
|
||
mpp_enc_cfg_set_s32(cfg, "rc:bps_target", bpsTarget);
|
||
mpp_enc_cfg_set_s32(cfg, "rc:bps_max", bpsMax);
|
||
mpp_enc_cfg_set_s32(cfg, "rc:bps_min", bpsMin);
|
||
mpp_enc_cfg_set_s32(cfg, "rc:fps_in_num", static_cast<RK_S32>(m_config.fps));
|
||
mpp_enc_cfg_set_s32(cfg, "rc:fps_in_denom", 1);
|
||
mpp_enc_cfg_set_s32(cfg, "rc:fps_out_num", static_cast<RK_S32>(m_config.fps));
|
||
mpp_enc_cfg_set_s32(cfg, "rc:fps_out_denom", 1);
|
||
mpp_enc_cfg_set_s32(cfg, "rc:gop", static_cast<RK_S32>(m_config.gop));
|
||
// 注:本 MPP 构建不支持 "rc:quality" 配置项(会报 invalid),CBR 下也不需要
|
||
|
||
// codec 参数
|
||
mpp_enc_cfg_set_s32(cfg, "codec:type", static_cast<RK_S32>(coding));
|
||
|
||
if (coding == MPP_VIDEO_CodingAVC)
|
||
{
|
||
// H.264: High Profile, Level 4.2(1080p@60 兼容)
|
||
mpp_enc_cfg_set_s32(cfg, "h264:profile", 100);
|
||
mpp_enc_cfg_set_s32(cfg, "h264:level", 42);
|
||
}
|
||
else if (coding == MPP_VIDEO_CodingHEVC)
|
||
{
|
||
// H.265: Main Profile, Level 5.1
|
||
mpp_enc_cfg_set_s32(cfg, "h265:profile", 0); // Main
|
||
mpp_enc_cfg_set_s32(cfg, "h265:level", 51);
|
||
}
|
||
|
||
// 3) 应用编码器配置
|
||
ret = m_mppApi->control(m_mppCtx, MPP_ENC_SET_CFG, cfg);
|
||
mpp_enc_cfg_deinit(cfg);
|
||
if (ret != MPP_SUCCESS)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: MPP_ENC_SET_CFG fail %d\n", ret);
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
}
|
||
|
||
// 3.1) 每个 IDR 帧都内嵌 SPS/PPS(in-band header),
|
||
// 这样 VLC / ffplay 等客户端中途接入时能立即拿到参数集开始解码,
|
||
// 不依赖 SDP(RTSP listen 模式下对兼容性很关键)。仅对 H.264/H.265 有意义。
|
||
if (coding == MPP_VIDEO_CodingAVC || coding == MPP_VIDEO_CodingHEVC)
|
||
{
|
||
MppEncHeaderMode headerMode = MPP_ENC_HEADER_MODE_EACH_IDR;
|
||
m_mppApi->control(m_mppCtx, MPP_ENC_SET_HEADER_MODE, &headerMode);
|
||
}
|
||
|
||
// 4) 创建 MppBufferGroup(用于分配编码输入帧缓冲区)
|
||
ret = mpp_buffer_group_get_internal(&m_mppBufGroup, MPP_BUFFER_TYPE_DRM);
|
||
if (ret != MPP_SUCCESS)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: buffer group get fail %d\n", ret);
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
}
|
||
|
||
return SUCCESS;
|
||
}
|
||
|
||
// ---- RTMP 输出初始化 ----
|
||
|
||
int CVrFFMediaPusher::initRtspOutput()
|
||
{
|
||
// 1) 构建 RTMP URL: 主动推流到 mediamtx (rtmp://{ip}:{port}{path})
|
||
// RTMP 比 RTSP 更稳定,避免 interleaved frame 等兼容性问题
|
||
char url[256];
|
||
snprintf(url, sizeof(url), "rtmp://%s:%u%s",
|
||
m_config.rtspHost.empty() ? "127.0.0.1" : m_config.rtspHost.c_str(),
|
||
m_config.rtspPort, m_config.rtspPath.c_str());
|
||
|
||
// 2) 创建 RTMP 输出上下文(格式改为 flv,RTMP 基于 FLV 容器)
|
||
int ret = avformat_alloc_output_context2(&m_fmtCtx, nullptr, "flv", url);
|
||
if (ret < 0 || !m_fmtCtx)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: avformat_alloc_output_context2 fail %d\n", ret);
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
}
|
||
m_fmtCtx->flags |= AVFMT_FLAG_FLUSH_PACKETS;
|
||
m_fmtCtx->max_delay = 0;
|
||
m_fmtCtx->max_interleave_delta = 0;
|
||
av_opt_set_int(m_fmtCtx, "flush_packets", 1, 0);
|
||
|
||
// 3) 创建视频流
|
||
AVStream* stream = avformat_new_stream(m_fmtCtx, nullptr);
|
||
if (!stream)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: avformat_new_stream fail\n");
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
}
|
||
stream->id = 0;
|
||
|
||
// 4) 设置 codecpar(从 MPP 编码器获取 extradata SPS/PPS)
|
||
AVCodecParameters* codecpar = stream->codecpar;
|
||
MppCodingType coding = ToMppCoding(m_config.encodeType);
|
||
codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||
|
||
if (coding == MPP_VIDEO_CodingAVC)
|
||
codecpar->codec_id = AV_CODEC_ID_H264;
|
||
else if (coding == MPP_VIDEO_CodingHEVC)
|
||
codecpar->codec_id = AV_CODEC_ID_H265;
|
||
else
|
||
codecpar->codec_id = AV_CODEC_ID_MJPEG;
|
||
|
||
codecpar->width = m_encWidth;
|
||
codecpar->height = m_encHeight;
|
||
codecpar->format = AV_PIX_FMT_YUV420P; // NV12 对应的 FFmpeg 格式
|
||
codecpar->bit_rate = static_cast<int64_t>(m_config.bitrateKbps) * 1000; // RTMP muxer 需要知道码率
|
||
|
||
// 设置流时基为微秒(PushFrame 以微秒传入 PTS,写帧时再 rescale 到 muxer 实际时基)
|
||
stream->time_base = AVRational{1, 1000000};
|
||
stream->avg_frame_rate = AVRational{static_cast<int>(m_config.fps), 1};
|
||
|
||
// 获取 SPS/PPS 头:MPP_ENC_GET_HDR_SYNC 需要传入【已分配缓冲区】的 MppPacket,
|
||
// 直接传 &nullptr 会触发 mpp_packet_copy invalid input,extradata 取不到。
|
||
char hdrBuf[1024];
|
||
MppPacket hdrPkt = nullptr;
|
||
if (mpp_packet_init(&hdrPkt, hdrBuf, sizeof(hdrBuf)) == MPP_OK && hdrPkt)
|
||
{
|
||
mpp_packet_set_length(hdrPkt, 0);
|
||
ret = m_mppApi->control(m_mppCtx, MPP_ENC_GET_HDR_SYNC, hdrPkt);
|
||
if (ret == MPP_SUCCESS)
|
||
{
|
||
void* hdrData = mpp_packet_get_data(hdrPkt);
|
||
size_t hdrSize = mpp_packet_get_length(hdrPkt);
|
||
if (hdrData && hdrSize > 0)
|
||
{
|
||
codecpar->extradata = static_cast<uint8_t*>(av_mallocz(hdrSize + AV_INPUT_BUFFER_PADDING_SIZE));
|
||
if (codecpar->extradata)
|
||
{
|
||
memcpy(codecpar->extradata, hdrData, hdrSize);
|
||
codecpar->extradata_size = static_cast<int>(hdrSize);
|
||
}
|
||
}
|
||
LOG_INFO("FFMediaPusher: got extradata %d bytes\n",
|
||
static_cast<int>(codecpar->extradata_size));
|
||
}
|
||
else
|
||
{
|
||
LOG_WARN("FFMediaPusher: MPP_ENC_GET_HDR_SYNC fail %d\n", ret);
|
||
}
|
||
mpp_packet_deinit(&hdrPkt); // 外部缓冲区 hdrBuf 不会被释放(栈内存)
|
||
}
|
||
|
||
return SUCCESS;
|
||
}
|
||
|
||
// ---- 格式转换初始化 ----
|
||
|
||
int CVrFFMediaPusher::initSwscale()
|
||
{
|
||
m_needConvert = (m_config.pixelFormat != EFFPushPixelFormat::NV12);
|
||
|
||
if (!m_needConvert)
|
||
return SUCCESS;
|
||
|
||
int srcW = static_cast<int>(m_config.width);
|
||
int srcH = static_cast<int>(m_config.height);
|
||
int srcFmt = ToAvPixelFormat(m_config.pixelFormat);
|
||
int dstW = static_cast<int>(m_encWidth);
|
||
int dstH = static_cast<int>(m_encHeight);
|
||
|
||
m_swsCtx = sws_getContext(srcW, srcH, static_cast<AVPixelFormat>(srcFmt),
|
||
dstW, dstH, AV_PIX_FMT_NV12,
|
||
SWS_BILINEAR, nullptr, nullptr, nullptr);
|
||
if (!m_swsCtx)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: sws_getContext fail\n");
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
}
|
||
|
||
LOG_INFO("FFMediaPusher: swscale enabled (pix %d → NV12, %dx%d → %dx%d)\n",
|
||
srcFmt, srcW, srcH, dstW, dstH);
|
||
return SUCCESS;
|
||
}
|
||
|
||
// ---- 主流程 ----
|
||
|
||
int CVrFFMediaPusher::Init(const VrFFPushConfig& config)
|
||
{
|
||
std::lock_guard<std::mutex> lk(m_mutex);
|
||
if (m_bInited.load()) return SUCCESS;
|
||
|
||
m_config = config;
|
||
|
||
// 计算编码器 stride(16 对齐)
|
||
m_encWidth = static_cast<int>(config.width);
|
||
m_encHeight = static_cast<int>(config.height);
|
||
m_encHorStride = static_cast<int>(AlignTo(config.width, 16));
|
||
m_encVerStride = static_cast<int>(AlignTo(config.height, 16));
|
||
|
||
// 初始化 FFmpeg 网络 + 降低 FFmpeg 日志噪声(仅 ERROR)
|
||
avformat_network_init();
|
||
av_log_set_level(AV_LOG_ERROR);
|
||
|
||
// 初始化 MPP 编码器
|
||
int ret = initMppEncoder();
|
||
if (ret != SUCCESS)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: initMppEncoder fail\n");
|
||
UnInit();
|
||
return ret;
|
||
}
|
||
|
||
// 初始化 RTSP 输出
|
||
ret = initRtspOutput();
|
||
if (ret != SUCCESS)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: initRtspOutput fail\n");
|
||
UnInit();
|
||
return ret;
|
||
}
|
||
|
||
// 初始化格式转换(如需要)
|
||
ret = initSwscale();
|
||
if (ret != SUCCESS)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: initSwscale fail\n");
|
||
UnInit();
|
||
return ret;
|
||
}
|
||
|
||
m_bInited = true;
|
||
LOG_INFO("FFMediaPusher: init done (%dx%d, stride=%dx%d, H%d, %ukbps, GOP=%d)\n",
|
||
m_encWidth, m_encHeight, m_encHorStride, m_encVerStride,
|
||
static_cast<int>(config.encodeType), config.bitrateKbps, config.gop);
|
||
return SUCCESS;
|
||
}
|
||
|
||
int CVrFFMediaPusher::Start()
|
||
{
|
||
if (!m_bInited.load()) return ERR_CODE(DEV_NO_OPEN);
|
||
if (m_bStarted.load()) return SUCCESS;
|
||
|
||
// RTMP 需要先打开 IO(RTSP 不需要,但 RTMP/FLV 必须)
|
||
if (!(m_fmtCtx->oformat->flags & AVFMT_NOFILE))
|
||
{
|
||
int ret = avio_open(&m_fmtCtx->pb, m_fmtCtx->url, AVIO_FLAG_WRITE);
|
||
if (ret < 0)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: avio_open fail %d url=%s\n", ret, m_fmtCtx->url);
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
}
|
||
}
|
||
|
||
// 写 RTMP header
|
||
int ret = avformat_write_header(m_fmtCtx, nullptr);
|
||
|
||
if (ret < 0)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: avformat_write_header fail %d (mediamtx 未运行或端口不对?)\n", ret);
|
||
if (m_fmtCtx->pb)
|
||
avio_closep(&m_fmtCtx->pb);
|
||
return ERR_CODE(DEV_OPEN_ERR);
|
||
}
|
||
|
||
m_headerWritten = true;
|
||
LOG_INFO("FFMediaPusher: connected to mediamtx, header written\n");
|
||
|
||
m_bStarted = true;
|
||
return SUCCESS;
|
||
}
|
||
|
||
int CVrFFMediaPusher::PushFrame(const void* data, size_t size, int64_t ptsUs)
|
||
{
|
||
if (!m_bStarted.load()) return ERR_CODE(DEV_NO_OPEN);
|
||
|
||
if (!data || size == 0)
|
||
return ERR_CODE(DEV_ARG_INVAILD);
|
||
|
||
// 使用调用方传入的 PTS(由 DroneScrewServerPresenter 按帧计数递增生成)
|
||
m_lastPtsUs = ptsUs;
|
||
|
||
// 1) 分配 MPP 输入缓冲区(hor_stride × ver_stride 对齐布局),直接把数据写进去
|
||
const int frameSize = m_encHorStride * m_encVerStride * 3 / 2;
|
||
MppBuffer mppBuf = nullptr;
|
||
MPP_RET mret = mpp_buffer_get(m_mppBufGroup, &mppBuf, frameSize);
|
||
if (mret != MPP_SUCCESS || !mppBuf)
|
||
{
|
||
LOG_ERROR("FFMediaPusher: mpp_buffer_get fail %d\n", mret);
|
||
return ERR_CODE(DEV_CTRL_ERR);
|
||
}
|
||
uint8_t* dst = static_cast<uint8_t*>(mpp_buffer_get_ptr(mppBuf));
|
||
if (!dst)
|
||
{
|
||
mpp_buffer_put(mppBuf);
|
||
return ERR_CODE(DEV_CTRL_ERR);
|
||
}
|
||
mpp_buffer_sync_begin(mppBuf);
|
||
uint8_t* dstY = dst; // Y 平面
|
||
uint8_t* dstUV = dst + m_encHorStride * m_encVerStride; // UV 平面(按 ver_stride 偏移)
|
||
|
||
if (m_needConvert && m_swsCtx)
|
||
{
|
||
// BGR/RGB/RGBA 等打包格式 → NV12,直接转换写入 MppBuffer
|
||
int srcStride = 0;
|
||
switch (m_config.pixelFormat)
|
||
{
|
||
case EFFPushPixelFormat::BGR24:
|
||
case EFFPushPixelFormat::RGB24: srcStride = m_encWidth * 3; break;
|
||
case EFFPushPixelFormat::BGRA32:
|
||
case EFFPushPixelFormat::RGBA32: srcStride = m_encWidth * 4; break;
|
||
case EFFPushPixelFormat::YUV420P: srcStride = m_encWidth; break;
|
||
default: srcStride = m_encWidth * 3; break;
|
||
}
|
||
uint8_t* srcSlice[4] = { const_cast<uint8_t*>(static_cast<const uint8_t*>(data)),
|
||
nullptr, nullptr, nullptr };
|
||
int srcLinesize[4] = { srcStride, 0, 0, 0 };
|
||
uint8_t* dstData[4] = { dstY, dstUV, nullptr, nullptr };
|
||
int dstLinesize[4] = { m_encHorStride, m_encHorStride, 0, 0 };
|
||
sws_scale(m_swsCtx, srcSlice, srcLinesize, 0, m_encHeight, dstData, dstLinesize);
|
||
}
|
||
else
|
||
{
|
||
// 输入为 NV12(紧凑排列,stride = width),按 hor_stride/ver_stride 逐行重排
|
||
const size_t needBytes = static_cast<size_t>(m_encWidth) * m_encHeight * 3 / 2;
|
||
if (size < needBytes)
|
||
{
|
||
mpp_buffer_put(mppBuf);
|
||
LOG_DEBUG("FFMediaPusher: NV12 input too small %zu < %zu\n", size, needBytes);
|
||
return ERR_CODE(DEV_ARG_INVAILD);
|
||
}
|
||
const uint8_t* src = static_cast<const uint8_t*>(data);
|
||
const int srcLn = m_encWidth; // 源 NV12 行宽 = width
|
||
const int copyW = std::min(srcLn, m_encHorStride);
|
||
// Y
|
||
for (int y = 0; y < m_encHeight; ++y)
|
||
memcpy(dstY + static_cast<size_t>(y) * m_encHorStride,
|
||
src + static_cast<size_t>(y) * srcLn, copyW);
|
||
// UV(高度为 height/2)
|
||
const uint8_t* srcUV = src + static_cast<size_t>(srcLn) * m_encHeight;
|
||
for (int y = 0; y < m_encHeight / 2; ++y)
|
||
memcpy(dstUV + static_cast<size_t>(y) * m_encHorStride,
|
||
srcUV + static_cast<size_t>(y) * srcLn, copyW);
|
||
}
|
||
|
||
mpp_buffer_sync_end(mppBuf);
|
||
|
||
// 2) 构建 MppFrame
|
||
MppFrame frame = nullptr;
|
||
mpp_frame_init(&frame);
|
||
mpp_frame_set_width(frame, m_encWidth);
|
||
mpp_frame_set_height(frame, m_encHeight);
|
||
mpp_frame_set_hor_stride(frame, m_encHorStride);
|
||
mpp_frame_set_ver_stride(frame, m_encVerStride);
|
||
mpp_frame_set_fmt(frame, MPP_FMT_YUV420SP);
|
||
mpp_frame_set_buffer(frame, mppBuf);
|
||
mpp_frame_set_pts(frame, ptsUs);
|
||
|
||
// 3) 编码。H.264/H.265 在当前 MPP 版本上需要显式提供输出 packet buffer,
|
||
// 否则 encode() 可能持续返回 success + null packet,RTMP 只有 header 没有媒体数据。
|
||
MppPacket packet = nullptr;
|
||
MppPacket outPacket = nullptr;
|
||
MppBuffer pktBuf = nullptr;
|
||
const size_t pktBufSize = static_cast<size_t>(frameSize);
|
||
|
||
mret = mpp_buffer_get(m_mppBufGroup, &pktBuf, pktBufSize);
|
||
if (mret != MPP_SUCCESS || !pktBuf)
|
||
{
|
||
mpp_frame_deinit(&frame);
|
||
mpp_buffer_put(mppBuf);
|
||
LOG_ERROR("FFMediaPusher: packet buffer get fail %d\n", mret);
|
||
return ERR_CODE(DEV_CTRL_ERR);
|
||
}
|
||
|
||
mret = mpp_packet_init_with_buffer(&packet, pktBuf);
|
||
if (mret != MPP_SUCCESS || !packet)
|
||
{
|
||
mpp_frame_deinit(&frame);
|
||
mpp_buffer_put(mppBuf);
|
||
mpp_buffer_put(pktBuf);
|
||
LOG_ERROR("FFMediaPusher: packet init fail %d\n", mret);
|
||
return ERR_CODE(DEV_CTRL_ERR);
|
||
}
|
||
mpp_packet_set_length(packet, 0);
|
||
mpp_packet_set_pos(packet, mpp_packet_get_data(packet));
|
||
|
||
MppMeta meta = mpp_frame_get_meta(frame);
|
||
if (meta)
|
||
mpp_meta_set_packet(meta, KEY_OUTPUT_PACKET, packet);
|
||
|
||
mret = m_mppApi->encode_put_frame(m_mppCtx, frame);
|
||
mpp_frame_deinit(&frame);
|
||
mpp_buffer_put(mppBuf);
|
||
|
||
if (mret != MPP_SUCCESS)
|
||
{
|
||
mpp_packet_deinit(&packet);
|
||
mpp_buffer_put(pktBuf);
|
||
LOG_DEBUG("FFMediaPusher: encode_put_frame fail %d\n", mret);
|
||
return ERR_CODE(DEV_CTRL_ERR);
|
||
}
|
||
|
||
mret = m_mppApi->encode_get_packet(m_mppCtx, &outPacket);
|
||
if (mret != MPP_SUCCESS)
|
||
{
|
||
mpp_packet_deinit(&packet);
|
||
mpp_buffer_put(pktBuf);
|
||
LOG_DEBUG("FFMediaPusher: encode_get_packet fail %d\n", mret);
|
||
return ERR_CODE(DEV_CTRL_ERR);
|
||
}
|
||
|
||
MppPacket encodedPacket = outPacket ? outPacket : packet;
|
||
|
||
// 编码器预热期可能无输出,跳过本帧(返回 SUCCESS,不算失败)
|
||
if (!encodedPacket || mpp_packet_get_length(encodedPacket) == 0)
|
||
{
|
||
mpp_packet_deinit(&packet);
|
||
mpp_buffer_put(pktBuf);
|
||
LOG_DEBUG("FFMediaPusher: encode produced no packet (warmup)\n");
|
||
return ERR_CODE(DEV_RESULT_EMPTY);
|
||
}
|
||
|
||
MppBuffer encodedBuf = mpp_packet_get_buffer(encodedPacket);
|
||
if (encodedBuf)
|
||
mpp_buffer_sync_ro_begin(encodedBuf);
|
||
auto releaseEncodedPacket = [&]() {
|
||
if (encodedBuf)
|
||
{
|
||
mpp_buffer_sync_ro_end(encodedBuf);
|
||
encodedBuf = nullptr;
|
||
}
|
||
if (outPacket && outPacket != packet)
|
||
mpp_packet_deinit(&outPacket);
|
||
if (packet)
|
||
mpp_packet_deinit(&packet);
|
||
if (pktBuf)
|
||
{
|
||
mpp_buffer_put(pktBuf);
|
||
pktBuf = nullptr;
|
||
}
|
||
};
|
||
|
||
void* pktData = mpp_packet_get_pos(encodedPacket);
|
||
if (!pktData)
|
||
pktData = mpp_packet_get_data(encodedPacket);
|
||
size_t pktSize = mpp_packet_get_length(encodedPacket);
|
||
|
||
// 双重检查:确保有效数据(理论上前面已检查过,这里是保险)
|
||
if (!pktData || pktSize == 0)
|
||
{
|
||
releaseEncodedPacket();
|
||
LOG_DEBUG("FFMediaPusher: encode produced empty packet data\n");
|
||
return ERR_CODE(DEV_RESULT_EMPTY);
|
||
}
|
||
|
||
// 4) 写入 RTSP(独立 AVPacket,pts rescale 到 muxer 时基)
|
||
AVPacket* avpkt = av_packet_alloc();
|
||
if (!avpkt)
|
||
{
|
||
releaseEncodedPacket();
|
||
return ERR_CODE(DEV_CTRL_ERR);
|
||
}
|
||
|
||
if (pktSize > static_cast<size_t>(std::numeric_limits<int>::max()) ||
|
||
av_new_packet(avpkt, static_cast<int>(pktSize)) < 0)
|
||
{
|
||
av_packet_free(&avpkt);
|
||
releaseEncodedPacket();
|
||
return ERR_CODE(DEV_CTRL_ERR);
|
||
}
|
||
|
||
memcpy(avpkt->data, pktData, pktSize);
|
||
avpkt->stream_index = 0;
|
||
avpkt->pts = ptsUs;
|
||
avpkt->dts = ptsUs;
|
||
avpkt->duration = m_config.fps > 0
|
||
? static_cast<int64_t>(1000000 / m_config.fps)
|
||
: 0;
|
||
|
||
// 判断关键帧(I/IDR 帧)
|
||
if (pktSize >= 5)
|
||
{
|
||
const uint8_t* p = static_cast<const uint8_t*>(pktData);
|
||
size_t offset = 0;
|
||
if (p[0] == 0 && p[1] == 0) {
|
||
if (p[2] == 1) offset = 3;
|
||
else if (p[2] == 0 && p[3] == 1) offset = 4;
|
||
}
|
||
if (offset < pktSize) {
|
||
MppCodingType coding = ToMppCoding(m_config.encodeType);
|
||
if (coding == MPP_VIDEO_CodingAVC) {
|
||
int nalType = p[offset] & 0x1F;
|
||
if (nalType == 5 || nalType == 7 || nalType == 8) // IDR / SPS / PPS
|
||
avpkt->flags |= AV_PKT_FLAG_KEY;
|
||
} else if (coding == MPP_VIDEO_CodingHEVC) {
|
||
int nalType = (p[offset] >> 1) & 0x3F; // HEVC: bit1~6
|
||
if ((nalType >= 16 && nalType <= 23) || nalType == 32 || nalType == 33 || nalType == 34)
|
||
avpkt->flags |= AV_PKT_FLAG_KEY; // IRAP / VPS / SPS / PPS
|
||
}
|
||
}
|
||
}
|
||
|
||
releaseEncodedPacket();
|
||
|
||
int aret;
|
||
{
|
||
std::lock_guard<std::mutex> wlk(m_writeMutex);
|
||
if (m_fmtCtx && m_headerWritten.load())
|
||
{
|
||
// 微秒 → muxer 实际时基
|
||
av_packet_rescale_ts(avpkt, AVRational{1, 1000000},
|
||
m_fmtCtx->streams[0]->time_base);
|
||
aret = av_interleaved_write_frame(m_fmtCtx, avpkt);
|
||
|
||
// RTMP 推流需要强制刷新缓冲区,否则数据会积压不发送
|
||
if (aret >= 0 && m_fmtCtx->pb)
|
||
{
|
||
avio_flush(m_fmtCtx->pb);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
aret = AVERROR_EOF; // 已停止,丢弃本帧
|
||
}
|
||
}
|
||
|
||
av_packet_free(&avpkt);
|
||
|
||
if (aret < 0)
|
||
{
|
||
LOG_DEBUG("FFMediaPusher: av_interleaved_write_frame fail %d\n", aret);
|
||
return ERR_CODE(DEV_CTRL_ERR);
|
||
}
|
||
|
||
return SUCCESS;
|
||
}
|
||
|
||
int CVrFFMediaPusher::Stop()
|
||
{
|
||
if (!m_bStarted.load()) return SUCCESS;
|
||
|
||
// 先置 started=false,使并发的 PushFrame 尽快早退
|
||
m_bStarted = false;
|
||
|
||
// 发送 trailer 并关闭 IO(与 PushFrame 写帧互斥)
|
||
{
|
||
std::lock_guard<std::mutex> wlk(m_writeMutex);
|
||
if (m_headerWritten.load() && m_fmtCtx)
|
||
{
|
||
av_write_trailer(m_fmtCtx);
|
||
|
||
// RTMP 需要手动关闭 IO(RTSP 不需要,AVFMT_NOFILE)
|
||
if (m_fmtCtx->pb)
|
||
{
|
||
avio_closep(&m_fmtCtx->pb);
|
||
}
|
||
}
|
||
m_headerWritten = false;
|
||
}
|
||
|
||
// 复位 MPP 编码器
|
||
if (m_mppApi && m_mppCtx)
|
||
{
|
||
m_mppApi->reset(m_mppCtx);
|
||
}
|
||
|
||
return SUCCESS;
|
||
}
|
||
|
||
int CVrFFMediaPusher::UnInit()
|
||
{
|
||
Stop();
|
||
std::lock_guard<std::mutex> lk(m_mutex);
|
||
|
||
// 释放 RTSP 输出上下文
|
||
if (m_fmtCtx)
|
||
{
|
||
avformat_free_context(m_fmtCtx);
|
||
m_fmtCtx = nullptr;
|
||
}
|
||
|
||
// 销毁 MPP 编码器上下文
|
||
if (m_mppCtx)
|
||
{
|
||
mpp_destroy(m_mppCtx);
|
||
m_mppCtx = nullptr;
|
||
m_mppApi = nullptr;
|
||
}
|
||
|
||
// 销毁 MPP buffer group
|
||
if (m_mppBufGroup)
|
||
{
|
||
mpp_buffer_group_put(m_mppBufGroup);
|
||
m_mppBufGroup = nullptr;
|
||
}
|
||
|
||
// 释放 swscale 上下文
|
||
if (m_swsCtx)
|
||
{
|
||
sws_freeContext(m_swsCtx);
|
||
m_swsCtx = nullptr;
|
||
}
|
||
|
||
// 关闭 FFmpeg 网络
|
||
avformat_network_deinit();
|
||
|
||
m_bInited = false;
|
||
return SUCCESS;
|
||
}
|
||
|
||
#endif // FFMEDIA_PLATFORM_DUMMY
|
||
|
||
std::string CVrFFMediaPusher::GetRtspUrl(const std::string& localIp) const
|
||
{
|
||
char buf[256];
|
||
snprintf(buf, sizeof(buf), "rtmp://%s:%u%s",
|
||
localIp.c_str(), m_config.rtspPort, m_config.rtspPath.c_str());
|
||
return std::string(buf);
|
||
}
|