ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

gstreamer pipeline梳理及插件新增方案

gstreamer pipeline梳理及插件新增方案

一,整体播放的pipeline
1,playbin 主要做两件事情:
收到 uridecodebin 的 raw pad(pad-added)→ 接入 input-selector 的 sink_%u(pad_added_cb)
收到 uridecodebin 的 no-more-pads → request playsink pad 并链接 combiner src → playsink sink(no_more_pads_cb),然后 unblock combiner

playbin (GstPipeline)
├─ playsink
├─ uridecodebin(main)
├─ uridecodebin(sub1) / sub2 ... (TCL_PATCH 多外挂字幕)
├─ input-selector(audio) ─┐
├─ input-selector(video) ─┼─> playsink (request pad)
└─ input-selector(text) ─┘

2,pipeline 详细流程:
playbin
├─ uridecodebin (有时叫 sourcebin/uridecodebin3,取决于实现)
│ ├─ source (按 URI scheme 选择)
│ │ ├─ filesrc / souphttpsrc / rtspsrc / ...
│ │ └─ (可选) queue2 / downloadbuffer / retry 等
│ ├─ typefind
│ ├─ demux (按容器选择)
│ │ ├─ qtdemux / matroskademux / tsdemux / ...
│ │ └─ 输出:audio_%u / video_%u / subtitle_%u (压缩流 pad)
│ └─ decodebin (GstDecodeBin / decodebin3)
│ ├─ (每条流一条 decode chain)
│ │ ├─ queue
│ │ ├─ parser (可选:h264parse/aacparse/…)
│ │ ├─ decoder (avdec_h264/faad/… 或硬解)
│ │ ├─ convert (audioconvert/videoconvert)
│ │ ├─ resample/scale (audioresample/videoscale,可选)
│ │ └─ 输出:audio/x-raw、video/x-raw、text/x-raw
│ └─ 暴露 pad:src_%u (raw)
│
├─ audio-bin (playbin 内部组的音频输出链)
│ ├─ queue
│ ├─ audioconvert
│ ├─ audioresample
│ ├─ (可选) volume / level / capsfilter
│ └─ audiosink (autoaudiosink / pulsesink / alsasink / openslessink / ... )
│
├─ video-bin
│ ├─ queue
│ ├─ videoconvert
│ ├─ videoscale
│ ├─ (可选) subtitleoverlay / videofilter
│ └─ videosink (autovideosink / glimagesink / waylandsink / ... )
│
└─ text/subtitle-bin (有的版本并入 video-bin)
├─ subparse / ttmlparse / ...
└─ textoverlay(叠到视频) 或 appsink(单独输出)


3,流程描述:

1. 初始化和URI设置
应用程序通过g_object_set(playbin, "uri", ...)设置URI。

gst_play_bin_set_uri()将URI存入next_group,标记valid = TRUE。

2. 状态变化到PAUSED
gst_play_bin_change_state处理READY→PAUSED时调用setup_next_source()。

setup_next_source()检查next_group是否有效,若有效则:

如果当前有正在播放的curr_group,先解除其与playsink的连接(deactivate_group)。

激活next_group(activate_group):

创建/获取uridecodebin(和suburidecodebin)。

设置其属性(URI、缓冲参数等)。

连接pad-added、no-more-pads等信号。

将uridecodebin加入bin并设置状态为目标状态(PAUSED)。

将curr_group指向next_group,原curr_group变为新的next_group(双缓冲交换)。

3. 解码流水线构建
当uridecodebin发现新pad时,触发pad_added_cb。

回调中根据pad的caps判断媒体类型(音频/视频/字幕),找到对应的GstSourceCombine。

如果该类型的组合器尚未创建,则创建input-selector(或使用用户自定义组合器)。

从组合器请求一个sink pad(如"sink_%u"),并链接uridecodebin的src pad到此sink pad。

将sink pad存入对应的channels数组。

如果组合器支持"active-pad"属性,连接notify::active-pad信号,以便在流切换时更新current-*属性。

当所有pad添加完毕,no-more-pads_cb被调用:

为每个有数据的媒体类型向playsink请求一个对应的sink pad(如音频sink pad),并与组合器的src pad链接。

解阻塞组合器的src pad(之前被BLOCK探针阻塞,防止数据过早流出)。

发送async-done,表示管道已准备好。

4. 流选择与切换
应用程序通过设置current-video/current-audio/current-text属性切换流。

gst_play_bin_set_current_*_stream()获取目标流的sink pad,然后:

向组合器发送自定义事件(如playsink-custom-video-flush)以触发刷新。

设置组合器的active-pad属性为目标pad。

组合器的notify::active-pad回调会更新playbin的current-*属性并可能发送刷新完成事件。

对于字幕流,还会处理特殊的阻塞/解阻塞逻辑(因为字幕可能来自独立的suburidecodebin)。

5. 缓冲与网络流
当uridecodebin内部队列缓冲时,会向上游发送GST_MESSAGE_BUFFERING消息。

gst_play_bin_handle_message中特殊处理此类消息:

如果当前正在切换组(stream_changed_pending为TRUE),则将缓冲消息暂存,待切换完成后重新发送。

否则直接传递给父类处理,最终由应用程序根据百分比控制状态(PAUSED/PLAYING)。

6. 标签处理
当组合器的sink pad上的"tags"属性变化时,notify_tags_cb被调用,发出*-tags-changed信号。

应用程序可通过get-video-tags等action信号获取具体标签。

7. 无缝切换(Gapless)
当当前URI即将结束时,uridecodebin发出drained信号,触发drained_cb。

drained_cb发出about-to-finish信号,通知应用程序设置下一个URI(通过uri属性)。

然后调用setup_next_source(),若新URI已设置,则开始预滚下一个片段,实现无缝衔接。

二,uridecodebin 的pipeline:
1,第一类,都是raw 源文件,不需要创建decodebin和typefind 链路:setup_source() → analyse_source() → expose_decoded_pad() → new_decoded_pad_added_cb()
uridecodebin
└─ source (例如 v4l2src / audiotestsrc / 某些自带解码输出的源)
└─ (optional) queue2 # decoder->need_queue 时插入
└─ expose_decoded_pad()
└─ ghost src_%u

2,非stream 的普通文件或者本地文件:setup_source() → make_decoder() → gst_element_link_pads(source, NULL, decodebin, "sink"),decodebin pad-added → new_decoded_pad_added_cb() 产生 ghost src_%u

uridecodebin
└─ source (filesrc/fdsrc/isosrc/...)
└─ decodebin
└─ (内部 autoplug demux/decoder/convert...)
└─ raw src pad(s)
└─ ghost src_%u

3,stream(http/hls):
setup_source() → setup_streaming():创建 typefind,并 link source→typefind
typefind have-type → type_found():
make_decoder()
非 adaptive:插 queue2 或 downloadbuffer,然后 link typefind→queue→decodebin
adaptive:不插 queue,直接 link typefind→decodebin
decodebin pad-added → new_decoded_pad_added_cb() 暴露 ghost src_%u

uridecodebin
└─ source (souphttpsrc/httppullsrc/...)
└─ typefind
└─ (queue2 或 downloadbuffer) # 由 type_found() 决定
└─ decodebin
└─ raw src pads
└─ ghost src_%u

三,decodebin 的详细流程:
decodebin
└─ typefind (固定存在)
└─ (动态 autoplug)
├─ parser
├─ demuxer
│ └─ multiqueue
│ ├─ chain(audio): parser -> decoder -> ... -> raw endpad
│ ├─ chain(video): parser -> decoder -> ... -> raw endpad
│ └─ chain(text) : subparse/dec -> ... -> raw endpad
└─ (最终) expose src_%u (GstDecodePad=ghostpad) # 注意:expose 后才对外可见

2.1 decodebin READY→PAUSED:开始异步,等 typefind have-type
gst_decode_bin_change_state(READY_TO_PAUSED):

清理旧 decode_chain
do_async_start()
connect(typefind, "have-type", type_found)
所以 decodebin 的根永远从 typefind 开始

2.1 decodebin READY→PAUSED:开始异步,等 typefind have-type
gst_decode_bin_change_state(READY_TO_PAUSED):

清理旧 decode_chain
do_async_start()
connect(typefind, "have-type", type_found)
所以 decodebin 的根永远从 typefind 开始

3) analyze_new_pad():这是 decodebin autoplug 的“总调度器”🧠
对一个新 pad(可能来自 typefind、demuxer、parser、decoder)它做:

autoplug-continue(只对 fixed caps)
如果返回 FALSE → 认为“到此为止”,直接 expose
playbin 在上层可以用它“让字幕直接暴露/阻止继续解码”等
检查是否已是 final caps:are_final_caps()
默认 final caps 是 dbin->caps,通常是 raw caps(audio/video/x-raw)
如果 caps 非 fixed → 挂 notify::caps 延迟处理(pending_pads)
获取可用 factories:发出 autoplug-factories
发出 autoplug-sort(可选)
进入 connect_pad():尝试一个个 factory,创建 element 并 link,递归向下。
关键映射到上层回调链:
decodebin autoplug- → uridecodebin proxy → playbin autoplug__cb**(你前面那套 factories/select/continue/query 就是在这里生效的)。

4) connect_pad():真正“创建 element 并把树接起来”的地方
4.1 demux 分叉:识别 demuxer → 插 multiqueue → 为每路流创建子 chain
当 is_demuxer(即“第一次从 demuxer 出来的 pad”)时:

默认会把 demuxer src pad 先接到 gst_decode_group_control_demuxer_pad():
multiqueue.request_pad("sink_%u")
demuxer_pad → mq sinkpad
取对应 mq srcpad(通过 internal-links)
然后后续对这个 mq srcpad 再继续 autoplug
4.2 对每个候选 factory:autoplug-select 决定 TRY/EXPOSE/SKIP
在创建 element 前:
g_signal_emit(SIGNAL_AUTOPLUG_SELECT, dpad, caps, factory, &ret);
TRY:继续创建并尝试 link
EXPOSE:不插 element,直接把当前 pad expose(常用于 sink 直接能接收、或字幕/特殊流希望原样输出)
SKIP:跳过这个 factory
在 playbin 里 autoplug_select_cb() 你们实现了:

如果 factory 是 sink,会尝试创建并 activate_sink() 验证能 READY 且接受 caps,然后返回 EXPOSE,让 decodebin 直接暴露 raw pad 给上层,由 playbin 自己接到 playsink/selector。✅
如果 factory 是 decoder,则会按你们的 decoder/sink 优先级列表(aelements/velements)做兼容性筛选
4.3 element 创建与“探测可用性”
decodebin 在 TRY 路径下会做一套非常稳的“试用流程”:
gst_element_factory_create(factory)
gst_element_set_locked_state(element, TRUE)(先不让 bin 控制状态)
gst_bin_add(dbin, element)
找 sink pad → link 上游 pad → sinkpad
gst_element_set_state(element, READY)
gst_pad_query_accept_caps(sinkpad, caps) 再确认
失败就 remove 并继续尝试下一个 factory。
这解释了为什么 decodebin 看起来“尝试了很多插件”但最终只留一条成功链路

4.4 connect_element() + 递归 analyze_new_pad()
element 连接完成后:

connect_element() 找 element 的 src pads:
ALWAYS/SOMETIMES 的已存在 pad 立刻纳入 to_connect
如果有动态 pads(SOMETIMES 未出现)就连 pad-added/no-more-pads 回调
对每个 output pad 再回到 analyze_new_pad()(递归长树)
所以 decodebin 的“树生长”是一个典型 DFS/递归过程

四,整体逻辑梳理:
playbin (pipeline)
├─ playsink
├─ uridecodebin(main)
│ └─ source -> (typefind -> queue2/downloadbuffer)? -> decodebin -> ghost src_%u(audio/video/text)
├─ uridecodebin(sub1)
│ └─ source -> ... -> decodebin -> ghost src_%u(text/subpicture)
├─ uridecodebin(sub2)
│ └─ ...
├─ input-selector (audio combiner)
│ ├─ sink_0 <- main src_%u(audio/x-raw) # 多音轨则多个 sink_n
│ └─ src -> playsink audio pad
├─ input-selector (video combiner)
│ ├─ sink_0 <- main src_%u(video/x-raw)
│ └─ src -> playsink video pad
└─ input-selector (text combiner)
├─ sink_0 <- main 内嵌字幕 src_%u(text/subpicture)
├─ sink_1 <- sub1 外挂字幕 src_%u(...)
└─ src -> playsink text pad

以mp4播放为例子梳理播放pipeline:

playbin (GstPipeline)
├─ playsink
│ ├─ (内部 audio chain) ... -> audiosink
│ ├─ (内部 video chain) ... -> videosink
│ └─ (内部 text/sub overlay chain) ...
│
├─ uridecodebin(main) # activate_group() 创建
│ └─ (内部见第2节)
│
├─ input-selector (audio combiner) # playbin pad_added_cb() 首次见到音频创建
│ ├─ sink_0 <--- uridecodebin(main):src_0 (audio/x-raw)
│ ├─ sink_1 <--- uridecodebin(main):src_3 (audio/x-raw) # 多音轨时
│ └─ src ---> playsink:audio_sinkpad (request)
│
├─ input-selector (video combiner)
│ ├─ sink_0 <--- uridecodebin(main):src_1 (video/x-raw)
│ └─ src ---> playsink:video_sinkpad (request)
│
└─ input-selector (text combiner) # 有字幕流时
├─ sink_0 <--- uridecodebin(main):src_2 (text/x-raw 或 subpicture/..)
├─ sink_1 <--- uridecodebin(sub1):src_0 (...)
└─ src ---> playsink:text_sinkpad (request)

以hls 协议为例子看问题的pipeline:
playbin
└─ uridecodebin(main)
└─ source(httpsrc/httppullsrc/...)
└─ typefind (caps=application/x-hls)
└─ decodebin
└─ hlsdemux (Adaptive Demux)
├─ (可能) tsdemux / qtdemux / fmp4demux # 取决于分片格式:TS or fMP4
│ ├─ audio pad -> aacparse -> aac decoder -> audio/x-raw -> decodebin:src_%u
│ └─ video pad -> h264parse -> h264 decoder -> video/x-raw -> decodebin:src_%u
└─ (有字幕则多一支路)

decodebin pipeline:
decodebin
└─ typefind ("typefind") # gst_decode_bin_init() 固定创建并 ghost "sink"
└─ qtdemux # connect_pad() 选 factory 后 create+link
└─ (multiqueue) # gst_decode_group_new() 创建;demuxer_pad -> mq
├─ queue branch A (audio pad from demux)
│ └─ aacparse (optional)
│ └─ faad/avdec_aac/硬解aac...
│ └─ (可能还有 audioconvert 等取决于插件链)
│ └─ [ENDPAD] audio/x-raw # GstDecodePad (ghost) 被 expose 成 decodebin:src_%u
│
└─ queue branch B (video pad from demux)
└─ h264parse (optional)
└─ avdec_h264/硬解h264...
└─ (可能还有 videoconvert 等)
└─ [ENDPAD] video/x-raw # expose 成 decodebin:src_%u

1:
gst_decode_bin_change_state(READY→PAUSED)

connect typefind::have-type → type_found()

2:type_found()

创建 top chain:gst_decode_chain_new(dbin, NULL, typefind.srcpad)
analyze_new_pad(dbin, typefind, typefind.srcpad, caps, chain, NULL)

3:analyze_new_pad() 发现 caps 不是 final(不是 raw)

发出 autoplug-factories(默认返回所有可接 caps 的 factory;但你们上层 playbin/uridecodebin 会插手重排/筛选)
进入 connect_pad() 尝试 factory

4:connect_pad() 选中 qtdemux 并创建 element、link:
gst_element_factory_create(qtdemux)
link typefind.srcpad -> qtdemux.sink
connect_element() 给 qtdemux 连接 pad-added/no-more-pads

5.qtdemux::pad-added → decodebin: pad_added_cb()

对每个 demux pad 再做一次 analyze_new_pad()
因为这是 demuxer 的分叉点,会创建 GstDecodeGroup,并插入 multiqueue:
gst_decode_group_new() 创建 multiqueue,并 decodebin_set_queue_size_full(preroll...)
gst_decode_group_control_demuxer_pad():demux pad -> mq.sink_%u,得到 mq.srcpad
后续对 mq.srcpad 继续 autoplug parser/decoder

6.当某条分支最终输出 raw(are_final_caps==TRUE 或 autoplug-continue==FALSE)

expose_pad():创建/激活 GstDecodePad(ghost),并 block target pad
若整棵树 complete:gst_decode_bin_expose()
gst_element_add_pad(decodebin, src_%u) + gst_element_no_more_pads(decodebin)
gst_decode_pad_unblock() 让流开始走

curlhttpsrc 下载总体框架:
1,两个队列:
src->queue:数据队列(装 GstBuffer)
src->downloadMsgQueue:消息队列(装 GstCurlHttpSrcMsg,包含 event、buf、seek startPos 等)
2,同步手段:
queue_lock + queue_cond:控制数据队列的“有数据/等待”
download_queuelock:保护消息队列
select_lock + select_cond:用于 seek 同步(create 等待 seek 完成)
stop_lock + stop_cond:用于 stop/clean request 的等待协调
request_lock:保护对 dl_handle 的 Curl_Create/Clean/Send 等调用,避免并发调用

3,生命周期:
2.1 start:启动下载线程 + Curl_Open + 发起请求
gst_curl_http_src_start():

gst_curl_http_src_reset() 清零状态(consume_len/download_len/total_len 等)
src->downloadloopRun = TRUE; gst_task_start(download_task);
Curl_Open(&src->dl_handle, DL_MULTI_MODE);
gst_curl_http_src_do_request(src, FALSE); 立即开始一次 GET(或 Range GET)
这里说明:download_task 只是负责“处理回调消息并推进状态”,真正下载由 Curl 库内部异步执行,下载结果通过 gst_curl_http_src_request_buffer_cb() 回调进来。

2.2 Curl 回调:把网络数据转成 GstBuffer 并入队
gst_curl_http_src_request_buffer_cb() 是 curl 库回调入口,分两大类:

A) 数据到达:DL_EVT_DATA_SUCCESS(或者 ginga 的 DL_EVT_SUCCESS)
逻辑:

msg = malloc(GstCurlHttpSrcMsg),msg->event = DL_EVT_DATA_SUCCESS
分配 msg->buf = gst_buffer_new_allocate(NULL, data_len, NULL),把 *data memcpy 进去
src->download_len += data_len
如果还不知道总长度:用 cb_data->content_len 推断 src->total_len
还会更新 basesrc->segment.duration = src->total_len
发送 DURATION_CHANGED message
发送 GST_MESSAGE_MSG_DOWNLOAD_BEGIN 自定义 message
节流(关键缓冲机制之一):当 download_len - consume_len > MSG_QUEUE_MAX_SIZE(40MB) 时,回调会 sleep 等待消费推进
这是一种“下载端自限速”,防止内存无限增长
把 msg 入 downloadMsgQueue
B) 非数据事件:停止/完成/失败/重定向等
对 DL_EVT_STOP_SUCCESS/DL_EVT_DOWNLOAD_FINISH:
如果正在 seek:把 msg 转成 SEEK_EVENT + startPos
否则标记 download finish,并记录 dl_handle
对失败类:DL_EVT_FAILED / DL_EVT_DOWNLOAD_FAILED
处理重定向:cb_data->redirect_url → 写入 src->redirection_uri
最后同样:把 msg 入 downloadMsgQueue(除非 stoped)

2.3 download_loop:消费消息队列,真正把 buf 推到数据队列
gst_curl_http_src_download_loop() 主循环:

锁 download_queuelock,如果 downloadMsgQueue 非空,pop 一个 msg
按 msg->event 分支:
(1) SEEK_EVENT:重置队列与偏移并重新发请求 🔁
gst_curl_http_src_clear_msgqueue()、gst_curl_http_src_clear_queue() 清空旧数据
consume_len = startPos; download_len = startPos;
src->ret = GST_FLOW_OK; src->is_seek = FALSE; signal(select_cond)
调用 gst_curl_http_src_do_request(src, FALSE) 发 Range 请求从 startPos 继续
这段实现了“seek 之后 create() 线程继续工作”的同步点:create 在等 is_seek 变 FALSE。🧷

(2) DL_EVT_DATA_SUCCESS:把 msg->buf 放入数据队列 ✅
GST_BUFFER_OFFSET(msg->buf) = src->consume_len(注意:这里 offset 用的是 当前 consume_len,严格来说更像“当前读指针”,不是下载位置)
gst_curl_http_src_push_data_to_queue(src, msg->buf):push 到 src->queue,signal(queue_cond)
此外还有 end_offset 逻辑:

如果设置了 end_offset 且 download_len >= end_offset:
有些情况下会 Curl_CleanRequest() 结束下载
当 consume_len >= end_offset,会制造一个 DL_EVT_DOWNLOAD_FINISH msg 进入队列,触发后续 EOS
(3) 失败:DL_EVT_FAILED / DL_EVT_DOWNLOAD_FAILED:重试或报错 💥
先 Curl_CleanRequest(dl_handle)(带 request_lock,避免并发)
进入 retry 策略:
reload_times++
满足:reload_times < 3 && !disable_retry && response_code 不是 4xx/5xx
清空队列
download_len = consume_len(从已消费位置续传)
gst_curl_http_src_do_request(src, TRUE) 再发起 Range 请求
否则跳到 download_error:
GST_ELEMENT_ERROR(...) 按 response_code 分类
src->ret = GST_FLOW_CUSTOM_SUCCESS(注意:这是一个比较奇怪的返回码,可能你们用来让上层识别“下载失败但不直接 ERROR”?)
signal(queue_cond) 唤醒 create 退出
(4) DL_EVT_DOWNLOAD_FINISH:最终 EOS 触发 🧾
如果仍觉得没下完且可重试:走 Curl_CreateResumeRequest() 续传(最多 3 次)
否则:
post 自定义 message:GST_MESSAGE_MSG_DOWNLOAD_EOS
等待消费线程把队列读到尾(while consume_len < total_len … sleep)
设置 src->ret = GST_FLOW_EOS; signal(queue_cond)
create() 之后返回 EOS

2.4 create:从数据队列取一个 buffer(消费者)🍽️
gst_curl_http_src_create():

如果 cancel_download:返回 GST_FLOW_FLUSHING
等待 seek 完成:
while(src->is_seek) wait(select_cond)
gst_curl_http_src_pop_data_from_queue():
如果队列为空且 src->ret==GST_FLOW_OK:wait(queue_cond)
pop 一个 buffer
src->consume_len += gst_buffer_get_size(buf)
如果 src->ret != OK 或 stoped:释放 outbuf,返回对应 flow(EOS/FLUSHING/CUSTOM等)

2.5 stop:停止下载、清队列、让 create 退出 🛑
gst_curl_http_src_stop():

stoped = TRUE; 清掉 start/end offset
pause 下载 task,并 signal stop_cond
request_lock 下 Curl_CleanRequest(),并 STOP_COND_WAIT 等待回调确认
Curl_Close(dl_handle)
清空 data queue / msg queue
src->ret = GST_FLOW_EOS; signal(queue_cond) 让 create 线程退出

2.6下载的数据如果传到后端组件,例如decodebin;
libcurl callback thread
└─ 写 src->buffer + signal buffer_cond

gst_curl_http_src_create()
├─ 等待 buffer_cond
├─ gst_buffer_new_allocate()
├─ memcpy HTTP 数据
└─ return GST_FLOW_OK + outbuf
↓
GstBaseSrc (内部)
└─ gst_pad_push(srcpad, outbuf)
↓
typefind
└─ demux / decodebin
↓
audio/video raw pads

2.7 curl 数据从PLAYING到首帧的逻辑通路:
2.7.1:
playbin → uridecodebin → curlhttpsrc
gst_base_src_start() 被调用
curlhttpsrc 内部:
创建 curl easy handle
请求 m3u8 / mpd(HLS/DASH)
download 线程启动
2.7.2:
curlhttpsrc 开始产生 首批 buffer,在你的 gst_curl_http_src_create() 中:
while (src->buffer_len == 0 && state OK)
g_cond_wait(buffer_cond);
一旦 curl 回调收到数据:
HTTP response body(m3u8 / mpd / segment)
memcpy → src->buffer
signal(buffer_cond)
然后:*outbuf = gst_buffer_new_allocate(...)
return GST_FLOW_OK;
2.7.3:
typefind 介入,缓存前 N 个 buffer,分析 magic/header,决定 caps
2.7.4:
decodebin 进入 adaptive 模式(HLS / DASH)
2.7.5:
Manifest 解析(m3u8 / mpd),
hlsdemux / dashqtdemux:
消费 curlhttpsrc 提供的 buffer
解析 manifest
决定:
variant / representation
segment URL
初始化 segment(init.mp4)
这个阶段:
不会出 audio/video pad
只是“控制面解析”
所以你会看到:有网络流量,但没首帧
2.7.6::
开始请求第一个 media segment,curlhttpsrc → 新的 HTTP 请求(segment),curlhttpsrc 又开始吐 segment 的二进制 buffer,这些 buffer 再次走
curlhttpsrc → typefind(这次是 video/quicktime / video/mpegts)
2.7.7:二级 demux + parser + decoder 插入
hlsdemux
└─ tsdemux
├─ h264parse → avdec_h264 / v4l2h264dec
└─ aacparse → faad / avdec_aac
2.7.8:decodebin 的 expose 条件(首帧关键点):
decodebin 不会立刻 expose pad,它要满足:
每个 group:
所有“必需流”(audio/video)已发现
decoder 完成 preroll
pad 不再 block
2.7.9:
pad expose → playbin selector → playsink
uridecodebin 把 pad 转交给 playbin
playbin:
接入 input-selector
连接 playsink
unblock selector.src

3, httppullsrc+curl_interface 下载流程:
GStreamer上层拉取
└── gst_http_pull_src_fill()
├── 按 offset/length 读 queue
├── 不够则等待 queue_cond
└── 超范围则触发 seek/re-request

中间调度层
└── gst_http_pull_src_download_loop()
├── 处理下载回调消息 downloadMsgQueue
├── DATA_SUCCESS -> push 到 data queue
├── FINISH -> EOS / resume
├── FAILED -> retry / error
└── SEEK_EVENT -> 清队列并重发请求

底层下载层
└── curl_interface.c
├── Curl_Open() 创建 multi 下载线程
├── Curl_CreateRequest() 配 easy/multi
├── Curl_SendRequest() 启动请求
├── CurlDownload_DLTask() 驱动 curl_multi_perform/select
├── CurlDownload_WriteData() 收数据
├── CurlDownload_WriteHeader() 解析头
└── 通过 gst_http_pull_src_request_buffer_cb() 回调给 httppullsrc


三个线程的流转过程:
线程A:CurlDownload_DLTask
职责:
驱动 libcurl multi
select/socket 等待
接收 header/data 回调
在完成/失败时回调上层

线程B:gst_http_pull_src_download_loop
职责:
消费回调生成的消息
把数据塞到 src->queue
处理 seek/retry/eos/error

线程C:GStreamer pull 线程
职责:
调用 gst_http_pull_src_fill()
按 offset 从 src->queue 拷数据返回 demux
这个“三线程模型”是理解整套逻辑的关键

3.1:下载主流程,对应线程B:
gst_http_pull_src_change_state(),开启下载gst_http_pull_src_download_start()线程,下载线程主要做如下几件事儿:
3.1.1:gst_http_pull_src_reset(src);
3.1.2:启动内部下载任务,gst_task_start(src->download_task);,这个线程不是直接下载网络,而是处理下载消息队列 downloadMsgQueue。
3.1.3:打开 curl 下载句柄 Curl_Open(&src->dl_handle, DL_MULTI_MODE);

3.2:真正发起http 请求gst_http_pull_src_do_request:
3.2.1:先构造DL_HTTPPARAM_t
3.2.2:根据 consume_len / end_offset / total_len 拼 Range
3.2.3:真正创建并发起请求:
Curl_CreateRequest(src->dl_handle, gst_http_pull_src_request_buffer_cb, src, &http_param);
Curl_SendRequest(src->dl_handle, DL_GET_METHOD);

3.3:curl_interface.c 这一层的下载流程
3.3.1:Curl_Open(),初始化 DL_CONTEXT_t,启动curl_download 下载线程,对应线程A,Task_Create(CurlDownload_DLTask,dl_context,&dl_context->task_id);
3.3.2:Curl_CreateRequest(),保存 URL / callback / 状态清零,创建或 reset easy handle,设置 libcurl 参数,multi 模式加入 multi handle,dl_context->multi_handle = curl_multi_init();
curl_multi_add_handle(dl_context->multi_handle, dl_context->easy_handle);

3.3.3:Curl_SendRequest(),CurlDownload_SetTaskStatus(dl_context,DL_STATE_START);

3.4:curl 后台线程 CurlDownload_DLTask() 是下载引擎核心
3.4.1:状态机,DL_STATE_INIT DL_STATE_START DL_STATE_LOADING DL_STATE_FINISHIED DL_STATE_STOP DL_STATE_CLOSED
3.4.2:curl_multi_perform(dl_context->multi_handle, &stillRunning),驱动curl,网络超时会清理 curl 资源,并回调失败
3.4.3:收到数据会怎么走?CurlDownload_WriteHeader() header 回调,CurlDownload_WriteData() 数据回调
3.4.3.1:header 回调 主要做如下动作:
Content-Length
Content-Range
Transfer-Encoding: chunked
Location
Accept-Ranges
HTTP 状态码

3.4.3.2:data 回调主要做如下动作:
{
if(data_size > 0)
{
DL_CB_DATA_t cb_data = {0};
dl_context->total_len += data_size;
dl_context->response_data_len = dl_context->total_len;
cb_data.content_len = dl_context->content_len;
dl_context->respose_callback(&data,data_size,&(dl_context->callback_para),DL_EVT_DATA_SUCCESS,&cb_data);
}
}libcurl 每收到一块网络数据,就立即通过 gst_http_pull_src_request_buffer_cb() 回调给 httppullsrc。这里不是先缓存到 curl_interface 再一次性交付,而是 streaming callback

3.5:httppullsrc 的下载回调:gst_http_pull_src_request_buffer_cb():
3.5.1:收到 DL_EVT_DATA_SUCCESS,GstBuffer* tmpbuff = gst_buffer_new_allocate(NULL, data_len, NULL);
memcpy(info.data, *data, data_len);src->partial_spu = gst_buffer_append(src->partial_spu, tmpbuff)
3.5.2:更新下载进度src->download_len += data_len;
3.5.3:首次拿到 content_len 时更新 total_len
3.5.4:限流:防止 queue 太大,
3.5.5:聚合到 300KB 再投递消息,if (gst_buffer_get_size(src->partial_spu) < 300*1024 && src->download_len < src->total_len) {
push_msg = false;
}每次 curl 回调的数据不直接进主 queue,而是先聚合,约到 300KB 再封成消息推给 downloadMsgQueue。这样做的目的源码注释已经写了,避免每个小块都发消息,导致 download loop 来不及处理,影响 source 性能

3.6:download_loop 如何把消息转成真正可读 queue
3.6.1:收到 DL_EVT_DATA_SUCCESS,gst_http_pull_src_push_data_to_queue(src, msg->buf);g_queue_push_tail(src->queue,data);src->queue_len += gst_buffer_get_size(data);g_cond_signal(&queue_cond);
3.6.2:收到 DL_EVT_FAILED / DL_EVT_DOWNLOAD_FAILED:
调 Curl_CleanRequest()
判断是否允许重试
重试次数 < 3
不是 4xx/5xx
清消息队列/数据队列
把 download_len / queue_len 回退到 consume_len
再调 gst_http_pull_src_do_request(src, TRUE)
3.6.3:收到 DL_EVT_DOWNLOAD_FINISH,如果数据没下完整,还能继续;
3.6.3.1:数据真的下完了,post 一个 download eos message,等待已下载数据被上游消费,最后塞一个特殊的 EOS buffer:
3.6.4:收到 seek 触发的 SEEK_EVENT:
清消息队列
清数据队列
把 consume_len/download_len/queue_len 都改成目标 startPos
src->is_seek = FALSE
再次调用 gst_http_pull_src_do_request(src, FALSE)

3.7:上层真正取数据:gst_http_pull_src_fill():
3.7.1:请给我从 offset 开始的 length 字节,先判断是否能从 backup 读,这是为了处理某些 seek/backward 场景,避免立刻重新下载
3.7.2:判断 offset 是否超出当前缓存窗口,fill() 如果发现请求位置不在当前缓存窗口内,会阻塞等待新 seek 请求重新建链并下载数据。
3.7.3:如果 offset 在缓存窗口内,就直接从 queue 拷贝:
遍历 queue 中的多个 GstBuffer
找到 offset 对应位置
按需丢掉前缀 drop_len
连续 memcpy 到目标 buf,把网络下载分片 queue 拼接成上游想要的任意 offset/length 数据块。
3.7.4:如果数据还不够,就等待更多 buffer: fill() 是一个典型的 消费者阻塞模型:queue 有数据 -> 直接拷贝,queue 不够 -> 等下载线程继续塞
3.7.5:什么时候返回 EOS,读到特殊 EOS buffer或者外部stop/cancel

总结:
1. READY->PAUSED 时,httppullsrc 启动 download_task
2. 调 Curl_Open 创建 dl_handle 和 curl 后台线程
3. do_request() 按 consume_len 拼 Range
4. Curl_CreateRequest + Curl_SendRequest 发起 GET
5. curl 线程执行 multi_perform/select
6. 收到 header,解析 Content-Length / Content-Range / Accept-Ranges
7. 收到 data,WriteData 以 PART 模式回调给 httppullsrc
8. httppullsrc 回调里把小块数据拼成 partial_spu(约300KB)
9. partial_spu 封成消息放入 downloadMsgQueue
10. download_loop 消费消息,把 GstBuffer push 到 data queue
11. fill(offset,length) 从 data queue 拷贝数据给上游 demux
12. 下载结束时投递 EOS 标记 buffer,fill 返回 GST_FLOW_EOS


gstculhttpsrc.c 跟gsthttppullsrc.c 的模式的区别:

gstcurlhttpsrc 是 PUSH 模式,核心函数是gst_curl_http_src_create (GstPushSrc * psrc, GstBuffer ** outbuf):
source 自己不断产出下一块数据”
下游通常按顺序消费
更适合顺序 HTTP body 流式传输


gsthttppullsrc 是 GstBaseSrc的pull/fill 模式,核心函数gst_http_pull_src_fill (GstBaseSrc * src, guint64 offset, guint length, GstBuffer * buf):
上游按 offset 指定读取
source 必须回答“这个 offset 开始的 length 字节是什么”
支持随机访问 / seek / range 更自然


两种不同的模块跟libcurl的耦合层级不一样:

1,gstcurlhttpsrc:直接耦合 libcurl:
gstcurlhttpsrc.c 里直接:
curl_easy_init
curl_easy_setopt
curl_multi_init
curl_multi_add_handle
curl_multi_perform
curl_multi_info_read

2,gsthttppullsrc:通过 curl_interface.c 间接耦合,不直接调用 curl,而是调用:
Curl_Open
Curl_CreateRequest
Curl_SendRequest
Curl_CleanRequest
Curl_Close
Curl_CheckHeader

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进