拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Android FFmpeg拉RTSP流获取H.264原始NALU数据实战

简介本资源是一套面向Android音视频开发者的FFmpeg实战工程聚焦于在移动端拉取RTSP流并提取原始H.264 NALU数据的核心场景适用于实时监控、视频分析、自定义解码等中高级开发需求。压缩包共358个文件涵盖72个XML配置与布局文件、135个C/C头文件h及6个cpp源码支撑JNI层FFmpeg调用与MediaCodec硬解对接含7个so动态库、14个bin可执行或中间产物以及gradlew等构建脚本完整复现从NDK编译集成、命令行流拉取、NALU起始码解析0x000001/0x00000001、SPS/PPS提取到Surface渲染的全链路逻辑。资源包大小为7.13MB结构清晰模块分层明确含CMake构建体系与Gradle工程配置便于快速编译调试。目前已有1115人学习下载提供可直接运行的参考实现、关键注释详尽的代码片段及典型错误处理思路是深入理解Android端音视频流处理底层机制的实用范例。1. 项目概述为什么要在Android上直接获取H.264 NALU原始数据在视频开发一线干了十多年从早期用VLC硬解RTSP流到后来自己搭JNI层调FFmpeg再到如今做低延迟安防监控SDK我见过太多人卡在“能播”和“能用”之间。很多人以为只要视频画面出来了就万事大吉但真正在做AI视觉分析、边缘帧级处理、自定义码流封装或硬件加速转码的团队很快就会撞上一堵墙Android原生MediaPlayer或ExoPlayer只给你解码后的YUV/RGB帧而你真正需要的是未经解码、未被重组、保持原始NALU边界与类型标记的H.264压缩数据流。这正是标题里“Android调用FFmpeg拉RTSP流获得H.264原始压缩数据NALU数据”的核心价值——它不是为了播放而是为了掌控。我去年帮一家做智能工地安全帽识别的客户重构视频接入模块他们原来用ExoPlayerSurfaceView渲染再用OpenCV从Surface抓YUV帧做推理。结果发现漏检率高、延迟超800ms、夜间低照度下误报频发。根本原因在于Surface输出的YUV帧已经过GPU缩放、色彩空间转换、甚至部分厂商还偷偷做了动态降噪原始运动细节全丢了。而他们真正需要的是RTSP流中每一个I帧/P帧的起始位置、SPS/PPS参数、时间戳精度到毫秒级的NALU包这样才能精准对齐AI模型的输入时序做帧间差分、ROI裁剪、关键帧强制提取。这类需求在工业质检、无人机图传、车载ADAS、医疗内窥镜实时分析等场景里早已不是“可选项”而是“必选项”。关键词“Android, FFmpeg, RTSP, H.264, NALU”背后实际指向的是一个典型的嵌入式音视频底层链路网络协议层RTSP over TCP/UDP→ 解复用层Demuxer→ 码流解析层H.264 Annex B格式识别→ 原始数据交付层逐包回调。整个过程绕开了Android Framework层的MediaCodec解码器也避开了SurfaceFlinger的合成路径把控制权完全交还给开发者。这不是炫技而是工程现实——当你的业务逻辑必须依赖SPS中的profile_level_id判断编码能力或需要根据NALU type0x05为IDR0x01为P帧做关键帧打标或要将NALU流直接喂给自研的国产化硬件解码芯片时这条路就是唯一选择。当然这条路不好走。Android上跑FFmpeg不是简单copy-paste几个so文件就行。你要面对ABI兼容性armeabi-v7a/arm64-v8a/x86_64、Java层线程安全AVPacket释放时机、内存管理libavutil的av_malloc vs Java Heap、RTSP鉴权Digest认证的nonce同步、TCP粘包/UDP丢包重传、以及最关键的——如何从AVPacket中准确剥离出独立的NALU单元而不是一堆拼接错误的字节流。很多开发者卡在最后一步拿到的“H.264数据”要么全是0x00 00 00 01开头的乱码要么I帧和P帧混在一起无法分离甚至SPS/PPS参数缺失导致后续解码失败。这背后不是FFmpeg配置问题而是对H.264 Annex B格式、NALU边界检测、AVPacket数据结构的理解偏差。接下来我会把这整条链路拆开揉碎从设计思路、核心细节、实操步骤到踩坑记录全部摊开讲透。2. 整体架构设计与方案选型逻辑2.1 为什么放弃MediaCodecRTSP坚持用FFmpeg原生拉流有人会问Android不是有MediaCodec吗配合RTSP URL直接setDataSource不香吗答案是香但不适用。MediaCodec本质是解码器接口它要求输入是“已解复用”的ES流Elementary Stream而RTSP协议本身是信令协议真正的媒体数据藏在RTP包里。Android系统MediaExtractor根本不支持RTSP协议栈你传个rtsp://192.168.1.100:554/stream1进去十有八九抛UnsupportedOperationException。市面上所谓“MediaCodec支持RTSP”实际都是厂商在底层偷偷集成了GStreamer或FFmpeg做RTP解析再把裸H.264 ES喂给MediaCodec——这等于绕了一圈又回到起点且你完全无法干预中间环节。更致命的是MediaCodec的输入缓冲区InputBuffer只接受完整NALU以0x00 00 00 01或0x00 00 01开头但它不保证每个InputBuffer只塞一个NALU。尤其在高码率场景下一个InputBuffer可能塞进多个短NALU或者一个长NALU被拆成多个InputBuffer。而你的业务如果需要逐帧分析比如检测IDR帧是否丢失这种不确定性就是灾难。FFmpeg则完全不同它的AVPacket结构天然对应RTP包或TS分片每个AVPacket携带一个完整的NALU或多个连续NALU取决于封装格式且通过pkt-size和pkt-data可精确控制字节边界。这才是“原始压缩数据”的根基。2.2 FFmpeg版本与编译策略为什么必须用4.4且禁用默认解码器我实测过FFmpeg 3.4、4.2、4.4、5.1四个版本在Android上的表现。结论很明确必须用4.4或更高版本且编译时严格禁用所有硬件解码器--disable-vaapi --disable-vdpau --disable-cuda --disable-cuvid --disable-nvdec。原因有三第一RTSP over TCP支持。FFmpeg 4.4之前RTSP默认走UDP遇到防火墙/NAT就断流。4.4引入了-rtsp_transport tcp参数且底层改用AVIOContext重写TCP传输层稳定性提升3倍以上。我们做过压力测试4.4版本连续拉流72小时无断连而3.4版本平均2.3小时就因UDP丢包触发重连失败。第二NALU边界识别可靠性。4.4重构了h264_parser对Annex B格式的start code0x00 00 00 01 vs 0x00 00 01检测更鲁棒。老版本在海康/大华设备的私有RTSP流中常把SPS前的0x00 00 00 01误判为帧分隔符导致SPS被截断。第三内存管理安全。4.4的av_packet_unref()彻底解决引用计数bug避免JNI层多次释放同一AVPacket导致的SIGSEGV。这点在多线程回调场景下尤为关键——你绝不想让主线程和解码线程同时操作一个pkt。编译时禁用硬件解码器不是性能妥协而是控制权让渡。一旦启用cuvid或mediacodecFFmpeg会在avcodec_send_packet()内部自动调用硬件解码你拿到的AVPacket就不再是原始NALU而是解码后的YUV数据。我们的目标是“原始压缩数据”所以必须确保整个pipeline停留在解复用demux阶段绝不进入解码decode阶段。编译命令核心参数如下./configure \ --enable-cross-compile \ --cross-prefix$TOOLCHAIN/bin/aarch64-linux-android- \ --target-osandroid \ --archaarch64 \ --cpuarmv8-a \ --sysroot$SYSROOT \ --extra-cflags-Os -fpic -D__ANDROID__ \ --extra-ldflags-L$SYSROOT/usr/lib -lc -lm \ --disable-encoder*\ --disable-decoder*\ --disable-hwaccels \ --disable-hwaccel-drivers \ --disable-parsers \ --disable-bsfs \ --disable-filters \ --disable-programs \ --disable-doc \ --disable-debug \ --enable-demuxerrtsp \ --enable-decoderh264 \ --enable-parserh264 \ --enable-protocoltcp \ --enable-protocolrtsp \ --enable-libzmq \ --prefix$PREFIX注意--disable-decoder*禁用所有解码器但保留--enable-decoderh264看似矛盾其实这是FFmpeg的机制——h264 decoder在此处仅用于解析SPS/PPS参数不执行解码而--disable-parsers被我们主动关闭因为h264_parser是NALU边界识别的核心。2.3 JNI层架构为何采用“单线程事件循环异步回调”而非多线程阻塞Android上FFmpeg拉流最常见错误就是把av_read_frame()放在子线程里死循环调用然后用Handler往主线程发消息。这会导致两个严重问题一是Java层Handler消息队列积压二是C层av_packet_unref()时机错乱。我们的方案是C层创建独立线程运行FFmpeg事件循环Java层通过JNI注册回调函数C层检测到关键NALU如IDR帧时直接调用Java回调传递ByteBuffer地址和长度。这样设计的理由很实在零拷贝ByteBuffer.allocateDirect()分配的堆外内存其address可通过GetDirectBufferAddress()直接获取C层无需memcpy直接填充数据。实测1080p30fps下CPU占用从32%降至11%。时序精准RTSP流的时间戳pkt-pts在C层即可读取回调时一并传入避免Java层System.nanoTime()带来的毫秒级误差。内存安全Java层ByteBuffer的capacity()和limit()在回调前已固定C层只写不读杜绝并发修改风险。具体流程Java端调用startRtspStream(url)→ JNI创建pthread → pthread执行avformat_open_input()→av_read_frame()循环 → 检测pkt-stream_index匹配视频流 → 调用env-CallVoidMethod(callback, methodID, byteBuffer, pkt-size, pkt-pts)→ Java层收到后立即处理不阻塞C层。3. 核心细节解析从RTSP握手到NALU精准提取3.1 RTSP连接建立如何处理Digest认证与TCP长连接保活海康、大华等主流IPC设备普遍采用RTSP Digest认证其流程比Basic认证复杂得多OPTIONS → DESCRIBE → SETUP → PLAY。很多开发者卡在DESCRIBE返回401后不知道如何解析WWW-Authenticate头里的realm、nonce、stale参数。FFmpeg内部已实现完整Digest流程但你需要正确配置URL参数String rtspUrl rtsp://admin:123456192.168.1.100:554/Streaming/Channels/101?tcp; // 关键添加?tcp强制走TCP避免UDP丢包 // 用户名密码必须URL编码否则含特殊字符会失败C层调用时需设置AVDictionary参数AVDictionary *opts NULL; av_dict_set(opts, rtsp_transport, tcp, 0); av_dict_set(opts, stimeout, 5000000, 0); // 5秒超时 av_dict_set(opts, max_delay, 500000, 0); // 500ms最大延迟 av_dict_set(opts, buffer_size, 1048576, 0); // 1MB缓冲区 int ret avformat_open_input(fmt_ctx, url, NULL, opts);其中stimeout单位是微秒设太小如100000会导致网络抖动时频繁重连设太大如30000000则故障恢复慢。我们实测5秒最平衡。TCP保活靠SO_KEEPALIVE但FFmpeg默认不开启。需在avformat_open_input后手动获取socket fd并设置// 获取底层socket fdFFmpeg 4.4 int fd ffurl_get_file_handle(fmt_ctx-pb-opaque); if (fd 0) { int keepalive 1; setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, keepalive, sizeof(keepalive)); int idle 60; // 60秒无数据后发送探测包 setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, idle, sizeof(idle)); int interval 10; // 每10秒探测一次 setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, interval, sizeof(interval)); int count 3; // 连续3次失败才断连 setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, count, sizeof(count)); }这套组合拳让RTSP连接在NAT环境下稳定维持8小时以上远超默认2小时。3.2 AVPacket到NALU的精准映射为什么不能直接用pkt-data这是90%开发者栽跟头的地方。AVPacket的pkt-data指向的是RTP负载Payload数据而RTP包头占12字节H.264的NALU前还有FU-A分片头Fragmentation Unit A。直接把pkt-data当NALU用必然失败。标准RTP over H.264的NALU封装有三种模式Single NALU Mode一个RTP包一个NALUpkt-data 12 即为NALU起始跳过RTP头FU-A Mode一个NALU被拆成多个RTP包需根据FU indicator和FU header重组STAP-A Mode一个RTP包含多个NALU需解析每个NALU的长度字段FFmpeg的h264_rtp_decoder在libavcodec/rtpdec_h264.c已实现完整解析但它只在解码模式下生效。我们要的是原始数据所以必须手动解析。核心逻辑如下// 判断是否FU-A分片 uint8_t *rtp_data pkt-data; if (rtp_data[12] 0x1C) { // FU Indicator 0x1C 表示H.264 uint8_t fu_header rtp_data[13]; uint8_t nal_type fu_header 0x1F; uint8_t start_bit (fu_header 7) 0x01; uint8_t end_bit (fu_header 6) 0x01; if (start_bit end_bit) { // 完整NALU替换FU indicator为原始NALU type uint8_t *nal_start rtp_data 12; nal_start[0] (nal_start[0] 0xE0) | nal_type; // 此时nal_start即为完整NALU长度pkt-size-12 } else if (start_bit) { // 分片起始需缓存 memcpy(fu_buffer, rtp_data 14, pkt-size - 14); fu_buffer_size pkt-size - 14; } else if (end_bit) { // 分片结束拼接 memcpy(fu_buffer fu_buffer_size, rtp_data 14, pkt-size - 14); fu_buffer_size pkt-size - 14; // fu_buffer即为完整NALU } }这个逻辑必须写在av_read_frame()之后、回调之前。我们实测发现海康设备默认用FU-A大华部分型号用Single NALU宇视用STAP-A——没有统一标准必须全兼容。3.3 NALU类型识别与关键帧提取SPS/PPS如何单独捕获H.264 NALU type定义在ITU-T H.264 Annex B关键类型有0x07SPSSequence Parameter Set0x08PPSPicture Parameter Set0x05IDR帧Instantaneous Decoding Refresh0x01非IDR帧P/B帧但直接比较pkt-data[0]是错的因为RTP负载中NALU type被编码在FU indicator或STAP-A头里。正确做法是先按3.2节解析出原始NALU字节流再取第一个字节跳过start code 0x00 00 00 01后的第一个字节。Start code识别也有坑Annex B标准允许0x00 00 01或0x00 00 00 01两种。FFmpeg的avpriv_find_start_code()函数可通用识别但需注意它返回的是start code结束位置而非NALU起始。我们封装的工具函数int find_nalu_start(uint8_t *buf, int size, int *start_pos) { uint32_t state 0; for (int i 0; i size; i) { state (state 8) | buf[i]; if (state 0x000001 || state 0x00000001) { *start_pos i - (state 0x000001 ? 2 : 3) 1; return 1; } } return 0; }SPS/PPS必须在首个IDR帧前送达解码器否则无法解码。因此我们的回调策略是收到SPS/PPS时立即回调并标记is_sps_ppstrue收到IDR帧时检查是否已有SPS/PPS若无则丢弃该IDR避免解码器崩溃所有NALU回调时附带nal_type和is_keyframe标志Java层可据此做路由提示SPS中profile_idc字段决定编码档次Baseline/Main/Highlevel_idc决定解码能力。很多国产芯片只支持Baseline Profile若IPC推送High Profile SPS必须在Java层拦截并告警。4. 实操全流程从Android Studio配置到JNI代码落地4.1 Android Studio环境搭建NDK版本与CMakeLists.txt关键配置Android Studio Giraffe2022.3.1及以上版本推荐用NDK 23.1.7779620它对aarch64支持最稳。创建新项目时勾选“Include C support”在app/src/main/cpp目录下新建文件。CMakeLists.txt核心配置cmake_minimum_required(VERSION 3.22.1) project(rtsp-nalu) # 设置FFmpeg库路径 set(FFMPEG_DIR ${CMAKE_SOURCE_DIR}/../libs) # 添加FFmpeg静态库 add_library(ffmpeg_avcodec STATIC IMPORTED) set_target_properties(ffmpeg_avcodec PROPERTIES IMPORTED_LOCATION ${FFMPEG_DIR}/arm64-v8a/libavcodec.a) add_library(ffmpeg_avformat STATIC IMPORTED) set_target_properties(ffmpeg_avformat PROPERTIES IMPORTED_LOCATION ${FFMPEG_DIR}/arm64-v8a/libavformat.a) add_library(ffmpeg_avutil STATIC IMPORTED) set_target_properties(ffmpeg_avutil PROPERTIES IMPORTED_LOCATION ${FFMPEG_DIR}/arm64-v8a/libavutil.a) add_library(ffmpeg_swresample STATIC IMPORTED) set_target_properties(ffmpeg_swresample PROPERTIES IMPORTED_LOCATION ${FFMPEG_DIR}/arm64-v8a/libswresample.a) # 添加头文件路径 include_directories(${CMAKE_SOURCE_DIR}/../ffmpeg/include) # 创建主库 add_library(nalu-jni SHARED native-lib.cpp) # 链接FFmpeg库 target_link_libraries(nalu-jni ffmpeg_avformat ffmpeg_avcodec ffmpeg_avutil ffmpeg_swresample log android z )注意libswresample虽不用于H.264但FFmpeg内部依赖它缺少会导致链接失败。z库是zlib用于解压SDP中的base64编码参数。4.2 Java层核心API设计如何安全传递ByteBuffer与元数据Java层暴露三个核心方法public class RtspNaluReceiver { static { System.loadLibrary(nalu-jni); } // 启动拉流url为rtsp://格式callback为接收NALU的接口 public native void startRtspStream(String url, NaluCallback callback); // 停止拉流 public native void stopRtspStream(); // 获取当前连接状态 public native int getConnectionStatus(); // 0disconnected, 1connecting, 2connected public interface NaluCallback { // buffer: Direct ByteBuffer, size: NALU长度, pts: 时间戳微秒, nalType: NALU类型, isKeyFrame: 是否关键帧 void onNaluReceived(ByteBuffer buffer, int size, long pts, int nalType, boolean isKeyFrame); } }ByteBuffer必须用allocateDirect()创建且容量需预估// 预估最大NALU长度1080p I帧约200KBP帧约50KB预留256KB ByteBuffer naluBuffer ByteBuffer.allocateDirect(256 * 1024); naluBuffer.order(ByteOrder.nativeOrder()); rtspReceiver.startRtspStream(rtspUrl, new RtspNaluReceiver.NaluCallback() { Override public void onNaluReceived(ByteBuffer buffer, int size, long pts, int nalType, boolean isKeyFrame) { // 注意buffer.position()和limit()未改变需手动设置 buffer.limit(size); buffer.position(0); // 此时buffer.array()不可用Direct Buffer必须用get()或nio操作 processNalu(buffer, size, pts, nalType, isKeyFrame); } });注意Direct ByteBuffer的array()方法会抛ReadOnlyBufferException必须用buffer.get(byteArray, 0, size)或buffer.asIntBuffer()等NIO方式读取。4.3 JNI核心代码实现av_read_frame()循环与NALU回调逻辑native-lib.cpp主体逻辑#include jni.h #include string #include android/log.h #include libavformat/avformat.h #include libavcodec/avcodec.h #include libavutil/avutil.h #include libswresample/swresample.h #define LOG_TAG RTSP-NALU #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) static JavaVM *jvm nullptr; static jobject java_callback nullptr; static jmethodID callback_method_id nullptr; extern C { JNIEXPORT jint JNICALL Java_com_example_rtspnalu_RtspNaluReceiver_startRtspStream(JNIEnv *env, jobject thiz, jstring url, jobject callback) { // 保存Java回调对象 env-GetJavaVM(jvm); java_callback env-NewGlobalRef(callback); jclass callback_class env-GetObjectClass(callback); callback_method_id env-GetMethodID(callback_class, onNaluReceived, (Ljava/nio/ByteBuffer;JIZ)V); const char *c_url env-GetStringUTFChars(url, nullptr); AVFormatContext *fmt_ctx nullptr; int video_stream_index -1; // 1. 打开RTSP流 AVDictionary *opts nullptr; av_dict_set(opts, rtsp_transport, tcp, 0); av_dict_set(opts, stimeout, 5000000, 0); av_dict_set(opts, max_delay, 500000, 0); int ret avformat_open_input(fmt_ctx, c_url, nullptr, opts); if (ret 0) { LOGE(avformat_open_input failed: %s, av_err2str(ret)); return -1; } // 2. 查找视频流 ret avformat_find_stream_info(fmt_ctx, nullptr); for (int i 0; i fmt_ctx-nb_streams; i) { if (fmt_ctx-streams[i]-codecpar-codec_type AVMEDIA_TYPE_VIDEO fmt_ctx-streams[i]-codecpar-codec_id AV_CODEC_ID_H264) { video_stream_index i; break; } } if (video_stream_index -1) { LOGE(No H.264 video stream found); avformat_close_input(fmt_ctx); return -2; } // 3. 启动拉流线程 pthread_t thread; pthread_create(thread, nullptr, [](void *arg) - void * { AVPacket pkt; av_init_packet(pkt); AVFormatContext *ctx static_castAVFormatContext *(arg); while (true) { int ret av_read_frame(ctx, pkt); if (ret 0) { if (ret AVERROR_EOF) { LOGI(RTSP stream ended); break; } LOGE(av_read_frame error: %s, av_err2str(ret)); usleep(10000); // 10ms重试 continue; } // 只处理视频流 if (pkt.stream_index video_stream_index) { // 4. 解析NALU调用3.2节的解析函数 uint8_t *nal_data nullptr; int nal_size 0; int nal_type 0; bool is_keyframe false; parse_rtp_h264_nalu(pkt, nal_data, nal_size, nal_type, is_keyframe); if (nal_data nal_size 0) { // 5. 回调Java层 JNIEnv *env; jvm-AttachCurrentThread(env, nullptr); jobject direct_buffer env-NewDirectByteBuffer(nal_data, nal_size); env-CallVoidMethod(java_callback, callback_method_id, direct_buffer, (jlong) pkt.pts, (jint) nal_type, (jboolean) is_keyframe); env-DeleteLocalRef(direct_buffer); jvm-DetachCurrentThread(); } } av_packet_unref(pkt); } return nullptr; }, fmt_ctx); env-ReleaseStringUTFChars(url, c_url); return 0; } JNIEXPORT void JNICALL Java_com_example_rtspnalu_RtspNaluReceiver_stopRtspStream(JNIEnv *env, jobject thiz) { // 实现停止逻辑需加锁保护 } }关键点av_init_packet(pkt)必须在循环外调用一次否则av_read_frame()会覆盖pkt结构av_packet_unref(pkt)必须在每次处理后调用否则内存泄漏NewDirectByteBuffer创建的buffer生命周期由Java GC管理C层无需free。4.4 实测性能与资源占用不同分辨率下的实测数据我们在Pixel 6Snapdragon 765G、小米12骁龙8 Gen1、华为Mate 50麒麟9000S三台设备上实测10分钟拉流设备分辨率/帧率CPU占用内存占用平均延迟丢包率Pixel 6720p15fps18.3%42MB320ms0.02%小米121080p25fps24.7%68MB280ms0.01%华为Mate 501080p30fps31.5%85MB250ms0.00%延迟测量方法在IPC端用PTZ控制云台转动手机端用高速摄像机拍摄屏幕对比转动起始时刻与画面出现时刻。数据证明纯FFmpeg拉流比ExoPlayer低200ms以上。内存占用主要来自FFmpeg的AVFormatContext约15MB和RTP缓冲区可配置。我们通过av_dict_set(opts, buffer_size, 524288, 0)将缓冲区从默认256KB降至512KB在低端机上内存节省30%。5. 常见问题排查与独家避坑指南5.1 典型问题速查表问题现象根本原因解决方案验证方法avformat_open_input返回-5EIORTSP URL未URL编码含特殊字符如、/对用户名密码调用URLEncoder.encode()抓包看OPTIONS请求是否400 Bad Request收到的NALU全是0x00长度为0RTP负载解析错误未跳过RTP头12字节检查pkt-data 12是否越界添加if (pkt-size 12)判断打印pkt-size和pkt-data[0]~pkt-data[15]SPS/PPS未回调只有P帧IPC设备未在DESCRIBE响应中返回SDP或FFmpeg未解析SDP强制在URL后加?tcp或手动构造SDP字符串传入用Wireshark抓RTP包看第一个包是否含SPSJava层收到buffer为空Direct ByteBuffer未设置position/limit在回调中添加buffer.position(0).limit(size)Log打印buffer.remaining()拉流几分钟后自动断连TCP Keepalive未启用NAT超时按3.1节设置SO_KEEPALIVE参数用netstat -an | grep :554看socket状态多个NALU粘连如0x000001后紧跟0x000001Annex B start code识别逻辑错误改用avpriv_find_start_code()替代手写循环用hexdump查看原始pkt-data5.2 三个血泪教训那些文档里不会写的细节教训一AVPacket.data的生命周期比你想的短很多开发者把pkt-data存起来异步处理结果收到的是野指针。FFmpeg的AVPacket.data指向内部缓冲区av_packet_unref()后立即失效。正确做法是在av_read_frame()和av_packet_unref()之间立即将NALU数据memcpy到Java层ByteBuffer。我们曾因忽略这点在华为某机型上出现随机崩溃堆栈显示SIGSEGV at 0xdeadbeef。教训二海康设备的“私有RTP时间戳”陷阱海康IPC的RTP时间戳不是标准的90kHz而是设备内部时钟如1000Hz。直接用pkt-pts计算播放时间会快进或卡顿。解决方案忽略pkt-pts改用av_gettime_relative()获取拉流开始后的相对时间并在Java层做平滑滤波滑动窗口取中位数。实测后时间戳抖动从±200ms降至±15ms。教训三Android 12的Scoped Storage权限变更从Android 12开始/sdcard/路径访问受限。如果你在FFmpeg中尝试写日志文件如av_log_set_callback必须改用context.getExternalFilesDir(null)获取应用专属目录。否则av_log_default_callback会因权限拒绝而静默失败导致调试信息全丢。5.3 硬件加速的另类思路如何在不启用FFmpeg硬件解码的前提下利用MediaCodec前面强调禁用FFmpeg硬件解码但不等于放弃硬件加速。我们的方案是FFmpeg只做解复用和NALU提取把原始NALU数据交给MediaCodec异步处理。这样既保留NALU原始性又享受硬件解码性能。Java层伪代码MediaCodec codec MediaCodec.createDecoderByType(video/avc); MediaFormat format MediaFormat.createVideoFormat(video/avc, width, height); format.setByteBuffer(csd-0, spsByteBuffer); // SPS format.setByteBuffer(csd-1, ppsByteBuffer); // PPS codec.configure(format, surface, null, 0); codec.start(); // 收到NALU后 ByteBuffer[] inputBuffers codec.getInputBuffers(); int inputBufferIndex codec.dequeueInputBuffer(10000); if (inputBufferIndex 0) { ByteBuffer inputBuffer inputBuffers[inputBufferIndex]; inputBuffer.clear(); inputBuffer.put(naluData); // naluData是FFmpeg回调的原始字节 codec.queueInputBuffer(inputBufferIndex, 0, naluData.length, pts, 0); }此方案在小米12上1080p解码功耗降低40%发热减少明显。关键是MediaCodec的csd-0/csd-1必须用FFmpeg解析出的SPS/PPS不能用硬编码值。最后分享个小技巧调试时在C层加一行LOGI(NALU type%02x, size%d, pts%lld, nal_type, nal_size, pkt-pts)比Logcat看Java层日志快10倍。因为Java层日志要经过Binder IPC而C层log直接写入kernel ring buffer。这招帮我们快速定位了3个海康固件bug——它们在特定码率下会发送非法NALU type本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门