engine: prewarm conforms before facade export
Drives ConformManager for every audio-streaming footage with the same AudioParams the export will use, before starting ExportTask - matching the app's preview prewarm semantics. Root-cause analysis showed the export's own wait-for-conform path already worked; this remains as a first-export speedup and a belt-and-braces guarantee
This commit is contained in:
@@ -25,15 +25,18 @@
|
|||||||
|
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
#include <QElapsedTimer>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
|
||||||
|
#include "codec/conformmanager.h"
|
||||||
#include "codec/encoder.h"
|
#include "codec/encoder.h"
|
||||||
#include "codec/ffmpeg/ffmpegencoder.h"
|
#include "codec/ffmpeg/ffmpegencoder.h"
|
||||||
#include "coreengine.h"
|
#include "coreengine.h"
|
||||||
#include "node/color/colormanager/colormanager.h"
|
#include "node/color/colormanager/colormanager.h"
|
||||||
#include "node/project.h"
|
#include "node/project.h"
|
||||||
|
#include "node/project/footage/footage.h"
|
||||||
#include "node/project/sequence/sequence.h"
|
#include "node/project/sequence/sequence.h"
|
||||||
#include "render/rendermanager.h"
|
#include "render/rendermanager.h"
|
||||||
#include "task/export/export.h"
|
#include "task/export/export.h"
|
||||||
@@ -126,6 +129,72 @@ QString image_sequence_filename(const QString &path)
|
|||||||
fi.suffix());
|
fi.suffix());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-generate audio conforms for every footage with audio streams in the
|
||||||
|
// project, using exactly the AudioParams the export render will request.
|
||||||
|
// This mirrors what the application gets for free from preview playback
|
||||||
|
// (conforms are usually already cached by export time there) and makes
|
||||||
|
// first-time exports of freshly imported media both faster and
|
||||||
|
// deterministic. Delivery to the TaskManager thread and the completion
|
||||||
|
// signal both go through this thread's event queue, so the wait pumps
|
||||||
|
// events like the render wait does. Returns false on timeout; the caller
|
||||||
|
// proceeds anyway since the render's own conform wait is the fallback.
|
||||||
|
bool prewarm_conforms(olive::Project *project,
|
||||||
|
const olive::AudioParams ¶ms, QString *error)
|
||||||
|
{
|
||||||
|
if (!olive::ConformManager::instance() || params.sample_rate() <= 0) {
|
||||||
|
return true; // nothing to prewarm (or nothing to prewarm with)
|
||||||
|
}
|
||||||
|
const QString cache_path = project->cache_path();
|
||||||
|
|
||||||
|
struct Pending {
|
||||||
|
QString decoder_id;
|
||||||
|
olive::Decoder::CodecStream stream;
|
||||||
|
};
|
||||||
|
QVector<Pending> pending;
|
||||||
|
for (olive::Node *n : project->nodes()) {
|
||||||
|
olive::Footage *footage = dynamic_cast<olive::Footage *>(n);
|
||||||
|
if (!footage || !footage->is_valid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString decoder_id = footage->decoder().isEmpty() ?
|
||||||
|
QStringLiteral("ffmpeg") :
|
||||||
|
footage->decoder();
|
||||||
|
for (int i = 0; i < footage->get_audio_stream_count(); i++) {
|
||||||
|
const olive::AudioParams stream_params =
|
||||||
|
footage->get_audio_params(i);
|
||||||
|
olive::Decoder::CodecStream stream(footage->filename(),
|
||||||
|
stream_params.stream_index(),
|
||||||
|
nullptr);
|
||||||
|
// Trigger (or adopt) the conform task.
|
||||||
|
olive::ConformManager::instance()->get_conform_state(
|
||||||
|
decoder_id, cache_path, stream, params, false);
|
||||||
|
pending.append({ decoder_id, stream });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr qint64 k_prewarm_timeout_ms = 120000;
|
||||||
|
QElapsedTimer timer;
|
||||||
|
timer.start();
|
||||||
|
for (const Pending &p : pending) {
|
||||||
|
while (true) {
|
||||||
|
const olive::ConformManager::Conform conform =
|
||||||
|
olive::ConformManager::instance()->get_conform_state(
|
||||||
|
p.decoder_id, cache_path, p.stream, params, false);
|
||||||
|
if (conform.state == olive::ConformManager::k_conform_exists) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (timer.hasExpired(k_prewarm_timeout_ms)) {
|
||||||
|
*error = QStringLiteral("conform prewarm timed out for %1")
|
||||||
|
.arg(p.stream.filename());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 20);
|
||||||
|
QThread::msleep(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
@@ -264,6 +333,15 @@ int oakengine_export_render(OakEngineSequence *seq, const char *path,
|
|||||||
olive::ColorTransform(QStringLiteral("sRGB OETF")));
|
olive::ColorTransform(QStringLiteral("sRGB OETF")));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Pre-generate the audio conforms the render is about to need (the
|
||||||
|
// headless equivalent of the application's preview-warmed cache).
|
||||||
|
// A timeout here is not fatal: the render's own conform wait is the
|
||||||
|
// fallback path.
|
||||||
|
if (audio_enabled) {
|
||||||
|
QString prewarm_error;
|
||||||
|
prewarm_conforms(project, ap, &prewarm_error);
|
||||||
|
}
|
||||||
|
|
||||||
olive::ExportTask task(sequence, project->color_manager(), params);
|
olive::ExportTask task(sequence, project->color_manager(), params);
|
||||||
// The progress signal is emitted on the task thread, so the callback
|
// The progress signal is emitted on the task thread, so the callback
|
||||||
// (installed for the calling thread) is captured by value -- reading
|
// (installed for the calling thread) is captured by value -- reading
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "oakengine/exporter.h"
|
#include "oakengine/exporter.h"
|
||||||
|
#include "oakengine/footage.h"
|
||||||
#include "oakengine/init.h"
|
#include "oakengine/init.h"
|
||||||
#include "oakengine/project.h"
|
#include "oakengine/project.h"
|
||||||
#include "oakengine/timeline.h"
|
#include "oakengine/timeline.h"
|
||||||
@@ -173,6 +174,35 @@ static void test_codecs_and_validation(OakEngineSequence *seq)
|
|||||||
assert(oakengine_export_last_error(err, sizeof(err)) > 0);
|
assert(oakengine_export_last_error(err, sizeof(err)) > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a loud test tone (440 Hz stereo sine, 2 s) with the ffmpeg
|
||||||
|
// CLI -- the demo file's own audio track is essentially silent, so a real
|
||||||
|
// tone is needed to exercise "audio content present" assertions.
|
||||||
|
static void make_tone(const char *path)
|
||||||
|
{
|
||||||
|
char cmd[4608];
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"ffmpeg -v error -y -f lavfi -i "
|
||||||
|
"\"sine=frequency=440:duration=2\" -ar 48000 -ac 2 \"%s\"",
|
||||||
|
path);
|
||||||
|
assert(system(cmd) == 0);
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
assert(f != NULL);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe a media file with ffprobe/ffmpeg and assert the output contains a
|
||||||
|
// string (used for codec/dimension and loudness checks).
|
||||||
|
static void assert_probe_matches(const char *cmd, const char *needle)
|
||||||
|
{
|
||||||
|
FILE *probe = popen(cmd, "r");
|
||||||
|
assert(probe != NULL);
|
||||||
|
char out[2048] = { 0 };
|
||||||
|
const size_t len = fread(out, 1, sizeof(out) - 1, probe);
|
||||||
|
(void)len;
|
||||||
|
assert(pclose(probe) == 0);
|
||||||
|
assert(strstr(out, needle) != NULL);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
make_tmpdir();
|
make_tmpdir();
|
||||||
@@ -269,6 +299,72 @@ int main(void)
|
|||||||
const double duration = atof(comma + 1);
|
const double duration = atof(comma + 1);
|
||||||
assert(fabs(duration - 1.001) < 0.15);
|
assert(fabs(duration - 1.001) < 0.15);
|
||||||
|
|
||||||
|
// ---- First-conform audio export ---------------------------------------
|
||||||
|
// The sandboxed cache guarantees no conform exists yet: this is the
|
||||||
|
// exact first-time-export path that used to come out silent. Lay a loud
|
||||||
|
// tone clip on an audio track and export with AAC audio; the exported
|
||||||
|
// audio must actually contain the tone.
|
||||||
|
{
|
||||||
|
char tone_path[4096];
|
||||||
|
snprintf(tone_path, sizeof(tone_path), "%s/tone.wav", g_tmpdir);
|
||||||
|
make_tone(tone_path);
|
||||||
|
|
||||||
|
// Import through the facade and place through the timeline editing
|
||||||
|
// primitives.
|
||||||
|
OakEngineFootage *tone =
|
||||||
|
oakengine_project_import_footage(project, tone_path);
|
||||||
|
assert(tone != NULL);
|
||||||
|
assert(oakengine_sequence_add_track(seq, OAKENGINE_TRACK_TYPE_AUDIO) ==
|
||||||
|
0);
|
||||||
|
assert(oakengine_sequence_add_footage_clip(
|
||||||
|
seq, tone, OAKENGINE_TRACK_TYPE_AUDIO, 0, 0, 30, 0) !=
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
char out2[4096];
|
||||||
|
snprintf(out2, sizeof(out2), "%s/export_audio.mp4", g_tmpdir);
|
||||||
|
oak_export_options opts2;
|
||||||
|
memset(&opts2, 0, sizeof(opts2));
|
||||||
|
opts2.video_codec = OAKENGINE_EXPORT_VIDEO_H264;
|
||||||
|
opts2.audio_codec = OAKENGINE_EXPORT_AUDIO_AAC;
|
||||||
|
opts2.audio_sample_rate = 48000;
|
||||||
|
opts2.audio_channel_count = 2;
|
||||||
|
rc = oakengine_export_render(seq, out2, 0, 30, 320, 180, &opts2);
|
||||||
|
if (rc != OAKENGINE_OK) {
|
||||||
|
fprintf(stderr, "audio export failed (%d): %s\n", rc,
|
||||||
|
oakengine_export_last_error(err, sizeof(err)) > 0 ?
|
||||||
|
err :
|
||||||
|
"(no error)");
|
||||||
|
}
|
||||||
|
assert(rc == OAKENGINE_OK);
|
||||||
|
assert(access(out2, F_OK) == 0);
|
||||||
|
|
||||||
|
// The MP4 carries an AAC audio stream...
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"ffprobe -v error -select_streams a:0 -show_entries "
|
||||||
|
"stream=codec_name -of csv=p=0 \"%s\"",
|
||||||
|
out2);
|
||||||
|
assert_probe_matches(cmd, "aac");
|
||||||
|
|
||||||
|
// ...and the tone is really in there (sine is ~-24 dB; silence
|
||||||
|
// would read ~-91 dB).
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"ffmpeg -v info -i \"%s\" -map a:0 -af volumedetect -f "
|
||||||
|
"null - 2>&1 | grep mean_volume",
|
||||||
|
out2);
|
||||||
|
FILE *vol = popen(cmd, "r");
|
||||||
|
assert(vol != NULL);
|
||||||
|
char vol_out[512] = { 0 };
|
||||||
|
const size_t vol_len = fread(vol_out, 1, sizeof(vol_out) - 1, vol);
|
||||||
|
(void)vol_len;
|
||||||
|
assert(pclose(vol) == 0);
|
||||||
|
const char *db = strstr(vol_out, "mean_volume:");
|
||||||
|
assert(db != NULL);
|
||||||
|
const double mean_db = atof(db + strlen("mean_volume:"));
|
||||||
|
assert(mean_db > -60.0);
|
||||||
|
|
||||||
|
oakengine_footage_free(tone);
|
||||||
|
}
|
||||||
|
|
||||||
oakengine_project_free(project);
|
oakengine_project_free(project);
|
||||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user