diff options
Diffstat (limited to 'dom/media/gmp/widevine-adapter/WidevineVideoDecoder.cpp')
-rw-r--r-- | dom/media/gmp/widevine-adapter/WidevineVideoDecoder.cpp | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/dom/media/gmp/widevine-adapter/WidevineVideoDecoder.cpp b/dom/media/gmp/widevine-adapter/WidevineVideoDecoder.cpp new file mode 100644 index 000000000..70d2fd8e0 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineVideoDecoder.cpp @@ -0,0 +1,400 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WidevineVideoDecoder.h" + +#include "mp4_demuxer/AnnexB.h" +#include "WidevineUtils.h" +#include "WidevineVideoFrame.h" +#include "mozilla/Move.h" + +using namespace cdm; + +namespace mozilla { + +WidevineVideoDecoder::WidevineVideoDecoder(GMPVideoHost* aVideoHost, + RefPtr<CDMWrapper> aCDMWrapper) + : mVideoHost(aVideoHost) + , mCDMWrapper(Move(aCDMWrapper)) + , mExtraData(new MediaByteBuffer()) + , mSentInput(false) + , mCodecType(kGMPVideoCodecInvalid) + , mReturnOutputCallDepth(0) + , mDrainPending(false) + , mResetInProgress(false) +{ + // Expect to start with a CDM wrapper, will release it in DecodingComplete(). + MOZ_ASSERT(mCDMWrapper); + Log("WidevineVideoDecoder created this=%p", this); + + // Corresponding Release is in DecodingComplete(). + AddRef(); +} + +WidevineVideoDecoder::~WidevineVideoDecoder() +{ + Log("WidevineVideoDecoder destroyed this=%p", this); +} + +static +VideoDecoderConfig::VideoCodecProfile +ToCDMH264Profile(uint8_t aProfile) +{ + switch (aProfile) { + case 66: return VideoDecoderConfig::kH264ProfileBaseline; + case 77: return VideoDecoderConfig::kH264ProfileMain; + case 88: return VideoDecoderConfig::kH264ProfileExtended; + case 100: return VideoDecoderConfig::kH264ProfileHigh; + case 110: return VideoDecoderConfig::kH264ProfileHigh10; + case 122: return VideoDecoderConfig::kH264ProfileHigh422; + case 144: return VideoDecoderConfig::kH264ProfileHigh444Predictive; + } + return VideoDecoderConfig::kUnknownVideoCodecProfile; +} + +void +WidevineVideoDecoder::InitDecode(const GMPVideoCodec& aCodecSettings, + const uint8_t* aCodecSpecific, + uint32_t aCodecSpecificLength, + GMPVideoDecoderCallback* aCallback, + int32_t aCoreCount) +{ + mCallback = aCallback; + VideoDecoderConfig config; + mCodecType = aCodecSettings.mCodecType; + if (mCodecType == kGMPVideoCodecH264) { + config.codec = VideoDecoderConfig::kCodecH264; + const GMPVideoCodecH264* h264 = (const GMPVideoCodecH264*)(aCodecSpecific); + config.profile = ToCDMH264Profile(h264->mAVCC.mProfile); + } else if (mCodecType == kGMPVideoCodecVP8) { + config.codec = VideoDecoderConfig::kCodecVp8; + config.profile = VideoDecoderConfig::kProfileNotNeeded; + } else if (mCodecType == kGMPVideoCodecVP9) { + config.codec = VideoDecoderConfig::kCodecVp9; + config.profile = VideoDecoderConfig::kProfileNotNeeded; + } else { + mCallback->Error(GMPInvalidArgErr); + return; + } + config.format = kYv12; + config.coded_size = Size(aCodecSettings.mWidth, aCodecSettings.mHeight); + mExtraData->AppendElements(aCodecSpecific + 1, aCodecSpecificLength); + config.extra_data = mExtraData->Elements(); + config.extra_data_size = mExtraData->Length(); + Status rv = CDM()->InitializeVideoDecoder(config); + if (rv != kSuccess) { + mCallback->Error(ToGMPErr(rv)); + return; + } + Log("WidevineVideoDecoder::InitDecode() rv=%d", rv); + mAnnexB = mp4_demuxer::AnnexB::ConvertExtraDataToAnnexB(mExtraData); +} + +void +WidevineVideoDecoder::Decode(GMPVideoEncodedFrame* aInputFrame, + bool aMissingFrames, + const uint8_t* aCodecSpecificInfo, + uint32_t aCodecSpecificInfoLength, + int64_t aRenderTimeMs) +{ + // We should not be given new input if a drain has been initiated + MOZ_ASSERT(!mDrainPending); + // We may not get the same out of the CDM decoder as we put in, and there + // may be some latency, i.e. we may need to input (say) 30 frames before + // we receive output. So we need to store the durations of the frames input, + // and retrieve them on output. + mFrameDurations[aInputFrame->TimeStamp()] = aInputFrame->Duration(); + + mSentInput = true; + InputBuffer sample; + + RefPtr<MediaRawData> raw( + new MediaRawData(aInputFrame->Buffer(), aInputFrame->Size())); + if (aInputFrame->Size() && !raw->Data()) { + // OOM. + mCallback->Error(GMPAllocErr); + return; + } + raw->mExtraData = mExtraData; + raw->mKeyframe = (aInputFrame->FrameType() == kGMPKeyFrame); + if (mCodecType == kGMPVideoCodecH264) { + // Convert input from AVCC, which GMPAPI passes in, to AnnexB, which + // Chromium uses internally. + mp4_demuxer::AnnexB::ConvertSampleToAnnexB(raw); + } + + const GMPEncryptedBufferMetadata* crypto = aInputFrame->GetDecryptionData(); + nsTArray<SubsampleEntry> subsamples; + InitInputBuffer(crypto, aInputFrame->TimeStamp(), raw->Data(), raw->Size(), sample, subsamples); + + // For keyframes, ConvertSampleToAnnexB will stick the AnnexB extra data + // at the start of the input. So we need to account for that as clear data + // in the subsamples. + if (raw->mKeyframe && !subsamples.IsEmpty() && mCodecType == kGMPVideoCodecH264) { + subsamples[0].clear_bytes += mAnnexB->Length(); + } + + WidevineVideoFrame frame; + Status rv = CDM()->DecryptAndDecodeFrame(sample, &frame); + Log("WidevineVideoDecoder::Decode(timestamp=%lld) rv=%d", sample.timestamp, rv); + + // Destroy frame, so that the shmem is now free to be used to return + // output to the Gecko process. + aInputFrame->Destroy(); + aInputFrame = nullptr; + + if (rv == kSuccess) { + if (!ReturnOutput(frame)) { + Log("WidevineVideoDecoder::Decode() Failed in ReturnOutput()"); + mCallback->Error(GMPDecodeErr); + return; + } + // A reset should only be started at most at level mReturnOutputCallDepth 1, + // and if it's started it should be finished by that call by the time + // the it returns, so it should always be false by this point. + MOZ_ASSERT(!mResetInProgress); + // Only request more data if we don't have pending samples. + if (mFrameAllocationQueue.empty()) { + MOZ_ASSERT(mCDMWrapper); + mCallback->InputDataExhausted(); + } + } else if (rv == kNeedMoreData) { + MOZ_ASSERT(mCDMWrapper); + mCallback->InputDataExhausted(); + } else { + mCallback->Error(ToGMPErr(rv)); + } + // Finish a drain if pending and we have no pending ReturnOutput calls on the stack. + if (mDrainPending && mReturnOutputCallDepth == 0) { + Drain(); + } +} + +// Util class to assist with counting mReturnOutputCallDepth. +class CounterHelper { +public: + // RAII, increment counter + explicit CounterHelper(int32_t& counter) + : mCounter(counter) + { + mCounter++; + } + + // RAII, decrement counter + ~CounterHelper() + { + mCounter--; + } + +private: + int32_t& mCounter; +}; + +// Util class to make sure GMP frames are freed. Holds a GMPVideoi420Frame* +// and will destroy it when the helper is destroyed unless the held frame +// if forgotten with ForgetFrame. +class FrameDestroyerHelper { +public: + explicit FrameDestroyerHelper(GMPVideoi420Frame*& frame) + : frame(frame) + { + } + + // RAII, destroy frame if held. + ~FrameDestroyerHelper() + { + if (frame) { + frame->Destroy(); + } + frame = nullptr; + } + + // Forget the frame without destroying it. + void ForgetFrame() + { + frame = nullptr; + } + +private: + GMPVideoi420Frame* frame; +}; + + +// Special handing is needed around ReturnOutput as it spins the IPC message +// queue when creating an empty frame and can end up with reentrant calls into +// the class methods. +bool +WidevineVideoDecoder::ReturnOutput(WidevineVideoFrame& aCDMFrame) +{ + MOZ_ASSERT(mReturnOutputCallDepth >= 0); + CounterHelper counterHelper(mReturnOutputCallDepth); + mFrameAllocationQueue.push_back(Move(aCDMFrame)); + if (mReturnOutputCallDepth > 1) { + // In a reentrant call. + return true; + } + while (!mFrameAllocationQueue.empty()) { + MOZ_ASSERT(mReturnOutputCallDepth == 1); + // If we're at call level 1 a reset should not have been started. A + // reset may be received during CreateEmptyFrame below, but we should not + // be in a reset at this stage -- this would indicate receiving decode + // messages before completing our reset, which we should not. + MOZ_ASSERT(!mResetInProgress); + WidevineVideoFrame currentCDMFrame = Move(mFrameAllocationQueue.front()); + mFrameAllocationQueue.pop_front(); + GMPVideoFrame* f = nullptr; + auto err = mVideoHost->CreateFrame(kGMPI420VideoFrame, &f); + if (GMP_FAILED(err) || !f) { + Log("Failed to create i420 frame!\n"); + return false; + } + auto gmpFrame = static_cast<GMPVideoi420Frame*>(f); + FrameDestroyerHelper frameDestroyerHelper(gmpFrame); + Size size = currentCDMFrame.Size(); + const int32_t yStride = currentCDMFrame.Stride(VideoFrame::kYPlane); + const int32_t uStride = currentCDMFrame.Stride(VideoFrame::kUPlane); + const int32_t vStride = currentCDMFrame.Stride(VideoFrame::kVPlane); + const int32_t halfHeight = size.height / 2; + // This call can cause a shmem alloc, during this alloc other calls + // may be made to this class and placed on the stack. ***WARNING***: + // other IPC calls can happen during this call, resulting in calls + // being made to the CDM. After this call state can have changed, + // and should be reevaluated. + err = gmpFrame->CreateEmptyFrame(size.width, + size.height, + yStride, + uStride, + vStride); + // Assert possible reentrant calls or resets haven't altered level unexpectedly. + MOZ_ASSERT(mReturnOutputCallDepth == 1); + ENSURE_GMP_SUCCESS(err, false); + + // If a reset started we need to dump the current frame and complete the reset. + if (mResetInProgress) { + MOZ_ASSERT(mCDMWrapper); + MOZ_ASSERT(mFrameAllocationQueue.empty()); + CompleteReset(); + return true; + } + + err = gmpFrame->SetWidth(size.width); + ENSURE_GMP_SUCCESS(err, false); + + err = gmpFrame->SetHeight(size.height); + ENSURE_GMP_SUCCESS(err, false); + + Buffer* buffer = currentCDMFrame.FrameBuffer(); + uint8_t* outBuffer = gmpFrame->Buffer(kGMPYPlane); + ENSURE_TRUE(outBuffer != nullptr, false); + MOZ_ASSERT(gmpFrame->AllocatedSize(kGMPYPlane) >= yStride*size.height); + memcpy(outBuffer, + buffer->Data() + currentCDMFrame.PlaneOffset(VideoFrame::kYPlane), + yStride * size.height); + + outBuffer = gmpFrame->Buffer(kGMPUPlane); + ENSURE_TRUE(outBuffer != nullptr, false); + MOZ_ASSERT(gmpFrame->AllocatedSize(kGMPUPlane) >= uStride * halfHeight); + memcpy(outBuffer, + buffer->Data() + currentCDMFrame.PlaneOffset(VideoFrame::kUPlane), + uStride * halfHeight); + + outBuffer = gmpFrame->Buffer(kGMPVPlane); + ENSURE_TRUE(outBuffer != nullptr, false); + MOZ_ASSERT(gmpFrame->AllocatedSize(kGMPVPlane) >= vStride * halfHeight); + memcpy(outBuffer, + buffer->Data() + currentCDMFrame.PlaneOffset(VideoFrame::kVPlane), + vStride * halfHeight); + + gmpFrame->SetTimestamp(currentCDMFrame.Timestamp()); + + auto d = mFrameDurations.find(currentCDMFrame.Timestamp()); + if (d != mFrameDurations.end()) { + gmpFrame->SetDuration(d->second); + mFrameDurations.erase(d); + } + + // Forget frame so it's not deleted, call back taking ownership. + frameDestroyerHelper.ForgetFrame(); + mCallback->Decoded(gmpFrame); + } + + return true; +} + +void +WidevineVideoDecoder::Reset() +{ + Log("WidevineVideoDecoder::Reset() mSentInput=%d", mSentInput); + // We shouldn't reset if a drain is pending. + MOZ_ASSERT(!mDrainPending); + mResetInProgress = true; + if (mSentInput) { + CDM()->ResetDecoder(kStreamTypeVideo); + } + // Remove queued frames, but do not reset mReturnOutputCallDepth, let the + // ReturnOutput calls unwind and decrement the counter as needed. + mFrameAllocationQueue.clear(); + mFrameDurations.clear(); + // Only if no ReturnOutput calls are in progress can we complete, otherwise + // ReturnOutput needs to finalize the reset. + if (mReturnOutputCallDepth == 0) { + CompleteReset(); + } +} + +void +WidevineVideoDecoder::CompleteReset() +{ + mCallback->ResetComplete(); + mSentInput = false; + mResetInProgress = false; +} + +void +WidevineVideoDecoder::Drain() +{ + Log("WidevineVideoDecoder::Drain()"); + if (mReturnOutputCallDepth > 0) { + Log("Drain call is reentrant, postponing drain"); + mDrainPending = true; + return; + } + + Status rv = kSuccess; + while (rv == kSuccess) { + WidevineVideoFrame frame; + InputBuffer sample; + Status rv = CDM()->DecryptAndDecodeFrame(sample, &frame); + Log("WidevineVideoDecoder::Drain(); DecryptAndDecodeFrame() rv=%d", rv); + if (frame.Format() == kUnknownVideoFormat) { + break; + } + if (rv == kSuccess) { + if (!ReturnOutput(frame)) { + Log("WidevineVideoDecoder::Decode() Failed in ReturnOutput()"); + } + } + } + // Shouldn't be reset while draining. + MOZ_ASSERT(!mResetInProgress); + + CDM()->ResetDecoder(kStreamTypeVideo); + mDrainPending = false; + mCallback->DrainComplete(); +} + +void +WidevineVideoDecoder::DecodingComplete() +{ + Log("WidevineVideoDecoder::DecodingComplete()"); + if (mCDMWrapper) { + CDM()->DeinitializeDecoder(kStreamTypeVideo); + mCDMWrapper = nullptr; + } + // Release that corresponds to AddRef() in constructor. + Release(); +} + +} // namespace mozilla |