diff --git a/.gitignore b/.gitignore
index a6c973c..56da5ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
/.idea/
/bin/
-/obj/
\ No newline at end of file
+/obj/
+**/bin/
+**/obj/
\ No newline at end of file
diff --git a/LottieViewConvert/Lang/Resources.Designer.cs b/LottieViewConvert/Lang/Resources.Designer.cs
index 7d17426..3c66a5d 100644
--- a/LottieViewConvert/Lang/Resources.Designer.cs
+++ b/LottieViewConvert/Lang/Resources.Designer.cs
@@ -1788,6 +1788,15 @@ public static string SaveAsGif {
}
}
+ ///
+ /// Looks up a localized string similar to Playback Speed.
+ ///
+ public static string PlaybackSpeed {
+ get {
+ return ResourceManager.GetString("PlaybackSpeed", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Save failed.
///
diff --git a/LottieViewConvert/Lang/Resources.resx b/LottieViewConvert/Lang/Resources.resx
index 9157d76..b20add0 100644
--- a/LottieViewConvert/Lang/Resources.resx
+++ b/LottieViewConvert/Lang/Resources.resx
@@ -777,6 +777,9 @@
Save as Gif
+
+ Playback Speed
+
Scale
diff --git a/LottieViewConvert/Lang/Resources.zh.resx b/LottieViewConvert/Lang/Resources.zh.resx
index 0c36c1b..1597adf 100644
--- a/LottieViewConvert/Lang/Resources.zh.resx
+++ b/LottieViewConvert/Lang/Resources.zh.resx
@@ -776,6 +776,9 @@
保存为 Gif
+
+ 播放速度
+
缩放
diff --git a/LottieViewConvert/ViewModels/TgsDownloadViewModel.cs b/LottieViewConvert/ViewModels/TgsDownloadViewModel.cs
index cd9b74a..e36693d 100644
--- a/LottieViewConvert/ViewModels/TgsDownloadViewModel.cs
+++ b/LottieViewConvert/ViewModels/TgsDownloadViewModel.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive;
@@ -11,6 +13,7 @@
using Avalonia.Controls.Notifications;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
+using ImageMagick;
using LottieViewConvert.Helper;
using LottieViewConvert.Helper.Convert;
using LottieViewConvert.Helper.LogHelper;
@@ -21,8 +24,6 @@
using Material.Icons;
using ReactiveUI;
using SukiUI.Toasts;
-using System.Collections.Generic;
-using ImageMagick;
namespace LottieViewConvert.ViewModels;
@@ -192,6 +193,13 @@ public double SaveGifProgress
get => _saveGifProgress;
set => this.RaiseAndSetIfChanged(ref _saveGifProgress, value);
}
+
+ private double _playbackSpeed = 1.0;
+ public double PlaybackSpeed
+ {
+ get => _playbackSpeed;
+ set => this.RaiseAndSetIfChanged(ref _playbackSpeed, Math.Max(0.25, Math.Min(4.0, value)));
+ }
public string SelectionCountText => $"{Resources.Selected} {StickerItems.Count(x => x.IsSelected)} / {StickerItems.Count}";
public string SelectedCountText => $"{Resources.Selected} {StickerItems.Count(x => x.IsSelected)} {Resources.Sticker}";
@@ -427,6 +435,70 @@ private async Task SaveSelectedStickers()
.Queue();
}
}
+
+ private async Task GetVideoFrameRateAsync(string videoPath, CommandExecutor executor)
+ {
+ try
+ {
+ // Use ffprobe to get the frame rate
+ var args = new List
+ {
+ "-v", "error",
+ "-select_streams", "v:0",
+ "-show_entries", "stream=r_frame_rate",
+ "-of", "default=noprint_wrappers=1:nokey=1",
+ videoPath
+ };
+
+ var tempOutput = Path.Combine(Path.GetTempPath(), $"fps_{Guid.NewGuid()}.txt");
+ try
+ {
+ // Execute ffprobe and capture output
+ var process = new System.Diagnostics.Process
+ {
+ StartInfo = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = "ffprobe",
+ Arguments = string.Join(" ", args.Select(a => a.Contains(" ") ? $"\"{a}\"" : a)),
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ }
+ };
+
+ process.Start();
+ var output = await process.StandardOutput.ReadToEndAsync();
+ await process.WaitForExitAsync();
+
+ if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output))
+ {
+ // Parse frame rate (format is usually "30/1" or "30000/1001")
+ var parts = output.Trim().Split('/');
+ if (parts.Length == 2 &&
+ double.TryParse(parts[0], out var numerator) &&
+ double.TryParse(parts[1], out var denominator) &&
+ denominator > 0)
+ {
+ return numerator / denominator;
+ }
+ }
+ }
+ finally
+ {
+ if (File.Exists(tempOutput))
+ File.Delete(tempOutput);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"Failed to get video frame rate: {ex}");
+ }
+
+ // Default to 30 fps if detection fails
+ return 30.0;
+ }
+
private async Task SaveSelectedStickersAsGif()
{
// initialize UI state for GIF saving
@@ -443,6 +515,9 @@ await Dispatcher.UIThread.InvokeAsync(() =>
try
{
+ // Capture the playback speed value to avoid threading issues
+ var playbackSpeed = PlaybackSpeed;
+
await Task.Run(async () =>
{
var totalCount = selectedStickers.Count;
@@ -461,6 +536,17 @@ await Task.Run(async () =>
{
using var imgList = new MagickImageCollection();
await imgList.ReadAsync(sticker.FilePath);
+ // Apply playback speed adjustment for animated images (like WebP)
+ if (imgList.Count > 1)
+ {
+ foreach (var img in imgList)
+ {
+ if (img.AnimationDelay > 0)
+ {
+ img.AnimationDelay = (uint)Math.Max(1, img.AnimationDelay / playbackSpeed);
+ }
+ }
+ }
await imgList.WriteAsync(destPath);
}
else
@@ -469,11 +555,22 @@ await Task.Run(async () =>
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var exec = new CommandExecutor();
+
+ // Get the frame rate from the WebM file
+ var fps = await GetVideoFrameRateAsync(sticker.FilePath, exec);
+
+ // Apply speed adjustment using ffmpeg's setpts filter
+ // For speed adjustment: setpts=PTS/speed (e.g., PTS/2 for 2x speed, PTS*2 for 0.5x speed)
+ var ptsMultiplier = 1.0 / playbackSpeed; // Inverse for setpts filter
+ var videoFilter = $"setpts={ptsMultiplier:F4}*PTS,format=yuva420p";
+
+ // Extract frames with speed adjustment applied
var extractArgs = new List
{
"-hide_banner", "-y",
- "-vcodec", "libvpx-vp9", "-i", sticker.FilePath,
- "-vf", "format=yuva420p", "-c:v", "png", "-pix_fmt", "rgba",
+ "-i", sticker.FilePath,
+ "-vf", videoFilter,
+ "-c:v", "png", "-pix_fmt", "rgba",
Path.Combine(tempDir, "frame_%03d.png")
};
if (await exec.ExecuteAsync("ffmpeg", extractArgs, Path.GetDirectoryName(sticker.FilePath) ?? string.Empty))
@@ -486,9 +583,12 @@ await Task.Run(async () =>
img.Alpha(AlphaOption.Set);
imgList.Add(img);
}
+ // Calculate delay based on the actual frame rate
+ // Since we've already adjusted speed in ffmpeg, use the original fps for delay
+ var delay = fps > 0 ? (uint)Math.Max(1, Math.Round(100.0 / fps)) : 3;
foreach (var img in imgList)
{
- img.AnimationDelay = 3;
+ img.AnimationDelay = delay;
img.Format = MagickFormat.Gif;
img.GifDisposeMethod = GifDisposeMethod.Background;
img.BackgroundColor = MagickColors.Transparent;
diff --git a/LottieViewConvert/Views/TgsDownloadView.axaml b/LottieViewConvert/Views/TgsDownloadView.axaml
index 0aa791c..4557534 100644
--- a/LottieViewConvert/Views/TgsDownloadView.axaml
+++ b/LottieViewConvert/Views/TgsDownloadView.axaml
@@ -325,25 +325,49 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+