Fix video info viewer and primary stream detection
This commit is contained in:
parent
75bcdff3b1
commit
7c722e21bf
40
EmbyToolbox/Behaviors/AvalonEditTextBindingBehavior.cs
Normal file
40
EmbyToolbox/Behaviors/AvalonEditTextBindingBehavior.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -29,4 +29,8 @@
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AvalonEdit" Version="6.3.1.120" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -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"
|
||||
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">
|
||||
<avalonEdit:TextEditor Grid.Row="1"
|
||||
behaviors:AvalonEditTextBindingBehavior.BoundText="{Binding FormattedJson, Mode=OneWay}"
|
||||
Background="Transparent"
|
||||
Foreground="{DynamicResource Ui.Brush.Text}"
|
||||
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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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())
|
||||
{
|
||||
return null;
|
||||
if (string.Equals(tag.Name, tagKey, StringComparison.OrdinalIgnoreCase) &&
|
||||
tag.Value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return tag.Value.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
return val.GetString();
|
||||
return null;
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user