手寫 Android 錄屏直播

簡介

觀看手游直播時,我們觀眾端看到的是選手的屏幕上的內(nèi)容洒疚,這是如何實現(xiàn)的呢歹颓?這篇博客將手寫一個錄屏直播 Demo,實現(xiàn)類似手游直播的效果

獲取屏幕數(shù)據(jù)很簡單油湖,Android 系統(tǒng)有提供對應(yīng)的服務(wù)巍扛,難點在于傳輸數(shù)據(jù)到直播服務(wù)器,我們使用 RtmpDump 來傳輸 Rtmp 數(shù)據(jù)乏德,由于 RtmpDump 使用 C 語言實現(xiàn)撤奸,我們還需要用到 NDK 開發(fā),單單用 Java 無法實現(xiàn)哈鹅经,當(dāng)然如果不怕麻煩的話寂呛,還可以自己編譯 Ffmpeg 實現(xiàn) Rtmp 推流怎诫,B 站開源的 ijkplayer 播放器也是基于 Ffmpeg 來開發(fā)的

實現(xiàn)效果

最終我們推流到 B 站直播間瘾晃,在直播間能夠?qū)崟r看到我們手機屏幕上的畫面


image.png

基本流程

  • 獲取錄屏數(shù)據(jù)
  • 對數(shù)據(jù)進(jìn)行 h264 編碼
  • Rtmp 數(shù)據(jù)包
  • 上傳到直播服務(wù)器推流地址

獲取錄屏數(shù)據(jù)

通過 Intent 獲取到 MediaProjectionService,繼而獲取到 Mediaprojection 的 VirtualCanvas幻妓,我們錄屏的原始數(shù)據(jù)就是從中得來的

    private void initLive() {
        mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        Intent screenRecordIntent = mediaProjectionManager.createScreenCaptureIntent();
        startActivityForResult(screenRecordIntent,100);
    }
    
    
        @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == 100 && resultCode == Activity.RESULT_OK) {
            //MediaProjection--->產(chǎn)生錄屏數(shù)據(jù)
            mediaProjection = mediaProjectionManager.getMediaProjection
                    (resultCode, data);
        }
    }


對數(shù)據(jù)進(jìn)行 h264 編碼

通過 MediaProjection 獲取到的 YUV 裸數(shù)據(jù)蹦误,我們先需要對其進(jìn)行 h264 編碼,此時我們使用原生 MediaCodec 進(jìn)行硬件編碼

    public void start(MediaProjection mediaProjection){
        this.mediaProjection = mediaProjection;
        // 配置 MediaCodec
        MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,width,height);
        // 顏色格式
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 400_000);
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
        // 設(shè)置觸發(fā)關(guān)鍵幀的時間間隔為 2 s
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);

        // 創(chuàng)建編碼器
        try {
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
            mediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
            Surface surface = mediaCodec.createInputSurface();
            mediaProjection.createVirtualDisplay(TAG,width,height,1, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
            ,surface,null,null);
        } catch (IOException e) {
            e.printStackTrace();
        }
        start();
    }
    
    
    @Override
    public void run() {
        isLiving = true;
        mediaCodec.start();
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

        while (isLiving){
            //若時間差大于 2 s肉津,則通知編碼器强胰,生成 I 幀
            if (System.currentTimeMillis() - timeStamp >= 2000){
                // Bundle 通知 Dsp
                Bundle msgBundle = new Bundle();
                msgBundle.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME,0);
                mediaCodec.setParameters(msgBundle);
                timeStamp = System.currentTimeMillis();
            }
            // 接下來就是 MediaCodec 常規(guī)操作,獲取 Buffer 可用索引妹沙,這里不需要獲取輸出索引偶洋,內(nèi)部已經(jīng)操作了
            int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,100_000);
            if (outputBufferIndex >=0){
                // 獲取到了
                ByteBuffer byteBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);
                byte[] outData = new byte[bufferInfo.size];
                byteBuffer.get(outData);
            }
        }
    }

Rtmp 數(shù)據(jù)包

經(jīng)過上面兩步,我們獲得了編碼好的 h264 數(shù)據(jù)距糖,接下來封裝 Rtmp 就比較頭疼了(Ndk 的知識都忘得差不多了)

首先我們在項目的 cpp 文件中玄窝,把 Rtmpdump 的源代碼導(dǎo)入,我們使用 rtmpdump 連接服務(wù)器悍引,以及傳輸 Rtmp 數(shù)據(jù)恩脂,要知道目前手里的數(shù)據(jù)還是 h264 碼流,是無法直接傳輸趣斤,需要封裝成 Rtmp 數(shù)據(jù)包

使用第三方庫 Rtmpdump 來實現(xiàn)推流到直播服務(wù)器俩块,由于 Rtmpdump 的代碼量不是很多,我們直接拷貝源代碼到 Android 的 cpp 文件,如果需要用到 Ffmpeg 不能才用該種調(diào)用方式了玉凯,需要提前編譯好 so 庫文件

對 Rtmp 暫時不需要做太深刻的理解,因為很容易給自己繞進(jìn)去,用 Rtmp 傳輸 h264 數(shù)據(jù),那么 sps,pps ,以及關(guān)鍵幀怎么擺放,Rtmp 都已經(jīng)規(guī)定好了,我們就需要使用 NDK 的方式,實現(xiàn) rtmp 數(shù)據(jù)的填充

RtmpDump 的使用

image.png
  • 連接服務(wù)器
  1. RTMP_Init(RTMP *r) 初始化
  2. RTMP_EnableWrite(RTMP *r) 配置開啟數(shù)據(jù)寫入
  3. RTMP_Connect(RTMP *r, RTMPPacket *cp)
  4. RTMP_ConnectStream(RTMP *r, int seekTime)
  • 發(fā)送數(shù)據(jù)
  1. RTMPPacket_Alloc(RTMPPacket *p, int nSize)
  2. RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue)
  3. RTMPPacket_Free(RTMPPacket *p)

連接直播服務(wù)器

這一步中势腮,需要預(yù)先準(zhǔn)備直播推流地址,然后實現(xiàn) native 方法


extern "C" JNIEXPORT jboolean JNICALL
Java_com_bailun_kai_rtmplivedemo_RtmpPack2Remote_connectLiveServer(JNIEnv *env, jobject thiz,
                                                                   jstring url) {
    // 首先 Java 的轉(zhuǎn)成 C 的字符串漫仆,不然無法使用
    const char *live_url = env->GetStringUTFChars(url,0);
    int result;

    do {
     // 結(jié)構(gòu)體對象分配內(nèi)存
     livePack = (LivePack *)(malloc(sizeof(LivePack)));
     // 清空內(nèi)存上的臟數(shù)據(jù)
     memset(livePack,0,sizeof(LivePack));
     // Rtmp 申請內(nèi)存
     livePack->rtmp = RTMP_Alloc();
     RTMP_Init(livePack->rtmp);
     // 設(shè)置 rtmp 初始化參數(shù)嫉鲸,比如超時時間、url
     livePack->rtmp->Link.timeout = 10;
     LOGI("connect %s", url);

     if (!(result = RTMP_SetupURL(livePack->rtmp,(char *)live_url))){
         break;
     }
     // 開啟 Rtmp 寫入
     RTMP_EnableWrite(livePack->rtmp);
     LOGI("RTMP_Connect");
     if (!(result = RTMP_Connect(livePack->rtmp,0))){
         break;
     }
     LOGI("RTMP_ConnectStream ");
     if (!(result = RTMP_ConnectStream(livePack->rtmp, 0)))
         break;
     LOGI("connect success");
    }while (0);

    if (!result && livePack){
        free(livePack);
        livePack = nullptr;
    }
    env->ReleaseStringUTFChars(url,live_url);
    return result;
}

發(fā)送數(shù)據(jù)到直播服務(wù)器

image.png

有意思的是歹啼,Rtmp 協(xié)議中不需要傳遞分隔符(h264 分隔符為 0 0 0 1),并且推流的第一個 Rtmp 包的內(nèi)容為 sps玄渗、pps 等


// 發(fā)送 rtmp 數(shù)據(jù)到服務(wù)器
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_bailun_kai_rtmplivedemo_RtmpPack2Remote_sendData2Server(JNIEnv *env, jobject thiz,
                                                                 jbyteArray buffer, jint length,
                                                                 jlong tms) {
    int result;
    // 拷貝數(shù)據(jù)
    jbyte *bufferArray = env->GetByteArrayElements(buffer, 0);
    result = sendDataInner(bufferArray,length,tms);
    //釋放內(nèi)存
    env->ReleaseByteArrayElements(buffer,bufferArray,0);
    return result;
}



int sendDataInner(jbyte *array, jint length, jlong tms) {
    int result = 0;
    //處理 sps、pps
    if (array[4] == 0x67){
        // 讀取 sps狸眼,pps 數(shù)據(jù)藤树,保存到結(jié)構(gòu)體中
       readSpsPps(array,length,livePack);
       return result;
    }


    //處理 I幀,其他幀
    if(array[4] == 0x65){
      RTMPPacket * spsPpsPacket = createRtmpSteramPack(livePack);
      sendPack(spsPpsPacket);
    }

    RTMPPacket* rtmpPacket = createRtmpPack(array,length,tms,livePack);
    result = sendPack(rtmpPacket);
    return result;
}


int sendPack(RTMPPacket *pPacket) {
    int result = RTMP_SendPacket(livePack->rtmp,pPacket,1);
    RTMPPacket_Free(pPacket);
    free(pPacket);
    return result;
}



// 發(fā)送 sps 拓萌,pps 對應(yīng)的 Rtmp 包
RTMPPacket *createRtmpSteramPack(LivePack *pack) {
    //  創(chuàng)建 Rtmp 數(shù)據(jù)包岁钓,對應(yīng) RtmpDump 庫的 RTMPPacket 結(jié)構(gòu)體
    int body_size = 16 + pack->sps_len + pack->pps_len;
    RTMPPacket  *rtmpPacket = static_cast<RTMPPacket *>(malloc(sizeof(RTMPPacket)));
    RTMPPacket_Alloc(rtmpPacket,body_size);
    int index = 0;
    rtmpPacket->m_body[index++] = 0x17;
    //AVC sequence header 設(shè)置為0x00
    rtmpPacket->m_body[index++] = 0x00;
    //CompositionTime
    rtmpPacket->m_body[index++] = 0x00;
    rtmpPacket->m_body[index++] = 0x00;
    rtmpPacket->m_body[index++] = 0x00;
    //AVC sequence header
    rtmpPacket->m_body[index++] = 0x01;
//    原始 操作

    rtmpPacket->m_body[index++] = pack->sps[1]; //profile 如baseline、main微王、 high

    rtmpPacket->m_body[index++] = pack->sps[2]; //profile_compatibility 兼容性
    rtmpPacket->m_body[index++] = pack->sps[3]; //profile level
    rtmpPacket->m_body[index++] = 0xFF;//已經(jīng)給你規(guī)定好了
    rtmpPacket->m_body[index++] = 0xE1; //reserved(111) + lengthSizeMinusOne(5位 sps 個數(shù)) 總是0xe1
//高八位
    rtmpPacket->m_body[index++] = (pack->sps_len >> 8) & 0xFF;
//    低八位
    rtmpPacket->m_body[index++] = pack->sps_len & 0xff;
//    拷貝sps的內(nèi)容
    memcpy(&rtmpPacket->m_body[index], pack->sps, pack->sps_len);
    index +=pack->sps_len;
//    pps
    rtmpPacket->m_body[index++] = 0x01; //pps number
//rtmp 協(xié)議
    //pps length
    rtmpPacket->m_body[index++] = (pack->pps_len >> 8) & 0xff;
    rtmpPacket->m_body[index++] = pack->pps_len & 0xff;
//    拷貝pps內(nèi)容
    memcpy(&rtmpPacket->m_body[index], pack->pps, pack->pps_len);
//packaet
//視頻類型
    rtmpPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
//
    rtmpPacket->m_nBodySize = body_size;
//    視頻 04
    rtmpPacket->m_nChannel = 0x04;
    rtmpPacket->m_nTimeStamp = 0;
    rtmpPacket->m_hasAbsTimestamp = 0;
    rtmpPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;
    rtmpPacket->m_nInfoField2 = livePack->rtmp->m_stream_id;
    return rtmpPacket;
}

RTMPPacket *createRtmpPack(jbyte *array, jint length, jlong tms, LivePack *pack) {
    array += 4;
    RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket));
    int body_size = length + 9;
    RTMPPacket_Alloc(packet, body_size);
    if (array[0] == 0x65) {
        packet->m_body[0] = 0x17;
        LOGI("發(fā)送關(guān)鍵幀 data");
    } else{
        packet->m_body[0] = 0x27;
        LOGI("發(fā)送非關(guān)鍵幀 data");
    }
//    固定的大小
    packet->m_body[1] = 0x01;
    packet->m_body[2] = 0x00;
    packet->m_body[3] = 0x00;
    packet->m_body[4] = 0x00;

    //長度
    packet->m_body[5] = (length >> 24) & 0xff;
    packet->m_body[6] = (length >> 16) & 0xff;
    packet->m_body[7] = (length >> 8) & 0xff;
    packet->m_body[8] = (length) & 0xff;

    //數(shù)據(jù)
    memcpy(&packet->m_body[9], array, length);
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nBodySize = body_size;
    packet->m_nChannel = 0x04;
    packet->m_nTimeStamp = tms;
    packet->m_hasAbsTimestamp = 0;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    packet->m_nInfoField2 = pack->rtmp->m_stream_id;


    return packet;
}




void readSpsPps(jbyte *array, jint length, LivePack *pack) {
    for (int i = 0; i < length; i++) {
        if (i+4 < length){
            // 找到 pps 的下標(biāo)
            if (array[i] == 0x00
                && array[i+1] == 0x00
                && array[i+2] == 0x00
                && array[i+3] == 0x01
                && array[i+4] == 0x68
            ){
                // 保存 sps
                livePack->sps_len = i - 4;
                livePack->sps = static_cast<int8_t *>(malloc(livePack->sps_len));
                memcpy(livePack->sps,array + 4,livePack->sps_len);
                // 保存 pps
                livePack->pps_len = length -(livePack->sps_len+4) - 4;
                livePack->pps = static_cast<int8_t *>(malloc(livePack->pps_len));
                memcpy(livePack->pps,array+4+livePack->sps_len+4,livePack->pps_len);
                LOGI("sps:%d pps:%d", livePack->sps_len, livePack->pps_len);
            }
        }
    }

指針的使用

  • malloc 只管分配內(nèi)存屡限,并不能對所得的內(nèi)存進(jìn)行初始化,所以得到的一片新內(nèi)存中炕倘,其值將是隨機的钧大,申請的內(nèi)存是連續(xù)的
  • 返回類型是 void* 類型,void* 表示未確定類型的指針罩旋。C/C++ 規(guī)定啊央,void* 類型可以強制轉(zhuǎn)換為任何其它類型的指針,malloc 函數(shù)返回的是 void * 類型涨醋,C++:p = malloc (sizeof(int)); 則程序無法通過編譯瓜饥,報錯:“不能將 void* 賦值給 int * 類型變量”。所以必須通過 (int *) 來將強制轉(zhuǎn)換

總結(jié)

首先我們通過系統(tǒng)服務(wù)拿到手機屏幕的畫面浴骂,此時取到的原始數(shù)據(jù)還無法進(jìn)行網(wǎng)絡(luò)傳輸乓土,在對其進(jìn)行 h264 編碼后,封裝 Rtmp 包溯警,然后按照 Rtmp 協(xié)議規(guī)定的方式進(jìn)行傳輸

相關(guān)鏈接

直播 I幀設(shè)置的意義

Cmake 基礎(chǔ)中的基礎(chǔ)

C語言 malloc()趣苏、memcpy()、free()

java 并發(fā)之阻塞隊列 LinkedBlockingQueue

談?wù)凩inkedBlockingQueue

C 移植到 Java 中愧膀,byte[] 與其他數(shù)據(jù)類型的轉(zhuǎn)換

在 Android Jni 編程中如何接收和返回 Java 不同類型變量或?qū)ο?/a>

  • 序言:七十年代末拦键,一起剝皮案震驚了整個濱河市失晴,隨后出現(xiàn)的幾起案子季率,更是在濱河造成了極大的恐慌,老刑警劉巖攀唯,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異媚朦,居然都是意外死亡氧敢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門询张,熙熙樓的掌柜王于貴愁眉苦臉地迎上來孙乖,“玉大人,你說我怎么就攤上這事份氧∥ò溃” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵蜗帜,是天一觀的道長恋拷。 經(jīng)常有香客問我,道長厅缺,這世上最難降的妖魔是什么蔬顾? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮湘捎,結(jié)果婚禮上诀豁,老公的妹妹穿的比我還像新娘。我一直安慰自己窥妇,他們只是感情好舷胜,可當(dāng)我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秩伞,像睡著了一般逞带。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纱新,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機與錄音穆趴,去河邊找鬼脸爱。 笑死,一個胖子當(dāng)著我的面吹牛未妹,可吹牛的內(nèi)容都是我干的簿废。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼络它,長吁一口氣:“原來是場噩夢啊……” “哼族檬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起化戳,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤单料,失蹤者是張志新(化名)和其女友劉穎埋凯,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扫尖,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡白对,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了换怖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甩恼。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖沉颂,靈堂內(nèi)的尸體忽然破棺而出条摸,到底是詐尸還是另有隱情,我是刑警寧澤铸屉,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布屈溉,位于F島的核電站,受9級特大地震影響抬探,放射性物質(zhì)發(fā)生泄漏子巾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一小压、第九天 我趴在偏房一處隱蔽的房頂上張望线梗。 院中可真熱鬧,春花似錦怠益、人聲如沸仪搔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烤咧。三九已至,卻和暖如春抢呆,著一層夾襖步出監(jiān)牢的瞬間煮嫌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工抱虐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留昌阿,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓恳邀,卻偏偏與公主長得像懦冰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子谣沸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容