blob: a4a1af7981183c391446f591e702cc02b49359fc [file] [log] [blame]
// 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 "util.h"
#include <lib/async-loop/cpp/loop.h>
#include <lib/fidl/cpp/clone.h>
#include <lib/fit/defer.h>
#include <lib/media/test/codec_buffer.h>
#include <lib/media/test/codec_client.h>
#include <lib/media/test/codec_output.h>
#include "garnet/lib/media/wav_writer/wav_writer.h"
#include <thread>
// Re. this example and threading:
//
// This example shows the handling needed to run a Codec using multiple client
// threads correctly. Any new codec client author should consider whether the
// benefits of using multiple threads are really worthwhile in the client's
// particular use case, or if goals can be met just fine with a single FIDL
// thread doing everything. Which is "best" depends on many factors, both
// technical and otherwise. The main downsides of doing everything on one FIDL
// thread from a codec point of view is potential lower responsiveness or timing
// glitches/hiccups especially if the single FIDL thread is used for additional
// non-FIDL things including _any_ time-consuming thing. If everything on the
// single FIDL thread is as async as possible and doesn't block the thread
// (including during things like buffer allocation via some other service), then
// it can work well - but fully achieving that of course brings its own fun.
// Consider that buffer allocation can _potentially_ take some duration that's
// not entirely under the client's control, so blocking a FIDL thread during
// that wouldn't be good for responsiveness of the FIDL thread.
//
// TODO(dustingreen): Write another simpler example that shows how to use the
// codec with only a single FIDL thread, single stream_lifetime_ordinal assumed,
// minimal output config change handling, maybe output config handling directly
// on the FIDL thread (despite potential for some duration not under the
// client's control there), not reserving any packets for the client, etc.
// 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.
namespace {
// This example only has one stream_lifetime_ordinal which is 1.
//
// TODO(dustingreen): actually re-use the Codec instance for at least one more
// stream, even if it's just to decode the same data again.
constexpr uint64_t kStreamLifetimeOrdinal = 1;
// Whether we actually output a wav depends on whether there are any args or
// not.
constexpr bool kWavWriterEnabled = true;
std::unique_ptr<uint8_t[]> make_AudioSpecificConfig_from_ADTS_header(
std::unique_ptr<uint8_t[]>* input_bytes) {
std::unique_ptr<uint8_t[]> asc = std::make_unique<uint8_t[]>(2);
// TODO(dustingreen): Switch from ADTS to .mp4 and fix AAC decoder to not
// require "AudioSpecificConfig()" when fed ADTS. In other words, move the
// stuff here into a shim around the AAC OMX decoder, just next to (above or
// below) the OmxCodecRunner in the codec_runner_sw_omx isolate, probably.
// For SoftAAC2.cpp, for no particularly good reason, a CODECCONFIG buffer is
// expected, even when running in ADTS mode, despite all the relevant data
// being available from the ADTS header. The CODECCONFIG buffer has an
// AudioSpecificConfig in it. The AudioSpecificConfig has to be created based
// on corresponding fields of the ADTS header - not that requiring this of
// the codec client makes any sense whatsoever...
//
// TODO(dustingreen): maybe add a per-codec compensation layer to un-crazy the
// quirks of each codec. For example, when decoding ADTS, all the needed info
// is there in the ADTS stream directly. No reason to hassle the codec client
// for a pointless translated form of the same info. In contrast, when it's
// an mp4 file (or mkv, or whatever modern container format), the codec config
// info is relevant. But we should only force a client to provide it if
// it's really needed.
// First, parse the stuff that's needed from the first ADTS header.
uint8_t* adts_header = static_cast<uint8_t*>(input_bytes->get());
uint8_t profile_ObjectType; // name in AAC spec in adts_fixed_header
uint8_t sampling_frequency_index; // name in AAC spec in adts_fixed_header
uint8_t channel_configuration; // name in AAC spec in adts_fixed_header
profile_ObjectType = (adts_header[2] >> 6) & 0x3;
sampling_frequency_index = (adts_header[2] >> 2) & 0xf;
if (sampling_frequency_index >= 11) {
Exit("sampling frequency index too large: %d - exiting\n",
sampling_frequency_index);
}
channel_configuration = (adts_header[2] & 0x1) << 2 | (adts_header[3] >> 6);
// Now let's convert these to the forms needed by AudioSpecificConfig.
uint8_t audioObjectType =
profile_ObjectType + 1; // see near Table 1.A.11, for AAC not MPEG-2
uint8_t samplingFrequencyIndex =
sampling_frequency_index; // no conversion needed
uint8_t channelConfiguration = channel_configuration; // no conversion needed
uint8_t frameLengthFlag = 0;
uint8_t dependsOnCoreCoder = 0;
uint8_t extensionFlag = 0;
// Now we are ready to build a two-byte AudioSpecificConfig. Not an
// AudioSpecificInfo as stated in avc_utils.cpp (AOSP) mind you, but an
// AudioSpecificConfig.
uint8_t* asc_header = static_cast<uint8_t*>(asc.get());
asc_header[0] = (audioObjectType << 3) | (samplingFrequencyIndex >> 1);
asc_header[1] = (samplingFrequencyIndex << 7) | (channelConfiguration << 3) |
(frameLengthFlag << 2) | (dependsOnCoreCoder << 1) |
(extensionFlag << 0);
return asc;
}
} // namespace
// On success, out_md contains the sha256 digest of the output audio data. This
// is intended as a golden-file value when this function is used as part of a
// test. This sha256 accounts for all the output WAV data and also the audio
// output format parameters. When the same input file is decoded we expect the
// sha256 to be the same.
//
// main_loop - the loop run by main(), codec_factory is bound to
// main_loop->dispatcher()
// codec_factory - codec_factory to take ownership of, use, and close by the
// time the function returns. This InterfacePtr would typically be obtained
// by calling
// startup_context->ConnectToEnvironmentService<fuchsia::mediacodec::CodecFactory>().
// input_adts_file - This must be set and must be the filename of an input .adts
// file (input file extension not checked / doesn't matter).
// output_wav_file - If nullptr, don't write the audio data to a wav file. If
// non-nullptr, output audio data to the specified wav file. When used as
// an example, this will tend to be set. When used as a test, this will not
// be set.
// out_md - SHA256_DIGEST_LENGTH bytes long
void use_aac_decoder(async::Loop* main_loop,
fuchsia::mediacodec::CodecFactoryPtr codec_factory,
const std::string& input_adts_file,
const std::string& output_wav_file, uint8_t* out_md) {
memset(out_md, 0, SHA256_DIGEST_LENGTH);
// In this example code, we're using this async::Loop for everything
// FIDL-related in this function. We explicitly specify which loop for all
// activity initiated by this function. We post to the loop and want to make
// sure it's the same loop. We rely on it being the same loop for serializing
// sending of messages via CodecPtr. We need to serialize sending messages
// since ProxyController isn't thread safe for sending messages at least in
// the case where sent requests require responses. We could use something
// like a send lock, but that would require locking around every send, even
// those sends which are already on the loop thread, vs. what we're doing
// which only needs anything extra for sends we queue from threads that aren't
// the loop thread.
async::Loop loop(&kAsyncLoopConfigNoAttachToThread);
// This example will give the loop it's own thread, so that the main thread
// can be used to sequence overall control of the Codec instance using a
// thread instead of chaining together a bunch of async activity (which would
// be more complicated to understand and serve little purpose in an example
// program like this).
loop.StartThread("use_aac_decoder_loop");
// From this point forward, because the loop is already running, this example
// needs to be careful to be ready for all potential FIDL channel messages and
// errors before attaching the channel to the loop. The loop will continue
// running until after we've deleted all the stuff using the loop.
// This example has these threads:
// * main thread - used for setup and to drive overall sequence progression
// without resorting to a bunch of chained async actions, so that the
// example can remain reasonably clear in terms of what overall big-picture
// steps are taken in what overall order. Note that any calls to FIDL
// interfaces are posted to the loop thread.
// * loop thread - this thread pumps all the FIDL interfaces in this example,
// using a separate thread from the main thread. This does require us to
// be a bit more careful to fully configure an interface to be ready to
// receive messages before binding a server endpoint locally, or fully
// ready to have error handler (or similar) called on the client end before
// sending the sever end somewhere else.
// * input thread - feeds in compressed input data - but note that the actual
// calls to any FIDL interface are posted to the loop thread.
// * output thread - accepts output data - but note that any actual calls to
// any FIDL interface are posted to the loop thread.
// Current FIDL-related threading aspects:
// * Safe to call client-side proxy methods from multiple threads? If there
// is a response: No. If there is not a response: Maybe. While a
// no-response send might currently be safe for one-way sends without a
// response_handler, we shouldn't rely on that in this example (see TODO
// on call to CreateDecoder() below for only known current exception).
// Certainly any calls with a response need to be started from a single
// thread at a time. Posting to the loop thread for all client-side sends
// covers this bullet.
// * Safe to call a server-side response_handler's operator() from an
// arbitrary server-side thread? Yes, and seems likely to remain yes.
// * The FIDL events are set up within CodecClient's constructor before
// binding.
VLOGF("reading adts file...\n");
size_t input_size;
std::unique_ptr<uint8_t[]> input_bytes =
read_whole_file(input_adts_file.c_str(), &input_size);
VLOGF("done reading adts file.\n");
codec_factory.set_error_handler([](zx_status_t status) {
// TODO(dustingreen): get and print CodecFactory channel epitaph once that's
// possible.
LOGF("codec_factory failed - unexpected\n");
});
VLOGF("before make_AudioSpecificConfig_from_ADTS_header()...\n");
VLOGF("input_bytes->get(): %p\n", input_bytes.get());
std::unique_ptr<uint8_t[]> asc =
make_AudioSpecificConfig_from_ADTS_header(&input_bytes);
VLOGF("after make_AudioSpecificConfig_from_ADTS_header()\n");
std::vector<uint8_t> asc_vector(2);
for (int i = 0; i < 2; i++) {
asc_vector[i] = asc[i];
}
// Set all fields to 0 / default.
fuchsia::mediacodec::CreateDecoder_Params create_params = {};
// TODO(dustingreen): Remove need for ADTS to specify any codec config since
// it's in-band, and maybe switch this program over to using .mp4 with
// AudioSpecificConfig() from the .mp4 file.
create_params.input_details.format_details_version_ordinal = 0;
create_params.input_details.mime_type = "audio/aac-adts";
// We don't do this here for now, because we want to cover what happens when
// CreateDecoder() doesn't specify oob_bytes, but
// QueueInputFormatDetails() does.
// create_params.input_details.oob_bytes.reset(std::move(asc_vector));
fuchsia::media::FormatDetails full_input_details =
fidl::Clone(create_params.input_details);
full_input_details.oob_bytes.reset(std::move(asc_vector));
// We're using CodecPtr here rather than CodecSyncPtr partly to have this
// example program be slightly more realistic (with respect to client programs
// that choose to use the async interface), and partly to avoid having to
// separately check the error return code of every call, since the sync proxy
// doesn't have any way to get an async error callback (that I've found).
//
// We let the CodecClient handle the creation of the CodecPtr, because the
// loop is already running, and we want the error handler to be set up by
// CodecClient in advance of the channel potentially being closed.
VLOGF("before CodecClient::CodecClient()...\n");
CodecClient codec_client(&loop);
async::PostTask(
main_loop->dispatcher(),
[&codec_factory, create_params = std::move(create_params),
codec_client_request = codec_client.GetTheRequestOnce()]() mutable {
VLOGF("before codec_factory->CreateDecoder() (async)\n");
codec_factory->CreateDecoder2(std::move(create_params),
std::move(codec_client_request));
});
VLOGF("before codec_client.Start()...\n");
// This does a Sync(), so after this we can drop the CodecFactory without it
// potentially cancelling our Codec create.
codec_client.Start();
// We don't need the CodecFactory any more, and at this point any Codec
// creation errors have had a chance to arrive via the
// codec_factory.set_error_handler() lambda.
//
// Unbind() is only safe to call on the interfaces's dispatcher thread. We
// also want to block the current thread until this is done, to avoid
// codec_factory potentially disappearing before this posted work finishes.
std::mutex unbind_mutex;
std::condition_variable unbind_done_condition;
bool unbind_done = false;
async::PostTask(
main_loop->dispatcher(),
[&codec_factory, &unbind_mutex, &unbind_done, &unbind_done_condition] {
codec_factory.Unbind();
{ // scope lock
std::lock_guard<std::mutex> lock(unbind_mutex);
unbind_done = true;
} // ~lock
unbind_done_condition.notify_all();
// All of codec_factory, unbind_mutex, unbind_done,
// unbind_done_condition are potentially gone by this point.
});
{ // scope lock
std::unique_lock<std::mutex> lock(unbind_mutex);
while (!unbind_done) {
unbind_done_condition.wait(lock);
}
} // ~lock
FXL_DCHECK(unbind_done);
// We use a separate thread to provide input data, separate thread for output
// data, and a separate FIDL thread (started above). This is to avoid the
// example being too simplistic to be of any use as a reference to authors of
// codec clients that use multiple threads, and to some degree, to keep the
// input handling and output handling code clear.
// The captures here require the main thread to call in_thread->join() before
// the captures go out of scope.
VLOGF("before starting in_thread...\n");
std::unique_ptr<std::thread> in_thread = std::make_unique<std::thread>(
[&codec_client, full_input_details = std::move(full_input_details),
&input_bytes, input_size]() mutable {
// First queue the full_input_details.
codec_client.QueueInputFormatDetails(kStreamLifetimeOrdinal,
std::move(full_input_details));
// "syncword" bits for ADTS are, starting at byte alignment: 0xFF 0xF.
// That's 12 1 bits, with the first 1 bit starting at a byte aligned
// boundary.
//
// Unfortunately, the "syncword" can show up in the middle of an aac
// frame, which means the syncword is more of a heuristic than a real
// sync. In this case the test file is clean, so by parsing the aac
// frame length we can skip forward and avoid getting fooled by the fake
// syncword(s).
auto queue_access_unit = [&codec_client, &input_bytes](
uint8_t* bytes, size_t byte_count) {
size_t bytes_so_far = 0;
// printf("queuing offset: %ld byte_count: %zu\n", bytes -
// input_bytes.get(), byte_count);
while (bytes_so_far != byte_count) {
std::unique_ptr<fuchsia::media::Packet> packet =
codec_client.BlockingGetFreeInputPacket();
const CodecBuffer& buffer =
codec_client.GetInputBufferByIndex(packet->header.packet_index);
size_t bytes_to_copy =
std::min(byte_count - bytes_so_far, buffer.size_bytes());
packet->stream_lifetime_ordinal = kStreamLifetimeOrdinal;
packet->start_offset = 0;
packet->valid_length_bytes = bytes_to_copy;
packet->timestamp_ish = 0;
packet->start_access_unit = true;
packet->known_end_access_unit = true;
memcpy(buffer.base(), bytes + bytes_so_far, bytes_to_copy);
codec_client.QueueInputPacket(std::move(packet));
bytes_so_far += bytes_to_copy;
}
};
int input_byte_count = input_size;
for (int i = 0; i < input_byte_count - 1;) {
if (!(input_bytes[i] == 0xFF &&
((input_bytes[i + 1] & 0xF0) == 0xF0))) {
printf("s");
i++;
continue;
}
uint32_t bytes_left = input_byte_count - i;
uint8_t* adts_header = &input_bytes[i];
bool protection_absent = (adts_header[1] & 1);
uint32_t adts_header_size = protection_absent ? 7 : 9;
if (bytes_left < adts_header_size) {
Exit(
"input data corrupt (maybe truncated) - vs header length - "
"bytes_left: %d adts_header_size: %d",
bytes_left, adts_header_size);
}
uint32_t aac_frame_length = ((adts_header[3] & 3) << 11) |
(adts_header[4] << 3) |
(adts_header[5] >> 5);
if (bytes_left < aac_frame_length) {
Exit(
"input data corrupt (maybe truncated) - vs frame length - "
"bytes_left: %d aac_frame_length: %d",
bytes_left, aac_frame_length);
}
queue_access_unit(&input_bytes[i], aac_frame_length);
i += aac_frame_length;
}
// Send through QueueInputEndOfStream().
codec_client.QueueInputEndOfStream(kStreamLifetimeOrdinal);
// input thread done
});
// Separate thread to process the output.
//
// codec_client outlives the thread.
std::unique_ptr<std::thread> out_thread = std::make_unique<
std::thread>([&codec_client, output_wav_file, out_md]() {
// The codec_client lock_ is not held for long durations in here, which is
// good since we're using this thread to do things like write to a WAV
// file.
media::audio::WavWriter<kWavWriterEnabled> wav_writer;
bool is_wav_initialized = false;
SHA256_CTX sha256_ctx;
SHA256_Init(&sha256_ctx);
// We allow the server to send multiple output format updates if it wants;
// see implementation of BlockingGetEmittedOutput() which will hide
// multiple configs before the first packet from this code.
//
// In this example, we only deal with one output format once we start seeing
// stream data show up, since WAV only supports a single format per file.
std::shared_ptr<const fuchsia::media::StreamOutputConfig> stream_config;
while (true) {
std::unique_ptr<CodecOutput> output =
codec_client.BlockingGetEmittedOutput();
if (output->stream_lifetime_ordinal() != kStreamLifetimeOrdinal) {
Exit(
"server emitted a stream_lifetime_ordinal that client didn't set "
"on any input");
}
if (output->end_of_stream()) {
VLOGF("output end_of_stream() - done with output\n");
// Just "break;" would be more fragile under code modification.
goto end_of_output;
}
const fuchsia::media::Packet& packet = output->packet();
// "packet" will live long enough because ~cleanup runs before ~output.
auto cleanup = fit::defer([&codec_client, &packet] {
// Using an auto call for this helps avoid losing track of the
// output_buffer.
//
// If the omx_state_ or omx_state_desired_ isn't correct,
// UseOutputBuffer() will fail. The only way that can happen here is
// if the OMX codec transitioned states unilaterally without any set
// state command, so if that occurs, exit.
codec_client.RecycleOutputPacket(packet.header);
});
std::shared_ptr<const fuchsia::media::StreamOutputConfig> config =
output->config();
// This will remain live long enough because this thread is the only
// thread that re-allocates output buffers.
const CodecBuffer& buffer =
codec_client.GetOutputBufferByIndex(packet.header.packet_index);
if (stream_config &&
(config->format_details.format_details_version_ordinal !=
stream_config->format_details.format_details_version_ordinal)) {
Exit(
"codec server unexpectedly changed output format mid-stream - "
"unexpected for this stream");
}
if (packet.valid_length_bytes == 0) {
// The server should not generate any empty packets.
Exit("broken server sent empty packet");
}
// We have a non-empty packet of the stream.
if (!stream_config) {
// Every output has a config. This happens exactly once.
stream_config = config;
const fuchsia::media::FormatDetails& format =
stream_config->format_details;
if (!format.domain->is_audio()) {
Exit("!format.domain.is_audio() - unexpected");
}
const fuchsia::media::AudioFormat& audio = format.domain->audio();
if (!audio.is_uncompressed()) {
Exit("!audio.is_uncompressed() - unexpected");
}
const fuchsia::media::AudioUncompressedFormat& uncompressed =
audio.uncompressed();
if (!uncompressed.is_pcm()) {
Exit("!uncompressed.is_pcm() - unexpected");
}
// For now, bail out if it's not audio PCM 16 bit 2 channel, if only
// because that's what we expect from the one test file so far, so if
// it's different it'll be good to know that immediately, for now.
//
// TODO(dustingreen): Try to figure out WAV channel ordering for > 2
// channels so we can deal with > 2 channels correctly. Tolerate sample
// rates other than 44100. Tolerate bits per sample other than 16. Set
// up test input files for those cases.
//
// TODO(dustingreen): Once we have a video decoder, update this example
// to handle at least one video format, or create a separate example for
// video with some of the code in this file moved to a source_set.
const fuchsia::media::PcmFormat& pcm = uncompressed.pcm();
if (pcm.channel_map.size() < 1 || pcm.channel_map.size() > 2) {
Exit(
"pcm.channel_map->size() outside range [1, 2] - unexpected - "
"actual: %zu\n",
pcm.channel_map.size());
}
if (static_cast<fuchsia::media::AudioChannelId>(
pcm.channel_map[0]) !=
fuchsia::media::AudioChannelId::LF) {
Exit(
"pcm.channel_map[0] is unexpected given the input data used in "
"this example");
}
if (pcm.channel_map.size() >= 2 &&
static_cast<fuchsia::media::AudioChannelId>(
pcm.channel_map[1]) !=
fuchsia::media::AudioChannelId::RF) {
Exit(
"pcm.channel_map[1] is unexpected given the input data used in "
"this example");
}
if (pcm.bits_per_sample != 16) {
Exit("pcm.bits_per_sample != 16 - unexpected - actual: %d",
pcm.bits_per_sample);
}
if (pcm.frames_per_second != 44100) {
Exit("pcm.frames_per_second != 44100 - unexpected - actual: %d",
pcm.frames_per_second);
}
if (!output_wav_file.empty()) {
if (!wav_writer.Initialize(
output_wav_file.c_str(),
fuchsia::media::AudioSampleFormat::SIGNED_16,
pcm.channel_map.size(), pcm.frames_per_second,
pcm.bits_per_sample)) {
Exit("wav_writer.Initialize() failed");
}
is_wav_initialized = true;
}
SHA256_Update_AudioParameters(&sha256_ctx, pcm);
}
// We have a non-empty buffer (EOS or not), so write the audio data to the
// WAV file.
if (is_wav_initialized) {
if (!wav_writer.Write(buffer.base() + packet.start_offset,
packet.valid_length_bytes)) {
Exit("wav_writer.Write() failed");
}
}
int16_t* int16_base =
reinterpret_cast<int16_t*>(buffer.base() + packet.start_offset);
for (size_t iter = 0; iter < packet.valid_length_bytes / sizeof(int16_t);
iter++) {
int16_t data_le = htole16(int16_base[iter]);
SHA256_Update(&sha256_ctx, &data_le, sizeof(data_le));
}
}
end_of_output:;
if (is_wav_initialized) {
wav_writer.Close();
}
if (!SHA256_Final(out_md, &sha256_ctx)) {
assert(false);
}
// output thread done
});
// decode some audio for a bit... in_thread, loop, out_thread, and the codec
// itself are taking care of it.
// First wait for the input thread to be done feeding input data. Before the
// in_thread terminates, it'll have sent in a last empty EOS input buffer.
VLOGF("before in_thread->join()...\n");
in_thread->join();
VLOGF("after in_thread->join()\n");
// The EOS queued as an input buffer should cause the codec to output an EOS
// output buffer, at which point out_thread should terminate, after it has
// finalized the output WAV file.
VLOGF("before out_thread->join()...\n");
out_thread->join();
VLOGF("after out_thread->join()\n");
// Because CodecClient posted work to the loop which captured the CodecClient
// as "this", it's important that we ensure that all such work is done trying
// to run before we delete CodecClient. We need to know that the work posted
// using PostSerial() won't be trying to touch the channel or pointers that
// are owned by CodecClient before we close the channel or destruct
// CodecClient (which happens before ~loop).
//
// We call loop.Quit();loop.JoinThreads(); before codec_client.Stop() because
// there can be at least a RecycleOutputPacket() still working its way toward
// the Codec (via the loop) at this point, so doing
// loop.Quit();loop.JoinThreads(); first avoids potential FIDL message send
// errors. We're done decoding so we don't care whether any remaining queued
// messages toward the codec actually reach the codec.
//
// We use loop.Quit();loop.JoinThreads(); instead of loop.Shutdown() because
// we don't want the Shutdown() side-effect of failing the channel bindings.
// The Shutdown() will happen later.
//
// By ensuring that the loop is done running code before closing the channel
// (or loop.Shutdown()), we can close the channel cleanly and avoid mitigation
// of expected normal channel closure (or loop.Shutdown()) in any code that
// runs on the loop. This way, unexpected channel failure is the only case to
// worry about.
VLOGF("before loop.Quit()\n");
loop.Quit();
VLOGF("before loop.JoinThreads()...\n");
loop.JoinThreads();
VLOGF("after loop.JoinThreads()\n");
// Close the channel explicitly (just so we can more easily print messages
// before and after vs. ~codec_client).
VLOGF("before codec_client stop...\n");
codec_client.Stop();
VLOGF("after codec_client stop.\n");
// loop.Shutdown() the rest of the way explicitly (just so we can more easily
// print messages before and after vs. ~loop). If we did this before
// codec_client.Stop() it would cause the channel bindings to fail because
// async waits are failed as cancelled during Shutdown().
VLOGF("before loop.Shutdown()...\n");
loop.Shutdown();
VLOGF("after loop.Shutdown()\n");
// The FIDL loop isn't running any more and the channels are closed. There
// are no other threads left that were started by this function. We can just
// delete stuff now.
// success
// ~codec_client
// ~loop
// ~codec_factory
return;
}