From 68e90a4c2d78b29a19b944353464288016212758 Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Sun, 7 Jun 2026 16:50:17 -0700 Subject: [PATCH 1/6] docs: Add ADR 0003 for logging infrastructure; add Microsoft.Extensions.Logging package reference - Document decision to use ILogger DI injection via Host.SetLoggerFactory() - Define log event structure, category hierarchy, and EventId ranges - Plan WebRTC log sink with lock-free .NET Channel threading model - Add Microsoft.Extensions.Logging v9.0.0 to WebRtcNet.Api and WebRtcNet Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- WebRtcNet.Api/WebRtcNet.Api.csproj | 27 +++---- WebRtcNet/WebRtcNet.csproj | 49 ++++++------- docs/adr/0003-logging-infrastructure.md | 95 +++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 36 deletions(-) create mode 100644 docs/adr/0003-logging-infrastructure.md diff --git a/WebRtcNet.Api/WebRtcNet.Api.csproj b/WebRtcNet.Api/WebRtcNet.Api.csproj index f6bb1ea..5c56402 100644 --- a/WebRtcNet.Api/WebRtcNet.Api.csproj +++ b/WebRtcNet.Api/WebRtcNet.Api.csproj @@ -1,13 +1,16 @@ - - - net10.0-windows;net48 - Library - ARM64;x64;x86 - WebRtcNet.Api - The public WebRtc interfaces and classes for the WebRtcNet library - 98433217-c99b-4b08-a19b-3f97a1f1e633 - $(OutputPath)$(TargetFramework)\WebRtcNet.Api.XML - latest - WebRtcNet - + + + net10.0-windows;net48 + Library + ARM64;x64;x86 + WebRtcNet.Api + The public WebRtc interfaces and classes for the WebRtcNet library + 98433217-c99b-4b08-a19b-3f97a1f1e633 + $(OutputPath)$(TargetFramework)\WebRtcNet.Api.XML + latest + WebRtcNet + + + + \ No newline at end of file diff --git a/WebRtcNet/WebRtcNet.csproj b/WebRtcNet/WebRtcNet.csproj index 0592882..3a4e7e9 100644 --- a/WebRtcNet/WebRtcNet.csproj +++ b/WebRtcNet/WebRtcNet.csproj @@ -1,24 +1,25 @@ - - - net10.0-windows;net48 - Library - ARM64;x64;x86 - WebRtcNet - A WebRtc implementation for .Net - 3CA16CE5-8124-4D86-8E37-2171B15BE276 - true - latest - - - - ..\LICENSE - True - $(OutputPath)$(TargetFramework)\WebRtcNet.XML - - - - - - - - + + + net10.0-windows;net48 + Library + ARM64;x64;x86 + WebRtcNet + A WebRtc implementation for .Net + 3CA16CE5-8124-4D86-8E37-2171B15BE276 + true + latest + + + ..\LICENSE + True + $(OutputPath)$(TargetFramework)\WebRtcNet.XML + + + + + + + + + + \ No newline at end of file diff --git a/docs/adr/0003-logging-infrastructure.md b/docs/adr/0003-logging-infrastructure.md new file mode 100644 index 0000000..535a111 --- /dev/null +++ b/docs/adr/0003-logging-infrastructure.md @@ -0,0 +1,95 @@ +# Logging infrastructure using Microsoft.Extensions.Logging + +WebRtcNet will adopt Microsoft.Extensions.Logging (MEL) as the standard for structured logging across managed and native interop code. + +## Context + +Issue #56 requires a consistent logging infrastructure to surface diagnostics and failures from both managed device enumeration and native WebRTC library operations. Without structured logging, audio/video device enumeration silently suppresses HRESULT failures (e.g., in MediaDevices.cpp:GetAudioDeviceLabel), making failures hard to diagnose. + +## Decision + +We chose **ILogger dependency injection via Host.SetLoggerFactory()** with the following characteristics: + +### Architecture +- **Injection point** — App calls Host.SetLoggerFactory(ILoggerFactory) early in startup to configure logging; no DI container required +- **Default behavior** — Debug builds default to console logger; Release builds default to NullLogger (silent) if SetLoggerFactory not called +- **Managed layer** — WebRtcNet.Api queries injected factory for loggers by category +- **Interop layer** — C++/CLI forwards WebRTC rtc::LogSink events and HRESULT failures to managed code via .NET Channel +- **Threading** — WebRTC logs arrive on arbitrary native threads; a lock-free .NET Channel with background dequeue ensures thread-safe, non-blocking forwarding to managed ILogger + +### Log Structure +All logs use: +- **Category** — Hierarchical category (e.g., "WebRTC.PeerConnection", "Interop.MediaDevices") for filtering and aggregation +- **EventId** — Numeric identifier for event type (ranges: WebRTC 1000-1999, Media 2000-2999, Interop 3000-3999) to enable telemetry grouping +- **Timestamp, Severity, ThreadId, Message** — Standard logging envelope + +### EventId Strategy +- **WebRtcLogEventId enum** — Managed code (InteropHResult failures, MediaDevices enumeration) references enum constants +- **Tag-to-category mapping (JSON)** — WebRTC rtc::LogSink logs matched by regex pattern to category + eventIdBase; all events in a category initially share the same EventId + +### WebRTC Log Sink +- **Eager initialization** — rtc::LogSink registered with WebRTC on RtcPeerConnectionFactory creation (not lazy) +- **Lock-free forwarding** — Logs pushed to .NET Channel from arbitrary WebRTC threads; background thread dequeues and writes to ILogger +- **Coverage** — Captures all WebRTC diagnostics (peer connection state, ICE, codec negotiation, audio/video device initialization, etc.) + +## Rationale + +### Why ILogger Injection vs. Alternatives? + +**OnLogging Callback** — Simple but requires app to wire handlers; threading complexity from C++ → managed; harder to capture WebRTC library logs. + +**Static/Singleton Registry** — Pragmatic but not idiomatic .NET; harder to unit test; violates DI principles. + +**Hybrid (Callback + Optional ILogger)** — Flexible but dual maintenance paths and more complex. + +We chose **ILogger Injection** because: +1. Standard .NET pattern, composable with DI containers (ASP.NET Core, generic host builder) +2. Aligns with ecosystem expectations (Serilog, NLog integrations) +3. Supports both simple and enterprise logging scenarios +4. Enables fine-grained filtering by category without app-side handler logic +5. No mandatory DI container required for this project (explicit Host.SetLoggerFactory call is minimal) + +### Why .NET Channel for Threading? + +**Synchronous direct marshaling** — Simple but risks blocking WebRTC threads or deadlock if ILogger throws. + +**std::queue + mutex** — Not truly lock-free; overkill complexity for C++. + +**moodycamel::ConcurrentQueue** — High-performance but external C++ dependency. + +We chose **.NET Channel** because: +1. Async/await, structured concurrency (built-in backpressure) +2. Integrates naturally with managed async code (background thread via Task) +3. No external dependencies +4. Straightforward C++/CLI marshaling to queue on unmanaged thread, dequeue on managed thread +5. Naturally handles channel closure on shutdown + +### Why Eager Initialization? + +WebRTC log sink started on RtcPeerConnectionFactory creation (not lazy or opt-in) to ensure comprehensive capture from first peer connection creation. Decouples logging lifecycle from early Host API calls. + +### Why Default Console Logger in Debug? + +Developer ergonomics: engineers building or debugging WebRtcNet should see diagnostics without additional setup. Production apps explicitly call Host.SetLoggerFactory and choose their logging provider. Release builds default to NullLogger (silent) to avoid overhead in production. + +## Consequences + +### Positive +- **Unified logging surface** — App configures logging once (via SetLoggerFactory), captures diagnostics from all layers +- **Zero required setup** — Debug builds provide console logs out-of-the-box; production apps opt-in +- **Web-aligned semantics** — Category hierarchy mirrors W3C WebRTC spec components (PeerConnection, DataChannel, Media, Transport, etc.) +- **Extensible EventIds** — Enum-based ranges allow future refinement (e.g., per-event-type discrimination via message pattern-matching) +- **Production-ready** — MEL is well-tested, widely adopted; integrates with Application Insights, Seq, ELK, etc. +- **No DI container required** — Single static API call (Host.SetLoggerFactory) fits this project's architecture + +### Negative +- **Manual configuration in Release** — Production apps must call Host.SetLoggerFactory(); no default +- **Background thread overhead** — Channel dequeue on background thread adds latency and thread pool work (minor at typical logging rates) +- **WebRTC thread blocking** — If ILogger is slow, Channel can back up; app responsible for fast logger implementations + +## Future Refinement + +1. **EventId discrimination** — Start with coarse (all events in category share eventIdBase); evolve to per-event-type EventIds if telemetry warrants +2. **Channel capacity** — Start unbounded; profile and cap if needed +3. **Host shutdown hook** — May need explicit channel drain on application shutdown +4. **Native WebRTC log sink configuration** — Options for enabling/disabling rtc::LogSink, filtering by severity From a1e75da849e9e1b074b0eb068ed6a573fe3beb89 Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Sun, 7 Jun 2026 17:20:25 -0700 Subject: [PATCH 2/6] feat(#56): Implement Phase 1 - managed logging infrastructure - Add WebRtcLogEventId enum with ranges (WebRTC 1000-1999, Media 2000-2999, Interop 3000-3999) - Add WebRtcLogEvent record capturing Timestamp, Severity, EventId, Category, ThreadId, Message - Add LogCategoryMapping class to parse JSON and map WebRTC tags to categories via regex - Add LogCategoryMapping.json resource with tag-to-category patterns - Add LoggerFactoryHolder with thread-safe factory holder and default console logger for Debug builds - Add Host.SetLoggerFactory() public API for apps to inject ILoggerFactory - Add Microsoft.Extensions.Logging package reference to both WebRtcNet.Api and WebRtcNet Phase 1 establishes baseline managed logging infrastructure ready for C++/CLI interop bridge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- WebRtcNet.Api/Logging/LogCategoryMapping.cs | 102 ++++++++++++++++++ WebRtcNet.Api/Logging/LoggerFactoryHolder.cs | 94 ++++++++++++++++ WebRtcNet.Api/Logging/WebRtcLogEvent.cs | 21 ++++ WebRtcNet.Api/Logging/WebRtcLogEventId.cs | 56 ++++++++++ .../Resources/LogCategoryMapping.json | 49 +++++++++ WebRtcNet.Api/WebRtcNet.Api.csproj | 3 + WebRtcNet/Host.cs | 18 +++- 7 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 WebRtcNet.Api/Logging/LogCategoryMapping.cs create mode 100644 WebRtcNet.Api/Logging/LoggerFactoryHolder.cs create mode 100644 WebRtcNet.Api/Logging/WebRtcLogEvent.cs create mode 100644 WebRtcNet.Api/Logging/WebRtcLogEventId.cs create mode 100644 WebRtcNet.Api/Resources/LogCategoryMapping.json diff --git a/WebRtcNet.Api/Logging/LogCategoryMapping.cs b/WebRtcNet.Api/Logging/LogCategoryMapping.cs new file mode 100644 index 0000000..556ff75 --- /dev/null +++ b/WebRtcNet.Api/Logging/LogCategoryMapping.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace WebRtcNet.Logging; + +/// +/// Maps WebRTC log tags to categories and EventId base values. +/// Loads mapping from JSON resource at startup. +/// +internal class LogCategoryMapping +{ + private readonly List mappings_ = []; + + /// + /// Represents a single tag-to-category mapping entry. + /// + private class CategoryMapping + { + [JsonPropertyName("tagPattern")] + public string TagPattern { get; set; } = string.Empty; + + [JsonPropertyName("category")] + public string Category { get; set; } = string.Empty; + + [JsonPropertyName("eventIdBase")] + public int EventIdBase { get; set; } + + public Regex CompiledPattern { get; set; } = null!; + } + + /// + /// Initializes the mapping from embedded JSON resource. + /// + public static LogCategoryMapping LoadFromResource() + { + var mapping = new LogCategoryMapping(); + + try + { + // Load JSON from embedded resource + var assembly = typeof(LogCategoryMapping).Assembly; + var resourceName = $"{typeof(LogCategoryMapping).Namespace}.Resources.LogCategoryMapping.json"; + + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + throw new InvalidOperationException($"Resource '{resourceName}' not found."); + + using var reader = new System.IO.StreamReader(stream); + var json = reader.ReadToEnd(); + + var root = JsonDocument.Parse(json); + var element = root.RootElement.GetProperty("logCategoryMappings"); + + foreach (var item in element.EnumerateArray()) + { + var tagPattern = item.GetProperty("tagPattern").GetString() ?? string.Empty; + var category = item.GetProperty("category").GetString() ?? string.Empty; + var eventIdBase = item.GetProperty("eventIdBase").GetInt32(); + + mapping.mappings_.Add(new CategoryMapping + { + TagPattern = tagPattern, + Category = category, + EventIdBase = eventIdBase, + CompiledPattern = new Regex(tagPattern, RegexOptions.Compiled) + }); + } + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to load log category mapping resource.", ex); + } + + return mapping; + } + + /// + /// Resolves a WebRTC tag to category and EventId base. + /// Returns ("WebRTC.Other", 1600) if no match found. + /// + public (string Category, int EventIdBase) ResolveTagToCategory(string tag) + { + Contract.Requires(!string.IsNullOrEmpty(tag), nameof(tag)); + + // Remove leading/trailing parentheses if present (tags come as "(tag_name)") + var cleanTag = tag.Trim('(', ')'); + + foreach (var mapping in mappings_) + { + if (mapping.CompiledPattern.IsMatch(cleanTag)) + return (mapping.Category, mapping.EventIdBase); + } + + // Default fallback + return ("WebRTC.Other", 1600); + } +} diff --git a/WebRtcNet.Api/Logging/LoggerFactoryHolder.cs b/WebRtcNet.Api/Logging/LoggerFactoryHolder.cs new file mode 100644 index 0000000..9fe25b6 --- /dev/null +++ b/WebRtcNet.Api/Logging/LoggerFactoryHolder.cs @@ -0,0 +1,94 @@ +using System; +using System.Diagnostics.Contracts; +using System.Threading; +using Microsoft.Extensions.Logging; + +namespace WebRtcNet.Logging; + +/// +/// Internal singleton holder for the injected ILoggerFactory. +/// Provides thread-safe access to loggers throughout the library. +/// +internal static class LoggerFactoryHolder +{ + private static ILoggerFactory? logger_factory_; + private static readonly object lock_ = new(); + + /// + /// Gets the current ILoggerFactory, or creates a default one if not set. + /// + public static ILoggerFactory Current + { + get + { + if (logger_factory_ != null) + return logger_factory_; + + lock (lock_) + { + if (logger_factory_ != null) + return logger_factory_; + + // Create default factory based on build configuration + logger_factory_ = CreateDefaultFactory(); + return logger_factory_; + } + } + } + + /// + /// Sets the ILoggerFactory for the library. + /// Must be called before creating MediaDevices or PeerConnection. + /// + public static void SetLoggerFactory(ILoggerFactory factory) + { + Contract.Requires(factory != null, nameof(factory)); + + lock (lock_) + { + logger_factory_ = factory; + } + } + + /// + /// Gets or creates a logger for the given category. + /// + public static ILogger GetLogger(string category) + { + Contract.Requires(!string.IsNullOrEmpty(category), nameof(category)); + return Current.CreateLogger(category); + } + + private static ILoggerFactory CreateDefaultFactory() + { +#if DEBUG + // Debug builds: console logger with verbose output + var factory = LoggerFactory.Create(builder => + { + builder + .SetMinimumLevel(LogLevel.Debug) + .AddSimpleConsole(options => + { + options.UseUtcTimestamp = false; + options.IncludeScopes = true; + }); + }); + return factory; +#else + // Release builds: NullLogger (silent) unless app calls SetLoggerFactory + return new NullLoggerFactory(); +#endif + } + + /// + /// A no-op logger factory for Release builds. + /// + private class NullLoggerFactory : ILoggerFactory + { + public ILogger CreateLogger(string categoryName) => NullLogger.Instance; + + public void AddProvider(ILoggerProvider provider) { } + + public void Dispose() { } + } +} diff --git a/WebRtcNet.Api/Logging/WebRtcLogEvent.cs b/WebRtcNet.Api/Logging/WebRtcLogEvent.cs new file mode 100644 index 0000000..1f1bc60 --- /dev/null +++ b/WebRtcNet.Api/Logging/WebRtcLogEvent.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace WebRtcNet.Logging; + +/// +/// Represents a structured log event from WebRTC or interop layers. +/// +/// When the event occurred. +/// Log level (Trace, Debug, Information, Warning, Error, Critical). +/// Structured event identifier for telemetry and grouping. +/// Logger category (e.g., "WebRTC.PeerConnection", "Interop.MediaDevices"). +/// Managed thread ID where the event was logged. +/// Log message. +public record WebRtcLogEvent( + DateTime Timestamp, + LogLevel Severity, + EventId EventId, + string Category, + int ThreadId, + string Message); diff --git a/WebRtcNet.Api/Logging/WebRtcLogEventId.cs b/WebRtcNet.Api/Logging/WebRtcLogEventId.cs new file mode 100644 index 0000000..ce245b2 --- /dev/null +++ b/WebRtcNet.Api/Logging/WebRtcLogEventId.cs @@ -0,0 +1,56 @@ +namespace WebRtcNet.Logging; + +/// +/// Event ID constants for structured logging across WebRtcNet layers. +/// Event IDs are grouped by category to enable telemetry aggregation. +/// +public enum WebRtcLogEventId : int +{ + // WebRTC events (1000-1999) + // PeerConnection events (1000-1099) + PeerConnectionStateChanged = 1000, + PeerConnectionCreated = 1001, + PeerConnectionClosed = 1002, + PeerConnectionError = 1003, + + // DataChannel events (1100-1199) + DataChannelOpened = 1100, + DataChannelClosed = 1101, + DataChannelError = 1102, + + // Media events (1200-1299) + AudioDeviceInitialized = 1200, + VideoDeviceInitialized = 1201, + AudioProcessingWarning = 1202, + + // Transport/ICE events (1300-1399) + IceStateChanged = 1300, + IceGatheringStateChanged = 1301, + IceConnectionError = 1302, + + // Codec/RTP events (1400-1499) + CodecNegotiation = 1400, + RtpStats = 1401, + + // Audio Processing events (1500-1599) + AecWarning = 1500, + NoiseSuppressionWarning = 1501, + + // Other WebRTC events (1600-1699) + WebRtcOther = 1600, + + // Media enumeration events (2000-2099) + AudioEnumerationStarted = 2000, + AudioEnumerationFailed = 2001, + VideoEnumerationStarted = 2002, + VideoEnumerationFailed = 2003, + + // Media constraints events (2100-2199) + ConstraintValidation = 2100, + ConstraintError = 2101, + + // Interop errors (3000-3099) + InteropMediaDevicesFailed = 3000, + InteropPeerConnectionFailed = 3001, + InteropHResultFailure = 3002, +} diff --git a/WebRtcNet.Api/Resources/LogCategoryMapping.json b/WebRtcNet.Api/Resources/LogCategoryMapping.json new file mode 100644 index 0000000..a849459 --- /dev/null +++ b/WebRtcNet.Api/Resources/LogCategoryMapping.json @@ -0,0 +1,49 @@ +{ + "logCategoryMappings": [ + { + "tagPattern": "peerconnection", + "category": "WebRTC.PeerConnection", + "eventIdBase": 1000 + }, + { + "tagPattern": "peerconnectionfactory", + "category": "WebRTC.PeerConnection", + "eventIdBase": 1000 + }, + { + "tagPattern": "datachannel", + "category": "WebRTC.DataChannel", + "eventIdBase": 1100 + }, + { + "tagPattern": "audio_device|audio_coding", + "category": "WebRTC.Audio", + "eventIdBase": 1200 + }, + { + "tagPattern": "audio_processing(\\.aec|\\.ns)?", + "category": "WebRTC.AudioProcessing", + "eventIdBase": 1300 + }, + { + "tagPattern": "video_capture|video_coding", + "category": "WebRTC.Video", + "eventIdBase": 1400 + }, + { + "tagPattern": "ice_.*|dtls_transport|srtp_transport|transport", + "category": "WebRTC.Transport", + "eventIdBase": 1500 + }, + { + "tagPattern": "webrtcsdp|rtp_rtcp", + "category": "WebRTC.Transport", + "eventIdBase": 1500 + }, + { + "tagPattern": ".*", + "category": "WebRTC.Other", + "eventIdBase": 1900 + } + ] +} diff --git a/WebRtcNet.Api/WebRtcNet.Api.csproj b/WebRtcNet.Api/WebRtcNet.Api.csproj index 5c56402..989a481 100644 --- a/WebRtcNet.Api/WebRtcNet.Api.csproj +++ b/WebRtcNet.Api/WebRtcNet.Api.csproj @@ -13,4 +13,7 @@ + + + \ No newline at end of file diff --git a/WebRtcNet/Host.cs b/WebRtcNet/Host.cs index ec6ac7c..9105a87 100644 --- a/WebRtcNet/Host.cs +++ b/WebRtcNet/Host.cs @@ -2,6 +2,8 @@ using System.Diagnostics.Contracts; using System.IO; using System.Threading; +using Microsoft.Extensions.Logging; +using WebRtcNet.Logging; using WebRtcNet.Media; namespace WebRtcNet; @@ -19,6 +21,20 @@ public static class Host /// public static MediaDevices MediaDevices => media_devices_.Value; + /// + /// Sets the ILoggerFactory for the host. + /// Must be called before creating MediaDevices or PeerConnection instances to configure logging for the library. + /// + /// Logger factory to use for all WebRtcNet layers. + /// + /// If not called, Debug builds will log to console; Release builds will be silent. + /// + public static void SetLoggerFactory(ILoggerFactory factory) + { + Contract.Requires(factory != null, nameof(factory)); + LoggerFactoryHolder.SetLoggerFactory(factory); + } + /// /// Creates a peer connection using the active native backend. /// @@ -49,7 +65,7 @@ ex is MissingMethodException || ex is BadImageFormatException) { throw new InvalidOperationException( - $"Failed to initialize native WebRTC backend type '{typeof(T).FullName}'. Ensure WebRtcInterop assemblies and native dependencies are present for this target.", + \Failed to initialize native WebRTC backend type '\{typeof(T).FullName}'. Ensure WebRtcInterop assemblies and native dependencies are present for this target.\, ex); } } From 5af1bb91a02db207abd5a42cc66d0e5b62d78507 Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Sun, 7 Jun 2026 17:39:41 -0700 Subject: [PATCH 3/6] feat(#56): Implement Phase 2 - managed-to-interop bridge - Add IWebRtcLogWriter interface for C++/CLI to call - Add WebRtcLogWriter with lock-free .NET Channel and background dequeue task - Logs enqueued from native threads, dequeued to ILogger by category - Graceful shutdown with channel closure and task timeout - Add WebRtcLogWriterBridge singleton exposing writer to C++/CLI Phase 2 establishes thread-safe channel infrastructure ready for C++/CLI log sink. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- WebRtcNet.Api/Logging/IWebRtcLogWriter.cs | 18 +++ WebRtcNet.Api/Logging/WebRtcLogWriter.cs | 104 ++++++++++++++++++ .../Logging/WebRtcLogWriterBridge.cs | 48 ++++++++ 3 files changed, 170 insertions(+) create mode 100644 WebRtcNet.Api/Logging/IWebRtcLogWriter.cs create mode 100644 WebRtcNet.Api/Logging/WebRtcLogWriter.cs create mode 100644 WebRtcNet.Api/Logging/WebRtcLogWriterBridge.cs diff --git a/WebRtcNet.Api/Logging/IWebRtcLogWriter.cs b/WebRtcNet.Api/Logging/IWebRtcLogWriter.cs new file mode 100644 index 0000000..fcd3245 --- /dev/null +++ b/WebRtcNet.Api/Logging/IWebRtcLogWriter.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace WebRtcNet.Logging; + +/// +/// Managed interface that C++/CLI interop calls to enqueue structured log events. +/// Logs flow from native WebRTC threads through this interface to a managed channel, +/// then to background dequeue and ILogger. +/// +internal interface IWebRtcLogWriter +{ + /// + /// Writes a structured log event to the log queue. + /// May be called from arbitrary native threads; implementation must be thread-safe. + /// + void WriteLog(WebRtcLogEvent logEvent); +} diff --git a/WebRtcNet.Api/Logging/WebRtcLogWriter.cs b/WebRtcNet.Api/Logging/WebRtcLogWriter.cs new file mode 100644 index 0000000..5fb8e87 --- /dev/null +++ b/WebRtcNet.Api/Logging/WebRtcLogWriter.cs @@ -0,0 +1,104 @@ +using System; +using System.Diagnostics.Contracts; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace WebRtcNet.Logging; + +/// +/// Implements the managed log writer with thread-safe .NET Channel. +/// Logs are enqueued from arbitrary native threads, dequeued on a background task, +/// and forwarded to ILogger. +/// +internal class WebRtcLogWriter : IWebRtcLogWriter, IDisposable +{ + private readonly Channel channel_ = Channel.CreateUnbounded(); + private readonly Task dequeue_task_; + private bool disposed_; + + public WebRtcLogWriter() + { + // Start background dequeue task + dequeue_task_ = DequeueAndLogAsync(); + } + + /// + /// Writes a log event to the channel (thread-safe from native threads). + /// + public void WriteLog(WebRtcLogEvent logEvent) + { + Contract.Requires(logEvent != null, nameof(logEvent)); + + if (disposed_) + return; + + try + { + // Non-blocking write; if channel is closed, silently drop + channel_.Writer.TryWrite(logEvent); + } + catch + { + // Suppress exceptions; do not disrupt native code paths + } + } + + /// + /// Dequeues log events from channel and writes to ILogger by category. + /// Runs on a background task. + /// + private async Task DequeueAndLogAsync() + { + try + { + await foreach (var logEvent in channel_.Reader.ReadAllAsync()) + { + try + { + var logger = LoggerFactoryHolder.GetLogger(logEvent.Category); + logger.Log( + logEvent.Severity, + logEvent.EventId, + logEvent.Message, + null, + (msg, _) => msg); + } + catch + { + // Suppress exceptions; do not disrupt dequeue loop + } + } + } + catch + { + // Suppress exceptions during dequeue + } + } + + public void Dispose() + { + if (disposed_) + return; + + disposed_ = true; + + // Signal channel closure + channel_.Writer.TryComplete(); + + // Give background task time to drain (best effort) + try + { + if (!dequeue_task_.Wait(TimeSpan.FromSeconds(1))) + { + // Timeout; task is still running, but dispose anyway + } + } + catch + { + // Suppress any exceptions during shutdown + } + + channel_.Dispose(); + } +} diff --git a/WebRtcNet.Api/Logging/WebRtcLogWriterBridge.cs b/WebRtcNet.Api/Logging/WebRtcLogWriterBridge.cs new file mode 100644 index 0000000..bb19e0a --- /dev/null +++ b/WebRtcNet.Api/Logging/WebRtcLogWriterBridge.cs @@ -0,0 +1,48 @@ +using System; +using System.Diagnostics.Contracts; + +namespace WebRtcNet.Logging; + +/// +/// Bridge between C++/CLI interop and managed log writer. +/// Exposes a singleton IWebRtcLogWriter instance that C++/CLI can call. +/// +internal static class WebRtcLogWriterBridge +{ + private static IWebRtcLogWriter? writer_; + private static readonly object lock_ = new(); + + /// + /// Gets or creates the log writer singleton. + /// Called from C++/CLI to enqueue log events. + /// + public static IWebRtcLogWriter Instance + { + get + { + if (writer_ != null) + return writer_; + + lock (lock_) + { + if (writer_ != null) + return writer_; + + writer_ = new WebRtcLogWriter(); + return writer_; + } + } + } + + /// + /// Disposes the writer singleton (called on app shutdown). + /// + public static void Shutdown() + { + lock (lock_) + { + (writer_ as IDisposable)?.Dispose(); + writer_ = null; + } + } +} From 1e9750041c28a551cf94eaec0c49a803a5f7d0ff Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Sun, 7 Jun 2026 17:47:37 -0700 Subject: [PATCH 4/6] feat(#56): Implement Phase 3 - C++/CLI WebRTC log sink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add MarshalLogging.h with marshal_as specialization for rtc::LoggingSeverity->LogLevel using map - Add WebRtcLogSink.h/cpp custom rtc::LogSink to capture all WebRTC diagnostics - OnLogMessage extracts tag, severity, message and forwards to managed writer via channel - Register log sink with WebRTC in RtcPeerConnectionFactory (eager initialization) - Add sink to WebRtcInterop.Shared.vcxitems project Phase 3 completes C++/CLI logging infrastructure; logs now flow from WebRTC → managed ILogger. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- WebRtcInterop/Logging/WebRtcLogSink.cpp | 79 +++++++++++++++++++++ WebRtcInterop/Logging/WebRtcLogSink.h | 44 ++++++++++++ WebRtcInterop/Marshaling/MarshalLogging.h | 22 ++++++ WebRtcInterop/RtcPeerConnectionFactory.cpp | 21 +++++- WebRtcInterop/RtcPeerConnectionFactory.h | 8 ++- WebRtcInterop/WebRtcInterop.Shared.vcxitems | 27 +++---- 6 files changed, 184 insertions(+), 17 deletions(-) create mode 100644 WebRtcInterop/Logging/WebRtcLogSink.cpp create mode 100644 WebRtcInterop/Logging/WebRtcLogSink.h create mode 100644 WebRtcInterop/Marshaling/MarshalLogging.h diff --git a/WebRtcInterop/Logging/WebRtcLogSink.cpp b/WebRtcInterop/Logging/WebRtcLogSink.cpp new file mode 100644 index 0000000..87b8d12 --- /dev/null +++ b/WebRtcInterop/Logging/WebRtcLogSink.cpp @@ -0,0 +1,79 @@ +#include "pch.h" + +#include "WebRtcLogSink.h" +#include "..\Marshaling\MarshalLogging.h" +#include + +using namespace System; +using namespace WebRtcNet::Logging; + +namespace WebRtcInterop::Logging +{ + WebRtcLogSink::WebRtcLogSink() + { + } + + WebRtcLogSink::~WebRtcLogSink() + { + } + + void WebRtcLogSink::OnLogMessage(const rtc::LogMessage& msg) + { + try + { + // Extract tag, message, and convert severity + auto tag = gcnew String(msg.tag); + auto message = gcnew String(msg.str().c_str()); + auto severity = ConvertSeverity(msg.severity); + + // Resolve category and EventId base + String^ category = String::Empty; + int eventIdBase = 0; + ResolveCategoryAndEventId(tag, category, eventIdBase); + + // Get current thread ID + int threadId = Threading::Thread::CurrentThread->ManagedThreadId; + + // Create log event + auto logEvent = gcnew WebRtcLogEvent( + DateTime::Now, + severity, + gcnew EventId(eventIdBase, category), + category, + threadId, + message); + + // Write to managed writer + WebRtcLogWriterBridge::Instance->WriteLog(logEvent); + } + catch (...) + { + // Suppress exceptions; do not disrupt native logging + } + } + + System::Diagnostics::LogLevel WebRtcLogSink::ConvertSeverity(rtc::LoggingSeverity severity) + { + return marshal_as(severity); + } + + void WebRtcLogSink::ResolveCategoryAndEventId( + const String^ tag, + String^% category, + int% eventIdBase) + { + try + { + auto mapping = LogCategoryMapping::LoadFromResource(); + auto [cat, base] = mapping->ResolveTagToCategory(tag); + category = cat; + eventIdBase = base; + } + catch (...) + { + // Fallback: use WebRTC.Other + category = "WebRTC.Other"; + eventIdBase = 1900; + } + } +} diff --git a/WebRtcInterop/Logging/WebRtcLogSink.h b/WebRtcInterop/Logging/WebRtcLogSink.h new file mode 100644 index 0000000..12eb91a --- /dev/null +++ b/WebRtcInterop/Logging/WebRtcLogSink.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +namespace WebRtcInterop::Logging +{ + /// + /// Custom WebRTC log sink that forwards rtc::LogMessage events to managed IWebRtcLogWriter. + /// Registers with WebRTC's logging system to capture all diagnostic output. + /// + class WebRtcLogSink : public rtc::LogSink + { + public: + WebRtcLogSink(); + ~WebRtcLogSink() override; + + /// + /// Called by WebRTC for each log message. + /// Extracts severity, tag, and message, then forwards to managed writer. + /// + void OnLogMessage(const rtc::LogMessage& msg) override; + + /// + /// Called by WebRTC for each log message (alternate interface). + /// + void OnLogMessage(const System::String^ message) override { } + + private: + /// + /// Converts WebRTC severity level to .NET LogLevel using marshal_as. + /// + static System::Diagnostics::LogLevel ConvertSeverity(rtc::LoggingSeverity severity); + + /// + /// Extracts category and EventId base from WebRTC tag via managed mapping. + /// + static void ResolveCategoryAndEventId( + const System::String^ tag, + System::String^% category, + System::Int32% eventIdBase); + }; +} diff --git a/WebRtcInterop/Marshaling/MarshalLogging.h b/WebRtcInterop/Marshaling/MarshalLogging.h new file mode 100644 index 0000000..d3cc322 --- /dev/null +++ b/WebRtcInterop/Marshaling/MarshalLogging.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include "MarshalEnums.h" + +// Static map for rtc::LoggingSeverity to LogLevel conversion +static const std::map rtc_logging_severity_map{ + { rtc::LS_VERBOSE, System::Diagnostics::LogLevel::Debug }, + { rtc::LS_INFO, System::Diagnostics::LogLevel::Information }, + { rtc::LS_WARNING, System::Diagnostics::LogLevel::Warning }, + { rtc::LS_ERROR, System::Diagnostics::LogLevel::Error }, + { rtc::LS_NONE, System::Diagnostics::LogLevel::None } +}; + +// Marshal rtc::LoggingSeverity to System::Diagnostics::LogLevel +template <> +System::Diagnostics::LogLevel marshal_as(const rtc::LoggingSeverity& from) +{ + return marshal_mapped_native_type(rtc_logging_severity_map, from); +} diff --git a/WebRtcInterop/RtcPeerConnectionFactory.cpp b/WebRtcInterop/RtcPeerConnectionFactory.cpp index 61329f9..1a43854 100644 --- a/WebRtcInterop/RtcPeerConnectionFactory.cpp +++ b/WebRtcInterop/RtcPeerConnectionFactory.cpp @@ -5,7 +5,8 @@ #include #include -#include +#include +#include "Logging/WebRtcLogSink.h" using namespace System; @@ -15,7 +16,8 @@ namespace WebRtcInterop { std::unique_ptr network_thread_; std::unique_ptr worker_thread_; - std::unique_ptr signaling_thread_; + std::unique_ptr signaling_thread_; + static WebRtcInterop::Logging::WebRtcLogSink* log_sink_; } RtcPeerConnectionFactory::RtcPeerConnectionFactory() @@ -42,7 +44,11 @@ namespace WebRtcInterop signaling_thread_->Start(); } - CreateNativePeerConnectionFactory(); + CreateNativePeerConnectionFactory(); + + // Register logging sink for WebRTC diagnostics + log_sink_ = new WebRtcInterop::Logging::WebRtcLogSink(); + rtc::LogMessage::AddLogToStream(log_sink_, rtc::LS_VERBOSE); } RtcPeerConnectionFactory::~RtcPeerConnectionFactory() @@ -52,6 +58,14 @@ namespace WebRtcInterop RtcPeerConnectionFactory::!RtcPeerConnectionFactory() { + // Unregister logging sink + if (log_sink_ != nullptr) + { + rtc::LogMessage::RemoveLogToStream(log_sink_); + delete log_sink_; + log_sink_ = nullptr; + } + DestroyNativePeerConnectionFactory(); } @@ -126,3 +140,4 @@ namespace WebRtcInterop network_thread_.reset(); } } + diff --git a/WebRtcInterop/RtcPeerConnectionFactory.h b/WebRtcInterop/RtcPeerConnectionFactory.h index 10d8e39..a3341f9 100644 --- a/WebRtcInterop/RtcPeerConnectionFactory.h +++ b/WebRtcInterop/RtcPeerConnectionFactory.h @@ -1,4 +1,6 @@ -#pragma once +#pragma once + +namespace WebRtcInterop::Logging { class WebRtcLogSink; } namespace webrtc { @@ -36,6 +38,8 @@ namespace WebRtcInterop webrtc::scoped_refptr* _rpPeerConnectionFactory; - static RtcPeerConnectionFactory^ _instance = nullptr; + static RtcPeerConnectionFactory^ _instance = nullptr; + static WebRtcInterop::Logging::WebRtcLogSink* _log_sink = nullptr; }; } + diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems b/WebRtcInterop/WebRtcInterop.Shared.vcxitems index 47dd28f..6f4cbb4 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems @@ -7,13 +7,13 @@ - cd "$(WebRtcSrcRoot)" -echo Generating WebRtc Build Files -cmd /c gn gen --ide=vs "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" --filters=//:webrtc --args=" + cd "$(WebRtcSrcRoot)" +echo Generating WebRtc Build Files +cmd /c gn gen --ide=vs "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" --filters=//:webrtc --args=" - " -echo Compiling WebRtc Build Files -ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" + " +echo Compiling WebRtc Build Files +ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" use_custom_libcxx=false libcxx_is_shared=true use_lld=false is_component_build=false clang_use_chrome_plugins=false rtc_include_tests=false rtc_build_tools=false rtc_build_examples=false rtc_enable_symbol_export=true rtc_enable_protobuf=false enable_libaom=false @@ -72,7 +72,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -80,7 +80,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -88,7 +88,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=false enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=false enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -96,7 +96,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=true enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=true enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -104,7 +104,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -112,7 +112,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -160,6 +160,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" + @@ -167,5 +168,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" Create + + \ No newline at end of file From 4b888f29572d4c7dac1eb5e8c3601b4741e3afc2 Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Sun, 7 Jun 2026 19:21:36 -0700 Subject: [PATCH 5/6] Add structured logging infrastructure (Phase 1-4) Implements #56. Adds ILogger injection via Host.SetLoggerFactory(), a .NET Channel-backed log writer bridging native WebRTC threads to managed loggers, a C++/CLI rtc::LogSink capturing WebRTC library diagnostics, and enriched HRESULT failure logging in MediaDevices. Managed layer (WebRtcNet.Api): - WebRtcLogEventId enum with category ranges (WebRTC 1000-1999, Media 2000-2999, Interop 3000-3999) - WebRtcLogEvent record (Timestamp, Severity, EventId, Category, ThreadId, Message) - LogCategoryMapping: JSON resource mapping WebRTC tags to categories via regex, loaded from embedded LogCategoryMapping.json - LoggerFactoryHolder: thread-safe factory holder; Debug builds default to console logger, Release builds silent unless configured - WebRtcLogWriter: Channel + background dequeue task - WebRtcLogWriterBridge: public bridge exposing WriteInteropLog and ResolveWebRtcCategory for C++/CLI callers - Host.SetLoggerFactory() public API Interop layer (WebRtcInterop): - Logging/WebRtcLogSink: rtc::LogSink registered eagerly on RtcPeerConnectionFactory creation; uses marshal_as for string conversion; routes through WebRtcLogWriterBridge - Logging/InteropHResult: adds LogIfFailed() alongside ThrowIfFailed(); formats HRESULT with facility, code, and FormatMessageW system text - Logging/Marshaling/MarshalLogging.h: removed (severity converted via switch in WebRtcLogSink using int passthrough to WriteInteropLog) - MediaDevices: all COM HRESULT failure points now call LogIfFailed Tests: - WebRtcNet.Api.UnitTests/LoggingInfrastructureTests.cs: category mapping resolution, EventId ranges, WebRtcLogWriter channel forwarding - WebRtcInterop.UnitTests/InteropHResultTests.cpp: LogIfFailed return value for success and failure HRESULTs Documentation: - README: logging setup section with Host.SetLoggerFactory example - docs/adr/0003-logging-infrastructure.md: architecture decisions - WebRtcLogEventId: XML doc comments on all enum members BugzId:56 --- README.md | 20 +++ .../InteropHResultTests.cpp | 19 +++ .../WebRtcInterop.UnitTests.vcxproj | 1 + WebRtcInterop/InteropHResult.cpp | 20 --- WebRtcInterop/Logging/InteropHResult.cpp | 114 +++++++++++++++ WebRtcInterop/{ => Logging}/InteropHResult.h | 1 + .../Logging/Marshaling/MarshalLogging.h | 22 +++ WebRtcInterop/Logging/WebRtcLogSink.cpp | 43 +++--- WebRtcInterop/Logging/WebRtcLogSink.h | 13 +- WebRtcInterop/Marshaling/MarshalLogging.h | 22 --- WebRtcInterop/Media/MediaDevices.cpp | 39 +++++- WebRtcInterop/RtcPeerConnectionFactory.cpp | 34 ++--- WebRtcInterop/RtcPeerConnectionFactory.h | 8 +- WebRtcInterop/WebRtcInterop.Shared.vcxitems | 30 ++-- .../WebRtcInterop.Shared.vcxitems.filters | 23 ++- .../LoggingInfrastructureTests.cs | 131 ++++++++++++++++++ WebRtcNet.Api/Logging/LogCategoryMapping.cs | 14 +- WebRtcNet.Api/Logging/LoggerFactoryHolder.cs | 10 +- WebRtcNet.Api/Logging/WebRtcLogEventId.cs | 27 ++++ WebRtcNet.Api/Logging/WebRtcLogWriter.cs | 38 ++--- .../Logging/WebRtcLogWriterBridge.cs | 55 +++++++- WebRtcNet.Api/WebRtcNet.Api.csproj | 3 + WebRtcNet/Host.cs | 11 +- 23 files changed, 539 insertions(+), 159 deletions(-) create mode 100644 WebRtcInterop.UnitTests/InteropHResultTests.cpp delete mode 100644 WebRtcInterop/InteropHResult.cpp create mode 100644 WebRtcInterop/Logging/InteropHResult.cpp rename WebRtcInterop/{ => Logging}/InteropHResult.h (68%) create mode 100644 WebRtcInterop/Logging/Marshaling/MarshalLogging.h delete mode 100644 WebRtcInterop/Marshaling/MarshalLogging.h create mode 100644 WebRtcNet.Api.UnitTests/LoggingInfrastructureTests.cs diff --git a/README.md b/README.md index f2871ab..23fd145 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,26 @@ dotnet msbuild WebRtcInterop.UnitTests\WebRtcInterop.UnitTests.vcxproj /p:Config .\WebRtcInterop.UnitTests\x64\Debug\WebRtcInterop.UnitTests.exe ``` +## Logging configuration + +`Host.SetLoggerFactory(...)` configures logging for managed API code, C++/CLI interop code, and WebRTC native logs. + +```csharp +using Microsoft.Extensions.Logging; +using WebRtcNet; + +var loggerFactory = LoggerFactory.Create(builder => +{ + builder + .SetMinimumLevel(LogLevel.Information) + .AddConsole(); +}); + +Host.SetLoggerFactory(loggerFactory); +``` + +If `SetLoggerFactory` is not called, Debug builds default to console logging and Release builds are silent. + ## Docker pipeline The build pipeline uses individual-stage Dockerfiles rather than a single monolithic file. `docker buildx` is not used — Windows containers require classic `docker build`. diff --git a/WebRtcInterop.UnitTests/InteropHResultTests.cpp b/WebRtcInterop.UnitTests/InteropHResultTests.cpp new file mode 100644 index 0000000..0cdd49c --- /dev/null +++ b/WebRtcInterop.UnitTests/InteropHResultTests.cpp @@ -0,0 +1,19 @@ +#include "pch.h" + +#include +#include "gtest/gtest.h" + +using namespace WebRtcInterop; + +class interop_hresult_tests +{}; + +TEST(interop_hresult_tests, log_if_failed_returns_false_for_success) +{ + EXPECT_FALSE(InteropHResult::LogIfFailed(S_OK, "success", "Interop.Tests")); +} + +TEST(interop_hresult_tests, log_if_failed_returns_true_for_failure) +{ + EXPECT_TRUE(InteropHResult::LogIfFailed(E_FAIL, "failure", "Interop.Tests")); +} diff --git a/WebRtcInterop.UnitTests/WebRtcInterop.UnitTests.vcxproj b/WebRtcInterop.UnitTests/WebRtcInterop.UnitTests.vcxproj index c6a2610..e6c30ff 100644 --- a/WebRtcInterop.UnitTests/WebRtcInterop.UnitTests.vcxproj +++ b/WebRtcInterop.UnitTests/WebRtcInterop.UnitTests.vcxproj @@ -51,6 +51,7 @@ + diff --git a/WebRtcInterop/InteropHResult.cpp b/WebRtcInterop/InteropHResult.cpp deleted file mode 100644 index 144959f..0000000 --- a/WebRtcInterop/InteropHResult.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "pch.h" - -#include "InteropHResult.h" - -using namespace System::ComponentModel; -using namespace System::Runtime::InteropServices; - -namespace WebRtcInterop -{ - void InteropHResult::ThrowIfFailed(HRESULT hr, System::String^ message) - { - if (SUCCEEDED(hr)) - return; - - if (HRESULT_FACILITY(hr) == FACILITY_WIN32) - throw gcnew Win32Exception(HRESULT_CODE(hr), message); - - throw gcnew COMException(message, hr); - } -} diff --git a/WebRtcInterop/Logging/InteropHResult.cpp b/WebRtcInterop/Logging/InteropHResult.cpp new file mode 100644 index 0000000..d17bc2f --- /dev/null +++ b/WebRtcInterop/Logging/InteropHResult.cpp @@ -0,0 +1,114 @@ +#include "pch.h" + +#include "InteropHResult.h" + +#include + +using namespace System::ComponentModel; +using namespace System::Runtime::InteropServices; +using namespace System::Threading; +using namespace System; +using namespace WebRtcNet::Logging; + +namespace WebRtcInterop +{ + namespace + { + String^ TrimSystemMessage(String^ message) + { + if (String::IsNullOrWhiteSpace(message)) + return "Unknown system error"; + + return message->Trim(); + } + + String^ FormatSystemMessage(const HRESULT hr) + { + LPWSTR rawMessage = nullptr; + const auto flags = + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS; + auto messageId = static_cast(hr); + auto length = FormatMessageW( + flags, + nullptr, + messageId, + 0, + reinterpret_cast(&rawMessage), + 0, + nullptr); + + if (length == 0 && HRESULT_FACILITY(hr) == FACILITY_WIN32) + { + messageId = HRESULT_CODE(hr); + length = FormatMessageW( + flags, + nullptr, + messageId, + 0, + reinterpret_cast(&rawMessage), + 0, + nullptr); + } + + if (length == 0 || rawMessage == nullptr) + return "Unknown system error"; + + try + { + return TrimSystemMessage(gcnew String(rawMessage)); + } + finally + { + LocalFree(rawMessage); + } + } + } + + void InteropHResult::ThrowIfFailed(HRESULT hr, System::String^ message) + { + if (SUCCEEDED(hr)) + return; + + if (HRESULT_FACILITY(hr) == FACILITY_WIN32) + throw gcnew Win32Exception(HRESULT_CODE(hr), message); + + throw gcnew COMException(message, hr); + } + + bool InteropHResult::LogIfFailed(HRESULT hr, String^ operation, String^ category) + { + if (SUCCEEDED(hr)) + return false; + + if (String::IsNullOrWhiteSpace(operation)) + operation = "Interop operation"; + if (String::IsNullOrWhiteSpace(category)) + category = "Interop.HResult"; + + try + { + const auto formatted = String::Format( + "{0} failed. HRESULT=0x{1:X8} ({2}), Facility={3}, Code={4}, SystemMessage=\"{5}\"", + operation, + static_cast(hr), + hr, + HRESULT_FACILITY(hr), + HRESULT_CODE(hr), + FormatSystemMessage(hr)); + + WebRtcNet::Logging::WebRtcLogWriterBridge::WriteInteropLog( + 4, + static_cast(WebRtcLogEventId::InteropHResultFailure), + category, + Thread::CurrentThread->ManagedThreadId, + formatted); + } + catch (...) + { + } + + return true; + } +} diff --git a/WebRtcInterop/InteropHResult.h b/WebRtcInterop/Logging/InteropHResult.h similarity index 68% rename from WebRtcInterop/InteropHResult.h rename to WebRtcInterop/Logging/InteropHResult.h index e557bf1..fe22818 100644 --- a/WebRtcInterop/InteropHResult.h +++ b/WebRtcInterop/Logging/InteropHResult.h @@ -8,5 +8,6 @@ namespace WebRtcInterop { public: static void ThrowIfFailed(HRESULT hr, System::String^ message); + static bool LogIfFailed(HRESULT hr, System::String^ operation, System::String^ category); }; } diff --git a/WebRtcInterop/Logging/Marshaling/MarshalLogging.h b/WebRtcInterop/Logging/Marshaling/MarshalLogging.h new file mode 100644 index 0000000..f912e90 --- /dev/null +++ b/WebRtcInterop/Logging/Marshaling/MarshalLogging.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include "MarshalEnums.h" + +// Static map for rtc::LoggingSeverity to LogLevel conversion +static const std::map rtc_logging_severity_map{ + { rtc::LS_VERBOSE, Microsoft::Extensions::Logging::LogLevel::Debug }, + { rtc::LS_INFO, Microsoft::Extensions::Logging::LogLevel::Information }, + { rtc::LS_WARNING, Microsoft::Extensions::Logging::LogLevel::Warning }, + { rtc::LS_ERROR, Microsoft::Extensions::Logging::LogLevel::Error }, + { rtc::LS_NONE, Microsoft::Extensions::Logging::LogLevel::None } +}; + +// Marshal rtc::LoggingSeverity to Microsoft.Extensions.Logging.LogLevel +template <> +Microsoft::Extensions::Logging::LogLevel marshal_as(const rtc::LoggingSeverity& from) +{ + return marshal_mapped_native_type(rtc_logging_severity_map, from); +} diff --git a/WebRtcInterop/Logging/WebRtcLogSink.cpp b/WebRtcInterop/Logging/WebRtcLogSink.cpp index 87b8d12..49d7e76 100644 --- a/WebRtcInterop/Logging/WebRtcLogSink.cpp +++ b/WebRtcInterop/Logging/WebRtcLogSink.cpp @@ -1,8 +1,6 @@ #include "pch.h" #include "WebRtcLogSink.h" -#include "..\Marshaling\MarshalLogging.h" -#include using namespace System; using namespace WebRtcNet::Logging; @@ -17,14 +15,14 @@ namespace WebRtcInterop::Logging { } - void WebRtcLogSink::OnLogMessage(const rtc::LogMessage& msg) + void WebRtcLogSink::OnLogMessage(const webrtc::LogLineRef& msg) { try { // Extract tag, message, and convert severity - auto tag = gcnew String(msg.tag); - auto message = gcnew String(msg.str().c_str()); - auto severity = ConvertSeverity(msg.severity); + auto tag = marshal_as(std::string(msg.tag())); + auto message = marshal_as(std::string(msg.message())); + auto severity = ConvertSeverity(msg.severity()); // Resolve category and EventId base String^ category = String::Empty; @@ -35,16 +33,12 @@ namespace WebRtcInterop::Logging int threadId = Threading::Thread::CurrentThread->ManagedThreadId; // Create log event - auto logEvent = gcnew WebRtcLogEvent( - DateTime::Now, + WebRtcLogWriterBridge::WriteInteropLog( severity, - gcnew EventId(eventIdBase, category), + eventIdBase, category, threadId, message); - - // Write to managed writer - WebRtcLogWriterBridge::Instance->WriteLog(logEvent); } catch (...) { @@ -52,22 +46,33 @@ namespace WebRtcInterop::Logging } } - System::Diagnostics::LogLevel WebRtcLogSink::ConvertSeverity(rtc::LoggingSeverity severity) + int WebRtcLogSink::ConvertSeverity(webrtc::LoggingSeverity severity) { - return marshal_as(severity); + switch (severity) + { + case webrtc::LS_VERBOSE: + return 1; + case webrtc::LS_INFO: + return 2; + case webrtc::LS_WARNING: + return 3; + case webrtc::LS_ERROR: + return 4; + case webrtc::LS_NONE: + return 6; + default: + return 2; + } } void WebRtcLogSink::ResolveCategoryAndEventId( - const String^ tag, + String^ tag, String^% category, int% eventIdBase) { try { - auto mapping = LogCategoryMapping::LoadFromResource(); - auto [cat, base] = mapping->ResolveTagToCategory(tag); - category = cat; - eventIdBase = base; + WebRtcLogWriterBridge::ResolveWebRtcCategory(tag, category, eventIdBase); } catch (...) { diff --git a/WebRtcInterop/Logging/WebRtcLogSink.h b/WebRtcInterop/Logging/WebRtcLogSink.h index 12eb91a..47e014a 100644 --- a/WebRtcInterop/Logging/WebRtcLogSink.h +++ b/WebRtcInterop/Logging/WebRtcLogSink.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -10,7 +9,7 @@ namespace WebRtcInterop::Logging /// Custom WebRTC log sink that forwards rtc::LogMessage events to managed IWebRtcLogWriter. /// Registers with WebRTC's logging system to capture all diagnostic output. /// - class WebRtcLogSink : public rtc::LogSink + class WebRtcLogSink : public webrtc::LogSink { public: WebRtcLogSink(); @@ -20,24 +19,24 @@ namespace WebRtcInterop::Logging /// Called by WebRTC for each log message. /// Extracts severity, tag, and message, then forwards to managed writer. /// - void OnLogMessage(const rtc::LogMessage& msg) override; + void OnLogMessage(const webrtc::LogLineRef& msg) override; /// /// Called by WebRTC for each log message (alternate interface). /// - void OnLogMessage(const System::String^ message) override { } + void OnLogMessage(const std::string& message) override { } private: /// - /// Converts WebRTC severity level to .NET LogLevel using marshal_as. + /// Converts WebRTC severity level to Microsoft.Extensions.Logging.LogLevel numeric values. /// - static System::Diagnostics::LogLevel ConvertSeverity(rtc::LoggingSeverity severity); + static int ConvertSeverity(webrtc::LoggingSeverity severity); /// /// Extracts category and EventId base from WebRTC tag via managed mapping. /// static void ResolveCategoryAndEventId( - const System::String^ tag, + System::String^ tag, System::String^% category, System::Int32% eventIdBase); }; diff --git a/WebRtcInterop/Marshaling/MarshalLogging.h b/WebRtcInterop/Marshaling/MarshalLogging.h deleted file mode 100644 index d3cc322..0000000 --- a/WebRtcInterop/Marshaling/MarshalLogging.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include -#include -#include -#include "MarshalEnums.h" - -// Static map for rtc::LoggingSeverity to LogLevel conversion -static const std::map rtc_logging_severity_map{ - { rtc::LS_VERBOSE, System::Diagnostics::LogLevel::Debug }, - { rtc::LS_INFO, System::Diagnostics::LogLevel::Information }, - { rtc::LS_WARNING, System::Diagnostics::LogLevel::Warning }, - { rtc::LS_ERROR, System::Diagnostics::LogLevel::Error }, - { rtc::LS_NONE, System::Diagnostics::LogLevel::None } -}; - -// Marshal rtc::LoggingSeverity to System::Diagnostics::LogLevel -template <> -System::Diagnostics::LogLevel marshal_as(const rtc::LoggingSeverity& from) -{ - return marshal_mapped_native_type(rtc_logging_severity_map, from); -} diff --git a/WebRtcInterop/Media/MediaDevices.cpp b/WebRtcInterop/Media/MediaDevices.cpp index b63c7ef..f29dc6e 100644 --- a/WebRtcInterop/Media/MediaDevices.cpp +++ b/WebRtcInterop/Media/MediaDevices.cpp @@ -3,7 +3,7 @@ #include #include "MediaDevices.h" #include "CameraVideoSource.h" -#include "InteropHResult.h" +#include "Logging/InteropHResult.h" #include "MediaStream.h" #include "MediaStreamTrack.h" #include "Marshaling/MarshalMedia.h" @@ -28,6 +28,11 @@ namespace WebRtcInterop::Media namespace { + String^ GetInteropMediaDevicesCategory() + { + return "Interop.MediaDevices"; + } + String^ GetDeviceMapKey(MediaDeviceInfo^ device) { return String::Format("{0}|{1}", (int)device->Kind, device->DeviceId); @@ -37,12 +42,16 @@ namespace WebRtcInterop::Media { auto label = fallbackLabel; IPropertyStore* propertyStore = nullptr; - if (FAILED(device->OpenPropertyStore(STGM_READ, &propertyStore)) || propertyStore == nullptr) + auto hr = device->OpenPropertyStore(STGM_READ, &propertyStore); + if (InteropHResult::LogIfFailed(hr, "IMMDevice::OpenPropertyStore", GetInteropMediaDevicesCategory()) || + propertyStore == nullptr) return label; PROPVARIANT friendlyName; PropVariantInit(&friendlyName); - if (SUCCEEDED(propertyStore->GetValue(PKEY_Device_FriendlyName, &friendlyName)) && + hr = propertyStore->GetValue(PKEY_Device_FriendlyName, &friendlyName); + InteropHResult::LogIfFailed(hr, "IPropertyStore::GetValue(PKEY_Device_FriendlyName)", GetInteropMediaDevicesCategory()); + if (SUCCEEDED(hr) && friendlyName.vt == VT_LPWSTR) label = gcnew String(friendlyName.pwszVal); @@ -60,12 +69,15 @@ namespace WebRtcInterop::Media { IMMDeviceCollection* collection = nullptr; auto hr = enumerator->EnumAudioEndpoints(flow, DEVICE_STATE_ACTIVE, &collection); - if (FAILED(hr) || collection == nullptr) + if (InteropHResult::LogIfFailed( + hr, + String::Format("IMMDeviceEnumerator::EnumAudioEndpoints(flow={0})", static_cast(flow)), + GetInteropMediaDevicesCategory()) || collection == nullptr) return; UINT count = 0; hr = collection->GetCount(&count); - if (FAILED(hr)) + if (InteropHResult::LogIfFailed(hr, "IMMDeviceCollection::GetCount", GetInteropMediaDevicesCategory())) { collection->Release(); return; @@ -74,11 +86,20 @@ namespace WebRtcInterop::Media for (UINT i = 0; i < count; ++i) { IMMDevice* device = nullptr; - if (FAILED(collection->Item(i, &device)) || device == nullptr) + hr = collection->Item(i, &device); + if (InteropHResult::LogIfFailed( + hr, + String::Format("IMMDeviceCollection::Item(index={0})", i), + GetInteropMediaDevicesCategory()) || device == nullptr) continue; LPWSTR deviceId = nullptr; - if (SUCCEEDED(device->GetId(&deviceId))) + hr = device->GetId(&deviceId); + InteropHResult::LogIfFailed( + hr, + String::Format("IMMDevice::GetId(index={0})", i), + GetInteropMediaDevicesCategory()); + if (SUCCEEDED(hr)) { auto id = gcnew String(deviceId); auto label = GetAudioDeviceLabel(device, fallbackLabel); @@ -294,11 +315,15 @@ namespace WebRtcInterop::Media // Initialize COM HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); if (FAILED(hr) && hr != S_FALSE) + { + InteropHResult::LogIfFailed(hr, "CoInitializeEx", GetInteropMediaDevicesCategory()); return devices; // COM already initialized or failed + } IMMDeviceEnumerator* enumerator = nullptr; hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), reinterpret_cast(&enumerator)); + InteropHResult::LogIfFailed(hr, "CoCreateInstance(MMDeviceEnumerator)", GetInteropMediaDevicesCategory()); if (SUCCEEDED(hr) && enumerator != nullptr) { diff --git a/WebRtcInterop/RtcPeerConnectionFactory.cpp b/WebRtcInterop/RtcPeerConnectionFactory.cpp index 1a43854..50eb05b 100644 --- a/WebRtcInterop/RtcPeerConnectionFactory.cpp +++ b/WebRtcInterop/RtcPeerConnectionFactory.cpp @@ -4,8 +4,9 @@ #include "RtcPeerConnection.h" #include +#include #include -#include +#include #include "Logging/WebRtcLogSink.h" using namespace System; @@ -16,8 +17,8 @@ namespace WebRtcInterop { std::unique_ptr network_thread_; std::unique_ptr worker_thread_; - std::unique_ptr signaling_thread_; - static WebRtcInterop::Logging::WebRtcLogSink* log_sink_; + std::unique_ptr signaling_thread_; + static Logging::WebRtcLogSink* log_sink_; } RtcPeerConnectionFactory::RtcPeerConnectionFactory() @@ -44,11 +45,11 @@ namespace WebRtcInterop signaling_thread_->Start(); } - CreateNativePeerConnectionFactory(); - - // Register logging sink for WebRTC diagnostics - log_sink_ = new WebRtcInterop::Logging::WebRtcLogSink(); - rtc::LogMessage::AddLogToStream(log_sink_, rtc::LS_VERBOSE); + CreateNativePeerConnectionFactory(); + + // Register logging sink for WebRTC diagnostics + log_sink_ = new Logging::WebRtcLogSink(); + webrtc::LogMessage::AddLogToStream(log_sink_, webrtc::LS_VERBOSE); } RtcPeerConnectionFactory::~RtcPeerConnectionFactory() @@ -58,14 +59,14 @@ namespace WebRtcInterop RtcPeerConnectionFactory::!RtcPeerConnectionFactory() { - // Unregister logging sink - if (log_sink_ != nullptr) - { - rtc::LogMessage::RemoveLogToStream(log_sink_); - delete log_sink_; - log_sink_ = nullptr; - } - + // Unregister logging sink + if (log_sink_ != nullptr) + { + webrtc::LogMessage::RemoveLogToStream(log_sink_); + delete log_sink_; + log_sink_ = nullptr; + } + DestroyNativePeerConnectionFactory(); } @@ -140,4 +141,3 @@ namespace WebRtcInterop network_thread_.reset(); } } - diff --git a/WebRtcInterop/RtcPeerConnectionFactory.h b/WebRtcInterop/RtcPeerConnectionFactory.h index a3341f9..4406e0a 100644 --- a/WebRtcInterop/RtcPeerConnectionFactory.h +++ b/WebRtcInterop/RtcPeerConnectionFactory.h @@ -1,5 +1,5 @@ -#pragma once - +#pragma once + namespace WebRtcInterop::Logging { class WebRtcLogSink; } namespace webrtc @@ -38,8 +38,6 @@ namespace WebRtcInterop webrtc::scoped_refptr* _rpPeerConnectionFactory; - static RtcPeerConnectionFactory^ _instance = nullptr; - static WebRtcInterop::Logging::WebRtcLogSink* _log_sink = nullptr; + static RtcPeerConnectionFactory^ _instance = nullptr; }; } - diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems b/WebRtcInterop/WebRtcInterop.Shared.vcxitems index 6f4cbb4..a008ef4 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems @@ -7,13 +7,13 @@ - cd "$(WebRtcSrcRoot)" -echo Generating WebRtc Build Files -cmd /c gn gen --ide=vs "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" --filters=//:webrtc --args=" + cd "$(WebRtcSrcRoot)" +echo Generating WebRtc Build Files +cmd /c gn gen --ide=vs "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" --filters=//:webrtc --args=" - " -echo Compiling WebRtc Build Files -ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" + " +echo Compiling WebRtc Build Files +ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" use_custom_libcxx=false libcxx_is_shared=true use_lld=false is_component_build=false clang_use_chrome_plugins=false rtc_include_tests=false rtc_build_tools=false rtc_build_examples=false rtc_enable_symbol_export=true rtc_enable_protobuf=false enable_libaom=false @@ -72,7 +72,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -80,7 +80,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -88,7 +88,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=false enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=false enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -96,7 +96,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=true enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=true enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -104,7 +104,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -112,7 +112,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) Building Google WebRtc Library $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib @@ -128,7 +128,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - + @@ -150,7 +150,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - + @@ -168,7 +168,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" Create - + \ No newline at end of file diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters b/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters index 1e08676..6f11d79 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters @@ -13,10 +13,18 @@ {9ba0ba64-d5f2-4e0f-9f00-5a827edbb003} + + {f3d60c9c-5de3-4ff8-b8f3-1fb426952f45} + + + {2df4c4f8-8774-4fee-8583-5b8d9a6ccb1b} + - + + Logging + Media @@ -38,10 +46,15 @@ + + Logging + - + + Logging + Marshaling @@ -83,5 +96,11 @@ + + Logging\Marshaling + + + Logging + \ No newline at end of file diff --git a/WebRtcNet.Api.UnitTests/LoggingInfrastructureTests.cs b/WebRtcNet.Api.UnitTests/LoggingInfrastructureTests.cs new file mode 100644 index 0000000..83ee81f --- /dev/null +++ b/WebRtcNet.Api.UnitTests/LoggingInfrastructureTests.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using WebRtcNet.Logging; + +namespace WebRtcNet.Api.UnitTests; + +[TestFixture] +[NonParallelizable] +public class LoggingInfrastructureTests +{ + private ILoggerFactory original_logger_factory_ = null!; + + [SetUp] + public void SetUp() + { + original_logger_factory_ = LoggerFactoryHolder.Current; + } + + [TearDown] + public void TearDown() + { + LoggerFactoryHolder.SetLoggerFactory(original_logger_factory_); + } + + [Test] + public void LogCategoryMapping_ResolveTagToCategory_MapsKnownAndFallbackTags() + { + var mapping = LogCategoryMapping.LoadFromResource(); + + var (peerCategory, peerEventId) = mapping.ResolveTagToCategory("(peerconnection)"); + var (fallbackCategory, fallbackEventId) = mapping.ResolveTagToCategory("(unmapped.tag)"); + + Assert.That(peerCategory, Is.EqualTo("WebRTC.PeerConnection")); + Assert.That(peerEventId, Is.EqualTo(1000)); + Assert.That(fallbackCategory, Is.EqualTo("WebRTC.Other")); + Assert.That(fallbackEventId, Is.EqualTo(1900)); + } + + [Test] + public void WebRtcLogEventId_UsesConfiguredCategoryRanges() + { + Assert.That((int)WebRtcLogEventId.PeerConnectionStateChanged, Is.InRange(1000, 1999)); + Assert.That((int)WebRtcLogEventId.WebRtcOther, Is.InRange(1000, 1999)); + Assert.That((int)WebRtcLogEventId.AudioEnumerationStarted, Is.InRange(2000, 2999)); + Assert.That((int)WebRtcLogEventId.ConstraintError, Is.InRange(2000, 2999)); + Assert.That((int)WebRtcLogEventId.InteropMediaDevicesFailed, Is.InRange(3000, 3999)); + Assert.That((int)WebRtcLogEventId.InteropHResultFailure, Is.InRange(3000, 3999)); + } + + [Test] + public void WebRtcLogWriter_WriteLog_ForwardsEventToLoggerFactoryCategory() + { + var factory = new CapturingLoggerFactory(); + LoggerFactoryHolder.SetLoggerFactory(factory); + using var writer = new WebRtcLogWriter(); + var logEvent = new WebRtcLogEvent( + DateTime.Now, + LogLevel.Warning, + new EventId((int)WebRtcLogEventId.PeerConnectionError, nameof(WebRtcLogEventId.PeerConnectionError)), + "WebRTC.PeerConnection", + 123, + "peer connection warning"); + + writer.WriteLog(logEvent); + + var timeout = Stopwatch.StartNew(); + while (timeout.Elapsed < TimeSpan.FromSeconds(2)) + { + if (factory.TryDequeue(out var captured)) + { + Assert.That(captured, Is.Not.Null); + var entry = captured!; + Assert.That(entry.Category, Is.EqualTo("WebRTC.PeerConnection")); + Assert.That(entry.Level, Is.EqualTo(LogLevel.Warning)); + Assert.That(entry.EventId.Id, Is.EqualTo((int)WebRtcLogEventId.PeerConnectionError)); + Assert.That(entry.Message, Is.EqualTo("peer connection warning")); + return; + } + } + + Assert.Fail("Expected one captured log entry from WebRtcLogWriter."); + } + + private sealed class CapturingLoggerFactory : ILoggerFactory + { + private readonly ConcurrentQueue entries_ = new(); + + public ILogger CreateLogger(string categoryName) => new CapturingLogger(categoryName, entries_); + + public void AddProvider(ILoggerProvider provider) + { + } + + public void Dispose() + { + } + + public bool TryDequeue(out CapturedLogEntry? entry) => entries_.TryDequeue(out entry); + } + + private sealed class CapturingLogger(string category, ConcurrentQueue entries) : ILogger + { + public IDisposable BeginScope(TState state) where TState : notnull => NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + entries.Enqueue(new CapturedLogEntry(category, logLevel, eventId, formatter(state, exception))); + } + } + + private sealed class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + + public void Dispose() + { + } + } + + private sealed record CapturedLogEntry(string Category, LogLevel Level, EventId EventId, string Message); +} diff --git a/WebRtcNet.Api/Logging/LogCategoryMapping.cs b/WebRtcNet.Api/Logging/LogCategoryMapping.cs index 556ff75..4e0b439 100644 --- a/WebRtcNet.Api/Logging/LogCategoryMapping.cs +++ b/WebRtcNet.Api/Logging/LogCategoryMapping.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.Contracts; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -44,7 +43,11 @@ public static LogCategoryMapping LoadFromResource() { // Load JSON from embedded resource var assembly = typeof(LogCategoryMapping).Assembly; - var resourceName = $"{typeof(LogCategoryMapping).Namespace}.Resources.LogCategoryMapping.json"; + var resourceName = assembly + .GetManifestResourceNames() + .FirstOrDefault(name => name.EndsWith("LogCategoryMapping.json", StringComparison.Ordinal)); + if (resourceName == null) + throw new InvalidOperationException("Embedded resource 'LogCategoryMapping.json' not found."); using var stream = assembly.GetManifestResourceStream(resourceName); if (stream == null) @@ -81,11 +84,12 @@ public static LogCategoryMapping LoadFromResource() /// /// Resolves a WebRTC tag to category and EventId base. - /// Returns ("WebRTC.Other", 1600) if no match found. + /// Returns ("WebRTC.Other", 1900) if no match found. /// public (string Category, int EventIdBase) ResolveTagToCategory(string tag) { - Contract.Requires(!string.IsNullOrEmpty(tag), nameof(tag)); + if (string.IsNullOrEmpty(tag)) + throw new ArgumentException("Tag must not be null or empty.", nameof(tag)); // Remove leading/trailing parentheses if present (tags come as "(tag_name)") var cleanTag = tag.Trim('(', ')'); @@ -97,6 +101,6 @@ public static LogCategoryMapping LoadFromResource() } // Default fallback - return ("WebRTC.Other", 1600); + return ("WebRTC.Other", 1900); } } diff --git a/WebRtcNet.Api/Logging/LoggerFactoryHolder.cs b/WebRtcNet.Api/Logging/LoggerFactoryHolder.cs index 9fe25b6..927e8dc 100644 --- a/WebRtcNet.Api/Logging/LoggerFactoryHolder.cs +++ b/WebRtcNet.Api/Logging/LoggerFactoryHolder.cs @@ -1,7 +1,7 @@ using System; -using System.Diagnostics.Contracts; -using System.Threading; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Console; namespace WebRtcNet.Logging; @@ -42,7 +42,8 @@ public static ILoggerFactory Current /// public static void SetLoggerFactory(ILoggerFactory factory) { - Contract.Requires(factory != null, nameof(factory)); + if (factory == null) + throw new ArgumentNullException(nameof(factory)); lock (lock_) { @@ -55,7 +56,8 @@ public static void SetLoggerFactory(ILoggerFactory factory) /// public static ILogger GetLogger(string category) { - Contract.Requires(!string.IsNullOrEmpty(category), nameof(category)); + if (string.IsNullOrEmpty(category)) + throw new ArgumentException("Category must not be null or empty.", nameof(category)); return Current.CreateLogger(category); } diff --git a/WebRtcNet.Api/Logging/WebRtcLogEventId.cs b/WebRtcNet.Api/Logging/WebRtcLogEventId.cs index ce245b2..c42028b 100644 --- a/WebRtcNet.Api/Logging/WebRtcLogEventId.cs +++ b/WebRtcNet.Api/Logging/WebRtcLogEventId.cs @@ -8,49 +8,76 @@ public enum WebRtcLogEventId : int { // WebRTC events (1000-1999) // PeerConnection events (1000-1099) + /// Peer connection state transition. PeerConnectionStateChanged = 1000, + /// Peer connection created. PeerConnectionCreated = 1001, + /// Peer connection closed. PeerConnectionClosed = 1002, + /// Peer connection error. PeerConnectionError = 1003, // DataChannel events (1100-1199) + /// Data channel opened. DataChannelOpened = 1100, + /// Data channel closed. DataChannelClosed = 1101, + /// Data channel error. DataChannelError = 1102, // Media events (1200-1299) + /// Audio device initialized. AudioDeviceInitialized = 1200, + /// Video device initialized. VideoDeviceInitialized = 1201, + /// Audio processing warning. AudioProcessingWarning = 1202, // Transport/ICE events (1300-1399) + /// ICE state changed. IceStateChanged = 1300, + /// ICE gathering state changed. IceGatheringStateChanged = 1301, + /// ICE connection error. IceConnectionError = 1302, // Codec/RTP events (1400-1499) + /// Codec negotiation event. CodecNegotiation = 1400, + /// RTP statistics event. RtpStats = 1401, // Audio Processing events (1500-1599) + /// Acoustic echo cancellation warning. AecWarning = 1500, + /// Noise suppression warning. NoiseSuppressionWarning = 1501, // Other WebRTC events (1600-1699) + /// Uncategorized WebRTC event. WebRtcOther = 1600, // Media enumeration events (2000-2099) + /// Audio device enumeration started. AudioEnumerationStarted = 2000, + /// Audio device enumeration failed. AudioEnumerationFailed = 2001, + /// Video device enumeration started. VideoEnumerationStarted = 2002, + /// Video device enumeration failed. VideoEnumerationFailed = 2003, // Media constraints events (2100-2199) + /// Constraint validation event. ConstraintValidation = 2100, + /// Constraint validation error. ConstraintError = 2101, // Interop errors (3000-3099) + /// Interop media devices failure. InteropMediaDevicesFailed = 3000, + /// Interop peer connection failure. InteropPeerConnectionFailed = 3001, + /// Interop HRESULT failure. InteropHResultFailure = 3002, } diff --git a/WebRtcNet.Api/Logging/WebRtcLogWriter.cs b/WebRtcNet.Api/Logging/WebRtcLogWriter.cs index 5fb8e87..b83297d 100644 --- a/WebRtcNet.Api/Logging/WebRtcLogWriter.cs +++ b/WebRtcNet.Api/Logging/WebRtcLogWriter.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.Contracts; using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -28,20 +27,14 @@ public WebRtcLogWriter() /// public void WriteLog(WebRtcLogEvent logEvent) { - Contract.Requires(logEvent != null, nameof(logEvent)); + if (logEvent == null) + throw new ArgumentNullException(nameof(logEvent)); if (disposed_) return; - try - { - // Non-blocking write; if channel is closed, silently drop - channel_.Writer.TryWrite(logEvent); - } - catch - { - // Suppress exceptions; do not disrupt native code paths - } + // Non-blocking write; if channel is closed, drop the message. + channel_.Writer.TryWrite(logEvent); } /// @@ -64,15 +57,15 @@ private async Task DequeueAndLogAsync() null, (msg, _) => msg); } - catch + catch (InvalidOperationException) { - // Suppress exceptions; do not disrupt dequeue loop + // Logger factory may be shutting down. } } } - catch + catch (InvalidOperationException) { - // Suppress exceptions during dequeue + // Channel closed while reading. } } @@ -87,18 +80,7 @@ public void Dispose() channel_.Writer.TryComplete(); // Give background task time to drain (best effort) - try - { - if (!dequeue_task_.Wait(TimeSpan.FromSeconds(1))) - { - // Timeout; task is still running, but dispose anyway - } - } - catch - { - // Suppress any exceptions during shutdown - } - - channel_.Dispose(); + if (!dequeue_task_.Wait(TimeSpan.FromSeconds(1))) + return; } } diff --git a/WebRtcNet.Api/Logging/WebRtcLogWriterBridge.cs b/WebRtcNet.Api/Logging/WebRtcLogWriterBridge.cs index bb19e0a..b124aa5 100644 --- a/WebRtcNet.Api/Logging/WebRtcLogWriterBridge.cs +++ b/WebRtcNet.Api/Logging/WebRtcLogWriterBridge.cs @@ -1,5 +1,5 @@ using System; -using System.Diagnostics.Contracts; +using Microsoft.Extensions.Logging; namespace WebRtcNet.Logging; @@ -7,7 +7,7 @@ namespace WebRtcNet.Logging; /// Bridge between C++/CLI interop and managed log writer. /// Exposes a singleton IWebRtcLogWriter instance that C++/CLI can call. /// -internal static class WebRtcLogWriterBridge +public static class WebRtcLogWriterBridge { private static IWebRtcLogWriter? writer_; private static readonly object lock_ = new(); @@ -16,7 +16,7 @@ internal static class WebRtcLogWriterBridge /// Gets or creates the log writer singleton. /// Called from C++/CLI to enqueue log events. /// - public static IWebRtcLogWriter Instance + internal static IWebRtcLogWriter Instance { get { @@ -34,6 +34,55 @@ public static IWebRtcLogWriter Instance } } + /// + /// Sets the shared logger factory used by all managed and interop logs. + /// + public static void SetLoggerFactory(ILoggerFactory factory) + { + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + LoggerFactoryHolder.SetLoggerFactory(factory); + } + + /// + /// Resolves a WebRTC tag to category and EventId base values. + /// + public static void ResolveWebRtcCategory(string tag, out string category, out int eventIdBase) + { + var mapping = LogCategoryMapping.LoadFromResource(); + var resolved = mapping.ResolveTagToCategory(tag); + category = resolved.Category; + eventIdBase = resolved.EventIdBase; + } + + /// + /// Writes an interop-originated log entry using primitive arguments. + /// + public static void WriteInteropLog( + int severity, + int eventId, + string category, + int threadId, + string message) + { + if (string.IsNullOrEmpty(category)) + category = "Interop.Other"; + message ??= string.Empty; + + var resolvedSeverity = Enum.IsDefined(typeof(LogLevel), severity) + ? (LogLevel)severity + : LogLevel.Information; + + var logEvent = new WebRtcLogEvent( + DateTime.Now, + resolvedSeverity, + new EventId(eventId, category), + category, + threadId, + message); + Instance.WriteLog(logEvent); + } + /// /// Disposes the writer singleton (called on app shutdown). /// diff --git a/WebRtcNet.Api/WebRtcNet.Api.csproj b/WebRtcNet.Api/WebRtcNet.Api.csproj index 989a481..16266e1 100644 --- a/WebRtcNet.Api/WebRtcNet.Api.csproj +++ b/WebRtcNet.Api/WebRtcNet.Api.csproj @@ -12,6 +12,9 @@ + + + diff --git a/WebRtcNet/Host.cs b/WebRtcNet/Host.cs index 9105a87..2d48250 100644 --- a/WebRtcNet/Host.cs +++ b/WebRtcNet/Host.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.Contracts; using System.IO; using System.Threading; using Microsoft.Extensions.Logging; @@ -31,8 +30,9 @@ public static class Host /// public static void SetLoggerFactory(ILoggerFactory factory) { - Contract.Requires(factory != null, nameof(factory)); - LoggerFactoryHolder.SetLoggerFactory(factory); + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + WebRtcLogWriterBridge.SetLoggerFactory(factory); } /// @@ -42,7 +42,8 @@ public static void SetLoggerFactory(ILoggerFactory factory) /// A new peer connection instance. public static RtcPeerConnection CreatePeerConnection(RtcConfiguration configuration) { - Contract.Requires(configuration != null, nameof(configuration)); + if (configuration == null) + throw new ArgumentNullException(nameof(configuration)); return CreateInteropInstance( () => WebRtcInterop.RtcPeerConnectionFactory.CreatePeerConnection(configuration)); } @@ -65,7 +66,7 @@ ex is MissingMethodException || ex is BadImageFormatException) { throw new InvalidOperationException( - \Failed to initialize native WebRTC backend type '\{typeof(T).FullName}'. Ensure WebRtcInterop assemblies and native dependencies are present for this target.\, + $"Failed to initialize native WebRTC backend type '{typeof(T).FullName}'. Ensure WebRtcInterop assemblies and native dependencies are present for this target.", ex); } } From 17a3d3f8b1b38484daa4ad2f21f1efc57bc69037 Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Sun, 7 Jun 2026 19:27:53 -0700 Subject: [PATCH 6/6] Minor code cleanup in logging and interop headers BugzId:56 --- WebRtcInterop/Logging/InteropHResult.cpp | 6 ++--- WebRtcInterop/Media/MediaDevices.h | 12 ++++----- WebRtcInterop/Media/MediaStreamTrack.h | 2 +- WebRtcInterop/RtcDataChannel.h | 2 +- WebRtcInterop/RtcIceTransport.h | 2 +- WebRtcInterop/RtcPeerConnection.h | 34 ++++++++++++------------ 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/WebRtcInterop/Logging/InteropHResult.cpp b/WebRtcInterop/Logging/InteropHResult.cpp index d17bc2f..c08209c 100644 --- a/WebRtcInterop/Logging/InteropHResult.cpp +++ b/WebRtcInterop/Logging/InteropHResult.cpp @@ -66,7 +66,7 @@ namespace WebRtcInterop } } - void InteropHResult::ThrowIfFailed(HRESULT hr, System::String^ message) + void InteropHResult::ThrowIfFailed(HRESULT hr, String^ message) { if (SUCCEEDED(hr)) return; @@ -92,13 +92,13 @@ namespace WebRtcInterop const auto formatted = String::Format( "{0} failed. HRESULT=0x{1:X8} ({2}), Facility={3}, Code={4}, SystemMessage=\"{5}\"", operation, - static_cast(hr), + static_cast(hr), hr, HRESULT_FACILITY(hr), HRESULT_CODE(hr), FormatSystemMessage(hr)); - WebRtcNet::Logging::WebRtcLogWriterBridge::WriteInteropLog( + WebRtcLogWriterBridge::WriteInteropLog( 4, static_cast(WebRtcLogEventId::InteropHResultFailure), category, diff --git a/WebRtcInterop/Media/MediaDevices.h b/WebRtcInterop/Media/MediaDevices.h index 4fdc2d1..bed488c 100644 --- a/WebRtcInterop/Media/MediaDevices.h +++ b/WebRtcInterop/Media/MediaDevices.h @@ -19,17 +19,17 @@ namespace WebRtcInterop::Media void remove(EventHandler^ value) override { on_device_change_ -= value; } } - virtual Task^>^ EnumerateDevices() override; - virtual WebRtcNet::Media::MediaTrackSupportedConstraints^ GetSupportedConstraints() override; - virtual Task^ GetUserMedia(WebRtcNet::Media::MediaStreamConstraints^ constraints) override; + Task^>^ EnumerateDevices() override; + WebRtcNet::Media::MediaTrackSupportedConstraints^ GetSupportedConstraints() override; + Task^ GetUserMedia(WebRtcNet::Media::MediaStreamConstraints^ constraints) override; private: void RefreshKnownDevices(bool raiseEvent); - void OnDevicePoll(Object^ sender, System::Timers::ElapsedEventArgs^ args); + void OnDevicePoll(Object^ sender, Timers::ElapsedEventArgs^ args); void StopDevicePolling(); - System::Collections::Generic::Dictionary^ known_devices_; - System::Timers::Timer^ device_poll_timer_; + Dictionary^ known_devices_; + Timers::Timer^ device_poll_timer_; Object^ device_poll_gate_; EventHandler^ on_device_change_; }; diff --git a/WebRtcInterop/Media/MediaStreamTrack.h b/WebRtcInterop/Media/MediaStreamTrack.h index d1dd3ec..8999592 100644 --- a/WebRtcInterop/Media/MediaStreamTrack.h +++ b/WebRtcInterop/Media/MediaStreamTrack.h @@ -53,7 +53,7 @@ public ref class MediaStreamTrack : WebRtcNet::Media::MediaStreamTrack void ApplyConstraints(MediaTrackConstraints^ constraints) override; public: - virtual System::IntPtr GetNativeMediaStreamTrackInterface(bool throwOnDisposed) override; + System::IntPtr GetNativeMediaStreamTrackInterface(bool throwOnDisposed) override; private: webrtc::scoped_refptr* _rpMediaStreamTrackInterface; diff --git a/WebRtcInterop/RtcDataChannel.h b/WebRtcInterop/RtcDataChannel.h index 617053e..a0d6f17 100644 --- a/WebRtcInterop/RtcDataChannel.h +++ b/WebRtcInterop/RtcDataChannel.h @@ -74,7 +74,7 @@ namespace WebRtcInterop webrtc::DataChannelInterface* GetNativeDataChannelInterface(bool throwOnDisposed); protected: - virtual IntPtr GetNativeDataChannelHandle(bool throwOnDisposed) override; + IntPtr GetNativeDataChannelHandle(bool throwOnDisposed) override; internal: //Event invocation diff --git a/WebRtcInterop/RtcIceTransport.h b/WebRtcInterop/RtcIceTransport.h index 43698ef..31ea587 100644 --- a/WebRtcInterop/RtcIceTransport.h +++ b/WebRtcInterop/RtcIceTransport.h @@ -50,7 +50,7 @@ namespace WebRtcInterop webrtc::IceTransportInterface* GetNativeIceTransportInterface(bool throwOnDisposed); protected: - virtual IntPtr GetNativeIceTransportHandle(bool throwOnDisposed) override; + IntPtr GetNativeIceTransportHandle(bool throwOnDisposed) override; internal: void FireOnStateChange() { if (on_state_change_ != nullptr) on_state_change_(this, EventArgs::Empty); } diff --git a/WebRtcInterop/RtcPeerConnection.h b/WebRtcInterop/RtcPeerConnection.h index fdd9d14..829987a 100644 --- a/WebRtcInterop/RtcPeerConnection.h +++ b/WebRtcInterop/RtcPeerConnection.h @@ -72,26 +72,26 @@ namespace WebRtcInterop void remove(EventHandler^ value) override { on_data_channel_ -= value; } } - virtual Task^ CreateOffer([System::Runtime::InteropServices::Optional] RtcOfferOptions^ options) override; - virtual Task^ CreateAnswer([System::Runtime::InteropServices::Optional] RtcAnswerOptions^ options) override; - virtual Task^ SetLocalDescription([System::Runtime::InteropServices::Optional] Nullable description) override; - virtual Task^ SetRemoteDescription(RtcSessionDescription description) override; - virtual Task^ AddIceCandidate([System::Runtime::InteropServices::Optional] RtcIceCandidate^ candidate) override; - virtual void RestartIce() override; - virtual void Close() override; - virtual Task^ GetStats([System::Runtime::InteropServices::Optional] WebRtcNet::Media::MediaStreamTrack^ selector) override; + Task^ CreateOffer([System::Runtime::InteropServices::Optional] RtcOfferOptions^ options) override; + Task^ CreateAnswer([System::Runtime::InteropServices::Optional] RtcAnswerOptions^ options) override; + Task^ SetLocalDescription([System::Runtime::InteropServices::Optional] Nullable description) override; + Task^ SetRemoteDescription(RtcSessionDescription description) override; + Task^ AddIceCandidate([System::Runtime::InteropServices::Optional] RtcIceCandidate^ candidate) override; + void RestartIce() override; + void Close() override; + Task^ GetStats([System::Runtime::InteropServices::Optional] WebRtcNet::Media::MediaStreamTrack^ selector) override; - virtual IEnumerable^ GetSenders() override; - virtual IEnumerable^ GetReceivers() override; - virtual IEnumerable^ GetTransceivers() override; - virtual RtcRtpSender^ AddTrack(WebRtcNet::Media::MediaStreamTrack^ track, ... array^ streams) override; - virtual RtcRtpTransceiver^ AddTransceiver(WebRtcNet::Media::MediaStreamTrack^ track, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; - virtual RtcRtpTransceiver^ AddTransceiver(WebRtcNet::Media::MediaStreamTrackKind kind, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; - virtual void RemoveTrack(RtcRtpSender^ sender) override; - virtual RtcDataChannel^ CreateDataChannel(String^ label, [System::Runtime::InteropServices::Optional] RtcDataChannelInit^ dataChannelInit) override; + IEnumerable^ GetSenders() override; + IEnumerable^ GetReceivers() override; + IEnumerable^ GetTransceivers() override; + RtcRtpSender^ AddTrack(WebRtcNet::Media::MediaStreamTrack^ track, ... array^ streams) override; + RtcRtpTransceiver^ AddTransceiver(WebRtcNet::Media::MediaStreamTrack^ track, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; + RtcRtpTransceiver^ AddTransceiver(WebRtcNet::Media::MediaStreamTrackKind kind, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; + void RemoveTrack(RtcRtpSender^ sender) override; + RtcDataChannel^ CreateDataChannel(String^ label, [System::Runtime::InteropServices::Optional] RtcDataChannelInit^ dataChannelInit) override; protected: - virtual IntPtr GetNativePeerConnectionHandle(bool throwOnDisposed) override; + IntPtr GetNativePeerConnectionHandle(bool throwOnDisposed) override; private: void ThrowShimNotImplemented(String^ memberName);