Android实现录屏直播(一)ScreenRecorder的简单分析

应项目需求瞄准了Bilibili的录屏直播功能,基本就仿着做一个吧。研究后发现Bilibili是使用的MediaProjection 与 VirtualDisplay结合实现的,需要 Android 5.0 Lollipop API 21以上的系统才能使用。

其实官方提供的android-ScreenCapture这个Sample中已经有了MediaRecorder的实现与使用方式,还有使用MediaRecorder实现的录制屏幕到本地文件的Demo,从中我们都能了解这些API的使用。

而如果需要直播推流的话就需要自定义MediaCodec,再从MediaCodec进行编码后获取编码后的帧,免去了我们进行原始帧的采集的步骤省了不少事。可是问题来了,因为之前没有仔细了解H264文件的结构与FLV封装的相关技术,其中爬了不少坑,此后我会一一记录下来,希望对用到的朋友有帮助。

项目中对我参考意义最大的一个Demo是网友Yrom的GitHub项目ScreenRecorder,Demo中实现了录屏并将视频流存为本地的MP4文件(咳咳,其实Yrom就是Bilibili的员工吧?( ゜- ゜)つロ)??。在此先大致分析一下该Demo的实现,之后我会再说明我的实现方式。

ScreenRecorder

具体的原理在Demo的README中已经说得很明白了:

  • Display 可以“投影”到一个 VirtualDisplay
  • 通过 MediaProjectionManager 取得的 MediaProjection创建VirtualDisplay
  • VirtualDisplay 会将图像渲染到 Surface中,而这个Surface是由MediaCodec所创建的
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
...
mSurface = mEncoder.createInputSurface();
...
mVirtualDisplay = mMediaProjection.createVirtualDisplay(name, mWidth, mHeight, mDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mSurface, null, null);
  • MediaMuxer 将从 MediaCodec 得到的图像元数据封装并输出到MP4文件中
int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US);
...
ByteBuffer encodedData = mEncoder.getOutputBuffer(index);
...
mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);

所以其实在Android 4.4上可以通过DisplayManager来创建VirtualDisplay也是可以实现录屏,但因为权限限制需要ROOT。 (see DisplayManager.createVirtualDisplay())

Demo很简单,两个Java文件:

  • MainActivity.java
  • ScreenRecorder.java

MainActivity

类中仅仅是实现的入口,最重要的方法是onActivityResult,因为MediaProjection就需要从该方法开启。但是别忘了先进行MediaProjectionManager的初始化

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    MediaProjection mediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
    if (mediaProjection == null) {
        Log.e("@@", "media projection is null");
        return;
    }
    // video size
    final int width = 1280;
    final int height = 720;
    File file = new File(Environment.getExternalStorageDirectory(),
            "record-" + width + "x" + height + "-" + System.currentTimeMillis() + ".mp4");
    final int bitrate = 6000000;
    mRecorder = new ScreenRecorder(width, height, bitrate, 1, mediaProjection, file.getAbsolutePath());
    mRecorder.start();
    mButton.setText("Stop Recorder");
    Toast.makeText(this, "Screen recorder is running...", Toast.LENGTH_SHORT).show();
    moveTaskToBack(true);
}

ScreenRecorder

这是一个线程,结构很清晰,run()方法中完成了MediaCodec的初始化,VirtualDisplay的创建,以及循环进行编码的全部实现。

线程主体

 @Override
public void run() {
    try {
        try {
            prepareEncoder();
            mMuxer = new MediaMuxer(mDstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        mVirtualDisplay = mMediaProjection.createVirtualDisplay(TAG + "-display",
                mWidth, mHeight, mDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
                mSurface, null, null);
        Log.d(TAG, "created virtual display: " + mVirtualDisplay);
        recordVirtualDisplay();
    } finally {
        release();
    }
}

MediaCodec的初始化

方法中进行了编码器的参数配置与启动、Surface的创建两个关键的步骤

private void prepareEncoder() throws IOException {
    MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
    format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
            MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // 录屏必须配置的参数
    format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
    format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
    format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
    Log.d(TAG, "created video format: " + format);
    mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
    mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    mSurface = mEncoder.createInputSurface(); // 需要在createEncoderByType之后和start()之前才能创建,源码注释写的很清楚
    Log.d(TAG, "created input surface: " + mSurface);
    mEncoder.start();
}

编码器实现循环编码

下面的代码就是编码过程,由于作者使用的是Muxer来进行视频的采集,所以在resetOutputFormat方法中实际意义是将编码后的视频参数信息传递给Muxer并启动Muxer。

private void recordVirtualDisplay() {
    while (!mQuit.get()) {
        int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US);
        Log.i(TAG, "dequeue output buffer index=" + index);
        if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            resetOutputFormat();
        } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
            Log.d(TAG, "retrieving buffers time out!");
            try {
                // wait 10ms
                Thread.sleep(10);
            } catch (InterruptedException e) {
            }
        } else if (index >= 0) {
            if (!mMuxerStarted) {
                throw new IllegalStateException("MediaMuxer dose not call addTrack(format) ");
            }
            encodeToVideoTrack(index);
            mEncoder.releaseOutputBuffer(index, false);
        }
    }
}
private void resetOutputFormat() {
    // should happen before receiving buffers, and should only happen once
    if (mMuxerStarted) {
        throw new IllegalStateException("output format already changed!");
    }
    MediaFormat newFormat = mEncoder.getOutputFormat();
    // 在此也可以进行sps与pps的获取,获取方式参见方法getSpsPpsByteBuffer()
    Log.i(TAG, "output format changed.\n new format: " + newFormat.toString());
    mVideoTrackIndex = mMuxer.addTrack(newFormat);
    mMuxer.start();
    mMuxerStarted = true;
    Log.i(TAG, "started media muxer, videoIndex=" + mVideoTrackIndex);
}

获取sps pps的ByteBuffer,注意此处的sps pps都是read-only只读状态

private void getSpsPpsByteBuffer(MediaFormat newFormat) {
    ByteBuffer rawSps = newFormat.getByteBuffer("csd-0");
    ByteBuffer rawPps = newFormat.getByteBuffer("csd-1");
}

录屏视频帧的编码过程

BufferInfo.flags表示当前编码的信息,如源码注释:

 /**
 * This indicates that the (encoded) buffer marked as such contains
 * the data for a key frame.
 */
public static final int BUFFER_FLAG_KEY_FRAME = 1; // 关键帧

/**
 * This indicated that the buffer marked as such contains codec
 * initialization / codec specific data instead of media data.
 */
public static final int BUFFER_FLAG_CODEC_CONFIG = 2; // 该状态表示当前数据是avcc,可以在此获取sps pps

/**
 * This signals the end of stream, i.e. no buffers will be available
 * after this, unless of course, {@link #flush} follows.
 */
public static final int BUFFER_FLAG_END_OF_STREAM = 4;

实现编码:

private void encodeToVideoTrack(int index) {
    ByteBuffer encodedData = mEncoder.getOutputBuffer(index);
    if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
        // The codec config data was pulled out and fed to the muxer when we got
        // the INFO_OUTPUT_FORMAT_CHANGED status.
        // Ignore it.
        // 大致意思就是配置信息(avcc)已经在之前的resetOutputFormat()中喂给了Muxer,此处已经用不到了,然而在我的项目中这一步却是十分重要的一步,因为我需要手动提前实现sps, pps的合成发送给流媒体服务器
        Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
        mBufferInfo.size = 0;
    }
    if (mBufferInfo.size == 0) {
        Log.d(TAG, "info.size == 0, drop it.");
        encodedData = null;
    } else {
        Log.d(TAG, "got buffer, info: size=" + mBufferInfo.size
                + ", presentationTimeUs=" + mBufferInfo.presentationTimeUs
                + ", offset=" + mBufferInfo.offset);
    }
    if (encodedData != null) {
        encodedData.position(mBufferInfo.offset);
        encodedData.limit(mBufferInfo.offset + mBufferInfo.size); // encodedData是编码后的视频帧,但注意作者在此并没有进行关键帧与普通视频帧的区别,统一将数据写入Muxer
        mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);
        Log.i(TAG, "sent " + mBufferInfo.size + " bytes to muxer...");
    }
}

以上就是对ScreenRecorder这个Demo的大体分析,由于总结时间仓促,很多细节部分我也没有进行深入的发掘研究,所以请大家抱着怀疑的态度阅读,如果说明有误或是理解不到位的地方,希望大家帮忙指出,谢谢!

参考文档

在功能的开发中还参考了很多有价值的资料与文章:

  1. Android屏幕直播方案
  2. Google官方的EncodeVirtualDisplayTest
  3. FLV文件格式解析
  4. 使用librtmp进行H264与AAC直播
  5. 后续更新…
时间: 2025-01-02 06:51:30

Android实现录屏直播(一)ScreenRecorder的简单分析的相关文章

游戏录屏直播的图文教程(基于云直播平台)

原创教程 ( 转载请注明出处 ) 2017-6-26,今天来做一下是电脑游戏桌面录屏直播的教程,就是把桌面的游戏直播出去,加上话筒做讲解.最终实现在电脑.手机.微信中都可以观看到游戏的直播和讲解画面. 提示:1. 本教程说的是游戏录屏直播的图文教程(基于云直播平台,不是基于自建流媒体直播平台) 2. 若要基于自建的流媒体平台,可以用OBS之类的软件来实现,OBS取流发送到自建平台上,实现直播. STEP1 . 硬件准备及设备连接 场景说明: 1.用户做一场电脑游戏桌面直播,实现PC端.手机端(A

Android自定义view教程06--Activity的绘制流程简单分析(基于android 4.0源码进行分析)

要明白这个流程,我们还得从第一部开始,大家都知道 在activity里面 setcontentview 调用结束以后 就可以看到程序加载好我们的布局文件了,从而让我们在手机上看到这个画面. 那么我们来看一下这个源码是如何实现的. 1 /** 2 * Set the activity content from a layout resource. The resource will be 3 * inflated, adding all top-level views to the activit

一键生成 Android 录屏 gif 的脚本

目的 编写 bash 脚本, 实现一行命令得到 Android 手机录制屏幕 gif 动图文件. 博主使用 ubuntu 系统, shell 为 bash. 这个脚本也可以用在 mac 系统上. 听说 windows 系统出了 ubuntu on windows, 不知道能不能使用这个脚本. 原理 adb shell screenrecord Android 4.4 版本后系统内预置了一个 screenrecord 命令, 可以用来将屏幕录制为 MP4 格式. 具体命令格式可以通过 –help

Android tips(九)-->Android录屏与转化gif图

转载请标明出处:一片枫叶的专栏 最近有同学问我android手机的录屏以及转化成gif图是如何实现的?今天正好就讲讲android的录屏与转化gif操作.整个Android系统的录制与转化GIF图是分为两个部分,录制过程与转化过程,下面就详细的介绍一下这两个部分的具体过程. android手机的录屏操作 android手机也有一些录制屏幕的软件,但是作为程序员还是推荐使用adb 命令实现对屏幕操作的录制操作的,而下面我们就介绍一下实现屏幕录制功能的adb命令:screenrecord. 关于sc

ffmpeg,rtmpdump和nginx rtmp实现录屏,直播和录制

公司最近在做视频直播的项目,我这里分配到对直播的视频进行录制,录制的方式是通过rtmpdump对rtmp的视频流进行录制 前置的知识 ffmpeg: 用于实现把录屏工具发出的视频和音频流,转换成我们需要的格式,然后发送到rtmp中转服务器上. rtmpdump: 用于实现视频的录制,从rtmp的中转服务器接受到视频流,并把视频流保存成flv文件 nginx-rtmp-module: 用户rtmp中转服务,虽然他可以做很多功能,但是我这里只是使用了这一个 screen capture: windo

android 调用 screenrecord 实现录屏

首先要说明的是并未实现,本文讲一下自己的思路. adb 使用shell 命令 screenrecord 可录屏. 自己写了个app,通过Process p = Runtime.getRuntime().exec(cmd)的方式调用shell命令,报错: java.lang.SecurityException: Permission Denial: broadcast asks to run as user -2 but is calling from user 0 需要android.permi

Android免Root录屏

首先确保你的Android系统版本是5.0+ 1.安装一个叫"游视秀"的手机应用,你可以直接扫描下方的二维码下载安装 2.打开“游视秀” 3.点击右上角“录屏”的图标 4.选择一个录屏方式,手机推荐“竖屏录制”平板推荐“横屏录制” 5.选择好录屏方式后会出现桌面会出现悬浮框,点击可以开始录屏,再次点击可以结束录屏 6.啊咧,怎么桌面TM的没有悬浮框啊!!!别着急,在“其他应用管理”->“游视秀”->“权限管理”->“显示悬浮窗”->“允许”,然后再次打开“游视秀

Android录屏应用开发研究

1截屏接口 在Android5.0之前如果希望截图屏幕,是需要获取系统root权限的.但在Android5.0之后Android开放了新的接口android.media.projection,开发者使用该接口,第三方应用程序无需再获取系统root权限也可以直接进行屏幕截图操作了.查询其官方api可知,该接口主要用来"屏幕截图"操作和"音频录制"操作,这里只讨论用于屏幕截图的功能.由于使用了媒体的映射技术手段,故截取的屏幕并不是真正的设备屏幕,而是截取的通过映射出来的

Android录屏命令、Android录Gif、Android录视频

NoHttp开源地址:https://github.com/yanzhenjie/NoHttp NoHttp具体使用文档已公布,你想知道的全都有,请点我移步! 版权声明:转载请注明本文转自严振杰的博客: http://blog.yanzhenjie.com 演示 大家看博客时常常看到以下这样的图片,都非常想知道怎么做的吧,好在自己写博客时也把操作录下来: 这个图是我还有一个博客的图讲Android三级联动和ListView单选多选的,博客和源代码传送门,如今呢就一步步教大家怎么来做这个图. 上方