kopia lustrzana https://github.com/jameshball/osci-render
382 wiersze
13 KiB
C++
382 wiersze
13 KiB
C++
#include "FFmpegEncoderManager.h"
|
|
|
|
FFmpegEncoderManager::FFmpegEncoderManager(juce::File& ffmpegExecutable)
|
|
: ffmpegExecutable(ffmpegExecutable) {
|
|
queryAvailableEncoders();
|
|
}
|
|
|
|
juce::String FFmpegEncoderManager::buildVideoEncodingCommand(
|
|
VideoCodec codec,
|
|
int crf,
|
|
int videoToolboxQuality,
|
|
int width,
|
|
int height,
|
|
double frameRate,
|
|
const juce::String& compressionPreset,
|
|
const juce::File& outputFile) {
|
|
switch (codec) {
|
|
case VideoCodec::H264:
|
|
return buildH264EncodingCommand(crf, width, height, frameRate, compressionPreset, outputFile);
|
|
case VideoCodec::H265:
|
|
return buildH265EncodingCommand(crf, videoToolboxQuality, width, height, frameRate, compressionPreset, outputFile);
|
|
case VideoCodec::VP9:
|
|
return buildVP9EncodingCommand(crf, width, height, frameRate, compressionPreset, outputFile);
|
|
#if JUCE_MAC
|
|
case VideoCodec::ProRes:
|
|
return buildProResEncodingCommand(width, height, frameRate, outputFile);
|
|
#endif
|
|
default:
|
|
// Default to H.264 if unknown codec
|
|
return buildH264EncodingCommand(crf, width, height, frameRate, compressionPreset, outputFile);
|
|
}
|
|
}
|
|
|
|
juce::Array<FFmpegEncoderManager::EncoderDetails> FFmpegEncoderManager::getAvailableEncodersForCodec(VideoCodec codec) {
|
|
// Return cached list of encoders if available
|
|
auto it = availableEncoders.find(codec);
|
|
if (it != availableEncoders.end()) {
|
|
return it->second;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
bool FFmpegEncoderManager::isHardwareEncoderAvailable(const juce::String& encoderName) {
|
|
// Check if the encoder is available and supported
|
|
for (auto& pair : availableEncoders) {
|
|
for (auto& encoder : pair.second) {
|
|
if (encoder.name == encoderName && encoder.isSupported && encoder.isHardwareAccelerated) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
juce::String FFmpegEncoderManager::getBestEncoderForCodec(VideoCodec codec) {
|
|
auto encoders = getAvailableEncodersForCodec(codec);
|
|
|
|
// Define priority lists for each codec type
|
|
juce::StringArray h264Encoders = {"h264_nvenc", "h264_amf", "h264_videotoolbox", "libx264"};
|
|
juce::StringArray h265Encoders = {"hevc_nvenc", "hevc_amf", "hevc_videotoolbox", "libx265"};
|
|
juce::StringArray vp9Encoders = {"libvpx-vp9"};
|
|
#if JUCE_MAC
|
|
juce::StringArray proResEncoders = {"prores_ks", "prores"};
|
|
#endif
|
|
|
|
// Select the appropriate priority list based on codec
|
|
juce::StringArray* priorityList = nullptr;
|
|
switch (codec) {
|
|
case VideoCodec::H264:
|
|
priorityList = &h264Encoders;
|
|
break;
|
|
case VideoCodec::H265:
|
|
priorityList = &h265Encoders;
|
|
break;
|
|
case VideoCodec::VP9:
|
|
priorityList = &vp9Encoders;
|
|
break;
|
|
#if JUCE_MAC
|
|
case VideoCodec::ProRes:
|
|
priorityList = &proResEncoders;
|
|
break;
|
|
#endif
|
|
default:
|
|
priorityList = &h264Encoders; // Default to H.264
|
|
}
|
|
|
|
// Find the highest priority encoder that is available and actually works
|
|
for (const auto& encoderName : *priorityList) {
|
|
for (const auto& encoder : encoders) {
|
|
if (encoder.name == encoderName && encoder.isSupported) {
|
|
// Test if the encoder actually works before selecting it
|
|
if (testEncoderWorks(encoderName)) {
|
|
return encoderName;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return default software encoder if no hardware encoder is available or working
|
|
switch (codec) {
|
|
case VideoCodec::H264:
|
|
return "libx264";
|
|
case VideoCodec::H265:
|
|
return "libx265";
|
|
case VideoCodec::VP9:
|
|
return "libvpx-vp9";
|
|
#if JUCE_MAC
|
|
case VideoCodec::ProRes:
|
|
return "prores";
|
|
#endif
|
|
default:
|
|
return "libx264";
|
|
}
|
|
}
|
|
|
|
void FFmpegEncoderManager::queryAvailableEncoders() {
|
|
// Query available encoders using ffmpeg -encoders
|
|
juce::String output = runFFmpegCommand({"-encoders", "-hide_banner"});
|
|
parseEncoderList(output);
|
|
}
|
|
|
|
void FFmpegEncoderManager::parseEncoderList(const juce::String& output) {
|
|
// Clear current encoders
|
|
availableEncoders.clear();
|
|
|
|
// Initialize codec-specific encoder arrays
|
|
availableEncoders[VideoCodec::H264] = {};
|
|
availableEncoders[VideoCodec::H265] = {};
|
|
availableEncoders[VideoCodec::VP9] = {};
|
|
#if JUCE_MAC
|
|
availableEncoders[VideoCodec::ProRes] = {};
|
|
#endif
|
|
|
|
// Split the output into lines
|
|
juce::StringArray lines;
|
|
lines.addLines(output);
|
|
|
|
// Skip the first 10 lines (header information from ffmpeg -encoders)
|
|
int linesToSkip = juce::jmin(10, lines.size());
|
|
|
|
// Parse each line to find encoder information
|
|
for (int i = linesToSkip; i < lines.size(); ++i) {
|
|
const auto& line = lines[i];
|
|
|
|
// Format: V..... libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
|
|
juce::String flags = line.substring(0, 6).trim();
|
|
juce::String name = line.substring(8).upToFirstOccurrenceOf(" ", false, true);
|
|
juce::String description = line.substring(8 + name.length()).trim();
|
|
|
|
EncoderDetails encoder;
|
|
encoder.name = name;
|
|
encoder.description = description;
|
|
encoder.isHardwareAccelerated = name.contains("nvenc") || name.contains("amf") || name.contains("videotoolbox");
|
|
encoder.isSupported = flags.contains("V"); // Video encoder
|
|
|
|
// Add encoder to appropriate codec list
|
|
if (name == "libx264" || name.startsWith("h264_")) {
|
|
availableEncoders[VideoCodec::H264].add(encoder);
|
|
} else if (name == "libx265" || name.startsWith("hevc_")) {
|
|
availableEncoders[VideoCodec::H265].add(encoder);
|
|
} else if (name == "libvpx-vp9") {
|
|
availableEncoders[VideoCodec::VP9].add(encoder);
|
|
}
|
|
#if JUCE_MAC
|
|
else if (name.startsWith("prores")) {
|
|
availableEncoders[VideoCodec::ProRes].add(encoder);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
juce::String FFmpegEncoderManager::runFFmpegCommand(const juce::StringArray& args) {
|
|
juce::ChildProcess process;
|
|
juce::StringArray command;
|
|
|
|
command.add(ffmpegExecutable.getFullPathName());
|
|
command.addArray(args);
|
|
|
|
process.start(command, juce::ChildProcess::wantStdOut);
|
|
|
|
juce::String output = process.readAllProcessOutput();
|
|
|
|
return output;
|
|
}
|
|
|
|
juce::String FFmpegEncoderManager::buildBaseEncodingCommand(
|
|
int width,
|
|
int height,
|
|
double frameRate,
|
|
const juce::File& outputFile) {
|
|
juce::String resolution = juce::String(width) + "x" + juce::String(height);
|
|
juce::String cmd = "\"" + ffmpegExecutable.getFullPathName() + "\"" +
|
|
" -r " + juce::String(frameRate) +
|
|
" -f rawvideo" +
|
|
" -pix_fmt rgba" +
|
|
" -s " + resolution +
|
|
" -i -" +
|
|
" -threads 4" +
|
|
" -y" +
|
|
" -pix_fmt yuv420p" +
|
|
" -vf vflip";
|
|
|
|
return cmd;
|
|
}
|
|
|
|
juce::String FFmpegEncoderManager::addH264EncoderSettings(
|
|
juce::String cmd,
|
|
const juce::String& encoderName,
|
|
int crf,
|
|
const juce::String& compressionPreset) {
|
|
if (encoderName == "h264_nvenc") {
|
|
cmd += " -c:v h264_nvenc";
|
|
cmd += " -preset p7";
|
|
cmd += " -profile:v high";
|
|
cmd += " -rc vbr";
|
|
cmd += " -cq " + juce::String(crf);
|
|
cmd += " -b:v 0";
|
|
} else if (encoderName == "h264_amf") {
|
|
cmd += " -c:v h264_amf";
|
|
cmd += " -quality quality";
|
|
cmd += " -rc cqp";
|
|
cmd += " -qp_i " + juce::String(crf);
|
|
cmd += " -qp_p " + juce::String(crf);
|
|
} else if (encoderName == "h264_videotoolbox") {
|
|
cmd += " -c:v h264_videotoolbox";
|
|
cmd += " -q " + juce::String(crf);
|
|
} else { // libx264 (software)
|
|
cmd += " -c:v libx264";
|
|
cmd += " -preset " + compressionPreset;
|
|
cmd += " -crf " + juce::String(crf);
|
|
}
|
|
|
|
return cmd;
|
|
}
|
|
|
|
juce::String FFmpegEncoderManager::addH265EncoderSettings(
|
|
juce::String cmd,
|
|
const juce::String& encoderName,
|
|
int crf,
|
|
int videoToolboxQuality,
|
|
const juce::String& compressionPreset) {
|
|
if (encoderName == "hevc_nvenc") {
|
|
cmd += " -c:v hevc_nvenc";
|
|
cmd += " -preset p7";
|
|
cmd += " -profile:v main";
|
|
cmd += " -rc vbr";
|
|
cmd += " -cq " + juce::String(crf);
|
|
cmd += " -b:v 0";
|
|
} else if (encoderName == "hevc_amf") {
|
|
cmd += " -c:v hevc_amf";
|
|
cmd += " -quality quality";
|
|
cmd += " -rc cqp";
|
|
cmd += " -qp_i " + juce::String(crf);
|
|
cmd += " -qp_p " + juce::String(crf);
|
|
} else if (encoderName == "hevc_videotoolbox") {
|
|
cmd += " -c:v hevc_videotoolbox";
|
|
cmd += " -q:v " + juce::String(videoToolboxQuality);
|
|
cmd += " -tag:v hvc1";
|
|
} else { // libx265 (software)
|
|
cmd += " -c:v libx265";
|
|
cmd += " -preset " + compressionPreset;
|
|
cmd += " -crf " + juce::String(crf);
|
|
}
|
|
|
|
return cmd;
|
|
}
|
|
|
|
juce::String FFmpegEncoderManager::buildH264EncodingCommand(
|
|
int crf,
|
|
int width,
|
|
int height,
|
|
double frameRate,
|
|
const juce::String& compressionPreset,
|
|
const juce::File& outputFile) {
|
|
juce::String cmd = buildBaseEncodingCommand(width, height, frameRate, outputFile);
|
|
juce::String bestEncoder = getBestEncoderForCodec(VideoCodec::H264);
|
|
|
|
cmd = addH264EncoderSettings(cmd, bestEncoder, crf, compressionPreset);
|
|
cmd += " \"" + outputFile.getFullPathName() + "\"";
|
|
|
|
return cmd;
|
|
}
|
|
|
|
juce::String FFmpegEncoderManager::buildH265EncodingCommand(
|
|
int crf,
|
|
int videoToolboxQuality,
|
|
int width,
|
|
int height,
|
|
double frameRate,
|
|
const juce::String& compressionPreset,
|
|
const juce::File& outputFile) {
|
|
juce::String cmd = buildBaseEncodingCommand(width, height, frameRate, outputFile);
|
|
juce::String bestEncoder = getBestEncoderForCodec(VideoCodec::H265);
|
|
|
|
cmd = addH265EncoderSettings(cmd, bestEncoder, crf, videoToolboxQuality, compressionPreset);
|
|
cmd += " \"" + outputFile.getFullPathName() + "\"";
|
|
|
|
return cmd;
|
|
}
|
|
|
|
juce::String FFmpegEncoderManager::buildVP9EncodingCommand(
|
|
int crf,
|
|
int width,
|
|
int height,
|
|
double frameRate,
|
|
const juce::String& compressionPreset,
|
|
const juce::File& outputFile) {
|
|
juce::String cmd = buildBaseEncodingCommand(width, height, frameRate, outputFile);
|
|
|
|
cmd += juce::String(" -c:v libvpx-vp9") +
|
|
" -b:v 0" +
|
|
" -crf " + juce::String(crf) +
|
|
" -deadline good -cpu-used 2";
|
|
|
|
cmd += " \"" + outputFile.getFullPathName() + "\"";
|
|
|
|
return cmd;
|
|
}
|
|
|
|
#if JUCE_MAC
|
|
juce::String FFmpegEncoderManager::buildProResEncodingCommand(
|
|
int width,
|
|
int height,
|
|
double frameRate,
|
|
const juce::File& outputFile) {
|
|
juce::String cmd = buildBaseEncodingCommand(width, height, frameRate, outputFile);
|
|
juce::String bestEncoder = getBestEncoderForCodec(VideoCodec::ProRes);
|
|
|
|
cmd += " -c:v " + bestEncoder +
|
|
" -profile:v 3"; // ProRes 422 HQ
|
|
|
|
cmd += " \"" + outputFile.getFullPathName() + "\"";
|
|
|
|
return cmd;
|
|
}
|
|
#endif
|
|
|
|
bool FFmpegEncoderManager::testEncoderWorks(const juce::String& encoderName) {
|
|
juce::ChildProcess process;
|
|
juce::StringArray command;
|
|
|
|
// Build a test command that will quickly verify if an encoder works
|
|
// -v error: Only show errors
|
|
// -f lavfi -i nullsrc: Generate a null input source
|
|
// -t 1: Only encode 1 second
|
|
// -c:v [encoderName]: Use the specified encoder
|
|
// -f null -: Output to null device
|
|
command.add(ffmpegExecutable.getFullPathName());
|
|
command.add("-v");
|
|
command.add("error");
|
|
command.add("-f");
|
|
command.add("lavfi");
|
|
command.add("-i");
|
|
command.add("nullsrc=s=640x360:r=30");
|
|
command.add("-t");
|
|
command.add("1");
|
|
command.add("-c:v");
|
|
command.add(encoderName);
|
|
command.add("-f");
|
|
command.add("null");
|
|
command.add("-");
|
|
|
|
// Start the process
|
|
bool started = process.start(command, juce::ChildProcess::wantStdErr);
|
|
|
|
if (!started)
|
|
return false;
|
|
|
|
// Wait for the process to finish with a timeout
|
|
if (!process.waitForProcessToFinish(5000)) { // 5 seconds timeout
|
|
process.kill();
|
|
return false;
|
|
}
|
|
|
|
// Check exit code - 0 means success
|
|
int exitCode = process.getExitCode();
|
|
juce::String errorOutput = process.readAllProcessOutput();
|
|
|
|
// If exit code is 0 and there's no error output, the encoder works
|
|
return exitCode == 0 && errorOutput.isEmpty();
|
|
} |