Skip to content
This repository has been archived by the owner on Apr 13, 2024. It is now read-only.

Commit

Permalink
Fixes video duration when subtitles are longer then video stream
Browse files Browse the repository at this point in the history
This happens a lot in kamikatsu, but for now only work on ass subtitles
  • Loading branch information
Marcos Cordeiro committed Nov 24, 2023
1 parent 62a802a commit 0d0c0ac
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 11 deletions.
167 changes: 156 additions & 11 deletions Wasari.FFmpeg/FFmpegService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,13 @@ private async IAsyncEnumerable<string> BuildArgumentsForEpisode(IWasariEpisode e
{
yield return "-c:v copy";
}

var fileExtension = Path.GetExtension(filePath);
var isMp4 = fileExtension == ".mp4";

if (isMp4)
yield return "-c:s mov_text";

yield return "-y";
yield return $"\"{filePath}\"";
}
Expand All @@ -177,10 +177,10 @@ private static Command CreateCommand()
{
var fileName = Path.GetFileNameWithoutExtension(baseFilePath);
var fileDirectory = Path.GetDirectoryName(baseFilePath);

return $"{fileDirectory}{Path.DirectorySeparatorChar}{fileName}_wasari_tmp{extension}";
}

if (!Options.Value.UseTemporaryEncodingPath)
return null;

Expand Down Expand Up @@ -212,7 +212,7 @@ private static Command CreateCommand()

return videoDuration == TimeSpan.Zero ? null : videoDuration;
}

public async Task<bool> CheckIfVideoStreamIsValid(string filePath)
{
try
Expand All @@ -224,7 +224,7 @@ public async Task<bool> CheckIfVideoStreamIsValid(string filePath)

var videoDuration = GetVideoDuration(fileAnalysis);

if(videoDuration == null)
if (videoDuration == null)
return false;

var delta = fileAnalysis.Duration - videoDuration;
Expand All @@ -234,7 +234,7 @@ public async Task<bool> CheckIfVideoStreamIsValid(string filePath)
{
Logger.LogWarning("File was found to be invalid: {FilePath}, the difference between the video duration and the file duration is {Delta}", filePath, delta);
}

return isValid;
}
catch (Exception e)
Expand All @@ -247,13 +247,45 @@ public async Task<bool> CheckIfVideoStreamIsValid(string filePath)
public async Task DownloadEpisode<T>(T episode, string filePath, IProgress<FFmpegProgressUpdate>? progress) where T : IWasariEpisode
{
var tempFileName = GetTemporaryFile(Path.GetExtension(filePath));
var arguments = await BuildArgumentsForEpisode(episode, tempFileName ?? filePath).ToArrayAsync();
var outputFile = tempFileName ?? filePath;
var arguments = await BuildArgumentsForEpisode(episode, outputFile).ToArrayAsync();
var ffmpegCommand = CreateCommand()
.WithValidation(CommandResultValidation.None)
.WithArguments(arguments, false);

await foreach (var commandEvent in ffmpegCommand.ListenAsync()) ProcessEvent(episode, progress, commandEvent, ffmpegCommand);


var checkIfVideoStreamIsValid = await CheckIfVideoStreamIsValid(outputFile);

if (!checkIfVideoStreamIsValid)
{
var outputDirectory = Path.Combine(Path.GetTempPath(), $"{Path.GetFileName(filePath)}-subs");
if (!Directory.Exists(outputDirectory)) Directory.CreateDirectory(outputDirectory);

var mediaAnalysis = await FFProbe.AnalyseAsync(outputFile);
var duration = GetVideoDuration(mediaAnalysis);

if (duration != null && mediaAnalysis.SubtitleStreams.All(x => x.CodecName == "ass"))
{
var subtitleFiles = await ExtractSubtitles(outputFile, outputDirectory, mediaAnalysis, duration.Value).ToListAsync();
var modifiedSubtitlesFile = await subtitleFiles
.ToAsyncEnumerable()
.SelectAwait(i => FixSubtitleDuration(i, duration.Value))
.ToHashSetAsync();

var outputFileName = $"{outputFile}.modified{Path.GetExtension(outputFile)}";
await ReplaceSubtitles(outputFile, modifiedSubtitlesFile, outputFileName);
Directory.Delete(outputDirectory, true);

if (await CheckIfVideoStreamIsValid(outputFileName))
{
File.Delete(outputFile);
File.Move(outputFileName, outputFile);
Logger.LogInformation("Video duration has been fixed");
}
}
}

if (tempFileName != null)
{
var destFileTempName = $"{filePath}.wasari_tmp";
Expand All @@ -265,6 +297,117 @@ public async Task DownloadEpisode<T>(T episode, string filePath, IProgress<FFmpe
}
}

private static async ValueTask<SubtitleFile> FixSubtitleDuration(SubtitleFile subtitleFile, TimeSpan duration)
{
var filePath = subtitleFile.FilePath;
var modifiedFilePath = $"{filePath}.modified{Path.GetExtension(filePath)}";

await using var readStream = File.OpenRead(filePath);
using var readStreamReader = new StreamReader(readStream);

await using var writeStream = File.OpenWrite(modifiedFilePath);
await using var streamWriter = new StreamWriter(writeStream);
var hasReachedEvents = false;

while (await readStreamReader.ReadLineAsync() is {} line)
{
if (!hasReachedEvents && line.StartsWith("[Events]", StringComparison.InvariantCultureIgnoreCase))
{
hasReachedEvents = true;
} else if (hasReachedEvents)
{
var match = AssEventRegex().Match(line);

if (match.Success)
{
var dateEnd = TimeSpan.Parse(match.Groups["dateEnd"].Value);

if (dateEnd > duration)
{
line = line.Replace(match.Groups["dateEnd"].Value, duration.ToString(@"hh\:mm\:ss\.ff"));
await streamWriter.WriteLineAsync(line);
break;
}
}
}

await streamWriter.WriteLineAsync(line);
}

await streamWriter.FlushAsync();
return new SubtitleFile(modifiedFilePath, subtitleFile.Language);
}

private async IAsyncEnumerable<SubtitleFile> ExtractSubtitles(string filePath, string outputDirectory, IMediaAnalysis mediaAnalysis, TimeSpan duration)
{
if (mediaAnalysis.PrimaryVideoStream != null)
{
foreach (var mediaAnalysisSubtitleStream in mediaAnalysis.SubtitleStreams)
{
var outputFile = await ExtractSubtitle(filePath, outputDirectory, duration, mediaAnalysisSubtitleStream);
yield return outputFile;
}

Logger.LogInformation("Extracted subtitles from {FilePath} to {OutputDirectory}", filePath, outputDirectory);
}
}

private CommandTask<CommandResult> ReplaceSubtitles(string videoFilePath, ICollection<SubtitleFile> subtitlesFilePaths, string outputFileName)
{
var arguments = CreateArguments().ToArray();
var ffmpegCommand = CreateCommand()
.WithArguments(arguments, false);

Logger.LogInformation("Replacing subtitles for {VideoFilePath} with {SubtitlesFilePaths} to {OutputFileName}", videoFilePath, subtitlesFilePaths, outputFileName);

return ffmpegCommand.ExecuteAsync();

IEnumerable<string> CreateArguments()
{
yield return $"-i \"{videoFilePath}\"";

foreach (var subtitlesFilePath in subtitlesFilePaths)
{
yield return $"-i \"{subtitlesFilePath.FilePath}\"";
}

yield return "-map 0";
yield return "-map -0:s";

foreach (var subtitle in subtitlesFilePaths.Select((s, y) => new {Index = y, s.Language}))
{
yield return $"-map {subtitle.Index + 1}";

if (subtitle.Language != null)
yield return $"-metadata:s:s:{subtitle.Index} language=\"{subtitle.Language}\"";
}

yield return "-c copy";
yield return "-y";
yield return $"\"{outputFileName}\"";
}
}

private static async ValueTask<SubtitleFile> ExtractSubtitle(string filePath, string outputDirectory, TimeSpan duration, MediaStream mediaAnalysisSubtitleStream)
{
var outputFileName = $"{outputDirectory}{Path.DirectorySeparatorChar}sub{mediaAnalysisSubtitleStream.Index:00}.{mediaAnalysisSubtitleStream.CodecName}";

var arguments = new[]
{
$"-i \"{filePath}\"",
$"-map 0:{mediaAnalysisSubtitleStream.Index}",
$"-t \"{duration}\"",
"-y",
$"\"{outputFileName}\""
};

var ffmpegCommand = CreateCommand()
.WithArguments(arguments, false);

await ffmpegCommand.ExecuteAsync();
return new SubtitleFile(outputFileName, mediaAnalysisSubtitleStream.Language);
}

private void ProcessEvent<T>(T episode, IProgress<FFmpegProgressUpdate>? progress, CommandEvent commandEvent, ICommandConfiguration ffmpegCommand) where T : IWasariEpisode
{
switch (commandEvent)
Expand Down Expand Up @@ -309,4 +452,6 @@ private void ProcessEvent<T>(T episode, IProgress<FFmpegProgressUpdate>? progres

[GeneratedRegex("\\d+\\:\\d+\\:\\d+(?<trail>\\.\\d+)")]
private static partial Regex DurationRegex();
[GeneratedRegex(@"^Dialogue:\s\d+,(?<dateStart>\d+:\d+:\d+\.\d+),(?<dateEnd>\d+:\d+:\d+\.\d+),")]
private static partial Regex AssEventRegex();
}
3 changes: 3 additions & 0 deletions Wasari.FFmpeg/SubtitleFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Wasari.FFmpeg;

internal record SubtitleFile(string FilePath, string? Language);

0 comments on commit 0d0c0ac

Please sign in to comment.