Android项目直播

Android PC投屏简单尝试(录屏直播)2—硬解章(Medi

2018-11-08  本文已影响293人  deep_sadness

代码地址 :https://github.com/deepsadness/MediaProjectionDemo

想法来源

上一边文章的最后说使用录制的Api进行录屏直播。本来这边文章是预计在5月份完成的。结果过了这么久,终于有时间了。就来填坑了。

主要思路

使用MediaProjection示意图.png

整体流程就是通过创建VirtualDisplay,并且直接通过MediaCodec的Surface直接得到数据。通过MediaCodec得到编码完成之后的数据,进行 flv格式的封装,最后通过rtmp协议进行发送。


获取屏幕的截屏

1. 使用MediaCodec Surface

这部分基本上和上一遍文章相同,不同的就是使用MediaCodec来获取Surface

 @Override
    public @Nullable
    Surface createSurface(int width, int height) {
        mBufferInfo = new MediaCodec.BufferInfo();
        //创建视频的mediaFormat
        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
        //还需要对器进行插值。设置自己设置的一些变量
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
        if (VERBOSE) Log.d(TAG, "format: " + format);

        // 创建一个MediaCodec编码器,并且使用format 进行configure.然后将其 Get a Surface给VirtualDisplay
        try {
            mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
            mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mInputSurface = mEncoder.createInputSurface();
            //直接开启编码器
            mEncoder.start();
            //...省去部分代码
            return mInputSurface;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

2. 获取编码后的数据

    private void createEncoderThread() {
        HandlerThread encoder = new HandlerThread("Encoder");
        encoder.start();
        Looper looper = encoder.getLooper();
        workHanlder = new Handler(looper);
    }
        //这里的1s延迟是因为开启encoder之后,硬件编码器进行初始化需要点时间
         workHanlder.postDelayed(new Runnable() {
                @Override
                public void run() {
                    doExtract(mEncoder,null);}, 1000);

注意是的是,这里推入任务,需要稍微的延迟,因为初始化和开启硬件编码器需要一点时间。

    /**
     * 不断循环获取,直到我们手动结束.同步的方式
     * @param encoder       编码器
     * @param frameCallback 获取的回调
     */
    private void doExtract(MediaCodec encoder,
                           FrameCallback frameCallback) {
        final int TIMEOUT_USEC = 10000;
        long firstInputTimeNsec = -1;
        boolean outputDone = false;
        //没有手动停止,就只能不断进行
        while (!outputDone) {
            //如果手动停止了。就结束吧
            if (mIsStopRequested) {
                Log.d(TAG, "Stop requested");
                return;
            }
            //因为给编码器获取状态和喂数据的方法都直接通过Surface直接进行了,这里只要直接获取解码后的状态就可以了
            int decoderStatus = encoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
            if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // no output available yet
//                if (VERBOSE) Log.d(TAG, "no output from decoder available");
            } else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // not important for us, since we're using Surface
//                if (VERBOSE) Log.d(TAG, "decoder output buffers changed");
            } else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                //上面几种状态,我们都可以直接忽略。这里是进行MediaCodec开始编码后,会得到一个有cs-0 和cs-1的数据,对应sps和pps .获取之后,我们后面需要处理,所以先设置成一个回调就好。
                MediaFormat newFormat = encoder.getOutputFormat();
                if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);
                if (frameCallback != null) {
                    frameCallback.formatChange(newFormat);
                }
            } else if (decoderStatus < 0) {
                //这种情况下是出错了。暂时先直接出异常吧
                throw new RuntimeException(
                        "unexpected result from decoder.dequeueOutputBuffer: " +
                                decoderStatus);
            } else { // decoderStatus >= 0
                //这里是正确获取到编码后的数据了
                if (firstInputTimeNsec != 0) {
                    long nowNsec = System.nanoTime();
                    Log.d(TAG, "startup lag " + ((nowNsec - firstInputTimeNsec) / 1000000.0) + " ms");
                    firstInputTimeNsec = 0;
                }
                if (VERBOSE) Log.d(TAG, "surface decoder given buffer " + decoderStatus +
                        " (size=" + mBufferInfo.size + ")");
                //获取到最后的数据了。这里就跳出循环。我们这个地方基本也不用用到
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    if (VERBOSE) Log.d(TAG, "output EOS");
                    outputDone = true;
                }
                
                //当size 大于0时,需要送显
                boolean doRender = (mBufferInfo.size != 0);
                //这个时候,来获取编码后的buffer,回调给外面
                if (doRender && frameCallback != null) {
                    ByteBuffer outputBuffer = encoder.getOutputBuffer(decoderStatus);
                    frameCallback.render(mBufferInfo, outputBuffer);
                }
                encoder.releaseOutputBuffer(decoderStatus, doRender);
            }
        }
    }

通过这样的循环获取,就可以通过回调获取编码后的数据了。
后面,我们可以将编码后的数据进行让rtmp推流。


使用 RTMP 推流

  1. 认识 rtmp 协议
  2. RMTP Connection
  3. 代码

1. 认识 rtmp 协议

RTMP协议是Real Time Message Protocol(实时信息传输协议)的缩写,它是由Adobe公司提出的一种应用层的协议,用来解决多媒体数据传输流的多路复用(Multiplexing)和分包(packetizing)的问题。

2. RTMP Connection

握手(HandShake)

一个RTMP连接以握手开始,双方分别发送大小固定的三个数据块

  1. 握手开始于客户端发送C0、C1块。服务器收到C0或C1后发送S0和S1。
  2. 当客户端收齐S0和S1后,开始发送C2。当服务器收齐C0和C1后,开始发送S2。
  3. 当客户端和服务器分别收到S2和C2后,握手完成。
image

理论上来讲只要满足以上条件,如何安排6个Message的顺序都是可以的,但实际实现中为了在保证握手的身份验证功能的基础上尽量减少通信的次数,一般的发送顺序是这样的:

  1. Client发送C0+C1到Sever
  2. Server发送S0+S1+S2到Client
  3. Client发送C2到Server,握手完成
建立网络连接(NetConnection)
  1. 客户端发送命令消息中的“连接”(connect)到服务器,请求与一个服务应用实例建立连接。
  2. 服务器接收到连接命令消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到客户端,同时连接到连接命令中提到的应用程序。
  3. 服务器发送设置带宽(Set Peer Bandwitdh)协议消息到客户端。
  4. 客户端处理设置带宽协议消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到服务器端。
  5. 服务器发送用户控制消息中的“流开始”(Stream Begin)消息到客户端。
  6. 服务器发送命令消息中的“结果”(_result),通知客户端连接的状态。
  7. 客户端在收到服务器发来的消息后,返回确认窗口大小,此时网络连接创建完成。

服务器在收到客户端发送的连接请求后发送如下信息:

image

主要是告诉客户端确认窗口大小,设置节点带宽,然后服务器把“连接”连接到指定的应用并返回结果,“网络连接成功”。并且返回流开始的的消息(Stream Begin 0)。

建立网络流(NetStream)
  1. 客户端发送命令消息中的“创建流”(createStream)命令到服务器端。
  2. 服务器端接收到“创建流”命令后,发送命令消息中的“结果”(_result),通知客户端流的状态。
推流流程
  1. 客户端发送publish推流指令。
  2. 服务器发送用户控制消息中的“流开始”(Stream Begin)消息到客户端。
  3. 客户端发送元数据(分辨率、帧率、音频采样率、音频码率等等)。
  4. 客户端发送音频数据。
  5. 客户端发送服务器发送设置块大小(ChunkSize)协议消息。
  6. 服务器发送命令消息中的“结果”(_result),通知客户端推送的状态。
  7. 客户端收到后,发送视频数据直到结束。
推流流程
播流流程
  1. 客户端发送命令消息中的“播放”(play)命令到服务器。
  2. 接收到播放命令后,服务器发送设置块大小(ChunkSize)协议消息。
  3. 服务器发送用户控制消息中的“streambegin”,告知客户端流ID。
  4. 播放命令成功的话,服务器发送命令消息中的“响应状态” NetStream.Play.Start & NetStream.Play.reset,告知客户端“播放”命令执行成功。
  5. 在此之后服务器发送客户端要播放的音频和视频数据。
播流流程

3. 代码集成

1. 集成RTMP

直接使用librestreaming 中的RTMP的代码,将其放到CMake中进行编译。

cmake_minimum_required(VERSION 3.4.1)
add_definitions("-DNO_CRYPTO")
include_directories(${CMAKE_SOURCE_DIR}/libs/rtmp/librtmp)
#native-lib
file(GLOB PROJECT_SOURCES "${CMAKE_SOURCE_DIR}/libs/rtmp/librtmp/*.c")
add_library(rtmp-lib
        SHARED
        src/main/cpp/rtmp-hanlde.cpp
        ${PROJECT_SOURCES}
        )
find_library( # Sets the name of the path variable.
        log-lib
        log)
target_link_libraries( # Specifies the target library.
        rtmp-lib
        ${log-lib})
public class RtmpClient {
    static {
        System.loadLibrary("rtmp-lib");
    }

    /**
     * @param url
     * @param isPublishMode
     * @return rtmpPointer ,pointer to native rtmp struct
     */
    public static native long open(String url, boolean isPublishMode);
    public static native int write(long rtmpPointer, byte[] data, int size, int type, int ts);
    public static native int close(long rtmpPointer);
    public static native String getIpAddr(long rtmpPointer);
}

2. RMTP推流

之前的文章,有分析过FLV的数据格式。这样还需要再将编码后的数据。
这里就不赘述了。

RTMP连接部分整体的流程
  1. 连接RTMP URL
    整体的连接的过程。上面的了解也有提到过。
   const char *url = env->GetStringUTFChars(url_, 0);
    LOGD("RTMP_OPENING:%s", url);
    //分配RTMP对象
    RTMP *rtmp = RTMP_Alloc();
    if (rtmp == NULL) {
        LOGD("RTMP_Alloc=NULL");
        return NULL;
    }
    
    //初始化RTMP
    RTMP_Init(rtmp);
    int ret = RTMP_SetupURL(rtmp, const_cast<char *>(url));

    if (!ret) {
        RTMP_Free(rtmp);
        rtmp = NULL;
        LOGD("RTMP_SetupURL=ret");
        return NULL;
    }
    if (isPublishMode) {
        RTMP_EnableWrite(rtmp);
    }
    //2. 开始Connect 。建立网络连接的过程。其中包括握手
    ret = RTMP_Connect(rtmp, NULL);
    if (!ret) {
        RTMP_Free(rtmp);
        rtmp = NULL;
        LOGD("RTMP_Connect=ret");
        return NULL;
    }
    //3. create stream 建立网络流的过程
    ret = RTMP_ConnectStream(rtmp, 0);

    if (!ret) {
        ret = RTMP_ConnectStream(rtmp, 0);
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
        rtmp = NULL;
        LOGD("RTMP_ConnectStream=ret");
        return NULL;
    }
    env->ReleaseStringUTFChars(url_, url);
    LOGD("RTMP_OPENED");
  1. 在得到MediaFormat回调时,将其进行推流发送,进行publish
  2. 不断得到编码后的数据,不断推流
    这两者主要的不同,在编码上就是type不同。我们知道第一个message必须为一个完整的message,必须为meta_data才可以。
  jbyte *buffer = env->GetByteArrayElements(data_, NULL);
    LOGD("start write");
    RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket));
    RTMPPacket_Alloc(packet, size);
    RTMPPacket_Reset(packet);
    if (type == RTMP_PACKET_TYPE_INFO) { // metadata
        packet->m_nChannel = 0x03;
    } else if (type == RTMP_PACKET_TYPE_VIDEO) { // video
        packet->m_nChannel = 0x04;
    } else if (type == RTMP_PACKET_TYPE_AUDIO) { //audio
        packet->m_nChannel = 0x05;
    } else {
        packet->m_nChannel = -1;
    }
    RTMP *r = (RTMP *) rtmpPointer;
    packet->m_nInfoField2 = r->m_stream_id;

    LOGD("write data type: %d, ts %d", type, ts);

    memcpy(packet->m_body, buffer, size);
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    packet->m_hasAbsTimestamp = FALSE;
    packet->m_nTimeStamp = ts;
    packet->m_packetType = type;
    packet->m_nBodySize = size;
    int ret = RTMP_SendPacket((RTMP *) rtmpPointer, packet, 0);
    RTMPPacket_Free(packet);
    free(packet);
    env->ReleaseByteArrayElements(data_, buffer, 0);
    if (!ret) {
        LOGD("end write error %d", ret);
        return ret;
    } else {
        LOGD("end write success");
        return 0;
    }
  1. 最后关闭
    RTMP_Close((RTMP *) rtmpPointer);
    RTMP_Free((RTMP *) rtmpPointer);
接受编码后的数据回调
  workHanlder.postDelayed(new Runnable() {
                @Override
                public void run() {
                    doExtract(mEncoder, new FrameCallback() {

                        @Override
                        public void render(MediaCodec.BufferInfo info, ByteBuffer outputBuffer) {
                            Sender.getInstance().rtmpSend(info, outputBuffer);
                        }

                        @Override
                        public void formatChange(MediaFormat mediaFormat) {
                            Sender.getInstance().rtmpSendFormat(mediaFormat);
                        }
                    });
                }
            }, 1000);
通过回调MediaFormat

之前对flv的格式详解,我们知道要实现flv推流。
需要将cs0 和cs1的头部位置进行推流才能正常显示。并且必须作为第一条信息。
这里通过这方法读取cs0 和cs1

public static byte[] generateAVCDecoderConfigurationRecord(MediaFormat mediaFormat) {
            ByteBuffer SPSByteBuff = mediaFormat.getByteBuffer("csd-0");
            SPSByteBuff.position(4);
            ByteBuffer PPSByteBuff = mediaFormat.getByteBuffer("csd-1");
            PPSByteBuff.position(4);
            int spslength = SPSByteBuff.remaining();
            int ppslength = PPSByteBuff.remaining();
            int length = 11 + spslength + ppslength;
            byte[] result = new byte[length];
            SPSByteBuff.get(result, 8, spslength);
            PPSByteBuff.get(result, 8 + spslength + 3, ppslength);
            /**
             * UB[8]configurationVersion
             * UB[8]AVCProfileIndication
             * UB[8]profile_compatibility
             * UB[8]AVCLevelIndication
             * UB[8]lengthSizeMinusOne
             */
            result[0] = 0x01;
            result[1] = result[9];
            result[2] = result[10];
            result[3] = result[11];
            result[4] = (byte) 0xFF;
            /**
             * UB[8]numOfSequenceParameterSets
             * UB[16]sequenceParameterSetLength
             */
            result[5] = (byte) 0xE1;
            ByteArrayTools.intToByteArrayTwoByte(result, 6, spslength);
            /**
             * UB[8]numOfPictureParameterSets
             * UB[16]pictureParameterSetLength
             */
            int pos = 8 + spslength;
            result[pos] = (byte) 0x01;
            ByteArrayTools.intToByteArrayTwoByte(result, pos + 1, ppslength);

            return result;
        }

根据flv格式的分析。填充到flv中

 public static void fillFlvVideoTag(byte[] dst, int pos, boolean isAVCSequenceHeader, boolean isIDR, int readDataLength) {
            //FrameType&CodecID
            dst[pos] = isIDR ? (byte) 0x17 : (byte) 0x27;
            //AVCPacketType
            dst[pos + 1] = isAVCSequenceHeader ? (byte) 0x00 : (byte) 0x01;
            //LAKETODO CompositionTime
            dst[pos + 2] = 0x00;
            dst[pos + 3] = 0x00;
            dst[pos + 4] = 0x00;
            if (!isAVCSequenceHeader) {
                //NALU HEADER
                ByteArrayTools.intToByteArrayFull(dst, pos + 5, readDataLength);
            }
        }

然后发送。

发送实际数据
 public static RESFlvData sendRealData(long tms, ByteBuffer realData) {
        int realDataLength = realData.remaining();
        int packetLen = Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +
                Packager.FLVPackager.NALU_HEADER_LENGTH +
                realDataLength;
        byte[] finalBuff = new byte[packetLen];
        realData.get(finalBuff, Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +
                        Packager.FLVPackager.NALU_HEADER_LENGTH,
                realDataLength);
        int frameType = finalBuff[Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +
                Packager.FLVPackager.NALU_HEADER_LENGTH] & 0x1F;
        Packager.FLVPackager.fillFlvVideoTag(finalBuff,
                0,
                false,
                frameType == 5,
                realDataLength);
        RESFlvData resFlvData = new RESFlvData();
        resFlvData.droppable = true;
        resFlvData.byteBuffer = finalBuff;
        resFlvData.size = finalBuff.length;
        resFlvData.dts = (int) tms;
        resFlvData.flvTagType = RESFlvData.FLV_RTMP_PACKET_TYPE_VIDEO;
        resFlvData.videoFrameType = frameType;
        return resFlvData;
//        dataCollecter.collect(resFlvData, RESRtmpSender.FROM_VIDEO);
    }

RMTP服务器

RMTP服务器的建立,可以简单的使用
RMTP服务器

总结

对比之前的一遍文章

Android PC投屏简单尝试

参考文章

Android实现录屏直播(一)ScreenRecorder的简单分析
直播推流实现RTMP协议的一些注意事项

投屏尝试系列文章

上一篇下一篇

猜你喜欢

热点阅读