Add H265, VP9, and ProRes codecs for recording

pull/298/head
James H Ball 2025-04-04 12:10:44 +01:00
rodzic c205de10b7
commit 6a1306ae15
4 zmienionych plików z 97 dodań i 14 usunięć

Wyświetl plik

@ -62,7 +62,7 @@ public:
VisualiserSettings visualiserSettings = VisualiserSettings(audioProcessor.visualiserParameters, 3);
RecordingSettings recordingSettings = RecordingSettings(audioProcessor.recordingParameters);
SettingsWindow recordingSettingsWindow = SettingsWindow("Recording Settings", recordingSettings, 330, 320, 330, 320);
SettingsWindow recordingSettingsWindow = SettingsWindow("Recording Settings", recordingSettings, 330, 350, 330, 350);
VisualiserComponent visualiser{
audioProcessor,
#if SOSCI_FEATURES

Wyświetl plik

@ -13,6 +13,8 @@ RecordingSettings::RecordingSettings(RecordingParameters& ps) : parameters(ps) {
addAndMakeVisible(recordVideo);
addAndMakeVisible(compressionPreset);
addAndMakeVisible(compressionPresetLabel);
addAndMakeVisible(videoCodecSelector);
addAndMakeVisible(videoCodecLabel);
addAndMakeVisible(customSharedTextureOutputLabel);
addAndMakeVisible(customSharedTextureOutputEditor);
@ -44,6 +46,19 @@ RecordingSettings::RecordingSettings(RecordingParameters& ps) : parameters(ps) {
compressionPreset.setSelectedId(parameters.compressionPresets.indexOf(parameters.compressionPreset) + 1);
compressionPresetLabel.setTooltip("The compression preset to use when recording video. Slower presets will produce smaller files at the expense of encoding time.");
// Setup the codec selector
videoCodecSelector.addItem("H.264", static_cast<int>(VideoCodec::H264) + 1);
videoCodecSelector.addItem("H.265/HEVC", static_cast<int>(VideoCodec::H265) + 1);
videoCodecSelector.addItem("VP9", static_cast<int>(VideoCodec::VP9) + 1);
#if JUCE_MAC
videoCodecSelector.addItem("ProRes", static_cast<int>(VideoCodec::ProRes) + 1);
#endif
videoCodecSelector.setSelectedId(static_cast<int>(parameters.videoCodec) + 1);
videoCodecSelector.onChange = [this] {
parameters.videoCodec = static_cast<VideoCodec>(videoCodecSelector.getSelectedId() - 1);
};
videoCodecLabel.setTooltip("The video codec to use when recording. Different codecs offer different trade-offs between quality, file size, and compatibility.");
customSharedTextureOutputLabel.setTooltip("Custom name for when creating a new Syphon/Spout server. WARNING: You should not use the same name when running multiple servers at once!.");
customSharedTextureOutputEditor.setText(parameters.customSharedTextureServerName);
customSharedTextureOutputEditor.onTextChange = [this] {
@ -77,10 +92,16 @@ void RecordingSettings::resized() {
frameRate.setBounds(area.removeFromTop(rowHeight).expanded(6, 0));
recordAudio.setBounds(area.removeFromTop(rowHeight));
recordVideo.setBounds(area.removeFromTop(rowHeight));
auto row = area.removeFromTop(rowHeight);
compressionPresetLabel.setBounds(row.removeFromLeft(170));
compressionPreset.setBounds(row.removeFromRight(100));
area.removeFromTop(5);
row = area.removeFromTop(rowHeight);
videoCodecLabel.setBounds(row.removeFromLeft(170));
videoCodecSelector.setBounds(row.removeFromRight(100));
area.removeFromTop(5);
row = area.removeFromTop(rowHeight);
customSharedTextureOutputLabel.setBounds(row.removeFromLeft(170));

Wyświetl plik

@ -8,6 +8,16 @@
#define VERSION_HINT 2
// Define codec options
enum class VideoCodec {
H264,
H265,
VP9,
#if JUCE_MAC
ProRes,
#endif
};
class RecordingParameters {
public:
RecordingParameters() {
@ -58,6 +68,7 @@ public:
Effect frameRateEffect = Effect(&frameRate);
juce::String compressionPreset = "fast";
VideoCodec videoCodec = VideoCodec::H264;
void save(juce::XmlElement* xml) {
auto settingsXml = xml->createNewChildElement("recordingSettings");
@ -66,6 +77,7 @@ public:
recordVideo.save(settingsXml->createNewChildElement("recordVideo"));
settingsXml->setAttribute("compressionPreset", compressionPreset);
settingsXml->setAttribute("customSharedTextureServerName", customSharedTextureServerName);
settingsXml->setAttribute("videoCodec", static_cast<int>(videoCodec));
auto qualityXml = settingsXml->createNewChildElement("quality");
qualityEffect.save(qualityXml);
@ -95,6 +107,10 @@ public:
if (settingsXml->hasAttribute("customSharedTextureServerName")) {
customSharedTextureServerName = settingsXml->getStringAttribute("customSharedTextureServerName");
}
if (settingsXml->hasAttribute("videoCodec")) {
int codecValue = settingsXml->getIntAttribute("videoCodec", 0);
videoCodec = static_cast<VideoCodec>(codecValue);
}
if (auto* qualityXml = settingsXml->getChildByName("quality")) {
qualityEffect.load(qualityXml);
}
@ -163,6 +179,24 @@ public:
return parameters.frameRate.getValueUnnormalised();
}
VideoCodec getVideoCodec() {
return parameters.videoCodec;
}
juce::String getFileExtensionForCodec() {
switch (parameters.videoCodec) {
#if JUCE_MAC
case VideoCodec::ProRes:
return "mov";
#endif
case VideoCodec::H264:
case VideoCodec::H265:
case VideoCodec::VP9:
default:
return "mp4";
}
}
RecordingParameters& parameters;
private:
@ -182,6 +216,9 @@ private:
juce::Label compressionPresetLabel{"Compression Speed", "Compression Speed"};
juce::ComboBox compressionPreset;
juce::Label videoCodecLabel{"Video Codec", "Video Codec"};
juce::ComboBox videoCodecSelector;
juce::Label customSharedTextureOutputLabel{"Custom Syphon/Spout Name", "Custom Syphon/Spout Name"};
juce::TextEditor customSharedTextureOutputEditor{"customSharedTextureOutputEditor"};

Wyświetl plik

@ -425,7 +425,11 @@ void VisualiserComponent::setRecording(bool recording) {
record.setToggleState(false, juce::NotificationType::dontSendNotification);
return;
}
tempVideoFile = std::make_unique<juce::TemporaryFile>(".mp4");
// Get the appropriate file extension based on codec
juce::String fileExtension = recordingSettings.getFileExtensionForCodec();
tempVideoFile = std::make_unique<juce::TemporaryFile>("." + fileExtension);
juce::String resolution = std::to_string(renderTexture.width) + "x" + std::to_string(renderTexture.height);
juce::String cmd = "\"" + ffmpegFile.getFullPathName() + "\"" +
" -r " + juce::String(recordingSettings.getFrameRate()) +
@ -436,18 +440,39 @@ void VisualiserComponent::setRecording(bool recording) {
" -threads 4" +
" -preset " + recordingSettings.getCompressionPreset() +
" -y" +
" -pix_fmt yuv420p" +
" -crf " + juce::String(recordingSettings.getCRF()) +
#if JUCE_MAC
#if JUCE_ARM
// use software encoding on Apple Silicon
" -c:v hevc_videotoolbox" +
" -q:v " + juce::String(recordingSettings.getVideoToolboxQuality()) +
" -tag:v hvc1" +
#endif
" -pix_fmt yuv420p";
// Apply codec-specific parameters
VideoCodec codec = recordingSettings.getVideoCodec();
if (codec == VideoCodec::H264) {
cmd += " -c:v libx264";
cmd += " -crf " + juce::String(recordingSettings.getCRF());
}
else if (codec == VideoCodec::H265) {
cmd += " -c:v libx265";
cmd += " -crf " + juce::String(recordingSettings.getCRF());
#if JUCE_MAC && JUCE_ARM
// use hardware encoding on Apple Silicon
cmd += " -c:v hevc_videotoolbox";
cmd += " -q:v " + juce::String(recordingSettings.getVideoToolboxQuality());
cmd += " -tag:v hvc1";
#endif
" -vf vflip" +
" \"" + tempVideoFile->getFile().getFullPathName() + "\"";
}
else if (codec == VideoCodec::VP9) {
cmd += " -c:v libvpx-vp9";
cmd += " -b:v 0";
cmd += " -crf " + juce::String(recordingSettings.getCRF());
cmd += " -deadline good -cpu-used 2";
}
#if JUCE_MAC
else if (codec == VideoCodec::ProRes) {
cmd += " -c:v prores";
cmd += " -profile:v 3"; // ProRes 422 HQ
}
#endif
cmd += " -vf vflip";
cmd += " \"" + tempVideoFile->getFile().getFullPathName() + "\"";
ffmpegProcess.start(cmd);
framePixels.resize(renderTexture.width * renderTexture.height * 4);
@ -472,7 +497,7 @@ void VisualiserComponent::setRecording(bool recording) {
recordingAudio = false;
recordingVideo = false;
juce::String extension = wasRecordingVideo ? "mp4" : "wav";
juce::String extension = wasRecordingVideo ? recordingSettings.getFileExtensionForCodec() : "wav";
if (wasRecordingAudio) {
audioRecorder.stop();
}