Android录屏功能之MediaCodec简单实现

1,310 阅读4分钟

Android录屏功能流程图

Android录屏功能的流程图如下:

1720676963322.png

启动编码器之后,在MediaCodec.Callback的onOutputBufferAvailable方法中对video的数据进行处理,可以保存到本地文件,也可以返送到服务器设备进行实时显示本手机的屏幕。

MediaCodec简介

MediaCodec类可用于访问低级媒体编解码器,即编码器/解码器组件。它是 Android 低级多媒体支持基础结构的一部分。MediaCodec可以通过传递不同的参数,调用静态方法来创建软/硬编码器。软硬编解码器有各自的优缺点。软编码器使用CPU进行工作,兼容性较好,压缩率比较高。硬编码器使用GPU进行工作,性能优良,内存占用少,压缩率比较低。MediaCodec可以通过名称和类型两种方式开创建相应的实例对象:MediaCodec.createByCodecName("OMX.google.h264.encoder")软编码器和MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)硬编码器。选择h264这种视频格式的原因:免授权费,各方面的性能都比较均衡。Android支持的比较好还有h265,VP8,VP9等等格式。

Android录屏相关的名词介绍

MediaProjection:Android系统分配给应用可以录屏的类,只能分配给一个应用独占,其他应用可以抢占。比如应用A通过MediaProjectionManager请求并获得MediaProjection,应用B可以通过MediaProjectionManager请求并获得MediaProjection,这时候应用A丢失MediaProjection。 VirtualDisplay:创建虚拟显示。获取VirtualDisplay关联的surface,各自实现。本例子中VirtualDisplay用的是MediaCodec创建的。

MediaCodec状态

MediaCodec的状态如下图所示: image.png MediaCodec的三个方法,

  1. configure 配置编码的相关参数。
  2. start 启动编码器。
  3. stop 停止编码器。

1720677845188.png

给MediaCodec设置回调,编码帧可用时,会从回调返回。

/**
 * 给MediaCodec设置回调,编码帧可用时,会从回调返回
 */
private void setCallback() {
    mMediaCodec.setCallback(new MediaCodec.Callback() {
        @Override
        public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {}
        @Override
        public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
            // 编码的outputBuffer可用,即有新的视频帧
            if (index >= 0 && isRunning) {
                mBufferInfo = info;
                try {
                    encodeFrame(index);
                } catch (Exception e) {
                    e.printStackTrace();
                    Log.e(TAG, "onOutputBufferAvailable: Exception: e = " + e);
                }
            } else {
                Log.d(TAG, "onOutputBufferAvailable: encoder is not running");
            }
        }

        @Override
        public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
            e.printStackTrace();
            Log.e(TAG, "onError: encoder Exception: e = " + e);
            // mMediaCodec.reset();
        }

        @Override
        public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
            // 更新sps、pps
            resetOutputFormat();
        }
    });
}

MainActivity的代码如下:

package com.techcaicai.screenrecord;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;


import java.io.File;

public class MainActivity extends Activity {
    private static final int RECORD_REQUEST_CODE = 101;
    private static final String TAG = "MainActivity";

    Button screen_record_start;
    Button screen_record_stop;
    private  MediaProjection mMediaProjection;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 启动一个前台的service,录屏安全机制需要
        startService(new Intent(this, MediaService.class));
        screen_record_start = findViewById(R.id.screen_record_start);
        screen_record_stop = findViewById(R.id.screen_record_stop);
        screen_record_start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                requestRecording();
            }
        });
     }

    private void stopRecording() {
        // 验证停止后立即启动
        RecordManager.getInstance().stopRecord();
        RecordManager.getInstance().release();
        if (mMediaProjection != null) {
            mMediaProjection.stop();
            mMediaProjection = null;
        }
        Toast.makeText(this, "录制结束", Toast.LENGTH_SHORT).show();
    }

    protected void requestRecording() {
        MediaProjectionManager mMediaProjectionManage = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        if (mMediaProjectionManage != null) {
            Intent captureIntent = mMediaProjectionManage.createScreenCaptureIntent();
            startActivityForResult(captureIntent, RECORD_REQUEST_CODE);
        }
    }
   
   @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == RECORD_REQUEST_CODE) {
        if (resultCode == RESULT_OK) {
            //一键投屏
            Log.i(TAG, "允许投屏权限");
            MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
            mMediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
            startRecord();
        } else {
            Log.i(TAG, "拒绝投屏权限");
        }
    }
}

/**
 * 开始录制
 */
@SuppressLint("SimpleDateFormat")
private void startRecord() {
    File cacheDir = getExternalCacheDir();
    // /storage/emulated/0/Android/data/com.techcaicai.screenrecord/cache
    Log.d(TAG, "startRecord: ========= path" + cacheDir.getAbsolutePath());
    RecordManager.getInstance().setFilePath(cacheDir.getAbsolutePath() + "/caicai.h264");
    RecordManager.getInstance().startRecord(mMediaProjection);
    Toast.makeText(this, "录制开始", Toast.LENGTH_SHORT).show();
}
}

MainActivity如果mMediaProjection没有实例化,就通过requestRecording方法请求系统权限来获取,如果用户同意了系统请求,参考上面的onActivityResult方法,初始化mMediaProjection之后再实例化MediaCodec,配置MediaCodec,实例化VirtualDisplay,然后启动MediaCodec来录屏。还有一个需要注意的知识点就是在高版本的Android系统上录屏的时候需要显示一个前台通知,否则会应用会报错不能成功录屏,这就需要定义一个Service,在Service内部启动一个前台通知。Manifest配置如下:

<service android:name=".MediaService"
    android:enabled="true"
    android:foregroundServiceType="mediaProjection"
    android:exported="true"/>

Service的具体实现如下面代码:

package com.techcaicai.screenrecord;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.IBinder;

import androidx.core.app.NotificationCompat;

public class MediaService extends Service {
    private final String NOTIFICATION_CHANNEL_ID="com.techcaicai.screenrecord.MediaService";
    private final String NOTIFICATION_CHANNEL_NAME="com.techcaicai.screenrecord.channel_name";
    private final String NOTIFICATION_CHANNEL_DESC="com.techcaicai.screenrecord.channel_desc";
    public MediaService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        startNotification();
    }

    public void startNotification() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            //Call Start foreground with notification
            Intent notificationIntent = new Intent(this, MediaService.class);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
            NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
                    .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_foreground))
                    .setSmallIcon(R.drawable.ic_launcher_foreground)
                    .setContentTitle("Starting Service")
                    .setContentText("Starting monitoring service")
                    .setContentIntent(pendingIntent);
            Notification notification = notificationBuilder.build();
            NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
            channel.setDescription(NOTIFICATION_CHANNEL_DESC);
            NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
            notificationManager.createNotificationChannel(channel);
            startForeground(1, notification); //必须使用此方法显示通知,不能使用notificationManager.notify,否则还是会报上面的错误
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }
}


查看或者分析h264格式的数据,我比较喜欢用VLC media player工具,我们用adb pull命令把视频文件从手机中导出。

1720683431935.png 用VLC media player工具播放,效果图如下:

动画.gif 视频中还有一些花屏的现象,等有时间再做优化。

总结

音视频编解码是一个比较有意思的领域,可以在很多方面发挥作用,比如是手机投屏,远程会议,远程控制手机等等领域,希望文章对您有帮助,如果文中有问题,希望您不吝指教。