callharness

package
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: Apache-2.0 Imports: 58 Imported by: 0

Documentation

Overview

Package callharness is the call harness: the single test seam for Relais media. It starts the system in-process, a Pion WebRTC client plays the caller, and tests check only what the caller can observe: the SDP answer, connection-state changes, the packets it receives and when, whether the echoed video decodes, SRTP decryption failures, keyframe requests, and any renegotiation or ICE restart.

Today the system is one media worker serving WHIP-style signaling, or several workers sharing one UDP socket so that a call can move between them (Call.Handover, a planned handover), or, with Options.Relay, one or more workers behind a relay (see topology.go). A test looks like this:

h, _ := callharness.Start(callharness.Options{})
defer h.Close()
call, _ := h.Dial(ctx, callharness.CallOptions{Video: true})
_ = call.SendMedia(ctx, 30*time.Second)
report, _ := call.Hangup(ctx)
t.Log(report.Summary())

Index

Constants

View Source
const (
	DialConnected = "connected"
	DialFailed    = "failed"
)

Dial outcomes (DialReport.Outcome).

View Source
const (
	// DialFailureSetup: the dial failed before it sent its offer.
	DialFailureSetup = "setup"
	// DialFailureSignaling: the offer/answer exchange failed.
	DialFailureSignaling = "signaling"
	// DialFailurePathNeverOpened: no connectivity check got an answer, so
	// the UDP path to the server never completed a round trip.
	DialFailurePathNeverOpened = "path_never_opened"
	// DialFailureICEStalled: some checks were answered, but ICE never
	// connected.
	DialFailureICEStalled = "ice_stalled"
	// DialFailureICEConnectedStalled: ICE connected, but DTLS or the
	// connected state did not follow. The path worked, so this is a product
	// failure.
	DialFailureICEConnectedStalled = "ice_connected_stalled"
	// DialFailureHarnessClosed: the harness closed during the dial.
	DialFailureHarnessClosed = "harness_closed"
)

Dial failure kinds (DialReport.Failure). The kind comes from what the caller observed, so a path that never opened is told apart from a server that opened the path and then stalled.

View Source
const MaxRetainedLostUnits = 4096

MaxRetainedLostUnits bounds each kind's whole-call lost-unit inventory.

View Source
const MaxRetainedMediaGaps = 4096
View Source
const MaxToleratedStall = 2*toleratedSendLateness + opusFrameDuration

MaxToleratedStall is the largest caller stall the load judges tolerate in an isolated generator sample: two 20 ms send-lateness limits plus one audio frame. Such a stall can stamp the packet before an outage late and shorten the measured gap by up to this much (#133).

View Source
const MediaGapCeiling = 150 * time.Millisecond

MediaGapCeiling is the whole-call interruption limit that the load judges apply outside scheduled events.

View Source
const MediaGapRetention = MediaGapCeiling - MaxToleratedStall

MediaGapRetention is the shortest gap kept in MediaGaps. A gap above MediaGapCeiling that a tolerated stall shortened is still retained.

View Source
const MinimumConclusivePercent = 80

MinimumConclusivePercent applies to every metric a scenario asserts. Unknown events do not count as passes; at least 80% must have trustworthy judgments.

Variables

View Source
var ErrClockEvidence = errors.New("invalid clock evidence")
View Source
var ErrHarnessClosed = errors.New("callharness: harness closed")

ErrHarnessClosed is returned by Dial once the harness is closing.

Functions

func EnoughConclusive

func EnoughConclusive(conclusive, total int) bool

func JudgeDistributedTrials

func JudgeDistributedTrials(moves []MoveReport) (string, []string)

JudgeDistributedTrials preserves the existing count policy. It does not invent a network-loss allowance for a real unshaped run.

func RequireFreshnessRecovered

func RequireFreshnessRecovered(t interface {
	Helper()
	Name() string
	Fatalf(string, ...any)
}, event EventMeasurement, required FreshnessPolicy)

RequireFreshnessRecovered gates acceptance freshness, independently of loss. Phase-1 scenarios only report freshness; buffered takeovers in #27 can use this helper without the race detector. Acceptance tests must be named TestAcceptance… and must not skip when run without race; the timing CI target includes that prefix. Measure using CallOptions.Freshness with the required recovery limit, at least its stability window, and at most its absolute ceiling and live lateness cap. Observe the full measured stability window first. Use StrictAcceptancePolicy for in-process/LAN tests; #37 network profiles must pass their required absolute ceiling explicitly. The recovery limit is always 2s, stability is at least 3s, and live lateness is at most 200ms, even if both the measurement and required policy are weaker. Audio is required; video is required when the call expects video. Unknown or failed freshness cannot pass this gate.

Types

type AnswerFacts

type AnswerFacts struct {
	SDP        string
	ICELite    bool     // a=ice-lite at session level
	Bundle     []string // MIDs in a=group:BUNDLE
	RTCPMux    bool     // every accepted m-line has a=rtcp-mux
	Candidates []string // every a=candidate value

	// Media are the answer's m-lines in order.
	Media []AnswerMedia
}

AnswerFacts is what the caller can read from the SDP answer.

func (AnswerFacts) AcceptedMedia

func (a AnswerFacts) AcceptedMedia(kind string) *AnswerMedia

AcceptedMedia returns the first accepted m-line of a kind, or nil.

func (AnswerFacts) HostCandidates

func (a AnswerFacts) HostCandidates() []string

HostCandidates returns the answer's host candidates.

type AnswerMedia

type AnswerMedia struct {
	Kind string // "audio", "video", "application", ...
	MID  string
	Port int // 0 when the answer rejects the m-line

	// Formats are the payload types; Codecs are their a=rtpmap values (for
	// example "opus/48000/2"), and RTCPFeedback the a=rtcp-fb values.
	Formats      []string
	Codecs       []string
	RTCPFeedback []string
}

AnswerMedia is one m-line of the answer.

func (AnswerMedia) Accepted

func (m AnswerMedia) Accepted() bool

Accepted reports whether the answer accepts the m-line.

type Call

type Call struct {
	// contains filtered or unexported fields
}

Call is one call made by the caller: a Pion PeerConnection that sends pre-encoded Opus audio (and VP8 video) and records everything it observes.

func (*Call) ArmDistributedEvent

func (c *Call) ArmDistributedEvent(id OperationIdentity) error

func (*Call) ClockProbes

func (c *Call) ClockProbes() []ClockProbe

func (*Call) DialReport

func (c *Call) DialReport() DialReport

DialReport returns what the caller observed while the call connected.

func (*Call) Handover

func (c *Call) Handover(opts HandoverOptions) error

Handover moves the call to another worker through the relay control plane or on a shared socket. The old worker exports the session to bytes and the new one resumes them. It needs Options.Workers of 2 or more. The report describes each move as the caller saw it (Report.Moves).

func (*Call) Hangup

func (c *Call) Hangup(ctx context.Context) (*Report, error)

Hangup ends the call and returns what the caller observed. It waits briefly for the last echoed packets, stops recording, sends the WHIP-style DELETE, closes the PeerConnection and runs the full video decode.

func (*Call) MoveExternal

func (c *Call) MoveExternal(ctx context.Context, to int) error

MoveExternal uses the operator HTTP endpoint and records the same caller yardstick as Handover. Worker names in the process driver are integer IDs. A definite control_not_ready refusal before any move starts can wait for readiness within the original two-second API budget. Started or uncertain outcomes are never replayed.

func (*Call) RecordDistributedEvent

func (c *Call) RecordDistributedEvent(e DistributedEvent) error

func (*Call) RecordExternalMove

func (c *Call) RecordExternalMove(result controlplane.MoveResult)

RecordExternalMove records a completed operator move (for example a drain). It observes the real control-plane result; it does not initiate recovery.

func (*Call) Recording

func (c *Call) Recording() RecordingReport

Recording snapshots retained caller history while a call is still running.

func (*Call) RequestKeyframe

func (c *Call) RequestKeyframe(request KeyframeRequest) error

RequestKeyframe asks the system for a keyframe on the echoed video, as a browser's receiver does when it cannot decode. It fails until the echoed video track has arrived.

func (*Call) SendLongRun

func (c *Call) SendLongRun(ctx context.Context, o LongRunOptions) (*LongRunReport, error)

SendLongRun runs one long call, schedules events, samples its memory, then hangs up. Dial with RecordingOptions{History:15*time.Second, DecodeEvery:30} to select bounded online sampling. Cancellation still closes the call.

func (*Call) SendMedia

func (c *Call) SendMedia(ctx context.Context, duration time.Duration) error

SendMedia plays the pre-encoded media in real time, looping it, for the given duration: Opus audio, and VP8 video when the call has video.

func (*Call) SessionID

func (c *Call) SessionID() string

SessionID is the call's session ID as the caller sees it: the ICE ufrag in the answer, which the media worker sets to the session ID.

type CallOptions

type CallOptions struct {
	// Freshness configures the caller's bounded recovery evaluation.
	// Defaults stay strict (2s recovery + 3s stability), including under race.
	// Acceptance callers such as #27 can set RecoveryLimit: 2*time.Second
	// explicitly and use RequireFreshnessRecovered with the required policy
	// (StrictAcceptancePolicy by default) without race. Phase-1
	// scenarios report freshness without gating it, with or without race.
	// These defaults and measured recovery times remain strict. Allow
	// enough media to observe Freshness.EvaluationWindow().
	Freshness FreshnessPolicy
	// SendTiming observes each successful RTP socket write. It must be fast
	// and safe for concurrent audio/video sends. Scheduled is the delivered
	// tick that triggered the frame; actual is after the UDP write.
	SendTiming func(kind string, scheduled, actual time.Time)
	// SendPacing counts successful frames and dropped ticker opportunities.
	// Video fragments count as one frame. The callback must be concurrent-safe.
	SendPacing func(kind string, sent, skipped uint64)
	Recording  RecordingOptions
	// SRTPDiagnostics authenticates a separate copy of received media for
	// bounded packet/index diagnostics. It never changes Pion's accounting.
	SRTPDiagnostics bool

	// InitialSequenceNumbers replaces random RTP starts for wrap tests. Nil
	// keeps Pion's random starts. Both tracks keep their normal packetizers.
	InitialSequenceNumbers *RTPSequenceNumbers

	// Video adds a VP8 video track next to the Opus audio track, on the same
	// bundled connection.
	Video bool
	// VideoData optionally supplies a VP8 IVF fixture. Nil uses the embedded
	// 320x240/30 fps fixture (natural keyframe interval one second).
	VideoData []byte

	// BrowserLikeOffer makes the caller offer what a browser offers rather
	// than just the codecs it sends: every codec Pion knows (VP8, VP9, H264,
	// H265 and AV1, each with RTX), RED, ULPFEC, telephone-event and comfort
	// noise, extra RTP header extensions, and an RTX ssrc-group for video.
	BrowserLikeOffer bool

	// Worker is the index of the media worker that takes the call (see
	// Options.Workers); 0 is the first in an owned topology.
	// In external mode the number is the literal registration name: the zero
	// value CallOptions{} pins to "0", which must exist.
	// -1 leaves placement unpinned on relay/control-plane topologies.
	Worker int
}

CallOptions shape the call the caller makes.

type CandidatePairChecks

type CandidatePairChecks struct {
	Local             string        `json:"local"`
	Remote            string        `json:"remote"`
	ChecksSent        uint64        `json:"checks_sent"`
	ResponsesReceived uint64        `json:"responses_received"`
	FirstCheck        time.Duration `json:"first_check_ns,omitempty"`
	LastCheck         time.Duration `json:"last_check_ns,omitempty"`
	LastResponse      time.Duration `json:"last_response_ns,omitempty"`
}

CandidatePairChecks counts the binding requests the caller sent on one candidate pair and the success responses it received. A zero time means the event did not happen.

type ClockProbe

type ClockProbe struct {
	CallerSend    time.Time `json:"caller_send"`
	CallerReceive time.Time `json:"caller_receive"`
	ServerReceive time.Time `json:"server_receive"`
	ServerSend    time.Time `json:"server_send"`
}

func ProbeClock

func ProbeClock(ctx context.Context, client *http.Client, endpoint string) (ClockProbe, error)

ProbeClock uses the caller's existing trusted/authenticated HTTP client.

func (ClockProbe) Offset

func (p ClockProbe) Offset() (OffsetInterval, error)

type ClockRateInterval

type ClockRateInterval struct {
	LowerPPM float64 `json:"lower_ppm"`
	UpperPPM float64 `json:"upper_ppm"`
}

ClockRateInterval is the envelope of frontend-minus-caller average rate bounds from probe pairs; the relative rate need not stay constant. A WAN pair may provide only a wide bound; its midpoint is never treated as a rate.

type ClockTracker

type ClockTracker struct {
	// contains filtered or unexported fields
}

ClockTracker retains timestamp probes. A detected jump invalidates the run, including earlier events; accepting a later sample cannot repair clock trust.

func NewClockTracker

func NewClockTracker(ppm float64) (*ClockTracker, error)

func (*ClockTracker) Add

func (c *ClockTracker) Add(p ClockProbe) error

func (*ClockTracker) DriftPPM

func (c *ClockTracker) DriftPPM() float64

DriftPPM is the immutable relative rate bound used for event mapping.

func (*ClockTracker) Err

func (c *ClockTracker) Err() error

func (*ClockTracker) Invalidate

func (c *ClockTracker) Invalidate(err error)

Invalidate preserves a failed probe as lost clock trust for this run.

func (*ClockTracker) Map

func (c *ClockTracker) Map(server time.Time, host OffsetInterval) (TimeInterval, error)

Map composes frontend-minus-caller probes with host-minus-frontend bounds. Intersect all drift-widened samples at the event. A low-RTT earlier probe can provide a tighter bound than the nearest probe; repeated measurements never assume symmetric one-way latency or divide the full bound by two.

func (*ClockTracker) ObservedRate

func (c *ClockTracker) ObservedRate() *ClockRateInterval

func (*ClockTracker) Probes

func (c *ClockTracker) Probes() []ClockProbe

type ConsentReport

type ConsentReport struct {
	// RequestsSent and ResponsesReceived are the totals until hangup.
	RequestsSent      uint64
	ResponsesReceived uint64

	// Since is when the window below starts: the end of the last move, or
	// when the call connected if nothing moved. ObservedFor runs from there
	// to hangup, or to when the caller's connection closed or failed if that
	// came first.
	Since       time.Duration
	ObservedFor time.Duration

	// ResponsesAfter counts responses received in the window, and
	// LongestWithoutResponse is the longest stretch of it without a new
	// response, the stretch from the last response to the end of the window
	// included. The caller checks every 2 s; with no packet at all for 5 s
	// its connection goes "disconnected".
	ResponsesAfter         uint64
	LongestWithoutResponse time.Duration
}

ConsentReport is what the caller observed of its ICE consent checks: the STUN binding requests it sent and the binding success responses to them, seen on its own UDP socket (see consent.go).

type ContentDelivery

type ContentDelivery struct {
	Sent, Returned, Lost, Unsettled int
	LostAt                          []time.Duration
	LostOverflow                    int
}

ContentDelivery counts one kind's identified content over the whole call, once per successfully written audio unit or video frame. A unit settles when it leaves rolling history, or at hangup when it was sent at least the 500ms content settle window earlier; later units are Unsettled and unjudged. Lost uses the yardstick's rule: no valid complete return by then. Late returns are not lost. LostAt holds each lost unit's send time (caller clock, sorted), up to MaxRetainedLostUnits; further losses only add LostOverflow.

type ContentLossCounts

type ContentLossCounts struct {
	Sent, Lost     int
	Trusted        bool
	LongestLostRun int
	RunTrusted     bool
}

ContentLossCounts counts identified, successfully written content, once per audio unit or complete video frame. Loss means no valid return by the same observation horizon used by EventMeasurement, independent of decoding.

type ContentLossEvidence

type ContentLossEvidence struct {
	Event     ContentLossWindow
	Baselines []ContentLossWindow
}

ContentLossEvidence retains raw denominators and matched no-event windows. Baselines have the event window's exact length, do not overlap each other, and end at least one settle window before the event window starts. It adds evidence without changing the caller yardstick's exact zero-loss verdicts.

type ContentLossWindow

type ContentLossWindow struct {
	Start, End   time.Duration
	Audio, Video ContentLossCounts
}

type DecodeFailure

type DecodeFailure string

DecodeFailure separates confirmed invalid bitstreams from an unavailable check (timeout, missing decoder, or execution failure).

const (
	DecodeInvalid     DecodeFailure = "invalid bitstream"
	DecodeUnavailable DecodeFailure = "unavailable"
)

type DecryptionFailures

type DecryptionFailures struct {
	AuthTag int // authentication tag did not verify (wrong key, index or rollover counter)
	Replay  int // rejected by the replay window
	Other   int
}

DecryptionFailures classifies packets the caller could not decrypt.

func (DecryptionFailures) Total

func (d DecryptionFailures) Total() int

Total is the number of packets the caller could not decrypt.

type DialError

type DialError struct {
	Report DialReport
	// contains filtered or unexported fields
}

DialError is every error Dial returns. It keeps the dial's evidence; errors.Is and errors.As see the underlying error, and Error returns its message unchanged.

func (*DialError) Error

func (e *DialError) Error() string

func (*DialError) Unwrap

func (e *DialError) Unwrap() error

type DialReport

type DialReport struct {
	Outcome string `json:"outcome"`
	// Failure is the kind of a failed dial (DialFailure*); empty if the
	// dial connected. Error is the dial's error.
	Failure string `json:"failure,omitempty"`
	Error   string `json:"error,omitempty"`
	// Session is the call's session ID, the server's ICE username fragment;
	// empty if no answer was applied.
	Session string `json:"session,omitempty"`
	// Elapsed is when the dial connected or failed.
	Elapsed time.Duration `json:"elapsed_ns"`

	// The ICE, DTLS and PeerConnection states when the dial connected or
	// failed, and every change of each before then.
	ICEState         string        `json:"ice_state"`
	DTLSState        string        `json:"dtls_state"`
	ConnectionState  string        `json:"connection_state"`
	ICEStates        []StateChange `json:"ice_states"`
	DTLSStates       []StateChange `json:"dtls_states"`
	ConnectionStates []StateChange `json:"connection_states"`

	// CandidatePairs counts the caller's connectivity checks on each
	// candidate pair, as observed on the caller's socket.
	CandidatePairs []CandidatePairChecks `json:"candidate_pairs"`
}

DialReport is what the caller observed while one dial set up its call: how ICE, DTLS and the PeerConnection progressed, and how the caller's connectivity checks were answered. Times are offsets from the start of the dial. A failed dial returns it in a *DialError; a connected one keeps it in Call.DialReport.

type DistributedEvent

type DistributedEvent struct {
	Identity  OperationIdentity       `json:"identity"`
	Kind      string                  `json:"kind"`
	Start     time.Time               `json:"start"`
	End       time.Time               `json:"end"`
	Result    controlplane.MoveResult `json:"result"`
	Uncertain bool                    `json:"uncertain,omitempty"`
}

DistributedEvent is a durable server receipt, never a command submission. Start is control-host move start or worker-host actual SIGKILL time. End is control-host completion. Uncertain preserves unknown operation outcomes.

type DistributedGap

type DistributedGap struct {
	BaselinePossible []time.Duration `json:"baseline_possible_ns"`
	BaselineCertain  []time.Duration `json:"baseline_certain_ns"`

	Kind        string           `json:"kind"`
	BaselineP95 DurationInterval `json:"baseline_p95"`
	EventGap    DurationInterval `json:"event_gap"`
	Outcome     string           `json:"outcome"`
	Causes      []string         `json:"causes,omitempty"`
}

type DistributedMeasurement

type DistributedMeasurement struct {
	Durations map[string]DurationInterval `json:"durations,omitempty"`
	// Evaluations counts start/end/floor cells, each checked against all first baselines.
	Evaluations    int              `json:"evaluations"`
	FirstBaselines []FirstBaselines `json:"first_baselines,omitempty"`
	BaselineFloor  DurationInterval `json:"baseline_floor"`

	Mapped bool `json:"mapped"`

	AudioLoss MetricVerdict `json:"audio_loss"`
	VideoLoss MetricVerdict `json:"video_loss"`

	WindowOutcome string `json:"window_outcome"`

	Start      TimeInterval      `json:"start"`
	End        TimeInterval      `json:"end"`
	KillToLive *DurationInterval `json:"kill_to_live,omitempty"`
	Gaps       []DistributedGap  `json:"gaps"`
	Outcome    string            `json:"outcome"`
	Causes     []string          `json:"causes,omitempty"`
}

DistributedMeasurement keeps raw caller gaps independent of event clocks. Union windows prove passes; intersection windows can prove failures. A failure seen only in the uncertainty margin is inconclusive.

type DistributedOptions

type DistributedOptions struct {
	RunID string
	Clock *ClockTracker
	// Host minus frontend offsets. These are measured input bounds, not a
	// claim that /clock observes control or worker time.
	ControlOffset OffsetInterval
	WorkerOffset  OffsetInterval
}

type DistributedRecording

type DistributedRecording struct {
	ObservedRate *ClockRateInterval `json:"observed_rate_ppm,omitempty"`
	DriftPPM     float64            `json:"drift_ppm"`

	Armed   []OperationIdentity `json:"armed"`
	Missing []OperationIdentity `json:"missing_receipts,omitempty"`

	RunID         string         `json:"run_id"`
	ControlOffset OffsetInterval `json:"control_offset"`
	WorkerOffset  OffsetInterval `json:"worker_offset"`
	Probes        []ClockProbe   `json:"probes"`
	ClockError    string         `json:"clock_error,omitempty"`
	Outcome       string         `json:"outcome"`
	Causes        []string       `json:"causes,omitempty"`
}

DistributedRecording makes clock evidence reviewable in the final artifact.

type DurationInterval

type DurationInterval struct {
	Lower time.Duration `json:"lower_ns"`
	Upper time.Duration `json:"upper_ns"`
}

type EventMeasurement

type EventMeasurement struct {
	ContentLoss                                                   ContentLossEvidence
	FirstContentIdentity, FirstNewContentIdentity                 uint64
	VideoExpected                                                 bool
	AudioPause, VideoPause                                        time.Duration
	WindowStart, WindowEnd, SettledAt, ObservedUntil              time.Duration
	SettleWindow                                                  time.Duration
	LostAudio, LateAudio                                          time.Duration
	LostVideoFrames, LateVideoFrames                              int
	AudioLoss, VideoLoss                                          MetricVerdict
	Audio, Video                                                  FreshnessReport
	MediaResumedAt                                                time.Duration
	ResumeVerdict                                                 MetricVerdict
	FirstLiveFrame, FirstNewFrame                                 time.Duration
	FirstContent, FirstNewContent                                 string
	FirstContentPictureID, FirstNewContentPictureID               uint16
	FirstContentSentAt, FirstNewContentSentAt                     time.Duration
	FirstContentVerdict, FirstNewContentVerdict, FirstLiveVerdict MetricVerdict
	DecodedFrames                                                 int
	DecodedSamples                                                int
	Inconclusive, Failed                                          bool
	Reasons                                                       []string
}

EventMeasurement separates interruption, missing content, content age and decoded recovery. All times use the caller clock, not RTP timestamps. Pause retains the existing per-kind MoveTrackReport.Gap. Loss counts successful sends in [Start-250ms, max(End,MediaResumedAt)+250ms). Lost means no valid complete return by ObservedUntil: hangup, or the saved summary horizon for an event whose bounded history expired. Returns after SettledAt are late, separately from loss. Hangup before SettledAt makes loss unknown. Duplicate/reordered returns count once. Nearby event windows can overlap; never sum their losses. Video loss does not depend on decoding. FirstContent classifies the first decoded frame after resume. Prior complete receipt before Start means cache, regardless of who replays the frame. FirstNewContent skips those prior receipts. Its classes are keyframe (PLI/FIR requested at/after Start), outage media (sent from WindowStart until resume with latency above baseline p95+tolerance), natural keyframe (a keyframe restarting a broken decode chain without a post-event request), or live. Ticket #27 passes its first-new-content gate only for "outage media"; a natural keyframe wait does not pass it. Ordinary in-flight frames are live. FirstNewFrame is event-to-decode for that new content. FirstLiveFrame retains event-to-decode for the first frame sent at/after MediaResumedAt. Each verdict applies to one metric. Inconclusive is informational: at least one metric is unknown. Failed means at least one trusted metric failed. Missing resume/decoded content/recovery are failures, not uncertainty. Conflicting payload identities fail the affected video metrics, including when their observation window or another metric is inconclusive.

func (EventMeasurement) Conclusive

func (m EventMeasurement) Conclusive() bool

func (EventMeasurement) ContentRecoveryFailed

func (m EventMeasurement) ContentRecoveryFailed() bool

ContentRecoveryFailed gates resume and decoded-content evidence, including identity conflicts, without applying a freshness acceptance limit. Phase-1 crash scenarios use this gate and retain loss/coverage as separate checks. Lost outage content is expected in today's unbuffered crashes.

func (EventMeasurement) LossConclusive

func (m EventMeasurement) LossConclusive() bool

func (EventMeasurement) RecoveryFailed

func (m EventMeasurement) RecoveryFailed() bool

RecoveryFailed excludes loss, which is expected in today's unbuffered crashes.

func (EventMeasurement) Summary

func (m EventMeasurement) Summary() string

Summary prints an event record with explicit units and measurement policy.

type ExternalTopology

type ExternalTopology struct {
	Distributed *DistributedOptions

	SignalingURL string
	RelayAddr    netip.AddrPort
	// HTTPClient supplies signaling and control requests, including status and
	// hangup. Nil uses the ordinary ten-second client. The caller owns the
	// client and its transport; the harness neither changes nor closes them.
	HTTPClient *http.Client
	// CallerSocket creates the caller's UDP4 media socket. Nil preserves the
	// loopback socket and candidates. A supplied socket must bind a specific
	// IPv4 address; the harness owns and closes it. Signaling stays separate.
	CallerSocket func() (net.PacketConn, error)
	// Status optionally supplies a shared status view for external callers.
	// Nil keeps the ordinary per-call HTTP reads and local harness behavior.
	Status func(context.Context) (controlplane.Status, error)
	// FinalStatus optionally refreshes the shared status view at Hangup with
	// a final-collection timeout. Nil preserves the ordinary Status path.
	FinalStatus func(context.Context) (controlplane.Status, error)
}

ExternalTopology connects the real caller to an already running system. The harness owns only its callers; it never stops external processes.

type FirstBaselines

type FirstBaselines struct {
	Audio FreshnessBaseline `json:"audio"`
	Video FreshnessBaseline `json:"video"`
}

type FreshnessBaseline

type FreshnessBaseline struct {
	Samples int           `json:"samples"`
	Median  time.Duration `json:"median_ns"`
	P95     time.Duration `json:"p95_ns"`
}

These are caller-local baseline observations, not server-clock estimates. Keep every distinct first-event baseline so later events cannot silently treat one possible noisy/slow first baseline as an established reference.

type FreshnessPolicy

type FreshnessPolicy struct {
	RecoveryLimit, StabilityWindow          time.Duration
	AbsoluteLatencyCeiling, LiveLatenessCap time.Duration
}

FreshnessPolicy configures event recovery attribution. Nonpositive values use defaults: 2s recovery, 3s stability, a 50ms absolute median ceiling, and a 200ms live lateness cap above baseline p95. Set the ceiling per call for a different network latency profile. RecoveryLimit is explicit through CallOptions.Freshness and retained on the report. Buffered-takeover acceptance (#27) uses 2s without race via RequireFreshnessRecovered. Existing phase-1 scenarios only report freshness, with or without race. Defaults and measured recovery times never change under the race detector.

func StrictAcceptancePolicy

func StrictAcceptancePolicy() FreshnessPolicy

StrictAcceptancePolicy is the in-process/LAN buffered-takeover acceptance policy. Network profiles can set their required absolute ceiling explicitly.

func (FreshnessPolicy) EvaluationWindow

func (p FreshnessPolicy) EvaluationWindow() time.Duration

EvaluationWindow is the full event observation duration, applying defaults.

type FreshnessReport

type FreshnessReport struct {
	Verdict                                            MetricVerdict
	RecoveryLimit, StabilityWindow, EvaluationEnd      time.Duration
	StabilityObserved                                  time.Duration
	AbsoluteLatencyCeiling, LiveLatenessCap            time.Duration
	AbsoluteLag                                        bool
	LateWindowUnits, LateWindowLimit, LatePercentLimit int
	LiveUnits, LateLiveUnits                           int
	JitterUnits                                        int
	JitterMaxExcess                                    time.Duration
	FirstBaselineTrusted                               bool
	LastAboveTolerance                                 time.Duration
	FirstBaselineSamples                               int
	FirstBaselineMedian, FirstBaselineP95              time.Duration
	LastingLag                                         bool
	BaselineStart                                      time.Duration
	BaselineSamples, PostEventSamples                  int
	BaselineMedian, BaselineP95                        time.Duration
	PeakLatency, PeakAboveBaseline                     time.Duration
	BackToBaseline                                     time.Duration
	Recovered                                          bool
	Tolerance                                          time.Duration
	SustainedUnits                                     int
	SpreadLimit                                        time.Duration
}

FreshnessReport uses one latency sample per unique returned content unit: arrival minus send time (video arrival is completion of all frame packets). BaselineMedian and BaselineP95 use units received in the preceding 1s whose sends also precede the event. After a preceding takeover, the window starts no earlier than that event's measured audio/video recovery, or its end/resume if recovery failed. A failed event cannot truncate every later baseline. BaselineStart records the actual window start. Ten samples are required; p95-minus-median must be <=20ms. PeakLatency includes every post-event cache/outage return, even repeated content. Repeats cannot add samples or confirm recovery, but a stale repeat interrupts a run of fresh units. Recovery is evaluated only through EvaluationEnd: event start plus RecoveryLimit (default 2s) and StabilityWindow (default 3s), clipped at the next event/hangup. StabilityObserved measures the observed portion after Start+RecoveryLimit. A shortened window is inconclusive, unless a known absolute-latency or identity failure applies. Later units do not affect this event, including its peak. BackToBaseline is the start of the last fresh run confirmed by three unique units. Any post-resume receipt sent before resume breaks the run, regardless of latency. After a candidate run starts, 3 or more late returns in any 10 consecutive eligible live returns break it. At window end, more than 10% late returns since the final candidate start also break it. LateWindowUnits, LateWindowLimit and LatePercentLimit record these rules. Fresh duplicates cannot improve confirmation or dilute either budget. LiveUnits/LateLiveUnits count eligible post-resume returns across the whole evaluation. Short live lateness streaks remain diagnostics, independently of the budget verdict. Any live return later than baseline p95 + LiveLatenessCap (default 200ms) breaks recovery, including duplicates, inside the evaluation window. JitterUnits and JitterMaxExcess report their count and maximum excess over baseline p95+tolerance. Repeats cannot confirm recovery. LastAboveTolerance records the last above-threshold unit. No recovery at window end, or recovery after RecoveryLimit, fails a trusted metric. FirstBaseline* retain the first call baseline; LastingLag flags a relative median/p95 above that trusted baseline by more than tolerance. LastingLag is diagnostic-only: small-sample p95 changes do not gate. AbsoluteLag fails when the trusted first baseline median exceeds the configurable AbsoluteLatencyCeiling (50ms by default for in-process/LAN). Freshness measures content age independently of whether it decoded.

type FullDecode

type FullDecode struct {
	// Skipped says why the decode did not finish (for example, a timeout or
	// ffmpeg not on PATH); empty when it completed.
	Skipped string

	Decoder       string // the ffmpeg binary
	FramesIn      int    // frames given to the decoder
	FramesDecoded int    // frames the decoder output
	Errors        string // the decoder's error output; empty when clean
	Failure       DecodeFailure
}

FullDecode is the result of decoding the received video with ffmpeg.

func (FullDecode) Ran

func (f FullDecode) Ran() bool

Ran reports whether the full decode ran.

type HandoverOptions

type HandoverOptions struct {
	// To is the index of the worker that takes the call over (0 is the
	// worker calls start on).
	To int

	// SequenceMargin moves the echoed tracks' sequence numbers forward by
	// this much across the move; see mediaworker.ResumeOptions. Zero, the
	// right value for a planned handover, keeps them continuous.
	SequenceMargin uint16
}

HandoverOptions shape a planned handover.

type Harness

type Harness struct {
	// contains filtered or unexported fields
}

Harness runs a caller against either local workers and signaling, or an externally owned process topology. External mode owns only its callers.

func Start

func Start(opts Options) (*Harness, error)

Start starts a local topology, or attaches callers to Options.External.

func (*Harness) Close

func (h *Harness) Close() error

Close closes any calls still open, then stops the signaling server, the media workers and the relay. In external mode it closes callers only. Dial fails with ErrHarnessClosed from the moment Close starts.

func (*Harness) Dial

func (h *Harness) Dial(ctx context.Context, opts CallOptions) (call *Call, err error)

Dial starts a call: it makes one WHIP-style offer/answer exchange with the system and waits until the caller's connection is "connected". It fails with ErrHarnessClosed once the harness is closing. Every error it returns is a *DialError that carries what the caller observed.

func (*Harness) Drain

func (h *Harness) Drain(worker int) error

Drain moves all calls off worker through the HTTP endpoint and records each move for the caller's gap and consent reports.

func (*Harness) ExchangeOffer

func (h *Harness) ExchangeOffer(ctx context.Context, offer string) (AnswerFacts, error)

ExchangeOffer posts an SDP offer the way a browser would, for example one captured from Chrome, and returns what the answer says. It then hangs the call up again; no media flows.

func (*Harness) Kill

func (h *Harness) Kill(worker int) error

Kill simulates process death without a flush, export, release or close_notify. Recovery is driven solely by missed heartbeats, never by this action.

func (*Harness) KillBeforeSnapshot

func (h *Harness) KillBeforeSnapshot(ctx context.Context, worker int, id string) error

KillBeforeSnapshot gates the next periodic copy and kills without flushing, so a stale-snapshot kill is deterministic rather than chosen by chance.

func (*Harness) KillMidKeyframe

func (h *Harness) KillMidKeyframe(ctx context.Context, worker int, id string, n int) error

KillMidKeyframe stops after n packets were echoed, before the marker packet. The cancellable gate prevents the worker from finishing the frame first.

func (*Harness) RecordProcessKill

func (h *Harness) RecordProcessKill(worker string, at time.Time)

RecordProcessKill aligns the control plane's takeover event with the real SIGKILL action. It does not cause or accelerate recovery.

func (*Harness) RelayAddr

func (h *Harness) RelayAddr() netip.AddrPort

RelayAddr is the relay's public address, which every answer advertises; the zero value without Options.Relay.

func (*Harness) ReplayStats

func (h *Harness) ReplayStats(worker int) (mediaworker.ReplayStats, error)

ReplayStats snapshots a worker's replay diagnostics. A fresh harness per recovery trial makes these counters specific to that trial.

func (*Harness) RestartRelay

func (h *Harness) RestartRelay() (time.Duration, error)

RestartRelay replaces the relay with a new one on the same public and relay-leg addresses, as a restarted relay process would come back: the flow table is lost, while the session-owner store and the workers carry on, and the new relay is configured with the same workers. It returns once the new relay is listening, with how long no relay was listening.

func (*Harness) RestartWorker

func (h *Harness) RestartWorker(worker int) error

RestartWorker creates an empty replacement on a new private socket after all the previous registration's leases have been handled.

func (*Harness) SessionOwner

func (h *Harness) SessionOwner(sessionID string) (int, bool)

SessionOwner returns the index of the worker that owns a session, as the relay's session-owner store records it (see Call.SessionID). It reports false without Options.Relay, or when no worker owns the session.

func (*Harness) SignalingURL

func (h *Harness) SignalingURL() string

SignalingURL is where a caller POSTs its SDP offer.

func (*Harness) Status

func (h *Harness) Status(ctx context.Context) (controlplane.Status, error)

Status reads the control plane's public HTTP view of ownership and moves.

func (*Harness) WaitForSnapshot

func (h *Harness) WaitForSnapshot(ctx context.Context, worker int, id string) error

WaitForSnapshot observes the initial store write before a harness starts media. It does not flush or inspect state; this is only a test timing seam.

type KeyframeRequest

type KeyframeRequest int

KeyframeRequest is an RTCP message that asks a sender for a keyframe.

const (
	// PLI is a Picture Loss Indication (RFC 4585).
	PLI KeyframeRequest = iota
	// FIR is a Full Intra Request (RFC 5104).
	FIR
)

type LongRunOptions

type LongRunOptions struct {
	Duration, EventEvery, SampleEvery time.Duration
	Event                             func(context.Context, int) error
	HeapAfterGC                       bool
	ObserveMemory                     func(MemorySample)
}

LongRunOptions schedules caller/operator events while the normal SendMedia path runs. Event receives a zero-based event index; nil means no events. HeapAfterGC is for isolated memory checks, and pauses this Go process at each sample. Heap numbers describe the whole process, not just the recorder.

type LongRunReport

type LongRunReport struct {
	Call           *Report
	Memory         []MemorySample
	SamplesDropped int
}

LongRunReport retains at most 256 memory samples, plus the normal per-event caller summaries. SamplesDropped records earlier samples rolled away.

type MediaGap

type MediaGap struct{ EndedAt, Duration time.Duration }

MediaGap records an interval between successfully decrypted packets. These sparse observations survive rolling history, up to MaxRetainedMediaGaps. Only gaps above MediaGapRetention are kept. MediaGapOverflow counts dropped gaps above MediaGapCeiling; a full inventory may also omit shorter gaps.

type MemorySample

type MemorySample struct {
	At        time.Duration
	HeapAlloc uint64
	Recording RecordingReport
}

type MetricVerdict

type MetricVerdict struct {
	Trusted, Failed bool
	Reasons         []string
}

MetricVerdict separates trust from an observed failure. Unknown metrics must not mask failures in other trusted metrics. Reasons explains either result.

func (MetricVerdict) Summary

func (v MetricVerdict) Summary() string

type MoveReport

type MoveReport struct {
	Receipt *DistributedEvent `json:"receipt,omitempty"`

	Identity    *OperationIdentity      `json:"identity,omitempty"`
	Distributed *DistributedMeasurement `json:"distributed,omitempty"`

	Lost      bool   `json:"lost,omitempty"`
	LossCause string `json:"loss_cause,omitempty"`
	// Recovery measures video recovery from the caller's first post-margin RTP
	// packet, including replay received while ResumeSession is still returning.
	Recovery VideoRecovery

	// Measurement is the caller content yardstick, independent of sequence
	// margins and of the system account in Result.
	Measurement EventMeasurement

	Kind          string        // "move" or "takeover"
	DetectionTime time.Duration // failure action to control-plane detection

	DecryptionFailuresAfterResume int
	From, To                      int // worker indexes

	// Start and End bracket the handover call, as offsets from dialing.
	Start, End time.Duration

	// Error is the handover's error; empty when it succeeded.
	Error string

	// Result is the system's account of the move: exported state size,
	// caller packets held and timings.
	Result mediaworker.HandoverResult

	// Tracks describe each received track around the move.
	Tracks []MoveTrackReport
	// contains filtered or unexported fields
}

MoveReport is one planned handover as the caller observed it, plus what the system reported about it.

type MoveTrackReport

type MoveTrackReport struct {
	Kind string

	// Gap is the longest interval between consecutive packets around the
	// move: the media gap the move caused. TypicalInterval is the track's
	// median interval between packets, for comparison.
	BaselineGapP95     time.Duration
	BaselineGapSamples int
	Gap                time.Duration
	TypicalInterval    time.Duration

	// SkippedSequenceNumbers counts sequence numbers missing around the
	// move: a sequence margin's jump, or lost packets.
	SkippedSequenceNumbers int

	// LargestTimestampStep is the largest RTP timestamp step between
	// consecutive packets around the move; TypicalTimestampStep is the
	// track's median step between packets of different frames. With
	// continuous timestamps the first is no larger than one frame's step.
	LargestTimestampStep uint32
	TypicalTimestampStep uint32

	// PacketsAfter counts packets that arrived after the move ended.
	PacketsAfter int

	// FirstDecodableFrameAfter is, for video, the time from the end of the
	// move to the first decodable frame after it; zero if none arrived.
	FirstDecodableFrameAfter time.Duration
	// contains filtered or unexported fields
}

MoveTrackReport is one received track around a move. "Around the move" runs from the last packet that arrived before the move started to the first that arrived after it ended.

type OffsetInterval

type OffsetInterval struct {
	Lower time.Duration `json:"lower_ns"`
	Upper time.Duration `json:"upper_ns"`
}

OffsetInterval is server minus caller time. Bounds are inclusive. Durations and media gaps always stay on the caller's monotonic clock.

func (OffsetInterval) Compose

func (i OffsetInterval) Compose(other OffsetInterval) (OffsetInterval, error)

func (OffsetInterval) Map

func (i OffsetInterval) Map(server time.Time) TimeInterval

func (OffsetInterval) Valid

func (i OffsetInterval) Valid() bool

func (OffsetInterval) Widen

func (i OffsetInterval) Widen(elapsed time.Duration, ppm float64) (OffsetInterval, error)

type OnlineDecodeReport

type OnlineDecodeReport struct {
	Every                                                         int
	Sampled, Decoded, UniqueDecoded, Errors, Dropped, Unavailable int
	LastError                                                     string
}

OnlineDecodeReport counts checked target frames, not the reference frames ffmpeg must decode before an interframe. Keyframes are always decoded in Go. A full queue drops a sample explicitly; it never blocks the RTP reader.

func (OnlineDecodeReport) Conclusive

func (d OnlineDecodeReport) Conclusive() bool

func (OnlineDecodeReport) SampleRate

func (d OnlineDecodeReport) SampleRate(frames int) float64

type OperationIdentity

type OperationIdentity struct {
	RunID       string `json:"run_id"`
	OperationID string `json:"operation_id"`
	SessionID   string `json:"session_id"`
	Worker      string `json:"worker"`
	Epoch       uint64 `json:"epoch"`
}

OperationIdentity is supplied by the operator feed, on arming and on the durable server receipt. Worker is the unique registration/generation name; Epoch identifies the ownership tenure affected, not its successor.

func (OperationIdentity) Valid

func (i OperationIdentity) Valid() bool

type Options

type Options struct {
	// External selects a process topology instead of starting local workers.
	External *ExternalTopology

	// WorkerLoggerFactory is passed to the media workers (and the relay).
	// Defaults to Pion's default logger factory (PION_LOG_* environment
	// variables).
	WorkerLoggerFactory logging.LoggerFactory

	// Workers is how many media workers run. Without Relay, one (the
	// default) owns its UDP socket; two or more share one UDP socket, calls
	// start on the first, and Call.Handover moves a call between them.
	// With Relay, each worker owns a private socket behind the relay, and a
	// call picks its worker with CallOptions.Worker. Call.Handover uses the
	// control plane to transfer ownership and re-point the relay.
	Workers int

	// Relay runs every call through a relay with a session-owner store
	// (Memory by default): answers advertise the relay's public address, and the workers
	// bind only private sockets behind it.
	Relay bool
	// SessionStore selects the relay topology's store. Nil uses Memory.
	// The caller owns its lifetime; it must outlive Harness.Close.
	SessionStore sessionstore.Store
	// FrameCache selects a shared cache, including Redis. Nil uses Memory.
	// The caller owns its lifetime and must keep it open through Harness.Close.
	FrameCache framecache.Store
	// SnapshotInterval sets the worker snapshot cadence for crash tests.
	// TakeoverConfig overrides control-plane bounds for fault scenarios.
	TakeoverConfig   controlplane.Config
	SnapshotInterval time.Duration
	// DisableFrameCache and DisableResumePLI independently select the video
	// recovery paths. Both paths are enabled by default.
	DisableFrameCache bool
	DisableResumePLI  bool
	// ReplayMaxBurstDuration overrides the worker's one-frame replay cap.
	ReplayMaxBurstDuration time.Duration
}

Options configures the system the harness starts.

type RTPSequenceNumbers

type RTPSequenceNumbers struct {
	Audio uint16
	Video uint16
}

RTPSequenceNumbers selects deterministic starts for the caller's tracks.

type RecordingOptions

type RecordingOptions struct {
	History     time.Duration
	DecodeEvery int
	// MaintenancePhase delays the call's one-second recording maintenance
	// (automatic event discovery and history pruning) by this much, so it
	// runs at connect + phase + k seconds. Zero keeps connect + k seconds.
	// Load generators give each call its own phase in [0, 1s), so many
	// callers do not run this work on the same instant (#85).
	MaintenancePhase time.Duration
}

RecordingOptions bounds caller history. Zero values preserve the short-call full-decode check, with at most two minutes of packet/content history and 4096 offline frames. Long calls should select DecodeEvery (e.g. 30), which checks samples online and retains no whole-call decode input. Event summaries survive history expiry. Their ObservedUntil names the finite observation horizon: packets older than History cannot repair a saved loss. History must cover the configured freshness evaluation plus baseline/margin.

type RecordingReport

type RecordingReport struct {
	History                                                      time.Duration
	HistoryStart                                                 time.Duration
	Packets, ContentUnits, Assemblies, FrameMarks, OfflineFrames int
	EventSummaries                                               int
	DecodedPictures                                              int
	HistoryBytes                                                 int64
	Delivery                                                     *WholeCallDelivery `json:",omitempty"`
}

RecordingReport is a caller-owned memory gauge, separate from process heap. EventSummaries grow only with events, not with packets or call duration. HistoryBytes counts retained payload bytes; it excludes Go object overhead. Delivery is set only in a call's final report; live gauges leave it nil.

type Report

type Report struct {
	// ReportBuildDuration measures evidence snapshot, evaluation and judgment.
	// It excludes call teardown, optional offline decoding and JSON transport.
	ReportBuildDuration time.Duration         `json:"report_build_duration_ns,omitempty"`
	Distributed         *DistributedRecording `json:"distributed,omitempty"`

	Recording RecordingReport
	// StartedAt aligns packet observations across calls sharing a worker.
	StartedAt time.Time
	// OfferAnswerExchanges counts the HTTP offer/answer exchanges the caller
	// made to start the call.
	OfferAnswerExchanges int

	// Offer is the caller's SDP offer, and Answer describes the SDP answer
	// the caller received.
	Offer  string
	Answer AnswerFacts

	// RemoteAddr is the remote end ("ip:port") of the caller's selected ICE
	// candidate pair at hangup: the one address the caller sent to and
	// accepted media from.
	RemoteAddr string

	// ConnectedAt is when the caller's connection state first reached
	// "connected"; zero if it never did.
	ConnectedAt time.Duration
	// HungUpAt is when the caller hung up.
	HungUpAt time.Duration

	// ConnectionStates and ICEConnectionStates are the caller's
	// PeerConnection and ICE connection state changes up to hangup.
	ConnectionStates    []StateChange
	ICEConnectionStates []StateChange

	// DecryptionFailures counts SRTP and SRTCP packets the caller received
	// but could not decrypt.
	DecryptionFailures DecryptionFailures
	// SRTPDiagnostics is optional independent packet evidence. DecryptionFailures
	// remains the authoritative rejection count from Pion.
	SRTPDiagnostics *SRTPDiagnosticReport `json:",omitempty"`

	// Renegotiations counts offer/answer rounds (or negotiationneeded events)
	// after the initial exchange. ICERestarts counts ICE restarts: ICE going
	// back to checking after it connected, or changed ICE credentials.
	Renegotiations int
	ICERestarts    int

	// SentAudio and SentVideo are the caller's own tracks. SentVideo is the
	// zero value for a call without video. FirstVideoSentAt is when the
	// caller sent its first video frame.
	SentAudio        SentTrack
	SentVideo        SentTrack
	FirstVideoSentAt time.Duration

	// KeyframeRequestsSent counts the PLI and FIR packets the caller sent for
	// the echoed video (see Call.RequestKeyframe).
	KeyframeRequestsSent int

	// Tracks are the tracks the caller received, in arrival order.
	Tracks []TrackReport

	// Moves are planned handovers and automatic crash takeovers during the call,
	// as the caller observed them. Consent is what the caller observed of
	// its ICE consent checks. See handover.go.
	Moves   []MoveReport
	Consent ConsentReport
}

Report is what the caller observed during one call. Times are offsets from the moment the caller started dialing.

func (*Report) ConnectedThroughout

func (r *Report) ConnectedThroughout() bool

ConnectedThroughout reports whether the caller's connection reached "connected" and stayed there until hangup.

func (*Report) HasDistributedEvidence

func (r *Report) HasDistributedEvidence() bool

HasDistributedEvidence marks event-relative scalar fields as point estimates. Local capacity/soak/crash judges must not use them as clock-proven evidence.

func (*Report) Summary

func (r *Report) Summary() string

Summary formats the report for a test log.

func (*Report) TimeToFirstDecodedFrame

func (r *Report) TimeToFirstDecodedFrame() time.Duration

TimeToFirstDecodedFrame is how long after the connection reached "connected" the first echoed video frame decoded; zero if none did.

func (*Report) Track

func (r *Report) Track(kind string) *TrackReport

Track returns the first received track of a kind ("audio" or "video"), or nil.

type SRTPDiagnosticReport

type SRTPDiagnosticReport struct {
	Ready          bool
	Unavailable    string `json:",omitempty"`
	Packets        uint64
	Skipped        uint64
	Failures       DecryptionFailures
	Samples        []SRTPFailureSample
	SamplesOmitted uint64
}

SRTPDiagnosticReport is a shadow receiver's evidence, not the caller verdict. Only packet metadata is retained. Keys and plaintext never enter the report.

type SRTPFailureSample

type SRTPFailureSample struct {
	At                 time.Duration
	Protocol           string
	SSRC               uint32
	Sequence           uint16 `json:",omitempty"`
	Timestamp          uint32 `json:",omitempty"`
	PayloadType        uint8  `json:",omitempty"`
	EstimatedIndex     uint64
	PreviousIndex      uint64
	PreviousIndexKnown bool
	AuthenticatedIndex *uint64 `json:",omitempty"`
	CiphertextSHA256   string
	Bytes              int
	Error              string
}

SRTPFailureSample identifies ciphertext without exposing its payload. At uses the same origin as moves. AuthenticatedIndex is populated only when a fresh context verifies an auth failure using an adjacent ROC (rollover counter). This probe does not deliver the packet or update the shadow receiver.

type SentTrack

type SentTrack struct {
	SSRC uint32
	// Frames counts the frames sent: Opus packets for audio (one frame per
	// packet), VP8 frames for video. Keyframes counts VP8 keyframes.
	Frames    int
	Keyframes int
	// KeyframeRequests counts the PLI and FIR packets the caller received
	// for this track's SSRC.
	KeyframeRequests int
}

SentTrack describes a track the caller sent.

type StateChange

type StateChange struct {
	At    time.Duration
	State string
}

StateChange is one connection state transition.

type TimeInterval

type TimeInterval struct {
	Earliest time.Time `json:"earliest"`
	Latest   time.Time `json:"latest"`
}

TimeInterval is a server event mapped onto the caller timeline.

type TrackReport

type TrackReport struct {
	MediaGaps         []MediaGap
	MediaGapsRecorded bool
	MediaGapOverflow  int
	Kind              string
	SSRC              uint32
	PayloadType       uint8

	// Packets counts all packets the caller decrypted on this track.
	// Arrivals holds the rolling history named by Report.Recording.
	Packets      int
	Arrivals     []time.Duration
	FirstArrival time.Duration
	LastArrival  time.Duration

	// MediaGap is the longest interval between consecutive successfully
	// decrypted packets on the track; MediaGapEndedAt is when it ended.
	MediaGap        time.Duration
	MediaGapEndedAt time.Duration

	// SequenceDiscontinuities counts packets whose sequence number does not
	// follow the previous packet's.
	SequenceDiscontinuities int

	// DuplicatePackets and OutOfOrderPackets distinguish stale sender traffic
	// from forward sequence gaps caused by lost packets.
	DuplicatePackets  int
	OutOfOrderPackets int
	// UnmatchedPayloads counts audio packets whose payload is not one the
	// caller sent, i.e. packets that are not an echo of the caller's media.
	// Video is matched frame by frame instead (VideoReport.UnmatchedFrames).
	UnmatchedPayloads int

	// Video describes the frames of a video track; nil for audio.
	Video *VideoReport
}

TrackReport describes a track the caller received.

type VideoRecovery

type VideoRecovery struct {
	// SourceVideoSkippedSequenceNumbers measures the old-live to new-live
	// sequence gap excluding inserted replay packets. The first replay index
	// uses a stale snapshot, so its observed jump can be smaller than the
	// margin; the resumed source stream must still carry the full margin.
	SourceVideoSkippedSequenceNumbers int
	ReplayPackets                     int

	// CachedPictureIDs includes even incomplete replayed frames, identified
	// from received RTP payload descriptors after media resumed. This proves
	// an interrupted keyframe's packets were never replayed.
	CachedPictureIDs []uint16
	MediaResumedAt   time.Duration
	// Both common-start metrics begin at the kill, for every mode. Cached
	// output can precede control-plane completion, so completion is unsuitable.
	FirstDecodedAfterKill time.Duration
	// FirstDecodedRepeated says the first returned decodable picture reuses
	// a prior decode. Its cache provenance is known, but no new decode ran.
	FirstDecodedRepeated bool
	// FirstDecodedLiveAfterKill measures a decoded frame sent at/after
	// MediaResumedAt, and is zero when no such live frame decoded.
	FirstDecodedLiveAfterKill time.Duration
	LivePath                  string
	// FirstDecodedPLIAfterKill measures the first newly decoded response to a
	// keyframe request received by the caller at/after the kill. It is zero
	// without that evidence, even when a natural keyframe restores video first.
	FirstDecodedPLIAfterKill     time.Duration
	KeyframeInterval             time.Duration
	FirstDecodedAfterMediaResume time.Duration
	FrameInterval                time.Duration
	WithinFrameInterval          bool
	Path                         string // "Cache", "Keyframe", "Live", or empty if no decoded frame
	PictureID                    uint16
	FrameSentAt                  time.Duration
}

VideoRecovery attributes a decoded frame using the VP8 PictureID and exact encoded payload observed at the caller, never worker-internal replay flags. Source identities include the send epoch; recent payload matching expires.

type VideoReport

type VideoReport struct {
	OnlineDecode OnlineDecodeReport
	// Frames counts complete frames (every packet, in order, ending with the
	// marker bit). IncompleteFrames counts frames that lost packets.
	Frames           int
	Keyframes        int
	IncompleteFrames int
	// RepeatedFrames counts exact pictures already decoded. They remain
	// complete receipts, but are excluded from decoder inputs and samples.
	RepeatedFrames int
	// FrameGaps counts complete frames that did not directly follow the
	// previous complete frame: frames were lost or reordered between them.
	FrameGaps int
	// NonMonotonicTimestamps counts complete frames whose RTP timestamp did
	// not advance from the previous complete frame (modulo the RTP clock).
	NonMonotonicTimestamps int
	// UnmatchedFrames counts frames that are not byte for byte a frame the
	// caller sent.
	UnmatchedFrames int

	// KeyframesDecoded counts keyframes decoded in pure Go;
	// KeyframeDecodeErrors counts keyframes that failed, the last with
	// LastDecodeError.
	KeyframesDecoded     int
	KeyframeDecodeErrors int
	LastDecodeError      string

	// DecodableFrames counts unique frames that decode: decoded keyframes, and
	// interframes in an unbroken run of complete frames since one.
	// FramesBeforeFirstKeyframe counts frames that arrived before any
	// keyframe decoded (a receiver waits for a keyframe). UndecodableFrames
	// counts later frames whose references were lost.
	DecodableFrames           int
	FramesBeforeFirstKeyframe int
	UndecodableFrames         int

	// FirstDecodedFrameAt is when the first frame decoded; zero if none did.
	// Width and Height are that keyframe's size.
	FirstDecodedFrameAt time.Duration
	Width, Height       int

	// FullDecode is ffmpeg's complete short-call check. Long calls select
	// OnlineDecode; offline history is capped and a truncated check is skipped.
	FullDecode FullDecode
}

VideoReport is what the caller could do with a received VP8 track: the frames it reassembled and whether they decode. See video.go for the method.

type WholeCallDelivery

type WholeCallDelivery struct {
	Audio, Video ContentDelivery
}

WholeCallDelivery survives rolling history as running counts, so a long call reports every unit without retaining every unit.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
⌂ 换网址