การบันทึกรูปภาพ Image หรือวิดีโอ Video ใน Flutter

บทความ เมื่อไม่กี่สัปดาห์ โดย Ninenik Narkdee
camera flutter image picker

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ camera flutter image_picker

ดูแล้ว 155 ครั้ง


เนื้อหานี้จะขอต่อยอดจากตอนที่แล้ว เกี่ยวกับการจัดการไฟล์
ซึ่งตอนที่แล้วเราได้รู้จักการจัดการเกี่ยวกับไฟล์เสียงหรือ auido ไฟล์
เนื้อหานี้เราจะมาดูเกี่ยวกับการจัดการไฟล์รูปภาพ และไฟล์วิดีโอ ซึ่ง
สำหรับไฟล์รูปภาพ เราได้มีเนื้อหาบ้างแล้วเกี่ยวกับการแสดงรูปในแบบ
Photo Gallery นั่นคือนำข้อมูลรูปภาพจากไฟล์ข้อมูลที่เรามีอยู่แล้ว
มาแสดง แต่ในตอนนี้เราจะพูดถึงการบันทึกหรือภารถ่ายภาพและบันทึก
เป็นไฟล์ในแอป
 
ทบทวนเกี่ยวกับ Photo Gallery ได้ที่
สร้าง Photo Gallery จาก API อย่างง่าย ใน Flutter http://niik.in/1071
 
นอกจากรูปภาพหรือไฟล์ภาพแล้ว เราจะพูดถึงการบันทึกวิดีโอหรือไฟล์ video ซึ่งเนื้อหา
เกี่ยวกับการแสดงวิดีโอนั้นเราพูดไปแล้วในบทความด้านล่าง
 
การจัดการและใช้งาน เกี่ยวกับ Video ใน Flutter ตอนที่ 1 http://niik.in/1113
ประยุกต์แสดงคลิป Video คล้าย TikTok ใน Flutter ตอนที่ 2 http://niik.in/1114
 
ซึ่งเป็นการนำข้อมูลไฟล์วิดีโอที่เรามีอยู่แล้วมาใช้งานหรือแสดง แต่ในบทความนี้เราจะพูดถึงการบันทึก
ไฟล์วิดีโอผ่านกล้องหรือ camera เพื่อใช้งานในแอป  และแน่นอนว่าเราจะมีการนำโค้ดจากบทความ
ที่เกี่ยวข้องมาใช้งาน เช่นการเปิดแสดงรูปภาพที่เราบันทึกไปแล้ว หรือการเล่นไฟล์วิดีโอที่เราได้บันทึก
ไป แบบนี้เป็นต้น
 
 

การกำหนดการขอสิทธิ์เข้าถึงการใช้งานข้อมูล

    ก่อนที่จะลงรายละเอียด สิ่งแรกที่เราต้องทำคือการขอสิทธิ์การเข้าถึงส่วนจัดการต่างๆ ในบทความนี้
จะมีเนื้อหาจากตอนที่แล้วด้วย ก็จะมีทั้ง การบันทึกเสียง การใช้กล้อง การอ่านไฟล์ เขียนไฟล์ ให้เรา
กำหนดดังนี้
 
ไฟล์ android > app > src > main > AndroidManifest.xml
 
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28"/>
    <!-- For Android 13+ -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
    <application ....
....
</manifest>
 
นอกจากการขอสิทธิ์ต่างเหล่านี้ในไฟล์ AndroidManifest.xml ในการกำหนดนี้แล้ว  เรายังอาจจะ
ต้องกำหนดการขอใช้สิทธิ์ในการใช้งานตอนรันแอป (runtime permission) เพิ่มด้วย
 

ติดตั้ง package ที่จำเป็นเพิ่มเติม ตามรายการด้านล่าง

    แพ็กเก็จที่จำเป็นต้องติดตั้งเพิ่มเติม สำหรับการทำงานมีดังนี้
 
  font_awesome_flutter: ^10.7.0
  path_provider: ^2.1.4
  record: ^5.1.2
  permission_handler: ^11.3.1
  audioplayers: ^6.1.0
  http: ^1.2.2 
  intl: ^0.17.0
  file_picker: ^8.1.2
  camera: ^0.11.0+2
  image_picker: ^1.1.2
  video_player: ^2.9.1
 
ในการบันทึกวิดีโอหรือรูปภาพด้วยกล้อง เราจะแนะนำการใช้งานทั้งสอง package คือ image_picker
และ camera โดยตัว image_picker จะมีรูปแบบการใช้งานหรือการตั้งค่าที่ง่ายกว่า รองรับทั้งการ
ถ่ายรูปด้วยกล้องและการดึงรูปภาพจาก gallery  ในขณะที่ camera จะรองรับการปรับแต่งที่มากกว่า
แต่ก็จะมาพร้อมกับการตั้งค่าที่ซับซ้อนขึ้น ดังนั้น เราเลือกใช้ได้ตามความเหมาะสมกับการใช้งาน
 
 

การใช้งาน Image_Picker โหลดหรือบันทึกไฟล์ภาพและวิดีโอ

    ในตัวอย่างเราจะสร้างไฟล์หน้าสำหรับใช้งาน image_picker คำอธิบายแสดงในโค้ด โดยในโค้ดจะ
เป็นการใช้งานฟังก์ชั่นเกี่ยวกับการโหลดไฟล์ภาพหรือไฟล์วิดีโอจาก gallery และการบันทึกไฟล์ หรือ
ไฟล์วิดีโอจากกล้อง camera ให้เป็นแนวทางนำไปปรับใช้งานได้
 

ไฟล์ imagepicker.dart

 
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

class Picker extends StatefulWidget {
  static const routeName = '/picker';

  const Picker({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _PickerState();
  }
}

class _PickerState extends State<Picker> {
  final ImagePicker _picker = ImagePicker();
  XFile? _videoFile;
  XFile? _imageFile; // ตัวแปรสำหรับเก็บไฟล์รูปภาพที่เลือก

  // ฟังก์ชันสำหรับการเลือกวิดีโอ
  Future<void> pickVideo(ImageSource source) async {
    XFile? video = await _picker.pickVideo(source: source);
    setState(() {
      _videoFile = video;
    });
  }  

  // ฟังก์ชันสำหรับการเลือกภาพ
  Future<void> pickImage(ImageSource source) async {
    XFile? image = await _picker.pickImage(source: source);
    setState(() {
      _imageFile = image;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Image & Video Picker'),
      ),
      body: Center(
          child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('Image & Video Picker Screen'),
          // แสดงภาพที่เลือก
          _imageFile != null
              ? Image.file(
                  File(_imageFile!.path),
                  height: 200,
                )
              : Text("No image selected"),
          // แสดงวิดีโอที่เลือก
          _videoFile != null
              ? Text("Video saved to: ${_videoFile!.path}")
              : Text("No video selected"),
          // ปุ่มสำหรับเลือกภาพจากแกลเลอรี
          ElevatedButton(
            onPressed: () => pickImage(ImageSource.gallery),
            child: Text('Pick Image from Gallery'),
          ),
          // ปุ่มสำหรับเลือกภาพจากกล้อง
          ElevatedButton(
            onPressed: () => pickImage(ImageSource.camera),
            child: Text('Capture Image from Camera'),
          ),
          // ปุ่มสำหรับเลือกวิดีโอจากแกลเลอรี
          ElevatedButton(
            onPressed: () => pickVideo(ImageSource.gallery),
            child: Text('Pick Video from Gallery'),
          ),          
          // ปุ่มสำหรับบันทึกวิดีโอ
          ElevatedButton(
            onPressed: () => pickVideo(ImageSource.camera),
            child: Text('Record Video from Camera'),
          ),
        ],
      )),
    );
  }
}
 

ผลลัพธ์ที่ได้


 
 
จากตัวอย่างโค้ด จะเห็นว่ามีวิธีการเรียกใช้งานที่ค่อนข้างง่ายไม่ยุ่งยาก หลักการงานคือ ถ้าเราเลือกเป็น
การโหลดไฟล์ภาพหรือวิดีโอจาก gallery หรือจากในเครื่อง ข้อมูลไฟล์ก็จะถูกนำไปเก็บในโฟลเดอร์
แคช (cache) โดยมีโฟลเดอร์สำหรับแคซไฟล์เก็บไฟล์เหล่านั้นไว้อีกที  ในขณะที่ถ้าเราเลือกเป้นการ
ถ่ายรูปหรือวิดีโอจากกล้อง ก็จะเป็นการการเก็บไว้ในโฟลเดอร์แคชเช่นกัน แต่ไม่มีโฟลเดอร์เก็บไฟล์นั้น
ในตัวอย่าง ถ้าเป็นไฟล์รูป เราสามารถใช้การตรวจสอบชื่อไฟล์และแสดงรูปได้ทันที ในขณะที่ถ้าเป็นไฟล์
วิดีโอ เราจะแสดงที่ path หรือตำแหน่งของไฟล์ที่ถูกบันทึกไว้  ซึ่งนี้ทำให้เราเห็นแนวทางการทำงานและ
การจัดเก็บไฟล์เท่านั้น หากนำไปใช้งานจริง เราอาจจะต้องนำไฟล์เหล่านี้ไปไว้ที่ app_flutter เพื่อนำ
ไปใช้งานในแอปต่อไป เพราะไฟล์ในโฟลเดอร์แคชจะเป็นไฟล์ชั่วคราว อาจจะโดนลบจากระบบในตอนไหน
ก็เป็นได้
    เราจะปรับแต่งโค้ดและแยกเป็นอีกตัวอย่างเพื่อให้เปรียบเทียบกัน โดยตัวแก้ไข เราจะมีการคัดลอกไฟล์
จากแคชมายังโฟลเดอร์ app_flutter หรือโฟลเดอร์ข้อมูลของแอป และมีการกำหนดชื่อไฟล์ใหม่ใน
รูปแบบ video_ddMMyyyyTHHmmss และ image_ddMMyyyyTHHmmss ดังโค้ดด้านล่าง
 

ไฟล์ imagepicker.dart (ปรับปรุง)

 
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:path_provider/path_provider.dart';
import 'package:intl/intl.dart';

class Picker extends StatefulWidget {
  static const routeName = '/picker';

  const Picker({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _PickerState();
  }
}

class _PickerState extends State<Picker> {
  final ImagePicker _picker = ImagePicker();
  XFile? _videoFile;
  XFile? _imageFile; // ตัวแปรสำหรับเก็บไฟล์รูปภาพที่เลือก
  Directory? _currentFolder;

  @override
  void initState() {
    super.initState();
    _getAppDirectory();
  }

  // ฟังก์ชันเพื่อให้ได้ path ของโฟลเดอร์แอป
  Future<void> _getAppDirectory() async {
    _currentFolder = await getApplicationDocumentsDirectory();
  }

  // ฟังก์ชันสำหรับการเลือกวิดีโอ
  Future<void> pickVideo(ImageSource source) async {
    XFile? video = await _picker.pickVideo(source: source);

    try {
      if (await Permission.storage.request().isGranted) {
        if (video != null) {
          // สร้างชื่อและ path ใหม่สำหรับไฟล์วิดีโอ
          String timestamp =
              DateFormat('ddMMyyyyTHHmmss').format(DateTime.now());
          String newFileName = 'video_$timestamp';
          String newPath =
              "${_currentFolder!.path}/$newFileName${video.name.substring(video.name.lastIndexOf('.'))}";

          // กำหนด file object
          var cacheFile = File(video.path);

          // คัดลอกไฟล์จาก cache ไปยัง path ใหม่ใน app
          await cacheFile.copy(newPath);

          // ใช้ File เพื่อเข้าถึงข้อมูลเกี่ยวกับไฟล์
          File newFile = File(newPath);
          int fileSize = await newFile.length(); // ใช้ await เพื่อรับขนาดไฟล์

          // แสดงข้อมูลไฟล์
          print("Video file copied to: $newPath");
          print("File name: ${video.name}");
          print("File size: $fileSize bytes");
          print("File path: ${video.path}");

          // โหลดข้อมูลใหม่อีกครั้งถ้าต้องการ
          setState(() {
            _videoFile = video; // เก็บไฟล์วิดีโอใน state
          });
        } else {
          print("No video selected.");
        }
      } else {
        print("Storage permission is denied.");
      }
    } catch (e) {
      print("Error occurred: $e");
    }
  }

  // ฟังก์ชันสำหรับการเลือกภาพ
  Future<void> pickImage(ImageSource source) async {
    XFile? image = await _picker.pickImage(source: source);

    try {
      if (await Permission.storage.request().isGranted) {
        if (image != null) {
          // สร้าง ชื่อใหม่ และ path ใหม่สำหรับไฟล์ภาพ
          String timestamp =
              DateFormat('ddMMyyyyTHHmmss').format(DateTime.now());
          String newFileName = 'image_$timestamp';
          String newPath =
              "${_currentFolder!.path}/$newFileName${image.name.substring(image.name.lastIndexOf('.'))}";

          // กำหนด file object
          var cacheFile = File(image.path);

          // คัดลอกไฟล์จาก cache ไปยัง path ใหม่ใน app
          await cacheFile.copy(newPath);

          // ใช้ File เพื่อเข้าถึงข้อมูลเกี่ยวกับไฟล์
          File newFile = File(newPath);
          int fileSize = await newFile.length(); // ใช้ await เพื่อรับขนาดไฟล์

          // แสดงข้อมูลไฟล์
          print("Image file copied to: $newPath");
          print("File name: ${image.name}");
          print("File size: $fileSize bytes");
          print("File path: ${image.path}");

          // โหลดข้อมูลใหม่อีกครั้งถ้าต้องการ
          setState(() {
            _imageFile = image; // เก็บไฟล์ภาพใน state
          });
        } else {
          print("No image selected.");
        }
      } else {
        print("Storage permission is denied.");
      }
    } catch (e) {
      print("Error occurred: $e");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Image & Video Picker'),
      ),
      body: Center(
          child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('Image & Video Picker Screen'),
          // แสดงภาพที่เลือก
          _imageFile != null
              ? Image.file(
                  File(_imageFile!.path),
                  height: 200,
                )
              : Text("No image selected"),
          // แสดงวิดีโอที่เลือก
          _videoFile != null
              ? Text("Video saved to: ${_videoFile!.path}")
              : Text("No video selected"),
          // ปุ่มสำหรับเลือกภาพจากแกลเลอรี
          ElevatedButton(
            onPressed: () => pickImage(ImageSource.gallery),
            child: Text('Pick Image from Gallery'),
          ),
          // ปุ่มสำหรับเลือกภาพจากกล้อง
          ElevatedButton(
            onPressed: () => pickImage(ImageSource.camera),
            child: Text('Capture Image from Camera'),
          ),
          // ปุ่มสำหรับเลือกวิดีโอจากแกลเลอรี
          ElevatedButton(
            onPressed: () => pickVideo(ImageSource.gallery),
            child: Text('Pick Video from Gallery'),
          ),
          // ปุ่มสำหรับบันทึกวิดีโอ
          ElevatedButton(
            onPressed: () => pickVideo(ImageSource.camera),
            child: Text('Record Video from Camera'),
          ),
        ],
      )),
    );
  }
}
 

ผลลัพธ์ที่ได้


 
 
จะเห็นว่าไฟล์ที่เราทำการเลือกเข้ามาหรือถ่ายด้วยกล้องจะถูกคัดลอกมาไว้ในโฟลเดอร์ app_flutter
พร้อมใช้รูปแบบชื่อไฟล์ใหม่ตามที่เราต้องการ
 
 

การใช้งาน Camera บันทึกไฟล์ภาพและวิดีโอ

    ส่วนของการใช้งาน camera plugin ถึงแม้จะรองรับการจัดการที่หลากหลายแต่ก็ไม่รองรับการ
เลือกหรือเรียกดูไฟล์จาก gallery ได้ จะเป็นลักษณะการใช้งานจากกล้องเป็นหลัก คือการบันทึกรูปภาพ
หรือการบันทึกไฟล์วิดีโอจากกล้อง โค้ดด้านล่างเป็นตัวอย่างอย่างง่ายในการใช้งาน ที่เราสามารถแสดง
พรีวิวหน้าจอกล้อง โดยสามารถกดเพื่อบันทึกวิดีโอ หรือถ่ายเป็นไฟล์รูปภาพแล้วบันทึกไว้ในโฟลเดอร์
app_flutter คำอธิบายแสดงในโค้ด
 

ไฟล์ camera.dart

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:intl/intl.dart';

class Camera extends StatefulWidget {
  static const routeName = '/camara';

  const Camera({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _CameraState();
  }
}

class _CameraState extends State<Camera> {
  // ส่วนควบคุมกล้อง
  CameraController? _cameraController;
  List<CameraDescription>? cameras;
  bool isRecording = false;
  String? _imagePath;
  String? _videoPath;
  Directory? _currentFolder;

  @override
  void initState() {
    super.initState();
    _getAppDirectory();
    initializeCamera();
  }

  // ฟังก์ชันเพื่อให้ได้ path ของโฟลเดอร์แอป
  Future<void> _getAppDirectory() async {
    _currentFolder = await getApplicationDocumentsDirectory();
  }

  // เตรียมความพร้อมในส่วนของกล้อง
  Future<void> initializeCamera() async {
    // Request camera permission
    final cameraStatus = await Permission.camera.request();
    if (cameraStatus.isGranted) {
      cameras = await availableCameras();
      _cameraController = CameraController(cameras![0], ResolutionPreset.high);
      await _cameraController!.initialize();
      setState(() {});
    } else {
      // Handle the case when the user denies permission
      print("Camera permission denied.");
    }
  }

  // ฟังก์ชั่นทำงานการบันทึกวิดีโอจากกล้อง
  Future<void> startVideoRecording() async {
    try {
      if (await Permission.storage.request().isGranted) {
        if (_cameraController != null &&
            !_cameraController!.value.isRecordingVideo) {
          // สร้างชื่อไฟล์วิดีโอ
          String timestamp =
              DateFormat('ddMMyyyyTHHmmss').format(DateTime.now());
          _videoPath = '${_currentFolder!.path}/video_$timestamp.mp4';

          await _cameraController!.startVideoRecording();
          setState(() {
            isRecording = true;
          });
        }
      } else {
        print("Storage permission is denied.");
      }
    } catch (e) {
      print("Error occurred: $e");
    }
  }

  // ฟังก์ชั่นหยุดการบันทึกวิดีโอ
  Future<void> stopVideoRecording() async {
    if (_cameraController != null &&
        _cameraController!.value.isRecordingVideo) {
      XFile videoFile = await _cameraController!.stopVideoRecording();
      setState(() {
        isRecording = false;
      });
      print("Video saved to: ${videoFile.path}");

      // บันทึกไฟล์วิดีโอไปยัง app โฟลเดอร์ app_flutter
      String newVideoPath =
          '${_currentFolder!.path}/video_${DateFormat('ddMMyyyyTHHmmss').format(DateTime.now())}.mp4';
      await videoFile.saveTo(newVideoPath);
      print("Video moved to: $newVideoPath");
    }
  }

  // ฟังก์ชั่นสำหรับบันทึกไฟล์ภาพ หรือจับภาพจากกล้อง
  Future<void> takePicture() async {
    try {
      if (await Permission.storage.request().isGranted) {
        if (_cameraController != null &&
            !_cameraController!.value.isTakingPicture) {
          // สร้างชื่อไฟล์รูปภาพ
          String timestamp =
              DateFormat('ddMMyyyyTHHmmss').format(DateTime.now());
          _imagePath = '${_currentFolder!.path}/image_$timestamp.jpg';

          // ถ่ายรูป
          XFile imageFile = await _cameraController!.takePicture();

          // บันทึกไฟล์รูปไปยัง app โฟลเดอร์ app_flutter
          await imageFile.saveTo(_imagePath!);
          print("Image saved to: $_imagePath");
        }
      } else {
        print("Storage permission is denied.");
      }
    } catch (e) {
      print("Error occurred: $e");
    }
  }

  @override
  void dispose() {
    _cameraController?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Camera'),
      ),
      body: SingleChildScrollView(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            _cameraController != null && _cameraController!.value.isInitialized
                ? Column(
                    children: [
                      CameraPreview(_cameraController!),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          ElevatedButton(
                            onPressed: isRecording
                                ? stopVideoRecording
                                : startVideoRecording,
                            child: Text(isRecording
                                ? 'Stop Recording'
                                : 'Start Recording'),
                          ),
                          ElevatedButton(
                            onPressed: takePicture,
                            child: Text('Take Picture'),
                          ),
                        ],
                      ),
                    ],
                  )
                : Center(child: CircularProgressIndicator()),
          ],
        ),
      ),
    );
  }
}
 

ผลลัพธ์ที่ได้


 
 
ข้างต้นเป็นรูปแบบการใช้งานอย่างง่าย ในกรณีเราไม่ต้องการโค้ดหรือการทำงานที่ซับซ้อน อย่างไรก็ดี
ด้วยความสามารถของตัว plugin ยังสามารถปรับแต่งส่วนต่างๆ หากต้องการได้ ไม่ว่าจะเป็นการกำหนด
เกี่ยวกับกล้องหน้ากล้องหลัง การใช้ไฟล์แฟลซ การโฟกัส เหล่านี้เป็นต้น ดูตัวอย่างรูปแบบปรับแต่ง เพิ่ม
เติมได้ที่โค้ดด้านล่าง สามารถเลือกเอาส่วนที่ต้องการไปประยุกต์ใช้งานได้
 

ไฟล์ camera.dart (ปรับแต่งเพิ่มเติมจากโค้ดตัวอย่าง)

import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:camera/camera.dart';
import 'package:video_player/video_player.dart';
import 'package:flutter/scheduler.dart';

class Camera extends StatefulWidget {
  static const routeName = '/camara';

  const Camera({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _CameraState();
  }
}

class _CameraState extends State<Camera>
    with WidgetsBindingObserver, TickerProviderStateMixin {
  CameraController? controller;
  bool _cameraReady = false;
  XFile? imageFile;
  XFile? videoFile;
  VideoPlayerController? videoController;
  VoidCallback? videoPlayerListener;
  // ค่าสำหรับกำหนดมีเสียงหรือไม่มีสียงเมื่อบันทึกวิดีโอ
  bool enableAudio = true;
  // ค่าสำหรับการกำหนดการชดเชยแสง
  double _minAvailableExposureOffset = 0.0;
  double _maxAvailableExposureOffset = 0.0;
  double _currentExposureOffset = 0.0;
  // ค่าจัดการเกี่ยวกับ animation การซ่อนแสดงปุ่มต่างๆ
  late AnimationController _flashModeControlRowAnimationController;
  late Animation<double> _flashModeControlRowAnimation;
  late AnimationController _exposureModeControlRowAnimationController;
  late Animation<double> _exposureModeControlRowAnimation;
  late AnimationController _focusModeControlRowAnimationController;
  late Animation<double> _focusModeControlRowAnimation;
  // ค่าเกี่ยวกับการซูม
  double _minAvailableZoom = 1.0;
  double _maxAvailableZoom = 1.0;
  double _currentScale = 1.0;
  double _baseScale = 1.0;

  // เก็บจำนวนนิ้วผู้ใช้บนหน้าจอที่กำลังกดหรือใช้งานอยู่
  int _pointers = 0;

  @override
  void initState() {
    super.initState();
    initialCamera();
    // ส่วนสำหรับตรวจจับสถาะของ state เพื่อใช้งาน AppLifecycle
    WidgetsBinding.instance.addObserver(this);

    // ส่วนจัดการ animation การแสดงของปุ่มต่างๆ
    _flashModeControlRowAnimationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _flashModeControlRowAnimation = CurvedAnimation(
      parent: _flashModeControlRowAnimationController,
      curve: Curves.easeInCubic,
    );
    _exposureModeControlRowAnimationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _exposureModeControlRowAnimation = CurvedAnimation(
      parent: _exposureModeControlRowAnimationController,
      curve: Curves.easeInCubic,
    );
    _focusModeControlRowAnimationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _focusModeControlRowAnimation = CurvedAnimation(
      parent: _focusModeControlRowAnimationController,
      curve: Curves.easeInCubic,
    );
  }

  // ฟังก์ชั่นเตรียมพร้อมกล้องเพื่อใช้งาน
  Future<void> initialCamera() async {
    try {
      WidgetsFlutterBinding.ensureInitialized();
      _cameras = await availableCameras();
      if (_cameras.isNotEmpty) {
        setState(() {
          _cameraReady = true;
        });
      }
    } on CameraException catch (e) {
      _logError(e.code, e.description);
    }
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _flashModeControlRowAnimationController.dispose();
    _exposureModeControlRowAnimationController.dispose();
    super.dispose();
  }

  // #docregion AppLifecycle
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    final CameraController? cameraController = controller;

    // App state changed before we got the chance to initialize.
    if (cameraController == null || !cameraController.value.isInitialized) {
      return;
    }

    if (state == AppLifecycleState.inactive) {
      cameraController.dispose();
    } else if (state == AppLifecycleState.resumed) {
      _initializeCameraController(cameraController.description);
    }
  }
  // #enddocregion AppLifecycle

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('การใข้งานกล้อง'),
      ),
      body: Column(
        children: <Widget>[
          Expanded(
            child: Container(
              decoration: BoxDecoration(
                color: Colors.black,
                border: Border.all(
                  color:
                      controller != null && controller!.value.isRecordingVideo
                          ? Colors.redAccent
                          : Colors.grey,
                  width: 3.0,
                ),
              ),
              child: Padding(
                padding: const EdgeInsets.all(1.0),
                child: Center(
                  child: _cameraPreviewWidget(),
                ),
              ),
            ),
          ),
          _captureControlRowWidget(),
          _modeControlRowWidget(),
          Padding(
            padding: const EdgeInsets.all(5.0),
            child: Row(
              children: <Widget>[
                _cameraTogglesRowWidget(),
                _thumbnailWidget(),
              ],
            ),
          ),
        ],
      ),
    );
  }

  // สร้างภาพหรือวิดีโอตัวอย่างจากกล้องที่กำลังเปิดใช้งานรอบันทึก
  Widget _cameraPreviewWidget() {
    final CameraController? cameraController = controller;

    if (cameraController == null || !cameraController.value.isInitialized) {
      return const Text(
        'เลือกกล้อง(ถ้ามี)',
        style: TextStyle(
          color: Colors.white,
          fontSize: 24.0,
          fontWeight: FontWeight.w900,
        ),
      );
    } else {
      return Listener(
        onPointerDown: (_) => _pointers++,
        onPointerUp: (_) => _pointers--,
        child: CameraPreview(
          controller!,
          child: LayoutBuilder(
              builder: (BuildContext context, BoxConstraints constraints) {
            return GestureDetector(
              behavior: HitTestBehavior.opaque,
              onScaleStart: _handleScaleStart,
              onScaleUpdate: _handleScaleUpdate,
              onTapDown: (TapDownDetails details) =>
                  onViewFinderTap(details, constraints),
            );
          }),
        ),
      );
    }
  }

  // ฟังก์ชั่นจัดการสเกลค่าเริ่มต้น
  void _handleScaleStart(ScaleStartDetails details) {
    _baseScale = _currentScale;
  }

  // ฟังก์ชั่นสำหรับจัดการสเกลการซูมกล้อง โดยใช้มือซูมเข้าออกหรือปรับตำแหน่งโฟกัส
  Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async {
    if (controller == null || _pointers != 2) {
      return;
    }

    _currentScale = (_baseScale * details.scale)
        .clamp(_minAvailableZoom, _maxAvailableZoom);

    await controller!.setZoomLevel(_currentScale);
  }

  // ฟังก์ชั่นแสดง thumbnail ของรูปหรือวิดีโอ
  Widget _thumbnailWidget() {
    final VideoPlayerController? localVideoController = videoController;

    return Expanded(
      child: Align(
        alignment: Alignment.centerRight,
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            if (localVideoController == null && imageFile == null)
              Container()
            else
              SizedBox(
                width: 64.0,
                height: 64.0,
                child: (localVideoController == null)
                    ? (
                        kIsWeb
                            ? Image.network(imageFile!.path)
                            : Image.file(File(imageFile!.path)))
                    : Container(
                        decoration: BoxDecoration(
                            border: Border.all(color: Colors.pink)),
                        child: Center(
                          child: AspectRatio(
                              aspectRatio:
                                  localVideoController.value.aspectRatio,
                              child: VideoPlayer(localVideoController)),
                        ),
                      ),
              ),
          ],
        ),
      ),
    );
  }

  // ฟังก์ชั่นสำหรับสร้างปุ่มควบคุมรวมหลัก
  Widget _modeControlRowWidget() {
    return Column(
      children: <Widget>[
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            IconButton(
              icon: const Icon(Icons.flash_on),
              color: Colors.blue,
              onPressed: controller != null ? onFlashModeButtonPressed : null,
            ),
            ...!kIsWeb
                ? <Widget>[
                    IconButton(
                      icon: const Icon(Icons.exposure),
                      color: Colors.blue,
                      onPressed: controller != null
                          ? onExposureModeButtonPressed
                          : null,
                    ),
                    IconButton(
                      icon: const Icon(Icons.filter_center_focus),
                      color: Colors.blue,
                      onPressed:
                          controller != null ? onFocusModeButtonPressed : null,
                    )
                  ]
                : <Widget>[],
            IconButton(
              icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute),
              color: Colors.blue,
              onPressed: controller != null ? onAudioModeButtonPressed : null,
            ),
            IconButton(
              icon: Icon(controller?.value.isCaptureOrientationLocked ?? false
                  ? Icons.screen_lock_rotation
                  : Icons.screen_rotation),
              color: Colors.blue,
              onPressed: controller != null
                  ? onCaptureOrientationLockButtonPressed
                  : null,
            ),
          ],
        ),
        _flashModeControlRowWidget(),
        _exposureModeControlRowWidget(),
        _focusModeControlRowWidget(),
      ],
    );
  }

  // ฟังก์ชั่นสร้างปุ่มควบคุมเกี่ยวกับแฟลช
  Widget _flashModeControlRowWidget() {
    return SizeTransition(
      sizeFactor: _flashModeControlRowAnimation,
      child: ClipRect(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            IconButton(
              icon: const Icon(Icons.flash_off),
              color: controller?.value.flashMode == FlashMode.off
                  ? Colors.orange
                  : Colors.blue,
              onPressed: controller != null
                  ? () => onSetFlashModeButtonPressed(FlashMode.off)
                  : null,
            ),
            IconButton(
              icon: const Icon(Icons.flash_auto),
              color: controller?.value.flashMode == FlashMode.auto
                  ? Colors.orange
                  : Colors.blue,
              onPressed: controller != null
                  ? () => onSetFlashModeButtonPressed(FlashMode.auto)
                  : null,
            ),
            IconButton(
              icon: const Icon(Icons.flash_on),
              color: controller?.value.flashMode == FlashMode.always
                  ? Colors.orange
                  : Colors.blue,
              onPressed: controller != null
                  ? () => onSetFlashModeButtonPressed(FlashMode.always)
                  : null,
            ),
            IconButton(
              icon: const Icon(Icons.highlight),
              color: controller?.value.flashMode == FlashMode.torch
                  ? Colors.orange
                  : Colors.blue,
              onPressed: controller != null
                  ? () => onSetFlashModeButtonPressed(FlashMode.torch)
                  : null,
            ),
          ],
        ),
      ),
    );
  }

  // ฟังก์ชั่นสร้างปุ่มควบคุมเกี่ยวกับการชดเชยแสง
  Widget _exposureModeControlRowWidget() {
    final ButtonStyle styleAuto = TextButton.styleFrom(
      foregroundColor: controller?.value.exposureMode == ExposureMode.auto
          ? Colors.orange
          : Colors.blue,
    );
    final ButtonStyle styleLocked = TextButton.styleFrom(
      foregroundColor: controller?.value.exposureMode == ExposureMode.locked
          ? Colors.orange
          : Colors.blue,
    );

    return SizeTransition(
      sizeFactor: _exposureModeControlRowAnimation,
      child: ClipRect(
        child: ColoredBox(
          color: Colors.grey.shade50,
          child: Column(
            children: <Widget>[
              const Center(
                child: Text('Exposure Mode'),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  TextButton(
                    style: styleAuto,
                    onPressed: controller != null
                        ? () =>
                            onSetExposureModeButtonPressed(ExposureMode.auto)
                        : null,
                    onLongPress: () {
                      if (controller != null) {
                        controller!.setExposurePoint(null);
                        showInSnackBar('Resetting exposure point');
                      }
                    },
                    child: const Text('AUTO'),
                  ),
                  TextButton(
                    style: styleLocked,
                    onPressed: controller != null
                        ? () =>
                            onSetExposureModeButtonPressed(ExposureMode.locked)
                        : null,
                    child: const Text('LOCKED'),
                  ),
                  TextButton(
                    style: styleLocked,
                    onPressed: controller != null
                        ? () => controller!.setExposureOffset(0.0)
                        : null,
                    child: const Text('RESET OFFSET'),
                  ),
                ],
              ),
              const Center(
                child: Text('Exposure Offset'),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  Text(_minAvailableExposureOffset.toString()),
                  Slider(
                    value: _currentExposureOffset,
                    min: _minAvailableExposureOffset,
                    max: _maxAvailableExposureOffset,
                    label: _currentExposureOffset.toString(),
                    onChanged: _minAvailableExposureOffset ==
                            _maxAvailableExposureOffset
                        ? null
                        : setExposureOffset,
                  ),
                  Text(_maxAvailableExposureOffset.toString()),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  // ฟังก์ชั่นสร้างปุ่มควบคุมเกี่ยวกับการโฟกัส
  Widget _focusModeControlRowWidget() {
    final ButtonStyle styleAuto = TextButton.styleFrom(
      foregroundColor: controller?.value.focusMode == FocusMode.auto
          ? Colors.orange
          : Colors.blue,
    );
    final ButtonStyle styleLocked = TextButton.styleFrom(
      foregroundColor: controller?.value.focusMode == FocusMode.locked
          ? Colors.orange
          : Colors.blue,
    );

    return SizeTransition(
      sizeFactor: _focusModeControlRowAnimation,
      child: ClipRect(
        child: ColoredBox(
          color: Colors.grey.shade50,
          child: Column(
            children: <Widget>[
              const Center(
                child: Text('Focus Mode'),
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  TextButton(
                    style: styleAuto,
                    onPressed: controller != null
                        ? () => onSetFocusModeButtonPressed(FocusMode.auto)
                        : null,
                    onLongPress: () {
                      if (controller != null) {
                        controller!.setFocusPoint(null);
                      }
                      showInSnackBar('Resetting focus point');
                    },
                    child: const Text('AUTO'),
                  ),
                  TextButton(
                    style: styleLocked,
                    onPressed: controller != null
                        ? () => onSetFocusModeButtonPressed(FocusMode.locked)
                        : null,
                    child: const Text('LOCKED'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  // ฟังก์ชั่นสร้างปุ่มควบคุมรูปภาพหรือวิดีโอ
  Widget _captureControlRowWidget() {
    final CameraController? cameraController = controller;

    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: <Widget>[
        IconButton(
          icon: const Icon(Icons.camera_alt),
          color: Colors.blue,
          onPressed: cameraController != null &&
                  cameraController.value.isInitialized &&
                  !cameraController.value.isRecordingVideo
              ? onTakePictureButtonPressed
              : null,
        ),
        IconButton(
          icon: const Icon(Icons.videocam),
          color: Colors.blue,
          onPressed: cameraController != null &&
                  cameraController.value.isInitialized &&
                  !cameraController.value.isRecordingVideo
              ? onVideoRecordButtonPressed
              : null,
        ),
        IconButton(
          icon: cameraController != null &&
                  cameraController.value.isRecordingPaused
              ? const Icon(Icons.play_arrow)
              : const Icon(Icons.pause),
          color: Colors.blue,
          onPressed: cameraController != null &&
                  cameraController.value.isInitialized &&
                  cameraController.value.isRecordingVideo
              ? (cameraController.value.isRecordingPaused)
                  ? onResumeButtonPressed
                  : onPauseButtonPressed
              : null,
        ),
        IconButton(
          icon: const Icon(Icons.stop),
          color: Colors.red,
          onPressed: cameraController != null &&
                  cameraController.value.isInitialized &&
                  cameraController.value.isRecordingVideo
              ? onStopButtonPressed
              : null,
        ),
        IconButton(
          icon: const Icon(Icons.pause_presentation),
          color:
              cameraController != null && cameraController.value.isPreviewPaused
                  ? Colors.red
                  : Colors.blue,
          onPressed:
              cameraController == null ? null : onPausePreviewButtonPressed,
        ),
      ],
    );
  }

  /// แถวแสดงข้อมูลกล้องให้เลือก ถ้ามี หรือแสดงข้อความถ้าไม่มีกล้องที่รองรับ
  Widget _cameraTogglesRowWidget() {
    final List<Widget> toggles = <Widget>[];

    void onChanged(CameraDescription? description) {
      if (description == null) {
        return;
      }
      // เมื่อเลือกกล้องที่จะใช้งาน
      onNewCameraSelected(description);
    }

    if (!_cameraReady) {
      return const Text('กำลังเตรียมกล้อง..');
    } else if (_cameras.isEmpty) {
      SchedulerBinding.instance.addPostFrameCallback((_) async {
        showInSnackBar('ไม่พบกล้องพร้อมใช้งาน.');
      });
      return const Text('ไม่พบกล้องพร้อมใช้งาน');
    } else {
      for (final CameraDescription cameraDescription in _cameras) {
        toggles.add(
          SizedBox(
            width: 90.0,
            child: RadioListTile<CameraDescription>(
              title: Icon(getCameraLensIcon(cameraDescription.lensDirection)),
              groupValue: controller?.description,
              value: cameraDescription,
              onChanged: onChanged,
            ),
          ),
        );
      }
    }

    return Row(children: toggles);
  }

  // ฟังก์ชั่นเลือกกล้องเพื่อใช้งาน
  Future<void> onNewCameraSelected(CameraDescription cameraDescription) async {
    if (controller != null) {
      return controller!.setDescription(cameraDescription);
    } else {
      return _initializeCameraController(cameraDescription);
    }
  }

  // เตรียมตัวควบคุมกล้องตามกล้องที่เลือก
  Future<void> _initializeCameraController(
      CameraDescription cameraDescription) async {
    final CameraController cameraController = CameraController(
      cameraDescription,
      kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium,
      enableAudio: enableAudio,
      imageFormatGroup: ImageFormatGroup.jpeg,
    );

    controller = cameraController;

    // เมื่อตัวควบคุมมีการเปลี่ยนแปลง ให้อัปเดท UI
    cameraController.addListener(() {
      if (mounted) {
        setState(() {});
      }
      if (cameraController.value.hasError) {
        showInSnackBar(
            'Camera error ${cameraController.value.errorDescription}');
      }
    });

    try {
      await cameraController.initialize();
      await Future.wait(<Future<Object?>>[
        // The exposure mode is currently not supported on the web.
        ...!kIsWeb
            ? <Future<Object?>>[
                cameraController.getMinExposureOffset().then(
                    (double value) => _minAvailableExposureOffset = value),
                cameraController
                    .getMaxExposureOffset()
                    .then((double value) => _maxAvailableExposureOffset = value)
              ]
            : <Future<Object?>>[],
        cameraController
            .getMaxZoomLevel()
            .then((double value) => _maxAvailableZoom = value),
        cameraController
            .getMinZoomLevel()
            .then((double value) => _minAvailableZoom = value),
      ]);
    } on CameraException catch (e) {
      switch (e.code) {
        case 'CameraAccessDenied':
          showInSnackBar('You have denied camera access.');
        case 'CameraAccessDeniedWithoutPrompt':
          // iOS only
          showInSnackBar('Please go to Settings app to enable camera access.');
        case 'CameraAccessRestricted':
          // iOS only
          showInSnackBar('Camera access is restricted.');
        case 'AudioAccessDenied':
          showInSnackBar('You have denied audio access.');
        case 'AudioAccessDeniedWithoutPrompt':
          // iOS only
          showInSnackBar('Please go to Settings app to enable audio access.');
        case 'AudioAccessRestricted':
          // iOS only
          showInSnackBar('Audio access is restricted.');
        default:
          _showCameraException(e);
          break;
      }
    }

    if (mounted) {
      setState(() {});
    }
  }

  // ฟังก์ชั่นคืนค่าข้อความเวลา ณ ปัจจุบัน
  String timestamp() => DateTime.now().millisecondsSinceEpoch.toString();

  // ฟังก์ชั่นแสดงข้อความใน SnackBar
  void showInSnackBar(String message) {
    ScaffoldMessenger.of(context)
        .showSnackBar(SnackBar(content: Text(message)));
  }

  // ฟังก์ชันควบคุมเมื่อกดระบุตำแหน่งในจอที่จะทำการโฟกัสหรือการชดเชยแสง
  void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) {
    if (controller == null) {
      return;
    }

    final CameraController cameraController = controller!;

    final Offset offset = Offset(
      details.localPosition.dx / constraints.maxWidth,
      details.localPosition.dy / constraints.maxHeight,
    );
    cameraController.setExposurePoint(offset);
    cameraController.setFocusPoint(offset);
  }

  // ฟังก์ชั่นส่วนทำงานการ ถ่าย รูปภาพ
  void onTakePictureButtonPressed() {
    takePicture().then((XFile? file) {
      if (mounted) {
        setState(() {
          imageFile = file;
          videoController?.dispose();
          videoController = null;
        });
        if (file != null) {
          showInSnackBar('Picture saved to ${file.path}');
        }
      }
    });
  }

  // ฟังก์ชั่นควบคุมการซ่อนหรือแสดงปุ่มเกี่ยวกับแฟลช เมื่อปุ่มเลือกโหมดแฟลชถูกกด
  void onFlashModeButtonPressed() {
    // ตรวจสอบว่าแอนิเมชันของแถบควบคุมแฟลชเสร็จสิ้นหรือยัง
    // (value == 1 หมายถึงแอนิเมชันได้เสร็จสิ้นแล้ว)
    if (_flashModeControlRowAnimationController.value == 1) {
      // ถ้าแอนิเมชันของแถบควบคุมแฟลชเสร็จสิ้นแล้ว (แสดงว่าแถบแฟลชเปิดอยู่)
      // จะเรียกใช้ reverse() เพื่อเลื่อนแถบควบคุมแฟลชกลับไปปิด
      _flashModeControlRowAnimationController.reverse();
    } else {
      // เรียกใช้ forward() เพื่อเริ่มแอนิเมชันแถบควบคุมแฟลชให้เลื่อนออกมา (เปิดแถบควบคุมแฟลช)
      _flashModeControlRowAnimationController.forward();
      // ปิด (เลื่อนกลับ) แถบควบคุมการชดเชยแสง (Exposure) หากเปิดอยู่
      _exposureModeControlRowAnimationController.reverse();
      // ปิด (เลื่อนกลับ) แถบควบคุมโหมดโฟกัส หากเปิดอยู่
      _focusModeControlRowAnimationController.reverse();
    }
  }

  // ฟังก์ชั่นควบคุมการชดเชยแสง เมื่อปุ่มควบคุมการชดเชยแสงถูกกด
  void onExposureModeButtonPressed() {
    // ตรวจสอบว่าแถบควบคุมการชดเชยแสงถูกเปิดอยู่หรือไม่
    // (value == 1 หมายถึงแถบควบคุมเปิดเต็มที่)
    if (_exposureModeControlRowAnimationController.value == 1) {
      // หากแถบควบคุมการชดเชยแสงเปิดอยู่ จะเลื่อนแถบนี้กลับไปปิด (ย้อนกลับแอนิเมชัน)
      _exposureModeControlRowAnimationController.reverse();
    } else {
      // เริ่มแอนิเมชันเพื่อเปิดแถบควบคุมการชดเชยแสง
      _exposureModeControlRowAnimationController.forward();
      // หากแถบควบคุมแฟลชเปิดอยู่ จะปิด (เลื่อนกลับ) แถบควบคุมแฟลชลง
      _flashModeControlRowAnimationController.reverse();
      // หากแถบควบคุมโหมดโฟกัสเปิดอยู่ จะปิด (เลื่อนกลับ) แถบควบคุมโหมดโฟกัสลง
      _focusModeControlRowAnimationController.reverse();
    }
  }

  // ตรวจสอบว่าแถบควบคุมโหมดโฟกัสถูกเปิดอยู่หรือไม่
  // (value == 1 หมายถึงแถบควบคุมเปิดเต็มที่)
  void onFocusModeButtonPressed() {
    if (_focusModeControlRowAnimationController.value == 1) {
      // ถ้าแถบควบคุมโหมดโฟกัสเปิดอยู่ จะเลื่อนแถบนี้กลับลงไปปิด (ย้อนกลับแอนิเมชัน)
      _focusModeControlRowAnimationController.reverse();
    } else {
      // เริ่มแอนิเมชันเพื่อเปิดแถบควบคุมโหมดโฟกัส
      _focusModeControlRowAnimationController.forward();
      // ถ้าแถบควบคุมแฟลชเปิดอยู่ จะปิด (เลื่อนกลับ) แถบควบคุมแฟลชลง
      _flashModeControlRowAnimationController.reverse();
      // ถ้าแถบควบคุมการชดเชยแสงเปิดอยู่ จะปิด (เลื่อนกลับ) แถบควบคุมการชดเชยแสงลง
      _exposureModeControlRowAnimationController.reverse();
    }
  }

  // เปลี่ยนค่าของตัวแปร enableAudio ให้ตรงข้ามกับค่าปัจจุบัน
  //(ถ้าเปิดเสียงอยู่ก็จะปิด ถ้าปิดอยู่ก็จะเปิด)
  void onAudioModeButtonPressed() {
    enableAudio = !enableAudio;
    if (controller != null) {
      // เรียกฟังก์ชัน onNewCameraSelected โดยส่งคำอธิบายของกล้องที่ใช้อยู่ (description)
      // ไปเป็นพารามิเตอร์ เพื่อเลือกกล้องใหม่และอัปเดตการตั้งค่าเสียงตามสถานะของ enableAudio
      onNewCameraSelected(controller!.description);
    }
  }

  // เมื่อกดปุ่มควบคุมการล็อกแนวกล้อง
  Future<void> onCaptureOrientationLockButtonPressed() async {
    try {
      if (controller != null) {
        final CameraController cameraController = controller!;
        if (cameraController.value.isCaptureOrientationLocked) {
          await cameraController.unlockCaptureOrientation();
          showInSnackBar('Capture orientation unlocked');
        } else {
          await cameraController.lockCaptureOrientation();
          showInSnackBar(
              'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}');
        }
      }
    } on CameraException catch (e) {
      _showCameraException(e);
    }
  }

  // ฟังก์ชั่นสำหรับควบคุมปุ่มกดการตั้งค่าแฟลช
  void onSetFlashModeButtonPressed(FlashMode mode) {
    setFlashMode(mode).then((_) {
      if (mounted) {
        setState(() {});
      }
      showInSnackBar('Flash mode set to ${mode.toString().split('.').last}');
    });
  }

  // ฟังก์ชั่นสำหรับกำหนดการชดเชยค่าแสง
  void onSetExposureModeButtonPressed(ExposureMode mode) {
    setExposureMode(mode).then((_) {
      if (mounted) {
        setState(() {});
      }
      showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}');
    });
  }

  // ฟังก์ชั่นควบคุมการโฟกัสของกล้อง
  void onSetFocusModeButtonPressed(FocusMode mode) {
    setFocusMode(mode).then((_) {
      if (mounted) {
        setState(() {});
      }
      showInSnackBar('Focus mode set to ${mode.toString().split('.').last}');
    });
  }

  // ฟังก์ชั่นควบคุมการเริ่มบันทึกวิดีโอ
  void onVideoRecordButtonPressed() {
    startVideoRecording().then((_) {
      if (mounted) {
        setState(() {});
      }
    });
  }

  // ฟังก์ชั่นควบคุมการหยุดบันทึกวิดีโอ
  void onStopButtonPressed() {
    stopVideoRecording().then((XFile? file) {
      if (mounted) {
        setState(() {});
      }
      if (file != null) {
        showInSnackBar('Video recorded to ${file.path}');
        videoFile = file;
        _startVideoPlayer();
      }
    });
  }

  // ฟังก์ชันสำหรับการหยุถพรีวิววิดีโอจากกล้อง
  Future<void> onPausePreviewButtonPressed() async {
    final CameraController? cameraController = controller;

    if (cameraController == null || !cameraController.value.isInitialized) {
      showInSnackBar('Error: select a camera first.');
      return;
    }

    if (cameraController.value.isPreviewPaused) {
      await cameraController.resumePreview();
    } else {
      await cameraController.pausePreview();
    }

    if (mounted) {
      setState(() {});
    }
  }

  // ฟังก์ชันสำหรับควบคุมการหยุดการบันทึกวิดีโอชั่วคราว
  void onPauseButtonPressed() {
    pauseVideoRecording().then((_) {
      if (mounted) {
        setState(() {});
      }
      showInSnackBar('Video recording paused');
    });
  }

  // ฟังก์ชั่นสำหรับควบคุมการกลับไปบันทึกวิดีโอต่อหลังจากหยุดชั่วคราว
  void onResumeButtonPressed() {
    resumeVideoRecording().then((_) {
      if (mounted) {
        setState(() {});
      }
      showInSnackBar('Video recording resumed');
    });
  }

  // ฟังก์ชั่นเริ่มการทำงานการบันทึกวิดีโอ  
  Future<void> startVideoRecording() async {
    final CameraController? cameraController = controller;

    if (cameraController == null || !cameraController.value.isInitialized) {
      showInSnackBar('Error: select a camera first.');
      return;
    }

    if (cameraController.value.isRecordingVideo) {
      // A recording is already started, do nothing.
      return;
    }

    try {
      await cameraController.startVideoRecording();
    } on CameraException catch (e) {
      _showCameraException(e);
      return;
    }
  }

  // ฟังก์ชันสำหรับหยุดการบันทึกวิดีโอ
  Future<XFile?> stopVideoRecording() async {
    final CameraController? cameraController = controller;

    if (cameraController == null || !cameraController.value.isRecordingVideo) {
      return null;
    }

    try {
      return cameraController.stopVideoRecording();
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    }
  }

  // ฟังก์ชั่นสำหรับหยุดการบันทึกวิดีโอชั่วคราว
  Future<void> pauseVideoRecording() async {
    final CameraController? cameraController = controller;

    if (cameraController == null || !cameraController.value.isRecordingVideo) {
      return;
    }

    try {
      await cameraController.pauseVideoRecording();
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    }
  }

  // ฟังก์ชั่นสำหรับกลับไปบันทึกวิดีโอต่อ หลังจากหยุดชั่วคราว
  Future<void> resumeVideoRecording() async {
    final CameraController? cameraController = controller;

    if (cameraController == null || !cameraController.value.isRecordingVideo) {
      return;
    }

    try {
      await cameraController.resumeVideoRecording();
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    }
  }

  // ฟังก์ชั่นสำหรับตั้งค่าแฟลช
  Future<void> setFlashMode(FlashMode mode) async {
    if (controller == null) {
      return;
    }

    try {
      await controller!.setFlashMode(mode);
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    }
  }

  // ฟังก์ชั่นสำหรับตั้งค่าการชดเชยแสง
  Future<void> setExposureMode(ExposureMode mode) async {
    if (controller == null) {
      return;
    }

    try {
      await controller!.setExposureMode(mode);
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    }
  }

  // ฟังก์ชั่นปรับระดับการชดเชยแสงของกล้อง
  Future<void> setExposureOffset(double offset) async {
    if (controller == null) {
      return;
    }

    setState(() {
      _currentExposureOffset = offset;
    });
    try {
      offset = await controller!.setExposureOffset(offset);
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    }
  }

  // ฟังก์ชั่นตั้งค่าการโฟกัส
  Future<void> setFocusMode(FocusMode mode) async {
    if (controller == null) {
      return;
    }

    try {
      await controller!.setFocusMode(mode);
    } on CameraException catch (e) {
      _showCameraException(e);
      rethrow;
    }
  }

  // ฟังก์ชั่นสำหรับเล่นวิดีโอ
  Future<void> _startVideoPlayer() async {
    if (videoFile == null) {
      return;
    }

    final VideoPlayerController vController = kIsWeb
        ? VideoPlayerController.networkUrl(Uri.parse(videoFile!.path))
        : VideoPlayerController.file(File(videoFile!.path));

    videoPlayerListener = () {
      if (videoController != null) {
        // Refreshing the state to update video player with the correct ratio.
        if (mounted) {
          setState(() {});
        }
        videoController!.removeListener(videoPlayerListener!);
      }
    };
    vController.addListener(videoPlayerListener!);
    await vController.setLooping(true);
    await vController.initialize();
    await videoController?.dispose();
    if (mounted) {
      setState(() {
        imageFile = null;
        videoController = vController;
      });
    }
    await vController.play();
  }

  // ฟังก์ชั่นสำหรับถ่ายรูปด้วยกล้อง คืนค่าเป็นไฟล์กลับออกไป
  Future<XFile?> takePicture() async {
    final CameraController? cameraController = controller;
    if (cameraController == null || !cameraController.value.isInitialized) {
      showInSnackBar('Error: select a camera first.');
      return null;
    }

    if (cameraController.value.isTakingPicture) {
      // A capture is already pending, do nothing.
      return null;
    }

    try {
      final XFile file = await cameraController.takePicture();
      return file;
    } on CameraException catch (e) {
      _showCameraException(e);
      return null;
    }
  }

  // ฟังก์ชั่นจัดการข้อผิดพลาดหรือการยกเว้นกรณีเกิดข้อผิดพลาดในการทำงาน
  void _showCameraException(CameraException e) {
    _logError(e.code, e.description);
    showInSnackBar('Error: ${e.code}n${e.description}');
  }
}

List<CameraDescription> _cameras = <CameraDescription>[];

/// จัดการไอคอนของกล้องที่เหมาะสม [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;
  }
  // ignore: dead_code
  return Icons.camera;
}

void _logError(String code, String? message) {
  // ignore: avoid_print
  print('Error: $code${message == null ? '' : 'nError Message: $message'}');
}
 

ผลลัพธ์ที่ได้



 
 
โค้ดข้างต้นเป็นโค้ดการปรับแต่งและการใช้งานทั้งหมดของ plugin camera ที่เราสามารถปรับแต่ง
ส่วนต่างๆ ได้เอง สามารถเลือกส่วนที่ต้องการไปปรับใช้งานได้
*จากที่ทดสอบการทำงานยังมี bug ในส่วนของการหยุดภาพพรีวิว กับการกำหนดเรื่องการชดเชยแสง
 
จากการใช้งานทั้งสองแพ็กเก็จ ทั้ง image_picker และ camera ที่ใช้สำหรับบันภาพหรือวิดีโอ
จะเห็นว่าตัว image_picker จะใช้งานง่ายและสะดวกกว่า กรณีเราต้องการความเรียบง่าย ไม่ต้อง
ปรับแต่งอะไรเยอะ เพราะใช้การจัดการของกล้องของระบบ ในขณะที่ถ้าเราใช้งาน camera หากต้อง
การปรับแต่งค่าๆ ต่างเป็นแบบที่ต้องการก็จะสามารถทำได้มากกว่า แต่ก็จะยากและซับซ้อนเพิ่มชึ้น
 
เนื้อหานี้เรานำเสนอให้เข้าใจถึงแนวทางและวิธีการใช้งานการบันทึกไฟล์วิดีโอใน flutter สำหรับนำ
ไปปรับใช้กับโปรเจ็คแอปของเรา กรณีที่ต้องการจัดการในส่วนนี้ หวังว่าจะเป็นประโยชน์ ไม่มากก็น้อย
สำหรับในตอนหน้าจะเป็นอะไร รอติดตาม


   เพิ่มเติมเนื้อหา ครั้งที่ 1 วันที่ 27-09-2024


ดาวน์โหลดโค้ดตัวอย่าง สามารถนำไปประยุกต์ หรือ run ทดสอบได้

http://niik.in/download/flutter/demo_047_27092024_source.rar


กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ



ทบทวนบทความที่แล้ว









เนื้อหาที่เกี่ยวข้อง






เนื้อหาพิเศษ เฉพาะสำหรับสมาชิก

กรุณาล็อกอิน เพื่ออ่านเนื้อหาบทความ

ยังไม่เป็นสมาชิก

สมาชิกล็อกอิน



( หรือ เข้าใช้งานผ่าน Social Login )




URL สำหรับอ้างอิง





คำแนะนำ และการใช้งาน

สมาชิก กรุณา ล็อกอินเข้าระบบ เพื่อตั้งคำถามใหม่ หรือ ตอบคำถาม สมาชิกใหม่ สมัครสมาชิกได้ที่ สมัครสมาชิก


  • ถาม-ตอบ กรุณา ล็อกอินเข้าระบบ
  • เปลี่ยน


    ( หรือ เข้าใช้งานผ่าน Social Login )







เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ