Video Recording in Oracle APEX
Introduction:-
Oracle APEX allows seamless integration of modern web technologies to enhance application capabilities. This document explains how to implement client-side video recording in Oracle APEX using JavaScript and browser Media APIs. The solution enables users to record video directly from the browser, preview it, retry if required, and securely save the recording into the database.
The following technologies are used to achieve this solution:
- Oracle APEX
- JavaScript (MediaRecorder API)
- HTML,CSS
- AJAX Callback (PL/SQL)
Why We Need to Do This?
1.Capture Real-Time User Evidence:-
Certain business processes require live video capture instead of file uploads to ensure authenticity and prevent misuse.
2.Improve User Experience:-
Users can record, preview, retry, and submit videos directly within the application without using external tools.
3.Secure and Controlled Storage:-
Videos are stored securely as BLOBs in the database with proper metadata such as filename, duration, and MIME type.
4.Reduce Dependency on Third-Party Tools:-
The entire implementation relies on native browser and Oracle APEX capabilities, avoiding external services.
5.Support Compliance and Audit Requirements:-
Video recordings can be used as verifiable proof for audits, compliance checks, and validations.
How Do We Solve It?
Below are the step-by-step instructions to implement video recording in Oracle APEX.
Step 1: Create a Static Content Region:-
Create a Static Content region and add HTML markup to display the video preview area, recorded video section, control buttons (Start, Stop, Save, Retry), and status messages.
HTML Code
<div id="recorderUI" class="t-Form--stretchInputs"> <div id="previewCard" class="video-card"> <div class="card"> <label class="t-Form-label">Preview</label> <div class="video-player"> <video id="preview" playsinline autoplay muted></video> </div> </div> </div> <div id="recordedCard" class="video-card" style="display:none;"> <div class="card"> <label class="t-Form-label">Recorded Clip</label> <div class="video-player"> <video id="playback" controls></video> </div></div> </div> <div id="recordButtons" class="t-ButtonRegion t-ButtonRegion--noBorder"> <button id="btnStart" type="button" class="t-Button t-Button--hot">Start</button> <button id="btnStop" type="button" class="t-Button" disabled>Stop</button> <span id="timer" style="font-weight:600;margin-left:12px;"></span> </div> <div id="postButtons" class="t-ButtonRegion t-ButtonRegion--noBorder" style="display:none;"> <button id="btnSave" type="button" class="t-Button t-Button--primary">Save</button> <button id="btnRetry" type="button" class="t-Button t-Button--danger">Retry</button> </div> <div id="status" class="u-success-text" style="margin-top:8px;"></div> </div>
Step 2: Add JavaScript for Camera Access and Recording in Function and Gloabal Declaration
Use the navigator.mediaDevices.getUserMedia API to access the
camera and microphone. The MediaRecorder API is used to start and
stop video recording while capturing video chunks.
Javascript Code
(function () {
// Elements
const preview = document.getElementById('preview');
const playback = document.getElementById('playback');
const previewCard = document.getElementById('previewCard');
const recordedCard = document.getElementById('recordedCard');
const btnStart = document.getElementById('btnStart');
const btnStop = document.getElementById('btnStop');
const btnSave = document.getElementById('btnSave');
const btnRetry = document.getElementById('btnRetry');
const recordButtons = document.getElementById('recordButtons');
const postButtons = document.getElementById('postButtons');
const statusEl = document.getElementById('status');
const timerEl = document.getElementById('timer');
// Recording state
let mediaStream = null;let mediaRecorder = null;
let chunks = [];let startTime = null; let timerInterval = null;
// Last recording
let lastRecording = {
blob: null, url: null, mime: null, durationMs: 0, filename: null
};
function formatMs(ms) {
const s = Math.floor(ms / 1000);
const m = Math.floor(s / 60);
const ss = s % 60;
return `${String(m).padStart(2,'0')}:${String(ss).padStart(2,'0')}`;
}
async function ensureCamera() {
if (mediaStream) return;
try {
mediaStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 720 } }
});
preview.src
Object = mediaStream;
} catch (err) {
statusEl.className = 'u-warning-text';
statusEl.textContent = `Camera/Mic access failed: ${err.message}`;
throw err;
} }
function setViewRecording() {
// Show Preview + Start/Stop, hide Recorded + Save/Retry previewCard.style.display = '';
recordButtons.style.display = '';
recordedCard.style.display = 'none';
postButtons.style.display = 'none';
}
function setViewPost() {
// Show Recorded + Save/Retry, hide Preview + Start/Stop
previewCard.style.display = 'none';
recordButtons.style.display = 'none';
recordedCard.style.display = '';
postButtons.style.display = '';
}
function startRecording() {
chunks = [];
const options = { mimeType: 'video/webm;codecs=vp8,opus' };
try {
mediaRecorder = new MediaRecorder(mediaStream, options);
} catch (e) {
mediaRecorder = new MediaRecorder(mediaStream);
}
mediaRecorder.ondataavailable = (e) => {
if (e.data && e.data.size > 0) chunks.push(e.data);
};
mediaRecorder.onstop = async () => {
// Build Blob and URL
const blob = new Blob(chunks, { type: mediaRecorder.mimeType || 'video/webm' });
const url = URL.createObjectURL(blob);
const durationMs = Date.now() - startTime;
// Save state
lastRecording.blob = blob;
lastRecording.url = url;
lastRecording.mime = blob.type || 'video/webm';
lastRecording.durationMs = durationMs;
lastRecording.filename = `recording_${new Date().toISOString().replace(/[:.]/g,'-')}.webm`;
// Update timer
clearInterval(timerInterval);
timerEl.textContent = formatMs(durationMs);
// Show recorded view
playback.src = url;
playback.load();
playback.play().catch(() => {});
statusEl.className = '';
statusEl.textContent = 'Recording ready. You can Save or Retry.';
setViewPost();
// Revoke temporary URL later
setTimeout(() => {
if (lastRecording.url) URL.revokeObjectURL(lastRecording.url);
}, 60000);
};
// Start recording
mediaRecorder.start(500);
startTime = Date.now();
timerEl.textContent = '00:00';
timerInterval = setInterval(() => {
timerEl.textContent = formatMs(Date.now() - startTime);
}, 200);
// Buttons
btnStart.disabled = true; btnStop.disabled = false;
statusEl.className = ''; statusEl.textContent = 'Recording...';
// Ensure preview is visible while recording
setViewRecording();
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop(); btnStop.disabled = true;
btnStart.disabled = false; statusEl.textContent = 'Stopping...';
}
}
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
}); }
async function saveRecording() {
If (!lastRecording.blob) {
apex.message.alert('No recording to save. Please record and stop first.');
return; }
statusEl.className = ''; statusEl.textContent = 'Uploading video...';
try {
const base64 = await blobToBase64(lastRecording.blob);
const payload = base64.split(',')[1];
apex.server.process(
'SAVE_VIDEO', {
x01: lastRecording.filename,x02: lastRecording.mime,
x03: payload, x04: String(lastRecording.durationMs) },
{
dataType: 'json',
success: function () {
statusEl.className = 'u-success-text';
statusEl.textContent = 'Saved successfully.';
apex.message.showPageSuccess('Video saved.');
// Optional: after save, return to recording view ready for a new recording
resetRecordingState();
setViewRecording(); },
error: function (req, status, err) {
statusEl.className = 'u-warning-text';
statusEl.textContent = 'Upload failed.';
apex.message.alert('Upload failed: ' + err);
} } );
} catch (e) {
statusEl.className = 'u-warning-text';
statusEl.textContent = 'Processing failed.';
}}
function resetRecordingState() {
// Clear last recording, clear playback, reset timer/status
if (lastRecording.url) {
try { URL.revokeObjectURL(lastRecording.url); } catch (e) {}
}
lastRecording = { blob: null, url: null, mime: null, durationMs: 0, filename: null }; playback.removeAttribute('src');
playback.load();
timerEl.textContent = ''; statusEl.textContent = '';
}
async function retryRecording() {
// Discard current recording and go back to preview
resetRecordingState();
setViewRecording();
// Ensure camera is active and preview shows stream
try {
await ensureCamera();
// Re-enable Start / disable Stop
btnStart.disabled = false; btnStop.disabled = true;
statusEl.textContent = 'Ready to record.';
} catch (e) {
// If camera fails, show warning
}
}
// Wire events
btnStart.addEventListener('click', async () => {
try { await ensureCamera(); } catch (e) { return; } startRecording();
});
btnStop.addEventListener('click', () => { stopRecording(); });
btnSave.addEventListener('click', () => { saveRecording(); }); btnRetry.addEventListener('click', () => { retryRecording(); });
// Initial view: preview + start/stop
setViewRecording();
})();
Step 3: Add CSS Code to Handle Recording State and UI Transitions
CSS Code
#recorderUI .video-card .card {
padding: 8px;border-radius: 6px;
}
#recorderUI .video-player {
width: 260px; height: 146px;
background: #000;border-radius: 6px;overflow: hidden;
}
#recorderUI video {
width: 100%; height: 100%; object-fit: cover;
}
#recorderUI .t-ButtonRegion {
display: flex;align-items: center;
gap: 6px; margin-top: 6px;
}
#recorderUI .t-Form-label {
font-size: 0.9rem; margin-bottom: 6px;
}
Step 4: Save Video Using AJAX Callback
Use apex.server.process to send the Base64 data and metadata to an AJAX Callback. The PL/SQL process decodes the Base64 content and stores it as a BLOB in the database table.
PLSQL Code
DECLARE
l_blob BLOB;
l_filename VARCHAR2(255) := apex_application.g_x01;
l_mime VARCHAR2(100) := apex_application.g_x02;
l_b64 CLOB := apex_application.g_x03;
l_duration NUMBER := TO_NUMBER(apex_application.g_x04);
BEGIN
l_blob := apex_web_service.clobbase642blob(l_b64);
INSERT INTO video_capture (filename, mime_type, blob_content, duration_ms, created_by)VALUES (l_filename, l_mime, l_blob, l_duration, v('APP_USER'));
apex_json.open_object;
apex_json.write('status', 'ok');
apex_json.close_object;
EXCEPTION
WHEN OTHERS THEN
apex_json.open_object;
apex_json.write('status', 'error');
apex_json.write('message', SQLERRM);
apex_json.close_object;
END;
Conclusion:-
Client-side video recording in Oracle APEX enhances application functionality by enabling real-time video capture directly within the browser. This approach improves user experience, ensures data authenticity, and supports compliance-driven use cases. The solution is reusable and can be extended for multiple business scenarios requiring video evidence.
Output:-
This video recording region allows users to preview the camera feed and start recording by clicking the Start button.

This screen shows the live camera preview while the video recording is in progress, along with the recording timer and status.

This screen displays the recorded video clip, allowing the user to preview it and either save the recording or retry.
