11using System ;
2+ using System . Collections . Generic ;
23using System . Collections . ObjectModel ;
4+ using System . Diagnostics ;
35using System . IO ;
46using System . Linq ;
57using System . Reactive ;
1113using Avalonia . Controls . Notifications ;
1214using Avalonia . Media . Imaging ;
1315using Avalonia . Threading ;
16+ using ImageMagick ;
1417using LottieViewConvert . Helper ;
1518using LottieViewConvert . Helper . Convert ;
1619using LottieViewConvert . Helper . LogHelper ;
2124using Material . Icons ;
2225using ReactiveUI ;
2326using SukiUI . Toasts ;
24- using System . Collections . Generic ;
25- using ImageMagick ;
2627
2728namespace LottieViewConvert . ViewModels ;
2829
@@ -192,6 +193,13 @@ public double SaveGifProgress
192193 get => _saveGifProgress ;
193194 set => this . RaiseAndSetIfChanged ( ref _saveGifProgress , value ) ;
194195 }
196+
197+ private double _playbackSpeed = 1.0 ;
198+ public double PlaybackSpeed
199+ {
200+ get => _playbackSpeed ;
201+ set => this . RaiseAndSetIfChanged ( ref _playbackSpeed , Math . Max ( 0.25 , Math . Min ( 4.0 , value ) ) ) ;
202+ }
195203
196204 public string SelectionCountText => $ "{ Resources . Selected } { StickerItems . Count ( x => x . IsSelected ) } / { StickerItems . Count } ";
197205 public string SelectedCountText => $ "{ Resources . Selected } { StickerItems . Count ( x => x . IsSelected ) } { Resources . Sticker } ";
@@ -427,6 +435,70 @@ private async Task SaveSelectedStickers()
427435 . Queue ( ) ;
428436 }
429437 }
438+
439+ private async Task < double > GetVideoFrameRateAsync ( string videoPath , CommandExecutor executor )
440+ {
441+ try
442+ {
443+ // Use ffprobe to get the frame rate
444+ var args = new List < string >
445+ {
446+ "-v" , "error" ,
447+ "-select_streams" , "v:0" ,
448+ "-show_entries" , "stream=r_frame_rate" ,
449+ "-of" , "default=noprint_wrappers=1:nokey=1" ,
450+ videoPath
451+ } ;
452+
453+ var tempOutput = Path . Combine ( Path . GetTempPath ( ) , $ "fps_{ Guid . NewGuid ( ) } .txt") ;
454+ try
455+ {
456+ // Execute ffprobe and capture output
457+ var process = new System . Diagnostics . Process
458+ {
459+ StartInfo = new System . Diagnostics . ProcessStartInfo
460+ {
461+ FileName = "ffprobe" ,
462+ Arguments = string . Join ( " " , args . Select ( a => a . Contains ( " " ) ? $ "\" { a } \" " : a ) ) ,
463+ RedirectStandardOutput = true ,
464+ RedirectStandardError = true ,
465+ UseShellExecute = false ,
466+ CreateNoWindow = true
467+ }
468+ } ;
469+
470+ process . Start ( ) ;
471+ var output = await process . StandardOutput . ReadToEndAsync ( ) ;
472+ await process . WaitForExitAsync ( ) ;
473+
474+ if ( process . ExitCode == 0 && ! string . IsNullOrWhiteSpace ( output ) )
475+ {
476+ // Parse frame rate (format is usually "30/1" or "30000/1001")
477+ var parts = output . Trim ( ) . Split ( '/' ) ;
478+ if ( parts . Length == 2 &&
479+ double . TryParse ( parts [ 0 ] , out var numerator ) &&
480+ double . TryParse ( parts [ 1 ] , out var denominator ) &&
481+ denominator > 0 )
482+ {
483+ return numerator / denominator ;
484+ }
485+ }
486+ }
487+ finally
488+ {
489+ if ( File . Exists ( tempOutput ) )
490+ File . Delete ( tempOutput ) ;
491+ }
492+ }
493+ catch ( Exception ex )
494+ {
495+ Logger . Error ( $ "Failed to get video frame rate: { ex } ") ;
496+ }
497+
498+ // Default to 30 fps if detection fails
499+ return 30.0 ;
500+ }
501+
430502 private async Task SaveSelectedStickersAsGif ( )
431503 {
432504 // initialize UI state for GIF saving
@@ -443,6 +515,9 @@ await Dispatcher.UIThread.InvokeAsync(() =>
443515
444516 try
445517 {
518+ // Capture the playback speed value to avoid threading issues
519+ var playbackSpeed = PlaybackSpeed ;
520+
446521 await Task . Run ( async ( ) =>
447522 {
448523 var totalCount = selectedStickers . Count ;
@@ -461,6 +536,17 @@ await Task.Run(async () =>
461536 {
462537 using var imgList = new MagickImageCollection ( ) ;
463538 await imgList . ReadAsync ( sticker . FilePath ) ;
539+ // Apply playback speed adjustment for animated images (like WebP)
540+ if ( imgList . Count > 1 )
541+ {
542+ foreach ( var img in imgList )
543+ {
544+ if ( img . AnimationDelay > 0 )
545+ {
546+ img . AnimationDelay = ( uint ) Math . Max ( 1 , img . AnimationDelay / playbackSpeed ) ;
547+ }
548+ }
549+ }
464550 await imgList . WriteAsync ( destPath ) ;
465551 }
466552 else
@@ -469,11 +555,22 @@ await Task.Run(async () =>
469555 var tempDir = Path . Combine ( Path . GetTempPath ( ) , Guid . NewGuid ( ) . ToString ( ) ) ;
470556 Directory . CreateDirectory ( tempDir ) ;
471557 var exec = new CommandExecutor ( ) ;
558+
559+ // Get the frame rate from the WebM file
560+ var fps = await GetVideoFrameRateAsync ( sticker . FilePath , exec ) ;
561+
562+ // Apply speed adjustment using ffmpeg's setpts filter
563+ // For speed adjustment: setpts=PTS/speed (e.g., PTS/2 for 2x speed, PTS*2 for 0.5x speed)
564+ var ptsMultiplier = 1.0 / playbackSpeed ; // Inverse for setpts filter
565+ var videoFilter = $ "setpts={ ptsMultiplier : F4} *PTS,format=yuva420p";
566+
567+ // Extract frames with speed adjustment applied
472568 var extractArgs = new List < string >
473569 {
474570 "-hide_banner" , "-y" ,
475- "-vcodec" , "libvpx-vp9" , "-i" , sticker . FilePath ,
476- "-vf" , "format=yuva420p" , "-c:v" , "png" , "-pix_fmt" , "rgba" ,
571+ "-i" , sticker . FilePath ,
572+ "-vf" , videoFilter ,
573+ "-c:v" , "png" , "-pix_fmt" , "rgba" ,
477574 Path . Combine ( tempDir , "frame_%03d.png" )
478575 } ;
479576 if ( await exec . ExecuteAsync ( "ffmpeg" , extractArgs , Path . GetDirectoryName ( sticker . FilePath ) ?? string . Empty ) )
@@ -486,9 +583,12 @@ await Task.Run(async () =>
486583 img . Alpha ( AlphaOption . Set ) ;
487584 imgList . Add ( img ) ;
488585 }
586+ // Calculate delay based on the actual frame rate
587+ // Since we've already adjusted speed in ffmpeg, use the original fps for delay
588+ var delay = fps > 0 ? ( uint ) Math . Max ( 1 , Math . Round ( 100.0 / fps ) ) : 3 ;
489589 foreach ( var img in imgList )
490590 {
491- img . AnimationDelay = 3 ;
591+ img . AnimationDelay = delay ;
492592 img . Format = MagickFormat . Gif ;
493593 img . GifDisposeMethod = GifDisposeMethod . Background ;
494594 img . BackgroundColor = MagickColors . Transparent ;
0 commit comments