เนื้อหาในตอนต่อไปนี้ เราจะมาดูเกี่ยวกับการใช้งาน 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 ในตอนหน้า รอติดตาม