简介:这是一份面向Android开发者的远程视频监控程序源码包,完整演示了在移动端实现实时监控所需的网络通信、音视频编解码、多媒体数据渲染与后台线程处理等关键技术。压缩包内共68个文件,以java源码、xml布局与配置、class编译产物为主,同时附带apk安装包、PC端exe程序及工程配置文件,便于直接安装调试并对照源码理解Android端与PC端的配合逻辑。已有353人学习下载,适合正在研究Android网络编程、流媒体传输或想快速搭建监控Demo的开发者。通过这份源码可以重点学习Socket/HTTP通信、MediaCodec硬解码、SurfaceView视频展示、权限声明与多线程异步处理等实践细节,也能看到Android项目从源码到构建产物(classes.dex、resources.ap_等)的完整结构,对理解APK打包流程也有帮助。
1. 拿到远程视频监控源码包,先别急着解压
“远程视频监控”这六个字,在 Android 源码里被压缩成一件容易误判的事:很多人以为难点在 Camera 调用,打开项目才发现,真正决定能否“远程”的,是视频编码怎么选、推流走什么协议、信令怎么握手、弱网下怎么保活。这个题目适合两类人:一是做安防、看护、车载类应用的工程师,想绕过从零造轮子的阶段;二是刚接触流媒体方向的 Android 开发者,想找一个能跑通的工程范本,理解 Camera2、MediaCodec、RTSP 这三者是怎么串成一条链路的。需要说明的是,市面上这类 ZIP 源码包质量参差不齐,名字叫“完整版”,打开后缺 native 库、缺 Gradle 配置、甚至缺 Manifest 权限的都常见。这篇文章不会假装你手里那份源码是完美的,而是按一个可落地的远程视频监控工程应该具备的结构,把采集、编码、推流、信令、优化这五段拆开讲,并附上可以直接上手用的关键代码和参数表。
2. 工程结构先行:从 ZIP 解压到 AS 编译通过
拿到一份 Android 远程视频监控源码包,第一件要做的事不是读业务代码,而是先确认工程能不能构建。这个环节卡住的人数远超想象,原因集中在三类:Gradle 版本与 Android Studio 不匹配、NDK 与 so 库架构缺失、Manifest 中权限或 Service 声明不全。下面按步骤来排查。
2.1 先看工程骨架,识别源码包属于哪类项目结构
解压后,先观察根目录。一个标准的 Android 工程至少要具备:
| 目录/文件 | 作用 | 缺失时的表现 |
|---|---|---|
settings.gradle | 声明模块 | Android Studio 无法识别项目结构 |
build.gradle(根级) | 插件版本与依赖仓库 | Gradle Sync 直接报错 |
app/模块 | 主工程代码 | 无app时需检查是否 multi-module |
app/src/main/jniLibs/ | so 动态库目录 | UnsatisfiedLinkError |
AndroidManifest.xml | 权限与组件声明 | 运行即崩溃,提示权限异常 |
如果settings.gradle里有多个模块,例如:app与:library,说明源码包采用了组件化拆分,监控逻辑可能封装在 library 里。此时不要只导入app目录,应该在整个根目录下执行 Open 操作。
2.2 Gradle 与 SDK 版本对齐
常见做法是先用 Android Studio 打开根目录,等 Gradle Sync 完成,再根据报错逐项调整。远程监控工程通常依赖 Camera2 API,对minSdkVersion的要求至少是 21,若要使用 CameraX 扩展,则需要 21 以上并启用了 Java 8 支持。
我一般会把app/build.gradle中的关键部分改成这样:
android { compileSdkVersion 33 defaultConfig { applicationId "com.example.remote.monitor" minSdkVersion 21 targetSdkVersion 33 versionCode 1 versionName "1.0" // 使用 C++ 时需配置 NDK 版本,建议统一到 21.4.7075529 ndk { abiFilters "armeabi-v7a", "arm64-v8a" } } }这里的关键参数是abiFilters。远程视频监控的 so 库(如 FFmpeg、x264 或内部封装好的编码器)在不同 CPU 架构下不可混用,如果源码包只提供arm64-v8a的 so,而测试机是 32 位模拟器,就必须在abiFilters里去掉armeabi-v7a,否则安装后加载 so 库直接崩溃。
2.3 Manifest 中容易被源码包裁剪掉的声明
远程监控 App 至少需要以下权限:
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.WAKE_LOCK" />WAKE_LOCK是这类源码中最容易被遗漏的一项。视频采集与编码需要 CPU 持续工作,屏幕熄灭后如果没有 WakeLock,系统会进入休眠,导致后置摄像头回调停止、编码器断流。拿到源码后应先在 Manifest 里检查这几项权限,再考虑业务逻辑。
2.4 编译期最常碰到的三个错误处理
Gradle Sync 时报Could not find com.android.tools.build:gradle:x.x.x,说明插件版本不存在或仓库未配置,可在根级build.gradle中加上google()与mavenCentral()仓库。
编译时报Invoke-customs are only supported starting with Android 0,说明app/build.gradle中缺少compileOptions配置,补上 Java 8 支持即可:
compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 }安装后启动崩溃并提示java.lang.UnsatisfiedLinkError,优先检查jniLibs目录下是否存在libavcodec.so等文件,且文件名是否与System.loadLibrary()中的名称对应。
提示:拿到源码后先备份一份原始 ZIP,避免修改 Gradle 文件后无法回退到初始状态。
3. Camera2 采集与 MediaCodec 硬编码:把摄像头数据变成可传输的 H.264 流
远程监控的核心链路是:摄像头帧 → 编码 → 网络传输。Android 上 95% 的源码工程走的都是同一套路:Camera2 负责取帧,MediaCodec 负责硬编码输出 H.264,RTP/RTSP 负责封装传输。这里最容易翻车的不是 API 调用,而是 Buffer 格式、色彩空间和时间戳的处理。
3.1 Camera2 的输出目标:ImageReader 还是 Surface?
Camera2 的采集链路有两种选择:
- 直接把
MediaCodec支持的Surface作为相机输出目标,帧数据从 Camera 到编码器零拷贝。 - 先用
ImageReader拿到Image,再通过getInputBuffer()喂给编码器,多一次拷贝,但便于做图像处理(比如画框、加水印)。
远程监控场景下,我倾向于推荐第二种思路,原因是源码工程往往需要在视频上叠加时间戳或设备编号,Surface直连模式虽然性能好,却无法在编码前修改画面。如果源码包默认采用ImageReader,需要确认ImageFormat是YUV_420_888,这是 MediaCodec 输入侧兼容性最好的格式。
ImageReader imageReader = ImageReader.newInstance( width, height, ImageFormat.YUV_420_888, 4 ); imageReader.setOnImageAvailableListener(reader -> { Image image = reader.acquireLatestImage(); if (image == null) { return; } // 将 YUV_420_888 转成 byte[],喂给 MediaCodec byte[] data = yuvImageToByteArray(image); feedEncoder(data); image.close(); }, backgroundHandler);这段代码里ImageReader.newInstance的第四个参数表示最大图片数量,建议不要小于 3。如果设为 1,相机回调频繁时会出现丢帧,表现为视频卡顿但 CPU 占用不高。acquireLatestImage与acquireNextImage的区别在于前者会跳过积压的旧帧,监控场景下应优先考虑前者,以此保证推流端拿到的始终是最新画面。
3.2 MediaCodec 编码器的参数设置,照着这张表调
编码器配置是整个工程中与“画面质量”和“延迟”关系最直接的部分:
| 参数 | 推荐值 | 说明 |
|---|---|---|
KEY_COLOR_FORMAT | COLOR_FormatYUV420Flexible | 兼容性最好,避免部分设备不支持COLOR_FormatYUV420SemiPlanar |
KEY_BIT_RATE | 1~4 Mbps | 720p 建议 2 Mbps,1080p 建议 4 Mbps,根据上行带宽调节 |
KEY_FRAME_RATE | 15~30 | 监控场景 15fps 可减少编码压力,动态画面多时拉到 30 |
KEY_I_FRAME_INTERVAL | 1~3 | 每隔多少秒插入一个关键帧,值越小花屏恢复越快,但码率占用越高 |
KEY_PROFILE | AVCProfileBaseline | Baseline 兼容性最强,Web 端播放不易出现黑屏 |
关键帧间隔是远程监控最需要关心的参数。播放端如果出现花屏,通常是因为 I 帧间隔过长,弱网下 P 帧丢失后就无法恢复画面。把KEY_I_FRAME_INTERVAL设为 1 或 2,虽然每秒多占用一部分码率,但换来回放端快速出图,这笔账是划算的。
编码器的配置代码一般长这样:
MediaFormat format = MediaFormat.createVideoFormat( MediaFormat.MIMETYPE_VIDEO_AVC, width, height ); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible); format.setInteger(MediaFormat.KEY_BIT_RATE, 2_000_000); format.setInteger(MediaFormat.KEY_FRAME_RATE, 15); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR); codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);BITRATE_MODE_VBR适合网络波动场景,画面复杂时自动提升码率,静止时降低。但如果你的推流协议是 RTSP 且传输层是 TCP,建议改成BITRATE_MODE_CBR,恒定码率对带宽预测更友好,避免出现瞬间数据量过大导致 socket 写阻塞。
3.3 时间戳:远程监控画面跳动的隐形元凶
编码器输出的 buffer 必须携带正确的presentationTimeUs。这个时间戳是解码端决定画面何时显示的依据,如果使用系统System.currentTimeMillis(),会因为挂起和唤醒产生跳动,正确做法是使用System.nanoTime()换算成微秒:
long presentationTimeUs = System.nanoTime() / 1000;从dequeueOutputBuffer拿到的BufferInfo中,flags包含BUFFER_FLAG_CODEC_CONFIG时需要单独存储这份 SPS/PPS 数据,它用于推流时的sps-pps参数拼接或 RTSP 的 AVCDecoderConfigurationRecord。很多源码包把这一步省略了,结果播放端显示黑屏,日志中只有解码失败。
4. RTSP 推流与信令通道:让画面真正实现“远程”
编码器产出的 H.264 裸流只是半成品,要让它能从手机端传到远端的播放器上,还需要完成传输协议封装和信令交互。远程视频监控最常用的传输协议是 RTSP,市面上开源播放器 VLC、FFplay 都内置 RTSP 客户端,测试方便,也符合“拿源码改一改就能用”的预期。
4.1 自建 RTSP 服务端还是推流到媒体服务器?
这里有一个经常被忽略的架构选择:
| 方案 | 适用场景 | 缺点 |
|---|---|---|
| 手机内置轻量 RTSP Server | 一对一监控,手机直接充当流媒体服务端 | 耗电高,App 被杀后流中断 |
| 手机作为客户端推流到 NVR 或媒体服务器 | 多设备统一管理,服务端持久化 | 需要额外部署服务端,如 SRS、ZLMediaKit |
源码包里如果自带了一个RtspServer类,说明采用方案一;如果只有推流器(推流端),则需要自己准备服务端。我建议后者,因为远程监控通常意味着设备端 IP 不固定,NAT 环境下手机无法被外部直接访问,主动推流到服务器更可靠。
以 SRS 为例,服务端启动后默认监听1935端口用于 RTMP,如果走 RTSP 需要开启1935与554端口。一条最小可用的推流链路命令如下:
# 服务端启动 SRS 并开启 RTSP 转发 ./objs/srs -c conf/rtsp.conf # 测试拉流,FFplay 直接从远端读取 ffplay -rtsp_transport tcp rtsp://your-server-ip:554/live/camera1这个命令能跑通的前提是推流端把流推到rtsp://your-server-ip:554/live/camera1这个路径。Android 端可用 MediaCodec 输出 H.264 后,自行实现 RTP 打包并发送到该地址。如果需要快速验证编码流是否合法,也可以用adb shell下的命令行工具来抓取裸流文件再做本地解码:
adb exec-out screencap -p > /tmp/screen.png # 或者抓取编码后的 H264: adb pull /sdcard/test.h264 . ffprobe test.h264这段命令里的ffprobe可以快速检查h264文件是否包含可解的 sps/pps,如果你在代码里没有发送 SPS/PPS,ffprobe会直接提示Could not find codec parameters,这是远程监控源码包里最常见的联调错误。
4.2 RTP 打包与 H.264 分片
H.264 的 NAL 单元可能大于 MTU(通常 1500 字节),RTP 打包时需要将大 NAL 分片,使用 FU-A 分片方式;小于 MTU 时采用 Single NAL Unit 方式。代码实现了这些逻辑。伪代码结构如下:
private void sendH264Nal(byte[] nal, int rtpPort, String host) { if (nal.length <= MAX_PACKET_SIZE) { // 单包发送 sendRtpPacket(nal, /* marker */ true); } else { // FU-A 分片,分包发送 int start = 0; while (start < nal.length) { int len = Math.min(nal.length - start, MAX_PACKET_SIZE - 2); // 组装 FU indicator + FU header + 数据 sendRtpPacket(fuPacket, /* marker */ start + len >= nal.length); start += len; } } }分片逻辑中,FU indicator的第一个字节由原始 NAL header 的高 5 位组成,FU header的低 5 位由分片序号组成。如果这里没处理好,播放端会出现马赛克或丢帧花屏。注意:RTSP 拉流时,除 RTP 包外还需发送 RTCP SR 包,用于时间戳同步,否则 VLC 的播放进度会一直跳动。
4.3 信令通道:控制命令与视频流分离
远程视频监控除了画面,还需要远程操作的能力,比如旋转摄像头、抓拍、开关夜视。常见做法是建立一条独立的 WebSocket 或 TCP 长连接,信令只传 JSON,不传视频数据。
{"cmd": "capture", "deviceId": "camera_001", "ts": 1699999999} {"cmd": "ptz", "direction": "left", "speed": 30}信令与视频流分离的好处是:网络拥塞时视频可以降帧率或丢帧,但控制指令仍能及时到达,不至于连停止转动这种基础操作都做不了。源码包中如果只在MainActivity里处理了SurfaceView的显示却没有独立信令 Service,建议自行补充一个MonitorCommandService,该 Service 只维护一个 Socket 连接并在后台保活。
5. 流畅度与弱网适配:远程监控的体验瓶颈在传输侧
当视频已经开始远程传输,问题就从“怎么推到服务器”转移到“网络波动下还能不能保持流畅”。大量源码包在这一层是裸奔的,它们只是死循环里不断send(),网络一抖就出现卡顿、花屏乃至 TCP 缓冲区堆积。这章讨论的是如何把一条“能跑”的推流链路,改成一条“在弱网下不那么容易崩”的推流链路。
5.1 帧率与码率的动态调节策略
弱网表现不理想时,第一反应是降低码率。MediaCodec 在编码过程中可以通过Bundle参数动态调整目标码率:
Bundle bitrate = new Bundle(); bitrate.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, 800_000); codec.setParameters(bitrate);这个接口没有返回值,部分设备上并不生效,实际测试时需要观察一秒钟实际编码输出的字节数,计算真实码率。若设备不支持动态调码,就要在采集侧做文章——降低帧率或分辨率。
有一个参数表格值得参考:
| 网络条件(RTT/丢包率) | 目标分辨率 | 帧率 | 码率 |
|---|---|---|---|
| 延迟 < 30ms,丢包 0% | 1080p | 30 | 4 Mbps |
| 延迟 30~100ms,丢包 1% | 720p | 20 | 2 Mbps |
| 延迟 > 100ms,丢包 3%+ | 480p | 12 | 800 Kbps |
监控场景中,我通常不会把这种调整放在应用层写死,而是让信令服务端根据 TCP 丢包率推送新的编码配置。这么做避免客户端频繁重配 MediaCodec 带来的编码秒级黑屏。
5.2 关键时刻的缓冲策略
播放端的缓冲策略也会影响观感。作为推流端,要关注的是发送缓冲区。TCP 默认写缓冲在大延迟下容易堆积,导致“新帧发不出去,旧帧卡在路上”。一个有效的做法是,发送线程每次写数据前检查当前socket.sendQueueSize,超限时丢弃非关键帧:
if (sendQueueSize > MAX_BUFFER_SIZE) { // 当前帧为 P 帧,直接丢弃,保留 I 帧 continue; }这种“丢帧不丢关键帧”的策略比降低码率更直接,能保证画面即使掉帧也不至于长时间花屏。
5.3 前后台切换与生命周期处理
远程监控源码如果没有处理好 App 退到后台的场景,很容易出现“屏幕关了,推流也断了”。监控类的 App 必须使用前台 Service,且通过startForeground()传一个常驻通知,才能在 Android 8.0 以上保证后台存活。
Notification notification = new Notification.Builder(this, CHANNEL_ID) .setContentTitle("远程监控运行中") .setContentText("正在推送视频流") .setSmallIcon(R.drawable.ic_monitor) .build(); startForeground(1, notification);在低端机上,Camera2 和 MediaCodec 同时运行时 CPU 占用率很高,如果整机温度上升,部分手机会自动降频,表现为从 30fps 跌到 20fps 以下。要排查这类问题,可以边推流边执行:
adb shell dumpsys cpuinfo | grep monitor观察monitor相关进程的 CPU 占比和线程优先级,若发现编码线程被抢占,可在代码中设置:
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);THREAD_PRIORITY_URGENT_AUDIO是 Android 中可用的较高调度优先级,虽然听起来像音频相关的,但在 Android 的调度器里,持续的视频推流线程也可以借用这一档优先级来与低优先级线程竞争 CPU。
5.4 编码结束时如何避免 B 帧问题
远程视频监控对实时性要求高,通常需要设置KEY_LATENCY或采用AVCProfileBaseline来避免 B 帧。B 帧会引入多帧延迟,对低延迟监控不利。在部分 MediaCodec 实现里,即使没有显式设置,也可能默认开启 B 帧,表现为端到端延迟达数百毫秒。可以在配置后通过codec.getCodecInfo().getCapabilitiesForType(mediaType).videoCapabilities查看编码器是否支持 B 帧,再决定是否调整 profile。
6. 把源码跑成一款可交付的监控应用:验证清单与设备适配技巧
源码能编译通过、能推流,这只是起跑线。在实际环境里,不同 SoC 和 Android 版本对 Camera2 行为的差异会直接暴露出来。这里给出一份可以从工程化层面对齐的内部验证模板和应对方案。
6.1 用 adb 命令压测摄像头与编码器的温度上限
远程监控设备经常挂在户外或固定位置,长时间运行后是否有稳定性风险,需要先跑一轮压力验证:
# 循环播放并对焦切换到前置/后置摄像头,持续运行 30 分钟 adb shell am start -n com.example.monitor/.MainActivity adb shell settings put system screen_off_timeout 1800000 # 同时开启 media.codec 的调试日志 adb shell setprop debug.media.codec 2 adb logcat -s MediaCodec:V这套命令的目的是验证长时间编码后是否出现MediaCodec.CodecException: Error 0x80000001,这类错误在部分海思、联发科平台上非常常见,出现后通常需要重启编码器,此时需要实现分层重试:先stop(),再start(),不用释放实例;还不行才执行release()并重新创建。如果源码包里并没有多设备适配分支,建议主动加一层CodecRetryManager。
6.2 验证推流的 5 个自查点
在交付前,按照以下顺序检查,能淘汰 80% 的隐藏问题:
- 用
ffprobe -v trace查看拉流地址的 SPS/PPS 是否在第一帧就到达。 - 将手机切换为飞行模式 5 秒再恢复,确认推流端能在 3 秒内恢复出图。
- 用另一台设备从 4G 网络拉流,观察卡顿收敛时间,标准是连续 5 秒无花屏。
adb shell top -n 1检查 App 是否占用超过 60% CPU,超出时确认分辨率与帧率档位是否合理。- 检查唤醒锁是否在锁屏后依然持锁,可通过以下命令确认:
adb shell dumpsys power | grep "PARTIAL_WAKE_LOCK"若发现没有持锁,看护场景下屏幕一关推流就中止;如果持锁时间过长,则需要注意温度控制与耗电之间的取舍,比如让用户在设置页里选择“连续模式”和“节能模式”。
6.3 一个被低估的功能:码流加密与鉴权
很多源码包的 RTSP 地址是明文公开的,任何知道 IP 的设备都能拉流。远程监控场景下,即使不做完整加密,至少要在信令通道加一个 token 鉴权。常见做法是:信令服务端鉴权成功后下发一个动态随机端口,推流端只把视频流推到这个临时端口,拉流端必须先通过信令获取端口,才能访问视频数据。这套机制能挡住大部分通过端口扫描直接拉流的场景,而且实现成本很低,只需在建立 Socket 连接时多传一个token字段:
{"cmd": "auth", "token": "8f2a9c3d", "timestamp": 1699999999}若源码包里已经有一个RtspServer但没有任何鉴权,优先补充以上逻辑;整个改造不需要动编码器,核心思路就是把“流媒体服务”和“控制鉴权”彻底拆开。这部分完成后,整个远程视频监控工程才真正达到可以部署的状态,而能被记住的监控产品,恰恰是从这种“不起眼的细节”开始积累信心的。
本文还有配套的精品资源,点击获取