Flutter视频播放扩展插件extended_video_player的使用

Flutter视频播放扩展插件extended_video_player的使用

背景故事

TimHoogstrate 添加了缓存功能,并为 video_player 插件创建了一个 拉取请求。然而,经过很长时间后该 PR 还未被合并。在测试之后,我发现缓存功能仍然有效,因此我决定创建一个分支并将其发布为插件,以便大家能够尝试并提供反馈。

pub package

extended_video_player 是一个用于在 Flutter 中播放视频的插件,适用于 iOS、Android 和 Web 平台。

Android iOS macOS Web
支持 SDK 16+ 12.0+ 10.14+ Any*

示例应用运行在 iOS 上

安装

首先,在你的 pubspec.yaml 文件中添加 extended_video_player 作为依赖项。

iOS

如果你需要通过 http URL 访问视频(而不是 https),你需要在应用程序的 Info.plist 文件中添加适当的 NSAppTransportSecurity 权限。Info.plist 文件位于 <项目根目录>/ios/Runner/Info.plist。查看 Apple 文档 以确定适合你用例和所支持的 iOS 版本的正确组合。

Android

如果你使用的是网络视频,请确保在 AndroidManifest.xml 文件中添加以下权限:

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

macOS

如果你使用的是网络视频,你需要添加 <code>com.apple.security.network.client</code> 权限。详情请参阅 Flutter 文档

Web

请注意,Web 平台不支持 dart:io,因此避免使用 VideoPlayerController.file 构造函数。使用该构造函数会尝试创建一个将抛出 UnimplementedErrorVideoPlayerController.file

不同浏览器可能有不同的视频播放能力(支持格式、自动播放等)。请参阅 package:video_player_web 获取更多特定于 Web 的信息。

VideoPlayerOptions.mixWithOthers 选项在 Web 上无法实现,至少目前是这样。如果在 Web 上使用此选项,它将被默默地忽略。

支持的格式

  • 在 iOS 和 macOS 上,底层播放器是 AVPlayer

  • 在 Android 上,底层播放器是 ExoPlayer,请参阅 这里 获取支持的格式列表。

  • 在 Web 上,可用格式取决于用户的浏览器(供应商和版本)。请参阅 package:video_player_web 获取更多特定信息。

示例

以下是使用 extended_video_player 的基本示例。

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

void main() => runApp(const VideoApp());

/// 状态fulWidget来获取并显示视频内容。
class VideoApp extends StatefulWidget {
  const VideoApp({super.key});

  [@override](/user/override)
  _VideoAppState createState() => _VideoAppState();
}

class _VideoAppState extends State<VideoApp> {
  late VideoPlayerController _controller;

  [@override](/user/override)
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(
      Uri.parse(
        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
      ),
      videoPlayerOptions: VideoPlayerOptions(
        enableCache: true,
        maxCacheSize: 1024 * 1024 * 1024,
        maxFileSize: 200 * 1024 * 1024,
      ),
    )..initialize().then((_) {
      // 确保在视频初始化后显示第一帧,即使播放按钮还未按下。
      setState(() {});
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Video Demo',
      home: Scaffold(
        body: Center(
          child: _controller.value.isInitialized
              ? AspectRatio(
                  aspectRatio: _controller.value.aspectRatio,
                  child: VideoPlayer(_controller),
                )
              : Container(),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              _controller.value.isPlaying
                  ? _controller.pause()
                  : _controller.play();
            });
          },
          child: Icon(
            _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
          ),
        ),
      ),
    );
  }

  [@override](/user/override)
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

使用方法

以下部分包含超出文档范围的使用信息,以便更详细地概述 API。

目前还不完整,你可以通过 提交拉取请求 来贡献这一部分内容。

播放速度

你可以通过调用 _controller.setPlaybackSpeed 设置播放速度。setPlaybackSpeed 接受一个表示视频播放速率的 double 值。例如,给定值 2.0,你的视频将以 2 倍的正常播放速度播放。

要了解播放速度限制,请参阅 setPlaybackSpeed 方法文档

此外,请参阅示例应用以了解播放速度的实现示例。

示例应用

以下是示例应用中的一些组件和功能。

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

void main() {
  runApp(
    MaterialApp(
      home: _App(),
    ),
  );
}

class _App extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 4,
      child: Scaffold(
        key: const ValueKey<String>('home_page'),
        appBar: AppBar(
          title: const Text('Video player example'),
          actions: [
            IconButton(
              key: const ValueKey<String>('push_tab'),
              icon: const Icon(Icons.navigation),
              onPressed: () {
                Navigator.push<_PlayerVideoAndPopPage>(
                  context,
                  MaterialPageRoute<_PlayerVideoAndPopPage>(
                    builder: (BuildContext context) => _PlayerVideoAndPopPage(),
                  ),
                );
              },
            )
          ],
          bottom: const TabBar(
            isScrollable: true,
            tabs: [
              Tab(
                icon: Icon(Icons.cloud),
                text: 'Remote',
              ),
              Tab(
                icon: Icon(Icons.cloud),
                text: 'Remote cache',
              ),
              Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'),
              Tab(icon: Icon(Icons.list), text: 'List example'),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            _BumbleBeeRemoteVideo(),
            _BumbleBeeRemoteCacheVideo(),
            _ButterFlyAssetVideo(),
            _ButterFlyAssetVideoInList(),
          ],
        ),
      ),
    );
  }
}

class _ButterFlyAssetVideoInList extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return ListView(
      children: [
        const _ExampleCard(title: 'Item a'),
        const _ExampleCard(title: 'Item b'),
        const _ExampleCard(title: 'Item c'),
        const _ExampleCard(title: 'Item d'),
        const _ExampleCard(title: 'Item e'),
        const _ExampleCard(title: 'Item f'),
        const _ExampleCard(title: 'Item g'),
        Card(
            child: Column(children: [
          Column(
            children: [
              const ListTile(
                leading: Icon(Icons.cake),
                title: Text('Video video'),
              ),
              Stack(
                  alignment: FractionalOffset.bottomRight +
                      const FractionalOffset(-0.1, -0.1),
                  children: [
                    _ButterFlyAssetVideo(),
                    Image.asset('assets/flutter-mark-square-64.png'),
                  ]),
            ],
          ),
        ])),
        const _ExampleCard(title: 'Item h'),
        const _ExampleCard(title: 'Item i'),
        const _ExampleCard(title: 'Item j'),
        const _ExampleCard(title: 'Item k'),
        const _ExampleCard(title: 'Item l'),
      ],
    );
  }
}

/// 用于在滚动内容列表中显示视频的填充卡。
class _ExampleCard extends StatelessWidget {
  const _ExampleCard({required this.title});

  final String title;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            leading: const Icon(Icons.airline_seat_flat_angled),
            title: Text(title),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: OverflowBar(
              alignment: MainAxisAlignment.end,
              spacing: 8.0,
              children: [
                TextButton(
                  child: const Text('BUY TICKETS'),
                  onPressed: () {
                    /* ... */
                  },
                ),
                TextButton(
                  child: const Text('SELL TICKETS'),
                  onPressed: () {
                    /* ... */
                  },
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _ButterFlyAssetVideo extends StatefulWidget {
  [@override](/user/override)
  _ButterFlyAssetVideoState createState() => _ButterFlyAssetVideoState();
}

class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> {
  late VideoPlayerController _controller;

  [@override](/user/override)
  void initState() {
    super.initState();
    _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4');

    _controller.addListener(() {
      setState(() {});
    });
    _controller.setLooping(true);
    _controller.initialize().then((_) => setState(() {}));
    _controller.play();
  }

  [@override](/user/override)
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: [
          Container(
            padding: const EdgeInsets.only(top: 20.0),
          ),
          const Text('With assets mp4'),
          Container(
            padding: const EdgeInsets.all(20),
            child: AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: Stack(
                alignment: Alignment.bottomCenter,
                children: [
                  VideoPlayer(_controller),
                  _ControlsOverlay(controller: _controller),
                  VideoProgressIndicator(_controller, allowScrubbing: true),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _BumbleBeeRemoteVideo extends StatefulWidget {
  [@override](/user/override)
  _BumbleBeeRemoteVideoState createState() => _BumbleBeeRemoteVideoState();
}

class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {
  late VideoPlayerController _controller;

  Future<ClosedCaptionFile> _loadCaptions() async {
    final String fileContents = await DefaultAssetBundle.of(context)
        .loadString('assets/bumble_bee_captions.vtt');
    return WebVTTCaptionFile(fileContents); // For vtt files, use WebVTTCaptionFile
  }

  [@override](/user/override)
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(
      Uri.parse(
        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
      ),
      closedCaptionFile: _loadCaptions(),
      videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
    );

    _controller.addListener(() {
      setState(() {});
    });
    _controller.setLooping(true);
    _controller.initialize();
  }

  [@override](/user/override)
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: [
          Container(padding: const EdgeInsets.only(top: 20.0)),
          const Text('With remote mp4'),
          Container(
            padding: const EdgeInsets.all(20),
            child: AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: Stack(
                alignment: Alignment.bottomCenter,
                children: [
                  VideoPlayer(_controller),
                  ClosedCaption(text: _controller.value.caption.text),
                  _ControlsOverlay(controller: _controller),
                  VideoProgressIndicator(_controller, allowScrubbing: true),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _BumbleBeeRemoteCacheVideo extends StatefulWidget {
  [@override](/user/override)
  _BumbleBeeRemoteCacheVideoState createState() => _BumbleBeeRemoteCacheVideoState();
}

class _BumbleBeeRemoteCacheVideoState extends State<_BumbleBeeRemoteCacheVideo> {
  late VideoPlayerController _controller;

  Future<ClosedCaptionFile> _loadCaptions() async {
    final String fileContents = await DefaultAssetBundle.of(context)
        .loadString('assets/bumble_bee_captions.vtt');
    return WebVTTCaptionFile(fileContents); // For vtt files, use WebVTTCaptionFile
  }

  [@override](/user/override)
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(
      Uri.parse(
        'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
      ),
      closedCaptionFile: _loadCaptions(),
      videoPlayerOptions: VideoPlayerOptions(
        mixWithOthers: true,
        enableCache: true,
        maxCacheSize: 1024 * 1024 * 1024,
        maxFileSize: 200 * 1024 * 1024,
      ),
    );

    _controller.addListener(() {
      setState(() {});
    });
    _controller.setLooping(true);
    _controller.initialize();
  }

  [@override](/user/override)
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: [
          Container(padding: const EdgeInsets.only(top: 20.0)),
          const Text('With remote cache mp4'),
          Container(
            padding: const EdgeInsets.all(20),
            child: AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: Stack(
                alignment: Alignment.bottomCenter,
                children: [
                  VideoPlayer(_controller),
                  ClosedCaption(text: _controller.value.caption.text),
                  _ControlsOverlay(controller: _controller),
                  VideoProgressIndicator(_controller, allowScrubbing: true),
                ],
              ),
            ),
          ),
          TextButton(
            style: ButtonStyle(
              foregroundColor: MaterialStateProperty.all<Color>(Colors.blue),
            ),
            onPressed: () {
              _controller.clearCache();
            },
            child: const Text('clear cache'),
          )
        ],
      ),
    );
  }
}

class _ControlsOverlay extends StatelessWidget {
  const _ControlsOverlay({required this.controller});

  static const List<Duration> _exampleCaptionOffsets = [
    Duration(seconds: -10),
    Duration(seconds: -3),
    Duration(seconds: -1, milliseconds: -500),
    Duration(milliseconds: -250),
    Duration.zero,
    Duration(milliseconds: 250),
    Duration(seconds: 1, milliseconds: 500),
    Duration(seconds: 3),
    Duration(seconds: 10),
  ];
  static const List<double> _examplePlaybackRates = [
    0.25,
    0.5,
    1.0,
    1.5,
    2.0,
    3.0,
    5.0,
    10.0,
  ];

  final VideoPlayerController controller;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Stack(
      children: [
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 50),
          reverseDuration: const Duration(milliseconds: 200),
          child: controller.value.isPlaying
              ? const SizedBox.shrink()
              : const ColoredBox(
                  color: Colors.black26,
                  child: Center(
                    child: Icon(
                      Icons.play_arrow,
                      color: Colors.white,
                      size: 100.0,
                      semanticLabel: 'Play',
                    ),
                  ),
                ),
        ),
        GestureDetector(
          onTap: () {
            controller.value.isPlaying ? controller.pause() : controller.play();
          },
        ),
        Align(
          alignment: Alignment.topLeft,
          child: PopupMenuButton<Duration>(
            initialValue: controller.value.captionOffset,
            tooltip: 'Caption Offset',
            onSelected: (Duration delay) {
              controller.setCaptionOffset(delay);
            },
            itemBuilder: (BuildContext context) {
              return [
                for (final Duration offsetDuration in _exampleCaptionOffsets)
                  PopupMenuItem<Duration>(
                    value: offsetDuration,
                    child: Text('${offsetDuration.inMilliseconds}ms'),
                  )
              ];
            },
            child: Padding(
              padding: const EdgeInsets.symmetric(
                // 使用较少的垂直间距,因为文本也是水平较长的,
                // 所以它看起来需要更多的水平间距(匹配视频的宽高比)。
                vertical: 12,
                horizontal: 16,
              ),
              child: Text('${controller.value.captionOffset.inMilliseconds}ms'),
            ),
          ),
        ),
        Align(
          alignment: Alignment.topRight,
          child: PopupMenuButton<double>(
            initialValue: controller.value.playbackSpeed,
            tooltip: 'Playback speed',
            onSelected: (double speed) {
              controller.setPlaybackSpeed(speed);
            },
            itemBuilder: (BuildContext context) {
              return [
                for (final double speed in _examplePlaybackRates)
                  PopupMenuItem<double>(
                    value: speed,
                    child: Text('${speed}x'),
                  )
              ];
            },
            child: Padding(
              padding: const EdgeInsets.symmetric(
                // 使用较少的垂直间距,因为文本也是水平较长的,
                // 所以它看起来需要更多的水平间距(匹配视频的宽高比)。
                vertical: 12,
                horizontal: 16,
              ),
              child: Text('${controller.value.playbackSpeed}x'),
            ),
          ),
        ),
      ],
    );
  }
}

class _PlayerVideoAndPopPage extends StatefulWidget {
  [@override](/user/override)
  _PlayerVideoAndPopPageState createState() => _PlayerVideoAndPopPageState();
}

class _PlayerVideoAndPopPageState extends State<_PlayerVideoAndPopPage> {
  late VideoPlayerController _videoPlayerController;
  bool startedPlaying = false;

  [@override](/user/override)
  void initState() {
    super.initState();

    _videoPlayerController = VideoPlayerController.asset('assets/Butterfly-209.mp4');
    _videoPlayerController.addListener(() {
      if (startedPlaying && !_videoPlayerController.value.isPlaying) {
        Navigator.pop(context);
      }
    });
  }

  [@override](/user/override)
  void dispose() {
    _videoPlayerController.dispose();
    super.dispose();
  }

  Future<bool> started() async {
    await _videoPlayerController.initialize();
    await _videoPlayerController.play();
    startedPlaying = true;
    return true;
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: FutureBuilder<bool>(
          future: started(),
          builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
            if (snapshot.data ?? false) {
              return AspectRatio(
                aspectRatio: _videoPlayerController.value.aspectRatio,
                child: VideoPlayer(_videoPlayerController),
              );
            } else {
              return const Text('waiting for video to load');
            }
          },
        ),
      ),
    );
  }
}

更多关于Flutter视频播放扩展插件extended_video_player的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter视频播放扩展插件extended_video_player的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


extended_video_player 是一个用于 Flutter 的视频播放插件,它基于 video_player 插件,并提供了更多的功能和扩展,例如全屏播放、手势控制、播放速度控制等。使用 extended_video_player 可以让你更方便地实现视频播放功能。

安装

首先,你需要在 pubspec.yaml 文件中添加 extended_video_player 依赖:

dependencies:
  flutter:
    sdk: flutter
  extended_video_player: ^1.0.0  # 请检查最新版本

然后运行 flutter pub get 安装依赖。

基本用法

以下是一个简单的示例,展示了如何使用 extended_video_player 播放视频:

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

class VideoPlayerScreen extends StatefulWidget {
  @override
  _VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}

class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
  ExtendedVideoPlayerController _controller;

  @override
  void initState() {
    super.initState();
    _controller = ExtendedVideoPlayerController.network(
      'https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4',
    )..initialize().then((_) {
        // Ensure the first frame is shown after the video is initialized
        setState(() {});
      });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Extended Video Player'),
      ),
      body: Center(
        child: _controller.value.isInitialized
            ? AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: ExtendedVideoPlayer(
                  controller: _controller,
                ),
              )
            : CircularProgressIndicator(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _controller.value.isPlaying
                ? _controller.pause()
                : _controller.play();
          });
        },
        child: Icon(
          _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
        ),
      ),
    );
  }

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

void main() => runApp(MaterialApp(
  home: VideoPlayerScreen(),
));

主要功能

  1. 全屏播放extended_video_player 支持自动全屏播放,用户可以通过双击视频或点击全屏按钮来切换全屏模式。

  2. 手势控制:支持手势控制,例如滑动调整音量、亮度、播放进度等。

  3. 播放速度控制:可以通过 _controller.setPlaybackSpeed(double speed) 来调整播放速度。

  4. 自定义控件:你可以自定义播放器的控件,例如播放/暂停按钮、进度条、全屏按钮等。

注意事项

  • extended_video_player 是基于 video_player 的扩展,因此你需要确保 video_player 插件的依赖已经正确安装。
  • 在 Android 上播放网络视频时,确保在 AndroidManifest.xml 中添加了网络权限:
<uses-permission android:name="android.permission.INTERNET"/>
  • 在 iOS 上播放网络视频时,确保在 Info.plist 中添加了以下内容:
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
回到顶部