Common.js

/**
 * @summary Default URL for API requests.
 * @constant
 * @type {string}
 */
export const DEFAULT_URL = 'https://local.capturebridge.net:9001'

function CaptureBridgeSetDefaultURL () {
  // Prefer <meta name="capturebridge:url" content="proto://fqdn" /> if set
  if (typeof document === 'object') {
    const url = document.querySelector('meta[name="capturebridge:url"]')
    if (url && url.content) {
      return url.content
    }
  }

  // Prefer CAPTURE_BRIDGE_URL if set on the global window object (Browser)
  if (typeof window === 'object' && window?.CAPTURE_BRIDGE_URL) {
    return window.CAPTURE_BRIDGE_URL
  }

  // Prefer CAPTURE_BRIDGE_URL if set on in the environment (Node.js)
  if (typeof process === 'object' && process?.env?.CAPTURE_BRIDGE_URL) {
    return process?.env?.CAPTURE_BRIDGE_URL
  }

  // Failing an explicit CAPTURE_BRIDGE_URL, use the URL in which the SDK
  // is being served from (Browser)
  if (typeof window === 'object' && window?.location?.origin) {
    return window.location.origin
  }

  // If all else fails, use the default vanity URL
  return DEFAULT_URL
}

/**
 * @summary Base URL for all HTTP API requests.
 *
 * Due to the various environments in which the SDK can be loaded and the
 * different ways that a Capture Bridge instance may be configured. The
 * `BASE_URL` variable used for all default HTTP API request can be set in
 * several ways:
 *
 * When running the SDK within a web browser the following locations are
 * checked, in order:
 * 1. A {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta|meta} tag containing a url: `<meta name="capturebridge:url" content="proto://fqdn:port" />`
 * 2. In the global {@link https://developer.mozilla.org/en-US/docs/Web/API/Window|Window} object: `window.CAPTURE_BRIDGE_URL`
 * 3. In the {@link https://developer.mozilla.org/en-US/docs/Web/API/Location/origin|origin} in which the SDK is running: `window.location.origin`
 * 4. The `DEFAULT_URL` constant from `Common.js`
 *
 * When running the SDK within Node.js the following locations are
 * checked, in order:
 * 1. In a `CAPTURE_BRIDGE_URL` {@link https://nodejs.org/api/process.html#processenv|environment variable}
 * 2. The `DEFAULT_URL` constant from `Common.js`
 *
 * **Note** that each constructor also accepts a `baseUrl` parameter that will override
 * any value for `BASE_URL` previously set.
 *
 * @constant
 * @type {string}
 * @default {@link https://local.capturebridge.net:9001}
 */
export const BASE_URL = CaptureBridgeSetDefaultURL()

/**
 * Common {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API|Fetch}
 * options for making
 * {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST|POST}
 * requests to the API.
 * @constant
 * @type {object}
 */
export const POST = {
  mode: 'cors',
  method: 'post'
}

/**
 * @summary
 * Common {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API|Fetch}
 * options for making
 * {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE|DELETE}
 * requests to the API.
 * @constant
 * @type {object}
 */
export const DELETE = {
  mode: 'cors',
  method: 'delete'
}

/**
 * @summary Default options for stream operations
 * @constant
 * @property {number?} rotate - Angle, in degrees to rotate the stream. Must be
 * a value from -359 to 359. A value outside of that range, or a 0 will perform
 * no rotation.
 * @typedef {object} StreamOptions
 */
export const DEFAULT_STREAM_OPT = {
  rotate: null
}

/**
 * @typedef {string} CaptureResponseKind
 * @summary Specifies the desired return type for a captured object. Valid
 * values are `objecturl`, `blob`, `buffer`, `base64`, or `dataurl`.
 *
 *  - `objecturl` will load the captured object into an
 * {@link https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static|ObjectURL}
 * that can be set directly on an img tag's src attribute. Note that these URLs
 * should be {@link https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL_static|revoked}
 * when they are no longer needed.
 *
 * - `buffer` will load the image into an
 * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer|ArrayBuffer}
 *
 * - `blob` will return a
 * {@link https://developer.mozilla.org/en-US/docs/Web/API/Blob|Blob} that can
 * be used with a
 * {@link https://developer.mozilla.org/en-US/docs/Web/API/FileReader|FileReader}.
 *
 * - `base64` will return the image contents as a
 * {@link https://en.wikipedia.org/wiki/Base64|Base64} encoded string that is
 * suitable for use with JSON serialization.
 *
 * - `dataurl` will return a
 * {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs|Data URL}
 * that can be set on an img tag's src attribute or loaded as a document
 * location.
 *
 */

/**
 * @typedef {string} CaptureResponseFormat
 * @summary The image format to be returned.
 *
 * Valid values:
 *
 *  - `jpg`
 *  - `tiff`
 *  - `gif`
 *  - `png`
 *
 * If not provided, a plugin or device-specific format will be returned.
 *
 * **Note:**
 *  - Some plugins have configuration options for specifying the return format,
 * such configuration options should be used instead of this parameter to
 * prevent unecessary transcoding.
 * - Web browsers are not typically capable of displaying TIFF images.
 *
 */

/**
 * @summary Default options for capture operations.
 * @constant
 * @property {number?} rotate - Angle, in degrees to rotate the captured object.
 * Must be a value from -359 to 359. A value of 0 will perform no rotation.
 * @property {boolean?} base64 - Return the captured object as a base64 encoded
 * string. The default behavior is to return the image as binary data.
 * @property {CaptureResponseKind} kind - Specifies how to return the captured
 * object. Some SDK methods that are designed to only return a particular type,
 * such as {@link Device#frameAsBase64}, ignore this parameter. This parameter
 * is also not sent in API requests, it is used internally by the SDK for
 * constructing the appropriate response type.
 * @property {CaptureResponseFormat} format - The image format to be returned.
 *
 * @typedef {object} CaptureOptions
 */
export const DEFAULT_CAPTURE_OPT = {
  rotate: null,
  base64: false,
  format: null,
  kind: 'objecturl'
}

/**
 * @summary `data:` prefix for JPEG
 * {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs|Data URLs}
 * @constant
 * @type {string}
 */
export const DATAURL_JPEG = 'data:image/jpeg;base64,'

/**
 * @summary `data:` prefix for PNG
 * {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs|Data URLs}
 * @constant
 * @type {string}
 */
export const DATAURL_PNG = 'data:image/png;base64,'

/**
 * @summary `data:` prefix for GIF
 * {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs|Data URLs}
 * @constant
 * @type {string}
 */
export const DATAURL_GIF = 'data:image/gif;base64,'

/**
 * @summary `data:` prefix for JSON
 * {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs|Data URLs}
 * @constant
 * @type {string}
 */
export const DATAURL_JSON = 'data:application/json;base64,'

/**
 * @summary For a given base64 string, convert it to the appropriate Data URL
 * @constant
 * @type {string}
 */
export const base64ToDataURL = base64 => {
  const magicBytes = atob(base64.slice(0, 6))
  const byteArray = new Uint8Array(magicBytes.length)
  for (let i = 0; i < magicBytes.length; i++) {
    byteArray[i] = magicBytes.charCodeAt(i)
  }

  // Check for JPEG magic number (FF D8 FF)
  if (byteArray[0] === 0xFF && byteArray[1] === 0xD8 && byteArray[2] === 0xFF) {
    return DATAURL_JPEG + base64
  }

  // Check for PNG magic number (89 50 4E 47)
  if (byteArray[0] === 0x89 && byteArray[1] === 0x50 && byteArray[2] === 0x4E && byteArray[3] === 0x47) {
    return DATAURL_PNG + base64
  }

  // Check for GIF magic number (47 49 46 38)
  if (byteArray[0] === 0x47 && byteArray[1] === 0x49 && byteArray[2] === 0x46 && byteArray[3] === 0x38) {
    return DATAURL_GIF + base64
  }

  throw new Error('Unknown image format provided in Base64 string')
}

/**
 * @summary Strip the data URL identifier from a string
 * @constant
 * @type {string}
 */
export const stripDataUrl = dataUrl => {
  const dataUrls = [DATAURL_JPEG, DATAURL_PNG, DATAURL_GIF, DATAURL_JSON]
  for (const i in dataUrls) {
    const prefix = dataUrls[i]
    if (dataUrl.startsWith(prefix)) {
      return dataUrl.replace(prefix, '')
    }
  }
  throw new Error(`Data URL prefix must be one of: ${dataUrls.join(',')}`)
}

/**
 * An Async/Await version of
 * {@link https://developer.mozilla.org/en-US/docs/Web/API/setTimeout|setTimeout}
 * @private
 */
export const timeout = millis => {
  return new Promise(resolve => setTimeout(resolve, millis))
}

/**
 * Convert a Blob to a Buffer
 * @private
 */
export const blobToBuffer = blob => {
  return new Promise(resolve => {
    const reader = new window.FileReader()
    reader.onload = event => resolve(event.target.result)
    reader.readAsArrayBuffer(blob)
  })
}

/**
 * Convert a Blob to a Base64 String
 * @private
 */
export const blobToBase64 = blob => {
  return new Promise(resolve => {
    const reader = new window.FileReader()
    reader.onload = event => resolve(event.target.result.split(',')[1])
    reader.readAsDataURL(blob)
  })
}

/**
 * Helper function to throw if an http response was a error.
 * @private
 */
export const checkResponse = async (response) => {
  if (!response.ok) {
    const error = await response.json()
    const msg = error.message || error.summary || error.code || 'Unexpected error'
    const ex = new Error(msg)
    ex.code = error.code || 500
    ex.summary = error.summary || error.error || 'Unexpected error'
    throw ex
  }
}

/**
 * Helper function to convert a key/value map to a string, removing any items
 * that are null or undefined (instead of converting them to a string which
 * URLSearchParams().toString() will do.)
 * @private
 */
export const urlParamsToString = (params) => {
  const filtered = {}
  Object.keys(params).forEach(key => {
    if (params[key] !== null && params[key] !== undefined) {
      filtered[key] = params[key]
    }
  })
  return new URLSearchParams(filtered).toString()
}

/**
 * Allows passing in a string for the response type of capture methods
 * @private
 */
export const DEPRECATION_01 = true

/**
 * Allows passing in a boolean to clear the screen for display methods
 * @private
 */
export const DEPRECATION_02 = true