一、 背景介紹及問題概述
? ? ?項目需求需要在rk3568開發板上面,通過rtsp協議拉流的形式獲取攝像頭預覽,然后進行人臉識別 姿態識別等后續其它操作。由于rtsp協議一般使用h.264 h265視頻編碼格式(也叫 AVC 和 HEVC)是不能直接用于后續處理,需要先解碼后獲取解碼后的數據,一般解碼后的數據格式為:YUV420P?或者 ?NV12 格式,YUV420P格式轉碼的可以參考libyuv庫?,這邊測試設備是大華攝像頭解碼后格式為NV12格式,所有是以NV12轉碼NV21(也稱之為:裸碼或原始數據) ,Android 手機相機預覽幀就是NV21格式。
先看NV21 輸出優化后的效果 55 -58秒? 4秒視頻,gif限制5M? 僅參考效果即可
二、 RTSP 拉流基礎與編碼格式簡述
RTSP 協議在 Android 中的應用:
RTSP 是用于控制流媒體播放的協議,Android 中常通過 FFmpeg 或 GStreamer 實現視頻流的接入與播放,適用于實時監控、遠程視頻等場景。
H264 與 H265 編碼格式區別
H265 相比 H264 壓縮效率更高,支持更高分辨率的視頻傳輸,但解碼開銷更大,對性能要求更高,尤其在移動端解碼時容易造成卡頓。
FFmpeg 在拉流中的角色
FFmpeg 負責解析 RTSP 協議、解封裝音視頻流,并將其解碼為原始幀數據,為后續圖像處理與播放提供底層支持。
NV12
YUV420 格式,Y 分量后緊跟交錯排列的 UV 分量,常用于解碼輸出。
NV21
YUV420 格式,Y 分量后緊跟交錯排列的 VU 分量,Android 攝像頭默認輸出格式。
三、RTSP 拉流
? ? ? ? 網上常見Android拉流第三方庫:VLC for Android / LibVLC? ? ?FFmpeg / FFmpegKit / FFmpeg Android Java?FFmpeg ?FFmpeg / FFmpegKit / FFmpeg Android Java? ??ExoPlayer + RTSP 擴展? ?GStreamer for Android
簡單說一下上面幾個庫的情況,第一個庫適用快速播放,第四個庫對Android支持不太友好,所以在第二和第三個之中挑選,由于也是第一次接觸rtsp協議,選擇簡單易于集成和官方庫第三個ExoPlayer + RTSP,嘗試直接通過rtsp協議獲取攝像頭預覽數據,單從播放流暢度來看,效果還是很不錯的,但是由于rtsp協議獲取到是壓縮后的數據格式H264.H.265,并不能滿足項目需求。
下面是一個?AndroidX Media ExoPlayer?demo示例參考地址:AndroidX Media ExoPlayer
四、RTSP? ?H.264、H.265 解碼、轉碼和出現的問題
RTSP拉流之后H.264 還需要java層通過MediaCodec硬解碼獲取NV12,
在轉碼格式NV21 后傳入YuvImage
構造 Bitmap
,再繪制到 Canvas
上這種形式效率很低,單純解碼NV12需要32毫秒左右,在轉碼NV21到Canvas顯示更是需要100毫秒,這種就會在預覽界面上看到明顯的延遲。
然后感覺java層rtsp協議讀取 轉碼確實效率比較低,哪怕是另開線程轉碼也不能解決效率問題。
只能想在C層處理了,那就有前面提到的FFmpeg庫,但是這個開源庫需要自己搭建linux/ubuntu環境編譯出來so和源碼,在集成到Android項目。必要時可以考慮自己編譯,網上有很多教程~
https://github.com/1244975831/RtmpPlayerDemo
直接找了一個比較貼近項目需求的FFmpeg解碼轉碼nv21的項目,拉下來后運行后發現畫面拉伸,卡頓掉幀嚴重,而且FFmpeg版本比較老舊。
下面是已經編譯的 FFmpeg_Android Demo地址:
RtmpPlayerDemo
?
五、解決卡頓掉幀 拉伸問題
優化前FFmpeg拉流源碼
extern "C"
JNIEXPORT void JNICALL
Java_com_example_rtmpplaydemo_RtmpPlayer_nativeStart(JNIEnv *env, jobject) {//開始播放stop = false;if (frameCallback == NULL) {return;}// 讀取數據包int count = 0;while (!stop) {if (av_read_frame(pFormatCtx, pPacket) >= 0) {//解碼int gotPicCount = 0;int decode_video2_size = avcodec_decode_video2(pCodecCtx, pAvFrame, &gotPicCount,pPacket);LOGI("decode_video2_size = %d , gotPicCount = %d", decode_video2_size, gotPicCount);LOGI("pAvFrame->linesize %d %d %d", pAvFrame->linesize[0], pAvFrame->linesize[1],pCodecCtx->height);if (gotPicCount != 0) {count++;sws_scale(pImgConvertCtx,(const uint8_t *const *) pAvFrame->data,pAvFrame->linesize,0,pCodecCtx->height,pFrameNv21->data,pFrameNv21->linesize);//獲取數據大小 寬高等數據int dataSize = pCodecCtx->height * (pAvFrame->linesize[0] + pAvFrame->linesize[1]);LOGI("pAvFrame->linesize %d %d %d %d", pAvFrame->linesize[0],pAvFrame->linesize[1], pCodecCtx->height, dataSize);jbyteArray data = env->NewByteArray(dataSize);env->SetByteArrayRegion(data, 0, dataSize,reinterpret_cast<const jbyte *>(v_out_buffer));// onFrameAvailable 回調jclass clazz = env->GetObjectClass(frameCallback);jmethodID onFrameAvailableId = env->GetMethodID(clazz, "onFrameAvailable", "([B)V");env->CallVoidMethod(frameCallback, onFrameAvailableId, data);env->DeleteLocalRef(clazz);env->DeleteLocalRef(data);}}av_packet_unref(pPacket);}
}
優化后FFmpeg拉流源碼
extern "C"
JNIEXPORT void JNICALL
Java_com_natives_lib_RtmpPlayer_nativeStart(JNIEnv *, jobject) {isPlaying = true;// 啟動解碼和渲染線程pthread_create(&decodeThread, nullptr, decodeFunc, nullptr);pthread_create(&renderThread, nullptr, renderFunc, nullptr);AVRational timeBase = formatCtx->streams[0]->time_base;while (isPlaying && av_read_frame(formatCtx, packet) >= 0) {if (packet->stream_index != 0) {av_packet_unref(packet);continue;}AVPacket *pktCopy = av_packet_alloc();if (!pktCopy) {av_packet_unref(packet);continue;}av_packet_ref(pktCopy, packet);// 使用 packet->pts 或 dts(回退方案),并轉換為微秒int64_t pts = (packet->pts != AV_NOPTS_VALUE) ? packet->pts : packet->dts;if (pts == AV_NOPTS_VALUE) {// 最后兜底:使用系統時間(非推薦)pts = av_gettime();} else {pts = av_rescale_q(pts, timeBase, {1, 1000000}); // 轉換為微秒單位}pktCopy->pts = pts;// 清理隊列中過期幀{std::unique_lock<std::mutex> lock(queueMutex);while (!packetQueue.empty()) {AVPacket *front = packetQueue.front();int64_t frontPts = front->pts;if (av_gettime() - frontPts > MAX_QUEUE_TIME * 1000000) {av_packet_unref(front);packetQueue.pop();} else {break;}}// 判斷是否可入隊(關鍵幀優先)if ((packet->flags & AV_PKT_FLAG_KEY) || packetQueue.size() < MAX_QUEUE_SIZE) {packetQueue.push(pktCopy);queueCond.notify_one();} else {av_packet_unref(pktCopy); // 丟棄非關鍵幀}}av_packet_unref(packet);}// 通知線程退出isPlaying = false;queueCond.notify_all();pthread_join(decodeThread, nullptr);pthread_join(renderThread, nullptr);
}
優化前:單線程處理拉流、解碼、渲染,所有處理都在單線程處理,無時間戳控制,播放幀不判斷時效性,可能導致延遲累積或卡頓所有幀無差別處理,avcodec_decode_video2
較舊 API效率低。
優化后:使用多線程(拉流、解碼、渲染分離)提升并發效率,使用 packet->pts
轉換為微秒進行幀時間控制時間,增加隊列管理邏輯、清理超時幀、控制最大緩存避免內存堆積,關鍵幀優先入隊丟棄非關鍵幀,保障解碼連續性和渲染提高播放流暢度。
注:
這邊還有一個問題,在視頻流分辨率在2688*1520下,在3568開發板下只有每秒9幀(輸入源每秒25幀),在1920*1080分辨率下有每秒18幀,1280*720分辨率則是? 每秒25幀。
市面上主流手機則不存在這種問題,在2688*1520下也可以跑滿25幀。
?
六、cmake編譯問題
? ? ? 一開始是在app里面直接編譯so庫,沒有其它問題。當我把編譯源碼相關文件拉到本地依賴庫時,就會找不到編譯的so庫。感覺很奇妙的問題,花了幾個小時才找到原因,還是遭了熟練度的坑。
流程:新建lib庫,在app里依賴lib,然后直接調用lib內的native
1.剛開始以為是so沒有編譯成功,但是在build里可以找到生成的so。
2 .考慮到是否生成so沒有打入apk問題,所以一直找不到。但是直接依賴lib是直接合并到APK 或 AAB 中 dex和lib庫的,會默認合并so庫才對哇。
3.直接查看apk包內的lib庫
確實有cpp生成的so庫,但是怎么有多出來了x86,arm64-v8a這些庫呢?在lib模塊里面指定了一個armeabi-v7a架構,在app里面也沒有其它編譯so的cmake文件。
只能猜測是在app第三方依賴庫里面,然后直接把依賴庫丟到gpt排查,經過測試果然是
上面這個第三方依賴生成的 libimage_processing_util jni.so庫
問題找到了,那就好解決了
方案一
在app內同步指定 ndk 為??armeabi-v7a 架構
方案二
去掉或替換生成多余的第三方依賴庫
附demo連接:?Demo