Flutter视频播放扩展插件extended_video_player的使用
Flutter视频播放扩展插件extended_video_player的使用
背景故事
TimHoogstrate 添加了缓存功能,并为 video_player
插件创建了一个 拉取请求。然而,经过很长时间后该 PR 还未被合并。在测试之后,我发现缓存功能仍然有效,因此我决定创建一个分支并将其发布为插件,以便大家能够尝试并提供反馈。
extended_video_player
是一个用于在 Flutter 中播放视频的插件,适用于 iOS、Android 和 Web 平台。
Android | iOS | macOS | Web | |
---|---|---|---|---|
支持 | SDK 16+ | 12.0+ | 10.14+ | Any* |
安装
首先,在你的 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
构造函数。使用该构造函数会尝试创建一个将抛出 UnimplementedError
的 VideoPlayerController.file
。
不同浏览器可能有不同的视频播放能力(支持格式、自动播放等)。请参阅 package:video_player_web 获取更多特定于 Web 的信息。
VideoPlayerOptions.mixWithOthers
选项在 Web 上无法实现,至少目前是这样。如果在 Web 上使用此选项,它将被默默地忽略。
支持的格式
-
在 iOS 和 macOS 上,底层播放器是 AVPlayer。
- 支持的格式取决于 iOS 的版本,AVURLAsset 类有 audiovisualTypes 可以查询支持的 AV 格式。
-
在 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