diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/media/gmp | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
129 files changed, 22289 insertions, 0 deletions
diff --git a/dom/media/gmp-plugin-openh264/fakeopenh264.info b/dom/media/gmp-plugin-openh264/fakeopenh264.info new file mode 100644 index 0000000000..814a96be32 --- /dev/null +++ b/dom/media/gmp-plugin-openh264/fakeopenh264.info @@ -0,0 +1,4 @@ +Name: fakeopenh264 +Description: Fake GMP Plugin +Version: 1.0 +APIs: encode-video[h264:fake], decode-video[h264:fake] diff --git a/dom/media/gmp-plugin-openh264/gmp-fake-openh264.cpp b/dom/media/gmp-plugin-openh264/gmp-fake-openh264.cpp new file mode 100644 index 0000000000..4c5e95643e --- /dev/null +++ b/dom/media/gmp-plugin-openh264/gmp-fake-openh264.cpp @@ -0,0 +1,406 @@ +/*! + * \copy + * Copyright (c) 2009-2014, Cisco Systems + * Copyright (c) 2014, Mozilla + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * + ************************************************************************************* + */ + +#include <stdint.h> +#include <cstring> +#include <iostream> +#include <assert.h> + +#include "gmp-platform.h" +#include "gmp-video-host.h" +#include "gmp-video-encode.h" +#include "gmp-video-decode.h" +#include "gmp-video-frame-i420.h" +#include "gmp-video-frame-encoded.h" + +#include "mozilla/PodOperations.h" + +#if defined(_MSC_VER) +# define PUBLIC_FUNC __declspec(dllexport) +#else +# define PUBLIC_FUNC +#endif + +#define BIG_FRAME 10000 + +#define GL_CRIT 0 +#define GL_ERROR 1 +#define GL_INFO 2 +#define GL_DEBUG 3 + +const char* kLogStrings[] = {"Critical", "Error", "Info", "Debug"}; + +static int g_log_level = GL_CRIT; + +#define GMPLOG(l, x) \ + do { \ + if (l <= g_log_level) { \ + const char* log_string = "unknown"; \ + if ((l >= 0) && (l <= 3)) { \ + log_string = kLogStrings[l]; \ + } \ + std::cerr << log_string << ": " << x << std::endl; \ + } \ + } while (0) + +class FakeVideoEncoder; +class FakeVideoDecoder; + +// Turn off padding for this structure. Having extra bytes can result in +// bitstream parsing problems. +#pragma pack(push, 1) +struct EncodedFrame { + struct SPSNalu { + uint32_t size_; + uint8_t payload[14]; + } sps_nalu; + struct PPSNalu { + uint32_t size_; + uint8_t payload[4]; + } pps_nalu; + struct IDRNalu { + uint32_t size_; + uint8_t h264_compat_; + uint32_t magic_; + uint32_t width_; + uint32_t height_; + uint8_t y_; + uint8_t u_; + uint8_t v_; + uint32_t timestamp_; + } idr_nalu; +}; +#pragma pack(pop) + +#define ENCODED_FRAME_MAGIC 0x004000b8 + +template <typename T> +class SelfDestruct { + public: + explicit SelfDestruct(T* t) : t_(t) {} + ~SelfDestruct() { + if (t_) { + t_->Destroy(); + } + } + + private: + T* t_; +}; + +class FakeVideoEncoder : public GMPVideoEncoder { + public: + explicit FakeVideoEncoder(GMPVideoHost* hostAPI) : host_(hostAPI) {} + + void InitEncode(const GMPVideoCodec& codecSettings, + const uint8_t* aCodecSpecific, uint32_t aCodecSpecificSize, + GMPVideoEncoderCallback* callback, int32_t numberOfCores, + uint32_t maxPayloadSize) override { + callback_ = callback; + frame_size_ = (maxPayloadSize > 0 && maxPayloadSize < BIG_FRAME) + ? maxPayloadSize + : BIG_FRAME; + frame_size_ -= 24 + 40; + // default header+extension size is 24, but let's leave extra room if + // we enable more extensions. + // XXX -- why isn't the size passed in based on the size minus extensions? + + const char* env = getenv("GMP_LOGGING"); + if (env) { + g_log_level = atoi(env); + } + GMPLOG(GL_INFO, "Initialized encoder"); + } + + void SendFrame(GMPVideoi420Frame* inputImage, GMPVideoFrameType frame_type, + int nal_type) { + // Encode this in a frame that looks a little bit like H.264. + // Send SPS/PPS/IDR to avoid confusing people + // Copy the data. This really should convert this to network byte order. + EncodedFrame eframe; + + // These values were chosen to force a SPS id of 0 + eframe.sps_nalu = {sizeof(EncodedFrame::SPSNalu) - sizeof(uint32_t), + {0x67, 0x42, 0xc0, 0xd, 0x8c, 0x8d, 0x40, 0xa0, 0xf9, + 0x0, 0xf0, 0x88, 0x46, 0xa0}}; + + // These values were chosen to force a PPS id of 0 + eframe.pps_nalu = {sizeof(EncodedFrame::PPSNalu) - sizeof(uint32_t), + {0x68, 0xce, 0x3c, 0x80}}; + + eframe.idr_nalu.size_ = sizeof(EncodedFrame::IDRNalu) - sizeof(uint32_t); + // We force IFrame here - if we send PFrames, the webrtc.org code gets + // tripped up attempting to find non-existent previous IFrames. + eframe.idr_nalu.h264_compat_ = + nal_type; // 5 = IFrame/IDR slice, 1=PFrame/slice + eframe.idr_nalu.magic_ = ENCODED_FRAME_MAGIC; + eframe.idr_nalu.width_ = inputImage->Width(); + eframe.idr_nalu.height_ = inputImage->Height(); + eframe.idr_nalu.y_ = AveragePlane(inputImage->Buffer(kGMPYPlane), + inputImage->AllocatedSize(kGMPYPlane)); + eframe.idr_nalu.u_ = AveragePlane(inputImage->Buffer(kGMPUPlane), + inputImage->AllocatedSize(kGMPUPlane)); + eframe.idr_nalu.v_ = AveragePlane(inputImage->Buffer(kGMPVPlane), + inputImage->AllocatedSize(kGMPVPlane)); + + eframe.idr_nalu.timestamp_ = inputImage->Timestamp(); + + // Now return the encoded data back to the parent. + GMPVideoFrame* ftmp; + GMPErr err = host_->CreateFrame(kGMPEncodedVideoFrame, &ftmp); + if (err != GMPNoErr) { + GMPLOG(GL_ERROR, "Error creating encoded frame"); + return; + } + + GMPVideoEncodedFrame* f = static_cast<GMPVideoEncodedFrame*>(ftmp); + + err = f->CreateEmptyFrame(sizeof(eframe)); + if (err != GMPNoErr) { + GMPLOG(GL_ERROR, "Error allocating frame data"); + f->Destroy(); + return; + } + memcpy(f->Buffer(), &eframe, sizeof(eframe)); + f->SetEncodedWidth(eframe.idr_nalu.width_); + f->SetEncodedHeight(eframe.idr_nalu.height_); + f->SetTimeStamp(eframe.idr_nalu.timestamp_); + f->SetFrameType(frame_type); + f->SetCompleteFrame(true); + f->SetBufferType(GMP_BufferLength32); + + GMPLOG(GL_DEBUG, "Encoding complete. type= " + << f->FrameType() + << " NAL_type=" << (int)eframe.idr_nalu.h264_compat_ + << " length=" << f->Size() + << " timestamp=" << f->TimeStamp() + << " width/height=" << eframe.idr_nalu.width_ << "x" + << eframe.idr_nalu.height_); + + // Return the encoded frame. + GMPCodecSpecificInfo info; + mozilla::PodZero(&info); + info.mCodecType = kGMPVideoCodecH264; + info.mBufferType = GMP_BufferLength32; + info.mCodecSpecific.mH264.mSimulcastIdx = 0; + GMPLOG(GL_DEBUG, "Calling callback"); + callback_->Encoded(f, reinterpret_cast<uint8_t*>(&info), sizeof(info)); + GMPLOG(GL_DEBUG, "Callback called"); + } + + void Encode(GMPVideoi420Frame* inputImage, const uint8_t* aCodecSpecificInfo, + uint32_t aCodecSpecificInfoLength, + const GMPVideoFrameType* aFrameTypes, + uint32_t aFrameTypesLength) override { + GMPLOG(GL_DEBUG, __FUNCTION__ << " size=" << inputImage->Width() << "x" + << inputImage->Height()); + + assert(aFrameTypesLength != 0); + GMPVideoFrameType frame_type = aFrameTypes[0]; + + SelfDestruct<GMPVideoi420Frame> ifd(inputImage); + + if (frame_type == kGMPKeyFrame) { + if (!inputImage) return; + } + if (!inputImage) { + GMPLOG(GL_ERROR, "no input image"); + return; + } + + if (frame_type == kGMPKeyFrame || + frames_encoded_++ % 10 == 0) { // periodically send iframes anyways + // 5 = IFrame/IDR slice + SendFrame(inputImage, kGMPKeyFrame, 5); + } else { + // 1 = PFrame/slice + SendFrame(inputImage, frame_type, 1); + } + } + + void SetChannelParameters(uint32_t aPacketLoss, uint32_t aRTT) override {} + + void SetRates(uint32_t aNewBitRate, uint32_t aFrameRate) override {} + + void SetPeriodicKeyFrames(bool aEnable) override {} + + void EncodingComplete() override { delete this; } + + private: + uint8_t AveragePlane(uint8_t* ptr, size_t len) { + uint64_t val = 0; + + for (size_t i = 0; i < len; ++i) { + val += ptr[i]; + } + + return (val / len) % 0xff; + } + + GMPVideoHost* host_; + GMPVideoEncoderCallback* callback_ = nullptr; + uint32_t frame_size_ = BIG_FRAME; + uint32_t frames_encoded_ = 0; +}; + +class FakeVideoDecoder : public GMPVideoDecoder { + public: + explicit FakeVideoDecoder(GMPVideoHost* hostAPI) + : host_(hostAPI), callback_(nullptr) {} + + ~FakeVideoDecoder() override = default; + + void InitDecode(const GMPVideoCodec& codecSettings, + const uint8_t* aCodecSpecific, uint32_t aCodecSpecificSize, + GMPVideoDecoderCallback* callback, + int32_t coreCount) override { + GMPLOG(GL_INFO, "InitDecode"); + + const char* env = getenv("GMP_LOGGING"); + if (env) { + g_log_level = atoi(env); + } + callback_ = callback; + } + + void Decode(GMPVideoEncodedFrame* inputFrame, bool missingFrames, + const uint8_t* aCodecSpecificInfo, + uint32_t aCodecSpecificInfoLength, + int64_t renderTimeMs = -1) override { + GMPLOG(GL_DEBUG, __FUNCTION__ + << "Decoding frame size=" << inputFrame->Size() + << " timestamp=" << inputFrame->TimeStamp()); + + // Attach a self-destructor so that the input frame is destroyed on return. + SelfDestruct<GMPVideoEncodedFrame> ifd(inputFrame); + + EncodedFrame* eframe; + eframe = reinterpret_cast<EncodedFrame*>(inputFrame->Buffer()); + GMPLOG(GL_DEBUG, "magic=" << eframe->idr_nalu.magic_ << " h264_compat=" + << (int)eframe->idr_nalu.h264_compat_ + << " width=" << eframe->idr_nalu.width_ + << " height=" << eframe->idr_nalu.height_ + << " timestamp=" << inputFrame->TimeStamp() + << " y/u/v=" << (int)eframe->idr_nalu.y_ << ":" + << (int)eframe->idr_nalu.u_ << ":" + << (int)eframe->idr_nalu.v_); + if (inputFrame->Size() != (sizeof(*eframe))) { + GMPLOG(GL_ERROR, "Couldn't decode frame. Size=" << inputFrame->Size()); + return; + } + + if (eframe->idr_nalu.magic_ != ENCODED_FRAME_MAGIC) { + GMPLOG(GL_ERROR, + "Couldn't decode frame. Magic=" << eframe->idr_nalu.magic_); + return; + } + if (eframe->idr_nalu.h264_compat_ != 5 && + eframe->idr_nalu.h264_compat_ != 1) { + // only return video for iframes or pframes + GMPLOG(GL_DEBUG, "Not a video frame: NAL type " + << (int)eframe->idr_nalu.h264_compat_); + return; + } + + int width = eframe->idr_nalu.width_; + int height = eframe->idr_nalu.height_; + int ystride = eframe->idr_nalu.width_; + int uvstride = eframe->idr_nalu.width_ / 2; + + GMPLOG(GL_DEBUG, "Video frame ready for display " + << width << "x" << height + << " timestamp=" << inputFrame->TimeStamp()); + + GMPVideoFrame* ftmp = nullptr; + + // Translate the image. + GMPErr err = host_->CreateFrame(kGMPI420VideoFrame, &ftmp); + if (err != GMPNoErr) { + GMPLOG(GL_ERROR, "Couldn't allocate empty I420 frame"); + return; + } + + GMPVideoi420Frame* frame = static_cast<GMPVideoi420Frame*>(ftmp); + err = frame->CreateEmptyFrame(width, height, ystride, uvstride, uvstride); + if (err != GMPNoErr) { + GMPLOG(GL_ERROR, "Couldn't make decoded frame"); + return; + } + + memset(frame->Buffer(kGMPYPlane), eframe->idr_nalu.y_, + frame->AllocatedSize(kGMPYPlane)); + memset(frame->Buffer(kGMPUPlane), eframe->idr_nalu.u_, + frame->AllocatedSize(kGMPUPlane)); + memset(frame->Buffer(kGMPVPlane), eframe->idr_nalu.v_, + frame->AllocatedSize(kGMPVPlane)); + + GMPLOG(GL_DEBUG, "Allocated size = " << frame->AllocatedSize(kGMPYPlane)); + frame->SetTimestamp(inputFrame->TimeStamp()); + frame->SetDuration(inputFrame->Duration()); + callback_->Decoded(frame); + } + + void Reset() override {} + + void Drain() override {} + + void DecodingComplete() override { delete this; } + + GMPVideoHost* host_; + GMPVideoDecoderCallback* callback_; +}; + +extern "C" { + +PUBLIC_FUNC GMPErr GMPInit(const GMPPlatformAPI* aPlatformAPI) { + return GMPNoErr; +} + +PUBLIC_FUNC GMPErr GMPGetAPI(const char* aApiName, void* aHostAPI, + void** aPluginApi) { + if (!strcmp(aApiName, GMP_API_VIDEO_DECODER)) { + *aPluginApi = new FakeVideoDecoder(static_cast<GMPVideoHost*>(aHostAPI)); + return GMPNoErr; + } + if (!strcmp(aApiName, GMP_API_VIDEO_ENCODER)) { + *aPluginApi = new FakeVideoEncoder(static_cast<GMPVideoHost*>(aHostAPI)); + return GMPNoErr; + } + return GMPGenericErr; +} + +PUBLIC_FUNC void GMPShutdown(void) {} + +} // extern "C" diff --git a/dom/media/gmp-plugin-openh264/moz.build b/dom/media/gmp-plugin-openh264/moz.build new file mode 100644 index 0000000000..e85a50673e --- /dev/null +++ b/dom/media/gmp-plugin-openh264/moz.build @@ -0,0 +1,25 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +# largely a copy of dom/media/gmp-fake/moz.build + +FINAL_TARGET = "dist/bin/gmp-fakeopenh264/1.0" + +FINAL_TARGET_FILES += [ + "fakeopenh264.info", +] + +SOURCES += [ + "gmp-fake-openh264.cpp", +] + +SharedLibrary("fakeopenh264") + +USE_STATIC_LIBS = True +NoVisibilityFlags() +# Don't use STL wrappers; this isn't Gecko code +DisableStlWrapping() +NO_PGO = True diff --git a/dom/media/gmp/CDMStorageIdProvider.cpp b/dom/media/gmp/CDMStorageIdProvider.cpp new file mode 100644 index 0000000000..52255879b3 --- /dev/null +++ b/dom/media/gmp/CDMStorageIdProvider.cpp @@ -0,0 +1,68 @@ +/* -*- 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 "CDMStorageIdProvider.h" +#include "GMPLog.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsICryptoHash.h" + +#ifdef SUPPORT_STORAGE_ID +# include "rlz/lib/machine_id.h" +#endif + +namespace mozilla { + +/*static*/ +nsCString CDMStorageIdProvider::ComputeStorageId(const nsCString& aOriginSalt) { +#ifndef SUPPORT_STORAGE_ID + return ""_ns; +#else + GMP_LOG_DEBUG("CDMStorageIdProvider::ComputeStorageId"); + + std::string machineId; + if (!rlz_lib::GetMachineId(&machineId)) { + GMP_LOG_DEBUG( + "CDMStorageIdProvider::ComputeStorageId: get machineId failed."); + return ""_ns; + } + + std::string originSalt(aOriginSalt.BeginReading(), aOriginSalt.Length()); + std::string input = + machineId + originSalt + CDMStorageIdProvider::kBrowserIdentifier; + nsCOMPtr<nsICryptoHash> hasher; + nsresult rv = NS_NewCryptoHash(nsICryptoHash::SHA256, getter_AddRefs(hasher)); + if (NS_WARN_IF(NS_FAILED(rv))) { + GMP_LOG_DEBUG( + "CDMStorageIdProvider::ComputeStorageId: failed to initialize " + "hash(0x%08" PRIx32 ")", + static_cast<uint32_t>(rv)); + return ""_ns; + } + + rv = hasher->Update(reinterpret_cast<const uint8_t*>(input.c_str()), + input.size()); + if (NS_WARN_IF(NS_FAILED(rv))) { + GMP_LOG_DEBUG( + "CDMStorageIdProvider::ComputeStorageId: failed to update " + "hash(0x%08" PRIx32 ")", + static_cast<uint32_t>(rv)); + return ""_ns; + } + + nsCString storageId; + rv = hasher->Finish(false, storageId); + if (NS_WARN_IF(NS_FAILED(rv))) { + GMP_LOG_DEBUG( + "CDMStorageIdProvider::ComputeStorageId: failed to get the final hash " + "result(0x%08" PRIx32 ")", + static_cast<uint32_t>(rv)); + return ""_ns; + } + return storageId; +#endif +} + +} // namespace mozilla diff --git a/dom/media/gmp/CDMStorageIdProvider.h b/dom/media/gmp/CDMStorageIdProvider.h new file mode 100644 index 0000000000..6904792607 --- /dev/null +++ b/dom/media/gmp/CDMStorageIdProvider.h @@ -0,0 +1,41 @@ +/* -*- 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/. */ + +#ifndef CDMStorageIdProvider_h_ +#define CDMStorageIdProvider_h_ + +/** + * CDM will try to request a latest version(0) of storage id. + * If the storage id computation algorithm changed, we should increase the + * kCurrentVersion. + */ + +#include <string> + +#include "nsString.h" + +namespace mozilla { + +class CDMStorageIdProvider { + static constexpr const char* kBrowserIdentifier = "mozilla_firefox_gecko"; + + public: + // Should increase the value when the storage id algorithm changed. + static constexpr int kCurrentVersion = 1; + static constexpr int kCDMRequestLatestVersion = 0; + + // Return empty string when + // 1. Call on unsupported storageid platform. + // 2. Failed to compute the storage id. + // This function only provide the storage id for kCurrentVersion=1. + // If you want to change the algorithm or output of storageid, + // you should keep the version 1 storage id and consider to provide + // higher version storage id in another function. + static nsCString ComputeStorageId(const nsCString& aOriginSalt); +}; + +} // namespace mozilla + +#endif // CDMStorageIdProvider_h_ diff --git a/dom/media/gmp/ChromiumCDMAdapter.cpp b/dom/media/gmp/ChromiumCDMAdapter.cpp new file mode 100644 index 0000000000..a5e866f83e --- /dev/null +++ b/dom/media/gmp/ChromiumCDMAdapter.cpp @@ -0,0 +1,307 @@ +/* -*- 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 "ChromiumCDMAdapter.h" + +#include <utility> + +#include "GMPLog.h" +#include "WidevineUtils.h" +#include "content_decryption_module.h" +#include "content_decryption_module_ext.h" +#include "gmp-api/gmp-entrypoints.h" +#include "gmp-api/gmp-video-codec.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/HelperMacros.h" +#include "mozilla/dom/KeySystemNames.h" + +#ifdef XP_WIN +# include "WinUtils.h" +# include "nsWindowsDllInterceptor.h" +# include <windows.h> +# include <strsafe.h> +# include <unordered_map> +# include <vector> +#else +# include <sys/types.h> +# include <sys/stat.h> +# include <unistd.h> +# include <fcntl.h> +#endif + +const GMPPlatformAPI* sPlatform = nullptr; + +namespace mozilla { + +#ifdef XP_WIN +static void InitializeHooks(); +#endif + +ChromiumCDMAdapter::ChromiumCDMAdapter( + nsTArray<std::pair<nsCString, nsCString>>&& aHostPathPairs) { +#ifdef XP_WIN + InitializeHooks(); +#endif + PopulateHostFiles(std::move(aHostPathPairs)); +} + +void ChromiumCDMAdapter::SetAdaptee(PRLibrary* aLib) { mLib = aLib; } + +void* ChromiumCdmHost(int aHostInterfaceVersion, void* aUserData) { + GMP_LOG_DEBUG("ChromiumCdmHostFunc(%d, %p)", aHostInterfaceVersion, + aUserData); + if (aHostInterfaceVersion != cdm::Host_10::kVersion) { + return nullptr; + } + return aUserData; +} + +#ifdef MOZILLA_OFFICIAL +static cdm::HostFile TakeToCDMHostFile(HostFileData& aHostFileData) { + return cdm::HostFile(aHostFileData.mBinary.Path().get(), + aHostFileData.mBinary.TakePlatformFile(), + aHostFileData.mSig.TakePlatformFile()); +} +#endif + +GMPErr ChromiumCDMAdapter::GMPInit(const GMPPlatformAPI* aPlatformAPI) { + GMP_LOG_DEBUG("ChromiumCDMAdapter::GMPInit"); + sPlatform = aPlatformAPI; + if (NS_WARN_IF(!mLib)) { + MOZ_CRASH("Missing library!"); + return GMPGenericErr; + } + +#ifdef MOZILLA_OFFICIAL + // Note: we must call the VerifyCdmHost_0 function if it's present before + // we call the initialize function. + auto verify = reinterpret_cast<decltype(::VerifyCdmHost_0)*>( + PR_FindFunctionSymbol(mLib, MOZ_STRINGIFY(VerifyCdmHost_0))); + if (verify) { + nsTArray<cdm::HostFile> files; + for (HostFileData& hostFile : mHostFiles) { + files.AppendElement(TakeToCDMHostFile(hostFile)); + } + bool result = verify(files.Elements(), files.Length()); + GMP_LOG_DEBUG("%s VerifyCdmHost_0 returned %d", __func__, result); + MOZ_DIAGNOSTIC_ASSERT(result, "Verification failed!"); + } +#endif + + auto init = reinterpret_cast<decltype(::INITIALIZE_CDM_MODULE)*>( + PR_FindFunctionSymbol(mLib, MOZ_STRINGIFY(INITIALIZE_CDM_MODULE))); + if (!init) { + MOZ_CRASH("Missing init method!"); + return GMPGenericErr; + } + + GMP_LOG_DEBUG(MOZ_STRINGIFY(INITIALIZE_CDM_MODULE) "()"); + init(); + + return GMPNoErr; +} + +GMPErr ChromiumCDMAdapter::GMPGetAPI(const char* aAPIName, void* aHostAPI, + void** aPluginAPI, + const nsACString& aKeySystem) { + MOZ_ASSERT( + aKeySystem.EqualsLiteral(kWidevineKeySystemName) || + aKeySystem.EqualsLiteral(kClearKeyKeySystemName) || + aKeySystem.EqualsLiteral(kClearKeyWithProtectionQueryKeySystemName) || + aKeySystem.EqualsLiteral("fake"), + "Should not get an unrecognized key system. Why didn't it get " + "blocked by MediaKeySystemAccess?"); + GMP_LOG_DEBUG("ChromiumCDMAdapter::GMPGetAPI(%s, 0x%p, 0x%p, %s) this=0x%p", + aAPIName, aHostAPI, aPluginAPI, + PromiseFlatCString(aKeySystem).get(), this); + bool isCdm10 = !strcmp(aAPIName, CHROMIUM_CDM_API); + + if (!isCdm10) { + MOZ_ASSERT_UNREACHABLE("We only support and expect cdm10!"); + GMP_LOG_DEBUG( + "ChromiumCDMAdapter::GMPGetAPI(%s, 0x%p, 0x%p) this=0x%p got " + "unsupported CDM version!", + aAPIName, aHostAPI, aPluginAPI, this); + return GMPGenericErr; + } + auto create = reinterpret_cast<decltype(::CreateCdmInstance)*>( + PR_FindFunctionSymbol(mLib, "CreateCdmInstance")); + if (!create) { + GMP_LOG_DEBUG( + "ChromiumCDMAdapter::GMPGetAPI(%s, 0x%p, 0x%p) this=0x%p " + "FAILED to find CreateCdmInstance", + aAPIName, aHostAPI, aPluginAPI, this); + return GMPGenericErr; + } + + const int version = cdm::ContentDecryptionModule_10::kVersion; + void* cdm = create(version, aKeySystem.BeginReading(), aKeySystem.Length(), + &ChromiumCdmHost, aHostAPI); + if (!cdm) { + GMP_LOG_DEBUG( + "ChromiumCDMAdapter::GMPGetAPI(%s, 0x%p, 0x%p) this=0x%p " + "FAILED to create cdm version %d", + aAPIName, aHostAPI, aPluginAPI, this, version); + return GMPGenericErr; + } + GMP_LOG_DEBUG("cdm: 0x%p, version: %d", cdm, version); + *aPluginAPI = cdm; + + return *aPluginAPI ? GMPNoErr : GMPNotImplementedErr; +} + +void ChromiumCDMAdapter::GMPShutdown() { + GMP_LOG_DEBUG("ChromiumCDMAdapter::GMPShutdown()"); + + decltype(::DeinitializeCdmModule)* deinit; + deinit = + (decltype(deinit))(PR_FindFunctionSymbol(mLib, "DeinitializeCdmModule")); + if (deinit) { + GMP_LOG_DEBUG("DeinitializeCdmModule()"); + deinit(); + } +} + +/* static */ +bool ChromiumCDMAdapter::Supports(int32_t aModuleVersion, + int32_t aInterfaceVersion, + int32_t aHostVersion) { + return aModuleVersion == CDM_MODULE_VERSION && + aInterfaceVersion == cdm::ContentDecryptionModule_10::kVersion && + aHostVersion == cdm::Host_10::kVersion; +} + +#ifdef XP_WIN + +static WindowsDllInterceptor sKernel32Intercept; + +typedef DWORD(WINAPI* QueryDosDeviceWFnPtr)(_In_opt_ LPCWSTR lpDeviceName, + _Out_ LPWSTR lpTargetPath, + _In_ DWORD ucchMax); + +static WindowsDllInterceptor::FuncHookType<QueryDosDeviceWFnPtr> + sOriginalQueryDosDeviceWFnPtr; + +static std::unordered_map<std::wstring, std::wstring>* sDeviceNames = nullptr; + +DWORD WINAPI QueryDosDeviceWHook(LPCWSTR lpDeviceName, LPWSTR lpTargetPath, + DWORD ucchMax) { + if (!sDeviceNames) { + return 0; + } + std::wstring name = std::wstring(lpDeviceName); + auto iter = sDeviceNames->find(name); + if (iter == sDeviceNames->end()) { + return 0; + } + const std::wstring& device = iter->second; + if (device.size() + 1 > ucchMax) { + return 0; + } + PodCopy(lpTargetPath, device.c_str(), device.size()); + lpTargetPath[device.size()] = 0; + GMP_LOG_DEBUG("QueryDosDeviceWHook %S -> %S", lpDeviceName, lpTargetPath); + return device.size(); +} + +static std::vector<std::wstring> GetDosDeviceNames() { + std::vector<std::wstring> v; + std::vector<wchar_t> buf; + buf.resize(1024); + DWORD rv = GetLogicalDriveStringsW(buf.size(), buf.data()); + if (rv == 0 || rv > buf.size()) { + return v; + } + + // buf will be a list of null terminated strings, with the last string + // being 0 length. + const wchar_t* p = buf.data(); + const wchar_t* end = &buf.back(); + size_t l; + while (p < end && (l = wcsnlen_s(p, end - p)) > 0) { + // The string is of the form "C:\". We need to strip off the trailing + // backslash. + std::wstring drive = std::wstring(p, p + l); + if (drive.back() == '\\') { + drive.erase(drive.end() - 1); + } + v.push_back(std::move(drive)); + p += l + 1; + } + return v; +} + +static std::wstring GetDeviceMapping(const std::wstring& aDosDeviceName) { + wchar_t buf[MAX_PATH] = {0}; + DWORD rv = QueryDosDeviceW(aDosDeviceName.c_str(), buf, MAX_PATH); + if (rv == 0) { + return std::wstring(L""); + } + return std::wstring(buf, buf + rv); +} + +static void InitializeHooks() { + static bool initialized = false; + if (initialized) { + return; + } + initialized = true; + sDeviceNames = new std::unordered_map<std::wstring, std::wstring>(); + for (const std::wstring& name : GetDosDeviceNames()) { + sDeviceNames->emplace(name, GetDeviceMapping(name)); + } + + sKernel32Intercept.Init("kernelbase.dll"); + sOriginalQueryDosDeviceWFnPtr.Set(sKernel32Intercept, "QueryDosDeviceW", + &QueryDosDeviceWHook); +} +#endif + +HostFile::HostFile(HostFile&& aOther) + : mPath(aOther.mPath), mFile(aOther.TakePlatformFile()) {} + +HostFile::~HostFile() { + if (mFile != cdm::kInvalidPlatformFile) { +#ifdef XP_WIN + CloseHandle(mFile); +#else + close(mFile); +#endif + mFile = cdm::kInvalidPlatformFile; + } +} + +#ifdef XP_WIN +HostFile::HostFile(const nsCString& aPath) + : mPath(NS_ConvertUTF8toUTF16(aPath)) { + HANDLE handle = + CreateFileW(mPath.get(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, 0, NULL); + mFile = (handle == INVALID_HANDLE_VALUE) ? cdm::kInvalidPlatformFile : handle; +} +#endif + +#ifndef XP_WIN +HostFile::HostFile(const nsCString& aPath) : mPath(aPath) { + // Note: open() returns -1 on failure; i.e. kInvalidPlatformFile. + mFile = open(aPath.get(), O_RDONLY); +} +#endif + +cdm::PlatformFile HostFile::TakePlatformFile() { + cdm::PlatformFile f = mFile; + mFile = cdm::kInvalidPlatformFile; + return f; +} + +void ChromiumCDMAdapter::PopulateHostFiles( + nsTArray<std::pair<nsCString, nsCString>>&& aHostPathPairs) { + for (const auto& pair : aHostPathPairs) { + mHostFiles.AppendElement(HostFileData(mozilla::HostFile(pair.first), + mozilla::HostFile(pair.second))); + } +} + +} // namespace mozilla diff --git a/dom/media/gmp/ChromiumCDMAdapter.h b/dom/media/gmp/ChromiumCDMAdapter.h new file mode 100644 index 0000000000..747345fb27 --- /dev/null +++ b/dom/media/gmp/ChromiumCDMAdapter.h @@ -0,0 +1,81 @@ +/* -*- 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/. */ + +#ifndef ChromiumAdapter_h_ +#define ChromiumAdapter_h_ + +#include "GMPLoader.h" +#include "prlink.h" +#include "GMPUtils.h" +#include "nsTArray.h" +#include "content_decryption_module_ext.h" +#include "nsString.h" + +#include <utility> + +struct GMPPlatformAPI; + +namespace mozilla { + +#if defined(XP_WIN) +typedef nsString HostFileString; +#else +typedef nsCString HostFileString; +#endif + +class HostFile { + public: + explicit HostFile(const nsCString& aPath); + HostFile(HostFile&& aOther); + ~HostFile(); + + const HostFileString& Path() const { return mPath; } + cdm::PlatformFile TakePlatformFile(); + + private: + const HostFileString mPath; + cdm::PlatformFile mFile = cdm::kInvalidPlatformFile; +}; + +struct HostFileData { + HostFileData(HostFile&& aBinary, HostFile&& aSig) + : mBinary(std::move(aBinary)), mSig(std::move(aSig)) {} + + HostFileData(HostFileData&& aOther) + : mBinary(std::move(aOther.mBinary)), mSig(std::move(aOther.mSig)) {} + + ~HostFileData() = default; + + HostFile mBinary; + HostFile mSig; +}; + +class ChromiumCDMAdapter : public gmp::GMPAdapter { + public: + explicit ChromiumCDMAdapter( + nsTArray<std::pair<nsCString, nsCString>>&& aHostPathPairs); + + void SetAdaptee(PRLibrary* aLib) override; + + // These are called in place of the corresponding GMP API functions. + GMPErr GMPInit(const GMPPlatformAPI* aPlatformAPI) override; + GMPErr GMPGetAPI(const char* aAPIName, void* aHostAPI, void** aPluginAPI, + const nsACString& aKeySystem) override; + void GMPShutdown() override; + + static bool Supports(int32_t aModuleVersion, int32_t aInterfaceVersion, + int32_t aHostVersion); + + private: + void PopulateHostFiles( + nsTArray<std::pair<nsCString, nsCString>>&& aHostFilePaths); + + PRLibrary* mLib = nullptr; + nsTArray<HostFileData> mHostFiles; +}; + +} // namespace mozilla + +#endif // ChromiumAdapter_h_ diff --git a/dom/media/gmp/ChromiumCDMCallback.h b/dom/media/gmp/ChromiumCDMCallback.h new file mode 100644 index 0000000000..3f62634933 --- /dev/null +++ b/dom/media/gmp/ChromiumCDMCallback.h @@ -0,0 +1,56 @@ +/* -*- 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/. */ + +#ifndef ChromiumCDMCallback_h_ +#define ChromiumCDMCallback_h_ + +#include "mozilla/CDMProxy.h" +#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus +#include "mozilla/dom/MediaKeyMessageEventBinding.h" // For MediaKeyMessageType +#include "mozilla/gmp/GMPTypes.h" // For CDMKeyInformation + +namespace mozilla { +class ErrorResult; +} + +class ChromiumCDMCallback { + public: + virtual ~ChromiumCDMCallback() = default; + + virtual void SetSessionId(uint32_t aPromiseId, + const nsCString& aSessionId) = 0; + + virtual void ResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccessful) = 0; + + virtual void ResolvePromiseWithKeyStatus(uint32_t aPromiseId, + uint32_t aKeyStatus) = 0; + + virtual void ResolvePromise(uint32_t aPromiseId) = 0; + + virtual void RejectPromise(uint32_t aPromiseId, mozilla::ErrorResult&& aError, + const nsCString& aErrorMessage) = 0; + + virtual void SessionMessage(const nsACString& aSessionId, + uint32_t aMessageType, + nsTArray<uint8_t>&& aMessage) = 0; + + virtual void SessionKeysChange( + const nsCString& aSessionId, + nsTArray<mozilla::gmp::CDMKeyInformation>&& aKeysInfo) = 0; + + virtual void ExpirationChange(const nsCString& aSessionId, + double aSecondsSinceEpoch) = 0; + + virtual void SessionClosed(const nsCString& aSessionId) = 0; + + virtual void QueryOutputProtectionStatus() = 0; + + virtual void Terminated() = 0; + + virtual void Shutdown() = 0; +}; + +#endif diff --git a/dom/media/gmp/ChromiumCDMCallbackProxy.cpp b/dom/media/gmp/ChromiumCDMCallbackProxy.cpp new file mode 100644 index 0000000000..481534c0e4 --- /dev/null +++ b/dom/media/gmp/ChromiumCDMCallbackProxy.cpp @@ -0,0 +1,159 @@ +/* -*- 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 "ChromiumCDMCallbackProxy.h" + +#include <type_traits> + +#include "ChromiumCDMProxy.h" +#include "content_decryption_module.h" + +namespace mozilla { + +template <class Func, class... Args> +void ChromiumCDMCallbackProxy::DispatchToMainThread(const char* const aLabel, + Func aFunc, + Args&&... aArgs) { + mMainThread->Dispatch( + // Use decay to ensure all the types are passed by value not by reference. + NewRunnableMethod<std::decay_t<Args>...>(aLabel, mProxy, aFunc, + std::forward<Args>(aArgs)...), + NS_DISPATCH_NORMAL); +} + +void ChromiumCDMCallbackProxy::SetSessionId(uint32_t aPromiseId, + const nsCString& aSessionId) { + DispatchToMainThread("ChromiumCDMProxy::OnSetSessionId", + &ChromiumCDMProxy::OnSetSessionId, aPromiseId, + NS_ConvertUTF8toUTF16(aSessionId)); +} + +void ChromiumCDMCallbackProxy::ResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccessful) { + DispatchToMainThread("ChromiumCDMProxy::OnResolveLoadSessionPromise", + &ChromiumCDMProxy::OnResolveLoadSessionPromise, + aPromiseId, aSuccessful); +} + +void ChromiumCDMCallbackProxy::ResolvePromise(uint32_t aPromiseId) { + DispatchToMainThread("ChromiumCDMProxy::ResolvePromise", + &ChromiumCDMProxy::ResolvePromise, aPromiseId); +} + +void ChromiumCDMCallbackProxy::RejectPromise(uint32_t aPromiseId, + ErrorResult&& aException, + const nsCString& aErrorMessage) { + // Use CopyableErrorResult to store our exception in the runnable, + // because ErrorResult is not OK to move across threads. + DispatchToMainThread<decltype(&ChromiumCDMProxy::RejectPromiseOnMainThread), + int32_t, StoreCopyPassByRRef<CopyableErrorResult>, + const nsCString&>( + "ChromiumCDMProxy::RejectPromise", + &ChromiumCDMProxy::RejectPromiseOnMainThread, aPromiseId, + std::move(aException), aErrorMessage); +} + +static dom::MediaKeyMessageType ToDOMMessageType(uint32_t aMessageType) { + switch (static_cast<cdm::MessageType>(aMessageType)) { + case cdm::kLicenseRequest: + return dom::MediaKeyMessageType::License_request; + case cdm::kLicenseRenewal: + return dom::MediaKeyMessageType::License_renewal; + case cdm::kLicenseRelease: + return dom::MediaKeyMessageType::License_release; + case cdm::kIndividualizationRequest: + return dom::MediaKeyMessageType::Individualization_request; + } + MOZ_ASSERT_UNREACHABLE("Invalid cdm::MessageType enum value."); + return dom::MediaKeyMessageType::License_request; +} + +void ChromiumCDMCallbackProxy::SessionMessage(const nsACString& aSessionId, + uint32_t aMessageType, + nsTArray<uint8_t>&& aMessage) { + DispatchToMainThread("ChromiumCDMProxy::OnSessionMessage", + &ChromiumCDMProxy::OnSessionMessage, + NS_ConvertUTF8toUTF16(aSessionId), + ToDOMMessageType(aMessageType), std::move(aMessage)); +} + +static dom::MediaKeyStatus ToDOMMediaKeyStatus(uint32_t aStatus) { + switch (static_cast<cdm::KeyStatus>(aStatus)) { + case cdm::kUsable: + return dom::MediaKeyStatus::Usable; + case cdm::kInternalError: + return dom::MediaKeyStatus::Internal_error; + case cdm::kExpired: + return dom::MediaKeyStatus::Expired; + case cdm::kOutputRestricted: + return dom::MediaKeyStatus::Output_restricted; + case cdm::kOutputDownscaled: + return dom::MediaKeyStatus::Output_downscaled; + case cdm::kStatusPending: + return dom::MediaKeyStatus::Status_pending; + case cdm::kReleased: + return dom::MediaKeyStatus::Released; + } + MOZ_ASSERT_UNREACHABLE("Invalid cdm::KeyStatus enum value."); + return dom::MediaKeyStatus::Internal_error; +} + +void ChromiumCDMCallbackProxy::ResolvePromiseWithKeyStatus( + uint32_t aPromiseId, uint32_t aKeyStatus) { + DispatchToMainThread("ChromiumCDMProxy::OnResolvePromiseWithKeyStatus", + &ChromiumCDMProxy::OnResolvePromiseWithKeyStatus, + aPromiseId, ToDOMMediaKeyStatus(aKeyStatus)); +} + +void ChromiumCDMCallbackProxy::SessionKeysChange( + const nsCString& aSessionId, + nsTArray<mozilla::gmp::CDMKeyInformation>&& aKeysInfo) { + bool keyStatusesChange = false; + { + auto caps = mProxy->Capabilites().Lock(); + for (const auto& keyInfo : aKeysInfo) { + keyStatusesChange |= caps->SetKeyStatus( + keyInfo.mKeyId(), NS_ConvertUTF8toUTF16(aSessionId), + dom::Optional<dom::MediaKeyStatus>( + ToDOMMediaKeyStatus(keyInfo.mStatus()))); + } + } + if (keyStatusesChange) { + DispatchToMainThread("ChromiumCDMProxy::OnKeyStatusesChange", + &ChromiumCDMProxy::OnKeyStatusesChange, + NS_ConvertUTF8toUTF16(aSessionId)); + } +} + +void ChromiumCDMCallbackProxy::ExpirationChange(const nsCString& aSessionId, + double aSecondsSinceEpoch) { + DispatchToMainThread("ChromiumCDMProxy::OnExpirationChange", + &ChromiumCDMProxy::OnExpirationChange, + NS_ConvertUTF8toUTF16(aSessionId), + UnixTime(aSecondsSinceEpoch * 1000)); +} + +void ChromiumCDMCallbackProxy::SessionClosed(const nsCString& aSessionId) { + DispatchToMainThread("ChromiumCDMProxy::OnSessionClosed", + &ChromiumCDMProxy::OnSessionClosed, + NS_ConvertUTF8toUTF16(aSessionId)); +} + +void ChromiumCDMCallbackProxy::QueryOutputProtectionStatus() { + DispatchToMainThread("ChromiumCDMProxy::QueryOutputProtectionStatus", + &ChromiumCDMProxy::QueryOutputProtectionStatus); +} + +void ChromiumCDMCallbackProxy::Terminated() { + DispatchToMainThread("ChromiumCDMProxy::Terminated", + &ChromiumCDMProxy::Terminated); +} + +void ChromiumCDMCallbackProxy::Shutdown() { + DispatchToMainThread("ChromiumCDMProxy::Shutdown", + &ChromiumCDMProxy::Shutdown); +} + +} // namespace mozilla diff --git a/dom/media/gmp/ChromiumCDMCallbackProxy.h b/dom/media/gmp/ChromiumCDMCallbackProxy.h new file mode 100644 index 0000000000..5b240f9102 --- /dev/null +++ b/dom/media/gmp/ChromiumCDMCallbackProxy.h @@ -0,0 +1,62 @@ +/* -*- 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/. */ + +#ifndef ChromiumCDMCallbackProxy_h_ +#define ChromiumCDMCallbackProxy_h_ + +#include "ChromiumCDMCallback.h" +#include "ChromiumCDMProxy.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +class ChromiumCDMCallbackProxy : public ChromiumCDMCallback { + public: + ChromiumCDMCallbackProxy(ChromiumCDMProxy* aProxy, + nsIEventTarget* aMainThread) + : mProxy(aProxy), mMainThread(aMainThread) {} + + void SetSessionId(uint32_t aPromiseId, const nsCString& aSessionId) override; + + void ResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccessful) override; + + void ResolvePromiseWithKeyStatus(uint32_t aPromiseId, + uint32_t aKeyStatus) override; + + void ResolvePromise(uint32_t aPromiseId) override; + + void RejectPromise(uint32_t aPromiseId, ErrorResult&& aException, + const nsCString& aErrorMessage) override; + + void SessionMessage(const nsACString& aSessionId, uint32_t aMessageType, + nsTArray<uint8_t>&& aMessage) override; + + void SessionKeysChange( + const nsCString& aSessionId, + nsTArray<mozilla::gmp::CDMKeyInformation>&& aKeysInfo) override; + + void ExpirationChange(const nsCString& aSessionId, + double aSecondsSinceEpoch) override; + + void SessionClosed(const nsCString& aSessionId) override; + + void QueryOutputProtectionStatus() override; + + void Terminated() override; + + void Shutdown() override; + + private: + template <class Func, class... Args> + void DispatchToMainThread(const char* const aLabel, Func aFunc, + Args&&... aArgs); + // Warning: Weak ref. + ChromiumCDMProxy* mProxy; + const nsCOMPtr<nsIEventTarget> mMainThread; +}; + +} // namespace mozilla +#endif diff --git a/dom/media/gmp/ChromiumCDMChild.cpp b/dom/media/gmp/ChromiumCDMChild.cpp new file mode 100644 index 0000000000..4592f2e291 --- /dev/null +++ b/dom/media/gmp/ChromiumCDMChild.cpp @@ -0,0 +1,866 @@ +/* -*- 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 "ChromiumCDMChild.h" +#include "GMPContentChild.h" +#include "WidevineUtils.h" +#include "WidevineFileIO.h" +#include "WidevineVideoFrame.h" +#include "GMPLog.h" +#include "GMPPlatform.h" +#include "mozilla/Unused.h" +#include "nsPrintfCString.h" +#include "base/time.h" +#include "GMPUtils.h" +#include "mozilla/ScopeExit.h" +#include "CDMStorageIdProvider.h" +#include "nsReadableUtils.h" + +#include <type_traits> + +namespace mozilla::gmp { + +ChromiumCDMChild::ChromiumCDMChild(GMPContentChild* aPlugin) + : mPlugin(aPlugin) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild:: ctor this=%p", this); +} + +void ChromiumCDMChild::Init(cdm::ContentDecryptionModule_10* aCDM, + const nsACString& aStorageId) { + MOZ_ASSERT(IsOnMessageLoopThread()); + mCDM = aCDM; + MOZ_ASSERT(mCDM); + mStorageId = aStorageId; +} + +void ChromiumCDMChild::TimerExpired(void* aContext) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::TimerExpired(context=0x%p)", aContext); + if (mCDM) { + mCDM->TimerExpired(aContext); + } +} + +class CDMShmemBuffer : public CDMBuffer { + public: + CDMShmemBuffer(ChromiumCDMChild* aProtocol, ipc::Shmem aShmem) + : mProtocol(aProtocol), mSize(aShmem.Size<uint8_t>()), mShmem(aShmem) { + GMP_LOG_DEBUG("CDMShmemBuffer(size=%" PRIu32 ") created", Size()); + // Note: Chrome initializes the size of a buffer to it capacity. We do the + // same. + } + + CDMShmemBuffer(ChromiumCDMChild* aProtocol, ipc::Shmem aShmem, + WidevineBuffer* aLocalBuffer) + : CDMShmemBuffer(aProtocol, aShmem) { + MOZ_ASSERT(aLocalBuffer->Size() == Size()); + memcpy(Data(), aLocalBuffer->Data(), + std::min<uint32_t>(aLocalBuffer->Size(), Size())); + } + + ~CDMShmemBuffer() override { + GMP_LOG_DEBUG("CDMShmemBuffer(size=%" PRIu32 ") destructed writable=%d", + Size(), mShmem.IsWritable()); + if (mShmem.IsWritable()) { + // The shmem wasn't extracted to send its data back up to the parent + // process, so we can reuse the shmem. + mProtocol->GiveBuffer(std::move(mShmem)); + } + } + + void Destroy() override { + GMP_LOG_DEBUG("CDMShmemBuffer::Destroy(size=%" PRIu32 ")", Size()); + delete this; + } + uint32_t Capacity() const override { return mShmem.Size<uint8_t>(); } + + uint8_t* Data() override { return mShmem.get<uint8_t>(); } + + void SetSize(uint32_t aSize) override { + MOZ_ASSERT(aSize <= Capacity()); + // Note: We can't use the shmem's size member after ExtractShmem(), + // has been called, so we track the size exlicitly so that we can use + // Size() in logging after we've called ExtractShmem(). + GMP_LOG_DEBUG("CDMShmemBuffer::SetSize(size=%" PRIu32 ")", Size()); + mSize = aSize; + } + + uint32_t Size() const override { return mSize; } + + ipc::Shmem ExtractShmem() { + ipc::Shmem shmem = mShmem; + mShmem = ipc::Shmem(); + return shmem; + } + + CDMShmemBuffer* AsShmemBuffer() override { return this; } + + private: + RefPtr<ChromiumCDMChild> mProtocol; + uint32_t mSize; + mozilla::ipc::Shmem mShmem; + CDMShmemBuffer(const CDMShmemBuffer&); + void operator=(const CDMShmemBuffer&); +}; + +static auto ToString(const nsTArray<ipc::Shmem>& aBuffers) { + return StringJoin(","_ns, aBuffers, [](auto& s, const ipc::Shmem& shmem) { + s.AppendInt(static_cast<uint32_t>(shmem.Size<uint8_t>())); + }); +} + +cdm::Buffer* ChromiumCDMChild::Allocate(uint32_t aCapacity) { + GMP_LOG_DEBUG("ChromiumCDMChild::Allocate(capacity=%" PRIu32 + ") bufferSizes={%s}", + aCapacity, ToString(mBuffers).get()); + MOZ_ASSERT(IsOnMessageLoopThread()); + + if (mBuffers.IsEmpty()) { + Unused << SendIncreaseShmemPoolSize(); + } + + // Find the shmem with the least amount of wasted space if we were to + // select it for this sized allocation. We need to do this because shmems + // for decrypted audio as well as video frames are both stored in this + // list, and we don't want to use the video frame shmems for audio samples. + const size_t invalid = std::numeric_limits<size_t>::max(); + size_t best = invalid; + auto wastedSpace = [this, aCapacity](size_t index) { + return mBuffers[index].Size<uint8_t>() - aCapacity; + }; + for (size_t i = 0; i < mBuffers.Length(); i++) { + if (mBuffers[i].Size<uint8_t>() >= aCapacity && + (best == invalid || wastedSpace(i) < wastedSpace(best))) { + best = i; + } + } + if (best == invalid) { + // The parent process should have bestowed upon us a shmem of appropriate + // size, but did not! Do a "dive and catch", and create an non-shared + // memory buffer. The parent will detect this and send us an extra shmem + // so future frames can be in shmems, i.e. returned on the fast path. + return new WidevineBuffer(aCapacity); + } + ipc::Shmem shmem = mBuffers[best]; + mBuffers.RemoveElementAt(best); + return new CDMShmemBuffer(this, shmem); +} + +void ChromiumCDMChild::SetTimer(int64_t aDelayMs, void* aContext) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::SetTimer(delay=%" PRId64 ", context=0x%p)", + aDelayMs, aContext); + RefPtr<ChromiumCDMChild> self(this); + SetTimerOnMainThread( + NewGMPTask([self, aContext]() { self->TimerExpired(aContext); }), + aDelayMs); +} + +cdm::Time ChromiumCDMChild::GetCurrentWallTime() { + return base::Time::Now().ToDoubleT(); +} + +template <typename MethodType, typename... ParamType> +void ChromiumCDMChild::CallMethod(MethodType aMethod, ParamType&&... aParams) { + MOZ_ASSERT(IsOnMessageLoopThread()); + // Avoid calling member function after destroy. + if (!mDestroyed) { + Unused << (this->*aMethod)(std::forward<ParamType>(aParams)...); + } +} + +template <typename MethodType, typename... ParamType> +void ChromiumCDMChild::CallOnMessageLoopThread(const char* const aName, + MethodType aMethod, + ParamType&&... aParams) { + if (NS_WARN_IF(!mPlugin)) { + return; + } + + if (IsOnMessageLoopThread()) { + CallMethod(aMethod, std::forward<ParamType>(aParams)...); + } else { + auto m = &ChromiumCDMChild::CallMethod< + decltype(aMethod), const std::remove_reference_t<ParamType>&...>; + RefPtr<mozilla::Runnable> t = + NewRunnableMethod<decltype(aMethod), + const std::remove_reference_t<ParamType>...>( + aName, this, m, aMethod, std::forward<ParamType>(aParams)...); + mPlugin->GMPMessageLoop()->PostTask(t.forget()); + } +} + +void ChromiumCDMChild::OnResolveKeyStatusPromise(uint32_t aPromiseId, + cdm::KeyStatus aKeyStatus) { + GMP_LOG_DEBUG("ChromiumCDMChild::OnResolveKeyStatusPromise(pid=%" PRIu32 + "keystatus=%d)", + aPromiseId, aKeyStatus); + CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnResolveKeyStatusPromise", + &ChromiumCDMChild::SendOnResolvePromiseWithKeyStatus, + aPromiseId, static_cast<uint32_t>(aKeyStatus)); +} + +bool ChromiumCDMChild::OnResolveNewSessionPromiseInternal( + uint32_t aPromiseId, const nsACString& aSessionId) { + MOZ_ASSERT(IsOnMessageLoopThread()); + if (mLoadSessionPromiseIds.Contains(aPromiseId)) { + // As laid out in the Chromium CDM API, if the CDM fails to load + // a session it calls OnResolveNewSessionPromise with nullptr as the + // sessionId. We can safely assume this means that we have failed to load a + // session as the other methods specify calling 'OnRejectPromise' when they + // fail. + bool loadSuccessful = !aSessionId.IsEmpty(); + GMP_LOG_DEBUG( + "ChromiumCDMChild::OnResolveNewSessionPromise(pid=%u, sid=%s) " + "resolving %s load session ", + aPromiseId, PromiseFlatCString(aSessionId).get(), + (loadSuccessful ? "successful" : "failed")); + mLoadSessionPromiseIds.RemoveElement(aPromiseId); + return SendResolveLoadSessionPromise(aPromiseId, loadSuccessful); + } + + return SendOnResolveNewSessionPromise(aPromiseId, aSessionId); +} +void ChromiumCDMChild::OnResolveNewSessionPromise(uint32_t aPromiseId, + const char* aSessionId, + uint32_t aSessionIdSize) { + GMP_LOG_DEBUG("ChromiumCDMChild::OnResolveNewSessionPromise(pid=%" PRIu32 + ", sid=%s)", + aPromiseId, aSessionId); + CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnResolveNewSessionPromise", + &ChromiumCDMChild::OnResolveNewSessionPromiseInternal, + aPromiseId, nsCString(aSessionId, aSessionIdSize)); +} + +void ChromiumCDMChild::OnResolvePromise(uint32_t aPromiseId) { + GMP_LOG_DEBUG("ChromiumCDMChild::OnResolvePromise(pid=%" PRIu32 ")", + aPromiseId); + CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnResolvePromise", + &ChromiumCDMChild::SendOnResolvePromise, aPromiseId); +} + +void ChromiumCDMChild::OnRejectPromise(uint32_t aPromiseId, + cdm::Exception aException, + uint32_t aSystemCode, + const char* aErrorMessage, + uint32_t aErrorMessageSize) { + GMP_LOG_DEBUG("ChromiumCDMChild::OnRejectPromise(pid=%" PRIu32 + ", err=%" PRIu32 " code=%" PRIu32 ", msg='%s')", + aPromiseId, aException, aSystemCode, aErrorMessage); + CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnRejectPromise", + &ChromiumCDMChild::SendOnRejectPromise, aPromiseId, + static_cast<uint32_t>(aException), aSystemCode, + nsCString(aErrorMessage, aErrorMessageSize)); +} + +void ChromiumCDMChild::OnSessionMessage(const char* aSessionId, + uint32_t aSessionIdSize, + cdm::MessageType aMessageType, + const char* aMessage, + uint32_t aMessageSize) { + GMP_LOG_DEBUG("ChromiumCDMChild::OnSessionMessage(sid=%s, type=%" PRIu32 + " size=%" PRIu32 ")", + aSessionId, aMessageType, aMessageSize); + CopyableTArray<uint8_t> message; + message.AppendElements(aMessage, aMessageSize); + CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnSessionMessage", + &ChromiumCDMChild::SendOnSessionMessage, + nsCString(aSessionId, aSessionIdSize), + static_cast<uint32_t>(aMessageType), message); +} + +static auto ToString(const cdm::KeyInformation* aKeysInfo, + uint32_t aKeysInfoCount) { + return StringJoin(","_ns, Span{aKeysInfo, aKeysInfoCount}, + [](auto& str, const cdm::KeyInformation& key) { + str.Append(ToHexString(key.key_id, key.key_id_size)); + str.AppendLiteral("="); + str.AppendInt(key.status); + }); +} + +void ChromiumCDMChild::OnSessionKeysChange(const char* aSessionId, + uint32_t aSessionIdSize, + bool aHasAdditionalUsableKey, + const cdm::KeyInformation* aKeysInfo, + uint32_t aKeysInfoCount) { + GMP_LOG_DEBUG("ChromiumCDMChild::OnSessionKeysChange(sid=%s) keys={%s}", + aSessionId, ToString(aKeysInfo, aKeysInfoCount).get()); + + CopyableTArray<CDMKeyInformation> keys; + keys.SetCapacity(aKeysInfoCount); + for (uint32_t i = 0; i < aKeysInfoCount; i++) { + const cdm::KeyInformation& key = aKeysInfo[i]; + nsTArray<uint8_t> kid; + kid.AppendElements(key.key_id, key.key_id_size); + keys.AppendElement(CDMKeyInformation(kid, key.status, key.system_code)); + } + CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnSessionMessage", + &ChromiumCDMChild::SendOnSessionKeysChange, + nsCString(aSessionId, aSessionIdSize), keys); +} + +void ChromiumCDMChild::OnExpirationChange(const char* aSessionId, + uint32_t aSessionIdSize, + cdm::Time aNewExpiryTime) { + GMP_LOG_DEBUG("ChromiumCDMChild::OnExpirationChange(sid=%s, time=%lf)", + aSessionId, aNewExpiryTime); + CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnExpirationChange", + &ChromiumCDMChild::SendOnExpirationChange, + nsCString(aSessionId, aSessionIdSize), + aNewExpiryTime); +} + +void ChromiumCDMChild::OnSessionClosed(const char* aSessionId, + uint32_t aSessionIdSize) { + GMP_LOG_DEBUG("ChromiumCDMChild::OnSessionClosed(sid=%s)", aSessionId); + CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnSessionClosed", + &ChromiumCDMChild::SendOnSessionClosed, + nsCString(aSessionId, aSessionIdSize)); +} + +void ChromiumCDMChild::QueryOutputProtectionStatus() { + GMP_LOG_DEBUG("ChromiumCDMChild::QueryOutputProtectionStatus()"); + // We'll handle the response in `CompleteQueryOutputProtectionStatus`. + CallOnMessageLoopThread("gmp::ChromiumCDMChild::QueryOutputProtectionStatus", + &ChromiumCDMChild::SendOnQueryOutputProtectionStatus); +} + +void ChromiumCDMChild::OnInitialized(bool aSuccess) { + MOZ_ASSERT(!mInitPromise.IsEmpty(), + "mInitPromise should exist during init callback!"); + if (!aSuccess) { + mInitPromise.RejectIfExists(NS_ERROR_FAILURE, __func__); + } + mInitPromise.ResolveIfExists(true, __func__); +} + +cdm::FileIO* ChromiumCDMChild::CreateFileIO(cdm::FileIOClient* aClient) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::CreateFileIO()"); + if (!mPersistentStateAllowed) { + return nullptr; + } + return new WidevineFileIO(aClient); +} + +void ChromiumCDMChild::RequestStorageId(uint32_t aVersion) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::RequestStorageId() aVersion = %u", aVersion); + // aVersion >= 0x80000000 are reserved. + if (aVersion >= 0x80000000) { + mCDM->OnStorageId(aVersion, nullptr, 0); + return; + } + if (aVersion > CDMStorageIdProvider::kCurrentVersion) { + mCDM->OnStorageId(aVersion, nullptr, 0); + return; + } + + mCDM->OnStorageId(CDMStorageIdProvider::kCurrentVersion, + !mStorageId.IsEmpty() + ? reinterpret_cast<const uint8_t*>(mStorageId.get()) + : nullptr, + mStorageId.Length()); +} + +ChromiumCDMChild::~ChromiumCDMChild() { + GMP_LOG_DEBUG("ChromiumCDMChild:: dtor this=%p", this); +} + +bool ChromiumCDMChild::IsOnMessageLoopThread() { + return mPlugin && mPlugin->GMPMessageLoop() == MessageLoop::current(); +} + +void ChromiumCDMChild::ActorDestroy(ActorDestroyReason aReason) { + mPlugin = nullptr; +} + +void ChromiumCDMChild::PurgeShmems() { + for (ipc::Shmem& shmem : mBuffers) { + DeallocShmem(shmem); + } + mBuffers.Clear(); +} + +ipc::IPCResult ChromiumCDMChild::RecvPurgeShmems() { + PurgeShmems(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvInit( + const bool& aAllowDistinctiveIdentifier, const bool& aAllowPersistentState, + InitResolver&& aResolver) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG( + "ChromiumCDMChild::RecvInit(distinctiveId=%s, persistentState=%s)", + aAllowDistinctiveIdentifier ? "true" : "false", + aAllowPersistentState ? "true" : "false"); + mPersistentStateAllowed = aAllowPersistentState; + + RefPtr<ChromiumCDMChild::InitPromise> promise = mInitPromise.Ensure(__func__); + promise->Then( + mPlugin->GMPMessageLoop()->SerialEventTarget(), __func__, + [aResolver](bool /* unused */) { aResolver(true); }, + [aResolver](nsresult rv) { + GMP_LOG_DEBUG( + "ChromiumCDMChild::RecvInit() init promise rejected with " + "rv=%" PRIu32, + static_cast<uint32_t>(rv)); + aResolver(false); + }); + + if (mCDM) { + // Once the CDM is initialized we expect it to resolve mInitPromise via + // ChromiumCDMChild::OnInitialized + mCDM->Initialize(aAllowDistinctiveIdentifier, aAllowPersistentState, + // We do not yet support hardware secure codecs + false); + } else { + GMP_LOG_DEBUG( + "ChromiumCDMChild::RecvInit() mCDM not set! Is GMP shutting down?"); + mInitPromise.RejectIfExists(NS_ERROR_FAILURE, __func__); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvSetServerCertificate( + const uint32_t& aPromiseId, nsTArray<uint8_t>&& aServerCert) + +{ + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvSetServerCertificate() certlen=%zu", + aServerCert.Length()); + if (mCDM) { + mCDM->SetServerCertificate(aPromiseId, aServerCert.Elements(), + aServerCert.Length()); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvCreateSessionAndGenerateRequest( + const uint32_t& aPromiseId, const uint32_t& aSessionType, + const uint32_t& aInitDataType, nsTArray<uint8_t>&& aInitData) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG( + "ChromiumCDMChild::RecvCreateSessionAndGenerateRequest(" + "pid=%" PRIu32 ", sessionType=%" PRIu32 ", initDataType=%" PRIu32 + ") initDataLen=%zu", + aPromiseId, aSessionType, aInitDataType, aInitData.Length()); + MOZ_ASSERT(aSessionType <= cdm::SessionType::kPersistentUsageRecord); + MOZ_ASSERT(aInitDataType <= cdm::InitDataType::kWebM); + if (mCDM) { + mCDM->CreateSessionAndGenerateRequest( + aPromiseId, static_cast<cdm::SessionType>(aSessionType), + static_cast<cdm::InitDataType>(aInitDataType), aInitData.Elements(), + aInitData.Length()); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvLoadSession( + const uint32_t& aPromiseId, const uint32_t& aSessionType, + const nsACString& aSessionId) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG( + "ChromiumCDMChild::RecvLoadSession(pid=%u, type=%u, sessionId=%s)", + aPromiseId, aSessionType, PromiseFlatCString(aSessionId).get()); + if (mCDM) { + mLoadSessionPromiseIds.AppendElement(aPromiseId); + mCDM->LoadSession(aPromiseId, static_cast<cdm::SessionType>(aSessionType), + aSessionId.BeginReading(), aSessionId.Length()); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvUpdateSession( + const uint32_t& aPromiseId, const nsACString& aSessionId, + nsTArray<uint8_t>&& aResponse) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvUpdateSession(pid=%" PRIu32 + ", sid=%s) responseLen=%zu", + aPromiseId, PromiseFlatCString(aSessionId).get(), + aResponse.Length()); + if (mCDM) { + mCDM->UpdateSession(aPromiseId, aSessionId.BeginReading(), + aSessionId.Length(), aResponse.Elements(), + aResponse.Length()); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvCloseSession( + const uint32_t& aPromiseId, const nsACString& aSessionId) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvCloseSession(pid=%" PRIu32 ", sid=%s)", + aPromiseId, PromiseFlatCString(aSessionId).get()); + if (mCDM) { + mCDM->CloseSession(aPromiseId, aSessionId.BeginReading(), + aSessionId.Length()); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvRemoveSession( + const uint32_t& aPromiseId, const nsACString& aSessionId) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvRemoveSession(pid=%" PRIu32 ", sid=%s)", + aPromiseId, PromiseFlatCString(aSessionId).get()); + if (mCDM) { + mCDM->RemoveSession(aPromiseId, aSessionId.BeginReading(), + aSessionId.Length()); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult +ChromiumCDMChild::RecvCompleteQueryOutputProtectionStatus( + const bool& aSuccess, const uint32_t& aLinkMask, + const uint32_t& aProtectionMask) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG( + "ChromiumCDMChild::RecvCompleteQueryOutputProtectionStatus(aSuccess=%s, " + "aLinkMask=%" PRIu32 ", aProtectionMask=%" PRIu32 ")", + aSuccess ? "true" : "false", aLinkMask, aProtectionMask); + + if (mCDM) { + cdm::QueryResult queryResult = aSuccess ? cdm::QueryResult::kQuerySucceeded + : cdm::QueryResult::kQueryFailed; + mCDM->OnQueryOutputProtectionStatus(queryResult, aLinkMask, + aProtectionMask); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvGetStatusForPolicy( + const uint32_t& aPromiseId, const cdm::HdcpVersion& aMinHdcpVersion) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvGetStatusForPolicy(pid=%" PRIu32 + ", MinHdcpVersion=%" PRIu32 ")", + aPromiseId, static_cast<uint32_t>(aMinHdcpVersion)); + if (mCDM) { + cdm::Policy policy; + policy.min_hdcp_version = aMinHdcpVersion; + mCDM->GetStatusForPolicy(aPromiseId, policy); + } + return IPC_OK(); +} + +static void InitInputBuffer(const CDMInputBuffer& aBuffer, + nsTArray<cdm::SubsampleEntry>& aSubSamples, + cdm::InputBuffer_2& aInputBuffer) { + aInputBuffer.data = aBuffer.mData().get<uint8_t>(); + aInputBuffer.data_size = aBuffer.mData().Size<uint8_t>(); + + if (aBuffer.mEncryptionScheme() != cdm::EncryptionScheme::kUnencrypted) { + MOZ_ASSERT(aBuffer.mEncryptionScheme() == cdm::EncryptionScheme::kCenc || + aBuffer.mEncryptionScheme() == cdm::EncryptionScheme::kCbcs); + aInputBuffer.key_id = aBuffer.mKeyId().Elements(); + aInputBuffer.key_id_size = aBuffer.mKeyId().Length(); + + aInputBuffer.iv = aBuffer.mIV().Elements(); + aInputBuffer.iv_size = aBuffer.mIV().Length(); + + aSubSamples.SetCapacity(aBuffer.mClearBytes().Length()); + for (size_t i = 0; i < aBuffer.mCipherBytes().Length(); i++) { + aSubSamples.AppendElement(cdm::SubsampleEntry{aBuffer.mClearBytes()[i], + aBuffer.mCipherBytes()[i]}); + } + aInputBuffer.subsamples = aSubSamples.Elements(); + aInputBuffer.num_subsamples = aSubSamples.Length(); + aInputBuffer.encryption_scheme = aBuffer.mEncryptionScheme(); + } + aInputBuffer.pattern.crypt_byte_block = aBuffer.mCryptByteBlock(); + aInputBuffer.pattern.skip_byte_block = aBuffer.mSkipByteBlock(); + aInputBuffer.timestamp = aBuffer.mTimestamp(); +} + +bool ChromiumCDMChild::HasShmemOfSize(size_t aSize) const { + for (const ipc::Shmem& shmem : mBuffers) { + if (shmem.Size<uint8_t>() == aSize) { + return true; + } + } + return false; +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvDecrypt( + const uint32_t& aId, const CDMInputBuffer& aBuffer) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecrypt()"); + + // Parent should have already gifted us a shmem to use as output. + size_t outputShmemSize = aBuffer.mData().Size<uint8_t>(); + MOZ_ASSERT(HasShmemOfSize(outputShmemSize)); + + // Ensure we deallocate the shmem used to send input. + RefPtr<ChromiumCDMChild> self = this; + auto autoDeallocateInputShmem = + MakeScopeExit([&, self] { self->DeallocShmem(aBuffer.mData()); }); + + // On failure, we need to ensure that the shmem that the parent sent + // for the CDM to use to return output back to the parent is deallocated. + // Otherwise, it will leak. + auto autoDeallocateOutputShmem = MakeScopeExit([self, outputShmemSize] { + self->mBuffers.RemoveElementsBy( + [outputShmemSize, self](ipc::Shmem& aShmem) { + if (aShmem.Size<uint8_t>() != outputShmemSize) { + return false; + } + self->DeallocShmem(aShmem); + return true; + }); + }); + + if (!mCDM) { + GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecrypt() no CDM"); + Unused << SendDecryptFailed(aId, cdm::kDecryptError); + return IPC_OK(); + } + if (aBuffer.mClearBytes().Length() != aBuffer.mCipherBytes().Length()) { + GMP_LOG_DEBUG( + "ChromiumCDMChild::RecvDecrypt() clear/cipher bytes length doesn't " + "match"); + Unused << SendDecryptFailed(aId, cdm::kDecryptError); + return IPC_OK(); + } + + cdm::InputBuffer_2 input = {}; + nsTArray<cdm::SubsampleEntry> subsamples; + InitInputBuffer(aBuffer, subsamples, input); + + WidevineDecryptedBlock output; + cdm::Status status = mCDM->Decrypt(input, &output); + + // CDM should have allocated a cdm::Buffer for output. + CDMShmemBuffer* buffer = + output.DecryptedBuffer() + ? static_cast<CDMShmemBuffer*>(output.DecryptedBuffer()) + : nullptr; + MOZ_ASSERT_IF(buffer, buffer->AsShmemBuffer()); + if (status != cdm::kSuccess || !buffer) { + Unused << SendDecryptFailed(aId, status); + return IPC_OK(); + } + + // Success! Return the decrypted sample to parent. + MOZ_ASSERT(!HasShmemOfSize(outputShmemSize)); + ipc::Shmem shmem = buffer->ExtractShmem(); + if (SendDecrypted(aId, cdm::kSuccess, std::move(shmem))) { + // No need to deallocate the output shmem; it should have been returned + // to the content process. + autoDeallocateOutputShmem.release(); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvInitializeVideoDecoder( + const CDMVideoDecoderConfig& aConfig) { + MOZ_ASSERT(IsOnMessageLoopThread()); + MOZ_ASSERT(!mDecoderInitialized); + if (!mCDM) { + GMP_LOG_DEBUG("ChromiumCDMChild::RecvInitializeVideoDecoder() no CDM"); + Unused << SendOnDecoderInitDone(cdm::kInitializationError); + return IPC_OK(); + } + cdm::VideoDecoderConfig_2 config = {}; + config.codec = static_cast<cdm::VideoCodec>(aConfig.mCodec()); + config.profile = static_cast<cdm::VideoCodecProfile>(aConfig.mProfile()); + config.format = static_cast<cdm::VideoFormat>(aConfig.mFormat()); + config.coded_size = + mCodedSize = {aConfig.mImageWidth(), aConfig.mImageHeight()}; + nsTArray<uint8_t> extraData(aConfig.mExtraData().Clone()); + config.extra_data = extraData.Elements(); + config.extra_data_size = extraData.Length(); + config.encryption_scheme = aConfig.mEncryptionScheme(); + cdm::Status status = mCDM->InitializeVideoDecoder(config); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvInitializeVideoDecoder() status=%u", + status); + Unused << SendOnDecoderInitDone(status); + mDecoderInitialized = status == cdm::kSuccess; + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvDeinitializeVideoDecoder() { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvDeinitializeVideoDecoder()"); + MOZ_ASSERT(mDecoderInitialized); + if (mDecoderInitialized && mCDM) { + mCDM->DeinitializeDecoder(cdm::kStreamTypeVideo); + } + mDecoderInitialized = false; + PurgeShmems(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvResetVideoDecoder() { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvResetVideoDecoder()"); + if (mDecoderInitialized && mCDM) { + mCDM->ResetDecoder(cdm::kStreamTypeVideo); + } + Unused << SendResetVideoDecoderComplete(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvDecryptAndDecodeFrame( + const CDMInputBuffer& aBuffer) { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecryptAndDecodeFrame() t=%" PRId64 ")", + aBuffer.mTimestamp()); + MOZ_ASSERT(mDecoderInitialized); + + if (!mCDM) { + GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecryptAndDecodeFrame() no CDM"); + Unused << SendDecodeFailed(cdm::kDecodeError); + return IPC_OK(); + } + + RefPtr<ChromiumCDMChild> self = this; + auto autoDeallocateShmem = + MakeScopeExit([&, self] { self->DeallocShmem(aBuffer.mData()); }); + + // The output frame may not have the same timestamp as the frame we put in. + // We may need to input a number of frames before we receive output. The + // CDM's decoder reorders to ensure frames output are in presentation order. + // So we need to store the durations of the frames input, and retrieve them + // on output. + mFrameDurations.Insert(aBuffer.mTimestamp(), aBuffer.mDuration()); + + cdm::InputBuffer_2 input = {}; + nsTArray<cdm::SubsampleEntry> subsamples; + InitInputBuffer(aBuffer, subsamples, input); + + WidevineVideoFrame frame; + cdm::Status rv = mCDM->DecryptAndDecodeFrame(input, &frame); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecryptAndDecodeFrame() t=%" PRId64 + " CDM decoder rv=%d", + aBuffer.mTimestamp(), rv); + + switch (rv) { + case cdm::kNeedMoreData: + Unused << SendDecodeFailed(rv); + break; + case cdm::kNoKey: + GMP_LOG_DEBUG("NoKey for sample at time=%" PRId64 "!", input.timestamp); + // Somehow our key became unusable. Typically this would happen when + // a stream requires output protection, and the configuration changed + // such that output protection is no longer available. For example, a + // non-compliant monitor was attached. The JS player should notice the + // key status changing to "output-restricted", and is supposed to switch + // to a stream that doesn't require OP. In order to keep the playback + // pipeline rolling, just output a black frame. See bug 1343140. + if (!frame.InitToBlack(mCodedSize.width, mCodedSize.height, + input.timestamp)) { + Unused << SendDecodeFailed(cdm::kDecodeError); + break; + } + [[fallthrough]]; + case cdm::kSuccess: + if (frame.FrameBuffer()) { + ReturnOutput(frame); + break; + } + // CDM didn't set a frame buffer on the sample, report it as an error. + [[fallthrough]]; + default: + Unused << SendDecodeFailed(rv); + break; + } + + return IPC_OK(); +} + +void ChromiumCDMChild::ReturnOutput(WidevineVideoFrame& aFrame) { + MOZ_ASSERT(IsOnMessageLoopThread()); + MOZ_ASSERT(aFrame.FrameBuffer()); + gmp::CDMVideoFrame output; + output.mFormat() = static_cast<cdm::VideoFormat>(aFrame.Format()); + output.mImageWidth() = aFrame.Size().width; + output.mImageHeight() = aFrame.Size().height; + output.mYPlane() = {aFrame.PlaneOffset(cdm::VideoPlane::kYPlane), + aFrame.Stride(cdm::VideoPlane::kYPlane)}; + output.mUPlane() = {aFrame.PlaneOffset(cdm::VideoPlane::kUPlane), + aFrame.Stride(cdm::VideoPlane::kUPlane)}; + output.mVPlane() = {aFrame.PlaneOffset(cdm::VideoPlane::kVPlane), + aFrame.Stride(cdm::VideoPlane::kVPlane)}; + output.mTimestamp() = aFrame.Timestamp(); + + uint64_t duration = 0; + if (mFrameDurations.Find(aFrame.Timestamp(), duration)) { + output.mDuration() = duration; + } + + CDMBuffer* base = reinterpret_cast<CDMBuffer*>(aFrame.FrameBuffer()); + if (base->AsShmemBuffer()) { + ipc::Shmem shmem = base->AsShmemBuffer()->ExtractShmem(); + Unused << SendDecodedShmem(output, std::move(shmem)); + } else { + MOZ_ASSERT(base->AsArrayBuffer()); + Unused << SendDecodedData(output, base->AsArrayBuffer()->ExtractBuffer()); + } +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvDrain() { + MOZ_ASSERT(IsOnMessageLoopThread()); + if (!mCDM) { + GMP_LOG_DEBUG("ChromiumCDMChild::RecvDrain() no CDM"); + Unused << SendDrainComplete(); + return IPC_OK(); + } + WidevineVideoFrame frame; + cdm::InputBuffer_2 sample = {}; + cdm::Status rv = mCDM->DecryptAndDecodeFrame(sample, &frame); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvDrain(); DecryptAndDecodeFrame() rv=%d", + rv); + if (rv == cdm::kSuccess) { + MOZ_ASSERT(frame.Format() != cdm::kUnknownVideoFormat); + ReturnOutput(frame); + } else { + Unused << SendDrainComplete(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvDestroy() { + MOZ_ASSERT(IsOnMessageLoopThread()); + GMP_LOG_DEBUG("ChromiumCDMChild::RecvDestroy()"); + + MOZ_ASSERT(!mDecoderInitialized); + + mInitPromise.RejectIfExists(NS_ERROR_ABORT, __func__); + + if (mCDM) { + mCDM->Destroy(); + mCDM = nullptr; + } + mDestroyed = true; + + Unused << Send__delete__(this); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult ChromiumCDMChild::RecvGiveBuffer(ipc::Shmem&& aBuffer) { + MOZ_ASSERT(IsOnMessageLoopThread()); + + GiveBuffer(std::move(aBuffer)); + return IPC_OK(); +} + +void ChromiumCDMChild::GiveBuffer(ipc::Shmem&& aBuffer) { + MOZ_ASSERT(IsOnMessageLoopThread()); + size_t sz = aBuffer.Size<uint8_t>(); + mBuffers.AppendElement(std::move(aBuffer)); + GMP_LOG_DEBUG( + "ChromiumCDMChild::RecvGiveBuffer(capacity=%zu" + ") bufferSizes={%s} mDecoderInitialized=%d", + sz, ToString(mBuffers).get(), mDecoderInitialized); +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/ChromiumCDMChild.h b/dom/media/gmp/ChromiumCDMChild.h new file mode 100644 index 0000000000..1bf153a5d5 --- /dev/null +++ b/dom/media/gmp/ChromiumCDMChild.h @@ -0,0 +1,148 @@ +/* -*- 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/. */ + +#ifndef ChromiumCDMChild_h_ +#define ChromiumCDMChild_h_ + +#include "content_decryption_module.h" +#include "mozilla/gmp/PChromiumCDMChild.h" +#include "SimpleMap.h" +#include "WidevineVideoFrame.h" + +namespace mozilla::gmp { + +class GMPContentChild; + +class ChromiumCDMChild : public PChromiumCDMChild, public cdm::Host_10 { + public: + // Mark AddRef and Release as `final`, as they overload pure virtual + // implementations in PChromiumCDMChild. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ChromiumCDMChild, final); + + explicit ChromiumCDMChild(GMPContentChild* aPlugin); + + void Init(cdm::ContentDecryptionModule_10* aCDM, + const nsACString& aStorageId); + + void TimerExpired(void* aContext); + + // cdm::Host_10 implementation + cdm::Buffer* Allocate(uint32_t aCapacity) override; + void SetTimer(int64_t aDelayMs, void* aContext) override; + cdm::Time GetCurrentWallTime() override; + void OnResolveKeyStatusPromise(uint32_t aPromiseId, + cdm::KeyStatus aKeyStatus) override; + void OnResolveNewSessionPromise(uint32_t aPromiseId, const char* aSessionId, + uint32_t aSessionIdSize) override; + void OnResolvePromise(uint32_t aPromiseId) override; + void OnRejectPromise(uint32_t aPromiseId, cdm::Exception aException, + uint32_t aSystemCode, const char* aErrorMessage, + uint32_t aErrorMessageSize) override; + void OnSessionMessage(const char* aSessionId, uint32_t aSessionIdSize, + cdm::MessageType aMessageType, const char* aMessage, + uint32_t aMessageSize) override; + void OnSessionKeysChange(const char* aSessionId, uint32_t aSessionIdSize, + bool aHasAdditionalUsableKey, + const cdm::KeyInformation* aKeysInfo, + uint32_t aKeysInfoCount) override; + void OnExpirationChange(const char* aSessionId, uint32_t aSessionIdSize, + cdm::Time aNewExpiryTime) override; + void OnSessionClosed(const char* aSessionId, + uint32_t aSessionIdSize) override; + void SendPlatformChallenge(const char* aServiceId, uint32_t aServiceIdSize, + const char* aChallenge, + uint32_t aChallengeSize) override {} + void EnableOutputProtection(uint32_t aDesiredProtectionMask) override {} + void QueryOutputProtectionStatus() override; + void OnDeferredInitializationDone(cdm::StreamType aStreamType, + cdm::Status aDecoderStatus) override {} + void RequestStorageId(uint32_t aVersion) override; + cdm::FileIO* CreateFileIO(cdm::FileIOClient* aClient) override; + void OnInitialized(bool aSuccess) override; + // end cdm::Host_10 specific methods + + void GiveBuffer(ipc::Shmem&& aBuffer); + + protected: + ~ChromiumCDMChild(); + + bool OnResolveNewSessionPromiseInternal(uint32_t aPromiseId, + const nsACString& aSessionId); + + bool IsOnMessageLoopThread(); + + void ActorDestroy(ActorDestroyReason aReason) override; + + ipc::IPCResult RecvGiveBuffer(ipc::Shmem&& aShmem) override; + ipc::IPCResult RecvPurgeShmems() override; + void PurgeShmems(); + ipc::IPCResult RecvInit(const bool& aAllowDistinctiveIdentifier, + const bool& aAllowPersistentState, + InitResolver&& aResolver) override; + ipc::IPCResult RecvSetServerCertificate( + const uint32_t& aPromiseId, nsTArray<uint8_t>&& aServerCert) override; + ipc::IPCResult RecvCreateSessionAndGenerateRequest( + const uint32_t& aPromiseId, const uint32_t& aSessionType, + const uint32_t& aInitDataType, nsTArray<uint8_t>&& aInitData) override; + ipc::IPCResult RecvLoadSession(const uint32_t& aPromiseId, + const uint32_t& aSessionType, + const nsACString& aSessionId) override; + ipc::IPCResult RecvUpdateSession(const uint32_t& aPromiseId, + const nsACString& aSessionId, + nsTArray<uint8_t>&& aResponse) override; + ipc::IPCResult RecvCloseSession(const uint32_t& aPromiseId, + const nsACString& aSessionId) override; + ipc::IPCResult RecvRemoveSession(const uint32_t& aPromiseId, + const nsACString& aSessionId) override; + ipc::IPCResult RecvCompleteQueryOutputProtectionStatus( + const bool& aSuccess, const uint32_t& aLinkMask, + const uint32_t& aProtectionMask) override; + ipc::IPCResult RecvGetStatusForPolicy( + const uint32_t& aPromiseId, + const cdm::HdcpVersion& aMinHdcpVersion) override; + ipc::IPCResult RecvDecrypt(const uint32_t& aId, + const CDMInputBuffer& aBuffer) override; + ipc::IPCResult RecvInitializeVideoDecoder( + const CDMVideoDecoderConfig& aConfig) override; + ipc::IPCResult RecvDeinitializeVideoDecoder() override; + ipc::IPCResult RecvResetVideoDecoder() override; + ipc::IPCResult RecvDecryptAndDecodeFrame( + const CDMInputBuffer& aBuffer) override; + ipc::IPCResult RecvDrain() override; + ipc::IPCResult RecvDestroy() override; + + void ReturnOutput(WidevineVideoFrame& aFrame); + bool HasShmemOfSize(size_t aSize) const; + + template <typename MethodType, typename... ParamType> + void CallMethod(MethodType, ParamType&&...); + + template <typename MethodType, typename... ParamType> + void CallOnMessageLoopThread(const char* const, MethodType, ParamType&&...); + + GMPContentChild* mPlugin = nullptr; + cdm::ContentDecryptionModule_10* mCDM = nullptr; + + typedef SimpleMap<uint64_t> DurationMap; + DurationMap mFrameDurations; + nsTArray<uint32_t> mLoadSessionPromiseIds; + + cdm::Size mCodedSize = {0, 0}; + nsTArray<ipc::Shmem> mBuffers; + + bool mDecoderInitialized = false; + bool mPersistentStateAllowed = false; + bool mDestroyed = false; + nsCString mStorageId; + + typedef MozPromise<bool, nsresult, /* IsExclusive = */ true> InitPromise; + // Created when we RecvInit, should be resolved once the CDM is initialized + // or rejected if init fails. + MozPromiseHolder<InitPromise> mInitPromise; +}; + +} // namespace mozilla::gmp + +#endif // ChromiumCDMChild_h_ diff --git a/dom/media/gmp/ChromiumCDMParent.cpp b/dom/media/gmp/ChromiumCDMParent.cpp new file mode 100644 index 0000000000..ddb1e38d33 --- /dev/null +++ b/dom/media/gmp/ChromiumCDMParent.cpp @@ -0,0 +1,1319 @@ +/* -*- 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 "ChromiumCDMParent.h" + +#include "ChromiumCDMCallback.h" +#include "ChromiumCDMCallbackProxy.h" +#include "ChromiumCDMProxy.h" +#include "content_decryption_module.h" +#include "GMPContentChild.h" +#include "GMPContentParent.h" +#include "GMPLog.h" +#include "GMPService.h" +#include "GMPUtils.h" +#include "VideoUtils.h" +#include "mozilla/dom/MediaKeyMessageEventBinding.h" +#include "mozilla/gmp/GMPTypes.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/Unused.h" +#include "AnnexB.h" +#include "H264.h" + +#define NS_DispatchToMainThread(...) CompileError_UseAbstractMainThreadInstead + +namespace mozilla::gmp { + +using namespace eme; + +ChromiumCDMParent::ChromiumCDMParent(GMPContentParent* aContentParent, + uint32_t aPluginId) + : mPluginId(aPluginId), + mContentParent(aContentParent), + mVideoShmemLimit(StaticPrefs::media_eme_chromium_api_video_shmems()) +#ifdef DEBUG + , + mGMPThread(aContentParent->GMPEventTarget()) +#endif +{ + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::ChromiumCDMParent(this=%p, contentParent=%p, " + "id=%" PRIu32 ")", + this, aContentParent, aPluginId); +} + +RefPtr<ChromiumCDMParent::InitPromise> ChromiumCDMParent::Init( + ChromiumCDMCallback* aCDMCallback, bool aAllowDistinctiveIdentifier, + bool aAllowPersistentState, nsIEventTarget* aMainThread) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::Init(this=%p) shutdown=%s abormalShutdown=%s " + "actorDestroyed=%s", + this, mIsShutdown ? "true" : "false", + mAbnormalShutdown ? "true" : "false", mActorDestroyed ? "true" : "false"); + if (!aCDMCallback || !aMainThread) { + GMP_LOG_DEBUG( + "ChromiumCDMParent::Init(this=%p) failed " + "nullCallback=%s nullMainThread=%s", + this, !aCDMCallback ? "true" : "false", + !aMainThread ? "true" : "false"); + + return ChromiumCDMParent::InitPromise::CreateAndReject( + MediaResult(NS_ERROR_FAILURE, + nsPrintfCString("ChromiumCDMParent::Init() failed " + "nullCallback=%s nullMainThread=%s", + !aCDMCallback ? "true" : "false", + !aMainThread ? "true" : "false")), + __func__); + } + + RefPtr<ChromiumCDMParent::InitPromise> promise = + mInitPromise.Ensure(__func__); + RefPtr<ChromiumCDMParent> self = this; + SendInit(aAllowDistinctiveIdentifier, aAllowPersistentState) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self, aCDMCallback](bool aSuccess) { + if (!aSuccess) { + GMP_LOG_DEBUG( + "ChromiumCDMParent::Init() failed with callback from " + "child indicating CDM failed init"); + self->mInitPromise.RejectIfExists( + MediaResult(NS_ERROR_FAILURE, + "ChromiumCDMParent::Init() failed with callback " + "from child indicating CDM failed init"), + __func__); + return; + } + GMP_LOG_DEBUG( + "ChromiumCDMParent::Init() succeeded with callback from child"); + self->mCDMCallback = aCDMCallback; + self->mInitPromise.ResolveIfExists(true /* unused */, __func__); + }, + [self](ResponseRejectReason&& aReason) { + RefPtr<gmp::GeckoMediaPluginService> service = + gmp::GeckoMediaPluginService::GetGeckoMediaPluginService(); + bool xpcomWillShutdown = + service && service->XPCOMWillShutdownReceived(); + GMP_LOG_DEBUG( + "ChromiumCDMParent::Init(this=%p) failed " + "shutdown=%s cdmCrash=%s actorDestroyed=%s " + "browserShutdown=%s promiseRejectReason=%d", + self.get(), self->mIsShutdown ? "true" : "false", + self->mAbnormalShutdown ? "true" : "false", + self->mActorDestroyed ? "true" : "false", + xpcomWillShutdown ? "true" : "false", + static_cast<int>(aReason)); + self->mInitPromise.RejectIfExists( + MediaResult( + NS_ERROR_FAILURE, + nsPrintfCString("ChromiumCDMParent::Init() failed " + "shutdown=%s cdmCrash=%s actorDestroyed=%s " + "browserShutdown=%s promiseRejectReason=%d", + self->mIsShutdown ? "true" : "false", + self->mAbnormalShutdown ? "true" : "false", + self->mActorDestroyed ? "true" : "false", + xpcomWillShutdown ? "true" : "false", + static_cast<int>(aReason))), + __func__); + }); + return promise; +} + +void ChromiumCDMParent::CreateSession(uint32_t aCreateSessionToken, + uint32_t aSessionType, + uint32_t aInitDataType, + uint32_t aPromiseId, + const nsTArray<uint8_t>& aInitData) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::CreateSession(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendCreateSessionAndGenerateRequest(aPromiseId, aSessionType, + aInitDataType, aInitData)) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send generateRequest to CDM process."_ns); + return; + } + mPromiseToCreateSessionToken.InsertOrUpdate(aPromiseId, aCreateSessionToken); +} + +void ChromiumCDMParent::LoadSession(uint32_t aPromiseId, uint32_t aSessionType, + nsString aSessionId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::LoadSession(this=%p, pid=%" PRIu32 + ", type=%" PRIu32 ", sid=%s)", + this, aPromiseId, aSessionType, + NS_ConvertUTF16toUTF8(aSessionId).get()); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendLoadSession(aPromiseId, aSessionType, + NS_ConvertUTF16toUTF8(aSessionId))) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send loadSession to CDM process."_ns); + return; + } +} + +void ChromiumCDMParent::SetServerCertificate(uint32_t aPromiseId, + const nsTArray<uint8_t>& aCert) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::SetServerCertificate(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendSetServerCertificate(aPromiseId, aCert)) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send setServerCertificate to CDM process"_ns); + } +} + +void ChromiumCDMParent::UpdateSession(const nsCString& aSessionId, + uint32_t aPromiseId, + const nsTArray<uint8_t>& aResponse) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::UpdateSession(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendUpdateSession(aPromiseId, aSessionId, aResponse)) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send updateSession to CDM process"_ns); + } +} + +void ChromiumCDMParent::CloseSession(const nsCString& aSessionId, + uint32_t aPromiseId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::CloseSession(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendCloseSession(aPromiseId, aSessionId)) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send closeSession to CDM process"_ns); + } +} + +void ChromiumCDMParent::RemoveSession(const nsCString& aSessionId, + uint32_t aPromiseId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RemoveSession(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + if (!SendRemoveSession(aPromiseId, aSessionId)) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send removeSession to CDM process"_ns); + } +} + +void ChromiumCDMParent::NotifyOutputProtectionStatus(bool aSuccess, + uint32_t aLinkMask, + uint32_t aProtectionMask) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::NotifyOutputProtectionStatus(this=%p)", + this); + if (mIsShutdown) { + return; + } + const bool haveCachedValue = mOutputProtectionLinkMask.isSome(); + if (mAwaitingOutputProtectionInformation && !aSuccess) { + MOZ_DIAGNOSTIC_ASSERT( + !haveCachedValue, + "Should not have a cached value if we're still awaiting infomation"); + // We're awaiting info and don't yet have a cached value, and the check + // failed, don't cache the result, just forward the failure. + CompleteQueryOutputProtectionStatus(false, aLinkMask, aProtectionMask); + return; + } + if (!mAwaitingOutputProtectionInformation && haveCachedValue && !aSuccess) { + // We're not awaiting info, already have a cached value, and the check + // failed. Ignore this, we'll update our info from any future successful + // checks. + return; + } + MOZ_ASSERT(aSuccess, "Failed checks should be handled by this point"); + // Update our protection information. + mOutputProtectionLinkMask = Some(aLinkMask); + + if (mAwaitingOutputProtectionInformation) { + // If we have an outstanding query, service that query with this + // information. + mAwaitingOutputProtectionInformation = false; + MOZ_ASSERT(!haveCachedValue, + "If we were waiting on information, we shouldn't have yet " + "cached a value"); + CompleteQueryOutputProtectionStatus(true, mOutputProtectionLinkMask.value(), + aProtectionMask); + } +} + +void ChromiumCDMParent::CompleteQueryOutputProtectionStatus( + bool aSuccess, uint32_t aLinkMask, uint32_t aProtectionMask) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::CompleteQueryOutputProtectionStatus(this=%p) " + "mIsShutdown=%s aSuccess=%s aLinkMask=%" PRIu32, + this, mIsShutdown ? "true" : "false", aSuccess ? "true" : "false", + aLinkMask); + if (mIsShutdown) { + return; + } + Unused << SendCompleteQueryOutputProtectionStatus(aSuccess, aLinkMask, + aProtectionMask); +} + +// See +// https://cs.chromium.org/chromium/src/media/blink/webcontentdecryptionmodule_impl.cc?l=33-66&rcl=d49aa59ac8c2925d5bec229f3f1906537b6b4547 +static Result<cdm::HdcpVersion, nsresult> ToCDMHdcpVersion( + const nsCString& aMinHdcpVersion) { + if (aMinHdcpVersion.IsEmpty()) { + return cdm::HdcpVersion::kHdcpVersionNone; + } + if (aMinHdcpVersion.EqualsIgnoreCase("1.0")) { + return cdm::HdcpVersion::kHdcpVersion1_0; + } + if (aMinHdcpVersion.EqualsIgnoreCase("1.1")) { + return cdm::HdcpVersion::kHdcpVersion1_1; + } + if (aMinHdcpVersion.EqualsIgnoreCase("1.2")) { + return cdm::HdcpVersion::kHdcpVersion1_2; + } + if (aMinHdcpVersion.EqualsIgnoreCase("1.3")) { + return cdm::HdcpVersion::kHdcpVersion1_3; + } + if (aMinHdcpVersion.EqualsIgnoreCase("1.4")) { + return cdm::HdcpVersion::kHdcpVersion1_4; + } + if (aMinHdcpVersion.EqualsIgnoreCase("2.0")) { + return cdm::HdcpVersion::kHdcpVersion2_0; + } + if (aMinHdcpVersion.EqualsIgnoreCase("2.1")) { + return cdm::HdcpVersion::kHdcpVersion2_1; + } + if (aMinHdcpVersion.EqualsIgnoreCase("2.2")) { + return cdm::HdcpVersion::kHdcpVersion2_2; + } + // When adding another version remember to update GMPMessageUtils so that we + // can serialize it correctly and have correct bounds on the enum! + + // Invalid hdcp version string. + return Err(NS_ERROR_INVALID_ARG); +} + +void ChromiumCDMParent::GetStatusForPolicy(uint32_t aPromiseId, + const nsCString& aMinHdcpVersion) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::GetStatusForPolicy(this=%p)", this); + if (mIsShutdown) { + RejectPromiseShutdown(aPromiseId); + return; + } + auto hdcpVersionResult = ToCDMHdcpVersion(aMinHdcpVersion); + if (hdcpVersionResult.isErr()) { + ErrorResult rv; + // XXXbz there's no spec for this yet, and + // <https://github.com/WICG/hdcp-detection/blob/master/explainer.md> + // does not define what exceptions get thrown. Let's assume + // TypeError for invalid args, as usual. + constexpr auto err = + "getStatusForPolicy failed due to bad hdcp version argument"_ns; + rv.ThrowTypeError(err); + RejectPromise(aPromiseId, std::move(rv), err); + return; + } + + if (!SendGetStatusForPolicy(aPromiseId, hdcpVersionResult.unwrap())) { + RejectPromiseWithStateError( + aPromiseId, "Failed to send getStatusForPolicy to CDM process"_ns); + } +} + +bool ChromiumCDMParent::InitCDMInputBuffer(gmp::CDMInputBuffer& aBuffer, + MediaRawData* aSample) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + const CryptoSample& crypto = aSample->mCrypto; + if (crypto.mEncryptedSizes.Length() != crypto.mPlainSizes.Length()) { + GMP_LOG_DEBUG("InitCDMInputBuffer clear/cipher subsamples don't match"); + return false; + } + + Shmem shmem; + if (!AllocShmem(aSample->Size(), &shmem)) { + return false; + } + memcpy(shmem.get<uint8_t>(), aSample->Data(), aSample->Size()); + cdm::EncryptionScheme encryptionScheme = cdm::EncryptionScheme::kUnencrypted; + switch (crypto.mCryptoScheme) { + case CryptoScheme::None: + break; // Default to none + case CryptoScheme::Cenc: + encryptionScheme = cdm::EncryptionScheme::kCenc; + break; + case CryptoScheme::Cbcs: + encryptionScheme = cdm::EncryptionScheme::kCbcs; + break; + default: + GMP_LOG_DEBUG( + "InitCDMInputBuffer got unexpected encryption scheme with " + "value of %" PRIu8 ". Treating as no encryption.", + static_cast<uint8_t>(crypto.mCryptoScheme)); + MOZ_ASSERT_UNREACHABLE("Should not have unrecognized encryption type"); + break; + } + + const nsTArray<uint8_t>& iv = encryptionScheme != cdm::EncryptionScheme::kCbcs + ? crypto.mIV + : crypto.mConstantIV; + aBuffer = gmp::CDMInputBuffer( + shmem, crypto.mKeyId, iv, aSample->mTime.ToMicroseconds(), + aSample->mDuration.ToMicroseconds(), crypto.mPlainSizes, + crypto.mEncryptedSizes, crypto.mCryptByteBlock, crypto.mSkipByteBlock, + encryptionScheme); + return true; +} + +bool ChromiumCDMParent::SendBufferToCDM(uint32_t aSizeInBytes) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::SendBufferToCDM() size=%" PRIu32, + aSizeInBytes); + Shmem shmem; + if (!AllocShmem(aSizeInBytes, &shmem)) { + return false; + } + if (!SendGiveBuffer(std::move(shmem))) { + DeallocShmem(shmem); + return false; + } + return true; +} + +RefPtr<DecryptPromise> ChromiumCDMParent::Decrypt(MediaRawData* aSample) { + if (mIsShutdown) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample), + __func__); + } + CDMInputBuffer buffer; + if (!InitCDMInputBuffer(buffer, aSample)) { + return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample), + __func__); + } + // Send a buffer to the CDM to store the output. The CDM will either fill + // it with the decrypted sample and return it, or deallocate it on failure. + if (!SendBufferToCDM(aSample->Size())) { + DeallocShmem(buffer.mData()); + return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample), + __func__); + } + + RefPtr<DecryptJob> job = new DecryptJob(aSample); + if (!SendDecrypt(job->mId, buffer)) { + GMP_LOG_DEBUG( + "ChromiumCDMParent::Decrypt(this=%p) failed to send decrypt message", + this); + DeallocShmem(buffer.mData()); + return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample), + __func__); + } + RefPtr<DecryptPromise> promise = job->Ensure(); + mDecrypts.AppendElement(job); + return promise; +} + +ipc::IPCResult ChromiumCDMParent::Recv__delete__() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + MOZ_ASSERT(mIsShutdown); + GMP_LOG_DEBUG("ChromiumCDMParent::Recv__delete__(this=%p)", this); + if (mContentParent) { + mContentParent->ChromiumCDMDestroyed(this); + mContentParent = nullptr; + } + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnResolvePromiseWithKeyStatus( + const uint32_t& aPromiseId, const uint32_t& aKeyStatus) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::RecvOnResolvePromiseWithKeyStatus(this=%p, " + "pid=%" PRIu32 ", keystatus=%" PRIu32 ")", + this, aPromiseId, aKeyStatus); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->ResolvePromiseWithKeyStatus(aPromiseId, aKeyStatus); + + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnResolveNewSessionPromise( + const uint32_t& aPromiseId, const nsCString& aSessionId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::RecvOnResolveNewSessionPromise(this=%p, pid=%" PRIu32 + ", sid=%s)", + this, aPromiseId, aSessionId.get()); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + Maybe<uint32_t> token = mPromiseToCreateSessionToken.Extract(aPromiseId); + if (token.isNothing()) { + RejectPromiseWithStateError(aPromiseId, + "Lost session token for new session."_ns); + return IPC_OK(); + } + + mCDMCallback->SetSessionId(token.value(), aSessionId); + + ResolvePromise(aPromiseId); + + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvResolveLoadSessionPromise( + const uint32_t& aPromiseId, const bool& aSuccessful) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::RecvResolveLoadSessionPromise(this=%p, pid=%" PRIu32 + ", successful=%d)", + this, aPromiseId, aSuccessful); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->ResolveLoadSessionPromise(aPromiseId, aSuccessful); + + return IPC_OK(); +} + +void ChromiumCDMParent::ResolvePromise(uint32_t aPromiseId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::ResolvePromise(this=%p, pid=%" PRIu32 ")", + this, aPromiseId); + + // Note: The MediaKeys rejects all pending DOM promises when it + // initiates shutdown. + if (!mCDMCallback || mIsShutdown) { + return; + } + + mCDMCallback->ResolvePromise(aPromiseId); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnResolvePromise( + const uint32_t& aPromiseId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + ResolvePromise(aPromiseId); + return IPC_OK(); +} + +void ChromiumCDMParent::RejectPromise(uint32_t aPromiseId, + ErrorResult&& aException, + const nsCString& aErrorMessage) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RejectPromise(this=%p, pid=%" PRIu32 ")", + this, aPromiseId); + // Note: The MediaKeys rejects all pending DOM promises when it + // initiates shutdown. + if (!mCDMCallback || mIsShutdown) { + // Suppress the exception as it will not be explicitly handled due to the + // early return. + aException.SuppressException(); + return; + } + + mCDMCallback->RejectPromise(aPromiseId, std::move(aException), aErrorMessage); +} + +void ChromiumCDMParent::RejectPromiseShutdown(uint32_t aPromiseId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + RejectPromiseWithStateError(aPromiseId, "CDM is shutdown"_ns); +} + +void ChromiumCDMParent::RejectPromiseWithStateError( + uint32_t aPromiseId, const nsCString& aErrorMessage) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + ErrorResult rv; + rv.ThrowInvalidStateError(aErrorMessage); + RejectPromise(aPromiseId, std::move(rv), aErrorMessage); +} + +static ErrorResult ToErrorResult(uint32_t aException, + const nsCString& aErrorMessage) { + // XXXbz could we have a CopyableErrorResult sent to us with a better error + // message? + ErrorResult rv; + switch (static_cast<cdm::Exception>(aException)) { + case cdm::Exception::kExceptionNotSupportedError: + rv.ThrowNotSupportedError(aErrorMessage); + break; + case cdm::Exception::kExceptionInvalidStateError: + rv.ThrowInvalidStateError(aErrorMessage); + break; + case cdm::Exception::kExceptionTypeError: + rv.ThrowTypeError(aErrorMessage); + break; + case cdm::Exception::kExceptionQuotaExceededError: + rv.ThrowQuotaExceededError(aErrorMessage); + break; + default: + MOZ_ASSERT_UNREACHABLE("Invalid cdm::Exception enum value."); + // Note: Unique placeholder. + rv.ThrowTimeoutError(aErrorMessage); + }; + return rv; +} + +ipc::IPCResult ChromiumCDMParent::RecvOnRejectPromise( + const uint32_t& aPromiseId, const uint32_t& aException, + const uint32_t& aSystemCode, const nsCString& aErrorMessage) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + RejectPromise(aPromiseId, ToErrorResult(aException, aErrorMessage), + aErrorMessage); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnSessionMessage( + const nsCString& aSessionId, const uint32_t& aMessageType, + nsTArray<uint8_t>&& aMessage) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnSessionMessage(this=%p, sid=%s)", + this, aSessionId.get()); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->SessionMessage(aSessionId, aMessageType, std::move(aMessage)); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnSessionKeysChange( + const nsCString& aSessionId, nsTArray<CDMKeyInformation>&& aKeysInfo) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnSessionKeysChange(this=%p)", this); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->SessionKeysChange(aSessionId, std::move(aKeysInfo)); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnExpirationChange( + const nsCString& aSessionId, const double& aSecondsSinceEpoch) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnExpirationChange(this=%p) time=%lf", + this, aSecondsSinceEpoch); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->ExpirationChange(aSessionId, aSecondsSinceEpoch); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnSessionClosed( + const nsCString& aSessionId) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnSessionClosed(this=%p)", this); + if (!mCDMCallback || mIsShutdown) { + return IPC_OK(); + } + + mCDMCallback->SessionClosed(aSessionId); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnQueryOutputProtectionStatus() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::RecvOnQueryOutputProtectionStatus(this=%p) " + "mIsShutdown=%s mCDMCallback=%s mAwaitingOutputProtectionInformation=%s", + this, mIsShutdown ? "true" : "false", mCDMCallback ? "true" : "false", + mAwaitingOutputProtectionInformation ? "true" : "false"); + if (mIsShutdown) { + // We're shutdown, don't try to service the query. + return IPC_OK(); + } + if (!mCDMCallback) { + // We don't have a callback. We're not yet outputting anything so can report + // we're safe. + CompleteQueryOutputProtectionStatus(true, uint32_t{}, uint32_t{}); + return IPC_OK(); + } + + if (mOutputProtectionLinkMask.isSome()) { + MOZ_DIAGNOSTIC_ASSERT( + !mAwaitingOutputProtectionInformation, + "If we have a cached value we should not be awaiting information"); + // We have a cached value, use that. + CompleteQueryOutputProtectionStatus(true, mOutputProtectionLinkMask.value(), + uint32_t{}); + return IPC_OK(); + } + + // We need to call up the stack to get the info. The CDM proxy will finish + // the request via `NotifyOutputProtectionStatus`. + mAwaitingOutputProtectionInformation = true; + mCDMCallback->QueryOutputProtectionStatus(); + return IPC_OK(); +} + +DecryptStatus ToDecryptStatus(uint32_t aStatus) { + switch (static_cast<cdm::Status>(aStatus)) { + case cdm::kSuccess: + return DecryptStatus::Ok; + case cdm::kNoKey: + return DecryptStatus::NoKeyErr; + default: + return DecryptStatus::GenericErr; + } +} + +ipc::IPCResult ChromiumCDMParent::RecvDecryptFailed(const uint32_t& aId, + const uint32_t& aStatus) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecryptFailed(this=%p, id=%" PRIu32 + ", status=%" PRIu32 ")", + this, aId, aStatus); + + if (mIsShutdown) { + MOZ_ASSERT(mDecrypts.IsEmpty()); + return IPC_OK(); + } + + for (size_t i = 0; i < mDecrypts.Length(); i++) { + if (mDecrypts[i]->mId == aId) { + mDecrypts[i]->PostResult(ToDecryptStatus(aStatus)); + mDecrypts.RemoveElementAt(i); + break; + } + } + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvDecrypted(const uint32_t& aId, + const uint32_t& aStatus, + ipc::Shmem&& aShmem) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecrypted(this=%p, id=%" PRIu32 + ", status=%" PRIu32 ")", + this, aId, aStatus); + + // We must deallocate the shmem once we've copied the result out of it + // in PostResult below. + auto autoDeallocateShmem = MakeScopeExit([&, this] { DeallocShmem(aShmem); }); + + if (mIsShutdown) { + MOZ_ASSERT(mDecrypts.IsEmpty()); + return IPC_OK(); + } + for (size_t i = 0; i < mDecrypts.Length(); i++) { + if (mDecrypts[i]->mId == aId) { + mDecrypts[i]->PostResult( + ToDecryptStatus(aStatus), + Span<const uint8_t>(aShmem.get<uint8_t>(), aShmem.Size<uint8_t>())); + mDecrypts.RemoveElementAt(i); + break; + } + } + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvIncreaseShmemPoolSize() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("%s(this=%p) limit=%" PRIu32 " active=%" PRIu32, __func__, this, + mVideoShmemLimit, mVideoShmemsActive); + + // Put an upper limit on the number of shmems we tolerate the CDM asking + // for, to prevent a memory blow-out. In practice, we expect the CDM to + // need less than 5, but some encodings require more. + // We'd expect CDMs to not have video frames larger than 720p-1080p + // (due to DRM robustness requirements), which is about 1.5MB-3MB per + // frame. + if (mVideoShmemLimit > 50) { + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("Failled to ensure CDM has enough shmems.")), + __func__); + Shutdown(); + return IPC_OK(); + } + mVideoShmemLimit++; + + EnsureSufficientShmems(mVideoFrameBufferSize); + + return IPC_OK(); +} + +bool ChromiumCDMParent::PurgeShmems() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::PurgeShmems(this=%p) frame_size=%zu" + " limit=%" PRIu32 " active=%" PRIu32, + this, mVideoFrameBufferSize, mVideoShmemLimit, mVideoShmemsActive); + + if (mVideoShmemsActive == 0) { + // We haven't allocated any shmems, nothing to do here. + return true; + } + if (!SendPurgeShmems()) { + return false; + } + mVideoShmemsActive = 0; + return true; +} + +bool ChromiumCDMParent::EnsureSufficientShmems(size_t aVideoFrameSize) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::EnsureSufficientShmems(this=%p) " + "size=%zu expected_size=%zu limit=%" PRIu32 " active=%" PRIu32, + this, aVideoFrameSize, mVideoFrameBufferSize, mVideoShmemLimit, + mVideoShmemsActive); + + // The Chromium CDM API requires us to implement a synchronous + // interface to supply buffers to the CDM for it to write decrypted samples + // into. We want our buffers to be backed by shmems, in order to reduce + // the overhead of transferring decoded frames. However due to sandboxing + // restrictions, the CDM process cannot allocate shmems itself. + // We don't want to be doing synchronous IPC to request shmems from the + // content process, nor do we want to have to do intr IPC or make async + // IPC conform to the sync allocation interface. So instead we have the + // content process pre-allocate a set of shmems and give them to the CDM + // process in advance of them being needed. + // + // When the CDM needs to allocate a buffer for storing a decoded video + // frame, the CDM host gives it one of these shmems' buffers. When this + // is sent back to the content process, we upload it to a GPU surface, + // and send the shmem back to the CDM process so it can reuse it. + // + // Normally the CDM won't allocate more than one buffer at once, but + // we've seen cases where it allocates multiple buffers, returns one and + // holds onto the rest. So we need to ensure we have several extra + // shmems pre-allocated for the CDM. This threshold is set by the pref + // media.eme.chromium-api.video-shmems. + // + // We also have a failure recovery mechanism; if the CDM asks for more + // buffers than we have shmem's available, ChromiumCDMChild gives the + // CDM a non-shared memory buffer, and returns the frame to the parent + // in an nsTArray<uint8_t> instead of a shmem. The child then sends a + // message to the parent asking it to increase the number of shmems in + // the pool. Via this mechanism we should recover from incorrectly + // predicting how many shmems to pre-allocate. + // + // At decoder start up, we guess how big the shmems need to be based on + // the video frame dimensions. If we guess wrong, the CDM will follow + // the non-shmem path, and we'll re-create the shmems of the correct size. + // This meanns we can recover from guessing the shmem size wrong. + // We must re-take this path after every decoder de-init/re-init, as the + // frame sizes should change every time we switch video stream. + + if (mVideoFrameBufferSize < aVideoFrameSize) { + if (!PurgeShmems()) { + return false; + } + mVideoFrameBufferSize = aVideoFrameSize; + } + + while (mVideoShmemsActive < mVideoShmemLimit) { + if (!SendBufferToCDM(mVideoFrameBufferSize)) { + return false; + } + mVideoShmemsActive++; + } + + return true; +} + +ipc::IPCResult ChromiumCDMParent::RecvDecodedData(const CDMVideoFrame& aFrame, + nsTArray<uint8_t>&& aData) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecodedData(this=%p) time=%" PRId64, + this, aFrame.mTimestamp()); + + if (mIsShutdown || mDecodePromise.IsEmpty()) { + return IPC_OK(); + } + + if (!EnsureSufficientShmems(aData.Length())) { + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("Failled to ensure CDM has enough shmems.")), + __func__); + return IPC_OK(); + } + + RefPtr<VideoData> v = CreateVideoFrame(aFrame, aData); + if (!v) { + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_OUT_OF_MEMORY, + RESULT_DETAIL("Can't create VideoData")), + __func__); + return IPC_OK(); + } + + ReorderAndReturnOutput(std::move(v)); + + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvDecodedShmem(const CDMVideoFrame& aFrame, + ipc::Shmem&& aShmem) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecodedShmem(this=%p) time=%" PRId64 + " duration=%" PRId64, + this, aFrame.mTimestamp(), aFrame.mDuration()); + + // On failure we need to deallocate the shmem we're to return to the + // CDM. On success we return it to the CDM to be reused. + auto autoDeallocateShmem = + MakeScopeExit([&, this] { this->DeallocShmem(aShmem); }); + + if (mIsShutdown || mDecodePromise.IsEmpty()) { + return IPC_OK(); + } + + RefPtr<VideoData> v = CreateVideoFrame( + aFrame, Span<uint8_t>(aShmem.get<uint8_t>(), aShmem.Size<uint8_t>())); + if (!v) { + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_OUT_OF_MEMORY, + RESULT_DETAIL("Can't create VideoData")), + __func__); + return IPC_OK(); + } + + // Return the shmem to the CDM so the shmem can be reused to send us + // another frame. + if (!SendGiveBuffer(std::move(aShmem))) { + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_OUT_OF_MEMORY, + RESULT_DETAIL("Can't return shmem to CDM process")), + __func__); + return IPC_OK(); + } + + // Don't need to deallocate the shmem since the CDM process is responsible + // for it again. + autoDeallocateShmem.release(); + + ReorderAndReturnOutput(std::move(v)); + + return IPC_OK(); +} + +void ChromiumCDMParent::ReorderAndReturnOutput(RefPtr<VideoData>&& aFrame) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mMaxRefFrames == 0) { + mDecodePromise.ResolveIfExists( + MediaDataDecoder::DecodedData({std::move(aFrame)}), __func__); + return; + } + mReorderQueue.Push(std::move(aFrame)); + MediaDataDecoder::DecodedData results; + while (mReorderQueue.Length() > mMaxRefFrames) { + results.AppendElement(mReorderQueue.Pop()); + } + mDecodePromise.Resolve(std::move(results), __func__); +} + +already_AddRefed<VideoData> ChromiumCDMParent::CreateVideoFrame( + const CDMVideoFrame& aFrame, Span<uint8_t> aData) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + VideoData::YCbCrBuffer b; + MOZ_ASSERT(aData.Length() > 0); + + // Since we store each plane separately we can just roll the offset + // into our pointer to that plane and store that. + b.mPlanes[0].mData = aData.Elements() + aFrame.mYPlane().mPlaneOffset(); + b.mPlanes[0].mWidth = aFrame.mImageWidth(); + b.mPlanes[0].mHeight = aFrame.mImageHeight(); + b.mPlanes[0].mStride = aFrame.mYPlane().mStride(); + b.mPlanes[0].mSkip = 0; + + b.mPlanes[1].mData = aData.Elements() + aFrame.mUPlane().mPlaneOffset(); + b.mPlanes[1].mWidth = (aFrame.mImageWidth() + 1) / 2; + b.mPlanes[1].mHeight = (aFrame.mImageHeight() + 1) / 2; + b.mPlanes[1].mStride = aFrame.mUPlane().mStride(); + b.mPlanes[1].mSkip = 0; + + b.mPlanes[2].mData = aData.Elements() + aFrame.mVPlane().mPlaneOffset(); + b.mPlanes[2].mWidth = (aFrame.mImageWidth() + 1) / 2; + b.mPlanes[2].mHeight = (aFrame.mImageHeight() + 1) / 2; + b.mPlanes[2].mStride = aFrame.mVPlane().mStride(); + b.mPlanes[2].mSkip = 0; + + b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT; + + // We unfortunately can't know which colorspace the video is using at this + // stage. + b.mYUVColorSpace = + DefaultColorSpace({aFrame.mImageWidth(), aFrame.mImageHeight()}); + + gfx::IntRect pictureRegion(0, 0, aFrame.mImageWidth(), aFrame.mImageHeight()); + RefPtr<VideoData> v = VideoData::CreateAndCopyData( + mVideoInfo, mImageContainer, mLastStreamOffset, + media::TimeUnit::FromMicroseconds(aFrame.mTimestamp()), + media::TimeUnit::FromMicroseconds(aFrame.mDuration()), b, false, + media::TimeUnit::FromMicroseconds(-1), pictureRegion, mKnowsCompositor); + + if (!v || !v->mImage) { + NS_WARNING("Failed to decode video frame."); + return v.forget(); + } + + // This is a DRM image. + v->mImage->SetIsDRM(true); + + return v.forget(); +} + +ipc::IPCResult ChromiumCDMParent::RecvDecodeFailed(const uint32_t& aStatus) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecodeFailed(this=%p status=%" PRIu32 + ")", + this, aStatus); + if (mIsShutdown) { + MOZ_ASSERT(mDecodePromise.IsEmpty()); + return IPC_OK(); + } + + if (aStatus == cdm::kNeedMoreData) { + mDecodePromise.ResolveIfExists(nsTArray<RefPtr<MediaData>>(), __func__); + return IPC_OK(); + } + + mDecodePromise.RejectIfExists( + MediaResult( + NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL( + "ChromiumCDMParent::RecvDecodeFailed with status %s (%" PRIu32 + ")", + CdmStatusToString(aStatus), aStatus)), + __func__); + return IPC_OK(); +} + +ipc::IPCResult ChromiumCDMParent::RecvShutdown() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::RecvShutdown(this=%p)", this); + Shutdown(); + return IPC_OK(); +} + +void ChromiumCDMParent::ActorDestroy(ActorDestroyReason aWhy) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::ActorDestroy(this=%p, reason=%d)", this, + aWhy); + MOZ_ASSERT(!mActorDestroyed); + mActorDestroyed = true; + // Shutdown() will clear mCDMCallback, so let's keep a reference for later + // use. + auto callback = mCDMCallback; + if (!mIsShutdown) { + // Plugin crash. + MOZ_ASSERT(aWhy == AbnormalShutdown); + Shutdown(); + } + MOZ_ASSERT(mIsShutdown); + RefPtr<ChromiumCDMParent> kungFuDeathGrip(this); + if (mContentParent) { + mContentParent->ChromiumCDMDestroyed(this); + mContentParent = nullptr; + } + mAbnormalShutdown = (aWhy == AbnormalShutdown); + if (mAbnormalShutdown && callback) { + callback->Terminated(); + } + MaybeDisconnect(mAbnormalShutdown); +} + +RefPtr<MediaDataDecoder::InitPromise> ChromiumCDMParent::InitializeVideoDecoder( + const gmp::CDMVideoDecoderConfig& aConfig, const VideoInfo& aInfo, + RefPtr<layers::ImageContainer> aImageContainer, + RefPtr<layers::KnowsCompositor> aKnowsCompositor) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mIsShutdown) { + return MediaDataDecoder::InitPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + } + + // The Widevine CDM version 1.4.8.970 and above contain a video decoder that + // does not optimally allocate video frames; it requests buffers much larger + // than required. The exact formula the CDM uses to calculate their frame + // sizes isn't obvious, but they normally request around or slightly more + // than 1.5X the optimal amount. So pad the size of buffers we allocate so + // that we're likely to have buffers big enough to accomodate the CDM's weird + // frame size calculation. + const size_t bufferSize = + 1.7 * I420FrameBufferSizePadded(aInfo.mImage.width, aInfo.mImage.height); + if (bufferSize <= 0) { + return MediaDataDecoder::InitPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("Video frame buffer size is invalid.")), + __func__); + } + + if (!EnsureSufficientShmems(bufferSize)) { + return MediaDataDecoder::InitPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("Failed to init shmems for video decoder")), + __func__); + } + + if (!SendInitializeVideoDecoder(aConfig)) { + return MediaDataDecoder::InitPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("Failed to send init video decoder to CDM")), + __func__); + } + + mMaxRefFrames = (aConfig.mCodec() == cdm::VideoCodec::kCodecH264) + ? H264::HasSPS(aInfo.mExtraData) + ? H264::ComputeMaxRefFrames(aInfo.mExtraData) + : 16 + : 0; + + mVideoDecoderInitialized = true; + mImageContainer = aImageContainer; + mKnowsCompositor = aKnowsCompositor; + mVideoInfo = aInfo; + mVideoFrameBufferSize = bufferSize; + + return mInitVideoDecoderPromise.Ensure(__func__); +} + +ipc::IPCResult ChromiumCDMParent::RecvOnDecoderInitDone( + const uint32_t& aStatus) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "ChromiumCDMParent::RecvOnDecoderInitDone(this=%p, status=%" PRIu32 ")", + this, aStatus); + if (mIsShutdown) { + MOZ_ASSERT(mInitVideoDecoderPromise.IsEmpty()); + return IPC_OK(); + } + if (aStatus == static_cast<uint32_t>(cdm::kSuccess)) { + mInitVideoDecoderPromise.ResolveIfExists(TrackInfo::kVideoTrack, __func__); + } else { + mVideoDecoderInitialized = false; + mInitVideoDecoderPromise.RejectIfExists( + MediaResult( + NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("CDM init decode failed with status %s (%" PRIu32 ")", + CdmStatusToString(aStatus), aStatus)), + __func__); + } + return IPC_OK(); +} + +RefPtr<MediaDataDecoder::DecodePromise> +ChromiumCDMParent::DecryptAndDecodeFrame(MediaRawData* aSample) { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mIsShutdown) { + return MediaDataDecoder::DecodePromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + } + + GMP_LOG_DEBUG("ChromiumCDMParent::DecryptAndDecodeFrame t=%" PRId64, + aSample->mTime.ToMicroseconds()); + + CDMInputBuffer buffer; + + if (!InitCDMInputBuffer(buffer, aSample)) { + return MediaDataDecoder::DecodePromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Failed to init CDM buffer."), + __func__); + } + + mLastStreamOffset = aSample->mOffset; + + if (!SendDecryptAndDecodeFrame(buffer)) { + GMP_LOG_DEBUG( + "ChromiumCDMParent::Decrypt(this=%p) failed to send decrypt message.", + this); + DeallocShmem(buffer.mData()); + return MediaDataDecoder::DecodePromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + "Failed to send decrypt to CDM process."), + __func__); + } + + return mDecodePromise.Ensure(__func__); +} + +RefPtr<MediaDataDecoder::FlushPromise> ChromiumCDMParent::FlushVideoDecoder() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mIsShutdown) { + MOZ_ASSERT(mReorderQueue.IsEmpty()); + return MediaDataDecoder::FlushPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + } + + mReorderQueue.Clear(); + + mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__); + if (!SendResetVideoDecoder()) { + return MediaDataDecoder::FlushPromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + "Failed to send flush to CDM."), + __func__); + } + return mFlushDecoderPromise.Ensure(__func__); +} + +ipc::IPCResult ChromiumCDMParent::RecvResetVideoDecoderComplete() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + MOZ_ASSERT(mReorderQueue.IsEmpty()); + if (mIsShutdown) { + MOZ_ASSERT(mFlushDecoderPromise.IsEmpty()); + return IPC_OK(); + } + mFlushDecoderPromise.ResolveIfExists(true, __func__); + return IPC_OK(); +} + +RefPtr<MediaDataDecoder::DecodePromise> ChromiumCDMParent::Drain() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + MOZ_ASSERT(mDecodePromise.IsEmpty(), "Must wait for decoding to complete"); + if (mIsShutdown) { + return MediaDataDecoder::DecodePromise::CreateAndReject( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + } + + RefPtr<MediaDataDecoder::DecodePromise> p = mDecodePromise.Ensure(__func__); + if (!SendDrain()) { + mDecodePromise.Resolve(MediaDataDecoder::DecodedData(), __func__); + } + return p; +} + +ipc::IPCResult ChromiumCDMParent::RecvDrainComplete() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mIsShutdown) { + MOZ_ASSERT(mDecodePromise.IsEmpty()); + return IPC_OK(); + } + + MediaDataDecoder::DecodedData samples; + while (!mReorderQueue.IsEmpty()) { + samples.AppendElement(mReorderQueue.Pop()); + } + + mDecodePromise.ResolveIfExists(std::move(samples), __func__); + return IPC_OK(); +} +RefPtr<ShutdownPromise> ChromiumCDMParent::ShutdownVideoDecoder() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + if (mIsShutdown || !mVideoDecoderInitialized) { + return ShutdownPromise::CreateAndResolve(true, __func__); + } + mInitVideoDecoderPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, + __func__); + mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__); + MOZ_ASSERT(mFlushDecoderPromise.IsEmpty()); + if (!SendDeinitializeVideoDecoder()) { + return ShutdownPromise::CreateAndResolve(true, __func__); + } + mVideoDecoderInitialized = false; + + GMP_LOG_DEBUG("ChromiumCDMParent::~ShutdownVideoDecoder(this=%p) ", this); + + // The ChromiumCDMChild will purge its shmems, so if the decoder is + // reinitialized the shmems need to be re-allocated, and they may need + // to be a different size. + mVideoShmemsActive = 0; + mVideoFrameBufferSize = 0; + return ShutdownPromise::CreateAndResolve(true, __func__); +} + +void ChromiumCDMParent::Shutdown() { + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); + GMP_LOG_DEBUG("ChromiumCDMParent::Shutdown(this=%p)", this); + + if (mIsShutdown) { + return; + } + mIsShutdown = true; + + // If we're shutting down due to the plugin shutting down due to application + // shutdown, we should tell the CDM proxy to also shutdown. Otherwise the + // proxy will shutdown when the owning MediaKeys is destroyed during cycle + // collection, and that will not shut down cleanly as the GMP thread will be + // shutdown by then. + if (mCDMCallback) { + mCDMCallback->Shutdown(); + } + + // We may be called from a task holding the last reference to the CDM + // callback, so let's clear our local weak pointer to ensure it will not be + // used afterward (including from an already-queued task, e.g.: ActorDestroy). + mCDMCallback = nullptr; + + mReorderQueue.Clear(); + + for (RefPtr<DecryptJob>& decrypt : mDecrypts) { + decrypt->PostResult(eme::AbortedErr); + } + mDecrypts.Clear(); + + if (mVideoDecoderInitialized && !mActorDestroyed) { + Unused << SendDeinitializeVideoDecoder(); + mVideoDecoderInitialized = false; + } + + // Note: MediaKeys rejects all outstanding promises when it initiates + // shutdown. + mPromiseToCreateSessionToken.Clear(); + + mInitPromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_ABORT_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + + mInitVideoDecoderPromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + mDecodePromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + mFlushDecoderPromise.RejectIfExists( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("ChromiumCDMParent is shutdown")), + __func__); + + if (!mActorDestroyed) { + Unused << SendDestroy(); + } +} + +} // namespace mozilla::gmp + +#undef NS_DispatchToMainThread diff --git a/dom/media/gmp/ChromiumCDMParent.h b/dom/media/gmp/ChromiumCDMParent.h new file mode 100644 index 0000000000..b6c28fb1c5 --- /dev/null +++ b/dom/media/gmp/ChromiumCDMParent.h @@ -0,0 +1,232 @@ +/* -*- 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/. */ + +#ifndef ChromiumCDMParent_h_ +#define ChromiumCDMParent_h_ + +#include "DecryptJob.h" +#include "GMPCrashHelper.h" +#include "GMPCrashHelperHolder.h" +#include "GMPMessageUtils.h" +#include "mozilla/gmp/PChromiumCDMParent.h" +#include "mozilla/RefPtr.h" +#include "nsTHashMap.h" +#include "PlatformDecoderModule.h" +#include "ImageContainer.h" +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" +#include "ReorderQueue.h" + +class ChromiumCDMCallback; + +namespace mozilla { + +class ErrorResult; +class MediaRawData; +class ChromiumCDMProxy; + +namespace gmp { + +class GMPContentParent; + +/** + * ChromiumCDMParent is the content process IPC actor used to communicate with a + * CDM in the GMP process (where ChromiumCDMChild lives). All non-static + * members of this class are GMP thread only. + */ +class ChromiumCDMParent final : public PChromiumCDMParent, + public GMPCrashHelperHolder { + friend class PChromiumCDMParent; + + public: + typedef MozPromise<bool, MediaResult, /* IsExclusive = */ true> InitPromise; + + // Mark AddRef and Release as `final`, as they overload pure virtual + // implementations in PChromiumCDMParent. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ChromiumCDMParent, final) + + ChromiumCDMParent(GMPContentParent* aContentParent, uint32_t aPluginId); + + uint32_t PluginId() const { return mPluginId; } + + RefPtr<InitPromise> Init(ChromiumCDMCallback* aCDMCallback, + bool aAllowDistinctiveIdentifier, + bool aAllowPersistentState, + nsIEventTarget* aMainThread); + + void CreateSession(uint32_t aCreateSessionToken, uint32_t aSessionType, + uint32_t aInitDataType, uint32_t aPromiseId, + const nsTArray<uint8_t>& aInitData); + + void LoadSession(uint32_t aPromiseId, uint32_t aSessionType, + nsString aSessionId); + + void SetServerCertificate(uint32_t aPromiseId, + const nsTArray<uint8_t>& aCert); + + void UpdateSession(const nsCString& aSessionId, uint32_t aPromiseId, + const nsTArray<uint8_t>& aResponse); + + void CloseSession(const nsCString& aSessionId, uint32_t aPromiseId); + + void RemoveSession(const nsCString& aSessionId, uint32_t aPromiseId); + + // Notifies this parent of the current output protection status. This will + // update cached status and resolve outstanding queries from the CDM if one + // exists. + void NotifyOutputProtectionStatus(bool aSuccess, uint32_t aLinkMask, + uint32_t aProtectionMask); + + void GetStatusForPolicy(uint32_t aPromiseId, + const nsCString& aMinHdcpVersion); + + RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample); + + // TODO: Add functions for clients to send data to CDM, and + // a Close() function. + RefPtr<MediaDataDecoder::InitPromise> InitializeVideoDecoder( + const gmp::CDMVideoDecoderConfig& aConfig, const VideoInfo& aInfo, + RefPtr<layers::ImageContainer> aImageContainer, + RefPtr<layers::KnowsCompositor> aKnowsCompositor); + + RefPtr<MediaDataDecoder::DecodePromise> DecryptAndDecodeFrame( + MediaRawData* aSample); + + RefPtr<MediaDataDecoder::FlushPromise> FlushVideoDecoder(); + + RefPtr<MediaDataDecoder::DecodePromise> Drain(); + + RefPtr<ShutdownPromise> ShutdownVideoDecoder(); + + void Shutdown(); + + protected: + ~ChromiumCDMParent() = default; + + ipc::IPCResult Recv__delete__() override; + ipc::IPCResult RecvOnResolvePromiseWithKeyStatus(const uint32_t& aPromiseId, + const uint32_t& aKeyStatus); + ipc::IPCResult RecvOnResolveNewSessionPromise(const uint32_t& aPromiseId, + const nsCString& aSessionId); + ipc::IPCResult RecvResolveLoadSessionPromise(const uint32_t& aPromiseId, + const bool& aSuccessful); + ipc::IPCResult RecvOnResolvePromise(const uint32_t& aPromiseId); + ipc::IPCResult RecvOnRejectPromise(const uint32_t& aPromiseId, + const uint32_t& aError, + const uint32_t& aSystemCode, + const nsCString& aErrorMessage); + ipc::IPCResult RecvOnSessionMessage(const nsCString& aSessionId, + const uint32_t& aMessageType, + nsTArray<uint8_t>&& aMessage); + ipc::IPCResult RecvOnSessionKeysChange( + const nsCString& aSessionId, nsTArray<CDMKeyInformation>&& aKeysInfo); + ipc::IPCResult RecvOnExpirationChange(const nsCString& aSessionId, + const double& aSecondsSinceEpoch); + ipc::IPCResult RecvOnSessionClosed(const nsCString& aSessionId); + ipc::IPCResult RecvOnQueryOutputProtectionStatus(); + ipc::IPCResult RecvDecrypted(const uint32_t& aId, const uint32_t& aStatus, + ipc::Shmem&& aData); + ipc::IPCResult RecvDecryptFailed(const uint32_t& aId, + const uint32_t& aStatus); + ipc::IPCResult RecvOnDecoderInitDone(const uint32_t& aStatus); + ipc::IPCResult RecvDecodedShmem(const CDMVideoFrame& aFrame, + ipc::Shmem&& aShmem); + ipc::IPCResult RecvDecodedData(const CDMVideoFrame& aFrame, + nsTArray<uint8_t>&& aData); + ipc::IPCResult RecvDecodeFailed(const uint32_t& aStatus); + ipc::IPCResult RecvShutdown(); + ipc::IPCResult RecvResetVideoDecoderComplete(); + ipc::IPCResult RecvDrainComplete(); + ipc::IPCResult RecvIncreaseShmemPoolSize(); + void ActorDestroy(ActorDestroyReason aWhy) override; + bool SendBufferToCDM(uint32_t aSizeInBytes); + + void ReorderAndReturnOutput(RefPtr<VideoData>&& aFrame); + + void RejectPromise(uint32_t aPromiseId, ErrorResult&& aException, + const nsCString& aErrorMessage); + + void ResolvePromise(uint32_t aPromiseId); + // Helpers to reject our promise if we are shut down. + void RejectPromiseShutdown(uint32_t aPromiseId); + // Helper to reject our promise with an InvalidStateError and the given + // message. + void RejectPromiseWithStateError(uint32_t aPromiseId, + const nsCString& aErrorMessage); + + // Complete the CDMs request for us to check protection status by responding + // to the CDM child with the requested info. + void CompleteQueryOutputProtectionStatus(bool aSuccess, uint32_t aLinkMask, + uint32_t aProtectionMask); + + bool InitCDMInputBuffer(gmp::CDMInputBuffer& aBuffer, MediaRawData* aSample); + + bool PurgeShmems(); + bool EnsureSufficientShmems(size_t aVideoFrameSize); + already_AddRefed<VideoData> CreateVideoFrame(const CDMVideoFrame& aFrame, + Span<uint8_t> aData); + + const uint32_t mPluginId; + GMPContentParent* mContentParent; + // Note: this pointer is a weak reference as ChromiumCDMProxy has a strong + // reference to the ChromiumCDMCallback. + ChromiumCDMCallback* mCDMCallback = nullptr; + nsTHashMap<nsUint32HashKey, uint32_t> mPromiseToCreateSessionToken; + nsTArray<RefPtr<DecryptJob>> mDecrypts; + + MozPromiseHolder<InitPromise> mInitPromise; + + MozPromiseHolder<MediaDataDecoder::InitPromise> mInitVideoDecoderPromise; + MozPromiseHolder<MediaDataDecoder::DecodePromise> mDecodePromise; + + RefPtr<layers::ImageContainer> mImageContainer; + RefPtr<layers::KnowsCompositor> mKnowsCompositor; + VideoInfo mVideoInfo; + uint64_t mLastStreamOffset = 0; + + MozPromiseHolder<MediaDataDecoder::FlushPromise> mFlushDecoderPromise; + + size_t mVideoFrameBufferSize = 0; + + // Count of the number of shmems in the set used to return decoded video + // frames from the CDM to Gecko. + uint32_t mVideoShmemsActive = 0; + // Maximum number of shmems to use to return decoded video frames. + uint32_t mVideoShmemLimit; + + // Tracks if we have an outstanding request for output protection information. + // This will be set to true if the CDM requests the information and we haven't + // yet received it from up the stack and need to query up. + bool mAwaitingOutputProtectionInformation = false; + // The cached link mask for QueryOutputProtectionStatus related calls. If + // this isn't set we'll call up the stack to MediaKeys to request the + // information, otherwise we'll use the cached value and rely on MediaKeys + // to notify us if the mask changes. + Maybe<uint32_t> mOutputProtectionLinkMask; + + bool mIsShutdown = false; + bool mVideoDecoderInitialized = false; + bool mActorDestroyed = false; + bool mAbnormalShutdown = false; + + // The H.264 decoder in Widevine CDM versions 970 and later output in decode + // order rather than presentation order, so we reorder in presentation order + // before presenting. mMaxRefFrames is non-zero if we have an initialized + // decoder and we are decoding H.264. If so, it stores the maximum length of + // the reorder queue that we need. Note we may have multiple decoders for the + // life time of this object, but never more than one active at once. + uint32_t mMaxRefFrames = 0; + ReorderQueue mReorderQueue; + +#ifdef DEBUG + // The GMP thread. Used to MOZ_ASSERT methods run on the GMP thread. + const nsCOMPtr<nsISerialEventTarget> mGMPThread; +#endif +}; + +} // namespace gmp +} // namespace mozilla + +#endif // ChromiumCDMParent_h_ diff --git a/dom/media/gmp/ChromiumCDMProxy.cpp b/dom/media/gmp/ChromiumCDMProxy.cpp new file mode 100644 index 0000000000..26d0475ad8 --- /dev/null +++ b/dom/media/gmp/ChromiumCDMProxy.cpp @@ -0,0 +1,636 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "ChromiumCDMProxy.h" +#include "ChromiumCDMCallbackProxy.h" +#include "MediaResult.h" +#include "mozilla/dom/MediaKeySession.h" +#include "GMPUtils.h" +#include "nsPrintfCString.h" +#include "GMPService.h" +#include "content_decryption_module.h" + +#define NS_DispatchToMainThread(...) CompileError_UseAbstractMainThreadInstead + +namespace mozilla { + +ChromiumCDMProxy::ChromiumCDMProxy(dom::MediaKeys* aKeys, + const nsAString& aKeySystem, + GMPCrashHelper* aCrashHelper, + bool aDistinctiveIdentifierRequired, + bool aPersistentStateRequired) + : CDMProxy(aKeys, aKeySystem, aDistinctiveIdentifierRequired, + aPersistentStateRequired), + mCrashHelper(aCrashHelper), + mCDMMutex("ChromiumCDMProxy"), + mGMPThread(GetGMPThread()) { + MOZ_ASSERT(NS_IsMainThread()); +} + +ChromiumCDMProxy::~ChromiumCDMProxy() { + EME_LOG("ChromiumCDMProxy::~ChromiumCDMProxy(this=%p)", this); +} + +void ChromiumCDMProxy::Init(PromiseId aPromiseId, const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aGMPName) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<GMPCrashHelper> helper(std::move(mCrashHelper)); + + NS_ENSURE_TRUE_VOID(!mKeys.IsNull()); + + EME_LOG("ChromiumCDMProxy::Init(this=%p, pid=%" PRIu32 + ", origin=%s, topLevelOrigin=%s, " + "gmp=%s)", + this, aPromiseId, NS_ConvertUTF16toUTF8(aOrigin).get(), + NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(), + NS_ConvertUTF16toUTF8(aGMPName).get()); + + if (!mGMPThread) { + RejectPromiseWithStateError( + aPromiseId, "Couldn't get GMP thread ChromiumCDMProxy::Init"_ns); + return; + } + + if (aGMPName.IsEmpty()) { + RejectPromiseWithStateError( + aPromiseId, nsPrintfCString("Unknown GMP for keysystem '%s'", + NS_ConvertUTF16toUTF8(mKeySystem).get())); + return; + } + + gmp::NodeIdParts nodeIdParts{nsString(aOrigin), nsString(aTopLevelOrigin), + nsString(aGMPName)}; + nsCOMPtr<nsISerialEventTarget> thread = mGMPThread; + RefPtr<ChromiumCDMProxy> self(this); + nsCString keySystem = NS_ConvertUTF16toUTF8(mKeySystem); + RefPtr<Runnable> task(NS_NewRunnableFunction( + "ChromiumCDMProxy::Init", + [self, nodeIdParts, helper, aPromiseId, thread, keySystem]() -> void { + MOZ_ASSERT(self->IsOnOwnerThread()); + + RefPtr<gmp::GeckoMediaPluginService> service = + gmp::GeckoMediaPluginService::GetGeckoMediaPluginService(); + if (!service) { + self->RejectPromiseWithStateError( + aPromiseId, + nsLiteralCString("Couldn't get GeckoMediaPluginService in " + "ChromiumCDMProxy::Init")); + return; + } + RefPtr<gmp::GetCDMParentPromise> promise = + service->GetCDM(nodeIdParts, keySystem, helper); + promise->Then( + thread, __func__, + [self, aPromiseId, thread](RefPtr<gmp::ChromiumCDMParent> cdm) { + // service->GetCDM succeeded + self->mCallback = + MakeUnique<ChromiumCDMCallbackProxy>(self, self->mMainThread); + cdm->Init(self->mCallback.get(), + self->mDistinctiveIdentifierRequired, + self->mPersistentStateRequired, self->mMainThread) + ->Then( + self->mMainThread, __func__, + [self, aPromiseId, cdm](bool /* unused */) { + // CDM init succeeded + { + MutexAutoLock lock(self->mCDMMutex); + self->mCDM = cdm; + } + if (self->mIsShutdown) { + self->RejectPromiseWithStateError( + aPromiseId, nsLiteralCString( + "ChromiumCDMProxy shutdown " + "during ChromiumCDMProxy::Init")); + // If shutdown happened while waiting to init, we + // need to explicitly shutdown the CDM to avoid it + // referencing this proxy which is on its way out. + self->ShutdownCDMIfExists(); + return; + } + self->OnCDMCreated(aPromiseId); + }, + [self, aPromiseId](MediaResult aResult) { + // CDM init failed. + ErrorResult rv; + // XXXbz MediaResult should really store a + // CopyableErrorResult or something. See + // <https://bugzilla.mozilla.org/show_bug.cgi?id=1612216>. + rv.Throw(aResult.Code()); + self->RejectPromise(aPromiseId, std::move(rv), + aResult.Message()); + }); + }, + [self, aPromiseId](MediaResult rv) { + // service->GetCDM failed + ErrorResult result; + // XXXbz MediaResult should really store a CopyableErrorResult or + // something. See + // <https://bugzilla.mozilla.org/show_bug.cgi?id=1612216>. + result.Throw(rv.Code()); + self->RejectPromise(aPromiseId, std::move(result), + rv.Description()); + }); + })); + + mGMPThread->Dispatch(task.forget()); +} + +void ChromiumCDMProxy::OnCDMCreated(uint32_t aPromiseId) { + EME_LOG("ChromiumCDMProxy::OnCDMCreated(this=%p, pid=%" PRIu32 + ") isMainThread=%d", + this, aPromiseId, NS_IsMainThread()); + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent(); + // This should only be called once the CDM has been created. + MOZ_ASSERT(cdm); + if (cdm) { + mKeys->OnCDMCreated(aPromiseId, cdm->PluginId()); + } else { + // No CDM? Shouldn't be possible, but reject the promise anyway... + constexpr auto err = "Null CDM in OnCDMCreated()"_ns; + ErrorResult rv; + rv.ThrowInvalidStateError(err); + mKeys->RejectPromise(aPromiseId, std::move(rv), err); + } +} + +void ChromiumCDMProxy::ShutdownCDMIfExists() { + EME_LOG( + "ChromiumCDMProxy::ShutdownCDMIfExists(this=%p) mCDM=%p, mIsShutdown=%s", + this, mCDM.get(), mIsShutdown ? "true" : "false"); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mGMPThread); + MOZ_ASSERT(mIsShutdown, + "Should only shutdown the CDM if the proxy is shutting down"); + RefPtr<gmp::ChromiumCDMParent> cdm; + { + MutexAutoLock lock(mCDMMutex); + cdm.swap(mCDM); + } + if (cdm) { + // We need to keep this proxy alive until the parent has finished its + // Shutdown (as it may still try to use the proxy until then). + RefPtr<ChromiumCDMProxy> self(this); + nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction( + "ChromiumCDMProxy::Shutdown", [self, cdm]() { cdm->Shutdown(); }); + mGMPThread->Dispatch(task.forget()); + } +} + +#ifdef DEBUG +bool ChromiumCDMProxy::IsOnOwnerThread() { + return mGMPThread && mGMPThread->IsOnCurrentThread(); +} +#endif + +static uint32_t ToCDMSessionType(dom::MediaKeySessionType aSessionType) { + switch (aSessionType) { + case dom::MediaKeySessionType::Temporary: + return static_cast<uint32_t>(cdm::kTemporary); + case dom::MediaKeySessionType::Persistent_license: + return static_cast<uint32_t>(cdm::kPersistentLicense); + default: + return static_cast<uint32_t>(cdm::kTemporary); + }; +}; + +static uint32_t ToCDMInitDataType(const nsAString& aInitDataType) { + if (aInitDataType.EqualsLiteral("cenc")) { + return static_cast<uint32_t>(cdm::kCenc); + } + if (aInitDataType.EqualsLiteral("webm")) { + return static_cast<uint32_t>(cdm::kWebM); + } + if (aInitDataType.EqualsLiteral("keyids")) { + return static_cast<uint32_t>(cdm::kKeyIds); + } + return static_cast<uint32_t>(cdm::kCenc); +} + +void ChromiumCDMProxy::CreateSession(uint32_t aCreateSessionToken, + dom::MediaKeySessionType aSessionType, + PromiseId aPromiseId, + const nsAString& aInitDataType, + nsTArray<uint8_t>& aInitData) { + MOZ_ASSERT(NS_IsMainThread()); + EME_LOG("ChromiumCDMProxy::CreateSession(this=%p, token=%" PRIu32 + ", type=%d, pid=%" PRIu32 + ") " + "initDataLen=%zu", + this, aCreateSessionToken, (int)aSessionType, aPromiseId, + aInitData.Length()); + + uint32_t sessionType = ToCDMSessionType(aSessionType); + uint32_t initDataType = ToCDMInitDataType(aInitDataType); + + RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent(); + if (!cdm) { + RejectPromiseWithStateError(aPromiseId, "Null CDM in CreateSession"_ns); + return; + } + + mGMPThread->Dispatch(NewRunnableMethod<uint32_t, uint32_t, uint32_t, uint32_t, + nsTArray<uint8_t>>( + "gmp::ChromiumCDMParent::CreateSession", cdm, + &gmp::ChromiumCDMParent::CreateSession, aCreateSessionToken, sessionType, + initDataType, aPromiseId, std::move(aInitData))); +} + +void ChromiumCDMProxy::LoadSession(PromiseId aPromiseId, + dom::MediaKeySessionType aSessionType, + const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent(); + if (!cdm) { + RejectPromiseWithStateError(aPromiseId, "Null CDM in LoadSession"_ns); + return; + } + + mGMPThread->Dispatch(NewRunnableMethod<uint32_t, uint32_t, nsString>( + "gmp::ChromiumCDMParent::LoadSession", cdm, + &gmp::ChromiumCDMParent::LoadSession, aPromiseId, + ToCDMSessionType(aSessionType), aSessionId)); +} + +void ChromiumCDMProxy::SetServerCertificate(PromiseId aPromiseId, + nsTArray<uint8_t>& aCert) { + MOZ_ASSERT(NS_IsMainThread()); + EME_LOG("ChromiumCDMProxy::SetServerCertificate(this=%p, pid=%" PRIu32 + ") certLen=%zu", + this, aPromiseId, aCert.Length()); + + RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent(); + if (!cdm) { + RejectPromiseWithStateError(aPromiseId, + "Null CDM in SetServerCertificate"_ns); + return; + } + + mGMPThread->Dispatch(NewRunnableMethod<uint32_t, nsTArray<uint8_t>>( + "gmp::ChromiumCDMParent::SetServerCertificate", cdm, + &gmp::ChromiumCDMParent::SetServerCertificate, aPromiseId, + std::move(aCert))); +} + +void ChromiumCDMProxy::UpdateSession(const nsAString& aSessionId, + PromiseId aPromiseId, + nsTArray<uint8_t>& aResponse) { + MOZ_ASSERT(NS_IsMainThread()); + EME_LOG("ChromiumCDMProxy::UpdateSession(this=%p, sid='%s', pid=%" PRIu32 + ") " + "responseLen=%zu", + this, NS_ConvertUTF16toUTF8(aSessionId).get(), aPromiseId, + aResponse.Length()); + + RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent(); + if (!cdm) { + RejectPromiseWithStateError(aPromiseId, "Null CDM in UpdateSession"_ns); + return; + } + mGMPThread->Dispatch( + NewRunnableMethod<nsCString, uint32_t, nsTArray<uint8_t>>( + "gmp::ChromiumCDMParent::UpdateSession", cdm, + &gmp::ChromiumCDMParent::UpdateSession, + NS_ConvertUTF16toUTF8(aSessionId), aPromiseId, std::move(aResponse))); +} + +void ChromiumCDMProxy::CloseSession(const nsAString& aSessionId, + PromiseId aPromiseId) { + MOZ_ASSERT(NS_IsMainThread()); + EME_LOG("ChromiumCDMProxy::CloseSession(this=%p, sid='%s', pid=%" PRIu32 ")", + this, NS_ConvertUTF16toUTF8(aSessionId).get(), aPromiseId); + + RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent(); + if (!cdm) { + RejectPromiseWithStateError(aPromiseId, "Null CDM in CloseSession"_ns); + return; + } + mGMPThread->Dispatch(NewRunnableMethod<nsCString, uint32_t>( + "gmp::ChromiumCDMParent::CloseSession", cdm, + &gmp::ChromiumCDMParent::CloseSession, NS_ConvertUTF16toUTF8(aSessionId), + aPromiseId)); +} + +void ChromiumCDMProxy::RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) { + MOZ_ASSERT(NS_IsMainThread()); + EME_LOG("ChromiumCDMProxy::RemoveSession(this=%p, sid='%s', pid=%" PRIu32 ")", + this, NS_ConvertUTF16toUTF8(aSessionId).get(), aPromiseId); + + RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent(); + if (!cdm) { + RejectPromiseWithStateError(aPromiseId, "Null CDM in RemoveSession"_ns); + return; + } + mGMPThread->Dispatch(NewRunnableMethod<nsCString, uint32_t>( + "gmp::ChromiumCDMParent::RemoveSession", cdm, + &gmp::ChromiumCDMParent::RemoveSession, NS_ConvertUTF16toUTF8(aSessionId), + aPromiseId)); +} + +void ChromiumCDMProxy::QueryOutputProtectionStatus() { + MOZ_ASSERT(NS_IsMainThread()); + EME_LOG("ChromiumCDMProxy::QueryOutputProtectionStatus(this=%p)", this); + + if (mKeys.IsNull()) { + EME_LOG( + "ChromiumCDMProxy::QueryOutputProtectionStatus(this=%p), mKeys " + "missing!", + this); + // If we can't get mKeys, we're probably in shutdown. But do our best to + // respond to the request and indicate the check failed. + NotifyOutputProtectionStatus(OutputProtectionCheckStatus::CheckFailed, + OutputProtectionCaptureStatus::Unused); + return; + } + // The keys will call back via `NotifyOutputProtectionStatus` to notify the + // result of the check. + mKeys->CheckIsElementCapturePossible(); +} + +void ChromiumCDMProxy::NotifyOutputProtectionStatus( + OutputProtectionCheckStatus aCheckStatus, + OutputProtectionCaptureStatus aCaptureStatus) { + MOZ_ASSERT(NS_IsMainThread()); + // If the check failed aCaptureStatus should be unused, otherwise not. + MOZ_ASSERT_IF(aCheckStatus == OutputProtectionCheckStatus::CheckFailed, + aCaptureStatus == OutputProtectionCaptureStatus::Unused); + MOZ_ASSERT_IF(aCheckStatus == OutputProtectionCheckStatus::CheckSuccessful, + aCaptureStatus != OutputProtectionCaptureStatus::Unused); + EME_LOG( + "ChromiumCDMProxy::NotifyOutputProtectionStatus(this=%p) " + "aCheckStatus=%" PRIu8 " aCaptureStatus=%" PRIu8, + this, static_cast<uint8_t>(aCheckStatus), + static_cast<uint8_t>(aCaptureStatus)); + + RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent(); + if (!cdm) { + // If we're in shutdown the CDM may have been cleared while a notification + // is in flight. If this happens outside of shutdown we have a bug. + MOZ_ASSERT(mIsShutdown); + return; + } + + uint32_t linkMask{}; + uint32_t protectionMask{}; // Unused/always zeroed. + if (aCheckStatus == OutputProtectionCheckStatus::CheckSuccessful && + aCaptureStatus == OutputProtectionCaptureStatus::CapturePossilbe) { + // The result indicates the capture is possible, so set the mask + // to indicate this. + linkMask |= cdm::OutputLinkTypes::kLinkTypeNetwork; + } + mGMPThread->Dispatch(NewRunnableMethod<bool, uint32_t, uint32_t>( + "gmp::ChromiumCDMParent::NotifyOutputProtectionStatus", cdm, + &gmp::ChromiumCDMParent::NotifyOutputProtectionStatus, + aCheckStatus == OutputProtectionCheckStatus::CheckSuccessful, linkMask, + protectionMask)); +} + +void ChromiumCDMProxy::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + EME_LOG("ChromiumCDMProxy::Shutdown(this=%p) mCDM=%p, mIsShutdown=%s", this, + mCDM.get(), mIsShutdown ? "true" : "false"); + if (mIsShutdown) { + return; + } + mIsShutdown = true; + mKeys.Clear(); + ShutdownCDMIfExists(); +} + +void ChromiumCDMProxy::RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason) { + if (!NS_IsMainThread()) { + // Use CopyableErrorResult to store our exception in the runnable, + // because ErrorResult is not OK to move across threads. + mMainThread->Dispatch( + NewRunnableMethod<PromiseId, StoreCopyPassByRRef<CopyableErrorResult>, + nsCString>( + "ChromiumCDMProxy::RejectPromise", this, + &ChromiumCDMProxy::RejectPromiseOnMainThread, aId, + std::move(aException), aReason), + NS_DISPATCH_NORMAL); + return; + } + EME_LOG("ChromiumCDMProxy::RejectPromise(this=%p, pid=%" PRIu32 + ", code=0x%x, " + "reason='%s')", + this, aId, aException.ErrorCodeAsInt(), aReason.get()); + if (!mKeys.IsNull()) { + mKeys->RejectPromise(aId, std::move(aException), aReason); + } else { + // We don't have a MediaKeys object to pass the exception to, so silence + // the exception to avoid it asserting due to being unused. + aException.SuppressException(); + } +} + +void ChromiumCDMProxy::RejectPromiseWithStateError(PromiseId aId, + const nsCString& aReason) { + ErrorResult rv; + rv.ThrowInvalidStateError(aReason); + RejectPromise(aId, std::move(rv), aReason); +} + +void ChromiumCDMProxy::RejectPromiseOnMainThread( + PromiseId aId, CopyableErrorResult&& aException, const nsCString& aReason) { + // Moving into or out of a non-copyable ErrorResult will assert that both + // ErorResults are from our current thread. Avoid the assertion by moving + // into a current-thread CopyableErrorResult first. Note that this is safe, + // because CopyableErrorResult never holds state that can't move across + // threads. + CopyableErrorResult rv(std::move(aException)); + RejectPromise(aId, std::move(rv), aReason); +} + +void ChromiumCDMProxy::ResolvePromise(PromiseId aId) { + if (!NS_IsMainThread()) { + mMainThread->Dispatch( + NewRunnableMethod<PromiseId>("ChromiumCDMProxy::ResolvePromise", this, + &ChromiumCDMProxy::ResolvePromise, aId), + NS_DISPATCH_NORMAL); + return; + } + + EME_LOG("ChromiumCDMProxy::ResolvePromise(this=%p, pid=%" PRIu32 ")", this, + aId); + if (!mKeys.IsNull()) { + mKeys->ResolvePromise(aId); + } else { + NS_WARNING("ChromiumCDMProxy unable to resolve promise!"); + } +} + +void ChromiumCDMProxy::OnSetSessionId(uint32_t aCreateSessionToken, + const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + EME_LOG("ChromiumCDMProxy::OnSetSessionId(this=%p, token=%" PRIu32 + ", sid='%s')", + this, aCreateSessionToken, NS_ConvertUTF16toUTF8(aSessionId).get()); + + if (mKeys.IsNull()) { + return; + } + RefPtr<dom::MediaKeySession> session( + mKeys->GetPendingSession(aCreateSessionToken)); + if (session) { + session->SetSessionId(aSessionId); + } +} + +void ChromiumCDMProxy::OnResolveLoadSessionPromise(uint32_t aPromiseId, + bool aSuccess) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + mKeys->OnSessionLoaded(aPromiseId, aSuccess); +} + +void ChromiumCDMProxy::OnResolvePromiseWithKeyStatus( + uint32_t aPromiseId, dom::MediaKeyStatus aKeyStatus) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + mKeys->ResolvePromiseWithKeyStatus(aPromiseId, aKeyStatus); +} + +void ChromiumCDMProxy::OnSessionMessage(const nsAString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray<uint8_t>& aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyMessage(aMessageType, aMessage); + } +} + +void ChromiumCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyStatusesChange(); + } +} + +void ChromiumCDMProxy::OnExpirationChange(const nsAString& aSessionId, + UnixTime aExpiryTime) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId)); + if (session) { + // Expiry of 0 is interpreted as "never expire". See bug 1345341. + double t = (aExpiryTime == 0) ? std::numeric_limits<double>::quiet_NaN() + : static_cast<double>(aExpiryTime); + session->SetExpiration(t); + } +} + +void ChromiumCDMProxy::OnSessionClosed(const nsAString& aSessionId) { + MOZ_ASSERT(NS_IsMainThread()); + + bool keyStatusesChange = false; + { + auto caps = Capabilites().Lock(); + keyStatusesChange = caps->RemoveKeysForSession(nsString(aSessionId)); + } + if (keyStatusesChange) { + OnKeyStatusesChange(aSessionId); + } + if (mKeys.IsNull()) { + return; + } + RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId)); + if (session) { + session->OnClosed(); + } +} + +void ChromiumCDMProxy::OnDecrypted(uint32_t aId, DecryptStatus aResult, + const nsTArray<uint8_t>& aDecryptedData) {} + +void ChromiumCDMProxy::OnSessionError(const nsAString& aSessionId, + nsresult aException, uint32_t aSystemCode, + const nsAString& aMsg) { + MOZ_ASSERT(NS_IsMainThread()); + if (mKeys.IsNull()) { + return; + } + RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId)); + if (session) { + session->DispatchKeyError(aSystemCode); + } + LogToConsole(aMsg); +} + +void ChromiumCDMProxy::OnRejectPromise(uint32_t aPromiseId, + ErrorResult&& aException, + const nsCString& aMsg) { + MOZ_ASSERT(NS_IsMainThread()); + RejectPromise(aPromiseId, std::move(aException), aMsg); +} + +RefPtr<DecryptPromise> ChromiumCDMProxy::Decrypt(MediaRawData* aSample) { + RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent(); + if (!cdm) { + return DecryptPromise::CreateAndReject( + DecryptResult(eme::AbortedErr, aSample), __func__); + } + RefPtr<MediaRawData> sample = aSample; + return InvokeAsync(mGMPThread, __func__, + [cdm, sample]() { return cdm->Decrypt(sample); }); +} + +void ChromiumCDMProxy::GetStatusForPolicy(PromiseId aPromiseId, + const nsAString& aMinHdcpVersion) { + MOZ_ASSERT(NS_IsMainThread()); + EME_LOG("ChromiumCDMProxy::GetStatusForPolicy(this=%p, pid=%" PRIu32 + ") minHdcpVersion=%s", + this, aPromiseId, NS_ConvertUTF16toUTF8(aMinHdcpVersion).get()); + + RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent(); + if (!cdm) { + RejectPromiseWithStateError(aPromiseId, + "Null CDM in GetStatusForPolicy"_ns); + return; + } + + mGMPThread->Dispatch(NewRunnableMethod<uint32_t, nsCString>( + "gmp::ChromiumCDMParent::GetStatusForPolicy", cdm, + &gmp::ChromiumCDMParent::GetStatusForPolicy, aPromiseId, + NS_ConvertUTF16toUTF8(aMinHdcpVersion))); +} + +void ChromiumCDMProxy::Terminated() { + if (!mKeys.IsNull()) { + mKeys->Terminated(); + } +} + +already_AddRefed<gmp::ChromiumCDMParent> ChromiumCDMProxy::GetCDMParent() { + MutexAutoLock lock(mCDMMutex); + RefPtr<gmp::ChromiumCDMParent> cdm = mCDM; + return cdm.forget(); +} + +} // namespace mozilla + +#undef NS_DispatchToMainThread diff --git a/dom/media/gmp/ChromiumCDMProxy.h b/dom/media/gmp/ChromiumCDMProxy.h new file mode 100644 index 0000000000..7f60153ae7 --- /dev/null +++ b/dom/media/gmp/ChromiumCDMProxy.h @@ -0,0 +1,136 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef ChromiumCDMProxy_h_ +#define ChromiumCDMProxy_h_ + +#include "mozilla/AbstractThread.h" +#include "mozilla/CDMProxy.h" +#include "ChromiumCDMParent.h" + +namespace mozilla { + +class ErrorResult; +class MediaRawData; +class DecryptJob; +class ChromiumCDMCallbackProxy; +class ChromiumCDMProxy : public CDMProxy { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ChromiumCDMProxy, override) + + ChromiumCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem, + GMPCrashHelper* aCrashHelper, + bool aAllowDistinctiveIdentifier, + bool aAllowPersistentState); + + void Init(PromiseId aPromiseId, const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aGMPName) override; + + void CreateSession(uint32_t aCreateSessionToken, + dom::MediaKeySessionType aSessionType, + PromiseId aPromiseId, const nsAString& aInitDataType, + nsTArray<uint8_t>& aInitData) override; + + void LoadSession(PromiseId aPromiseId, dom::MediaKeySessionType aSessionType, + const nsAString& aSessionId) override; + + void SetServerCertificate(PromiseId aPromiseId, + nsTArray<uint8_t>& aCert) override; + + void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId, + nsTArray<uint8_t>& aResponse) override; + + void CloseSession(const nsAString& aSessionId, PromiseId aPromiseId) override; + + void RemoveSession(const nsAString& aSessionId, + PromiseId aPromiseId) override; + + void QueryOutputProtectionStatus() override; + + void NotifyOutputProtectionStatus( + OutputProtectionCheckStatus aCheckStatus, + OutputProtectionCaptureStatus aCaptureStatus) override; + + void Shutdown() override; + + void Terminated() override; + + void OnSetSessionId(uint32_t aCreateSessionToken, + const nsAString& aSessionId) override; + + void OnResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) override; + + void OnSessionMessage(const nsAString& aSessionId, + dom::MediaKeyMessageType aMessageType, + const nsTArray<uint8_t>& aMessage) override; + + void OnExpirationChange(const nsAString& aSessionId, + UnixTime aExpiryTime) override; + + void OnSessionClosed(const nsAString& aSessionId) override; + + void OnSessionError(const nsAString& aSessionId, nsresult aException, + uint32_t aSystemCode, const nsAString& aMsg) override; + + void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException, + const nsCString& aMsg) override; + + RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample) override; + + void OnDecrypted(uint32_t aId, DecryptStatus aResult, + const nsTArray<uint8_t>& aDecryptedData) override; + + void RejectPromise(PromiseId aId, ErrorResult&& aException, + const nsCString& aReason) override; + // Reject promise with an InvalidStateError and the given message. + void RejectPromiseWithStateError(PromiseId aId, const nsCString& aReason); + // For use for moving rejections from off-main thread. + void RejectPromiseOnMainThread(PromiseId aId, + CopyableErrorResult&& aException, + const nsCString& aReason); + + void ResolvePromise(PromiseId aId) override; + + void OnKeyStatusesChange(const nsAString& aSessionId) override; + + void GetStatusForPolicy(PromiseId aPromiseId, + const nsAString& aMinHdcpVersion) override; + +#ifdef DEBUG + bool IsOnOwnerThread() override; +#endif + + ChromiumCDMProxy* AsChromiumCDMProxy() override { return this; } + + // Threadsafe. Note this may return a reference to a shutdown + // CDM, which will fail on all operations. + already_AddRefed<gmp::ChromiumCDMParent> GetCDMParent(); + + void OnResolvePromiseWithKeyStatus(uint32_t aPromiseId, + dom::MediaKeyStatus aKeyStatus); + + private: + void OnCDMCreated(uint32_t aPromiseId); + void ShutdownCDMIfExists(); + + ~ChromiumCDMProxy(); + + // True if Shutdown() has been called. Should only be read and written on + // main thread. + bool mIsShutdown = false; + + RefPtr<GMPCrashHelper> mCrashHelper; + + Mutex mCDMMutex MOZ_UNANNOTATED; + RefPtr<gmp::ChromiumCDMParent> mCDM; + nsCOMPtr<nsISerialEventTarget> mGMPThread; + UniquePtr<ChromiumCDMCallbackProxy> mCallback; +}; + +} // namespace mozilla + +#endif // ChromiumCDMProxy_h_ diff --git a/dom/media/gmp/DecryptJob.cpp b/dom/media/gmp/DecryptJob.cpp new file mode 100644 index 0000000000..affda3668f --- /dev/null +++ b/dom/media/gmp/DecryptJob.cpp @@ -0,0 +1,46 @@ +/* -*- 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 "DecryptJob.h" +#include "mozilla/Atomics.h" + +namespace mozilla { + +static Atomic<uint32_t> sDecryptJobInstanceCount(0u); + +DecryptJob::DecryptJob(MediaRawData* aSample) + : mId(++sDecryptJobInstanceCount), mSample(aSample) {} + +RefPtr<DecryptPromise> DecryptJob::Ensure() { + return mPromise.Ensure(__func__); +} + +void DecryptJob::PostResult(DecryptStatus aResult) { + nsTArray<uint8_t> empty; + PostResult(aResult, empty); +} + +void DecryptJob::PostResult(DecryptStatus aResult, + Span<const uint8_t> aDecryptedData) { + if (aDecryptedData.Length() != mSample->Size()) { + NS_WARNING("CDM returned incorrect number of decrypted bytes"); + } + if (aResult == eme::Ok) { + UniquePtr<MediaRawDataWriter> writer(mSample->CreateWriter()); + PodCopy(writer->Data(), aDecryptedData.Elements(), + std::min<size_t>(aDecryptedData.Length(), mSample->Size())); + } else if (aResult == eme::NoKeyErr) { + NS_WARNING("CDM returned NoKeyErr"); + // We still have the encrypted sample, so we can re-enqueue it to be + // decrypted again once the key is usable again. + } else { + nsAutoCString str("CDM returned decode failure DecryptStatus="); + str.AppendInt(aResult); + NS_WARNING(str.get()); + } + mPromise.Resolve(DecryptResult(aResult, mSample), __func__); +} + +} // namespace mozilla diff --git a/dom/media/gmp/DecryptJob.h b/dom/media/gmp/DecryptJob.h new file mode 100644 index 0000000000..c4f25206e3 --- /dev/null +++ b/dom/media/gmp/DecryptJob.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef DecryptJob_h_ +#define DecryptJob_h_ + +#include "mozilla/CDMProxy.h" +#include "mozilla/Span.h" + +namespace mozilla { + +class DecryptJob { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DecryptJob) + + explicit DecryptJob(MediaRawData* aSample); + + void PostResult(DecryptStatus aResult, Span<const uint8_t> aDecryptedData); + void PostResult(DecryptStatus aResult); + + RefPtr<DecryptPromise> Ensure(); + + const uint32_t mId; + RefPtr<MediaRawData> mSample; + + private: + ~DecryptJob() = default; + MozPromiseHolder<DecryptPromise> mPromise; +}; + +} // namespace mozilla + +#endif // DecryptJob_h_ diff --git a/dom/media/gmp/GMPCallbackBase.h b/dom/media/gmp/GMPCallbackBase.h new file mode 100644 index 0000000000..7284f18e86 --- /dev/null +++ b/dom/media/gmp/GMPCallbackBase.h @@ -0,0 +1,24 @@ +/* -*- 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/. */ + +#ifndef GMPCallbackBase_h_ +#define GMPCallbackBase_h_ + +#include "nsISupportsImpl.h" + +class GMPCallbackBase { + public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual ~GMPCallbackBase() = default; + + // The GMP code will call this if the codec crashes or shuts down. It's + // expected that the consumer (destination of this callback) will respond + // by dropping their reference to the proxy, allowing the proxy/parent to + // be destroyed. + virtual void Terminated() = 0; +}; + +#endif diff --git a/dom/media/gmp/GMPChild.cpp b/dom/media/gmp/GMPChild.cpp new file mode 100644 index 0000000000..0d485345bc --- /dev/null +++ b/dom/media/gmp/GMPChild.cpp @@ -0,0 +1,657 @@ +/* -*- 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 "GMPChild.h" + +#include "base/command_line.h" +#include "base/task.h" +#include "ChildProfilerController.h" +#include "ChromiumCDMAdapter.h" +#ifdef XP_LINUX +# include "dlfcn.h" +#endif +#include "gmp-video-decode.h" +#include "gmp-video-encode.h" +#include "GMPContentChild.h" +#include "GMPLoader.h" +#include "GMPLog.h" +#include "GMPPlatform.h" +#include "GMPProcessChild.h" +#include "GMPProcessParent.h" +#include "GMPUtils.h" +#include "GMPVideoDecoderChild.h" +#include "GMPVideoEncoderChild.h" +#include "GMPVideoHost.h" +#include "mozilla/Algorithm.h" +#include "mozilla/FOGIPC.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/ipc/CrashReporterClient.h" +#include "mozilla/ipc/Endpoint.h" +#include "mozilla/ipc/ProcessChild.h" +#include "mozilla/TextUtils.h" +#include "nsDebugImpl.h" +#include "nsExceptionHandler.h" +#include "nsIFile.h" +#include "nsReadableUtils.h" +#include "nsThreadManager.h" +#include "nsXULAppAPI.h" +#include "nsIXULRuntime.h" +#include "prio.h" +#ifdef XP_WIN +# include <stdlib.h> // for _exit() +# include "WinUtils.h" +#else +# include <unistd.h> // for _exit() +#endif + +using namespace mozilla::ipc; + +namespace mozilla { + +#define GMP_CHILD_LOG_DEBUG(x, ...) \ + GMP_LOG_DEBUG("GMPChild[pid=%d] " x, (int)base::GetCurrentProcId(), \ + ##__VA_ARGS__) + +namespace gmp { + +GMPChild::GMPChild() + : mGMPMessageLoop(MessageLoop::current()), mGMPLoader(nullptr) { + GMP_CHILD_LOG_DEBUG("GMPChild ctor"); + nsDebugImpl::SetMultiprocessMode("GMP"); +} + +GMPChild::~GMPChild() { + GMP_CHILD_LOG_DEBUG("GMPChild dtor"); +#ifdef XP_LINUX + for (auto& libHandle : mLibHandles) { + dlclose(libHandle); + } +#endif +} + +bool GMPChild::Init(const nsAString& aPluginPath, + mozilla::ipc::UntypedEndpoint&& aEndpoint) { + GMP_CHILD_LOG_DEBUG("%s pluginPath=%s", __FUNCTION__, + NS_ConvertUTF16toUTF8(aPluginPath).get()); + + // GMPChild needs nsThreadManager to create the ProfilerChild thread. + // It is also used on debug builds for the sandbox tests. + if (NS_WARN_IF(NS_FAILED(nsThreadManager::get().Init()))) { + return false; + } + + if (NS_WARN_IF(!aEndpoint.Bind(this))) { + return false; + } + + CrashReporterClient::InitSingleton(this); + + mPluginPath = aPluginPath; + + return true; +} + +mozilla::ipc::IPCResult GMPChild::RecvProvideStorageId( + const nsCString& aStorageId) { + GMP_CHILD_LOG_DEBUG("%s", __FUNCTION__); + mStorageId = aStorageId; + return IPC_OK(); +} + +GMPErr GMPChild::GetAPI(const char* aAPIName, void* aHostAPI, void** aPluginAPI, + const nsACString& aKeySystem) { + if (!mGMPLoader) { + return GMPGenericErr; + } + return mGMPLoader->GetAPI(aAPIName, aHostAPI, aPluginAPI, aKeySystem); +} + +mozilla::ipc::IPCResult GMPChild::RecvPreloadLibs(const nsCString& aLibs) { + // Pre-load libraries that may need to be used by the EME plugin but that + // can't be loaded after the sandbox has started. +#ifdef XP_WIN + // Items in this must be lowercase! + constexpr static const char16_t* whitelist[] = { + u"dxva2.dll", // Get monitor information + u"evr.dll", // MFGetStrideForBitmapInfoHeader + u"freebl3.dll", // NSS for clearkey CDM + u"mfplat.dll", // MFCreateSample, MFCreateAlignedMemoryBuffer, + // MFCreateMediaType + u"msmpeg2vdec.dll", // H.264 decoder + u"nss3.dll", // NSS for clearkey CDM + u"ole32.dll", // required for OPM + u"oleaut32.dll", // For _bstr_t use in libwebrtc, see bug 1788592 + u"psapi.dll", // For GetMappedFileNameW, see bug 1383611 + u"softokn3.dll", // NSS for clearkey CDM + u"winmm.dll", // Dependency for widevine + }; + constexpr static bool (*IsASCII)(const char16_t*) = + IsAsciiNullTerminated<char16_t>; + static_assert(AllOf(std::begin(whitelist), std::end(whitelist), IsASCII), + "Items in the whitelist must not contain non-ASCII " + "characters!"); + + nsTArray<nsCString> libs; + SplitAt(", ", aLibs, libs); + for (nsCString lib : libs) { + ToLowerCase(lib); + for (const char16_t* whiteListedLib : whitelist) { + if (nsDependentString(whiteListedLib) + .EqualsASCII(lib.Data(), lib.Length())) { + LoadLibraryW(char16ptr_t(whiteListedLib)); + break; + } + } + } +#elif defined(XP_LINUX) + constexpr static const char* whitelist[] = { + // NSS libraries used by clearkey. + "libfreeblpriv3.so", + "libsoftokn3.so", + // glibc libraries merged into libc.so.6; see bug 1725828 and + // the corresponding code in GMPParent.cpp. + "libdl.so.2", + "libpthread.so.0", + "librt.so.1", + }; + + nsTArray<nsCString> libs; + SplitAt(", ", aLibs, libs); + for (const nsCString& lib : libs) { + for (const char* whiteListedLib : whitelist) { + if (lib.EqualsASCII(whiteListedLib)) { + auto libHandle = dlopen(whiteListedLib, RTLD_NOW | RTLD_GLOBAL); + if (libHandle) { + mLibHandles.AppendElement(libHandle); + } else { + // TODO(bug 1698718): remove the logging once we've identified + // the cause of the load failure. + const char* error = dlerror(); + if (error) { + // We should always have an error, but gracefully handle just in + // case. + nsAutoCString nsError{error}; + CrashReporter::AppendAppNotesToCrashReport(nsError); + } + // End bug 1698718 logging. + + MOZ_CRASH("Couldn't load lib needed by media plugin"); + } + } + } + } +#endif + return IPC_OK(); +} + +bool GMPChild::GetUTF8LibPath(nsACString& aOutLibPath) { + nsCOMPtr<nsIFile> libFile; + +#define GMP_PATH_CRASH(explain) \ + do { \ + nsAutoString path; \ + if (!libFile || NS_FAILED(libFile->GetPath(path))) { \ + path = mPluginPath; \ + } \ + CrashReporter::AnnotateCrashReport( \ + CrashReporter::Annotation::GMPLibraryPath, \ + NS_ConvertUTF16toUTF8(path)); \ + MOZ_CRASH(explain); \ + } while (false) + + nsresult rv = NS_NewLocalFile(mPluginPath, true, getter_AddRefs(libFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + GMP_PATH_CRASH("Failed to create file for plugin path"); + return false; + } + + nsCOMPtr<nsIFile> parent; + rv = libFile->GetParent(getter_AddRefs(parent)); + if (NS_WARN_IF(NS_FAILED(rv))) { + GMP_PATH_CRASH("Failed to get parent file for plugin file"); + return false; + } + + nsAutoString parentLeafName; + rv = parent->GetLeafName(parentLeafName); + if (NS_WARN_IF(NS_FAILED(rv))) { + GMP_PATH_CRASH("Failed to get leaf for plugin file"); + return false; + } + + nsAutoString baseName; + baseName = Substring(parentLeafName, 4, parentLeafName.Length() - 1); + +#if defined(XP_MACOSX) + nsAutoString binaryName = u"lib"_ns + baseName + u".dylib"_ns; +#elif defined(OS_POSIX) + nsAutoString binaryName = u"lib"_ns + baseName + u".so"_ns; +#elif defined(XP_WIN) + nsAutoString binaryName = baseName + u".dll"_ns; +#else +# error not defined +#endif + rv = libFile->AppendRelativePath(binaryName); + if (NS_WARN_IF(NS_FAILED(rv))) { + GMP_PATH_CRASH("Failed to append lib to plugin file"); + return false; + } + + if (NS_WARN_IF(!FileExists(libFile))) { + GMP_PATH_CRASH("Plugin file does not exist"); + return false; + } + + nsAutoString path; + rv = libFile->GetPath(path); + if (NS_WARN_IF(NS_FAILED(rv))) { + GMP_PATH_CRASH("Failed to get path for plugin file"); + return false; + } + + CopyUTF16toUTF8(path, aOutLibPath); + return true; +} + +static nsCOMPtr<nsIFile> AppendFile(nsCOMPtr<nsIFile>&& aFile, + const nsString& aStr) { + return (aFile && NS_SUCCEEDED(aFile->Append(aStr))) ? aFile : nullptr; +} + +static nsCOMPtr<nsIFile> CloneFile(const nsCOMPtr<nsIFile>& aFile) { + nsCOMPtr<nsIFile> clone; + return (aFile && NS_SUCCEEDED(aFile->Clone(getter_AddRefs(clone)))) ? clone + : nullptr; +} + +static nsCOMPtr<nsIFile> GetParentFile(const nsCOMPtr<nsIFile>& aFile) { + nsCOMPtr<nsIFile> parent; + return (aFile && NS_SUCCEEDED(aFile->GetParent(getter_AddRefs(parent)))) + ? parent + : nullptr; +} + +#if defined(XP_WIN) +static bool IsFileLeafEqualToASCII(const nsCOMPtr<nsIFile>& aFile, + const char* aStr) { + nsAutoString leafName; + return aFile && NS_SUCCEEDED(aFile->GetLeafName(leafName)) && + leafName.EqualsASCII(aStr); +} +#endif + +#if defined(XP_WIN) +# define FIREFOX_FILE u"firefox.exe"_ns +# define XUL_LIB_FILE u"xul.dll"_ns +#elif defined(XP_MACOSX) +# define FIREFOX_FILE u"firefox"_ns +# define XUL_LIB_FILE u"XUL"_ns +#else +# define FIREFOX_FILE u"firefox"_ns +# define XUL_LIB_FILE u"libxul.so"_ns +#endif + +static nsCOMPtr<nsIFile> GetFirefoxAppPath( + nsCOMPtr<nsIFile> aPluginContainerPath) { + MOZ_ASSERT(aPluginContainerPath); +#if defined(XP_MACOSX) + // On MacOS the firefox binary is a few parent directories up from + // plugin-container. + // aPluginContainerPath will end with something like: + // xxxx/NightlyDebug.app/Contents/MacOS/plugin-container.app/Contents/MacOS/plugin-container + nsCOMPtr<nsIFile> path = aPluginContainerPath; + for (int i = 0; i < 4; i++) { + path = GetParentFile(path); + } + return path; +#else + nsCOMPtr<nsIFile> parent = GetParentFile(aPluginContainerPath); +# if XP_WIN + if (IsFileLeafEqualToASCII(parent, "i686")) { + // We must be on Windows on ARM64, where the plugin-container path will + // be in the 'i686' subdir. The firefox.exe is in the parent directory. + parent = GetParentFile(parent); + } +# endif + return parent; +#endif +} + +#if defined(XP_MACOSX) +static bool GetSigPath(const int aRelativeLayers, + const nsString& aTargetSigFileName, + nsCOMPtr<nsIFile> aExecutablePath, + nsCOMPtr<nsIFile>& aOutSigPath) { + // The sig file will be located in + // xxxx/NightlyDebug.app/Contents/Resources/XUL.sig + // xxxx/NightlyDebug.app/Contents/Resources/firefox.sig + // xxxx/NightlyDebug.app/Contents/MacOS/plugin-container.app/Contents/Resources/plugin-container.sig + // On MacOS the sig file is a few parent directories up from + // its executable file. + // Start to search the path from the path of the executable file we provided. + MOZ_ASSERT(aExecutablePath); + nsCOMPtr<nsIFile> path = aExecutablePath; + for (int i = 0; i < aRelativeLayers; i++) { + nsCOMPtr<nsIFile> parent; + if (NS_WARN_IF(NS_FAILED(path->GetParent(getter_AddRefs(parent))))) { + return false; + } + path = parent; + } + MOZ_ASSERT(path); + aOutSigPath = path; + return NS_SUCCEEDED(path->Append(u"Resources"_ns)) && + NS_SUCCEEDED(path->Append(aTargetSigFileName)); +} +#endif + +static bool AppendHostPath(nsCOMPtr<nsIFile>& aFile, + nsTArray<std::pair<nsCString, nsCString>>& aPaths) { + nsString str; + if (!FileExists(aFile) || NS_FAILED(aFile->GetPath(str))) { + return false; + } + + nsCString filePath = NS_ConvertUTF16toUTF8(str); + nsCString sigFilePath; +#if defined(XP_MACOSX) + nsAutoString binary; + if (NS_FAILED(aFile->GetLeafName(binary))) { + return false; + } + binary.Append(u".sig"_ns); + nsCOMPtr<nsIFile> sigFile; + if (GetSigPath(2, binary, aFile, sigFile) && + NS_SUCCEEDED(sigFile->GetPath(str))) { + CopyUTF16toUTF8(str, sigFilePath); + } else { + // Cannot successfully get the sig file path. + // Assume it is located at the same place as plugin-container + // alternatively. + sigFilePath = nsCString(NS_ConvertUTF16toUTF8(str) + ".sig"_ns); + } +#else + sigFilePath = nsCString(NS_ConvertUTF16toUTF8(str) + ".sig"_ns); +#endif + aPaths.AppendElement( + std::make_pair(std::move(filePath), std::move(sigFilePath))); + return true; +} + +nsTArray<std::pair<nsCString, nsCString>> +GMPChild::MakeCDMHostVerificationPaths(const nsACString& aPluginLibPath) { + // Record the file path and its sig file path. + nsTArray<std::pair<nsCString, nsCString>> paths; + // Plugin binary path. + paths.AppendElement( + std::make_pair(nsCString(aPluginLibPath), aPluginLibPath + ".sig"_ns)); + + // Plugin-container binary path. + // Note: clang won't let us initialize an nsString from a wstring, so we + // need to go through UTF8 to get to an nsString. + const std::string pluginContainer = + WideToUTF8(CommandLine::ForCurrentProcess()->program()); + nsString str; + + CopyUTF8toUTF16(nsDependentCString(pluginContainer.c_str()), str); + nsCOMPtr<nsIFile> path; + if (NS_FAILED(NS_NewLocalFile(str, true, /* aFollowLinks */ + getter_AddRefs(path))) || + !AppendHostPath(path, paths)) { + // Without successfully determining plugin-container's path, we can't + // determine libxul's or Firefox's. So give up. + return paths; + } + +#if defined(XP_WIN) + // On Windows on ARM64, we should also append the x86 plugin-container's + // xul.dll. + const bool isWindowsOnARM64 = + IsFileLeafEqualToASCII(GetParentFile(path), "i686"); + if (isWindowsOnARM64) { + nsCOMPtr<nsIFile> x86XulPath = + AppendFile(GetParentFile(path), XUL_LIB_FILE); + if (!AppendHostPath(x86XulPath, paths)) { + return paths; + } + } +#endif + + // Firefox application binary path. + nsCOMPtr<nsIFile> appDir = GetFirefoxAppPath(path); + path = AppendFile(CloneFile(appDir), FIREFOX_FILE); + if (!AppendHostPath(path, paths)) { + return paths; + } + + // Libxul path. Note: re-using 'appDir' var here, as we assume libxul is in + // the same directory as Firefox executable. + appDir->GetPath(str); + path = AppendFile(CloneFile(appDir), XUL_LIB_FILE); + if (!AppendHostPath(path, paths)) { + return paths; + } + + return paths; +} + +static auto ToCString(const nsTArray<std::pair<nsCString, nsCString>>& aPairs) { + return StringJoin(","_ns, aPairs, [](nsACString& dest, const auto& p) { + dest.AppendPrintf("(%s,%s)", p.first.get(), p.second.get()); + }); +} + +mozilla::ipc::IPCResult GMPChild::RecvStartPlugin(const nsString& aAdapter) { + GMP_CHILD_LOG_DEBUG("%s", __FUNCTION__); + + nsAutoCString libPath; + if (!GetUTF8LibPath(libPath)) { + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::GMPLibraryPath, + NS_ConvertUTF16toUTF8(mPluginPath)); +#ifdef XP_WIN + return IPC_FAIL(this, + nsPrintfCString("Failed to get lib path with error(%lu).", + GetLastError()) + .get()); +#else + return IPC_FAIL(this, "Failed to get lib path."); +#endif + } + + auto platformAPI = new GMPPlatformAPI(); + InitPlatformAPI(*platformAPI, this); + + mGMPLoader = MakeUnique<GMPLoader>(); +#if defined(MOZ_SANDBOX) && !defined(XP_MACOSX) + if (!mGMPLoader->CanSandbox()) { + GMP_CHILD_LOG_DEBUG("%s Can't sandbox GMP, failing", __FUNCTION__); + delete platformAPI; + return IPC_FAIL(this, "Can't sandbox GMP."); + } +#endif + + GMPAdapter* adapter = nullptr; + if (aAdapter.EqualsLiteral("chromium")) { + auto&& paths = MakeCDMHostVerificationPaths(libPath); + GMP_CHILD_LOG_DEBUG("%s CDM host paths=%s", __func__, + ToCString(paths).get()); + adapter = new ChromiumCDMAdapter(std::move(paths)); + } + + if (!mGMPLoader->Load(libPath.get(), libPath.Length(), platformAPI, + adapter)) { + NS_WARNING("Failed to load GMP"); + delete platformAPI; + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::GMPLibraryPath, + NS_ConvertUTF16toUTF8(mPluginPath)); + +#ifdef XP_WIN + return IPC_FAIL(this, nsPrintfCString("Failed to load GMP with error(%lu).", + GetLastError()) + .get()); +#else + return IPC_FAIL(this, "Failed to load GMP."); +#endif + } + + return IPC_OK(); +} + +MessageLoop* GMPChild::GMPMessageLoop() { return mGMPMessageLoop; } + +void GMPChild::ActorDestroy(ActorDestroyReason aWhy) { + GMP_CHILD_LOG_DEBUG("%s reason=%d", __FUNCTION__, aWhy); + + for (uint32_t i = mGMPContentChildren.Length(); i > 0; i--) { + MOZ_ASSERT_IF(aWhy == NormalShutdown, + !mGMPContentChildren[i - 1]->IsUsed()); + mGMPContentChildren[i - 1]->Close(); + } + + if (mGMPLoader) { + mGMPLoader->Shutdown(); + } + if (AbnormalShutdown == aWhy) { + NS_WARNING("Abnormal shutdown of GMP process!"); + ProcessChild::QuickExit(); + } + + // Send the last bits of Glean data over to the main process. + glean::FlushFOGData( + [](ByteBuf&& aBuf) { glean::SendFOGData(std::move(aBuf)); }); + + if (mProfilerController) { + mProfilerController->Shutdown(); + mProfilerController = nullptr; + } + + CrashReporterClient::DestroySingleton(); + + XRE_ShutdownChildProcess(); +} + +void GMPChild::ProcessingError(Result aCode, const char* aReason) { + switch (aCode) { + case MsgDropped: + _exit(0); // Don't trigger a crash report. + case MsgNotKnown: + MOZ_CRASH("aborting because of MsgNotKnown"); + case MsgNotAllowed: + MOZ_CRASH("aborting because of MsgNotAllowed"); + case MsgPayloadError: + MOZ_CRASH("aborting because of MsgPayloadError"); + case MsgProcessingError: + MOZ_CRASH("aborting because of MsgProcessingError"); + case MsgRouteError: + MOZ_CRASH("aborting because of MsgRouteError"); + case MsgValueError: + MOZ_CRASH("aborting because of MsgValueError"); + default: + MOZ_CRASH("not reached"); + } +} + +PGMPTimerChild* GMPChild::AllocPGMPTimerChild() { + return new GMPTimerChild(this); +} + +bool GMPChild::DeallocPGMPTimerChild(PGMPTimerChild* aActor) { + MOZ_ASSERT(mTimerChild == static_cast<GMPTimerChild*>(aActor)); + mTimerChild = nullptr; + return true; +} + +GMPTimerChild* GMPChild::GetGMPTimers() { + if (!mTimerChild) { + PGMPTimerChild* sc = SendPGMPTimerConstructor(); + if (!sc) { + return nullptr; + } + mTimerChild = static_cast<GMPTimerChild*>(sc); + } + return mTimerChild; +} + +PGMPStorageChild* GMPChild::AllocPGMPStorageChild() { + return new GMPStorageChild(this); +} + +bool GMPChild::DeallocPGMPStorageChild(PGMPStorageChild* aActor) { + mStorage = nullptr; + return true; +} + +GMPStorageChild* GMPChild::GetGMPStorage() { + if (!mStorage) { + PGMPStorageChild* sc = SendPGMPStorageConstructor(); + if (!sc) { + return nullptr; + } + mStorage = static_cast<GMPStorageChild*>(sc); + } + return mStorage; +} + +mozilla::ipc::IPCResult GMPChild::RecvCrashPluginNow() { + MOZ_CRASH(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPChild::RecvCloseActive() { + for (uint32_t i = mGMPContentChildren.Length(); i > 0; i--) { + mGMPContentChildren[i - 1]->CloseActive(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPChild::RecvInitGMPContentChild( + Endpoint<PGMPContentChild>&& aEndpoint) { + GMPContentChild* child = + mGMPContentChildren.AppendElement(new GMPContentChild(this))->get(); + aEndpoint.Bind(child); + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPChild::RecvFlushFOGData( + FlushFOGDataResolver&& aResolver) { + GMP_CHILD_LOG_DEBUG("GMPChild RecvFlushFOGData"); + glean::FlushFOGData(std::move(aResolver)); + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPChild::RecvTestTriggerMetrics( + TestTriggerMetricsResolver&& aResolve) { + GMP_CHILD_LOG_DEBUG("GMPChild RecvTestTriggerMetrics"); + mozilla::glean::test_only_ipc::a_counter.Add( + nsIXULRuntime::PROCESS_TYPE_GMPLUGIN); + aResolve(true); + return IPC_OK(); +} + +void GMPChild::GMPContentChildActorDestroy(GMPContentChild* aGMPContentChild) { + for (uint32_t i = mGMPContentChildren.Length(); i > 0; i--) { + RefPtr<GMPContentChild>& destroyedActor = mGMPContentChildren[i - 1]; + if (destroyedActor.get() == aGMPContentChild) { + SendPGMPContentChildDestroyed(); + mGMPContentChildren.RemoveElementAt(i - 1); + break; + } + } +} + +mozilla::ipc::IPCResult GMPChild::RecvInitProfiler( + Endpoint<PProfilerChild>&& aEndpoint) { + mProfilerController = + mozilla::ChildProfilerController::Create(std::move(aEndpoint)); + return IPC_OK(); +} + +} // namespace gmp +} // namespace mozilla + +#undef GMP_CHILD_LOG_DEBUG +#undef __CLASS__ diff --git a/dom/media/gmp/GMPChild.h b/dom/media/gmp/GMPChild.h new file mode 100644 index 0000000000..7201833501 --- /dev/null +++ b/dom/media/gmp/GMPChild.h @@ -0,0 +1,106 @@ +/* -*- 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/. */ + +#ifndef GMPChild_h_ +#define GMPChild_h_ + +#include "mozilla/gmp/PGMPChild.h" +#include "GMPTimerChild.h" +#include "GMPStorageChild.h" +#include "GMPLoader.h" +#include "gmp-entrypoints.h" +#include "prlink.h" + +namespace mozilla { + +class ChildProfilerController; + +namespace gmp { + +class GMPContentChild; + +class GMPChild : public PGMPChild { + friend class PGMPChild; + + public: + NS_INLINE_DECL_REFCOUNTING(GMPChild, override) + + GMPChild(); + + bool Init(const nsAString& aPluginPath, + mozilla::ipc::UntypedEndpoint&& aEndpoint); + MessageLoop* GMPMessageLoop(); + + // Main thread only. + GMPTimerChild* GetGMPTimers(); + GMPStorageChild* GetGMPStorage(); + +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) + bool SetMacSandboxInfo(bool aAllowWindowServer); +#endif + + private: + friend class GMPContentChild; + + virtual ~GMPChild(); + + bool GetUTF8LibPath(nsACString& aOutLibPath); + + mozilla::ipc::IPCResult RecvProvideStorageId(const nsCString& aStorageId); + + mozilla::ipc::IPCResult RecvStartPlugin(const nsString& aAdapter); + mozilla::ipc::IPCResult RecvPreloadLibs(const nsCString& aLibs); + + PGMPTimerChild* AllocPGMPTimerChild(); + bool DeallocPGMPTimerChild(PGMPTimerChild* aActor); + + PGMPStorageChild* AllocPGMPStorageChild(); + bool DeallocPGMPStorageChild(PGMPStorageChild* aActor); + + void GMPContentChildActorDestroy(GMPContentChild* aGMPContentChild); + + mozilla::ipc::IPCResult RecvCrashPluginNow(); + mozilla::ipc::IPCResult RecvCloseActive(); + + mozilla::ipc::IPCResult RecvInitGMPContentChild( + Endpoint<PGMPContentChild>&& aEndpoint); + + mozilla::ipc::IPCResult RecvFlushFOGData(FlushFOGDataResolver&& aResolver); + + mozilla::ipc::IPCResult RecvTestTriggerMetrics( + TestTriggerMetricsResolver&& aResolve); + + mozilla::ipc::IPCResult RecvInitProfiler( + Endpoint<mozilla::PProfilerChild>&& aEndpoint); + + void ActorDestroy(ActorDestroyReason aWhy) override; + void ProcessingError(Result aCode, const char* aReason) override; + + GMPErr GetAPI(const char* aAPIName, void* aHostAPI, void** aPluginAPI, + const nsACString& aKeySystem = ""_ns); + + nsTArray<std::pair<nsCString, nsCString>> MakeCDMHostVerificationPaths( + const nsACString& aPluginLibPath); + + nsTArray<RefPtr<GMPContentChild>> mGMPContentChildren; + + RefPtr<GMPTimerChild> mTimerChild; + RefPtr<GMPStorageChild> mStorage; + + RefPtr<ChildProfilerController> mProfilerController; + + MessageLoop* mGMPMessageLoop; + nsString mPluginPath; + nsCString mStorageId; + UniquePtr<GMPLoader> mGMPLoader; +#ifdef XP_LINUX + nsTArray<void*> mLibHandles; +#endif +}; + +} // namespace gmp +} // namespace mozilla + +#endif // GMPChild_h_ diff --git a/dom/media/gmp/GMPContentChild.cpp b/dom/media/gmp/GMPContentChild.cpp new file mode 100644 index 0000000000..85fc6e7a70 --- /dev/null +++ b/dom/media/gmp/GMPContentChild.cpp @@ -0,0 +1,131 @@ +/* -*- 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 "GMPContentChild.h" +#include "GMPChild.h" +#include "GMPVideoDecoderChild.h" +#include "GMPVideoEncoderChild.h" +#include "ChromiumCDMChild.h" +#include "base/task.h" +#include "GMPUtils.h" + +namespace mozilla::gmp { + +MessageLoop* GMPContentChild::GMPMessageLoop() { + return mGMPChild->GMPMessageLoop(); +} + +void GMPContentChild::CheckThread() { + MOZ_ASSERT(mGMPChild->mGMPMessageLoop == MessageLoop::current()); +} + +#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS) +mozilla::ipc::IPCResult GMPContentChild::RecvInitSandboxTesting( + Endpoint<PSandboxTestingChild>&& aEndpoint) { + if (!SandboxTestingChild::Initialize(std::move(aEndpoint))) { + return IPC_FAIL( + this, "InitSandboxTesting failed to initialise the child process."); + } + return IPC_OK(); +} +#endif + +void GMPContentChild::ActorDestroy(ActorDestroyReason aWhy) { + mGMPChild->GMPContentChildActorDestroy(this); +} + +void GMPContentChild::ProcessingError(Result aCode, const char* aReason) { + mGMPChild->ProcessingError(aCode, aReason); +} + +already_AddRefed<PGMPVideoDecoderChild> +GMPContentChild::AllocPGMPVideoDecoderChild() { + return MakeAndAddRef<GMPVideoDecoderChild>(this); +} + +already_AddRefed<PGMPVideoEncoderChild> +GMPContentChild::AllocPGMPVideoEncoderChild() { + return MakeAndAddRef<GMPVideoEncoderChild>(this); +} + +already_AddRefed<PChromiumCDMChild> GMPContentChild::AllocPChromiumCDMChild( + const nsACString& aKeySystem) { + return MakeAndAddRef<ChromiumCDMChild>(this); +} + +mozilla::ipc::IPCResult GMPContentChild::RecvPGMPVideoDecoderConstructor( + PGMPVideoDecoderChild* aActor) { + auto vdc = static_cast<GMPVideoDecoderChild*>(aActor); + + void* vd = nullptr; + GMPErr err = mGMPChild->GetAPI(GMP_API_VIDEO_DECODER, &vdc->Host(), &vd); + if (err != GMPNoErr || !vd) { + return IPC_FAIL(this, "GMPGetAPI call failed trying to construct decoder."); + } + + vdc->Init(static_cast<GMPVideoDecoder*>(vd)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPContentChild::RecvPGMPVideoEncoderConstructor( + PGMPVideoEncoderChild* aActor) { + auto vec = static_cast<GMPVideoEncoderChild*>(aActor); + + void* ve = nullptr; + GMPErr err = mGMPChild->GetAPI(GMP_API_VIDEO_ENCODER, &vec->Host(), &ve); + if (err != GMPNoErr || !ve) { + return IPC_FAIL(this, "GMPGetAPI call failed trying to construct encoder."); + } + + vec->Init(static_cast<GMPVideoEncoder*>(ve)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPContentChild::RecvPChromiumCDMConstructor( + PChromiumCDMChild* aActor, const nsACString& aKeySystem) { + ChromiumCDMChild* child = static_cast<ChromiumCDMChild*>(aActor); + cdm::Host_10* host10 = child; + + void* cdm = nullptr; + GMPErr err = mGMPChild->GetAPI(CHROMIUM_CDM_API, host10, &cdm, aKeySystem); + if (err != GMPNoErr || !cdm) { + return IPC_FAIL(this, "GMPGetAPI call failed trying to get CDM."); + } + + child->Init(static_cast<cdm::ContentDecryptionModule_10*>(cdm), + mGMPChild->mStorageId); + + return IPC_OK(); +} + +void GMPContentChild::CloseActive() { + // Invalidate and remove any remaining API objects. + const ManagedContainer<PGMPVideoDecoderChild>& videoDecoders = + ManagedPGMPVideoDecoderChild(); + for (const auto& key : videoDecoders) { + key->SendShutdown(); + } + + const ManagedContainer<PGMPVideoEncoderChild>& videoEncoders = + ManagedPGMPVideoEncoderChild(); + for (const auto& key : videoEncoders) { + key->SendShutdown(); + } + + const ManagedContainer<PChromiumCDMChild>& cdms = ManagedPChromiumCDMChild(); + for (const auto& key : cdms) { + key->SendShutdown(); + } +} + +bool GMPContentChild::IsUsed() { + return !ManagedPGMPVideoDecoderChild().IsEmpty() || + !ManagedPGMPVideoEncoderChild().IsEmpty() || + !ManagedPChromiumCDMChild().IsEmpty(); +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPContentChild.h b/dom/media/gmp/GMPContentChild.h new file mode 100644 index 0000000000..a31b48b00f --- /dev/null +++ b/dom/media/gmp/GMPContentChild.h @@ -0,0 +1,66 @@ +/* -*- 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/. */ + +#ifndef GMPContentChild_h_ +#define GMPContentChild_h_ + +#include "mozilla/gmp/PGMPContentChild.h" +#include "GMPSharedMemManager.h" + +#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS) +# include "mozilla/SandboxTestingChild.h" +#endif + +namespace mozilla::gmp { + +class GMPChild; + +class GMPContentChild : public PGMPContentChild, public GMPSharedMem { + public: + // Mark AddRef and Release as `final`, as they overload pure virtual + // implementations in PGMPContentChild. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPContentChild, final) + + explicit GMPContentChild(GMPChild* aChild) : mGMPChild(aChild) {} + + MessageLoop* GMPMessageLoop(); + + mozilla::ipc::IPCResult RecvPGMPVideoDecoderConstructor( + PGMPVideoDecoderChild* aActor) override; + mozilla::ipc::IPCResult RecvPGMPVideoEncoderConstructor( + PGMPVideoEncoderChild* aActor) override; + mozilla::ipc::IPCResult RecvPChromiumCDMConstructor( + PChromiumCDMChild* aActor, const nsACString& aKeySystem) override; + + already_AddRefed<PGMPVideoDecoderChild> AllocPGMPVideoDecoderChild(); + + already_AddRefed<PGMPVideoEncoderChild> AllocPGMPVideoEncoderChild(); + + already_AddRefed<PChromiumCDMChild> AllocPChromiumCDMChild( + const nsACString& aKeySystem); + +#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS) + mozilla::ipc::IPCResult RecvInitSandboxTesting( + Endpoint<PSandboxTestingChild>&& aEndpoint); +#endif + + void ActorDestroy(ActorDestroyReason aWhy) override; + void ProcessingError(Result aCode, const char* aReason) override; + + // GMPSharedMem + void CheckThread() override; + + void CloseActive(); + bool IsUsed(); + + GMPChild* mGMPChild; + + private: + ~GMPContentChild() = default; +}; + +} // namespace mozilla::gmp + +#endif // GMPContentChild_h_ diff --git a/dom/media/gmp/GMPContentParent.cpp b/dom/media/gmp/GMPContentParent.cpp new file mode 100644 index 0000000000..153d4da7fe --- /dev/null +++ b/dom/media/gmp/GMPContentParent.cpp @@ -0,0 +1,204 @@ +/* -*- 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 "GMPContentParent.h" +#include "GMPLog.h" +#include "GMPParent.h" +#include "GMPServiceChild.h" +#include "GMPVideoDecoderParent.h" +#include "GMPVideoEncoderParent.h" +#include "ChromiumCDMParent.h" +#include "mozIGeckoMediaPluginService.h" +#include "mozilla/Logging.h" +#include "mozilla/Unused.h" +#include "base/task.h" + +namespace mozilla::gmp { + +static const char* GetBoolString(bool aBool) { + return aBool ? "true" : "false"; +} + +GMPContentParent::GMPContentParent(GMPParent* aParent) + : mParent(aParent), mPluginId(0) { + GMP_LOG_DEBUG("GMPContentParent::GMPContentParent(this=%p), aParent=%p", this, + aParent); + if (mParent) { + SetDisplayName(mParent->GetDisplayName()); + SetPluginId(mParent->GetPluginId()); + SetPluginType(mParent->GetPluginType()); + } +} + +GMPContentParent::~GMPContentParent() { + GMP_LOG_DEBUG( + "GMPContentParent::~GMPContentParent(this=%p) mVideoDecoders.IsEmpty=%s, " + "mVideoEncoders.IsEmpty=%s, mChromiumCDMs.IsEmpty=%s, " + "mCloseBlockerCount=%" PRIu32, + this, GetBoolString(mVideoDecoders.IsEmpty()), + GetBoolString(mVideoEncoders.IsEmpty()), + GetBoolString(mChromiumCDMs.IsEmpty()), mCloseBlockerCount); +} + +void GMPContentParent::ActorDestroy(ActorDestroyReason aWhy) { + GMP_LOG_DEBUG("GMPContentParent::ActorDestroy(this=%p, aWhy=%d)", this, + static_cast<int>(aWhy)); + MOZ_ASSERT(mVideoDecoders.IsEmpty() && mVideoEncoders.IsEmpty() && + mChromiumCDMs.IsEmpty()); +} + +void GMPContentParent::CheckThread() { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); +} + +void GMPContentParent::ChromiumCDMDestroyed(ChromiumCDMParent* aCDM) { + GMP_LOG_DEBUG("GMPContentParent::ChromiumCDMDestroyed(this=%p, aCDM=%p)", + this, aCDM); + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + + MOZ_ALWAYS_TRUE(mChromiumCDMs.RemoveElement(aCDM)); + CloseIfUnused(); +} + +void GMPContentParent::VideoDecoderDestroyed(GMPVideoDecoderParent* aDecoder) { + GMP_LOG_DEBUG("GMPContentParent::VideoDecoderDestroyed(this=%p, aDecoder=%p)", + this, aDecoder); + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + + // If the constructor fails, we'll get called before it's added + Unused << NS_WARN_IF(!mVideoDecoders.RemoveElement(aDecoder)); + CloseIfUnused(); +} + +void GMPContentParent::VideoEncoderDestroyed(GMPVideoEncoderParent* aEncoder) { + GMP_LOG_DEBUG("GMPContentParent::VideoEncoderDestroyed(this=%p, aEncoder=%p)", + this, aEncoder); + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + + // If the constructor fails, we'll get called before it's added + Unused << NS_WARN_IF(!mVideoEncoders.RemoveElement(aEncoder)); + CloseIfUnused(); +} + +void GMPContentParent::AddCloseBlocker() { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + ++mCloseBlockerCount; + GMP_LOG_DEBUG( + "GMPContentParent::AddCloseBlocker(this=%p) mCloseBlockerCount=%" PRIu32, + this, mCloseBlockerCount); +} + +void GMPContentParent::RemoveCloseBlocker() { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + --mCloseBlockerCount; + GMP_LOG_DEBUG( + "GMPContentParent::RemoveCloseBlocker(this=%p) " + "mCloseBlockerCount=%" PRIu32, + this, mCloseBlockerCount); + CloseIfUnused(); +} + +void GMPContentParent::CloseIfUnused() { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + GMP_LOG_DEBUG( + "GMPContentParent::CloseIfUnused(this=%p) mVideoDecoders.IsEmpty=%s, " + "mVideoEncoders.IsEmpty=%s, mChromiumCDMs.IsEmpty=%s, " + "mCloseBlockerCount=%" PRIu32, + this, GetBoolString(mVideoDecoders.IsEmpty()), + GetBoolString(mVideoEncoders.IsEmpty()), + GetBoolString(mChromiumCDMs.IsEmpty()), mCloseBlockerCount); + if (mVideoDecoders.IsEmpty() && mVideoEncoders.IsEmpty() && + mChromiumCDMs.IsEmpty() && mCloseBlockerCount == 0) { + RefPtr<GMPContentParent> toClose; + if (mParent) { + toClose = mParent->ForgetGMPContentParent(); + } else { + toClose = this; + RefPtr<GeckoMediaPluginServiceChild> gmp( + GeckoMediaPluginServiceChild::GetSingleton()); + if (gmp) { + gmp->RemoveGMPContentParent(toClose); + } + } + NS_DispatchToCurrentThread(NewRunnableMethod( + "gmp::GMPContentParent::Close", toClose, &GMPContentParent::Close)); + } +} + +nsCOMPtr<nsISerialEventTarget> GMPContentParent::GMPEventTarget() { + if (!mGMPEventTarget) { + GMP_LOG_DEBUG("GMPContentParent::GMPEventTarget(this=%p)", this); + nsCOMPtr<mozIGeckoMediaPluginService> mps = + do_GetService("@mozilla.org/gecko-media-plugin-service;1"); + MOZ_ASSERT(mps); + if (!mps) { + return nullptr; + } + // Not really safe if we just grab to the mGMPEventTarget, as we don't know + // what thread we're running on and other threads may be trying to + // access this without locks! However, debug only, and primary failure + // mode outside of compiler-helped TSAN is a leak. But better would be + // to use swap() under a lock. + nsCOMPtr<nsIThread> gmpThread; + mps->GetThread(getter_AddRefs(gmpThread)); + MOZ_ASSERT(gmpThread); + + mGMPEventTarget = gmpThread; + } + + return mGMPEventTarget; +} + +already_AddRefed<ChromiumCDMParent> GMPContentParent::GetChromiumCDM( + const nsCString& aKeySystem) { + GMP_LOG_DEBUG("GMPContentParent::GetChromiumCDM(this=%p aKeySystem=%s)", this, + aKeySystem.get()); + + RefPtr<ChromiumCDMParent> parent = new ChromiumCDMParent(this, GetPluginId()); + if (!SendPChromiumCDMConstructor(parent, aKeySystem)) { + return nullptr; + } + + // TODO: Remove parent from mChromiumCDMs in ChromiumCDMParent::Destroy(). + mChromiumCDMs.AppendElement(parent); + + return parent.forget(); +} + +nsresult GMPContentParent::GetGMPVideoDecoder(GMPVideoDecoderParent** aGMPVD) { + GMP_LOG_DEBUG("GMPContentParent::GetGMPVideoDecoder(this=%p)", this); + + RefPtr<GMPVideoDecoderParent> vdp = new GMPVideoDecoderParent(this); + if (!SendPGMPVideoDecoderConstructor(vdp)) { + return NS_ERROR_FAILURE; + } + + // This addref corresponds to the Proxy pointer the consumer is returned. + // It's dropped by calling Close() on the interface. + vdp.get()->AddRef(); + *aGMPVD = vdp; + mVideoDecoders.AppendElement(vdp); + + return NS_OK; +} + +nsresult GMPContentParent::GetGMPVideoEncoder(GMPVideoEncoderParent** aGMPVE) { + GMP_LOG_DEBUG("GMPContentParent::GetGMPVideoEncoder(this=%p)", this); + + RefPtr<GMPVideoEncoderParent> vep = new GMPVideoEncoderParent(this); + if (!SendPGMPVideoEncoderConstructor(vep)) { + return NS_ERROR_FAILURE; + } + + // This addref corresponds to the Proxy pointer the consumer is returned. + // It's dropped by calling Close() on the interface. + vep.get()->AddRef(); + *aGMPVE = vep; + mVideoEncoders.AppendElement(vep); + + return NS_OK; +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPContentParent.h b/dom/media/gmp/GMPContentParent.h new file mode 100644 index 0000000000..eb68e0fd4a --- /dev/null +++ b/dom/media/gmp/GMPContentParent.h @@ -0,0 +1,94 @@ +/* -*- 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/. */ + +#ifndef GMPContentParent_h_ +#define GMPContentParent_h_ + +#include "mozilla/gmp/PGMPContentParent.h" +#include "GMPSharedMemManager.h" +#include "GMPNativeTypes.h" +#include "nsISupportsImpl.h" + +namespace mozilla::gmp { + +class GMPParent; +class GMPVideoDecoderParent; +class GMPVideoEncoderParent; +class ChromiumCDMParent; + +class GMPContentParent final : public PGMPContentParent, public GMPSharedMem { + friend class PGMPContentParent; + + public: + // Mark AddRef and Release as `final`, as they overload pure virtual + // implementations in PGMPContentParent. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPContentParent, final) + + explicit GMPContentParent(GMPParent* aParent = nullptr); + + nsresult GetGMPVideoDecoder(GMPVideoDecoderParent** aGMPVD); + void VideoDecoderDestroyed(GMPVideoDecoderParent* aDecoder); + + nsresult GetGMPVideoEncoder(GMPVideoEncoderParent** aGMPVE); + void VideoEncoderDestroyed(GMPVideoEncoderParent* aEncoder); + + already_AddRefed<ChromiumCDMParent> GetChromiumCDM( + const nsCString& aKeySystem); + void ChromiumCDMDestroyed(ChromiumCDMParent* aCDM); + + nsCOMPtr<nsISerialEventTarget> GMPEventTarget(); + + // GMPSharedMem + void CheckThread() override; + + void SetDisplayName(const nsCString& aDisplayName) { + mDisplayName = aDisplayName; + } + const nsCString& GetDisplayName() const { return mDisplayName; } + void SetPluginId(const uint32_t aPluginId) { mPluginId = aPluginId; } + uint32_t GetPluginId() const { return mPluginId; } + void SetPluginType(GMPPluginType aPluginType) { mPluginType = aPluginType; } + GMPPluginType GetPluginType() const { return mPluginType; } + + class CloseBlocker { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CloseBlocker) + + explicit CloseBlocker(GMPContentParent* aParent) : mParent(aParent) { + mParent->AddCloseBlocker(); + } + RefPtr<GMPContentParent> mParent; + + private: + ~CloseBlocker() { mParent->RemoveCloseBlocker(); } + }; + + private: + void AddCloseBlocker(); + void RemoveCloseBlocker(); + + ~GMPContentParent(); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + void CloseIfUnused(); + // Needed because NewRunnableMethod tried to use the class that the method + // lives on to store the receiver, but PGMPContentParent isn't refcounted. + void Close() { PGMPContentParent::Close(); } + + nsTArray<RefPtr<GMPVideoDecoderParent>> mVideoDecoders; + nsTArray<RefPtr<GMPVideoEncoderParent>> mVideoEncoders; + nsTArray<RefPtr<ChromiumCDMParent>> mChromiumCDMs; + nsCOMPtr<nsISerialEventTarget> mGMPEventTarget; + RefPtr<GMPParent> mParent; + nsCString mDisplayName; + uint32_t mPluginId; + GMPPluginType mPluginType = GMPPluginType::Unknown; + uint32_t mCloseBlockerCount = 0; +}; + +} // namespace mozilla::gmp + +#endif // GMPParent_h_ diff --git a/dom/media/gmp/GMPCrashHelper.h b/dom/media/gmp/GMPCrashHelper.h new file mode 100644 index 0000000000..0034d54ee7 --- /dev/null +++ b/dom/media/gmp/GMPCrashHelper.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#if !defined(GMPCrashHelper_h_) +# define GMPCrashHelper_h_ + +# include "MainThreadUtils.h" +# include "nsISupportsImpl.h" + +class nsPIDOMWindowInner; + +namespace mozilla { + +// For every GMP actor requested, the caller can specify a crash helper, +// which is an object which supplies the nsPIDOMWindowInner to which we'll +// dispatch the PluginCrashed event if the GMP crashes. +// GMPCrashHelper has threadsafe refcounting. Its release method ensures +// that instances are destroyed on the main thread. +class GMPCrashHelper { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_MAIN_THREAD( + GMPCrashHelper); + + // Called on the main thread. + virtual already_AddRefed<nsPIDOMWindowInner> + GetPluginCrashedEventTarget() = 0; + + protected: + virtual ~GMPCrashHelper() { MOZ_ASSERT(NS_IsMainThread()); } +}; + +} // namespace mozilla + +#endif // GMPCrashHelper_h_ diff --git a/dom/media/gmp/GMPCrashHelperHolder.cpp b/dom/media/gmp/GMPCrashHelperHolder.cpp new file mode 100644 index 0000000000..1c0182155b --- /dev/null +++ b/dom/media/gmp/GMPCrashHelperHolder.cpp @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "GMPCrashHelperHolder.h" +#include "GMPService.h" +#include "mozilla/RefPtr.h" +#include "nsPIDOMWindow.h" +#include "mozilla/ipc/ProtocolUtils.h" + +namespace mozilla { + +void GMPCrashHelperHolder::SetCrashHelper(GMPCrashHelper* aHelper) { + mCrashHelper = aHelper; +} + +GMPCrashHelper* GMPCrashHelperHolder::GetCrashHelper() { return mCrashHelper; } + +void GMPCrashHelperHolder::MaybeDisconnect(bool aAbnormalShutdown) { + if (!aAbnormalShutdown) { + RefPtr<gmp::GeckoMediaPluginService> service( + gmp::GeckoMediaPluginService::GetGeckoMediaPluginService()); + if (service) { + service->DisconnectCrashHelper(GetCrashHelper()); + } + } +} + +} // namespace mozilla diff --git a/dom/media/gmp/GMPCrashHelperHolder.h b/dom/media/gmp/GMPCrashHelperHolder.h new file mode 100644 index 0000000000..d8c63234da --- /dev/null +++ b/dom/media/gmp/GMPCrashHelperHolder.h @@ -0,0 +1,63 @@ +/* -*- 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/. */ + +#ifndef GMPCrashHelperHolder_h_ +#define GMPCrashHelperHolder_h_ + +#include "mozilla/RefPtr.h" +#include "GMPCrashHelper.h" + +namespace mozilla { + +// Disconnecting the GMPCrashHelpers at the right time is hard. We need to +// ensure that in the crashing case that we stay connected until the +// "gmp-plugin-crashed" message is processed in the content process. +// +// We have two channels connecting to the GMP; PGMP which connects from +// chrome to GMP process, and PGMPContent, which bridges between the content +// and GMP process. If the GMP crashes both PGMP and PGMPContent receive +// ActorDestroy messages and begin to shutdown at the same time. +// +// However the crash report mini dump must be generated in the chrome +// process' ActorDestroy, before the "gmp-plugin-crashed" message can be sent +// to the content process. We fire the "PluginCrashed" event when we handle +// the "gmp-plugin-crashed" message in the content process, and we need the +// crash helpers to do that. +// +// The PGMPContent's managed actors' ActorDestroy messages are the only shutdown +// notification we get in the content process, but we can't disconnect the +// crash handlers there in the crashing case, as ActorDestroy happens before +// we've received the "gmp-plugin-crashed" message and had a chance for the +// crash helpers to generate the window to dispatch PluginCrashed to initiate +// the crash report submission notification box. +// +// So we need to ensure that in the content process, the GMPCrashHelpers stay +// connected to the GMPService until after ActorDestroy messages are received +// if there's an abnormal shutdown. In the case where the GMP doesn't crash, +// we do actually want to disconnect GMPCrashHandlers in ActorDestroy, since +// we don't have any other signal that a GMP actor is shutting down. If we don't +// disconnect the crash helper there in the normal shutdown case, the helper +// will stick around forever and leak. +// +// In the crashing case, the GMPCrashHelpers are deallocated when the crash +// report is processed in GeckoMediaPluginService::RunPluginCrashCallbacks(). +// +// It's a bit yuck that we have to have two paths for disconnecting the crash +// helpers, but there aren't really any better options. +class GMPCrashHelperHolder { + public: + void SetCrashHelper(GMPCrashHelper* aHelper); + + GMPCrashHelper* GetCrashHelper(); + + void MaybeDisconnect(bool aAbnormalShutdown); + + private: + RefPtr<GMPCrashHelper> mCrashHelper; +}; + +} // namespace mozilla + +#endif // GMPCrashHelperHolder_h_ diff --git a/dom/media/gmp/GMPDiskStorage.cpp b/dom/media/gmp/GMPDiskStorage.cpp new file mode 100644 index 0000000000..5f7a023b9b --- /dev/null +++ b/dom/media/gmp/GMPDiskStorage.cpp @@ -0,0 +1,454 @@ +/* -*- 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 "plhash.h" +#include "nsDirectoryServiceUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsAppDirectoryServiceDefs.h" +#include "GMPParent.h" +#include "gmp-storage.h" +#include "mozilla/Unused.h" +#include "mozilla/EndianUtils.h" +#include "nsClassHashtable.h" +#include "prio.h" +#include "nsContentCID.h" +#include "nsServiceManagerUtils.h" + +namespace mozilla::gmp { + +// We store the records for a given GMP as files in the profile dir. +// $profileDir/gmp/$platform/$gmpName/storage/$nodeId/ +static nsresult GetGMPStorageDir(nsIFile** aTempDir, const nsAString& aGMPName, + const nsACString& aNodeId) { + if (NS_WARN_IF(!aTempDir)) { + return NS_ERROR_INVALID_ARG; + } + + nsCOMPtr<mozIGeckoMediaPluginChromeService> mps = + do_GetService("@mozilla.org/gecko-media-plugin-service;1"); + if (NS_WARN_IF(!mps)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIFile> tmpFile; + nsresult rv = mps->GetStorageDir(getter_AddRefs(tmpFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = tmpFile->Append(aGMPName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = tmpFile->AppendNative("storage"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = tmpFile->AppendNative(aNodeId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = tmpFile->Create(nsIFile::DIRECTORY_TYPE, 0700); + if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + tmpFile.forget(aTempDir); + + return NS_OK; +} + +// Disk-backed GMP storage. Records are stored in files on disk in +// the profile directory. The record name is a hash of the filename, +// and we resolve hash collisions by just adding 1 to the hash code. +// The format of records on disk is: +// 4 byte, uint32_t $recordNameLength, in little-endian byte order, +// record name (i.e. $recordNameLength bytes, no null terminator) +// record bytes (entire remainder of file) +class GMPDiskStorage : public GMPStorage { + public: + explicit GMPDiskStorage(const nsACString& aNodeId, const nsAString& aGMPName) + : mNodeId(aNodeId), mGMPName(aGMPName) {} + + ~GMPDiskStorage() { + // Close all open file handles. + for (const auto& record : mRecords.Values()) { + if (record->mFileDesc) { + PR_Close(record->mFileDesc); + record->mFileDesc = nullptr; + } + } + } + + nsresult Init() { + // Build our index of records on disk. + nsCOMPtr<nsIFile> storageDir; + nsresult rv = + GetGMPStorageDir(getter_AddRefs(storageDir), mGMPName, mNodeId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + DirectoryEnumerator iter(storageDir, DirectoryEnumerator::FilesAndDirs); + for (nsCOMPtr<nsIFile> dirEntry; (dirEntry = iter.Next()) != nullptr;) { + PRFileDesc* fd = nullptr; + if (NS_WARN_IF( + NS_FAILED(dirEntry->OpenNSPRFileDesc(PR_RDONLY, 0, &fd)))) { + continue; + } + int32_t recordLength = 0; + nsCString recordName; + nsresult err = ReadRecordMetadata(fd, recordLength, recordName); + PR_Close(fd); + if (NS_WARN_IF(NS_FAILED(err))) { + // File is not a valid storage file. Don't index it. Delete the file, + // to make our indexing faster in future. + dirEntry->Remove(false); + continue; + } + + nsAutoString filename; + rv = dirEntry->GetLeafName(filename); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + mRecords.InsertOrUpdate(recordName, + MakeUnique<Record>(filename, recordName)); + } + + return NS_OK; + } + + GMPErr Open(const nsACString& aRecordName) override { + MOZ_ASSERT(!IsOpen(aRecordName)); + + Record* const record = + mRecords.WithEntryHandle(aRecordName, [&](auto&& entry) -> Record* { + if (!entry) { + // New file. + nsAutoString filename; + nsresult rv = GetUnusedFilename(aRecordName, filename); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + return entry.Insert(MakeUnique<Record>(filename, aRecordName)) + .get(); + } + + return entry->get(); + }); + if (!record) { + return GMPGenericErr; + } + + MOZ_ASSERT(record); + if (record->mFileDesc) { + NS_WARNING("Tried to open already open record"); + return GMPRecordInUse; + } + + nsresult rv = + OpenStorageFile(record->mFilename, ReadWrite, &record->mFileDesc); + if (NS_WARN_IF(NS_FAILED(rv))) { + return GMPGenericErr; + } + + MOZ_ASSERT(IsOpen(aRecordName)); + + return GMPNoErr; + } + + bool IsOpen(const nsACString& aRecordName) const override { + // We are open if we have a record indexed, and it has a valid + // file descriptor. + const Record* record = mRecords.Get(aRecordName); + return record && !!record->mFileDesc; + } + + GMPErr Read(const nsACString& aRecordName, + nsTArray<uint8_t>& aOutBytes) override { + if (!IsOpen(aRecordName)) { + return GMPClosedErr; + } + + Record* record = nullptr; + mRecords.Get(aRecordName, &record); + MOZ_ASSERT(record && !!record->mFileDesc); // IsOpen() guarantees this. + + // Our error strategy is to report records with invalid contents as + // containing 0 bytes. Zero length records are considered "deleted" by + // the GMPStorage API. + aOutBytes.SetLength(0); + + int32_t recordLength = 0; + nsCString recordName; + nsresult err = + ReadRecordMetadata(record->mFileDesc, recordLength, recordName); + if (NS_WARN_IF(NS_FAILED(err) || recordLength == 0)) { + // We failed to read the record metadata. Or the record is 0 length. + // Treat damaged records as empty. + // ReadRecordMetadata() could fail if the GMP opened a new record and + // tried to read it before anything was written to it.. + return GMPNoErr; + } + + if (!aRecordName.Equals(recordName)) { + NS_WARNING("Record file contains some other record's contents!"); + return GMPRecordCorrupted; + } + + // After calling ReadRecordMetadata, we should be ready to read the + // record data. + if (PR_Available(record->mFileDesc) != recordLength) { + NS_WARNING("Record file length mismatch!"); + return GMPRecordCorrupted; + } + + aOutBytes.SetLength(recordLength); + int32_t bytesRead = + PR_Read(record->mFileDesc, aOutBytes.Elements(), recordLength); + return (bytesRead == recordLength) ? GMPNoErr : GMPRecordCorrupted; + } + + GMPErr Write(const nsACString& aRecordName, + const nsTArray<uint8_t>& aBytes) override { + if (!IsOpen(aRecordName)) { + return GMPClosedErr; + } + + Record* record = nullptr; + mRecords.Get(aRecordName, &record); + MOZ_ASSERT(record && !!record->mFileDesc); // IsOpen() guarantees this. + + // Write operations overwrite the entire record. So close it now. + PR_Close(record->mFileDesc); + record->mFileDesc = nullptr; + + // Writing 0 bytes means removing (deleting) the file. + if (aBytes.Length() == 0) { + nsresult rv = RemoveStorageFile(record->mFilename); + if (NS_WARN_IF(NS_FAILED(rv))) { + // Could not delete file -> Continue with trying to erase the contents. + } else { + return GMPNoErr; + } + } + + // Write operations overwrite the entire record. So re-open the file + // in truncate mode, to clear its contents. + if (NS_WARN_IF(NS_FAILED(OpenStorageFile(record->mFilename, Truncate, + &record->mFileDesc)))) { + return GMPGenericErr; + } + + // Store the length of the record name followed by the record name + // at the start of the file. + int32_t bytesWritten = 0; + char buf[sizeof(uint32_t)] = {0}; + LittleEndian::writeUint32(buf, aRecordName.Length()); + bytesWritten = PR_Write(record->mFileDesc, buf, MOZ_ARRAY_LENGTH(buf)); + if (bytesWritten != MOZ_ARRAY_LENGTH(buf)) { + NS_WARNING("Failed to write GMPStorage record name length."); + return GMPRecordCorrupted; + } + bytesWritten = PR_Write(record->mFileDesc, aRecordName.BeginReading(), + aRecordName.Length()); + if (bytesWritten != (int32_t)aRecordName.Length()) { + NS_WARNING("Failed to write GMPStorage record name."); + return GMPRecordCorrupted; + } + + bytesWritten = + PR_Write(record->mFileDesc, aBytes.Elements(), aBytes.Length()); + if (bytesWritten != (int32_t)aBytes.Length()) { + NS_WARNING("Failed to write GMPStorage record data."); + return GMPRecordCorrupted; + } + + // Try to sync the file to disk, so that in the event of a crash, + // the record is less likely to be corrupted. + PR_Sync(record->mFileDesc); + + return GMPNoErr; + } + + void Close(const nsACString& aRecordName) override { + Record* record = nullptr; + mRecords.Get(aRecordName, &record); + if (record && !!record->mFileDesc) { + PR_Close(record->mFileDesc); + record->mFileDesc = nullptr; + } + MOZ_ASSERT(!IsOpen(aRecordName)); + } + + private: + // We store records in a file which is a hash of the record name. + // If there is a hash collision, we just keep adding 1 to the hash + // code, until we find a free slot. + nsresult GetUnusedFilename(const nsACString& aRecordName, + nsString& aOutFilename) { + nsCOMPtr<nsIFile> storageDir; + nsresult rv = + GetGMPStorageDir(getter_AddRefs(storageDir), mGMPName, mNodeId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint64_t recordNameHash = HashString(PromiseFlatCString(aRecordName).get()); + for (int i = 0; i < 1000000; i++) { + nsCOMPtr<nsIFile> f; + rv = storageDir->Clone(getter_AddRefs(f)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsAutoString hashStr; + hashStr.AppendInt(recordNameHash); + rv = f->Append(hashStr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + bool exists = false; + f->Exists(&exists); + if (!exists) { + // Filename not in use, we can write into this file. + aOutFilename = hashStr; + return NS_OK; + } else { + // Hash collision; just increment the hash name and try that again. + ++recordNameHash; + continue; + } + } + // Somehow, we've managed to completely fail to find a vacant file name. + // Give up. + NS_WARNING("GetUnusedFilename had extreme hash collision!"); + return NS_ERROR_FAILURE; + } + + enum OpenFileMode { ReadWrite, Truncate }; + + nsresult OpenStorageFile(const nsAString& aFileLeafName, + const OpenFileMode aMode, PRFileDesc** aOutFD) { + MOZ_ASSERT(aOutFD); + + nsCOMPtr<nsIFile> f; + nsresult rv = GetGMPStorageDir(getter_AddRefs(f), mGMPName, mNodeId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + f->Append(aFileLeafName); + + auto mode = PR_RDWR | PR_CREATE_FILE; + if (aMode == Truncate) { + mode |= PR_TRUNCATE; + } + + return f->OpenNSPRFileDesc(mode, PR_IRWXU, aOutFD); + } + + nsresult ReadRecordMetadata(PRFileDesc* aFd, int32_t& aOutRecordLength, + nsACString& aOutRecordName) { + int32_t offset = PR_Seek(aFd, 0, PR_SEEK_END); + PR_Seek(aFd, 0, PR_SEEK_SET); + + if (offset < 0 || offset > GMP_MAX_RECORD_SIZE) { + // Refuse to read big records, or records where we can't get a length. + return NS_ERROR_FAILURE; + } + const uint32_t fileLength = static_cast<uint32_t>(offset); + + // At the start of the file the length of the record name is stored in a + // uint32_t (little endian byte order) followed by the record name at the + // start of the file. The record name is not null terminated. The remainder + // of the file is the record's data. + + if (fileLength < sizeof(uint32_t)) { + // Record file doesn't have enough contents to store the record name + // length. Fail. + return NS_ERROR_FAILURE; + } + + // Read length, and convert to host byte order. + uint32_t recordNameLength = 0; + char buf[sizeof(recordNameLength)] = {0}; + int32_t bytesRead = PR_Read(aFd, &buf, sizeof(recordNameLength)); + recordNameLength = LittleEndian::readUint32(buf); + if (sizeof(recordNameLength) != bytesRead || recordNameLength == 0 || + recordNameLength + sizeof(recordNameLength) > fileLength || + recordNameLength > GMP_MAX_RECORD_NAME_SIZE) { + // Record file has invalid contents. Fail. + return NS_ERROR_FAILURE; + } + + nsCString recordName; + recordName.SetLength(recordNameLength); + bytesRead = PR_Read(aFd, recordName.BeginWriting(), recordNameLength); + if ((uint32_t)bytesRead != recordNameLength) { + // Read failed. + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(fileLength >= sizeof(recordNameLength) + recordNameLength); + int32_t recordLength = + fileLength - (sizeof(recordNameLength) + recordNameLength); + + aOutRecordLength = recordLength; + aOutRecordName = recordName; + + // Read cursor should be positioned after the record name, before the record + // contents. + if (PR_Seek(aFd, 0, PR_SEEK_CUR) != + (int32_t)(sizeof(recordNameLength) + recordNameLength)) { + NS_WARNING("Read cursor mismatch after ReadRecordMetadata()"); + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + + nsresult RemoveStorageFile(const nsAString& aFilename) { + nsCOMPtr<nsIFile> f; + nsresult rv = GetGMPStorageDir(getter_AddRefs(f), mGMPName, mNodeId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = f->Append(aFilename); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return f->Remove(/* bool recursive= */ false); + } + + struct Record { + Record(const nsAString& aFilename, const nsACString& aRecordName) + : mFilename(aFilename), mRecordName(aRecordName), mFileDesc(0) {} + ~Record() { MOZ_ASSERT(!mFileDesc); } + nsString mFilename; + nsCString mRecordName; + PRFileDesc* mFileDesc; + }; + + // Hash record name to record data. + nsClassHashtable<nsCStringHashKey, Record> mRecords; + const nsCString mNodeId; + const nsString mGMPName; +}; + +already_AddRefed<GMPStorage> CreateGMPDiskStorage(const nsACString& aNodeId, + const nsAString& aGMPName) { + RefPtr<GMPDiskStorage> storage(new GMPDiskStorage(aNodeId, aGMPName)); + if (NS_FAILED(storage->Init())) { + NS_WARNING("Failed to initialize on disk GMP storage"); + return nullptr; + } + return storage.forget(); +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPLoader.cpp b/dom/media/gmp/GMPLoader.cpp new file mode 100644 index 0000000000..9fa7756165 --- /dev/null +++ b/dom/media/gmp/GMPLoader.cpp @@ -0,0 +1,203 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=4 et : + * 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 "GMPLoader.h" +#include <stdio.h> +#include "mozilla/Attributes.h" +#include "nsExceptionHandler.h" +#include "gmp-entrypoints.h" +#include "prlink.h" +#include "prenv.h" +#include "prerror.h" +#if defined(XP_WIN) && defined(MOZ_SANDBOX) +# include "mozilla/sandboxTarget.h" +# include "mozilla/sandboxing/SandboxInitialization.h" +# include "mozilla/sandboxing/sandboxLogging.h" +#endif +#if defined(XP_LINUX) && defined(MOZ_SANDBOX) +# include "mozilla/Sandbox.h" +# include "mozilla/SandboxInfo.h" +#endif + +#include <string> + +#ifdef XP_WIN +# include <windows.h> +#endif + +namespace mozilla::gmp { +class PassThroughGMPAdapter : public GMPAdapter { + public: + ~PassThroughGMPAdapter() override { + // Ensure we're always shutdown, even if caller forgets to call + // GMPShutdown(). + GMPShutdown(); + } + + void SetAdaptee(PRLibrary* aLib) override { mLib = aLib; } + + GMPErr GMPInit(const GMPPlatformAPI* aPlatformAPI) override { + if (NS_WARN_IF(!mLib)) { + MOZ_CRASH("Missing library!"); + return GMPGenericErr; + } + GMPInitFunc initFunc = + reinterpret_cast<GMPInitFunc>(PR_FindFunctionSymbol(mLib, "GMPInit")); + if (!initFunc) { + MOZ_CRASH("Missing init method!"); + return GMPNotImplementedErr; + } + return initFunc(aPlatformAPI); + } + + GMPErr GMPGetAPI(const char* aAPIName, void* aHostAPI, void** aPluginAPI, + const nsACString& /* aKeySystem */) override { + if (!mLib) { + return GMPGenericErr; + } + GMPGetAPIFunc getapiFunc = reinterpret_cast<GMPGetAPIFunc>( + PR_FindFunctionSymbol(mLib, "GMPGetAPI")); + if (!getapiFunc) { + return GMPNotImplementedErr; + } + return getapiFunc(aAPIName, aHostAPI, aPluginAPI); + } + + void GMPShutdown() override { + if (mLib) { + GMPShutdownFunc shutdownFunc = reinterpret_cast<GMPShutdownFunc>( + PR_FindFunctionSymbol(mLib, "GMPShutdown")); + if (shutdownFunc) { + shutdownFunc(); + } + PR_UnloadLibrary(mLib); + mLib = nullptr; + } + } + + private: + PRLibrary* mLib = nullptr; +}; + +bool GMPLoader::Load(const char* aUTF8LibPath, uint32_t aUTF8LibPathLen, + const GMPPlatformAPI* aPlatformAPI, GMPAdapter* aAdapter) { + CrashReporter::AutoAnnotateCrashReport autoLibPath( + CrashReporter::Annotation::GMPLibraryPath, + nsDependentCString(aUTF8LibPath)); + + if (!getenv("MOZ_DISABLE_GMP_SANDBOX") && mSandboxStarter && + !mSandboxStarter->Start(aUTF8LibPath)) { + MOZ_CRASH("Cannot start sandbox!"); + return false; + } + + // Load the GMP. + PRLibSpec libSpec; +#ifdef XP_WIN + int pathLen = MultiByteToWideChar(CP_UTF8, 0, aUTF8LibPath, -1, nullptr, 0); + if (pathLen == 0) { + MOZ_CRASH("Cannot get path length as wide char!"); + return false; + } + + auto widePath = MakeUnique<wchar_t[]>(pathLen); + if (MultiByteToWideChar(CP_UTF8, 0, aUTF8LibPath, -1, widePath.get(), + pathLen) == 0) { + MOZ_CRASH("Cannot convert path to wide char!"); + return false; + } + + libSpec.value.pathname_u = widePath.get(); + libSpec.type = PR_LibSpec_PathnameU; +#else + libSpec.value.pathname = aUTF8LibPath; + libSpec.type = PR_LibSpec_Pathname; +#endif + PRLibrary* lib = PR_LoadLibraryWithFlags(libSpec, 0); + if (!lib) { + MOZ_CRASH_UNSAFE_PRINTF("Cannot load plugin as library %d %d", + PR_GetError(), PR_GetOSError()); + return false; + } + + mAdapter.reset((!aAdapter) ? new PassThroughGMPAdapter() : aAdapter); + mAdapter->SetAdaptee(lib); + + if (mAdapter->GMPInit(aPlatformAPI) != GMPNoErr) { + MOZ_CRASH("Cannot initialize plugin adapter!"); + return false; + } + + return true; +} + +GMPErr GMPLoader::GetAPI(const char* aAPIName, void* aHostAPI, + void** aPluginAPI, const nsACString& aKeySystem) { + return mAdapter->GMPGetAPI(aAPIName, aHostAPI, aPluginAPI, aKeySystem); +} + +void GMPLoader::Shutdown() { + if (mAdapter) { + mAdapter->GMPShutdown(); + } +} + +#if defined(XP_WIN) && defined(MOZ_SANDBOX) +class WinSandboxStarter : public mozilla::gmp::SandboxStarter { + public: + bool Start(const char* aLibPath) override { + // Cause advapi32 to load before the sandbox is turned on, as + // Widevine version 970 and later require it and the sandbox + // blocks it on Win7. + unsigned int dummy_rand; + rand_s(&dummy_rand); + + mozilla::SandboxTarget::Instance()->StartSandbox(); + return true; + } +}; +#endif + +#if defined(XP_LINUX) && defined(MOZ_SANDBOX) +namespace { +class LinuxSandboxStarter : public mozilla::gmp::SandboxStarter { + private: + LinuxSandboxStarter() = default; + friend mozilla::detail::UniqueSelector<LinuxSandboxStarter>::SingleObject + mozilla::MakeUnique<LinuxSandboxStarter>(); + + public: + static UniquePtr<SandboxStarter> Make() { + if (mozilla::SandboxInfo::Get().CanSandboxMedia()) { + return MakeUnique<LinuxSandboxStarter>(); + } + // Sandboxing isn't possible, but the parent has already + // checked that this plugin doesn't require it. (Bug 1074561) + return nullptr; + } + bool Start(const char* aLibPath) override { + mozilla::SetMediaPluginSandbox(aLibPath); + return true; + } +}; +} // anonymous namespace +#endif // XP_LINUX && MOZ_SANDBOX + +static UniquePtr<SandboxStarter> MakeSandboxStarter() { +#if defined(XP_WIN) && defined(MOZ_SANDBOX) + return mozilla::MakeUnique<WinSandboxStarter>(); +#elif defined(XP_LINUX) && defined(MOZ_SANDBOX) + return LinuxSandboxStarter::Make(); +#else + return nullptr; +#endif +} + +GMPLoader::GMPLoader() : mSandboxStarter(MakeSandboxStarter()) {} + +bool GMPLoader::CanSandbox() const { return !!mSandboxStarter; } + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPLoader.h b/dom/media/gmp/GMPLoader.h new file mode 100644 index 0000000000..a0e1513d34 --- /dev/null +++ b/dom/media/gmp/GMPLoader.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=4 et : + * 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/. */ + +#ifndef GMP_LOADER_H__ +#define GMP_LOADER_H__ + +#include <stdint.h> +#include "prlink.h" +#include "gmp-entrypoints.h" +#include "mozilla/UniquePtr.h" +#include "nsString.h" + +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) +# include "mozilla/Sandbox.h" +#endif + +namespace mozilla::gmp { + +class SandboxStarter { + public: + virtual ~SandboxStarter() = default; + virtual bool Start(const char* aLibPath) = 0; +}; + +// Interface that adapts a plugin to the GMP API. +class GMPAdapter { + public: + virtual ~GMPAdapter() = default; + // Sets the adapted to plugin library module. + // Note: the GMPAdapter is responsible for calling PR_UnloadLibrary on aLib + // when it's finished with it. + virtual void SetAdaptee(PRLibrary* aLib) = 0; + + // These are called in place of the corresponding GMP API functions. + virtual GMPErr GMPInit(const GMPPlatformAPI* aPlatformAPI) = 0; + // The `aKeySystem` arg is used to specify the key system when loading CDMs, + // and will be ignored by non-CDM GMPs. It is not part of the public GMP API + // Gecko exposes. + virtual GMPErr GMPGetAPI(const char* aAPIName, void* aHostAPI, + void** aPluginAPI, const nsACString& aKeySystem) = 0; + virtual void GMPShutdown() = 0; +}; + +// Encapsulates activating the sandbox, and loading the GMP. +// Load() takes an optional GMPAdapter which can be used to adapt non-GMPs +// to adhere to the GMP API. +class GMPLoader { + public: + GMPLoader(); + + // Activates the sandbox, then loads the GMP library. If aAdapter is + // non-null, the lib path is assumed to be a non-GMP, and the adapter + // is initialized with the lib and the adapter is used to interact with + // the plugin. + bool Load(const char* aUTF8LibPath, uint32_t aLibPathLen, + const GMPPlatformAPI* aPlatformAPI, GMPAdapter* aAdapter = nullptr); + + // Retrieves an interface pointer from the GMP. If the GMP is loading a CDM, + // aKeySystem is passed to the CDM to allow for key system specific + // configuration by the CDM. + GMPErr GetAPI(const char* aAPIName, void* aHostAPI, void** aPluginAPI, + const nsACString& aKeySystem); + + // Calls the GMPShutdown function exported by the GMP lib, and unloads the + // plugin library. + void Shutdown(); + + bool CanSandbox() const; + + private: + UniquePtr<SandboxStarter> mSandboxStarter; + UniquePtr<GMPAdapter> mAdapter; +}; + +} // namespace mozilla::gmp + +#endif // GMP_LOADER_H__ diff --git a/dom/media/gmp/GMPLog.h b/dom/media/gmp/GMPLog.h new file mode 100644 index 0000000000..94e797e827 --- /dev/null +++ b/dom/media/gmp/GMPLog.h @@ -0,0 +1,62 @@ +/* -*- 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/. */ + +#ifndef DOM_MEDIA_GMPLOG_H_ +#define DOM_MEDIA_GMPLOG_H_ + +#include "content_decryption_module.h" +#include "gmp-video-codec.h" +#include "mozilla/Logging.h" + +namespace mozilla { + +extern LogModule* GetGMPLog(); +extern LogModule* GetGMPLibraryLog(); +extern GMPLogLevel GetGMPLibraryLogLevel(); + +#define GMP_LOG_ERROR(msg, ...) \ + MOZ_LOG(GetGMPLog(), LogLevel::Error, (msg, ##__VA_ARGS__)) +#define GMP_LOG_WARNING(msg, ...) \ + MOZ_LOG(GetGMPLog(), LogLevel::Warning, (msg, ##__VA_ARGS__)) +#define GMP_LOG_INFO(msg, ...) \ + MOZ_LOG(GetGMPLog(), LogLevel::Info, (msg, ##__VA_ARGS__)) +#define GMP_LOG_DEBUG(msg, ...) \ + MOZ_LOG(GetGMPLog(), LogLevel::Debug, (msg, ##__VA_ARGS__)) +#define GMP_LOG_VERBOSE(msg, ...) \ + MOZ_LOG(GetGMPLog(), LogLevel::Verbose, (msg, ##__VA_ARGS__)) + +// Helpers + +inline const char* CdmStatusToString(cdm::Status aStatus) { + switch (aStatus) { + case cdm::Status::kSuccess: + return "success"; + case cdm::Status::kNeedMoreData: + return "need more data"; + case cdm::Status::kNoKey: + return "no key"; + case cdm::Status::kInitializationError: + return "initialization error"; + case cdm::Status::kDecryptError: + return "decrypt error"; + case cdm::Status::kDecodeError: + return "decode error"; + case cdm::Status::kDeferredInitialization: + return "deferred initialization"; + default: + MOZ_ASSERT_UNREACHABLE("Should have coverage of entire enum"); + return "unexpected status code"; // Gracefully handle disabled asserts. + } +} + +inline const char* CdmStatusToString(uint32_t aStatus) { + return CdmStatusToString(cdm::Status(aStatus)); +} + +// End helpers + +} // namespace mozilla + +#endif // DOM_MEDIA_GMPLOG_H_ diff --git a/dom/media/gmp/GMPMemoryStorage.cpp b/dom/media/gmp/GMPMemoryStorage.cpp new file mode 100644 index 0000000000..fe8cfee5f8 --- /dev/null +++ b/dom/media/gmp/GMPMemoryStorage.cpp @@ -0,0 +1,75 @@ +/* -*- 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 "GMPStorage.h" +#include "nsClassHashtable.h" + +namespace mozilla::gmp { + +class GMPMemoryStorage : public GMPStorage { + public: + GMPErr Open(const nsACString& aRecordName) override { + MOZ_ASSERT(!IsOpen(aRecordName)); + + Record* record = mRecords.GetOrInsertNew(aRecordName); + record->mIsOpen = true; + return GMPNoErr; + } + + bool IsOpen(const nsACString& aRecordName) const override { + const Record* record = mRecords.Get(aRecordName); + if (!record) { + return false; + } + return record->mIsOpen; + } + + GMPErr Read(const nsACString& aRecordName, + nsTArray<uint8_t>& aOutBytes) override { + const Record* record = mRecords.Get(aRecordName); + if (!record) { + return GMPGenericErr; + } + aOutBytes = record->mData.Clone(); + return GMPNoErr; + } + + GMPErr Write(const nsACString& aRecordName, + const nsTArray<uint8_t>& aBytes) override { + Record* record = nullptr; + if (!mRecords.Get(aRecordName, &record)) { + return GMPClosedErr; + } + record->mData = aBytes.Clone(); + return GMPNoErr; + } + + void Close(const nsACString& aRecordName) override { + Record* record = nullptr; + if (!mRecords.Get(aRecordName, &record)) { + return; + } + if (!record->mData.Length()) { + // Record is empty, delete. + mRecords.Remove(aRecordName); + } else { + record->mIsOpen = false; + } + } + + private: + struct Record { + nsTArray<uint8_t> mData; + bool mIsOpen = false; + }; + + nsClassHashtable<nsCStringHashKey, Record> mRecords; +}; + +already_AddRefed<GMPStorage> CreateGMPMemoryStorage() { + return RefPtr<GMPStorage>(new GMPMemoryStorage()).forget(); +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPMessageUtils.h b/dom/media/gmp/GMPMessageUtils.h new file mode 100644 index 0000000000..29740a49a6 --- /dev/null +++ b/dom/media/gmp/GMPMessageUtils.h @@ -0,0 +1,190 @@ +/* -*- 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/. */ + +#ifndef GMPMessageUtils_h_ +#define GMPMessageUtils_h_ + +#include "gmp-video-codec.h" +#include "gmp-video-frame-encoded.h" +#include "ipc/EnumSerializer.h" +#include "ipc/IPCMessageUtilsSpecializations.h" +#include "GMPNativeTypes.h" +#include "GMPSanitizedExports.h" + +namespace IPC { + +template <> +struct ParamTraits<GMPPluginType> + : public ContiguousEnumSerializerInclusive< + GMPPluginType, GMPPluginType::Unknown, GMPPluginType::Widevine> {}; + +template <> +struct ParamTraits<GMPErr> + : public ContiguousEnumSerializer<GMPErr, GMPNoErr, GMPLastErr> {}; + +template <> +struct ParamTraits<GMPVideoFrameType> + : public ContiguousEnumSerializer<GMPVideoFrameType, kGMPKeyFrame, + kGMPVideoFrameInvalid> {}; + +template <> +struct ParamTraits<GMPVideoCodecComplexity> + : public ContiguousEnumSerializer<GMPVideoCodecComplexity, + kGMPComplexityNormal, + kGMPComplexityInvalid> {}; + +template <> +struct ParamTraits<GMPVP8ResilienceMode> + : public ContiguousEnumSerializer<GMPVP8ResilienceMode, kResilienceOff, + kResilienceInvalid> {}; + +template <> +struct ParamTraits<GMPVideoCodecType> + : public ContiguousEnumSerializer<GMPVideoCodecType, kGMPVideoCodecVP8, + kGMPVideoCodecInvalid> {}; + +template <> +struct ParamTraits<GMPVideoCodecMode> + : public ContiguousEnumSerializer<GMPVideoCodecMode, kGMPRealtimeVideo, + kGMPCodecModeInvalid> {}; + +template <> +struct ParamTraits<GMPLogLevel> + : public ContiguousEnumSerializerInclusive<GMPLogLevel, kGMPLogDefault, + kGMPLogInvalid> {}; + +template <> +struct ParamTraits<GMPBufferType> + : public ContiguousEnumSerializer<GMPBufferType, GMP_BufferSingle, + GMP_BufferInvalid> {}; + +template <> +struct ParamTraits<cdm::EncryptionScheme> + : public ContiguousEnumSerializerInclusive< + cdm::EncryptionScheme, cdm::EncryptionScheme::kUnencrypted, + cdm::EncryptionScheme::kCbcs> {}; + +template <> +struct ParamTraits<cdm::HdcpVersion> + : public ContiguousEnumSerializerInclusive< + cdm::HdcpVersion, cdm::HdcpVersion::kHdcpVersionNone, + cdm::HdcpVersion::kHdcpVersion2_2> {}; + +template <> +struct ParamTraits<GMPSimulcastStream> { + typedef GMPSimulcastStream paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mWidth); + WriteParam(aWriter, aParam.mHeight); + WriteParam(aWriter, aParam.mNumberOfTemporalLayers); + WriteParam(aWriter, aParam.mMaxBitrate); + WriteParam(aWriter, aParam.mTargetBitrate); + WriteParam(aWriter, aParam.mMinBitrate); + WriteParam(aWriter, aParam.mQPMax); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (ReadParam(aReader, &(aResult->mWidth)) && + ReadParam(aReader, &(aResult->mHeight)) && + ReadParam(aReader, &(aResult->mNumberOfTemporalLayers)) && + ReadParam(aReader, &(aResult->mMaxBitrate)) && + ReadParam(aReader, &(aResult->mTargetBitrate)) && + ReadParam(aReader, &(aResult->mMinBitrate)) && + ReadParam(aReader, &(aResult->mQPMax))) { + return true; + } + return false; + } +}; + +template <> +struct ParamTraits<GMPVideoCodec> { + typedef GMPVideoCodec paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mGMPApiVersion); + WriteParam(aWriter, aParam.mCodecType); + WriteParam(aWriter, static_cast<const nsCString&>( + nsDependentCString(aParam.mPLName))); + WriteParam(aWriter, aParam.mPLType); + WriteParam(aWriter, aParam.mWidth); + WriteParam(aWriter, aParam.mHeight); + WriteParam(aWriter, aParam.mStartBitrate); + WriteParam(aWriter, aParam.mMaxBitrate); + WriteParam(aWriter, aParam.mMinBitrate); + WriteParam(aWriter, aParam.mMaxFramerate); + WriteParam(aWriter, aParam.mFrameDroppingOn); + WriteParam(aWriter, aParam.mKeyFrameInterval); + WriteParam(aWriter, aParam.mQPMax); + WriteParam(aWriter, aParam.mNumberOfSimulcastStreams); + for (uint32_t i = 0; i < aParam.mNumberOfSimulcastStreams; i++) { + WriteParam(aWriter, aParam.mSimulcastStream[i]); + } + WriteParam(aWriter, aParam.mMode); + WriteParam(aWriter, aParam.mUseThreadedDecode); + WriteParam(aWriter, aParam.mLogLevel); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + // NOTE: make sure this matches any versions supported + if (!ReadParam(aReader, &(aResult->mGMPApiVersion)) || + (aResult->mGMPApiVersion != kGMPVersion33 && + aResult->mGMPApiVersion != kGMPVersion34)) { + return false; + } + if (!ReadParam(aReader, &(aResult->mCodecType))) { + return false; + } + + nsAutoCString plName; + if (!ReadParam(aReader, &plName) || + plName.Length() > kGMPPayloadNameSize - 1) { + return false; + } + memcpy(aResult->mPLName, plName.get(), plName.Length()); + memset(aResult->mPLName + plName.Length(), 0, + kGMPPayloadNameSize - plName.Length()); + + if (!ReadParam(aReader, &(aResult->mPLType)) || + !ReadParam(aReader, &(aResult->mWidth)) || + !ReadParam(aReader, &(aResult->mHeight)) || + !ReadParam(aReader, &(aResult->mStartBitrate)) || + !ReadParam(aReader, &(aResult->mMaxBitrate)) || + !ReadParam(aReader, &(aResult->mMinBitrate)) || + !ReadParam(aReader, &(aResult->mMaxFramerate)) || + !ReadParam(aReader, &(aResult->mFrameDroppingOn)) || + !ReadParam(aReader, &(aResult->mKeyFrameInterval))) { + return false; + } + + if (!ReadParam(aReader, &(aResult->mQPMax)) || + !ReadParam(aReader, &(aResult->mNumberOfSimulcastStreams))) { + return false; + } + + if (aResult->mNumberOfSimulcastStreams > kGMPMaxSimulcastStreams) { + return false; + } + + for (uint32_t i = 0; i < aResult->mNumberOfSimulcastStreams; i++) { + if (!ReadParam(aReader, &(aResult->mSimulcastStream[i]))) { + return false; + } + } + + if (!ReadParam(aReader, &(aResult->mMode)) || + !ReadParam(aReader, &(aResult->mUseThreadedDecode)) || + !ReadParam(aReader, &(aResult->mLogLevel))) { + return false; + } + + return true; + } +}; + +} // namespace IPC + +#endif // GMPMessageUtils_h_ diff --git a/dom/media/gmp/GMPNativeTypes.h b/dom/media/gmp/GMPNativeTypes.h new file mode 100644 index 0000000000..39cd6ddc05 --- /dev/null +++ b/dom/media/gmp/GMPNativeTypes.h @@ -0,0 +1,17 @@ +/* -*- 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/. */ + +#ifndef GMPNativeTypes_h_ +#define GMPNativeTypes_h_ + +enum class GMPPluginType { + Unknown, + Fake, + Clearkey, + OpenH264, + Widevine, +}; + +#endif diff --git a/dom/media/gmp/GMPParent.cpp b/dom/media/gmp/GMPParent.cpp new file mode 100644 index 0000000000..6c1acc3a20 --- /dev/null +++ b/dom/media/gmp/GMPParent.cpp @@ -0,0 +1,1209 @@ +/* -*- 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 "GMPParent.h" + +#include "CDMStorageIdProvider.h" +#include "ChromiumCDMAdapter.h" +#include "GMPContentParent.h" +#include "GMPLog.h" +#include "GMPTimerParent.h" +#include "MediaResult.h" +#include "mozIGeckoMediaPluginService.h" +#include "mozilla/dom/KeySystemNames.h" +#include "mozilla/dom/WidevineCDMManifestBinding.h" +#include "mozilla/FOGIPC.h" +#include "mozilla/ipc/CrashReporterHost.h" +#include "mozilla/ipc/Endpoint.h" +#include "mozilla/ipc/GeckoChildProcessHost.h" +#if defined(XP_LINUX) && defined(MOZ_SANDBOX) +# include "mozilla/SandboxInfo.h" +# include "base/shared_memory.h" +#endif +#include "mozilla/Services.h" +#include "mozilla/SSE.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "nsComponentManagerUtils.h" +#include "nsIRunnable.h" +#include "nsIObserverService.h" +#include "nsIWritablePropertyBag2.h" +#include "nsPrintfCString.h" +#include "nsThreadUtils.h" +#include "ProfilerParent.h" +#include "runnable_utils.h" +#ifdef XP_WIN +# include "mozilla/FileUtilsWin.h" +# include "WMFDecoderModule.h" +#endif +#if defined(MOZ_WIDGET_ANDROID) +# include "mozilla/java/GeckoProcessManagerWrappers.h" +# include "mozilla/java/GeckoProcessTypeWrappers.h" +#endif // defined(MOZ_WIDGET_ANDROID) +#if defined(XP_MACOSX) +# include "nsMacUtilsImpl.h" +# include "base/process_util.h" +#endif // defined(XP_MACOSX) + +using mozilla::ipc::GeckoChildProcessHost; + +using CrashReporter::AnnotationTable; + +namespace mozilla::gmp { + +#define GMP_PARENT_LOG_DEBUG(x, ...) \ + GMP_LOG_DEBUG("GMPParent[%p|childPid=%d] " x, this, mChildPid, ##__VA_ARGS__) + +#ifdef __CLASS__ +# undef __CLASS__ +#endif +#define __CLASS__ "GMPParent" + +GMPParent::GMPParent() + : mState(GMPState::NotLoaded), + mPluginId(GeckoChildProcessHost::GetUniqueID()), + mProcess(nullptr), + mDeleteProcessOnlyOnUnload(false), + mAbnormalShutdownInProgress(false), + mIsBlockingDeletion(false), + mCanDecrypt(false), + mGMPContentChildCount(0), + mChildPid(0), +#ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH + mChildLaunchArch(base::PROCESS_ARCH_INVALID), +#endif + mMainThread(GetMainThreadSerialEventTarget()) { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + GMP_PARENT_LOG_DEBUG("GMPParent ctor id=%u", mPluginId); +} + +GMPParent::~GMPParent() { + // This method is not restricted to a specific thread. + GMP_PARENT_LOG_DEBUG("GMPParent dtor id=%u", mPluginId); + MOZ_ASSERT(!mProcess); +} + +void GMPParent::CloneFrom(const GMPParent* aOther) { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + MOZ_ASSERT(aOther->mDirectory && aOther->mService, "null plugin directory"); + + mService = aOther->mService; + mDirectory = aOther->mDirectory; + mName = aOther->mName; + mVersion = aOther->mVersion; + mDescription = aOther->mDescription; + mDisplayName = aOther->mDisplayName; + mPluginType = aOther->mPluginType; +#if defined(XP_WIN) || defined(XP_LINUX) + mLibs = aOther->mLibs; +#endif + for (const GMPCapability& cap : aOther->mCapabilities) { + mCapabilities.AppendElement(cap); + } + mAdapter = aOther->mAdapter; + +#ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH + mChildLaunchArch = aOther->mChildLaunchArch; +#endif +} + +#if defined(XP_WIN) || defined(XP_MACOSX) +nsresult GMPParent::GetPluginFileArch(nsIFile* aPluginDir, + const nsString& aBaseName, + uint32_t& aArchSet) { + // Build up the plugin filename +# if defined(XP_MACOSX) + nsAutoString pluginFileName = u"lib"_ns + aBaseName + u".dylib"_ns; +# elif defined(XP_WIN) + nsAutoString pluginFileName = aBaseName + u".dll"_ns; +# endif + GMP_PARENT_LOG_DEBUG("%s: pluginFileName: %s", __FUNCTION__, + NS_LossyConvertUTF16toASCII(pluginFileName).get()); + + // Create an nsIFile representing the plugin + nsCOMPtr<nsIFile> pluginFile; + nsresult rv = aPluginDir->Clone(getter_AddRefs(pluginFile)); + NS_ENSURE_SUCCESS(rv, rv); + pluginFile->AppendRelativePath(pluginFileName); + +# if defined(XP_MACOSX) + // Get the full plugin path + nsAutoCString pluginPath; + rv = pluginFile->GetNativePath(pluginPath); + NS_ENSURE_SUCCESS(rv, rv); + GMP_PARENT_LOG_DEBUG("%s: pluginPath: %s", __FUNCTION__, pluginPath.get()); + + rv = nsMacUtilsImpl::GetArchitecturesForBinary(pluginPath.get(), &aArchSet); + NS_ENSURE_SUCCESS(rv, rv); + +# if defined(__aarch64__) + mPluginFilePath = pluginPath; +# endif +# elif defined(XP_WIN) + // Get the full plugin path + nsAutoString pluginPath; + rv = pluginFile->GetTarget(pluginPath); + NS_ENSURE_SUCCESS(rv, rv); + GMP_PARENT_LOG_DEBUG("%s: pluginPath: %s", __FUNCTION__, + NS_LossyConvertUTF16toASCII(pluginPath).get()); + + aArchSet = GetExecutableArchitecture(pluginPath.get()); + if (aArchSet == base::PROCESS_ARCH_INVALID) { + return NS_ERROR_FAILURE; + } +# endif + + return NS_OK; +} +#endif // defined(XP_WIN) || defined(XP_MACOSX) + +RefPtr<GenericPromise> GMPParent::Init(GeckoMediaPluginServiceParent* aService, + nsIFile* aPluginDir) { + MOZ_ASSERT(aPluginDir); + MOZ_ASSERT(aService); + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + + mService = aService; + mDirectory = aPluginDir; + + // aPluginDir is <profile-dir>/<gmp-plugin-id>/<version> + // where <gmp-plugin-id> should be gmp-gmpopenh264 + nsCOMPtr<nsIFile> parent; + nsresult rv = aPluginDir->GetParent(getter_AddRefs(parent)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return GenericPromise::CreateAndReject(rv, __func__); + } + nsAutoString parentLeafName; + rv = parent->GetLeafName(parentLeafName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return GenericPromise::CreateAndReject(rv, __func__); + } + GMP_PARENT_LOG_DEBUG("%s: for %s", __FUNCTION__, + NS_LossyConvertUTF16toASCII(parentLeafName).get()); + + MOZ_ASSERT(parentLeafName.Length() > 4); + mName = Substring(parentLeafName, 4); + +#if defined(XP_WIN) || defined(XP_MACOSX) + uint32_t pluginArch = base::PROCESS_ARCH_INVALID; + rv = GetPluginFileArch(aPluginDir, mName, pluginArch); + if (NS_FAILED(rv)) { + GMP_PARENT_LOG_DEBUG("%s: Plugin arch error: %d", __FUNCTION__, rv); + } else { + GMP_PARENT_LOG_DEBUG("%s: Plugin arch: 0x%x", __FUNCTION__, pluginArch); + } + + const uint32_t x86 = base::PROCESS_ARCH_X86_64 | base::PROCESS_ARCH_I386; +# ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH + const uint32_t arm64 = base::PROCESS_ARCH_ARM_64; + + mChildLaunchArch = pluginArch; + // When executing in an ARM64 process, if the library is x86 or x64, + // set |mChildLaunchArch| to x64 and allow the library to be used as long + // as this process is a universal binary. + if (!(pluginArch & arm64) && (pluginArch & x86)) { + bool isWidevine = parentLeafName.Find(u"widevine") != kNotFound; + bool isWidevineAllowed = + StaticPrefs::media_gmp_widevinecdm_allow_x64_plugin_on_arm64(); + bool isH264 = parentLeafName.Find(u"openh264") != kNotFound; + bool isH264Allowed = + StaticPrefs::media_gmp_gmpopenh264_allow_x64_plugin_on_arm64(); + bool isClearkey = parentLeafName.Find(u"clearkey") != kNotFound; + bool isClearkeyAllowed = + StaticPrefs::media_gmp_gmpclearkey_allow_x64_plugin_on_arm64(); + + // Only allow x64 child GMP processes for Widevine, OpenH264 and Clearkey + if (!isWidevine && !isH264 && !isClearkey) { + return GenericPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, + __func__); + } + // And only if prefs permit it. + if ((isWidevine && !isWidevineAllowed) || (isH264 && !isH264Allowed) || + (isClearkey && !isClearkeyAllowed)) { + return GenericPromise::CreateAndReject(NS_ERROR_PLUGIN_DISABLED, + __func__); + } + +# ifdef XP_MACOSX + // We have an x64 library. Get the bundle architecture to determine + // if we are a universal binary and hence if we can launch an x64 + // child process to host this plugin. + uint32_t bundleArch = base::PROCESS_ARCH_INVALID; + rv = nsMacUtilsImpl::GetArchitecturesForBundle(&bundleArch); + if (NS_FAILED(rv)) { + // If we fail here, continue as if this is not a univeral binary. + GMP_PARENT_LOG_DEBUG("%s: Bundle arch error: %d", __FUNCTION__, rv); + } else { + GMP_PARENT_LOG_DEBUG("%s: Bundle arch: 0x%x", __FUNCTION__, bundleArch); + } + + bool isUniversalBinary = (bundleArch & base::PROCESS_ARCH_X86_64) && + (bundleArch & base::PROCESS_ARCH_ARM_64); + if (isUniversalBinary) { + mChildLaunchArch = base::PROCESS_ARCH_X86_64; + PreTranslateBins(); + } else { + return GenericPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, + __func__); + } +# endif + } +# else + // When executing in a non-ARM process, if the library is not x86 or x64, + // remove it and return an error. This prevents a child process crash due + // to loading an incompatible library and forces a new plugin version to be + // downloaded when the check is next performed. This could occur if a profile + // is moved from an ARM64 system to an x64 system. + if ((pluginArch & x86) == 0) { + GMP_PARENT_LOG_DEBUG("%s: Removing plugin directory", __FUNCTION__); + aPluginDir->Remove(true); + return GenericPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__); + } +# endif // defined(ALLOW_GECKO_CHILD_PROCESS_ARCH) +#endif // defined(XP_WIN) || defined(XP_MACOSX) + + return ReadGMPMetaData(); +} + +void GMPParent::Crash() { + if (mState != GMPState::NotLoaded) { + Unused << SendCrashPluginNow(); + } +} + +class NotifyGMPProcessLoadedTask : public Runnable { + public: + explicit NotifyGMPProcessLoadedTask(const ::base::ProcessId aProcessId, + GMPParent* aGMPParent) + : Runnable("NotifyGMPProcessLoadedTask"), + mProcessId(aProcessId), + mGMPParent(aGMPParent) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + bool canProfile = true; + +#if defined(XP_LINUX) && defined(MOZ_SANDBOX) + if (SandboxInfo::Get().Test(SandboxInfo::kEnabledForMedia) && + base::SharedMemory::UsingPosixShm()) { + canProfile = false; + } +#endif + + if (canProfile) { + nsCOMPtr<nsISerialEventTarget> gmpEventTarget = + mGMPParent->GMPEventTarget(); + if (!gmpEventTarget) { + return NS_ERROR_FAILURE; + } + + ipc::Endpoint<PProfilerChild> profilerParent( + ProfilerParent::CreateForProcess(mProcessId)); + + gmpEventTarget->Dispatch( + NewRunnableMethod<ipc::Endpoint<mozilla::PProfilerChild>&&>( + "GMPParent::SendInitProfiler", mGMPParent, + &GMPParent::SendInitProfiler, std::move(profilerParent))); + } + + return NS_OK; + } + + ::base::ProcessId mProcessId; + const RefPtr<GMPParent> mGMPParent; +}; + +nsresult GMPParent::LoadProcess() { + MOZ_ASSERT(mDirectory, "Plugin directory cannot be NULL!"); + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + MOZ_ASSERT(mState == GMPState::NotLoaded); + + nsAutoString path; + if (NS_WARN_IF(NS_FAILED(mDirectory->GetPath(path)))) { + return NS_ERROR_FAILURE; + } + GMP_PARENT_LOG_DEBUG("%s: for %s", __FUNCTION__, + NS_ConvertUTF16toUTF8(path).get()); + + if (!mProcess) { + mProcess = new GMPProcessParent(NS_ConvertUTF16toUTF8(path).get()); +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) + mProcess->SetRequiresWindowServer(mAdapter.EqualsLiteral("chromium")); +#endif + +#ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH + mProcess->SetLaunchArchitecture(mChildLaunchArch); +#endif + + if (!mProcess->Launch(30 * 1000)) { + GMP_PARENT_LOG_DEBUG("%s: Failed to launch new child process", + __FUNCTION__); + mProcess->Delete(); + mProcess = nullptr; + return NS_ERROR_FAILURE; + } + + mChildPid = mProcess->GetChildProcessId(); + GMP_PARENT_LOG_DEBUG("%s: Launched new child process", __FUNCTION__); + + bool opened = mProcess->TakeInitialEndpoint().Bind(this); + if (!opened) { + GMP_PARENT_LOG_DEBUG("%s: Failed to open channel to new child process", + __FUNCTION__); + mProcess->Delete(); + mProcess = nullptr; + return NS_ERROR_FAILURE; + } + GMP_PARENT_LOG_DEBUG("%s: Opened channel to new child process", + __FUNCTION__); + + // ComputeStorageId may return empty string, we leave the error handling to + // CDM. The CDM will reject the promise once we provide a empty string of + // storage id. + bool ok = + SendProvideStorageId(CDMStorageIdProvider::ComputeStorageId(mNodeId)); + if (!ok) { + GMP_PARENT_LOG_DEBUG("%s: Failed to send storage id to child process", + __FUNCTION__); + return NS_ERROR_FAILURE; + } + GMP_PARENT_LOG_DEBUG("%s: Sent storage id to child process", __FUNCTION__); + +#if defined(XP_WIN) || defined(XP_LINUX) + if (!mLibs.IsEmpty()) { + bool ok = SendPreloadLibs(mLibs); + if (!ok) { + GMP_PARENT_LOG_DEBUG("%s: Failed to send preload-libs to child process", + __FUNCTION__); + return NS_ERROR_FAILURE; + } + GMP_PARENT_LOG_DEBUG("%s: Sent preload-libs ('%s') to child process", + __FUNCTION__, mLibs.get()); + } +#endif + + NS_DispatchToMainThread(new NotifyGMPProcessLoadedTask(OtherPid(), this)); + + // Intr call to block initialization on plugin load. + if (!SendStartPlugin(mAdapter)) { + GMP_PARENT_LOG_DEBUG("%s: Failed to send start to child process", + __FUNCTION__); + return NS_ERROR_FAILURE; + } + GMP_PARENT_LOG_DEBUG("%s: Sent StartPlugin to child process", __FUNCTION__); + } + + mState = GMPState::Loaded; + + return NS_OK; +} + +mozilla::ipc::IPCResult GMPParent::RecvPGMPContentChildDestroyed() { + --mGMPContentChildCount; + if (!IsUsed()) { + CloseIfUnused(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPParent::RecvFOGData(ByteBuf&& aBuf) { + GMP_PARENT_LOG_DEBUG("GMPParent RecvFOGData"); + glean::FOGData(std::move(aBuf)); + return IPC_OK(); +} + +void GMPParent::CloseIfUnused() { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + GMP_PARENT_LOG_DEBUG("%s", __FUNCTION__); + + if ((mDeleteProcessOnlyOnUnload || mState == GMPState::Loaded || + mState == GMPState::Unloading) && + !IsUsed()) { + // Ensure all timers are killed. + for (uint32_t i = mTimers.Length(); i > 0; i--) { + mTimers[i - 1]->Shutdown(); + } + + // Shutdown GMPStorage. Given that all protocol actors must be shutdown + // (!Used() is true), all storage operations should be complete. + for (size_t i = mStorage.Length(); i > 0; i--) { + mStorage[i - 1]->Shutdown(); + } + Shutdown(); + } +} + +void GMPParent::CloseActive(bool aDieWhenUnloaded) { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + GMP_PARENT_LOG_DEBUG("%s: state %u", __FUNCTION__, + uint32_t(GMPState(mState))); + + if (aDieWhenUnloaded) { + mDeleteProcessOnlyOnUnload = true; // don't allow this to go back... + } + if (mState == GMPState::Loaded) { + mState = GMPState::Unloading; + } + if (mState != GMPState::NotLoaded && IsUsed()) { + Unused << SendCloseActive(); + CloseIfUnused(); + } +} + +void GMPParent::MarkForDeletion() { + mDeleteProcessOnlyOnUnload = true; + mIsBlockingDeletion = true; +} + +bool GMPParent::IsMarkedForDeletion() { return mIsBlockingDeletion; } + +void GMPParent::Shutdown() { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + GMP_PARENT_LOG_DEBUG("%s", __FUNCTION__); + + if (mAbnormalShutdownInProgress) { + return; + } + + MOZ_ASSERT(!IsUsed()); + if (mState == GMPState::NotLoaded || mState == GMPState::Closing) { + return; + } + + RefPtr<GMPParent> self(this); + DeleteProcess(); + + // XXX Get rid of mDeleteProcessOnlyOnUnload and this code when + // Bug 1043671 is fixed + if (!mDeleteProcessOnlyOnUnload) { + // Destroy ourselves and rise from the fire to save memory + mService->ReAddOnGMPThread(self); + } // else we've been asked to die and stay dead + MOZ_ASSERT(mState == GMPState::NotLoaded); +} + +class NotifyGMPShutdownTask : public Runnable { + public: + explicit NotifyGMPShutdownTask(const nsAString& aNodeId) + : Runnable("NotifyGMPShutdownTask"), mNodeId(aNodeId) {} + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIObserverService> obsService = + mozilla::services::GetObserverService(); + MOZ_ASSERT(obsService); + if (obsService) { + obsService->NotifyObservers(nullptr, "gmp-shutdown", mNodeId.get()); + } + return NS_OK; + } + nsString mNodeId; +}; + +void GMPParent::ChildTerminated() { + RefPtr<GMPParent> self(this); + nsCOMPtr<nsISerialEventTarget> gmpEventTarget = GMPEventTarget(); + + if (!gmpEventTarget) { + // Bug 1163239 - this can happen on shutdown. + // PluginTerminated removes the GMP from the GMPService. + // On shutdown we can have this case where it is already been + // removed so there is no harm in not trying to remove it again. + GMP_PARENT_LOG_DEBUG("%s::%s: GMPEventTarget() returned nullptr.", + __CLASS__, __FUNCTION__); + } else { + gmpEventTarget->Dispatch( + NewRunnableMethod<RefPtr<GMPParent>>( + "gmp::GeckoMediaPluginServiceParent::PluginTerminated", mService, + &GeckoMediaPluginServiceParent::PluginTerminated, self), + NS_DISPATCH_NORMAL); + } +} + +void GMPParent::DeleteProcess() { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + GMP_PARENT_LOG_DEBUG("%s", __FUNCTION__); + + if (mState != GMPState::Closing) { + // Don't Close() twice! + // Probably remove when bug 1043671 is resolved + mState = GMPState::Closing; + Close(); + } + mProcess->Delete(NewRunnableMethod("gmp::GMPParent::ChildTerminated", this, + &GMPParent::ChildTerminated)); + GMP_PARENT_LOG_DEBUG("%s: Shut down process", __FUNCTION__); + mProcess = nullptr; + +#if defined(MOZ_WIDGET_ANDROID) + if (mState != GMPState::NotLoaded) { + nsCOMPtr<nsIEventTarget> launcherThread(GetIPCLauncher()); + MOZ_ASSERT(launcherThread); + + auto procType = java::GeckoProcessType::GMPLUGIN(); + auto selector = + java::GeckoProcessManager::Selector::New(procType, OtherPid()); + + launcherThread->Dispatch(NS_NewRunnableFunction( + "GMPParent::DeleteProcess", + [selector = + java::GeckoProcessManager::Selector::GlobalRef(selector)]() { + java::GeckoProcessManager::ShutdownProcess(selector); + })); + } +#endif // defined(MOZ_WIDGET_ANDROID) + + mState = GMPState::NotLoaded; + + nsCOMPtr<nsIRunnable> r = + new NotifyGMPShutdownTask(NS_ConvertUTF8toUTF16(mNodeId)); + mMainThread->Dispatch(r.forget()); +} + +GMPState GMPParent::State() const { return mState; } + +nsCOMPtr<nsISerialEventTarget> GMPParent::GMPEventTarget() { + nsCOMPtr<mozIGeckoMediaPluginService> mps = + do_GetService("@mozilla.org/gecko-media-plugin-service;1"); + MOZ_ASSERT(mps); + if (!mps) { + return nullptr; + } + // Note: GeckoMediaPluginService::GetThread() is threadsafe, and returns + // nullptr if the GeckoMediaPluginService has started shutdown. + nsCOMPtr<nsIThread> gmpThread; + mps->GetThread(getter_AddRefs(gmpThread)); + return gmpThread; +} + +/* static */ +bool GMPCapability::Supports(const nsTArray<GMPCapability>& aCapabilities, + const nsACString& aAPI, + const nsTArray<nsCString>& aTags) { + for (const nsCString& tag : aTags) { + if (!GMPCapability::Supports(aCapabilities, aAPI, tag)) { + return false; + } + } + return true; +} + +/* static */ +bool GMPCapability::Supports(const nsTArray<GMPCapability>& aCapabilities, + const nsACString& aAPI, const nsCString& aTag) { + for (const GMPCapability& capabilities : aCapabilities) { + if (!capabilities.mAPIName.Equals(aAPI)) { + continue; + } + for (const nsCString& tag : capabilities.mAPITags) { + if (tag.Equals(aTag)) { +#ifdef XP_WIN + // Clearkey on Windows advertises that it can decode in its GMP info + // file, but uses Windows Media Foundation to decode. That's not present + // on Windows XP, and on some Vista, Windows N, and KN variants without + // certain services packs. + if (tag.EqualsLiteral(kClearKeyKeySystemName)) { + if (capabilities.mAPIName.EqualsLiteral(GMP_API_VIDEO_DECODER)) { + if (!WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::H264)) { + continue; + } + } + } +#endif + return true; + } + } + } + return false; +} + +bool GMPParent::EnsureProcessLoaded() { + switch (mState) { + case GMPState::NotLoaded: + return NS_SUCCEEDED(LoadProcess()); + case GMPState::Loaded: + return true; + case GMPState::Unloading: + case GMPState::Closing: + return false; + } + + MOZ_ASSERT_UNREACHABLE("Unhandled GMPState!"); + return false; +} + +void GMPParent::AddCrashAnnotations() { + if (mCrashReporter) { + mCrashReporter->AddAnnotation(CrashReporter::Annotation::GMPPlugin, true); + mCrashReporter->AddAnnotation(CrashReporter::Annotation::PluginFilename, + NS_ConvertUTF16toUTF8(mName)); + mCrashReporter->AddAnnotation(CrashReporter::Annotation::PluginName, + mDisplayName); + mCrashReporter->AddAnnotation(CrashReporter::Annotation::PluginVersion, + mVersion); + } +} + +void GMPParent::GetCrashID(nsString& aResult) { + AddCrashAnnotations(); + GenerateCrashReport(OtherPid(), &aResult); +} + +static void GMPNotifyObservers(const uint32_t aPluginID, + const nsACString& aPluginName, + const nsAString& aPluginDumpID) { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + nsCOMPtr<nsIWritablePropertyBag2> propbag = + do_CreateInstance("@mozilla.org/hash-property-bag;1"); + if (obs && propbag) { + propbag->SetPropertyAsUint32(u"pluginID"_ns, aPluginID); + propbag->SetPropertyAsACString(u"pluginName"_ns, aPluginName); + propbag->SetPropertyAsAString(u"pluginDumpID"_ns, aPluginDumpID); + obs->NotifyObservers(propbag, "gmp-plugin-crash", nullptr); + } + + RefPtr<gmp::GeckoMediaPluginService> service = + gmp::GeckoMediaPluginService::GetGeckoMediaPluginService(); + if (service) { + service->RunPluginCrashCallbacks(aPluginID, aPluginName); + } +} + +void GMPParent::ActorDestroy(ActorDestroyReason aWhy) { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + GMP_PARENT_LOG_DEBUG("%s: (%d)", __FUNCTION__, (int)aWhy); + + if (AbnormalShutdown == aWhy) { + Telemetry::Accumulate(Telemetry::SUBPROCESS_ABNORMAL_ABORT, "gmplugin"_ns, + 1); + nsString dumpID; + GetCrashID(dumpID); + if (dumpID.IsEmpty()) { + NS_WARNING("GMP crash without crash report"); + dumpID = mName; + dumpID += '-'; + AppendUTF8toUTF16(mVersion, dumpID); + } + + // NotifyObservers is mainthread-only + nsCOMPtr<nsIRunnable> r = + WrapRunnableNM(&GMPNotifyObservers, mPluginId, mDisplayName, dumpID); + mMainThread->Dispatch(r.forget()); + } + + // warn us off trying to close again + mState = GMPState::Closing; + mAbnormalShutdownInProgress = true; + CloseActive(false); + + // Normal Shutdown() will delete the process on unwind. + if (AbnormalShutdown == aWhy) { + RefPtr<GMPParent> self(this); + // Must not call Close() again in DeleteProcess(), as we'll recurse + // infinitely if we do. + MOZ_ASSERT(mState == GMPState::Closing); + DeleteProcess(); + // Note: final destruction will be Dispatched to ourself + mService->ReAddOnGMPThread(self); + } +} + +PGMPStorageParent* GMPParent::AllocPGMPStorageParent() { + GMPStorageParent* p = new GMPStorageParent(mNodeId, this); + mStorage.AppendElement(p); // Addrefs, released in DeallocPGMPStorageParent. + return p; +} + +bool GMPParent::DeallocPGMPStorageParent(PGMPStorageParent* aActor) { + GMPStorageParent* p = static_cast<GMPStorageParent*>(aActor); + p->Shutdown(); + mStorage.RemoveElement(p); + return true; +} + +mozilla::ipc::IPCResult GMPParent::RecvPGMPStorageConstructor( + PGMPStorageParent* aActor) { + GMPStorageParent* p = (GMPStorageParent*)aActor; + if (NS_FAILED(p->Init())) { + // TODO: Verify if this is really a good reason to IPC_FAIL. + // There might be shutdown edge cases here. + return IPC_FAIL(this, + "GMPParent::RecvPGMPStorageConstructor: p->Init() failed."); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPParent::RecvPGMPTimerConstructor( + PGMPTimerParent* actor) { + return IPC_OK(); +} + +PGMPTimerParent* GMPParent::AllocPGMPTimerParent() { + nsCOMPtr<nsISerialEventTarget> target = GMPEventTarget(); + GMPTimerParent* p = new GMPTimerParent(target); + mTimers.AppendElement( + p); // Released in DeallocPGMPTimerParent, or on shutdown. + return p; +} + +bool GMPParent::DeallocPGMPTimerParent(PGMPTimerParent* aActor) { + GMPTimerParent* p = static_cast<GMPTimerParent*>(aActor); + p->Shutdown(); + mTimers.RemoveElement(p); + return true; +} + +bool ReadInfoField(GMPInfoFileParser& aParser, const nsCString& aKey, + nsACString& aOutValue) { + if (!aParser.Contains(aKey) || aParser.Get(aKey).IsEmpty()) { + return false; + } + aOutValue = aParser.Get(aKey); + return true; +} + +RefPtr<GenericPromise> GMPParent::ReadGMPMetaData() { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + MOZ_ASSERT(mDirectory, "Plugin directory cannot be NULL!"); + MOZ_ASSERT(!mName.IsEmpty(), "Plugin mName cannot be empty!"); + + nsCOMPtr<nsIFile> infoFile; + nsresult rv = mDirectory->Clone(getter_AddRefs(infoFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return GenericPromise::CreateAndReject(rv, __func__); + } + infoFile->AppendRelativePath(mName + u".info"_ns); + + if (FileExists(infoFile)) { + return ReadGMPInfoFile(infoFile); + } + + // Maybe this is the Widevine adapted plugin? + nsCOMPtr<nsIFile> manifestFile; + rv = mDirectory->Clone(getter_AddRefs(manifestFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return GenericPromise::CreateAndReject(rv, __func__); + } + manifestFile->AppendRelativePath(u"manifest.json"_ns); + return ReadChromiumManifestFile(manifestFile); +} + +#if defined(XP_LINUX) +static void ApplyGlibcWorkaround(nsCString& aLibs) { + // These glibc libraries were merged into libc.so.6 as of glibc + // 2.34; they now exist only as stub libraries for compatibility and + // newly linked code won't depend on them, so we need to ensure + // they're loaded for plugins that may have been linked against a + // different version of glibc. (See also bug 1725828.) + if (!aLibs.IsEmpty()) { + aLibs.AppendLiteral(", "); + } + aLibs.AppendLiteral("libdl.so.2, libpthread.so.0, librt.so.1"); +} +#endif + +#if defined(XP_WIN) +static void ApplyOleaut32(nsCString& aLibs) { + // In the libwebrtc update in bug 1766646 an include of comdef.h for using + // _bstr_t was introduced. This resulted in a dependency on comsupp.lib which + // contains a `_variant_t vtMissing` that would get cleared in an exit + // handler. VariantClear is defined in oleaut32.dll, and so we'd try to load + // oleaut32.dll on exit but get denied by the sandbox. + // Note that we had includes of comdef.h before bug 1766646 but it is the use + // of _bstr_t that triggers the vtMissing exit handler. + // See bug 1788592 for details. + if (!aLibs.IsEmpty()) { + aLibs.AppendLiteral(", "); + } + aLibs.AppendLiteral("oleaut32.dll"); +} +#endif + +RefPtr<GenericPromise> GMPParent::ReadGMPInfoFile(nsIFile* aFile) { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + GMPInfoFileParser parser; + if (!parser.Init(aFile)) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + nsAutoCString apis; + if (!ReadInfoField(parser, "name"_ns, mDisplayName) || + !ReadInfoField(parser, "description"_ns, mDescription) || + !ReadInfoField(parser, "version"_ns, mVersion) || + !ReadInfoField(parser, "apis"_ns, apis)) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + +#if defined(XP_WIN) || defined(XP_LINUX) + // "Libraries" field is optional. + ReadInfoField(parser, "libraries"_ns, mLibs); +#endif + + UpdatePluginType(); + +#ifdef XP_LINUX + // The glibc workaround (see above) isn't needed for clearkey + // because it's built along with the browser. + if (mPluginType != GMPPluginType::Clearkey) { + ApplyGlibcWorkaround(mLibs); + } +#endif + +#ifdef XP_WIN + ApplyOleaut32(mLibs); +#endif + + nsTArray<nsCString> apiTokens; + SplitAt(", ", apis, apiTokens); + for (nsCString api : apiTokens) { + int32_t tagsStart = api.FindChar('['); + if (tagsStart == 0) { + // Not allowed to be the first character. + // API name must be at least one character. + continue; + } + + GMPCapability cap; + if (tagsStart == -1) { + // No tags. + cap.mAPIName.Assign(api); + } else { + auto tagsEnd = api.FindChar(']'); + if (tagsEnd == -1 || tagsEnd < tagsStart) { + // Invalid syntax, skip whole capability. + continue; + } + + cap.mAPIName.Assign(Substring(api, 0, tagsStart)); + + if ((tagsEnd - tagsStart) > 1) { + const nsDependentCSubstring ts( + Substring(api, tagsStart + 1, tagsEnd - tagsStart - 1)); + nsTArray<nsCString> tagTokens; + SplitAt(":", ts, tagTokens); + for (nsCString tag : tagTokens) { + cap.mAPITags.AppendElement(tag); + } + } + } + + mCapabilities.AppendElement(std::move(cap)); + } + + if (mCapabilities.IsEmpty()) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + return GenericPromise::CreateAndResolve(true, __func__); +} + +RefPtr<GenericPromise> GMPParent::ReadChromiumManifestFile(nsIFile* aFile) { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + nsAutoCString json; + if (!ReadIntoString(aFile, json, 5 * 1024)) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + // DOM JSON parsing needs to run on the main thread. + return InvokeAsync(mMainThread, this, __func__, + &GMPParent::ParseChromiumManifest, + NS_ConvertUTF8toUTF16(json)); +} + +static bool IsCDMAPISupported( + const mozilla::dom::WidevineCDMManifest& aManifest) { + nsresult ignored; // Note: ToInteger returns 0 on failure. + int32_t moduleVersion = aManifest.mX_cdm_module_versions.ToInteger(&ignored); + int32_t interfaceVersion = + aManifest.mX_cdm_interface_versions.ToInteger(&ignored); + int32_t hostVersion = aManifest.mX_cdm_host_versions.ToInteger(&ignored); + return ChromiumCDMAdapter::Supports(moduleVersion, interfaceVersion, + hostVersion); +} + +RefPtr<GenericPromise> GMPParent::ParseChromiumManifest( + const nsAString& aJSON) { + GMP_PARENT_LOG_DEBUG("%s: for '%s'", __FUNCTION__, + NS_LossyConvertUTF16toASCII(aJSON).get()); + + MOZ_ASSERT(NS_IsMainThread()); + mozilla::dom::WidevineCDMManifest m; + if (!m.Init(aJSON)) { + GMP_PARENT_LOG_DEBUG("%s: Failed to initialize json parser, failing.", + __FUNCTION__); + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + if (!IsCDMAPISupported(m)) { + GMP_PARENT_LOG_DEBUG("%s: CDM API not supported, failing.", __FUNCTION__); + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + CopyUTF16toUTF8(m.mName, mDisplayName); + CopyUTF16toUTF8(m.mDescription, mDescription); + CopyUTF16toUTF8(m.mVersion, mVersion); + +#if defined(XP_LINUX) && defined(MOZ_SANDBOX) + if (!mozilla::SandboxInfo::Get().CanSandboxMedia()) { + nsPrintfCString msg( + "GMPParent::ParseChromiumManifest: Plugin \"%s\" is an EME CDM" + " but this system can't sandbox it; not loading.", + mDisplayName.get()); + printf_stderr("%s\n", msg.get()); + GMP_PARENT_LOG_DEBUG("%s", msg.get()); + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } +#endif + + UpdatePluginType(); + + GMPCapability video; + + // We hard code a few of the settings because they can't be stored in the + // widevine manifest without making our API different to widevine's. + switch (mPluginType) { + case GMPPluginType::Clearkey: + video.mAPITags.AppendElement(nsCString{kClearKeyKeySystemName}); + video.mAPITags.AppendElement( + nsCString{kClearKeyWithProtectionQueryKeySystemName}); +#if XP_WIN + mLibs = nsLiteralCString( + "dxva2.dll, evr.dll, freebl3.dll, mfh264dec.dll, mfplat.dll, " + "msmpeg2vdec.dll, nss3.dll, softokn3.dll"); +#elif XP_LINUX + mLibs = "libfreeblpriv3.so, libsoftokn3.so"_ns; +#endif + break; + case GMPPluginType::Widevine: + video.mAPITags.AppendElement(nsCString{kWidevineKeySystemName}); +#if XP_WIN + // psapi.dll added for GetMappedFileNameW, which could possibly be avoided + // in future versions, see bug 1383611 for details. + mLibs = "dxva2.dll, ole32.dll, psapi.dll, winmm.dll"_ns; +#endif + break; + case GMPPluginType::Fake: + // The fake CDM just exposes a key system with id "fake". + video.mAPITags.AppendElement(nsCString{"fake"}); +#if XP_WIN + mLibs = "dxva2.dll, ole32.dll"_ns; +#endif + break; + default: + GMP_PARENT_LOG_DEBUG("%s: Unrecognized key system: %s, failing.", + __FUNCTION__, mDisplayName.get()); + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + +#ifdef XP_LINUX + ApplyGlibcWorkaround(mLibs); +#endif + +#ifdef XP_WIN + ApplyOleaut32(mLibs); +#endif + + nsCString codecsString = NS_ConvertUTF16toUTF8(m.mX_cdm_codecs); + nsTArray<nsCString> codecs; + SplitAt(",", codecsString, codecs); + + // Parse the codec strings in the manifest and map them to strings used + // internally by Gecko for capability recognition. + // + // Google's code to parse manifests can be used as a reference for strings + // the manifest may contain + // https://source.chromium.org/chromium/chromium/src/+/master:components/cdm/common/cdm_manifest.cc;l=74;drc=775880ced8a989191281e93854c7f2201f25068f + // + // Gecko's internal strings can be found at + // https://searchfox.org/mozilla-central/rev/ea63a0888d406fae720cf24f4727d87569a8cab5/dom/media/eme/MediaKeySystemAccess.cpp#149-155 + for (const nsCString& chromiumCodec : codecs) { + nsCString codec; + if (chromiumCodec.EqualsASCII("vp8")) { + codec = "vp8"_ns; + } else if (chromiumCodec.EqualsASCII("vp9.0") || // Legacy string. + chromiumCodec.EqualsASCII("vp09")) { + codec = "vp9"_ns; + } else if (chromiumCodec.EqualsASCII("avc1")) { + codec = "h264"_ns; + } else if (chromiumCodec.EqualsASCII("av01")) { + codec = "av1"_ns; + } else { + GMP_PARENT_LOG_DEBUG("%s: Unrecognized codec: %s.", __FUNCTION__, + chromiumCodec.get()); + MOZ_ASSERT_UNREACHABLE( + "Unhandled codec string! Need to add it to the parser."); + continue; + } + + video.mAPITags.AppendElement(codec); + } + + video.mAPIName = nsLiteralCString(CHROMIUM_CDM_API); + mAdapter = u"chromium"_ns; + + mCapabilities.AppendElement(std::move(video)); + + GMP_PARENT_LOG_DEBUG("%s: Successfully parsed manifest.", __FUNCTION__); + return GenericPromise::CreateAndResolve(true, __func__); +} + +bool GMPParent::CanBeSharedCrossNodeIds() const { + return mNodeId.IsEmpty() && + // XXX bug 1159300 hack -- maybe remove after openh264 1.4 + // We don't want to use CDM decoders for non-encrypted playback + // just yet; especially not for WebRTC. Don't allow CDMs to be used + // without a node ID. + !mCanDecrypt; +} + +bool GMPParent::CanBeUsedFrom(const nsACString& aNodeId) const { + return mNodeId == aNodeId; +} + +void GMPParent::SetNodeId(const nsACString& aNodeId) { + MOZ_ASSERT(!aNodeId.IsEmpty()); + mNodeId = aNodeId; +} + +void GMPParent::UpdatePluginType() { + if (mDisplayName.EqualsLiteral("WidevineCdm")) { + mPluginType = GMPPluginType::Widevine; + } else if (mDisplayName.EqualsLiteral("gmpopenh264")) { + mPluginType = GMPPluginType::OpenH264; + } else if (mDisplayName.EqualsLiteral("clearkey")) { + mPluginType = GMPPluginType::Clearkey; + } else if (mDisplayName.EqualsLiteral("fake")) { + mPluginType = GMPPluginType::Fake; + } else { + mPluginType = GMPPluginType::Unknown; + } +} + +const nsCString& GMPParent::GetDisplayName() const { return mDisplayName; } + +const nsCString& GMPParent::GetVersion() const { return mVersion; } + +uint32_t GMPParent::GetPluginId() const { return mPluginId; } + +void GMPParent::ResolveGetContentParentPromises() { + nsTArray<UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>>> promises = + std::move(mGetContentParentPromises); + MOZ_ASSERT(mGetContentParentPromises.IsEmpty()); + RefPtr<GMPContentParent::CloseBlocker> blocker( + new GMPContentParent::CloseBlocker(mGMPContentParent)); + for (auto& holder : promises) { + holder->Resolve(blocker, __func__); + } +} + +bool GMPParent::OpenPGMPContent() { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + MOZ_ASSERT(!mGMPContentParent); + + Endpoint<PGMPContentParent> parent; + Endpoint<PGMPContentChild> child; + if (NS_WARN_IF(NS_FAILED(PGMPContent::CreateEndpoints( + base::GetCurrentProcId(), OtherPid(), &parent, &child)))) { + return false; + } + + mGMPContentParent = new GMPContentParent(this); + + if (!parent.Bind(mGMPContentParent)) { + return false; + } + + if (!SendInitGMPContentChild(std::move(child))) { + return false; + } + + ResolveGetContentParentPromises(); + + return true; +} + +void GMPParent::RejectGetContentParentPromises() { + nsTArray<UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>>> promises = + std::move(mGetContentParentPromises); + MOZ_ASSERT(mGetContentParentPromises.IsEmpty()); + for (auto& holder : promises) { + holder->Reject(NS_ERROR_FAILURE, __func__); + } +} + +void GMPParent::GetGMPContentParent( + UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>>&& aPromiseHolder) { + MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread()); + GMP_PARENT_LOG_DEBUG("%s %p", __FUNCTION__, this); + + if (mGMPContentParent) { + RefPtr<GMPContentParent::CloseBlocker> blocker( + new GMPContentParent::CloseBlocker(mGMPContentParent)); + aPromiseHolder->Resolve(blocker, __func__); + } else { + mGetContentParentPromises.AppendElement(std::move(aPromiseHolder)); + // If we don't have a GMPContentParent and we try to get one for the first + // time (mGetContentParentPromises.Length() == 1) then call + // PGMPContent::Open. If more calls to GetGMPContentParent happen before + // mGMPContentParent has been set then we should just store them, so that + // they get called when we set mGMPContentParent as a result of the + // PGMPContent::Open call. + if (mGetContentParentPromises.Length() == 1) { + if (!EnsureProcessLoaded() || !OpenPGMPContent()) { + RejectGetContentParentPromises(); + return; + } + // We want to increment this as soon as possible, to avoid that we'd try + // to shut down the GMP process while we're still trying to get a + // PGMPContentParent actor. + ++mGMPContentChildCount; + } + } +} + +already_AddRefed<GMPContentParent> GMPParent::ForgetGMPContentParent() { + MOZ_ASSERT(mGetContentParentPromises.IsEmpty()); + return mGMPContentParent.forget(); +} + +bool GMPParent::EnsureProcessLoaded(base::ProcessId* aID) { + if (!EnsureProcessLoaded()) { + return false; + } + *aID = OtherPid(); + return true; +} + +void GMPParent::IncrementGMPContentChildCount() { ++mGMPContentChildCount; } + +nsString GMPParent::GetPluginBaseName() const { return u"gmp-"_ns + mName; } + +#if defined(XP_MACOSX) && defined(__aarch64__) +void GMPParent::PreTranslateBins() { + nsCOMPtr<nsIRunnable> event = mozilla::NewRunnableMethod( + "RosettaTranslation", this, &GMPParent::PreTranslateBinsWorker); + + DebugOnly<nsresult> rv = + NS_DispatchBackgroundTask(event.forget(), NS_DISPATCH_EVENT_MAY_BLOCK); + + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void GMPParent::PreTranslateBinsWorker() { + int rv = nsMacUtilsImpl::PreTranslateXUL(); + GMP_PARENT_LOG_DEBUG("%s: XUL translation result: %d", __FUNCTION__, rv); + + rv = nsMacUtilsImpl::PreTranslateBinary(mPluginFilePath); + GMP_PARENT_LOG_DEBUG("%s: %s translation result: %d", __FUNCTION__, + mPluginFilePath.get(), rv); +} +#endif + +} // namespace mozilla::gmp + +#undef GMP_PARENT_LOG_DEBUG +#undef __CLASS__ diff --git a/dom/media/gmp/GMPParent.h b/dom/media/gmp/GMPParent.h new file mode 100644 index 0000000000..25fe1f88d1 --- /dev/null +++ b/dom/media/gmp/GMPParent.h @@ -0,0 +1,244 @@ +/* -*- 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/. */ + +#ifndef GMPParent_h_ +#define GMPParent_h_ + +#include "GMPNativeTypes.h" +#include "GMPProcessParent.h" +#include "GMPServiceParent.h" +#include "GMPVideoDecoderParent.h" +#include "GMPVideoEncoderParent.h" +#include "GMPTimerParent.h" +#include "GMPStorageParent.h" +#include "mozilla/gmp/PGMPParent.h" +#include "mozilla/ipc/CrashReporterHelper.h" +#include "nsCOMPtr.h" +#include "nscore.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsIFile.h" +#include "mozilla/Atomics.h" +#include "mozilla/MozPromise.h" + +namespace mozilla::gmp { + +class GMPCapability { + public: + explicit GMPCapability() = default; + GMPCapability(GMPCapability&& aOther) + : mAPIName(std::move(aOther.mAPIName)), + mAPITags(std::move(aOther.mAPITags)) {} + explicit GMPCapability(const nsACString& aAPIName) : mAPIName(aAPIName) {} + explicit GMPCapability(const GMPCapability& aOther) = default; + nsCString mAPIName; + CopyableTArray<nsCString> mAPITags; + + static bool Supports(const nsTArray<GMPCapability>& aCapabilities, + const nsACString& aAPI, + const nsTArray<nsCString>& aTags); + + static bool Supports(const nsTArray<GMPCapability>& aCapabilities, + const nsACString& aAPI, const nsCString& aTag); +}; + +enum class GMPState : uint32_t { NotLoaded, Loaded, Unloading, Closing }; + +class GMPContentParent; + +class GMPParent final + : public PGMPParent, + public ipc::CrashReporterHelper<GeckoProcessType_GMPlugin> { + friend class PGMPParent; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPParent, final) + + GMPParent(); + + RefPtr<GenericPromise> Init(GeckoMediaPluginServiceParent* aService, + nsIFile* aPluginDir); + void CloneFrom(const GMPParent* aOther); + + void Crash(); + + nsresult LoadProcess(); + + // Called internally to close this if we don't need it + void CloseIfUnused(); + + // Notify all active de/encoders that we are closing, either because of + // normal shutdown or unexpected shutdown/crash. + void CloseActive(bool aDieWhenUnloaded); + + // Tell the plugin to die after shutdown. + void MarkForDeletion(); + bool IsMarkedForDeletion(); + + // Called by the GMPService to forcibly close active de/encoders at shutdown + void Shutdown(); + + // This must not be called while we're in the middle of abnormal ActorDestroy + void DeleteProcess(); + + GMPState State() const; + nsCOMPtr<nsISerialEventTarget> GMPEventTarget(); + + // A GMP can either be a single instance shared across all NodeIds (like + // in the OpenH264 case), or we can require a new plugin instance for every + // NodeIds running the plugin (as in the EME plugin case). + // + // A NodeId is a hash of the ($urlBarOrigin, $ownerDocOrigin) pair. + // + // Plugins are associated with an NodeIds by calling SetNodeId() before + // loading. + // + // If a plugin has no NodeId specified and it is loaded, it is assumed to + // be shared across NodeIds. + + // Specifies that a GMP can only work with the specified NodeIds. + void SetNodeId(const nsACString& aNodeId); + const nsACString& GetNodeId() const { return mNodeId; } + + const nsCString& GetDisplayName() const; + const nsCString& GetVersion() const; + uint32_t GetPluginId() const; + GMPPluginType GetPluginType() const { return mPluginType; } + nsString GetPluginBaseName() const; + + // Returns true if a plugin can be or is being used across multiple NodeIds. + bool CanBeSharedCrossNodeIds() const; + + // A GMP can be used from a NodeId if it's already been set to work with + // that NodeId, or if it's not been set to work with any NodeId and has + // not yet been loaded (i.e. it's not shared across NodeIds). + bool CanBeUsedFrom(const nsACString& aNodeId) const; + + already_AddRefed<nsIFile> GetDirectory() { + return nsCOMPtr<nsIFile>(mDirectory).forget(); + } + + void AbortAsyncShutdown(); + + // Called when the child process has died. + void ChildTerminated(); + + bool OpenPGMPContent(); + + void GetGMPContentParent( + UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>>&& aPromiseHolder); + already_AddRefed<GMPContentParent> ForgetGMPContentParent(); + + bool EnsureProcessLoaded(base::ProcessId* aID); + + void IncrementGMPContentChildCount(); + + const nsTArray<GMPCapability>& GetCapabilities() const { + return mCapabilities; + } + + private: + ~GMPParent(); + void UpdatePluginType(); + + RefPtr<GeckoMediaPluginServiceParent> mService; + bool EnsureProcessLoaded(); + RefPtr<GenericPromise> ReadGMPMetaData(); + RefPtr<GenericPromise> ReadGMPInfoFile(nsIFile* aFile); + RefPtr<GenericPromise> ParseChromiumManifest( + const nsAString& aJSON); // Worker thread. + RefPtr<GenericPromise> ReadChromiumManifestFile( + nsIFile* aFile); // GMP thread. + void AddCrashAnnotations(); + void GetCrashID(nsString& aResult); + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvPGMPStorageConstructor( + PGMPStorageParent* actor) override; + PGMPStorageParent* AllocPGMPStorageParent(); + bool DeallocPGMPStorageParent(PGMPStorageParent* aActor); + + mozilla::ipc::IPCResult RecvPGMPTimerConstructor( + PGMPTimerParent* actor) override; + PGMPTimerParent* AllocPGMPTimerParent(); + bool DeallocPGMPTimerParent(PGMPTimerParent* aActor); + + mozilla::ipc::IPCResult RecvPGMPContentChildDestroyed(); + + mozilla::ipc::IPCResult RecvFOGData(ByteBuf&& aBuf); + + bool IsUsed() { + return mGMPContentChildCount > 0 || !mGetContentParentPromises.IsEmpty(); + } + + void ResolveGetContentParentPromises(); + void RejectGetContentParentPromises(); + +#if defined(XP_MACOSX) && defined(__aarch64__) + // We pre-translate XUL and our plugin file to avoid x64 child process + // startup delays caused by translation for instances when the child + // process binary translations have not already been cached. i.e., the + // first time we launch an x64 child process after installation or + // update. Measured by binary size of a recent XUL and Widevine plugin, + // this makes up 94% of the translation needed. Re-translating the + // same binary does not cause translation to occur again. + void PreTranslateBins(); + void PreTranslateBinsWorker(); +#endif + +#if defined(XP_WIN) || defined(XP_MACOSX) + nsresult GetPluginFileArch(nsIFile* aPluginDir, const nsString& aBaseName, + uint32_t& aArchSet); +#endif + + Atomic<GMPState> mState; + nsCOMPtr<nsIFile> mDirectory; // plugin directory on disk + nsString mName; // base name of plugin on disk, UTF-16 because used for paths + nsCString mDisplayName; // name of plugin displayed to users + nsCString mDescription; // description of plugin for display to users + nsCString mVersion; +#if defined(XP_WIN) || defined(XP_LINUX) + nsCString mLibs; +#endif + nsString mAdapter; + const uint32_t mPluginId; + GMPPluginType mPluginType = GMPPluginType::Unknown; + nsTArray<GMPCapability> mCapabilities; + GMPProcessParent* mProcess; + bool mDeleteProcessOnlyOnUnload; + bool mAbnormalShutdownInProgress; + bool mIsBlockingDeletion; + + bool mCanDecrypt; + + nsTArray<RefPtr<GMPTimerParent>> mTimers; + nsTArray<RefPtr<GMPStorageParent>> mStorage; + // NodeId the plugin is assigned to, or empty if the the plugin is not + // assigned to a NodeId. + nsCString mNodeId; + // This is used for GMP content in the parent, there may be more of these in + // the content processes. + RefPtr<GMPContentParent> mGMPContentParent; + nsTArray<UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>>> + mGetContentParentPromises; + uint32_t mGMPContentChildCount; + + int mChildPid; + +#ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH + // The child process architecture to use. + uint32_t mChildLaunchArch; +#endif +#if defined(XP_MACOSX) && defined(__aarch64__) + nsCString mPluginFilePath; +#endif + + const nsCOMPtr<nsISerialEventTarget> mMainThread; +}; + +} // namespace mozilla::gmp + +#endif // GMPParent_h_ diff --git a/dom/media/gmp/GMPPlatform.cpp b/dom/media/gmp/GMPPlatform.cpp new file mode 100644 index 0000000000..1a76015ed4 --- /dev/null +++ b/dom/media/gmp/GMPPlatform.cpp @@ -0,0 +1,281 @@ +/* -*- 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 "GMPPlatform.h" +#include "GMPStorageChild.h" +#include "GMPTimerChild.h" +#include "mozilla/Monitor.h" +#include "GMPChild.h" +#include "mozilla/Mutex.h" +#include "base/thread.h" +#include "base/time.h" +#include "mozilla/ReentrantMonitor.h" + +#include <ctime> + +namespace mozilla::gmp { + +static MessageLoop* sMainLoop = nullptr; +static GMPChild* sChild = nullptr; + +static bool IsOnChildMainThread() { + return sMainLoop && sMainLoop == MessageLoop::current(); +} + +// We just need a refcounted wrapper for GMPTask objects. +class GMPRunnable final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPRunnable) + + explicit GMPRunnable(GMPTask* aTask) : mTask(aTask) { MOZ_ASSERT(mTask); } + + void Run() { + mTask->Run(); + mTask->Destroy(); + mTask = nullptr; + } + + private: + ~GMPRunnable() = default; + + GMPTask* mTask; +}; + +class GMPSyncRunnable final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPSyncRunnable) + + GMPSyncRunnable(GMPTask* aTask, MessageLoop* aMessageLoop) + : mDone(false), + mTask(aTask), + mMessageLoop(aMessageLoop), + mMonitor("GMPSyncRunnable") { + MOZ_ASSERT(mTask); + MOZ_ASSERT(mMessageLoop); + } + + void Post() { + // We assert here for two reasons. + // 1) Nobody should be blocking the main thread. + // 2) This prevents deadlocks when doing sync calls to main which if the + // main thread tries to do a sync call back to the calling thread. + MOZ_ASSERT(!IsOnChildMainThread()); + + mMessageLoop->PostTask(NewRunnableMethod("gmp::GMPSyncRunnable::Run", this, + &GMPSyncRunnable::Run)); + MonitorAutoLock lock(mMonitor); + while (!mDone) { + lock.Wait(); + } + } + + void Run() { + mTask->Run(); + mTask->Destroy(); + mTask = nullptr; + MonitorAutoLock lock(mMonitor); + mDone = true; + lock.Notify(); + } + + private: + ~GMPSyncRunnable() = default; + + bool mDone; + GMPTask* mTask; + MessageLoop* mMessageLoop; + Monitor mMonitor MOZ_UNANNOTATED; +}; + +class GMPThreadImpl : public GMPThread { + public: + GMPThreadImpl(); + virtual ~GMPThreadImpl(); + + // GMPThread + void Post(GMPTask* aTask) override; + void Join() override; + + private: + Mutex mMutex MOZ_UNANNOTATED; + base::Thread mThread; +}; + +GMPErr CreateThread(GMPThread** aThread) { + if (!aThread) { + return GMPGenericErr; + } + + *aThread = new GMPThreadImpl(); + + return GMPNoErr; +} + +GMPErr RunOnMainThread(GMPTask* aTask) { + if (!aTask || !sMainLoop) { + return GMPGenericErr; + } + + RefPtr<GMPRunnable> r = new GMPRunnable(aTask); + sMainLoop->PostTask( + NewRunnableMethod("gmp::GMPRunnable::Run", r, &GMPRunnable::Run)); + + return GMPNoErr; +} + +GMPErr SyncRunOnMainThread(GMPTask* aTask) { + if (!aTask || !sMainLoop || IsOnChildMainThread()) { + return GMPGenericErr; + } + + RefPtr<GMPSyncRunnable> r = new GMPSyncRunnable(aTask, sMainLoop); + + r->Post(); + + return GMPNoErr; +} + +class GMPMutexImpl : public GMPMutex { + public: + GMPMutexImpl(); + virtual ~GMPMutexImpl(); + + // GMPMutex + void Acquire() override; + void Release() override; + void Destroy() override; + + private: + ReentrantMonitor mMonitor MOZ_UNANNOTATED; +}; + +GMPErr CreateMutex(GMPMutex** aMutex) { + if (!aMutex) { + return GMPGenericErr; + } + + *aMutex = new GMPMutexImpl(); + + return GMPNoErr; +} + +GMPErr CreateRecord(const char* aRecordName, uint32_t aRecordNameSize, + GMPRecord** aOutRecord, GMPRecordClient* aClient) { + if (aRecordNameSize > GMP_MAX_RECORD_NAME_SIZE || aRecordNameSize == 0) { + NS_WARNING("GMP tried to CreateRecord with too long or 0 record name"); + return GMPGenericErr; + } + GMPStorageChild* storage = sChild->GetGMPStorage(); + if (!storage) { + return GMPGenericErr; + } + MOZ_ASSERT(storage); + return storage->CreateRecord(nsDependentCString(aRecordName, aRecordNameSize), + aOutRecord, aClient); +} + +GMPErr SetTimerOnMainThread(GMPTask* aTask, int64_t aTimeoutMS) { + if (!aTask || !sMainLoop || !IsOnChildMainThread()) { + return GMPGenericErr; + } + GMPTimerChild* timers = sChild->GetGMPTimers(); + NS_ENSURE_TRUE(timers, GMPGenericErr); + return timers->SetTimer(aTask, aTimeoutMS); +} + +GMPErr GetClock(GMPTimestamp* aOutTime) { + if (!aOutTime) { + return GMPGenericErr; + } + *aOutTime = base::Time::Now().ToDoubleT() * 1000.0; + return GMPNoErr; +} + +void InitPlatformAPI(GMPPlatformAPI& aPlatformAPI, GMPChild* aChild) { + if (!sMainLoop) { + sMainLoop = MessageLoop::current(); + } + if (!sChild) { + sChild = aChild; + } + + aPlatformAPI.version = 0; + aPlatformAPI.createthread = &CreateThread; + aPlatformAPI.runonmainthread = &RunOnMainThread; + aPlatformAPI.syncrunonmainthread = &SyncRunOnMainThread; + aPlatformAPI.createmutex = &CreateMutex; + aPlatformAPI.createrecord = &CreateRecord; + aPlatformAPI.settimer = &SetTimerOnMainThread; + aPlatformAPI.getcurrenttime = &GetClock; +} + +void SendFOGData(ipc::ByteBuf&& buf) { + if (sChild) { + sChild->SendFOGData(std::move(buf)); + } +} + +GMPThreadImpl::GMPThreadImpl() : mMutex("GMPThreadImpl"), mThread("GMPThread") { + MOZ_COUNT_CTOR(GMPThread); +} + +GMPThreadImpl::~GMPThreadImpl() { MOZ_COUNT_DTOR(GMPThread); } + +void GMPThreadImpl::Post(GMPTask* aTask) { + MutexAutoLock lock(mMutex); + + if (!mThread.IsRunning()) { + bool started = mThread.Start(); + if (!started) { + NS_WARNING("Unable to start GMPThread!"); + return; + } + } + + RefPtr<GMPRunnable> r = new GMPRunnable(aTask); + mThread.message_loop()->PostTask( + NewRunnableMethod("gmp::GMPRunnable::Run", r.get(), &GMPRunnable::Run)); +} + +void GMPThreadImpl::Join() { + { + MutexAutoLock lock(mMutex); + if (mThread.IsRunning()) { + mThread.Stop(); + } + } + delete this; +} + +GMPMutexImpl::GMPMutexImpl() : mMonitor("gmp-mutex") { + MOZ_COUNT_CTOR(GMPMutexImpl); +} + +GMPMutexImpl::~GMPMutexImpl() { MOZ_COUNT_DTOR(GMPMutexImpl); } + +void GMPMutexImpl::Destroy() { delete this; } + +MOZ_PUSH_IGNORE_THREAD_SAFETY +void GMPMutexImpl::Acquire() { mMonitor.Enter(); } + +void GMPMutexImpl::Release() { mMonitor.Exit(); } +MOZ_POP_THREAD_SAFETY + +GMPTask* NewGMPTask(std::function<void()>&& aFunction) { + class Task : public GMPTask { + public: + explicit Task(std::function<void()>&& aFunction) + : mFunction(std::move(aFunction)) {} + void Destroy() override { delete this; } + ~Task() override = default; + void Run() override { mFunction(); } + + private: + std::function<void()> mFunction; + }; + return new Task(std::move(aFunction)); +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPPlatform.h b/dom/media/gmp/GMPPlatform.h new file mode 100644 index 0000000000..ee4571d856 --- /dev/null +++ b/dom/media/gmp/GMPPlatform.h @@ -0,0 +1,32 @@ +/* -*- 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/. */ + +#ifndef GMPPlatform_h_ +#define GMPPlatform_h_ + +#include "gmp-platform.h" +#include <functional> + +namespace mozilla::ipc { +class ByteBuf; +} // namespace mozilla::ipc + +namespace mozilla::gmp { + +class GMPChild; + +void InitPlatformAPI(GMPPlatformAPI& aPlatformAPI, GMPChild* aChild); + +GMPErr RunOnMainThread(GMPTask* aTask); + +GMPTask* NewGMPTask(std::function<void()>&& aFunction); + +GMPErr SetTimerOnMainThread(GMPTask* aTask, int64_t aTimeoutMS); + +void SendFOGData(ipc::ByteBuf&& buf); + +} // namespace mozilla::gmp + +#endif // GMPPlatform_h_ diff --git a/dom/media/gmp/GMPProcessChild.cpp b/dom/media/gmp/GMPProcessChild.cpp new file mode 100644 index 0000000000..17c70079cb --- /dev/null +++ b/dom/media/gmp/GMPProcessChild.cpp @@ -0,0 +1,45 @@ +/* -*- 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 "GMPProcessChild.h" + +#include "base/command_line.h" +#include "base/string_util.h" +#include "mozilla/ipc/IOThreadChild.h" +#include "mozilla/BackgroundHangMonitor.h" + +using mozilla::ipc::IOThreadChild; + +namespace mozilla::gmp { + +GMPProcessChild::~GMPProcessChild() = default; + +bool GMPProcessChild::Init(int aArgc, char* aArgv[]) { + nsAutoString pluginFilename; + +#if defined(OS_POSIX) + // NB: need to be very careful in ensuring that the first arg + // (after the binary name) here is indeed the plugin module path. + // Keep in sync with dom/plugins/PluginModuleParent. + std::vector<std::string> values = CommandLine::ForCurrentProcess()->argv(); + MOZ_ASSERT(values.size() >= 2, "not enough args"); + CopyUTF8toUTF16(nsDependentCString(values[1].c_str()), pluginFilename); +#elif defined(OS_WIN) + std::vector<std::wstring> values = + CommandLine::ForCurrentProcess()->GetLooseValues(); + MOZ_ASSERT(values.size() >= 1, "not enough loose args"); + pluginFilename = nsDependentString(values[0].c_str()); +#else +# error Not implemented +#endif + + BackgroundHangMonitor::Startup(); + + return mPlugin->Init(pluginFilename, TakeInitialEndpoint()); +} + +void GMPProcessChild::CleanUp() { BackgroundHangMonitor::Shutdown(); } + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPProcessChild.h b/dom/media/gmp/GMPProcessChild.h new file mode 100644 index 0000000000..88d0141979 --- /dev/null +++ b/dom/media/gmp/GMPProcessChild.h @@ -0,0 +1,33 @@ +/* -*- 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/. */ + +#ifndef GMPProcessChild_h_ +#define GMPProcessChild_h_ + +#include "mozilla/ipc/ProcessChild.h" +#include "GMPChild.h" + +namespace mozilla::gmp { + +class GMPLoader; + +class GMPProcessChild final : public mozilla::ipc::ProcessChild { + protected: + typedef mozilla::ipc::ProcessChild ProcessChild; + + public: + using ProcessChild::ProcessChild; + ~GMPProcessChild(); + + bool Init(int aArgc, char* aArgv[]) override; + void CleanUp() override; + + private: + const RefPtr<GMPChild> mPlugin = new GMPChild; +}; + +} // namespace mozilla::gmp + +#endif // GMPProcessChild_h_ diff --git a/dom/media/gmp/GMPProcessParent.cpp b/dom/media/gmp/GMPProcessParent.cpp new file mode 100644 index 0000000000..f5ffafcce4 --- /dev/null +++ b/dom/media/gmp/GMPProcessParent.cpp @@ -0,0 +1,297 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et : + * 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 "GMPProcessParent.h" +#include "GMPUtils.h" +#include "nsIRunnable.h" +#ifdef XP_WIN +# include "WinUtils.h" +#endif +#include "GMPLog.h" + +#include "base/string_util.h" +#include "base/process_util.h" + +#include <string> + +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) +# include "mozilla/Omnijar.h" +# include "mozilla/Preferences.h" +# include "mozilla/Sandbox.h" +# include "mozilla/SandboxSettings.h" +# include "nsMacUtilsImpl.h" +#endif + +using std::string; +using std::vector; + +using mozilla::gmp::GMPProcessParent; +using mozilla::ipc::GeckoChildProcessHost; + +#ifdef MOZ_WIDGET_ANDROID +static const int kInvalidFd = -1; +#endif + +namespace mozilla::gmp { + +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) +bool GMPProcessParent::sLaunchWithMacSandbox = true; +bool GMPProcessParent::sMacSandboxGMPLogging = false; +# if defined(DEBUG) +bool GMPProcessParent::sIsMainThreadInitDone = false; +# endif +#endif + +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) +/* static */ +void GMPProcessParent::InitStaticMainThread() { + // The GMPProcessParent constructor is called off the + // main thread. Do main thread initialization here. + MOZ_ASSERT(NS_IsMainThread()); + sMacSandboxGMPLogging = + Preferences::GetBool("security.sandbox.logging.enabled") || + PR_GetEnv("MOZ_SANDBOX_GMP_LOGGING") || PR_GetEnv("MOZ_SANDBOX_LOGGING"); + GMP_LOG_DEBUG("GMPProcessParent::InitStaticMainThread: sandbox logging=%s", + sMacSandboxGMPLogging ? "true" : "false"); +# if defined(DEBUG) + sIsMainThreadInitDone = true; +# endif +} +#endif + +GMPProcessParent::GMPProcessParent(const std::string& aGMPPath) + : GeckoChildProcessHost(GeckoProcessType_GMPlugin), + mGMPPath(aGMPPath) +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) + , + mRequiresWindowServer(false) +#endif +{ + MOZ_COUNT_CTOR(GMPProcessParent); +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) + MOZ_ASSERT(sIsMainThreadInitDone == true); + mDisableOSActivityMode = sLaunchWithMacSandbox; +#endif +} + +GMPProcessParent::~GMPProcessParent() { MOZ_COUNT_DTOR(GMPProcessParent); } + +bool GMPProcessParent::Launch(int32_t aTimeoutMs) { + vector<string> args; + +#ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH + GMP_LOG_DEBUG("GMPProcessParent::Launch() mLaunchArch: %d", mLaunchArch); +# if defined(XP_MACOSX) + mLaunchOptions->arch = mLaunchArch; + if (mLaunchArch == base::PROCESS_ARCH_X86_64) { + mLaunchOptions->env_map["MOZ_SHMEM_PAGESIZE_16K"] = 1; + } +# endif +#endif + + // Resolve symlinks in the plugin path. The sandbox prevents + // resolving symlinks in the child process if access to link + // source file is denied. +#ifdef XP_WIN + nsAutoString normalizedPath; +#else + nsAutoCString normalizedPath; +#endif + nsresult rv = NormalizePath(mGMPPath.c_str(), normalizedPath); + if (NS_WARN_IF(NS_FAILED(rv))) { + GMP_LOG_DEBUG( + "GMPProcessParent::Launch: " + "plugin path normaliziation failed for path: %s", + mGMPPath.c_str()); + } + +#ifdef XP_WIN + std::wstring wGMPPath; + if (NS_SUCCEEDED(rv)) { + wGMPPath = normalizedPath.get(); + } else { + wGMPPath = UTF8ToWide(mGMPPath.c_str()); + } + + // The sandbox doesn't allow file system rules where the paths contain + // symbolic links or junction points. Sometimes the Users folder has been + // moved to another drive using a junction point, so allow for this specific + // case. See bug 1236680 for details. + if (NS_WARN_IF( + !widget::WinUtils::ResolveJunctionPointsAndSymLinks(wGMPPath))) { + GMP_LOG_DEBUG("ResolveJunctionPointsAndSymLinks failed for GMP path=%S", + wGMPPath.c_str()); + return false; + } + GMP_LOG_DEBUG("GMPProcessParent::Launch() resolved path to %S", + wGMPPath.c_str()); + +# ifdef MOZ_SANDBOX + // If the GMP path is a network path that is not mapped to a drive letter, + // then we need to fix the path format for the sandbox rule. + wchar_t volPath[MAX_PATH]; + if (::GetVolumePathNameW(wGMPPath.c_str(), volPath, MAX_PATH) && + ::GetDriveTypeW(volPath) == DRIVE_REMOTE && + wGMPPath.compare(0, 2, L"\\\\") == 0) { + std::wstring sandboxGMPPath(wGMPPath); + sandboxGMPPath.insert(1, L"??\\UNC"); + mAllowedFilesRead.push_back(sandboxGMPPath + L"\\*"); + } else { + mAllowedFilesRead.push_back(wGMPPath + L"\\*"); + } +# endif + + args.push_back(WideToUTF8(wGMPPath)); +#else + if (NS_SUCCEEDED(rv)) { + args.push_back(normalizedPath.get()); + } else { + args.push_back(mGMPPath); + } +#endif + +#ifdef MOZ_WIDGET_ANDROID + // Add dummy values for pref and pref map to the file descriptors remapping + // table. See bug 1440207 and 1481139. + AddFdToRemap(kInvalidFd, kInvalidFd); + AddFdToRemap(kInvalidFd, kInvalidFd); +#endif + return SyncLaunch(args, aTimeoutMs); +} + +void GMPProcessParent::Delete(nsCOMPtr<nsIRunnable> aCallback) { + mDeletedCallback = aCallback; + XRE_GetIOMessageLoop()->PostTask(NewNonOwningRunnableMethod( + "gmp::GMPProcessParent::DoDelete", this, &GMPProcessParent::DoDelete)); +} + +void GMPProcessParent::DoDelete() { + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + if (mDeletedCallback) { + mDeletedCallback->Run(); + } + + Destroy(); +} + +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) +bool GMPProcessParent::IsMacSandboxLaunchEnabled() { + return sLaunchWithMacSandbox; +} + +void GMPProcessParent::SetRequiresWindowServer(bool aRequiresWindowServer) { + mRequiresWindowServer = aRequiresWindowServer; +} + +bool GMPProcessParent::FillMacSandboxInfo(MacSandboxInfo& aInfo) { + aInfo.type = MacSandboxType_GMP; + aInfo.hasWindowServer = mRequiresWindowServer; + aInfo.shouldLog = (aInfo.shouldLog || sMacSandboxGMPLogging); + nsAutoCString appPath; + if (!nsMacUtilsImpl::GetAppPath(appPath)) { + GMP_LOG_DEBUG( + "GMPProcessParent::FillMacSandboxInfo: failed to get app path"); + return false; + } + aInfo.appPath.assign(appPath.get()); + + GMP_LOG_DEBUG( + "GMPProcessParent::FillMacSandboxInfo: " + "plugin dir path: %s", + mGMPPath.c_str()); + nsCOMPtr<nsIFile> pluginDir; + nsresult rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(mGMPPath.c_str()), true, + getter_AddRefs(pluginDir)); + if (NS_FAILED(rv)) { + GMP_LOG_DEBUG( + "GMPProcessParent::FillMacSandboxInfo: " + "NS_NewLocalFile failed for plugin dir, rv=%d", + rv); + return false; + } + + rv = pluginDir->Normalize(); + if (NS_FAILED(rv)) { + GMP_LOG_DEBUG( + "GMPProcessParent::FillMacSandboxInfo: " + "failed to normalize plugin dir path, rv=%d", + rv); + return false; + } + + nsAutoCString resolvedPluginPath; + pluginDir->GetNativePath(resolvedPluginPath); + aInfo.pluginPath.assign(resolvedPluginPath.get()); + GMP_LOG_DEBUG( + "GMPProcessParent::FillMacSandboxInfo: " + "resolved plugin dir path: %s", + resolvedPluginPath.get()); + + if (!mozilla::IsPackagedBuild()) { + GMP_LOG_DEBUG( + "GMPProcessParent::FillMacSandboxInfo: IsPackagedBuild()=false"); + + // Repo dir + nsCOMPtr<nsIFile> repoDir; + rv = nsMacUtilsImpl::GetRepoDir(getter_AddRefs(repoDir)); + if (NS_FAILED(rv)) { + GMP_LOG_DEBUG( + "GMPProcessParent::FillMacSandboxInfo: failed to get repo dir"); + return false; + } + nsCString repoDirPath; + Unused << repoDir->GetNativePath(repoDirPath); + aInfo.testingReadPath1 = repoDirPath.get(); + GMP_LOG_DEBUG( + "GMPProcessParent::FillMacSandboxInfo: " + "repo dir path: %s", + repoDirPath.get()); + + // Object dir + nsCOMPtr<nsIFile> objDir; + rv = nsMacUtilsImpl::GetObjDir(getter_AddRefs(objDir)); + if (NS_FAILED(rv)) { + GMP_LOG_DEBUG( + "GMPProcessParent::FillMacSandboxInfo: failed to get object dir"); + return false; + } + nsCString objDirPath; + Unused << objDir->GetNativePath(objDirPath); + aInfo.testingReadPath2 = objDirPath.get(); + GMP_LOG_DEBUG( + "GMPProcessParent::FillMacSandboxInfo: " + "object dir path: %s", + objDirPath.get()); + } + return true; +} +#endif + +nsresult GMPProcessParent::NormalizePath(const char* aPath, + PathString& aNormalizedPath) { + nsCOMPtr<nsIFile> fileOrDir; + nsresult rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(aPath), true, + getter_AddRefs(fileOrDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = fileOrDir->Normalize(); + NS_ENSURE_SUCCESS(rv, rv); + +#ifdef XP_WIN + return fileOrDir->GetTarget(aNormalizedPath); +#else + bool isLink = false; + rv = fileOrDir->IsSymlink(&isLink); + NS_ENSURE_SUCCESS(rv, rv); + if (isLink) { + return fileOrDir->GetNativeTarget(aNormalizedPath); + } + return fileOrDir->GetNativePath(aNormalizedPath); +#endif +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPProcessParent.h b/dom/media/gmp/GMPProcessParent.h new file mode 100644 index 0000000000..efe6bff17f --- /dev/null +++ b/dom/media/gmp/GMPProcessParent.h @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=4 et : + * 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/. */ + +#ifndef GMPProcessParent_h +#define GMPProcessParent_h 1 + +#include "mozilla/Attributes.h" +#include "base/basictypes.h" +#include "base/file_path.h" +#include "base/thread.h" +#include "chrome/common/child_process_host.h" +#include "mozilla/ipc/GeckoChildProcessHost.h" +#include "nsIFile.h" + +class nsIRunnable; + +namespace mozilla::gmp { + +class GMPProcessParent final : public mozilla::ipc::GeckoChildProcessHost { + public: + explicit GMPProcessParent(const std::string& aGMPPath); + + // Synchronously launch the plugin process. If the process fails to launch + // after timeoutMs, this method will return false. + bool Launch(int32_t aTimeoutMs); + + void Delete(nsCOMPtr<nsIRunnable> aCallback = nullptr); + + bool CanShutdown() override { return true; } + const std::string& GetPluginFilePath() { return mGMPPath; } + +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) + // Init static members on the main thread + static void InitStaticMainThread(); + + // Read prefs and environment variables to determine + // when and if to start the Mac sandbox for the child + // process. Starting the sandbox at launch is the new + // preferred method. Code to support starting the sandbox + // later at plugin start time should be removed once + // starting at launch is stable and shipping. + bool IsMacSandboxLaunchEnabled() override; + + // For process sandboxing purposes, set whether or not this + // instance of the GMP process requires access to the macOS + // window server. At present, Widevine requires window server + // access, but OpenH264 decoding does not. + void SetRequiresWindowServer(bool aRequiresWindowServer); + + // Return the sandbox type to be used with this process type. + static MacSandboxType GetMacSandboxType() { return MacSandboxType_GMP; }; +#endif + + using mozilla::ipc::GeckoChildProcessHost::GetChannel; + using mozilla::ipc::GeckoChildProcessHost::GetChildProcessHandle; + + private: + ~GMPProcessParent(); + + void DoDelete(); + + std::string mGMPPath; + nsCOMPtr<nsIRunnable> mDeletedCallback; + +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) + // Indicates whether we'll start the Mac GMP sandbox during + // process launch (earlyinit) which is the new preferred method + // or later in the process lifetime. + static bool sLaunchWithMacSandbox; + + // Whether or not Mac sandbox violation logging is enabled. + static bool sMacSandboxGMPLogging; + + // Override so we can set GMP-specific sandbox parameters + bool FillMacSandboxInfo(MacSandboxInfo& aInfo) override; + + // Controls whether or not the sandbox will be configured with + // window service access. + bool mRequiresWindowServer; + +# if defined(DEBUG) + // Used to assert InitStaticMainThread() is called before the constructor. + static bool sIsMainThreadInitDone; +# endif +#endif + + // For normalizing paths to be compatible with sandboxing. + // We use normalized paths to generate the sandbox ruleset. Once + // the sandbox has been started, resolving symlinks that point to + // allowed directories could require reading paths not allowed by + // the sandbox, so we should only attempt to load plugin libraries + // using normalized paths. + static nsresult NormalizePath(const char* aPath, PathString& aNormalizedPath); + + DISALLOW_COPY_AND_ASSIGN(GMPProcessParent); +}; + +} // namespace mozilla::gmp + +#endif // ifndef GMPProcessParent_h diff --git a/dom/media/gmp/GMPSanitizedExports.h b/dom/media/gmp/GMPSanitizedExports.h new file mode 100644 index 0000000000..76c00dba9b --- /dev/null +++ b/dom/media/gmp/GMPSanitizedExports.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +// This file exposes symbols from the CDM headers + undefines macros which +// X11 defines and which clash with the CDM headers. +// +// Ideally we can prune where X11 headers are included so that we don't need +// this, but this band aid is useful until then. + +#ifndef DOM_MEDIA_GMP_GMP_API_GMP_SANITIZED_CDM_EXPORTS_H_ +#define DOM_MEDIA_GMP_GMP_API_GMP_SANITIZED_CDM_EXPORTS_H_ + +// If Status is defined undef it so we don't break the CDM headers. +#ifdef Status +# undef Status +#endif // Status +#include "content_decryption_module.h" + +#endif // DOM_MEDIA_GMP_GMP_API_GMP_SANITIZED_CDM_EXPORTS_H_ diff --git a/dom/media/gmp/GMPService.cpp b/dom/media/gmp/GMPService.cpp new file mode 100644 index 0000000000..1d2444a4aa --- /dev/null +++ b/dom/media/gmp/GMPService.cpp @@ -0,0 +1,570 @@ +/* -*- 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 "GMPService.h" + +#include "ChromiumCDMParent.h" +#include "GMPLog.h" +#include "GMPParent.h" +#include "GMPProcessParent.h" +#include "GMPServiceChild.h" +#include "GMPServiceParent.h" +#include "GMPVideoDecoderParent.h" +#include "mozilla/ipc/GeckoChildProcessHost.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/PluginCrashedEvent.h" +#include "nsThreadUtils.h" +#if defined(XP_LINUX) && defined(MOZ_SANDBOX) +# include "mozilla/SandboxInfo.h" +#endif +#include "VideoUtils.h" +#include "mozilla/Services.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/Unused.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsComponentManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsHashKeys.h" +#include "nsIObserverService.h" +#include "nsIXULAppInfo.h" +#include "nsNativeCharsetUtils.h" +#include "nsXPCOMPrivate.h" +#include "prio.h" +#include "runnable_utils.h" + +namespace mozilla { + +LogModule* GetGMPLog() { + static LazyLogModule sLog("GMP"); + return sLog; +} + +LogModule* GetGMPLibraryLog() { + static LazyLogModule sLog("GMPLibrary"); + return sLog; +} + +GMPLogLevel GetGMPLibraryLogLevel() { + switch (GetGMPLibraryLog()->Level()) { + case LogLevel::Disabled: + return kGMPLogQuiet; + case LogLevel::Error: + return kGMPLogError; + case LogLevel::Warning: + return kGMPLogWarning; + case LogLevel::Info: + return kGMPLogInfo; + case LogLevel::Debug: + return kGMPLogDebug; + case LogLevel::Verbose: + return kGMPLogDetail; + } + return kGMPLogInvalid; +} + +#ifdef __CLASS__ +# undef __CLASS__ +#endif +#define __CLASS__ "GMPService" + +namespace gmp { + +static StaticRefPtr<GeckoMediaPluginService> sSingletonService; + +class GMPServiceCreateHelper final : public mozilla::Runnable { + RefPtr<GeckoMediaPluginService> mService; + + public: + static already_AddRefed<GeckoMediaPluginService> GetOrCreate() { + RefPtr<GeckoMediaPluginService> service; + + if (NS_IsMainThread()) { + service = GetOrCreateOnMainThread(); + } else { + RefPtr<GMPServiceCreateHelper> createHelper = + new GMPServiceCreateHelper(); + + mozilla::SyncRunnable::DispatchToThread(GetMainThreadSerialEventTarget(), + createHelper, true); + + service = std::move(createHelper->mService); + } + + return service.forget(); + } + + private: + GMPServiceCreateHelper() : Runnable("GMPServiceCreateHelper") {} + + ~GMPServiceCreateHelper() { MOZ_ASSERT(!mService); } + + static already_AddRefed<GeckoMediaPluginService> GetOrCreateOnMainThread() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!sSingletonService) { + if (XRE_IsParentProcess()) { + RefPtr<GeckoMediaPluginServiceParent> service = + new GeckoMediaPluginServiceParent(); + if (NS_WARN_IF(NS_FAILED(service->Init()))) { + return nullptr; + } + sSingletonService = service; +#if defined(XP_MACOSX) && defined(MOZ_SANDBOX) + // GMPProcessParent should only be instantiated in the parent + // so initialization only needs to be done in the parent. + GMPProcessParent::InitStaticMainThread(); +#endif + } else { + RefPtr<GeckoMediaPluginServiceChild> service = + new GeckoMediaPluginServiceChild(); + if (NS_WARN_IF(NS_FAILED(service->Init()))) { + return nullptr; + } + sSingletonService = service; + } + ClearOnShutdown(&sSingletonService); + } + + RefPtr<GeckoMediaPluginService> service = sSingletonService.get(); + return service.forget(); + } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + mService = GetOrCreateOnMainThread(); + return NS_OK; + } +}; + +already_AddRefed<GeckoMediaPluginService> +GeckoMediaPluginService::GetGeckoMediaPluginService() { + return GMPServiceCreateHelper::GetOrCreate(); +} + +NS_IMPL_ISUPPORTS(GeckoMediaPluginService, mozIGeckoMediaPluginService, + nsIObserver) + +GeckoMediaPluginService::GeckoMediaPluginService() + : mMutex("GeckoMediaPluginService::mMutex"), + mMainThread(GetMainThreadSerialEventTarget()), + mGMPThreadShutdown(false), + mShuttingDownOnGMPThread(false), + mXPCOMWillShutdown(false) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIXULAppInfo> appInfo = + do_GetService("@mozilla.org/xre/app-info;1"); + if (appInfo) { + nsAutoCString version; + nsAutoCString buildID; + if (NS_SUCCEEDED(appInfo->GetVersion(version)) && + NS_SUCCEEDED(appInfo->GetAppBuildID(buildID))) { + GMP_LOG_DEBUG( + "GeckoMediaPluginService created; Gecko version=%s buildID=%s", + version.get(), buildID.get()); + } + } +} + +GeckoMediaPluginService::~GeckoMediaPluginService() = default; + +NS_IMETHODIMP +GeckoMediaPluginService::RunPluginCrashCallbacks( + uint32_t aPluginId, const nsACString& aPluginName) { + MOZ_ASSERT(NS_IsMainThread()); + GMP_LOG_DEBUG("%s::%s(%i)", __CLASS__, __FUNCTION__, aPluginId); + + mozilla::UniquePtr<nsTArray<RefPtr<GMPCrashHelper>>> helpers; + { + MutexAutoLock lock(mMutex); + mPluginCrashHelpers.Remove(aPluginId, &helpers); + } + if (!helpers) { + GMP_LOG_DEBUG("%s::%s(%i) No crash helpers, not handling crash.", __CLASS__, + __FUNCTION__, aPluginId); + return NS_OK; + } + + for (const auto& helper : *helpers) { + nsCOMPtr<nsPIDOMWindowInner> window = helper->GetPluginCrashedEventTarget(); + if (NS_WARN_IF(!window)) { + continue; + } + RefPtr<dom::Document> document(window->GetExtantDoc()); + if (NS_WARN_IF(!document)) { + continue; + } + + dom::PluginCrashedEventInit init; + init.mPluginID = aPluginId; + init.mBubbles = true; + init.mCancelable = true; + init.mGmpPlugin = true; + CopyUTF8toUTF16(aPluginName, init.mPluginName); + init.mSubmittedCrashReport = false; + RefPtr<dom::PluginCrashedEvent> event = + dom::PluginCrashedEvent::Constructor(document, u"PluginCrashed"_ns, + init); + event->SetTrusted(true); + event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true; + + EventDispatcher::DispatchDOMEvent(window, nullptr, event, nullptr, nullptr); + } + + return NS_OK; +} + +nsresult GeckoMediaPluginService::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obsService = + mozilla::services::GetObserverService(); + MOZ_ASSERT(obsService); + MOZ_ALWAYS_SUCCEEDS(obsService->AddObserver( + this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false)); + + // Kick off scanning for plugins + nsCOMPtr<nsIThread> thread; + return GetThread(getter_AddRefs(thread)); +} + +RefPtr<GetCDMParentPromise> GeckoMediaPluginService::GetCDM( + const NodeIdParts& aNodeIdParts, const nsACString& aKeySystem, + GMPCrashHelper* aHelper) { + AssertOnGMPThread(); + + if (mShuttingDownOnGMPThread || aKeySystem.IsEmpty()) { + nsPrintfCString reason( + "%s::%s failed, aKeySystem.IsEmpty() = %d, mShuttingDownOnGMPThread = " + "%d.", + __CLASS__, __FUNCTION__, aKeySystem.IsEmpty(), + mShuttingDownOnGMPThread); + return GetCDMParentPromise::CreateAndReject( + MediaResult(NS_ERROR_FAILURE, reason.get()), __func__); + } + + typedef MozPromiseHolder<GetCDMParentPromise> PromiseHolder; + PromiseHolder* rawHolder(new PromiseHolder()); + RefPtr<GetCDMParentPromise> promise = rawHolder->Ensure(__func__); + nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread()); + RefPtr<GMPCrashHelper> helper(aHelper); + nsTArray<nsCString> tags{nsCString{aKeySystem}}; + GetContentParent(aHelper, NodeIdVariant{aNodeIdParts}, + nsLiteralCString(CHROMIUM_CDM_API), tags) + ->Then( + thread, __func__, + [rawHolder, helper, keySystem = nsCString{aKeySystem}]( + RefPtr<GMPContentParent::CloseBlocker> wrapper) { + RefPtr<GMPContentParent> parent = wrapper->mParent; + MOZ_ASSERT( + parent, + "Wrapper should wrap a valid parent if we're in this path."); + UniquePtr<PromiseHolder> holder(rawHolder); + RefPtr<ChromiumCDMParent> cdm = parent->GetChromiumCDM(keySystem); + if (!cdm) { + nsPrintfCString reason( + "%s::%s failed since GetChromiumCDM returns nullptr.", + __CLASS__, __FUNCTION__); + holder->Reject(MediaResult(NS_ERROR_FAILURE, reason.get()), + __func__); + return; + } + if (helper) { + cdm->SetCrashHelper(helper); + } + holder->Resolve(cdm, __func__); + }, + [rawHolder](MediaResult result) { + nsPrintfCString reason( + "%s::%s failed since GetContentParent rejects the promise with " + "reason %s.", + __CLASS__, __FUNCTION__, result.Description().get()); + UniquePtr<PromiseHolder> holder(rawHolder); + holder->Reject(MediaResult(NS_ERROR_FAILURE, reason.get()), + __func__); + }); + + return promise; +} + +#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS) +RefPtr<GetGMPContentParentPromise> +GeckoMediaPluginService::GetContentParentForTest() { + AssertOnGMPThread(); + + nsTArray<nsCString> tags; + tags.AppendElement("fake"_ns); + + const nsString origin1 = u"http://example1.com"_ns; + const nsString origin2 = u"http://example2.org"_ns; + const nsString gmpName = u"gmp-fake"_ns; + + NodeIdParts nodeIdParts = NodeIdParts{origin1, origin2, gmpName}; + + if (mShuttingDownOnGMPThread) { + nsPrintfCString reason("%s::%s failed, mShuttingDownOnGMPThread = %d.", + __CLASS__, __FUNCTION__, mShuttingDownOnGMPThread); + return GetGMPContentParentPromise::CreateAndReject( + MediaResult(NS_ERROR_FAILURE, reason.get()), __func__); + } + + using PromiseHolder = MozPromiseHolder<GetGMPContentParentPromise>; + PromiseHolder* rawHolder(new PromiseHolder()); + RefPtr<GetGMPContentParentPromise> promise = rawHolder->Ensure(__func__); + nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread()); + GetContentParent(nullptr, NodeIdVariant{nodeIdParts}, + nsLiteralCString(CHROMIUM_CDM_API), tags) + ->Then( + thread, __func__, + [rawHolder](const RefPtr<GMPContentParent::CloseBlocker>& wrapper) { + RefPtr<GMPContentParent> parent = wrapper->mParent; + MOZ_ASSERT( + parent, + "Wrapper should wrap a valid parent if we're in this path."); + UniquePtr<PromiseHolder> holder(rawHolder); + if (!parent) { + nsPrintfCString reason("%s::%s failed since no GMPContentParent.", + __CLASS__, __FUNCTION__); + holder->Reject(MediaResult(NS_ERROR_FAILURE, reason.get()), + __func__); + return; + } + holder->Resolve(wrapper, __func__); + }, + [rawHolder](const MediaResult& result) { + nsPrintfCString reason( + "%s::%s failed since GetContentParent rejects the promise with " + "reason %s.", + __CLASS__, __FUNCTION__, result.Description().get()); + UniquePtr<PromiseHolder> holder(rawHolder); + holder->Reject(MediaResult(NS_ERROR_FAILURE, reason.get()), + __func__); + }); + + return promise; +} +#endif + +void GeckoMediaPluginService::ShutdownGMPThread() { + GMP_LOG_DEBUG("%s::%s", __CLASS__, __FUNCTION__); + nsCOMPtr<nsIThread> gmpThread; + { + MutexAutoLock lock(mMutex); + mGMPThreadShutdown = true; + mGMPThread.swap(gmpThread); + } + + if (gmpThread) { + gmpThread->Shutdown(); + } +} + +/* static */ +nsCOMPtr<nsIAsyncShutdownClient> GeckoMediaPluginService::GetShutdownBarrier() { + nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService(); + if (NS_WARN_IF(!svc)) { + MOZ_ASSERT_UNREACHABLE("No async shutdown service!"); + return nullptr; + } + + nsCOMPtr<nsIAsyncShutdownClient> barrier; + nsresult rv = svc->GetXpcomWillShutdown(getter_AddRefs(barrier)); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_ASSERT_UNREACHABLE("Could not create shutdown barrier!"); + return nullptr; + } + + MOZ_RELEASE_ASSERT(barrier); + return barrier; +} + +nsresult GeckoMediaPluginService::GMPDispatch(nsIRunnable* event, + uint32_t flags) { + nsCOMPtr<nsIRunnable> r(event); + return GMPDispatch(r.forget(), flags); +} + +nsresult GeckoMediaPluginService::GMPDispatch( + already_AddRefed<nsIRunnable> event, uint32_t flags) { + nsCOMPtr<nsIRunnable> r(event); + nsCOMPtr<nsIThread> thread; + nsresult rv = GetThread(getter_AddRefs(thread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return thread->Dispatch(r, flags); +} + +// always call with getter_AddRefs, because it does +NS_IMETHODIMP +GeckoMediaPluginService::GetThread(nsIThread** aThread) { + MOZ_ASSERT(aThread); + + // This can be called from any thread. + MutexAutoLock lock(mMutex); + + return GetThreadLocked(aThread); +} + +// always call with getter_AddRefs, because it does +nsresult GeckoMediaPluginService::GetThreadLocked(nsIThread** aThread) { + MOZ_ASSERT(aThread); + + mMutex.AssertCurrentThreadOwns(); + + if (!mGMPThread) { + // Don't allow the thread to be created after shutdown has started. + if (mGMPThreadShutdown) { + return NS_ERROR_FAILURE; + } + + nsresult rv = NS_NewNamedThread("GMPThread", getter_AddRefs(mGMPThread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Tell the thread to initialize plugins + InitializePlugins(mGMPThread); + } + + nsCOMPtr<nsIThread> copy = mGMPThread; + copy.forget(aThread); + + return NS_OK; +} + +already_AddRefed<nsISerialEventTarget> GeckoMediaPluginService::GetGMPThread() { + nsCOMPtr<nsISerialEventTarget> thread; + { + MutexAutoLock lock(mMutex); + thread = mGMPThread; + } + return thread.forget(); +} + +NS_IMETHODIMP +GeckoMediaPluginService::GetGMPVideoDecoder( + GMPCrashHelper* aHelper, nsTArray<nsCString>* aTags, + const nsACString& aNodeId, + UniquePtr<GetGMPVideoDecoderCallback>&& aCallback) { + AssertOnGMPThread(); + NS_ENSURE_ARG(aTags && aTags->Length() > 0); + NS_ENSURE_ARG(aCallback); + + if (mShuttingDownOnGMPThread) { + return NS_ERROR_FAILURE; + } + + GetGMPVideoDecoderCallback* rawCallback = aCallback.release(); + nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread()); + RefPtr<GMPCrashHelper> helper(aHelper); + GetContentParent(aHelper, NodeIdVariant{nsCString(aNodeId)}, + nsLiteralCString(GMP_API_VIDEO_DECODER), *aTags) + ->Then( + thread, __func__, + [rawCallback, + helper](RefPtr<GMPContentParent::CloseBlocker> wrapper) { + RefPtr<GMPContentParent> parent = wrapper->mParent; + UniquePtr<GetGMPVideoDecoderCallback> callback(rawCallback); + GMPVideoDecoderParent* actor = nullptr; + GMPVideoHostImpl* host = nullptr; + if (parent && NS_SUCCEEDED(parent->GetGMPVideoDecoder(&actor))) { + host = &(actor->Host()); + actor->SetCrashHelper(helper); + } + callback->Done(actor, host); + }, + [rawCallback] { + UniquePtr<GetGMPVideoDecoderCallback> callback(rawCallback); + callback->Done(nullptr, nullptr); + }); + + return NS_OK; +} + +NS_IMETHODIMP +GeckoMediaPluginService::GetGMPVideoEncoder( + GMPCrashHelper* aHelper, nsTArray<nsCString>* aTags, + const nsACString& aNodeId, + UniquePtr<GetGMPVideoEncoderCallback>&& aCallback) { + AssertOnGMPThread(); + NS_ENSURE_ARG(aTags && aTags->Length() > 0); + NS_ENSURE_ARG(aCallback); + + if (mShuttingDownOnGMPThread) { + return NS_ERROR_FAILURE; + } + + GetGMPVideoEncoderCallback* rawCallback = aCallback.release(); + nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread()); + RefPtr<GMPCrashHelper> helper(aHelper); + GetContentParent(aHelper, NodeIdVariant{nsCString(aNodeId)}, + nsLiteralCString(GMP_API_VIDEO_ENCODER), *aTags) + ->Then( + thread, __func__, + [rawCallback, + helper](RefPtr<GMPContentParent::CloseBlocker> wrapper) { + RefPtr<GMPContentParent> parent = wrapper->mParent; + UniquePtr<GetGMPVideoEncoderCallback> callback(rawCallback); + GMPVideoEncoderParent* actor = nullptr; + GMPVideoHostImpl* host = nullptr; + if (parent && NS_SUCCEEDED(parent->GetGMPVideoEncoder(&actor))) { + host = &(actor->Host()); + actor->SetCrashHelper(helper); + } + callback->Done(actor, host); + }, + [rawCallback] { + UniquePtr<GetGMPVideoEncoderCallback> callback(rawCallback); + callback->Done(nullptr, nullptr); + }); + + return NS_OK; +} + +void GeckoMediaPluginService::ConnectCrashHelper(uint32_t aPluginId, + GMPCrashHelper* aHelper) { + if (!aHelper) { + return; + } + + MutexAutoLock lock(mMutex); + mPluginCrashHelpers.WithEntryHandle(aPluginId, [&](auto&& entry) { + if (!entry) { + entry.Insert(MakeUnique<nsTArray<RefPtr<GMPCrashHelper>>>()); + } else if (entry.Data()->Contains(aHelper)) { + return; + } + entry.Data()->AppendElement(aHelper); + }); +} + +void GeckoMediaPluginService::DisconnectCrashHelper(GMPCrashHelper* aHelper) { + if (!aHelper) { + return; + } + MutexAutoLock lock(mMutex); + for (auto iter = mPluginCrashHelpers.Iter(); !iter.Done(); iter.Next()) { + nsTArray<RefPtr<GMPCrashHelper>>* helpers = iter.UserData(); + if (!helpers->Contains(aHelper)) { + continue; + } + helpers->RemoveElement(aHelper); + MOZ_ASSERT(!helpers->Contains(aHelper)); // Ensure there aren't duplicates. + if (helpers->IsEmpty()) { + iter.Remove(); + } + } +} + +} // namespace gmp +} // namespace mozilla + +#undef __CLASS__ diff --git a/dom/media/gmp/GMPService.h b/dom/media/gmp/GMPService.h new file mode 100644 index 0000000000..8f4271812e --- /dev/null +++ b/dom/media/gmp/GMPService.h @@ -0,0 +1,130 @@ +/* -*- 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/. */ + +#ifndef GMPService_h_ +#define GMPService_h_ + +#include "GMPContentParent.h" +#include "GMPCrashHelper.h" +#include "gmp-video-codec.h" +#include "mozIGeckoMediaPluginService.h" +#include "mozilla/Atomics.h" +#include "mozilla/gmp/GMPTypes.h" +#include "mozilla/MozPromise.h" +#include "nsCOMPtr.h" +#include "nsClassHashtable.h" +#include "nsIObserver.h" +#include "nsString.h" +#include "nsTArray.h" + +class nsIAsyncShutdownClient; +class nsIRunnable; +class nsISerialEventTarget; +class nsIThread; + +template <class> +struct already_AddRefed; + +namespace mozilla { + +class GMPCrashHelper; +class MediaResult; + +extern LogModule* GetGMPLog(); +extern LogModule* GetGMPLibraryLog(); +extern GMPLogLevel GetGMPLibraryLogLevel(); + +namespace gmp { + +typedef MozPromise<RefPtr<GMPContentParent::CloseBlocker>, MediaResult, + /* IsExclusive = */ true> + GetGMPContentParentPromise; +typedef MozPromise<RefPtr<ChromiumCDMParent>, MediaResult, + /* IsExclusive = */ true> + GetCDMParentPromise; + +class GeckoMediaPluginService : public mozIGeckoMediaPluginService, + public nsIObserver { + public: + static already_AddRefed<GeckoMediaPluginService> GetGeckoMediaPluginService(); + + virtual nsresult Init(); + + NS_DECL_THREADSAFE_ISUPPORTS + + RefPtr<GetCDMParentPromise> GetCDM(const NodeIdParts& aNodeIdParts, + const nsACString& aKeySystem, + GMPCrashHelper* aHelper); + +#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS) + RefPtr<GetGMPContentParentPromise> GetContentParentForTest(); +#endif + + // mozIGeckoMediaPluginService + NS_IMETHOD GetThread(nsIThread** aThread) override; + nsresult GetThreadLocked(nsIThread** aThread); + NS_IMETHOD GetGMPVideoDecoder( + GMPCrashHelper* aHelper, nsTArray<nsCString>* aTags, + const nsACString& aNodeId, + UniquePtr<GetGMPVideoDecoderCallback>&& aCallback) override; + NS_IMETHOD GetGMPVideoEncoder( + GMPCrashHelper* aHelper, nsTArray<nsCString>* aTags, + const nsACString& aNodeId, + UniquePtr<GetGMPVideoEncoderCallback>&& aCallback) override; + + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD RunPluginCrashCallbacks( + uint32_t aPluginId, const nsACString& aPluginName) override; + + already_AddRefed<nsISerialEventTarget> GetGMPThread(); + + void ConnectCrashHelper(uint32_t aPluginId, GMPCrashHelper* aHelper); + void DisconnectCrashHelper(GMPCrashHelper* aHelper); + + bool XPCOMWillShutdownReceived() const { return mXPCOMWillShutdown; } + + protected: + GeckoMediaPluginService(); + virtual ~GeckoMediaPluginService(); + + void AssertOnGMPThread() { +#ifdef DEBUG + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mGMPThread->IsOnCurrentThread()); +#endif + } + + virtual void InitializePlugins(nsISerialEventTarget* aGMPThread) = 0; + + virtual RefPtr<GetGMPContentParentPromise> GetContentParent( + GMPCrashHelper* aHelper, const NodeIdVariant& aNodeIdVariant, + const nsACString& aAPI, const nsTArray<nsCString>& aTags) = 0; + + nsresult GMPDispatch(nsIRunnable* event, uint32_t flags = NS_DISPATCH_NORMAL); + nsresult GMPDispatch(already_AddRefed<nsIRunnable> event, + uint32_t flags = NS_DISPATCH_NORMAL); + void ShutdownGMPThread(); + + static nsCOMPtr<nsIAsyncShutdownClient> GetShutdownBarrier(); + + Mutex mMutex MOZ_UNANNOTATED; // Protects mGMPThread, mPluginCrashHelpers, + // mGMPThreadShutdown and some members in + // derived classes. + + const nsCOMPtr<nsISerialEventTarget> mMainThread; + + nsCOMPtr<nsIThread> mGMPThread; + bool mGMPThreadShutdown; + bool mShuttingDownOnGMPThread; + Atomic<bool> mXPCOMWillShutdown; + + nsClassHashtable<nsUint32HashKey, nsTArray<RefPtr<GMPCrashHelper>>> + mPluginCrashHelpers; +}; + +} // namespace gmp +} // namespace mozilla + +#endif // GMPService_h_ diff --git a/dom/media/gmp/GMPServiceChild.cpp b/dom/media/gmp/GMPServiceChild.cpp new file mode 100644 index 0000000000..ebcb9dc84c --- /dev/null +++ b/dom/media/gmp/GMPServiceChild.cpp @@ -0,0 +1,586 @@ +/* -*- 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 "GMPServiceChild.h" + +#include "GMPContentParent.h" +#include "GMPLog.h" +#include "GMPParent.h" +#include "base/task.h" +#include "mozIGeckoMediaPluginChromeService.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/ipc/Endpoint.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsIObserverService.h" +#include "nsReadableUtils.h" +#include "nsXPCOMPrivate.h" +#include "runnable_utils.h" + +namespace mozilla::gmp { + +#ifdef __CLASS__ +# undef __CLASS__ +#endif +#define __CLASS__ "GMPServiceChild" + +already_AddRefed<GeckoMediaPluginServiceChild> +GeckoMediaPluginServiceChild::GetSingleton() { + MOZ_ASSERT(!XRE_IsParentProcess()); + RefPtr<GeckoMediaPluginService> service( + GeckoMediaPluginService::GetGeckoMediaPluginService()); +#ifdef DEBUG + if (service) { + nsCOMPtr<mozIGeckoMediaPluginChromeService> chromeService; + CallQueryInterface(service.get(), getter_AddRefs(chromeService)); + MOZ_ASSERT(!chromeService); + } +#endif + return service.forget().downcast<GeckoMediaPluginServiceChild>(); +} + +nsresult GeckoMediaPluginServiceChild::Init() { + MOZ_ASSERT(NS_IsMainThread()); + GMP_LOG_DEBUG("%s::%s", __CLASS__, __FUNCTION__); + + nsresult rv = AddShutdownBlocker(); + if (NS_FAILED(rv)) { + MOZ_ASSERT_UNREACHABLE( + "We expect xpcom to be live when calling this, so we should be able to " + "add a blocker"); + GMP_LOG_DEBUG("%s::%s failed to add shutdown blocker!", __CLASS__, + __FUNCTION__); + return rv; + } + + return GeckoMediaPluginService::Init(); +} + +NS_IMPL_ISUPPORTS_INHERITED(GeckoMediaPluginServiceChild, + GeckoMediaPluginService, nsIAsyncShutdownBlocker) + +// Used to identify blockers that we put in place. +static const nsLiteralString kShutdownBlockerName = + u"GeckoMediaPluginServiceChild: shutdown"_ns; + +// nsIAsyncShutdownBlocker members +NS_IMETHODIMP +GeckoMediaPluginServiceChild::GetName(nsAString& aName) { + aName = kShutdownBlockerName; + return NS_OK; +} + +NS_IMETHODIMP +GeckoMediaPluginServiceChild::GetState(nsIPropertyBag**) { return NS_OK; } + +NS_IMETHODIMP +GeckoMediaPluginServiceChild::BlockShutdown(nsIAsyncShutdownClient*) { + MOZ_ASSERT(NS_IsMainThread()); + GMP_LOG_DEBUG("%s::%s", __CLASS__, __FUNCTION__); + + mXPCOMWillShutdown = true; + + MutexAutoLock lock(mMutex); + Unused << NS_WARN_IF(NS_FAILED(mGMPThread->Dispatch( + NewRunnableMethod("GeckoMediaPluginServiceChild::BeginShutdown", this, + &GeckoMediaPluginServiceChild::BeginShutdown)))); + return NS_OK; +} +// End nsIAsyncShutdownBlocker members + +GeckoMediaPluginServiceChild::~GeckoMediaPluginServiceChild() { + MOZ_ASSERT(!mServiceChild); +} + +RefPtr<GetGMPContentParentPromise> +GeckoMediaPluginServiceChild::GetContentParent( + GMPCrashHelper* aHelper, const NodeIdVariant& aNodeIdVariant, + const nsACString& aAPI, const nsTArray<nsCString>& aTags) { + AssertOnGMPThread(); + MOZ_ASSERT(!mShuttingDownOnGMPThread, + "Should not be called if GMPThread is shutting down!"); + + MozPromiseHolder<GetGMPContentParentPromise>* rawHolder = + new MozPromiseHolder<GetGMPContentParentPromise>(); + RefPtr<GetGMPContentParentPromise> promise = rawHolder->Ensure(__func__); + nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread()); + + nsCString api(aAPI); + RefPtr<GMPCrashHelper> helper(aHelper); + RefPtr<GeckoMediaPluginServiceChild> self(this); + + mPendingGetContentParents += 1; + + GetServiceChild()->Then( + thread, __func__, + [nodeIdVariant = aNodeIdVariant, self, api, tags = aTags.Clone(), helper, + rawHolder](GMPServiceChild* child) { + UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>> holder( + rawHolder); + nsresult rv; + + nsTArray<base::ProcessId> alreadyBridgedTo; + child->GetAlreadyBridgedTo(alreadyBridgedTo); + + base::ProcessId otherProcess; + nsCString displayName; + uint32_t pluginId = 0; + GMPPluginType pluginType = GMPPluginType::Unknown; + ipc::Endpoint<PGMPContentParent> endpoint; + nsCString errorDescription; + + bool ok = child->SendLaunchGMP( + nodeIdVariant, api, tags, alreadyBridgedTo, &pluginId, &pluginType, + &otherProcess, &displayName, &endpoint, &rv, &errorDescription); + + if (helper && pluginId) { + // Note: Even if the launch failed, we need to connect the crash + // helper so that if the launch failed due to the plugin crashing, we + // can report the crash via the crash reporter. The crash handling + // notification will arrive shortly if the launch failed due to the + // plugin crashing. + self->ConnectCrashHelper(pluginId, helper); + } + + if (!ok || NS_FAILED(rv)) { + MediaResult error( + rv, nsPrintfCString( + "GeckoMediaPluginServiceChild::GetContentParent " + "SendLaunchGMPForNodeId failed with description (%s)", + errorDescription.get())); + + GMP_LOG_DEBUG("%s failed to launch GMP with error: %s", __CLASS__, + error.Description().get()); + self->mPendingGetContentParents -= 1; + self->RemoveShutdownBlockerIfNeeded(); + + holder->Reject(error, __func__); + return; + } + + RefPtr<GMPContentParent> parent = child->GetBridgedGMPContentParent( + otherProcess, std::move(endpoint)); + if (!alreadyBridgedTo.Contains(otherProcess)) { + parent->SetDisplayName(displayName); + parent->SetPluginId(pluginId); + parent->SetPluginType(pluginType); + } + + // The content parent is no longer pending. + self->mPendingGetContentParents -= 1; + MOZ_ASSERT(child->HaveContentParents(), + "We should have at least one content parent!"); + // We don't check if we need to remove the shutdown blocker here as + // we should always have at least one live content parent. + + RefPtr<GMPContentParent::CloseBlocker> blocker( + new GMPContentParent::CloseBlocker(parent)); + holder->Resolve(blocker, __func__); + }, + [self, rawHolder](MediaResult result) { + self->mPendingGetContentParents -= 1; + self->RemoveShutdownBlockerIfNeeded(); + UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>> holder( + rawHolder); + holder->Reject(result, __func__); + }); + + return promise; +} + +typedef mozilla::dom::GMPCapabilityData GMPCapabilityData; +typedef mozilla::dom::GMPAPITags GMPAPITags; + +struct GMPCapabilityAndVersion { + explicit GMPCapabilityAndVersion(const GMPCapabilityData& aCapabilities) + : mName(aCapabilities.name()), mVersion(aCapabilities.version()) { + for (const GMPAPITags& tags : aCapabilities.capabilities()) { + GMPCapability cap; + cap.mAPIName = tags.api(); + for (const nsACString& tag : tags.tags()) { + cap.mAPITags.AppendElement(tag); + } + mCapabilities.AppendElement(std::move(cap)); + } + } + + nsCString ToString() const { + nsCString s; + s.Append(mName); + s.AppendLiteral(" version="); + s.Append(mVersion); + s.AppendLiteral(" tags=["); + StringJoinAppend(s, " "_ns, mCapabilities, + [](auto& tags, const GMPCapability& cap) { + tags.Append(cap.mAPIName); + for (const nsACString& tag : cap.mAPITags) { + tags.AppendLiteral(":"); + tags.Append(tag); + } + }); + s.AppendLiteral("]"); + return s; + } + + nsCString mName; + nsCString mVersion; + nsTArray<GMPCapability> mCapabilities; +}; + +StaticMutex sGMPCapabilitiesMutex; +StaticAutoPtr<nsTArray<GMPCapabilityAndVersion>> sGMPCapabilities; + +static auto GMPCapabilitiesToString() { + return StringJoin(", "_ns, *sGMPCapabilities, + [](nsACString& dest, const GMPCapabilityAndVersion& gmp) { + dest.Append(gmp.ToString()); + }); +} + +/* static */ +void GeckoMediaPluginServiceChild::UpdateGMPCapabilities( + nsTArray<GMPCapabilityData>&& aCapabilities) { + { + // The mutex should unlock before sending the "gmp-changed" observer + // service notification. + StaticMutexAutoLock lock(sGMPCapabilitiesMutex); + if (!sGMPCapabilities) { + sGMPCapabilities = new nsTArray<GMPCapabilityAndVersion>(); + ClearOnShutdown(&sGMPCapabilities); + } + sGMPCapabilities->Clear(); + for (const GMPCapabilityData& plugin : aCapabilities) { + sGMPCapabilities->AppendElement(GMPCapabilityAndVersion(plugin)); + } + + GMP_LOG_DEBUG("%s::%s {%s}", __CLASS__, __FUNCTION__, + GMPCapabilitiesToString().get()); + } + + // Fire a notification so that any MediaKeySystemAccess + // requests waiting on a CDM to download will retry. + nsCOMPtr<nsIObserverService> obsService = + mozilla::services::GetObserverService(); + MOZ_ASSERT(obsService); + if (obsService) { + obsService->NotifyObservers(nullptr, "gmp-changed", nullptr); + } +} + +void GeckoMediaPluginServiceChild::BeginShutdown() { + AssertOnGMPThread(); + GMP_LOG_DEBUG("%s::%s: mServiceChild=%p,", __CLASS__, __FUNCTION__, + mServiceChild.get()); + // It's possible this gets called twice if the parent sends us a message to + // shutdown and we block shutdown in content in close proximity. + mShuttingDownOnGMPThread = true; + RemoveShutdownBlockerIfNeeded(); +} + +NS_IMETHODIMP +GeckoMediaPluginServiceChild::HasPluginForAPI(const nsACString& aAPI, + const nsTArray<nsCString>& aTags, + bool* aHasPlugin) { + StaticMutexAutoLock lock(sGMPCapabilitiesMutex); + if (!sGMPCapabilities) { + *aHasPlugin = false; + return NS_OK; + } + + nsCString api(aAPI); + for (const GMPCapabilityAndVersion& plugin : *sGMPCapabilities) { + if (GMPCapability::Supports(plugin.mCapabilities, api, aTags)) { + *aHasPlugin = true; + return NS_OK; + } + } + + *aHasPlugin = false; + return NS_OK; +} + +NS_IMETHODIMP +GeckoMediaPluginServiceChild::GetNodeId( + const nsAString& aOrigin, const nsAString& aTopLevelOrigin, + const nsAString& aGMPName, UniquePtr<GetNodeIdCallback>&& aCallback) { + AssertOnGMPThread(); + + GetNodeIdCallback* rawCallback = aCallback.release(); + nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread()); + nsString origin(aOrigin); + nsString topLevelOrigin(aTopLevelOrigin); + nsString gmpName(aGMPName); + GetServiceChild()->Then( + thread, __func__, + [rawCallback, origin, topLevelOrigin, gmpName](GMPServiceChild* child) { + UniquePtr<GetNodeIdCallback> callback(rawCallback); + nsCString outId; + if (!child->SendGetGMPNodeId(origin, topLevelOrigin, gmpName, &outId)) { + callback->Done(NS_ERROR_FAILURE, ""_ns); + return; + } + + callback->Done(NS_OK, outId); + }, + [rawCallback](nsresult rv) { + UniquePtr<GetNodeIdCallback> callback(rawCallback); + callback->Done(NS_ERROR_FAILURE, ""_ns); + }); + + return NS_OK; +} + +NS_IMETHODIMP +GeckoMediaPluginServiceChild::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aSomeData) { + MOZ_ASSERT(NS_IsMainThread()); + GMP_LOG_DEBUG("%s::%s: aTopic=%s", __CLASS__, __FUNCTION__, aTopic); + if (!strcmp(NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, aTopic)) { + if (mServiceChild) { + MutexAutoLock lock(mMutex); + mozilla::SyncRunnable::DispatchToThread( + mGMPThread, + WrapRunnable(mServiceChild.get(), &PGMPServiceChild::Close)); + mServiceChild = nullptr; + } + ShutdownGMPThread(); + } + + return NS_OK; +} + +RefPtr<GeckoMediaPluginServiceChild::GetServiceChildPromise> +GeckoMediaPluginServiceChild::GetServiceChild() { + AssertOnGMPThread(); + + if (!mServiceChild) { + if (mShuttingDownOnGMPThread) { + // We have begun shutdown. Don't allow a new connection to the main + // process to be instantiated. This also prevents new plugins being + // instantiated. + return GetServiceChildPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + dom::ContentChild* contentChild = dom::ContentChild::GetSingleton(); + if (!contentChild) { + return GetServiceChildPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + MozPromiseHolder<GetServiceChildPromise>* holder = + mGetServiceChildPromises.AppendElement(); + RefPtr<GetServiceChildPromise> promise = holder->Ensure(__func__); + if (mGetServiceChildPromises.Length() == 1) { + nsCOMPtr<nsIRunnable> r = + WrapRunnable(contentChild, &dom::ContentChild::SendCreateGMPService); + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()); + } + return promise; + } + return GetServiceChildPromise::CreateAndResolve(mServiceChild.get(), + __func__); +} + +void GeckoMediaPluginServiceChild::SetServiceChild( + RefPtr<GMPServiceChild>&& aServiceChild) { + AssertOnGMPThread(); + MOZ_ASSERT(!mServiceChild, "Should not already have service child!"); + GMP_LOG_DEBUG("%s::%s: aServiceChild=%p", __CLASS__, __FUNCTION__, + aServiceChild.get()); + + mServiceChild = std::move(aServiceChild); + + nsTArray<MozPromiseHolder<GetServiceChildPromise>> holders = + std::move(mGetServiceChildPromises); + for (MozPromiseHolder<GetServiceChildPromise>& holder : holders) { + holder.Resolve(mServiceChild.get(), __func__); + } +} + +void GeckoMediaPluginServiceChild::RemoveGMPContentParent( + GMPContentParent* aGMPContentParent) { + AssertOnGMPThread(); + GMP_LOG_DEBUG( + "%s::%s: aGMPContentParent=%p, mServiceChild=%p, " + "mShuttingDownOnGMPThread=%s", + __CLASS__, __FUNCTION__, aGMPContentParent, mServiceChild.get(), + mShuttingDownOnGMPThread ? "true" : "false"); + + if (mServiceChild) { + mServiceChild->RemoveGMPContentParent(aGMPContentParent); + GMP_LOG_DEBUG( + "%s::%s: aGMPContentParent removed, " + "mServiceChild->HaveContentParents()=%s", + __CLASS__, __FUNCTION__, + mServiceChild->HaveContentParents() ? "true" : "false"); + RemoveShutdownBlockerIfNeeded(); + } +} + +nsresult GeckoMediaPluginServiceChild::AddShutdownBlocker() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mShuttingDownOnGMPThread, + "No call paths should add blockers once we're shutting down!"); + MOZ_ASSERT(!mShutdownBlockerAdded, "Should only add blocker once!"); + GMP_LOG_DEBUG("%s::%s ", __CLASS__, __FUNCTION__); + + nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier(); + if (NS_WARN_IF(!barrier)) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = + barrier->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), + __LINE__, kShutdownBlockerName); +#ifdef DEBUG + mShutdownBlockerAdded = NS_SUCCEEDED(rv); +#endif + return rv; +} + +void GeckoMediaPluginServiceChild::RemoveShutdownBlocker() { + AssertOnGMPThread(); + MOZ_ASSERT(mShuttingDownOnGMPThread, + "We should only remove blockers once we're " + "shutting down!"); + GMP_LOG_DEBUG("%s::%s ", __CLASS__, __FUNCTION__); + nsresult rv = mMainThread->Dispatch(NS_NewRunnableFunction( + "GeckoMediaPluginServiceChild::" + "RemoveShutdownBlocker", + [this, self = RefPtr<GeckoMediaPluginServiceChild>(this)]() { + nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier(); + if (NS_WARN_IF(!barrier)) { + return; + } + + nsresult rv = barrier->RemoveBlocker(this); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + })); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_ASSERT_UNREACHABLE( + "Main thread should always be alive when we " + "call this!"); + } +} + +void GeckoMediaPluginServiceChild::RemoveShutdownBlockerIfNeeded() { + AssertOnGMPThread(); + GMP_LOG_DEBUG( + "%s::%s mPendingGetContentParents=%" PRIu32 + " mServiceChild->HaveContentParents()=%s " + "mShuttingDownOnGMPThread=%s", + __CLASS__, __FUNCTION__, mPendingGetContentParents, + mServiceChild && mServiceChild->HaveContentParents() ? "true" : "false", + mShuttingDownOnGMPThread ? "true" : "false"); + + bool haveOneOrMoreContentParents = + mPendingGetContentParents > 0 || + (mServiceChild && mServiceChild->HaveContentParents()); + + if (!mShuttingDownOnGMPThread || haveOneOrMoreContentParents) { + return; + } + RemoveShutdownBlocker(); +} + +already_AddRefed<GMPContentParent> GMPServiceChild::GetBridgedGMPContentParent( + ProcessId aOtherPid, ipc::Endpoint<PGMPContentParent>&& endpoint) { + return do_AddRef(mContentParents.LookupOrInsertWith(aOtherPid, [&] { + MOZ_ASSERT(aOtherPid == endpoint.OtherPid()); + + auto parent = MakeRefPtr<GMPContentParent>(); + + DebugOnly<bool> ok = endpoint.Bind(parent); + MOZ_ASSERT(ok); + + return parent; + })); +} + +void GMPServiceChild::RemoveGMPContentParent( + GMPContentParent* aGMPContentParent) { + for (auto iter = mContentParents.Iter(); !iter.Done(); iter.Next()) { + RefPtr<GMPContentParent>& parent = iter.Data(); + if (parent == aGMPContentParent) { + iter.Remove(); + break; + } + } +} + +void GMPServiceChild::GetAlreadyBridgedTo( + nsTArray<base::ProcessId>& aAlreadyBridgedTo) { + AppendToArray(aAlreadyBridgedTo, mContentParents.Keys()); +} + +class OpenPGMPServiceChild : public mozilla::Runnable { + public: + OpenPGMPServiceChild(RefPtr<GMPServiceChild>&& aGMPServiceChild, + ipc::Endpoint<PGMPServiceChild>&& aEndpoint) + : Runnable("gmp::OpenPGMPServiceChild"), + mGMPServiceChild(std::move(aGMPServiceChild)), + mEndpoint(std::move(aEndpoint)) {} + + NS_IMETHOD Run() override { + RefPtr<GeckoMediaPluginServiceChild> gmp = + GeckoMediaPluginServiceChild::GetSingleton(); + MOZ_RELEASE_ASSERT(gmp); + MOZ_ASSERT(!gmp->mServiceChild); + if (mEndpoint.Bind(mGMPServiceChild.get())) { + gmp->SetServiceChild(std::move(mGMPServiceChild)); + } else { + gmp->SetServiceChild(nullptr); + } + return NS_OK; + } + + private: + RefPtr<GMPServiceChild> mGMPServiceChild; + ipc::Endpoint<PGMPServiceChild> mEndpoint; +}; + +/* static */ +bool GMPServiceChild::Create(Endpoint<PGMPServiceChild>&& aGMPService) { + RefPtr<GeckoMediaPluginServiceChild> gmp = + GeckoMediaPluginServiceChild::GetSingleton(); + if (NS_WARN_IF(!gmp)) { + return false; + } + + MOZ_ASSERT(!gmp->mServiceChild); + + RefPtr<GMPServiceChild> serviceChild(new GMPServiceChild()); + + nsCOMPtr<nsIThread> gmpThread; + nsresult rv = gmp->GetThread(getter_AddRefs(gmpThread)); + NS_ENSURE_SUCCESS(rv, false); + + rv = gmpThread->Dispatch( + new OpenPGMPServiceChild(std::move(serviceChild), std::move(aGMPService)), + NS_DISPATCH_NORMAL); + return NS_SUCCEEDED(rv); +} + +ipc::IPCResult GMPServiceChild::RecvBeginShutdown() { + RefPtr<GeckoMediaPluginServiceChild> service = + GeckoMediaPluginServiceChild::GetSingleton(); + MOZ_ASSERT(service && service->mServiceChild.get() == this); + if (service) { + service->BeginShutdown(); + } + return IPC_OK(); +} + +bool GMPServiceChild::HaveContentParents() const { + return mContentParents.Count() > 0; +} + +} // namespace mozilla::gmp + +#undef __CLASS__ diff --git a/dom/media/gmp/GMPServiceChild.h b/dom/media/gmp/GMPServiceChild.h new file mode 100644 index 0000000000..2775a83489 --- /dev/null +++ b/dom/media/gmp/GMPServiceChild.h @@ -0,0 +1,164 @@ +/* -*- 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/. */ + +#ifndef GMPServiceChild_h_ +#define GMPServiceChild_h_ + +#include "GMPService.h" +#include "MediaResult.h" +#include "base/process.h" +#include "mozilla/dom/PContent.h" +#include "mozilla/gmp/PGMPServiceChild.h" +#include "mozilla/MozPromise.h" +#include "nsIAsyncShutdown.h" +#include "nsRefPtrHashtable.h" + +namespace mozilla::gmp { + +class GMPContentParent; +class GMPServiceChild; + +class GeckoMediaPluginServiceChild : public GeckoMediaPluginService, + public nsIAsyncShutdownBlocker { + friend class GMPServiceChild; + + public: + static already_AddRefed<GeckoMediaPluginServiceChild> GetSingleton(); + nsresult Init() override; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIASYNCSHUTDOWNBLOCKER + + NS_IMETHOD HasPluginForAPI(const nsACString& aAPI, + const nsTArray<nsCString>& aTags, + bool* aRetVal) override; + NS_IMETHOD GetNodeId(const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aGMPName, + UniquePtr<GetNodeIdCallback>&& aCallback) override; + + NS_DECL_NSIOBSERVER + + void SetServiceChild(RefPtr<GMPServiceChild>&& aServiceChild); + + void RemoveGMPContentParent(GMPContentParent* aGMPContentParent); + + static void UpdateGMPCapabilities( + nsTArray<mozilla::dom::GMPCapabilityData>&& aCapabilities); + + void BeginShutdown(); + + protected: + void InitializePlugins(nsISerialEventTarget*) override { + // Nothing to do here. + } + + RefPtr<GetGMPContentParentPromise> GetContentParent( + GMPCrashHelper* aHelper, const NodeIdVariant& aNodeIdVariant, + const nsACString& aAPI, const nsTArray<nsCString>& aTags) override; + + private: + friend class OpenPGMPServiceChild; + + ~GeckoMediaPluginServiceChild() override; + + typedef MozPromise<GMPServiceChild*, MediaResult, /* IsExclusive = */ true> + GetServiceChildPromise; + RefPtr<GetServiceChildPromise> GetServiceChild(); + + nsTArray<MozPromiseHolder<GetServiceChildPromise>> mGetServiceChildPromises; + RefPtr<GMPServiceChild> mServiceChild; + + // Shutdown blocker management. A shutdown blocker is used to ensure that + // we do not shutdown until all content parents are removed from + // GMPServiceChild::mContentParents. + // + // The following rules let us shutdown block (and thus we shouldn't + // violate them): + // - We will only call GetContentParent if mShuttingDownOnGMPThread is false. + // - mShuttingDownOnGMPThread will become true once profile teardown is + // observed in the parent process (and once GMPServiceChild receives a + // message from GMPServiceParent) or if we block shutdown (which implies + // we're in shutdown). + // - If we currently have mPendingGetContentParents > 0 or parents in + // GMPServiceChild::mContentParents we should block shutdown so such + // parents can be cleanly shutdown. + // therefore + // - We can block shutdown at xpcom-will-shutdown until our content parents + // are handled. + // - Because once mShuttingDownOnGMPThread is true we cannot add new content + // parents, we know that when mShuttingDownOnGMPThread && all content + // parents are handled we'll never add more and it's safe to stop blocking + // shutdown. + // this relies on + // - Once we're shutting down, we need to ensure all content parents are + // shutdown and removed. Failure to do so will result in a shutdown stall. + + // Note that at the time of writing there are significant differences in how + // xpcom shutdown is handled in release and non-release builds. For example, + // in release builds content processes are exited early, so xpcom shutdown + // is not observed (and not blocked by blockers). This is important to keep + // in mind when testing the shutdown blocking machinery (you won't see most + // of it be invoked in release). + + // All of these members should only be used on the GMP thread unless + // otherwise noted! + + // Add a shutdown blocker. Main thread only. Should only be called once when + // we init the service. + nsresult AddShutdownBlocker(); + // Remove a shutdown blocker. Should be called once at most and only when + // mShuttingDownOnGMPThread. Prefer RemoveShutdownBlockerIfNeeded unless + // absolutely certain you need to call this directly. + void RemoveShutdownBlocker(); + // Remove shutdown blocker if the following conditions are met: + // - mShuttingDownOnGMPThread. + // - !mServiceChild->HaveContentParents. + // - mPendingGetContentParents == 0. + // - mShutdownBlockerHasBeenAdded. + void RemoveShutdownBlockerIfNeeded(); + +#ifdef DEBUG + // Track if we've added a shutdown blocker for sanity checking. Main thread + // only. + bool mShutdownBlockerAdded = false; +#endif // DEBUG + // The number of GetContentParent calls that have not yet been resolved or + // rejected. We use this value to help determine if we need to block + // shutdown. Should only be used on GMP thread to avoid races. + uint32_t mPendingGetContentParents = 0; + // End shutdown blocker management. +}; + +class GMPServiceChild : public PGMPServiceChild { + public: + // Mark AddRef and Release as `final`, as they overload pure virtual + // implementations in PGMPServiceChild. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPServiceChild, final) + + explicit GMPServiceChild() = default; + + already_AddRefed<GMPContentParent> GetBridgedGMPContentParent( + ProcessId aOtherPid, ipc::Endpoint<PGMPContentParent>&& endpoint); + + void RemoveGMPContentParent(GMPContentParent* aGMPContentParent); + + void GetAlreadyBridgedTo(nsTArray<ProcessId>& aAlreadyBridgedTo); + + static bool Create(Endpoint<PGMPServiceChild>&& aGMPService); + + ipc::IPCResult RecvBeginShutdown() override; + + bool HaveContentParents() const; + + private: + ~GMPServiceChild() = default; + + nsRefPtrHashtable<nsUint64HashKey, GMPContentParent> mContentParents; +}; + +} // namespace mozilla::gmp + +#endif // GMPServiceChild_h_ diff --git a/dom/media/gmp/GMPServiceParent.cpp b/dom/media/gmp/GMPServiceParent.cpp new file mode 100644 index 0000000000..d5d72a3655 --- /dev/null +++ b/dom/media/gmp/GMPServiceParent.cpp @@ -0,0 +1,1943 @@ +/* -*- 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 "GMPServiceParent.h" + +#include <limits> + +#include "GMPDecoderModule.h" +#include "GMPLog.h" +#include "GMPParent.h" +#include "GMPVideoDecoderParent.h" +#include "mozilla/ipc/GeckoChildProcessHost.h" +#include "base/task.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Logging.h" +#include "mozilla/Preferences.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/ipc/Endpoint.h" +#include "nsThreadUtils.h" +#if defined(XP_LINUX) && defined(MOZ_SANDBOX) +# include "mozilla/SandboxInfo.h" +#endif +#include "VideoUtils.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsComponentManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsNetUtil.h" +#include "nsHashKeys.h" +#include "nsIFile.h" +#include "nsIObserverService.h" +#include "nsIXULRuntime.h" +#include "nsNativeCharsetUtils.h" +#include "nsXPCOMPrivate.h" +#include "prio.h" +#include "runnable_utils.h" + +#ifdef DEBUG +# include "mozilla/dom/MediaKeys.h" // MediaKeys::kMediaKeysRequestTopic +#endif + +namespace mozilla::gmp { + +#ifdef __CLASS__ +# undef __CLASS__ +#endif +#define __CLASS__ "GMPServiceParent" + +static const uint32_t NodeIdSaltLength = 32; + +already_AddRefed<GeckoMediaPluginServiceParent> +GeckoMediaPluginServiceParent::GetSingleton() { + MOZ_ASSERT(XRE_IsParentProcess()); + RefPtr<GeckoMediaPluginService> service( + GeckoMediaPluginServiceParent::GetGeckoMediaPluginService()); +#ifdef DEBUG + if (service) { + nsCOMPtr<mozIGeckoMediaPluginChromeService> chromeService; + CallQueryInterface(service.get(), getter_AddRefs(chromeService)); + MOZ_ASSERT(chromeService); + } +#endif + return service.forget().downcast<GeckoMediaPluginServiceParent>(); +} + +NS_IMPL_ISUPPORTS_INHERITED(GeckoMediaPluginServiceParent, + GeckoMediaPluginService, + mozIGeckoMediaPluginChromeService, + nsIAsyncShutdownBlocker) + +GeckoMediaPluginServiceParent::GeckoMediaPluginServiceParent() + : mScannedPluginOnDisk(false), + mShuttingDown(false), + mWaitingForPluginsSyncShutdown(false), + mInitPromiseMonitor("GeckoMediaPluginServiceParent::mInitPromiseMonitor"), + mInitPromise(&mInitPromiseMonitor), + mLoadPluginsFromDiskComplete(false) { + MOZ_ASSERT(NS_IsMainThread()); +} + +GeckoMediaPluginServiceParent::~GeckoMediaPluginServiceParent() { + MOZ_ASSERT(mPlugins.IsEmpty()); +} + +nsresult GeckoMediaPluginServiceParent::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + if (AppShutdown::GetCurrentShutdownPhase() != ShutdownPhase::NotInShutdown) { + return NS_OK; + } + + nsCOMPtr<nsIObserverService> obsService = + mozilla::services::GetObserverService(); + MOZ_ASSERT(obsService); + + MOZ_ALWAYS_SUCCEEDS( + obsService->AddObserver(this, "profile-change-teardown", false)); + MOZ_ALWAYS_SUCCEEDS( + obsService->AddObserver(this, "last-pb-context-exited", false)); + MOZ_ALWAYS_SUCCEEDS( + obsService->AddObserver(this, "browser:purge-session-history", false)); + MOZ_ALWAYS_SUCCEEDS( + obsService->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, false)); + +#ifdef DEBUG + MOZ_ALWAYS_SUCCEEDS(obsService->AddObserver( + this, dom::MediaKeys::kMediaKeysRequestTopic, false)); +#endif + + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) { + prefs->AddObserver("media.gmp.plugin.crash", this, false); + } + + nsresult rv = InitStorage(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Kick off scanning for plugins + nsCOMPtr<nsIThread> thread; + rv = GetThread(getter_AddRefs(thread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Detect if GMP storage has an incompatible version, and if so nuke it. + int32_t version = + Preferences::GetInt("media.gmp.storage.version.observed", 0); + int32_t expected = + Preferences::GetInt("media.gmp.storage.version.expected", 0); + if (version != expected) { + Preferences::SetInt("media.gmp.storage.version.observed", expected); + return GMPDispatch( + NewRunnableMethod("gmp::GeckoMediaPluginServiceParent::ClearStorage", + this, &GeckoMediaPluginServiceParent::ClearStorage)); + } + return NS_OK; +} + +already_AddRefed<nsIFile> CloneAndAppend(nsIFile* aFile, + const nsAString& aDir) { + nsCOMPtr<nsIFile> f; + nsresult rv = aFile->Clone(getter_AddRefs(f)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + rv = f->Append(aDir); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + return f.forget(); +} + +static nsresult GMPPlatformString(nsAString& aOutPlatform) { + // Append the OS and arch so that we don't reuse the storage if the profile is + // copied or used under a different bit-ness, or copied to another platform. + nsCOMPtr<nsIXULRuntime> runtime = do_GetService("@mozilla.org/xre/runtime;1"); + if (!runtime) { + return NS_ERROR_FAILURE; + } + + nsAutoCString OS; + nsresult rv = runtime->GetOS(OS); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString arch; + rv = runtime->GetXPCOMABI(arch); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString platform; + platform.Append(OS); + platform.AppendLiteral("_"); + platform.Append(arch); + + CopyUTF8toUTF16(platform, aOutPlatform); + + return NS_OK; +} + +nsresult GeckoMediaPluginServiceParent::InitStorage() { + MOZ_ASSERT(NS_IsMainThread()); + + // GMP storage should be used in the chrome process only. + if (!XRE_IsParentProcess()) { + return NS_OK; + } + + // Directory service is main thread only, so cache the profile dir here + // so that we can use it off main thread. + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mStorageBaseDir)); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mStorageBaseDir->AppendNative("gmp"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoString platform; + rv = GMPPlatformString(platform); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mStorageBaseDir->Append(platform); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return GeckoMediaPluginService::Init(); +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aSomeData) { + GMP_LOG_DEBUG("%s::%s topic='%s' data='%s'", __CLASS__, __FUNCTION__, aTopic, + NS_ConvertUTF16toUTF8(aSomeData).get()); + if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + nsCOMPtr<nsIPrefBranch> branch(do_QueryInterface(aSubject)); + if (branch) { + bool crashNow = false; + if (u"media.gmp.plugin.crash"_ns.Equals(aSomeData)) { + branch->GetBoolPref("media.gmp.plugin.crash", &crashNow); + } + if (crashNow) { + nsCOMPtr<nsIThread> gmpThread; + { + MutexAutoLock lock(mMutex); + gmpThread = mGMPThread; + } + if (gmpThread) { + // Note: the GeckoMediaPluginServiceParent singleton is kept alive by + // a static refptr that is only cleared in the final stage of shutdown + // after everything else is shutdown, so this RefPtr<> is not strictly + // necessary so long as that is true, but it's safer. + gmpThread->Dispatch( + WrapRunnable(RefPtr<GeckoMediaPluginServiceParent>(this), + &GeckoMediaPluginServiceParent::CrashPlugins), + NS_DISPATCH_NORMAL); + } + } + } + } else if (!strcmp("profile-change-teardown", aTopic)) { + mWaitingForPluginsSyncShutdown = true; + + nsCOMPtr<nsIThread> gmpThread; + DebugOnly<bool> plugins_empty; + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(!mShuttingDown); + mShuttingDown = true; + gmpThread = mGMPThread; +#ifdef DEBUG + plugins_empty = mPlugins.IsEmpty(); +#endif + } + + if (gmpThread) { + GMP_LOG_DEBUG( + "%s::%s Starting to unload plugins, waiting for sync shutdown...", + __CLASS__, __FUNCTION__); + gmpThread->Dispatch( + NewRunnableMethod("gmp::GeckoMediaPluginServiceParent::UnloadPlugins", + this, + &GeckoMediaPluginServiceParent::UnloadPlugins), + NS_DISPATCH_NORMAL); + + // Wait for UnloadPlugins() to do sync shutdown... + SpinEventLoopUntil( + "GeckoMediaPluginServiceParent::Observe " + "WaitingForPluginsSyncShutdown"_ns, + [&]() { return !mWaitingForPluginsSyncShutdown; }); + } else { + // GMP thread has already shutdown. + MOZ_ASSERT(plugins_empty); + mWaitingForPluginsSyncShutdown = false; + } + + } else if (!strcmp(NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, aTopic)) { +#ifdef DEBUG + MOZ_ASSERT(mShuttingDown); +#endif + ShutdownGMPThread(); + } else if (!strcmp(NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, aTopic)) { + mXPCOMWillShutdown = true; + } else if (!strcmp("last-pb-context-exited", aTopic)) { + // When Private Browsing mode exits, all we need to do is clear + // mTempNodeIds. This drops all the node ids we've cached in memory + // for PB origin-pairs. If we try to open an origin-pair for non-PB + // mode, we'll get the NodeId salt stored on-disk, and if we try to + // open a PB mode origin-pair, we'll re-generate new salt. + mTempNodeIds.Clear(); + } else if (!strcmp("browser:purge-session-history", aTopic)) { + // Clear everything! + if (!aSomeData || nsDependentString(aSomeData).IsEmpty()) { + return GMPDispatch(NewRunnableMethod( + "gmp::GeckoMediaPluginServiceParent::ClearStorage", this, + &GeckoMediaPluginServiceParent::ClearStorage)); + } + + // Clear nodeIds/records modified after |t|. + nsresult rv; + PRTime t = nsDependentString(aSomeData).ToInteger64(&rv, 10); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return GMPDispatch(NewRunnableMethod<PRTime>( + "gmp::GeckoMediaPluginServiceParent::ClearRecentHistoryOnGMPThread", + this, &GeckoMediaPluginServiceParent::ClearRecentHistoryOnGMPThread, + t)); + } + + return NS_OK; +} + +RefPtr<GenericPromise> GeckoMediaPluginServiceParent::EnsureInitialized() { + MonitorAutoLock lock(mInitPromiseMonitor); + if (mLoadPluginsFromDiskComplete) { + return GenericPromise::CreateAndResolve(true, __func__); + } + // We should have an init promise in flight. + MOZ_ASSERT(!mInitPromise.IsEmpty()); + return mInitPromise.Ensure(__func__); +} + +RefPtr<GetGMPContentParentPromise> +GeckoMediaPluginServiceParent::GetContentParent( + GMPCrashHelper* aHelper, const NodeIdVariant& aNodeIdVariant, + const nsACString& aAPI, const nsTArray<nsCString>& aTags) { + AssertOnGMPThread(); + + nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread()); + if (!thread) { + MOZ_ASSERT_UNREACHABLE( + "We should always be called on GMP thread, so it should be live"); + return GetGMPContentParentPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + + nsCString nodeIdString; + nsresult rv = GetNodeId(aNodeIdVariant, nodeIdString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return GetGMPContentParentPromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + + auto holder = MakeUnique<MozPromiseHolder<GetGMPContentParentPromise>>(); + RefPtr<GetGMPContentParentPromise> promise = holder->Ensure(__func__); + EnsureInitialized()->Then( + thread, __func__, + [self = RefPtr<GeckoMediaPluginServiceParent>(this), + nodeIdString = std::move(nodeIdString), api = nsCString(aAPI), + tags = aTags.Clone(), helper = RefPtr<GMPCrashHelper>(aHelper), + holder = std::move(holder)]( + const GenericPromise::ResolveOrRejectValue& aValue) mutable -> void { + if (aValue.IsReject()) { + NS_WARNING("GMPService::EnsureInitialized failed."); + holder->Reject(NS_ERROR_FAILURE, __func__); + return; + } + RefPtr<GMPParent> gmp = + self->SelectPluginForAPI(nodeIdString, api, tags); + GMP_LOG_DEBUG("%s: %p returning %p for api %s", __FUNCTION__, + self.get(), gmp.get(), api.get()); + if (!gmp) { + NS_WARNING( + "GeckoMediaPluginServiceParent::GetContentParentFrom failed"); + holder->Reject(NS_ERROR_FAILURE, __func__); + return; + } + self->ConnectCrashHelper(gmp->GetPluginId(), helper); + gmp->GetGMPContentParent(std::move(holder)); + }); + + return promise; +} + +void GeckoMediaPluginServiceParent::InitializePlugins( + nsISerialEventTarget* aGMPThread) { + MOZ_ASSERT(aGMPThread); + MonitorAutoLock lock(mInitPromiseMonitor); + if (mLoadPluginsFromDiskComplete) { + return; + } + + RefPtr<GeckoMediaPluginServiceParent> self(this); + RefPtr<GenericPromise> p = mInitPromise.Ensure(__func__); + InvokeAsync(aGMPThread, this, __func__, + &GeckoMediaPluginServiceParent::LoadFromEnvironment) + ->Then( + aGMPThread, __func__, + [self]() -> void { + MonitorAutoLock lock(self->mInitPromiseMonitor); + self->mLoadPluginsFromDiskComplete = true; + self->mInitPromise.Resolve(true, __func__); + }, + [self]() -> void { + MonitorAutoLock lock(self->mInitPromiseMonitor); + self->mLoadPluginsFromDiskComplete = true; + self->mInitPromise.Reject(NS_ERROR_FAILURE, __func__); + }); +} + +void GeckoMediaPluginServiceParent::NotifySyncShutdownComplete() { + MOZ_ASSERT(NS_IsMainThread()); + mWaitingForPluginsSyncShutdown = false; +} + +bool GeckoMediaPluginServiceParent::IsShuttingDown() { + AssertOnGMPThread(); + return mShuttingDownOnGMPThread; +} + +void GeckoMediaPluginServiceParent::UnloadPlugins() { + AssertOnGMPThread(); + MOZ_ASSERT(!mShuttingDownOnGMPThread); + mShuttingDownOnGMPThread = true; + + nsTArray<RefPtr<GMPParent>> plugins; + { + MutexAutoLock lock(mMutex); + // Move all plugins references to a local array. This way mMutex won't be + // locked when calling CloseActive (to avoid inter-locking). + std::swap(plugins, mPlugins); + + for (GMPServiceParent* parent : mServiceParents) { + Unused << parent->SendBeginShutdown(); + } + + GMP_LOG_DEBUG("%s::%s plugins:%zu", __CLASS__, __FUNCTION__, + plugins.Length()); +#ifdef DEBUG + for (const auto& plugin : plugins) { + GMP_LOG_DEBUG("%s::%s plugin: '%s'", __CLASS__, __FUNCTION__, + plugin->GetDisplayName().get()); + } +#endif + } + + // Note: CloseActive may be async; it could actually finish + // shutting down when all the plugins have unloaded. + for (const auto& plugin : plugins) { + plugin->CloseActive(true); + } + + nsCOMPtr<nsIRunnable> task = NewRunnableMethod( + "GeckoMediaPluginServiceParent::NotifySyncShutdownComplete", this, + &GeckoMediaPluginServiceParent::NotifySyncShutdownComplete); + mMainThread->Dispatch(task.forget()); +} + +void GeckoMediaPluginServiceParent::CrashPlugins() { + GMP_LOG_DEBUG("%s::%s", __CLASS__, __FUNCTION__); + AssertOnGMPThread(); + + MutexAutoLock lock(mMutex); + for (size_t i = 0; i < mPlugins.Length(); i++) { + mPlugins[i]->Crash(); + } +} + +RefPtr<GenericPromise> GeckoMediaPluginServiceParent::LoadFromEnvironment() { + AssertOnGMPThread(); + nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread()); + if (!thread) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + const char* env = PR_GetEnv("MOZ_GMP_PATH"); + if (!env || !*env) { + return GenericPromise::CreateAndResolve(true, __func__); + } + + nsString allpaths; + if (NS_WARN_IF(NS_FAILED( + NS_CopyNativeToUnicode(nsDependentCString(env), allpaths)))) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + nsTArray<RefPtr<GenericPromise>> promises; + uint32_t pos = 0; + while (pos < allpaths.Length()) { + // Loop over multiple path entries separated by colons (*nix) or + // semicolons (Windows) + int32_t next = allpaths.FindChar(XPCOM_ENV_PATH_SEPARATOR[0], pos); + if (next == -1) { + promises.AppendElement( + AddOnGMPThread(nsString(Substring(allpaths, pos)))); + break; + } else { + promises.AppendElement( + AddOnGMPThread(nsString(Substring(allpaths, pos, next - pos)))); + pos = next + 1; + } + } + + mScannedPluginOnDisk = true; + return GenericPromise::All(thread, promises) + ->Then( + thread, __func__, + []() { return GenericPromise::CreateAndResolve(true, __func__); }, + []() { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + }); +} + +class NotifyObserversTask final : public mozilla::Runnable { + public: + explicit NotifyObserversTask(const char* aTopic, nsString aData = u""_ns) + : Runnable(aTopic), mTopic(aTopic), mData(aData) {} + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsIObserverService> obsService = + mozilla::services::GetObserverService(); + MOZ_ASSERT(obsService); + if (obsService) { + obsService->NotifyObservers(nullptr, mTopic, mData.get()); + } + return NS_OK; + } + + private: + ~NotifyObserversTask() = default; + const char* mTopic; + const nsString mData; +}; + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::PathRunnable::Run() { + mService->RemoveOnGMPThread(mPath, mOperation == REMOVE_AND_DELETE_FROM_DISK, + mDefer); + + mService->UpdateContentProcessGMPCapabilities(); + return NS_OK; +} + +void GeckoMediaPluginServiceParent::UpdateContentProcessGMPCapabilities( + ContentParent* aContentProcess) { + if (!NS_IsMainThread()) { + nsCOMPtr<nsIRunnable> task = NewRunnableMethod<ContentParent*>( + "GeckoMediaPluginServiceParent::UpdateContentProcessGMPCapabilities", + this, + &GeckoMediaPluginServiceParent::UpdateContentProcessGMPCapabilities, + aContentProcess); + mMainThread->Dispatch(task.forget()); + return; + } + + typedef mozilla::dom::GMPCapabilityData GMPCapabilityData; + typedef mozilla::dom::GMPAPITags GMPAPITags; + typedef mozilla::dom::ContentParent ContentParent; + + const uint32_t NO_H264 = 0; + const uint32_t HAS_H264 = 1; + const uint32_t NO_H264_1_DIR = 2; + const uint32_t NO_H264_2_PLUS_DIRS = 3; + const uint32_t NO_H264_DIR_IN_PROGRESS = 4; + uint32_t hasH264 = NO_H264; + if (mDirectoriesAdded == 1) { + hasH264 = NO_H264_1_DIR; + } else if (mDirectoriesAdded > 1) { + hasH264 = NO_H264_2_PLUS_DIRS; + } + if (mDirectoriesInProgress) { + hasH264 = NO_H264_DIR_IN_PROGRESS; + } + nsTArray<GMPCapabilityData> caps; + { + MutexAutoLock lock(mMutex); + for (const RefPtr<GMPParent>& gmp : mPlugins) { + // We have multiple instances of a GMPParent for a given GMP in the + // list, one per origin. So filter the list so that we don't include + // the same GMP's capabilities twice. + NS_ConvertUTF16toUTF8 name(gmp->GetPluginBaseName()); + bool found = false; + for (const GMPCapabilityData& cap : caps) { + if (cap.name().Equals(name)) { + found = true; + break; + } + } + if (found) { + continue; + } + GMPCapabilityData x; + x.name() = name; + x.version() = gmp->GetVersion(); + for (const GMPCapability& tag : gmp->GetCapabilities()) { + x.capabilities().AppendElement(GMPAPITags(tag.mAPIName, tag.mAPITags)); + if (tag.mAPIName == nsLiteralCString(GMP_API_VIDEO_ENCODER) && + tag.mAPITags.Contains("h264"_ns)) { + hasH264 = HAS_H264; + } + } + caps.AppendElement(std::move(x)); + } + } + + Telemetry::Accumulate(Telemetry::MEDIA_GMP_UPDATE_CONTENT_PROCESS_HAS_H264, + hasH264); + + if (aContentProcess) { + Unused << aContentProcess->SendGMPsChanged(caps); + return; + } + + for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) { + Unused << cp->SendGMPsChanged(caps); + } + + // For non-e10s, we must fire a notification so that any MediaKeySystemAccess + // requests waiting on a CDM to download will retry. + nsCOMPtr<nsIObserverService> obsService = + mozilla::services::GetObserverService(); + MOZ_ASSERT(obsService); + if (obsService) { + obsService->NotifyObservers(nullptr, "gmp-changed", nullptr); + } +} + +void GeckoMediaPluginServiceParent::SendFlushFOGData( + nsTArray<RefPtr<FlushFOGDataPromise>>& promises) { + MOZ_ASSERT(NS_IsMainThread()); + MutexAutoLock lock(mMutex); + + for (const RefPtr<GMPParent>& gmp : mPlugins) { + if (gmp->State() != GMPState::Loaded) { + // Plugins that are not in the Loaded state have no process attached to + // them, and any IPC we would attempt to send them would be ignored (or + // result in a warning on debug builds). + continue; + } + RefPtr<FlushFOGDataPromise::Private> promise = + new FlushFOGDataPromise::Private(__func__); + // Direct dispatch will resolve the promise on the same thread, which is + // faster; FOGIPC will move execution back to the main thread. + promise->UseDirectTaskDispatch(__func__); + promises.EmplaceBack(promise); + + mGMPThread->Dispatch( + NewRunnableMethod<ipc::ResolveCallback<ipc::ByteBuf>&&, + ipc::RejectCallback&&>( + "GMPParent::SendFlushFOGData", gmp, + static_cast<void (GMPParent::*)( + mozilla::ipc::ResolveCallback<ipc::ByteBuf>&& aResolve, + mozilla::ipc::RejectCallback&& aReject)>( + &GMPParent::SendFlushFOGData), + + [promise](ipc::ByteBuf&& aValue) { + promise->Resolve(std::move(aValue), __func__); + }, + [promise](ipc::ResponseRejectReason&& aReason) { + promise->Reject(std::move(aReason), __func__); + }), + NS_DISPATCH_NORMAL); + } +} + +RefPtr<PGMPParent::TestTriggerMetricsPromise> +GeckoMediaPluginServiceParent::TestTriggerMetrics() { + MOZ_ASSERT(NS_IsMainThread()); + { + MutexAutoLock lock(mMutex); + for (const RefPtr<GMPParent>& gmp : mPlugins) { + if (gmp->State() != GMPState::Loaded) { + // Plugins that are not in the Loaded state have no process attached to + // them, and any IPC we would attempt to send them would be ignored (or + // result in a warning on debug builds). + continue; + } + + RefPtr<PGMPParent::TestTriggerMetricsPromise::Private> promise = + new PGMPParent::TestTriggerMetricsPromise::Private(__func__); + // Direct dispatch will resolve the promise on the same thread, which is + // faster; FOGIPC will move execution back to the main thread. + promise->UseDirectTaskDispatch(__func__); + + mGMPThread->Dispatch( + NewRunnableMethod<ipc::ResolveCallback<bool>&&, + ipc::RejectCallback&&>( + "GMPParent::SendTestTriggerMetrics", gmp, + static_cast<void (GMPParent::*)( + mozilla::ipc::ResolveCallback<bool>&& aResolve, + mozilla::ipc::RejectCallback&& aReject)>( + &PGMPParent::SendTestTriggerMetrics), + + [promise](bool aValue) { + promise->Resolve(std::move(aValue), __func__); + }, + [promise](ipc::ResponseRejectReason&& aReason) { + promise->Reject(std::move(aReason), __func__); + }), + NS_DISPATCH_NORMAL); + + return promise; + } + } + + return PGMPParent::TestTriggerMetricsPromise::CreateAndReject( + ipc::ResponseRejectReason::SendError, __func__); +} + +RefPtr<GenericPromise> GeckoMediaPluginServiceParent::AsyncAddPluginDirectory( + const nsAString& aDirectory) { + nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread()); + if (!thread) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + mDirectoriesAdded++; + mDirectoriesInProgress++; + + nsString dir(aDirectory); + RefPtr<GeckoMediaPluginServiceParent> self = this; + return InvokeAsync(thread, this, __func__, + &GeckoMediaPluginServiceParent::AddOnGMPThread, dir) + ->Then( + mMainThread, __func__, + [dir, self](bool aVal) { + GMP_LOG_DEBUG( + "GeckoMediaPluginServiceParent::AsyncAddPluginDirectory %s " + "succeeded", + NS_ConvertUTF16toUTF8(dir).get()); + MOZ_ASSERT(NS_IsMainThread()); + self->mDirectoriesInProgress--; + self->UpdateContentProcessGMPCapabilities(); + return GenericPromise::CreateAndResolve(aVal, __func__); + }, + [dir, self](nsresult aResult) { + GMP_LOG_DEBUG( + "GeckoMediaPluginServiceParent::AsyncAddPluginDirectory %s " + "failed", + NS_ConvertUTF16toUTF8(dir).get()); + self->mDirectoriesInProgress--; + return GenericPromise::CreateAndReject(aResult, __func__); + }); +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::AddPluginDirectory(const nsAString& aDirectory) { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<GenericPromise> p = AsyncAddPluginDirectory(aDirectory); + Unused << p; + return NS_OK; +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::RemovePluginDirectory( + const nsAString& aDirectory) { + MOZ_ASSERT(NS_IsMainThread()); + return GMPDispatch( + new PathRunnable(this, aDirectory, PathRunnable::EOperation::REMOVE)); +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::RemoveAndDeletePluginDirectory( + const nsAString& aDirectory, const bool aDefer) { + MOZ_ASSERT(NS_IsMainThread()); + return GMPDispatch(new PathRunnable( + this, aDirectory, PathRunnable::EOperation::REMOVE_AND_DELETE_FROM_DISK, + aDefer)); +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::HasPluginForAPI(const nsACString& aAPI, + const nsTArray<nsCString>& aTags, + bool* aHasPlugin) { + NS_ENSURE_ARG(!aTags.IsEmpty()); + NS_ENSURE_ARG(aHasPlugin); + + nsresult rv = EnsurePluginsOnDiskScanned(); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to load GMPs from disk."); + return rv; + } + + { + MutexAutoLock lock(mMutex); + nsCString api(aAPI); + size_t index = 0; + RefPtr<GMPParent> gmp = FindPluginForAPIFrom(index, api, aTags, &index); + *aHasPlugin = !!gmp; + } + + return NS_OK; +} + +nsresult GeckoMediaPluginServiceParent::EnsurePluginsOnDiskScanned() { + const char* env = nullptr; + if (!mScannedPluginOnDisk && (env = PR_GetEnv("MOZ_GMP_PATH")) && *env) { + // We have a MOZ_GMP_PATH environment variable which may specify the + // location of plugins to load, and we haven't yet scanned the disk to + // see if there are plugins there. Get the GMP thread, which will + // cause an event to be dispatched to which scans for plugins. We + // dispatch a sync event to the GMP thread here in order to wait until + // after the GMP thread has scanned any paths in MOZ_GMP_PATH. + nsCOMPtr<nsIThread> thread; + nsresult rv = GetThread(getter_AddRefs(thread)); + NS_ENSURE_SUCCESS(rv, rv); + rv = NS_DispatchAndSpinEventLoopUntilComplete( + "GeckoMediaPluginServiceParent::EnsurePluginsOnDiskScanned"_ns, thread, + MakeAndAddRef<mozilla::Runnable>("GMPDummyRunnable")); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(mScannedPluginOnDisk, "Should have scanned MOZ_GMP_PATH by now"); + } + + return NS_OK; +} + +already_AddRefed<GMPParent> GeckoMediaPluginServiceParent::FindPluginForAPIFrom( + size_t aSearchStartIndex, const nsACString& aAPI, + const nsTArray<nsCString>& aTags, size_t* aOutPluginIndex) { + mMutex.AssertCurrentThreadOwns(); + for (size_t i = aSearchStartIndex; i < mPlugins.Length(); i++) { + RefPtr<GMPParent> gmp = mPlugins[i]; + if (!GMPCapability::Supports(gmp->GetCapabilities(), aAPI, aTags)) { + continue; + } + if (aOutPluginIndex) { + *aOutPluginIndex = i; + } + return gmp.forget(); + } + return nullptr; +} + +already_AddRefed<GMPParent> GeckoMediaPluginServiceParent::SelectPluginForAPI( + const nsACString& aNodeId, const nsACString& aAPI, + const nsTArray<nsCString>& aTags) { + AssertOnGMPThread(); + + GMPParent* gmpToClone = nullptr; + { + MutexAutoLock lock(mMutex); + size_t index = 0; + RefPtr<GMPParent> gmp; + while ((gmp = FindPluginForAPIFrom(index, aAPI, aTags, &index))) { + if (aNodeId.IsEmpty()) { + if (gmp->CanBeSharedCrossNodeIds()) { + return gmp.forget(); + } + } else if (gmp->CanBeUsedFrom(aNodeId)) { + return gmp.forget(); + } + + if (!gmpToClone || + (gmpToClone->IsMarkedForDeletion() && !gmp->IsMarkedForDeletion())) { + // This GMP has the correct type but has the wrong nodeId; hold on to it + // in case we need to clone it. + // Prefer GMPs in-use for the case where an upgraded plugin version is + // waiting for the old one to die. If the old plugin is in use, we + // should continue using it so that any persistent state remains + // consistent. Otherwise, just check that the plugin isn't scheduled + // for deletion. + gmpToClone = gmp; + } + // Loop around and try the next plugin; it may be usable from aNodeId. + index++; + } + } + + // Plugin exists, but we can't use it due to cross-origin separation. Create a + // new one. + if (gmpToClone) { + RefPtr<GMPParent> clone = ClonePlugin(gmpToClone); + { + MutexAutoLock lock(mMutex); + mPlugins.AppendElement(clone); + } + if (!aNodeId.IsEmpty()) { + clone->SetNodeId(aNodeId); + } + return clone.forget(); + } + + return nullptr; +} + +static already_AddRefed<GMPParent> CreateGMPParent() { + // Should run on the GMP thread. +#if defined(XP_LINUX) && defined(MOZ_SANDBOX) + if (!SandboxInfo::Get().CanSandboxMedia()) { + if (!StaticPrefs::media_gmp_insecure_allow()) { + NS_WARNING("Denying media plugin load due to lack of sandboxing."); + return nullptr; + } + NS_WARNING("Loading media plugin despite lack of sandboxing."); + } +#endif + RefPtr<GMPParent> gmpParent = new GMPParent(); + return gmpParent.forget(); +} + +already_AddRefed<GMPParent> GeckoMediaPluginServiceParent::ClonePlugin( + const GMPParent* aOriginal) { + AssertOnGMPThread(); + MOZ_ASSERT(aOriginal); + + RefPtr<GMPParent> gmp = CreateGMPParent(); + if (!gmp) { + NS_WARNING("Can't Create GMPParent"); + return nullptr; + } + + gmp->CloneFrom(aOriginal); + return gmp.forget(); +} + +RefPtr<GenericPromise> GeckoMediaPluginServiceParent::AddOnGMPThread( + nsString aDirectory) { +#ifdef XP_WIN + // On Windows our various test harnesses often pass paths with UNIX dir + // separators, or a mix of dir separators. NS_NewLocalFile() can't handle + // that, so fixup to match the platform's expected format. This makes us + // more robust in the face of bad input and test harnesses changing... + std::replace(aDirectory.BeginWriting(), aDirectory.EndWriting(), '/', '\\'); +#endif + + AssertOnGMPThread(); + nsCString dir = NS_ConvertUTF16toUTF8(aDirectory); + nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread()); + if (!thread) { + GMP_LOG_DEBUG("%s::%s: %s No GMP Thread", __CLASS__, __FUNCTION__, + dir.get()); + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + GMP_LOG_DEBUG("%s::%s: %s", __CLASS__, __FUNCTION__, dir.get()); + + nsCOMPtr<nsIFile> directory; + nsresult rv = NS_NewLocalFile(aDirectory, false, getter_AddRefs(directory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + GMP_LOG_DEBUG("%s::%s: failed to create nsIFile for dir=%s rv=%" PRIx32, + __CLASS__, __FUNCTION__, dir.get(), + static_cast<uint32_t>(rv)); + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + RefPtr<GMPParent> gmp = CreateGMPParent(); + if (!gmp) { + NS_WARNING("Can't Create GMPParent"); + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + RefPtr<GeckoMediaPluginServiceParent> self(this); + return gmp->Init(this, directory) + ->Then( + thread, __func__, + [gmp, self, dir](bool aVal) { + GMP_LOG_DEBUG("%s::%s: %s Succeeded", __CLASS__, __FUNCTION__, + dir.get()); + { + MutexAutoLock lock(self->mMutex); + self->mPlugins.AppendElement(gmp); + } + return GenericPromise::CreateAndResolve(aVal, __func__); + }, + [dir](nsresult aResult) { + GMP_LOG_DEBUG("%s::%s: %s Failed", __CLASS__, __FUNCTION__, + dir.get()); + return GenericPromise::CreateAndReject(aResult, __func__); + }); +} + +void GeckoMediaPluginServiceParent::RemoveOnGMPThread( + const nsAString& aDirectory, const bool aDeleteFromDisk, + const bool aCanDefer) { + AssertOnGMPThread(); + GMP_LOG_DEBUG("%s::%s: %s", __CLASS__, __FUNCTION__, + NS_LossyConvertUTF16toASCII(aDirectory).get()); + + nsCOMPtr<nsIFile> directory; + nsresult rv = NS_NewLocalFile(aDirectory, false, getter_AddRefs(directory)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // Plugin destruction can modify |mPlugins|. Put them aside for now and + // destroy them once we're done with |mPlugins|. + nsTArray<RefPtr<GMPParent>> deadPlugins; + + bool inUse = false; + MutexAutoLock lock(mMutex); + for (size_t i = mPlugins.Length(); i-- > 0;) { + nsCOMPtr<nsIFile> pluginpath = mPlugins[i]->GetDirectory(); + bool equals; + if (NS_FAILED(directory->Equals(pluginpath, &equals)) || !equals) { + continue; + } + + RefPtr<GMPParent> gmp = mPlugins[i]; + if (aDeleteFromDisk && gmp->State() != GMPState::NotLoaded) { + // We have to wait for the child process to release its lib handle + // before we can delete the GMP. + inUse = true; + gmp->MarkForDeletion(); + + if (!mPluginsWaitingForDeletion.Contains(aDirectory)) { + mPluginsWaitingForDeletion.AppendElement(aDirectory); + } + } + + if (gmp->State() == GMPState::NotLoaded || !aCanDefer) { + // GMP not in use or shutdown is being forced; can shut it down now. + deadPlugins.AppendElement(gmp); + mPlugins.RemoveElementAt(i); + } + } + + { + MutexAutoUnlock unlock(mMutex); + for (auto& gmp : deadPlugins) { + gmp->CloseActive(true); + } + } + + if (aDeleteFromDisk && !inUse) { + // Ensure the GMP dir and all files in it are writable, so we have + // permission to delete them. + directory->SetPermissions(0700); + DirectoryEnumerator iter(directory, DirectoryEnumerator::FilesAndDirs); + for (nsCOMPtr<nsIFile> dirEntry; (dirEntry = iter.Next()) != nullptr;) { + dirEntry->SetPermissions(0700); + } + if (NS_SUCCEEDED(directory->Remove(true))) { + mPluginsWaitingForDeletion.RemoveElement(aDirectory); + nsCOMPtr<nsIRunnable> task = new NotifyObserversTask( + "gmp-directory-deleted", nsString(aDirectory)); + mMainThread->Dispatch(task.forget()); + } + } +} + +// May remove when Bug 1043671 is fixed +static void Dummy(RefPtr<GMPParent> aOnDeathsDoor) { + // exists solely to do nothing and let the Runnable kill the GMPParent + // when done. +} + +void GeckoMediaPluginServiceParent::PluginTerminated( + const RefPtr<GMPParent>& aPlugin) { + AssertOnGMPThread(); + + if (aPlugin->IsMarkedForDeletion()) { + nsString path; + RefPtr<nsIFile> dir = aPlugin->GetDirectory(); + nsresult rv = dir->GetPath(path); + NS_ENSURE_SUCCESS_VOID(rv); + if (mPluginsWaitingForDeletion.Contains(path)) { + RemoveOnGMPThread(path, true /* delete */, true /* can defer */); + } + } +} + +void GeckoMediaPluginServiceParent::ReAddOnGMPThread( + const RefPtr<GMPParent>& aOld) { + AssertOnGMPThread(); + GMP_LOG_DEBUG("%s::%s: %p", __CLASS__, __FUNCTION__, (void*)aOld); + + RefPtr<GMPParent> gmp; + if (!mShuttingDownOnGMPThread) { + // We're not shutting down, so replace the old plugin in the list with a + // clone which is in a pristine state. Note: We place the plugin in + // the same slot in the array as a hack to ensure if we re-request with + // the same capabilities we get an instance of the same plugin. + gmp = ClonePlugin(aOld); + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mPlugins.Contains(aOld)); + if (mPlugins.Contains(aOld)) { + mPlugins[mPlugins.IndexOf(aOld)] = gmp; + } + } else { + // We're shutting down; don't re-add plugin, let the old plugin die. + MutexAutoLock lock(mMutex); + mPlugins.RemoveElement(aOld); + } + // Schedule aOld to be destroyed. We can't destroy it from here since we + // may be inside ActorDestroyed() for it. + NS_DispatchToCurrentThread(WrapRunnableNM(&Dummy, aOld)); +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::GetStorageDir(nsIFile** aOutFile) { + if (NS_WARN_IF(!mStorageBaseDir)) { + return NS_ERROR_FAILURE; + } + return mStorageBaseDir->Clone(aOutFile); +} + +nsresult WriteToFile(nsIFile* aPath, const nsACString& aFileName, + const nsACString& aData) { + nsCOMPtr<nsIFile> path; + nsresult rv = aPath->Clone(getter_AddRefs(path)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = path->AppendNative(aFileName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + PRFileDesc* f = nullptr; + rv = path->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, PR_IRWXU, &f); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + int32_t len = PR_Write(f, aData.BeginReading(), aData.Length()); + PR_Close(f); + if (NS_WARN_IF(len < 0 || (size_t)len != aData.Length())) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +static nsresult ReadFromFile(nsIFile* aPath, const nsACString& aFileName, + nsACString& aOutData, int32_t aMaxLength) { + nsCOMPtr<nsIFile> path; + nsresult rv = aPath->Clone(getter_AddRefs(path)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = path->AppendNative(aFileName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + PRFileDesc* f = nullptr; + rv = path->OpenNSPRFileDesc(PR_RDONLY | PR_CREATE_FILE, PR_IRWXU, &f); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + auto size = PR_Seek(f, 0, PR_SEEK_END); + PR_Seek(f, 0, PR_SEEK_SET); + + if (size > aMaxLength) { + return NS_ERROR_FAILURE; + } + aOutData.SetLength(size); + + auto len = PR_Read(f, aOutData.BeginWriting(), size); + PR_Close(f); + if (NS_WARN_IF(len != size)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult ReadSalt(nsIFile* aPath, nsACString& aOutData) { + return ReadFromFile(aPath, "salt"_ns, aOutData, NodeIdSaltLength); +} + +already_AddRefed<GMPStorage> GeckoMediaPluginServiceParent::GetMemoryStorageFor( + const nsACString& aNodeId) { + return do_AddRef(mTempGMPStorage.LookupOrInsertWith( + aNodeId, [] { return CreateGMPMemoryStorage(); })); +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::IsPersistentStorageAllowed( + const nsACString& aNodeId, bool* aOutAllowed) { + AssertOnGMPThread(); + NS_ENSURE_ARG(aOutAllowed); + // We disallow persistent storage for the NodeId used for shared GMP + // decoding, to prevent GMP decoding being used to track what a user + // watches somehow. + *aOutAllowed = !aNodeId.Equals(SHARED_GMP_DECODING_NODE_ID) && + mPersistentStorageAllowed.Get(aNodeId); + return NS_OK; +} + +nsresult GeckoMediaPluginServiceParent::GetNodeId( + const nsAString& aOrigin, const nsAString& aTopLevelOrigin, + const nsAString& aGMPName, nsACString& aOutId) { + AssertOnGMPThread(); + GMP_LOG_DEBUG("%s::%s: (%s, %s)", __CLASS__, __FUNCTION__, + NS_ConvertUTF16toUTF8(aOrigin).get(), + NS_ConvertUTF16toUTF8(aTopLevelOrigin).get()); + + nsresult rv; + + if (aOrigin.EqualsLiteral("null") || aOrigin.IsEmpty() || + aTopLevelOrigin.EqualsLiteral("null") || aTopLevelOrigin.IsEmpty()) { + // (origin, topLevelOrigin) is null or empty; this is for an anonymous + // origin, probably a local file, for which we don't provide persistent + // storage. Generate a random node id, and don't store it so that the GMP's + // storage is temporary and the process for this GMP is not shared with GMP + // instances that have the same nodeId. + nsAutoCString salt; + rv = GenerateRandomPathName(salt, NodeIdSaltLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + aOutId = salt; + mPersistentStorageAllowed.InsertOrUpdate(salt, false); + return NS_OK; + } + + const uint32_t hash = + AddToHash(HashString(aOrigin), HashString(aTopLevelOrigin)); + + if (OriginAttributes::IsPrivateBrowsing(NS_ConvertUTF16toUTF8(aOrigin))) { + // For PB mode, we store the node id, indexed by the origin pair and GMP + // name, so that if the same origin pair is opened for the same GMP in this + // session, it gets the same node id. + const uint32_t pbHash = AddToHash(HashString(aGMPName), hash); + return mTempNodeIds.WithEntryHandle(pbHash, [&](auto&& entry) { + if (!entry) { + // No salt stored, generate and temporarily store some for this id. + nsAutoCString newSalt; + rv = GenerateRandomPathName(newSalt, NodeIdSaltLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + auto salt = MakeUnique<nsCString>(newSalt); + + mPersistentStorageAllowed.InsertOrUpdate(*salt, false); + + entry.Insert(std::move(salt)); + } + + aOutId = *entry.Data(); + return NS_OK; + }); + } + + // Otherwise, try to see if we've previously generated and stored salt + // for this origin pair. + nsCOMPtr<nsIFile> path; // $profileDir/gmp/$platform/ + rv = GetStorageDir(getter_AddRefs(path)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // $profileDir/gmp/$platform/$gmpName/ + rv = path->Append(aGMPName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // $profileDir/gmp/$platform/$gmpName/id/ + rv = path->AppendNative("id"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString hashStr; + hashStr.AppendInt((int64_t)hash); + + // $profileDir/gmp/$platform/$gmpName/id/$hash + rv = path->AppendNative(hashStr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700); + if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIFile> saltFile; + rv = path->Clone(getter_AddRefs(saltFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = saltFile->AppendNative("salt"_ns); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString salt; + bool exists = false; + rv = saltFile->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (!exists) { + // No stored salt for this origin. Generate salt, and store it and + // the origin on disk. + nsresult rv = GenerateRandomPathName(salt, NodeIdSaltLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(salt.Length() == NodeIdSaltLength); + + // $profileDir/gmp/$platform/$gmpName/id/$hash/salt + rv = WriteToFile(path, "salt"_ns, salt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // $profileDir/gmp/$platform/$gmpName/id/$hash/origin + rv = WriteToFile(path, "origin"_ns, NS_ConvertUTF16toUTF8(aOrigin)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // $profileDir/gmp/$platform/$gmpName/id/$hash/topLevelOrigin + rv = WriteToFile(path, "topLevelOrigin"_ns, + NS_ConvertUTF16toUTF8(aTopLevelOrigin)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + } else { + rv = ReadSalt(path, salt); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + aOutId = salt; + mPersistentStorageAllowed.InsertOrUpdate(salt, true); + + return NS_OK; +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::GetNodeId( + const nsAString& aOrigin, const nsAString& aTopLevelOrigin, + const nsAString& aGMPName, UniquePtr<GetNodeIdCallback>&& aCallback) { + nsCString nodeId; + nsresult rv = GetNodeId(aOrigin, aTopLevelOrigin, aGMPName, nodeId); + aCallback->Done(rv, nodeId); + return rv; +} + +nsresult GeckoMediaPluginServiceParent::GetNodeId( + const NodeIdVariant& aNodeIdVariant, nsACString& aOutId) { + if (aNodeIdVariant.type() == NodeIdVariant::TnsCString) { + // The union already contains a node ID as a string, use that. + aOutId = aNodeIdVariant.get_nsCString(); + return NS_OK; + } + // The union contains a NodeIdParts, convert it to a node ID string. + NodeIdParts nodeIdParts{aNodeIdVariant.get_NodeIdParts()}; + return GetNodeId(nodeIdParts.mOrigin(), nodeIdParts.mTopLevelOrigin(), + nodeIdParts.mGMPName(), aOutId); +} + +static bool ExtractHostName(const nsACString& aOrigin, nsACString& aOutData) { + nsCString str; + str.Assign(aOrigin); + int begin = str.Find("://"); + // The scheme is missing! + if (begin == -1) { + return false; + } + + int end = str.RFind(":"); + // Remove the port number + if (end != begin) { + str.SetLength(end); + } + + nsDependentCSubstring host(str, begin + 3); + aOutData.Assign(host); + return true; +} + +constexpr uint32_t kMaxDomainLength = 253; +// http://en.wikipedia.org/wiki/Domain_Name_System#Domain_name_syntax + +constexpr std::array<nsLiteralCString, 2> kFileNames = {"origin"_ns, + "topLevelOrigin"_ns}; + +bool MatchOrigin(nsIFile* aPath, const nsACString& aSite, + const mozilla::OriginAttributesPattern& aPattern) { + nsresult rv; + nsCString str; + nsCString originNoSuffix; + for (const auto& fileName : kFileNames) { + rv = ReadFromFile(aPath, fileName, str, kMaxDomainLength); + mozilla::OriginAttributes originAttributes; + if (NS_FAILED(rv) || + !originAttributes.PopulateFromOrigin(str, originNoSuffix)) { + // Fails on parsing the originAttributes, treat this as a non-match. + return false; + } + + if (ExtractHostName(originNoSuffix, str) && str.Equals(aSite) && + aPattern.Matches(originAttributes)) { + return true; + } + } + return false; +} + +bool MatchBaseDomain(nsIFile* aPath, const nsACString& aBaseDomain) { + nsresult rv; + nsCString fileContent; + nsCString originNoSuffix; + for (const auto& fileName : kFileNames) { + rv = ReadFromFile(aPath, fileName, fileContent, kMaxDomainLength); + mozilla::OriginAttributes originAttributes; + if (NS_FAILED(rv) || + !originAttributes.PopulateFromOrigin(fileContent, originNoSuffix)) { + // Fails on parsing the originAttributes, treat this as a non-match. + return false; + } + nsCString originHostname; + if (!ExtractHostName(originNoSuffix, originHostname)) { + return false; + } + bool success; + rv = net::HasRootDomain(originHostname, aBaseDomain, &success); + if (NS_SUCCEEDED(rv) && success) { + return true; + } + } + return false; +} + +template <typename T> +static void KillPlugins(const nsTArray<RefPtr<GMPParent>>& aPlugins, + Mutex& aMutex, T&& aFilter) { + // Shutdown the plugins when |aFilter| evaluates to true. + // After we clear storage data, node IDs will become invalid and shouldn't be + // used anymore. We need to kill plugins with such nodeIDs. + // Note: we can't shut them down while holding the lock, + // as the lock is not re-entrant and shutdown requires taking the lock. + // The plugin list is only edited on the GMP thread, so this should be OK. + nsTArray<RefPtr<GMPParent>> pluginsToKill; + { + MutexAutoLock lock(aMutex); + for (size_t i = 0; i < aPlugins.Length(); i++) { + RefPtr<GMPParent> parent(aPlugins[i]); + if (aFilter(parent)) { + pluginsToKill.AppendElement(parent); + } + } + } + + for (size_t i = 0; i < pluginsToKill.Length(); i++) { + pluginsToKill[i]->CloseActive(false); + } +} + +struct NodeFilter { + explicit NodeFilter(const nsTArray<nsCString>& nodeIDs) : mNodeIDs(nodeIDs) {} + bool operator()(GMPParent* aParent) { + return mNodeIDs.Contains(aParent->GetNodeId()); + } + + private: + const nsTArray<nsCString>& mNodeIDs; +}; + +void GeckoMediaPluginServiceParent::ClearNodeIdAndPlugin( + DirectoryFilter& aFilter) { + // $profileDir/gmp/$platform/ + nsCOMPtr<nsIFile> path; + nsresult rv = GetStorageDir(getter_AddRefs(path)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // Iterate all sub-folders of $profileDir/gmp/$platform/, i.e. the dirs in + // which specific GMPs store their data. + DirectoryEnumerator iter(path, DirectoryEnumerator::DirsOnly); + for (nsCOMPtr<nsIFile> pluginDir; (pluginDir = iter.Next()) != nullptr;) { + ClearNodeIdAndPlugin(pluginDir, aFilter); + } +} + +void GeckoMediaPluginServiceParent::ClearNodeIdAndPlugin( + nsIFile* aPluginStorageDir, DirectoryFilter& aFilter) { + // $profileDir/gmp/$platform/$gmpName/id/ + nsCOMPtr<nsIFile> path = CloneAndAppend(aPluginStorageDir, u"id"_ns); + if (!path) { + return; + } + + // Iterate all sub-folders of $profileDir/gmp/$platform/$gmpName/id/ + nsTArray<nsCString> nodeIDsToClear; + DirectoryEnumerator iter(path, DirectoryEnumerator::DirsOnly); + for (nsCOMPtr<nsIFile> dirEntry; (dirEntry = iter.Next()) != nullptr;) { + // dirEntry is the hash of origins, i.e.: + // $profileDir/gmp/$platform/$gmpName/id/$originHash/ + if (!aFilter(dirEntry)) { + continue; + } + nsAutoCString salt; + if (NS_SUCCEEDED(ReadSalt(dirEntry, salt))) { + // Keep node IDs to clear data/plugins associated with them later. + nodeIDsToClear.AppendElement(salt); + // Also remove node IDs from the table. + mPersistentStorageAllowed.Remove(salt); + } + // Now we can remove the directory for the origin pair. + if (NS_FAILED(dirEntry->Remove(true))) { + NS_WARNING("Failed to delete the directory for the origin pair"); + } + } + + // Kill plugin instances that have node IDs being cleared. + KillPlugins(mPlugins, mMutex, NodeFilter(nodeIDsToClear)); + + // Clear all storage in $profileDir/gmp/$platform/$gmpName/storage/$nodeId/ + path = CloneAndAppend(aPluginStorageDir, u"storage"_ns); + if (!path) { + return; + } + + for (const nsACString& nodeId : nodeIDsToClear) { + nsCOMPtr<nsIFile> dirEntry; + nsresult rv = path->Clone(getter_AddRefs(dirEntry)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + rv = dirEntry->AppendNative(nodeId); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + if (NS_FAILED(dirEntry->Remove(true))) { + NS_WARNING("Failed to delete GMP storage directory for the node"); + } + } +} + +void GeckoMediaPluginServiceParent::ForgetThisSiteOnGMPThread( + const nsACString& aSite, const mozilla::OriginAttributesPattern& aPattern) { + AssertOnGMPThread(); + GMP_LOG_DEBUG("%s::%s: origin=%s", __CLASS__, __FUNCTION__, aSite.Data()); + + struct OriginFilter : public DirectoryFilter { + explicit OriginFilter(const nsACString& aSite, + const mozilla::OriginAttributesPattern& aPattern) + : mSite(aSite), mPattern(aPattern) {} + bool operator()(nsIFile* aPath) override { + return MatchOrigin(aPath, mSite, mPattern); + } + + private: + const nsACString& mSite; + const mozilla::OriginAttributesPattern& mPattern; + } filter(aSite, aPattern); + + ClearNodeIdAndPlugin(filter); +} + +void GeckoMediaPluginServiceParent::ForgetThisBaseDomainOnGMPThread( + const nsACString& aBaseDomain) { + AssertOnGMPThread(); + GMP_LOG_DEBUG("%s::%s: baseDomain=%s", __CLASS__, __FUNCTION__, + aBaseDomain.Data()); + + struct BaseDomainFilter : public DirectoryFilter { + explicit BaseDomainFilter(const nsACString& aBaseDomain) + : mBaseDomain(aBaseDomain) {} + bool operator()(nsIFile* aPath) override { + return MatchBaseDomain(aPath, mBaseDomain); + } + + private: + const nsACString& mBaseDomain; + } filter(aBaseDomain); + + ClearNodeIdAndPlugin(filter); +} + +void GeckoMediaPluginServiceParent::ClearRecentHistoryOnGMPThread( + PRTime aSince) { + AssertOnGMPThread(); + GMP_LOG_DEBUG("%s::%s: since=%" PRId64, __CLASS__, __FUNCTION__, + (int64_t)aSince); + + struct MTimeFilter : public DirectoryFilter { + explicit MTimeFilter(PRTime aSince) : mSince(aSince) {} + + // Return true if any files under aPath is modified after |mSince|. + bool IsModifiedAfter(nsIFile* aPath) { + PRTime lastModified; + nsresult rv = aPath->GetLastModifiedTime(&lastModified); + if (NS_SUCCEEDED(rv) && lastModified >= mSince) { + return true; + } + DirectoryEnumerator iter(aPath, DirectoryEnumerator::FilesAndDirs); + for (nsCOMPtr<nsIFile> dirEntry; (dirEntry = iter.Next()) != nullptr;) { + if (IsModifiedAfter(dirEntry)) { + return true; + } + } + return false; + } + + // |aPath| is $profileDir/gmp/$platform/$gmpName/id/$originHash/ + bool operator()(nsIFile* aPath) override { + if (IsModifiedAfter(aPath)) { + return true; + } + + nsAutoCString salt; + if (NS_WARN_IF(NS_FAILED(ReadSalt(aPath, salt)))) { + return false; + } + + // $profileDir/gmp/$platform/$gmpName/id/ + nsCOMPtr<nsIFile> idDir; + if (NS_WARN_IF(NS_FAILED(aPath->GetParent(getter_AddRefs(idDir))))) { + return false; + } + // $profileDir/gmp/$platform/$gmpName/ + nsCOMPtr<nsIFile> temp; + if (NS_WARN_IF(NS_FAILED(idDir->GetParent(getter_AddRefs(temp))))) { + return false; + } + + // $profileDir/gmp/$platform/$gmpName/storage/ + if (NS_WARN_IF(NS_FAILED(temp->Append(u"storage"_ns)))) { + return false; + } + // $profileDir/gmp/$platform/$gmpName/storage/$originSalt + return NS_SUCCEEDED(temp->AppendNative(salt)) && IsModifiedAfter(temp); + } + + private: + const PRTime mSince; + } filter(aSince); + + ClearNodeIdAndPlugin(filter); + + nsCOMPtr<nsIRunnable> task = + new NotifyObserversTask("gmp-clear-storage-complete"); + mMainThread->Dispatch(task.forget()); +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::ForgetThisSite(const nsAString& aSite, + const nsAString& aPattern) { + MOZ_ASSERT(NS_IsMainThread()); + + mozilla::OriginAttributesPattern pattern; + + if (!pattern.Init(aPattern)) { + return NS_ERROR_INVALID_ARG; + } + + return ForgetThisSiteNative(aSite, pattern); +} + +nsresult GeckoMediaPluginServiceParent::ForgetThisSiteNative( + const nsAString& aSite, const mozilla::OriginAttributesPattern& aPattern) { + MOZ_ASSERT(NS_IsMainThread()); + + return GMPDispatch( + NewRunnableMethod<nsCString, mozilla::OriginAttributesPattern>( + "gmp::GeckoMediaPluginServiceParent::ForgetThisSiteOnGMPThread", this, + &GeckoMediaPluginServiceParent::ForgetThisSiteOnGMPThread, + NS_ConvertUTF16toUTF8(aSite), aPattern)); +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::ForgetThisBaseDomain( + const nsAString& aBaseDomain) { + MOZ_ASSERT(NS_IsMainThread()); + + return ForgetThisBaseDomainNative(aBaseDomain); +} + +nsresult GeckoMediaPluginServiceParent::ForgetThisBaseDomainNative( + const nsAString& aBaseDomain) { + MOZ_ASSERT(NS_IsMainThread()); + + return GMPDispatch(NewRunnableMethod<nsCString>( + "gmp::GeckoMediaPluginServiceParent::ForgetThisBaseDomainOnGMPThread", + this, &GeckoMediaPluginServiceParent::ForgetThisBaseDomainOnGMPThread, + NS_ConvertUTF16toUTF8(aBaseDomain))); +} + +static bool IsNodeIdValid(GMPParent* aParent) { + return !aParent->GetNodeId().IsEmpty(); +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::GetName(nsAString& aName) { + aName = u"GeckoMediaPluginServiceParent: shutdown"_ns; + return NS_OK; +} + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::GetState(nsIPropertyBag**) { return NS_OK; } + +NS_IMETHODIMP +GeckoMediaPluginServiceParent::BlockShutdown(nsIAsyncShutdownClient*) { + return NS_OK; +} + +// Called from GMPServiceParent::Create() which holds the lock +void GeckoMediaPluginServiceParent::ServiceUserCreated( + GMPServiceParent* aServiceParent) { + MOZ_ASSERT(NS_IsMainThread()); + mMutex.AssertCurrentThreadOwns(); + + MOZ_ASSERT(!mServiceParents.Contains(aServiceParent)); + mServiceParents.AppendElement(aServiceParent); + if (mServiceParents.Length() == 1) { + nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier(); + MOZ_RELEASE_ASSERT(barrier); + + nsresult rv = barrier->AddBlocker( + this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, + u"GeckoMediaPluginServiceParent shutdown"_ns); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + } +} + +void GeckoMediaPluginServiceParent::ServiceUserDestroyed( + GMPServiceParent* aServiceParent) { + MOZ_ASSERT(NS_IsMainThread()); + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mServiceParents.Length() > 0); + MOZ_ASSERT(mServiceParents.Contains(aServiceParent)); + mServiceParents.RemoveElement(aServiceParent); + if (mServiceParents.IsEmpty()) { + nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier(); + MOZ_RELEASE_ASSERT(barrier); + nsresult rv = barrier->RemoveBlocker(this); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + } +} + +void GeckoMediaPluginServiceParent::ClearStorage() { + AssertOnGMPThread(); + GMP_LOG_DEBUG("%s::%s", __CLASS__, __FUNCTION__); + + // Kill plugins with valid nodeIDs. + KillPlugins(mPlugins, mMutex, &IsNodeIdValid); + + nsCOMPtr<nsIFile> path; // $profileDir/gmp/$platform/ + nsresult rv = GetStorageDir(getter_AddRefs(path)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (NS_FAILED(path->Remove(true))) { + NS_WARNING("Failed to delete GMP storage directory"); + } + + // Clear private-browsing storage. + mTempGMPStorage.Clear(); + + nsCOMPtr<nsIRunnable> task = + new NotifyObserversTask("gmp-clear-storage-complete"); + mMainThread->Dispatch(task.forget()); +} + +already_AddRefed<GMPParent> GeckoMediaPluginServiceParent::GetById( + uint32_t aPluginId) { + MutexAutoLock lock(mMutex); + for (const RefPtr<GMPParent>& gmp : mPlugins) { + if (gmp->GetPluginId() == aPluginId) { + return do_AddRef(gmp); + } + } + return nullptr; +} + +GMPServiceParent::GMPServiceParent(GeckoMediaPluginServiceParent* aService) + : mService(aService) { + MOZ_ASSERT(NS_IsMainThread(), "Should be constructed on the main thread"); + MOZ_ASSERT(mService); + mService->ServiceUserCreated(this); +} + +GMPServiceParent::~GMPServiceParent() { + MOZ_ASSERT(NS_IsMainThread(), "Should be destroyted on the main thread"); + MOZ_ASSERT(mService); + mService->ServiceUserDestroyed(this); +} + +mozilla::ipc::IPCResult GMPServiceParent::RecvLaunchGMP( + const NodeIdVariant& aNodeIdVariant, const nsACString& aAPI, + nsTArray<nsCString>&& aTags, nsTArray<ProcessId>&& aAlreadyBridgedTo, + uint32_t* aOutPluginId, GMPPluginType* aOutPluginType, + ProcessId* aOutProcessId, nsCString* aOutDisplayName, + Endpoint<PGMPContentParent>* aOutEndpoint, nsresult* aOutRv, + nsCString* aOutErrorDescription) { + if (mService->IsShuttingDown()) { + *aOutRv = NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + *aOutErrorDescription = "Service is shutting down."_ns; + *aOutPluginId = 0; + *aOutPluginType = GMPPluginType::Unknown; + return IPC_OK(); + } + + nsCString nodeIdString; + nsresult rv = mService->GetNodeId(aNodeIdVariant, nodeIdString); + if (!NS_SUCCEEDED(rv)) { + *aOutRv = rv; + *aOutErrorDescription = "GetNodeId failed."_ns; + *aOutPluginId = 0; + *aOutPluginType = GMPPluginType::Unknown; + return IPC_OK(); + } + + RefPtr<GMPParent> gmp = + mService->SelectPluginForAPI(nodeIdString, aAPI, aTags); + if (gmp) { + *aOutPluginId = gmp->GetPluginId(); + *aOutPluginType = gmp->GetPluginType(); + } else { + *aOutRv = NS_ERROR_FAILURE; + *aOutErrorDescription = "SelectPluginForAPI returns nullptr."_ns; + *aOutPluginId = 0; + *aOutPluginType = GMPPluginType::Unknown; + return IPC_OK(); + } + + if (!gmp->EnsureProcessLoaded(aOutProcessId)) { + *aOutRv = NS_ERROR_FAILURE; + *aOutErrorDescription = "Process has not loaded."_ns; + return IPC_OK(); + } + + *aOutDisplayName = gmp->GetDisplayName(); + + if (aAlreadyBridgedTo.Contains(*aOutProcessId)) { + *aOutRv = NS_OK; + return IPC_OK(); + } + + Endpoint<PGMPContentParent> parent; + Endpoint<PGMPContentChild> child; + rv = + PGMPContent::CreateEndpoints(OtherPid(), *aOutProcessId, &parent, &child); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aOutRv = rv; + *aOutErrorDescription = "PGMPContent::CreateEndpoints failed."_ns; + return IPC_OK(); + } + + *aOutEndpoint = std::move(parent); + + if (!gmp->SendInitGMPContentChild(std::move(child))) { + *aOutRv = NS_ERROR_FAILURE; + *aOutErrorDescription = "SendInitGMPContentChild failed."_ns; + return IPC_OK(); + } + + gmp->IncrementGMPContentChildCount(); + + *aOutRv = NS_OK; + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPServiceParent::RecvGetGMPNodeId( + const nsAString& aOrigin, const nsAString& aTopLevelOrigin, + const nsAString& aGMPName, nsCString* aID) { + nsresult rv = mService->GetNodeId(aOrigin, aTopLevelOrigin, aGMPName, *aID); + if (!NS_SUCCEEDED(rv)) { + return IPC_FAIL( + this, + "GMPServiceParent::RecvGetGMPNodeId: mService->GetNodeId failed."); + } + return IPC_OK(); +} + +class OpenPGMPServiceParent : public mozilla::Runnable { + public: + OpenPGMPServiceParent(RefPtr<GMPServiceParent>&& aGMPServiceParent, + ipc::Endpoint<PGMPServiceParent>&& aEndpoint, + bool* aResult) + : Runnable("gmp::OpenPGMPServiceParent"), + mGMPServiceParent(aGMPServiceParent), + mEndpoint(std::move(aEndpoint)), + mResult(aResult) {} + + NS_IMETHOD Run() override { + *mResult = mEndpoint.Bind(mGMPServiceParent); + return NS_OK; + } + + private: + RefPtr<GMPServiceParent> mGMPServiceParent; + ipc::Endpoint<PGMPServiceParent> mEndpoint; + bool* mResult; +}; + +/* static */ +bool GMPServiceParent::Create(Endpoint<PGMPServiceParent>&& aGMPService) { + RefPtr<GeckoMediaPluginServiceParent> gmp = + GeckoMediaPluginServiceParent::GetSingleton(); + + if (!gmp || gmp->mShuttingDown) { + // Shutdown is initiated. There is no point creating a new actor. + return false; + } + + nsCOMPtr<nsIThread> gmpThread; + RefPtr<GMPServiceParent> serviceParent; + { + MutexAutoLock lock(gmp->mMutex); + nsresult rv = gmp->GetThreadLocked(getter_AddRefs(gmpThread)); + NS_ENSURE_SUCCESS(rv, false); + serviceParent = new GMPServiceParent(gmp); + } + bool ok; + nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete( + "GMPServiceParent::Create"_ns, gmpThread, + do_AddRef(new OpenPGMPServiceParent(std::move(serviceParent), + std::move(aGMPService), &ok))); + + if (NS_WARN_IF(NS_FAILED(rv) || !ok)) { + return false; + } + + // Now that the service parent is set up, it will be released by IPC + // refcounting, so we don't need to hold any more references here. + + return true; +} + +} // namespace mozilla::gmp + +#undef __CLASS__ diff --git a/dom/media/gmp/GMPServiceParent.h b/dom/media/gmp/GMPServiceParent.h new file mode 100644 index 0000000000..0a897d6a39 --- /dev/null +++ b/dom/media/gmp/GMPServiceParent.h @@ -0,0 +1,281 @@ +/* -*- 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/. */ + +#ifndef GMPServiceParent_h_ +#define GMPServiceParent_h_ + +#include "GMPService.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/gmp/PGMPServiceParent.h" +#include "mozIGeckoMediaPluginChromeService.h" +#include "nsClassHashtable.h" +#include "nsTHashMap.h" +#include "mozilla/Atomics.h" +#include "nsNetUtil.h" +#include "nsIAsyncShutdown.h" +#include "nsRefPtrHashtable.h" +#include "nsThreadUtils.h" +#include "mozilla/gmp/PGMPParent.h" +#include "mozilla/MozPromise.h" +#include "GMPStorage.h" + +template <class> +struct already_AddRefed; +using FlushFOGDataPromise = mozilla::dom::ContentParent::FlushFOGDataPromise; +using ContentParent = mozilla::dom::ContentParent; + +namespace mozilla { +class OriginAttributesPattern; + +namespace gmp { + +class GMPParent; +class GMPServiceParent; + +class GeckoMediaPluginServiceParent final + : public GeckoMediaPluginService, + public mozIGeckoMediaPluginChromeService, + public nsIAsyncShutdownBlocker { + public: + static already_AddRefed<GeckoMediaPluginServiceParent> GetSingleton(); + + GeckoMediaPluginServiceParent(); + nsresult Init() override; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIASYNCSHUTDOWNBLOCKER + + // mozIGeckoMediaPluginService + NS_IMETHOD HasPluginForAPI(const nsACString& aAPI, + const nsTArray<nsCString>& aTags, + bool* aRetVal) override; + NS_IMETHOD GetNodeId(const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aGMPName, + UniquePtr<GetNodeIdCallback>&& aCallback) override; + + NS_DECL_MOZIGECKOMEDIAPLUGINCHROMESERVICE + NS_DECL_NSIOBSERVER + + RefPtr<GenericPromise> EnsureInitialized(); + RefPtr<GenericPromise> AsyncAddPluginDirectory(const nsAString& aDirectory); + + // GMP thread access only + bool IsShuttingDown(); + + already_AddRefed<GMPStorage> GetMemoryStorageFor(const nsACString& aNodeId); + nsresult ForgetThisSiteNative( + const nsAString& aSite, const mozilla::OriginAttributesPattern& aPattern); + + nsresult ForgetThisBaseDomainNative(const nsAString& aBaseDomain); + + // Notifies that some user of this class is created/destroyed. + void ServiceUserCreated(GMPServiceParent* aServiceParent); + void ServiceUserDestroyed(GMPServiceParent* aServiceParent); + + // If aContentProcess is specified, this will only update GMP caps in that + // content process, otherwise will update all content processes. + void UpdateContentProcessGMPCapabilities( + ContentParent* aContentProcess = nullptr); + + void SendFlushFOGData(nsTArray<RefPtr<FlushFOGDataPromise>>& promises); + + /* + * ** Test-only Method ** + * + * Trigger GMP-process test metric instrumentation. + */ + RefPtr<PGMPParent::TestTriggerMetricsPromise> TestTriggerMetrics(); + + private: + friend class GMPServiceParent; + + virtual ~GeckoMediaPluginServiceParent(); + + void ClearStorage(); + + already_AddRefed<GMPParent> SelectPluginForAPI( + const nsACString& aNodeId, const nsACString& aAPI, + const nsTArray<nsCString>& aTags); + + already_AddRefed<GMPParent> FindPluginForAPIFrom( + size_t aSearchStartIndex, const nsACString& aAPI, + const nsTArray<nsCString>& aTags, size_t* aOutPluginIndex); + + nsresult GetNodeId(const nsAString& aOrigin, const nsAString& aTopLevelOrigin, + const nsAString& aGMPName, nsACString& aOutId); + + void UnloadPlugins(); + void CrashPlugins(); + void NotifySyncShutdownComplete(); + + void RemoveOnGMPThread(const nsAString& aDirectory, + const bool aDeleteFromDisk, const bool aCanDefer); + + struct DirectoryFilter { + virtual bool operator()(nsIFile* aPath) = 0; + ~DirectoryFilter() = default; + }; + void ClearNodeIdAndPlugin(DirectoryFilter& aFilter); + void ClearNodeIdAndPlugin(nsIFile* aPluginStorageDir, + DirectoryFilter& aFilter); + void ForgetThisSiteOnGMPThread( + const nsACString& aSite, + const mozilla::OriginAttributesPattern& aPattern); + void ForgetThisBaseDomainOnGMPThread(const nsACString& aBaseDomain); + void ClearRecentHistoryOnGMPThread(PRTime aSince); + + already_AddRefed<GMPParent> GetById(uint32_t aPluginId); + + protected: + friend class GMPParent; + void ReAddOnGMPThread(const RefPtr<GMPParent>& aOld); + void PluginTerminated(const RefPtr<GMPParent>& aOld); + void InitializePlugins(nsISerialEventTarget* GMPThread) override; + RefPtr<GenericPromise> LoadFromEnvironment(); + RefPtr<GenericPromise> AddOnGMPThread(nsString aDirectory); + + RefPtr<GetGMPContentParentPromise> GetContentParent( + GMPCrashHelper* aHelper, const NodeIdVariant& aNodeIdVariant, + const nsACString& aAPI, const nsTArray<nsCString>& aTags) override; + + private: + // Creates a copy of aOriginal. Note that the caller is responsible for + // adding this to GeckoMediaPluginServiceParent::mPlugins. + already_AddRefed<GMPParent> ClonePlugin(const GMPParent* aOriginal); + nsresult EnsurePluginsOnDiskScanned(); + nsresult InitStorage(); + + // Get a string based node ID from a NodeIdVariant. This will + // either fetch the internal string, or convert the internal NodeIdParts to a + // string. The conversion process is fallible, so the return value should be + // checked. + nsresult GetNodeId(const NodeIdVariant& aNodeIdVariant, nsACString& aOutId); + + class PathRunnable : public Runnable { + public: + enum EOperation { + REMOVE, + REMOVE_AND_DELETE_FROM_DISK, + }; + + PathRunnable(GeckoMediaPluginServiceParent* aService, + const nsAString& aPath, EOperation aOperation, + bool aDefer = false) + : Runnable("gmp::GeckoMediaPluginServiceParent::PathRunnable"), + mService(aService), + mPath(aPath), + mOperation(aOperation), + mDefer(aDefer) {} + + NS_DECL_NSIRUNNABLE + + private: + RefPtr<GeckoMediaPluginServiceParent> mService; + nsString mPath; + EOperation mOperation; + bool mDefer; + }; + + // Protected by mMutex from the base class. + nsTArray<RefPtr<GMPParent>> mPlugins; + + // True if we've inspected MOZ_GMP_PATH on the GMP thread and loaded any + // plugins found there into mPlugins. + Atomic<bool> mScannedPluginOnDisk; + + template <typename T> + class MainThreadOnly { + public: + MOZ_IMPLICIT MainThreadOnly(T aValue) : mValue(aValue) {} + operator T&() { + MOZ_ASSERT(NS_IsMainThread()); + return mValue; + } + + private: + T mValue; + }; + + MainThreadOnly<bool> mShuttingDown; + MainThreadOnly<bool> mWaitingForPluginsSyncShutdown; + + nsTArray<nsString> mPluginsWaitingForDeletion; + + nsCOMPtr<nsIFile> mStorageBaseDir; + + // Hashes of (origin,topLevelOrigin) to the node id for + // non-persistent sessions. + nsClassHashtable<nsUint32HashKey, nsCString> mTempNodeIds; + + // Hashes node id to whether that node id is allowed to store data + // persistently on disk. + nsTHashMap<nsCStringHashKey, bool> mPersistentStorageAllowed; + + // Synchronization for barrier that ensures we've loaded GMPs from + // MOZ_GMP_PATH before allowing GetContentParentFrom() to proceed. + Monitor mInitPromiseMonitor MOZ_UNANNOTATED; + MozMonitoredPromiseHolder<GenericPromise> mInitPromise; + bool mLoadPluginsFromDiskComplete; + + // Hashes nodeId to the hashtable of storage for that nodeId. + nsRefPtrHashtable<nsCStringHashKey, GMPStorage> mTempGMPStorage; + + // Tracks how many IPC connections to GMPServices running in content + // processes we have. When this is empty we can safely shut down. + // Synchronized across thread via mMutex in base class. + nsTArray<GMPServiceParent*> mServiceParents; + + uint32_t mDirectoriesAdded = 0; + uint32_t mDirectoriesInProgress = 0; +}; + +nsresult WriteToFile(nsIFile* aPath, const nsACString& aFileName, + const nsACString& aData); +nsresult ReadSalt(nsIFile* aPath, nsACString& aOutData); +bool MatchOrigin(nsIFile* aPath, const nsACString& aSite, + const mozilla::OriginAttributesPattern& aPattern); +bool MatchBaseDomain(nsIFile* aPath, const nsACString& aBaseDomain); + +class GMPServiceParent final : public PGMPServiceParent { + public: + explicit GMPServiceParent(GeckoMediaPluginServiceParent* aService); + + // Our refcounting is thread safe, and when our refcount drops to zero + // we dispatch an event to the main thread to delete the GMPServiceParent. + // Note that this means it's safe for references to this object to be + // released on a non main thread, but the destructor will always run on + // the main thread. + + // Mark AddRef and Release as `final`, as they overload pure virtual + // implementations in PGMPServiceParent. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_MAIN_THREAD( + GMPServiceParent, final); + + ipc::IPCResult RecvGetGMPNodeId(const nsAString& aOrigin, + const nsAString& aTopLevelOrigin, + const nsAString& aGMPName, + nsCString* aID) override; + + static bool Create(Endpoint<PGMPServiceParent>&& aGMPService); + + ipc::IPCResult RecvLaunchGMP( + const NodeIdVariant& aNodeIdVariant, const nsACString& aAPI, + nsTArray<nsCString>&& aTags, nsTArray<ProcessId>&& aAlreadyBridgedTo, + uint32_t* aOutPluginId, GMPPluginType* aOutPluginType, + ProcessId* aOutProcessId, nsCString* aOutDisplayName, + Endpoint<PGMPContentParent>* aOutEndpoint, nsresult* aOutRv, + nsCString* aOutErrorDescription) override; + + private: + ~GMPServiceParent(); + + RefPtr<GeckoMediaPluginServiceParent> mService; +}; + +} // namespace gmp +} // namespace mozilla + +#endif // GMPServiceParent_h_ diff --git a/dom/media/gmp/GMPSharedMemManager.cpp b/dom/media/gmp/GMPSharedMemManager.cpp new file mode 100644 index 0000000000..5da04247cb --- /dev/null +++ b/dom/media/gmp/GMPSharedMemManager.cpp @@ -0,0 +1,89 @@ +/* -*- 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 "GMPSharedMemManager.h" +#include "GMPMessageUtils.h" +#include "mozilla/ipc/SharedMemory.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/ClearOnShutdown.h" + +namespace mozilla::gmp { + +// Really one set of pools on each side of the plugin API. + +// YUV buffers go from Encoder parent to child; pool there, and then return +// with Decoded() frames to the Decoder parent and goes into the parent pool. +// Compressed (encoded) data goes from the Decoder parent to the child; +// pool there, and then return with Encoded() frames and goes into the parent +// pool. +bool GMPSharedMemManager::MgrAllocShmem(GMPSharedMem::GMPMemoryClasses aClass, + size_t aSize, ipc::Shmem* aMem) { + mData->CheckThread(); + + // first look to see if we have a free buffer large enough + for (uint32_t i = 0; i < GetGmpFreelist(aClass).Length(); i++) { + MOZ_ASSERT(GetGmpFreelist(aClass)[i].IsWritable()); + if (aSize <= GetGmpFreelist(aClass)[i].Size<uint8_t>()) { + *aMem = GetGmpFreelist(aClass)[i]; + GetGmpFreelist(aClass).RemoveElementAt(i); + return true; + } + } + + // Didn't find a buffer free with enough space; allocate one + size_t pagesize = ipc::SharedMemory::SystemPageSize(); + aSize = (aSize + (pagesize - 1)) & ~(pagesize - 1); // round up to page size + bool retval = Alloc(aSize, aMem); + if (retval) { + // The allocator (or NeedsShmem call) should never return less than we ask + // for... + MOZ_ASSERT(aMem->Size<uint8_t>() >= aSize); + mData->mGmpAllocated[aClass]++; + } + return retval; +} + +bool GMPSharedMemManager::MgrDeallocShmem(GMPSharedMem::GMPMemoryClasses aClass, + ipc::Shmem& aMem) { + mData->CheckThread(); + + size_t size = aMem.Size<uint8_t>(); + + // XXX Bug NNNNNNN Until we put better guards on ipc::shmem, verify we + // weren't fed an shmem we already had. + for (uint32_t i = 0; i < GetGmpFreelist(aClass).Length(); i++) { + if (NS_WARN_IF(aMem == GetGmpFreelist(aClass)[i])) { + // Safest to crash in this case; should never happen in normal + // operation. + MOZ_CRASH("Deallocating Shmem we already have in our cache!"); + // return true; + } + } + + // XXX This works; there are better pool algorithms. We need to avoid + // "falling off a cliff" with too low a number + if (GetGmpFreelist(aClass).Length() > 10) { + Dealloc(std::move(GetGmpFreelist(aClass)[0])); + GetGmpFreelist(aClass).RemoveElementAt(0); + // The allocation numbers will be fubar on the Child! + mData->mGmpAllocated[aClass]--; + } + for (uint32_t i = 0; i < GetGmpFreelist(aClass).Length(); i++) { + MOZ_ASSERT(GetGmpFreelist(aClass)[i].IsWritable()); + if (size < GetGmpFreelist(aClass)[i].Size<uint8_t>()) { + GetGmpFreelist(aClass).InsertElementAt(i, aMem); + return true; + } + } + GetGmpFreelist(aClass).AppendElement(aMem); + + return true; +} + +uint32_t GMPSharedMemManager::NumInUse(GMPSharedMem::GMPMemoryClasses aClass) { + return mData->mGmpAllocated[aClass] - GetGmpFreelist(aClass).Length(); +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPSharedMemManager.h b/dom/media/gmp/GMPSharedMemManager.h new file mode 100644 index 0000000000..1a04bdde14 --- /dev/null +++ b/dom/media/gmp/GMPSharedMemManager.h @@ -0,0 +1,78 @@ +/* -*- 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/. */ + +#ifndef GMPSharedMemManager_h_ +#define GMPSharedMemManager_h_ + +#include "mozilla/ipc/Shmem.h" +#include "nsTArray.h" + +namespace mozilla::gmp { + +class GMPSharedMemManager; + +class GMPSharedMem { + public: + typedef enum { + kGMPFrameData = 0, + kGMPEncodedData, + kGMPNumTypes + } GMPMemoryClasses; + + // This is a heuristic - max of 10 free in the Child pool, plus those + // in-use for the encoder and decoder at the given moment and not yet + // returned to the parent pool (which is not included). If more than + // this are needed, we presume the client has either crashed or hung + // (perhaps temporarily). + static const uint32_t kGMPBufLimit = 40; + + GMPSharedMem() { + for (size_t i = 0; i < sizeof(mGmpAllocated) / sizeof(mGmpAllocated[0]); + i++) { + mGmpAllocated[i] = 0; + } + } + virtual ~GMPSharedMem() = default; + + // Parent and child impls will differ here + virtual void CheckThread() = 0; + + protected: + friend class GMPSharedMemManager; + + nsTArray<ipc::Shmem> mGmpFreelist[GMPSharedMem::kGMPNumTypes]; + uint32_t mGmpAllocated[GMPSharedMem::kGMPNumTypes]; +}; + +class GMPSharedMemManager { + public: + explicit GMPSharedMemManager(GMPSharedMem* aData) : mData(aData) {} + virtual ~GMPSharedMemManager() = default; + + virtual bool MgrAllocShmem(GMPSharedMem::GMPMemoryClasses aClass, + size_t aSize, ipc::Shmem* aMem); + virtual bool MgrDeallocShmem(GMPSharedMem::GMPMemoryClasses aClass, + ipc::Shmem& aMem); + + // So we can know if data is "piling up" for the plugin - I.e. it's hung or + // crashed + virtual uint32_t NumInUse(GMPSharedMem::GMPMemoryClasses aClass); + + // These have to be implemented using the AllocShmem/etc provided by the + // IPDL-generated interfaces, so have the Parent/Child implement them. + virtual bool Alloc(size_t aSize, ipc::Shmem* aMem) = 0; + virtual void Dealloc(ipc::Shmem&& aMem) = 0; + + private: + nsTArray<ipc::Shmem>& GetGmpFreelist(GMPSharedMem::GMPMemoryClasses aTypes) { + return mData->mGmpFreelist[aTypes]; + } + + GMPSharedMem* mData; +}; + +} // namespace mozilla::gmp + +#endif // GMPSharedMemManager_h_ diff --git a/dom/media/gmp/GMPStorage.h b/dom/media/gmp/GMPStorage.h new file mode 100644 index 0000000000..aca8ca3f26 --- /dev/null +++ b/dom/media/gmp/GMPStorage.h @@ -0,0 +1,39 @@ +/* -*- 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/. */ + +#ifndef GMPStorage_h_ +#define GMPStorage_h_ + +#include "gmp-storage.h" +#include "mozilla/AlreadyAddRefed.h" +#include "nsISupportsImpl.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla::gmp { + +class GMPStorage { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPStorage) + + virtual GMPErr Open(const nsACString& aRecordName) = 0; + virtual bool IsOpen(const nsACString& aRecordName) const = 0; + virtual GMPErr Read(const nsACString& aRecordName, + nsTArray<uint8_t>& aOutBytes) = 0; + virtual GMPErr Write(const nsACString& aRecordName, + const nsTArray<uint8_t>& aBytes) = 0; + virtual void Close(const nsACString& aRecordName) = 0; + + protected: + virtual ~GMPStorage() = default; +}; + +already_AddRefed<GMPStorage> CreateGMPMemoryStorage(); +already_AddRefed<GMPStorage> CreateGMPDiskStorage(const nsACString& aNodeId, + const nsAString& aGMPName); + +} // namespace mozilla::gmp + +#endif diff --git a/dom/media/gmp/GMPStorageChild.cpp b/dom/media/gmp/GMPStorageChild.cpp new file mode 100644 index 0000000000..b08356e0d6 --- /dev/null +++ b/dom/media/gmp/GMPStorageChild.cpp @@ -0,0 +1,244 @@ +/* -*- 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 "GMPStorageChild.h" +#include "GMPChild.h" +#include "gmp-storage.h" +#include "base/task.h" + +#define ON_GMP_THREAD() (mPlugin->GMPMessageLoop() == MessageLoop::current()) + +#define CALL_ON_GMP_THREAD(_func, ...) \ + do { \ + if (ON_GMP_THREAD()) { \ + _func(__VA_ARGS__); \ + } else { \ + mPlugin->GMPMessageLoop()->PostTask( \ + dont_add_new_uses_of_this::NewRunnableMethod( \ + this, &GMPStorageChild::_func, ##__VA_ARGS__)); \ + } \ + } while (false) + +namespace mozilla::gmp { + +static nsTArray<uint8_t> ToArray(const uint8_t* aData, uint32_t aDataSize) { + nsTArray<uint8_t> data; + data.AppendElements(aData, aDataSize); + return data; +} + +GMPRecordImpl::GMPRecordImpl(GMPStorageChild* aOwner, const nsCString& aName, + GMPRecordClient* aClient) + : mName(aName), mClient(aClient), mOwner(aOwner) {} + +GMPErr GMPRecordImpl::Open() { return mOwner->Open(this); } + +void GMPRecordImpl::OpenComplete(GMPErr aStatus) { + mClient->OpenComplete(aStatus); +} + +GMPErr GMPRecordImpl::Read() { return mOwner->Read(this); } + +void GMPRecordImpl::ReadComplete(GMPErr aStatus, const uint8_t* aBytes, + uint32_t aLength) { + mClient->ReadComplete(aStatus, aBytes, aLength); +} + +GMPErr GMPRecordImpl::Write(const uint8_t* aData, uint32_t aDataSize) { + return mOwner->Write(this, aData, aDataSize); +} + +void GMPRecordImpl::WriteComplete(GMPErr aStatus) { + mClient->WriteComplete(aStatus); +} + +GMPErr GMPRecordImpl::Close() { + RefPtr<GMPRecordImpl> kungfuDeathGrip(this); + // Delete our self reference. + Release(); + mOwner->Close(this->Name()); + return GMPNoErr; +} + +GMPStorageChild::GMPStorageChild(GMPChild* aPlugin) + : mMonitor("GMPStorageChild"), mPlugin(aPlugin), mShutdown(false) { + MOZ_ASSERT(ON_GMP_THREAD()); +} + +GMPErr GMPStorageChild::CreateRecord(const nsCString& aRecordName, + GMPRecord** aOutRecord, + GMPRecordClient* aClient) { + MonitorAutoLock lock(mMonitor); + + if (mShutdown) { + NS_WARNING("GMPStorage used after it's been shutdown!"); + return GMPClosedErr; + } + + MOZ_ASSERT(aRecordName.Length() && aOutRecord); + + if (HasRecord(aRecordName)) { + return GMPRecordInUse; + } + + RefPtr<GMPRecordImpl> record(new GMPRecordImpl(this, aRecordName, aClient)); + mRecords.InsertOrUpdate(aRecordName, RefPtr{record}); // Addrefs + + // The GMPRecord holds a self reference until the GMP calls Close() on + // it. This means the object is always valid (even if neutered) while + // the GMP expects it to be. + record.forget(aOutRecord); + + return GMPNoErr; +} + +bool GMPStorageChild::HasRecord(const nsCString& aRecordName) { + mMonitor.AssertCurrentThreadOwns(); + return mRecords.Contains(aRecordName); +} + +already_AddRefed<GMPRecordImpl> GMPStorageChild::GetRecord( + const nsCString& aRecordName) { + MonitorAutoLock lock(mMonitor); + RefPtr<GMPRecordImpl> record; + mRecords.Get(aRecordName, getter_AddRefs(record)); + return record.forget(); +} + +GMPErr GMPStorageChild::Open(GMPRecordImpl* aRecord) { + MonitorAutoLock lock(mMonitor); + + if (mShutdown) { + NS_WARNING("GMPStorage used after it's been shutdown!"); + return GMPClosedErr; + } + + if (!HasRecord(aRecord->Name())) { + // Trying to re-open a record that has already been closed. + return GMPClosedErr; + } + + CALL_ON_GMP_THREAD(SendOpen, aRecord->Name()); + + return GMPNoErr; +} + +GMPErr GMPStorageChild::Read(GMPRecordImpl* aRecord) { + MonitorAutoLock lock(mMonitor); + + if (mShutdown) { + NS_WARNING("GMPStorage used after it's been shutdown!"); + return GMPClosedErr; + } + + if (!HasRecord(aRecord->Name())) { + // Record not opened. + return GMPClosedErr; + } + + CALL_ON_GMP_THREAD(SendRead, aRecord->Name()); + + return GMPNoErr; +} + +GMPErr GMPStorageChild::Write(GMPRecordImpl* aRecord, const uint8_t* aData, + uint32_t aDataSize) { + if (aDataSize > GMP_MAX_RECORD_SIZE) { + return GMPQuotaExceededErr; + } + + MonitorAutoLock lock(mMonitor); + + if (mShutdown) { + NS_WARNING("GMPStorage used after it's been shutdown!"); + return GMPClosedErr; + } + + if (!HasRecord(aRecord->Name())) { + // Record not opened. + return GMPClosedErr; + } + + CALL_ON_GMP_THREAD(SendWrite, aRecord->Name(), ToArray(aData, aDataSize)); + + return GMPNoErr; +} + +GMPErr GMPStorageChild::Close(const nsCString& aRecordName) { + MonitorAutoLock lock(mMonitor); + + if (!HasRecord(aRecordName)) { + // Already closed. + return GMPClosedErr; + } + + mRecords.Remove(aRecordName); + + if (!mShutdown) { + CALL_ON_GMP_THREAD(SendClose, aRecordName); + } + + return GMPNoErr; +} + +mozilla::ipc::IPCResult GMPStorageChild::RecvOpenComplete( + const nsCString& aRecordName, const GMPErr& aStatus) { + // We don't need a lock to read |mShutdown| since it is only changed in + // the GMP thread. + if (mShutdown) { + return IPC_OK(); + } + RefPtr<GMPRecordImpl> record = GetRecord(aRecordName); + if (!record) { + // Not fatal. + return IPC_OK(); + } + record->OpenComplete(aStatus); + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPStorageChild::RecvReadComplete( + const nsCString& aRecordName, const GMPErr& aStatus, + nsTArray<uint8_t>&& aBytes) { + if (mShutdown) { + return IPC_OK(); + } + RefPtr<GMPRecordImpl> record = GetRecord(aRecordName); + if (!record) { + // Not fatal. + return IPC_OK(); + } + record->ReadComplete(aStatus, aBytes.Elements(), aBytes.Length()); + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPStorageChild::RecvWriteComplete( + const nsCString& aRecordName, const GMPErr& aStatus) { + if (mShutdown) { + return IPC_OK(); + } + RefPtr<GMPRecordImpl> record = GetRecord(aRecordName); + if (!record) { + // Not fatal. + return IPC_OK(); + } + record->WriteComplete(aStatus); + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPStorageChild::RecvShutdown() { + // Block any new storage requests, and thus any messages back to the + // parent. We don't delete any objects here, as that may invalidate + // GMPRecord pointers held by the GMP. + MonitorAutoLock lock(mMonitor); + mShutdown = true; + return IPC_OK(); +} + +} // namespace mozilla::gmp + +// avoid redefined macro in unified build +#undef ON_GMP_THREAD +#undef CALL_ON_GMP_THREAD diff --git a/dom/media/gmp/GMPStorageChild.h b/dom/media/gmp/GMPStorageChild.h new file mode 100644 index 0000000000..97c61a5007 --- /dev/null +++ b/dom/media/gmp/GMPStorageChild.h @@ -0,0 +1,94 @@ +/* -*- 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/. */ + +#ifndef GMPStorageChild_h_ +#define GMPStorageChild_h_ + +#include "mozilla/gmp/PGMPStorageChild.h" +#include "gmp-storage.h" +#include "nsTHashtable.h" +#include "nsRefPtrHashtable.h" +#include "gmp-platform.h" + +#include <queue> + +namespace mozilla::gmp { + +class GMPChild; +class GMPStorageChild; + +class GMPRecordImpl : public GMPRecord { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPRecordImpl) + + GMPRecordImpl(GMPStorageChild* aOwner, const nsCString& aName, + GMPRecordClient* aClient); + + // GMPRecord. + GMPErr Open() override; + GMPErr Read() override; + GMPErr Write(const uint8_t* aData, uint32_t aDataSize) override; + GMPErr Close() override; + + const nsCString& Name() const { return mName; } + + void OpenComplete(GMPErr aStatus); + void ReadComplete(GMPErr aStatus, const uint8_t* aBytes, uint32_t aLength); + void WriteComplete(GMPErr aStatus); + + private: + ~GMPRecordImpl() = default; + const nsCString mName; + GMPRecordClient* const mClient; + GMPStorageChild* const mOwner; +}; + +class GMPStorageChild : public PGMPStorageChild { + friend class PGMPStorageChild; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPStorageChild) + + explicit GMPStorageChild(GMPChild* aPlugin); + + GMPErr CreateRecord(const nsCString& aRecordName, GMPRecord** aOutRecord, + GMPRecordClient* aClient); + + GMPErr Open(GMPRecordImpl* aRecord); + + GMPErr Read(GMPRecordImpl* aRecord); + + GMPErr Write(GMPRecordImpl* aRecord, const uint8_t* aData, + uint32_t aDataSize); + + GMPErr Close(const nsCString& aRecordName); + + private: + bool HasRecord(const nsCString& aRecordName); + already_AddRefed<GMPRecordImpl> GetRecord(const nsCString& aRecordName); + + protected: + ~GMPStorageChild() = default; + + // PGMPStorageChild + mozilla::ipc::IPCResult RecvOpenComplete(const nsCString& aRecordName, + const GMPErr& aStatus); + mozilla::ipc::IPCResult RecvReadComplete(const nsCString& aRecordName, + const GMPErr& aStatus, + nsTArray<uint8_t>&& aBytes); + mozilla::ipc::IPCResult RecvWriteComplete(const nsCString& aRecordName, + const GMPErr& aStatus); + mozilla::ipc::IPCResult RecvShutdown(); + + private: + Monitor mMonitor MOZ_UNANNOTATED; + nsRefPtrHashtable<nsCStringHashKey, GMPRecordImpl> mRecords; + GMPChild* mPlugin; + bool mShutdown; +}; + +} // namespace mozilla::gmp + +#endif // GMPStorageChild_h_ diff --git a/dom/media/gmp/GMPStorageParent.cpp b/dom/media/gmp/GMPStorageParent.cpp new file mode 100644 index 0000000000..d153f5cace --- /dev/null +++ b/dom/media/gmp/GMPStorageParent.cpp @@ -0,0 +1,194 @@ +/* -*- 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 "GMPStorageParent.h" +#include "GMPParent.h" +#include "gmp-storage.h" +#include "mozilla/Unused.h" + +namespace mozilla { + +#ifdef LOG +# undef LOG +#endif + +extern LogModule* GetGMPLog(); + +#define LOGD(msg) MOZ_LOG(GetGMPLog(), mozilla::LogLevel::Debug, msg) +#define LOG(level, msg) MOZ_LOG(GetGMPLog(), (level), msg) + +namespace gmp { + +GMPStorageParent::GMPStorageParent(const nsACString& aNodeId, + GMPParent* aPlugin) + : mNodeId(aNodeId), mPlugin(aPlugin), mShutdown(true) {} + +nsresult GMPStorageParent::Init() { + LOGD(("GMPStorageParent[%p]::Init()", this)); + + if (NS_WARN_IF(mNodeId.IsEmpty())) { + return NS_ERROR_FAILURE; + } + RefPtr<GeckoMediaPluginServiceParent> mps( + GeckoMediaPluginServiceParent::GetSingleton()); + if (NS_WARN_IF(!mps)) { + return NS_ERROR_FAILURE; + } + + bool persistent = false; + if (NS_WARN_IF( + NS_FAILED(mps->IsPersistentStorageAllowed(mNodeId, &persistent)))) { + return NS_ERROR_FAILURE; + } + if (persistent) { + mStorage = CreateGMPDiskStorage(mNodeId, mPlugin->GetPluginBaseName()); + } else { + mStorage = mps->GetMemoryStorageFor(mNodeId); + } + if (!mStorage) { + return NS_ERROR_FAILURE; + } + + mShutdown = false; + return NS_OK; +} + +mozilla::ipc::IPCResult GMPStorageParent::RecvOpen( + const nsACString& aRecordName) { + LOGD(("GMPStorageParent[%p]::RecvOpen(record='%s')", this, + PromiseFlatCString(aRecordName).get())); + + if (mShutdown) { + // Shutdown is an expected state, so we do not IPC_FAIL. + return IPC_OK(); + } + + if (mNodeId.EqualsLiteral("null")) { + // Refuse to open storage if the page is opened from local disk, + // or shared across origin. + LOGD(("GMPStorageParent[%p]::RecvOpen(record='%s') failed; null nodeId", + this, PromiseFlatCString(aRecordName).get())); + Unused << SendOpenComplete(aRecordName, GMPGenericErr); + return IPC_OK(); + } + + if (aRecordName.IsEmpty()) { + LOGD(( + "GMPStorageParent[%p]::RecvOpen(record='%s') failed; record name empty", + this, PromiseFlatCString(aRecordName).get())); + Unused << SendOpenComplete(aRecordName, GMPGenericErr); + return IPC_OK(); + } + + if (mStorage->IsOpen(aRecordName)) { + LOGD(("GMPStorageParent[%p]::RecvOpen(record='%s') failed; record in use", + this, PromiseFlatCString(aRecordName).get())); + Unused << SendOpenComplete(aRecordName, GMPRecordInUse); + return IPC_OK(); + } + + auto err = mStorage->Open(aRecordName); + MOZ_ASSERT(GMP_FAILED(err) || mStorage->IsOpen(aRecordName)); + LOGD(("GMPStorageParent[%p]::RecvOpen(record='%s') complete; rv=%d", this, + PromiseFlatCString(aRecordName).get(), err)); + Unused << SendOpenComplete(aRecordName, err); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPStorageParent::RecvRead( + const nsACString& aRecordName) { + LOGD(("GMPStorageParent[%p]::RecvRead(record='%s')", this, + PromiseFlatCString(aRecordName).get())); + + if (mShutdown) { + // Shutdown is an expected state, so we do not IPC_FAIL. + return IPC_OK(); + } + + nsTArray<uint8_t> data; + if (!mStorage->IsOpen(aRecordName)) { + LOGD(("GMPStorageParent[%p]::RecvRead(record='%s') failed; record not open", + this, PromiseFlatCString(aRecordName).get())); + Unused << SendReadComplete(aRecordName, GMPClosedErr, data); + } else { + GMPErr rv = mStorage->Read(aRecordName, data); + LOGD( + ("GMPStorageParent[%p]::RecvRead(record='%s') read %zu bytes " + "rv=%" PRIu32, + this, PromiseFlatCString(aRecordName).get(), data.Length(), + static_cast<uint32_t>(rv))); + Unused << SendReadComplete(aRecordName, rv, data); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPStorageParent::RecvWrite( + const nsACString& aRecordName, nsTArray<uint8_t>&& aBytes) { + LOGD(("GMPStorageParent[%p]::RecvWrite(record='%s') %zu bytes", this, + PromiseFlatCString(aRecordName).get(), aBytes.Length())); + + if (mShutdown) { + // Shutdown is an expected state, so we do not IPC_FAIL. + return IPC_OK(); + } + + if (!mStorage->IsOpen(aRecordName)) { + LOGD(("GMPStorageParent[%p]::RecvWrite(record='%s') failed record not open", + this, PromiseFlatCString(aRecordName).get())); + Unused << SendWriteComplete(aRecordName, GMPClosedErr); + return IPC_OK(); + } + + if (aBytes.Length() > GMP_MAX_RECORD_SIZE) { + LOGD(("GMPStorageParent[%p]::RecvWrite(record='%s') failed record too big", + this, PromiseFlatCString(aRecordName).get())); + Unused << SendWriteComplete(aRecordName, GMPQuotaExceededErr); + return IPC_OK(); + } + + GMPErr rv = mStorage->Write(aRecordName, aBytes); + LOGD(("GMPStorageParent[%p]::RecvWrite(record='%s') write complete rv=%d", + this, PromiseFlatCString(aRecordName).get(), rv)); + + Unused << SendWriteComplete(aRecordName, rv); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPStorageParent::RecvClose( + const nsACString& aRecordName) { + LOGD(("GMPStorageParent[%p]::RecvClose(record='%s')", this, + PromiseFlatCString(aRecordName).get())); + + if (mShutdown) { + return IPC_OK(); + } + + mStorage->Close(aRecordName); + + return IPC_OK(); +} + +void GMPStorageParent::ActorDestroy(ActorDestroyReason aWhy) { + LOGD(("GMPStorageParent[%p]::ActorDestroy(reason=%d)", this, aWhy)); + Shutdown(); +} + +void GMPStorageParent::Shutdown() { + LOGD(("GMPStorageParent[%p]::Shutdown()", this)); + + if (mShutdown) { + return; + } + mShutdown = true; + Unused << SendShutdown(); + + mStorage = nullptr; +} + +} // namespace gmp +} // namespace mozilla diff --git a/dom/media/gmp/GMPStorageParent.h b/dom/media/gmp/GMPStorageParent.h new file mode 100644 index 0000000000..b0cbba14c1 --- /dev/null +++ b/dom/media/gmp/GMPStorageParent.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +#ifndef GMPStorageParent_h_ +#define GMPStorageParent_h_ + +#include "mozilla/gmp/PGMPStorageParent.h" +#include "GMPStorage.h" + +namespace mozilla::gmp { + +class GMPParent; + +class GMPStorageParent : public PGMPStorageParent { + friend class PGMPStorageParent; + + public: + NS_INLINE_DECL_REFCOUNTING(GMPStorageParent) + GMPStorageParent(const nsACString& aNodeId, GMPParent* aPlugin); + + nsresult Init(); + void Shutdown(); + + protected: + mozilla::ipc::IPCResult RecvOpen(const nsACString& aRecordName) override; + mozilla::ipc::IPCResult RecvRead(const nsACString& aRecordName) override; + mozilla::ipc::IPCResult RecvWrite(const nsACString& aRecordName, + nsTArray<uint8_t>&& aBytes) override; + mozilla::ipc::IPCResult RecvClose(const nsACString& aRecordName) override; + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + ~GMPStorageParent() = default; + + RefPtr<GMPStorage> mStorage; + + const nsCString mNodeId; + RefPtr<GMPParent> mPlugin; + // True after Shutdown(), or if Init() has not completed successfully. + bool mShutdown; +}; + +} // namespace mozilla::gmp + +#endif // GMPStorageParent_h_ diff --git a/dom/media/gmp/GMPTimerChild.cpp b/dom/media/gmp/GMPTimerChild.cpp new file mode 100644 index 0000000000..8858f5f87f --- /dev/null +++ b/dom/media/gmp/GMPTimerChild.cpp @@ -0,0 +1,59 @@ +/* -*- 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 "GMPTimerChild.h" +#include "GMPPlatform.h" +#include "GMPChild.h" + +#define MAX_NUM_TIMERS 1000 + +namespace mozilla::gmp { + +GMPTimerChild::GMPTimerChild(GMPChild* aPlugin) + : mTimerCount(1), mPlugin(aPlugin) { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); +} + +GMPTimerChild::~GMPTimerChild() { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); +} + +GMPErr GMPTimerChild::SetTimer(GMPTask* aTask, int64_t aTimeoutMS) { + if (!aTask) { + NS_WARNING("Tried to set timer with null task!"); + return GMPGenericErr; + } + + if (mPlugin->GMPMessageLoop() != MessageLoop::current()) { + NS_WARNING("Tried to set GMP timer on non-main thread."); + return GMPGenericErr; + } + + if (mTimers.Count() > MAX_NUM_TIMERS) { + return GMPQuotaExceededErr; + } + uint32_t timerId = mTimerCount; + mTimers.InsertOrUpdate(timerId, aTask); + mTimerCount++; + + if (!SendSetTimer(timerId, aTimeoutMS)) { + return GMPGenericErr; + } + return GMPNoErr; +} + +mozilla::ipc::IPCResult GMPTimerChild::RecvTimerExpired( + const uint32_t& aTimerId) { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + GMPTask* task = mTimers.Get(aTimerId); + mTimers.Remove(aTimerId); + if (task) { + RunOnMainThread(task); + } + return IPC_OK(); +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPTimerChild.h b/dom/media/gmp/GMPTimerChild.h new file mode 100644 index 0000000000..f81f796e12 --- /dev/null +++ b/dom/media/gmp/GMPTimerChild.h @@ -0,0 +1,45 @@ +/* -*- 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/. */ + +#ifndef GMPTimerChild_h_ +#define GMPTimerChild_h_ + +#include "mozilla/gmp/PGMPTimerChild.h" +#include "mozilla/Monitor.h" +#include "nsTHashMap.h" +#include "nsHashKeys.h" +#include "gmp-errors.h" +#include "gmp-platform.h" + +namespace mozilla::gmp { + +class GMPChild; + +class GMPTimerChild : public PGMPTimerChild { + friend class PGMPTimerChild; + + public: + NS_INLINE_DECL_REFCOUNTING(GMPTimerChild) + + explicit GMPTimerChild(GMPChild* aPlugin); + + GMPErr SetTimer(GMPTask* aTask, int64_t aTimeoutMS); + + protected: + // GMPTimerChild + mozilla::ipc::IPCResult RecvTimerExpired(const uint32_t& aTimerId); + + private: + ~GMPTimerChild(); + + nsTHashMap<nsUint32HashKey, GMPTask*> mTimers; + uint32_t mTimerCount; + + GMPChild* mPlugin; +}; + +} // namespace mozilla::gmp + +#endif // GMPTimerChild_h_ diff --git a/dom/media/gmp/GMPTimerParent.cpp b/dom/media/gmp/GMPTimerParent.cpp new file mode 100644 index 0000000000..31b17ef214 --- /dev/null +++ b/dom/media/gmp/GMPTimerParent.cpp @@ -0,0 +1,105 @@ +/* -*- 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 "GMPTimerParent.h" + +#include "GMPLog.h" +#include "mozilla/Unused.h" +#include "nsComponentManagerUtils.h" + +namespace mozilla { + +extern LogModule* GetGMPLog(); + +#ifdef __CLASS__ +# undef __CLASS__ +#endif +#define __CLASS__ "GMPTimerParent" + +namespace gmp { + +GMPTimerParent::GMPTimerParent(nsISerialEventTarget* aGMPEventTarget) + : mGMPEventTarget(aGMPEventTarget), mIsOpen(true) {} + +mozilla::ipc::IPCResult GMPTimerParent::RecvSetTimer( + const uint32_t& aTimerId, const uint32_t& aTimeoutMs) { + GMP_LOG_DEBUG("%s::%s: %p mIsOpen=%d", __CLASS__, __FUNCTION__, this, + mIsOpen); + + MOZ_ASSERT(mGMPEventTarget->IsOnCurrentThread()); + + if (!mIsOpen) { + return IPC_OK(); + } + + nsresult rv; + UniquePtr<Context> ctx(new Context()); + + rv = NS_NewTimerWithFuncCallback( + getter_AddRefs(ctx->mTimer), &GMPTimerParent::GMPTimerExpired, ctx.get(), + aTimeoutMs, nsITimer::TYPE_ONE_SHOT, "gmp::GMPTimerParent::RecvSetTimer", + mGMPEventTarget); + NS_ENSURE_SUCCESS(rv, IPC_OK()); + + ctx->mId = aTimerId; + ctx->mParent = this; + + mTimers.Insert(ctx.release()); + + return IPC_OK(); +} + +void GMPTimerParent::Shutdown() { + GMP_LOG_DEBUG("%s::%s: %p mIsOpen=%d", __CLASS__, __FUNCTION__, this, + mIsOpen); + + MOZ_ASSERT(mGMPEventTarget->IsOnCurrentThread()); + + for (Context* context : mTimers) { + context->mTimer->Cancel(); + delete context; + } + + mTimers.Clear(); + mIsOpen = false; +} + +void GMPTimerParent::ActorDestroy(ActorDestroyReason aWhy) { + GMP_LOG_DEBUG("%s::%s: %p mIsOpen=%d", __CLASS__, __FUNCTION__, this, + mIsOpen); + + Shutdown(); +} + +/* static */ +void GMPTimerParent::GMPTimerExpired(nsITimer* aTimer, void* aClosure) { + MOZ_ASSERT(aClosure); + UniquePtr<Context> ctx(static_cast<Context*>(aClosure)); + MOZ_ASSERT(ctx->mParent); + if (ctx->mParent) { + ctx->mParent->TimerExpired(ctx.get()); + } +} + +void GMPTimerParent::TimerExpired(Context* aContext) { + GMP_LOG_DEBUG("%s::%s: %p mIsOpen=%d", __CLASS__, __FUNCTION__, this, + mIsOpen); + MOZ_ASSERT(mGMPEventTarget->IsOnCurrentThread()); + + if (!mIsOpen) { + return; + } + + uint32_t id = aContext->mId; + mTimers.Remove(aContext); + if (id) { + Unused << SendTimerExpired(id); + } +} + +} // namespace gmp +} // namespace mozilla + +#undef __CLASS__ diff --git a/dom/media/gmp/GMPTimerParent.h b/dom/media/gmp/GMPTimerParent.h new file mode 100644 index 0000000000..dae1d9a1fb --- /dev/null +++ b/dom/media/gmp/GMPTimerParent.h @@ -0,0 +1,56 @@ +/* -*- 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/. */ + +#ifndef GMPTimerParent_h_ +#define GMPTimerParent_h_ + +#include "mozilla/gmp/PGMPTimerParent.h" +#include "nsITimer.h" +#include "nsCOMPtr.h" +#include "nsTHashSet.h" +#include "mozilla/Monitor.h" + +namespace mozilla::gmp { + +class GMPTimerParent : public PGMPTimerParent { + friend class PGMPTimerParent; + + public: + NS_INLINE_DECL_REFCOUNTING(GMPTimerParent) + explicit GMPTimerParent(nsISerialEventTarget* aGMPEventTarget); + + void Shutdown(); + + protected: + mozilla::ipc::IPCResult RecvSetTimer(const uint32_t& aTimerId, + const uint32_t& aTimeoutMs); + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + ~GMPTimerParent() = default; + + static void GMPTimerExpired(nsITimer* aTimer, void* aClosure); + + struct Context { + Context() : mId(0) { MOZ_COUNT_CTOR(Context); } + MOZ_COUNTED_DTOR(Context) + nsCOMPtr<nsITimer> mTimer; + RefPtr<GMPTimerParent> + mParent; // Note: live timers keep the GMPTimerParent alive. + uint32_t mId; + }; + + void TimerExpired(Context* aContext); + + nsTHashSet<Context*> mTimers; + + nsCOMPtr<nsISerialEventTarget> mGMPEventTarget; + + bool mIsOpen; +}; + +} // namespace mozilla::gmp + +#endif // GMPTimerParent_h_ diff --git a/dom/media/gmp/GMPTypes.ipdlh b/dom/media/gmp/GMPTypes.ipdlh new file mode 100644 index 0000000000..6cb37b6068 --- /dev/null +++ b/dom/media/gmp/GMPTypes.ipdlh @@ -0,0 +1,116 @@ +/* -*- 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 "GMPMessageUtils.h"; + +using cdm::EncryptionScheme from "GMPSanitizedExports.h"; +using GMPBufferType from "gmp-video-codec.h"; + +namespace mozilla { +namespace gmp { + +// GMP processes are associated with a specific node ID, so all GMP requests +// which have the same node ID will use the same GMP process. Depending on the +// use case, the node ID may be represented by a string (such as when used for +// WebRTC) or by this structure, which is populated according to the origin +// initiating the request. This structure will eventually be converted to a +// string representing a node ID. For this structure, the process ensures the +// strings are unique for the combination of origin, top level origin and GMP +// name. +struct NodeIdParts { + nsString mOrigin; + nsString mTopLevelOrigin; + nsString mGMPName; +}; + +// A NodeIdVariant should contain either +// - A string representing an already computed node ID. +// - A NodeIdParts representing a node ID that still needs to be computed by +// processing those parts. +// This union is used to simplify passing of node ID information. Some +// GMP use cases can hard code their node ID, while others need to compute +// the node ID later. This lets us avoid having overloads to handle +// the two different paths. +union NodeIdVariant { + nsCString; + NodeIdParts; +}; + +struct GMPVideoEncodedFrameData { + uint32_t mEncodedWidth; + uint32_t mEncodedHeight; + uint64_t mTimestamp; // microseconds + uint64_t mDuration; // microseconds + uint32_t mFrameType; + uint32_t mSize; + GMPBufferType mBufferType; + Shmem mBuffer; + bool mCompleteFrame; +}; + +struct GMPPlaneData { + int32_t mSize; + int32_t mStride; + Shmem mBuffer; +}; + +struct GMPVideoi420FrameData { + GMPPlaneData mYPlane; + GMPPlaneData mUPlane; + GMPPlaneData mVPlane; + int32_t mWidth; + int32_t mHeight; + uint64_t mTimestamp; // microseconds + uint64_t? mUpdatedTimestamp; // microseconds + uint64_t mDuration; // microseconds +}; + +struct CDMInputBuffer { + Shmem mData; + uint8_t[] mKeyId; + uint8_t[] mIV; + int64_t mTimestamp; + int64_t mDuration; + uint32_t[] mClearBytes; + uint32_t[] mCipherBytes; + uint8_t mCryptByteBlock; + uint8_t mSkipByteBlock; + EncryptionScheme mEncryptionScheme; +}; + +struct CDMVideoDecoderConfig { + uint32_t mCodec; + uint32_t mProfile; + uint32_t mFormat; + int32_t mImageWidth; + int32_t mImageHeight; + uint8_t[] mExtraData; + EncryptionScheme mEncryptionScheme; +}; + +struct CDMKeyInformation { + uint8_t[] mKeyId; + uint32_t mStatus; + uint32_t mSystemCode; +}; + +struct CDMVideoPlane { + uint32_t mPlaneOffset; + uint32_t mStride; +}; + +struct CDMVideoFrame { + uint32_t mFormat; + int32_t mImageWidth; + int32_t mImageHeight; + CDMVideoPlane mYPlane; + CDMVideoPlane mUPlane; + CDMVideoPlane mVPlane; + int64_t mTimestamp; + int64_t mDuration; +}; + +} // namespace gmp +} // namespace mozilla diff --git a/dom/media/gmp/GMPUtils.cpp b/dom/media/gmp/GMPUtils.cpp new file mode 100644 index 0000000000..1bc02d7027 --- /dev/null +++ b/dom/media/gmp/GMPUtils.cpp @@ -0,0 +1,228 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "GMPUtils.h" + +#include "GMPService.h" +#include "VideoLimits.h" +#include "mozIGeckoMediaPluginService.h" +#include "mozilla/Base64.h" +#include "nsCOMPtr.h" +#include "nsCRTGlue.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIConsoleService.h" +#include "nsIFile.h" +#include "nsLiteralString.h" +#include "prio.h" + +namespace mozilla { + +void SplitAt(const char* aDelims, const nsACString& aInput, + nsTArray<nsCString>& aOutTokens) { + nsAutoCString str(aInput); + char* end = str.BeginWriting(); + const char* start = nullptr; + while (!!(start = NS_strtok(aDelims, &end))) { + aOutTokens.AppendElement(nsCString(start)); + } +} + +nsCString ToHexString(const uint8_t* aBytes, uint32_t aLength) { + static const char hex[] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + nsCString str; + for (uint32_t i = 0; i < aLength; i++) { + char buf[3]; + buf[0] = hex[(aBytes[i] & 0xf0) >> 4]; + buf[1] = hex[aBytes[i] & 0x0f]; + buf[2] = 0; + str.AppendASCII(buf); + } + return str; +} + +nsCString ToHexString(const nsTArray<uint8_t>& aBytes) { + return ToHexString(aBytes.Elements(), aBytes.Length()); +} + +bool FileExists(nsIFile* aFile) { + bool exists = false; + return aFile && NS_SUCCEEDED(aFile->Exists(&exists)) && exists; +} + +DirectoryEnumerator::DirectoryEnumerator(nsIFile* aPath, Mode aMode) + : mMode(aMode) { + aPath->GetDirectoryEntries(getter_AddRefs(mIter)); +} + +already_AddRefed<nsIFile> DirectoryEnumerator::Next() { + if (!mIter) { + return nullptr; + } + bool hasMore = false; + while (NS_SUCCEEDED(mIter->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsISupports> supports; + nsresult rv = mIter->GetNext(getter_AddRefs(supports)); + if (NS_FAILED(rv)) { + continue; + } + + nsCOMPtr<nsIFile> path(do_QueryInterface(supports, &rv)); + if (NS_FAILED(rv)) { + continue; + } + + if (mMode == DirsOnly) { + bool isDirectory = false; + rv = path->IsDirectory(&isDirectory); + if (NS_FAILED(rv) || !isDirectory) { + continue; + } + } + return path.forget(); + } + return nullptr; +} + +bool ReadIntoArray(nsIFile* aFile, nsTArray<uint8_t>& aOutDst, + size_t aMaxLength) { + if (!FileExists(aFile)) { + return false; + } + + PRFileDesc* fd = nullptr; + nsresult rv = aFile->OpenNSPRFileDesc(PR_RDONLY, 0, &fd); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + int32_t length = PR_Seek(fd, 0, PR_SEEK_END); + PR_Seek(fd, 0, PR_SEEK_SET); + + if (length < 0 || (size_t)length > aMaxLength) { + NS_WARNING("EME file is longer than maximum allowed length"); + PR_Close(fd); + return false; + } + aOutDst.SetLength(length); + int32_t bytesRead = PR_Read(fd, aOutDst.Elements(), length); + PR_Close(fd); + return (bytesRead == length); +} + +bool ReadIntoString(nsIFile* aFile, nsCString& aOutDst, size_t aMaxLength) { + nsTArray<uint8_t> buf; + bool rv = ReadIntoArray(aFile, buf, aMaxLength); + if (rv) { + buf.AppendElement(0); // Append null terminator, required by nsC*String. + aOutDst = nsDependentCString((const char*)buf.Elements(), buf.Length() - 1); + } + return rv; +} + +bool GMPInfoFileParser::Init(nsIFile* aInfoFile) { + nsTArray<nsCString> lines; + static const size_t MAX_GMP_INFO_FILE_LENGTH = 5 * 1024; + + nsAutoCString info; + if (!ReadIntoString(aInfoFile, info, MAX_GMP_INFO_FILE_LENGTH)) { + NS_WARNING("Failed to read info file in GMP process."); + return false; + } + + // Note: we pass "\r\n" to SplitAt so that we'll split lines delimited + // by \n (Unix), \r\n (Windows) and \r (old MacOSX). + SplitAt("\r\n", info, lines); + + for (nsCString line : lines) { + // Field name is the string up to but not including the first ':' + // character on the line. + int32_t colon = line.FindChar(':'); + if (colon <= 0) { + // Not allowed to be the first character. + // Info field name must be at least one character. + continue; + } + nsAutoCString key(Substring(line, 0, colon)); + ToLowerCase(key); + key.Trim(" "); + + auto value = MakeUnique<nsCString>(Substring(line, colon + 1)); + value->Trim(" "); + mValues.InsertOrUpdate( + key, + std::move(value)); // Hashtable assumes ownership of value. + } + + return true; +} + +bool GMPInfoFileParser::Contains(const nsCString& aKey) const { + nsCString key(aKey); + ToLowerCase(key); + return mValues.Contains(key); +} + +nsCString GMPInfoFileParser::Get(const nsCString& aKey) const { + MOZ_ASSERT(Contains(aKey)); + nsCString key(aKey); + ToLowerCase(key); + nsCString* p = nullptr; + if (mValues.Get(key, &p)) { + return nsCString(*p); + } + return ""_ns; +} + +bool HaveGMPFor(const nsACString& aAPI, const nsTArray<nsCString>& aTags) { + nsCOMPtr<mozIGeckoMediaPluginService> mps = + do_GetService("@mozilla.org/gecko-media-plugin-service;1"); + if (NS_WARN_IF(!mps)) { + return false; + } + + bool hasPlugin = false; + if (NS_FAILED(mps->HasPluginForAPI(aAPI, aTags, &hasPlugin))) { + return false; + } + return hasPlugin; +} + +void LogToConsole(const nsAString& aMsg) { + nsCOMPtr<nsIConsoleService> console( + do_GetService("@mozilla.org/consoleservice;1")); + if (!console) { + NS_WARNING("Failed to log message to console."); + return; + } + nsAutoString msg(aMsg); + console->LogStringMessage(msg.get()); +} + +already_AddRefed<nsISerialEventTarget> GetGMPThread() { + RefPtr<gmp::GeckoMediaPluginService> service = + gmp::GeckoMediaPluginService::GetGeckoMediaPluginService(); + nsCOMPtr<nsISerialEventTarget> thread = + service ? service->GetGMPThread() : nullptr; + return thread.forget(); +} + +static size_t Align16(size_t aNumber) { + const size_t mask = 15; // Alignment - 1. + return (aNumber + mask) & ~mask; +} + +size_t I420FrameBufferSizePadded(int32_t aWidth, int32_t aHeight) { + if (aWidth <= 0 || aHeight <= 0 || aWidth > MAX_VIDEO_WIDTH || + aHeight > MAX_VIDEO_HEIGHT) { + return 0; + } + + size_t ySize = Align16(aWidth) * Align16(aHeight); + return ySize + (ySize / 4) * 2; +} + +} // namespace mozilla diff --git a/dom/media/gmp/GMPUtils.h b/dom/media/gmp/GMPUtils.h new file mode 100644 index 0000000000..0ac097420c --- /dev/null +++ b/dom/media/gmp/GMPUtils.h @@ -0,0 +1,83 @@ +/* -*- 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/. */ + +#ifndef GMPUtils_h_ +#define GMPUtils_h_ + +#include "mozilla/AbstractThread.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "nsCOMPtr.h" +#include "nsClassHashtable.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +#define CHROMIUM_CDM_API_BACKWARD_COMPAT "chromium-cdm9-host4" +#define CHROMIUM_CDM_API "chromium-cdm10-host4" + +class nsIFile; +class nsIDirectoryEnumerator; + +namespace mozilla { + +template <typename T> +struct DestroyPolicy { + void operator()(T* aGMPObject) const { aGMPObject->Destroy(); } +}; + +template <typename T> +using GMPUniquePtr = mozilla::UniquePtr<T, DestroyPolicy<T>>; + +void SplitAt(const char* aDelims, const nsACString& aInput, + nsTArray<nsCString>& aOutTokens); + +nsCString ToHexString(const nsTArray<uint8_t>& aBytes); + +nsCString ToHexString(const uint8_t* aBytes, uint32_t aLength); + +bool FileExists(nsIFile* aFile); + +// Enumerate directory entries for a specified path. +class DirectoryEnumerator { + public: + enum Mode { + DirsOnly, // Enumeration only includes directories. + FilesAndDirs // Enumeration includes directories and non-directory files. + }; + + DirectoryEnumerator(nsIFile* aPath, Mode aMode); + + already_AddRefed<nsIFile> Next(); + + private: + Mode mMode; + nsCOMPtr<nsIDirectoryEnumerator> mIter; +}; + +class GMPInfoFileParser { + public: + bool Init(nsIFile* aFile); + bool Contains(const nsCString& aKey) const; + nsCString Get(const nsCString& aKey) const; + + private: + nsClassHashtable<nsCStringHashKey, nsCString> mValues; +}; + +bool ReadIntoString(nsIFile* aFile, nsCString& aOutDst, size_t aMaxLength); + +bool HaveGMPFor(const nsACString& aAPI, const nsTArray<nsCString>& aTags); + +void LogToConsole(const nsAString& aMsg); + +already_AddRefed<nsISerialEventTarget> GetGMPThread(); + +// Returns the number of bytes required to store an aWidth x aHeight image in +// I420 format, padded so that the width and height are multiples of 16. +size_t I420FrameBufferSizePadded(int32_t aWidth, int32_t aHeight); + +} // namespace mozilla + +#endif diff --git a/dom/media/gmp/GMPVideoDecoderChild.cpp b/dom/media/gmp/GMPVideoDecoderChild.cpp new file mode 100644 index 0000000000..0f605cca9b --- /dev/null +++ b/dom/media/gmp/GMPVideoDecoderChild.cpp @@ -0,0 +1,210 @@ +/* -*- 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 "GMPVideoDecoderChild.h" +#include "GMPVideoi420FrameImpl.h" +#include "GMPContentChild.h" +#include <stdio.h> +#include "mozilla/Unused.h" +#include "GMPVideoEncodedFrameImpl.h" +#include "runnable_utils.h" + +namespace mozilla::gmp { + +GMPVideoDecoderChild::GMPVideoDecoderChild(GMPContentChild* aPlugin) + : GMPSharedMemManager(aPlugin), + mPlugin(aPlugin), + mVideoDecoder(nullptr), + mVideoHost(this), + mNeedShmemIntrCount(0), + mPendingDecodeComplete(false) { + MOZ_ASSERT(mPlugin); +} + +GMPVideoDecoderChild::~GMPVideoDecoderChild() { + MOZ_ASSERT(!mNeedShmemIntrCount); +} + +void GMPVideoDecoderChild::Init(GMPVideoDecoder* aDecoder) { + MOZ_ASSERT(aDecoder, + "Cannot initialize video decoder child without a video decoder!"); + mVideoDecoder = aDecoder; +} + +GMPVideoHostImpl& GMPVideoDecoderChild::Host() { return mVideoHost; } + +void GMPVideoDecoderChild::Decoded(GMPVideoi420Frame* aDecodedFrame) { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + if (!aDecodedFrame) { + MOZ_CRASH("Not given a decoded frame!"); + } + + auto df = static_cast<GMPVideoi420FrameImpl*>(aDecodedFrame); + + GMPVideoi420FrameData frameData; + df->InitFrameData(frameData); + SendDecoded(frameData); + + aDecodedFrame->Destroy(); +} + +void GMPVideoDecoderChild::ReceivedDecodedReferenceFrame( + const uint64_t aPictureId) { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + SendReceivedDecodedReferenceFrame(aPictureId); +} + +void GMPVideoDecoderChild::ReceivedDecodedFrame(const uint64_t aPictureId) { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + SendReceivedDecodedFrame(aPictureId); +} + +void GMPVideoDecoderChild::InputDataExhausted() { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + SendInputDataExhausted(); +} + +void GMPVideoDecoderChild::DrainComplete() { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + SendDrainComplete(); +} + +void GMPVideoDecoderChild::ResetComplete() { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + SendResetComplete(); +} + +void GMPVideoDecoderChild::Error(GMPErr aError) { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + SendError(aError); +} + +mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvInitDecode( + const GMPVideoCodec& aCodecSettings, nsTArray<uint8_t>&& aCodecSpecific, + const int32_t& aCoreCount) { + if (!mVideoDecoder) { + return IPC_FAIL(this, "!mVideoDecoder"); + } + + // Ignore any return code. It is OK for this to fail without killing the + // process. + mVideoDecoder->InitDecode(aCodecSettings, aCodecSpecific.Elements(), + aCodecSpecific.Length(), this, aCoreCount); + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvDecode( + const GMPVideoEncodedFrameData& aInputFrame, const bool& aMissingFrames, + nsTArray<uint8_t>&& aCodecSpecificInfo, const int64_t& aRenderTimeMs) { + if (!mVideoDecoder) { + return IPC_FAIL(this, "!mVideoDecoder"); + } + + auto f = new GMPVideoEncodedFrameImpl(aInputFrame, &mVideoHost); + + // Ignore any return code. It is OK for this to fail without killing the + // process. + mVideoDecoder->Decode(f, aMissingFrames, aCodecSpecificInfo.Elements(), + aCodecSpecificInfo.Length(), aRenderTimeMs); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvChildShmemForPool( + Shmem&& aFrameBuffer) { + if (aFrameBuffer.IsWritable()) { + mVideoHost.SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPFrameData, + aFrameBuffer); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvReset() { + if (!mVideoDecoder) { + return IPC_FAIL(this, "!mVideoDecoder"); + } + + // Ignore any return code. It is OK for this to fail without killing the + // process. + mVideoDecoder->Reset(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvDrain() { + if (!mVideoDecoder) { + return IPC_FAIL(this, "!mVideoDecoder"); + } + + // Ignore any return code. It is OK for this to fail without killing the + // process. + mVideoDecoder->Drain(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvDecodingComplete() { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + if (mNeedShmemIntrCount) { + // There's a GMP blocked in Alloc() waiting for the CallNeedShem() to + // return a frame they can use. Don't call the GMP's DecodingComplete() + // now and don't delete the GMPVideoDecoderChild, defer processing the + // DecodingComplete() until once the Alloc() finishes. + mPendingDecodeComplete = true; + return IPC_OK(); + } + if (mVideoDecoder) { + // Ignore any return code. It is OK for this to fail without killing the + // process. + mVideoDecoder->DecodingComplete(); + mVideoDecoder = nullptr; + } + + mVideoHost.DoneWithAPI(); + + mPlugin = nullptr; + + Unused << Send__delete__(this); + + return IPC_OK(); +} + +bool GMPVideoDecoderChild::Alloc(size_t aSize, Shmem* aMem) { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + bool rv; +#ifndef SHMEM_ALLOC_IN_CHILD + ++mNeedShmemIntrCount; + rv = SendNeedShmem(aSize, aMem); + --mNeedShmemIntrCount; + if (mPendingDecodeComplete && mNeedShmemIntrCount == 0) { + mPendingDecodeComplete = false; + mPlugin->GMPMessageLoop()->PostTask( + NewRunnableMethod("gmp::GMPVideoDecoderChild::RecvDecodingComplete", + this, &GMPVideoDecoderChild::RecvDecodingComplete)); + } +#else + rv = AllocShmem(aSize, aType, aMem); +#endif + return rv; +} + +void GMPVideoDecoderChild::Dealloc(Shmem&& aMem) { +#ifndef SHMEM_ALLOC_IN_CHILD + SendParentShmemForPool(std::move(aMem)); +#else + DeallocShmem(aMem); +#endif +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPVideoDecoderChild.h b/dom/media/gmp/GMPVideoDecoderChild.h new file mode 100644 index 0000000000..3c74e5f02c --- /dev/null +++ b/dom/media/gmp/GMPVideoDecoderChild.h @@ -0,0 +1,75 @@ +/* -*- 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/. */ + +#ifndef GMPVideoDecoderChild_h_ +#define GMPVideoDecoderChild_h_ + +#include "nsString.h" +#include "mozilla/gmp/PGMPVideoDecoderChild.h" +#include "gmp-video-decode.h" +#include "GMPSharedMemManager.h" +#include "GMPVideoHost.h" +#include "mozilla/gmp/GMPTypes.h" + +namespace mozilla::gmp { + +class GMPContentChild; + +class GMPVideoDecoderChild : public PGMPVideoDecoderChild, + public GMPVideoDecoderCallback, + public GMPSharedMemManager { + friend class PGMPVideoDecoderChild; + + public: + // Mark AddRef and Release as `final`, as they overload pure virtual + // implementations in PGMPVideoDecoderChild. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPVideoDecoderChild, final); + + explicit GMPVideoDecoderChild(GMPContentChild* aPlugin); + + void Init(GMPVideoDecoder* aDecoder); + GMPVideoHostImpl& Host(); + + // GMPVideoDecoderCallback + void Decoded(GMPVideoi420Frame* decodedFrame) override; + void ReceivedDecodedReferenceFrame(const uint64_t pictureId) override; + void ReceivedDecodedFrame(const uint64_t pictureId) override; + void InputDataExhausted() override; + void DrainComplete() override; + void ResetComplete() override; + void Error(GMPErr aError) override; + + // GMPSharedMemManager + bool Alloc(size_t aSize, Shmem* aMem) override; + void Dealloc(Shmem&& aMem) override; + + private: + virtual ~GMPVideoDecoderChild(); + + // PGMPVideoDecoderChild + mozilla::ipc::IPCResult RecvInitDecode(const GMPVideoCodec& aCodecSettings, + nsTArray<uint8_t>&& aCodecSpecific, + const int32_t& aCoreCount); + mozilla::ipc::IPCResult RecvDecode( + const GMPVideoEncodedFrameData& aInputFrame, const bool& aMissingFrames, + nsTArray<uint8_t>&& aCodecSpecificInfo, const int64_t& aRenderTimeMs); + mozilla::ipc::IPCResult RecvChildShmemForPool(Shmem&& aFrameBuffer); + mozilla::ipc::IPCResult RecvReset(); + mozilla::ipc::IPCResult RecvDrain(); + mozilla::ipc::IPCResult RecvDecodingComplete(); + + GMPContentChild* mPlugin; + GMPVideoDecoder* mVideoDecoder; + GMPVideoHostImpl mVideoHost; + + // Non-zero when a GMP is blocked spinning the IPC message loop while + // waiting on an NeedShmem to complete. + int mNeedShmemIntrCount; + bool mPendingDecodeComplete; +}; + +} // namespace mozilla::gmp + +#endif // GMPVideoDecoderChild_h_ diff --git a/dom/media/gmp/GMPVideoDecoderParent.cpp b/dom/media/gmp/GMPVideoDecoderParent.cpp new file mode 100644 index 0000000000..dfe7182c41 --- /dev/null +++ b/dom/media/gmp/GMPVideoDecoderParent.cpp @@ -0,0 +1,461 @@ +/* -*- 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 "GMPVideoDecoderParent.h" + +#include "GMPContentParent.h" +#include "GMPUtils.h" +#include "GMPLog.h" +#include "GMPMessageUtils.h" +#include "GMPVideoEncodedFrameImpl.h" +#include "GMPVideoi420FrameImpl.h" +#include "mozilla/gmp/GMPTypes.h" +#include "mozilla/Unused.h" +#include "nsAutoRef.h" +#include "nsPrintfCString.h" +#include "nsThreadUtils.h" + +namespace mozilla::gmp { + +// States: +// Initial: mIsOpen == false +// on InitDecode success -> Open +// on Shutdown -> Dead +// Open: mIsOpen == true +// on Close -> Dead +// on ActorDestroy -> Dead +// on Shutdown -> Dead +// Dead: mIsOpen == false + +GMPVideoDecoderParent::GMPVideoDecoderParent(GMPContentParent* aPlugin) + : GMPSharedMemManager(aPlugin), + mIsOpen(false), + mShuttingDown(false), + mActorDestroyed(false), + mIsAwaitingResetComplete(false), + mIsAwaitingDrainComplete(false), + mPlugin(aPlugin), + mCallback(nullptr), + mVideoHost(this), + mPluginId(aPlugin->GetPluginId()), + mPluginType(aPlugin->GetPluginType()), + mFrameCount(0) { + MOZ_ASSERT(mPlugin); +} + +GMPVideoDecoderParent::~GMPVideoDecoderParent() = default; + +GMPVideoHostImpl& GMPVideoDecoderParent::Host() { return mVideoHost; } + +// Note: may be called via Terminated() +void GMPVideoDecoderParent::Close() { + GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::Close()", this); + MOZ_ASSERT(!mPlugin || mPlugin->GMPEventTarget()->IsOnCurrentThread()); + + // Ensure if we've received a Close while waiting for a ResetComplete + // or DrainComplete notification, we'll unblock the caller before processing + // the close. This seems unlikely to happen, but better to be careful. + UnblockResetAndDrain(); + + // Consumer is done with us; we can shut down. No more callbacks should + // be made to mCallback. Note: do this before Shutdown()! + mCallback = nullptr; + // Let Shutdown mark us as dead so it knows if we had been alive + + // In case this is the last reference + RefPtr<GMPVideoDecoderParent> kungfudeathgrip(this); + Release(); + Shutdown(); +} + +nsresult GMPVideoDecoderParent::InitDecode( + const GMPVideoCodec& aCodecSettings, + const nsTArray<uint8_t>& aCodecSpecific, + GMPVideoDecoderCallbackProxy* aCallback, int32_t aCoreCount) { + GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::InitDecode()", this); + + if (mActorDestroyed) { + NS_WARNING("Trying to use a destroyed GMP video decoder!"); + return NS_ERROR_FAILURE; + } + if (mIsOpen) { + NS_WARNING("Trying to re-init an in-use GMP video decoder!"); + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + + if (!aCallback) { + return NS_ERROR_FAILURE; + } + mCallback = aCallback; + + if (!SendInitDecode(aCodecSettings, aCodecSpecific, aCoreCount)) { + return NS_ERROR_FAILURE; + } + mIsOpen = true; + + // Async IPC, we don't have access to a return value. + return NS_OK; +} + +nsresult GMPVideoDecoderParent::Decode( + GMPUniquePtr<GMPVideoEncodedFrame> aInputFrame, bool aMissingFrames, + const nsTArray<uint8_t>& aCodecSpecificInfo, int64_t aRenderTimeMs) { + GMP_LOG_VERBOSE( + "GMPVideoDecoderParent[%p]::Decode() timestamp=%" PRId64 " keyframe=%d", + this, aInputFrame->TimeStamp(), aInputFrame->FrameType() == kGMPKeyFrame); + + if (!mIsOpen) { + GMP_LOG_ERROR( + "GMPVideoDecoderParent[%p]::Decode() ERROR; dead GMPVideoDecoder", + this); + NS_WARNING("Trying to use an dead GMP video decoder"); + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + + GMPUniquePtr<GMPVideoEncodedFrameImpl> inputFrameImpl( + static_cast<GMPVideoEncodedFrameImpl*>(aInputFrame.release())); + + // Very rough kill-switch if the plugin stops processing. If it's merely + // hung and continues, we'll come back to life eventually. + // 3* is because we're using 3 buffers per frame for i420 data for now. + if ((NumInUse(GMPSharedMem::kGMPFrameData) > + 3 * GMPSharedMem::kGMPBufLimit) || + (NumInUse(GMPSharedMem::kGMPEncodedData) > GMPSharedMem::kGMPBufLimit)) { + GMP_LOG_ERROR( + "GMPVideoDecoderParent[%p]::Decode() ERROR; shmem buffer limit hit " + "frame=%d encoded=%d", + this, NumInUse(GMPSharedMem::kGMPFrameData), + NumInUse(GMPSharedMem::kGMPEncodedData)); + return NS_ERROR_FAILURE; + } + + GMPVideoEncodedFrameData frameData; + inputFrameImpl->RelinquishFrameData(frameData); + + if (!SendDecode(frameData, aMissingFrames, aCodecSpecificInfo, + aRenderTimeMs)) { + GMP_LOG_ERROR( + "GMPVideoDecoderParent[%p]::Decode() ERROR; SendDecode() failure.", + this); + return NS_ERROR_FAILURE; + } + mFrameCount++; + + // Async IPC, we don't have access to a return value. + return NS_OK; +} + +nsresult GMPVideoDecoderParent::Reset() { + GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::Reset()", this); + + if (!mIsOpen) { + NS_WARNING("Trying to use an dead GMP video decoder"); + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + + if (!SendReset()) { + return NS_ERROR_FAILURE; + } + + mIsAwaitingResetComplete = true; + + RefPtr<GMPVideoDecoderParent> self(this); + nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction( + "gmp::GMPVideoDecoderParent::Reset", [self]() -> void { + GMP_LOG_DEBUG( + "GMPVideoDecoderParent[%p]::ResetCompleteTimeout() timed out " + "waiting for ResetComplete", + self.get()); + self->mResetCompleteTimeout = nullptr; + LogToBrowserConsole(nsLiteralString( + u"GMPVideoDecoderParent timed out waiting for ResetComplete()")); + }); + CancelResetCompleteTimeout(); + nsCOMPtr<nsISerialEventTarget> target = mPlugin->GMPEventTarget(); + mResetCompleteTimeout = SimpleTimer::Create(task, 5000, target); + + // Async IPC, we don't have access to a return value. + return NS_OK; +} + +void GMPVideoDecoderParent::CancelResetCompleteTimeout() { + if (mResetCompleteTimeout) { + mResetCompleteTimeout->Cancel(); + mResetCompleteTimeout = nullptr; + } +} + +nsresult GMPVideoDecoderParent::Drain() { + GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::Drain() frameCount=%d", this, + mFrameCount); + + if (!mIsOpen) { + NS_WARNING("Trying to use an dead GMP video decoder"); + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + + if (!SendDrain()) { + return NS_ERROR_FAILURE; + } + + mIsAwaitingDrainComplete = true; + + // Async IPC, we don't have access to a return value. + return NS_OK; +} + +nsCString GMPVideoDecoderParent::GetDisplayName() const { + if (NS_WARN_IF(!mIsOpen)) { + return ""_ns; + } + + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + return mPlugin->GetDisplayName(); +} + +// Note: Consider keeping ActorDestroy sync'd up when making changes here. +nsresult GMPVideoDecoderParent::Shutdown() { + GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::Shutdown()", this); + MOZ_ASSERT(!mPlugin || mPlugin->GMPEventTarget()->IsOnCurrentThread()); + + if (mShuttingDown) { + return NS_OK; + } + mShuttingDown = true; + + // Ensure if we've received a shutdown while waiting for a ResetComplete + // or DrainComplete notification, we'll unblock the caller before processing + // the shutdown. + UnblockResetAndDrain(); + + // Notify client we're gone! Won't occur after Close() + if (mCallback) { + mCallback->Terminated(); + mCallback = nullptr; + } + + mIsOpen = false; + if (!mActorDestroyed) { + Unused << SendDecodingComplete(); + } + + return NS_OK; +} + +// Note: Keep this sync'd up with Shutdown +void GMPVideoDecoderParent::ActorDestroy(ActorDestroyReason aWhy) { + GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::ActorDestroy reason=%d", this, + aWhy); + + mIsOpen = false; + mActorDestroyed = true; + + // Ensure if we've received a destroy while waiting for a ResetComplete + // or DrainComplete notification, we'll unblock the caller before processing + // the error. + UnblockResetAndDrain(); + + if (mCallback) { + // May call Close() (and Shutdown()) immediately or with a delay + mCallback->Terminated(); + mCallback = nullptr; + } + if (mPlugin) { + // Ignore any return code. It is OK for this to fail without killing the + // process. + mPlugin->VideoDecoderDestroyed(this); + mPlugin = nullptr; + } + mVideoHost.ActorDestroyed(); + MaybeDisconnect(aWhy == AbnormalShutdown); +} + +mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvDecoded( + const GMPVideoi420FrameData& aDecodedFrame) { + --mFrameCount; + if (aDecodedFrame.mUpdatedTimestamp() && + aDecodedFrame.mUpdatedTimestamp().value() != aDecodedFrame.mTimestamp()) { + GMP_LOG_VERBOSE( + "GMPVideoDecoderParent[%p]::RecvDecoded() timestamp=[%" PRId64 + " -> %" PRId64 "] frameCount=%d", + this, aDecodedFrame.mTimestamp(), + aDecodedFrame.mUpdatedTimestamp().value(), mFrameCount); + } else { + GMP_LOG_VERBOSE( + "GMPVideoDecoderParent[%p]::RecvDecoded() timestamp=%" PRId64 + " frameCount=%d", + this, aDecodedFrame.mTimestamp(), mFrameCount); + } + + if (mCallback) { + if (GMPVideoi420FrameImpl::CheckFrameData(aDecodedFrame)) { + auto f = new GMPVideoi420FrameImpl(aDecodedFrame, &mVideoHost); + + mCallback->Decoded(f); + } else { + GMP_LOG_ERROR( + "GMPVideoDecoderParent[%p]::RecvDecoded() " + "timestamp=%" PRId64 " decoded frame corrupt, ignoring", + this, aDecodedFrame.mTimestamp()); + // TODO: Verify if we should take more serious the arrival of + // a corrupted frame, see bug 1750506. + } + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +GMPVideoDecoderParent::RecvReceivedDecodedReferenceFrame( + const uint64_t& aPictureId) { + if (mCallback) { + mCallback->ReceivedDecodedReferenceFrame(aPictureId); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvReceivedDecodedFrame( + const uint64_t& aPictureId) { + if (mCallback) { + mCallback->ReceivedDecodedFrame(aPictureId); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvInputDataExhausted() { + GMP_LOG_VERBOSE("GMPVideoDecoderParent[%p]::RecvInputDataExhausted()", this); + + if (mCallback) { + mCallback->InputDataExhausted(); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvDrainComplete() { + GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::RecvDrainComplete() frameCount=%d", + this, mFrameCount); + nsAutoString msg; + msg.AppendLiteral( + "GMPVideoDecoderParent::RecvDrainComplete() outstanding frames="); + msg.AppendInt(mFrameCount); + LogToBrowserConsole(msg); + + if (mCallback && mIsAwaitingDrainComplete) { + mIsAwaitingDrainComplete = false; + + mCallback->DrainComplete(); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvResetComplete() { + GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::RecvResetComplete()", this); + + CancelResetCompleteTimeout(); + + if (mCallback && mIsAwaitingResetComplete) { + mIsAwaitingResetComplete = false; + mFrameCount = 0; + + mCallback->ResetComplete(); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvError(const GMPErr& aError) { + GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::RecvError(error=%d)", this, aError); + + if (mCallback) { + // Ensure if we've received an error while waiting for a ResetComplete + // or DrainComplete notification, we'll unblock the caller before processing + // the error. + UnblockResetAndDrain(); + + mCallback->Error(aError); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvShutdown() { + GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::RecvShutdown()", this); + + Shutdown(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvParentShmemForPool( + Shmem&& aEncodedBuffer) { + if (aEncodedBuffer.IsWritable()) { + mVideoHost.SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPEncodedData, + aEncodedBuffer); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvNeedShmem( + const uint32_t& aFrameBufferSize, Shmem* aMem) { + ipc::Shmem mem; + + if (!mVideoHost.SharedMemMgr()->MgrAllocShmem(GMPSharedMem::kGMPFrameData, + aFrameBufferSize, &mem)) { + GMP_LOG_ERROR("%s: Failed to get a shared mem buffer for Child! size %u", + __FUNCTION__, aFrameBufferSize); + return IPC_FAIL(this, "Failed to get a shared mem buffer for Child!"); + } + *aMem = mem; + mem = ipc::Shmem(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoDecoderParent::Recv__delete__() { + GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::Recv__delete__()", this); + + if (mPlugin) { + // Ignore any return code. It is OK for this to fail without killing the + // process. + mPlugin->VideoDecoderDestroyed(this); + mPlugin = nullptr; + } + + return IPC_OK(); +} + +void GMPVideoDecoderParent::UnblockResetAndDrain() { + GMP_LOG_DEBUG( + "GMPVideoDecoderParent[%p]::UnblockResetAndDrain() " + "awaitingResetComplete=%d awaitingDrainComplete=%d", + this, mIsAwaitingResetComplete, mIsAwaitingDrainComplete); + + if (!mCallback) { + MOZ_ASSERT(!mIsAwaitingResetComplete); + MOZ_ASSERT(!mIsAwaitingDrainComplete); + return; + } + if (mIsAwaitingResetComplete) { + mIsAwaitingResetComplete = false; + mCallback->ResetComplete(); + } + if (mIsAwaitingDrainComplete) { + mIsAwaitingDrainComplete = false; + mCallback->DrainComplete(); + } + CancelResetCompleteTimeout(); +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPVideoDecoderParent.h b/dom/media/gmp/GMPVideoDecoderParent.h new file mode 100644 index 0000000000..9265202a63 --- /dev/null +++ b/dom/media/gmp/GMPVideoDecoderParent.h @@ -0,0 +1,103 @@ +/* -*- 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/. */ + +#ifndef GMPVideoDecoderParent_h_ +#define GMPVideoDecoderParent_h_ + +#include "mozilla/RefPtr.h" +#include "gmp-video-decode.h" +#include "mozilla/gmp/PGMPVideoDecoderParent.h" +#include "GMPMessageUtils.h" +#include "GMPSharedMemManager.h" +#include "GMPUtils.h" +#include "GMPVideoHost.h" +#include "GMPVideoDecoderProxy.h" +#include "VideoUtils.h" +#include "GMPCrashHelperHolder.h" + +namespace mozilla::gmp { + +class GMPContentParent; + +class GMPVideoDecoderParent final : public PGMPVideoDecoderParent, + public GMPVideoDecoderProxy, + public GMPSharedMemManager, + public GMPCrashHelperHolder { + friend class PGMPVideoDecoderParent; + + public: + // Mark AddRef and Release as `final`, as they overload pure virtual + // implementations in PGMPVideoDecoderParent. + NS_INLINE_DECL_REFCOUNTING(GMPVideoDecoderParent, final) + + explicit GMPVideoDecoderParent(GMPContentParent* aPlugin); + + GMPVideoHostImpl& Host(); + nsresult Shutdown(); + + // GMPVideoDecoder + void Close() override; + nsresult InitDecode(const GMPVideoCodec& aCodecSettings, + const nsTArray<uint8_t>& aCodecSpecific, + GMPVideoDecoderCallbackProxy* aCallback, + int32_t aCoreCount) override; + nsresult Decode(GMPUniquePtr<GMPVideoEncodedFrame> aInputFrame, + bool aMissingFrames, + const nsTArray<uint8_t>& aCodecSpecificInfo, + int64_t aRenderTimeMs = -1) override; + nsresult Reset() override; + nsresult Drain() override; + uint32_t GetPluginId() const override { return mPluginId; } + GMPPluginType GetPluginType() const override { return mPluginType; } + nsCString GetDisplayName() const override; + + // GMPSharedMemManager + bool Alloc(size_t aSize, Shmem* aMem) override { + return AllocShmem(aSize, aMem); + } + void Dealloc(Shmem&& aMem) override { DeallocShmem(aMem); } + + private: + ~GMPVideoDecoderParent(); + + // PGMPVideoDecoderParent + void ActorDestroy(ActorDestroyReason aWhy) override; + mozilla::ipc::IPCResult RecvDecoded( + const GMPVideoi420FrameData& aDecodedFrame) override; + mozilla::ipc::IPCResult RecvReceivedDecodedReferenceFrame( + const uint64_t& aPictureId) override; + mozilla::ipc::IPCResult RecvReceivedDecodedFrame( + const uint64_t& aPictureId) override; + mozilla::ipc::IPCResult RecvInputDataExhausted() override; + mozilla::ipc::IPCResult RecvDrainComplete() override; + mozilla::ipc::IPCResult RecvResetComplete() override; + mozilla::ipc::IPCResult RecvError(const GMPErr& aError) override; + mozilla::ipc::IPCResult RecvShutdown() override; + mozilla::ipc::IPCResult RecvParentShmemForPool( + Shmem&& aEncodedBuffer) override; + mozilla::ipc::IPCResult RecvNeedShmem(const uint32_t& aFrameBufferSize, + Shmem* aMem) override; + mozilla::ipc::IPCResult Recv__delete__() override; + + void UnblockResetAndDrain(); + void CancelResetCompleteTimeout(); + + bool mIsOpen; + bool mShuttingDown; + bool mActorDestroyed; + bool mIsAwaitingResetComplete; + bool mIsAwaitingDrainComplete; + RefPtr<GMPContentParent> mPlugin; + RefPtr<GMPVideoDecoderCallbackProxy> mCallback; + GMPVideoHostImpl mVideoHost; + const uint32_t mPluginId; + GMPPluginType mPluginType = GMPPluginType::Unknown; + int32_t mFrameCount; + RefPtr<SimpleTimer> mResetCompleteTimeout; +}; + +} // namespace mozilla::gmp + +#endif // GMPVideoDecoderParent_h_ diff --git a/dom/media/gmp/GMPVideoDecoderProxy.h b/dom/media/gmp/GMPVideoDecoderProxy.h new file mode 100644 index 0000000000..e12668755c --- /dev/null +++ b/dom/media/gmp/GMPVideoDecoderProxy.h @@ -0,0 +1,57 @@ +/* -*- 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/. */ + +#ifndef GMPVideoDecoderProxy_h_ +#define GMPVideoDecoderProxy_h_ + +#include "nsTArray.h" +#include "gmp-video-decode.h" +#include "gmp-video-frame-i420.h" +#include "gmp-video-frame-encoded.h" + +#include "GMPCallbackBase.h" +#include "GMPNativeTypes.h" +#include "GMPUtils.h" + +class GMPVideoDecoderCallbackProxy : public GMPCallbackBase, + public GMPVideoDecoderCallback { + public: + virtual ~GMPVideoDecoderCallbackProxy() = default; +}; + +// A proxy to GMPVideoDecoder in the child process. +// GMPVideoDecoderParent exposes this to users the GMP. +// This enables Gecko to pass nsTArrays to the child GMP and avoid +// an extra copy when doing so. + +// The consumer must call Close() when done with the codec, or when +// Terminated() is called by the GMP plugin indicating an abnormal shutdown +// of the underlying plugin. After calling Close(), the consumer must +// not access this again. + +// This interface is not thread-safe and must only be used from GMPThread. +class GMPVideoDecoderProxy { + public: + virtual nsresult InitDecode(const GMPVideoCodec& aCodecSettings, + const nsTArray<uint8_t>& aCodecSpecific, + GMPVideoDecoderCallbackProxy* aCallback, + int32_t aCoreCount) = 0; + virtual nsresult Decode( + mozilla::GMPUniquePtr<GMPVideoEncodedFrame> aInputFrame, + bool aMissingFrames, const nsTArray<uint8_t>& aCodecSpecificInfo, + int64_t aRenderTimeMs = -1) = 0; + virtual nsresult Reset() = 0; + virtual nsresult Drain() = 0; + virtual uint32_t GetPluginId() const = 0; + virtual GMPPluginType GetPluginType() const = 0; + + // Call to tell GMP/plugin the consumer will no longer use this + // interface/codec. + virtual void Close() = 0; + + virtual nsCString GetDisplayName() const = 0; +}; + +#endif diff --git a/dom/media/gmp/GMPVideoEncodedFrameImpl.cpp b/dom/media/gmp/GMPVideoEncodedFrameImpl.cpp new file mode 100644 index 0000000000..c0ed87df4d --- /dev/null +++ b/dom/media/gmp/GMPVideoEncodedFrameImpl.cpp @@ -0,0 +1,226 @@ +/* -*- 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 "GMPVideoEncodedFrameImpl.h" +#include "GMPVideoHost.h" +#include "mozilla/gmp/GMPTypes.h" +#include "GMPSharedMemManager.h" + +namespace mozilla::gmp { + +GMPVideoEncodedFrameImpl::GMPVideoEncodedFrameImpl(GMPVideoHostImpl* aHost) + : mEncodedWidth(0), + mEncodedHeight(0), + mTimeStamp(0ll), + mDuration(0ll), + mFrameType(kGMPDeltaFrame), + mSize(0), + mCompleteFrame(false), + mHost(aHost), + mBufferType(GMP_BufferSingle) { + MOZ_ASSERT(aHost); + aHost->EncodedFrameCreated(this); +} + +GMPVideoEncodedFrameImpl::GMPVideoEncodedFrameImpl( + const GMPVideoEncodedFrameData& aFrameData, GMPVideoHostImpl* aHost) + : mEncodedWidth(aFrameData.mEncodedWidth()), + mEncodedHeight(aFrameData.mEncodedHeight()), + mTimeStamp(aFrameData.mTimestamp()), + mDuration(aFrameData.mDuration()), + mFrameType(static_cast<GMPVideoFrameType>(aFrameData.mFrameType())), + mSize(aFrameData.mSize()), + mCompleteFrame(aFrameData.mCompleteFrame()), + mHost(aHost), + mBuffer(aFrameData.mBuffer()), + mBufferType(aFrameData.mBufferType()) { + MOZ_ASSERT(aHost); + aHost->EncodedFrameCreated(this); +} + +GMPVideoEncodedFrameImpl::~GMPVideoEncodedFrameImpl() { + DestroyBuffer(); + if (mHost) { + mHost->EncodedFrameDestroyed(this); + } +} + +GMPVideoFrameFormat GMPVideoEncodedFrameImpl::GetFrameFormat() { + return kGMPEncodedVideoFrame; +} + +void GMPVideoEncodedFrameImpl::DoneWithAPI() { + DestroyBuffer(); + + // Do this after destroying the buffer because destruction + // involves deallocation, which requires a host. + mHost = nullptr; +} + +void GMPVideoEncodedFrameImpl::ActorDestroyed() { + // Simply clear out Shmem reference, do not attempt to + // properly free it. It has already been freed. + mBuffer = ipc::Shmem(); + // No more host. + mHost = nullptr; +} + +bool GMPVideoEncodedFrameImpl::RelinquishFrameData( + GMPVideoEncodedFrameData& aFrameData) { + aFrameData.mEncodedWidth() = mEncodedWidth; + aFrameData.mEncodedHeight() = mEncodedHeight; + aFrameData.mTimestamp() = mTimeStamp; + aFrameData.mDuration() = mDuration; + aFrameData.mFrameType() = mFrameType; + aFrameData.mSize() = mSize; + aFrameData.mCompleteFrame() = mCompleteFrame; + aFrameData.mBuffer() = mBuffer; + aFrameData.mBufferType() = mBufferType; + + // This method is called right before Shmem is sent to another process. + // We need to effectively zero out our member copy so that we don't + // try to delete Shmem we don't own later. + mBuffer = ipc::Shmem(); + + return true; +} + +void GMPVideoEncodedFrameImpl::DestroyBuffer() { + if (mHost && mBuffer.IsWritable()) { + mHost->SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPEncodedData, + mBuffer); + } + mBuffer = ipc::Shmem(); +} + +GMPErr GMPVideoEncodedFrameImpl::CreateEmptyFrame(uint32_t aSize) { + if (aSize == 0) { + DestroyBuffer(); + } else if (aSize > AllocatedSize()) { + DestroyBuffer(); + if (!mHost->SharedMemMgr()->MgrAllocShmem(GMPSharedMem::kGMPEncodedData, + aSize, &mBuffer) || + !Buffer()) { + return GMPAllocErr; + } + } + mSize = aSize; + + return GMPNoErr; +} + +GMPErr GMPVideoEncodedFrameImpl::CopyFrame(const GMPVideoEncodedFrame& aFrame) { + auto& f = static_cast<const GMPVideoEncodedFrameImpl&>(aFrame); + + if (f.mSize != 0) { + GMPErr err = CreateEmptyFrame(f.mSize); + if (err != GMPNoErr) { + return err; + } + memcpy(Buffer(), f.Buffer(), f.mSize); + } + mEncodedWidth = f.mEncodedWidth; + mEncodedHeight = f.mEncodedHeight; + mTimeStamp = f.mTimeStamp; + mDuration = f.mDuration; + mFrameType = f.mFrameType; + mSize = f.mSize; // already set... + mCompleteFrame = f.mCompleteFrame; + mBufferType = f.mBufferType; + // Don't copy host, that should have been set properly on object creation via + // host. + + return GMPNoErr; +} + +void GMPVideoEncodedFrameImpl::SetEncodedWidth(uint32_t aEncodedWidth) { + mEncodedWidth = aEncodedWidth; +} + +uint32_t GMPVideoEncodedFrameImpl::EncodedWidth() { return mEncodedWidth; } + +void GMPVideoEncodedFrameImpl::SetEncodedHeight(uint32_t aEncodedHeight) { + mEncodedHeight = aEncodedHeight; +} + +uint32_t GMPVideoEncodedFrameImpl::EncodedHeight() { return mEncodedHeight; } + +void GMPVideoEncodedFrameImpl::SetTimeStamp(uint64_t aTimeStamp) { + mTimeStamp = aTimeStamp; +} + +uint64_t GMPVideoEncodedFrameImpl::TimeStamp() { return mTimeStamp; } + +void GMPVideoEncodedFrameImpl::SetDuration(uint64_t aDuration) { + mDuration = aDuration; +} + +uint64_t GMPVideoEncodedFrameImpl::Duration() const { return mDuration; } + +void GMPVideoEncodedFrameImpl::SetFrameType(GMPVideoFrameType aFrameType) { + mFrameType = aFrameType; +} + +GMPVideoFrameType GMPVideoEncodedFrameImpl::FrameType() { return mFrameType; } + +void GMPVideoEncodedFrameImpl::SetAllocatedSize(uint32_t aNewSize) { + if (aNewSize <= AllocatedSize()) { + return; + } + + if (!mHost) { + return; + } + + ipc::Shmem new_mem; + if (!mHost->SharedMemMgr()->MgrAllocShmem(GMPSharedMem::kGMPEncodedData, + aNewSize, &new_mem) || + !new_mem.get<uint8_t>()) { + return; + } + + if (mBuffer.IsReadable()) { + memcpy(new_mem.get<uint8_t>(), Buffer(), mSize); + } + + DestroyBuffer(); + + mBuffer = new_mem; +} + +uint32_t GMPVideoEncodedFrameImpl::AllocatedSize() { + if (mBuffer.IsWritable()) { + return mBuffer.Size<uint8_t>(); + } + return 0; +} + +void GMPVideoEncodedFrameImpl::SetSize(uint32_t aSize) { mSize = aSize; } + +uint32_t GMPVideoEncodedFrameImpl::Size() { return mSize; } + +void GMPVideoEncodedFrameImpl::SetCompleteFrame(bool aCompleteFrame) { + mCompleteFrame = aCompleteFrame; +} + +bool GMPVideoEncodedFrameImpl::CompleteFrame() { return mCompleteFrame; } + +const uint8_t* GMPVideoEncodedFrameImpl::Buffer() const { + return mBuffer.get<uint8_t>(); +} + +uint8_t* GMPVideoEncodedFrameImpl::Buffer() { return mBuffer.get<uint8_t>(); } + +void GMPVideoEncodedFrameImpl::Destroy() { delete this; } + +GMPBufferType GMPVideoEncodedFrameImpl::BufferType() const { + return mBufferType; +} + +void GMPVideoEncodedFrameImpl::SetBufferType(GMPBufferType aBufferType) { + mBufferType = aBufferType; +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPVideoEncodedFrameImpl.h b/dom/media/gmp/GMPVideoEncodedFrameImpl.h new file mode 100644 index 0000000000..fbb5f92146 --- /dev/null +++ b/dom/media/gmp/GMPVideoEncodedFrameImpl.h @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2014, Mozilla Corporation + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMPVideoEncodedFrameImpl_h_ +#define GMPVideoEncodedFrameImpl_h_ + +#include "gmp-errors.h" +#include "gmp-video-frame.h" +#include "gmp-video-frame-encoded.h" +#include "mozilla/ipc/Shmem.h" + +namespace mozilla { +class CryptoSample; + +namespace gmp { + +class GMPVideoHostImpl; +class GMPVideoEncodedFrameData; + +class GMPVideoEncodedFrameImpl : public GMPVideoEncodedFrame { + friend struct IPC::ParamTraits<mozilla::gmp::GMPVideoEncodedFrameImpl>; + + public: + explicit GMPVideoEncodedFrameImpl(GMPVideoHostImpl* aHost); + GMPVideoEncodedFrameImpl(const GMPVideoEncodedFrameData& aFrameData, + GMPVideoHostImpl* aHost); + virtual ~GMPVideoEncodedFrameImpl(); + + // This is called during a normal destroy sequence, which is + // when a consumer is finished or during XPCOM shutdown. + void DoneWithAPI(); + // Does not attempt to release Shmem, as the Shmem has already been released. + void ActorDestroyed(); + + bool RelinquishFrameData(GMPVideoEncodedFrameData& aFrameData); + + // GMPVideoFrame + GMPVideoFrameFormat GetFrameFormat() override; + void Destroy() override; + + // GMPVideoEncodedFrame + GMPErr CreateEmptyFrame(uint32_t aSize) override; + GMPErr CopyFrame(const GMPVideoEncodedFrame& aFrame) override; + void SetEncodedWidth(uint32_t aEncodedWidth) override; + uint32_t EncodedWidth() override; + void SetEncodedHeight(uint32_t aEncodedHeight) override; + uint32_t EncodedHeight() override; + // Microseconds + void SetTimeStamp(uint64_t aTimeStamp) override; + uint64_t TimeStamp() override; + // Set frame duration (microseconds) + // NOTE: next-frame's Timestamp() != this-frame's TimeStamp()+Duration() + // depending on rounding to avoid having to track roundoff errors + // and dropped/missing frames(!) (which may leave a large gap) + void SetDuration(uint64_t aDuration) override; + uint64_t Duration() const override; + void SetFrameType(GMPVideoFrameType aFrameType) override; + GMPVideoFrameType FrameType() override; + void SetAllocatedSize(uint32_t aNewSize) override; + uint32_t AllocatedSize() override; + void SetSize(uint32_t aSize) override; + uint32_t Size() override; + void SetCompleteFrame(bool aCompleteFrame) override; + bool CompleteFrame() override; + const uint8_t* Buffer() const override; + uint8_t* Buffer() override; + GMPBufferType BufferType() const override; + void SetBufferType(GMPBufferType aBufferType) override; + + private: + void DestroyBuffer(); + + uint32_t mEncodedWidth; + uint32_t mEncodedHeight; + uint64_t mTimeStamp; + uint64_t mDuration; + GMPVideoFrameType mFrameType; + uint32_t mSize; + bool mCompleteFrame; + GMPVideoHostImpl* mHost; + ipc::Shmem mBuffer; + GMPBufferType mBufferType; +}; + +} // namespace gmp + +} // namespace mozilla + +#endif // GMPVideoEncodedFrameImpl_h_ diff --git a/dom/media/gmp/GMPVideoEncoderChild.cpp b/dom/media/gmp/GMPVideoEncoderChild.cpp new file mode 100644 index 0000000000..19a96b5efe --- /dev/null +++ b/dom/media/gmp/GMPVideoEncoderChild.cpp @@ -0,0 +1,203 @@ +/* -*- 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 "GMPVideoEncoderChild.h" +#include "GMPContentChild.h" +#include <stdio.h> +#include "mozilla/Unused.h" +#include "GMPVideoEncodedFrameImpl.h" +#include "GMPVideoi420FrameImpl.h" +#include "runnable_utils.h" + +namespace mozilla::gmp { + +GMPVideoEncoderChild::GMPVideoEncoderChild(GMPContentChild* aPlugin) + : GMPSharedMemManager(aPlugin), + mPlugin(aPlugin), + mVideoEncoder(nullptr), + mVideoHost(this), + mNeedShmemIntrCount(0), + mPendingEncodeComplete(false) { + MOZ_ASSERT(mPlugin); +} + +GMPVideoEncoderChild::~GMPVideoEncoderChild() { + MOZ_ASSERT(!mNeedShmemIntrCount); +} + +void GMPVideoEncoderChild::Init(GMPVideoEncoder* aEncoder) { + MOZ_ASSERT(aEncoder, + "Cannot initialize video encoder child without a video encoder!"); + mVideoEncoder = aEncoder; +} + +GMPVideoHostImpl& GMPVideoEncoderChild::Host() { return mVideoHost; } + +void GMPVideoEncoderChild::Encoded(GMPVideoEncodedFrame* aEncodedFrame, + const uint8_t* aCodecSpecificInfo, + uint32_t aCodecSpecificInfoLength) { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + auto ef = static_cast<GMPVideoEncodedFrameImpl*>(aEncodedFrame); + + GMPVideoEncodedFrameData frameData; + ef->RelinquishFrameData(frameData); + + nsTArray<uint8_t> codecSpecific; + codecSpecific.AppendElements(aCodecSpecificInfo, aCodecSpecificInfoLength); + SendEncoded(frameData, codecSpecific); + + aEncodedFrame->Destroy(); +} + +void GMPVideoEncoderChild::Error(GMPErr aError) { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + SendError(aError); +} + +mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvInitEncode( + const GMPVideoCodec& aCodecSettings, nsTArray<uint8_t>&& aCodecSpecific, + const int32_t& aNumberOfCores, const uint32_t& aMaxPayloadSize) { + if (!mVideoEncoder) { + return IPC_FAIL(this, "!mVideoDecoder"); + } + + // Ignore any return code. It is OK for this to fail without killing the + // process. + mVideoEncoder->InitEncode(aCodecSettings, aCodecSpecific.Elements(), + aCodecSpecific.Length(), this, aNumberOfCores, + aMaxPayloadSize); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvEncode( + const GMPVideoi420FrameData& aInputFrame, + nsTArray<uint8_t>&& aCodecSpecificInfo, + nsTArray<GMPVideoFrameType>&& aFrameTypes) { + if (!mVideoEncoder) { + return IPC_FAIL(this, "!mVideoDecoder"); + } + + auto f = new GMPVideoi420FrameImpl(aInputFrame, &mVideoHost); + + // Ignore any return code. It is OK for this to fail without killing the + // process. + mVideoEncoder->Encode(f, aCodecSpecificInfo.Elements(), + aCodecSpecificInfo.Length(), aFrameTypes.Elements(), + aFrameTypes.Length()); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvChildShmemForPool( + Shmem&& aEncodedBuffer) { + if (aEncodedBuffer.IsWritable()) { + mVideoHost.SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPEncodedData, + aEncodedBuffer); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvSetChannelParameters( + const uint32_t& aPacketLoss, const uint32_t& aRTT) { + if (!mVideoEncoder) { + return IPC_FAIL(this, "!mVideoDecoder"); + } + + // Ignore any return code. It is OK for this to fail without killing the + // process. + mVideoEncoder->SetChannelParameters(aPacketLoss, aRTT); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvSetRates( + const uint32_t& aNewBitRate, const uint32_t& aFrameRate) { + if (!mVideoEncoder) { + return IPC_FAIL(this, "!mVideoDecoder"); + } + + // Ignore any return code. It is OK for this to fail without killing the + // process. + mVideoEncoder->SetRates(aNewBitRate, aFrameRate); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvSetPeriodicKeyFrames( + const bool& aEnable) { + if (!mVideoEncoder) { + return IPC_FAIL(this, "!mVideoDecoder"); + } + + // Ignore any return code. It is OK for this to fail without killing the + // process. + mVideoEncoder->SetPeriodicKeyFrames(aEnable); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvEncodingComplete() { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + if (mNeedShmemIntrCount) { + // There's a GMP blocked in Alloc() waiting for the CallNeedShem() to + // return a frame they can use. Don't call the GMP's EncodingComplete() + // now and don't delete the GMPVideoEncoderChild, defer processing the + // EncodingComplete() until once the Alloc() finishes. + mPendingEncodeComplete = true; + return IPC_OK(); + } + + if (!mVideoEncoder) { + // There is not much to clean up anymore. + Unused << Send__delete__(this); + return IPC_OK(); + } + + // Ignore any return code. It is OK for this to fail without killing the + // process. + mVideoEncoder->EncodingComplete(); + + mVideoHost.DoneWithAPI(); + + mPlugin = nullptr; + + Unused << Send__delete__(this); + + return IPC_OK(); +} + +bool GMPVideoEncoderChild::Alloc(size_t aSize, Shmem* aMem) { + MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current()); + + bool rv; +#ifndef SHMEM_ALLOC_IN_CHILD + ++mNeedShmemIntrCount; + rv = SendNeedShmem(aSize, aMem); + --mNeedShmemIntrCount; + if (mPendingEncodeComplete && mNeedShmemIntrCount == 0) { + mPendingEncodeComplete = false; + mPlugin->GMPMessageLoop()->PostTask( + NewRunnableMethod("gmp::GMPVideoEncoderChild::RecvEncodingComplete", + this, &GMPVideoEncoderChild::RecvEncodingComplete)); + } +#else + rv = AllocShmem(aSize, aMem); +#endif + return rv; +} + +void GMPVideoEncoderChild::Dealloc(Shmem&& aMem) { +#ifndef SHMEM_ALLOC_IN_CHILD + SendParentShmemForPool(std::move(aMem)); +#else + DeallocShmem(aMem); +#endif +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPVideoEncoderChild.h b/dom/media/gmp/GMPVideoEncoderChild.h new file mode 100644 index 0000000000..dd3c0fdf37 --- /dev/null +++ b/dom/media/gmp/GMPVideoEncoderChild.h @@ -0,0 +1,75 @@ +/* -*- 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/. */ + +#ifndef GMPVideoEncoderChild_h_ +#define GMPVideoEncoderChild_h_ + +#include "nsString.h" +#include "mozilla/gmp/PGMPVideoEncoderChild.h" +#include "gmp-video-encode.h" +#include "GMPSharedMemManager.h" +#include "GMPVideoHost.h" + +namespace mozilla::gmp { + +class GMPContentChild; + +class GMPVideoEncoderChild : public PGMPVideoEncoderChild, + public GMPVideoEncoderCallback, + public GMPSharedMemManager { + friend class PGMPVideoEncoderChild; + + public: + // Mark AddRef and Release as `final`, as they overload pure virtual + // implementations in PGMPVideoEncoderChild. + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPVideoEncoderChild, final); + + explicit GMPVideoEncoderChild(GMPContentChild* aPlugin); + + void Init(GMPVideoEncoder* aEncoder); + GMPVideoHostImpl& Host(); + + // GMPVideoEncoderCallback + void Encoded(GMPVideoEncodedFrame* aEncodedFrame, + const uint8_t* aCodecSpecificInfo, + uint32_t aCodecSpecificInfoLength) override; + void Error(GMPErr aError) override; + + // GMPSharedMemManager + bool Alloc(size_t aSize, Shmem* aMem) override; + void Dealloc(Shmem&& aMem) override; + + private: + virtual ~GMPVideoEncoderChild(); + + // PGMPVideoEncoderChild + mozilla::ipc::IPCResult RecvInitEncode(const GMPVideoCodec& aCodecSettings, + nsTArray<uint8_t>&& aCodecSpecific, + const int32_t& aNumberOfCores, + const uint32_t& aMaxPayloadSize); + mozilla::ipc::IPCResult RecvEncode(const GMPVideoi420FrameData& aInputFrame, + nsTArray<uint8_t>&& aCodecSpecificInfo, + nsTArray<GMPVideoFrameType>&& aFrameTypes); + mozilla::ipc::IPCResult RecvChildShmemForPool(Shmem&& aEncodedBuffer); + mozilla::ipc::IPCResult RecvSetChannelParameters(const uint32_t& aPacketLoss, + const uint32_t& aRTT); + mozilla::ipc::IPCResult RecvSetRates(const uint32_t& aNewBitRate, + const uint32_t& aFrameRate); + mozilla::ipc::IPCResult RecvSetPeriodicKeyFrames(const bool& aEnable); + mozilla::ipc::IPCResult RecvEncodingComplete(); + + GMPContentChild* mPlugin; + GMPVideoEncoder* mVideoEncoder; + GMPVideoHostImpl mVideoHost; + + // Non-zero when a GMP is blocked spinning the IPC message loop while + // waiting on an NeedShmem to complete. + int mNeedShmemIntrCount; + bool mPendingEncodeComplete; +}; + +} // namespace mozilla::gmp + +#endif // GMPVideoEncoderChild_h_ diff --git a/dom/media/gmp/GMPVideoEncoderParent.cpp b/dom/media/gmp/GMPVideoEncoderParent.cpp new file mode 100644 index 0000000000..21c86ff9ab --- /dev/null +++ b/dom/media/gmp/GMPVideoEncoderParent.cpp @@ -0,0 +1,307 @@ +/* -*- 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 "GMPVideoEncoderParent.h" + +#include "GMPContentParent.h" +#include "GMPCrashHelper.h" +#include "GMPLog.h" +#include "GMPMessageUtils.h" +#include "GMPVideoEncodedFrameImpl.h" +#include "GMPVideoi420FrameImpl.h" +#include "mozilla/gmp/GMPTypes.h" +#include "mozilla/Unused.h" +#include "nsAutoRef.h" +#include "nsThread.h" +#include "nsThreadUtils.h" +#include "runnable_utils.h" + +namespace mozilla::gmp { + +#ifdef __CLASS__ +# undef __CLASS__ +#endif +#define __CLASS__ "GMPVideoEncoderParent" + +// States: +// Initial: mIsOpen == false +// on InitDecode success -> Open +// on Shutdown -> Dead +// Open: mIsOpen == true +// on Close -> Dead +// on ActorDestroy -> Dead +// on Shutdown -> Dead +// Dead: mIsOpen == false + +GMPVideoEncoderParent::GMPVideoEncoderParent(GMPContentParent* aPlugin) + : GMPSharedMemManager(aPlugin), + mIsOpen(false), + mShuttingDown(false), + mActorDestroyed(false), + mPlugin(aPlugin), + mCallback(nullptr), + mVideoHost(this), + mPluginId(aPlugin->GetPluginId()) { + MOZ_ASSERT(mPlugin); +} + +GMPVideoHostImpl& GMPVideoEncoderParent::Host() { return mVideoHost; } + +// Note: may be called via Terminated() +void GMPVideoEncoderParent::Close() { + GMP_LOG_DEBUG("%s::%s: %p", __CLASS__, __FUNCTION__, this); + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + // Consumer is done with us; we can shut down. No more callbacks should + // be made to mCallback. Note: do this before Shutdown()! + mCallback = nullptr; + + // Let Shutdown mark us as dead so it knows if we had been alive + + // In case this is the last reference + RefPtr<GMPVideoEncoderParent> kungfudeathgrip(this); + Release(); + Shutdown(); +} + +GMPErr GMPVideoEncoderParent::InitEncode( + const GMPVideoCodec& aCodecSettings, + const nsTArray<uint8_t>& aCodecSpecific, + GMPVideoEncoderCallbackProxy* aCallback, int32_t aNumberOfCores, + uint32_t aMaxPayloadSize) { + GMP_LOG_DEBUG("%s::%s: %p", __CLASS__, __FUNCTION__, this); + if (mIsOpen) { + NS_WARNING("Trying to re-init an in-use GMP video encoder!"); + return GMPGenericErr; + ; + } + + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + MOZ_ASSERT(!mCallback); + + if (!aCallback) { + return GMPGenericErr; + } + mCallback = aCallback; + + if (!SendInitEncode(aCodecSettings, aCodecSpecific, aNumberOfCores, + aMaxPayloadSize)) { + return GMPGenericErr; + } + mIsOpen = true; + + // Async IPC, we don't have access to a return value. + return GMPNoErr; +} + +GMPErr GMPVideoEncoderParent::Encode( + GMPUniquePtr<GMPVideoi420Frame> aInputFrame, + const nsTArray<uint8_t>& aCodecSpecificInfo, + const nsTArray<GMPVideoFrameType>& aFrameTypes) { + if (!mIsOpen) { + NS_WARNING("Trying to use an dead GMP video encoder"); + return GMPGenericErr; + } + + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + + GMPUniquePtr<GMPVideoi420FrameImpl> inputFrameImpl( + static_cast<GMPVideoi420FrameImpl*>(aInputFrame.release())); + + // Very rough kill-switch if the plugin stops processing. If it's merely + // hung and continues, we'll come back to life eventually. + // 3* is because we're using 3 buffers per frame for i420 data for now. + if ((NumInUse(GMPSharedMem::kGMPFrameData) > + 3 * GMPSharedMem::kGMPBufLimit) || + (NumInUse(GMPSharedMem::kGMPEncodedData) > GMPSharedMem::kGMPBufLimit)) { + GMP_LOG_ERROR( + "%s::%s: Out of mem buffers. Frame Buffers:%lu Max:%lu, Encoded " + "Buffers: %lu Max: %lu", + __CLASS__, __FUNCTION__, + static_cast<unsigned long>(NumInUse(GMPSharedMem::kGMPFrameData)), + static_cast<unsigned long>(3 * GMPSharedMem::kGMPBufLimit), + static_cast<unsigned long>(NumInUse(GMPSharedMem::kGMPEncodedData)), + static_cast<unsigned long>(GMPSharedMem::kGMPBufLimit)); + return GMPGenericErr; + } + + GMPVideoi420FrameData frameData; + inputFrameImpl->InitFrameData(frameData); + + if (!SendEncode(frameData, aCodecSpecificInfo, aFrameTypes)) { + GMP_LOG_ERROR("%s::%s: failed to send encode", __CLASS__, __FUNCTION__); + return GMPGenericErr; + } + + // Async IPC, we don't have access to a return value. + return GMPNoErr; +} + +GMPErr GMPVideoEncoderParent::SetChannelParameters(uint32_t aPacketLoss, + uint32_t aRTT) { + if (!mIsOpen) { + NS_WARNING("Trying to use an invalid GMP video encoder!"); + return GMPGenericErr; + } + + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + + if (!SendSetChannelParameters(aPacketLoss, aRTT)) { + return GMPGenericErr; + } + + // Async IPC, we don't have access to a return value. + return GMPNoErr; +} + +GMPErr GMPVideoEncoderParent::SetRates(uint32_t aNewBitRate, + uint32_t aFrameRate) { + if (!mIsOpen) { + NS_WARNING("Trying to use an dead GMP video decoder"); + return GMPGenericErr; + } + + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + + if (!SendSetRates(aNewBitRate, aFrameRate)) { + return GMPGenericErr; + } + + // Async IPC, we don't have access to a return value. + return GMPNoErr; +} + +GMPErr GMPVideoEncoderParent::SetPeriodicKeyFrames(bool aEnable) { + if (!mIsOpen) { + NS_WARNING("Trying to use an invalid GMP video encoder!"); + return GMPGenericErr; + } + + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + + if (!SendSetPeriodicKeyFrames(aEnable)) { + return GMPGenericErr; + } + + // Async IPC, we don't have access to a return value. + return GMPNoErr; +} + +// Note: Consider keeping ActorDestroy sync'd up when making changes here. +void GMPVideoEncoderParent::Shutdown() { + GMP_LOG_DEBUG("%s::%s: %p", __CLASS__, __FUNCTION__, this); + MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread()); + + if (mShuttingDown) { + return; + } + mShuttingDown = true; + + // Notify client we're gone! Won't occur after Close() + if (mCallback) { + mCallback->Terminated(); + mCallback = nullptr; + } + + mIsOpen = false; + if (!mActorDestroyed) { + Unused << SendEncodingComplete(); + } +} + +// Note: Keep this sync'd up with Shutdown +void GMPVideoEncoderParent::ActorDestroy(ActorDestroyReason aWhy) { + GMP_LOG_DEBUG("%s::%s: %p (%d)", __CLASS__, __FUNCTION__, this, (int)aWhy); + mIsOpen = false; + mActorDestroyed = true; + if (mCallback) { + // May call Close() (and Shutdown()) immediately or with a delay + mCallback->Terminated(); + mCallback = nullptr; + } + if (mPlugin) { + // Ignore any return code. It is OK for this to fail without killing the + // process. + mPlugin->VideoEncoderDestroyed(this); + mPlugin = nullptr; + } + mVideoHost.ActorDestroyed(); // same as DoneWithAPI + MaybeDisconnect(aWhy == AbnormalShutdown); +} + +mozilla::ipc::IPCResult GMPVideoEncoderParent::RecvEncoded( + const GMPVideoEncodedFrameData& aEncodedFrame, + nsTArray<uint8_t>&& aCodecSpecificInfo) { + if (mCallback) { + auto f = new GMPVideoEncodedFrameImpl(aEncodedFrame, &mVideoHost); + mCallback->Encoded(f, aCodecSpecificInfo); + f->Destroy(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoEncoderParent::RecvError(const GMPErr& aError) { + if (mCallback) { + mCallback->Error(aError); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoEncoderParent::RecvShutdown() { + Shutdown(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoEncoderParent::RecvParentShmemForPool( + Shmem&& aFrameBuffer) { + if (aFrameBuffer.IsWritable()) { + // This test may be paranoia now that we don't shut down the VideoHost + // in ::Shutdown, but doesn't hurt + if (mVideoHost.SharedMemMgr()) { + mVideoHost.SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPFrameData, + aFrameBuffer); + } else { + GMP_LOG_DEBUG( + "%s::%s: %p Called in shutdown, ignoring and freeing directly", + __CLASS__, __FUNCTION__, this); + DeallocShmem(aFrameBuffer); + } + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoEncoderParent::RecvNeedShmem( + const uint32_t& aEncodedBufferSize, Shmem* aMem) { + ipc::Shmem mem; + + // This test may be paranoia now that we don't shut down the VideoHost + // in ::Shutdown, but doesn't hurt + if (!mVideoHost.SharedMemMgr() || + !mVideoHost.SharedMemMgr()->MgrAllocShmem(GMPSharedMem::kGMPEncodedData, + aEncodedBufferSize, &mem)) { + GMP_LOG_ERROR( + "%s::%s: Failed to get a shared mem buffer for Child! size %u", + __CLASS__, __FUNCTION__, aEncodedBufferSize); + return IPC_FAIL(this, "Failed to get a shared mem buffer for Child!"); + } + *aMem = mem; + mem = ipc::Shmem(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult GMPVideoEncoderParent::Recv__delete__() { + if (mPlugin) { + // Ignore any return code. It is OK for this to fail without killing the + // process. + mPlugin->VideoEncoderDestroyed(this); + mPlugin = nullptr; + } + + return IPC_OK(); +} + +} // namespace mozilla::gmp + +#undef __CLASS__ diff --git a/dom/media/gmp/GMPVideoEncoderParent.h b/dom/media/gmp/GMPVideoEncoderParent.h new file mode 100644 index 0000000000..c5c81194ec --- /dev/null +++ b/dom/media/gmp/GMPVideoEncoderParent.h @@ -0,0 +1,85 @@ +/* -*- 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/. */ + +#ifndef GMPVideoEncoderParent_h_ +#define GMPVideoEncoderParent_h_ + +#include "mozilla/RefPtr.h" +#include "gmp-video-encode.h" +#include "mozilla/gmp/PGMPVideoEncoderParent.h" +#include "GMPMessageUtils.h" +#include "GMPSharedMemManager.h" +#include "GMPUtils.h" +#include "GMPVideoHost.h" +#include "GMPVideoEncoderProxy.h" +#include "GMPCrashHelperHolder.h" + +namespace mozilla::gmp { + +class GMPContentParent; + +class GMPVideoEncoderParent : public GMPVideoEncoderProxy, + public PGMPVideoEncoderParent, + public GMPSharedMemManager, + public GMPCrashHelperHolder { + friend class PGMPVideoEncoderParent; + + public: + // Mark AddRef and Release as `final`, as they overload pure virtual + // implementations in PGMPVideoEncoderParent. + NS_INLINE_DECL_REFCOUNTING(GMPVideoEncoderParent, final) + + explicit GMPVideoEncoderParent(GMPContentParent* aPlugin); + + GMPVideoHostImpl& Host(); + void Shutdown(); + + // GMPVideoEncoderProxy + void Close() override; + GMPErr InitEncode(const GMPVideoCodec& aCodecSettings, + const nsTArray<uint8_t>& aCodecSpecific, + GMPVideoEncoderCallbackProxy* aCallback, + int32_t aNumberOfCores, uint32_t aMaxPayloadSize) override; + GMPErr Encode(GMPUniquePtr<GMPVideoi420Frame> aInputFrame, + const nsTArray<uint8_t>& aCodecSpecificInfo, + const nsTArray<GMPVideoFrameType>& aFrameTypes) override; + GMPErr SetChannelParameters(uint32_t aPacketLoss, uint32_t aRTT) override; + GMPErr SetRates(uint32_t aNewBitRate, uint32_t aFrameRate) override; + GMPErr SetPeriodicKeyFrames(bool aEnable) override; + uint32_t GetPluginId() const override { return mPluginId; } + + // GMPSharedMemManager + bool Alloc(size_t aSize, Shmem* aMem) override { + return AllocShmem(aSize, aMem); + } + void Dealloc(Shmem&& aMem) override { DeallocShmem(aMem); } + + private: + virtual ~GMPVideoEncoderParent() = default; + + // PGMPVideoEncoderParent + void ActorDestroy(ActorDestroyReason aWhy) override; + mozilla::ipc::IPCResult RecvEncoded( + const GMPVideoEncodedFrameData& aEncodedFrame, + nsTArray<uint8_t>&& aCodecSpecificInfo) override; + mozilla::ipc::IPCResult RecvError(const GMPErr& aError) override; + mozilla::ipc::IPCResult RecvShutdown() override; + mozilla::ipc::IPCResult RecvParentShmemForPool(Shmem&& aFrameBuffer) override; + mozilla::ipc::IPCResult RecvNeedShmem(const uint32_t& aEncodedBufferSize, + Shmem* aMem) override; + mozilla::ipc::IPCResult Recv__delete__() override; + + bool mIsOpen; + bool mShuttingDown; + bool mActorDestroyed; + RefPtr<GMPContentParent> mPlugin; + RefPtr<GMPVideoEncoderCallbackProxy> mCallback; + GMPVideoHostImpl mVideoHost; + const uint32_t mPluginId; +}; + +} // namespace mozilla::gmp + +#endif // GMPVideoEncoderParent_h_ diff --git a/dom/media/gmp/GMPVideoEncoderProxy.h b/dom/media/gmp/GMPVideoEncoderProxy.h new file mode 100644 index 0000000000..ad638ebb3a --- /dev/null +++ b/dom/media/gmp/GMPVideoEncoderProxy.h @@ -0,0 +1,56 @@ +/* -*- 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/. */ + +#ifndef GMPVideoEncoderProxy_h_ +#define GMPVideoEncoderProxy_h_ + +#include "nsTArray.h" +#include "gmp-video-encode.h" +#include "gmp-video-frame-i420.h" +#include "gmp-video-frame-encoded.h" + +#include "GMPCallbackBase.h" +#include "GMPUtils.h" + +class GMPVideoEncoderCallbackProxy : public GMPCallbackBase { + public: + virtual ~GMPVideoEncoderCallbackProxy() = default; + virtual void Encoded(GMPVideoEncodedFrame* aEncodedFrame, + const nsTArray<uint8_t>& aCodecSpecificInfo) = 0; + virtual void Error(GMPErr aError) = 0; +}; + +// A proxy to GMPVideoEncoder in the child process. +// GMPVideoEncoderParent exposes this to users the GMP. +// This enables Gecko to pass nsTArrays to the child GMP and avoid +// an extra copy when doing so. + +// The consumer must call Close() when done with the codec, or when +// Terminated() is called by the GMP plugin indicating an abnormal shutdown +// of the underlying plugin. After calling Close(), the consumer must +// not access this again. + +// This interface is not thread-safe and must only be used from GMPThread. +class GMPVideoEncoderProxy { + public: + virtual GMPErr InitEncode(const GMPVideoCodec& aCodecSettings, + const nsTArray<uint8_t>& aCodecSpecific, + GMPVideoEncoderCallbackProxy* aCallback, + int32_t aNumberOfCores, + uint32_t aMaxPayloadSize) = 0; + virtual GMPErr Encode(mozilla::GMPUniquePtr<GMPVideoi420Frame> aInputFrame, + const nsTArray<uint8_t>& aCodecSpecificInfo, + const nsTArray<GMPVideoFrameType>& aFrameTypes) = 0; + virtual GMPErr SetChannelParameters(uint32_t aPacketLoss, uint32_t aRTT) = 0; + virtual GMPErr SetRates(uint32_t aNewBitRate, uint32_t aFrameRate) = 0; + virtual GMPErr SetPeriodicKeyFrames(bool aEnable) = 0; + virtual uint32_t GetPluginId() const = 0; + + // Call to tell GMP/plugin the consumer will no longer use this + // interface/codec. + virtual void Close() = 0; +}; + +#endif // GMPVideoEncoderProxy_h_ diff --git a/dom/media/gmp/GMPVideoHost.cpp b/dom/media/gmp/GMPVideoHost.cpp new file mode 100644 index 0000000000..7ed6c5abc2 --- /dev/null +++ b/dom/media/gmp/GMPVideoHost.cpp @@ -0,0 +1,94 @@ +/* -*- 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 "GMPVideoHost.h" +#include "mozilla/Assertions.h" +#include "GMPVideoi420FrameImpl.h" +#include "GMPVideoEncodedFrameImpl.h" + +namespace mozilla::gmp { + +GMPVideoHostImpl::GMPVideoHostImpl(GMPSharedMemManager* aSharedMemMgr) + : mSharedMemMgr(aSharedMemMgr) {} + +GMPVideoHostImpl::~GMPVideoHostImpl() = default; + +GMPErr GMPVideoHostImpl::CreateFrame(GMPVideoFrameFormat aFormat, + GMPVideoFrame** aFrame) { + if (!mSharedMemMgr) { + return GMPGenericErr; + } + + if (!aFrame) { + return GMPGenericErr; + } + *aFrame = nullptr; + + switch (aFormat) { + case kGMPI420VideoFrame: + *aFrame = new GMPVideoi420FrameImpl(this); + return GMPNoErr; + case kGMPEncodedVideoFrame: + *aFrame = new GMPVideoEncodedFrameImpl(this); + return GMPNoErr; + default: + MOZ_ASSERT_UNREACHABLE("Unknown frame format!"); + } + + return GMPGenericErr; +} + +GMPErr GMPVideoHostImpl::CreatePlane(GMPPlane** aPlane) { + if (!mSharedMemMgr) { + return GMPGenericErr; + } + + if (!aPlane) { + return GMPGenericErr; + } + *aPlane = nullptr; + + auto p = new GMPPlaneImpl(this); + + *aPlane = p; + + return GMPNoErr; +} + +GMPSharedMemManager* GMPVideoHostImpl::SharedMemMgr() { return mSharedMemMgr; } + +// XXX This should merge with ActorDestroyed +void GMPVideoHostImpl::DoneWithAPI() { ActorDestroyed(); } + +void GMPVideoHostImpl::ActorDestroyed() { + for (uint32_t i = mPlanes.Length(); i > 0; i--) { + mPlanes[i - 1]->DoneWithAPI(); + mPlanes.RemoveElementAt(i - 1); + } + for (uint32_t i = mEncodedFrames.Length(); i > 0; i--) { + mEncodedFrames[i - 1]->DoneWithAPI(); + mEncodedFrames.RemoveElementAt(i - 1); + } + mSharedMemMgr = nullptr; +} + +void GMPVideoHostImpl::PlaneCreated(GMPPlaneImpl* aPlane) { + mPlanes.AppendElement(aPlane); +} + +void GMPVideoHostImpl::PlaneDestroyed(GMPPlaneImpl* aPlane) { + MOZ_ALWAYS_TRUE(mPlanes.RemoveElement(aPlane)); +} + +void GMPVideoHostImpl::EncodedFrameCreated( + GMPVideoEncodedFrameImpl* aEncodedFrame) { + mEncodedFrames.AppendElement(aEncodedFrame); +} + +void GMPVideoHostImpl::EncodedFrameDestroyed(GMPVideoEncodedFrameImpl* aFrame) { + MOZ_ALWAYS_TRUE(mEncodedFrames.RemoveElement(aFrame)); +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPVideoHost.h b/dom/media/gmp/GMPVideoHost.h new file mode 100644 index 0000000000..8b75ff28fe --- /dev/null +++ b/dom/media/gmp/GMPVideoHost.h @@ -0,0 +1,54 @@ +/* -*- 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/. */ + +#ifndef GMPVideoHost_h_ +#define GMPVideoHost_h_ + +#include "gmp-video-host.h" +#include "gmp-video-plane.h" +#include "gmp-video-frame.h" +#include "nsTArray.h" + +namespace mozilla::gmp { + +class GMPSharedMemManager; +class GMPPlaneImpl; +class GMPVideoEncodedFrameImpl; + +class GMPVideoHostImpl : public GMPVideoHost { + public: + explicit GMPVideoHostImpl(GMPSharedMemManager* aSharedMemMgr); + virtual ~GMPVideoHostImpl(); + + // Used for shared memory allocation and deallocation. + GMPSharedMemManager* SharedMemMgr(); + void DoneWithAPI(); + void ActorDestroyed(); + void PlaneCreated(GMPPlaneImpl* aPlane); + void PlaneDestroyed(GMPPlaneImpl* aPlane); + void EncodedFrameCreated(GMPVideoEncodedFrameImpl* aEncodedFrame); + void EncodedFrameDestroyed(GMPVideoEncodedFrameImpl* aFrame); + + // GMPVideoHost + GMPErr CreateFrame(GMPVideoFrameFormat aFormat, + GMPVideoFrame** aFrame) override; + GMPErr CreatePlane(GMPPlane** aPlane) override; + + private: + // All shared memory allocations have to be made by an IPDL actor. + // This is a reference to the owning actor. If this reference is + // null then the actor has died and all allocations must fail. + GMPSharedMemManager* mSharedMemMgr; + + // We track all of these things because they need to handle further + // allocations through us and we need to notify them when they + // can't use us any more. + nsTArray<GMPPlaneImpl*> mPlanes; + nsTArray<GMPVideoEncodedFrameImpl*> mEncodedFrames; +}; + +} // namespace mozilla::gmp + +#endif // GMPVideoHost_h_ diff --git a/dom/media/gmp/GMPVideoPlaneImpl.cpp b/dom/media/gmp/GMPVideoPlaneImpl.cpp new file mode 100644 index 0000000000..6c24b5b4a6 --- /dev/null +++ b/dom/media/gmp/GMPVideoPlaneImpl.cpp @@ -0,0 +1,179 @@ +/* -*- 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 "GMPVideoPlaneImpl.h" +#include "mozilla/gmp/GMPTypes.h" +#include "GMPVideoHost.h" +#include "GMPSharedMemManager.h" + +namespace mozilla::gmp { + +GMPPlaneImpl::GMPPlaneImpl(GMPVideoHostImpl* aHost) + : mSize(0), mStride(0), mHost(aHost) { + MOZ_ASSERT(mHost); + mHost->PlaneCreated(this); +} + +GMPPlaneImpl::GMPPlaneImpl(const GMPPlaneData& aPlaneData, + GMPVideoHostImpl* aHost) + : mBuffer(aPlaneData.mBuffer()), + mSize(aPlaneData.mSize()), + mStride(aPlaneData.mStride()), + mHost(aHost) { + MOZ_ASSERT(mHost); + mHost->PlaneCreated(this); +} + +GMPPlaneImpl::~GMPPlaneImpl() { + DestroyBuffer(); + if (mHost) { + mHost->PlaneDestroyed(this); + } +} + +void GMPPlaneImpl::DoneWithAPI() { + DestroyBuffer(); + + // Do this after destroying the buffer because destruction + // involves deallocation, which requires a host. + mHost = nullptr; +} + +void GMPPlaneImpl::ActorDestroyed() { + // Simply clear out Shmem reference, do not attempt to + // properly free it. It has already been freed. + mBuffer = ipc::Shmem(); + // No more host. + mHost = nullptr; +} + +bool GMPPlaneImpl::InitPlaneData(GMPPlaneData& aPlaneData) { + aPlaneData.mBuffer() = mBuffer; + aPlaneData.mSize() = mSize; + aPlaneData.mStride() = mStride; + + // This method is called right before Shmem is sent to another process. + // We need to effectively zero out our member copy so that we don't + // try to delete memory we don't own later. + mBuffer = ipc::Shmem(); + + return true; +} + +GMPErr GMPPlaneImpl::MaybeResize(int32_t aNewSize) { + if (aNewSize <= AllocatedSize()) { + return GMPNoErr; + } + + if (!mHost) { + return GMPGenericErr; + } + + ipc::Shmem new_mem; + if (!mHost->SharedMemMgr()->MgrAllocShmem(GMPSharedMem::kGMPFrameData, + aNewSize, &new_mem) || + !new_mem.get<uint8_t>()) { + return GMPAllocErr; + } + + if (mBuffer.IsReadable()) { + memcpy(new_mem.get<uint8_t>(), Buffer(), mSize); + } + + DestroyBuffer(); + + mBuffer = new_mem; + + return GMPNoErr; +} + +void GMPPlaneImpl::DestroyBuffer() { + if (mHost && mBuffer.IsWritable()) { + mHost->SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPFrameData, + mBuffer); + } + mBuffer = ipc::Shmem(); +} + +GMPErr GMPPlaneImpl::CreateEmptyPlane(int32_t aAllocatedSize, int32_t aStride, + int32_t aPlaneSize) { + if (aAllocatedSize < 1 || aStride < 1 || aPlaneSize < 1) { + return GMPGenericErr; + } + + GMPErr err = MaybeResize(aAllocatedSize); + if (err != GMPNoErr) { + return err; + } + + mSize = aPlaneSize; + mStride = aStride; + + return GMPNoErr; +} + +GMPErr GMPPlaneImpl::Copy(const GMPPlane& aPlane) { + auto& planeimpl = static_cast<const GMPPlaneImpl&>(aPlane); + + GMPErr err = MaybeResize(planeimpl.mSize); + if (err != GMPNoErr) { + return err; + } + + if (planeimpl.Buffer() && planeimpl.mSize > 0) { + memcpy(Buffer(), planeimpl.Buffer(), mSize); + } + + mSize = planeimpl.mSize; + mStride = planeimpl.mStride; + + return GMPNoErr; +} + +GMPErr GMPPlaneImpl::Copy(int32_t aSize, int32_t aStride, + const uint8_t* aBuffer) { + GMPErr err = MaybeResize(aSize); + if (err != GMPNoErr) { + return err; + } + + if (aBuffer && aSize > 0) { + memcpy(Buffer(), aBuffer, aSize); + } + + mSize = aSize; + mStride = aStride; + + return GMPNoErr; +} + +void GMPPlaneImpl::Swap(GMPPlane& aPlane) { + auto& planeimpl = static_cast<GMPPlaneImpl&>(aPlane); + + std::swap(mStride, planeimpl.mStride); + std::swap(mSize, planeimpl.mSize); + std::swap(mBuffer, planeimpl.mBuffer); +} + +int32_t GMPPlaneImpl::AllocatedSize() const { + if (mBuffer.IsWritable()) { + return mBuffer.Size<uint8_t>(); + } + return 0; +} + +void GMPPlaneImpl::ResetSize() { mSize = 0; } + +bool GMPPlaneImpl::IsZeroSize() const { return (mSize == 0); } + +int32_t GMPPlaneImpl::Stride() const { return mStride; } + +const uint8_t* GMPPlaneImpl::Buffer() const { return mBuffer.get<uint8_t>(); } + +uint8_t* GMPPlaneImpl::Buffer() { return mBuffer.get<uint8_t>(); } + +void GMPPlaneImpl::Destroy() { delete this; } + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPVideoPlaneImpl.h b/dom/media/gmp/GMPVideoPlaneImpl.h new file mode 100644 index 0000000000..d0c1de1cd3 --- /dev/null +++ b/dom/media/gmp/GMPVideoPlaneImpl.h @@ -0,0 +1,61 @@ +/* -*- 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/. */ + +#ifndef GMPVideoPlaneImpl_h_ +#define GMPVideoPlaneImpl_h_ + +#include "gmp-video-plane.h" +#include "mozilla/ipc/Shmem.h" + +namespace mozilla::gmp { + +class GMPVideoHostImpl; +class GMPPlaneData; + +class GMPPlaneImpl : public GMPPlane { + friend struct IPC::ParamTraits<mozilla::gmp::GMPPlaneImpl>; + + public: + explicit GMPPlaneImpl(GMPVideoHostImpl* aHost); + GMPPlaneImpl(const GMPPlaneData& aPlaneData, GMPVideoHostImpl* aHost); + virtual ~GMPPlaneImpl(); + + // This is called during a normal destroy sequence, which is + // when a consumer is finished or during XPCOM shutdown. + void DoneWithAPI(); + // This is called when something has gone wrong - specicifically, + // a child process has crashed. Does not attempt to release Shmem, + // as the Shmem has already been released. + void ActorDestroyed(); + + bool InitPlaneData(GMPPlaneData& aPlaneData); + + // GMPPlane + GMPErr CreateEmptyPlane(int32_t aAllocatedSize, int32_t aStride, + int32_t aPlaneSize) override; + GMPErr Copy(const GMPPlane& aPlane) override; + GMPErr Copy(int32_t aSize, int32_t aStride, const uint8_t* aBuffer) override; + void Swap(GMPPlane& aPlane) override; + int32_t AllocatedSize() const override; + void ResetSize() override; + bool IsZeroSize() const override; + int32_t Stride() const override; + const uint8_t* Buffer() const override; + uint8_t* Buffer() override; + void Destroy() override; + + private: + GMPErr MaybeResize(int32_t aNewSize); + void DestroyBuffer(); + + ipc::Shmem mBuffer; + int32_t mSize; + int32_t mStride; + GMPVideoHostImpl* mHost; +}; + +} // namespace mozilla::gmp + +#endif // GMPVideoPlaneImpl_h_ diff --git a/dom/media/gmp/GMPVideoi420FrameImpl.cpp b/dom/media/gmp/GMPVideoi420FrameImpl.cpp new file mode 100644 index 0000000000..1ca3e8123f --- /dev/null +++ b/dom/media/gmp/GMPVideoi420FrameImpl.cpp @@ -0,0 +1,328 @@ +/* -*- 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 "GMPVideoi420FrameImpl.h" +#include "mozilla/gmp/GMPTypes.h" +#include "mozilla/CheckedInt.h" + +namespace mozilla::gmp { + +GMPVideoi420FrameImpl::GMPVideoi420FrameImpl(GMPVideoHostImpl* aHost) + : mYPlane(aHost), + mUPlane(aHost), + mVPlane(aHost), + mWidth(0), + mHeight(0), + mTimestamp(0ll), + mDuration(0ll) { + MOZ_ASSERT(aHost); +} + +GMPVideoi420FrameImpl::GMPVideoi420FrameImpl( + const GMPVideoi420FrameData& aFrameData, GMPVideoHostImpl* aHost) + : mYPlane(aFrameData.mYPlane(), aHost), + mUPlane(aFrameData.mUPlane(), aHost), + mVPlane(aFrameData.mVPlane(), aHost), + mWidth(aFrameData.mWidth()), + mHeight(aFrameData.mHeight()), + mTimestamp(aFrameData.mTimestamp()), + mUpdatedTimestamp(aFrameData.mUpdatedTimestamp()), + mDuration(aFrameData.mDuration()) { + MOZ_ASSERT(aHost); +} + +GMPVideoi420FrameImpl::~GMPVideoi420FrameImpl() = default; + +bool GMPVideoi420FrameImpl::InitFrameData(GMPVideoi420FrameData& aFrameData) { + mYPlane.InitPlaneData(aFrameData.mYPlane()); + mUPlane.InitPlaneData(aFrameData.mUPlane()); + mVPlane.InitPlaneData(aFrameData.mVPlane()); + aFrameData.mWidth() = mWidth; + aFrameData.mHeight() = mHeight; + aFrameData.mTimestamp() = mTimestamp; + aFrameData.mUpdatedTimestamp() = mUpdatedTimestamp; + aFrameData.mDuration() = mDuration; + return true; +} + +GMPVideoFrameFormat GMPVideoi420FrameImpl::GetFrameFormat() { + return kGMPI420VideoFrame; +} + +void GMPVideoi420FrameImpl::Destroy() { delete this; } + +/* static */ +bool GMPVideoi420FrameImpl::CheckFrameData( + const GMPVideoi420FrameData& aFrameData) { + // We may be passed the "wrong" shmem (one smaller than the actual size). + // This implies a bug or serious error on the child size. Ignore this frame + // if so. Note: Size() greater than expected is also an error, but with no + // negative consequences + int32_t half_width = (aFrameData.mWidth() + 1) / 2; + if ((aFrameData.mYPlane().mStride() <= 0) || + (aFrameData.mYPlane().mSize() <= 0) || + (aFrameData.mUPlane().mStride() <= 0) || + (aFrameData.mUPlane().mSize() <= 0) || + (aFrameData.mVPlane().mStride() <= 0) || + (aFrameData.mVPlane().mSize() <= 0) || + (aFrameData.mYPlane().mSize() > + (int32_t)aFrameData.mYPlane().mBuffer().Size<uint8_t>()) || + (aFrameData.mUPlane().mSize() > + (int32_t)aFrameData.mUPlane().mBuffer().Size<uint8_t>()) || + (aFrameData.mVPlane().mSize() > + (int32_t)aFrameData.mVPlane().mBuffer().Size<uint8_t>()) || + (aFrameData.mYPlane().mStride() < aFrameData.mWidth()) || + (aFrameData.mUPlane().mStride() < half_width) || + (aFrameData.mVPlane().mStride() < half_width) || + (aFrameData.mYPlane().mSize() < + aFrameData.mYPlane().mStride() * aFrameData.mHeight()) || + (aFrameData.mUPlane().mSize() < + aFrameData.mUPlane().mStride() * ((aFrameData.mHeight() + 1) / 2)) || + (aFrameData.mVPlane().mSize() < + aFrameData.mVPlane().mStride() * ((aFrameData.mHeight() + 1) / 2))) { + return false; + } + return true; +} + +bool GMPVideoi420FrameImpl::CheckDimensions(int32_t aWidth, int32_t aHeight, + int32_t aStride_y, + int32_t aStride_u, + int32_t aStride_v) { + int32_t half_width = (aWidth + 1) / 2; + if (aWidth < 1 || aHeight < 1 || aStride_y < aWidth || + aStride_u < half_width || aStride_v < half_width || + !(CheckedInt<int32_t>(aHeight) * aStride_y + + ((CheckedInt<int32_t>(aHeight) + 1) / 2) * + (CheckedInt<int32_t>(aStride_u) + aStride_v)) + .isValid()) { + return false; + } + return true; +} + +const GMPPlaneImpl* GMPVideoi420FrameImpl::GetPlane(GMPPlaneType aType) const { + switch (aType) { + case kGMPYPlane: + return &mYPlane; + case kGMPUPlane: + return &mUPlane; + case kGMPVPlane: + return &mVPlane; + default: + MOZ_CRASH("Unknown plane type!"); + } + return nullptr; +} + +GMPPlaneImpl* GMPVideoi420FrameImpl::GetPlane(GMPPlaneType aType) { + switch (aType) { + case kGMPYPlane: + return &mYPlane; + case kGMPUPlane: + return &mUPlane; + case kGMPVPlane: + return &mVPlane; + default: + MOZ_CRASH("Unknown plane type!"); + } + return nullptr; +} + +GMPErr GMPVideoi420FrameImpl::CreateEmptyFrame(int32_t aWidth, int32_t aHeight, + int32_t aStride_y, + int32_t aStride_u, + int32_t aStride_v) { + if (!CheckDimensions(aWidth, aHeight, aStride_y, aStride_u, aStride_v)) { + return GMPGenericErr; + } + + int32_t size_y = aStride_y * aHeight; + int32_t half_height = (aHeight + 1) / 2; + int32_t size_u = aStride_u * half_height; + int32_t size_v = aStride_v * half_height; + + GMPErr err = mYPlane.CreateEmptyPlane(size_y, aStride_y, size_y); + if (err != GMPNoErr) { + return err; + } + err = mUPlane.CreateEmptyPlane(size_u, aStride_u, size_u); + if (err != GMPNoErr) { + return err; + } + err = mVPlane.CreateEmptyPlane(size_v, aStride_v, size_v); + if (err != GMPNoErr) { + return err; + } + + mWidth = aWidth; + mHeight = aHeight; + mTimestamp = 0ll; + mUpdatedTimestamp.reset(); + mDuration = 0ll; + + return GMPNoErr; +} + +GMPErr GMPVideoi420FrameImpl::CreateFrame( + int32_t aSize_y, const uint8_t* aBuffer_y, int32_t aSize_u, + const uint8_t* aBuffer_u, int32_t aSize_v, const uint8_t* aBuffer_v, + int32_t aWidth, int32_t aHeight, int32_t aStride_y, int32_t aStride_u, + int32_t aStride_v) { + MOZ_ASSERT(aBuffer_y); + MOZ_ASSERT(aBuffer_u); + MOZ_ASSERT(aBuffer_v); + + if (aSize_y < 1 || aSize_u < 1 || aSize_v < 1) { + return GMPGenericErr; + } + + if (!CheckDimensions(aWidth, aHeight, aStride_y, aStride_u, aStride_v)) { + return GMPGenericErr; + } + + GMPErr err = mYPlane.Copy(aSize_y, aStride_y, aBuffer_y); + if (err != GMPNoErr) { + return err; + } + err = mUPlane.Copy(aSize_u, aStride_u, aBuffer_u); + if (err != GMPNoErr) { + return err; + } + err = mVPlane.Copy(aSize_v, aStride_v, aBuffer_v); + if (err != GMPNoErr) { + return err; + } + + mWidth = aWidth; + mHeight = aHeight; + + return GMPNoErr; +} + +GMPErr GMPVideoi420FrameImpl::CopyFrame(const GMPVideoi420Frame& aFrame) { + auto& f = static_cast<const GMPVideoi420FrameImpl&>(aFrame); + + GMPErr err = mYPlane.Copy(f.mYPlane); + if (err != GMPNoErr) { + return err; + } + + err = mUPlane.Copy(f.mUPlane); + if (err != GMPNoErr) { + return err; + } + + err = mVPlane.Copy(f.mVPlane); + if (err != GMPNoErr) { + return err; + } + + mWidth = f.mWidth; + mHeight = f.mHeight; + mTimestamp = f.mTimestamp; + mUpdatedTimestamp = f.mUpdatedTimestamp; + mDuration = f.mDuration; + + return GMPNoErr; +} + +void GMPVideoi420FrameImpl::SwapFrame(GMPVideoi420Frame* aFrame) { + auto f = static_cast<GMPVideoi420FrameImpl*>(aFrame); + mYPlane.Swap(f->mYPlane); + mUPlane.Swap(f->mUPlane); + mVPlane.Swap(f->mVPlane); + std::swap(mWidth, f->mWidth); + std::swap(mHeight, f->mHeight); + std::swap(mTimestamp, f->mTimestamp); + std::swap(mUpdatedTimestamp, f->mUpdatedTimestamp); + std::swap(mDuration, f->mDuration); +} + +uint8_t* GMPVideoi420FrameImpl::Buffer(GMPPlaneType aType) { + GMPPlane* p = GetPlane(aType); + if (p) { + return p->Buffer(); + } + return nullptr; +} + +const uint8_t* GMPVideoi420FrameImpl::Buffer(GMPPlaneType aType) const { + const GMPPlane* p = GetPlane(aType); + if (p) { + return p->Buffer(); + } + return nullptr; +} + +int32_t GMPVideoi420FrameImpl::AllocatedSize(GMPPlaneType aType) const { + const GMPPlane* p = GetPlane(aType); + if (p) { + return p->AllocatedSize(); + } + return -1; +} + +int32_t GMPVideoi420FrameImpl::Stride(GMPPlaneType aType) const { + const GMPPlane* p = GetPlane(aType); + if (p) { + return p->Stride(); + } + return -1; +} + +GMPErr GMPVideoi420FrameImpl::SetWidth(int32_t aWidth) { + if (!CheckDimensions(aWidth, mHeight, mYPlane.Stride(), mUPlane.Stride(), + mVPlane.Stride())) { + return GMPGenericErr; + } + mWidth = aWidth; + return GMPNoErr; +} + +GMPErr GMPVideoi420FrameImpl::SetHeight(int32_t aHeight) { + if (!CheckDimensions(mWidth, aHeight, mYPlane.Stride(), mUPlane.Stride(), + mVPlane.Stride())) { + return GMPGenericErr; + } + mHeight = aHeight; + return GMPNoErr; +} + +int32_t GMPVideoi420FrameImpl::Width() const { return mWidth; } + +int32_t GMPVideoi420FrameImpl::Height() const { return mHeight; } + +void GMPVideoi420FrameImpl::SetTimestamp(uint64_t aTimestamp) { + mTimestamp = aTimestamp; +} + +uint64_t GMPVideoi420FrameImpl::Timestamp() const { return mTimestamp; } + +void GMPVideoi420FrameImpl::SetUpdatedTimestamp(uint64_t aTimestamp) { + mUpdatedTimestamp = Some(aTimestamp); +} + +uint64_t GMPVideoi420FrameImpl::UpdatedTimestamp() const { + return mUpdatedTimestamp ? *mUpdatedTimestamp : mTimestamp; +} + +void GMPVideoi420FrameImpl::SetDuration(uint64_t aDuration) { + mDuration = aDuration; +} + +uint64_t GMPVideoi420FrameImpl::Duration() const { return mDuration; } + +bool GMPVideoi420FrameImpl::IsZeroSize() const { + return (mYPlane.IsZeroSize() && mUPlane.IsZeroSize() && mVPlane.IsZeroSize()); +} + +void GMPVideoi420FrameImpl::ResetSize() { + mYPlane.ResetSize(); + mUPlane.ResetSize(); + mVPlane.ResetSize(); +} + +} // namespace mozilla::gmp diff --git a/dom/media/gmp/GMPVideoi420FrameImpl.h b/dom/media/gmp/GMPVideoi420FrameImpl.h new file mode 100644 index 0000000000..53377beb8d --- /dev/null +++ b/dom/media/gmp/GMPVideoi420FrameImpl.h @@ -0,0 +1,80 @@ +/* -*- 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/. */ + +#ifndef GMPVideoi420FrameImpl_h_ +#define GMPVideoi420FrameImpl_h_ + +#include "gmp-video-frame-i420.h" +#include "mozilla/ipc/Shmem.h" +#include "GMPVideoPlaneImpl.h" +#include "mozilla/Maybe.h" + +namespace mozilla::gmp { + +class GMPVideoi420FrameData; + +class GMPVideoi420FrameImpl : public GMPVideoi420Frame { + friend struct IPC::ParamTraits<mozilla::gmp::GMPVideoi420FrameImpl>; + + public: + explicit GMPVideoi420FrameImpl(GMPVideoHostImpl* aHost); + GMPVideoi420FrameImpl(const GMPVideoi420FrameData& aFrameData, + GMPVideoHostImpl* aHost); + virtual ~GMPVideoi420FrameImpl(); + + static bool CheckFrameData(const GMPVideoi420FrameData& aFrameData); + + bool InitFrameData(GMPVideoi420FrameData& aFrameData); + const GMPPlaneImpl* GetPlane(GMPPlaneType aType) const; + GMPPlaneImpl* GetPlane(GMPPlaneType aType); + + // GMPVideoFrame + GMPVideoFrameFormat GetFrameFormat() override; + void Destroy() override; + + // GMPVideoi420Frame + GMPErr CreateEmptyFrame(int32_t aWidth, int32_t aHeight, int32_t aStride_y, + int32_t aStride_u, int32_t aStride_v) override; + GMPErr CreateFrame(int32_t aSize_y, const uint8_t* aBuffer_y, int32_t aSize_u, + const uint8_t* aBuffer_u, int32_t aSize_v, + const uint8_t* aBuffer_v, int32_t aWidth, int32_t aHeight, + int32_t aStride_y, int32_t aStride_u, + int32_t aStride_v) override; + GMPErr CopyFrame(const GMPVideoi420Frame& aFrame) override; + void SwapFrame(GMPVideoi420Frame* aFrame) override; + uint8_t* Buffer(GMPPlaneType aType) override; + const uint8_t* Buffer(GMPPlaneType aType) const override; + int32_t AllocatedSize(GMPPlaneType aType) const override; + int32_t Stride(GMPPlaneType aType) const override; + GMPErr SetWidth(int32_t aWidth) override; + GMPErr SetHeight(int32_t aHeight) override; + int32_t Width() const override; + int32_t Height() const override; + void SetTimestamp(uint64_t aTimestamp) override; + uint64_t Timestamp() const override; + void SetUpdatedTimestamp(uint64_t aTimestamp) override; + uint64_t UpdatedTimestamp() const override; + void SetDuration(uint64_t aDuration) override; + uint64_t Duration() const override; + bool IsZeroSize() const override; + void ResetSize() override; + + private: + bool CheckDimensions(int32_t aWidth, int32_t aHeight, int32_t aStride_y, + int32_t aStride_u, int32_t aStride_v); + + GMPPlaneImpl mYPlane; + GMPPlaneImpl mUPlane; + GMPPlaneImpl mVPlane; + int32_t mWidth; + int32_t mHeight; + uint64_t mTimestamp; + Maybe<uint64_t> mUpdatedTimestamp; + uint64_t mDuration; +}; + +} // namespace mozilla::gmp + +#endif // GMPVideoi420FrameImpl_h_ diff --git a/dom/media/gmp/PChromiumCDM.ipdl b/dom/media/gmp/PChromiumCDM.ipdl new file mode 100644 index 0000000000..2355839c17 --- /dev/null +++ b/dom/media/gmp/PChromiumCDM.ipdl @@ -0,0 +1,129 @@ +/* -*- 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 protocol PGMPContent; +include GMPTypes; + +include "GMPMessageUtils.h"; +include "ChromiumCDMParent.h"; + +using cdm::HdcpVersion from "GMPSanitizedExports.h"; + +namespace mozilla { +namespace gmp { + +[ChildImpl=virtual, ParentImpl="ChromiumCDMParent"] +async protocol PChromiumCDM +{ + manager PGMPContent; +child: + + // cdm::ContentDecryptionModule_10 + async Init(bool aAllowDistinctiveIdentifier, + bool aAllowPersistentState) returns (bool aSuccess); + + async GetStatusForPolicy(uint32_t aPromiseId, + HdcpVersion aMinHdcpVersion); + + async SetServerCertificate(uint32_t aPromiseId, + uint8_t[] aServerCert); + + async CreateSessionAndGenerateRequest(uint32_t aPromiseId, + uint32_t aSessionType, + uint32_t aInitDataType, + uint8_t[] aInitData); + + async LoadSession(uint32_t aPromiseId, + uint32_t aSessionType, + nsCString aSessionId); + + async UpdateSession(uint32_t aPromiseId, + nsCString aSessionId, + uint8_t[] aResponse); + + async CloseSession(uint32_t aPromiseId, + nsCString aSessionId); + + async RemoveSession(uint32_t aPromiseId, + nsCString aSessionId); + + // This is called after the parent has verified the protection status and + // round tripped the call back to the child. + async CompleteQueryOutputProtectionStatus(bool aSuccess, + uint32_t aLinkMask, + uint32_t aProtectionMask); + + async Decrypt(uint32_t aId, CDMInputBuffer aBuffer); + + async InitializeVideoDecoder(CDMVideoDecoderConfig aConfig); + + async DeinitializeVideoDecoder(); + + async ResetVideoDecoder(); + + async DecryptAndDecodeFrame(CDMInputBuffer aBuffer); + + async Drain(); + + async Destroy(); + + async GiveBuffer(Shmem aShmem); + + async PurgeShmems(); + + +parent: + async __delete__(); + + // cdm::Host_10 + async OnResolvePromiseWithKeyStatus(uint32_t aPromiseId, uint32_t aKeyStatus); + + async OnResolveNewSessionPromise(uint32_t aPromiseId, nsCString aSessionId); + + async OnResolvePromise(uint32_t aPromiseId); + + async OnRejectPromise(uint32_t aPromiseId, + uint32_t aException, + uint32_t aSystemCode, + nsCString aErrorMessage); + + async OnSessionMessage(nsCString aSessionId, + uint32_t aMessageType, + uint8_t[] aMessage); + + async OnSessionKeysChange(nsCString aSessionId, + CDMKeyInformation[] aKeysInfo); + + async OnExpirationChange(nsCString aSessionId, + double aSecondsSinceEpoch); + + async OnSessionClosed(nsCString aSessionId); + + async OnQueryOutputProtectionStatus(); + + async ResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccessful); + + // Return values of cdm::ContentDecryptionModule_10::Decrypt + async Decrypted(uint32_t aId, uint32_t aStatus, Shmem aDecryptedData); + async DecryptFailed(uint32_t aId, uint32_t aStatus); + + async OnDecoderInitDone(uint32_t aStatus); + + // Return values of cdm::ContentDecryptionModule_10::DecryptAndDecodeFrame + async DecodedShmem(CDMVideoFrame aFrame, Shmem aData); + async DecodedData(CDMVideoFrame aFrame, uint8_t[] aData); + async DecodeFailed(uint32_t aStatus); + + async ResetVideoDecoderComplete(); + + async DrainComplete(); + + async Shutdown(); + + async IncreaseShmemPoolSize(); +}; + +} // namespace gmp +} // namespace mozilla diff --git a/dom/media/gmp/PGMP.ipdl b/dom/media/gmp/PGMP.ipdl new file mode 100644 index 0000000000..076592f49b --- /dev/null +++ b/dom/media/gmp/PGMP.ipdl @@ -0,0 +1,59 @@ +/* -*- 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 protocol PGMPContent; +include protocol PGMPTimer; +include protocol PGMPStorage; +include protocol PProfiler; + +include "mozilla/ipc/ByteBufUtils.h"; +include "GMPParent.h"; +include "GMPChild.h"; + +using mozilla::dom::NativeThreadId from "mozilla/dom/NativeThreadId.h"; + +namespace mozilla { +namespace gmp { + +[NeedsOtherPid, NestedUpTo=inside_sync, ChildImpl="GMPChild", ParentImpl="GMPParent"] +sync protocol PGMP +{ + manages PGMPTimer; + manages PGMPStorage; + +parent: + async InitCrashReporter(NativeThreadId threadId); + async PGMPTimer(); + async PGMPStorage(); + + async PGMPContentChildDestroyed(); + + // Sent from time-to-time to limit the amount of telemetry vulnerable to loss + // Buffer contains bincoded Rust structs. + // https://firefox-source-docs.mozilla.org/toolkit/components/glean/dev/ipc.html + async FOGData(ByteBuf buf); + +child: + async CrashPluginNow(); + [Nested=inside_sync] sync StartPlugin(nsString adapter); + async ProvideStorageId(nsCString storageId); + async PreloadLibs(nsCString libs); + async CloseActive(); + async InitGMPContentChild(Endpoint<PGMPContentChild> endpoint); + async InitProfiler(Endpoint<PProfilerChild> endpoint); + + // Tells the GMP process to flush any pending telemetry. + // Used in tests and ping assembly. Buffer contains bincoded Rust structs. + // https://firefox-source-docs.mozilla.org/toolkit/components/glean/dev/ipc.html + async FlushFOGData() returns (ByteBuf buf); + + // Test-only method. + // Asks the GMP process to trigger test-only instrumentation. + // The unused returned value is to have a promise we can await. + async TestTriggerMetrics() returns (bool unused); +}; + +} // namespace gmp +} // namespace mozilla diff --git a/dom/media/gmp/PGMPContent.ipdl b/dom/media/gmp/PGMPContent.ipdl new file mode 100644 index 0000000000..db47aadc62 --- /dev/null +++ b/dom/media/gmp/PGMPContent.ipdl @@ -0,0 +1,37 @@ +/* -*- 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 protocol PGMPVideoDecoder; +include protocol PGMPVideoEncoder; +include protocol PChromiumCDM; + +#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS) +include protocol PSandboxTesting; +#endif + +include "GMPContentChild.h"; + +namespace mozilla { +namespace gmp { + +[NeedsOtherPid, ChildImpl="GMPContentChild", ParentImpl=virtual] +sync protocol PGMPContent +{ + manages PGMPVideoDecoder; + manages PGMPVideoEncoder; + manages PChromiumCDM; + +child: + async PGMPVideoDecoder(); + async PGMPVideoEncoder(); + async PChromiumCDM(nsCString aKeySystem); + +#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS) + async InitSandboxTesting(Endpoint<PSandboxTestingChild> aEndpoint); +#endif +}; + +} // namespace gmp +} // namespace mozilla diff --git a/dom/media/gmp/PGMPService.ipdl b/dom/media/gmp/PGMPService.ipdl new file mode 100644 index 0000000000..e186aaee80 --- /dev/null +++ b/dom/media/gmp/PGMPService.ipdl @@ -0,0 +1,38 @@ +/* -*- 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 protocol PGMPContent; +include GMPTypes; + +using base::ProcessId from "base/process.h"; +using GMPPluginType from "GMPNativeTypes.h"; + +namespace mozilla { +namespace gmp { + +[NeedsOtherPid, ChildImpl=virtual, ParentImpl=virtual] +sync protocol PGMPService +{ +parent: + sync LaunchGMP(NodeIdVariant nodeIdVariant, + nsCString api, + nsCString[] tags, + ProcessId[] alreadyBridgedTo) + returns (uint32_t pluginId, + GMPPluginType pluginType, + ProcessId id, + nsCString displayName, + Endpoint<PGMPContentParent> endpoint, + nsresult aResult, + nsCString aErrorDescription); + + sync GetGMPNodeId(nsString origin, nsString topLevelOrigin, nsString gmpName) + returns (nsCString id); +child: + async BeginShutdown(); +}; + +} // namespace gmp +} // namespace mozilla diff --git a/dom/media/gmp/PGMPStorage.ipdl b/dom/media/gmp/PGMPStorage.ipdl new file mode 100644 index 0000000000..6efe7fbb23 --- /dev/null +++ b/dom/media/gmp/PGMPStorage.ipdl @@ -0,0 +1,37 @@ +/* -*- 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 protocol PGMP; +include GMPTypes; + +include "GMPStorageChild.h"; + +using GMPErr from "gmp-errors.h"; + +namespace mozilla { +namespace gmp { + +[ManualDealloc, ChildImpl="GMPStorageChild", ParentImpl=virtual] +async protocol PGMPStorage +{ + manager PGMP; + +child: + async OpenComplete(nsCString aRecordName, GMPErr aStatus); + async ReadComplete(nsCString aRecordName, GMPErr aStatus, uint8_t[] aBytes); + async WriteComplete(nsCString aRecordName, GMPErr aStatus); + async Shutdown(); + +parent: + async Open(nsCString aRecordName); + async Read(nsCString aRecordName); + async Write(nsCString aRecordName, uint8_t[] aBytes); + async Close(nsCString aRecordName); + async __delete__(); + +}; + +} // namespace gmp +} // namespace mozilla diff --git a/dom/media/gmp/PGMPTimer.ipdl b/dom/media/gmp/PGMPTimer.ipdl new file mode 100644 index 0000000000..c6c8170b43 --- /dev/null +++ b/dom/media/gmp/PGMPTimer.ipdl @@ -0,0 +1,26 @@ +/* -*- 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 protocol PGMP; + +include "GMPTimerParent.h"; +include "GMPTimerChild.h"; + +namespace mozilla { +namespace gmp { + +[ManualDealloc, ChildImpl="GMPTimerChild", ParentImpl="GMPTimerParent"] +async protocol PGMPTimer +{ + manager PGMP; +child: + async TimerExpired(uint32_t aTimerId); +parent: + async SetTimer(uint32_t aTimerId, uint32_t aTimeoutMs); + async __delete__(); +}; + +} // namespace gmp +} // namespace mozilla diff --git a/dom/media/gmp/PGMPVideoDecoder.ipdl b/dom/media/gmp/PGMPVideoDecoder.ipdl new file mode 100644 index 0000000000..e0c7a21a64 --- /dev/null +++ b/dom/media/gmp/PGMPVideoDecoder.ipdl @@ -0,0 +1,50 @@ +/* -*- 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 protocol PGMPContent; +include GMPTypes; + +using GMPVideoCodec from "gmp-video-codec.h"; +using GMPErr from "gmp-errors.h"; + +include "GMPMessageUtils.h"; +include "GMPVideoDecoderChild.h"; + +namespace mozilla { +namespace gmp { + +[ChildImpl="GMPVideoDecoderChild", ParentImpl=virtual] +sync protocol PGMPVideoDecoder +{ + manager PGMPContent; +child: + async InitDecode(GMPVideoCodec aCodecSettings, + uint8_t[] aCodecSpecific, + int32_t aCoreCount); + async Decode(GMPVideoEncodedFrameData aInputFrame, + bool aMissingFrames, + uint8_t[] aCodecSpecificInfo, + int64_t aRenderTimeMs); + async Reset(); + async Drain(); + async DecodingComplete(); + async ChildShmemForPool(Shmem aFrameBuffer); + +parent: + async __delete__(); + async Decoded(GMPVideoi420FrameData aDecodedFrame); + async ReceivedDecodedReferenceFrame(uint64_t aPictureId); + async ReceivedDecodedFrame(uint64_t aPictureId); + async InputDataExhausted(); + async DrainComplete(); + async ResetComplete(); + async Error(GMPErr aErr); + async Shutdown(); + async ParentShmemForPool(Shmem aEncodedBuffer); + sync NeedShmem(uint32_t aFrameBufferSize) returns (Shmem aMem); +}; + +} // namespace gmp +} // namespace mozilla diff --git a/dom/media/gmp/PGMPVideoEncoder.ipdl b/dom/media/gmp/PGMPVideoEncoder.ipdl new file mode 100644 index 0000000000..afd4f7ce9a --- /dev/null +++ b/dom/media/gmp/PGMPVideoEncoder.ipdl @@ -0,0 +1,48 @@ +/* -*- 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 protocol PGMPContent; +include GMPTypes; + +using GMPVideoCodec from "gmp-video-codec.h"; +using GMPVideoFrameType from "gmp-video-frame-encoded.h"; +using GMPErr from "gmp-errors.h"; + +include "GMPMessageUtils.h"; +include "GMPVideoEncoderChild.h"; + +namespace mozilla { +namespace gmp { + +[ChildImpl="GMPVideoEncoderChild", ParentImpl=virtual] +sync protocol PGMPVideoEncoder +{ + manager PGMPContent; +child: + async InitEncode(GMPVideoCodec aCodecSettings, + uint8_t[] aCodecSpecific, + int32_t aNumberOfCores, + uint32_t aMaxPayloadSize); + async Encode(GMPVideoi420FrameData aInputFrame, + uint8_t[] aCodecSpecificInfo, + GMPVideoFrameType[] aFrameTypes); + async SetChannelParameters(uint32_t aPacketLoss, uint32_t aRTT); + async SetRates(uint32_t aNewBitRate, uint32_t aFrameRate); + async SetPeriodicKeyFrames(bool aEnable); + async EncodingComplete(); + async ChildShmemForPool(Shmem aEncodedBuffer); + +parent: + async __delete__(); + async Encoded(GMPVideoEncodedFrameData aEncodedFrame, + uint8_t[] aCodecSpecificInfo); + async Error(GMPErr aErr); + async Shutdown(); + async ParentShmemForPool(Shmem aFrameBuffer); + sync NeedShmem(uint32_t aEncodedBufferSize) returns (Shmem aMem); +}; + +} // namespace gmp +} // namespace mozilla diff --git a/dom/media/gmp/README.txt b/dom/media/gmp/README.txt new file mode 100644 index 0000000000..189cf3b302 --- /dev/null +++ b/dom/media/gmp/README.txt @@ -0,0 +1 @@ +This directory contains code supporting Gecko Media Plugins (GMPs). The GMP API is not the same thing as the Media Plugin API (MPAPI). diff --git a/dom/media/gmp/gmp-api/gmp-entrypoints.h b/dom/media/gmp/gmp-api/gmp-entrypoints.h new file mode 100644 index 0000000000..fd2457e9be --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-entrypoints.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * Copyright (c) 2014, Mozilla + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + ** Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + ** Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + ** Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMP_ENTRYPOINTS_h_ +#define GMP_ENTRYPOINTS_h_ + +#include "gmp-errors.h" +#include "gmp-platform.h" + +/* C functions exposed by Gecko Media Plugin shared library. */ + +// GMPInit +// - Called once after plugin library is loaded, before GMPGetAPI or GMPShutdown +// are called. +// - Called on main thread. +// - 'aPlatformAPI' is a structure containing platform-provided APIs. It is +// valid until +// 'GMPShutdown' is called. Owned and must be deleted by plugin. +typedef GMPErr (*GMPInitFunc)(const GMPPlatformAPI* aPlatformAPI); + +// GMPGetAPI +// - Called when host wants to use an API. +// - Called on main thread. +// - 'aAPIName' is a string indicating the API being requested. This should +// match one of the GMP_API_* macros. Subsequent iterations of the GMP_APIs +// may change the value of the GMP_API_* macros when ABI changes occur. So +// make sure you compare aAPIName against the corresponding GMP_API_* macro! +// - 'aHostAPI' is the host API which is specific to the API being requested +// from the plugin. It is valid so long as the API object requested from the +// plugin is valid. It is owned by the host, plugin should not attempt to +// delete. May be null. +// - 'aPluginAPI' is for returning the requested API. Destruction of the +// requsted +// API object is defined by the API. +typedef GMPErr (*GMPGetAPIFunc)(const char* aAPIName, void* aHostAPI, + void** aPluginAPI); + +// GMPShutdown +// - Called once before exiting process (unloading library). +// - Called on main thread. +typedef void (*GMPShutdownFunc)(void); + +#endif // GMP_ENTRYPOINTS_h_ diff --git a/dom/media/gmp/gmp-api/gmp-errors.h b/dom/media/gmp/gmp-api/gmp-errors.h new file mode 100644 index 0000000000..a95db1f7a5 --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-errors.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2014, Mozilla + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + ** Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + ** Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + ** Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMP_ERRORS_h_ +#define GMP_ERRORS_h_ + +typedef enum { + GMPNoErr = 0, + GMPGenericErr = 1, + GMPClosedErr = 2, + GMPAllocErr = 3, + GMPNotImplementedErr = 4, + GMPRecordInUse = 5, + GMPQuotaExceededErr = 6, + GMPDecodeErr = 7, + GMPEncodeErr = 8, + GMPNoKeyErr = 9, + GMPCryptoErr = 10, + GMPEndOfEnumeration = 11, + GMPInvalidArgErr = 12, + GMPAbortedErr = 13, + GMPRecordCorrupted = 14, + GMPLastErr // Placeholder, must be last. This enum's values must remain + // consecutive! +} GMPErr; + +#define GMP_SUCCEEDED(x) ((x) == GMPNoErr) +#define GMP_FAILED(x) ((x) != GMPNoErr) + +#endif // GMP_ERRORS_h_ diff --git a/dom/media/gmp/gmp-api/gmp-platform.h b/dom/media/gmp/gmp-api/gmp-platform.h new file mode 100644 index 0000000000..530abcbbfe --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-platform.h @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2014, Mozilla + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + ** Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + ** Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + ** Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMP_PLATFORM_h_ +#define GMP_PLATFORM_h_ + +#include "gmp-errors.h" +#include "gmp-storage.h" +#include <stdint.h> + +/* Platform helper API. */ + +class GMPTask { + public: + virtual void Destroy() = 0; // Deletes object. + virtual ~GMPTask() = default; + virtual void Run() = 0; +}; + +class GMPThread { + public: + virtual ~GMPThread() = default; + virtual void Post(GMPTask* aTask) = 0; + virtual void Join() = 0; // Deletes object after join completes. +}; + +// A re-entrant monitor; can be locked from the same thread multiple times. +// Must be unlocked the same number of times it's locked. +class GMPMutex { + public: + virtual ~GMPMutex() = default; + virtual void Acquire() = 0; + virtual void Release() = 0; + virtual void Destroy() = 0; // Deletes object. +}; + +// Time is defined as the number of milliseconds since the +// Epoch (00:00:00 UTC, January 1, 1970). +typedef int64_t GMPTimestamp; + +typedef GMPErr (*GMPCreateThreadPtr)(GMPThread** aThread); +typedef GMPErr (*GMPRunOnMainThreadPtr)(GMPTask* aTask); +typedef GMPErr (*GMPSyncRunOnMainThreadPtr)(GMPTask* aTask); +typedef GMPErr (*GMPCreateMutexPtr)(GMPMutex** aMutex); + +// Call on main thread only. +typedef GMPErr (*GMPCreateRecordPtr)(const char* aRecordName, + uint32_t aRecordNameSize, + GMPRecord** aOutRecord, + GMPRecordClient* aClient); + +// Call on main thread only. +typedef GMPErr (*GMPSetTimerOnMainThreadPtr)(GMPTask* aTask, + int64_t aTimeoutMS); +typedef GMPErr (*GMPGetCurrentTimePtr)(GMPTimestamp* aOutTime); + +struct GMPPlatformAPI { + // Increment the version when things change. Can only add to the struct, + // do not change what already exists. Pointers to functions may be NULL + // when passed to plugins, but beware backwards compat implications of + // doing that. + uint16_t version; // Currently version 0 + + GMPCreateThreadPtr createthread; + GMPRunOnMainThreadPtr runonmainthread; + GMPSyncRunOnMainThreadPtr syncrunonmainthread; + GMPCreateMutexPtr createmutex; + GMPCreateRecordPtr createrecord; + GMPSetTimerOnMainThreadPtr settimer; + GMPGetCurrentTimePtr getcurrenttime; +}; + +#endif // GMP_PLATFORM_h_ diff --git a/dom/media/gmp/gmp-api/gmp-storage.h b/dom/media/gmp/gmp-api/gmp-storage.h new file mode 100644 index 0000000000..af53256d43 --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-storage.h @@ -0,0 +1,110 @@ +/* + * Copyright 2013, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GMP_STORAGE_h_ +#define GMP_STORAGE_h_ + +#include "gmp-errors.h" +#include <stdint.h> + +// Maximum size of a record, in bytes; 10 megabytes. +#define GMP_MAX_RECORD_SIZE (10 * 1024 * 1024) + +// Maximum length of a record name in bytes. +#define GMP_MAX_RECORD_NAME_SIZE 2000 + +// Provides basic per-origin storage for CDMs. GMPRecord instances can be +// retrieved by calling GMPPlatformAPI->openstorage. Multiple GMPRecords +// with different names can be open at once, but a single record can only +// be opened by one client at a time. This interface is asynchronous, with +// results being returned via callbacks to the GMPRecordClient pointer +// provided to the GMPPlatformAPI->openstorage call, on the main thread. +// +// Lifecycle: Once opened, the GMPRecord object remains allocated until +// GMPRecord::Close() is called. If any GMPRecord function, either +// synchronously or asynchronously through a GMPRecordClient callback, +// returns an error, the GMP is responsible for calling Close() on the +// GMPRecord to delete the GMPRecord object's memory. If your GMP does not +// call Close(), the GMPRecord's memory will leak. +class GMPRecord { + public: + // Opens the record. Calls OpenComplete() once the record is open. + // Note: Only work when GMP is loading content from a webserver. + // Does not work for web pages on loaded from disk. + // Note: OpenComplete() is only called if this returns GMPNoErr. + virtual GMPErr Open() = 0; + + // Reads the entire contents of the record, and calls + // GMPRecordClient::ReadComplete() once the operation is complete. + // Note: ReadComplete() is only called if this returns GMPNoErr. + virtual GMPErr Read() = 0; + + // Writes aDataSize bytes of aData into the record, overwriting the + // contents of the record, truncating it to aDataSize length. + // Overwriting with 0 bytes "deletes" the record. + // Note: WriteComplete is only called if this returns GMPNoErr. + virtual GMPErr Write(const uint8_t* aData, uint32_t aDataSize) = 0; + + // Closes a record, deletes the GMPRecord object. The GMPRecord object + // must not be used after this is called, request a new one with + // GMPPlatformAPI->openstorage to re-open this record. Cancels all + // callbacks. + virtual GMPErr Close() = 0; + + virtual ~GMPRecord() = default; +}; + +// Callback object that receives the results of GMPRecord calls. Callbacks +// run asynchronously to the GMPRecord call, on the main thread. +class GMPRecordClient { + public: + // Response to a GMPRecord::Open() call with the open |status|. + // aStatus values: + // - GMPNoErr - Record opened successfully. Record may be empty. + // - GMPRecordInUse - This record is in use by another client. + // - GMPGenericErr - Unspecified error. + // If aStatus is not GMPNoErr, the GMPRecord is unusable, and you must + // call Close() on the GMPRecord to dispose of it. + virtual void OpenComplete(GMPErr aStatus) = 0; + + // Response to a GMPRecord::Read() call, where aData is the record contents, + // of length aDataSize. + // aData is only valid for the duration of the call to ReadComplete. + // Copy it if you want to hang onto it! + // aStatus values: + // - GMPNoErr - Record contents read successfully, aDataSize 0 means record + // is empty. + // - GMPRecordInUse - There are other operations or clients in use on + // this record. + // - GMPGenericErr - Unspecified error. + // If aStatus is not GMPNoErr, the GMPRecord is unusable, and you must + // call Close() on the GMPRecord to dispose of it. + virtual void ReadComplete(GMPErr aStatus, const uint8_t* aData, + uint32_t aDataSize) = 0; + + // Response to a GMPRecord::Write() call. + // - GMPNoErr - File contents written successfully. + // - GMPRecordInUse - There are other operations or clients in use on + // this record. + // - GMPGenericErr - Unspecified error. + // If aStatus is not GMPNoErr, the GMPRecord is unusable, and you must + // call Close() on the GMPRecord to dispose of it. + virtual void WriteComplete(GMPErr aStatus) = 0; + + virtual ~GMPRecordClient() = default; +}; + +#endif // GMP_STORAGE_h_ diff --git a/dom/media/gmp/gmp-api/gmp-video-codec.h b/dom/media/gmp/gmp-api/gmp-video-codec.h new file mode 100644 index 0000000000..1fb4930a8b --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-video-codec.h @@ -0,0 +1,302 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * Copyright (c) 2014, Mozilla + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + ** Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + ** Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + ** Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMP_VIDEO_CODEC_h_ +#define GMP_VIDEO_CODEC_h_ + +#include <stdint.h> +#include <stddef.h> + +enum { kGMPPayloadNameSize = 32 }; +enum { kGMPMaxSimulcastStreams = 4 }; + +enum GMPVideoCodecComplexity { + kGMPComplexityNormal = 0, + kGMPComplexityHigh = 1, + kGMPComplexityHigher = 2, + kGMPComplexityMax = 3, + kGMPComplexityInvalid // Should always be last +}; + +enum GMPVP8ResilienceMode { + kResilienceOff, // The stream produced by the encoder requires a + // recovery frame (typically a key frame) to be + // decodable after a packet loss. + kResilientStream, // A stream produced by the encoder is resilient to + // packet losses, but packets within a frame subsequent + // to a loss can't be decoded. + kResilientFrames, // Same as kResilientStream but with added resilience + // within a frame. + kResilienceInvalid // Should always be last. +}; + +// VP8 specific +struct GMPVideoCodecVP8 { + bool mPictureLossIndicationOn; + bool mFeedbackModeOn; + GMPVideoCodecComplexity mComplexity; + GMPVP8ResilienceMode mResilience; + uint32_t mNumberOfTemporalLayers; + bool mDenoisingOn; + bool mErrorConcealmentOn; + bool mAutomaticResizeOn; +}; + +// H264 specific + +// Needs to match a binary spec for this structure. +// Note: the mSPS at the end of this structure is variable length. +struct GMPVideoCodecH264AVCC { + uint8_t mVersion; // == 0x01 + uint8_t mProfile; // these 3 are profile_level_id + uint8_t mConstraints; + uint8_t mLevel; + uint8_t mLengthSizeMinusOne; // lower 2 bits (== GMPBufferType-1). Top 6 + // reserved (1's) + + // SPS/PPS will not generally be present for interactive use unless SDP + // parameter-sets are used. + uint8_t mNumSPS; // lower 5 bits; top 5 reserved (1's) + + /*** uint8_t mSPS[]; (Not defined due to compiler warnings and + * warnings-as-errors ...) **/ + // Following mNumSPS is a variable number of bytes, which is the SPS and PPS. + // Each SPS == 16 bit size, ("N"), then "N" bytes, + // then uint8_t mNumPPS, then each PPS == 16 bit size ("N"), then "N" bytes. +}; + +// Codec specific data for H.264 decoding/encoding. +// Cast the "aCodecSpecific" parameter of GMPVideoDecoder::InitDecode() and +// GMPVideoEncoder::InitEncode() to this structure. +struct GMPVideoCodecH264 { + uint8_t mPacketizationMode; // 0 or 1 + struct GMPVideoCodecH264AVCC + mAVCC; // holds a variable-sized struct GMPVideoCodecH264AVCC mAVCC; +}; + +enum GMPVideoCodecType { + kGMPVideoCodecVP8, + + // Encoded frames are in AVCC format; NAL length field of 4 bytes, followed + // by frame data. May be multiple NALUs per sample. Codec specific extra data + // is the AVCC extra data (in AVCC format). + kGMPVideoCodecH264, + kGMPVideoCodecVP9, + kGMPVideoCodecInvalid // Should always be last. +}; + +// Simulcast is when the same stream is encoded multiple times with different +// settings such as resolution. +struct GMPSimulcastStream { + uint32_t mWidth; + uint32_t mHeight; + uint32_t mNumberOfTemporalLayers; + uint32_t mMaxBitrate; // kilobits/sec. + uint32_t mTargetBitrate; // kilobits/sec. + uint32_t mMinBitrate; // kilobits/sec. + uint32_t mQPMax; // minimum quality +}; + +enum GMPVideoCodecMode { + kGMPRealtimeVideo, + kGMPScreensharing, + kGMPStreamingVideo, + kGMPNonRealtimeVideo, + kGMPCodecModeInvalid // Should always be last. +}; + +enum GMPLogLevel { + kGMPLogDefault, + kGMPLogQuiet, + kGMPLogError, + kGMPLogWarning, + kGMPLogInfo, + kGMPLogDebug, + kGMPLogDetail, + kGMPLogInvalid // Should always be last. +}; + +enum GMPProfile { + kGMPH264ProfileUnknown, + kGMPH264ProfileBaseline, + kGMPH264ProfileMain, + kGMPH264ProfileExtended, + kGMPH264ProfileHigh, + kGMPH264ProfileHigh10, + kGMPH264ProfileHigh422, + kGMPH264ProfileHigh444, + kGMPH264ProfileCavlc444, + kGMPH264ProfileScalableBaseline, + kGMPH264ProfileScalableHigh +}; + +enum GMPLevel { + kGMPH264LevelUnknown, + kGMPH264Level1_0, + kGMPH264Level1_B, + kGMPH264Level1_1, + kGMPH264Level1_2, + kGMPH264Level1_3, + kGMPH264Level2_0, + kGMPH264Level2_1, + kGMPH264Level2_2, + kGMPH264Level3_0, + kGMPH264Level3_1, + kGMPH264Level3_2, + kGMPH264Level4_0, + kGMPH264Level4_1, + kGMPH264Level4_2, + kGMPH264Level5_0, + kGMPH264Level5_1, + kGMPH264Level5_2 +}; + +enum GMPRateControlMode { + kGMPRateControlUnknown, + kGMPRateControlBitrate, + kGMPRateControlQuality, + kGMPRateControlBufferBased, + kGMPRateControlTimestamp, + kGMPRateControlBitratePostskip, + kGMPRateControlOff +}; + +enum GMPSliceMode { + kGMPSliceUnknown, + kGMPSliceSingle, + kGMPSliceFixedSlcNum, + kGMPSliceRaster, + kGMPSliceSizeLimited +}; + +enum GMPApiVersion { + kGMPVersion32 = + 1, // leveraging that V32 had mCodecType first, and only supported H264 + kGMPVersion33 = 33, + + // Adds GMPVideoi420Frame::SetUpdatedTimestamp/UpdatedTimestamp + kGMPVersion34 = 34, + + // Adds new options for encoding + kGMPVersion35 = 35, +}; + +struct GMPVideoCodec { + uint32_t mGMPApiVersion; + + GMPVideoCodecType mCodecType; + char mPLName[kGMPPayloadNameSize]; // Must be NULL-terminated! + uint32_t mPLType; + + uint32_t mWidth; + uint32_t mHeight; + + uint32_t mStartBitrate; // kilobits/sec. + uint32_t mMaxBitrate; // kilobits/sec. + uint32_t mMinBitrate; // kilobits/sec. + uint32_t mMaxFramerate; + + bool mFrameDroppingOn; + int32_t mKeyFrameInterval; + + uint32_t mQPMax; + uint32_t mNumberOfSimulcastStreams; + GMPSimulcastStream mSimulcastStream[kGMPMaxSimulcastStreams]; + + GMPVideoCodecMode mMode; + + // Since GMP version 34 + bool mUseThreadedDecode; + GMPLogLevel mLogLevel; + + // Since GMP version 35 + GMPLevel mLevel; + GMPProfile mProfile; + GMPRateControlMode mRateControlMode; + GMPSliceMode mSliceMode; + bool mUseThreadedEncode; +}; + +// Either single encoded unit, or multiple units separated by 8/16/24/32 +// bit lengths, all with the same timestamp. Note there is no final 0-length +// entry; one should check the overall end-of-buffer against where the next +// length would be. +enum GMPBufferType { + GMP_BufferSingle = 0, + GMP_BufferLength8, + GMP_BufferLength16, + GMP_BufferLength24, + GMP_BufferLength32, + GMP_BufferInvalid, +}; + +struct GMPCodecSpecificInfoGeneric { + uint8_t mSimulcastIdx; +}; + +struct GMPCodecSpecificInfoH264 { + uint8_t mSimulcastIdx; +}; + +// Note: if any pointers are added to this struct, it must be fitted +// with a copy-constructor. See below. +struct GMPCodecSpecificInfoVP8 { + bool mHasReceivedSLI; + uint8_t mPictureIdSLI; + bool mHasReceivedRPSI; + uint64_t mPictureIdRPSI; + int16_t mPictureId; // negative value to skip pictureId + bool mNonReference; + uint8_t mSimulcastIdx; + uint8_t mTemporalIdx; + bool mLayerSync; + int32_t mTL0PicIdx; // negative value to skip tl0PicIdx + int8_t mKeyIdx; // negative value to skip keyIdx +}; + +union GMPCodecSpecificInfoUnion { + GMPCodecSpecificInfoGeneric mGeneric; + GMPCodecSpecificInfoVP8 mVP8; + GMPCodecSpecificInfoH264 mH264; +}; + +// Note: if any pointers are added to this struct or its sub-structs, it +// must be fitted with a copy-constructor. This is because it is copied +// in the copy-constructor of VCMEncodedFrame. +struct GMPCodecSpecificInfo { + GMPVideoCodecType mCodecType; + GMPBufferType mBufferType; + GMPCodecSpecificInfoUnion mCodecSpecific; +}; + +#endif // GMP_VIDEO_CODEC_h_ diff --git a/dom/media/gmp/gmp-api/gmp-video-decode.h b/dom/media/gmp/gmp-api/gmp-video-decode.h new file mode 100644 index 0000000000..561e841d39 --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-video-decode.h @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * Copyright (c) 2014, Mozilla + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + ** Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + ** Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + ** Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMP_VIDEO_DECODE_h_ +#define GMP_VIDEO_DECODE_h_ + +#include "gmp-errors.h" +#include "gmp-video-frame-i420.h" +#include "gmp-video-frame-encoded.h" +#include "gmp-video-codec.h" +#include <stdint.h> + +// ALL METHODS MUST BE CALLED ON THE MAIN THREAD +class GMPVideoDecoderCallback { + public: + virtual ~GMPVideoDecoderCallback() {} + + virtual void Decoded(GMPVideoi420Frame* aDecodedFrame) = 0; + + virtual void ReceivedDecodedReferenceFrame(const uint64_t aPictureId) = 0; + + virtual void ReceivedDecodedFrame(const uint64_t aPictureId) = 0; + + virtual void InputDataExhausted() = 0; + + virtual void DrainComplete() = 0; + + virtual void ResetComplete() = 0; + + // Called when the decoder encounters a catestrophic error and cannot + // continue. Gecko will not send any more input for decoding. + virtual void Error(GMPErr aError) = 0; +}; + +#define GMP_API_VIDEO_DECODER "decode-video" + +// Video decoding for a single stream. A GMP may be asked to create multiple +// decoders concurrently. +// +// API name macro: GMP_API_VIDEO_DECODER +// Host API: GMPVideoHost +// +// ALL METHODS MUST BE CALLED ON THE MAIN THREAD +class GMPVideoDecoder { + public: + virtual ~GMPVideoDecoder() {} + + // - aCodecSettings: Details of decoder to create. + // - aCodecSpecific: codec specific data, cast to a GMPVideoCodecXXX struct + // to get codec specific config data. + // - aCodecSpecificLength: number of bytes in aCodecSpecific. + // - aCallback: Subclass should retain reference to it until DecodingComplete + // is called. Do not attempt to delete it, host retains + // ownership. + // aCoreCount: number of CPU cores. + virtual void InitDecode(const GMPVideoCodec& aCodecSettings, + const uint8_t* aCodecSpecific, + uint32_t aCodecSpecificLength, + GMPVideoDecoderCallback* aCallback, + int32_t aCoreCount) = 0; + + // Decode encoded frame (as a part of a video stream). The decoded frame + // will be returned to the user through the decode complete callback. + // + // - aInputFrame: Frame to decode. Call Destroy() on frame when it's decoded. + // - aMissingFrames: True if one or more frames have been lost since the + // previous decode call. + // - aCodecSpecificInfo : codec specific data, pointer to a + // GMPCodecSpecificInfo structure appropriate for + // this codec type. + // - aCodecSpecificInfoLength : number of bytes in aCodecSpecificInfo + // - renderTimeMs : System time to render in milliseconds. Only used by + // decoders with internal rendering. + virtual void Decode(GMPVideoEncodedFrame* aInputFrame, bool aMissingFrames, + const uint8_t* aCodecSpecificInfo, + uint32_t aCodecSpecificInfoLength, + int64_t aRenderTimeMs = -1) = 0; + + // Reset decoder state and prepare for a new call to Decode(...). + // Flushes the decoder pipeline. + // The decoder should enqueue a task to run ResetComplete() on the main + // thread once the reset has finished. + virtual void Reset() = 0; + + // Output decoded frames for any data in the pipeline, regardless of ordering. + // All remaining decoded frames should be immediately returned via callback. + // The decoder should enqueue a task to run DrainComplete() on the main + // thread once the reset has finished. + virtual void Drain() = 0; + + // May free decoder memory. + virtual void DecodingComplete() = 0; +}; + +#endif // GMP_VIDEO_DECODE_h_ diff --git a/dom/media/gmp/gmp-api/gmp-video-encode.h b/dom/media/gmp/gmp-api/gmp-video-encode.h new file mode 100644 index 0000000000..850d34abd1 --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-video-encode.h @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * Copyright (c) 2014, Mozilla + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + ** Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + ** Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + ** Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMP_VIDEO_ENCODE_h_ +#define GMP_VIDEO_ENCODE_h_ + +#include <vector> +#include <stdint.h> + +#include "gmp-errors.h" +#include "gmp-video-frame-i420.h" +#include "gmp-video-frame-encoded.h" +#include "gmp-video-codec.h" + +// ALL METHODS MUST BE CALLED ON THE MAIN THREAD +class GMPVideoEncoderCallback { + public: + virtual ~GMPVideoEncoderCallback() {} + + virtual void Encoded(GMPVideoEncodedFrame* aEncodedFrame, + const uint8_t* aCodecSpecificInfo, + uint32_t aCodecSpecificInfoLength) = 0; + + // Called when the encoder encounters a catestrophic error and cannot + // continue. Gecko will not send any more input for encoding. + virtual void Error(GMPErr aError) = 0; +}; + +#define GMP_API_VIDEO_ENCODER "encode-video" + +// Video encoding for a single stream. A GMP may be asked to create multiple +// encoders concurrently. +// +// API name macro: GMP_API_VIDEO_ENCODER +// Host API: GMPVideoHost +// +// ALL METHODS MUST BE CALLED ON THE MAIN THREAD +class GMPVideoEncoder { + public: + virtual ~GMPVideoEncoder() {} + + // Initialize the encoder with the information from the VideoCodec. + // + // Input: + // - codecSettings : Codec settings + // - aCodecSpecific : codec specific data, pointer to a + // GMPCodecSpecific structure appropriate for + // this codec type. + // - aCodecSpecificLength : number of bytes in aCodecSpecific + // - aCallback: Subclass should retain reference to it until EncodingComplete + // is called. Do not attempt to delete it, host retains + // ownership. + // - aNnumberOfCores : Number of cores available for the encoder + // - aMaxPayloadSize : The maximum size each payload is allowed + // to have. Usually MTU - overhead. + virtual void InitEncode(const GMPVideoCodec& aCodecSettings, + const uint8_t* aCodecSpecific, + uint32_t aCodecSpecificLength, + GMPVideoEncoderCallback* aCallback, + int32_t aNumberOfCores, uint32_t aMaxPayloadSize) = 0; + + // Encode an I420 frame (as a part of a video stream). The encoded frame + // will be returned to the user through the encode complete callback. + // + // Input: + // - aInputFrame : Frame to be encoded + // - aCodecSpecificInfo : codec specific data, pointer to a + // GMPCodecSpecificInfo structure appropriate for + // this codec type. + // - aCodecSpecificInfoLength : number of bytes in aCodecSpecific + // - aFrameTypes : The frame type to encode + // - aFrameTypesLength : The number of elements in aFrameTypes array. + virtual void Encode(GMPVideoi420Frame* aInputFrame, + const uint8_t* aCodecSpecificInfo, + uint32_t aCodecSpecificInfoLength, + const GMPVideoFrameType* aFrameTypes, + uint32_t aFrameTypesLength) = 0; + + // Inform the encoder about the packet loss and round trip time on the + // network used to decide the best pattern and signaling. + // + // - packetLoss : Fraction lost (loss rate in percent = + // 100 * packetLoss / 255) + // - rtt : Round-trip time in milliseconds + virtual void SetChannelParameters(uint32_t aPacketLoss, uint32_t aRTT) = 0; + + // Inform the encoder about the new target bit rate. + // + // - newBitRate : New target bit rate + // - frameRate : The target frame rate + virtual void SetRates(uint32_t aNewBitRate, uint32_t aFrameRate) = 0; + + // Use this function to enable or disable periodic key frames. Can be useful + // for codecs which have other ways of stopping error propagation. + // + // - enable : Enable or disable periodic key frames + virtual void SetPeriodicKeyFrames(bool aEnable) = 0; + + // May free Encoder memory. + virtual void EncodingComplete() = 0; +}; + +#endif // GMP_VIDEO_ENCODE_h_ diff --git a/dom/media/gmp/gmp-api/gmp-video-frame-encoded.h b/dom/media/gmp/gmp-api/gmp-video-frame-encoded.h new file mode 100644 index 0000000000..42683aa129 --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-video-frame-encoded.h @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * Copyright (c) 2014, Mozilla + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + ** Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + ** Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + ** Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMP_VIDEO_FRAME_ENCODED_h_ +#define GMP_VIDEO_FRAME_ENCODED_h_ + +#include <stdint.h> +#include "gmp-video-frame.h" +#include "gmp-video-codec.h" + +enum GMPVideoFrameType { + kGMPKeyFrame = 0, + kGMPDeltaFrame = 1, + kGMPGoldenFrame = 2, + kGMPAltRefFrame = 3, + kGMPSkipFrame = 4, + kGMPVideoFrameInvalid = 5 // Must always be last. +}; + +// The implementation backing this interface uses shared memory for the +// buffer(s). This means it can only be used by the "owning" process. +// At first the process which created the object owns it. When the object +// is passed to an interface the creator loses ownership and must Destroy() +// the object. Further attempts to use it may fail due to not being able to +// access the underlying buffer(s). +// +// Methods that create or destroy shared memory must be called on the main +// thread. They are marked below. +class GMPVideoEncodedFrame : public GMPVideoFrame { + public: + // MAIN THREAD ONLY + virtual GMPErr CreateEmptyFrame(uint32_t aSize) = 0; + // MAIN THREAD ONLY + virtual GMPErr CopyFrame(const GMPVideoEncodedFrame& aVideoFrame) = 0; + virtual void SetEncodedWidth(uint32_t aEncodedWidth) = 0; + virtual uint32_t EncodedWidth() = 0; + virtual void SetEncodedHeight(uint32_t aEncodedHeight) = 0; + virtual uint32_t EncodedHeight() = 0; + // Microseconds + virtual void SetTimeStamp(uint64_t aTimeStamp) = 0; + virtual uint64_t TimeStamp() = 0; + // Set frame duration (microseconds) + // NOTE: next-frame's Timestamp() != this-frame's TimeStamp()+Duration() + // depending on rounding to avoid having to track roundoff errors + // and dropped/missing frames(!) (which may leave a large gap) + virtual void SetDuration(uint64_t aDuration) = 0; + virtual uint64_t Duration() const = 0; + virtual void SetFrameType(GMPVideoFrameType aFrameType) = 0; + virtual GMPVideoFrameType FrameType() = 0; + virtual void SetAllocatedSize(uint32_t aNewSize) = 0; + virtual uint32_t AllocatedSize() = 0; + virtual void SetSize(uint32_t aSize) = 0; + virtual uint32_t Size() = 0; + virtual void SetCompleteFrame(bool aCompleteFrame) = 0; + virtual bool CompleteFrame() = 0; + virtual const uint8_t* Buffer() const = 0; + virtual uint8_t* Buffer() = 0; + virtual GMPBufferType BufferType() const = 0; + virtual void SetBufferType(GMPBufferType aBufferType) = 0; +}; + +#endif // GMP_VIDEO_FRAME_ENCODED_h_ diff --git a/dom/media/gmp/gmp-api/gmp-video-frame-i420.h b/dom/media/gmp/gmp-api/gmp-video-frame-i420.h new file mode 100644 index 0000000000..bf64b90903 --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-video-frame-i420.h @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * Copyright (c) 2014, Mozilla + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + ** Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + ** Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + ** Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMP_VIDEO_FRAME_I420_h_ +#define GMP_VIDEO_FRAME_I420_h_ + +#include "gmp-errors.h" +#include "gmp-video-frame.h" +#include "gmp-video-plane.h" + +#include <stdint.h> + +enum GMPPlaneType { + kGMPYPlane = 0, + kGMPUPlane = 1, + kGMPVPlane = 2, + kGMPNumOfPlanes = 3 +}; + +// The implementation backing this interface uses shared memory for the +// buffer(s). This means it can only be used by the "owning" process. +// At first the process which created the object owns it. When the object +// is passed to an interface the creator loses ownership and must Destroy() +// the object. Further attempts to use it may fail due to not being able to +// access the underlying buffer(s). +// +// Methods that create or destroy shared memory must be called on the main +// thread. They are marked below. +class GMPVideoi420Frame : public GMPVideoFrame { + public: + // MAIN THREAD ONLY + // CreateEmptyFrame: Sets frame dimensions and allocates buffers based + // on set dimensions - height and plane stride. + // If required size is bigger than the allocated one, new buffers of adequate + // size will be allocated. + virtual GMPErr CreateEmptyFrame(int32_t aWidth, int32_t aHeight, + int32_t aStride_y, int32_t aStride_u, + int32_t aStride_v) = 0; + + // MAIN THREAD ONLY + // CreateFrame: Sets the frame's members and buffers. If required size is + // bigger than allocated one, new buffers of adequate size will be allocated. + virtual GMPErr CreateFrame(int32_t aSize_y, const uint8_t* aBuffer_y, + int32_t aSize_u, const uint8_t* aBuffer_u, + int32_t aSize_v, const uint8_t* aBuffer_v, + int32_t aWidth, int32_t aHeight, int32_t aStride_y, + int32_t aStride_u, int32_t aStride_v) = 0; + + // MAIN THREAD ONLY + // Copy frame: If required size is bigger than allocated one, new buffers of + // adequate size will be allocated. + virtual GMPErr CopyFrame(const GMPVideoi420Frame& aVideoFrame) = 0; + + // Swap Frame. + virtual void SwapFrame(GMPVideoi420Frame* aVideoFrame) = 0; + + // Get pointer to buffer per plane. + virtual uint8_t* Buffer(GMPPlaneType aType) = 0; + + // Overloading with const. + virtual const uint8_t* Buffer(GMPPlaneType aType) const = 0; + + // Get allocated size per plane. + virtual int32_t AllocatedSize(GMPPlaneType aType) const = 0; + + // Get allocated stride per plane. + virtual int32_t Stride(GMPPlaneType aType) const = 0; + + // Set frame width. + virtual GMPErr SetWidth(int32_t aWidth) = 0; + + // Set frame height. + virtual GMPErr SetHeight(int32_t aHeight) = 0; + + // Get frame width. + virtual int32_t Width() const = 0; + + // Get frame height. + virtual int32_t Height() const = 0; + + // Set frame timestamp (microseconds) + virtual void SetTimestamp(uint64_t aTimestamp) = 0; + + // Get frame timestamp (microseconds) + virtual uint64_t Timestamp() const = 0; + + // Set frame duration (microseconds) + // NOTE: next-frame's Timestamp() != this-frame's TimeStamp()+Duration() + // depending on rounding to avoid having to track roundoff errors + // and dropped/missing frames(!) (which may leave a large gap) + virtual void SetDuration(uint64_t aDuration) = 0; + + // Get frame duration (microseconds) + virtual uint64_t Duration() const = 0; + + // Return true if underlying plane buffers are of zero size, false if not. + virtual bool IsZeroSize() const = 0; + + // Reset underlying plane buffers sizes to 0. This function doesn't clear + // memory. + virtual void ResetSize() = 0; + + // -- These methods have been added in kGMPVersion34 -- + + // Set an updated frame timestamp (microseconds) from decoder + virtual void SetUpdatedTimestamp(uint64_t aTimestamp) = 0; + + // Get an updated frame timestamp (microseconds) from decoder + virtual uint64_t UpdatedTimestamp() const = 0; +}; + +#endif // GMP_VIDEO_FRAME_I420_h_ diff --git a/dom/media/gmp/gmp-api/gmp-video-frame.h b/dom/media/gmp/gmp-api/gmp-video-frame.h new file mode 100644 index 0000000000..8f202e1196 --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-video-frame.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * Copyright (c) 2014, Mozilla + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + ** Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + ** Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + ** Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMP_VIDEO_FRAME_h_ +#define GMP_VIDEO_FRAME_h_ + +#include "gmp-video-plane.h" + +enum GMPVideoFrameFormat { kGMPEncodedVideoFrame = 0, kGMPI420VideoFrame = 1 }; + +class GMPVideoFrame { + public: + virtual GMPVideoFrameFormat GetFrameFormat() = 0; + // MAIN THREAD ONLY IF OWNING PROCESS + virtual void Destroy() = 0; +}; + +#endif // GMP_VIDEO_FRAME_h_ diff --git a/dom/media/gmp/gmp-api/gmp-video-host.h b/dom/media/gmp/gmp-api/gmp-video-host.h new file mode 100644 index 0000000000..e1a6c742aa --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-video-host.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * Copyright (c) 2014, Mozilla + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + ** Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + ** Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + ** Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMP_VIDEO_HOST_h_ +#define GMP_VIDEO_HOST_h_ + +#include "gmp-errors.h" +#include "gmp-video-frame-i420.h" +#include "gmp-video-frame-encoded.h" +#include "gmp-video-codec.h" + +// This interface must be called on the main thread only. +class GMPVideoHost { + public: + // Construct various video API objects. Host does not retain reference, + // caller is owner and responsible for deleting. + // MAIN THREAD ONLY + virtual GMPErr CreateFrame(GMPVideoFrameFormat aFormat, + GMPVideoFrame** aFrame) = 0; + virtual GMPErr CreatePlane(GMPPlane** aPlane) = 0; +}; + +#endif // GMP_VIDEO_HOST_h_ diff --git a/dom/media/gmp/gmp-api/gmp-video-plane.h b/dom/media/gmp/gmp-api/gmp-video-plane.h new file mode 100644 index 0000000000..58ca1041ca --- /dev/null +++ b/dom/media/gmp/gmp-api/gmp-video-plane.h @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright (c) 2011, The WebRTC project authors. All rights reserved. + * Copyright (c) 2014, Mozilla + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + ** Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + ** Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + ** Neither the name of Google nor the names of its contributors may + * be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef GMP_VIDEO_PLANE_h_ +#define GMP_VIDEO_PLANE_h_ + +#include "gmp-errors.h" +#include <stdint.h> + +// The implementation backing this interface uses shared memory for the +// buffer(s). This means it can only be used by the "owning" process. +// At first the process which created the object owns it. When the object +// is passed to an interface the creator loses ownership and must Destroy() +// the object. Further attempts to use it may fail due to not being able to +// access the underlying buffer(s). +// +// Methods that create or destroy shared memory must be called on the main +// thread. They are marked below. +class GMPPlane { + public: + // MAIN THREAD ONLY + // CreateEmptyPlane - set allocated size, actual plane size and stride: + // If current size is smaller than current size, then a buffer of sufficient + // size will be allocated. + virtual GMPErr CreateEmptyPlane(int32_t aAllocatedSize, int32_t aStride, + int32_t aPlaneSize) = 0; + + // MAIN THREAD ONLY + // Copy the entire plane data. + virtual GMPErr Copy(const GMPPlane& aPlane) = 0; + + // MAIN THREAD ONLY + // Copy buffer: If current size is smaller + // than current size, then a buffer of sufficient size will be allocated. + virtual GMPErr Copy(int32_t aSize, int32_t aStride, + const uint8_t* aBuffer) = 0; + + // Swap plane data. + virtual void Swap(GMPPlane& aPlane) = 0; + + // Get allocated size. + virtual int32_t AllocatedSize() const = 0; + + // Set actual size. + virtual void ResetSize() = 0; + + // Return true is plane size is zero, false if not. + virtual bool IsZeroSize() const = 0; + + // Get stride value. + virtual int32_t Stride() const = 0; + + // Return data pointer. + virtual const uint8_t* Buffer() const = 0; + + // Overloading with non-const. + virtual uint8_t* Buffer() = 0; + + // MAIN THREAD ONLY IF OWNING PROCESS + // Call this when done with the object. This may delete it. + virtual void Destroy() = 0; +}; + +#endif // GMP_VIDEO_PLANE_h_ diff --git a/dom/media/gmp/moz.build b/dom/media/gmp/moz.build new file mode 100644 index 0000000000..4557793b4a --- /dev/null +++ b/dom/media/gmp/moz.build @@ -0,0 +1,149 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPIDL_MODULE = "content_geckomediaplugins" + +XPIDL_SOURCES += [ + "mozIGeckoMediaPluginChromeService.idl", + "mozIGeckoMediaPluginService.idl", +] + +EXPORTS += [ + "ChromiumCDMCallback.h", + "ChromiumCDMParent.h", + "ChromiumCDMProxy.h", + "DecryptJob.h", + "gmp-api/gmp-entrypoints.h", + "gmp-api/gmp-errors.h", + "gmp-api/gmp-platform.h", + "gmp-api/gmp-storage.h", + "gmp-api/gmp-video-codec.h", + "gmp-api/gmp-video-decode.h", + "gmp-api/gmp-video-encode.h", + "gmp-api/gmp-video-frame-encoded.h", + "gmp-api/gmp-video-frame-i420.h", + "gmp-api/gmp-video-frame.h", + "gmp-api/gmp-video-host.h", + "gmp-api/gmp-video-plane.h", + "GMPCallbackBase.h", + "GMPChild.h", + "GMPContentChild.h", + "GMPContentParent.h", + "GMPCrashHelper.h", + "GMPCrashHelperHolder.h", + "GMPLoader.h", + "GMPMessageUtils.h", + "GMPNativeTypes.h", + "GMPParent.h", + "GMPPlatform.h", + "GMPProcessChild.h", + "GMPProcessParent.h", + "GMPSanitizedExports.h", + "GMPService.h", + "GMPServiceChild.h", + "GMPServiceParent.h", + "GMPSharedMemManager.h", + "GMPStorage.h", + "GMPStorageChild.h", + "GMPStorageParent.h", + "GMPTimerChild.h", + "GMPTimerParent.h", + "GMPUtils.h", + "GMPVideoDecoderChild.h", + "GMPVideoDecoderParent.h", + "GMPVideoDecoderProxy.h", + "GMPVideoEncodedFrameImpl.h", + "GMPVideoEncoderChild.h", + "GMPVideoEncoderParent.h", + "GMPVideoEncoderProxy.h", + "GMPVideoHost.h", + "GMPVideoi420FrameImpl.h", + "GMPVideoPlaneImpl.h", + "widevine-adapter/content_decryption_module.h", + "widevine-adapter/content_decryption_module_export.h", + "widevine-adapter/content_decryption_module_ext.h", + "widevine-adapter/content_decryption_module_proxy.h", +] + +UNIFIED_SOURCES += [ + "CDMStorageIdProvider.cpp", + "ChromiumCDMAdapter.cpp", + "ChromiumCDMCallbackProxy.cpp", + "ChromiumCDMChild.cpp", + "ChromiumCDMParent.cpp", + "ChromiumCDMProxy.cpp", + "DecryptJob.cpp", + "GMPChild.cpp", + "GMPContentChild.cpp", + "GMPContentParent.cpp", + "GMPCrashHelperHolder.cpp", + "GMPDiskStorage.cpp", + "GMPLoader.cpp", + "GMPMemoryStorage.cpp", + "GMPParent.cpp", + "GMPPlatform.cpp", + "GMPProcessChild.cpp", + "GMPProcessParent.cpp", + "GMPService.cpp", + "GMPServiceChild.cpp", + "GMPServiceParent.cpp", + "GMPSharedMemManager.cpp", + "GMPStorageChild.cpp", + "GMPStorageParent.cpp", + "GMPTimerChild.cpp", + "GMPTimerParent.cpp", + "GMPUtils.cpp", + "GMPVideoDecoderChild.cpp", + "GMPVideoDecoderParent.cpp", + "GMPVideoEncodedFrameImpl.cpp", + "GMPVideoEncoderChild.cpp", + "GMPVideoEncoderParent.cpp", + "GMPVideoHost.cpp", + "GMPVideoi420FrameImpl.cpp", + "GMPVideoPlaneImpl.cpp", +] + +DIRS += [ + "rlz", + "widevine-adapter", +] + +IPDL_SOURCES += [ + "GMPTypes.ipdlh", + "PChromiumCDM.ipdl", + "PGMP.ipdl", + "PGMPService.ipdl", + "PGMPStorage.ipdl", + "PGMPTimer.ipdl", + "PGMPVideoDecoder.ipdl", + "PGMPVideoEncoder.ipdl", +] + +PREPROCESSED_IPDL_SOURCES += [ + "PGMPContent.ipdl", +] + +if CONFIG["OS_TARGET"] in ["WINNT", "Darwin"]: + DEFINES["SUPPORT_STORAGE_ID"] = 1 + +include("/ipc/chromium/chromium-config.mozbuild") + +if CONFIG["MOZ_SANDBOX"]: + # For sandbox includes and the include dependencies those have + LOCAL_INCLUDES += [ + "/security/sandbox/chromium", + "/security/sandbox/chromium-shim", + ] + + +FINAL_LIBRARY = "xul" +# dom/media/webrtc/transport so we work with --disable-webrtc +LOCAL_INCLUDES += [ + "/dom/media/webrtc/transport", + "/xpcom/base", + "/xpcom/build", + "/xpcom/threads", +] diff --git a/dom/media/gmp/mozIGeckoMediaPluginChromeService.idl b/dom/media/gmp/mozIGeckoMediaPluginChromeService.idl new file mode 100644 index 0000000000..51dc545092 --- /dev/null +++ b/dom/media/gmp/mozIGeckoMediaPluginChromeService.idl @@ -0,0 +1,58 @@ +/* -*- 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 "nsISupports.idl" +#include "nsIFile.idl" + +[scriptable, uuid(32d35d21-181f-4630-8caa-a431e2ebad72)] +interface mozIGeckoMediaPluginChromeService : nsISupports +{ + /** + * Add a directory to scan for gecko media plugins. + * @note Main-thread API. + */ + void addPluginDirectory(in AString directory); + + /** + * Remove a directory for gecko media plugins. + * @note Main-thread API. + */ + void removePluginDirectory(in AString directory); + + /** + * Remove a directory for gecko media plugins and delete it from disk. + * If |defer| is true, wait until the plugin is unused before removing. + * @note Main-thread API. + */ + void removeAndDeletePluginDirectory(in AString directory, + [optional] in bool defer); + + /** + * Clears storage data associated with the site and the originAttributes + * pattern in JSON format. + */ + void forgetThisSite(in AString site, + in AString aPattern); + + /** + * Clears storage data associated with the base domain + * This means cleaning any storage that is associated + * either by origin or top level origin with the base domain + */ + void forgetThisBaseDomain(in AString baseDomain); + + /** + * Returns true if the given node id is allowed to store things + * persistently on disk. Private Browsing and local content are not + * allowed to store persistent data. + */ + bool isPersistentStorageAllowed(in ACString nodeId); + + /** + * Returns the directory to use as the base for storing data about GMPs. + */ + nsIFile getStorageDir(); + +}; diff --git a/dom/media/gmp/mozIGeckoMediaPluginService.idl b/dom/media/gmp/mozIGeckoMediaPluginService.idl new file mode 100644 index 0000000000..1691e82ac7 --- /dev/null +++ b/dom/media/gmp/mozIGeckoMediaPluginService.idl @@ -0,0 +1,122 @@ +/* -*- 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 "nsISupports.idl" +#include "nsIThread.idl" + +%{C++ +#include "mozilla/UniquePtr.h" +#include "nsTArray.h" +#include "nsString.h" +class GMPDecryptorProxy; +class GMPVideoDecoderProxy; +class GMPVideoEncoderProxy; +class GMPVideoHost; + +namespace mozilla { +class GMPCrashHelper; +} + +template<class T> +class GMPGetterCallback +{ +public: + GMPGetterCallback() { MOZ_COUNT_CTOR(GMPGetterCallback<T>); } + virtual ~GMPGetterCallback() { MOZ_COUNT_DTOR(GMPGetterCallback<T>); } + virtual void Done(T*) = 0; +}; +template<class T> +class GMPVideoGetterCallback +{ +public: + GMPVideoGetterCallback() { MOZ_COUNT_CTOR(GMPVideoGetterCallback<T>); } + virtual ~GMPVideoGetterCallback() { MOZ_COUNT_DTOR(GMPVideoGetterCallback<T>); } + virtual void Done(T*, GMPVideoHost*) = 0; +}; +typedef GMPGetterCallback<GMPDecryptorProxy> GetGMPDecryptorCallback; +typedef GMPVideoGetterCallback<GMPVideoDecoderProxy> GetGMPVideoDecoderCallback; +typedef GMPVideoGetterCallback<GMPVideoEncoderProxy> GetGMPVideoEncoderCallback; +class GetNodeIdCallback +{ +public: + MOZ_COUNTED_DEFAULT_CTOR(GetNodeIdCallback) + MOZ_COUNTED_DTOR_VIRTUAL(GetNodeIdCallback) + virtual void Done(nsresult aResult, const nsACString& aNodeId) = 0; +}; +%} + +[ptr] native TagArray(nsTArray<nsCString>); +[ref] native ConstTagArrayRef(const nsTArray<nsCString>); +native GetGMPDecryptorCallback(mozilla::UniquePtr<GetGMPDecryptorCallback>&&); +native GetGMPVideoDecoderCallback(mozilla::UniquePtr<GetGMPVideoDecoderCallback>&&); +native GetGMPVideoEncoderCallback(mozilla::UniquePtr<GetGMPVideoEncoderCallback>&&); +native GetNodeIdCallback(mozilla::UniquePtr<GetNodeIdCallback>&&); +native GMPCrashHelperPtr(mozilla::GMPCrashHelper*); + +[scriptable, uuid(44d362ae-937a-4803-bee6-f2512a0149d1)] +interface mozIGeckoMediaPluginService : nsISupports +{ + + /** + * The GMP thread. Callable from any thread. + */ + readonly attribute nsIThread thread; + + /** + * Run through windows registered registered for pluginId, sending + * 'PluginCrashed' chrome-only event + */ + void RunPluginCrashCallbacks(in unsigned long pluginId, in ACString pluginName); + + /** + * Get a plugin that supports the specified tags. + * Callable on any thread + */ + [noscript] + boolean hasPluginForAPI(in ACString api, in ConstTagArrayRef tags); + + /** + * Get a video decoder that supports the specified tags. + * The array of tags should at least contain a codec tag, and optionally + * other tags such as for EME keysystem. + * Callable only on GMP thread. + * This is an asynchronous operation, the Done method of the callback object + * will be called on the GMP thread with the result (which might be null in + * the case of failure). This method always takes ownership of the callback + * object, but if this method returns an error then the Done method of the + * callback object will not be called at all. + */ + [noscript] + void getGMPVideoDecoder(in GMPCrashHelperPtr helper, + in TagArray tags, + [optional] in ACString nodeId, + in GetGMPVideoDecoderCallback callback); + + /** + * Get a video encoder that supports the specified tags. + * The array of tags should at least contain a codec tag, and optionally + * other tags. + * Callable only on GMP thread. + * This is an asynchronous operation, the Done method of the callback object + * will be called on the GMP thread with the result (which might be null in + * the case of failure). This method always takes ownership of the callback + * object, but if this method returns an error then the Done method of the + * callback object will not be called at all. + */ + [noscript] + void getGMPVideoEncoder(in GMPCrashHelperPtr helper, + in TagArray tags, + [optional] in ACString nodeId, + in GetGMPVideoEncoderCallback callback); + + /** + * Gets the NodeId for a (origin, urlbarOrigin) pair. + */ + [noscript] + void getNodeId(in AString origin, + in AString topLevelOrigin, + in AString gmpName, + in GetNodeIdCallback callback); +}; diff --git a/dom/media/gmp/rlz/OWNERS b/dom/media/gmp/rlz/OWNERS new file mode 100644 index 0000000000..66f24ebb37 --- /dev/null +++ b/dom/media/gmp/rlz/OWNERS @@ -0,0 +1,4 @@ +rogerta@chromium.org +thakis@chromium.org + +# COMPONENT: Internals>Core diff --git a/dom/media/gmp/rlz/README.mozilla b/dom/media/gmp/rlz/README.mozilla new file mode 100644 index 0000000000..4408737a5e --- /dev/null +++ b/dom/media/gmp/rlz/README.mozilla @@ -0,0 +1,4 @@ +Code taken from rlz project in Chromium repository: https://chromium.googlesource.com/chromium/src.git/+/6f3478dfd7d29b9872871718bd08493ed0c8bc8e + +Note: base/ contains wrappers/dummies to provide implementations of the +Chromium APIs that this code relies upon. diff --git a/dom/media/gmp/rlz/lib/assert.h b/dom/media/gmp/rlz/lib/assert.h new file mode 100644 index 0000000000..c64dd7ff82 --- /dev/null +++ b/dom/media/gmp/rlz/lib/assert.h @@ -0,0 +1,14 @@ +/* -*- 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/. */ + +#ifndef FAKE_ASSERT_H_ +#define FAKE_ASSERT_H_ + +#include <assert.h> + +#define ASSERT_STRING(x) { assert(false); } +#define VERIFY(x) { assert(x); }; + +#endif diff --git a/dom/media/gmp/rlz/lib/crc8.cc b/dom/media/gmp/rlz/lib/crc8.cc new file mode 100644 index 0000000000..fe02eabf1b --- /dev/null +++ b/dom/media/gmp/rlz/lib/crc8.cc @@ -0,0 +1,90 @@ +// Copyright (c) 2012 The Chromium 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 "rlz/lib/crc8.h" + +namespace { + +// The CRC lookup table used for ATM HES (Polynomial = 0x07). +// These are 256 unique 8-bit values. +const unsigned char kCrcTable[256] = { + 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, + 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D, + 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, + 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D, + 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, + 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD, + 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, + 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD, + 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, + 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA, + 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, + 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, + 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, + 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, + 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, + 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, + 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, + 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, + 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, + 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, + 0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, + 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, + 0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, + 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, + 0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, + 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, + 0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, + 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, + 0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, + 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, + 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, + 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3 +}; + +} // namespace anonymous + + +namespace rlz_lib { + +bool Crc8::Generate(const unsigned char *data, int length, + unsigned char* check_sum) { + if (!check_sum) + return false; + + *check_sum = 0; + if (!data) + return false; + + // The inital and final constants are as used in the ATM HEC. + static const unsigned char kInitial = 0x00; + static const unsigned char kFinal = 0x55; + unsigned char crc = kInitial; + for (int i = 0; i < length; ++i) { + crc = kCrcTable[(data[i] ^ crc) & 0xFFU]; + } + + *check_sum = crc ^ kFinal; + return true; +} + +bool Crc8::Verify(const unsigned char* data, int length, + unsigned char check_sum, bool* matches) { + if (!matches) + return false; + + *matches = false; + if (!data) + return false; + + unsigned char calculated_crc; + if (!Generate(data, length, &calculated_crc)) + return false; + + *matches = check_sum == calculated_crc; + + return true; +} + +} // namespace rlz_lib diff --git a/dom/media/gmp/rlz/lib/crc8.h b/dom/media/gmp/rlz/lib/crc8.h new file mode 100644 index 0000000000..6c3c84859b --- /dev/null +++ b/dom/media/gmp/rlz/lib/crc8.h @@ -0,0 +1,24 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Crc8 utility functions. + +#ifndef RLZ_LIB_CRC8_H_ +#define RLZ_LIB_CRC8_H_ + +namespace rlz_lib { +// CRC-8 methods: +class Crc8 { + public: + static bool Generate(const unsigned char* data, + int length, + unsigned char* check_sum); + static bool Verify(const unsigned char* data, + int length, + unsigned char checksum, + bool * matches); +}; +}; // namespace rlz_lib + +#endif // RLZ_LIB_CRC8_H_ diff --git a/dom/media/gmp/rlz/lib/machine_id.cc b/dom/media/gmp/rlz/lib/machine_id.cc new file mode 100644 index 0000000000..b2943f90ff --- /dev/null +++ b/dom/media/gmp/rlz/lib/machine_id.cc @@ -0,0 +1,93 @@ +// Copyright (c) 2012 The Chromium 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 "rlz/lib/machine_id.h" + +#include <stddef.h> + +#include "rlz/lib/assert.h" +#include "rlz/lib/crc8.h" +#include "rlz/lib/string_utils.h" + +// Note: The original machine_id.cc code depends on Chromium's sha1 implementation. +// Using Mozilla's implmentation as replacement to reduce the dependency of +// some external files. +#include "mozilla/SHA1.h" + +namespace rlz_lib { + +bool GetMachineId(std::string* machine_id) { + if (!machine_id) + return false; + + static std::string calculated_id; + static bool calculated = false; + if (calculated) { + *machine_id = calculated_id; + return true; + } + + std::vector<uint8_t> sid_bytes; + int volume_id; + if (!GetRawMachineId(&sid_bytes, &volume_id)) + return false; + + if (!testing::GetMachineIdImpl(sid_bytes, volume_id, machine_id)) + return false; + + calculated = true; + calculated_id = *machine_id; + return true; +} + +namespace testing { + +bool GetMachineIdImpl(const std::vector<uint8_t>& sid_bytes, + int volume_id, + std::string* machine_id) { + machine_id->clear(); + + // The ID should be the SID hash + the Hard Drive SNo. + checksum byte. + static const int kSizeWithoutChecksum = mozilla::SHA1Sum::kHashSize + sizeof(int); + std::basic_string<unsigned char> id_binary(kSizeWithoutChecksum + 1, 0); + + if (!sid_bytes.empty()) { + // In order to be compatible with the old version of RLZ, the hash of the + // SID must be done with all the original bytes from the unicode string. + // However, the chromebase SHA1 hash function takes only an std::string as + // input, so the unicode string needs to be converted to std::string + // "as is". + size_t byte_count = sid_bytes.size() * sizeof(std::vector<uint8_t>::value_type); + const char* buffer = reinterpret_cast<const char*>(sid_bytes.data()); + + // Note that digest can have embedded nulls. + mozilla::SHA1Sum SHA1; + mozilla::SHA1Sum::Hash hash; + SHA1.update(buffer, byte_count); + SHA1.finish(hash); + std::string digest(reinterpret_cast<char*>(hash), mozilla::SHA1Sum::kHashSize); + VERIFY(digest.size() == mozilla::SHA1Sum::kHashSize); + std::copy(digest.begin(), digest.end(), id_binary.begin()); + } + + // Convert from int to binary (makes big-endian). + for (size_t i = 0; i < sizeof(int); i++) { + int shift_bits = 8 * (sizeof(int) - i - 1); + id_binary[mozilla::SHA1Sum::kHashSize + i] = static_cast<unsigned char>( + (volume_id >> shift_bits) & 0xFF); + } + + // Append the checksum byte. + if (!sid_bytes.empty() || (0 != volume_id)) + rlz_lib::Crc8::Generate(id_binary.c_str(), + kSizeWithoutChecksum, + &id_binary[kSizeWithoutChecksum]); + + return rlz_lib::BytesToString( + id_binary.c_str(), kSizeWithoutChecksum + 1, machine_id); +} + +} // namespace testing + +} // namespace rlz_lib diff --git a/dom/media/gmp/rlz/lib/machine_id.h b/dom/media/gmp/rlz/lib/machine_id.h new file mode 100644 index 0000000000..5fa343efa3 --- /dev/null +++ b/dom/media/gmp/rlz/lib/machine_id.h @@ -0,0 +1,33 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RLZ_LIB_MACHINE_ID_H_ +#define RLZ_LIB_MACHINE_ID_H_ + +#include <string> +#include <vector> + +namespace rlz_lib { + +// Gets the unique ID for the machine used for RLZ tracking purposes. On +// Windows, this ID is derived from the Windows machine SID, and is the string +// representation of a 20 byte hash + 4 bytes volum id + a 1 byte checksum. +// Included in financial pings with events, unless explicitly forbidden by the +// calling application. +bool GetMachineId(std::string* machine_id); + +// Retrieves a raw machine identifier string and a machine-specific +// 4 byte value. GetMachineId() will SHA1 |data|, append |more_data|, compute +// the Crc8 of that, and return a hex-encoded string of that data. +bool GetRawMachineId(std::vector<uint8_t>* data, int* more_data); + +namespace testing { +bool GetMachineIdImpl(const std::vector<uint8_t>& sid_bytes, + int volume_id, + std::string* machine_id); +} // namespace testing + +} // namespace rlz_lib + +#endif // RLZ_LIB_MACHINE_ID_H_ diff --git a/dom/media/gmp/rlz/lib/string_utils.cc b/dom/media/gmp/rlz/lib/string_utils.cc new file mode 100644 index 0000000000..6da7323823 --- /dev/null +++ b/dom/media/gmp/rlz/lib/string_utils.cc @@ -0,0 +1,34 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// String manipulation functions used in the RLZ library. + +#include "rlz/lib/string_utils.h" + +namespace rlz_lib { + +bool BytesToString(const unsigned char* data, + int data_len, + std::string* string) { + if (!string) + return false; + + string->clear(); + if (data_len < 1 || !data) + return false; + + static const char kHex[] = "0123456789ABCDEF"; + + // Fix the buffer size to begin with to avoid repeated re-allocation. + string->resize(data_len * 2); + int index = data_len; + while (index--) { + string->at(2 * index) = kHex[data[index] >> 4]; // high digit + string->at(2 * index + 1) = kHex[data[index] & 0x0F]; // low digit + } + + return true; +} + +} // namespace rlz_lib diff --git a/dom/media/gmp/rlz/lib/string_utils.h b/dom/media/gmp/rlz/lib/string_utils.h new file mode 100644 index 0000000000..500133ade1 --- /dev/null +++ b/dom/media/gmp/rlz/lib/string_utils.h @@ -0,0 +1,20 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// String manipulation functions used in the RLZ library. + +#ifndef RLZ_LIB_STRING_UTILS_H_ +#define RLZ_LIB_STRING_UTILS_H_ + +#include <string> + +namespace rlz_lib { + +bool BytesToString(const unsigned char* data, + int data_len, + std::string* string); + +}; // namespace + +#endif // RLZ_LIB_STRING_UTILS_H_ diff --git a/dom/media/gmp/rlz/mac/lib/machine_id_mac.cc b/dom/media/gmp/rlz/mac/lib/machine_id_mac.cc new file mode 100644 index 0000000000..8c0bae22e8 --- /dev/null +++ b/dom/media/gmp/rlz/mac/lib/machine_id_mac.cc @@ -0,0 +1,322 @@ +// Copyright (c) 2012 The Chromium 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 <CoreFoundation/CoreFoundation.h> +#include <IOKit/IOKitLib.h> +#include <IOKit/network/IOEthernetController.h> +#include <IOKit/network/IOEthernetInterface.h> +#include <IOKit/network/IONetworkInterface.h> +#include <stddef.h> +#include <stdint.h> + +#include <vector> +#include <string> +// Note: The original machine_id_mac.cc code is in namespace rlz_lib below. +// It depends on some external files, which would bring in a log of Chromium +// code if imported as well. +// Instead only the necessary code has been extracted from the relevant files, +// and further combined and reduced to limit the maintenance burden. + +// [Extracted from base/logging.h] +#define DCHECK assert + +namespace base { + +// [Extracted from base/mac/scoped_typeref.h and base/mac/scoped_cftyperef.h] +template<typename T> +class ScopedCFTypeRef { + public: + typedef T element_type; + + explicit ScopedCFTypeRef(T object) + : object_(object) { + } + + ScopedCFTypeRef(const ScopedCFTypeRef<T>& that) = delete; + ScopedCFTypeRef(ScopedCFTypeRef<T>&& that) = delete; + + ~ScopedCFTypeRef() { + if (object_) + CFRelease(object_); + } + + ScopedCFTypeRef& operator=(const ScopedCFTypeRef<T>& that) = delete; + ScopedCFTypeRef& operator=(ScopedCFTypeRef<T>&& that) = delete; + + operator T() const { + return object_; + } + + // ScopedCFTypeRef<>::release() is like scoped_ptr<>::release. It is NOT + // a wrapper for CFRelease(). + T release() { + T temp = object_; + object_ = NULL; + return temp; + } + + private: + T object_; +}; + +namespace mac { + +// [Extracted from base/mac/scoped_ioobject.h] +// Just like ScopedCFTypeRef but for io_object_t and subclasses. +template<typename IOT> +class ScopedIOObject { + public: + typedef IOT element_type; + + explicit ScopedIOObject(IOT object = IO_OBJECT_NULL) + : object_(object) { + } + + ~ScopedIOObject() { + if (object_) + IOObjectRelease(object_); + } + + ScopedIOObject(const ScopedIOObject&) = delete; + void operator=(const ScopedIOObject&) = delete; + + void reset(IOT object = IO_OBJECT_NULL) { + if (object_) + IOObjectRelease(object_); + object_ = object; + } + + operator IOT() const { + return object_; + } + + private: + IOT object_; +}; + +// [Extracted from base/mac/foundation_util.h] +template<typename T> +T CFCast(const CFTypeRef& cf_val); + +template<> +CFDataRef +CFCast<CFDataRef>(const CFTypeRef& cf_val) { + if (cf_val == NULL) { + return NULL; + } + if (CFGetTypeID(cf_val) == CFDataGetTypeID()) { + return (CFDataRef)(cf_val); + } + return NULL; +} + +template<> +CFStringRef +CFCast<CFStringRef>(const CFTypeRef& cf_val) { + if (cf_val == NULL) { + return NULL; + } + if (CFGetTypeID(cf_val) == CFStringGetTypeID()) { + return (CFStringRef)(cf_val); + } + return NULL; +} + +} // namespace mac + +// [Extracted from base/strings/sys_string_conversions_mac.mm] +static const CFStringEncoding kNarrowStringEncoding = kCFStringEncodingUTF8; + +template<typename StringType> +static StringType CFStringToSTLStringWithEncodingT(CFStringRef cfstring, + CFStringEncoding encoding) { + CFIndex length = CFStringGetLength(cfstring); + if (length == 0) + return StringType(); + + CFRange whole_string = CFRangeMake(0, length); + CFIndex out_size; + CFIndex converted = CFStringGetBytes(cfstring, + whole_string, + encoding, + 0, // lossByte + false, // isExternalRepresentation + NULL, // buffer + 0, // maxBufLen + &out_size); + if (converted == 0 || out_size == 0) + return StringType(); + + // out_size is the number of UInt8-sized units needed in the destination. + // A buffer allocated as UInt8 units might not be properly aligned to + // contain elements of StringType::value_type. Use a container for the + // proper value_type, and convert out_size by figuring the number of + // value_type elements per UInt8. Leave room for a NUL terminator. + typename StringType::size_type elements = + out_size * sizeof(UInt8) / sizeof(typename StringType::value_type) + 1; + + std::vector<typename StringType::value_type> out_buffer(elements); + converted = CFStringGetBytes(cfstring, + whole_string, + encoding, + 0, // lossByte + false, // isExternalRepresentation + reinterpret_cast<UInt8*>(&out_buffer[0]), + out_size, + NULL); // usedBufLen + if (converted == 0) + return StringType(); + + out_buffer[elements - 1] = '\0'; + return StringType(&out_buffer[0], elements - 1); +} + +std::string SysCFStringRefToUTF8(CFStringRef ref) +{ + return CFStringToSTLStringWithEncodingT<std::string>(ref, + kNarrowStringEncoding); +} + +} // namespace base + +namespace rlz_lib { + +namespace { + +// See http://developer.apple.com/library/mac/#technotes/tn1103/_index.html + +// The caller is responsible for freeing |matching_services|. +bool FindEthernetInterfaces(io_iterator_t* matching_services) { + base::ScopedCFTypeRef<CFMutableDictionaryRef> matching_dict( + IOServiceMatching(kIOEthernetInterfaceClass)); + if (!matching_dict) + return false; + + base::ScopedCFTypeRef<CFMutableDictionaryRef> primary_interface( + CFDictionaryCreateMutable(kCFAllocatorDefault, + 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks)); + if (!primary_interface) + return false; + + CFDictionarySetValue( + primary_interface, CFSTR(kIOPrimaryInterface), kCFBooleanTrue); + CFDictionarySetValue( + matching_dict, CFSTR(kIOPropertyMatchKey), primary_interface); + + kern_return_t kern_result = IOServiceGetMatchingServices( + kIOMasterPortDefault, matching_dict.release(), matching_services); + + return kern_result == KERN_SUCCESS; +} + +bool GetMACAddressFromIterator(io_iterator_t primary_interface_iterator, + uint8_t* buffer, size_t buffer_size) { + if (buffer_size < kIOEthernetAddressSize) + return false; + + bool success = false; + + bzero(buffer, buffer_size); + base::mac::ScopedIOObject<io_object_t> primary_interface; + for (primary_interface.reset(IOIteratorNext(primary_interface_iterator)); + primary_interface; + primary_interface.reset(IOIteratorNext(primary_interface_iterator))) { + io_object_t primary_interface_parent; + kern_return_t kern_result = IORegistryEntryGetParentEntry( + primary_interface, kIOServicePlane, &primary_interface_parent); + base::mac::ScopedIOObject<io_object_t> primary_interface_parent_deleter( + primary_interface_parent); + success = kern_result == KERN_SUCCESS; + + if (!success) + continue; + + base::ScopedCFTypeRef<CFTypeRef> mac_data( + IORegistryEntryCreateCFProperty(primary_interface_parent, + CFSTR(kIOMACAddress), + kCFAllocatorDefault, + 0)); + CFDataRef mac_data_data = base::mac::CFCast<CFDataRef>(mac_data); + if (mac_data_data) { + CFDataGetBytes( + mac_data_data, CFRangeMake(0, kIOEthernetAddressSize), buffer); + } + } + + return success; +} + +bool GetMacAddress(unsigned char* buffer, size_t size) { + io_iterator_t primary_interface_iterator; + if (!FindEthernetInterfaces(&primary_interface_iterator)) + return false; + bool result = GetMACAddressFromIterator( + primary_interface_iterator, buffer, size); + IOObjectRelease(primary_interface_iterator); + return result; +} + +CFStringRef CopySerialNumber() { + base::mac::ScopedIOObject<io_service_t> expert_device( + IOServiceGetMatchingService(kIOMasterPortDefault, + IOServiceMatching("IOPlatformExpertDevice"))); + if (!expert_device) + return NULL; + + base::ScopedCFTypeRef<CFTypeRef> serial_number( + IORegistryEntryCreateCFProperty(expert_device, + CFSTR(kIOPlatformSerialNumberKey), + kCFAllocatorDefault, + 0)); + CFStringRef serial_number_cfstring = + base::mac::CFCast<CFStringRef>(serial_number.release()); + if (!serial_number_cfstring) + return NULL; + + return serial_number_cfstring; +} + +} // namespace + +bool GetRawMachineId(std::vector<uint8_t>* data, int* more_data) { + uint8_t mac_address[kIOEthernetAddressSize]; + + std::string id; + if (GetMacAddress(mac_address, sizeof(mac_address))) { + id += "mac:"; + static const char hex[] = + { '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + for (int i = 0; i < kIOEthernetAddressSize; ++i) { + uint8_t byte = mac_address[i]; + id += hex[byte >> 4]; + id += hex[byte & 0xF]; + } + } + + // A MAC address is enough to uniquely identify a machine, but it's only 6 + // bytes, 3 of which are manufacturer-determined. To make brute-forcing the + // SHA1 of this harder, also append the system's serial number. + CFStringRef serial = CopySerialNumber(); + if (serial) { + if (!id.empty()) { + id += ' '; + } + id += "serial:"; + id += base::SysCFStringRefToUTF8(serial); + CFRelease(serial); + } + + // Get the contents of the string 'id' as a bunch of bytes. + data->assign(&id[0], &id[id.size()]); + + // On windows, this is set to the volume id. Since it's not scrambled before + // being sent, just set it to 1. + *more_data = 1; + return true; +} + +} // namespace rlz_lib diff --git a/dom/media/gmp/rlz/moz.build b/dom/media/gmp/rlz/moz.build new file mode 100644 index 0000000000..8e2d9ea5d1 --- /dev/null +++ b/dom/media/gmp/rlz/moz.build @@ -0,0 +1,34 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +# Note: build rlz in its own moz.build, so it doesn't pickup any of +# Chromium IPC's headers used in the moz.build of the parent file. + +FINAL_LIBRARY = 'xul' + +if CONFIG['OS_TARGET'] in ['WINNT', 'Darwin']: + UNIFIED_SOURCES += [ + 'lib/crc8.cc', + 'lib/machine_id.cc', + 'lib/string_utils.cc', + ] + +if CONFIG['OS_TARGET'] == 'WINNT': + UNIFIED_SOURCES += [ + 'win/lib/machine_id_win.cc', + ] + +if CONFIG['OS_TARGET'] == 'Darwin': + UNIFIED_SOURCES += [ + 'mac/lib/machine_id_mac.cc', + ] + OS_LIBS += [ + '-framework IOKit', + ] + +LOCAL_INCLUDES += [ + '..', +] diff --git a/dom/media/gmp/rlz/win/lib/machine_id_win.cc b/dom/media/gmp/rlz/win/lib/machine_id_win.cc new file mode 100644 index 0000000000..85f1d6cf54 --- /dev/null +++ b/dom/media/gmp/rlz/win/lib/machine_id_win.cc @@ -0,0 +1,136 @@ +// Copyright (c) 2012 The Chromium 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 <windows.h> +#include <sddl.h> // For ConvertSidToStringSidW. + +#include <memory> +#include <string> +#include <vector> + +#include "mozilla/ArrayUtils.h" + +#include "rlz/lib/assert.h" + +namespace rlz_lib { + +namespace { + +bool GetSystemVolumeSerialNumber(int* number) { + if (!number) + return false; + + *number = 0; + + // Find the system root path (e.g: C:\). + wchar_t system_path[MAX_PATH + 1]; + if (!GetSystemDirectoryW(system_path, MAX_PATH)) + return false; + + wchar_t* first_slash = wcspbrk(system_path, L"\\/"); + if (first_slash != NULL) + *(first_slash + 1) = 0; + + DWORD number_local = 0; + if (!GetVolumeInformationW(system_path, NULL, 0, &number_local, NULL, NULL, + NULL, 0)) + return false; + + *number = (int)number_local; + return true; +} + +bool GetComputerSid(const wchar_t* account_name, SID* sid, DWORD sid_size) { + static const DWORD kStartDomainLength = 128; // reasonable to start with + + std::unique_ptr<wchar_t[]> domain_buffer(new wchar_t[kStartDomainLength]); + DWORD domain_size = kStartDomainLength; + DWORD sid_dword_size = sid_size; + SID_NAME_USE sid_name_use; + + BOOL success = ::LookupAccountNameW(NULL, account_name, sid, + &sid_dword_size, domain_buffer.get(), + &domain_size, &sid_name_use); + if (!success && ::GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + // We could have gotten the insufficient buffer error because + // one or both of sid and szDomain was too small. Check for that + // here. + if (sid_dword_size > sid_size) + return false; + + if (domain_size > kStartDomainLength) + domain_buffer.reset(new wchar_t[domain_size]); + + success = ::LookupAccountNameW(NULL, account_name, sid, &sid_dword_size, + domain_buffer.get(), &domain_size, + &sid_name_use); + } + + return success != FALSE; +} + +std::vector<uint8_t> ConvertSidToBytes(SID* sid) { + std::wstring sid_string; +#if _WIN32_WINNT >= 0x500 + wchar_t* sid_buffer = NULL; + if (ConvertSidToStringSidW(sid, &sid_buffer)) { + sid_string = sid_buffer; + LocalFree(sid_buffer); + } +#else + SID_IDENTIFIER_AUTHORITY* sia = ::GetSidIdentifierAuthority(sid); + + if(sia->Value[0] || sia->Value[1]) { + base::SStringPrintf( + &sid_string, L"S-%d-0x%02hx%02hx%02hx%02hx%02hx%02hx", + SID_REVISION, (USHORT)sia->Value[0], (USHORT)sia->Value[1], + (USHORT)sia->Value[2], (USHORT)sia->Value[3], (USHORT)sia->Value[4], + (USHORT)sia->Value[5]); + } else { + ULONG authority = 0; + for (int i = 2; i < 6; ++i) { + authority <<= 8; + authority |= sia->Value[i]; + } + base::SStringPrintf(&sid_string, L"S-%d-%lu", SID_REVISION, authority); + } + + int sub_auth_count = *::GetSidSubAuthorityCount(sid); + for(int i = 0; i < sub_auth_count; ++i) + base::StringAppendF(&sid_string, L"-%lu", *::GetSidSubAuthority(sid, i)); +#endif + + // Get the contents of the string as a bunch of bytes. + return std::vector<uint8_t>( + reinterpret_cast<uint8_t*>(&sid_string[0]), + reinterpret_cast<uint8_t*>(&sid_string[sid_string.size()])); +} + +} // namespace + +bool GetRawMachineId(std::vector<uint8_t>* sid_bytes, int* volume_id) { + // Calculate the Windows SID. + + wchar_t computer_name[MAX_COMPUTERNAME_LENGTH + 1] = {0}; + DWORD size = mozilla::ArrayLength(computer_name); + + if (GetComputerNameW(computer_name, &size)) { + char sid_buffer[SECURITY_MAX_SID_SIZE]; + SID* sid = reinterpret_cast<SID*>(sid_buffer); + if (GetComputerSid(computer_name, sid, SECURITY_MAX_SID_SIZE)) { + *sid_bytes = ConvertSidToBytes(sid); + } + } + + // Get the system drive volume serial number. + *volume_id = 0; + if (!GetSystemVolumeSerialNumber(volume_id)) { + ASSERT_STRING("GetMachineId: Failed to retrieve volume serial number"); + *volume_id = 0; + } + + return true; +} + +} // namespace rlz_lib diff --git a/dom/media/gmp/widevine-adapter/WidevineFileIO.cpp b/dom/media/gmp/widevine-adapter/WidevineFileIO.cpp new file mode 100644 index 0000000000..913f6ba288 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineFileIO.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "WidevineFileIO.h" +#include "GMPLog.h" +#include "WidevineUtils.h" + +#include "gmp-api/gmp-platform.h" + +// Declared in ChromiumCDMAdapter.cpp. +extern const GMPPlatformAPI* sPlatform; + +namespace mozilla { + +void WidevineFileIO::Open(const char* aFilename, uint32_t aFilenameLength) { + mName = std::string(aFilename, aFilename + aFilenameLength); + GMPRecord* record = nullptr; + GMPErr err = sPlatform->createrecord(aFilename, aFilenameLength, &record, + static_cast<GMPRecordClient*>(this)); + if (GMP_FAILED(err)) { + GMP_LOG_DEBUG("WidevineFileIO::Open() '%s' GMPCreateRecord failed", + mName.c_str()); + mClient->OnOpenComplete(cdm::FileIOClient::Status::kError); + return; + } + if (GMP_FAILED(record->Open())) { + GMP_LOG_DEBUG("WidevineFileIO::Open() '%s' record open failed", + mName.c_str()); + mClient->OnOpenComplete(cdm::FileIOClient::Status::kError); + return; + } + + GMP_LOG_DEBUG("WidevineFileIO::Open() '%s'", mName.c_str()); + mRecord = record; +} + +void WidevineFileIO::Read() { + if (!mRecord) { + GMP_LOG_DEBUG("WidevineFileIO::Read() '%s' used uninitialized!", + mName.c_str()); + mClient->OnReadComplete(cdm::FileIOClient::Status::kError, nullptr, 0); + return; + } + GMP_LOG_DEBUG("WidevineFileIO::Read() '%s'", mName.c_str()); + mRecord->Read(); +} + +void WidevineFileIO::Write(const uint8_t* aData, uint32_t aDataSize) { + if (!mRecord) { + GMP_LOG_DEBUG("WidevineFileIO::Write() '%s' used uninitialized!", + mName.c_str()); + mClient->OnWriteComplete(cdm::FileIOClient::Status::kError); + return; + } + mRecord->Write(aData, aDataSize); +} + +void WidevineFileIO::Close() { + GMP_LOG_DEBUG("WidevineFileIO::Close() '%s'", mName.c_str()); + if (mRecord) { + mRecord->Close(); + mRecord = nullptr; + } + delete this; +} + +static cdm::FileIOClient::Status GMPToWidevineFileStatus(GMPErr aStatus) { + switch (aStatus) { + case GMPRecordInUse: + return cdm::FileIOClient::Status::kInUse; + case GMPNoErr: + return cdm::FileIOClient::Status::kSuccess; + default: + return cdm::FileIOClient::Status::kError; + } +} + +void WidevineFileIO::OpenComplete(GMPErr aStatus) { + GMP_LOG_DEBUG("WidevineFileIO::OpenComplete() '%s' status=%d", mName.c_str(), + aStatus); + mClient->OnOpenComplete(GMPToWidevineFileStatus(aStatus)); +} + +void WidevineFileIO::ReadComplete(GMPErr aStatus, const uint8_t* aData, + uint32_t aDataSize) { + GMP_LOG_DEBUG("WidevineFileIO::OnReadComplete() '%s' status=%d", + mName.c_str(), aStatus); + mClient->OnReadComplete(GMPToWidevineFileStatus(aStatus), aData, aDataSize); +} + +void WidevineFileIO::WriteComplete(GMPErr aStatus) { + GMP_LOG_DEBUG("WidevineFileIO::WriteComplete() '%s' status=%d", mName.c_str(), + aStatus); + mClient->OnWriteComplete(GMPToWidevineFileStatus(aStatus)); +} + +} // namespace mozilla diff --git a/dom/media/gmp/widevine-adapter/WidevineFileIO.h b/dom/media/gmp/widevine-adapter/WidevineFileIO.h new file mode 100644 index 0000000000..8c36f032be --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineFileIO.h @@ -0,0 +1,41 @@ +/* -*- 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/. */ + +#ifndef WidevineFileIO_h_ +#define WidevineFileIO_h_ + +#include <stddef.h> +#include "content_decryption_module.h" +#include "gmp-api/gmp-storage.h" +#include <string> + +namespace mozilla { + +class WidevineFileIO : public cdm::FileIO, public GMPRecordClient { + public: + explicit WidevineFileIO(cdm::FileIOClient* aClient) + : mClient(aClient), mRecord(nullptr) {} + + // cdm::FileIO + void Open(const char* aFilename, uint32_t aFilenameLength) override; + void Read() override; + void Write(const uint8_t* aData, uint32_t aDataSize) override; + void Close() override; + + // GMPRecordClient + void OpenComplete(GMPErr aStatus) override; + void ReadComplete(GMPErr aStatus, const uint8_t* aData, + uint32_t aDataSize) override; + void WriteComplete(GMPErr aStatus) override; + + private: + cdm::FileIOClient* mClient; + GMPRecord* mRecord; + std::string mName; +}; + +} // namespace mozilla + +#endif // WidevineFileIO_h_ diff --git a/dom/media/gmp/widevine-adapter/WidevineUtils.cpp b/dom/media/gmp/widevine-adapter/WidevineUtils.cpp new file mode 100644 index 0000000000..d0551406b2 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineUtils.cpp @@ -0,0 +1,61 @@ +/* -*- 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 "WidevineUtils.h" +#include "GMPLog.h" +#include "gmp-api/gmp-errors.h" +#include <stdarg.h> +#include <stdio.h> +#include <inttypes.h> + +namespace mozilla { + +WidevineBuffer::WidevineBuffer(size_t aSize) { + GMP_LOG_DEBUG("WidevineBuffer(size=%zu) created", aSize); + mBuffer.SetLength(aSize); +} + +WidevineBuffer::~WidevineBuffer() { + GMP_LOG_DEBUG("WidevineBuffer(size=%" PRIu32 ") destroyed", Size()); +} + +void WidevineBuffer::Destroy() { delete this; } + +uint32_t WidevineBuffer::Capacity() const { return mBuffer.Length(); } + +uint8_t* WidevineBuffer::Data() { return mBuffer.Elements(); } + +void WidevineBuffer::SetSize(uint32_t aSize) { mBuffer.SetLength(aSize); } + +uint32_t WidevineBuffer::Size() const { return mBuffer.Length(); } + +nsTArray<uint8_t> WidevineBuffer::ExtractBuffer() { + nsTArray<uint8_t> out = std::move(mBuffer); + return out; +} + +WidevineDecryptedBlock::WidevineDecryptedBlock() + : mBuffer(nullptr), mTimestamp(0) {} + +WidevineDecryptedBlock::~WidevineDecryptedBlock() { + if (mBuffer) { + mBuffer->Destroy(); + mBuffer = nullptr; + } +} + +void WidevineDecryptedBlock::SetDecryptedBuffer(cdm::Buffer* aBuffer) { + mBuffer = aBuffer; +} + +cdm::Buffer* WidevineDecryptedBlock::DecryptedBuffer() { return mBuffer; } + +void WidevineDecryptedBlock::SetTimestamp(int64_t aTimestamp) { + mTimestamp = aTimestamp; +} + +int64_t WidevineDecryptedBlock::Timestamp() const { return mTimestamp; } + +} // namespace mozilla diff --git a/dom/media/gmp/widevine-adapter/WidevineUtils.h b/dom/media/gmp/widevine-adapter/WidevineUtils.h new file mode 100644 index 0000000000..1502621451 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineUtils.h @@ -0,0 +1,84 @@ +/* -*- 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/. */ + +#ifndef WidevineUtils_h_ +#define WidevineUtils_h_ + +#include "stddef.h" +#include "content_decryption_module.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" +#include "mozilla/Logging.h" + +namespace mozilla { + +#define ENSURE_TRUE(condition, rv) \ + { \ + if (!(condition)) { \ + GMP_LOG_DEBUG("ENSURE_TRUE FAILED %s:%d", __FILE__, __LINE__); \ + return rv; \ + } \ + } + +#define ENSURE_GMP_SUCCESS(err, rv) \ + { \ + if (GMP_FAILED(err)) { \ + GMP_LOG_DEBUG("ENSURE_GMP_SUCCESS FAILED %s:%d", __FILE__, __LINE__); \ + return rv; \ + } \ + } + +namespace gmp { +class CDMShmemBuffer; +} +class WidevineBuffer; + +// Base class for our cdm::Buffer implementations, so we can tell at runtime +// whether the buffer is a Shmem or non-Shmem buffer. +class CDMBuffer : public cdm::Buffer { + public: + virtual WidevineBuffer* AsArrayBuffer() { return nullptr; } + virtual gmp::CDMShmemBuffer* AsShmemBuffer() { return nullptr; } +}; + +class WidevineBuffer : public CDMBuffer { + public: + explicit WidevineBuffer(size_t aSize); + ~WidevineBuffer() override; + void Destroy() override; + uint32_t Capacity() const override; + uint8_t* Data() override; + void SetSize(uint32_t aSize) override; + uint32_t Size() const override; + + // Moves contents of buffer out into temporary. + // Note: This empties the buffer. + nsTArray<uint8_t> ExtractBuffer(); + + WidevineBuffer* AsArrayBuffer() override { return this; } + + private: + nsTArray<uint8_t> mBuffer; + WidevineBuffer(const WidevineBuffer&); + void operator=(const WidevineBuffer&); +}; + +class WidevineDecryptedBlock : public cdm::DecryptedBlock { + public: + WidevineDecryptedBlock(); + ~WidevineDecryptedBlock() override; + void SetDecryptedBuffer(cdm::Buffer* aBuffer) override; + cdm::Buffer* DecryptedBuffer() override; + void SetTimestamp(int64_t aTimestamp) override; + int64_t Timestamp() const override; + + private: + cdm::Buffer* mBuffer; + int64_t mTimestamp; +}; + +} // namespace mozilla + +#endif // WidevineUtils_h_ diff --git a/dom/media/gmp/widevine-adapter/WidevineVideoFrame.cpp b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.cpp new file mode 100644 index 0000000000..3f136c4ac8 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.cpp @@ -0,0 +1,142 @@ +/* -*- 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 "WidevineVideoFrame.h" +#include "GMPLog.h" +#include "WidevineUtils.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/IntegerPrintfMacros.h" + +namespace mozilla { + +WidevineVideoFrame::WidevineVideoFrame() + : mFormat(cdm::VideoFormat::kUnknownVideoFormat), + mSize{0, 0}, + mBuffer(nullptr), + mTimestamp(0) { + MOZ_ASSERT(mSize.height == 0 && mSize.width == 0, "Size should be zeroed"); + GMP_LOG_DEBUG("WidevineVideoFrame::WidevineVideoFrame() this=%p", this); + memset(mPlaneOffsets, 0, sizeof(mPlaneOffsets)); + memset(mPlaneStrides, 0, sizeof(mPlaneStrides)); +} + +WidevineVideoFrame::WidevineVideoFrame(WidevineVideoFrame&& aOther) + : mFormat(aOther.mFormat), + mSize(aOther.mSize), + mBuffer(aOther.mBuffer), + mTimestamp(aOther.mTimestamp) { + GMP_LOG_DEBUG( + "WidevineVideoFrame::WidevineVideoFrame(WidevineVideoFrame&&) " + "this=%p, other=%p", + this, &aOther); + memcpy(mPlaneOffsets, aOther.mPlaneOffsets, sizeof(mPlaneOffsets)); + memcpy(mPlaneStrides, aOther.mPlaneStrides, sizeof(mPlaneStrides)); + aOther.mBuffer = nullptr; +} + +WidevineVideoFrame::~WidevineVideoFrame() { + if (mBuffer) { + mBuffer->Destroy(); + mBuffer = nullptr; + } +} + +void WidevineVideoFrame::SetFormat(cdm::VideoFormat aFormat) { + GMP_LOG_DEBUG("WidevineVideoFrame::SetFormat(%d) this=%p", aFormat, this); + mFormat = aFormat; +} + +cdm::VideoFormat WidevineVideoFrame::Format() const { return mFormat; } + +void WidevineVideoFrame::SetSize(cdm::Size aSize) { + GMP_LOG_DEBUG("WidevineVideoFrame::SetSize(%d,%d) this=%p", aSize.width, + aSize.height, this); + mSize.width = aSize.width; + mSize.height = aSize.height; +} + +cdm::Size WidevineVideoFrame::Size() const { return mSize; } + +void WidevineVideoFrame::SetFrameBuffer(cdm::Buffer* aFrameBuffer) { + GMP_LOG_DEBUG("WidevineVideoFrame::SetFrameBuffer(%p) this=%p", aFrameBuffer, + this); + MOZ_ASSERT(!mBuffer); + mBuffer = aFrameBuffer; +} + +cdm::Buffer* WidevineVideoFrame::FrameBuffer() { return mBuffer; } + +void WidevineVideoFrame::SetPlaneOffset(cdm::VideoPlane aPlane, + uint32_t aOffset) { + GMP_LOG_DEBUG("WidevineVideoFrame::SetPlaneOffset(%d, %" PRIu32 ") this=%p", + aPlane, aOffset, this); + mPlaneOffsets[aPlane] = aOffset; +} + +uint32_t WidevineVideoFrame::PlaneOffset(cdm::VideoPlane aPlane) { + return mPlaneOffsets[aPlane]; +} + +void WidevineVideoFrame::SetStride(cdm::VideoPlane aPlane, uint32_t aStride) { + GMP_LOG_DEBUG("WidevineVideoFrame::SetStride(%d, %" PRIu32 ") this=%p", + aPlane, aStride, this); + mPlaneStrides[aPlane] = aStride; +} + +uint32_t WidevineVideoFrame::Stride(cdm::VideoPlane aPlane) { + return mPlaneStrides[aPlane]; +} + +void WidevineVideoFrame::SetTimestamp(int64_t timestamp) { + GMP_LOG_DEBUG("WidevineVideoFrame::SetTimestamp(%" PRId64 ") this=%p", + timestamp, this); + mTimestamp = timestamp; +} + +int64_t WidevineVideoFrame::Timestamp() const { return mTimestamp; } + +bool WidevineVideoFrame::InitToBlack(int32_t aWidth, int32_t aHeight, + int64_t aTimeStamp) { + if (NS_WARN_IF(aWidth < 0 || aHeight < 0)) { + MOZ_ASSERT_UNREACHABLE("Frame dimensions should be positive"); + return false; + } + + const uint32_t halfWidth = (uint32_t(aWidth) + 1) / 2; + CheckedInt<size_t> ySizeChk = aWidth; + ySizeChk *= aHeight; + CheckedInt<size_t> uSizeChk = halfWidth; + uSizeChk *= (uint32_t(aHeight) + 1) / 2; + CheckedInt<size_t> yuSizeChk = ySizeChk + uSizeChk; + if (NS_WARN_IF(!yuSizeChk.isValid())) { + return false; + } + + WidevineBuffer* buffer = new WidevineBuffer(yuSizeChk.value()); + const size_t ySize = ySizeChk.value(); + const size_t uSize = uSizeChk.value(); + // Black in YCbCr is (0,128,128). + memset(buffer->Data(), 0, ySize); + memset(buffer->Data() + ySize, 128, uSize); + if (mBuffer) { + mBuffer->Destroy(); + mBuffer = nullptr; + } + SetFormat(cdm::VideoFormat::kI420); + SetSize(cdm::Size{aWidth, aHeight}); + SetFrameBuffer(buffer); + SetPlaneOffset(cdm::VideoPlane::kYPlane, 0); + SetStride(cdm::VideoPlane::kYPlane, aWidth); + // Note: U and V planes are stored at the same place in order to + // save memory since their contents are the same. + SetPlaneOffset(cdm::VideoPlane::kUPlane, ySize); + SetStride(cdm::VideoPlane::kUPlane, halfWidth); + SetPlaneOffset(cdm::VideoPlane::kVPlane, ySize); + SetStride(cdm::VideoPlane::kVPlane, halfWidth); + SetTimestamp(aTimeStamp); + return true; +} + +} // namespace mozilla diff --git a/dom/media/gmp/widevine-adapter/WidevineVideoFrame.h b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.h new file mode 100644 index 0000000000..0ae65ac1e5 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.h @@ -0,0 +1,54 @@ +/* -*- 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/. */ + +#ifndef WidevineVideoFrame_h_ +#define WidevineVideoFrame_h_ + +#include "stddef.h" +#include "content_decryption_module.h" +#include "mozilla/Attributes.h" +#include <vector> + +namespace mozilla { + +class WidevineVideoFrame : public cdm::VideoFrame { + public: + WidevineVideoFrame(); + WidevineVideoFrame(WidevineVideoFrame&& other); + ~WidevineVideoFrame(); + + void SetFormat(cdm::VideoFormat aFormat) override; + cdm::VideoFormat Format() const override; + + void SetSize(cdm::Size aSize) override; + cdm::Size Size() const override; + + void SetFrameBuffer(cdm::Buffer* aFrameBuffer) override; + cdm::Buffer* FrameBuffer() override; + + void SetPlaneOffset(cdm::VideoPlane aPlane, uint32_t aOffset) override; + uint32_t PlaneOffset(cdm::VideoPlane aPlane) override; + + void SetStride(cdm::VideoPlane aPlane, uint32_t aStride) override; + uint32_t Stride(cdm::VideoPlane aPlane) override; + + void SetTimestamp(int64_t aTimestamp) override; + int64_t Timestamp() const override; + + [[nodiscard]] bool InitToBlack(int32_t aWidth, int32_t aHeight, + int64_t aTimeStamp); + + protected: + cdm::VideoFormat mFormat; + cdm::Size mSize; + cdm::Buffer* mBuffer; + uint32_t mPlaneOffsets[cdm::VideoPlane::kMaxPlanes]; + uint32_t mPlaneStrides[cdm::VideoPlane::kMaxPlanes]; + int64_t mTimestamp; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module.h b/dom/media/gmp/widevine-adapter/content_decryption_module.h new file mode 100644 index 0000000000..68fee35195 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/content_decryption_module.h @@ -0,0 +1,1359 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CDM_CONTENT_DECRYPTION_MODULE_H_ +#define CDM_CONTENT_DECRYPTION_MODULE_H_ + +#include <type_traits> + +#include "content_decryption_module_export.h" +#include "content_decryption_module_proxy.h" + +#if defined(_MSC_VER) +typedef unsigned char uint8_t; +typedef unsigned int uint32_t; +typedef int int32_t; +typedef __int64 int64_t; +#else +# include <stdint.h> +#endif + +// The version number must be rolled when the exported functions are updated! +// If the CDM and the adapter use different versions of these functions, the +// adapter will fail to load or crash! +#define CDM_MODULE_VERSION 4 + +// Build the versioned entrypoint name. +// The extra macros are necessary to expand version to an actual value. +#define INITIALIZE_CDM_MODULE \ + BUILD_ENTRYPOINT(InitializeCdmModule, CDM_MODULE_VERSION) +#define BUILD_ENTRYPOINT(name, version) \ + BUILD_ENTRYPOINT_NO_EXPANSION(name, version) +#define BUILD_ENTRYPOINT_NO_EXPANSION(name, version) name##_##version + +// Macro to check that |type| does the following: +// 1. is a standard layout. +// 2. is trivial. +// 3. sizeof(type) matches the expected size in bytes. As some types contain +// pointers, the size is specified for both 32 and 64 bit. +#define CHECK_TYPE(type, size_32, size_64) \ + static_assert(std::is_standard_layout<type>(), \ + #type " not standard_layout"); \ + static_assert(std::is_trivial<type>(), #type " not trivial"); \ + static_assert((sizeof(void*) == 4 && sizeof(type) == size_32) || \ + (sizeof(void*) == 8 && sizeof(type) == size_64), \ + #type " size mismatch") + +extern "C" { + +CDM_API void INITIALIZE_CDM_MODULE(); + +CDM_API void DeinitializeCdmModule(); + +// Returns a pointer to the requested CDM Host interface upon success. +// Returns NULL if the requested CDM Host interface is not supported. +// The caller should cast the returned pointer to the type matching +// |host_interface_version|. +typedef void* (*GetCdmHostFunc)(int host_interface_version, void* user_data); + +// Returns a pointer to the requested CDM upon success. +// Returns NULL if an error occurs or the requested |cdm_interface_version| or +// |key_system| is not supported or another error occurs. +// The caller should cast the returned pointer to the type matching +// |cdm_interface_version|. +// Caller retains ownership of arguments and must call Destroy() on the returned +// object. +CDM_API void* CreateCdmInstance(int cdm_interface_version, + const char* key_system, + uint32_t key_system_size, + GetCdmHostFunc get_cdm_host_func, + void* user_data); + +CDM_API const char* GetCdmVersion(); + +} // extern "C" + +namespace cdm { + +enum Status : uint32_t { + kSuccess = 0, + kNeedMoreData, // Decoder needs more data to produce a decoded frame/sample. + kNoKey, // The required decryption key is not available. + kInitializationError, // Initialization error. + kDecryptError, // Decryption failed. + kDecodeError, // Error decoding audio or video. + kDeferredInitialization // Decoder is not ready for initialization. +}; +CHECK_TYPE(Status, 4, 4); + +// Exceptions used by the CDM to reject promises. +// https://w3c.github.io/encrypted-media/#exceptions +enum Exception : uint32_t { + kExceptionTypeError, + kExceptionNotSupportedError, + kExceptionInvalidStateError, + kExceptionQuotaExceededError +}; +CHECK_TYPE(Exception, 4, 4); + +// The encryption scheme. The definitions are from ISO/IEC 23001-7:2016. +enum class EncryptionScheme : uint32_t { + kUnencrypted = 0, + kCenc, // 'cenc' subsample encryption using AES-CTR mode. + kCbcs // 'cbcs' pattern encryption using AES-CBC mode. +}; +CHECK_TYPE(EncryptionScheme, 4, 4); + +// The pattern used for pattern encryption. Note that ISO/IEC 23001-7:2016 +// defines each block to be 16-bytes. +struct Pattern { + uint32_t crypt_byte_block; // Count of the encrypted blocks. + uint32_t skip_byte_block; // Count of the unencrypted blocks. +}; +CHECK_TYPE(Pattern, 8, 8); + +enum class ColorRange : uint8_t { + kInvalid, + kLimited, // 709 color range with RGB values ranging from 16 to 235. + kFull, // Full RGB color range with RGB values from 0 to 255. + kDerived // Range is defined by |transfer_id| and |matrix_id|. +}; +CHECK_TYPE(ColorRange, 1, 1); + +// Described in ISO 23001-8:2016, section 7. All the IDs are in the range +// [0, 255] so 8-bit integer is sufficient. An unspecified ColorSpace should be +// {2, 2, 2, ColorRange::kInvalid}, where value 2 means "Unspecified" for all +// the IDs, as defined by the spec. +struct ColorSpace { + uint8_t primary_id; // 7.1 colour primaries, table 2 + uint8_t transfer_id; // 7.2 transfer characteristics, table 3 + uint8_t matrix_id; // 7.3 matrix coefficients, table 4 + ColorRange range; +}; +CHECK_TYPE(ColorSpace, 4, 4); + +// Time is defined as the number of seconds since the Epoch +// (00:00:00 UTC, January 1, 1970), not including any added leap second. +// Also see Time definition in spec: https://w3c.github.io/encrypted-media/#time +// Note that Time is defined in millisecond accuracy in the spec but in second +// accuracy here. +typedef double Time; + +// An input buffer can be split into several continuous subsamples. +// A SubsampleEntry specifies the number of clear and cipher bytes in each +// subsample. For example, the following buffer has three subsamples: +// +// |<----- subsample1 ----->|<----- subsample2 ----->|<----- subsample3 ----->| +// | clear1 | cipher1 | clear2 | cipher2 | clear3 | cipher3 | +// +// For decryption, all of the cipher bytes in a buffer should be concatenated +// (in the subsample order) into a single logical stream. The clear bytes should +// not be considered as part of decryption. +// +// Stream to decrypt: | cipher1 | cipher2 | cipher3 | +// Decrypted stream: | decrypted1| decrypted2 | decrypted3 | +// +// After decryption, the decrypted bytes should be copied over the position +// of the corresponding cipher bytes in the original buffer to form the output +// buffer. Following the above example, the decrypted buffer should be: +// +// |<----- subsample1 ----->|<----- subsample2 ----->|<----- subsample3 ----->| +// | clear1 | decrypted1| clear2 | decrypted2 | clear3 | decrypted3 | +// +struct SubsampleEntry { + uint32_t clear_bytes; + uint32_t cipher_bytes; +}; +CHECK_TYPE(SubsampleEntry, 8, 8); + +// Represents an input buffer to be decrypted (and possibly decoded). It does +// not own any pointers in this struct. If |encryption_scheme| = kUnencrypted, +// the data is unencrypted. +// Note that this struct is organized so that sizeof(InputBuffer_2) +// equals the sum of sizeof() all members in both 32-bit and 64-bit compiles. +// Padding has been added to keep the fields aligned. +struct InputBuffer_2 { + const uint8_t* data; // Pointer to the beginning of the input data. + uint32_t data_size; // Size (in bytes) of |data|. + + EncryptionScheme encryption_scheme; + + const uint8_t* key_id; // Key ID to identify the decryption key. + uint32_t key_id_size; // Size (in bytes) of |key_id|. + uint32_t : 32; // Padding. + + const uint8_t* iv; // Initialization vector. + uint32_t iv_size; // Size (in bytes) of |iv|. + uint32_t : 32; // Padding. + + const struct SubsampleEntry* subsamples; + uint32_t num_subsamples; // Number of subsamples in |subsamples|. + uint32_t : 32; // Padding. + + // |pattern| is required if |encryption_scheme| specifies pattern encryption. + Pattern pattern; + + int64_t timestamp; // Presentation timestamp in microseconds. +}; +CHECK_TYPE(InputBuffer_2, 64, 80); + +enum AudioCodec : uint32_t { kUnknownAudioCodec = 0, kCodecVorbis, kCodecAac }; +CHECK_TYPE(AudioCodec, 4, 4); + +struct AudioDecoderConfig_2 { + AudioCodec codec; + int32_t channel_count; + int32_t bits_per_channel; + int32_t samples_per_second; + + // Optional byte data required to initialize audio decoders, such as the + // vorbis setup header. + uint8_t* extra_data; + uint32_t extra_data_size; + + // Encryption scheme. + EncryptionScheme encryption_scheme; +}; +CHECK_TYPE(AudioDecoderConfig_2, 28, 32); + +// Supported sample formats for AudioFrames. +enum AudioFormat : uint32_t { + kUnknownAudioFormat = 0, // Unknown format value. Used for error reporting. + kAudioFormatU8, // Interleaved unsigned 8-bit w/ bias of 128. + kAudioFormatS16, // Interleaved signed 16-bit. + kAudioFormatS32, // Interleaved signed 32-bit. + kAudioFormatF32, // Interleaved float 32-bit. + kAudioFormatPlanarS16, // Signed 16-bit planar. + kAudioFormatPlanarF32, // Float 32-bit planar. +}; +CHECK_TYPE(AudioFormat, 4, 4); + +// Surface formats based on FOURCC labels, see: http://www.fourcc.org/yuv.php +// Values are chosen to be consistent with Chromium's VideoPixelFormat values. +enum VideoFormat : uint32_t { + kUnknownVideoFormat = 0, // Unknown format value. Used for error reporting. + kYv12 = 1, // 12bpp YVU planar 1x1 Y, 2x2 VU samples. + kI420 = 2, // 12bpp YUV planar 1x1 Y, 2x2 UV samples. + + // In the following formats, each sample uses 16-bit in storage, while the + // sample value is stored in the least significant N bits where N is + // specified by the number after "P". For example, for YUV420P9, each Y, U, + // and V sample is stored in the least significant 9 bits in a 2-byte block. + kYUV420P9 = 16, + kYUV420P10 = 17, + kYUV422P9 = 18, + kYUV422P10 = 19, + kYUV444P9 = 20, + kYUV444P10 = 21, + kYUV420P12 = 22, + kYUV422P12 = 23, + kYUV444P12 = 24, +}; +CHECK_TYPE(VideoFormat, 4, 4); + +struct Size { + int32_t width; + int32_t height; +}; +CHECK_TYPE(Size, 8, 8); + +enum VideoCodec : uint32_t { + kUnknownVideoCodec = 0, + kCodecVp8, + kCodecH264, + kCodecVp9, + kCodecAv1 +}; +CHECK_TYPE(VideoCodec, 4, 4); + +enum VideoCodecProfile : uint32_t { + kUnknownVideoCodecProfile = 0, + kProfileNotNeeded, + kH264ProfileBaseline, + kH264ProfileMain, + kH264ProfileExtended, + kH264ProfileHigh, + kH264ProfileHigh10, + kH264ProfileHigh422, + kH264ProfileHigh444Predictive, + kVP9Profile0, + kVP9Profile1, + kVP9Profile2, + kVP9Profile3, + kAv1ProfileMain, + kAv1ProfileHigh, + kAv1ProfilePro +}; +CHECK_TYPE(VideoCodecProfile, 4, 4); + +// Deprecated: New CDM implementations should use VideoDecoderConfig_3. +// Note that this struct is organized so that sizeof(VideoDecoderConfig_2) +// equals the sum of sizeof() all members in both 32-bit and 64-bit compiles. +// Padding has been added to keep the fields aligned. +struct VideoDecoderConfig_2 { + VideoCodec codec; + VideoCodecProfile profile; + VideoFormat format; + uint32_t : 32; // Padding. + + // Width and height of video frame immediately post-decode. Not all pixels + // in this region are valid. + Size coded_size; + + // Optional byte data required to initialize video decoders, such as H.264 + // AAVC data. + uint8_t* extra_data; + uint32_t extra_data_size; + + // Encryption scheme. + EncryptionScheme encryption_scheme; +}; +CHECK_TYPE(VideoDecoderConfig_2, 36, 40); + +struct VideoDecoderConfig_3 { + VideoCodec codec; + VideoCodecProfile profile; + VideoFormat format; + ColorSpace color_space; + + // Width and height of video frame immediately post-decode. Not all pixels + // in this region are valid. + Size coded_size; + + // Optional byte data required to initialize video decoders, such as H.264 + // AAVC data. + uint8_t* extra_data; + uint32_t extra_data_size; + + EncryptionScheme encryption_scheme; +}; +CHECK_TYPE(VideoDecoderConfig_3, 36, 40); + +enum StreamType : uint32_t { kStreamTypeAudio = 0, kStreamTypeVideo = 1 }; +CHECK_TYPE(StreamType, 4, 4); + +// Structure provided to ContentDecryptionModule::OnPlatformChallengeResponse() +// after a platform challenge was initiated via Host::SendPlatformChallenge(). +// All values will be NULL / zero in the event of a challenge failure. +struct PlatformChallengeResponse { + // |challenge| provided during Host::SendPlatformChallenge() combined with + // nonce data and signed with the platform's private key. + const uint8_t* signed_data; + uint32_t signed_data_length; + + // RSASSA-PKCS1-v1_5-SHA256 signature of the |signed_data| block. + const uint8_t* signed_data_signature; + uint32_t signed_data_signature_length; + + // X.509 device specific certificate for the |service_id| requested. + const uint8_t* platform_key_certificate; + uint32_t platform_key_certificate_length; +}; +CHECK_TYPE(PlatformChallengeResponse, 24, 48); + +// The current status of the associated key. The valid types are defined in the +// spec: https://w3c.github.io/encrypted-media/#dom-mediakeystatus +enum KeyStatus : uint32_t { + kUsable = 0, + kInternalError = 1, + kExpired = 2, + kOutputRestricted = 3, + kOutputDownscaled = 4, + kStatusPending = 5, + kReleased = 6 +}; +CHECK_TYPE(KeyStatus, 4, 4); + +// Used when passing arrays of key information. Does not own the referenced +// data. |system_code| is an additional error code for unusable keys and +// should be 0 when |status| == kUsable. +struct KeyInformation { + const uint8_t* key_id; + uint32_t key_id_size; + KeyStatus status; + uint32_t system_code; +}; +CHECK_TYPE(KeyInformation, 16, 24); + +// Supported output protection methods for use with EnableOutputProtection() and +// returned by OnQueryOutputProtectionStatus(). +enum OutputProtectionMethods : uint32_t { + kProtectionNone = 0, + kProtectionHDCP = 1 << 0 +}; +CHECK_TYPE(OutputProtectionMethods, 4, 4); + +// Connected output link types returned by OnQueryOutputProtectionStatus(). +enum OutputLinkTypes : uint32_t { + kLinkTypeNone = 0, + kLinkTypeUnknown = 1 << 0, + kLinkTypeInternal = 1 << 1, + kLinkTypeVGA = 1 << 2, + kLinkTypeHDMI = 1 << 3, + kLinkTypeDVI = 1 << 4, + kLinkTypeDisplayPort = 1 << 5, + kLinkTypeNetwork = 1 << 6 +}; +CHECK_TYPE(OutputLinkTypes, 4, 4); + +// Result of the QueryOutputProtectionStatus() call. +enum QueryResult : uint32_t { kQuerySucceeded = 0, kQueryFailed }; +CHECK_TYPE(QueryResult, 4, 4); + +// The Initialization Data Type. The valid types are defined in the spec: +// https://w3c.github.io/encrypted-media/format-registry/initdata/index.html#registry +enum InitDataType : uint32_t { kCenc = 0, kKeyIds = 1, kWebM = 2 }; +CHECK_TYPE(InitDataType, 4, 4); + +// The type of session to create. The valid types are defined in the spec: +// https://w3c.github.io/encrypted-media/#dom-mediakeysessiontype +enum SessionType : uint32_t { + kTemporary = 0, + kPersistentLicense = 1, + kPersistentUsageRecord = 2 +}; +CHECK_TYPE(SessionType, 4, 4); + +// The type of the message event. The valid types are defined in the spec: +// https://w3c.github.io/encrypted-media/#dom-mediakeymessagetype +enum MessageType : uint32_t { + kLicenseRequest = 0, + kLicenseRenewal = 1, + kLicenseRelease = 2, + kIndividualizationRequest = 3 +}; +CHECK_TYPE(MessageType, 4, 4); + +enum HdcpVersion : uint32_t { + kHdcpVersionNone, + kHdcpVersion1_0, + kHdcpVersion1_1, + kHdcpVersion1_2, + kHdcpVersion1_3, + kHdcpVersion1_4, + kHdcpVersion2_0, + kHdcpVersion2_1, + kHdcpVersion2_2, + kHdcpVersion2_3 +}; +CHECK_TYPE(HdcpVersion, 4, 4); + +struct Policy { + HdcpVersion min_hdcp_version; +}; +CHECK_TYPE(Policy, 4, 4); + +// Represents a buffer created by Allocator implementations. +class CDM_CLASS_API Buffer { + public: + // Destroys the buffer in the same context as it was created. + virtual void Destroy() = 0; + + virtual uint32_t Capacity() const = 0; + virtual uint8_t* Data() = 0; + virtual void SetSize(uint32_t size) = 0; + virtual uint32_t Size() const = 0; + + protected: + Buffer() {} + virtual ~Buffer() {} + + private: + Buffer(const Buffer&); + void operator=(const Buffer&); +}; + +// Represents a decrypted block that has not been decoded. +class CDM_CLASS_API DecryptedBlock { + public: + virtual void SetDecryptedBuffer(Buffer* buffer) = 0; + virtual Buffer* DecryptedBuffer() = 0; + + // TODO(tomfinegan): Figure out if timestamp is really needed. If it is not, + // we can just pass Buffer pointers around. + virtual void SetTimestamp(int64_t timestamp) = 0; + virtual int64_t Timestamp() const = 0; + + protected: + DecryptedBlock() {} + virtual ~DecryptedBlock() {} +}; + +enum VideoPlane : uint32_t { + kYPlane = 0, + kUPlane = 1, + kVPlane = 2, + kMaxPlanes = 3, +}; +CHECK_TYPE(VideoPlane, 4, 4); + +class CDM_CLASS_API VideoFrame { + public: + virtual void SetFormat(VideoFormat format) = 0; + virtual VideoFormat Format() const = 0; + + virtual void SetSize(cdm::Size size) = 0; + virtual cdm::Size Size() const = 0; + + virtual void SetFrameBuffer(Buffer* frame_buffer) = 0; + virtual Buffer* FrameBuffer() = 0; + + virtual void SetPlaneOffset(VideoPlane plane, uint32_t offset) = 0; + virtual uint32_t PlaneOffset(VideoPlane plane) = 0; + + virtual void SetStride(VideoPlane plane, uint32_t stride) = 0; + virtual uint32_t Stride(VideoPlane plane) = 0; + + // Sets and gets the presentation timestamp which is in microseconds. + virtual void SetTimestamp(int64_t timestamp) = 0; + virtual int64_t Timestamp() const = 0; + + protected: + VideoFrame() {} + virtual ~VideoFrame() {} +}; + +// Represents a decoded video frame. The CDM should call the interface methods +// to set the frame attributes. See DecryptAndDecodeFrame(). +class CDM_CLASS_API VideoFrame_2 { + public: + virtual void SetFormat(VideoFormat format) = 0; + virtual void SetSize(cdm::Size size) = 0; + virtual void SetFrameBuffer(Buffer* frame_buffer) = 0; + virtual void SetPlaneOffset(VideoPlane plane, uint32_t offset) = 0; + virtual void SetStride(VideoPlane plane, uint32_t stride) = 0; + // Sets the presentation timestamp which is in microseconds. + virtual void SetTimestamp(int64_t timestamp) = 0; + virtual void SetColorSpace(ColorSpace color_space) = 0; + + protected: + VideoFrame_2() {} + virtual ~VideoFrame_2() {} +}; + +// Represents decrypted and decoded audio frames. AudioFrames can contain +// multiple audio output buffers, which are serialized into this format: +// +// |<------------------- serialized audio buffer ------------------->| +// | int64_t timestamp | int64_t length | length bytes of audio data | +// +// For example, with three audio output buffers, the AudioFrames will look +// like this: +// +// |<----------------- AudioFrames ------------------>| +// | audio buffer 0 | audio buffer 1 | audio buffer 2 | +class CDM_CLASS_API AudioFrames { + public: + virtual void SetFrameBuffer(Buffer* buffer) = 0; + virtual Buffer* FrameBuffer() = 0; + + // The CDM must call this method, providing a valid format, when providing + // frame buffers. Planar data should be stored end to end; e.g., + // |ch1 sample1||ch1 sample2|....|ch1 sample_last||ch2 sample1|... + virtual void SetFormat(AudioFormat format) = 0; + virtual AudioFormat Format() const = 0; + + protected: + AudioFrames() {} + virtual ~AudioFrames() {} +}; + +// FileIO interface provides a way for the CDM to store data in a file in +// persistent storage. This interface aims only at providing basic read/write +// capabilities and should not be used as a full fledged file IO API. +// Each CDM and origin (e.g. HTTPS, "foo.example.com", 443) combination has +// its own persistent storage. All instances of a given CDM associated with a +// given origin share the same persistent storage. +// Note to implementors of this interface: +// Per-origin storage and the ability for users to clear it are important. +// See http://www.w3.org/TR/encrypted-media/#privacy-storedinfo. +class CDM_CLASS_API FileIO { + public: + // Opens the file with |file_name| for read and write. + // FileIOClient::OnOpenComplete() will be called after the opening + // operation finishes. + // - When the file is opened by a CDM instance, it will be classified as "in + // use". In this case other CDM instances in the same domain may receive + // kInUse status when trying to open it. + // - |file_name| must only contain letters (A-Za-z), digits(0-9), or "._-". + // It must not start with an underscore ('_'), and must be at least 1 + // character and no more than 256 characters long. + virtual void Open(const char* file_name, uint32_t file_name_size) = 0; + + // Reads the contents of the file. FileIOClient::OnReadComplete() will be + // called with the read status. Read() should not be called if a previous + // Read() or Write() call is still pending; otherwise OnReadComplete() will + // be called with kInUse. + virtual void Read() = 0; + + // Writes |data_size| bytes of |data| into the file. + // FileIOClient::OnWriteComplete() will be called with the write status. + // All existing contents in the file will be overwritten. Calling Write() with + // NULL |data| will clear all contents in the file. Write() should not be + // called if a previous Write() or Read() call is still pending; otherwise + // OnWriteComplete() will be called with kInUse. + virtual void Write(const uint8_t* data, uint32_t data_size) = 0; + + // Closes the file if opened, destroys this FileIO object and releases any + // resources allocated. The CDM must call this method when it finished using + // this object. A FileIO object must not be used after Close() is called. + virtual void Close() = 0; + + protected: + FileIO() {} + virtual ~FileIO() {} +}; + +// Responses to FileIO calls. All responses will be called asynchronously. +// When kError is returned, the FileIO object could be in an error state. All +// following calls (other than Close()) could return kError. The CDM should +// still call Close() to destroy the FileIO object. +class CDM_CLASS_API FileIOClient { + public: + enum class Status : uint32_t { kSuccess = 0, kInUse, kError }; + + // Response to a FileIO::Open() call with the open |status|. + virtual void OnOpenComplete(Status status) = 0; + + // Response to a FileIO::Read() call to provide |data_size| bytes of |data| + // read from the file. + // - kSuccess indicates that all contents of the file has been successfully + // read. In this case, 0 |data_size| means that the file is empty. + // - kInUse indicates that there are other read/write operations pending. + // - kError indicates read failure, e.g. the storage is not open or cannot be + // fully read. + virtual void OnReadComplete(Status status, const uint8_t* data, + uint32_t data_size) = 0; + + // Response to a FileIO::Write() call. + // - kSuccess indicates that all the data has been written into the file + // successfully. + // - kInUse indicates that there are other read/write operations pending. + // - kError indicates write failure, e.g. the storage is not open or cannot be + // fully written. Upon write failure, the contents of the file should be + // regarded as corrupt and should not used. + virtual void OnWriteComplete(Status status) = 0; + + protected: + FileIOClient() {} + virtual ~FileIOClient() {} +}; + +class CDM_CLASS_API Host_10; +class CDM_CLASS_API Host_11; + +// ContentDecryptionModule interface that all CDMs need to implement. +// The interface is versioned for backward compatibility. +// Note: ContentDecryptionModule implementations must use the allocator +// provided in CreateCdmInstance() to allocate any Buffer that needs to +// be passed back to the caller. Implementations must call Buffer::Destroy() +// when a Buffer is created that will never be returned to the caller. +class CDM_CLASS_API ContentDecryptionModule_10 { + public: + static const int kVersion = 10; + static const bool kIsStable = true; + typedef Host_10 Host; + + // Initializes the CDM instance, providing information about permitted + // functionalities. The CDM must respond by calling Host::OnInitialized() + // with whether the initialization succeeded. No other calls will be made by + // the host before Host::OnInitialized() returns. + // If |allow_distinctive_identifier| is false, messages from the CDM, + // such as message events, must not contain a Distinctive Identifier, + // even in an encrypted form. + // If |allow_persistent_state| is false, the CDM must not attempt to + // persist state. Calls to CreateFileIO() will fail. + // If |use_hw_secure_codecs| is true, the CDM must ensure the decryption key + // and video buffers (compressed and uncompressed) are securely protected by + // hardware. + virtual void Initialize(bool allow_distinctive_identifier, + bool allow_persistent_state, + bool use_hw_secure_codecs) = 0; + + // Gets the key status if the CDM has a hypothetical key with the |policy|. + // The CDM must respond by calling either Host::OnResolveKeyStatusPromise() + // with the result key status or Host::OnRejectPromise() if an unexpected + // error happened or this method is not supported. + virtual void GetStatusForPolicy(uint32_t promise_id, + const Policy& policy) = 0; + + // SetServerCertificate(), CreateSessionAndGenerateRequest(), LoadSession(), + // UpdateSession(), CloseSession(), and RemoveSession() all accept a + // |promise_id|, which must be passed to the completion Host method + // (e.g. Host::OnResolveNewSessionPromise()). + + // Provides a server certificate to be used to encrypt messages to the + // license server. The CDM must respond by calling either + // Host::OnResolvePromise() or Host::OnRejectPromise(). + // If the CDM does not support server certificates, the promise should be + // rejected with kExceptionNotSupportedError. If |server_certificate_data| + // is empty, reject with kExceptionTypeError. Any other error should be + // rejected with kExceptionInvalidStateError or kExceptionQuotaExceededError. + // TODO(crbug.com/796417): Add support for the promise to return true or + // false, rather than using kExceptionNotSupportedError to mean false. + virtual void SetServerCertificate(uint32_t promise_id, + const uint8_t* server_certificate_data, + uint32_t server_certificate_data_size) = 0; + + // Creates a session given |session_type|, |init_data_type|, and |init_data|. + // The CDM must respond by calling either Host::OnResolveNewSessionPromise() + // or Host::OnRejectPromise(). + virtual void CreateSessionAndGenerateRequest(uint32_t promise_id, + SessionType session_type, + InitDataType init_data_type, + const uint8_t* init_data, + uint32_t init_data_size) = 0; + + // Loads the session of type |session_type| specified by |session_id|. + // The CDM must respond by calling either Host::OnResolveNewSessionPromise() + // or Host::OnRejectPromise(). If the session is not found, call + // Host::OnResolveNewSessionPromise() with session_id = NULL. + virtual void LoadSession(uint32_t promise_id, SessionType session_type, + const char* session_id, + uint32_t session_id_size) = 0; + + // Updates the session with |response|. The CDM must respond by calling + // either Host::OnResolvePromise() or Host::OnRejectPromise(). + virtual void UpdateSession(uint32_t promise_id, const char* session_id, + uint32_t session_id_size, const uint8_t* response, + uint32_t response_size) = 0; + + // Requests that the CDM close the session. The CDM must respond by calling + // either Host::OnResolvePromise() or Host::OnRejectPromise() when the request + // has been processed. This may be before the session is closed. Once the + // session is closed, Host::OnSessionClosed() must also be called. + virtual void CloseSession(uint32_t promise_id, const char* session_id, + uint32_t session_id_size) = 0; + + // Removes any stored session data associated with this session. Will only be + // called for persistent sessions. The CDM must respond by calling either + // Host::OnResolvePromise() or Host::OnRejectPromise() when the request has + // been processed. + virtual void RemoveSession(uint32_t promise_id, const char* session_id, + uint32_t session_id_size) = 0; + + // Performs scheduled operation with |context| when the timer fires. + virtual void TimerExpired(void* context) = 0; + + // Decrypts the |encrypted_buffer|. + // + // Returns kSuccess if decryption succeeded, in which case the callee + // should have filled the |decrypted_buffer| and passed the ownership of + // |data| in |decrypted_buffer| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kDecryptError if any other error happened. + // If the return value is not kSuccess, |decrypted_buffer| should be ignored + // by the caller. + virtual Status Decrypt(const InputBuffer_2& encrypted_buffer, + DecryptedBlock* decrypted_buffer) = 0; + + // Initializes the CDM audio decoder with |audio_decoder_config|. This + // function must be called before DecryptAndDecodeSamples() is called. + // + // Returns kSuccess if the |audio_decoder_config| is supported and the CDM + // audio decoder is successfully initialized. + // Returns kInitializationError if |audio_decoder_config| is not supported. + // The CDM may still be able to do Decrypt(). + // Returns kDeferredInitialization if the CDM is not ready to initialize the + // decoder at this time. Must call Host::OnDeferredInitializationDone() once + // initialization is complete. + virtual Status InitializeAudioDecoder( + const AudioDecoderConfig_2& audio_decoder_config) = 0; + + // Initializes the CDM video decoder with |video_decoder_config|. This + // function must be called before DecryptAndDecodeFrame() is called. + // + // Returns kSuccess if the |video_decoder_config| is supported and the CDM + // video decoder is successfully initialized. + // Returns kInitializationError if |video_decoder_config| is not supported. + // The CDM may still be able to do Decrypt(). + // Returns kDeferredInitialization if the CDM is not ready to initialize the + // decoder at this time. Must call Host::OnDeferredInitializationDone() once + // initialization is complete. + virtual Status InitializeVideoDecoder( + const VideoDecoderConfig_2& video_decoder_config) = 0; + + // De-initializes the CDM decoder and sets it to an uninitialized state. The + // caller can initialize the decoder again after this call to re-initialize + // it. This can be used to reconfigure the decoder if the configuration + // changes. + virtual void DeinitializeDecoder(StreamType decoder_type) = 0; + + // Resets the CDM decoder to an initialized clean state. All internal buffers + // MUST be flushed. + virtual void ResetDecoder(StreamType decoder_type) = 0; + + // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into a + // |video_frame|. Upon end-of-stream, the caller should call this function + // repeatedly with empty |encrypted_buffer| (|data| == NULL) until + // kNeedMoreData is returned. + // + // Returns kSuccess if decryption and decoding both succeeded, in which case + // the callee will have filled the |video_frame| and passed the ownership of + // |frame_buffer| in |video_frame| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kNeedMoreData if more data was needed by the decoder to generate + // a decoded frame (e.g. during initialization and end-of-stream). + // Returns kDecryptError if any decryption error happened. + // Returns kDecodeError if any decoding error happened. + // If the return value is not kSuccess, |video_frame| should be ignored by + // the caller. + virtual Status DecryptAndDecodeFrame(const InputBuffer_2& encrypted_buffer, + VideoFrame* video_frame) = 0; + + // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into + // |audio_frames|. Upon end-of-stream, the caller should call this function + // repeatedly with empty |encrypted_buffer| (|data| == NULL) until only empty + // |audio_frames| is produced. + // + // Returns kSuccess if decryption and decoding both succeeded, in which case + // the callee will have filled |audio_frames| and passed the ownership of + // |data| in |audio_frames| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kNeedMoreData if more data was needed by the decoder to generate + // audio samples (e.g. during initialization and end-of-stream). + // Returns kDecryptError if any decryption error happened. + // Returns kDecodeError if any decoding error happened. + // If the return value is not kSuccess, |audio_frames| should be ignored by + // the caller. + virtual Status DecryptAndDecodeSamples(const InputBuffer_2& encrypted_buffer, + AudioFrames* audio_frames) = 0; + + // Called by the host after a platform challenge was initiated via + // Host::SendPlatformChallenge(). + virtual void OnPlatformChallengeResponse( + const PlatformChallengeResponse& response) = 0; + + // Called by the host after a call to Host::QueryOutputProtectionStatus(). The + // |link_mask| is a bit mask of OutputLinkTypes and |output_protection_mask| + // is a bit mask of OutputProtectionMethods. If |result| is kQueryFailed, + // then |link_mask| and |output_protection_mask| are undefined and should + // be ignored. + virtual void OnQueryOutputProtectionStatus( + QueryResult result, uint32_t link_mask, + uint32_t output_protection_mask) = 0; + + // Called by the host after a call to Host::RequestStorageId(). If the + // version of the storage ID requested is available, |storage_id| and + // |storage_id_size| are set appropriately. |version| will be the same as + // what was requested, unless 0 (latest) was requested, in which case + // |version| will be the actual version number for the |storage_id| returned. + // If the requested version is not available, null/zero will be provided as + // |storage_id| and |storage_id_size|, respectively, and |version| should be + // ignored. + virtual void OnStorageId(uint32_t version, const uint8_t* storage_id, + uint32_t storage_id_size) = 0; + + // Destroys the object in the same context as it was created. + virtual void Destroy() = 0; + + protected: + ContentDecryptionModule_10() {} + virtual ~ContentDecryptionModule_10() {} +}; + +// ----- Note: CDM interface(s) below still in development and not stable! ----- + +// ContentDecryptionModule interface that all CDMs need to implement. +// The interface is versioned for backward compatibility. +// Note: ContentDecryptionModule implementations must use the allocator +// provided in CreateCdmInstance() to allocate any Buffer that needs to +// be passed back to the caller. Implementations must call Buffer::Destroy() +// when a Buffer is created that will never be returned to the caller. +class CDM_CLASS_API ContentDecryptionModule_11 { + public: + static const int kVersion = 11; + static const bool kIsStable = false; + typedef Host_11 Host; + + // Initializes the CDM instance, providing information about permitted + // functionalities. The CDM must respond by calling Host::OnInitialized() + // with whether the initialization succeeded. No other calls will be made by + // the host before Host::OnInitialized() returns. + // If |allow_distinctive_identifier| is false, messages from the CDM, + // such as message events, must not contain a Distinctive Identifier, + // even in an encrypted form. + // If |allow_persistent_state| is false, the CDM must not attempt to + // persist state. Calls to CreateFileIO() will fail. + // If |use_hw_secure_codecs| is true, the CDM must ensure the decryption key + // and video buffers (compressed and uncompressed) are securely protected by + // hardware. + virtual void Initialize(bool allow_distinctive_identifier, + bool allow_persistent_state, + bool use_hw_secure_codecs) = 0; + + // Gets the key status if the CDM has a hypothetical key with the |policy|. + // The CDM must respond by calling either Host::OnResolveKeyStatusPromise() + // with the result key status or Host::OnRejectPromise() if an unexpected + // error happened or this method is not supported. + virtual void GetStatusForPolicy(uint32_t promise_id, + const Policy& policy) = 0; + + // SetServerCertificate(), CreateSessionAndGenerateRequest(), LoadSession(), + // UpdateSession(), CloseSession(), and RemoveSession() all accept a + // |promise_id|, which must be passed to the completion Host method + // (e.g. Host::OnResolveNewSessionPromise()). + + // Provides a server certificate to be used to encrypt messages to the + // license server. The CDM must respond by calling either + // Host::OnResolvePromise() or Host::OnRejectPromise(). + // If the CDM does not support server certificates, the promise should be + // rejected with kExceptionNotSupportedError. If |server_certificate_data| + // is empty, reject with kExceptionTypeError. Any other error should be + // rejected with kExceptionInvalidStateError or kExceptionQuotaExceededError. + // TODO(crbug.com/796417): Add support for the promise to return true or + // false, rather than using kExceptionNotSupportedError to mean false. + virtual void SetServerCertificate(uint32_t promise_id, + const uint8_t* server_certificate_data, + uint32_t server_certificate_data_size) = 0; + + // Creates a session given |session_type|, |init_data_type|, and |init_data|. + // The CDM must respond by calling either Host::OnResolveNewSessionPromise() + // or Host::OnRejectPromise(). + virtual void CreateSessionAndGenerateRequest(uint32_t promise_id, + SessionType session_type, + InitDataType init_data_type, + const uint8_t* init_data, + uint32_t init_data_size) = 0; + + // Loads the session of type |session_type| specified by |session_id|. + // The CDM must respond by calling either Host::OnResolveNewSessionPromise() + // or Host::OnRejectPromise(). If the session is not found, call + // Host::OnResolveNewSessionPromise() with session_id = NULL. + virtual void LoadSession(uint32_t promise_id, SessionType session_type, + const char* session_id, + uint32_t session_id_size) = 0; + + // Updates the session with |response|. The CDM must respond by calling + // either Host::OnResolvePromise() or Host::OnRejectPromise(). + virtual void UpdateSession(uint32_t promise_id, const char* session_id, + uint32_t session_id_size, const uint8_t* response, + uint32_t response_size) = 0; + + // Requests that the CDM close the session. The CDM must respond by calling + // either Host::OnResolvePromise() or Host::OnRejectPromise() when the request + // has been processed. This may be before the session is closed. Once the + // session is closed, Host::OnSessionClosed() must also be called. + virtual void CloseSession(uint32_t promise_id, const char* session_id, + uint32_t session_id_size) = 0; + + // Removes any stored session data associated with this session. Removes all + // license(s) and key(s) associated with the session, whether they are in + // memory, persistent store, or both. For persistent session types, other + // session data (e.g. record of license destruction) will be cleared as + // defined for each session type once a release message acknowledgment is + // processed by UpdateSession(). The CDM must respond by calling either + // Host::OnResolvePromise() or Host::OnRejectPromise() when the request has + // been processed. + virtual void RemoveSession(uint32_t promise_id, const char* session_id, + uint32_t session_id_size) = 0; + + // Performs scheduled operation with |context| when the timer fires. + virtual void TimerExpired(void* context) = 0; + + // Decrypts the |encrypted_buffer|. + // + // Returns kSuccess if decryption succeeded, in which case the callee + // should have filled the |decrypted_buffer| and passed the ownership of + // |data| in |decrypted_buffer| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kDecryptError if any other error happened. + // If the return value is not kSuccess, |decrypted_buffer| should be ignored + // by the caller. + virtual Status Decrypt(const InputBuffer_2& encrypted_buffer, + DecryptedBlock* decrypted_buffer) = 0; + + // Initializes the CDM audio decoder with |audio_decoder_config|. This + // function must be called before DecryptAndDecodeSamples() is called. + // + // Returns kSuccess if the |audio_decoder_config| is supported and the CDM + // audio decoder is successfully initialized. + // Returns kInitializationError if |audio_decoder_config| is not supported. + // The CDM may still be able to do Decrypt(). + // Returns kDeferredInitialization if the CDM is not ready to initialize the + // decoder at this time. Must call Host::OnDeferredInitializationDone() once + // initialization is complete. + virtual Status InitializeAudioDecoder( + const AudioDecoderConfig_2& audio_decoder_config) = 0; + + // Initializes the CDM video decoder with |video_decoder_config|. This + // function must be called before DecryptAndDecodeFrame() is called. + // + // Returns kSuccess if the |video_decoder_config| is supported and the CDM + // video decoder is successfully initialized. + // Returns kInitializationError if |video_decoder_config| is not supported. + // The CDM may still be able to do Decrypt(). + // Returns kDeferredInitialization if the CDM is not ready to initialize the + // decoder at this time. Must call Host::OnDeferredInitializationDone() once + // initialization is complete. + virtual Status InitializeVideoDecoder( + const VideoDecoderConfig_3& video_decoder_config) = 0; + + // De-initializes the CDM decoder and sets it to an uninitialized state. The + // caller can initialize the decoder again after this call to re-initialize + // it. This can be used to reconfigure the decoder if the configuration + // changes. + virtual void DeinitializeDecoder(StreamType decoder_type) = 0; + + // Resets the CDM decoder to an initialized clean state. All internal buffers + // MUST be flushed. + virtual void ResetDecoder(StreamType decoder_type) = 0; + + // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into a + // |video_frame|. Upon end-of-stream, the caller should call this function + // repeatedly with empty |encrypted_buffer| (|data| == NULL) until + // kNeedMoreData is returned. + // + // Returns kSuccess if decryption and decoding both succeeded, in which case + // the callee will have filled the |video_frame| and passed the ownership of + // |frame_buffer| in |video_frame| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kNeedMoreData if more data was needed by the decoder to generate + // a decoded frame (e.g. during initialization and end-of-stream). + // Returns kDecryptError if any decryption error happened. + // Returns kDecodeError if any decoding error happened. + // If the return value is not kSuccess, |video_frame| should be ignored by + // the caller. + virtual Status DecryptAndDecodeFrame(const InputBuffer_2& encrypted_buffer, + VideoFrame_2* video_frame) = 0; + + // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into + // |audio_frames|. Upon end-of-stream, the caller should call this function + // repeatedly with empty |encrypted_buffer| (|data| == NULL) until only empty + // |audio_frames| is produced. + // + // Returns kSuccess if decryption and decoding both succeeded, in which case + // the callee will have filled |audio_frames| and passed the ownership of + // |data| in |audio_frames| to the caller. + // Returns kNoKey if the CDM did not have the necessary decryption key + // to decrypt. + // Returns kNeedMoreData if more data was needed by the decoder to generate + // audio samples (e.g. during initialization and end-of-stream). + // Returns kDecryptError if any decryption error happened. + // Returns kDecodeError if any decoding error happened. + // If the return value is not kSuccess, |audio_frames| should be ignored by + // the caller. + virtual Status DecryptAndDecodeSamples(const InputBuffer_2& encrypted_buffer, + AudioFrames* audio_frames) = 0; + + // Called by the host after a platform challenge was initiated via + // Host::SendPlatformChallenge(). + virtual void OnPlatformChallengeResponse( + const PlatformChallengeResponse& response) = 0; + + // Called by the host after a call to Host::QueryOutputProtectionStatus(). The + // |link_mask| is a bit mask of OutputLinkTypes and |output_protection_mask| + // is a bit mask of OutputProtectionMethods. If |result| is kQueryFailed, + // then |link_mask| and |output_protection_mask| are undefined and should + // be ignored. + virtual void OnQueryOutputProtectionStatus( + QueryResult result, uint32_t link_mask, + uint32_t output_protection_mask) = 0; + + // Called by the host after a call to Host::RequestStorageId(). If the + // version of the storage ID requested is available, |storage_id| and + // |storage_id_size| are set appropriately. |version| will be the same as + // what was requested, unless 0 (latest) was requested, in which case + // |version| will be the actual version number for the |storage_id| returned. + // If the requested version is not available, null/zero will be provided as + // |storage_id| and |storage_id_size|, respectively, and |version| should be + // ignored. + virtual void OnStorageId(uint32_t version, const uint8_t* storage_id, + uint32_t storage_id_size) = 0; + + // Destroys the object in the same context as it was created. + virtual void Destroy() = 0; + + protected: + ContentDecryptionModule_11() {} + virtual ~ContentDecryptionModule_11() {} +}; + +class CDM_CLASS_API Host_10 { + public: + static const int kVersion = 10; + + // Returns a Buffer* containing non-zero members upon success, or NULL on + // failure. The caller owns the Buffer* after this call. The buffer is not + // guaranteed to be zero initialized. The capacity of the allocated Buffer + // is guaranteed to be not less than |capacity|. + virtual Buffer* Allocate(uint32_t capacity) = 0; + + // Requests the host to call ContentDecryptionModule::TimerFired() |delay_ms| + // from now with |context|. + virtual void SetTimer(int64_t delay_ms, void* context) = 0; + + // Returns the current wall time. + virtual Time GetCurrentWallTime() = 0; + + // Called by the CDM with the result after the CDM instance was initialized. + virtual void OnInitialized(bool success) = 0; + + // Called by the CDM when a key status is available in response to + // GetStatusForPolicy(). + virtual void OnResolveKeyStatusPromise(uint32_t promise_id, + KeyStatus key_status) = 0; + + // Called by the CDM when a session is created or loaded and the value for the + // MediaKeySession's sessionId attribute is available (|session_id|). + // This must be called before OnSessionMessage() or + // OnSessionKeysChange() is called for the same session. |session_id_size| + // should not include null termination. + // When called in response to LoadSession(), the |session_id| must be the + // same as the |session_id| passed in LoadSession(), or NULL if the + // session could not be loaded. + virtual void OnResolveNewSessionPromise(uint32_t promise_id, + const char* session_id, + uint32_t session_id_size) = 0; + + // Called by the CDM when a session is updated or released. + virtual void OnResolvePromise(uint32_t promise_id) = 0; + + // Called by the CDM when an error occurs as a result of one of the + // ContentDecryptionModule calls that accept a |promise_id|. + // |exception| must be specified. |error_message| and |system_code| + // are optional. |error_message_size| should not include null termination. + virtual void OnRejectPromise(uint32_t promise_id, Exception exception, + uint32_t system_code, const char* error_message, + uint32_t error_message_size) = 0; + + // Called by the CDM when it has a message for session |session_id|. + // Size parameters should not include null termination. + virtual void OnSessionMessage(const char* session_id, + uint32_t session_id_size, + MessageType message_type, const char* message, + uint32_t message_size) = 0; + + // Called by the CDM when there has been a change in keys or their status for + // session |session_id|. |has_additional_usable_key| should be set if a + // key is newly usable (e.g. new key available, previously expired key has + // been renewed, etc.) and the browser should attempt to resume playback. + // |keys_info| is the list of key IDs for this session along with their + // current status. |keys_info_count| is the number of entries in |keys_info|. + // Size parameter for |session_id| should not include null termination. + virtual void OnSessionKeysChange(const char* session_id, + uint32_t session_id_size, + bool has_additional_usable_key, + const KeyInformation* keys_info, + uint32_t keys_info_count) = 0; + + // Called by the CDM when there has been a change in the expiration time for + // session |session_id|. This can happen as the result of an Update() call + // or some other event. If this happens as a result of a call to Update(), + // it must be called before resolving the Update() promise. |new_expiry_time| + // represents the time after which the key(s) in the session will no longer + // be usable for decryption. It can be 0 if no such time exists or if the + // license explicitly never expires. Size parameter should not include null + // termination. + virtual void OnExpirationChange(const char* session_id, + uint32_t session_id_size, + Time new_expiry_time) = 0; + + // Called by the CDM when session |session_id| is closed. Size + // parameter should not include null termination. + virtual void OnSessionClosed(const char* session_id, + uint32_t session_id_size) = 0; + + // The following are optional methods that may not be implemented on all + // platforms. + + // Sends a platform challenge for the given |service_id|. |challenge| is at + // most 256 bits of data to be signed. Once the challenge has been completed, + // the host will call ContentDecryptionModule::OnPlatformChallengeResponse() + // with the signed challenge response and platform certificate. Size + // parameters should not include null termination. + virtual void SendPlatformChallenge(const char* service_id, + uint32_t service_id_size, + const char* challenge, + uint32_t challenge_size) = 0; + + // Attempts to enable output protection (e.g. HDCP) on the display link. The + // |desired_protection_mask| is a bit mask of OutputProtectionMethods. No + // status callback is issued, the CDM must call QueryOutputProtectionStatus() + // periodically to ensure the desired protections are applied. + virtual void EnableOutputProtection(uint32_t desired_protection_mask) = 0; + + // Requests the current output protection status. Once the host has the status + // it will call ContentDecryptionModule::OnQueryOutputProtectionStatus(). + virtual void QueryOutputProtectionStatus() = 0; + + // Must be called by the CDM if it returned kDeferredInitialization during + // InitializeAudioDecoder() or InitializeVideoDecoder(). + virtual void OnDeferredInitializationDone(StreamType stream_type, + Status decoder_status) = 0; + + // Creates a FileIO object from the host to do file IO operation. Returns NULL + // if a FileIO object cannot be obtained. Once a valid FileIO object is + // returned, |client| must be valid until FileIO::Close() is called. The + // CDM can call this method multiple times to operate on different files. + virtual FileIO* CreateFileIO(FileIOClient* client) = 0; + + // Requests a specific version of the storage ID. A storage ID is a stable, + // device specific ID used by the CDM to securely store persistent data. The + // ID will be returned by the host via ContentDecryptionModule::OnStorageId(). + // If |version| is 0, the latest version will be returned. All |version|s + // that are greater than or equal to 0x80000000 are reserved for the CDM and + // should not be supported or returned by the host. The CDM must not expose + // the ID outside the client device, even in encrypted form. + virtual void RequestStorageId(uint32_t version) = 0; + + protected: + Host_10() {} + virtual ~Host_10() {} +}; + +class CDM_CLASS_API Host_11 { + public: + static const int kVersion = 11; + + // Returns a Buffer* containing non-zero members upon success, or NULL on + // failure. The caller owns the Buffer* after this call. The buffer is not + // guaranteed to be zero initialized. The capacity of the allocated Buffer + // is guaranteed to be not less than |capacity|. + virtual Buffer* Allocate(uint32_t capacity) = 0; + + // Requests the host to call ContentDecryptionModule::TimerFired() |delay_ms| + // from now with |context|. + virtual void SetTimer(int64_t delay_ms, void* context) = 0; + + // Returns the current wall time. + virtual Time GetCurrentWallTime() = 0; + + // Called by the CDM with the result after the CDM instance was initialized. + virtual void OnInitialized(bool success) = 0; + + // Called by the CDM when a key status is available in response to + // GetStatusForPolicy(). + virtual void OnResolveKeyStatusPromise(uint32_t promise_id, + KeyStatus key_status) = 0; + + // Called by the CDM when a session is created or loaded and the value for the + // MediaKeySession's sessionId attribute is available (|session_id|). + // This must be called before OnSessionMessage() or + // OnSessionKeysChange() is called for the same session. |session_id_size| + // should not include null termination. + // When called in response to LoadSession(), the |session_id| must be the + // same as the |session_id| passed in LoadSession(), or NULL if the + // session could not be loaded. + virtual void OnResolveNewSessionPromise(uint32_t promise_id, + const char* session_id, + uint32_t session_id_size) = 0; + + // Called by the CDM when a session is updated or released. + virtual void OnResolvePromise(uint32_t promise_id) = 0; + + // Called by the CDM when an error occurs as a result of one of the + // ContentDecryptionModule calls that accept a |promise_id|. + // |exception| must be specified. |error_message| and |system_code| + // are optional. |error_message_size| should not include null termination. + virtual void OnRejectPromise(uint32_t promise_id, Exception exception, + uint32_t system_code, const char* error_message, + uint32_t error_message_size) = 0; + + // Called by the CDM when it has a message for session |session_id|. + // Size parameters should not include null termination. + virtual void OnSessionMessage(const char* session_id, + uint32_t session_id_size, + MessageType message_type, const char* message, + uint32_t message_size) = 0; + + // Called by the CDM when there has been a change in keys or their status for + // session |session_id|. |has_additional_usable_key| should be set if a + // key is newly usable (e.g. new key available, previously expired key has + // been renewed, etc.) and the browser should attempt to resume playback. + // |keys_info| is the list of key IDs for this session along with their + // current status. |keys_info_count| is the number of entries in |keys_info|. + // Size parameter for |session_id| should not include null termination. + virtual void OnSessionKeysChange(const char* session_id, + uint32_t session_id_size, + bool has_additional_usable_key, + const KeyInformation* keys_info, + uint32_t keys_info_count) = 0; + + // Called by the CDM when there has been a change in the expiration time for + // session |session_id|. This can happen as the result of an Update() call + // or some other event. If this happens as a result of a call to Update(), + // it must be called before resolving the Update() promise. |new_expiry_time| + // represents the time after which the key(s) in the session will no longer + // be usable for decryption. It can be 0 if no such time exists or if the + // license explicitly never expires. Size parameter should not include null + // termination. + virtual void OnExpirationChange(const char* session_id, + uint32_t session_id_size, + Time new_expiry_time) = 0; + + // Called by the CDM when session |session_id| is closed. Size + // parameter should not include null termination. + virtual void OnSessionClosed(const char* session_id, + uint32_t session_id_size) = 0; + + // The following are optional methods that may not be implemented on all + // platforms. + + // Sends a platform challenge for the given |service_id|. |challenge| is at + // most 256 bits of data to be signed. Once the challenge has been completed, + // the host will call ContentDecryptionModule::OnPlatformChallengeResponse() + // with the signed challenge response and platform certificate. Size + // parameters should not include null termination. + virtual void SendPlatformChallenge(const char* service_id, + uint32_t service_id_size, + const char* challenge, + uint32_t challenge_size) = 0; + + // Attempts to enable output protection (e.g. HDCP) on the display link. The + // |desired_protection_mask| is a bit mask of OutputProtectionMethods. No + // status callback is issued, the CDM must call QueryOutputProtectionStatus() + // periodically to ensure the desired protections are applied. + virtual void EnableOutputProtection(uint32_t desired_protection_mask) = 0; + + // Requests the current output protection status. Once the host has the status + // it will call ContentDecryptionModule::OnQueryOutputProtectionStatus(). + virtual void QueryOutputProtectionStatus() = 0; + + // Must be called by the CDM if it returned kDeferredInitialization during + // InitializeAudioDecoder() or InitializeVideoDecoder(). + virtual void OnDeferredInitializationDone(StreamType stream_type, + Status decoder_status) = 0; + + // Creates a FileIO object from the host to do file IO operation. Returns NULL + // if a FileIO object cannot be obtained. Once a valid FileIO object is + // returned, |client| must be valid until FileIO::Close() is called. The + // CDM can call this method multiple times to operate on different files. + virtual FileIO* CreateFileIO(FileIOClient* client) = 0; + + // Requests a CdmProxy that proxies part of CDM functionalities to a different + // entity, e.g. a hardware CDM module. A CDM instance can have at most one + // CdmProxy throughout its lifetime, which must be requested and initialized + // during CDM instance initialization time, i.e. in or after CDM::Initialize() + // and before OnInitialized() is called, to ensure proper connection of the + // CdmProxy and the media player (e.g. hardware decoder). The CdmProxy is + // owned by the host and is guaranteed to be valid throughout the CDM + // instance's lifetime. The CDM must ensure that the |client| remain valid + // before the CDM instance is destroyed. Returns null if CdmProxy is not + // supported, called before CDM::Initialize(), RequestCdmProxy() is called + // more than once, or called after the CDM instance has been initialized. + virtual CdmProxy* RequestCdmProxy(CdmProxyClient* client) = 0; + + // Requests a specific version of the storage ID. A storage ID is a stable, + // device specific ID used by the CDM to securely store persistent data. The + // ID will be returned by the host via ContentDecryptionModule::OnStorageId(). + // If |version| is 0, the latest version will be returned. All |version|s + // that are greater than or equal to 0x80000000 are reserved for the CDM and + // should not be supported or returned by the host. The CDM must not expose + // the ID outside the client device, even in encrypted form. + virtual void RequestStorageId(uint32_t version) = 0; + + protected: + Host_11() {} + virtual ~Host_11() {} +}; + +} // namespace cdm + +#endif // CDM_CONTENT_DECRYPTION_MODULE_H_ diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module_export.h b/dom/media/gmp/widevine-adapter/content_decryption_module_export.h new file mode 100644 index 0000000000..932708deba --- /dev/null +++ b/dom/media/gmp/widevine-adapter/content_decryption_module_export.h @@ -0,0 +1,38 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CDM_CONTENT_DECRYPTION_MODULE_EXPORT_H_ +#define CDM_CONTENT_DECRYPTION_MODULE_EXPORT_H_ + +// Define CDM_API so that functionality implemented by the CDM module +// can be exported to consumers. +#if defined(_WIN32) + +#if defined(CDM_IMPLEMENTATION) +#define CDM_API __declspec(dllexport) +#else +#define CDM_API __declspec(dllimport) +#endif // defined(CDM_IMPLEMENTATION) + +#else // defined(_WIN32) +#define CDM_API __attribute__((visibility("default"))) +#endif // defined(_WIN32) + +// Define CDM_CLASS_API to export class types. We have to add visibility +// attributes to make sure virtual tables in CDM consumer and CDM implementation +// are the same. Generally, it was always a good idea, as there're no guarantees +// about that for the internal symbols, but it has only become a practical issue +// after introduction of LTO devirtualization. See more details on +// https://crbug.com/609564#c35 +#if defined(_WIN32) +#if defined(__clang__) +#define CDM_CLASS_API [[clang::lto_visibility_public]] +#else +#define CDM_CLASS_API +#endif +#else // defined(_WIN32) +#define CDM_CLASS_API __attribute__((visibility("default"))) +#endif // defined(_WIN32) + +#endif // CDM_CONTENT_DECRYPTION_MODULE_EXPORT_H_ diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module_ext.h b/dom/media/gmp/widevine-adapter/content_decryption_module_ext.h new file mode 100644 index 0000000000..5df8344e60 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/content_decryption_module_ext.h @@ -0,0 +1,64 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CDM_CONTENT_DECRYPTION_MODULE_EXT_H_ +#define CDM_CONTENT_DECRYPTION_MODULE_EXT_H_ + +#if defined(_WIN32) +#include <windows.h> +#endif + +#include "content_decryption_module_export.h" + +#if defined(_MSC_VER) +typedef unsigned int uint32_t; +#else +#include <stdint.h> +#endif + +namespace cdm { + +#if defined(_WIN32) +typedef wchar_t FilePathCharType; +typedef HANDLE PlatformFile; +const PlatformFile kInvalidPlatformFile = INVALID_HANDLE_VALUE; +#else +typedef char FilePathCharType; +typedef int PlatformFile; +const PlatformFile kInvalidPlatformFile = -1; +#endif // defined(_WIN32) + +struct HostFile { + HostFile(const FilePathCharType* file_path, + PlatformFile file, + PlatformFile sig_file) + : file_path(file_path), file(file), sig_file(sig_file) {} + + // File that is part of the host of the CDM. + const FilePathCharType* file_path = nullptr; + PlatformFile file = kInvalidPlatformFile; + + // Signature file for |file|. + PlatformFile sig_file = kInvalidPlatformFile; +}; + +} // namespace cdm + +extern "C" { + +// Functions in this file are dynamically retrieved by their versioned function +// names. Increment the version number for any backward incompatible API +// changes. + +// Verifies CDM host. All files in |host_files| are opened in read-only mode. +// +// Returns false and closes all files if there is an immediate failure. +// Otherwise returns true as soon as possible and processes the files +// asynchronously. All files MUST be closed by the CDM after this one-time +// processing is finished. +CDM_API bool VerifyCdmHost_0(const cdm::HostFile* host_files, + uint32_t num_files); +} + +#endif // CDM_CONTENT_DECRYPTION_MODULE_EXT_H_ diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module_proxy.h b/dom/media/gmp/widevine-adapter/content_decryption_module_proxy.h new file mode 100644 index 0000000000..d3edff8b37 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/content_decryption_module_proxy.h @@ -0,0 +1,121 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CDM_CONTENT_DECRYPTION_MODULE_PROXY_H_ +#define CDM_CONTENT_DECRYPTION_MODULE_PROXY_H_ + +#include "content_decryption_module_export.h" + +#if defined(_MSC_VER) +typedef unsigned char uint8_t; +typedef unsigned int uint32_t; +typedef unsigned __int64 uint64_t; +#else +# include <stdint.h> +#endif + +namespace cdm { + +class CDM_CLASS_API CdmProxyClient; + +// A proxy class for the CDM. +// In general, the interpretation of the CdmProxy and CdmProxyClient method +// parameters are protocol dependent. For enum parameters, values outside the +// enum range may not work. +class CDM_CLASS_API CdmProxy { + public: + enum Function : uint32_t { + // For Intel Negotiate Crypto SessionKey Exchange (CSME) path to call + // ID3D11VideoContext::NegotiateCryptoSessionKeyExchange. + kIntelNegotiateCryptoSessionKeyExchange = 1, + // There will be more values in the future e.g. for D3D11 RSA method. + }; + + enum KeyType : uint32_t { + kDecryptOnly = 0, + kDecryptAndDecode = 1, + }; + + // Initializes the proxy. The results will be returned in + // CdmProxyClient::OnInitialized(). + virtual void Initialize() = 0; + + // Processes and updates the state of the proxy. + // |output_data_size| is required by some protocol to set up the output data. + // The operation may fail if the |output_data_size| is wrong. The results will + // be returned in CdmProxyClient::OnProcessed(). + virtual void Process(Function function, uint32_t crypto_session_id, + const uint8_t* input_data, uint32_t input_data_size, + uint32_t output_data_size) = 0; + + // Creates a crypto session for handling media. + // If extra data has to be passed to further setup the media crypto session, + // pass the data as |input_data|. The results will be returned in + // CdmProxyClient::OnMediaCryptoSessionCreated(). + virtual void CreateMediaCryptoSession(const uint8_t* input_data, + uint32_t input_data_size) = 0; + + // Sets a key for the session identified by |crypto_session_id|. + virtual void SetKey(uint32_t crypto_session_id, const uint8_t* key_id, + uint32_t key_id_size, KeyType key_type, + const uint8_t* key_blob, uint32_t key_blob_size) = 0; + + // Removes a key for the session identified by |crypto_session_id|. + virtual void RemoveKey(uint32_t crypto_session_id, const uint8_t* key_id, + uint32_t key_id_size) = 0; + + protected: + CdmProxy() {} + virtual ~CdmProxy() {} +}; + +// Responses to CdmProxy calls. All responses will be called asynchronously. +class CDM_CLASS_API CdmProxyClient { + public: + enum Status : uint32_t { + kOk, + kFail, + }; + + enum Protocol : uint32_t { + kNone = 0, // No protocol supported. Can be used in failure cases. + kIntel, // Method using Intel CSME. + // There will be more values in the future e.g. kD3D11RsaHardware, + // kD3D11RsaSoftware to use the D3D11 RSA method. + }; + + // Callback for Initialize(). If the proxy created a crypto session, then the + // ID for the crypto session is |crypto_session_id|. + virtual void OnInitialized(Status status, Protocol protocol, + uint32_t crypto_session_id) = 0; + + // Callback for Process(). |output_data| is the output of processing. + virtual void OnProcessed(Status status, const uint8_t* output_data, + uint32_t output_data_size) = 0; + + // Callback for CreateMediaCryptoSession(). On success: + // - |crypto_session_id| is the ID for the created crypto session. + // - |output_data| is extra value, if any. + // Otherwise, |crypto_session_id| and |output_data| should be ignored. + virtual void OnMediaCryptoSessionCreated(Status status, + uint32_t crypto_session_id, + uint64_t output_data) = 0; + + // Callback for SetKey(). + virtual void OnKeySet(Status status) = 0; + + // Callback for RemoveKey(). + virtual void OnKeyRemoved(Status status) = 0; + + // Called when there is a hardware reset and all the hardware context is lost. + virtual void NotifyHardwareReset() = 0; + + protected: + CdmProxyClient() {} + virtual ~CdmProxyClient() {} +}; + +} // namespace cdm + +#endif // CDM_CONTENT_DECRYPTION_MODULE_PROXY_H_ diff --git a/dom/media/gmp/widevine-adapter/moz.build b/dom/media/gmp/widevine-adapter/moz.build new file mode 100644 index 0000000000..b1b7582407 --- /dev/null +++ b/dom/media/gmp/widevine-adapter/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +SOURCES += [ + "WidevineFileIO.cpp", + "WidevineUtils.cpp", + "WidevineVideoFrame.cpp", +] + +EXPORTS += ["WidevineFileIO.h", "WidevineUtils.h", "WidevineVideoFrame.h"] + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/dom/media/gmp", +] + +include("/ipc/chromium/chromium-config.mozbuild") |