// SPDX-License-Identifier: GPL-3.0-or-later // Copyright (C) 2026 steredbits and Pelagia contributors #include "player/player.h" #include #include "util/log.h" namespace player { namespace { uint64_t default_time_fn(void*) { const auto now = std::chrono::steady_clock::now().time_since_epoch(); return static_cast( std::chrono::duration_cast(now).count()); } double chrono_now_ms() { const auto now = std::chrono::steady_clock::now().time_since_epoch(); return std::chrono::duration(now).count(); } void sleep_ms_portable(unsigned ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); } void release_packet_entry(BoundedQueue::Entry* e) { if (e->item) { av_packet_free(&e->item); } } // Target audio buffer on the device side (latency vs robustness). constexpr int64_t kAudioTargetMs = 250; // Maximum time allowed for the end of the audio to drain before the Ended state. constexpr uint64_t kEndDrainTimeoutMs = 2000; // Audio track declared but with no packet: after this delay following the // first frame, the clock locks on the video (otherwise the picture stays frozen // waiting for a sound that never comes). constexpr uint64_t kAudioClockGraceMs = 3000; } // namespace static bool audio_params_differ(const AVCodecParameters* a, const AVCodecParameters* b) { return a->codec_id != b->codec_id || a->sample_rate != b->sample_rate || a->ch_layout.nb_channels != b->ch_layout.nb_channels; } // --- Opening / life cycle --------------------------------------------------- Player::~Player() { stop(); avcodec_parameters_free(&audio_params_); avcodec_parameters_free(&video_params_); avcodec_parameters_free(&audio_pending_); avcodec_parameters_free(&video_pending_); } bool Player::open(MediaSource* source, const Callbacks& callbacks) { source_ = source; cb_ = callbacks; if (!cb_.time_fn) { cb_.time_fn = default_time_fn; cb_.time_ctx = nullptr; } // Before open(): a network opening (up to several tens of // seconds of transcoding) must stay interruptible. source_->set_interrupt(&Player::source_interrupt, this); if (!source_->open() || abort_requested_) { return false; // abort requested during the opening, even if it barely succeeded } AVFormatContext* ctx = source_->format_context(); int idx = av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); video_stream_ = idx >= 0 ? idx : -1; idx = av_find_best_stream(ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0); audio_stream_ = idx >= 0 ? idx : -1; if (video_stream_ >= 0 && !vdec_.open(ctx->streams[video_stream_])) { video_stream_ = -1; } if (audio_stream_ >= 0 && !adec_.open(ctx->streams[audio_stream_], cb_.audio_sample_rate, cb_.audio_channels)) { audio_stream_ = -1; } if (video_stream_ < 0 && audio_stream_ < 0) { LOG_ERROR("Player: no usable audio or video stream"); return false; } // Reference parameters of the decoders, to detect a stream change. { AVRational tb{1, 1000}, fr{0, 1}; if (video_stream_ >= 0) { video_params_ = avcodec_parameters_alloc(); if (!source_->stream_info(AVMEDIA_TYPE_VIDEO, video_params_, &tb, &fr)) { avcodec_parameters_free(&video_params_); } } if (audio_stream_ >= 0) { audio_params_ = avcodec_parameters_alloc(); if (!source_->stream_info(AVMEDIA_TYPE_AUDIO, audio_params_, &tb, &fr)) { avcodec_parameters_free(&audio_params_); } } } if (video_stream_ >= 0) { video_tb_ = ctx->streams[video_stream_]->time_base; } if (audio_stream_ >= 0) { audio_tb_ = ctx->streams[audio_stream_]->time_base; } if (cb_.deep_buffer) { // ~20 s at 8 Mbit/s: network jitter is absorbed without a shortage. video_pkts_.set_limits(2000, 24u * 1024 * 1024); audio_pkts_.set_limits(4000, 4u * 1024 * 1024); } gate_.set_policy(cb_.buffering); gate_.reset_for_start(); buffering_ = gate_.buffering(); buffering_since_ = mono_ms(); video_eof_ = (video_stream_ < 0); audio_eof_ = (audio_stream_ < 0); tracker_.init(source_->timestamps_absolute(), -source_->start_time_ms()); tracker_.on_seek(cb_.start_position_ms); if (cb_.start_position_ms > 0) { last_seek_target_ = cb_.start_position_ms; } last_position_ms_ = cb_.start_position_ms; clock_.init(cb_.time_fn, cb_.time_ctx); consumed_serial_ = serial_.load(); LOG_INFO("Player: duration %lld ms, video=%d, audio=%d", static_cast(source_->duration_ms()), video_stream_ >= 0, audio_stream_ >= 0); return true; } bool Player::start() { if (!source_) { return false; } if (started_.exchange(true)) { return true; } quit_ = false; state_ = PlayerState::Playing; failure_ = FailureKind::None; data_wait_since_ = mono_ms(); if (cb_.audio) { cb_.audio->pause(false); } demux_thread_ = std::thread(&Player::demux_loop, this); if (video_stream_ >= 0) { video_thread_ = std::thread(&Player::video_loop, this); } if (audio_stream_ >= 0) { audio_thread_ = std::thread(&Player::audio_loop, this); } return true; } void Player::stop() { if (!started_.exchange(false)) { return; } quit_ = true; video_pkts_.abort(); audio_pkts_.abort(); video_frames_.abort(); audio_frames_.abort(); if (demux_thread_.joinable()) { demux_thread_.join(); } if (video_thread_.joinable()) { video_thread_.join(); } if (audio_thread_.joinable()) { audio_thread_.join(); } drain_queues(); state_ = PlayerState::Stopped; } void Player::drain_queues() { video_pkts_.flush(release_packet_entry); audio_pkts_.flush(release_packet_entry); video_frames_.flush([](BoundedQueue::Entry* e) { if (e->item.frame) { av_frame_free(&e->item.frame); } }); audio_frames_.flush([](BoundedQueue::Entry* e) { if (e->item.chunk.data) { av_free(e->item.chunk.data); } }); drop_pending_items(); } // --- Demux thread ------------------------------------------------------------- void Player::demux_loop() { while (!quit_) { bool do_seek = false; int64_t target = 0; { std::lock_guard lock(seek_mutex_); if (seek_pending_) { seek_pending_ = false; seek_flag_ = false; target = seek_target_ms_; do_seek = true; } } if (do_seek) { perform_seek(target, false); continue; } if (demux_eof_sent_) { sleep_ms_portable(10); continue; } AVPacket* pkt = av_packet_alloc(); const int err = source_->read(pkt); const int serial = serial_.load(); if (err < 0) { av_packet_free(&pkt); if (err == AVERROR(EAGAIN)) { sleep_ms_portable(5); continue; } if (err == AVERROR_EXIT) { // Read interrupted by a stop or a seek: handled at the top of the // loop. Never an empty loop if the interruption is not followed. if (!seek_flag_ && !quit_) { sleep_ms_portable(5); } continue; } if (err != AVERROR_EOF && source_->recoverable() && cb_.recovery.enabled()) { if (!recover_source()) { LOG_ERROR("Player: stream lost, %d recovery attempts without success", cb_.recovery.max_attempts); fail(FailureKind::Network); demux_eof_sent_ = true; // nothing left to read; a seek will restart } continue; } if (err != AVERROR_EOF) { LOG_WARN("Player: source read error (err=%d)", err); } push_eof_markers(serial); demux_eof_sent_ = true; continue; } if (pkt->stream_index == video_stream_) { push_packet(&video_pkts_, pkt, serial, packet_duration_ms(pkt, video_tb_, &video_prev_dts_ms_)); } else if (pkt->stream_index == audio_stream_) { push_packet(&audio_pkts_, pkt, serial, packet_duration_ms(pkt, audio_tb_, &audio_prev_dts_ms_)); } else { av_packet_free(&pkt); } } } // Non-blocking push in a loop: if a seek arrives while the queues are // full, the packet is dropped (it belongs to the stale generation) and // the demux thread stays free to process the seek. A blocking push here // would be a deadlock: the queue flush happens in this same thread. int64_t Player::packet_duration_ms(const AVPacket* pkt, AVRational time_base, int64_t* prev_dts_ms) { const AVRational ms = {1, 1000}; int64_t duration = 0; if (pkt->duration > 0) { duration = av_rescale_q(pkt->duration, time_base, ms); } if (pkt->dts != AV_NOPTS_VALUE) { const int64_t dts = av_rescale_q(pkt->dts, time_base, ms); if (duration <= 0 && *prev_dts_ms >= 0 && dts > *prev_dts_ms) { duration = dts - *prev_dts_ms; } *prev_dts_ms = dts; } return duration; } bool Player::push_packet(BoundedQueue* queue, AVPacket* pkt, int serial, int64_t duration_ms) { while (!quit_) { if (queue->try_push(pkt, static_cast(pkt->size), serial, duration_ms)) { return true; } { std::lock_guard lock(seek_mutex_); if (seek_pending_) { break; } } sleep_ms_portable(5); } av_packet_free(&pkt); return false; } void Player::push_eof_markers(int serial) { // Drain marker (null packet) for each active decoder. bool video_sent = (video_stream_ < 0); bool audio_sent = (audio_stream_ < 0); while (!quit_ && (!video_sent || !audio_sent)) { if (!video_sent && video_pkts_.try_push(nullptr, 0, serial)) { video_sent = true; } if (!audio_sent && audio_pkts_.try_push(nullptr, 0, serial)) { audio_sent = true; } if (video_sent && audio_sent) { break; } { std::lock_guard lock(seek_mutex_); if (seek_pending_) { break; // the generation changes, the markers would be dropped } } sleep_ms_portable(5); } } bool Player::wait_demux(int ms) { for (int waited = 0; waited < ms; waited += 10) { if (quit_ || seek_flag_) { return true; } sleep_ms_portable(10); } return quit_ || seek_flag_; } // Network drop: a few spaced attempts, each reopening the stream at // the current playback position (playback continues on the reserve // while waiting). True if the stream is restored, or if a seek / the stop // takes over. bool Player::recover_source() { const RetryPolicy& policy = cb_.recovery; for (int attempt = 1; attempt <= policy.max_attempts; ++attempt) { const int delay = policy.delay_ms(attempt); LOG_WARN("Player: network drop, attempt %d/%d in %d ms", attempt, policy.max_attempts, delay); if (wait_demux(delay)) { return true; } const int64_t target = clamp_seek_target(last_position_ms_.load(), duration_ms()); if (perform_seek(target, true)) { stat_reconnections_.fetch_add(1); LOG_INFO("Player: stream restored at %lld ms (attempt %d)", static_cast(target), attempt); return true; } if (quit_ || seek_flag_) { return true; } } return false; } // recovery: restart decided by the demux (drop) and not by the // user; the consumer is notified by restart_pending_. bool Player::perform_seek(int64_t target_ms, bool recovery) { // New generation first: the decoders now drop any // packet/frame carrying the old serial, flush or not. const int new_serial = serial_.fetch_add(1) + 1; if (recovery) { last_seek_target_ = target_ms; min_valid_serial_ = new_serial; restart_pending_ = true; } { std::lock_guard lock(seek_mutex_); tracker_.on_seek(target_ms); } video_pkts_.flush(release_packet_entry); audio_pkts_.flush(release_packet_entry); video_frames_.flush([](BoundedQueue::Entry* e) { if (e->item.frame) { av_frame_free(&e->item.frame); } }); audio_frames_.flush([](BoundedQueue::Entry* e) { if (e->item.chunk.data) { av_free(e->item.chunk.data); } }); // Before the network reopening (several seconds): an end of stream // from before the seek must not pass for "source exhausted". video_eof_ = (video_stream_ < 0); audio_eof_ = (audio_stream_ < 0); demux_eof_sent_ = false; video_prev_dts_ms_ = -1; audio_prev_dts_ms_ = -1; if (!source_->seek(target_ms)) { if (seek_flag_ || quit_) { // A more recent seek (or the stop) interrupted this one: only the // last one counts, it will be handled on the next turn. LOG_DEBUG("Player: seek to %lld ms replaced", static_cast(target_ms)); return false; } if (!recovery) { LOG_WARN("Player: seek to %lld ms failed", static_cast(target_ms)); } return false; } note_stream_change(); // before the first packet of the new generation force_present_ = true; // when paused, present the first frame of the seek data_wait_since_ = mono_ms(); // stream reopened: the data timeout is running PlayerState expected = PlayerState::Ended; state_.compare_exchange_strong(expected, PlayerState::Playing); expected = PlayerState::Failed; if (state_.compare_exchange_strong(expected, PlayerState::Playing)) { failure_ = FailureKind::None; } LOG_DEBUG("Player: seek -> %lld ms (generation %d)", static_cast(target_ms), serial_.load()); return true; } // Demux thread, after the source reopening: does the new stream (other audio // track) have other parameters than the decoders'? If so they // are published for the decoding threads. Incomplete parameters (the probe // did not find the rate or the channels) replace nothing: the decoder // in place is kept, like when resuming a stream without audio parameters. void Player::note_stream_change() { AVCodecParameters* par = avcodec_parameters_alloc(); AVRational tb{1, 1000}, fr{0, 1}; if (audio_params_ && source_->stream_info(AVMEDIA_TYPE_AUDIO, par, &tb, &fr) && par->sample_rate > 0 && par->ch_layout.nb_channels > 0 && audio_params_differ(audio_params_, par)) { LOG_INFO("Player: new audio track (%s %d Hz %d channels -> %s %d Hz %d channels)", avcodec_get_name(audio_params_->codec_id), audio_params_->sample_rate, audio_params_->ch_layout.nb_channels, avcodec_get_name(par->codec_id), par->sample_rate, par->ch_layout.nb_channels); { std::lock_guard lock(codec_mutex_); if (!audio_pending_) { audio_pending_ = avcodec_parameters_alloc(); } avcodec_parameters_copy(audio_pending_, par); audio_pending_tb_ = tb; ++audio_codec_version_; } avcodec_parameters_copy(audio_params_, par); audio_tb_ = tb; } if (video_params_ && source_->stream_info(AVMEDIA_TYPE_VIDEO, par, &tb, &fr) && par->codec_id != video_params_->codec_id) { LOG_INFO("Player: new video codec (%s -> %s)", avcodec_get_name(video_params_->codec_id), avcodec_get_name(par->codec_id)); { std::lock_guard lock(codec_mutex_); if (!video_pending_) { video_pending_ = avcodec_parameters_alloc(); } avcodec_parameters_copy(video_pending_, par); video_pending_tb_ = tb; video_pending_fr_ = fr; ++video_codec_version_; } avcodec_parameters_copy(video_params_, par); video_tb_ = tb; } avcodec_parameters_free(&par); } // Audio thread, at the first packet of a generation: applies the parameters // published by the demux. False if nothing changed (a simple flush afterwards). bool Player::reopen_audio_decoder() { AVCodecParameters* par = avcodec_parameters_alloc(); AVRational tb{1, 1000}; { std::lock_guard lock(codec_mutex_); if (audio_codec_version_ == audio_codec_applied_ || !audio_pending_) { avcodec_parameters_free(&par); return false; } audio_codec_applied_ = audio_codec_version_; avcodec_parameters_copy(par, audio_pending_); tb = audio_pending_tb_; } adec_.close(); const bool ok = adec_.open(par, tb, cb_.audio_sample_rate, cb_.audio_channels); audio_decoder_ok_ = ok; if (ok) { stat_codec_reopens_.fetch_add(1); } else { LOG_ERROR("Player: cannot open the audio decoder of the new track: playing without sound"); } avcodec_parameters_free(&par); return true; } bool Player::reopen_video_decoder() { AVCodecParameters* par = avcodec_parameters_alloc(); AVRational tb{1, 1000}, fr{0, 1}; { std::lock_guard lock(codec_mutex_); if (video_codec_version_ == video_codec_applied_ || !video_pending_) { avcodec_parameters_free(&par); return false; } video_codec_applied_ = video_codec_version_; avcodec_parameters_copy(par, video_pending_); tb = video_pending_tb_; fr = video_pending_fr_; } vdec_.close(); const bool ok = vdec_.open(par, tb, fr); video_decoder_ok_ = ok; if (ok) { stat_codec_reopens_.fetch_add(1); } else { LOG_ERROR("Player: cannot open the video decoder of the new stream"); } avcodec_parameters_free(&par); return true; } bool Player::source_interrupt(void* ctx) { const Player* self = static_cast(ctx); // abort_open() only concerns the opening: once started, a flag // left raised would interrupt each read. return self->quit_.load() || self->seek_flag_.load() || (!self->started_.load() && self->abort_requested_.load()); } // --- Decoding threads --------------------------------------------------------- void Player::video_loop() { int cur_serial = -1; while (true) { BoundedQueue::Entry e; if (!video_pkts_.pop(&e)) { return; // abort (stop) } AVPacket* pkt = e.item; if (e.serial != serial_.load()) { if (pkt) { av_packet_free(&pkt); } continue; } if (e.serial != cur_serial) { if (!reopen_video_decoder()) { vdec_.flush(); } cur_serial = e.serial; } if (!video_decoder_ok_) { if (!pkt) { // end marker: playback must be able to finish VideoItem eof_item; eof_item.eof = true; video_frames_.push(eof_item, 0, e.serial); } else { av_packet_free(&pkt); } continue; } if (vdec_.send(pkt) < 0 && pkt) { LOG_WARN("Player: video packet rejected by the decoder"); av_packet_free(&pkt); continue; } if (pkt) { av_packet_free(&pkt); } int status = 0; while (AVFrame* frame = vdec_.receive(&status)) { VideoItem item; item.frame = frame; item.pts_ms = vdec_.frame_pts_ms(frame); stat_video_frames_.fetch_add(1); const size_t bytes = static_cast(frame->width) * frame->height * 3 / 2; if (!video_frames_.push(item, bytes, e.serial, vdec_.frame_duration_ms())) { av_frame_free(&frame); return; // abort } } if (status == AVERROR_EOF) { VideoItem eof_item; eof_item.eof = true; video_frames_.push(eof_item, 0, e.serial); vdec_.flush(); // ready for the next generation } } } void Player::audio_loop() { int cur_serial = -1; while (true) { BoundedQueue::Entry e; if (!audio_pkts_.pop(&e)) { return; } AVPacket* pkt = e.item; if (e.serial != serial_.load()) { if (pkt) { av_packet_free(&pkt); } continue; } if (e.serial != cur_serial) { if (!reopen_audio_decoder()) { adec_.flush(); } cur_serial = e.serial; } if (!audio_decoder_ok_) { if (!pkt) { // end marker: playback must be able to finish AudioItem eof_item; eof_item.eof = true; audio_frames_.push(eof_item, 0, e.serial); } else { av_packet_free(&pkt); } continue; } if (adec_.send(pkt) < 0 && pkt) { LOG_WARN("Player: audio packet rejected by the decoder"); av_packet_free(&pkt); continue; } if (pkt) { av_packet_free(&pkt); } int status = 0; AudioChunk chunk; while (adec_.receive(&chunk, &status)) { AudioItem item; item.chunk = chunk; stat_audio_frames_.fetch_add(1); if (!audio_frames_.push(item, chunk.bytes, e.serial, chunk.duration_ms)) { av_free(chunk.data); return; } chunk = AudioChunk{}; } if (status == AVERROR_EOF) { AudioItem eof_item; eof_item.eof = true; audio_frames_.push(eof_item, 0, e.serial); adec_.flush(); } } } // --- Consommation (tick / headless) ------------------------------------------ // tick(), set_paused(), seek_*() and position_ms() are called from a single // thread (the main loop): the clock and the consumer state are not // shared beyond it. int64_t Player::tracker_media_time(int64_t stream_pts_ms) { std::lock_guard lock(seek_mutex_); return tracker_.media_time(stream_pts_ms); } bool Player::is_stale(int serial) const { return serial != serial_.load() || serial < min_valid_serial_.load(); } void Player::drop_pending_items() { if (has_pending_video_ && pending_video_.frame) { av_frame_free(&pending_video_.frame); } has_pending_video_ = false; pending_video_ = VideoItem{}; if (has_pending_audio_ && pending_audio_.chunk.data) { av_free(pending_audio_.chunk.data); } has_pending_audio_ = false; pending_audio_ = AudioItem{}; } void Player::note_serial_change(int serial) { if (serial != consumed_serial_) { consumed_serial_ = serial; clock_.invalidate(); first_video_shown_ = false; audio_wait_since_ = 0; audio_clock_waived_ = false; end_wait_start_ = 0; } } // True while waiting for audio to set the clock. bool Player::audio_clock_expected() const { return has_audio() && !audio_eof_.load() && !audio_clock_waived_; } void Player::fail(FailureKind kind) { failure_ = kind; state_ = PlayerState::Failed; } uint64_t Player::mono_ms() const { return cb_.time_fn ? cb_.time_fn(cb_.time_ctx) : default_time_fn(nullptr); } size_t Player::audio_bytes_per_second() const { return static_cast(cb_.audio_sample_rate) * cb_.audio_channels * sizeof(int16_t); } bool Player::pump_audio() { bool progressed = false; const size_t target_bytes = audio_bytes_per_second() * kAudioTargetMs / 1000; while (true) { if (!has_pending_audio_) { BoundedQueue::Entry e; if (!audio_frames_.try_pop(&e)) { break; } if (is_stale(e.serial)) { if (e.item.chunk.data) { av_free(e.item.chunk.data); } continue; } note_serial_change(e.serial); pending_audio_ = e.item; has_pending_audio_ = true; } if (pending_audio_.eof) { audio_eof_ = true; has_pending_audio_ = false; pending_audio_ = AudioItem{}; break; } const int64_t media_pts = tracker_media_time(pending_audio_.chunk.pts_ms); if (cb_.audio) { if (cb_.audio->queued_bytes() >= target_bytes) { break; } cb_.audio->queue(pending_audio_.chunk.data, pending_audio_.chunk.bytes); const size_t queued_after = cb_.audio->queued_bytes(); const int64_t queued_ms = static_cast(queued_after) * 1000 / static_cast(audio_bytes_per_second()); clock_.set(media_pts + pending_audio_.chunk.duration_ms - queued_ms); } else { // No audio output: chunks are released at the pace of their PTS // so that the clock keeps a realistic pace (tests, debugging). if (!clock_.valid()) { clock_.set(media_pts); } else if (clock_.now() < media_pts) { break; // too early to consume this chunk } else { clock_.set(media_pts); } } av_free(pending_audio_.chunk.data); pending_audio_ = AudioItem{}; has_pending_audio_ = false; progressed = true; } return progressed; } bool Player::pump_video(bool* new_frame) { const bool paused = (state_.load() == PlayerState::Paused); while (true) { if (!has_pending_video_) { BoundedQueue::Entry e; if (!video_frames_.try_pop(&e)) { return false; } if (is_stale(e.serial)) { if (e.item.frame) { av_frame_free(&e.item.frame); } continue; } note_serial_change(e.serial); pending_video_ = e.item; has_pending_video_ = true; } if (pending_video_.eof) { video_eof_ = true; has_pending_video_ = false; pending_video_ = VideoItem{}; return false; } const int64_t media_pts = tracker_media_time(pending_video_.pts_ms); if (paused) { audio_wait_since_ = 0; // pause does not count in the wait for audio if (!force_present_.exchange(false)) { return false; } // Seek during pause: the first frame of the new generation is presented // so that the picture matches the position. } else { FrameAction action = FrameAction::Present; const int64_t clock_now = clock_.now(); if (clock_now < 0 && audio_clock_expected()) { // Clock not yet set by the audio: a single primer frame, // then wait for the setting so as not to scroll too fast. if (first_video_shown_) { const uint64_t now = mono_ms(); if (audio_wait_since_ == 0) { audio_wait_since_ = now; } if (now - audio_wait_since_ < kAudioClockGraceMs) { return false; } // The audio does not come (parameters not found, silent stream): the // video leads the clock, as without an audio track. If the audio // finally arrives, it takes over again. LOG_WARN("Player: no audio %llu ms after the first frame, clock set on" "the video", static_cast(kAudioClockGraceMs)); audio_clock_waived_ = true; continue; } } else { action = decide_frame(media_pts, clock_now, SyncPolicy{}, nullptr); } if (action == FrameAction::Wait) { return false; } if (action == FrameAction::Drop) { stat_video_dropped_.fetch_add(1); av_frame_free(&pending_video_.frame); has_pending_video_ = false; pending_video_ = VideoItem{}; continue; // frame suivante } } // Present. if (cb_.video) { cb_.video->submit(pending_video_.frame); } force_present_ = false; first_video_shown_ = true; const int64_t clock_now = clock_.now(); if (clock_now >= 0) { // Without an audio track the clock is free-running, set on the video // itself: the difference would be meaningless, it is not measured. if (has_audio()) { int64_t drift = media_pts - clock_now; if (drift < 0) { drift = -drift; } stat_drift_measured_ = true; int64_t prev = stat_max_drift_.load(); while (drift > prev && !stat_max_drift_.compare_exchange_weak(prev, drift)) { } } } else if (!audio_clock_expected()) { clock_.set(media_pts); // free-running without audio } av_frame_free(&pending_video_.frame); has_pending_video_ = false; pending_video_ = VideoItem{}; if (new_frame) { *new_frame = true; } return true; // one frame presented per tick } } void Player::check_ended() { if (seek_in_flight()) { return; // the end flags date from before the seek } if (!video_eof_.load() || !audio_eof_.load() || has_pending_video_) { end_wait_start_ = 0; return; } if (state_.load() != PlayerState::Playing) { return; // when paused, we do not finish } if (cb_.audio && has_audio() && cb_.audio->queued_bytes() > 0) { // Lets the end of the audio play (bounded for dummy drivers). if (end_wait_start_ == 0) { end_wait_start_ = mono_ms(); return; } if (mono_ms() - end_wait_start_ < kEndDrainTimeoutMs) { return; } } state_ = PlayerState::Ended; apply_output_pause(); LOG_INFO("Player: end of playback"); } Player::TickResult Player::tick() { TickResult result; if (!started_.load()) { return result; } const PlayerState st = state_.load(); if (st == PlayerState::Stopped) { return result; } if (st == PlayerState::Ended) { result.ended = true; return result; } if (restart_pending_.exchange(false)) { // Stream restarted after a drop: like a seek on the consumer side. drop_pending_items(); if (cb_.audio) { cb_.audio->flush(); } gate_.reset_for_start(); buffering_ = gate_.buffering(); buffering_since_ = mono_ms(); } if (st == PlayerState::Failed) { apply_output_pause(); result.failed = true; return result; } result.buffering = update_buffering(); check_data_wait(); if (state_.load() == PlayerState::Failed) { apply_output_pause(); result.failed = true; return result; } apply_output_pause(); if (result.buffering) { return result; } if (st == PlayerState::Playing && has_audio()) { pump_audio(); } pump_video(&result.new_video_frame); last_position_ms_ = position_ms(); check_ended(); if (state_.load() == PlayerState::Ended) { result.ended = true; } return result; } // Opening or seek: if the stream is (re)opened but nothing readable // arrives within the delay, the "Seeking..." state does not last forever. void Player::check_data_wait() { if (cb_.data_wait_timeout_ms <= 0 || state_.load() == PlayerState::Failed) { return; } if (!seek_in_flight() && !gate_.priming()) { return; } const uint64_t since = data_wait_since_.load(); if (since == 0 || seek_flag_.load()) { return; } const uint64_t waited = mono_ms() - since; if (waited < static_cast(cb_.data_wait_timeout_ms)) { return; } LOG_ERROR("Player: no picture or sound %llu ms after the stream opening at %lld ms:" "abandon", static_cast(waited), static_cast(last_seek_target_.load())); fail(FailureKind::Stalled); } int64_t Player::buffered_ms() const { if (has_audio()) { int64_t ms = audio_pkts_.duration_ms() + audio_frames_.duration_ms(); if (has_pending_audio_ && !pending_audio_.eof) { ms += pending_audio_.chunk.duration_ms; } return ms; } return video_pkts_.duration_ms() + video_frames_.duration_ms(); } bool Player::update_buffering() { if (!cb_.buffering.enabled()) { return false; } // As long as the demux has not started the seek generation, the queues // still contain the old position: we stay in the primer state. if (seek_flag_.load() || serial_.load() < min_valid_serial_.load()) { buffering_ = true; return true; } BufferingEvent event = BufferingEvent::None; const bool queues_full = video_pkts_.full() || audio_pkts_.full(); const int64_t buffered = buffered_ms(); const bool hold = gate_.update(buffered, demux_eof_sent_.load(), queues_full, &event); if (event == BufferingEvent::Started) { buffering_since_ = mono_ms(); LOG_INFO("Player: buffering (reserve exhausted at %lld ms)", static_cast(position_ms())); } else if (event == BufferingEvent::Finished) { LOG_INFO("Player: playback after %llu ms of waiting (reserve %lld ms)", static_cast(mono_ms() - buffering_since_), static_cast(buffered)); } buffering_ = hold; return hold; } // Clock and audio device follow a single state: paused if // the user paused, if playback is finished or waiting for // data. Called at every tick: transitions coming from other threads // (end cancelled by a seek) are caught up. void Player::apply_output_pause() { const bool paused = state_.load() != PlayerState::Playing || buffering_.load(); if (paused == output_paused_) { return; } output_paused_ = paused; clock_.pause(paused); if (cb_.audio) { cb_.audio->pause(paused); } } // --- Controls ------------------------------------------------------------------- void Player::set_paused(bool paused) { if (paused) { PlayerState expected = PlayerState::Playing; if (state_.compare_exchange_strong(expected, PlayerState::Paused)) { apply_output_pause(); LOG_INFO("Player: pause at %lld ms", static_cast(position_ms())); } } else { PlayerState expected = PlayerState::Paused; if (state_.compare_exchange_strong(expected, PlayerState::Playing)) { apply_output_pause(); LOG_INFO("Player: resume"); } } } void Player::toggle_pause() { set_paused(state_.load() == PlayerState::Playing); } void Player::seek_to(int64_t target_ms) { const int64_t clamped = clamp_seek_target(target_ms, duration_ms()); { std::lock_guard lock(seek_mutex_); seek_pending_ = true; seek_flag_ = true; seek_target_ms_ = clamped; data_wait_since_ = 0; // the stream will be reopened: the delay is not running yet // Everything carrying the current generation is now stale, even what // the decoders will produce before the demux handles the seek. min_valid_serial_ = serial_.load() + 1; } last_seek_target_ = clamped; drop_pending_items(); gate_.reset_for_start(); buffering_ = gate_.buffering(); buffering_since_ = mono_ms(); if (cb_.audio) { cb_.audio->flush(); } LOG_INFO("Player: seek requested -> %lld ms", static_cast(clamped)); } void Player::seek_relative(int64_t delta_ms) { seek_to(position_ms() + delta_ms); } bool Player::seek_in_flight() const { { std::lock_guard lock(seek_mutex_); if (seek_pending_) { return true; } } return consumed_serial_ < min_valid_serial_.load(); } int64_t Player::position_ms() const { // During a seek, the clock still describes the old position: a // chained seek_relative() must start from the target, not from the clock. if (seek_in_flight()) { return last_seek_target_.load(); } const int64_t now = clock_.now(); if (now < 0) { const int64_t target = last_seek_target_.load(); return target >= 0 ? target : 0; } return clamp_seek_target(now, duration_ms()); } int64_t Player::duration_ms() const { return source_ ? source_->duration_ms() : 0; } PlayerStats Player::stats() const { PlayerStats s; s.video_frames_decoded = stat_video_frames_.load(); s.audio_frames_decoded = stat_audio_frames_.load(); s.video_frames_dropped = stat_video_dropped_.load(); s.max_drift_ms = stat_max_drift_.load(); s.drift_measured = stat_drift_measured_.load(); s.reconnections = stat_reconnections_.load(); s.media_duration_ms = source_ ? source_->duration_ms() : 0; return s; } // --- Mode headless ------------------------------------------------------------ bool Player::run_headless(PlayerStats* out) { if (!start()) { return false; } const double wall_start = chrono_now_ms(); int64_t last_audio_end_ms = -1; int64_t max_drift = 0; bool drift_measured = false; // Consumption merged by increasing PTS (as the real synchronization // would do, but without real-time waiting): the simulated drift then measures // the effective alignment of the two streams, not the lead of a queue. BoundedQueue::Entry held_audio; bool has_held_audio = false; int64_t held_audio_pts = 0; BoundedQueue::Entry held_video; bool has_held_video = false; int64_t held_video_pts = 0; while (!quit_) { if (!has_held_audio && !audio_eof_.load()) { if (audio_frames_.try_pop(&held_audio)) { if (held_audio.item.eof) { audio_eof_ = true; } else { held_audio_pts = tracker_media_time(held_audio.item.chunk.pts_ms); has_held_audio = true; } } } if (!has_held_video && !video_eof_.load()) { if (video_frames_.try_pop(&held_video)) { if (held_video.item.eof) { video_eof_ = true; } else { held_video_pts = tracker_media_time(held_video.item.pts_ms); has_held_video = true; } } } // Consumes the stream whose PTS is the smallest; a finished stream (EOF) // lets the other drain freely. const bool audio_ready = has_held_audio && (!has_held_video ? video_eof_.load() : held_audio_pts <= held_video_pts); const bool video_ready = has_held_video && (!has_held_audio ? audio_eof_.load() : held_video_pts < held_audio_pts); if (audio_ready) { last_audio_end_ms = held_audio_pts + held_audio.item.chunk.duration_ms; av_free(held_audio.item.chunk.data); held_audio = BoundedQueue::Entry{}; has_held_audio = false; } else if (video_ready) { if (has_audio() && last_audio_end_ms >= 0) { int64_t drift = held_video_pts - last_audio_end_ms; if (drift < 0) { drift = -drift; } drift_measured = true; if (drift > max_drift) { max_drift = drift; } } av_frame_free(&held_video.item.frame); held_video = BoundedQueue::Entry{}; has_held_video = false; } else if (video_eof_.load() && audio_eof_.load()) { break; } else { sleep_ms_portable(1); // waiting for the decoders } } if (has_held_audio && held_audio.item.chunk.data) { av_free(held_audio.item.chunk.data); } if (has_held_video && held_video.item.frame) { av_frame_free(&held_video.item.frame); } const double wall = chrono_now_ms() - wall_start; stop(); if (out) { *out = stats(); out->max_drift_ms = max_drift; // simulated drift (PTS differences) out->drift_measured = drift_measured; out->wall_ms = wall; out->video_decode_busy_ms = vdec_.busy_ms(); out->audio_decode_busy_ms = adec_.busy_ms(); } return true; } } // namespace player