add first version
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'screens/camera_screen.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
@@ -7,116 +8,96 @@ void main() {
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
title: 'AISee Camera',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
home: const HomeScreen(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
}
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
title: const Text('AISee'),
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
//
|
||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||
// action in the IDE, or press "p" in the console), to see the
|
||||
// wireframe for each widget.
|
||||
mainAxisAlignment: .center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('You have pushed the button this many times:'),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
const Icon(
|
||||
Icons.visibility,
|
||||
size: 100,
|
||||
color: Colors.blue,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'AISee 视觉辅助',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'实时相机图像传输演示',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CameraScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.camera_alt, size: 28),
|
||||
label: const Text(
|
||||
'打开相机',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
'功能说明:\n'
|
||||
'• 拍照分析:拍摄单张照片并上传分析\n'
|
||||
'• 开始传输:每 500ms 自动捕获并上传\n'
|
||||
'• 切换:切换前后摄像头',
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black87,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
290
aisee_flutter/lib/screens/camera_screen.dart
Normal file
290
aisee_flutter/lib/screens/camera_screen.dart
Normal file
@@ -0,0 +1,290 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import '../services/camera_service.dart';
|
||||
import '../services/api_service.dart';
|
||||
|
||||
class CameraScreen extends StatefulWidget {
|
||||
const CameraScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CameraScreen> createState() => _CameraScreenState();
|
||||
}
|
||||
|
||||
class _CameraScreenState extends State<CameraScreen> {
|
||||
late CameraService _cameraService;
|
||||
late ApiService _apiService;
|
||||
bool _isProcessing = false;
|
||||
String _statusMessage = '准备就绪';
|
||||
int _frameCount = 0;
|
||||
int _uploadCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cameraService = CameraService();
|
||||
_apiService = ApiService();
|
||||
_initializeCamera();
|
||||
}
|
||||
|
||||
Future<void> _initializeCamera() async {
|
||||
// 请求相机权限
|
||||
final status = await Permission.camera.request();
|
||||
if (!status.isGranted) {
|
||||
setState(() {
|
||||
_statusMessage = '相机权限被拒绝';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化相机
|
||||
await _cameraService.initialize();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = '相机已就绪';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始实时传输
|
||||
void _startStreaming() {
|
||||
if (_cameraService.isStreaming) return;
|
||||
|
||||
setState(() {
|
||||
_statusMessage = '实时传输中...';
|
||||
_frameCount = 0;
|
||||
_uploadCount = 0;
|
||||
});
|
||||
|
||||
_cameraService.startStreaming((imageBytes) async {
|
||||
setState(() {
|
||||
_frameCount++;
|
||||
});
|
||||
|
||||
// 上传到后端(异步,不阻塞捕获)
|
||||
_uploadToBackend(imageBytes);
|
||||
});
|
||||
}
|
||||
|
||||
/// 停止实时传输
|
||||
void _stopStreaming() {
|
||||
_cameraService.stopStreaming();
|
||||
setState(() {
|
||||
_statusMessage = '已停止传输';
|
||||
});
|
||||
}
|
||||
|
||||
/// 上传图像到后端
|
||||
Future<void> _uploadToBackend(imageBytes) async {
|
||||
if (_isProcessing) return; // 防止并发上传
|
||||
|
||||
_isProcessing = true;
|
||||
|
||||
try {
|
||||
// 上传图像
|
||||
final result = await _apiService.uploadImage(imageBytes);
|
||||
|
||||
setState(() {
|
||||
_uploadCount++;
|
||||
_statusMessage = '已上传 $_uploadCount 帧';
|
||||
});
|
||||
|
||||
print('上传成功: ${result['image_id']}');
|
||||
} catch (e) {
|
||||
print('上传失败: $e');
|
||||
setState(() {
|
||||
_statusMessage = '上传失败: $e';
|
||||
});
|
||||
} finally {
|
||||
_isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 拍摄单张照片并分析
|
||||
Future<void> _captureAndAnalyze() async {
|
||||
setState(() {
|
||||
_statusMessage = '正在拍照...';
|
||||
});
|
||||
|
||||
final imageBytes = await _cameraService.takePicture();
|
||||
if (imageBytes == null) {
|
||||
setState(() {
|
||||
_statusMessage = '拍照失败';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_statusMessage = '正在分析...';
|
||||
});
|
||||
|
||||
try {
|
||||
// 上传并分析
|
||||
final result = await _apiService.uploadAndAnalyze(imageBytes);
|
||||
|
||||
setState(() {
|
||||
_statusMessage = '分析完成';
|
||||
});
|
||||
|
||||
// 显示结果
|
||||
_showAnalysisResult(result);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_statusMessage = '分析失败: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示分析结果
|
||||
void _showAnalysisResult(Map<String, dynamic> result) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('分析结果'),
|
||||
content: SingleChildScrollView(
|
||||
child: Text(result.toString()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cameraService.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('实时相机'),
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
body: _cameraService.isInitialized
|
||||
? _buildCameraView()
|
||||
: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCameraView() {
|
||||
return Stack(
|
||||
children: [
|
||||
// 相机预览
|
||||
Positioned.fill(
|
||||
child: CameraPreview(_cameraService.controller!),
|
||||
),
|
||||
|
||||
// 状态信息
|
||||
Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_statusMessage,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'捕获帧数: $_frameCount | 上传帧数: $_uploadCount',
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 控制按钮
|
||||
Positioned(
|
||||
bottom: 32,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// 拍照并分析按钮
|
||||
_buildControlButton(
|
||||
icon: Icons.camera_alt,
|
||||
label: '拍照分析',
|
||||
onPressed: _captureAndAnalyze,
|
||||
color: Colors.blue,
|
||||
),
|
||||
|
||||
// 开始/停止实时传输按钮
|
||||
_buildControlButton(
|
||||
icon: _cameraService.isStreaming ? Icons.stop : Icons.play_arrow,
|
||||
label: _cameraService.isStreaming ? '停止传输' : '开始传输',
|
||||
onPressed: _cameraService.isStreaming ? _stopStreaming : _startStreaming,
|
||||
color: _cameraService.isStreaming ? Colors.red : Colors.green,
|
||||
),
|
||||
|
||||
// 切换摄像头按钮
|
||||
_buildControlButton(
|
||||
icon: Icons.flip_camera_android,
|
||||
label: '切换',
|
||||
onPressed: () => _cameraService.switchCamera(),
|
||||
color: Colors.orange,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlButton({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onPressed,
|
||||
required Color color,
|
||||
}) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
onPressed: onPressed,
|
||||
backgroundColor: color,
|
||||
child: Icon(icon, size: 32),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black,
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
79
aisee_flutter/lib/services/api_service.dart
Normal file
79
aisee_flutter/lib/services/api_service.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../utils/app_config.dart';
|
||||
|
||||
class ApiService {
|
||||
late final Dio _dio;
|
||||
|
||||
ApiService() {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: AppConfig.apiBaseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
));
|
||||
|
||||
// 添加日志拦截器
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
requestBody: false, // 不打印请求体(图片太大)
|
||||
responseBody: true,
|
||||
error: true,
|
||||
));
|
||||
}
|
||||
|
||||
/// 上传图像到后端
|
||||
Future<Map<String, dynamic>> uploadImage(Uint8List imageBytes) async {
|
||||
try {
|
||||
final formData = FormData.fromMap({
|
||||
'file': MultipartFile.fromBytes(
|
||||
imageBytes,
|
||||
filename: 'image_${DateTime.now().millisecondsSinceEpoch}.jpg',
|
||||
),
|
||||
});
|
||||
|
||||
final response = await _dio.post(
|
||||
'/api/v1/images/upload',
|
||||
data: formData,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
print('上传图像失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 请求 AI 分析
|
||||
Future<Map<String, dynamic>> analyzeImage(String imageUrl) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/api/v1/analysis/analyze',
|
||||
data: {'image_url': imageUrl},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
print('分析图像失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 一步到位:上传并分析
|
||||
Future<Map<String, dynamic>> uploadAndAnalyze(Uint8List imageBytes) async {
|
||||
try {
|
||||
// 1. 上传图像
|
||||
final uploadResult = await uploadImage(imageBytes);
|
||||
final imageUrl = uploadResult['url'];
|
||||
|
||||
// 2. 请求分析
|
||||
final analysisResult = await analyzeImage(imageUrl);
|
||||
|
||||
return analysisResult;
|
||||
} catch (e) {
|
||||
print('上传并分析失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
aisee_flutter/lib/services/camera_service.dart
Normal file
158
aisee_flutter/lib/services/camera_service.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import '../utils/app_config.dart';
|
||||
|
||||
class CameraService extends ChangeNotifier {
|
||||
CameraController? _controller;
|
||||
List<CameraDescription> _cameras = [];
|
||||
bool _isInitialized = false;
|
||||
bool _isStreaming = false;
|
||||
Timer? _captureTimer;
|
||||
|
||||
CameraController? get controller => _controller;
|
||||
bool get isInitialized => _isInitialized;
|
||||
bool get isStreaming => _isStreaming;
|
||||
|
||||
/// 初始化相机
|
||||
Future<void> initialize() async {
|
||||
try {
|
||||
_cameras = await availableCameras();
|
||||
if (_cameras.isEmpty) {
|
||||
print('没有可用的相机');
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用后置摄像头
|
||||
final camera = _cameras.firstWhere(
|
||||
(camera) => camera.lensDirection == CameraLensDirection.back,
|
||||
orElse: () => _cameras.first,
|
||||
);
|
||||
|
||||
_controller = CameraController(
|
||||
camera,
|
||||
ResolutionPreset.medium, // 中等分辨率,平衡质量和性能
|
||||
enableAudio: false,
|
||||
imageFormatGroup: ImageFormatGroup.jpeg,
|
||||
);
|
||||
|
||||
await _controller!.initialize();
|
||||
_isInitialized = true;
|
||||
notifyListeners();
|
||||
|
||||
print('相机初始化成功');
|
||||
} catch (e) {
|
||||
print('相机初始化失败: $e');
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 拍摄单张照片
|
||||
Future<Uint8List?> takePicture() async {
|
||||
if (!_isInitialized || _controller == null) return null;
|
||||
|
||||
try {
|
||||
final XFile image = await _controller!.takePicture();
|
||||
final bytes = await image.readAsBytes();
|
||||
|
||||
// 压缩图像
|
||||
final compressedBytes = await _compressImage(bytes);
|
||||
return compressedBytes;
|
||||
} catch (e) {
|
||||
print('拍照失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始实时捕获(定时拍照)
|
||||
void startStreaming(Function(Uint8List) onImageCaptured) {
|
||||
if (_isStreaming) return;
|
||||
|
||||
_isStreaming = true;
|
||||
notifyListeners();
|
||||
|
||||
_captureTimer = Timer.periodic(AppConfig.captureInterval, (timer) async {
|
||||
if (!_isStreaming) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
final imageBytes = await takePicture();
|
||||
if (imageBytes != null) {
|
||||
onImageCaptured(imageBytes);
|
||||
}
|
||||
});
|
||||
|
||||
print('开始实时捕获,间隔: ${AppConfig.captureInterval.inMilliseconds}ms');
|
||||
}
|
||||
|
||||
/// 停止实时捕获
|
||||
void stopStreaming() {
|
||||
_isStreaming = false;
|
||||
_captureTimer?.cancel();
|
||||
_captureTimer = null;
|
||||
notifyListeners();
|
||||
print('停止实时捕获');
|
||||
}
|
||||
|
||||
/// 压缩图像
|
||||
Future<Uint8List> _compressImage(Uint8List bytes) async {
|
||||
return await compute(_compressImageIsolate, bytes);
|
||||
}
|
||||
|
||||
/// 在独立 Isolate 中压缩图像(避免阻塞 UI)
|
||||
static Uint8List _compressImageIsolate(Uint8List bytes) {
|
||||
// 解码图像
|
||||
img.Image? image = img.decodeImage(bytes);
|
||||
if (image == null) return bytes;
|
||||
|
||||
// 调整大小
|
||||
if (image.width > AppConfig.imageMaxWidth ||
|
||||
image.height > AppConfig.imageMaxHeight) {
|
||||
image = img.copyResize(
|
||||
image,
|
||||
width: AppConfig.imageMaxWidth,
|
||||
height: AppConfig.imageMaxHeight,
|
||||
);
|
||||
}
|
||||
|
||||
// 压缩为 JPEG
|
||||
final compressed = img.encodeJpg(image, quality: AppConfig.imageQuality);
|
||||
|
||||
print('图像压缩: ${bytes.length} bytes -> ${compressed.length} bytes');
|
||||
return Uint8List.fromList(compressed);
|
||||
}
|
||||
|
||||
/// 切换摄像头
|
||||
Future<void> switchCamera() async {
|
||||
if (_cameras.length < 2) return;
|
||||
|
||||
final currentLens = _controller?.description.lensDirection;
|
||||
final newCamera = _cameras.firstWhere(
|
||||
(camera) => camera.lensDirection != currentLens,
|
||||
orElse: () => _cameras.first,
|
||||
);
|
||||
|
||||
await _controller?.dispose();
|
||||
|
||||
_controller = CameraController(
|
||||
newCamera,
|
||||
ResolutionPreset.medium,
|
||||
enableAudio: false,
|
||||
imageFormatGroup: ImageFormatGroup.jpeg,
|
||||
);
|
||||
|
||||
await _controller!.initialize();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
@override
|
||||
void dispose() {
|
||||
stopStreaming();
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
13
aisee_flutter/lib/utils/app_config.dart
Normal file
13
aisee_flutter/lib/utils/app_config.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
class AppConfig {
|
||||
// API 配置
|
||||
static const String apiBaseUrl = 'http://10.0.2.2:8000'; // Android 模拟器访问本机
|
||||
// 如果使用真机,改为你的电脑 IP,例如:'http://192.168.1.100:8000'
|
||||
|
||||
// 图像配置
|
||||
static const int imageMaxWidth = 640;
|
||||
static const int imageMaxHeight = 480;
|
||||
static const int imageQuality = 85;
|
||||
|
||||
// 实时传输配置
|
||||
static const Duration captureInterval = Duration(milliseconds: 500); // 每 500ms 捕获一帧
|
||||
}
|
||||
Reference in New Issue
Block a user