flutter端的AVFoundationVideoPlayer是实现PlatformInterface接口的插件。它就是flutter端跟原生端通信的桥梁。
AVFoundationVideoPlayer
/// iOS平台的视频播放器实现,使用Pigeon生成的VideoPlayerApi
class AVFoundationVideoPlayer extends VideoPlayerPlatform {
/// 创建一个新的基于AVFoundation的视频播放器实现实例
AVFoundationVideoPlayer({
@visibleForTesting AVFoundationVideoPlayerApi? pluginApi,
@visibleForTesting VideoPlayerInstanceApi Function(int playerId)? playerProvider,
}) : _api = pluginApi ?? AVFoundationVideoPlayerApi(),
_playerProvider = playerProvider ?? _productionApiProvider;
final AVFoundationVideoPlayerApi _api;
// 用于创建VideoPlayerInstanceApi实例的方法,可以在测试中被重写
final VideoPlayerInstanceApi Function(int mapId) _playerProvider;
/// 将播放器ID与视图状态关联的映射表
/// 用于确定构建视图时使用哪种视图类型
@visibleForTesting
final Map<int, VideoPlayerViewState> playerViewStates = <int, VideoPlayerViewState>{};
final Map<int, VideoPlayerInstanceApi> _players = <int, VideoPlayerInstanceApi>{};
/// 将此类注册为VideoPlayerPlatform的默认实例
static void registerWith() {
VideoPlayerPlatform.instance = AVFoundationVideoPlayer();
}
@override
Future<void> init() {
return _api.initialize();
}
@override
Future<void> dispose(int playerId) async {
final VideoPlayerInstanceApi? player = _players.remove(playerId);
await player?.dispose();
playerViewStates.remove(playerId);
}
@override
Future<int?> create(DataSource dataSource) async {
return createWithOptions(
VideoCreationOptions(
dataSource: dataSource,
// 在引入createWithOptions之前,纹理视图是唯一支持的视图类型
viewType: VideoViewType.textureView,
),
);
}
@override
Future<int?> createWithOptions(VideoCreationOptions options) async {
final DataSource dataSource = options.dataSource;
final VideoViewType viewType = options.viewType;
String? uri;
switch (dataSource.sourceType) {
case DataSourceType.asset:
final String? asset = dataSource.asset;
if (asset == null) {
throw ArgumentError('"asset"对于资源数据源必须非空');
}
uri = await _api.getAssetUrl(asset, dataSource.package);
if (uri == null) {
// 为了与之前的实现兼容,抛出平台异常
throw PlatformException(code: 'video_player', message: '在包${dataSource.package}中找不到资源$asset');
}
case DataSourceType.network:
case DataSourceType.file:
case DataSourceType.contentUri:
uri = dataSource.uri;
}
if (uri == null) {
throw ArgumentError('无法从$options构造视频资源');
}
final CreationOptions pigeonCreationOptions = CreationOptions(uri: uri, httpHeaders: dataSource.httpHeaders);
final int playerId;
final VideoPlayerViewState state;
switch (viewType) {
case VideoViewType.textureView:
final TexturePlayerIds ids = await _api.createForTextureView(pigeonCreationOptions);
playerId = ids.playerId;
state = VideoPlayerTextureViewState(textureId: ids.textureId);
case VideoViewType.platformView:
playerId = await _api.createForPlatformView(pigeonCreationOptions);
state = const VideoPlayerPlatformViewState();
}
playerViewStates[playerId] = state;
ensureApiInitialized(playerId);
return playerId;
}
/// 返回playerId对应的API实例,如果不存在则创建它
@visibleForTesting
VideoPlayerInstanceApi ensureApiInitialized(int playerId) {
return _players.putIfAbsent(playerId, () {
return _playerProvider(playerId);
});
}
@override
Future<void> setLooping(int playerId, bool looping) {
return _playerWith(id: playerId).setLooping(looping);
}
@override
Future<void> play(int playerId) {
return _playerWith(id: playerId).play();
}
@override
Future<void> pause(int playerId) {
return _playerWith(id: playerId).pause();
}
@override
Future<void> setVolume(int playerId, double volume) {
return _playerWith(id: playerId).setVolume(volume);
}
@override
Future<void> setPlaybackSpeed(int playerId, double speed) {
assert(speed > 0);
return _playerWith(id: playerId).setPlaybackSpeed(speed);
}
@override
Future<void> seekTo(int playerId, Duration position) {
return _playerWith(id: playerId).seekTo(position.inMilliseconds);
}
@override
Future<Duration> getPosition(int playerId) async {
final int position = await _playerWith(id: playerId).getPosition();
return Duration(milliseconds: position);
}
@override
Stream<VideoEvent> videoEventsFor(int playerId) {
debugPrint('---✅dart启动视频播放器流监听videoEventsFor, playerId: $playerId✅---');
return _eventChannelFor(playerId).receiveBroadcastStream().map((dynamic event) {
final Map<dynamic, dynamic> map = event as Map<dynamic, dynamic>;
debugPrint('---✅dart视频播放器流监听videoEventsFor结果, map: $map✅---');
return switch (map['event']) {
'initialized' => VideoEvent(
eventType: VideoEventType.initialized,
duration: Duration(milliseconds: map['duration'] as int),
size: Size((map['width'] as num?)?.toDouble() ?? 0.0, (map['height'] as num?)?.toDouble() ?? 0.0),
),
'completed' => VideoEvent(eventType: VideoEventType.completed),
'bufferingUpdate' => VideoEvent(
buffered: (map['values'] as List<dynamic>).map<DurationRange>(_toDurationRange).toList(),
eventType: VideoEventType.bufferingUpdate,
),
'bufferingStart' => VideoEvent(eventType: VideoEventType.bufferingStart),
'bufferingEnd' => VideoEvent(eventType: VideoEventType.bufferingEnd),
'isPlayingStateUpdate' => VideoEvent(eventType: VideoEventType.isPlayingStateUpdate, isPlaying: map['isPlaying'] as bool),
_ => VideoEvent(eventType: VideoEventType.unknown),
};
});
}
@override
Future<void> setMixWithOthers(bool mixWithOthers) {
return _api.setMixWithOthers(mixWithOthers);
}
@override
Widget buildView(int playerId) {
return buildViewWithOptions(VideoViewOptions(playerId: playerId));
}
@override
Widget buildViewWithOptions(VideoViewOptions options) {
final int playerId = options.playerId;
final VideoPlayerViewState? viewState = playerViewStates[playerId];
return switch (viewState) {
VideoPlayerTextureViewState(:final int textureId) => Texture(textureId: textureId),
VideoPlayerPlatformViewState() => _buildPlatformView(playerId),
null => throw Exception('找不到playerId对应的视图类型: $playerId'),
};
}
Widget _buildPlatformView(int playerId) {
final PlatformVideoViewCreationParams creationParams = PlatformVideoViewCreationParams(playerId: playerId);
return IgnorePointer(
// 使用IgnorePointer以便在平台视图上方使用GestureDetector
child: UiKitView(
viewType: 'plugins.flutter.dev/video_player_ios',
creationParams: creationParams,
creationParamsCodec: AVFoundationVideoPlayerApi.pigeonChannelCodec,
),
);
}
EventChannel _eventChannelFor(int playerId) {
return EventChannel('flutter.io/videoPlayer/videoEvents$playerId');
}
VideoPlayerInstanceApi _playerWith({required int id}) {
final VideoPlayerInstanceApi? player = _players[id];
return player ?? (throw StateError('没有ID为$id的活跃播放器'));
}
DurationRange _toDurationRange(dynamic value) {
final List<dynamic> pair = value as List<dynamic>;
final int startMilliseconds = pair[0] as int;
final int durationMilliseconds = pair[1] as int;
return DurationRange(
Duration(milliseconds: startMilliseconds),
Duration(milliseconds: startMilliseconds + durationMilliseconds),
);
}
}
AVFoundationVideoPlayerApi
/**
* AVFoundation视频播放器API
*
* 这是一个由Pigeon工具自动生成的类,用于Flutter与原生iOS/macOS平台之间的通信。
* 该类提供了视频播放器的核心平台级别操作接口。
*
* 主要功能:
* 1. 播放器初始化和生命周期管理
* 2. 创建不同渲染模式的播放器(平台视图/纹理视图)
* 3. 音频会话配置管理
* 4. Flutter资源文件路径解析
*
* 通信机制:
* - 使用BasicMessageChannel进行跨平台消息传递
* - 支持异步操作和错误处理
* - 自动处理数据序列化和反序列化
*/
class AVFoundationVideoPlayerApi {
/// AVFoundationVideoPlayerApi构造函数
///
/// [binaryMessenger] 二进制消息传递器参数,用于依赖注入。
/// 如果为null,将使用默认的BinaryMessenger路由到宿主平台。
///
/// [messageChannelSuffix] 消息通道后缀,用于区分不同的API实例
AVFoundationVideoPlayerApi({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) : pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix =
messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
/// 二进制消息传递器实例,用于与原生平台通信
final BinaryMessenger? pigeonVar_binaryMessenger;
/// Pigeon消息编解码器,处理数据类型转换
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
/// 消息通道后缀,用于标识特定的API实例
final String pigeonVar_messageChannelSuffix;
/**
* 初始化AVFoundation视频播放器插件
*
* 这是使用播放器之前必须调用的方法,用于:
* - 设置AVFoundation框架的基础配置
* - 初始化音频会话
* - 准备播放器工厂和相关组件
*
* @return Future<void> 异步操作完成标识
* @throws PlatformException 当初始化失败时抛出平台异常
*/
Future<void> initialize() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
/**
* 创建使用平台视图渲染的视频播放器
*
* 平台视图模式特点:
* - 使用原生UIView/NSView进行渲染
* - 更好的兼容性,支持所有视频格式和特性
* - 可能存在性能开销和层级问题
* - 适用于需要完整原生功能的场景
*
* @param params 创建选项,包含视频URI和HTTP头信息
* @return Future<int> 返回创建的播放器ID
* @throws PlatformException 当创建失败时抛出异常
*/
Future<int> createForPlatformView(CreationOptions params) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[params],
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as int?)!;
}
}
/**
* 创建使用纹理渲染的视频播放器
*
* 纹理模式特点:
* - 使用Flutter的Texture Widget进行渲染
* - 更好的性能表现和Flutter集成
* - 支持Flutter的变换、动画等效果
* - 可能在某些特殊视频格式上有限制
* - 推荐用于大多数常规播放场景
*
* @param creationOptions 创建选项,包含视频URI和HTTP头信息
* @return Future<TexturePlayerIds> 返回包含播放器ID和纹理ID的对象
* @throws PlatformException 当创建失败时抛出异常
*/
Future<TexturePlayerIds> createForTextureView(
CreationOptions creationOptions,
) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[creationOptions],
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as TexturePlayerIds?)!;
}
}
/**
* 设置音频混合模式
*
* 控制视频播放器的音频是否与其他应用的音频混合播放:
* - true: 允许与其他音频混合(如音乐应用、通话等)
* - false: 独占音频输出,会暂停其他应用的音频
*
* 使用场景:
* - 背景音乐播放:设置为true,允许与系统音频混合
* - 视频通话、游戏:设置为false,获得独占音频控制
*
* @param mixWithOthers 是否允许与其他音频混合
* @return Future<void> 异步操作完成标识
* @throws PlatformException 当设置失败时抛出异常
*/
Future<void> setMixWithOthers(bool mixWithOthers) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[mixWithOthers],
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
/**
* 获取Flutter资源文件的本地URL路径
*
* 将Flutter项目中的资源文件路径转换为原生平台可访问的本地文件URL。
* 这对于播放打包在应用内的视频文件非常重要。
*
* 资源路径解析过程:
* 1. 根据资源名称在Flutter资源清单中查找
* 2. 考虑包名(如果指定)进行路径解析
* 3. 返回原生平台可直接访问的file:// URL
*
* @param asset 资源文件名称(如 "videos/sample.mp4")
* @param package 可选的包名,用于访问其他包中的资源
* @return Future<String?> 返回本地文件URL,如果资源不存在则返回null
* @throws PlatformException 当路径解析失败时抛出异常
*/
Future<String?> getAssetUrl(String asset, String? package) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[asset, package],
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return (pigeonVar_replyList[0] as String?);
}
}
}
VideoPlayerInstanceApi
/**
* 视频播放器实例API
*
* 这是一个由Pigeon工具自动生成的类,用于控制单个视频播放器实例的播放行为。
* 该类提供了视频播放器的核心控制功能,每个播放器实例都有一个对应的API实例。
*
* 主要功能:
* 1. 播放控制(播放、暂停、跳转)
* 2. 播放参数设置(音量、播放速度、循环模式)
* 3. 播放状态查询(当前播放位置)
* 4. 资源管理(释放播放器资源)
*
* 使用方式:
* - 通过AVFoundationVideoPlayerApi创建播放器后获得实例
* - 使用messageChannelSuffix区分不同的播放器实例
* - 所有操作都是异步的,返回Future对象
*/
class VideoPlayerInstanceApi {
/// VideoPlayerInstanceApi构造函数
///
/// [binaryMessenger] 二进制消息传递器参数,用于依赖注入。
/// 如果为null,将使用默认的BinaryMessenger路由到宿主平台。
///
/// [messageChannelSuffix] 消息通道后缀,用于标识特定的播放器实例
/// 通常使用播放器ID作为后缀,确保每个播放器实例有独立的通信通道
VideoPlayerInstanceApi({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) : pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix =
messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
/// 二进制消息传递器实例,用于与原生平台通信
final BinaryMessenger? pigeonVar_binaryMessenger;
/// Pigeon消息编解码器,处理数据类型转换
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
/// 消息通道后缀,用于标识特定的播放器实例
final String pigeonVar_messageChannelSuffix;
/**
* 设置视频循环播放模式
*
* 控制视频播放完成后的行为:
* - true: 视频播放完成后自动重新开始播放,形成无限循环
* - false: 视频播放完成后停止,不会自动重播
*
* 使用场景:
* - 背景视频、装饰性视频:通常设置为true,持续循环播放
* - 教学视频、宣传片:通常设置为false,播放完成后停止
* - 短视频应用:可根据用户设置或内容类型动态调整
*
* @param looping 是否启用循环播放
* @return Future<void> 异步操作完成标识
* @throws PlatformException 当设置失败时抛出异常
*/
Future<void> setLooping(bool looping) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[looping],
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
/**
* 设置视频播放音量
*
* 调整当前播放器实例的音频输出音量大小。
* 这个设置只影响当前播放器,不会改变系统全局音量。
*
* 音量范围和效果:
* - 0.0: 完全静音,无音频输出
* - 1.0: 最大音量,使用视频原始音频级别
* - 0.0-1.0之间: 按比例调整音量大小
* - 超出范围的值会被自动限制在有效范围内
*
* 使用场景:
* - 多媒体应用中的音量控制滑块
* - 背景视频的音量调节
* - 音频淡入淡出效果实现
*
* @param volume 音量值,范围0.0-1.0
* @return Future<void> 异步操作完成标识
* @throws PlatformException 当设置失败时抛出异常
*/
Future<void> setVolume(double volume) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[volume],
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
/**
* 设置视频播放速度
*
* 调整视频播放的速率,可以实现快进、慢放、倒放等效果。
* 播放速度的改变会同时影响视频和音频的播放速率。
*
* 速度值说明:
* - 1.0: 正常播放速度(默认值)
* - 0.5: 半速播放(慢放)
* - 2.0: 双倍速播放(快进)
* - 0.0: 暂停播放
* - 负值: 倒放(部分平台支持)
*
* 常用速度设置:
* - 0.25x, 0.5x, 0.75x: 慢速播放,适用于学习、分析场景
* - 1.25x, 1.5x, 2.0x: 快速播放,适用于快速浏览
* - 变速播放在在线教育、体育分析等场景中非常有用
*
* @param speed 播放速度倍率
* @return Future<void> 异步操作完成标识
* @throws PlatformException 当设置失败时抛出异常
*/
Future<void> setPlaybackSpeed(double speed) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[speed],
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
/**
* 开始播放视频
*
* 启动或恢复视频播放。如果视频处于暂停状态,将从当前位置继续播放;
* 如果视频尚未开始,将从头开始播放。
*
* 播放行为说明:
* - 首次调用:从视频开头开始播放
* - 暂停后调用:从暂停位置继续播放
* - 播放完成后调用:根据循环设置决定是否重新播放
* - 可以在视频加载过程中调用,播放器会在准备就绪后自动开始
*
* 注意事项:
* - 某些平台可能需要用户交互才能开始播放(自动播放策略)
* - 网络视频需要缓冲时间,可能不会立即开始播放
* - 播放状态变化会通过事件流通知Flutter层
*
* @return Future<void> 异步操作完成标识
* @throws PlatformException 当播放启动失败时抛出异常
*/
Future<void> play() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
/**
* 获取当前播放位置
*
* 返回视频当前的播放进度,以毫秒为单位。
* 这个值表示从视频开始到当前播放点的时间长度。
*
* 返回值说明:
* - 返回值为毫秒数(int类型)
* - 视频开始位置为0
* - 播放过程中该值会持续增加
* - 跳转后会立即反映新的位置
*
* 使用场景:
* - 实现播放进度条显示
* - 保存播放断点,支持续播功能
* - 实现播放时间显示(需要转换为时分秒格式)
* - 同步多个播放器的播放进度
*
* 注意事项:
* - 频繁调用可能影响性能,建议配合定时器适度查询
* - 网络视频的位置可能不够精确,存在缓冲影响
*
* @return Future<int> 当前播放位置(毫秒)
* @throws PlatformException 当获取位置失败时抛出异常
*/
Future<int> getPosition() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as int?)!;
}
}
/**
* 跳转到指定播放位置
*
* 将视频播放位置跳转到指定的时间点。这是一个异步操作,
* 跳转完成后播放器会从新位置继续播放或保持暂停状态。
*
* 参数说明:
* - position: 目标位置,以毫秒为单位
* - 可以跳转到视频的任意有效位置
* - 超出视频长度的值会被限制到视频末尾
* - 负值会被限制到视频开头(0位置)
*
* 跳转行为:
* - 播放中跳转:会在新位置继续播放
* - 暂停中跳转:会跳转到新位置但保持暂停状态
* - 网络视频跳转:可能需要重新缓冲
*
* 使用场景:
* - 进度条拖拽跳转
* - 章节跳转功能
* - 续播功能(跳转到上次播放位置)
* - 精确定位到特定时间点
*
* @param position 目标播放位置(毫秒)
* @return Future<void> 异步操作完成标识
* @throws PlatformException 当跳转失败时抛出异常
*/
Future<void> seekTo(int position) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[position],
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
/**
* 暂停视频播放
*
* 暂停当前正在播放的视频,保持在当前播放位置。
* 暂停后可以通过调用play()方法从当前位置恢复播放。
*
* 暂停行为说明:
* - 立即停止视频和音频播放
* - 保持当前播放位置不变
* - 可以多次调用,不会产生副作用
* - 如果视频已经暂停,调用此方法无效果
*
* 使用场景:
* - 用户主动暂停播放
* - 应用进入后台时自动暂停
* - 接收到电话等中断事件时暂停
* - 实现播放/暂停切换功能
*
* 与其他操作的关系:
* - 暂停后仍可以调用seekTo()跳转位置
* - 暂停后可以调用setVolume()等设置方法
* - 播放状态变化会通过事件流通知Flutter层
*
* @return Future<void> 异步操作完成标识
* @throws PlatformException 当暂停操作失败时抛出异常
*/
Future<void> pause() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
/**
* 释放播放器资源
*
* 彻底释放当前播放器实例占用的所有系统资源,包括内存、解码器、
* 网络连接等。调用此方法后,播放器实例将不再可用。
*
* 释放操作包括:
* - 停止视频播放和音频输出
* - 释放视频解码器和音频解码器
* - 关闭网络连接和文件句柄
* - 清理缓存和临时数据
* - 注销事件监听器和KVO观察者
*
* 调用时机:
* - 播放器不再需要时(如页面销毁)
* - 应用内存不足时主动释放
* - 切换到新的视频源之前
* - 应用进入后台长时间不使用时
*
* 重要注意事项:
* - 调用dispose()后不能再使用此播放器实例
* - 必须在适当的时机调用,避免内存泄漏
* - Flutter Widget销毁时应确保调用此方法
* - 这是一个不可逆操作,无法恢复播放器状态
*
* @return Future<void> 异步操作完成标识
* @throws PlatformException 当资源释放失败时抛出异常
*/
Future<void> dispose() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
}
MiniController
VideoPlayerPlatform? _cachedPlatform;
VideoPlayerPlatform get _platform {
if (_cachedPlatform == null) {
_cachedPlatform = VideoPlayerPlatform.instance;
// 立即执行初始化
_cachedPlatform!.init();
}
return _cachedPlatform!;
}
/**
* 视频播放器状态值类
*
* 这是一个不可变的数据类,用于存储和传递视频播放器的所有状态信息。
* 包含了播放器的持续时间、当前位置、缓冲状态、错误状态和播放设置等。
*
* 主要功能:
* 1. 存储视频播放器的完整状态信息
* 2. 提供状态查询方法(如错误检查、宽高比计算)
* 3. 支持状态复制和更新(copyWith方法)
* 4. 实现相等性比较和哈希计算
*
* 使用场景:
* - MiniController 使用此类来管理播放器状态
* - UI 组件通过此类获取播放器状态信息
* - 状态变化时创建新实例来通知监听者
*/
/// The duration, current position, buffering state, error state and settings
/// of a [MiniController].
@immutable
class VideoPlayerValue {
/**
* 构造一个视频播放器状态值
*
* 创建 VideoPlayerValue 实例,只有 duration 是必需的参数。
* 其他参数如果未设置,将使用默认值进行初始化。
*
* @param duration 视频总时长(必需参数)
* @param size 视频尺寸,默认为 Size.zero
* @param position 当前播放位置,默认为 Duration.zero
* @param buffered 已缓冲的时间范围列表,默认为空列表
* @param isInitialized 是否已初始化,默认为 false
* @param isPlaying 是否正在播放,默认为 false
* @param isBuffering 是否正在缓冲,默认为 false
* @param playbackSpeed 播放速度,默认为 1.0
* @param errorDescription 错误描述,默认为 null
*/
/// Constructs a video with the given values. Only [duration] is required. The
/// rest will initialize with default values when unset.
const VideoPlayerValue({
required this.duration,
this.size = Size.zero,
this.position = Duration.zero,
this.buffered = const <DurationRange>[],
this.isInitialized = false,
this.isPlaying = false,
this.isBuffering = false,
this.playbackSpeed = 1.0,
this.errorDescription,
});
/**
* 创建一个未初始化的视频播放器状态值
*
* 返回一个表示视频尚未加载的实例。
* duration 为 Duration.zero,isInitialized 为 false。
*
* 使用场景:
* - 播放器创建时的初始状态
* - 重置播放器状态时使用
*/
/// Returns an instance for a video that hasn't been loaded.
const VideoPlayerValue.uninitialized() : this(duration: Duration.zero, isInitialized: false);
/**
* 创建一个包含错误信息的视频播放器状态值
*
* 返回一个包含指定错误描述的实例。
* duration 为 Duration.zero,isInitialized 为 false。
*
* @param errorDescription 错误描述信息
*
* 使用场景:
* - 视频加载失败时创建错误状态
* - 播放过程中发生错误时更新状态
*/
/// Returns an instance with the given [errorDescription].
const VideoPlayerValue.erroneous(String errorDescription)
: this(duration: Duration.zero, isInitialized: false, errorDescription: errorDescription);
/**
* 视频总时长
*
* 表示视频文件的完整播放时长。
* 如果视频尚未初始化,该值为 Duration.zero。
*
* 使用场景:
* - 显示视频总时长信息
* - 计算播放进度百分比
* - 验证跳转位置是否有效
*/
/// The total duration of the video.
///
/// The duration is [Duration.zero] if the video hasn't been initialized.
final Duration duration;
/**
* 当前播放位置
*
* 表示视频当前的播放进度位置。
* 从视频开始(Duration.zero)到当前播放点的时间长度。
*
* 使用场景:
* - 显示当前播放时间
* - 更新进度条位置
* - 实现续播功能
*/
/// The current playback position.
final Duration position;
/**
* 当前已缓冲的时间范围
*
* 包含所有已缓冲的视频片段的时间范围列表。
* 每个 DurationRange 表示一个连续的已缓冲区间。
*
* 使用场景:
* - 显示缓冲进度条
* - 判断是否可以流畅播放到某个位置
* - 优化网络视频的播放体验
*/
/// The currently buffered ranges.
final List<DurationRange> buffered;
/**
* 视频是否正在播放
*
* true 表示视频正在播放,false 表示视频已暂停。
* 这个状态反映了用户的播放意图,而不是实际的播放状态。
*
* 使用场景:
* - 控制播放/暂停按钮的显示状态
* - 决定是否显示播放控制覆盖层
* - 管理播放器的生命周期
*/
/// True if the video is playing. False if it's paused.
final bool isPlaying;
/**
* 视频是否正在缓冲
*
* true 表示播放器正在缓冲数据,通常会显示加载指示器。
* false 表示缓冲完成,可以正常播放。
*
* 使用场景:
* - 显示/隐藏缓冲加载指示器
* - 提供用户反馈,说明播放暂停的原因
* - 统计缓冲时间和频率
*/
/// True if the video is currently buffering.
final bool isBuffering;
/**
* 当前播放速度
*
* 表示视频播放的速率倍数。
* 1.0 为正常速度,0.5 为半速,2.0 为双倍速。
*
* 使用场景:
* - 实现变速播放功能
* - 显示当前播放速度
* - 快进/慢放控制
*/
/// The current speed of the playback.
final double playbackSpeed;
/**
* 错误描述信息
*
* 如果播放器发生错误,此字段包含错误的详细描述。
* 如果 hasError 为 false,此字段为 null。
*
* 使用场景:
* - 向用户显示错误信息
* - 日志记录和错误分析
* - 错误恢复处理
*/
/// A description of the error if present.
///
/// If [hasError] is false this is `null`.
final String? errorDescription;
/**
* 当前加载视频的尺寸
*
* 表示视频的原始宽度和高度。
* 只有在视频初始化完成后才会有有效值。
*
* 使用场景:
* - 计算视频显示的宽高比
* - 调整播放器容器尺寸
* - 实现全屏播放功能
*/
/// The [size] of the currently loaded video.
final Size size;
/**
* 视频是否已初始化完成
*
* true 表示视频已加载完成,可以开始播放。
* false 表示视频尚未加载或加载失败。
*
* 使用场景:
* - 判断是否可以开始播放操作
* - 控制 UI 组件的显示状态
* - 决定是否显示视频信息
*/
/// Indicates whether or not the video has been loaded and is ready to play.
final bool isInitialized;
/**
* 是否存在错误
*
* 返回播放器是否处于错误状态。
* 如果此值为 true,errorDescription 应该包含错误信息。
*
* @return true 如果存在错误,false 如果正常
*
* 使用场景:
* - 快速检查播放器状态
* - 条件渲染错误提示
* - 错误状态处理逻辑
*/
/// Indicates whether or not the video is in an error state. If this is true
/// [errorDescription] should have information about the problem.
bool get hasError => errorDescription != null;
/**
* 获取视频宽高比
*
* 返回视频的宽高比(宽度/高度)。
*
* 返回值说明:
* - 如果 isInitialized 为 false,返回 1.0
* - 如果 size.width 或 size.height 等于 0.0,返回 1.0
* - 如果计算出的宽高比小于等于 0.0,返回 1.0
* - 否则返回实际的宽高比值
*
* 使用场景:
* - 调整视频显示容器的尺寸
* - 保持视频原始比例显示
* - 实现响应式视频播放器布局
*/
/// Returns [size.width] / [size.height].
///
/// Will return `1.0` if:
/// * [isInitialized] is `false`
/// * [size.width], or [size.height] is equal to `0.0`
/// * aspect ratio would be less than or equal to `0.0`
double get aspectRatio {
if (!isInitialized || size.width == 0 || size.height == 0) {
return 1.0;
}
final double aspectRatio = size.width / size.height;
if (aspectRatio <= 0) {
return 1.0;
}
return aspectRatio;
}
/**
* 复制并更新状态值
*
* 返回一个新的实例,该实例具有与当前实例相同的值,
* 但会使用传入的参数覆盖对应的字段。
*
* 这是不可变对象的标准更新模式,确保状态变化的可追踪性。
*
* @param duration 新的视频时长
* @param size 新的视频尺寸
* @param position 新的播放位置
* @param buffered 新的缓冲范围列表
* @param isInitialized 新的初始化状态
* @param isPlaying 新的播放状态
* @param isBuffering 新的缓冲状态
* @param playbackSpeed 新的播放速度
* @param errorDescription 新的错误描述
* @return 更新后的新实例
*
* 使用场景:
* - 播放器状态更新时创建新状态
* - 响应播放事件时更新特定字段
* - 保持状态不可变性的同时进行更新
*/
/// Returns a new instance that has the same values as this current instance,
/// except for any overrides passed in as arguments to [copyWidth].
VideoPlayerValue copyWith({
Duration? duration,
Size? size,
Duration? position,
List<DurationRange>? buffered,
bool? isInitialized,
bool? isPlaying,
bool? isBuffering,
double? playbackSpeed,
String? errorDescription,
}) {
return VideoPlayerValue(
duration: duration ?? this.duration,
size: size ?? this.size,
position: position ?? this.position,
buffered: buffered ?? this.buffered,
isInitialized: isInitialized ?? this.isInitialized,
isPlaying: isPlaying ?? this.isPlaying,
isBuffering: isBuffering ?? this.isBuffering,
playbackSpeed: playbackSpeed ?? this.playbackSpeed,
errorDescription: errorDescription ?? this.errorDescription,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is VideoPlayerValue &&
runtimeType == other.runtimeType &&
duration == other.duration &&
position == other.position &&
listEquals(buffered, other.buffered) &&
isPlaying == other.isPlaying &&
isBuffering == other.isBuffering &&
playbackSpeed == other.playbackSpeed &&
errorDescription == other.errorDescription &&
size == other.size &&
isInitialized == other.isInitialized;
@override
int get hashCode =>
Object.hash(duration, position, buffered, isPlaying, isBuffering, playbackSpeed, errorDescription, size, isInitialized);
}
/**
* 迷你视频播放器控制器
*
* 这是一个非常精简版本的 VideoPlayerController,专门用于运行示例程序,
* 而不依赖于完整的 video_player 包。它继承自 ValueNotifier,
* 可以通知监听者播放器状态的变化。
*
* 主要功能:
* 1. 支持多种视频源(资源文件、网络、本地文件)
* 2. 提供基本的播放控制(播放、暂停、跳转、变速)
* 3. 管理播放器生命周期(初始化、销毁)
* 4. 处理播放事件和错误状态
* 5. 定时更新播放位置信息
*
* 设计特点:
* - 轻量级实现,适合示例和测试使用
* - 基于平台接口,与原生播放器通信
* - 状态管理基于 ValueNotifier 模式
* - 支持异步操作和错误处理
*/
/// A very minimal version of `VideoPlayerController` for running the example
/// without relying on `video_player`.
class MiniController extends ValueNotifier<VideoPlayerValue> {
/**
* 构造一个播放资源文件视频的 MiniController
*
* 创建一个用于播放应用资源包中视频文件的控制器。
*
* @param dataSource 资源文件名称,不能为 null
* @param package 资源包名称,如果资源来自包则必须非空,否则为 null
* @param viewType 视频视图类型,默认为 textureView
*
* 使用场景:
* - 播放应用内置的演示视频
* - 播放教程或引导视频
* - 播放应用主题相关的装饰视频
*/
/// Constructs a [MiniController] playing a video from an asset.
///
/// The name of the asset is given by the [dataSource] argument and must not be
/// null. The [package] argument must be non-null when the asset comes from a
/// package and null otherwise.
MiniController.asset(this.dataSource, {this.package, this.viewType = VideoViewType.textureView})
: dataSourceType = DataSourceType.asset,
super(const VideoPlayerValue(duration: Duration.zero));
/**
* 构造一个播放网络视频的 MiniController
*
* 创建一个用于播放网络视频的控制器。
*
* @param dataSource 网络视频的 URL 地址
* @param viewType 视频视图类型,默认为 textureView
*
* 使用场景:
* - 播放在线视频内容
* - 流媒体视频播放
* - 远程视频资源播放
*
* 注意事项:
* - 需要网络权限
* - 可能存在缓冲延迟
* - 需要处理网络错误
*/
/// Constructs a [MiniController] playing a video from obtained from
/// the network.
MiniController.network(this.dataSource, {this.viewType = VideoViewType.textureView})
: dataSourceType = DataSourceType.network,
package = null,
super(const VideoPlayerValue(duration: Duration.zero));
/**
* 构造一个播放本地文件视频的 MiniController
*
* 创建一个用于播放本地文件系统中视频文件的控制器。
*
* @param file 本地视频文件对象
* @param viewType 视频视图类型,默认为 textureView
*
* 使用场景:
* - 播放用户录制的视频
* - 播放下载到本地的视频文件
* - 播放相册中的视频
*
* 注意事项:
* - 需要文件读取权限
* - 文件路径必须有效
* - 支持的视频格式取决于平台
*/
/// Constructs a [MiniController] playing a video from obtained from a file.
MiniController.file(File file, {this.viewType = VideoViewType.textureView})
: dataSource = Uri.file(file.absolute.path).toString(),
dataSourceType = DataSourceType.file,
package = null,
super(const VideoPlayerValue(duration: Duration.zero));
/**
* 视频文件的 URI
*
* 根据 DataSourceType 的不同,这个 URI 会有不同的格式:
* - asset: 资源文件路径
* - network: HTTP/HTTPS URL
* - file: file:// 协议的本地文件路径
* - contentUri: content:// 协议的内容 URI
*/
/// The URI to the video file. This will be in different formats depending on
/// the [DataSourceType] of the original video.
final String dataSource;
/**
* 数据源类型
*
* 描述此 MiniController 构造时使用的数据源类型。
* 决定了如何解释和处理 dataSource 字段。
*
* 可能的值:
* - DataSourceType.asset: 应用资源文件
* - DataSourceType.network: 网络视频
* - DataSourceType.file: 本地文件
* - DataSourceType.contentUri: 内容 URI
*/
/// Describes the type of data source this [MiniController]
/// is constructed with.
final DataSourceType dataSourceType;
/**
* 资源包名称
*
* 仅在播放资源文件视频时设置。
* 指定资源文件所在的包名称。
*
* - 如果资源来自应用本身,则为 null
* - 如果资源来自第三方包,则为包名称
*/
/// Only set for [asset] videos. The package that the asset was loaded from.
final String? package;
/**
* 视频视图类型
*
* 指定用于显示视频的视图类型。
*
* 可能的值:
* - VideoViewType.textureView: 使用纹理视图(默认)
* - VideoViewType.platformView: 使用平台视图
*
* 不同类型的特点:
* - textureView: 性能更好,支持变换和动画
* - platformView: 兼容性更好,但性能稍差
*/
/// The type of view used to display the video.
final VideoViewType viewType;
/// 定时器,用于定期更新播放位置
Timer? _timer;
/// 创建完成器,用于等待播放器创建完成
Completer<void>? _creatingCompleter;
/// 事件订阅,用于监听播放器事件
StreamSubscription<dynamic>? _eventSubscription;
/**
* 未初始化播放器的 ID 常量
*
* 表示播放器尚未初始化时的 ID 值。
* 用于测试和状态检查。
*/
/// The id of a player that hasn't been initialized.
@visibleForTesting
static const int kUninitializedPlayerId = -1;
/// 播放器 ID,由平台分配的唯一标识符
int _playerId = kUninitializedPlayerId;
/**
* 获取播放器 ID
*
* 这个方法仅用于测试目的,不应该被依赖此插件的任何人使用。
*
* @return 当前播放器的 ID
*/
/// This is just exposed for testing. It shouldn't be used by anyone depending
/// on the plugin.
@visibleForTesting
int get playerId => _playerId;
/**
* 初始化播放器
*
* 尝试打开给定的数据源并加载视频的元数据信息。
* 这是一个异步操作,会创建原生播放器实例并建立事件监听。
*
* 初始化过程:
* 1. 根据数据源类型创建 DataSource 描述对象
* 2. 调用平台接口创建播放器实例
* 3. 建立事件监听,处理播放器状态变化
* 4. 等待初始化完成事件
* 5. 设置默认播放参数(音量、循环等)
*
* @return Future<void> 初始化完成的异步标识
* @throws PlatformException 当初始化失败时抛出异常
*
* 使用场景:
* - 在播放视频之前必须调用
* - 通常在 Widget 的 initState 中调用
* - 可以在 FutureBuilder 中使用
*/
/// Attempts to open the given [dataSource] and load metadata about the video.
Future<void> initialize() async {
_creatingCompleter = Completer<void>();
late DataSource dataSourceDescription;
switch (dataSourceType) {
case DataSourceType.asset:
dataSourceDescription = DataSource(sourceType: DataSourceType.asset, asset: dataSource, package: package);
case DataSourceType.network:
dataSourceDescription = DataSource(sourceType: DataSourceType.network, uri: dataSource);
case DataSourceType.file:
dataSourceDescription = DataSource(sourceType: DataSourceType.file, uri: dataSource);
case DataSourceType.contentUri:
dataSourceDescription = DataSource(sourceType: DataSourceType.contentUri, uri: dataSource);
}
final VideoCreationOptions creationOptions = VideoCreationOptions(dataSource: dataSourceDescription, viewType: viewType);
// 跟原生通信, 调用createWithOptions方法创建视频播放器
_playerId = (await _platform.createWithOptions(creationOptions)) ?? kUninitializedPlayerId;
_creatingCompleter!.complete(null);
final Completer<void> initializingCompleter = Completer<void>();
void eventListener(VideoEvent event) {
switch (event.eventType) {
case VideoEventType.initialized:
value = value.copyWith(duration: event.duration, size: event.size, isInitialized: event.duration != null);
initializingCompleter.complete(null);
_platform.setVolume(_playerId, 1.0);
_platform.setLooping(_playerId, true);
// 初始化成功后,根据用户手动设置的播放状态决定是否开始播放
_applyPlayPause();
case VideoEventType.completed:
// 播放完成后,暂停
pause().then((void pauseResult) => seekTo(value.duration));
case VideoEventType.bufferingUpdate:
value = value.copyWith(buffered: event.buffered);
case VideoEventType.bufferingStart:
value = value.copyWith(isBuffering: true);
case VideoEventType.bufferingEnd:
value = value.copyWith(isBuffering: false);
case VideoEventType.isPlayingStateUpdate:
value = value.copyWith(isPlaying: event.isPlaying);
case VideoEventType.unknown:
break;
}
}
void errorListener(Object obj) {
final PlatformException e = obj as PlatformException;
value = VideoPlayerValue.erroneous(e.message!);
_timer?.cancel();
if (!initializingCompleter.isCompleted) {
initializingCompleter.completeError(obj);
}
}
// 启动视频播放器流监听
_eventSubscription = _platform.videoEventsFor(_playerId).listen(eventListener, onError: errorListener);
return initializingCompleter.future;
}
/**
* 释放播放器资源
*
* 清理播放器占用的所有资源,包括定时器、事件订阅和原生播放器实例。
* 这是一个异步操作,确保所有资源都被正确释放。
*
* 释放过程:
* 1. 等待播放器创建完成(如果正在创建中)
* 2. 取消定时器
* 3. 取消事件订阅
* 4. 释放原生播放器实例
* 5. 调用父类的 dispose 方法
*
* 注意事项:
* - 必须在不再使用播放器时调用
* - 通常在 Widget 的 dispose 方法中调用
* - 调用后播放器实例不能再使用
*/
@override
Future<void> dispose() async {
if (_creatingCompleter != null) {
await _creatingCompleter!.future;
_timer?.cancel();
await _eventSubscription?.cancel();
await _platform.dispose(_playerId);
}
super.dispose();
}
/**
* 开始播放视频
*
* 启动视频播放,如果视频处于暂停状态,将从当前位置继续播放。
* 这个方法会更新播放状态并应用播放/暂停逻辑。
*
* 播放行为:
* - 更新内部播放状态为 true
* - 调用原生播放器开始播放
* - 启动定时器定期更新播放位置
* - 应用当前的播放速度设置
*
* @return Future<void> 异步操作完成标识
*
* 使用场景:
* - 用户点击播放按钮
* - 自动播放功能
* - 从暂停状态恢复播放
*/
/// Starts playing the video.
Future<void> play() async {
value = value.copyWith(isPlaying: true);
await _applyPlayPause();
}
/**
* 暂停视频播放
*
* 暂停当前正在播放的视频,保持在当前播放位置。
* 这个方法会更新播放状态并应用播放/暂停逻辑。
*
* 暂停行为:
* - 更新内部播放状态为 false
* - 调用原生播放器暂停播放
* - 停止定时器,不再更新播放位置
*
* @return Future<void> 异步操作完成标识
*
* 使用场景:
* - 用户点击暂停按钮
* - 应用进入后台时自动暂停
* - 接收到中断事件时暂停
*/
/// Pauses the video.
Future<void> pause() async {
value = value.copyWith(isPlaying: false);
await _applyPlayPause();
}
/**
* 应用播放/暂停状态
*
* 根据当前的播放状态,执行相应的播放或暂停操作。
* 这是一个内部方法,用于统一处理播放状态变化。
*
* 播放时的操作:
* - 调用原生播放器开始播放
* - 启动定时器,每500毫秒更新一次播放位置
* - 应用当前的播放速度设置
*
* 暂停时的操作:
* - 取消定时器
* - 调用原生播放器暂停播放
*/
Future<void> _applyPlayPause() async {
_timer?.cancel();
if (value.isPlaying) {
await _platform.play(_playerId);
_timer = Timer.periodic(const Duration(milliseconds: 500), (Timer timer) async {
final Duration? newPosition = await position;
if (newPosition == null) {
return;
}
_updatePosition(newPosition);
});
await _applyPlaybackSpeed();
} else {
await _platform.pause(_playerId);
}
}
/**
* 应用播放速度设置
*
* 如果视频正在播放,将当前的播放速度设置应用到原生播放器。
* 这是一个内部方法,确保播放速度设置的一致性。
*
* 注意事项:
* - 只在播放状态下应用速度设置
* - 暂停状态下不会应用速度设置
*/
Future<void> _applyPlaybackSpeed() async {
if (value.isPlaying) {
await _platform.setPlaybackSpeed(_playerId, value.playbackSpeed);
}
}
/**
* 获取当前播放位置
*
* 从原生播放器获取当前的播放位置。
* 这是一个异步操作,返回值可能为 null。
*
* @return Future<Duration?> 当前播放位置,可能为 null
*
* 使用场景:
* - 定时器中更新播放位置
* - 用户查询当前播放进度
* - 实现播放位置同步
*/
/// The position in the current video.
Future<Duration?> get position async {
return _platform.getPosition(_playerId);
}
/**
* 跳转到指定播放位置
*
* 将视频播放位置设置为指定的时间点。
* 会自动处理边界情况,确保位置在有效范围内。
*
* 边界处理:
* - 如果位置超过视频总时长,设置为视频末尾
* - 如果位置小于零,设置为视频开头
*
* @param position 目标播放位置
* @return Future<void> 异步操作完成标识
*
* 使用场景:
* - 进度条拖拽跳转
* - 章节跳转功能
* - 续播功能实现
*/
/// Sets the video's current timestamp to be at [position].
Future<void> seekTo(Duration position) async {
if (position > value.duration) {
position = value.duration;
} else if (position < Duration.zero) {
position = Duration.zero;
}
await _platform.seekTo(_playerId, position);
_updatePosition(position);
}
/**
* 设置播放速度
*
* 调整视频的播放速率。支持快进、慢放等功能。
*
* @param speed 播放速度倍率(1.0 为正常速度)
* @return Future<void> 异步操作完成标识
*
* 常用速度值:
* - 0.5: 半速播放
* - 1.0: 正常速度
* - 1.5: 1.5倍速
* - 2.0: 双倍速
*
* 使用场景:
* - 快进/慢放功能
* - 学习模式的慢速播放
* - 快速浏览的高速播放
*/
/// Sets the playback speed.
Future<void> setPlaybackSpeed(double speed) async {
value = value.copyWith(playbackSpeed: speed);
await _applyPlaybackSpeed();
}
/**
* 更新播放位置
*
* 内部方法,用于更新播放器状态中的当前位置。
* 会触发状态变化通知,让监听者知道位置已更新。
*
* @param position 新的播放位置
*/
void _updatePosition(Duration position) {
value = value.copyWith(position: position);
}
}
VideoPlayer
/**
* 视频播放器 Widget
*
* 显示由控制器管理的视频内容的 Widget。
* 这是一个有状态的 Widget,会监听控制器的状态变化并相应地更新显示。
*
* 主要功能:
* 1. 显示视频内容
* 2. 监听播放器状态变化
* 3. 处理播放器 ID 变化
* 4. 管理播放器生命周期
*
* 使用方式:
* ```dart
* VideoPlayer(controller)
* ```
*/
/// Widget that displays the video controlled by [controller].
class VideoPlayer extends StatefulWidget {
/**
* 构造视频播放器 Widget
*
* @param controller 负责此 Widget 中视频渲染的 MiniController
*/
/// Uses the given [controller] for all video rendered in this widget.
const VideoPlayer(this.controller, {super.key});
/**
* MiniController 实例
*
* 负责在此 Widget 中渲染视频的控制器。
* 提供视频播放的所有控制功能和状态信息。
*/
/// The [MiniController] responsible for the video being rendered in
/// this widget.
final MiniController controller;
@override
State<VideoPlayer> createState() => _VideoPlayerState();
}
class _VideoPlayerState extends State<VideoPlayer> {
_VideoPlayerState() {
_listener = () {
final int newPlayerId = widget.controller.playerId;
if (newPlayerId != _playerId) {
setState(() {
_playerId = newPlayerId;
});
}
};
}
late VoidCallback _listener;
late int _playerId;
@override
void initState() {
super.initState();
_playerId = widget.controller.playerId;
// Need to listen for initialization events since the actual player ID
// becomes available after asynchronous initialization finishes.
widget.controller.addListener(_listener);
}
@override
void didUpdateWidget(VideoPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.controller.removeListener(_listener);
_playerId = widget.controller.playerId;
widget.controller.addListener(_listener);
}
@override
void deactivate() {
super.deactivate();
widget.controller.removeListener(_listener);
}
@override
Widget build(BuildContext context) {
return _playerId == MiniController.kUninitializedPlayerId
? Container()
: _platform.buildViewWithOptions(VideoViewOptions(playerId: _playerId));
}
}
VideoProgressIndicator
/**
* 视频播放进度指示器
*
* 显示由控制器管理的视频的播放/缓冲状态。
* 提供可视化的进度条显示,包括播放进度和缓冲进度。
*
* 主要功能:
* 1. 显示播放进度(红色进度条)
* 2. 显示缓冲进度(蓝色进度条)
* 3. 支持点击跳转到指定位置
* 4. 自动更新进度显示
*
* 使用方式:
* ```dart
* VideoProgressIndicator(controller)
* ```
*/
/// Displays the play/buffering status of the video controlled by [controller].
class VideoProgressIndicator extends StatefulWidget {
/**
* 构造视频播放进度指示器
*
* 创建一个显示由控制器管理的视频播放/缓冲状态的实例。
*
* @param controller 与此 Widget 关联的 MiniController
*/
/// Construct an instance that displays the play/buffering status of the video
/// controlled by [controller].
const VideoProgressIndicator(this.controller, {super.key});
/**
* MiniController 实例
*
* 实际与此 Widget 关联视频的 MiniController。
* 提供视频状态信息和播放控制功能。
*/
/// The [MiniController] that actually associates a video with this
/// widget.
final MiniController controller;
@override
State<VideoProgressIndicator> createState() => _VideoProgressIndicatorState();
}
class _VideoProgressIndicatorState extends State<VideoProgressIndicator> {
_VideoProgressIndicatorState() {
listener = () {
if (mounted) {
setState(() {});
}
};
}
late VoidCallback listener;
MiniController get controller => widget.controller;
@override
void initState() {
super.initState();
controller.addListener(listener);
}
@override
void deactivate() {
controller.removeListener(listener);
super.deactivate();
}
@override
Widget build(BuildContext context) {
const Color playedColor = Color.fromRGBO(255, 0, 0, 0.7);
const Color bufferedColor = Color.fromRGBO(50, 50, 200, 0.2);
const Color backgroundColor = Color.fromRGBO(200, 200, 200, 0.5);
Widget progressIndicator;
if (controller.value.isInitialized) {
final int duration = controller.value.duration.inMilliseconds;
final int position = controller.value.position.inMilliseconds;
int maxBuffering = 0;
for (final DurationRange range in controller.value.buffered) {
final int end = range.end.inMilliseconds;
if (end > maxBuffering) {
maxBuffering = end;
}
}
progressIndicator = Stack(
fit: StackFit.passthrough,
children: <Widget>[
LinearProgressIndicator(
value: maxBuffering / duration,
valueColor: const AlwaysStoppedAnimation<Color>(bufferedColor),
backgroundColor: backgroundColor,
),
LinearProgressIndicator(
value: position / duration,
valueColor: const AlwaysStoppedAnimation<Color>(playedColor),
backgroundColor: Colors.transparent,
),
],
);
} else {
progressIndicator = const LinearProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(playedColor),
backgroundColor: backgroundColor,
);
}
return _VideoScrubber(
controller: controller,
child: Padding(padding: const EdgeInsets.only(top: 5.0), child: progressIndicator),
);
}
}
_BumbleBeeRemoteVideo
class _BumbleBeeRemoteVideo extends StatefulWidget {
const _BumbleBeeRemoteVideo(this.viewType);
final VideoViewType viewType;
@override
_BumbleBeeRemoteVideoState createState() => _BumbleBeeRemoteVideoState();
}
class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {
late MiniController _controller;
@override
void initState() {
super.initState();
_controller = MiniController.network(
'https://testcdn.vspo.cn/ec/test/2025-09-17/1758080659889485455.MOV',
// 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
viewType: widget.viewType,
);
_controller.addListener(() {
setState(() {});
});
// 初始化
_controller.initialize();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
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: <Widget>[
VideoPlayer(_controller),
_ControlsOverlay(controller: _controller),
VideoProgressIndicator(_controller),
],
),
),
),
],
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
_BumbleBeeEncryptedLiveStream
class _BumbleBeeEncryptedLiveStream extends StatefulWidget {
const _BumbleBeeEncryptedLiveStream(this.viewType);
final VideoViewType viewType;
@override
_BumbleBeeEncryptedLiveStreamState createState() => _BumbleBeeEncryptedLiveStreamState();
}
class _BumbleBeeEncryptedLiveStreamState extends State<_BumbleBeeEncryptedLiveStream> {
late MiniController _controller;
@override
void initState() {
super.initState();
_controller = MiniController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/hls/encrypted_bee.m3u8',
viewType: widget.viewType,
);
_controller.addListener(() {
setState(() {});
});
_controller.initialize().then((_) {
_controller.play();
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
Container(padding: const EdgeInsets.only(top: 20.0)),
const Text('With remote encrypted m3u8'),
Container(
padding: const EdgeInsets.all(20),
child:
_controller.value.isInitialized
? AspectRatio(aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller))
: const Text('loading...'),
),
],
),
);
}
}
_ControlsOverlay
class _ControlsOverlay extends StatelessWidget {
const _ControlsOverlay({required this.controller});
static const List<double> _examplePlaybackRates = <double>[0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0];
final MiniController controller;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
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.topRight,
child: PopupMenuButton<double>(
initialValue: controller.value.playbackSpeed,
tooltip: 'Playback speed',
onSelected: (double speed) {
controller.setPlaybackSpeed(speed);
},
itemBuilder: (BuildContext context) {
return <PopupMenuItem<double>>[
for (final double speed in _examplePlaybackRates) PopupMenuItem<double>(value: speed, child: Text('${speed}x')),
];
},
child: Padding(
padding: const EdgeInsets.symmetric(
// Using less vertical padding as the text is also longer
// horizontally, so it feels like it would need more spacing
// horizontally (matching the aspect ratio of the video).
vertical: 12,
horizontal: 16,
),
child: Text('${controller.value.playbackSpeed}x'),
),
),
),
],
);
}
}
ConfigurePigeon
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/src/messages.g.dart',
objcHeaderOut:
'darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h',
objcSourceOut:
'darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m',
objcOptions: ObjcOptions(
prefix: 'FVP',
headerIncludePath: './include/video_player_avfoundation/messages.g.h',
),
copyrightHeader: 'pigeons/copyright.txt',
),
)
/// Information passed to the platform view creation.
class PlatformVideoViewCreationParams {
const PlatformVideoViewCreationParams({required this.playerId});
final int playerId;
}
class CreationOptions {
CreationOptions({required this.uri, required this.httpHeaders});
String uri;
Map<String, String> httpHeaders;
}
class TexturePlayerIds {
TexturePlayerIds({required this.playerId, required this.textureId});
final int playerId;
final int textureId;
}
@HostApi()
abstract class AVFoundationVideoPlayerApi {
@ObjCSelector('initialize')
void initialize();
// Creates a new player using a platform view for rendering and returns its
// ID.
@ObjCSelector('createPlatformViewPlayerWithOptions:')
int createForPlatformView(CreationOptions params);
// Creates a new player using a texture for rendering and returns its IDs.
@ObjCSelector('createTexturePlayerWithOptions:')
TexturePlayerIds createForTextureView(CreationOptions creationOptions);
@ObjCSelector('setMixWithOthers:')
void setMixWithOthers(bool mixWithOthers);
@ObjCSelector('fileURLForAssetWithName:package:')
String? getAssetUrl(String asset, String? package);
}
@HostApi()
abstract class VideoPlayerInstanceApi {
@ObjCSelector('setLooping:')
void setLooping(bool looping);
@ObjCSelector('setVolume:')
void setVolume(double volume);
@ObjCSelector('setPlaybackSpeed:')
void setPlaybackSpeed(double speed);
void play();
@ObjCSelector('position')
int getPosition();
@async
@ObjCSelector('seekTo:')
void seekTo(int position);
void pause();
void dispose();
}