ประยุกต์แสดงคลิป Video คล้าย TikTok ใน Flutter ตอนที่ 2

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

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

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


จากตอนที่แล้ว เราได้รู้จักกับการนำ video มาแสดงใน flutter
และได้เพิ่มเติมเนื้อหาเกี่ยวกับการใช้จาก file และจาก asset ใน
ตอนท้ายของเนื้อหาเพิ่มเติม เนื้อหานี้เราจะมาลองประยุกต์การแสดง
video ให้คล้ายกับ tiktok คือสามารถเลื่อนขึ้นและเลื่อนลงดูคลิปได้ง่าย
รวมถึงการประยุกต์อื่นๆ เช่น โหลดอัตโนมัติเพื่อให้การแสดงผลดูไหลลื่นขึ้น
การให้สามารถซูมคลิปวิดีโอได้ หรืออื่นๆ เพิ่มเติมที่อาจจะมีในอนาคต
*ท้ายบทความมีตัวอย่างโค้ดให้ดาวน์โหลด
 
ทบทวนตอนที่แล้วได้ที่
การจัดการและใช้งาน เกี่ยวกับ Video ใน Flutter ตอนที่ 1 http://niik.in/1113
 

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

    - การแสดง video คล้ายรูปแบบ tiktok
    - รู้จักการ preload video เพื่อให้การแสดงลื่นไหลขึ้น
    - สามารถทำการ zoom video ได้
    - และอื่นๆ ในอนาคตในเนื้อหาเพิ่มเติม
 

แนวทางการแสดง video คล้ายรูปแบบ Tiktok

    ในที่นี้เราจะตั้งชื่อหัวข้อว่า loopster คล้ายกับรูปแบบการแสดงที่วิดีโอจะเล่นและวนซ้ำไปเรื่อยๆ
อย่างที่เราทราบกันว่า ในการเลื่อนคลิปใน tiktok จะมีช่วงจังหวะของคลิปที่เล่นอยู่และกำลังถูกเลื่อน
และปิดไปจากหน้าจอ กับจังหวะของคลิปที่กำลังถูกเลื่อนขึ้นมาแสดงแทน ซึ่งการจัดการในส่วนนี้เราจะ
ใช้ plugin ที่ชื่อว่า visibility_detector ดังนั้นก่อนใช้งาน เราควรติดตั้งส่วนนี้ก่อน
 
visibility_detector: ^0.4.0+2
 
เราใช้ VisibilityDetector ในการทำงานเช่น จังหวะเลื่อนปิดคลิปเดิมและเปิดคลิปใหม่ เราจะทำการ
หยุดการเล่นคลิปเดิม และกลับมาเล่นอีกครั้งถ้าเลื่อนกลับมาแสดง ซึ่งอาศัยความสามารถของ plugin
ตัวนี้ที่มีมาให้มาช่วย
 
และอีกส่วนที่สำคัญหลัก สำหรับการแสดงรายการหรือจะเรียกว่า หลายๆ เพจ และให้สามารถเลื่อน
รายการไปในทิศทางที่ต้องการได้ ก็คือการใช้งาน PageView.builder โดยจะทำการวนลูปนำราย
การ url ของคลิปวิดีโอไปแสดง
 
มาดูตัวอย่างโค้ดรูปแบบแรก
 

ไฟล์ loopster.dart

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

class Loopster extends StatefulWidget {
  static const routeName = '/loopster';

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

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

class _LoopsterState extends State<Loopster> {

  // ลิสรายการคลิปสำหรับทดสอบ สามารถประยุก๖ืดึงจาก api ได้
  final List<String> videoUrls = [
    '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',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      extendBodyBehindAppBar: true, 
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        foregroundColor: Colors.white,
        title: const Text('Loopster'),
      ),   
      body: PageView.builder(
        scrollDirection: Axis.vertical, // เลื่อนแนวตั้ง
        itemCount: videoUrls.length,
        itemBuilder: (context, index) {
          // วนลูปสร้างหน้ารายการคลิปสำหรับแสดง โดยส่ง url เข้าไปใช้งาน
          return VideoPlayerItem(videoUrl: videoUrls[index]);
        },
      ),
    );
  }
}  

// สร้าง class สำหรับแสดงรายการวิดีโอ จาก url ที่ส่งมา โดยใช้งาน video_player
class VideoPlayerItem extends StatefulWidget {
  final String videoUrl;
  const VideoPlayerItem({Key? key, required this.videoUrl}) : super(key: key);

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

class _VideoPlayerItemState extends State<VideoPlayerItem> {
  // กำหนด controller สำหรับควบคุม video_player
  late VideoPlayerController _controller;
  // สำหรับกำหนดว่าส่วนควบคุมวิดีโอนั้น ถูกลบออกไปแล้วหรือไม่
  bool _isControllerDisposed = false;

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

  void _initializeVideoPlayer() {
    _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl))
      ..initialize().then((_) {
        if (mounted) {
          setState(() {
            _controller.play();
            _controller.setLooping(true);
          });
        }
      });
  }

  @override
  void dispose() {
    _isControllerDisposed = true;
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return VisibilityDetector(
      key: Key(widget.videoUrl),
      onVisibilityChanged: (VisibilityInfo info) {
        if (_isControllerDisposed) return;
        if (info.visibleFraction == 0) {
          _controller.pause(); // หยุดวิดีโอเมื่อถูกซ่อน
        } else {
          _controller.play(); // เล่นวิดีโอเมื่อแสดง
        }
      },
      child: Stack(
        alignment: Alignment.center,
        children: [
          _controller.value.isInitialized
              ? AspectRatio(
                  aspectRatio: _controller.value.aspectRatio,
                  child: VideoPlayer(_controller),
                )
              : Center(child: CircularProgressIndicator()),
          Positioned(
            bottom: 50,
            left: 20,
            child: Text(
              'Video Description Here',
              style: TextStyle(color: Colors.white, fontSize: 16),
            ),
          ),
        ],
      ),
    );
  }
}
 

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

 

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

การเพิ่มการ Zoom ให้กับคลิปวิดีโอ

    สมมติเราต้องการให้ video สามารถ zoom ได้ สามารถปรับส่วนของการแสดง video เล็กน้อย
โดยการใช้งาน InteractiveViewer เพิ่มเข้าไปในส่วนนี้
 
          _controller.value.isInitialized
/*               ? AspectRatio(
                  aspectRatio: _controller.value.aspectRatio,
                  child: VideoPlayer(_controller),
                ) */
              ? InteractiveViewer(
                  panEnabled: true, // Enable panning
                  boundaryMargin: EdgeInsets.all(0),
                  minScale: 1.0, // Minimum zoom scale
                  maxScale: 4.0, // Maximum zoom scale
                  child: AspectRatio(
                    aspectRatio: _controller!.value.aspectRatio,
                    child: VideoPlayer(_controller!),
                  ),
                )                  
              : Center(child: CircularProgressIndicator()),
 
ส่วนที่ปิดคอมเม้น คือตัวเดิมที่ไม่มีการ zoom ดังนั้น หากต้องการให้มีการ zoom ก็ใช้รูปแบบดัง
ต่อไปในแทนหรือแทรกเข้าไปได้ โดยสามารถทำได้ทั้งการ zoom และ การ pan หรือเลื่อนตำแหน่ง
 
 

แนวทางการแสดง video แบบ Preload 

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

ไฟล์ loopster.dart

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

class Loopster extends StatefulWidget {
  static const routeName = '/loopster';

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

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

class _LoopsterState extends State<Loopster> {

  // ลิสรายการคลิปสำหรับทดสอบ สามารถประยุก๖ืดึงจาก api ได้
  final List<String> videoUrls = [
    '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/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',     
  ];

  // จัดการตัวควบคุม pageview
  late PageController _pageController;
  // ใช้งาน controller ของ video แบบ array หรือ list
  Map<int, VideoPlayerController> _videoControllers = {};  


  @override
  void initState() {
    super.initState();
    _pageController = PageController();
    _initializeVideo(0); // Preload the first video
    _initializeVideo(1); // Preload the second video
  }

  @override
  void dispose() {
    _pageController.dispose();
    // Dispose all VideoPlayerControllers
    for (var controller in _videoControllers.values) {
      controller.dispose();
    }
    super.dispose();
  }  

  // ฟังก์ชั่นควบคุม controler ของ video play 
  void _initializeVideo(int index) {
    // ถ้ามีแล้วไม่ต้องทำอะไร
    if (_videoControllers.containsKey(index)) return;

    // สร้างและ กำหนดค่าเริ่มต้น ของ video player สำหรับแต่ละ url ของ video
    final controller = VideoPlayerController.networkUrl(Uri.parse(videoUrls[index]));

    // เริ่มต้นการควบคุมการแสดงวิดีโอในห้นาที่กำลังทำงาน
    controller.initialize().then((_) {
      if (mounted) {
        setState(() {});
        if (index == _pageController.page?.toInt()) {
          controller.play();
        }
        controller.setLooping(true);
      }
    });
    // แยกแต่ละ controller สำหรับแต่ละคลิป
    _videoControllers[index] = controller;
  }

  void _preloadVideosAround(int index) {
    // Preload previous and next videos if within bounds
    if (index > 0) {
      _initializeVideo(index - 1);
    }
    if (index < videoUrls.length - 1) {
      _initializeVideo(index + 1);
    }
  }  

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      extendBodyBehindAppBar: true, 
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        foregroundColor: Colors.white,
        title: const Text('Loopster'),
      ),   
      body: PageView.builder(
        controller: _pageController, //ควบคุมด้วย controller
        scrollDirection: Axis.vertical, // เลื่อนแนวตั้ง
        itemCount: videoUrls.length,
        onPageChanged: (index) { // เพิ่มส่วนจัดการสำหรับ preload เข้ามา
          // preload video เมื่อมีการเปลี่ยนหน้า
          _preloadVideosAround(index);
        },        
        itemBuilder: (context, index) {
          // วนลูปสร้างหน้ารายการคลิปสำหรับแสดง โดยส่ง url เข้าไปใช้งาน
          // รูปแบบนี้เพิ่มส่วนของ controller แต่ละ video player เข้าไปด้วย
          return VideoPlayerItem(
            videoController: _videoControllers[index]!,
            videoUrl: videoUrls[index],
          );          
        },
      ),
    );
  }
}  

// สร้าง class สำหรับแสดงรายการวิดีโอ จาก url ที่ส่งมา โดยใช้งาน video_player
// รองรับ videoController เพิ่มเข้ามา
class VideoPlayerItem extends StatefulWidget {
  final VideoPlayerController videoController;
  final String videoUrl;
  
  const VideoPlayerItem({
    Key? key,
    required this.videoController,
    required this.videoUrl,
  }) : super(key: key);

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

class _VideoPlayerItemState extends State<VideoPlayerItem> {
  // สำหรับกำหนดว่าส่วนควบคุมวิดีโอนั้น ถูกลบออกไปแล้วหรือไม่
  bool _isControllerDisposed = false;

  @override
  void dispose() {
    _isControllerDisposed = true;
    widget.videoController.pause();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return VisibilityDetector(
      key: Key(widget.videoUrl),
      onVisibilityChanged: (VisibilityInfo info) {
        if (_isControllerDisposed) return;
        if (info.visibleFraction == 0) {
          widget.videoController.pause(); // หยุดวิดีโอเมื่อถูกซ่อน
        } else {
          widget.videoController.play(); // เล่นวิดีโอเมื่อแสดง
        }
      },
      child: Stack(
        alignment: Alignment.center,
        children: [
          widget.videoController.value.isInitialized
              ? InteractiveViewer(
                  panEnabled: true, // Enable panning
                  boundaryMargin: EdgeInsets.all(0),
                  minScale: 1.0, // Minimum zoom scale
                  maxScale: 4.0, // Maximum zoom scale
                  child: AspectRatio(
                  aspectRatio: widget.videoController.value.aspectRatio,
                  child: VideoPlayer(widget.videoController),
                  ),
                )                     
              : const Center(child: CircularProgressIndicator()),
          Positioned(
            bottom: 50,
            left: 20,
            child: const Text(
              'Video Description Here',
              style: TextStyle(color: Colors.white, fontSize: 16),
            ),
          ),
        ],
      ),
    );
  }
}
 

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



 
 
รูปแบบที่สอง เราแยกการจัดการส่วนของ controller แต่ละตัว และมีการ preload video ทำให้
การแสดงผลดูเนียนคล้าย tiktok มากขึ้น จังหวะเลื่อนระหว่างคลิป แทบจะไม่ค่อยเห็นการโหลดคลิป
ตัวถัดไป นั่นคือคลิปแสดงรอ แต่ยังไม่เล่น พอเลื่อนเข้าไปจะเริ่มเล่นคลิปนั้น อีกทั้งคลิปก่อนหน้าที่เล่น
ไปแล้ว และย้อนกลับไป ก็ไม่ต้องทำการโหลดใหม่ สามารถแสดงและเล่นได้ทันที
 
ในโค้ดไม่ได้ตกแต่ง หรือเพิ่มฟังก์ชั่นอะไรมาก อย่างไรก็ดี เราสามารถนำไปประยุกต์ได้ตามต้องการ เช่น
อาจจะสามารถกดเพื่อหยุดคลิปชั่วคราวได้  เราจะขอพาประยุกต์กัน
ดังนี้คือ ถ้าคลิปกำลังเล่นอยู่ เมื่อเรากดหนึ่งครั้ง จะหยุดชั่วคราว ให้เราสามารถกดซ้ำอีกครั้งเพื่อเล่นได้ 
แบบนี้เป็นต้น 
 
เดิม เล่นอย่างเดียว ไม่มีหยุด
 
  child: AspectRatio(
	  aspectRatio: widget.videoController.value.aspectRatio,
	  child: VideoPlayer(widget.videoController),
  ),
 
เปลี่ยนเป็น เล่นอยู่ กดหนึ่งครั้งเพื่อหยุด และกดซ้ำเพื่อเล่นต่อ
 
  child: AspectRatio(
  aspectRatio: widget.videoController.value.aspectRatio,
  child: GestureDetector(
	  onTap: () {
		setState(() {
		  widget.videoController.value.isPlaying
		  ? widget.videoController.pause()
		  : widget.videoController.play();
		});
	  },
	child: VideoPlayer(widget.videoController)),
  ),
 
เนื้อหาเกี่ยวกับการจัดการวิดีโอด้วย video_player ใน flutter ก็ขอจบเพียงเท่านี้
ตอนหน้าจะเป็นอะไร รอติดตาม


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


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

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


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


กรณีมีรายการวิดีโอจำนวนมากๆ แล้วเกิดปัญหาหน่วยความจำไม่เพียงพอ

 
ให้ทำการแก้ไขส่วนของการ พรีโหลดวิดีโอ หรือฟังก์ชั่น _preloadVideosAround โดยให้
ปรับใหม่เป็นดังนี้

  void _preloadVideosAround(int index) {
    // Dispose of controllers that are more than one item away from the current video
    _videoControllers.keys
        .where((i) => (i < index - 1) || (i > index + 1))
        .toList()
        .forEach((i) {
          _videoControllers[i]?.dispose();
          _videoControllers.remove(i);
        });

    // Preload the current, previous, and next videos if within bounds
    _initializeVideo(index);
    if (index > 0) {
      _initializeVideo(index - 1);
    }
    if (index < _videoUrls.length - 1) {
      _initializeVideo(index + 1);
    }    
  }  


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



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









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






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

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

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

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



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




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





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

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


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


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







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