├── 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 --------------------------------------------------------------------------------