mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
Full ffprobe output parsing. No SaR/DaR support yet.
This commit is contained in:
parent
18928a23f9
commit
4f83fc1dd2
@ -1,7 +1,6 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Splitter_UI;
|
||||
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using OpenCvSharp;
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
internal class GlobalLogger(ILogService _logService) : ILogger
|
||||
{
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public interface IFileJobFactory
|
||||
{
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public interface IFileProbeService
|
||||
{
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public interface IProcessingService
|
||||
{
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Media.Imaging;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public sealed class LogService : ILogService
|
||||
{
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public sealed class ProcessingService : IProcessingService
|
||||
{
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Controls;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Splitter_UI.ViewModels;
|
||||
|
||||
@ -155,9 +155,16 @@ public partial class JobViewModel : ObservableObject
|
||||
|
||||
private async Task LoadThumbnailAsync()
|
||||
{
|
||||
Probe = await _fileProbe.ProbeAsync(Job);
|
||||
Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, rotateDegree: Job.Rotate);
|
||||
SuggestedAction = Probe.Rotation == 0 ? "crop" : "rotate";
|
||||
try
|
||||
{
|
||||
Probe = await _fileProbe.ProbeAsync(Job);
|
||||
Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, rotateDegree: Job.Rotate);
|
||||
SuggestedAction = Probe.Rotation == 0 ? "crop" : "rotate";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError($"Error creating thumbnail for {FileName}: {ex.Message}");
|
||||
}
|
||||
|
||||
await CreatePreview();
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Splitter_UI.ViewModels;
|
||||
|
||||
namespace Splitter_UI.Views;
|
||||
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
|
||||
namespace Splitter_UI.Views;
|
||||
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Splitter_UI.Views;
|
||||
|
||||
|
||||
31
splitter-cli/FlexibleDoubleConverter.cs
Normal file
31
splitter-cli/FlexibleDoubleConverter.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace splitter;
|
||||
|
||||
public sealed class FlexibleDoubleConverter : JsonConverter<double?>
|
||||
{
|
||||
public override double? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Number)
|
||||
return reader.GetDouble();
|
||||
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var s = reader.GetString();
|
||||
if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v))
|
||||
return v;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, double? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value.HasValue)
|
||||
writer.WriteNumberValue(value.Value);
|
||||
else
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
31
splitter-cli/FlexibleIntConverter.cs
Normal file
31
splitter-cli/FlexibleIntConverter.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace splitter;
|
||||
|
||||
public sealed class FlexibleIntConverter : JsonConverter<int?>
|
||||
{
|
||||
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Number)
|
||||
return reader.GetInt32();
|
||||
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var s = reader.GetString();
|
||||
if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v))
|
||||
return v;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value.HasValue)
|
||||
writer.WriteNumberValue(value.Value);
|
||||
else
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
31
splitter-cli/FlexibleLongConverter.cs
Normal file
31
splitter-cli/FlexibleLongConverter.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace splitter;
|
||||
|
||||
public sealed class FlexibleLongConverter : JsonConverter<long?>
|
||||
{
|
||||
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Number)
|
||||
return reader.GetInt64();
|
||||
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var s = reader.GetString();
|
||||
if (long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v))
|
||||
return v;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value.HasValue)
|
||||
writer.WriteNumberValue(value.Value);
|
||||
else
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace splitter;
|
||||
|
||||
@ -9,11 +11,20 @@ public record VideoInfo(
|
||||
int Height,
|
||||
double Fps,
|
||||
double Bitrate,
|
||||
double SarX,
|
||||
double SarY,
|
||||
int Rotation = 0
|
||||
);
|
||||
|
||||
public static class ProbeVideo
|
||||
{
|
||||
static ProbeVideo()
|
||||
{
|
||||
_ffprobeJsonOptions.Converters.Add(new FlexibleDoubleConverter());
|
||||
_ffprobeJsonOptions.Converters.Add(new FlexibleIntConverter());
|
||||
_ffprobeJsonOptions.Converters.Add(new FlexibleLongConverter());
|
||||
}
|
||||
|
||||
public static async Task<VideoInfo> Probe(SingleJob job)
|
||||
{
|
||||
var info = ProbeSize(job.InputFile);
|
||||
@ -32,15 +43,188 @@ public static class ProbeVideo
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public sealed class FfprobeResult
|
||||
{
|
||||
public List<FfprobeStream>? Streams { get; set; }
|
||||
public FfprobeFormat? Format { get; set; }
|
||||
}
|
||||
|
||||
public sealed class FfprobeFormat
|
||||
{
|
||||
public string? Filename { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Nb_streams { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Nb_programs { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Nb_stream_groups { get; set; }
|
||||
|
||||
public string? Format_name { get; set; }
|
||||
public string? Format_long_name { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double? Start_time { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double? Duration { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleLongConverter))]
|
||||
public long? Size { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleLongConverter))]
|
||||
public long? Bit_rate { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Probe_score { get; set; }
|
||||
|
||||
public Dictionary<string, string>? Tags { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public sealed class FfprobeStream
|
||||
{
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Index { get; set; }
|
||||
|
||||
public string? Codec_name { get; set; }
|
||||
public string? Codec_long_name { get; set; }
|
||||
public string? Profile { get; set; }
|
||||
public string? Codec_type { get; set; }
|
||||
public string? Codec_tag_string { get; set; }
|
||||
public string? Codec_tag { get; set; }
|
||||
public string? Mime_codec_string { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Width { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Height { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Coded_width { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Coded_height { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Closed_captions { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Film_grain { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Has_b_frames { get; set; }
|
||||
|
||||
public string? Sample_aspect_ratio { get; set; }
|
||||
public string? Display_aspect_ratio { get; set; }
|
||||
|
||||
public string? Pix_fmt { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Level { get; set; }
|
||||
|
||||
public string? Color_range { get; set; }
|
||||
public string? Color_space { get; set; }
|
||||
public string? Color_transfer { get; set; }
|
||||
public string? Color_primaries { get; set; }
|
||||
public string? Chroma_location { get; set; }
|
||||
public string? Field_order { get; set; }
|
||||
|
||||
public string? Is_avc { get; set; }
|
||||
public string? Nal_length_size { get; set; }
|
||||
|
||||
public string? Id { get; set; }
|
||||
public string? R_frame_rate { get; set; }
|
||||
public string? Avg_frame_rate { get; set; }
|
||||
public string? Time_base { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleLongConverter))]
|
||||
public long? Start_pts { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double? Start_time { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleLongConverter))]
|
||||
public long? Duration_ts { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double? Duration { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleLongConverter))]
|
||||
public long? Bit_rate { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleLongConverter))]
|
||||
public long? Max_bit_rate { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Bits_per_raw_sample { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Bits_per_sample { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleLongConverter))]
|
||||
public long? Nb_frames { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Extradata_size { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Channels { get; set; }
|
||||
|
||||
public string? Channel_layout { get; set; }
|
||||
public string? Sample_fmt { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Sample_rate { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Initial_padding { get; set; }
|
||||
|
||||
public string? Disposition_raw { get; set; }
|
||||
public Dictionary<string, int>? Disposition { get; set; }
|
||||
|
||||
public Dictionary<string, string>? Tags { get; set; }
|
||||
|
||||
public string? Language { get; set; }
|
||||
public string? Title { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleIntConverter))]
|
||||
public int? Bits_per_coded_sample { get; set; }
|
||||
|
||||
public string? Codec_time_base { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double? Start_pts_time { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double? Duration_time { get; set; }
|
||||
|
||||
public string? Extradata { get; set; }
|
||||
public string? Default { get; set; }
|
||||
public string? Forced { get; set; }
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions _ffprobeJsonOptions =
|
||||
new ()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
IgnoreReadOnlyProperties = false,
|
||||
AllowTrailingCommas = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
UnknownTypeHandling = JsonUnknownTypeHandling.JsonElement
|
||||
};
|
||||
|
||||
private static VideoInfo ProbeSize(string inputFile)
|
||||
{
|
||||
var args =
|
||||
"-v error " +
|
||||
"-select_streams v:0 " +
|
||||
"-show_entries format=duration " +
|
||||
"-show_entries stream=width,height,avg_frame_rate,bit_rate " +
|
||||
"-of default=noprint_wrappers=1:nokey=0 " + // <-- IMPORTANT: include keys
|
||||
$"\"{inputFile}\"";
|
||||
"-v error " +
|
||||
"-show_streams " +
|
||||
"-show_format " +
|
||||
"-of json " +
|
||||
$"\"{inputFile}\"";
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
@ -55,54 +239,62 @@ public static class ProbeVideo
|
||||
using var p = new Process { StartInfo = psi };
|
||||
p.Start();
|
||||
|
||||
var duration = -1.0;
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
var fps = 0.0;
|
||||
var bitrate = 0.0;
|
||||
var json = p.StandardOutput.ReadToEnd();
|
||||
p.WaitForExit();
|
||||
|
||||
while (!p.StandardOutput.EndOfStream)
|
||||
var result = JsonSerializer.Deserialize<FfprobeResult>(json, _ffprobeJsonOptions);
|
||||
var stream = result?.Streams?.FirstOrDefault();
|
||||
var format = result?.Format;
|
||||
|
||||
var duration = format?.Duration ?? 0.0;
|
||||
var width = stream?.Width ?? 0;
|
||||
var height = stream?.Height ?? 0;
|
||||
|
||||
double fps = 0.0;
|
||||
if (!string.IsNullOrWhiteSpace(stream?.Avg_frame_rate))
|
||||
{
|
||||
var line = p.StandardOutput.ReadLine()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
if (line.StartsWith("duration="))
|
||||
var parts = stream.Avg_frame_rate.Split('/');
|
||||
if (parts.Length == 2 &&
|
||||
double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var num) &&
|
||||
double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) &&
|
||||
den != 0)
|
||||
{
|
||||
var v = line.Substring("duration=".Length);
|
||||
double.TryParse(v, NumberStyles.Any, CultureInfo.InvariantCulture, out duration);
|
||||
}
|
||||
else if (line.StartsWith("width="))
|
||||
{
|
||||
var v = line.Substring("width=".Length);
|
||||
int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out width);
|
||||
}
|
||||
else if (line.StartsWith("bit_rate="))
|
||||
{
|
||||
var v = line.Substring("bit_rate=".Length);
|
||||
double.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out bitrate);
|
||||
}
|
||||
else if (line.StartsWith("height="))
|
||||
{
|
||||
var v = line.Substring("height=".Length);
|
||||
int.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out height);
|
||||
}
|
||||
else if (line.StartsWith("avg_frame_rate="))
|
||||
{
|
||||
var v = line.Substring("avg_frame_rate=".Length);
|
||||
var parts = v.Split('/');
|
||||
if (parts.Length == 2 &&
|
||||
double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var num) &&
|
||||
double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) &&
|
||||
den != 0)
|
||||
{
|
||||
fps = num / den;
|
||||
}
|
||||
fps = num / den;
|
||||
}
|
||||
}
|
||||
|
||||
p.WaitForExit();
|
||||
var bitrate = stream?.Bit_rate ?? 0.0;
|
||||
|
||||
return new(duration, width, height, fps, bitrate);
|
||||
var (sarX, sarY) = ParseSar(stream?.Sample_aspect_ratio);
|
||||
|
||||
return new VideoInfo(duration, width, height, fps, bitrate, sarX, sarY);
|
||||
}
|
||||
|
||||
static double ParseDouble(string? s)
|
||||
{
|
||||
if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v))
|
||||
return v;
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
private static (double x, double y) ParseSar(string? sar)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sar))
|
||||
return (1.0, 1.0);
|
||||
|
||||
var parts = sar.Split(':');
|
||||
if (parts.Length != 2)
|
||||
return (1.0, 1.0);
|
||||
|
||||
if (double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var num) &&
|
||||
double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var den) &&
|
||||
den != 0)
|
||||
{
|
||||
return (num, den);
|
||||
}
|
||||
|
||||
return (1.0, 1.0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user