diff --git a/Samples/Genetec Web Player/GwpDesktopPlayerSample/App.xaml b/Samples/Genetec Web Player/GwpDesktopPlayerSample/App.xaml new file mode 100644 index 0000000..97f8afe --- /dev/null +++ b/Samples/Genetec Web Player/GwpDesktopPlayerSample/App.xaml @@ -0,0 +1,8 @@ + + + + + diff --git a/Samples/Genetec Web Player/GwpDesktopPlayerSample/App.xaml.cs b/Samples/Genetec Web Player/GwpDesktopPlayerSample/App.xaml.cs new file mode 100644 index 0000000..3f2605f --- /dev/null +++ b/Samples/Genetec Web Player/GwpDesktopPlayerSample/App.xaml.cs @@ -0,0 +1,11 @@ +// Copyright 2025 Genetec Inc. +// Licensed under the Apache License, Version 2.0 + +using System.Windows; + +namespace Genetec.Dap.CodeSamples; + +public partial class App : Application +{ +} + diff --git a/Samples/Genetec Web Player/GwpDesktopPlayerSample/AssemblyInfo.cs b/Samples/Genetec Web Player/GwpDesktopPlayerSample/AssemblyInfo.cs new file mode 100644 index 0000000..1119c32 --- /dev/null +++ b/Samples/Genetec Web Player/GwpDesktopPlayerSample/AssemblyInfo.cs @@ -0,0 +1,13 @@ +// Copyright 2025 Genetec Inc. +// Licensed under the Apache License, Version 2.0 + +using System.Windows; + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/Samples/Genetec Web Player/GwpDesktopPlayerSample/GwpDesktopPlayerSample.csproj b/Samples/Genetec Web Player/GwpDesktopPlayerSample/GwpDesktopPlayerSample.csproj new file mode 100644 index 0000000..1ac0c7d --- /dev/null +++ b/Samples/Genetec Web Player/GwpDesktopPlayerSample/GwpDesktopPlayerSample.csproj @@ -0,0 +1,26 @@ + + + + WinExe + net8.0-windows + enable + enable + true + GwpDesktopPlayerSample + Genetec.Dap.CodeSamples + Sample project + Genetec Inc. + Copyright © Genetec Inc. 2025 + + + + + + + + + PreserveNewest + + + + diff --git a/Samples/Genetec Web Player/GwpDesktopPlayerSample/MainWindow.xaml b/Samples/Genetec Web Player/GwpDesktopPlayerSample/MainWindow.xaml new file mode 100644 index 0000000..dcb4e9f --- /dev/null +++ b/Samples/Genetec Web Player/GwpDesktopPlayerSample/MainWindow.xaml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/Genetec Web Player/GwpDesktopPlayerSample/MainWindow.xaml.cs b/Samples/Genetec Web Player/GwpDesktopPlayerSample/MainWindow.xaml.cs new file mode 100644 index 0000000..6d9d5a1 --- /dev/null +++ b/Samples/Genetec Web Player/GwpDesktopPlayerSample/MainWindow.xaml.cs @@ -0,0 +1,157 @@ +// Copyright 2025 Genetec Inc. +// Licensed under the Apache License, Version 2.0 + +using System; +using System.IO; +using System.Text.Json; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Microsoft.Web.WebView2.Core; + +namespace Genetec.Dap.CodeSamples; + +public partial class MainWindow : Window +{ + private static readonly Uri s_appUri = new("https://app.local/index.html"); + + private NativePlaybackConfiguration? m_playbackConfiguration; + private TokenProvider? m_tokenProvider; + + public MainWindow() + { + InitializeComponent(); +#if !DEBUG + DevToolsButton.Visibility = Visibility.Collapsed; +#endif + Loaded += OnLoaded; + Closed += OnClosed; + PreviewKeyDown += OnPreviewKeyDown; + } + + private async void OnLoaded(object sender, RoutedEventArgs e) + { + try + { + var webRoot = Path.Combine(AppContext.BaseDirectory, "wwwroot"); + m_playbackConfiguration = NativePlaybackConfiguration.LoadFromEnvironment(); + UsernameTextBox.Text = m_playbackConfiguration.Username; + PasswordBox.Password = m_playbackConfiguration.Password; + SdkCertificateTextBox.Text = m_playbackConfiguration.SdkCertificate; + m_tokenProvider = new TokenProvider(m_playbackConfiguration); + + var userDataFolder = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "GwpDesktopPlayerSample", + "WebView2Data"); + var environment = await CoreWebView2Environment.CreateAsync(userDataFolder: userDataFolder); + + await Browser.EnsureCoreWebView2Async(environment); +#if DEBUG + Browser.CoreWebView2.ServerCertificateErrorDetected += OnServerCertificateErrorDetected; +#endif + Browser.CoreWebView2.SetVirtualHostNameToFolderMapping( + "app.local", + webRoot, + CoreWebView2HostResourceAccessKind.Allow); + Browser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = true; +#if DEBUG + Browser.CoreWebView2.Settings.AreDevToolsEnabled = true; +#else + Browser.CoreWebView2.Settings.AreDevToolsEnabled = false; +#endif + Browser.CoreWebView2.Settings.IsStatusBarEnabled = true; + + var bootstrapScript = $"window.__GWP_HOST_CONFIG__ = {JsonSerializer.Serialize(new + { + mediaGatewayEndpoint = m_playbackConfiguration.MediaGatewayEndpoint, + serverVersion = m_playbackConfiguration.ServerVersion, + })};"; + await Browser.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(bootstrapScript); + + Browser.CoreWebView2.AddHostObjectToScript("tokenProvider", m_tokenProvider); + Browser.CoreWebView2.NavigationStarting += OnNavigationStarting; + Browser.CoreWebView2.NavigationCompleted += OnNavigationCompleted; + Browser.CoreWebView2.Navigate(s_appUri.ToString()); + StatusTextBlock.Text = + $"WebView2 ready. Native playback configuration loaded for {m_playbackConfiguration.MediaGatewayEndpoint}."; + } + catch (Exception exception) + { + StatusTextBlock.Text = "Failed to initialize WebView2."; + MessageBox.Show(this, exception.ToString(), "WebView2 initialization failed", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void OnNavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs e) + { + if (Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri) && uri.Host != s_appUri.Host) + { + e.Cancel = true; + } + } + + private void OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e) + { + StatusTextBlock.Text = e.IsSuccess + ? "Local GWP host page loaded. Enter a camera GUID to start playback." + : $"Navigation failed with error {e.WebErrorStatus}."; + } + + private void OnClosed(object? sender, EventArgs e) + { + (m_tokenProvider as IDisposable)?.Dispose(); + m_tokenProvider = null; + } + + private void OnServerCertificateErrorDetected(object? sender, CoreWebView2ServerCertificateErrorDetectedEventArgs e) + { + e.Action = CoreWebView2ServerCertificateErrorAction.AlwaysAllow; + + if (Uri.TryCreate(e.RequestUri, UriKind.Absolute, out var uri)) + { + StatusTextBlock.Text = $"Allowed certificate warning for development endpoint {uri.Host}."; + } + } + + private void RefreshButton_Click(object sender, RoutedEventArgs e) + { + if (Browser.CoreWebView2 is null) + { + return; + } + + Browser.Reload(); + } + + private void UsernameTextBox_TextChanged(object sender, TextChangedEventArgs e) => SyncNativeCredentials(); + + private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e) => SyncNativeCredentials(); + + private void SdkCertificateTextBox_TextChanged(object sender, TextChangedEventArgs e) => SyncNativeCredentials(); + + private void DevToolsButton_Click(object sender, RoutedEventArgs e) + { +#if DEBUG + Browser.CoreWebView2?.OpenDevToolsWindow(); +#endif + } + + private void OnPreviewKeyDown(object sender, KeyEventArgs e) + { +#if DEBUG + if (e.Key != Key.F12) + { + return; + } + + Browser.CoreWebView2?.OpenDevToolsWindow(); + e.Handled = true; +#endif + } + + private void SyncNativeCredentials() + { + m_tokenProvider?.UpdateAuthentication(UsernameTextBox.Text, PasswordBox.Password, SdkCertificateTextBox.Text); + } +} diff --git a/Samples/Genetec Web Player/GwpDesktopPlayerSample/README.md b/Samples/Genetec Web Player/GwpDesktopPlayerSample/README.md new file mode 100644 index 0000000..db6557e --- /dev/null +++ b/Samples/Genetec Web Player/GwpDesktopPlayerSample/README.md @@ -0,0 +1,89 @@ +# GWP Desktop Player Sample + +This sample demonstrates the feasible hosting model for the Genetec Web Player inside a .NET WPF application: + +- WPF hosts a `WebView2` control. +- The `WebView2` instance serves a local page from the virtual host `https://app.local`. +- The local page loads `gwp.js` directly from the target Media Gateway and runs GWP in the browser environment it expects. +- Token retrieval runs in .NET via a COM-visible `TokenProvider` exposed to JavaScript through `AddHostObjectToScript`. +- The hosted page receives non-secret bootstrap settings and requests opaque camera tokens from the native token provider. Media Gateway credentials stay in the WPF host process. + +## Run + +```powershell +dotnet run +``` + +### Native playback configuration + +The WPF host loads Media Gateway settings from environment variables before it creates the WebView: + +- `GWP_MEDIA_GATEWAY_ENDPOINT` +- `GWP_USERNAME` +- `GWP_PASSWORD` +- `GWP_SDK_CERTIFICATE` +- `GWP_SERVER_VERSION` (optional) + +If you do not set them, the sample falls back to the local development defaults: + +- Media Gateway endpoint: `https://localhost/media` +- Username: `admin` +- Password: blank +- SDK certificate: Genetec development certificate + +After startup, the WPF header lets the operator edit the username, password, and SDK certificate natively. Those values stay in the host process and become the authentication inputs used by the .NET `TokenProvider` for later token requests. + +Example: + +```powershell +$env:GWP_MEDIA_GATEWAY_ENDPOINT = 'https://localhost/media' +$env:GWP_USERNAME = 'admin' +$env:GWP_PASSWORD = '' +$env:GWP_SDK_CERTIFICATE = 'KxsD11z743Hf5Gq9mv3+5ekxzemlCiUXkTFY5ba1NOGcLCmGstt2n0zYE9NsNimv' +$env:GWP_SERVER_VERSION = '5.12.0.0' +dotnet run +``` + +## Required environment setup + +### 1. Trust the Media Gateway certificate + +If the Media Gateway certificate is self-signed or otherwise untrusted, WebView2 will fail to load `gwp.js` or connect to the gateway. + +For development (`Debug` builds only), this sample automatically allows certificate warnings for `localhost`, `127.0.0.1`, and `::1` inside both WebView2 and the .NET `TokenProvider`. These bypasses are compiled out in `Release` builds. Production deployments should use a trusted certificate. + +### 2. Allow the hosted page origin in Media Gateway CORS + +This sample uses the origin `https://app.local`. + +If strict CORS is enabled, add that origin to `MediaGateway.gconfig`: + +```xml + + + + + + +``` + +Restart the Media Gateway role after the change. + +### 3. Use a matching GWP build + +The sample loads `gwp.js` from `${mediaGatewayEndpoint}/v2/files/gwp.js` so the player version matches the Security Center version. + +## Scope and limitations + +- This sample demonstrates a feasible hosting pattern. It is not a production-ready security design. +- Media Gateway authentication is configured in the WPF host and token requests are executed by native `HttpClient`. +- Username, password, and SDK certificate can be edited in the WPF header at runtime. They never enter the browser page, but they remain operator-supplied authentication values that should be handled carefully in a real application. +- The default SDK certificate is the Genetec development certificate intended for SDK development only, until the operator overrides it in the WPF header. +- The effective trust boundary for this sample is the local `https://app.local` page plus the `gwp.js` file loaded from the configured Media Gateway. For production, move to a fully native authentication flow, for example through the Security Center SDK `Engine`, and consider how you want to version and trust the GWP asset itself. +- DevTools access and certificate bypass are gated behind `#if DEBUG` and are compiled out of `Release` builds. +- A Content Security Policy meta tag restricts script sources, connections, and media to `self`, `https:`, `wss:`, and `blob:`. +- The WPF host blocks top-level navigations away from the `app.local` virtual host. +- Player startup is cancellable. Clicking Stop during script load or session establishment cancels the in-flight start and cleans up any partially created player. +- WebView2 user data is stored under `%LOCALAPPDATA%\GwpDesktopPlayerSample\WebView2Data`. +- Browser autoplay rules apply to audio. +- Video rendering and overlays remain in the HTML layer, not the WPF visual tree. diff --git a/Samples/Genetec Web Player/GwpDesktopPlayerSample/TokenProvider.cs b/Samples/Genetec Web Player/GwpDesktopPlayerSample/TokenProvider.cs new file mode 100644 index 0000000..78ad7e3 --- /dev/null +++ b/Samples/Genetec Web Player/GwpDesktopPlayerSample/TokenProvider.cs @@ -0,0 +1,158 @@ +// Copyright 2025 Genetec Inc. +// Licensed under the Apache License, Version 2.0 + +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Genetec.Dap.CodeSamples; + +internal sealed class NativePlaybackConfiguration +{ + private const string DefaultMediaGatewayEndpoint = "https://localhost/media"; + private const string DefaultUsername = "admin"; + private const string DefaultSdkCertificate = "KxsD11z743Hf5Gq9mv3+5ekxzemlCiUXkTFY5ba1NOGcLCmGstt2n0zYE9NsNimv"; + + public string MediaGatewayEndpoint { get; } + + public string Username { get; } + + public string Password { get; } + + public string SdkCertificate { get; } + + public string? ServerVersion { get; } + + private NativePlaybackConfiguration( + string mediaGatewayEndpoint, + string username, + string password, + string sdkCertificate, + string? serverVersion) + { + MediaGatewayEndpoint = NormalizeEndpoint(mediaGatewayEndpoint); + Username = RequireValue(username, nameof(username)); + Password = password; + SdkCertificate = RequireValue(sdkCertificate, nameof(sdkCertificate)); + ServerVersion = string.IsNullOrWhiteSpace(serverVersion) ? null : serverVersion.Trim(); + } + + public static NativePlaybackConfiguration LoadFromEnvironment() => + new( + mediaGatewayEndpoint: ReadValue("GWP_MEDIA_GATEWAY_ENDPOINT", DefaultMediaGatewayEndpoint), + username: ReadValue("GWP_USERNAME", DefaultUsername), + password: Environment.GetEnvironmentVariable("GWP_PASSWORD") ?? string.Empty, + sdkCertificate: ReadValue("GWP_SDK_CERTIFICATE", DefaultSdkCertificate), + serverVersion: Environment.GetEnvironmentVariable("GWP_SERVER_VERSION")); + + private static string NormalizeEndpoint(string mediaGatewayEndpoint) + { + var candidate = RequireValue(mediaGatewayEndpoint, nameof(mediaGatewayEndpoint)).TrimEnd('/'); + if (!Uri.TryCreate(candidate, UriKind.Absolute, out _)) + { + throw new InvalidOperationException( + $"The configured Media Gateway endpoint '{candidate}' is not a valid absolute URI."); + } + + return candidate; + } + + private static string ReadValue(string environmentVariableName, string fallbackValue) + { + var candidate = Environment.GetEnvironmentVariable(environmentVariableName); + return string.IsNullOrWhiteSpace(candidate) ? fallbackValue : candidate.Trim(); + } + + private static string RequireValue(string value, string parameterName) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new InvalidOperationException($"The configured value for '{parameterName}' cannot be empty."); + } + + return value.Trim(); + } +} + +[ClassInterface(ClassInterfaceType.AutoDual)] +[ComVisible(true)] +public sealed class TokenProvider : IDisposable +{ + private readonly object m_credentialSync = new(); + private readonly NativePlaybackConfiguration m_configuration; + private readonly HttpClient m_httpClient; + private bool m_hasUsername; + private bool m_hasSdkCertificate; + private string m_authorizationParameter = string.Empty; + + internal TokenProvider(NativePlaybackConfiguration configuration) + { + m_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + + var handler = new HttpClientHandler(); +#if DEBUG + handler.ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; +#endif + m_httpClient = new HttpClient(handler); + UpdateAuthentication(m_configuration.Username, m_configuration.Password, m_configuration.SdkCertificate); + } + + internal void UpdateAuthentication(string username, string password, string sdkCertificate) + { + var normalizedUsername = username?.Trim() ?? string.Empty; + var normalizedPassword = password ?? string.Empty; + var normalizedSdkCertificate = sdkCertificate?.Trim() ?? string.Empty; + var authorizationParameter = Convert.ToBase64String( + Encoding.UTF8.GetBytes($"{normalizedUsername};{normalizedSdkCertificate}:{normalizedPassword}")); + + lock (m_credentialSync) + { + m_hasUsername = normalizedUsername.Length > 0; + m_hasSdkCertificate = normalizedSdkCertificate.Length > 0; + m_authorizationParameter = authorizationParameter; + } + } + + public async Task GetToken(string cameraId) + { + if (string.IsNullOrWhiteSpace(cameraId)) + { + throw new ArgumentException("A camera GUID is required.", nameof(cameraId)); + } + + string authorizationParameter; + bool hasUsername; + bool hasSdkCertificate; + lock (m_credentialSync) + { + hasUsername = m_hasUsername; + hasSdkCertificate = m_hasSdkCertificate; + authorizationParameter = m_authorizationParameter; + } + + if (!hasUsername) + { + throw new InvalidOperationException("A native username is required before requesting a token."); + } + + if (!hasSdkCertificate) + { + throw new InvalidOperationException("A native SDK certificate is required before requesting a token."); + } + + using var request = new HttpRequestMessage( + HttpMethod.Get, + $"{m_configuration.MediaGatewayEndpoint}/v2/token/{Uri.EscapeDataString(cameraId.Trim())}"); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", authorizationParameter); + + using var response = await m_httpClient.SendAsync(request).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } + + void IDisposable.Dispose() => m_httpClient.Dispose(); +} diff --git a/Samples/Genetec Web Player/GwpDesktopPlayerSample/wwwroot/index.html b/Samples/Genetec Web Player/GwpDesktopPlayerSample/wwwroot/index.html new file mode 100644 index 0000000..b5fe91d --- /dev/null +++ b/Samples/Genetec Web Player/GwpDesktopPlayerSample/wwwroot/index.html @@ -0,0 +1,554 @@ + + + + + + + GWP Desktop Player Sample + + + + + + + This page runs with the origin shown below. Add that origin to the Media Gateway allowed origins list when strict CORS is enabled. + + + + + + Media Gateway endpoint + + + + Server version + + + + Camera GUID + + + + + + Start Player + Stop + Live + Pause + Resume + Toggle Audio + + + + + + + Idle + + + + + + + + + +
This page runs with the origin shown below. Add that origin to the Media Gateway allowed origins list when strict CORS is enabled.