Background Audio

Continuous Microphone Recording with Background & Lock Screen Support

Background Audio enables native, continuous microphone recording in your Median-powered app, even when the app is in the background or the device is locked. The plugin offers seamless recording with persistent notifications on Android and system-level recording indicators on iOS, plus real-time state updates and optional on-device speech-to-text transcription.

The plugin supports start, stop, pause, and resume controls with real-time event callbacks for state changes and optional transcript delivery. Implement it using async JavaScript on top of the Median JavaScript Bridge.

Why use Background Audio

  • Background and lock screen recording: Keep capturing audio when users leave the app or lock the device.
  • Native platform UX: Surface a persistent Android foreground service notification and iOS system recording indicators without custom native code.
  • Lifecycle controls: Start, stop, pause, and resume without releasing the background session when pausing.
  • Realtime updates: Listen for recording state changes and handle completion or errors in your UI.

Prerequisites

  • Enable JavaScript Bridge in your Median.co app
  • Grant microphone permission before or during your recording flow (see checkPermission and requestPermission)
  • Learn the basics of async JavaScript (async / await) and the optional callbacks used below
👍

Developer Demo

Display our demo page in your app to test during development https://median.dev/background-audio/


When to use this plugin

Background Audio is essential for apps that need to:

Use caseExample
Capture audio while multitaskingRecord voice notes or field audio while the user switches apps.
Record with the device lockedContinue a session after the screen locks, within platform rules and user consent.
Optional on-device transcriptionEnable speech-to-text during recording and read the transcript when stopping.

What you'll do

Use this section as a fast navigation map into the full setup and implementation details:

  1. Confirm permissions: Check and request microphone access before starting a session using the methods documented in JavaScript bridge functions.
  2. Configure recording options: See Plugin setup for startRecording options and recommended settings.
  3. Implement bridge calls: Add checkPermission, requestPermission, startRecording, pause, resume, stopRecording, getStatus, and listeners from JavaScript bridge functions.
  4. Understand platform UX: Review the Platform UX during recording note under startRecording to see what users will see on Android and iOS.
  5. Validate flows and errors: Use Error handling, Testing Checklist, and Troubleshooting to verify your implementation.

Key terms

The following terms will help you implement Background Audio effectively:

AVAudioSession (iOS): The app activates the native audio session in background mode while recording. The system shows a recording indicator in the Dynamic Island (iPhone 14+) or status bar pill while the session is active.

Foreground service (Android): The Android component that runs while recording, showing a persistent notification with app icon, name, status text, and actions.

On-device speech-to-text (STT): The plugin can produce optional transcription during recording. Configure it with enableTranscription and sttLanguage. Availability can vary by locale.

Listener events: Real-time callbacks for lifecycle changes (idle, recording, paused, stopped, error).

📘

Important considerations

  • iOS shows a system-level recording indicator (orange dot with waveform animation while actively recording); it hides when recording stops.
  • Android shows status text such as "audio recording in progress" or "paused" and exposes Launch App and Stop Recording actions on the notification.
  • WAV works only on iOS; Android automatically uses M4A if you request WAV.
  • OS interruptions (for example, calls or Siri) can pause the session (SESSION_INTERRUPTED); handle error listener state and the error reference table.

Plugin setup

Enable the Background Audio plugin in your Median App Studio configuration, then rebuild your app. Once you enable it, you can call median.backgroundAudio.* functions from the JavaScript Bridge at runtime.


JavaScript bridge functions

Document each method available on median.backgroundAudio. Most functions return a promise; getStatus additionally supports a callback form.

Check permissions

Returns whether the user has already granted microphone permission. This method returns a promise.

↔️Median JavaScript Bridge

const permission = await median.backgroundAudio.checkPermission();

if (!permission.granted) {
  // Prompt the user to grant permission
}

Return value:

interface PermissionStatus {
    granted: boolean;
}

Request permissions

Prompts the user for microphone permission. If the user grants it, you can start recording. This method returns a promise.

↔️Median JavaScript Bridge

const permission = await median.backgroundAudio.requestPermission();

if (permission.granted) {
    const result = await median.backgroundAudio.startRecording();
}

Return value:

interface PermissionStatus {
    granted: boolean;
}

Start recording

Starts a background-aware recording session. Optional configuration controls format, duration, and transcription. This method returns a promise.

↔️Median JavaScript Bridge

// Minimal configuration
const result = await median.backgroundAudio.startRecording();

// Full configuration
const result = await median.backgroundAudio.startRecording({
    format: 'm4a',
    maxDuration: 3600,
    enableTranscription: true,
    sttLanguage: 'en-US'
});

Parameters:

ParameterTypeRequiredDescription
formatstringNo'm4a' or 'wav' (WAV iOS only; Android uses M4A).
maxDurationnumberNoMaximum recording length in seconds.
enableTranscriptionbooleanNoEnable on-device speech-to-text.
sttLanguagestringNoLanguage code for transcription (default: device locale).

Return value:

interface StartRecordingResult {
    success: boolean;
}

Platform UX during recording:

When startRecording succeeds, each platform surfaces native UI automatically. You do not need additional manifest or Info.plist changes beyond enabling the plugin and declaring standard microphone usage descriptions.

Android displays a persistent notification with:

  • App icon and name
  • Status text: "audio recording in progress" or "paused"
  • Two action buttons:
    • Launch App — deep-links back into the app
    • Stop Recording — terminates the session and launches the app

iOS activates AVAudioSession with background mode and displays a system-level recording indicator while the session is active:

  • Dynamic Island (iPhone 14+) or status bar pill
  • Red dot with waveform animation during active recording
  • iOS hides it automatically when recording stops

Pause recording

Pauses an active recording without releasing the background session. This method returns a promise.

↔️Median JavaScript Bridge

const status = await median.backgroundAudio.pause();
console.log('Recording paused');

Return value:

Returns a status object reflecting the updated recording state.


Resume recording

Resumes a paused session. This method returns a promise.

↔️Median JavaScript Bridge

const status = await median.backgroundAudio.resume();
console.log('Recording resumed');

Parameters:

This function takes no parameters.

Return value:

Returns a status object reflecting the updated recording state.


Stop recording

Stops the session and retrieves the file URI, duration, and optional transcript. This method returns a promise.

↔️Median JavaScript Bridge

const result = await median.backgroundAudio.stopRecording();

console.log('File URI:', result.fileUri);
console.log('Duration:', result.durationSeconds, 'seconds');

if (result.transcript) {
    console.log('Transcript:', result.transcript);
}

Parameters:

This function takes no parameters.

Return value:

interface StopRecordingResult {
    fileUri: string;
    durationSeconds: number;
    transcript?: string;
}

Get status

Returns the current recording state and elapsed time. Pass a callback function to use the callback form. Otherwise, this method returns a promise.

↔️Median JavaScript Bridge

// Using promises (recommended)
const status = await median.backgroundAudio.getStatus();
console.log(JSON.stringify(status));
// Using callback
median.backgroundAudio.getStatus({
    callback: function (status) {
        console.log('Current state:', status.state);
        console.log('Elapsed time:', status.elapsedSeconds, 'seconds');
    }
});

Parameters:

ParameterTypeRequiredDescription
callbackfunctionNoReceives the status object when not using await.

Return value:

interface RecordingStatus {
    state: 'idle' | 'recording' | 'paused' | 'stopped' | 'error';
    elapsedSeconds: number;
}

Example JSON:

{
    "state": "recording",
    "elapsedSeconds": 127
}

Add a listener

Registers a listener for real-time recording lifecycle updates. Returns a listener identifier that you pass to removeListener.

↔️Median JavaScript Bridge

const listenerId = median.backgroundAudio.addListener((event) => {
    console.log('State changed:', event.state);

    if (event.state === 'stopped') {
        console.log('Recording complete');
    } else if (event.state === 'error') {
        console.error('Error code:', event.errorCode);
    }
});

Parameters:

ParameterTypeRequiredDescription
callbackfunctionYesThe bridge invokes this callback with an event object containing state and optional errorCode.

Return value:

Returns a listener identifier (opaque value) that you pass to removeListener.

Event states you may receive:

StateDescription
idleNo recording active; ready to start
recordingActively recording audio
pausedRecording paused; session still active
stoppedRecording stopped; file available
errorAn error occurred (check errorCode)

Remove a listener

Removes a previously added listener so it stops receiving further events.

↔️Median JavaScript Bridge

median.backgroundAudio.removeListener(listenerId);

Parameters:

ParameterTypeRequiredDescription
listenerId(opaque)YesIdentifier returned from addListener.

Return value:

No return value.


Error handling

Error codeDescriptionCommon causesResolution
PERMISSION_DENIEDUser denied microphone accessUser declined or revoked mic accessGuide the user to the Settings app
ALREADY_RECORDINGYou called startRecording() while a session is activeDuplicate start callsReturn or surface current session info
NOT_RECORDINGYou called stop() / pause() / resume() with no active sessionCommands run out of orderCheck getStatus or listener state first
STT_UNAVAILABLEThe platform does not support speech-to-text for the requested localeLocale or OS capabilityFall back to file URI only
SESSION_INTERRUPTEDThe OS interrupted the session (phone call, Siri)Incoming call or assistantAuto-pause; inform the user and resume when safe
FILE_ERRORFile write failureDisk full or sandbox errorCheck storage availability

Best practices for error handling

try {
    const result = await median.backgroundAudio.startRecording({
        format: 'm4a',
        enableTranscription: true
    });

    if (!result.success) {
        console.error('Failed to start recording');
    }
} catch (error) {
    switch (error.code) {
        case 'PERMISSION_DENIED':
            // Prompt the user to enable the microphone in Settings
            break;
        case 'ALREADY_RECORDING':
            // Surface current session state instead of starting again
            break;
        case 'STT_UNAVAILABLE':
            // Fall back to file URI only
            break;
        default:
            console.error('Unexpected background audio error:', error);
    }
}
📘

Important

Use the listener error state and the errorCode it provides together with this reference table when presenting in-app guidance. Combine it with getStatus checks before issuing pause, resume, or stop commands to avoid NOT_RECORDING and ALREADY_RECORDING race conditions.


Example implementation

See the example implementation below for the basic recording flow

Basic Recording Flow

Before starting a recording session, check if the user has granted microphone permissions:

const permission = await median.backgroundAudio.checkPermission();

if (!permission.granted) {
  // Prompt user to grant permission
}

If permissions haven't been granted, prompt the user:

const permission = await median.backgroundAudio.requestPermission();

if (permission.granted) {
  // Start recording
  const result = await median.backgroundAudio.startRecording();
}

Start a background-aware recording session with optional configuration:

↔️Median JavaScript Bridge

Minimal Configuration:

const result = await median.backgroundAudio.startRecording();

Full Configuration:

const result = await median.backgroundAudio.startRecording({
  format: 'm4a',                    // Audio format (m4a or wav)
  maxDuration: 3600,                // Maximum duration in seconds (default: 3600)
  enableTranscription: true,        // Enable on-device STT (default: false)
  sttLanguage: 'en-US'              // Language code for transcription (default: device locale)
});

HTML Example:

<button type="button" onclick="startRecording()">Start Recording</button>

<script>
async function startRecording() {
  const result = await median.backgroundAudio.startRecording({
    format: 'm4a',
    enableTranscription: true,
    sttLanguage: 'en-US'
  });
  
  if (result.success) {
    console.log('Recording started');
  }
}
</script>

Full Recipe

Follow the full recipe for the full implementation


Diagrams

1. Overview Flowchart

High-level view: permission gate → start recording → success or failure.

flowchart TD
    Trigger["User taps Start Recording"]
    CheckPerm["checkPermission()"]
    Granted{{"permission.granted?"}}
    RequestPerm["requestPermission()"]
    GrantedAfter{{"granted after request?"}}
    StartRec["startRecording(options)"]
    Success{{"result.success?"}}
    Recording["Recording active"]
    Denied["Show settings guidance"]
    Failed["Log error — session not started"]

    Trigger --> CheckPerm
    CheckPerm --> Granted
    Granted -->|"Yes"| StartRec
    Granted -->|"No"| RequestPerm
    RequestPerm --> GrantedAfter
    GrantedAfter -->|"Yes"| StartRec
    GrantedAfter -->|"No"| Denied
    StartRec --> Success
    Success -->|"true"| Recording
    Success -->|"false"| Failed

Testing Checklist

Use this checklist to ensure Background Audio is working correctly in your app:

  • checkPermission and requestPermission behave as expected before startRecording
  • Minimal startRecording() and full configuration calls succeed when permitted
  • pause and resume update behavior without releasing the session
  • stopRecording returns fileUri, durationSeconds, and transcript when you enable transcription and the platform supports it
  • Plugin loads without JavaScript errors
  • Android shows the foreground notification with correct status text and Launch App / Stop Recording actions
  • iOS shows the recording indicator during active recording and hides it when stopped
  • WAV records on iOS as documented; Android falls back to M4A when you request WAV
  • Recording continues when you background the app and lock the device
  • getStatus returns state and elapsedSeconds via promise and via callback form
  • addListener receives transitions across idle, recording, paused, stopped, and error
  • removeListener stops further callbacks for that id
  • Your app surfaces the errors in the reference table as documented (PERMISSION_DENIED, ALREADY_RECORDING, NOT_RECORDING, STT_UNAVAILABLE, SESSION_INTERRUPTED, FILE_ERROR)
  • Incoming calls or Siri trigger SESSION_INTERRUPTED, and your listener handles it
  • Denying microphone permission returns PERMISSION_DENIED and surfaces a clear user-facing path to Settings

Troubleshooting

Guide the user to the system Settings app to enable the microphone. Confirm you call checkPermission and, if needed, requestPermission before startRecording.

Avoid duplicate starts. Use getStatus or your listener state to decide whether to start, resume, or stop instead of calling startRecording again.

Speech-to-text may be unavailable for the requested locale. Fall back to the recorded file using fileUri from stopRecording, and consider setting sttLanguage explicitly to a supported locale.

Interruptions such as phone calls or Siri can pause recording. Inform the user in your UI and let them resume when appropriate via resume.

The notification includes Launch App (deep-links into the app) and Stop Recording (ends the session and launches the app). Test both actions against your app's navigation stack.

📘

Still having trouble?

For issues related to platform-level microphone, background audio, or speech-to-text behavior outside Median.co, refer to the platform documentation for Apple AVAudioSession and Android foreground services.