src/backgroundfilter/BackgroundFilterVideoFrameProcessor.ts (116 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { loadWorker } from '../../libs/voicefocus/loader';
import { supportsWASM, supportsWorker } from '../../libs/voicefocus/support';
import { AssetSpec } from '../../libs/voicefocus/voicefocus';
import ModelSpecBuilder from '../backgroundblurprocessor/ModelSpecBuilder';
import BackgroundFilterPaths from '../backgroundfilter/BackgroundFilterPaths';
import DefaultBrowserBehavior from '../browserbehavior/DefaultBrowserBehavior';
import Logger from '../logger/Logger';
import ModelSpec from '../modelspec/ModelSpec';
import Versioning from '../versioning/Versioning';
import BackgroundFilterOptions from './BackgroundFilterOptions';
import BackgroundFilterSpec from './BackgroundFilterSpec';
/** @internal */
const CREATE_DEFAULT_MODEL_SPEC = (): ModelSpec =>
ModelSpecBuilder.builder().withSelfieSegmentationDefaults().build();
/** @internal */
const DEFAULT_CDN = 'https://static.sdkassets.chime.aws';
/** @internal */
const DEFAULT_PATHS: BackgroundFilterPaths = {
worker: `${DEFAULT_CDN}/bgblur/workers/worker.js`,
wasm: `${DEFAULT_CDN}/bgblur/wasm/_cwt-wasm.wasm`,
simd: `${DEFAULT_CDN}/bgblur/wasm/_cwt-wasm-simd.wasm`,
};
export default class BackgroundFilterVideoFrameProcessor {
/**
* Based on the SDK version, return an asset group.
*
* @returns the default asset spec, based on the SDK version.
*/
private static defaultAssetSpec(): AssetSpec {
const version = Versioning.sdkVersionSemVer;
return {
assetGroup: `sdk-${version.major}.${version.minor}`,
};
}
/**
* Set the given parameters to the url. Existing parameters in the url are preserved.
* If duplicate parameters exist, they are overwritten, so it's safe to call this method multiple
* times on the same url.
*
* @param url the initial url, can include query parameters
* @param queryParams the query parameters to set
* @returns a new url with the given query parameters.
*/
private static createUrlWithParams(url: string, queryParams: { [key: string]: string }): string {
const u = new URL(url);
const keys = Object.keys(queryParams);
for (const key of keys) {
if (queryParams[key] !== undefined) {
u.searchParams.set(key, queryParams[key]);
}
}
return u.toString();
}
/**
* Based on the spec that is passed in set defaults for spec
* @param spec the spec that was passed in
* @returns An updated spec with defaults set
*/
protected static resolveSpec(spec?: BackgroundFilterSpec): BackgroundFilterSpec {
const {
paths = DEFAULT_PATHS,
model = CREATE_DEFAULT_MODEL_SPEC(),
assetGroup = this.defaultAssetSpec().assetGroup,
revisionID = this.defaultAssetSpec().revisionID,
} = spec || {};
const params = {
assetGroup,
revisionID,
sdk: encodeURIComponent(Versioning.sdkVersion),
ua: encodeURIComponent(Versioning.sdkUserAgentLowResolution),
};
paths.worker = this.createUrlWithParams(paths.worker, params);
paths.wasm = this.createUrlWithParams(paths.wasm, params);
paths.simd = this.createUrlWithParams(paths.simd, params);
model.path = this.createUrlWithParams(model.path, params);
return {
paths,
model,
assetGroup,
revisionID,
};
}
/**
* Based on the options that are passed in set defaults for options
* @param options the options that are passed in
* @returns An updated set of options with defaults set
*/
protected static resolveOptions(options?: BackgroundFilterOptions): BackgroundFilterOptions {
if (!options.reportingPeriodMillis) {
options.reportingPeriodMillis = 1000;
}
const DEFAULT_FILTER_CPU_UTILIZATION = 30;
if (!options.filterCPUUtilization) {
options.filterCPUUtilization = DEFAULT_FILTER_CPU_UTILIZATION;
} else if (options.filterCPUUtilization < 0 || options.filterCPUUtilization > 100) {
options.logger.warn(
`filterCPUUtilization must be set to a range between 0 and 100 percent. Falling back to default of ${DEFAULT_FILTER_CPU_UTILIZATION} percent`
);
options.filterCPUUtilization = DEFAULT_FILTER_CPU_UTILIZATION;
}
return options;
}
/**
* This method will detect the environment in which it is being used and determine if background
* blur/replacement can be used.
* @param spec The {@link BackgroundBlurSpec} spec that will be used to initialize assets
* @param options options such as logger
* @returns a boolean promise that will resolve to true if supported and false if not
*/
static isSupported(spec?: BackgroundFilterSpec, options?: { logger?: Logger }): Promise<boolean> {
const { logger } = options;
// could not figure out how to remove globalThis to test failure case
/* istanbul ignore next */
if (typeof globalThis === 'undefined') {
logger.info('Browser does not have globalThis.');
return Promise.resolve(false);
}
const browser = new DefaultBrowserBehavior();
if (!browser.supportsBackgroundFilter()) {
logger.info('Browser is not supported.');
return Promise.resolve(false);
}
if (!supportsWASM(globalThis, logger)) {
logger.info('Browser does not support WASM.');
return Promise.resolve(false);
}
return this.supportsBackgroundFilter(globalThis, spec, logger);
}
private static async supportsBackgroundFilter(
/* istanbul ignore next */
scope: { Worker?: typeof Worker } = globalThis,
spec?: BackgroundFilterSpec,
logger?: Logger
): Promise<boolean> {
if (!supportsWorker(scope, logger)) {
logger.info('Browser does not support web workers.');
return false;
}
// Use the actual worker path -- it's only 20KB, and it'll get the cache warm.
const workerURL = spec.paths.worker;
try {
const worker = await loadWorker(workerURL, 'BackgroundFilterWorker', {}, null);
try {
worker.terminate();
} catch (e) {
logger.info(`Failed to terminate worker. ${e.message}`);
}
return true;
} catch (e) {
logger.info(`Failed to fetch and instantiate test worker ${e.message}`);
return false;
}
}
}