一、前言:
本次设计是对原有基于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" />