github.com/pion/webrtc/v4@v4.0.1/pkg/media/samplebuilder/samplebuilder.go (about) 1 // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly> 2 // SPDX-License-Identifier: MIT 3 4 // Package samplebuilder provides functionality to reconstruct media frames from RTP packets. 5 package samplebuilder 6 7 import ( 8 "math" 9 "time" 10 11 "github.com/pion/rtp" 12 "github.com/pion/webrtc/v4/pkg/media" 13 ) 14 15 // SampleBuilder buffers packets until media frames are complete. 16 type SampleBuilder struct { 17 maxLate uint16 // how many packets to wait until we get a valid Sample 18 maxLateTimestamp uint32 // max timestamp between old and new timestamps before dropping packets 19 buffer [math.MaxUint16 + 1]*rtp.Packet 20 preparedSamples [math.MaxUint16 + 1]*media.Sample 21 22 // Interface that allows us to take RTP packets to samples 23 depacketizer rtp.Depacketizer 24 25 // sampleRate allows us to compute duration of media.SamplecA 26 sampleRate uint32 27 28 // the handler to be called when the builder is about to remove the 29 // reference to some packet. 30 packetReleaseHandler func(*rtp.Packet) 31 32 // filled contains the head/tail of the packets inserted into the buffer 33 filled sampleSequenceLocation 34 35 // active contains the active head/tail of the timestamp being actively processed 36 active sampleSequenceLocation 37 38 // prepared contains the samples that have been processed to date 39 prepared sampleSequenceLocation 40 41 lastSampleTimestamp *uint32 42 43 // number of packets forced to be dropped 44 droppedPackets uint16 45 46 // number of padding packets detected and dropped (this will be a subset of `droppedPackets`) 47 paddingPackets uint16 48 49 // allows inspecting head packets of each sample and then returns a custom metadata 50 packetHeadHandler func(headPacket interface{}) interface{} 51 52 // return array of RTP headers as Sample.RTPHeaders 53 returnRTPHeaders bool 54 } 55 56 // New constructs a new SampleBuilder. 57 // maxLate is how long to wait until we can construct a completed media.Sample. 58 // maxLate is measured in RTP packet sequence numbers. 59 // A large maxLate will result in less packet loss but higher latency. 60 // The depacketizer extracts media samples from RTP packets. 61 // Several depacketizers are available in package github.com/pion/rtp/codecs. 62 func New(maxLate uint16, depacketizer rtp.Depacketizer, sampleRate uint32, opts ...Option) *SampleBuilder { 63 s := &SampleBuilder{maxLate: maxLate, depacketizer: depacketizer, sampleRate: sampleRate} 64 for _, o := range opts { 65 o(s) 66 } 67 return s 68 } 69 70 func (s *SampleBuilder) tooOld(location sampleSequenceLocation) bool { 71 if s.maxLateTimestamp == 0 { 72 return false 73 } 74 75 var foundHead *rtp.Packet 76 var foundTail *rtp.Packet 77 78 for i := location.head; i != location.tail; i++ { 79 if packet := s.buffer[i]; packet != nil { 80 foundHead = packet 81 break 82 } 83 } 84 85 if foundHead == nil { 86 return false 87 } 88 89 for i := location.tail - 1; i != location.head; i-- { 90 if packet := s.buffer[i]; packet != nil { 91 foundTail = packet 92 break 93 } 94 } 95 96 if foundTail == nil { 97 return false 98 } 99 100 return timestampDistance(foundHead.Timestamp, foundTail.Timestamp) > s.maxLateTimestamp 101 } 102 103 // fetchTimestamp returns the timestamp associated with a given sample location 104 func (s *SampleBuilder) fetchTimestamp(location sampleSequenceLocation) (timestamp uint32, hasData bool) { 105 if location.empty() { 106 return 0, false 107 } 108 packet := s.buffer[location.head] 109 if packet == nil { 110 return 0, false 111 } 112 return packet.Timestamp, true 113 } 114 115 func (s *SampleBuilder) releasePacket(i uint16) { 116 var p *rtp.Packet 117 p, s.buffer[i] = s.buffer[i], nil 118 if p != nil && s.packetReleaseHandler != nil { 119 s.packetReleaseHandler(p) 120 } 121 } 122 123 // purgeConsumedBuffers clears all buffers that have already been consumed by 124 // popping. 125 func (s *SampleBuilder) purgeConsumedBuffers() { 126 s.purgeConsumedLocation(s.active, false) 127 } 128 129 // purgeConsumedLocation clears all buffers that have already been consumed 130 // during a sample building method. 131 func (s *SampleBuilder) purgeConsumedLocation(consume sampleSequenceLocation, forceConsume bool) { 132 if !s.filled.hasData() { 133 return 134 } 135 136 switch consume.compare(s.filled.head) { 137 case slCompareInside: 138 if !forceConsume { 139 break 140 } 141 fallthrough 142 case slCompareBefore: 143 s.releasePacket(s.filled.head) 144 s.filled.head++ 145 } 146 } 147 148 // purgeBuffers flushes all buffers that are already consumed or those buffers 149 // that are too late to consume. 150 func (s *SampleBuilder) purgeBuffers(flush bool) { 151 s.purgeConsumedBuffers() 152 153 for (s.tooOld(s.filled) || (s.filled.count() > s.maxLate) || flush) && s.filled.hasData() { 154 if s.active.empty() { 155 // refill the active based on the filled packets 156 s.active = s.filled 157 } 158 159 if s.active.hasData() && (s.active.head == s.filled.head) { 160 // attempt to force the active packet to be consumed even though 161 // outstanding data may be pending arrival 162 if s.buildSample(true) != nil { 163 continue 164 } 165 166 // could not build the sample so drop it 167 s.active.head++ 168 s.droppedPackets++ 169 } 170 171 s.releasePacket(s.filled.head) 172 s.filled.head++ 173 } 174 } 175 176 // Push adds an RTP Packet to s's buffer. 177 // 178 // Push does not copy the input. If you wish to reuse 179 // this memory make sure to copy before calling Push 180 func (s *SampleBuilder) Push(p *rtp.Packet) { 181 s.buffer[p.SequenceNumber] = p 182 183 switch s.filled.compare(p.SequenceNumber) { 184 case slCompareVoid: 185 s.filled.head = p.SequenceNumber 186 s.filled.tail = p.SequenceNumber + 1 187 case slCompareBefore: 188 s.filled.head = p.SequenceNumber 189 case slCompareAfter: 190 s.filled.tail = p.SequenceNumber + 1 191 case slCompareInside: 192 break 193 } 194 s.purgeBuffers(false) 195 } 196 197 // Flush marks all samples in the buffer to be popped. 198 func (s *SampleBuilder) Flush() { 199 s.purgeBuffers(true) 200 } 201 202 const secondToNanoseconds = 1000000000 203 204 // buildSample creates a sample from a valid collection of RTP Packets by 205 // walking forwards building a sample if everything looks good clear and 206 // update buffer+values 207 // nolint: gocognit 208 func (s *SampleBuilder) buildSample(purgingBuffers bool) *media.Sample { 209 if s.active.empty() { 210 s.active = s.filled 211 } 212 213 if s.active.empty() { 214 return nil 215 } 216 217 if s.filled.compare(s.active.tail) == slCompareInside { 218 s.active.tail = s.filled.tail 219 } 220 221 var consume sampleSequenceLocation 222 223 for i := s.active.head; s.buffer[i] != nil && s.active.compare(i) != slCompareAfter; i++ { 224 if s.depacketizer.IsPartitionTail(s.buffer[i].Marker, s.buffer[i].Payload) { 225 consume.head = s.active.head 226 consume.tail = i + 1 227 break 228 } 229 headTimestamp, hasData := s.fetchTimestamp(s.active) 230 if hasData && s.buffer[i].Timestamp != headTimestamp { 231 consume.head = s.active.head 232 consume.tail = i 233 break 234 } 235 } 236 237 if consume.empty() { 238 return nil 239 } 240 241 if !purgingBuffers && s.buffer[consume.tail] == nil { 242 // wait for the next packet after this set of packets to arrive 243 // to ensure at least one post sample timestamp is known 244 // (unless we have to release right now) 245 return nil 246 } 247 248 sampleTimestamp, _ := s.fetchTimestamp(s.active) 249 afterTimestamp := sampleTimestamp 250 251 // scan for any packet after the current and use that time stamp as the diff point 252 for i := consume.tail; i < s.active.tail; i++ { 253 if s.buffer[i] != nil { 254 afterTimestamp = s.buffer[i].Timestamp 255 break 256 } 257 } 258 259 // the head set of packets is now fully consumed 260 s.active.head = consume.tail 261 262 // prior to decoding all the packets, check if this packet 263 // would end being disposed anyway 264 if !s.depacketizer.IsPartitionHead(s.buffer[consume.head].Payload) { 265 isPadding := false 266 for i := consume.head; i != consume.tail; i++ { 267 if s.lastSampleTimestamp != nil && *s.lastSampleTimestamp == s.buffer[i].Timestamp && len(s.buffer[i].Payload) == 0 { 268 isPadding = true 269 } 270 } 271 s.droppedPackets += consume.count() 272 if isPadding { 273 s.paddingPackets += consume.count() 274 } 275 s.purgeConsumedLocation(consume, true) 276 s.purgeConsumedBuffers() 277 return nil 278 } 279 280 // merge all the buffers into a sample 281 data := []byte{} 282 var metadata interface{} 283 var rtpHeaders []*rtp.Header 284 for i := consume.head; i != consume.tail; i++ { 285 p, err := s.depacketizer.Unmarshal(s.buffer[i].Payload) 286 if err != nil { 287 return nil 288 } 289 if i == consume.head && s.packetHeadHandler != nil { 290 metadata = s.packetHeadHandler(s.depacketizer) 291 } 292 if s.returnRTPHeaders { 293 h := s.buffer[i].Header.Clone() 294 rtpHeaders = append(rtpHeaders, &h) 295 } 296 297 data = append(data, p...) 298 } 299 samples := afterTimestamp - sampleTimestamp 300 301 sample := &media.Sample{ 302 Data: data, 303 Duration: time.Duration((float64(samples)/float64(s.sampleRate))*secondToNanoseconds) * time.Nanosecond, 304 PacketTimestamp: sampleTimestamp, 305 PrevDroppedPackets: s.droppedPackets, 306 Metadata: metadata, 307 RTPHeaders: rtpHeaders, 308 } 309 310 s.droppedPackets = 0 311 s.paddingPackets = 0 312 s.lastSampleTimestamp = new(uint32) 313 *s.lastSampleTimestamp = sampleTimestamp 314 315 s.preparedSamples[s.prepared.tail] = sample 316 s.prepared.tail++ 317 318 s.purgeConsumedLocation(consume, true) 319 s.purgeConsumedBuffers() 320 321 return sample 322 } 323 324 // Pop compiles pushed RTP packets into media samples and then 325 // returns the next valid sample (or nil if no sample is compiled). 326 func (s *SampleBuilder) Pop() *media.Sample { 327 _ = s.buildSample(false) 328 if s.prepared.empty() { 329 return nil 330 } 331 var result *media.Sample 332 result, s.preparedSamples[s.prepared.head] = s.preparedSamples[s.prepared.head], nil 333 s.prepared.head++ 334 return result 335 } 336 337 // seqnumDistance computes the distance between two sequence numbers 338 func seqnumDistance(x, y uint16) uint16 { 339 diff := int16(x - y) 340 if diff < 0 { 341 return uint16(-diff) 342 } 343 344 return uint16(diff) 345 } 346 347 // timestampDistance computes the distance between two timestamps 348 func timestampDistance(x, y uint32) uint32 { 349 diff := int32(x - y) 350 if diff < 0 { 351 return uint32(-diff) 352 } 353 354 return uint32(diff) 355 } 356 357 // An Option configures a SampleBuilder. 358 type Option func(o *SampleBuilder) 359 360 // WithPacketReleaseHandler set a callback when the builder is about to release 361 // some packet. 362 func WithPacketReleaseHandler(h func(*rtp.Packet)) Option { 363 return func(o *SampleBuilder) { 364 o.packetReleaseHandler = h 365 } 366 } 367 368 // WithPacketHeadHandler set a head packet handler to allow inspecting 369 // the packet to extract certain information and return as custom metadata 370 func WithPacketHeadHandler(h func(headPacket interface{}) interface{}) Option { 371 return func(o *SampleBuilder) { 372 o.packetHeadHandler = h 373 } 374 } 375 376 // WithMaxTimeDelay ensures that packets that are too old in the buffer get 377 // purged based on time rather than building up an extraordinarily long delay. 378 func WithMaxTimeDelay(maxLateDuration time.Duration) Option { 379 return func(o *SampleBuilder) { 380 totalMillis := maxLateDuration.Milliseconds() 381 o.maxLateTimestamp = uint32(int64(o.sampleRate) * totalMillis / 1000) 382 } 383 } 384 385 // WithRTPHeaders enables to collect RTP headers forming a Sample. 386 // Useful for accessing RTP extensions associated to the Sample. 387 func WithRTPHeaders(enable bool) Option { 388 return func(o *SampleBuilder) { 389 o.returnRTPHeaders = enable 390 } 391 }