> ## Documentation Index
> Fetch the complete documentation index at: https://cloudinary.com/documentation/llms.txt
> Use this file to discover all available pages before exploring further.

# Video review sample project


This guide demonstrates how to build a mockup of a product page that handles user-generated content (UGC) using Cloudinary's advanced capabilities. Customers can upload a video review of the product, which Cloudinary moderates for inappropriate content and malware. It also provides video chapters and transcriptions that the [Cloudinary Video Player](cloudinary_video_player) uses for captions and translated subtitles. The Video Player displays the video at 16:9 aspect ratio with automatic gravity.

Product page

Video displaying chapters and captions 

> **TIP**: :title=View the code

You can find the code for this sample project in [GitHub](https://github.com/cloudinary-devs/video-review-demo).

## Overview

The app serves as a demonstration platform for handling user-generated videos in a product review context. It implements these main features:

* The uploaded video is moderated for appropriate content and checked for malware before being displayed on the page.
* The Cloudinary Video Player displays the video at 16:9 aspect ratio with automatic gravity and uses the auto-generated chapters.
* Auto transcription and translation features provide the text for the captions and subtitles.

### Key Cloudinary features

Learn about how each of these Cloudinary features have been implemented in this app:

* [Upload preset](#upload_preset): the instructions for uploading videos
* [Upload widget](#upload_widget): the method of uploading videos
* [Moderation and malware detection](#moderation_and_malware_detection): ensuring uploaded videos are appropriate and safe
* [Video transcription and localization](#video_transcription_and_localization): making the videos accessible
* [Video chaptering](#video_chaptering): providing ease of navigation through longer videos
* [Video transformations](#video_transformations): fitting the videos to the display area

## Try it out

Here's the app in action:

To run the app yourself: 

1. Clone or fork the [GitHub repo](https://github.com/cloudinary-devs/video-review-demo).
1. In **app/config/cloudinary.ts**, replace **MY_CLOUD_NAME** with your Cloudinary product environment cloud name.
    
What's my cloud name?

You can find your **Cloud name** near the top of the [Dashboard](https://console.cloudinary.com/app/home/dashboard) of the Cloudinary Console. [Sign up for free](https://cloudinary.com/users/register_free) if you don't yet have a Cloudinary account.

2. [Register](https://console.cloudinary.com/app/settings/addons) for the following add-ons (they all have free tiers):
     * [Google Translation add-on](translation_addons)
     * [Amazon Rekognition Video Moderation add-on](aws_rekognition_video_moderation_addon)
     * [Perception Point Malware Detection add-on](perception_point_malware_detection_addon)
3. To try out your app locally, you need to set up a secure tunnel connecting the internet to your locally running application so that the webhooks sent by Cloudinary on upload are caught and handled by the app. You can use a tool such as [Ngrok](https://ngrok.com/) to do this. Otherwise, you need to deploy the app using a service such as [Vercel](https://vercel.com/). Whichever method you choose, make a note of your app's domain (for example, `a-b-c-d.ngrok-free.app` or `a-b-c-d.vercel.app`). By default, the app runs on port 3000.
4. Create an upload preset called **ugc-video-langs**. (You can use a different name, but if you do, you also need update the `uploadPreset` value in **cloudinary.ts**.) See instructions on how to [configure your upload preset](#upload_preset_configuration).
5. Ensure that the **Notification URL** in your upload preset is set to:`https://<your app's domain>/api/moderate`
6. If running locally, run:
   
      ```terminal
      npm i
      ```
      then

      ```terminal
      npm run dev
      ```

      Then open [http://localhost:3000](http://localhost:3000) in your browser to see the app running. 

### Upload preset configuration

To configure the upload preset:

1. Log into your [Cloudinary Console](https://console.cloudinary.com). 
1. Navigate to **Settings > Upload > Upload Presets**.
1. Click **Add Upload Preset**. 
1. Configure each of the sections as shown below, then click **Save**:

#### General

{table:class=wide-2ndcol} Parameter | Value | Meaning
--|--|--
 **Upload preset name** | `ugc-video-langs` | The name of the upload preset. This must match the **uploadPreset** parameter used in the [Upload widget configuration](#upload_widget) (set in **cloudinary.ts**).
 **Signing mode** | `Unsigned` | No signature is required for uploading assets using this upload preset.
 **Auto-generate an unguessable public ID value** | `true` | It's best to generate a random value to avoid conflicts if you have many users uploading their videos to your product environment.

The rest of the **General** settings can be set as you like.

![Advanced upload preset settings](https://cloudinary-res.cloudinary.com/image/upload/bo_1px_solid_gray/f_auto/q_auto/docs/ugc-video-langs-general-tab.png "thumb: c_scale,w_600/dpr_2.0, width:600, popup:true")

#### Transform

{table:class=wide-2ndcol} Parameter | Value | Meaning
--|--|--
 **Incoming transformation** | `c_limit,h_1000,w_1000/eo_600` | Limit the dimensions of the video to 1000 by 1000 pixels and truncate the video to 10 minutes (600 seconds).
 **Eager transformations** | `ar_16:9,c_fill,g_auto,w_600/mp4`&#124;`ar_16:9,c_fill,g_auto,w_600/webm` | Prepare these variants of the video in advance, so that when the video loads on the page, it's ready to be played with the given dimensions.
 **Apply eager transformations asynchronously** | `true` | Notify when the variants have been generated, rather than wait for these to be generated as part of the upload call.
 **Eager Notification URL** | `https://<your app's domain>/api/moderate` | The API endpoint for your app. To try out your app locally, you need to set up a secure tunnel connecting the internet to your locally running application so that the webhooks sent by Cloudinary on upload are caught and handled by the app. You can use a tool such as [Ngrok](https://ngrok.com/) to do this. Otherwise, you need to deploy the app using a service such as [Vercel](https://vercel.com/).

![Transform upload preset settings](https://cloudinary-res.cloudinary.com/image/upload/bo_1px_solid_gray/f_auto/q_auto/docs/ugc-video-langs-transform.png "thumb: c_scale,w_600/dpr_2.0, width:600, popup:true")

#### Manage and Analyze

{table:class=wide-2ndcol} Parameter | Value | Meaning
--|--|--
 **Auto chaptering** | `true` | Automatically create a VTT file of auto-generated chapters that the Cloudinary Video Player can display. 
 **Auto transcription** | `true` | Automatically create a transcription of the speech. 
 **Translate** | `French (French)`, `German`, `Spanish` | Automatically translate the transcription into French, German and Spanish. 

![Manage and analyze upload preset settings](https://cloudinary-res.cloudinary.com/image/upload/bo_1px_solid_gray/f_auto/q_auto/docs/ugc-video-langs-manage-and-analyze-fgs.png "thumb: c_scale,w_600/dpr_2.0, width:600, popup:true")

#### Optimize and Deliver

{table:class=wide-2ndcol} Parameter | Value | Meaning
--|--|--
 **Delivery type** | `Upload` | Make the video publicly available.

![Optimize and deliver upload preset settings](https://cloudinary-res.cloudinary.com/image/upload/bo_1px_solid_gray/f_auto/q_auto/docs/ugc-video-langs-optimize-and-deliver.png "thumb: c_scale,w_600/dpr_2.0, width:600, popup:true")

#### Advanced

{table:class=wide-2ndcol} Parameter | Value | Meaning
--|--|--
 **Notification URL** | `https://<your app's domain>/api/moderate` | The API endpoint for your app. To try out your app locally, you need to set up a secure tunnel connecting the internet to your locally running application so that the webhooks sent by Cloudinary on upload are caught and handled by the app. You can use a tool such as [Ngrok](https://ngrok.com/) to do this. Otherwise, you need to deploy the app using a service such as [Vercel](https://vercel.com/).

![Advanced upload preset settings](https://cloudinary-res.cloudinary.com/image/upload/bo_1px_solid_gray/f_auto/q_auto/docs/ugc-video-langs-advanced.png "thumb: c_scale,w_600/dpr_2.0, width:600, popup:true")

#### Addons

Add the **Rekognition AI Video Moderation** add-on.  Click **Options** to optionally set thresholds for each type of moderation.

Add the **Perception Point** add-on.

![Addons upload preset settings](https://cloudinary-res.cloudinary.com/image/upload/bo_1px_solid_gray/f_auto/q_auto/docs/ugc-video-langs-addons.png "thumb: c_scale,w_600/dpr_2.0, width:600, popup:true")

> **NOTES**:
>
> * Ensure you're [registered](https://console.cloudinary.com/app/settings/addons) to both of these add-ons.

> * You also need to register to the [Google Translation add-on](translation_addons) (as [mentioned](#try_it_out)) but the upload preset doesn't invoke this (it's invoked as part of the auto transcription).

## Deep dive  

If you want to learn how each of the features have been implemented in detail, expand the following sections:

### Architecture #### Technology stack

The app is built using the following technology:

* Next.js 14 (React framework)
* TypeScript
* Tailwind CSS
* Cloudinary React SDK and Upload widget
* AWS Rekognition Video Moderation (Cloudinary add-on)
* Perception Point (Cloudinary add-on)
* Google Translation (Cloudinary add-on)

#### Project structure

The Next.js application is built using `create-next-app` and the Next.js App Router, and makes use of standard routing conventions, such as `layout`, `page` and `route`.

##### Project files

The main project files are as follows:

{table:class=no-borders overview align-bullets} File | Functionality
--|--
[app/page.tsx](https://github.com/cloudinary-devs/video-review-demo/blob/main/app/page.tsx) | The main page, which displays the product and the customer reviews
[app/components/product-reviews.tsx](https://github.com/cloudinary-devs/video-review-demo/blob/main/app/components/product-reviews.tsx) | Allows users to add text reviews and video reviews as a component of the main pageHandles video uploads using Cloudinary's Upload widgetManages upload states and error handlingCommunicates with the moderation API
[app/components/cloudinary-video-player.tsx](https://github.com/cloudinary-devs/video-review-demo/blob/main/app/components/cloudinary-video-player.tsx) | Creates and configures an instance of the Cloudinary Video PlayerReturns a `video` element to render the Video Player
[app/config/cloudinary.ts](https://github.com/cloudinary-devs/video-review-demo/blob/main/app/config/cloudinary.ts) | Contains configuration details specific to the app instance
[app/api/moderate/route.ts](https://github.com/cloudinary-devs/video-review-demo/blob/main/app/api/moderate/route.ts) | Handles webhooks from Cloudinary when the following processing is complete: moderation, auto chaptering, auto transcription, eager transformationsHandles frontend requests checking the moderation status of an uploaded video

### Upload preset The upload preset specifies what happens when a customer uploads a video. 

In the case of this app, the upload preset:

* Generates a random value for the public ID of any uploaded video
* Applies an incoming transformation to limit the video's dimensions and duration
* Prepares variants of the video in advance, so it's ready for displaying on the page
* Makes the video publicly available
* Sets the app's `moderate` endpoint as the notification URL for the all the asynchronous video processing
* Requests auto chaptering, and auto transcription with translation
* Invokes the **Rekognition AI Video Moderation** and **Perception Point** add-ons.
   
See how to [configure the upload preset](#upload_preset_configuration).

### Upload widget The Cloudinary [Upload widget](upload_widget) provides a fully functional, configurable, graphical interface for uploading files to Cloudinary. 

In this app, it's used for the video reviews in the **ProductReviews** component.

#### Import the script

Import the Upload widget script in **layout.tsx**:

layout.tsx

```html
<Script src="https://upload-widget.cloudinary.com/global/all.js" strategy="beforeInteractive" />
```

#### Upload widget creation and configuration

The `createUploadWidget` function creates the Upload widget:

product-reviews.tsx

```React
const widget = window.cloudinary.createUploadWidget({
    cloudName: CLOUDINARY_CONFIG.cloudName,
    clientAllowedFormats: 'video',
    uploadPreset: CLOUDINARY_CONFIG.uploadPreset,
    sources: ["local"],
    multiple: false,
    maxFiles: 1,
    resourceType: "video",
  },
  (error: any, result: any) => {
    if (error) {
      console.error("Error in upload widget:", error)
      return
    }
    if (result && result.event === "success") {
      const newReviewWithVideo: Review = {
        id: result.info.asset_id,
        text: newReview,
        videoUrl: result.info.public_id,
        date: new Date().toLocaleDateString(),
        status: 'processing'
      }
      setReviews(prevReviews => [newReviewWithVideo, ...prevReviews])
      setNewReview("")
      checkVideoStatus(result.info.asset_id, result.info.public_id)
    }
  }
)
```

The configuration parameters are as follows:

{table:class=wide-2ndcol} Parameter | Value | Meaning
--|--|--
 **cloudName** | `CLOUDINARY_CONFIG.cloudName` | This is the name of product environment to which the widget uploads the videos. You need to change this for your environment in **cloudinary.ts**.
**clientAllowedFormats** | `video` | The widget allows customers to upload videos only.
**uploadPreset** | `CLOUDINARY_CONFIG.uploadPreset` | This is the name of the upload preset, which defines what happens on upload. Set this in **cloudinary.ts**.  See [Upload preset configuration](#upload_preset_configuration).
**sources** | `["local"]` | The customer can upload videos only from their local environment. You can change this to allow more [sources](upload_widget_reference#widget_parameters), if required.  
**multiple** | `false` | The customer can select one video only.
**maxFiles** | `1` | The customer can upload only one video at a time.

The callback function `(error: any, result: any) => ` handles the upload results:

* If the upload returns an error, then the error is logged.
* A successful upload starts the polling for the status of the video processing and [moderation check](#moderation_and_malware_detection).

#### Upload widget rendering

The Upload widget opens when the customer clicks the **Add Video Review** button:

product-reviews.tsx

```React
const handleUploadVideo = () => {
  if (uploadWidget) {
    uploadWidget.open()
  } else {
    console.error("Upload widget is not initialized")
  }
}

... 

<Button variant="secondary" onClick={handleUploadVideo} disabled={!isCloudinaryLoaded}>
  Add Video Review
</Button>
```

#### Upload widget state management

The `ProductReviews` component manages the state of the Cloudinary Upload widget instance:

product-reviews.tsx

```react
const [uploadWidget, setUploadWidget] = useState<any>(null)
```

This state is handled in few places:

**Initialization** - in the `useEffect` once the Cloudinary instance loads and on creation of the widget:

product-reviews.tsx

```react
  useEffect(() => {
    if (isCloudinaryLoaded && !uploadWidget) {
      initializeUploadWidget()
    }
  }, [isCloudinaryLoaded])

  const initializeUploadWidget = useCallback(() => {
    if (window.cloudinary && !uploadWidget) {
      const widget = window.cloudinary.createUploadWidget(
        // config options and handling of results...
      )
      setUploadWidget(widget)
    }
  }, [newReview])
```

**Usage** - in the click handler:

product-reviews.tsx

```react
const handleUploadVideo = () => {
  if (uploadWidget) {
    uploadWidget.open()
  } else {
    console.error("Upload widget is not initialized")
  }
}
```

### Moderation and malware detection When a customer uploads a video to Cloudinary, the [Amazon Rekognition Video Moderation](aws_rekognition_video_moderation_addon) and [Perception Point Malware Detection](perception_point_malware_detection_addon) add-ons are invoked in turn (as requested in the [upload preset](#addons)).

If either of these checks fails, the video isn't saved to the product environment, and the page displays an error message.

#### Moderation flow

The key stages in the moderation flow, as depicted in the diagram below, are:

* User actions trigger the Upload widget
* The widget uploads the video to Cloudinary
* Cloudinary processes the moderation and sends webhooks
* Frontend polls the moderation status
* If moderation passes and all the video processing is complete, the Video Player displays the video

![Video review sequence](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/docs/video-review-sequence.png "thumb: c_scale,w_700/dpr_2.0, width:700, popup:true")

#### Catch the webhooks from Cloudinary

On completing the moderation and malware checks, Cloudinary sends a webhook notification, which the backend route handler catches:

route.ts

```javascript
  if (notification_type === 'moderation' || notification_type === 'moderation_summary') {
    const { moderation_status, moderation_kind } = data

    if (moderation_status === 'rejected') {
      result.moderation = { 
        status: 'rejected', 
        message: moderation_kind === 'aws_rek_video' 
          ? 'Your video was rejected due to unsuitable content'
          : 'Your video was rejected due to potential malware'
      }
    } else if (moderation_status === 'approved') {
      result.moderation = { status: 'approved', message: 'Video approved' }
    }
  }
```

#### Poll the backend endpoint

On successful upload, the `checkStatus` function is called every five seconds to poll the backend endpoint, seeing if a moderation result has been returned for the asset. The poll times out after six minutes.

product-reviews.tsx

```react
  const checkVideoStatus = useCallback(async (assetId: string, publicId: string) => {
    const startTime = Date.now()
    const timeoutDuration = 360000 // 6 minutes in milliseconds

    const checkStatus = async () => {
      try {
        const response = await fetch('/api/moderate', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ checkStatus: true, asset_id: assetId, public_id: publicId })
        })
        const data = await response.json()

        if (data.status === 'approved') {
          setReviews(prevReviews => 
            prevReviews.map(review => 
              review.id === assetId 
                ? { ...review, status: 'approved', eagerTransformationComplete: data.eagerTransformationComplete }
                : review
            )
          )
        } else if (data.status === 'rejected') {
          setReviews(prevReviews => 
            prevReviews.map(review => 
              review.id === assetId 
                ? { ...review, status: 'rejected', rejectionReason: data.message }
                : review
            )
          )
        } else if (Date.now() - startTime < timeoutDuration) {
          // If still processing and within timeout, check again after 5 seconds
          setTimeout(() => checkStatus(), 5000)
        } else {
          // Timeout reached
          setReviews(prevReviews => 
            prevReviews.map(review => 
              review.id === assetId 
                ? { ...review, status: 'rejected', rejectionReason: 'Processing timeout reached' }
                : review
            )
          )
        }
      } catch (error) {
        console.error("Error checking video status:", error)
      }
    }

    checkStatus()
  }, [])
```

#### Handle the frontend polling requests

The backend endpoint handles the frontend polling requests to check the status.

If a moderation result is found and the status is `rejected`, a response is returned with the status set to  `rejected`.  If `approved`, the status of the auto chaptering, auto transcription and eager transformations is checked. If they're all complete, then a response is returned with the status set to `approved`.

route.ts

```javascript
  // If it's a request from our frontend
  if (data.checkStatus) {
    const processingResult = processingResults.get(asset_id)

    if (!processingResult) {
      return NextResponse.json({ status: 'pending', message: 'Processing in progress' })
    }

    const { moderation, autoChaptering, autoTranscription, eagerTransformation } = processingResult

    if (moderation.status === 'rejected') {
      return NextResponse.json({ status: 'rejected', message: moderation.message })
    }

    if (moderation.status === 'approved' && 
        (autoChaptering.status === 'complete' || autoChaptering.status === 'failed') &&
        (autoTranscription.status === 'complete' || autoTranscription.status === 'failed') &&
        eagerTransformation.status === 'complete') {
      // Clear the result from our temporary storage
      processingResults.delete(asset_id)

      return NextResponse.json({ 
        status: 'approved',
        publicId: public_id,
        autoChaptering: autoChaptering.status === 'complete',
        autoTranscription: autoTranscription.status === 'complete',
        eagerTransformationComplete: true
      })
    }

    return NextResponse.json({ status: 'pending', message: 'Processing in progress' })
  }

  return NextResponse.json({ success: true })


```

### Video transcription and localization When you set the **Auto transcription** parameter in the [upload preset](video_review_sample_project#manage_and_analyze), Cloudinary automatically produces [transcripts of videos](video_transcription) with speech when they're uploaded. The Video Player displays the transcription as captions when you configure `captions` in the Video Player configuration.

Similarly, Cloudinary [translates the transcription](video_transcription#requesting_translation) to the languages that you specify in the **Translate** parameter. The Video Player displays the translations as subtitles when you configure `subtitles` in the Video Player configuration.

The Video Player doesn't display the video until the transcriptions and translations are ready (or fail, for example, due to no speech in the video clip). See **Handle the frontend polling requests** in the [Moderation and malware detection](#moderation_and_malware_detection) section.

Here's the code used to configure the cloudinary Video Player:

video-player.tsx

```react
  useEffect(() => {
    if (isScriptLoaded && videoRef.current && window.cloudinary && window.cloudinary.videoPlayer) {
      const player = window.cloudinary.videoPlayer(videoRef.current, {
        cloud_name: CLOUDINARY_CONFIG.cloudName,
        controls: true,
        chaptersButton: true,
        width: 600,
        sourceTypes: ['webm','mp4'],
        posterOptions: {aspect_ratio: "16:9", crop: "fill", gravity: "auto", width: 600}
      })

      player.source(
        publicId, {
        transformation: [{aspect_ratio: "16:9", crop: "fill", gravity: "auto", width: 600}],
        textTracks: {
          captions: {
            label: 'English (captions)',
            default: true,
            maxWords: 5,
          },
          subtitles: [
            {
              label: 'French',
              language: 'fr-FR',
            },
            {
              label: 'Spanish',
              language: 'es',
            },
            {
              label: 'German',
              language: 'de',
            }
          ]
        },
        chapters: true
      })

      

      return () => {
        player.dispose()
      }
    }
  }, [isScriptLoaded, publicId])
```

### Video chaptering When you set the **Auto chaptering** parameter in the [upload preset](video_review_sample_project#manage_and_analyze), Cloudinary automatically generates a VTT file containing chapter headings of a video with speech when it's uploaded. The Video Player shows chapter markers on the seek bar and the chapter titles appear in the control bar when you configure `chapters` in the Video Player configuration.

The Video Player doesn't display the video until the VTT is ready (or fails, for example, due to no speech in the video clip). See **Handle the frontend polling requests** in the [Moderation and malware detection](#moderation_and_malware_detection) section.

### Video transformations The **Customer Reviews** area allows for a 600 pixel width video with an aspect ratio of 16:9. If a customer uploads a video with different dimensions, Cloudinary resizes the video to those dimensions, cropping out parts of the video where necessary. To keep the most important part of the video in the result, the transformation includes the [automatic gravity](video_resizing_and_cropping#automatic_gravity_for_crops_g_auto) option.  

As this transformation can take a while to generate, it's requested '[eagerly](eager_and_incoming_transformations#eager_transformations)' on upload through the [upload preset](#transform) and takes into account MP4 and WEBM formats:

```
ar_16:9,c_fill,g_auto,w_600/mp4|ar_16:9,c_fill,g_auto,w_600/webm
```

 With the **Eager Notification URL** set to the `moderate` endpoint, the Video Player doesn't display the video until these variants are ready. See **Handle the frontend polling requests** in the [Moderation and malware detection](#moderation_and_malware_detection) section.

 Configure the Video Player with the `width`, `sourceTypes`, `posterOptions` and `transformation` parameters to match the eager transformation:  

video-player.tsx

```react
  useEffect(() => {
    if (isScriptLoaded && videoRef.current && window.cloudinary && window.cloudinary.videoPlayer) {
      const player = window.cloudinary.videoPlayer(videoRef.current, {
        cloud_name: CLOUDINARY_CONFIG.cloudName,
        controls: true,
        chaptersButton: true,
        width: 600,
        sourceTypes: ['webm','mp4'],
        posterOptions: {aspect_ratio: "16:9", crop: "fill", gravity: "auto", width: 600}
      })

      player.source(
        publicId, {
        transformation: [{aspect_ratio: "16:9", crop: "fill", gravity: "auto", width: 600}],
        ...
```

 

  sequenceDiagram
      participant User
      participant CloudinaryUploadWidget
      participant Frontend as ProductReviews Component
      participant Backend as Moderation API Route
      participant CloudinaryWebhook

      User->>CloudinaryUploadWidget: Upload Video
      CloudinaryUploadWidget->>Frontend: Upload Success
      Frontend->>Backend: Check Video Status
      Frontend->>Backend: Periodically Poll Status

      note over CloudinaryWebhook, Backend: Moderation Webhook
      CloudinaryWebhook->>Backend: Send Moderation Notification
      Backend--&gt;>Backend: Update Moderation Status

      note over CloudinaryWebhook, Backend: Auto Chaptering Webhook
      CloudinaryWebhook->>Backend: Send Auto Chaptering Notification
      Backend--&gt;>Backend: Update Chaptering Status

      note over CloudinaryWebhook, Backend: Auto Transcription Webhook
      CloudinaryWebhook->>Backend: Send Auto Transcription Notification
      Backend--&gt;>Backend: Update Transcription Status

      note over CloudinaryWebhook, Backend: Eager Transformation Webhook
      CloudinaryWebhook->>Backend: Send Eager Transformation Notification
      Backend--&gt;>Backend: Update Transformation Status
      
      alt Moderation Rejected
          Backend->>Frontend: Return Rejection Status
          Frontend->>User: Display Rejection Reason
      else Moderation Approved
          alt All Processing Complete
              Backend->>Frontend: Return Approved Status
              Frontend->>User: Display Approved Video Review
          else Processing Incomplete
              Backend->>Frontend: Return Pending Status
              Frontend->>User: Show Processing Indicator
          end
      end

-->