Android PC投屏简单尝试(录屏直播)3—软解章(ImageReader+FFMpeg with X264)
使用FFmpeg進(jìn)行軟件解碼并通過RTMP進(jìn)行推流
通過ImageReader的回調(diào),我們就可以得到截屏的數(shù)據(jù)了。第一遍文章是通過自定義的Socket 協(xié)議進(jìn)行傳輸。這里通過FFmpeg,將得到的數(shù)據(jù)進(jìn)行軟件編碼,然后同樣通過RTMP進(jìn)行推流。
配套使用示意圖.png
編譯
去官網(wǎng)下載源碼,并且解壓。按照下面的文件夾路徑進(jìn)行存放。
├── ffmpeg├── x264└── others....編寫編譯腳本。
其實(shí)我們是先編譯出libx264.a 然后與ffmpeg進(jìn)行交叉編譯。編譯出完整的libFFmpeg.so 文件。
腳本放到ffmpeg的目錄下進(jìn)行運(yùn)行就可以了。
這里需要修改的就是你自己的ndk路徑了
編譯結(jié)果
?
image.png
image.png
?
這個就是我們想要的帶有x264的ffmpeg了
?
因?yàn)槲覀冞@里得到的數(shù)據(jù)將是RGBA的數(shù)據(jù),所以我們還需要將其轉(zhuǎn)成YUV420P,進(jìn)行處理。我們需要libyuv,使用這個庫進(jìn)行轉(zhuǎn)換能大大提升我們的效果。而且使用起來非常方便。
所以我們也將其加入編譯
配置項(xiàng)目
將源碼全部復(fù)制到
image.png
同時(shí)我們注意到,這里面就已經(jīng)配置好Cmake文件了。我只需要將其做一下簡單的修改,就可以使用了
?
image.png
?
將我們不需要的so文件和bin文件的安裝給去掉。
接下來配置我們自己的cmake文件
#libyuv include_directories(${CMAKE_SOURCE_DIR}/libs/libyuv/include) # 這樣就可以直接使用內(nèi)部的cmake文件了 add_subdirectory(${CMAKE_SOURCE_DIR}/libs/libyuv) #...部分省略 #同時(shí)將其鏈接到我們自己的庫中,來進(jìn)行使用 target_link_libraries( # Specifies the target library.native-libffmpegyuv# Links the target library to the log library# included in the NDK.${log-lib})進(jìn)行代碼的編寫
RTMP的鏈接
同樣,需要先進(jìn)行RTMP的鏈接。FFMpeg不同的是,因?yàn)樽约壕陀芯幋a器,所以可以直接將頭寫到流里。完成publish
使用FFmpeg的必備套路。
注冊編碼器和網(wǎng)絡(luò)。(因?yàn)檎娴挠杏玫桨?
在FFmpeg中,同樣需要MediaFormat和Encoder。而且ffmpeg 的編程離不開各種上下文對象.所以這里就是先去獲取上下文對象。然后給其配置參數(shù)。進(jìn)行初始化
這個上下文十分重要和常見。他是包含IO的格式上下文。我們先獲取他。
接著。我們需要來找到我們的編碼器
找到編碼器之后,同樣,需要先得到編碼器的上下文對象。這個對象也很重要
pCodecCtx = avcodec_alloc_context3(pCodec); //下面就是對上下文對象的參數(shù)配置//編碼器的ID號,這里為264編碼器,可以根據(jù)video_st里的codecID 參數(shù)賦值pCodecCtx->codec_id = pCodec->id;//像素的格式,也就是說采用什么樣的色彩空間來表明一個像素點(diǎn)pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;//編碼器編碼的數(shù)據(jù)類型pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;//編碼目標(biāo)的視頻幀大小,以像素為單位pCodecCtx->width = width;pCodecCtx->height = height;pCodecCtx->framerate = (AVRational) {fps, 1};//幀率的基本單位,我們用分?jǐn)?shù)來表示,pCodecCtx->time_base = (AVRational) {1, fps};//目標(biāo)的碼率,即采樣的碼率;顯然,采樣碼率越大,視頻大小越大pCodecCtx->bit_rate = 400000;//固定允許的碼率誤差,數(shù)值越大,視頻越小 // pCodecCtx->bit_rate_tolerance = 4000000;pCodecCtx->gop_size = 50;/* Some formats want stream headers to be separate. */if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;這里主要配置的都是一些常見的參數(shù)。包括編碼器的ID,視頻的長寬信息,比特率,幀率,時(shí)基和gop_size
接著配置一些 H.264需要的參數(shù)
//H264 codec param // pCodecCtx->me_range = 16;//pCodecCtx->max_qdiff = 4;pCodecCtx->qcompress = 0.6;//最大和最小量化系數(shù)pCodecCtx->qmin = 10;pCodecCtx->qmax = 51;//Optional Param//兩個非B幀之間允許出現(xiàn)多少個B幀數(shù)//設(shè)置0表示不使用B幀//b 幀越多,圖片越小pCodecCtx->max_b_frames = 0;// Set H264 preset and tuneAVDictionary *param = 0;//H.264if (pCodecCtx->codec_id == AV_CODEC_ID_H264) { // av_dict_set(¶m, "preset", "slow", 0);/*** 這個非常重要,如果不設(shè)置延時(shí)非常的大* ultrafast,superfast, veryfast, faster, fast, medium* slow, slower, veryslow, placebo. 這是x264編碼速度的選項(xiàng)*/av_dict_set(¶m, "preset", "superfast", 0);av_dict_set(¶m, "tune", "zerolatency", 0);}這里有兩個必須要注意的地方。
pCodecCtx->qcompress = 0.6;
//最大和最小量化系數(shù)
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;
這幾個參數(shù)必須配置對。如果不是這樣的話,好像是會出錯的。
編碼速度的選項(xiàng)。這個也很有影響
接著配置完參數(shù),我們就開啟encoder
if (avcodec_open2(pCodecCtx, pCodec, ¶m) < 0) {LOGE("Failed to open encoder!\n");return -1;}因?yàn)槲覀冞@兒只推流視頻,所以,我們還需要創(chuàng)建一個stream.將我們的編碼器信息同樣保存到這個視頻流中
//Add a new stream to output,should be called by the user before avformat_write_header() for muxingvideo_st = avformat_new_stream(ofmt_ctx, pCodec);if (video_st == NULL) {return -1;}video_st->time_base.num = 1;video_st->time_base.den = fps; // video_st->codec = pCodecCtx;video_st->codecpar->codec_tag = 0;avcodec_parameters_from_context(video_st->codecpar, pCodecCtx);最后,就是通過avio_open 打開鏈接,進(jìn)行鏈接。
并且我們知道進(jìn)行推流,必須先將其頭部的編碼器信息寫入,才可以。所以同樣
avformat_write_header 寫入信息,這樣,publish RTMP成功了。
接下來,就是推送實(shí)際的nal了
回顧ImageReader的配置
我們要求輸出的是RGBA格式的Image數(shù)據(jù)。
通過ImageReader的回調(diào),我們可以得到Image數(shù)據(jù)
@Overridepublic void onImageAvailable(ImageReader reader) {Image image = reader.acquireLatestImage();if (image != null) {long timestamp = image.getTimestamp();if (this.timestamp == 0) {this.timestamp = timestamp;if (VERBOSE) {Log.d(TAG, "onImageAvailable timeStamp=" + this.timestamp);}} else {if (VERBOSE) {long delta = timestamp - this.timestamp;Log.d(TAG, "onImageAvailable timeStamp delta in ms=" + delta / 1000000);}}Image.Plane[] planes = image.getPlanes();//因?yàn)槲覀円蟮氖荝GBA格式的數(shù)據(jù),所以全部的存儲在planes[0]中Image.Plane plane = planes[0];//由于Image中的緩沖區(qū)存在數(shù)據(jù)對齊,所以其大小不一定是我們生成ImageReader實(shí)例時(shí)指定的大小,//ImageReader會自動為畫面每一行最右側(cè)添加一個padding,以進(jìn)行對齊,對齊多少字節(jié)可能因硬件而異,//所以我們在取出數(shù)據(jù)時(shí)需要忽略這一部分?jǐn)?shù)據(jù)。int rowStride = plane.getRowStride();int pixelStride = plane.getPixelStride();int rowPadding = rowStride - pixelStride * width;ByteBuffer buffer = plane.getBuffer();//將得到的buffer 和 寬高傳入進(jìn)行處理FFmpegSender.getInstance().rtmpSend(buffer, height, width * 4, rowPadding);image.close();}}發(fā)送的方法。
1.我們這里傳入了未編碼的RGBA數(shù)據(jù),需要先轉(zhuǎn)成YUV420P.
AVFrame來保存未編碼的數(shù)據(jù)。所以我們需要先給其分配內(nèi)存空間和數(shù)據(jù)
然后,我們將我們的數(shù)據(jù)轉(zhuǎn)成yuv,并且將數(shù)據(jù)傳遞給pFrameYUV
//之前我們說過,得到的數(shù)據(jù)是有字節(jié)對齊的問題的。我們在這里,進(jìn)行處理。得到真正的argb數(shù)據(jù)jbyte *srcBuffer = static_cast<jbyte *>(env->GetDirectBufferAddress(buffer));jbyte *dest = new jbyte[yuv_width * yuv_height * 4];int offset = 0;for (int i = 0; i < row;i++) {memcpy(dest + offset, srcBuffer + offset + i * rowPadding, stride);offset += stride;}利用 libyuv 將數(shù)據(jù)轉(zhuǎn)成yuv420p,同時(shí)保存起來
libyuv::ConvertToI420((uint8_t *) dest, yuv_width * yuv_height,pFrameYUV->data[0], yuv_width,pFrameYUV->data[1], yuv_width / 2,pFrameYUV->data[2], yuv_width / 2,0, 0,yuv_width, yuv_height,yuv_width, yuv_height,libyuv::kRotate0, libyuv::FOURCC_ABGR);先配置參數(shù)
AVPacket是存儲編碼之后的數(shù)據(jù)的。我們需要進(jìn)行的操作就是將AVFrame送入編碼器,然后得到AVPacket. 所以我們對其進(jìn)行初始化。并且按照上面所說。來得到包含編碼數(shù)據(jù)的AvPacket
//例如對于H.264來說。1個AVPacket的data通常對應(yīng)一個NAL//初始化AVPacketav_init_packet(&enc_pkt);//開始編碼YUV數(shù)據(jù)ret = avcodec_send_frame(pCodecCtx, pFrameYUV);if (ret != 0) {LOGE("avcodec_send_frame error");return -1;}//獲取編碼后的數(shù)據(jù)ret = avcodec_receive_packet(pCodecCtx, &enc_pkt);//是否編碼前的YUV數(shù)據(jù)av_frame_free(&pFrameYUV);if (ret != 0 || enc_pkt.size <= 0) {LOGE("avcodec_receive_packet error");avError(ret);return -2;}得到編碼后的數(shù)據(jù),再對其進(jìn)行參數(shù)配置,需要注意的pts 和dts的配置,這里的方式不對。這里把他當(dāng)作是恒定的幀率來處理來。但實(shí)際上,因?yàn)橛僧?dāng)前的實(shí)際來決定。
enc_pkt.stream_index = video_st->index;AVRational time_base = ofmt_ctx->streams[0]->time_base;//{ 1, 1000 };enc_pkt.pts = count * (video_st->time_base.den) / ((video_st->time_base.num) * fps);enc_pkt.dts = enc_pkt.pts;enc_pkt.duration = (video_st->time_base.den) / ((video_st->time_base.num) * fps);LOGI("index:%d,pts:%lld,dts:%lld,duration:%lld,time_base:%d,%d",count,(long long) enc_pkt.pts,(long long) enc_pkt.dts,(long long) enc_pkt.duration,time_base.num, time_base.den);enc_pkt.pos = -1;這部分很簡單,只要調(diào)用write方法就可以完成了。
最后是關(guān)閉的方法。
關(guān)閉的時(shí)候,我們需要釋放掉我們創(chuàng)建的IO鏈接/AVFormatContext和Encoder。
總結(jié)
需要注意的兩點(diǎn)
1. FFmpeg的裁剪編譯
直接編譯出來的so文件巨大。在APK文件中6M大小。
-
定位裁剪需求
我們根據(jù)之前的文章,來分析和定位裁剪的腳本。
整個流程中,我們只需要libx264 的編碼器。flv的muxer 和 RTMP協(xié)議。因?yàn)镽TMP協(xié)議是基于TCP的。所以我們也打開tcp協(xié)議。 -
編寫腳本
基于上面的分析,我們修改了FFmpeg的配置
image.png
-
結(jié)果
?
原大小image.png
現(xiàn)在的大小
?
image.png
在APK中的大小
?
image.png
完美~~
?
作者:deep_sadness
鏈接:https://www.jianshu.com/p/6559567a973c
來源:簡書
簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。
總結(jié)
以上是生活随笔為你收集整理的Android PC投屏简单尝试(录屏直播)3—软解章(ImageReader+FFMpeg with X264)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android PC投屏简单尝试(录屏直
- 下一篇: Android PC投屏简单尝试—最终章