การจัดการและใช้งาน เกี่ยวกับ Video ใน Flutter ตอนที่ 1

บทความใหม่ ไม่กี่เดือนก่อน โดย Ninenik Narkdee
video player flutter

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

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


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

เกี่ยวกับการใช้งาน video ในตอนนี้ สิ่งที่เราควรจะได้เรียนรู้คือ 

    - เราต้องรู้จักการนำ video มาแสดง ภายในแอป
    - แหล่ง video นั้นๆ จะต้องรองรับทั้งที่เป็นไฟล์เลือกเอง หรือ asset หรือจาก Network
    - สามารถเปิด video แบบเต็มจอในหน้าใหม่ได้
    - แสดง video ตามรูปแบบแนวตั้งหรือแนวนอนตามค่าเริ่มต้นของ video นั้นได้
 
เนื้อหาในตอนต่อไปนี้ เราจะพูดถึง plugin ที่ชื่อว่า video_player ติดตั้งด้วย
 
video_player: ^2.9.1
 
ตอนนี้เรามีเครื่องมือที่จะใช้งานแล้ว ต่อไปเป็นส่วนที่จะมาเรียนรู้การใช้งานเครื่องมืนี้ในการแสดง
วิดีโอในแอป รวมถึงควบคุม เงื่อนไข และการจัดการเกี่ยวกับวิดีโอ เช่น การกำหนดการเล่นอัตโนมัติ
การหยุด การเล่นต่อ และอื่นๆ 
 

การแสดง video ในแอปด้วย video_player

    เราจะสร้างไฟล์ชื่อ clip.dart ใช้สำหรับทดสอบการนำ video มาแสดงในแอป โดยจะใช้ video
จาก Network มาใช้งาน สามารถใช้ url เหล่านี้สำหรับทดสอบได้
 
   // vertical video 
    /*
    https://cdn.pixabay.com/video/2024/08/30/228847_tiny.mp4
    https://cdn.pixabay.com/video/2023/07/28/173530-849610807_tiny.mp4
    https://cdn.pixabay.com/video/2024/03/31/206294_tiny.mp4
    https://cdn.pixabay.com/video/2023/10/27/186714-878826932_tiny.mp4
    https://cdn.pixabay.com/video/2024/07/28/223551_tiny.mp4
    https://cdn.pixabay.com/video/2024/08/18/227174_tiny.mp4
    */
    // horizontal video
    /*
    https://cdn.pixabay.com/video/2024/07/27/223461_tiny.mp4
    https://cdn.pixabay.com/video/2023/10/15/185096-874643413_tiny.mp4
    https://cdn.pixabay.com/video/2022/11/22/140111-774507949_tiny.mp4
    https://cdn.pixabay.com/video/2024/07/14/221180_tiny.mp4
    https://cdn.pixabay.com/video/2022/10/19/135658-764361528_tiny.mp4
    https://cdn.pixabay.com/video/2021/08/04/83880-585600454_tiny.mp4
    https://cdn.pixabay.com/video/2024/09/06/230060_tiny.mp4
    */
 

ไฟล์ clip.dart

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

import 'fullscreenvideo.dart';

class Clip extends StatefulWidget {
  static const routeName = '/clip';

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

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

class _ClipState extends State<Clip> {
  late VideoPlayerController _controller;
  String videoUrl = ''; // Variable to store video URL

  @override
  void initState() {
    super.initState();
    print("debug: initialize");
    videoUrl =
        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
    _controller = VideoPlayerController.networkUrl(Uri.parse(videoUrl))
      ..addListener(() {
        // Listening for changes in VideoPlayerValue
        setState(() {
          if (_controller.value.isPlaying) {
            print("debug: Playing");
            print("debug: ${_controller.value.duration}");
          } else if (_controller.value.isBuffering) {
            print("debug: Buffering");
          } else if (_controller.value.isCompleted) {
            print("debug: Finished");            
          } else {
            print("debug: Paused");
          }
        });
      })
      ..initialize().then((_) {
        print("debug: video initialize");
        setState(() {});
      });
  }

  @override
  void dispose() {
    _controller.removeListener(() {});
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Clip'),
      ),
      body: Center(
        child: _controller.value.isInitialized
            ? AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: GestureDetector(
                  onTap: () {
                    // Navigate to fullscreen mode when tapped
/*                     Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => FullScreenVideo(videoUrl),
                      ),
                    ); */
                  
                  },
                  child: VideoPlayer(_controller),
                ),
              )
            : const CircularProgressIndicator(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // ควบคุมการทำงานของปุ่ม ถ้าเล่นอยู่ให้รอคำสั่ง หยุดชั่วคราว
          // ถ้าหยุดอยู่ ให้รอคำสั่ง เล่น
          setState(() {
            _controller.value.isPlaying && !_controller.value.isCompleted
                ? _controller.pause()
                : _controller.play();
          });
        },
        child: Icon(
          _controller.value.isPlaying && !_controller.value.isCompleted
              ? Icons.pause
              : Icons.play_arrow,
        ),
      ),
    );
  }
}

 

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


 
 
ในตัวอย่าง เราวางคลิป video 1 คลิป ไว้ตรงกลาง โดยระบุ url ของ video ที่ต้องการเล่น และตรง
floatingActionButton เรากำหนดปุ่มเล่น และ ปุ่มหยุดชั่วคราว ไว้ควบคุม video การทำงานของโค้ดหน้านี้
ไม่ยุ่งยากอะไร เข้าใจไม่ยาก เมื่อเปิดเข้ามา ตัว plugin จะทำงานในส่วนของ initState เพื่อเตรียม video ให้
พร้อมเล่น รวมทั้งในตัวอย่าง เราให้ตรวจจับสถานะของ video เพื่อให้เข้าใจการทำงานว่า video กำลังเล่นอยู่
หรือเล่นจบแล้ว หรือหยุดชั่วคราว  ซึ่งเมื่อผู้กดปุ่ม play ตัว video ก็จะเริ่มเล่น และถ้ากดหยุดชั่วคราว video 
ก็จะหยุด
 
สำหรับการนำตัวคลิป video มาแสดงจะใช้ widget ที่ชื่อว่า AspectRatio เพื่อให้พื้นที่ของ video ที่เราจะ
แสดง เป็นสัดส่วนตามที่เราต้องการแบบเต็มพื้นที่ โดยสัดส่วนจะกำหนดจาก ความกว้าง / ความสูง เราสามารถ
กำหนดเป็น 4/3 หรือ 16/9 หรือสี่เหลี่ยมจัตตุรัสก็เป็นค่า 1 หรืออื่นๆ
 
aspectRatio: 16/9,
aspectRatio: 4/3,
aspectRatio: 1,
 
แต่ในที่นี้เราใช้ค่าตามอัตราส่วนของ video นั้นๆ เพื่อให้ video ที่แสดงมาสัดส่วนที่ถูกต้องไม่บิดเบี้ยวตามค่าที่
กำหนด เพราะถ้าค่าไม่ตรงตามอัราส่วนของ video คลิป video อาจจะยืดหรือหดไม่สวยงามและไม่ได้สัดส่วน
 
      body: Center(
        child: _controller.value.isInitialized
            ? AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: GestureDetector(
                  onTap: () {
                    // Navigate to fullscreen mode when tapped
/*                     Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => FullScreenVideo(videoUrl),
                      ),
                    ); */
                  },
                  child: VideoPlayer(_controller),
                ),
              )
            : const CircularProgressIndicator(),
      ),
 
ถ้า video พร้อมใช้เล่นและใช้งาน ภายใต้เงื่อนไข 
 
_controller.value.isInitialized
 
ก็ให้แสดง video widget จากการใช้งาน video_player
 
VideoPlayer(_controller),
 
แต่ถ้ากำลังเตรียมและยังไม่พร้อมก็จะแสดงตัว loading
 
const CircularProgressIndicator(),
 
ในตัวอย่าง เรามีการใช้งาน GestureDetector เพื่อคลิปส่วนของ video ให้สามารถกด เพื่อเปิดแบบเต็มหน้า
จอได้ แต่ขอปิดส่วนของการกดเพื่อเปิดหน้าใหม่ไว้ก่อน
 
การแทรก video ลงในแอปเบี้ยงต้นก็จะประมาณนี้  ต่อไป เรามาลองปรับแต่งกัน
 
 

ให้กดที่ video เพื่อเริ่มเล่น กดอีกครั้งเพื่อหยุดชั่วคราว 

    สมมติเราไม่ต้องการปุ่มแยกไปไว้ข้างนอก แต่อยากให้กดที่ตัว video เพื่อเล่น และกดอีกทีเพื่อหยุด ก็สามารถ
นำส่วนของการทำงานที่ในปุ่ม มาใส่ในส่วน onTap ของ GestureDetector ดังนี้ได้
 
      body: Center(
        child: _controller.value.isInitialized
            ? AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: GestureDetector(
                  onTap: () {
                    setState(() {
                      _controller.value.isPlaying && !_controller.value.isCompleted
                          ? _controller.pause()
                          : _controller.play();
                    });                    
                  },
                  child: VideoPlayer(_controller),
                ),
              )
            : const CircularProgressIndicator(),
      ),
 

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

class _ClipState extends State<Clip> {
  late VideoPlayerController _controller;
  String videoUrl = ''; // Variable to store video URL
  // เพิ่มส่วนของ สถานะการเล่นอยู่ และเวลาของตำแหน่งคลิปปัจจุบัน
  bool _isPlaying = false;
  Duration _currentPosition = Duration.zero;

  @override
  void initState() {
    super.initState();
    print("debug: initialize");
    videoUrl =
        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
    _controller = VideoPlayerController.networkUrl(Uri.parse(videoUrl))
      ..addListener(() {
        // Listening for changes in VideoPlayerValue
        setState(() {
          if (_controller.value.isPlaying) {
            print("debug: Playing");
            print("debug: ${_controller.value.duration}");
          } else if (_controller.value.isBuffering) {
            print("debug: Buffering");
          } else if (_controller.value.isCompleted) {
            print("debug: Finished");
          } else {
            print("debug: Paused");
          }
          _currentPosition =
              _controller.value.position; // Update current position
        });
      })
      ..initialize().then((_) {
        print("debug: video initialize");
        setState(() {});
      });
  }

  // Helper function to format duration as mm:ss
  String _formatDuration(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    final minutes = twoDigits(duration.inMinutes.remainder(60));
    final seconds = twoDigits(duration.inSeconds.remainder(60));
    return "$minutes:$seconds";
  }

  @override
  void dispose() {
    _controller.removeListener(() {});
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Clip'),
      ),
      body: Stack(
        children: [
          Center(
            child: _controller.value.isInitialized
                ? AspectRatio(
                    aspectRatio: _controller.value.aspectRatio,
                    child: GestureDetector(
                      onTap: () {
                        setState(() {
                          _isPlaying = !_isPlaying;
                          _controller.value.isPlaying &&
                                  !_controller.value.isCompleted
                              ? _controller.pause()
                              : _controller.play();
                        });
                      },
                      child: VideoPlayer(_controller),
                    ),
                  )
                : const CircularProgressIndicator(),
          ),
          // Video seek bar
          if (_controller.value.isInitialized)
            Positioned(
              bottom: 30,
              left: 20,
              right: 20,
              child: Column(
                children: [
                  // Slider for seeking the video
                  Slider(
                    value: _currentPosition.inMicroseconds.toDouble(),
                    min: 0,
                    max: _controller.value.duration.inMicroseconds.toDouble(),
                    onChanged: (value) {
                      setState(() {
                        _controller.seekTo(Duration(microseconds: value.toInt()));
                      });
                    },
                  ),
                  // Display current position and total duration
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(
                        _formatDuration(_currentPosition),
                        style: const TextStyle(color: Colors.black),
                      ),
                      Text(
                        _formatDuration(_controller.value.duration),
                        style: const TextStyle(color: Colors.black),
                      ),
                    ],
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}
 

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


 
 

เพิ่มการวนเล่น video ทันทีเมื่อเริ่ม และให้วนลูป 

    เราสามารถกำหนดให้ video เล่นได้ทัน พร้อมทั้งให้สามารถวนลูป เล่นซ้ำได้ โดยการเพิ่มคำสั่งนี้ลงไป
 
....
..
      ..initialize().then((_) {
        print("debug: video initialize");
        _controller.play(); // เล่นทันทีเมื่อวิดีโอพร้อม
        _controller.setLooping(true); // ให้วนลูป
        setState(() {});
      });
....
..
 

เพิ่มการแสดงแบบเต็มหน้าจอ เข้าไปใน video preview

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

ไฟล์ fullscreenvideo.dart

 
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:flutter/services.dart'; 


// Fullscreen video player with landscape orientation
class FullScreenVideo extends StatefulWidget {
  final String videoUrl; // Use video URL to create separate controller

  const FullScreenVideo(this.videoUrl, {Key? key}) : super(key: key);

  @override
  _FullScreenVideoState createState() => _FullScreenVideoState();
}

class _FullScreenVideoState extends State<FullScreenVideo> {
  late VideoPlayerController _controller;

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

    // Initialize a new controller for fullscreen mode
    _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl))
      ..initialize().then((_) {
        setState(() {}); // Ensure UI is refreshed when video is loaded
        _controller.play(); // Optionally auto-play video when in fullscreen
      });

    // Check if the video is initialized before setting orientation
    if (_controller.value.isInitialized) {
      _setOrientationBasedOnAspectRatio();
    } else {
      // Add a listener to wait for video initialization
      _controller.addListener(() {
        if (_controller.value.isInitialized) {
          _setOrientationBasedOnAspectRatio();
        }
      });
    }

    // Hide the status bar and navigation bar for true fullscreen
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
  }

  void _setOrientationBasedOnAspectRatio() {
    // Set the orientation based on the aspect ratio of the video
    if (_controller.value.aspectRatio > 1) {
      // If the video is wider than it is tall, set to landscape
      SystemChrome.setPreferredOrientations([
        DeviceOrientation.landscapeRight,
        DeviceOrientation.landscapeLeft,
      ]);
    } else {
      // If the video is taller than it is wide, set to portrait
      SystemChrome.setPreferredOrientations([
        DeviceOrientation.portraitUp,
      ]);
    }
  }

  @override
  void dispose() {
    // Restore the orientation to portrait when exiting fullscreen
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
    ]);
    // Restore system UI when exiting fullscreen
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Dismissible(
      direction: DismissDirection.vertical,
      key: const Key('key'),
      onDismissed: (_) => Navigator.of(context).pop(),
      child: Scaffold(
        backgroundColor: Colors.black,
        body: Stack(
          children: [
            Center(
              child: _controller.value.isInitialized
                  ? AspectRatio(
                      aspectRatio: _controller.value.aspectRatio,
                      child: VideoPlayer(_controller),
                    )
                  : const CircularProgressIndicator(),
            ),
            // Positioned close button at the top-left corner
            Positioned(
              top: 20,
              left: 10,
              child: IconButton(
                icon: const Icon(Icons.close, color: Colors.white, size: 30),
                onPressed: () {
                  Navigator.pop(context);
                },
              ),
            ),
            // Centered play/pause button with opacity
            _controller.value.isInitialized
                ? Positioned(
                    left: MediaQuery.of(context).size.width / 2 - 30,
                    top: MediaQuery.of(context).size.height / 2 - 30,
                    child: Opacity(
                      opacity: _controller.value.isPlaying && !_controller.value.isCompleted
                      ? 0.0
                      : 0.5,
                      child: GestureDetector(
                        onTap: () {
                          setState(() {
                            _controller.value.isPlaying &&
                                    !_controller.value.isCompleted
                                ? _controller.pause()
                                : _controller.play();
                          });
                        },
                        child: Icon(
                          _controller.value.isPlaying &&
                                  !_controller.value.isCompleted
                              ? Icons.pause
                              : Icons.play_arrow,
                          color: Colors.white,
                          size: 60,
                        ),
                      ),
                    ),
                  )
                : Container(),
          ],
        ),
      ),
    );
  }
}
 
จากนั้น เราจะมาเรียกใช้งานที่ไฟล์ clip.dart เป็นดังนี้
 

ไฟล์ clip.dart

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

import 'fullscreenvideo.dart';

class Clip extends StatefulWidget {
  static const routeName = '/clip';

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

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

class _ClipState extends State<Clip> {
  late VideoPlayerController _controller;
  String videoUrl = ''; // Variable to store video URL
  // เพิ่มส่วนของ สถานะการเล่นอยู่ และเวลาของตำแหน่งคลิปปัจจุบัน
  bool _isPlaying = false;
  Duration _currentPosition = Duration.zero;

  @override
  void initState() {
    super.initState();
    print("debug: initialize");
    videoUrl =
        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
    _controller = VideoPlayerController.networkUrl(Uri.parse(videoUrl))
      ..addListener(() {
        // Listening for changes in VideoPlayerValue
        setState(() {
          if (_controller.value.isPlaying) {
            print("debug: Playing");
            print("debug: ${_controller.value.duration}");
          } else if (_controller.value.isBuffering) {
            print("debug: Buffering");
          } else if (_controller.value.isCompleted) {
            print("debug: Finished");
          } else {
            print("debug: Paused");
          }
          _currentPosition =
              _controller.value.position; // Update current position
        });
      })
      ..initialize().then((_) {
        print("debug: video initialize");
        // _controller.play();
        // _controller.setLooping(true);
        setState(() {});
      });
  }

  // Helper function to format duration as mm:ss
  String _formatDuration(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    final minutes = twoDigits(duration.inMinutes.remainder(60));
    final seconds = twoDigits(duration.inSeconds.remainder(60));
    return "$minutes:$seconds";
  }

  @override
  void dispose() {
    _controller.removeListener(() {});
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Clip'),
      ),
      body: Stack(
        children: [
          Center(
            child: _controller.value.isInitialized
                ? AspectRatio(
                    aspectRatio: _controller.value.aspectRatio,
                    child: GestureDetector(
                      onTap: () {
                        setState(() {
                          _isPlaying = !_isPlaying;
                          _controller.value.isPlaying &&
                                  !_controller.value.isCompleted
                              ? _controller.pause()
                              : _controller.play();
                        });
                      },
                      child: Stack(
                        children: [
                          VideoPlayer(_controller),
                          // Centered play/pause button with opacity
                          Positioned(
                            right: 0,
                            bottom: 0,
                            child: Opacity(
                              opacity: _controller.value.isPlaying &&
                                      !_controller.value.isCompleted
                                  ? 0.5
                                  : 0.5,
                              child: GestureDetector(
                                onTap: () {
                                  _controller.pause();
                                  // Navigate to fullscreen mode when tapped
                                  Navigator.push(
                                    context,
                                    MaterialPageRoute(
                                      builder: (context) =>
                                          FullScreenVideo(videoUrl),
                                    ),
                                  );
                                },
                                child: Icon(
                                  _controller.value.isPlaying &&
                                          !_controller.value.isCompleted
                                      ? Icons.fullscreen
                                      : Icons.fullscreen_outlined,
                                  color: Colors.white,
                                  size: 60,
                                ),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  )
                : const CircularProgressIndicator(),
          ),
          // Video seek bar
          if (_controller.value.isInitialized)
            Positioned(
              bottom: 30,
              left: 20,
              right: 20,
              child: Column(
                children: [
                  // Slider for seeking the video
                  Slider(
                    value: _currentPosition.inMicroseconds.toDouble(),
                    min: 0,
                    max: _controller.value.duration.inMicroseconds.toDouble(),
                    onChanged: (value) {
                      setState(() {
                        _controller
                            .seekTo(Duration(microseconds: value.toInt()));
                      });
                    },
                  ),
                  // Display current position and total duration
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(
                        _formatDuration(_currentPosition),
                        style: const TextStyle(color: Colors.black),
                      ),
                      Text(
                        _formatDuration(_controller.value.duration),
                        style: const TextStyle(color: Colors.black),
                      ),
                    ],
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}
 
จากโค้ด เราใช้ Stack และ Position widget กำหนดตำแหน่งของปุ่มสำหรับเล่นวิดีโอแบบแสดงเต็มหน้าจอ
โดย Stack ใช้สำหรับซ้อน widget โดยเราให้ตัว ปุ่ม แสดงเต็มหน้าจอ ซ้อนอยู่ด้านบนของวิดีโอ เมื่อกดปุ่ม ก็
จะให้หยุดวิดีโอหลักถ้าเล่นอยู่ จากนั้นเปิดหน้าวิดีโอแบบเต็มหน้าจอ โดยการส่ง url ของ video ไปแสดงในหน้า
นั้น
 

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

 



 
แนวทางการนำไปประยุกต์เช่น เราแสดง thumbnail รายการวิดีโอ ที่ต้องการ โดยมี url ของ video ที่จะส่งค่า
ไว้สำหรับส่งไปหน้า fullscreenvideo.dart เพื่อเปิดแบบเต็มหน้าจอ แบบนี้เป็นต้น
 
เนื้อหาเกี่ยวกับการใช้งาน video ใน flutter ในตอนที่ 1 ก็ขอจบไว้เพียงเท่านี้ หวังว่าเป็นแนวทางนำไปปรับใช้งาน
ต่อไป เรายังมีเนื้อหาเกี่ยวกับการใช้งาน video ในตอนหน้า รอติดตาม


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


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

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


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


การปรับแต่งส่วนของ Slider ทำได้ดังนี้

SliderTheme(
  data: SliderTheme.of(context).copyWith(
    trackHeight:2.0, // Thicker track height like a progress bar
    activeTrackColor:Colors.blue, // Color for active progress
    inactiveTrackColor:Colors.grey[300], // Color for the remaining part
    thumbColor: Colors.transparent, // Hide thumb to make it look like a progress indicator
    thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6.0),
    overlayColor: Colors.transparent, // Hide the thumb overlay
  ),
  child: Slider(
    value: _currentPosition.inMicroseconds.toDouble(),
    min: 0,
    max: _controller.value.duration.inMicroseconds.toDouble(),
    onChanged: (value) {
      setState(() {
      _controller
          .seekTo(Duration(microseconds: value.toInt()));
      });
    },
  ),
),


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


การแสดงวิดีโอจาก Asset และ File

 
เนื้อหานี้จะเพิ่มเติมการใช้งานเล็กน้อย ให้สามารถนำไปประยุกต์ใช้งานได้ คือ เดิมเราดึงข้อมูล
วิดีโอจาก network หรือผ่าน internet  แต่สมมติว่าเรามีไฟล์คลิปเล็กๆ ที่จะใช้ในแอป และ
ไม่ต้องการดึงจาก internet ก็สามารถปรับเล็กน้อยเป็นดังนี้ได้ 
 
ไฟล์กำหนดใน assets ผ่าน pubspec.yaml
 
  # To add assets to your application, add an assets section, like this:
  assets:
    - assets/butterfly.mp4
 
ส่วนของการใช้งานก็กำหนดเป็นดังนี้
 
....
..
  String videoAsset = ''; // Variable to store video Asset

  @override
  void initState() {
    super.initState();
    print("debug: initialize");
    videoAsset = 'assets/butterfly.mp4';
    videoUrl =
        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
    // _controller = VideoPlayerController.networkUrl(Uri.parse(videoUrl))
    _controller = VideoPlayerController.asset(videoAsset)
      ..addListener(() {
        // Listening for changes in VideoPlayerValue
        setState(() {
          if (_controller.value.isPlaying) {
            print("debug: Playing");
            print("debug: ${_controller.value.duration}");
          } else if (_controller.value.isBuffering) {
            print("debug: Buffering");
          } else if (_controller.value.isCompleted) {
            print("debug: Finished");
          } else {
            print("debug: Paused");
          }
          _currentPosition =
              _controller.value.position; // Update current position
        });
      })
      ..initialize().then((_) {
        print("debug: video initialize");
        // _controller.play();
        // _controller.setLooping(true);
        setState(() {});
      });
      
  }
..
....
 
เปลี่ยนจาก VideoPlayerController.networkUrl() เป็น 
VideoPlayerController.asset()
 
ต่อไป ถ้าสมมติเราต้องการเลือกวิดีโอไฟล์จากเครื่องมือถือเข้ามาแสดงและเล่นใน video_player
ก็สามารถประยุกต์ใช้งานร่วมกับ path_provider และ file_picker
 
การใช้งาน Path Provider และการเชียนอ่าน File ใน Flutter http://niik.in/1066
การ Import ไฟล์โดยใช้ File Picker ใน Flutter http://niik.in/1070 
 
แล้วเรียกใช้งานผ่าน VideoPlayerController.file()
 
ไฟล์ด้านล่างเป็นโค้ดตัวอย่าง การประยุกต์ทั้งหมด
 

ไฟล์ clip.dart

 

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:file_picker/file_picker.dart';
import 'package:video_player/video_player.dart';


class Clip extends StatefulWidget {
  static const routeName = '/clip';

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

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

class _ClipState extends State<Clip> {
  late VideoPlayerController _controller;
  String videoUrl = ''; // Variable to store video URL
  String videoAsset = ''; // Variable to store video Asset
  // เพิ่มส่วนของ สถานะการเล่นอยู่ และเวลาของตำแหน่งคลิปปัจจุบัน
  bool _isPlaying = false;
  Duration _currentPosition = Duration.zero;

  // ส่วนของการจัดการ path ไฟล์
  List<FileSystemEntity?>? _folders;
  String _currentPath = ''; // เก็บ path ปัจจุบัน
  Directory? _currentFolder; // เก็บ โฟลเดอร์ที่กำลังใช้งาน

  @override
  void initState() {
    super.initState();
    print("debug: initialize");
    videoAsset = 'assets/butterfly.mp4';
    videoUrl =
        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4';
    // _controller = VideoPlayerController.networkUrl(Uri.parse(videoUrl))
    _controller = VideoPlayerController.asset(videoAsset)
      ..addListener(() {
        // Listening for changes in VideoPlayerValue
        setState(() {
          if (_controller.value.isPlaying) {
            print("debug: Playing");
            print("debug: ${_controller.value.duration}");
          } else if (_controller.value.isBuffering) {
            print("debug: Buffering");
          } else if (_controller.value.isCompleted) {
            print("debug: Finished");
          } else {
            print("debug: Paused");
          }
          _currentPosition =
              _controller.value.position; // Update current position
        });
      })
      ..initialize().then((_) {
        print("debug: video initialize");
        // _controller.play();
        // _controller.setLooping(true);
        setState(() {});
      });
      
    _loadFolder();
  }

  void _loadFolder() async {
    final appDocumentsDirectory = await getApplicationDocumentsDirectory();
    // เมื่อโหลดขึ้นมา เาจะเปิดโฟลเดอร์ของ package เป้นโฟลเดอร์หลัก
    _currentFolder = appDocumentsDirectory.parent;
    _currentPath = appDocumentsDirectory.parent.path;
    final myDir = Directory(_currentPath);
    setState(() {
      _folders = myDir.listSync(recursive: false, followLinks: false);
    });
  }

  // เปิดโฟลเดอร์ และแสดงรายการในโฟลเดอร์
  void _setPath(dir) async {
    _currentFolder = dir;
    _currentPath = dir.path;
    final myDir = Directory(_currentPath);
    try {
      setState(() {
        _folders = myDir.listSync(recursive: false, followLinks: false);
      });
    } catch (e) {
      print(e);
    }
  }

  // Helper function to format duration as mm:ss
  String _formatDuration(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    final minutes = twoDigits(duration.inMinutes.remainder(60));
    final seconds = twoDigits(duration.inSeconds.remainder(60));
    return "$minutes:$seconds";
  }

  // คำสั่ง import ไฟล์ผ่าน file_picker
  void _importFile() async {
    try {
      // FilePickerResult? result = await FilePicker.platform.pickFiles();
      FilePickerResult? result = await FilePicker.platform.pickFiles(
        // allowMultiple: true,
        type: FileType.custom,
        allowedExtensions: ['mp4'],
      );
      if (result != null) {
        // File _file = File(result.files.single.path!);
        List<File> _file = result.paths.map((path) => File(path!)).toList();
        result.files.forEach((file) async {
          // วนลุป copy ไฟล์ไปยังโฟลเดอร์ที่ใช้งาน
          // กำหนด path ของไฟล์ใหม่ แล้วทำการ copy จาก cache ไปไว้ใน app
          String newPath = "${_currentFolder!.path}/${file.name}";
          var cachefile = File(file.path!); // กำหนด file object
          // var newFile = await cachefile.copy(newPath);
          // var newFile = File(newPath);
          

          // ข้อมูลไฟล์
          print("debug: file name ${file.name}");
          print("debug: file bytes ${file.bytes}");
          print("debug: file size ${file.size}");
          print("debug: file extension ${file.extension}");
          print("debug: file cache path ${file.path}");
          print("debug: new file path ${newPath}");
          // ตัวสุดท้ายทำงานเสร็จ
          if (file == result.files.last) {
            _controller = VideoPlayerController.file(cachefile)
              ..addListener(() {
                // Listening for changes in VideoPlayerValue
                setState(() {
                  if (_controller.value.isPlaying) {
                    print("debug: Playing");
                    print("debug: ${_controller.value.duration}");
                  } else if (_controller.value.isBuffering) {
                    print("debug: Buffering");
                  } else if (_controller.value.isCompleted) {
                    print("debug: Finished");
                  } else {
                    print("debug: Paused");
                  }
                  _setPath(_currentFolder!);
                  _currentPosition =
                      _controller.value.position; // Update current position
                });
              })
              ..initialize().then((_) {
                print("debug: video initialize");
                _controller.play();
                _controller.setLooping(true);
                setState(() {});
              });
          }
        });
      } else {
        print("debug: User canceled the picker");
      }
    } catch (e) {
      print(e);
    }
  }

  @override
  void dispose() {
    _controller.removeListener(() {});
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Clip'),
        actions: [
          IconButton(
            onPressed: _importFile, //
            icon: FaIcon(FontAwesomeIcons.fileImport),
          ),
        ],
      ),
      body: Stack(
        children: [
          Center(
            child: _controller.value.isInitialized
                ? AspectRatio(
                    aspectRatio: _controller.value.aspectRatio,
                    child: GestureDetector(
                      onTap: () {
                        setState(() {
                          _isPlaying = !_isPlaying;
                          _controller.value.isPlaying &&
                                  !_controller.value.isCompleted
                              ? _controller.pause()
                              : _controller.play();
                        });
                      },
                      child: VideoPlayer(_controller),
                    ),
                  )
                : const CircularProgressIndicator(),
          ),
          // Video seek bar
          if (_controller.value.isInitialized)
            Positioned(
              bottom: 30,
              left: 20,
              right: 20,
              child: Column(
                children: [
                  SliderTheme(
                    data: SliderTheme.of(context).copyWith(
                      trackHeight:
                          2.0, // Thicker track height like a progress bar
                      activeTrackColor:
                          Colors.blue, // Color for active progress
                      inactiveTrackColor:
                          Colors.grey[300], // Color for the remaining part
                      thumbColor: Colors
                          .transparent, // Hide thumb to make it look like a progress indicator
                      thumbShape:
                          RoundSliderThumbShape(enabledThumbRadius: 6.0),
                      overlayColor:
                          Colors.transparent, // Hide the thumb overlay
                    ),
                    child: Slider(
                      value: _currentPosition.inMicroseconds.toDouble(),
                      min: 0,
                      max: _controller.value.duration.inMicroseconds.toDouble(),
                      onChanged: (value) {
                        setState(() {
                          _controller
                              .seekTo(Duration(microseconds: value.toInt()));
                        });
                      },
                    ),
                  ),
                  // Display current position and total duration
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(
                        _formatDuration(_currentPosition),
                        style: const TextStyle(color: Colors.black),
                      ),
                      Text(
                        _formatDuration(_controller.value.duration),
                        style: const TextStyle(color: Colors.black),
                      ),
                    ],
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}


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



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



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









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






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

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

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

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



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




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





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

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


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


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







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