GStreamer播放时如何安全切换文件源避免HLS列表重置
问题描述
需要实现通过键盘输入切换两张图片、实时生成HLS流的功能,初始实现代码如下:
import gi gi.require_version("Gst", "1.0") from gi.repository import Gst, GLib from threading import Thread Gst.init(None) main_loop = GLib.MainLoop() thread = Thread(target=main_loop.run) thread.start() pipeline = Gst.Pipeline() source = Gst.ElementFactory.make("filesrc", "source") source.set_property("location", "image1.jpeg") decoder = Gst.ElementFactory.make("jpegdec", "decoder") freeze = Gst.ElementFactory.make("imagefreeze", "freeze") convert = Gst.ElementFactory.make("videoconvert", "convert") encoder = Gst.ElementFactory.make("x264enc", "encoder") encoder.set_property("speed-preset", "ultrafast") encoder.set_property("tune", "zerolatency") muxer = Gst.ElementFactory.make("mpegtsmux", "muxer") queue = Gst.ElementFactory.make("queue", "queue") hls = Gst.ElementFactory.make("hlssink", "hls") hls.set_property("max-files", 0) hls.set_property("playlist-length", 0) hls.set_property("target-duration", 1) pipeline.add(source, decoder, freeze, convert, encoder, muxer, queue, hls) source.link(decoder) decoder.link(freeze) freeze.link(convert) convert.link(encoder) encoder.link(muxer) muxer.link(queue) queue.link(hls) pipeline.set_state(Gst.State.PLAYING) input() source.set_property("location", "image2.jpeg") input() pipeline.set_state(Gst.State.NULL) main_loop.quit()
运行后按回车键切换源时,playlist.m3u8会被重置,不符合预期。排查确认该问题由元素状态自动切换至READY导致,若跳过对应状态切换操作虽然能实现切换效果,但会触发元素警告。需要找到无警告的安全实现方案,支持在播放过程中完成源文件切换。
此前曾尝试使用gst-interpipe实现该功能但未能成功运行,已在对应项目仓库提交bug报告。
问题根源
直接修改处于PLAYING状态的filesrc元素的location属性不属于GStreamer规范的动态流切换操作:filesrc收到属性修改请求后会强制关闭当前文件句柄、重新加载新文件,过程中会向下游发送流中断、重配置事件,hlssink收到重配置事件后会默认重置播放列表;同时jpegdec、imagefreeze等上游元素在数据流意外中断时会触发状态切换警告,这就是异常的来源。
推荐实现方案
方案1:基于input-selector的双路源切换(无额外依赖,稳定性最高)
使用GStreamer官方内置的input-selector元素实现两路静态图片源的无缝切换,全程不会中断下游编码、HLS切片流程,不会触发播放列表重置,无运行警告。
实现逻辑:
- 搭建两路独立的「filesrc -> jpegdec -> imagefreeze -> videoconvert」分支,分别预加载两张待切换的图片
- 两路分支的输出统一接入
input-selector的不同请求衬垫(sink pad) input-selector的输出直接对接原有编码、HLS下游链路- 切换图片时仅需修改
input-selector的active-pad属性指定当前激活的分支即可,全程不需要调整任何元素状态,不会触发管道重置
可直接运行的参考代码:
import gi gi.require_version("Gst", "1.0") from gi.repository import Gst, GLib from threading import Thread Gst.init(None) main_loop = GLib.MainLoop() thread = Thread(target=main_loop.run, daemon=True) thread.start() pipeline = Gst.Pipeline() # 第一路图片源分支 src1 = Gst.ElementFactory.make("filesrc", "src1") src1.set_property("location", "image1.jpeg") dec1 = Gst.ElementFactory.make("jpegdec", "dec1") freeze1 = Gst.ElementFactory.make("imagefreeze", "freeze1") conv1 = Gst.ElementFactory.make("videoconvert", "conv1") # 固定输出格式,避免切换时格式不匹配 caps1 = Gst.ElementFactory.make("capsfilter", "caps1") caps1.set_property("caps", Gst.Caps.from_string("video/x-raw,width=1920,height=1080,framerate=25/1,format=I420")) # 第二路图片源分支 src2 = Gst.ElementFactory.make("filesrc", "src2") src2.set_property("location", "image2.jpeg") dec2 = Gst.ElementFactory.make("jpegdec", "dec2") freeze2 = Gst.ElementFactory.make("imagefreeze", "freeze2") conv2 = Gst.ElementFactory.make("videoconvert", "conv2") caps2 = Gst.ElementFactory.make("capsfilter", "caps2") caps2.set_property("caps", Gst.Caps.from_string("video/x-raw,width=1920,height=1080,framerate=25/1,format=I420")) # 流选择器 input_sel = Gst.ElementFactory.make("input-selector", "input-sel") # 原有下游编码、HLS链路 encoder = Gst.ElementFactory.make("x264enc", "encoder") encoder.set_property("speed-preset", "ultrafast") encoder.set_property("tune", "zerolatency") muxer = Gst.ElementFactory.make("mpegtsmux", "muxer") queue = Gst.ElementFactory.make("queue", "queue") hls = Gst.ElementFactory.make("hlssink", "hls") hls.set_property("max-files", 0) hls.set_property("playlist-length", 0) hls.set_property("target-duration", 1) # 添加所有元素到管道 pipeline.add( src1, dec1, freeze1, conv1, caps1, src2, dec2, freeze2, conv2, caps2, input_sel, encoder, muxer, queue, hls ) # 链接第一路分支到选择器 src1.link(dec1) dec1.link(freeze1) freeze1.link(conv1) conv1.link(caps1) sink_pad1 = input_sel.get_request_pad("sink_0") caps1.get_static_pad("src").link(sink_pad1) # 链接第二路分支到选择器 src2.link(dec2) dec2.link(freeze2) freeze2.link(conv2) conv2.link(caps2) sink_pad2 = input_sel.get_request_pad("sink_1") caps2.get_static_pad("src").link(sink_pad2) # 链接选择器到下游链路 input_sel.link(encoder) encoder.link(muxer) muxer.link(queue) queue.link(hls) # 初始激活第一路源 input_sel.set_property("active-pad", sink_pad1) pipeline.set_state(Gst.State.PLAYING) # 第一次回车切换到第二路源 input() input_sel.set_property("active-pad", sink_pad2) # 第二次回车退出程序 input() pipeline.set_state(Gst.State.NULL) main_loop.quit()
注意事项:
- 两路分支的capsfilter参数必须完全一致,分辨率、帧率、色彩格式不匹配会导致编码器报错、切换失败
- 切换操作是瞬时完成的,不会产生流中断事件,hlssink会持续生成切片,不会重置m3u8播放列表
方案2:基于appsrc的动态帧喂入(适合多源切换场景)
如果后续需要切换2张以上的图片,多分支的input-selector方案会导致管道结构冗余,此时可以使用appsrc替代原有的filesrc+jpegdec链路:程序内部读取图片文件、解码为固定格式的原始视频帧后持续喂给appsrc,切换图片时仅需要替换喂入的帧内容即可,同样不会影响下游链路稳定性。
实现要点:
- appsrc需要配置固定的输出caps,和编码器要求的格式对齐
- 喂入帧时需要按配置的帧率递增PTS时间戳,避免时间戳跳变导致的HLS切片异常
- 切换新图片时不需要暂停管道,直接持续喂入新图片的冻结帧即可
内容的提问来源于stack exchange,提问作者Jpac14

