Android屏幕录制核心技术:MediaProjection与VirtualDisplay详解
1. Android录屏采集的核心技术栈解析在Android平台上实现屏幕录制功能关键在于理解MediaProjection这套系统级API的设计理念和工作机制。这套API自Android 5.0API 21引入最初是为了支持Chromecast投屏功能而开发后来逐渐成为系统级录屏的基石实现方案。1.1 MediaProjection的权限机制与普通权限不同MediaProjection需要用户显式授权这涉及到特殊的Intent启动流程。当应用调用MediaProjectionManager.createScreenCaptureIntent()时系统会弹出一个半透明的授权对话框这个对话框会截取当前屏幕内容作为预览图让用户明确知道即将被录制的画面范围。重要提示从Android 10开始后台服务启动Activity受到限制因此务必在前台Activity中触发权限申请否则会导致崩溃。授权通过后返回的Intent中包含一个特殊的token这个token是IMediaProjection接口的Binder对象。我们需要将其转换为MediaProjection实例private static final int REQUEST_CODE_SCREEN_CAPTURE 1; // 在Activity中启动授权 void startScreenCapture() { MediaProjectionManager manager (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); startActivityForResult( manager.createScreenCaptureIntent(), REQUEST_CODE_SCREEN_CAPTURE); } Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode REQUEST_CODE_SCREEN_CAPTURE) { if (resultCode RESULT_OK) { mediaProjection manager.getMediaProjection(resultCode, data); // 后续初始化VirtualDisplay } } }1.2 VirtualDisplay的配置奥秘VirtualDisplay是实际承载屏幕内容的虚拟显示层其配置参数直接影响录制的质量和性能。关键参数包括width/height建议使用DisplayMetrics获取实际分辨率而非固定值dpi设置为0时系统会自动计算合适值flags组合使用DisplayManager.VIRTUAL_DISPLAY_FLAG_*控制行为surface决定数据输出目标编码器/预览等典型创建代码如下VirtualDisplay virtualDisplay mediaProjection.createVirtualDisplay( ScreenCapture, width, height, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, outputSurface, // 编码器或预览的Surface null, // 回调 handler // 可选Handler );1.3 Surface的三种数据通路Surface作为Android图形系统的核心抽象在录屏场景中有三种典型用法编码器Surface连接MediaCodec输入预览Surface连接SurfaceView或TextureView图像处理Surface连接OpenGL ES上下文实际开发中我们常需要同时配置多个Surface实现边录边预览。这涉及到SurfaceTexture的巧妙运用// 创建共享纹理 SurfaceTexture texture new SurfaceTexture(0); texture.setDefaultBufferSize(width, height); // 生成预览Surface Surface previewSurface new Surface(texture); previewView.setSurfaceTexture(texture); // 创建编码器Surface MediaCodec codec MediaCodec.createEncoderByType(MIME_TYPE); codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); Surface encodeSurface codec.createInputSurface();2. 完整录屏采集实现流程2.1 工程配置准备在开始编码前需要确保AndroidManifest.xml包含必要权限声明uses-permission android:nameandroid.permission.FOREGROUND_SERVICE/ uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28/ !-- 适配旧版本 -- uses-feature android:nameandroid.hardware.type.pc android:requiredfalse/注意从Android 10开始Scoped Storage限制了外部存储访问建议使用MediaStore API保存视频文件。2.2 编码器初始化策略选择合适的视频编码参数对录制质量至关重要。以下是推荐的配置模板MediaFormat format MediaFormat.createVideoFormat( MediaFormat.MIMETYPE_VIDEO_AVC, // H.264 width, height); // 关键参数配置 format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 兼容性处理 if (Build.VERSION.SDK_INT Build.VERSION_CODES.LOLLIPOP) { format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh); if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31); } }2.3 数据流管道搭建完整的录制流程需要建立以下处理链MediaProjection→ 产生原始帧数据VirtualDisplay→ 承载显示内容Surface→ 数据中转站MediaCodec→ 视频编码MediaMuxer→ 封装为MP4文件核心实现代码结构// 初始化编码器 MediaCodec videoCodec MediaCodec.createEncoderByType(MIME_TYPE); videoCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); Surface inputSurface videoCodec.createInputSurface(); // 初始化复用器 MediaMuxer muxer new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); // 创建VirtualDisplay virtualDisplay mediaProjection.createVirtualDisplay( ScreenRecorder, width, height, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, inputSurface, null, null); // 启动编码线程 videoCodec.start(); new Thread(this::encodeLoop).start();3. 性能优化与疑难排错3.1 帧率稳定的关键技巧实测中发现直接使用默认配置容易出现帧率波动问题。通过以下优化可显著改善Surface缓冲控制设置合适的BufferQueue深度if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { surface.setBufferCount(3); // 推荐3-5个缓冲 }编码器异步模式Android 5.0支持codec.setCallback(new MediaCodec.Callback() { Override public void onInputBufferAvailable(MediaCodec mc, int index) { // 处理输入缓冲 } Override public void onOutputBufferAvailable(...) { // 处理编码完成数据 } });线程优先级调整Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);3.2 常见崩溃问题排查问题1Service未前台化java.lang.IllegalStateException: Not allowed to start service Intent...解决方案Android 8.0必须使用startForegroundService()并显示通知问题2Surface已释放E/Surface: queueBuffer: error queuing buffer to SurfaceTexture根因分析Activity进入后台时Surface会被销毁处理方案Override protected void onPause() { if (isRecording) { // 保持屏幕常亮 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } super.onPause(); }3.3 厂商ROM适配经验不同厂商设备可能存在以下差异小米/Redmi需额外申请后台弹出界面权限华为EMUI电池优化白名单设置OPPO ColorOS需关闭应用速冻功能三星OneUI注意Dex模式下的分辨率变化通用检测方法public static boolean isXiaomiDevice() { return Build.MANUFACTURER.toLowerCase(Locale.US).contains(xiaomi); }4. 高级功能扩展实现4.1 实时音频采集融合完整录屏需要同步系统音频可通过AudioRecord实现// 音频配置 int sampleRate 44100; int channelConfig AudioFormat.CHANNEL_IN_STEREO; int audioFormat AudioFormat.ENCODING_PCM_16BIT; AudioRecord audioRecord new AudioRecord( MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, audioFormat, AudioRecord.getMinBufferSize(...)); // 启动音频线程 new Thread(() - { byte[] buffer new byte[1024]; while (recording) { int read audioRecord.read(buffer, 0, buffer.length); // 写入音频编码器 } }).start();4.2 OpenGL后处理管线通过GLES可以实现实时滤镜效果// 创建EGL环境 EGLDisplay eglDisplay EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); EGL14.eglInitialize(eglDisplay, null, 0, null, 0); // 配置Surface EGLSurface eglSurface EGL14.eglCreateWindowSurface(...); // 着色器示例灰度滤镜 private static final String FRAGMENT_SHADER precision mediump float;\n varying vec2 vTexCoord;\n uniform sampler2D uTexture;\n void main() {\n vec4 color texture2D(uTexture, vTexCoord);\n float gray 0.299*color.r 0.587*color.g 0.114*color.b;\n gl_FragColor vec4(gray, gray, gray, color.a);\n };4.3 动态分辨率适配应对设备旋转等场景的解决方案private class DisplayChangeCallback extends DisplayManager.DisplayListener { Override public void onDisplayChanged(int displayId) { if (displayId getDisplay().getDisplayId()) { // 获取新分辨率 Point size new Point(); getDisplay().getRealSize(size); // 重建VirtualDisplay recreateVirtualDisplay(size.x, size.y); } } } // 注册监听 DisplayManager dm (DisplayManager) getSystemService(DISPLAY_SERVICE); dm.registerDisplayListener(callback, handler);在实际项目中我发现正确处理Activity生命周期与录制状态的同步至关重要。一个健壮的实现应该考虑以下场景屏幕旋转时的配置变更多窗口模式切换电源键锁屏恢复通知栏快速设置操作建议采用状态机模式管理录制流程定义明确的转换条件。例如使用枚举表示状态enum RecorderState { IDLE, PREPARING, RECORDING, PAUSED, STOPPING, ERROR }每次状态变更时同步更新UI和释放资源这样可以避免90%以上的异常崩溃问题。