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>
|
<PackagePath>\</PackagePath>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AvalonEdit" Version="6.3.1.120" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
xmlns:views="clr-namespace:EmbyToolbox.Views"
|
xmlns:views="clr-namespace:EmbyToolbox.Views"
|
||||||
xmlns:viewModels="clr-namespace:EmbyToolbox.ViewModels"
|
xmlns:viewModels="clr-namespace:EmbyToolbox.ViewModels"
|
||||||
xmlns:converters="clr-namespace:EmbyToolbox.Converters"
|
xmlns:converters="clr-namespace:EmbyToolbox.Converters"
|
||||||
|
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
@ -472,21 +473,19 @@
|
|||||||
</TextBlock.Style>
|
</TextBlock.Style>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
|
||||||
<ListBox Grid.Row="1"
|
<avalonEdit:TextEditor Grid.Row="1"
|
||||||
ItemsSource="{Binding FormattedJsonLines}"
|
behaviors:AvalonEditTextBindingBehavior.BoundText="{Binding FormattedJson, Mode=OneWay}"
|
||||||
Margin="0"
|
|
||||||
BorderThickness="0"
|
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
Foreground="{DynamicResource Ui.Brush.Text}"
|
Foreground="{DynamicResource Ui.Brush.Text}"
|
||||||
ScrollViewer.CanContentScroll="True"
|
FontFamily="Consolas"
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
FontSize="13"
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
IsReadOnly="True"
|
||||||
VirtualizingPanel.IsVirtualizing="True"
|
ShowLineNumbers="False"
|
||||||
VirtualizingPanel.VirtualizationMode="Recycling"
|
WordWrap="False"
|
||||||
VirtualizingPanel.ScrollUnit="Pixel"
|
HorizontalScrollBarVisibility="Auto"
|
||||||
SelectionMode="Extended">
|
VerticalScrollBarVisibility="Auto">
|
||||||
<ListBox.Style>
|
<avalonEdit:TextEditor.Style>
|
||||||
<Style TargetType="ListBox">
|
<Style TargetType="avalonEdit:TextEditor">
|
||||||
<Setter Property="Visibility" Value="Visible" />
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<DataTrigger Binding="{Binding SelectedFilePath}" Value="">
|
<DataTrigger Binding="{Binding SelectedFilePath}" Value="">
|
||||||
@ -494,36 +493,8 @@
|
|||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
</ListBox.Style>
|
</avalonEdit:TextEditor.Style>
|
||||||
<ListBox.ItemContainerStyle>
|
</avalonEdit:TextEditor>
|
||||||
<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>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -18,12 +18,12 @@ public sealed class MediaAnalysisResult
|
|||||||
public IReadOnlyList<MediaStreamInfo> AllStreams { get; init; } = Array.Empty<MediaStreamInfo>();
|
public IReadOnlyList<MediaStreamInfo> AllStreams { get; init; } = Array.Empty<MediaStreamInfo>();
|
||||||
public long? SourceVideoBitrateBps { get; init; }
|
public long? SourceVideoBitrateBps { get; init; }
|
||||||
|
|
||||||
/// <summary>Основной видеопоток: самый крупный по площади кадра (не первый подряд в JSON — иначе cover/mjpeg может оказаться «основным»).</summary>
|
/// <summary>Основной видеопоток: default среди обычных video, затем самый крупный по площади кадра.</summary>
|
||||||
public MediaStreamInfo? PrimaryVideo =>
|
public MediaStreamInfo? PrimaryVideo =>
|
||||||
VideoStreams
|
VideoStreams
|
||||||
.Where(static v => !v.IsAttachedPicture)
|
.Where(static v => !v.IsAttachedPicture)
|
||||||
.OrderByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0))
|
.OrderByDescending(static v => v.IsDefault ? 1 : 0)
|
||||||
.ThenByDescending(static v => v.IsDefault ? 1 : 0)
|
.ThenByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0))
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
/// <summary>format.duration, иначе максимум duration среди streams (ffprobe).</summary>
|
/// <summary>format.duration, иначе максимум duration среди streams (ffprobe).</summary>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EmbyToolbox.Models;
|
using EmbyToolbox.Models;
|
||||||
@ -69,8 +70,8 @@ public static class MediaAnalysisParser
|
|||||||
var primaryVideoBitrate = streams
|
var primaryVideoBitrate = streams
|
||||||
.Where(x => x.Kind == MediaStreamKind.Video)
|
.Where(x => x.Kind == MediaStreamKind.Video)
|
||||||
.Where(x => !x.IsAttachedPicture)
|
.Where(x => !x.IsAttachedPicture)
|
||||||
.OrderByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0))
|
.OrderByDescending(static v => v.IsDefault ? 1 : 0)
|
||||||
.ThenByDescending(static v => v.IsDefault ? 1 : 0)
|
.ThenByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0))
|
||||||
.Select(static v => v.BitRateBps)
|
.Select(static v => v.BitRateBps)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
@ -100,6 +101,7 @@ public static class MediaAnalysisParser
|
|||||||
var t = (ct.GetString() ?? string.Empty).ToLowerInvariant();
|
var t = (ct.GetString() ?? string.Empty).ToLowerInvariant();
|
||||||
if (t == "video")
|
if (t == "video")
|
||||||
{
|
{
|
||||||
|
var isAttachedPicture = GetDisposition(s, "attached_pic", false) || IsCoverArtVideoStream(s);
|
||||||
return new MediaStreamInfo
|
return new MediaStreamInfo
|
||||||
{
|
{
|
||||||
Index = GetInt(s, "index", -1),
|
Index = GetInt(s, "index", -1),
|
||||||
@ -118,7 +120,7 @@ public static class MediaAnalysisParser
|
|||||||
ColorSpace = GetStr(s, "color_space", null),
|
ColorSpace = GetStr(s, "color_space", null),
|
||||||
ColorPrimaries = GetStr(s, "color_primaries", null),
|
ColorPrimaries = GetStr(s, "color_primaries", null),
|
||||||
ColorTransfer = GetStr(s, "color_transfer", 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"),
|
BitRateBps = ParseLong(s, "bit_rate") ?? ParseLong(s, "max_bit_rate"),
|
||||||
DurationSeconds = streamDuration
|
DurationSeconds = streamDuration
|
||||||
};
|
};
|
||||||
@ -301,12 +303,58 @@ public static class MediaAnalysisParser
|
|||||||
return null;
|
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 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)
|
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 _analysisStateText = string.Empty;
|
||||||
private string _errorMessage = string.Empty;
|
private string _errorMessage = string.Empty;
|
||||||
private string _formattedJson = string.Empty;
|
private string _formattedJson = string.Empty;
|
||||||
private IReadOnlyList<string> _formattedJsonLines = Array.Empty<string>();
|
|
||||||
private string _summaryText = string.Empty;
|
private string _summaryText = string.Empty;
|
||||||
private bool _isBusy;
|
private bool _isBusy;
|
||||||
private bool _isVideoInfoDropHighlight;
|
private bool _isVideoInfoDropHighlight;
|
||||||
@ -110,22 +109,6 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
|
|
||||||
_formattedJson = value;
|
_formattedJson = value;
|
||||||
FormattedJsonLines = SplitTextLines(value);
|
|
||||||
OnPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyList<string> FormattedJsonLines
|
|
||||||
{
|
|
||||||
get => _formattedJsonLines;
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
if (ReferenceEquals(_formattedJsonLines, value))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_formattedJsonLines = value;
|
|
||||||
OnPropertyChanged();
|
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