/* -*- 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 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 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 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(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