ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android RTSP拉流转RTMP推流:参数调优与工程实现

Android RTSP拉流转RTMP推流:参数调优与工程实现 简介面向 Android 音视频开发与服务器运维人员的工程实践资源围绕 FFmpeg 拉取 RTSP 实时流并推送至 RTMP 服务器的核心任务展开。压缩包内含可直接导入的 Android 工程目录划分清晰涵盖 Java 调用层、JNI 桥接层、C 处理逻辑与 CMake 构建配置并提供 so 动态库、gradle 脚本及批处理工具便于对照源码理解编解码、封装与推流参数设置。资源共 484 个文件主要类型包括 138 个 h 头文件、72 个 xml 配置、56 个 json 数据、42 个 txt 说明其余为 cmake、bin、so、log 等构建运行文件压缩包大小仅 7.63MB整体轻量且依赖关系明确目录中没有冗余内容便于按模块查阅。已有 1399 人学习下载适合具备 Android 或 NDK 基础的开发者可直接复用工程模板、参考关键代码段并从中获取排错与参数调优思路整体覆盖从工程导入到推流上线的常见流程项目内函数注释和关键位置标识能有效缩短环境调试时间。1. Android上的RTSP拉流转RTMP推流难在参数不在API摄像头输出的RTSP流要变成RTMP推送表面上是FFmpeg一条命令行的事真正落到Android工程里就会遇到协议栈差异、缓冲策略冲突和so库裁剪三个坎。同样的命令在PC上能跑通放到手机上一会花屏一会断流多半是没搞清楚输入输出端各自的参数边界以及JNI层数据怎么喂给编码器。这篇文章面向要把推流管线做进App的一线开发讲清楚拉流端、推流端各自要控哪些参数重连和延迟怎么调最后给出可以直接抄的验证方法。适合用Android设备做采集端、需要对接已有RTMP直播服务的团队也适合想绕过商业化推流SDK、自己掌控码流的开发者。2. 集成方式选型预编译库、源码编译与NDK工程结构FFmpeg进Android工程第一步就是决定so库从哪来。选错方式会在后面改参数时处处受制所以先把三种途径的边界说清楚。2.1 预编译库与源码编译的取舍大多数业务场景不需要修改FFmpeg内部逻辑直接下载预编译产物最省事。下载前确认三件事ABI是否覆盖实际设备的目标架构最低要arm64-v8a是否带--enable-gpl和libx264这决定H.264是用系统编码器还是x264协议层是否完整RTSP、RTMP、TCP、UDP都在别拿一个只做了M3U8裁剪的包来跑直播链路。预编译库的痛点在于配置固化。当你发现so里没编进去rtsp demuxer或者想打开--enable-openssl支持RTMPS除了重新找包就只能回到源码编译。源码编译本身不难难的是第一次configure参数给不全编出的so会上百MB。常见的最小集合是这样的configure开关作用为什么这个场景必须要有--disable-everything默认全关按需打开不加这个so体积会到80MB以上--enable-demuxerrtspRTSP解封装没有它avformat_open_input不认rtsp协议--enable-muxerflvFLV封装RTMP的封装层就是flv muxer--enable-protocoltcp,udp,rtmp底层传输协议rtsp走tcprtmp推流走rtmp协议--enable-decoderh264 --enable-encoderlibx264编解码拉流过来的H.264要解推流要重新编码--enable-parserh264 --enable-bsfh264_mp4toannexb码流转换摄像头给的是Annex-B部分场景要转AVCC编译命令里一定带上--enable-gpl --enable-libx264否则x264编不出来。如果音频也要转码再加--enable-encoderaacFFmpeg内建的原生AAC编码器在低码率下够用不需要额外编libfdk_aac。2.2 CMake链接与JNI封装的最小骨架拿到so之后在src/main/cpp里写一个薄薄的JNI层把Java传入的RTSP地址和RTMP地址交给Native函数。CMakeLists.txt的关键写法cmake_minimum_required(VERSION 3.22.1) project(rtsp2rtmp) add_library(avcodec SHARED IMPORTED) set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libavcodec.so) add_library(avformat SHARED IMPORTED) set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libavformat.so) # avutil、swscale 同理略 add_library(native_rtsp_rtmp SHARED src/main/cpp/rtsp_rtmp_jni.c) target_link_libraries(native_rtsp_rtmp avcodec avformat avutil swscale log)IMPORTED_LOCATION指向jniLibs下对应ABI目录${ANDROID_ABI}会自动代入当前构建架构。JNI函数用GetStringUTFChars接收两个URLJNIEXPORT jint JNICALL Java_com_example_pusher_NativePusher_start( JNIEnv *env, jobject thiz, jstring jInput, jstring jOutput) { const char *input (*env)-GetStringUTFChars(env, jInput, NULL); const char *output (*env)-GetStringUTFChars(env, jOutput, NULL); int ret run_pipeline(input, output); (*env)-ReleaseStringUTFChars(env, jInput, input); (*env)-ReleaseStringUTFChars(env, jOutput, output); return ret; }注意两点GetStringUTFChars返回的是JVM内部内存的拷贝用完后必须Release防止泄漏run_pipeline是阻塞的死循环必须放在Java层new出来的子线程里执行不能占主线程否则ANR是必然的。2.3 编译产物与运行时的依赖边界裁剪后的so建议按libavformat.so、libavcodec.so、libavutil.so、libswscale.so四个文件拆分。FFmpeg各库之间有严格的版本依赖升级其中一个必须全部替换混搭版本会在运行时直接报undefined symbol。排查这类问题先对每个so跑一遍objdump -T看导出符号确认链路上谁缺依赖。提示run_pipeline内部会长期持有编解码器上下文。stop时要先置中断标志再让读包循环自然退出不能粗暴pthread_kill否则会留下野指针下一次start直接段错误。3. 拉流端RTSP输入参数分析与缓冲控制RTSP拉流是整个链路的源头。源头延迟一高后面推流怎么调都救不回来。这一章围绕avformat_open_input展开把能影响实时性的参数逐个拆开。3.1 打开RTSP流的最小可行配置在Android上打开RTSP流的代码骨架如下AVFormatContext *in_ctx avformat_alloc_context(); AVDictionary *opts NULL; av_dict_set(opts, rtsp_transport, tcp, 0); av_dict_set(opts, buffer_size, 10485760, 0); av_dict_set(opts, max_delay, 500000, 0); av_dict_set(opts, stimeout, 3000000, 0); int ret avformat_open_input(in_ctx, rtsp_url, NULL, opts); if (ret 0) { char errbuf[AV_ERROR_MAX_STRING_SIZE]; av_strerror(ret, errbuf, sizeof(errbuf)); // 回调Java层带上 errbuf 便于排查 return ret; }参数含义需要逐个说清楚。rtsp_transport设为tcp是移动网络下的首选UDP在NAT和弱网环境下端口穿透失败率极高表现为一直连接中或画面马赛克用TCP会牺牲一小部分实时性换来连接稳定性。stimeout的单位是微秒3000000就是3秒这是RTSP内部socket的读写超时不设它时设备断网会导致av_read_frame永远卡住。max_delay控制RTSP模块内部的缓冲上限单位同样是微秒设得太大延迟会累积设太小弱网下容易花屏。buffer_size在Android上经常被忽略它是底层socket接收缓冲区大小单位是字节。手机默认的缓冲区偏小Wi-Fi下偶发高码率会丢包设到10MB以上能缓解。3.2 探测阶段与实时性取舍avformat_open_input之后还要调用avformat_find_stream_info才能拿到视频流参数。这个函数内部会先读一部分数据做探测产生额外延迟和网络请求。对RTSP这种实时源可以把探测参数压到最小in_ctx-probesize 32768; // 默认约5MB直播源不需要那么大 in_ctx-max_analyze_duration 500000; // 微秒别在分析上耗时间 ret avformat_find_stream_info(in_ctx, NULL);probesize限制用来分析的数据量max_analyze_duration限制分析时长。摄像头这类设备码流规律32KB探针足够拿到SPS和PPS。这里还要做一步关键的参数提取for (int i 0; i in_ctx-nb_streams; i) { AVStream *st in_ctx-streams[i]; if (st-codecpar-codec_type AVMEDIA_TYPE_VIDEO) { video_stream_idx i; // RTSP流的time_base一般是1/90000 // 记住它推流之前要用 } }拿到time_base只是开始。RTSP协议里时间戳基准是90kHz源端摄像头按这个频率给每帧打时间戳到了推流端必须换算成FLV要求的毫秒时间基这个换算在第4章细说这里先记住原样保留AVStream指针。3.3 读包循环与错误分类读包是主循环循环写的不好重连逻辑就会误判。参考下面的骨架AVPacket pkt; while (atomic_load(running_flag)) { int ret av_read_frame(in_ctx, pkt); if (ret AVERROR(EAGAIN)) { av_usleep(1000); continue; } if (ret AVERROR_EOF) { handle_reconnect(in_ctx, rtsp_url); continue; } if (ret 0) { // 真实网络错误打印日志后等待重连 handle_reconnect(in_ctx, rtsp_url); continue; } if (pkt.stream_index video_stream_idx) { push_packet(in_ctx, out_ctx, video_stream_idx, pkt); } av_packet_unref(pkt); }EAGAIN表示当前无数据可读这是正常状态短暂sleep后继续即可不能当成错误触发重连。AVERROR_EOF在RTSP实时流里很少出现一旦出现说明流已被服务端主动关闭。ret 0囊括了socket超时、RTP解析失败等情况处理方式是关闭整个AVFormatContext再重新打开复用一个损坏的context基本注定失败。提示重连和错误上报要分开。UI层需要知道是网络断、摄像头重启还是推流服务器拒绝所以错误回调至少带错误码和URL两个字段。4. 推流端RTMP输出格式与编码参数拉流搞定了下一步是把数据灌到RTMP服务器。这一段不只要会调avformat_write_header还得理解FLV封装对编码器提出的约束。4.1 输出上下文与URL的隐藏要求创建输出上下文时第三个参数要显式指定flvAVFormatContext *out_ctx NULL; ret avformat_alloc_output_context2(out_ctx, NULL, flv, rtmp_url); if (ret 0 || out_ctx NULL) { av_log(NULL, AV_LOG_ERROR, output context allocate failed\n); return ret; }RTMP本身没有独立封装格式承载媒体数据用的是FLV所以muxer固定为flv。URL格式是rtmp://ip:port/app/playname例如本机测试rtmp://192.168.1.100:1935/live/stream。端口默认1935不在URL里写时FFmpeg会自动补。注意区分rtmp和rtmps后者需要TLS支持对应编译时打开openssl一般的直播服务器都是前者。打开输出后avformat_write_header会向服务器发送连接请求和元数据。这一步如果失败绝大多数原因是目标服务器没开对应端口或者认证失败。可以先用PC端telnet ip 1935确认端口可达再回来看FFmpeg日志。4.2 H.264/AAC编码参数配置若摄像头出来的码流能被RTMP播放器直接接受就做纯转封装avcodec不介入只是把H.264的Annex-B流原样塞进FLV。这种方式延迟最低、CPU占用几乎为零但一旦源端不是H.264就推不了。为保证通用性工程里通常还是加一条转码链路。编码器配置如下AVCodec *codec avcodec_find_encoder_by_name(libx264); if (!codec) return -1; AVCodecContext *enc_ctx avcodec_alloc_context3(codec); enc_ctx-width 1280; enc_ctx-height 720; enc_ctx-time_base (AVRational){1, 25}; enc_ctx-framerate (AVRational){25, 1}; enc_ctx-gop_size 50; enc_ctx-max_b_frames 0; enc_ctx-pix_fmt AV_PIX_FMT_YUV420P; enc_ctx-bit_rate 2000000; av_opt_set(enc_ctx-priv_data, preset, veryfast, 0); av_opt_set(enc_ctx-priv_data, tune, zerolatency, 0); av_opt_set(enc_ctx-priv_data, profile, baseline, 0); ret avcodec_open2(enc_ctx, codec, NULL);关键参数表中各个字段的作用参数值作用与误设后果gop_size50每50帧一个关键帧约2秒太小浪费带宽太大拉流端起播变成max_b_frames0禁止B帧FLV顺序播放友好且去掉编码时B帧引入的延迟presetveryfast对移动端CPU友好slow会明显提升画质但Android盒子会掉帧tunezerolatency关闭编码器内部缓冲是低延迟的最后一环profilebaseline老FLV播放器兼容性最好main/high对老设备不友好不要一上来就开1080p加high profile手机端实时转码的CPU预算很紧张。先720p验证延迟再逐步抬分辨率观察Codec丢帧率。4.3 时间基换算与时间戳修正这是最常见的坑。RTSP输入流的时间基多是1/90000而FLV要求毫秒。直接不换算就把AVPacket写出去播出来会快进到十几倍速。比较稳妥的做法是创建输出流时让FFmpeg按自己规则生成一个时间基再用av_packet_rescale_ts换算AVStream *out_stream avformat_new_stream(out_ctx, NULL); if (!out_stream) return -1; out_stream-time_base av_make_q(1, 1000); out_stream-codecpar-codec_type AVMEDIA_TYPE_VIDEO; out_stream-codecpar-codec_id AV_CODEC_ID_H264; out_stream-codecpar-width enc_ctx-width; out_stream-codecpar-height enc_ctx-height; // 编码器extradata在这里会带上SPS/PPS必须从编码器context复制给codecpar av_packet_rescale_ts(pkt, in_stream-time_base, out_stream-time_base);av_packet_rescale_ts是按AVRational比例缩放时间戳不会引入浮点误差。若摄像头dts缺失AV_NOPTS_VALUE会在RTSP内部被修正转封装时stream_index对应的in_stream必须是实际来源的那个流不能拿AVFormatContext里第0个流凑数。5. 延迟优化与断流重连直播场景最怕两个问题延迟越堆越高和半夜断流没人管。前者靠参数后者靠重连策略。5.1 三个必调的延迟参数除了编码器端已经设过的zerolatency输入侧还有三个参数应该打开。第一个是fflags的nobufferav_dict_set(opts, fflags, nobuffer, 0);这一个option能消掉AVFormatContext内部为平滑播放做的读缓冲代价是网络抖动时更容易感知到卡顿。对推流中转场景宁可感知卡顿也不能堆积延迟。第二个是解码器的低延迟开关dec_ctx-flags | AV_CODEC_FLAG_LOW_DELAY;这个标志指示解码器减少DPB缓冲多用于实时通信。第三个是传输层的发送缓冲推流端socket写缓冲调大反而会加剧断流时数据堆积所以不调。三档延迟目标可以参考源端到FFmpeg读包不超过500ms编码延迟控制在100ms内推流缓冲不设上限时整体端到端约1到2秒。纯转封装不转码时可以把优化重点全放在网络参数上。5.2 断线检测与指数退避重连重连不是读包失败马上重新open那样会在网络恢复前疯狂打摄像头把源端设备直接打挂。给一份带退避的伪逻辑int retry_count 0; while (atomic_load(running_flag) retry_count 10) { int ret avformat_open_input(in_ctx, url, NULL, opts); if (ret 0) { retry_count; int delay_ms (1 retry_count) * 1000; // 1s,2s,4s... if (delay_ms 8000) delay_ms 8000; av_usleep(delay_ms * 1000); continue; } retry_count 0; run_read_loop(in_ctx, ...); }指数退避的上限设为8秒避免在长断网场景下每8秒打一次源端造成无谓负载。重连成功之前推流端out_ctx可以保持打开也可以重连取决于RTMP服务器的容忍策略。提示重连时先avformat_free_context释放旧context再avformat_alloc_context重建这是避免内存持续上涨的关键。5.3 常见异常与排查方向拉流失败但PC上VLC能播放先看stimeout是不是设得太小再把日志中的错误码用av_strerror转成可读文本。推流握手失败检查RTMP服务器地址的app路径是否正确很多nginx-rtmp配置的默认路径是live而playname段不能漏。画面绿屏花屏优先确认编码profile与播放器兼容性然后看封装时是否漏了SPS/PPS。音频没有声音确认输入流不是AAC而muxer却按AAC封装出现这种问题要先用ffprobe查看源流编码参数再做判断。6. 验证链路用ffprobe和本地RTMP服务确认打通推流代码写完之后先别急着连线上服务器用一条本地验证链路把两端拆开测。第一步确认RTSP源端可用在PC上跑ffprobe -rtsp_transport tcp -v error -show_streams rtsp://192.168.1.64/live/ch0看输出里有没有codec_nameh264和nb_streams确认分辨率、帧率、profile都符合预期。若这里就失败问题在摄像头和网络与Android代码无关。第二步在本地起一个RTMP测试服务用nginx加rtmp模块是最常见的做法配置文件里加上这样一段rtmp { server { listen 1935; application live { live on; record off; } } }用ffmpeg从同一个RTSP源推一条流到本地服务验证源端本身的可用性ffmpeg -rtsp_transport tcp -i rtsp://192.168.1.64/live/ch0 \ -c:v libx264 -preset veryfast -tune zerolatency \ -c:a aac -f flv rtmp://127.0.0.1:1935/live/test如果这一步在PC上能稳定跑2小时以上再让Android端去连同一台服务器。这样可以有效缩小排查范围Android起不来是工程问题连上了但花屏是编码参数问题跑一会断流则是Android弱网处理和重连策略问题。第三步验证Android端最终效果同样用ffprobe去拉自己推出来的流ffprobe -rtmp_transport tcp -v trace -i rtmp://127.0.0.1:1935/live/test连句口诀可以记住先探源、后推流、再验收三个链路分开排错不要开着摸黑的安卓包直接上真机看画面。用这个顺序排查你会发现大多数所谓“FFmpeg不兼容”的问题最后都落在参数没喂对或者so库裁剪过头这两件事上。本文还有配套的精品资源点击获取
返回列表