Files
terebi/Sources/Core/player.cpp
T

1013 lines
42 KiB
C++

#include "player.h"
#include "player_macos.h"
#include "output.hpp"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/channel_layout.h>
#include <libavutil/error.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>
}
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include <cstdio>
#include <cstring>
#include <deque>
#include <filesystem>
#include <limits>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
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<AVFrame, FrameDeleter>;
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<Command> commands;
PlayerState state{};
uint64_t publishedEpoch = 0;
std::array<char, 512> requestedError{};
bool errorRequested = false;
uint64_t errorEpoch = 0;
std::atomic<bool> stopping{false};
std::atomic<uint64_t> requestedEpoch{0};
uint64_t activeEpoch = 0;
// Everything below, including FFmpeg and Output audio operations, is worker-owned.
AVFormatContext *format = nullptr;
std::array<Track, 2> 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<VideoFrame> 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<float> 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<char, 512> 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<std::mutex> 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<uint64_t>::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<std::mutex> 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<Player *>(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<std::mutex> 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<char, 512> 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<int>(i) != tracks[0].index && static_cast<int>(i) != tracks[1].index)
format->streams[i]->discard = AVDISCARD_ALL;
}
if (format->start_time != AV_NOPTS_VALUE) {
origin = static_cast<double>(format->start_time) / AV_TIME_BASE;
} else {
double first = std::numeric_limits<double>::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<double>(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<double>(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<long double>(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<int64_t>(absolute);
check(avformat_seek_file(format, -1, std::numeric_limits<int64_t>::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<std::mutex> 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<long double>(std::numeric_limits<int64_t>::min() + 1),
static_cast<long double>(std::numeric_limits<int64_t>::max()));
const int64_t beginning = std::min(reference, std::max(static_cast<int64_t>(originTimestamp),
stream->start_time == AV_NOPTS_VALUE ? static_cast<int64_t>(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<int64_t>(std::max(static_cast<long double>(beginning),
static_cast<long double>(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<AVSampleFormat>(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<int64_t>(rate) * sampleRate;
pcmPts = contiguous ? resampleNextPts :
timestamp - static_cast<double>(swr_get_delay(resampler, delayBase)) / delayBase;
track.nextPts = (contiguous ? track.nextPts : timestamp) + static_cast<double>(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<std::size_t>(capacity) * 2;
if (pcm.size() < samples) pcm.resize(samples);
uint8_t *destination = reinterpret_cast<uint8_t *>(pcm.data());
const int count = swr_convert(resampler, &destination, capacity,
const_cast<const uint8_t **>(decoded->extended_data), decoded->nb_samples);
check(count, "Resampling audio");
resampleNextPts = pcmPts + static_cast<double>(count) / sampleRate;
pcmFrames = static_cast<std::size_t>(count);
pcmOffset = 0;
trimAudio();
}
void trimAudio() {
if (pcmPts < seekTarget) {
const double skip = std::ceil((seekTarget - pcmPts) * sampleRate - 1e-6);
pcmOffset = static_cast<std::size_t>(std::min(skip, static_cast<double>(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::size_t>(std::max(capacity, 32)) * 2;
if (pcm.size() < samples) pcm.resize(samples);
uint8_t *destination = reinterpret_cast<uint8_t *>(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<double>(count) / sampleRate;
pcmFrames = static_cast<std::size_t>(count);
pcmOffset = 0;
resamplerDrained = count == 0;
trimAudio();
}
bool writeSilence(double end) {
if (end <= audioWriteEnd + sampleTime * 0.5) return false;
static constexpr std::array<float, 4096> silence{};
const double remaining = std::round((end - audioWriteEnd) * sampleRate);
const std::size_t frames = static_cast<std::size_t>(std::min(remaining, 2048.0));
if (frames == 0) return false;
const std::size_t accepted = output.write(silence.data(), frames, audioWriteEnd);
audioWriteEnd += static_cast<double>(accepted) / sampleRate;
return accepted != 0;
}
bool pumpAudio() {
if (pcmOffset >= pcmFrames) return false;
const double start = pcmPts + static_cast<double>(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::size_t>(std::min(overlap, static_cast<double>(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<double>(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<AVPixelFormat>(decoded->format),
decoded->width, decoded->height, 1);
check(size, "Sizing decoded video");
const auto bytes = static_cast<std::size_t>(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<char, 512> apiFailure{};
bool haveFailure = false;
uint64_t failureEpoch = 0;
{
std::lock_guard<std::mutex> 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<std::mutex> 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 <typename Function>
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<std::mutex> 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 (...) {}
}
}