 /* Copyright 2018 Streampunk Media Ltd.

  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.
*/

/* -LICENSE-START-
 ** Copyright (c) 2010 Blackmagic Design
 **
 ** Permission is hereby granted, free of charge, to any person or organization
 ** obtaining a copy of the software and accompanying documentation covered by
 ** this license (the "Software") to use, reproduce, display, distribute,
 ** execute, and transmit the Software, and to prepare derivative works of the
 ** Software, and to permit third-parties to whom the Software is furnished to
 ** do so, all subject to the following:
 **
 ** The copyright notices in the Software and this entire statement, including
 ** the above license grant, this restriction and the following disclaimer,
 ** must be included in all copies of the Software, in whole or in part, and
 ** all derivative works of the Software, unless such copies or derivative
 ** works are solely in the form of machine-executable object code generated by
 ** a source language processor.
 **
 ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 ** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 ** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 ** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 ** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 ** DEALINGS IN THE SOFTWARE.
 ** -LICENSE-END-
 */

#include "playback_promise.h"

HRESULT playbackThreadsafe::ScheduledFrameCompleted(
  IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result) {

  macadamFrame* frame = (macadamFrame*) completedFrame;
  napi_status status, hangover;

  status = napi_acquire_threadsafe_function(tsFn);
  if (status != napi_ok) {
    printf("DEBUG: Failed to acquire NAPI threadsafe function on scheduled frame completion.");
    return E_FAIL;
  }

  frame->deckLinkOutput->GetFrameCompletionReferenceTimestamp(frame, frame->timeScale,
    &frame->completionTimestamp);
  frame->result = result;

  if (frame->tc != nullptr) {
    frame->tc->Update();
  }

  hangover = napi_call_threadsafe_function(tsFn, frame, napi_tsfn_nonblocking);
  if (hangover != napi_ok) {
    printf("DEBUG: Failed to call NAPI threadsafe function on scheduled frame completion.");
  }

  status = napi_release_threadsafe_function(tsFn, napi_tsfn_release);
  if (status != napi_ok) {
    printf("DEBUG: Failed to acquire NAPI threadsafe function on scheduled frame completion.");
    return E_FAIL;
  }

  // delete frame;
  return S_OK;
}

HRESULT playbackThreadsafe::ScheduledPlaybackHasStopped() {

  return S_OK;
}

void finalizePlaybackCarrier(napi_env env, void* finalize_data, void* finalize_hint) {
  // printf("Finalizing playback threadsafe.\n");
  playbackThreadsafe* c = (playbackThreadsafe*) finalize_data;
  delete c;
}

void playbackExecute(napi_env env, void* data) {
  playbackCarrier* c = (playbackCarrier*) data;

  IDeckLinkIterator* deckLinkIterator;
  IDeckLink* deckLink;
  IDeckLinkOutput* deckLinkOutput;
  IDeckLinkKeyer* deckLinkKeyer = nullptr;
  BMDVideoOutputFlags outputFlags = bmdVideoOutputFlagDefault;
  HRESULT hresult;

  #ifdef WIN32
  hresult = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
  CoCreateInstance(CLSID_CDeckLinkIterator, NULL, CLSCTX_ALL, IID_IDeckLinkIterator, (void**)&deckLinkIterator);
  #else
  deckLinkIterator = CreateDeckLinkIteratorInstance();
  #endif
  if (deckLinkIterator == nullptr) {
    c->status = MACADAM_ERROR_START;
    c->errorMsg = "Unable to load DeckLinkAPI.";
    return;
  }

  for ( uint32_t x = 0 ; x <= c->deviceIndex ; x++ ) {
    if (deckLinkIterator->Next(&deckLink) != S_OK) {
      deckLinkIterator->Release();
      c->status = MACADAM_OUT_OF_BOUNDS;
      c->errorMsg = "Device index exceeds the number of installed devices.";
      return;
    }
  }

  deckLinkIterator->Release();

  if (deckLink->QueryInterface(IID_IDeckLinkOutput, (void **)&deckLinkOutput) != S_OK) {
    deckLink->Release();
    c->status = MACADAM_NO_OUTPUT;
    c->errorMsg = "Could not obtain the DeckLink Output interface. Does the device have an output?";
    return;
  }

  if (c->enableKeying) {
    if (deckLink->QueryInterface(IID_IDeckLinkKeyer, (void **)&deckLinkKeyer) != S_OK) {
      deckLink->Release();
      c->status = MACADAM_NO_OUTPUT;
      c->errorMsg = "Unable to retrieve the requested keyer. Is keying supported?";
      return;
    }
    deckLinkKeyer->AddRef();
  }

  deckLink->Release();
  c->deckLinkOutput = deckLinkOutput;
  c->deckLinkKeyer = deckLinkKeyer;

  BMDDisplayModeSupport supported;

  hresult = deckLinkOutput->DoesSupportVideoMode(c->requestedDisplayMode,
    c->requestedPixelFormat, bmdVideoOutputFlagDefault,
    &supported, &c->selectedDisplayMode);
  if (hresult != S_OK) {
    c->status = MACADAM_CALL_FAILURE;
    c->errorMsg = "Unable to determine if video mode is supported by output device.";
    return;
  }
  switch (supported) {
    case bmdDisplayModeSupported:
      break;
    case bmdDisplayModeSupportedWithConversion:
      c->status = MACADAM_NO_CONVERESION; // TODO consider adding conversion support
      c->errorMsg = "Display mode is supported via conversion and not by macadam.";
      return;
    default:
      c->status = MACADAM_MODE_NOT_SUPPORTED;
      c->errorMsg = "Requested display mode is not supported.";
      return;
  }

  if (c->timecode != nullptr) {
    switch (c->selectedDisplayMode->GetDisplayMode()) {
      case bmdModePAL:
      case bmdModePALp:
      case bmdModeNTSC:
      case bmdModeNTSCp:
      case bmdModeNTSC2398:
        outputFlags = (BMDVideoOutputFlags) (outputFlags | bmdVideoOutputVITC);
        break;
      default:
        outputFlags = (BMDVideoOutputFlags) (outputFlags | bmdVideoOutputRP188);
        break;
    }
  }

  hresult = deckLinkOutput->EnableVideoOutput(c->requestedDisplayMode, outputFlags);
  switch (hresult) {
    case E_INVALIDARG: // Should have been picked up by DoesSupportVideoMode
      c->status = MACADAM_INVALID_ARGS;
      c->errorMsg = "Invalid arguments used to enable video output.";
      return;
    case E_ACCESSDENIED:
      c->status = MACADAM_ACCESS_DENIED;
      c->errorMsg = "Unable to access the hardware or input stream is currently active.";
      return;
    case E_OUTOFMEMORY:
      c->status = MACADAM_OUT_OF_MEMORY;
      c->errorMsg = "Unable to create an output video frame - out of memory.";
      return;
    case E_FAIL:
      c->status = MACADAM_CALL_FAILURE;
      c->errorMsg = "Failed to enable video input.";
      return;
    case S_OK:
      break;
  }

  if (c->channels > 0) {
    hresult = deckLinkOutput->EnableAudioOutput(c->requestedSampleRate,
      c->requestedSampleType, c->channels, bmdAudioOutputStreamTimestamped);
    switch (hresult)  {
      case E_INVALIDARG:
        c->status = MACADAM_INVALID_ARGS;
        c->errorMsg = "Invalid arguments used to enable audio output. BMD supports 48kHz, 16- or 32-bit integer only.";
        return;
      case E_FAIL:
        c->status = MACADAM_CALL_FAILURE;
        c->errorMsg = "Failed to enable audio input.";
        return;
      case E_ACCESSDENIED:
        c->status = MACADAM_ACCESS_DENIED;
        c->errorMsg = "Unable to access the hardware or audio output is not enabled.";
        return;
      case E_OUTOFMEMORY:
        c->status = MACADAM_OUT_OF_MEMORY;
        c->errorMsg = "Unable to create a new internal audio frame - out of memory.";
        return;
      case S_OK:
        break;
    }
    hresult = deckLinkOutput->BeginAudioPreroll();
  }

  if (c->enableKeying) {
    hresult = deckLinkKeyer->Enable(c->isExternal);
    if (hresult != S_OK) {
      c->status = MACADAM_CALL_FAILURE;
      c->errorMsg = "Failed to enable keying.";
      return;
    }

    hresult = deckLinkKeyer->SetLevel(c->keyLevel);
    if (hresult != S_OK) {
      c->status = MACADAM_CALL_FAILURE;
      c->errorMsg = "Failed to set key level.";
      return;
    }
    // printf("Enabled decklink %s keying at level %i.\n",
    //   c->isExternal ? "external" : "internal", c->keyLevel);
  }
}

void playbackComplete(napi_env env, napi_status asyncStatus, void* data) {
  napi_value param, paramPart, result, asyncName;
  BMDTimeValue frameRateDuration;
  BMDTimeScale frameRateScale;
  HRESULT hresult;

  playbackCarrier* c = (playbackCarrier*) data;

  if (asyncStatus != napi_ok) {
    c->status = asyncStatus;
    c->errorMsg = "Async capture creator failed to complete.";
  }
  REJECT_STATUS;

  c->status = napi_create_object(env, &result);
  REJECT_STATUS;
  c->status = napi_create_string_utf8(env, "playback", NAPI_AUTO_LENGTH, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "type", param);
  REJECT_STATUS;

  #ifdef WIN32
  BSTR displayModeBSTR = NULL;
  hresult = c->selectedDisplayMode->GetName(&displayModeBSTR);
  if (hresult == S_OK) {
    _bstr_t deviceName(displayModeBSTR, false);
    c->status = napi_create_string_utf8(env, (char*) deviceName, NAPI_AUTO_LENGTH, &param);
    REJECT_STATUS;
  }
  #elif __APPLE__
  CFStringRef displayModeCFString = NULL;
  hresult = c->selectedDisplayMode->GetName(&displayModeCFString);
  if (hresult == S_OK) {
    char displayModeName[64];
    CFStringGetCString(displayModeCFString, displayModeName, sizeof(displayModeName), kCFStringEncodingMacRoman);
    CFRelease(displayModeCFString);
    c->status = napi_create_string_utf8(env, displayModeName, NAPI_AUTO_LENGTH, &param);
    REJECT_STATUS;
  }
  #else
  char* displayModeName;
  hresult = c->selectedDisplayMode->GetName((const char **) &displayModeName);
  if (hresult == S_OK) {
    c->status = napi_create_string_utf8(env, displayModeName, NAPI_AUTO_LENGTH, &param);
    free(displayModeName);
    REJECT_STATUS;
  }
  #endif

  c->status = napi_set_named_property(env, result, "displayModeName", param);
  REJECT_STATUS;

  int32_t width, height, rowBytes;
  width = c->selectedDisplayMode->GetWidth();
  height = c->selectedDisplayMode->GetHeight();
  switch (c->requestedPixelFormat) {
    case bmdFormat8BitYUV:
      rowBytes = width * 2;
      break;
    case bmdFormat10BitYUV:
      rowBytes = ((int32_t) ((width + 47) / 48)) * 128;
      break;
    case bmdFormat8BitARGB:
    case bmdFormat8BitBGRA:
      rowBytes = width * 4;
      break;
    case bmdFormat10BitRGB:
    case bmdFormat10BitRGBXLE:
    case bmdFormat10BitRGBX:
      rowBytes = ((int32_t) ((width + 63) / 64)) * 256;
      break;
    case bmdFormat12BitRGB:
    case bmdFormat12BitRGBLE:
      rowBytes = (int32_t) ((width * 36) / 8);
      break;
    default:
      rowBytes = -1;
      break;
  }

  c->status = napi_create_int32(env, width, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "width", param);
  REJECT_STATUS;

  c->status = napi_create_int32(env, height, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "height", param);
  REJECT_STATUS;

  c->status = napi_create_int32(env, rowBytes, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "rowBytes", param);
  REJECT_STATUS;

  c->status = napi_create_int32(env, rowBytes * height, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "bufferSize", param);
  REJECT_STATUS;

  switch (c->selectedDisplayMode->GetFieldDominance()) {
    case bmdLowerFieldFirst:
      c->status = napi_create_string_utf8(env, "lowerFieldFirst", NAPI_AUTO_LENGTH, &param);
      break;
    case bmdUpperFieldFirst:
      c->status = napi_create_string_utf8(env, "upperFieldFirst", NAPI_AUTO_LENGTH, &param);
      break;
    case bmdProgressiveFrame:
      c->status = napi_create_string_utf8(env, "progressiveFrame", NAPI_AUTO_LENGTH, &param);
      break;
    default:
      c->status = napi_create_string_utf8(env, "unknown", NAPI_AUTO_LENGTH, &param);
      break;
  }
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "fieldDominance", param);
  REJECT_STATUS;

  hresult = c->selectedDisplayMode->GetFrameRate(&frameRateDuration, &frameRateScale);
  if (hresult == S_OK) {
    c->status = napi_create_array(env, &param);
    REJECT_STATUS;
    c->status = napi_create_int64(env, frameRateDuration, &paramPart);
    REJECT_STATUS;
    c->status = napi_set_element(env, param, 0, paramPart);
    REJECT_STATUS;
    c->status = napi_create_int64(env, frameRateScale, &paramPart);
    REJECT_STATUS;
    c->status = napi_set_element(env, param, 1, paramPart);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "frameRate", param);
    REJECT_STATUS;
  }

  uint32_t pixelFormatIndex = 0;

  while ((gKnownPixelFormats[pixelFormatIndex] != 0) &&
      (gKnownPixelFormatNames[pixelFormatIndex] != NULL)) {
    if (c->requestedPixelFormat == gKnownPixelFormats[pixelFormatIndex]) {
      c->status = napi_create_string_utf8(env, gKnownPixelFormatNames[pixelFormatIndex],
        NAPI_AUTO_LENGTH, &param);
      REJECT_STATUS;
      c->status = napi_set_named_property(env, result, "pixelFormat", param);
      REJECT_STATUS;
      break;
    }
    pixelFormatIndex++;
  }

  c->status = napi_get_boolean(env, (c->channels > 0), &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "audioEnabled", param);
  REJECT_STATUS;

  if (c->channels > 0) {
    c->status = napi_create_int32(env, c->requestedSampleRate, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "sampleRate", param);
    REJECT_STATUS;

    c->status = napi_create_int32(env, c->requestedSampleType, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "sampleType", param);
    REJECT_STATUS;

    c->status = napi_create_int32(env, c->channels, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "channels", param);
    REJECT_STATUS;
  };

  c->status = napi_create_int32(env, c->rejectTimeoutMs, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "rejectTimeout", param);
  REJECT_STATUS;

  c->status = napi_get_boolean(env, c->enableKeying, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "enableKeying", param);
  REJECT_STATUS;

  if (c->enableKeying) {
    c->status = napi_get_boolean(env, c->isExternal, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "isExternal", param);
    REJECT_STATUS;

    // TODO this is the initial key level, not a maintained level
    c->status = napi_create_int32(env, c->keyLevel, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "level", param);
    REJECT_STATUS;
  }

  if (c->timecode != nullptr) {
    const char* tcstr;
    hresult = c->timecode->formatTimecodeString(&tcstr, true);
    if (hresult != S_OK) {
      c->status = MACADAM_CALL_FAILURE;
      c->errorMsg = "Unable to format timecode string.";
      REJECT_STATUS;
    }
    c->status = napi_create_string_utf8(env, tcstr, NAPI_AUTO_LENGTH, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "startTimecode", param);
    REJECT_STATUS;
  }
  else {
    c->status = napi_get_undefined(env, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "startTimecode", param);
    REJECT_STATUS;
  }

  playbackThreadsafe* pbts = new playbackThreadsafe;
  pbts->deckLinkOutput = c->deckLinkOutput;
  c->deckLinkOutput = nullptr;
  pbts->displayMode = c->selectedDisplayMode;
  c->selectedDisplayMode = nullptr;
  pbts->timeScale = frameRateScale;
  pbts->frameDuration = frameRateDuration;
  pbts->width = width;
  pbts->height = height;
  pbts->rowBytes = rowBytes;
  pbts->pixelFormat = c->requestedPixelFormat;
  pbts->channels = c->channels;
  pbts->pendingTimeoutTicks = (c->rejectTimeoutMs / 1000) * frameRateScale;
  if (c->channels > 0) {
    pbts->sampleRate = c->requestedSampleRate;
    pbts->sampleType = c->requestedSampleType;
    pbts->sampleByteFactor = c->channels * (pbts->sampleType / 8);
  }
  pbts->enableKeying = c->enableKeying;
  if (c->enableKeying) {
    pbts->isExternal = c->isExternal;
    pbts->keyLevel = c->keyLevel;
    pbts->deckLinkKeyer = c->deckLinkKeyer;
    c->deckLinkKeyer = nullptr;
  }
  pbts->timecode = c->timecode;
  c->timecode = nullptr;

  // printf("Address of %s keyer at level %i in pbts is %p.\n",
  //   pbts->isExternal ? "external" : "internal", pbts->keyLevel,
  //   pbts->deckLinkKeyer);

  hresult = pbts->deckLinkOutput->SetScheduledFrameCompletionCallback(pbts);
  if (hresult != S_OK) {
    c->status = MACADAM_CALL_FAILURE;
    c->errorMsg = "Unable to set callback for deck link output.";
    REJECT_STATUS;
  }

  c->status = napi_create_function(env, "displayFrame", NAPI_AUTO_LENGTH, displayFrame,
    nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "displayFrame", param);
  REJECT_STATUS;

  c->status = napi_create_function(env, "start", NAPI_AUTO_LENGTH, startPlayback,
    nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "start", param);
  REJECT_STATUS;

  c->status = napi_create_function(env, "stop", NAPI_AUTO_LENGTH, stopPlayback,
    nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "stop", param);
  REJECT_STATUS;

  c->status = napi_create_function(env, "schedule", NAPI_AUTO_LENGTH, schedule,
    nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "schedule", param);
  REJECT_STATUS;

  c->status = napi_create_function(env, "played", NAPI_AUTO_LENGTH, played,
    nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "played", param);
  REJECT_STATUS;

  c->status = napi_create_function(env, "referenceStatus", NAPI_AUTO_LENGTH,
    referenceStatus, nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "referenceStatus", param);
  REJECT_STATUS;

  c->status = napi_create_function(env, "scheduledTime", NAPI_AUTO_LENGTH,
    scheduledStreamTime, nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "scheduledTime", param);
  REJECT_STATUS;

  c->status = napi_create_function(env, "hardwareTime", NAPI_AUTO_LENGTH,
    hardwareReferenceClock, nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "hardwareTime", param);
  REJECT_STATUS;

  c->status = napi_create_function(env, "bufferedFrames", NAPI_AUTO_LENGTH,
    bufferedVideoFrameCount, nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "bufferedFrames", param);
  REJECT_STATUS;

  c->status = napi_create_function(env, "bufferedAudioFrames", NAPI_AUTO_LENGTH,
    bufferedAudioSampleFrameCount, nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "bufferedAudioFrames", param);
  REJECT_STATUS;

  if (pbts->enableKeying) {
    c->status = napi_create_function(env, "rampUp", NAPI_AUTO_LENGTH,
      rampUp, nullptr, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "rampUp", param);
    REJECT_STATUS;

    c->status = napi_create_function(env, "rampDown", NAPI_AUTO_LENGTH,
      rampDown, nullptr, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "rampDown", param);
    REJECT_STATUS;

    c->status = napi_create_function(env, "setLevel", NAPI_AUTO_LENGTH,
      setLevel, nullptr, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "setLevel", param);
    REJECT_STATUS;
  }

  if (pbts->timecode != nullptr) {
    c->status = napi_create_function(env, "setTimecode", NAPI_AUTO_LENGTH,
      setTimecode, nullptr, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "setTimecode", param);
    REJECT_STATUS;

    c->status = napi_create_function(env, "getTimecode", NAPI_AUTO_LENGTH,
      getTimecode, nullptr, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "getTimecode", param);
    REJECT_STATUS;

    c->status = napi_create_function(env, "setTimecodeUserbits", NAPI_AUTO_LENGTH,
      setTimecodeUserbits, nullptr, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "setTimecodeUserbits", param);
    REJECT_STATUS;

    c->status = napi_create_function(env, "getTimecodeUserbits", NAPI_AUTO_LENGTH,
      getTimecodeUserbits, nullptr, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "getTimecodeUserbits", param);
    REJECT_STATUS;
  }

  c->status = napi_create_string_utf8(env, "playback", NAPI_AUTO_LENGTH, &asyncName);
  REJECT_STATUS;
  c->status = napi_create_function(env, "nop", NAPI_AUTO_LENGTH, nop, nullptr, &param);
  REJECT_STATUS;
  c->status = napi_create_threadsafe_function(env, param, nullptr, asyncName,
    20, 1, nullptr, playbackTsFnFinalize, pbts, playedFrame, &pbts->tsFn);
  REJECT_STATUS;

  c->status = napi_create_external(env, pbts, finalizePlaybackCarrier, nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "deckLinkOutput", param);
  REJECT_STATUS;

  napi_status status;
  status = napi_resolve_deferred(env, c->_deferred, result);
  FLOATING_STATUS;

  tidyCarrier(env, c);
}

napi_value playback(napi_env env, napi_callback_info info) {
  napi_value promise, resourceName, options, param;
  napi_valuetype type;
  bool isArray;
  playbackCarrier* c = new playbackCarrier;
  int32_t keyLevel = 255;
  HRESULT hresult;
  char tcstr[14];
  size_t tclen;

  c->status = napi_create_promise(env, &c->_deferred, &promise);
  REJECT_RETURN;

  c->requestedDisplayMode = bmdModeHD1080i50;
  c->requestedPixelFormat = bmdFormat10BitYUV;
  size_t argc = 1;
  napi_value args[1];
  c->status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
  REJECT_RETURN;

  if (argc >= 1) {
    c->status = napi_typeof(env, args[0], &type);
    REJECT_RETURN;
    c->status = napi_is_array(env, args[0], &isArray);
    REJECT_RETURN;
    if ((type != napi_object) || (isArray == true)) REJECT_ERROR_RETURN(
        "Options provided to capture create must be an object and not an array.",
        MACADAM_INVALID_ARGS);
    options = args[0];
  }
  else {
    c->status = napi_create_object(env, &options);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "deviceIndex", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Device index must be a number.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, &c->deviceIndex);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "displayMode", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Display mode must be an enumeration value.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, (uint32_t *) &c->requestedDisplayMode);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "pixelFormat", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Pixel format must be an enumeration value.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, (uint32_t *) &c->requestedPixelFormat);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "channels", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Audio channel count must be a number.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, &c->channels);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "sampleRate", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Audio sample rate must be an enumeration value.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, (uint32_t *) &c->requestedSampleRate);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "sampleType", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Audio sample type must be an enumeration value.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, (uint32_t *) &c->requestedSampleType);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "rejectTimeout", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Promise rejection timeout must be a number.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_int32(env, param, &c->rejectTimeoutMs);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "enableKeying", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_boolean) REJECT_ERROR_RETURN(
      "Enable keying with a 'true' boolean value for 'enableKeying' parameter.",
      MACADAM_INVALID_ARGS);
    c->status = napi_get_value_bool(env, param, &c->enableKeying);
    REJECT_RETURN;
    if (c->enableKeying) {
      switch (c->requestedPixelFormat) {
        case bmdFormat8BitARGB:
        case bmdFormat8BitBGRA:
          break;
        default:
          REJECT_ERROR_RETURN(
            "Pixel format must include an alpha channel. Only 8-bit ARGB and BGRA are supported.",
            MACADAM_INVALID_ARGS);
      }
    }
  }

  c->status = napi_get_named_property(env, options, "isExternal", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_boolean) REJECT_ERROR_RETURN(
      "Setting external or internal keying requires a boolean parameter.",
      MACADAM_INVALID_ARGS);
    if (!c->enableKeying) REJECT_ERROR_RETURN(
      "Setting keying type withing explicitly enabling keying.",
      MACADAM_INVALID_ARGS);
    c->status = napi_get_value_bool(env, param, &c->isExternal);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "level", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Keying level must be a number.", MACADAM_INVALID_ARGS);
    if (!c->enableKeying) REJECT_ERROR_RETURN(
      "Setting keying level withing explicitly enabling keying.",
      MACADAM_INVALID_ARGS);
    c->status = napi_get_value_int32(env, param, &keyLevel);
    REJECT_RETURN;
    if ((keyLevel < 0) || (keyLevel > 255)) REJECT_ERROR_RETURN(
      "Keying level is outside of range 0 to 255.", MACADAM_OUT_OF_BOUNDS);
    c->keyLevel = (uint8_t) keyLevel;
  }

  c->status = napi_get_named_property(env, options, "startTimecode", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_string) REJECT_ERROR_RETURN(
      "Start timecode must be provided as a string value.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_string_utf8(env, param, tcstr, 14, &tclen);
    REJECT_RETURN;
    uint16_t fps = 25;
    switch (c->requestedDisplayMode) {
      case bmdModeHD1080p2398:
      case bmdModeHD1080p24:
      case bmdMode2k2398:
      case bmdMode2k24:
      case bmdMode2kDCI2398:
      case bmdMode4K2160p2398:
      case bmdMode4K2160p24:
      case bmdMode2kDCI24:
      case bmdMode4kDCI2398:
      case bmdMode4kDCI24:
        fps = 24;
        break;
      case bmdModePAL:
      case bmdModeHD1080p25:
      case bmdModeHD1080i50:
      case bmdMode2k25:
      case bmdMode2kDCI25:
      case bmdMode4K2160p25:
      case bmdMode4kDCI25:
        fps = 25;
        break;
      case bmdModeNTSC:
      case bmdModeNTSC2398: // 3:2 pulldown applied on card
      case bmdModeHD1080p30:
      case bmdModeHD1080p2997:
      case bmdMode4K2160p2997:
      case bmdMode4K2160p30:
        fps = 30;
        break;
      case bmdModePALp:
      case bmdModeHD720p50:
      case bmdModeHD1080p50:
      case bmdMode4K2160p50:
        fps = 50;
        break;
      case bmdModeNTSCp:
      case bmdModeHD720p5994:
      case bmdModeHD720p60:
      case bmdModeHD1080i5994:
      case bmdModeHD1080i6000:
      case bmdModeHD1080p5994:
      case bmdModeHD1080p6000:
      case bmdMode4K2160p5994:
      case bmdMode4K2160p60:
        fps = 60;
        break;
      default:
        REJECT_ERROR_RETURN("Unrecognised mode for establishing timecode FPS.",
          MACADAM_INVALID_ARGS);
    }
    hresult = parseTimecode(fps, tcstr, &c->timecode);
    if (hresult != S_OK) {
      c->timecode = nullptr;
      REJECT_ERROR_RETURN("Incorrectly formatted timecode. Should be HH:MM:SS:FF.f or HH:MM:SS;FF etc..",
        MACADAM_INVALID_ARGS);
    }
  }

  c->status = napi_create_string_utf8(env, "CreatePlayback", NAPI_AUTO_LENGTH, &resourceName);
  REJECT_RETURN;
  c->status = napi_create_async_work(env, NULL, resourceName, playbackExecute,
    playbackComplete, c, &c->_request);
  REJECT_RETURN;
  c->status = napi_queue_async_work(env, c->_request);
  REJECT_RETURN;

  return promise;
}

void playedFrame(napi_env env, napi_value jsCb, void* context, void* data) {
  napi_status status;
  napi_value resres, param, errorValue, errorCode, errorMsg;
  macadamFrame* frame = (macadamFrame*) data;
  playbackThreadsafe* pbts = (playbackThreadsafe*) context;
  scheduleCarrier* c = nullptr;

  //printf("Scheduled frame %lld playback completed with timestamp %lld and result %i.\n",
  //  frame->scheduledTime, frame->completionTimestamp, frame->result);

  for (std::map<BMDTimeValue, scheduleCarrier*>::iterator it = pbts->pendingPlays.begin() ;
    it != pbts->pendingPlays.end() ; ++it) {

    if (it->first > frame->scheduledTime - pbts->pendingTimeoutTicks) break;
    char* extMsg = (char *) malloc(sizeof(char) * 200);
    sprintf(extMsg, "Pending frame promise timed out for scheduled time %lld as just played %lld.",
      (long long) it->second->scheduledTime, (long long) frame->scheduledTime);
    char errorCodeChars[20];
    sprintf(errorCodeChars, "%d", MACADAM_FRAME_TIMEOUT);
    status = napi_create_string_utf8(env, errorCodeChars,
      NAPI_AUTO_LENGTH, &errorCode);
    FLOATING_STATUS;
    status = napi_create_string_utf8(env, extMsg, NAPI_AUTO_LENGTH, &errorMsg);
    FLOATING_STATUS;
    status = napi_create_error(env, errorCode, errorMsg, &errorValue);
    FLOATING_STATUS;
    status = napi_reject_deferred(env, it->second->_deferred, errorValue);
    FLOATING_STATUS;

    tidyCarrier(env, it->second);
    pbts->pendingPlays.erase(it->first);
    if (pbts->pendingPlays.empty()) break;
  }

  // See if any pending play promises exist in map and fulfil
  auto played = pbts->pendingPlays.find(frame->scheduledTime);
  if (played != pbts->pendingPlays.end()) {
    c = played->second;
    c->status = napi_create_object(env, &resres);
    REJECT_BAIL;

    c->status = napi_create_string_utf8(env, "played", NAPI_AUTO_LENGTH, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, resres, "type", param);
    REJECT_BAIL;

    c->status = napi_create_int64(env, frame->scheduledTime, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, resres, "scheduledTime", param);
    REJECT_BAIL;

    c->status = napi_create_int64(env, pbts->timeScale, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, resres, "timeScale", param);
    REJECT_BAIL;

    c->status = napi_create_int32(env, frame->result, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, resres, "result", param);
    REJECT_BAIL;

    switch (frame->result) {
      case bmdOutputFrameCompleted:
        c->status = napi_create_string_utf8(env, "completed", NAPI_AUTO_LENGTH, &param);
        break;
      case bmdOutputFrameDisplayedLate:
        c->status = napi_create_string_utf8(env, "late", NAPI_AUTO_LENGTH, &param);
        break;
      case bmdOutputFrameDropped:
        c->status = napi_create_string_utf8(env, "dropped", NAPI_AUTO_LENGTH, &param);
        break;
      case bmdOutputFrameFlushed:
        c->status = napi_create_string_utf8(env, "flushed", NAPI_AUTO_LENGTH, &param);
        break;
      default:
        c->status = napi_create_string_utf8(env, "unknown", NAPI_AUTO_LENGTH, &param);
        break;
    }
    REJECT_BAIL;
    c->status = napi_set_named_property(env, resres, "summary", param);
    REJECT_BAIL;

    c->status = napi_create_int64(env, frame->completionTimestamp, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, resres, "completionTimestamp", param);
    REJECT_BAIL;

    c->status = napi_resolve_deferred(env, c->_deferred, resres);
    REJECT_BAIL;

    pbts->pendingPlays.erase(frame->scheduledTime);
    tidyCarrier(env, c);
  }
  else {
    printf("DEBUG: No promise to resolve for played frame with scheduled time %lld.\n",
      (long long) frame->scheduledTime);
  }

bail:
  status = napi_delete_reference(env, frame->sourceBufferRef);
  if (status != napi_ok) {
    printf("DEBUG: Failed to delete video buffer reference for scheduled frame %lld.\n",
      (long long) frame->scheduledTime);
  }
  delete frame;
  return;
}

void displayFrameExecute(napi_env env, void* data) {
  displayFrameCarrier* c = (displayFrameCarrier*) data;
  uint32_t sampleFramesWritten;
  HRESULT hresult;

  // This call may block - make sure thread pool is large enough
  hresult = c->deckLinkOutput->DisplayVideoFrameSync(c);
  switch (hresult) {
    case E_FAIL:
      c->status = MACADAM_CALL_FAILURE;
      c->errorMsg = "Failed to display a video frame.";
      break;
    case E_ACCESSDENIED:
      c->status = MACADAM_ACCESS_DENIED;
      c->errorMsg = "On request to display a frame, the video output is not enabled.";
      break;
    case E_INVALIDARG:
      c->status = MACADAM_INVALID_ARGS;
      c->errorMsg = "On request to display a frame, the frame attributes are not valid.";
      break;
    case S_OK:
      break;
    default:
      break;
  }
  // frame->Release();
  if ((c->audioRef != nullptr) && (c->status == napi_ok)) {
    hresult = c->deckLinkOutput->WriteAudioSamplesSync(c->audioData,
      c->sampleFrameCount, &sampleFramesWritten);
    // printf("Sample frame count %i and samples written %i.\n", c->sampleFrameCount,
    //   sampleFramesWritten);
    if (hresult != S_OK) {
      c->status = MACADAM_CALL_FAILURE;
      c->errorMsg = "Failed to write audio samples synchronously.";
    }
  }
}

void displayFrameComplete(napi_env env, napi_status asyncStatus, void* data) {
  displayFrameCarrier* c = (displayFrameCarrier*) data;
  napi_value result;

  if (asyncStatus != napi_ok) {
    c->status = asyncStatus;
    c->errorMsg = "Display frame failed to complete.";
  }
  REJECT_STATUS;

  c->status = napi_create_object(env, &result);
  REJECT_STATUS;

  if (c->audioRef != nullptr) {
    c->status = napi_delete_reference(env, c->audioRef);
    REJECT_STATUS;
  }

  if (c->tc != nullptr) {
    c->tc->Update();
  }

  napi_status status;
  status = napi_resolve_deferred(env, c->_deferred, result);
  FLOATING_STATUS;

  tidyCarrier(env, c);
}

napi_value displayFrame(napi_env env, napi_callback_info info) {
  napi_value promise, resourceName, playback, param;
  playbackThreadsafe* pbts;
  displayFrameCarrier* c = new displayFrameCarrier;
  bool isBuffer;

  c->status = napi_create_promise(env, &c->_deferred, &promise);
  REJECT_RETURN;

  size_t argc = 2;
  napi_value argv[2];
  c->status = napi_get_cb_info(env, info, &argc, argv, &playback, nullptr);
  REJECT_RETURN;

  if (argc < 1) REJECT_ERROR_RETURN(
    "Frame can only be displayed from a buffer of data.", MACADAM_INVALID_ARGS);

  c->status = napi_is_buffer(env, argv[0], &isBuffer);
  REJECT_RETURN;

  if (!isBuffer) REJECT_ERROR_RETURN(
    "Frame data must be provided as a node buffer.", MACADAM_INVALID_ARGS);

  c->status = napi_get_buffer_info(env, argv[0], &c->data, &c->dataSize);
  REJECT_RETURN;

  c->status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  REJECT_RETURN;
  c->status = napi_get_value_external(env, param, (void**) &pbts);
  if (c->status == napi_invalid_arg) REJECT_ERROR_RETURN(
    "Display frame cannot be used once an output is stopped.", MACADAM_ALREADY_STOPPED);
  REJECT_RETURN;

  if (pbts->started) REJECT_ERROR_RETURN(
    "Display frame cannot be used in conjuction with scheduled playback.",
    MACADAM_ERROR_START);

  if (argc == 2) { // Audio data provided
    c->status = napi_is_buffer(env, argv[1], &isBuffer);
    REJECT_RETURN;

    if (!isBuffer) REJECT_ERROR_RETURN(
      "Audio data must be provided as a node buffer.", MACADAM_INVALID_ARGS);

    c->status = napi_get_buffer_info(env, argv[1], &c->audioData, &c->audioDataSize);
    REJECT_RETURN;

    c->sampleFrameCount = c->audioDataSize / pbts->sampleByteFactor;
  }

  c->width = pbts->width;
  c->height = pbts->height;
  c->rowBytes = pbts->rowBytes;
  c->pixelFormat = pbts->pixelFormat;
  c->deckLinkOutput = pbts->deckLinkOutput;

  if (((int32_t) c->dataSize) < c->rowBytes * c->height) REJECT_ERROR_RETURN(
    "Insufficient number of bytes in buffer for frame playback.",
    MACADAM_INSUFFICIENT_BYTES);

  c->status = napi_create_reference(env, argv[0], 1, &c->passthru);
  REJECT_RETURN;

  if (argc == 2) {
    c->status = napi_create_reference(env, argv[1], 1, &c->audioRef);
    REJECT_RETURN;
    pbts->deckLinkOutput->EndAudioPreroll();
  }

  if (pbts->timecode != nullptr) {
    c->tc = pbts->timecode;
  }

  c->status = napi_create_string_utf8(env, "DisplayFrame", NAPI_AUTO_LENGTH, &resourceName);
  REJECT_RETURN;
  c->status = napi_create_async_work(env, nullptr, resourceName, displayFrameExecute,
    displayFrameComplete, c, &c->_request);
  REJECT_RETURN;
  c->status = napi_queue_async_work(env, c->_request);
  REJECT_RETURN;

  return promise;
}

napi_value schedule(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value playback, param, videoBuffer, audioBuffer, value;
  napi_valuetype type;
  playbackThreadsafe* pbts;
  HRESULT hresult;
  macadamFrame* frame = new macadamFrame;
  bool hasProp, isBuffer;
  void* audioData;
  size_t audioDataSize;
  uint32_t sampleFrameCount, sampleFramesWritten;

  size_t argc = 1;
  napi_value argv[1];
  status = napi_get_cb_info(env, info, &argc, argv, &playback, nullptr);
  CHECK_STATUS;

  if (argc != 1) NAPI_THROW_ERROR(
    "Frame can only be scheduled with an object containing payload and time.");

  status = napi_typeof(env, argv[0], &type);
  CHECK_STATUS;
  if (type != napi_object) NAPI_THROW_ERROR(
    "Type of value passed to schedule must be an object.");

  status = napi_has_named_property(env, argv[0], "video", &hasProp);
  CHECK_STATUS;
  if (!hasProp) NAPI_THROW_ERROR("To schedule a frame, a video buffer must be provided.");

  status = napi_has_named_property(env, argv[0], "time", &hasProp);
  CHECK_STATUS;
  if (!hasProp) NAPI_THROW_ERROR("To schedule a frame, a time value must be provided.");

  status = napi_get_named_property(env, argv[0], "video", &videoBuffer);
  CHECK_STATUS;

  status = napi_is_buffer(env, videoBuffer, &isBuffer);
  CHECK_STATUS;

  if (!isBuffer) NAPI_THROW_ERROR("Video data must be provided as a node buffer.");

  status = napi_get_buffer_info(env, videoBuffer, &frame->data, &frame->dataSize);
  CHECK_STATUS;

  status = napi_get_named_property(env, argv[0], "time", &param);
  CHECK_STATUS;
  status = napi_typeof(env, param, &type);
  CHECK_STATUS;
  if (type != napi_number) NAPI_THROW_ERROR("Scheduled time must be a number.");
  status = napi_get_value_int64(env, param, &frame->scheduledTime);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg)
    NAPI_THROW_ERROR("Cannot schedule frames after playout has stopped.");
  CHECK_STATUS;

  if (pbts->channels > 0) {
    status = napi_has_named_property(env, argv[0], "audio", &hasProp);
    CHECK_STATUS;
    if (!hasProp) NAPI_THROW_ERROR("To schedule a frame, an audio buffer must be provided.");

    status = napi_get_named_property(env, argv[0], "audio", &audioBuffer);
    CHECK_STATUS;

    status = napi_is_buffer(env, videoBuffer, &isBuffer);
    CHECK_STATUS;

    if (!isBuffer) NAPI_THROW_ERROR("Audio data must be provided as a node buffer.");

    status = napi_get_named_property(env, argv[0], "sampleFrameCount", &param);
    CHECK_STATUS;
    status = napi_typeof(env, param, &type);
    CHECK_STATUS;
    if (type == napi_number) {
      status = napi_get_value_uint32(env, param, &sampleFrameCount);
      CHECK_STATUS;
    }
    else {
      sampleFrameCount = 0;
    }
  }

  if (((int32_t) frame->dataSize) < (pbts->rowBytes * pbts->height))
    NAPI_THROW_ERROR("Insufficient bytes provided to schedule video frame.");

  frame->width = pbts->width;
  frame->height = pbts->height;
  frame->rowBytes = pbts->rowBytes;
  frame->pixelFormat = pbts->pixelFormat;
  frame->timeScale = pbts->timeScale;
  frame->deckLinkOutput = pbts->deckLinkOutput;

  if (pbts->timecode != nullptr) {
    frame->tc = pbts->timecode;
  }

  hresult = pbts->deckLinkOutput->ScheduleVideoFrame(frame, frame->scheduledTime,
    pbts->frameDuration, pbts->timeScale);

  switch (hresult) {
    case S_OK:
      break;
    case E_ACCESSDENIED:
      NAPI_THROW_ERROR("Failed to schedule frame as the video output is not enabled.");
    case E_INVALIDARG:
      NAPI_THROW_ERROR("Failed to schedule frame as the attributes are invalid.");
    case E_OUTOFMEMORY:
      NAPI_THROW_ERROR("Fauled to schedule frame as too many frames are already scheduled.");
    case E_FAIL:
    default:
      NAPI_THROW_ERROR("Failed to schedule frame - general failure.");
  }

  if (pbts->channels > 0) {
    status = napi_get_buffer_info(env, audioBuffer, &audioData, &audioDataSize);
    CHECK_STATUS;
    if (sampleFrameCount == 0) { // Not provided
      sampleFrameCount = audioDataSize / pbts->sampleByteFactor;
    }
    // TODO Assuming audio data is copied
    hresult = pbts->deckLinkOutput->ScheduleAudioSamples(audioData, sampleFrameCount,
      frame->scheduledTime, pbts->timeScale, &sampleFramesWritten);
    switch (hresult) {
      case S_OK:
        break;
      case E_ACCESSDENIED:
        NAPI_THROW_ERROR("Audio output has not been enabled or audio sample write in progress.");
      case E_INVALIDARG:
        NAPI_THROW_ERROR("No timescale was provided when scheduling audio samples.");
      case E_FAIL:
        NAPI_THROW_ERROR("Failed to schedule audio for frame - general failure.");
    }
    // TODO check sample frames provided and written are the same
    // printf("Sample frame count %u frames written %u.\n", sampleFrameCount, sampleFramesWritten);
  }

  // Ensure that buffer is not garbage collected during playback
  status = napi_create_reference(env, videoBuffer, 1, &frame->sourceBufferRef);
  CHECK_STATUS;

  status = napi_get_undefined(env, &value);
  CHECK_STATUS;
  return value;
}

napi_value startPlayback(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value playback, param, value;
  napi_valuetype type;
  playbackThreadsafe* pbts;
  HRESULT hresult;
  BMDTimeValue startTime = 0;
  double playbackSpeed = 1.0;

  size_t argc = 1;
  napi_value argv[1];
  status = napi_get_cb_info(env, info, &argc, argv, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already started and then stopped.");
  CHECK_STATUS;

  if (pbts->started) NAPI_THROW_ERROR("Already started.");

  if (argc >= 1) {
    status = napi_typeof(env, argv[0], &type);
    CHECK_STATUS;
    if (type != napi_object) NAPI_THROW_ERROR("Parameters must be provided in an object.");

    status = napi_get_named_property(env, argv[0], "startTime", &param);
    CHECK_STATUS;
    status = napi_typeof(env, param, &type);
    CHECK_STATUS;
    if (type == napi_number) {
      status = napi_get_value_int64(env, param, &startTime);
      CHECK_STATUS;
    } else if (type != napi_undefined) NAPI_THROW_ERROR("Playback start time must be a number.");

    status = napi_get_named_property(env, argv[0], "playbackSpeed", &param);
    CHECK_STATUS;
    status = napi_typeof(env, param, &type);
    CHECK_STATUS;
    if (type == napi_number) {
      status = napi_get_value_double(env, param, &playbackSpeed);
      CHECK_STATUS;
    } else if (type != napi_undefined) NAPI_THROW_ERROR("Playback speed must be a number.");
  }

  if (pbts->channels > 0) {
    hresult = pbts->deckLinkOutput->EndAudioPreroll();
    if (hresult != S_OK) NAPI_THROW_ERROR("Failed to end audio preroll.\n");
  }

  hresult = pbts->deckLinkOutput->StartScheduledPlayback(
    startTime, pbts->timeScale, playbackSpeed);
  if (hresult != S_OK) NAPI_THROW_ERROR("Failed to start scheduled playback.");

  pbts->started = true;

  status = napi_get_undefined(env, &value);
  CHECK_STATUS;
  return value;
}

napi_value played(napi_env env, napi_callback_info info) {
  napi_value promise, playback, param;
  napi_valuetype type;
  scheduleCarrier* c = new scheduleCarrier;
  playbackThreadsafe* pbts;

  c->status = napi_create_promise(env, &c->_deferred, &promise);
  REJECT_RETURN;

  size_t argc = 1;
  napi_value argv[1];
  c->status = napi_get_cb_info(env, info, &argc, argv, &playback, nullptr);
  REJECT_RETURN;

  if (argc != 1) REJECT_ERROR_RETURN(
    "Scheduled play time for played frame must be provided.", MACADAM_INVALID_ARGS);

  c->status = napi_typeof(env, argv[0], &type);
  REJECT_RETURN;
  if (type != napi_number) REJECT_ERROR_RETURN(
    "Played frame promise requires the scheduled time.",
    MACADAM_INVALID_ARGS);

  c->status = napi_get_value_int64(env, argv[0], &c->scheduledTime);
  REJECT_RETURN;

  c->status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  REJECT_RETURN;
  c->status = napi_get_value_external(env, param, (void**) &pbts);
  REJECT_RETURN;

  auto existing = pbts->pendingPlays.find(c->scheduledTime);
  if (existing != pbts->pendingPlays.end()) { // Should always go in here
    c->status = napi_get_reference_value(env, existing->second->passthru, &promise);
    REJECT_RETURN;
    return promise;
  }

  c->status = napi_create_reference(env, promise, 1, &c->passthru);
  REJECT_RETURN;

  pbts->pendingPlays.insert(std::make_pair(c->scheduledTime, c));
  return promise;
}

napi_value scheduledStreamTime(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value playback, param, value;
  playbackThreadsafe* pbts;
  HRESULT hresult;

  size_t argc = 0;
  status = napi_get_cb_info(env, info, &argc, nullptr, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  BMDTimeValue streamTime;
  double playbackSpeed;
  hresult = pbts->deckLinkOutput->GetScheduledStreamTime(pbts->timeScale,
    &streamTime, &playbackSpeed);
  if (hresult != S_OK) NAPI_THROW_ERROR("Failed to retrieve scheduled stream time.");

  status = napi_create_object(env, &value);
  CHECK_STATUS;

  status = napi_create_string_utf8(env, "scheduledStreamTime", NAPI_AUTO_LENGTH, &param);
  CHECK_STATUS;
  status = napi_set_named_property(env, value, "type", param);
  CHECK_STATUS;

  status = napi_create_int32(env, (int32_t) streamTime, &param);
  CHECK_STATUS;
  status = napi_set_named_property(env, value, "streamTime", param);
  CHECK_STATUS;

  status = napi_create_double(env, playbackSpeed, &param);
  CHECK_STATUS;
  status = napi_set_named_property(env, value, "playbackSpeed", param);
  CHECK_STATUS;

  return value;
}

napi_value referenceStatus(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value playback, param, value;
  playbackThreadsafe* pbts;
  HRESULT hresult;

  size_t argc = 0;
  status = napi_get_cb_info(env, info, &argc, nullptr, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  BMDReferenceStatus refStatus;
  hresult = pbts->deckLinkOutput->GetReferenceStatus(&refStatus);
  if (hresult != S_OK)
    NAPI_THROW_ERROR("Failed to get reference status.");

  switch (refStatus) {
    case bmdReferenceNotSupportedByHardware:
      status = napi_create_string_utf8(env, "ReferenceNotSupportedByHardware",
        NAPI_AUTO_LENGTH, &value);
      break;
    case bmdReferenceLocked:
      status = napi_create_string_utf8(env, "ReferenceLocked",
        NAPI_AUTO_LENGTH, &value);
      break;
    default:
      status = napi_create_string_utf8(env, "ReferenceNotLocked",
        NAPI_AUTO_LENGTH, &value);
      break;
  }
  CHECK_STATUS;
  return value;
}

napi_value hardwareReferenceClock(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value playback, param, value;
  playbackThreadsafe* pbts;
  HRESULT hresult;
  BMDTimeValue hardwareTime, timeInFrame, ticksPerFrame;

  size_t argc = 0;
  status = napi_get_cb_info(env, info, &argc, nullptr, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  hresult = pbts->deckLinkOutput->GetHardwareReferenceClock(pbts->timeScale,
    &hardwareTime, &timeInFrame, &ticksPerFrame);
  if (hresult != S_OK) NAPI_THROW_ERROR("Failed to get hardware reference clock.");

  status = napi_create_object(env, &value);
  CHECK_STATUS;

  status = napi_create_string_utf8(env, "hardwareRefClock", NAPI_AUTO_LENGTH, &param);
  CHECK_STATUS;
  status = napi_set_named_property(env, value, "type", param);
  CHECK_STATUS;

  status = napi_create_int64(env, pbts->timeScale, &param);
  CHECK_STATUS;
  status = napi_set_named_property(env, value, "timeScale", param);
  CHECK_STATUS;

  status = napi_create_int64(env, hardwareTime, &param);
  CHECK_STATUS;
  status = napi_set_named_property(env, value, "hardwareTime", param);
  CHECK_STATUS;

  status = napi_create_int64(env, timeInFrame, &param);
  CHECK_STATUS;
  status = napi_set_named_property(env, value, "timeInFrame", param);
  CHECK_STATUS;

  status = napi_create_int64(env, ticksPerFrame, &param);
  CHECK_STATUS;
  status = napi_set_named_property(env, value, "ticksPerFrame", param);
  CHECK_STATUS;

  return value;
}

napi_value bufferedVideoFrameCount(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value playback, param, value;
  playbackThreadsafe* pbts;
  HRESULT hresult;

  size_t argc = 0;
  status = napi_get_cb_info(env, info, &argc, nullptr, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  uint32_t sampleFrameCount;
  hresult = pbts->deckLinkOutput->GetBufferedVideoFrameCount(&sampleFrameCount);
  if (hresult != S_OK)
    NAPI_THROW_ERROR("Failed to get buffered video sample frame count.");

  status = napi_create_int32(env, sampleFrameCount, &value);
  CHECK_STATUS;
  return value;
}

napi_value bufferedAudioSampleFrameCount(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value playback, param, value;
  playbackThreadsafe* pbts;
  HRESULT hresult;

  size_t argc = 0;
  status = napi_get_cb_info(env, info, &argc, nullptr, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  uint32_t sampleFrameCount;
  hresult = pbts->deckLinkOutput->GetBufferedAudioSampleFrameCount(&sampleFrameCount);
  if (hresult != S_OK)
    NAPI_THROW_ERROR("Failed to get buffered audio sample frame count.");

  status = napi_create_int32(env, sampleFrameCount, &value);
  CHECK_STATUS;
  return value;
}

napi_value stopPlayback(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value playback, param, value;
  playbackThreadsafe* pbts;
  HRESULT hresult;

  size_t argc = 0;
  status = napi_get_cb_info(env, info, &argc, nullptr, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  if (pbts->started) {
    hresult = pbts->deckLinkOutput->StopScheduledPlayback(0, nullptr, 0);
    if (hresult != S_OK) NAPI_THROW_ERROR("Failed to stop scheduled playback.");
  }

  if (pbts->enableKeying) {
    hresult = pbts->deckLinkKeyer->Disable();
    if (hresult != S_OK) NAPI_THROW_ERROR("Failed to disable keyer.");
    pbts->deckLinkKeyer->Release();
    pbts->deckLinkKeyer = nullptr;
  }

  hresult = pbts->deckLinkOutput->DisableVideoOutput();
  if (hresult != S_OK) NAPI_THROW_ERROR("Failed to disable video output.");

  hresult = pbts->deckLinkOutput->SetScheduledFrameCompletionCallback(nullptr);
  if (hresult != S_OK) NAPI_THROW_ERROR("Failed to clear the frame completion callback.");

  if (pbts->channels > 0) {
    hresult = pbts->deckLinkOutput->DisableAudioOutput();
    if (hresult != S_OK) NAPI_THROW_ERROR("Failed to disable audio output.");
  }

  // TODO consider clearing audio callback as a matter of course

  status = napi_release_threadsafe_function(pbts->tsFn, napi_tsfn_release);
  CHECK_STATUS;

  status = napi_get_undefined(env, &value);
  CHECK_STATUS;

  // Don't hold onto PBTS after delete.
  status = napi_set_named_property(env, playback, "deckLinkOutput", value);
  CHECK_STATUS;

  pbts->deckLinkOutput->Release();
  pbts->deckLinkOutput = nullptr;

  return value;
}

napi_value rampUp(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value playback, param, value;
  napi_valuetype type;
  playbackThreadsafe* pbts;
  HRESULT hresult;
  uint32_t frameCount;

  size_t argc = 1;
  napi_value argv[1];
  status = napi_get_cb_info(env, info, &argc, argv, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  if (!pbts->enableKeying) NAPI_THROW_ERROR("Keying is not enabled for this output.");
  if (argc != 1) NAPI_THROW_ERROR("To set keying ramp up frame count, a value must be provided.");

  status = napi_typeof(env, argv[0], &type);
  CHECK_STATUS;
  if (type != napi_number) NAPI_THROW_ERROR("Keying ramp up frame count must be set with a number.");

  status = napi_get_value_uint32(env, argv[0], &frameCount);
  CHECK_STATUS;

  hresult = pbts->deckLinkKeyer->RampUp(frameCount);
  if (hresult != S_OK) NAPI_THROW_ERROR("Failed to request keyer ramp up. Call failure.");

  pbts->keyLevel = 255; // TODO eventually
  status = napi_create_int32(env, 255, &param);
  CHECK_STATUS;
  status = napi_set_named_property(env, playback, "level", param);
  CHECK_STATUS;

  status = napi_get_undefined(env, &value);
  CHECK_STATUS;
  return value;
}

napi_value rampDown(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value playback, param, value;
  napi_valuetype type;
  playbackThreadsafe* pbts;
  HRESULT hresult;
  uint32_t frameCount;

  size_t argc = 1;
  napi_value argv[1];
  status = napi_get_cb_info(env, info, &argc, argv, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  if (!pbts->enableKeying) NAPI_THROW_ERROR("Keying is not enabled for this output.");
  if (argc != 1) NAPI_THROW_ERROR("To set keying ramp down frame count, a value must be provided.");

  status = napi_typeof(env, argv[0], &type);
  CHECK_STATUS;
  if (type != napi_number) NAPI_THROW_ERROR("Keying ramp down frame count must be set with a number.");

  status = napi_get_value_uint32(env, argv[0], &frameCount);
  CHECK_STATUS;

  hresult = pbts->deckLinkKeyer->RampDown(frameCount);
  if (hresult != S_OK) NAPI_THROW_ERROR("Failed to request keyer ramp down. Call failure.");

  pbts->keyLevel = 0; // TODO eventually
  status = napi_create_int32(env, 0, &param);
  CHECK_STATUS;
  status = napi_set_named_property(env, playback, "level", param);
  CHECK_STATUS;

  status = napi_get_undefined(env, &value);
  CHECK_STATUS;
  return value;
}

napi_value setLevel(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value playback, param, value;
  napi_valuetype type;
  playbackThreadsafe* pbts;
  HRESULT hresult;
  int32_t keyLevel;

  size_t argc = 1;
  napi_value argv[1];
  status = napi_get_cb_info(env, info, &argc, argv, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  if (!pbts->enableKeying) NAPI_THROW_ERROR("Keying is not enabled for this output.");
  if (argc != 1) NAPI_THROW_ERROR("To set key level, a value must be provided.");

  status = napi_typeof(env, argv[0], &type);
  CHECK_STATUS;
  if (type != napi_number) NAPI_THROW_ERROR("Key level must be set with a number.");

  status = napi_get_value_int32(env, argv[0], &keyLevel);
  CHECK_STATUS;
  if ((keyLevel < 0) || (keyLevel > 255))
    NAPI_THROW_ERROR("Key level must be a value between 0 and 255.");

  hresult = pbts->deckLinkKeyer->SetLevel((uint8_t) keyLevel);
  if (hresult != S_OK) NAPI_THROW_ERROR("Unalbe to set key level. Call failure.");

  pbts->keyLevel = (uint8_t) keyLevel;
  status = napi_set_named_property(env, playback, "level", argv[0]);
  CHECK_STATUS;

  status = napi_get_undefined(env, &value);
  CHECK_STATUS;
  return value;
}

napi_value setTimecode(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value result, playback, param;
  napi_valuetype type;
  macadamTimecode* timecode;
  playbackThreadsafe* pbts;
  HRESULT hresult;
  char tcstr[14];
  const char* ftc;
  size_t tclen;

  size_t argc = 1;
  napi_value argv[1];
  status = napi_get_cb_info(env, info, &argc, argv, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  if (argc < 1) NAPI_THROW_ERROR("Setting timecode must be provided with at least one argument, a string value.");

  status = napi_typeof(env, argv[0], &type);
  CHECK_STATUS;
  if (type != napi_string) NAPI_THROW_ERROR("Setting timecode must be provided with a string value.");

  status = napi_get_value_string_utf8(env, argv[0], tcstr, 14, &tclen);
  CHECK_STATUS;
  hresult = parseTimecode(pbts->timeScale / 1000, tcstr, &timecode);
  if (hresult != S_OK) NAPI_THROW_ERROR("Error parsing timecode.");

  if (pbts->timecode != nullptr) { delete pbts->timecode; }
  pbts->timecode = timecode;

  pbts->timecode->formatTimecodeString(&ftc, pbts->timecode->fps > 30);
  status = napi_create_string_utf8(env, ftc, NAPI_AUTO_LENGTH, &result);
  CHECK_STATUS;
  status = napi_set_named_property(env, playback, "timecode", result);
  CHECK_STATUS;

  return result;
}

napi_value getTimecode(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value result, playback, param;
  playbackThreadsafe* pbts;
  HRESULT hresult;
  const char* ftc;

  size_t argc = 0;
  status = napi_get_cb_info(env, info, &argc, nullptr, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  if (pbts->timecode == nullptr) {
    status = napi_get_undefined(env, &result);
    CHECK_STATUS;
    return result;
  }

  hresult = pbts->timecode->formatTimecodeString(&ftc, pbts->timecode->fps > 30);
  if (hresult != S_OK) NAPI_THROW_ERROR("Error parsing timecode.");
  status = napi_create_string_utf8(env, ftc, NAPI_AUTO_LENGTH, &result);
  CHECK_STATUS;
  return result;
}

napi_value setTimecodeUserbits(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value result, playback, param;
  napi_valuetype type;
  playbackThreadsafe* pbts;
  HRESULT hresult;
  BMDTimecodeUserBits userBits;

  size_t argc = 1;
  napi_value argv[1];
  status = napi_get_cb_info(env, info, &argc, argv, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  if (argc < 1) NAPI_THROW_ERROR("Setting timecode user bits must be provided with at least one value, an integer bit field.");

  status = napi_typeof(env, argv[0], &type);
  CHECK_STATUS;
  if (type != napi_number) NAPI_THROW_ERROR("Setting timecode user bits must be provided with an 32-bit integer value.");

  status = napi_get_value_uint32(env, argv[0], &userBits);
  CHECK_STATUS;
  hresult = pbts->timecode->SetTimecodeUserBits(userBits);
  if (hresult != S_OK) NAPI_THROW_ERROR("Failed to set user bits.");

  status = napi_get_undefined(env, &result);
  CHECK_STATUS;
  return result;
}

napi_value getTimecodeUserbits(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value result, playback, param;
  playbackThreadsafe* pbts;
  HRESULT hresult;
  BMDTimecodeUserBits userBits;

  size_t argc = 0;
  napi_value argv[1];
  status = napi_get_cb_info(env, info, &argc, argv, &playback, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, playback, "deckLinkOutput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &pbts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  hresult = pbts->timecode->GetTimecodeUserBits(&userBits);
  if (hresult != S_OK) NAPI_THROW_ERROR("Cannot retrieve timecode user bits.");

  status = napi_create_uint32(env, userBits, &result);
  CHECK_STATUS;
  return result;
}

void playbackTsFnFinalize(napi_env env, void* data, void* hint) {
  // Decided to let the garbage collector clear this up.
  /* printf("Threadsafe playback finalizer called with data %p and hint %p.\n", data, hint);
  playbackThreadsafe* pbts = (playbackThreadsafe*) hint;
  delete pbts; */
}
