完善契约与客户端、服务端的收发代码
This commit is contained in:
65
SHH.CameraDashboard/Pages/CameraWall/VideoTile.xaml
Normal file
65
SHH.CameraDashboard/Pages/CameraWall/VideoTile.xaml
Normal 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>
|
||||
15
SHH.CameraDashboard/Pages/CameraWall/VideoTile.xaml.cs
Normal file
15
SHH.CameraDashboard/Pages/CameraWall/VideoTile.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// VideoTile.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class VideoTile : UserControl
|
||||
{
|
||||
public VideoTile()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
87
SHH.CameraDashboard/Pages/CameraWall/VideoTileViewModel.cs
Normal file
87
SHH.CameraDashboard/Pages/CameraWall/VideoTileViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
93
SHH.CameraDashboard/Pages/CameraWall/VideoWall.xaml
Normal file
93
SHH.CameraDashboard/Pages/CameraWall/VideoWall.xaml
Normal 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>
|
||||
15
SHH.CameraDashboard/Pages/CameraWall/VideoWall.xaml.cs
Normal file
15
SHH.CameraDashboard/Pages/CameraWall/VideoWall.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace SHH.CameraDashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// VideoWall.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class VideoWall : UserControl
|
||||
{
|
||||
public VideoWall()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
84
SHH.CameraDashboard/Pages/CameraWall/VideoWallViewModel.cs
Normal file
84
SHH.CameraDashboard/Pages/CameraWall/VideoWallViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
SHH.CameraDashboard/Pages/CameraWall/ViewModelBase.cs
Normal file
24
SHH.CameraDashboard/Pages/CameraWall/ViewModelBase.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user