TripleG3.P2P
1.1.7
See the version list below for details.
dotnet add package TripleG3.P2P --version 1.1.7
NuGet\Install-Package TripleG3.P2P -Version 1.1.7
<PackageReference Include="TripleG3.P2P" Version="1.1.7" />
<PackageVersion Include="TripleG3.P2P" Version="1.1.7" />
<PackageReference Include="TripleG3.P2P" />
paket add TripleG3.P2P --version 1.1.7
#r "nuget: TripleG3.P2P, 1.1.7"
#:package TripleG3.P2P@1.1.7
#addin nuget:?package=TripleG3.P2P&version=1.1.7
#tool nuget:?package=TripleG3.P2P&version=1.1.7
TripleG3.P2P
Alice-facing transport APIs
TripleG3.P2P exposes transport-only primitives that a host such as Alice can map into its own
authorization, workflow, and UI state services:
TripleG3.P2P.FileTransfer.IFileTransferClientprovides explicit-consent, SHA-256-verified TCP file transfer. A receiver rejects offers by default if it has noTransferRequestedhandler.TripleG3.P2P.Core.ISubscriptionSerialBus.Subscribe<T>returns a disposable subscription. All registrations are cleared when the bus connection closes or the bus is disposed.TripleG3.P2P.Video.RtpVideoSenderandRtpVideoReceiverprovide H.264 Annex-B/RTP transport.TripleG3.P2P.Audio.RtpAudioSenderandRtpAudioReceiverprovide 48 kHz mono, 20 ms Opus/RTP transport with bounded receiver delivery.IPeerAuthorizeris host-supplied and is evaluated before inbound serial, file-transfer, and audio media payloads are processed. Use an approved session/device allowlist; endpoint identity alone is not sufficient.- File and audio transports expose
P2PDiagnosticEventArgsfor lifecycle, authorization, and transfer progress state projection.
Production callers must supply an authenticated encryption/channel implementation before accepting
untrusted user traffic. NoOpCipher and test ciphers are not appropriate for deployed traffic.
High-performance, attribute-driven peer-to-peer messaging for .NET 10 / MAUI apps over UDP and TCP. Ship strongly-typed messages (records / classes / primitives / strings) with a tiny 8-byte header and pluggable serialization strategy.
Status: UDP and TCP transports plus three serializers (
None,JsonRaw, andLengthPrefixed) are implemented. Direct peer-to-peer TCP file transfer is available. RTP/H.264 video remains experimental.
Why?
Typical networking layers force you to hand-roll framing, routing, and serialization. TripleG3.P2P gives you:
- A single minimal interface:
ISerialBus(send, subscribe, start, close) - Deterministic wire contract via
[Udp]&[UdpMessage]attributes (order + protocol name stability) - Envelope-based dispatch that is assembly agnostic (type names / attribute names, not CLR identity)
- Choice between ultra-light delimiter serialization or raw JSON
- Safe, isolated subscriptions (late subscribers don't crash the loop)
- Zero allocations for header parsing (Span/Memory friendly design internally)
- NEW: Multi-endpoint broadcast fan-out with duplicate endpoint suppression
Features At A Glance
- Target framework:
net10.0(single TFM here; platform heads handled by MAUI host project) - 8‑byte header layout (Length / MessageType / SerializationProtocol)
- Attribute ordered property serialization with stable delimiter
@-@ - Automatic Envelope wrapping so handlers receive strong types directly
- Multiple simultaneous protocol instances via separate buses
- Multi-endpoint broadcast (one
SendAsync→ N peers) - Plug-in serializer model (
IMessageSerializer) - Graceful cancellation / disposal
- TCP transport (reliable stream) alongside UDP; same API
- Direct peer-to-peer TCP file transfer with receiver consent, cancellation, progress, and SHA-256 verification
- Separate stdio MCP server for AI-assisted configuration, code generation, documentation, and troubleshooting
Transport Summary
| Transport | Reliability | Ordering | Broadcast Fan-Out | Status |
|---|---|---|---|---|
| UDP | Best effort / loss possible | Not guaranteed across datagrams | Yes (multi-endpoint send) | Implemented |
| TCP | Reliable | Preserved per connection | Yes (writes to each stream) | Implemented |
Use UDP when you need lowest overhead and can tolerate loss; use TCP when you need reliability / ordering without building it yourself. Neither transport provides authentication or NAT traversal; applications must provide peer authorization and network connectivity.
Peer-to-peer file transfer
File transfer is intentionally separate from ISerialBus. Create a PeerFileTransferClient for each peer, start its listener, and handle incoming requests explicitly. A receiver can reject a request or accept it with a destination path; cancellation tokens cancel active sender or receiver operations. Transfers use a dedicated versioned TCP protocol, stream in bounded chunks, write to a temporary .part file, and verify SHA-256 before moving the completed file into place.
using TripleG3.P2P.FileTransfer;
var receiver = new PeerFileTransferClient(new FileTransferOptions {
LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 9100)
});
receiver.TransferRequested += (request, cancellationToken) =>
new ValueTask<FileTransferDecision>(
FileTransferDecision.Accept(Path.Combine("received", request.FileName)));
await receiver.StartAsync();
var sender = new PeerFileTransferClient(new FileTransferOptions {
LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 9101)
});
await sender.StartAsync();
var results = await sender.SendAsync(
"video.mp4",
[new IPEndPoint(IPAddress.Loopback, 9100)],
cancellationToken: cancellationToken);
If no TransferRequested handler is registered, inbound transfers are rejected. The receiver may reject a request from the handler, and the sender may cancel through its cancellation token. Configure MaximumFileBytes, BufferSize, and MaximumConcurrentTransfers for the host.
Installation
Install the core library from NuGet:
dotnet add package TripleG3.P2P
Symbols are published; enable source stepping to debug internals.
The MCP server is a separate executable and is not included in the NuGet package. Run it locally from the repository with:
dotnet run --project src/TripleG3.P2P.McpServer/TripleG3.P2P.McpServer.csproj --no-launch-profile
VS Code integration is configured in .vscode/mcp.json.
Versioning & CI
The release workflow derives Major.Minor.RunNumber from the project version and publishes the package when a push reaches main. Set the repository NUGET_API_KEY secret before enabling publishing. Local packages can be created with dotnet pack src/TripleG3.P2P/TripleG3.P2P.csproj -c Release.
Target Framework
net10.0
MAUI/platform variants are produced in a sibling project; this core library stays lean.
Core Concepts
ISerialBus
public interface ISerialBus {
bool IsListening { get; }
ValueTask StartListeningAsync(ProtocolConfiguration config, CancellationToken ct = default);
ValueTask CloseConnectionAsync();
void SubscribeTo<T>(Action<T> handler);
ValueTask SendAsync<T>(T message, MessageType messageType = MessageType.Data, CancellationToken ct = default);
}
Abstracts the transport (currently UDP + TCP). Your code remains identical besides construction via the factory.
Both built-in buses also implement ISubscriptionSerialBus, whose Subscribe<T> method returns an IDisposable unsubscription registration.
ProtocolConfiguration
public sealed class ProtocolConfiguration {
IPAddress LocalAddress { get; init; }
IPEndPoint RemoteEndPoint { get; init; }
IReadOnlyCollection<IPEndPoint> BroadcastEndPoints { get; init; }
int LocalPort { get; init; }
int MaxPayloadBytes { get; init; }
int MaxInboundConnections { get; init; }
SerializationProtocol SerializationProtocol { get; init; }
}
Controls binding + outbound destination and the serialization protocol used for every message on this bus instance.
Broadcasting / Fan-Out
Provide one or more BroadcastEndPoints to automatically fan out every SendAsync from a bus instance to: RemoteEndPoint ∪ BroadcastEndPoints (set semantics). Duplicate endpoints (same address:port) are suppressed.
Basic one-to-many:
await bus.StartListeningAsync(new ProtocolConfiguration {
LocalPort = 7000,
RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 7001), // primary
BroadcastEndPoints = new [] {
new IPEndPoint(IPAddress.Loopback, 7002),
new IPEndPoint(IPAddress.Loopback, 7003)
},
SerializationProtocol = SerializationProtocol.None
});
await bus.SendAsync(new Chat("me", "hi everyone")); // reaches 7001, 7002, 7003
Hub & Spokes (hub at 8000 → spokes 8001/8002, spokes reply only to hub):
// Hub
await hub.StartListeningAsync(new ProtocolConfiguration {
LocalPort = 8000,
RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 8001),
BroadcastEndPoints = new [] { new IPEndPoint(IPAddress.Loopback, 8002) },
SerializationProtocol = SerializationProtocol.JsonRaw
});
// Spoke 1
await s1.StartListeningAsync(new ProtocolConfiguration {
LocalPort = 8001,
RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 8000),
SerializationProtocol = SerializationProtocol.JsonRaw
});
// Spoke 2 (similar to s1; LocalPort = 8002)
await hub.SendAsync(new BroadcastAnnouncement("server", "hello spokes"));
Full Mesh (N peers each send to all others): create N buses where for each index i choose one peer as RemoteEndPoint and all remaining as BroadcastEndPoints. (See integration test Concurrent_Broadcasts_All_Messages_Delivered_Exactly_Once).
Duplicate endpoint suppression:
await bus.StartListeningAsync(new ProtocolConfiguration {
LocalPort = 8100,
RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 8101),
BroadcastEndPoints = new [] { // 8102 duplicated
new IPEndPoint(IPAddress.Loopback, 8102),
new IPEndPoint(IPAddress.Loopback, 8102)
}
});
// Only one datagram sent to 8102.
Mixed types broadcast (string + complex):
await sender.SendAsync("alpha");
await sender.SendAsync(new Person("carol", 27, new Address("2 Ave", "Metro")));
Concurrency: safe to invoke SendAsync concurrently; the underlying UDP socket will serialize sends. Out-of-order arrival is still possible (UDP). If ordering matters, include sequence numbers inside your messages.
Reliability Disclaimer: UDP does not guarantee delivery / ordering. The library only guarantees attempted one-shot fan-out; implement retries / ACK at the message layer if required.
Dependency Injection
Register UDP components:
services.AddP2PUdp(); // Registers None + JsonRaw + LengthPrefixed serializers and UDP bus
var bus = services.BuildServiceProvider().GetRequiredService<ISerialBus>();
Need TCP as well? Use SerialBusFactory.CreateTcp(). A combined TCP DI extension is not currently provided.
Envelope (Generic)
Internal transport wrapper: TypeName + Message. The receiver inspects TypeName to look up subscriptions, then materializes only the requested type.
SerializationProtocol
None // Attribute-delimited (fast, compact)
JsonRaw // System.Text.Json UTF-8 payload
LengthPrefixed // Versioned attribute contract with lengths and explicit null markers
Add more by implementing IMessageSerializer.
Use LengthPrefixed for new attribute-based contracts. None remains available for wire compatibility, but delimiter text inside values, null versus empty strings, and some nested shapes are inherently ambiguous.
Attributes
[UdpMessage]or[UdpMessage("CustomName")]gives the logical protocol name (stable across assemblies)[UdpMessage<T>]generic variant usestypeof(T).Name(or supplied override) for convenience[Udp(order)]marks and orders properties participating in attribute serialization.- Unannotated properties are ignored by
NoneandLengthPrefixed. - Constructor parameters are matched to annotated properties by name and type.
MessageType
Currently: Data (extensible placeholder for control, ack, etc.)
Wire Format (UDP)
Header (8 bytes total):
- Bytes 0-3: Int32 PayloadLength (bytes after header)
- Bytes 4-5: Int16 MessageType
- Bytes 6-7: Int16 SerializationProtocol
Payload:
- If
SerializationProtocol.None:TypeName+ optional@-@+ serialized property segments (each delimited by@-@) - If
JsonRaw: UTF-8 JSON of theEnvelope<T> - If
LengthPrefixed: format version + length-prefixed UTF-8 type name + recursively framed ordered properties
Quick Start
using TripleG3.P2P.Attributes;
using TripleG3.P2P.Core;
using System.Net;
[UdpMessage("Person")] // Protocol type name
public record Person([property: Udp(1)] string Name,
[property: Udp(2)] int Age,
[property: Udp(3)] Address Address);
[UdpMessage<Address>] // Uses nameof(Address) unless overridden
public record Address([property: Udp(1)] string Street,
[property: Udp(2)] string City,
[property: Udp(3)] string State,
[property: Udp(4)] string Zip);
var bus = SerialBusFactory.CreateUdp();
await bus.StartListeningAsync(new ProtocolConfiguration {
LocalPort = 7000,
RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 7001),
SerializationProtocol = SerializationProtocol.None
});
bus.SubscribeTo<Person>(p => Console.WriteLine($"Person: {p.Name} ({p.Age}) {p.Address.City}"));
await bus.SendAsync(new Person("Alice", 28, new Address("1 Way", "Town", "ST", "00001")));
Run a second process with reversed ports (7001 ↔ 7000) to complete the loop.
TCP Quick Start (Reliable)
var tcpBus = SerialBusFactory.CreateTcp();
await tcpBus.StartListeningAsync(new ProtocolConfiguration {
LocalPort = 9000,
RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 9001),
SerializationProtocol = SerializationProtocol.JsonRaw
});
tcpBus.SubscribeTo<Person>(p => Console.WriteLine($"[TCP] {p.Name} ({p.Age})"));
await tcpBus.SendAsync(new Person("Alice", 28, new Address("1 Way", "Town", "ST", "00001")));
Multi-Protocol Usage (Same Message Types)
var udp = SerialBusFactory.CreateUdp();
var tcp = SerialBusFactory.CreateTcp();
await udp.StartListeningAsync(new ProtocolConfiguration { LocalPort = 7000, RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 7001), SerializationProtocol = SerializationProtocol.None });
await tcp.StartListeningAsync(new ProtocolConfiguration { LocalPort = 9000, RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 9001), SerializationProtocol = SerializationProtocol.JsonRaw });
udp.SubscribeTo<Chat>(c => Console.WriteLine($"[UDP] {c.User}: {c.Text}"));
tcp.SubscribeTo<Chat>(c => Console.WriteLine($"[TCP] {c.User}: {c.Text}"));
await udp.SendAsync(new Chat("me","low-latency"));
await tcp.SendAsync(new Chat("me","reliable"));
Using JSON Instead
var jsonBus = SerialBusFactory.CreateUdp();
await jsonBus.StartListeningAsync(new ProtocolConfiguration {
LocalPort = 7002,
RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 7003),
SerializationProtocol = SerializationProtocol.JsonRaw
});
JSON serializer ignores [Udp] ordering—standard JSON rules apply; TypeName embedded as typeName/TypeName.
Subscriptions
bus.SubscribeTo<string>(s => Console.WriteLine($"Raw string: {s}"));
bus.SubscribeTo<Person>(HandlePerson);
void HandlePerson(Person p) { /*...*/ }
- Multiple handlers per type allowed
- Subscription key is the protocol type name (attribute override or CLR name)
- If no handler matches, message is silently ignored
- Cast to
ISubscriptionSerialBusand useSubscribe<T>when the handler needs an independent lifetime.
Sending
await bus.SendAsync("Hello peer");
await bus.SendAsync(new Person("Bob", 42, new Address("2 Road", "City", "ST", "22222")));
All messages on a bus instance use that instance’s SerializationProtocol.
Graceful Shutdown
await bus.CloseConnectionAsync();
// or dispose
(bus as IDisposable)?.Dispose();
Cancels the receive loop & disposes socket.
Designing Message Contracts (Delimited Serializer)
- Add
[UdpMessage](optional if CLR name is acceptable) to each root message type. - Annotate properties you want serialized with
[Udp(order)](1-based ordering recommended). - Use only deterministic, immutable shapes (records ideal).
- Nested complex types must also follow the same attribute pattern.
- Changing order or adding/removing annotated properties is a protocol breaking change.
Example
[UdpMessage("Ping")] public record Ping([property: Udp(1)] long Ticks);
Primitive & String Support
Primitive-like types use invariant round-trip formatting and explicit converters, including DateTimeOffset.
Implementing a Custom Serializer
class MyBinarySerializer : IMessageSerializer {
public SerializationProtocol Protocol => (SerializationProtocol)42; // Add new enum value first
public byte[] Serialize<T>(T value) { /* return bytes */ }
public T? Deserialize<T>(ReadOnlySpan<byte> data) { /* parse */ }
public object? Deserialize(Type t, ReadOnlySpan<byte> data) { /* parse */ }
}
Registration options:
- Extend
SerialBusFactorywith a helper that injects your serializer. - Or (DI) register it as another
IMessageSerializer; the bus chooses byProtocolenum value.
Best practices:
- Keep format deterministic & version tolerant.
- Reuse buffers; avoid per-message large allocations.
- Reserve new enum value before shipping (ensure both sides understand it).
Error Handling & Resilience
- Malformed, truncated, oversized, or unknown-protocol frames are rejected before deserialization or large allocation.
- Cancellation is propagated to callers.
- A send throws when every configured endpoint fails; partial fan-out failures are logged.
- Individual subscriber exceptions are logged and do not block other handlers.
- TCP serializes writes per configured connection and removes failed connections so a later send can reconnect.
Performance Notes
- Header parsing uses
BinaryPrimitiveson a single span - Delimited serializer caches reflection lookups per type
- No dynamic allocations for header path; serialization aims to minimize intermediate copies
- Envelope design avoids repeated type discovery; only
TypeNamestring extracted first
Extending To Other Transports
Transport abstraction lives behind ISerialBus.
TCP (Implemented)
Use SerialBusFactory.CreateTcp() and the same ProtocolConfiguration (LocalPort is the listener; RemoteEndPoint and BroadcastEndPoints are configured send targets). Accepted sockets are receive-only sessions and are never added to fan-out. Ordering is preserved per configured connection, while different peers may progress independently.
Example:
var tcpBus = SerialBusFactory.CreateTcp();
await tcpBus.StartListeningAsync(new ProtocolConfiguration {
LocalPort = 9000,
RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 9001),
BroadcastEndPoints = new [] { new IPEndPoint(IPAddress.Loopback, 9002) },
SerializationProtocol = SerializationProtocol.JsonRaw
});
tcpBus.SubscribeTo<Chat>(c => Console.WriteLine($"[TCP] {c.User}: {c.Text}"));
await tcpBus.SendAsync(new Chat("alice", "over tcp"));
Examples and Tests
Tests are split by execution contract:
TripleG3.P2P.UnitTestscontains deterministic, in-process serializer, packetizer, depacketizer, cipher, sequence, bounds, and validation tests.TripleG3.P2P.IntegrationTestscontains loopback sockets, multi-peer fan-out, reconnect, malformed network input, receiver lifecycle, DI-over-UDP video, RTCP timing, and negotiation flows.
The pull-request and publish pipelines currently restore, build, and run only the unit-test project. Integration tests bind local ports and use timing-sensitive multi-component flows, so run them locally before release validation.
# Pipeline-equivalent unit tests
dotnet test tests/TripleG3.P2P.UnitTests/TripleG3.P2P.UnitTests.csproj -c Release -warnaserror
# Manual integration tests
dotnet test tests/TripleG3.P2P.IntegrationTests/TripleG3.P2P.IntegrationTests.csproj -c Release -warnaserror
# Complete local validation
dotnet test TripleG3.P2P.slnx -c Release -warnaserror
The solution also contains the executable MCP server project. It is built by the solution but is not packed into TripleG3.P2P.
The integration suite (MultiBroadcastTests, TcpIntegrationTests, TransportHardeningTests, and the video integration fixtures) proves:
- UDP multi-endpoint broadcast
- TCP fan-out & ordering guarantees
- Concurrent full-mesh delivery
- Duplicate endpoint de-duplication
- TCP reconnect and concurrent frame integrity
- Malformed input recovery, cancellation, and disposable subscriptions
- All three serialization protocols
- RTP video DI transfer over a real UDP socket
- Receiver start/stop/restart and disposal races
- RTCP timing and negotiation/keyframe signaling
Roadmap
- Authentication and authorization for messaging and file-transfer peers
- Resumable file transfers and receiver-selected progress reporting
- Optional authenticated secure-channel wrapper
- Source generator for zero-reflection fast path
- Optional compression & encryption layers
- Health / metrics callbacks
FAQ
Q: Do both peers need the exact same CLR types?
A: They need matching protocol type names and compatible property ordering (Delimited) or matching JSON contracts (JsonRaw). CLR assembly identity is not required.
Q: Can I mix serializers on the same socket?
A: One ISerialBus instance uses one SerializationProtocol. Create multiple instances for mixed protocols.
Q: Is ordering enforced?
A: Receiver trusts the order defined by [Udp(n)]. Reordering is a breaking change.
Minimal Cheat Sheet
var bus = SerialBusFactory.CreateUdp();
await bus.StartListeningAsync(new ProtocolConfiguration {
LocalPort = 7000,
RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 7001),
SerializationProtocol = SerializationProtocol.None
});
[UdpMessage("Chat")]
public record Chat([property: Udp(1)] string User, [property: Udp(2)] string Text);
bus.SubscribeTo<Chat>(c => Console.WriteLine($"{c.User}: {c.Text}"));
await bus.SendAsync(new Chat("me", "hi there"));
License
GPL-3.0-only. See LICENSE.
Contributing
Issues & PRs welcome: add tests / samples for new serializers or transports.
Happy messaging!
Memory Pooling (Partially Implemented)
The video pipeline now uses pooled buffers for:
- FU‑A (fragmented) NAL reassembly (rents a buffer, grows, returns after NAL complete)
- Consolidated frame (Annex B access unit) assembly via an
ArrayPoolFrameinsideEncodedAccessUnit(returned when disposed) - Intermediate single‑NAL copies (rented arrays tracked and released after frame consolidation)
Why: Reduce allocation pressure & GC pauses under sustained 30/60fps streaming. Hundreds or even thousands of frame buffers per minute are efficiently recycled.
Still Allocating:
- Outbound RTP packets use exact-length managed arrays so callback consumers may retain them safely.
- Control / negotiation JSON payloads
Guidance:
Dispose each EncodedAccessUnit you create/receive (tests show using var au = ...). If you skip disposal, large buffers remain rented and the pool cannot reclaim them quickly.
Planned refinements: size threshold to bypass pooling for very small frames, packet buffer recycling, allocation metrics counters.
Experimental: Real-Time Video (H.264 over RTP)
Early, evolving support for sending pre‑encoded H.264 access units (Annex B) over RTP/UDP.
Current Capabilities
- H.264 packetization: single NAL + FU‑A (RFC 6184 subset)
- Depacketization & reassembly to Annex B (start codes preserved)
- Minimal RTCP: Sender Report (SR) + Receiver Report (RR) with RTT & fraction lost
- Jitter, cumulative loss, fraction lost statistics (RFC3550 style)
- Forward‑only reorder buffer (drops late retrograde packets)
- Keyframe request path (PLI analogue) via control channel
- Simple JSON negotiation (Offer/Answer + keyframe request) over injectable reliable channel
- Pluggable payload cipher abstraction (NoOp / XOR test cipher)
- Partial memory pooling & zero‑copy optimizations for frame assembly
Not Yet Implemented / Limitations
- No NACK/RTX retransmissions, FEC, or packet pacing
- No SRTP / DTLS keying (encryption is placeholder only)
- No automatic SPS/PPS extraction; callers may supply
ProfileLevelIdandSpropParameterSetsduring negotiation. - No adaptive bitrate / congestion control (REMB/TCC/GCC)
- Reordering is bounded and loss-recovering, but NACK/RTX is not implemented.
- Only H.264 (no VP8/VP9/AV1/H.265)
- No simulcast / SVC layers, no audio, no A/V sync
- No ICE/STUN/TURN NAT traversal (you must provide transport)
Essential Types
| Type | Purpose |
|---|---|
EncodedAccessUnit |
Represents one encoded frame (Annex B) + metadata; disposable (pooled buffer) |
RtpVideoSender |
High-level sender: packetizes AUs, encrypts, emits RTP datagrams & sends SR |
RtpVideoReceiver |
Consumes RTP / RTCP, reassembles frames, updates stats, raises AccessUnitReceived |
H264RtpPacketizer / H264RtpDepacketizer |
Low-level packetization building blocks |
NegotiationManager |
Offer/Answer & keyframe (PLI analogue) signaling |
IVideoPayloadCipher |
Cipher abstraction (NoOp / XOR examples) |
Basic End‑to‑End Flow
// Outbound network send hooks (wire these to your UDP socket send method)
void SendRtp(ReadOnlySpan<byte> datagram) => udpSocket.SendTo(datagram.ToArray(), remoteRtpEndPoint);
void SendRtcp(ReadOnlySpan<byte> datagram) => udpSocket.SendTo(datagram.ToArray(), remoteRtcpEndPoint);
var sender = new RtpVideoSender(
ssrc: 0x1234_5678,
mtu: 1200,
cipher: new NoOpCipher(),
datagramOut: d => SendRtp(d.Span),
rtcpOut: d => SendRtcp(d.Span));
var receiver = new RtpVideoReceiver(new NoOpCipher());
receiver.AccessUnitReceived += au => {
try { Render(au); } finally { au.Dispose(); }
};
// For each encoded frame (Annex B) you obtain from your encoder:
using var au = new EncodedAccessUnit(encodedAnnexBFrame, isKeyFrame, rtpTimestamp90k, captureTicks);
sender.Send(au);
// Periodically (every ~2s or on key events) send a Sender Report:
sender.SendSenderReport(rtpTimestamp90k);
// Incoming network data:
void OnUdpData(byte[] buffer, int len)
{
var span = new ReadOnlySpan<byte>(buffer,0,len);
if (Rtcp.IsRtcpPacket(span))
{
receiver.ProcessRtcp(span);
sender.ProcessRtcp(span); // so sender can compute RTT
}
else
{
receiver.ProcessRtp(span);
}
}
Negotiation & Keyframe Requests
var chA = new InMemoryControlChannel();
var chB = new InMemoryControlChannel();
chA.MessageReceived += m => chB.SendReliableAsync(m);
chB.MessageReceived += m => chA.SendReliableAsync(m);
var offerer = new NegotiationManager(chA);
var answerer = new NegotiationManager(chB);
answerer.AttachEncoder(yourEncoder); // so remote PLI triggers RequestKeyFrame()
await offerer.CreateOfferAsync(new VideoSessionConfig(1280,720, 2_000_000, 30));
// After negotiation completes, either side can request a keyframe:
offerer.RequestKeyFrame();
Stats & RTT
var senderStats = sender.GetStats();
// senderStats.RttEstimateMs, PacketsSent, BytesSent
var recvStats = receiver.GetStats();
// recvStats.Jitter, PacketsLost, FractionLost (last interval), PacketsReceived
Memory & Disposal
Dispose every received EncodedAccessUnit after consuming its data. It returns the underlying pooled frame buffer to ArrayPool<byte>. If you retain frames (e.g., for rewind), copy out the bytes first with au.AnnexB.ToArray().
Keyframe Logic
Receiver issues RequestKeyFrame() on NegotiationManager (or directly via application logic) when:
- Startup / first frame needed
- Decoder error / corruption detected (you decide)
The answering side invokes encoder.RequestKeyFrame() (you supply the encoder implementation) so the next access unit is a keyframe.
Roadmap Snapshot
Minimal RTP Video API (Stable Surface)
Verbatim stable signatures exposed in TripleG3.P2P.Video namespace:
public sealed class EncodedAccessUnit : IDisposable
{
public EncodedAccessUnit(ReadOnlyMemory<byte> annexB, bool isKeyFrame, uint rtpTimestamp90k, long captureTicks);
public ReadOnlyMemory<byte> AnnexB { get; }
public bool IsKeyFrame { get; }
public uint RtpTimestamp90k { get; }
public long CaptureTicks { get; }
public void Dispose();
}
public interface IVideoPayloadCipher
{
int OverheadBytes { get; }
int Encrypt(Span<byte> buffer);
int Decrypt(Span<byte> buffer);
int Encrypt(ReadOnlySpan<byte> payload, Span<byte> output);
int Decrypt(ReadOnlySpan<byte> payload, Span<byte> output);
}
public sealed class NoOpCipher : IVideoPayloadCipher { /* OverheadBytes=0; pass-through */ }
public sealed class RtpVideoSender
{
public RtpVideoSender(uint ssrc, int mtu, IVideoPayloadCipher cipher, Action<ReadOnlyMemory<byte>> datagramOut, Action<ReadOnlyMemory<byte>>? rtcpOut = null);
public void Send(EncodedAccessUnit au);
public void SendSenderReport(uint rtpTimestamp90k);
public void ProcessRtcp(ReadOnlySpan<byte> packet);
public RtpVideoSenderStats GetStats(); // optional lightweight stats
}
public sealed class RtpVideoReceiver
{
public RtpVideoReceiver(IVideoPayloadCipher cipher);
public event Action<EncodedAccessUnit> AccessUnitReceived;
public void ProcessRtp(ReadOnlySpan<byte> packet);
public void ProcessRtcp(ReadOnlySpan<byte> packet);
public RtpVideoReceiverStats GetStats();
}
public static class Rtcp
{
public static bool IsRtcpPacket(ReadOnlySpan<byte> packet);
}
These are intended for direct consumption by TripleG3.Camera.Maui without reflection.
Logging & Diagnostics
Add logging (video pipeline & new code paths use Microsoft.Extensions.Logging):
services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Information));
Security Note (Video Ciphers)
NoOpCipher & XorTestCipher are NOT secure. They exist only for testing. Use a proper SRTP / DTLS-SRTP layer for real encryption (on roadmap).
Creating a New Transport
- Implement
ISerialBus(mirror UDP/TCP structure). - Accept an
IEnumerable<IMessageSerializer>. - Preserve the 8‑byte header (or version it explicitly).
- Provide a
SerialBusFactory.CreateX()helper. - Add integration tests: start, send, broadcast, mixed serializers.
- Update README & bump minor version.
MAUI Integration
In MauiProgram.CreateMauiApp:
builder.Services.AddP2PUdp();
builder.Services.AddLogging(b => b.AddDebug());
Inject ISerialBus into pages / services. Handle disposal on shutdown for clean socket release.
- Immediate: NACK/RTX, proper RTCP PLI/FIR packets, SRTP integration
- Near-term: Bandwidth estimation (REMB/TCC) & adaptive send pacing
- Later: ICE/STUN/TURN integration hooks, multi‑codec negotiation, richer jitter buffer
APIs are experimental; expect adjustments.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 9.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.9)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on TripleG3.P2P:
| Package | Downloads |
|---|---|
|
TripleG3.Camera.Maui
Cross-platform .NET MAUI camera view with frame broadcasting, live & buffered preview, and remote feed scaffolding for Android, Windows, iOS & Mac Catalyst. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.2.21 | 113 | 8/27/2026 |
| 1.1.17 | 308 | 9/3/2025 |
| 1.1.16 | 241 | 9/1/2025 |
| 1.1.15 | 249 | 9/1/2025 |
| 1.1.8 | 99 | 8/21/2026 |
| 1.1.7 | 117 | 7/30/2026 |
| 1.1.6 | 103 | 7/27/2026 |
| 1.1.5 | 104 | 7/27/2026 |
| 1.1.4 | 111 | 7/16/2026 |
| 1.1.3 | 111 | 7/15/2026 |
| 1.1.2 | 187 | 9/26/2025 |
| 1.1.1 | 356 | 9/15/2025 |
| 1.0.14 | 236 | 9/1/2025 |
| 1.0.12 | 232 | 9/1/2025 |
| 1.0.11 | 242 | 8/31/2025 |
| 1.0.10 | 251 | 8/30/2025 |
| 1.0.9 | 250 | 8/30/2025 |
| 1.0.8 | 295 | 8/30/2025 |
| 1.0.7 | 279 | 8/30/2025 |