|
|
|
@@ -0,0 +1,386 @@ |
|
|
|
import 'dart:io'; |
|
|
|
|
|
|
|
import 'package:camera/camera.dart'; |
|
|
|
import 'package:farm_tpf/main.dart'; |
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
import 'package:video_player/video_player.dart'; |
|
|
|
import 'package:path_provider/path_provider.dart'; |
|
|
|
|
|
|
|
class CameraHelper extends StatefulWidget { |
|
|
|
@override |
|
|
|
_CameraHelperState createState() => _CameraHelperState(); |
|
|
|
} |
|
|
|
|
|
|
|
/// Returns a suitable camera icon for [direction]. |
|
|
|
IconData getCameraLensIcon(CameraLensDirection direction) { |
|
|
|
switch (direction) { |
|
|
|
case CameraLensDirection.back: |
|
|
|
return Icons.camera_rear; |
|
|
|
case CameraLensDirection.front: |
|
|
|
return Icons.camera_front; |
|
|
|
case CameraLensDirection.external: |
|
|
|
return Icons.camera; |
|
|
|
} |
|
|
|
throw ArgumentError('Unknown lens direction'); |
|
|
|
} |
|
|
|
|
|
|
|
void logError(String code, String message) => |
|
|
|
print('Error: $code\nError Message: $message'); |
|
|
|
|
|
|
|
class _CameraHelperState extends State<CameraHelper> |
|
|
|
with WidgetsBindingObserver { |
|
|
|
CameraController controller; |
|
|
|
String imagePath; |
|
|
|
String videoPath; |
|
|
|
VideoPlayerController videoController; |
|
|
|
VoidCallback videoPlayerListener; |
|
|
|
bool enableAudio = true; |
|
|
|
int indexCamera = 0; |
|
|
|
|
|
|
|
@override |
|
|
|
void initState() { |
|
|
|
super.initState(); |
|
|
|
controller = |
|
|
|
CameraController(cameras[indexCamera], ResolutionPreset.medium); |
|
|
|
controller.initialize().then((_) { |
|
|
|
if (!mounted) { |
|
|
|
return; |
|
|
|
} |
|
|
|
setState(() {}); |
|
|
|
}); |
|
|
|
WidgetsBinding.instance.addObserver(this); |
|
|
|
} |
|
|
|
|
|
|
|
@override |
|
|
|
void dispose() { |
|
|
|
WidgetsBinding.instance.removeObserver(this); |
|
|
|
super.dispose(); |
|
|
|
} |
|
|
|
|
|
|
|
@override |
|
|
|
void didChangeAppLifecycleState(AppLifecycleState state) { |
|
|
|
// App state changed before we got the chance to initialize. |
|
|
|
if (controller == null || !controller.value.isInitialized) { |
|
|
|
return; |
|
|
|
} |
|
|
|
if (state == AppLifecycleState.inactive) { |
|
|
|
controller?.dispose(); |
|
|
|
} else if (state == AppLifecycleState.resumed) { |
|
|
|
if (controller != null) { |
|
|
|
onNewCameraSelected(controller.description); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); |
|
|
|
|
|
|
|
@override |
|
|
|
Widget build(BuildContext context) { |
|
|
|
return Scaffold( |
|
|
|
key: _scaffoldKey, |
|
|
|
body: SafeArea( |
|
|
|
top: false, |
|
|
|
bottom: false, |
|
|
|
child: Column( |
|
|
|
children: <Widget>[ |
|
|
|
Expanded( |
|
|
|
child: Container( |
|
|
|
child: Padding( |
|
|
|
padding: const EdgeInsets.all(1.0), |
|
|
|
child: Center( |
|
|
|
child: _cameraPreviewWidget(), |
|
|
|
), |
|
|
|
), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.black, |
|
|
|
border: Border.all( |
|
|
|
color: |
|
|
|
controller != null && controller.value.isRecordingVideo |
|
|
|
? Colors.redAccent |
|
|
|
: Colors.grey, |
|
|
|
width: |
|
|
|
controller != null && controller.value.isRecordingVideo |
|
|
|
? 1.0 |
|
|
|
: 0.0, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
_captureControlRowWidget(), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
/// Display the preview from the camera (or a message if the preview is not available). |
|
|
|
Widget _cameraPreviewWidget() { |
|
|
|
if (controller == null || !controller.value.isInitialized) { |
|
|
|
return const Text( |
|
|
|
'Đang mở camera ....', |
|
|
|
style: TextStyle( |
|
|
|
color: Colors.white, |
|
|
|
fontSize: 20.0, |
|
|
|
fontWeight: FontWeight.w900, |
|
|
|
), |
|
|
|
); |
|
|
|
} else { |
|
|
|
return AspectRatio( |
|
|
|
aspectRatio: controller.value.aspectRatio, |
|
|
|
child: CameraPreview(controller), |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// Display the control bar with buttons to take pictures and record videos. |
|
|
|
Widget _captureControlRowWidget() { |
|
|
|
return Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|
|
|
mainAxisSize: MainAxisSize.max, |
|
|
|
children: <Widget>[ |
|
|
|
IconButton( |
|
|
|
icon: Icon(Icons.arrow_back), |
|
|
|
onPressed: () { |
|
|
|
Get.back(); |
|
|
|
}), |
|
|
|
_cameraTogglesRowWidget(), |
|
|
|
IconButton( |
|
|
|
icon: const Icon(Icons.camera_alt), |
|
|
|
color: Colors.blue, |
|
|
|
onPressed: controller != null && |
|
|
|
controller.value.isInitialized && |
|
|
|
!controller.value.isRecordingVideo |
|
|
|
? onTakePictureButtonPressed |
|
|
|
: null, |
|
|
|
), |
|
|
|
IconButton( |
|
|
|
icon: const Icon(Icons.videocam), |
|
|
|
color: Colors.blue, |
|
|
|
onPressed: controller != null && |
|
|
|
controller.value.isInitialized && |
|
|
|
!controller.value.isRecordingVideo |
|
|
|
? onVideoRecordButtonPressed |
|
|
|
: null, |
|
|
|
), |
|
|
|
IconButton( |
|
|
|
icon: controller != null && controller.value.isRecordingPaused |
|
|
|
? Icon(Icons.play_arrow) |
|
|
|
: Icon(Icons.pause), |
|
|
|
color: Colors.blue, |
|
|
|
onPressed: controller != null && |
|
|
|
controller.value.isInitialized && |
|
|
|
controller.value.isRecordingVideo |
|
|
|
? (controller != null && controller.value.isRecordingPaused |
|
|
|
? onResumeButtonPressed |
|
|
|
: onPauseButtonPressed) |
|
|
|
: null, |
|
|
|
), |
|
|
|
IconButton( |
|
|
|
icon: const Icon(Icons.stop), |
|
|
|
color: Colors.red, |
|
|
|
onPressed: controller != null && |
|
|
|
controller.value.isInitialized && |
|
|
|
controller.value.isRecordingVideo |
|
|
|
? onStopButtonPressed |
|
|
|
: null, |
|
|
|
) |
|
|
|
], |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
/// Display a row of toggle to select the camera (or a message if no camera is available). |
|
|
|
Widget _cameraTogglesRowWidget() { |
|
|
|
if (cameras.isEmpty) { |
|
|
|
return const Text('Không có camera'); |
|
|
|
} else { |
|
|
|
bool disableSwitch = |
|
|
|
controller != null && controller.value.isRecordingVideo; |
|
|
|
if (indexCamera == cameras.length - 1) { |
|
|
|
indexCamera = 0; |
|
|
|
} else { |
|
|
|
indexCamera++; |
|
|
|
} |
|
|
|
return IconButton( |
|
|
|
icon: Icon( |
|
|
|
Icons.switch_camera, |
|
|
|
color: Colors.green, |
|
|
|
), |
|
|
|
onPressed: disableSwitch == true |
|
|
|
? null |
|
|
|
: () { |
|
|
|
onNewCameraSelected(cameras[indexCamera]); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); |
|
|
|
|
|
|
|
void showInSnackBar(String message) { |
|
|
|
_scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(message))); |
|
|
|
} |
|
|
|
|
|
|
|
void onNewCameraSelected(CameraDescription cameraDescription) async { |
|
|
|
if (controller != null) { |
|
|
|
await controller.dispose(); |
|
|
|
} |
|
|
|
controller = CameraController( |
|
|
|
cameraDescription, |
|
|
|
ResolutionPreset.medium, |
|
|
|
enableAudio: enableAudio, |
|
|
|
); |
|
|
|
|
|
|
|
// If the controller is updated then update the UI. |
|
|
|
controller.addListener(() { |
|
|
|
if (mounted) setState(() {}); |
|
|
|
if (controller.value.hasError) { |
|
|
|
showInSnackBar('Lỗi camera: ${controller.value.errorDescription}'); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
try { |
|
|
|
await controller.initialize(); |
|
|
|
} on CameraException catch (e) { |
|
|
|
_showCameraException(e); |
|
|
|
} |
|
|
|
|
|
|
|
if (mounted) { |
|
|
|
setState(() {}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void onTakePictureButtonPressed() { |
|
|
|
takePicture().then((String filePath) { |
|
|
|
if (mounted) { |
|
|
|
setState(() { |
|
|
|
imagePath = filePath; |
|
|
|
videoController?.dispose(); |
|
|
|
videoController = null; |
|
|
|
}); |
|
|
|
if (filePath != null) print('Picture saved to $filePath'); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
void onVideoRecordButtonPressed() { |
|
|
|
startVideoRecording().then((String filePath) { |
|
|
|
if (mounted) setState(() {}); |
|
|
|
if (filePath != null) print('Saving video to $filePath'); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
void onStopButtonPressed() { |
|
|
|
stopVideoRecording().then((_) { |
|
|
|
if (mounted) setState(() {}); |
|
|
|
print('Video recorded to: $videoPath'); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
void onPauseButtonPressed() { |
|
|
|
pauseVideoRecording().then((_) { |
|
|
|
if (mounted) setState(() {}); |
|
|
|
print('Video recording paused'); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
void onResumeButtonPressed() { |
|
|
|
resumeVideoRecording().then((_) { |
|
|
|
if (mounted) setState(() {}); |
|
|
|
print('Video recording resumed'); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
Future<String> startVideoRecording() async { |
|
|
|
if (!controller.value.isInitialized) { |
|
|
|
showInSnackBar('Vui lòng chọn camera'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
final Directory extDir = await getApplicationDocumentsDirectory(); |
|
|
|
final String dirPath = '${extDir.path}/Movies/tpf'; |
|
|
|
await Directory(dirPath).create(recursive: true); |
|
|
|
final String filePath = '$dirPath/${timestamp()}.mp4'; |
|
|
|
|
|
|
|
if (controller.value.isRecordingVideo) { |
|
|
|
// A recording is already started, do nothing. |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
videoPath = filePath; |
|
|
|
await controller.startVideoRecording(filePath); |
|
|
|
} on CameraException catch (e) { |
|
|
|
_showCameraException(e); |
|
|
|
return null; |
|
|
|
} |
|
|
|
return filePath; |
|
|
|
} |
|
|
|
|
|
|
|
Future<void> stopVideoRecording() async { |
|
|
|
if (!controller.value.isRecordingVideo) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
await controller.stopVideoRecording(); |
|
|
|
} on CameraException catch (e) { |
|
|
|
_showCameraException(e); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Future<void> pauseVideoRecording() async { |
|
|
|
if (!controller.value.isRecordingVideo) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
await controller.pauseVideoRecording(); |
|
|
|
} on CameraException catch (e) { |
|
|
|
_showCameraException(e); |
|
|
|
rethrow; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Future<void> resumeVideoRecording() async { |
|
|
|
if (!controller.value.isRecordingVideo) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
await controller.resumeVideoRecording(); |
|
|
|
} on CameraException catch (e) { |
|
|
|
_showCameraException(e); |
|
|
|
rethrow; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Future<String> takePicture() async { |
|
|
|
if (!controller.value.isInitialized) { |
|
|
|
showInSnackBar('Vui lòng chọn camera'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
final Directory extDir = await getApplicationDocumentsDirectory(); |
|
|
|
final String dirPath = '${extDir.path}/Pictures/tpf'; |
|
|
|
await Directory(dirPath).create(recursive: true); |
|
|
|
final String filePath = '$dirPath/${timestamp()}.jpg'; |
|
|
|
|
|
|
|
if (controller.value.isTakingPicture) { |
|
|
|
// A capture is already pending, do nothing. |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
await controller.takePicture(filePath); |
|
|
|
} on CameraException catch (e) { |
|
|
|
_showCameraException(e); |
|
|
|
return null; |
|
|
|
} |
|
|
|
return filePath; |
|
|
|
} |
|
|
|
|
|
|
|
void _showCameraException(CameraException e) { |
|
|
|
logError(e.code, e.description); |
|
|
|
showInSnackBar('Lỗi: ${e.code}\n${e.description}'); |
|
|
|
} |
|
|
|
} |