| // Copyright 2014 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 "media/cast/sender/external_video_encoder.h" |
| |
| #include <array> |
| #include <cmath> |
| #include <sstream> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/logging.h" |
| #include "base/memory/shared_memory_mapping.h" |
| #include "base/memory/unsafe_shared_memory_region.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "build/build_config.h" |
| #include "media/base/bind_to_current_loop.h" |
| #include "media/base/bitrate.h" |
| #include "media/base/media_switches.h" |
| #include "media/base/video_frame.h" |
| #include "media/base/video_types.h" |
| #include "media/base/video_util.h" |
| #include "media/cast/cast_config.h" |
| #include "media/cast/common/rtp_time.h" |
| #include "media/cast/logging/logging_defines.h" |
| #include "media/cast/net/cast_transport_config.h" |
| #include "media/cast/sender/vpx_quantizer_parser.h" |
| #include "media/video/h264_parser.h" |
| |
| namespace { |
| |
| // The percentage of each frame to sample. This value is based on an |
| // analysis that showed sampling 10% of the rows of a frame generated |
| // reasonably accurate results. |
| constexpr int kFrameSamplingPercentage = 10; |
| |
| // QP for H.264 encoders ranges from [0, 51] (inclusive). |
| constexpr int kMaxH264Quantizer = 51; |
| |
| // Number of buffers for encoded bit stream. |
| constexpr size_t kOutputBufferCount = 3; |
| |
| // Maximum number of extra input buffers for encoder. The input buffers are only |
| // used when copy is needed to match the required coded size. |
| constexpr size_t kExtraInputBufferCount = 2; |
| |
| // This value is used to calculate the encoder utilization. The encoder is |
| // assumed to be in full usage when the number of frames in progress reaches it. |
| constexpr int kBacklogRedlineThreshold = 4; |
| |
| // The number of histogram buckets for quantization estimation. These |
| // histograms must encompass the range [-255, 255] (inclusive). |
| constexpr int kQuantizationHistogramSize = 511; |
| |
| } // namespace |
| |
| namespace media { |
| namespace cast { |
| |
| // Container for the associated data of a video frame being processed. |
| struct InProgressExternalVideoFrameEncode { |
| // The source content to encode. |
| const scoped_refptr<VideoFrame> video_frame; |
| |
| // The reference time for this frame. |
| const base::TimeTicks reference_time; |
| |
| // The callback to run when the result is ready. |
| VideoEncoder::FrameEncodedCallback frame_encoded_callback; |
| |
| // The target encode bit rate. |
| const int target_bit_rate; |
| |
| // The real-world encode start time. This is used to compute the encoded |
| // frame's |encoder_utilization| and so it uses the real-world clock instead |
| // of the CastEnvironment clock, the latter of which might be simulated. |
| const base::TimeTicks start_time; |
| |
| InProgressExternalVideoFrameEncode( |
| scoped_refptr<VideoFrame> v_frame, |
| base::TimeTicks r_time, |
| VideoEncoder::FrameEncodedCallback callback, |
| int bit_rate) |
| : video_frame(std::move(v_frame)), |
| reference_time(r_time), |
| frame_encoded_callback(std::move(callback)), |
| target_bit_rate(bit_rate), |
| start_time(base::TimeTicks::Now()) {} |
| }; |
| |
| // Owns a VideoEncoderAccelerator instance and provides the necessary adapters |
| // to encode media::VideoFrames and emit media::cast::EncodedFrames. All |
| // methods must be called on the thread associated with the given |
| // SingleThreadTaskRunner, except for the task_runner() accessor. |
| class ExternalVideoEncoder::VEAClientImpl final |
| : public VideoEncodeAccelerator::Client, |
| public base::RefCountedThreadSafe<VEAClientImpl> { |
| public: |
| VEAClientImpl( |
| const scoped_refptr<CastEnvironment>& cast_environment, |
| const scoped_refptr<base::SingleThreadTaskRunner>& encoder_task_runner, |
| std::unique_ptr<media::VideoEncodeAccelerator> vea, |
| double max_frame_rate, |
| StatusChangeCallback status_change_cb) |
| : cast_environment_(cast_environment), |
| task_runner_(encoder_task_runner), |
| max_frame_rate_(max_frame_rate), |
| status_change_cb_(std::move(status_change_cb)), |
| video_encode_accelerator_(std::move(vea)), |
| encoder_active_(false), |
| next_frame_id_(FrameId::first()), |
| key_frame_encountered_(false), |
| codec_profile_(media::VIDEO_CODEC_PROFILE_UNKNOWN), |
| key_frame_quantizer_parsable_(false), |
| requested_bit_rate_(-1), |
| allocate_input_buffer_in_progress_(false) {} |
| |
| base::SingleThreadTaskRunner* task_runner() const { |
| return task_runner_.get(); |
| } |
| |
| void Initialize(const gfx::Size& frame_size, |
| VideoCodecProfile codec_profile, |
| int start_bit_rate, |
| FrameId first_frame_id) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| requested_bit_rate_ = start_bit_rate; |
| const media::VideoEncodeAccelerator::Config config( |
| media::PIXEL_FORMAT_I420, frame_size, codec_profile, |
| media::Bitrate::ConstantBitrate(start_bit_rate)); |
| encoder_active_ = video_encode_accelerator_->Initialize(config, this); |
| next_frame_id_ = first_frame_id; |
| codec_profile_ = codec_profile; |
| |
| UMA_HISTOGRAM_BOOLEAN("Cast.Sender.VideoEncodeAcceleratorInitializeSuccess", |
| encoder_active_); |
| |
| cast_environment_->PostTask( |
| CastEnvironment::MAIN, FROM_HERE, |
| base::BindOnce(status_change_cb_, encoder_active_ |
| ? STATUS_INITIALIZED |
| : STATUS_CODEC_INIT_FAILED)); |
| } |
| |
| void SetBitRate(int bit_rate) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| requested_bit_rate_ = bit_rate; |
| if (encoder_active_) { |
| video_encode_accelerator_->RequestEncodingParametersChange( |
| Bitrate::ConstantBitrate(bit_rate), |
| static_cast<uint32_t>(max_frame_rate_ + 0.5)); |
| } |
| } |
| |
| // The destruction call back of the copied video frame to free its use of |
| // the input buffer. |
| void ReturnInputBufferToPool(int index) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK_GE(index, 0); |
| DCHECK_LT(index, static_cast<int>(input_buffers_.size())); |
| free_input_buffer_index_.push_back(index); |
| } |
| |
| void EncodeVideoFrame( |
| scoped_refptr<media::VideoFrame> video_frame, |
| base::TimeTicks reference_time, |
| bool key_frame_requested, |
| VideoEncoder::FrameEncodedCallback frame_encoded_callback) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| in_progress_frame_encodes_.push_back(InProgressExternalVideoFrameEncode( |
| video_frame, reference_time, std::move(frame_encoded_callback), |
| requested_bit_rate_)); |
| |
| if (!encoder_active_) { |
| AbortLatestEncodeAttemptDueToErrors(); |
| return; |
| } |
| |
| // If there are no free input buffers in the pool, request allocation of |
| // another one. Since that's an asynchronous process, simply abort encoding |
| // this frame and hope that the input buffer is ready for the next frame(s). |
| if (free_input_buffer_index_.empty()) { |
| if (!allocate_input_buffer_in_progress_ && |
| input_buffers_.size() < max_allowed_input_buffers_) { |
| allocate_input_buffer_in_progress_ = true; |
| const size_t buffer_size = media::VideoFrame::AllocationSize( |
| media::PIXEL_FORMAT_I420, frame_coded_size_); |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&VEAClientImpl::AllocateInputBuffer, this, |
| buffer_size)); |
| } |
| AbortLatestEncodeAttemptDueToErrors(); |
| return; |
| } |
| |
| // Copy the |video_frame| into the input buffer provided by the VEA |
| // implementation, and with the exact row stride required. Note that, even |
| // if |video_frame|'s stride matches VEA's requirement, |video_frame|'s |
| // memory backing (heap, base::UnsafeSharedMemoryRegion, etc.) could be |
| // something VEA can't handle (as of this writing, it expects an unsafe |
| // region). |
| // |
| // TODO(crbug.com/888829): Revisit whether we can remove this memcpy, if VEA |
| // can accept other "memory backing" methods. |
| scoped_refptr<media::VideoFrame> frame = video_frame; |
| if (video_frame->coded_size() != frame_coded_size_ || |
| video_frame->storage_type() != |
| media::VideoFrame::StorageType::STORAGE_SHMEM) { |
| const int index = free_input_buffer_index_.back(); |
| std::pair<base::UnsafeSharedMemoryRegion, |
| base::WritableSharedMemoryMapping>* input_buffer = |
| input_buffers_[index].get(); |
| DCHECK(input_buffer->first.IsValid()); |
| DCHECK(input_buffer->second.IsValid()); |
| frame = VideoFrame::WrapExternalData( |
| video_frame->format(), frame_coded_size_, video_frame->visible_rect(), |
| video_frame->visible_rect().size(), |
| input_buffer->second.GetMemoryAsSpan<uint8_t>().data(), |
| input_buffer->second.size(), video_frame->timestamp()); |
| if (!frame || !media::I420CopyWithPadding(*video_frame, frame.get())) { |
| LOG(DFATAL) << "Error: ExternalVideoEncoder: copy failed."; |
| AbortLatestEncodeAttemptDueToErrors(); |
| return; |
| } |
| frame->BackWithSharedMemory(&input_buffer->first); |
| |
| frame->AddDestructionObserver(media::BindToCurrentLoop(base::BindOnce( |
| &ExternalVideoEncoder::VEAClientImpl::ReturnInputBufferToPool, this, |
| index))); |
| free_input_buffer_index_.pop_back(); |
| } |
| // BitstreamBufferReady will be called once the encoder is done. |
| video_encode_accelerator_->Encode(std::move(frame), key_frame_requested); |
| } |
| |
| protected: |
| void NotifyError(VideoEncodeAccelerator::Error error) final { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| DCHECK(error != VideoEncodeAccelerator::kInvalidArgumentError && |
| error != VideoEncodeAccelerator::kIllegalStateError); |
| |
| encoder_active_ = false; |
| |
| cast_environment_->PostTask( |
| CastEnvironment::MAIN, FROM_HERE, |
| base::BindOnce(status_change_cb_, STATUS_CODEC_RUNTIME_ERROR)); |
| |
| // TODO(crbug.com/1199930): Force-flush all |in_progress_frame_encodes_| |
| // immediately so pending frames do not become stuck, freezing VideoSender. |
| } |
| |
| void AllocateInputBuffer(size_t size) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| auto memory = base::UnsafeSharedMemoryRegion::Create(size); |
| if (memory.IsValid()) { |
| base::WritableSharedMemoryMapping mapping = memory.Map(); |
| DCHECK(mapping.IsValid()); |
| input_buffers_.push_back( |
| std::make_unique<std::pair<base::UnsafeSharedMemoryRegion, |
| base::WritableSharedMemoryMapping>>( |
| std::move(memory), std::move(mapping))); |
| free_input_buffer_index_.push_back(input_buffers_.size() - 1); |
| } |
| allocate_input_buffer_in_progress_ = false; |
| } |
| |
| void AllocateOutputBuffers(size_t size) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| for (size_t i = 0; i < kOutputBufferCount; ++i) { |
| auto memory = base::UnsafeSharedMemoryRegion::Create(size); |
| base::WritableSharedMemoryMapping mapping = memory.Map(); |
| DCHECK(mapping.IsValid()); |
| output_buffers_.push_back( |
| std::make_pair(std::move(memory), std::move(mapping))); |
| |
| video_encode_accelerator_->UseOutputBitstreamBuffer( |
| media::BitstreamBuffer(static_cast<int32_t>(i), |
| output_buffers_[i].first.Duplicate(), |
| output_buffers_[i].first.GetSize())); |
| } |
| } |
| |
| // Called by the VEA to indicate its buffer requirements. |
| void RequireBitstreamBuffers(unsigned int input_count, |
| const gfx::Size& input_coded_size, |
| size_t output_buffer_size) final { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| frame_coded_size_ = input_coded_size; |
| max_allowed_input_buffers_ = input_count + kExtraInputBufferCount; |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&VEAClientImpl::AllocateOutputBuffers, this, |
| output_buffer_size)); |
| } |
| |
| // Encoder has encoded a frame and it's available in one of the output |
| // buffers. Package the result in a media::cast::EncodedFrame and post it |
| // to the Cast MAIN thread via the supplied callback. |
| void BitstreamBufferReady(int32_t bitstream_buffer_id, |
| const BitstreamBufferMetadata& metadata) final { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| if (bitstream_buffer_id < 0 || |
| bitstream_buffer_id >= static_cast<int32_t>(output_buffers_.size())) { |
| NOTREACHED(); |
| VLOG(1) << "BitstreamBufferReady(): invalid bitstream_buffer_id=" |
| << bitstream_buffer_id; |
| NotifyError(media::VideoEncodeAccelerator::kPlatformFailureError); |
| return; |
| } |
| const char* output_buffer_memory = output_buffers_[bitstream_buffer_id] |
| .second.GetMemoryAsSpan<char>() |
| .data(); |
| if (metadata.payload_size_bytes > |
| output_buffers_[bitstream_buffer_id].second.size()) { |
| NOTREACHED(); |
| VLOG(1) << "BitstreamBufferReady(): invalid payload_size = " |
| << metadata.payload_size_bytes; |
| NotifyError(media::VideoEncodeAccelerator::kPlatformFailureError); |
| return; |
| } |
| if (metadata.key_frame) { |
| key_frame_encountered_ = true; |
| } |
| if (!key_frame_encountered_) { |
| // Do not send video until we have encountered the first key frame. |
| // Save the bitstream buffer in |stream_header_| to be sent later along |
| // with the first key frame. |
| stream_header_.write(output_buffer_memory, metadata.payload_size_bytes); |
| } else if (!in_progress_frame_encodes_.empty()) { |
| InProgressExternalVideoFrameEncode& request = |
| in_progress_frame_encodes_.front(); |
| |
| std::unique_ptr<SenderEncodedFrame> encoded_frame( |
| new SenderEncodedFrame()); |
| encoded_frame->dependency = |
| metadata.key_frame ? EncodedFrame::KEY : EncodedFrame::DEPENDENT; |
| encoded_frame->frame_id = next_frame_id_++; |
| if (metadata.key_frame) { |
| encoded_frame->referenced_frame_id = encoded_frame->frame_id; |
| } else { |
| encoded_frame->referenced_frame_id = encoded_frame->frame_id - 1; |
| } |
| encoded_frame->rtp_timestamp = RtpTimeTicks::FromTimeDelta( |
| request.video_frame->timestamp(), kVideoFrequency); |
| encoded_frame->reference_time = request.reference_time; |
| |
| std::string header = stream_header_.str(); |
| if (!header.empty()) { |
| encoded_frame->data = std::move(header); |
| std::ostringstream().swap(stream_header_); |
| } |
| encoded_frame->data.append(output_buffer_memory, |
| metadata.payload_size_bytes); |
| DCHECK(!encoded_frame->data.empty()) << "BUG: Encoder must provide data."; |
| |
| // If FRAME_DURATION metadata was provided in the source VideoFrame, |
| // compute the utilization metrics. |
| base::TimeDelta frame_duration = |
| request.video_frame->metadata().frame_duration.value_or( |
| base::TimeDelta()); |
| if (frame_duration > base::TimeDelta()) { |
| // Compute encoder utilization in terms of the number of frames in |
| // backlog, including the current frame encode that is finishing |
| // here. This "backlog" model works as follows: First, assume that all |
| // frames utilize the encoder by the same amount. This is actually a |
| // false assumption, but it still works well because any frame that |
| // takes longer to encode will naturally cause the backlog to |
| // increase, and this will result in a higher computed utilization for |
| // the offending frame. If the backlog continues to increase, because |
| // the following frames are also taking too long to encode, the |
| // computed utilization for each successive frame will be higher. At |
| // some point, upstream control logic will decide that the data volume |
| // must be reduced. |
| encoded_frame->encoder_utilization = |
| static_cast<double>(in_progress_frame_encodes_.size()) / |
| kBacklogRedlineThreshold; |
| |
| const double actual_bit_rate = |
| encoded_frame->data.size() * 8.0 / frame_duration.InSecondsF(); |
| DCHECK_GT(request.target_bit_rate, 0); |
| const double bitrate_utilization = |
| actual_bit_rate / request.target_bit_rate; |
| double quantizer = QuantizerEstimator::NO_RESULT; |
| // If the quantizer can be parsed from the key frame, try to parse |
| // the following delta frames as well. |
| // Otherwise, switch back to entropy estimation for the key frame |
| // and all the following delta frames. |
| if (metadata.key_frame || key_frame_quantizer_parsable_) { |
| if (codec_profile_ == media::VP8PROFILE_ANY) { |
| quantizer = ParseVpxHeaderQuantizer( |
| reinterpret_cast<const uint8_t*>(encoded_frame->data.data()), |
| encoded_frame->data.size()); |
| } else if (codec_profile_ == media::H264PROFILE_MAIN) { |
| quantizer = GetH264FrameQuantizer( |
| reinterpret_cast<const uint8_t*>(encoded_frame->data.data()), |
| encoded_frame->data.size()); |
| } else { |
| NOTIMPLEMENTED(); |
| } |
| if (quantizer < 0) { |
| LOG(ERROR) << "Unable to parse quantizer from encoded " |
| << (metadata.key_frame ? "key" : "delta") |
| << " frame, id=" << encoded_frame->frame_id; |
| if (metadata.key_frame) { |
| key_frame_quantizer_parsable_ = false; |
| quantizer = quantizer_estimator_.EstimateForKeyFrame( |
| *request.video_frame); |
| } |
| } else { |
| if (metadata.key_frame) { |
| key_frame_quantizer_parsable_ = true; |
| } |
| } |
| } else { |
| quantizer = |
| quantizer_estimator_.EstimateForDeltaFrame(*request.video_frame); |
| } |
| if (quantizer >= 0) { |
| const double max_quantizer = |
| codec_profile_ == media::VP8PROFILE_ANY |
| ? static_cast<int>(QuantizerEstimator::MAX_VP8_QUANTIZER) |
| : static_cast<int>(kMaxH264Quantizer); |
| encoded_frame->lossy_utilization = |
| bitrate_utilization * (quantizer / max_quantizer); |
| } |
| } else { |
| quantizer_estimator_.Reset(); |
| } |
| |
| encoded_frame->encode_completion_time = |
| cast_environment_->Clock()->NowTicks(); |
| cast_environment_->PostTask( |
| CastEnvironment::MAIN, FROM_HERE, |
| base::BindOnce(std::move(request.frame_encoded_callback), |
| std::move(encoded_frame))); |
| |
| in_progress_frame_encodes_.pop_front(); |
| } else { |
| VLOG(1) << "BitstreamBufferReady(): no encoded frame data available"; |
| } |
| |
| // We need to re-add the output buffer to the encoder after we are done |
| // with it. |
| if (encoder_active_) { |
| video_encode_accelerator_->UseOutputBitstreamBuffer( |
| media::BitstreamBuffer( |
| bitstream_buffer_id, |
| output_buffers_[bitstream_buffer_id].first.Duplicate(), |
| output_buffers_[bitstream_buffer_id].first.GetSize())); |
| } |
| } |
| |
| private: |
| friend class base::RefCountedThreadSafe<VEAClientImpl>; |
| |
| ~VEAClientImpl() final { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| while (!in_progress_frame_encodes_.empty()) |
| AbortLatestEncodeAttemptDueToErrors(); |
| |
| // According to the media::VideoEncodeAccelerator interface, Destroy() |
| // should be called instead of invoking its private destructor. |
| if (video_encode_accelerator_) { |
| video_encode_accelerator_.release()->Destroy(); |
| } |
| } |
| |
| // This is called when an error occurs while preparing a VideoFrame for |
| // encode, or to abort a frame encode when shutting down. |
| void AbortLatestEncodeAttemptDueToErrors() { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| |
| std::unique_ptr<SenderEncodedFrame> no_result(nullptr); |
| cast_environment_->PostTask( |
| CastEnvironment::MAIN, FROM_HERE, |
| base::BindOnce( |
| std::move(in_progress_frame_encodes_.back().frame_encoded_callback), |
| std::move(no_result))); |
| in_progress_frame_encodes_.pop_back(); |
| } |
| |
| // Parse H264 SPS, PPS, and Slice header, and return the averaged frame |
| // quantizer in the range of [0, 51], or -1 on parse error. |
| double GetH264FrameQuantizer(const uint8_t* encoded_data, off_t size) { |
| DCHECK(task_runner_->RunsTasksInCurrentSequence()); |
| DCHECK(encoded_data); |
| |
| if (!size) { |
| return -1; |
| } |
| h264_parser_.SetStream(encoded_data, size); |
| double total_quantizer = 0; |
| int num_slices = 0; |
| |
| while (true) { |
| H264NALU nalu; |
| H264Parser::Result res = h264_parser_.AdvanceToNextNALU(&nalu); |
| if (res == H264Parser::kEOStream) { |
| break; |
| } |
| if (res != H264Parser::kOk) { |
| return -1; |
| } |
| switch (nalu.nal_unit_type) { |
| case H264NALU::kIDRSlice: |
| case H264NALU::kNonIDRSlice: { |
| H264SliceHeader slice_header; |
| if (h264_parser_.ParseSliceHeader(nalu, &slice_header) != |
| H264Parser::kOk) |
| return -1; |
| const H264PPS* pps = |
| h264_parser_.GetPPS(slice_header.pic_parameter_set_id); |
| if (!pps) { |
| return -1; |
| } |
| ++num_slices; |
| int slice_quantizer = |
| 26 + |
| ((slice_header.IsSPSlice() || slice_header.IsSISlice()) |
| ? pps->pic_init_qs_minus26 + slice_header.slice_qs_delta |
| : pps->pic_init_qp_minus26 + slice_header.slice_qp_delta); |
| DCHECK_GE(slice_quantizer, 0); |
| DCHECK_LE(slice_quantizer, kMaxH264Quantizer); |
| total_quantizer += slice_quantizer; |
| break; |
| } |
| case H264NALU::kSPS: { |
| int id; |
| if (h264_parser_.ParseSPS(&id) != H264Parser::kOk) { |
| return -1; |
| } |
| break; |
| } |
| case H264NALU::kPPS: { |
| int id; |
| if (h264_parser_.ParsePPS(&id) != H264Parser::kOk) { |
| return -1; |
| } |
| break; |
| } |
| default: |
| // Skip other NALUs. |
| break; |
| } |
| } |
| return (num_slices == 0) ? -1 : (total_quantizer / num_slices); |
| } |
| |
| const scoped_refptr<CastEnvironment> cast_environment_; |
| const scoped_refptr<base::SingleThreadTaskRunner> task_runner_; |
| const double max_frame_rate_; |
| const StatusChangeCallback status_change_cb_; // Must be run on MAIN thread. |
| std::unique_ptr<media::VideoEncodeAccelerator> video_encode_accelerator_; |
| bool encoder_active_; |
| FrameId next_frame_id_; |
| bool key_frame_encountered_; |
| std::ostringstream stream_header_; |
| VideoCodecProfile codec_profile_; |
| bool key_frame_quantizer_parsable_; |
| H264Parser h264_parser_; |
| |
| // Shared memory buffers for output with the VideoAccelerator. |
| std::vector<std::pair<base::UnsafeSharedMemoryRegion, |
| base::WritableSharedMemoryMapping>> |
| output_buffers_; |
| |
| // Shared memory buffers for input video frames with the VideoAccelerator. |
| // These buffers will be allocated only when copy is needed to match the |
| // required coded size for encoder. They are allocated on-demand, up to |
| // |max_allowed_input_buffers_|. A VideoFrame wrapping the region will point |
| // to it, so std::unique_ptr is used to ensure the region has a stable address |
| // even if the vector grows or shrinks. |
| std::vector<std::unique_ptr<std::pair<base::UnsafeSharedMemoryRegion, |
| base::WritableSharedMemoryMapping>>> |
| input_buffers_; |
| |
| // Available input buffer index. These buffers are used in FILO order. |
| std::vector<int> free_input_buffer_index_; |
| |
| // FIFO list. |
| std::list<InProgressExternalVideoFrameEncode> in_progress_frame_encodes_; |
| |
| // The requested encode bit rate for the next frame. |
| int requested_bit_rate_; |
| |
| // Used to compute utilization metrics for each frame. |
| QuantizerEstimator quantizer_estimator_; |
| |
| // The coded size of the video frame required by Encoder. This size is |
| // obtained from VEA through |RequireBitstreamBuffers()|. |
| gfx::Size frame_coded_size_; |
| |
| // The maximum number of input buffers. These buffers are used to copy |
| // VideoFrames in order to match the required coded size for encoder. |
| size_t max_allowed_input_buffers_; |
| |
| // Set to true when the allocation of an input buffer is in progress, and |
| // reset to false after the allocated buffer is received. |
| bool allocate_input_buffer_in_progress_; |
| |
| DISALLOW_COPY_AND_ASSIGN(VEAClientImpl); |
| }; |
| |
| // static |
| bool ExternalVideoEncoder::IsSupported(const FrameSenderConfig& video_config) { |
| if (video_config.codec != CODEC_VIDEO_VP8 && |
| video_config.codec != CODEC_VIDEO_H264) |
| return false; |
| |
| // We assume that the system provides a hardware encoder at this point. |
| return video_config.use_external_encoder; |
| } |
| |
| ExternalVideoEncoder::ExternalVideoEncoder( |
| const scoped_refptr<CastEnvironment>& cast_environment, |
| const FrameSenderConfig& video_config, |
| const gfx::Size& frame_size, |
| FrameId first_frame_id, |
| StatusChangeCallback status_change_cb, |
| const CreateVideoEncodeAcceleratorCallback& create_vea_cb) |
| : cast_environment_(cast_environment), |
| frame_size_(frame_size), |
| bit_rate_(video_config.start_bitrate) { |
| DCHECK(cast_environment_->CurrentlyOn(CastEnvironment::MAIN)); |
| DCHECK_GT(video_config.max_frame_rate, 0); |
| DCHECK(!frame_size_.IsEmpty()); |
| DCHECK(status_change_cb); |
| DCHECK(create_vea_cb); |
| DCHECK_GT(bit_rate_, 0); |
| |
| create_vea_cb.Run( |
| base::BindOnce(&ExternalVideoEncoder::OnCreateVideoEncodeAccelerator, |
| weak_factory_.GetWeakPtr(), video_config, first_frame_id, |
| std::move(status_change_cb))); |
| } |
| |
| ExternalVideoEncoder::~ExternalVideoEncoder() { |
| DCHECK(cast_environment_->CurrentlyOn(CastEnvironment::MAIN)); |
| DestroyClientSoon(); |
| } |
| |
| void ExternalVideoEncoder::DestroyClientSoon() { |
| DCHECK(cast_environment_->CurrentlyOn(CastEnvironment::MAIN)); |
| // Ensure |client_| is destroyed from the encoder task runner by dropping the |
| // reference to it within an encoder task. |
| if (client_) { |
| client_->task_runner()->PostTask( |
| FROM_HERE, base::BindOnce([](scoped_refptr<VEAClientImpl> client) {}, |
| std::move(client_))); |
| } |
| } |
| |
| bool ExternalVideoEncoder::EncodeVideoFrame( |
| scoped_refptr<media::VideoFrame> video_frame, |
| base::TimeTicks reference_time, |
| FrameEncodedCallback frame_encoded_callback) { |
| DCHECK(cast_environment_->CurrentlyOn(CastEnvironment::MAIN)); |
| DCHECK(!frame_encoded_callback.is_null()); |
| |
| if (!client_ || video_frame->visible_rect().size() != frame_size_) { |
| return false; |
| } |
| |
| client_->task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&VEAClientImpl::EncodeVideoFrame, client_, |
| std::move(video_frame), reference_time, |
| key_frame_requested_, std::move(frame_encoded_callback))); |
| key_frame_requested_ = false; |
| return true; |
| } |
| |
| void ExternalVideoEncoder::SetBitRate(int new_bit_rate) { |
| DCHECK(cast_environment_->CurrentlyOn(CastEnvironment::MAIN)); |
| DCHECK_GT(new_bit_rate, 0); |
| |
| bit_rate_ = new_bit_rate; |
| if (!client_) { |
| return; |
| } |
| client_->task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&VEAClientImpl::SetBitRate, client_, bit_rate_)); |
| } |
| |
| void ExternalVideoEncoder::GenerateKeyFrame() { |
| DCHECK(cast_environment_->CurrentlyOn(CastEnvironment::MAIN)); |
| key_frame_requested_ = true; |
| } |
| |
| void ExternalVideoEncoder::OnCreateVideoEncodeAccelerator( |
| const FrameSenderConfig& video_config, |
| FrameId first_frame_id, |
| const StatusChangeCallback& status_change_cb, |
| scoped_refptr<base::SingleThreadTaskRunner> encoder_task_runner, |
| std::unique_ptr<media::VideoEncodeAccelerator> vea) { |
| DCHECK(cast_environment_->CurrentlyOn(CastEnvironment::MAIN)); |
| |
| // The callback will be invoked with null pointers in the case where the |
| // system does not support or lacks the resources to provide GPU-accelerated |
| // video encoding. |
| if (!encoder_task_runner || !vea) { |
| cast_environment_->PostTask( |
| CastEnvironment::MAIN, FROM_HERE, |
| base::BindOnce(status_change_cb, STATUS_CODEC_INIT_FAILED)); |
| return; |
| } |
| |
| VideoCodecProfile codec_profile; |
| switch (video_config.codec) { |
| case CODEC_VIDEO_VP8: |
| codec_profile = media::VP8PROFILE_ANY; |
| break; |
| case CODEC_VIDEO_H264: |
| codec_profile = media::H264PROFILE_MAIN; |
| break; |
| case CODEC_VIDEO_FAKE: |
| NOTREACHED() << "Fake software video encoder cannot be external"; |
| FALLTHROUGH; |
| default: |
| cast_environment_->PostTask( |
| CastEnvironment::MAIN, FROM_HERE, |
| base::BindOnce(status_change_cb, STATUS_UNSUPPORTED_CODEC)); |
| return; |
| } |
| |
| // Create a callback that wraps the StatusChangeCallback. It monitors when a |
| // fatal error occurs and schedules destruction of the VEAClientImpl. |
| StatusChangeCallback wrapped_status_change_cb = base::BindRepeating( |
| [](base::WeakPtr<ExternalVideoEncoder> self, |
| const StatusChangeCallback& status_change_cb, |
| OperationalStatus status) { |
| if (self.get()) { |
| switch (status) { |
| case STATUS_UNINITIALIZED: |
| case STATUS_INITIALIZED: |
| case STATUS_CODEC_REINIT_PENDING: |
| break; |
| |
| case STATUS_INVALID_CONFIGURATION: |
| case STATUS_UNSUPPORTED_CODEC: |
| case STATUS_CODEC_INIT_FAILED: |
| case STATUS_CODEC_RUNTIME_ERROR: |
| // Something bad happened. Destroy the client to: 1) fail-out any |
| // currently in-progress frame encodes; and 2) prevent future |
| // EncodeVideoFrame() calls from queuing frames indefinitely. |
| self->DestroyClientSoon(); |
| break; |
| } |
| } |
| status_change_cb.Run(status); |
| }, |
| weak_factory_.GetWeakPtr(), status_change_cb); |
| |
| DCHECK(!client_); |
| client_ = new VEAClientImpl(cast_environment_, encoder_task_runner, |
| std::move(vea), video_config.max_frame_rate, |
| std::move(wrapped_status_change_cb)); |
| client_->task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&VEAClientImpl::Initialize, client_, frame_size_, |
| codec_profile, bit_rate_, first_frame_id)); |
| } |
| |
| SizeAdaptableExternalVideoEncoder::SizeAdaptableExternalVideoEncoder( |
| const scoped_refptr<CastEnvironment>& cast_environment, |
| const FrameSenderConfig& video_config, |
| StatusChangeCallback status_change_cb, |
| const CreateVideoEncodeAcceleratorCallback& create_vea_cb) |
| : SizeAdaptableVideoEncoderBase(cast_environment, |
| video_config, |
| std::move(status_change_cb)), |
| create_vea_cb_(create_vea_cb) {} |
| |
| SizeAdaptableExternalVideoEncoder::~SizeAdaptableExternalVideoEncoder() = |
| default; |
| |
| std::unique_ptr<VideoEncoder> |
| SizeAdaptableExternalVideoEncoder::CreateEncoder() { |
| return std::make_unique<ExternalVideoEncoder>( |
| cast_environment(), video_config(), frame_size(), next_frame_id(), |
| CreateEncoderStatusChangeCallback(), create_vea_cb_); |
| } |
| |
| QuantizerEstimator::QuantizerEstimator() = default; |
| |
| QuantizerEstimator::~QuantizerEstimator() = default; |
| |
| void QuantizerEstimator::Reset() { |
| last_frame_pixel_buffer_.reset(); |
| } |
| |
| double QuantizerEstimator::EstimateForKeyFrame(const VideoFrame& frame) { |
| if (!CanExamineFrame(frame)) { |
| return NO_RESULT; |
| } |
| |
| // If the size of the frame is different from the last frame, allocate a new |
| // buffer. The buffer only needs to be a fraction of the size of the entire |
| // frame, since the entropy analysis only examines a subset of each frame. |
| const gfx::Size size = frame.visible_rect().size(); |
| const int rows_in_subset = |
| std::max(1, size.height() * kFrameSamplingPercentage / 100); |
| if (last_frame_size_ != size || !last_frame_pixel_buffer_) { |
| last_frame_pixel_buffer_.reset(new uint8_t[size.width() * rows_in_subset]); |
| last_frame_size_ = size; |
| } |
| |
| // Compute a histogram where each bucket represents the number of times two |
| // neighboring pixels were different by a specific amount. |
| std::array<int, kQuantizationHistogramSize> histogram{}; |
| const int row_skip = size.height() / rows_in_subset; |
| int y = 0; |
| for (int i = 0; i < rows_in_subset; ++i, y += row_skip) { |
| const uint8_t* const row_begin = frame.visible_data(VideoFrame::kYPlane) + |
| y * frame.stride(VideoFrame::kYPlane); |
| const uint8_t* const row_end = row_begin + size.width(); |
| int left_hand_pixel_value = static_cast<int>(*row_begin); |
| for (const uint8_t* p = row_begin + 1; p < row_end; ++p) { |
| const int right_hand_pixel_value = static_cast<int>(*p); |
| const int difference = right_hand_pixel_value - left_hand_pixel_value; |
| const int histogram_index = difference + 255; |
| ++histogram[histogram_index]; |
| left_hand_pixel_value = right_hand_pixel_value; // For next iteration. |
| } |
| |
| // Copy the row of pixels into the buffer. This will be used when |
| // generating histograms for future delta frames. |
| memcpy(last_frame_pixel_buffer_.get() + i * size.width(), |
| row_begin, |
| size.width()); |
| } |
| |
| // Estimate a quantizer value depending on the difference data in the |
| // histogram and return it. |
| const int num_samples = (size.width() - 1) * rows_in_subset; |
| return ToQuantizerEstimate(ComputeEntropyFromHistogram( |
| histogram.data(), histogram.size(), num_samples)); |
| } |
| |
| double QuantizerEstimator::EstimateForDeltaFrame(const VideoFrame& frame) { |
| if (!CanExamineFrame(frame)) { |
| return NO_RESULT; |
| } |
| |
| // If the size of the |frame| has changed, no difference can be examined. |
| // In this case, process this frame as if it were a key frame. |
| const gfx::Size& size = frame.visible_rect().size(); |
| if (last_frame_size_ != size || !last_frame_pixel_buffer_) { |
| return EstimateForKeyFrame(frame); |
| } |
| const int rows_in_subset = |
| std::max(1, size.height() * (kFrameSamplingPercentage / 100)); |
| |
| // Compute a histogram where each bucket represents the number of times the |
| // same pixel in this frame versus the last frame was different by a specific |
| // amount. |
| std::array<int, kQuantizationHistogramSize> histogram{}; |
| const int row_skip = size.height() / rows_in_subset; |
| int y = 0; |
| for (int i = 0; i < rows_in_subset; ++i, y += row_skip) { |
| const uint8_t* const row_begin = frame.visible_data(VideoFrame::kYPlane) + |
| y * frame.stride(VideoFrame::kYPlane); |
| const uint8_t* const row_end = row_begin + size.width(); |
| uint8_t* const last_frame_row_begin = |
| last_frame_pixel_buffer_.get() + i * size.width(); |
| for (const uint8_t *p = row_begin, *q = last_frame_row_begin; p < row_end; |
| ++p, ++q) { |
| const int difference = static_cast<int>(*p) - static_cast<int>(*q); |
| const int histogram_index = difference + 255; |
| ++histogram[histogram_index]; |
| } |
| |
| // Copy the row of pixels into the buffer. This will be used when |
| // generating histograms for future delta frames. |
| memcpy(last_frame_row_begin, row_begin, size.width()); |
| } |
| |
| // Estimate a quantizer value depending on the difference data in the |
| // histogram and return it. |
| const int num_samples = size.width() * rows_in_subset; |
| return ToQuantizerEstimate(ComputeEntropyFromHistogram( |
| histogram.data(), histogram.size(), num_samples)); |
| } |
| |
| // static |
| bool QuantizerEstimator::CanExamineFrame(const VideoFrame& frame) { |
| DCHECK_EQ(8, VideoFrame::PlaneHorizontalBitsPerPixel(frame.format(), |
| VideoFrame::kYPlane)); |
| return media::IsYuvPlanar(frame.format()) && |
| !frame.visible_rect().IsEmpty(); |
| } |
| |
| // static |
| double QuantizerEstimator::ComputeEntropyFromHistogram(const int* histogram, |
| size_t histogram_size, |
| int num_samples) { |
| DCHECK_LT(0, num_samples); |
| double entropy = 0.0; |
| for (size_t i = 0; i < histogram_size; ++i) { |
| const double probability = static_cast<double>(histogram[i]) / num_samples; |
| if (probability > 0.0) { |
| entropy = entropy - probability * std::log2(probability); |
| } |
| } |
| return entropy; |
| } |
| |
| // static |
| double QuantizerEstimator::ToQuantizerEstimate(double shannon_entropy) { |
| DCHECK_GE(shannon_entropy, 0.0); |
| |
| // This math is based on an analysis of data produced by running a wide range |
| // of mirroring content in a Cast streaming session on a Chromebook Pixel |
| // (2013 edition). The output from the Pixel's built-in hardware encoder was |
| // compared to an identically-configured software implementation (libvpx) |
| // running alongside. Based on an analysis of the data, the following linear |
| // mapping seems to produce reasonable VP8 quantizer values from the |
| // |shannon_entropy| values. |
| constexpr double kEntropyAtMaxQuantizer = 7.5; |
| constexpr double kSlope = |
| (MAX_VP8_QUANTIZER - MIN_VP8_QUANTIZER) / kEntropyAtMaxQuantizer; |
| const double quantizer = std::min<double>( |
| MAX_VP8_QUANTIZER, MIN_VP8_QUANTIZER + kSlope * shannon_entropy); |
| return quantizer; |
| } |
| |
| } // namespace cast |
| } // namespace media |