Skip to main content

Screen Recorder in Oracle APEX (Single Page)

Oracle APEX Tutorial

Screen Recorder in Oracle APEX :
Single Page

Build a fully functional browser-based screen recorder inside Oracle APEX using just one Static Content region and native JavaScript. No plugins, no external libraries, no server uploads required.

✍️ Why I Built This

I was working on a client project where the support team needed to record screen issues and share them directly from the APEX application, without switching to any external tool. Installing third-party software was not an option on their machines, and every screen recorder extension required IT approval.

That is when I thought: the browser already has everything we need. Why not build it right inside APEX? That idea turned into this.

🎬 Start / Stop Recording
👁 Instant Preview
⬇️ One-click Download
🔊 Audio + Video
🚫 No Plugins Needed
📅 Coming Soon: A Multi-Page Screen Recorder with advanced controls, session management, and upload to APEX collections will be covered in an upcoming blog post. Stay tuned!
Step 01
Static Content Region
Step 02
HTML UI Code
Step 03
JavaScript Functions
💼 Real Use Case

One of our end users was trying to report a bug in an APEX form: a validation error that only appeared under a very specific sequence of steps. Describing it over a chat message took 20 minutes and still was not clear. With this recorder built into the same APEX page, she clicked Start Recording, reproduced the issue in 30 seconds, clicked Stop, and downloaded the video. The developer understood it immediately.

That is the kind of problem this solves: not just "screen recording" as a feature, but removing friction from real day-to-day workflows inside APEX.

1

Create a Static Content Region

In APEX Page Designer, add a new region and configure it as a Static Content type. This is where the recorder UI (buttons, video preview, and status text) will live.

Page Designer Regions Create Region Type: Static Content
Region TypeStatic Content
TitleScreen Recorder (or any label you prefer)
TemplateStandard or Blank, your choice
Source / HTML CodePaste the HTML from Step 2 here
💡 This entire recorder runs 100% client-side. The MediaRecorder API is built into all modern browsers. No APEX plug-ins, no PL/SQL, and no server storage are needed for this single-page version.
2

HTML Code: Region Source

Paste this HTML into the region's Source > HTML Code attribute. It renders the Start and Stop buttons, a status label, a hidden video preview, and a download button. All elements are shown or hidden dynamically by the JavaScript in Step 3.

HTML
<div id="apexScreenRecorder">

  <button type="button"
          id="startBtn"
          class="t-Button t-Button--primary"
          onclick="startRec()">
    Start Recording
  </button>

  <button type="button"
          id="stopBtn"
          class="t-Button t-Button--danger"
          onclick="stopRec()"
          disabled>
    Stop
  </button>

  <span id="recStatus"
        style="margin-left:12px;font-size:13px;color:gray;">
    Idle
  </span>

  <br><br>

  <video id="previewVideo"
         controls
         style="width:100%;display:none;border-radius:8px;">
  </video>

  <br>

  <button type="button"
          id="downloadBtn"
          class="t-Button"
          onclick="downloadRec()"
          style="display:none;margin-top:8px;">
    Download Recording
  </button>

</div>
startBtnTriggers startRec(), requests screen share permission and begins recording
stopBtnTriggers stopRec(), stops MediaRecorder and stream; starts as disabled
recStatusLive text label showing: Idle / Recording... / Done / Permission denied
previewVideoHidden until recording stops; auto-fills with the recorded blob URL
downloadBtnHidden until recording stops; triggers downloadRec() to save the .webm file
3

JavaScript: Function and Global Variable Declaration

Paste this into Page > Function and Global Variable Declaration. This declares all global state variables and the three recorder functions that the HTML buttons call.

Page Designer Page Function and Global Variable Declaration
🐛 Bug I Faced

I initially put the JavaScript inside Execute when Page Loads and the functions worked fine the first time. But after navigating away and coming back (APEX partial page refresh), calling startRec() threw a ReferenceError: startRec is not defined. It took me a while to figure out why.

The problem was that Execute when Page Loads wraps your code inside an anonymous function scope, so startRec never becomes a global and the HTML onclick attributes cannot reach it after a refresh.

Moving everything to Function and Global Variable Declaration fixed it completely. That section places your code in the true global scope, exactly where onclick handlers expect to find named functions.
JavaScript
var mediaRecorder, recChunks = [], recStream, recBlobUrl;

function startRec() {
  navigator.mediaDevices.getDisplayMedia({ video: true, audio: true })
    .then(function(stream) {

      recStream  = stream;
      recChunks  = [];
      mediaRecorder = new MediaRecorder(stream);

      mediaRecorder.ondataavailable = function(e) {
        if (e.data.size > 0) recChunks.push(e.data);
      };

      mediaRecorder.onstop = function() {
        var blob = new Blob(recChunks, { type: 'video/webm' });
        recBlobUrl = URL.createObjectURL(blob);

        var vid = document.getElementById('previewVideo');
        vid.src           = recBlobUrl;
        vid.style.display = 'block';

        document.getElementById('downloadBtn').style.display = 'inline-block';
        document.getElementById('recStatus').textContent    = 'Done';
      };

      mediaRecorder.start(100);

      document.getElementById('startBtn').disabled = true;
      document.getElementById('stopBtn').disabled  = false;
      document.getElementById('recStatus').textContent = 'Recording...';

      stream.getVideoTracks()[0].onended = stopRec;

    })
    .catch(function() {
      document.getElementById('recStatus').textContent = 'Permission denied';
    });
}

function stopRec() {
  if (mediaRecorder && mediaRecorder.state !== 'inactive')
    mediaRecorder.stop();
  if (recStream)
    recStream.getTracks().forEach(function(t) { t.stop(); });

  document.getElementById('startBtn').disabled = false;
  document.getElementById('stopBtn').disabled  = true;
}

function downloadRec() {
  if (!recBlobUrl) return;
  var a      = document.createElement('a');
  a.href     = recBlobUrl;
  a.download = 'recording-' + Date.now() + '.webm';
  a.click();
}
How It Works
getDisplayMedia() is the browser's native screen-capture API. It prompts the user to choose which screen, window, or tab to share. The browser handles all permissions natively with no APEX involvement.
MediaRecorder receives the display stream and records in chunks every 100ms. The small chunk interval keeps memory usage manageable for longer recordings and prevents data loss if the tab crashes.
ondataavailable fires every 100ms, pushing each chunk into the recChunks array. On stop, all chunks are combined into a single Blob of type video/webm.
URL.createObjectURL(blob) creates a temporary in-memory URL. This is assigned to the <video> element for instant in-page playback with no upload and no server round-trip.
stream.getVideoTracks()[0].onended is a safety net. If the user closes the screen-share dialog via the browser's own UI rather than clicking Stop, the recording still ends cleanly.
downloadRec() creates an invisible <a> tag, sets a timestamped filename, and programmatically clicks it. The browser saves the .webm file straight to the user's Downloads folder.
📼
📅 Coming in a Future Blog

This blog covers the Single Page screen recorder: a lightweight, zero-dependency solution that gets the job done with minimal setup. The next blog will cover the Multi-Page Screen Recorder with enhanced features including session management, recording history, upload to APEX Collections or REST APIs, and more polished UI controls.

Follow the blog series to get notified when it is published!

🎥
Quick Checklist
Create a Static Content region on your APEX page
Paste the HTML into the region's Source > HTML Code attribute
Paste the JavaScript into Page > Function and Global Variable Declaration, not Execute when Page Loads
Run the page, click Start Recording, select a screen or window, and allow permission
Click Stop. The preview appears instantly. Click Download Recording to save the file.

Comments

Popular posts from this blog

Freeze panes in Interactive Grid : Driven by a page item

Oracle APEX Tutorial Freeze Panes in Interactive Grid : Driven by a Page Item Lock N columns in place with a select list : no plugins, no hacks. Pure JavaScript, a Dynamic Action, and clean CSS that survives sort, search, and pagination. Live Demo GitHub Code Create page item Add page JavaScript Dynamic Action Optional CSS 1 Create the P1_FREEZE_COLS select list In Page Designer, add a Select List page item. Configure it as shown below. Also set the Interactive Grid region's Static ID to emp_grid via Properties → Advanced → Static ID . Name P1_FREEZE_COLS List of Values Static Values : Display/Return 0 through 5 Default Value 0 Template Optional / Floating Label Region Static ID emp_grid 2 Page JavaScript : Execute when Page Loads Paste t...

Excel-Style Keyboard Shortcuts in Interactive Grid

Oracle APEX Tutorial Excel-Style Keyboard Shortcuts in Interactive Grid Add Alt+A , Alt+D , Alt+S and Alt+R to your editable IG : no plugins, just four clean setup steps using APEX's built-in actions registry. Alt + A   Add Row Alt + D   Delete Row Alt + S   Save Alt + R   Refresh Alt + Shift + F1   View All Shortcuts Live Demo GitHub Code Editable IG Static ID Cursor Focus JS Init Code Testing 1 Prerequisites : Make your IG Editable ⚠️ Keyboard shortcuts only work on an Editable Interactive Grid . The row-add-row , row-delete , and save actions only exist in the IG's action registry when the grid is in editable mode. If you skip this, actions.lookup() will return null and throw an error. In your Interactive Grid region's attributes, set Edit → Enabled to...