安卓App与ESP32-Cam的视频传输(优化版)

本文介绍了对基于ESP32-Cam的安卓应用视频传输方案的升级,重点在于采用异步加载技术解决数据传输延迟导致的卡顿问题,并提供了ESP32-Cam的代码片段以及配套的Android应用部分的逻辑实现。
摘要由CSDN通过智能技术生成

一、前言:

       本次设计是对原有基于dsxcode的安卓App与ESP32Cam视频传输方案的优化版本。采用了异步加载技术,确保视频数据在传输过程中能够持续、稳定地流动,从而避免了因数据传输延迟而导致的卡顿问题。

二、Esp32-Cam代码(基于Arduino IDE)

#include <Arduino.h>
#include <WiFi.h>
#include "esp_camera.h"
#include <vector>
#include <Wire.h>

#define maxcache 1024
const char* ssid = "此处输入WiFi名称";
const char* password = "此处输入WiFi密码";
 
const int LED = 4;
const int ZHESHI_LED = 33; 
bool cam_state = true;
const int port = 8080;
String  frame_begin = "Begin";
String  frame_over = "Over";
String  msg_begin = "Head";

WiFiServer server;
WiFiClient client;

#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
 
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

static camera_config_t camera_config = {
    .pin_pwdn = PWDN_GPIO_NUM,
    .pin_reset = RESET_GPIO_NUM,
    .pin_xclk = XCLK_GPIO_NUM,
    .pin_sscb_sda = SIOD_GPIO_NUM,
    .pin_sscb_scl = SIOC_GPIO_NUM,
    .pin_d7 = Y9_GPIO_NUM,
    .pin_d6 = Y8_GPIO_NUM,
    .pin_d5 = Y7_GPIO_NUM,
    .pin_d4 = Y6_GPIO_NUM,
    .pin_d3 = Y5_GPIO_NUM,
    .pin_d2 = Y4_GPIO_NUM,
    .pin_d1 = Y3_GPIO_NUM,
    .pin_d0 = Y2_GPIO_NUM,
    .pin_vsync = VSYNC_GPIO_NUM,
    .pin_href = HREF_GPIO_NUM,
    .pin_pclk = PCLK_GPIO_NUM,   
    .xclk_freq_hz = 20000000,
    .ledc_timer = LEDC_TIMER_0,
    .ledc_channel = LEDC_CHANNEL_0, 
    .pixel_format = PIXFORMAT_JPEG,
    .frame_size = FRAMESIZE_VGA,
    .jpeg_quality = 30, 
    .fb_count = 1,
};

esp_err_t camera_init() {
    esp_err_t err = esp_camera_init(&camera_config);
    if (err != ESP_OK) {
        return err;
    }
    sensor_t *s = esp_camera_sensor_get();
    if (s->id.PID == OV5640_PID) {
        s->set_brightness(s, 1);
        s->set_contrast(s, 1);
    }
    return ESP_OK;
}

bool wifi_init(const char *ssid, const char *password) {
    WiFi.mode(WIFI_STA); 
    WiFi.setSleep(false);
#ifdef staticIP
    WiFi.config(staticIP, gateway, subnet);
#endif
    WiFi.begin(ssid, password); 
    for (uint8_t i = 0; i < 20; ++i) {
        if (WiFi.status() == WL_CONNECTED) {
            break;
        }
        delay(500);
    }
    if (WiFi.status() != WL_CONNECTED) {
        digitalWrite(ZHESHI_LED, HIGH); 
        return false;
    }
    digitalWrite(ZHESHI_LED, LOW);
    return true;
}

void TCPServerInit() {
  server.begin(port);
  server.setNoDelay(true);
}

void cssp() {
  camera_fb_t *fb = esp_camera_fb_get();
  uint8_t *temp = fb->buf;

  if (!fb) {
  } else {
    client.print(frame_begin);
    int leng = fb->len; 
    int timess = leng / maxcache;
    int extra = leng % maxcache;
    uint8_t *ptr = fb->buf;

    for (int j = 0; j < timess; j++) {
      client.write(ptr, maxcache); 
      ptr += maxcache; 
    }

    client.write(ptr, extra);
    client.print(frame_over);
    fb->buf = temp;
    esp_camera_fb_return(fb);
  }
}
void TCPServerMonitor() {
  if (server.hasClient()) {
    if (client && client.connected()) {
      WiFiClient serverClient = server.available();
      serverClient.stop();
    } else {
      client = server.available();
      client.println(msg_begin + "客户端已连接!");
    }
  }

  if (client && client.connected()) {
    if (client.available()) {
      String line = client.readStringUntil('\n');
      if (line == "LedOFF") {
        digitalWrite(LED, LOW);
        client.println(msg_begin + "关闭灯!");
      }
      if (line == "LedON") {
        digitalWrite(LED, HIGH);
        client.println(msg_begin + "打开灯!");
      }
    }
  }
  if (cam_state) {
    if (client && client.connected()) {
      cssp();
    }
  }
}
void setup() {
  Serial.begin(115200);
  pinMode(ZHESHI_LED, OUTPUT);
  digitalWrite(ZHESHI_LED, HIGH);
  pinMode(LED, OUTPUT);
  digitalWrite(LED, LOW);
  wifi_init(ssid,password);
  camera_init();
  TCPServerInit();
} 
void loop() {
  TCPServerMonitor();
}

三、安卓app代码(基于Android Studio)

1、页面布局代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".monitoring">

    <ImageView
        android:id="@+id/show_cam"
        android:layout_width="match_parent"
        android:layout_height="249dp"
        android:layout_marginBottom="4dp"
        android:background="#333"
        android:scaleType="center" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="vertical"
        android:padding="8dp">

        <Button
            android:id="@+id/connect_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="连接" />

        <Button
            android:id="@+id/led"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="灯" />
        
    </LinearLayout>
</LinearLayout>

2、逻辑代码:

package com.skyqi.camera;

import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

public class monitoring extends AppCompatActivity {
    private boolean iscamOn = false;
    Button connect_button,led;
    ImageView show_cam;
    Socket socket;
    InputStream inputStream;
    OutputStream outputStream;
    byte[] RevBuff = new byte[1024];  //定义接收数据流的包的大小
    MyHandler myHandler;
    byte[] temp = new byte[0];  //存放一帧图像的数据
    int headFlag = 0;    // 0 数据流不是图像数据   1 数据流是图像数据
    Bitmap bitmap = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.monitoring);
        connect_button= findViewById(R.id.connect_button);//连接服务器按钮
        led = findViewById(R.id.led);//灯的按钮
        connect_button.setText("连接"); //设置连接按钮名称为连接 如果已连接上显示断开
        show_cam = findViewById(R.id.show_cam); //存放图像数据
        myHandler = new MyHandler();
        
        connect_button.setOnClickListener(view -> {
            if(connect_button.getText() == "连接"){
                new Thread(() -> {
                    Message msg = myHandler.obtainMessage();
                    try {
                        socket = new Socket("这里填写WiFi的IP地址",8080);
                        if(socket.isConnected()){
                            msg.what = 0;//显示连接服务器成功信息
                            inputStream = socket.getInputStream();
                            outputStream = socket.getOutputStream();
                            Recv();//接收数据
                        }else{
                            msg.what = 1;//显示连接服务器失败信息
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        msg.what = 1;//显示连接服务器失败信息
                    }
                    myHandler.sendMessage(msg);
                }).start();
            }else{
                //关闭socket连接
                try { socket.close(); } catch (IOException e) { e.printStackTrace(); }
                try { inputStream.close(); }catch (IOException e) { e.printStackTrace(); }
                try { outputStream.close(); }catch (IOException e) { e.printStackTrace(); }
                connect_button.setText("连接");
            }
        });
        led.setOnClickListener(view -> new Thread(() -> {
            try {
                // 根据当前状态决定发送哪个字符串
                String dataToSend = iscamOn ? "LedOFF" : "LedON";
                // 更新状态
                iscamOn = !iscamOn;
                // 发送数据
                outputStream.write(dataToSend.getBytes());
            } catch (IOException e) {
                // 如果发送数据失败,显示连接服务器失败信息
                e.printStackTrace();
                Message msg = myHandler.obtainMessage();
                msg.what = 1;
                myHandler.sendMessage(msg);
            }

        }).start());

    }
    //    接收数据方法
    public void Recv() {
        new Thread(() -> {
            while (socket != null && socket.isConnected()) {
                try {
                    int Len = inputStream.read(RevBuff);
                    if (Len != -1) {
                        // 处理摄像头开始和结束标志
                        boolean begin_cam_flag = RevBuff[0] == 66 && RevBuff[1] == 101 && RevBuff[2] == 103 && RevBuff[3] == 105 && RevBuff[4] == 110;
                        boolean end_cam_flag = RevBuff[0] == 79 && RevBuff[1] == 118 && RevBuff[2] == 101 && RevBuff[3] == 114;

                        if (headFlag == 0 && begin_cam_flag) {
                            headFlag = 1;
                        } else if (end_cam_flag) {
                            // 使用异步处理图片解码
                            decodeAndShowImage(temp);
                            headFlag = 0;
                            temp = new byte[0]; // 重置临时缓冲区
                        } else if (headFlag == 1) {
                            temp = byteMerger(temp, RevBuff);
                        }

                        // 处理消息开始标志
                        boolean begin_msg_begin = RevBuff[0] == 72 && RevBuff[1] == 101 && RevBuff[2] == 97 && RevBuff[3] == 100;
                        if (begin_msg_begin) {
                            // 处理消息...
                        }
                    } else {
                        // 连接已断开,发送消息并退出循环
                        Message msg = myHandler.obtainMessage();
                        msg.what = 1;
                        myHandler.sendMessage(msg);
                        break;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    // 处理异常,发送消息并退出循环
                    Message msg = myHandler.obtainMessage();
                    msg.what = 1;
                    myHandler.sendMessage(msg);
                    break;
                }
            }
        }).start();
    }

    // 异步解码并显示图片
    private void decodeAndShowImage(final byte[] imageBytes) {
        new AsyncTask<Void, Void, Bitmap>() {
            @Override
            protected Bitmap doInBackground(Void... voids) {
                return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
            }

            @Override
            protected void onPostExecute(Bitmap bitmap) {
                super.onPostExecute(bitmap);
                if (bitmap != null) {
                    // 根据ImageView的大小调整Bitmap大小
                    int width = show_cam.getWidth();
                    int height = show_cam.getHeight();
                    if (width > 0 && height > 0) {
                        bitmap = Bitmap.createScaledBitmap(bitmap, width, height, false);
                    }
                    show_cam.setImageBitmap(bitmap);
                }
            }
        }.execute();
    }

    // 合并一帧图像数据。全局变量 temp 和接受的一个数据包 RevBuff 被合并。
    public byte[] byteMerger(byte[] a, byte[] b) {
        int i = a.length + b.length; // 计算合并后数组的总长度
        byte[] t = new byte[i]; // 定义一个长度为全局变量temp和数据包RevBuff之和的字节数组t
        System.arraycopy(a, 0, t, 0, a.length); // 将全局变量temp(先传过来的数据包)复制到数组t的起始位置
        System.arraycopy(b, 0, t, a.length, b.length); // 将后进来的数据包RevBuff复制到数组t的剩余位置
        return t; // 返回合并后的数组t,通常这个返回的数组会赋值给全局变量temp
    }

    // 定义一个Handler子类,用于处理不能在线程中直接执行的信息(例如UI更新)
    @SuppressLint("HandlerLeak")
    class MyHandler extends Handler {
        public void handleMessage(Message msg) {
            super.handleMessage(msg); // 调用父类的handleMessage方法
            switch (msg.what) { // 根据消息的内容(what字段)来执行不同的操作
                case 0:
                    // 连接服务器成功信息
                    Toast.makeText(monitoring.this, "连接服务器成功!", Toast.LENGTH_SHORT).show(); // 显示一个短暂的Toast消息
                    connect_button.setText("断开"); // 设置连接按钮的文本为“断开”
                    break;
                case 1:
                    // 连接服务器失败信息
                    Toast.makeText(monitoring.this, "连接服务器失败!", Toast.LENGTH_SHORT).show(); // 显示一个短暂的Toast消息
                    break;
                case 2:
                    // 处理接收到的非图像数据
                    byte[] Buffer = new byte[msg.arg1]; // 根据消息的arg1字段创建一个新的字节数组
                    System.arraycopy((byte[]) msg.obj, 0, Buffer, 0, msg.arg1); // 将消息中的obj字段(一个字节数组)复制到Buffer中
                    @SuppressLint("SimpleDateFormat") SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); // 创建一个日期格式器
                    Date date = new Date(System.currentTimeMillis()); // 获取当前时间
                    String content = (new String(Buffer)) + "----" + formatter.format(date) + "\n"; // 构造一个包含数据和时间的字符串
                    //rec_data.append(content); // 将这个字符串追加到rec_data(可能是一个TextView或者StringBuilder)中
                    break;
                case 3:
                    // 处理接收到的图像数据并展示
                    bitmap = BitmapFactory.decodeByteArray(temp, 0, temp.length); // 使用BitmapFactory将字节数组解码成Bitmap对象
                    show_cam.setImageBitmap(bitmap); // 将这个Bitmap对象设置为show_cam(可能是一个ImageView)的显示内容
                    temp = new byte[0]; // 将全局变量temp重置为一个空字节数组,表示一帧图像处理结束
                    break;
                default:
                    break; // 默认情况,不执行任何操作
            }
        }
    }
    //    销毁窗体 释放资源
    protected void onDestroy() {
        super.onDestroy();
        if(inputStream != null){
            try {inputStream.close();}catch(IOException e) {e.printStackTrace();}
        }

        if(outputStream != null){
            try {outputStream.close();} catch (IOException e) {e.printStackTrace();}
        }
        if(socket != null){
            try {socket.close();} catch (IOException e) {e.printStackTrace();}
        }
    }
}

3、权限代码:

<uses-permission android:name="android.permission.INTERNET" />

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SkyQi_奇崽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值