feat(gateway): relay complete encoded frames
This commit is contained in:
+107
-4
@@ -28,7 +28,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
nativeApolloVideoQueuePackets = 256
|
||||
nativeApolloVideoQueuePackets = 16
|
||||
nativeApolloVideoQueueBytes = 4 << 20
|
||||
nativeApolloVideoQueueLatency = 250 * time.Millisecond
|
||||
nativeApolloAudioQueuePackets = 16
|
||||
nativeApolloEventQueuePackets = 16
|
||||
)
|
||||
@@ -316,6 +318,9 @@ type nativeApolloSession struct {
|
||||
mediaRecovered atomic.Uint64
|
||||
mediaEnqueued atomic.Uint64
|
||||
mediaQueueMaximum atomic.Uint64
|
||||
mediaQueueBytes atomic.Int64
|
||||
mediaQueueMaximumBytes atomic.Uint64
|
||||
mediaQueueSequence atomic.Uint64
|
||||
}
|
||||
|
||||
func newNativeApolloSession(sessionID string) *nativeApolloSession {
|
||||
@@ -762,10 +767,22 @@ func (s *nativeApolloSession) quiesceMedia() {
|
||||
|
||||
func (s *nativeApolloSession) closeMediaChannels() {
|
||||
s.channelsOnce.Do(func() {
|
||||
s.mediaQuiesced.Store(true)
|
||||
s.mediaMu.Lock()
|
||||
defer s.mediaMu.Unlock()
|
||||
close(s.video)
|
||||
close(s.audio)
|
||||
for {
|
||||
select {
|
||||
case media := <-s.video:
|
||||
if media.expiry != nil {
|
||||
media.expiry.Stop()
|
||||
}
|
||||
media.releaseQueue()
|
||||
default:
|
||||
close(s.video)
|
||||
close(s.audio)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -775,9 +792,66 @@ func (s *nativeApolloSession) enqueueMedia(output chan ProviderMedia, payload []
|
||||
if len(payload) == 0 || s.mediaQuiesced.Load() {
|
||||
return false
|
||||
}
|
||||
if output == s.video && len(payload) > maxCompleteFrameBytes {
|
||||
s.mediaDrops.Add(1)
|
||||
return false
|
||||
}
|
||||
s.mediaRecovered.Add(1)
|
||||
media := ProviderMedia{Payload: payload, ReceivedAt: receivedAt, EnqueuedAt: time.Now()}
|
||||
if pushLatest(output, media) {
|
||||
if output == s.video {
|
||||
dropped := uint64(0)
|
||||
for s.mediaQueueBytes.Load()+int64(len(payload)) > nativeApolloVideoQueueBytes {
|
||||
select {
|
||||
case replaced := <-output:
|
||||
if replaced.expiry != nil {
|
||||
replaced.expiry.Stop()
|
||||
}
|
||||
replaced.releaseQueue()
|
||||
dropped++
|
||||
default:
|
||||
s.mediaDrops.Add(dropped + 1)
|
||||
return false
|
||||
}
|
||||
}
|
||||
media.queueID = s.mediaQueueSequence.Add(1)
|
||||
media.accounting = &providerMediaQueueAccounting{
|
||||
bytes: int64(len(payload)), total: &s.mediaQueueBytes,
|
||||
}
|
||||
currentBytes := uint64(s.mediaQueueBytes.Add(int64(len(payload))))
|
||||
media.expiry = time.AfterFunc(nativeApolloVideoQueueLatency, func() {
|
||||
s.expireVideo(media.queueID)
|
||||
media.releaseQueue()
|
||||
})
|
||||
for maximum := s.mediaQueueMaximumBytes.Load(); currentBytes > maximum && !s.mediaQueueMaximumBytes.CompareAndSwap(maximum, currentBytes); maximum = s.mediaQueueMaximumBytes.Load() {
|
||||
}
|
||||
if dropped > 0 {
|
||||
s.mediaDrops.Add(dropped)
|
||||
}
|
||||
}
|
||||
dropped := false
|
||||
select {
|
||||
case output <- media:
|
||||
default:
|
||||
select {
|
||||
case replaced := <-output:
|
||||
if replaced.expiry != nil {
|
||||
replaced.expiry.Stop()
|
||||
}
|
||||
replaced.releaseQueue()
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case output <- media:
|
||||
dropped = true
|
||||
default:
|
||||
if media.expiry != nil {
|
||||
media.expiry.Stop()
|
||||
}
|
||||
media.releaseQueue()
|
||||
dropped = true
|
||||
}
|
||||
}
|
||||
if dropped {
|
||||
s.mediaDrops.Add(1)
|
||||
}
|
||||
s.mediaEnqueued.Add(1)
|
||||
@@ -787,6 +861,35 @@ func (s *nativeApolloSession) enqueueMedia(output chan ProviderMedia, payload []
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) expireVideo(queueID uint64) {
|
||||
s.mediaMu.Lock()
|
||||
defer s.mediaMu.Unlock()
|
||||
if s.mediaQuiesced.Load() {
|
||||
return
|
||||
}
|
||||
retained := make([]ProviderMedia, 0, cap(s.video))
|
||||
removed := false
|
||||
for {
|
||||
select {
|
||||
case media := <-s.video:
|
||||
if media.queueID == queueID {
|
||||
removed = true
|
||||
media.releaseQueue()
|
||||
continue
|
||||
}
|
||||
retained = append(retained, media)
|
||||
default:
|
||||
for _, media := range retained {
|
||||
s.video <- media
|
||||
}
|
||||
if removed {
|
||||
s.mediaDrops.Add(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) readUDPMedia() {
|
||||
if s.media == nil {
|
||||
close(s.readDone)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
@@ -531,19 +532,6 @@ func TestNativeApolloSetupRequiresModernEncryptedRTSPOrder(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
awaitControl(apolloControlTypeFEC, false)
|
||||
remote := <-controlRemote
|
||||
hostTermination := sourceSealHostControl(t, material.key, 0, apolloControlTypeTerm, []byte{1, 2, 3, 4})
|
||||
if _, err := controlServer.WriteToUDP(sourceShapedENetReliablePacketOn(7, 2, apolloChannelGeneric, 1, hostTermination), remote); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case event := <-session.Events():
|
||||
if event.Kind != ProviderEventTerminated || string(event.Payload) != string([]byte{1, 2, 3, 4}) {
|
||||
t.Fatalf("provider termination event = %#v", event)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("encrypted host termination was not forwarded")
|
||||
}
|
||||
select {
|
||||
case media := <-session.Video():
|
||||
payload := media.Payload
|
||||
@@ -562,6 +550,19 @@ func TestNativeApolloSetupRequiresModernEncryptedRTSPOrder(t *testing.T) {
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("source-shaped audio was not relayed")
|
||||
}
|
||||
remote := <-controlRemote
|
||||
hostTermination := sourceSealHostControl(t, material.key, 0, apolloControlTypeTerm, []byte{1, 2, 3, 4})
|
||||
if _, err := controlServer.WriteToUDP(sourceShapedENetReliablePacketOn(7, 2, apolloChannelGeneric, 1, hostTermination), remote); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case event := <-session.Events():
|
||||
if event.Kind != ProviderEventTerminated || string(event.Payload) != string([]byte{1, 2, 3, 4}) {
|
||||
t.Fatalf("provider termination event = %#v", event)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("encrypted host termination was not forwarded")
|
||||
}
|
||||
terminateCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := session.Terminate(terminateCtx); err != nil {
|
||||
@@ -899,6 +900,55 @@ func TestPushLatestDropsExactlyOneOldPayload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeProviderVideoQueueBoundsRealFrames(t *testing.T) {
|
||||
const maximumQueuedVideoBytes = 4 << 20
|
||||
session := newNativeApolloSession("bounded-video")
|
||||
frame := bytes.Repeat([]byte{0x65}, 768<<10)
|
||||
for index := 0; index < 12; index++ {
|
||||
if !session.enqueueMedia(session.video, append([]byte(nil), frame...), time.Now()) {
|
||||
t.Fatalf("frame %d was not accepted", index)
|
||||
}
|
||||
}
|
||||
|
||||
var queuedBytes int
|
||||
for {
|
||||
select {
|
||||
case media := <-session.Video():
|
||||
queuedBytes += len(media.Payload)
|
||||
default:
|
||||
if queuedBytes > maximumQueuedVideoBytes {
|
||||
t.Fatalf("video queue retained %d bytes, limit %d", queuedBytes, maximumQueuedVideoBytes)
|
||||
}
|
||||
if drops := session.Telemetry().MediaDrops; drops != 7 {
|
||||
t.Fatalf("latest-frame replacements = %d, want 7", drops)
|
||||
}
|
||||
if maximum := session.mediaQueueMaximum.Load(); maximum > nativeApolloVideoQueuePackets {
|
||||
t.Fatalf("maximum video queue entries = %d", maximum)
|
||||
}
|
||||
if maximum := session.mediaQueueMaximumBytes.Load(); maximum > maximumQueuedVideoBytes {
|
||||
t.Fatalf("maximum video queue bytes = %d", maximum)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeProviderVideoQueueExpiresResidence(t *testing.T) {
|
||||
session := newNativeApolloSession("expiring-video")
|
||||
if !session.enqueueMedia(session.video, []byte("stale-frame"), time.Now()) {
|
||||
t.Fatal("video frame was not accepted")
|
||||
}
|
||||
time.Sleep(nativeApolloVideoQueueLatency + 25*time.Millisecond)
|
||||
select {
|
||||
case media := <-session.Video():
|
||||
t.Fatalf("expired video remained queued: %#v", media)
|
||||
default:
|
||||
}
|
||||
if drops := session.Telemetry().MediaDrops; drops != 1 {
|
||||
t.Fatalf("expired video drops = %d, want 1", drops)
|
||||
}
|
||||
}
|
||||
|
||||
func sourceShapedEncryptedVideoPacket(t *testing.T, key, encoded []byte) []byte {
|
||||
t.Helper()
|
||||
payload := make([]byte, apolloVideoShardPayloadSize)
|
||||
|
||||
@@ -11,7 +11,7 @@ var ErrNoCapabilityOverlap = errors.New("no capability overlap")
|
||||
func DefaultCapabilities() protocol.CapabilityProfile {
|
||||
return protocol.CapabilityProfile{
|
||||
Transport: "quic-tls13",
|
||||
Framing: "datagram-v1",
|
||||
Framing: "datagram-v2",
|
||||
Media: "encoded",
|
||||
Audio: "encoded",
|
||||
SourceRateControl: "server",
|
||||
|
||||
+96
-34
@@ -7,9 +7,15 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
frameHeaderSize = 21
|
||||
maxFrameSize = 1 << 16
|
||||
maxFragmentCount = 16
|
||||
frameV1HeaderSize = 21
|
||||
frameV2HeaderSize = 23
|
||||
frameV1PayloadSize = 1179
|
||||
frameV2PayloadSize = 1177
|
||||
maxV1FragmentCount = 16
|
||||
maxV2FragmentCount = 891
|
||||
maxCompleteFrameBytes = 1 << 20
|
||||
maxFrameSize = 1 << 16
|
||||
frameHeaderSize = frameV2HeaderSize
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,16 +41,25 @@ var (
|
||||
)
|
||||
|
||||
type Frame struct {
|
||||
Version byte
|
||||
Channel byte
|
||||
Flags byte
|
||||
Sequence uint32
|
||||
TimestampMS uint64
|
||||
FragmentIndex byte
|
||||
FragmentCount byte
|
||||
FragmentIndex uint16
|
||||
FragmentCount uint16
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func channelLimit(channel byte) (int, bool) {
|
||||
func channelLimit(version, channel byte) (int, bool) {
|
||||
if version == 2 {
|
||||
switch channel {
|
||||
case ChannelVideo, ChannelAudio:
|
||||
return frameV2PayloadSize, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
switch channel {
|
||||
case ChannelControl:
|
||||
return 1024, true
|
||||
@@ -53,93 +68,139 @@ func channelLimit(channel byte) (int, bool) {
|
||||
case ChannelText:
|
||||
return 65515, true
|
||||
case ChannelVideo, ChannelAudio, ChannelInput:
|
||||
return 1179, true
|
||||
return frameV1PayloadSize, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeFrame(frame Frame) ([]byte, error) {
|
||||
limit, ok := channelLimit(frame.Channel)
|
||||
version := frame.Version
|
||||
if version == 0 {
|
||||
version = 1
|
||||
}
|
||||
if version != 1 && version != 2 {
|
||||
return nil, ErrFrameVersion
|
||||
}
|
||||
limit, ok := channelLimit(version, frame.Channel)
|
||||
if !ok {
|
||||
return nil, ErrFrameChannel
|
||||
}
|
||||
if frame.Flags != 0 {
|
||||
return nil, ErrFrameFlags
|
||||
}
|
||||
if frame.FragmentCount == 0 || frame.FragmentCount > maxFragmentCount || frame.FragmentIndex >= frame.FragmentCount {
|
||||
maxFragments := uint16(maxV1FragmentCount)
|
||||
headerSize := frameV1HeaderSize
|
||||
if version == 2 {
|
||||
maxFragments = maxV2FragmentCount
|
||||
headerSize = frameV2HeaderSize
|
||||
}
|
||||
if frame.FragmentCount == 0 || frame.FragmentCount > maxFragments || frame.FragmentIndex >= frame.FragmentCount {
|
||||
return nil, ErrFrameFragment
|
||||
}
|
||||
if len(frame.Payload) > limit {
|
||||
return nil, ErrFramePayloadLimit
|
||||
}
|
||||
if len(frame.Payload) > maxFrameSize-frameHeaderSize {
|
||||
if len(frame.Payload) > 1<<16-headerSize {
|
||||
return nil, ErrFrameSize
|
||||
}
|
||||
encoded := make([]byte, frameHeaderSize+len(frame.Payload))
|
||||
encoded[0], encoded[1], encoded[2], encoded[3], encoded[4] = 'V', 'D', 1, frame.Channel, frame.Flags
|
||||
encoded := make([]byte, headerSize+len(frame.Payload))
|
||||
encoded[0], encoded[1], encoded[2], encoded[3], encoded[4] = 'V', 'D', version, frame.Channel, frame.Flags
|
||||
binary.BigEndian.PutUint32(encoded[5:9], frame.Sequence)
|
||||
binary.BigEndian.PutUint64(encoded[9:17], frame.TimestampMS)
|
||||
encoded[17], encoded[18] = frame.FragmentIndex, frame.FragmentCount
|
||||
binary.BigEndian.PutUint16(encoded[19:21], uint16(len(frame.Payload)))
|
||||
copy(encoded[frameHeaderSize:], frame.Payload)
|
||||
if version == 1 {
|
||||
encoded[17], encoded[18] = byte(frame.FragmentIndex), byte(frame.FragmentCount)
|
||||
binary.BigEndian.PutUint16(encoded[19:21], uint16(len(frame.Payload)))
|
||||
} else {
|
||||
binary.BigEndian.PutUint16(encoded[17:19], frame.FragmentIndex)
|
||||
binary.BigEndian.PutUint16(encoded[19:21], frame.FragmentCount)
|
||||
binary.BigEndian.PutUint16(encoded[21:23], uint16(len(frame.Payload)))
|
||||
}
|
||||
copy(encoded[headerSize:], frame.Payload)
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func DecodeFrame(raw []byte) (Frame, error) {
|
||||
if len(raw) < frameHeaderSize {
|
||||
if len(raw) < 3 {
|
||||
return Frame{}, ErrFrameTruncated
|
||||
}
|
||||
if len(raw) > maxFrameSize {
|
||||
return Frame{}, ErrFrameSize
|
||||
}
|
||||
if raw[0] != 'V' || raw[1] != 'D' {
|
||||
return Frame{}, ErrFrameMagic
|
||||
}
|
||||
if raw[2] != 1 {
|
||||
version := raw[2]
|
||||
if version != 1 && version != 2 {
|
||||
return Frame{}, ErrFrameVersion
|
||||
}
|
||||
limit, ok := channelLimit(raw[3])
|
||||
headerSize := frameV1HeaderSize
|
||||
maxFragments := uint16(maxV1FragmentCount)
|
||||
if version == 2 {
|
||||
headerSize = frameV2HeaderSize
|
||||
maxFragments = maxV2FragmentCount
|
||||
}
|
||||
if len(raw) < headerSize {
|
||||
return Frame{}, ErrFrameTruncated
|
||||
}
|
||||
if version == 1 && len(raw) > 1<<16 || version == 2 && len(raw) > 1200 {
|
||||
return Frame{}, ErrFrameSize
|
||||
}
|
||||
limit, ok := channelLimit(version, raw[3])
|
||||
if !ok {
|
||||
return Frame{}, ErrFrameChannel
|
||||
}
|
||||
if raw[4] != 0 {
|
||||
return Frame{}, ErrFrameFlags
|
||||
}
|
||||
if raw[18] == 0 || raw[18] > maxFragmentCount || raw[17] >= raw[18] {
|
||||
var fragmentIndex, fragmentCount uint16
|
||||
payloadOffset := 19
|
||||
if version == 1 {
|
||||
fragmentIndex, fragmentCount = uint16(raw[17]), uint16(raw[18])
|
||||
} else {
|
||||
fragmentIndex = binary.BigEndian.Uint16(raw[17:19])
|
||||
fragmentCount = binary.BigEndian.Uint16(raw[19:21])
|
||||
payloadOffset = 21
|
||||
}
|
||||
if fragmentCount == 0 || fragmentCount > maxFragments || fragmentIndex >= fragmentCount {
|
||||
return Frame{}, ErrFrameFragment
|
||||
}
|
||||
payloadLength := int(binary.BigEndian.Uint16(raw[19:21]))
|
||||
payloadLength := int(binary.BigEndian.Uint16(raw[payloadOffset : payloadOffset+2]))
|
||||
if payloadLength > limit {
|
||||
return Frame{}, ErrFramePayloadLimit
|
||||
}
|
||||
if len(raw) != frameHeaderSize+payloadLength {
|
||||
if len(raw) != headerSize+payloadLength {
|
||||
return Frame{}, ErrFrameLength
|
||||
}
|
||||
return Frame{
|
||||
Version: version,
|
||||
Channel: raw[3],
|
||||
Flags: raw[4],
|
||||
Sequence: binary.BigEndian.Uint32(raw[5:9]),
|
||||
TimestampMS: binary.BigEndian.Uint64(raw[9:17]),
|
||||
FragmentIndex: raw[17],
|
||||
FragmentCount: raw[18],
|
||||
Payload: append([]byte(nil), raw[frameHeaderSize:]...),
|
||||
FragmentIndex: fragmentIndex,
|
||||
FragmentCount: fragmentCount,
|
||||
Payload: append([]byte(nil), raw[headerSize:]...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func FragmentPayload(channel byte, sequence uint32, timestampMS uint64, payload []byte) ([]Frame, error) {
|
||||
limit, ok := channelLimit(channel)
|
||||
if !ok {
|
||||
version := byte(1)
|
||||
limit := frameV1PayloadSize
|
||||
maxFragments := maxV1FragmentCount
|
||||
if channel == ChannelVideo || channel == ChannelAudio {
|
||||
version = 2
|
||||
limit = frameV2PayloadSize
|
||||
maxFragments = maxV2FragmentCount
|
||||
}
|
||||
if _, ok := channelLimit(version, channel); !ok {
|
||||
return nil, ErrFrameChannel
|
||||
}
|
||||
if limit > 1179 {
|
||||
limit = 1179
|
||||
if len(payload) > maxCompleteFrameBytes {
|
||||
return nil, ErrFrameFragmentedLimit
|
||||
}
|
||||
count := (len(payload) + limit - 1) / limit
|
||||
if count == 0 {
|
||||
count = 1
|
||||
}
|
||||
if count > maxFragmentCount {
|
||||
if count > maxFragments {
|
||||
return nil, ErrFrameFragmentedLimit
|
||||
}
|
||||
frames := make([]Frame, 0, count)
|
||||
@@ -150,11 +211,12 @@ func FragmentPayload(channel byte, sequence uint32, timestampMS uint64, payload
|
||||
end = len(payload)
|
||||
}
|
||||
frames = append(frames, Frame{
|
||||
Version: version,
|
||||
Channel: channel,
|
||||
Sequence: sequence,
|
||||
TimestampMS: timestampMS,
|
||||
FragmentIndex: byte(index),
|
||||
FragmentCount: byte(count),
|
||||
FragmentIndex: uint16(index),
|
||||
FragmentCount: uint16(count),
|
||||
Payload: append([]byte(nil), payload[start:end]...),
|
||||
})
|
||||
}
|
||||
|
||||
+271
-9
@@ -1,6 +1,7 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
@@ -32,7 +33,7 @@ const protocolTerminalReceiptVector = "VGF1\x00\x03\x00\x00"
|
||||
|
||||
func TestFrameValidationAndFragmentation(t *testing.T) {
|
||||
frames, err := FragmentPayload(ChannelVideo, 7, 11, make([]byte, 1180))
|
||||
if err != nil || len(frames) != 2 || len(frames[0].Payload) != 1179 || len(frames[1].Payload) != 1 {
|
||||
if err != nil || len(frames) != 2 || len(frames[0].Payload) != 1177 || len(frames[1].Payload) != 3 {
|
||||
t.Fatalf("fragmentation = %#v, err = %v", frames, err)
|
||||
}
|
||||
encoded, err := EncodeFrame(frames[0])
|
||||
@@ -161,11 +162,11 @@ func TestNewServerRejectsPartiallyConfiguredCapabilities(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSyntheticImpairmentPacingAndResourceBounds(t *testing.T) {
|
||||
payload := make([]byte, 1179*16+1)
|
||||
payload := make([]byte, maxCompleteFrameBytes+1)
|
||||
if _, err := FragmentPayload(ChannelVideo, 1, 0, payload); !errors.Is(err, ErrFrameFragmentedLimit) {
|
||||
t.Fatalf("oversized media payload accepted: %v", err)
|
||||
}
|
||||
frames, err := FragmentPayload(ChannelVideo, 1, 0, bytesRepeat(0x5a, 1179*4))
|
||||
frames, err := FragmentPayload(ChannelVideo, 1, 0, bytesRepeat(0x5a, frameV2PayloadSize*4))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -186,11 +187,29 @@ func TestSyntheticImpairmentPacingAndResourceBounds(t *testing.T) {
|
||||
}
|
||||
deliveredBytes += len(decoded.Payload)
|
||||
}
|
||||
if delivered != 3 || deliveredBytes != 1179*3 {
|
||||
if delivered != 3 || deliveredBytes != frameV2PayloadSize*3 {
|
||||
t.Fatalf("synthetic impairment delivered=%d bytes=%d", delivered, deliveredBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFragmentPayloadCarriesCompleteEncodedFrame(t *testing.T) {
|
||||
payload := bytesRepeat(0x5a, 256*1024)
|
||||
frames, err := FragmentPayload(ChannelVideo, 9, 11, payload)
|
||||
if errors.Is(err, ErrFrameFragmentedLimit) {
|
||||
t.Fatalf("complete encoded frame rejected at legacy fragment ceiling: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var recovered []byte
|
||||
for _, frame := range frames {
|
||||
recovered = append(recovered, frame.Payload...)
|
||||
}
|
||||
if !bytes.Equal(recovered, payload) {
|
||||
t.Fatal("complete encoded frame payload changed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApolloFixturesAndLifecycle(t *testing.T) {
|
||||
management, err := os.ReadFile("testdata/apollo-management.xml")
|
||||
if err != nil {
|
||||
@@ -218,10 +237,10 @@ func TestApolloFixturesAndLifecycle(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := <-session.Video(); string(got.Payload) != string(video) {
|
||||
t.Fatalf("video changed: %x", got)
|
||||
t.Fatalf("video changed: %x", got.Payload)
|
||||
}
|
||||
if got := <-session.Audio(); string(got.Payload) != string(audio) {
|
||||
t.Fatalf("audio changed: %x", got)
|
||||
t.Fatalf("audio changed: %x", got.Payload)
|
||||
}
|
||||
if err := session.Input(context.Background(), InputEvent{Sequence: 1, Device: "keyboard", Code: 7, Pressed: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -401,7 +420,7 @@ func TestGatewayTelemetrySeparatesQueueProcessingAndPacing(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
receiveCtx, receiveCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
for index := byte(0); index < 2; index++ {
|
||||
for index := uint16(0); index < 2; index++ {
|
||||
frame, err := client.ReceiveFrame(receiveCtx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -838,6 +857,7 @@ func newNativeGatewayLifecycleHarness(t *testing.T, sessionID string) nativeGate
|
||||
type independentGatewayClient struct {
|
||||
connection *quic.Conn
|
||||
control *quic.Stream
|
||||
media independentMediaReassembler
|
||||
}
|
||||
|
||||
func dialIndependentGateway(ctx context.Context, address string, tlsConfig *tls.Config, request protocol.TunnelAdmissionRequest) (*independentGatewayClient, error) {
|
||||
@@ -866,7 +886,11 @@ func dialIndependentGateway(ctx context.Context, address string, tlsConfig *tls.
|
||||
_ = connection.CloseWithError(applicationError, "independent client admission failed")
|
||||
return nil, err
|
||||
}
|
||||
return &independentGatewayClient{connection: connection, control: stream}, nil
|
||||
return &independentGatewayClient{
|
||||
connection: connection,
|
||||
control: stream,
|
||||
media: independentMediaReassembler{incomplete: make(map[independentMediaKey]*independentMediaUnit)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *independentGatewayClient) ReceiveProviderEvent(ctx context.Context) (ProviderEvent, error) {
|
||||
@@ -912,7 +936,245 @@ func (c *independentGatewayClient) ReceiveFrame(ctx context.Context) (Frame, err
|
||||
if err != nil {
|
||||
return Frame{}, err
|
||||
}
|
||||
return DecodeFrame(data)
|
||||
return independentDecodeFrame(data)
|
||||
}
|
||||
|
||||
func (c *independentGatewayClient) ReceiveMedia(ctx context.Context) ([]byte, error) {
|
||||
for {
|
||||
data, err := c.connection.ReceiveDatagram(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, complete, err := c.media.Add(data, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if complete {
|
||||
return payload, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type independentMediaKey struct {
|
||||
channel byte
|
||||
sequence uint32
|
||||
}
|
||||
|
||||
type independentMediaUnit struct {
|
||||
started time.Time
|
||||
timestamp uint64
|
||||
fragments [][]byte
|
||||
received []bool
|
||||
bytes int
|
||||
}
|
||||
|
||||
type independentMediaReassembler struct {
|
||||
incomplete map[independentMediaKey]*independentMediaUnit
|
||||
}
|
||||
|
||||
func independentDecodeFrame(data []byte) (Frame, error) {
|
||||
const (
|
||||
v1Header, v2Header = 21, 23
|
||||
v1Payload, v2Payload = 1179, 1177
|
||||
)
|
||||
if len(data) < 3 {
|
||||
return Frame{}, ErrFrameTruncated
|
||||
}
|
||||
if data[0] != 'V' || data[1] != 'D' {
|
||||
return Frame{}, ErrFrameMagic
|
||||
}
|
||||
version := data[2]
|
||||
headerSize, payloadLimit := v1Header, v1Payload
|
||||
if version == 2 {
|
||||
headerSize, payloadLimit = v2Header, v2Payload
|
||||
} else if version != 1 {
|
||||
return Frame{}, ErrFrameVersion
|
||||
}
|
||||
if len(data) < headerSize || version == 2 && len(data) > 1200 {
|
||||
return Frame{}, ErrFrameSize
|
||||
}
|
||||
frame := Frame{
|
||||
Version: version,
|
||||
Channel: data[3],
|
||||
Flags: data[4],
|
||||
Sequence: binary.BigEndian.Uint32(data[5:9]),
|
||||
TimestampMS: binary.BigEndian.Uint64(data[9:17]),
|
||||
}
|
||||
payloadLengthOffset := 19
|
||||
if version == 1 {
|
||||
frame.FragmentIndex = uint16(data[17])
|
||||
frame.FragmentCount = uint16(data[18])
|
||||
} else {
|
||||
frame.FragmentIndex = binary.BigEndian.Uint16(data[17:19])
|
||||
frame.FragmentCount = binary.BigEndian.Uint16(data[19:21])
|
||||
payloadLengthOffset = 21
|
||||
}
|
||||
if (frame.Channel != ChannelVideo && frame.Channel != ChannelAudio) || frame.Flags != 0 ||
|
||||
frame.FragmentCount == 0 || version == 1 && frame.FragmentCount > 16 ||
|
||||
version == 2 && frame.FragmentCount > 891 || frame.FragmentIndex >= frame.FragmentCount {
|
||||
return Frame{}, ErrFrameFragment
|
||||
}
|
||||
payloadLength := int(binary.BigEndian.Uint16(data[payloadLengthOffset : payloadLengthOffset+2]))
|
||||
if payloadLength > payloadLimit || len(data) != headerSize+payloadLength {
|
||||
return Frame{}, ErrFrameLength
|
||||
}
|
||||
frame.Payload = append([]byte(nil), data[headerSize:]...)
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
func (r *independentMediaReassembler) Add(data []byte, now time.Time) ([]byte, bool, error) {
|
||||
frame, err := independentDecodeFrame(data)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
for key, unit := range r.incomplete {
|
||||
if now.Sub(unit.started) > 250*time.Millisecond {
|
||||
delete(r.incomplete, key)
|
||||
}
|
||||
}
|
||||
key := independentMediaKey{channel: frame.Channel, sequence: frame.Sequence}
|
||||
unit := r.incomplete[key]
|
||||
if unit == nil {
|
||||
if len(r.incomplete) == 4 {
|
||||
var oldestKey independentMediaKey
|
||||
var oldest time.Time
|
||||
for candidate, current := range r.incomplete {
|
||||
if oldest.IsZero() || current.started.Before(oldest) {
|
||||
oldestKey, oldest = candidate, current.started
|
||||
}
|
||||
}
|
||||
delete(r.incomplete, oldestKey)
|
||||
}
|
||||
unit = &independentMediaUnit{
|
||||
started: now, timestamp: frame.TimestampMS,
|
||||
fragments: make([][]byte, frame.FragmentCount), received: make([]bool, frame.FragmentCount),
|
||||
}
|
||||
r.incomplete[key] = unit
|
||||
}
|
||||
if len(unit.fragments) != int(frame.FragmentCount) || unit.timestamp != frame.TimestampMS {
|
||||
return nil, false, ErrFrameFragment
|
||||
}
|
||||
index := int(frame.FragmentIndex)
|
||||
if unit.received[index] {
|
||||
if !bytes.Equal(unit.fragments[index], frame.Payload) {
|
||||
return nil, false, ErrFrameFragment
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
if unit.bytes+len(frame.Payload) > 1<<20 {
|
||||
delete(r.incomplete, key)
|
||||
return nil, false, ErrFrameSize
|
||||
}
|
||||
unit.fragments[index] = frame.Payload
|
||||
unit.received[index] = true
|
||||
unit.bytes += len(frame.Payload)
|
||||
for _, received := range unit.received {
|
||||
if !received {
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
payload := make([]byte, 0, unit.bytes)
|
||||
for _, fragment := range unit.fragments {
|
||||
payload = append(payload, fragment...)
|
||||
}
|
||||
delete(r.incomplete, key)
|
||||
return payload, true, nil
|
||||
}
|
||||
|
||||
func TestIndependentClientReassemblesProtocolDatagramV2(t *testing.T) {
|
||||
fixed, err := hex.DecodeString("5644020a00000000010000000000000002000000010003010203")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
frame, err := independentDecodeFrame(fixed)
|
||||
if err != nil || frame.Version != 2 || frame.Channel != ChannelVideo ||
|
||||
frame.Sequence != 1 || frame.TimestampMS != 2 || frame.FragmentCount != 1 ||
|
||||
!bytes.Equal(frame.Payload, []byte{1, 2, 3}) {
|
||||
t.Fatalf("fixed Protocol v2 frame = %#v, %v", frame, err)
|
||||
}
|
||||
|
||||
payload := bytes.Repeat([]byte("frame-boundary-"), 300)
|
||||
fragments, err := FragmentPayload(ChannelVideo, 7, 11, payload)
|
||||
if err != nil || len(fragments) < 3 {
|
||||
t.Fatalf("fragments = %d, %v", len(fragments), err)
|
||||
}
|
||||
encoded := make([][]byte, len(fragments))
|
||||
for index, fragment := range fragments {
|
||||
encoded[index], err = EncodeFrame(fragment)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
reassembler := independentMediaReassembler{incomplete: make(map[independentMediaKey]*independentMediaUnit)}
|
||||
now := time.Unix(0, 0)
|
||||
order := []int{2, 0, 0, 1, 3}
|
||||
var recovered []byte
|
||||
for _, index := range order {
|
||||
var complete bool
|
||||
recovered, complete, err = reassembler.Add(encoded[index], now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if index != 3 && complete {
|
||||
t.Fatalf("unit completed at fragment %d", index)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(recovered, payload) {
|
||||
t.Fatal("independent client changed the complete encoded frame")
|
||||
}
|
||||
|
||||
if _, _, err := reassembler.Add(encoded[0], now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conflict := append([]byte(nil), encoded[0]...)
|
||||
conflict[len(conflict)-1] ^= 0xff
|
||||
if _, _, err := reassembler.Add(conflict, now); !errors.Is(err, ErrFrameFragment) {
|
||||
t.Fatalf("conflicting duplicate = %v", err)
|
||||
}
|
||||
reassembler = independentMediaReassembler{incomplete: make(map[independentMediaKey]*independentMediaUnit)}
|
||||
if _, _, err := reassembler.Add(encoded[0], now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expired := append([]byte(nil), encoded[0]...)
|
||||
binary.BigEndian.PutUint32(expired[5:9], 8)
|
||||
if _, _, err := reassembler.Add(expired, now.Add(251*time.Millisecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(reassembler.incomplete) != 1 {
|
||||
t.Fatalf("expired incomplete units = %d, want 1", len(reassembler.incomplete))
|
||||
}
|
||||
|
||||
reassembler = independentMediaReassembler{incomplete: make(map[independentMediaKey]*independentMediaUnit)}
|
||||
for sequence := uint32(1); sequence <= 5; sequence++ {
|
||||
partial, encodeErr := EncodeFrame(Frame{
|
||||
Version: 2, Channel: ChannelVideo, Sequence: sequence, TimestampMS: 1,
|
||||
FragmentIndex: 0, FragmentCount: 2, Payload: []byte{byte(sequence)},
|
||||
})
|
||||
if encodeErr != nil {
|
||||
t.Fatal(encodeErr)
|
||||
}
|
||||
if _, _, err := reassembler.Add(partial, now.Add(time.Duration(sequence)*time.Nanosecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(reassembler.incomplete) != 4 || reassembler.incomplete[independentMediaKey{channel: ChannelVideo, sequence: 1}] != nil {
|
||||
t.Fatalf("fifth unit did not evict the oldest: %#v", reassembler.incomplete)
|
||||
}
|
||||
|
||||
reassembler = independentMediaReassembler{incomplete: make(map[independentMediaKey]*independentMediaUnit)}
|
||||
for index := uint16(0); index < 891; index++ {
|
||||
fragment, encodeErr := EncodeFrame(Frame{
|
||||
Version: 2, Channel: ChannelVideo, Sequence: 99, TimestampMS: 1,
|
||||
FragmentIndex: index, FragmentCount: 891, Payload: make([]byte, 1177),
|
||||
})
|
||||
if encodeErr != nil {
|
||||
t.Fatal(encodeErr)
|
||||
}
|
||||
_, _, err = reassembler.Add(fragment, now)
|
||||
}
|
||||
if !errors.Is(err, ErrFrameSize) || len(reassembler.incomplete) != 0 {
|
||||
t.Fatalf("oversized reassembly = %v, incomplete=%d", err, len(reassembler.incomplete))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *independentGatewayClient) waitClosed(t *testing.T) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||
@@ -234,6 +235,21 @@ type ProviderMedia struct {
|
||||
Payload []byte
|
||||
ReceivedAt time.Time
|
||||
EnqueuedAt time.Time
|
||||
queueID uint64
|
||||
expiry *time.Timer
|
||||
accounting *providerMediaQueueAccounting
|
||||
}
|
||||
|
||||
type providerMediaQueueAccounting struct {
|
||||
released atomic.Bool
|
||||
bytes int64
|
||||
total *atomic.Int64
|
||||
}
|
||||
|
||||
func (media ProviderMedia) releaseQueue() {
|
||||
if media.accounting != nil && media.accounting.released.CompareAndSwap(false, true) {
|
||||
media.accounting.total.Add(-media.accounting.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
|
||||
@@ -22,9 +22,9 @@ func TestQualificationCatalogMatchesSection7(t *testing.T) {
|
||||
t.Fatalf("media profile count = %d, want 3", len(media))
|
||||
}
|
||||
wantMedia := []qualificationMediaProfile{
|
||||
{Name: "1080p60-h264", Codec: "h264", BitrateKbps: 20000, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
{Name: "1440p120-hevc", Codec: "hevc", BitrateKbps: 50000, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
{Name: "4k60-hevc", Codec: "hevc", BitrateKbps: 80000, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
{Name: "1080p60-h264", Codec: "h264", BitrateKbps: 20000, FPS: 60, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
{Name: "1440p120-hevc", Codec: "hevc", BitrateKbps: 50000, FPS: 120, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
{Name: "4k60-hevc", Codec: "hevc", BitrateKbps: 80000, FPS: 60, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
}
|
||||
if !reflect.DeepEqual(media, wantMedia) {
|
||||
t.Fatalf("media profiles = %#v, want %#v", media, wantMedia)
|
||||
@@ -145,6 +145,14 @@ func TestQualificationShortProcessingSubprocessCoversFixedProfiles(t *testing.T)
|
||||
}
|
||||
if summary.Count < 1 || summary.CPUScope != qualificationGatewayCPUScope ||
|
||||
summary.ClockOverhead <= 0 || summary.ClockMethod != qualificationClockOverheadMethod ||
|
||||
summary.Count != int64(profile.FPS) ||
|
||||
summary.ConfiguredFPS != profile.FPS ||
|
||||
summary.ObservedFPS < float64(profile.FPS)*0.95 ||
|
||||
summary.ObservedFPS > float64(profile.FPS)*1.05 ||
|
||||
summary.PayloadBytes != profile.BitrateKbps*1000/8 ||
|
||||
summary.MaximumFrameBytes <= summary.MinimumFrameBytes ||
|
||||
summary.MaximumFrameBytes <= 18_864 ||
|
||||
summary.PayloadSHA256 == "" ||
|
||||
summary.ObservedBitrateKbps < float64(profile.BitrateKbps)*0.95 ||
|
||||
summary.ObservedBitrateKbps > float64(profile.BitrateKbps)*1.05 {
|
||||
t.Fatalf("%s subprocess summary = %#v", profile.Name, summary)
|
||||
@@ -152,6 +160,19 @@ func TestQualificationShortProcessingSubprocessCoversFixedProfiles(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualificationShortProcessingUsesCompleteFrameCadence(t *testing.T) {
|
||||
profile := qualificationMediaProfiles()[0]
|
||||
profile.Duration = 100 * time.Millisecond
|
||||
profile.Warmup = time.Millisecond
|
||||
summary, err := runQualificationProcessing(t, profile, filepath.Join(t.TempDir(), "frames.csv.gz"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if summary.Count != 6 {
|
||||
t.Fatalf("100 ms of 1080p60 processed %d units, want 6 complete encoded frames", summary.Count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualificationSustainedProcessingKeepsCleanPathBounded(t *testing.T) {
|
||||
profile := qualificationMediaProfiles()[2]
|
||||
profile.Duration = 2 * time.Minute
|
||||
@@ -400,7 +421,7 @@ func TestQualificationGatewaySubprocessResourcesResetAndTrackWork(t *testing.T)
|
||||
Name: "resource-process", Codec: "h264", BitrateKbps: 100000,
|
||||
Duration: time.Second, Warmup: time.Millisecond, PacketBytes: 1000,
|
||||
}
|
||||
path := newQualificationProcessingPath(t, profile, qualificationMediaPacerKbps(profile, 0))
|
||||
path := newQualificationProcessingPath(t, profile, qualificationFramePacerKbps(profile))
|
||||
defer path.Close()
|
||||
output := t.TempDir()
|
||||
record := func(name string, work func() error) qualificationProcessRecordResult {
|
||||
@@ -552,3 +573,25 @@ func TestQualificationSmokeTraversesNativeApolloRecoveryQueuePacerAndQUIC(t *tes
|
||||
t.Fatalf("qualification production-path trace = %#v", trace)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualificationCarriesCompleteLargeFramesThroughPublicPath(t *testing.T) {
|
||||
profile := qualificationMediaProfiles()[2]
|
||||
path := newQualificationPath(t, profile, 200000)
|
||||
defer path.Close()
|
||||
|
||||
for _, size := range []int{24 * 1024, 96 * 1024, 384 * 1024} {
|
||||
payload := make([]byte, size)
|
||||
for index := range payload {
|
||||
payload[index] = byte(index*31 + size)
|
||||
}
|
||||
trace, _, err := path.traverse(t, payload)
|
||||
if err != nil {
|
||||
t.Fatalf("frame bytes=%d: %v", size, err)
|
||||
}
|
||||
if !trace.NativeUDPIngress || !trace.ApolloRecovered || !trace.ProductionQueue ||
|
||||
!trace.ProductionMediaLoop || !trace.ProductionPacer || !trace.VerseQUIC ||
|
||||
!trace.PublicClientDecode || !trace.PayloadPreserved {
|
||||
t.Fatalf("frame bytes=%d skipped path: %#v", size, trace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ import (
|
||||
|
||||
const (
|
||||
qualificationToolVersion = "versevdi-gateway-qualification/v6"
|
||||
qualificationImpairmentQueuePackets = 256
|
||||
qualificationImpairmentQueuePackets = nativeApolloVideoQueuePackets
|
||||
qualificationImpairmentMaxPackets = 100_000
|
||||
qualificationImpairmentPacketCount = 10_000
|
||||
qualificationProcessingLimit = 5 * time.Millisecond
|
||||
@@ -54,6 +54,7 @@ type qualificationMediaProfile struct {
|
||||
Name string
|
||||
Codec string
|
||||
BitrateKbps int64
|
||||
FPS int
|
||||
Duration time.Duration
|
||||
Warmup time.Duration
|
||||
PacketBytes int
|
||||
@@ -73,7 +74,7 @@ type qualificationPathTrace struct {
|
||||
}
|
||||
|
||||
type qualificationPath struct {
|
||||
client *Client
|
||||
client *independentGatewayClient
|
||||
server *Server
|
||||
session *nativeApolloSession
|
||||
fixture *qualificationApolloFixture
|
||||
@@ -102,6 +103,11 @@ type qualificationProcessingSummary struct {
|
||||
Codec string `json:"codec"`
|
||||
ConfiguredBitrateKbps int64 `json:"configured_bitrate_kbps"`
|
||||
ObservedBitrateKbps float64 `json:"observed_bitrate_kbps"`
|
||||
ConfiguredFPS int `json:"configured_fps"`
|
||||
ObservedFPS float64 `json:"observed_fps"`
|
||||
PayloadBytes int64 `json:"payload_bytes"`
|
||||
MinimumFrameBytes int `json:"minimum_frame_bytes"`
|
||||
MaximumFrameBytes int `json:"maximum_frame_bytes"`
|
||||
Warmup time.Duration `json:"warmup_ns"`
|
||||
ConfiguredDuration time.Duration `json:"configured_duration_ns"`
|
||||
ActualDuration time.Duration `json:"actual_duration_ns"`
|
||||
@@ -154,6 +160,7 @@ type qualificationImpairmentObservation struct {
|
||||
GatewayForwarded int `json:"gateway_forwarded"`
|
||||
GatewayDropped int `json:"gateway_dropped"`
|
||||
QUICSent int `json:"quic_sent"`
|
||||
QUICDatagramsSent int `json:"quic_datagrams_sent"`
|
||||
QUICSendDropped int `json:"quic_send_dropped"`
|
||||
ClientDeliveryDropped int `json:"client_delivery_dropped"`
|
||||
UnexplainedDropped int `json:"unexplained_dropped"`
|
||||
@@ -241,9 +248,9 @@ type qualificationManifest struct {
|
||||
|
||||
func qualificationMediaProfiles() []qualificationMediaProfile {
|
||||
return []qualificationMediaProfile{
|
||||
{Name: "1080p60-h264", Codec: "h264", BitrateKbps: 20000, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
{Name: "1440p120-hevc", Codec: "hevc", BitrateKbps: 50000, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
{Name: "4k60-hevc", Codec: "hevc", BitrateKbps: 80000, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
{Name: "1080p60-h264", Codec: "h264", BitrateKbps: 20000, FPS: 60, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
{Name: "1440p120-hevc", Codec: "hevc", BitrateKbps: 50000, FPS: 120, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
{Name: "4k60-hevc", Codec: "hevc", BitrateKbps: 80000, FPS: 60, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,9 +285,65 @@ func qualificationPayload(profile qualificationMediaProfile) []byte {
|
||||
return payload
|
||||
}
|
||||
|
||||
func qualificationFrameRate(profile qualificationMediaProfile) int {
|
||||
if profile.FPS > 0 {
|
||||
return profile.FPS
|
||||
}
|
||||
return 60
|
||||
}
|
||||
|
||||
func qualificationFrameSize(profile qualificationMediaProfile, index int64) int {
|
||||
fps := qualificationFrameRate(profile)
|
||||
bytesPerSecond := int(profile.BitrateKbps * 1000 / 8)
|
||||
position := int(index % int64(fps))
|
||||
if fps == 1 {
|
||||
return min(bytesPerSecond, maxCompleteFrameBytes)
|
||||
}
|
||||
keyframeBytes := min(bytesPerSecond/fps*4, maxCompleteFrameBytes)
|
||||
if position == 0 {
|
||||
return keyframeBytes
|
||||
}
|
||||
remaining := bytesPerSecond - keyframeBytes
|
||||
size := remaining / (fps - 1)
|
||||
if position <= remaining%(fps-1) {
|
||||
size++
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
func qualificationFramePayload(profile qualificationMediaProfile, index int64) []byte {
|
||||
payload := make([]byte, qualificationFrameSize(profile, index))
|
||||
if profile.Codec == "h264" {
|
||||
copy(payload, []byte{0, 0, 1, 0x65})
|
||||
} else {
|
||||
copy(payload, []byte{0, 0, 1, 0x26})
|
||||
}
|
||||
for offset := 4; offset < len(payload); offset++ {
|
||||
payload[offset] = byte(int64(offset)*31 + index*17 + int64(len(profile.Name)))
|
||||
}
|
||||
if len(payload) >= 8 {
|
||||
binary.BigEndian.PutUint64(payload[len(payload)-8:], uint64(index))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func qualificationMediaPacerKbps(profile qualificationMediaProfile, reduction int) int64 {
|
||||
payloadKbps := profile.BitrateKbps * int64(100-reduction) / 100
|
||||
return (payloadKbps*int64(profile.PacketBytes+frameHeaderSize) + int64(profile.PacketBytes) - 1) / int64(profile.PacketBytes)
|
||||
datagrams := (profile.PacketBytes + frameV2PayloadSize - 1) / frameV2PayloadSize
|
||||
wireBytes := profile.PacketBytes + datagrams*frameV2HeaderSize
|
||||
return (payloadKbps*int64(wireBytes) + int64(profile.PacketBytes) - 1) / int64(profile.PacketBytes)
|
||||
}
|
||||
|
||||
func qualificationFramePacerKbps(profile qualificationMediaProfile) int64 {
|
||||
fps := qualificationFrameRate(profile)
|
||||
var payloadBytes, wireBytes int64
|
||||
for index := range fps {
|
||||
size := qualificationFrameSize(profile, int64(index))
|
||||
datagrams := (size + frameV2PayloadSize - 1) / frameV2PayloadSize
|
||||
payloadBytes += int64(size)
|
||||
wireBytes += int64(size + datagrams*frameV2HeaderSize)
|
||||
}
|
||||
return (profile.BitrateKbps*wireBytes + payloadBytes - 1) / payloadBytes
|
||||
}
|
||||
|
||||
func qualificationBoundedRelease(target, next, now time.Time, spacing time.Duration) time.Time {
|
||||
@@ -767,7 +830,7 @@ type qualificationFleet struct {
|
||||
t *testing.T
|
||||
server *Server
|
||||
paths []*qualificationPath
|
||||
clients []*Client
|
||||
clients []*independentGatewayClient
|
||||
cancel context.CancelFunc
|
||||
serveDone chan error
|
||||
closeOnce sync.Once
|
||||
@@ -812,7 +875,7 @@ func newQualificationFleet(t *testing.T, count int, profile qualificationMediaPr
|
||||
Grant: strings.Repeat(string(rune('a'+index)), 64), ClientNonce: fmt.Sprintf("nonce-fleet-%06d", index),
|
||||
DeviceSignature: strings.Repeat("s", 86), Capabilities: DefaultCapabilities(),
|
||||
}
|
||||
client, err := Dial(context.Background(), server.Addr().String(), clientTLS, request)
|
||||
client, err := dialIndependentGateway(context.Background(), server.Addr().String(), clientTLS, request)
|
||||
if err != nil {
|
||||
fleet.Close()
|
||||
t.Fatal(err)
|
||||
@@ -894,7 +957,7 @@ func newQualificationPathWithImpairment(t *testing.T, profile qualificationMedia
|
||||
ClientNonce: "nonce-qualification", DeviceSignature: strings.Repeat("s", 86),
|
||||
Capabilities: DefaultCapabilities(),
|
||||
}
|
||||
client, err := Dial(context.Background(), server.Addr().String(), clientTLS, request)
|
||||
client, err := dialIndependentGateway(context.Background(), server.Addr().String(), clientTLS, request)
|
||||
if err != nil {
|
||||
cancel()
|
||||
_ = server.Close()
|
||||
@@ -941,7 +1004,7 @@ func newQualificationProcessingPath(t *testing.T, profile qualificationMediaProf
|
||||
ClientNonce: "nonce-qualification", DeviceSignature: strings.Repeat("s", 86),
|
||||
Capabilities: DefaultCapabilities(),
|
||||
}
|
||||
client, err := Dial(context.Background(), process.ready.GatewayAddress, clientTLS, request)
|
||||
client, err := dialIndependentGateway(context.Background(), process.ready.GatewayAddress, clientTLS, request)
|
||||
if err != nil {
|
||||
process.Close()
|
||||
t.Fatal(err)
|
||||
@@ -1007,7 +1070,7 @@ func (p *qualificationPath) traverse(t *testing.T, payload []byte) (qualificatio
|
||||
|
||||
func (p *qualificationPath) emit(t *testing.T, payload []byte) (qualificationPathTrace, error) {
|
||||
t.Helper()
|
||||
if p == nil || p.fixture == nil || len(payload) == 0 || len(payload) > 2*apolloVideoShardPayloadSize-8 {
|
||||
if p == nil || p.fixture == nil || len(payload) == 0 || len(payload) > apolloVideoMaximumBlocks*apolloVideoMaximumDataShards*apolloVideoShardPayloadSize-8 {
|
||||
return qualificationPathTrace{}, ErrProviderMalformed
|
||||
}
|
||||
p.frame++
|
||||
@@ -1024,60 +1087,50 @@ func (p *qualificationPath) emit(t *testing.T, payload []byte) (qualificationPat
|
||||
func (p *qualificationPath) receivePayload(parent context.Context) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
|
||||
defer cancel()
|
||||
var recovered []byte
|
||||
var sequence uint32
|
||||
var fragmentCount byte
|
||||
for {
|
||||
frame, err := p.client.ReceiveFrame(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if frame.Channel != ChannelVideo {
|
||||
continue
|
||||
}
|
||||
if fragmentCount == 0 {
|
||||
sequence, fragmentCount = frame.Sequence, frame.FragmentCount
|
||||
}
|
||||
if frame.Sequence != sequence || frame.FragmentIndex != byte(len(recovered)/1179) {
|
||||
return nil, errors.New("qualification QUIC fragments reordered")
|
||||
}
|
||||
recovered = append(recovered, frame.Payload...)
|
||||
if frame.FragmentIndex+1 == fragmentCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
return recovered, nil
|
||||
return p.client.ReceiveMedia(ctx)
|
||||
}
|
||||
|
||||
func qualificationSourceVideoPackets(t *testing.T, key []byte, frame uint32, encoded []byte) [][]byte {
|
||||
t.Helper()
|
||||
if len(encoded) <= apolloVideoShardPayloadSize-8 {
|
||||
payload := make([]byte, apolloVideoShardPayloadSize)
|
||||
payload[0], payload[3] = 0x01, 0x01
|
||||
binary.LittleEndian.PutUint16(payload[4:6], uint16(8+len(encoded)))
|
||||
copy(payload[8:], encoded)
|
||||
raw := sourceShapedVideoRaw(frame, uint16(frame), frame, 0x07, 1, 0, 0, payload)
|
||||
return [][]byte{sourceEncryptVideoRaw(t, key, raw, qualificationVideoIV(frame, 0))}
|
||||
total := 8 + len(encoded)
|
||||
shardCount := (total + apolloVideoShardPayloadSize - 1) / apolloVideoShardPayloadSize
|
||||
if shardCount > apolloVideoMaximumBlocks*apolloVideoMaximumDataShards {
|
||||
t.Fatal("qualification encoded frame exceeds source-shaped Apollo bound")
|
||||
}
|
||||
combined := make([]byte, 2*apolloVideoShardPayloadSize)
|
||||
combined := make([]byte, shardCount*apolloVideoShardPayloadSize)
|
||||
combined[0], combined[3] = 0x01, 0x01
|
||||
binary.LittleEndian.PutUint16(combined[4:6], uint16(8+len(encoded)-apolloVideoShardPayloadSize))
|
||||
lastPayloadLength := total - (shardCount-1)*apolloVideoShardPayloadSize
|
||||
binary.LittleEndian.PutUint16(combined[4:6], uint16(lastPayloadLength))
|
||||
copy(combined[8:], encoded)
|
||||
first := sourceShapedVideoRaw(frame, uint16(frame*3), frame*3, 0x05, 2, 50, 0, combined[:apolloVideoShardPayloadSize])
|
||||
second := sourceShapedVideoRaw(frame, uint16(frame*3+1), frame*3+1, 0x03, 2, 50, 1, combined[apolloVideoShardPayloadSize:])
|
||||
parity := make([]byte, len(first))
|
||||
for index := range parity {
|
||||
parity[index] = first[index] ^ sourceGFMultiply(second[index], 142)
|
||||
}
|
||||
sourceConfigureVideoShard(parity, frame, uint16(frame*3+2), frame*3+2, 2, 50, 2)
|
||||
return [][]byte{
|
||||
sourceEncryptVideoRaw(t, key, second, qualificationVideoIV(frame, 1)),
|
||||
sourceEncryptVideoRaw(t, key, parity, qualificationVideoIV(frame, 2)),
|
||||
|
||||
lastBlock := (shardCount - 1) / apolloVideoMaximumDataShards
|
||||
packets := make([][]byte, 0, shardCount)
|
||||
for globalIndex := 0; globalIndex < shardCount; {
|
||||
block := globalIndex / apolloVideoMaximumDataShards
|
||||
dataShards := min(apolloVideoMaximumDataShards, shardCount-globalIndex)
|
||||
for shardIndex := 0; shardIndex < dataShards; shardIndex++ {
|
||||
flags := byte(0x01)
|
||||
if shardIndex == 0 {
|
||||
flags |= 0x04
|
||||
}
|
||||
if shardIndex == dataShards-1 {
|
||||
flags |= 0x02
|
||||
}
|
||||
offset := globalIndex * apolloVideoShardPayloadSize
|
||||
raw := sourceShapedVideoRaw(
|
||||
frame, uint16(frame*1024+uint32(globalIndex)), frame*1024+uint32(globalIndex),
|
||||
flags, dataShards, 0, shardIndex, combined[offset:offset+apolloVideoShardPayloadSize],
|
||||
)
|
||||
raw[27] = byte(block<<4 | lastBlock<<6)
|
||||
packets = append(packets, sourceEncryptVideoRaw(t, key, raw, qualificationVideoIV(frame, globalIndex)))
|
||||
globalIndex++
|
||||
}
|
||||
}
|
||||
return packets
|
||||
}
|
||||
|
||||
func qualificationVideoIV(frame uint32, shard byte) string {
|
||||
return fmt.Sprintf("%09x%01xQV", frame, shard)
|
||||
func qualificationVideoIV(frame uint32, shard int) string {
|
||||
return fmt.Sprintf("%06x%04xQV", frame&0xffffff, shard&0xffff)
|
||||
}
|
||||
|
||||
func qualificationProductionPathSmoke(t *testing.T, profile qualificationMediaProfile) qualificationPathTrace {
|
||||
@@ -1409,7 +1462,10 @@ func runQualificationImpairment(t *testing.T, profile qualificationImpairmentPro
|
||||
observation.ObservedOutOfOrder++
|
||||
}
|
||||
previousDelivered = packet.index
|
||||
deliveries = append(deliveries, qualificationDeliverySample{At: packet.deliveredAt, Bytes: int64(media.PacketBytes + frameHeaderSize)})
|
||||
datagrams := (media.PacketBytes + frameV2PayloadSize - 1) / frameV2PayloadSize
|
||||
deliveries = append(deliveries, qualificationDeliverySample{
|
||||
At: packet.deliveredAt, Bytes: int64(media.PacketBytes + datagrams*frameV2HeaderSize),
|
||||
})
|
||||
observation.MaxQueuePackets = max(observation.MaxQueuePackets, packet.queuePackets)
|
||||
}
|
||||
observation.Delivered = len(received.packets)
|
||||
@@ -1426,7 +1482,8 @@ func runQualificationImpairment(t *testing.T, profile qualificationImpairmentPro
|
||||
observation.ProviderEnqueued = int(path.session.mediaEnqueued.Load() - beforeEnqueued)
|
||||
observation.ProviderQueueReplaced = int(path.session.mediaDrops.Load() - beforeProviderDrops)
|
||||
observation.GatewayForwarded = int(afterMetrics.ProcessingSamples - beforeMetrics.ProcessingSamples)
|
||||
observation.QUICSent = int(afterMetrics.MediaPackets - beforeMetrics.MediaPackets)
|
||||
observation.QUICSent = observation.GatewayForwarded
|
||||
observation.QUICDatagramsSent = int(afterMetrics.MediaPackets - beforeMetrics.MediaPackets)
|
||||
observation.ProviderFECDropped = max(observation.SourceEmitted-observation.ProviderRecovered, 0)
|
||||
observation.ProviderEnqueueDropped = max(observation.ProviderRecovered-observation.ProviderEnqueued, 0)
|
||||
observation.GatewayDropped = max(observation.ProviderEnqueued-observation.ProviderQueueReplaced-observation.GatewayForwarded, 0)
|
||||
@@ -1586,11 +1643,11 @@ func qualificationMaximumDeliveryBytes(deliveries []qualificationDeliverySample,
|
||||
|
||||
func runQualificationProcessing(t *testing.T, profile qualificationMediaProfile, rawPath string) (qualificationProcessingSummary, error) {
|
||||
t.Helper()
|
||||
payload := qualificationPayload(profile)
|
||||
payload := qualificationFramePayload(profile, 0)
|
||||
if len(payload) < 4 {
|
||||
return qualificationProcessingSummary{}, errors.New("qualification payload too small")
|
||||
}
|
||||
path := newQualificationProcessingPath(t, profile, qualificationMediaPacerKbps(profile, 0))
|
||||
path := newQualificationProcessingPath(t, profile, qualificationFramePacerKbps(profile))
|
||||
defer path.Close()
|
||||
if err := runQualificationProcessWarmup(t, path, profile, payload); err != nil {
|
||||
return qualificationProcessingSummary{}, err
|
||||
@@ -1603,38 +1660,42 @@ func runQualificationProcessing(t *testing.T, profile qualificationMediaProfile,
|
||||
if err != nil {
|
||||
return qualificationProcessingSummary{}, err
|
||||
}
|
||||
bytesPerSecond := profile.BitrateKbps * 1000 / 8
|
||||
targetBytes := bytesPerSecond * profile.Duration.Nanoseconds() / int64(time.Second)
|
||||
targetPackets := (targetBytes + int64(profile.PacketBytes) - 1) / int64(profile.PacketBytes)
|
||||
spacing := time.Duration(int64(time.Second) * int64(profile.PacketBytes) * 8 / (profile.BitrateKbps * 1000))
|
||||
fps := qualificationFrameRate(profile)
|
||||
targetFrames := profile.Duration.Nanoseconds() * int64(fps) / int64(time.Second)
|
||||
if targetFrames < 1 {
|
||||
return qualificationProcessingSummary{}, errors.New("qualification duration produces no complete frames")
|
||||
}
|
||||
spacing := time.Second / time.Duration(fps)
|
||||
var targetBytes int64
|
||||
var expectedDatagrams uint64
|
||||
minimumFrameBytes, maximumFrameBytes := maxCompleteFrameBytes, 0
|
||||
for index := int64(0); index < targetFrames; index++ {
|
||||
size := qualificationFrameSize(profile, index)
|
||||
targetBytes += int64(size)
|
||||
expectedDatagrams += uint64((size + frameV2PayloadSize - 1) / frameV2PayloadSize)
|
||||
minimumFrameBytes = min(minimumFrameBytes, size)
|
||||
maximumFrameBytes = max(maximumFrameBytes, size)
|
||||
}
|
||||
maximumDuration := profile.Duration*105/100 + 250*time.Millisecond
|
||||
started := time.Now()
|
||||
receiveCtx, receiveCancel := context.WithDeadline(context.Background(), started.Add(maximumDuration+2*time.Second))
|
||||
defer receiveCancel()
|
||||
receivedDone := make(chan error, 1)
|
||||
go func() {
|
||||
for index := int64(0); index < targetPackets; index++ {
|
||||
for index := int64(0); index < targetFrames; index++ {
|
||||
recovered, receiveErr := path.receivePayload(receiveCtx)
|
||||
if receiveErr != nil {
|
||||
receivedDone <- receiveErr
|
||||
return
|
||||
}
|
||||
if len(recovered) != len(payload) {
|
||||
expected := qualificationFramePayload(profile, index)
|
||||
if len(recovered) != len(expected) {
|
||||
receivedDone <- fmt.Errorf(
|
||||
"qualification processing payload length = %d, want %d at sequence %d",
|
||||
len(recovered), len(payload), index,
|
||||
len(recovered), len(expected), index,
|
||||
)
|
||||
return
|
||||
}
|
||||
sequence := binary.BigEndian.Uint32(recovered[len(recovered)-4:])
|
||||
if sequence != uint32(index) {
|
||||
receivedDone <- fmt.Errorf(
|
||||
"qualification processing sequence = %d, want %d", sequence, index,
|
||||
)
|
||||
return
|
||||
}
|
||||
expected := append([]byte(nil), payload...)
|
||||
binary.BigEndian.PutUint32(expected[len(expected)-4:], uint32(index))
|
||||
if !bytes.Equal(recovered, expected) {
|
||||
receivedDone <- fmt.Errorf("qualification processing payload bytes changed at sequence %d", index)
|
||||
return
|
||||
@@ -1644,7 +1705,8 @@ func runQualificationProcessing(t *testing.T, profile qualificationMediaProfile,
|
||||
}()
|
||||
var processed int64
|
||||
var nextRelease time.Time
|
||||
for processed < targetPackets {
|
||||
payloadDigest := sha256.New()
|
||||
for processed < targetFrames {
|
||||
select {
|
||||
case receiveErr := <-receivedDone:
|
||||
if receiveErr != nil {
|
||||
@@ -1659,8 +1721,8 @@ func runQualificationProcessing(t *testing.T, profile qualificationMediaProfile,
|
||||
)
|
||||
qualificationWaitUntil(release)
|
||||
nextRelease = release.Add(spacing)
|
||||
current := append([]byte(nil), payload...)
|
||||
binary.BigEndian.PutUint32(current[len(current)-4:], uint32(processed))
|
||||
current := qualificationFramePayload(profile, processed)
|
||||
_, _ = payloadDigest.Write(current)
|
||||
if _, err := path.emit(t, current); err != nil {
|
||||
return qualificationProcessingSummary{}, err
|
||||
}
|
||||
@@ -1684,9 +1746,11 @@ func runQualificationProcessing(t *testing.T, profile qualificationMediaProfile,
|
||||
after.MediaRecovered-before.MediaRecovered != uint64(processed) ||
|
||||
after.MediaEnqueued-before.MediaEnqueued != uint64(processed) ||
|
||||
after.MediaDrops != before.MediaDrops ||
|
||||
after.MediaQueueMaximum > nativeApolloVideoQueuePackets ||
|
||||
after.MediaQueueMaximumBytes > nativeApolloVideoQueueBytes ||
|
||||
after.Metrics.ProcessingSamples-before.Metrics.ProcessingSamples != uint64(processed) ||
|
||||
after.PacerReservations <= before.PacerReservations ||
|
||||
after.Metrics.MediaPackets-before.Metrics.MediaPackets != uint64(processed) {
|
||||
after.Metrics.MediaPackets-before.Metrics.MediaPackets != expectedDatagrams {
|
||||
return qualificationProcessingSummary{}, fmt.Errorf("qualification subprocess bypassed a production stage: before=%#v after=%#v processed=%d", before, after, processed)
|
||||
}
|
||||
samples, err := readQualificationProcessingSamples(rawPath, record.Count)
|
||||
@@ -1704,13 +1768,18 @@ func runQualificationProcessing(t *testing.T, profile qualificationMediaProfile,
|
||||
summary.Profile = profile.Name
|
||||
summary.Codec = profile.Codec
|
||||
summary.ConfiguredBitrateKbps = profile.BitrateKbps
|
||||
summary.ObservedBitrateKbps = float64(processed*int64(profile.PacketBytes)*8) / actualDuration.Seconds() / 1000
|
||||
summary.ObservedBitrateKbps = float64(targetBytes*8) / actualDuration.Seconds() / 1000
|
||||
summary.ConfiguredFPS = fps
|
||||
summary.ObservedFPS = float64(processed) / actualDuration.Seconds()
|
||||
summary.PayloadBytes = targetBytes
|
||||
summary.MinimumFrameBytes = minimumFrameBytes
|
||||
summary.MaximumFrameBytes = maximumFrameBytes
|
||||
summary.Warmup = profile.Warmup
|
||||
summary.ConfiguredDuration = profile.Duration
|
||||
summary.ActualDuration = actualDuration
|
||||
summary.ClockOverhead = record.ClockOverhead
|
||||
summary.ClockMethod = record.ClockMethod
|
||||
summary.PayloadSHA256 = fmt.Sprintf("%x", sha256.Sum256(payload))
|
||||
summary.PayloadSHA256 = fmt.Sprintf("%x", payloadDigest.Sum(nil))
|
||||
summary.RawSamples = filepath.Base(rawPath)
|
||||
summary.RawSamplesSHA256 = sum
|
||||
summary.RawSamplesBytes = size
|
||||
|
||||
@@ -47,16 +47,17 @@ type qualificationGatewayProcessReady struct {
|
||||
}
|
||||
|
||||
type qualificationGatewayProcessSnapshot struct {
|
||||
Metrics MetricsSnapshot
|
||||
NativeSetups uint64
|
||||
NativeOpens uint64
|
||||
MediaIngress uint64
|
||||
MediaRecovered uint64
|
||||
MediaEnqueued uint64
|
||||
MediaDrops uint64
|
||||
MediaQueueMaximum uint64
|
||||
PacerReservations uint64
|
||||
ProviderTelemetry ProviderTelemetry
|
||||
Metrics MetricsSnapshot
|
||||
NativeSetups uint64
|
||||
NativeOpens uint64
|
||||
MediaIngress uint64
|
||||
MediaRecovered uint64
|
||||
MediaEnqueued uint64
|
||||
MediaDrops uint64
|
||||
MediaQueueMaximum uint64
|
||||
MediaQueueMaximumBytes uint64
|
||||
PacerReservations uint64
|
||||
ProviderTelemetry ProviderTelemetry
|
||||
}
|
||||
|
||||
type qualificationProcessRecordRequest struct {
|
||||
@@ -458,6 +459,7 @@ func TestQualificationGatewayProcessChild(t *testing.T) {
|
||||
snapshot.MediaEnqueued = session.mediaEnqueued.Load()
|
||||
snapshot.MediaDrops = session.mediaDrops.Load()
|
||||
snapshot.MediaQueueMaximum = session.mediaQueueMaximum.Load()
|
||||
snapshot.MediaQueueMaximumBytes = session.mediaQueueMaximumBytes.Load()
|
||||
snapshot.ProviderTelemetry = session.Telemetry()
|
||||
}
|
||||
_ = json.NewEncoder(response).Encode(snapshot)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -17,6 +18,24 @@ func TestGatewaySlowReaderStillCleansUpWithinBound(t *testing.T) {
|
||||
harness.waitReleased(t)
|
||||
}
|
||||
|
||||
func TestGatewayDropsVideoPastQueueResidenceBound(t *testing.T) {
|
||||
harness := newGatewayTransportHarness(t)
|
||||
harness.drainInitialMedia(t)
|
||||
before := harness.server.Metrics().MediaDrops
|
||||
harness.session.video <- ProviderMedia{
|
||||
Payload: []byte("stale-complete-frame"), ReceivedAt: time.Now().Add(-time.Second),
|
||||
EnqueuedAt: time.Now().Add(-nativeApolloVideoQueueLatency - time.Millisecond),
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
|
||||
defer cancel()
|
||||
if frame, err := harness.client.ReceiveFrame(ctx); err == nil {
|
||||
t.Fatalf("expired provider frame crossed the public transport: %#v", frame)
|
||||
}
|
||||
if drops := harness.server.Metrics().MediaDrops - before; drops != 1 {
|
||||
t.Fatalf("expired queue drops = %d, want 1", drops)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayMalformedUDPDoesNotAmplify(t *testing.T) {
|
||||
harness := newGatewayTransportHarness(t)
|
||||
connection, err := net.DialUDP("udp", nil, harness.server.Addr().(*net.UDPAddr))
|
||||
|
||||
@@ -673,6 +673,14 @@ func (s *gatewaySession) mediaLoop() {
|
||||
video = nil
|
||||
continue
|
||||
}
|
||||
if media.expiry != nil {
|
||||
media.expiry.Stop()
|
||||
}
|
||||
media.releaseQueue()
|
||||
if !media.EnqueuedAt.IsZero() && time.Since(media.EnqueuedAt) > nativeApolloVideoQueueLatency {
|
||||
s.server.metrics.MediaDrops.Add(1)
|
||||
continue
|
||||
}
|
||||
if err := s.forwardMedia(ChannelVideo, media); err != nil {
|
||||
s.result <- err
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user