如何通过GStreamer-Sharp获取摄像头物理采集时间戳(C#实现)
问题
刚接触GStreamer领域,使用C#的GStreamer-Sharp库搭建了RTSP管道:rtspsrc ! decodebin ! jpegenc ! appsink,希望获取摄像头实际采集图像的物理时间戳(而非PTS或DTS)。曾了解到需要用到RTCPBuffer、RTCPPacket,但不知如何在AppSink的NewSample事件处理器中实现该需求,寻求指导。
对应的管道代码如下:
private static Pipeline BuildRtspPipe() { // Build the pipeline var rtspSrc = ElementFactory.Make("rtspsrc", "rtspsrc"); rtspSrc["location"] = "rtsp://someaddr"; var dec = ElementFactory.Make("decodebin", "decodebin"); var jpegenc = ElementFactory.Make("jpegenc", "jpegenc"); var appSink = new AppSink(""); appSink.NewSample += AppSink_NewSample; appSink.EmitSignals = true; var pipeline = new Pipeline("test"); pipeline.Add(rtspSrc, dec, jpegenc, appSink); if ( !rtspSrc.Link(dec) ) { Console.WriteLine("Could not link pipeline elements"); } rtspSrc.PadAdded += (o, args) => { var decpad = dec.GetStaticPad("sink"); if (decpad.IsLinked) { return; } args.NewPad.Link(decpad); }; dec.PadAdded += (o, args) => { var jpegencpad = jpegenc.GetStaticPad("sink"); if (jpegencpad.IsLinked) { return; } var caps = args.NewPad.QueryCaps(); if (caps == null) return; args.NewPad.Link(jpegencpad); }; if (!Element.Link(jpegenc, appSink)) { Console.WriteLine("Could not link pipeline elements"); throw new Exception(); } return pipeline; }
解决方案
要获取摄像头采集的物理时间戳,核心是通过RTSP流中的*RTCP Sender Report(SR)*包提取NTP时间,再关联到对应的RTP数据包的时间戳,最终映射到AppSink中的Sample。具体实现步骤如下:
1. 监听rtspsrc的on-rtcp信号
rtspsrc收到RTCP包时会触发on-rtcp信号,我们需要在这里解析Sender Report,建立RTP时间戳到NTP物理时间的映射关系。
在构建管道时,给rtspsrc添加信号处理:
// 缓存RTP时间戳与NTP物理时间的映射,保留最近几条避免内存占用过大 private static Dictionary<uint, DateTime> _rtpToNtpMap = new Dictionary<uint, DateTime>(); // 给rtspsrc绑定RTCP信号处理方法 rtspSrc.Connect("on-rtcp", new SignalHandler(OnRtcpReceived));
2. 解析RTCP Sender Report提取时间映射
实现OnRtcpReceived方法,解析RTCP包中的Sender Report,将NTP时间和对应RTP时间戳存入缓存:
private static void OnRtcpReceived(object o, SignalArgs args) { var buffer = args[0] as Buffer; if (buffer == null) return; using var rtcpBuffer = new RTCPBuffer(buffer); while (rtcpBuffer.HasPacket()) { using var packet = rtcpBuffer.NextPacket(); if (packet.Type == RTCPType.SenderReport) { var sr = packet.SenderReport; // 解析NTP时间:高32位是秒数,低32位是小数部分 var ntpSeconds = sr.NtpTimestamp >> 32; var ntpFraction = sr.NtpTimestamp & 0xFFFFFFFF; // NTP纪元(1900-01-01)转Unix纪元(1970-01-01),差值为2208988800秒 var epochDiff = 2208988800; var secondsSinceUnixEpoch = ntpSeconds - epochDiff; var fractionalSeconds = (double)ntpFraction / 0x100000000; // 转换为UTC时间 var ntpTime = DateTimeOffset.FromUnixTimeSeconds(secondsSinceUnixEpoch) .AddSeconds(fractionalSeconds) .UtcDateTime; // 更新缓存 _rtpToNtpMap[sr.RtpTimestamp] = ntpTime; // 只保留最近10条映射 if (_rtpToNtpMap.Count > 10) { var oldestKey = _rtpToNtpMap.Keys.OrderBy(k => k).First(); _rtpToNtpMap.Remove(oldestKey); } } } }
3. 在AppSink的NewSample事件中计算物理时间戳
在AppSink_NewSample方法中,从Sample的Buffer中提取RTP时间戳,再通过缓存的映射关系计算出摄像头采集的物理时间:
private static FlowReturn AppSink_NewSample(AppSink sender) { using var sample = sender.PullSample(); using var buffer = sample.Buffer; // 提取Buffer中的RTP元数据 if (buffer.GetMeta<RTPBufferMeta>(out var rtpMeta)) { var rtpTimestamp = rtpMeta.Timestamp; // 查找最接近当前RTP时间戳的映射条目 var closestEntry = _rtpToNtpMap .OrderBy(kv => Math.Abs((long)kv.Key - (long)rtpTimestamp)) .FirstOrDefault(); if (closestEntry.Key != 0) { // 大多数视频流的RTP时钟频率为90kHz,需根据实际流调整 var rtpClockRate = 90000; // 计算当前RTP时间戳与映射点的时间差 var timeDiff = (rtpTimestamp - closestEntry.Key) / (double)rtpClockRate; // 得到摄像头采集的物理时间戳 var captureTime = closestEntry.Value.AddSeconds(timeDiff); Console.WriteLine($"采集物理时间戳:{captureTime:yyyy-MM-dd HH:mm:ss.fff}"); } } return FlowReturn.Ok; }
关键注意事项
- RTP时钟频率确认:不同编码的RTP时钟频率不同,比如H.264、MJPEG为90kHz,音频可能是44.1kHz。可在decodebin的PadAdded事件中解析caps获取准确值。
- 时区转换:代码返回的是UTC时间,可根据需求转换为本地时区。
- 缓存维护:定期清理过期的映射条目,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Sharov
相关产品推荐
相关产品推荐

