Skip to content

Commit

Permalink
✨ Implement MP4 and ffmpeg converter
Browse files Browse the repository at this point in the history
  • Loading branch information
pleonex committed Nov 11, 2023
1 parent e81052f commit 66c3bf3
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 28 deletions.
27 changes: 14 additions & 13 deletions src/PlayMobic.UI/Pages/ConvertVideoView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@
Command="{Binding SelectOutputPathCommand}" />

<Label Grid.Row="1" Grid.Column="0" Margin="5" Content="Output format:" />
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal">
<ComboBox
Grid.Row="1" Grid.Column="1"
Margin="5"
MinWidth="150"
ItemsSource="{Binding AvailableOutputFormats}"
SelectedItem="{Binding SelectedOutputFormat}" />

Margin="5"
MinWidth="150"
ItemsSource="{Binding AvailableOutputFormats}"
SelectedItem="{Binding SelectedOutputFormat}" />
<fluent:InfoBadge
Classes="Caution Icon"
IsVisible="{Binding InvalidFfmpegRequirement}"
ToolTip.Tip="ffmpeg binary does not exist. Set if from settings." />
</StackPanel>

<Label Grid.Row="2" Grid.Column="0" Margin="5" Content="MODS input files:" />
<StackPanel Grid.Row="3" Grid.Column="2" Orientation="Vertical">
<Button
Expand Down Expand Up @@ -90,16 +95,12 @@
Margin="5"
IsOpen="{Binding HasFfmpegCommand}"
IsClosable="False"
Title="Convert raw streams">
<StackPanel Orientation="Vertical">
<TextBlock
TextWrapping="Wrap"
Text="You can convert the raw streams into a standard format using ffmpeg:" />
<TextBlock
Title="You can convert the raw streams into a standard format using ffmpeg:">
<SelectableTextBlock
SelectionBrush="{DynamicResource AccentFillColorSelectedTextBackgroundBrush}"
Margin="0 0 0 5"
TextWrapping="Wrap"
Text="{Binding FfmpegCommand}" />
</StackPanel>
</fluent:InfoBar>
</Grid>
</UserControl>
68 changes: 59 additions & 9 deletions src/PlayMobic.UI/Pages/ConvertVideoViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using PlayMobic.Containers.Mods;
using PlayMobic.UI.Mvvm;
using PlayMobic.UI.Settings;
using Yarhl.FileSystem;
using Yarhl.IO;

public partial class ConvertVideoViewModel : ObservableObject
{
private CancellationTokenSource? convertCancellation;
private string? ffmpegPath;

[ObservableProperty]
private ObservableCollection<string> inputFiles;
Expand All @@ -28,6 +28,9 @@ public partial class ConvertVideoViewModel : ObservableObject
private string outputPath;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasFfmpegCommand))]
[NotifyPropertyChangedFor(nameof(InvalidFfmpegRequirement))]
[NotifyCanExecuteChangedFor(nameof(StartConvertCommand))]
private OutputFormatKind selectedOutputFormat;

[ObservableProperty]
Expand All @@ -37,17 +40,22 @@ public partial class ConvertVideoViewModel : ObservableObject
[ObservableProperty]
private string ffmpegCommand;

[ObservableProperty]
private bool hasFfmpegCommand;

public ConvertVideoViewModel()
{
inputFiles = new ObservableCollection<string>();
outputPath = string.Empty;
selectedOutputFormat = OutputFormatKind.MP4;

hasFfmpegCommand = false;
ffmpegCommand = string.Empty;
ffmpegCommand = "ffmpeg " +
"-f s16le -channel_layout {mono/stereo} -ar {sampleRate} -ac {audioChannels} -i {rawAudio} " +
"-f rawvideo -pix_fmt yuv420p -r {fps} -s {width}x{height} -i {rawVideo} " +
"-ac {audioChannels} {outputPath}.mp4";
ffmpegPath = AppSettingManager.Instance.LoadSettingFile()?.FfmpegPath;
AppSettingManager.Instance.SettingsChanged += (_, e) => {
ffmpegPath = e.FfmpegPath;
StartConvertCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(InvalidFfmpegRequirement));
};

AskOutputFolder = new AsyncInteraction<IStorageFolder?>();
AskInputFiles = new AsyncInteraction<IEnumerable<IStorageFile>>();
Expand All @@ -65,6 +73,11 @@ public ConvertVideoViewModel()

public AsyncInteraction<object> ShowConvertDialog { get; }

public bool HasFfmpegCommand => SelectedOutputFormat == OutputFormatKind.Raw;

public bool InvalidFfmpegRequirement =>
SelectedOutputFormat is OutputFormatKind.MP4 && !File.Exists(ffmpegPath);

public event EventHandler<ConversionProgressEventArgs>? ConversionProgressed;

[RelayCommand]
Expand Down Expand Up @@ -162,7 +175,12 @@ private async Task StartConvert()

private bool CanConvert()
{
return InputFiles.Count > 0 && !string.IsNullOrEmpty(OutputPath);
bool validIO = InputFiles.Count > 0 && !string.IsNullOrEmpty(OutputPath);
if (SelectedOutputFormat is OutputFormatKind.MP4) {
return validIO && File.Exists(ffmpegPath);
}

return validIO;
}

public async Task ConvertAsync()
Expand Down Expand Up @@ -194,7 +212,7 @@ private bool ConvertFile(int fileIndex, CancellationToken token)
return ConvertToAvi(input, fileIndex);

case OutputFormatKind.MP4:
break;
return ConvertToMp4(input, fileIndex);
}

return true;
Expand Down Expand Up @@ -266,6 +284,38 @@ private bool ConvertToRaw(Node input, int index)
}
}

private bool ConvertToMp4(Node input, int index)
{
string name = Path.GetFileNameWithoutExtension(input.Name);
string outputVideo = Path.Combine(OutputPath, name + ".rawvideo");
string outputAudio = Path.Combine(OutputPath, name + ".rawaudio");

string outputMp4 = Path.Combine(OutputPath, name + ".mp4");
ModsInfo videoInfo = input.GetFormatAs<ModsVideo>()!.Info;
var ffmpegParams = new FfmpegConverterParameters(
ffmpegPath!,
outputVideo,
outputAudio,
outputMp4,
videoInfo);

try {
bool success = ConvertToRaw(input, index);
if (success) {
_ = input.TransformWith(new FfmpegConverter(ffmpegParams));
}

return success;
} catch (Exception ex) {
File.Delete(outputMp4);
ConversionProgressed?.Invoke(this, new ConversionProgressEventArgs(input.Name, 100, ex.Message));
return false;
} finally {
File.Delete(outputVideo);
File.Delete(outputAudio);
}
}

private double GetProgress(int frame, int framesCount, int fileIndex)
{
double currentFileProgress = 100d * frame / framesCount / InputFiles.Count;
Expand Down
6 changes: 3 additions & 3 deletions src/PlayMobic.UI/Pages/SettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public SettingsViewModel()
using var licenseStreamReader = new StreamReader(licenseStream);
License = licenseStreamReader.ReadToEnd();

ffmpegPath = AppSettingManager.LoadSettingFile()?.FfmpegPath ?? string.Empty;
ffmpegPath = AppSettingManager.Instance.LoadSettingFile()?.FfmpegPath ?? string.Empty;
IsValidFfmpegPath = File.Exists(ffmpegPath);
OpenFfmpegBinary = new AsyncInteraction<IStorageFile?>();
}
Expand Down Expand Up @@ -75,11 +75,11 @@ partial void OnFfmpegPathChanged(string? oldValue, string newValue)
IsValidFfmpegPath = File.Exists(FfmpegPath);

// Thread-issue
var currentSettings = AppSettingManager.LoadSettingFile();
var currentSettings = AppSettingManager.Instance.LoadSettingFile();
var newSettings = (currentSettings is null)
? new AppSettings(FfmpegPath)
: currentSettings with { FfmpegPath = FfmpegPath };
AppSettingManager.SaveSettingFile(newSettings);
AppSettingManager.Instance.SaveSettingFile(newSettings);
}

partial void OnCurrentThemeChanged(ApplicationThemes value)
Expand Down
13 changes: 10 additions & 3 deletions src/PlayMobic.UI/Settings/AppSettingManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
using System.IO;
using System.Text.Json;

internal static class AppSettingManager
internal class AppSettingManager
{
private static AppSettingManager? instance;
private static string? SettingsPath =>
Environment.ProcessPath is null
? null
: Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "settings.json");

public static AppSettings? LoadSettingFile()
public static AppSettingManager Instance => instance ??= new AppSettingManager();

public event EventHandler<AppSettings>? SettingsChanged;

public AppSettings? LoadSettingFile()
{
if (!File.Exists(SettingsPath)) {
return null;
Expand All @@ -20,13 +25,15 @@ Environment.ProcessPath is null
return JsonSerializer.Deserialize<AppSettings>(json);
}

public static void SaveSettingFile(AppSettings settings)
public void SaveSettingFile(AppSettings settings)
{
if (SettingsPath is null) {
throw new InvalidOperationException();
}

string json = JsonSerializer.Serialize(settings);
File.WriteAllText(SettingsPath, json);

SettingsChanged?.Invoke(this, settings);
}
}
65 changes: 65 additions & 0 deletions src/PlayMobic/FfmpegConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
namespace PlayMobic;

using System.Diagnostics;
using System.Text;
using Yarhl.FileFormat;
using Yarhl.FileSystem;
using Yarhl.IO;

public class FfmpegConverter : IConverter<NodeContainerFormat, BinaryFormat>
{
private readonly FfmpegConverterParameters parameters;

public FfmpegConverter(FfmpegConverterParameters parameters)
{
this.parameters = parameters;
}

public BinaryFormat Convert(NodeContainerFormat source)
{
ArgumentNullException.ThrowIfNull(source);

var process = new Process();
process.StartInfo.FileName = parameters.ExecutablePath;
process.StartInfo.Arguments = GetArguments();
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;

_ = process.Start();
process.WaitForExit();

if (process.ExitCode != 0) {
throw new FormatException($"Error running: {parameters.ExecutablePath} {process.StartInfo.Arguments}");
}

return new BinaryFormat(DataStreamFactory.FromFile(parameters.OutputPath, FileOpenMode.ReadWrite));
}

private string GetArguments()
{
var arguments = new StringBuilder();

var videoInfo = parameters.VideoInfo;
if (videoInfo.AudioChannelsCount > 0) {
_ = arguments.AppendFormat(
"-f s16le -channel_layout {0} -ar {1} -ac {2} -i {3} ",
videoInfo.AudioChannelsCount > 1 ? "stereo" : "mono",
videoInfo.AudioFrequency,
videoInfo.AudioChannelsCount,
parameters.RawAudioPath);
}

_ = arguments.AppendFormat(
"-f rawvideo -pix_fmt yuv420p -r {0:F1} -s {1}x{2} -i {3} ",
videoInfo.FramesPerSecond,
videoInfo.Width,
videoInfo.Height,
parameters.RawVideoPath);
_ = arguments.AppendFormat(
"-y -hide_banner -ac {0} {1}",
videoInfo.AudioChannelsCount,
parameters.OutputPath);

return arguments.ToString();
}
}
10 changes: 10 additions & 0 deletions src/PlayMobic/FfmpegConverterParameters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace PlayMobic;

using PlayMobic.Containers.Mods;

public record FfmpegConverterParameters(
string ExecutablePath,
string RawVideoPath,
string RawAudioPath,
string OutputPath,
ModsInfo VideoInfo);

0 comments on commit 66c3bf3

Please sign in to comment.