chromium/ash/webui/camera_app_ui/resources/js/type.ts

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

import {assertExists, assertInstanceof} from './assert.js';
import {WaitableEvent} from './waitable_event.js';

/**
 * Photo or video resolution.
 */
export class Resolution {
  readonly width: number;

  readonly height: number;

  constructor();
  constructor(width: number, height: number);
  constructor(width?: number, height?: number) {
    this.width = width ?? 0;
    this.height = height ?? -1;
  }

  /**
   * @return Total pixel number.
   */
  get area(): number {
    return this.width * this.height;
  }

  /**
   * Aspect ratio calculates from width divided by height.
   */
  get aspectRatio(): number {
    // Special aspect ratio mapping rule, see http://b/147986763.
    if (this.width === 848 && this.height === 480) {
      return (new Resolution(16, 9)).aspectRatio;
    }
    // Approximate to 4 decimal places to prevent precision error during
    // comparing.
    return parseFloat((this.width / this.height).toFixed(4));
  }

  /**
   * @return The amount of mega pixels to 1 decimal place.
   */
  get mp(): number {
    return parseFloat((this.area / 1000000).toFixed(1));
  }

  /**
   * Compares width/height of resolutions, see if they are equal or not.
   *
   * @param resolution Resolution to be compared with.
   */
  equals(resolution: Resolution|null): boolean {
    if (resolution === null) {
      return false;
    }
    return this.width === resolution.width && this.height === resolution.height;
  }

  /**
   * Compares width/height of resolutions, see if they are equal or not. It also
   * returns true if the resolution is rotated.
   *
   * @param resolution Resolution to be compared with.
   */
  equalsWithRotation(resolution: Resolution): boolean {
    return (this.width === resolution.width &&
            this.height === resolution.height) ||
        (this.width === resolution.height && this.height === resolution.width);
  }

  /**
   * Compares aspect ratio of resolutions, see if they are equal or not.
   *
   * @param resolution Resolution to be compared with.
   */
  aspectRatioEquals(resolution: Resolution): boolean {
    return this.aspectRatio === resolution.aspectRatio;
  }

  /**
   * Create Resolution object from string.
   */
  static fromString(s: string): Resolution {
    const [width, height] = s.split('x').map((x) => Number(x));
    return new Resolution(width, height);
  }

  toString(): string {
    return `${this.width}x${this.height}`;
  }
}

/**
 * Types of common mime types.
 */
export enum MimeType {
  GIF = 'image/gif',
  JPEG = 'image/jpeg',
  JSON = 'application/json',
  MP4 = 'video/mp4',
  PDF = 'application/pdf',
}

/**
 * Capture modes.
 */
export enum Mode {
  PHOTO = 'photo',
  PORTRAIT = 'portrait',
  SCAN = 'scan',
  VIDEO = 'video',
}

/**
 * Camera facings.
 */
export enum Facing {
  ENVIRONMENT = 'environment',
  EXTERNAL = 'external',
  USER = 'user',
  // VIRTUAL_{facing} is for labeling video device for configuring extra stream
  // from corresponding {facing} video device.
  VIRTUAL_ENV = 'virtual_environment',
  VIRTUAL_EXT = 'virtual_external',
  VIRTUAL_USER = 'virtual_user',
}

export enum ViewName {
  CAMERA = 'view-camera',
  DOCUMENT_REVIEW = 'view-document-review',
  EXPERT_SETTINGS = 'view-expert-settings',
  FLASH = 'view-flash',
  LOW_STORAGE_DIALOG = 'view-low-storage-dialog',
  OPTION_PANEL = 'view-option-panel',
  PHOTO_ASPECT_RATIO_SETTINGS = 'view-photo-aspect-ratio-settings',
  PHOTO_RESOLUTION_SETTINGS = 'view-photo-resolution-settings',
  PTZ_PANEL = 'view-ptz-panel',
  REVIEW = 'view-review',
  SETTINGS = 'view-settings',
  SPLASH = 'view-splash',
  SUPER_RES_INTRO_DIALOG = 'view-super-res-intro-dialog',
  VIDEO_RESOLUTION_SETTINGS = 'view-video-resolution-settings',
  WARNING = 'view-warning',
}

export enum VideoType {
  GIF = 'gif',
  MP4 = 'mp4',
}

export enum PhotoResolutionLevel {
  FULL = 'full',
  MEDIUM = 'medium',
  UNKNOWN = 'unknown',
}

/* eslint-disable cca/string-enum-order */
export enum VideoResolutionLevel {
  FOUR_K = '4K',
  QUAD_HD = 'Quad HD',
  FULL_HD = 'Full HD',
  HD = 'HD',
  THREE_SIXTY_P = '360p',

  FULL = 'full',
  MEDIUM = 'medium',

  UNKNOWN = 'unknown',
}
/* eslint-enable cca/string-enum-order */

export enum AspectRatioSet {
  RATIO_4_3 = 1.3333,
  RATIO_16_9 = 1.7778,
  RATIO_OTHER = 0.0000,
  RATIO_SQUARE = 1.0000,
}

export enum Rotation {
  ANGLE_0 = 0,
  ANGLE_90 = 90,
  ANGLE_180 = 180,
  ANGLE_270 = 270,
}
// `ROTATION_ORDER` is used for document scanning fix mode to show/crop images.
// The length must be fixed at 4.
export const ROTATION_ORDER =
    Object.values(Rotation).filter((r): r is Rotation => typeof r === 'number');

export interface VideoConfig {
  width: number;
  height: number;
  maxFps: number;
}

export interface FpsRange {
  minFps: number;
  maxFps: number;
}

/**
 * A list of resolutions.
 */
export type ResolutionList = Resolution[];

/**
 * Map of all available resolution to its maximal supported capture fps. The key
 * of the map is the resolution and the corresponding value is the maximal
 * capture fps under that resolution.
 */
export type MaxFpsInfo = Record<string, number>;

/**
 * List of supported capture fps ranges.
 */
export type FpsRangeList = FpsRange[];

/**
 * Type for performance event.
 */
export enum PerfEvent {
  // In all modes, the duration between the camera switch button being clicked
  // and the preview stream being updated.
  CAMERA_SWITCHING = 'camera-switching',
  // In Doc Scan mode, the duration between a shutter sound playing and the
  // image appearing in the review page.
  DOCUMENT_CAPTURE_POST_PROCESSING = 'document-capture-post-processing',
  // In Doc Scan mode, the duration between "Save as PDF" button being clicked
  // and the review page closing.
  DOCUMENT_PDF_SAVING = 'document-pdf-saving',
  // In GIF mode, the duration between GIF recording stopping and the temporal
  // GIF appearing in the review page.
  GIF_CAPTURE_POST_PROCESSING = 'gif-capture-post-processing',
  // In GIF mode, the duration between "Save" button being clicked and the
  // result file saving finished.
  GIF_CAPTURE_SAVING = 'gif-capture-saving',
  // Used for testing. The duration between app window being created and the app
  // being launched.
  LAUNCHING_FROM_LAUNCH_APP_COLD = 'launching-from-launch-app-cold',
  // Used for testing. The duration between app window being created and the app
  // being launched.
  LAUNCHING_FROM_LAUNCH_APP_WARM = 'launching-from-launch-app-warm',
  // The duration between CCA window being created and the preview stream
  // appearing.
  LAUNCHING_FROM_WINDOW_CREATION = 'launching-from-window-creation',
  // In all modes, the duration between the mode switch button being clicked and
  // the preview stream being updated.
  MODE_SWITCHING = 'mode-switching',
  // In Photo mode, the duration between a snapshot of the preview being scanned
  // by OCR(automatically, with 500ms intervals) and the scanned result
  // appearing in the preview. The result might not be shown if it is empty or
  // if other scanners have detected results.
  OCR_SCANNING = 'ocr-scanning',
  // In Photo mode, the duration between a shutter sound playing and the
  // result file saving finished.
  PHOTO_CAPTURE_POST_PROCESSING_SAVING = 'photo-capture-post-processing-saving',
  // In Photo, Doc Scan and Portrait mode, the duration between the shutter
  // button being clicked or a timer expiring and a shutter sound playing.
  PHOTO_CAPTURE_SHUTTER = 'photo-capture-shutter',
  // In Portrait mode, the duration between a shutter sound playing and the
  // two result files saving finished.
  PORTRAIT_MODE_CAPTURE_POST_PROCESSING_SAVING =
      'portrait-mode-capture-post-processing-saving',
  // In Video mode, the duration between the video snapshot button being clicked
  // and the result file saving finished.
  SNAPSHOT_TAKING = 'snapshot-taking',
  // In Time lapse mode, the duration between a shutter sound playing and
  // the result file saving finished.
  TIME_LAPSE_CAPTURE_POST_PROCESSING_SAVING =
      'time-lapse-capture-post-processing-saving',
  // In Video mode, the duration between the shutter button being clicked to
  // stop recording and the result file saving finished.
  VIDEO_CAPTURE_POST_PROCESSING_SAVING = 'video-capture-post-processing-saving',
}

export enum Pressure {
  NOMINAL,
  FAIR,
  SERIOUS,
  CRITICAL,
}

export interface ImageBlob {
  blob: Blob;
  resolution: Resolution;
}

// The key-value pair of the entries in metadata are stored as key-value of an
// |Object| type
export type Metadata = Record<string, unknown>;

export interface PerfInformation {
  hasError?: boolean;
  resolution?: Resolution;
  facing?: Facing;
  pageCount?: number;  // Only for DOCUMENT_PDF_SAVING
  pressure?: Pressure;
}

export interface PerfEntry {
  event: PerfEvent;
  duration: number;
  perfInfo: PerfInformation;
}

export interface VideoTrackSettings {
  deviceId: string;
  width: number;
  height: number;
  frameRate: number;
}

/**
 * Gets video track settings from a video track.
 *
 * This asserts that all property that should exists on video track settings
 * (.width, .height, .deviceId, .frameRate) all exists and narrow the type.
 */
export function getVideoTrackSettings(videoTrack: MediaStreamTrack):
    VideoTrackSettings {
  // TODO(pihsun): The type from TypeScript lib.dom.d.ts is wrong on Chrome and
  // the .deviceId should never be undefined. Try to override that when we have
  // newer TypeScript compiler (>= 4.5) that supports overriding lib.dom.d.ts.
  const {deviceId, width, height, frameRate} = videoTrack.getSettings();
  return {
    deviceId: assertExists(deviceId),
    width: assertExists(width),
    height: assertExists(height),
    frameRate: assertExists(frameRate),
  };
}

/**
 * A proxy to get preview video or stream with notification of when the video
 * stream is expired.
 */
export class PreviewVideo {
  constructor(
      readonly video: HTMLVideoElement, readonly onExpired: WaitableEvent) {}

  getStream(): MediaStream {
    return assertInstanceof(this.video.srcObject, MediaStream);
  }

  getVideoTrack(): MediaStreamTrack {
    return this.getStream().getVideoTracks()[0];
  }

  getVideoSettings(): VideoTrackSettings {
    return getVideoTrackSettings(this.getVideoTrack());
  }

  isExpired(): boolean {
    return this.onExpired.isSignaled();
  }
}

/**
 * Error reported in testing run.
 */
export interface ErrorInfo {
  type: ErrorType;
  level: ErrorLevel;
  stack: string;
  time: number;
  name: string;
}

/**
 * Types of error used in ERROR metrics.
 */
export enum ErrorType {
  BIG_BUFFER_FAILURE = 'big-buffer-failure',
  BROKEN_THUMBNAIL = 'broken-thumbnail',
  CHECK_COVER_FAILURE = 'check-cover-failed',
  DEVICE_INFO_UPDATE_FAILURE = 'device-info-update-failure',
  DEVICE_NOT_EXIST = 'device-not-exist',
  EMPTY_FILE = 'empty-file',
  FILE_SYSTEM_FAILURE = 'file-system-failure',
  FRAME_ROTATION_NOT_DISABLED = 'frame-rotation-not-disabled',
  HANDLE_CAMERA_RESULT_FAILURE = 'handle-camera-result-failure',
  INVALID_REVIEW_UI_STATE = 'invalid-review-ui-state',
  METADATA_MAPPING_FAILURE = 'metadata-mapping-failure',
  MULTI_WINDOW_HANDLING_FAILURE = 'multi-window-handling-failure',
  MULTIPLE_STREAMS_FAILURE = 'multiple-streams-failure',
  NO_AVAILABLE_LEVEL = 'no-available-level',
  PERF_METRICS_FAILURE = 'perf-metrics-failure',
  PRELOAD_IMAGE_FAILURE = 'preload-image-failure',
  RESUME_CAMERA_FAILURE = 'resume-camera-failure',
  RESUME_PAUSE_FAILURE = 'resume-pause-failure',
  SET_FPS_RANGE_FAILURE = 'set-fps-range-failure',
  START_CAMERA_FAILURE = 'start-camera-failure',
  START_CAPTURE_FAILURE = 'start-capture-failure',
  STOP_CAPTURE_FAILURE = 'stop-capture-failure',
  SUSPEND_CAMERA_FAILURE = 'suspend-camera-failure',
  UNCAUGHT_ERROR = 'uncaught-error',
  UNCAUGHT_PROMISE = 'uncaught-promise',
  UNSAFE_INTEGER = 'unsafe-integer',
  UNSUPPORTED_PROTOCOL = 'unsupported-protocol',
}

/**
 * Error level used in ERROR metrics.
 */
export enum ErrorLevel {
  ERROR = 'ERROR',
  WARNING = 'WARNING',
}

/**
 * Throws when a method is not implemented.
 */
export class NotImplementedError extends Error {
  constructor(message = 'Method is not implemented') {
    super(message);
    this.name = this.constructor.name;
  }
}

/**
 * Throws when an action is canceled.
 */
export class CanceledError extends Error {
  constructor(message = 'The action is canceled') {
    super(message);
    this.name = this.constructor.name;
  }
}

/**
 * Throws when an element fails to load a source.
 */
export class LoadError extends Error {
  constructor(message = 'Source failed to load') {
    super(message);
    this.name = this.constructor.name;
  }
}

/**
 * Throws when an media element fails to play.
 */
export class PlayError extends Error {
  constructor(message = 'Media element failed to play') {
    super(message);
    this.name = this.constructor.name;
  }
}

/**
 * Throws when an media element play a malformed file.
 */
export class PlayMalformedError extends Error {
  constructor(message = 'Media element failed to play a malformed file') {
    super(message);
    this.name = this.constructor.name;
  }
}

/**
 * Throws when the data to generate thumbnail is totally empty.
 */
export class EmptyThumbnailError extends Error {
  constructor(message = 'The thumbnail is empty') {
    super(message);
    this.name = this.constructor.name;
  }
}

export class LowStorageError extends Error {
  constructor() {
    const message = 'Cannot start recording due to low storage.';
    super(message);
    this.name = this.constructor.name;
  }
}

/**
 * Throws when the recording is ended with no chunk returned.
 */
export class NoChunkError extends Error {
  constructor(message = 'No chunk is received during recording session') {
    super(message);
    this.name = this.constructor.name;
  }
}

/**
 * Throws when the GIF or time lapse recording is ended with no frame captured.
 */
export class NoFrameError extends Error {
  constructor(message = 'No frames captured during the recording') {
    super(message);
    this.name = this.constructor.name;
  }
}

/**
 * Throws when the portrait mode fails to detect a human face.
 */
export class PortraitErrorNoFaceDetected extends Error {
  constructor(message = 'No human face detected in the scene') {
    super(message);
    this.name = this.constructor.name;
  }
}

/**
 * Throws when the camera is suspended while camera effects are ongoing.
 */
export class CameraSuspendError extends Error {
  constructor(message = 'Camera suspended') {
    super(message);
    this.name = this.constructor.name;
  }
}

export class NoCameraError extends Error {
  constructor(message = 'No available cameras') {
    super(message);
    this.name = this.constructor.name;
  }
}

/**
 * Types of local storage key.
 */
export enum LocalStorageKey {
  CUSTOM_VIDEO_PARAMETERS = 'customVideoParameters',
  ENABLE_FPS_PICKER = 'enableFPSPicker',
  ENABLE_FULL_SIZED_VIDEO_SNAPSHOT = 'enableFullSizedVideoSnapshot',
  ENABLE_PREVIEW_OCR = 'enablePreviewOcr',
  ENABLE_PTZ_FOR_BUILTIN = 'enablePTZForBuiltin',
  EXPERT_MODE = 'expert',
  FIRST_OPENING = 'firstOpening',
  GA_ID_REFRESH_TIME = 'gaIdRefreshTime',
  GA_USER_ID = 'google-analytics.analytics.user-id',
  GA4_CLIENT_ID = 'ga4ClientId',
  MIRRORING_TOGGLES = 'mirroringToggles',
  PREF_DEVICE_PHOTO_ASPECT_RATIO_SET = 'devicePhotoAspectRatioSet',
  PREF_DEVICE_PHOTO_RESOLUTION_EXPERT = 'devicePhotoResolutionExpert',
  PREF_DEVICE_PHOTO_RESOLUTION_LEVEL = 'devicePhotoResolutionLevel',
  PREF_DEVICE_VIDEO_RESOLUTION_EXPERT = 'deviceVideoResolutionExpert',
  PREF_DEVICE_VIDEO_RESOLUTION_FPS = 'deviceVideoResolutionFps',
  PREF_DEVICE_VIDEO_RESOLUTION_LEVEL = 'deviceVideoResolutionLevel',
  PREVIEW_OCR_TOAST_SHOWN = 'previewOcrToastShown',
  PRINT_PERFORMANCE_LOGS = 'printPerformanceLogs',
  SAVE_METADATA = 'saveMetadata',
  SHOW_ALL_RESOLUTIONS = 'showAllResolutions',
  SHOW_METADATA = 'showMetadata',
  SUPER_RES_DIALOG_SHOWN = 'superResDialogShown',
  TOGGLE_MIC = 'toggleMic',
}

/**
 * Type of low storage dialog.
 */
export enum LowStorageDialogType {
  AUTO_STOP = 'auto-stop',
  CANNOT_START = 'cannot-start',
}

/**
 * A rectangle representing a crop region with size (width, height) and having
 * the top-left coordinate at (x, y).
 */
export interface CropRegionRect {
  height: number;
  width: number;
  x: number;
  y: number;
}

export type Awaitable<T> = PromiseLike<T>|T;