Android PC投屏简单尝试(录屏直播)3—软解章(ImageReader+FFMpeg with X264)
使用FFmpeg進行軟件解碼并通過RTMP進行推流
通過ImageReader的回調,我們就可以得到截屏的數據了。第一遍文章是通過自定義的Socket 協議進行傳輸。這里通過FFmpeg,將得到的數據進行軟件編碼,然后同樣通過RTMP進行推流。
配套使用示意圖.png
編譯
去官網下載源碼,并且解壓。按照下面的文件夾路徑進行存放。
├── ffmpeg├── x264└── others....編寫編譯腳本。
其實我們是先編譯出libx264.a 然后與ffmpeg進行交叉編譯。編譯出完整的libFFmpeg.so 文件。
腳本放到ffmpeg的目錄下進行運行就可以了。
這里需要修改的就是你自己的ndk路徑了
編譯結果
?
image.png
image.png
?
這個就是我們想要的帶有x264的ffmpeg了
?
因為我們這里得到的數據將是RGBA的數據,所以我們還需要將其轉成YUV420P,進行處理。我們需要libyuv,使用這個庫進行轉換能大大提升我們的效果。而且使用起來非常方便。
所以我們也將其加入編譯
配置項目
將源碼全部復制到
image.png
同時我們注意到,這里面就已經配置好Cmake文件了。我只需要將其做一下簡單的修改,就可以使用了
?
image.png
?
將我們不需要的so文件和bin文件的安裝給去掉。
接下來配置我們自己的cmake文件
#libyuv include_directories(${CMAKE_SOURCE_DIR}/libs/libyuv/include) # 這樣就可以直接使用內部的cmake文件了 add_subdirectory(${CMAKE_SOURCE_DIR}/libs/libyuv) #...部分省略 #同時將其鏈接到我們自己的庫中,來進行使用 target_link_libraries( # Specifies the target library.native-libffmpegyuv# Links the target library to the log library# included in the NDK.${log-lib})進行代碼的編寫
RTMP的鏈接
同樣,需要先進行RTMP的鏈接。FFMpeg不同的是,因為自己就有編碼器,所以可以直接將頭寫到流里。完成publish
使用FFmpeg的必備套路。
注冊編碼器和網絡。(因為真的有用到啊)
在FFmpeg中,同樣需要MediaFormat和Encoder。而且ffmpeg 的編程離不開各種上下文對象.所以這里就是先去獲取上下文對象。然后給其配置參數。進行初始化
這個上下文十分重要和常見。他是包含IO的格式上下文。我們先獲取他。
接著。我們需要來找到我們的編碼器
找到編碼器之后,同樣,需要先得到編碼器的上下文對象。這個對象也很重要
pCodecCtx = avcodec_alloc_context3(pCodec); //下面就是對上下文對象的參數配置//編碼器的ID號,這里為264編碼器,可以根據video_st里的codecID 參數賦值pCodecCtx->codec_id = pCodec->id;//像素的格式,也就是說采用什么樣的色彩空間來表明一個像素點pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;//編碼器編碼的數據類型pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;//編碼目標的視頻幀大小,以像素為單位pCodecCtx->width = width;pCodecCtx->height = height;pCodecCtx->framerate = (AVRational) {fps, 1};//幀率的基本單位,我們用分數來表示,pCodecCtx->time_base = (AVRational) {1, fps};//目標的碼率,即采樣的碼率;顯然,采樣碼率越大,視頻大小越大pCodecCtx->bit_rate = 400000;//固定允許的碼率誤差,數值越大,視頻越小 // 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;這里主要配置的都是一些常見的參數。包括編碼器的ID,視頻的長寬信息,比特率,幀率,時基和gop_size
接著配置一些 H.264需要的參數
//H264 codec param // pCodecCtx->me_range = 16;//pCodecCtx->max_qdiff = 4;pCodecCtx->qcompress = 0.6;//最大和最小量化系數pCodecCtx->qmin = 10;pCodecCtx->qmax = 51;//Optional Param//兩個非B幀之間允許出現多少個B幀數//設置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);/*** 這個非常重要,如果不設置延時非常的大* ultrafast,superfast, veryfast, faster, fast, medium* slow, slower, veryslow, placebo. 這是x264編碼速度的選項*/av_dict_set(¶m, "preset", "superfast", 0);av_dict_set(¶m, "tune", "zerolatency", 0);}這里有兩個必須要注意的地方。
pCodecCtx->qcompress = 0.6;
//最大和最小量化系數
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;
這幾個參數必須配置對。如果不是這樣的話,好像是會出錯的。
編碼速度的選項。這個也很有影響
接著配置完參數,我們就開啟encoder
if (avcodec_open2(pCodecCtx, pCodec, ¶m) < 0) {LOGE("Failed to open encoder!\n");return -1;}因為我們這兒只推流視頻,所以,我們還需要創建一個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 打開鏈接,進行鏈接。
并且我們知道進行推流,必須先將其頭部的編碼器信息寫入,才可以。所以同樣
avformat_write_header 寫入信息,這樣,publish RTMP成功了。
接下來,就是推送實際的nal了
回顧ImageReader的配置
我們要求輸出的是RGBA格式的Image數據。
通過ImageReader的回調,我們可以得到Image數據
@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();//因為我們要求的是RGBA格式的數據,所以全部的存儲在planes[0]中Image.Plane plane = planes[0];//由于Image中的緩沖區存在數據對齊,所以其大小不一定是我們生成ImageReader實例時指定的大小,//ImageReader會自動為畫面每一行最右側添加一個padding,以進行對齊,對齊多少字節可能因硬件而異,//所以我們在取出數據時需要忽略這一部分數據。int rowStride = plane.getRowStride();int pixelStride = plane.getPixelStride();int rowPadding = rowStride - pixelStride * width;ByteBuffer buffer = plane.getBuffer();//將得到的buffer 和 寬高傳入進行處理FFmpegSender.getInstance().rtmpSend(buffer, height, width * 4, rowPadding);image.close();}}發送的方法。
1.我們這里傳入了未編碼的RGBA數據,需要先轉成YUV420P.
AVFrame來保存未編碼的數據。所以我們需要先給其分配內存空間和數據
然后,我們將我們的數據轉成yuv,并且將數據傳遞給pFrameYUV
//之前我們說過,得到的數據是有字節對齊的問題的。我們在這里,進行處理。得到真正的argb數據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 將數據轉成yuv420p,同時保存起來
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);先配置參數
AVPacket是存儲編碼之后的數據的。我們需要進行的操作就是將AVFrame送入編碼器,然后得到AVPacket. 所以我們對其進行初始化。并且按照上面所說。來得到包含編碼數據的AvPacket
//例如對于H.264來說。1個AVPacket的data通常對應一個NAL//初始化AVPacketav_init_packet(&enc_pkt);//開始編碼YUV數據ret = avcodec_send_frame(pCodecCtx, pFrameYUV);if (ret != 0) {LOGE("avcodec_send_frame error");return -1;}//獲取編碼后的數據ret = avcodec_receive_packet(pCodecCtx, &enc_pkt);//是否編碼前的YUV數據av_frame_free(&pFrameYUV);if (ret != 0 || enc_pkt.size <= 0) {LOGE("avcodec_receive_packet error");avError(ret);return -2;}得到編碼后的數據,再對其進行參數配置,需要注意的pts 和dts的配置,這里的方式不對。這里把他當作是恒定的幀率來處理來。但實際上,因為由當前的實際來決定。
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;這部分很簡單,只要調用write方法就可以完成了。
最后是關閉的方法。
關閉的時候,我們需要釋放掉我們創建的IO鏈接/AVFormatContext和Encoder。
總結
需要注意的兩點
1. FFmpeg的裁剪編譯
直接編譯出來的so文件巨大。在APK文件中6M大小。
-
定位裁剪需求
我們根據之前的文章,來分析和定位裁剪的腳本。
整個流程中,我們只需要libx264 的編碼器。flv的muxer 和 RTMP協議。因為RTMP協議是基于TCP的。所以我們也打開tcp協議。 -
編寫腳本
基于上面的分析,我們修改了FFmpeg的配置
image.png
-
結果
?
原大小image.png
現在的大小
?
image.png
在APK中的大小
?
image.png
完美~~
?
作者:deep_sadness
鏈接:https://www.jianshu.com/p/6559567a973c
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。
總結
以上是生活随笔為你收集整理的Android PC投屏简单尝试(录屏直播)3—软解章(ImageReader+FFMpeg with X264)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android PC投屏简单尝试(录屏直
- 下一篇: Android PC投屏简单尝试—最终章