chromium/ash/webui/projector_app/resources/app/untrusted/launch.js

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

import {assert} from '//resources/ash/common/assert.js';

const VIDEO_EXTENSIONS = [
  'webm',
];

const VIDEO_MIME_TYPES = [
  'video/webm',
];

/**
 * Callback to pass the launched file to application.
 * Callback is set when installLaunchHelper is called once
 * when communication is initialized in untrusted_app_comm_factory.js.
 * @type {Function<string, ?File, ?DOMException>}
 */
let sendVideoFile;

/**
 * Installs the handler for launch files, if window.launchQueue is available.
 * @param {!Function<string, ?File, ?DOMException>} callback
 */
export function installLaunchHandler(callback) {
  if (!window.launchQueue) {
    console.error('FileHandling API missing.');
    return;
  }

  sendVideoFile = callback;

  window.launchQueue.setConsumer(wrappedLaunchConsumer);
}

/**
 * Wrapper for the launch consumer to ensure it doesn't return a Promise, nor
 * propagate exceptions. Tests will want to target `launchConsumer` directly so
 * that they can properly await launch results.
 * @param {?LaunchParams} params
 */
function wrappedLaunchConsumer(params) {
  launchConsumer(params).catch(e => {
    console.error(e, '(launch aborted)');
  });
}

/**
 * The launchQueue consumer. This returns a promise to help tests, but the file
 * handling API will ignore it.
 * @param {?LaunchParams} params
 * @return {!Promise<undefined>}
 */
async function launchConsumer(params) {
  if (!params || !params.files || params.files.length !== 2) {
    console.error('Invalid launch (missing files): ', params);
    return;
  }
  // Caution! This first param is a file handler with the file id as its name,
  // not a real file. Don't try to access it on disk. Calling getFile() on it
  // will throw a DOM exception.
  const fileId = assert(params.files[0]).name;
  const fileHandle = assert(params.files[1]);
  try {
    await launchVideoFile(fileId, fileHandle);
  } catch (e) {
    console.error(e, '(launchFile aborted)');
  }
}

/**
 * Sends the provided video file to the.app via the
 * sendVideoFile callback.
 * @param {string} fileId
 * @param {!FileSystemHandle} handle
 */
async function launchVideoFile(fileId, handle) {
  try {
    const file = await getVideoFileFromHandle(handle);
    sendVideoFile(fileId, file, /*error=*/ null);
  } catch (/** @type {!DOMException} */ e) {
    console.error(`${handle.name}: ${e.message}`);
    sendVideoFile(fileId, /*file=*/ null, /*error=*/ e);
  }
}

/**
 * Gets a video file from a handle received via the fileHandling API. Only
 * handles expected to be video files should be passed to this function. Throws
 * a DOMException if opening the file fails - usually because the handle is
 * stale.
 * @param {?FileSystemHandle} fileSystemHandle
 * @return {!Promise<!File>}
 */
async function getVideoFileFromHandle(fileSystemHandle) {
  if (!fileSystemHandle || fileSystemHandle.kind !== 'file') {
    // Invent our own exception for this corner case. It might happen if a file
    // is deleted and replaced with a directory with the same name.
    throw new DOMException('Not a file.', 'NotAFile');
  }
  const handle = /** @type {!FileSystemFileHandle} */ (fileSystemHandle);
  const video = await handle.getFile();  // Note: throws DOMException.
  const extension = video.name.split('.').pop();
  if (!VIDEO_MIME_TYPES.includes(video.type) ||
      !VIDEO_EXTENSIONS.includes(extension)) {
    throw new DOMException('Not a video.', 'NotAVideo');
  }
  return video;
}