Android内置RTSP服务器:实现局域网低延迟视频推流与播放
做局域网视频传输的朋友一定有过这种憋屈时刻临时想把手机摄像头画面投到会议大屏或者把一台设备的摄像头画面共享给局域网里几台机器同时看结果发现不是得装一堆App就是绕道公网平台延迟高还受网络限制。于是我就动了这个念头在Android设备上直接内置RTSP/RTMP服务器让视频推流和播放全部在局域网内闭环手机既是采集端也是服务端。这篇文章会从方案选型、核心实现、播放联调到问题排查把整条链路完整拆开讲适合做局域网视频传输、设备互联或者刚接触流媒体协议的开发者参考。1. 项目定位与方案选型为什么要在Android里塞一台流媒体服务器1.1 从需求说起局域网视频推流/播放的几种做法我一开始的需求其实很朴素一台Android手机把摄像头采集的画面编码成H.264然后让会议室里的Windows电脑、macOS笔记本、甚至另一台安卓平板都能实时看到画面。当时摆在面前的有三条路第一条是走公网平台推流到云端的流媒体服务再让播放端去云端拉流。这种方案部署最快但问题也很明显一是要求设备能出公网很多内网环境压根不满足二是即使能出网经过公网转发后延迟通常会在3秒以上做演示时画面和声音明显脱节三是数据经过第三方平台很多内部场景不愿意。第二条是使用现成的局域网投屏协议比如Miracast、AirPlay。这类协议的问题在于生态封闭苹果设备只能走AirPlayWindows和Android之间兼容性很差而且夹持画面后通常只能单路显示很难同时让多台设备独立拉取不同分辨率的码流。第三条就是我最终选择的路线设备自己实现一个RTSP/RTMP服务端监听局域网的特定端口。电脑也好、手机也好只要装一个支持RTSP的播放器输入rtsp://安卓设备IP:端口/路径就能直接播放。这条路的好处是不依赖公网、延迟可以控制在毫秒到几百毫秒级别、播放端只要支持标准协议就能互通。整条链路中Android设备角色从“采集终端”变成了“采集分发一体机”。这个项目定位并不是做一个能撑住上万并发的流媒体服务器而是解决“现场、临时、多端、低延迟”这几个关键词组合起来的场景。所以方案设计上优先考虑三点实现简单、协议标准、资源占用可控。后面所有的选型都是围绕这三点展开的。1.2 RTSP和RTMP到底怎么选很多刚接触流媒体的人会把RTSP和RTMP搞混这俩虽然都能传视频流但设计思路和使用场景差别很大。RTSPReal Time Streaming Protocol本质上是一个信令协议它负责控制会话的建立、播放、暂停、停止真正的音视频数据走的是RTP/RTCP。默认端口是554但这个端口在Android上受权限限制所以我项目里使用8554之类的高位端口。RTSP的特点是实时性高播放控制灵活VLC、ffplay、PotPlayer以及大量安防监控平台都原生支持。RTMPReal Time Messaging Protocol是基于TCP的直播协议最早用于Flash播放器和直播推流现在依然是很多直播平台和流媒体服务比如SRS、MediaMTX的主流接入协议。它把音视频数据封装成FLV tag不断往上推实现逻辑相对简单而且穿防火墙比RTSP的UDP通道更稳定所以大量“推流端”使用RTMP再由服务端转成其他协议分发给播放端。在局域网这个场景里我的选择原则很简单如果播放端主要是VLC、ffplay这类本地播放器或者安防平台优先用RTSP因为播放器输入URL就能拉流无脑方便如果播放端在网页里或者需要接到已有的直播链路那就走RTMP后面再接MediaMTX这类服务做协议转换。这个项目最终做的是双栈兼容核心实现一套RTSP服务同时在需要时把码流推到局域网内已有的RTMP服务上。这样在测试现场基本是“想播就播、想推就推”。对比项RTSPRTMP传输层信令走TCP媒体可走UDP/TCP全部基于TCP默认端口554Android常用高位端口1935延迟表现可做到几百毫秒内通常1秒以内播放端支持VLC/ffplay/监控平台网页直播/推流服务实现复杂度信令交互多RTP打包要处理推流封装相对直接适用场景局域网内拉流播放推流给中心服务器再分发2. 技术底座Android端的编码、协议与库2.1 视频源头Camera2采集与MediaCodec硬编码Android端做视频推流第一步是把摄像头数据变成编码器能接受的数据。Camera2 API目前是官方主推方案通过ImageReader拿YUV帧或者直接把Surface交给编码器省去CPU拷贝。我实际用的是更省事的方式给MediaCodec创建一个输入Surface然后通过CameraCaptureSession把预览目标指向这个Surface这样摄像头输出的数据流会直接进入编码器全程不经过应用层拷贝。这个做法在CPU占用和延迟表现上都比先拿YUV再手动喂给编码器好很多强烈推荐。编码器选H.264协议兼容性最好。配置关键参数时有几个点容易踩坑KEY_BIT_RATE和KEY_FRAME_RATE决定了码流基本形态局域网环境建议码率设4Mbps左右帧率30fps。KEY_I_FRAME_INTERVAL是关键帧间隔单位是秒。默认值可能是1秒但如果希望播放器打开后快速出画面、拖动后更快恢复建议设成1到2秒。太长会导致播放端启动后好几秒都是黑屏。KEY_PROFILE建议用AVCProfileBaseline或者AVCProfileMain兼容性优先不要一上来就High Profile部分老播放器不支持。如果未来想支持屏幕采集思路完全一样只是把Camera2替换成MediaProjection给编码器提供的依然是一个Surface。协议侧和服务器侧不用动。2.2 RTSP服务端是怎么工作的RSTSP服务端听起来高大上剥开看其实就是一个TCP服务器加一个RTP发送器。TCP服务器负责处理客户端的信令请求常见有四个OPTIONS客户端问服务端支持哪些方法服务端返回支持列表。DESCRIBE客户端请求媒体描述服务端返回SDP信息里面包含编码格式、分辨率、帧率等。SETUP客户端请求建立媒体通道服务端分配端口并响应。PLAY客户端请求开始播放。整个交互过程有点像吃饭点菜先看菜单OPTIONS/DESCRIBE然后告诉服务员我要坐哪桌SETUP最后说“可以上菜了”PLAY。真正的视频数据通过RTP包持续发送。如果之前只做过HTTP接口那理解RTSP信令的“请求-响应”模型会非常容易差异在于RTSP是有状态的服务端要记得当前会话进行到哪一步。在Android上实现时要注意一点RTSP信令默认走TCP媒体数据默认走UDPRTP。UDP在局域网里表现很好延迟低但如果播放端和手机不在同一个网段UDP可能被交换机或路由器丢弃。所以我实现里同时支持RTP over UDP和RTP over TCP两种传输方式播放端如果UDP拉流失败可以切到TCP模式重试这类细节在项目里属于“能救命但很容易漏掉”的部分。2.3 现成方案盘点从自研到MediaMTX、GStreamer我见过不少朋友一听到“自研RTSP服务器”就觉得工作量很大其实完全取决于选择。如果只是要快速在局域网出画面有几个现成方案可以走MediaMTX原rtsp-simple-server开源的流媒体服务器支持RTSP、RTMP、WebRTC、HLS等多种协议接入和分发。可以在局域网内一台Linux主机或开发板上跑起来然后Android端通过FFmpeg推流给它再由MediaMTX输出RTSP/RTMP流给其他播放端。优势是稳定、功能全面、不需要自己处理协议细节。GStreamer RTSP server在嵌入式开发板上比较常见配合rk3588这类芯片做硬件编解码可以快速实现USB摄像头转RTSP流。但GStreamer在Android上的构建工程量不小我一般只在Linux开发板上用。libstreaming早期Android推流的经典库缺点是多年没大更新对新版本Android适配不好。最终我选择自研RTSP服务器不是因为现成方案不好而是这个项目需要对协议和链路做深度定制甚至要控制到每一个RTP包的发送时机。如果你只是应付短期交付建议直接用MediaMTX路线。但如果你想真正理解流媒体或者需要做一些定制逻辑后面这几节的自研实现拆解会很实用。3. 手写一个轻量级RTSP服务器核心代码与实施步骤3.1 初始化摄像头与编码器编码器这块是整条链路的“起点”配置正确与否直接决定后面所有环节是否顺利。我用Kotlin写了一个简化版初始化逻辑fun createEncoder(width: Int, height: Int, fps: Int, bitrate: Int): MediaCodec { val format MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height).apply { setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface) setInteger(MediaFormat.KEY_BIT_RATE, bitrate) setInteger(MediaFormat.KEY_FRAME_RATE, fps) setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1) setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline) setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31) setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR) } return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC).apply { configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) createInputSurface() start() } }注意KEY_BITRATE_MODE我用了VBR可变码率。局域网内带宽充裕VBR的好处是画面复杂时动态提高码率保证清晰度画面静态时降低码率减少发热和功耗。如果你的场景是长时间固定码率录制那就用CBR码率恒定调度更可控但画面质量波动会明显一些。Camera2部分我直接把预览目标绑定到编码器的输入Surfaceval captureRequestBuilder cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD) captureRequestBuilder.addTarget(encoderInputSurface) cameraCaptureSession cameraDevice.createCaptureSession( listOf(encoderInputSurface), sessionCallback, backgroundHandler )这一步完成后编码器的输出Buffer里会开始不断产生H.264码流接下来的问题就是怎么把这些字节流通过RTSP协议发出去。3.2 RTSP信令处理最容易被忽视的细节RTSP信令层不复杂但格式非常敏感任何一个换行符错了播放器都可能直接拒绝连接。我用ServerSocket监听8554端口每个客户端连接分配一个会话线程逐行读取请求解析方法名和URL然后分发处理。拿DESCRIBE举例播放器比如VLC发来请求后服务端要返回带SDP的响应。SDP里最关键的信息是媒体描述和编码参数v0 o- 123456 2 IN IP4 192.168.1.100 sLive Stream t0 0 mvideo 0 RTP/AVP 96 artpmap:96 H264/90000 afmtp:96 packetization-mode1;sprop-parameter-setsZ2QAH6zZQFAFuBQICAoKAA,aOvssiw这里Z2QAH6zZQFAFuBQICAoKAA和aOvssiw分别是SPS和PPS的Base64编码。播放器要拿到这两个参数才能正确解码H.264流如果你在响应里漏掉了sprop-parameter-sets很多播放器会一直黑屏或花屏。这也是我在前面强调关键帧和SPS/PPS的原因——它们是播放兼容性的基础。PLAY请求处理时服务端回复200 OK后才真正开始从编码器取数据并发送RTP包。整个信令处理里值得注意的一个坑是RTSP请求头之间的换行必须是\r\n我一开始图省事用了\n结果VLC能连上但发不出画面排查了很久才发现是这个细节。3.3 H.264的RTP打包从NALU到网络包MediaCodec输出的H.264码流本质上是一串NALU每个NALU前面有4字节长度前缀AVCC格式。而RTSP传输时播放器期望的是Annex-B格式也就是每个NALU前面用00 00 00 01分隔。所以第一步是把长度前缀转成起始码。接下来要将Annex-B格式的NALU封装成RTP包。NALU分三种情况处理单个NALU小于MTU我按1200字节处理时直接加12字节RTP头发送。大NALU比如关键帧通常远大于MTU需要分片使用FU-A分片方式。SPS/PPS在发送关键帧之前会单独发一遍确保播放器能重新初始化解码器。我简化后的RTP打包逻辑大概是fun sendNalu(nalu: ByteArray, rtpSeq: Int, timestamp: Long) { if (nalu.size MTU_SIZE) { sendSingleNalu(nalu, rtpSeq, timestamp) } else { sendFragmentedNalu(nalu, rtpSeq, timestamp) } }timestamp是按RTP时钟算的H.264的RTP时钟频率是90000Hz所以一帧的时间戳增量是90000 / fps。这个值算错会导致播放器花屏或者音画不同步。我早期用系统毫秒乘了个随意的因子结果VLC画面每隔几秒就跳一下后来老老实实按90000Hz算才正常。接收端用de-jitter buffer来缓存乱序包所以发送端可以按序发不用太担心网络抖动但RTP序号必须连续递增不能跳号否则播放器会判定丢包并触发纠错逻辑导致画面卡顿。3.4 兼容RTMP推流到局域网MediaMTX自研RTSP服务器适合播放端以RTSP拉流为主但现场经常有人拿着网页或者特定App只支持RTMP或者HLS播放。这种场景下最稳妥的路线是Android推流端把编码后的H.264封装成FLV通过RTMP协议推到局域网内的一台MediaMTX再由MediaMTX统一转成RTSP、RTMP、WebRTC、HLS等协议前端想看哪种都可以。搭建MediaMTX很简单下载可执行文件后直接跑./mediamtx默认配置里它已经开启了RTSP端口8554和RTMP端口1935。Android端推RTMP既可以自己实现FLV封装和RTMP chunk流也可以简单粗暴用FFmpeg命令行ffmpeg -f android_camera -i /dev/video0 -c:v copy -f flv rtmp://192.168.1.10:1935/live/camera但FFmpeg在Android上集成体积比较大如果要控包体还是得自己实现RTMP推流核心在于FLV tag头的构建和RTMP消息块chunk的拆分发送。如果前期不想碰RTMP最快捷的方式就是走MediaMTX。4. 端到端联调手机推流、电脑播放与参数调优4.1 准备播放端与环境检查服务端写完后第一件事是拿电脑上的VLC做冒烟测试。打开VLC选择“打开网络串流”输入rtsp://192.168.1.100:8554/live正常情况下3秒内就能出画面。如果连不上先按优先级排查手机和电脑是否在同一网段、手机上的RTSP端口是否真的在监听、防火墙是否拦截了端口。测试监听可以用adb shell netstat -an | grep 8554也可以先用一个公开的RTSP测试地址验证播放器本身比如rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mp4如果连这个都播不了那问题大概率在播放器网络环境而不是你的服务器。局域网里测试时建议用手机关闭Wi-Fi节能模式防止系统在屏幕灭掉后把编码器挂起。之前我吃过这个亏手机锁屏后码流直接断了解锁又恢复排查半天发现是系统省电策略在干预。4.2 延迟和画质怎么调优RTSP方案理论上可以做到几百毫秒内的延迟但实际效果取决于一整条链路。链路里最容易拖慢延迟的其实是播放端缓冲其次是编码器自身buffer。调优时我按这个顺序来编码端设短GOP。关键帧间隔越小播放器启动越快拖动恢复越快。局域网带宽充足我通常设置KEY_I_FRAME_INTERVAL1也就是1秒一个关键帧虽然同样码率下画质会稍微下降但换来的交互性很值。播放端关闭大缓冲。VLC里把“网络缓存”从默认的1000ms到3000ms调到300msffplay用-fflags nobuffer -flags low_delay参数启动。这一步通常能让延迟从1秒以上降到300毫秒以内。传输模式优先UDP。RTSP over UDP延迟最低但抗丢包差。同一局域网内基本无丢包优先UDP没问题。如果跨越了带防火墙的网段再切TCP模式。我的实现里对这两种模式都做了支持这也是项目里比较费劲但也比较有价值的一部分。音频如果也要加优先级排在视频后面。音频编码用AAC-LC采样率44100HzRTP打包逻辑和H.264类似只是时钟频率和负载类型不同。音频没配好很容易出现音画不同步时间戳基准必须统一。4.3 扩展应用RK3588等设备上的USB摄像头转RTSP这项目做到后面我又把它移植到了RK3588开发板上目标是把USB摄像头转成RTSP流。原理和Android手机完全一致拿到/dev/video0设备用硬件编码器RK3588自带MPP或V4L2编码成H.264然后塞给同一个RTSP服务端模块。USB摄像头和手机Camera2的区别只是数据来源不同。USB摄像头通过V4L2抓到的通常是YUV或MJPEG帧需要先转成编码器需要的输入格式。在RK3588上做法一般是USB摄像头输出MJPEG硬件解码回YUV再用硬件编码器压成H.264这样全程不用CPU处理视频像素。移动端和嵌入式端最大的差异在电源和散热。RK3588的散热条件通常比手机好可以长时间跑1080P 30fps而手机连续推流半小时后外壳温度明显上升系统会降频画质跟着波动。所以长期运行的方案设计时要考虑温度阈值温度过高时主动降码率或者降分辨率保证链路不断。5. 常见问题与排查技巧实录5.1 花屏、绿屏与关键帧间隔现象播放器能连上但画面持续花屏或绿屏。排查顺序先确认SPS/PPS是否正确下发再确认每个关键帧前是否都单独发了SPS/PPS。很多播放器在关键帧刷新时才重新初始化解码器如果关键帧的SPS/PPS缺失画面就会一直花。还有一个容易被忽略的原因是编码器输出的SPS/PPS是动态变化的有些设备会在分辨率或码率变化后重新生成SPS/PPS服务端要动态读取并更新不能只在会话建立时取一次。5.2 延迟忽高忽低与时间戳问题现象画面整体还能看但延迟忽大忽小后期越来越卡。这类问题大概率出在RTP时间戳。如果时间戳不是按90000Hz计算而是一会儿用当前毫秒、一会儿用帧序号播放端会认为包乱序丢失反复请求关键帧延迟自然越滚越大。排查时看服务端日志里时间戳增量是否稳定在90000 / fps附近如果不是优先修正时间戳计算。宽高也容易引出问题。编码器要求宽高是16像素对齐部分设备要求2像素对齐如果直接用1080x1920这种尺寸没问题但有些自定义分辨率没有对齐编码器会内部裁切或者报错导致画面变形或编码失败。建议统一用标准分辨率或者先调MediaCodecInfo查询对齐要求。5.3 连接不上、被防火墙拦截、网段隔离现象服务端进程正常但手机连不上。先分清楚是“端口没监听”还是“数据包被拦截”。netstat确认监听后再用另一台设备telnet IP 8554测试端口连通性。局域网内最常见问题其实是AP隔离也就是路由器开启了“访客网络隔离”设备间无法互访关掉这个开关即可。另外Android 9以上对网络权限要求严格INTERNET权限要在Manifest里声明漏掉的话服务端连socket都绑不上。5.4 长时间运行发热、掉帧和码率下降设备跑半小时后码率下降这是系统温度保护在起作用。通过adb shell dumpsys thermalservice可以看到温度状态达到温控阈值后CPU和GPU都会降频。缓解手段是在应用层加码率限制策略比如检测到设备温度超过55度自动从4Mbps降到2Mbps分辨率从1080P降到720P。我看到有些项目还会在编码器前加一层环形缓冲丢帧策略把优先级设为“保关键帧优先”。也就是画面动态变化太大导致编码压力上升时先丢弃非关键帧确保GOP结构完整这样播放端虽然帧率低一些但不会花屏卡死。我个人在实际操作中的体会是这个项目做下来最大的收获不是某一端写得多漂亮而是把“编码、信令、RTP打包、播放端兼容”这条链路彻底打通了。以后不管换什么设备、接什么摄像头核心思路都能复用。如果你想快速验证效果建议先搭一台MediaMTX用手机推RTMP的方式把整条链路跑通再回到自研RTSP服务器的方向上做深度定制。最后再分享一个小技巧不管你的播放端要兼容多少种设备先把SPS/PPS和关键帧这两个基础问题解决好后面百分之八十的兼容性问题都不会发生。