简介:面向需要处理实时流媒体播放的Android开发者,这套可运行Demo集成IjkPlayer,用于播放RTSP/RTMP视频流,适合在视频监控、直播推拉流等场景中快速起步。压缩包共147个文件,大小13.68MB,包含aar库、so动态库、kotlin/Java源码、xml布局与配置、gradle构建脚本及png图标等,可直接导入Android工程运行调试。已有980人学习下载。Demo以MyIjkPlayerTest-RTSP为例,演示了从SurfaceView/TextureView绑定、setDataSource设置RTSP地址,到prepareAsync异步准备、播放监听与资源释放的完整流程,同时提供ijkplayer-java-release.aar和基础排错逻辑,开发者可对照源码理解IjkPlayer的集成方式,并迁移到RTMP等协议的播放实现中。 做安防类App或者直播客户端的Android开发,十有八九会碰到同一个需求:在手机端直接播放摄像头的RTSP视频流,或者播放RTMP直播源。我最早接这个需求的时候,第一反应是直接用系统自带的MediaPlayer,结果被协议兼容性和延迟问题来回教育。后来换成IjkPlayer这套方案,配合FFmpeg底层的能力,才算是把RTSP和RTMP的播放彻底跑通。这篇文章就围绕一个可运行的Android Demo展开,从环境配置、核心代码到排障经验一次讲清楚。如果你正在做IPC摄像头对接、监控客户端,或者需要在App里嵌入直播播放能力,这篇内容应该能帮你省掉不少弯路。
1. 项目由来与播放器选型逻辑
1.1 这个Demo到底解决什么问题
先把需求拆开看,RTSP和RTMP其实是两条路线的产物。RTSP(Real Time Streaming Protocol)是安防领域的“老大哥”,海康、大华这些厂商的IP摄像头基本都支持RTSP取流,URL大概长这样:rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101。RTMP(Real Time Messaging Protocol)则是直播行业的“老兵”,很多推流端、CDN、直播平台到今天还在用它做音视频上行传输,常见格式是rtmp://192.168.1.100:1935/live/stream。
这两类协议浏览器默认都不支持,但在Android原生端,只要选对播放器内核,就能直接解码渲染。这个Demo的目标就是:拿到一个RTSP或RTMP地址,打开App,点一下播放按钮,画面就出来。适合作为视频类项目的基础模块,往后再去扩展什么云台控制、录像回放、码流切换,都是在它上面做加法。
1.2 播放器方案横评:IjkPlayer赢在哪
动手之前我对比过三个主流方案:系统MediaPlayer、Google的ExoPlayer、B站开源的IjkPlayer。表格可能更直观:
| 对比维度 | 系统MediaPlayer | ExoPlayer | IjkPlayer |
|---|---|---|---|
| RTSP支持 | 很弱,依赖硬件厂商实现 | 需要扩展,默认不友好 | FFmpeg内核,开箱即用 |
| RTMP支持 | 基本不支持 | 有限支持 | 原生支持,兼容性好 |
| 延迟控制 | 不可控 | 可调,但有上限 | 可通过参数精准控制 |
| 自定义能力 | 极弱 | 强 | 极强,可改源码重编译 |
| 集成难度 | 最低 | 中等 | 中等,但资料多 |
IjkPlayer底层是FFmpeg,对RTSP和RTMP这类非HTTP协议的解析能力是另外两个方案没法比的。实际测试下来,同一台海康摄像头,用系统MediaPlayer十次有八次弹“无法播放”,ExoPlayer倒是能出画面,但延迟和缓冲策略不好调,IjkPlayer基本是即插即用。另外IjkPlayer在硬解码和软解码之间可以自由切换,这也是后面排查兼容性问题时的一个关键开关。
1.3 “可运行”这几个字的份量
说实话,网上打着“可运行Demo”旗号的仓库很多,但clone下来经常跑不起来:要么so库没打全,要么本地地址已经失效,要么漏了权限声明。我这边的经验是,要让Demo真正“可运行”,得把三件事做扎实:依赖完整、地址可换、设备适配。尤其是RTSP地址,如果你没有真实摄像头,可以先用公开测试源或本地搭建的流媒体服务器顶上,后面会具体说怎么找可用的测试地址。
2. 工程搭建与环境准备
2.1 依赖引入:Gradle一行配置与局限
IjkPlayer在JCenter上有一个历史版本0.8.8,虽然仓库后来不更新了,但社区还在用,常规播放需求完全够。在build.gradle里加依赖:
dependencies { implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8' implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8' // 真机以arm64为主,建议加上arm64 implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8' // 如果你用模拟器调试,才需要x86,真机不用加 // implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8' }这里有个坑要提前说清楚,JCenter已经停止服务很久了,老项目如果仓库源里没有JCenter,需要显式加上:
repositories { maven { url "https://jitpack.io" } mavenCentral() }如果Gradle同步失败,或者你需要自定义FFmpeg模块(比如裁剪协议、控制openssl),就得走源码编译路线,下载IjkPlayer源码+Android NDK自己去编so库。这个过程比较折腾,ButterKnife时代的项目用老NDK版本会有一堆兼容报错,我建议优先用0.8.8的现成库,踩坑成本最低。
2.2 AndroidManifest权限与网络配置
播放网络视频流,最基本的权限就一个:
<uses-permission android:name="android.permission.INTERNET" />如果Demo里有截图、录像之类的功能,再额外申请存储权限。还有一个容易漏掉的地方:Android 9之后默认禁止明文HTTP流量,而RTSP和RTMP虽然不是HTTP,但某些本地资源如果用明文HTTP地址,必须开启明文流量。
<application android:usesCleartextTraffic="true" ... />如果是Android 7及以上的系统,使用FileProvider加载本地视频文件时,Uri配置也得在Manifest里声明,这部分我遇到过一个非常典型的错误:Content Uri拼接错误导致系统播放器找不到文件,后面排查章节会展开。
2.3 工程结构设计
一个能跑的Demo不需要复杂的架构,我习惯建一个PlayActivity承载播放,再写一个IjkPlayerHelper做播放器封装。目录大致这样:
app/src/main/java/com/example/ijkplayerdemo/ ├── MainActivity.java // 主界面,输入地址+播放按钮 ├── PlayActivity.java // 全屏播放页 └── helper/ └── IjkPlayerHelper.java // 播放器初始化、销毁、重连封装MainActivity放一个EditText用来输入RTSP/RTMP地址,一个Spinner切换协议,一个按钮跳转PlayActivity。PlayActivity就是一块SurfaceView/TextureView加一个加载进度条。这里不塞太多功能,保证核心链路简单可靠。
3. 播放器核心实现与关键参数
3.1 初始化播放器与参数配置
IjkMediaPlayer的初始化和系统MediaPlayer不太一样,它多了setOption这个环节,很多播放问题都是从这里调好的。核心配置如下:
public IjkMediaPlayer createPlayer() { IjkMediaPlayer player = new IjkMediaPlayer(); // 硬解码开关,true走MediaCodec,false走FFmpeg软解 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 1); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-error", 1); // 音频输出方案,1为OpenSL ES,延迟更低 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 1); // 渲染格式,RGB565在低端机上更省带宽 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32); // 关键:RTSP传输协议,tcp比udp稳定很多 player.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp"); // 弱网丢帧策略,直播场景建议打开 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1); // 降低缓冲延迟 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0); player.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 10000000); // 硬解失败自动回退软解的风险控制 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-error", 1); return player; }rtsp_transport这个参数尤其重要。默认RTSP走UDP,UDP在局域网内延迟低,但跨网段、有防火墙的场景下丢包严重,画面全是马赛克或者卡在缓冲。强制走TCP之后,虽然延迟会多几十毫秒,但稳定性提升非常明显,我接的十几个项目里,有一半以上是靠这句话救回来的。
3.2 渲染层选择:SurfaceView还是TextureView
IjkPlayer可以直接setSurface给SurfaceView,也可以用TextureView。我的建议是:普通播放用SurfaceView,要做弹幕、缩放动画、双指手势就用TextureView。
SurfaceView的渲染性能更好,内存开销更低,但有一个特性:它不在View树中,而是单独有一个Window,所以做旋转动画时会有“置顶”问题。TextureView可以当作普通View参与动画,但性能略低。Demo里我用了TextureView,因为方便往外面包一层GestureDetector做双击全屏和手势缩放,体验更完整。
player.setSurface(holder.getSurface());用TextureView的话,需要在TextureView的onSurfaceTextureAvailable回调里再把Surface传给播放器,否则会出现“界面黑屏但音频正常”的问题。
3.3 地址拼接规则:海康/大华/公共测试源
写代码之前先搞清楚地址怎么来。海康摄像头的RTSP规范是:
rtsp://用户名:密码@IP:554/Streaming/Channels/101 rtsp://用户名:密码@IP:554/Streaming/Channels/102101代表通道1的主码流,102是通道1的子码流。主码流分辨率高、码率大,适合录像和全屏查看;子码流适合网格预览和弱网环境。
大华的地址格式是:
rtsp://用户名:密码@IP:554/cam/realmonitor?channel=1&subtype=0 rtsp://用户名:密码@IP:554/cam/realmonitor?channel=1&subtype=1subtype=0是主码流,subtype=1是子码流。如果你拿到的摄像头地址带端口号或者认证信息,照原样填进去就好。
没有摄像头时,可以用几个公开测试源来验证流程:
| 协议 | 公开测试地址 |
|---|---|
| RTSP | rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mp4 |
| RTMP | rtmp://ns8.51.ru:1935/live/000 |
| RTMP | rtmp://202.69.69.180:443/webcast/bshdlive-pc |
不过公开源说挂就挂,尤其是RTMP源,可能今天能播明天就停。我遇到这种情况一般直接用mediamtx(原rtsp-simple-server)在本地起一个RTSP服务端,用FFmpeg推一路视频进去,再让IjkPlayer去拉,整个链路完全可控。FFmpeg推流命令是这样:
ffmpeg -re -i test.mp4 -c copy -f rtsp rtsp://192.168.1.100:8554/live/test3.4 生命周期管理与资源释放
播放器这块最容易被初学者忽略的是生命周期。Activity onDestroy时必须释放播放器,否则底层FFmpeg的线程不会自动退出,长时间操作会闪退或者内存泄漏。
@Override protected void onResume() { super.onResume(); if (mPlayer != null && !mPlayer.isPlaying()) { mPlayer.start(); } } @Override protected void onPause() { super.onPause(); if (mPlayer != null) { mPlayer.pause(); } } @Override protected void onDestroy() { super.onDestroy(); if (mPlayer != null) { mPlayer.stop(); mPlayer.release(); mPlayer = null; } }值得注意:release()后不要再调用任何播放器方法,否则会抛IllegalStateException。我习惯在释放前把setDisplay(null)清一下Surface引用,防止Surface销毁顺序问题。
4. 双场景实测:RTSP摄像头流和RTMP直播流
4.1 RTSP拉流实测记录
我拿了一台海康DS-2CD3T系列摄像头做测试,IP为192.168.1.64,主码流地址:
rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101实测结果:
| 指标 | 主码流(101) | 子码流(102) |
|---|---|---|
| 分辨率 | 2560x1440 | 640x360 |
| 打开耗时(TCP) | 约700ms | 约350ms |
| 画面流畅度 | 稳定25fps | 稳定25fps |
| 首帧显示 | 约1s以内 | 500ms左右 |
打开主码流时,体积较大的TCP包在弱网下可能触发缓冲,可以适当把max-fps调低:
player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-fps", 30);如果摄像头有H.265编码的主码流,IjkPlayer 0.8.8自带的FFmpeg默认可能不支持解码,表现就是黑屏或者只有声音。这种场景建议切换到H.264编码的码流,或者自己编译带H.265解码的FFmpeg版本。
4.2 RTMP播放实测记录
用SRS或nginx-rtmp起一个本地RTMP服务端,推流地址rtmp://192.168.1.100:1935/live/test,IjkPlayer播放这个地址,延迟大致在500ms到1s之间,相比RTSP的200ms左右略高。RTMP走的是TCP,所以不存在UDP被防火墙丢弃的问题,弱网下的表现反而比RTSP更稳。IjkPlayer对RTMP内部封装协议处理得比较完整,常见的FLV封装、AAC音频、H.264视频都能直接解。
4.3 延迟表现与调优心得
有的场景对延迟极其敏感,比如远程遥控车、无人机图传,这时可以通过三个参数压低延迟:
player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1); player.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");实测下来,RTSP可以把延迟压到200ms以内,RTMP能压到400ms左右。这组参数只适合实时性要求高的场景,普通直播可以稍微留一点缓冲,否则网络抖动会特别明显。
5. 高频报错与排查手记
5.1 黑屏、无画面的第一排查顺序
遇到黑屏,先别调代码,按这个顺序排查:
- 确认没有在
onSurfaceTextureAvailable之前设置Surface。TextureView的Surface不是立刻可用的,一定要在回调里接管。 - 确认地址是否真的通。手机上装个VLC,输入同一个RTSP地址,如果VLC也放不了,那就是地址或网络问题,不是IjkPlayer的锅。
- 确认硬解是否有兼容问题。把
mediacodec设为0,强制软解,如果画面出来了,说明硬解码器对该分辨率或编码格式支持不好。
提示:软解能放、硬解黑屏时,不要盲目怀疑IjkPlayer,多半是设备厂商的MediaCodec实现有bug。这种情况下,可以保留硬解开关,在应用设置里加一个“硬解/软解切换”项,用户自己选。
5.2 一直缓冲,进度条不动
这是RTSP最常见的连接问题。原因按概率排序:UDP传输被防火墙丢包、摄像头在线路数超限、网关路由MTU问题。
我的第一动作永远是:
player.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp");换成TCP后,90%的缓冲问题能解决。如果还有剩余情况,检查摄像头是不是被其他客户端占用了,海康设备普遍有在线用户数上限,超了就直接拒绝新连接。另外,部分摄像头在工地等复杂网络环境中对MTU很敏感,可以把本地路由器MTU调整到1400左右再试。
5.3 花屏、音画不同步
花屏大概率是网络丢包+播放器来不及重传导致的。RTSP的UDP模式不保证可靠性,丢几个RTP包就会出现花屏。之前提过切TCP,基本能解决。
音画不同步则更多是音频缓冲策略的问题。IjkPlayer的音频默认走OpenSL ES,延迟低,但某些设备上解码格外慢。可以在初始化时打开:
player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 1); player.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "aformat", "s160le"); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "sync-av-start", 0);还有一个细节:部分RTMP源会把音频封装为AAC LATM格式,IjkPlayer可能不识别。如果遇到“播放网络直播流有画面无声音”,优先向服务端要标准的AAC ADTS格式,或者检查推流端编码参数。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 黑屏无声音 | 硬解兼容问题 | 关闭mediacodec,强制软解 |
| 一直缓冲 | RTSP默认UDP被防火墙拦截 | 设置rtsp_transport=tcp |
| 花屏 | 网络丢包 | 切TCP,降低码流档位 |
| 有画面无声音 | 音频编码格式不标准 | 统一为AAC ADTS,检查opensles |
| 切换地址崩溃 | 未release旧player | 先stop再release,再创建新实例 |
| RTSP地址带空格或特殊字符 | URL编码问题 | 使用Uri.encode处理地址 |
6. 从Demo到生产:还需要补齐的能力
6.1 自动重连与播放状态机
Demo能跑只是开始。真实项目中摄像头断网、断电、重启是家常便饭,播放器必须能自己重连。IjkPlayer播放失败会回调onError,此时不要直接重建播放器,而是进入一个重连状态机:先检查网络,再指数退避重连,间隔从1秒逐步增加到10秒,直到成功或用户主动取消。封装的时候记一下当前播放地址,重连后继续用同一个地址就好。
6.2 多路视频与码流切换
监控类App经常要同时看四路、九路画面。IjkPlayer没提供GPU硬解码的零拷贝方案,多路同时解码极其吃CPU,主流做法是:
- 预览画面统一用子码流,全屏时才切主码流
- 播放器实例池化,切流时复用
- 每路最多保留两个播放器实例,超过就关掉最久没看的
码流切换的核心操作是先reset(),再setDataSource,最后prepareAsync,不能直接改URL。
6.3 与流媒体服务端配合的扩展方向
很多人做完Android拉流后发现,产品经理转头就要“网页上也能看”。RTSP协议浏览器原生不支持,这时候就需要后端转发。我常用的方案是用mediamtx或ZLMediaKit这类开源流媒体服务,把RTSP/RTMP源统一转成HLS或WebRTC,再给Web端播放。IjkPlayer的拉流能力没有浪费,它可以直接对接这类服务器的输出地址,反而变成了整个视频链路里的一个高可靠客户端模块。
另外,IjkPlayer也支持播放本地文件、HLS(.m3u8)地址、HTTP-FLV地址,所以从监控到直播到点播,一套Demo的播放内核基本都能覆盖。把这个模块沉淀好,后面接什么业务都不慌。
最后分享一个实操细节:把播放器常用参数(比如rtsp_transport、mediacodec、timeout)放到一个配置类里管理,而不是散落在Activity代码里,调优的时候效率会高很多。我每次接新项目,第一件事就是复制这套播放器封装,然后是改参数、测试、迭代,基本上一个下午就能交付一个可用的播放模块。
本文还有配套的精品资源,点击获取