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.
Initial State: The default state when
MediaRecorderis first created. You can return to this state vianew MediaRecorder()orreset().-
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 aSurface. -
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, callprepare().
-
Prepared State: Entered after calling
prepare().MediaRecorderperforms internal checks and resource allocation to ensure readiness for recording.Recording State: Entered after calling
start().MediaRecorderbegins capturing, encoding, and writing audio/video data to the file. This is the core phase of recording.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.Error State: Entered if an error occurs at any stage, triggering a
RuntimeException.
-
reset(): ReturnsMediaRecorderto the Initial state for reconfiguration and new recording. -
release(): Fully releases all resources held byMediaRecorder. 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
- User Authorization Mechanism: Requires explicit user consent via a system dialog to ensure privacy.
-
Virtual Display Support: Creates a
VirtualDisplayto redirect screen content to aSurface. - 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
This content originally appeared on DEV Community and was authored by cat dog (running_in_the_storm)