| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/cast/encoding/media_video_encoder_wrapper.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/task/thread_pool.h" |
| #include "build/build_config.h" |
| #include "media/base/async_destroy_video_encoder.h" |
| #include "media/base/encoder_status.h" |
| #include "media/base/media_switches.h" |
| #include "media/base/media_util.h" |
| #include "media/base/video_codecs.h" |
| #include "media/base/video_encoder.h" |
| #include "media/base/video_encoder_metrics_provider.h" |
| #include "media/base/video_frame.h" |
| #include "media/cast/cast_config.h" |
| #include "media/cast/common/openscreen_conversion_helpers.h" |
| #include "media/cast/common/sender_encoded_frame.h" |
| #include "media/cast/constants.h" |
| #include "media/cast/encoding/encoding_support.h" |
| #include "media/cast/encoding/video_encoder.h" |
| #include "media/media_buildflags.h" |
| #include "media/video/gpu_video_accelerator_factories.h" |
| #include "media/video/video_encode_accelerator_adapter.h" |
| #include "media/video/video_encoder_info.h" |
| #include "media_video_encoder_wrapper.h" |
| |
| #if BUILDFLAG(ENABLE_LIBVPX) |
| #include "media/video/vpx_video_encoder.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_LIBAOM) |
| #include "media/video/av1_video_encoder.h" |
| #endif // BUILDFLAG(ENABLE_LIBAOM) |
| |
| namespace media::cast { |
| namespace { |
| |
| std::unique_ptr<media::VideoEncoder> CreateHardwareEncoder( |
| media::GpuVideoAcceleratorFactories& gpu_factories, |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner) { |
| // TODO(crbug.com/282984511): consider providing a non-null MediaLog. |
| return std::make_unique< |
| media::AsyncDestroyVideoEncoder<media::VideoEncodeAcceleratorAdapter>>( |
| std::make_unique<media::VideoEncodeAcceleratorAdapter>( |
| &gpu_factories, std::make_unique<media::NullMediaLog>(), |
| std::move(task_runner))); |
| } |
| |
| // TODO(crbug.com/282984511): consider adding support for H264 here, using |
| // the media::OpenH264VideoEncoder. |
| std::unique_ptr<media::VideoEncoder> CreateSoftwareEncoder(VideoCodec codec) { |
| switch (codec) { |
| #if BUILDFLAG(ENABLE_LIBVPX) |
| case VideoCodec::kVP8: |
| case VideoCodec::kVP9: |
| return std::make_unique<media::VpxVideoEncoder>(); |
| #endif |
| #if BUILDFLAG(ENABLE_LIBAOM) |
| case VideoCodec::kAV1: |
| return std::make_unique<media::Av1VideoEncoder>(); |
| #endif // BUILDFLAG(ENABLE_LIBAOM) |
| default: |
| NOTREACHED() << "Unhandled codec. value=" << std::to_underlying(codec); |
| } |
| } |
| |
| // Encoder utilization is calculated differently depending on encoder |
| // acceleration: |
| // * Hardware encoders are pipelined, so we use the maximum of the backlog of |
| // frames waiting to be processed, and the processing time of each frame |
| // relative to the target frame duration. |
| // * Software encoders are typically synchronous, so we can directly map the |
| // processing time of each frame relative to the target frame duration. |
| double CalculateEncoderUtilization(bool is_hardware_encoder, |
| size_t backlog_size, |
| base::TimeDelta processing_time, |
| base::TimeDelta target_frame_duration) { |
| double utilization = 0.0; |
| if (is_hardware_encoder) { |
| // The "pipeline" is the set of frames that is actively undergoing |
| // processing. This is similar in size to the backlog but represents a |
| // separate stage in the encoding process. This value is chosen based on the |
| // capacity of a typical hardware encoder. |
| static constexpr int kPipelineSize = 3; |
| |
| // The "backlog" is the set of frames that has been submitted for encoding |
| // but not yet taken for processing. Our value is based on the size of the |
| // pipeline, since the backlog is fully saturated when we have more frames |
| // than the pipeline can process. |
| static constexpr int kMaxBacklogSize = kPipelineSize + 1; |
| |
| // 1. Backlog Model: Is the encoder keeping up with the submission rate? |
| double backlog_utilization = |
| static_cast<double>(backlog_size) / kMaxBacklogSize; |
| |
| // 2. Latency Model: Is the pipeline taking too long glass-to-glass? |
| double latency_utilization = |
| processing_time / (target_frame_duration * kPipelineSize); |
| |
| // Hardware utilization is the worst-case of the two metrics. |
| utilization = std::max(backlog_utilization, latency_utilization); |
| } else { |
| // Software is synchronous/bounded, so latency directly maps to CPU load. |
| utilization = processing_time / target_frame_duration; |
| } |
| |
| // Clamp to a safe range to prevent PID controller explosion on anomalies |
| // (e.g., system sleep or thread starvation). |
| return std::clamp(utilization, 0.0, 2.0); |
| } |
| |
| // Must be called on the ENCODER thread, which resolves to VIDEO for hardware |
| // encoding, and MAIN for software encoding. |
| void CallInitializeEncoder(media::VideoEncoder& encoder, |
| VideoCodecProfile profile, |
| const media::VideoEncoder::Options& options, |
| media::VideoEncoder::EncoderInfoCB info_cb, |
| media::VideoEncoder::OutputCB output_cb, |
| media::VideoEncoder::EncoderStatusCB done_cb) { |
| encoder.Initialize(profile, options, std::move(info_cb), std::move(output_cb), |
| std::move(done_cb)); |
| |
| // Our callbacks post to the correct thread, instead of needing the encoder to |
| // manage posting back. |
| encoder.DisablePostedCallbacks(); |
| } |
| |
| // Must be called on the ENCODER thread, which resolves to VIDEO for hardware |
| // encoding, and MAIN for software encoding. |
| void CallEncodeVideoFrame( |
| media::VideoEncoder& encoder, |
| scoped_refptr<media::VideoFrame> video_frame, |
| const media::VideoEncoder::EncodeOptions& encode_options, |
| media::VideoEncoder::EncoderStatusCB done_cb) { |
| encoder.Encode(std::move(video_frame), encode_options, std::move(done_cb)); |
| } |
| |
| // Must be called on the ENCODER thread, which resolves to VIDEO for hardware |
| // encoding, and MAIN for software encoding. |
| // |
| // NOTE: since this call depends on a successful flush, `flush_result` is |
| // checked before the encoder is accessed. |
| void CallChangeOptions(media::VideoEncoder& encoder, |
| media::VideoEncoder::Options options, |
| media::VideoEncoder::OutputCB output_cb, |
| media::VideoEncoder::EncoderStatusCB done_cb) { |
| encoder.ChangeOptions(std::move(options), std::move(output_cb), |
| std::move(done_cb)); |
| } |
| |
| // Must be called on the ENCODER thread, which resolves to VIDEO for hardware |
| // encoding, and MAIN for software encoding. |
| void CallFlush(media::VideoEncoder& encoder, |
| media::VideoEncoder::EncoderStatusCB done_cb) { |
| encoder.Flush(std::move(done_cb)); |
| } |
| |
| // TODO(crbug.com/282984511): just use EncoderStatus directly once we remove |
| // media::cast::VideoEncoder. |
| OperationalStatus ToOperationalStatus(EncoderStatus status) { |
| switch (status.code()) { |
| case EncoderStatus::Codes::kOk: |
| return STATUS_INITIALIZED; |
| |
| case EncoderStatus::Codes::kEncoderInitializeNeverCompleted: |
| case EncoderStatus::Codes::kEncoderInitializeTwice: |
| case EncoderStatus::Codes::kEncoderInitializationError: |
| return STATUS_CODEC_INIT_FAILED; |
| |
| case EncoderStatus::Codes::kEncoderUnsupportedProfile: |
| case EncoderStatus::Codes::kEncoderUnsupportedCodec: |
| return STATUS_UNSUPPORTED_CODEC; |
| |
| case EncoderStatus::Codes::kEncoderUnsupportedConfig: |
| return STATUS_INVALID_CONFIGURATION; |
| |
| // Most encoder statuses can just be considered runtime errors. |
| default: |
| return STATUS_CODEC_RUNTIME_ERROR; |
| } |
| } |
| |
| } // namespace |
| |
| MediaVideoEncoderWrapper::MediaVideoEncoderWrapper( |
| scoped_refptr<CastEnvironment> cast_environment, |
| const FrameSenderConfig& video_config, |
| std::unique_ptr<VideoEncoderMetricsProvider> metrics_provider, |
| StatusChangeCallback status_change_cb, |
| media::GpuVideoAcceleratorFactories* gpu_factories) |
| : cast_environment_(std::move(cast_environment)), |
| metrics_provider_(std::move(metrics_provider)), |
| status_change_cb_(std::move(status_change_cb)), |
| gpu_factories_(gpu_factories), |
| is_hardware_encoder_(video_config.use_hardware_encoder), |
| codec_(video_config.video_codec()), |
| target_frame_duration_(base::Seconds(1) / video_config.max_frame_rate), |
| encoder_(nullptr, |
| base::OnTaskRunnerDeleter(cast_environment_->GetTaskRunner( |
| CastEnvironment::ThreadId::kVideo))), |
| frame_complexity_estimator_(base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), base::TaskPriority::USER_VISIBLE})) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| CHECK(metrics_provider_); |
| CHECK(status_change_cb_); |
| |
| encode_options_.key_frame = true; |
| options_.bitrate = Bitrate::ConstantBitrate( |
| base::checked_cast<uint32_t>(video_config.start_bitrate)); |
| |
| // NOTE: the H264 encoder can produce either AVC or annexb formatted frames. |
| // Annexb is better supported by Cast receivers. |
| if (codec_ == media::VideoCodec::kH264) { |
| options_.avc.produce_annexb = true; |
| } |
| |
| // NOTE: since we don't actually know the frame size until the first |
| // frame, the encoder will not get created until the first call to |
| // `EncodeVideoFrame`. However, it is ready to start receiving frames. |
| status_change_cb_.Run(STATUS_INITIALIZED); |
| } |
| |
| MediaVideoEncoderWrapper::~MediaVideoEncoderWrapper() { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| weak_factory_.InvalidateWeakPtrs(); |
| } |
| |
| bool MediaVideoEncoderWrapper::EncodeVideoFrame( |
| scoped_refptr<media::VideoFrame> video_frame, |
| base::TimeTicks reference_time, |
| FrameEncodedCallback frame_encoded_callback) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| CHECK(!video_frame->visible_rect().IsEmpty()); |
| |
| // TODO(crbug.com/282984511): consider adding optimization to store frames |
| // that are queued for encoding and sending them once updating options is |
| // complete. |
| if (num_pending_updates_ > 0) { |
| return false; |
| } |
| |
| // Construct and initialize the encoder on the first call to this method. |
| const gfx::Size frame_size = video_frame->visible_rect().size(); |
| if (frame_size != options_.frame_size) { |
| if (options_.frame_size.IsEmpty()) { |
| // First frame, reconstruct immediately to avoid startup latency. |
| options_.frame_size = frame_size; |
| ConstructEncoder(); |
| } else if (!target_resize_size_ || frame_size != *target_resize_size_) { |
| // Debounce subsequent size changes. Only restart timer if the new size |
| // is different from the current target. |
| target_resize_size_ = frame_size; |
| resize_debounce_timer_.Start( |
| FROM_HERE, base::Milliseconds(250), |
| base::BindOnce( |
| &MediaVideoEncoderWrapper::ReconstructEncoderForNewSize, |
| weak_factory_.GetWeakPtr(), frame_size)); |
| } |
| } else if (resize_debounce_timer_.IsRunning()) { |
| // If the frame size returns to the original size before the timer fires, |
| // cancel the resize. |
| resize_debounce_timer_.Stop(); |
| target_resize_size_.reset(); |
| } |
| CHECK(encoder_); |
| |
| frame_complexity_estimator_ |
| .AsyncCall(encode_options_.key_frame |
| ? &FrameComplexityEstimator::EstimateForKeyFrame |
| : &FrameComplexityEstimator::EstimateForDeltaFrame) |
| .WithArgs(std::cref(*video_frame)) |
| .Then(base::BindOnce( |
| &MediaVideoEncoderWrapper::OnComplexityEstimated, |
| weak_factory_.GetWeakPtr(), video_frame, encode_options_, |
| reference_time, std::move(frame_encoded_callback), encoder_version_)); |
| |
| encode_options_.key_frame = false; |
| |
| return true; |
| } |
| |
| void MediaVideoEncoderWrapper::OnComplexityEstimated( |
| scoped_refptr<media::VideoFrame> video_frame, |
| media::VideoEncoder::EncodeOptions encode_options, |
| base::TimeTicks reference_time, |
| FrameEncodedCallback frame_encoded_callback, |
| int encoder_version, |
| std::optional<double> estimated_complexity) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| |
| if (encoder_version != encoder_version_) { |
| // The encoder was reconstructed while we were estimating the quantizer. |
| // Drop this frame as it was meant for the old encoder. |
| std::move(frame_encoded_callback).Run(nullptr); |
| return; |
| } |
| |
| CHECK(encoder_); |
| |
| recent_metadata_.emplace( |
| video_frame->metadata().capture_begin_time, |
| video_frame->metadata().capture_end_time, base::TimeTicks::Now(), |
| ToRtpTimeTicks(video_frame->timestamp(), kVideoFrequency), reference_time, |
| GetFrameDuration(*video_frame), estimated_complexity, |
| std::move(frame_encoded_callback)); |
| |
| // Now that `GetFrameDuration` has been called, we can update the last frame |
| // timestamp. It must be monotonically increasing. |
| if (last_frame_timestamp_) { |
| CHECK_GT(video_frame->timestamp(), last_frame_timestamp_.value()); |
| } |
| last_frame_timestamp_ = video_frame->timestamp(); |
| |
| auto encode_task = base::BindOnce( |
| &CallEncodeVideoFrame, std::ref(*encoder_), std::move(video_frame), |
| encode_options, |
| CreateCallback(&MediaVideoEncoderWrapper::OnFrameEncodeDone, |
| encoder_version, reference_time)); |
| |
| if (num_pending_updates_ > 0) { |
| pending_encodes_.push_back(std::move(encode_task)); |
| } else { |
| CallEncoderOnCorrectThread(std::move(encode_task)); |
| } |
| } |
| |
| // Inform the encoder about the new target bit rate. |
| void MediaVideoEncoderWrapper::SetBitRate(uint32_t new_bit_rate) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| options_.bitrate = Bitrate::ConstantBitrate(new_bit_rate); |
| |
| // If this method is called before the encoder_ is constructed, the bitrate |
| // will be set as part of construction. |
| if (encoder_) { |
| // If we have an encoder, we need to update its options. |
| UpdateEncoderOptions(); |
| } |
| } |
| |
| void MediaVideoEncoderWrapper::ReconstructEncoderForNewSize( |
| const gfx::Size& new_size) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| target_resize_size_.reset(); |
| if (new_size != options_.frame_size) { |
| options_.frame_size = new_size; |
| ConstructEncoder(); |
| } |
| } |
| |
| // Inform the encoder to encode the next frame as a key frame. |
| void MediaVideoEncoderWrapper::GenerateKeyFrame() { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| encode_options_.key_frame = true; |
| } |
| |
| void MediaVideoEncoderWrapper::OnEncodedFrame( |
| int encoder_version, |
| VideoEncoderOutput output, |
| std::optional<media::VideoEncoder::CodecDescription> description) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| if (encoder_version != encoder_version_) { |
| return; |
| } |
| |
| CachedMetadata& metadata = recent_metadata_.front(); |
| auto encoded_frame = std::make_unique<SenderEncodedFrame>(); |
| encoded_frame->is_key_frame = output.key_frame; |
| encoded_frame->frame_id = next_frame_id_++; |
| encoded_frame->referenced_frame_id = encoded_frame->is_key_frame |
| ? encoded_frame->frame_id |
| : encoded_frame->frame_id - 1; |
| encoded_frame->rtp_timestamp = metadata.rtp_timestamp; |
| encoded_frame->reference_time = metadata.reference_time; |
| |
| encoded_frame->encode_completion_time = cast_environment_->NowTicks(); |
| |
| const base::TimeDelta processing_time = |
| encoded_frame->encode_completion_time - metadata.encode_start_time; |
| encoded_frame->encoder_utilization = |
| ComputeAndRecordUtilization(processing_time); |
| |
| encoded_frame->lossiness = |
| metadata.estimated_complexity |
| ? ComputeLossiness(output.data.size(), metadata.frame_duration, |
| *metadata.estimated_complexity) |
| : 0.0f; |
| |
| encoded_frame->capture_begin_time = metadata.capture_begin_time; |
| encoded_frame->capture_end_time = metadata.capture_end_time; |
| encoded_frame->data = std::move(output.data); |
| |
| auto frame_encoded_callback = std::move(metadata.frame_encoded_callback); |
| recent_metadata_.pop(); |
| metrics_provider_->IncrementEncodedFrameCount(); |
| std::move(frame_encoded_callback).Run(std::move(encoded_frame)); |
| } |
| |
| double MediaVideoEncoderWrapper::ComputeAndRecordUtilization( |
| base::TimeDelta processing_time) { |
| const double raw_utilization = |
| CalculateEncoderUtilization(is_hardware_encoder_, recent_metadata_.size(), |
| processing_time, target_frame_duration_); |
| |
| // Apply EMA to smooth out I-frame spikes. |
| // A weight of 0.1 for the new value is standard for 30-60fps media pipelines. |
| static constexpr double kEmaWeight = 0.1; |
| if (!ema_encoder_utilization_) { |
| ema_encoder_utilization_ = raw_utilization; |
| } else { |
| ema_encoder_utilization_ = (raw_utilization * kEmaWeight) + |
| (*ema_encoder_utilization_ * (1.0 - kEmaWeight)); |
| } |
| return *ema_encoder_utilization_; |
| } |
| |
| // Computes a heuristic representing the visual quality degradation of the |
| // encoded frame. Upstream rate controllers use this "lossiness" metric to |
| // decide if they need to drop the framerate to give individual frames a |
| // larger bit budget. |
| // |
| // A higher return value indicates a more heavily compressed (lower quality) |
| // frame. This is estimated by multiplying the frame's bitrate utilization |
| // by its estimated spatial/temporal complexity. |
| double MediaVideoEncoderWrapper::ComputeLossiness( |
| size_t data_size, |
| base::TimeDelta frame_duration, |
| double estimated_complexity) const { |
| const double duration = frame_duration.InSecondsF(); |
| if (duration <= 0.0) { |
| return 0.0; |
| } |
| |
| // 1. Calculate the effective bitrate of this specific frame. |
| const double data_size_in_bits = data_size * 8.0; |
| const double actual_bitrate = data_size_in_bits / duration; |
| |
| // 2. Calculate bitrate utilization compared to target bitrate. |
| // This measures how much of our target bandwidth budget this frame consumed. |
| // If actual_bitrate > target_bitrate, the encoder is overshooting to handle |
| // difficult content. |
| const double target_bitrate = options_.bitrate->target_bps(); |
| CHECK_GT(target_bitrate, 0.0); |
| const double bitrate_utilization = actual_bitrate / target_bitrate; |
| |
| // 3. Scale utilization by the estimated complexity of the frame. |
| // The estimated_complexity represents how difficult the frame is to compress |
| // (e.g., high motion or high detail). A frame that uses 100% of the bitrate |
| // budget on a blank wall (low complexity) is not very lossy. A frame that |
| // uses 100% of the budget on a confetti explosion (high complexity) is |
| // assumed to be highly lossy. |
| const double raw_lossiness = bitrate_utilization * estimated_complexity; |
| |
| // 4. Clamp to a safe upper bound to protect the rate controller's internal |
| // state from timestamp jitter anomalies. |
| constexpr double kMaxLossiness = 5.0; |
| return std::clamp(raw_lossiness, 0.0, kMaxLossiness); |
| } |
| |
| void MediaVideoEncoderWrapper::OnEncoderStatus(int encoder_version, |
| EncoderStatus error) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| if (encoder_version != encoder_version_) { |
| return; |
| } |
| if (!last_recorded_status_ || error != last_recorded_status_.value()) { |
| last_recorded_status_ = error; |
| |
| if (!error.is_ok()) { |
| CHECK(metrics_provider_); |
| metrics_provider_->SetError(std::move(error)); |
| } |
| |
| // Running the status change callback may delete `this`, so this should be |
| // done last. |
| status_change_cb_.Run(ToOperationalStatus(error)); |
| } |
| } |
| |
| void MediaVideoEncoderWrapper::OnEncoderInfo( |
| int encoder_version, |
| const VideoEncoderInfo& encoder_info) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| if (encoder_version != encoder_version_) { |
| return; |
| } |
| // TODO(crbug.com/282984511): support handling `supports_frame_size_change` |
| // property. |
| } |
| |
| void MediaVideoEncoderWrapper::SetEncoderForTesting( |
| std::unique_ptr<media::VideoEncoder> encoder) { |
| encoder_is_overridden_for_testing_ = true; |
| SetEncoder(std::move(encoder)); |
| } |
| |
| MediaVideoEncoderWrapper::CachedMetadata::CachedMetadata( |
| std::optional<base::TimeTicks> capture_begin_time, |
| std::optional<base::TimeTicks> capture_end_time, |
| base::TimeTicks encode_start_time, |
| RtpTimeTicks rtp_timestamp, |
| base::TimeTicks reference_time, |
| base::TimeDelta frame_duration, |
| std::optional<double> estimated_complexity, |
| FrameEncodedCallback frame_encoded_callback) |
| : capture_begin_time(capture_begin_time), |
| capture_end_time(capture_end_time), |
| encode_start_time(encode_start_time), |
| rtp_timestamp(rtp_timestamp), |
| reference_time(reference_time), |
| frame_duration(frame_duration), |
| estimated_complexity(estimated_complexity), |
| frame_encoded_callback(std::move(frame_encoded_callback)) {} |
| MediaVideoEncoderWrapper::CachedMetadata::CachedMetadata() = default; |
| MediaVideoEncoderWrapper::CachedMetadata::CachedMetadata( |
| MediaVideoEncoderWrapper::CachedMetadata&& other) = default; |
| MediaVideoEncoderWrapper::CachedMetadata& |
| MediaVideoEncoderWrapper::CachedMetadata::operator=( |
| MediaVideoEncoderWrapper::CachedMetadata&& other) = default; |
| MediaVideoEncoderWrapper::CachedMetadata::~CachedMetadata() = default; |
| |
| void MediaVideoEncoderWrapper::ConstructEncoder() { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| encoder_version_++; |
| |
| // Clear any pending encodes and options updates meant for the old encoder. |
| pending_encodes_.clear(); |
| num_pending_updates_ = 0; |
| |
| // Clear metadata and run callbacks with nullptr for any frames that were |
| // in-flight to the old encoder, as the old encoder will be deleted and will |
| // not emit them. |
| while (!recent_metadata_.empty()) { |
| auto callback = std::move(recent_metadata_.front().frame_encoded_callback); |
| recent_metadata_.pop(); |
| if (callback) { |
| std::move(callback).Run(nullptr); |
| } |
| } |
| |
| if (encoder_is_overridden_for_testing_) { |
| // Don't construct a new encoder if it is overridden for testing. |
| } else if (is_hardware_encoder_) { |
| CHECK(gpu_factories_); |
| SetEncoder(CreateHardwareEncoder( |
| *gpu_factories_, |
| cast_environment_->GetTaskRunner(CastEnvironment::ThreadId::kMain))); |
| } else { |
| SetEncoder(CreateSoftwareEncoder(codec_)); |
| } |
| CHECK(encoder_); |
| |
| const VideoCodecProfile profile = encoding_support::ToProfile(codec_); |
| metrics_provider_->Initialize(profile, options_.frame_size, |
| is_hardware_encoder_); |
| |
| CallEncoderOnCorrectThread(base::BindOnce( |
| &CallInitializeEncoder, std::ref(*encoder_), profile, options_, |
| CreateCallback(&MediaVideoEncoderWrapper::OnEncoderInfo, |
| encoder_version_), |
| CreateCallback(&MediaVideoEncoderWrapper::OnEncodedFrame, |
| encoder_version_), |
| CreateCallback(&MediaVideoEncoderWrapper::OnEncoderStatus, |
| encoder_version_))); |
| } |
| |
| void MediaVideoEncoderWrapper::SetEncoder( |
| std::unique_ptr<media::VideoEncoder> encoder) { |
| // This unusual syntax unwraps `encoder`, which has a default deleter, and |
| // transfers ownership to `encoder_`, which has a base::OnTaskRunnerDeleter. |
| encoder_.reset(encoder.release()); |
| } |
| |
| base::TimeDelta MediaVideoEncoderWrapper::GetFrameDuration( |
| const VideoFrame& frame) { |
| // Frame has duration in metadata, use it. |
| if (frame.metadata().frame_duration.has_value()) { |
| return frame.metadata().frame_duration.value(); |
| } |
| |
| // No real way to figure out duration, use time passed since the last frame |
| // as an educated guess, but clamp it within reasonable limits. |
| constexpr auto min_duration = base::Seconds(1.0 / 60.0); |
| constexpr auto max_duration = base::Seconds(1.0 / 24.0); |
| const base::TimeDelta duration = |
| frame.timestamp() - last_frame_timestamp_.value_or(base::TimeDelta{}); |
| return std::clamp(duration, min_duration, max_duration); |
| } |
| |
| // The VideoEncoder API requires you to Flush() and wait before calling |
| // ChangeOptions(). This results in this annoying set of nested callbacks. |
| void MediaVideoEncoderWrapper::UpdateEncoderOptions() { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| num_pending_updates_++; |
| |
| auto flush_done_callback = |
| CreateCallback(&MediaVideoEncoderWrapper::OnFlushDoneForOptionsUpdate, |
| encoder_version_, options_); |
| CallEncoderOnCorrectThread(base::BindOnce(&CallFlush, std::ref(*encoder_), |
| std::move(flush_done_callback))); |
| } |
| |
| void MediaVideoEncoderWrapper::OnFlushDoneForOptionsUpdate( |
| int encoder_version, |
| media::VideoEncoder::Options options, |
| EncoderStatus status) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| if (encoder_version != encoder_version_) { |
| return; |
| } |
| |
| if (!status.is_ok()) { |
| OnOptionsUpdated(encoder_version, status); |
| return; |
| } |
| |
| CallEncoderOnCorrectThread(base::BindOnce( |
| &CallChangeOptions, std::ref(*encoder_), std::move(options), |
| CreateCallback(&MediaVideoEncoderWrapper::OnEncodedFrame, |
| encoder_version), |
| CreateCallback(&MediaVideoEncoderWrapper::OnOptionsUpdated, |
| encoder_version))); |
| } |
| |
| void MediaVideoEncoderWrapper::CallEncoderOnCorrectThread( |
| base::OnceClosure closure) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| |
| // If hardware, let the encoder do the post to the accelerator / VIDEO thread |
| // on its own. |
| if (is_hardware_encoder_) { |
| std::move(closure).Run(); |
| |
| // If software, manually post it to the VIDEO thread to not block MAIN. |
| } else { |
| cast_environment_->PostTask(CastEnvironment::ThreadId::kVideo, FROM_HERE, |
| std::move(closure)); |
| } |
| } |
| |
| void MediaVideoEncoderWrapper::OnFrameEncodeDone(int encoder_version, |
| base::TimeTicks reference_time, |
| EncoderStatus status) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| if (encoder_version != encoder_version_) { |
| return; |
| } |
| // An "OK" status is a no-op: the frame is handled in OnEncodedFrame(). |
| if (status.is_ok()) { |
| return; |
| } |
| |
| // Otherwise notify the VideoSender that we had a failed encode (so that it |
| // can appropriately update the duration in flight). |
| CachedMetadata& metadata = recent_metadata_.front(); |
| CHECK(metadata.reference_time == reference_time); |
| auto callback = std::move(metadata.frame_encoded_callback); |
| recent_metadata_.pop(); |
| |
| std::move(callback).Run(nullptr); |
| |
| OnEncoderStatus(encoder_version, status); |
| } |
| |
| void MediaVideoEncoderWrapper::OnOptionsUpdated(int encoder_version, |
| EncoderStatus status) { |
| CHECK(cast_environment_->CurrentlyOn(CastEnvironment::ThreadId::kMain)); |
| if (encoder_version != encoder_version_) { |
| return; |
| } |
| --num_pending_updates_; |
| |
| if (num_pending_updates_ == 0) { |
| for (auto& task : pending_encodes_) { |
| CallEncoderOnCorrectThread(std::move(task)); |
| } |
| pending_encodes_.clear(); |
| } |
| |
| OnEncoderStatus(encoder_version, status); |
| } |
| |
| } // namespace media::cast |