Karol
 · Your go-to Flutterflow guy

Custom video player with play / pause on tap

When using Flutterflow native video player component, I was missing one crucial feature: play/pause video on tap.

Decided I will create my own version of FF player with this feature.

This Custom Video Player allows to play / pause video on tap or by variable connected to bool playPauseVideoAction. That way you can control your video by external logic.

Now you can also get this custom widget on marketplace:
https://marketplace.flutterflow.io/item/xqVb4LMkVDU7ndL6sij0

Installation

  1. Create new custom widget. Here I called it CustomVidPlayer.

  2. Add chewie package as a dependency:

    chewie: ^1.8.5

    You can find more info about package here:

    https://pub.dev/packages/chewie

  3. Insert other widget parameters:

    • videoPath is the only required parameter here.
      Other params are optional:

    • playPauseVideoAction allows you to play / pause your video based by external variable state

    • looping, showControls, allowFullScreen, allowPlaybackSpeedChanging are parameters from the default video player widget

    • controlAudio - turn audio on/off by variable

    • loadingCircleColor - change loading spinning icon color

    • enablePlayOnFocus - when enabled, video will auto play when you focus on it (eg. in a feed of videos, only one with focus will play). When disabled, you need to turn it on/off manually

    • imageThumbnail - display thumbnail image instead of loading circle

    • playbackSpeed - control speed of the video by variable

    • startTimeSeconds - manually set video start time (if you don't wait it to start from 0)

    • actualTimestamp - callback action returning current video timestamp. Use it to retrieve actual video time (eg. to resume video from that specific point).

  4. Copy paste code (note: this is new code from v2 version of custom widget)

    // Automatic FlutterFlow imports
    import '/flutter_flow/flutter_flow_theme.dart';
    import '/flutter_flow/flutter_flow_util.dart';
    import '/custom_code/widgets/index.dart'; // Imports other custom widgets
    import '/flutter_flow/custom_functions.dart'; // Imports custom functions
    import 'package:flutter/material.dart';
    // Begin custom widget code
    // DO NOT REMOVE OR MODIFY THE CODE ABOVE!
    
    import 'package:video_player/video_player.dart';
    import 'package:chewie/chewie.dart';
    import 'package:visibility_detector/visibility_detector.dart';
    import 'dart:async';
    
    class CustomVidPlayer extends StatefulWidget {
      CustomVidPlayer({
        Key? key,
        this.width,
        this.height,
        required this.videoPath,
        this.playPauseVideoAction = false,
        this.looping = false,
        this.showControls = false,
        this.allowFullScreen = false,
        this.allowPlayBackSpeedChanging = false,
        this.controlAudio = false,
        this.loadingCircleColor,
        this.enablePlayOnFocus = false,
        this.imageThumbnail,
        this.playbackSpeed,
        this.startTimeSeconds,
        this.actualTimestamp,
      }) : super(key: key);
    
      final double? width;
      final double? height;
      final String videoPath;
      bool playPauseVideoAction;
      final bool looping;
      final bool showControls;
      final bool allowFullScreen;
      final bool allowPlayBackSpeedChanging;
      final bool controlAudio;
      final Color? loadingCircleColor;
      final bool enablePlayOnFocus;
      final String? imageThumbnail;
      final double? playbackSpeed;
      final int? startTimeSeconds;
      final Future Function(int timestampSeconds)? actualTimestamp;
    
      @override
      _CustomVidPlayerState createState() => _CustomVidPlayerState();
    }
    
    class _CustomVidPlayerState extends State<CustomVidPlayer> {
      late VideoPlayerController _videoPlayerController;
      late ChewieController _chewieController;
      Timer? _positionTimer;
    
      void _startPositionTracking() {
        //print('Starting position tracking...'); // Debug print
        _positionTimer?.cancel();
    
        _positionTimer = Timer.periodic(Duration(milliseconds: 800), (timer) {
          if (_videoPlayerController.value.isInitialized) {
            final currentSeconds = _videoPlayerController.value.position.inSeconds;
            // print('Video position: $currentSeconds seconds'); // Debug print
            widget.actualTimestamp!(currentSeconds);
          }
        });
      }
    
      @override
      void initState() {
        super.initState();
        _videoPlayerController = VideoPlayerController.network(widget.videoPath)
          ..initialize().then((_) {
            setState(() {});
            if (widget.startTimeSeconds != null && widget.startTimeSeconds! > 0) {
              _videoPlayerController
                  .seekTo(Duration(seconds: widget.startTimeSeconds!));
            }
    
            _chewieController = ChewieController(
              videoPlayerController: _videoPlayerController,
              aspectRatio: _videoPlayerController.value.aspectRatio,
              looping: widget.looping,
              showControls: widget.showControls,
              allowFullScreen: widget.allowFullScreen,
              allowPlaybackSpeedChanging: widget.allowPlayBackSpeedChanging,
              autoPlay: false,
              materialProgressColors: ChewieProgressColors(
                playedColor: widget.loadingCircleColor ?? Colors.blue,
                handleColor: widget.loadingCircleColor ?? Colors.blue,
              ),
            );
    
            _startPositionTracking();
            _updateAudioVolume();
            _updatePlaybackSpeed();
          });
      }
    
      @override
      void didUpdateWidget(CustomVidPlayer oldWidget) {
        super.didUpdateWidget(oldWidget);
        if (widget.playPauseVideoAction != oldWidget.playPauseVideoAction) {
          if (widget.playPauseVideoAction) {
            _videoPlayerController.play();
            _startPositionTracking();
          } else {
            _videoPlayerController.pause();
            _positionTimer?.cancel();
          }
        }
        if (widget.controlAudio != oldWidget.controlAudio) {
          _updateAudioVolume();
        }
        if (widget.playbackSpeed != oldWidget.playbackSpeed) {
          _updatePlaybackSpeed();
        }
      }
    
      void _updateAudioVolume() {
        double volume = widget.controlAudio ? 1.0 : 0.0;
        _videoPlayerController.setVolume(volume);
      }
    
      void _updatePlaybackSpeed() {
        double speed = widget.playbackSpeed ?? 1.0;
        // Limit playback speed between 0.5 and 2.0
        speed = speed.clamp(0.5, 2.0);
        _videoPlayerController.setPlaybackSpeed(speed);
      }
    
      @override
      void dispose() {
        _positionTimer?.cancel();
        _videoPlayerController.dispose();
        _chewieController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return VisibilityDetector(
          key: Key(widget.videoPath),
          onVisibilityChanged: (visibilityInfo) {
            if (widget.enablePlayOnFocus) {
              if (visibilityInfo.visibleFraction >= 1) {
                if (widget.playPauseVideoAction) {
                  _videoPlayerController.play();
                }
              } else {
                _videoPlayerController.pause();
              }
            }
          },
          child: Container(
            width: widget.width ?? double.infinity,
            height: widget.height ?? double.infinity,
            child: _videoPlayerController.value.isInitialized
                ? Stack(
                    children: [
                      Chewie(controller: _chewieController),
                      if (!widget.showControls)
                        GestureDetector(
                          onTap: () {
                            setState(() {
                              widget.playPauseVideoAction =
                                  !widget.playPauseVideoAction;
                              if (widget.playPauseVideoAction) {
                                _videoPlayerController.play();
                              } else {
                                _videoPlayerController.pause();
                              }
                            });
                          },
                          behavior: HitTestBehavior.opaque,
                        ),
                    ],
                  )
                : widget.imageThumbnail != null
                    ? Image.network(
                        widget.imageThumbnail!,
                        width: widget.width,
                        height: widget.height,
                        fit: BoxFit.cover,
                      )
                    : Center(
                        child: CircularProgressIndicator(
                          valueColor: AlwaysStoppedAnimation<Color>(
                            widget.loadingCircleColor ?? Colors.blue,
                          ),
                        ),
                      ),
          ),
        );
      }
    }

And that's it! Hope that Flutterflow team eventually will add this feature to their video player. But in the meantime, feel free to use it and share feedback!

24
95 replies