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