| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "cast/standalone_receiver/sdl_player_base.h" |
| |
| #include <chrono> |
| #include <sstream> |
| #include <utility> |
| |
| #include "absl/types/span.h" |
| #include "cast/standalone_receiver/avcodec_glue.h" |
| #include "cast/streaming/constants.h" |
| #include "cast/streaming/encoded_frame.h" |
| #include "util/big_endian.h" |
| #include "util/chrono_helpers.h" |
| #include "util/osp_logging.h" |
| #include "util/trace_logging.h" |
| |
| namespace openscreen { |
| namespace cast { |
| |
| SDLPlayerBase::SDLPlayerBase(ClockNowFunctionPtr now_function, |
| TaskRunner* task_runner, |
| Receiver* receiver, |
| const std::string& codec_name, |
| std::function<void()> error_callback, |
| const char* media_type) |
| : now_(now_function), |
| receiver_(receiver), |
| error_callback_(std::move(error_callback)), |
| media_type_(media_type), |
| decoder_(codec_name), |
| decode_alarm_(now_, task_runner), |
| render_alarm_(now_, task_runner), |
| presentation_alarm_(now_, task_runner) { |
| OSP_DCHECK(receiver_); |
| OSP_DCHECK(media_type_); |
| |
| decoder_.set_client(this); |
| receiver_->SetConsumer(this); |
| ResumeRendering(); |
| } |
| |
| SDLPlayerBase::~SDLPlayerBase() { |
| receiver_->SetConsumer(nullptr); |
| decoder_.set_client(nullptr); |
| } |
| |
| void SDLPlayerBase::OnFatalError(std::string message) { |
| state_ = kError; |
| error_status_ = Error(Error::Code::kUnknownError, std::move(message)); |
| |
| // Halt decoding and clear the rendering queue. |
| receiver_->SetConsumer(nullptr); |
| decoder_.set_client(nullptr); |
| decode_alarm_.Cancel(); |
| frames_to_render_.clear(); |
| |
| // Resume rendering, to emit an error indication (e.g., "red splash" screen). |
| ResumeRendering(); |
| |
| if (error_callback_) { |
| const auto callback = std::move(error_callback_); |
| callback(); |
| } |
| } |
| |
| Clock::time_point SDLPlayerBase::ResyncAndDeterminePresentationTime( |
| const EncodedFrame& frame) { |
| constexpr auto kMaxPlayoutDrift = milliseconds(100); |
| const auto media_time_since_last_sync = |
| (frame.rtp_timestamp - last_sync_rtp_timestamp_) |
| .ToDuration<Clock::duration>(receiver_->rtp_timebase()); |
| Clock::time_point presentation_time = |
| last_sync_reference_time_ + media_time_since_last_sync; |
| const auto drift = to_milliseconds(frame.reference_time - presentation_time); |
| if (drift > kMaxPlayoutDrift || drift < -kMaxPlayoutDrift) { |
| // Only log if not the very first frame. |
| OSP_LOG_IF(INFO, frame.frame_id != FrameId::first()) |
| << "Playout drift (" << drift.count() << " ms) exceeded threshold (" |
| << kMaxPlayoutDrift.count() << " ms) for " << media_type_ |
| << ". Re-synchronizing..."; |
| // This is the "big-stick" way to re-synchronize. If the amount of drift |
| // is small, a production-worthy player should "nudge" things gradually |
| // back into sync over several frames. |
| last_sync_rtp_timestamp_ = frame.rtp_timestamp; |
| last_sync_reference_time_ = frame.reference_time; |
| presentation_time = frame.reference_time; |
| } |
| return presentation_time; |
| } |
| |
| void SDLPlayerBase::OnFramesReady(int buffer_size) { |
| TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); |
| // Do not consume anything if there are too many frames in the pipeline |
| // already. |
| if (static_cast<int>(frames_to_render_.size()) > kMaxFramesInPipeline) { |
| return; |
| } |
| |
| // Consume the next frame. |
| const Clock::time_point start_time = now_(); |
| buffer_.Resize(buffer_size); |
| EncodedFrame frame = receiver_->ConsumeNextFrame(buffer_.GetSpan()); |
| |
| // Create the tracking state for the frame in the player pipeline. |
| OSP_DCHECK_EQ(frames_to_render_.count(frame.frame_id), 0); |
| PendingFrame& pending_frame = frames_to_render_[frame.frame_id]; |
| pending_frame.start_time = start_time; |
| |
| pending_frame.presentation_time = ResyncAndDeterminePresentationTime(frame); |
| |
| // Start decoding the frame. This call may synchronously call back into the |
| // AVCodecDecoder::Client methods in this class. |
| decoder_.Decode(frame.frame_id, buffer_); |
| } |
| |
| void SDLPlayerBase::OnFrameDecoded(FrameId frame_id, const AVFrame& frame) { |
| TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); |
| const auto it = frames_to_render_.find(frame_id); |
| if (it == frames_to_render_.end()) { |
| return; |
| } |
| OSP_DCHECK(!it->second.decoded_frame); |
| // av_clone_frame() does a shallow copy here, incrementing a ref-count on the |
| // memory backing the frame. |
| it->second.decoded_frame = AVFrameUniquePtr(av_frame_clone(&frame)); |
| ResumeRendering(); |
| } |
| |
| void SDLPlayerBase::OnDecodeError(FrameId frame_id, std::string message) { |
| const auto it = frames_to_render_.find(frame_id); |
| if (it != frames_to_render_.end()) { |
| frames_to_render_.erase(it); |
| } |
| OSP_LOG_WARN << "Requesting " << media_type_ |
| << " key frame because of error decoding" << frame_id << ": " |
| << message; |
| receiver_->RequestKeyFrame(); |
| ResumeDecoding(); |
| } |
| |
| void SDLPlayerBase::RenderAndSchedulePresentation() { |
| TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); |
| // If something has already been scheduled to present at an exact time point, |
| // don't render anything new yet. |
| if (state_ == kScheduledToPresent) { |
| return; |
| } |
| |
| // If no frames are available, just re-render the currently-presented frame |
| // (or the error screen). |
| auto it = |
| (state_ == kError) ? frames_to_render_.end() : frames_to_render_.begin(); |
| if (it == frames_to_render_.end() || !it->second.decoded_frame) { |
| if (RenderWhileIdle(state_ == kPresented ? ¤t_frame_ : nullptr)) { |
| // Schedule presentation to happen after a rather lengthy interval, to |
| // minimize redraw/etc. resource usage while doing "idle mode" play-out. |
| // The interval here, is "lengthy" from the program's perspective, but |
| // reasonably "snappy" from the user's perspective. |
| constexpr auto kIdlePresentInterval = milliseconds(250); |
| presentation_alarm_.ScheduleFromNow( |
| [this] { |
| Present(); |
| ResumeRendering(); |
| }, |
| kIdlePresentInterval); |
| } |
| return; |
| } |
| |
| // Skip late frames, to render the first not-late frame. If all decoded frames |
| // are late, skip-forward to the least-late frame. |
| const Clock::time_point now = now_(); |
| while (it->second.presentation_time < now) { |
| const auto next_it = std::next(it); |
| if (next_it == frames_to_render_.end() || !next_it->second.decoded_frame) { |
| break; |
| } |
| frames_to_render_.erase(it); // Drop the late frame. |
| it = next_it; |
| } |
| |
| // Remove the frame from the queue, making it the |current_frame_|. Then, |
| // render it and, if successful, schedule its presentation. |
| current_frame_ = std::move(it->second); |
| frames_to_render_.erase(it); |
| const ErrorOr<Clock::time_point> presentation_time = |
| RenderNextFrame(current_frame_); |
| if (!presentation_time) { |
| OnFatalError(presentation_time.error().message()); |
| return; |
| } |
| state_ = kScheduledToPresent; |
| presentation_alarm_.Schedule( |
| [this] { |
| Present(); |
| if (state_ == kScheduledToPresent) { |
| state_ = kPresented; |
| } |
| ResumeRendering(); |
| }, |
| presentation_time.value()); |
| |
| // Resume consuming/decoding frames, since some of the prior OnFramesReady() |
| // calls may have been ignored to leave things in the Receiver's queue. |
| ResumeDecoding(); |
| |
| // Compute how long it took to decode/render this frame, and notify the |
| // Receiver of the recent-average per-frame processing time. This is used by |
| // the Receiver to determine when to drop late frames. |
| const Clock::duration measured_processing_time = |
| now_() - current_frame_.start_time; |
| constexpr int kCumulativeAveragePoints = 8; |
| recent_processing_time_ = |
| ((kCumulativeAveragePoints - 1) * recent_processing_time_ + |
| 1 * measured_processing_time) / |
| kCumulativeAveragePoints; |
| receiver_->SetPlayerProcessingTime(recent_processing_time_); |
| } |
| |
| void SDLPlayerBase::ResumeDecoding() { |
| decode_alarm_.Schedule( |
| [this] { |
| const int buffer_size = receiver_->AdvanceToNextFrame(); |
| if (buffer_size != Receiver::kNoFramesReady) { |
| OnFramesReady(buffer_size); |
| } |
| }, |
| Alarm::kImmediately); |
| } |
| |
| void SDLPlayerBase::ResumeRendering() { |
| render_alarm_.Schedule([this] { RenderAndSchedulePresentation(); }, |
| Alarm::kImmediately); |
| } |
| |
| // static |
| constexpr int SDLPlayerBase::kMaxFramesInPipeline; |
| |
| SDLPlayerBase::PresentableFrame::PresentableFrame() = default; |
| SDLPlayerBase::PresentableFrame::~PresentableFrame() = default; |
| SDLPlayerBase::PresentableFrame::PresentableFrame(PresentableFrame&&) noexcept = |
| default; |
| SDLPlayerBase::PresentableFrame& SDLPlayerBase::PresentableFrame::operator=( |
| PresentableFrame&&) noexcept = default; |
| |
| SDLPlayerBase::PendingFrame::PendingFrame() = default; |
| SDLPlayerBase::PendingFrame::~PendingFrame() = default; |
| SDLPlayerBase::PendingFrame::PendingFrame(PendingFrame&&) noexcept = default; |
| SDLPlayerBase::PendingFrame& SDLPlayerBase::PendingFrame::operator=( |
| PendingFrame&&) noexcept = default; |
| |
| } // namespace cast |
| } // namespace openscreen |