Efficient Android Screen Recording Using MediaRecorder + MediaProjection



This content originally appeared on DEV Community and was authored by cat dog (running_in_the_storm)

Efficient Android Screen Recording Using MediaRecorder + MediaProjection

Preface

On the Android platform, there are two primary approaches to implement screen recording: the MediaCodec + MediaMuxer combination and MediaRecorder. The former offers high flexibility and customization but is relatively complex, requiring manual handling of video/audio encoding and muxing processes.

In contrast, MediaRecorder is a high-level API that encapsulates the underlying audio/video recording and encoding workflows, significantly reducing development effort while maintaining excellent performance and compatibility.

MediaProjection, introduced in Android 5.0 (API 21), is a system-level service that resolves the core challenge of screen recording—how to securely and efficiently capture screen content. It provides a standardized, app-oriented interface for screen capture.

This article explains how to efficiently implement Android screen recording using MediaRecorder + MediaProjection.

Core of MediaRecorder: Lifecycle State Machine

To use MediaRecorder effectively, you must understand its working principles and master its lifecycle states. The state transitions are strict—any incorrect operation may cause exceptions.

  1. Initial State: The default state when MediaRecorder is first created. You can return to this state via new MediaRecorder() or reset().

  2. Configuring State: In this state, you configure recording parameters using various set... methods, such as:

    • setAudioSource(MediaRecorder.AudioSource.MIC): Set audio source.
    • setVideoSource(MediaRecorder.VideoSource.SURFACE): Set video source to a Surface.
    • setOutputFormat(MediaRecorder.OutputFormat.MPEG_4): Set output container format.
    • setVideoEncoder(MediaRecorder.VideoEncoder.H264): Set video encoder.
    • setOutputFile(filePath): Specify the output file path. After configuration, call prepare().
  3. Prepared State: Entered after calling prepare(). MediaRecorder performs internal checks and resource allocation to ensure readiness for recording.

  4. Recording State: Entered after calling start(). MediaRecorder begins capturing, encoding, and writing audio/video data to the file. This is the core phase of recording.

  5. Stopped State: Entered after calling stop(). Recording halts, file finalization completes, and some resources are released. Note: You cannot resume recording into the same file once stopped.

  6. Error State: Entered if an error occurs at any stage, triggering a RuntimeException.

  • reset(): Returns MediaRecorder to the Initial state for reconfiguration and new recording.
  • release(): Fully releases all resources held by MediaRecorder. The object becomes unusable afterward.

The design philosophy of MediaRecorder prioritizes ease of use, abstracting complex audio/video encoding and muxing behind a simple interface.

Core Features of MediaProjection

  1. User Authorization Mechanism: Requires explicit user consent via a system dialog to ensure privacy.
  2. Virtual Display Support: Creates a VirtualDisplay to redirect screen content to a Surface.
  3. System-Level Integration: Interacts directly with the display system without requiring root access.

Compared to traditional root-based or ADB-based solutions, MediaProjection provides a legal, standardized, app-friendly interface, enabling screen recording apps to be published on official app stores.

Building Your Screen Recording App

Permission Requests and User Authorization

Screen recording is a long-running task. To prevent the app from being killed by the system, run the recording task in a foreground service and display recording status in the notification bar.

We create a foreground service RecordingService for screen recording. First, declare necessary permissions like RECORD_AUDIO and FOREGROUND_SERVICE in AndroidManifest.xml.

<manifest ...>

    <application
    ...
        <service
            android:name=".services.RecordingService"
            android:foregroundServiceType="mediaProjection"
            android:enabled="true"
            android:exported="false"></service>

    </application>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
</manifest>

Then, in a Compose function, use MediaProjectionManager and rememberLauncherForActivityResult to request screen capture permission. This step triggers a system dialog asking for user authorization.

// 1. Define ActivityResultLauncher
//    It takes an Intent as input and returns an ActivityResult
val screenCaptureLauncher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.StartActivityForResult()
) { result ->
    val activity = context.findActivity()
    if (result.resultCode == Activity.RESULT_OK && result.data != null) {
        // User granted permission, start recording service
        viewModel.startRecordingService(activity, result.resultCode, result.data!!)
        Toast.makeText(activity, activity.getString(R.string.start), Toast.LENGTH_SHORT).show()
    } else {
        Toast.makeText(activity, activity.getString(R.string.rejected), Toast.LENGTH_SHORT).show()
    }
}

// 2. Function to launch screen capture request
val startScreenCaptureRequest = remember<(Context) -> Unit> {
    { ctx ->
        // Get MediaProjectionManager
        val projectionManager = ctx.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager

        // Create screen capture intent
        val intent = projectionManager.createScreenCaptureIntent()
        // Launch intent using Compose launcher
        screenCaptureLauncher.launch(intent)
    }
}

...
// Request screen recording
startScreenCaptureRequest(context)
...

The startRecordingService in the ViewModel starts a foreground recording service.

fun startRecordingService(context: Context, resultCode: Int, data: Intent) {
    val serviceIntent = Intent(context, RecordingService::class.java).apply {
        action = "ACTION_START"
        putExtra("resultCode", resultCode)
        putExtra("data", data)
    }
    ContextCompat.startForegroundService(context, serviceIntent)
    context.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE) // Bind to Service
    binder_flag = true
}

Core of Screen Recording: Foreground Service RecordingService

The core logic resides in RecordingService.

In onStartCommand, we handle the recording request, create a notification, register a callback for when recording ends, and finally start the recording task. This order is critical—otherwise, MediaProjection cannot properly capture screen data.

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    if (intent != null) {
        when (intent.action) {
            "ACTION_START" -> {
                if (isRecording) return START_NOT_STICKY
                val resultCode = intent.getIntExtra("resultCode", Activity.RESULT_CANCELED)
                // Use the new, type-safe getParcelableExtra method
                val data = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    intent.getParcelableExtra("data", Intent::class.java)
                } else {
                    @Suppress("DEPRECATION")
                    intent.getParcelableExtra("data")
                }

                if (resultCode != Activity.RESULT_OK || data == null) {
                    Toast.makeText(this, "Screen recording permission denied, cannot start service", Toast.LENGTH_SHORT).show()
                    stopSelf()
                    return START_NOT_STICKY
                }
                // Ensure notification channel exists before creating notification
                createNotificationChannel()
                val notification = createRecordingNotification(this)
                startForeground(notificationId, notification)
                // 2. Register anonymous callback object (more concise!)
                val recordingCallback = object : MediaProjection.Callback() {
                    override fun onStop() {
                        super.onStop()
                        // Perform cleanup when user/system revokes permission
                        // Note: We must manually unregister it. Although stop() releases resources,
                        // it's best to clean up immediately after onStop() is called.
                        mediaProjection?.unregisterCallback(this)
                    }
                }
                mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data)
                // Register callback using current thread's Handler (null)
                mediaProjection?.registerCallback(recordingCallback, null)
                startRecording()
            }
            "ACTION_STOP" -> {
                stopRecording()
            }
        }
    }
    return START_NOT_STICKY
}

In startRecording(), we configure MediaRecorder, set video/audio sources, output format, encoder, resolution, etc., and pass the screen data captured by MediaProjection to MediaRecorder via a Surface.

fun startRecording() {
    try {
        // Configure MediaRecorder
        mediaRecorder =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                MediaRecorder(this)
            } else {
                MediaRecorder()
            }
            .apply {
                if (recordAudioType == 1) {
                    setAudioSource(MediaRecorder.AudioSource.MIC)
                    setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
                }

                setVideoSource(MediaRecorder.VideoSource.SURFACE)
                setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)

                if (mediaUri != null) {
                    pfd = contentResolver.openFileDescriptor(mediaUri!!, "w")
                    if (pfd != null) {
                        setOutputFile(pfd!!.fileDescriptor)
                    }
                } else {
                    setOutputFile(filePath)
                }

                // Video configuration
                setVideoSize(VIDEO_WIDTH, VIDEO_HEIGHT) // Example resolution
                setVideoEncoder(MediaRecorder.VideoEncoder.H264)
                setVideoEncodingBitRate(VIDEO_BIT_RATE) // 5 Mbps
                setVideoFrameRate(VIDEO_FRAME_RATE)
                prepare()
            }

        // Create virtual display
        val displayMetrics = resources.displayMetrics
        val densityDpi = displayMetrics.densityDpi
        virtualDisplay = mediaProjection?.createVirtualDisplay(
            "RecordingDisplay",
            VIDEO_WIDTH, VIDEO_HEIGHT, // Must match MediaRecorder video size
            densityDpi,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
            mediaRecorder?.surface,
            null,
            null
        )

        mediaRecorder?.start()
        Log.d("RecordingService", "Recording started!")
        isRecording = true
    } catch (e: IOException) {
        Log.e("RecordingService", "startRecording failed", e)
        stopRecording()
    }
}

Finally, do not forget to release resources properly.

private fun stopRecording() {
    isRecording = false
    mediaRecorder?.apply {
        stop()
        reset()
        release()
    }
    pfd?.close()
    pfd = null
    mediaRecorder = null

    virtualDisplay?.release()
    virtualDisplay = null

    mediaProjection?.stop()
    mediaProjection = null

    // 1. Remove foreground state
    // Use STOP_FOREGROUND_REMOVE flag to remove both foreground state and notification.
    // This is the most common and complete way to stop foreground service.
    ServiceCompat.stopForeground(
        /* service = */ this,
        /* flags = */ ServiceCompat.STOP_FOREGROUND_REMOVE
    )
    stopSelf()
    Log.d("RecordingService", "Recording stopped.")
}

override fun onDestroy() {
    super.onDestroy()
    stopRecording()
}

With the above steps, we have completed a fully functional screen recording feature.

Summary

The combination of MediaRecorder + MediaProjection is a powerful tool for implementing screen recording on Android. By encapsulating complex underlying implementations, it allows developers to achieve high-quality recording with minimal code.

The complete sample code can be found in the screen recording section of the Audio and Video Editor project (note: the code is somewhat rough).

References

  1. Media Projection – Official Google Tutorial
  2. MediaRecorder Overview


This content originally appeared on DEV Community and was authored by cat dog (running_in_the_storm)