td_fplayer

pub package

基于 ijkplayer 的 Flutter 视频播放器插件,支持 Android 和 iOS 平台。

本项目基于 fplayer 进行二次开发,替换为标准 ijkplayer 内核。

特性

  • ✅ 支持 Android 和 iOS 双平台
  • ✅ 支持常见视频格式(MP4、FLV、RTMP、HLS 等)
  • ✅ 支持硬件解码
  • ✅ 支持倍速播放
  • ✅ 支持清晰度切换
  • ✅ 支持视频列表播放
  • ✅ 支持全屏模式
  • ✅ 支持亮度和音量手势调节
  • ✅ 支持播放进度手势调节
  • ✅ 支持视频截图(iOS)
  • ✅ 内置美观的播放控制面板

安装

pubspec.yaml 中添加依赖:

dependencies:
  td_fplayer: ^0.0.1-beta.1

快速开始

基本使用

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

class VideoPlayerPage extends StatefulWidget {
  @override
  _VideoPlayerPageState createState() => _VideoPlayerPageState();
}

class _VideoPlayerPageState extends State<VideoPlayerPage> {
  final FPlayer player = FPlayer();

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

  Future<void> _initPlayer() async {
    // 基础配置
    await player.setOption(FOption.hostCategory, "request-screen-on", 1);
    await player.setOption(FOption.hostCategory, "request-audio-focus", 1);
    
    // 硬件解码配置(防止花屏)
    await player.setOption(FOption.playerCategory, "mediacodec", 1);
    await player.setOption(FOption.playerCategory, "mediacodec-auto-rotate", 1);
    await player.setOption(FOption.playerCategory, "mediacodec-handle-resolution-change", 1);
    
    // 缓冲配置(防止花屏)
    await player.setOption(FOption.playerCategory, "packet-buffering", 1);
    
    // 设置视频源并播放
    await player.setDataSource(
      'http://player.alicdn.com/video/aliyunmedia.mp4',
      autoPlay: true,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('视频播放器')),
      body: FView(
        player: player,
        width: double.infinity,
        height: 200,
        color: Colors.black,
        panelBuilder: fPanelBuilder(
          title: '视频标题',
        ),
      ),
    );
  }

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

完整示例

import 'package:flutter/material.dart';
import 'package:td_fplayer/td_fplayer.dart';
import 'package:screen_brightness/screen_brightness.dart';

class VideoScreen extends StatefulWidget {
  final String url;

  const VideoScreen({super.key, required this.url});

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

class VideoScreenState extends State<VideoScreen> {
  final FPlayer player = FPlayer();

  // 视频列表
  List<VideoItem> videoList = [
    VideoItem(
      title: '第一集',
      subTitle: '视频1副标题',
      url: 'http://player.alicdn.com/video/aliyunmedia.mp4',
    ),
    VideoItem(
      title: '第二集',
      subTitle: '视频2副标题',
      url: 'https://www.runoob.com/try/demo_source/mov_bbb.mp4',
    ),
  ];

  // 倍速列表
  Map<String, double> speedList = {
    "2.0": 2.0,
    "1.5": 1.5,
    "1.0": 1.0,
    "0.5": 0.5,
  };

  // 清晰度列表
  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',
    ),
  };

  int videoIndex = 0;

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

  void startPlay() async {
    // 基础配置
    await player.setOption(FOption.hostCategory, "enable-snapshot", 1);
    await player.setOption(FOption.hostCategory, "request-screen-on", 1);
    await player.setOption(FOption.hostCategory, "request-audio-focus", 1);
    
    // 播放器配置
    await player.setOption(FOption.playerCategory, "reconnect", 20);
    await player.setOption(FOption.playerCategory, "framedrop", 20);
    await player.setOption(FOption.playerCategory, "enable-accurate-seek", 1);
    // soundtouch: 变速播放时的音频处理
    // 1 = 变速不变调(音调正常,但直播录制等不规则音频可能有杂音)
    // 0 = 变速变调(无杂音,但加速声音变尖、减速声音变低沉)
    await player.setOption(FOption.playerCategory, "soundtouch", 0);
    
    // 硬件解码配置
    await player.setOption(FOption.playerCategory, "mediacodec", 1);
    await player.setOption(FOption.playerCategory, "mediacodec-auto-rotate", 1);
    await player.setOption(FOption.playerCategory, "mediacodec-handle-resolution-change", 1);
    
    // 缓冲配置(0=快速首帧,1=稳定但慢)
    await player.setOption(FOption.playerCategory, "packet-buffering", 0);

    setVideoUrl(videoList[videoIndex].url);
  }

  Future<void> setVideoUrl(String url) async {
    try {
      await player.setDataSource(url, autoPlay: true, showCover: true);
    } catch (error) {
      print("播放异常: $error");
    }
  }

  @override
  Widget build(BuildContext context) {
    double videoHeight = MediaQuery.of(context).size.width * 9 / 16;
    
    return Scaffold(
      appBar: AppBar(title: Text('视频播放')),
      body: Column(
        children: [
          FView(
            player: player,
            width: double.infinity,
            height: videoHeight,
            color: Colors.black,
            fsFit: FFit.contain,
            fit: FFit.fill,
            panelBuilder: fPanelBuilder(
              title: '视频标题',
              subTitle: '视频副标题',
              isSnapShot: true,
              isVideos: true,
              videoList: videoList,
              videoIndex: videoIndex,
              speedList: speedList,
              isResolution: true,
              resolutionList: resolutionList,
              onError: () async {
                await player.reset();
                setVideoUrl(videoList[videoIndex].url);
              },
              onVideoEnd: () async {
                var index = videoIndex + 1;
                if (index < videoList.length) {
                  await player.reset();
                  setState(() => videoIndex = index);
                  setVideoUrl(videoList[index].url);
                }
              },
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() async {
    super.dispose();
    try {
      await ScreenBrightness().resetScreenBrightness();
    } catch (e) {
      print(e);
    }
    player.release();
  }
}

API 说明

FPlayer 主要方法

方法 说明
setDataSource(url, {autoPlay, showCover}) 设置视频源
start() 开始播放
pause() 暂停播放
stop() 停止播放
reset() 重置播放器
release() 释放播放器资源
seekTo(msec) 跳转到指定位置
setSpeed(speed) 设置播放速度
setVolume(volume) 设置音量
takeSnapShot() 截图(仅 iOS)
setOption(category, key, value) 设置播放器选项

FView 参数

参数 类型 说明
player FPlayer 播放器实例
width double 播放器宽度
height double 播放器高度
color Color 背景颜色
fit FFit 视频填充模式
fsFit FFit 全屏模式填充
panelBuilder FPanelWidgetBuilder 控制面板构建器

fPanelBuilder 参数

参数 类型 说明
title String 视频标题
subTitle String 视频副标题
isSnapShot bool 是否显示截图按钮
isVideos bool 是否显示视频列表
videoList List<VideoItem> 视频列表
videoIndex int 当前视频索引
speedList Map<String, double> 倍速列表
isResolution bool 是否显示清晰度
resolutionList Map<String, ResolutionItem> 清晰度列表
onError Function 错误回调
onVideoEnd Function 播放完成回调
onVideoPrepared Function 准备完成回调
onVideoTimeChange Function 进度变化回调

平台支持

平台 最低版本 推荐版本
Android API 21 (Android 5.0) API 24+
iOS iOS 12.0 iOS 13.0+

注意事项

iOS

  • ⚠️ iOS 模拟器不支持视频播放,请使用真机测试
  • 需要在 Info.plist 中添加网络权限(如播放 HTTP 视频):
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

Android

  • 需要在 AndroidManifest.xml 中添加网络权限:
<uses-permission android:name="android.permission.INTERNET"/>

花屏问题

如果在 Android 上遇到播放开始时花屏(1-2秒后恢复正常),可以尝试以下方案:

方案1:开启缓冲(推荐,首帧会变慢约1-2秒)

await player.setOption(FOption.playerCategory, "packet-buffering", 1);
await player.setOption(FOption.playerCategory, "min-frames", 25);

方案2:关闭硬件解码(最稳定,但耗电)

await player.setOption(FOption.playerCategory, "mediacodec", 0);

鸣谢

许可证

MIT License

支持作者

如果这个项目对你有帮助,欢迎请作者喝杯咖啡 ☕

微信            支付宝
微信 支付宝

联系方式

如有问题或建议,欢迎联系作者:📧 [email protected]