Android实时获取摄像头画面传输至PC端
前言
最近在做一个PC端小应用,需要获取摄像头画面,但是电脑摄像头像素太低,而且位置调整不方便,又不想为此单独买个摄像头。于是想起了之前淘汰掉的手机,成像质量还是杠杠的,能不能把手机摄像头连接到电脑上使用呢?经过搜索,在网上找到了几款这类应用,但是都是闭源的。我一向偏好使用开源软件,但是找了挺久也没有找到一个比较合适的。想着算了,自己开发一个吧,反正这么个简单的需求,应该大概也许不难吧(
思路
通过Android的Camera API是可以拿到摄像头每一帧的原始图像数据的,一般都是YUV格式的数据,一帧2400x1080的图片大小为2400x1080x3/2字节,约等于3.7M。25fps的话,带宽要达到741mbps,太费带宽了,所以只能压缩一下再传输了。最简单的方法,把每一帧压缩成jpeg再传输,就是效率有点低,而更好的方法是压缩成视频流后再传输,PC端接收到视频流后再实时解压缩还原回图片。
实现
思路有了,那就开搞吧。
获取摄像头数据
新建一个Android项目,然后在AndroidManifest.xml中声明摄像头和网络权限:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
界面上搞一个SurfaceView用于预览
<SurfaceView
android:id="@+id/surfaceview"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
进入主Activity时,打开摄像头:
private void openCamera(int cameraId) {
class CameraHandlerThread extends HandlerThread {
private Handler mHandler;
public CameraHandlerThread(String name) {
super(name);
start();
mHandler = new Handler(getLooper());
}
synchronized void notifyCameraOpened() {
notify();
}
void openCamera() {
mHandler.post(() -> {
camera = Camera.open(cameraId);
notifyCameraOpened();
});
try {
wait();
} catch (InterruptedException e) {
Log.w(TAG, "wait was interrupted");
}
}
}
if (camera == null) {
CameraHandlerThread mThread = new CameraHandlerThread("camera thread");
synchronized (mThread) {
mThread.openCamera();
}
}
}
然后绑定预览surface并调用摄像头预览接口开始获取摄像头数据:
camera.setPreviewDisplay(surfaceHolder);
buffer.data = new byte[bufferSize];
camera.setPreviewCallbackWithBuffer(this);
camera.addCallbackBuffer(buffer.data);
camera.startPreview();
每一帧图像的数据准备好后,会通过onPreviewFrame回调把YUV数据传送过来,处理完后,一定要再调一次addCallbackBuffer以获取下一帧的数据。
@Override
public void onPreviewFrame(byte[] data, Camera c) {
// data就是原始YUV数据
// 这里处理YUV数据
camera.addCallbackBuffer(buffer.data);
}
监听PC端连接
直接用ServerSocket就行了,反正也不需要考虑高并发场景。
try (ServerSocket srvSocket = new ServerSocket(6666)) {
this.socketServer = srvSocket;
for (; ; ) {
Socket socket = srvSocket.accept();
this.outputStream = new DataOutputStream(socket.getOutputStream());
// 初始化视频编码器
}
} catch (IOException ex) {
Log.e(TAG, ex.getMessage(), ex);
}
视频编码
Android上可以使用系统自带的MediaCodec实现视频编解码,但是这里我并不打算使用它,而是使用灵活度更高的ffmpeg(谁知道后面有没有一些奇奇怪怪的需求)。 网上已经有大神封装好适用于Android的ffmpeg了,直接在Gradle上引用javacv库就行。
configurations {
javacpp
}
task javacppExtract(type: Copy) {
dependsOn configurations.javacpp
from { configurations.javacpp.collect { zipTree(it) } }
include "lib/**"
into "$buildDir/javacpp/"
android.sourceSets.main.jniLibs.srcDirs += ["$buildDir/javacpp/lib/"]
tasks.getByName('preBuild').dependsOn javacppExtract
}
dependencies {
implementation group: 'org.bytedeco', name: 'javacv', version: '1.5.9'
javacpp group: 'org.bytedeco', name: 'openblas-platform', version: '0.3.23-1.5.9'
javacpp group: 'org.bytedeco', name: 'opencv-platform', version: '4.7.0-1.5.9'
javacpp group: 'org.bytedeco', name: 'ffmpeg-platform', version: '6.0-1.5.9'
}
javacv库自带了一个FFmpegFrameRecorder类可以实现视频录制功能,但是灵活度太低,还是直接调原生ffmpeg接口吧。
初始化H264编码器:
public void init(int width, int height, int[] preferredPixFmt) throws IOException {
int bitRate = width * height * 3 / 2 * 16;
int frameRate = 25;
encoder = avcodec_find_encoder(AV_CODEC_ID_H264);
codecCtx = initCodecCtx(width, height, fmt, bitRate, frameRate);
tempFrame = av_frame_alloc();
scaledFrame = av_frame_alloc();
tempFrame.pts(-1);
packet = av_packet_alloc();
}
private AVCodecContext initCodecCtx(int width, int height,int pixFmt, int bitRate, int frameRate) {
AVCodecContext codec_ctx = avcodec_alloc_context3(encoder);
codec_ctx.codec_id(AV_CODEC_ID_H264);
codec_ctx.pix_fmt(pixFmt);
codec_ctx.width(width);
codec_ctx.height(height);
codec_ctx.bit_rate(bitRate);
codec_ctx.rc_buffer_size(bitRate);
codec_ctx.framerate().num(frameRate);
codec_ctx.framerate().den(1);
codec_ctx.gop_size(frameRate);//每秒1个关键帧
codec_ctx.time_base().num(1);
codec_ctx.time_base().den(frameRate);
codec_ctx.has_b_frames(0);
codec_ctx.global_quality(1);
codec_ctx.max_b_frames(0);
av_opt_set(codec_ctx.priv_data(), "tune", "zerolatency", 0);
av_opt_set(codec_ctx.priv_data(), "preset", "ultrafast", 0);
int ret = avcodec_open2(codec_ctx, encoder, (AVDictionary) null);
return ret == 0 ? codec_ctx : null;
}
把摄像头数据送进来编码,由于摄像头获取到的数据格式和视频编码需要的数据格式往往不一样,所以,编码前需要调用sws_scale对图像数据进行格式转换。
public int recordFrame(Frame frame) {
byte[] data = frame.data; // 对应onPreviewFrame回调里的data
int pf = frame.pixelFormat;
if (tempFrameDataLen < data.length) {
if (tempFrameData != null) {
tempFrameData.releaseReference();
}
tempFrameData = new BytePointer(data.length);
tempFrameDataLen = data.length;
}
tempFrameData.put(data);
int width = frame.width;
int height = frame.height;
av_image_fill_arrays(tempFrame.data(), tempFrame.linesize(), tempFrameData, pf, width, height, frame.align);
tempFrame.format(pf);
tempFrame.width(width);
tempFrame.height(height);
tempFrame.pts(tempFrame.pts() + 1);
return recordFrame(tempFrame);
}
public int recordFrame(AVFrame frame) {
int res = 0;
int srcFmt = frame.format();
int dstFmt = codecCtx.pix_fmt();
int width = frame.width();
int height = frame.height();
if (srcFmt != dstFmt) {
// 图像数据格式转换
convertCtx = sws_getCachedContext(
convertCtx,
width, height, srcFmt,
width, height, dstFmt,
SWS_BILINEAR, null, null, (DoublePointer) null
);
int requiredDataLen = width * height * 3 / 2;
if (scaledFrameDataLen < requiredDataLen) {
if (scaledFrameData != null) {
scaledFrameData.releaseReference();
}
scaledFrameData = new BytePointer(requiredDataLen);
scaledFrameDataLen = requiredDataLen;
}
av_image_fill_arrays(scaledFrame.data(), scaledFrame.linesize(), scaledFrameData, dstFmt, width, height, 1);
scaledFrame.format(dstFmt);
scaledFrame.width(width);
scaledFrame.height(height);
scaledFrame.pts(frame.pts());
res = sws_scale(convertCtx, frame.data(), frame.linesize(), 0, height, scaledFrame.data(), scaledFrame.linesize());
if (res == 0) {
throw new RuntimeException("scale frame failed");
}
frame = scaledFrame;
}
res = avcodec_send_frame(codecCtx, frame);
scaledFrame.pts(scaledFrame.pts() + 1);
if (res != 0 && res != AVERROR_EAGAIN()) {
throw new RuntimeException("Failed to encode frame:" + res);
}
res = avcodec_receive_packet(codecCtx, packet);
if (res != 0 && res != AVERROR_EAGAIN()) {
return res;
}
return res;
}
编码完一帧图像后,需要检查是否有AVPacket生成,如果有,把它回写给请求端即可。
AVPacket pkg = encoder.getPacket();
if (outBuffer == null || outBuffer.length < pkg.size()) {
outBuffer = new byte[pkg.size()];
}
BytePointer pkgData = pkg.data();
if (pkgData == null) {
return;
}
pkgData.get(outBuffer, 0, pkg.size());
os.write(outBuffer, 0, pkg.size());
重点流程的代码都写好了,把它们连接起来就可以收工了。
收尾
请求端还没写好,先在电脑端使用ffplay测试一下。
ffplay tcp://手机IP:6666
嗯,一切正常!就是延时有点大,主要是ffplay不知道视频流的格式,所以缓冲了很多帧的数据来侦测视频格式,造成了较大的延时。后面有时间,再写篇使用ffmpeg api实时解码H264的文章(
完整项目代码:https://github.com/kasonyang/net-camera
Android实时获取摄像头画面传输至PC端的更多相关文章
- Android 关于获取摄像头帧数据解码
由于Android下摄像头预览数据只能 ImageFormat.NV21 格式的,所以解码时要经过一翻周折. Camera mCamera = Camera.open(); Camera.Param ...
- Android实时获取音量(单位:分贝)
基础知识 度量声音强度,大家最熟悉的单位就是分贝(decibel,缩写为dB).这是一个无纲量的相对单位,计算公式如下: 分子是测量值的声压,分母是参考值的声压(20微帕,人类所能听到的最小声压).因 ...
- android 实时获取网速
public class NetSpeed { private static final String TAG = NetSpeed.class.getSimpleName(); private lo ...
- HTML5调用本地摄像头画面,拍照,上传服务器
实现功能和适用业务 采集本地摄像头获取摄像头画面,拍照保存,上传服务器: 前端上传图片处理,展示,缩小,裁剪,上传服务器 实现步骤 调取本地摄像头(getUserMedia)/上传图片,将图片/视频显 ...
- 悄摸直播(一)—— 推流器的实现(获取笔记本摄像头画面,转流推流到rtmp服务器)
悄摸直播 -- JavaCV实现本机摄像头画面远程直播 推流器 一.功能说明 获取pc端的摄像头流数据 + 展示直播效果 + 推流到rtmp服务器 二.代码实现 /** * 推流器 * @param ...
- Android网络开发之实时获取最新数据
在实际开发中更多的是需要我们实时获取最新数据,比如道路流量.实时天气信息等,这时就需要通过一个线程来控制视图的更新. 示例:我们首先创建一个网页来显示系统当前的时间,然后在Android程序中每隔5秒 ...
- PC端使用opencv获取webcam,通过socket把Mat图像传输到android手机端
demo效果图: PC端 android端 大体流程 android端是服务器端,绑定IP和端口,监听来自PC端的连接, pc端通过socket与服务器andorid端传输图片. 主要代码 andro ...
- android自动获取短信验证码
前言:android应用的自动化测试必然会涉及到注册登录功能,而许多的注册登录或修改密码功能常常需要输入短信验证码,因此有必要能够自动获得下发的短信验证码.主要就是实时获取短信信息.android上获 ...
- Android Camera2采集摄像头原始数据并手动预览
Android Camera2采集摄像头原始数据并手动预览 最近研究了一下android摄像头开发相关的技术,也看了Google提供的Camera2Basic调用示例,以及网上一部分代码,但都是在Te ...
- 如何实现1080P延迟低于500ms的实时超清直播传输技术<转>
转载地址:http://www.yunweipai.com/archives/9037.html 最近由于公司业务关系,需要一个在公网上能实时互动超清视频的架构和技术方案.众所周知,视频直播用 CDN ...
随机推荐
- 【LeetCode动态规划#06】分割等和子集(01背包问题一维写法实战)
分割等和子集 分割等和子集 给你一个 只包含正整数 的 非空 数组 nums .请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等. 示例 1: 输入:nums = [1,5,11,5 ...
- vs的常用配置【以及vs常用的快捷键】
1.颜色设置 (1) 编译器的主题颜色设置 (2) 字体和颜色设置 (3) 字体大小 更快捷的修改字体大小方式:ctr+鼠标滚轮 2.行号设置 默认就有,不用设置了 3.把解决方案资源管理器移动到左边 ...
- springboot-poi ---封装注解式导入导出
此demo 是基于poi封装对象式注解导入导出,项目框架为springboot项目! 简单的说明一下此demo涉及到的知识点,希望能给初学者带来方便! poi-excel 基本操作(工具) 自定义注解 ...
- 基于RL(Q-Learning)的迷宫寻路算法
强化学习是一种机器学习方法,旨在通过智能体在与环境交互的过程中不断优化其行动策略来实现特定目标.与其他机器学习方法不同,强化学习涉及到智能体对环境的观测.选择行动并接收奖励或惩罚.因此,强化学习适用于 ...
- 尝试CentOS8---部署集群(生产环境7.9为好)
一.LVS集群简介 什么是集群 通过网络将很多服务器集中起来,提供同一种服务,在客户端看来就像是只有一个服务器 二.LVS-NAT集群 1.环境准备 启动3台虚拟机,禁用selinux和firewal ...
- 安装MongoDB、及基本使用
1.MongoDB简介 MongoDB是一个介于关系数据库和非关系数据库之间的产品,基于分布式文件存储的数据库.是非关系数据库当中功能最丰富,最像关系数据库的.它支持的数据结构非常松散,是类似json ...
- Appweb+ESP学习笔记
Appweb+ESP学习笔记 1.Appweb简介 Appweb HTTP Web服务器是最快的小型Web服务器.这是一个高性能,紧凑的嵌入式网络服务器,具有模块化,安全的核心.它支持广 ...
- Python_14 接口测试报告
一.查缺补漏 1. 测试用例要复制到pycharm执行的项目中,才能显示 2. 函数用下划线,类用大驼峰 3. pycharm一行显示(不换行): File -> settings-> E ...
- SpringBoot SpringSecurity 介绍(基于内存的验证)
SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘 SpringBoot已经为用户采用默认配置,只需要引入pom依赖就能快速启动Spring ...
- sklearn数据预处理中fit(),transform()与fit_transform()的区别
1 概述 注意这是数据预处理中的方法: Fit(): Method calculates the parameters μ and σ and saves them as internal objec ...