aws-amplify / amplify-ui

Amplify UI is a collection of accessible, themeable, performant React (and more!) components that can connect directly to the cloud.
https://ui.docs.amplify.aws
Apache License 2.0
909 stars 289 forks source link

LivenessDetect: @smithy/util-base64: toBase64 encoder function only accepts string | Uint8Array. #5057

Closed lwin-kyaw closed 8 months ago

lwin-kyaw commented 8 months ago

Before creating a new issue, please confirm:

On which framework/platform are you having an issue?

React

Which UI component?

Liveness

How is your app built?

Webpack 5

What browsers are you seeing the problem on?

Chrome, Firefox, Safari

Which region are you seeing the problem in?

ap-south-1

Please describe your bug.

Hi, I'm not sure whether this is doable or not. To give you the context, what I'm trying to do is exporting the liveness check react component as a webpack bundle and use it in the other projects. However, I'm getting this error when using @aws-amplify/ui-react-liveness,

@smithy/util-base64: toBase64 encoder function only accepts string | Uint8Array.

during the liveness analysis (video stream). The error occurs during the call to the aws start-face-liveness-session-websocket session (during the series of light flashes).

What's the expected behaviour?

Complete the liveness analysis and get the result from the Liveness session.

Help us reproduce the bug!

Bundle the liveness-check component with webpack 5 and import it to the another project. Start the liveness analysis.

Code Snippet

Exposed class to the other projects

/**
 * entry point of the webpack bundle
 * # example usage
 * const livenessUI = new LivenessUI({ language: "en"});
 * livenessUI.init();
**/
class LivenessUI {
  private language: string;

  constructor(config?: LivenessUIConfig) {
    this.language = config?.language || "en";
  }

  setLanguage(lang: string): void {
    this.language = lang;
  }

  // attach this method to button click or some HTML event
  // upon firing this method, `LivenessCheck` component will be mounted to the UI (DOM)
  init(): void {
    const container = this.createWrapper("100");
    const mountPoint = ReactDOMClient.createRoot(container);
    mountPoint.render(<LivenessCheck language={this.language} />);
  }

  private createWrapper(parentZIndex: string): HTMLElement {
    const existingWrapper = document.getElementById("liveness-container");
    if (existingWrapper) existingWrapper.remove();
    const parent = document.createElement("section");
    parent.classList.add("liveness-container");
    parent.setAttribute("id", "liveness-container");
    parent.style.zIndex = parentZIndex;
    parent.style.position = "relative";
    const wrapper = document.createElement("section");
    wrapper.setAttribute("id", "liveness-container");
    parent.appendChild(wrapper);
    document.body.appendChild(parent);
    return wrapper;
  }
}

export default LivenessUI;

React Component

// Liveness Check Component
export function LivenessAnalysis({ onCompleteAnalysis, language }: ILivenessProps) {
  const [loading, setLoading] = useState<boolean>(true);
  const [sessionId, setSessionId] = useState<string | null>(null);
  const [success, setSuccess] = useState("");

  const fetchCreateLiveness = useCallback(async () => {
    const response = await fetch(`${LIVENESS_API_ENDPOINT}/create-session`);
    const data = (await response.json()) as { sessionId: string };
    log.info("json", data);
    console.log("json", data);
    if (data.sessionId) {
      setSessionId(data.sessionId);
    } else {
      onCompleteAnalysis("Failed", "failed to create liveness-session");
    }
    setLoading(false);
  }, [onCompleteAnalysis]);

  useEffect(() => {
    fetchCreateLiveness();
  }, [fetchCreateLiveness]);

  const handleAnalysisComplete = async () => {
    if (sessionId) {
      const url = `${LIVENESS_API_ENDPOINT}/session-result?sessionId=${sessionId}`;
      const response = await fetch(url);

      const data = await response.json();
      log.info(data);
      if (data.response.isLive) {
        setSuccess("Liveness check success");
      } else {
        setSuccess("Liveness check failed");
      }
      onCompleteAnalysis(data.response.isLive ? "Success" : "Failed");
    }
  };

  const handleOnUserCancel = () => {
    log.info("handleOnUserCancel");
    onCompleteAnalysis("Cancelled");
  };
  if (loading || !sessionId) {
    return <Loader />;
  }
  return (
    <div>
      <FaceLivenessDetector
        sessionId={sessionId}
        region="ap-south-1"
        onAnalysisComplete={handleAnalysisComplete}
        onError={(error) => {
          log.error(error);
          onCompleteAnalysis("Failed", error.error.message);
        }}
        onUserCancel={handleOnUserCancel}
        displayText={livenessDisplayTexts(language)}
      />
      <Heading level={2}>{success}</Heading>
    </div>
  );
}

Console log output

Screenshot 2024-03-11 at 11 47 38

Additional information and screenshots

Relevant deps in package.json

"name": "liveness-ui",
"dependencies": {
    "@aws-amplify/ui-react": "^6.1.5",
    "@aws-amplify/ui-react-liveness": "^3.0.11",
    "aws-amplify": "^6.0.16",
    "react": "^18.x",
    "react-dom": "^18.x",
    "react-i18next": "^13.5.0",
    "typescript": "^5.4.2",
  },

webpack.config.js (I've removed some redundant snippets)

module.exports = {
  entry: "./src/index.ts",
  target: "web",
  output: {
     path: path.resolve(__dirname, "dist"),
     .... 
  },
  module: [
    ....
    {
      test: /(ts|js)x?$/,
      exclude: /node_modules/,
      use: "babel-loader",
    }
    ....
  ],
}

Usage in the other project

// tsx
import LivenessUI from "liveness-ui";

function MyComponent() {
    const livenessUI = new LivenessUI();

    function onInitUI() {
        livenessUI.init({language: "en"});
    }

   return (
      <div>
           ....
          <button on-click={onInitUI}>Init livness</button>
           ....
      </div>
   )
}
akabiria commented 8 months ago

I am experiencing the same exact issue after updating my packages.

On which framework/platform are you having an issue? React Remix Run

Which UI component? Liveness

How is your app built? Remix Build

What browsers are you seeing the problem on? Chrome, Firefox, Safari

Which region are you seeing the problem in? us-west-2

Please describe your bug. I am experiencing the same exact issue. It happens during liveness check analysis

esauerbo commented 8 months ago

Hi @lwin-kyaw thanks for including all these details. Looking at the LivenessUI class, where is the LivenessCheck component coming from? Are you able to provide a minimum reproducible example to help us recreate this error?

lwin-kyaw commented 8 months ago

Hi @esauerbo, thanks for the reply.

The below are the implementations of the components.

LivenessCheck.tsx

// imports
....
Amplify.configure(awsexports);

export default function LivenessCheck({
  language = "en",
}: LivenessCheckProps) {
  const [livnessResult, setLivenessResult] = useState<AnalysisResult | null>(null);
  const [analysisStatus, setAnalysisStatus] = useState<string | undefined>();

  const onTryAgain = () => {
    setLivenessResult(null);
  };

  const onCompleteAnalysis = (result: AnalysisResult, error?: string) => {
    setLivenessResult(result);
    setAnalysisStatus(error);
  };

  return (
    <ThemeProvider>
      {livnessResult ? (
        <Completion result={livnessResult} onTryAgain={onTryAgain} statusText={analysisStatus} />
      ) : (
        <LivenessAnalysis onCompleteAnalysis={onCompleteAnalysis} language={language} />
      )}
    </ThemeProvider>
  );
}

LivenessAnalysis.tsx

// Liveness Check Component
export function LivenessAnalysis({ onCompleteAnalysis, language }: ILivenessProps) {
  const [loading, setLoading] = useState<boolean>(true);
  const [sessionId, setSessionId] = useState<string | null>(null);
  const [success, setSuccess] = useState("");

  const fetchCreateLiveness = useCallback(async () => {
    const response = await fetch(`${LIVENESS_API_ENDPOINT}/create-session`);
    const data = (await response.json()) as { sessionId: string };
    log.info("json", data);
    console.log("json", data);
    if (data.sessionId) {
      setSessionId(data.sessionId);
    } else {
      onCompleteAnalysis("Failed", "failed to create liveness-session");
    }
    setLoading(false);
  }, [onCompleteAnalysis]);

  useEffect(() => {
    fetchCreateLiveness();
  }, [fetchCreateLiveness]);

  const handleAnalysisComplete = async () => {
    if (sessionId) {
      const url = `${LIVENESS_API_ENDPOINT}/session-result?sessionId=${sessionId}`;
      const response = await fetch(url);

      const data = await response.json();
      log.info(data);
      if (data.response.isLive) {
        setSuccess("Liveness check success");
      } else {
        setSuccess("Liveness check failed");
      }
      onCompleteAnalysis(data.response.isLive ? "Success" : "Failed");
    }
  };

  const handleOnUserCancel = () => {
    log.info("handleOnUserCancel");
    onCompleteAnalysis("Cancelled");
  };
  if (loading || !sessionId) {
    return <Loader />;
  }
  return (
    <div>
      <FaceLivenessDetector
        sessionId={sessionId}
        region="ap-south-1"
        onAnalysisComplete={handleAnalysisComplete}
        onError={(error) => {
          log.error(error);
          onCompleteAnalysis("Failed", error.error.message);
        }}
        onUserCancel={handleOnUserCancel}
        displayText={livenessDisplayTexts(language)}
      />
      <Heading level={2}>{success}</Heading>
    </div>
  );
}
davesahaj commented 8 months ago

I am facing the exact same issue while following these guides from official docs:

esauerbo commented 8 months ago

Thanks for reporting this everyone. We have another issue tracking this: https://github.com/aws-amplify/amplify-ui/issues/5058 so closing this out as a duplicate.

Please see the new issue for a temporary workaround and future updates on a fix. Feel free to comment on that thread with any questions or issues you run into.