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 ...
随机推荐
- Redis缓存高可用集群
作者:京东零售 王雷 1.Redis集群方案比较 • 哨兵模式 在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态,如果master节点异常,则会做主 ...
- c/c++快乐算法第一天
c/c++感受算法乐趣(1) 开始时间2023-04-14 18:31:47 结束时间2023-04-14 22:06:02 前言:经过两天的学习,是不是发现编程也挺简单的.其实不然,学好算法同时也是 ...
- 开源后台管理系统解决方案 boot-admin 简介
介绍 boot-admin 是一款采用前后端分离架构模式的后台管理框架.系统提炼自实际项目,兼具RuoYi-Vue前端分离版和Ruoyi-Cloud微服务版功能与技术特点. boot-admin 既有 ...
- Kubesphere中DevOps流水线无法部署/部署失败
摘要 总算能让devops运行以后,流水线却卡在了deploy这一步.碰到了两个比较大的问题,一个是无法使用k8sp自带的kubeconfig认证去部署:一个是部署好了以后但是没有办法解析镜像名. 版 ...
- 分享Zeal的全套离线文档
鉴于Zeal自身的下载速度... 为了方便大家,现在把我自己下载好的Zeal离线文档全部分享出来 百度网盘链接:https://pan.baidu.com/s/19WeEWij3evnuMWhzbHu ...
- Navicat Premium 16 安装教程
使用数据库时经常会使用到Navicat,码一个教程 转载自https://www.bilibili.com/read/cv21586676?spm_id_from=444.41.list.card_a ...
- CTFShow 反序列化 Web 255-266
CTFShow 反序列化 255-266 漏洞原理 未队用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入,目录遍历等后果. 触发条件 unserialize ...
- 2021-10-15:单词拆分。给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。说明:拆分时可以重复使用字典中的单词。你
2021-10-15:单词拆分.给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词.说明:拆分时可以重复使用字典中的单词.你 ...
- vue全家桶进阶之路48:Vue3 跨域配置devServer的参数和设置
devServer 是一个用于配置开发服务器的选项对象.它可以用来配置服务器的各种选项,例如代理,端口号,HTTPS 等. 以下是一些常用的 devServer 参数和设置: port:指定开发服务器 ...
- Transformer 估算 101
本文主要介绍用于估算 transformer 类模型计算量需求和内存需求的相关数学方法. 引言 其实,很多有关 transformer 语言模型的一些基本且重要的信息都可以用很简单的方法估算出来.不幸 ...