feat(gateway): relay complete encoded frames

This commit is contained in:
sechmachine
2026-07-30 22:47:48 +07:00
parent f12ed6c685
commit 786c96b110
22 changed files with 1023 additions and 159 deletions
+107 -4
View File
@@ -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)
+63 -13
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+16
View File
@@ -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 {
+47 -4
View File
@@ -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)
}
}
}
+150 -81
View File
@@ -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
+12 -10
View File
@@ -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)
+19
View File
@@ -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))
+8
View File
@@ -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