@snap/camera-kit
Version:
Camera Kit Web
209 lines • 11.7 kB
JavaScript
import { defineAction, defineActions, defineState, defineStates, dispatch, forActions, inStates, StateMachine, } from "@snap/state-management";
import { Injectable } from "@snap/ts-inject";
import { catchError, exhaustMap, forkJoin, from, map, merge, mergeMap, Observable, of, switchMap, take, takeUntil, tap, } from "rxjs";
import { lensRepositoryFactory } from "../lens/LensRepository";
import { lensPersistenceStoreFactory } from "../lens/LensPersistenceStore";
import { encodeLensLaunchData } from "../lens/LensLaunchData";
import { legalStateFactory } from "../legal/legalState";
import { lensAssetRepositoryFactory } from "../lens/assets/LensAssetRepository";
import { legalError, lensContentValidationError, lensError } from "../namedErrors";
import { getLogger } from "../logger/logger";
import { Timer } from "../metrics/operational/Timer";
import { unsubscribed } from "../observable-operators/unsubscribed";
import { assertUnreachable } from "../common/assertions";
import { metricsClientFactory } from "../clients/metricsClient";
import { lensCoreFactory } from "../lens-core-module/loader/lensCoreFactory";
import { remoteConfigurationFactory } from "../remote-configuration/remoteConfiguration";
import { watermarksLensGroup } from "../lens/fetchWatermarkLens";
import { userDataAccessResolverFactory } from "../lens/userDataAccessResolver";
const logger = getLogger("LensState");
const createLensState = () => {
const actions = defineActions(defineAction("applyLens")(), defineAction("downloadComplete")(), defineAction("turnedOn")(), defineAction("resourcesLoaded")(), defineAction("firstFrameProcessed")(), defineAction("applyLensComplete")(), defineAction("applyLensFailed")(), defineAction("applyLensAborted")(), defineAction("removeLens")(), defineAction("turnedOff")(), defineAction("removeLensComplete")(), defineAction("removeLensFailed")());
const states = defineStates(defineState("noLensApplied")(), defineState("applyingLens")(), defineState("lensApplied")());
return new StateMachine(actions, states, states.noLensApplied(), (events) => merge(events.pipe(inStates("noLensApplied", "applyingLens", "lensApplied"), forActions("applyLens"), map(([a]) => states.applyingLens(a.data.lens))), events.pipe(inStates("applyingLens"), forActions("applyLensComplete"), map(([a]) => states.lensApplied(a.data))), events.pipe(inStates("applyingLens"), forActions("applyLensFailed"), map(() => states.noLensApplied())), events.pipe(inStates("lensApplied"), forActions("removeLensComplete"), map(() => states.noLensApplied()))));
};
export const lensStateFactory = Injectable("lensState", [
lensCoreFactory.token,
lensRepositoryFactory.token,
lensAssetRepositoryFactory.token,
lensPersistenceStoreFactory.token,
legalStateFactory.token,
metricsClientFactory.token,
remoteConfigurationFactory.token,
userDataAccessResolverFactory.token,
], (lensCore, lensRepository, lensAssetRepository, lensPersistence, legalState, metrics, remoteConfig, getUserDataAccess) => {
const lensState = createLensState();
let firstLensApply = true;
lensState.events
.pipe(forActions("applyLens"), exhaustMap(([a]) => of(legalState.actions.requestLegalPrompt()).pipe(dispatch(legalState), inStates("accepted", "rejected"), take(1), map(([, { name }]) => {
if (name === "accepted")
return a;
return lensState.actions.applyLensFailed({
error: legalError(`Failed to apply lens ${a.data.lens.id}. Required legal terms were not accepted.`),
lens: a.data.lens,
});
}))), switchMap((a) => {
if (a.name === "applyLensFailed")
return of(a);
const { lens } = a.data;
const dispatch = (action) => {
lensState.dispatch(action, lens);
};
const applyTimer = new Timer("lens").mark("apply", { first: `${firstLensApply}` });
firstLensApply = false;
return forkJoin({
watermarkInput: remoteConfig.getInitializationConfig().pipe(mergeMap((config) => {
if (!config.watermarkEnabled)
return of(undefined);
return from(lensRepository.loadLens("", watermarksLensGroup)).pipe(mergeMap((watermark) => from(lensRepository.getLensContent(watermark)).pipe(map(({ lensBuffer, lensChecksum }) => {
return {
lensId: watermark.id,
lensDataBuffer: lensBuffer.slice(0),
lensChecksum,
launchData: new ArrayBuffer(0),
};
}))));
})),
lensInput: of(a.data).pipe(mergeMap(({ lens, launchData }) => {
return from(lensPersistence.retrieve(lens.id).catch(() => undefined)).pipe(map((persistentStore) => ({ lens, launchData, persistentStore })));
}), map(({ lens, launchData, persistentStore }) => {
const lensDetails = lensRepository.getLensMetadata(lens.id);
if (!lensDetails) {
throw new Error(`Cannot apply lens ${lens.id}. It has not been loaded by the Lens ` +
`repository. Use CameraKit.lensRepository.loadLens (or loadLensGroups) ` +
`to load lens metadata before calling CameraKitSession.applyLens.`);
}
const { content, isThirdParty } = lensDetails;
if (!content) {
throw new Error(`Cannot apply lens ${lens.id}. Metadata retrieved for this lens does not ` +
`include the lens content URL.`);
}
return {
lens,
launchData: encodeLensLaunchData(launchData !== null && launchData !== void 0 ? launchData : {}, persistentStore !== null && persistentStore !== void 0 ? persistentStore : new ArrayBuffer(0)),
content,
isThirdParty,
};
}), mergeMap(({ lens, launchData, content, isThirdParty }) => {
const networkTimer = applyTimer.mark("network");
return from(Promise.all([
lensRepository.getLensContent(lens).finally(() => networkTimer.measure("lens")),
isThirdParty ? getUserDataAccess(lens) : undefined,
content.assetManifest.length > 0
? lensAssetRepository
.cacheAssets(content.assetManifest, lens)
.finally(() => networkTimer.measure("assets"))
: Promise.resolve(),
])).pipe(tap(() => {
networkTimer.measure();
lensState.dispatch("downloadComplete", lens);
}), map(([{ lensBuffer, lensChecksum }, userDataAccess]) => {
const lensDataBuffer = lensBuffer.slice(0);
return {
lensId: lens.id,
lensDataBuffer,
lensChecksum,
launchData,
apiVisibility: isThirdParty
? lensCore.LensApiVisibility.Public
: lensCore.LensApiVisibility.Private,
publicApiUserDataAccess: userDataAccess === "restricted"
? lensCore.UserDataAccess.Restricted
: lensCore.UserDataAccess.Unrestricted,
};
}));
})),
}).pipe(takeUntil(lensState.events.pipe(forActions("removeLens"))), mergeMap(({ lensInput, watermarkInput }) => new Observable((subscriber) => {
const coreTimer = applyTimer.mark("core");
lensCore
.replaceLenses({
lenses: [
Object.assign(Object.assign({}, lensInput), { onTurnOn: () => dispatch("turnedOn"), onResourcesLoaded: () => dispatch("resourcesLoaded"), onFirstFrameProcessed: () => {
coreTimer.measure("first-frame");
applyTimer.measure("success");
applyTimer.stopAndReport(metrics);
dispatch("firstFrameProcessed");
}, onTurnOff: () => dispatch("turnedOff") }),
...(watermarkInput ? [watermarkInput] : []),
],
})
.then(() => {
coreTimer.measure("success");
subscriber.next(lensState.actions.applyLensComplete(lens));
subscriber.complete();
})
.catch((lensCoreError) => {
coreTimer.measure("failure");
applyTimer.measure("failure");
applyTimer.stopAndReport(metrics);
const message = `Failed to apply lens ${lensInput.lensId}.`;
const error = /validation failed/.test(lensCoreError.message)
? lensContentValidationError(message, lensCoreError)
: lensError(message, lensCoreError);
subscriber.next(lensState.actions.applyLensFailed({ error, lens }));
subscriber.complete();
});
})), catchError((error) => {
applyTimer.measure("failure");
applyTimer.stopAndReport(metrics);
return of(lensState.actions.applyLensFailed({ error, lens }));
}), unsubscribed(() => {
applyTimer.measure("abort");
applyTimer.stopAndReport(metrics);
}));
}), dispatch(lensState))
.subscribe({
error: logger.error,
});
lensState.events
.pipe(inStates("lensApplied", "noLensApplied"), forActions("removeLens"), mergeMap(() => new Observable((subscriber) => {
lensCore
.clearAllLenses()
.then(() => {
subscriber.next(lensState.actions.removeLensComplete());
subscriber.complete();
})
.catch((lensCoreError) => {
const error = new Error("Failed to remove lenses.", { cause: lensCoreError });
subscriber.next(lensState.actions.removeLensFailed(error));
subscriber.complete();
});
})), dispatch(lensState))
.subscribe({
error: logger.error,
});
lensState.events
.pipe(inStates("applyingLens"), forActions("removeLens"), switchMap(([a]) => lensState.events.pipe(inStates("lensApplied"), takeUntil(lensState.events.pipe(forActions("applyLens"))), map(() => a))), dispatch(lensState))
.subscribe({
error: logger.error,
});
lensState.events.subscribe(([a, s]) => {
const data = extractLoggableData(a);
logger.debug(`Action: "${a.name}", state: "${s.name}"${data ? ", data: " + JSON.stringify(data) : ""}`);
});
return lensState;
});
function extractLoggableData(action) {
switch (action.name) {
case "applyLens":
return { lensId: action.data.lens.id };
case "applyLensFailed":
return { lensId: action.data.lens.id, error: action.data.error.message };
case "downloadComplete":
case "turnedOn":
case "resourcesLoaded":
case "firstFrameProcessed":
case "applyLensComplete":
case "applyLensAborted":
case "turnedOff":
return { lensId: action.data.id };
case "removeLens":
case "removeLensComplete":
return undefined;
case "removeLensFailed":
return { error: action.data.message };
default:
assertUnreachable(action);
}
}
//# sourceMappingURL=lensState.js.map