Android录屏通过udp共享到其它手机

###简介
注:因为是视频学习demo,在传输块没有做udp分包、并包处理,喂的帧数据不全导致了花屏等问题,有兴趣可以做分并包,关键字 40964。(2019.07.24
设备是华为平板和红米note4

发送的是H264裸流到其它设备上解码显示,H264可见我整理的笔记:

ttp://blog.csdn.net/pds574834424/article/details/78150474

整体项目结构和使用方式如下:
这里写图片描述
这里写图片描述
先在手机上运行ScreenReceiveDemo接收APP,再在其它手机上运行主项目app并输入接收APP手机的IP地址。

###录屏发送
5.0开放了录屏API,在使用录屏时会用DialogAcitivty(部分奇葩ROM里没有此类会报错,建议处理catch)的形式提示用户是否授权。
主Activity如下:

private static final int ACTIVITY_RESULT_CODE = 110;
    private MediaProjectionManager projectionManager;
    private Context context;
    private TextView main_demo_click_txt;
    private VideoEncoderUtil videoEncoder;
    private EditText main_demo_edit;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        setContentView(R.layout.activity_main);
        context = this;
        initView();
    }


    private void initView() {
        projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);

        main_demo_edit = (EditText) findViewById(R.id.main_demo_edit);
        main_demo_edit.setText("192.168.31.193");
        main_demo_click_txt = (TextView) findViewById(R.id.main_demo_click_txt);

        main_demo_click_txt.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(main_demo_click_txt.getText().equals(context.getString(R.string.open_share))){
                    if(!NetworkUtil.ipCheck(main_demo_edit.getText().toString())){
                        ToastUtil.makeText(context, "大兄弟IP不对");
                        return;
                    }
                    Intent captureIntent = projectionManager.createScreenCaptureIntent();
                    startActivityForResult(captureIntent, ACTIVITY_RESULT_CODE);
                }else{
                    videoEncoder.stop();
                    main_demo_click_txt.setText(context.getString(R.string.open_share));
                }
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == ACTIVITY_RESULT_CODE && resultCode == RESULT_OK) {
            MediaProjection mediaProjection = projectionManager.getMediaProjection(resultCode, data);
            videoEncoder = new VideoEncoderUtil(mediaProjection, main_demo_edit.getText().toString());
            videoEncoder.start();
            main_demo_click_txt.setText(context.getString(R.string.close_share));
        }
    }

在onActivityResult拿到MediaProjectionManager管理下的MediaProjection,MediaProjection主要是用来授予应用程序捕获屏幕内容或记录系统音频的能力。

主要发送代码:

private class Encoder implements Runnable {

        String MIME_TYPE = "video/avc";//编码格式,  h264
        int VIDEO_FRAME_PER_SECOND = 20;//fps
        int VIDEO_I_FRAME_INTERVAL = 5;
        private int mWidth = 1280;//大屏上会因为分辨率显示马赛克
        private int mHeight = 720;
        private int VIDEO_BITRATE = 2 * 1024 * 1024; //2M码率
//      private int VIDEO_BITRATE = 500 * 1024; //500K码率,有兴趣的可以看看2M和500K的区别~
        /**
         * 子线程的hanlder
         */
        private Handler threadHandler;
        private DatagramSocket mDatagramSocket;
        private MediaCodec mCodec;
        private Surface mSurface;
        private Bundle params = new Bundle();

        Encoder() {
            try {
                if(mDatagramSocket == null){
                    mDatagramSocket = new DatagramSocket(null);
                    mDatagramSocket.setReuseAddress(true);
                    mDatagramSocket.bind(new InetSocketAddress(6666));
                }
            } catch (SocketException e) {
                e.printStackTrace();
            }
            params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);//做Bundle初始化  主要目的是请求编码器“即时”产生同步帧
            prepare();
        }

        @Override
        public void run() {
            Looper.prepare();
            threadHandler = new Handler() {

                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    byte[] dataFrame = (byte[]) msg.obj;
                    int frameLength = dataFrame.length;
                    byte[] lengthByte = TyteUtil.intToByteArray(frameLength);
                    byte[] concat = ArrayUtil.concat(lengthByte, dataFrame);
                    try {
                        DatagramPacket dp = new DatagramPacket(concat, concat.length, InetAddress.getByName(ip), 2333);
                        mDatagramSocket.send(dp);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

            };
            Looper.loop();
        }

        void sendData(byte[] data) {
            Message message = new Message();
            message.obj = data;
            threadHandler.sendMessage(message);
        }

        private void release() {
            onSurfaceDestroyed(mSurface);
            if (mCodec != null) {
                mCodec.stop();
                mCodec.release();
                mCodec = null;
            }
        }
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        private boolean prepare() {
            MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
            //COLOR_FormatSurface这里表明数据将是一个graphicbuffer元数据s
            format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            format.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_BITRATE);//编码器需要, 解码器可选
            format.setInteger(MediaFormat.KEY_FRAME_RATE, VIDEO_FRAME_PER_SECOND);
            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, VIDEO_I_FRAME_INTERVAL);//帧间隔  这个参数在很多手机上无效, 第二帧关键帧过了之后全是P帧。 GOP实现还有其它方法,全局搜关键字GOP

            try {
                mCodec = MediaCodec.createEncoderByType(MIME_TYPE);
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
            mCodec.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) {
                    if (index > -1) {
                        ByteBuffer outputBuffer = codec.getOutputBuffer(index);
                        byte[] data = new byte[info.size];
                        assert outputBuffer != null;
                        outputBuffer.get(data);
                        sendData(data);
                        codec.releaseOutputBuffer(index, false);
                    }
                    if (System.currentTimeMillis() - timeStamp >= secondFrame) {//5秒后,设置请求关键帧的参数    GOP
                        timeStamp = System.currentTimeMillis();
                        codec.setParameters(params);
                    }
                }

                @Override
                public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
                    codec.reset();
                }

                @Override
                public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {

                }
            });
            mCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            //创建关联的输入surface
            mSurface = mCodec.createInputSurface();
            mCodec.start();
            onSurfaceCreated(mSurface, mWidth, mHeight);
            return true;
        }

关键方法onSurfaceBind把屏幕数据与编码器创建用来代替输入缓冲区的surface进行关联,在输出缓冲区onOutputBufferAvailable即可得到H264数据,注意GOP是在输出缓冲区做的。UDP的代码比较简单,写的也比较粗糙,主要是在前四位合并了一个数据总长度。如果接收方没有正常显示发送方的页面,请在接收方检查是否接收到数据。还是没数据的话,可以在电脑上用NetAssist软件检查,把屏幕发送的ip改成电脑ip尝试即可。

###解码显示
主要解码部分代码:

public void onFrame(byte[] buf) {
        if (buf == null)
            return;
        int length = buf.length;
        ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();//拿到输入缓冲区,用于传送数据进行编码
        //返回一个填充了有效数据的input buffer的索引,如果没有可用的buffer则返回-1.当timeoutUs==0时,该方法立即返回;当timeoutUs<0时,无限期地等待一个可用的input buffer;当timeoutUs>0时,至多等待timeoutUs微妙
//        int inputBufferIndex = mediaCodec.dequeueInputBuffer(1);// =>0时,至多等待x微妙   如果发送源快速滑动比如放视频, 花屏明显.. ...
       int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);//    <-1时,无限期地等待一个可用的input buffer  会出现:一直等待导致加载异常, 甚至会吃掉网络通道, 没有任何异常出现...(调试中大部分是因为sps和pps没有写入到解码器, 保证图像信息的参数写入解码器很重要)
        if (inputBufferIndex >= 0) {//当输入缓冲区有效时,就是>=0
            ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
            inputBuffer.clear();
            inputBuffer.put(buf, 0, length);//往输入缓冲区写入数据,关键点
//            int value = buf[4] & 0x0f;//nalu, 5是I帧, 7是sps 8是pps.
//            if (value == 7)//如果不能保证第一帧写入的是sps和pps, 可以用这种方式等待sps和pps发送到之后写入解码器
//                mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, mCount * 30, MediaCodec.BUFFER_FLAG_CODEC_CONFIG);//更新sps和pps
//            else
                mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, mCount * 30, 0);//将缓冲区入队
            mCount++;//用于queueInputBuffer presentationTimeUs 此缓冲区的显示时间戳(以微秒为单位),通常是这个缓冲区应该呈现的媒体时间
        }

        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);//拿到输出缓冲区的索引
//        Log.e(TAG, "outputBufferIndex" + outputBufferIndex);
        while (outputBufferIndex >= 0) {
            mediaCodec.releaseOutputBuffer(outputBufferIndex, true);//显示并释放资源
            outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);//再次获取数据,如果没有数据输出则outIndex=-1 循环结束
        }
    }

解码显示比较简单,类里的备注也很多。注意需要优先初始化接收方,再发送,因为解码器需要喂入相应的sps和pps参数再加上关键帧才能达到理论上的秒开效果,并且不优先喂入此数据,会产生黑屏,花屏等异常。UDP的接收长度是写死的,前面四位是数据总长度,后面数据是实际的H264数据。

###DEMO代码

https://github.com/PengSen/ScreenRecordDemo

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值