Files
flowframes/Code/Media/FfmpegAudioAndMetadata.cs
N00MKRAD 55d71d6328 Output streams: Support missing stream files, skip instead of erroring
If for some reason an audio or sub track failed to extract, it will now simply ommit it in the output file instead of failing to mux the video entirely.
2021-02-24 09:59:18 +01:00

251 lines
12 KiB
C#

using Flowframes.Data;
using Flowframes.IO;
using Flowframes.Main;
using Flowframes.MiscUtils;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using static Flowframes.AvProcess;
using Utils = Flowframes.Media.FFmpegUtils;
namespace Flowframes.Media
{
partial class FfmpegAudioAndMetadata : FfmpegCommands
{
#region Audio
public static async Task ExtractAudioTracks(string inputFile, string outFolder, bool showMsg = true)
{
string msg = "Extracting audio from video...";
if (showMsg) Logger.Log(msg);
List<AudioTrack> audioTracks = await GetAudioTracks(inputFile);
int counter = 1;
foreach (AudioTrack track in audioTracks)
{
string audioExt = Utils.GetAudioExt(inputFile, track.streamIndex);
string outPath = Path.Combine(outFolder, $"{track.streamIndex}_{track.title}_audio.{audioExt}");
string args = $" -loglevel panic -i {inputFile.Wrap()} -map 0:{track.streamIndex} -vn -c:a copy {outPath.Wrap()}";
await RunFfmpeg(args, LogMode.Hidden);
if (File.Exists(outPath) && IOUtils.GetFilesize(outPath) < 512)
{
Logger.Log($"Failed to extract audio stream #{track.streamIndex} losslessly! Trying to re-encode.");
File.Delete(outPath);
outPath = Path.ChangeExtension(outPath, Utils.GetAudioExtForContainer(Path.GetExtension(inputFile)));
args = $" -loglevel panic -i {inputFile.Wrap()} -vn {Utils.GetAudioFallbackArgs(Path.GetExtension(inputFile))} {outPath.Wrap()}";
await RunFfmpeg(args, LogMode.Hidden, TaskType.ExtractOther, true);
if (File.Exists(outPath) && IOUtils.GetFilesize(outPath) < 512)
{
Logger.Log($"Failed to extract audio stream #{track.streamIndex}, even with re-encoding. Will be missing from output.");
IOUtils.TryDeleteIfExists(outPath);
return;
}
Logger.Log($"Audio stream #{track.streamIndex} has been re-encoded as it can't be extracted losslessly. This may decrease the quality slightly.", false, true);
}
if (audioTracks.Count > 1) Program.mainForm.SetProgress(FormatUtils.RatioInt(counter, audioTracks.Count));
Logger.Log($"[FFCmds] Extracted audio track {track.streamIndex} to {outPath} ({FormatUtils.Bytes(IOUtils.GetFilesize(outPath))})", true, false, "ffmpeg");
counter++;
}
}
public static async Task<List<AudioTrack>> GetAudioTracks(string inputFile)
{
List<AudioTrack> audioTracks = new List<AudioTrack>();
string args = $"-i {inputFile.Wrap()}";
Logger.Log("GetAudioTracks()", true, false, "ffmpeg");
string[] outputLines = (await GetFfmpegOutputAsync(args)).SplitIntoLines();
for (int i = 0; i < outputLines.Length; i++)
{
string line = outputLines[i];
if (!line.Contains(" Audio: ")) continue;
int streamIndex = line.Remove("Stream #0:").Split(':')[0].GetInt();
string title = "";
string codec = "";
if ((i + 2 < outputLines.Length) && outputLines[i + 1].Contains("Metadata:") && outputLines[i + 2].Contains("title"))
title = outputLines[i + 2].Remove("title").Remove(":").TrimWhitespaces();
Logger.Log($"Found audio stream #{streamIndex} {title}", true, false, "ffmpeg");
audioTracks.Add(new AudioTrack(streamIndex, title, codec));
}
return audioTracks;
}
#endregion
#region Subtitles
public static async Task ExtractSubtitles(string inputFile, string outFolder, Interpolate.OutMode outMode,bool showMsg = true)
{
try
{
string msg = "Extracting subtitles from video...";
if (showMsg) Logger.Log(msg);
List<SubtitleTrack> subtitleTracks = await GetSubtitleTracks(inputFile);
int counter = 1;
foreach (SubtitleTrack subTrack in subtitleTracks)
{
string outPath = Path.Combine(outFolder, $"{subTrack.streamIndex}_{subTrack.langFriendly}_{subTrack.encoding}.srt");
string args = $" -loglevel error -sub_charenc {subTrack.encoding} -i {inputFile.Wrap()} -map 0:{subTrack.streamIndex} {outPath.Wrap()}";
await RunFfmpeg(args, LogMode.Hidden);
if (subtitleTracks.Count > 4) Program.mainForm.SetProgress(FormatUtils.RatioInt(counter, subtitleTracks.Count));
Logger.Log($"[FFCmds] Extracted subtitle track {subTrack.streamIndex} to {outPath} ({FormatUtils.Bytes(IOUtils.GetFilesize(outPath))})", true, false, "ffmpeg");
counter++;
}
if (subtitleTracks.Count > 0)
{
Logger.Log($"Extracted {subtitleTracks.Count} subtitle tracks from the input video.", false, Logger.GetLastLine().Contains(msg));
Utils.ContainerSupportsSubs(Utils.GetExt(outMode), true);
}
}
catch (Exception e)
{
Logger.Log("Error extracting subtitles: " + e.Message);
}
Program.mainForm.SetProgress(0);
}
public static async Task<List<SubtitleTrack>> GetSubtitleTracks(string inputFile)
{
List<SubtitleTrack> subtitleTracks = new List<SubtitleTrack>();
string args = $"-i {inputFile.Wrap()}";
Logger.Log("GetSubtitleTracks()", true, false, "ffmpeg");
string[] outputLines = (await GetFfmpegOutputAsync(args)).SplitIntoLines();
for (int i = 0; i < outputLines.Length; i++)
{
string line = outputLines[i];
if (!line.Contains(" Subtitle: ")) continue;
int streamIndex = line.Remove("Stream #0:").Split(':')[0].GetInt();
string lang = "unknown";
string subEnc = "UTF-8";
if (line.Contains("(") && line.Contains("): Subtitle: ")) // Lang code in stream name, like "Stream #0:2(eng): Subtitle: ..."
lang = line.Split('(')[1].Split(')')[0];
if ((i + 2 < outputLines.Length) && outputLines[i + 1].Contains("Metadata:") && outputLines[i + 2].Contains("SUB_CHARENC")) // Subtitle encoding is in metadata!
subEnc = outputLines[i + 2].Remove("SUB_CHARENC").Remove(":").TrimWhitespaces();
Logger.Log($"Found subtitle track #{streamIndex} with language '{lang}' and encoding '{subEnc}'", true, false, "ffmpeg");
subtitleTracks.Add(new SubtitleTrack(streamIndex, lang, subEnc));
}
return subtitleTracks;
}
#endregion
public static async Task MergeAudioAndSubs(string inputFile, string tempFolder) // https://superuser.com/a/277667
{
// Logger.Log($"[FFCmds] Merging audio from {audioPath} into {inputFile}", true);
string containerExt = Path.GetExtension(inputFile);
string tempPath = Path.Combine(tempFolder, $"vid{containerExt}");
string outPath = Path.Combine(tempFolder, $"muxed{containerExt}");
File.Move(inputFile, tempPath);
string inName = Path.GetFileName(tempPath);
string outName = Path.GetFileName(outPath);
string[] audioTracks = IOUtils.GetFilesSorted(tempFolder, false, "*_audio.*"); // Find audio files
string[] subTracks = IOUtils.GetFilesSorted(tempFolder, false, "*.srt"); // Find subtitle files
Dictionary<int, string> trackFiles = new Dictionary<int, string>(); // Dict holding all track files with their index
foreach (string audioTrack in audioTracks) // Loop through audio streams to add them to the dict
trackFiles[Path.GetFileNameWithoutExtension(audioTrack).Split('_')[0].GetInt()] = audioTrack; // Add file, dict key is stream index
foreach (string subTrack in subTracks) // Loop through subtitle streams to add them to the dict
trackFiles[Path.GetFileNameWithoutExtension(subTrack).Split('_')[0].GetInt()] = subTrack; // Add file, dict key is stream index
bool audio = Config.GetBool("keepSubs");
bool subs = Utils.ContainerSupportsSubs(containerExt, false) && Config.GetBool("keepSubs");
string trackInputArgs = "";
string trackMapArgs = "";
string trackMetaArgs = "";
SortedDictionary<int, string> sortedTrackFiles = new SortedDictionary<int, string>(trackFiles);
int inputIndex = 1; // Input index (= output stream index) - Start at 1 since 0 is the video stream
foreach (KeyValuePair<int, string> track in sortedTrackFiles)
{
int streamIndex = track.Key;
string trackFile = track.Value;
trackInputArgs += $" -i {Path.GetFileName(trackFile)}"; // Input filename
trackMapArgs += $" -map {inputIndex}"; // Map input file
trackMetaArgs += $" -metadata:s:{inputIndex} title={Path.GetFileNameWithoutExtension(trackFile).Split('_')[1]}"; // Language or title
inputIndex++;
}
bool allAudioCodecsSupported = true;
foreach (string audioTrack in audioTracks)
if (!Utils.ContainerSupportsAudioFormat(Interpolate.current.outMode, Path.GetExtension(audioTrack)))
allAudioCodecsSupported = false;
if(!allAudioCodecsSupported)
Logger.Log("Warning: Input audio format(s) not fully support in output container. Audio transfer will not be lossless.", false, false, "ffmpeg");
string subArgs = "-c:s " + Utils.GetSubCodecForContainer(containerExt);
string audioArgs = allAudioCodecsSupported ? "-c:a copy" : Utils.GetAudioFallbackArgs(Path.GetExtension(inputFile));
string args = $" -i {inName} {trackInputArgs} -map 0:v {trackMapArgs} -c:v copy {audioArgs} {subArgs} {trackMetaArgs} -shortest {outName}";
await RunFfmpeg(args, tempFolder, LogMode.Hidden);
// if (File.Exists(outPath) && IOUtils.GetFilesize(outPath) < 1024)
// {
// Logger.Log("Failed to merge audio losslessly! Trying to re-encode.", false, false, "ffmpeg");
//
// args = $" -i {inName} -stream_loop {looptimes} -i {audioName.Wrap()}" +
// $"{trackInputArgs} -map 0:v -map 1:a {trackMapArgs} -c:v copy {Utils.GetAudioFallbackArgs(Path.GetExtension(inputFile))} -c:s {subCodec} {trackMetaArgs} -shortest {outName}";
//
// await RunFfmpeg(args, tempFolder, LogMode.Hidden);
//
// if (File.Exists(outPath) && IOUtils.GetFilesize(outPath) < 1024)
// {
// Logger.Log("Failed to merge audio, even with re-encoding. Output will not have audio.", false, false, "ffmpeg");
// IOUtils.TryMove(tempPath, inputFile); // Move temp file back
// IOUtils.TryDeleteIfExists(tempPath);
// return;
// }
//
// string audioExt = Path.GetExtension(audioPath).Remove(".").ToUpper();
// Logger.Log($"Source audio ({audioExt}) has been re-encoded to fit into the target container ({containerExt.Remove(".").ToUpper()}). This may decrease the quality slightly.", false, true, "ffmpeg");
// }
if (File.Exists(outPath) && IOUtils.GetFilesize(outPath) > 512)
{
File.Delete(tempPath);
File.Move(outPath, inputFile);
}
else
{
File.Move(tempPath, inputFile);
}
}
}
}