# td_fplayer

[![pub package](https://img.shields.io/pub/v/td_fplayer.svg)](https://pub.dev/packages/td_fplayer)

基于 [ijkplayer](https://github.com/debugly/ijkplayer) 的 Flutter 视频播放器插件，支持 Android 和 iOS 平台。

> 本项目基于 [fplayer](https://github.com/FlutterPlayer/fplayer) 进行二次开发，替换为标准 ijkplayer 内核。

## 特性

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

## 安装

在 `pubspec.yaml` 中添加依赖：

```yaml
dependencies:
  td_fplayer: ^0.0.1-beta.1
```

## 快速开始

### 基本使用

```dart
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();
  }
}
```

### 完整示例

```dart
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 视频）：

```xml
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
```

### Android

- 需要在 `AndroidManifest.xml` 中添加网络权限：

```xml
<uses-permission android:name="android.permission.INTERNET"/>
```

### 花屏问题

如果在 Android 上遇到播放开始时花屏（1-2秒后恢复正常），可以尝试以下方案：

**方案1：开启缓冲（推荐，首帧会变慢约1-2秒）**

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

**方案2：关闭硬件解码（最稳定，但耗电）**

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

## 鸣谢

- [fplayer](https://github.com/FlutterPlayer/fplayer) - 原始项目
- [fijkplayer](https://github.com/befovy/fijkplayer) - 参考项目
- [ijkplayer](https://github.com/debugly/ijkplayer) - 播放内核
- [FFmpeg](https://github.com/FFmpeg/FFmpeg) - 音视频处理

## 许可证

MIT License

## 支持作者

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

| 微信 | &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | 支付宝 |
|:----:|:----:|:------:|
| <img src="https://raw.githubusercontent.com/fanei/assets/main/images/wx.jpg" width="150" alt="微信" /> | | <img src="https://raw.githubusercontent.com/fanei/assets/main/images/zfb.jpg" width="150" alt="支付宝" /> |

## 联系方式

如有问题或建议，欢迎联系作者：📧 faneizn@gmail.com
