
做Android端音视频这些年我接到最多的需求就是“把延迟降下来”。不管你是对接海康、大华这类安防摄像头的RTSP流还是做直播App里接RTMP源“延迟”两个字永远绕不过去。我自己在几个项目里最终都是用 SmartMediaKit 把这条链路搭完的它处理RTSP/RTMP拉流、解码、渲染都够稳而且把缓冲控制权交到了开发手里做低延迟直播播放模块非常合适。这篇文章就把整套技术实践完整拆开先讲选型和协议差异再讲集成与取流参数然后给核心代码和调优逻辑最后把我实测过的延迟数据和踩过的坑一并列出来。适合正在接摄像头、做直播播放、以及想把播放延迟从秒级压到毫秒级的Android开发同学。我不太喜欢讲虚的所以下面尽量多给能直接抄的参数和代码。不同项目细节会有差异但思路是通用的。只要你能自己改Java层代码这篇文章里的内容就可以直接搬到你自己的模块里。1. 选型之前这个模块到底解决什么问题1.1 RTSP 和 RTMP两套协议打天下先说RTSPRealtime Streaming Protocol实时流传输协议。它在安防领域几乎是标配海康、大华、宇视的摄像头和NVR都开放RTSP取流。控制面用RTSP的DESCRIBE、SETUP、PLAY这些命令数据面实际走RTP默认基于UDP也可以指定走TCP。地址一般长这样rtsp://admin:password192.168.1.64:554/Streaming/Channels/101后面这个“101”代表主码流“102”是子码流。RTSP本身不算复杂但它的协商流程多播放器需要先和摄像头来回几次交互才能开始拉媒体数据协议上就比RTMP多了不少握手时间首帧延迟也天然偏高。再说RTMPAdobe制定的直播协议基于TCP数据以FLV tag封装。现在很多直播平台、CDN、推流工具比如OBS仍然大量使用RTMP因为它虽然古老但稳定、穿透性好。常见的播放地址长这样rtmp://192.168.1.10:1935/live/stream。Android这边原生MediaPlayer对RTMP基本不买账对RTSP也只是“能用”级别延迟高、可控性差。我早期试过用VideoView去放海康的RTSP流能出画面但延迟动辄两三秒而且切码流、断线重连这些需求完全没法做。ExoPlayer对RTSP的新版本支持也在补但对RTMP同样不原生。所以要在Android上做一个统一的低延迟播放模块最省力的方案就是借助一个成熟的多媒体播放器内核。这就是SmartMediaKit的定位它底层用FFmpeg做协议解析和解码上层暴露接近MediaPlayer的Java接口RTSP、RTMP都能拉硬解软解都能配关键是jitter buffer这类影响延迟的参数是可以调的。下面这张表是我整理过的几个候选方案差异可以帮你判断为什么选它。方案协议支持定制空间维护状态适合场景SmartMediaKitRTSP/RTMP/HTTP-FLV等高暴露缓冲和解码参数活跃低延迟播放模块ijkplayerRTMP/HTTP/部分RTSP中需编译期改配置维护放缓通用直播播放器VLC for AndroidRTSP/RTMP等低模块化程度差活跃但体积大万能播放器自研FFmpeg方案取决于自己最高自己维护长期产品化1.2 为什么不自己写 FFmpeg而是选 SmartMediaKit很多人一听到“低延迟”第一反应就是自己用FFmpeg拉流觉得可控性最强。这个想法没错但实际上代价非常高首先你需要在Linux环境交叉编译FFmpeg生成带openssl、rtmp等依赖的so库光编出来就是一门手艺然后要写JNI封装把AVFormatContext、AVCodecContext这些结构体暴露给Java层还要自己处理Surface渲染涉及ANativeWindow、音频播放AudioTrack那套线程模型、音视频同步、丢帧策略。这一套做下来没有两三个月的稳定期根本不可能直接上线。SmartMediaKit的价值在于它把这些脏活累活都封装好了但没把手脚捆住。它同样跑在FFmpeg之上所以协议和解码能力不会有天花板它在JNI层做了缓冲管理和音视频同步同时把“缓冲上界”、“硬解开关”、“传输模式”这些关键参数暴露出来。你在Java层只需要创建播放器、设置Surface、填URL、调两个参数就能拿到一条可以调优的播放链路。对大多数项目来说这是“用最少的成本把延迟做下来”的最优解而不是自己去和Native层的线程模型作斗争。我选它的另一个原因是接口风格接近MediaPlayer。项目里其他工程师上手快不用每个人都去啃FFmpeg那套API文档成本低。后面所有示例代码也都基于这套接口。需要说明的是不同版本SmartMediaKit的API名可能有差异但核心思想不变照着思路迁移很轻松。这也是我推荐先把它跑通、再考虑二次开发的原因。2. 环境集成与取流地址的正确姿势2.1 Android 工程接入 SmartMediaKit先说怎么把SmartMediaKit接进工程。两种方式如果你只是想快速验证优先用Gradle直接依赖仓库里的预编译包如果你的项目需要改底层逻辑或者要把特定解码器编进去再考虑源码编译。implementation com.github.smartmediakit:smart-player:1.x.xAndroidManifest里加权限uses-permission android:nameandroid.permission.INTERNET /这里有一个很容易踩的坑Android 9API 28之后默认禁止明文HTTP流量而很多RTSP/RTMP测试地址并不是HTTPS尤其局域网里的海康摄像头直接访问会报“Cleartext HTTP traffic not permitted”。如果只在调试阶段用可以给debug变体配置一个network security config允许特定IP段明文传输不要图省事把整个App的cleartextTrafficPermitted设成true就上生产后面安全审计会找你麻烦。还有NDK和ABI的问题一般真机只需要arm64-v8a模拟器可能需要x86_64多打包几个ABI会导致包体积变大自己按需取舍。源码编译方式就不给具体命令了因为它依赖你和NDK环境的版本匹配。我见过最多的错误是NDK版本过高导致JNI库编译失败或者CMake路径不对。建议直接跟随项目README的版本组合来配环境不要用Android Studio自动下载的最新NDK头铁去试。如果你用Android Studio打开工程一直报so库找不到多半是ABI过滤问题检查一下app的build.gradle里abiFilters有没有写对。2.2 RTSP 取流参数安防摄像头是主要场景我实际项目里RTSP场景九成是安防摄像头。海康的取流地址分主码流和子码流标准格式是rtsp://用户名:密码IP:554/Streaming/Channels/101101是主码流102是子码流。大华则是rtsp://用户名:密码IP:554/cam/realmonitor?channel1subtype0subtype0是主码流、1是子码流。主码流分辨率高、码率高画质好但解码压力大子码流适合多路预览和低延迟场景。做低延迟播放模块时我通常建议业务侧默认接子码流画面只需要“看得到”而不是“看得精”时子码流能让整条链路的延迟和卡顿都降一个档次。接下来是RTSP的传输模式。SmartMediaKit一般会让你选UDP、TCP或AUTO。UDP延迟低因为省了TCP的重传和拥塞控制但丢包后会花屏、拉丝TCP抗丢包能力强延迟会略高。我的经验是局域网内、网络质量好的场景直接UDP低延迟优先跨公网、Wi-Fi信号不稳的场景选TCP否则一旦丢包画面卡成PPT更让人崩溃。调试时还可以先开AUTO模式看它自动选型再结合具体网络手动指定。这里贴一段我常用的Kotlin配置代码player.setDataSource(rtspUrl) player.setOption(rtsp_transport, udp) player.setConnectTimeout(3000) player.setReadTimeout(5000) player.prepareAsync()还有一个容易被忽略的点超时配置。摄像头不在线、密码错误、网络断连时如果播放器傻等用户就会觉得“卡死”了。要给连接超时和读超时都设上比较短的值比如连接3秒、读超时5秒并在回调里及时抛错误、走重连逻辑。如果你对接的是H.265摄像头还要留意硬解兼容性。很多中低端手机的MediaCodec不支持H.265遇到这种流不提前做软解兜底出来的就是绿屏或者黑屏。这个问题我后面在排坑部分专门讲。2.3 RTMP 拉流参数与测试地址RTMP场景主要是直播地址形如rtmp://host:1935/live/streamKey。它基于TCP所以传输层面的坑比RTSP少延迟主要被缓冲和播放器内部的队列吃掉。SmartMediaKit内部对RTMP流的处理方式是解析成FLV tag再走解复用这个细节决定了你要关注的关键参数关键帧间隔、GOP大小以及FLV tag缓冲长度。调试RTMP最缺的是一个稳定可用的测试地址。我的常规做法是本地用OBS推流然后用SRS或nginx-rtmp搭一个简单的RTMP服务最后用rtmp://127.0.0.1:1935/live/test来测。如果你是在Android模拟器里访问宿主机记得用10.0.2.2代替127.0.0.1真机调试则要填电脑的局域网IP。不想自己搭服务的话也可以找公开的rtmp测试地址但公开地址往往不稳定我建议还是本地起一个排查起来可控。RTMP的低延迟配置思路和RTSP其实差不多都是控制缓冲上界。不过RTMP因为是TCP传输不会出现UDP那种大量丢包把缓冲调低后延迟能比较稳定。我还习惯在RTMP场景额外关注“队列不要被慢渲染拖垮”如果你的SurfaceView在后台被回收了但播放器还在跑解码队列会无限堆积这是延迟飙升的元凶之一。这个留到第3节统一讲。3. 低延迟播放模块的核心实现逻辑3.1 播放器初始化与渲染绑定核心流程其实不复杂创建一个播放器实例绑定Surface设置数据源然后异步prepare。下面是我手机端模块初始化的Kotlin骨架class LowLatencyPlayer(private val surface: Surface) { private val player SmartPlayer() fun start(url: String) { player.setSurface(surface) player.setEnableHardDecode(true) player.setBufferSize(300) player.setOption(rtsp_transport, udp) player.setDataSource(url) player.prepareAsync() player.setOnPreparedListener { it.start() } player.setOnErrorListener { _, code, msg - handleError(code, msg) } } }渲染层我建议优先用SurfaceView不是TextureView。原因很直接SurfaceView在系统里是独立窗口合成路径短GPU开销小实测延迟比TextureView低。TextureView的优势是可以随便做动画、截图、变形但它本质是一个普通View要经过View渲染管线再合成多了一道流程延迟在部分机型上会高出二三十毫秒甚至更多。低延迟直播播放能上SurfaceView就上SurfaceView。只有在需要做圆角、蒙层这种视觉效果时才退回去用TextureView。硬解和软解的选择也直接影响延迟和兼容性。硬解走MediaCodec延迟低、省电但各家芯片对编码格式的支持参差不齐软解走FFmpeg内置解码器兼容性好但CPU占用高。我的默认策略是“硬解优先出错自动切软解”先尝试硬解一旦收到解码错误回调就释放播放器、换软解重来。不要一个策略走到底这两个是互斥取舍没有绝对最优。3.2 低延迟靠什么缓存与时钟两手抓播放延迟不是单点问题而是整条链路累积的结果网络抖动缓冲jitter buffer、解复用器的队列、解码器内部缓冲、渲染线程的排队、音视频同步的等待每一环都会吃掉几十到几百毫秒。SmartMediaKit能做的就是把其中几个可控点交给我们。最重要的一招是压缩缓冲上界。很多播放器默认会缓存3到5秒数据来抗网络抖动对监控和直播这种实时性场景这个数显然是灾难。我在模块里把缓冲上界压到300毫秒左右让播放器宁可偶尔卡一下也不让画面永远慢半拍。具体数值按你的网络环境微调Wi-Fi稳定可以再低弱网环境则适当放宽到500毫秒否则频繁卡顿体验更差。第二招是丢帧策略。直播播放到后期最常见的问题不是首帧慢而是“延迟越拉越大”。原因很简单网络有一点波动播放器就攒一点缓冲渲染线程消化不完队列越排越长画面和真实时间的差距就越拉越大。解决办法是在同步层做“追帧”当检测到当前缓冲播放位置落后太多了就主动跳过未渲染的视频帧只解码关键帧和最新帧快速追赶。这个逻辑用伪代码表达就是if (player.bufferedPosition - player.currentPosition MAX_DELAY) { player.dropVideoFrames() }音频方面我的做法是维持AudioTrack低延迟输出。SmartMediaKit在播放带音频的RTSP/RTMP流时默认会做A/V同步通常以音频时钟为主因为人耳对声音的卡顿比视觉更敏感。你要做的是把音频缓冲长度也压下来让声音不要比画面快太多也不要一直积压。假设音频buffer有300毫秒视频buffer也有300毫秒光这两个叠加就600毫秒了再加上解码和渲染开销整体延迟自然高。所以整个模块优化时视频和音频的缓冲参数要一起调别只盯画面。3.3 生命周期、断线重连与后续扩展这部分是很多做过播放器的人都踩过的坑Activity还在画面没了或者退后台再回来播放器傻了。我的标准做法很简单onPause的时候把Surface从播放器摘掉并暂停播放onResume的时候重新绑定并恢复播放。不要想着让播放器自己扛Android的Surface生命周期跟Activity是绑定的你不主动释放大概率会遇到“黑屏但声音还在”的诡异状态。override fun onPause() { super.onPause() player.pause() player.setSurface(null) } override fun onResume() { super.onResume() player.setSurface(surface) player.start() }断线重连也是直播模块的刚需。我的做法是在onError回调里区分错误类型连接超时、IO错误这类网络问题才走重连解码错误这种硬件问题直接提示用户或切软解不要盲目重连死循环。重连用指数退避第一次等1秒第二次2秒再往后4秒最多到15秒封顶避免网络没恢复时疯狂打请求。具体可以这样组织var retryCount 0 private fun handleError(code: Int, msg: String) { if (isNetworkError(code)) { val delay min(15000L, 1000L shl retryCount) retryCount handler.postDelayed({ start(url) }, delay) } }再聊几句扩展。我在这套模块上还做过预览多路切换、录像回放和倍速播放。思路是复用一个播放器实例切换数据源时先把旧流释放干净再setDataSource新地址倍速播放则不要把Turbo模式拉到太高2倍以上丢帧策略要跟着调整。说白了播放模块的价值不全在“能播”而在于“怎么播得又稳又低延迟”这些扩展在后面加的时候才能体会到架构设计的重要性。4. 实测与排坑从 800ms 压到 200ms 的过程4.1 延迟是怎么测出来的在调优之前先说怎么量化延迟。我用的方法很土但非常准电脑上打开一个毫秒级秒表页面手机用另一个摄像头或者录屏功能对着电脑屏幕同时拍摄播放画面然后对比两边时间戳的差值。也可以在推流端把带时间戳的画面推出去播放端直接截图对比。这个方法的输出就是“画面比真实时间慢了多少毫秒”。别凭感觉判断延迟优化最忌讳拍脑袋。我实际测试的环境同一局域网海康子码流RTSPH.264常见720P级别手机和摄像头都连同一个路由器。初始参数下播放器默认缓冲较大实测延迟在780到850毫秒之间浮动。我把RTSP传输切成UDP、缓冲上界压到300毫秒、打开硬解、音频缓冲同步调低之后同样的流稳定在200到280毫秒区间。RTMP场景用OBS推流到本地SRS延迟大概在220到330毫秒主要波动来自关键帧间隔和电脑编码负载。下面这个表是我在不同配置下记录的参考数据网络环境是稳定Wi-Fi配置RTSP延迟RTMP延迟卡顿表现默认缓冲3s级780ms900ms几乎不卡但不可用TCP缓冲500ms380ms420ms正常偶发小卡UDP缓冲300ms硬解220ms-稳定偶见花屏TCP缓冲200ms300ms320ms偶发卡顿可接受数据不一定代表所有机型但趋势很明确延迟和抗抖动是一对反比关系你要根据业务容忍度找平衡点。4.2 那些年踩过的坑坑一延迟越拉越大。这是直播播放最典型的问题。现象是刚开始延迟还行直播放了十分钟延迟从200ms慢慢涨到1秒多。根因就是解码和渲染队列的累积速度大于消费速度又不做追帧。排查方法在代码里周期打印currentPosition和bufferedPosition如果两者差持续增大基本实锤。解决办法就是我前面说的丢帧策略在同步层加延时检测超阈值就丢帧追上。坑二花屏、绿屏、画面撕裂。大体分两类。一类是H.265流在硬解不支持的机型上解码失败表现是绿屏甚至直接没画面对策是先查MediaCodecInfo是否支持HEVC不支持就强制软解另一类是UDP传输丢包导致RTP数据丢失画面出现马赛克或局部绿块对策是切TCP或降低码流规格。我遇到过最坑的一种情况是SurfaceView前置到非主线程、或者Surface被释放后播放器还在写导致渲染崩溃这个要用好onPause/onResume的摘Surface逻辑。坑三首帧太慢。RTSP因为要DESCRIBE、SETUP、PLAY来回几轮协商加上必须等到IDR关键帧才能出图没优化时首帧可能要两三秒。优化方向缩短连接超时、启用快速启动模式、在UI层先显示一个loading占位而不是无助地等待。另外把取流地址换成子码流也能显著加快首帧因为子码流的GOP通常更小关键帧更快到来。坑四声音和画面各说各话。音频比视频快或者慢核心是A/V同步策略没调好。直播流如果音频缓冲比视频缓冲大很多声音会明显滞后反之声音超前。我在模块里的做法是统一以音频时钟为主把音频缓冲调小、视频缓冲追音频同时保留一个“偏差超过300ms就快进音频”的自愈逻辑。这个方法在多个项目里验证过比所谓“自动同步”稳得多。坑五国产ROM和模拟器兼容性问题。华为、小米、部分定制系统对SurfaceView的生命周期管理有差异偶尔会出现播放器启动后黑屏三星反而正常。我比较有效的应急手段是把渲染层切换到TextureView或者通过postDelayed在Surface真正available之后再绑定。模拟器上则优先检查ABI匹配用x86的镜像却只打了arm64的so库必然找不着解码器。5. 写在最后的经验小结踩了这么多坑之后我个人的体会是低延迟播放模块的成败不在某一个参数上而在于你是否理解整条链路在哪里“囤货”。网络层囤包、缓冲层囤数据、解码层囤帧、渲染层囤画面任何一环都会把延迟撑大。所以我的习惯是每次优化都先量化当前延迟贡献最大的环节再动手调而不是凭感觉把所有缓冲都压到最低那样延迟是下来了卡顿也跟着来了用户照样骂。另外再说一个很多新手会忽略的小细节调试阶段一定要把播放器状态回调打得足够详细尤其是onInfo里关于缓冲开始、缓冲结束、首帧渲染、丢帧数量这些消息。我见过太多人上线之后才加日志结果问题复现不了。这类经验在文档里没人会教你只能靠实战攒。最后补充一个我常用的思路不要把RTSP和RTMP两套场景的配置写死在一套参数里。我的模块里提供两套Profile一套UDP优先、低缓冲专门给安防内网一套TCP稳、缓冲略高、带完善重连专门给公网直播。业务方按需取用比统一调参要省心得多。如果你也在折腾SmartMediaKit的低延迟播放希望这篇实践记录能让你少走几段弯路。