11#include <condition_variable>
24#ifndef WIN32_LEAN_AND_MEAN
25#define WIN32_LEAN_AND_MEAN
34 using Clock = std::chrono::steady_clock;
43 return {
reinterpret_cast<const char*
>(
bytes.data()),
bytes.size()};
56 const auto result = std::from_chars(
text.data(),
text.data() +
text.size(),
value);
57 return result.ec == std::errc{} && result.ptr ==
text.data() +
text.size();
64 if (
slash == std::string_view::npos ||
72 std::expected<DecoderMetadata, std::string>
parseProbe(std::string_view
text)
74 DecoderMetadata result;
83 std::istringstream
input{std::string(
text)};
85 while (std::getline(
input, line))
87 if (!line.empty() && line.back() ==
'\r') line.pop_back();
88 if (line ==
"[STREAM]") { stream = {};
inStream =
true;
continue; }
89 if (line ==
"[/STREAM]")
91 if (stream.kind ==
"video" && result.videoCodec.empty())
93 result.width = stream.width;
94 result.height = stream.height;
95 result.videoCodec = stream.codec;
96 if (!
rational(stream.average, result.frameRateNumerator,
97 result.frameRateDenominator) &&
98 !
rational(stream.rate, result.frameRateNumerator,
99 result.frameRateDenominator))
100 return std::unexpected(
"FFprobe: video has no supported rational cadence (maximum 240 fps)");
101 if (std::isfinite(stream.duration) && stream.duration > 0 &&
103 result.durationMicroseconds =
static_cast<std::uint64_t
>(stream.duration * 1000000.0);
105 else if (stream.kind ==
"audio" && !result.hasAudio)
107 result.hasAudio =
true;
108 result.audioSampleRate = stream.sampleRate;
109 result.audioCodec = stream.codec;
114 const auto split = line.find(
'=');
115 if (
split == std::string::npos)
continue;
116 const std::string_view
key(line.data(),
split);
117 const std::string_view
value(line.data() +
split + 1, line.size() -
split - 1);
122 else if (
key ==
"codec_type") stream.kind =
value;
123 else if (
key ==
"codec_name") stream.codec =
value;
126 else if (
key ==
"avg_frame_rate") stream.average =
value;
127 else if (
key ==
"r_frame_rate") stream.rate =
value;
131 if (result.videoCodec.empty() || result.width == 0 || result.height == 0)
132 return std::unexpected(
"FFprobe: input has no video stream with valid dimensions");
133 if (result.width > 8192 || result.height > 8192)
134 return std::unexpected(
"FFprobe: video dimension exceeds the 8192-pixel limit");
135 if (result.durationMicroseconds == 0 && std::isfinite(
formatDuration) &&
137 result.durationMicroseconds =
static_cast<std::uint64_t
>(
formatDuration * 1000000.0);
138 if (result.durationMicroseconds == 0)
139 return std::unexpected(
"FFprobe: input has no finite movie duration within the 24-hour limit");
162 Child(
const Child&) =
delete;
163 Child& operator=(
const Child&) =
delete;
167 if (!process)
return;
182 std::expected<void, std::string> start(
const std::vector<std::string>&
arguments)
184 std::vector<const char*>
argv;
187 argv.push_back(
nullptr);
189 if (!properties)
return std::unexpected(std::string(
"SDL process properties: ") +
SDL_GetError());
205 if (!process)
return std::unexpected(
"Cannot start " +
arguments.front() +
206 "; install FFmpeg/FFprobe or configure their executable paths: " +
failure);
213 return std::unexpected(
"Cannot retain decoder process exit status (Windows error " +
219 if (!output || !errors)
return std::unexpected(
"SDL did not create the requested decoder pipes");
240 std::array<char, 4096>
buffer{};
247 if (
count == 0)
break;
258 else ioError =
"Cannot query decoder process exit status (Windows error " +
267 return std::string(
operation) +
" (exit " + std::to_string(exitCode) +
"): " +
282 std::deque<DecodedVideoFrame>
video;
283 std::deque<DecodedAudioChunk>
audio;
288 std::unique_lock lock(
mutex);
289 wake.wait_for(lock, std::chrono::milliseconds(2));
294 std::lock_guard lock(
mutex);
300 std::expected<DecoderMetadata, std::string>
probe()
304 "-protocol_whitelist",
"file,pipe",
"-show_entries",
305 "stream=codec_type,codec_name,width,height,avg_frame_rate,r_frame_rate,sample_rate,duration:format=duration",
307 if (!started)
return std::unexpected(started.error());
310 std::array<char, 4096>
buffer{};
316 return std::unexpected(
"FFprobe metadata exceeds the 64 KiB limit");
318 if (!
child.ioError.empty())
return std::unexpected(
child.failure(
"FFprobe IO failure"));
321 if (
child.exitCode != 0)
return std::unexpected(
child.failure(
"FFprobe rejected input"));
324 if (Clock::now() >=
deadline)
return std::unexpected(
"FFprobe metadata timeout");
327 return std::unexpected(
"Video probe cancelled");
333 std::vector<std::string>
args{
options.
ffmpeg,
"-nostdin",
"-hide_banner",
"-v",
"error",
"-xerror",
334 "-max_alloc",
"67108864",
"-threads",
"1",
"-protocol_whitelist",
"file,pipe",
339 const std::string
filter =
"fps=fps=" + std::to_string(
metadata.frameRateNumerator) +
340 "/" + std::to_string(
metadata.frameRateDenominator) +
":start_time=0,scale=" +
342 args.insert(
args.end(), {
"-map",
"0:v:0",
"-an",
"-sn",
"-dn",
"-vf", filter,
343 "-fps_mode",
"passthrough",
"-pix_fmt",
"rgba",
"-c:v",
"rawvideo",
344 "-f",
"rawvideo",
"pipe:1"});
346 else args.insert(
args.end(), {
"-map",
"0:a:0",
"-vn",
"-sn",
"-dn",
347 "-af",
"aresample=48000:async=1:first_pts=0",
"-ac",
"2",
"-ar",
"48000",
348 "-c:a",
"pcm_s16le",
"-f",
"s16le",
"pipe:1"});
354 std::uint64_t generation)
357 std::expected<void, std::string> started;
361 if (!started) {
fail(started.error());
return; }
366 if (!started) {
fail(started.error());
return; }
375 std::lock_guard lock(
mutex);
385 std::lock_guard lock(
mutex);
405 std::lock_guard lock(
mutex);
418 {
fail(
"FFmpeg video decoder " +
metadata.videoCodec +
" returned an incomplete or empty RGBA stream");
return; }
424 {
fail(
"FFmpeg video decoder " +
metadata.videoCodec +
" ended before the declared movie duration");
return; }
426 std::lock_guard lock(
mutex);
439 {
fail(
"FFmpeg audio decoder " +
metadata.audioCodec +
" returned incomplete stereo S16LE PCM");
return; }
445 std::lock_guard lock(
mutex);
457 std::lock_guard lock(
mutex);
463 std::lock_guard lock(
mutex);
469 else if (Clock::now() -
lastProgress > std::chrono::seconds(30))
470 {
fail(
"FFmpeg decode timed out without output for video codec " +
metadata.videoCodec);
return; }
483 {
fail(
"Decoded RGBA frame exceeds the configured video memory limit");
return; }
485 std::lock_guard lock(
mutex);
489 std::uint64_t
offset{}, generation{};
493 std::lock_guard lock(
mutex);
501 std::unique_lock lock(
mutex);
506 catch (
const std::exception&
error) {
fail(std::string(
"Video decoder worker: ") +
error.what()); }
507 catch (...) {
fail(
"Video decoder worker: unknown failure"); }
514 std::expected<void, std::string>
Decoder::open(
const std::filesystem::path& file,
518 return std::unexpected(
"FFmpeg and FFprobe executable paths must not be empty");
525 return std::unexpected(
"Invalid decoder limits (RGBA queues maximum 64 MiB, frame maximum 16 MiB)");
526 std::error_code
error;
527 const auto absolute = std::filesystem::absolute(file,
error);
529 return std::unexpected(
"Video input is not a readable local file: " +
utf8Path(file));
531 impl_->options = options;
533 impl_->stopping.store(
false);
535 std::lock_guard lock(impl_->mutex);
537 ++impl_->state.generation;
539 try { impl_->worker = std::thread([
this] { impl_->run(); }); }
542 const auto failure = std::string(
"Cannot start video decoder worker: ") +
exception.what();
544 return std::unexpected(
failure);
549 std::expected<void, std::string>
Decoder::seek(std::uint64_t timestampMicroseconds)
552 std::lock_guard lock(impl_->mutex);
555 return std::unexpected(
"Cannot seek before movie metadata is ready or after decoder failure");
556 if (timestampMicroseconds >= impl_->state.metadata->durationMicroseconds)
557 return std::unexpected(
"Video seek position is outside the movie duration");
558 impl_->video.clear();
559 impl_->audio.clear();
560 impl_->pendingSeek = timestampMicroseconds;
561 ++impl_->state.generation;
563 impl_->state.videoEnded =
false;
564 impl_->state.audioEnded =
false;
566 impl_->wake.notify_all();
572 impl_->stopping.store(
true);
573 impl_->wake.notify_all();
574 if (impl_->worker.joinable()) impl_->worker.join();
575 std::lock_guard lock(impl_->mutex);
576 impl_->video.clear();
577 impl_->audio.clear();
578 impl_->pendingSeek.reset();
579 const auto generation = impl_->state.generation;
581 impl_->state.generation = generation;
586 std::lock_guard lock(impl_->mutex);
587 auto state = impl_->state;
588 state.queuedVideoFrames = impl_->video.size();
589 state.queuedAudioChunks = impl_->audio.size();
595 std::lock_guard lock(impl_->mutex);
596 if (impl_->video.empty())
return std::nullopt;
597 auto frame = std::move(impl_->video.front());
598 impl_->video.pop_front();
599 impl_->wake.notify_all();
605 std::lock_guard lock(impl_->mutex);
606 if (impl_->audio.empty())
return std::nullopt;
607 auto chunk = std::move(impl_->audio.front());
608 impl_->audio.pop_front();
609 impl_->wake.notify_all();
LegacyBitmapMetadata metadata
std::vector< std::byte > bytes
HelpClock::time_point deadline
std::deque< Bytes > output
Clock::time_point lastProgress
std::optional< DecodedVideoFrame > popVideo()
std::expected< void, std::string > seek(std::uint64_t timestampMicroseconds)
std::expected< void, std::string > open(const std::filesystem::path &file, const DecoderOptions &options={})
std::optional< DecodedAudioChunk > popAudio()
DecoderSnapshot snapshot() const
static constexpr std::uint32_t sampleRate
std::size_t maximumFrameBytes
std::uint32_t probeTimeoutMilliseconds
std::size_t videoQueueFrames
std::size_t audioQueueChunks
std::optional< DecoderMetadata > metadata
std::deque< DecodedVideoFrame > video
std::expected< DecoderMetadata, std::string > probe()
std::deque< DecodedAudioChunk > audio
std::vector< std::string > command(std::uint64_t offset, bool isVideo, const DecoderMetadata &metadata) const
std::atomic< bool > stopping
void decode(const DecoderMetadata &metadata, std::uint64_t offset, std::uint64_t generation)
std::optional< std::uint64_t > pendingSeek
void fail(std::string error)
std::condition_variable wake
std::filesystem::path file