| // Copyright 2022 The Fuchsia 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 "codec_adapter_vaapi_encoder.h" |
| |
| #include <fuchsia/media/cpp/fidl.h> |
| #include <zircon/status.h> |
| |
| #include <va/va_drmcommon.h> |
| |
| #include "h264_accelerator.h" |
| #include "media/base/video_codecs.h" |
| #include "media/gpu/h264_decoder.h" |
| #include "src/media/codec/codecs/vaapi/third_party/chromium/h264_vaapi_video_encoder_delegate.h" |
| #include "src/media/codec/codecs/vaapi/third_party/chromium/vaapi_picture.h" |
| #include "src/media/codec/codecs/vaapi/third_party/chromium/vaapi_wrapper.h" |
| #include "src/media/third_party/chromium_media/media/gpu/gpu_video_encode_accelerator_helpers.h" |
| |
| CodecAdapterVaApiEncoder::CodecAdapterVaApiEncoder(std::mutex& lock, |
| CodecAdapterEvents* codec_adapter_events) |
| : CodecAdapter(lock, codec_adapter_events) { |
| ZX_DEBUG_ASSERT(events_); |
| } |
| CodecAdapterVaApiEncoder::~CodecAdapterVaApiEncoder() { |
| input_processing_loop_.Shutdown(); |
| encoder_.reset(); |
| } |
| |
| void CodecAdapterVaApiEncoder::CoreCodecInit( |
| const fuchsia::media::FormatDetails& initial_input_format_details) { |
| if (!initial_input_format_details.has_format_details_version_ordinal()) { |
| events_->onCoreCodecFailCodec( |
| "CoreCodecInit(): Initial input format details missing version " |
| "ordinal."); |
| return; |
| } |
| // Will always be 0 for now. |
| input_format_details_version_ordinal_ = |
| initial_input_format_details.format_details_version_ordinal(); |
| vaapi_wrapper_ = std::make_shared<media::VaapiWrapper>(); |
| |
| if (!HandleInputFormatChange(initial_input_format_details, true)) { |
| return; |
| } |
| zx_status_t result = |
| input_processing_loop_.StartThread("input_processing_thread_", &input_processing_thread_); |
| if (result != ZX_OK) { |
| events_->onCoreCodecFailCodec( |
| "CodecCodecInit(): Failed to start input processing thread with " |
| "zx_status_t: %d", |
| result); |
| return; |
| } |
| } |
| |
| bool CodecAdapterVaApiEncoder::HandleInputFormatChange( |
| const fuchsia::media::FormatDetails& input_format_details, bool initial) { |
| VADisplay va_dpy = VADisplayWrapper::GetSingleton()->display(); |
| const std::string& mime_type = input_format_details.mime_type(); |
| if (mime_type != "video/h264") { |
| events_->onCoreCodecFailCodec("HandleInputFormatChange(): Unknown mime_type %s\n", |
| mime_type.c_str()); |
| return false; |
| } |
| if (!input_format_details.has_domain()) { |
| events_->onCoreCodecFailCodec("HandleInputFormatChange(): No domain"); |
| return false; |
| } |
| if (!input_format_details.domain().is_video()) { |
| events_->onCoreCodecFailCodec("HandleInputFormatChange(): Input not video"); |
| return false; |
| } |
| if (!input_format_details.domain().video().is_uncompressed()) { |
| events_->onCoreCodecFailCodec("HandleInputFormatChange(): Input not uncompressed"); |
| return false; |
| } |
| if (input_format_details.has_profile()) { |
| if (input_format_details.profile() != fuchsia::media::CodecProfile::H264PROFILE_HIGH) { |
| events_->onCoreCodecFailCodec( |
| "HandleInputFormatChange(): Encoder only supports HIGH profile"); |
| return false; |
| } |
| } |
| uint32_t width = input_format_details.domain().video().uncompressed().image_format.display_width; |
| uint32_t height = |
| input_format_details.domain().video().uncompressed().image_format.display_height; |
| auto checked_width = safemath::MakeCheckedNum(width).Cast<int>(); |
| auto checked_height = safemath::MakeCheckedNum(height).Cast<int>(); |
| if (!checked_width.IsValid() || checked_width.ValueOrDie() == 0) { |
| events_->onCoreCodecFailCodec("HandleInputFormatChange(): Initial width %d invalid", width); |
| return false; |
| } |
| if (!checked_height.IsValid() || checked_height.ValueOrDie() == 0) { |
| events_->onCoreCodecFailCodec("HandleInputFormatChange(): Initial height %d invalid", height); |
| return false; |
| } |
| uint32_t coded_width = |
| input_format_details.domain().video().uncompressed().image_format.coded_width; |
| uint32_t coded_height = |
| input_format_details.domain().video().uncompressed().image_format.coded_height; |
| auto checked_coded_width = safemath::MakeCheckedNum(coded_width).Cast<int>(); |
| auto checked_coded_height = safemath::MakeCheckedNum(coded_height).Cast<int>(); |
| if (!checked_coded_width.IsValid() || checked_coded_width.ValueOrDie() == 0) { |
| events_->onCoreCodecFailCodec("HandleInputFormatChange(): Initial coded width %d invalid", |
| coded_width); |
| return false; |
| } |
| if (!checked_coded_height.IsValid() || checked_coded_height.ValueOrDie() == 0) { |
| events_->onCoreCodecFailCodec("HandleInputFormatChange(): Initial height %d invalid", |
| coded_height); |
| return false; |
| } |
| |
| bool reset_encoder = initial; |
| |
| gfx::Size display_size(checked_width.ValueOrDie(), checked_height.ValueOrDie()); |
| |
| if (display_size_ != display_size) { |
| reset_encoder = true; |
| input_surface_.reset(); |
| |
| std::lock_guard lock(surfaces_lock_); |
| // Increment surface generation so all existing surfaces will be freed |
| // when they're released instead of being returned to the pool. |
| surface_size_ = display_size_; |
| surface_generation_++; |
| surfaces_.clear(); |
| } |
| |
| display_size_ = display_size; |
| coded_size_ = gfx::Size(checked_coded_width.ValueOrDie(), checked_coded_height.ValueOrDie()); |
| if (display_size_.width() > coded_size_.width() || |
| display_size_.height() > coded_size_.height()) { |
| events_->onCoreCodecFailCodec( |
| "HandleInputFormatChange(): Display dimensions %s larger than coded dimensions %s", |
| display_size_.ToString().c_str(), coded_size_.ToString().c_str()); |
| return false; |
| } |
| |
| auto accelerator_config = media::VideoEncodeAccelerator::Config(); |
| accelerator_config.input_visible_size = display_size_; |
| accelerator_config.output_profile = media::H264PROFILE_HIGH; |
| media::VaapiVideoEncoderDelegate::Config ave_config; |
| |
| VAConfigAttrib attrib; |
| attrib.type = VAConfigAttribEncMaxRefFrames; |
| // TODO: Cache this value instead of querying every time. |
| VAStatus va_res = vaGetConfigAttributes(va_dpy, va_profile_, va_entrypoint_, &attrib, 1); |
| if (va_res != VA_STATUS_SUCCESS) { |
| events_->onCoreCodecFailCodec("vaGetConfigAttributes failed: %d", va_res); |
| return false; |
| } |
| |
| ave_config.max_num_ref_frames = attrib.value; |
| |
| // Defaults taken from fuchsia::media fidl. |
| accelerator_config.initial_framerate = 30; |
| accelerator_config.bitrate = media::Bitrate::ConstantBitrate(200000u); |
| accelerator_config.gop_length = 8 + 1; |
| if (input_format_details.has_encoder_settings()) { |
| auto& encoder_settings = input_format_details.encoder_settings(); |
| if (encoder_settings.is_h264()) { |
| auto& h264 = encoder_settings.h264(); |
| if (h264.has_frame_rate()) { |
| accelerator_config.initial_framerate = h264.frame_rate(); |
| } |
| if (h264.has_bit_rate()) { |
| accelerator_config.bitrate = media::Bitrate::ConstantBitrate(h264.bit_rate()); |
| } |
| if (h264.has_gop_size()) { |
| // gop_length includes the initial IDR frame, so add 1. |
| auto new_gop_size = safemath::CheckAdd(h264.gop_size(), 1); |
| if (!new_gop_size.IsValid()) { |
| events_->onCoreCodecFailCodec("HandleInputFormatChange(): Invalid gop_size %u", |
| h264.gop_size()); |
| return false; |
| } |
| accelerator_config.gop_length = new_gop_size.ValueOrDie(); |
| } |
| if (h264.has_force_key_frame()) { |
| next_frame_keyframe_ = next_frame_keyframe_ || h264.force_key_frame(); |
| } |
| if (h264.has_quantization_params()) { |
| events_->onCoreCodecFailCodec( |
| "HandleInputFormatChange(): Setting quantization params not supported"); |
| return false; |
| } |
| } else { |
| events_->onCoreCodecFailCodec("HandleInputFormatChange(): Incorrect encoder setting type"); |
| return false; |
| } |
| } |
| |
| if (accelerator_config.gop_length != accelerator_config_.gop_length) { |
| reset_encoder = true; |
| } |
| |
| accelerator_config_ = accelerator_config; |
| |
| if (reset_encoder) { |
| context_id_ = {}; |
| config_.reset(); |
| encoder_ = std::make_unique<media::H264VaapiVideoEncoderDelegate>(vaapi_wrapper_, |
| fit::function<void()>()); |
| if (!encoder_->Initialize(accelerator_config, ave_config)) { |
| events_->onCoreCodecFailCodec("Failed to initialize encoder"); |
| return false; |
| } |
| VAConfigAttrib attribs[1]; |
| attribs[0].type = VAConfigAttribRTFormat; |
| attribs[0].value = VA_RT_FORMAT_YUV420; |
| VAConfigID config_id; |
| VAStatus va_status = vaCreateConfig(va_dpy, va_profile_, va_entrypoint_, attribs, |
| std::size(attribs), &config_id); |
| if (va_status != VA_STATUS_SUCCESS) { |
| events_->onCoreCodecFailCodec("Failed to create config."); |
| return false; |
| } |
| config_.emplace(config_id); |
| } else { |
| if (!encoder_->UpdateRates(media::AllocateBitrateForDefaultEncoding(accelerator_config_), |
| *accelerator_config.initial_framerate)) { |
| events_->onCoreCodecFailCodec("Failed to update bitrate"); |
| return false; |
| } |
| } |
| if (!input_surface_) { |
| VASurfaceID input_surface; |
| VAStatus va_res = vaCreateSurfaces(va_dpy, VA_RT_FORMAT_YUV420, display_size_.width(), |
| display_size_.height(), &input_surface, 1, nullptr, 0); |
| if (va_res != VA_STATUS_SUCCESS) { |
| events_->onCoreCodecFailCodec("vaCreateSurfaces failed: %d", va_res); |
| return false; |
| } |
| |
| input_surface_ = ScopedSurfaceID(input_surface); |
| } |
| return true; |
| } |
| |
| void CodecAdapterVaApiEncoder::ProcessInputLoop() { |
| std::optional<CodecInputItem> maybe_input_item; |
| |
| while ((maybe_input_item = input_queue_.WaitForElement())) { |
| CodecInputItem input_item = std::move(maybe_input_item.value()); |
| if (input_item.is_format_details()) { |
| if (!HandleInputFormatChange(input_item.format_details(), false)) { |
| // If there's an error, HandleInputFormatChange will signal an async error itself. |
| return; |
| } |
| |
| } else if (input_item.is_end_of_stream()) { |
| // Chromium's video encoder code doesn't support frame reordering, so all frames will already |
| // have been output and no additional flushing is necessary. |
| events_->onCoreCodecOutputEndOfStream(/*error_detected_before=*/false); |
| } else if (input_item.is_packet()) { |
| if (!ProcessPacket(input_item.packet())) { |
| // If there's an error, ProcessPacket will signal an async error itself. |
| return; |
| } |
| } |
| } |
| } |
| |
| bool CodecAdapterVaApiEncoder::ProcessPacket(CodecPacket* packet) { |
| VADisplay va_dpy = VADisplayWrapper::GetSingleton()->display(); |
| if (!context_id_) { |
| // We intentionally delay triggering the output buffer allocation until some input has arrived, |
| // to avoid clients potentially taking a generally-incorrect dependency on output config |
| // happening prior to any delivered input. |
| events_->onCoreCodecMidStreamOutputConstraintsChange(true); |
| VAContextID context_id; |
| VAStatus va_res = |
| vaCreateContext(va_dpy, config_->id(), display_size_.width(), display_size_.height(), |
| VA_PROGRESSIVE, nullptr, 0, &context_id); |
| if (va_res != VA_STATUS_SUCCESS) { |
| events_->onCoreCodecFailCodec("vaCreateContext failed: %d", va_res); |
| return false; |
| } |
| context_id_.emplace(context_id); |
| vaapi_wrapper_->set_context_id(context_id); |
| } |
| { |
| std::lock_guard lock(surfaces_lock_); |
| if (surfaces_.empty()) { |
| std::vector<VASurfaceID> va_surfaces; |
| va_surfaces.resize(1); |
| VAStatus va_res = vaCreateSurfaces(va_dpy, VA_RT_FORMAT_YUV420, display_size_.width(), |
| display_size_.height(), va_surfaces.data(), |
| static_cast<uint32_t>(va_surfaces.size()), nullptr, 0); |
| if (va_res != VA_STATUS_SUCCESS) { |
| events_->onCoreCodecFailCodec("vaCreateSurfaces failed: %d", va_res); |
| return false; |
| } |
| |
| for (VASurfaceID id : va_surfaces) { |
| surfaces_.emplace_back(id); |
| } |
| } |
| } |
| |
| auto video_frame = std::make_shared<media::VideoFrame>(); |
| video_frame->display_size = display_size_; |
| video_frame->coded_size = coded_size_; |
| video_frame->base = packet->buffer()->base(); |
| video_frame->size_bytes = packet->buffer()->size(); |
| video_frame->stride = fbl::round_up( |
| static_cast<uint32_t>(display_size_.width()), |
| *buffer_settings_[kInputPort]->image_format_constraints()->bytes_per_row_divisor()); |
| |
| scoped_refptr<VASurface> va_surface = GetVASurface(); |
| VABufferID coded_buffer; |
| // The VA-API driver can efficiently reuse deleted buffers, so we create a enw buffer every frame. |
| VAStatus va_res = |
| vaCreateBuffer(va_dpy, context_id_->id(), VAEncCodedBufferType, |
| static_cast<uint32_t>(media::GetEncodeBitstreamBufferSize(coded_size_)), 1, |
| nullptr, &coded_buffer); |
| if (va_res != VA_STATUS_SUCCESS) { |
| events_->onCoreCodecFailCodec("vaCreateBuffer failed: %d", va_res); |
| return false; |
| } |
| |
| auto buffer_id = std::make_unique<ScopedBufferID>(coded_buffer); |
| auto picture = std::make_shared<VaapiPicture>(); |
| picture->va_surface = std::move(va_surface); |
| |
| auto encode_job = std::make_unique<media::VaapiVideoEncoderDelegate::EncodeJob>( |
| video_frame, false, input_surface_->id(), display_size_, picture, std::move(buffer_id)); |
| |
| if (next_frame_keyframe_) { |
| encode_job->ProduceKeyframe(); |
| next_frame_keyframe_ = false; |
| } |
| |
| if (!encoder_->Encode(*encode_job)) { |
| events_->onCoreCodecFailCodec("Encoding video failed"); |
| return false; |
| } |
| auto encode_result = encoder_->GetEncodeResult(std::move(encode_job)); |
| std::optional<uint64_t> input_timestampish = std::nullopt; |
| if (packet->has_timestamp_ish()) { |
| input_timestampish = packet->timestamp_ish(); |
| } |
| events_->onCoreCodecInputPacketDone(packet); |
| const CodecBuffer* buffer = |
| output_buffer_pool_.AllocateBuffer(encode_result->metadata().payload_size_bytes); |
| if (!buffer) { |
| // May fail if codec is shutting down. |
| return false; |
| } |
| { |
| uint8_t* target_ptr = buffer->base(); |
| void* buffer_data; |
| VAStatus va_res = vaMapBuffer(va_dpy, encode_result->coded_buffer_id(), &buffer_data); |
| if (va_res != VA_STATUS_SUCCESS) { |
| events_->onCoreCodecFailCodec("Failed to map buffer: %d\n", va_res); |
| return false; |
| } |
| auto* buffer_segment = reinterpret_cast<VACodedBufferSegment*>(buffer_data); |
| |
| while (buffer_segment) { |
| DCHECK(buffer_segment->buf); |
| |
| memcpy(target_ptr, buffer_segment->buf, buffer_segment->size); |
| |
| target_ptr += buffer_segment->size; |
| buffer_segment = reinterpret_cast<VACodedBufferSegment*>(buffer_segment->next); |
| } |
| vaUnmapBuffer(va_dpy, encode_result->coded_buffer_id()); |
| } |
| std::optional<CodecPacket*> maybe_output_packet = free_output_packets_.WaitForElement(); |
| if (!maybe_output_packet) { |
| // May fail if codec is shutting down. |
| return false; |
| } |
| auto output_packet = *maybe_output_packet; |
| output_packet->SetBuffer(buffer); |
| output_packet->SetStartOffset(0); |
| output_packet->SetValidLengthBytes( |
| static_cast<uint32_t>(encode_result->metadata().payload_size_bytes)); |
| if (input_timestampish) { |
| output_packet->SetTimstampIsh(*input_timestampish); |
| } else { |
| output_packet->ClearTimestampIsh(); |
| } |
| |
| { |
| std::lock_guard<std::mutex> lock(lock_); |
| ZX_DEBUG_ASSERT(in_use_by_client_.find(output_packet) == in_use_by_client_.end()); |
| |
| in_use_by_client_.emplace(output_packet, VaApiEncoderOutput(buffer->base(), this)); |
| } |
| events_->onCoreCodecOutputPacket(output_packet, |
| /*error_detected_before=*/false, |
| /*error_detected_during=*/false); |
| return true; |
| } |
| |
| void CodecAdapterVaApiEncoder::CleanUpAfterStream() {} |
| |
| scoped_refptr<VASurface> CodecAdapterVaApiEncoder::GetVASurface() { |
| uint64_t surface_generation; |
| VASurfaceID surface_id; |
| gfx::Size pic_size; |
| { |
| std::lock_guard lock(surfaces_lock_); |
| if (surfaces_.empty()) |
| return {}; |
| surface_id = surfaces_.back().release(); |
| surfaces_.pop_back(); |
| surface_generation = surface_generation_; |
| pic_size = surface_size_; |
| } |
| return std::make_shared<VASurface>( |
| surface_id, pic_size, VA_RT_FORMAT_YUV420, |
| fit::function<void(VASurfaceID)>([this, surface_generation](VASurfaceID surface_id) { |
| std::lock_guard lock(surfaces_lock_); |
| if (surface_generation_ == surface_generation) { |
| surfaces_.emplace_back(surface_id); |
| } else { |
| vaDestroySurfaces(VADisplayWrapper::GetSingleton()->display(), &surface_id, 1); |
| } |
| })); |
| } |
| |
| VaApiEncoderOutput::~VaApiEncoderOutput() { |
| if (adapter_) |
| adapter_->output_buffer_pool_.FreeBuffer(base_address_); |
| } |
| |
| VaApiEncoderOutput::VaApiEncoderOutput(VaApiEncoderOutput&& other) noexcept { |
| adapter_ = other.adapter_; |
| base_address_ = other.base_address_; |
| other.adapter_ = nullptr; |
| } |
| |
| VaApiEncoderOutput& VaApiEncoderOutput::operator=(VaApiEncoderOutput&& other) noexcept { |
| adapter_ = other.adapter_; |
| base_address_ = other.base_address_; |
| other.adapter_ = nullptr; |
| return *this; |
| } |