blob: 35e33eb0e6f0e6f9bc647884bc91d0e1f84a455a [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/gpu/android/codec_wrapper.h"
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <vector>
#include "base/debug/crash_logging.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/sequenced_task_runner.h"
#include "media/base/android/media_codec_util.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace media {
// CodecWrapperImpl is the implementation for CodecWrapper but is separate so
// we can keep its refcounting as an implementation detail. CodecWrapper and
// CodecOutputBuffer are the only two things that hold references to it.
class CodecWrapperImpl : public base::RefCountedThreadSafe<CodecWrapperImpl> {
public:
CodecWrapperImpl(CodecSurfacePair codec_surface_pair,
CodecWrapper::OutputReleasedCB output_buffer_release_cb,
scoped_refptr<base::SequencedTaskRunner> release_task_runner,
const gfx::Size& initial_expected_size);
CodecWrapperImpl(const CodecWrapperImpl&) = delete;
CodecWrapperImpl& operator=(const CodecWrapperImpl&) = delete;
using DequeueStatus = CodecWrapper::DequeueStatus;
using QueueStatus = CodecWrapper::QueueStatus;
CodecSurfacePair TakeCodecSurfacePair();
bool HasUnreleasedOutputBuffers() const;
void DiscardOutputBuffers();
bool IsFlushed() const;
bool IsDraining() const;
bool IsDrained() const;
bool Flush();
bool SetSurface(scoped_refptr<CodecSurfaceBundle> surface_bundle);
scoped_refptr<CodecSurfaceBundle> SurfaceBundle();
QueueStatus QueueInputBuffer(const DecoderBuffer& buffer);
DequeueStatus DequeueOutputBuffer(
base::TimeDelta* presentation_time,
bool* end_of_stream,
std::unique_ptr<CodecOutputBuffer>* codec_buffer);
// Releases the codec buffer and optionally renders it. This is a noop if
// the codec buffer is not valid. Can be called on any thread. Returns true if
// the buffer was released.
bool ReleaseCodecOutputBuffer(int64_t id, bool render);
private:
enum class State {
kError,
kFlushed,
kRunning,
kDraining,
kDrained,
};
friend base::RefCountedThreadSafe<CodecWrapperImpl>;
~CodecWrapperImpl();
void DiscardOutputBuffers_Locked();
// |lock_| protects access to all member variables.
mutable base::Lock lock_;
State state_;
std::unique_ptr<MediaCodecBridge> codec_;
// The currently configured surface.
scoped_refptr<CodecSurfaceBundle> surface_bundle_;
// Buffer ids are unique for a given CodecWrapper and map to MediaCodec buffer
// indices.
int64_t next_buffer_id_;
base::flat_map<int64_t, int> buffer_ids_;
// An input buffer that was dequeued but subsequently rejected from
// QueueInputBuffer() because the codec didn't have the crypto key. We
// maintain ownership of it and reuse it next time.
absl::optional<int> owned_input_buffer_;
// The current output size. Updated when DequeueOutputBuffer() reports
// OUTPUT_FORMAT_CHANGED.
gfx::Size size_;
// A callback that's called whenever an output buffer is released back to the
// codec.
CodecWrapper::OutputReleasedCB output_buffer_release_cb_;
// Do we owe the client an EOS in DequeueOutput, due to an eos that we elided
// while we're already flushed?
bool elided_eos_pending_ = false;
// Most recently reported color space.
gfx::ColorSpace color_space_ = gfx::ColorSpace::CreateSRGB();
// Task runner on which we'll release codec buffers without rendering. May be
// null to always do this on the calling task runner.
scoped_refptr<base::SequencedTaskRunner> release_task_runner_;
};
CodecOutputBuffer::CodecOutputBuffer(scoped_refptr<CodecWrapperImpl> codec,
int64_t id,
const gfx::Size& size,
const gfx::ColorSpace& color_space)
: codec_(std::move(codec)),
id_(id),
size_(size),
color_space_(color_space) {}
// For testing.
CodecOutputBuffer::CodecOutputBuffer(int64_t id,
const gfx::Size& size,
const gfx::ColorSpace& color_space)
: id_(id), size_(size), color_space_(color_space) {}
CodecOutputBuffer::~CodecOutputBuffer() {
// While it will work if we re-release the buffer, since CodecWrapper handles
// it properly, we can save a lock + (possibly) post by checking here if we
// know that it has been rendered already.
//
// |codec_| might be null, but only for tests.
if (!was_rendered_ && codec_)
codec_->ReleaseCodecOutputBuffer(id_, false);
}
bool CodecOutputBuffer::ReleaseToSurface() {
was_rendered_ = true;
auto result = codec_->ReleaseCodecOutputBuffer(id_, true);
if (render_cb_)
std::move(render_cb_).Run();
return result;
}
CodecWrapperImpl::CodecWrapperImpl(
CodecSurfacePair codec_surface_pair,
CodecWrapper::OutputReleasedCB output_buffer_release_cb,
scoped_refptr<base::SequencedTaskRunner> release_task_runner,
const gfx::Size& initial_expected_size)
: state_(State::kFlushed),
codec_(std::move(codec_surface_pair.first)),
surface_bundle_(std::move(codec_surface_pair.second)),
next_buffer_id_(0),
size_(initial_expected_size),
output_buffer_release_cb_(std::move(output_buffer_release_cb)),
release_task_runner_(std::move(release_task_runner)) {
DVLOG(2) << __func__;
}
CodecWrapperImpl::~CodecWrapperImpl() = default;
CodecSurfacePair CodecWrapperImpl::TakeCodecSurfacePair() {
DVLOG(2) << __func__;
base::AutoLock l(lock_);
if (!codec_)
return {nullptr, nullptr};
DiscardOutputBuffers_Locked();
return {std::move(codec_), std::move(surface_bundle_)};
}
bool CodecWrapperImpl::IsFlushed() const {
base::AutoLock l(lock_);
return state_ == State::kFlushed;
}
bool CodecWrapperImpl::IsDraining() const {
base::AutoLock l(lock_);
return state_ == State::kDraining;
}
bool CodecWrapperImpl::IsDrained() const {
base::AutoLock l(lock_);
return state_ == State::kDrained;
}
bool CodecWrapperImpl::HasUnreleasedOutputBuffers() const {
base::AutoLock l(lock_);
return !buffer_ids_.empty();
}
void CodecWrapperImpl::DiscardOutputBuffers() {
DVLOG(2) << __func__;
base::AutoLock l(lock_);
DiscardOutputBuffers_Locked();
}
void CodecWrapperImpl::DiscardOutputBuffers_Locked() {
DVLOG(2) << __func__;
lock_.AssertAcquired();
for (auto& kv : buffer_ids_)
codec_->ReleaseOutputBuffer(kv.second, false);
buffer_ids_.clear();
}
bool CodecWrapperImpl::Flush() {
DVLOG(2) << __func__;
base::AutoLock l(lock_);
DCHECK(codec_ && state_ != State::kError);
// Dequeued buffers are invalidated by flushing.
buffer_ids_.clear();
owned_input_buffer_.reset();
auto status = codec_->Flush();
if (status == MEDIA_CODEC_ERROR) {
state_ = State::kError;
return false;
}
state_ = State::kFlushed;
elided_eos_pending_ = false;
return true;
}
CodecWrapperImpl::QueueStatus CodecWrapperImpl::QueueInputBuffer(
const DecoderBuffer& buffer) {
DVLOG(4) << __func__;
base::AutoLock l(lock_);
DCHECK(codec_ && state_ != State::kError);
// Dequeue an input buffer if we don't already own one.
int input_buffer;
if (owned_input_buffer_) {
input_buffer = *owned_input_buffer_;
owned_input_buffer_.reset();
} else {
MediaCodecStatus status =
codec_->DequeueInputBuffer(base::TimeDelta(), &input_buffer);
switch (status) {
case MEDIA_CODEC_ERROR:
state_ = State::kError;
return QueueStatus::kError;
case MEDIA_CODEC_TRY_AGAIN_LATER:
return QueueStatus::kTryAgainLater;
case MEDIA_CODEC_OK:
break;
default:
NOTREACHED();
return QueueStatus::kError;
}
}
// Queue EOS if it's an EOS buffer.
if (buffer.end_of_stream()) {
// Some MediaCodecs consider it an error to get an EOS as the first buffer
// (http://crbug.com/672268). Just elide it. We also elide kDrained, since
// kFlushed => elided eos => kDrained, and it would still be the first
// buffer from MediaCodec's perspective. While kDrained does not imply that
// it's the first buffer in all cases, it's still safe to elide.
if (state_ == State::kFlushed || state_ == State::kDrained)
elided_eos_pending_ = true;
else
codec_->QueueEOS(input_buffer);
state_ = State::kDraining;
return QueueStatus::kOk;
}
// Queue a buffer.
const DecryptConfig* decrypt_config = buffer.decrypt_config();
MediaCodecStatus status;
if (decrypt_config) {
// TODO(crbug.com/813845): Use encryption scheme settings from
// DecryptConfig.
status = codec_->QueueSecureInputBuffer(
input_buffer, buffer.data(), buffer.data_size(),
decrypt_config->key_id(), decrypt_config->iv(),
decrypt_config->subsamples(), decrypt_config->encryption_scheme(),
decrypt_config->encryption_pattern(), buffer.timestamp());
} else {
status = codec_->QueueInputBuffer(input_buffer, buffer.data(),
buffer.data_size(), buffer.timestamp());
}
switch (status) {
case MEDIA_CODEC_OK:
state_ = State::kRunning;
return QueueStatus::kOk;
case MEDIA_CODEC_ERROR:
state_ = State::kError;
return QueueStatus::kError;
case MEDIA_CODEC_NO_KEY:
// The input buffer remains owned by us, so save it for reuse.
owned_input_buffer_ = input_buffer;
return QueueStatus::kNoKey;
default:
NOTREACHED();
return QueueStatus::kError;
}
}
CodecWrapperImpl::DequeueStatus CodecWrapperImpl::DequeueOutputBuffer(
base::TimeDelta* presentation_time,
bool* end_of_stream,
std::unique_ptr<CodecOutputBuffer>* codec_buffer) {
DVLOG(4) << __func__;
base::AutoLock l(lock_);
DCHECK(codec_ && state_ != State::kError);
// If |*codec_buffer| were not null, deleting it would deadlock when its
// destructor calls ReleaseCodecOutputBuffer().
DCHECK(!*codec_buffer);
if (elided_eos_pending_) {
// An eos was sent while we were already flushed -- pretend it's ready.
elided_eos_pending_ = false;
state_ = State::kDrained;
if (end_of_stream)
*end_of_stream = true;
return DequeueStatus::kOk;
}
// Dequeue in a loop so we can avoid propagating the uninteresting
// OUTPUT_FORMAT_CHANGED and OUTPUT_BUFFERS_CHANGED statuses to our caller.
for (int attempt = 0; attempt < 3; ++attempt) {
int index = -1;
size_t unused;
bool eos = false;
auto status =
codec_->DequeueOutputBuffer(base::TimeDelta(), &index, &unused, &unused,
presentation_time, &eos, nullptr);
switch (status) {
case MEDIA_CODEC_OK: {
if (eos) {
state_ = State::kDrained;
// We assume that the EOS flag is only ever attached to empty output
// buffers because we submit the EOS flag on empty input buffers. The
// MediaCodec docs leave open the possibility that the last non-empty
// output buffer has the EOS flag but we haven't seen that happen.
codec_->ReleaseOutputBuffer(index, false);
if (end_of_stream)
*end_of_stream = true;
return DequeueStatus::kOk;
}
int64_t buffer_id = next_buffer_id_++;
buffer_ids_[buffer_id] = index;
*codec_buffer = base::WrapUnique(
new CodecOutputBuffer(this, buffer_id, size_, color_space_));
return DequeueStatus::kOk;
}
case MEDIA_CODEC_TRY_AGAIN_LATER: {
return DequeueStatus::kTryAgainLater;
}
case MEDIA_CODEC_ERROR: {
state_ = State::kError;
return DequeueStatus::kError;
}
case MEDIA_CODEC_OUTPUT_FORMAT_CHANGED: {
gfx::Size temp_size;
if (codec_->GetOutputSize(&temp_size) == MEDIA_CODEC_ERROR) {
state_ = State::kError;
return DequeueStatus::kError;
}
// In automated testing, we regularly see a blip where MediaCodec sends
// a format change to size 0,0, some number of output buffer available
// signals, and then finally the real size. Ignore this transient size
// change to avoid output errors. We'll either reuse the previous size
// information or the size provided during configure.
// See https://crbug.com/1207682.
if (!temp_size.IsEmpty())
size_ = temp_size;
bool error =
codec_->GetOutputColorSpace(&color_space_) == MEDIA_CODEC_ERROR;
UMA_HISTOGRAM_BOOLEAN("Media.Android.GetColorSpaceError", error);
if (error && !size_.IsEmpty()) {
// If we get back an unsupported color space, then just default to
// sRGB for < 720p, or 709 otherwise. It's better than nothing.
color_space_ = size_.width() >= 1280 ? gfx::ColorSpace::CreateREC709()
: gfx::ColorSpace::CreateSRGB();
}
continue;
}
case MEDIA_CODEC_OUTPUT_BUFFERS_CHANGED: {
continue;
}
case MEDIA_CODEC_NO_KEY: {
NOTREACHED();
return DequeueStatus::kError;
}
}
}
state_ = State::kError;
return DequeueStatus::kError;
}
bool CodecWrapperImpl::SetSurface(
scoped_refptr<CodecSurfaceBundle> surface_bundle) {
DVLOG(2) << __func__;
base::AutoLock l(lock_);
DCHECK(surface_bundle);
DCHECK(codec_ && state_ != State::kError);
if (!codec_->SetSurface(surface_bundle->GetJavaSurface())) {
state_ = State::kError;
return false;
}
surface_bundle_ = std::move(surface_bundle);
return true;
}
scoped_refptr<CodecSurfaceBundle> CodecWrapperImpl::SurfaceBundle() {
base::AutoLock l(lock_);
return surface_bundle_;
}
bool CodecWrapperImpl::ReleaseCodecOutputBuffer(int64_t id, bool render) {
if (!render && release_task_runner_ &&
!release_task_runner_->RunsTasksInCurrentSequence()) {
// Note that this can only delay releases, but that won't ultimately change
// the ordering at the codec, assuming that releases / renders originate
// from the same thread.
//
// We know that a render call that happens before a release call will still
// run before the release's posted task, since it happens before we even
// post it.
//
// Similarly, renders are kept in order with each other.
//
// It is possible that a render happens before the posted task(s) of some
// earlier release(s) (with no intervening renders, since those are
// ordered). In this case, though, the loop below will still release
// everything earlier than the rendered buffer, so the codec still sees the
// same sequence of calls -- some releases follwed by a render.
//
// Of course, if releases and renders are posted from different threads,
// then it's unclear what the ordering was anyway.
release_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(
base::IgnoreResult(&CodecWrapperImpl::ReleaseCodecOutputBuffer),
this, id, render));
return true;
}
base::AutoLock l(lock_);
// Adding a scoped crash key here to detect the cause of gpu hang.
// crbug.com/1292936.
static auto* kCrashKey_1 = base::debug::AllocateCrashKeyString(
"acquired_lock_inside_codecwrapperimpl_releasecodecoutputbuffer",
base::debug::CrashKeySize::Size256);
base::debug::ScopedCrashKeyString scoped_crash_key_1(kCrashKey_1, "1");
if (!codec_ || state_ == State::kError)
return false;
auto buffer_it = buffer_ids_.find(id);
bool valid = buffer_it != buffer_ids_.end();
DVLOG(2) << __func__ << " id=" << id << " render=" << render
<< " valid=" << valid;
if (!valid)
return false;
int index = buffer_it->second;
{
// Adding another scoped crash key here to detect the cause of gpu hang.
// crbug.com/1292936.
static auto* kCrashKey_2 = base::debug::AllocateCrashKeyString(
"executing_mediacodec_releaseoutputbuffer",
base::debug::CrashKeySize::Size256);
base::debug::ScopedCrashKeyString scoped_crash_key_2(kCrashKey_2, "1");
codec_->ReleaseOutputBuffer(index, render);
}
buffer_ids_.erase(buffer_it);
if (output_buffer_release_cb_) {
output_buffer_release_cb_.Run(state_ == State::kDrained ||
state_ == State::kDraining ||
buffer_ids_.empty());
}
return true;
}
CodecWrapper::CodecWrapper(
CodecSurfacePair codec_surface_pair,
OutputReleasedCB output_buffer_release_cb,
scoped_refptr<base::SequencedTaskRunner> release_task_runner,
const gfx::Size& initial_expected_size)
: impl_(new CodecWrapperImpl(std::move(codec_surface_pair),
std::move(output_buffer_release_cb),
std::move(release_task_runner),
initial_expected_size)) {}
CodecWrapper::~CodecWrapper() {
// The codec must have already been taken.
DCHECK(!impl_->TakeCodecSurfacePair().first);
}
CodecSurfacePair CodecWrapper::TakeCodecSurfacePair() {
return impl_->TakeCodecSurfacePair();
}
bool CodecWrapper::HasUnreleasedOutputBuffers() const {
return impl_->HasUnreleasedOutputBuffers();
}
void CodecWrapper::DiscardOutputBuffers() {
impl_->DiscardOutputBuffers();
}
bool CodecWrapper::IsFlushed() const {
return impl_->IsFlushed();
}
bool CodecWrapper::IsDraining() const {
return impl_->IsDraining();
}
bool CodecWrapper::IsDrained() const {
return impl_->IsDrained();
}
bool CodecWrapper::Flush() {
return impl_->Flush();
}
CodecWrapper::QueueStatus CodecWrapper::QueueInputBuffer(
const DecoderBuffer& buffer) {
return impl_->QueueInputBuffer(buffer);
}
CodecWrapper::DequeueStatus CodecWrapper::DequeueOutputBuffer(
base::TimeDelta* presentation_time,
bool* end_of_stream,
std::unique_ptr<CodecOutputBuffer>* codec_buffer) {
return impl_->DequeueOutputBuffer(presentation_time, end_of_stream,
codec_buffer);
}
bool CodecWrapper::SetSurface(
scoped_refptr<CodecSurfaceBundle> surface_bundle) {
return impl_->SetSurface(std::move(surface_bundle));
}
scoped_refptr<CodecSurfaceBundle> CodecWrapper::SurfaceBundle() {
return impl_->SurfaceBundle();
}
} // namespace media