Android RTSP播放器Demo:基于ijkplayer的硬解软解切换与断线重连实践
简介面向Android开发者的RTSP流媒体播放示例工程演示了基于MediaPlayer实现RTSP视频源拉取与播放的核心流程。工程结构完整包含Java源码、AndroidManifest配置文件、drawable系列UI资源、armeabi下的so库以及可直接安装的APK整体7z压缩包4.73MB共55个文件。已有727人学习下载适合正在集成视频播放功能或学习RTSP协议的移动端开发者。通过修改setDataSource中的RTSP地址即可切换视频源代码覆盖prepareAsync异步准备、OnPreparedListener启动播放、OnErrorListener异常处理以及播放控制与release释放资源等关键环节。预览中还可见android-support-v4.jar与classes.dex便于还原真实构建环境。这份DEMO可作为自定义播放器的基础模板也能帮助理解Android多媒体框架与RTSP命令交互的实际应用。1. 一个能跑的 Android RTSP 播放 Demo从「VLC 能播」到「App 能播」的最后一公里做安防类 App 的开发者应该都经历过这种场景某个摄像头型号在 VLC 里拉流秒开一到 Android 工程里就成了黑屏转圈。RTSP 协议本身不复杂复杂的是 Android 端解码器、渲染器和网络栈三层叠在一起后的玄学表现。这个 Android RTSP 播放 DEMO 要解决的就是这件事——用 ijkplayer 封装一条从「输入 RTSP 地址」到「画面出现在 SurfaceView 上」的完整播放链路附带硬解/软解切换、断线重连和常见坑的规避方案。适合在做设备监控、行车记录仪图传、无人机地面站或者任何需要内嵌摄像头画面的从业者作为起点工程去改新手能直接跑通熟手可以把它当成一个参数样板来调。2. 播放器选型ijkplayer、ExoPlayer 与系统 MediaPlayer 的三选一2.1 为什么从业者默认选 ijkplayer 而不是另外两个Android 上能播 RTSP 的无非三条路系统自带的 MediaPlayer、Google 官方 ExoPlayer、开源社区维护的 ijkplayer。MediaPlayer 对 RTSP 的支持一直处于「能播但不可控」的状态RTSP over TCP、H.265 硬解、自定义缓冲策略这些需求基本没法满足适合拿来验证摄像头地址对不对不适合做产品。ExoPlayer 在 HLS 和 DASH 上很强但对 RTSP 的 Authentication带用户名密码的地址支持一直不算完整有些摄像头厂商的 RTSP 握手细节会翻车。ijkplayer 的本质是把 FFmpeg 的 demuxer 和 decoder 通过 JNI 暴露给 Java 层RTSP 只是它支持的众多协议之一这意味着你能拿到从打开流、解包、解码到渲染的全链路控制权。这个 Demo 选的也是 ijkplayer 这条路因为它够底层、够开放遇到问题至少能把日志挖出来。选型时还要考虑一个实际因素你的摄像头是否支持 H.265。MediaPlayer 在部分 Android 版本上对 H.265 硬解的支持不稳定ExoPlayer 依赖设备的 MediaCodec 能力ijkplayer 则同时保留了 MediaCodec 硬解和 FFmpeg 软件解码两条路径。对于安防设备H.265 几乎是标配只支持 H.264 的播放器方案在老设备上会遇到兼容性墙。ijkplayer 刚好能用软解兜底这是它成为默认选项的另一个原因。2.2 RTSP 地址格式与三者的支持边界对比RTSP 地址不是随便填的常见的摄像头厂商标配格式是rtsp://username:password192.168.1.64:554/Streaming/Channels/101其中username:password是鉴权信息554是默认端口Streaming/Channels/101是海康风格的路径规则101表示主码流102表示子码流。不同厂商路径规则差异很大大华为风格是/h264/ch1/main/av_stream其他厂商又有自己的玩法。这个 Demo 里地址是直接通过 EditText 输入的没有做任何协议预处理所以拿到新摄像头后第一件事就是确认路径规则。三者的支持边界我整理了一张表能力项系统 MediaPlayerExoPlayerijkplayerRTSP over UDP支持但不稳定支持支持可切换 TCP/UDPRTSP 带鉴权部分版本有问题有问题正常H.265 硬解依赖版本和厂商依赖设备 MediaCodec硬解失败可自动退软解自定义缓冲区不支持支持有限支持丢包、缓冲阈值、超时断线重连自己写自己写自己写但可控性强3. 把 Demo 跑起来Gradle 配置、权限声明与 IJKPlayer 初始化3.1 拿到工程后的目录与关键文件定位这个 Demo 是一个标准的 Android Studio 工程主模块结构比较直观一个MainActivity负责 UI 交互一个PlayerSurfaceView负责渲染承载一个RtspPlayerHelper把 ijkplayer 的初始化、播放、销毁做成了可复用的工具类。建议先打开build.gradle确认依赖版本再打开MainActivity走一遍播放启动链路最后再看RtspPlayerHelper的各方法实现。整体来说没有复杂的架构设计是典型的单模块 Demo。工程里值得注意的文件就三个build.gradle依赖和打包配置、AndroidManifest.xml权限和组件声明、RtspPlayerHelper.java核心播放控制逻辑。拿到手先不要急着改代码把这个顺序走完基本上就能把这个播放链路吃透。3.2 依赖引入和权限配置这两步不能错在build.gradle中需要引入 ijkplayer 的核心库dependencies { implementation tv.danmaku.ijk.media:ijkplayer-java:0.8.8 implementation tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8 implementation tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8 }依赖版本0.8.8是这个项目里常用的稳定版本。只加armv7a和arm64两个 ABI 是因为现在市面上的 Android 设备基本都是 64 位架构如果还需要兼容老旧的x86模拟器再加ijkplayer-x86即可。加完依赖后同步一次 Gradle如果遇到下载超时多半是网络问题重试几次通常能解决。接着配置AndroidManifest.xml的权限这部分容易漏uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE /其中INTERNET权限用于拉流ACCESS_NETWORK_STATE和ACCESS_WIFI_STATE用于网络状态监听这是断线重连逻辑里判断网络恢复的基础。很多开发者只加了INTERNET结果摄像头地址在局域网内可以播一旦走进断线重连流程就发现逻辑不触发原因就是缺少网络状态权限。3.3 播放器初始化的核心代码拆解RtspPlayerHelper里的初始化逻辑是这个 Demo 的核心代码如下public void initPlayer(SurfaceView surfaceView) { IjkMediaPlayer ijkPlayer new IjkMediaPlayer(); // 设置允许使用 MediaCodec 硬解 ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec, 1); ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec-auto-rotate, 1); ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec-handle-error, 1); // FFmpeg 层面的超时设置单位微秒。5 秒内连不上就报错 ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, rtsp_transport, tcp); ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, timeout, 5000000); // 设置渲染 Surface ijkPlayer.setSurface(surfaceView.getHolder().getSurface()); // 设置数据源并开始异步准备 ijkPlayer.setDataSource(rtspUrl); ijkPlayer.prepareAsync(); ijkPlayer.setOnPreparedListener(player - player.start()); ijkPlayer.setOnErrorListener((player, what, extra) - { handlePlayError(what, extra); return true; }); this.ijkPlayer ijkPlayer; }逻辑上分四层第一层设置解码方式mediacodec系列选项让设备优先用硬解码当硬解码不被支持或发生错误时ijkplayer 会自动回退到 FFmpeg 软解这个机制比手动切换稳健得多第二层设置网络传输方式rtsp_transporttcp会让 RTSP 走 TCP 通道避免在 WiFi 环境下 UDP 丢包导致花屏代价是延迟稍高一点第三层设置超时时间timeout5000000表示 5 秒如果摄像头地址不可达播放器会在这个时间后触发错误回调而不是无限等待第四层是 Surface 绑定和异步 prepare注意这里用的是prepareAsync而不是prepare避免阻塞主线程。参数里最值得调的是rtsp_transport如果你的应用场景是局域网内固定点位的摄像头TCP 延迟可以接受如果是无人机图传这类对延迟敏感的移动端场景改成udp会让延迟更低但要接受丢包花屏的风险。另外timeout的单位是微秒不是毫秒有人把它写成5000结果 5 毫秒就超时了这是新手容易踩的坑。4. 播放体验调优软硬解码切换、缓冲策略与断线重连4.1 硬解码与软解码什么时候该放弃硬解ijkplayer 的硬解码切换并不是在 UI 上加一个开关那么简单。虽然mediacodec1开启了硬解优先但实际会遇到一些设备的 MediaCodec 实现不完整表现为画面有声音没图像、偶尔绿屏或者播放一段时间后直接 crash。这种时候需要手动把硬解关掉让 FFmpeg 软解接管public void switchToSoftwareDecode() { if (ijkPlayer ! null) { // 先释放当前播放器再重建硬解参数不能播放中途改 releasePlayer(); IjkMediaPlayer player new IjkMediaPlayer(); // 硬解相关选项全部关闭 player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec, 0); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec-auto-rotate, 0); player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec-handle-error, 0); // 低分辨率情况下软解消耗不高但 1080P 主码流会比较吃力 player.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, skip_loop_filter, 16); // 其余初始化流程保持一致 player.setDataSource(rtspUrl); player.prepareAsync(); this.ijkPlayer player; } }一个关键细节是硬解参数不能播放中途直接改必须先释放播放器再重建否则设置不会生效。skip_loop_filter16是 FFmpeg 的 loop filter 跳过策略值16表示跳过非关键帧的环路滤波能显著降低软解 CPU 占用代价是画面上可能出现轻微的块状噪点。如果摄像头是 720P 及以下软解体验尚可如果是 1080P 主码流建议还是优先排查硬解花屏的根因而不是指望软解扛住。4.2 缓冲策略参数让起播更快、卡顿更少ijkplayer 的缓冲行为可以通过start-on-prepared和min-frames等选项控制这在监控类场景里尤其重要。默认情况下播放器会缓冲一定帧数才起播这在网络良好时没问题但 WiFi 环境不稳定时会导致明显的画面延迟放大。以下是调试过的缓冲参数组合// 起播不等待额外缓冲prepare 完成立刻显示首帧 ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, start-on-prepared, 1); // 最小缓冲帧数值越小起播越快但抗抖动能力越弱 ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, min-frames, 2); // 关键禁止播放器自动丢帧追赶否则画面会加速播放 ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, max-fps, 30);min-frames2意味着播放器只缓冲 2 帧就开始渲染大部分摄像头的 RTSP 码流帧间隔在 33ms 到 100ms 之间所以实际起播延迟在 100ms 级别。max-fps30限制了播放器不会因为解码速度超过显示速度而自动丢帧简单说就是防止画面出现「快进」现象。这三个参数组合下来在局域网内拉流起播耗时基本在 0.5 秒左右观感上接近「秒开」。4.3 断线重连最小可靠实现与参数设计RTSP 播放的断线场景基本就三种摄像头主动断开、网络抖动导致 socket 超时、摄像头重启导致会话失效。重连逻辑的核心是先释放旧播放器再重新走一遍初始化流程但要加一个退避机制防止死循环。以下是一个可行的实现private void reconnect() { if (isReconnecting) return; isReconnecting true; // 指数退避第1次2秒第2次4秒最多32秒 int delaySeconds Math.min(2 reconnectCount, 32); reconnectCount; new Handler(Looper.getMainLooper()).postDelayed(() - { releasePlayer(); // 彻底释放旧播放器 initPlayer(surfaceView); // 重新初始化 isReconnecting false; }, delaySeconds * 1000L); }指数退避的意图是瞬时网络抖动只需 2 秒后重试摄像头断电重启需要几十秒不应该高频空转所以退避到 32 秒封顶。注意releasePlayer()是一个必须做彻底的方法需要把 IjkMediaPlayer 的stop()、release()都调用一遍并且置空引用否则旧会话的 socket 没有释放下次重连会报 address already in use 之类的错误。一个稳妥的习惯是重连前先打印当前播放器状态和错误码也方便定位是网络层问题还是解码层问题。5. 避坑指南RTSP 播放里最容易翻车的五个场景与排查记录5.1 有声音没画面现象摄像头地址能开播Logcat 里能听到音频解码日志但 SurfaceView 上一直黑屏或白屏。原因绝大多数情况是渲染 Surface 没有成功绑定到播放器上setSurface()传入了 SurfaceView 尚未创建完成的 Surface或者 SurfaceView 的holder类型不是SURFACE_TYPE_PUSH_BUFFERS。解决在surfaceCreated回调之后再调用setSurface不要尝试提前绑定同时确认布局里 SurfaceView 没有和其他 View 叠放导致被覆盖。另一个容易被忽略的原因是摄像头只输出音频流没有视频轨道这种就需要提供正确的子码流地址。5.2 硬解码花屏或绿屏现象同一个 RTSP 地址在 VLC 里正常在 Demo 里画面每隔几秒出现绿色色块或整屏撕裂。原因摄像头的 H.265 编码参数和设备 MediaCodec 硬解的兼容性存在问题比如参考帧数量超出硬解器能力范围。解决先用adb shell dumpsys media.codec查看设备支持的硬解能力再对照摄像头的编码设置把摄像头侧的 H.265 编码参数改为基础档Baseline/High 的差异会影响如果摄像头侧没法改那就只能走软解兜底。这里可以记住一个判断方法硬解花屏时切到软解如果正常锁定是硬解器兼容性问题。5.3 拉流卡顿但同一个地址在 VLC 里秒开现象VLC 播放流畅ijkplayer 播放卡顿画面有延迟但没有明显丢帧。原因路由器或 WiFi 环境下 UDP 丢包率较高而默认的rtsp_transportudp会把丢包表现为花屏和卡顿VLC 内部重传机制掩盖了这个问题。解决显式设置rtsp_transporttcp让 RTSP 走 TCP 通道用播放延迟换网络稳定性。对于帧率较高的运动画面球机转动时把max-fps从 30 提高到 60很多时候卡顿只是播放器主动丢帧策略导致的。如果 TCP 模式仍然卡就要怀疑是设备端编码码率超出解码能力这时候应该看的是 CPU 占用率。5.4 播放一段时间后自动崩溃现象播放 10 到 30 分钟后进程直接 crash且没有 Java 层异常堆栈。原因这种大多数是 native 层的解码内存泄漏集中在老的 ijkplayer 版本release()时没有正确清理 MediaCodec buffer长时间运行后内存持续增长。解决把依赖版本从0.8.8之前的版本切换到稳定版另外确保 Activity 的onDestroy中执行完整的stop和release流程不要在onPause里只调pause不释放。这不是一个 Demo 层能百分百解决的问题但新版本修复了大部分已知 native 泄漏点。5.5 RTSP 地址里带密码且密码含特殊字符现象摄像头地址带了用户名密码鉴权方式切换到 Basic 模式后播放报Unauthorized错误。原因RTSP 的 Basic 鉴权是Base64(username:password)如果密码里含、:、/这类字符未转义直接拼接进 URL协议栈解析时会切错 host 或 path。解决写一个工具方法把用户名和密码单独做URLEncoder编码再拼地址更省事的方案是改用 ijkplayer 的setOption单独设置鉴权信息不放进 URLijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, rtsp_transport, tcp); // 将用户名密码通过 option 传递避免 URL 转义问题 ijkPlayer.setDataSource(rtspHostAndPath); ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, rtsp_flags, basic_auth);这个坑比较隐蔽现象是播放前报错但查代码时 URL 看起来没问题实际上问题出在 URL 拼接时符号让协议栈把密码后半段当成了 host 字段报错时 Logcat 里看到的 host 是错的注意看完整报错就能发现。6. 封装成可复用的 RTSP 播放组件事件回调、生命周期绑定与扩展思路Demo 里RtspPlayerHelper已经完成了播放控制基本封装做到能跑没有问题。但如果要考虑接入多个页面比如套壳在一个设备详情页、一个预览列表页那最好把播放器的状态回调做成接口事件并把 Activity 生命周期绑定到播放器上。一个可行的抽象是定义回调事件集public interface RtspEventListener { void onPlayStarted(); void onPlayError(int what, int extra); void onPlayCompleted(); void onReconnecting(int retryCount, int delaySeconds); }事件回调能解决的是一个黑匣子问题播放器内部发生了什么上层的 UI 完全不知道。通过onPlayError把错误码抛出去UI 才能决定「弹出重试按钮」还是「提示摄像头不在线」。如果不做这一层播放失败就只能瞎猜。生命周期绑定的思路也很简单Demo 当前的播放器持有SurfaceView引用在onPause时需要调用ijkPlayer.pause()onResume时需要ijkPlayer.start()onDestroy时必须 release否则会继续占用摄像头连接数。有些摄像头设备只允许单一客户端建立 RTSP 会话上一个连接没释放新连接会被主动拒绝。我之前的做法是在 Activity 基类里声明一个抽象方法getRtspSurfaceView()然后在基类的onDestroy里统一释放这样每个页面就不用重复写释放逻辑也防住了「页面销毁但播放器还活着」这种崩溃来源。组件化的另一个收益是方便后续扩展清晰度切换。摄像头主码流和子码流的 RTSP 地址路径不同只能通过切换地址来换清晰度而清晰度切换和断线重连是同一条路径只是重连前把rtspUrl换掉再调reconnect()而已。把这个逻辑封装进组件上层只需调用switchToChannel(channelIndex)不用关心底层释放和重建的细节。从那以后我每次接一个新摄像头做 RTSP Demo都会先确认三件事摄像头的 RTSP 端口默认是不是 554、子码流的 URL 路径规则长什么样、编码格式是 H.264 还是 H.265然后把这三条写在一个临时文件里初始化播放器时再逐一核对。这样做之后排查问题的时间至少砍掉一半。这份 Demo 的完整工程可以直接下载按第 3 章的步骤配置好依赖和权限把摄像头地址填进输入框就能跑起来希望帮到你。本文还有配套的精品资源点击获取