TripleG3.P2P
1.1.17
dotnet add package TripleG3.P2P --version 1.1.17
NuGet\Install-Package TripleG3.P2P -Version 1.1.17
<PackageReference Include="TripleG3.P2P" Version="1.1.17" />
<PackageVersion Include="TripleG3.P2P" Version="1.1.17" />
<PackageReference Include="TripleG3.P2P" />
paket add TripleG3.P2P --version 1.1.17
#r "nuget: TripleG3.P2P, 1.1.17"
#:package TripleG3.P2P@1.1.17
#addin nuget:?package=TripleG3.P2P&version=1.1.17
#tool nuget:?package=TripleG3.P2P&version=1.1.17
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
andJsonRaw
) 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 usestypeof(T).Name
(or supplied override) for convenience[Udp(order)]
marks & orders properties participating in delimiter serialization- Unannotated properties are ignored by the
None
serializer
- Unannotated properties are ignored by the
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>
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)
- 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 (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:
- Extend
SerialBusFactory
with a helper that injects your serializer. - Or (DI) register it as another
IMessageSerializer
; the bus chooses byProtocol
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:
- Lightweight command channel (possibly TCP) implementing
ISerialBus
semantics for control messages. - Escalation to file transfer for large payloads (chunked + resume).
- 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
insideEncodedAccessUnit
(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
- 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 | 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. |
-
net9.0
- Microsoft.Extensions.DependencyInjection (>= 9.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.0)
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.