Extracting Thumbnails

This guide explains how to parse the thumbnail pack and extract all the thumbnails

The thumbnail pack is a TAR archive. To unpack it you can use the js-untar, which can process TAR archives directly on the browser.

The archive can contain several JPEG image files. The name of each file has two parts separated by a dot: <capture-time>.<source-name>.

The following file defines the processThumbnails function for downloading the thumbnail pack URL and extracting all thumbnails in it:

import untar from 'js-untar';

/**
 * Downloads a file and get content as ArrayBuffer
 *
 * @param {string} fileUrl
 * @param {AbortSignal} [signal]
 * @returns {Promise<ArrayBuffer>}
 */
export async function downloadFileToBuffer(fileUrl, signal) {
  if (!fileUrl?.startsWith('http')) {
    throw Error('Invalid URL: ' + fileUrl);
  }
  const resp = await fetch(fileUrl, { signal, cache: 'force-cache' });
  const data = await resp.arrayBuffer();
  return data.slice(0);
}

/**
 * @param {Blob} blob
 */
export function blobAsDataUrl(blob) {
  return new Promise((resolve, reject) => {
    var reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);
    reader.abort = () => reject(new Error('aborted'));
    reader.readAsDataURL(blob);
  });
}

/**
 * Downloads and process all thumbnails
 *
 * @param {Array<ThumbnailPack>} thumbnails
 * @param {AbortSignal} [signal]
 * @yields {VideoThumbnailData}
 */
export async function* processThumbnails(thumbnails, signal) {
  for (const thumbnail of thumbnails || []) {
    try {
      const buffer = await downloadFileToBuffer(thumbnail.url, signal);
      if (!buffer?.byteLength) {
        console.debug('Empty buffer for', thumbnail.url);
        continue;
      }
      const files = await untar(buffer);
      for (const file of files) {
        if (file.size !== file.buffer.byteLength) continue;
        const nameParts = file.name.split('.');
        if (nameParts.length !== 2) continue;
        const ts = Number(nameParts[0]);
        const source = nameParts[1];
        const blob = new Blob([file.buffer], { type: 'image/jpeg' });
        const src = await blobAsDataUrl(blob);
        /** @type {VideoThumbnailData} */
        const item = { source, src, ts };
        yield item;
      }
    } catch (err) {
      console.warn('Failed to process recording thumbnail', thumbnail, err);
    }
  }
}

/**
 * @typedef {object} VideoThumbnailData
 * @property {number} [ts] UNIX Timestamp
 * @property {string} [source] Input source name
 * @property {string} [src] The image url
 * @property {any} [error]
 */

Here is a sample usage of the above function for extracting all thumbnails from the API response:

async function retrieveThumbnails(saiKey, endpointId) {
	const sdk = require('api')('@smarterai/v4#bg74lc3bl801lxij');
	sdk.auth(saiKey);
	const result = await sdk.thumbnailList({endpointId: endpointId});

	const thumbnails = [];
	for await (const thumb of processThumbnails(result.thumbnailPacks)) {
	  thumbnails.push(thumb);
	}
	return thumbnails;
}