chromium/chromecast/starboard/media/cdm/starboard_decryptor_cast.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromecast/starboard/media/cdm/starboard_decryptor_cast.h"

#include <cast_starboard_api_adapter.h>

#include <cstdint>
#include <cstring>
#include <optional>

#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/hash/hash.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "chromecast/media/base/decrypt_context_impl.h"
#include "chromecast/starboard/media/cdm/starboard_drm_key_tracker.h"
#include "chromecast/starboard/media/media/starboard_api_wrapper.h"
#include "google_apis/google_api_keys.h"
#include "media/base/cdm_callback_promise.h"
#include "media/base/eme_constants.h"

namespace chromecast {
namespace media {

namespace {

// The API key must be added.
constexpr char kProvisionServerUrlMinusKey[] =
    "https://www.googleapis.com/certificateprovisioning/v1/devicecertificates/"
    "create?key=";

// A decrypt context that fakes decryption, so that the real decryption can be
// done in Starboard.
class DummyDecryptContext : public DecryptContextImpl {
 public:
  DummyDecryptContext()
      : DecryptContextImpl(CastKeySystem::KEY_SYSTEM_WIDEVINE) {}

  ~DummyDecryptContext() override = default;

  // DecryptContextImpl implementation:
  void DecryptAsync(CastDecoderBuffer* buffer,
                    uint8_t* output_or_handle,
                    size_t data_offset,
                    bool clear_output,
                    DecryptCB decrypt_cb) override {
    LOG(FATAL) << "Decryption in cast is not allowed for the starboard "
                  "pipeline (decryption must be done in starboard itself, via "
                  "SbPlayer and SbDrmSystem).";
  }

  DecryptContextImpl::OutputType GetOutputType() const override {
    // This should force decryption to be done in the MediaPipelineBackend (see
    // CdmDecryptor::Decrypt for the relevant logic that reads this value).
    return OutputType::kSecure;
  }
};

std::string DrmKeyStatusToString(StarboardDrmKeyStatus status) {
  switch (status) {
    case kStarboardDrmKeyStatusUsable:
      return "kStarboardDrmKeyStatusUsable";
    case kStarboardDrmKeyStatusExpired:
      return "kStarboardDrmKeyStatusExpired";
    case kStarboardDrmKeyStatusReleased:
      return "kStarboardDrmKeyStatusReleased";
    case kStarboardDrmKeyStatusRestricted:
      return "kStarboardDrmKeyStatusRestricted";
    case kStarboardDrmKeyStatusDownscaled:
      return "kStarboardDrmKeyStatusDownscaled";
    case kStarboardDrmKeyStatusPending:
      return "kStarboardDrmKeyStatusPending";
    case kStarboardDrmKeyStatusError:
      return "kStarboardDrmKeyStatusError";
  }
}

// Converts a starboard DRM status to a CdmPromise exception. This must not be
// called for a success status. Defaults to NotSupportedError.
::media::CdmPromise::Exception StarboardDrmErrorStatusToCdmException(
    StarboardDrmStatus status) {
  switch (status) {
    case kStarboardDrmStatusSuccess:
      LOG(FATAL) << "Success status cannot be converted to a CDM Exception";
    case kStarboardDrmStatusTypeError:
      return ::media::CdmPromise::Exception::TYPE_ERROR;
    case kStarboardDrmStatusNotSupportedError:
      return ::media::CdmPromise::Exception::NOT_SUPPORTED_ERROR;
    case kStarboardDrmStatusInvalidStateError:
      return ::media::CdmPromise::Exception::INVALID_STATE_ERROR;
    case kStarboardDrmStatusQuotaExceededError:
      return ::media::CdmPromise::Exception::QUOTA_EXCEEDED_ERROR;
    case kStarboardDrmStatusUnknownError:
    default:
      return ::media::CdmPromise::Exception::NOT_SUPPORTED_ERROR;
  }
}

::media::CdmKeyInformation::KeyStatus ToMediaKeyStatus(
    StarboardDrmKeyStatus status) {
  switch (status) {
    case kStarboardDrmKeyStatusUsable:
      return ::media::CdmKeyInformation::KeyStatus::USABLE;
    case kStarboardDrmKeyStatusExpired:
      return ::media::CdmKeyInformation::KeyStatus::EXPIRED;
    case kStarboardDrmKeyStatusReleased:
      return ::media::CdmKeyInformation::KeyStatus::RELEASED;
    case kStarboardDrmKeyStatusRestricted:
      return ::media::CdmKeyInformation::KeyStatus::OUTPUT_RESTRICTED;
    case kStarboardDrmKeyStatusDownscaled:
      return ::media::CdmKeyInformation::KeyStatus::OUTPUT_DOWNSCALED;
    case kStarboardDrmKeyStatusPending:
      return ::media::CdmKeyInformation::KeyStatus::KEY_STATUS_PENDING;
    case kStarboardDrmKeyStatusError:
      return ::media::CdmKeyInformation::KeyStatus::INTERNAL_ERROR;
  }
}

::media::CdmMessageType ToCdmMessageType(StarboardDrmSessionRequestType type) {
  switch (type) {
    case kStarboardDrmSessionRequestTypeLicenseRequest:
      return ::media::CdmMessageType::LICENSE_REQUEST;
    case kStarboardDrmSessionRequestTypeLicenseRenewal:
      return ::media::CdmMessageType::LICENSE_RENEWAL;
    case kStarboardDrmSessionRequestTypeLicenseRelease:
      return ::media::CdmMessageType::LICENSE_RELEASE;
    case kStarboardDrmSessionRequestTypeIndividualizationRequest:
      return ::media::CdmMessageType::INDIVIDUALIZATION_REQUEST;
  }
}

}  // namespace

StarboardDecryptorCast::StarboardDecryptorCast(
    ::media::CreateFetcherCB create_provision_fetcher_cb,
    MediaResourceTracker* media_resource_tracker)
    : CastCdm(media_resource_tracker),
      create_provision_fetcher_cb_(std::move(create_provision_fetcher_cb)),
      callback_handler_{
          this,
          &CallOnSessionUpdateRequest,
          &CallOnSessionUpdated,
          &CallOnKeyStatusesChanged,
          &CallOnCertificateUpdated,
          &CallOnSessionClosed,
      },
      starboard_(GetStarboardApiWrapper()) {
  CHECK(base::SequencedTaskRunner::HasCurrentDefault());
  task_runner_ = base::SequencedTaskRunner::GetCurrentDefault();
}

void StarboardDecryptorCast::CreateSessionAndGenerateRequest(
    ::media::CdmSessionType session_type,
    ::media::EmeInitDataType init_data_type,
    const std::vector<uint8_t>& init_data,
    std::unique_ptr<::media::NewSessionCdmPromise> promise) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  LOG(INFO) << "StarboardDecryptorCast::CreateSessionAndGenerateRequest";

  // Persistent session types are not supported, since SbDrmSystem does not
  // support load/remove.
  if (session_type == ::media::CdmSessionType::kPersistentLicense) {
    promise->reject(::media::CdmPromise::Exception::NOT_SUPPORTED_ERROR, 0,
                    "Persistent licenses are not supported by starboard.");
    return;
  }

  CHECK_EQ(session_type, ::media::CdmSessionType::kTemporary)
      << "Unsupported session type: " << static_cast<int>(session_type);

  queued_session_requests_.push(
      {base::BindOnce(
           &StarboardDecryptorCast::HandleCreateSessionAndGenerateRequest,
           weak_factory_.GetWeakPtr(), session_type, init_data_type, init_data),
       std::move(promise)});

  ProcessQueuedSessionRequests();
}

void StarboardDecryptorCast::HandleCreateSessionAndGenerateRequest(
    ::media::CdmSessionType session_type,
    ::media::EmeInitDataType init_data_type,
    const std::vector<uint8_t>& init_data,
    std::unique_ptr<::media::NewSessionCdmPromise> promise) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(!pending_session_setup_);
  DCHECK(drm_system_);

  pending_session_setup_ = true;
  std::string init_type;
  switch (init_data_type) {
    case ::media::EmeInitDataType::WEBM:
      init_type = "webm";
      break;
    case ::media::EmeInitDataType::CENC:
      init_type = "cenc";
      break;
    case ::media::EmeInitDataType::KEYIDS:
      init_type = "keyids";
      break;
    default:
      // Covers the case of the UNKOWN type, or any unrecognized types.
      promise->reject(
          ::media::CdmPromise::Exception::NOT_SUPPORTED_ERROR, 0,
          "Unsupported init_data_type: " +
              base::NumberToString(static_cast<int>(init_data_type)));
      return;
  }

  const int ticket = current_ticket_++;
  ticket_to_new_session_promise_[ticket] = std::move(promise);
  starboard_->DrmGenerateSessionUpdateRequest(
      drm_system_, ticket, init_type.c_str(), init_data.data(),
      init_data.size());
}

void StarboardDecryptorCast::LoadSession(
    ::media::CdmSessionType session_type,
    const std::string& session_id,
    std::unique_ptr<::media::NewSessionCdmPromise> promise) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  LOG(ERROR) << "StarboardDecryptorCast::LoadSession (not supported)";
  promise->reject(::media::CdmPromise::Exception::NOT_SUPPORTED_ERROR, 0,
                  "LoadSession is not supported by starboard.");
}

void StarboardDecryptorCast::UpdateSession(
    const std::string& web_session_id,
    const std::vector<uint8_t>& response,
    std::unique_ptr<::media::SimpleCdmPromise> promise) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(drm_system_);

  LOG(INFO) << "StarboardDecryptorCast::UpdateSession, web session id = "
            << web_session_id;

  const int ticket = current_ticket_++;
  ticket_to_simple_cdm_promise_[ticket] = std::move(promise);
  // This will eventually call OnSessionUpdated.
  starboard_->DrmUpdateSession(drm_system_, ticket, response.data(),
                               response.size(), web_session_id.c_str(),
                               web_session_id.size());
}

void StarboardDecryptorCast::CloseSession(
    const std::string& web_session_id,
    std::unique_ptr<::media::SimpleCdmPromise> promise) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(drm_system_);

  LOG(INFO) << "StarboardDecryptorCast::CloseSession, web session id = "
            << web_session_id;

  std::vector<std::unique_ptr<::media::SimpleCdmPromise>>& promises =
      session_id_to_simple_cdm_promises_[web_session_id];
  promises.push_back(std::move(promise));

  if (promises.size() == 1) {
    // This is the first request to close the session; mark the session as
    // removed and call starboard to perform the close logic
    StarboardDrmKeyTracker::GetInstance().RemoveKeysForSession(web_session_id);
    starboard_->DrmCloseSession(drm_system_, web_session_id.c_str(),
                                web_session_id.size());
  } else {
    LOG(INFO) << "Session is already closing.";
  }
}

void StarboardDecryptorCast::RemoveSession(
    const std::string& session_id,
    std::unique_ptr<::media::SimpleCdmPromise> promise) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(drm_system_);

  LOG(INFO)
      << "StarboardDecryptorCast::RemoveSession (implemented as CloseSession), "
         "session id = "
      << session_id;
  CloseSession(session_id, std::move(promise));
}

void StarboardDecryptorCast::SetServerCertificate(
    const std::vector<uint8_t>& certificate_data,
    std::unique_ptr<::media::SimpleCdmPromise> promise) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(drm_system_);

  LOG(INFO) << "StarboardDecryptorCast::SetServerCertificate";

  if (!server_certificate_updatable_) {
    LOG(ERROR) << "Tried to update a server certificate for a DRM system that "
                  "does not support it.";
    promise->reject(::media::CdmPromise::Exception::NOT_SUPPORTED_ERROR, 0,
                    "DRM system does not support certificate update");
    return;
  }

  const int ticket = current_ticket_++;
  ticket_to_simple_cdm_promise_[ticket] = std::move(promise);
  starboard_->DrmUpdateServerCertificate(
      drm_system_, ticket, certificate_data.data(), certificate_data.size());
}

std::unique_ptr<DecryptContextImpl> StarboardDecryptorCast::GetDecryptContext(
    const std::string& key_id,
    EncryptionScheme encryption_scheme) const {
  return std::make_unique<DummyDecryptContext>();
}

void StarboardDecryptorCast::SetKeyStatus(const std::string& key_id,
                                          CastKeyStatus key_status,
                                          uint32_t system_code) {
  NOTIMPLEMENTED();
}

void StarboardDecryptorCast::SetVideoResolution(int width, int height) {
  // This would normally be used to support per resolution keys, but is not
  // supported via any starboard API.
  LOG(ERROR) << "Called StarboardDecryptorCast::SetVideoResolution(" << width
             << ", " << height << "). This is not supported by starboard.";
}

StarboardDecryptorCast::~StarboardDecryptorCast() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  if (drm_system_) {
    LOG(INFO) << "Destroying DRM system with address " << drm_system_;
    // Once this call returns, all DRM-related callbacks from Starboard are
    // guaranteed to be finished.
    starboard_->DrmDestroySystem(drm_system_);

    for (const std::string& session_id : session_ids_) {
      StarboardDrmKeyTracker::GetInstance().RemoveKeysForSession(session_id);
    }
  }

  RejectPendingPromises();
}

void StarboardDecryptorCast::InitializeInternal() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  // This just calls EnsureStarboardInitialized in production, but in tests the
  // behavior can be overridden to prevent relying on a real Starboard
  // implementation.
  starboard_->EnsureInitialized();

  drm_system_ = starboard_->CreateDrmSystem(
      /*key_system=*/"com.widevine.alpha",
      /*callback_handler=*/&callback_handler_);
  CHECK(drm_system_) << "Failed to create an SbDrmSystem";
  LOG(INFO) << "Created DRM system with address " << drm_system_;

  server_certificate_updatable_ =
      starboard_->DrmIsServerCertificateUpdatable(drm_system_);
}

void StarboardDecryptorCast::RejectPendingPromises() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  while (!queued_session_requests_.empty()) {
    SessionRequest session_request =
        std::move(queued_session_requests_.front());
    queued_session_requests_.pop();
    if (session_request.second) {
      session_request.second->reject(
          ::media::CdmPromise::Exception::INVALID_STATE_ERROR, 0,
          "GenerateKeyRequest not completed by teardown");
    }
  }
}

void StarboardDecryptorCast::SendProvisionRequest(
    int ticket,
    const std::string& session_id,
    const std::vector<uint8_t>& content) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  if (!provision_fetcher_) {
    provision_fetcher_ = create_provision_fetcher_cb_.Run();
  }
  LOG(INFO) << "Sending provision request";

  const std::string api_key = google_apis::GetAPIKey();
  if (api_key.empty()) {
    LOG(ERROR)
        << "Could not retrieve Google API key. Unable to provision the device.";
    return;
  }
  provision_fetcher_->Retrieve(
      GURL(base::StrCat({kProvisionServerUrlMinusKey, api_key})),
      std::string(reinterpret_cast<const char*>(content.data()),
                  content.size()),
      base::BindOnce(&StarboardDecryptorCast::OnProvisionResponse,
                     weak_factory_.GetWeakPtr(), ticket, session_id));
}

void StarboardDecryptorCast::ProcessQueuedSessionRequests() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  if (queued_session_requests_.empty()) {
    return;
  }

  if (pending_session_setup_) {
    LOG(INFO) << "Queueing session request because another one is in flight";
    return;
  }

  SessionRequest session_request = std::move(queued_session_requests_.front());
  queued_session_requests_.pop();
  // This runs HandleCreateSessionAndGenerateRequest().
  std::move(session_request.first).Run(std::move(session_request.second));
}

void StarboardDecryptorCast::OnSessionUpdateRequest(
    void* drm_system,
    int ticket,
    StarboardDrmStatus status,
    StarboardDrmSessionRequestType type,
    std::optional<std::string> error_message,
    std::optional<std::string> session_id,
    std::vector<uint8_t> content) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  pending_session_setup_ = false;

  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&StarboardDecryptorCast::ProcessQueuedSessionRequests,
                     weak_factory_.GetWeakPtr()));

  if (status == kStarboardDrmStatusSuccess && session_id &&
      type == kStarboardDrmSessionRequestTypeIndividualizationRequest) {
    // Provision requests need to be sent regardless of whether the ticket is
    // valid, so we send this before checking the ticket map.
    SendProvisionRequest(ticket, *session_id, content);
    return;
  }

  auto it = ticket_to_new_session_promise_.find(ticket);

  if (it == ticket_to_new_session_promise_.end()) {
    LOG(WARNING) << "Bad ticket for DRM session create request: " << ticket;
    if (session_id) {
      CHECK_NE(type, kStarboardDrmSessionRequestTypeIndividualizationRequest);
      OnSessionMessage(*session_id, content, ToCdmMessageType(type));
    }
    return;
  }

  // Regardless of success/failure, the promise at it->second must be
  // resolved/rejected and then deleted.
  if (status != kStarboardDrmStatusSuccess) {
    LOG(ERROR) << "Call to StarboardDrmGenerateSessionUpdateRequest for ticket "
               << it->first << "  failed with error: "
               << (error_message ? *error_message : "(null)");
    it->second->reject(StarboardDrmErrorStatusToCdmException(status), 0,
                       error_message ? *error_message : "");
  } else if (!session_id) {
    LOG(ERROR) << "Starboard returned a null session_id on success for ticket "
               << it->first;
    it->second->reject(::media::CdmPromise::Exception::INVALID_STATE_ERROR, 0,
                       "");
  } else {
    // Success case.
    LOG(INFO) << "Created session id " << *session_id << " for ticket "
              << it->first;
    it->second->resolve(*session_id);
  }
  ticket_to_new_session_promise_.erase(it);
  if (status == kStarboardDrmStatusSuccess && session_id) {
    // This function -- defined by the parent class -- should ultimately send
    // the message to the JS app. That will eventually trigger a license
    // request, followed by an update session message being sent to this class.
    // Note that, per the documentation of
    // ContentDecryptionModule::CreateSessionAndGenerateRequest, this must be
    // called AFTER the promise has been resolved.
    OnSessionMessage(*session_id, content, ToCdmMessageType(type));
  }
}

void StarboardDecryptorCast::OnProvisionResponse(int ticket,
                                                 const std::string& session_id,
                                                 bool success,
                                                 const std::string& response) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  if (!success) {
    LOG(ERROR) << "Provisioning failed.";
    auto it = ticket_to_new_session_promise_.find(ticket);
    if (it == ticket_to_new_session_promise_.end()) {
      return;
    }
    it->second->reject(::media::CdmPromise::Exception::INVALID_STATE_ERROR,
                       /*system_code=*/0, "Widevine provisioning failed");
    return;
  }

  LOG(INFO) << "Provisioning succeeded. Updating session in starboard.";
  std::vector<uint8_t> response_vec(response.size());
  memcpy(response_vec.data(), response.c_str(), response.size());

  // This will be called if we successfully update the session.
  auto success_cb =
      [](base::flat_map<int, std::unique_ptr<::media::NewSessionCdmPromise>>*
             promise_map,
         int ticket, std::string session_id) {
        CHECK(promise_map);
        LOG(INFO) << "UpdateSession for provision response succeeded";
        auto it = promise_map->find(ticket);
        if (it == promise_map->end()) {
          return;
        }
        it->second->resolve(session_id);
        promise_map->erase(it);
      };

  // This will be called if the session cannot be updated. The first two args
  // are bound here.
  auto failure_cb =
      [](base::flat_map<int, std::unique_ptr<::media::NewSessionCdmPromise>>*
             promise_map,
         int ticket, ::media::CdmPromise::Exception exception_code,
         uint32_t system_code, const std::string& error_message) {
        CHECK(promise_map);
        LOG(ERROR) << "UpdateSession for provision response failed with "
                      "exception "
                   << static_cast<int>(exception_code) << ", system code "
                   << system_code << ", and error message " << error_message;
        auto it = promise_map->find(ticket);
        if (it == promise_map->end()) {
          return;
        }
        it->second->reject(exception_code, system_code, error_message);
        promise_map->erase(it);
      };

  UpdateSession(
      session_id, response_vec,
      std::make_unique<::media::CdmCallbackPromise<>>(
          /*resolve_cb=*/base::BindOnce(
              success_cb, &ticket_to_new_session_promise_, ticket, session_id),
          /*reject_cb=*/base::BindOnce(
              failure_cb, &ticket_to_new_session_promise_, ticket)));
}

// Called by starboard (via CallOnSessionUpdated) once a session has been
// updated.
void StarboardDecryptorCast::OnSessionUpdated(
    void* drm_system,
    int ticket,
    StarboardDrmStatus status,
    std::optional<std::string> error_message,
    std::optional<std::string> session_id) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  LOG(INFO) << "StarboardDecryptorCast::OnSessionUpdated, ticket: " << ticket
            << ", status: " << status
            << ", session id: " << (session_id ? *session_id : "nullopt")
            << ", error: " << (error_message ? *error_message : "none");
  auto it = ticket_to_simple_cdm_promise_.find(ticket);

  if (it == ticket_to_simple_cdm_promise_.end()) {
    LOG(WARNING) << "Bad ticket for DRM session update request: " << ticket;
    return;
  }

  // Regardless of success/failure, the promise at it->second must be
  // resolved/rejected and then deleted.
  if (status != kStarboardDrmStatusSuccess) {
    LOG(ERROR) << "Call to StarboardDrmUpdateSession for ticket " << it->first
               << "  failed with error: "
               << (error_message ? *error_message : "(null)");
    it->second->reject(StarboardDrmErrorStatusToCdmException(status), 0,
                       error_message ? *error_message : "");
  } else if (!session_id) {
    LOG(ERROR) << "Starboard returned a null session_id on success for ticket "
               << it->first;
    it->second->reject(::media::CdmPromise::Exception::INVALID_STATE_ERROR, 0,
                       "");
  } else {
    // Success case.
    LOG(INFO) << "Updated session id " << *session_id << " for ticket "
              << it->first;
    it->second->resolve();
  }
  ticket_to_simple_cdm_promise_.erase(it);
}

// Called by starboard (via CallOnKeyStatusesChanged) when the status of keys
// change.
void StarboardDecryptorCast::OnKeyStatusesChanged(
    void* drm_system,
    std::string session_id,
    std::vector<std::pair<StarboardDrmKeyId, StarboardDrmKeyStatus>>
        key_ids_and_statuses) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  LOG(INFO) << "StarboardDecryptorCast::OnKeyStatusChanged for "
            << key_ids_and_statuses.size() << " keys";
  ::media::CdmKeysInfo keys_info;
  bool usable_keys_exist = false;
  for (const auto& key_id_and_status : key_ids_and_statuses) {
    const StarboardDrmKeyId& key_id = key_id_and_status.first;
    const StarboardDrmKeyStatus status = key_id_and_status.second;

    const std::string key_name(
        reinterpret_cast<const char*>(&key_id.identifier),
        key_id.identifier_size);
    CHECK_GE(key_id.identifier_size, 0);
    const size_t key_hash = base::FastHash(base::make_span(
        key_id.identifier, static_cast<size_t>(key_id.identifier_size)));
    LOG(INFO) << "DRM key (hash) " << key_hash << " changed status to "
              << DrmKeyStatusToString(status) << " for DRM system with address "
              << drm_system << ", for session " << session_id;

    auto key_info = std::make_unique<::media::CdmKeyInformation>();
    key_info->key_id.assign(key_id.identifier,
                            key_id.identifier + key_id.identifier_size);
    key_info->status = ToMediaKeyStatus(status);
    keys_info.push_back(std::move(key_info));

    usable_keys_exist =
        usable_keys_exist || (status == kStarboardDrmKeyStatusUsable);

    switch (status) {
      case kStarboardDrmKeyStatusUsable:
      case kStarboardDrmKeyStatusRestricted:
      case kStarboardDrmKeyStatusDownscaled:
        // As long as the key is available to the DRM system in some way, we
        // should treat the key as available so that the MediaPipelineBackend
        // can push buffers for the given key.
        StarboardDrmKeyTracker::GetInstance().AddKey(key_name, session_id);
        break;
      case kStarboardDrmKeyStatusPending:
        // The key status will be updated later; do nothing for now.
        break;
      case kStarboardDrmKeyStatusExpired:
      case kStarboardDrmKeyStatusReleased:
      case kStarboardDrmKeyStatusError:
        // The key is no longer usable.
        StarboardDrmKeyTracker::GetInstance().RemoveKey(key_name, session_id);
        break;
    }
  }

  if (usable_keys_exist) {
    // At least one key is available for the session, so we need to track the
    // session to clean it up on destruction.
    session_ids_.insert(session_id);
  }

  OnSessionKeysChange(session_id, usable_keys_exist, std::move(keys_info));
}

// Called by starboard (via CallOnCertificateUpdated) when a certificate has
// been updated.
void StarboardDecryptorCast::OnCertificateUpdated(
    void* drm_system,
    int ticket,
    StarboardDrmStatus status,
    std::optional<std::string> error_message) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  auto it = ticket_to_simple_cdm_promise_.find(ticket);

  if (it == ticket_to_simple_cdm_promise_.end()) {
    LOG(WARNING) << "Bad ticket for DRM certificate update request: " << ticket;
    return;
  }

  // Regardless of success/failure, the promise at it->second must be
  // resolved/rejected and then deleted.
  if (status != kStarboardDrmStatusSuccess) {
    LOG(ERROR) << "Call to StarboardDrmUpdateServerCertificate for ticket "
               << it->first << "  failed with error: "
               << (error_message ? *error_message : "(null)");
    it->second->reject(StarboardDrmErrorStatusToCdmException(status), 0,
                       error_message ? *error_message : "");
  } else {
    // Success case.
    LOG(INFO) << "Updated DRM certificate for ticket " << it->first;
    it->second->resolve();
  }
  ticket_to_simple_cdm_promise_.erase(it);
}

// Called by starboard (via CallOnSessionClosed) when a session has closed.
void StarboardDecryptorCast::OnSessionClosed(void* drm_system,
                                             std::string session_id) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  LOG(INFO) << "StarboardDecryptorCast::OnSessionClosed, session_id: "
            << session_id;
  auto it = session_id_to_simple_cdm_promises_.find(session_id);
  if (it == session_id_to_simple_cdm_promises_.end()) {
    LOG(ERROR)
        << "Bad session ID passed to StarboardDrmCloseSession's callback: "
        << session_id;
    return;
  }

  for (std::unique_ptr<::media::SimpleCdmPromise>& promise : it->second) {
    promise->resolve();
  }
  session_id_to_simple_cdm_promises_.erase(it);

  CastCdm::OnSessionClosed(session_id, ::media::CdmSessionClosedReason::kClose);
}

void StarboardDecryptorCast::CallOnSessionUpdateRequest(
    void* drm_system,
    void* context,
    int ticket,
    StarboardDrmStatus status,
    StarboardDrmSessionRequestType type,
    const char* error_message,
    const void* session_id,
    int session_id_size,
    const void* content,
    int content_size,
    const char* url) {
  if (url && strcmp(url, "") != 0) {
    LOG(ERROR)
        << "Non-empty URL was specified in SessionUpdateRequest callback: "
        << url;
  }
  std::optional<std::string> error_message_copy;
  if (error_message) {
    error_message_copy.emplace(error_message);
  }

  std::optional<std::string> session_id_copy;
  std::vector<uint8_t> content_copy;
  if (session_id) {
    session_id_copy.emplace(reinterpret_cast<const char*>(session_id),
                            session_id_size);
    content_copy.resize(content_size);
    memcpy(content_copy.data(), content, content_size);
  }
  auto* decryptor = reinterpret_cast<StarboardDecryptorCast*>(context);
  decryptor->task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&StarboardDecryptorCast::OnSessionUpdateRequest,
                     decryptor->weak_factory_.GetWeakPtr(), drm_system, ticket,
                     status, type, std::move(error_message_copy),
                     std::move(session_id_copy), std::move(content_copy)));
}

void StarboardDecryptorCast::CallOnSessionUpdated(void* drm_system,
                                                  void* context,
                                                  int ticket,
                                                  StarboardDrmStatus status,
                                                  const char* error_message,
                                                  const void* session_id,
                                                  int session_id_size) {
  std::optional<std::string> error_message_copy;
  if (error_message) {
    error_message_copy.emplace(error_message);
  }

  std::optional<std::string> session_id_copy;
  if (session_id) {
    session_id_copy.emplace(reinterpret_cast<const char*>(session_id),
                            session_id_size);
  }
  auto* decryptor = reinterpret_cast<StarboardDecryptorCast*>(context);
  decryptor->task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&StarboardDecryptorCast::OnSessionUpdated,
                     decryptor->weak_factory_.GetWeakPtr(), drm_system, ticket,
                     status, std::move(error_message_copy),
                     std::move(session_id_copy)));
}

void StarboardDecryptorCast::CallOnKeyStatusesChanged(
    void* drm_system,
    void* context,
    const void* session_id,
    int session_id_size,
    int number_of_keys,
    const StarboardDrmKeyId* key_ids,
    const StarboardDrmKeyStatus* key_statuses) {
  if (!session_id) {
    LOG(ERROR) << "StarboardDecryptorCast::CallOnKeyStatusesChanged was called "
                  "by starboard with a null session_id. Ignoring the call.";
    return;
  }
  std::string session_id_copy(reinterpret_cast<const char*>(session_id),
                              session_id_size);
  if (number_of_keys <= 0) {
    LOG(ERROR) << "Invalid number of keys (" << number_of_keys
               << ") for session " << session_id_copy;
    return;
  }
  std::vector<std::pair<StarboardDrmKeyId, StarboardDrmKeyStatus>>
      key_ids_and_statuses;
  for (int i = 0; i < number_of_keys; ++i) {
    key_ids_and_statuses.push_back({key_ids[i], key_statuses[i]});
  }

  auto* decryptor = reinterpret_cast<StarboardDecryptorCast*>(context);
  decryptor->task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&StarboardDecryptorCast::OnKeyStatusesChanged,
                                decryptor->weak_factory_.GetWeakPtr(),
                                drm_system, std::move(session_id_copy),
                                std::move(key_ids_and_statuses)));
}

void StarboardDecryptorCast::CallOnCertificateUpdated(
    void* drm_system,
    void* context,
    int ticket,
    StarboardDrmStatus status,
    const char* error_message) {
  std::optional<std::string> error_message_copy;
  if (error_message) {
    error_message_copy.emplace(error_message);
  }

  auto* decryptor = reinterpret_cast<StarboardDecryptorCast*>(context);
  decryptor->task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&StarboardDecryptorCast::OnCertificateUpdated,
                     decryptor->weak_factory_.GetWeakPtr(), drm_system, ticket,
                     status, std::move(error_message_copy)));
}

void StarboardDecryptorCast::CallOnSessionClosed(void* drm_system,
                                                 void* context,
                                                 const void* session_id,
                                                 int session_id_size) {
  if (!session_id) {
    LOG(ERROR)
        << "Null session_id was passed to StarboardDrmCloseSession's callback";
    return;
  }

  std::string session_id_copy(reinterpret_cast<const char*>(session_id),
                              session_id_size);

  auto* decryptor = reinterpret_cast<StarboardDecryptorCast*>(context);
  decryptor->task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&StarboardDecryptorCast::OnSessionClosed,
                                decryptor->weak_factory_.GetWeakPtr(),
                                drm_system, std::move(session_id_copy)));
}

void StarboardDecryptorCast::SetStarboardApiWrapperForTest(
    std::unique_ptr<StarboardApiWrapper> starboard) {
  LOG(INFO) << "Replacing the StarboardApiWrapper used by "
               "StarboardDecryptorCast. This should only happen in tests.";
  starboard_ = std::move(starboard);
}

}  // namespace media
}  // namespace chromecast