完善契约与客户端、服务端的收发代码

This commit is contained in:
2026-01-03 00:16:28 +08:00
parent d039559402
commit dcf424a86e
30 changed files with 3292 additions and 349 deletions

View File

@@ -0,0 +1,65 @@
<UserControl
x:Class="SHH.CameraDashboard.VideoTile"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="200"
d:DesignWidth="300"
mc:Ignorable="d">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis" />
</UserControl.Resources>
<Border
Margin="2"
Background="Black"
BorderBrush="#444"
BorderThickness="1"
CornerRadius="4">
<Grid>
<Image Source="{Binding DisplayImage}" Stretch="Uniform" />
<Grid Background="#222" Visibility="{Binding IsConnected, Converter={StaticResource BoolToVis}, ConverterParameter=Inverse}">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock
HorizontalAlignment="Center"
FontSize="24"
Foreground="#666"
Text="❌" />
<TextBlock
Margin="0,5,0,0"
Foreground="#666"
Text="无信号" />
</StackPanel>
</Grid>
<Grid
Height="28"
VerticalAlignment="Top"
Background="#66000000">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="8,0"
VerticalAlignment="Center"
FontWeight="Bold"
Foreground="White"
Text="{Binding CameraName}" />
<TextBlock
Grid.Column="1"
Margin="8,0"
VerticalAlignment="Center"
FontSize="11"
Foreground="#00FF00"
Text="{Binding StatusInfo}" />
</Grid>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,15 @@
using System.Windows.Controls;
namespace SHH.CameraDashboard
{
/// <summary>
/// VideoTile.xaml 的交互逻辑
/// </summary>
public partial class VideoTile : UserControl
{
public VideoTile()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,87 @@
using SHH.Contracts;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace SHH.CameraDashboard
{
public class VideoTileViewModel : ViewModelBase, IDisposable
{
// --- 绑定属性 ---
private ImageSource _displayImage;
public ImageSource DisplayImage
{
get => _displayImage;
set => SetProperty(ref _displayImage, value);
}
private string _cameraName;
public string CameraName
{
get => _cameraName;
set => SetProperty(ref _cameraName, value);
}
private string _statusInfo;
public string StatusInfo
{
get => _statusInfo;
set => SetProperty(ref _statusInfo, value);
}
private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
// --- 构造函数 ---
public VideoTileViewModel(string ip, int port, string name)
{
CameraName = name;
StatusInfo = "连接中...";
IsConnected = true;
}
private void HandleNewFrame(VideoPayload payload)
{
// 必须回到 UI 线程更新 ImageSource
Application.Current.Dispatcher.Invoke(() =>
{
// 1. 更新图片
byte[] data = payload.TargetImageBytes ?? payload.OriginalImageBytes;
if (data != null && data.Length > 0)
{
DisplayImage = ByteToBitmap(data);
}
// 2. 更新状态文字
StatusInfo = $"{payload.CaptureTime:HH:mm:ss} | {data?.Length / 1024} KB";
});
}
// 简单的 Bytes 转 BitmapImage (生产环境建议优化为 WriteableBitmap)
private BitmapImage ByteToBitmap(byte[] bytes)
{
var bitmap = new BitmapImage();
using (var stream = new MemoryStream(bytes))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
}
bitmap.Freeze(); // 必须冻结才能跨线程
return bitmap;
}
public void Dispose()
{
IsConnected = false;
}
}
}

View File

@@ -0,0 +1,93 @@
<UserControl
x:Class="SHH.CameraDashboard.VideoWall"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SHH.CameraDashboard"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:SHH.CameraDashboard"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.DataContext>
<vm:VideoWallViewModel />
</UserControl.DataContext>
<Grid Background="#111">
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Margin="0,0,0,2"
Background="#252526"
Orientation="Horizontal">
<TextBlock
Margin="10,0"
VerticalAlignment="Center"
Foreground="White"
Text="📺 视频墙布局:" />
<Button
Margin="5,0"
Command="{Binding SetLayoutCommand}"
CommandParameter="1x1"
Content="1画面"
Style="{StaticResource PrimaryBtnStyle}" />
<Button
Margin="5,0"
Command="{Binding SetLayoutCommand}"
CommandParameter="2x2"
Content="4画面"
Style="{StaticResource PrimaryBtnStyle}" />
<Button
Margin="5,0"
Command="{Binding SetLayoutCommand}"
CommandParameter="3x3"
Content="9画面"
Style="{StaticResource PrimaryBtnStyle}" />
</StackPanel>
<ListBox
Grid.Row="1"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Background="Transparent"
BorderThickness="0"
ItemsSource="{Binding VideoTiles}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding DataContext.Columns, RelativeSource={RelativeSource AncestorType=UserControl}}" Rows="0" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<ContentPresenter />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<local:VideoTile />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>

View File

@@ -0,0 +1,15 @@
using System.Windows.Controls;
namespace SHH.CameraDashboard
{
/// <summary>
/// VideoWall.xaml 的交互逻辑
/// </summary>
public partial class VideoWall : UserControl
{
public VideoWall()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,84 @@
using SHH.Contracts;
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace SHH.CameraDashboard
{
public class VideoWallViewModel : ViewModelBase
{
// 引用推流接收服务
private readonly VideoPushServer _pushServer;
// 视频列表
public ObservableCollection<VideoTileViewModel> VideoTiles { get; } = new ObservableCollection<VideoTileViewModel>();
// 控制 UniformGrid 的列数 (决定是 2x2 还是 3x3)
private int _columns = 2;
public int Columns
{
get => _columns;
set => SetProperty(ref _columns, value);
}
// 切换布局命令
public ICommand SetLayoutCommand { get; }
public VideoWallViewModel()
{
SetLayoutCommand = new RelayCommand<string>(ExecuteSetLayout);
// 1. 初始化并启动接收服务
_pushServer = new VideoPushServer();
_pushServer.OnFrameReceived += OnGlobalFrameReceived;
// 2. 启动监听端口 (比如 6000)
// 之后你的采集端 ForwarderClient 需要 Connect("tcp://你的IP:6000")
_pushServer.Start(6000);
// 3. 初始化格子 (不再需要传入 IP/Port 去主动连接了)
// 我们用 CameraId 或 Name 来作为匹配标识
InitVideoTiles();
}
/// <summary>
/// 全局接收回调:收到任何一路视频都会进这里
/// </summary>
private void OnGlobalFrameReceived(VideoPayload payload)
{
// 1. 在 VideoTiles 集合中找到对应的格子
// 假设 payload.CameraId 与我们 VideoTileViewModel 中的 ID 对应
//var targetTile = VideoTiles.FirstOrDefault(t => t.id == payload.CameraId);
//if (targetTile != null)
//{
// // 2. 将数据交给格子去渲染
// targetTile.UpdateFrame(payload);
//}
}
private void InitVideoTiles()
{
// 假设我们预设 4 个格子,分别对应不同的摄像头 ID
// 这里 ID 必须和采集端发送的 VideoPayload.CameraId 一致
//VideoTiles.Add(new VideoTileViewModel("1004", "仓库通道"));
}
public void AddCamera(string ip, int port, string name)
{
var tile = new VideoTileViewModel(ip, port, name);
VideoTiles.Add(tile);
}
private void ExecuteSetLayout(string layoutType)
{
switch (layoutType)
{
case "1x1": Columns = 1; break;
case "2x2": Columns = 2; break;
case "3x3": Columns = 3; break;
case "4x4": Columns = 4; break;
default: Columns = 2; break;
}
}
}
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace SHH.CameraDashboard
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// 辅助方法:设置属性值并触发通知
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
}