TripleG3.P2P 1.1.17

dotnet add package TripleG3.P2P --version 1.1.17
                    
NuGet\Install-Package TripleG3.P2P -Version 1.1.17
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TripleG3.P2P" Version="1.1.17" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TripleG3.P2P" Version="1.1.17" />
                    
Directory.Packages.props
<PackageReference Include="TripleG3.P2P" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TripleG3.P2P --version 1.1.17
                    
#r "nuget: TripleG3.P2P, 1.1.17"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TripleG3.P2P@1.1.17
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TripleG3.P2P&version=1.1.17
                    
Install as a Cake Addin
#tool nuget:?package=TripleG3.P2P&version=1.1.17
                    
Install as a Cake Tool

TripleG3.P2P

High-performance, attribute-driven peer-to-peer messaging for .NET 9 / MAUI apps over UDP (extensible to future TCP / other transports). Ship strongly-typed messages (records / classes / primitives / strings) with a tiny 8‑byte header + pluggable serialization strategy.

Status: UDP transport + two serializers (Delimited / None and JsonRaw) implemented. Designed so additional transports (TCP, etc.) and serializers can slot in without breaking user code.


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: net9.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 (Delimited + JSON) 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
  • FTP transport placeholder (factory method exists; not implemented yet)

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
FTP N/A (file transfer layer) N/A Planned (large payload offload) Planned

Use UDP when you need lowest overhead and can tolerate loss; use TCP when you need reliability / ordering without building it yourself.


Installation

Install from NuGet:

dotnet add package TripleG3.P2P

Symbols are published; enable source stepping to debug internals.

Versioning & CI

CI rewrites the patch component using the GitHub Actions run number. Base <Version> in the csproj should be updated only for major/minor increments (e.g. 1.1.0 / 2.0.0). Published versions become Major.Minor.RunNumber and the workflow commits the updated version back without re-triggering.


Target Framework

net9.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; FTP planned). Your code remains identical besides construction via the factory.

ProtocolConfiguration

public sealed class ProtocolConfiguration {
    IPEndPoint RemoteEndPoint { get; init; }
    IReadOnlyCollection<IPEndPoint> BroadcastEndPoints { get; init; }
    int        LocalPort      { 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 serializers and UDP bus
var bus = services.BuildServiceProvider().GetRequiredService<ISerialBus>();

Need TCP as well? Use SerialBusFactory.CreateTcp() (a combined DI extension can be added later).

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

Add more by implementing IMessageSerializer.

Attributes

  • [UdpMessage] or [UdpMessage("CustomName")] gives the logical protocol name (stable across assemblies)
  • [UdpMessage<T>] generic variant uses typeof(T).Name (or supplied override) for convenience
  • [Udp(order)] marks & orders properties participating in delimiter serialization
    • Unannotated properties are ignored by the None serializer

MessageType

Currently: Data (extensible placeholder for control, ack, etc.)


Wire Format (UDP)

Header (8 bytes total):

  1. Bytes 0-3: Int32 PayloadLength (bytes after header)
  2. Bytes 4-5: Int16 MessageType
  3. Bytes 6-7: Int16 SerializationProtocol

Payload:

  • If SerializationProtocol.None: TypeName + optional @-@ + serialized property segments (each delimited by @-@)
  • If JsonRaw: UTF-8 JSON of the Envelope<T>

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

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)

  1. Add [UdpMessage] (optional if CLR name is acceptable) to each root message type.
  2. Annotate properties you want serialized with [Udp(order)] (1-based ordering recommended).
  3. Use only deterministic, immutable shapes (records ideal).
  4. Nested complex types must also follow the same attribute pattern.
  5. 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 (numeric, enum, Guid, DateTime, DateTimeOffset, decimal, string) are converted with ToString() / parsed at receive time.


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:

  1. Extend SerialBusFactory with a helper that injects your serializer.
  2. Or (DI) register it as another IMessageSerializer; the bus chooses by Protocol enum 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

  • Receive loop swallows unexpected exceptions to keep the socket alive (add logging hook where catch { } blocks exist if needed)
  • Malformed messages are skipped
  • Individual subscriber exceptions do not block other handlers

Performance Notes

  • Header parsing uses BinaryPrimitives on 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 TypeName string extracted first

Extending To Other Transports

Transport abstraction lives behind ISerialBus.

TCP (Implemented)

Use SerialBusFactory.CreateTcp() and the same ProtocolConfiguration (ports now mean: LocalPort for listener; RemoteEndPoint / BroadcastEndPoints for outgoing connections). Broadcast fan-out writes the framed message to each active TCP stream (inbound accepted + established outbound). Ordering is preserved per-connection (TCP guarantee) but different peers may drift due to scheduling.

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"));

FTP (Planned)

SerialBusFactory.CreateFtp() is a stub today (throws). The intended design is a control channel for small messages and opportunistic file uploads for large payloads.

try { var ftp = SerialBusFactory.CreateFtp(); }
catch (NotImplementedException) { /* expected for now */ }

Planned outline:

  1. Lightweight command channel (possibly TCP) implementing ISerialBus semantics for control messages.
  2. Escalation to file transfer for large payloads (chunked + resume).
  3. Optional integrity (hash) verification & parallel segment streaming.

Samples

See sandbox/ClientA and sandbox/ClientB for dual-process demonstration using both protocols simultaneously (Delimited + JSON) over loopback with independent ports.

Integration tests (MultiBroadcastTests, TcpIntegrationTests) show:

  • UDP multi-endpoint broadcast
  • TCP fan-out & ordering guarantees
  • Concurrent full-mesh delivery
  • Duplicate endpoint de-duplication

Roadmap

  • Additional transports: TCP first, then optional secure channel wrapper
  • Binary packed serializer (struct layout aware)
  • 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

MIT (add LICENSE file if not already present).


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 ArrayPoolFrame inside EncodedAccessUnit (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 packet buffers (rented but not yet recycled by caller; future API may expose explicit return or custom pool)
  • 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 SPS/PPS out‑of‑band management (assumes stream already decodable at decoder)
  • No adaptive bitrate / congestion control (REMB/TCC/GCC)
  • Reorder buffer is simplistic (forward‑only)
  • 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);
}

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

  1. Implement ISerialBus (mirror UDP/TCP structure).
  2. Accept an IEnumerable<IMessageSerializer>.
  3. Preserve the 8‑byte header (or version it explicitly).
  4. Provide a SerialBusFactory.CreateX() helper.
  5. Add integration tests: start, send, broadcast, mixed serializers.
  6. 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 Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.1.17 142 9/3/2025
1.1.16 124 9/1/2025
1.1.15 136 9/1/2025
1.0.14 123 9/1/2025
1.0.12 127 9/1/2025
1.0.11 128 8/31/2025
1.0.10 139 8/30/2025
1.0.9 139 8/30/2025
1.0.8 182 8/30/2025
1.0.7 168 8/30/2025