通过协同浏览和 PDF 编辑将实时电子签名添加到 WebRTC 应用程序中

本文将详细介绍如何在 WebRTC 应用程序中启用实时电子签名。

实时电子签名的前提条件

  • 假设您已经拥有一个安全的视频会议应用程序。
  • 此处的代码示例使用 TypeScript 编写,在 NodeJS 和 Next.js 上运行。如果您的视频会议应用程序使用不同的协议栈,您仍然可以应用这些概念,但需要调整代码。
  • 您需要一个 Docker 和 Docker Compose,以便在本地运行协同浏览服务。
  • PubNub 账户用于处理与编辑和共享 PDF 文件相关的事件。

第一部分:启用 PDF 编辑

第一步是让机构员工(我们称之为 “高级用户”)在应用程序中添加 PDF 文档。为此,让我们创建一个新的 React 组件,将其包含在应用程序的视频通话页面中。

在这个新组件中,我们将执行三项任务:

  • 添加 PDF 文档
  • 允许编辑 PDF 文档
  • 编辑完成后,在共同浏览会话中呈现它

在应用程序中添加 PDF 文件

让我们先在视频通话组件中添加新的 ESignContainer 组件,以及一个用于存储 PDF 文件 URL 的 useState 钩子。该 URL 将作为道具传递给新组件。根据应用程序的逻辑,您可能还需要向其传递其他道具。

...
// the video call component
export const MeetingContainer = function MeetingContainer() {
  ...
  // state hook for file URL
  const [fileUrl, setFileUrl] = useState('')
  ...
  // add the new container and pass fileUrl as prop
  return (
    ...
    <ESignContainer
      ...
      fileUrl={fileUrl} />
  )
}

现在,我们需要一种方法,让所有参与者都知道有文件可用。为此,我们将向参与者订阅一个 PubNub 频道。我们需要为频道设置一个唯一的名称,也许是视频通话会话的标识符或房间名称。请务必根据自己的应用程序进行调整。

然后,我们使用 useEffect 钩子初始化 PubNub 客户端,并开始监听新消息。现在,我们只想在状态中设置文件 URL。在这篇文章中,我们使用 Pubnub-react 库,通过一个方便的钩子来初始化客户端。

// import pubnub-react library
import { usePubNub } from 'pubnub-react'
...
// the video call component
const const MeetingContainer = function MeetingContainer(){
  ...
  // initialize PubNub client
  const pubnub = usePubNub()
  // set the channel name to room name
  //   or whatever unique value works for your application
  const channel = ['roomName'] 
  ...
  useEffect(() => {
    // variable for listener
    let listenerParams

    // different subscriptions types for users and power users
    if (user.role === 'client') {
      listenerParams = {
        // when a file arrives, store the file's URL in the state
        file(event) {
          setFile(event.file.url)
        } 
      }
    } else {
      listenerParams = {
        // same for power user
        file(event) {
          setFile(event.file.url)
        }
      }
    }

    // initialize the listener
    pubnub.addListener(listenerParams)
    pubnub.subscribe({ channel })
    return () => {
      // deregister the listener when unmounting
      pubnub.unsubscribe({ channel })
      pubnub.removeListener(listenerParams)
    }
  }, [pubnub, channel])
  ...
}

现在,让我们创建 ESignContainer 组件。在这里,我们要管理共享的文件以及这些共享的状态。这包括针对此类文件的 React useState 钩子和一组指示其状态的多个标志。

它还包括当用户拖放文件或点击 “添加 “按钮时的处理函数。此外,我们还使用 PubNub 的 sendFile 功能为 PDF 文件设置临时存储空间,并共享其 URL。

我们还创建了一个新组件 ESignView,它允许高级用户添加 PDF 文件,并在文件可用时对其进行渲染。稍后我们将深入了解该组件。

...
export const ESignContainer = function ESignContainer({
	...
	fileUrl // we pass the fileUrl as props, along with any other
              //   that your application may need
}: Props) {
  // useState hook for files
  const [files, setFiles] = useState([])
  // a flag that indicates if file has been uploaded to PubNub
  const [uploadFinished, setUploadFinished] = useState(false)
  // a flag that indicates if the power user has clicked the Add button
  const [uploadClicked, setUploadClicked] = useState(false)
  ...
  // handler function for when power users drag and drop files
  const handleDrop = (e) => {
    e.preventDefault()
    const filesArray = []
    const { items } = e.dataTransfer
    for (let i = 0; i < items.length; i++) {
      if (items[i].kind === 'file') {
        const file = items[i].getAsFile()
        filesArray.push(file)
      }
    }
    setFiles(filesArray)
  }
  // handler function for when power users click the Add button
  const handleUpload = async () => {
    files.forEach(async (file) => {
      // we upload the file to PubNub and notify users
      const result = await pubnub.sendFile({
        channel: 'roomName',
        file,
        message: 'AdminFile',
      })
    })
  }
  ...
  // rendering the UI component for PDF upload and editing
  //  if the upload hasn't finished, we show the ESignView component
  //  which we will look later
  return (
    ...
    {!uploadFinished && user.role === 'powerUser' && (
      <ESignView
        files={files}
        fileUrl={fileUrl}
        handleDrop={handleDrop}
        handleUpload={handleUpload}
        uploadClicked={uploadClicked}
        setUploadClicked={setUploadClicked} />
    )}
  )
}

现在让我们看一下该ESignView 组件。这里我们想要展示一个表单,高级用户可以在其中添加 PDF 文件。之后,我们希望高级用户能够在与客户端共享之前准备文件。

为此,我们将呈现一个支持拖放文件的简单上传表单。为此,我们在表单的 onDrop 属性中设置了之前定义的 handleDrop 函数。该表单包含一个按钮,通过传递到其 onClick 属性的之前定义的 handleUpload 函数启动上传。

PDF 编辑功能将来自PSPDFKit库,我们将在下一节中探讨。现在,我们只需在 PDF 编辑器所在的位置添加一个 HTML div 标签即可。 

...
export const ESignView = function ESignView({
  // props
}: Props) {
  return (
    <div
      {/* we add the onDrop and onDragOver events */}
      onDrop={handleDrop}
      onDragOver={(e) => e.preventDefault()}
    >
      {/* we show the form until the user clicks Add */}
      {!uploadClicked && (
        <div>
          <p>Drag and drop file here and click to upload</p>
          <small>Formats accepted: pdf, doc, docx</small>
          <ul>
            {files.map((file) => (
              <li key={file.name}>{file.name}</li>
            ))}
          </ul>
          <Button type="button" onClick={handleUpload}>
            Add
          </Button>
        </div>
      )}
      {/* div for PDF editor */}
      <div />
    </div>
  )
}

输入 PSPDFKit

一旦文件在 PubNub 中可用,我们就会下载并渲染它,以便高级用户可以在与客户端共享之前对其进行编辑。我们将使用PSPDFKit库。根据库文档,我们首先安装库并将 Web 资源复制到public项目的文件夹中,如下所示:

npm install pspdfkit
cp -R ./node_modules/pspdfkit/dist/pspdfkit-lib public/pspdfkit-lib

接下来,我们需要实际呈现一个文件。我们目前编写的代码允许高级用户将文件上传到 PubNub。此时,PubNub 会让所有参与者都知道有一个文件可用,反过来,他们也会将文件的 URL 保存在 ESignContainer 组件的 fileUrl 变量中。让我们使用该 fileUrl 变量来了解文件何时可以编辑,并使用 PSPDFKit 对其进行实际渲染。

首先,让我们使用 useState 钩子来存储 PDF 编辑器的实例。我们将使用该实例导出最终的 PDF 文件。我们还需要初始化 PSPDFKit 客户端,并使用 useRef 钩子获取编辑器渲染的 HTML div 标签的引用。

PDF 魔术发生在 useEffect 钩子中,它会对 fileUrl 值的变化做出反应。我们首先要做的是使用 axios 库下载文件,然后将其添加到编辑器中。为此,我们添加了一个自定义的 convertPdfToBase64 函数,该函数接收 fileUrl 并将其转换为编辑器可用的格式。

...
export const ESignView = function ESignView({
  ...
  // useState hook for the PDF editor instance
  const [instance, setInstance] = useState<any>()
  // PSPDFKit variable
  let PSPDFKit
  // useRef hook for the div element
  const containerRef = useRef(null)

  // custom function for converting PDF up base64
  const convertPdfToBase64 = async (url) => {
    try {
      // Download the PDF file
      const response = await axios.get(url, {
        responseType: 'arraybuffer',
      })
      const pdfData = Buffer.from(response.data, 'binary')

      // Convert PDF data to Base64
      const base64Data = pdfData.toString('base64')
      return base64Data
  } catch (error) {
      console.error('Error converting PDF to Base64:', error)
      return null
    }
  }

  // all the magic happens here
  useEffect(() => {
    const container = containerRef.current

    ;(async function () {
      PSPDFKit = await import('pspdfkit')

      if (PSPDFKit) {
        PSPDFKit.unload(container)
      }
      if (fileUrl) {
        setUploadClicked(true)
        convertPdfToBase64(fileUrl).then(async (base64Data) => {
          await PSPDFKit.load({
            container,
            document: `data:application/pdf;base64,${base64Data}`,
            baseUrl: `${window.location.protocol}//${window.location.host}/`,
            toolbarItems: [
              ...PSPDFKit.defaultToolbarItems,
              {
                type: 'form-creator',
              },
            ],
            initialViewState: new PSPDFKit.ViewState({
              interactionMode: 
                PSPDFKit.InteractionMode.FORM_CREATOR,
            }),
          }).then((ins) => {
            console.log('Succesfully loaded')
            setInstance(ins)
          })
        })
      }
    })()

    return () => PSPDFKit && PSPDFKit.unload(container)
  }, [fileUrl])
  …
  {/* Adding ref attribute to div for PDF editor */}
  <div ref={containerRef} />
}

启动 PDF 共享

现在,你的应用程序应该能够上传 PDF 文件,将其存储在 PubNub 中,并允许高级用户对其进行编辑。现在,让我们添加一种与其他参与者共享文件的方法。

为此,我们在 ESignContainer 中加入一个新标志:shareAndStart。该标志表示文件已准备就绪,参与者可以开始电子签名。我们还有一个新的 useState 钩子:finalPDFBuffer,用于存储添加到 PubNub 之前的最终 PDF。

通过点击新的 “共享文档并开始电子签名 “按钮,可以启用新的标志,该按钮在编辑文件和点击上传按钮后可用。该标志的值将传递给 ESignView 组件,我们稍后将对此进行说明。

我们还会监听 finalPDFBuffer 值的变化,当文件准备就绪时,我们会再次使用 sendFile 功能将其上传到 PubNub。之后,我们就可以进行协同浏览了。下一节将详细介绍。

除了新的标志和钩子,我们还想向 PubNub 频道发送一条消息,通知所有参与者,一旦共同浏览会话准备就绪,他们就可以加入。现在,我们将定义一个 sendShareAndStart 函数,该函数将在点击上述按钮时被调用。

export const ESignContainer = function EsignContainer({ // props }) {
  ...
  // new flag for sharing pdf file and starting esign
  const [shareAndStart, setShareAndStart] = useState(false)
  // new variable for storing final PDF file as buffer
  const [finalPDFBuffer, setFinalPDFBuffer] = useState()
  ...
  // function for sending the message to PubNub channel
  const sendStartEsign = async () => {
    const publishPayload = {
      channel: 'roomName',
      message: {
        title: 'StartEsign'
      },
    }
    // Send the notification
    const resp = await pubnub.publish(publishPayload)
  }

  // function for sending the final PDF to PubNub channel
  const handleUploadComplete = async () => {
    if (finalPDFBuffer) {
      const uint8Array = new Uint8Array(finalPDFBuffer)

      // Create an object with the Uint8Array data
      const fileObject = {
        data: uint8Array,
        name: 'file.pdf',
        type: 'application/pdf',
      }

      const result = await pubnub.sendFile({
        channel: 'roomName',
        file: fileObject,
        message: 'FinalFile',
      })

      setUploadFinished(true)
    }
  }

  // we listen for changes in finalPDFBuffer
  useEffect(() => {
    handleUploadComplete()
  }, [finalPDFBuffer])
  ...
  return (
    ...
    {uploadClicked && user.role === 'powerUser' && (
      <Button
        variant="primary"
        onClick={async () => {
            // send the URL
            await sendStartEsign()
            // set the flag as true
            setShareAndStart(true)
          }
        }
      >
        Share Document and Start eSign
      </Button>
    )}
    ...
      <ESignView
        ...
        setFinalPDFBuffer={setFinalPDFBuffer}
        shareAndStart={shareAndStart} />
    ...
  )
}

现在,让我们来看看 ESignView 组件。在这里,我们会监听作为道具接收到的 shareAndStart 变量的变化,当 PDF 文件的实例准备就绪时,我们会将其分配给刚刚创建的 finalPDFBuffer 状态。这一逻辑存在于一个单独的函数中,我们使用 useCallback 钩子使其在组件渲染时达到最佳效果。我们要做的另一件事是只在shareAndStart标志为 false 时才渲染 PDF 文件,编辑文件时就是这种情况。

export const ESignView = function ESignView({
  ...
  setFinalPDFBuffer,
  shareAndStart
} : Props) {
  ...
  // a function to set finalPDFBuffer variable when an instance of PDF
  //  file is available
  const getArrayBuffer = useCallback(async () => {
    if (instance) {
      const buffer = await instance.exportPDF()
      setFinalPDFBuffer(buffer)
    }
  }, [PSPDFKit, instance])

  // we listen for changes in shareAndStart prop
  useEffect(() => {
    getArrayBuffer()
  }, [shareAndStart])

  useEffect(() => {
    if (!shareAndStart) {
      // the code that renders the PDF goes here
    }
  }, [fileUrl])
  ...
}

现在,我们已经准备好共同浏览了!但在此之前,让我们为将要共同浏览的页面创建一个组件。我们将把这个组件称为 ESignPageContainer。在这个新组件中,我们使用 PSPDFKit 来渲染上传到 PubNub 频道的最新 PDF 文件。

我们还需要该频道的唯一标识符,我们称之为 esignId。在我们的示例中,我们将其设置为房间名称,请确保根据你的应用程序的逻辑进行调整。

export const ESignPageContainer = function ESignPageContainer() {
  // useState hook for storing PDF file's URL
  const [fileUrl, setFileUrl] = useState<string>()

  // hooks and variables for PSPDFKit
  const [instance, setInstance] = useState<any>()
  let PSPDFKit
  const containerRef = useRef(null)

  // usePubNub hook for initializing PubNub variable
  const pubnub = usePubNub()

  // set the esignId 
  const esignId = roomName // or any other value that makes sense for 
                           //   your application

  // useEffect hook for downloading the more recent file from PubNub
  useEffect(() => {
    // list all the available files in the channel
    pubnub.listFiles(
      {
        channel: esignId,
        limit: 25,
      },
      (status, response) => {
        // sort the files from newest to oldest
        const sortedFiles = response.data.sort(
          (a, b) =>
            new Date(b.created).getTime() -
            new Date(a.created).getTime()
        )
        // get the URL of the file
        const result = pubnub.getFileUrl({
          channel: esignId,
          id: sortedFiles[0].id,
          name: sortedFiles[0].name,
        })
        // set the file URL in the state
        setFileUrl(result)
      }
    )
  }, [])

  // useEffect hook for rendering PDF editor
  useEffect(() => {
    // PSPDFKit code goes here
  }, [fileUrl])

  return (
    <div>
      <div
        ref={containerRef}
        style={{ height: '100%', width: '100%' }}
      />
    </div>
  )
}

第二部分:集成协同浏览功能

使用n.eko启用协同浏览。n.eko 将自己定义为“一个在 Docker 中运行的自托管虚拟浏览器”。该工具允许您在通过 WebRTC 控制的远程服务器中运行虚拟浏览器。对于开发人员测试 Web 应用程序、寻求安全浏览体验的注重隐私的用户或任何可以利用在隔离环境中运行的虚拟服务器的事物来说,它是一个理想的解决方案。

对于我们的特定用例,最有趣的功能是 n.eko 允许多个用户同时访问浏览器。因此,它提供了实时电子签名发生的“共享”环境。

在本地运行 n.eko

第一步是运行 n.eko。使用 Docker 和 Docker Compose 可以轻松完成此操作。官方文档提供了使用 Google Chrome 的 docker-compose.yml 文件示例。将此文件添加到您的项目中。

version: "3.4"
services:
  neko:
    image: "m1k1o/neko:google-chrome"
    restart: "unless-stopped"
    shm_size: "2gb"
    ports:
      - "8080:8080"
      - "52000-52100:52000-52100/udp"
    cap_add:
      - SYS_ADMIN
    environment:
      NEKO_SCREEN: '1920x1080@30'
      NEKO_PASSWORD: neko
      NEKO_PASSWORD_ADMIN: admin
      NEKO_EPR: 52000-52100
      NEKO_NAT1TO1: 127.0.0.1

然后您可以使用 docker-compose up 命令运行 n.eko。因此,您将在http://localhost:8080上获得一个 Web 客户端,您可以使用它通过 WebRTC 与远程浏览器进行交互。

将 n.eko 添加到视频会议应用程序

将此功能添加到视频会议应用程序的最简单方法是通过 HTML iframe 标签显示 n.eko 的网络客户端。您甚至可以创建自己的客户端,使其按照您想要的方式运行,并添加其他功能,如更高级的验证机制或支持额外参数。在本篇文章中,我们将使用默认的客户端。

在研究这个问题之前,我们先来回顾一下上一节的内容。您可能还记得,在高级用户点击 “共享文档并开始电子签名 “按钮后,应用程序会通知客户端它已准备好开始电子签名,同时更新 shareAndStart 标志,这反过来又会触发一个设置 finalPDFBuffer 的函数。一旦 finalPDFBuffer 准备就绪,就会使用 sendFile 上传到 PubNub。

因此,我们需要监听两条消息:一条是 StartEsign 消息,让客户端知道电子签名已经开始;另一条是 FinalFile 消息,在将编辑好的文件上传到 PubNub 时发送。我们利用这些信息让客户和高级用户设置 iframe。

首先,让我们在视频容器中添加 n.eko 网络客户端 http://localhost:8080 的 URL,并将其作为道具发送到 ESignContainer 组件。接下来,我们需要回到 PubNub 监听器,确保在收到通知时初始化 iframe。

正如前面提到的,客户端需要监听消息StartEsign,而高级用户需要监听FinalFile,所以让我们添加用于设置 iframe 的代码。

为了设置 iframe,我们需要 n.eko Web 客户端的 URL,并传递一些允许用户直接浏览的查询参数。这些参数是显示名称和密码。

显示用户名可以是您想要的任何名称,它用于识别共同浏览会话中的用户。您可以使用应用程序为用户提供的名称。另一方面,密码是在 n.eko 级别定义的。默认情况下,admin是向用户授予提升权限的密码,neko用于普通用户。

// the video call component
export const MeetingContainer = function MeetingContainer() {
  ...
  // useState hook for storing URL of n.eko
  const nekoUrl = "http://localhost:8080"
  const [nekoSrc, setNekoSrc] = useState()
  ...
  // useEffect hook where we define PubNub listeners
  useEffect(() => {
    ...
    // different subscriptions types for users and power users
    if (user.role === 'client') {
      listenerParams = {
        ...
        // we listen for messages
        message(s) {
          if (s.message.title === "StartEsign") {
            const src = `${nekoUrl}?usr=${encodeURIComponent(
              username // or whatever other value your application uses
            )}&pwd=neko`
            setNekoSrc(src)
          }
        }
      }
    } else {
      listenerParams = {
        file(event) {
          setFile(event.file.url)
          if (event.message === "FinalFile") {
            const src = `${nekoUrl}?usr=${encodeURIComponent(
              username // or whatever other value your application uses
            )}&pwd=admin`
            setNekoSrc(src)
          }
        }
      }
    }
    ...
  }, [pubnub, channel])
  ...
  // we pass nekoSrc as prop to ESignContainer
  return (
    ...
    <ESignContainer
      ...
      nekoSrc={nekoSrc} />
    ...
  )

现在,在 ESignContainer 中,我们只需在 setAndShareStart 值为 true 时添加 HTML iframe 元素,并将其 src 属性设置为我们刚刚创建的 prop。

export const ESignContainer = function ESignContainer({
  ...
  nekoSrc
}: Props) {
  ...
  return (
    ...
    {shareAndStart && (
      <iframe 
        title="neko"
        src={nekoSrc}
        width="100%"
        height="100%"
    )}
    ...
  )
}

之后,只需手动导航到我们之前创建的 ESignPageContainer 页面即可。您也可以为 n.eko 创建一个自定义Web客户端,该客户端支持将目标页面指定为查询参数。如果你对后者感兴趣,可以查看 neko 客户端的代码。

就是这样!

在 WebRTC 应用程序中启用共同浏览和 PDF 编辑功能后,所有各方都可以远程签署法律文件、关闭贷款、执行电子按揭以及其他通常需要亲自见证、公证或可视化身份证明的活动。

作者:Tahir Golge
原文:https://webrtc.ventures/2023/10/adding-live-esign-to-your-webrtc-application-with-co-browsing-and-pdf-editing/

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/35430.html

(0)

相关推荐

发表回复

登录后才能评论