#include "player.h" #include "player_macos.h" #include "output.hpp" extern "C" { #include #include #include #include #include #include } #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr int sampleRate = 48000; constexpr std::size_t commandLimit = 32; constexpr std::size_t videoFrameLimit = 64; constexpr std::size_t videoByteLimit = 128 * 1024 * 1024; constexpr int audioFrameLimit = sampleRate * 4; constexpr double sampleTime = 1.0 / sampleRate; using Clock = std::chrono::steady_clock; struct Cancelled {}; struct FrameDeleter { void operator()(AVFrame *frame) const { av_frame_free(&frame); } }; using Frame = std::unique_ptr; struct VideoFrame { Frame frame; double pts; double end; std::size_t bytes; uint64_t timestampOccurrence = 0; }; struct Track { AVCodecContext *codec = nullptr; int index = -1; bool drainSent = false; bool drained = false; double nextPts = 0; int64_t countedTimestamp = AV_NOPTS_VALUE; uint64_t timestampOccurrence = 0; }; enum class Action { Open, Play, Pause, Seek, Step, Close }; struct Command { Action action; std::string path; double seconds = 0; int direction = 0; uint64_t epoch = 0; uint64_t repetitions = 1; }; void copyError(char *destination, const char *message) noexcept { std::snprintf(destination, 512, "%s", message ? message : "Unknown playback error"); } int64_t presentationTimestamp(const AVFrame *frame) { return frame->best_effort_timestamp != AV_NOPTS_VALUE ? frame->best_effort_timestamp : frame->pts; } } struct Player { playback::Output output; std::mutex mutex; std::condition_variable wake; std::deque commands; PlayerState state{}; uint64_t publishedEpoch = 0; std::array requestedError{}; bool errorRequested = false; uint64_t errorEpoch = 0; std::atomic stopping{false}; std::atomic requestedEpoch{0}; uint64_t activeEpoch = 0; // Everything below, including FFmpeg and Output audio operations, is worker-owned. AVFormatContext *format = nullptr; std::array tracks; AVPacket *packet = nullptr; AVFrame *decoded = nullptr; SwrContext *resampler = nullptr; AVChannelLayout inputLayout{}; int inputRate = 0; AVSampleFormat inputFormat = AV_SAMPLE_FMT_NONE; bool resamplerDrained = false; bool packetPending = false; bool demuxEnded = false; int receiving = -1; std::deque video; std::size_t videoBytes = 0; VideoFrame presented{}; int64_t skipPresentedTimestamp = AV_NOPTS_VALUE; uint64_t skipPresentedOccurrence = 0; bool frameStepping = false; // ponytail: long forward runs can make resume preroll long; refresh verified anchors if that latency matters. int64_t steppingAnchor = AV_NOPTS_VALUE; std::vector pcm; std::size_t pcmFrames = 0; std::size_t pcmOffset = 0; double pcmPts = 0; double resampleNextPts = 0; double origin = 0; double duration = 0; double position = 0; double seekTarget = 0; double audioWriteEnd = 0; double videoEnd = 0; double videoFrameTime = 1.0 / 25; double steadyPosition = 0; Clock::time_point steadyAnchor = Clock::now(); PlayerStatus status = PLAYER_EMPTY; bool previewShown = false; std::array error{}; std::thread worker; Player() : worker([this] { run(); }) {} ~Player() { stopping.store(true, std::memory_order_relaxed); requestedEpoch.fetch_add(1, std::memory_order_relaxed); wake.notify_one(); if (worker.joinable()) worker.join(); } bool enqueue(Command command) { std::lock_guard lock(mutex); if (stopping.load(std::memory_order_relaxed)) return false; if (command.action == Action::Step && (!state.has_video || publishedEpoch != requestedEpoch.load(std::memory_order_relaxed) || (state.status != PLAYER_PAUSED && state.status != PLAYER_PLAYING && state.status != PLAYER_ENDED))) return false; const bool newTimeline = command.action == Action::Open || command.action == Action::Close; command.epoch = requestedEpoch.load(std::memory_order_relaxed) + (newTimeline ? 1 : 0); if (command.action == Action::Step && !commands.empty()) { auto &last = commands.back(); if (last.action == Action::Step && last.direction == command.direction && last.epoch == command.epoch) { if (last.repetitions == std::numeric_limits::max()) return false; ++last.repetitions; state.seek_pending = true; wake.notify_one(); return true; } } if (commands.size() >= commandLimit) return false; commands.push_back(std::move(command)); if (newTimeline) requestedEpoch.store(commands.back().epoch, std::memory_order_relaxed); if (commands.back().action == Action::Open) { const auto revision = state.revision + 1; state = {}; state.status = PLAYER_OPENING; state.revision = revision; } if (commands.back().action == Action::Seek || commands.back().action == Action::Step) state.seek_pending = true; wake.notify_one(); return true; } void apiError(const char *message) noexcept { try { std::lock_guard lock(mutex); copyError(requestedError.data(), message); errorRequested = true; errorEpoch = requestedEpoch.fetch_add(1, std::memory_order_relaxed) + 1; state.status = PLAYER_ERROR; copyError(state.error, message); ++state.revision; wake.notify_one(); } catch (...) { // No exception may cross the C boundary, including a failed mutex operation. } } bool interrupted() const { return stopping.load(std::memory_order_relaxed) || requestedEpoch.load(std::memory_order_relaxed) != activeEpoch; } static int interrupt(void *opaque) { return static_cast(opaque)->interrupted(); } void check(int result, const char *operation) { if (result >= 0) return; if (interrupted()) throw Cancelled{}; char details[AV_ERROR_MAX_STRING_SIZE]; av_strerror(result, details, sizeof(details)); throw std::runtime_error(std::string(operation) + ": " + details); } void publish() { std::lock_guard lock(mutex); if (activeEpoch != requestedEpoch.load(std::memory_order_relaxed)) return; publishedEpoch = activeEpoch; state.status = status; state.position = position; state.duration = duration; state.has_audio = tracks[0].codec != nullptr; state.has_video = tracks[1].codec != nullptr; // Commands remain pending while executing; publication acknowledges only completed work. state.seek_pending = std::any_of(commands.begin(), commands.end(), [this](const Command &command) { return (command.action == Action::Seek || command.action == Action::Step) && command.epoch == activeEpoch; }); state.video_aspect_ratio = 0; if (const auto *video = tracks[1].codec; video && video->width > 0 && video->height > 0) { const auto aspect = av_guess_sample_aspect_ratio(format, format->streams[tracks[1].index], nullptr); state.video_aspect_ratio = double(video->width) / video->height * (aspect.num > 0 && aspect.den > 0 ? av_q2d(aspect) : 1); } std::memcpy(state.error, error.data(), error.size()); ++state.revision; } void release() { output.pause(); output.reset(); video.clear(); videoBytes = 0; presented = {}; skipPresentedTimestamp = AV_NOPTS_VALUE; frameStepping = false; steppingAnchor = AV_NOPTS_VALUE; swr_free(&resampler); av_channel_layout_uninit(&inputLayout); for (auto &track : tracks) { avcodec_free_context(&track.codec); track = {}; } av_packet_free(&packet); av_frame_free(&decoded); avformat_close_input(&format); inputRate = 0; inputFormat = AV_SAMPLE_FMT_NONE; pcmFrames = pcmOffset = 0; packetPending = demuxEnded = resamplerDrained = false; receiving = -1; origin = duration = position = seekTarget = audioWriteEnd = videoEnd = resampleNextPts = 0; previewShown = false; error.fill(0); status = PLAYER_EMPTY; } void fail(const char *message) { // Preserve messages that point into one of this object's own state buffers. std::array saved{}; copyError(saved.data(), message); release(); error = saved; status = PLAYER_ERROR; publish(); } void openTrack(int slot, AVMediaType type, int related) { const AVCodec *decoder = nullptr; int index = av_find_best_stream(format, type, -1, related, &decoder, 0); if (index == AVERROR_STREAM_NOT_FOUND) return; check(index, type == AVMEDIA_TYPE_AUDIO ? "Finding audio decoder" : "Finding video decoder"); auto &track = tracks[slot]; track.index = index; track.codec = avcodec_alloc_context3(decoder); if (!track.codec) throw std::bad_alloc(); check(avcodec_parameters_to_context(track.codec, format->streams[index]->codecpar), "Reading codec parameters"); track.codec->pkt_timebase = format->streams[index]->time_base; // A bounded software decoder pool; FFmpeg owns its internal frame reorder buffer. track.codec->thread_count = 2; check(avcodec_open2(track.codec, decoder, nullptr), "Opening decoder"); } void resetTimeline(double target, bool clearVideo = true) { output.pause(); output.reset(clearVideo); for (auto &track : tracks) { if (!track.codec) continue; avcodec_flush_buffers(track.codec); track.drainSent = track.drained = false; track.countedTimestamp = AV_NOPTS_VALUE; track.timestampOccurrence = 0; const AVStream *stream = format->streams[track.index]; track.nextPts = stream->start_time == AV_NOPTS_VALUE ? target : std::max(target, stream->start_time * av_q2d(stream->time_base) - origin); } swr_free(&resampler); av_channel_layout_uninit(&inputLayout); inputRate = 0; inputFormat = AV_SAMPLE_FMT_NONE; av_packet_unref(packet); av_frame_unref(decoded); video.clear(); videoBytes = 0; presented = {}; skipPresentedTimestamp = AV_NOPTS_VALUE; frameStepping = false; steppingAnchor = AV_NOPTS_VALUE; pcmFrames = pcmOffset = 0; packetPending = demuxEnded = resamplerDrained = false; receiving = -1; position = seekTarget = audioWriteEnd = videoEnd = resampleNextPts = target; previewShown = false; steadyPosition = target; steadyAnchor = Clock::now(); } void open(const std::string &path) { release(); status = PLAYER_OPENING; publish(); std::error_code filesystemError; if (!std::filesystem::is_regular_file(path, filesystemError)) throw std::runtime_error(filesystemError ? filesystemError.message() : "Open requires a local regular media file"); const std::string localPath = std::filesystem::absolute(path).string(); format = avformat_alloc_context(); if (!format) throw std::bad_alloc(); format->interrupt_callback = {interrupt, this}; AVDictionary *options = nullptr; const int optionResult = av_dict_set(&options, "protocol_whitelist", "file", 0); if (optionResult < 0) { av_dict_free(&options); check(optionResult, "Restricting input to local files"); } const int result = avformat_open_input(&format, localPath.c_str(), nullptr, &options); av_dict_free(&options); check(result, "Opening media"); check(avformat_find_stream_info(format, nullptr), "Reading media information"); openTrack(1, AVMEDIA_TYPE_VIDEO, -1); openTrack(0, AVMEDIA_TYPE_AUDIO, tracks[1].index); if (!tracks[0].codec && !tracks[1].codec) throw std::runtime_error("The file contains no playable audio or video track"); for (unsigned i = 0; i < format->nb_streams; ++i) { if (static_cast(i) != tracks[0].index && static_cast(i) != tracks[1].index) format->streams[i]->discard = AVDISCARD_ALL; } if (format->start_time != AV_NOPTS_VALUE) { origin = static_cast(format->start_time) / AV_TIME_BASE; } else { double first = std::numeric_limits::infinity(); for (const auto &track : tracks) { if (!track.codec) continue; const AVStream *stream = format->streams[track.index]; if (stream->start_time != AV_NOPTS_VALUE) first = std::min(first, stream->start_time * av_q2d(stream->time_base)); } origin = std::isfinite(first) ? first : 0; } if (format->duration != AV_NOPTS_VALUE && format->duration > 0) duration = static_cast(format->duration) / AV_TIME_BASE; if (duration == 0) { for (const auto &track : tracks) { if (!track.codec) continue; const AVStream *stream = format->streams[track.index]; if (stream->duration == AV_NOPTS_VALUE || stream->duration <= 0) continue; const double start = stream->start_time == AV_NOPTS_VALUE ? origin : stream->start_time * av_q2d(stream->time_base); duration = std::max(duration, start - origin + stream->duration * av_q2d(stream->time_base)); } } if (tracks[1].codec) { const AVRational rate = av_guess_frame_rate(format, format->streams[tracks[1].index], nullptr); videoFrameTime = rate.num > 0 && rate.den > 0 ? av_q2d(av_inv_q(rate)) : 1.0 / 25; } if (tracks[0].codec) { std::string deviceError; if (!output.configure_audio(deviceError)) throw std::runtime_error(deviceError.empty() ? "Cannot configure audio output" : deviceError); } packet = av_packet_alloc(); decoded = av_frame_alloc(); if (!packet || !decoded) throw std::bad_alloc(); resetTimeline(0); status = PLAYER_PAUSED; publish(); } double clockPosition() const { if (status != PLAYER_PLAYING) return position; if (tracks[0].codec) { const double audioClock = output.clock(); return std::isfinite(audioClock) ? std::max(seekTarget, audioClock) : position; } return steadyPosition + std::chrono::duration(Clock::now() - steadyAnchor).count(); } void start() { if (!format || status == PLAYER_ERROR || status == PLAYER_EMPTY) return; if (status == PLAYER_ENDED) seek(0, false); if (status == PLAYER_PLAYING) return; if (frameStepping) { VideoFrame selected = std::move(presented); const int64_t timestamp = presentationTimestamp(selected.frame.get()); const uint64_t occurrence = selected.timestampOccurrence; const double selectedPts = selected.pts; // Audio was skipped while stepping. Rebuild it once, only on resume. seekVideo(steppingAnchor, selectedPts); videoEnd = selected.end; presentVideo(std::move(selected)); skipPresentedTimestamp = timestamp; skipPresentedOccurrence = occurrence; } if (tracks[0].codec) { std::string deviceError; if (!output.start(deviceError)) throw std::runtime_error(deviceError.empty() ? "Cannot start audio output" : deviceError); } steadyPosition = position; steadyAnchor = Clock::now(); status = PLAYER_PLAYING; } void pause() { if (status != PLAYER_PLAYING) return; position = clockPosition(); output.pause(); if (tracks[0].codec) { const double frozen = output.clock(); if (std::isfinite(frozen)) position = std::max(seekTarget, frozen); } status = PLAYER_PAUSED; } void seek(double seconds, bool resume) { if (!format) return; const double target = duration > 0 ? std::clamp(seconds, 0.0, duration) : std::max(0.0, seconds); const long double absolute = std::round((static_cast(target) + origin) * AV_TIME_BASE); if (absolute < -0x1p63L || absolute >= 0x1p63L) throw std::runtime_error("Seek target is outside the supported timestamp range"); output.pause(); const int64_t timestamp = static_cast(absolute); check(avformat_seek_file(format, -1, std::numeric_limits::min(), timestamp, timestamp, 0), "Seeking media"); resetTimeline(target); status = PLAYER_PAUSED; if (resume) start(); } void seekVideo(int64_t timestamp, double target) { output.pause(); check(av_seek_frame(format, tracks[1].index, timestamp, AVSEEK_FLAG_BACKWARD), "Seeking video keyframe"); // Decoder preroll must not blank the last presented frame between steps. resetTimeline(target, false); status = PLAYER_PAUSED; } void presentVideo(VideoFrame frame) { output.present(frame.frame.get()); presented = std::move(frame); previewShown = true; } VideoFrame nextSteppingFrame() { while (video.empty() && !tracks[1].drained) { if (interrupted()) throw Cancelled{}; if (!decodeOne(true)) { std::unique_lock lock(mutex); wake.wait_for(lock, std::chrono::milliseconds(5), [this] { return interrupted(); }); } } if (video.empty()) return {}; VideoFrame frame = std::move(video.front()); video.pop_front(); videoBytes -= frame.bytes; return frame; } void step(int direction) { if (!format || !tracks[1].codec) return; pause(); status = PLAYER_PAUSED; // A seek followed immediately by a queued step must first establish its preview. while (!presented.frame) { auto frame = nextSteppingFrame(); if (!frame.frame) return; if (frame.pts + sampleTime * 0.5 < seekTarget) continue; presentVideo(std::move(frame)); } if (frameStepping && direction > 0) { // The stepping decoder is already positioned immediately after the displayed frame. auto next = nextSteppingFrame(); if (next.frame) presentVideo(std::move(next)); position = presented.pts; return; } VideoFrame selected = std::move(presented); const int64_t reference = presentationTimestamp(selected.frame.get()); const uint64_t referenceOccurrence = selected.timestampOccurrence; if (reference == AV_NOPTS_VALUE) throw std::runtime_error("Frame stepping requires video presentation timestamps"); const AVStream *stream = format->streams[tracks[1].index]; const long double ticksPerSecond = 1.0L / av_q2d(stream->time_base); const long double originTimestamp = std::clamp(std::floor(origin * ticksPerSecond), static_cast(std::numeric_limits::min() + 1), static_cast(std::numeric_limits::max())); const int64_t beginning = std::min(reference, std::max(static_cast(originTimestamp), stream->start_time == AV_NOPTS_VALUE ? static_cast(originTimestamp) : stream->start_time)); int64_t target = reference > beginning ? reference - 1 : reference; long double backoff = std::max(1.0L, ticksPerSecond); for (;;) { // FFmpeg's decoder emits presentation order, including delayed B-frames at drain. // ponytail: a one-GOP/unindexed file can require a prefix scan; retained frames stay bounded. seekVideo(target, 0); VideoFrame previous{}; bool sawReference = false; for (;;) { auto frame = nextSteppingFrame(); if (!frame.frame) break; const int64_t timestamp = presentationTimestamp(frame.frame.get()); const bool atReference = timestamp == reference && frame.timestampOccurrence == referenceOccurrence; const bool beforeReference = timestamp < reference || (timestamp == reference && frame.timestampOccurrence < referenceOccurrence); if (atReference) sawReference = true; if (direction > 0) { if (!beforeReference && !atReference) { if (sawReference) selected = std::move(frame); break; } } else { if (!beforeReference) { if (previous.frame) { // Preserve the adjacent successor for a subsequent forward step. videoBytes += frame.bytes; video.push_front(std::move(frame)); } break; } if (timestamp >= beginning) previous = std::move(frame); } } if ((direction > 0 && sawReference) || previous.frame || target == beginning) { if (previous.frame) selected = std::move(previous); break; } // Some demuxers land later than requested. Expand the keyframe search, not an FPS guess. target = static_cast(std::max(static_cast(beginning), static_cast(reference) - backoff)); backoff *= 2; } if (interrupted()) throw Cancelled{}; position = selected.pts; steppingAnchor = target; frameStepping = true; presentVideo(std::move(selected)); } void execute(const Command &command) { if (command.epoch != requestedEpoch.load(std::memory_order_relaxed)) return; activeEpoch = command.epoch; switch (command.action) { case Action::Open: open(command.path); break; case Action::Play: start(); break; case Action::Pause: pause(); break; case Action::Seek: seek(command.seconds, status == PLAYER_PLAYING); break; case Action::Step: step(command.direction); break; case Action::Close: release(); break; } publish(); } double framePts(Track &track, const AVFrame *frame) { const int64_t timestamp = presentationTimestamp(frame); if (timestamp == AV_NOPTS_VALUE) return track.nextPts; return timestamp * av_q2d(format->streams[track.index]->time_base) - origin; } void convertAudio() { const bool continuing = resampler != nullptr; auto &track = tracks[0]; const int rate = decoded->sample_rate > 0 ? decoded->sample_rate : track.codec->sample_rate; const AVChannelLayout *layout = decoded->ch_layout.nb_channels > 0 ? &decoded->ch_layout : &track.codec->ch_layout; const auto sampleFormat = static_cast(decoded->format); if (rate <= 0 || layout->nb_channels <= 0 || decoded->nb_samples < 0) throw std::runtime_error("Invalid decoded audio format"); if (!resampler) { AVChannelLayout stereo = AV_CHANNEL_LAYOUT_STEREO; check(swr_alloc_set_opts2(&resampler, &stereo, AV_SAMPLE_FMT_FLT, sampleRate, layout, sampleFormat, rate, 0, nullptr), "Allocating audio resampler"); check(swr_init(resampler), "Initializing audio resampler"); check(av_channel_layout_copy(&inputLayout, layout), "Saving audio channel layout"); inputRate = rate; inputFormat = sampleFormat; } else if (inputRate != rate || inputFormat != sampleFormat || av_channel_layout_compare(&inputLayout, layout) != 0) { throw std::runtime_error("Audio format changes within a track are not supported"); } const double timestamp = framePts(track, decoded); const double tick = av_q2d(format->streams[track.index]->time_base); const bool contiguous = continuing && std::abs(timestamp - track.nextPts) <= tick + 1.0 / rate; // Quantized packet PTS and rounded resampler delay must not insert/drop PCM samples. const int64_t delayBase = static_cast(rate) * sampleRate; pcmPts = contiguous ? resampleNextPts : timestamp - static_cast(swr_get_delay(resampler, delayBase)) / delayBase; track.nextPts = (contiguous ? track.nextPts : timestamp) + static_cast(decoded->nb_samples) / rate; const int capacity = swr_get_out_samples(resampler, decoded->nb_samples); check(capacity, "Sizing resampled audio"); if (capacity > audioFrameLimit) throw std::runtime_error("Decoded audio block exceeds the four-second buffer limit"); const auto samples = static_cast(capacity) * 2; if (pcm.size() < samples) pcm.resize(samples); uint8_t *destination = reinterpret_cast(pcm.data()); const int count = swr_convert(resampler, &destination, capacity, const_cast(decoded->extended_data), decoded->nb_samples); check(count, "Resampling audio"); resampleNextPts = pcmPts + static_cast(count) / sampleRate; pcmFrames = static_cast(count); pcmOffset = 0; trimAudio(); } void trimAudio() { if (pcmPts < seekTarget) { const double skip = std::ceil((seekTarget - pcmPts) * sampleRate - 1e-6); pcmOffset = static_cast(std::min(skip, static_cast(pcmFrames))); } } void drainResampler() { if (!resampler) { resamplerDrained = true; return; } const int capacity = swr_get_out_samples(resampler, 0); check(capacity, "Sizing final audio"); if (capacity > audioFrameLimit) throw std::runtime_error("Resampler drain exceeds the audio buffer limit"); // A nonzero destination also lets a resampler with no delayed samples signal completion. const auto samples = static_cast(std::max(capacity, 32)) * 2; if (pcm.size() < samples) pcm.resize(samples); uint8_t *destination = reinterpret_cast(pcm.data()); const int count = swr_convert(resampler, &destination, std::max(capacity, 32), nullptr, 0); check(count, "Draining audio resampler"); pcmPts = resampleNextPts; resampleNextPts += static_cast(count) / sampleRate; pcmFrames = static_cast(count); pcmOffset = 0; resamplerDrained = count == 0; trimAudio(); } bool writeSilence(double end) { if (end <= audioWriteEnd + sampleTime * 0.5) return false; static constexpr std::array silence{}; const double remaining = std::round((end - audioWriteEnd) * sampleRate); const std::size_t frames = static_cast(std::min(remaining, 2048.0)); if (frames == 0) return false; const std::size_t accepted = output.write(silence.data(), frames, audioWriteEnd); audioWriteEnd += static_cast(accepted) / sampleRate; return accepted != 0; } bool pumpAudio() { if (pcmOffset >= pcmFrames) return false; const double start = pcmPts + static_cast(pcmOffset) / sampleRate; if (start > audioWriteEnd + sampleTime * 0.5) return writeSilence(start); if (start < audioWriteEnd - sampleTime * 0.5) { const double overlap = std::round((audioWriteEnd - start) * sampleRate); const auto skip = static_cast(std::min(overlap, static_cast(pcmFrames - pcmOffset))); pcmOffset += skip; if (pcmOffset >= pcmFrames) return true; } const std::size_t accepted = output.write(pcm.data() + pcmOffset * 2, pcmFrames - pcmOffset, audioWriteEnd); pcmOffset += accepted; audioWriteEnd += static_cast(accepted) / sampleRate; return accepted != 0; } bool videoFull() const { return video.size() >= videoFrameLimit || (!video.empty() && videoBytes > videoByteLimit - video.back().bytes); } void queueVideo(bool stepping = false) { auto &track = tracks[1]; const auto transfer = decoded->color_trc == AVCOL_TRC_UNSPECIFIED ? track.codec->color_trc : decoded->color_trc; if (transfer == AVCOL_TRC_SMPTE2084 || transfer == AVCOL_TRC_ARIB_STD_B67) throw std::runtime_error("HDR PQ/HLG video is unsupported: the native view currently supports SDR only"); const int64_t timestamp = presentationTimestamp(decoded); if (stepping && timestamp == AV_NOPTS_VALUE) throw std::runtime_error("Frame stepping requires video presentation timestamps"); const double pts = framePts(track, decoded); const double frameDuration = decoded->duration > 0 ? decoded->duration * av_q2d(format->streams[track.index]->time_base) : videoFrameTime; track.nextPts = pts + frameDuration; if (decoded->flags & AV_FRAME_FLAG_DISCARD) return; if (timestamp != track.countedTimestamp) { track.countedTimestamp = timestamp; track.timestampOccurrence = 0; } else { ++track.timestampOccurrence; } if (!stepping && pts + sampleTime * 0.5 < seekTarget) return; if (!stepping && skipPresentedTimestamp != AV_NOPTS_VALUE) { if (timestamp != AV_NOPTS_VALUE && (timestamp < skipPresentedTimestamp || (timestamp == skipPresentedTimestamp && track.timestampOccurrence <= skipPresentedOccurrence))) return; skipPresentedTimestamp = AV_NOPTS_VALUE; } const int size = av_image_get_buffer_size(static_cast(decoded->format), decoded->width, decoded->height, 1); check(size, "Sizing decoded video"); const auto bytes = static_cast(size); if (bytes > videoByteLimit) throw std::runtime_error("A decoded video frame exceeds the 128 MiB buffer limit"); videoEnd = std::max(videoEnd, pts + frameDuration); // ponytail: extreme interleaving can starve audio behind a full video queue. // Keep nearest frames plus the newest; packet spooling is the upgrade if that matters. while (!video.empty() && (video.size() >= videoFrameLimit || videoBytes + bytes > videoByteLimit)) { videoBytes -= video.back().bytes; video.pop_back(); } Frame frame(av_frame_clone(decoded)); if (!frame) throw std::bad_alloc(); VideoFrame item{std::move(frame), pts, pts + frameDuration, bytes, track.timestampOccurrence}; auto insertion = std::upper_bound(video.begin(), video.end(), pts, [](double value, const VideoFrame &entry) { return value < entry.pts; }); video.insert(insertion, std::move(item)); videoBytes += bytes; } bool decodeOne(bool stepping = false) { if (!stepping && pcmOffset < pcmFrames) return false; if (receiving >= 0) { if (stepping && receiving == 0) { receiving = -1; return true; } auto &track = tracks[receiving]; if (receiving == 1 && videoFull() && (!tracks[0].codec || output.queued() >= 1024 || status != PLAYER_PLAYING)) return false; const int result = avcodec_receive_frame(track.codec, decoded); if (result == AVERROR(EAGAIN)) { receiving = -1; return true; } if (result == AVERROR_EOF) { track.drained = true; receiving = -1; return true; } check(result, "Decoding media"); if (receiving == 0) convertAudio(); else queueVideo(stepping); av_frame_unref(decoded); return true; } if (packetPending) { if (stepping && packet->stream_index == tracks[0].index) { av_packet_unref(packet); packetPending = false; return true; } const int slot = packet->stream_index == tracks[0].index ? 0 : 1; const int result = avcodec_send_packet(tracks[slot].codec, packet); if (result == AVERROR(EAGAIN)) { receiving = slot; return true; } check(result, "Sending media packet to decoder"); av_packet_unref(packet); packetPending = false; receiving = slot; return true; } if (demuxEnded) { for (int slot = 0; slot < 2; ++slot) { auto &track = tracks[slot]; if (!track.codec || track.drainSent || (stepping && slot == 0)) continue; const int result = avcodec_send_packet(track.codec, nullptr); if (result == AVERROR(EAGAIN)) { receiving = slot; return true; } if (result != AVERROR_EOF) check(result, "Draining decoder"); track.drainSent = true; receiving = slot; return true; } if (!stepping && (!tracks[0].codec || tracks[0].drained) && !resamplerDrained) { drainResampler(); return true; } return false; } const int result = av_read_frame(format, packet); if (result == AVERROR_EOF) { demuxEnded = true; return true; } if (result == AVERROR(EAGAIN)) return false; check(result, "Reading media packet"); if ((packet->stream_index != tracks[0].index && packet->stream_index != tracks[1].index) || (stepping && packet->stream_index == tracks[0].index)) { av_packet_unref(packet); } else { packetPending = true; } return true; } void renderVideo(double now) { VideoFrame latest{}; while (!video.empty() && video.front().pts <= now + 0.002) { videoBytes -= video.front().bytes; latest = std::move(video.front()); video.pop_front(); } if (latest.frame) presentVideo(std::move(latest)); } bool tick() { if (!format || interrupted() || status == PLAYER_ENDED || status == PLAYER_ERROR) return false; if (frameStepping) return false; bool progressed = pumpAudio(); if (status == PLAYER_PAUSED) { if (!previewShown && !video.empty()) { auto frame = std::move(video.front()); videoBytes -= frame.bytes; video.pop_front(); presentVideo(std::move(frame)); progressed = true; } const bool needPreview = tracks[1].codec && !previewShown; const bool needAudio = tracks[0].codec && output.queued() < 4800; if (needPreview || needAudio) progressed = decodeOne() || progressed; return progressed; } position = clockPosition(); renderVideo(position); progressed = decodeOne() || progressed; if (demuxEnded && (!tracks[0].codec || tracks[0].drained) && resamplerDrained && pcmOffset >= pcmFrames && tracks[0].codec) progressed = writeSilence(videoEnd) || progressed; const bool drained = demuxEnded && resamplerDrained && pcmOffset >= pcmFrames && (!tracks[0].codec || tracks[0].drained) && (!tracks[1].codec || tracks[1].drained); if (drained && video.empty()) { const double end = std::max(audioWriteEnd, videoEnd); if (position + sampleTime >= end && (!tracks[0].codec || output.queued() == 0)) { output.pause(); position = end; if (duration <= 0) duration = end; status = PLAYER_ENDED; } } publish(); return progressed; } void run() noexcept { while (!stopping.load(std::memory_order_relaxed)) { try { Command command{Action::Pause, {}}; bool haveCommand = false; std::array apiFailure{}; bool haveFailure = false; uint64_t failureEpoch = 0; { std::lock_guard lock(mutex); if (errorRequested) { apiFailure = requestedError; failureEpoch = errorEpoch; errorRequested = false; haveFailure = true; } else if (!commands.empty()) { auto &next = commands.front(); if (next.action == Action::Step && next.repetitions > 1 && next.epoch == requestedEpoch.load(std::memory_order_relaxed)) { command = {Action::Step, {}, 0, next.direction, next.epoch}; --next.repetitions; } else { command = std::move(next); commands.pop_front(); } haveCommand = true; } } if (haveFailure) { if (failureEpoch == requestedEpoch.load(std::memory_order_relaxed)) { activeEpoch = failureEpoch; fail(apiFailure.data()); } continue; } if (haveCommand) { execute(command); continue; } if (tick()) continue; std::unique_lock lock(mutex); wake.wait_for(lock, std::chrono::milliseconds(status == PLAYER_PLAYING ? 5 : 20), [this] { return stopping.load(std::memory_order_relaxed) || errorRequested || !commands.empty(); }); } catch (const Cancelled &) { // An accepted open/close or destruction superseded the current I/O. } catch (const std::exception &exception) { try { fail(exception.what()); } catch (...) {} } catch (...) { try { fail("Unexpected playback failure"); } catch (...) {} } } try { release(); } catch (...) {} } }; namespace { template bool submit(Player *player, Function function) noexcept { if (!player) return false; try { return function(); } catch (const std::exception &exception) { player->apiError(exception.what()); } catch (...) { player->apiError("Unexpected command failure"); } return false; } } extern "C" { Player *player_create(void) { try { return new Player; } catch (...) { return nullptr; } } bool player_open(Player *player, const char *path) { if (!path || !*path) return false; return submit(player, [=] { return player->enqueue({Action::Open, path}); }); } bool player_play(Player *player) { return submit(player, [=] { return player->enqueue({Action::Play, {}}); }); } bool player_pause(Player *player) { return submit(player, [=] { return player->enqueue({Action::Pause, {}}); }); } bool player_seek(Player *player, double seconds) { if (!std::isfinite(seconds)) return false; return submit(player, [=] { return player->enqueue({Action::Seek, {}, seconds}); }); } bool player_step_frame(Player *player, int direction) { if (direction != -1 && direction != 1) return false; return submit(player, [=] { return player->enqueue({Action::Step, {}, 0, direction}); }); } bool player_close(Player *player) { return submit(player, [=] { return player->enqueue({Action::Close, {}}); }); } bool player_set_volume(Player *player, float normalized0to1) { if (!std::isfinite(normalized0to1) || normalized0to1 < 0 || normalized0to1 > 1) return false; return submit(player, [=] { player->output.set_volume(normalized0to1); return true; }); } float player_get_volume(Player *player) { return player ? player->output.volume() : 0; } PlayerState player_get_state(Player *player) { try { if (player) { std::lock_guard lock(player->mutex); return player->state; } } catch (const std::exception &exception) { PlayerState result{}; result.status = PLAYER_ERROR; copyError(result.error, exception.what()); return result; } catch (...) { PlayerState result{}; result.status = PLAYER_ERROR; copyError(result.error, "Cannot read playback state"); return result; } PlayerState result{}; result.status = PLAYER_ERROR; copyError(result.error, "Invalid player handle"); return result; } void *player_macos_make_video_view(Player *player) { if (!player) return nullptr; try { return player->output.make_video_view(); } catch (const std::exception &exception) { player->apiError(exception.what()); } catch (...) { player->apiError("Cannot create the native video view"); } return nullptr; } void player_destroy(Player *player) { try { delete player; } catch (...) {} } }