前言

最近在做一个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端的更多相关文章

  1. Android 关于获取摄像头帧数据解码

    由于Android下摄像头预览数据只能  ImageFormat.NV21 格式的,所以解码时要经过一翻周折. Camera mCamera = Camera.open(); Camera.Param ...

  2. Android实时获取音量(单位:分贝)

    基础知识 度量声音强度,大家最熟悉的单位就是分贝(decibel,缩写为dB).这是一个无纲量的相对单位,计算公式如下: 分子是测量值的声压,分母是参考值的声压(20微帕,人类所能听到的最小声压).因 ...

  3. android 实时获取网速

    public class NetSpeed { private static final String TAG = NetSpeed.class.getSimpleName(); private lo ...

  4. HTML5调用本地摄像头画面,拍照,上传服务器

    实现功能和适用业务 采集本地摄像头获取摄像头画面,拍照保存,上传服务器: 前端上传图片处理,展示,缩小,裁剪,上传服务器 实现步骤 调取本地摄像头(getUserMedia)/上传图片,将图片/视频显 ...

  5. 悄摸直播(一)—— 推流器的实现(获取笔记本摄像头画面,转流推流到rtmp服务器)

    悄摸直播 -- JavaCV实现本机摄像头画面远程直播 推流器 一.功能说明 获取pc端的摄像头流数据 + 展示直播效果 + 推流到rtmp服务器 二.代码实现 /** * 推流器 * @param ...

  6. Android网络开发之实时获取最新数据

    在实际开发中更多的是需要我们实时获取最新数据,比如道路流量.实时天气信息等,这时就需要通过一个线程来控制视图的更新. 示例:我们首先创建一个网页来显示系统当前的时间,然后在Android程序中每隔5秒 ...

  7. PC端使用opencv获取webcam,通过socket把Mat图像传输到android手机端

    demo效果图: PC端 android端 大体流程 android端是服务器端,绑定IP和端口,监听来自PC端的连接, pc端通过socket与服务器andorid端传输图片. 主要代码 andro ...

  8. android自动获取短信验证码

    前言:android应用的自动化测试必然会涉及到注册登录功能,而许多的注册登录或修改密码功能常常需要输入短信验证码,因此有必要能够自动获得下发的短信验证码.主要就是实时获取短信信息.android上获 ...

  9. Android Camera2采集摄像头原始数据并手动预览

    Android Camera2采集摄像头原始数据并手动预览 最近研究了一下android摄像头开发相关的技术,也看了Google提供的Camera2Basic调用示例,以及网上一部分代码,但都是在Te ...

  10. 如何实现1080P延迟低于500ms的实时超清直播传输技术<转>

    转载地址:http://www.yunweipai.com/archives/9037.html 最近由于公司业务关系,需要一个在公网上能实时互动超清视频的架构和技术方案.众所周知,视频直播用 CDN ...

随机推荐

  1. python:字典中遍历key对应的value值

    问题描述:python在一个字典中想要遍历某个可以对应的value值. 效果如下: 方法一:使用循环遍历所有的key,然后对比输入的值跟便利出来的key有没有相同的 constellation = [ ...

  2. ORA-12560: TNS: 协议适配器错误 windows

    1.监听服务没有起起来.windows平台个一如下操作:开始-程序-管理工具-服务,打开服务面板,启动oraclehome92TNSlistener服务. 2.database instance没有起 ...

  3. .NET周报 【4月第4期 2023-04-23】

    国内文章 2023成都.NET线下技术沙龙圆满结束 https://www.cnblogs.com/edisonchou/p/2023_chengdu_dotnet_club_activity_rev ...

  4. shell基本命令与参数

    1:一行可以有多个命令,用";"分开 如: cd ..; ls -l 2:先项用"-"开始,多个连接可连在一起,如:ls -lh, 3:"--&quo ...

  5. 搭建SpringBoot项目依赖和配置快速篇

    maven依赖及一些配置 这里主要是搭建项目常用到的maven依赖以及搭建项目会需要用到的一些配置文件,可能下面这些依赖还不是很全,但是应该会满足日常大部分的需求了 Spring Spring项目的依 ...

  6. Java代码读取properties配置文件

    读取properties配置文件 package com.easycrud.utils; import java.io.IOException; import java.io.InputStream; ...

  7. 项目打包后配置到node服务器

    1.将项目进行打包 npm run build项目根目录下会多出一个打包好的由.js .html .css文件组成的dist文件夹,如图 2.搭建node微型服务器   新建文件夹命名"no ...

  8. 2023-03-10:YUV420P像素数据编码为JPEG图片,请用go语言实现。

    2023-03-10:YUV420P像素数据编码为JPEG图片,请用go语言实现. 答案2023-03-10: 方法一.使用 github.com/moonfdd/ffmpeg-go 库,基于雷霄骅的 ...

  9. 2022-03-18:arr数组长度为n, magic数组长度为m 比如 arr = { 3, 1, 4, 5, 7 },如果完全不改变arr中的值, 那么收益就是累加和 = 3 + 1 + 4 +

    2022-03-18:arr数组长度为n, magic数组长度为m 比如 arr = { 3, 1, 4, 5, 7 },如果完全不改变arr中的值, 那么收益就是累加和 = 3 + 1 + 4 + ...

  10. 2021-12-21:任务调度器。 给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间

    2021-12-21:任务调度器. 给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表.其中每个字母表示一种不同种类的任务.任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间 ...