Jetson 上视频拖不动:NVDEC 硬解 seek 卡死到软解的排查

Jetson 上用 nvv4l2decoder 硬解视频、拖动进度条 seek 必卡死进程——从把 SIGKILL 误判成 OOM,到加日志定位 seek_simple 永久阻塞,最后靠给硬解码器降 rank、让 decodebin 自动切软解(avdec)彻底解决。附硬解/软解、NVMM、decodebin rank 的背景知识。

给一个 Rust + egui 写的视觉检测客户端加了个小功能:视频文件节点上放一根进度条,拖动就能跳到指定位置,方便测试人员调试视频、不用每次从头看。x86 上写完一切正常,一上 Jetson——一拖动,整个进程就没了:

make: *** [Makefile:82: run] 已杀死

这篇记录从”以为是 OOM”一路查到”NVDEC 硬解码器的 seek 有硬伤”,最后用一个四两拨千斤的办法(给硬解码器降权)解决的全过程。先补齐背景,再讲排查。

一、先补背景:硬解、软解、NVDEC、NVMM

视频文件是压缩的(H.264 / H.265 等),要放出来得先解码成一帧帧画面。谁来解,有两条路:

  • 硬解(HW,硬件解码):用芯片里专门的解码硬件。Jetson 上叫 NVDEC,GStreamer 里对应的元素是 nvv4l2decoder快、省 CPU,是 NVIDIA 主推的。
  • 软解(SW,软件解码):用 CPU 跑解码算法。GStreamer 里是 avdec_h264 / avdec_h265(来自 gst-libav / ffmpeg)。通用、稳,但费 CPU

关键的坑在于解出来的画面放在哪种内存里:

NVMM(NVIDIA Multi-Media Memory)是 NVDEC 硬解输出画面用的专用内存块,不是普通内存。硬解出来的帧躺在 NVMM 里,普通元素(比如 videoconvert)读不了,必须先经 nvvidconv 把它从 NVMM 搬到普通 video/x-raw 才能用。

日志里反复出现的 NvMMLiteOpen / NvMMLiteBlockCreate,就是这个 NVMM 硬解模块在启动。记住这几个词,下面全是围着它们转。

还有一个主角是 decodebin:GStreamer 里一个”智能”元素,你只管把压缩流喂给它,它按每个解码器的 rank(优先级数字)自动挑一个能解这个编码的解码器。Jetson 上硬解 nvv4l2decoder 的 rank 很高,所以 decodebin 默认都走硬解。这个 rank,后面就是我们的突破口。

二、现象:一拖就”崩”,但进程其实还活着

第一版管线是标准的 Jetson 硬解:

filesrc ! decodebin ! nvvidconv ! video/x-raw,format=BGRx
       ! videoconvert ! video/x-raw,format=RGB ! appsink

播放正常,一拖进度条就死。日志尾巴:

NvMMLiteOpen : Block : BlockType = 261
NvMMLiteBlockCreate : Block : BlockType = 261
已杀死

已杀死SIGKILL(不是段错误的 Segmentation fault)。在 Jetson + NVDEC 上看到 SIGKILL,第一反应是 OOM killer——Jetson 的 CPU 和 GPU 共享同一块物理内存,内存爆得特别快。

三、排查一:是 OOM 吗?——不是

确认 OOM 只需一条命令:

sudo dmesg | grep -iE "oom|killed process"

结果:最近没有任何 OOM 记录。唯一一条 Killed process 是十几天前的旧事(时间戳差了一百多万秒),跟这次崩溃无关;dmesg 尾部全是 nvme/loop 之类的无关事件。

没 OOM 记录,却被 SIGKILL——这条线索很重要:它排除了内存,把嫌疑指向了卡死(进程假死后被外部/看门狗杀掉)。

四、排查二:加日志,把 seek 拆成四步

崩在哪一步?给 seek 加分步埋点,SIGKILL 时最后一行日志就是死亡现场:

log::info!("[seek] step1: set_state(Paused) 前");
let r1 = pipeline.set_state(gst::State::Paused);
log::info!("[seek] step1: 返回 = {:?}", r1);

log::info!("[seek] step2: 等状态就绪(最多5s)前");
let (r2, cur, pend) = pipeline.state(gst::ClockTime::from_seconds(5));
log::info!("[seek] step2: ret={:?} current={:?} pending={:?}", r2, cur, pend);

log::info!("[seek] step3: seek_simple 前 pos_ns={}", pos_ns);
let ok = pipeline.seek_simple(flags, pos).is_ok();
log::info!("[seek] step3: seek_simple 返回 ok={}", ok);   // ← 永远看不到这行

跑一次崩溃,日志停在:

step1: set_state(Paused) 返回 = Ok(Async)
step2: state() ret=Ok(Success) current=Paused pending=VoidPending   ← 管线确已就绪
step3: seek_simple 前 (pos_ns=155723388649)
<之后无任何输出,进程被 kill>

结论一锤定音:

  • step2 返回 Ok(Success)pending=VoidPending → 管线确实已经 preroll 到 Paused,不是”没准备好”。
  • step3 打了”前”,却永远没有”返回”seek_simple 这一个调用把线程卡死了,永不返回。

而且——这台机器上,连播放时的检测线程都还在正常刷日志。进程没死,是 seek_simple 挂了,然后进程假死被杀。

翻回原来的代码注释,作者早写过一句预言:

// 硬件解码器 (nvv4l2decoder) 的 seek_simple 会永久阻塞

日志刚好把这句实锤了。

五、根因 + 试过的所有死路

根因:这台 Jetson 的 nvv4l2decoder 做 FLUSH seek 必然永久阻塞。 这是 NVDEC 驱动层的硬伤,跟你的调用姿势基本无关。为确认,把能想到的硬解姿势全试了一遍:

姿势结果
停旧管线 → 重建新管线 → 在新管线 Paused 下 seek崩在 NvMMLiteBlockCreate(新旧硬解码器抢 NVMM)
现有管线上 Paused 态 seek_simple卡死(上面的日志)
现有管线上 Playing 态 seek_simple还是卡死
把 seek 挪到独立线程执行seek 线程卡死;而且 UI 也跟着冻——因为进度条每帧要 query_position 查管线位置,而卡死的 seek_simple 正握着管线内部锁,UI 一查就被锁住

FLUSH seek 的原理是”发 FLUSH_START 丢缓冲 → 重定位 → FLUSH_STOP → 重新 preroll”,这套握手要 streaming 线程配合完成;nvv4l2decoder 在这台机器上完不成这个握手 → seek_simple 死等。硬解这条路,彻底走不通。

六、转向软解,却踩了 decodebin 的第二个坑

既然硬解 seek 有硬伤,那就让视频文件节点(纯调试用途)走软解——avdec 用 CPU 解码,输出普通内存,不碰 NVMM,seek 自然稳。

第一反应是用 decodebinforce-sw-decoders 属性:

filesrc ! decodebin force-sw-decoders=true ! videoconvert ! ... ! appsink

结果又崩,报的是新错:

视频管线方案1创建成功: ... decodebin force-sw-decoders=true ...
NvMMLiteOpen : Block : BlockType = 260               ← 还是开了硬解!
GStreamer 错误: Internal data stream error.
  reason not-negotiated (-4)  (qtdemux → decodebin)

一开始怀疑是没装软解码器,gst-inspect-1.0 avdec_h264 一查——装着的(gst-libav 1.20.3)。那问题是:

decodebin 在这台 Jetson 上无视了 force-sw-decoders,还是选了硬解 nvv4l2decoder(NvMMLiteOpen),硬解输出 NVMM,而这条管线没接 nvvidconv,videoconvert 读不了 NVMM → not-negotiated 崩

NVIDIA 定制的 decodebin / 解码器插件不老实,force-sw-decoders 在这里不好使。

试过第二招:显式软解,绕开 decodebin:

filesrc ! qtdemux ! h264parse ! avdec_h264 ! videoconvert ! ... ! appsink

又崩,not-linked:qtdemux 的视频 pad 接不上 h264parse。原因很简单——这个视频是 H.265/HEVC,不是 H.264,h264parse 认不了。显式指定解码器就得”认编码”,换个 H.265 的视频又得改成 h265parse ! avdec_h265,

七、最终解:给硬解码器降 rank

回到 decodebin 的本质:它按 rank 挑解码器。硬解 rank 高所以被选中——那我在本进程内把硬解码器的 rank 压到最低,decodebin 不就只能挑到软解了吗?而且软解是按编码自动匹配的(H.264 匹配 avdec_h264,H.265 匹配 avdec_h265),编码无关,一劳永逸:

// 把 Jetson 硬解码器 nvv4l2decoder 在本进程内降到最低优先级(NONE),
// decodebin 就会改选软件解码器(avdec_h264 / avdec_h265 / …),
// 兼容任意编码且 seek 稳定不卡。
if let Some(f) = gst::ElementFactory::find("nvv4l2decoder") {
    f.set_rank(gst::Rank::NONE);
}

管线随之变回最朴素的一条,decodebin 会自己搞定容器解析 + 选软解:

filesrc ! decodebin ! videoconvert ! video/x-raw,format=RGB ! appsink

一次拖动就通了。 H.265 也能拖。

这招好在极其局部:set_rank 只改 decodebin 的解码器选择,而整个 app 里只有视频文件节点用 decodebin——USB 相机走 v4l2src(吐原始帧,根本不解码)、工业相机走各自 SDK。所以”降硬解权重”实际等价于”只让视频文件节点走软解”,相机的硬解通路一根汗毛没动。

想切回硬解也简单——软/硬其实由这两处配套决定:

是否降权管线
软解降(Rank::NONE)decodebin ! videoconvert
硬解不降decodebin ! nvvidconv ! videoconvert

八、配套:seek 挪出 UI 线程 + 非阻塞 appsink

软解虽然让 seek 不卡了,但前面暴露的两个结构性问题顺手一并修掉,让它更稳:

1. seek 走独立控制线程,UI 只发目标位置、立即返回,永不因 seek 阻塞/冻结;拖动期间堆积的请求合并到最新串行执行:

// UI 线程:只发目标秒数,立即返回
pub fn seek_to_secs(&mut self, secs: f64) -> bool {
    self.seek_tx.as_ref().map(|tx| tx.send(secs).is_ok()).unwrap_or(false)
}

// 独立控制线程:串行 + 合并
while let Ok(mut target) = seek_rx.recv() {
    while let Ok(newer) = seek_rx.try_recv() { target = newer; } // 只跳最新
    let _ = pipe.set_state(gst::State::Playing);
    if pipe.seek_simple(FLUSH | KEY_UNIT, ns(target)).is_ok() {
        let _ = pipe.state(gst::ClockTime::from_seconds(5)); // 等完成再下一次
    }
}

2. appsink 设成非阻塞,flush 期间不让上游因等消费而阻塞(拆掉死锁的另一半);sync 保持默认 true,否则视频会按解码速度飞速播放(10 倍速):

appsink.set_property("drop", true);
appsink.set_property("max-buffers", 2u32);
// 注意:千万别设 sync=false —— 那是给实时摄像头的,视频文件会变 10 倍速

UI 侧进度条也只在松手时发一次 seek:

if resp.drag_stopped() || resp.clicked() {
    self.seek(dur * ratio);
}

九、一句话总结 + 教训

Jetson 的 NVDEC 硬解码器 seek 有硬伤(FLUSH seek 永久阻塞);想在视频文件上可靠拖动,就给 nvv4l2decoder 降 rank、让 decodebin 自动切软解(avdec)——编码无关、只影响视频节点、seek 稳。

几条能带走的经验:

  • SIGKILL ≠ OOMdmesg 一条命令就能排除内存,别一上来就往 OOM 上想。
  • 卡死靠分步日志定位。把可疑调用拆成”前/后”两条日志,SIGKILL 时最后一行就是现场——比盯着代码猜快得多。
  • decodebin 按 rank 选解码器。想强制走某类解码器,改 rankforce-sw-decoders(在 NVIDIA 定制栈上会被无视)、比显式指定解码器(要认编码)都更稳、更通用。
  • sync=false 只给实时源。给视频文件设 sync=false 会变 10 倍速——它是”不按时钟节流”的意思,摄像头本身就是实时的才用。
  • 能局部就别全局。这套改动全部锁在视频文件节点的解码路径里,相机(硬解 / SDK)一律不碰——“只改该改的”,出问题时排查面也小。

评论

0
还没有评论, 抢首楼