Android 录屏

Android录屏

参考

概念

通过MediaProjection创建一个投影,可以将这个投影显示到自己的 SurfaceView 上,也可以通过 MediaRecorder 编码存储到本地实现录屏效果,也可以通过 MediaCodec 编码后获取实时数据推送直播

相关权限

权限 说明 是否动态申请
android.permission.RECORD_AUDIO 录音权限
android.permission.FOREGROUND_SERVICE 前台服务

相关类

说明
MediaProjectionManager MediaProjection管理
MediaProjection 授予捕获屏幕或记录系统音频的功能
VirtualDisplay 类似投影仪?捕获屏幕后将数据输出到投影仪 投影仪可以获取视频的信息,指定输出的位置等
MediaRecorder 用于将音视频编码输出
MediaMuxer 将音视频混合生成多媒体文件
MediaCodec 进行音视频压缩编解码

流程

1.申请录屏

通过MediaProjectionManager.createScreenCaptureIntent()获取一个Intent

调用startActivityForResult()发起录屏请求

onActivityResult()中获取请求结果并开始录屏

MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
Intent screenCaptureIntent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(screenCaptureIntent,10012);

2.启用前台服务

Android 10之后使用录屏等功能要求在前台Service中进行

AndroidManifest.xml中要为该Service设置android:foregroundServiceType="mediaProjection"属性

且需要声明<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

启动Service时,需要调用startForegroundService()作为前台服务启动

在Service中需要先调用startForeground()启动一个Notification后才能调用录屏

流程:

AndroidManifest.xml

<!-- 声明权限 -->
 <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
 
 <!-- 声明属性 -->
 <service android:name=".service.MediaRecordService"
  android:foregroundServiceType="mediaProjection">
 </service>

MediaRecordActivity

@RequiresApi(api = Build.VERSION_CODES.O)
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    // 获取申请录屏结果
    if (requestCode == 10012 && resultCode == RESULT_OK){
        Intent intent = new Intent(this, MediaRecordService.class);
        intent.putExtra("data",data);
        intent.putExtra("resultCode",resultCode);
        intent.putExtra("width",WindowUtils.getWindowWidth(this)); // 屏幕的宽
        intent.putExtra("height",WindowUtils.getWindowHeight(this)); // 屏幕的高
        intent.putExtra("surface",surface); // Surface 用于显示录屏的数据
        startForegroundService(intent); // 启动前台服务
    }
}

MediaRecordService

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    // 创建通知栏
    NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    Notification notification = new NotificationCompat.Builder(this, "123123")
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContentTitle("录屏")
        .setContentText(getString(R.string.app_name) + "录屏中")
        .build();

    if(Build.VERSION.SDK_INT>=26) {
        // 推送通道
        NotificationChannel channel = new NotificationChannel("123123", "通道说明", NotificationManager.IMPORTANCE_DEFAULT);
        notificationManager.createNotificationChannel(channel);
    }
    // 展示前台服务
    startForeground(123123, notification);


    int resultCode = intent.getIntExtra("resultCode", -1);
    width = intent.getIntExtra("width", -1);
    height = intent.getIntExtra("height", -1);
    Intent data = intent.getParcelableExtra("data");
    final Surface surface = intent.getParcelableExtra("surface");
    // 获取 MediaProjectionManager
    MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
    // 获取 MediaProjection
    final MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
    if (mediaProjection != null) {
            /**
             * 创建投影
             * name 本次虚拟显示的名称
             * width 录制后视频的宽
             * height 录制后视频的高
             * dpi 显示屏像素
             * flags VIRTUAL_DISPLAY_FLAG_PUBLIC 通用显示屏
             * Surface 输出的Surface
             */
            VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay("record-video", 200, 200, 6000000,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);
    }
    return super.onStartCommand(intent, flags, startId);
}
使用 MediaRecorder 录制保存到本地
  1. 初始化 MediaRecorder
private void initMediaRecorder() {
    mediaRecorder = new MediaRecorder();
    // 设置音频来源 需要动态申请 android.permission.RECORD_AUDIO 权限
    mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
    // 设置视频来源
    mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
    // 设置输出格式
    mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
    // 设置输出文件
    String absolutePath = new File(recordFile + "/record.mp4").getAbsolutePath();
    mediaRecorder.setOutputFile(absolutePath);
    // 设置视频宽高
    mediaRecorder.setVideoSize(width,height);
    // 设置视频帧率
    mediaRecorder.setVideoFrameRate(60);
    // 设置视频编码比特率
    mediaRecorder.setVideoEncodingBitRate(6000000);
    // 设置音频编码
    mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
    // 设置视频编码
    mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);

    try {
        mediaRecorder.prepare();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  1. 创建投影时,将 MediaRecorder 的 Surface 设为输出位置
// mediaRecorder.getSurface() 获取要记录的 Surface
virtualDisplay = mediaProjection.createVirtualDisplay("record-video", width, height, 6000000, 
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mediaRecorder.getSurface(), null, null);
  1. 开始
mediaRecorder.start()
使用 MediaCodec 编码

编码后数据未验证是否可以直接进行推流,按 使用MediaCodec和RTMP做直播推流 对数据进行RTMP编码后应该是可以推流的

  1. 初始化 MediaCodec
private void initMediaCodec() {
    String MIME_TYPE = "video/avc"; // H.264 类型
    MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
    // 颜色格式
    format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
    // 比特率
    format.setInteger(MediaFormat.KEY_BIT_RATE, 6000000);
    // 帧速率
    format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
    // I帧的帧率
    format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);

    try {
        // 创建指定类型的编码器
        videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
        // 设置编码器属性
        videoEncoder.configure(format,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
        // 创建作为输入的 Surface
        inputSurface = videoEncoder.createInputSurface();
        videoEncoder.start();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  1. 创建投影时,将 MediaCodec的 Surface 设为输出位置
virtualDisplay = mediaProjection.createVirtualDisplay("record-video", width, height, 6000000, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, inputSurface, null, null);
  1. 读取解码后数据
new Thread(new Runnable() {
    @Override
    public void run() {
        while (isRecord){
            // 获取已经解码的缓冲区索引
            int index = videoEncoder.dequeueOutputBuffer(bufferInfo, 10000);
            if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
                // 输出格式已改变
                resetOutputFormat();
            }else if (index == MediaCodec.INFO_TRY_AGAIN_LATER){
                // 超时
                
            }else if (index >= 0){
                ByteBuffer outputBuffer = videoEncoder.getOutputBuffer(index);
                MediaFormat outputFormat = videoEncoder.getOutputFormat();

                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    bufferInfo.size = 0;
                }
                if (bufferInfo.size == 0){
                    outputBuffer = null;
                }else {
                    if (outputBuffer != null){
                        // 将 ByteBuffer 转换为 byte[]
                        // 得到编码后数据(需要验证)
                        byte[] bytes = bytebuffer2ByteArray(outputBuffer);
                    }
                }
                videoEncoder.releaseOutputBuffer(index, false);
            }
        }
    }
}).start();
  1. 使用 MediaMuxer 将编码后数据写入到本地
// 创建 MediaMuxer
mediaMuxer = new MediaMuxer(absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 写入
mediaMuxer.writeSampleData(videoTrackIndex,outputBuffer,bufferInfo);
MediaRecordService
public class MediaRecordService extends Service {

    private MediaRecorder mediaRecorder;
    private File recordFile;
    private int width;
    private int height;
    private Surface surface;
    private VirtualDisplay virtualDisplay;
    private MediaCodec videoEncoder;
    private Surface inputSurface;
    private MediaMuxer mediaMuxer;
    private int videoTrackIndex;
    private MediaCodec.BufferInfo bufferInfo;
    private boolean isRecord = false;
    private NotificationManager notificationManager;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }

    public class MyBinder extends Binder {

        public void paused(){
            // 置为null时,表示暂停
            virtualDisplay.setSurface(null);
        }

        public void stop(){
            isRecord  = false;

            virtualDisplay.setSurface(null);
            virtualDisplay.release();

            videoEncoder.stop();
            videoEncoder.release();

            mediaMuxer.stop();
            mediaMuxer.release();

            notificationManager.cancel(123123);
        }

        public void resume(){
            virtualDisplay.setSurface(surface);
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        Notification notification = new NotificationCompat.Builder(this, "123123")
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle("录屏")
                .setContentText(getString(R.string.app_name) + "录屏中")
                .build();

        if(Build.VERSION.SDK_INT>=26) {
            // 推送通道
            NotificationChannel channel = new NotificationChannel("123123", "通道说明", NotificationManager.IMPORTANCE_DEFAULT);
            notificationManager.createNotificationChannel(channel);
        }
        // 展示前台服务
        startForeground(123123, notification);


        int resultCode = intent.getIntExtra("resultCode", -1);
        width = intent.getIntExtra("width", -1);
        height = intent.getIntExtra("height", -1);
        Intent data = intent.getParcelableExtra("data");
        surface = intent.getParcelableExtra("surface");

        MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        final MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
        if (mediaProjection != null) {
            // 获取存储的位置
            recordFile = getExternalFilesDir("RecordFile");
            boolean mkdirs = recordFile.mkdirs();

//          initMediaRecorder();

            initMediaCodec();

            String absolutePath = new File(recordFile + "/record.mp4").getAbsolutePath();
            try {
                final FileOutputStream fos = new FileOutputStream(absolutePath);
                mediaMuxer = new MediaMuxer(absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

                /**
                 * 创建投影
                 * name 本次虚拟显示的名称
                 * width 录制后视频的宽
                 * height 录制后视频的高
                 * dpi 显示屏像素
                 * flags VIRTUAL_DISPLAY_FLAG_PUBLIC 通用显示屏
                 * Surface 输出位置
                 */
                virtualDisplay = mediaProjection.createVirtualDisplay("record-video", width, height, 6000000,
                        DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, inputSurface, null, null);

                isRecord = true;
                bufferInfo = new MediaCodec.BufferInfo();

                readEncoderData();

            } catch (IOException e) {
                e.printStackTrace();
            }

//                    mediaRecorder.start();
        }

        return super.onStartCommand(intent, flags, startId);
    }

    private void readEncoderData() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (isRecord){
                    // 获取已经解码的缓冲区索引
                    int index = videoEncoder.dequeueOutputBuffer(bufferInfo, 10000);
                    if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
                        // 输出格式已改变
                        resetOutputFormat();
                    }else if (index == MediaCodec.INFO_TRY_AGAIN_LATER){
                        // 超时

                    }else if (index >= 0){
                        // 获取数据
                        ByteBuffer outputBuffer = videoEncoder.getOutputBuffer(index);

                        if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                            bufferInfo.size = 0;
                        }
                        if (bufferInfo.size == 0){
                            outputBuffer = null;
                        }else {
                            if (outputBuffer != null){
                                // 将 ByteBuffer 转换为 byte[]
//                                byte[] bytes = bytebuffer2ByteArray(outputBuffer);

                                mediaMuxer.writeSampleData(videoTrackIndex,outputBuffer,bufferInfo);
                            }
                        }
                        videoEncoder.releaseOutputBuffer(index, false);
                    }
                }
            }
        }).start();
    }

    /**
     * byteBuffer 转 byte数组
     * @param buffer
     * @return
     */
    public static byte[] bytebuffer2ByteArray(ByteBuffer buffer) {
        //获取buffer中有效大小
        int len = buffer.limit() - buffer.position();
        byte[] bytes = new byte[len];
        buffer.get(bytes);
        return bytes;
    }

    private void initMediaCodec() {
        String MIME_TYPE = "video/avc"; // H.264 类型
        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
    
        // 颜色格式
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        // 比特率
        format.setInteger(MediaFormat.KEY_BIT_RATE, 6000000);
        // 帧速率
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
        // I帧的帧率
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
    
        try {
            // 创建指定类型的编码器
            videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
            // 设置编码器属性
            videoEncoder.configure(format,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
            // 创建作为输入的 Surface
            inputSurface = videoEncoder.createInputSurface();
            videoEncoder.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void resetOutputFormat() {
        MediaFormat newFormat = videoEncoder.getOutputFormat();
        videoTrackIndex = mediaMuxer.addTrack(newFormat);
        mediaMuxer.start();
    }

    private void initMediaRecorder() {
        mediaRecorder = new MediaRecorder();
        // 设置音频来源
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        // 设置视频来源
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
        // 设置输出格式
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        // 设置输出文件
        String absolutePath = new File(recordFile + "/record.mp4").getAbsolutePath();
        mediaRecorder.setOutputFile(absolutePath);
        // 设置视频宽高
        mediaRecorder.setVideoSize(width,height);
        // 设置视频帧率
        mediaRecorder.setVideoFrameRate(60);
        // 设置视频编码比特率
        mediaRecorder.setVideoEncodingBitRate(6000000);
        // 设置音频编码
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
        // 设置视频编码
        mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
    
        try {
            mediaRecorder.prepare();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

源码
MediaRecordActivity.java
MediaRecordService.java

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270