Built-in UI


The fplayer core package ships a minimal panel called minimalFPanelBuilder. The optional fplayer_ui package adds a full playback bar, gestures, and a settings panel:

dependencies:
  fplayer: ^1.3.0
  fplayer_ui:
    git:
      url: https://github.com/FlutterPlayer/fplayer.git
      path: packages/fplayer_ui
import 'package:fplayer/fplayer.dart';
import 'package:fplayer_ui/fplayer_ui.dart';

fplayer_ui isn't published separately on pub.dev. It's maintained in the fplayer repository.

Default skin

The default panel in fplayer_ui covers the most basic playback capability.

Usage:

FView(
  player: player,
  panelBuilder: defaultFPanelBuilder,
)

Screenshot:

Screenshot of the default FPlayer panel: a landscape video frame with the progress bar on the left

Advanced skin

The advanced skin extends the default UI with a playback bar, gestures, a settings panel, and more. You'll need the fplayer_ui dependency and imports shown above before using it.

Single video setup

Configuration for playing a single video.

Usage:


void initState() {
  super.initState();
  // Play the supplied video
  player.setDataSource(widget.url, autoPlay: true, showCover: true);
}


void dispose() {
  super.dispose();
  player.release();
}

FView(
  player: player,
  width: double.infinity,
  height: videoHeight, // Set these yourself; width / height = 16:9 here
  color: Colors.black,
  fsFit: FFit.contain, // Fill mode in fullscreen
  fit: FFit.fill, // Fill mode in normal mode
  panelBuilder: fPanelBuilder(
    title: 'Video title',
    subTitle: 'Video subtitle',
  ),
)

Screenshots:

Inline modeFullscreen mode
Single video inline mode demo screenshotSingle video fullscreen mode demo screenshot

Multi video setup

Configuration for playing a list of videos, for example advancing to the next episode automatically when one finishes.

Start with a few variables and a helper method:

// Video list
List<VideoItem> videoList = [
  VideoItem(
    title: 'Episode 1',
    subTitle: 'Episode 1 subtitle',
    url: 'http://player.alicdn.com/video/aliyunmedia.mp4',
  ),
  VideoItem(
    title: 'Episode 2',
    subTitle: 'Episode 2 subtitle',
    url: 'https://www.runoob.com/try/demo_source/mov_bbb.mp4',
  ),
  VideoItem(
    title: 'Episode 3',
    subTitle: 'Episode 3 subtitle',
    url: 'http://player.alicdn.com/video/aliyunmedia.mp4',
  ),
];

// Video index; omit it for a single video
int videoIndex = 0;

// Play the supplied URL
Future<void> setVideoUrl(String url) async {
  try {
    await player.setDataSource(url, autoPlay: true, showCover: true);
  } catch (error) {
    print("Playback error: $error");
    return;
  }
}


void initState() {
  super.initState();
  // Play the first video in the list
  setVideoUrl(videoList[videoIndex].url);
}


void dispose() {
  super.dispose();
  player.release();
}

Then pass them to FView:

Column(
  children: [
    FView(
      player: player,
      width: double.infinity,
      height: videoHeight, // Set these yourself; width / height = 16:9 here
      color: Colors.black,
      fsFit: FFit.contain, // Fill mode in fullscreen
      fit: FFit.fill, // Fill mode in normal mode
      panelBuilder: fPanelBuilder(
        // Video list toggle
        isVideos: true,
        // Video list
        videoList: videoList,
        // Current video index
        videoIndex: videoIndex,
        // Callback for playing the next episode in fullscreen
        playNextVideoFun: () {
          setState(() {
            videoIndex += 1;
          });
        },
        // Playback completed callback
        onVideoEnd: () async {
          var index = videoIndex + 1;
          if (index < videoList.length) {
            await player.reset();
            setState(() {
              videoIndex = index;
            });
            setVideoUrl(videoList[index].url);
          }
        },
      ),
    ),
    // Custom compact video list
    Container(
      width: double.infinity,
      height: 30,
      margin: const EdgeInsets.all(20),
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        padding: EdgeInsets.zero,
        itemCount: videoList.length,
        itemBuilder: (context, index) {
          bool isCurrent = videoIndex == index;
          Color textColor = Theme.of(context).primaryColor;
          Color bgColor = Theme.of(context).primaryColorDark;
          Color borderColor = Theme.of(context).primaryColor;
          if (isCurrent) {
            textColor = Theme.of(context).primaryColorDark;
            bgColor = Theme.of(context).primaryColor;
            borderColor = Theme.of(context).primaryColor;
          }
          return GestureDetector(
            onTap: () async {
              await player.reset();
              setState(() {
                videoIndex = index;
              });
              setVideoUrl(videoList[index].url);
            },
            child: Container(
              margin: EdgeInsets.only(left: index == 0 ? 0 : 10),
              padding: const EdgeInsets.symmetric(horizontal: 5),
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(5),
                color: bgColor,
                border: Border.all(
                  width: 1.5,
                  color: borderColor,
                ),
              ),
              alignment: Alignment.center,
              child: Text(
                videoList[index].title,
                style: TextStyle(
                  fontSize: 15,
                  color: textColor,
                ),
              ),
            ),
          );
        },
      ),
    ),
  ],
)

Screenshots:

Inline modeFullscreen mode
Multi video inline mode demo screenshotMulti video fullscreen mode demo screenshot

Features

The player has both built-in and configurable features. The built-in ones are gestures: drag horizontally to rewind or fast forward, drag vertically to adjust brightness and volume, double tap to pause or resume, and long press to speed up, then release to return to normal speed. The rest of this section covers what you can configure.

Usage:

// Playback speed list
Map<String, double> speedList = {
  "2.0": 2.0,
  "1.5": 1.5,
  "1.0": 1.0,
  "0.5": 0.5,
};

// Resolution list
Map<String, ResolutionItem> resolutionList = {
  "480P": ResolutionItem(
    value: 480,
    url: 'https://www.runoob.com/try/demo_source/mov_bbb.mp4',
  ),
  "270P": ResolutionItem(
    value: 270,
    url: 'http://player.alicdn.com/video/aliyunmedia.mp4',
  ),
};

// Progress to seek to after initialization when restoring history
int seekTime = 100000;

fPanelBuilder(
  // Snapshot button at bottom right
  isSnapShot: true,
  // Top-right button group toggle
  isRightButton: true,
  // Top-right button group
  rightButtonList: [
    InkWell(
      onTap: () {},
      child: Container(
        padding: const EdgeInsets.all(10),
        decoration: BoxDecoration(
          color: Theme.of(context).primaryColorLight,
          borderRadius: const BorderRadius.vertical(
            top: Radius.circular(5),
          ),
        ),
        child: Icon(
          Icons.favorite,
          color: Theme.of(context).primaryColor,
        ),
      ),
    ),
    InkWell(
      onTap: () {},
      child: Container(
        padding: const EdgeInsets.all(10),
        decoration: BoxDecoration(
          color: Theme.of(context).primaryColorLight,
          borderRadius: const BorderRadius.vertical(
            bottom: Radius.circular(5),
          ),
        ),
        child: Icon(
          Icons.thumb_up,
          color: Theme.of(context).primaryColor,
        ),
      ),
    )
  ],
  // Custom playback speed list
  speedList: speedList,
  // Resolution toggle
  isResolution: true,
  // Custom resolution list
  resolutionList: resolutionList,
  settingFun: () {
    print('Settings button clicked');
  },
  // Refresh callback for playback errors
  onError: () async {
    await player.reset();
    setVideoUrl(videoList[videoIndex].url);
  },
  onVideoTimeChange: () {
    // Fires when playback position changes; persist history here
  },
  onVideoPrepared: () async {
    // After initialization, seek to the saved position when history exists
    try {
      if (seekTime >= 1) {
        /// seekTo requires FState.prepared
        print('seekTo');
        await player.seekTo(seekTime);
        // print("Seek to $seekTime");
        seekTime = 0;
      }
    } catch (error) {
      print("Seek after initialization failed: $error");
    }
  },
)

Screenshot:

Screenshot of the advanced FPlayer skin: a landscape video with the bottom playback bar and side action buttons

API

Properties

NameTypeDescription
titleStringVideo title in single video mode
subTitleStringVideo subtitle in single video mode
isSnapShotboolWhether to show the snapshot button. Defaults to false
isRightButtonboolWhether to show the button group at the top right of the fullscreen area. Defaults to false
rightButtonListList<Widget>?Button group at the top right of the fullscreen area; three buttons or fewer is recommended
isVideosboolWhether to use multi video mode. Defaults to false
videoListList<VideoItem>?List of videos for multi video mode
speedListMap<String, double>?Playback speed list; must include 1.0x
isResolutionboolWhether to show the resolution button. Defaults to false
resolutionListMap<String, ResolutionItem>?Resolution list

Methods

Method nameDescription
playNextVideoFunFires when the play next button is tapped in multi video fullscreen mode
settingFunFires when the settings button at the top right is tapped
onErrorCallback for tapping refresh after a playback error
onVideoEndFires when a video finishes playing
onVideoTimeChangeFires once whenever the playback position changes, so you can save progress
onVideoPreparedFires when the video is ready; seek to a saved position here if you have one