chromium/ash/webui/camera_app_ui/resources/js/device/mode/mode_base.ts

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

import {assertInstanceof} from '../../assert.js';
import * as error from '../../error.js';
import {CrosImageCapture} from '../../mojo/image_capture.js';
import {
  CanceledError,
  ErrorLevel,
  ErrorType,
  Facing,
  PreviewVideo,
  Resolution,
} from '../../type.js';
import {StreamConstraints} from '../stream_constraints.js';

/**
 * Base class for controlling capture sequence in different camera modes.
 */
export abstract class ModeBase {
  /**
   * Promise for ongoing capture operation.
   */
  private capture: Promise<[Promise<void>]>|null = null;

  /**
   * CrosImageCapture object to capture still photos.
   */
  protected crosImageCapture: CrosImageCapture;

  /**
   * @param video Preview video.
   * @param facing Camera facing of current mode.
   */
  constructor(
      protected video: PreviewVideo, protected readonly facing: Facing) {
    this.crosImageCapture = new CrosImageCapture(video.getVideoTrack());
  }

  /**
   * Initiates video/photo capture operation.
   *
   * @return Promise for the ongoing capture operation. The outer promise is
   *     resolved after the camere usage is finished. The inner promise is
   *     resolved after the post processing part are finished.
   */
  startCapture(): Promise<[Promise<void>]> {
    if (this.capture === null) {
      this.capture = (async () => {
        try {
          return await this.start();
        } finally {
          this.capture = null;
        }
      })();
    }
    return this.capture;
  }

  /**
   * Stops the ongoing capture operation.
   */
  async stopCapture(): Promise<void> {
    this.stop();
    try {
      await this.capture;
    } catch (e) {
      if (e instanceof CanceledError) {
        return;
      }
      error.reportError(
          ErrorType.STOP_CAPTURE_FAILURE, ErrorLevel.ERROR,
          assertInstanceof(e, Error));
    }
  }

  getImageCapture(): CrosImageCapture {
    return this.crosImageCapture;
  }

  /**
   * Adds an observer to save image metadata.
   */
  async addMetadataObserver(): Promise<void> {
    if (this.video.isExpired()) {
      return;
    }
    await this.crosImageCapture.addMetadataObserver();
  }

  /**
   * Removes the observer that saves metadata.
   */
  removeMetadataObserver(): void {
    if (!this.video.isExpired()) {
      return;
    }
    this.crosImageCapture.removeMetadataObserver();
  }

  /**
   * Clears everything when mode is not needed anymore.
   */
  async clear(): Promise<void> {
    await this.stopCapture();
  }

  /**
   * Initiates video/photo capture operation under this mode.
   */
  protected abstract start(): Promise<[Promise<void>]>;

  /**
   * Stops the ongoing capture operation under this mode.
   */
  protected stop(): void {
    // To be overridden by subclass.
  }
}

export abstract class ModeFactory {
  protected previewVideo: PreviewVideo|null = null;

  /**
   * Preview stream.
   */
  protected stream: MediaStream|null = null;

  /**
   * Camera facing of current mode.
   */
  protected facing: Facing|null = null;

  /**
   * @param constraints Constraints for preview stream.
   * @param captureResolution Capture resolution.
   */
  constructor(
      protected readonly constraints: StreamConstraints,
      protected readonly captureResolution: Resolution|null) {}

  setFacing(facing: Facing): void {
    this.facing = facing;
  }

  setPreviewVideo(previewVideo: PreviewVideo): void {
    this.previewVideo = previewVideo;
  }

  /**
   * Produces the mode capture object.
   */
  abstract produce(): ModeBase;
}