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

📅 发布时间:2026/9/3 2:47:31
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可执行文件及CMake/Ninja构建脚本完整呈现跨平台编译与Native层数据管道设计另有18个txt说明文档与24个json配置辅助理解NALU解析逻辑与SPS/PPS提取流程。资源包大小为7.13MB结构清晰、模块分离包含gradlew构建入口与完整Android Studio工程骨架。目前已有1115人学习下载读者可直接复用该工程实现RTSP→NALU→MediaCodec硬解的端到端链路掌握起始码识别、字节流分帧、线程同步及错误处理等关键实践细节。1. 项目概述为什么在Android上直接获取H.264 NALU数据是刚需而不是炫技你有没有遇到过这样的场景在安防监控App里用户点开一个RTSP流地址画面卡顿、花屏、延迟高达3秒或者想在移动端做AI视频分析——比如实时人脸检测、车牌识别、行为轨迹追踪——但发现用SurfaceView或TextureView播放出来的视频帧要么拿不到原始YUV数据要么拿到的是解码后的RGB/BGRA格式再转回H.264编码耗电巨大、延迟翻倍又或者客户明确要求“不显示画面只把原始H.264码流转发到边缘网关做二次分发”而你用MediaCodec硬解MediaCodec硬编的链路中间多了一次解码再编码画质劣化、时间戳错乱、关键帧丢失最后连I帧都对不上。这些不是边缘问题而是Android音视频开发中真实存在的“能力断层”——系统API封装得太深把开发者和原始码流隔开了。这个标题“Android调用FFmpeg拉RTSP流获得H.264原始压缩数据NALU数据”说白了就是绕过Android MediaCodec的黑盒封装用FFmpeg作为“透明管道”把RTSP服务器吐出来的字节流原封不动地、按NALU边界精准切分后交到你的Java/Kotlin代码手里。它不渲染、不解码、不转格式只做一件事把网络上的H.264比特流变成你能逐个处理的byte[]数组每个数组对应一个NALUNetwork Abstraction Layer Unit。这才是真正意义上的“原始数据控制权”。关键词Android、FFmpeg、RTSP、H.264、NALU每一个都不是孤立存在Android是运行环境约束JNI、线程模型、内存管理FFmpeg是跨平台解协议解析NALU的核心引擎RTSP是信令与传输协议带DESCRIBE/SETUP/PLAY交互H.264是编码标准决定了NALU类型、起始码、参数集结构NALU是数据最小单元SPS/PPS/I/P/B帧的载体。这五者咬合在一起构成一个必须亲手拧紧每一颗螺丝的工程闭环。适合谁不是初学者照着教程跑通Hello World的人而是已经用过MediaCodec但被业务需求逼到墙角的开发者——你需要做低延迟推流、自定义丢帧策略、NALU级加密、SEI信息注入、或对接非标准硬件解码器。它不教你怎么写UI只告诉你当系统API不够用时怎么用C/C和FFmpeg把比特流从网线里一帧一帧抠出来。2. 整体架构设计与技术选型逻辑为什么必须用FFmpeg为什么不能只靠MediaCodec2.1 为什么FFmpeg是不可替代的底层支柱先说结论Android原生MediaCodec根本不支持“只解协议、不解码”的模式。MediaCodec的设计哲学是“输入压缩数据→输出解码帧”它强制你走完解码流程。而本项目的核心诉求是“拿到NALU就停手”中间不经过任何像素级转换。这就像你要从快递柜取包裹MediaCodec非要拆开箱子、清点每件商品、再给你一张清单而FFmpeg允许你只扫描包裹外贴的条形码即NALU头确认型号NALU type、批次IDR帧标记、重量size然后整箱搬走。这种能力差异源于底层实现MediaCodec依赖厂商HALHardware Abstraction Layer不同SoC高通/联发科/瑞芯微对RTSP协议栈支持程度天差地别。海康、大华的私有RTSP变种如带AES加密的SDP、非标准认证头MediaCodec大概率直接报ERROR_UNSUPPORTEDFFmpeg的libavformat模块是纯C实现的协议栈RTSP支持由RFC2326严格定义且社区持续维护如2023年新增对RTSP over HTTP Tunneling的支持对海康IPC的rtsp://admin:password192.168.1.100:554/Streaming/Channels/101这类地址兼容性极佳更关键的是FFmpeg的libavcodec提供AV_CODEC_ID_H264解码器但它有一个隐藏开关设置AVCodecContext.skip_frame AVDISCARD_ALL即可跳过所有解码操作只做NALU解析与分割。这是MediaCodec API里根本不存在的选项。我实测过三种方案对比同一台Pixel 4a海康DS-2CD3T47G0-I摄像头1080p15fps方案平均延迟msCPU占用率%是否支持自定义NALU过滤是否能获取SPS/PPS独立回调兼容海康私有RTSPMediaCodec SurfaceView820±15032±5否需解码后逐帧分析否SPS/PPS已合并进解码上下文否认证失败ExoPlayer CustomRenderer650±12041±6有限需Hook DecoderInputBuffer否需手动patch RTSPDataSourceFFmpeg JNI NALU Callback120±3018±3是C层直接if判断nal_unit_type是avc_parse_nal_units返回独立buffer是libavformat自动处理Digest Auth数据不会说谎FFmpeg方案延迟降低近7成CPU省一半且唯一支持“拿到SPS立刻触发初始化事件”这种关键业务逻辑。这不是性能优化而是架构层面的降维打击。2.2 为什么必须自己写JNI桥接而不是用现成SDK市面上确实有FFmpeg Android封装库比如ffmpeg-android-maker、mobile-ffmpeg它们打包了预编译的so库提供Java接口。但用过就知道它们为“易用性”牺牲了“可控性”。当你需要在NALU到达时立即检查nal_unit_type 7SPS并提取profile_idc、level_idc字段做兼容性校验对P帧NALU做长度截断因某些IPC设备会把多个P帧拼在一个UDP包里需按start code0x00000001精确切分在RTSP连接断开瞬间主动flush未发送的B帧NALU到环形缓冲区这些操作现有SDK的Java层API根本无法触达。它们把av_read_frame()封装成readPacket()把AVPacket.data转成ByteBuffer就完事中间的NALU解析逻辑全在C层黑盒里。而本项目要求“每个NALU的生命周期完全由Java侧调度”就必须自己写JNI函数暴露底层指针// native-lib.c 关键函数声明 JNIEXPORT void JNICALL Java_com_example_ffmpeg_NaluReceiver_init(JNIEnv *env, jobject thiz, jstring url) { // 初始化AVFormatContext打开RTSP流 avformat_open_input(fmt_ctx, (*env)-GetStringUTFChars(env, url, NULL), NULL, options); // 设置超时RTSP握手阶段最长等5秒 av_dict_set(options, stimeout, 5000000, 0); } JNIEXPORT void JNICALL Java_com_example_ffmpeg_NaluReceiver_startNaluLoop(JNIEnv *env, jobject thiz) { // 独立线程循环读取packet while (running) { if (av_read_frame(fmt_ctx, pkt) 0) { if (pkt.stream_index video_stream_idx) { // 关键此处调用自定义NALU分割函数 parse_h264_nalus(env, thiz, pkt.data, pkt.size); } av_packet_unref(pkt); } } }这个设计让Java层能精确控制何时开始拉流、何时暂停、何时丢弃特定类型NALU、何时将NALU存入ConcurrentLinkedQueue供AI线程消费。SDK做不到这点因为它的设计目标是“播放”而我们的目标是“数据管道”。2.3 为什么选择H.264而非H.265或AV1虽然H.265HEVC压缩率更高AV1开源免授权但本项目锁定H.264是经过产线验证的务实选择硬件兼容性Android 5.0设备99%支持H.264硬解而H.265硬解需Android 7.0且SoC明确支持如骁龙835以上低端机MT6737/6739基本无H.265解码器RTSP生态成熟度海康、大华、宇视等主流IPC厂商H.264 RTSP流是默认配置H.265需手动开启且常伴随SDP协商失败问题NALU结构简单H.264 NALU以0x00000001或0x000001为起始码解析逻辑稳定H.265起始码相同但NALU header字段更多如nuh_layer_id、nuh_temporal_id_plus1解析出错概率上升带宽敏感场景安防监控常部署在4G/5G弱网环境H.264的GOP结构I帧间隔更易调控配合FFmpeg的-vsync 0 -skip_frame nokey参数可实现精准关键帧控制。我曾尝试在红米Note 9Helio G85上跑H.265 RTSP流结果avformat_find_stream_info()卡死30秒日志显示[rtsp 0x7f8a123450] Could not find codec parameters for stream 0 (Video: hevc)——这就是生态断层的现实。H.264不是落后而是经过十年验证的“工业标准”。3. 核心细节解析与实操要点从RTSP握手到NALU精准切分的每一步3.1 RTSP协议交互的隐式陷阱与规避策略RTSP不是简单的TCP连接它是一套状态机驱动的信令协议。FFmpeg的libavformat会自动完成DESCRIBE→SETUP→PLAY流程但实际开发中以下陷阱必须手动干预认证方式适配海康IPC默认用Digest认证大华用Basic部分定制IPC甚至用Token。FFmpeg的av_dict_set(options, rtsp_transport, tcp, 0)只能指定传输层认证需额外设置// 强制使用Digest认证海康必需 av_dict_set(options, rtsp_flags, prefer_tcp, 0); av_dict_set(options, user_agent, MyApp/1.0, 0); // 关键添加认证凭据FFmpeg自动识别scheme av_dict_set(options, rtsp_auth, digest, 0);若不设rtsp_authFFmpeg可能降级用Basic导致401 Unauthorized。UDP vs TCP传输选择RTSP默认用UDP传RTP包但公网环境下UDP丢包率高。必须强制TCPav_dict_set(options, rtsp_transport, tcp, 0);注意tcp是字符串值不是布尔量。设错会导致avformat_open_input返回-5EIO错误。超时控制三重保险RTSP握手涉及多次HTTP交互单点超时不够av_dict_set(options, stimeout, 5000000, 0); // socket超时5秒 av_dict_set(options, max_delay, 500000, 0); // RTP包最大延迟500ms av_dict_set(options, rw_timeout, 10000000, 0); // 读写总超时10秒我踩过的坑某次调试海康NVR因未设stimeoutavformat_open_input在防火墙拦截时卡死60秒ANR直接杀死App。加了超时后失败立即返回可快速fallback到备用流地址。3.2 H.264 NALU结构深度解析如何从字节流中精准定位每个NALUH.264码流不是连续字节而是由NALUNetwork Abstraction Layer Unit组成每个NALU包含Header1字节 RBSPRaw Byte Sequence Payload。Header结构如下--------------- | forbidden_bit | 1 bit - 必须为0 | nal_ref_idc | 2 bits - 参考帧等级0非参考3关键参考 | nal_unit_type | 5 bits - NALU类型1非IDR图像片5IDR图像片7SPS8PPS ---------------起始码Start Code有两种0x000000014字节和0x0000013字节。FFmpeg的avc_parse_nal_units()函数会自动处理起始码但我们必须理解其分割逻辑才能正确处理边界情况场景1SPS/PPS在Annex B格式中紧邻海康IPC的SDP通常返回afmtp:96 packetization-mode1;profile-level-id420029;sprop-parameter-setsZ0IACYNkAAADAAEAAAMAAgAAAw,aMljiA这意味着SPS/PPS已Base64编码内嵌。但RTSP流中它们会以独立NALU形式出现且SPStype7必在PPStype8之前两者间无其他NALU。场景2IDR帧前必须有SPS/PPS如果流中SPS/PPS丢失如网络抖动后续所有I帧都无法解码。因此Java层需监听SPS回调一旦收到立即触发解码器初始化并缓存SPS/PPS数据。场景3FU-A分片NALU处理当单个NALU超过MTU1500字节会被拆分为Fragmentation UnitsFU-A。FU-A Header结构------------------------------ | F | NRI | Type| S | E | R | Type| ------------------------------其中S1表示首片E1表示末片Type28表示FU-A。FFmpeg默认不重组FU-A需手动处理if (nal_unit_type 28) { // FU-A uint8_t start_bit (data[1] 0x80) ? 1 : 0; uint8_t end_bit (data[1] 0x40) ? 1 : 0; uint8_t nal_type data[1] 0x1F; if (start_bit) { // 开始新FU-A重组 fu_buffer_size 0; } // 拷贝FU-A payload跳过FU Header的2字节 memcpy(fu_buffer fu_buffer_size, data 2, size - 2); fu_buffer_size size - 2; if (end_bit) { // 完整NALU重组完成添加原始NALU Header uint8_t header (data[0] 0xE0) | nal_type; memcpy(final_nalu, header, 1); memcpy(final_nalu 1, fu_buffer, fu_buffer_size); on_nalu_received(final_nalu, fu_buffer_size 1); } }这段代码是实战中必须的手动逻辑FFmpeg的高层API不暴露FU-A细节。3.3 JNI层NALU回调设计零拷贝与线程安全的平衡术Java层接收NALU最忌讳频繁创建byte[]对象。Android GC对短生命周期byte[]压力极大尤其在1080p30fps下每秒产生100个NALU平均大小20KBGC pause可达200ms。解决方案是预分配Direct ByteBuffer池// Java层NALU缓冲池 private static final int MAX_NALU_SIZE 1024 * 1024; // 1MB足够存最大NALU private final ByteBuffer[] naluBuffers new ByteBuffer[16]; static { for (int i 0; i naluBuffers.length; i) { naluBuffers[i] ByteBuffer.allocateDirect(MAX_NALU_SIZE); } } // JNI回调函数注册 public native void setNaluCallback(NaluCallback callback); // NaluCallback接口 public interface NaluCallback { void onNaluReceived(ByteBuffer buffer, int offset, int length, long pts, int type); }JNI层对应实现// native-lib.c static JavaVM* g_jvm NULL; static jobject g_callback_obj NULL; JNIEXPORT void JNICALL Java_com_example_ffmpeg_NaluReceiver_setNaluCallback(JNIEnv *env, jobject thiz, jobject callback) { (*env)-GetJavaVM(env, g_jvm); g_callback_obj (*env)-NewGlobalRef(env, callback); } void deliver_nalu_to_java(uint8_t* data, int size, int64_t pts, int nal_type) { JNIEnv* env; (*g_jvm)-AttachCurrentThread(g_jvm, env, NULL); // 从缓冲池取一个ByteBuffer jobject buffer (*env)-GetObjectArrayElement(env, g_nalu_buffers, buffer_index); // 直接memcpy到Direct Buffer内存 uint8_t* dst (uint8_t*) (*env)-GetDirectBufferAddress(env, buffer); memcpy(dst, data, size); // 调用Java回调 jclass callback_class (*env)-GetObjectClass(env, g_callback_obj); jmethodID method_id (*env)-GetMethodID(env, callback_class, onNaluReceived, (Ljava/nio/ByteBuffer;IIJ)V); (*env)-CallVoidMethod(env, g_callback_obj, method_id, buffer, 0, size, pts, nal_type); (*g_jvm)-DetachCurrentThread(g_jvm); }此设计实现零拷贝C层数据直接写入Java Direct Buffer内存避免NewByteArray和SetByteArrayRegion的开销。实测内存分配减少92%GC次数从每秒3次降至0.2次。4. 实操过程与核心环节实现从Android Studio配置到真机调试的完整链路4.1 Android Studio环境搭建NDK、CMake与FFmpeg源码编译本项目必须编译FFmpeg源码而非用预编译so原因有三1需启用--enable-decoderh264且禁用无关解码器减小体积2需修改libavformat/rtsp.c添加私有认证支持3需确保libavcodec启用CONFIG_H264_DECODERyes。步骤如下NDK与CMake版本锁定NDKr21e兼容Android 4.1且r22对ARMv7支持有bugCMake3.10.2Android Gradle Plugin 4.1要求在local.properties中指定ndk.dir/Users/xxx/Library/Android/sdk/ndk/21.4.7075529 cmake.dir/Users/xxx/Library/Android/sdk/cmake/3.10.2.4988404FFmpeg源码编译脚本build_android.sh#!/bin/bash NDK/Users/xxx/Library/Android/sdk/ndk/21.4.7075529 SYSROOT$NDK/platforms/android-21/arch-arm64 TOOLCHAIN$NDK/toolchains/llvm/prebuilt/darwin-x86_64 CPUarm64-v8a PREFIX$(pwd)/android/$CPU ./configure \ --prefix$PREFIX \ --target-osandroid \ --archaarch64 \ --cpuarmv8-a \ --cc$TOOLCHAIN/bin/aarch64-linux-android21-clang \ --cxx$TOOLCHAIN/bin/aarch64-linux-android21-clang \ --enable-cross-compile \ --sysroot$SYSROOT \ --extra-cflags-Os -fpic -D__ANDROID_API__21 \ --extra-ldflags-L$SYSROOT/lib \ --enable-shared \ --disable-static \ --disable-doc \ --disable-ffmpeg \ --disable-ffplay \ --disable-ffprobe \ --disable-symver \ --enable-decoderh264 \ --enable-parserh264 \ --enable-demuxerrtsp \ --enable-protocolrtsp \ --enable-network \ --enable-gpl \ --enable-libx264 \ # 可选用于后续编码 --enable-zlib \ --enable-mediacodec \ --enable-jni make -j4 make install提示--enable-mediacodec和--enable-jni是Android专用选项前者启用MediaCodec硬解支持虽本项目不用但避免编译报错后者启用JNI接口。Android Studio CMakeLists.txt配置# 引入FFmpeg库 add_library( avformat SHARED IMPORTED ) set_target_properties( avformat PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a/libavformat.so ) add_library( avcodec SHARED IMPORTED ) set_target_properties( avcodec PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a/libavcodec.so ) add_library( avutil SHARED IMPORTED ) set_target_properties( avutil PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/arm64-v8a/libavutil.so ) # 主库链接 target_link_libraries( native-lib avformat avcodec avutil log android )编译后app/src/main/jniLibs/arm64-v8a/下应有libavformat.so、libavcodec.so、libavutil.so三个文件总大小约3.2MB精简后。4.2 Java层核心类设计NaluReceiver与状态机管理NaluReceiver是Java侧控制中枢需处理RTSP连接状态、NALU分发、异常恢复public class NaluReceiver { private static final String TAG NaluReceiver; private final Object lock new Object(); private volatile boolean isRunning false; private volatile boolean isConnected false; private final QueueByteBuffer naluQueue new ConcurrentLinkedQueue(); // Native方法声明 public native void init(String url); public native void start(); public native void stop(); public native void release(); // 外部调用入口 public void connect(String rtspUrl) { synchronized (lock) { if (isRunning) return; init(rtspUrl); isRunning true; isConnected false; // 启动后台线程 new Thread(this::pullLoop).start(); } } private void pullLoop() { try { start(); // 触发JNI层av_read_frame循环 isConnected true; Log.d(TAG, RTSP connected: rtspUrl); // 主循环从队列取NALU处理 while (isRunning isConnected) { ByteBuffer nalu naluQueue.poll(); if (nalu ! null) { // 分发给业务处理器如AI分析、转发 onNaluReceived(nalu); } else { Thread.sleep(1); // 避免忙等 } } } catch (InterruptedException e) { Log.e(TAG, Pull loop interrupted, e); } finally { stop(); release(); } } // NALU回调由JNI触发 public void onNaluReceived(ByteBuffer buffer, int offset, int length, long pts, int type) { if (!isConnected) return; // 从缓冲池取buffer设置limit ByteBuffer reused getReusableBuffer(); reused.clear(); reused.put(buffer.array(), offset, length); reused.flip(); naluQueue.offer(reused); } }关键设计点双状态标志isRunning控制线程生命周期isConnected标识RTSP连接是否成功避免stop()后仍处理旧NALUConcurrentLinkedQueue无锁队列比ArrayBlockingQueue吞吐量高3倍实测1080p30fps下缓冲池复用getReusableBuffer()从预分配池取buffer避免频繁new。4.3 真机调试与性能调优从Logcat到Systrace的全链路排查真机调试不是Log.d就能搞定的。以下是我在华为Mate 40 ProKirin 9000上的调优记录第一步确认RTSP握手成功在adb logcat | grep -i rtsp\|avformat中搜索I libavformat/rtsp.c: Sending request: DESCRIBE rtsp://... I libavformat/rtsp.c: Received 200 OK I libavformat/rtsp.c: SETUP trackID0 I libavformat/rtsp.c: PLAY sent, waiting for response若卡在DESCRIBE检查URL格式海康必须带端口554大华常用8554。第二步验证NALU接收在onNaluReceived中添加统计private long lastTime System.nanoTime(); private int frameCount 0; public void onNaluReceived(...) { frameCount; long now System.nanoTime(); if (now - lastTime 1_000_000_000) { // 1秒 Log.d(TAG, FPS: frameCount , Type: type); frameCount 0; lastTime now; } }正常1080p15fps应输出FPS: 15, Type: 5IDR或FPS: 15, Type: 1P帧。第三步Systrace性能分析录制命令python $ANDROID_HOME/platform-tools/systrace.py -t 10 -a com.example.ffmpeg sched freq idle am wm gfx view binder_driver irq -o trace.html关键观察点native-lib线程CPU占用若持续80%说明NALU处理逻辑过重如没做零拷贝Binder线程阻塞若onNaluReceived回调中调用Handler.sendMessage()会触发Binder通信增加延迟RenderThread抖动即使不渲染SurfaceFlinger仍可能抢占GPU资源需在AndroidManifest.xml中添加application android:hardwareAcceleratedfalse /第四步内存泄漏检测使用Android Profiler监控ByteBuffer对象数。若随时间线性增长说明Direct Buffer未被回收。根源通常是JNI层未调用DeleteGlobalRef释放g_callback_objJava层ByteBuffer.allocateDirect()后未显式cleaner.clean()Android 11需反射调用。5. 常见问题与排查技巧实录那些文档里不会写的血泪教训5.1 典型问题速查表问题现象可能原因排查命令/方法解决方案avformat_open_input返回-2ENOENTRTSP URL格式错误或DNS解析失败ping 摄像头IPtelnet 摄像头IP 554检查URL是否含空格用IP直连避开DNS确认IPC端口开放日志显示[rtsp ...] method DESCRIBE failed: 401 Unauthorized认证凭据未传递或格式错误抓包Wireshark看RTSP请求头Authorization字段在URL中嵌入凭据rtsp://admin:12345192.168.1.100:554/...或设av_dict_set(options, rtsp_auth, digest, 0)av_read_frame返回0但pkt.size0流中存在空RTP包FFmpeg未过滤adb logcatgrep -i av_read_frame收到NALU但nal_unit_type0未定义IPC发送了非法NALU或FU-A重组错误打印pkt.data[0]和pkt.data[1]十六进制检查FU-A处理逻辑确保nal_type从data[1]0x1F提取而非data[0]App启动后立即ANRJNI初始化耗时过长如avformat_network_init()阻塞adb shell dumpsys activity anr将avformat_network_init()移至Application.onCreate()异步执行或设av_dict_set(options, stimeout, 1000000, 0)5.2 独家避坑技巧来自产线的3个硬核经验技巧1SPS/PPS的“双重保险”机制海康IPC有时在流中不发SPS/PPS只在SDP中导致FFmpeg解析失败。解决方案是在init()时主动从SDP提取// 在avformat_open_input后解析SDP if (fmt_ctx-nb_streams 0 fmt_ctx-streams[0]-codecpar-codec_id AV_CODEC_ID_H264) { AVDictionaryEntry *sdp_entry av_dict_get(fmt_ctx-metadata, sdp, NULL, 0); if (sdp_entry) { // 解析sdp中的sprop-parameter-sets char *sps_b64 strstr(sdp_entry-value, sprop-parameter-sets); if (sps_b64) { sps_b64 21; // 跳过前缀 char *comma strchr(sps_b64, ,); if (comma) *comma \0; // Base64解码sps_b64 - 写入NALU缓冲区 decode_base64_and_dispatch(sps_b64, SPS); } } }这样即使流中断也能用SDP中的SPS重建解码上下文。技巧2RTSP断线自动重连的指数退避简单while(true) { connect(); sleep(1000); }会触发运营商限频。采用指数退避private int retryCount 0; private void reconnect() { int delay (int) Math.min(30000, Math.pow(2, retryCount) * 1000); // 最大30秒 new Handler(Looper.getMainLooper()).postDelayed(() - { connect(rtspUrl); retryCount; }, delay); }首次失败等1秒第二次2秒第三次4秒……避免雪崩式重连。技巧3Android 12后台服务限制绕过Android 12禁止后台服务启动前台Activity而RTSP拉流常需通知用户连接状态。解决方案是使用ForegroundService!-- AndroidManifest.xml -- service android:name.NaluService android:enabledtrue android:exportedfalse android:foregroundServiceTypespecialUse /并在startForeground()中指定FOREGROUND_SERVICE_SPECIAL_USE类型通过NotificationManager.createNotificationChannel()创建高优先级通道。最后分享一个小技巧在onNaluReceived回调中不要直接做耗时操作如JNI调用AI模型而是把ByteBuffer放入ConcurrentLinkedQueue由独立线程消费。我曾因在回调里调用TensorFlow Lite推理导致NALU积压最终OOM崩溃。现在用Executors.newSingleThreadExecutor()专管AI推理主线程只负责数据搬运——这才是生产环境该有的分工。本文还有配套的精品资源点击获取