news 2026/9/16 1:26:24

老旧Android工程解析:Camera采集+Socket推流的远程监控

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
老旧Android工程解析:Camera采集+Socket推流的远程监控

简介:安卓Android源码——基于手机的远程视频监控系统,是一份面向Android初中级开发者的完整工程,演示如何利用手机摄像头实现远程画面的实时采集与查看,可用于毕业设计、课程项目或移动监控方向的技术练手。压缩包共41个文件,以5个Java源文件(核心类ImageServer.java、CameraTest等)和14个class编译文件为核心,另含6个xml配置与布局、8张png图片资源、可直接安装的apk包及项目配置文件,整体仅168KB,轻量易阅。工程目录涵盖src源码、res资源、gen自动生成文件及bin编译输出等标准Android结构,便于按模块研读。已有289人学习。代码覆盖Android四大组件、Camera API调用、Socket/HTTP网络通信、后台Service保活、多线程图像压缩与传输、权限声明和UI布局等关键点,并附有AndroidManifest.xml与工程配置,便于对照源码理解视频流采集、压缩、传输到显示的完整链路,可在此基础上快速二次开发。

1. 老旧Android工程里挖出的远程监控:ImageServer与CameraTest不是摆设

一个zip压缩包,里面躺着一个Eclipse时代的老旧Android工程。第一眼会误以为它只是个普通的相机demo,实际上,这台手机被设计成同时承担摄像头采集和Socket服务端的监控设备。用户在同一局域网内用电脑或另一台手机连接手机显示的IP和端口,就能实时看到摄像头画面。它的架构并不复杂,却能把Camera API、JPEG图像编码、多线程队列和TCP网络通信整合成一个可以跑通的闭环。这个源码适合有一定Android基础、想从零搭建轻量监控端的人,也适合想复习老项目迁移到Android Studio的工程师。我会从工程结构、采集链路、网络传输、权限与生命周期,一直讲到用Python客户端验证推帧结果。

2. 源码工程结构拆解:从zip解压到Eclipse项目的可执行路径

2.1 src下只有两个Java文件,却能跑通监控链路?

解压zip后,你会看到典型的Eclipse Android项目布局:src目录里是核心源码,gen目录是R.java和构建产物,bin里留着上一次打包的CameraTest.apkclasses.dex。真正值得读的是src下的ImageServer.javaCameraTest.java,前者承担网络服务,后者负责摄像头采集和预览。assetsresproguard-project.txtproject.properties这些目录和配置文件,决定了老工程能否在新环境下继续构建。

先给出一张文件定位表,方便后续按图索骥:

路径/文件作用是否需要重点关注
AndroidManifest.xml声明权限、Activity、Service,是应用入口配置必须改,尤其是权限与targetSdk
src/org/.../ImageServer.javaSocket服务端,接收采集到的JPEG帧并发送给客户端核心,决定视频流能不能出去
src/org/.../CameraTest.javaActivity类,打开摄像头,预览画面并回调帧数据核心,决定图像采得清不清楚
res/layout/页面布局,通常放置SurfaceView和启动按钮次要,但要先能显示预览
project.properties声明Eclipse编译目标SDK版本迁移到Android Studio前要改
proguard-project.txt混淆规则普通调试可以不理会
bin/CameraTest.apk预编译安装包只供参考,不作为源码依据
gen/R.java资源索引自动生成,不要手动编辑

这个表把“老工程都有哪些文件”交代清楚了。很多人一看到bin里的APK就急着反编译,其实先读源码、再构建一次,远比逆向classes.dex容易理解。

2.2 AndroidManifest.xml里的权限声明与组件注册

监控应用需要摄像头和网络,第一件事就是在清单文件里声明权限。老工程通常长这样:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.cameratest"> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <application android:label="CameraTest"> <activity android:name="CameraTest" android:screenOrientation="landscape"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name="ImageServer" /> </application> </manifest>

这个清单的逻辑很直接:CAMERAINTERNET是远程视频监控的一对基础权限,少了任何一个都会在真机上直接失败。screenOrientation="landscape"强制横屏,可以省去预览画面旋转的换算,摄像头传感器的原始方向与横屏更匹配。这里把ImageServer注册成service,表示它可能运行在后台线程里,不依赖界面显示。

要注意的是,Android 6.0以后CAMERA被归为危险权限,仅靠<uses-permission>不够,运行时还要弹窗申请。老工程把target=android-19写在project.properties里,并没有运行时权限代码,所以源码搬到新手机上必须先补requestPermissions,否则必定闪退。

2.3 project.properties与targetSdk对构建的影响

老项目没有build.gradleproject.properties是Eclipse的构建配置入口:

# This file is automatically generated. target=android-19

target=android-19表示这个工程编译时面向Android 4.4。当迁移到Android Studio时,只要把这句话翻译成compileSdkVersionminSdkVersion即可。实际操作中,我一般会创建一个新Gradle模块,把这几个配置文件保留在仓库里做历史参考,而不是直接依赖Eclipse生成的项目结构。

除了target,还有.classpath.project这两个Eclipse专用文件,Android Studio不需要它们。如果直接用Android Studio打开旧工程,经常会出现“Invalid project description”或找不到SDK的错误。最简单的处理方式是新建Android项目,把srcresAndroidManifest.xml拷贝进去,再手工调整包名和依赖。

2.4 一个容易踩的坑:直接把bin目录里的APK当权威

bin里的APK虽然是上一次构建的产物,但它的classes.dex很可能是一份旧代码。读源码时不要被bin里的编译时间误导,例如resources.ap_jarlist.cache都只是Eclipse的中间文件。真正能反映设计意图的只有srcres。如果发现ImageServer.java里监听的端口和APK实际端口不一致,以源码为准,因为APK可能来自更早的版本。

3. CameraTest的采集链路:从Camera.open到onPreviewFrame的每一帧

3.1 老Camera API为什么够用

监控场景只需要持续输出预览帧,不需要复杂的对焦和HDR,所以老Camera API反而比Camera2更容易上手。CameraTest.java里大概率会出现Camera.open(0)来获取后置摄像头,然后通过setPreviewCallback把每一帧YUV数据交给ImageServer处理。与Camera2的CaptureRequest管线相比,老Camera API省略了会话、缓冲区排队等概念,代码量少,适合快速验证视频传输链路。

一个典型的预览回调骨架如下:

public class CameraTest extends Activity implements Camera.PreviewCallback { private Camera mCamera; private ImageServer mServer; @Override protected void onResume() { super.onResume(); mCamera = Camera.open(); Camera.Parameters params = mCamera.getParameters(); params.setPreviewSize(640, 480); params.setPreviewFormat(ImageFormat.NV21); mCamera.setParameters(params); mCamera.setPreviewCallback(this); mCamera.startPreview(); } @Override public void onPreviewFrame(byte[] data, Camera camera) { if (mServer != null && mServer.isConnected()) { mServer.offerFrame(data); } } @Override protected void onPause() { super.onPause(); if (mCamera != null) { mCamera.stopPreview(); mCamera.release(); mCamera = null; } } }

onPreviewFrame里的data是NV21格式的裸帧,分辨率640x480时,一帧数据大约640 * 480 * 3 / 2 = 460800字节。如果直接把这样的裸数据通过Socket发送,每秒15帧就是6.9MB左右,WiFi下勉强能传,但公网或弱网环境下会非常卡。正确的做法是先把NV21压缩成JPEG,再把体积砍到原来的三分之一甚至更小。

3.2 NV21转JPEG的两种常用方案

把NV21转成JPEG有很多种写法,最常见的两种:

转换方式优点缺点
YuvImage.compressToJpeg简单直接,不依赖Bitmap分辨率大时耗时明显,需要控制质量
Bitmap+compress可以做缩放、水印、画框内存开销大,Bitmap需要手动recycle

如果只是做监控画面传输,我一般选第一种:

public static byte[] nv21ToJpeg(byte[] nv21, int width, int height, int quality) { YuvImage image = new YuvImage(nv21, ImageFormat.NV21, width, height, null); ByteArrayOutputStream out = new ByteArrayOutputStream(); image.compressToJpeg(new Rect(0, 0, width, height), quality, out); return out.toByteArray(); }

这里quality取值范围0到100,监控场景设在65到75比较合适。太高的话单帧会有50KB以上,太低会出现明显色块。compressToJpeg内部做的是YUV到RGB再到JPEG的转换,过程中没有额外申请Bitmap,适合放在后台线程执行。

需要注意一点:YuvImage生成的JPEG不包含EXIF信息,客户端显示时不会自动旋转。这也是为什么清单文件里要强制横屏,横屏下摄像头传感器方向和屏幕方向一致,能省掉旋转角度计算。

3.3 分辨率、帧率与带宽的取舍

远程监控是典型的带宽敏感型应用。如果接收端是同一WiFi下的电脑,640x480@15fps还能接受;如果走4G公网,建议把分辨率降到320x240,帧率降到10fps。下面是一个粗略的带宽估算表:

分辨率单帧JPEG体积(质量70)15fps所需带宽25fps所需带宽
320x24010-18KB1.2-2.2Mbps2-3.6Mbps
640x48025-45KB3-5.4Mbps5-9Mbps
1280x72060-110KB7.2-13.2Mbps12-22Mbps

这张表只是经验值,具体体积取决于画面复杂度和JPEG编码器。画面里静态场景多,体积会小很多;如果对着一个满是树叶抖动的地方,同样的质量数值体积会迅速翻倍。

在旧Camera API中设置帧率,可以用:

Camera.Parameters params = mCamera.getParameters(); params.setPreviewFpsRange(15000, 15000); // 表示15fps mCamera.setParameters(params);

不是所有设备都支持任意的FPS范围,setPreviewFpsRange之前最好遍历getSupportedPreviewFpsRange()。硬编码一个不支持的区间会导致setParameters抛异常,这也是老机器上常见的崩溃点。

4. ImageServer的网络循环:用原始Socket推送JPEG,而不是HTTP服务器

4.1 Socket长连接比HTTP更适合视频流

从名字看,ImageServer容易让人误以为它要开一个Web服务,其实它更可能是用ServerSocket监听端口,把采集到的JPEG帧推送给连接进来的客户端。HTTP请求-响应模型适合单张图片获取,但实时视频流需要连续推帧,每帧都建立一个TCP连接会产生大量握手开销。Socket长连接一旦建立,服务端可以持续往同一个通道写入帧数据,延迟和资源占用都更小。

一个简化的服务端循环如下:

public class ImageServer { private ServerSocket serverSocket; private volatile boolean running; private BlockingQueue<byte[]> frameQueue = new LinkedBlockingQueue<>(3); public void start(int port) throws IOException { serverSocket = new ServerSocket(port); running = true; new Thread(this::acceptLoop).start(); new Thread(this::sendLoop).start(); } private void acceptLoop() { while (running) { try { Socket client = serverSocket.accept(); client.setTcpNoDelay(true); handleClient(client); } catch (IOException ignored) {} } } private void handleClient(Socket socket) { try (DataOutputStream out = new DataOutputStream(socket.getOutputStream())) { while (running) { byte[] frame = frameQueue.poll(1000, TimeUnit.MILLISECONDS); if (frame != null) { out.writeInt(frame.length); out.write(frame); out.flush(); } } } catch (IOException e) { // 客户端断开连接 } } public void offerFrame(byte[] jpeg) { if (!frameQueue.offer(jpeg)) { frameQueue.poll(); frameQueue.offer(jpeg); } } }

这里的数据帧格式很微小,却决定了客户端能不能正确解析。协议可以定义为:

字段类型说明
帧长度int(大端)表示后面跟着的JPEG字节数
JPEG数据byte[]一帧完整图像

DataOutputStream.writeInt默认按大端序写入,客户端解析时必须使用struct.unpack(">I", ...)解包。代码里的frameQueue容量设为3,意思是最多缓存3帧。当采集速度大于网络发送速度时,新帧会顶掉最旧帧,避免延迟越堆越大。

4.2 TCP_NODELAY与队列大小的设置

视频帧通常大于一个MSS(约1460字节),即使不设置TCP_NODELAY,大包也会立即拆包发送。真正需要setTcpNoDelay(true)的场景是控制信令和视频帧混发时,小包可能被Nagle算法缓冲到200ms后再发,视觉上就是画面突然卡一下。所以对Socket的写通道,我总会先设置TCP_NODELAY再开始循环。

队列容量为什么是3,而不是10或者无限?因为监控要的是“新画面”,不是“完整历史”。如果队列无限增大,客户端看到的是越来越迟的画面,这不是监控而是录像回放。当网络恢复后,队列里的旧帧很快被清空,新帧被发送,延迟自然回到正常水平。

4.3 连接不上的排错顺序

从真机调试到局域网环境,最常见的失败是端口不可达。先用下面命令验证端口:

# 查看手机当前的wlan IP adb shell ip addr show wlan0 # 从电脑测试端口连通性 nc -vz 192.168.1.10 8080

如果nc命令提示Connection refused,先确认ImageServer有没有真正启动,再确认防火墙是否拦了入站端口。很多公共WiFi启用了AP隔离,手机和电脑虽然在同一局域网但互相不可见。此时可以用adb reverse把手机端口映射到电脑本地,绕开网络隔离:

adb reverse tcp:8080 tcp:8080

这条命令的语义是:电脑访问自己的8080端口时,数据通过USB转发给手机的8080端口。它只对电脑本机连接有效,但非常适合开发阶段验证ImageServer的推帧逻辑是否正常。

5. Android权限、多线程与生命周期:让Camera和Socket不互相拖垮

5.1 权限声明之外,还要处理运行时权限

老工程默认跑在Android 4.4上,所以AndroidManifest.xml里只写<uses-permission>就够了。但现在随便一台真机都是Android 10以上,CAMERARECORD_AUDIO这类危险权限必须动态申请。补齐运行时权限的模板代码并不复杂:

if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{Manifest.permission.CAMERA}, 1); } else { openCamera(); }

这里checkSelfPermission用来判断是否已经授权,requestPermissions弹出系统授权对话框。用户拒绝后,openCamera()不会被调用,所以不会出现Camera.open()SecurityException的情况。INTERNETACCESS_NETWORK_STATE是普通权限,不需要动态申请。

还有一点容易被忽略:当targetSdkVersion >= 28时,Android默认禁止明文Socket通信。如果你直接用ServerSocket收发明文JPEG,系统会抛出Cleartext communication not permitted。解决办法是在AndroidManifest.xml<application>标签里加:

<application android:usesCleartextTraffic="true">

这行配置只适合局域网调试,正式产品建议用SSLSocket或至少做端到端加密,否则视频流裸奔在公网上非常危险。

5.2 相机回调线程不能做网络IO

onPreviewFrame运行在系统Binder线程,不是主线程,但也不适合直接做网络写操作。我见过很多改造者把out.write直接写在回调里,看起来能跑,可一旦网络抖动,写阻塞会把相机回调全部卡住,画面变成“冻住”的状态。正确的做法是把byte[]先放进有界队列,再由独立网络线程消费。下面是一个用HandlerThread的例子:

HandlerThread senderThread = new HandlerThread("frame-sender"); senderThread.start(); Handler sender = new Handler(senderThread.getLooper()); sender.post(() -> { byte[] frame = imageServer.blockingPoll(); imageServer.send(frame); });

这个方案的边界要清楚:Handler.post的任务会排队,如果网络发送一直慢于采集,任务队列还是会增长。更好的方案是像第4章那样用BlockingQueue,队列满时丢弃旧帧。采集线程永远不阻塞,网络线程永远在低延迟状态下等待新帧。

5.3 Activity生命周期管理:不要在onPause里忘记释放Camera

栗子:

生命周期相机操作网络操作
onCreate初始化UI,申请权限创建ImageServer对象
onResumeCamera.openstartPreview绑定端口,开启线程
onPausestopPreviewrelease断开客户端连接
onDestroy释放所有资源关闭ServerSocket

Camera是独占资源,不释放的话,其他应用甚至自己再次打开时都会失败。onPause不一定伴随应用退出,来电、锁屏、切后台都会触发,所以在onPause里释放相机是底线。

5.4 让监控端屏幕常亮

老式监控应用通常希望手机放在某个角落长时间运行。屏幕灭掉后系统可能进入休眠,相机预览虽然还在,但应用线程调度会受影响。最简单的方法是保持屏幕常亮:

getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

这个方法不需要任何权限。如果还想让Service在后台不被回收,就需要把ImageServer放进前台Service并绑定通知栏。这个工程如果只是局域网测试,Activity常亮加WAKE_LOCK就够了。

6. 真机验证:用Python和adb forward确认ImageServer在正常推帧

6.1 先写一个最简Python接收端

验证监控链路不需要Android客户端,直接写一个Python脚本在电脑上解析TCP流即可。先用adb reverse把手机端口映射到电脑,再连接127.0.0.1:8080

import socket import struct import numpy as np import cv2 s = socket.socket() s.connect(("127.0.0.1", 8080)) while True: raw_len = s.recv(4) if len(raw_len) < 4: break frame_len = struct.unpack(">I", raw_len)[0] data = b'' while len(data) < frame_len: chunk = s.recv(frame_len - len(data)) if not chunk: break data += chunk image = cv2.imdecode(np.frombuffer(data, dtype=np.uint8), cv2.IMREAD_COLOR) cv2.imshow("remote_monitor", image) if cv2.waitKey(1) & 0xFF == ord('q'): break

这段代码把TCP流按“长度+内容”拆包,recv循环处理半包。如果一帧JPEG比较大,TCP不会一次性把整帧数据送过来,必须循环读取直到拿满frame_len字节。cv2.imdecode直接解码JPEG缓冲区,省去写临时文件再读回的过程。

验证步骤是:手机开启ImageServer,PC执行adb reverse tcp:8080 tcp:8080,再运行上面的Python脚本。如果画面一帧帧出现,说明摄像头采集、JPEG编码、Socket推帧、接收端解析整条链路都是通的。

6.2 迁移到Android Studio时最容易踩的三个坑

第一,不要直接导入Eclipse的.classpath,Android Studio不认识它。正确做法是在IDE里选择“Import Project”,让它自动创建Gradle骨架。第二,project.properties里的target=android-19要转换成compileSdkVersionminSdkVersion,否则系统会默认用很老的SDK编译,部分API出现兼容问题。第三,如果运行在Android 9以上手机,注意usesCleartextTraffic="true"requestPermissions有没有补上。

如果Python端一直提示连接失败,先确认adb reverse是否成功,再确认App里的ImageServer有没有在onCreateonResume中启动。画面出现但颜色不对,多数是预览格式不是NV21,或者YuvImage转换时宽高参数与setPreviewSize不一致。对于这类老Camera项目,把日志写在onPreviewFrame入口处,观察data.length是否等于width * height * 3 / 2,基本上几分钟就能定位问题。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 1:26:17

MySQL报错ERROR 1406 Data too long for column ‘name‘排查与修复指南

我上周就在一个线上项目里遇到这个报错&#xff0c;正在跑一个数据导入任务&#xff0c;往user_info表插一条用户自我介绍&#xff0c;结果一句几百字的文本刚塞进去&#xff0c;客户端直接甩出来一串红字&#xff1a;ERROR 1406 (22001): Data truncation: Data too long for …

作者头像 李华
网站建设 2026/9/16 1:26:05

扑克牌目标识别数据集标注全流程:从工具选型到YOLO训练验证

简介&#xff1a;面向目标检测入门与扑克牌识别场景的标注数据集&#xff0c;适合学习YOLO、SSD等检测模型的初学者&#xff0c;也适用于需要自定义扑克牌识别任务的开发者。数据集中图片均来自真实拍摄画面&#xff0c;标注类别涵盖queen、ten、nine、king、jack、ace六种常见…

作者头像 李华
网站建设 2026/9/16 1:25:46

SAP用户状态管理深度解析:OK02配置与S_USERSTAT权限实战

1. 这不是教你怎么点菜单&#xff0c;而是带你真正看懂SAP用户状态管理的底层逻辑“跟着团子学SAP&#xff1a;SAP用户状态管理详解&#xff08;含权限分配等&#xff09;OK02”——这个标题里藏着一个被无数新手反复踩坑、却被资深顾问刻意回避的真相&#xff1a;OK02不是个简…

作者头像 李华
网站建设 2026/9/16 1:24:17

ClickHouse在实时监控系统中的应用与优化

1. 为什么选择ClickHouse做实时监控&#xff1f;在数据量爆炸式增长的今天&#xff0c;传统监控系统面临三大痛点&#xff1a;一是数据延迟高&#xff0c;往往要等几分钟甚至更久才能看到监控指标&#xff1b;二是存储成本居高不下&#xff0c;原始监控数据通常需要定期清理&am…

作者头像 李华
网站建设 2026/9/16 1:23:56

LIS2DW12硬件活动静止检测原理与STM32低功耗实现

简介&#xff1a;本资源是一套面向嵌入式开发者与STM32初学者的LIS2DW12三轴加速度计实战开发资料&#xff0c;聚焦于活动/静止状态检测这一典型低功耗应用场景&#xff0c;适用于可穿戴设备、智能手环、安防终端等需实时运动识别的嵌入式项目。压缩包共181个文件&#xff0c;含…

作者头像 李华