Compare commits
No commits in common. "ff7e949466f1cca19f42242a0cb1e36b0ba4fe81" and "01ef4250d7147d124792521dcac2aa010ee5083e" have entirely different histories.
ff7e949466
...
01ef4250d7
12 changed files with 32 additions and 525 deletions
|
|
@ -58,15 +58,6 @@
|
||||||
<!-- ARPNOMODIFY is set by WixUI_InstallDir; don't redeclare. -->
|
<!-- ARPNOMODIFY is set by WixUI_InstallDir; don't redeclare. -->
|
||||||
<Property Id="ARPNOREPAIR" Value="1" />
|
<Property Id="ARPNOREPAIR" Value="1" />
|
||||||
|
|
||||||
<!--
|
|
||||||
ARP icon — references the same .ico the WPF host uses. WiX requires the
|
|
||||||
icon resource to live next to the wxs OR be reachable at build time;
|
|
||||||
we point at the published copy under src/TeamsISO.App/Assets so the icon
|
|
||||||
embedded in the MSI matches the icon in the running exe.
|
|
||||||
-->
|
|
||||||
<Icon Id="TeamsISOIcon" SourceFile="$(var.AssetsDir)teamsiso.ico" />
|
|
||||||
<Property Id="ARPPRODUCTICON" Value="TeamsISOIcon" />
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
NDI Runtime detection. We check for NDI_RUNTIME_DIR_V6 in the system
|
NDI Runtime detection. We check for NDI_RUNTIME_DIR_V6 in the system
|
||||||
environment block. Missing → warn during install, don't block. The
|
environment block. Missing → warn during install, don't block. The
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
<PublishDir>$(MSBuildThisFileDirectory)..\publish\TeamsISO\</PublishDir>
|
<PublishDir>$(MSBuildThisFileDirectory)..\publish\TeamsISO\</PublishDir>
|
||||||
|
|
||||||
<!-- Pass MSBuild values into WiX preprocessor. -->
|
<!-- Pass MSBuild values into WiX preprocessor. -->
|
||||||
<DefineConstants>PublishDir=$(PublishDir);AssetsDir=$(MSBuildThisFileDirectory)..\src\TeamsISO.App\Assets\</DefineConstants>
|
<DefineConstants>PublishDir=$(PublishDir)</DefineConstants>
|
||||||
|
|
||||||
<!-- Code-signing hook (set externally for release builds; left empty for dev). -->
|
<!-- Code-signing hook (set externally for release builds; left empty for dev). -->
|
||||||
<SignOutput Condition=" '$(SignOutput)' == '' ">false</SignOutput>
|
<SignOutput Condition=" '$(SignOutput)' == '' ">false</SignOutput>
|
||||||
|
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
<Window x:Class="TeamsISO.App.AboutWindow"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
|
||||||
Title="About TeamsISO"
|
|
||||||
Icon="/Assets/teamsiso.ico"
|
|
||||||
Width="460" Height="500"
|
|
||||||
WindowStartupLocation="CenterOwner"
|
|
||||||
WindowStyle="None"
|
|
||||||
ResizeMode="NoResize"
|
|
||||||
Background="{DynamicResource Wd.Canvas}"
|
|
||||||
UseLayoutRounding="True"
|
|
||||||
TextOptions.TextFormattingMode="Ideal"
|
|
||||||
TextOptions.TextRenderingMode="ClearType">
|
|
||||||
|
|
||||||
<shell:WindowChrome.WindowChrome>
|
|
||||||
<shell:WindowChrome
|
|
||||||
CaptionHeight="32"
|
|
||||||
ResizeBorderThickness="0"
|
|
||||||
CornerRadius="0"
|
|
||||||
GlassFrameThickness="0"
|
|
||||||
UseAeroCaptionButtons="False"/>
|
|
||||||
</shell:WindowChrome.WindowChrome>
|
|
||||||
|
|
||||||
<Border BorderBrush="{DynamicResource Wd.Border}" BorderThickness="1">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- Caption -->
|
|
||||||
<Grid Grid.Row="0">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Text="About TeamsISO"
|
|
||||||
Style="{StaticResource Wd.Text.Caption}"
|
|
||||||
Margin="20,12,0,0"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
<Button Grid.Column="1"
|
|
||||||
Style="{StaticResource Wd.Button.CaptionClose}"
|
|
||||||
Click="OnClose"
|
|
||||||
shell:WindowChrome.IsHitTestVisibleInChrome="True">
|
|
||||||
<Path Data="M 0,0 L 10,10 M 10,0 L 0,10"
|
|
||||||
Stroke="{DynamicResource Wd.Text.Primary}"
|
|
||||||
StrokeThickness="1.2"
|
|
||||||
Width="10" Height="10"
|
|
||||||
Stretch="None"/>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<!-- Body -->
|
|
||||||
<StackPanel Grid.Row="1"
|
|
||||||
Margin="32,16,32,16"
|
|
||||||
VerticalAlignment="Top">
|
|
||||||
<Image Source="/Assets/dragon-mark.png"
|
|
||||||
Width="80" Height="80"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Margin="0,0,0,16"
|
|
||||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
|
||||||
|
|
||||||
<TextBlock Text="TeamsISO"
|
|
||||||
Style="{StaticResource Wd.Text.Title}"
|
|
||||||
FontSize="28"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Per-participant NDI ISO Controller for Microsoft Teams"
|
|
||||||
Style="{StaticResource Wd.Text.Subtle}"
|
|
||||||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
TextAlignment="Center"
|
|
||||||
Margin="0,4,0,0"
|
|
||||||
TextWrapping="Wrap"/>
|
|
||||||
|
|
||||||
<Border Style="{StaticResource Wd.Card}"
|
|
||||||
Margin="0,20,0,0"
|
|
||||||
Padding="16">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Version"
|
|
||||||
Style="{StaticResource Wd.Text.Subtle}"
|
|
||||||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
|
||||||
Margin="0,0,16,4"/>
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1"
|
|
||||||
x:Name="VersionText"
|
|
||||||
Style="{StaticResource Wd.Text.Mono}"
|
|
||||||
FontSize="11"
|
|
||||||
Foreground="{DynamicResource Wd.Text.Primary}"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Margin="0,0,0,4"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text=".NET"
|
|
||||||
Style="{StaticResource Wd.Text.Subtle}"
|
|
||||||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
|
||||||
Margin="0,0,16,4"/>
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="1"
|
|
||||||
x:Name="RuntimeText"
|
|
||||||
Style="{StaticResource Wd.Text.Mono}"
|
|
||||||
FontSize="11"
|
|
||||||
Foreground="{DynamicResource Wd.Text.Primary}"
|
|
||||||
Margin="0,0,0,4"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="OS"
|
|
||||||
Style="{StaticResource Wd.Text.Subtle}"
|
|
||||||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
|
||||||
Margin="0,0,16,4"/>
|
|
||||||
<TextBlock Grid.Row="2" Grid.Column="1"
|
|
||||||
x:Name="OsText"
|
|
||||||
Style="{StaticResource Wd.Text.Mono}"
|
|
||||||
FontSize="11"
|
|
||||||
Foreground="{DynamicResource Wd.Text.Primary}"
|
|
||||||
Margin="0,0,0,4"/>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="NDI runtime"
|
|
||||||
Style="{StaticResource Wd.Text.Subtle}"
|
|
||||||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
|
||||||
Margin="0,0,16,0"/>
|
|
||||||
<TextBlock Grid.Row="3" Grid.Column="1"
|
|
||||||
x:Name="NdiText"
|
|
||||||
Style="{StaticResource Wd.Text.Mono}"
|
|
||||||
FontSize="11"
|
|
||||||
Foreground="{DynamicResource Wd.Text.Primary}"
|
|
||||||
TextWrapping="Wrap"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<Border Grid.Row="2"
|
|
||||||
BorderBrush="{DynamicResource Wd.Border}"
|
|
||||||
BorderThickness="0,1,0,0"
|
|
||||||
Padding="20,12">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<TextBlock Style="{StaticResource Wd.Text.Mono}"
|
|
||||||
Foreground="{DynamicResource Wd.Text.Tertiary}"
|
|
||||||
VerticalAlignment="Center">
|
|
||||||
<Hyperlink x:Name="WebsiteLink"
|
|
||||||
Foreground="{DynamicResource Wd.Accent.Cyan}"
|
|
||||||
TextDecorations="None"
|
|
||||||
Click="OnWebsiteClick">
|
|
||||||
wilddragon.net
|
|
||||||
</Hyperlink>
|
|
||||||
<Run Text=" · © Wild Dragon LLC"/>
|
|
||||||
</TextBlock>
|
|
||||||
<Button Grid.Column="1"
|
|
||||||
Style="{StaticResource Wd.Button.Ghost}"
|
|
||||||
Content="Close"
|
|
||||||
Click="OnClose"
|
|
||||||
MinWidth="80"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</Window>
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using TeamsISO.Engine.NdiInterop;
|
|
||||||
|
|
||||||
namespace TeamsISO.App;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Modal "About" dialog. Surfaces enough info that a user filing a support ticket
|
|
||||||
/// can paste version + NDI runtime + OS in a single screenshot.
|
|
||||||
/// </summary>
|
|
||||||
public partial class AboutWindow : Window
|
|
||||||
{
|
|
||||||
public AboutWindow()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
PopulateText();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PopulateText()
|
|
||||||
{
|
|
||||||
var asm = typeof(App).Assembly;
|
|
||||||
var info = asm.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
|
|
||||||
?? asm.GetName().Version?.ToString()
|
|
||||||
?? "unknown";
|
|
||||||
VersionText.Text = info;
|
|
||||||
RuntimeText.Text = $".NET {Environment.Version}";
|
|
||||||
OsText.Text = Environment.OSVersion.ToString();
|
|
||||||
NdiText.Text = TryGetNdiVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
private static string TryGetNdiVersion()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var interop = new NdiInteropPInvoke(
|
|
||||||
Microsoft.Extensions.Logging.Abstractions.NullLogger<NdiInteropPInvoke>.Instance);
|
|
||||||
return interop.GetRuntimeVersion();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return $"not initialized ({ex.Message})";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Open the company site in the default browser. We intentionally use the
|
|
||||||
/// shell's URL handler rather than a tab inside the app — this is a
|
|
||||||
/// "tell me more" link, not a workflow.
|
|
||||||
/// </summary>
|
|
||||||
private void OnWebsiteClick(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Process.Start(new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "https://wilddragon.net",
|
|
||||||
UseShellExecute = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// best-effort; if shell launch fails the click is a no-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
|
|
@ -5,7 +5,6 @@
|
||||||
xmlns:conv="clr-namespace:TeamsISO.App.Converters"
|
xmlns:conv="clr-namespace:TeamsISO.App.Converters"
|
||||||
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||||||
Title="TeamsISO"
|
Title="TeamsISO"
|
||||||
Icon="/Assets/teamsiso.ico"
|
|
||||||
Height="780" Width="1280"
|
Height="780" Width="1280"
|
||||||
MinHeight="640" MinWidth="1080"
|
MinHeight="640" MinWidth="1080"
|
||||||
Background="{DynamicResource Wd.Canvas}"
|
Background="{DynamicResource Wd.Canvas}"
|
||||||
|
|
@ -53,23 +52,14 @@
|
||||||
BorderThickness="0,0,1,0">
|
BorderThickness="0,0,1,0">
|
||||||
<DockPanel LastChildFill="False">
|
<DockPanel LastChildFill="False">
|
||||||
|
|
||||||
<!-- Wild Dragon mark — real logo from wilddragon.net. Clickable: opens About.
|
<!-- Wild Dragon mark — real logo from wilddragon.net -->
|
||||||
Lives inside the chromeless title bar's drag region, so we opt into
|
<Image DockPanel.Dock="Top"
|
||||||
hit-testing so clicks land on the button rather than starting a drag. -->
|
Source="/Assets/dragon-mark.png"
|
||||||
<Button DockPanel.Dock="Top"
|
Width="40" Height="40"
|
||||||
Style="{StaticResource Wd.Button.RailIcon}"
|
Margin="0,18,0,4"
|
||||||
Width="48" Height="56"
|
HorizontalAlignment="Center"
|
||||||
Margin="0,12,0,4"
|
RenderOptions.BitmapScalingMode="HighQuality"
|
||||||
Click="OnAboutClick"
|
ToolTip="Wild Dragon"/>
|
||||||
shell:WindowChrome.IsHitTestVisibleInChrome="True"
|
|
||||||
ToolTip="About TeamsISO">
|
|
||||||
<StackPanel>
|
|
||||||
<Image Source="/Assets/dragon-mark.png"
|
|
||||||
Width="32" Height="32"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<TextBlock DockPanel.Dock="Top"
|
<TextBlock DockPanel.Dock="Top"
|
||||||
Text="Wild Dragon"
|
Text="Wild Dragon"
|
||||||
|
|
@ -77,7 +67,7 @@
|
||||||
Foreground="{DynamicResource Wd.Accent.Cyan}"
|
Foreground="{DynamicResource Wd.Accent.Cyan}"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
FontSize="9"
|
FontSize="9"
|
||||||
Margin="0,0,0,12"/>
|
Margin="0,0,0,16"/>
|
||||||
|
|
||||||
<!-- Divider -->
|
<!-- Divider -->
|
||||||
<Border DockPanel.Dock="Top"
|
<Border DockPanel.Dock="Top"
|
||||||
|
|
@ -359,34 +349,29 @@
|
||||||
<StackPanel VerticalAlignment="Center">
|
<StackPanel VerticalAlignment="Center">
|
||||||
<TextBlock Text="{Binding SourceMachine}"
|
<TextBlock Text="{Binding SourceMachine}"
|
||||||
Style="{StaticResource Wd.Text.Mono}"/>
|
Style="{StaticResource Wd.Text.Mono}"/>
|
||||||
<TextBlock Style="{StaticResource Wd.Text.Mono}"
|
<TextBlock Text="{Binding IncomingResolution}"
|
||||||
|
Style="{StaticResource Wd.Text.Mono}"
|
||||||
FontSize="10"
|
FontSize="10"
|
||||||
Foreground="{DynamicResource Wd.Text.Tertiary}">
|
Foreground="{DynamicResource Wd.Text.Tertiary}"/>
|
||||||
<Run Text="{Binding IncomingResolution, Mode=OneWay}"/>
|
|
||||||
<Run Text=" · "/>
|
|
||||||
<Run Text="{Binding IncomingFps, Mode=OneWay}"/>
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
<DataGridTemplateColumn Header="Live" Width="120">
|
<DataGridTemplateColumn Header="Live" Width="100">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<StackPanel VerticalAlignment="Center">
|
<StackPanel VerticalAlignment="Center">
|
||||||
<TextBlock Style="{StaticResource Wd.Text.Mono}"
|
<TextBlock Style="{StaticResource Wd.Text.Mono}"
|
||||||
FontSize="11">
|
FontSize="11">
|
||||||
<Run Text="↓ "/>
|
<Run Text="↓ "/>
|
||||||
<Run Text="{Binding FramesIn, Mode=OneWay}"/>
|
<Run Text="{Binding FramesIn}"/>
|
||||||
<Run Text=" ↑ "/>
|
|
||||||
<Run Text="{Binding FramesOut, Mode=OneWay}"/>
|
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<TextBlock Style="{StaticResource Wd.Text.Mono}"
|
<TextBlock Style="{StaticResource Wd.Text.Mono}"
|
||||||
FontSize="10"
|
FontSize="10"
|
||||||
Foreground="{DynamicResource Wd.Accent.Coral}">
|
Foreground="{DynamicResource Wd.Text.Tertiary}">
|
||||||
<Run Text="drop "/>
|
<Run Text="↑ "/>
|
||||||
<Run Text="{Binding FramesDropped, Mode=OneWay}"/>
|
<Run Text="{Binding FramesOut}"/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
|
||||||
|
|
@ -29,43 +29,13 @@ public partial class MainWindow : Window
|
||||||
/// <summary>Custom close button.</summary>
|
/// <summary>Custom close button.</summary>
|
||||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
||||||
|
|
||||||
/// <summary>Opens the About dialog — version, NDI runtime, build SHA.</summary>
|
|
||||||
private void OnAboutClick(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var about = new AboutWindow { Owner = this };
|
|
||||||
about.ShowDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Toggle behavior: if Teams is already running, ask to stop it; otherwise
|
/// First step toward the Embedded-Teams roadmap (Phase E.1) — launches the MS
|
||||||
/// launch via TeamsLauncher's fallback chain. First step toward the
|
/// Teams desktop client as a subprocess so the operator doesn't have to switch
|
||||||
/// Embedded-Teams roadmap (Phase E.1).
|
/// apps to start a meeting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnLaunchTeamsClick(object sender, RoutedEventArgs e)
|
private void OnLaunchTeamsClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (TeamsLauncher.IsRunning())
|
|
||||||
{
|
|
||||||
var confirm = MessageBox.Show(
|
|
||||||
"Microsoft Teams is currently running.\n\nClose all Teams windows now?",
|
|
||||||
"TeamsISO — Stop Teams",
|
|
||||||
MessageBoxButton.YesNo,
|
|
||||||
MessageBoxImage.Question);
|
|
||||||
if (confirm != MessageBoxResult.Yes) return;
|
|
||||||
|
|
||||||
var asked = TeamsLauncher.StopAll();
|
|
||||||
if (TeamsLauncher.IsRunning())
|
|
||||||
{
|
|
||||||
MessageBox.Show(
|
|
||||||
asked == 0
|
|
||||||
? "No Teams windows responded to close."
|
|
||||||
: $"Sent close to {asked} Teams window(s); some may still be exiting.",
|
|
||||||
"TeamsISO — Stop Teams",
|
|
||||||
MessageBoxButton.OK,
|
|
||||||
MessageBoxImage.Information);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TeamsLauncher.TryLaunch(out var error))
|
if (!TeamsLauncher.TryLaunch(out var error))
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
|
|
|
||||||
|
|
@ -22,24 +22,6 @@ namespace TeamsISO.App.Services;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class TeamsLauncher
|
public static class TeamsLauncher
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Heuristic process-name candidates we'll consider as "the Teams process" when
|
|
||||||
/// the rail toggle wants to find a running instance. New MSTeams comes first.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly string[] TeamsProcessNames =
|
|
||||||
{
|
|
||||||
"ms-teams", // new MSTeams binary basename
|
|
||||||
"msteams", // alternate basename observed on some installs
|
|
||||||
"Teams", // classic Teams desktop client
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// True if any process matching the known Teams binary basenames is running.
|
|
||||||
/// Used by the rail to decide whether to show "Launch Teams" vs "Stop Teams".
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsRunning() =>
|
|
||||||
TeamsProcessNames.Any(n => Process.GetProcessesByName(n).Length > 0);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Launches Teams. Returns true if a launch was started successfully (the
|
/// Launches Teams. Returns true if a launch was started successfully (the
|
||||||
/// process may take a few seconds to actually appear). False if every
|
/// process may take a few seconds to actually appear). False if every
|
||||||
|
|
@ -87,41 +69,6 @@ public static class TeamsLauncher
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Asks every running Teams process to close gracefully via WM_CLOSE
|
|
||||||
/// (CloseMainWindow). Returns the count of processes that exited cleanly within
|
|
||||||
/// <paramref name="gracePeriod"/>. Stragglers are NOT force-killed — Teams' own
|
|
||||||
/// "are you sure" prompt may legitimately keep a process alive briefly, and we
|
|
||||||
/// don't want to nuke the user's call mid-transition.
|
|
||||||
/// </summary>
|
|
||||||
public static int StopAll(TimeSpan? gracePeriod = null)
|
|
||||||
{
|
|
||||||
var grace = gracePeriod ?? TimeSpan.FromSeconds(3);
|
|
||||||
var deadline = DateTime.UtcNow + grace;
|
|
||||||
var asked = 0;
|
|
||||||
foreach (var name in TeamsProcessNames)
|
|
||||||
{
|
|
||||||
foreach (var p in Process.GetProcessesByName(name))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (p.HasExited) { p.Dispose(); continue; }
|
|
||||||
if (p.MainWindowHandle != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
p.CloseMainWindow();
|
|
||||||
asked++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { /* defensive: process may have died between enumeration and signal */ }
|
|
||||||
finally { p.Dispose(); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Best-effort wait so the rail can flip its icon promptly.
|
|
||||||
while (DateTime.UtcNow < deadline && IsRunning())
|
|
||||||
Thread.Sleep(150);
|
|
||||||
return asked;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryStart(string target, bool useShell)
|
private static bool TryStart(string target, bool useShell)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<RootNamespace>TeamsISO.App</RootNamespace>
|
<RootNamespace>TeamsISO.App</RootNamespace>
|
||||||
<AssemblyName>TeamsISO</AssemblyName>
|
<AssemblyName>TeamsISO</AssemblyName>
|
||||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
<ApplicationIcon>Assets\teamsiso.ico</ApplicationIcon>
|
<ApplicationIcon></ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Resource Include="Assets\dragon-mark.png" />
|
<Resource Include="Assets\dragon-mark.png" />
|
||||||
<Resource Include="Assets\wild-dragon-wordmark.png" />
|
<Resource Include="Assets\wild-dragon-wordmark.png" />
|
||||||
<Resource Include="Assets\teamsiso.ico" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,7 @@ public sealed class ParticipantViewModel : ObservableObject
|
||||||
|
|
||||||
private long _framesIn;
|
private long _framesIn;
|
||||||
private long _framesOut;
|
private long _framesOut;
|
||||||
private long _framesDropped;
|
|
||||||
private string _incomingResolution = "—";
|
private string _incomingResolution = "—";
|
||||||
private string _incomingFps = "—";
|
|
||||||
|
|
||||||
/// <summary>Number of frames the receiver has captured so far.</summary>
|
/// <summary>Number of frames the receiver has captured so far.</summary>
|
||||||
public long FramesIn { get => _framesIn; set => SetField(ref _framesIn, value); }
|
public long FramesIn { get => _framesIn; set => SetField(ref _framesIn, value); }
|
||||||
|
|
@ -48,31 +46,17 @@ public sealed class ParticipantViewModel : ObservableObject
|
||||||
/// <summary>Number of frames the sender has emitted so far.</summary>
|
/// <summary>Number of frames the sender has emitted so far.</summary>
|
||||||
public long FramesOut { get => _framesOut; set => SetField(ref _framesOut, value); }
|
public long FramesOut { get => _framesOut; set => SetField(ref _framesOut, value); }
|
||||||
|
|
||||||
/// <summary>Frames dropped by the closest-frame strategy when the receiver outpaces the processor.</summary>
|
|
||||||
public long FramesDropped { get => _framesDropped; set => SetField(ref _framesDropped, value); }
|
|
||||||
|
|
||||||
/// <summary>Source resolution as "WxH", or em-dash when no frames have been seen yet.</summary>
|
/// <summary>Source resolution as "WxH", or em-dash when no frames have been seen yet.</summary>
|
||||||
public string IncomingResolution { get => _incomingResolution; set => SetField(ref _incomingResolution, value); }
|
public string IncomingResolution { get => _incomingResolution; set => SetField(ref _incomingResolution, value); }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Live incoming framerate as "59.94 fps", or em-dash when fewer than 2 frames
|
|
||||||
/// have been observed since the pipeline started. Computed in the engine via a
|
|
||||||
/// 30-frame moving window.
|
|
||||||
/// </summary>
|
|
||||||
public string IncomingFps { get => _incomingFps; set => SetField(ref _incomingFps, value); }
|
|
||||||
|
|
||||||
/// <summary>Updates the live stats display from a controller-side snapshot.</summary>
|
/// <summary>Updates the live stats display from a controller-side snapshot.</summary>
|
||||||
public void UpdateStats(IsoHealthStats stats)
|
public void UpdateStats(IsoHealthStats stats)
|
||||||
{
|
{
|
||||||
FramesIn = stats.FramesIn;
|
FramesIn = stats.FramesIn;
|
||||||
FramesOut = stats.FramesOut;
|
FramesOut = stats.FramesOut;
|
||||||
FramesDropped = stats.FramesDropped;
|
|
||||||
IncomingResolution = stats.IncomingWidth > 0 && stats.IncomingHeight > 0
|
IncomingResolution = stats.IncomingWidth > 0 && stats.IncomingHeight > 0
|
||||||
? $"{stats.IncomingWidth}×{stats.IncomingHeight}"
|
? $"{stats.IncomingWidth}×{stats.IncomingHeight}"
|
||||||
: "—";
|
: "—";
|
||||||
IncomingFps = stats.IncomingFps > 0
|
|
||||||
? $"{stats.IncomingFps:0.0} fps"
|
|
||||||
: "—";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsProcessing
|
public bool IsProcessing
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,6 @@ using SysConsole = System.Console;
|
||||||
/// teamsiso-console --list-sources # diagnostic: print every raw NDI source string visible
|
/// teamsiso-console --list-sources # diagnostic: print every raw NDI source string visible
|
||||||
/// on the network for ~5s, then exit. Useful for debugging
|
/// on the network for ~5s, then exit. Useful for debugging
|
||||||
/// why expected Teams sources aren't being classified.
|
/// why expected Teams sources aren't being classified.
|
||||||
/// teamsiso-console --version # print engine version, NDI runtime version, exit codes,
|
|
||||||
/// then exit 0. Useful for support requests.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Program
|
public static class Program
|
||||||
{
|
{
|
||||||
|
|
@ -35,14 +33,6 @@ public static class Program
|
||||||
{
|
{
|
||||||
var enableAll = args.Contains("--enable-all", StringComparer.OrdinalIgnoreCase);
|
var enableAll = args.Contains("--enable-all", StringComparer.OrdinalIgnoreCase);
|
||||||
var listSources = args.Contains("--list-sources", StringComparer.OrdinalIgnoreCase);
|
var listSources = args.Contains("--list-sources", StringComparer.OrdinalIgnoreCase);
|
||||||
var version = args.Contains("--version", StringComparer.OrdinalIgnoreCase)
|
|
||||||
|| args.Contains("-v", StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
if (version)
|
|
||||||
{
|
|
||||||
return PrintVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
using var loggerFactory = EngineLogging.CreateConsole(LogLevel.Information);
|
using var loggerFactory = EngineLogging.CreateConsole(LogLevel.Information);
|
||||||
var logger = loggerFactory.CreateLogger("TeamsISO.Console");
|
var logger = loggerFactory.CreateLogger("TeamsISO.Console");
|
||||||
|
|
||||||
|
|
@ -146,54 +136,6 @@ public static class Program
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prints version + diagnostic info. Always exits 0; the strings are intended to
|
|
||||||
/// be pasted into a support ticket.
|
|
||||||
/// </summary>
|
|
||||||
private static int PrintVersion()
|
|
||||||
{
|
|
||||||
var asm = typeof(Program).Assembly;
|
|
||||||
var asmVersion = asm.GetName().Version?.ToString() ?? "unknown";
|
|
||||||
var info = asm.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false)
|
|
||||||
.OfType<System.Reflection.AssemblyInformationalVersionAttribute>()
|
|
||||||
.FirstOrDefault()?.InformationalVersion ?? asmVersion;
|
|
||||||
|
|
||||||
SysConsole.WriteLine($"TeamsISO Console");
|
|
||||||
SysConsole.WriteLine($" Version: {info}");
|
|
||||||
SysConsole.WriteLine($" Engine: {asmVersion} (TeamsISO.Engine + TeamsISO.Engine.NdiInterop)");
|
|
||||||
SysConsole.WriteLine($" Runtime: .NET {Environment.Version}");
|
|
||||||
SysConsole.WriteLine($" OS: {Environment.OSVersion}");
|
|
||||||
SysConsole.WriteLine();
|
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var lf = EngineLogging.CreateConsole(LogLevel.Warning);
|
|
||||||
using var interop = new NdiInteropPInvoke(lf.CreateLogger<NdiInteropPInvoke>());
|
|
||||||
SysConsole.WriteLine($" NDI runtime: {interop.GetRuntimeVersion()}");
|
|
||||||
SysConsole.WriteLine($" Expects prefix: {NdiVersion.ExpectedRuntimeVersionPrefix}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
SysConsole.WriteLine($" NDI runtime: NOT INITIALIZED ({ex.Message})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SysConsole.WriteLine($" NDI runtime: requires Windows");
|
|
||||||
}
|
|
||||||
|
|
||||||
SysConsole.WriteLine();
|
|
||||||
SysConsole.WriteLine($"Exit codes:");
|
|
||||||
SysConsole.WriteLine($" 0 clean exit");
|
|
||||||
SysConsole.WriteLine($" 1 not running on Windows");
|
|
||||||
SysConsole.WriteLine($" 2 NDI runtime initialization failed (install from https://ndi.video/tools/)");
|
|
||||||
SysConsole.WriteLine();
|
|
||||||
SysConsole.WriteLine($"Wild Dragon LLC · https://wilddragon.net");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Diagnostic mode: enumerates every raw NDI source string visible to the local
|
/// Diagnostic mode: enumerates every raw NDI source string visible to the local
|
||||||
/// NDI finder for ~5 seconds, prints each unique one, then exits. Bypasses the
|
/// NDI finder for ~5 seconds, prints each unique one, then exits. Bypasses the
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,10 @@ public sealed class IsoPipeline : IAsyncDisposable
|
||||||
private Task? _supervisorTask;
|
private Task? _supervisorTask;
|
||||||
private int _consecutiveFailures;
|
private int _consecutiveFailures;
|
||||||
|
|
||||||
// Refs to the currently-live receiver, sender, and frame processor, set by the
|
// Refs to the currently-live receiver and sender, set by the inner loop on each
|
||||||
// inner loop on each restart. Reads via Volatile.Read are safe from any thread
|
// restart. Reads via Volatile.Read are safe from any thread (UI's stats poll).
|
||||||
// (UI's stats poll).
|
|
||||||
private NdiReceiver? _liveReceiver;
|
private NdiReceiver? _liveReceiver;
|
||||||
private NdiSender? _liveSender;
|
private NdiSender? _liveSender;
|
||||||
private FrameProcessor? _liveProcessor;
|
|
||||||
|
|
||||||
// Last-frame metadata, snapshotted out of the RawFrame on capture so we don't
|
// Last-frame metadata, snapshotted out of the RawFrame on capture so we don't
|
||||||
// hold a reference to the frame's pixel buffer past its useful life. Two ints
|
// hold a reference to the frame's pixel buffer past its useful life. Two ints
|
||||||
|
|
@ -35,15 +33,6 @@ public sealed class IsoPipeline : IAsyncDisposable
|
||||||
private int _lastHeight;
|
private int _lastHeight;
|
||||||
private DateTimeOffset? _lastReceivedAt;
|
private DateTimeOffset? _lastReceivedAt;
|
||||||
|
|
||||||
// Ring buffer of the last 30 incoming-frame timestamps for live fps display.
|
|
||||||
// Updated on the receiver's capture thread (single writer) and read by the UI
|
|
||||||
// poll thread (single reader); we use a lock for the snapshot path because
|
|
||||||
// an array-of-ticks can't be torn-read atomically.
|
|
||||||
private readonly long[] _frameTimes = new long[30];
|
|
||||||
private int _frameTimesHead;
|
|
||||||
private int _frameTimesCount;
|
|
||||||
private readonly object _frameTimesGate = new();
|
|
||||||
|
|
||||||
public Guid ParticipantId { get; }
|
public Guid ParticipantId { get; }
|
||||||
public IsoState State { get; private set; } = IsoState.Idle;
|
public IsoState State { get; private set; } = IsoState.Idle;
|
||||||
public int ConsecutiveFailures => _consecutiveFailures;
|
public int ConsecutiveFailures => _consecutiveFailures;
|
||||||
|
|
@ -58,7 +47,6 @@ public sealed class IsoPipeline : IAsyncDisposable
|
||||||
{
|
{
|
||||||
var receiver = Volatile.Read(ref _liveReceiver);
|
var receiver = Volatile.Read(ref _liveReceiver);
|
||||||
var sender = Volatile.Read(ref _liveSender);
|
var sender = Volatile.Read(ref _liveSender);
|
||||||
var processor = Volatile.Read(ref _liveProcessor);
|
|
||||||
var w = Volatile.Read(ref _lastWidth);
|
var w = Volatile.Read(ref _lastWidth);
|
||||||
var h = Volatile.Read(ref _lastHeight);
|
var h = Volatile.Read(ref _lastHeight);
|
||||||
var lastAt = _lastReceivedAt;
|
var lastAt = _lastReceivedAt;
|
||||||
|
|
@ -66,68 +54,17 @@ public sealed class IsoPipeline : IAsyncDisposable
|
||||||
if (receiver is null || sender is null)
|
if (receiver is null || sender is null)
|
||||||
return Domain.IsoHealthStats.Empty;
|
return Domain.IsoHealthStats.Empty;
|
||||||
|
|
||||||
// FrameProcessor.Stats already aggregates FramesDropped (older frames dropped
|
|
||||||
// by the closest-frame strategy when the input channel had backlog) and
|
|
||||||
// FramesDuplicated (last-frame re-emits when no new frame arrived this tick).
|
|
||||||
var procStats = processor?.Stats;
|
|
||||||
|
|
||||||
return new Domain.IsoHealthStats(
|
return new Domain.IsoHealthStats(
|
||||||
FramesIn: receiver.FramesCaptured,
|
FramesIn: receiver.FramesCaptured,
|
||||||
FramesOut: sender.FramesSent,
|
FramesOut: sender.FramesSent,
|
||||||
FramesDropped: procStats?.FramesDropped ?? 0,
|
FramesDropped: 0, // FrameProcessor currently doesn't surface drops; wire later
|
||||||
FramesDuplicated: procStats?.FramesDuplicated ?? 0,
|
FramesDuplicated: 0, // same — last-frame re-emits aren't counted yet
|
||||||
LastFrameAt: lastAt,
|
LastFrameAt: lastAt,
|
||||||
IncomingFps: ComputeFps(),
|
IncomingFps: 0, // running rate computation is a follow-up
|
||||||
IncomingWidth: w,
|
IncomingWidth: w,
|
||||||
IncomingHeight: h);
|
IncomingHeight: h);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Computes a moving-average incoming framerate from the last N frame timestamps.
|
|
||||||
/// Rate = (count - 1) / (newest - oldest). Returns 0 if fewer than 2 frames are
|
|
||||||
/// recorded or if the window is degenerate (clock skew, all-equal timestamps).
|
|
||||||
/// </summary>
|
|
||||||
private double ComputeFps()
|
|
||||||
{
|
|
||||||
long oldest, newest;
|
|
||||||
int count;
|
|
||||||
lock (_frameTimesGate)
|
|
||||||
{
|
|
||||||
count = _frameTimesCount;
|
|
||||||
if (count < 2) return 0;
|
|
||||||
// Oldest is at the slot AFTER head when buffer is full; otherwise at index 0.
|
|
||||||
var oldestIdx = count < _frameTimes.Length
|
|
||||||
? 0
|
|
||||||
: _frameTimesHead;
|
|
||||||
var newestIdx = (_frameTimesHead - 1 + _frameTimes.Length) % _frameTimes.Length;
|
|
||||||
oldest = _frameTimes[oldestIdx];
|
|
||||||
newest = _frameTimes[newestIdx];
|
|
||||||
}
|
|
||||||
var deltaTicks = newest - oldest;
|
|
||||||
if (deltaTicks <= 0) return 0;
|
|
||||||
var seconds = deltaTicks / (double)TimeSpan.TicksPerSecond;
|
|
||||||
return (count - 1) / seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RecordFrameTimestamp(long ticks)
|
|
||||||
{
|
|
||||||
lock (_frameTimesGate)
|
|
||||||
{
|
|
||||||
_frameTimes[_frameTimesHead] = ticks;
|
|
||||||
_frameTimesHead = (_frameTimesHead + 1) % _frameTimes.Length;
|
|
||||||
if (_frameTimesCount < _frameTimes.Length) _frameTimesCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResetFrameTimestamps()
|
|
||||||
{
|
|
||||||
lock (_frameTimesGate)
|
|
||||||
{
|
|
||||||
_frameTimesHead = 0;
|
|
||||||
_frameTimesCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Test ctor. The caller supplies the inner runner directly so failures and lifetimes
|
/// Test ctor. The caller supplies the inner runner directly so failures and lifetimes
|
||||||
/// can be controlled from a unit test.
|
/// can be controlled from a unit test.
|
||||||
|
|
@ -168,19 +105,15 @@ public sealed class IsoPipeline : IAsyncDisposable
|
||||||
{
|
{
|
||||||
_runInner = ct => RunInnerPipelineAsync(
|
_runInner = ct => RunInnerPipelineAsync(
|
||||||
config, interop, scaler, frameClock, loggerFactory, ct,
|
config, interop, scaler, frameClock, loggerFactory, ct,
|
||||||
onLive: (recv, send, proc) =>
|
onLive: (recv, send) =>
|
||||||
{
|
{
|
||||||
Volatile.Write(ref _liveReceiver, recv);
|
Volatile.Write(ref _liveReceiver, recv);
|
||||||
Volatile.Write(ref _liveSender, send);
|
Volatile.Write(ref _liveSender, send);
|
||||||
Volatile.Write(ref _liveProcessor, proc);
|
|
||||||
ResetFrameTimestamps(); // fresh window on every supervisor restart
|
|
||||||
},
|
},
|
||||||
onClear: () =>
|
onClear: () =>
|
||||||
{
|
{
|
||||||
Volatile.Write(ref _liveReceiver, null);
|
Volatile.Write(ref _liveReceiver, null);
|
||||||
Volatile.Write(ref _liveSender, null);
|
Volatile.Write(ref _liveSender, null);
|
||||||
Volatile.Write(ref _liveProcessor, null);
|
|
||||||
ResetFrameTimestamps();
|
|
||||||
},
|
},
|
||||||
onFrame: frame =>
|
onFrame: frame =>
|
||||||
{
|
{
|
||||||
|
|
@ -189,9 +122,7 @@ public sealed class IsoPipeline : IAsyncDisposable
|
||||||
// late stats read can never resurrect a dropped frame's pixel buffer.
|
// late stats read can never resurrect a dropped frame's pixel buffer.
|
||||||
Volatile.Write(ref _lastWidth, frame.Width);
|
Volatile.Write(ref _lastWidth, frame.Width);
|
||||||
Volatile.Write(ref _lastHeight, frame.Height);
|
Volatile.Write(ref _lastHeight, frame.Height);
|
||||||
var nowTicks = DateTimeOffset.UtcNow.UtcTicks;
|
_lastReceivedAt = DateTimeOffset.UtcNow;
|
||||||
_lastReceivedAt = new DateTimeOffset(nowTicks, TimeSpan.Zero);
|
|
||||||
RecordFrameTimestamp(nowTicks);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,7 +220,7 @@ public sealed class IsoPipeline : IAsyncDisposable
|
||||||
IFrameClock frameClock,
|
IFrameClock frameClock,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
Action<NdiReceiver, NdiSender, FrameProcessor>? onLive = null,
|
Action<NdiReceiver, NdiSender>? onLive = null,
|
||||||
Action? onClear = null,
|
Action? onClear = null,
|
||||||
Action<RawFrame>? onFrame = null)
|
Action<RawFrame>? onFrame = null)
|
||||||
{
|
{
|
||||||
|
|
@ -318,13 +249,13 @@ public sealed class IsoPipeline : IAsyncDisposable
|
||||||
interop, config.OutputName, processedChannel.Reader, loggerFactory.CreateLogger<NdiSender>(),
|
interop, config.OutputName, processedChannel.Reader, loggerFactory.CreateLogger<NdiSender>(),
|
||||||
config.OutputGroups);
|
config.OutputGroups);
|
||||||
|
|
||||||
|
onLive?.Invoke(receiver, sender);
|
||||||
|
|
||||||
var processor = new FrameProcessor(
|
var processor = new FrameProcessor(
|
||||||
config.Settings, scaler, new SolidFrameRenderer(),
|
config.Settings, scaler, new SolidFrameRenderer(),
|
||||||
frameClock, rawChannel.Reader, processedChannel.Writer,
|
frameClock, rawChannel.Reader, processedChannel.Writer,
|
||||||
config.SlateThreshold, loggerFactory.CreateLogger<FrameProcessor>());
|
config.SlateThreshold, loggerFactory.CreateLogger<FrameProcessor>());
|
||||||
|
|
||||||
onLive?.Invoke(receiver, sender, processor);
|
|
||||||
|
|
||||||
var receiverTask = receiver.RunAsync(ct);
|
var receiverTask = receiver.RunAsync(ct);
|
||||||
var senderTask = sender.RunAsync(ct);
|
var senderTask = sender.RunAsync(ct);
|
||||||
var processorTask = ProcessorLoopAsync(processor, frameClock, ct);
|
var processorTask = ProcessorLoopAsync(processor, frameClock, ct);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue