- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
最近在做一个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);
}
直接用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实时获取摄像头画面传输至PC端的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有一个表,用于测量数据集中两个不同值的分布百分比(我有一个计数,替换为 Percentage )。我只想在仪表板中发布 85% 的值。这样做的步骤是什么? .现在,如果我过滤掉 NO 值, YES
我想平均N通过Type对于 Inst 的值 我的数据设置为: Type, Inst, N A, A1, 4 A, A1, 13 A, A1, 13 A, A2, 4 A, A3, 5 B, B1, 4
当我使用 Node 的 http2 库(仅支持 HTTPS,不支持 HTTP)进行开发时,当我在 Chrome 中打开 localhost 时,我会看到一个警告屏幕: Your connection
我想创建一个具有响应式布局的导航栏菜单。当网站显示在显示器上时,我想播放导航栏。当网站在手机上显示时,我会显示最终显示导航栏的菜单图标。 我该怎么做? 最佳答案 试试这个,在移动设备上,导航栏不仅会显
我聘请了一名程序员为我创建一个 iPhone 应用程序。该应用程序的目的是拍照并将其上传到服务器。我们想制作一个特殊用途的屏幕,以便在上传照片之前查看照片。这个专门开发的屏幕将具有至关重要的缩放功能。
我在 Tableau 上有一个我无法解决的简单问题。 我想显示一个图表,显示随时间变化的度量。我想将用户通过参数选择的一个客户端与未选择的所有其他客户端进行比较。该图将显示具有 2 种不同颜色的两条线
我使用 Python 脚本从 3 个不同的 RDS 执行一系列复杂查询,然后将数据导出到一个 CSV 文件中。我现在正在尝试找到一种方法,每周将使用这些数据的仪表板自动发布到 Tableau 服务器中
我在工作中使用 tableau 来处理各种数据类型,包括敏感的个人数据,这些数据只能以聚合格式共享。我试图找到一种方法来保护私有(private)信息,方法是在单元格值小于 5 时隐藏它。这样,当用户
我最近开始在网站上嵌入 Tableau 可视化效果,并遇到了在浏览器中直接使用 Control + P 打印它们的问题。大多数完全扭曲,如果有的话。我做了一些挖掘,发现这是一个已知问题: http:/
例如,此 URL 包含十几个项目:https://tableautest.domain.uk/t/CustomerSharing/view/projects 在每个项目中都有几个工作簿。每个工作簿中都
我正在研究如何使用 Tableau 连接到 Cloudera Hadoop。我提供服务器和端口详细信息并使用“Impala”进行连接。我能够成功连接,选择默认模式并选择所需的表。 在此之后,当我将维度
我正在尝试将 Tableau 工作表嵌入到我的 ReactJS 应用程序中。我有一个包含报告名称列表的菜单(在 react 中),当单击菜单项时,它会更新包含报告名称的状态。我决定使用 tableau
我有以下问题!我有一个这样的表: Data Source 我想创建一个可以获取 apl_id 的字段(我想这是一个字段),有一些我想要的 service_offered。 上表中的示例。如果我想要只有
我有一个航类延误数据电子表格,我正在处理一个显示每个机场总延误时间的工作表。我想过滤维度“机场”,即根据每个机场的起飞次数创建机场类别,“小型”、“中型”和“大型”,这是通过计算维度“航类号”计算得出
我想创建一个带有过滤器的表格,用于选择和比较事物: 假设我有一个变量 Var,包含值 A、B、C、D、E。我想要一个过滤器,以便用户可以选择 A B C D 之一,同时 E 始终被选中。这样选中的E和
我是一名优秀的程序员,十分优秀!