| // Copyright 2013 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/gpu/android/android_video_encode_accelerator.h" |
| |
| #include <memory> |
| #include <set> |
| #include <tuple> |
| |
| #include "base/functional/bind.h" |
| #include "base/location.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/task/sequenced_task_runner.h" |
| #include "gpu/command_buffer/service/gles2_cmd_decoder.h" |
| #include "gpu/ipc/service/gpu_channel.h" |
| #include "media/base/android/media_codec_util.h" |
| #include "media/base/bitstream_buffer.h" |
| #include "media/base/limits.h" |
| #include "media/base/media_log.h" |
| #include "media/video/picture.h" |
| #include "third_party/libyuv/include/libyuv/convert_from.h" |
| #include "ui/gl/android/scoped_java_surface.h" |
| #include "ui/gl/gl_bindings.h" |
| |
| namespace media { |
| |
| // Limit default max video codec size for Android to avoid |
| // HW codec initialization failure for resolution higher than 720p. |
| // Default values are from Libjingle "jsepsessiondescription.cc". |
| const int kMaxEncodeFrameWidth = 1280; |
| const int kMaxEncodeFrameHeight = 720; |
| const int kMaxFramerateNumerator = 30; |
| const int kMaxFramerateDenominator = 1; |
| |
| enum PixelFormat { |
| // Subset of MediaCodecInfo.CodecCapabilities. |
| COLOR_FORMAT_YUV420_PLANAR = 19, |
| COLOR_FORMAT_YUV420_SEMIPLANAR = 21, |
| }; |
| |
| // Because MediaCodec is thread-hostile (must be poked on a single thread) and |
| // has no callback mechanism (b/11990118), we must drive it by polling for |
| // complete frames (and available input buffers, when the codec is fully |
| // saturated). This function defines the polling delay. The value used is an |
| // arbitrary choice that trades off CPU utilization (spinning) against latency. |
| // Mirrors android_video_decode_accelerator.cc::DecodePollDelay(). |
| static inline const base::TimeDelta EncodePollDelay() { |
| // An alternative to this polling scheme could be to dedicate a new thread |
| // (instead of using the ChildThread) to run the MediaCodec, and make that |
| // thread use the timeout-based flavor of MediaCodec's dequeue methods when it |
| // believes the codec should complete "soon" (e.g. waiting for an input |
| // buffer, or waiting for a picture when it knows enough complete input |
| // pictures have been fed to saturate any internal buffering). This is |
| // speculative and it's unclear that this would be a win (nor that there's a |
| // reasonably device-agnostic way to fill in the "believes" above). |
| return base::Milliseconds(10); |
| } |
| |
| static inline const base::TimeDelta NoWaitTimeOut() { |
| return base::Microseconds(0); |
| } |
| |
| static bool GetSupportedColorFormatForMime(const std::string& mime, |
| PixelFormat* pixel_format) { |
| if (mime.empty()) |
| return false; |
| |
| std::set<int> formats = MediaCodecUtil::GetEncoderColorFormats(mime); |
| if (formats.count(COLOR_FORMAT_YUV420_SEMIPLANAR) > 0) |
| *pixel_format = COLOR_FORMAT_YUV420_SEMIPLANAR; |
| else if (formats.count(COLOR_FORMAT_YUV420_PLANAR) > 0) |
| *pixel_format = COLOR_FORMAT_YUV420_PLANAR; |
| else |
| return false; |
| |
| return true; |
| } |
| |
| AndroidVideoEncodeAccelerator::AndroidVideoEncodeAccelerator() = default; |
| |
| AndroidVideoEncodeAccelerator::~AndroidVideoEncodeAccelerator() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| VideoEncodeAccelerator::SupportedProfiles |
| AndroidVideoEncodeAccelerator::GetSupportedProfiles() { |
| SupportedProfiles profiles; |
| |
| const struct { |
| const VideoCodec codec; |
| const VideoCodecProfile profile; |
| } kSupportedCodecs[] = {{VideoCodec::kVP8, VP8PROFILE_ANY}, |
| {VideoCodec::kH264, H264PROFILE_BASELINE}}; |
| |
| for (const auto& supported_codec : kSupportedCodecs) { |
| if (supported_codec.codec == VideoCodec::kVP8 && |
| !MediaCodecUtil::IsVp8EncoderAvailable()) { |
| continue; |
| } |
| |
| if (supported_codec.codec == VideoCodec::kH264 && |
| !MediaCodecUtil::IsH264EncoderAvailable()) { |
| continue; |
| } |
| |
| if (MediaCodecUtil::IsKnownUnaccelerated(supported_codec.codec, |
| MediaCodecDirection::ENCODER)) { |
| continue; |
| } |
| |
| SupportedProfile profile; |
| profile.profile = supported_codec.profile; |
| // It would be nice if MediaCodec exposes the maximum capabilities of |
| // the encoder. Hard-code some reasonable defaults as workaround. |
| profile.max_resolution.SetSize(kMaxEncodeFrameWidth, kMaxEncodeFrameHeight); |
| profile.max_framerate_numerator = kMaxFramerateNumerator; |
| profile.max_framerate_denominator = kMaxFramerateDenominator; |
| profile.rate_control_modes = media::VideoEncodeAccelerator::kConstantMode; |
| profiles.push_back(profile); |
| } |
| return profiles; |
| } |
| |
| bool AndroidVideoEncodeAccelerator::Initialize( |
| const Config& config, |
| Client* client, |
| std::unique_ptr<MediaLog> media_log) { |
| DVLOG(3) << __func__ << " " << config.AsHumanReadableString(); |
| DCHECK(!media_codec_); |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(client); |
| log_ = std::move(media_log); |
| client_ptr_factory_ = std::make_unique<base::WeakPtrFactory<Client>>(client); |
| |
| if (config.input_format != PIXEL_FORMAT_I420) { |
| MEDIA_LOG(ERROR, log_) << "Unexpected combo: " << config.input_format |
| << ", " << GetProfileName(config.output_profile); |
| return false; |
| } |
| |
| std::string mime_type; |
| VideoCodec codec; |
| // The client should be prepared to feed at least this many frames into the |
| // encoder before being returned any output frames, since the encoder may |
| // need to hold onto some subset of inputs as reference pictures. |
| uint32_t frame_input_count; |
| uint32_t i_frame_interval; |
| if (config.output_profile == VP8PROFILE_ANY) { |
| codec = VideoCodec::kVP8; |
| mime_type = "video/x-vnd.on2.vp8"; |
| frame_input_count = 1; |
| i_frame_interval = IFRAME_INTERVAL_VPX; |
| } else if (config.output_profile == H264PROFILE_BASELINE || |
| config.output_profile == H264PROFILE_MAIN) { |
| codec = VideoCodec::kH264; |
| mime_type = "video/avc"; |
| frame_input_count = 30; |
| i_frame_interval = IFRAME_INTERVAL_H264; |
| } else { |
| return false; |
| } |
| |
| frame_size_ = config.input_visible_size; |
| last_set_bitrate_ = config.bitrate.target_bps(); |
| |
| // Only consider using MediaCodec if it's likely backed by hardware. |
| if (MediaCodecUtil::IsKnownUnaccelerated(codec, |
| MediaCodecDirection::ENCODER)) { |
| MEDIA_LOG(ERROR, log_) << "No HW support"; |
| return false; |
| } |
| |
| PixelFormat pixel_format = COLOR_FORMAT_YUV420_SEMIPLANAR; |
| if (!GetSupportedColorFormatForMime(mime_type, &pixel_format)) { |
| MEDIA_LOG(ERROR, log_) << "No color format support."; |
| return false; |
| } |
| media_codec_ = MediaCodecBridgeImpl::CreateVideoEncoder( |
| codec, config.input_visible_size, config.bitrate.target_bps(), |
| INITIAL_FRAMERATE, i_frame_interval, pixel_format); |
| |
| if (!media_codec_) { |
| MEDIA_LOG(ERROR, log_) << "Failed to create/start the codec: " |
| << config.input_visible_size.ToString(); |
| return false; |
| } |
| |
| if (!SetInputBufferLayout()) { |
| MEDIA_LOG(ERROR, log_) << "Can't get input buffer layout from MediaCodec"; |
| return false; |
| } |
| |
| // Conservative upper bound for output buffer size: decoded size + 2KB. |
| const size_t output_buffer_capacity = |
| VideoFrame::AllocationSize(config.input_format, |
| config.input_visible_size) + |
| 2048; |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&VideoEncodeAccelerator::Client::RequireBitstreamBuffers, |
| client_ptr_factory_->GetWeakPtr(), frame_input_count, |
| config.input_visible_size, output_buffer_capacity)); |
| return true; |
| } |
| |
| void AndroidVideoEncodeAccelerator::MaybeStartIOTimer() { |
| if (!io_timer_.IsRunning() && |
| (num_buffers_at_codec_ > 0 || !pending_frames_.empty())) { |
| io_timer_.Start(FROM_HERE, EncodePollDelay(), this, |
| &AndroidVideoEncodeAccelerator::DoIOTask); |
| } |
| } |
| |
| void AndroidVideoEncodeAccelerator::MaybeStopIOTimer() { |
| if (io_timer_.IsRunning() && |
| (num_buffers_at_codec_ == 0 && pending_frames_.empty())) { |
| io_timer_.Stop(); |
| } |
| } |
| |
| void AndroidVideoEncodeAccelerator::Encode(scoped_refptr<VideoFrame> frame, |
| bool force_keyframe) { |
| DVLOG(3) << __PRETTY_FUNCTION__ << ": " << force_keyframe; |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (frame->format() != PIXEL_FORMAT_I420) { |
| NotifyErrorStatus( |
| {EncoderStatus::Codes::kUnsupportedFrameFormat, |
| "Unexpected format: " + VideoPixelFormatToString(frame->format())}); |
| return; |
| } |
| if (frame->visible_rect().size() != frame_size_) { |
| NotifyErrorStatus({EncoderStatus::Codes::kInvalidInputFrame, |
| "Unexpected resolution: got " + |
| frame->visible_rect().size().ToString() + |
| ", expected " + frame_size_.ToString()}); |
| return; |
| } |
| pending_frames_.emplace( |
| std::make_tuple(std::move(frame), force_keyframe, base::Time::Now())); |
| DoIOTask(); |
| } |
| |
| void AndroidVideoEncodeAccelerator::UseOutputBitstreamBuffer( |
| BitstreamBuffer buffer) { |
| DVLOG(3) << __PRETTY_FUNCTION__ << ": bitstream_buffer_id=" << buffer.id(); |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| available_bitstream_buffers_.push_back(std::move(buffer)); |
| DoIOTask(); |
| } |
| |
| void AndroidVideoEncodeAccelerator::RequestEncodingParametersChange( |
| const Bitrate& bitrate, |
| uint32_t framerate) { |
| // If this is changed to use variable bitrate encoding, change the mode check |
| // to check that the mode matches the current mode. |
| if (bitrate.mode() != Bitrate::Mode::kConstant) { |
| NotifyErrorStatus( |
| {EncoderStatus::Codes::kEncoderUnsupportedConfig, |
| "Unexpected bitrate mode: " + |
| base::NumberToString(static_cast<int>(bitrate.mode()))}); |
| return; |
| } |
| DVLOG(3) << __PRETTY_FUNCTION__ << ": bitrate: " << bitrate.ToString() |
| << ", framerate: " << framerate; |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (bitrate.target_bps() != last_set_bitrate_) { |
| last_set_bitrate_ = bitrate.target_bps(); |
| media_codec_->SetVideoBitrate(bitrate.target_bps(), framerate); |
| } |
| // Note: Android's MediaCodec doesn't allow mid-stream adjustments to |
| // framerate, so we ignore that here. This is OK because Android only uses |
| // the framerate value from MediaFormat during configure() as a proxy for |
| // bitrate, and we set that explicitly. |
| } |
| |
| void AndroidVideoEncodeAccelerator::Destroy() { |
| DVLOG(3) << __PRETTY_FUNCTION__; |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| client_ptr_factory_.reset(); |
| if (media_codec_) { |
| if (io_timer_.IsRunning()) |
| io_timer_.Stop(); |
| media_codec_->Stop(); |
| } |
| delete this; |
| } |
| |
| void AndroidVideoEncodeAccelerator::DoIOTask() { |
| QueueInput(); |
| DequeueOutput(); |
| MaybeStartIOTimer(); |
| MaybeStopIOTimer(); |
| } |
| |
| void AndroidVideoEncodeAccelerator::QueueInput() { |
| if (error_occurred_ || pending_frames_.empty()) |
| return; |
| |
| int input_buf_index = 0; |
| MediaCodecStatus status = |
| media_codec_->DequeueInputBuffer(NoWaitTimeOut(), &input_buf_index); |
| if (status != MEDIA_CODEC_OK) { |
| DCHECK(status == MEDIA_CODEC_TRY_AGAIN_LATER || |
| status == MEDIA_CODEC_ERROR); |
| if (status == MEDIA_CODEC_ERROR) { |
| NotifyErrorStatus({EncoderStatus::Codes::kEncoderHardwareDriverError, |
| "MediaCodec error in DequeueInputBuffer"}); |
| return; |
| } |
| } |
| |
| const PendingFrames::value_type& input = pending_frames_.front(); |
| bool is_key_frame = std::get<1>(input); |
| if (is_key_frame) { |
| // Ideally MediaCodec would honor BUFFER_FLAG_SYNC_FRAME so we could |
| // indicate this in the QueueInputBuffer() call below and guarantee _this_ |
| // frame be encoded as a key frame, but sadly that flag is ignored. |
| // Instead, we request a key frame "soon". |
| media_codec_->RequestKeyFrameSoon(); |
| } |
| scoped_refptr<VideoFrame> frame = std::get<0>(input); |
| |
| uint8_t* buffer = nullptr; |
| size_t capacity = 0; |
| status = media_codec_->GetInputBuffer(input_buf_index, &buffer, &capacity); |
| if (status != MEDIA_CODEC_OK) { |
| NotifyErrorStatus({EncoderStatus::Codes::kEncoderHardwareDriverError, |
| "MediaCodec error in GetInputBuffer: " + |
| base::NumberToString(status)}); |
| return; |
| } |
| const auto visible_size = |
| aligned_size_.value_or(frame->visible_rect().size()); |
| |
| uint8_t* dst_y = buffer; |
| const int dst_stride_y = input_buffer_stride_; |
| const int uv_plane_offset = |
| input_buffer_yplane_height_ * input_buffer_stride_; |
| uint8_t* dst_uv = buffer + uv_plane_offset; |
| const int dst_stride_uv = input_buffer_stride_; |
| |
| const gfx::Size uv_plane_size = VideoFrame::PlaneSizeInSamples( |
| PIXEL_FORMAT_NV12, VideoFrame::kUVPlane, visible_size); |
| const size_t queued_size = |
| // size of Y-plane plus padding till UV-plane |
| uv_plane_offset + |
| // size of all UV-plane lines but the last one |
| (uv_plane_size.height() - 1) * dst_stride_uv + |
| // size of the very last line in UV-plane (it's not padded to full stride) |
| uv_plane_size.width() * 2; |
| |
| if (queued_size > capacity) { |
| NotifyErrorStatus({EncoderStatus::Codes::kInvalidInputFrame, |
| "Frame doesn't fit into the input buffer. queue_size: " + |
| base::NumberToString(queued_size) + |
| "capacity: " + base::NumberToString(capacity)}); |
| return; |
| } |
| |
| // Why NV12? Because COLOR_FORMAT_YUV420_SEMIPLANAR. See comment at other |
| // mention of that constant. |
| bool converted = !libyuv::I420ToNV12( |
| frame->visible_data(VideoFrame::kYPlane), |
| frame->stride(VideoFrame::kYPlane), |
| frame->visible_data(VideoFrame::kUPlane), |
| frame->stride(VideoFrame::kUPlane), |
| frame->visible_data(VideoFrame::kVPlane), |
| frame->stride(VideoFrame::kVPlane), dst_y, dst_stride_y, dst_uv, |
| dst_stride_uv, visible_size.width(), visible_size.height()); |
| if (!converted) { |
| NotifyErrorStatus({EncoderStatus::Codes::kFormatConversionError, |
| "Failed to I420ToNV12()"}); |
| return; |
| } |
| |
| // MediaCodec encoder assumes the presentation timestamps to be monotonically |
| // increasing at initialized framerate. But in Chromium, the video capture |
| // may be paused for a while or drop some frames, so the timestamp in input |
| // frames won't be continious. Here we cache the timestamps of input frames, |
| // mapping to the generated |presentation_timestamp_|, and will read them out |
| // after encoding. Then encoder can work happily always and we can preserve |
| // the timestamps in captured frames for other purpose. |
| presentation_timestamp_ += base::Microseconds( |
| base::Time::kMicrosecondsPerSecond / INITIAL_FRAMERATE); |
| DCHECK(frame_timestamp_map_.find(presentation_timestamp_) == |
| frame_timestamp_map_.end()); |
| frame_timestamp_map_[presentation_timestamp_] = frame->timestamp(); |
| |
| status = media_codec_->QueueInputBuffer(input_buf_index, nullptr, queued_size, |
| presentation_timestamp_); |
| UMA_HISTOGRAM_TIMES("Media.AVDA.InputQueueTime", |
| base::Time::Now() - std::get<2>(input)); |
| if (status != MEDIA_CODEC_OK) { |
| NotifyErrorStatus( |
| {EncoderStatus::Codes::kEncoderHardwareDriverError, |
| "Failed to QueueInputBuffer " + base::NumberToString(status)}); |
| return; |
| } |
| ++num_buffers_at_codec_; |
| DCHECK(static_cast<int32_t>(frame_timestamp_map_.size()) == |
| num_buffers_at_codec_); |
| |
| pending_frames_.pop(); |
| } |
| |
| void AndroidVideoEncodeAccelerator::DequeueOutput() { |
| if (error_occurred_ || available_bitstream_buffers_.empty() || |
| num_buffers_at_codec_ == 0) { |
| return; |
| } |
| |
| int32_t buf_index = 0; |
| size_t offset = 0; |
| size_t size = 0; |
| bool key_frame = false; |
| |
| base::TimeDelta presentaion_timestamp; |
| MediaCodecStatus status = media_codec_->DequeueOutputBuffer( |
| NoWaitTimeOut(), &buf_index, &offset, &size, &presentaion_timestamp, |
| nullptr, &key_frame); |
| switch (status) { |
| case MEDIA_CODEC_TRY_AGAIN_LATER: |
| return; |
| |
| case MEDIA_CODEC_ERROR: |
| NotifyErrorStatus({EncoderStatus::Codes::kEncoderFailedEncode, |
| "MediaCodec error in DequeueOutputBuffer"}); |
| // Unreachable because of previous statement, but included for clarity. |
| return; |
| |
| case MEDIA_CODEC_OUTPUT_FORMAT_CHANGED: |
| return; |
| |
| case MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED: |
| return; |
| |
| case MEDIA_CODEC_OK: |
| DCHECK_GE(buf_index, 0); |
| break; |
| |
| default: |
| NOTREACHED(); |
| break; |
| } |
| |
| const auto it = frame_timestamp_map_.find(presentaion_timestamp); |
| DCHECK(it != frame_timestamp_map_.end()); |
| const base::TimeDelta frame_timestamp = it->second; |
| frame_timestamp_map_.erase(it); |
| |
| BitstreamBuffer bitstream_buffer = |
| std::move(available_bitstream_buffers_.back()); |
| available_bitstream_buffers_.pop_back(); |
| base::UnsafeSharedMemoryRegion region = bitstream_buffer.TakeRegion(); |
| auto mapping = |
| region.MapAt(bitstream_buffer.offset(), bitstream_buffer.size()); |
| if (!mapping.IsValid()) { |
| NotifyErrorStatus( |
| {EncoderStatus::Codes::kSystemAPICallError, "Failed to map SHM"}); |
| return; |
| } |
| if (size > bitstream_buffer.size()) { |
| NotifyErrorStatus( |
| {EncoderStatus::Codes::kEncoderFailedEncode, |
| "Encoded buffer too large: " + base::NumberToString(size) + ">" + |
| base::NumberToString(bitstream_buffer.size())}); |
| return; |
| } |
| status = media_codec_->CopyFromOutputBuffer(buf_index, offset, |
| mapping.memory(), size); |
| if (status != MEDIA_CODEC_OK) { |
| NotifyErrorStatus({EncoderStatus::Codes::kEncoderFailedEncode, |
| "MediaCodec error in CopyFromOutputBuffer: " + |
| base::NumberToString(status)}); |
| return; |
| } |
| media_codec_->ReleaseOutputBuffer(buf_index, false); |
| --num_buffers_at_codec_; |
| |
| auto metadata = BitstreamBufferMetadata(size, key_frame, frame_timestamp); |
| if (aligned_size_) { |
| metadata.encoded_size = *aligned_size_; |
| } |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&VideoEncodeAccelerator::Client::BitstreamBufferReady, |
| client_ptr_factory_->GetWeakPtr(), bitstream_buffer.id(), |
| metadata)); |
| } |
| |
| bool AndroidVideoEncodeAccelerator::SetInputBufferLayout() { |
| // Non 16x16 aligned resolutions don't work well with MediaCodec |
| // unfortunately, see https://crbug.com/1084702 for details. It seems they |
| // only work when stride/y_plane_height information is provided. |
| gfx::Size encoded_size; |
| auto status = media_codec_->GetInputFormat( |
| &input_buffer_stride_, &input_buffer_yplane_height_, &encoded_size); |
| if (status != MEDIA_CODEC_OK) { |
| return false; |
| } |
| |
| // If the size is the same, nothing to do. |
| if (encoded_size == frame_size_) { |
| return true; |
| } |
| |
| aligned_size_ = encoded_size; |
| |
| // Give the client a chance to handle realignment itself. |
| DCHECK_EQ(aligned_size_->width() % 16, 0); |
| DCHECK_EQ(aligned_size_->height() % 16, 0); |
| VideoEncoderInfo encoder_info; |
| encoder_info.requested_resolution_alignment = 16; |
| encoder_info.apply_alignment_to_all_simulcast_layers = true; |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&VideoEncodeAccelerator::Client::NotifyEncoderInfoChange, |
| client_ptr_factory_->GetWeakPtr(), encoder_info)); |
| |
| MEDIA_LOG(INFO, log_) |
| << "MediaCodec encoder requires 16x16 aligned resolution. Cropping to " |
| << aligned_size_->ToString(); |
| |
| return true; |
| } |
| |
| void AndroidVideoEncodeAccelerator::NotifyErrorStatus(EncoderStatus status) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(!status.is_ok()); |
| CHECK(log_); |
| MEDIA_LOG(ERROR, log_) << status.message(); |
| LOG(ERROR) << "Call NotifyErrorStatus(): code=" |
| << static_cast<int>(status.code()) |
| << ", message=" << status.message(); |
| if (!error_occurred_) { |
| client_ptr_factory_->GetWeakPtr()->NotifyErrorStatus(status); |
| error_occurred_ = true; |
| } |
| } |
| } // namespace media |