首页 > 软件编程 > Android > Android获取摄像头画面

Android实时获取摄像头画面传输至PC端思路详解

作者:kason

这篇文章主要介绍了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设备adb连接后显示device unauthorized解决方案
    Android设备adb连接后显示device unauthorized解决方案
    2023-06-06
  • Android App 如何防止抓包方法及分析
    Android App 如何防止抓包方法及分析
    2023-06-06
  • 常见的8个Android内存泄漏问题及解决方法
    常见的8个Android内存泄漏问题及解决方法
    2023-07-07
  • Android连接MySQL数据库详细教程
    Android连接MySQL数据库详细教程
    2023-05-05
  • Android实时获取摄像头画面传输至PC端思路详解
    Android实时获取摄像头画面传输至PC端思路详解
    2023-05-05
  • Android Xml转换为View过程详解
    Android Xml转换为View过程详解
    2023-05-05
  • Android类加载ClassLoader双亲委托机制详解
    Android类加载ClassLoader双亲委托机制详解
    2023-05-05
  • Android FrameWork之Zygote启动示例详解
    Android FrameWork之Zygote启动示例详解
    2023-05-05
  • 美国设下计谋,用娘炮文化重塑日本,已影响至中国
    美国设下计谋,用娘炮文化重塑日本,已影响至中国
    2021-11-19
  • 时空伴随者是什么意思?时空伴随者介绍
    时空伴随者是什么意思?时空伴随者介绍
    2021-11-09
  • 工信部称网盘企业免费用户最低速率应满足基本下载需求,天翼云盘回应:坚决支持,始终
    工信部称网盘企业免费用户最低速率应满足基本下载需求,天翼云盘回应:坚决支持,始终
    2021-11-05
  • 2022年放假安排出炉:五一连休5天 2022年所有节日一览表
    2022年放假安排出炉:五一连休5天 2022年所有节日一览表
    2021-10-26
  • 电脑版 - 返回首页

    2006-2024 脚本之家 JB51.Net , All Rights Reserved.
    苏ICP备14036222号