服务端:
1.设备:树莓派、CSI摄像头、手机/平板。
2.功能:低延时实时图传。
3.实现:
用Python创建一个低延时的视频传输程序,读取CSI摄像头画面,实时捕获图像,设置分辨率大小为640*480,设置帧率为20;设置发送包压缩大小为30,将其发送到指定的UDP地址和端口号。
客户端:
1.Android Studio编程软件
2.创建
页面一:一个IP地址输入框、一个端口号输入框、一个确认按钮。
当点击按钮时,传递IP地址和端口号到第二页,并跳转到第二页
页面二: 获取第一页传递的IP地址和端口号,创建一个线程,在线程中接收UDP服务端发出的图像数据并播放出来,在活动销毁时关闭线程,释放资源。
问题:服务端Python运行正常,使用了PyCharm运行的客户端是可以接收到画面的,但如果是客户端Android Studio模拟器运行后输入IP和端口号,跳转第二页页面没出现视频画面。
帮忙看看,哪有问题,不怎么会Android_Studio编程。相关的代码是使用GPT构建实现,然后将相关报错一个一个的改的,但改完,程序是运行了,也没闪退啥的,就是无法实现功能,不知道什么原因。
有想过原因
1.树莓派UDP传过来的数据包,在app无法解析或者是解析的库是不对的。
2.UDP数据包的字节数,跟app设置的不一样啥的。
3.视频转成数据包后,在app解压时不完整。
4.app就没有接收到数据包。
有人解惑不
下面是相关的代码
服务端:
import cv2
import numpy as np
import socket
# 定义UDP通信的IP地址和端口号
UDP_IP_ADDRESS = "***.***.***.***." # 服务器端IP地址
UDP_PORT_NO = **** # 端口号
# 创建socket对象
serverSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
serverSock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serverSock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024*1024*16)
# 设置视频捕获对象
capture = cv2.VideoCapture(0)
capture.set(3, 640) # 设置分辨率,宽度为640
capture.set(4, 480) # 设置分辨率,高度为480
capture.set(5, 15) # 设置帧率,每秒10帧
while True:
ret, frame = capture.read()
# 将捕获到的图像压缩成JPG格式
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 50]
result, encoded_img = cv2.imencode('.jpg', frame, encode_param)
# 将压缩后的图像数据转成byte类型
data = encoded_img.tobytes()
# 发送数据包
serverSock.sendto(data, (UDP_IP_ADDRESS, UDP_PORT_NO))
客户端
Android_Studio
app/manifests/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".PlayerActivity"
android:exported="true">
<intent-filter android:scheme="http"
tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
</activity>
</application>
</manifest>
app/java/com.example.myapplication/
MainActivity
package com.example.myapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
private EditText editTextIP;
private EditText editTextPort;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editTextIP = findViewById(R.id.editTextIP);
editTextPort = findViewById(R.id.editTextPort);
Button buttonConnect = findViewById(R.id.buttonConnect);
buttonConnect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String ip = editTextIP.getText().toString();
String port = editTextPort.getText().toString();
Intent intent = new Intent(MainActivity.this, PlayerActivity.class);
intent.putExtra("ip", ip);
intent.putExtra("port", port);
startActivity(intent);
}
});
}
}
app/java/com.example.myapplication/
PlayerActivity
package com.example.myapplication;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.zip.InflaterInputStream;
public class PlayerActivity extends Activity {
private ImageView imageView;
private String address;
private int port;
private DatagramSocket socket;
private Thread thread;
private boolean running;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
// 获取IP地址和端口号
Intent intent = getIntent();
address = intent.getStringExtra("address");
port = intent.getIntExtra("port", 0);
imageView = (ImageView) findViewById(R.id.imageView);
// 创建UDP套接字
try {
socket = new DatagramSocket();
socket.setSoTimeout(2000);
} catch (SocketException e) {
e.printStackTrace();
}
// 创建接收图像数据的线程
running = true;
thread = new Thread(new Runnable() {
@Override
public void run() {
while (running) {
try {
// 接收数据
byte[] buffer = new byte[65000];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
// 解压缩图像数据
byte[] data = packet.getData();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
InflaterInputStream inflaterInputStream = new InflaterInputStream(byteArrayInputStream);
byte[] buf = new byte[1024];
int len;
while ((len = inflaterInputStream.read(buf)) != -1) {
byteArrayOutputStream.write(buf, 0, len);
}
// 显示图像
Bitmap bitmap = BitmapFactory.decodeByteArray(byteArrayOutputStream.toByteArray(), 0, byteArrayOutputStream.size());
imageView.setImageBitmap(bitmap);
} catch (SocketTimeoutException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
thread.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 关闭线程
running = false;
// 关闭UDP套接字
socket.close();
}
}
app/res/layout/
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<EditText
android:id="@+id/editTextIP"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="IP Address"/>
<EditText
android:id="@+id/editTextPort"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Port"/>
<Button
android:id="@+id/buttonConnect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Connect"/>
</LinearLayout>
app/res/layout/
activity_player.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
安卓的版本可能有要求的
不行。因为:重传时IP数据报的标识字段有另外一个标识符。标识符相同的IP数据报片才能组装成一个IP数据报。前两个IP数据报片的标识符与后两个IP数据报片的标识符不同,所以不能组装成一个IP数据报。
IP地址和端口传递问题:在 MainActivity 中,你获取了 EditText 的文本并尝试把它转换成字符串。对于 IP 地址,这是没有问题的。但是对于端口,你也尝试把它转换为字符串,然后在 PlayerActivity 中尝试将这个字符串转换为整型,这可能会引起问题。当你从 MainActivity 向 PlayerActivity 传递端口号时,你应该首先将其转化为整型:
String ip = editTextIP.getText().toString();
int port = Integer.parseInt(editTextPort.getText().toString());
Intent intent = new Intent(MainActivity.this, PlayerActivity.class);
intent.putExtra("ip", ip);
intent.putExtra("port", port);
startActivity(intent);
UDP接收数据解压问题:在服务端,你使用了JPEG压缩来压缩图像,但在 Android 客户端的 PlayerActivity 中,你尝试使用InflaterInputStream来解压接收到的数据。这可能是因为数据不能正确解压,所以没有图像显示。你应该使用与服务端相同的解压方式,也就是直接解码JPEG图像:
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, packet.getLength());
imageView.setImageBitmap(bitmap);
接收数据更新UI问题:你尝试在接收线程中直接更新 UI,但这在 Android 中是禁止的。你需要使用 runOnUiThread 方法来更新 UI:
runOnUiThread(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
给一个看上去不太专业,但是实际情况下常出现的可能:Android Studio毕竟不是真机,可能真机就能跑了,之前做过一个app,Android Studio中连服务主机都连不上(确认Android studio虚拟机联网了,并且浏览器打开网站正常),结果,打包apk后,真机一试,没问题了😂🦊
建议你采用服务器进行类似的直播推流,将树莓派的视频推到服务器,安卓再拉取直播流
用wireshark抓包软件抓下包,看是不是传输过程丢包了?还是程序中有问题,数据包没发送出来~
可以在Android Studio中使用Debug工具来单步调试客户端代码,以查看它是否正确地接收了数据包。
使用第三方的视频播放库,例如Vitamio,来播放来自服务端的视频数据。
我先初步分析下,再有问题发我或者私信
服务端发送数据时设置了发送包压缩大小为30,而在客户端收到数据时没有对数据进行解压缩,因此无法正常解码。建议在客户端的代码中对数据进行解压缩,使用与服务端相同的压缩格式对数据进行解码。
其次,您没有提供客户端的相关代码,因此无法确定客户端代码的问题。您可以确认是否已正确获取IP地址和端口号,并且您的代码是否正确将接收到的数据解码为图像并进行显示。以下是一个简单的客户端代码示例,供您参考:
public class PlayerActivity extends AppCompatActivity {
private SurfaceView surfaceView;
private SurfaceHolder surfaceHolder;
private ImageView imageView;
private DatagramSocket clientSocket;
private String udpIp;
private int udpPort;
private boolean isRunning = true;
private ExecutorService executorService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
Intent intent = getIntent();
udpIp = intent.getStringExtra("udp_ip");
udpPort = intent.getIntExtra("udp_port", 0);
surfaceView = findViewById(R.id.surface_view);
imageView = findViewById(R.id.imageView);
surfaceHolder = surfaceView.getHolder();
executorService = Executors.newSingleThreadExecutor();
executorService.execute(new DataReceiver());
}
@Override
public void onDestroy() {
super.onDestroy();
isRunning = false;
executorService.shutdown();
try {
clientSocket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private class DataReceiver implements Runnable {
private Paint paint = new Paint();
private Bitmap bitmap;
public DataReceiver() {
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.WHITE);
paint.setTextSize(20);
}
@Override
public void run() {
int width = 640;
int height = 480;
int bufferSize = width * height * 3;
byte[] buffer = new byte[bufferSize];
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
try {
clientSocket = new DatagramSocket(udpPort);
while (isRunning) {
DatagramPacket packet = new DatagramPacket(buffer, bufferSize);
clientSocket.receive(packet);
byte[] data = packet.getData();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
try {
bitmap = BitmapFactory.decodeStream(byteArrayInputStream, null, options);
if (bitmap != null) {
Canvas canvas = surfaceHolder.lockCanvas();
if (canvas != null) {
canvas.drawBitmap(bitmap, 0, 0, null);
surfaceHolder.unlockCanvasAndPost(canvas);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
在以上代码中,DataReceiver 类是接收 UDP 数据的线程。由于视频数据是压缩的,需要使用 BitmapFactory 对象创建 Bitmap 对象,并将其绘制到画布上以显示图像。需要注意的是,这段代码仅供参考,您需要根据您的具体需求进行修改。
树莓派UDP传输视频到安卓APP
https://blog.csdn.net/Dontla/article/details/109844290
sudo apt-get install python-opencv
2. 在树莓派上编写Python代码,以打开摄像头并将视频流传输到UDP套接字。以下是一个示例代码:
import cv2
import socket
import pickle
import struct
# Set up socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
host_ip = '192.168.0.100' # Replace with your Android device's IP address
port = 9999
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Open camera
cap = cv2.VideoCapture(0)
while True:
# Read frame from camera
ret, frame = cap.read()
# Serialize frame
data = pickle.dumps(frame)
# Pack message and send
message = struct.pack("Q", len(data)) + data
client_socket.sendto(message, (host_ip, port))
# Release resources
cap.release()
client_socket.close()
这个代码将打开摄像头并使用OpenCV从中读取帧。然后,它将使用pickle模块将帧序列化为字节字符串,并使用struct模块将其打包为UDP数据包。最后,它将数据包发送到指定的IP地址和端口。
3. 在安卓应用程序中编写代码,以接收UDP数据包,并使用OpenCV显示视频流。以下是一个示例代码:
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.WindowManager;
import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;
import org.opencv.videoio.VideoCapture;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.ByteBuffer;
public class MainActivity extends AppCompatActivity {
private VideoCapture videoCapture;
private Mat frame;
private boolean isPlaying;
private BaseLoaderCallback loaderCallback = new BaseLoaderCallback(this) {
@Override
public void onManagerConnected(int status) {
switch (status) {
case LoaderCallbackInterface.SUCCESS:
videoCapture = new VideoCapture();
isPlaying = true;
new Thread(new Runnable() {
@Override
public void run() {
try {
DatagramSocket socket = new DatagramSocket(9999);
byte[] buffer = new byte[65507];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (isPlaying) {
socket.receive(packet);
byte[] data = packet.getData();
ByteBuffer bb = ByteBuffer.wrap(data);
long length = bb.getLong();
byte[] imageData = new byte[(int) length];
bb.get(imageData, 0, imageData.length);
frame = new Mat(480, 640, CvType.CV_8UC3);
frame.put(0, 0, imageData);
Imgproc.cvtColor(frame, frame, Imgproc.COLOR_RGB2BGR);
runOnUiThread(new Runnable() {
@Override
public void run() {
showFrame();
}
});
}
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
break;
default:
super.onManagerConnected(status);
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.activity_main);
}
@Override
protected void onResume() {
super.onResume();
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_4_0, this, loaderCallback);
}
@Override
protected void onPause() {
super.onPause();
isPlaying = false;
if (videoCapture != null) {
videoCapture.release();
videoCapture = null;
}
}
private void showFrame() {
if (frame != null) {
if (videoCapture.isOpened()) {
videoCapture.release();
}
videoCapture.open(0);
videoCapture.set(Highgui.CV_CAP_PROP_FRAME_WIDTH, 640);
videoCapture.set(Highgui.CV_CAP_PROP_FRAME_HEIGHT, 480);
videoCapture.grab();
videoCapture.retrieve(frame);
Imgproc.cvtColor(frame, frame, Imgproc.COLOR_BGR2RGB);
pycharm中可以接收到视频画面,而安卓模app中不行,首先要确保树莓派和安卓APP连接到同一个网络,并且网络连接正常。确保视频编码在树莓派和安卓APP上都支持,并且视频数据可以正确解码。或者更换tcp看看
可以采用直接推流的方式试试
搞个服务器,先上传到服务器,然后app再从服务器中请求视频