Fix video info viewer and primary stream detection

This commit is contained in:
Emby Toolbox 2026-05-25 15:43:20 +05:00
parent 75bcdff3b1
commit 7c722e21bf
6 changed files with 117 additions and 80 deletions

View File

@ -0,0 +1,40 @@
using System.Windows;
using ICSharpCode.AvalonEdit;
namespace EmbyToolbox.Behaviors;
public static class AvalonEditTextBindingBehavior
{
public static readonly DependencyProperty BoundTextProperty =
DependencyProperty.RegisterAttached(
"BoundText",
typeof(string),
typeof(AvalonEditTextBindingBehavior),
new PropertyMetadata(string.Empty, OnBoundTextChanged));
public static string GetBoundText(DependencyObject obj)
{
return (string)obj.GetValue(BoundTextProperty);
}
public static void SetBoundText(DependencyObject obj, string value)
{
obj.SetValue(BoundTextProperty, value);
}
private static void OnBoundTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not TextEditor editor)
{
return;
}
var nextText = e.NewValue as string ?? string.Empty;
if (editor.Text == nextText)
{
return;
}
editor.Text = nextText;
}
}

View File

@ -29,4 +29,8 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.3.1.120" />
</ItemGroup>
</Project>

View File

@ -7,6 +7,7 @@
xmlns:views="clr-namespace:EmbyToolbox.Views"
xmlns:viewModels="clr-namespace:EmbyToolbox.ViewModels"
xmlns:converters="clr-namespace:EmbyToolbox.Converters"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
@ -472,21 +473,19 @@
</TextBlock.Style>
</TextBlock>
<ListBox Grid.Row="1"
ItemsSource="{Binding FormattedJsonLines}"
Margin="0"
BorderThickness="0"
<avalonEdit:TextEditor Grid.Row="1"
behaviors:AvalonEditTextBindingBehavior.BoundText="{Binding FormattedJson, Mode=OneWay}"
Background="Transparent"
Foreground="{DynamicResource Ui.Brush.Text}"
ScrollViewer.CanContentScroll="True"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
VirtualizingPanel.ScrollUnit="Pixel"
SelectionMode="Extended">
<ListBox.Style>
<Style TargetType="ListBox">
FontFamily="Consolas"
FontSize="13"
IsReadOnly="True"
ShowLineNumbers="False"
WordWrap="False"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<avalonEdit:TextEditor.Style>
<Style TargetType="avalonEdit:TextEditor">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedFilePath}" Value="">
@ -494,36 +493,8 @@
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Style>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Focusable" Value="False" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Mode=OneWay}"
BorderThickness="0"
Background="Transparent"
Foreground="{DynamicResource Ui.Brush.Text}"
FontFamily="Consolas"
FontSize="13"
IsReadOnly="True"
IsUndoEnabled="False"
IsInactiveSelectionHighlightEnabled="True"
AcceptsReturn="False"
AcceptsTab="False"
TextWrapping="NoWrap"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Disabled" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</avalonEdit:TextEditor.Style>
</avalonEdit:TextEditor>
</Grid>
</Border>
</Grid>

View File

@ -18,12 +18,12 @@ public sealed class MediaAnalysisResult
public IReadOnlyList<MediaStreamInfo> AllStreams { get; init; } = Array.Empty<MediaStreamInfo>();
public long? SourceVideoBitrateBps { get; init; }
/// <summary>Основной видеопоток: самый крупный по площади кадра (не первый подряд в JSON — иначе cover/mjpeg может оказаться «основным»).</summary>
/// <summary>Основной видеопоток: default среди обычных video, затем самый крупный по площади кадра.</summary>
public MediaStreamInfo? PrimaryVideo =>
VideoStreams
.Where(static v => !v.IsAttachedPicture)
.OrderByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0))
.ThenByDescending(static v => v.IsDefault ? 1 : 0)
.OrderByDescending(static v => v.IsDefault ? 1 : 0)
.ThenByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0))
.FirstOrDefault();
/// <summary>format.duration, иначе максимум duration среди streams (ffprobe).</summary>

View File

@ -1,4 +1,5 @@
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using EmbyToolbox.Models;
@ -69,8 +70,8 @@ public static class MediaAnalysisParser
var primaryVideoBitrate = streams
.Where(x => x.Kind == MediaStreamKind.Video)
.Where(x => !x.IsAttachedPicture)
.OrderByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0))
.ThenByDescending(static v => v.IsDefault ? 1 : 0)
.OrderByDescending(static v => v.IsDefault ? 1 : 0)
.ThenByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0))
.Select(static v => v.BitRateBps)
.FirstOrDefault();
@ -100,6 +101,7 @@ public static class MediaAnalysisParser
var t = (ct.GetString() ?? string.Empty).ToLowerInvariant();
if (t == "video")
{
var isAttachedPicture = GetDisposition(s, "attached_pic", false) || IsCoverArtVideoStream(s);
return new MediaStreamInfo
{
Index = GetInt(s, "index", -1),
@ -118,7 +120,7 @@ public static class MediaAnalysisParser
ColorSpace = GetStr(s, "color_space", null),
ColorPrimaries = GetStr(s, "color_primaries", null),
ColorTransfer = GetStr(s, "color_transfer", null),
IsAttachedPicture = GetDisposition(s, "attached_pic", false),
IsAttachedPicture = isAttachedPicture,
BitRateBps = ParseLong(s, "bit_rate") ?? ParseLong(s, "max_bit_rate"),
DurationSeconds = streamDuration
};
@ -301,12 +303,58 @@ public static class MediaAnalysisParser
return null;
}
if (!tags.TryGetProperty(tagKey, out var val) || val.ValueKind != JsonValueKind.String)
foreach (var tag in tags.EnumerateObject())
{
if (string.Equals(tag.Name, tagKey, StringComparison.OrdinalIgnoreCase) &&
tag.Value.ValueKind == JsonValueKind.String)
{
return tag.Value.GetString();
}
}
return null;
}
return val.GetString();
private static bool IsCoverArtVideoStream(JsonElement stream)
{
var mimeType = GetTagExact(stream, "mimetype");
if (!string.IsNullOrWhiteSpace(mimeType) &&
mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
{
return true;
}
var fileName = GetTagExact(stream, "filename");
if (string.IsNullOrWhiteSpace(fileName))
{
return false;
}
var codec = GetStr(stream, "codec_name", string.Empty) ?? string.Empty;
if (!IsStillImageCodec(codec))
{
return false;
}
var extension = Path.GetExtension(fileName);
return extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".png", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".webp", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".bmp", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".gif", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".tif", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".tiff", StringComparison.OrdinalIgnoreCase);
}
private static bool IsStillImageCodec(string codec)
{
return codec.Equals("mjpeg", StringComparison.OrdinalIgnoreCase)
|| codec.Equals("png", StringComparison.OrdinalIgnoreCase)
|| codec.Equals("webp", StringComparison.OrdinalIgnoreCase)
|| codec.Equals("bmp", StringComparison.OrdinalIgnoreCase)
|| codec.Equals("gif", StringComparison.OrdinalIgnoreCase)
|| codec.Equals("tiff", StringComparison.OrdinalIgnoreCase);
}
private static bool GetDisposition(JsonElement s, string d, bool def)

View File

@ -23,7 +23,6 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
private string _analysisStateText = string.Empty;
private string _errorMessage = string.Empty;
private string _formattedJson = string.Empty;
private IReadOnlyList<string> _formattedJsonLines = Array.Empty<string>();
private string _summaryText = string.Empty;
private bool _isBusy;
private bool _isVideoInfoDropHighlight;
@ -110,22 +109,6 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
}
_formattedJson = value;
FormattedJsonLines = SplitTextLines(value);
OnPropertyChanged();
}
}
public IReadOnlyList<string> FormattedJsonLines
{
get => _formattedJsonLines;
private set
{
if (ReferenceEquals(_formattedJsonLines, value))
{
return;
}
_formattedJsonLines = value;
OnPropertyChanged();
}
}
@ -496,14 +479,5 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
}
}
private static IReadOnlyList<string> SplitTextLines(string text)
{
if (string.IsNullOrEmpty(text))
{
return Array.Empty<string>();
}
return text.ReplaceLineEndings("\n").Split('\n');
}
}