bramley-engine 0.4.0

Playback engine that runs Apple's MusicKit JS in an embedded JavaScript runtime with native media and Widevine DRM host objects.
Documentation
#include <dlfcn.h>

#include <chrono>
#include <cstdio>
#include <cstring>
#include <deque>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <vector>

#include "../cdm-api/content_decryption_module.h"

extern "C" {
struct ShimCallbacks {
  void* ctx;
  void (*on_initialized)(void* ctx, bool success);
  void (*on_new_session)(void* ctx, uint32_t promise_id, const char* sid, uint32_t sid_len);
  void (*on_resolve)(void* ctx, uint32_t promise_id);
  void (*on_reject)(void* ctx, uint32_t promise_id, uint32_t exception, uint32_t system_code, const char* msg, uint32_t msg_len);
  void (*on_message)(void* ctx, const char* sid, uint32_t sid_len, uint32_t type, const char* msg, uint32_t msg_len);
  void (*on_key)(void* ctx, const char* sid, uint32_t sid_len, const uint8_t* kid, uint32_t kid_len, uint32_t status, uint32_t system_code);
  void (*on_keys_done)(void* ctx, const char* sid, uint32_t sid_len, bool has_additional_usable_key);
  void (*on_expiration)(void* ctx, const char* sid, uint32_t sid_len, double expiry);
  void (*on_closed)(void* ctx, const char* sid, uint32_t sid_len);
  void (*on_log)(void* ctx, const char* msg);
};
}

namespace {

using InitFn = void (*)();
using CreateFn = void* (*)(int, const char*, uint32_t, GetCdmHostFunc, void*);
using VersionFn = const char* (*)();
using DeinitFn = void (*)();

struct Library {
  void* handle = nullptr;
  InitFn init = nullptr;
  CreateFn create = nullptr;
  VersionFn version = nullptr;
  DeinitFn deinit = nullptr;
};

Library g_lib;

class BufferImpl final : public cdm::Buffer {
 public:
  explicit BufferImpl(uint32_t capacity) : data_(capacity), size_(0) {}
  void Destroy() override { delete this; }
  uint32_t Capacity() const override { return static_cast<uint32_t>(data_.size()); }
  uint8_t* Data() override { return data_.data(); }
  void SetSize(uint32_t size) override { size_ = size; }
  uint32_t Size() const override { return size_; }

 private:
  ~BufferImpl() override = default;
  std::vector<uint8_t> data_;
  uint32_t size_;
};

class DecryptedBlockImpl final : public cdm::DecryptedBlock {
 public:
  DecryptedBlockImpl() = default;
  ~DecryptedBlockImpl() override {
    if (buffer_) buffer_->Destroy();
  }
  void SetDecryptedBuffer(cdm::Buffer* buffer) override {
    if (buffer_) buffer_->Destroy();
    buffer_ = buffer;
  }
  cdm::Buffer* DecryptedBuffer() override { return buffer_; }
  void SetTimestamp(int64_t timestamp) override { timestamp_ = timestamp; }
  int64_t Timestamp() const override { return timestamp_; }

 private:
  cdm::Buffer* buffer_ = nullptr;
  int64_t timestamp_ = 0;
};

class Host;

class MemoryFileIO final : public cdm::FileIO {
 public:
  MemoryFileIO(Host* host, cdm::FileIOClient* client) : host_(host), client_(client) {}
  ~MemoryFileIO() override = default;
  void Open(const char* file_name, uint32_t file_name_size) override;
  void Read() override;
  void Write(const uint8_t* data, uint32_t data_size) override;
  void Close() override;
  bool closed = false;

 private:
  Host* host_;
  cdm::FileIOClient* client_;
  std::string name_;
};

class Host final : public cdm::Host_10 {
 public:
  explicit Host(const ShimCallbacks& callbacks) : cb_(callbacks) {}
  ~Host() override = default;

  cdm::ContentDecryptionModule_10* cdm = nullptr;
  std::map<std::string, std::vector<uint8_t>> files;
  std::deque<std::function<void()>> tasks;
  std::vector<std::unique_ptr<MemoryFileIO>> file_ios;

  void Log(const std::string& msg) {
    if (cb_.on_log) cb_.on_log(cb_.ctx, msg.c_str());
  }

  int64_t NextTimerMs() {
    if (!tasks.empty()) return 0;
    if (timers_.empty()) return -1;
    auto now = std::chrono::steady_clock::now();
    auto best = timers_.front().first;
    for (auto& t : timers_) best = std::min(best, t.first);
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(best - now).count();
    return ms < 0 ? 0 : ms;
  }

  void Pump() {
    auto now = std::chrono::steady_clock::now();
    std::vector<void*> due;
    for (auto it = timers_.begin(); it != timers_.end();) {
      if (it->first <= now) {
        due.push_back(it->second);
        it = timers_.erase(it);
      } else {
        ++it;
      }
    }
    for (void* context : due) cdm->TimerExpired(context);
    while (!tasks.empty()) {
      auto task = std::move(tasks.front());
      tasks.pop_front();
      task();
    }
  }

  cdm::Buffer* Allocate(uint32_t capacity) override { return new BufferImpl(capacity); }

  void SetTimer(int64_t delay_ms, void* context) override {
    timers_.emplace_back(std::chrono::steady_clock::now() + std::chrono::milliseconds(delay_ms), context);
  }

  cdm::Time GetCurrentWallTime() override {
    auto now = std::chrono::system_clock::now().time_since_epoch();
    return std::chrono::duration<double>(now).count();
  }

  void OnInitialized(bool success) override { cb_.on_initialized(cb_.ctx, success); }

  void OnResolveKeyStatusPromise(uint32_t promise_id, cdm::KeyStatus key_status) override {
    Log("OnResolveKeyStatusPromise status=" + std::to_string(key_status));
    cb_.on_resolve(cb_.ctx, promise_id);
  }

  void OnResolveNewSessionPromise(uint32_t promise_id, const char* session_id, uint32_t session_id_size) override {
    cb_.on_new_session(cb_.ctx, promise_id, session_id, session_id_size);
  }

  void OnResolvePromise(uint32_t promise_id) override { cb_.on_resolve(cb_.ctx, promise_id); }

  void OnRejectPromise(uint32_t promise_id, cdm::Exception exception, uint32_t system_code, const char* error_message, uint32_t error_message_size) override {
    cb_.on_reject(cb_.ctx, promise_id, exception, system_code, error_message, error_message_size);
  }

  void OnSessionMessage(const char* session_id, uint32_t session_id_size, cdm::MessageType message_type, const char* message, uint32_t message_size) override {
    cb_.on_message(cb_.ctx, session_id, session_id_size, message_type, message, message_size);
  }

  void OnSessionKeysChange(const char* session_id, uint32_t session_id_size, bool has_additional_usable_key, const cdm::KeyInformation* keys_info, uint32_t keys_info_count) override {
    for (uint32_t i = 0; i < keys_info_count; ++i) {
      cb_.on_key(cb_.ctx, session_id, session_id_size, keys_info[i].key_id, keys_info[i].key_id_size, keys_info[i].status, keys_info[i].system_code);
    }
    cb_.on_keys_done(cb_.ctx, session_id, session_id_size, has_additional_usable_key);
  }

  void OnExpirationChange(const char* session_id, uint32_t session_id_size, cdm::Time new_expiry_time) override {
    cb_.on_expiration(cb_.ctx, session_id, session_id_size, new_expiry_time);
  }

  void OnSessionClosed(const char* session_id, uint32_t session_id_size) override { cb_.on_closed(cb_.ctx, session_id, session_id_size); }

  void SendPlatformChallenge(const char*, uint32_t, const char*, uint32_t) override {
    Log("SendPlatformChallenge requested; answering unavailable");
    tasks.push_back([this] {
      cdm::PlatformChallengeResponse empty{};
      cdm->OnPlatformChallengeResponse(empty);
    });
  }

  void EnableOutputProtection(uint32_t desired_protection_mask) override {
    Log("EnableOutputProtection mask=" + std::to_string(desired_protection_mask));
  }

  void QueryOutputProtectionStatus() override {
    Log("QueryOutputProtectionStatus requested; answering no display link");
    tasks.push_back([this] { cdm->OnQueryOutputProtectionStatus(cdm::kQuerySucceeded, cdm::kLinkTypeNone, cdm::kProtectionNone); });
  }

  void OnDeferredInitializationDone(cdm::StreamType stream_type, cdm::Status decoder_status) override {
    Log("OnDeferredInitializationDone " + std::to_string(stream_type) + " " + std::to_string(decoder_status));
  }

  cdm::FileIO* CreateFileIO(cdm::FileIOClient* client) override {
    Log("CreateFileIO (in-memory, never written to disk)");
    file_ios.push_back(std::make_unique<MemoryFileIO>(this, client));
    return file_ios.back().get();
  }

  void RequestStorageId(uint32_t version) override {
    Log("RequestStorageId version=" + std::to_string(version) + "; answering unavailable");
    tasks.push_back([this, version] { cdm->OnStorageId(version, nullptr, 0); });
  }

 private:
  ShimCallbacks cb_;
  std::vector<std::pair<std::chrono::steady_clock::time_point, void*>> timers_;
};

void MemoryFileIO::Open(const char* file_name, uint32_t file_name_size) {
  name_.assign(file_name, file_name_size);
  host_->Log("FileIO::Open " + name_);
  host_->tasks.push_back([this] {
    if (!closed) client_->OnOpenComplete(cdm::FileIOClient::Status::kSuccess);
  });
}

void MemoryFileIO::Read() {
  host_->Log("FileIO::Read " + name_);
  host_->tasks.push_back([this] {
    if (closed) return;
    auto& data = host_->files[name_];
    client_->OnReadComplete(cdm::FileIOClient::Status::kSuccess, data.data(), static_cast<uint32_t>(data.size()));
  });
}

void MemoryFileIO::Write(const uint8_t* data, uint32_t data_size) {
  host_->Log("FileIO::Write " + name_ + " bytes=" + std::to_string(data_size));
  host_->files[name_].assign(data, data + data_size);
  host_->tasks.push_back([this] {
    if (!closed) client_->OnWriteComplete(cdm::FileIOClient::Status::kSuccess);
  });
}

void MemoryFileIO::Close() {
  host_->Log("FileIO::Close " + name_);
  closed = true;
  Host* host = host_;
  host->tasks.push_back([host, this] {
    auto& ios = host->file_ios;
    for (auto it = ios.begin(); it != ios.end(); ++it) {
      if (it->get() == this) {
        ios.erase(it);
        return;
      }
    }
  });
}

int g_requested_host_version = -1;

void* GetHost(int host_interface_version, void* user_data) {
  g_requested_host_version = host_interface_version;
  auto* host = static_cast<Host*>(user_data);
  if (host_interface_version == cdm::Host_10::kVersion) return static_cast<cdm::Host_10*>(host);
  return nullptr;
}

}

extern "C" {

int shim_load(const char* path, char* err, uint32_t err_cap) {
  if (g_lib.handle) return 0;
  g_lib.handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
  if (!g_lib.handle) {
    std::snprintf(err, err_cap, "%s", dlerror());
    return -1;
  }
  g_lib.init = reinterpret_cast<InitFn>(dlsym(g_lib.handle, "InitializeCdmModule_4"));
  g_lib.create = reinterpret_cast<CreateFn>(dlsym(g_lib.handle, "CreateCdmInstance"));
  g_lib.version = reinterpret_cast<VersionFn>(dlsym(g_lib.handle, "GetCdmVersion"));
  g_lib.deinit = reinterpret_cast<DeinitFn>(dlsym(g_lib.handle, "DeinitializeCdmModule"));
  if (!g_lib.init || !g_lib.create || !g_lib.version) {
    std::snprintf(err, err_cap, "missing entry points");
    dlclose(g_lib.handle);
    g_lib = Library{};
    return -2;
  }
  g_lib.init();
  return 0;
}

const char* shim_cdm_version() { return g_lib.version ? g_lib.version() : ""; }

int shim_requested_host_version() { return g_requested_host_version; }

void* shim_create(int interface_version, const ShimCallbacks* callbacks) {
  if (!g_lib.create) return nullptr;
  auto* host = new Host(*callbacks);
  static const char kKeySystem[] = "com.widevine.alpha";
  void* instance = g_lib.create(interface_version, kKeySystem, sizeof(kKeySystem) - 1, &GetHost, host);
  if (!instance) {
    delete host;
    return nullptr;
  }
  host->cdm = static_cast<cdm::ContentDecryptionModule_10*>(instance);
  return host;
}

void shim_initialize(void* handle, bool allow_persistent_state) {
  auto* host = static_cast<Host*>(handle);
  host->cdm->Initialize(false, allow_persistent_state, false);
}

void shim_set_server_certificate(void* handle, uint32_t promise_id, const uint8_t* data, uint32_t size) {
  static_cast<Host*>(handle)->cdm->SetServerCertificate(promise_id, data, size);
}

void shim_create_session(void* handle, uint32_t promise_id, uint32_t init_data_type, const uint8_t* init_data, uint32_t init_data_size) {
  auto* host = static_cast<Host*>(handle);
  host->cdm->CreateSessionAndGenerateRequest(promise_id, cdm::kTemporary, static_cast<cdm::InitDataType>(init_data_type), init_data, init_data_size);
}

void shim_update_session(void* handle, uint32_t promise_id, const char* session_id, uint32_t session_id_size, const uint8_t* response, uint32_t response_size) {
  static_cast<Host*>(handle)->cdm->UpdateSession(promise_id, session_id, session_id_size, response, response_size);
}

void shim_close_session(void* handle, uint32_t promise_id, const char* session_id, uint32_t session_id_size) {
  static_cast<Host*>(handle)->cdm->CloseSession(promise_id, session_id, session_id_size);
}

void shim_remove_session(void* handle, uint32_t promise_id, const char* session_id, uint32_t session_id_size) {
  static_cast<Host*>(handle)->cdm->RemoveSession(promise_id, session_id, session_id_size);
}

uint32_t shim_decrypt(void* handle, uint32_t scheme, uint32_t crypt_blocks, uint32_t skip_blocks, const uint8_t* data, uint32_t data_size, const uint8_t* key_id, uint32_t key_id_size, const uint8_t* iv, uint32_t iv_size, const uint32_t* subsample_pairs, uint32_t num_subsamples, int64_t timestamp, uint8_t* out, uint32_t out_capacity, uint32_t* out_size) {
  auto* host = static_cast<Host*>(handle);
  std::vector<cdm::SubsampleEntry> subsamples(num_subsamples);
  for (uint32_t i = 0; i < num_subsamples; ++i) {
    subsamples[i].clear_bytes = subsample_pairs[2 * i];
    subsamples[i].cipher_bytes = subsample_pairs[2 * i + 1];
  }
  cdm::InputBuffer_2 input{};
  input.data = data;
  input.data_size = data_size;
  input.encryption_scheme = scheme == 2 ? cdm::EncryptionScheme::kCbcs : cdm::EncryptionScheme::kCenc;
  input.key_id = key_id;
  input.key_id_size = key_id_size;
  input.iv = iv;
  input.iv_size = iv_size;
  input.subsamples = subsamples.empty() ? nullptr : subsamples.data();
  input.num_subsamples = num_subsamples;
  input.pattern = {crypt_blocks, skip_blocks};
  input.timestamp = timestamp;
  DecryptedBlockImpl block;
  cdm::Status status = host->cdm->Decrypt(input, &block);
  *out_size = 0;
  if (status == cdm::kSuccess && block.DecryptedBuffer()) {
    uint32_t size = block.DecryptedBuffer()->Size();
    if (size > out_capacity) return 1000;
    std::memcpy(out, block.DecryptedBuffer()->Data(), size);
    *out_size = size;
  }
  return status;
}

void shim_pump(void* handle) { static_cast<Host*>(handle)->Pump(); }

int64_t shim_next_timer_ms(void* handle) { return static_cast<Host*>(handle)->NextTimerMs(); }

void shim_destroy(void* handle) {
  auto* host = static_cast<Host*>(handle);
  host->cdm->Destroy();
  delete host;
}

}