> ## Content Index
> Fetch the complete content index at: https://blog.addpipe.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Build a Video Recording Form: Capture, Preview and Upload.
- URL: https://blog.addpipe.com/video-recording-form-javascript/
- Published: 2026-11-12T10:24:32.000Z
- Updated: 2026-11-12T10:24:32.000Z
- Description: A practical guide to recording video inside a form, keeping it linked to the response and confirming upload without confusing receipt with processing.
- Author: Victor Cucu
- Tags: Technology

A user records an answer, watches it back and clicks Submit. Your form displays “Thanks for your response.” But did the server receive the video, and can your application connect it to the right answer?

A video recording form needs to coordinate browser capture, file creation, form validation and upload confirmation. A working camera preview only proves the first part.

This guide builds the workflow for a short recorded response inside a custom HTML form. It explains the decisions that prevent empty recordings, duplicate submissions and success messages shown too early. Use it for customer feedback, research responses or candidate answers in a product you control. 

## How do you add video recording to an HTML form?

Use `getUserMedia()` to obtain camera and microphone tracks, `MediaRecorder` to encode them into media data, and `FormData` to send the completed recording with the form fields. Enable submission only when a usable recording exists, and confirm success only after your server returns the expected receipt.

Alternatively, embed a recording client such as AddPipe and submit a recording reference with the form. Your application still owns response association, validation and the message shown after submission.

The native-browser example below records first and uploads after review. Some managed recording paths stream data while capture is running; their event sequence differs.

The accompanying example contains `index.html`, `recorder.js` and `server.mjs`. Start it with Node.js 22 or newer:

```Bash
node server.mjs

```

Open `http://localhost:8080`. The server saves submissions locally. This is a learning example, not a production upload service. The excerpts below explain the important parts; the download contains the connected implementation.

## 1\. Define what “submitted” means.

Keep the recording’s state separate from the form’s state. A person can finish recording while required fields are incomplete. A file can finish uploading while processing is still pending.

For this example, submission is confirmed when the server has written both the media file and response metadata, then returned a response ID. It does not claim that the file has been decoded, converted or approved for playback.  

| State               | What you can safely say                                                                     |
| ------------------- | ------------------------------------------------------------------------------------------- |
| Recording           | Camera and microphone data is being captured. Nothing here confirms receipt by your server. |
| Finalizing          | The recorder is delivering the last media data. Keep Submit disabled.                       |
| Review              | A completed local Blob is available for preview. It has not necessarily left the browser.   |
| Uploading           | A request is in flight. Keep the recording available until a receipt is validated.          |
| Upload failed       | Receipt was not confirmed. Preserve the take and offer a retry.                             |
| Received            | The server reached the agreed persistence milestone and returned a response ID.             |
| Processing or ready | Track these backend states separately. Do not infer readiness from upload completion.       |

![In this example, the upload begins after review. Streaming recording paths may overlap capture and upload.](https://blog.addpipe.com/content/images/2026/11/02-recording-form-states.png)

Use explicit button states. Disable Record while a permission request is pending, disable Submit during recording and finalization, and prevent a second submission while an upload is in flight.

Announce state changes in a visible `role="status"` region so the interface does not rely on a changing button color.

## 2\. Request the camera when the user chooses to record.

Request access from a clearly labelled Record action. Tell the person what will be captured and how to stop. Start with reasonable preferences rather than mandatory high-resolution constraints:

```javascript
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    width: { ideal: 640 },
    height: { ideal: 480 }
  },
  audio: true
});
```

`ideal` expresses a preference, not a guarantee. This example requires both a camera and a microphone; an audio-only alternative needs its own constraints and recorder configuration.

Camera and microphone access requires a secure context, normally HTTPS; localhost is suitable for local development. Embedded forms also need the appropriate camera and microphone permissions policy. See the [getUserMedia reference](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) for these requirements. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)

If access fails, preserve written answers and give a useful next step. Permission refusal, no matching device and an unavailable device are different problems. AddPipe’s [common getUserMedia errors guide](https://blog.addpipe.com/common-getusermedia-errors/) covers the error families in more detail.

A permission request can remain unanswered. Do not start several requests because someone clicks Record repeatedly. If your application offers cancellation while access is pending, discard and stop any stream that resolves after cancellation.

## 3\. Select a recording format without pretending to convert it.

The browser’s recording format is an implementation detail your upload service must accept. Choose a supported format at runtime:

```javascript
const candidates = [
  'video/webm;codecs=vp8,opus',
  'video/mp4'
];

const mimeType = candidates.find((type) =>
  MediaRecorder.isTypeSupported(type)
);

const recorder = mimeType
  ? new MediaRecorder(stream, { mimeType })
  : new MediaRecorder(stream);
```

This is a small candidate list, not a complete compatibility matrix. A positive [isTypeSupported() result](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/isTypeSupported%5Fstatic) does not guarantee that recording will succeed under current resource conditions. Handle constructor failures and recording errors too. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/isTypeSupported%5Fstatic)

Use the resulting media type to choose a filename extension. Giving WebM data an `.mp4` filename does not make it MP4\. When your application needs a normalized output, that is a separate media-processing job.

## 4\. Wait for the final recording data before building the preview.

Calling `stop()` is not the point to immediately assemble and upload the file. The recorder delivers its final `dataavailable` event before the `stop` event. Assemble the completed recording in the stop handler, after collecting the data.

The following excerpt assumes a recorder and stream already exist. In the full example, recording errors prevent submission and the interface enters a review state only after these checks pass. 

The [MediaStream Recording specification](https://www.w3.org/TR/mediastream-recording/?utm%5Fsource=chatgpt.com) also makes an important distinction: individual chunks need not be independently playable. Do not assume every `dataavailable` Blob is a complete video file. [w3.org](https://www.w3.org/TR/mediastream-recording/?utm%5Fsource=chatgpt.com)

A `timeslice` value can request periodic data events, but it does not upload those chunks, guarantee exact intervals or cap total memory when you retain every chunk. See the [dataavailable documentation](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/dataavailable%5Fevent?utm%5Fsource=chatgpt.com). [MDN](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/dataavailable%5Fevent?utm%5Fsource=chatgpt.com)

The short local example holds one recording in memory and applies a 60-second interface limit. Browser scheduling means that timer is not a strict server-enforced duration limit.

For preview, assign an object URL to a video element with controls. Clear its live `srcObject` and unmute playback so the person can check their answer. A `blob:` URL is a local browser reference; storing it in your database will not give reviewers access to the recording.

Release camera and microphone tracks when capture ends. Revoke the old object URL when replacing or discarding a preview. These are different cleanup operations: [stopping tracks](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop?utm%5Fsource=chatgpt.com) releases this stream’s use of the devices; [revoking an object URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL%5Fstatic?utm%5Fsource=chatgpt.com) releases its browser reference to the Blob. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop?utm%5Fsource=chatgpt.com)

## 5\. Keep the answer and its recording together

A recording needs an identity before an upload can be retried safely.

In the local example, each take gets an `attemptId`. The answer and video travel in one multipart request. The server creates the final `responseId` and returns it in the receipt. The same take retains its attempt ID when a failed upload is retried; recording a replacement creates a new attempt ID.

In a production application, associate the attempt with a server-controlled response or application record and the authenticated user or authorized invitation. A UUID in a hidden field is useful for correlation, but it does not prove ownership.

For example, a research platform can link the recording to a specific response and question. An ATS can link it to an authorized candidate answer. Avoid putting names, email addresses or sensitive answers into filenames just to establish that link.

If you upload files directly to object storage, create the authorized response record first. Issue a short-lived upload authorization for its object key, then verify the upload before attaching the object to the final submission. Keep storage credentials on the server.

## 6\. Upload the completed Blob and validate the receipt.

Use `FormData` for the media and ordinary fields. Let the browser set the multipart boundary; do not manually set the request’s `Content-Type` header.

This excerpt runs inside the submit handler, after recording and form validation. `blob`, `attemptId` and `answer` come from the form controller:

```javascript
const body = new FormData();

const extension = blob.type.split(';')[0] === 'video/mp4'
  ? 'mp4'
  : 'webm';

body.append('attemptId', attemptId);
body.append('answer', answer);
body.append('recording', blob, `response.${extension}`);

const response = await fetch('/api/responses', {
  method: 'POST',
  body,
  credentials: 'same-origin'
});

if (!response.ok) {
  throw new Error(`Upload returned HTTP ${response.status}.`);
}

const receipt = await response.json();

if (
  receipt.attemptId !== attemptId ||
  receipt.status !== 'received' ||
  !receipt.responseId
) {
  throw new Error('The server did not return the expected receipt.');
}
```

The browser rules are documented in [Using FormData Objects](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest%5FAPI/Using%5FFormData%5FObjects) and [Using Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch%5FAPI/Using%5FFetch). A fulfilled fetch promise alone is insufficient: HTTP errors such as 413 or 500 still require an explicit status check. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Fetch%5FAPI/Using%5FFetch)

The receipt is an application contract, not a built-in browser feature. Return it only after the server reaches the persistence milestone you promise. If the server merely queues a job, say that it accepted or queued the upload.

Do not display an invented upload percentage. The fetch call above does not report upload progress. Show an indeterminate uploading state, or use a transport with upload progress events if a percentage is needed. Bytes sent still do not prove that the server committed the response.

## 7\. Retry without forcing the person to record again.

An upload can fail after the server has saved the file but before its receipt reaches the browser. A timeout cannot tell you which side of that boundary failed.

Keep the Blob, written answer and attempt ID available after an upload error. Let the person retry the same attempt. On the server, make repeated requests resolve to the same response rather than inserting another answer each time.

Scope the idempotency key to the authorized response, enforce uniqueness and reject reuse with different content. In production, keep that mapping in durable storage and handle concurrent requests. Disabling the Submit button helps the interface; it does not provide server-side idempotency.

![If the response is lost after a save, retrying the same attempt should not create a second submission.](https://blog.addpipe.com/content/images/2026/11/03-recording-upload-retry.png)

The example preserves media in the current page’s memory. Refreshing or closing the page loses that local copy. Cross-session recovery needs an explicit storage and retention design, including the privacy implications of keeping recordings on a device.

## 8\. Know what the example does not validate.

The local server limits request size, checks required fields and the declared media type, generates its own filenames, and writes a receipt after saving the media and metadata.

Its retry map lasts only for the server process. It does not decode media, scan uploads, enforce duration from file contents or implement a production database transaction.

Before public deployment, add authentication or authorized invitation handling, CSRF protection where applicable, rate limits and storage quotas. Inspect uploaded media rather than trusting its filename or client-supplied MIME type. Keep untrusted files outside executable/public application paths and define who can access them and when they are deleted.

For larger recordings, avoid buffering every upload in your application server. Evaluate streaming request handling or authorized direct-to-storage uploads, with cleanup for incomplete and abandoned attempts. 

| Scenario                                                              | Expected result                                                                                    |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Permission refused or left unanswered                                 | Written fields remain intact. The form does not issue overlapping capture requests.                |
| Record, stop and record again                                         | The preview belongs to the latest take. Old tracks and object URLs are released.                   |
| Final media data arrives after Stop is clicked                        | Submission waits for finalization, not merely the button click.                                    |
| Device ends or an encoding error occurs                               | No empty or known-failed take is silently accepted.                                                |
| Upload returns 413, 500 or a timeout                                  | No success message. Preserve the recording and explain the next step.                              |
| Server saves, but its response is lost                                | Retry returns the original response using the same authorized attempt key.                         |
| Two requests arrive for the same attempt                              | Server-side uniqueness prevents duplicate responses.                                               |
| User records a replacement or edits a previously attempted submission | Use a new attempt for changed content, or explicitly reject conflicting key reuse.                 |
| Upload completes before processing                                    | Confirm receipt only. Show playable or delivered status when the required backend work finishes.   |
| Mobile device locks, app switches or page refreshes                   | Document what is recoverable on target devices. Do not promise persistence from an in-memory Blob. |

## Using AddPipe instead of owning the recording pipeline.

The native example shows the browser mechanics. If your team wants to keep its own form while using managed capture and processing, start with AddPipe’s [custom-form integration](https://addpipe.com/integrations/forms/custom-form/?utm%5Fsource=chatgpt.com) and [form-with-recorder demo](https://addpipe.com/embed-code-v2-demos/custom-form/?utm%5Fsource=chatgpt.com). For a React application, use the separate [React integration guide](https://blog.addpipe.com/integrating-pipe-with-react-via-npm/?utm%5Fsource=chatgpt.com).

Attach your response reference through [custom metadata](https://addpipe.com/docs/custom-data/custom-data/?utm%5Fsource=chatgpt.com) and store the recording identifier against the authorized response. Treat browser-supplied identifiers as claims to verify, not authorization. [Pipe Docs](https://addpipe.com/docs/custom-data/custom-data/?utm%5Fsource=chatgpt.com)

For the recording client, `onSaveOk` confirms that recording data has reached AddPipe’s media servers for processing. It does not confirm completed conversion or storage delivery. File uploads and the legacy mobile recorder have different callbacks; choose the events for the path you enable in the [JavaScript API reference](https://addpipe.com/docs/javascript/js-api-v2/). [Pipe Docs](https://addpipe.com/docs/javascript/js-api-v2/?utm%5Fsource=chatgpt.com)

Update later processing and delivery states on your backend using the relevant [recording webhooks](https://addpipe.com/docs/webhooks/setting-up-webhooks/). For example, conversion and copying to a storage destination are distinct events. Verify [webhook signatures](https://addpipe.com/docs/webhooks/signing-webhooks/) and handle repeated notifications without duplicating application actions. [Pipe Docs](https://addpipe.com/docs/webhooks/setting-up-webhooks/)

Your form can acknowledge that a response was received while the application tracks processing separately. That gives the person a clear completion point without claiming that a playable file is already available.

**Want recorded answers inside your existing form?** Explore the [AddPipe recording client](https://addpipe.com/recording-client/) or [start a 14-day free trial](https://dashboard.addpipe.com/signup). No credit card required.