├── ffmpeg-wjz-sorry-generator
├── Subtitle.cs
├── wwwroot
│ └── favicon.ico
├── appsettings.Development.json
├── VideoTime.cs
├── SubtitleValue.cs
├── .config
│ └── dotnet-tools.json
├── SubtitleDef.cs
├── appsettings.json
├── sorry.csproj
├── Properties
│ └── launchSettings.json
├── DxRes.cs
├── Mp4SourceDef.cs
├── Mp4Source.cs
├── FramesExtensions.cs
└── Program.cs
├── README.md
├── LICENSE.txt
├── sorry.sln
├── .gitattributes
└── .gitignore
/ffmpeg-wjz-sorry-generator/Subtitle.cs:
--------------------------------------------------------------------------------
1 | namespace Sorry;
2 |
3 | public record Subtitle(SubtitleDef Def, SubtitleValue Val);
4 |
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sdcb/sorry/HEAD/ffmpeg-wjz-sorry-generator/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/VideoTime.cs:
--------------------------------------------------------------------------------
1 | namespace Sorry;
2 |
3 | public record struct VideoTime(int Frame, TimeSpan Elapsed, int TotalFrame)
4 | {
5 | public readonly float Percent => 1.0f * Frame / TotalFrame;
6 | }
7 |
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/SubtitleValue.cs:
--------------------------------------------------------------------------------
1 | namespace Sorry;
2 |
3 | public record SubtitleValue(string? Font, float? FontSize, string Text)
4 | {
5 | public static SubtitleValue CreateDefault(string text) => new(null, null, text);
6 | }
7 |
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "dotnet-ef": {
6 | "version": "8.0.0",
7 | "commands": [
8 | "dotnet-ef"
9 | ]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/SubtitleDef.cs:
--------------------------------------------------------------------------------
1 | namespace Sorry;
2 |
3 | public record SubtitleDef(double StartTS, double EndTS, string RefText)
4 | {
5 | public double Duration => EndTS - StartTS;
6 | public bool WillShowInTS(double ts) => StartTS <= ts && ts < EndTS;
7 | }
8 |
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*",
9 | "S3": {
10 | "Enabled": false,
11 | "ServiceUrl": "http://localhost:4572",
12 | "BucketName": "testbucket",
13 | "AccessKey": "test",
14 | "SecretKey": "test"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/sorry.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | Sorry
8 | 21f37615-909b-4218-8473-c33ca3ec04ca
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:25791",
7 | "sslPort": 0
8 | }
9 | },
10 | "profiles": {
11 | "ffmpeg_wjz_sorry_generator": {
12 | "commandName": "Project",
13 | "dotnetRunMessages": true,
14 | "launchBrowser": true,
15 | "applicationUrl": "http://localhost:5147",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "IIS Express": {
21 | "commandName": "IISExpress",
22 | "launchBrowser": true,
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FFmpeg生成GIF动图
2 |
3 | ### 体验网站:
4 | * 新版本(基于Gradio.NET):https://sorry.starworks.cc:88/
5 | * 老版本:https://ffmpeg-sorry-demo.starworks.cc:88/
6 |
7 | ### 要点:
8 | * 视频解码
9 | * 将每一帧转换为BGRA像素格式
10 | * 使用Direct2D读取并绘制字幕
11 | * 将每一帧输入视频过滤器,转换为PAL8格式
12 | * 将PAL8编码像素格式的帧编码为gif
13 |
14 | 示例:
15 | https://ffmpeg-sorry-demo.starworks.cc:88/sorry/generate?type=wjz&subtitle=%E8%BF%98%E6%84%A3%E7%9D%80%E5%B9%B2%E5%98%9B|%E4%B8%8A%E9%A1%B5%E9%9D%A2%E6%98%BE%E7%A4%BA|%E4%B8%8A%E6%8A%A5%E9%94%99%E6%97%A5%E5%BF%97|%E4%BD%A0%E6%89%BE%E5%88%AB%E4%BA%BA%E5%90%A7%EF%BC%8C%E6%88%91%E4%B8%8D%E4%BC%9A
16 |
17 | ### 依赖
18 | * Sdcb.FFmpeg (使用纯C API平台调用生成,不是命令行): https://github.com/sdcb/sdcb.ffmpeg
19 | * [Vortice.Windows] (https://github.com/amerkoleci/Vortice.Windows)
20 | * [Gradio.NET](https://github.com/feiyun0112/Gradio.Net/)
21 |
22 | ### 特别感谢
23 |
24 | [Gradio.NET](https://github.com/feiyun0112/Gradio.Net/)特别好用,有了Gradio.NET,代码量大大减少,开发效率大大提高!
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [year] [fullname]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/sorry.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.4.33103.184
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "sorry", "ffmpeg-wjz-sorry-generator\sorry.csproj", "{369B0054-0E74-47F4-A710-33CA297CB2BD}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决方案项", "{7E4D355C-15E1-4410-B41E-7C53773558A9}"
9 | ProjectSection(SolutionItems) = preProject
10 | README.md = README.md
11 | EndProjectSection
12 | EndProject
13 | Global
14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
15 | Debug|Any CPU = Debug|Any CPU
16 | Release|Any CPU = Release|Any CPU
17 | EndGlobalSection
18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
19 | {369B0054-0E74-47F4-A710-33CA297CB2BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20 | {369B0054-0E74-47F4-A710-33CA297CB2BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
21 | {369B0054-0E74-47F4-A710-33CA297CB2BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
22 | {369B0054-0E74-47F4-A710-33CA297CB2BD}.Release|Any CPU.Build.0 = Release|Any CPU
23 | EndGlobalSection
24 | GlobalSection(SolutionProperties) = preSolution
25 | HideSolutionNode = FALSE
26 | EndGlobalSection
27 | GlobalSection(ExtensibilityGlobals) = postSolution
28 | SolutionGuid = {95CE9915-6EC9-4DD1-8312-F2151C3976B2}
29 | EndGlobalSection
30 | EndGlobal
31 |
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/DxRes.cs:
--------------------------------------------------------------------------------
1 | using Vortice.Direct2D1;
2 | using Vortice.DirectWrite;
3 | using Vortice.DXGI;
4 | using Vortice.Mathematics;
5 | using Vortice.WIC;
6 |
7 | namespace Sorry;
8 |
9 | public class DxRes : IDisposable
10 | {
11 | public readonly IWICImagingFactory WicFactory = new();
12 | public readonly ID2D1Factory2 D2dFactory = D2D1.D2D1CreateFactory();
13 | public readonly IWICBitmap WicBmp;
14 | public readonly ID2D1RenderTarget RenderTarget;
15 | private readonly ID2D1SolidColorBrush DefaultColor;
16 | public readonly IDWriteFactory DWriteFactory = DWrite.DWriteCreateFactory();
17 |
18 | public DxRes(int width, int height)
19 | {
20 | WicBmp = WicFactory.CreateBitmap(width, height, Vortice.WIC.PixelFormat.Format32bppPBGRA, BitmapCreateCacheOption.CacheOnLoad);
21 | RenderTarget = D2dFactory.CreateWicBitmapRenderTarget(WicBmp, new RenderTargetProperties(new Vortice.DCommon.PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied)));
22 | DefaultColor = RenderTarget.CreateSolidColorBrush(Colors.CornflowerBlue);
23 | }
24 |
25 | public ID2D1SolidColorBrush GetColor(Color4 color)
26 | {
27 | DefaultColor.Color = color;
28 | return DefaultColor;
29 | }
30 |
31 | public void Dispose()
32 | {
33 | DefaultColor.Dispose();
34 | RenderTarget.Dispose();
35 | WicBmp.Dispose();
36 | D2dFactory.Dispose();
37 | WicFactory.Dispose();
38 | DWriteFactory.Dispose();
39 | GC.SuppressFinalize(this);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/Mp4SourceDef.cs:
--------------------------------------------------------------------------------
1 | namespace Sorry;
2 |
3 | public record Mp4SourceDef(string Title, string Mp4Url, SubtitleDef[] SubtitleDefs)
4 | {
5 | public IEnumerable GetSubtitlesDefOnTS(float ts) => SubtitleDefs.Where(x => x.WillShowInTS(ts));
6 |
7 | public Mp4Source CreateDefault() => new(Mp4Url, SubtitleDefs.Select(x => new Subtitle(x, SubtitleValue.CreateDefault(x.RefText))).ToArray());
8 |
9 | public Mp4Source CreateLines(params string[] lines) => new(Mp4Url, SubtitleDefs.Zip(lines).Select(x => new Subtitle(x.First, SubtitleValue.CreateDefault(x.Second))).ToArray());
10 |
11 | public string CombinedText => string.Join(Environment.NewLine, SubtitleDefs.Select(x => x.RefText));
12 |
13 | public static readonly Mp4SourceDef Xiang = new("真香", "https://io.starworks.cc:88/cv-public/2022/gif-wjz.mp4",
14 | [
15 | new SubtitleDef(0, 1.04, "还愣着干嘛"),
16 | new SubtitleDef(1.46, 2.9, "上页面显示"),
17 | new SubtitleDef(3.09, 4.33, "上报错日志"),
18 | new SubtitleDef(4.59, 5.93, "你找别人吧,我不会"),
19 | ]);
20 |
21 | public static readonly Mp4SourceDef Sorry = new("Sorry", "https://io.starworks.cc:88/cv-public/2022/gif-sorry.mp4",
22 | [
23 | new SubtitleDef(1.18, 1.56, "好啊"),
24 | new SubtitleDef(3.18, 4.43, "就算你是一流程序员"),
25 | new SubtitleDef(5.31, 7.43, "就算你代码再完美"),
26 | new SubtitleDef(7.56, 9.93, "毕竟我是产品"),
27 | new SubtitleDef(10.06, 11.56, "我叫你改需求你就要改"),
28 | new SubtitleDef(11.93, 13.06, "产品了不起啊"),
29 | new SubtitleDef(13.81, 16.31, "sorry 产品真的也不起"),
30 | new SubtitleDef(18.06, 19.56, "以后天天让他改需求"),
31 | new SubtitleDef(19.60, 21.60, "哈哈,天天改"),
32 | ]);
33 |
34 | public static readonly Mp4SourceDef Nlgl = new("哪里贵了", "https://io.starworks.cc:88/cv-public/2023/nlgl-mini.mp4",
35 | [
36 | new SubtitleDef(0.333, 1.399, "Java 8越来越不行了?"),
37 | new SubtitleDef(1.400, 2.333, "哪里不行了?"),
38 | new SubtitleDef(2.766, 4.733, "这么多年来大家都用Java 8"),
39 | new SubtitleDef(4.733, 6.166, "不要睁着眼睛乱说"),
40 | new SubtitleDef(6.166, 8.966, "我们Java程序员很不容易的哦"),
41 | new SubtitleDef(8.966, 10.199, "而且Java真的不是那种"),
42 | new SubtitleDef(10.200, 11.700, "只能写写Hello World的语言"),
43 | new SubtitleDef(11.700, 13.966, "哎,我写Java多少年了"),
44 | new SubtitleDef(13.966, 15.899, "它怎么牛逼我是最知道的"),
45 | new SubtitleDef(15.900, 16.700, "是啊"),
46 | new SubtitleDef(17.100, 18.966, "C#就语法好点"),
47 | new SubtitleDef(19.100, 19.900, "哈哈好不好"),
48 | new SubtitleDef(19.900, 20.600, "真的乱说"),
49 | new SubtitleDef(20.600, 22.266, "这么多年都是Java 8"),
50 | new SubtitleDef(22.266, 23.333, "哪里落后了"),
51 | new SubtitleDef(23.366, 24.899, "有的时候找找自己原因好吧"),
52 | new SubtitleDef(24.900, 25.333, "这么多年了"),
53 | new SubtitleDef(25.333, 25.999, "工资涨没涨"),
54 | new SubtitleDef(26.000, 28.200, "有没有认真工作好不好"),
55 | new SubtitleDef(28.800, 29.966, "这么多年都用的Java 8"),
56 | new SubtitleDef(29.966, 30.899, "我真的快疯掉了"),
57 | ]);
58 |
59 | public static Mp4SourceDef[] All => [Xiang, Sorry, Nlgl];
60 | }
61 |
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/Mp4Source.cs:
--------------------------------------------------------------------------------
1 | using Sdcb.FFmpeg.Codecs;
2 | using Sdcb.FFmpeg.Formats;
3 | using Sdcb.FFmpeg.Raw;
4 | using Sdcb.FFmpeg.Toolboxs.Extensions;
5 | using System.Numerics;
6 | using Vortice.Direct2D1;
7 | using Vortice.DirectWrite;
8 | using Vortice.Mathematics;
9 |
10 | namespace Sorry;
11 |
12 | public record Mp4Source(string Mp4Url, Subtitle[] Subtitles)
13 | {
14 | public IEnumerable GetSubtitlesOnTS(double ts) => Subtitles.Where(x => x.Def.WillShowInTS(ts));
15 |
16 | static void RenderOneFrame(VideoTime time, ID2D1RenderTarget ctx, DxRes res, Mp4Source mp4Source)
17 | {
18 | IEnumerable subtitles = mp4Source.GetSubtitlesOnTS(time.Elapsed.TotalSeconds);
19 | foreach (Subtitle subtitle in subtitles)
20 | {
21 | using IDWriteTextFormat font = res.DWriteFactory.CreateTextFormat(subtitle.Val.Font ?? "Consolas", subtitle.Val.FontSize ?? 20);
22 | using IDWriteTextLayout layout = res.DWriteFactory.CreateTextLayout(subtitle.Val.Text, font, ctx.Size.Width, ctx.Size.Height);
23 | TextMetrics metrics = layout.Metrics;
24 | ctx.Transform = Matrix3x2.CreateTranslation(-metrics.Width / 2, -metrics.Height / 2) * Matrix3x2.CreateTranslation(ctx.Size.Width / 2, ctx.Size.Height * 0.80f) * Matrix3x2.CreateTranslation(font.FontSize / 14, font.FontSize / 14);
25 | ctx.DrawTextLayout(Vector2.Zero, layout, res.GetColor(Colors.Black));
26 | ctx.Transform = Matrix3x2.CreateTranslation(-metrics.Width / 2, -metrics.Height / 2) * Matrix3x2.CreateTranslation(ctx.Size.Width / 2, ctx.Size.Height * 0.80f);
27 | ctx.DrawTextLayout(Vector2.Zero, layout, res.GetColor(Colors.White));
28 | }
29 | }
30 |
31 | public byte[] DecodeAddSubtitle()
32 | {
33 | byte[] downloadedMp4 = new HttpClient().GetByteArrayAsync(Mp4Url).GetAwaiter().GetResult();
34 | using IOContext inIO = IOContext.ReadStream(new MemoryStream(downloadedMp4));
35 | using FormatContext inFc = FormatContext.OpenInputIO(inIO);
36 | inFc.LoadStreamInfo();
37 | MediaStream inVideoStream = inFc.GetVideoStream();
38 | CodecParameters inCodecpar = inVideoStream.Codecpar ?? throw new InvalidOperationException("Codecpar should not be null");
39 | double durationInSeconds = inVideoStream.GetDurationInSeconds();
40 | using CodecContext inVCodec = new(Codec.FindDecoderById(inVideoStream.Codecpar.CodecId));
41 | inVCodec.FillParameters(inCodecpar);
42 | inVCodec.Open();
43 |
44 | using FormatContext fc = FormatContext.AllocOutput(formatName: "gif");
45 | fc.VideoCodec = Codec.FindEncoderById(AVCodecID.Gif);
46 | MediaStream vstream = fc.NewStream(fc.VideoCodec);
47 | using CodecContext vcodec = new(fc.VideoCodec)
48 | {
49 | Width = inCodecpar.Width,
50 | Height = inCodecpar.Height,
51 | TimeBase = inVideoStream.RFrameRate.Inverse(),
52 | PixelFormat = AVPixelFormat.Pal8,
53 | };
54 | vcodec.Open(fc.VideoCodec);
55 | vstream.Codecpar!.CopyFrom(vcodec);
56 | vstream.TimeBase = vcodec.TimeBase;
57 |
58 | using DynamicIOContext io = IOContext.OpenDynamic();
59 | fc.Pb = io;
60 | fc.WriteHeader();
61 | int frameCount = (int)Math.Ceiling(durationInSeconds / vcodec.TimeBase.ToDouble());
62 | inFc.ReadPackets(inVideoStream.Index)
63 | .DecodePackets(inVCodec)
64 | .ConvertVideoFrames(() => new SizeI(vcodec.Width, vcodec.Height), AVPixelFormat.Bgra)
65 | .RenderAll(vcodec, RenderOneFrame, this, frameCount: frameCount)
66 | //.ConvertFrames(vcodec)
67 | .ApplyVideoFilters(vcodec.TimeBase, AVPixelFormat.Pal8, $"scale=flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse")
68 | .EncodeAllFrames(fc, null, vcodec)
69 | .WriteAll(fc);
70 | fc.WriteTrailer();
71 | return io.GetBuffer().ToArray();
72 | }
73 | }
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/FramesExtensions.cs:
--------------------------------------------------------------------------------
1 | using Sdcb.FFmpeg.Codecs;
2 | using Sdcb.FFmpeg.Raw;
3 | using Sdcb.FFmpeg.Swscales;
4 | using Sdcb.FFmpeg.Utils;
5 | using Sorry;
6 | using System.Numerics;
7 | using Vortice.Direct2D1;
8 | using Vortice.DXGI;
9 | using Vortice.Mathematics;
10 | using Vortice.WIC;
11 |
12 | namespace Sorry;
13 |
14 | public static class FramesExtensions
15 | {
16 | public static IEnumerable RenderAll(this IEnumerable frames, CodecContext codecCtx, FrameRendererDelegate frameRenderer, Mp4Source mp4Source, int frameCount, bool unref = true)
17 | {
18 | using DxRes basic = new(codecCtx.Width, codecCtx.Height);
19 | using VideoFrameConverter frameConverter = new();
20 | using Frame rgbFrame = new()
21 | {
22 | Width = codecCtx.Width,
23 | Height = codecCtx.Height,
24 | Format = (int)AVPixelFormat.Bgra
25 | };
26 | using Frame refFrame = new();
27 |
28 | int i = 0;
29 | foreach (Frame frame in frames)
30 | {
31 | using ID2D1DeviceContext ctx = basic.RenderTarget.QueryInterface();
32 | using ID2D1Bitmap bmp = ctx.CreateBitmap(new SizeI(frame.Width, frame.Height), frame.Data._0, frame.Linesize[0], new BitmapProperties1(new Vortice.DCommon.PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied)));
33 | if (unref) frame.Unref();
34 | ctx.BeginDraw();
35 | {
36 | ctx.Transform = Matrix3x2.Identity;
37 | ctx.DrawImage(bmp, new Vector2(0, 0));
38 | VideoTime time = new(i, TimeSpan.FromSeconds(1.0 * i * codecCtx.TimeBase.Num / codecCtx.TimeBase.Den), frameCount);
39 | frameRenderer(time, ctx, basic, mp4Source);
40 | }
41 | ctx.EndDraw();
42 |
43 | using (IWICBitmapLock bmpLock = basic.WicBmp.Lock(BitmapLockFlags.Read))
44 | {
45 | rgbFrame.Data._0 = bmpLock.Data.DataPointer;
46 | rgbFrame.Linesize[0] = bmpLock.Data.Pitch;
47 | refFrame.Ref(rgbFrame);
48 | yield return refFrame;
49 | }
50 | ++i;
51 | };
52 | }
53 |
54 | public static IEnumerable ConvertVideoFrames(this IEnumerable sourceFrames, Func sizeAccessor, AVPixelFormat pixelFormat, SWS swsFlags = SWS.Bilinear, bool unref = true)
55 | {
56 | Frame dest = null!;
57 | {
58 | SizeI size = sizeAccessor();
59 | dest = Frame.CreateVideo(size.Width, size.Height, pixelFormat);
60 | }
61 | using Frame destRef = new();
62 |
63 | try
64 | {
65 | int pts = 0;
66 | using VideoFrameConverter frameConverter = new();
67 | foreach (Frame sourceFrame in sourceFrames)
68 | {
69 | if (sourceFrame.Width > 0)
70 | {
71 | SizeI newSize = sizeAccessor();
72 | if (dest.Width != newSize.Width || dest.Height != newSize.Height)
73 | {
74 | dest.Dispose();
75 | dest = Frame.CreateVideo(newSize.Width, newSize.Height, pixelFormat);
76 | }
77 |
78 | dest.MakeWritable();
79 | frameConverter.ConvertFrame(sourceFrame, dest, swsFlags);
80 | if (unref) sourceFrame.Unref();
81 | dest.Pts = pts++;
82 | destRef.Ref(dest);
83 | yield return destRef;
84 | }
85 | else
86 | {
87 | // bypass not a video frame
88 | yield return sourceFrame;
89 | }
90 | }
91 | }
92 | finally
93 | {
94 | dest.Dispose();
95 | }
96 | }
97 | }
98 |
99 | public delegate void FrameRendererDelegate(VideoTime time, ID2D1RenderTarget ctx, DxRes res, Mp4Source mp4Source);
--------------------------------------------------------------------------------
/ffmpeg-wjz-sorry-generator/Program.cs:
--------------------------------------------------------------------------------
1 | using Amazon.S3;
2 | using Amazon.S3.Model;
3 | using Gradio.Net;
4 | using System.Net;
5 | using System.Text.RegularExpressions;
6 |
7 | namespace Sorry;
8 |
9 | public static partial class Program
10 | {
11 | static void Main()
12 | {
13 | WebApplicationBuilder builder = WebApplication.CreateBuilder();
14 | builder.Services.AddGradio();
15 | WebApplication webApplication = builder.Build();
16 | webApplication.UseGradio(CreateBlocks(webApplication.Services));
17 | webApplication.Run();
18 | }
19 |
20 | [GeneratedRegex(@"\r\n|\r|\n")]
21 | public static partial Regex LineSpliter();
22 |
23 | static Blocks CreateBlocks(IServiceProvider sp)
24 | {
25 | using Blocks blocks = gr.Blocks();
26 |
27 |
28 | gr.Markdown("# 视频模板、字幕,生成视频gif");
29 | Radio template = gr.Radio(Mp4SourceDef.All.Select(x => x.Title).ToArray(), label: "选择模板", value: Mp4SourceDef.All.First().Title);
30 |
31 | Image image;
32 | Textbox subtitle;
33 | using (gr.Row())
34 | {
35 | subtitle = gr.Textbox(Mp4SourceDef.Xiang.CombinedText, lines: 8, label: "输入字幕");
36 | image = gr.Image(interactive: false);
37 | }
38 | gr.Button("生成视频").Click(async i =>
39 | {
40 | string template = Radio.Payload(i.Data[0]).Single();
41 | Mp4SourceDef? def = Mp4SourceDef.All.FirstOrDefault(x => x.Title == template);
42 | if (def == null) throw new Exception($"模板{template}错误,请输入 {string.Join("|", Mp4SourceDef.All.Select(x => x.Title))} 之一");
43 | string subtitle = i.Data[1].ToString()!;
44 |
45 | byte[] gif = def.CreateLines(LineSpliter().Split(subtitle)).DecodeAddSubtitle();
46 | string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
47 | File.WriteAllBytes(Path.Combine(desktop, "output.gif"), gif);
48 |
49 | IConfiguration config = sp.GetRequiredService();
50 | string path = null!;
51 | if (config.GetValue("S3:Enabled"))
52 | {
53 | string accessKey = config["S3:AccessKey"] ?? throw new Exception("S3:AccessKey is required");
54 | string secret = config["S3:SecretKey"] ?? throw new Exception("S3:SecretKey is required");
55 | string bucketName = config["S3:BucketName"] ?? throw new Exception("S3:BucketName is required");
56 | string serviceUrl = config["S3:ServiceUrl"] ?? throw new Exception("S3:ServiceUrl is required");
57 |
58 | using AmazonS3Client s3 = new(accessKey, secret, new AmazonS3Config
59 | {
60 | ForcePathStyle = true,
61 | ServiceURL = serviceUrl,
62 | });
63 | string objectKey = $"{DateTime.Now:yyyy/MM/dd}/{template}-{Guid.NewGuid()}.gif";
64 | PutObjectResponse resp = await s3.PutObjectAsync(new PutObjectRequest()
65 | {
66 | BucketName = bucketName,
67 | Key = objectKey,
68 | InputStream = new MemoryStream(gif),
69 | ContentType = "image/gif",
70 | });
71 | if (resp.HttpStatusCode != HttpStatusCode.OK) throw new Exception($"上传失败 {resp.HttpStatusCode}");
72 |
73 | string downloadUrl = s3.GetPreSignedURL(new GetPreSignedUrlRequest()
74 | {
75 | BucketName = bucketName,
76 | Key = objectKey,
77 | Expires = DateTime.UtcNow.AddHours(1),
78 | });
79 | path = downloadUrl;
80 | }
81 | else
82 | {
83 | path = Path.GetTempFileName();
84 | File.WriteAllBytes(path, gif);
85 | }
86 |
87 | return gr.Output(path);
88 | }, inputs: [template, subtitle], outputs: [image]);
89 |
90 | template.Select(i =>
91 | {
92 | string template = Radio.Payload(i.Data[0]).Single();
93 | Mp4SourceDef? def = Mp4SourceDef.All.FirstOrDefault(x => x.Title == template);
94 | if (def == null) throw new Exception($"模板{template}错误,请输入 {string.Join("|", Mp4SourceDef.All.Select(x => x.Title))} 之一");
95 | return Task.FromResult(gr.Output(def.CombinedText));
96 | }, inputs: [template], outputs: [subtitle]);
97 |
98 | using (gr.Row())
99 | {
100 | gr.Markdown("""
101 | ## Github:
102 | * https://github.com/sdcb/sorry
103 | * https://github.com/sdcb/Sdcb.FFmpeg
104 | """);
105 |
106 | gr.Markdown("""
107 | ## QQ: 495782587
108 | """);
109 | }
110 |
111 | return blocks;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
--------------------------------------------------------------------------------