OneSpan Sign Developers: Virtual Room Recording – Part 3

Duo Liang,

In the Part 1 and Part 2 blogs, we walked through the process to configure a transaction for Virtual Room recording. From there, we introduced two methods for status notifications and downloading the video recordings.

For the third part of this continuing blog series, we will dive deeper into the callback notification support of the Virtual Room feature and demonstrate a working example in .NET code. Without further delay, let’s get started!

Event Notification for Video Recording

After all signers have finished signing a Virtual Room transaction, the recorded session starts to be processed behind the scenes. Besides being notified via email, you can also register the callback notification “Video recordings ready” via sender UI portal, as shown below:

6-8-1

Once the recordings are ready to download and this configuration is set, OneSpan Sign will send an HTTP POST request to your registered listener endpoint with a payload similar to the JSON below:

{"@class":"com.silanis.esl.packages.event.ESLProcessEvent","name":"RECORDINGS_READY","sessionUser":"ATQOPd60xE4V","packageId":"t0xp9WDwJrisseOl3JkIMXe_X0Y=","message":null,"documentId":null,"createdDate":"2022-06-03T14:46:36.808Z"}

If the event name appears as “RECORDINGS_READY”, your listener application knows it’s the time to pull the recording list and download each individual session.

Event Notification in Action

In this section, we will demonstrate how to implement a callback listener and process the recording ready event. It’s an ASP.NET Web API Application project, and the complete code can be downloaded here.

The first step is to expose an API controller which handles the callback requests from OneSpan Sign system:

    public class CallbackController : ApiController
    {
        // POST api/
        public ActionResult Post(JObject jsonResult)
        {
            #region "check callback key if applicable"
            string callbackKey = Request.Headers.GetValues("Authorization").First();
            Debug.WriteLine("callback key: "+callbackKey);
            #endregion

            #region "Extract data from request"
            string name = jsonResult["name"].ToString();
            string sessionUser = jsonResult["sessionUser"].ToString();
            string packageId = jsonResult["packageId"].ToString();
            string message = jsonResult["message"].ToString();
            string documentId = jsonResult["documentId"].ToString();
            string createdDate = jsonResult["createdDate"].ToString();

            OssCallbackVo ossCallbackVo = new OssCallbackVo
            {
                Name = name,
                SessionUser = sessionUser,
                PackageId = packageId,
                Message = message,
                DocumentId = documentId,
                CreatedDate = createdDate
            };

            Debug.WriteLine("callback payload: " + jsonResult.ToString());
            #endregion

            #region "process callback"
            OssCallbackHandler callbackHandlder = new OssCallbackHandler();
            Task.Run(async () => await callbackHandlder.ProcessOssCallback(ossCallbackVo));
            #endregion

            return new HttpStatusCodeResult(200);
        }
    }

In the code above, any POST request targeting the “/api/Callback” route will be handled by our controller.

Then, we will distribute to different service functions depending on the callback event. A service class with asynchronous method was used to handle this business logic.

    public class OssCallbackHandler
    {
        public Task ProcessOssCallback(OssCallbackVo ossCallbackVo)
        {
            switch (ossCallbackVo.Name)
            {
                case C.OssCallbackEvents.PACKAGE_CREATE:
                    break;
                case C.OssCallbackEvents.PACKAGE_ACTIVATE:
                    OssCallback_PACKAGE_ACTIVATE(ossCallbackVo);
                    break;
                case C.OssCallbackEvents.PACKAGE_DEACTIVATE:
                    break;
                case C.OssCallbackEvents.PACKAGE_READY_FOR_COMPLETION:
                    break;
                case C.OssCallbackEvents.RECORDINGS_READY:
                    OssCallback_RECORDINGS_READY(ossCallbackVo);
                    break;	       
                 ……
            }

            return Task.CompletedTask;
        }

Now, we have declared all possible stub functions to handle the callback events in our code. In this example, “RECORDINGS_READY” event was monitored and triggered the corresponding service function: 

            //step1: check recording status
            //GET /virtual-room-video/{packageId}/recordings
            HttpClient myClient = new HttpClient();
            myClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", C.OSS.API_KEY);
            myClient.DefaultRequestHeaders.Add("Accept", "application/json");

            var result = myClient.GetAsync(new Uri(C.OSS.BASE_URL + $"/virtual-room-video/{packageId}/recordings")).Result;
            Debug.WriteLine(result.Content.ReadAsStringAsync().Result);
            JArray returnJSON = JArray.Parse(result.Content.ReadAsStringAsync().Result);
            if (returnJSON != null && returnJSON.Count > 0)
            {
                foreach (JObject item in returnJSON) 
                {
                    string uid = item.GetValue("uid").ToString();
                    string fileName = item.GetValue("fileName").ToString();
                    string creationDate = item.GetValue("creationDate").ToString();
                    string fileSize = item.GetValue("fileSize").ToString();
                    string fileFormat = item.GetValue("fileFormat").ToString();
                    string status = item.GetValue("status").ToString();
                    string md5Hash = item.GetValue("md5Hash").ToString();
                    string deletionDate = item.GetValue("deletionDate").ToString();

                    if (status == "completed") {
                        //step2: download individual recordings
                        //GET /virtual-room-video/{packageId}/download-recording/{recording_uid}
                        HttpClient myClient2 = new HttpClient();
                        myClient2.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", C.OSS.API_KEY);
                        myClient2.DefaultRequestHeaders.Add("Accept", "video/mp4");
                        var result2 = myClient2.GetAsync(new Uri(C.OSS.BASE_URL + $"/virtual-room-video/{packageId}/download-recording/{uid}")).Result;
                        var stream = result2.Content.ReadAsStreamAsync().Result;
                        string fileLocation = C.SIGNED_DOCUMENTS_STORAGE_PATH + "\\" + Regex.Replace(fileName, "[^a-zA-Z0-9_.]+", "_", RegexOptions.Compiled);
                        Debug.WriteLine("file save path: " + fileLocation);
                        using (var fileStream = new FileStream(fileLocation, FileMode.Create))
                        {
                            stream.CopyToAsync(fileStream);
                        }               
                    }
                }
            }

As we discussed in Part 2, the suggested steps are to retrieve the status of the recordings first, then download the ones that have been successfully processed by its UID. 

Before you run the code, don’t forget to adjust the OneSpan Sign related configurations in Constants.cs file.

Perform a Test

Now it’s the time to run a quick test. If we created a Virtual Room signing transaction and quickly signed it, you should be able to find the downloaded recording files in the location of your choice.

If you have any questions regarding this blog or anything else concerning the integration of OneSpan Sign into your application, visit the Developer Community Forums. Your feedback matters to us!

Duo Liang is a Technical Evangelist and Partner Integrations Developer at OneSpan where he creates and maintains integration guides and code shares, helps customers and partners integrate OneSpan products into their applications, and builds integrations within third party platforms.