使用自定义 appsrc 时,GStreamer mp4mux 给出“缓冲区没有 PTS”错误

Mig*_*hez 5 video android gstreamer qgroundcontrol

我有一个用 C++ 编码的管道,如下所示:

\n
appsrc do-timestamp=TRUE is-live=TRUE caps=\n\xe2\x80\x9cvideo/x-h264, stream-format=(string)byte-stream, alignment=(string)none, framerate=(fraction)0/1\xe2\x80\x9d min-latency=300000000 ! h264parse ! video/x-h264, stream-format=(string)avc, alignment=(string)au ! tee name=t \\\nt. ! queue ! valve drop=FALSE ! decodebin ! glupload ! glcolorconvert ! qtsink sync=FALSE \\\nt. ! queue ! valve drop=FALSE ! mp4mux reserved-max-duration=3600000000000 reserved-moov-update-period=10000000000 ! filesink sync=FALSE location=\xe2\x80\x9d....../out.mp4\xe2\x80\x9d\n
Run Code Online (Sandbox Code Playgroud)\n

appsrc 将来自 Drone\xe2\x80\x99s USB 无线视频接收器的视频注入管道中。

\n

更多背景信息:

\n
    \n
  • USB 接收器硬件为我们提供了 512 字节的无时间戳\n原始附件 B h.264 视频块
  • \n
  • 帧速率应为 60 fps,但实际上它很少跟上它,并且根据信号强度而变化(因此帧速率=(fraction)0/1\xe2\x80\x9d,并且\xe2\x80\x99s \nqtsink 和 filesink 均未同步\xe2\x80\x99d 到管道的原因\n(sync=FALSE))
  • \n
  • 硬件引入了至少 300 毫秒的延迟,\nas 在 appsrc 中设置
  • \n
  • appsrc 会自动为我的缓冲区添加时间戳\n(do-timestamp=TRUE)
  • \n
  • I\xe2\x80\x99m 使用 mp4muxserved-max-duration 和\nreserved-moov-update-period 来防止应用程序崩溃破坏\nmp4 文件
  • \n
  • I\xe2\x80\x99m 使用适用于 Android 的 GStreamer 1.18.4
  • \n
\n

当无人机不在空中时,视频录制效果很好。但当它起飞时,经过大约 15 秒的正确视频录制后,mp4mux 元素失败,并显示消息 \xe2\x80\x9c缓冲区没有 PTS \xe2\x80\x9d。不幸的是,一些用户一直报告这一点,但我无法重现它(因为它需要驾驶我没有的无人机),这不会使很多意义。到目前为止,我的猜测是,在该特定时刻,无线视频链路中可能存在一些拥塞,并且某些视频数据包可能会延迟几毫秒,这可能会造成一些麻烦。

\n

这里\xe2\x80\x99s是创建appsrc的(简化的)代码

\n
   _pAppSrc = gst_element_factory_make("appsrc", "artosyn_source");\n    gpointer pAppSrc = static_cast<gpointer>(_pAppSrc);\n\n    // Retain one more ref, so the source is destroyed\n    // in a controlled way\n    gst_object_ref(_pAppSrc);\n\n    pCaps = gst_caps_from_string("video/x-h264, stream-format=(string)byte-stream, alignment=none, framerate=(fraction)0/1"));\n    g_object_set(G_OBJECT(pAppSrc), "caps", pCaps,\n                                    "is-live", TRUE,\n                                    "min-latency", G_GINT64_CONSTANT(300000000),\n                                    "format", GST_FORMAT_TIME,\n                                    "do-timestamp", TRUE,\n                                    nullptr);\n\n   _pBufferPool = gst_buffer_pool_new();\n\n   pConfig = gst_buffer_pool_get_config (_pBufferPool);\n\n   static const guint kBufferSize  = 512;\n   static const guint kPoolSize    = 0x400000;\n   static const guint kPoolSizeMax = 0x600000;\n\n    qsizetype nBuffersMin = kPoolSize / kBufferSize;\n    qsizetype nBuffersMax = kPoolSizeMax / kBufferSize;\n\n    gst_buffer_pool_config_set_params(pConfig, pCaps, kBufferSize, nBuffersMin, nBuffersMax);\n\n   gst_buffer_pool_set_config(_pBufferPool, pConfig);\n   gst_buffer_pool_set_active(_pBufferPool, TRUE);\n\n   gst_caps_unref(GST_CAPS(pCaps));\n
Run Code Online (Sandbox Code Playgroud)\n

当 USB 驱动程序填满新的缓冲区时,它将\xe2\x80\x99 推入管道,如下所示:

\n
bool unref = false;\n\ngst_buffer_unmap(b->pBuffer, &b->mapInfo);\ngst_buffer_set_size(b->pBuffer, xfer.pXfer->actual_length);\n\nif(result == LIBUSB_TRANSFER_COMPLETED)\n{\n    //-- DROP DATA IF NOT IN PLAYING STATE --\n    GstState st, pend;\n    GstStateChangeReturn scr = gst_element_get_state(GST_ELEMENT(_pAppSrc), &st, &pend, GST_CLOCK_TIME_NONE);\n    Q_UNUSED(scr)\n    bool drop = (st != GST_STATE_PLAYING);\n\n    if(!drop)\n    {\n        GstFlowReturn ret = GST_FLOW_OK;\n\n        // Push into pipeline\n        ret = gst_app_src_push_buffer(GST_APP_SRC(_pAppSrc), b->pBuffer);\n\n        if(ret != GST_FLOW_OK)\n            qCDebug(MMCVideoLog()) << "Can't push buffer to the pipeline (" << ret << ")";\n        else\n            unref = false;  // Don't unref since gst_app_src_push_buffer() steals one reference and takes ownership\n    }\n} else if(result == LIBUSB_TRANSFER_CANCELLED)\n{\n    qCDebug(MMCVideoLog()) << "! Buffer canceled";\n} else {\n    qCDebug(MMCVideoLog()) << "? Buffer result = " << result;\n}    \n\nif(unref)\n    gst_buffer_unref(b->pBuffer);\n
Run Code Online (Sandbox Code Playgroud)\n

这是我从受影响的机器的 Android logcat 中得到的信息:

\n
[07-22 18:37:45.753 17414:18734 E/QGroundControl]\nVideoReceiverLog: GStreamer error: [element ' "mp4mux0" ']  Could not multiplex stream.\n\n[07-22 18:37:45.753 17414:18734 E/QGroundControl]\nVideoReceiverLog: Details:  ../gst/isomp4/gstqtmux.c(5010): gst_qt_mux_add_buffer (): /GstPipeline:receiver/GstBin:sinkbin/GstMP4Mux:mp4mux0:\nBuffer has no PTS.\n
Run Code Online (Sandbox Code Playgroud)\n

我\xe2\x80\x99尝试过的:

\n
    \n
  • 将 GstBaseParser pts_interpolation 设置为 TRUE,并将 infer_ts 设置为 TRUE
  • \n
\n

所以我的问题是:

\n
    \n
  • 你能看到我的代码有什么问题吗?我错过了什么?
  • \n
  • 在找到真正的原因之前,我可以依靠 matroskamux 来暂时避免该问题吗?
  • \n
\n

编辑:我设法“原位”复制它,同时使用连接到我的 T 形元件的接收垫的探针打印出每个缓冲区的 PTS 和 DTS,并发现问题缓冲区没有DTS 和 PTS. Perhaps my h264parse or my capsfilter are doing something nasty inbetween my appsrc and my tee?

\n
07-28 17:54:49.025  1932  2047 D : PTS:  295659241497 DTS:  295659241497\n07-28 17:54:49.026  1932  2047 D : PTS:  295682488791 DTS:  295682488791\n07-28 17:54:49.053  1932  2047 D : PTS:  295710463127 DTS:  295710463127\n07-28 17:54:49.054  1932  2047 D : PTS:  18446744073709551615  DTS:  18446744073709551615\n07-28 17:54:49.054  1932  2047 E : ************** NO PTS\n07-28 17:54:49.054  1932  2047 E : ************** NO DTS\n07-28 17:54:49.110  1932  2047 D : PTS:  295738607214 DTS:  295738607214\n07-28 17:54:49.111  1932  2199 E : GStreamer error: [element ' "mp4mux1" ']  Could not multiplex stream.\n07-28 17:54:49.111  1932  2199 E : Details:  ../gst/isomp4/gstqtmux.c(5010): gst_qt_mux_add_buffer (): /GstPipeline:receiver/GstBin:sinkbin/GstMP4Mux:mp4mux1:\n07-28 17:54:49.111  1932  2199 E : Buffer has no PTS.\n
Run Code Online (Sandbox Code Playgroud)\n

编辑2: After much more digging, I found more clues: I wrote some code to dump every video packet that comes through USB along with a timestamp to a binary file, and a player to play it back. Then I went to the field and have the customer fly the drone until the bug was triggered. This way I have a way of reproducing the error at will.

\n

通过使用两个探针,一个连接到“appsrc”元素的“src”垫,另一个连接到“tee”元素的“sink”垫,我打印了通过它们的每个数据包的 PTS 和 DTS。

\n

以下是我的发现:

\n

长话短说:在某个(随机)点,即使 h264parse 被馈送到带时间戳的缓冲区,它也会输出一个没有 PTS 和 DTS 的缓冲区。

\n
    \n
  • 生成我通过 USB 获取的流的硬件 h264 编码器插入 SPS NALS,无需 VUI,"[parser] unable to compute timestamp: VUI not present"当将 h264parse 调试级别设置为 6 时,我会遇到很多这样的错误

    \n
  • \n
  • “VUI 不存在”错误非常一致并且频繁出现。大多数时候都没有被注意到,因此我不能 100% 确定这是原因

    \n
  • \n
  • 由于 appsrc 推送的 h264 缓冲区没有对齐,而 h264parse 输出 au 对齐的缓冲区,因此 appsrc 中的 512 字节缓冲区数量与 h264parse 中的缓冲区数量之间没有直接关系。因此,我相信可以相当肯定地说,appsrc 生成的时间戳与 h264parse 生成的时间戳之间也没有直接关系。h264parse 必须重新计算它们。

    \n
  • \n
  • 我的 h264 流非常简单:SPS->PPS->I 帧->(多个 P 帧)。没有 B 帧,并且每一帧都独立包含在一个大的脂肪切片中(仅 1 个 NAL)。

    \n
  • \n
\n

所以,现在我正在尝试在这两个选项之间进行选择:

\n
    \n
  1. 每次检测到无 VUI 的 SPS 时,手动计算并插入带有 VUI 的假 SPS NAL。由于无线电信号强度(尽管以 60fps 编码),流应该具有可变的帧速率,并且分辨率和图像属性始终相同。
  2. \n
  3. 我自己解析流以向管道提供带时间戳的 au 对齐缓冲区。
  4. \n
\n

Mig*_*hez 7

经过一番绞尽脑汁,我终于找到了问题的根本原因。而且有点晦涩难懂..

无人机中的无线视频发射器能够根据无线电链路的可用带宽动态改变视频比特率。或者换句话说:当无人机距离太远或干扰较强时,视频质量会下降。

当这种情况发生时,视频帧(仅包含在单个 NAL 中的一个切片中)开始变得明显更小。由于我正在读取没有特定对齐的 h264 流的 512 字节块并将它们作为 GstBuffers 转发到 GStreamer,因此如果一帧所需的数据大小低于 512 字节,则缓冲区可能包含多个帧。在这种情况下,h264parse 将此视为具有相同时间戳的N 个不同缓冲区。默认行为似乎是忽略上游 PTS 和 DTS,并尝试通过从 SPS 读取 VUI(我的流中不存在)来根据帧的持续时间计算时间戳。因此,离开 h264parse 源代码垫的缓冲区将没有 PTS 和 DTS,从而使 mp4mux 抱怨。

正如我之前提到的,我的流非常简单,因此我编写了一个简单的解析器来检测每个 NAL 的开头。通过这种方式,我可以“解压”来自 USB 硬件的流,并确保推入管道的每个缓冲区将仅包含一个 NAL(因此,最多一帧),并且具有独立的时间戳。

为了冗余,我在 T 形元件的水槽垫上添加了一个探针,以确保通过它的每个缓冲区都有正确的时间戳。否则,他们将被迫像这样的元素的运行时间。

if (!GST_BUFFER_PTS_IS_VALID(buffer) || !GST_BUFFER_DTS_IS_VALID(buffer))
{

    GstElement* elm = gst_pad_get_parent_element(pad);
    qCDebug(VideoReceiverLog) << "Invalid timestamp out of source. Replacing with element running time.";
    GstClockTime ts = gst_element_get_current_running_time(elm);
    GST_BUFFER_PTS(buffer) = ts;
    GST_BUFFER_DTS(buffer) = ts;
}
Run Code Online (Sandbox Code Playgroud)

执行此操作后,我无法再通过测试转储重现该问题。