📄
src/Infrastructure/Ffmpeg/ClipExtractor.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
using System; using System.Drawing; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using FFMpegCore; using FFMpegCore.Arguments; using FFMpegCore.Enums; using Microsoft.Extensions.Logging; using Slopper.Domain; namespace Slopper.Infrastructure.Ffmpeg; internal sealed partial class ClipExtractor(ILogger<ClipExtractor> logger) : IClipExtractor { public async Task ExtractClip( MediaItem media, TimeSpan start, TimeSpan duration, string outputPath, CancellationToken cancellationToken ) { var crop = await AnalyzeCropArea(media, start, duration, cancellationToken); await CreateClip(media, start, duration, crop, outputPath, cancellationToken); } private async Task<Rectangle?> AnalyzeCropArea( MediaItem media, TimeSpan start, TimeSpan duration, CancellationToken cancellationToken ) { Rectangle? crop = null; var args = FFMpegArguments .FromFileInput(media.Path, addArguments: options => options.Seek(start).EndSeek(start + duration)) .OutputToFile( "-", addArguments: options => options .WithVideoFilters(filter => filter .Add("zscale", "t=linear:npl=100") .Add("format", "yuv420p") .Add("cropdetect", "mode=black:limit=24:round=2") ) .ForceFormat("null") ) .NotifyOnError(s => { var match = CropPattern.Match(s); if ( !match.Success || !int.TryParse(match.Groups[1].ValueSpan, out var width) || !int.TryParse(match.Groups[2].ValueSpan, out var height) || !int.TryParse(match.Groups[3].ValueSpan, out var x) || !int.TryParse(match.Groups[4].ValueSpan, out var y) ) { return; } crop = new(x, y, width, height); }) .CancellableThrough(cancellationToken); using var activity = Tracing.StartCropAreaAnalysis(media.Path, start, duration); logger.LogInformation("Running ffmpeg {FfmpegArguments}", args.Arguments); await args.ProcessAsynchronously(); return crop; } [GeneratedRegex(@"crop=(\d+):(\d+):(\d+):(\d+)")] private static partial Regex CropPattern { get; } private async Task CreateClip( MediaItem media, TimeSpan start, TimeSpan duration, Rectangle? crop, string outputPath, CancellationToken cancellationToken ) { var subtitleOptions = await CreateSubtitleOptions(media, cancellationToken); var args = FFMpegArguments .FromFileInput( media.Path, addArguments: options => options.Seek(start).EndSeek(start + duration).WithArgument("-copyts") ) .OutputToFile( outputPath, addArguments: options => options .Seek(start) .EndSeek(start + duration) .WithVideoFilters(filter => { if (crop is { Width: var width, Height: var height, X: var x, Y: var y }) { filter.Add("crop", $"{width}:{height}:{x}:{y}"); } filter.Add("split", "2[s0][s1]"); filter.Add("[s0]scale", "1080:-1[sharp]"); filter .Add("[s1]scale", "-1:1920[scaled1]") .Add("[scaled1]gblur", "sigma=30[blurred]") .Add("[blurred]crop", "1080:1920:(iw - 1080) / 2:0[cropped]") .Add("[cropped][sharp]overlay", "0:(H - h) / 2"); filter.HardBurnSubtitle(subtitleOptions); }) .WithFastStart() .WithVideoCodec(VideoCodec.LibX264) .WithAudioCodec(AudioCodec.Aac) .WithFramerate(30.0d) .WithVideoBitrate(4000) .WithAudioBitrate(128) .ForceFormat("mp4") ) .CancellableThrough(cancellationToken); using var activity = Tracing.StartCreateClip(media.Path, start, duration); logger.LogInformation("Running ffmpeg {FfmpegArguments}", args.Arguments); await args.ProcessAsynchronously(); } private async Task<SubtitleHardBurnOptions> CreateSubtitleOptions( MediaItem media, CancellationToken cancellationToken ) => ( media.Subtitles switch { Subtitles.External(var path) => SubtitleHardBurnOptions.Create(path), Subtitles.Embedded(var index) => await CreateEmbeddedSubtitleOptions( media.Path, index, cancellationToken ), _ => throw new ArgumentException("Unknown subtitle type"), } ).WithParameter( "force_style", "FontSize=10,BorderStyle=1,Alignment=2,MarginV=80,PrimaryColour=&HFFFFFF&,BackColour=&H000000&" ); private async Task<SubtitleHardBurnOptions> CreateEmbeddedSubtitleOptions( string path, int index, CancellationToken cancellationToken ) { var analysis = await FFProbe.AnalyseAsync(path, cancellationToken: cancellationToken); var subIndex = analysis.SubtitleStreams.Count(s => s.Index < index); return SubtitleHardBurnOptions.Create(path).SetSubtitleIndex(subIndex); } }