| // Copyright 2018 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 <lib/media/codec_impl/codec_impl.h> |
| |
| #include <fbl/macros.h> |
| #include <fuchsia/mediacodec/cpp/fidl.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/fidl/cpp/clone.h> |
| #include <lib/fit/defer.h> |
| |
| #include <threads.h> |
| |
| // "is_bound_checks" - In several lambdas that just send a message, we check |
| // is_bound() first, only because of ZX_POL_BAD_HANDLE ZX_POL_ACTION_EXCEPTION. |
| // If it weren't for that, we really wouldn't care about passing |
| // ZX_HANDLE_INVALID to zx_channel_write(), since the channel error handling is |
| // async (we Unbind(), sweep the in-proc send queue, and only then delete the |
| // Binding). |
| |
| // The VLOGF() and LOGF() macros are here because we want the calls sites to |
| // look like FX_VLOGF and FX_LOGF, but without hard-wiring to those. For now, |
| // printf() seems to work fine. |
| |
| #define VLOG_ENABLED 0 |
| |
| #if (VLOG_ENABLED) |
| #define VLOGF(...) printf(__VA_ARGS__) |
| #else |
| #define VLOGF(...) \ |
| do { \ |
| } while (0) |
| #endif |
| |
| #define LOGF(...) printf(__VA_ARGS__) |
| |
| namespace { |
| |
| // The protocol does not permit an unbounded number of in-flight streams, as |
| // that would potentially result in unbounded data queued in the incoming |
| // channel with no valid circuit-breaker value for the incoming channel data. |
| constexpr size_t kMaxInFlightStreams = 10; |
| |
| constexpr uint64_t kInputBufferConstraintsVersionOrdinal = 1; |
| constexpr uint64_t kInputDefaultBufferConstraintsVersionOrdinal = |
| kInputBufferConstraintsVersionOrdinal; |
| |
| // TODO(dustingreen): Make these defaults/settings overridable per CodecAdapter |
| // implementation. For a few of them, maybe require the CodecAdapter to |
| // specify (as in no default for some of them). |
| |
| constexpr uint32_t kInputPacketCountForCodecMin = 2; |
| // This is fairly arbitrary, but roughly speaking, 1 to be decoding, 1 to be in |
| // flight from the client, 1 to be in flight back to the client. We may want |
| // to adjust this upward if we find it's needed to keep the HW busy when there's |
| // any backlog. |
| constexpr uint32_t kInputPacketCountForCodecRecommended = 3; |
| constexpr uint32_t kInputPacketCountForCodecRecommendedMax = 16; |
| constexpr uint32_t kInputPacketCountForCodecMax = 64; |
| |
| constexpr uint32_t kInputDefaultPacketCountForCodec = |
| kInputPacketCountForCodecRecommended; |
| |
| constexpr uint32_t kInputPacketCountForClientMax = |
| std::numeric_limits<uint32_t>::max(); |
| // This is fairly arbitrary, but rough speaking, 1 to be filling, 1 to be in |
| // flight toward the codec, and 1 to be in flight from the codec. This doesn't |
| // intend to be large enough to ride out any hypothetical decoder performance |
| // variability vs. needed decode rate. |
| constexpr uint32_t kInputDefaultPacketCountForClient = 3; |
| |
| // TODO(dustingreen): Implement and permit single-buffer mode. (The default |
| // will probably remain buffer per packet mode though.) |
| constexpr bool kInputSingleBufferModeAllowed = false; |
| constexpr bool kInputDefaultSingleBufferMode = false; |
| |
| // A client using the min shouldn't necessarily expect performance to be |
| // acceptable when running higher bit-rates. |
| constexpr uint32_t kInputPerPacketBufferBytesMin = 8 * 1024; |
| // This is fairly arbitrary, but roughly speaking, ~266 KiB for an average frame |
| // at 50 Mbps for 4k video, rounded up to 512 KiB buffer space per packet to |
| // allow most but not all frames to fit in one packet. It could be equally |
| // reasonable to say the average-size compressed from should barely fit in one |
| // packet's buffer space, or the average-size compressed frame should split to |
| // ~1.5 packets, but we don't want an excessive number of packets required per |
| // frame (not even for I frames). |
| constexpr uint32_t kInputPerPacketBufferBytesRecommended = 512 * 1024; |
| // This is an arbitrary cap for now. The only reason it's larger than |
| // recommended is to allow some room to profile whether larger buffer space per |
| // packet might be useful for performance. |
| constexpr uint32_t kInputPerPacketBufferBytesMax = 4 * 1024 * 1024; |
| |
| constexpr uint32_t kInputDefaultPerPacketBufferBytes = |
| kInputPerPacketBufferBytesRecommended; |
| |
| class ScopedUnlock { |
| public: |
| explicit ScopedUnlock(std::unique_lock<std::mutex>& unique_lock) |
| : unique_lock_(unique_lock) { |
| unique_lock_.unlock(); |
| } |
| ~ScopedUnlock() { unique_lock_.lock(); } |
| |
| private: |
| std::unique_lock<std::mutex>& unique_lock_; |
| ScopedUnlock() = delete; |
| DISALLOW_COPY_ASSIGN_AND_MOVE(ScopedUnlock); |
| }; |
| |
| // Used within ScopedUnlock only. Normally we'd just leave a std::unique_lock |
| // locked until it's destructed. |
| class ScopedRelock { |
| public: |
| explicit ScopedRelock(std::unique_lock<std::mutex>& unique_lock) |
| : unique_lock_(unique_lock) { |
| unique_lock_.lock(); |
| } |
| ~ScopedRelock() { unique_lock_.unlock(); } |
| |
| private: |
| std::unique_lock<std::mutex>& unique_lock_; |
| ScopedRelock() = delete; |
| DISALLOW_COPY_ASSIGN_AND_MOVE(ScopedRelock); |
| }; |
| |
| uint32_t PacketCountFromPortSettings( |
| const fuchsia::mediacodec::CodecPortBufferSettings& settings) { |
| return settings.packet_count_for_codec + settings.packet_count_for_client; |
| } |
| |
| uint32_t BufferCountFromPortSettings( |
| const fuchsia::mediacodec::CodecPortBufferSettings& settings) { |
| if (settings.single_buffer_mode) { |
| return 1; |
| } |
| return PacketCountFromPortSettings(settings); |
| } |
| |
| } // namespace |
| |
| CodecImpl::CodecImpl( |
| std::unique_ptr<CodecAdmission> codec_admission, |
| async_dispatcher_t* shared_fidl_dispatcher, thrd_t shared_fidl_thread, |
| std::unique_ptr<fuchsia::mediacodec::CreateDecoder_Params> decoder_params, |
| fidl::InterfaceRequest<fuchsia::mediacodec::Codec> codec_request) |
| // The parameters to CodecAdapter constructor here aren't important. |
| : CodecAdapter(lock_, this), |
| codec_admission_(std::move(codec_admission)), |
| shared_fidl_dispatcher_(shared_fidl_dispatcher), |
| shared_fidl_thread_(shared_fidl_thread), |
| // TODO(dustingreen): Maybe have another parameter for encoder params, or |
| // maybe separate constructor. |
| decoder_params_(std::move(decoder_params)), |
| tmp_interface_request_(std::move(codec_request)), |
| binding_(this), |
| stream_control_loop_(&kAsyncLoopConfigNoAttachToThread) { |
| // For now, decoder_params is required. |
| // |
| // TODO(dustingreen): Make decoder_params || encoder_params required. |
| ZX_DEBUG_ASSERT(decoder_params_); |
| ZX_DEBUG_ASSERT(tmp_interface_request_); |
| // This is the binding_'s error handler, not the owner_error_handler_ which |
| // is related but separate. |
| binding_.set_error_handler([this](zx_status_t status) { this->Unbind(); }); |
| initial_input_format_details_ = &decoder_params_->input_details; |
| } |
| |
| CodecImpl::~CodecImpl() { |
| // We need ~binding_ to run on fidl_thread() else it's not safe to |
| // un-bind unilaterally. Unless not ever bound in the first place. |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| |
| ZX_DEBUG_ASSERT(was_unbind_started_ && was_unbind_completed_ || |
| !was_logically_bound_); |
| |
| // Ensure the CodecAdmission is deleted entirely after ~this, including after |
| // any relevant base class destructors have run. |
| PostSerial(shared_fidl_dispatcher_, |
| [codec_admission = std::move(codec_admission_), |
| codec_to_close = std::move(codec_to_close_)]() mutable { |
| // Ensure codec_to_close is destructed only after the |
| // codec_admission is destructed. We have to be fairly explicit |
| // about this since the order of lambda members is explicitly |
| // unspecified in C++, so their destruction order is also |
| // unspecified. |
| codec_admission.reset(); |
| |
| // ~codec_to_close (after ~CodecAdmission above). |
| }); |
| } |
| |
| std::mutex& CodecImpl::lock() { return lock_; } |
| |
| void CodecImpl::SetCoreCodecAdapter( |
| std::unique_ptr<CodecAdapter> codec_adapter) { |
| ZX_DEBUG_ASSERT(!codec_adapter_); |
| codec_adapter_ = std::move(codec_adapter); |
| } |
| |
| void CodecImpl::BindAsync(fit::closure error_handler) { |
| // While it would potentially be safe to call Bind() from a thread other than |
| // fidl_thread(), we have no reason to permit that. |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| // Up to once only. No re-use. |
| ZX_DEBUG_ASSERT(!was_bind_async_called_); |
| ZX_DEBUG_ASSERT(!binding_.is_bound()); |
| ZX_DEBUG_ASSERT(tmp_interface_request_); |
| was_bind_async_called_ = true; |
| |
| zx_status_t start_thread_result = stream_control_loop_.StartThread( |
| "StreamControl_loop", &stream_control_thread_); |
| if (start_thread_result != ZX_OK) { |
| // Handle the error async, to be consistent with later errors that must |
| // occur async anyway. Inability to start StreamControl is the only case |
| // where we just allow the owner to "delete this" without using |
| // UnbindLocked(), since UnbindLocked() relies on StreamControl. |
| PostToSharedFidl(std::move(error_handler)); |
| return; |
| } |
| |
| // From here on, we'll only fail the CodecImpl via UnbindLocked(). |
| was_logically_bound_ = true; |
| |
| // This doesn't really need to be set until the start of the posted lambda |
| // below, but here is also fine. |
| owner_error_handler_ = std::move(error_handler); |
| |
| // Do most of the bind work on StreamControl async, since CoreCodecInit() |
| // might potentially take a little while longer than makes sense to run on |
| // fidl_thread(). Potential examples: if CoreCodecInit() ends up |
| // essentially evicting some other CodecImpl, or if setting up HW can take a |
| // while, or if getting a scheduling slot on decode HW can require some |
| // waiting, or similar. |
| PostToStreamControl([this] { |
| // This is allowed to take a little while if necessary, using the current |
| // StreamControl thread, which is not shared with any other CodecImpl. |
| CoreCodecInit(*initial_input_format_details_); |
| is_core_codec_init_called_ = true; |
| |
| // We touch FIDL stuff only from the fidl_thread(). While it would |
| // be more efficient to post once to bind and send up to two messages below, |
| // by posting individually we can share more code and have simpler rules for |
| // calling that code. |
| |
| // Once this is posted, we can be dispatching incoming FIDL messages, |
| // concurrent with the rest of the current lambda. Aside from Sync(), most |
| // of that dispatching would tend to land in FailLocked(). The concurrency |
| // is just worth keeping in mind for the rest of the current lambda is all. |
| PostToSharedFidl([this] { |
| zx_status_t bind_result = binding_.Bind(std::move(tmp_interface_request_), |
| shared_fidl_dispatcher_); |
| if (bind_result != ZX_OK) { |
| Fail("binding_.Bind() failed"); |
| return; |
| } |
| ZX_DEBUG_ASSERT(!tmp_interface_request_); |
| }); |
| |
| input_constraints_ = |
| std::make_unique<fuchsia::mediacodec::CodecBufferConstraints>( |
| fuchsia::mediacodec::CodecBufferConstraints{ |
| .buffer_constraints_version_ordinal = |
| kInputBufferConstraintsVersionOrdinal, |
| // This is not really a suggestion; actual values must be odd, |
| // and the client should be the source of this value. |
| .default_settings.buffer_lifetime_ordinal = 0, |
| .default_settings.buffer_constraints_version_ordinal = |
| kInputDefaultBufferConstraintsVersionOrdinal, |
| .default_settings.packet_count_for_codec = |
| kInputDefaultPacketCountForCodec, |
| .default_settings.packet_count_for_client = |
| kInputDefaultPacketCountForClient, |
| .default_settings.per_packet_buffer_bytes = |
| kInputDefaultPerPacketBufferBytes, |
| .default_settings.single_buffer_mode = |
| kInputDefaultSingleBufferMode, |
| .per_packet_buffer_bytes_min = kInputPerPacketBufferBytesMin, |
| .per_packet_buffer_bytes_recommended = |
| kInputPerPacketBufferBytesRecommended, |
| .per_packet_buffer_bytes_max = kInputPerPacketBufferBytesMax, |
| .packet_count_for_codec_min = kInputPacketCountForCodecMin, |
| .packet_count_for_codec_recommended = |
| kInputPacketCountForCodecRecommended, |
| .packet_count_for_codec_recommended_max = |
| kInputPacketCountForCodecRecommendedMax, |
| .packet_count_for_codec_max = kInputPacketCountForCodecMax, |
| .packet_count_for_client_max = kInputPacketCountForClientMax, |
| .single_buffer_mode_allowed = kInputSingleBufferModeAllowed, |
| }); |
| |
| // If/when this sends OnOutputConfig(), it posts to do so. |
| onInputConstraintsReady(); |
| |
| sent_buffer_constraints_version_ordinal_[kInputPort] = |
| kInputBufferConstraintsVersionOrdinal; |
| PostToSharedFidl([this] { |
| // See "is_bound_checks" comment up top. |
| if (binding_.is_bound()) { |
| binding_.events().OnInputConstraints(fidl::Clone(*input_constraints_)); |
| } |
| }); |
| }); |
| } |
| |
| void CodecImpl::EnableOnStreamFailed() { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| is_on_stream_failed_enabled_ = true; |
| } |
| |
| void CodecImpl::SetInputBufferSettings( |
| fuchsia::mediacodec::CodecPortBufferSettings input_settings) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| PostToStreamControl([this, input_settings = std::move(input_settings)] { |
| SetInputBufferSettings_StreamControl(std::move(input_settings)); |
| }); |
| } |
| |
| void CodecImpl::SetInputBufferSettings_StreamControl( |
| fuchsia::mediacodec::CodecPortBufferSettings input_settings) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| if (IsStreamActiveLocked()) { |
| Fail("client sent SetInputBufferSettings() with stream active"); |
| return; |
| } |
| SetBufferSettingsCommon(lock, kInputPort, input_settings, |
| *input_constraints_); |
| } // ~lock |
| } |
| |
| void CodecImpl::AddInputBuffer(fuchsia::mediacodec::CodecBuffer buffer) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| PostToStreamControl([this, buffer = std::move(buffer)]() mutable { |
| AddInputBuffer_StreamControl(std::move(buffer)); |
| }); |
| } |
| |
| void CodecImpl::AddInputBuffer_StreamControl( |
| fuchsia::mediacodec::CodecBuffer buffer) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| if (IsStopping()) { |
| return; |
| } |
| // We must check, because __WARN_UNUSED_RESULT, and it's worth it for the |
| // enforcement and consistency. |
| if (!AddBufferCommon(kInputPort, std::move(buffer))) { |
| return; |
| } |
| } |
| |
| void CodecImpl::SetOutputBufferSettings( |
| fuchsia::mediacodec::CodecPortBufferSettings output_settings) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| |
| if (!output_config_) { |
| // invalid client behavior |
| // |
| // client must have received at least the initial OnOutputConfig() first |
| // before sending SetOutputBufferSettings(). |
| FailLocked( |
| "client sent SetOutputBufferSettings() when no output_config_"); |
| return; |
| } |
| |
| // For a mid-stream output format change, this also enforces that the client |
| // can only catch up to the mid-stream format change once. In other words, |
| // if the client has already caught up to the mid-stream config change, the |
| // client no longer has an excuse to re-configure again with a stream |
| // active. |
| // |
| // There's a check in SetBufferSettingsCommonLocked() that ignores this |
| // message if the client's buffer_constraints_version_ordinal is behind |
| // last_required_buffer_constraints_version_ordinal_, which gets updated |
| // under the same lock hold interval as the server's de-configuring of |
| // output buffers. |
| // |
| // There's a check in SetBufferSettingsCommonLocked() that closes the |
| // channel if the client is sending a buffer_constraints_version_ordinal |
| // that's newer than the last sent_buffer_constraints_version_ordinal_. |
| if (IsOutputConfiguredLocked() && IsStreamActiveLocked()) { |
| FailLocked( |
| "client sent SetOutputBufferSettings() with IsStreamActiveLocked() + " |
| "already-configured output"); |
| return; |
| } |
| |
| SetBufferSettingsCommon(lock, kOutputPort, output_settings, |
| output_config_->buffer_constraints); |
| } // ~lock |
| } |
| |
| void CodecImpl::AddOutputBuffer(fuchsia::mediacodec::CodecBuffer buffer) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| bool output_done_configuring = |
| AddBufferCommon(kOutputPort, std::move(buffer)); |
| if (output_done_configuring) { |
| // The StreamControl domain _might_ be waiting for output to be configured. |
| wake_stream_control_condition_.notify_all(); |
| } |
| } |
| |
| void CodecImpl::FlushEndOfStreamAndCloseStream( |
| uint64_t stream_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!EnsureFutureStreamFlushSeenLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| } |
| PostToStreamControl([this, stream_lifetime_ordinal] { |
| FlushEndOfStreamAndCloseStream_StreamControl(stream_lifetime_ordinal); |
| }); |
| } |
| |
| void CodecImpl::FlushEndOfStreamAndCloseStream_StreamControl( |
| uint64_t stream_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| |
| // We re-check some things which were already future-verified a different |
| // way, to allow for flexibility in the future-tracking stuff to permit less |
| // checking in the Output ordering domain (fidl_thread()) without |
| // breaking overall verification of a flush. Any checking in the Output |
| // ordering domain is for the future-tracking's own convenience only. The |
| // checking here is the real checking. |
| |
| if (!CheckStreamLifetimeOrdinalLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal >= stream_lifetime_ordinal_); |
| if (!IsStreamActiveLocked() || |
| stream_lifetime_ordinal != stream_lifetime_ordinal_) { |
| // TODO(dustingreen): epitaph |
| FailLocked( |
| "FlushEndOfStreamAndCloseStream() only valid on an active current " |
| "stream (flush does not auto-create a new stream)"); |
| return; |
| } |
| // At this point we know that the stream is not discarded, and not already |
| // flushed previously (because flush will discard the stream as there's |
| // nothing more that the stream is permitted to do). |
| ZX_DEBUG_ASSERT(stream_); |
| ZX_DEBUG_ASSERT(stream_->stream_lifetime_ordinal() == |
| stream_lifetime_ordinal); |
| if (!stream_->input_end_of_stream()) { |
| FailLocked( |
| "FlushEndOfStreamAndCloseStream() is only permitted after " |
| "QueueInputEndOfStream()"); |
| return; |
| } |
| while (!stream_->output_end_of_stream()) { |
| // While waiting, we'll continue to send OnOutputPacket(), |
| // OnOutputConfig(), and continue to process RecycleOutputPacket(), until |
| // the client catches up to the latest config (as needed) and we've |
| // started the send of output end_of_stream packet to the client. |
| // |
| // There is no way for the client to cancel a |
| // FlushEndOfStreamAndCloseStream() short of closing the Codec channel. |
| // Before long, the server will either send the OnOutputEndOfStream(), or |
| // will send OnOmxStreamFailed(), or will close the Codec channel. The |
| // server must do one of those things before long (not allowed to get |
| // stuck while flushing). |
| // |
| // Some core codecs (such as OMX codecs) have no way to report mid-stream |
| // input data corruption errors or similar without it being a stream |
| // failure, so if there's any stream error it turns into OnStreamFailed(). |
| // It's also permitted for a server to set error_detected_ bool(s) on |
| // output packets and send OnOutputEndOfStream() despite detected errors, |
| // but this is only a reasonable behavior for the server if the server |
| // normally would detect and report mid-stream input corruption errors |
| // without an OnStreamFailed(). |
| output_end_of_stream_seen_.wait(lock); |
| } |
| |
| // Now that flush is done, we close the current stream because there is not |
| // any subsequent message for the current stream that's valid. |
| EnsureStreamClosed(lock); |
| } // ~lock |
| } |
| |
| void CodecImpl::CloseCurrentStream(uint64_t stream_lifetime_ordinal, |
| bool release_input_buffers, |
| bool release_output_buffers) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!EnsureFutureStreamCloseSeenLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| } // ~lock |
| PostToStreamControl([this, stream_lifetime_ordinal, release_input_buffers, |
| release_output_buffers] { |
| CloseCurrentStream_StreamControl( |
| stream_lifetime_ordinal, release_input_buffers, release_output_buffers); |
| }); |
| } |
| |
| void CodecImpl::CloseCurrentStream_StreamControl( |
| uint64_t stream_lifetime_ordinal, bool release_input_buffers, |
| bool release_output_buffers) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| EnsureStreamClosed(lock); |
| if (release_input_buffers) { |
| EnsureBuffersNotConfigured(lock, kInputPort); |
| } |
| if (release_output_buffers) { |
| EnsureBuffersNotConfigured(lock, kOutputPort); |
| } |
| } |
| |
| void CodecImpl::Sync(SyncCallback callback) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| // By posting to StreamControl ordering domain, we sync both Output ordering |
| // domain (on fidl_thread()) and the StreamControl ordering domain. |
| PostToStreamControl([this, callback = std::move(callback)]() mutable { |
| Sync_StreamControl(std::move(callback)); |
| }); |
| } |
| |
| void CodecImpl::Sync_StreamControl(SyncCallback callback) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| if (IsStopping()) { |
| // In this case ~callback will happen instead of callback(), in which case |
| // the response won't be sent, which is appropriate - the channel is getting |
| // closed soon instead, and the client has to tolerate that. |
| return; |
| } |
| callback(); |
| } |
| |
| void CodecImpl::RecycleOutputPacket( |
| fuchsia::mediacodec::CodecPacketHeader available_output_packet) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| CodecPacket* packet = nullptr; |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!CheckOldBufferLifetimeOrdinalLocked( |
| kOutputPort, available_output_packet.buffer_lifetime_ordinal)) { |
| return; |
| } |
| if (available_output_packet.buffer_lifetime_ordinal < |
| buffer_lifetime_ordinal_[kOutputPort]) { |
| // ignore arbitrarily-stale required by protocol |
| // |
| // Thanks to even values from the client being prohibited, this also |
| // covers mid-stream output config change where the server has already |
| // de-configured output buffers but the client doesn't know about that |
| // yet. We include that case here by setting |
| // buffer_lifetime_ordinal_[kOutputPort] to the next even value |
| // when de-configuring output server-side until the client has |
| // re-configured output. |
| return; |
| } |
| ZX_DEBUG_ASSERT(available_output_packet.buffer_lifetime_ordinal == |
| buffer_lifetime_ordinal_[kOutputPort]); |
| if (!IsOutputConfiguredLocked()) { |
| FailLocked( |
| "client sent RecycleOutputPacket() for buffer_lifetime_ordinal that " |
| "isn't fully configured yet - bad client behavior"); |
| return; |
| } |
| ZX_DEBUG_ASSERT(IsOutputConfiguredLocked()); |
| if (available_output_packet.packet_index >= |
| all_packets_[kOutputPort].size()) { |
| FailLocked( |
| "out of range packet_index from client in RecycleOutputPacket()"); |
| return; |
| } |
| uint32_t packet_index = available_output_packet.packet_index; |
| if (all_packets_[kOutputPort][packet_index]->is_free()) { |
| FailLocked( |
| "packet_index already free at protocol level - invalid client " |
| "message"); |
| return; |
| } |
| // Mark free at protocol level. |
| all_packets_[kOutputPort][packet_index]->SetFree(true); |
| |
| // Before handing the packet to the core codec, clear some fields that the |
| // core codec is expected to set (or optionally set in the case of |
| // timestamp_ish). In addition to these parameters, a core codec can emit |
| // output config changes via onCoreCodecMidStreamOutputConfigChange(). |
| packet = all_packets_[kOutputPort][packet_index].get(); |
| packet->ClearStartOffset(); |
| packet->ClearValidLengthBytes(); |
| packet->ClearTimestampIsh(); |
| } |
| |
| // Recycle to core codec. |
| CoreCodecRecycleOutputPacket(packet); |
| } |
| |
| void CodecImpl::QueueInputFormatDetails( |
| uint64_t stream_lifetime_ordinal, |
| fuchsia::mediacodec::CodecFormatDetails format_details) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!EnsureFutureStreamSeenLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| } // ~lock |
| PostToStreamControl([this, stream_lifetime_ordinal, |
| format_details = std::move(format_details)]() mutable { |
| QueueInputFormatDetails_StreamControl(stream_lifetime_ordinal, |
| std::move(format_details)); |
| }); |
| } |
| |
| // TODO(dustingreen): Need test coverage for this method, to cover at least |
| // the same format including OOB bytes as were specified during codec creation, |
| // and codec creation with no OOB bytes then this method setting OOB bytes (not |
| // the ideal client usage pattern in the long run since the CreateDecoder() |
| // might decline to provide a optimized but partial Codec implementation, but |
| // should be allowed nonetheless). |
| void CodecImpl::QueueInputFormatDetails_StreamControl( |
| uint64_t stream_lifetime_ordinal, |
| fuchsia::mediacodec::CodecFormatDetails format_details) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| if (!CheckStreamLifetimeOrdinalLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal >= stream_lifetime_ordinal_); |
| if (stream_lifetime_ordinal > stream_lifetime_ordinal_) { |
| if (!StartNewStream(lock, stream_lifetime_ordinal)) { |
| return; |
| } |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal == stream_lifetime_ordinal_); |
| if (stream_->input_end_of_stream()) { |
| FailLocked( |
| "QueueInputFormatDetails() after QueueInputEndOfStream() unexpected"); |
| return; |
| } |
| if (stream_->future_discarded()) { |
| // No reason to handle since the stream is future-discarded. |
| return; |
| } |
| stream_->SetInputFormatDetails( |
| std::make_unique<fuchsia::mediacodec::CodecFormatDetails>( |
| std::move(format_details))); |
| // SetOobConfigPending(true) to ensure oob_config_pending() is true. |
| // |
| // This call is needed only to properly handle a call to |
| // QueueInputFormatDetails() mid-stream. For new streams that lack any calls |
| // to QueueInputFormatDetails() before an input packet arrives, the |
| // oob_config_pending() will already be true because it starts true for a new |
| // stream. For QueueInputFormatDetails() at the start of a stream before any |
| // packets, oob_config_pending() will already be true. |
| // |
| // For decoders this is basically a pending codec_oob_bytes. For encoders |
| // this pending config change can potentially include uncompressed format |
| // details, if mid-stream format change is supported by the encoder. |
| stream_->SetOobConfigPending(true); |
| } |
| |
| void CodecImpl::QueueInputPacket(fuchsia::mediacodec::CodecPacket packet) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| if (!EnsureFutureStreamSeenLocked(packet.stream_lifetime_ordinal)) { |
| return; |
| } |
| } // ~lock |
| PostToStreamControl([this, packet = std::move(packet)]() mutable { |
| QueueInputPacket_StreamControl(std::move(packet)); |
| }); |
| } |
| |
| void CodecImpl::QueueInputPacket_StreamControl( |
| fuchsia::mediacodec::CodecPacket packet) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| |
| fuchsia::mediacodec::CodecPacketHeader temp_header_copy = |
| fidl::Clone(packet.header); |
| |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| |
| // Unless we cancel this cleanup, we'll free the input packet back to the |
| // client. |
| auto send_free_input_packet_locked = |
| fit::defer([this, header = std::move(temp_header_copy)]() mutable { |
| // Mute sending this if FailLocked() was called previously, in case |
| // the reason we're here is something horribly wrong with the packet |
| // header. This way we avoid repeating gibberish back to the client. |
| // While that gibberish might be a slight clue for debugging in some |
| // cases, it's not valid protocol, so don't send it. If |
| // IsStoppingLocked(), the Codec channel will close soon, making this |
| // response unnecessary. |
| if (!IsStoppingLocked()) { |
| SendFreeInputPacketLocked(std::move(header)); |
| } |
| }); |
| |
| if (!CheckOldBufferLifetimeOrdinalLocked( |
| kInputPort, packet.header.buffer_lifetime_ordinal)) { |
| return; |
| } |
| |
| // For input, mid-stream config changes are not a thing and input buffers |
| // are never unilaterally de-configured by the Codec server. |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[kInputPort] == |
| port_settings_[kInputPort]->buffer_lifetime_ordinal); |
| // For this message we're extra-strict re. buffer_lifetime_ordinal, at least |
| // for now. |
| // |
| // In contrast to output, the server doesn't use even values to track config |
| // changes that the client doesn't know about yet, since the server can't |
| // unilaterally demand any changes to the input settings after initially |
| // specifying the input constraints. |
| // |
| // One could somewhat-convincingly argue that this field in this particular |
| // message is a bit pointless, but it might serve to detect client-side |
| // bugs faster thanks to this check. |
| if (packet.header.buffer_lifetime_ordinal != |
| port_settings_[kInputPort]->buffer_lifetime_ordinal) { |
| FailLocked( |
| "client QueueInputPacket() with invalid buffer_lifetime_ordinal."); |
| return; |
| } |
| |
| if (!CheckStreamLifetimeOrdinalLocked(packet.stream_lifetime_ordinal)) { |
| return; |
| } |
| ZX_DEBUG_ASSERT(packet.stream_lifetime_ordinal >= stream_lifetime_ordinal_); |
| |
| if (packet.stream_lifetime_ordinal > stream_lifetime_ordinal_) { |
| // This case implicitly starts a new stream. If the client wanted to |
| // ensure that the old stream would be fully processed, the client would |
| // have sent FlushEndOfStreamAndCloseStream() previously, whose |
| // processing (previous to reaching here) takes care of the flush. |
| // |
| // Start a new stream, synchronously. |
| if (!StartNewStream(lock, packet.stream_lifetime_ordinal)) { |
| return; |
| } |
| } |
| ZX_DEBUG_ASSERT(packet.stream_lifetime_ordinal == stream_lifetime_ordinal_); |
| |
| if (!IsInputConfiguredLocked()) { |
| FailLocked("client QueueInputPacket() with input buffers not configured"); |
| return; |
| } |
| if (packet.header.packet_index >= all_packets_[kInputPort].size()) { |
| FailLocked("client QueueInputPacket() with packet_index out of range"); |
| return; |
| } |
| if (packet.buffer_index >= all_buffers_[kInputPort].size()) { |
| FailLocked("client QueueInputPacket() with buffer_index out of range"); |
| return; |
| } |
| |
| // Protocol check re. free/busy coherency. This applies to packets only, |
| // not buffers. |
| if (!all_packets_[kInputPort][packet.header.packet_index]->is_free()) { |
| FailLocked("client QueueInputPacket() with packet_index !free"); |
| return; |
| } |
| all_packets_[kInputPort][packet.header.packet_index]->SetFree(false); |
| |
| if (stream_->input_end_of_stream()) { |
| FailLocked("QueueInputPacket() after QueueInputEndOfStream() unexpeted"); |
| return; |
| } |
| |
| if (stream_->future_discarded()) { |
| // Don't queue to core codec. The stream_ may have never fully started, |
| // or may have been future-discarded since. Either way, skip queueing to |
| // the core codec. |
| // |
| // If the stream didn't fully start - as in, the client moved on to |
| // another stream before fully configuring output, then the core codec is |
| // not presently in a state compatible with queueing input, but the Codec |
| // interface is. So in that case, we must avoid queueing to the core |
| // codec for correctness. |
| // |
| // If the stream was just future-discarded after fully starting, then this |
| // is just an optimization to avoid giving the core codec more work to do |
| // for a stream the client has already discarded. |
| // |
| // ~send_free_input_packet_locked |
| // ~lock |
| return; |
| } |
| |
| // Sending OnFreeInputPacket() will happen later instead, when the core |
| // codec gives back the packet. |
| send_free_input_packet_locked.cancel(); |
| } // ~lock |
| |
| if (stream_->oob_config_pending()) { |
| HandlePendingInputFormatDetails(); |
| stream_->SetOobConfigPending(false); |
| } |
| |
| CodecPacket* core_codec_packet = |
| all_packets_[kInputPort][packet.header.packet_index].get(); |
| core_codec_packet->SetBuffer( |
| all_buffers_[kInputPort][packet.buffer_index].get()); |
| core_codec_packet->SetStartOffset(packet.start_offset); |
| core_codec_packet->SetValidLengthBytes(packet.valid_length_bytes); |
| if (packet.has_timestamp_ish) { |
| core_codec_packet->SetTimstampIsh(packet.timestamp_ish); |
| } else { |
| core_codec_packet->ClearTimestampIsh(); |
| } |
| |
| // We don't need to be under lock for this, because the fact that we're on the |
| // StreamControl domain is enough to guarantee that any other control of the |
| // core codec will occur after this. |
| CoreCodecQueueInputPacket(core_codec_packet); |
| } |
| |
| void CodecImpl::QueueInputEndOfStream(uint64_t stream_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (!EnsureFutureStreamSeenLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| } // ~lock |
| PostToStreamControl([this, stream_lifetime_ordinal] { |
| QueueInputEndOfStream_StreamControl(stream_lifetime_ordinal); |
| }); |
| } |
| |
| void CodecImpl::QueueInputEndOfStream_StreamControl( |
| uint64_t stream_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| return; |
| } |
| if (!CheckStreamLifetimeOrdinalLocked(stream_lifetime_ordinal)) { |
| return; |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal >= stream_lifetime_ordinal_); |
| if (stream_lifetime_ordinal > stream_lifetime_ordinal_) { |
| // We start a new stream given an end-of-stream for a stream we've not |
| // seen before, since allowing empty streams to not be errors may be nicer |
| // to use. |
| if (!StartNewStream(lock, stream_lifetime_ordinal)) { |
| return; |
| } |
| } |
| |
| if (stream_->input_end_of_stream()) { |
| FailLocked("client already sent QueueInputEndOfStream() for this stream"); |
| return; |
| } |
| stream_->SetInputEndOfStream(); |
| |
| if (stream_->future_discarded()) { |
| // Don't queue to OMX. The stream_ may have never fully started, or may |
| // have been future-discarded since. Either way, skip queueing to OMX. We |
| // only really must do this because the stream may not have ever fully |
| // started, in the case where the client moves on to a new stream before |
| // catching up to latest output config. |
| return; |
| } |
| } // ~lock |
| |
| CoreCodecQueueInputEndOfStream(); |
| } |
| |
| void CodecImpl::onInputConstraintsReady() { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| if (!IsCoreCodecRequiringOutputConfigForFormatDetection()) { |
| return; |
| } |
| std::unique_lock<std::mutex> lock(lock_); |
| StartIgnoringClientOldOutputConfig(lock); |
| GenerateAndSendNewOutputConfig(lock, true); |
| } |
| |
| void CodecImpl::UnbindLocked() { |
| // We must have first gotten far enough through BindAsync() before calling |
| // UnbindLocked(). |
| ZX_DEBUG_ASSERT(was_logically_bound_); |
| |
| if (was_unbind_started_) { |
| // Ignore the second trigger if we have a near-simultaneous failure from |
| // StreamControl thread (for example) and from fidl_thread() (for |
| // example). The first will start unbinding, and the second will be |
| // ignored. Since completion of the Unbind() call doesn't imply anything |
| // about how done the unbind is, there's no need for the second caller to |
| // be blocked waiting for the first caller's unbind to be done. |
| return; |
| } |
| was_unbind_started_ = true; |
| wake_stream_control_condition_.notify_all(); |
| |
| // Unbind() / UnbindLocked() can be called from any thread. |
| // |
| // Regardless of what thread UnbindLocked() is called on, "this" will remain |
| // allocated at least until the caller of UnbindLocked() releases lock_. |
| // |
| // The shutdown sequence here is meant to be general enough to accommodate code |
| // changes without being super brittle. Not all the potential cases accounted |
| // for in this sequence can necessarily happen currently, but it seems good to |
| // stop all activity in a way that'll hold up even if a change posts another |
| // lambda or similar. |
| // |
| // In all cases, this posted lambda runs after BindAsync()'s work that's |
| // posted to StreamControl, because any/all calls to UnbindLocked() happen |
| // after BindAsync() has posted to StreamControl. |
| PostToStreamControl([this] { |
| // At this point we know that no more streams will be started by |
| // StreamControl ordering domain (thanks to was_unbind_started_ / |
| // IsStoppingLocked() checks), but lambdas posted to the StreamControl |
| // ordering domain (by the fidl_thread() or by core codec) may still |
| // be creating other activity such as posting lambdas to StreamControl or |
| // fidl_thread(). |
| // |
| // There are two purposes to this lock acquire, one of which is subtle. |
| // |
| // This lock acquire also delays execution here until the caller of |
| // UnbindLocked() has released lock_. This delay is nice to do on the |
| // stream control thread instead of later on the fidl_thread(), and |
| // we need the lock here to call EnsureStreamClosed() anyway. |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| // Stop core codec associated with this CodecImpl, partly to make sure it |
| // stops running code that could make calls into this CodecImpl, and |
| // partly to ensure the core codec isn't in the middle of anything when it |
| // gets deleted. |
| // |
| // We know the core codec won't start more activity because the core codec |
| // isn't allowed to initiate actions while there's no active stream, and |
| // because no new active stream will be created. All _StreamControl |
| // methods check IsStoppingLocked() at the start, and the StreamControl |
| // ordering domain is the only domain that ever starts a stream. |
| // |
| // We intentionally don't check for IsStoppingLocked() in protocol |
| // dispatch methods running on fidl_thread(). For example the codec |
| // must tolerate calls to configure buffers after EnsureStreamClosed() |
| // here. The Unbind() later is what silences the protocol message |
| // dispatch methods. Checking for IsStoppingLocked() in protocol dispatch |
| // methods would only decrease the probability of certain event orderings, |
| // not eliminate those orderings, so it's actually better to let them |
| // happen to get more coverage of those orderings. |
| if (is_core_codec_init_called_) { |
| EnsureStreamClosed(lock); |
| } |
| } // ~lock |
| |
| PostToSharedFidl([this] { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| // If not being called from binding_'s error handler, unbind from the |
| // channel so we won't see any more incoming FIDL messages. This binding |
| // doesn't own "this". |
| // |
| // The Unbind() stops any additional FIDL dispatching re. this CodecImpl, |
| // but it doesn't stop lambdas re. this CodecImpl from being queued to |
| // fidl_thread(). Potentially such lambdas can be coming from |
| // StreamControl domain still at this point (even after the Unbind()). |
| // Those which are sending a FIDL message can omit their send if binding_ |
| // is no longer bound, but they need binding_ to still be allocated when |
| // they run. |
| // |
| // We close the Codec channel only after ~CodecAdmission has freed up the |
| // concurrency tally, so that a client that re-tries on channel closure |
| // can retry immediately without potentially bouncing off still-existing |
| // old CodecAdmission. |
| if (binding_.is_bound()) { |
| codec_to_close_ = binding_.Unbind().TakeChannel(); |
| } |
| |
| // We need to shut down the StreamControl thread, which can be shut down |
| // quickly (it's not waiting any significant duration on anything) thanks |
| // to was_unbind_started_ and wake_stream_control_condition_. Normally |
| // the fidl_thread() waiting for the StreamControl thread to do |
| // anything would be bad, because the fidl_thread() is non-blocking |
| // and the StreamControl thread can block on stuff, but StreamControl |
| // thread behavior after was_unbind_started_ = true and |
| // wake_stream_control_condition_.notify_all() does not block and does not |
| // wait on fidl_thread(). So in this case it's ok to wait here. |
| stream_control_loop_.Quit(); |
| stream_control_loop_.JoinThreads(); |
| // This is when we first know that StreamControl can't be queueing any |
| // more lambdas re. this CodecImpl toward fidl_thread(). (We |
| // already know the core codec isn't queuing any more). If any lambdas |
| // are queued to StreamControl at/beyond this point, we rely on those |
| // being safe to just delete. |
| stream_control_loop_.Shutdown(); |
| |
| // Before calling the owner_error_handler_, we declare that unbind is |
| // done so that during the destructor we can check that unbind is done. |
| was_unbind_completed_ = true; |
| |
| // This post ensures that any other items posted to the |
| // fidl_thread() for this CodecImpl run before "delete this". By |
| // the time we post here, we know that no further lambdas will be posted |
| // to fidl_thread() regarding this CodecImpl other than this post |
| // itself - specifically: |
| // * The core codec has been stopped, in the sense that it has no |
| // current stream. The core codec is required to be delete-able when |
| // it has no current stream, and required not to asynchronously post |
| // more work to the CodecImpl (because calling onCoreCodec... methods |
| // is not allowed when there is no current stream). |
| // * The binding_.Unbind() has run, so no more FIDL dispatching to this |
| // CodecImpl. |
| // * The stream_control_loop_.JoinThreads() has run, so no more posting |
| // from the stream_control_thread_ since it's no longer running. |
| // * The previous bullets are the complete list of sources of items |
| // posted to the fidl_thread() regarding this CodecImpl. |
| // |
| // By posting to run _after_ any of the above sources, we know that by the |
| // time this posted lambda runs, the "delete this" in this lambda will be |
| // after any other posted lambdas. |
| // |
| // For example, any lambdas previously posted to send a message via |
| // this->binding_ (which is soon to be deleted) will run before the lambda |
| // posted here. |
| // |
| // This relies on other previously-posted _lambdas_ running on |
| // fidl_thread() re. this CodecImpl to not re-post to the fidl_thread(). |
| // In contrast, it is ok if a FIDL dispatch (on FIDL thread) re-posts to |
| // the fidl_thread(); this is ok because we've already stopped any more |
| // FIDL dispatching by binding_.Unbind() above. |
| PostSerial(shared_fidl_dispatcher_, |
| [client_error_handler = std::move(owner_error_handler_)] { |
| // This call deletes the CodecImpl. |
| client_error_handler(); |
| }); |
| // "this" will be deleted shortly async when lambda posted just above |
| // runs. |
| return; |
| }); |
| }); |
| // "this" remains allocated until caller releases lock_. |
| } |
| |
| void CodecImpl::Unbind() { |
| std::lock_guard<std::mutex> lock(lock_); |
| UnbindLocked(); |
| // ~lock |
| // |
| // "this" may be deleted very shortly after ~lock, depending on what thread |
| // Unbind() is called from. |
| } |
| |
| bool CodecImpl::IsStreamActiveLocked() { |
| return stream_lifetime_ordinal_ % 2 == 1; |
| } |
| |
| void CodecImpl::SetBufferSettingsCommon( |
| std::unique_lock<std::mutex>& lock, CodecPort port, |
| const fuchsia::mediacodec::CodecPortBufferSettings& settings, |
| const fuchsia::mediacodec::CodecBufferConstraints& constraints) { |
| ZX_DEBUG_ASSERT(port == kInputPort && |
| thrd_current() == stream_control_thread_ || |
| port == kOutputPort && thrd_current() == fidl_thread()); |
| ZX_DEBUG_ASSERT(!IsStoppingLocked()); |
| // Invariant |
| // |
| // Either we've never seen settings, or the logical buffer_lifetime_ordinal_ |
| // is either the last accepted from the client or one more than that as a way |
| // of cleanly permitting the server to unilaterally de-configure output |
| // buffers. |
| ZX_DEBUG_ASSERT( |
| (!port_settings_[port && buffer_lifetime_ordinal_[port] == 0]) || |
| (buffer_lifetime_ordinal_[port] >= |
| port_settings_[port]->buffer_lifetime_ordinal && |
| buffer_lifetime_ordinal_[port] <= |
| port_settings_[port]->buffer_lifetime_ordinal + 1)); |
| if (settings.buffer_lifetime_ordinal <= |
| protocol_buffer_lifetime_ordinal_[port]) { |
| FailLocked( |
| "settings.buffer_lifetime_ordinal <= " |
| "protocol_buffer_lifetime_ordinal_[port] - port: %d", |
| port); |
| return; |
| } |
| protocol_buffer_lifetime_ordinal_[port] = settings.buffer_lifetime_ordinal; |
| |
| if (settings.buffer_lifetime_ordinal % 2 == 0) { |
| FailLocked( |
| "Only odd values for buffer_lifetime_ordinal are permitted - port: %d " |
| "value %lu", |
| port, settings.buffer_lifetime_ordinal); |
| return; |
| } |
| |
| if (settings.buffer_constraints_version_ordinal > |
| sent_buffer_constraints_version_ordinal_[port]) { |
| FailLocked( |
| "Client sent too-new buffer_constraints_version_ordinal - port: %d", |
| port); |
| return; |
| } |
| |
| if (settings.buffer_constraints_version_ordinal < |
| last_required_buffer_constraints_version_ordinal_[port]) { |
| // ignore - client will probably catch up later |
| return; |
| } |
| |
| // We've peeled off too new and too old above. |
| ZX_DEBUG_ASSERT(settings.buffer_constraints_version_ordinal >= |
| last_required_buffer_constraints_version_ordinal_[port] && |
| settings.buffer_constraints_version_ordinal <= |
| sent_buffer_constraints_version_ordinal_[port]); |
| |
| // We've already checked above that the buffer_lifetime_ordinal is in |
| // sequence. |
| ZX_DEBUG_ASSERT(!port_settings_[port] || settings.buffer_lifetime_ordinal > |
| buffer_lifetime_ordinal_[port]); |
| |
| if (!ValidateBufferSettingsVsConstraintsLocked(port, settings, constraints)) { |
| // This assert is safe only because this thread still holds lock_. |
| ZX_DEBUG_ASSERT(IsStoppingLocked()); |
| return; |
| } |
| |
| // Little if any reason to do this outside the lock. |
| EnsureBuffersNotConfigured(lock, port); |
| |
| // This also starts the new buffer_lifetime_ordinal. |
| port_settings_[port] = |
| std::make_unique<fuchsia::mediacodec::CodecPortBufferSettings>( |
| std::move(settings)); |
| buffer_lifetime_ordinal_[port] = |
| port_settings_[port]->buffer_lifetime_ordinal; |
| } |
| |
| void CodecImpl::EnsureBuffersNotConfigured(std::unique_lock<std::mutex>& lock, |
| CodecPort port) { |
| // This method can be called on input only if there's no current stream. |
| // |
| // On output, this method can be called if there's no current stream or if |
| // we're in the middle of an output config change. |
| // |
| // On input, this can only be called on stream_control_thread_. |
| // |
| // On output, this can be called on stream_control_thread_ or output_thread_. |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_ || |
| (port == kOutputPort && (thrd_current() == fidl_thread()))); |
| |
| is_port_configured_[port] = false; |
| |
| // Ensure that buffers aren't with the core codec. |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| CoreCodecEnsureBuffersNotConfigured(port); |
| } |
| |
| // For mid-stream output config change, the caller is responsible for ensuring |
| // that buffers are not with the HW first. |
| // |
| // TODO(dustingreen): Check anything relevant to buffers not presently being |
| // with the HW. |
| // ZX_DEBUG_ASSERT(all_packets_[port].empty() || |
| // !all_packets_[port][0]->is_with_hw()); |
| |
| all_packets_[port].clear(); |
| all_buffers_[port].clear(); |
| ZX_DEBUG_ASSERT(all_packets_[port].empty()); |
| ZX_DEBUG_ASSERT(all_buffers_[port].empty()); |
| } |
| |
| bool CodecImpl::ValidateBufferSettingsVsConstraintsLocked( |
| CodecPort port, |
| const fuchsia::mediacodec::CodecPortBufferSettings& settings, |
| const fuchsia::mediacodec::CodecBufferConstraints& constraints) { |
| if (settings.packet_count_for_codec < |
| constraints.packet_count_for_codec_min) { |
| FailLocked("packet_count_for_codec < packet_count_for_codec_min"); |
| return false; |
| } |
| if (settings.packet_count_for_codec > |
| constraints.packet_count_for_codec_max) { |
| FailLocked("packet_count_for_codec > packet_count_for_codec_max"); |
| return false; |
| } |
| if (settings.packet_count_for_client > |
| constraints.packet_count_for_client_max) { |
| FailLocked("packet_count_for_client > packet_count_for_client_max"); |
| return false; |
| } |
| if (settings.per_packet_buffer_bytes < |
| constraints.per_packet_buffer_bytes_min) { |
| FailLocked("per_packet_buffer_bytes < per_packet_buffer_bytes_min"); |
| return false; |
| } |
| if (settings.per_packet_buffer_bytes > |
| constraints.per_packet_buffer_bytes_max) { |
| FailLocked("per_packet_buffer_bytes > per_packet_buffer_bytes_max"); |
| return false; |
| } |
| if (settings.single_buffer_mode && !constraints.single_buffer_mode_allowed) { |
| FailLocked("single_buffer_mode && !single_buffer_mode_allowed"); |
| return false; |
| } |
| return true; |
| } |
| |
| bool CodecImpl::AddBufferCommon(CodecPort port, |
| fuchsia::mediacodec::CodecBuffer buffer) { |
| ZX_DEBUG_ASSERT(port == kInputPort && |
| (thrd_current() == stream_control_thread_) || |
| port == kOutputPort && (thrd_current() == fidl_thread())); |
| bool done_configuring = false; |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| |
| if (buffer.buffer_lifetime_ordinal % 2 == 0) { |
| FailLocked( |
| "Client sent even buffer_lifetime_ordinal, but must be odd - exiting " |
| "- port: %u\n", |
| port); |
| return false; |
| } |
| |
| if (buffer.buffer_lifetime_ordinal != |
| protocol_buffer_lifetime_ordinal_[port]) { |
| FailLocked( |
| "Incoherent SetOutputBufferSettings()/SetInputBufferSettings() + " |
| "AddOutputBuffer()/AddInputBuffer()s - exiting - port: %d\n", |
| port); |
| return false; |
| } |
| |
| // If the server is not interested in the client's buffer_lifetime_ordinal, |
| // the client's buffer_lifetime_ordinal won't match the server's |
| // buffer_lifetime_ordinal_. The client will probably later catch up. |
| if (buffer.buffer_lifetime_ordinal != buffer_lifetime_ordinal_[port]) { |
| // The case that ends up here is when a client's output configuration |
| // (whole or last part) is being ignored because it's not yet caught up |
| // with last_required_buffer_constraints_version_ordinal_. |
| |
| // This case won't happen for input, at least for now. This is an assert |
| // rather than a client behavior check, because previous client protocol |
| // checks have already peeled off any invalid client behavior that might |
| // otherwise cause this assert to trigger. |
| ZX_DEBUG_ASSERT(port == kOutputPort); |
| |
| // Ignore the client's message. The client will probably catch up later. |
| return false; |
| } |
| |
| if (buffer.buffer_index != all_buffers_[port].size()) { |
| FailLocked( |
| "AddOutputBuffer()/AddInputBuffer() had buffer_index out of sequence " |
| "- port: %d buffer_index: %u all_buffers_[port].size(): %lu", |
| port, buffer.buffer_index, all_buffers_[port].size()); |
| return false; |
| } |
| |
| uint32_t required_buffer_count = |
| BufferCountFromPortSettings(*port_settings_[port]); |
| if (buffer.buffer_index >= required_buffer_count) { |
| FailLocked("AddOutputBuffer()/AddInputBuffer() extra buffer - port: %d", |
| port); |
| return false; |
| } |
| |
| // So far, there's little reason to avoid doing the Init() part under the |
| // lock, even if it can be a bit more time consuming, since there's no data |
| // processing happening at this point anyway, and there wouldn't be any |
| // happening in any other code location where we could potentially move the |
| // Init() either. |
| |
| std::unique_ptr<CodecBuffer> local_buffer = std::unique_ptr<CodecBuffer>( |
| new CodecBuffer(this, port, std::move(buffer))); |
| if (!local_buffer->Init()) { |
| FailLocked( |
| "AddOutputBuffer()/AddInputBuffer() couldn't Init() new buffer - " |
| "port: %d", |
| port); |
| return false; |
| } |
| { |
| ScopedUnlock unlock(lock); |
| // Inform the core codec up-front about each buffer. |
| CoreCodecAddBuffer(port, local_buffer.get()); |
| } |
| all_buffers_[port].push_back(std::move(local_buffer)); |
| if (all_buffers_[port].size() == required_buffer_count) { |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[port] == |
| port_settings_[port]->buffer_lifetime_ordinal); |
| // Stash this while we can, before the client de-configures. |
| last_provided_buffer_constraints_version_ordinal_[port] = |
| port_settings_[port]->buffer_constraints_version_ordinal; |
| // Now we allocate all_packets_[port]. |
| ZX_DEBUG_ASSERT(all_packets_[port].empty()); |
| uint32_t packet_count = |
| PacketCountFromPortSettings(*port_settings_[port]); |
| for (uint32_t i = 0; i < packet_count; i++) { |
| // Private constructor to prevent core codec maybe creating its own |
| // Packet instances (which isn't the intent) seems worth the hassle of |
| // not using make_unique<>() here. |
| all_packets_[port].push_back(std::unique_ptr<CodecPacket>( |
| new CodecPacket(port_settings_[port]->buffer_lifetime_ordinal, i))); |
| } |
| |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| |
| // A core codec can take action here to finish configuring buffers if |
| // it's able, or can delay configuring buffers until |
| // CoreCodecStartStream() if that works better for the core codec. |
| CoreCodecConfigureBuffers(port, all_packets_[port]); |
| |
| // All output packets need to start with the core codec. This is |
| // implicit for the Codec interface (implied by adding the last output |
| // buffer) but explicit in the CodecAdapter interface. |
| if (port == kOutputPort) { |
| for (uint32_t i = 0; i < packet_count; i++) { |
| CoreCodecRecycleOutputPacket(all_packets_[kOutputPort][i].get()); |
| } |
| } |
| } // ~unlock |
| |
| // For OMX case, we tell OMX about the potentially-new buffer count |
| // separately later, just before moving from OMX loaded to OMX idle, or as |
| // part of mid-stream output config change. |
| |
| // For OMX case, we don't allocate OMX_BUFFERHEADERTYPE yet here by |
| // calling OMX UseBuffer() yet, because we can be in OMX_StateLoaded |
| // currently, and OMX UseBuffer() isn't valid until we're moving from |
| // OMX_StateLoaded to OMX_StateIdle. |
| |
| is_port_configured_[port] = true; |
| done_configuring = true; |
| } |
| } |
| return done_configuring; |
| } |
| |
| bool CodecImpl::CheckOldBufferLifetimeOrdinalLocked( |
| CodecPort port, uint64_t buffer_lifetime_ordinal) { |
| // The client must only send odd values. 0 is even so we don't need a |
| // separate check for that. |
| if (buffer_lifetime_ordinal % 2 == 0) { |
| FailLocked( |
| "CheckOldBufferLifetimeOrdinalLocked() - buffer_lifetime_ordinal must " |
| "be odd"); |
| return false; |
| } |
| if (buffer_lifetime_ordinal > protocol_buffer_lifetime_ordinal_[port]) { |
| FailLocked( |
| "client sent new buffer_lifetime_ordinal in message type that doesn't " |
| "allow new buffer_lifetime_ordinals"); |
| return false; |
| } |
| return true; |
| } |
| |
| bool CodecImpl::CheckStreamLifetimeOrdinalLocked( |
| uint64_t stream_lifetime_ordinal) { |
| if (stream_lifetime_ordinal % 2 != 1) { |
| FailLocked("stream_lifetime_ordinal must be odd.\n"); |
| return false; |
| } |
| if (stream_lifetime_ordinal < stream_lifetime_ordinal_) { |
| FailLocked("client sent stream_lifetime_ordinal that went backwards"); |
| return false; |
| } |
| return true; |
| } |
| |
| bool CodecImpl::StartNewStream(std::unique_lock<std::mutex>& lock, |
| uint64_t stream_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| ZX_DEBUG_ASSERT((stream_lifetime_ordinal % 2 == 1) && |
| "new stream_lifetime_ordinal must be odd"); |
| |
| if (IsStoppingLocked()) { |
| // Don't start a new stream if the whole CodecImpl is already stopping. |
| // |
| // A completely different path will take care of calling |
| // EnsureStreamClosed() during CodecImpl stop. |
| // |
| // TODO(dustingreen): If all callers are already checking this at the top |
| // of each relevant .*_StreamControl method, then we don't necessarily need |
| // this check, but consider any intervals where lock_ isn't held also - we |
| // don't want the wait for stream_control_thread_ to exit to ever be long |
| // when stopping this CodecImpl. |
| return false; |
| } |
| |
| EnsureStreamClosed(lock); |
| |
| ZX_DEBUG_ASSERT((stream_lifetime_ordinal_ % 2 == 0) && |
| "expecting no current stream"); |
| ZX_DEBUG_ASSERT(!stream_); |
| |
| // Now it's time to start the new stream. We start the new stream at |
| // Codec layer first then core codec layer. |
| |
| if (!IsInputConfiguredLocked()) { |
| FailLocked( |
| "input not configured before start of stream (QueueInputPacket())"); |
| return false; |
| } |
| |
| ZX_DEBUG_ASSERT(stream_queue_.size() >= 1); |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal == |
| stream_queue_.front()->stream_lifetime_ordinal()); |
| stream_ = stream_queue_.front().get(); |
| // Update the stream_lifetime_ordinal_ to the new stream. We need to do |
| // this before we send new output config, since the output config will be |
| // generated using the current stream ordinal. |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal > stream_lifetime_ordinal_); |
| stream_lifetime_ordinal_ = stream_lifetime_ordinal; |
| ZX_DEBUG_ASSERT(stream_->stream_lifetime_ordinal() == |
| stream_lifetime_ordinal_); |
| |
| // The client is not permitted to unilaterally re-configure output while a |
| // stream is active, but the client may still be responding to a previous |
| // server-initiated mid-stream format change. |
| // |
| // ########################################################################### |
| // We don't attempt to optimize every case as much as might be possible here. |
| // The main overall optimization is that it's possible to switch streams |
| // without reallocating buffers. We also need to make sure it's possible to |
| // detect output format at the start of a stream regardless of what happened |
| // before, and possible to perform a mid-stream format change. |
| // ########################################################################### |
| // |
| // Given the above, our *main concern* here is that we get to a state where we |
| // *know* the client isn't trying to re-configure output during format |
| // detection, which at best would be confusing to allow, so we avoid that |
| // possibility here by forcing a client to catch up with the server, if there's |
| // *any possibility* that the client might still be working on catching up |
| // with the server. |
| // |
| // If the client's most recently fully-completed output config is less than |
| // the most recently sent output constraints with action_required true, then |
| // we force an even fresher output constraints here tagged as being relevant |
| // to the current stream, and wait for the client to catch up to that before |
| // continuing. By marking as being for this stream, we ensure that the client |
| // will bother to finish configuring output, which gets us to a state where we |
| // know it's safe to do another mid-stream format change as needed (vs. the |
| // client maybe finishing the old config or maybe not). |
| // |
| // We also force the client to catch up if the core codec previously indicated |
| // that the current config is "meh". This may not be strictly necessary since |
| // the "meh" was with respect to the old stream, but just in case a core codec |
| // cares, we move on from the old config before delivering new stream data. |
| // |
| // Some core codecs (such as OMX codecs) require the output to be configured |
| // to _something_ as they don't support giving us the real output config |
| // unless the output is configured to at least something at first. |
| // |
| // Other core codecs (such as some HW-based codecs) can deal with no output |
| // configured while detecting the output format, but even for those codecs, we |
| // only do this if the above cases don't apply. These codecs have to deal |
| // with an output config that's already set across a stream switch anyway, to |
| // permit buffers to stay configured across a stream switch when possible, so |
| // the cases above potentially setting an output config that's not super |
| // relevant to the new stream doesn't really complicate the core codec since |
| // an old stream's config might not be super relevant to a new stream either. |
| // |
| // Format detection is separate and handled like a mid-stream format change. |
| // This stuff here is just getting output config into a non-changing state |
| // before we start format detection. |
| bool is_new_config_needed; |
| // The statement below could obviously be re-written as a giant boolean |
| // expression, but this way seems easier to comment. |
| if (last_provided_buffer_constraints_version_ordinal_[kOutputPort] < |
| last_required_buffer_constraints_version_ordinal_[kOutputPort]) { |
| // The client _might_ still be trying to catch up, so to disambiguate, |
| // require an even fresher config with respect to this new stream to |
| // unambiguously force the client to catch up to the even newer config. |
| is_new_config_needed = true; |
| } else if (IsCoreCodecRequiringOutputConfigForFormatDetection() && |
| !IsOutputConfiguredLocked()) { |
| // The core codec requires output to be configured before format detection, |
| // so we force the client to provide an output config before format |
| // detection. |
| is_new_config_needed = true; |
| } else if (IsOutputConfiguredLocked() && |
| port_settings_[kOutputPort]->buffer_constraints_version_ordinal <= |
| core_codec_meh_output_buffer_constraints_version_ordinal_) { |
| // The core codec previously expressed "meh" regarding the current config's |
| // buffer_constraints_version_ordinal, so to avoid mixing that with core |
| // codec stream switch, force the client to configure output buffers before |
| // format detection for the new stream. |
| is_new_config_needed = true; |
| } else { |
| // The core codec is ok to perform format detection in the current state, |
| // and we know that a well-behaved client is not currently trying to change |
| // the output config. |
| is_new_config_needed = false; |
| } |
| |
| if (is_new_config_needed) { |
| StartIgnoringClientOldOutputConfig(lock); |
| EnsureBuffersNotConfigured(lock, kOutputPort); |
| // This does count as a mid-stream output config change, even when this is |
| // at the start of a stream - it's still while a stream is active, and still |
| // prevents this stream from outputting any data to the Codec client until |
| // the Codec client re-configures output while this stream is active. |
| GenerateAndSendNewOutputConfig(lock, true); |
| |
| // Now we can wait for the client to catch up to the current output config |
| // or for the client to tell the server to discard the current stream. |
| while (!IsStoppingLocked() && !stream_->future_discarded() && |
| !IsOutputConfiguredLocked()) { |
| wake_stream_control_condition_.wait(lock); |
| } |
| |
| if (IsStoppingLocked()) { |
| return false; |
| } |
| |
| if (stream_->future_discarded()) { |
| // A discarded stream isn't an error for the CodecImpl instance. |
| return true; |
| } |
| } |
| |
| // Now we have input configured, and output configured if needed by the core |
| // codec, so we can move the core codec to running state. |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| CoreCodecStartStream(); |
| } // ~unlock |
| |
| // Track this so the core codec doesn't have to bother with "ensure" |
| // semantics, just start/stop, where stop isn't called unless the core codec |
| // has a started stream. |
| is_core_codec_stream_started_ = true; |
| |
| return true; |
| } |
| |
| void CodecImpl::EnsureStreamClosed(std::unique_lock<std::mutex>& lock) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| // Stop the core codec, by using this thread to directly drive the core codec |
| // from running to stopped (if not already stopped). We do this first so the |
| // core codec won't try to send us output while we have no stream at the Codec |
| // layer. |
| if (is_core_codec_stream_started_) { |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| CoreCodecStopStream(); |
| } |
| is_core_codec_stream_started_ = false; |
| } |
| |
| // Now close the old stream at the Codec layer. |
| EnsureCodecStreamClosedLockedInternal(); |
| |
| ZX_DEBUG_ASSERT((stream_lifetime_ordinal_ % 2 == 0) && |
| "expecting no current stream"); |
| ZX_DEBUG_ASSERT(!stream_); |
| } |
| |
| // The only valid caller of this is EnsureStreamClosed(). We have this in a |
| // separate method only to make it easier to assert a couple things in the |
| // caller. |
| void CodecImpl::EnsureCodecStreamClosedLockedInternal() { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| if (stream_lifetime_ordinal_ % 2 == 0) { |
| // Already closed. |
| return; |
| } |
| ZX_DEBUG_ASSERT(stream_queue_.front()->stream_lifetime_ordinal() == |
| stream_lifetime_ordinal_); |
| stream_ = nullptr; |
| stream_queue_.pop_front(); |
| stream_lifetime_ordinal_++; |
| // Even values mean no current stream. |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal_ % 2 == 0); |
| } |
| |
| // This is called on Output ordering domain (FIDL thread) any time a message is |
| // received which would be able to start a new stream. |
| // |
| // More complete protocol validation happens on StreamControl ordering domain. |
| // The validation here is just to validate to degree needed to not break our |
| // stream_queue_ and future_stream_lifetime_ordinal_. |
| bool CodecImpl::EnsureFutureStreamSeenLocked(uint64_t stream_lifetime_ordinal) { |
| if (future_stream_lifetime_ordinal_ == stream_lifetime_ordinal) { |
| return true; |
| } |
| if (stream_lifetime_ordinal < future_stream_lifetime_ordinal_) { |
| FailLocked("stream_lifetime_ordinal went backward - exiting\n"); |
| return false; |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal > future_stream_lifetime_ordinal_); |
| if (future_stream_lifetime_ordinal_ % 2 == 1) { |
| if (!EnsureFutureStreamCloseSeenLocked(future_stream_lifetime_ordinal_)) { |
| return false; |
| } |
| } |
| future_stream_lifetime_ordinal_ = stream_lifetime_ordinal; |
| stream_queue_.push_back(std::make_unique<Stream>(stream_lifetime_ordinal)); |
| if (stream_queue_.size() > kMaxInFlightStreams) { |
| FailLocked( |
| "kMaxInFlightStreams reached - clients capable of causing this are " |
| "instead supposed to wait/postpone to prevent this from occurring - " |
| "exiting\n"); |
| return false; |
| } |
| return true; |
| } |
| |
| // This is called on Output ordering domain (FIDL thread) any time a message is |
| // received which would close a stream. |
| // |
| // More complete protocol validation happens on StreamControl ordering domain. |
| // The validation here is just to validate to degree needed to not break our |
| // stream_queue_ and future_stream_lifetime_ordinal_. |
| bool CodecImpl::EnsureFutureStreamCloseSeenLocked( |
| uint64_t stream_lifetime_ordinal) { |
| if (future_stream_lifetime_ordinal_ % 2 == 0) { |
| // Already closed. |
| if (stream_lifetime_ordinal != future_stream_lifetime_ordinal_ - 1) { |
| FailLocked( |
| "CloseCurrentStream() seen with stream_lifetime_ordinal != " |
| "most-recent seen stream"); |
| return false; |
| } |
| return true; |
| } |
| if (stream_lifetime_ordinal != future_stream_lifetime_ordinal_) { |
| FailLocked("attempt to close a stream other than the latest seen stream"); |
| return false; |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal == future_stream_lifetime_ordinal_); |
| ZX_DEBUG_ASSERT(stream_queue_.size() >= 1); |
| Stream* closing_stream = stream_queue_.back().get(); |
| ZX_DEBUG_ASSERT(closing_stream->stream_lifetime_ordinal() == |
| stream_lifetime_ordinal); |
| // It is permitted to see a FlushCurrentStream() before a CloseCurrentStream() |
| // and this can make sense if a client just wants to inform the server of all |
| // stream closes, or if the client wants to release_input_buffers or |
| // release_output_buffers after the flush is done. |
| // |
| // If we didn't previously flush, then this close is discarding. |
| if (!closing_stream->future_flush_end_of_stream()) { |
| closing_stream->SetFutureDiscarded(); |
| } |
| future_stream_lifetime_ordinal_++; |
| ZX_DEBUG_ASSERT(future_stream_lifetime_ordinal_ % 2 == 0); |
| return true; |
| } |
| |
| // This is called on Output ordering domain (FIDL thread) any time a flush is |
| // seen. |
| // |
| // More complete protocol validation happens on StreamControl ordering domain. |
| // The validation here is just to validate to degree needed to not break our |
| // stream_queue_ and future_stream_lifetime_ordinal_. |
| bool CodecImpl::EnsureFutureStreamFlushSeenLocked( |
| uint64_t stream_lifetime_ordinal) { |
| if (stream_lifetime_ordinal != future_stream_lifetime_ordinal_) { |
| FailLocked("FlushCurrentStream() stream_lifetime_ordinal inconsistent"); |
| return false; |
| } |
| ZX_DEBUG_ASSERT(stream_queue_.size() >= 1); |
| Stream* flushing_stream = stream_queue_.back().get(); |
| // Thanks to the above future_stream_lifetime_ordinal_ check, we know the |
| // future stream is not discarded yet. |
| ZX_DEBUG_ASSERT(!flushing_stream->future_discarded()); |
| if (flushing_stream->future_flush_end_of_stream()) { |
| FailLocked("FlushCurrentStream() used twice on same stream"); |
| return false; |
| } |
| |
| // We don't future-verify that we have a QueueInputEndOfStream(). We'll verify |
| // that later when StreamControl catches up to this stream. |
| |
| // Remember the flush so we later know that a close doesn't imply discard. |
| flushing_stream->SetFutureFlushEndOfStream(); |
| |
| // A FlushEndOfStreamAndCloseStream() is also a close, after the flush. This |
| // keeps future_stream_lifetime_ordinal_ consistent. |
| if (!EnsureFutureStreamCloseSeenLocked(stream_lifetime_ordinal)) { |
| return false; |
| } |
| return true; |
| } |
| |
| // This method is only called when buffer_constraints_action_required will be |
| // true in an OnOutputConfig() message sent shortly after this method call. |
| // |
| // Even if the client is switching streams rapidly without configuring output, |
| // this method and GenerateAndSendNewOutputConfig() with |
| // buffer_constraints_action_required true always run in pairs. |
| // |
| // This is what starts the interval during which |
| // OmxTryRecycleOutputPacketLocked() won't call OMX. |
| // |
| // If the client is in the middle of configuring output, we'll start ignoring |
| // the client's messages re. the old buffer_lifetime_ordinal and old |
| // buffer_constraints_version_ordinal until the client catches up to the new |
| // last_required_buffer_constraints_version_ordinal_[kOutputPort]. |
| void CodecImpl::StartIgnoringClientOldOutputConfig( |
| std::unique_lock<std::mutex>& lock) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| |
| // The buffer_lifetime_ordinal_[kOutputPort] can be even on entry due to at |
| // least two cases: 0, and when the client is switching streams repeatedly |
| // without setting a new buffer_lifetime_ordinal_[kOutputPort]. |
| if (buffer_lifetime_ordinal_[kOutputPort] % 2 == 1) { |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[kOutputPort] % 2 == 1); |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[kOutputPort] == |
| port_settings_[kOutputPort]->buffer_lifetime_ordinal); |
| buffer_lifetime_ordinal_[kOutputPort]++; |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[kOutputPort] % 2 == 0); |
| ZX_DEBUG_ASSERT(buffer_lifetime_ordinal_[kOutputPort] == |
| port_settings_[kOutputPort]->buffer_lifetime_ordinal + 1); |
| } |
| |
| // When buffer_constraints_action_required true, we can assert in |
| // GenerateAndSendNewOutputConfig() that this value is still the |
| // next_output_buffer_constraints_version_ordinal_ in that method. |
| last_required_buffer_constraints_version_ordinal_[kOutputPort] = |
| next_output_buffer_constraints_version_ordinal_; |
| |
| // Now that we've stopped any new calls to CoreCodecRecycleOutputPacket(), |
| // fence through any previously-started call to CoreCodecRecycleOutputPacket() |
| // that maybe have been started previously, before returning from this method. |
| // |
| // We can't just be holding lock_ during the call to |
| // CoreCodecRecycleOutputPacket() because it acquires the video_decoder_lock_ |
| // and in other paths the video_decoder_lock_ is held while acquiring lock_. |
| // |
| // It's ok for the StreamControl domain to wait on the Output domain (but not |
| // the other way around). |
| bool is_output_ordering_domain_done_with_recycle_output_packet = false; |
| std::condition_variable condition_changed; |
| PostToSharedFidl([this, |
| &is_output_ordering_domain_done_with_recycle_output_packet, |
| &condition_changed] { |
| { // scope lock |
| std::lock_guard<std::mutex> lock(lock_); |
| is_output_ordering_domain_done_with_recycle_output_packet = true; |
| } |
| condition_changed.notify_all(); |
| }); |
| while (!is_output_ordering_domain_done_with_recycle_output_packet) { |
| condition_changed.wait(lock); |
| } |
| ZX_DEBUG_ASSERT(is_output_ordering_domain_done_with_recycle_output_packet); |
| } |
| |
| void CodecImpl::GenerateAndSendNewOutputConfig( |
| std::unique_lock<std::mutex>& lock, |
| bool buffer_constraints_action_required) { |
| // When client action is required, this can only happen on the StreamControl |
| // ordering domain. When client action is not required, it can happen from |
| // the InputData ordering domain. |
| ZX_DEBUG_ASSERT(buffer_constraints_action_required && |
| thrd_current() == stream_control_thread_ || |
| !buffer_constraints_action_required && |
| IsPotentiallyCoreCodecThread()); |
| |
| uint64_t current_stream_lifetime_ordinal = stream_lifetime_ordinal_; |
| uint64_t new_output_buffer_constraints_version_ordinal = |
| next_output_buffer_constraints_version_ordinal_++; |
| uint64_t new_output_format_details_version_ordinal = |
| next_output_format_details_version_ordinal_++; |
| |
| // If buffer_constraints_action_required true, the caller bumped the |
| // last_required_buffer_constraints_version_ordinal_[kOutputPort] before |
| // calling this method (using StartIgnoringClientOldOutputConfig()), to |
| // ensure any output config messages from the client are ignored until the |
| // client catches up to at least |
| // last_required_buffer_constraints_version_ordinal_. |
| ZX_DEBUG_ASSERT( |
| !buffer_constraints_action_required || |
| (last_required_buffer_constraints_version_ordinal_[kOutputPort] == |
| new_output_buffer_constraints_version_ordinal)); |
| |
| // printf("GenerateAndSendNewOutputConfig |
| // new_output_buffer_constraints_version_ordinal: %lu |
| // buffer_constraints_action_required: %d\n", |
| // new_output_buffer_constraints_version_ordinal, |
| // buffer_constraints_action_required); |
| |
| std::unique_ptr<const fuchsia::mediacodec::CodecOutputConfig> output_config; |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| // Don't call the core codec under the lock_, because we can avoid doing so, |
| // and to allow the core codec to use this thread to call back into |
| // CodecImpl using this stack if needed. So far we don't have any actual |
| // known examples of a core codec using this thread to call back into |
| // CodecImpl using this stack. |
| output_config = CoreCodecBuildNewOutputConfig( |
| current_stream_lifetime_ordinal, |
| new_output_buffer_constraints_version_ordinal, |
| new_output_format_details_version_ordinal, |
| buffer_constraints_action_required); |
| } // ~unlock |
| // We only call GenerateAndSendNewOutputConfig() from contexts that won't be |
| // changing the stream_lifetime_ordinal_, so the fact that we released the |
| // lock above doesn't mean the stream_lifetime_ordinal_ could have changed, so |
| // we can assert here that it's still the same as above. |
| ZX_DEBUG_ASSERT(current_stream_lifetime_ordinal == stream_lifetime_ordinal_); |
| |
| output_config_ = std::move(output_config); |
| |
| // Stay under lock after setting output_config_, to get proper ordering of |
| // sent messages even if a hostile client deduces the content of this message |
| // before we've sent it and manages to get the server to send another |
| // subsequent OnOutputConfig(). |
| |
| ZX_DEBUG_ASSERT(sent_buffer_constraints_version_ordinal_[kOutputPort] + 1 == |
| new_output_buffer_constraints_version_ordinal); |
| ZX_DEBUG_ASSERT(sent_format_details_version_ordinal_[kOutputPort] + 1 == |
| new_output_format_details_version_ordinal); |
| |
| // Setting this within same lock hold interval as we queue the message to be |
| // sent in order vs. other OnOutputConfig() messages. This way we can verify |
| // that the client's incoming messages are not trying to configure with |
| // respect to a buffer_constraints_version_ordinal that is newer than we've |
| // actually sent the client. |
| sent_buffer_constraints_version_ordinal_[kOutputPort] = |
| new_output_buffer_constraints_version_ordinal; |
| sent_format_details_version_ordinal_[kOutputPort] = |
| new_output_format_details_version_ordinal; |
| |
| // Intentional copy of fuchsia::mediacodec::OutputConfig output_config_ here, |
| // as we want output_config_ to remain valid (at least for debugging reasons |
| // for now). |
| PostToSharedFidl( |
| [this, output_config = fidl::Clone(*output_config_)]() mutable { |
| // See "is_bound_checks" comment up top. |
| if (binding_.is_bound()) { |
| binding_.events().OnOutputConfig(std::move(output_config)); |
| } |
| }); |
| } |
| |
| void CodecImpl::MidStreamOutputConfigChange(uint64_t stream_lifetime_ordinal) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (stream_lifetime_ordinal < stream_lifetime_ordinal_) { |
| // ignore; The omx_meh_output_buffer_constraints_version_ordinal_ took |
| // care of it. |
| return; |
| } |
| ZX_DEBUG_ASSERT(stream_lifetime_ordinal == stream_lifetime_ordinal_); |
| |
| // Now we need to start disabling the port, wait for buffers to come back |
| // from OMX, free buffer headers, wait for the port to become fully |
| // disabled, unilaterally de-configure output buffers, demand a new output |
| // config from the client, wait for the client to configure output (but be |
| // willing to bail on waiting for the client if we notice future stream |
| // discard), re-enable the output port, allocate headers, wait for the port |
| // to be fully enabled, call FillThisBuffer() on the protocol-free buffers. |
| |
| // This is what starts the interval during which |
| // OmxTryRecycleOutputPacketLocked() won't call OMX, and the interval during |
| // which we'll ignore any in-progress client output config until the client |
| // catches up. |
| StartIgnoringClientOldOutputConfig(lock); |
| |
| { // scope unlock |
| ScopedUnlock unlock(lock); |
| CoreCodecMidStreamOutputBufferReConfigPrepare(); |
| } // ~unlock |
| |
| EnsureBuffersNotConfigured(lock, kOutputPort); |
| |
| GenerateAndSendNewOutputConfig(lock, true); |
| |
| // Now we can wait for the client to catch up to the current output config |
| // or for the client to tell the server to discard the current stream. |
| while (!IsStoppingLocked() && !stream_->future_discarded() && |
| !IsOutputConfiguredLocked()) { |
| wake_stream_control_condition_.wait(lock); |
| } |
| |
| if (IsStoppingLocked()) { |
| return; |
| } |
| |
| if (stream_->future_discarded()) { |
| // We already know how to handle this case, and |
| // core_codec_meh_output_buffer_constraints_version_ordinal_ is still set |
| // such that the client will be forced to re-configure output buffers at |
| // the start of the new stream. |
| return; |
| } |
| } // ~lock |
| |
| CoreCodecMidStreamOutputBufferReConfigFinish(); |
| |
| VLOGF("Done with mid-stream format change.\n"); |
| } |
| |
| thrd_t CodecImpl::fidl_thread() { return shared_fidl_thread_; } |
| |
| void CodecImpl::SendFreeInputPacketLocked( |
| fuchsia::mediacodec::CodecPacketHeader header) { |
| // We allow calling this method on StreamControl or InputData ordering domain. |
| // Because the InputData ordering domain thread isn't visible to this code, |
| // if this isn't the StreamControl then we can only assert that this thread |
| // isn't the FIDL thread, because we know the codec's InputData thread isn't |
| // the FIDL thread. |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_ || |
| thrd_current() != fidl_thread()); |
| // We only send using fidl_thread(). |
| PostToSharedFidl([this, header = std::move(header)] { |
| // See "is_bound_checks" comment up top. |
| if (binding_.is_bound()) { |
| binding_.events().OnFreeInputPacket(std::move(header)); |
| } |
| }); |
| } |
| |
| bool CodecImpl::IsInputConfiguredLocked() { |
| return IsPortConfiguredCommonLocked(kInputPort); |
| } |
| |
| bool CodecImpl::IsOutputConfiguredLocked() { |
| return IsPortConfiguredCommonLocked(kOutputPort); |
| } |
| |
| bool CodecImpl::IsPortConfiguredCommonLocked(CodecPort port) { |
| // In addition to what we're able to assert here, when |
| // is_port_configured_[port], the core codec also has the port |
| // configured. |
| ZX_DEBUG_ASSERT(!is_port_configured_[port] || |
| port_settings_[port] && |
| all_buffers_[port].size() == |
| BufferCountFromPortSettings(*port_settings_[port])); |
| return is_port_configured_[port]; |
| } |
| |
| void CodecImpl::Fail(const char* format, ...) { |
| va_list args; |
| va_start(args, format); |
| { // scope lock |
| std::lock_guard<std::mutex> lock(lock_); |
| vFailLocked(false, format, args); |
| } // ~lock |
| // "this" can be deallocated by this point (as soon as ~lock above). |
| va_end(args); |
| } |
| |
| void CodecImpl::FailLocked(const char* format, ...) { |
| va_list args; |
| va_start(args, format); |
| vFailLocked(false, format, args); |
| va_end(args); |
| // At this point know "this" is still allocated only because we still hold |
| // lock_. As soon as lock_ is released by the caller, "this" can immediately |
| // be deallocated by another thread, if this isn't currently the |
| // fidl_thread(). |
| } |
| |
| void CodecImpl::FailFatalLocked(const char* format, ...) { |
| va_list args; |
| va_start(args, format); |
| // This doesn't return. |
| vFailLocked(true, format, args); |
| va_end(args); |
| } |
| |
| void CodecImpl::vFail(bool is_fatal, const char* format, va_list args) { |
| { // scope lock |
| std::lock_guard<std::mutex> lock(lock_); |
| vFailLocked(false, format, args); |
| } // ~lock |
| } |
| |
| // Only meant to be called from Fail() and FailLocked(). Only meant to be |
| // called for async failure cases after was_logically_bound_ has become true. |
| // Failures before that point are handled separately. |
| void CodecImpl::vFailLocked(bool is_fatal, const char* format, va_list args) { |
| // TODO(dustingreen): Send epitaph when possible. |
| |
| // Let's not have a buffer on the stack, not because it couldn't be done |
| // safely, but because we'd potentially run into stack size vs. message length |
| // tradeoffs, stack expansion granularity fun, or whatever else. |
| |
| va_list args2; |
| va_copy(args2, args); |
| |
| size_t buffer_bytes = vsnprintf(nullptr, 0, format, args) + 1; |
| |
| // ~buffer never actually runs since this method never returns |
| std::unique_ptr<char[]> buffer(new char[buffer_bytes]); |
| |
| size_t buffer_bytes_2 = |
| vsnprintf(buffer.get(), buffer_bytes, format, args2) + 1; |
| (void)buffer_bytes_2; |
| // sanity check; should match so go ahead and assert that it does. |
| ZX_DEBUG_ASSERT(buffer_bytes == buffer_bytes_2); |
| va_end(args2); |
| |
| // TODO(dustingreen): It might be worth wiring this up to the log in a more |
| // official way, especially if doing so would print a timestamp automatically |
| // and/or provide filtering goodness etc. |
| const char* message = |
| is_fatal ? "devhost will fail" : "Codec channel will close async"; |
| printf("%s -- %s\n", buffer.get(), message); |
| |
| // TODO(dustingreen): Send string in buffer via epitaph, when possible. First |
| // we should switch to events so we'll only have the Codec channel not the |
| // CodecEvents channel. Note to self: The channel failing server-side may race |
| // with trying to send. |
| |
| if (is_fatal) { |
| abort(); |
| } else { |
| UnbindLocked(); |
| } |
| |
| // At this point we know "this" is still allocated only because we still hold |
| // lock_. As soon as lock_ is released by the caller, "this" can immediately |
| // be deallocated by another thread, if this isn't currently the |
| // fidl_thread(). |
| } |
| |
| void CodecImpl::PostSerial(async_dispatcher_t* async, fit::closure to_run) { |
| zx_status_t result = async::PostTask(async, std::move(to_run)); |
| ZX_ASSERT(result == ZX_OK); |
| } |
| |
| void CodecImpl::PostToSharedFidl( |
| fit::closure to_run, |
| bool promise_not_on_previously_posted_fidl_thread_lambda) { |
| // Re-posting to fidl_thread() is potentially problematic because of |
| // how CodecImpl::UnbindLocked() relies on re-posting itself to run "delete |
| // this" after any other work posted to fidl_thread() previously - that |
| // only works if re-posts to the fidl_thread() aren't allowed. |
| // TODO(dustingreen): Avoid the need for |
| // promise_not_on_previously_posted_fidl_thread_lambda; make enforcement |
| // self-contained while still permitting FIDL dispatch to lambda self-post. |
| ZX_DEBUG_ASSERT(promise_not_on_previously_posted_fidl_thread_lambda || |
| (thrd_current() != fidl_thread())); |
| PostSerial(shared_fidl_dispatcher_, std::move(to_run)); |
| } |
| |
| void CodecImpl::PostToStreamControl(fit::closure to_run) { |
| PostSerial(stream_control_loop_.dispatcher(), std::move(to_run)); |
| } |
| |
| bool CodecImpl::IsStoppingLocked() { return was_unbind_started_; } |
| |
| bool CodecImpl::IsStopping() { |
| std::lock_guard<std::mutex> lock(lock_); |
| return IsStoppingLocked(); |
| } |
| |
| // true - maybe it's the core codec thread. |
| // false - it's definitely not the core codec thread. |
| bool CodecImpl::IsPotentiallyCoreCodecThread() { |
| // FXL_CHECK(false) << "not yet implemented"; |
| return (thrd_current() != stream_control_thread_) && |
| (thrd_current() != fidl_thread()); |
| } |
| |
| void CodecImpl::HandlePendingInputFormatDetails() { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| const fuchsia::mediacodec::CodecFormatDetails* input_details = nullptr; |
| if (stream_->input_format_details()) { |
| input_details = stream_->input_format_details(); |
| } else { |
| input_details = initial_input_format_details_; |
| } |
| ZX_DEBUG_ASSERT(input_details); |
| CoreCodecQueueInputFormatDetails(*input_details); |
| } |
| |
| void CodecImpl::onCoreCodecFailCodec(const char* format, ...) { |
| std::string local_format = |
| std::string("onCoreCodecFailCodec() called -- ") + format; |
| va_list args; |
| va_start(args, format); |
| vFail(false, local_format.c_str(), args); |
| // "this" can be deallocated by this point (as soon as ~lock above). |
| va_end(args); |
| } |
| |
| void CodecImpl::onCoreCodecFailStream() { |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| if (IsStoppingLocked()) { |
| // This CodecImpl is already stopping due to a previous FailLocked(), |
| // which will result in the Codec channel getting closed soon. So don't |
| // send OnStreamFailed(). |
| return; |
| } |
| |
| // We rely on the CodecAdapter to only call this method when there's a |
| // current stream. |
| ZX_DEBUG_ASSERT(stream_ && stream_->stream_lifetime_ordinal() == |
| stream_lifetime_ordinal_); |
| |
| if (stream_->output_end_of_stream()) { |
| // Tolerate a CodecAdapter failing the stream after output EndOfStream |
| // seen, and avoid notifying the client of a stream failure that's too late |
| // to matter. |
| return; |
| } |
| |
| if (stream_->failure_seen()) { |
| // We already know. We don't auto-close the stream because the client is |
| // in control of stream lifetime, so it's plausible that a CodecAdapter |
| // could notify of stream failure more than once. We can ignore the |
| // redundant stream failure to avoid sending OnStreamFailed() again. |
| return; |
| } |
| stream_->SetFailureSeen(); |
| |
| // We're failing the current stream. We should still queue to the output |
| // ordering domain to ensure ordering vs. any previously-sent output on this |
| // stream that was sent directly from codec processing thread. |
| // |
| // This failure is async, in the sense that the client may still be sending |
| // input data, and the core codec is expected to just hold onto those |
| // packets until the client has moved on from this stream. |
| printf("onStreamFailed() - stream_lifetime_ordinal_: %lu\n", |
| stream_lifetime_ordinal_); |
| if (!is_on_stream_failed_enabled_) { |
| FailLocked( |
| "onStreamFailed() with a client that didn't send " |
| "EnableOnStreamFailed(), so closing the Codec channel instead."); |
| return; |
| } |
| // There's not actually any need to track that the stream failed anywhere |
| // in the CodecImpl. The client needs to move on from the failed |
| // stream to a new stream, or close the Codec channel. |
| PostToSharedFidl( |
| [this, stream_lifetime_ordinal = stream_lifetime_ordinal_] { |
| // See "is_bound_checks" comment up top. |
| if (binding_.is_bound()) { |
| binding_.events().OnStreamFailed(stream_lifetime_ordinal); |
| } |
| }); |
| } // ~lock |
| } |
| |
| void CodecImpl::onCoreCodecMidStreamOutputConfigChange( |
| bool output_re_config_required) { |
| // For now, the core codec thread is the only thread this gets called from. |
| ZX_DEBUG_ASSERT(IsPotentiallyCoreCodecThread()); |
| // For a OMX_EventPortSettingsChanged that doesn't demand output buffer |
| // re-config before more output data, this translates to an ordered emit |
| // of a no-action-required OnOutputConfig() that just updates to the new |
| // format, without demanding output buffer re-config. HDR info can be |
| // conveyed this way, ordered with respect to output frames. OMX |
| // requires that we use this thread to collect OMX format info during |
| // EventHandler(). |
| if (!output_re_config_required) { |
| std::unique_lock<std::mutex> lock(lock_); |
| GenerateAndSendNewOutputConfig( |
| lock, |
| false); // buffer_constraints_action_required |
| return; |
| } |
| |
| // We have an OMX_EventPortSettingsChanged that does demand output |
| // buffer re-config before more output data. |
| ZX_DEBUG_ASSERT(output_re_config_required); |
| |
| // We post over to StreamControl domain because we need to synchronize |
| // with any changes to stream state that might be driven by the client. |
| // When we get over there to StreamControl, we'll check if we're still |
| // talking about the same stream_lifetime_ordinal, and if not, we ignore |
| // the event, because a new stream may or may not have the same output |
| // settings, and we'll be re-generating an OnOutputConfig() as needed |
| // from current/later OMX output config anyway. Here are the |
| // possibilities: |
| // * Prior to the client moving to a new stream, we process this event |
| // on StreamControl ordering domain and have bumped |
| // buffer_lifetime_ordinal by the time we start any subsequent |
| // new stream from the client, which means we'll require the client |
| // to catch up to the new buffer_lifetime_ordinal before we start |
| // that new stream. |
| // * The client moves to a new stream before this event gets over to |
| // StreamControl. In this case we ignore the event on StreamControl |
| // domain since its stale by that point, but instead we use |
| // omx_meh_output_buffer_constraints_version_ordinal_ to cause the |
| // client's next stream to start with a new OnOutputConfig() that |
| // the client must catch up to before the stream can fully start. |
| // This way we know we're not ignoring a potential change to |
| // nBufferCountMin or anything like that. |
| uint64_t local_stream_lifetime_ordinal; |
| { // scope lock |
| std::lock_guard<std::mutex> lock(lock_); |
| // This part is not speculative. OMX has indicated that it's at least |
| // meh about the current output config, so ensure we do a required |
| // OnOutputConfig() before the next stream starts, even if the client |
| // moves on to a new stream such that the speculative part below becomes |
| // stale. |
| core_codec_meh_output_buffer_constraints_version_ordinal_ = |
| port_settings_[kOutputPort] |
| ? port_settings_[kOutputPort]->buffer_constraints_version_ordinal |
| : 0; |
| // Speculative part - this part is speculative, in that we don't know if |
| // this post over to StreamControl will beat any client driving to a new |
| // stream. So we snap the stream_lifetime_ordinal so we know whether to |
| // ignore the post once it reaches StreamControl. |
| local_stream_lifetime_ordinal = stream_lifetime_ordinal_; |
| } // ~lock |
| PostToStreamControl( |
| [this, stream_lifetime_ordinal = local_stream_lifetime_ordinal] { |
| MidStreamOutputConfigChange(stream_lifetime_ordinal); |
| }); |
| } |
| |
| void CodecImpl::onCoreCodecInputPacketDone(CodecPacket* packet) { |
| // Free/busy coherency from Codec interface to OMX doesn't involve trusting |
| // the client, so assert we're doing it right server-side. |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| // The core codec says the buffer-referening in-flight lifetime of this |
| // packet is over. We'll set the buffer again when this packet get's used |
| // by the client again to deliver more input data. |
| packet->SetBuffer(nullptr); |
| // Unfortunately we have to insist that the core codec not call |
| // onCoreCodecInputPacketDone() arbitrarily late because we need to know |
| // when it's safe to deallocate binding_, and the core codec, etc. So the |
| // rule is the core codec needs to ensure that all calls to stream-related |
| // callbacks have completed (to structure-touching degree; not |
| // code-unloading degree) before CoreCodecStopStream() returns. |
| ZX_DEBUG_ASSERT(is_core_codec_stream_started_); |
| ZX_DEBUG_ASSERT( |
| !all_packets_[kInputPort][packet->packet_index()]->is_free()); |
| all_packets_[kInputPort][packet->packet_index()]->SetFree(true); |
| SendFreeInputPacketLocked(fuchsia::mediacodec::CodecPacketHeader{ |
| .buffer_lifetime_ordinal = packet->buffer_lifetime_ordinal(), |
| .packet_index = packet->packet_index()}); |
| } // ~lock |
| } |
| |
| void CodecImpl::onCoreCodecOutputPacket(CodecPacket* packet, |
| bool error_detected_before, |
| bool error_detected_during) { |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| // This helps verify that packet lifetimes are coherent, but we can't do the |
| // same for buffer_index because VP9 has show_existing_frame which is |
| // allowed to output the same buffer repeatedly. |
| // |
| // TODO(dustingreen): We could _optionally_ verify that buffer lifetimes are |
| // coherent for codecs that don't output the same buffer repeatedly and |
| // concurrently. |
| all_packets_[kOutputPort][packet->packet_index()]->SetFree(false); |
| ZX_DEBUG_ASSERT(packet->has_start_offset()); |
| ZX_DEBUG_ASSERT(packet->has_valid_length_bytes()); |
| // packet->has_timestamp_ish() is optional even if |
| // promise_separate_access_units_on_input is true. We do want to enforce |
| // that the client gets no set timestamp_ish values if the client didn't |
| // promise_separate_access_units_on_input. |
| bool has_timestamp_ish = |
| decoder_params_->promise_separate_access_units_on_input && |
| packet->has_timestamp_ish(); |
| uint64_t timestamp_ish = has_timestamp_ish ? packet->timestamp_ish() : 0; |
| PostToSharedFidl( |
| [this, |
| p = |
| fuchsia::mediacodec::CodecPacket{ |
| .header.buffer_lifetime_ordinal = |
| packet->buffer_lifetime_ordinal(), |
| .header.packet_index = packet->packet_index(), |
| .buffer_index = packet->buffer()->buffer_index(), |
| .stream_lifetime_ordinal = stream_lifetime_ordinal_, |
| .start_offset = packet->start_offset(), |
| .valid_length_bytes = packet->valid_length_bytes(), |
| .has_timestamp_ish = has_timestamp_ish, |
| .timestamp_ish = timestamp_ish, |
| // TODO(dustingreen): These two "true" values should be fine |
| // for decoders, but need to revisit here for encoders. |
| .start_access_unit = decoder_params_ ? true : false, |
| .known_end_access_unit = decoder_params_ ? true : false, |
| }, |
| error_detected_before, error_detected_during] { |
| // See "is_bound_checks" comment up top. |
| if (binding_.is_bound()) { |
| binding_.events().OnOutputPacket( |
| std::move(p), error_detected_before, error_detected_during); |
| } |
| }); |
| } // ~lock |
| } |
| |
| void CodecImpl::onCoreCodecOutputEndOfStream(bool error_detected_before) { |
| { // scope lock |
| std::unique_lock<std::mutex> lock(lock_); |
| stream_->SetOutputEndOfStream(); |
| output_end_of_stream_seen_.notify_all(); |
| VLOGF("sending OnOutputEndOfStream()\n"); |
| PostSerial(shared_fidl_dispatcher_, |
| [this, stream_lifetime_ordinal = stream_lifetime_ordinal_, |
| error_detected_before] { |
| // See "is_bound_checks" comment up top. |
| if (binding_.is_bound()) { |
| binding_.events().OnOutputEndOfStream( |
| stream_lifetime_ordinal, error_detected_before); |
| } |
| }); |
| } // ~lock |
| } |
| |
| CodecImpl::Stream::Stream(uint64_t stream_lifetime_ordinal) |
| : stream_lifetime_ordinal_(stream_lifetime_ordinal) { |
| // nothing else to do here |
| } |
| |
| uint64_t CodecImpl::Stream::stream_lifetime_ordinal() { |
| return stream_lifetime_ordinal_; |
| } |
| |
| void CodecImpl::Stream::SetFutureDiscarded() { |
| ZX_DEBUG_ASSERT(!future_discarded_); |
| future_discarded_ = true; |
| } |
| |
| bool CodecImpl::Stream::future_discarded() { return future_discarded_; } |
| |
| void CodecImpl::Stream::SetFutureFlushEndOfStream() { |
| ZX_DEBUG_ASSERT(!future_flush_end_of_stream_); |
| future_flush_end_of_stream_ = true; |
| } |
| |
| bool CodecImpl::Stream::future_flush_end_of_stream() { |
| return future_flush_end_of_stream_; |
| } |
| |
| CodecImpl::Stream::~Stream() { |
| VLOGF("~Stream() stream_lifetime_ordinal: %lu\n", stream_lifetime_ordinal_); |
| } |
| |
| void CodecImpl::Stream::SetInputFormatDetails( |
| std::unique_ptr<fuchsia::mediacodec::CodecFormatDetails> |
| input_format_details) { |
| // This is allowed to happen multiple times per stream. |
| input_format_details_ = std::move(input_format_details); |
| } |
| |
| const fuchsia::mediacodec::CodecFormatDetails* |
| CodecImpl::Stream::input_format_details() { |
| return input_format_details_.get(); |
| } |
| |
| void CodecImpl::Stream::SetOobConfigPending(bool pending) { |
| // SetOobConfigPending(true) is legal regardless of current state, but |
| // SetOobConfigPending(false) is only legal if the state is currently true. |
| ZX_DEBUG_ASSERT(pending || oob_config_pending_); |
| oob_config_pending_ = pending; |
| } |
| |
| bool CodecImpl::Stream::oob_config_pending() { return oob_config_pending_; } |
| |
| void CodecImpl::Stream::SetInputEndOfStream() { |
| ZX_DEBUG_ASSERT(!input_end_of_stream_); |
| input_end_of_stream_ = true; |
| } |
| |
| bool CodecImpl::Stream::input_end_of_stream() { return input_end_of_stream_; } |
| |
| void CodecImpl::Stream::SetOutputEndOfStream() { |
| ZX_DEBUG_ASSERT(!output_end_of_stream_); |
| output_end_of_stream_ = true; |
| } |
| |
| bool CodecImpl::Stream::output_end_of_stream() { return output_end_of_stream_; } |
| |
| void CodecImpl::Stream::SetFailureSeen() { |
| ZX_DEBUG_ASSERT(!failure_seen_); |
| failure_seen_ = true; |
| } |
| |
| bool CodecImpl::Stream::failure_seen() { return failure_seen_; } |
| |
| // |
| // CoreCodec wrappers, for the asserts. These asserts, and the way we ensure |
| // at compile time that this class has a method for every method of |
| // CodecAdapter, are essentially costing a double vtable call instead of a |
| // single vtable call. If we don't like that at some point, we can remove the |
| // private CodecAdapter inheritance from CodecImpl and have these be normal |
| // methods instead of virtual methods. |
| // |
| |
| void CodecImpl::CoreCodecInit(const fuchsia::mediacodec::CodecFormatDetails& |
| initial_input_format_details) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| codec_adapter_->CoreCodecInit(initial_input_format_details); |
| } |
| |
| void CodecImpl::CoreCodecAddBuffer(CodecPort port, const CodecBuffer* buffer) { |
| ZX_DEBUG_ASSERT(port == kInputPort && |
| thrd_current() == stream_control_thread_ || |
| port == kOutputPort && thrd_current() == fidl_thread()); |
| codec_adapter_->CoreCodecAddBuffer(port, buffer); |
| } |
| |
| void CodecImpl::CoreCodecConfigureBuffers( |
| CodecPort port, const std::vector<std::unique_ptr<CodecPacket>>& packets) { |
| ZX_DEBUG_ASSERT(port == kInputPort && |
| thrd_current() == stream_control_thread_ || |
| port == kOutputPort && thrd_current() == fidl_thread()); |
| codec_adapter_->CoreCodecConfigureBuffers(port, packets); |
| } |
| |
| void CodecImpl::CoreCodecEnsureBuffersNotConfigured(CodecPort port) { |
| ZX_DEBUG_ASSERT( |
| port == kInputPort && thrd_current() == stream_control_thread_ || |
| port == kOutputPort && (thrd_current() == fidl_thread() || |
| thrd_current() == stream_control_thread_)); |
| codec_adapter_->CoreCodecEnsureBuffersNotConfigured(port); |
| } |
| |
| void CodecImpl::CoreCodecStartStream() { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| codec_adapter_->CoreCodecStartStream(); |
| } |
| |
| void CodecImpl::CoreCodecQueueInputFormatDetails( |
| const fuchsia::mediacodec::CodecFormatDetails& |
| per_stream_override_format_details) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| codec_adapter_->CoreCodecQueueInputFormatDetails( |
| per_stream_override_format_details); |
| } |
| |
| void CodecImpl::CoreCodecQueueInputPacket(CodecPacket* packet) { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| codec_adapter_->CoreCodecQueueInputPacket(packet); |
| } |
| |
| void CodecImpl::CoreCodecQueueInputEndOfStream() { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| codec_adapter_->CoreCodecQueueInputEndOfStream(); |
| } |
| |
| void CodecImpl::CoreCodecStopStream() { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| codec_adapter_->CoreCodecStopStream(); |
| } |
| |
| bool CodecImpl::IsCoreCodecRequiringOutputConfigForFormatDetection() { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread() || |
| thrd_current() == stream_control_thread_); |
| return codec_adapter_->IsCoreCodecRequiringOutputConfigForFormatDetection(); |
| } |
| |
| // Caller must ensure that this is called only on one thread at a time, only |
| // during setup, during a core codec initiated mid-stream format change, or |
| // during stream start before any input data has been delivered for the new |
| // stream. |
| std::unique_ptr<const fuchsia::mediacodec::CodecOutputConfig> |
| CodecImpl::CoreCodecBuildNewOutputConfig( |
| uint64_t stream_lifetime_ordinal, |
| uint64_t new_output_buffer_constraints_version_ordinal, |
| uint64_t new_output_format_details_version_ordinal, |
| bool buffer_constraints_action_required) { |
| ZX_DEBUG_ASSERT(IsPotentiallyCoreCodecThread() || |
| thrd_current() == stream_control_thread_); |
| return codec_adapter_->CoreCodecBuildNewOutputConfig( |
| stream_lifetime_ordinal, new_output_buffer_constraints_version_ordinal, |
| new_output_format_details_version_ordinal, |
| buffer_constraints_action_required); |
| } |
| |
| void CodecImpl::CoreCodecMidStreamOutputBufferReConfigPrepare() { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| codec_adapter_->CoreCodecMidStreamOutputBufferReConfigPrepare(); |
| } |
| |
| void CodecImpl::CoreCodecMidStreamOutputBufferReConfigFinish() { |
| ZX_DEBUG_ASSERT(thrd_current() == stream_control_thread_); |
| codec_adapter_->CoreCodecMidStreamOutputBufferReConfigFinish(); |
| } |
| |
| void CodecImpl::CoreCodecRecycleOutputPacket(CodecPacket* packet) { |
| ZX_DEBUG_ASSERT(thrd_current() == fidl_thread()); |
| codec_adapter_->CoreCodecRecycleOutputPacket(packet); |
| } |