Commit b56c6a299326a77855a887ed0fe1c63fa3b789c7

Authored by 吴启风
2 parents 413a50da dbca8811

Merge branch 'feature/android' into fix-build-wqf

Showing 54 changed files with 5797 additions and 754 deletions
android/app/build.gradle
... ... @@ -29,7 +29,7 @@ apply from: "../archive_apk.gradle"
29 29 android {
30 30 namespace "com.kouyuxingqiu.wow_english"
31 31 // compileSdkVersion flutter.compileSdkVersion
32   - compileSdkVersion 34
  32 + compileSdkVersion 35
33 33 // 暂时没有ndk需求
34 34 // ndkVersion flutter.ndkVersion
35 35  
... ...
android/build.gradle
1 1 apply from: "/Users/stay/StudioProjects/ishowMavenRepositories.gradle"
2 2  
3 3 buildscript {
4   - ext.kotlin_version = '1.8.21'
  4 + ext.kotlin_version = '1.8.22'
5 5 repositories {
6 6 maven { url 'https://maven.aliyun.com/repository/google' }
7 7 maven { url "https://maven.aliyun.com/repository/public" }
... ... @@ -12,8 +12,7 @@ buildscript {
12 12 }
13 13  
14 14 dependencies {
15   - // AGP版本,目前自动创建的Gradle版本是用的7.5.0
16   - classpath 'com.android.tools.build:gradle:7.3.0'
  15 + classpath 'com.android.tools.build:gradle:8.1.0'
17 16 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
18 17 }
19 18 }
... ...
android/gradle/wrapper/gradle-wrapper.properties
... ... @@ -2,5 +2,4 @@ distributionBase=GRADLE_USER_HOME
2 2 distributionPath=wrapper/dists
3 3 zipStoreBase=GRADLE_USER_HOME
4 4 zipStorePath=wrapper/dists
5   -#distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
6   -distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-7.5-all.zip
  5 +distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-8.3-all.zip
... ...
lib/common/utils/video_playback_diagnostics.dart 0 → 100644
  1 +import 'dart:async';
  2 +
  3 +import 'package:flutter/foundation.dart';
  4 +import 'package:video_player/video_player.dart';
  5 +import 'package:wow_english/utils/toast_util.dart';
  6 +
  7 +/// Shared timeout and user-visible diagnostics for video_player based screens.
  8 +class VideoPlaybackDiagnostics {
  9 + static const initializeTimeout = Duration(seconds: 10);
  10 + static const commandTimeout = Duration(seconds: 5);
  11 + static const bufferingWarningDelay = Duration(seconds: 10);
  12 +
  13 + /// Starts native player disposal while keeping a stuck teardown bounded.
  14 + static Future<void> disposeController(
  15 + VideoPlayerController controller, {
  16 + required String context,
  17 + }) async {
  18 + try {
  19 + await controller.dispose().timeout(commandTimeout);
  20 + } catch (error) {
  21 + debugPrint('$context播放器释放失败或超时: $error');
  22 + }
  23 + }
  24 +
  25 + static String messageFor(Object error, {required String stage}) {
  26 + final detail = error.toString().toLowerCase();
  27 + if (error is TimeoutException) {
  28 + if (stage == '视频初始化') return '视频初始化超时,请检查网络或设备解码能力';
  29 + if (stage == '播放器配置') return '播放器配置超时,设备可能不支持当前播放参数';
  30 + if (stage == '恢复播放进度') return '恢复播放进度超时';
  31 + return '$stage超时';
  32 + }
  33 + if (detail.contains('decoder') ||
  34 + detail.contains('codec') ||
  35 + detail.contains('mediacodec') ||
  36 + detail.contains('acodec')) {
  37 + return '视频解码器启动失败,可能是设备兼容性问题';
  38 + }
  39 + if (detail.contains('http') ||
  40 + detail.contains('socket') ||
  41 + detail.contains('network') ||
  42 + detail.contains('connection') ||
  43 + detail.contains('source error') ||
  44 + detail.contains('sourceerror')) {
  45 + return '视频网络请求失败,请检查网络或视频地址';
  46 + }
  47 + return '$stage失败,请重试';
  48 + }
  49 +
  50 + static void show(String message, {int? videoId}) {
  51 + final idSuffix = videoId == null ? '' : '(视频ID: $videoId)';
  52 + showToast('$message$idSuffix', duration: const Duration(seconds: 4));
  53 + }
  54 +}
... ...
lib/common/utils/video_rendering_preference.dart 0 → 100644
  1 +import 'package:flutter/foundation.dart';
  2 +import 'package:video_player/video_player.dart';
  3 +import 'package:wow_english/utils/sp_util.dart';
  4 +
  5 +/// User-selectable Android video output path for device compatibility testing.
  6 +class VideoRenderingPreference {
  7 + static const _usePlatformViewKey = 'video_player_use_platform_view';
  8 +
  9 + static bool get usesPlatformView =>
  10 + SpUtil.getInstance().get<bool>(_usePlatformViewKey) ?? false;
  11 +
  12 + static VideoViewType get viewType =>
  13 + defaultTargetPlatform == TargetPlatform.android && usesPlatformView
  14 + ? VideoViewType.platformView
  15 + : VideoViewType.textureView;
  16 +
  17 + static Future<void> setUsesPlatformView(bool value) =>
  18 + SpUtil.getInstance().setData(_usePlatformViewKey, value);
  19 +}
... ...
lib/pages/repeataftercontent/widgets/repeat_video_widget.dart
  1 +import 'dart:async';
  2 +
1 3 import 'package:flutter/material.dart';
2 4 import 'package:flutter_bloc/flutter_bloc.dart';
3 5 import 'package:flutter_screenutil/flutter_screenutil.dart';
4 6 import 'package:video_player/video_player.dart';
  7 +import 'package:wow_english/common/utils/video_playback_diagnostics.dart';
  8 +import 'package:wow_english/common/utils/video_rendering_preference.dart';
5 9 import 'package:wow_english/models/read_content_entity.dart';
6 10  
7 11 import '../bloc/repeat_after_content_bloc.dart';
... ... @@ -23,32 +27,75 @@ class _RepeatVideoWidgetState extends State&lt;RepeatVideoWidget&gt; {
23 27 String _totalTime = '00:00';
24 28 double _playDegree = 0.5;
25 29 int _currentPlayIndex = 0;
  30 + Object? _loadError;
  31 + Timer? _bufferingTimer;
  32 + VoidCallback? _controllerListener;
  33 + bool _bufferingWarningShown = false;
  34 + int _loadGeneration = 0;
26 35  
27 36 String formatDuration(Duration duration) {
28   - String minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
29   - String seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
  37 + String minutes =
  38 + duration.inMinutes.remainder(60).toString().padLeft(2, '0');
  39 + String seconds =
  40 + duration.inSeconds.remainder(60).toString().padLeft(2, '0');
30 41 return "$minutes:$seconds";
31 42 }
32 43  
33 44 void _addListener() {
34   - _controller!.addListener(() {
35   - if(_controller!.value.isInitialized) {
36   - if (_controller!.value.isPlaying) {
37   - setState(() {
38   - double currentSecond = (_controller!.value.position.inMinutes.remainder(60)*60+_controller!.value.position.inSeconds.remainder(60)).toDouble();
39   - int totalSecond = _controller!.value.duration.inMinutes.remainder(60)*60+_controller!.value.duration.inSeconds.remainder(60);
40   - _currentTime = formatDuration(_controller!.value.position);
41   - _playDegree = currentSecond/totalSecond;
42   - if(_playDegree < 0) {
43   - _playDegree = 0.0;
44   - }
  45 + _controllerListener = _onControllerChanged;
  46 + _controller?.addListener(_controllerListener!);
  47 + }
  48 +
  49 + void _onControllerChanged() {
  50 + final controller = _controller;
  51 + if (!mounted || controller == null) return;
  52 + _updateBufferingWatch(controller);
  53 + if (controller.value.hasError && _loadError == null) {
  54 + final error = controller.value.errorDescription ?? '未知播放器错误';
  55 + debugPrint('课程儿歌/视频播放失败: $error');
  56 + VideoPlaybackDiagnostics.show(
  57 + VideoPlaybackDiagnostics.messageFor(error, stage: '视频播放/解码'),
  58 + );
  59 + setState(() => _loadError = error);
  60 + return;
  61 + }
  62 + if (!controller.value.isInitialized || !controller.value.isPlaying) return;
  63 + setState(() {
  64 + final currentSecond =
  65 + (controller.value.position.inMinutes.remainder(60) * 60 +
  66 + controller.value.position.inSeconds.remainder(60))
  67 + .toDouble();
  68 + final totalSecond =
  69 + controller.value.duration.inMinutes.remainder(60) * 60 +
  70 + controller.value.duration.inSeconds.remainder(60);
  71 + _currentTime = formatDuration(controller.value.position);
  72 + _playDegree = totalSecond == 0 ? 0 : currentSecond / totalSecond;
  73 + _playDegree = _playDegree.clamp(0.0, 1.0);
  74 + });
  75 + }
45 76  
46   - if(_playDegree > 1) {
47   - _playDegree = 1.0;
48   - }
49   - });
50   - }
  77 + void _updateBufferingWatch(VideoPlayerController controller) {
  78 + if (!controller.value.isBuffering) {
  79 + _bufferingTimer?.cancel();
  80 + _bufferingTimer = null;
  81 + _bufferingWarningShown = false;
  82 + return;
  83 + }
  84 + if (_bufferingTimer != null || _bufferingWarningShown) return;
  85 + _bufferingTimer = Timer(VideoPlaybackDiagnostics.bufferingWarningDelay, () {
  86 + _bufferingTimer = null;
  87 + if (!mounted ||
  88 + controller != _controller ||
  89 + !controller.value.isBuffering) {
  90 + return;
51 91 }
  92 + _bufferingWarningShown = true;
  93 + final error = controller.value.errorDescription;
  94 + final message = error == null
  95 + ? '视频持续缓冲,网络或设备解码可能异常'
  96 + : VideoPlaybackDiagnostics.messageFor(error, stage: '视频播放/解码');
  97 + debugPrint('课程儿歌/视频持续缓冲: $message ${error ?? ''}');
  98 + VideoPlaybackDiagnostics.show(message);
52 99 });
53 100 }
54 101  
... ... @@ -56,127 +103,207 @@ class _RepeatVideoWidgetState extends State&lt;RepeatVideoWidget&gt; {
56 103 void initState() {
57 104 super.initState();
58 105 var videoUrl = '';
59   - if(widget.videoUrls.isNotEmpty) {
60   - videoUrl = widget.videoUrls[_currentPlayIndex]?.videoUrl??'';
  106 + if (widget.videoUrls.isNotEmpty) {
  107 + videoUrl = widget.videoUrls[_currentPlayIndex]?.videoUrl ?? '';
61 108 }
62   - _initVideo(videoUrl);
  109 + unawaited(_initVideo(videoUrl));
63 110 }
64 111  
65 112 @override
66 113 Widget build(BuildContext context) {
67 114 return Center(
68   - child: _controller!.value.isInitialized ? BlocListener<RepeatAfterContentBloc,RepeatAfterContentState>(
69   - listener: (context, state){
70   - if (state is VideoPlayChangeState) {
71   - if (context.read<RepeatAfterContentBloc>().videoPlaying) {
72   - _controller!.play();
73   - } else {
74   - _controller!.pause();
75   - }
76   - }
77   -
78   - if (state is ChangeVideoPlayIndexState) {
79   - if(state.isNext) {
80   - if (_currentPlayIndex != (widget.videoUrls.length-1)) {
81   - _currentPlayIndex++;
82   - _destroyVideo();
83   - _initVideo(widget.videoUrls[_currentPlayIndex]!.videoUrl!);
84   - }
85   - } else {
86   - if (_currentPlayIndex > 0) {
87   - _currentPlayIndex--;
88   - _destroyVideo();
89   - _initVideo(widget.videoUrls[_currentPlayIndex]!.videoUrl!);
90   - }
91   - }
92   - }
93   - },
94   - child: SizedBox(
95   - child: Column(
96   - children: [
97   - Expanded(
98   - child: Stack(
99   - children: [
100   - Container(
101   - width: double.infinity,
102   - height: double.infinity,
103   - alignment: Alignment.center,
104   - decoration: BoxDecoration(
105   - color: Colors.black,
106   - border: Border.all(
107   - width: 1.0,
108   - color: const Color(0xFF140C10),
  115 + child: _loadError != null
  116 + ? Column(
  117 + mainAxisSize: MainAxisSize.min,
  118 + children: [
  119 + const Text('视频加载失败,请重试'),
  120 + const SizedBox(height: 8),
  121 + ElevatedButton(
  122 + onPressed: () => unawaited(_initVideo(
  123 + widget.videoUrls.isEmpty
  124 + ? ''
  125 + : widget.videoUrls[_currentPlayIndex]?.videoUrl ?? '',
  126 + )),
  127 + child: const Text('重试'),
  128 + ),
  129 + ],
  130 + )
  131 + : _controller?.value.isInitialized == true
  132 + ? BlocListener<RepeatAfterContentBloc, RepeatAfterContentState>(
  133 + listener: (context, state) {
  134 + if (state is VideoPlayChangeState) {
  135 + unawaited(_setPlayback(
  136 + context.read<RepeatAfterContentBloc>().videoPlaying,
  137 + ));
  138 + }
  139 + if (state is ChangeVideoPlayIndexState) {
  140 + final nextIndex = state.isNext
  141 + ? _currentPlayIndex + 1
  142 + : _currentPlayIndex - 1;
  143 + if (nextIndex >= 0 &&
  144 + nextIndex < widget.videoUrls.length) {
  145 + _currentPlayIndex = nextIndex;
  146 + unawaited(_initVideo(
  147 + widget.videoUrls[nextIndex]?.videoUrl ?? '',
  148 + ));
  149 + }
  150 + }
  151 + },
  152 + child: SizedBox(
  153 + child: Column(
  154 + children: [
  155 + Expanded(
  156 + child: Stack(
  157 + children: [
  158 + Container(
  159 + width: double.infinity,
  160 + height: double.infinity,
  161 + alignment: Alignment.center,
  162 + decoration: BoxDecoration(
  163 + color: Colors.black,
  164 + border: Border.all(
  165 + width: 1.0,
  166 + color: const Color(0xFF140C10),
  167 + ),
  168 + borderRadius: BorderRadius.circular(5.r),
  169 + ),
  170 + child: AspectRatio(
  171 + aspectRatio: _controller!.value.aspectRatio,
  172 + child: VideoPlayer(_controller!),
  173 + ),
  174 + ),
  175 + Positioned(
  176 + left: 0,
  177 + right: 0,
  178 + bottom: 0,
  179 + child: LinearProgressIndicator(
  180 + color: const Color(0xFF78B72D),
  181 + backgroundColor: const Color(0xFF7E756C),
  182 + value: _playDegree,
  183 + ),
  184 + ),
  185 + ],
109 186 ),
110   - borderRadius: BorderRadius.circular(5.r)
111   - ),
112   - child: AspectRatio(
113   - aspectRatio: _controller!.value.aspectRatio,
114   - child: VideoPlayer(_controller!),
115   - ),
  187 + ),
  188 + Row(
  189 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
  190 + children: [Text(_currentTime), Text(_totalTime)],
  191 + ),
  192 + ],
116 193 ),
117   - Positioned(
118   - left: 0,
119   - right: 0,
120   - bottom: 0,
121   - child: LinearProgressIndicator(
122   - color: const Color(0xFF78B72D),
123   - backgroundColor: const Color(0xFF7E756C),
124   - value: _playDegree,
125   - ))
126   - ],
  194 + ),
  195 + )
  196 + : Container(
  197 + color: Colors.white,
  198 + child: const CircularProgressIndicator(),
127 199 ),
128   - ),
129   - Row(
130   - mainAxisAlignment: MainAxisAlignment.spaceBetween,
131   - children: [
132   - Text(_currentTime),
133   - Text(_totalTime),
134   - ],
135   - )
136   - ],
137   - ),
138   - )
139   - )
140   - :
141   - Container(
142   - color: Colors.white,
143   - child: const CircularProgressIndicator(),
144   - // Text(
145   - // '视频加载中....',
146   - // style: TextStyle(
147   - // fontSize: 20.sp,
148   - // color: Colors.black
149   - // ),
150   - // ),
151   - ),
152 200 );
153 201 }
154 202  
155   - void _initVideo(String videoUrl) {
156   - if(videoUrl.isNotEmpty) {
157   - Uri uri = Uri.parse(videoUrl);
158   - _controller = VideoPlayerController.networkUrl(uri)
159   - ..initialize().then((_){
160   - setState(() {
161   - _currentTime = formatDuration(_controller!.value.position);
162   - _totalTime = formatDuration(_controller!.value.duration);
163   - _controller!.setLooping(false);
164   - _controller!.setVolume(100);
165   - _controller!.play();
166   - });
167   - _addListener();
168   - });
  203 + Future<void> _initVideo(String videoUrl) async {
  204 + final generation = ++_loadGeneration;
  205 + _bufferingTimer?.cancel();
  206 + _bufferingWarningShown = false;
  207 + await _destroyController();
  208 + if (!mounted || generation != _loadGeneration) return;
  209 + _loadError = null;
  210 + final url = videoUrl.trim();
  211 + if (url.isEmpty) {
  212 + _loadError = StateError('课程视频地址为空');
  213 + VideoPlaybackDiagnostics.show('课程视频地址为空');
  214 + if (mounted) setState(() {});
  215 + return;
  216 + }
  217 + final controller = VideoPlayerController.networkUrl(
  218 + Uri.parse(url),
  219 + viewType: VideoRenderingPreference.viewType,
  220 + );
  221 + _controller = controller;
  222 + if (mounted) setState(() {});
  223 + var stage = '视频初始化';
  224 + try {
  225 + await controller
  226 + .initialize()
  227 + .timeout(VideoPlaybackDiagnostics.initializeTimeout);
  228 + if (!mounted ||
  229 + generation != _loadGeneration ||
  230 + controller != _controller) {
  231 + await controller.dispose();
  232 + return;
  233 + }
  234 + stage = '播放器配置';
  235 + await controller
  236 + .setLooping(false)
  237 + .timeout(VideoPlaybackDiagnostics.commandTimeout);
  238 + await controller
  239 + .setVolume(1)
  240 + .timeout(VideoPlaybackDiagnostics.commandTimeout);
  241 + _addListener();
  242 + _updateBufferingWatch(controller);
  243 + await controller.play().timeout(VideoPlaybackDiagnostics.commandTimeout);
  244 + if (!mounted || generation != _loadGeneration) return;
  245 + setState(() {
  246 + _currentTime = formatDuration(controller.value.position);
  247 + _totalTime = formatDuration(controller.value.duration);
  248 + });
  249 + } catch (error) {
  250 + final failedCurrentController = controller == _controller;
  251 + if (failedCurrentController) {
  252 + _bufferingTimer?.cancel();
  253 + _bufferingTimer = null;
  254 + }
  255 + await VideoPlaybackDiagnostics.disposeController(
  256 + controller,
  257 + context: '课程儿歌/视频初始化失败($stage)',
  258 + );
  259 + if (!mounted ||
  260 + generation != _loadGeneration ||
  261 + !failedCurrentController) {
  262 + return;
  263 + }
  264 + _controller = null;
  265 + final message = VideoPlaybackDiagnostics.messageFor(error, stage: stage);
  266 + debugPrint('课程儿歌/视频加载失败(stage=$stage): $error');
  267 + VideoPlaybackDiagnostics.show(message);
  268 + setState(() => _loadError = error);
169 269 }
170 270 }
171 271  
172   - void _destroyVideo() {
173   - _controller?.dispose();
174   - _controller?.removeListener(() {});
  272 + Future<void> _setPlayback(bool shouldPlay) async {
  273 + final controller = _controller;
  274 + if (controller?.value.isInitialized != true) return;
  275 + try {
  276 + final operation = shouldPlay ? controller!.play() : controller!.pause();
  277 + await operation.timeout(VideoPlaybackDiagnostics.commandTimeout);
  278 + } catch (error) {
  279 + debugPrint('课程视频播放状态切换失败: $error');
  280 + VideoPlaybackDiagnostics.show(
  281 + VideoPlaybackDiagnostics.messageFor(error, stage: '播放器控制'),
  282 + );
  283 + }
  284 + }
  285 +
  286 + Future<void> _destroyController() async {
  287 + final controller = _controller;
  288 + final listener = _controllerListener;
  289 + _controllerListener = null;
  290 + _controller = null;
  291 + if (controller != null) {
  292 + if (listener != null) {
  293 + controller.removeListener(listener);
  294 + }
  295 + await VideoPlaybackDiagnostics.disposeController(
  296 + controller,
  297 + context: '课程儿歌/视频切换',
  298 + );
  299 + }
175 300 }
176 301  
177 302 @override
178 303 void dispose() {
179   - _destroyVideo();
  304 + _loadGeneration++;
  305 + _bufferingTimer?.cancel();
  306 + unawaited(_destroyController());
180 307 super.dispose();
181 308 }
182 309 }
183 310 \ No newline at end of file
... ...
lib/pages/shopping/bloc.dart
... ... @@ -15,13 +15,17 @@ import &#39;../../utils/toast_util.dart&#39;;
15 15 import 'event.dart';
16 16 import 'state.dart';
17 17  
  18 +const bool isWechatPayOptionVisible = false;
  19 +
18 20 class ShoppingBloc extends Bloc<ShoppingEvent, ShoppingState> {
19 21  
20 22 ProductEntity? _productData;
21 23  
22 24 ProductEntity? get productData => _productData;
23 25  
24   - PaymentChannel _curPaymentChannel = PaymentChannel.wechatPay;
  26 + PaymentChannel _curPaymentChannel = isWechatPayOptionVisible
  27 + ? PaymentChannel.wechatPay
  28 + : PaymentChannel.aliPay;
25 29  
26 30 PaymentChannel get curPaymentChannel => _curPaymentChannel;
27 31  
... ...
lib/pages/shopping/view.dart
... ... @@ -149,17 +149,26 @@ Widget _paymentWidget() =&gt; BlocBuilder&lt;ShoppingBloc, ShoppingState&gt;(
149 149 fontFamily: 'PingFangSC-Regular')),
150 150 ),
151 151 const SizedBox(height: 18.0),
152   - buildRadioOption(
153   - value: PaymentChannel.wechatPay.payChannelType,
154   - icon: AssetImage('weixin'.assetPng),
155   - text: PaymentChannel.wechatPay.payChannelName,
156   - isSelected: bloc.curPaymentChannel.payChannelType ==
157   - PaymentChannel.wechatPay.payChannelType,
158   - onSelect: (newValue) {
159   - bloc.add(ChangePaymentChannelEvent(PaymentChannel.wechatPay));
160   - },
  152 + Visibility(
  153 + visible: isWechatPayOptionVisible,
  154 + child: Column(
  155 + children: [
  156 + buildRadioOption(
  157 + value: PaymentChannel.wechatPay.payChannelType,
  158 + icon: AssetImage('weixin'.assetPng),
  159 + text: PaymentChannel.wechatPay.payChannelName,
  160 + isSelected: bloc.curPaymentChannel.payChannelType ==
  161 + PaymentChannel.wechatPay.payChannelType,
  162 + onSelect: (newValue) {
  163 + bloc.add(
  164 + ChangePaymentChannelEvent(PaymentChannel.wechatPay),
  165 + );
  166 + },
  167 + ),
  168 + const SizedBox(height: 15.0),
  169 + ],
  170 + ),
161 171 ),
162   - const SizedBox(height: 15.0),
163 172 buildRadioOption(
164 173 value: PaymentChannel.aliPay.payChannelType,
165 174 icon: AssetImage('zhifubao'.assetPng),
... ...
lib/pages/user/setting/setting_page.dart
  1 +import 'package:flutter/foundation.dart';
1 2 import 'package:flutter/material.dart';
2 3 import 'package:flutter_easyloading/flutter_easyloading.dart';
3 4 import 'package:flutter_screenutil/flutter_screenutil.dart';
4 5 import 'package:package_info_plus/package_info_plus.dart';
  6 +import 'package:wow_english/common/utils/video_rendering_preference.dart';
5 7 import 'package:wow_english/common/widgets/we_app_bar.dart';
6 8 import 'package:wow_english/utils/toast_util.dart';
7 9  
... ... @@ -19,9 +21,11 @@ class SettingPage extends StatefulWidget {
19 21 class SettingPageState extends State<SettingPage> {
20 22 String? _version;
21 23 String? _buildNum;
  24 + bool _usePlatformView = false;
22 25 @override
23 26 void initState() {
24 27 super.initState();
  28 + _usePlatformView = VideoRenderingPreference.usesPlatformView;
25 29 _retrieveVersionInfo();
26 30 }
27 31  
... ... @@ -62,6 +66,10 @@ class SettingPageState extends State&lt;SettingPage&gt; {
62 66 _buildItemWidget('帮助与反馈', onPress: () {
63 67 pushNamed(AppRouteName.reBack);
64 68 }),
  69 + if (defaultTargetPlatform == TargetPlatform.android) ...[
  70 + 12.verticalSpace,
  71 + _buildVideoRenderingSetting(),
  72 + ],
65 73 12.verticalSpace,
66 74 _buildItemWidget('Version: $_version Build:$_buildNum',
67 75 onPress: () {}),
... ... @@ -73,6 +81,49 @@ class SettingPageState extends State&lt;SettingPage&gt; {
73 81 );
74 82 }
75 83  
  84 + Widget _buildVideoRenderingSetting() {
  85 + return Container(
  86 + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
  87 + decoration: BoxDecoration(
  88 + border: Border.all(color: const Color(0xFF140C10), width: 1.5.w),
  89 + borderRadius: BorderRadius.circular(15.r),
  90 + color: Colors.white,
  91 + ),
  92 + child: Row(
  93 + children: [
  94 + Expanded(
  95 + child: Column(
  96 + crossAxisAlignment: CrossAxisAlignment.start,
  97 + children: [
  98 + Text(
  99 + '视频兼容模式',
  100 + style: TextStyle(
  101 + color: const Color(0xFF333333), fontSize: 19.sp),
  102 + ),
  103 + 4.verticalSpace,
  104 + Text(
  105 + _usePlatformView
  106 + ? 'PlatformView(兼容测试)· 新打开的视频生效'
  107 + : 'Texture(默认)· 新打开的视频生效',
  108 + style: TextStyle(
  109 + color: const Color(0xFF777777), fontSize: 13.sp),
  110 + ),
  111 + ],
  112 + ),
  113 + ),
  114 + Switch.adaptive(
  115 + value: _usePlatformView,
  116 + onChanged: (value) async {
  117 + setState(() => _usePlatformView = value);
  118 + await VideoRenderingPreference.setUsesPlatformView(value);
  119 + showToast(value ? '已切换为 PlatformView' : '已切换为 Texture');
  120 + },
  121 + ),
  122 + ],
  123 + ),
  124 + );
  125 + }
  126 +
76 127 Widget _buildItemWidget(String text, {VoidCallback? onPress}) {
77 128 return OutlinedButton(
78 129 onPressed: () => onPress?.call(),
... ...
lib/pages/video/lookvideo/widgets/video_widget.dart
  1 +import 'dart:async';
  2 +
1 3 import 'package:common_utils/common_utils.dart';
2 4 import 'package:flutter/material.dart';
3 5 import 'package:flutter_bloc/flutter_bloc.dart';
4 6 import 'package:flutter_screenutil/flutter_screenutil.dart';
5 7 import 'package:video_player/video_player.dart';
6 8 import 'package:wow_english/common/extension/string_extension.dart';
  9 +import 'package:wow_english/common/utils/video_playback_diagnostics.dart';
  10 +import 'package:wow_english/common/utils/video_rendering_preference.dart';
7 11 import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart';
8 12 import 'package:wow_english/route/route.dart';
9 13  
... ... @@ -36,6 +40,11 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
36 40 String _totalTime = '00:00';
37 41 double _playDegree = 0.0;
38 42 bool _hiddenTipView = false;
  43 + Object? _loadError;
  44 + Timer? _bufferingTimer;
  45 + int _loadGeneration = 0;
  46 + VoidCallback? _diagnosticListener;
  47 + bool _bufferingWarningShown = false;
39 48 TimerUtil? timerUtil;
40 49  
41 50 String formatDuration(Duration duration) {
... ... @@ -142,19 +151,132 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
142 151 @override
143 152 void initState() {
144 153 super.initState();
145   - Uri uri = Uri.parse(widget.videoUrl);
146   - _controller = VideoPlayerController.networkUrl(uri)
147   - ..initialize().then((_) {
148   - startTimer();
149   - setState(() {
150   - _currentTime = formatDuration(_controller!.value.position);
151   - _totalTime = formatDuration(_controller!.value.duration);
152   - _controller!.setLooping(!widget.isTopic);
153   - _controller!.setVolume(100);
154   - _controller!.play();
155   - });
156   - _addListener();
  154 + unawaited(_initializeVideo());
  155 + }
  156 +
  157 + Future<void> _initializeVideo() async {
  158 + final generation = ++_loadGeneration;
  159 + _bufferingTimer?.cancel();
  160 + _bufferingWarningShown = false;
  161 + final oldController = _controller;
  162 + if (oldController != null) {
  163 + if (_diagnosticListener != null) {
  164 + oldController.removeListener(_diagnosticListener!);
  165 + }
  166 + }
  167 + _diagnosticListener = null;
  168 + _controller = null;
  169 + _loadError = null;
  170 + if (oldController != null) {
  171 + await VideoPlaybackDiagnostics.disposeController(
  172 + oldController,
  173 + context: '课程视频切换',
  174 + );
  175 + }
  176 + if (!mounted || generation != _loadGeneration) return;
  177 + final url = widget.videoUrl.trim();
  178 + if (url.isEmpty) {
  179 + _loadError = StateError('课程视频地址为空');
  180 + VideoPlaybackDiagnostics.show('课程视频地址为空');
  181 + if (mounted) setState(() {});
  182 + return;
  183 + }
  184 + final controller = VideoPlayerController.networkUrl(
  185 + Uri.parse(url),
  186 + viewType: VideoRenderingPreference.viewType,
  187 + );
  188 + _controller = controller;
  189 + if (mounted) setState(() {});
  190 + var stage = '视频初始化';
  191 + try {
  192 + await controller
  193 + .initialize()
  194 + .timeout(VideoPlaybackDiagnostics.initializeTimeout);
  195 + if (!mounted ||
  196 + generation != _loadGeneration ||
  197 + controller != _controller) {
  198 + await controller.dispose();
  199 + return;
  200 + }
  201 + stage = '播放器配置';
  202 + await controller
  203 + .setLooping(!widget.isTopic)
  204 + .timeout(VideoPlaybackDiagnostics.commandTimeout);
  205 + await controller
  206 + .setVolume(1)
  207 + .timeout(VideoPlaybackDiagnostics.commandTimeout);
  208 + _addListener();
  209 + _diagnosticListener = _onDiagnosticChanged;
  210 + controller.addListener(_diagnosticListener!);
  211 + _updateBufferingWatch(controller);
  212 + await controller.play().timeout(VideoPlaybackDiagnostics.commandTimeout);
  213 + if (!mounted || generation != _loadGeneration) return;
  214 + startTimer();
  215 + setState(() {
  216 + _currentTime = formatDuration(controller.value.position);
  217 + _totalTime = formatDuration(controller.value.duration);
157 218 });
  219 + } catch (error) {
  220 + final failedCurrentController = controller == _controller;
  221 + if (failedCurrentController) {
  222 + _bufferingTimer?.cancel();
  223 + _bufferingTimer = null;
  224 + controller.removeListener(_diagnosticListener ?? _onDiagnosticChanged);
  225 + }
  226 + await VideoPlaybackDiagnostics.disposeController(
  227 + controller,
  228 + context: '课程视频初始化失败($stage)',
  229 + );
  230 + if (!mounted ||
  231 + generation != _loadGeneration ||
  232 + !failedCurrentController) {
  233 + return;
  234 + }
  235 + _controller = null;
  236 + final message = VideoPlaybackDiagnostics.messageFor(error, stage: stage);
  237 + debugPrint('课程视频加载失败(stage=$stage): $error');
  238 + VideoPlaybackDiagnostics.show(message);
  239 + setState(() => _loadError = error);
  240 + }
  241 + }
  242 +
  243 + void _onDiagnosticChanged() {
  244 + final controller = _controller;
  245 + if (!mounted || controller == null) return;
  246 + _updateBufferingWatch(controller);
  247 + if (controller.value.hasError && _loadError == null) {
  248 + final error = controller.value.errorDescription ?? '未知播放器错误';
  249 + debugPrint('课程视频播放失败: $error');
  250 + VideoPlaybackDiagnostics.show(
  251 + VideoPlaybackDiagnostics.messageFor(error, stage: '视频播放/解码'),
  252 + );
  253 + setState(() => _loadError = error);
  254 + }
  255 + }
  256 +
  257 + void _updateBufferingWatch(VideoPlayerController controller) {
  258 + if (!controller.value.isBuffering) {
  259 + _bufferingTimer?.cancel();
  260 + _bufferingTimer = null;
  261 + _bufferingWarningShown = false;
  262 + return;
  263 + }
  264 + if (_bufferingTimer != null || _bufferingWarningShown) return;
  265 + _bufferingTimer = Timer(VideoPlaybackDiagnostics.bufferingWarningDelay, () {
  266 + _bufferingTimer = null;
  267 + if (!mounted ||
  268 + controller != _controller ||
  269 + !controller.value.isBuffering) {
  270 + return;
  271 + }
  272 + _bufferingWarningShown = true;
  273 + final error = controller.value.errorDescription;
  274 + final message = error == null
  275 + ? '视频持续缓冲,网络或设备解码可能异常'
  276 + : VideoPlaybackDiagnostics.messageFor(error, stage: '视频播放/解码');
  277 + debugPrint('课程视频持续缓冲: $message ${error ?? ''}');
  278 + VideoPlaybackDiagnostics.show(message);
  279 + });
158 280 }
159 281  
160 282 @override
... ... @@ -162,12 +284,18 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
162 284 return BlocListener<LookVideoBloc, BaseSectionState>(
163 285 listener: (context, state) async {
164 286 if (state is SectionAgainState) {
165   - await _controller?.seekTo(Duration.zero);
166   - _controller?.play();
  287 + final controller = _controller;
  288 + if (controller?.value.isInitialized == true) {
  289 + await controller!.seekTo(Duration.zero);
  290 + await controller.play();
  291 + }
167 292 }
168 293 },
169 294 child: GestureDetector(
170 295 onTap: () {
  296 + if (_loadError != null || _controller?.value.isInitialized != true) {
  297 + return;
  298 + }
171 299 setState(() {
172 300 _hiddenTipView = !_hiddenTipView;
173 301 if (!_hiddenTipView) {
... ... @@ -180,7 +308,7 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
180 308 });
181 309 },
182 310 onDoubleTap: () {
183   - if (_controller!.value.isInitialized) {
  311 + if (_controller?.value.isInitialized == true) {
184 312 if (_controller!.value.isPlaying) {
185 313 _controller!.pause();
186 314 } else {
... ... @@ -190,65 +318,78 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
190 318 }
191 319 },
192 320 child: Center(
193   - child: _controller!.value.isInitialized
194   - ? Stack(
195   - alignment: Alignment.center,
  321 + child: _loadError != null
  322 + ? Column(
  323 + mainAxisSize: MainAxisSize.min,
196 324 children: [
197   - SizedBox(
198   - height: double.infinity,
199   - width: double.infinity,
200   - child: AspectRatio(
201   - aspectRatio: _controller!.value.aspectRatio,
202   - child: VideoPlayer(_controller!),
203   - ),
204   - ),
205   - Offstage(
206   - offstage: _hiddenTipView,
207   - child: VideoOperaWidget(
208   - title: widget.typeTitle ?? 'song',
209   - degree: _playDegree,
210   - totalTime: _totalTime,
211   - currentTime: _currentTime,
212   - isPlay: _controller!.value.isPlaying,
213   - actionEvent: (OperationType type) {
214   - actionType(type);
215   - },
216   - sliderChangeEvent: (double degree) {
217   - int totalSecond = _controller!
218   - .value.duration.inMinutes
219   - .remainder(60) *
220   - 60 +
221   - _controller!.value.duration.inSeconds
222   - .remainder(60);
223   - int positionSecond = (totalSecond * degree).toInt();
224   - _controller!
225   - .seekTo(Duration(seconds: positionSecond));
226   - },
227   - ),
  325 + const Text('视频加载失败,请重试'),
  326 + const SizedBox(height: 8),
  327 + ElevatedButton(
  328 + onPressed: _initializeVideo,
  329 + child: const Text('重试'),
228 330 ),
229   - Offstage(
230   - offstage: _controller!.value.isPlaying,
231   - child: IconButton(
232   - onPressed: () {
233   - _controller!.play();
234   - },
235   - icon: Image.asset(
236   - 'video_stop'.assetPng,
237   - width: 70.w,
238   - height: 70.h,
239   - ),
240   - ),
241   - )
242 331 ],
243 332 )
244   - : Container(
245   - color: Colors.white,
246   - child: const CircularProgressIndicator(),
247   - // Text(
248   - // '视频加载中....',
249   - // style: TextStyle(fontSize: 20.sp, color: Colors.black),
250   - // ),
251   - ),
  333 + : _controller?.value.isInitialized == true
  334 + ? Stack(
  335 + alignment: Alignment.center,
  336 + children: [
  337 + SizedBox(
  338 + height: double.infinity,
  339 + width: double.infinity,
  340 + child: AspectRatio(
  341 + aspectRatio: _controller!.value.aspectRatio,
  342 + child: VideoPlayer(_controller!),
  343 + ),
  344 + ),
  345 + Offstage(
  346 + offstage: _hiddenTipView,
  347 + child: VideoOperaWidget(
  348 + title: widget.typeTitle ?? 'song',
  349 + degree: _playDegree,
  350 + totalTime: _totalTime,
  351 + currentTime: _currentTime,
  352 + isPlay: _controller!.value.isPlaying,
  353 + actionEvent: (OperationType type) {
  354 + actionType(type);
  355 + },
  356 + sliderChangeEvent: (double degree) {
  357 + int totalSecond = _controller!
  358 + .value.duration.inMinutes
  359 + .remainder(60) *
  360 + 60 +
  361 + _controller!.value.duration.inSeconds
  362 + .remainder(60);
  363 + int positionSecond =
  364 + (totalSecond * degree).toInt();
  365 + _controller!
  366 + .seekTo(Duration(seconds: positionSecond));
  367 + },
  368 + ),
  369 + ),
  370 + Offstage(
  371 + offstage: _controller!.value.isPlaying,
  372 + child: IconButton(
  373 + onPressed: () {
  374 + _controller!.play();
  375 + },
  376 + icon: Image.asset(
  377 + 'video_stop'.assetPng,
  378 + width: 70.w,
  379 + height: 70.h,
  380 + ),
  381 + ),
  382 + )
  383 + ],
  384 + )
  385 + : Container(
  386 + color: Colors.white,
  387 + child: const CircularProgressIndicator(),
  388 + // Text(
  389 + // '视频加载中....',
  390 + // style: TextStyle(fontSize: 20.sp, color: Colors.black),
  391 + // ),
  392 + ),
252 393 ),
253 394 ),
254 395 );
... ... @@ -256,8 +397,18 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
256 397  
257 398 @override
258 399 void dispose() {
259   - _controller?.dispose();
260   - _controller?.removeListener(() {});
  400 + _loadGeneration++;
  401 + _bufferingTimer?.cancel();
  402 + if (_diagnosticListener != null) {
  403 + _controller?.removeListener(_diagnosticListener!);
  404 + }
  405 + final controller = _controller;
  406 + if (controller != null) {
  407 + unawaited(VideoPlaybackDiagnostics.disposeController(
  408 + controller,
  409 + context: '课程视频页面退出',
  410 + ));
  411 + }
261 412 if (timerUtil != null) {
262 413 timerUtil!.cancel();
263 414 timerUtil = null;
... ...
lib/pages/video/zone/video_zone_page.dart
... ... @@ -2,12 +2,12 @@ import &#39;dart:async&#39;;
2 2  
3 3 import 'package:cached_network_image/cached_network_image.dart';
4 4 import 'package:flutter/material.dart';
5   -import 'package:flutter/services.dart';
6 5 import 'package:flutter_screenutil/flutter_screenutil.dart';
7 6 import 'package:wow_english/common/dialogs/show_dialog.dart';
8 7 import 'package:wow_english/common/request/dao/video_dao.dart';
9 8 import 'package:wow_english/common/widgets/we_app_bar.dart';
10 9 import 'package:wow_english/route/route.dart';
  10 +import 'package:wow_english/utils/toast_util.dart';
11 11  
12 12 import 'video_zone_item.dart';
13 13  
... ... @@ -24,104 +24,93 @@ class _VideoZonePageState extends State&lt;VideoZonePage&gt; {
24 24 @override
25 25 void initState() {
26 26 super.initState();
27   - unawaited(
28   - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky),
29   - );
30   - _itemsFuture = VideoDao.list();
  27 + _itemsFuture = _loadItems();
31 28 }
32 29  
33   - @override
34   - void dispose() {
35   - unawaited(
36   - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky),
37   - );
38   - super.dispose();
  30 + Future<List<VideoZoneItem>> _loadItems() async {
  31 + try {
  32 + return await VideoDao.list().timeout(const Duration(seconds: 10));
  33 + } on TimeoutException catch (error) {
  34 + debugPrint('获取视频列表超时: $error');
  35 + showToast('视频列表加载超时,请检查网络后重试');
  36 + rethrow;
  37 + } catch (error) {
  38 + debugPrint('获取视频列表失败: $error');
  39 + showToast('视频列表加载失败,请稍后重试');
  40 + rethrow;
  41 + }
39 42 }
40 43  
41 44 void _reload() {
42 45 setState(() {
43   - _itemsFuture = VideoDao.list();
  46 + _itemsFuture = _loadItems();
44 47 });
45 48 }
46 49  
47 50 @override
48 51 Widget build(BuildContext context) {
49   - return MediaQuery.removePadding(
50   - context: context,
51   - removeTop: true,
52   - child: Scaffold(
53   - backgroundColor: Colors.white,
54   - appBar: const WEAppBar(
55   - titleText: '视频专区',
56   - centerTitle: false,
57   - ),
58   - body: FutureBuilder<List<VideoZoneItem>>(
59   - future: _itemsFuture,
60   - builder: (context, snapshot) {
61   - if (snapshot.connectionState == ConnectionState.waiting) {
62   - return const Center(child: CircularProgressIndicator());
63   - }
64   - if (snapshot.hasError) {
65   - return _VideoLoadError(onRetry: _reload);
66   - }
67   - final items = snapshot.data ?? const <VideoZoneItem>[];
68   - if (items.isEmpty) {
69   - return const Center(
70   - child: Text(
71   - '暂无视频',
72   - style: TextStyle(color: Color(0xFF999999), fontSize: 18),
73   - ),
74   - );
75   - }
76   - return RefreshIndicator(
77   - onRefresh: () async {
78   - final future = VideoDao.list();
79   - setState(() => _itemsFuture = future);
80   - await future;
81   - },
82   - child: LayoutBuilder(
83   - builder: (context, constraints) {
84   - final horizontalPadding = 30.w;
85   - final crossAxisSpacing = 12.w;
86   - final titleStyle = _VideoCard.createTitleStyle(context);
87   - final titlePainter = TextPainter(
88   - text: TextSpan(text: '视频标题', style: titleStyle),
89   - maxLines: 1,
90   - textDirection: Directionality.of(context),
91   - textScaler: MediaQuery.textScalerOf(context),
92   - )..layout();
93   - final cardWidth = (constraints.maxWidth -
94   - horizontalPadding * 2 -
95   - crossAxisSpacing * 2) /
96   - 3;
97   - final cardHeight =
98   - cardWidth * 9 / 16 + 5.h + titlePainter.height;
99   - return GridView.builder(
100   - physics: const AlwaysScrollableScrollPhysics(),
101   - padding: EdgeInsets.fromLTRB(
102   - horizontalPadding,
103   - 10.h,
104   - horizontalPadding,
105   - 16.h,
106   - ),
107   - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
108   - crossAxisCount: 3,
109   - mainAxisSpacing: 14.h,
110   - crossAxisSpacing: crossAxisSpacing,
111   - mainAxisExtent: cardHeight,
112   - ),
113   - itemCount: items.length,
114   - itemBuilder: (context, index) => _VideoCard(
115   - item: items[index],
116   - onTap: () => _openVideo(items, index),
117   - titleStyle: titleStyle,
118   - ),
119   - );
120   - },
  52 + return Scaffold(
  53 + backgroundColor: Colors.white,
  54 + appBar: const WEAppBar(
  55 + titleText: '视频专区',
  56 + centerTitle: false,
  57 + ),
  58 + body: FutureBuilder<List<VideoZoneItem>>(
  59 + future: _itemsFuture,
  60 + builder: (context, snapshot) {
  61 + if (snapshot.connectionState == ConnectionState.waiting) {
  62 + return const Center(child: CircularProgressIndicator());
  63 + }
  64 + if (snapshot.hasError) {
  65 + return _VideoLoadError(onRetry: _reload);
  66 + }
  67 + final items = snapshot.data ?? const <VideoZoneItem>[];
  68 + if (items.isEmpty) {
  69 + return const Center(
  70 + child: Text(
  71 + '暂无视频',
  72 + style: TextStyle(color: Color(0xFF999999), fontSize: 18),
121 73 ),
122 74 );
123   - },
124   - ),
  75 + }
  76 + return RefreshIndicator(
  77 + onRefresh: () async {
  78 + final future = _loadItems();
  79 + setState(() => _itemsFuture = future);
  80 + try {
  81 + await future;
  82 + } catch (_) {
  83 + // The FutureBuilder shows the error state; _loadItems already toasts it.
  84 + }
  85 + },
  86 + child: LayoutBuilder(
  87 + builder: (context, constraints) {
  88 + final titleStyle = _VideoCard.createTitleStyle(context);
  89 + return GridView.builder(
  90 + physics: const AlwaysScrollableScrollPhysics(),
  91 + padding: EdgeInsets.fromLTRB(
  92 + 14.w,
  93 + 10.h,
  94 + 14.w,
  95 + 16.h,
  96 + ),
  97 + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
  98 + crossAxisCount: 4,
  99 + mainAxisSpacing: 14.h,
  100 + crossAxisSpacing: 12.w,
  101 + childAspectRatio: 1.55,
  102 + ),
  103 + itemCount: items.length,
  104 + itemBuilder: (context, index) => _VideoCard(
  105 + item: items[index],
  106 + onTap: () => _openVideo(items, index),
  107 + titleStyle: titleStyle,
  108 + ),
  109 + );
  110 + },
  111 + ),
  112 + );
  113 + },
125 114 ),
126 115 );
127 116 }
... ... @@ -144,13 +133,7 @@ class _VideoZonePageState extends State&lt;VideoZonePage&gt; {
144 133 .toList(),
145 134 'initialVideoId': item.id,
146 135 },
147   - ).then((_) {
148   - if (!mounted) return;
149   - unawaited(
150   - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky),
151   - );
152   - _reload();
153   - });
  136 + ).then((_) => _reload());
154 137 }
155 138 }
156 139  
... ... @@ -203,8 +186,7 @@ class _VideoCard extends StatelessWidget {
203 186 child: Column(
204 187 crossAxisAlignment: CrossAxisAlignment.start,
205 188 children: [
206   - AspectRatio(
207   - aspectRatio: 16 / 9,
  189 + Expanded(
208 190 child: ClipRRect(
209 191 borderRadius: BorderRadius.circular(12.r),
210 192 child: CachedNetworkImage(
... ...
lib/pages/video/zone/video_zone_player_page.dart
... ... @@ -6,7 +6,10 @@ import &#39;package:flutter/material.dart&#39;;
6 6 import 'package:flutter/services.dart';
7 7 import 'package:video_player/video_player.dart';
8 8 import 'package:wow_english/common/request/dao/video_dao.dart';
  9 +import 'package:wow_english/common/utils/video_playback_diagnostics.dart';
9 10 import 'package:wow_english/common/utils/video_progress_cache.dart';
  11 +import 'package:wow_english/common/utils/video_rendering_preference.dart';
  12 +import 'package:wow_english/utils/toast_util.dart';
10 13  
11 14 import 'video_zone_item.dart';
12 15  
... ... @@ -26,13 +29,20 @@ class VideoZonePlayerPage extends StatefulWidget {
26 29  
27 30 class _VideoZonePlayerPageState extends State<VideoZonePlayerPage>
28 31 with WidgetsBindingObserver {
  32 + static const _initializeTimeout = Duration(seconds: 10);
  33 + static const _playerCommandTimeout = Duration(seconds: 5);
  34 + static const _progressReadTimeout = Duration(seconds: 2);
  35 + static const _bufferingWarningDelay = Duration(seconds: 10);
  36 +
29 37 VideoPlayerController? _controller;
30 38 ChewieController? _chewieController;
31 39 Timer? _cacheTimer;
  40 + Timer? _bufferingTimer;
32 41 late int _currentIndex;
33 42 double _playbackSpeed = 1;
34 43 bool _switchingVideo = false;
35 44 bool _handlingCompletion = false;
  45 + bool _bufferingWarningShown = false;
36 46 bool? _lastIsPlaying;
37 47 int? _completedReportedVideoId;
38 48 Object? _loadError;
... ... @@ -56,6 +66,8 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
56 66 Future<void> _loadCurrentVideo() async {
57 67 final useCupertinoControls = defaultTargetPlatform == TargetPlatform.iOS;
58 68 _cacheTimer?.cancel();
  69 + _bufferingTimer?.cancel();
  70 + _bufferingWarningShown = false;
59 71 final oldController = _controller;
60 72 final oldChewieController = _chewieController;
61 73 if (oldController?.value.isInitialized == true) {
... ... @@ -63,25 +75,32 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
63 75 }
64 76 oldController?.removeListener(_onVideoChanged);
65 77 _lastIsPlaying = null;
66   - final controller =
67   - VideoPlayerController.networkUrl(Uri.parse(_currentItem.videoUrl));
  78 + final controller = VideoPlayerController.networkUrl(
  79 + Uri.parse(_currentItem.videoUrl),
  80 + viewType: VideoRenderingPreference.viewType,
  81 + );
68 82 _controller = controller;
69 83 _chewieController = null;
70 84 _loadError = null;
71 85 if (mounted) {
72 86 setState(() {});
73 87 }
74   - oldChewieController?.dispose();
75   - await oldController?.dispose();
76   -
  88 + var stage = '释放旧播放器';
77 89 try {
78   - await controller.initialize().timeout(const Duration(seconds: 20));
  90 + oldChewieController?.dispose();
  91 + await oldController?.dispose().timeout(_playerCommandTimeout);
  92 + stage = '初始化视频流';
  93 + await controller.initialize().timeout(_initializeTimeout);
79 94 if (!mounted || controller != _controller) {
80 95 await controller.dispose();
81 96 return;
82 97 }
83   - await controller.setLooping(false);
84   - await controller.setPlaybackSpeed(_playbackSpeed);
  98 + stage = '配置播放器';
  99 + await controller.setLooping(false).timeout(_playerCommandTimeout);
  100 + await controller
  101 + .setPlaybackSpeed(_playbackSpeed)
  102 + .timeout(_playerCommandTimeout);
  103 + stage = '读取本地播放进度';
85 104 final savedProgress = await _resumeProgressFor(_currentItem.id);
86 105 if (!mounted || controller != _controller) {
87 106 await controller.dispose();
... ... @@ -89,10 +108,25 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
89 108 }
90 109 if (savedProgress > 0 &&
91 110 savedProgress < controller.value.duration.inSeconds) {
92   - await controller.seekTo(Duration(seconds: savedProgress));
  111 + stage = '恢复播放进度';
  112 + try {
  113 + await controller
  114 + .seekTo(Duration(seconds: savedProgress))
  115 + .timeout(_playerCommandTimeout);
  116 + } on TimeoutException catch (error) {
  117 + debugPrint(
  118 + '恢复本地视频进度超时(videoId=${_currentItem.id}): $error',
  119 + );
  120 + _showVideoToast('恢复进度超时,已尝试从头播放');
  121 + } catch (error) {
  122 + debugPrint('恢复本地视频进度失败(videoId=${_currentItem.id}): $error');
  123 + _showVideoToast('恢复进度失败,已尝试从头播放');
  124 + }
93 125 }
  126 + stage = '创建播放界面';
94 127 _lastIsPlaying = controller.value.isPlaying;
95 128 controller.addListener(_onVideoChanged);
  129 + _updateBufferingWatch(controller);
96 130 _chewieController = ChewieController(
97 131 videoPlayerController: controller,
98 132 autoPlay: true,
... ... @@ -139,15 +173,42 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
139 173 );
140 174 setState(() {});
141 175 } catch (error) {
142   - if (mounted && controller == _controller) {
143   - setState(() => _loadError = error);
  176 + final failedCurrentController = controller == _controller;
  177 + if (failedCurrentController) {
  178 + controller.removeListener(_onVideoChanged);
  179 + _bufferingTimer?.cancel();
  180 + _bufferingTimer = null;
  181 + _cacheTimer?.cancel();
  182 + _cacheTimer = null;
  183 + _chewieController?.dispose();
  184 + _chewieController = null;
144 185 }
  186 + await VideoPlaybackDiagnostics.disposeController(
  187 + controller,
  188 + context: '视频专区初始化失败($stage)',
  189 + );
  190 + if (!mounted || !failedCurrentController) return;
  191 + _controller = null;
  192 + final message = _diagnosticMessage(stage, error);
  193 + debugPrint('$message (videoId=${_currentItem.id}): $error');
  194 + _showVideoToast('$message(视频ID: ${_currentItem.id})');
  195 + setState(() => _loadError = error);
145 196 }
146 197 }
147 198  
148 199 void _onVideoChanged() {
149 200 final controller = _controller;
150 201 if (!mounted || controller == null) return;
  202 + _updateBufferingWatch(controller);
  203 + if (controller.value.hasError && _loadError == null) {
  204 + final error = controller.value.errorDescription ?? '未知播放器错误';
  205 + debugPrint('视频播放失败(videoId=${_currentItem.id}): $error');
  206 + _showVideoToast(
  207 + '${_diagnosticMessage('播放/解码', error)}(视频ID: ${_currentItem.id})',
  208 + );
  209 + setState(() => _loadError = error);
  210 + return;
  211 + }
151 212 final isPlaying = controller.value.isPlaying;
152 213 if (_lastIsPlaying != null && _lastIsPlaying != isPlaying) {
153 214 unawaited(_cacheCurrentProgress());
... ... @@ -162,6 +223,30 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
162 223 }
163 224 }
164 225  
  226 + void _updateBufferingWatch(VideoPlayerController controller) {
  227 + if (!controller.value.isBuffering) {
  228 + _bufferingTimer?.cancel();
  229 + _bufferingTimer = null;
  230 + _bufferingWarningShown = false;
  231 + return;
  232 + }
  233 + if (_bufferingTimer != null || _bufferingWarningShown) return;
  234 + _bufferingTimer = Timer(_bufferingWarningDelay, () {
  235 + _bufferingTimer = null;
  236 + if (!mounted ||
  237 + controller != _controller ||
  238 + !controller.value.isBuffering) {
  239 + return;
  240 + }
  241 + _bufferingWarningShown = true;
  242 + final error = controller.value.errorDescription;
  243 + final reason =
  244 + error == null ? '网络缓冲或设备解码可能异常' : _diagnosticMessage('播放/解码', error);
  245 + debugPrint('视频持续缓冲(videoId=${_currentItem.id}): $reason ${error ?? ''}');
  246 + _showVideoToast('视频持续缓冲超过10秒:$reason(视频ID: ${_currentItem.id})');
  247 + });
  248 + }
  249 +
165 250 Future<void> _playNext({bool currentCompleted = false}) async {
166 251 if (_switchingVideo || _currentIndex >= widget.items.length - 1) return;
167 252 _switchingVideo = true;
... ... @@ -187,6 +272,7 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
187 272 SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky),
188 273 );
189 274 _cacheTimer?.cancel();
  275 + _bufferingTimer?.cancel();
190 276 _controller?.removeListener(_onVideoChanged);
191 277 unawaited(_reportCurrentProgress());
192 278 _chewieController?.dispose();
... ... @@ -237,14 +323,52 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
237 323  
238 324 Future<int> _resumeProgressFor(int videoId) async {
239 325 try {
240   - final cached = await VideoProgressCache.get(videoId);
  326 + final cached =
  327 + await VideoProgressCache.get(videoId).timeout(_progressReadTimeout);
241 328 if (cached != null) return cached.progressSeconds;
  329 + } on TimeoutException catch (error) {
  330 + debugPrint('读取本地视频进度超时(videoId=$videoId): $error');
  331 + _showVideoToast('本地进度读取超时,已从头播放');
242 332 } catch (error) {
243 333 debugPrint('读取本地视频播放进度失败: $error');
  334 + _showVideoToast('本地进度读取失败,已从头播放');
244 335 }
245 336 return 0;
246 337 }
247 338  
  339 + String _diagnosticMessage(String stage, Object error) {
  340 + final detail = error.toString().toLowerCase();
  341 + if (error is TimeoutException) {
  342 + if (stage == '初始化视频流') return '视频初始化超时,请检查网络或设备解码能力';
  343 + if (stage == '释放旧播放器') return '播放器释放超时,请退出后重试';
  344 + if (stage == '配置播放器') return '播放器配置超时,设备可能不支持当前播放参数';
  345 + if (stage == '恢复播放进度') return '恢复播放进度超时';
  346 + return '$stage超时';
  347 + }
  348 + if (detail.contains('decoder') ||
  349 + detail.contains('codec') ||
  350 + detail.contains('mediacodec') ||
  351 + detail.contains('acodec')) {
  352 + return '视频解码器启动失败,可能是设备兼容性问题';
  353 + }
  354 + if (detail.contains('http') ||
  355 + detail.contains('socket') ||
  356 + detail.contains('network') ||
  357 + detail.contains('connection') ||
  358 + detail.contains('source error') ||
  359 + detail.contains('sourceerror')) {
  360 + return '视频网络请求失败,请检查网络或视频地址';
  361 + }
  362 + if (stage == '初始化视频流') return '视频流初始化失败';
  363 + if (stage == '配置播放器') return '播放器配置失败';
  364 + if (stage == '播放/解码') return '视频播放/解码失败,请检查网络或设备兼容性';
  365 + return '$stage失败';
  366 + }
  367 +
  368 + void _showVideoToast(String message) {
  369 + showToast(message, duration: const Duration(seconds: 4));
  370 + }
  371 +
248 372 Future<void> _cacheCurrentProgress() async {
249 373 final controller = _controller;
250 374 if (controller == null || !controller.value.isInitialized) return;
... ...
packages/chivox_aiengine/android/build.gradle
... ... @@ -22,6 +22,7 @@ rootProject.allprojects {
22 22 apply plugin: 'com.android.library'
23 23  
24 24 android {
  25 + namespace 'com.example.chivox_aiengine'
25 26 compileSdkVersion 31
26 27  
27 28 compileOptions {
... ...
packages/video_player_android/LICENSE 0 → 100644
  1 +Copyright 2013 The Flutter Authors. All rights reserved.
  2 +
  3 +Redistribution and use in source and binary forms, with or without modification,
  4 +are permitted provided that the following conditions are met:
  5 +
  6 + * Redistributions of source code must retain the above copyright
  7 + notice, this list of conditions and the following disclaimer.
  8 + * Redistributions in binary form must reproduce the above
  9 + copyright notice, this list of conditions and the following
  10 + disclaimer in the documentation and/or other materials provided
  11 + with the distribution.
  12 + * Neither the name of Google Inc. nor the names of its
  13 + contributors may be used to endorse or promote products derived
  14 + from this software without specific prior written permission.
  15 +
  16 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  17 +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18 +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
  20 +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21 +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22 +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  23 +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24 +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25 +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
... ...
packages/video_player_android/android/build.gradle 0 → 100644
  1 +group 'io.flutter.plugins.videoplayer'
  2 +version '1.0-SNAPSHOT'
  3 +
  4 +buildscript {
  5 + repositories {
  6 + google()
  7 + mavenCentral()
  8 + }
  9 +
  10 + dependencies {
  11 + classpath 'com.android.tools.build:gradle:8.5.0'
  12 + }
  13 +}
  14 +
  15 +rootProject.allprojects {
  16 + repositories {
  17 + google()
  18 + mavenCentral()
  19 + }
  20 +}
  21 +
  22 +apply plugin: 'com.android.library'
  23 +
  24 +android {
  25 + namespace 'io.flutter.plugins.videoplayer'
  26 + compileSdk = flutter.compileSdkVersion
  27 +
  28 + defaultConfig {
  29 + minSdkVersion 21
  30 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  31 + }
  32 + lintOptions {
  33 + checkAllWarnings true
  34 + warningsAsErrors true
  35 + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency', 'NewerVersionAvailable'
  36 + }
  37 + compileOptions {
  38 + sourceCompatibility JavaVersion.VERSION_11
  39 + targetCompatibility JavaVersion.VERSION_11
  40 + }
  41 +
  42 + dependencies {
  43 + def exoplayer_version = "1.4.1"
  44 + implementation "androidx.media3:media3-exoplayer:${exoplayer_version}"
  45 + implementation "androidx.media3:media3-exoplayer-hls:${exoplayer_version}"
  46 + implementation "androidx.media3:media3-exoplayer-dash:${exoplayer_version}"
  47 + implementation "androidx.media3:media3-exoplayer-rtsp:${exoplayer_version}"
  48 + implementation "androidx.media3:media3-exoplayer-smoothstreaming:${exoplayer_version}"
  49 + testImplementation 'junit:junit:4.13.2'
  50 + testImplementation 'androidx.test:core:1.6.1'
  51 + testImplementation 'org.mockito:mockito-inline:5.2.0'
  52 + testImplementation 'org.robolectric:robolectric:4.14.1'
  53 + testImplementation "androidx.media3:media3-test-utils:${exoplayer_version}"
  54 + }
  55 +
  56 + testOptions {
  57 + unitTests.includeAndroidResources = true
  58 + unitTests.returnDefaultValues = true
  59 + unitTests.all {
  60 + // The org.gradle.jvmargs property that may be set in gradle.properties does not impact
  61 + // the Java heap size when running the Android unit tests. The following property here
  62 + // sets the heap size to a size large enough to run the robolectric tests across
  63 + // multiple SDK levels.
  64 + jvmArgs "-Xmx4G"
  65 + testLogging {
  66 + events "passed", "skipped", "failed", "standardOut", "standardError"
  67 + outputs.upToDateWhen {false}
  68 + showStandardStreams = true
  69 + }
  70 + }
  71 + }
  72 +}
... ...
packages/video_player_android/android/settings.gradle 0 → 100644
  1 +rootProject.name = 'video_player_android'
... ...
packages/video_player_android/android/src/main/AndroidManifest.xml 0 → 100644
  1 +<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  2 + package="io.flutter.plugins.videoplayer">
  3 +</manifest>
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import androidx.annotation.NonNull;
  8 +import androidx.media3.common.PlaybackException;
  9 +import androidx.media3.common.Player;
  10 +import androidx.media3.exoplayer.ExoPlayer;
  11 +
  12 +public abstract class ExoPlayerEventListener implements Player.Listener {
  13 + private boolean isBuffering = false;
  14 + private boolean isInitialized;
  15 + protected final ExoPlayer exoPlayer;
  16 + protected final VideoPlayerCallbacks events;
  17 +
  18 + protected enum RotationDegrees {
  19 + ROTATE_0(0),
  20 + ROTATE_90(90),
  21 + ROTATE_180(180),
  22 + ROTATE_270(270);
  23 +
  24 + private final int degrees;
  25 +
  26 + RotationDegrees(int degrees) {
  27 + this.degrees = degrees;
  28 + }
  29 +
  30 + public static RotationDegrees fromDegrees(int degrees) {
  31 + for (RotationDegrees rotationDegrees : RotationDegrees.values()) {
  32 + if (rotationDegrees.degrees == degrees) {
  33 + return rotationDegrees;
  34 + }
  35 + }
  36 + throw new IllegalArgumentException("Invalid rotation degrees specified: " + degrees);
  37 + }
  38 +
  39 + public int getDegrees() {
  40 + return this.degrees;
  41 + }
  42 + }
  43 +
  44 + public ExoPlayerEventListener(
  45 + @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events, boolean initialized) {
  46 + this.exoPlayer = exoPlayer;
  47 + this.events = events;
  48 + this.isInitialized = initialized;
  49 + }
  50 +
  51 + private void setBuffering(boolean buffering) {
  52 + if (isBuffering == buffering) {
  53 + return;
  54 + }
  55 + isBuffering = buffering;
  56 + if (buffering) {
  57 + events.onBufferingStart();
  58 + } else {
  59 + events.onBufferingEnd();
  60 + }
  61 + }
  62 +
  63 + protected abstract void sendInitialized();
  64 +
  65 + @Override
  66 + public void onPlaybackStateChanged(final int playbackState) {
  67 + switch (playbackState) {
  68 + case Player.STATE_BUFFERING:
  69 + setBuffering(true);
  70 + events.onBufferingUpdate(exoPlayer.getBufferedPosition());
  71 + break;
  72 + case Player.STATE_READY:
  73 + if (isInitialized) {
  74 + return;
  75 + }
  76 + isInitialized = true;
  77 + sendInitialized();
  78 + break;
  79 + case Player.STATE_ENDED:
  80 + events.onCompleted();
  81 + break;
  82 + case Player.STATE_IDLE:
  83 + break;
  84 + }
  85 + if (playbackState != Player.STATE_BUFFERING) {
  86 + setBuffering(false);
  87 + }
  88 + }
  89 +
  90 + @Override
  91 + public void onPlayerError(@NonNull final PlaybackException error) {
  92 + setBuffering(false);
  93 + if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
  94 + // See
  95 + // https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window
  96 + exoPlayer.seekToDefaultPosition();
  97 + exoPlayer.prepare();
  98 + } else {
  99 + events.onError("VideoError", "Video player had error " + error, null);
  100 + }
  101 + }
  102 +
  103 + @Override
  104 + public void onIsPlayingChanged(boolean isPlaying) {
  105 + events.onIsPlayingStateUpdate(isPlaying);
  106 + }
  107 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerState.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import androidx.annotation.NonNull;
  8 +import androidx.media3.common.PlaybackParameters;
  9 +import androidx.media3.exoplayer.ExoPlayer;
  10 +
  11 +/**
  12 + * Internal state representing an {@link ExoPlayer} instance at a snapshot in time.
  13 + *
  14 + * <p>During the Android application lifecycle, the underlying {@link android.view.Surface} being
  15 + * rendered to by the player can be destroyed when the application is in the background and memory
  16 + * is reclaimed. Upon <em>resume</em>, the player will need to be recreated, but start again at the
  17 + * previous point (and settings).
  18 + */
  19 +public final class ExoPlayerState {
  20 + /**
  21 + * Saves a representation of the current state of the player at the current point in time.
  22 + *
  23 + * <p>The inverse of this operation is {@link #restore(ExoPlayer)}.
  24 + *
  25 + * @param exoPlayer the active player instance.
  26 + * @return an opaque object representing the state.
  27 + */
  28 + @NonNull
  29 + public static ExoPlayerState save(@NonNull ExoPlayer exoPlayer) {
  30 + return new ExoPlayerState(
  31 + /* position= */ exoPlayer.getCurrentPosition(),
  32 + /* repeatMode= */ exoPlayer.getRepeatMode(),
  33 + /* volume= */ exoPlayer.getVolume(),
  34 + /* playbackParameters= */ exoPlayer.getPlaybackParameters());
  35 + }
  36 +
  37 + private ExoPlayerState(
  38 + long position, int repeatMode, float volume, PlaybackParameters playbackParameters) {
  39 + this.position = position;
  40 + this.repeatMode = repeatMode;
  41 + this.volume = volume;
  42 + this.playbackParameters = playbackParameters;
  43 + }
  44 +
  45 + /** Previous value of {@link ExoPlayer#getCurrentPosition()}. */
  46 + private final long position;
  47 +
  48 + /** Previous value of {@link ExoPlayer#getRepeatMode()}. */
  49 + private final int repeatMode;
  50 +
  51 + /** Previous value of {@link ExoPlayer#getVolume()}. */
  52 + private final float volume;
  53 +
  54 + /** Previous value of {@link ExoPlayer#getPlaybackParameters()}. */
  55 + private final PlaybackParameters playbackParameters;
  56 +
  57 + /**
  58 + * Restores the captured state onto the provided player.
  59 + *
  60 + * <p>This will typically be done after creating a new player, setting up a media source, and
  61 + * listening to events.
  62 + *
  63 + * @param exoPlayer the new player instance to reflect the state back to.
  64 + */
  65 + public void restore(@NonNull ExoPlayer exoPlayer) {
  66 + exoPlayer.seekTo(position);
  67 + exoPlayer.setRepeatMode(repeatMode);
  68 + exoPlayer.setVolume(volume);
  69 + exoPlayer.setPlaybackParameters(playbackParameters);
  70 + }
  71 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.Nullable;
  10 +import androidx.annotation.OptIn;
  11 +import androidx.annotation.VisibleForTesting;
  12 +import androidx.media3.common.MediaItem;
  13 +import androidx.media3.common.MimeTypes;
  14 +import androidx.media3.common.util.UnstableApi;
  15 +import androidx.media3.datasource.DataSource;
  16 +import androidx.media3.datasource.DefaultDataSource;
  17 +import androidx.media3.datasource.DefaultHttpDataSource;
  18 +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
  19 +import androidx.media3.exoplayer.source.MediaSource;
  20 +import java.util.Map;
  21 +
  22 +final class HttpVideoAsset extends VideoAsset {
  23 + private static final String DEFAULT_USER_AGENT = "ExoPlayer";
  24 + private static final String HEADER_USER_AGENT = "User-Agent";
  25 +
  26 + @NonNull private final StreamingFormat streamingFormat;
  27 + @NonNull private final Map<String, String> httpHeaders;
  28 +
  29 + HttpVideoAsset(
  30 + @Nullable String assetUrl,
  31 + @NonNull StreamingFormat streamingFormat,
  32 + @NonNull Map<String, String> httpHeaders) {
  33 + super(assetUrl);
  34 + this.streamingFormat = streamingFormat;
  35 + this.httpHeaders = httpHeaders;
  36 + }
  37 +
  38 + @NonNull
  39 + @Override
  40 + public MediaItem getMediaItem() {
  41 + MediaItem.Builder builder = new MediaItem.Builder().setUri(assetUrl);
  42 + String mimeType = null;
  43 + switch (streamingFormat) {
  44 + case SMOOTH:
  45 + mimeType = MimeTypes.APPLICATION_SS;
  46 + break;
  47 + case DYNAMIC_ADAPTIVE:
  48 + mimeType = MimeTypes.APPLICATION_MPD;
  49 + break;
  50 + case HTTP_LIVE:
  51 + mimeType = MimeTypes.APPLICATION_M3U8;
  52 + break;
  53 + }
  54 + if (mimeType != null) {
  55 + builder.setMimeType(mimeType);
  56 + }
  57 + return builder.build();
  58 + }
  59 +
  60 + @NonNull
  61 + @Override
  62 + public MediaSource.Factory getMediaSourceFactory(@NonNull Context context) {
  63 + return getMediaSourceFactory(context, new DefaultHttpDataSource.Factory());
  64 + }
  65 +
  66 + /**
  67 + * Returns a configured media source factory, starting at the provided factory.
  68 + *
  69 + * <p>This method is provided for ease of testing without making real HTTP calls.
  70 + *
  71 + * @param context application context.
  72 + * @param initialFactory initial factory, to be configured.
  73 + * @return configured factory, or {@code null} if not needed for this asset type.
  74 + */
  75 + @VisibleForTesting
  76 + MediaSource.Factory getMediaSourceFactory(
  77 + Context context, DefaultHttpDataSource.Factory initialFactory) {
  78 + String userAgent = DEFAULT_USER_AGENT;
  79 + if (!httpHeaders.isEmpty() && httpHeaders.containsKey(HEADER_USER_AGENT)) {
  80 + userAgent = httpHeaders.get(HEADER_USER_AGENT);
  81 + }
  82 + unstableUpdateDataSourceFactory(initialFactory, httpHeaders, userAgent);
  83 + DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context, initialFactory);
  84 + return new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory);
  85 + }
  86 +
  87 + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
  88 + @OptIn(markerClass = UnstableApi.class)
  89 + private static void unstableUpdateDataSourceFactory(
  90 + @NonNull DefaultHttpDataSource.Factory factory,
  91 + @NonNull Map<String, String> httpHeaders,
  92 + @Nullable String userAgent) {
  93 + factory.setUserAgent(userAgent).setAllowCrossProtocolRedirects(true);
  94 + if (!httpHeaders.isEmpty()) {
  95 + factory.setDefaultRequestProperties(httpHeaders);
  96 + }
  97 + }
  98 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/LocalVideoAsset.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.media3.common.MediaItem;
  10 +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
  11 +import androidx.media3.exoplayer.source.MediaSource;
  12 +
  13 +final class LocalVideoAsset extends VideoAsset {
  14 + LocalVideoAsset(@NonNull String assetUrl) {
  15 + super(assetUrl);
  16 + }
  17 +
  18 + @NonNull
  19 + @Override
  20 + public MediaItem getMediaItem() {
  21 + return new MediaItem.Builder().setUri(assetUrl).build();
  22 + }
  23 +
  24 + @NonNull
  25 + @Override
  26 + public MediaSource.Factory getMediaSourceFactory(@NonNull Context context) {
  27 + return new DefaultMediaSourceFactory(context);
  28 + }
  29 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +// Autogenerated from Pigeon (v22.6.1), do not edit directly.
  5 +// See also: https://pub.dev/packages/pigeon
  6 +
  7 +package io.flutter.plugins.videoplayer;
  8 +
  9 +import static java.lang.annotation.ElementType.METHOD;
  10 +import static java.lang.annotation.RetentionPolicy.CLASS;
  11 +
  12 +import android.util.Log;
  13 +import androidx.annotation.NonNull;
  14 +import androidx.annotation.Nullable;
  15 +import io.flutter.plugin.common.BasicMessageChannel;
  16 +import io.flutter.plugin.common.BinaryMessenger;
  17 +import io.flutter.plugin.common.MessageCodec;
  18 +import io.flutter.plugin.common.StandardMessageCodec;
  19 +import java.io.ByteArrayOutputStream;
  20 +import java.lang.annotation.Retention;
  21 +import java.lang.annotation.Target;
  22 +import java.nio.ByteBuffer;
  23 +import java.util.ArrayList;
  24 +import java.util.Map;
  25 +import java.util.Objects;
  26 +
  27 +/** Generated class from Pigeon. */
  28 +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})
  29 +public class Messages {
  30 +
  31 + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */
  32 + public static class FlutterError extends RuntimeException {
  33 +
  34 + /** The error code. */
  35 + public final String code;
  36 +
  37 + /** The error details. Must be a datatype supported by the api codec. */
  38 + public final Object details;
  39 +
  40 + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) {
  41 + super(message);
  42 + this.code = code;
  43 + this.details = details;
  44 + }
  45 + }
  46 +
  47 + @NonNull
  48 + protected static ArrayList<Object> wrapError(@NonNull Throwable exception) {
  49 + ArrayList<Object> errorList = new ArrayList<>(3);
  50 + if (exception instanceof FlutterError) {
  51 + FlutterError error = (FlutterError) exception;
  52 + errorList.add(error.code);
  53 + errorList.add(error.getMessage());
  54 + errorList.add(error.details);
  55 + } else {
  56 + errorList.add(exception.toString());
  57 + errorList.add(exception.getClass().getSimpleName());
  58 + errorList.add(
  59 + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
  60 + }
  61 + return errorList;
  62 + }
  63 +
  64 + @Target(METHOD)
  65 + @Retention(CLASS)
  66 + @interface CanIgnoreReturnValue {}
  67 +
  68 + /** Pigeon equivalent of VideoViewType. */
  69 + public enum PlatformVideoViewType {
  70 + TEXTURE_VIEW(0),
  71 + PLATFORM_VIEW(1);
  72 +
  73 + final int index;
  74 +
  75 + PlatformVideoViewType(final int index) {
  76 + this.index = index;
  77 + }
  78 + }
  79 +
  80 + /**
  81 + * Information passed to the platform view creation.
  82 + *
  83 + * <p>Generated class from Pigeon that represents data sent in messages.
  84 + */
  85 + public static final class PlatformVideoViewCreationParams {
  86 + private @NonNull Long playerId;
  87 +
  88 + public @NonNull Long getPlayerId() {
  89 + return playerId;
  90 + }
  91 +
  92 + public void setPlayerId(@NonNull Long setterArg) {
  93 + if (setterArg == null) {
  94 + throw new IllegalStateException("Nonnull field \"playerId\" is null.");
  95 + }
  96 + this.playerId = setterArg;
  97 + }
  98 +
  99 + /** Constructor is non-public to enforce null safety; use Builder. */
  100 + PlatformVideoViewCreationParams() {}
  101 +
  102 + @Override
  103 + public boolean equals(Object o) {
  104 + if (this == o) {
  105 + return true;
  106 + }
  107 + if (o == null || getClass() != o.getClass()) {
  108 + return false;
  109 + }
  110 + PlatformVideoViewCreationParams that = (PlatformVideoViewCreationParams) o;
  111 + return playerId.equals(that.playerId);
  112 + }
  113 +
  114 + @Override
  115 + public int hashCode() {
  116 + return Objects.hash(playerId);
  117 + }
  118 +
  119 + public static final class Builder {
  120 +
  121 + private @Nullable Long playerId;
  122 +
  123 + @CanIgnoreReturnValue
  124 + public @NonNull Builder setPlayerId(@NonNull Long setterArg) {
  125 + this.playerId = setterArg;
  126 + return this;
  127 + }
  128 +
  129 + public @NonNull PlatformVideoViewCreationParams build() {
  130 + PlatformVideoViewCreationParams pigeonReturn = new PlatformVideoViewCreationParams();
  131 + pigeonReturn.setPlayerId(playerId);
  132 + return pigeonReturn;
  133 + }
  134 + }
  135 +
  136 + @NonNull
  137 + ArrayList<Object> toList() {
  138 + ArrayList<Object> toListResult = new ArrayList<>(1);
  139 + toListResult.add(playerId);
  140 + return toListResult;
  141 + }
  142 +
  143 + static @NonNull PlatformVideoViewCreationParams fromList(
  144 + @NonNull ArrayList<Object> pigeonVar_list) {
  145 + PlatformVideoViewCreationParams pigeonResult = new PlatformVideoViewCreationParams();
  146 + Object playerId = pigeonVar_list.get(0);
  147 + pigeonResult.setPlayerId((Long) playerId);
  148 + return pigeonResult;
  149 + }
  150 + }
  151 +
  152 + /** Generated class from Pigeon that represents data sent in messages. */
  153 + public static final class CreateMessage {
  154 + private @Nullable String asset;
  155 +
  156 + public @Nullable String getAsset() {
  157 + return asset;
  158 + }
  159 +
  160 + public void setAsset(@Nullable String setterArg) {
  161 + this.asset = setterArg;
  162 + }
  163 +
  164 + private @Nullable String uri;
  165 +
  166 + public @Nullable String getUri() {
  167 + return uri;
  168 + }
  169 +
  170 + public void setUri(@Nullable String setterArg) {
  171 + this.uri = setterArg;
  172 + }
  173 +
  174 + private @Nullable String packageName;
  175 +
  176 + public @Nullable String getPackageName() {
  177 + return packageName;
  178 + }
  179 +
  180 + public void setPackageName(@Nullable String setterArg) {
  181 + this.packageName = setterArg;
  182 + }
  183 +
  184 + private @Nullable String formatHint;
  185 +
  186 + public @Nullable String getFormatHint() {
  187 + return formatHint;
  188 + }
  189 +
  190 + public void setFormatHint(@Nullable String setterArg) {
  191 + this.formatHint = setterArg;
  192 + }
  193 +
  194 + private @NonNull Map<String, String> httpHeaders;
  195 +
  196 + public @NonNull Map<String, String> getHttpHeaders() {
  197 + return httpHeaders;
  198 + }
  199 +
  200 + public void setHttpHeaders(@NonNull Map<String, String> setterArg) {
  201 + if (setterArg == null) {
  202 + throw new IllegalStateException("Nonnull field \"httpHeaders\" is null.");
  203 + }
  204 + this.httpHeaders = setterArg;
  205 + }
  206 +
  207 + private @Nullable PlatformVideoViewType viewType;
  208 +
  209 + public @Nullable PlatformVideoViewType getViewType() {
  210 + return viewType;
  211 + }
  212 +
  213 + public void setViewType(@Nullable PlatformVideoViewType setterArg) {
  214 + this.viewType = setterArg;
  215 + }
  216 +
  217 + /** Constructor is non-public to enforce null safety; use Builder. */
  218 + CreateMessage() {}
  219 +
  220 + @Override
  221 + public boolean equals(Object o) {
  222 + if (this == o) {
  223 + return true;
  224 + }
  225 + if (o == null || getClass() != o.getClass()) {
  226 + return false;
  227 + }
  228 + CreateMessage that = (CreateMessage) o;
  229 + return Objects.equals(asset, that.asset)
  230 + && Objects.equals(uri, that.uri)
  231 + && Objects.equals(packageName, that.packageName)
  232 + && Objects.equals(formatHint, that.formatHint)
  233 + && httpHeaders.equals(that.httpHeaders)
  234 + && Objects.equals(viewType, that.viewType);
  235 + }
  236 +
  237 + @Override
  238 + public int hashCode() {
  239 + return Objects.hash(asset, uri, packageName, formatHint, httpHeaders, viewType);
  240 + }
  241 +
  242 + public static final class Builder {
  243 +
  244 + private @Nullable String asset;
  245 +
  246 + @CanIgnoreReturnValue
  247 + public @NonNull Builder setAsset(@Nullable String setterArg) {
  248 + this.asset = setterArg;
  249 + return this;
  250 + }
  251 +
  252 + private @Nullable String uri;
  253 +
  254 + @CanIgnoreReturnValue
  255 + public @NonNull Builder setUri(@Nullable String setterArg) {
  256 + this.uri = setterArg;
  257 + return this;
  258 + }
  259 +
  260 + private @Nullable String packageName;
  261 +
  262 + @CanIgnoreReturnValue
  263 + public @NonNull Builder setPackageName(@Nullable String setterArg) {
  264 + this.packageName = setterArg;
  265 + return this;
  266 + }
  267 +
  268 + private @Nullable String formatHint;
  269 +
  270 + @CanIgnoreReturnValue
  271 + public @NonNull Builder setFormatHint(@Nullable String setterArg) {
  272 + this.formatHint = setterArg;
  273 + return this;
  274 + }
  275 +
  276 + private @Nullable Map<String, String> httpHeaders;
  277 +
  278 + @CanIgnoreReturnValue
  279 + public @NonNull Builder setHttpHeaders(@NonNull Map<String, String> setterArg) {
  280 + this.httpHeaders = setterArg;
  281 + return this;
  282 + }
  283 +
  284 + private @Nullable PlatformVideoViewType viewType;
  285 +
  286 + @CanIgnoreReturnValue
  287 + public @NonNull Builder setViewType(@Nullable PlatformVideoViewType setterArg) {
  288 + this.viewType = setterArg;
  289 + return this;
  290 + }
  291 +
  292 + public @NonNull CreateMessage build() {
  293 + CreateMessage pigeonReturn = new CreateMessage();
  294 + pigeonReturn.setAsset(asset);
  295 + pigeonReturn.setUri(uri);
  296 + pigeonReturn.setPackageName(packageName);
  297 + pigeonReturn.setFormatHint(formatHint);
  298 + pigeonReturn.setHttpHeaders(httpHeaders);
  299 + pigeonReturn.setViewType(viewType);
  300 + return pigeonReturn;
  301 + }
  302 + }
  303 +
  304 + @NonNull
  305 + ArrayList<Object> toList() {
  306 + ArrayList<Object> toListResult = new ArrayList<>(6);
  307 + toListResult.add(asset);
  308 + toListResult.add(uri);
  309 + toListResult.add(packageName);
  310 + toListResult.add(formatHint);
  311 + toListResult.add(httpHeaders);
  312 + toListResult.add(viewType);
  313 + return toListResult;
  314 + }
  315 +
  316 + static @NonNull CreateMessage fromList(@NonNull ArrayList<Object> pigeonVar_list) {
  317 + CreateMessage pigeonResult = new CreateMessage();
  318 + Object asset = pigeonVar_list.get(0);
  319 + pigeonResult.setAsset((String) asset);
  320 + Object uri = pigeonVar_list.get(1);
  321 + pigeonResult.setUri((String) uri);
  322 + Object packageName = pigeonVar_list.get(2);
  323 + pigeonResult.setPackageName((String) packageName);
  324 + Object formatHint = pigeonVar_list.get(3);
  325 + pigeonResult.setFormatHint((String) formatHint);
  326 + Object httpHeaders = pigeonVar_list.get(4);
  327 + pigeonResult.setHttpHeaders((Map<String, String>) httpHeaders);
  328 + Object viewType = pigeonVar_list.get(5);
  329 + pigeonResult.setViewType((PlatformVideoViewType) viewType);
  330 + return pigeonResult;
  331 + }
  332 + }
  333 +
  334 + private static class PigeonCodec extends StandardMessageCodec {
  335 + public static final PigeonCodec INSTANCE = new PigeonCodec();
  336 +
  337 + private PigeonCodec() {}
  338 +
  339 + @Override
  340 + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
  341 + switch (type) {
  342 + case (byte) 129:
  343 + {
  344 + Object value = readValue(buffer);
  345 + return value == null ? null : PlatformVideoViewType.values()[((Long) value).intValue()];
  346 + }
  347 + case (byte) 130:
  348 + return PlatformVideoViewCreationParams.fromList((ArrayList<Object>) readValue(buffer));
  349 + case (byte) 131:
  350 + return CreateMessage.fromList((ArrayList<Object>) readValue(buffer));
  351 + default:
  352 + return super.readValueOfType(type, buffer);
  353 + }
  354 + }
  355 +
  356 + @Override
  357 + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
  358 + if (value instanceof PlatformVideoViewType) {
  359 + stream.write(129);
  360 + writeValue(stream, value == null ? null : ((PlatformVideoViewType) value).index);
  361 + } else if (value instanceof PlatformVideoViewCreationParams) {
  362 + stream.write(130);
  363 + writeValue(stream, ((PlatformVideoViewCreationParams) value).toList());
  364 + } else if (value instanceof CreateMessage) {
  365 + stream.write(131);
  366 + writeValue(stream, ((CreateMessage) value).toList());
  367 + } else {
  368 + super.writeValue(stream, value);
  369 + }
  370 + }
  371 + }
  372 +
  373 + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
  374 + public interface AndroidVideoPlayerApi {
  375 +
  376 + void initialize();
  377 +
  378 + @NonNull
  379 + Long create(@NonNull CreateMessage msg);
  380 +
  381 + void dispose(@NonNull Long playerId);
  382 +
  383 + void setLooping(@NonNull Long playerId, @NonNull Boolean looping);
  384 +
  385 + void setVolume(@NonNull Long playerId, @NonNull Double volume);
  386 +
  387 + void setPlaybackSpeed(@NonNull Long playerId, @NonNull Double speed);
  388 +
  389 + void play(@NonNull Long playerId);
  390 +
  391 + @NonNull
  392 + Long position(@NonNull Long playerId);
  393 +
  394 + void seekTo(@NonNull Long playerId, @NonNull Long position);
  395 +
  396 + void pause(@NonNull Long playerId);
  397 +
  398 + void setMixWithOthers(@NonNull Boolean mixWithOthers);
  399 +
  400 + /** The codec used by AndroidVideoPlayerApi. */
  401 + static @NonNull MessageCodec<Object> getCodec() {
  402 + return PigeonCodec.INSTANCE;
  403 + }
  404 +
  405 + /**
  406 + * Sets up an instance of `AndroidVideoPlayerApi` to handle messages through the
  407 + * `binaryMessenger`.
  408 + */
  409 + static void setUp(
  410 + @NonNull BinaryMessenger binaryMessenger, @Nullable AndroidVideoPlayerApi api) {
  411 + setUp(binaryMessenger, "", api);
  412 + }
  413 +
  414 + static void setUp(
  415 + @NonNull BinaryMessenger binaryMessenger,
  416 + @NonNull String messageChannelSuffix,
  417 + @Nullable AndroidVideoPlayerApi api) {
  418 + messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix;
  419 + {
  420 + BasicMessageChannel<Object> channel =
  421 + new BasicMessageChannel<>(
  422 + binaryMessenger,
  423 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize"
  424 + + messageChannelSuffix,
  425 + getCodec());
  426 + if (api != null) {
  427 + channel.setMessageHandler(
  428 + (message, reply) -> {
  429 + ArrayList<Object> wrapped = new ArrayList<>();
  430 + try {
  431 + api.initialize();
  432 + wrapped.add(0, null);
  433 + } catch (Throwable exception) {
  434 + wrapped = wrapError(exception);
  435 + }
  436 + reply.reply(wrapped);
  437 + });
  438 + } else {
  439 + channel.setMessageHandler(null);
  440 + }
  441 + }
  442 + {
  443 + BasicMessageChannel<Object> channel =
  444 + new BasicMessageChannel<>(
  445 + binaryMessenger,
  446 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.create"
  447 + + messageChannelSuffix,
  448 + getCodec());
  449 + if (api != null) {
  450 + channel.setMessageHandler(
  451 + (message, reply) -> {
  452 + ArrayList<Object> wrapped = new ArrayList<>();
  453 + ArrayList<Object> args = (ArrayList<Object>) message;
  454 + CreateMessage msgArg = (CreateMessage) args.get(0);
  455 + try {
  456 + Long output = api.create(msgArg);
  457 + wrapped.add(0, output);
  458 + } catch (Throwable exception) {
  459 + wrapped = wrapError(exception);
  460 + }
  461 + reply.reply(wrapped);
  462 + });
  463 + } else {
  464 + channel.setMessageHandler(null);
  465 + }
  466 + }
  467 + {
  468 + BasicMessageChannel<Object> channel =
  469 + new BasicMessageChannel<>(
  470 + binaryMessenger,
  471 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose"
  472 + + messageChannelSuffix,
  473 + getCodec());
  474 + if (api != null) {
  475 + channel.setMessageHandler(
  476 + (message, reply) -> {
  477 + ArrayList<Object> wrapped = new ArrayList<>();
  478 + ArrayList<Object> args = (ArrayList<Object>) message;
  479 + Long playerIdArg = (Long) args.get(0);
  480 + try {
  481 + api.dispose(playerIdArg);
  482 + wrapped.add(0, null);
  483 + } catch (Throwable exception) {
  484 + wrapped = wrapError(exception);
  485 + }
  486 + reply.reply(wrapped);
  487 + });
  488 + } else {
  489 + channel.setMessageHandler(null);
  490 + }
  491 + }
  492 + {
  493 + BasicMessageChannel<Object> channel =
  494 + new BasicMessageChannel<>(
  495 + binaryMessenger,
  496 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setLooping"
  497 + + messageChannelSuffix,
  498 + getCodec());
  499 + if (api != null) {
  500 + channel.setMessageHandler(
  501 + (message, reply) -> {
  502 + ArrayList<Object> wrapped = new ArrayList<>();
  503 + ArrayList<Object> args = (ArrayList<Object>) message;
  504 + Long playerIdArg = (Long) args.get(0);
  505 + Boolean loopingArg = (Boolean) args.get(1);
  506 + try {
  507 + api.setLooping(playerIdArg, loopingArg);
  508 + wrapped.add(0, null);
  509 + } catch (Throwable exception) {
  510 + wrapped = wrapError(exception);
  511 + }
  512 + reply.reply(wrapped);
  513 + });
  514 + } else {
  515 + channel.setMessageHandler(null);
  516 + }
  517 + }
  518 + {
  519 + BasicMessageChannel<Object> channel =
  520 + new BasicMessageChannel<>(
  521 + binaryMessenger,
  522 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setVolume"
  523 + + messageChannelSuffix,
  524 + getCodec());
  525 + if (api != null) {
  526 + channel.setMessageHandler(
  527 + (message, reply) -> {
  528 + ArrayList<Object> wrapped = new ArrayList<>();
  529 + ArrayList<Object> args = (ArrayList<Object>) message;
  530 + Long playerIdArg = (Long) args.get(0);
  531 + Double volumeArg = (Double) args.get(1);
  532 + try {
  533 + api.setVolume(playerIdArg, volumeArg);
  534 + wrapped.add(0, null);
  535 + } catch (Throwable exception) {
  536 + wrapped = wrapError(exception);
  537 + }
  538 + reply.reply(wrapped);
  539 + });
  540 + } else {
  541 + channel.setMessageHandler(null);
  542 + }
  543 + }
  544 + {
  545 + BasicMessageChannel<Object> channel =
  546 + new BasicMessageChannel<>(
  547 + binaryMessenger,
  548 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setPlaybackSpeed"
  549 + + messageChannelSuffix,
  550 + getCodec());
  551 + if (api != null) {
  552 + channel.setMessageHandler(
  553 + (message, reply) -> {
  554 + ArrayList<Object> wrapped = new ArrayList<>();
  555 + ArrayList<Object> args = (ArrayList<Object>) message;
  556 + Long playerIdArg = (Long) args.get(0);
  557 + Double speedArg = (Double) args.get(1);
  558 + try {
  559 + api.setPlaybackSpeed(playerIdArg, speedArg);
  560 + wrapped.add(0, null);
  561 + } catch (Throwable exception) {
  562 + wrapped = wrapError(exception);
  563 + }
  564 + reply.reply(wrapped);
  565 + });
  566 + } else {
  567 + channel.setMessageHandler(null);
  568 + }
  569 + }
  570 + {
  571 + BasicMessageChannel<Object> channel =
  572 + new BasicMessageChannel<>(
  573 + binaryMessenger,
  574 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.play"
  575 + + messageChannelSuffix,
  576 + getCodec());
  577 + if (api != null) {
  578 + channel.setMessageHandler(
  579 + (message, reply) -> {
  580 + ArrayList<Object> wrapped = new ArrayList<>();
  581 + ArrayList<Object> args = (ArrayList<Object>) message;
  582 + Long playerIdArg = (Long) args.get(0);
  583 + try {
  584 + api.play(playerIdArg);
  585 + wrapped.add(0, null);
  586 + } catch (Throwable exception) {
  587 + wrapped = wrapError(exception);
  588 + }
  589 + reply.reply(wrapped);
  590 + });
  591 + } else {
  592 + channel.setMessageHandler(null);
  593 + }
  594 + }
  595 + {
  596 + BasicMessageChannel<Object> channel =
  597 + new BasicMessageChannel<>(
  598 + binaryMessenger,
  599 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.position"
  600 + + messageChannelSuffix,
  601 + getCodec());
  602 + if (api != null) {
  603 + channel.setMessageHandler(
  604 + (message, reply) -> {
  605 + ArrayList<Object> wrapped = new ArrayList<>();
  606 + ArrayList<Object> args = (ArrayList<Object>) message;
  607 + Long playerIdArg = (Long) args.get(0);
  608 + try {
  609 + Long output = api.position(playerIdArg);
  610 + wrapped.add(0, output);
  611 + } catch (Throwable exception) {
  612 + wrapped = wrapError(exception);
  613 + }
  614 + reply.reply(wrapped);
  615 + });
  616 + } else {
  617 + channel.setMessageHandler(null);
  618 + }
  619 + }
  620 + {
  621 + BasicMessageChannel<Object> channel =
  622 + new BasicMessageChannel<>(
  623 + binaryMessenger,
  624 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.seekTo"
  625 + + messageChannelSuffix,
  626 + getCodec());
  627 + if (api != null) {
  628 + channel.setMessageHandler(
  629 + (message, reply) -> {
  630 + ArrayList<Object> wrapped = new ArrayList<>();
  631 + ArrayList<Object> args = (ArrayList<Object>) message;
  632 + Long playerIdArg = (Long) args.get(0);
  633 + Long positionArg = (Long) args.get(1);
  634 + try {
  635 + api.seekTo(playerIdArg, positionArg);
  636 + wrapped.add(0, null);
  637 + } catch (Throwable exception) {
  638 + wrapped = wrapError(exception);
  639 + }
  640 + reply.reply(wrapped);
  641 + });
  642 + } else {
  643 + channel.setMessageHandler(null);
  644 + }
  645 + }
  646 + {
  647 + BasicMessageChannel<Object> channel =
  648 + new BasicMessageChannel<>(
  649 + binaryMessenger,
  650 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.pause"
  651 + + messageChannelSuffix,
  652 + getCodec());
  653 + if (api != null) {
  654 + channel.setMessageHandler(
  655 + (message, reply) -> {
  656 + ArrayList<Object> wrapped = new ArrayList<>();
  657 + ArrayList<Object> args = (ArrayList<Object>) message;
  658 + Long playerIdArg = (Long) args.get(0);
  659 + try {
  660 + api.pause(playerIdArg);
  661 + wrapped.add(0, null);
  662 + } catch (Throwable exception) {
  663 + wrapped = wrapError(exception);
  664 + }
  665 + reply.reply(wrapped);
  666 + });
  667 + } else {
  668 + channel.setMessageHandler(null);
  669 + }
  670 + }
  671 + {
  672 + BasicMessageChannel<Object> channel =
  673 + new BasicMessageChannel<>(
  674 + binaryMessenger,
  675 + "dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers"
  676 + + messageChannelSuffix,
  677 + getCodec());
  678 + if (api != null) {
  679 + channel.setMessageHandler(
  680 + (message, reply) -> {
  681 + ArrayList<Object> wrapped = new ArrayList<>();
  682 + ArrayList<Object> args = (ArrayList<Object>) message;
  683 + Boolean mixWithOthersArg = (Boolean) args.get(0);
  684 + try {
  685 + api.setMixWithOthers(mixWithOthersArg);
  686 + wrapped.add(0, null);
  687 + } catch (Throwable exception) {
  688 + wrapped = wrapError(exception);
  689 + }
  690 + reply.reply(wrapped);
  691 + });
  692 + } else {
  693 + channel.setMessageHandler(null);
  694 + }
  695 + }
  696 + }
  697 + }
  698 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import io.flutter.plugin.common.EventChannel;
  8 +import java.util.ArrayList;
  9 +
  10 +/**
  11 + * And implementation of {@link EventChannel.EventSink} which can wrap an underlying sink.
  12 + *
  13 + * <p>It delivers messages immediately when downstream is available, but it queues messages before
  14 + * the delegate event sink is set with setDelegate.
  15 + *
  16 + * <p>This class is not thread-safe. All calls must be done on the same thread or synchronized
  17 + * externally.
  18 + */
  19 +final class QueuingEventSink implements EventChannel.EventSink {
  20 + private EventChannel.EventSink delegate;
  21 + private final ArrayList<Object> eventQueue = new ArrayList<>();
  22 + private boolean done = false;
  23 +
  24 + public void setDelegate(EventChannel.EventSink delegate) {
  25 + this.delegate = delegate;
  26 + maybeFlush();
  27 + }
  28 +
  29 + @Override
  30 + public void endOfStream() {
  31 + enqueue(new EndOfStreamEvent());
  32 + maybeFlush();
  33 + done = true;
  34 + }
  35 +
  36 + @Override
  37 + public void error(String code, String message, Object details) {
  38 + enqueue(new ErrorEvent(code, message, details));
  39 + maybeFlush();
  40 + }
  41 +
  42 + @Override
  43 + public void success(Object event) {
  44 + enqueue(event);
  45 + maybeFlush();
  46 + }
  47 +
  48 + private void enqueue(Object event) {
  49 + if (done) {
  50 + return;
  51 + }
  52 + eventQueue.add(event);
  53 + }
  54 +
  55 + private void maybeFlush() {
  56 + if (delegate == null) {
  57 + return;
  58 + }
  59 + for (Object event : eventQueue) {
  60 + if (event instanceof EndOfStreamEvent) {
  61 + delegate.endOfStream();
  62 + } else if (event instanceof ErrorEvent) {
  63 + ErrorEvent errorEvent = (ErrorEvent) event;
  64 + delegate.error(errorEvent.code, errorEvent.message, errorEvent.details);
  65 + } else {
  66 + delegate.success(event);
  67 + }
  68 + }
  69 + eventQueue.clear();
  70 + }
  71 +
  72 + static class EndOfStreamEvent {}
  73 +
  74 + private static class ErrorEvent {
  75 + String code;
  76 + String message;
  77 + Object details;
  78 +
  79 + ErrorEvent(String code, String message, Object details) {
  80 + this.code = code;
  81 + this.message = message;
  82 + this.details = details;
  83 + }
  84 + }
  85 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RtspVideoAsset.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.OptIn;
  10 +import androidx.media3.common.MediaItem;
  11 +import androidx.media3.common.util.UnstableApi;
  12 +import androidx.media3.exoplayer.rtsp.RtspMediaSource;
  13 +import androidx.media3.exoplayer.source.MediaSource;
  14 +
  15 +final class RtspVideoAsset extends VideoAsset {
  16 + RtspVideoAsset(@NonNull String assetUrl) {
  17 + super(assetUrl);
  18 + }
  19 +
  20 + @NonNull
  21 + @Override
  22 + public MediaItem getMediaItem() {
  23 + return new MediaItem.Builder().setUri(assetUrl).build();
  24 + }
  25 +
  26 + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
  27 + @OptIn(markerClass = UnstableApi.class)
  28 + @Override
  29 + public MediaSource.Factory getMediaSourceFactory(Context context) {
  30 + return new RtspMediaSource.Factory();
  31 + }
  32 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAsset.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.Nullable;
  10 +import androidx.media3.common.MediaItem;
  11 +import androidx.media3.exoplayer.source.MediaSource;
  12 +import java.util.HashMap;
  13 +import java.util.Map;
  14 +
  15 +/** A video to be played by {@link VideoPlayer}. */
  16 +public abstract class VideoAsset {
  17 + /**
  18 + * Returns an asset from a local {@code asset:///} URL, i.e. an on-device asset.
  19 + *
  20 + * @param assetUrl local asset, beginning in {@code asset:///}.
  21 + * @return the asset.
  22 + */
  23 + @NonNull
  24 + static VideoAsset fromAssetUrl(@NonNull String assetUrl) {
  25 + if (!assetUrl.startsWith("asset:///")) {
  26 + throw new IllegalArgumentException("assetUrl must start with 'asset:///'");
  27 + }
  28 + return new LocalVideoAsset(assetUrl);
  29 + }
  30 +
  31 + /**
  32 + * Returns an asset from a remote URL.
  33 + *
  34 + * @param remoteUrl remote asset, i.e. typically beginning with {@code https://} or similar.
  35 + * @param streamingFormat which streaming format, provided as a hint if able.
  36 + * @param httpHeaders HTTP headers to set for a request.
  37 + * @return the asset.
  38 + */
  39 + @NonNull
  40 + static VideoAsset fromRemoteUrl(
  41 + @Nullable String remoteUrl,
  42 + @NonNull StreamingFormat streamingFormat,
  43 + @NonNull Map<String, String> httpHeaders) {
  44 + return new HttpVideoAsset(remoteUrl, streamingFormat, new HashMap<>(httpHeaders));
  45 + }
  46 +
  47 + /**
  48 + * Returns an asset from a RTSP URL.
  49 + *
  50 + * @param rtspUrl remote asset, beginning with {@code rtsp://}.
  51 + * @return the asset.
  52 + */
  53 + @NonNull
  54 + static VideoAsset fromRtspUrl(@NonNull String rtspUrl) {
  55 + if (!rtspUrl.startsWith("rtsp://")) {
  56 + throw new IllegalArgumentException("rtspUrl must start with 'rtsp://'");
  57 + }
  58 + return new RtspVideoAsset(rtspUrl);
  59 + }
  60 +
  61 + @Nullable protected final String assetUrl;
  62 +
  63 + protected VideoAsset(@Nullable String assetUrl) {
  64 + this.assetUrl = assetUrl;
  65 + }
  66 +
  67 + /**
  68 + * Returns the configured media item to be played.
  69 + *
  70 + * @return media item.
  71 + */
  72 + @NonNull
  73 + public abstract MediaItem getMediaItem();
  74 +
  75 + /**
  76 + * Returns the configured media source factory, if needed for this asset type.
  77 + *
  78 + * @param context application context.
  79 + * @return configured factory, or {@code null} if not needed for this asset type.
  80 + */
  81 + @NonNull
  82 + public abstract MediaSource.Factory getMediaSourceFactory(@NonNull Context context);
  83 +
  84 + /** Streaming formats that can be provided to the video player as a hint. */
  85 + enum StreamingFormat {
  86 + /** Default, if the format is either not known or not another valid format. */
  87 + UNKNOWN,
  88 +
  89 + /** Smooth Streaming. */
  90 + SMOOTH,
  91 +
  92 + /** MPEG-DASH (Dynamic Adaptive over HTTP). */
  93 + DYNAMIC_ADAPTIVE,
  94 +
  95 + /** HTTP Live Streaming (HLS). */
  96 + HTTP_LIVE
  97 + }
  98 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static androidx.media3.common.Player.REPEAT_MODE_ALL;
  8 +import static androidx.media3.common.Player.REPEAT_MODE_OFF;
  9 +
  10 +import androidx.annotation.NonNull;
  11 +import androidx.annotation.Nullable;
  12 +import androidx.media3.common.AudioAttributes;
  13 +import androidx.media3.common.C;
  14 +import androidx.media3.common.MediaItem;
  15 +import androidx.media3.common.PlaybackParameters;
  16 +import androidx.media3.exoplayer.ExoPlayer;
  17 +import io.flutter.view.TextureRegistry.SurfaceProducer;
  18 +
  19 +/**
  20 + * A class responsible for managing video playback using {@link ExoPlayer}.
  21 + *
  22 + * <p>It provides methods to control playback, adjust volume, and handle seeking.
  23 + */
  24 +public abstract class VideoPlayer {
  25 + @NonNull private final ExoPlayerProvider exoPlayerProvider;
  26 + @NonNull private final MediaItem mediaItem;
  27 + @NonNull private final VideoPlayerOptions options;
  28 + @NonNull protected final VideoPlayerCallbacks videoPlayerEvents;
  29 + @Nullable protected final SurfaceProducer surfaceProducer;
  30 + @NonNull protected ExoPlayer exoPlayer;
  31 +
  32 + /** A closure-compatible signature since {@link java.util.function.Supplier} is API level 24. */
  33 + public interface ExoPlayerProvider {
  34 + /**
  35 + * Returns a new {@link ExoPlayer}.
  36 + *
  37 + * @return new instance.
  38 + */
  39 + @NonNull
  40 + ExoPlayer get();
  41 + }
  42 +
  43 + public VideoPlayer(
  44 + @NonNull VideoPlayerCallbacks events,
  45 + @NonNull MediaItem mediaItem,
  46 + @NonNull VideoPlayerOptions options,
  47 + @Nullable SurfaceProducer surfaceProducer,
  48 + @NonNull ExoPlayerProvider exoPlayerProvider) {
  49 + this.videoPlayerEvents = events;
  50 + this.mediaItem = mediaItem;
  51 + this.options = options;
  52 + this.exoPlayerProvider = exoPlayerProvider;
  53 + this.surfaceProducer = surfaceProducer;
  54 + this.exoPlayer = createVideoPlayer();
  55 + }
  56 +
  57 + @NonNull
  58 + protected ExoPlayer createVideoPlayer() {
  59 + ExoPlayer exoPlayer = exoPlayerProvider.get();
  60 + exoPlayer.setMediaItem(mediaItem);
  61 + exoPlayer.prepare();
  62 + exoPlayer.addListener(createExoPlayerEventListener(exoPlayer, surfaceProducer));
  63 + setAudioAttributes(exoPlayer, options.mixWithOthers);
  64 +
  65 + return exoPlayer;
  66 + }
  67 +
  68 + @NonNull
  69 + protected abstract ExoPlayerEventListener createExoPlayerEventListener(
  70 + @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer);
  71 +
  72 + void sendBufferingUpdate() {
  73 + videoPlayerEvents.onBufferingUpdate(exoPlayer.getBufferedPosition());
  74 + }
  75 +
  76 + private static void setAudioAttributes(ExoPlayer exoPlayer, boolean isMixMode) {
  77 + exoPlayer.setAudioAttributes(
  78 + new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MOVIE).build(),
  79 + !isMixMode);
  80 + }
  81 +
  82 + void play() {
  83 + exoPlayer.play();
  84 + }
  85 +
  86 + void pause() {
  87 + exoPlayer.pause();
  88 + }
  89 +
  90 + void setLooping(boolean value) {
  91 + exoPlayer.setRepeatMode(value ? REPEAT_MODE_ALL : REPEAT_MODE_OFF);
  92 + }
  93 +
  94 + void setVolume(double value) {
  95 + float bracketedValue = (float) Math.max(0.0, Math.min(1.0, value));
  96 + exoPlayer.setVolume(bracketedValue);
  97 + }
  98 +
  99 + void setPlaybackSpeed(double value) {
  100 + // We do not need to consider pitch and skipSilence for now as we do not handle them and
  101 + // therefore never diverge from the default values.
  102 + final PlaybackParameters playbackParameters = new PlaybackParameters(((float) value));
  103 +
  104 + exoPlayer.setPlaybackParameters(playbackParameters);
  105 + }
  106 +
  107 + void seekTo(int location) {
  108 + exoPlayer.seekTo(location);
  109 + }
  110 +
  111 + long getPosition() {
  112 + return exoPlayer.getCurrentPosition();
  113 + }
  114 +
  115 + @NonNull
  116 + public ExoPlayer getExoPlayer() {
  117 + return exoPlayer;
  118 + }
  119 +
  120 + public void dispose() {
  121 + exoPlayer.release();
  122 + }
  123 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import androidx.annotation.NonNull;
  8 +import androidx.annotation.Nullable;
  9 +
  10 +/**
  11 + * Callbacks representing events invoked by {@link VideoPlayer}.
  12 + *
  13 + * <p>In the actual plugin, this will always be {@link VideoPlayerEventCallbacks}, which creates the
  14 + * expected events to send back through the plugin channel. In tests methods can be overridden in
  15 + * order to assert results.
  16 + *
  17 + * <p>See {@link androidx.media3.common.Player.Listener} for details.
  18 + */
  19 +public interface VideoPlayerCallbacks {
  20 + void onInitialized(int width, int height, long durationInMs, int rotationCorrectionInDegrees);
  21 +
  22 + void onBufferingStart();
  23 +
  24 + void onBufferingUpdate(long bufferedPosition);
  25 +
  26 + void onBufferingEnd();
  27 +
  28 + void onCompleted();
  29 +
  30 + void onError(@NonNull String code, @Nullable String message, @Nullable Object details);
  31 +
  32 + void onIsPlayingStateUpdate(boolean isPlaying);
  33 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import androidx.annotation.NonNull;
  8 +import androidx.annotation.Nullable;
  9 +import androidx.annotation.VisibleForTesting;
  10 +import io.flutter.plugin.common.EventChannel;
  11 +import java.util.Arrays;
  12 +import java.util.Collections;
  13 +import java.util.HashMap;
  14 +import java.util.List;
  15 +import java.util.Map;
  16 +
  17 +final class VideoPlayerEventCallbacks implements VideoPlayerCallbacks {
  18 + private final EventChannel.EventSink eventSink;
  19 +
  20 + static VideoPlayerEventCallbacks bindTo(EventChannel eventChannel) {
  21 + QueuingEventSink eventSink = new QueuingEventSink();
  22 + eventChannel.setStreamHandler(
  23 + new EventChannel.StreamHandler() {
  24 + @Override
  25 + public void onListen(Object arguments, EventChannel.EventSink events) {
  26 + eventSink.setDelegate(events);
  27 + }
  28 +
  29 + @Override
  30 + public void onCancel(Object arguments) {
  31 + eventSink.setDelegate(null);
  32 + }
  33 + });
  34 + return VideoPlayerEventCallbacks.withSink(eventSink);
  35 + }
  36 +
  37 + @VisibleForTesting
  38 + static VideoPlayerEventCallbacks withSink(EventChannel.EventSink eventSink) {
  39 + return new VideoPlayerEventCallbacks(eventSink);
  40 + }
  41 +
  42 + private VideoPlayerEventCallbacks(EventChannel.EventSink eventSink) {
  43 + this.eventSink = eventSink;
  44 + }
  45 +
  46 + @Override
  47 + public void onInitialized(
  48 + int width, int height, long durationInMs, int rotationCorrectionInDegrees) {
  49 + Map<String, Object> event = new HashMap<>();
  50 + event.put("event", "initialized");
  51 + event.put("width", width);
  52 + event.put("height", height);
  53 + event.put("duration", durationInMs);
  54 + if (rotationCorrectionInDegrees != 0) {
  55 + event.put("rotationCorrection", rotationCorrectionInDegrees);
  56 + }
  57 + eventSink.success(event);
  58 + }
  59 +
  60 + @Override
  61 + public void onBufferingStart() {
  62 + Map<String, Object> event = new HashMap<>();
  63 + event.put("event", "bufferingStart");
  64 + eventSink.success(event);
  65 + }
  66 +
  67 + @Override
  68 + public void onBufferingUpdate(long bufferedPosition) {
  69 + // iOS supports a list of buffered ranges, so we send as a list with a single range.
  70 + Map<String, Object> event = new HashMap<>();
  71 + event.put("event", "bufferingUpdate");
  72 +
  73 + List<? extends Number> range = Arrays.asList(0, bufferedPosition);
  74 + event.put("values", Collections.singletonList(range));
  75 + eventSink.success(event);
  76 + }
  77 +
  78 + @Override
  79 + public void onBufferingEnd() {
  80 + Map<String, Object> event = new HashMap<>();
  81 + event.put("event", "bufferingEnd");
  82 + eventSink.success(event);
  83 + }
  84 +
  85 + @Override
  86 + public void onCompleted() {
  87 + Map<String, Object> event = new HashMap<>();
  88 + event.put("event", "completed");
  89 + eventSink.success(event);
  90 + }
  91 +
  92 + @Override
  93 + public void onError(@NonNull String code, @Nullable String message, @Nullable Object details) {
  94 + eventSink.error(code, message, details);
  95 + }
  96 +
  97 + @Override
  98 + public void onIsPlayingStateUpdate(boolean isPlaying) {
  99 + Map<String, Object> event = new HashMap<>();
  100 + event.put("event", "isPlayingStateUpdate");
  101 + event.put("isPlaying", isPlaying);
  102 + eventSink.success(event);
  103 + }
  104 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +public class VideoPlayerOptions {
  8 + public boolean mixWithOthers;
  9 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import android.util.LongSparseArray;
  9 +import androidx.annotation.NonNull;
  10 +import io.flutter.FlutterInjector;
  11 +import io.flutter.Log;
  12 +import io.flutter.embedding.engine.plugins.FlutterPlugin;
  13 +import io.flutter.plugin.common.BinaryMessenger;
  14 +import io.flutter.plugin.common.EventChannel;
  15 +import io.flutter.plugins.videoplayer.Messages.AndroidVideoPlayerApi;
  16 +import io.flutter.plugins.videoplayer.Messages.CreateMessage;
  17 +import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory;
  18 +import io.flutter.plugins.videoplayer.platformview.PlatformViewVideoPlayer;
  19 +import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer;
  20 +import io.flutter.view.TextureRegistry;
  21 +
  22 +/** Android platform implementation of the VideoPlayerPlugin. */
  23 +public class VideoPlayerPlugin implements FlutterPlugin, AndroidVideoPlayerApi {
  24 + private static final String TAG = "VideoPlayerPlugin";
  25 + private final LongSparseArray<VideoPlayer> videoPlayers = new LongSparseArray<>();
  26 + private FlutterState flutterState;
  27 + private final VideoPlayerOptions options = new VideoPlayerOptions();
  28 +
  29 + // TODO(stuartmorgan): Decouple identifiers for platform views and texture views.
  30 + /**
  31 + * The next non-texture player ID, initialized to a high number to avoid collisions with texture
  32 + * IDs (which are generated separately).
  33 + */
  34 + private Long nextPlatformViewPlayerId = Long.MAX_VALUE;
  35 +
  36 + /** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */
  37 + public VideoPlayerPlugin() {}
  38 +
  39 + @Override
  40 + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
  41 + final FlutterInjector injector = FlutterInjector.instance();
  42 + this.flutterState =
  43 + new FlutterState(
  44 + binding.getApplicationContext(),
  45 + binding.getBinaryMessenger(),
  46 + injector.flutterLoader()::getLookupKeyForAsset,
  47 + injector.flutterLoader()::getLookupKeyForAsset,
  48 + binding.getTextureRegistry());
  49 + flutterState.startListening(this, binding.getBinaryMessenger());
  50 +
  51 + binding
  52 + .getPlatformViewRegistry()
  53 + .registerViewFactory(
  54 + "plugins.flutter.dev/video_player_android",
  55 + new PlatformVideoViewFactory(videoPlayers::get));
  56 + }
  57 +
  58 + @Override
  59 + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
  60 + if (flutterState == null) {
  61 + Log.wtf(TAG, "Detached from the engine before registering to it.");
  62 + }
  63 + flutterState.stopListening(binding.getBinaryMessenger());
  64 + flutterState = null;
  65 + onDestroy();
  66 + }
  67 +
  68 + private void disposeAllPlayers() {
  69 + for (int i = 0; i < videoPlayers.size(); i++) {
  70 + videoPlayers.valueAt(i).dispose();
  71 + }
  72 + videoPlayers.clear();
  73 + }
  74 +
  75 + public void onDestroy() {
  76 + // The whole FlutterView is being destroyed. Here we release resources acquired for all
  77 + // instances
  78 + // of VideoPlayer. Once https://github.com/flutter/flutter/issues/19358 is resolved this may
  79 + // be replaced with just asserting that videoPlayers.isEmpty().
  80 + // https://github.com/flutter/flutter/issues/20989 tracks this.
  81 + disposeAllPlayers();
  82 + }
  83 +
  84 + @Override
  85 + public void initialize() {
  86 + disposeAllPlayers();
  87 + }
  88 +
  89 + @Override
  90 + public @NonNull Long create(@NonNull CreateMessage arg) {
  91 + final VideoAsset videoAsset;
  92 + if (arg.getAsset() != null) {
  93 + String assetLookupKey;
  94 + if (arg.getPackageName() != null) {
  95 + assetLookupKey =
  96 + flutterState.keyForAssetAndPackageName.get(arg.getAsset(), arg.getPackageName());
  97 + } else {
  98 + assetLookupKey = flutterState.keyForAsset.get(arg.getAsset());
  99 + }
  100 + videoAsset = VideoAsset.fromAssetUrl("asset:///" + assetLookupKey);
  101 + } else if (arg.getUri().startsWith("rtsp://")) {
  102 + videoAsset = VideoAsset.fromRtspUrl(arg.getUri());
  103 + } else {
  104 + VideoAsset.StreamingFormat streamingFormat = VideoAsset.StreamingFormat.UNKNOWN;
  105 + String formatHint = arg.getFormatHint();
  106 + if (formatHint != null) {
  107 + switch (formatHint) {
  108 + case "ss":
  109 + streamingFormat = VideoAsset.StreamingFormat.SMOOTH;
  110 + break;
  111 + case "dash":
  112 + streamingFormat = VideoAsset.StreamingFormat.DYNAMIC_ADAPTIVE;
  113 + break;
  114 + case "hls":
  115 + streamingFormat = VideoAsset.StreamingFormat.HTTP_LIVE;
  116 + break;
  117 + }
  118 + }
  119 + videoAsset = VideoAsset.fromRemoteUrl(arg.getUri(), streamingFormat, arg.getHttpHeaders());
  120 + }
  121 +
  122 + long id;
  123 + VideoPlayer videoPlayer;
  124 + if (arg.getViewType() == Messages.PlatformVideoViewType.PLATFORM_VIEW) {
  125 + id = nextPlatformViewPlayerId--;
  126 + videoPlayer =
  127 + PlatformViewVideoPlayer.create(
  128 + flutterState.applicationContext,
  129 + VideoPlayerEventCallbacks.bindTo(createEventChannel(id)),
  130 + videoAsset,
  131 + options);
  132 + } else {
  133 + TextureRegistry.SurfaceProducer handle = flutterState.textureRegistry.createSurfaceProducer();
  134 + id = handle.id();
  135 + videoPlayer =
  136 + TextureVideoPlayer.create(
  137 + flutterState.applicationContext,
  138 + VideoPlayerEventCallbacks.bindTo(createEventChannel(id)),
  139 + handle,
  140 + videoAsset,
  141 + options);
  142 + }
  143 +
  144 + videoPlayers.put(id, videoPlayer);
  145 + return id;
  146 + }
  147 +
  148 + @NonNull
  149 + private EventChannel createEventChannel(long id) {
  150 + return new EventChannel(
  151 + flutterState.binaryMessenger, "flutter.io/videoPlayer/videoEvents" + id);
  152 + }
  153 +
  154 + @NonNull
  155 + private VideoPlayer getPlayer(long playerId) {
  156 + VideoPlayer player = videoPlayers.get(playerId);
  157 +
  158 + // Avoid a very ugly un-debuggable NPE that results in returning a null player.
  159 + if (player == null) {
  160 + String message = "No player found with playerId <" + playerId + ">";
  161 + if (videoPlayers.size() == 0) {
  162 + message += " and no active players created by the plugin.";
  163 + }
  164 + throw new IllegalStateException(message);
  165 + }
  166 +
  167 + return player;
  168 + }
  169 +
  170 + @Override
  171 + public void dispose(@NonNull Long playerId) {
  172 + VideoPlayer player = getPlayer(playerId);
  173 + player.dispose();
  174 + videoPlayers.remove(playerId);
  175 + }
  176 +
  177 + @Override
  178 + public void setLooping(@NonNull Long playerId, @NonNull Boolean looping) {
  179 + VideoPlayer player = getPlayer(playerId);
  180 + player.setLooping(looping);
  181 + }
  182 +
  183 + @Override
  184 + public void setVolume(@NonNull Long playerId, @NonNull Double volume) {
  185 + VideoPlayer player = getPlayer(playerId);
  186 + player.setVolume(volume);
  187 + }
  188 +
  189 + @Override
  190 + public void setPlaybackSpeed(@NonNull Long playerId, @NonNull Double speed) {
  191 + VideoPlayer player = getPlayer(playerId);
  192 + player.setPlaybackSpeed(speed);
  193 + }
  194 +
  195 + @Override
  196 + public void play(@NonNull Long playerId) {
  197 + VideoPlayer player = getPlayer(playerId);
  198 + player.play();
  199 + }
  200 +
  201 + @Override
  202 + public @NonNull Long position(@NonNull Long playerId) {
  203 + VideoPlayer player = getPlayer(playerId);
  204 + long position = player.getPosition();
  205 + player.sendBufferingUpdate();
  206 + return position;
  207 + }
  208 +
  209 + @Override
  210 + public void seekTo(@NonNull Long playerId, @NonNull Long position) {
  211 + VideoPlayer player = getPlayer(playerId);
  212 + player.seekTo(position.intValue());
  213 + }
  214 +
  215 + @Override
  216 + public void pause(@NonNull Long playerId) {
  217 + VideoPlayer player = getPlayer(playerId);
  218 + player.pause();
  219 + }
  220 +
  221 + @Override
  222 + public void setMixWithOthers(@NonNull Boolean mixWithOthers) {
  223 + options.mixWithOthers = mixWithOthers;
  224 + }
  225 +
  226 + private interface KeyForAssetFn {
  227 + String get(String asset);
  228 + }
  229 +
  230 + private interface KeyForAssetAndPackageName {
  231 + String get(String asset, String packageName);
  232 + }
  233 +
  234 + private static final class FlutterState {
  235 + final Context applicationContext;
  236 + final BinaryMessenger binaryMessenger;
  237 + final KeyForAssetFn keyForAsset;
  238 + final KeyForAssetAndPackageName keyForAssetAndPackageName;
  239 + final TextureRegistry textureRegistry;
  240 +
  241 + FlutterState(
  242 + Context applicationContext,
  243 + BinaryMessenger messenger,
  244 + KeyForAssetFn keyForAsset,
  245 + KeyForAssetAndPackageName keyForAssetAndPackageName,
  246 + TextureRegistry textureRegistry) {
  247 + this.applicationContext = applicationContext;
  248 + this.binaryMessenger = messenger;
  249 + this.keyForAsset = keyForAsset;
  250 + this.keyForAssetAndPackageName = keyForAssetAndPackageName;
  251 + this.textureRegistry = textureRegistry;
  252 + }
  253 +
  254 + void startListening(VideoPlayerPlugin methodCallHandler, BinaryMessenger messenger) {
  255 + AndroidVideoPlayerApi.setUp(messenger, methodCallHandler);
  256 + }
  257 +
  258 + void stopListening(BinaryMessenger messenger) {
  259 + AndroidVideoPlayerApi.setUp(messenger, null);
  260 + }
  261 + }
  262 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformVideoView.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.platformview;
  6 +
  7 +import android.content.Context;
  8 +import android.os.Build;
  9 +import android.view.SurfaceHolder;
  10 +import android.view.SurfaceView;
  11 +import android.view.View;
  12 +import androidx.annotation.NonNull;
  13 +import androidx.annotation.OptIn;
  14 +import androidx.media3.common.util.UnstableApi;
  15 +import androidx.media3.exoplayer.ExoPlayer;
  16 +import io.flutter.plugin.platform.PlatformView;
  17 +
  18 +/**
  19 + * A class used to create a native video view that can be embedded in a Flutter app. It wraps an
  20 + * {@link ExoPlayer} instance and displays its video content.
  21 + */
  22 +public final class PlatformVideoView implements PlatformView {
  23 + @NonNull private final SurfaceView surfaceView;
  24 +
  25 + /**
  26 + * Constructs a new PlatformVideoView.
  27 + *
  28 + * @param context The context in which the view is running.
  29 + * @param exoPlayer The ExoPlayer instance used to play the video.
  30 + */
  31 + @OptIn(markerClass = UnstableApi.class)
  32 + public PlatformVideoView(@NonNull Context context, @NonNull ExoPlayer exoPlayer) {
  33 + surfaceView = new SurfaceView(context);
  34 +
  35 + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
  36 + // Workaround for rendering issues on Android 9 (API 28).
  37 + // On Android 9, using setVideoSurfaceView seems to lead to issues where the first frame is
  38 + // not displayed if the video is paused initially.
  39 + // To ensure the first frame is visible, the surface is directly set using holder.getSurface()
  40 + // when the surface is created, and ExoPlayer seeks to a position to force rendering of the
  41 + // first frame.
  42 + setupSurfaceWithCallback(exoPlayer);
  43 + } else {
  44 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
  45 + // Avoid blank space instead of a video on Android versions below 8 by adjusting video's
  46 + // z-layer within the Android view hierarchy:
  47 + surfaceView.setZOrderMediaOverlay(true);
  48 + }
  49 + exoPlayer.setVideoSurfaceView(surfaceView);
  50 + }
  51 + }
  52 +
  53 + private void setupSurfaceWithCallback(@NonNull ExoPlayer exoPlayer) {
  54 + surfaceView
  55 + .getHolder()
  56 + .addCallback(
  57 + new SurfaceHolder.Callback() {
  58 + @Override
  59 + public void surfaceCreated(@NonNull SurfaceHolder holder) {
  60 + exoPlayer.setVideoSurface(holder.getSurface());
  61 + // Force first frame rendering:
  62 + exoPlayer.seekTo(1);
  63 + }
  64 +
  65 + @Override
  66 + public void surfaceChanged(
  67 + @NonNull SurfaceHolder holder, int format, int width, int height) {
  68 + // No implementation needed.
  69 + }
  70 +
  71 + @Override
  72 + public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
  73 + exoPlayer.setVideoSurface(null);
  74 + }
  75 + });
  76 + }
  77 +
  78 + /**
  79 + * Returns the view associated with this PlatformView.
  80 + *
  81 + * @return The SurfaceView used to display the video.
  82 + */
  83 + @NonNull
  84 + @Override
  85 + public View getView() {
  86 + return surfaceView;
  87 + }
  88 +
  89 + /** Disposes of the resources used by this PlatformView. */
  90 + @Override
  91 + public void dispose() {
  92 + surfaceView.getHolder().getSurface().release();
  93 + }
  94 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformVideoViewFactory.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.platformview;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.Nullable;
  10 +import androidx.media3.exoplayer.ExoPlayer;
  11 +import io.flutter.plugin.platform.PlatformView;
  12 +import io.flutter.plugin.platform.PlatformViewFactory;
  13 +import io.flutter.plugins.videoplayer.Messages;
  14 +import io.flutter.plugins.videoplayer.VideoPlayer;
  15 +import java.util.Objects;
  16 +
  17 +/**
  18 + * A factory class responsible for creating platform video views that can be embedded in a Flutter
  19 + * app.
  20 + */
  21 +public class PlatformVideoViewFactory extends PlatformViewFactory {
  22 + private final VideoPlayerProvider videoPlayerProvider;
  23 +
  24 + /** Functional interface for providing a VideoPlayer instance based on the player ID. */
  25 + @FunctionalInterface
  26 + public interface VideoPlayerProvider {
  27 + /**
  28 + * Retrieves a VideoPlayer instance based on the provided player ID.
  29 + *
  30 + * @param playerId The unique identifier for the video player.
  31 + * @return A VideoPlayer instance associated with the given player ID.
  32 + */
  33 + @NonNull
  34 + VideoPlayer getVideoPlayer(@NonNull Long playerId);
  35 + }
  36 +
  37 + /**
  38 + * Constructs a new PlatformVideoViewFactory.
  39 + *
  40 + * @param videoPlayerProvider The provider used to retrieve the video player associated with the
  41 + * view.
  42 + */
  43 + public PlatformVideoViewFactory(@NonNull VideoPlayerProvider videoPlayerProvider) {
  44 + super(Messages.AndroidVideoPlayerApi.getCodec());
  45 + this.videoPlayerProvider = videoPlayerProvider;
  46 + }
  47 +
  48 + /**
  49 + * Creates a new instance of platform view.
  50 + *
  51 + * @param context The context in which the view is running.
  52 + * @param id The unique identifier for the view.
  53 + * @param args The arguments for creating the view.
  54 + * @return A new instance of PlatformVideoView.
  55 + */
  56 + @NonNull
  57 + @Override
  58 + public PlatformView create(@NonNull Context context, int id, @Nullable Object args) {
  59 + final Messages.PlatformVideoViewCreationParams params =
  60 + Objects.requireNonNull((Messages.PlatformVideoViewCreationParams) args);
  61 + final Long playerId = params.getPlayerId();
  62 +
  63 + final VideoPlayer player = videoPlayerProvider.getVideoPlayer(playerId);
  64 + final ExoPlayer exoPlayer = player.getExoPlayer();
  65 +
  66 + return new PlatformVideoView(context, exoPlayer);
  67 + }
  68 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewExoPlayerEventListener.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.platformview;
  6 +
  7 +import androidx.annotation.NonNull;
  8 +import androidx.annotation.OptIn;
  9 +import androidx.annotation.VisibleForTesting;
  10 +import androidx.media3.common.Format;
  11 +import androidx.media3.common.util.UnstableApi;
  12 +import androidx.media3.exoplayer.ExoPlayer;
  13 +import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
  14 +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks;
  15 +import java.util.Objects;
  16 +
  17 +public final class PlatformViewExoPlayerEventListener extends ExoPlayerEventListener {
  18 + @VisibleForTesting
  19 + public PlatformViewExoPlayerEventListener(
  20 + @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events) {
  21 + this(exoPlayer, events, false);
  22 + }
  23 +
  24 + public PlatformViewExoPlayerEventListener(
  25 + @NonNull ExoPlayer exoPlayer, @NonNull VideoPlayerCallbacks events, boolean initialized) {
  26 + super(exoPlayer, events, initialized);
  27 + }
  28 +
  29 + @OptIn(markerClass = UnstableApi.class)
  30 + @Override
  31 + protected void sendInitialized() {
  32 + // We can't rely on VideoSize here, because at this point it is not available - the platform
  33 + // view was not created yet. We use the video format instead.
  34 + Format videoFormat = exoPlayer.getVideoFormat();
  35 + RotationDegrees rotationCorrection =
  36 + RotationDegrees.fromDegrees(Objects.requireNonNull(videoFormat).rotationDegrees);
  37 + int width = videoFormat.width;
  38 + int height = videoFormat.height;
  39 +
  40 + // Switch the width/height if video was taken in portrait mode and a rotation
  41 + // correction was detected.
  42 + if (rotationCorrection == RotationDegrees.ROTATE_90
  43 + || rotationCorrection == RotationDegrees.ROTATE_270) {
  44 + width = videoFormat.height;
  45 + height = videoFormat.width;
  46 +
  47 + rotationCorrection = RotationDegrees.fromDegrees(0);
  48 + }
  49 +
  50 + events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection.getDegrees());
  51 + }
  52 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.platformview;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.Nullable;
  10 +import androidx.annotation.VisibleForTesting;
  11 +import androidx.media3.common.MediaItem;
  12 +import androidx.media3.exoplayer.DefaultRenderersFactory;
  13 +import androidx.media3.exoplayer.ExoPlayer;
  14 +import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
  15 +import io.flutter.plugins.videoplayer.VideoAsset;
  16 +import io.flutter.plugins.videoplayer.VideoPlayer;
  17 +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks;
  18 +import io.flutter.plugins.videoplayer.VideoPlayerOptions;
  19 +import io.flutter.view.TextureRegistry.SurfaceProducer;
  20 +
  21 +/**
  22 + * A subclass of {@link VideoPlayer} that adds functionality related to platform view as a way of
  23 + * displaying the video in the app.
  24 + */
  25 +public class PlatformViewVideoPlayer extends VideoPlayer {
  26 + @VisibleForTesting
  27 + public PlatformViewVideoPlayer(
  28 + @NonNull VideoPlayerCallbacks events,
  29 + @NonNull MediaItem mediaItem,
  30 + @NonNull VideoPlayerOptions options,
  31 + @NonNull ExoPlayerProvider exoPlayerProvider) {
  32 + super(events, mediaItem, options, /* surfaceProducer */ null, exoPlayerProvider);
  33 + }
  34 +
  35 + /**
  36 + * Creates a platform view video player.
  37 + *
  38 + * @param context application context.
  39 + * @param events event callbacks.
  40 + * @param asset asset to play.
  41 + * @param options options for playback.
  42 + * @return a video player instance.
  43 + */
  44 + @NonNull
  45 + public static PlatformViewVideoPlayer create(
  46 + @NonNull Context context,
  47 + @NonNull VideoPlayerCallbacks events,
  48 + @NonNull VideoAsset asset,
  49 + @NonNull VideoPlayerOptions options) {
  50 + return new PlatformViewVideoPlayer(
  51 + events,
  52 + asset.getMediaItem(),
  53 + options,
  54 + () -> {
  55 + DefaultRenderersFactory renderersFactory =
  56 + new DefaultRenderersFactory(context).setEnableDecoderFallback(true);
  57 + ExoPlayer.Builder builder =
  58 + new ExoPlayer.Builder(context, renderersFactory)
  59 + .setMediaSourceFactory(asset.getMediaSourceFactory(context));
  60 + return builder.build();
  61 + });
  62 + }
  63 +
  64 + @NonNull
  65 + @Override
  66 + protected ExoPlayerEventListener createExoPlayerEventListener(
  67 + @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer) {
  68 + // Platform view video player does not suspend and re-create the exoPlayer, hence initialized
  69 + // is always false. It also does not require a reference to the SurfaceProducer.
  70 + return new PlatformViewExoPlayerEventListener(exoPlayer, videoPlayerEvents, false);
  71 + }
  72 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureExoPlayerEventListener.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.texture;
  6 +
  7 +import android.os.Build;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.OptIn;
  10 +import androidx.annotation.VisibleForTesting;
  11 +import androidx.media3.common.Format;
  12 +import androidx.media3.common.VideoSize;
  13 +import androidx.media3.exoplayer.ExoPlayer;
  14 +import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
  15 +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks;
  16 +import java.util.Objects;
  17 +
  18 +public final class TextureExoPlayerEventListener extends ExoPlayerEventListener {
  19 + private boolean surfaceProducerHandlesCropAndRotation;
  20 +
  21 + @VisibleForTesting
  22 + public TextureExoPlayerEventListener(
  23 + @NonNull ExoPlayer exoPlayer,
  24 + @NonNull VideoPlayerCallbacks events,
  25 + boolean surfaceProducerHandlesCropAndRotation) {
  26 + this(exoPlayer, events, surfaceProducerHandlesCropAndRotation, false);
  27 + }
  28 +
  29 + public TextureExoPlayerEventListener(
  30 + @NonNull ExoPlayer exoPlayer,
  31 + @NonNull VideoPlayerCallbacks events,
  32 + boolean surfaceProducerHandlesCropAndRotation,
  33 + boolean initialized) {
  34 + super(exoPlayer, events, initialized);
  35 + this.surfaceProducerHandlesCropAndRotation = surfaceProducerHandlesCropAndRotation;
  36 + }
  37 +
  38 + @Override
  39 + protected void sendInitialized() {
  40 + VideoSize videoSize = exoPlayer.getVideoSize();
  41 + RotationDegrees rotationCorrection = RotationDegrees.ROTATE_0;
  42 + int width = videoSize.width;
  43 + int height = videoSize.height;
  44 + if (width != 0 && height != 0) {
  45 + if (Build.VERSION.SDK_INT <= 21) {
  46 + // On API 21 and below, Exoplayer may not internally handle rotation correction
  47 + // and reports it through VideoSize.unappliedRotationDegrees. We may apply it to
  48 + // fix the case of upside-down playback.
  49 + try {
  50 + RotationDegrees unappliedRotation =
  51 + RotationDegrees.fromDegrees(videoSize.unappliedRotationDegrees);
  52 + rotationCorrection = getRotationCorrectionFromUnappliedRotation(unappliedRotation);
  53 + } catch (IllegalArgumentException e) {
  54 + // Unapplied rotation other than 0, 90, 180, 270 reported by VideoSize. Because this is
  55 + // unexpected, we apply no rotation correction.
  56 + rotationCorrection = RotationDegrees.ROTATE_0;
  57 + }
  58 + } else if (surfaceProducerHandlesCropAndRotation) {
  59 + // When the SurfaceTexture backend for Impeller is used, the preview should already
  60 + // be correctly rotated.
  61 + rotationCorrection = RotationDegrees.ROTATE_0;
  62 + } else {
  63 + // The video's Format also provides a rotation correction that may be used to
  64 + // correct the rotation, so we try to use that to correct the video rotation
  65 + // when the ImageReader backend for Impeller is used.
  66 + int rawVideoFormatRotation = getRotationCorrectionFromFormat(exoPlayer);
  67 +
  68 + try {
  69 + rotationCorrection = RotationDegrees.fromDegrees(rawVideoFormatRotation);
  70 + } catch (IllegalArgumentException e) {
  71 + // Rotation correction other than 0, 90, 180, 270 reported by Format. Because this is
  72 + // unexpected we apply no rotation correction.
  73 + rotationCorrection = RotationDegrees.ROTATE_0;
  74 + }
  75 + }
  76 + }
  77 + events.onInitialized(width, height, exoPlayer.getDuration(), rotationCorrection.getDegrees());
  78 + }
  79 +
  80 + private RotationDegrees getRotationCorrectionFromUnappliedRotation(
  81 + RotationDegrees unappliedRotationDegrees) {
  82 + RotationDegrees rotationCorrection = RotationDegrees.ROTATE_0;
  83 +
  84 + // Rotating the video with ExoPlayer does not seem to be possible with a Surface,
  85 + // so inform the Flutter code that the widget needs to be rotated to prevent
  86 + // upside-down playback for videos with unappliedRotationDegrees of 180 (other orientations
  87 + // work correctly without correction).
  88 + if (unappliedRotationDegrees == RotationDegrees.ROTATE_180) {
  89 + rotationCorrection = unappliedRotationDegrees;
  90 + }
  91 +
  92 + return rotationCorrection;
  93 + }
  94 +
  95 + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
  96 + // A video's Format and its rotation degrees are unstable because they are not guaranteed
  97 + // the same implementation across API versions. It is possible that this logic may need
  98 + // revisiting should the implementation change across versions of the Exoplayer API.
  99 + private int getRotationCorrectionFromFormat(ExoPlayer exoPlayer) {
  100 + Format videoFormat = Objects.requireNonNull(exoPlayer.getVideoFormat());
  101 + return videoFormat.rotationDegrees;
  102 + }
  103 +}
... ...
packages/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer.texture;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.annotation.Nullable;
  10 +import androidx.annotation.RestrictTo;
  11 +import androidx.annotation.VisibleForTesting;
  12 +import androidx.media3.common.MediaItem;
  13 +import androidx.media3.exoplayer.DefaultRenderersFactory;
  14 +import androidx.media3.exoplayer.ExoPlayer;
  15 +import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
  16 +import io.flutter.plugins.videoplayer.ExoPlayerState;
  17 +import io.flutter.plugins.videoplayer.VideoAsset;
  18 +import io.flutter.plugins.videoplayer.VideoPlayer;
  19 +import io.flutter.plugins.videoplayer.VideoPlayerCallbacks;
  20 +import io.flutter.plugins.videoplayer.VideoPlayerOptions;
  21 +import io.flutter.view.TextureRegistry.SurfaceProducer;
  22 +
  23 +/**
  24 + * A subclass of {@link VideoPlayer} that adds functionality related to texture view as a way of
  25 + * displaying the video in the app.
  26 + *
  27 + * <p>It manages the lifecycle of the texture and ensures that the video is properly displayed on
  28 + * the texture.
  29 + */
  30 +public final class TextureVideoPlayer extends VideoPlayer implements SurfaceProducer.Callback {
  31 + @Nullable private ExoPlayerState savedStateDuring;
  32 +
  33 + /**
  34 + * Creates a texture video player.
  35 + *
  36 + * @param context application context.
  37 + * @param events event callbacks.
  38 + * @param surfaceProducer produces a texture to render to.
  39 + * @param asset asset to play.
  40 + * @param options options for playback.
  41 + * @return a video player instance.
  42 + */
  43 + @NonNull
  44 + public static TextureVideoPlayer create(
  45 + @NonNull Context context,
  46 + @NonNull VideoPlayerCallbacks events,
  47 + @NonNull SurfaceProducer surfaceProducer,
  48 + @NonNull VideoAsset asset,
  49 + @NonNull VideoPlayerOptions options) {
  50 + return new TextureVideoPlayer(
  51 + events,
  52 + surfaceProducer,
  53 + asset.getMediaItem(),
  54 + options,
  55 + () -> {
  56 + DefaultRenderersFactory renderersFactory =
  57 + new DefaultRenderersFactory(context).setEnableDecoderFallback(true);
  58 + ExoPlayer.Builder builder =
  59 + new ExoPlayer.Builder(context, renderersFactory)
  60 + .setMediaSourceFactory(asset.getMediaSourceFactory(context));
  61 + return builder.build();
  62 + });
  63 + }
  64 +
  65 + @VisibleForTesting
  66 + public TextureVideoPlayer(
  67 + @NonNull VideoPlayerCallbacks events,
  68 + @NonNull SurfaceProducer surfaceProducer,
  69 + @NonNull MediaItem mediaItem,
  70 + @NonNull VideoPlayerOptions options,
  71 + @NonNull ExoPlayerProvider exoPlayerProvider) {
  72 + super(events, mediaItem, options, surfaceProducer, exoPlayerProvider);
  73 +
  74 + surfaceProducer.setCallback(this);
  75 +
  76 + this.exoPlayer.setVideoSurface(surfaceProducer.getSurface());
  77 + }
  78 +
  79 + @NonNull
  80 + @Override
  81 + protected ExoPlayerEventListener createExoPlayerEventListener(
  82 + @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer) {
  83 + if (surfaceProducer == null) {
  84 + throw new IllegalArgumentException(
  85 + "surfaceProducer cannot be null to create an ExoPlayerEventListener for TextureVideoPlayer.");
  86 + }
  87 + boolean surfaceProducerHandlesCropAndRotation = surfaceProducer.handlesCropAndRotation();
  88 + return new TextureExoPlayerEventListener(
  89 + exoPlayer,
  90 + videoPlayerEvents,
  91 + surfaceProducerHandlesCropAndRotation,
  92 + playerHasBeenSuspended());
  93 + }
  94 +
  95 + @RestrictTo(RestrictTo.Scope.LIBRARY)
  96 + public void onSurfaceAvailable() {
  97 + if (savedStateDuring != null) {
  98 + exoPlayer = createVideoPlayer();
  99 + exoPlayer.setVideoSurface(surfaceProducer.getSurface());
  100 + savedStateDuring.restore(exoPlayer);
  101 + savedStateDuring = null;
  102 + }
  103 + }
  104 +
  105 + @RestrictTo(RestrictTo.Scope.LIBRARY)
  106 + // TODO(bparrishMines): Replace with onSurfaceCleanup once available on stable. See
  107 + // https://github.com/flutter/flutter/issues/161256.
  108 + @SuppressWarnings({"deprecation", "removal"})
  109 + public void onSurfaceDestroyed() {
  110 + // Intentionally do not call pause/stop here, because the surface has already been released
  111 + // at this point (see https://github.com/flutter/flutter/issues/156451).
  112 + savedStateDuring = ExoPlayerState.save(exoPlayer);
  113 + exoPlayer.release();
  114 + }
  115 +
  116 + private boolean playerHasBeenSuspended() {
  117 + return savedStateDuring != null;
  118 + }
  119 +
  120 + public void dispose() {
  121 + // Super must be called first to ensure the player is released before the surface.
  122 + super.dispose();
  123 +
  124 + surfaceProducer.release();
  125 + // TODO(matanlurey): Remove when embedder no longer calls-back once released.
  126 + // https://github.com/flutter/flutter/issues/156434.
  127 + surfaceProducer.setCallback(null);
  128 + }
  129 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.mockito.ArgumentMatchers.contains;
  8 +import static org.mockito.ArgumentMatchers.eq;
  9 +import static org.mockito.ArgumentMatchers.isNull;
  10 +import static org.mockito.Mockito.verify;
  11 +import static org.mockito.Mockito.verifyNoInteractions;
  12 +import static org.mockito.Mockito.verifyNoMoreInteractions;
  13 +import static org.mockito.Mockito.when;
  14 +
  15 +import androidx.media3.common.PlaybackException;
  16 +import androidx.media3.common.Player;
  17 +import androidx.media3.exoplayer.ExoPlayer;
  18 +import org.junit.Before;
  19 +import org.junit.Rule;
  20 +import org.junit.Test;
  21 +import org.junit.runner.RunWith;
  22 +import org.mockito.Mock;
  23 +import org.mockito.junit.MockitoJUnit;
  24 +import org.mockito.junit.MockitoRule;
  25 +import org.robolectric.RobolectricTestRunner;
  26 +
  27 +/**
  28 + * Unit tests for {@link ExoPlayerEventListener}.
  29 + *
  30 + * <p>This test suite <em>narrowly verifies</em> that the events emitted by the underlying {@link
  31 + * androidx.media3.exoplayer.ExoPlayer} instance are translated to the callback interface we expect
  32 + * ({@link VideoPlayerCallbacks} and/or interface with the player instance as expected).
  33 + */
  34 +@RunWith(RobolectricTestRunner.class)
  35 +public final class ExoPlayerEventListenerTest {
  36 + @Mock private ExoPlayer mockExoPlayer;
  37 + @Mock private VideoPlayerCallbacks mockCallbacks;
  38 + private ExoPlayerEventListener eventListener;
  39 +
  40 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  41 +
  42 + /**
  43 + * A test subclass of {@link ExoPlayerEventListener} that exposes the abstract class for testing.
  44 + */
  45 + private static final class TestExoPlayerEventListener extends ExoPlayerEventListener {
  46 + public TestExoPlayerEventListener(ExoPlayer exoPlayer, VideoPlayerCallbacks callbacks) {
  47 + super(exoPlayer, callbacks, false);
  48 + }
  49 +
  50 + @Override
  51 + protected void sendInitialized() {
  52 + // No implementation needed.
  53 + }
  54 + }
  55 +
  56 + @Before
  57 + public void setUp() {
  58 + eventListener = new TestExoPlayerEventListener(mockExoPlayer, mockCallbacks);
  59 + }
  60 +
  61 + @Test
  62 + public void onPlaybackStateChangedBufferingSendsBufferingStartAndUpdates() {
  63 + when(mockExoPlayer.getBufferedPosition()).thenReturn(10L);
  64 + eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING);
  65 +
  66 + verify(mockCallbacks).onBufferingStart();
  67 + verify(mockCallbacks).onBufferingUpdate(10L);
  68 + verifyNoMoreInteractions(mockCallbacks);
  69 +
  70 + // If it's invoked again, only the update event is called.
  71 + verify(mockCallbacks).onBufferingUpdate(10L);
  72 + verifyNoMoreInteractions(mockCallbacks);
  73 + }
  74 +
  75 + @Test
  76 + public void onPlaybackStateChangedEndedSendsOnCompleted() {
  77 + eventListener.onPlaybackStateChanged(Player.STATE_ENDED);
  78 +
  79 + verify(mockCallbacks).onCompleted();
  80 + verifyNoMoreInteractions(mockCallbacks);
  81 + }
  82 +
  83 + @Test
  84 + public void onPlaybackStateChangedEndedAfterBufferingSendsBufferingEndAndOnCompleted() {
  85 + when(mockExoPlayer.getBufferedPosition()).thenReturn(10L);
  86 + eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING);
  87 + verify(mockCallbacks).onBufferingStart();
  88 + verify(mockCallbacks).onBufferingUpdate(10L);
  89 +
  90 + eventListener.onPlaybackStateChanged(Player.STATE_ENDED);
  91 + verify(mockCallbacks).onCompleted();
  92 + verify(mockCallbacks).onBufferingEnd();
  93 +
  94 + verifyNoMoreInteractions(mockCallbacks);
  95 + }
  96 +
  97 + @Test
  98 + public void onPlaybackStateChangedIdleDoNothing() {
  99 + eventListener.onPlaybackStateChanged(Player.STATE_IDLE);
  100 +
  101 + verifyNoInteractions(mockCallbacks);
  102 + }
  103 +
  104 + @Test
  105 + public void onPlaybackStateChangedIdleAfterBufferingSendsBufferingEnd() {
  106 + when(mockExoPlayer.getBufferedPosition()).thenReturn(10L);
  107 + eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING);
  108 + verify(mockCallbacks).onBufferingStart();
  109 + verify(mockCallbacks).onBufferingUpdate(10L);
  110 +
  111 + eventListener.onPlaybackStateChanged(Player.STATE_IDLE);
  112 + verify(mockCallbacks).onBufferingEnd();
  113 +
  114 + verifyNoMoreInteractions(mockCallbacks);
  115 + }
  116 +
  117 + @Test
  118 + public void onErrorVideoErrorWhenBufferingInProgressAlsoEndBuffering() {
  119 + when(mockExoPlayer.getBufferedPosition()).thenReturn(10L);
  120 + eventListener.onPlaybackStateChanged(Player.STATE_BUFFERING);
  121 + verify(mockCallbacks).onBufferingStart();
  122 + verify(mockCallbacks).onBufferingUpdate(10L);
  123 +
  124 + eventListener.onPlayerError(
  125 + new PlaybackException("BAD", null, PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED));
  126 + verify(mockCallbacks).onBufferingEnd();
  127 + verify(mockCallbacks).onError(eq("VideoError"), contains("BAD"), isNull());
  128 + }
  129 +
  130 + @Test
  131 + public void onErrorBehindLiveWindowSeekToDefaultAndPrepare() {
  132 + eventListener.onPlayerError(
  133 + new PlaybackException("SORT_OF_OK", null, PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW));
  134 +
  135 + verify(mockExoPlayer).seekToDefaultPosition();
  136 + verify(mockExoPlayer).prepare();
  137 + verifyNoInteractions(mockCallbacks);
  138 + }
  139 +
  140 + @Test
  141 + public void onIsPlayingChangedToggled() {
  142 + eventListener.onIsPlayingChanged(true);
  143 + verify(mockCallbacks).onIsPlayingStateUpdate(true);
  144 +
  145 + eventListener.onIsPlayingChanged(false);
  146 + verify(mockCallbacks).onIsPlayingStateUpdate(false);
  147 + }
  148 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/FakeVideoAsset.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import android.content.Context;
  8 +import androidx.annotation.NonNull;
  9 +import androidx.media3.common.MediaItem;
  10 +import androidx.media3.exoplayer.source.MediaSource;
  11 +import androidx.media3.test.utils.FakeMediaSourceFactory;
  12 +
  13 +/** A fake implementation of the {@link VideoAsset} class. */
  14 +final class FakeVideoAsset extends VideoAsset {
  15 + @NonNull private final MediaSource.Factory mediaSourceFactory;
  16 +
  17 + FakeVideoAsset(String assetUrl) {
  18 + this(assetUrl, new FakeMediaSourceFactory());
  19 + }
  20 +
  21 + FakeVideoAsset(String assetUrl, @NonNull MediaSource.Factory mediaSourceFactory) {
  22 + super(assetUrl);
  23 + this.mediaSourceFactory = mediaSourceFactory;
  24 + }
  25 +
  26 + @NonNull
  27 + @Override
  28 + public MediaItem getMediaItem() {
  29 + return new MediaItem.Builder().setUri(assetUrl).build();
  30 + }
  31 +
  32 + @NonNull
  33 + @Override
  34 + public MediaSource.Factory getMediaSourceFactory(@NonNull Context context) {
  35 + return mediaSourceFactory;
  36 + }
  37 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformVideoViewFactoryTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertTrue;
  8 +import static org.mockito.Mockito.*;
  9 +
  10 +import android.content.Context;
  11 +import androidx.media3.exoplayer.ExoPlayer;
  12 +import io.flutter.plugin.platform.PlatformView;
  13 +import io.flutter.plugins.videoplayer.platformview.PlatformVideoView;
  14 +import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory;
  15 +import org.junit.Test;
  16 +
  17 +public class PlatformVideoViewFactoryTest {
  18 + @Test
  19 + public void createsPlatformVideoViewBasedOnSuppliedArguments() {
  20 + final PlatformVideoViewFactory.VideoPlayerProvider videoPlayerProvider =
  21 + mock(PlatformVideoViewFactory.VideoPlayerProvider.class);
  22 + final VideoPlayer videoPlayer = mock(VideoPlayer.class);
  23 + final ExoPlayer exoPlayer = mock(ExoPlayer.class);
  24 + final Context context = mock(Context.class);
  25 + final long playerId = 1L;
  26 +
  27 + when(videoPlayerProvider.getVideoPlayer(playerId)).thenReturn(videoPlayer);
  28 + when(videoPlayer.getExoPlayer()).thenReturn(exoPlayer);
  29 +
  30 + final PlatformVideoViewFactory factory = new PlatformVideoViewFactory(videoPlayerProvider);
  31 + final Messages.PlatformVideoViewCreationParams args =
  32 + new Messages.PlatformVideoViewCreationParams.Builder().setPlayerId(playerId).build();
  33 +
  34 + final PlatformView view = factory.create(context, 0, args);
  35 +
  36 + assertTrue(view instanceof PlatformVideoView);
  37 + verify(videoPlayerProvider).getVideoPlayer(playerId);
  38 + verify(videoPlayer).getExoPlayer();
  39 + }
  40 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformVideoViewTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertNotNull;
  8 +import static org.mockito.Mockito.*;
  9 +
  10 +import android.content.Context;
  11 +import android.view.SurfaceView;
  12 +import androidx.media3.exoplayer.ExoPlayer;
  13 +import io.flutter.plugins.videoplayer.platformview.PlatformVideoView;
  14 +import java.lang.reflect.Field;
  15 +import org.junit.Test;
  16 +
  17 +/** Unit tests for {@link PlatformVideoViewTest}. */
  18 +public class PlatformVideoViewTest {
  19 + @Test
  20 + public void createsSurfaceViewAndSetsItForExoPlayer() throws Exception {
  21 + final Context mockContext = mock(Context.class);
  22 + final ExoPlayer mockExoPlayer = mock(ExoPlayer.class);
  23 +
  24 + final PlatformVideoView view = new PlatformVideoView(mockContext, mockExoPlayer);
  25 +
  26 + final Field field = PlatformVideoView.class.getDeclaredField("surfaceView");
  27 + field.setAccessible(true);
  28 + final SurfaceView surfaceView = (SurfaceView) field.get(view);
  29 +
  30 + assertNotNull(surfaceView);
  31 + verify(mockExoPlayer).setVideoSurfaceView(surfaceView);
  32 + }
  33 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/PlatformViewExoPlayerEventListenerTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.mockito.Mockito.verify;
  8 +import static org.mockito.Mockito.when;
  9 +
  10 +import androidx.media3.common.Format;
  11 +import androidx.media3.common.Player;
  12 +import androidx.media3.exoplayer.ExoPlayer;
  13 +import io.flutter.plugins.videoplayer.platformview.PlatformViewExoPlayerEventListener;
  14 +import org.junit.Before;
  15 +import org.junit.Rule;
  16 +import org.junit.Test;
  17 +import org.junit.runner.RunWith;
  18 +import org.mockito.Mock;
  19 +import org.mockito.junit.MockitoJUnit;
  20 +import org.mockito.junit.MockitoRule;
  21 +import org.robolectric.RobolectricTestRunner;
  22 +
  23 +/**
  24 + * Unit tests for {@link PlatformViewExoPlayerEventListener}.
  25 + *
  26 + * <p>This test suite <em>narrowly verifies</em> that the events emitted by the underlying {@link
  27 + * androidx.media3.exoplayer.ExoPlayer} instance are translated to the callback interface we expect
  28 + * ({@link VideoPlayerCallbacks} and/or interface with the player instance as expected).
  29 + */
  30 +@RunWith(RobolectricTestRunner.class)
  31 +public final class PlatformViewExoPlayerEventListenerTest {
  32 + @Mock private ExoPlayer mockExoPlayer;
  33 + @Mock private VideoPlayerCallbacks mockCallbacks;
  34 + private ExoPlayerEventListener eventListener;
  35 +
  36 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  37 +
  38 + @Before
  39 + public void setUp() {
  40 + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks);
  41 + }
  42 +
  43 + @Test
  44 + public void onPlaybackStateChangedReadySendInitialized() {
  45 + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks);
  46 +
  47 + Format format = new Format.Builder().setWidth(800).setHeight(400).build();
  48 + when(mockExoPlayer.getVideoFormat()).thenReturn(format);
  49 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  50 +
  51 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  52 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  53 + }
  54 +
  55 + @Test
  56 + public void onPlaybackStateChangedReadyInPortraitMode90DegreesSwapsWidthAndHeight() {
  57 + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks);
  58 +
  59 + Format format =
  60 + new Format.Builder().setWidth(800).setHeight(400).setRotationDegrees(90).build();
  61 + when(mockExoPlayer.getVideoFormat()).thenReturn(format);
  62 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  63 +
  64 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  65 + verify(mockCallbacks).onInitialized(400, 800, 10L, 0);
  66 + }
  67 +
  68 + @Test
  69 + public void onPlaybackStateChangedReadyInPortraitMode270DegreesSwapsWidthAndHeight() {
  70 + eventListener = new PlatformViewExoPlayerEventListener(mockExoPlayer, mockCallbacks);
  71 +
  72 + Format format =
  73 + new Format.Builder().setWidth(800).setHeight(400).setRotationDegrees(270).build();
  74 + when(mockExoPlayer.getVideoFormat()).thenReturn(format);
  75 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  76 +
  77 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  78 + verify(mockCallbacks).onInitialized(400, 800, 10L, 0);
  79 + }
  80 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureExoPlayerEventListenerTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.mockito.Mockito.verify;
  8 +import static org.mockito.Mockito.when;
  9 +
  10 +import androidx.media3.common.Format;
  11 +import androidx.media3.common.Player;
  12 +import androidx.media3.common.VideoSize;
  13 +import androidx.media3.exoplayer.ExoPlayer;
  14 +import io.flutter.plugins.videoplayer.texture.TextureExoPlayerEventListener;
  15 +import org.junit.Rule;
  16 +import org.junit.Test;
  17 +import org.junit.runner.RunWith;
  18 +import org.mockito.Mock;
  19 +import org.mockito.junit.MockitoJUnit;
  20 +import org.mockito.junit.MockitoRule;
  21 +import org.robolectric.RobolectricTestRunner;
  22 +import org.robolectric.annotation.Config;
  23 +
  24 +/**
  25 + * Unit tests for {@link TextureExoPlayerEventListener}.
  26 + *
  27 + * <p>This test suite <em>narrowly verifies</em> that the events emitted by the underlying {@link
  28 + * androidx.media3.exoplayer.ExoPlayer} instance are translated to the callback interface we expect
  29 + * ({@link VideoPlayerCallbacks} and/or interface with the player instance as expected).
  30 + */
  31 +@RunWith(RobolectricTestRunner.class)
  32 +public class TextureExoPlayerEventListenerTest {
  33 + @Mock private ExoPlayer mockExoPlayer;
  34 + @Mock private VideoPlayerCallbacks mockCallbacks;
  35 +
  36 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  37 +
  38 + @Test
  39 + @Config(maxSdk = 21)
  40 + public void onPlaybackStateChangedReadySendInitialized_belowAndroid21() {
  41 + TextureExoPlayerEventListener eventListener =
  42 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  43 + VideoSize size = new VideoSize(800, 400, 0, 0);
  44 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  45 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  46 +
  47 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  48 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  49 + }
  50 +
  51 + @Test
  52 + @Config(minSdk = 22)
  53 + public void
  54 + onPlaybackStateChangedReadySendInitialized_whenSurfaceProducerHandlesCropAndRotation() {
  55 + TextureExoPlayerEventListener eventListener =
  56 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  57 + VideoSize size = new VideoSize(800, 400, 0, 0);
  58 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  59 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  60 +
  61 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  62 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  63 + }
  64 +
  65 + @Test
  66 + @Config(minSdk = 22)
  67 + public void
  68 + onPlaybackStateChangedReadySendInitializedWithRotationCorrectionAndWidthAndHeightSwap_whenSurfaceProducerDoesNotHandleCropAndRotation() {
  69 + TextureExoPlayerEventListener eventListener =
  70 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false);
  71 + VideoSize size = new VideoSize(800, 400, 0, 0);
  72 + int rotationCorrection = 90;
  73 + Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build();
  74 +
  75 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  76 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  77 + when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat);
  78 +
  79 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  80 + verify(mockCallbacks).onInitialized(800, 400, 10L, rotationCorrection);
  81 + }
  82 +
  83 + @Test
  84 + @Config(maxSdk = 21)
  85 + public void
  86 + onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_belowAndroid21() {
  87 + TextureExoPlayerEventListener eventListener =
  88 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  89 + VideoSize size = new VideoSize(800, 400, 90, 0);
  90 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  91 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  92 +
  93 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  94 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  95 + }
  96 +
  97 + @Test
  98 + @Config(minSdk = 22)
  99 + public void
  100 + onPlaybackStateChangedReadyInPortraitMode90DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerHandlesCropAndRotation() {
  101 + TextureExoPlayerEventListener eventListener =
  102 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  103 + VideoSize size = new VideoSize(800, 400, 90, 0);
  104 +
  105 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  106 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  107 +
  108 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  109 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  110 + }
  111 +
  112 + @Test
  113 + @Config(minSdk = 22)
  114 + public void
  115 + onPlaybackStateChangedReadyInPortraitMode90DegreesSwapWidthAndHeight_whenSurfaceProducerDoesNotHandleCropAndRotation() {
  116 + TextureExoPlayerEventListener eventListener =
  117 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false);
  118 + VideoSize size = new VideoSize(800, 400, 0, 0);
  119 + int rotationCorrection = 90;
  120 + Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build();
  121 +
  122 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  123 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  124 + when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat);
  125 +
  126 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  127 + verify(mockCallbacks).onInitialized(800, 400, 10L, 90);
  128 + }
  129 +
  130 + @Test
  131 + @Config(maxSdk = 21)
  132 + public void
  133 + onPlaybackStateChangedReadyInPortraitMode270DegreesSwapWidthAndHeight_belowAndroid21() {
  134 + TextureExoPlayerEventListener eventListener =
  135 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  136 + VideoSize size = new VideoSize(800, 400, 270, 0);
  137 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  138 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  139 +
  140 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  141 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  142 + }
  143 +
  144 + @Test
  145 + @Config(minSdk = 22)
  146 + public void
  147 + onPlaybackStateChangedReadyInPortraitMode270DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerHandlesCropAndRotation() {
  148 + TextureExoPlayerEventListener eventListener =
  149 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  150 + VideoSize size = new VideoSize(800, 400, 270, 0);
  151 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  152 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  153 +
  154 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  155 + verify(mockCallbacks).onInitialized(800, 400, 10L, 0);
  156 + }
  157 +
  158 + @Test
  159 + @Config(minSdk = 22)
  160 + public void
  161 + onPlaybackStateChangedReadyInPortraitMode270DegreesDoesNotSwapWidthAndHeight_whenSurfaceProducerDoesNotHandleCropAndRotation() {
  162 + TextureExoPlayerEventListener eventListener =
  163 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, false);
  164 + VideoSize size = new VideoSize(800, 400, 0, 0);
  165 + int rotationCorrection = 270;
  166 + Format videoFormat = new Format.Builder().setRotationDegrees(rotationCorrection).build();
  167 +
  168 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  169 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  170 + when(mockExoPlayer.getVideoFormat()).thenReturn(videoFormat);
  171 +
  172 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  173 + verify(mockCallbacks).onInitialized(800, 400, 10L, 270);
  174 + }
  175 +
  176 + @Test
  177 + @Config(maxSdk = 21)
  178 + public void onPlaybackStateChangedReadyFlipped180DegreesInformEventHandler_belowAndroid21() {
  179 + TextureExoPlayerEventListener eventListener =
  180 + new TextureExoPlayerEventListener(mockExoPlayer, mockCallbacks, true);
  181 + VideoSize size = new VideoSize(800, 400, 180, 0);
  182 + when(mockExoPlayer.getVideoSize()).thenReturn(size);
  183 + when(mockExoPlayer.getDuration()).thenReturn(10L);
  184 +
  185 + eventListener.onPlaybackStateChanged(Player.STATE_READY);
  186 + verify(mockCallbacks).onInitialized(800, 400, 10L, 180);
  187 + }
  188 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/TextureVideoPlayerTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertEquals;
  8 +import static org.mockito.ArgumentMatchers.any;
  9 +import static org.mockito.Mockito.*;
  10 +
  11 +import android.view.Surface;
  12 +import androidx.media3.common.AudioAttributes;
  13 +import androidx.media3.common.C;
  14 +import androidx.media3.common.PlaybackParameters;
  15 +import androidx.media3.common.Player;
  16 +import androidx.media3.common.VideoSize;
  17 +import androidx.media3.exoplayer.ExoPlayer;
  18 +import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer;
  19 +import io.flutter.view.TextureRegistry;
  20 +import org.junit.Before;
  21 +import org.junit.Rule;
  22 +import org.junit.Test;
  23 +import org.junit.runner.RunWith;
  24 +import org.mockito.ArgumentCaptor;
  25 +import org.mockito.Captor;
  26 +import org.mockito.InOrder;
  27 +import org.mockito.Mock;
  28 +import org.mockito.junit.MockitoJUnit;
  29 +import org.mockito.junit.MockitoRule;
  30 +import org.robolectric.RobolectricTestRunner;
  31 +
  32 +/**
  33 + * Unit tests for {@link TextureVideoPlayer}.
  34 + *
  35 + * <p>This test suite <em>narrowly verifies</em> that {@link TextureVideoPlayer} interfaces with the
  36 + * {@link ExoPlayer} interface <em>exactly</em> as it did when the test suite was created. That is,
  37 + * if the behavior changes, this test will need to change. However, this suite should catch bugs
  38 + * related to <em>"this is a safe refactor with no behavior changes"</em>.
  39 + */
  40 +@RunWith(RobolectricTestRunner.class)
  41 +public final class TextureVideoPlayerTest {
  42 + private static final String FAKE_ASSET_URL = "https://flutter.dev/movie.mp4";
  43 + private FakeVideoAsset fakeVideoAsset;
  44 +
  45 + @Mock private VideoPlayerCallbacks mockEvents;
  46 + @Mock private TextureRegistry.SurfaceProducer mockProducer;
  47 + @Mock private ExoPlayer mockExoPlayer;
  48 + @Captor private ArgumentCaptor<AudioAttributes> attributesCaptor;
  49 + @Captor private ArgumentCaptor<TextureRegistry.SurfaceProducer.Callback> callbackCaptor;
  50 + @Captor private ArgumentCaptor<Player.Listener> listenerCaptor;
  51 +
  52 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  53 +
  54 + @Before
  55 + public void setUp() {
  56 + fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL);
  57 + when(mockProducer.getSurface()).thenReturn(mock(Surface.class));
  58 + when(mockProducer.handlesCropAndRotation()).thenReturn(true);
  59 + }
  60 +
  61 + private VideoPlayer createVideoPlayer() {
  62 + return createVideoPlayer(new VideoPlayerOptions());
  63 + }
  64 +
  65 + private TextureVideoPlayer createVideoPlayer(VideoPlayerOptions options) {
  66 + return new TextureVideoPlayer(
  67 + mockEvents, mockProducer, fakeVideoAsset.getMediaItem(), options, () -> mockExoPlayer);
  68 + }
  69 +
  70 + @Test
  71 + public void loadsAndPreparesProvidedMediaEnablesAudioFocusByDefault() {
  72 + VideoPlayer videoPlayer = createVideoPlayer();
  73 +
  74 + verify(mockExoPlayer).setMediaItem(fakeVideoAsset.getMediaItem());
  75 + verify(mockExoPlayer).prepare();
  76 + verify(mockProducer).getSurface();
  77 + verify(mockExoPlayer).setVideoSurface(any());
  78 +
  79 + verify(mockExoPlayer).setAudioAttributes(attributesCaptor.capture(), eq(true));
  80 + assertEquals(attributesCaptor.getValue().contentType, C.AUDIO_CONTENT_TYPE_MOVIE);
  81 +
  82 + videoPlayer.dispose();
  83 + }
  84 +
  85 + @Test
  86 + public void onSurfaceProducerDestroyedAndAvailableReleasesAndThenRecreatesAndResumesPlayer() {
  87 + VideoPlayer videoPlayer = createVideoPlayer();
  88 +
  89 + verify(mockProducer).setCallback(callbackCaptor.capture());
  90 + verify(mockExoPlayer, never()).release();
  91 +
  92 + when(mockExoPlayer.getCurrentPosition()).thenReturn(10L);
  93 + when(mockExoPlayer.getRepeatMode()).thenReturn(Player.REPEAT_MODE_ALL);
  94 + when(mockExoPlayer.getVolume()).thenReturn(0.5f);
  95 + when(mockExoPlayer.getPlaybackParameters()).thenReturn(new PlaybackParameters(2.5f));
  96 +
  97 + TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue();
  98 + simulateSurfaceDestruction(producerLifecycle);
  99 +
  100 + verify(mockExoPlayer).release();
  101 +
  102 + // Create a new mock exo player so that we get a new instance.
  103 + mockExoPlayer = mock(ExoPlayer.class);
  104 + producerLifecycle.onSurfaceAvailable();
  105 +
  106 + verify(mockExoPlayer).setVideoSurface(any());
  107 + verify(mockExoPlayer).seekTo(10L);
  108 + verify(mockExoPlayer).setRepeatMode(Player.REPEAT_MODE_ALL);
  109 + verify(mockExoPlayer).setVolume(0.5f);
  110 + verify(mockExoPlayer).setPlaybackParameters(new PlaybackParameters(2.5f));
  111 +
  112 + videoPlayer.dispose();
  113 + }
  114 +
  115 + @Test
  116 + public void onSurfaceProducerDestroyedDoesNotStopOrPauseVideo() {
  117 + VideoPlayer videoPlayer = createVideoPlayer();
  118 +
  119 + verify(mockProducer).setCallback(callbackCaptor.capture());
  120 + TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue();
  121 + simulateSurfaceDestruction(producerLifecycle);
  122 +
  123 + verify(mockExoPlayer, never()).stop();
  124 + verify(mockExoPlayer, never()).pause();
  125 + verify(mockExoPlayer, never()).setPlayWhenReady(anyBoolean());
  126 +
  127 + videoPlayer.dispose();
  128 + }
  129 +
  130 + @Test
  131 + public void onDisposeSurfaceProducerCallbackIsDisconnected() {
  132 + // Regression test for https://github.com/flutter/flutter/issues/156158.
  133 + VideoPlayer videoPlayer = createVideoPlayer();
  134 + verify(mockProducer).setCallback(any());
  135 +
  136 + videoPlayer.dispose();
  137 + verify(mockProducer).setCallback(null);
  138 + }
  139 +
  140 + @Test
  141 + public void onInitializedCalledWhenVideoPlayerInitiallyCreated() {
  142 + VideoPlayer videoPlayer = createVideoPlayer();
  143 +
  144 + // Pretend we have a video, and capture the registered event listener.
  145 + when(mockExoPlayer.getVideoSize()).thenReturn(new VideoSize(300, 200));
  146 + verify(mockExoPlayer).addListener(listenerCaptor.capture());
  147 + Player.Listener listener = listenerCaptor.getValue();
  148 +
  149 + // Trigger an event that would trigger onInitialized.
  150 + listener.onPlaybackStateChanged(Player.STATE_READY);
  151 + verify(mockEvents).onInitialized(anyInt(), anyInt(), anyLong(), anyInt());
  152 +
  153 + videoPlayer.dispose();
  154 + }
  155 +
  156 + @Test
  157 + public void onSurfaceAvailableDoesNotSendInitializeEventAgain() {
  158 + // The VideoPlayer contract assumes that the event "initialized" is sent exactly once
  159 + // (duplicate events cause an error to be thrown at the shared Dart layer). This test verifies
  160 + // that the onInitialized event is sent exactly once per player.
  161 + //
  162 + // Regression test for https://github.com/flutter/flutter/issues/154602.
  163 + VideoPlayer videoPlayer = createVideoPlayer();
  164 + when(mockExoPlayer.getVideoSize()).thenReturn(new VideoSize(300, 200));
  165 +
  166 + // Capture the lifecycle events so we can simulate onSurfaceAvailableDestroyed.
  167 + verify(mockProducer).setCallback(callbackCaptor.capture());
  168 + TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue();
  169 +
  170 + // Trigger destroyed/available.
  171 + simulateSurfaceDestruction(producerLifecycle);
  172 + producerLifecycle.onSurfaceAvailable();
  173 +
  174 + // Initial listener, and the new one from the resume.
  175 + verify(mockExoPlayer, times(2)).addListener(listenerCaptor.capture());
  176 + Player.Listener listener = listenerCaptor.getValue();
  177 +
  178 + // Now trigger that same event, which would happen in the case of a background/resume.
  179 + listener.onPlaybackStateChanged(Player.STATE_READY);
  180 +
  181 + // Was not called because it was a result of a background/resume.
  182 + verify(mockEvents, never()).onInitialized(anyInt(), anyInt(), anyLong(), anyInt());
  183 +
  184 + videoPlayer.dispose();
  185 + }
  186 +
  187 + @Test
  188 + public void onSurfaceAvailableWithoutDestroyDoesNotRecreate() {
  189 + // Initially create the video player, which creates the initial surface.
  190 + VideoPlayer videoPlayer = createVideoPlayer();
  191 + verify(mockProducer).getSurface();
  192 + verify(mockProducer).handlesCropAndRotation();
  193 +
  194 + // Capture the lifecycle events so we can simulate onSurfaceAvailable/Destroyed.
  195 + verify(mockProducer).setCallback(callbackCaptor.capture());
  196 + TextureRegistry.SurfaceProducer.Callback producerLifecycle = callbackCaptor.getValue();
  197 +
  198 + // Calling onSurfaceAvailable does not do anything, since the surface was never destroyed.
  199 + producerLifecycle.onSurfaceAvailable();
  200 + verifyNoMoreInteractions(mockProducer);
  201 +
  202 + videoPlayer.dispose();
  203 + }
  204 +
  205 + @Test
  206 + public void disposeReleasesExoPlayerBeforeTexture() {
  207 + VideoPlayer videoPlayer = createVideoPlayer();
  208 +
  209 + videoPlayer.dispose();
  210 +
  211 + // Regression test for https://github.com/flutter/flutter/issues/156158.
  212 + // The player must be destroyed before the surface it is writing to.
  213 + InOrder inOrder = inOrder(mockExoPlayer, mockProducer);
  214 + inOrder.verify(mockExoPlayer).release();
  215 + inOrder.verify(mockProducer).release();
  216 + }
  217 +
  218 + // TODO(matanlurey): Replace with inline calls to onSurfaceAvailable once
  219 + // available on stable; see https://github.com/flutter/flutter/issues/155131.
  220 + // This separate method only exists to scope the suppression.
  221 + @SuppressWarnings({"deprecation", "removal"})
  222 + void simulateSurfaceCreation(TextureRegistry.SurfaceProducer.Callback producerLifecycle) {
  223 + producerLifecycle.onSurfaceCreated();
  224 + }
  225 +
  226 + // TODO(bparrishMines): Replace with inline calls to onSurfaceCleanup once available on stable;
  227 + // see https://github.com/flutter/flutter/issues/16125. This separate method only exists to scope
  228 + // the suppression.
  229 + @SuppressWarnings({"deprecation", "removal"})
  230 + void simulateSurfaceDestruction(TextureRegistry.SurfaceProducer.Callback producerLifecycle) {
  231 + producerLifecycle.onSurfaceDestroyed();
  232 + }
  233 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoAssetTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertEquals;
  8 +import static org.junit.Assert.assertThrows;
  9 +import static org.mockito.ArgumentMatchers.anyBoolean;
  10 +import static org.mockito.ArgumentMatchers.anyMap;
  11 +import static org.mockito.ArgumentMatchers.anyString;
  12 +import static org.mockito.Mockito.mock;
  13 +import static org.mockito.Mockito.never;
  14 +import static org.mockito.Mockito.verify;
  15 +import static org.mockito.Mockito.when;
  16 +
  17 +import android.net.Uri;
  18 +import androidx.media3.common.MediaItem;
  19 +import androidx.media3.datasource.DefaultHttpDataSource;
  20 +import androidx.media3.exoplayer.source.MediaSource;
  21 +import androidx.test.core.app.ApplicationProvider;
  22 +import java.util.HashMap;
  23 +import java.util.Map;
  24 +import java.util.Objects;
  25 +import org.junit.Test;
  26 +import org.junit.runner.RunWith;
  27 +import org.robolectric.RobolectricTestRunner;
  28 +
  29 +/**
  30 + * Unit tests for {@link VideoAsset}.
  31 + *
  32 + * <p>This test suite <em>narrowly verifies</em> that the {@link VideoAsset} factory methods, {@link
  33 + * VideoAsset#fromRemoteUrl(String, VideoAsset.StreamingFormat, Map)} and {@link
  34 + * VideoAsset#fromAssetUrl(String)} follow the contract they have documented.
  35 + *
  36 + * <p>In other tests of the player, a fake asset is likely to be used.
  37 + */
  38 +@RunWith(RobolectricTestRunner.class)
  39 +public final class VideoAssetTest {
  40 + @Test
  41 + public void localVideoRequiresAssetUrl() {
  42 + assertThrows(
  43 + IllegalArgumentException.class,
  44 + () -> VideoAsset.fromAssetUrl("https://not.local/video.mp4"));
  45 + }
  46 +
  47 + @Test
  48 + public void localVideoCreatesMediaItem() {
  49 + VideoAsset asset = VideoAsset.fromAssetUrl("asset:///asset-key");
  50 + MediaItem mediaItem = asset.getMediaItem();
  51 +
  52 + assert mediaItem.localConfiguration != null;
  53 + assertEquals(mediaItem.localConfiguration.uri, Uri.parse("asset:///asset-key"));
  54 + }
  55 +
  56 + private static DefaultHttpDataSource.Factory mockHttpFactory() {
  57 + DefaultHttpDataSource.Factory httpFactory = mock(DefaultHttpDataSource.Factory.class);
  58 + when(httpFactory.setUserAgent(anyString())).thenReturn(httpFactory);
  59 + when(httpFactory.setAllowCrossProtocolRedirects(anyBoolean())).thenReturn(httpFactory);
  60 + when(httpFactory.setDefaultRequestProperties(anyMap())).thenReturn(httpFactory);
  61 + return httpFactory;
  62 + }
  63 +
  64 + @Test
  65 + public void remoteVideoByDefaultSetsUserAgentAndCrossProtocolRedirects() {
  66 + VideoAsset asset =
  67 + VideoAsset.fromRemoteUrl(
  68 + "https://flutter.dev/video.mp4", VideoAsset.StreamingFormat.UNKNOWN, new HashMap<>());
  69 +
  70 + DefaultHttpDataSource.Factory mockFactory = mockHttpFactory();
  71 +
  72 + // Cast to HttpVideoAsset to call a testing-only method to intercept calls.
  73 + ((HttpVideoAsset) asset)
  74 + .getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory);
  75 +
  76 + verify(mockFactory).setUserAgent("ExoPlayer");
  77 + verify(mockFactory).setAllowCrossProtocolRedirects(true);
  78 + verify(mockFactory, never()).setDefaultRequestProperties(anyMap());
  79 + }
  80 +
  81 + @Test
  82 + public void remoteVideoOverridesUserAgentIfProvided() {
  83 + Map<String, String> headers = new HashMap<>();
  84 + headers.put("User-Agent", "FantasticalVideoBot");
  85 +
  86 + VideoAsset asset =
  87 + VideoAsset.fromRemoteUrl(
  88 + "https://flutter.dev/video.mp4", VideoAsset.StreamingFormat.UNKNOWN, headers);
  89 +
  90 + DefaultHttpDataSource.Factory mockFactory = mockHttpFactory();
  91 +
  92 + // Cast to HttpVideoAsset to call a testing-only method to intercept calls.
  93 + ((HttpVideoAsset) asset)
  94 + .getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory);
  95 +
  96 + verify(mockFactory).setUserAgent("FantasticalVideoBot");
  97 + verify(mockFactory).setAllowCrossProtocolRedirects(true);
  98 + verify(mockFactory).setDefaultRequestProperties(headers);
  99 + }
  100 +
  101 + // This tests that without using the overrides we get a working, non-mocked object.
  102 + //
  103 + // I guess you could also start a local HTTP server, and try fetching with it, YMMV.
  104 + @Test
  105 + public void remoteVideoGetMediaSourceFactoryInProductionReturnsRealMediaSource() {
  106 + VideoAsset asset =
  107 + VideoAsset.fromRemoteUrl(
  108 + "https://flutter.dev/video.mp4", VideoAsset.StreamingFormat.UNKNOWN, new HashMap<>());
  109 +
  110 + MediaSource source =
  111 + asset
  112 + .getMediaSourceFactory(ApplicationProvider.getApplicationContext())
  113 + .createMediaSource(asset.getMediaItem());
  114 + assertEquals(
  115 + Uri.parse("https://flutter.dev/video.mp4"),
  116 + Objects.requireNonNull(source.getMediaItem().localConfiguration).uri);
  117 + }
  118 +
  119 + @Test
  120 + public void remoteVideoSetsAdditionalHttpHeadersIfProvided() {
  121 + Map<String, String> headers = new HashMap<>();
  122 + headers.put("X-Cache-Forever", "true");
  123 +
  124 + VideoAsset asset =
  125 + VideoAsset.fromRemoteUrl(
  126 + "https://flutter.dev/video.mp4", VideoAsset.StreamingFormat.UNKNOWN, headers);
  127 +
  128 + DefaultHttpDataSource.Factory mockFactory = mockHttpFactory();
  129 +
  130 + // Cast to HttpVideoAsset to call a testing-only method to intercept calls.
  131 + ((HttpVideoAsset) asset)
  132 + .getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory);
  133 +
  134 + verify(mockFactory).setUserAgent("ExoPlayer");
  135 + verify(mockFactory).setAllowCrossProtocolRedirects(true);
  136 + verify(mockFactory).setDefaultRequestProperties(headers);
  137 + }
  138 +
  139 + @Test
  140 + public void rtspVideoRequiresRtspUrl() {
  141 + assertThrows(
  142 + IllegalArgumentException.class, () -> VideoAsset.fromRtspUrl("https://not.rtsp/video.mp4"));
  143 + }
  144 +
  145 + @Test
  146 + public void rtspVideoCreatesMediaItem() {
  147 + VideoAsset asset = VideoAsset.fromRtspUrl("rtsp://test:pass@flutter.dev/stream");
  148 + MediaItem mediaItem = asset.getMediaItem();
  149 +
  150 + assert mediaItem.localConfiguration != null;
  151 + assertEquals(
  152 + mediaItem.localConfiguration.uri, Uri.parse("rtsp://test:pass@flutter.dev/stream"));
  153 + }
  154 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacksTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertEquals;
  8 +import static org.mockito.ArgumentMatchers.eq;
  9 +import static org.mockito.Mockito.verify;
  10 +
  11 +import java.util.Arrays;
  12 +import java.util.Collections;
  13 +import java.util.HashMap;
  14 +import java.util.Map;
  15 +import org.junit.Before;
  16 +import org.junit.Rule;
  17 +import org.junit.Test;
  18 +import org.junit.runner.RunWith;
  19 +import org.mockito.ArgumentCaptor;
  20 +import org.mockito.Captor;
  21 +import org.mockito.Mock;
  22 +import org.mockito.junit.MockitoJUnit;
  23 +import org.mockito.junit.MockitoRule;
  24 +import org.robolectric.RobolectricTestRunner;
  25 +
  26 +/**
  27 + * Unit tests {@link VideoPlayerEventCallbacks}.
  28 + *
  29 + * <p>This test suite <em>narrowly verifies</em> that calling the provided event callbacks, such as
  30 + * {@link VideoPlayerEventCallbacks#onBufferingUpdate(long)}, produces the expected data as an
  31 + * encoded {@link Map}.
  32 + *
  33 + * <p>In other words, this tests that "the Java-side of the event channel works as expected".
  34 + */
  35 +@RunWith(RobolectricTestRunner.class)
  36 +public final class VideoPlayerEventCallbacksTest {
  37 + private VideoPlayerEventCallbacks eventCallbacks;
  38 +
  39 + @Mock private QueuingEventSink mockEventSink;
  40 +
  41 + @Captor private ArgumentCaptor<Map<String, Object>> eventCaptor;
  42 +
  43 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  44 +
  45 + @Before
  46 + public void setUp() {
  47 + eventCallbacks = VideoPlayerEventCallbacks.withSink(mockEventSink);
  48 + }
  49 +
  50 + @Test
  51 + public void onInitializedSendsWidthHeightAndDuration() {
  52 + eventCallbacks.onInitialized(800, 400, 10L, 0);
  53 +
  54 + verify(mockEventSink).success(eventCaptor.capture());
  55 +
  56 + Map<String, Object> actual = eventCaptor.getValue();
  57 + Map<String, Object> expected = new HashMap<>();
  58 + expected.put("event", "initialized");
  59 + expected.put("duration", 10L);
  60 + expected.put("width", 800);
  61 + expected.put("height", 400);
  62 +
  63 + assertEquals(expected, actual);
  64 + }
  65 +
  66 + @Test
  67 + public void onInitializedIncludesRotationCorrectIfNonZero() {
  68 + eventCallbacks.onInitialized(800, 400, 10L, 180);
  69 +
  70 + verify(mockEventSink).success(eventCaptor.capture());
  71 +
  72 + Map<String, Object> actual = eventCaptor.getValue();
  73 + Map<String, Object> expected = new HashMap<>();
  74 + expected.put("event", "initialized");
  75 + expected.put("duration", 10L);
  76 + expected.put("width", 800);
  77 + expected.put("height", 400);
  78 + expected.put("rotationCorrection", 180);
  79 +
  80 + assertEquals(expected, actual);
  81 + }
  82 +
  83 + @Test
  84 + public void onBufferingStart() {
  85 + eventCallbacks.onBufferingStart();
  86 +
  87 + verify(mockEventSink).success(eventCaptor.capture());
  88 +
  89 + Map<String, Object> actual = eventCaptor.getValue();
  90 + Map<String, Object> expected = new HashMap<>();
  91 + expected.put("event", "bufferingStart");
  92 + assertEquals(expected, actual);
  93 + }
  94 +
  95 + @Test
  96 + public void onBufferingUpdateProvidesAListWithASingleRange() {
  97 + eventCallbacks.onBufferingUpdate(10L);
  98 +
  99 + verify(mockEventSink).success(eventCaptor.capture());
  100 +
  101 + Map<String, Object> actual = eventCaptor.getValue();
  102 + Map<String, Object> expected = new HashMap<>();
  103 + expected.put("event", "bufferingUpdate");
  104 + expected.put("values", Collections.singletonList(Arrays.asList(0, 10L)));
  105 + assertEquals(expected, actual);
  106 + }
  107 +
  108 + @Test
  109 + public void onBufferingEnd() {
  110 + eventCallbacks.onBufferingEnd();
  111 +
  112 + verify(mockEventSink).success(eventCaptor.capture());
  113 +
  114 + Map<String, Object> actual = eventCaptor.getValue();
  115 + Map<String, Object> expected = new HashMap<>();
  116 + expected.put("event", "bufferingEnd");
  117 + assertEquals(expected, actual);
  118 + }
  119 +
  120 + @Test
  121 + public void onCompleted() {
  122 + eventCallbacks.onCompleted();
  123 +
  124 + verify(mockEventSink).success(eventCaptor.capture());
  125 +
  126 + Map<String, Object> actual = eventCaptor.getValue();
  127 + Map<String, Object> expected = new HashMap<>();
  128 + expected.put("event", "completed");
  129 + assertEquals(expected, actual);
  130 + }
  131 +
  132 + @Test
  133 + public void onError() {
  134 + eventCallbacks.onError("code", "message", "details");
  135 +
  136 + verify(mockEventSink).error(eq("code"), eq("message"), eq("details"));
  137 + }
  138 +
  139 + @Test
  140 + public void onIsPlayingStateUpdate() {
  141 + eventCallbacks.onIsPlayingStateUpdate(true);
  142 +
  143 + verify(mockEventSink).success(eventCaptor.capture());
  144 +
  145 + Map<String, Object> actual = eventCaptor.getValue();
  146 + Map<String, Object> expected = new HashMap<>();
  147 + expected.put("event", "isPlayingStateUpdate");
  148 + expected.put("isPlaying", true);
  149 + assertEquals(expected, actual);
  150 + }
  151 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerPluginTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.*;
  8 +import static org.mockito.Mockito.*;
  9 +
  10 +import android.content.Context;
  11 +import android.util.LongSparseArray;
  12 +import io.flutter.embedding.engine.plugins.FlutterPlugin;
  13 +import io.flutter.plugin.platform.PlatformViewRegistry;
  14 +import io.flutter.plugins.videoplayer.Messages.CreateMessage;
  15 +import io.flutter.plugins.videoplayer.Messages.PlatformVideoViewType;
  16 +import io.flutter.plugins.videoplayer.platformview.PlatformVideoViewFactory;
  17 +import io.flutter.plugins.videoplayer.platformview.PlatformViewVideoPlayer;
  18 +import io.flutter.plugins.videoplayer.texture.TextureVideoPlayer;
  19 +import io.flutter.view.TextureRegistry;
  20 +import java.lang.reflect.Field;
  21 +import java.util.HashMap;
  22 +import org.junit.Before;
  23 +import org.junit.Test;
  24 +import org.junit.runner.RunWith;
  25 +import org.mockito.Mock;
  26 +import org.mockito.MockedStatic;
  27 +import org.mockito.MockitoAnnotations;
  28 +import org.robolectric.RobolectricTestRunner;
  29 +
  30 +@RunWith(RobolectricTestRunner.class)
  31 +public class VideoPlayerPluginTest {
  32 + @Mock private TextureRegistry mockTextureRegistry;
  33 + @Mock private TextureRegistry.SurfaceProducer mockSurfaceProducer;
  34 + @Mock private PlatformViewRegistry mockPlatformViewRegistry;
  35 + private VideoPlayerPlugin plugin;
  36 +
  37 + @Before
  38 + public void setUp() {
  39 + MockitoAnnotations.openMocks(this);
  40 + when(mockTextureRegistry.createSurfaceProducer()).thenReturn(mockSurfaceProducer);
  41 +
  42 + FlutterPlugin.FlutterPluginBinding binding = mock(FlutterPlugin.FlutterPluginBinding.class);
  43 + when(binding.getApplicationContext()).thenReturn(mock(Context.class));
  44 + when(binding.getTextureRegistry()).thenReturn(mockTextureRegistry);
  45 + when(binding.getBinaryMessenger())
  46 + .thenReturn(mock(io.flutter.plugin.common.BinaryMessenger.class));
  47 + when(binding.getPlatformViewRegistry()).thenReturn(mockPlatformViewRegistry);
  48 +
  49 + plugin = new VideoPlayerPlugin();
  50 + plugin.onAttachedToEngine(binding);
  51 + }
  52 +
  53 + @SuppressWarnings("unchecked")
  54 + private LongSparseArray<VideoPlayer> getVideoPlayers() throws Exception {
  55 + final Field field = VideoPlayerPlugin.class.getDeclaredField("videoPlayers");
  56 + field.setAccessible(true);
  57 + return (LongSparseArray<VideoPlayer>) field.get(plugin);
  58 + }
  59 +
  60 + // This is only a placeholder test and doesn't actually initialize the plugin.
  61 + @Test
  62 + public void initPluginDoesNotThrow() {
  63 + final VideoPlayerPlugin plugin = new VideoPlayerPlugin();
  64 + }
  65 +
  66 + @Test
  67 + public void registersPlatformVideoViewFactory() {
  68 + verify(mockPlatformViewRegistry)
  69 + .registerViewFactory(
  70 + eq("plugins.flutter.dev/video_player_android"), any(PlatformVideoViewFactory.class));
  71 + }
  72 +
  73 + @Test
  74 + public void createsPlatformViewVideoPlayer() throws Exception {
  75 + try (MockedStatic<PlatformViewVideoPlayer> mockedPlatformViewVideoPlayerStatic =
  76 + mockStatic(PlatformViewVideoPlayer.class)) {
  77 + mockedPlatformViewVideoPlayerStatic
  78 + .when(() -> PlatformViewVideoPlayer.create(any(), any(), any(), any()))
  79 + .thenReturn(mock(PlatformViewVideoPlayer.class));
  80 +
  81 + final CreateMessage createMessage =
  82 + new CreateMessage.Builder()
  83 + .setViewType(PlatformVideoViewType.PLATFORM_VIEW)
  84 + .setUri("https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4")
  85 + .setHttpHeaders(new HashMap<>())
  86 + .build();
  87 +
  88 + final long playerId = plugin.create(createMessage);
  89 +
  90 + final LongSparseArray<VideoPlayer> videoPlayers = getVideoPlayers();
  91 + assertNotNull(videoPlayers.get(playerId));
  92 + }
  93 + }
  94 +
  95 + @Test
  96 + public void createsTextureVideoPlayer() throws Exception {
  97 + try (MockedStatic<TextureVideoPlayer> mockedTextureVideoPlayerStatic =
  98 + mockStatic(TextureVideoPlayer.class)) {
  99 + mockedTextureVideoPlayerStatic
  100 + .when(() -> TextureVideoPlayer.create(any(), any(), any(), any(), any()))
  101 + .thenReturn(mock(TextureVideoPlayer.class));
  102 +
  103 + final CreateMessage createMessage =
  104 + new CreateMessage.Builder()
  105 + .setViewType(PlatformVideoViewType.TEXTURE_VIEW)
  106 + .setUri("https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4")
  107 + .setHttpHeaders(new HashMap<>())
  108 + .build();
  109 +
  110 + final long playerId = plugin.create(createMessage);
  111 +
  112 + final LongSparseArray<VideoPlayer> videoPlayers = getVideoPlayers();
  113 + assertTrue(videoPlayers.get(playerId) instanceof TextureVideoPlayer);
  114 + }
  115 + }
  116 +}
... ...
packages/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +package io.flutter.plugins.videoplayer;
  6 +
  7 +import static org.junit.Assert.assertEquals;
  8 +import static org.mockito.Mockito.*;
  9 +
  10 +import androidx.annotation.NonNull;
  11 +import androidx.annotation.Nullable;
  12 +import androidx.media3.common.AudioAttributes;
  13 +import androidx.media3.common.C;
  14 +import androidx.media3.common.Format;
  15 +import androidx.media3.common.MediaItem;
  16 +import androidx.media3.common.PlaybackParameters;
  17 +import androidx.media3.common.Player;
  18 +import androidx.media3.exoplayer.ExoPlayer;
  19 +import io.flutter.plugins.videoplayer.platformview.PlatformViewExoPlayerEventListener;
  20 +import io.flutter.view.TextureRegistry.SurfaceProducer;
  21 +import org.junit.Before;
  22 +import org.junit.Rule;
  23 +import org.junit.Test;
  24 +import org.junit.runner.RunWith;
  25 +import org.mockito.ArgumentCaptor;
  26 +import org.mockito.Captor;
  27 +import org.mockito.Mock;
  28 +import org.mockito.junit.MockitoJUnit;
  29 +import org.mockito.junit.MockitoRule;
  30 +import org.robolectric.RobolectricTestRunner;
  31 +
  32 +/**
  33 + * Unit tests for {@link VideoPlayer}.
  34 + *
  35 + * <p>This test suite <em>narrowly verifies</em> that {@link VideoPlayer} interfaces with the {@link
  36 + * ExoPlayer} interface <em>exactly</em> as it did when the test suite was created. That is, if the
  37 + * behavior changes, this test will need to change. However, this suite should catch bugs related to
  38 + * <em>"this is a safe refactor with no behavior changes"</em>.
  39 + *
  40 + * <p>It's hypothetically possible to write better tests using {@link
  41 + * androidx.media3.test.utils.FakeMediaSource}, but you really need a PhD in the Android media APIs
  42 + * in order to figure out how to set everything up so the player "works".
  43 + */
  44 +@RunWith(RobolectricTestRunner.class)
  45 +public final class VideoPlayerTest {
  46 + private static final String FAKE_ASSET_URL = "https://flutter.dev/movie.mp4";
  47 + private FakeVideoAsset fakeVideoAsset;
  48 +
  49 + @Mock private VideoPlayerCallbacks mockEvents;
  50 + @Mock private ExoPlayer mockExoPlayer;
  51 + @Captor private ArgumentCaptor<AudioAttributes> attributesCaptor;
  52 + @Captor private ArgumentCaptor<Player.Listener> listenerCaptor;
  53 +
  54 + @Rule public MockitoRule initRule = MockitoJUnit.rule();
  55 +
  56 + /** A test subclass of {@link VideoPlayer} that exposes the abstract class for testing. */
  57 + private final class TestVideoPlayer extends VideoPlayer {
  58 + private TestVideoPlayer(
  59 + @NonNull VideoPlayerCallbacks events,
  60 + @NonNull MediaItem mediaItem,
  61 + @NonNull VideoPlayerOptions options,
  62 + @Nullable SurfaceProducer surfaceProducer,
  63 + @NonNull ExoPlayerProvider exoPlayerProvider) {
  64 + super(events, mediaItem, options, surfaceProducer, exoPlayerProvider);
  65 + }
  66 +
  67 + @NonNull
  68 + @Override
  69 + protected ExoPlayerEventListener createExoPlayerEventListener(
  70 + @NonNull ExoPlayer exoPlayer, @Nullable SurfaceProducer surfaceProducer) {
  71 + // Use platform view implementation for testing.
  72 + return new PlatformViewExoPlayerEventListener(exoPlayer, mockEvents);
  73 + }
  74 + }
  75 +
  76 + @Before
  77 + public void setUp() {
  78 + fakeVideoAsset = new FakeVideoAsset(FAKE_ASSET_URL);
  79 + }
  80 +
  81 + private VideoPlayer createVideoPlayer() {
  82 + return createVideoPlayer(new VideoPlayerOptions());
  83 + }
  84 +
  85 + private VideoPlayer createVideoPlayer(VideoPlayerOptions options) {
  86 + return new TestVideoPlayer(
  87 + mockEvents, fakeVideoAsset.getMediaItem(), options, null, () -> mockExoPlayer);
  88 + }
  89 +
  90 + @Test
  91 + public void loadsAndPreparesProvidedMediaEnablesAudioFocusByDefault() {
  92 + VideoPlayer videoPlayer = createVideoPlayer();
  93 +
  94 + verify(mockExoPlayer).setMediaItem(fakeVideoAsset.getMediaItem());
  95 + verify(mockExoPlayer).prepare();
  96 +
  97 + verify(mockExoPlayer).setAudioAttributes(attributesCaptor.capture(), eq(true));
  98 + assertEquals(attributesCaptor.getValue().contentType, C.AUDIO_CONTENT_TYPE_MOVIE);
  99 +
  100 + videoPlayer.dispose();
  101 + }
  102 +
  103 + @Test
  104 + public void loadsAndPreparesProvidedMediaDisablesAudioFocusWhenMixModeSet() {
  105 + VideoPlayerOptions options = new VideoPlayerOptions();
  106 + options.mixWithOthers = true;
  107 +
  108 + VideoPlayer videoPlayer = createVideoPlayer(options);
  109 +
  110 + verify(mockExoPlayer).setAudioAttributes(attributesCaptor.capture(), eq(false));
  111 + assertEquals(attributesCaptor.getValue().contentType, C.AUDIO_CONTENT_TYPE_MOVIE);
  112 +
  113 + videoPlayer.dispose();
  114 + }
  115 +
  116 + @Test
  117 + public void playsAndPausesProvidedMedia() {
  118 + VideoPlayer videoPlayer = createVideoPlayer();
  119 +
  120 + videoPlayer.play();
  121 + verify(mockExoPlayer).play();
  122 +
  123 + videoPlayer.pause();
  124 + verify(mockExoPlayer).pause();
  125 +
  126 + videoPlayer.dispose();
  127 + }
  128 +
  129 + @Test
  130 + public void sendsBufferingUpdatesOnDemand() {
  131 + VideoPlayer videoPlayer = createVideoPlayer();
  132 +
  133 + when(mockExoPlayer.getBufferedPosition()).thenReturn(10L);
  134 + videoPlayer.sendBufferingUpdate();
  135 + verify(mockEvents).onBufferingUpdate(10L);
  136 +
  137 + videoPlayer.dispose();
  138 + }
  139 +
  140 + @Test
  141 + public void togglesLoopingEnablesAndDisablesRepeatMode() {
  142 + VideoPlayer videoPlayer = createVideoPlayer();
  143 +
  144 + videoPlayer.setLooping(true);
  145 + verify(mockExoPlayer).setRepeatMode(Player.REPEAT_MODE_ALL);
  146 +
  147 + videoPlayer.setLooping(false);
  148 + verify(mockExoPlayer).setRepeatMode(Player.REPEAT_MODE_OFF);
  149 +
  150 + videoPlayer.dispose();
  151 + }
  152 +
  153 + @Test
  154 + public void setVolumeIsClampedBetween0and1() {
  155 + VideoPlayer videoPlayer = createVideoPlayer();
  156 +
  157 + videoPlayer.setVolume(-1.0);
  158 + verify(mockExoPlayer).setVolume(0f);
  159 +
  160 + videoPlayer.setVolume(2.0);
  161 + verify(mockExoPlayer).setVolume(1f);
  162 +
  163 + videoPlayer.setVolume(0.5);
  164 + verify(mockExoPlayer).setVolume(0.5f);
  165 +
  166 + videoPlayer.dispose();
  167 + }
  168 +
  169 + @Test
  170 + public void setPlaybackSpeedSetsPlaybackParametersWithValue() {
  171 + VideoPlayer videoPlayer = createVideoPlayer();
  172 +
  173 + videoPlayer.setPlaybackSpeed(2.5);
  174 + verify(mockExoPlayer).setPlaybackParameters(new PlaybackParameters(2.5f));
  175 +
  176 + videoPlayer.dispose();
  177 + }
  178 +
  179 + @Test
  180 + public void seekAndGetPosition() {
  181 + VideoPlayer videoPlayer = createVideoPlayer();
  182 +
  183 + videoPlayer.seekTo(10);
  184 + verify(mockExoPlayer).seekTo(10);
  185 +
  186 + when(mockExoPlayer.getCurrentPosition()).thenReturn(20L);
  187 + assertEquals(20L, videoPlayer.getPosition());
  188 + }
  189 +
  190 + @Test
  191 + public void onInitializedCalledWhenVideoPlayerInitiallyAvailable() {
  192 + VideoPlayer videoPlayer = createVideoPlayer();
  193 +
  194 + // Pretend we have a video, and capture the registered event listener.
  195 + when(mockExoPlayer.getVideoFormat())
  196 + .thenReturn(
  197 + new Format.Builder().setWidth(300).setHeight(200).setRotationDegrees(0).build());
  198 + verify(mockExoPlayer).addListener(listenerCaptor.capture());
  199 + Player.Listener listener = listenerCaptor.getValue();
  200 +
  201 + // Trigger an event that would trigger onInitialized.
  202 + listener.onPlaybackStateChanged(Player.STATE_READY);
  203 + verify(mockEvents).onInitialized(anyInt(), anyInt(), anyLong(), anyInt());
  204 +
  205 + videoPlayer.dispose();
  206 + }
  207 +
  208 + @Test
  209 + public void disposeReleasesExoPlayer() {
  210 + VideoPlayer videoPlayer = createVideoPlayer();
  211 +
  212 + videoPlayer.dispose();
  213 +
  214 + verify(mockExoPlayer).release();
  215 + }
  216 +}
... ...
packages/video_player_android/lib/src/android_video_player.dart 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +import 'dart:async';
  6 +
  7 +import 'package:flutter/services.dart';
  8 +import 'package:flutter/widgets.dart';
  9 +import 'package:video_player_platform_interface/video_player_platform_interface.dart';
  10 +
  11 +import 'messages.g.dart';
  12 +import 'platform_view_player.dart';
  13 +
  14 +/// An Android implementation of [VideoPlayerPlatform] that uses the
  15 +/// Pigeon-generated [VideoPlayerApi].
  16 +class AndroidVideoPlayer extends VideoPlayerPlatform {
  17 + final AndroidVideoPlayerApi _api = AndroidVideoPlayerApi();
  18 +
  19 + /// A map that associates player ID with a view state.
  20 + /// This is used to determine which view type to use when building a view.
  21 + final Map<int, _VideoPlayerViewState> _playerViewStates =
  22 + <int, _VideoPlayerViewState>{};
  23 +
  24 + /// Registers this class as the default instance of [PathProviderPlatform].
  25 + static void registerWith() {
  26 + VideoPlayerPlatform.instance = AndroidVideoPlayer();
  27 + }
  28 +
  29 + @override
  30 + Future<void> init() {
  31 + return _api.initialize();
  32 + }
  33 +
  34 + @override
  35 + Future<void> dispose(int playerId) async {
  36 + await _api.dispose(playerId);
  37 + _playerViewStates.remove(playerId);
  38 + }
  39 +
  40 + @override
  41 + Future<int?> create(DataSource dataSource) {
  42 + return createWithOptions(
  43 + VideoCreationOptions(
  44 + dataSource: dataSource,
  45 + // Compatibility; "create" is always a textureView (createWithOptions
  46 + // allows selecting).
  47 + viewType: VideoViewType.textureView,
  48 + ),
  49 + );
  50 + }
  51 +
  52 + @override
  53 + Future<int?> createWithOptions(VideoCreationOptions options) async {
  54 + final DataSource dataSource = options.dataSource;
  55 +
  56 + String? asset;
  57 + String? packageName;
  58 + String? uri;
  59 + String? formatHint;
  60 + Map<String, String> httpHeaders = <String, String>{};
  61 + switch (dataSource.sourceType) {
  62 + case DataSourceType.asset:
  63 + asset = dataSource.asset;
  64 + packageName = dataSource.package;
  65 + case DataSourceType.network:
  66 + uri = dataSource.uri;
  67 + formatHint = _videoFormatStringMap[dataSource.formatHint];
  68 + httpHeaders = dataSource.httpHeaders;
  69 + case DataSourceType.file:
  70 + uri = dataSource.uri;
  71 + httpHeaders = dataSource.httpHeaders;
  72 + case DataSourceType.contentUri:
  73 + uri = dataSource.uri;
  74 + }
  75 + final CreateMessage message = CreateMessage(
  76 + asset: asset,
  77 + packageName: packageName,
  78 + uri: uri,
  79 + httpHeaders: httpHeaders,
  80 + formatHint: formatHint,
  81 + viewType: _platformVideoViewTypeFromVideoViewType(options.viewType),
  82 + );
  83 +
  84 + final int playerId = await _api.create(message);
  85 + _playerViewStates[playerId] = switch (options.viewType) {
  86 + // playerId is also the textureId when using texture view.
  87 + VideoViewType.textureView =>
  88 + _VideoPlayerTextureViewState(textureId: playerId),
  89 + VideoViewType.platformView => const _VideoPlayerPlatformViewState(),
  90 + };
  91 +
  92 + return playerId;
  93 + }
  94 +
  95 + @override
  96 + Future<void> setLooping(int playerId, bool looping) {
  97 + return _api.setLooping(playerId, looping);
  98 + }
  99 +
  100 + @override
  101 + Future<void> play(int playerId) {
  102 + return _api.play(playerId);
  103 + }
  104 +
  105 + @override
  106 + Future<void> pause(int playerId) {
  107 + return _api.pause(playerId);
  108 + }
  109 +
  110 + @override
  111 + Future<void> setVolume(int playerId, double volume) {
  112 + return _api.setVolume(playerId, volume);
  113 + }
  114 +
  115 + @override
  116 + Future<void> setPlaybackSpeed(int playerId, double speed) {
  117 + assert(speed > 0);
  118 +
  119 + return _api.setPlaybackSpeed(playerId, speed);
  120 + }
  121 +
  122 + @override
  123 + Future<void> seekTo(int playerId, Duration position) {
  124 + return _api.seekTo(playerId, position.inMilliseconds);
  125 + }
  126 +
  127 + @override
  128 + Future<Duration> getPosition(int playerId) async {
  129 + final int position = await _api.position(playerId);
  130 + return Duration(milliseconds: position);
  131 + }
  132 +
  133 + @override
  134 + Stream<VideoEvent> videoEventsFor(int playerId) {
  135 + return _eventChannelFor(playerId)
  136 + .receiveBroadcastStream()
  137 + .map((dynamic event) {
  138 + final Map<dynamic, dynamic> map = event as Map<dynamic, dynamic>;
  139 + switch (map['event']) {
  140 + case 'initialized':
  141 + return VideoEvent(
  142 + eventType: VideoEventType.initialized,
  143 + duration: Duration(milliseconds: map['duration'] as int),
  144 + size: Size((map['width'] as num?)?.toDouble() ?? 0.0,
  145 + (map['height'] as num?)?.toDouble() ?? 0.0),
  146 + rotationCorrection: map['rotationCorrection'] as int? ?? 0,
  147 + );
  148 + case 'completed':
  149 + return VideoEvent(
  150 + eventType: VideoEventType.completed,
  151 + );
  152 + case 'bufferingUpdate':
  153 + final List<dynamic> values = map['values'] as List<dynamic>;
  154 +
  155 + return VideoEvent(
  156 + buffered: values.map<DurationRange>(_toDurationRange).toList(),
  157 + eventType: VideoEventType.bufferingUpdate,
  158 + );
  159 + case 'bufferingStart':
  160 + return VideoEvent(eventType: VideoEventType.bufferingStart);
  161 + case 'bufferingEnd':
  162 + return VideoEvent(eventType: VideoEventType.bufferingEnd);
  163 + case 'isPlayingStateUpdate':
  164 + return VideoEvent(
  165 + eventType: VideoEventType.isPlayingStateUpdate,
  166 + isPlaying: map['isPlaying'] as bool,
  167 + );
  168 + default:
  169 + return VideoEvent(eventType: VideoEventType.unknown);
  170 + }
  171 + });
  172 + }
  173 +
  174 + @override
  175 + Widget buildView(int playerId) {
  176 + return buildViewWithOptions(
  177 + VideoViewOptions(playerId: playerId),
  178 + );
  179 + }
  180 +
  181 + @override
  182 + Widget buildViewWithOptions(VideoViewOptions options) {
  183 + final int playerId = options.playerId;
  184 + final _VideoPlayerViewState? viewState = _playerViewStates[playerId];
  185 +
  186 + return switch (viewState) {
  187 + _VideoPlayerTextureViewState(:final int textureId) =>
  188 + Texture(textureId: textureId),
  189 + _VideoPlayerPlatformViewState() => PlatformViewPlayer(playerId: playerId),
  190 + null => throw Exception(
  191 + 'Could not find corresponding view type for playerId: $playerId',
  192 + ),
  193 + };
  194 + }
  195 +
  196 + @override
  197 + Future<void> setMixWithOthers(bool mixWithOthers) {
  198 + return _api.setMixWithOthers(mixWithOthers);
  199 + }
  200 +
  201 + EventChannel _eventChannelFor(int playerId) {
  202 + return EventChannel('flutter.io/videoPlayer/videoEvents$playerId');
  203 + }
  204 +
  205 + static const Map<VideoFormat, String> _videoFormatStringMap =
  206 + <VideoFormat, String>{
  207 + VideoFormat.ss: 'ss',
  208 + VideoFormat.hls: 'hls',
  209 + VideoFormat.dash: 'dash',
  210 + VideoFormat.other: 'other',
  211 + };
  212 +
  213 + DurationRange _toDurationRange(dynamic value) {
  214 + final List<dynamic> pair = value as List<dynamic>;
  215 + return DurationRange(
  216 + Duration(milliseconds: pair[0] as int),
  217 + Duration(milliseconds: pair[1] as int),
  218 + );
  219 + }
  220 +}
  221 +
  222 +PlatformVideoViewType _platformVideoViewTypeFromVideoViewType(
  223 + VideoViewType viewType,
  224 +) {
  225 + return switch (viewType) {
  226 + VideoViewType.textureView => PlatformVideoViewType.textureView,
  227 + VideoViewType.platformView => PlatformVideoViewType.platformView,
  228 + };
  229 +}
  230 +
  231 +/// Base class representing the state of a video player view.
  232 +@immutable
  233 +sealed class _VideoPlayerViewState {
  234 + const _VideoPlayerViewState();
  235 +}
  236 +
  237 +/// Represents the state of a video player view that uses a texture.
  238 +final class _VideoPlayerTextureViewState extends _VideoPlayerViewState {
  239 + /// Creates a new instance of [_VideoPlayerTextureViewState].
  240 + const _VideoPlayerTextureViewState({
  241 + required this.textureId,
  242 + });
  243 +
  244 + /// The ID of the texture used by the video player.
  245 + final int textureId;
  246 +}
  247 +
  248 +/// Represents the state of a video player view that uses a platform view.
  249 +final class _VideoPlayerPlatformViewState extends _VideoPlayerViewState {
  250 + /// Creates a new instance of [_VideoPlayerPlatformViewState].
  251 + const _VideoPlayerPlatformViewState();
  252 +}
... ...
packages/video_player_android/lib/src/messages.g.dart 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +// Autogenerated from Pigeon (v22.6.1), do not edit directly.
  5 +// See also: https://pub.dev/packages/pigeon
  6 +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
  7 +
  8 +import 'dart:async';
  9 +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
  10 +
  11 +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
  12 +import 'package:flutter/services.dart';
  13 +
  14 +PlatformException _createConnectionError(String channelName) {
  15 + return PlatformException(
  16 + code: 'channel-error',
  17 + message: 'Unable to establish connection on channel: "$channelName".',
  18 + );
  19 +}
  20 +
  21 +List<Object?> wrapResponse(
  22 + {Object? result, PlatformException? error, bool empty = false}) {
  23 + if (empty) {
  24 + return <Object?>[];
  25 + }
  26 + if (error == null) {
  27 + return <Object?>[result];
  28 + }
  29 + return <Object?>[error.code, error.message, error.details];
  30 +}
  31 +
  32 +/// Pigeon equivalent of VideoViewType.
  33 +enum PlatformVideoViewType {
  34 + textureView,
  35 + platformView,
  36 +}
  37 +
  38 +/// Information passed to the platform view creation.
  39 +class PlatformVideoViewCreationParams {
  40 + PlatformVideoViewCreationParams({
  41 + required this.playerId,
  42 + });
  43 +
  44 + int playerId;
  45 +
  46 + Object encode() {
  47 + return <Object?>[
  48 + playerId,
  49 + ];
  50 + }
  51 +
  52 + static PlatformVideoViewCreationParams decode(Object result) {
  53 + result as List<Object?>;
  54 + return PlatformVideoViewCreationParams(
  55 + playerId: result[0]! as int,
  56 + );
  57 + }
  58 +}
  59 +
  60 +class CreateMessage {
  61 + CreateMessage({
  62 + this.asset,
  63 + this.uri,
  64 + this.packageName,
  65 + this.formatHint,
  66 + required this.httpHeaders,
  67 + this.viewType,
  68 + });
  69 +
  70 + String? asset;
  71 +
  72 + String? uri;
  73 +
  74 + String? packageName;
  75 +
  76 + String? formatHint;
  77 +
  78 + Map<String, String> httpHeaders;
  79 +
  80 + PlatformVideoViewType? viewType;
  81 +
  82 + Object encode() {
  83 + return <Object?>[
  84 + asset,
  85 + uri,
  86 + packageName,
  87 + formatHint,
  88 + httpHeaders,
  89 + viewType,
  90 + ];
  91 + }
  92 +
  93 + static CreateMessage decode(Object result) {
  94 + result as List<Object?>;
  95 + return CreateMessage(
  96 + asset: result[0] as String?,
  97 + uri: result[1] as String?,
  98 + packageName: result[2] as String?,
  99 + formatHint: result[3] as String?,
  100 + httpHeaders:
  101 + (result[4] as Map<Object?, Object?>?)!.cast<String, String>(),
  102 + viewType: result[5] as PlatformVideoViewType?,
  103 + );
  104 + }
  105 +}
  106 +
  107 +class _PigeonCodec extends StandardMessageCodec {
  108 + const _PigeonCodec();
  109 + @override
  110 + void writeValue(WriteBuffer buffer, Object? value) {
  111 + if (value is int) {
  112 + buffer.putUint8(4);
  113 + buffer.putInt64(value);
  114 + } else if (value is PlatformVideoViewType) {
  115 + buffer.putUint8(129);
  116 + writeValue(buffer, value.index);
  117 + } else if (value is PlatformVideoViewCreationParams) {
  118 + buffer.putUint8(130);
  119 + writeValue(buffer, value.encode());
  120 + } else if (value is CreateMessage) {
  121 + buffer.putUint8(131);
  122 + writeValue(buffer, value.encode());
  123 + } else {
  124 + super.writeValue(buffer, value);
  125 + }
  126 + }
  127 +
  128 + @override
  129 + Object? readValueOfType(int type, ReadBuffer buffer) {
  130 + switch (type) {
  131 + case 129:
  132 + final int? value = readValue(buffer) as int?;
  133 + return value == null ? null : PlatformVideoViewType.values[value];
  134 + case 130:
  135 + return PlatformVideoViewCreationParams.decode(readValue(buffer)!);
  136 + case 131:
  137 + return CreateMessage.decode(readValue(buffer)!);
  138 + default:
  139 + return super.readValueOfType(type, buffer);
  140 + }
  141 + }
  142 +}
  143 +
  144 +class AndroidVideoPlayerApi {
  145 + /// Constructor for [AndroidVideoPlayerApi]. The [binaryMessenger] named argument is
  146 + /// available for dependency injection. If it is left null, the default
  147 + /// BinaryMessenger will be used which routes to the host platform.
  148 + AndroidVideoPlayerApi(
  149 + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
  150 + : pigeonVar_binaryMessenger = binaryMessenger,
  151 + pigeonVar_messageChannelSuffix =
  152 + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
  153 + final BinaryMessenger? pigeonVar_binaryMessenger;
  154 +
  155 + static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
  156 +
  157 + final String pigeonVar_messageChannelSuffix;
  158 +
  159 + Future<void> initialize() async {
  160 + final String pigeonVar_channelName =
  161 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix';
  162 + final BasicMessageChannel<Object?> pigeonVar_channel =
  163 + BasicMessageChannel<Object?>(
  164 + pigeonVar_channelName,
  165 + pigeonChannelCodec,
  166 + binaryMessenger: pigeonVar_binaryMessenger,
  167 + );
  168 + final List<Object?>? pigeonVar_replyList =
  169 + await pigeonVar_channel.send(null) as List<Object?>?;
  170 + if (pigeonVar_replyList == null) {
  171 + throw _createConnectionError(pigeonVar_channelName);
  172 + } else if (pigeonVar_replyList.length > 1) {
  173 + throw PlatformException(
  174 + code: pigeonVar_replyList[0]! as String,
  175 + message: pigeonVar_replyList[1] as String?,
  176 + details: pigeonVar_replyList[2],
  177 + );
  178 + } else {
  179 + return;
  180 + }
  181 + }
  182 +
  183 + Future<int> create(CreateMessage msg) async {
  184 + final String pigeonVar_channelName =
  185 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.create$pigeonVar_messageChannelSuffix';
  186 + final BasicMessageChannel<Object?> pigeonVar_channel =
  187 + BasicMessageChannel<Object?>(
  188 + pigeonVar_channelName,
  189 + pigeonChannelCodec,
  190 + binaryMessenger: pigeonVar_binaryMessenger,
  191 + );
  192 + final List<Object?>? pigeonVar_replyList =
  193 + await pigeonVar_channel.send(<Object?>[msg]) as List<Object?>?;
  194 + if (pigeonVar_replyList == null) {
  195 + throw _createConnectionError(pigeonVar_channelName);
  196 + } else if (pigeonVar_replyList.length > 1) {
  197 + throw PlatformException(
  198 + code: pigeonVar_replyList[0]! as String,
  199 + message: pigeonVar_replyList[1] as String?,
  200 + details: pigeonVar_replyList[2],
  201 + );
  202 + } else if (pigeonVar_replyList[0] == null) {
  203 + throw PlatformException(
  204 + code: 'null-error',
  205 + message: 'Host platform returned null value for non-null return value.',
  206 + );
  207 + } else {
  208 + return (pigeonVar_replyList[0] as int?)!;
  209 + }
  210 + }
  211 +
  212 + Future<void> dispose(int playerId) async {
  213 + final String pigeonVar_channelName =
  214 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.dispose$pigeonVar_messageChannelSuffix';
  215 + final BasicMessageChannel<Object?> pigeonVar_channel =
  216 + BasicMessageChannel<Object?>(
  217 + pigeonVar_channelName,
  218 + pigeonChannelCodec,
  219 + binaryMessenger: pigeonVar_binaryMessenger,
  220 + );
  221 + final List<Object?>? pigeonVar_replyList =
  222 + await pigeonVar_channel.send(<Object?>[playerId]) as List<Object?>?;
  223 + if (pigeonVar_replyList == null) {
  224 + throw _createConnectionError(pigeonVar_channelName);
  225 + } else if (pigeonVar_replyList.length > 1) {
  226 + throw PlatformException(
  227 + code: pigeonVar_replyList[0]! as String,
  228 + message: pigeonVar_replyList[1] as String?,
  229 + details: pigeonVar_replyList[2],
  230 + );
  231 + } else {
  232 + return;
  233 + }
  234 + }
  235 +
  236 + Future<void> setLooping(int playerId, bool looping) async {
  237 + final String pigeonVar_channelName =
  238 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setLooping$pigeonVar_messageChannelSuffix';
  239 + final BasicMessageChannel<Object?> pigeonVar_channel =
  240 + BasicMessageChannel<Object?>(
  241 + pigeonVar_channelName,
  242 + pigeonChannelCodec,
  243 + binaryMessenger: pigeonVar_binaryMessenger,
  244 + );
  245 + final List<Object?>? pigeonVar_replyList = await pigeonVar_channel
  246 + .send(<Object?>[playerId, looping]) as List<Object?>?;
  247 + if (pigeonVar_replyList == null) {
  248 + throw _createConnectionError(pigeonVar_channelName);
  249 + } else if (pigeonVar_replyList.length > 1) {
  250 + throw PlatformException(
  251 + code: pigeonVar_replyList[0]! as String,
  252 + message: pigeonVar_replyList[1] as String?,
  253 + details: pigeonVar_replyList[2],
  254 + );
  255 + } else {
  256 + return;
  257 + }
  258 + }
  259 +
  260 + Future<void> setVolume(int playerId, double volume) async {
  261 + final String pigeonVar_channelName =
  262 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setVolume$pigeonVar_messageChannelSuffix';
  263 + final BasicMessageChannel<Object?> pigeonVar_channel =
  264 + BasicMessageChannel<Object?>(
  265 + pigeonVar_channelName,
  266 + pigeonChannelCodec,
  267 + binaryMessenger: pigeonVar_binaryMessenger,
  268 + );
  269 + final List<Object?>? pigeonVar_replyList = await pigeonVar_channel
  270 + .send(<Object?>[playerId, volume]) as List<Object?>?;
  271 + if (pigeonVar_replyList == null) {
  272 + throw _createConnectionError(pigeonVar_channelName);
  273 + } else if (pigeonVar_replyList.length > 1) {
  274 + throw PlatformException(
  275 + code: pigeonVar_replyList[0]! as String,
  276 + message: pigeonVar_replyList[1] as String?,
  277 + details: pigeonVar_replyList[2],
  278 + );
  279 + } else {
  280 + return;
  281 + }
  282 + }
  283 +
  284 + Future<void> setPlaybackSpeed(int playerId, double speed) async {
  285 + final String pigeonVar_channelName =
  286 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix';
  287 + final BasicMessageChannel<Object?> pigeonVar_channel =
  288 + BasicMessageChannel<Object?>(
  289 + pigeonVar_channelName,
  290 + pigeonChannelCodec,
  291 + binaryMessenger: pigeonVar_binaryMessenger,
  292 + );
  293 + final List<Object?>? pigeonVar_replyList = await pigeonVar_channel
  294 + .send(<Object?>[playerId, speed]) as List<Object?>?;
  295 + if (pigeonVar_replyList == null) {
  296 + throw _createConnectionError(pigeonVar_channelName);
  297 + } else if (pigeonVar_replyList.length > 1) {
  298 + throw PlatformException(
  299 + code: pigeonVar_replyList[0]! as String,
  300 + message: pigeonVar_replyList[1] as String?,
  301 + details: pigeonVar_replyList[2],
  302 + );
  303 + } else {
  304 + return;
  305 + }
  306 + }
  307 +
  308 + Future<void> play(int playerId) async {
  309 + final String pigeonVar_channelName =
  310 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.play$pigeonVar_messageChannelSuffix';
  311 + final BasicMessageChannel<Object?> pigeonVar_channel =
  312 + BasicMessageChannel<Object?>(
  313 + pigeonVar_channelName,
  314 + pigeonChannelCodec,
  315 + binaryMessenger: pigeonVar_binaryMessenger,
  316 + );
  317 + final List<Object?>? pigeonVar_replyList =
  318 + await pigeonVar_channel.send(<Object?>[playerId]) as List<Object?>?;
  319 + if (pigeonVar_replyList == null) {
  320 + throw _createConnectionError(pigeonVar_channelName);
  321 + } else if (pigeonVar_replyList.length > 1) {
  322 + throw PlatformException(
  323 + code: pigeonVar_replyList[0]! as String,
  324 + message: pigeonVar_replyList[1] as String?,
  325 + details: pigeonVar_replyList[2],
  326 + );
  327 + } else {
  328 + return;
  329 + }
  330 + }
  331 +
  332 + Future<int> position(int playerId) async {
  333 + final String pigeonVar_channelName =
  334 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.position$pigeonVar_messageChannelSuffix';
  335 + final BasicMessageChannel<Object?> pigeonVar_channel =
  336 + BasicMessageChannel<Object?>(
  337 + pigeonVar_channelName,
  338 + pigeonChannelCodec,
  339 + binaryMessenger: pigeonVar_binaryMessenger,
  340 + );
  341 + final List<Object?>? pigeonVar_replyList =
  342 + await pigeonVar_channel.send(<Object?>[playerId]) as List<Object?>?;
  343 + if (pigeonVar_replyList == null) {
  344 + throw _createConnectionError(pigeonVar_channelName);
  345 + } else if (pigeonVar_replyList.length > 1) {
  346 + throw PlatformException(
  347 + code: pigeonVar_replyList[0]! as String,
  348 + message: pigeonVar_replyList[1] as String?,
  349 + details: pigeonVar_replyList[2],
  350 + );
  351 + } else if (pigeonVar_replyList[0] == null) {
  352 + throw PlatformException(
  353 + code: 'null-error',
  354 + message: 'Host platform returned null value for non-null return value.',
  355 + );
  356 + } else {
  357 + return (pigeonVar_replyList[0] as int?)!;
  358 + }
  359 + }
  360 +
  361 + Future<void> seekTo(int playerId, int position) async {
  362 + final String pigeonVar_channelName =
  363 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.seekTo$pigeonVar_messageChannelSuffix';
  364 + final BasicMessageChannel<Object?> pigeonVar_channel =
  365 + BasicMessageChannel<Object?>(
  366 + pigeonVar_channelName,
  367 + pigeonChannelCodec,
  368 + binaryMessenger: pigeonVar_binaryMessenger,
  369 + );
  370 + final List<Object?>? pigeonVar_replyList = await pigeonVar_channel
  371 + .send(<Object?>[playerId, position]) as List<Object?>?;
  372 + if (pigeonVar_replyList == null) {
  373 + throw _createConnectionError(pigeonVar_channelName);
  374 + } else if (pigeonVar_replyList.length > 1) {
  375 + throw PlatformException(
  376 + code: pigeonVar_replyList[0]! as String,
  377 + message: pigeonVar_replyList[1] as String?,
  378 + details: pigeonVar_replyList[2],
  379 + );
  380 + } else {
  381 + return;
  382 + }
  383 + }
  384 +
  385 + Future<void> pause(int playerId) async {
  386 + final String pigeonVar_channelName =
  387 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.pause$pigeonVar_messageChannelSuffix';
  388 + final BasicMessageChannel<Object?> pigeonVar_channel =
  389 + BasicMessageChannel<Object?>(
  390 + pigeonVar_channelName,
  391 + pigeonChannelCodec,
  392 + binaryMessenger: pigeonVar_binaryMessenger,
  393 + );
  394 + final List<Object?>? pigeonVar_replyList =
  395 + await pigeonVar_channel.send(<Object?>[playerId]) as List<Object?>?;
  396 + if (pigeonVar_replyList == null) {
  397 + throw _createConnectionError(pigeonVar_channelName);
  398 + } else if (pigeonVar_replyList.length > 1) {
  399 + throw PlatformException(
  400 + code: pigeonVar_replyList[0]! as String,
  401 + message: pigeonVar_replyList[1] as String?,
  402 + details: pigeonVar_replyList[2],
  403 + );
  404 + } else {
  405 + return;
  406 + }
  407 + }
  408 +
  409 + Future<void> setMixWithOthers(bool mixWithOthers) async {
  410 + final String pigeonVar_channelName =
  411 + 'dev.flutter.pigeon.video_player_android.AndroidVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix';
  412 + final BasicMessageChannel<Object?> pigeonVar_channel =
  413 + BasicMessageChannel<Object?>(
  414 + pigeonVar_channelName,
  415 + pigeonChannelCodec,
  416 + binaryMessenger: pigeonVar_binaryMessenger,
  417 + );
  418 + final List<Object?>? pigeonVar_replyList = await pigeonVar_channel
  419 + .send(<Object?>[mixWithOthers]) as List<Object?>?;
  420 + if (pigeonVar_replyList == null) {
  421 + throw _createConnectionError(pigeonVar_channelName);
  422 + } else if (pigeonVar_replyList.length > 1) {
  423 + throw PlatformException(
  424 + code: pigeonVar_replyList[0]! as String,
  425 + message: pigeonVar_replyList[1] as String?,
  426 + details: pigeonVar_replyList[2],
  427 + );
  428 + } else {
  429 + return;
  430 + }
  431 + }
  432 +}
... ...
packages/video_player_android/lib/src/platform_view_player.dart 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +import 'package:flutter/foundation.dart';
  6 +import 'package:flutter/gestures.dart';
  7 +import 'package:flutter/rendering.dart';
  8 +import 'package:flutter/services.dart';
  9 +import 'package:flutter/widgets.dart';
  10 +
  11 +import 'messages.g.dart';
  12 +
  13 +/// A widget that displays a video player using a platform view.
  14 +class PlatformViewPlayer extends StatelessWidget {
  15 + /// Creates a new instance of [PlatformViewPlayer].
  16 + const PlatformViewPlayer({
  17 + super.key,
  18 + required this.playerId,
  19 + });
  20 +
  21 + /// The ID of the player.
  22 + final int playerId;
  23 +
  24 + @override
  25 + Widget build(BuildContext context) {
  26 + const String viewType = 'plugins.flutter.dev/video_player_android';
  27 + final PlatformVideoViewCreationParams creationParams =
  28 + PlatformVideoViewCreationParams(playerId: playerId);
  29 +
  30 + // IgnorePointer so that GestureDetector can be used above the platform view.
  31 + return IgnorePointer(
  32 + child: PlatformViewLink(
  33 + viewType: viewType,
  34 + surfaceFactory: (
  35 + BuildContext context,
  36 + PlatformViewController controller,
  37 + ) {
  38 + return AndroidViewSurface(
  39 + controller: controller as AndroidViewController,
  40 + gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
  41 + hitTestBehavior: PlatformViewHitTestBehavior.opaque,
  42 + );
  43 + },
  44 + onCreatePlatformView: (PlatformViewCreationParams params) {
  45 + return PlatformViewsService.initSurfaceAndroidView(
  46 + id: params.id,
  47 + viewType: viewType,
  48 + layoutDirection:
  49 + Directionality.maybeOf(context) ?? TextDirection.ltr,
  50 + creationParams: creationParams,
  51 + creationParamsCodec: AndroidVideoPlayerApi.pigeonChannelCodec,
  52 + onFocus: () => params.onFocusChanged(true),
  53 + )
  54 + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
  55 + ..create();
  56 + },
  57 + ),
  58 + );
  59 + }
  60 +}
... ...
packages/video_player_android/lib/video_player_android.dart 0 → 100644
  1 +// Copyright 2013 The Flutter Authors. All rights reserved.
  2 +// Use of this source code is governed by a BSD-style license that can be
  3 +// found in the LICENSE file.
  4 +
  5 +export 'src/android_video_player.dart';
... ...
packages/video_player_android/pubspec.yaml 0 → 100644
  1 +name: video_player_android
  2 +description: Android implementation of the video_player plugin.
  3 +repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android
  4 +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
  5 +version: 2.8.4
  6 +
  7 +environment:
  8 + sdk: ^3.6.0
  9 + flutter: ">=3.27.0"
  10 +
  11 +flutter:
  12 + plugin:
  13 + implements: video_player
  14 + platforms:
  15 + android:
  16 + dartPluginClass: AndroidVideoPlayer
  17 + package: io.flutter.plugins.videoplayer
  18 + pluginClass: VideoPlayerPlugin
  19 +
  20 +dependencies:
  21 + flutter:
  22 + sdk: flutter
  23 + video_player_platform_interface: ^6.3.0
  24 +
  25 +dev_dependencies:
  26 + flutter_test:
  27 + sdk: flutter
  28 + pigeon: ^22.4.2
  29 +
  30 +topics:
  31 + - video
  32 + - video-player
... ...
pubspec.lock
... ... @@ -5,56 +5,56 @@ packages:
5 5 dependency: transitive
6 6 description:
7 7 name: _fe_analyzer_shared
8   - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
9   - url: "https://pub.dev"
  8 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
  9 + url: "https://pub.flutter-io.cn"
10 10 source: hosted
11   - version: "85.0.0"
  11 + version: "67.0.0"
12 12 analyzer:
13 13 dependency: transitive
14 14 description:
15 15 name: analyzer
16   - sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
17   - url: "https://pub.dev"
  16 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
  17 + url: "https://pub.flutter-io.cn"
18 18 source: hosted
19   - version: "7.7.1"
  19 + version: "6.4.1"
20 20 archive:
21 21 dependency: transitive
22 22 description:
23 23 name: archive
24   - sha256: ace891da0862b0e4cabbb064ee3fd87b2728b898949fdb366d83fe98342c9f19
25   - url: "https://pub.dev"
  24 + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
  25 + url: "https://pub.flutter-io.cn"
26 26 source: hosted
27   - version: "4.2.0"
  27 + version: "3.6.1"
28 28 args:
29 29 dependency: transitive
30 30 description:
31 31 name: args
32   - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
33   - url: "https://pub.dev"
  32 + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
  33 + url: "https://pub.flutter-io.cn"
34 34 source: hosted
35   - version: "2.7.0"
  35 + version: "2.6.0"
36 36 async:
37 37 dependency: transitive
38 38 description:
39 39 name: async
40 40 sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
41   - url: "https://pub.dev"
  41 + url: "https://pub.flutter-io.cn"
42 42 source: hosted
43 43 version: "2.11.0"
44 44 audio_session:
45 45 dependency: "direct main"
46 46 description:
47 47 name: audio_session
48   - sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac"
49   - url: "https://pub.dev"
  48 + sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261"
  49 + url: "https://pub.flutter-io.cn"
50 50 source: hosted
51   - version: "0.1.25"
  51 + version: "0.1.21"
52 52 audioplayers:
53 53 dependency: "direct main"
54 54 description:
55 55 name: audioplayers
56 56 sha256: c346ba5a39dc208f1bab55fc239855f573d69b0e832402114bf0b793622adc4d
57   - url: "https://pub.dev"
  57 + url: "https://pub.flutter-io.cn"
58 58 source: hosted
59 59 version: "6.1.0"
60 60 audioplayers_android:
... ... @@ -62,55 +62,55 @@ packages:
62 62 description:
63 63 name: audioplayers_android
64 64 sha256: "5a09cf83ac1a1a8e4392eafa5f66ddc21fc435a605d37542dd9c2f838d516e26"
65   - url: "https://pub.dev"
  65 + url: "https://pub.flutter-io.cn"
66 66 source: hosted
67 67 version: "5.0.2"
68 68 audioplayers_darwin:
69 69 dependency: transitive
70 70 description:
71 71 name: audioplayers_darwin
72   - sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91
73   - url: "https://pub.dev"
  72 + sha256: "34e1fb3a88ba02566615c6ca7173aa93b39cf61a38a05a729b60ba14c4899169"
  73 + url: "https://pub.flutter-io.cn"
74 74 source: hosted
75   - version: "6.4.0"
  75 + version: "6.1.0"
76 76 audioplayers_linux:
77 77 dependency: transitive
78 78 description:
79 79 name: audioplayers_linux
80   - sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
81   - url: "https://pub.dev"
  80 + sha256: ce8383c1ff0db1ba93b5b0b8ef5b260ec2d0ce2598ad37dc8b946b773dd2c5fa
  81 + url: "https://pub.flutter-io.cn"
82 82 source: hosted
83   - version: "4.2.1"
  83 + version: "4.1.0"
84 84 audioplayers_platform_interface:
85 85 dependency: transitive
86 86 description:
87 87 name: audioplayers_platform_interface
88   - sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
89   - url: "https://pub.dev"
  88 + sha256: "6834dd48dfb7bc6c2404998ebdd161f79cd3774a7e6779e1348d54a3bfdcfaa5"
  89 + url: "https://pub.flutter-io.cn"
90 90 source: hosted
91   - version: "7.1.1"
  91 + version: "7.0.0"
92 92 audioplayers_web:
93 93 dependency: transitive
94 94 description:
95 95 name: audioplayers_web
96   - sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9
97   - url: "https://pub.dev"
  96 + sha256: "3609bdf0e05e66a3d9750ee40b1e37f2a622c4edb796cc600b53a90a30a2ace4"
  97 + url: "https://pub.flutter-io.cn"
98 98 source: hosted
99   - version: "5.2.0"
  99 + version: "5.0.1"
100 100 audioplayers_windows:
101 101 dependency: transitive
102 102 description:
103 103 name: audioplayers_windows
104   - sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734
105   - url: "https://pub.dev"
  104 + sha256: "52b57c706a20fc85daa911be67381b3a5c9d03f8e27cb9fdb226de5bddf293b1"
  105 + url: "https://pub.flutter-io.cn"
106 106 source: hosted
107   - version: "4.3.0"
  107 + version: "4.1.0"
108 108 bloc:
109 109 dependency: transitive
110 110 description:
111 111 name: bloc
112 112 sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
113   - url: "https://pub.dev"
  113 + url: "https://pub.flutter-io.cn"
114 114 source: hosted
115 115 version: "8.1.4"
116 116 boolean_selector:
... ... @@ -118,103 +118,103 @@ packages:
118 118 description:
119 119 name: boolean_selector
120 120 sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
121   - url: "https://pub.dev"
  121 + url: "https://pub.flutter-io.cn"
122 122 source: hosted
123 123 version: "2.1.1"
124 124 build:
125 125 dependency: transitive
126 126 description:
127 127 name: build
128   - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
129   - url: "https://pub.dev"
  128 + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
  129 + url: "https://pub.flutter-io.cn"
130 130 source: hosted
131   - version: "2.4.2"
  131 + version: "2.4.1"
132 132 build_config:
133 133 dependency: transitive
134 134 description:
135 135 name: build_config
136   - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
137   - url: "https://pub.dev"
  136 + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
  137 + url: "https://pub.flutter-io.cn"
138 138 source: hosted
139   - version: "1.1.2"
  139 + version: "1.1.1"
140 140 build_daemon:
141 141 dependency: transitive
142 142 description:
143 143 name: build_daemon
144   - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
145   - url: "https://pub.dev"
  144 + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
  145 + url: "https://pub.flutter-io.cn"
146 146 source: hosted
147   - version: "4.0.4"
  147 + version: "4.0.1"
148 148 build_resolvers:
149 149 dependency: transitive
150 150 description:
151 151 name: build_resolvers
152   - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
153   - url: "https://pub.dev"
  152 + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
  153 + url: "https://pub.flutter-io.cn"
154 154 source: hosted
155   - version: "2.4.4"
  155 + version: "2.4.2"
156 156 build_runner:
157 157 dependency: "direct dev"
158 158 description:
159 159 name: build_runner
160   - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
161   - url: "https://pub.dev"
  160 + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
  161 + url: "https://pub.flutter-io.cn"
162 162 source: hosted
163   - version: "2.4.15"
  163 + version: "2.4.9"
164 164 build_runner_core:
165 165 dependency: transitive
166 166 description:
167 167 name: build_runner_core
168   - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
169   - url: "https://pub.dev"
  168 + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799"
  169 + url: "https://pub.flutter-io.cn"
170 170 source: hosted
171   - version: "8.0.0"
  171 + version: "7.3.0"
172 172 built_collection:
173 173 dependency: transitive
174 174 description:
175 175 name: built_collection
176 176 sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
177   - url: "https://pub.dev"
  177 + url: "https://pub.flutter-io.cn"
178 178 source: hosted
179 179 version: "5.1.1"
180 180 built_value:
181 181 dependency: transitive
182 182 description:
183 183 name: built_value
184   - sha256: f87ea98192116f7093cb214551ce1929caae0681fdba282b3d8b4462adee7bb7
185   - url: "https://pub.dev"
  184 + sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61"
  185 + url: "https://pub.flutter-io.cn"
186 186 source: hosted
187   - version: "8.13.0"
  187 + version: "8.9.4"
188 188 cached_network_image:
189 189 dependency: "direct main"
190 190 description:
191 191 name: cached_network_image
192   - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
193   - url: "https://pub.dev"
  192 + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
  193 + url: "https://pub.flutter-io.cn"
194 194 source: hosted
195   - version: "3.4.1"
  195 + version: "3.3.1"
196 196 cached_network_image_platform_interface:
197 197 dependency: transitive
198 198 description:
199 199 name: cached_network_image_platform_interface
200   - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
201   - url: "https://pub.dev"
  200 + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f"
  201 + url: "https://pub.flutter-io.cn"
202 202 source: hosted
203   - version: "4.1.1"
  203 + version: "4.0.0"
204 204 cached_network_image_web:
205 205 dependency: transitive
206 206 description:
207 207 name: cached_network_image_web
208   - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
209   - url: "https://pub.dev"
  208 + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7"
  209 + url: "https://pub.flutter-io.cn"
210 210 source: hosted
211   - version: "1.3.1"
  211 + version: "1.2.0"
212 212 card_swiper:
213 213 dependency: "direct main"
214 214 description:
215 215 name: card_swiper
216 216 sha256: "0c94c538f47be1dab52d018d4900a7046b4cb0700dc7f95b8628da89d1212b35"
217   - url: "https://pub.dev"
  217 + url: "https://pub.flutter-io.cn"
218 218 source: hosted
219 219 version: "2.0.4"
220 220 characters:
... ... @@ -222,7 +222,7 @@ packages:
222 222 description:
223 223 name: characters
224 224 sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
225   - url: "https://pub.dev"
  225 + url: "https://pub.flutter-io.cn"
226 226 source: hosted
227 227 version: "1.3.0"
228 228 checked_yaml:
... ... @@ -230,7 +230,7 @@ packages:
230 230 description:
231 231 name: checked_yaml
232 232 sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
233   - url: "https://pub.dev"
  233 + url: "https://pub.flutter-io.cn"
234 234 source: hosted
235 235 version: "2.0.3"
236 236 chewie:
... ... @@ -238,7 +238,7 @@ packages:
238 238 description:
239 239 name: chewie
240 240 sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144"
241   - url: "https://pub.dev"
  241 + url: "https://pub.flutter-io.cn"
242 242 source: hosted
243 243 version: "1.7.5"
244 244 chivox_aiengine:
... ... @@ -253,23 +253,23 @@ packages:
253 253 description:
254 254 name: clock
255 255 sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
256   - url: "https://pub.dev"
  256 + url: "https://pub.flutter-io.cn"
257 257 source: hosted
258 258 version: "1.1.1"
259 259 code_builder:
260 260 dependency: transitive
261 261 description:
262 262 name: code_builder
263   - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
264   - url: "https://pub.dev"
  263 + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
  264 + url: "https://pub.flutter-io.cn"
265 265 source: hosted
266   - version: "4.10.1"
  266 + version: "4.10.0"
267 267 collection:
268 268 dependency: transitive
269 269 description:
270 270 name: collection
271 271 sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
272   - url: "https://pub.dev"
  272 + url: "https://pub.flutter-io.cn"
273 273 source: hosted
274 274 version: "1.19.0"
275 275 common_utils:
... ... @@ -277,7 +277,7 @@ packages:
277 277 description:
278 278 name: common_utils
279 279 sha256: c26884339b13ff99b0739e56f4b02090c84054ed9dd3a045435cd24e7b99c2c1
280   - url: "https://pub.dev"
  280 + url: "https://pub.flutter-io.cn"
281 281 source: hosted
282 282 version: "2.1.0"
283 283 connectivity_plus:
... ... @@ -285,7 +285,7 @@ packages:
285 285 description:
286 286 name: connectivity_plus
287 287 sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b"
288   - url: "https://pub.dev"
  288 + url: "https://pub.flutter-io.cn"
289 289 source: hosted
290 290 version: "4.0.2"
291 291 connectivity_plus_platform_interface:
... ... @@ -293,39 +293,39 @@ packages:
293 293 description:
294 294 name: connectivity_plus_platform_interface
295 295 sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
296   - url: "https://pub.dev"
  296 + url: "https://pub.flutter-io.cn"
297 297 source: hosted
298 298 version: "1.2.4"
299 299 convert:
300 300 dependency: transitive
301 301 description:
302 302 name: convert
303   - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
304   - url: "https://pub.dev"
  303 + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
  304 + url: "https://pub.flutter-io.cn"
305 305 source: hosted
306   - version: "3.1.2"
  306 + version: "3.1.1"
307 307 cross_file:
308 308 dependency: transitive
309 309 description:
310 310 name: cross_file
311 311 sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
312   - url: "https://pub.dev"
  312 + url: "https://pub.flutter-io.cn"
313 313 source: hosted
314 314 version: "0.3.4+2"
315 315 crypto:
316 316 dependency: transitive
317 317 description:
318 318 name: crypto
319   - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
320   - url: "https://pub.dev"
  319 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
  320 + url: "https://pub.flutter-io.cn"
321 321 source: hosted
322   - version: "3.0.7"
  322 + version: "3.0.3"
323 323 csslib:
324 324 dependency: transitive
325 325 description:
326 326 name: csslib
327 327 sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
328   - url: "https://pub.dev"
  328 + url: "https://pub.flutter-io.cn"
329 329 source: hosted
330 330 version: "1.0.2"
331 331 cupertino_icons:
... ... @@ -333,31 +333,31 @@ packages:
333 333 description:
334 334 name: cupertino_icons
335 335 sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
336   - url: "https://pub.dev"
  336 + url: "https://pub.flutter-io.cn"
337 337 source: hosted
338 338 version: "1.0.8"
339 339 dart_style:
340 340 dependency: transitive
341 341 description:
342 342 name: dart_style
343   - sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
344   - url: "https://pub.dev"
  343 + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
  344 + url: "https://pub.flutter-io.cn"
345 345 source: hosted
346   - version: "3.0.1"
  346 + version: "2.3.6"
347 347 dbus:
348 348 dependency: transitive
349 349 description:
350 350 name: dbus
351   - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
352   - url: "https://pub.dev"
  351 + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
  352 + url: "https://pub.flutter-io.cn"
353 353 source: hosted
354   - version: "0.7.12"
  354 + version: "0.7.11"
355 355 decimal:
356 356 dependency: "direct main"
357 357 description:
358 358 name: decimal
359 359 sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21"
360   - url: "https://pub.dev"
  360 + url: "https://pub.flutter-io.cn"
361 361 source: hosted
362 362 version: "2.3.3"
363 363 device_info_plus:
... ... @@ -365,7 +365,7 @@ packages:
365 365 description:
366 366 name: device_info_plus
367 367 sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110"
368   - url: "https://pub.dev"
  368 + url: "https://pub.flutter-io.cn"
369 369 source: hosted
370 370 version: "9.1.2"
371 371 device_info_plus_platform_interface:
... ... @@ -373,39 +373,39 @@ packages:
373 373 description:
374 374 name: device_info_plus_platform_interface
375 375 sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
376   - url: "https://pub.dev"
  376 + url: "https://pub.flutter-io.cn"
377 377 source: hosted
378 378 version: "7.0.2"
379 379 dio:
380 380 dependency: "direct main"
381 381 description:
382 382 name: dio
383   - sha256: "852ec3b48cc431ac04fff978413c541502b67ffc3e26921e74e3d994694192c1"
384   - url: "https://pub.dev"
  383 + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
  384 + url: "https://pub.flutter-io.cn"
385 385 source: hosted
386   - version: "5.11.1"
  386 + version: "5.8.0+1"
387 387 dio_web_adapter:
388 388 dependency: transitive
389 389 description:
390 390 name: dio_web_adapter
391   - sha256: "3a1b2cd7be71086f38504956e3ebcd2837288d231ff454bafa78021244102bfc"
392   - url: "https://pub.dev"
  391 + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
  392 + url: "https://pub.flutter-io.cn"
393 393 source: hosted
394   - version: "2.2.2"
  394 + version: "2.1.1"
395 395 equatable:
396 396 dependency: "direct main"
397 397 description:
398 398 name: equatable
399   - sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2"
400   - url: "https://pub.dev"
  399 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
  400 + url: "https://pub.flutter-io.cn"
401 401 source: hosted
402   - version: "2.1.0"
  402 + version: "2.0.7"
403 403 extended_text:
404 404 dependency: "direct main"
405 405 description:
406 406 name: extended_text
407 407 sha256: "32119a9447888fe97f506df6c87e23744ea2dbdf25a75fd713030cc7bc055c3a"
408   - url: "https://pub.dev"
  408 + url: "https://pub.flutter-io.cn"
409 409 source: hosted
410 410 version: "11.2.0"
411 411 extended_text_library:
... ... @@ -413,7 +413,7 @@ packages:
413 413 description:
414 414 name: extended_text_library
415 415 sha256: f6ae456324ff6ba3f9fe560a5c12f51ec02c5f463ef5a02bf165b678f049d547
416   - url: "https://pub.dev"
  416 + url: "https://pub.flutter-io.cn"
417 417 source: hosted
418 418 version: "11.1.0"
419 419 fake_async:
... ... @@ -421,7 +421,7 @@ packages:
421 421 description:
422 422 name: fake_async
423 423 sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
424   - url: "https://pub.dev"
  424 + url: "https://pub.flutter-io.cn"
425 425 source: hosted
426 426 version: "1.3.1"
427 427 ffi:
... ... @@ -429,55 +429,55 @@ packages:
429 429 description:
430 430 name: ffi
431 431 sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
432   - url: "https://pub.dev"
  432 + url: "https://pub.flutter-io.cn"
433 433 source: hosted
434 434 version: "2.1.3"
435 435 file:
436 436 dependency: transitive
437 437 description:
438 438 name: file
439   - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
440   - url: "https://pub.dev"
  439 + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
  440 + url: "https://pub.flutter-io.cn"
441 441 source: hosted
442   - version: "7.0.1"
  442 + version: "6.1.4"
443 443 file_selector_linux:
444 444 dependency: transitive
445 445 description:
446 446 name: file_selector_linux
447 447 sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
448   - url: "https://pub.dev"
  448 + url: "https://pub.flutter-io.cn"
449 449 source: hosted
450 450 version: "0.9.3+2"
451 451 file_selector_macos:
452 452 dependency: transitive
453 453 description:
454 454 name: file_selector_macos
455   - sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711"
456   - url: "https://pub.dev"
  455 + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
  456 + url: "https://pub.flutter-io.cn"
457 457 source: hosted
458   - version: "0.9.4+3"
  458 + version: "0.9.4+2"
459 459 file_selector_platform_interface:
460 460 dependency: transitive
461 461 description:
462 462 name: file_selector_platform_interface
463 463 sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
464   - url: "https://pub.dev"
  464 + url: "https://pub.flutter-io.cn"
465 465 source: hosted
466 466 version: "2.6.2"
467 467 file_selector_windows:
468 468 dependency: transitive
469 469 description:
470 470 name: file_selector_windows
471   - sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
472   - url: "https://pub.dev"
  471 + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
  472 + url: "https://pub.flutter-io.cn"
473 473 source: hosted
474   - version: "0.9.3+4"
  474 + version: "0.9.3+3"
475 475 fixnum:
476 476 dependency: transitive
477 477 description:
478 478 name: fixnum
479 479 sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
480   - url: "https://pub.dev"
  480 + url: "https://pub.flutter-io.cn"
481 481 source: hosted
482 482 version: "1.1.1"
483 483 flutter:
... ... @@ -489,32 +489,32 @@ packages:
489 489 dependency: "direct main"
490 490 description:
491 491 name: flutter_app_update
492   - sha256: "336a87c9b8f5c2f040ac314939f61a567e812a3ec3c066d8c99a524d95171d81"
493   - url: "https://pub.dev"
  492 + sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65"
  493 + url: "https://pub.flutter-io.cn"
494 494 source: hosted
495   - version: "3.2.4"
  495 + version: "3.2.2"
496 496 flutter_bloc:
497 497 dependency: "direct main"
498 498 description:
499 499 name: flutter_bloc
500 500 sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
501   - url: "https://pub.dev"
  501 + url: "https://pub.flutter-io.cn"
502 502 source: hosted
503 503 version: "8.1.6"
504 504 flutter_cache_manager:
505 505 dependency: transitive
506 506 description:
507 507 name: flutter_cache_manager
508   - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
509   - url: "https://pub.dev"
  508 + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
  509 + url: "https://pub.flutter-io.cn"
510 510 source: hosted
511   - version: "3.4.1"
  511 + version: "3.3.1"
512 512 flutter_easyloading:
513 513 dependency: "direct main"
514 514 description:
515 515 name: flutter_easyloading
516 516 sha256: ba21a3c883544e582f9cc455a4a0907556714e1e9cf0eababfcb600da191d17c
517   - url: "https://pub.dev"
  517 + url: "https://pub.flutter-io.cn"
518 518 source: hosted
519 519 version: "3.0.5"
520 520 flutter_inappwebview:
... ... @@ -522,63 +522,63 @@ packages:
522 522 description:
523 523 name: flutter_inappwebview
524 524 sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959"
525   - url: "https://pub.dev"
  525 + url: "https://pub.flutter-io.cn"
526 526 source: hosted
527 527 version: "6.0.0"
528 528 flutter_inappwebview_android:
529 529 dependency: transitive
530 530 description:
531 531 name: flutter_inappwebview_android
532   - sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
533   - url: "https://pub.dev"
  532 + sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421
  533 + url: "https://pub.flutter-io.cn"
534 534 source: hosted
535   - version: "1.1.3"
  535 + version: "1.0.13"
536 536 flutter_inappwebview_internal_annotations:
537 537 dependency: transitive
538 538 description:
539 539 name: flutter_inappwebview_internal_annotations
540 540 sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd"
541   - url: "https://pub.dev"
  541 + url: "https://pub.flutter-io.cn"
542 542 source: hosted
543 543 version: "1.2.0"
544 544 flutter_inappwebview_ios:
545 545 dependency: transitive
546 546 description:
547 547 name: flutter_inappwebview_ios
548   - sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
549   - url: "https://pub.dev"
  548 + sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f
  549 + url: "https://pub.flutter-io.cn"
550 550 source: hosted
551   - version: "1.1.2"
  551 + version: "1.0.13"
552 552 flutter_inappwebview_macos:
553 553 dependency: transitive
554 554 description:
555 555 name: flutter_inappwebview_macos
556   - sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
557   - url: "https://pub.dev"
  556 + sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636
  557 + url: "https://pub.flutter-io.cn"
558 558 source: hosted
559   - version: "1.1.2"
  559 + version: "1.0.11"
560 560 flutter_inappwebview_platform_interface:
561 561 dependency: transitive
562 562 description:
563 563 name: flutter_inappwebview_platform_interface
564   - sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500
565   - url: "https://pub.dev"
  564 + sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187"
  565 + url: "https://pub.flutter-io.cn"
566 566 source: hosted
567   - version: "1.3.0+1"
  567 + version: "1.0.10"
568 568 flutter_inappwebview_web:
569 569 dependency: transitive
570 570 description:
571 571 name: flutter_inappwebview_web
572   - sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"
573   - url: "https://pub.dev"
  572 + sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07
  573 + url: "https://pub.flutter-io.cn"
574 574 source: hosted
575   - version: "1.1.2"
  575 + version: "1.0.8"
576 576 flutter_lints:
577 577 dependency: "direct dev"
578 578 description:
579 579 name: flutter_lints
580 580 sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
581   - url: "https://pub.dev"
  581 + url: "https://pub.flutter-io.cn"
582 582 source: hosted
583 583 version: "2.0.3"
584 584 flutter_oss_aliyun:
... ... @@ -586,57 +586,57 @@ packages:
586 586 description:
587 587 name: flutter_oss_aliyun
588 588 sha256: "8280c1e8dfb792dec6449d1e6052e1d3492b3b3a1dfee456cc41d3f31b4cbc26"
589   - url: "https://pub.dev"
  589 + url: "https://pub.flutter-io.cn"
590 590 source: hosted
591 591 version: "6.4.2"
592 592 flutter_plugin_android_lifecycle:
593 593 dependency: transitive
594 594 description:
595 595 name: flutter_plugin_android_lifecycle
596   - sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
597   - url: "https://pub.dev"
  596 + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f"
  597 + url: "https://pub.flutter-io.cn"
598 598 source: hosted
599   - version: "2.0.29"
  599 + version: "2.0.19"
600 600 flutter_screenutil:
601 601 dependency: "direct main"
602 602 description:
603 603 name: flutter_screenutil
604 604 sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de"
605   - url: "https://pub.dev"
  605 + url: "https://pub.flutter-io.cn"
606 606 source: hosted
607 607 version: "5.9.3"
608 608 flutter_sound:
609 609 dependency: "direct main"
610 610 description:
611 611 name: flutter_sound
612   - sha256: ef89477f6e8ce2fa395158ebc4a8b11982e3ada440b4021c06fd97a4e771554b
613   - url: "https://pub.dev"
  612 + sha256: "1f40b26b92907a433afe877c927cd48f5a2e4d0f7188e5d39eb5756008aa51ab"
  613 + url: "https://pub.flutter-io.cn"
614 614 source: hosted
615   - version: "9.28.0"
  615 + version: "9.6.0"
616 616 flutter_sound_platform_interface:
617 617 dependency: transitive
618 618 description:
619 619 name: flutter_sound_platform_interface
620   - sha256: "3394d7e664a09796818014ff85a81db0dec397f4c286cbe52f8783886fa5a497"
621   - url: "https://pub.dev"
  620 + sha256: "2e218521d8187b9a4c65063ad9c79bfe88119531ff68047a2eaa6b027cb276bb"
  621 + url: "https://pub.flutter-io.cn"
622 622 source: hosted
623   - version: "9.28.0"
  623 + version: "9.6.0"
624 624 flutter_sound_web:
625 625 dependency: transitive
626 626 description:
627 627 name: flutter_sound_web
628   - sha256: "4e10c94a8574bd93bb8668af59bf76f5312a890bccd3778d73168a7133217dc5"
629   - url: "https://pub.dev"
  628 + sha256: "5013a15e4e69a4bdc8badd729130eef43c3305e30ba8e6933f863436ce969932"
  629 + url: "https://pub.flutter-io.cn"
630 630 source: hosted
631   - version: "9.28.0"
  631 + version: "9.6.0"
632 632 flutter_spinkit:
633 633 dependency: transitive
634 634 description:
635 635 name: flutter_spinkit
636   - sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f"
637   - url: "https://pub.dev"
  636 + sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472
  637 + url: "https://pub.flutter-io.cn"
638 638 source: hosted
639   - version: "5.2.2"
  639 + version: "5.2.1"
640 640 flutter_test:
641 641 dependency: "direct dev"
642 642 description: flutter
... ... @@ -651,16 +651,16 @@ packages:
651 651 dependency: "direct main"
652 652 description:
653 653 name: fluttertoast
654   - sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8"
655   - url: "https://pub.dev"
  654 + sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc"
  655 + url: "https://pub.flutter-io.cn"
656 656 source: hosted
657   - version: "8.2.14"
  657 + version: "8.2.8"
658 658 fluwx:
659 659 dependency: "direct main"
660 660 description:
661 661 name: fluwx
662 662 sha256: d25c26a12a1fa280833105869ea255a5877695e03dc1190f0d2c027a7c989ef3
663   - url: "https://pub.dev"
  663 + url: "https://pub.flutter-io.cn"
664 664 source: hosted
665 665 version: "4.6.3"
666 666 frontend_server_client:
... ... @@ -668,47 +668,47 @@ packages:
668 668 description:
669 669 name: frontend_server_client
670 670 sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
671   - url: "https://pub.dev"
  671 + url: "https://pub.flutter-io.cn"
672 672 source: hosted
673 673 version: "4.0.0"
674 674 glob:
675 675 dependency: transitive
676 676 description:
677 677 name: glob
678   - sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb"
679   - url: "https://pub.dev"
  678 + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
  679 + url: "https://pub.flutter-io.cn"
680 680 source: hosted
681   - version: "2.2.0"
  681 + version: "2.1.3"
682 682 google_fonts:
683 683 dependency: "direct main"
684 684 description:
685 685 name: google_fonts
686 686 sha256: "2776c66b3e97c6cdd58d1bd3281548b074b64f1fd5c8f82391f7456e38849567"
687   - url: "https://pub.dev"
  687 + url: "https://pub.flutter-io.cn"
688 688 source: hosted
689 689 version: "4.0.5"
690 690 graphs:
691 691 dependency: transitive
692 692 description:
693 693 name: graphs
694   - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
695   - url: "https://pub.dev"
  694 + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
  695 + url: "https://pub.flutter-io.cn"
696 696 source: hosted
697   - version: "2.3.2"
  697 + version: "2.3.1"
698 698 html:
699 699 dependency: transitive
700 700 description:
701 701 name: html
702   - sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699"
703   - url: "https://pub.dev"
  702 + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
  703 + url: "https://pub.flutter-io.cn"
704 704 source: hosted
705   - version: "0.15.7"
  705 + version: "0.15.5"
706 706 http:
707 707 dependency: "direct overridden"
708 708 description:
709 709 name: http
710 710 sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
711   - url: "https://pub.dev"
  711 + url: "https://pub.flutter-io.cn"
712 712 source: hosted
713 713 version: "0.13.6"
714 714 http_multi_server:
... ... @@ -716,111 +716,111 @@ packages:
716 716 description:
717 717 name: http_multi_server
718 718 sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
719   - url: "https://pub.dev"
  719 + url: "https://pub.flutter-io.cn"
720 720 source: hosted
721 721 version: "3.2.2"
722 722 http_parser:
723 723 dependency: transitive
724 724 description:
725 725 name: http_parser
726   - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
727   - url: "https://pub.dev"
  726 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
  727 + url: "https://pub.flutter-io.cn"
728 728 source: hosted
729   - version: "4.1.2"
  729 + version: "4.0.2"
730 730 image:
731 731 dependency: "direct main"
732 732 description:
733 733 name: image
734   - sha256: "1976370a4df3091bb0f72409c187ad1f9132a818bc6b95ca59c0bae1c75c688e"
735   - url: "https://pub.dev"
  734 + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
  735 + url: "https://pub.flutter-io.cn"
736 736 source: hosted
737   - version: "4.9.2"
  737 + version: "4.3.0"
738 738 image_picker:
739 739 dependency: "direct main"
740 740 description:
741 741 name: image_picker
742 742 sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c
743   - url: "https://pub.dev"
  743 + url: "https://pub.flutter-io.cn"
744 744 source: hosted
745 745 version: "0.8.9"
746 746 image_picker_android:
747 747 dependency: transitive
748 748 description:
749 749 name: image_picker_android
750   - sha256: e83b2b05141469c5e19d77e1dfa11096b6b1567d09065b2265d7c6904560050c
751   - url: "https://pub.dev"
  750 + sha256: "0f57fee1e8bfadf8cc41818bbcd7f72e53bb768a54d9496355d5e8a5681a19f1"
  751 + url: "https://pub.flutter-io.cn"
752 752 source: hosted
753   - version: "0.8.13"
  753 + version: "0.8.12+1"
754 754 image_picker_for_web:
755 755 dependency: transitive
756 756 description:
757 757 name: image_picker_for_web
758 758 sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0"
759   - url: "https://pub.dev"
  759 + url: "https://pub.flutter-io.cn"
760 760 source: hosted
761 761 version: "2.2.0"
762 762 image_picker_ios:
763 763 dependency: transitive
764 764 description:
765 765 name: image_picker_ios
766   - sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
767   - url: "https://pub.dev"
  766 + sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
  767 + url: "https://pub.flutter-io.cn"
768 768 source: hosted
769   - version: "0.8.13"
  769 + version: "0.8.12+1"
770 770 image_picker_linux:
771 771 dependency: transitive
772 772 description:
773 773 name: image_picker_linux
774   - sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
775   - url: "https://pub.dev"
  774 + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
  775 + url: "https://pub.flutter-io.cn"
776 776 source: hosted
777   - version: "0.2.2"
  777 + version: "0.2.1+1"
778 778 image_picker_macos:
779 779 dependency: transitive
780 780 description:
781 781 name: image_picker_macos
782   - sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
783   - url: "https://pub.dev"
  782 + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
  783 + url: "https://pub.flutter-io.cn"
784 784 source: hosted
785   - version: "0.2.2"
  785 + version: "0.2.1+1"
786 786 image_picker_platform_interface:
787 787 dependency: transitive
788 788 description:
789 789 name: image_picker_platform_interface
790   - sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
791   - url: "https://pub.dev"
  790 + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
  791 + url: "https://pub.flutter-io.cn"
792 792 source: hosted
793   - version: "2.11.0"
  793 + version: "2.10.0"
794 794 image_picker_windows:
795 795 dependency: transitive
796 796 description:
797 797 name: image_picker_windows
798   - sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
799   - url: "https://pub.dev"
  798 + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
  799 + url: "https://pub.flutter-io.cn"
800 800 source: hosted
801   - version: "0.2.2"
  801 + version: "0.2.1+1"
802 802 intl:
803 803 dependency: "direct main"
804 804 description:
805 805 name: intl
806 806 sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
807   - url: "https://pub.dev"
  807 + url: "https://pub.flutter-io.cn"
808 808 source: hosted
809 809 version: "0.19.0"
810 810 io:
811 811 dependency: transitive
812 812 description:
813 813 name: io
814   - sha256: "2635216ca6a737e60de577ffa1a48a0bec76ca8a62917cfc1bb88c14c570646f"
815   - url: "https://pub.dev"
  814 + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
  815 + url: "https://pub.flutter-io.cn"
816 816 source: hosted
817   - version: "1.1.0"
  817 + version: "1.0.4"
818 818 js:
819 819 dependency: transitive
820 820 description:
821 821 name: js
822 822 sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
823   - url: "https://pub.dev"
  823 + url: "https://pub.flutter-io.cn"
824 824 source: hosted
825 825 version: "0.6.7"
826 826 json_annotation:
... ... @@ -828,23 +828,23 @@ packages:
828 828 description:
829 829 name: json_annotation
830 830 sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
831   - url: "https://pub.dev"
  831 + url: "https://pub.flutter-io.cn"
832 832 source: hosted
833 833 version: "4.9.0"
834 834 json_serializable:
835 835 dependency: "direct dev"
836 836 description:
837 837 name: json_serializable
838   - sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
839   - url: "https://pub.dev"
  838 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
  839 + url: "https://pub.flutter-io.cn"
840 840 source: hosted
841   - version: "6.9.5"
  841 + version: "6.8.0"
842 842 leak_tracker:
843 843 dependency: transitive
844 844 description:
845 845 name: leak_tracker
846 846 sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
847   - url: "https://pub.dev"
  847 + url: "https://pub.flutter-io.cn"
848 848 source: hosted
849 849 version: "10.0.7"
850 850 leak_tracker_flutter_testing:
... ... @@ -852,7 +852,7 @@ packages:
852 852 description:
853 853 name: leak_tracker_flutter_testing
854 854 sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
855   - url: "https://pub.dev"
  855 + url: "https://pub.flutter-io.cn"
856 856 source: hosted
857 857 version: "3.0.8"
858 858 leak_tracker_testing:
... ... @@ -860,7 +860,7 @@ packages:
860 860 description:
861 861 name: leak_tracker_testing
862 862 sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
863   - url: "https://pub.dev"
  863 + url: "https://pub.flutter-io.cn"
864 864 source: hosted
865 865 version: "3.0.1"
866 866 limiting_direction_csx:
... ... @@ -868,7 +868,7 @@ packages:
868 868 description:
869 869 name: limiting_direction_csx
870 870 sha256: "283a846809a076ad641924ba830e3e39ff5385b9fb9bab7ecd322200948a89bd"
871   - url: "https://pub.dev"
  871 + url: "https://pub.flutter-io.cn"
872 872 source: hosted
873 873 version: "0.2.0+5"
874 874 lints:
... ... @@ -876,39 +876,39 @@ packages:
876 876 description:
877 877 name: lints
878 878 sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
879   - url: "https://pub.dev"
  879 + url: "https://pub.flutter-io.cn"
880 880 source: hosted
881 881 version: "2.1.1"
882 882 logger:
883 883 dependency: transitive
884 884 description:
885 885 name: logger
886   - sha256: "2a0dc097e7b01d942475bdd552356db2d0f768b05540bd4b2b53f1840f2239a7"
887   - url: "https://pub.dev"
  886 + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1
  887 + url: "https://pub.flutter-io.cn"
888 888 source: hosted
889   - version: "2.8.0"
  889 + version: "2.5.0"
890 890 logging:
891 891 dependency: transitive
892 892 description:
893 893 name: logging
894   - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
895   - url: "https://pub.dev"
  894 + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
  895 + url: "https://pub.flutter-io.cn"
896 896 source: hosted
897   - version: "1.3.0"
  897 + version: "1.2.0"
898 898 lottie:
899 899 dependency: "direct main"
900 900 description:
901 901 name: lottie
902   - sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950
903   - url: "https://pub.dev"
  902 + sha256: "7afc60865a2429d994144f7d66ced2ae4305fe35d82890b8766e3359872d872c"
  903 + url: "https://pub.flutter-io.cn"
904 904 source: hosted
905   - version: "3.3.1"
  905 + version: "3.1.3"
906 906 matcher:
907 907 dependency: transitive
908 908 description:
909 909 name: matcher
910 910 sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
911   - url: "https://pub.dev"
  911 + url: "https://pub.flutter-io.cn"
912 912 source: hosted
913 913 version: "0.12.16+1"
914 914 material_color_utilities:
... ... @@ -916,7 +916,7 @@ packages:
916 916 description:
917 917 name: material_color_utilities
918 918 sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
919   - url: "https://pub.dev"
  919 + url: "https://pub.flutter-io.cn"
920 920 source: hosted
921 921 version: "0.11.1"
922 922 meta:
... ... @@ -924,7 +924,7 @@ packages:
924 924 description:
925 925 name: meta
926 926 sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
927   - url: "https://pub.dev"
  927 + url: "https://pub.flutter-io.cn"
928 928 source: hosted
929 929 version: "1.15.0"
930 930 mime:
... ... @@ -932,7 +932,7 @@ packages:
932 932 description:
933 933 name: mime
934 934 sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
935   - url: "https://pub.dev"
  935 + url: "https://pub.flutter-io.cn"
936 936 source: hosted
937 937 version: "1.0.6"
938 938 nested:
... ... @@ -940,7 +940,7 @@ packages:
940 940 description:
941 941 name: nested
942 942 sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
943   - url: "https://pub.dev"
  943 + url: "https://pub.flutter-io.cn"
944 944 source: hosted
945 945 version: "1.0.0"
946 946 nested_scroll_views:
... ... @@ -948,7 +948,7 @@ packages:
948 948 description:
949 949 name: nested_scroll_views
950 950 sha256: "37d0f35682441a4cdf5f50bd5767a53b7192780a085c9dc728fc273df8a3443a"
951   - url: "https://pub.dev"
  951 + url: "https://pub.flutter-io.cn"
952 952 source: hosted
953 953 version: "0.0.12"
954 954 nm:
... ... @@ -956,7 +956,7 @@ packages:
956 956 description:
957 957 name: nm
958 958 sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
959   - url: "https://pub.dev"
  959 + url: "https://pub.flutter-io.cn"
960 960 source: hosted
961 961 version: "0.5.0"
962 962 octo_image:
... ... @@ -964,23 +964,23 @@ packages:
964 964 description:
965 965 name: octo_image
966 966 sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
967   - url: "https://pub.dev"
  967 + url: "https://pub.flutter-io.cn"
968 968 source: hosted
969 969 version: "2.1.0"
970 970 package_config:
971 971 dependency: transitive
972 972 description:
973 973 name: package_config
974   - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
975   - url: "https://pub.dev"
  974 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
  975 + url: "https://pub.flutter-io.cn"
976 976 source: hosted
977   - version: "2.2.0"
  977 + version: "2.1.0"
978 978 package_info_plus:
979 979 dependency: "direct main"
980 980 description:
981 981 name: package_info_plus
982 982 sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
983   - url: "https://pub.dev"
  983 + url: "https://pub.flutter-io.cn"
984 984 source: hosted
985 985 version: "4.2.0"
986 986 package_info_plus_platform_interface:
... ... @@ -988,7 +988,7 @@ packages:
988 988 description:
989 989 name: package_info_plus_platform_interface
990 990 sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
991   - url: "https://pub.dev"
  991 + url: "https://pub.flutter-io.cn"
992 992 source: hosted
993 993 version: "2.0.1"
994 994 path:
... ... @@ -996,31 +996,31 @@ packages:
996 996 description:
997 997 name: path
998 998 sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
999   - url: "https://pub.dev"
  999 + url: "https://pub.flutter-io.cn"
1000 1000 source: hosted
1001 1001 version: "1.9.0"
1002 1002 path_provider:
1003 1003 dependency: "direct main"
1004 1004 description:
1005 1005 name: path_provider
1006   - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
1007   - url: "https://pub.dev"
  1006 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
  1007 + url: "https://pub.flutter-io.cn"
1008 1008 source: hosted
1009   - version: "2.1.5"
  1009 + version: "2.1.4"
1010 1010 path_provider_android:
1011 1011 dependency: transitive
1012 1012 description:
1013 1013 name: path_provider_android
1014   - sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
1015   - url: "https://pub.dev"
  1014 + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
  1015 + url: "https://pub.flutter-io.cn"
1016 1016 source: hosted
1017   - version: "2.2.17"
  1017 + version: "2.2.4"
1018 1018 path_provider_foundation:
1019 1019 dependency: transitive
1020 1020 description:
1021 1021 name: path_provider_foundation
1022 1022 sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
1023   - url: "https://pub.dev"
  1023 + url: "https://pub.flutter-io.cn"
1024 1024 source: hosted
1025 1025 version: "2.4.1"
1026 1026 path_provider_linux:
... ... @@ -1028,7 +1028,7 @@ packages:
1028 1028 description:
1029 1029 name: path_provider_linux
1030 1030 sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
1031   - url: "https://pub.dev"
  1031 + url: "https://pub.flutter-io.cn"
1032 1032 source: hosted
1033 1033 version: "2.2.1"
1034 1034 path_provider_platform_interface:
... ... @@ -1036,7 +1036,7 @@ packages:
1036 1036 description:
1037 1037 name: path_provider_platform_interface
1038 1038 sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
1039   - url: "https://pub.dev"
  1039 + url: "https://pub.flutter-io.cn"
1040 1040 source: hosted
1041 1041 version: "2.1.2"
1042 1042 path_provider_windows:
... ... @@ -1044,23 +1044,23 @@ packages:
1044 1044 description:
1045 1045 name: path_provider_windows
1046 1046 sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
1047   - url: "https://pub.dev"
  1047 + url: "https://pub.flutter-io.cn"
1048 1048 source: hosted
1049 1049 version: "2.3.0"
1050 1050 percent_indicator:
1051 1051 dependency: "direct main"
1052 1052 description:
1053 1053 name: percent_indicator
1054   - sha256: "157d29133bbc6ecb11f923d36e7960a96a3f28837549a20b65e5135729f0f9fd"
1055   - url: "https://pub.dev"
  1054 + sha256: "0d77d5c6fa9b7f60202cedf748b568ba9ba38d3f30405d6ceae4da76f5185462"
  1055 + url: "https://pub.flutter-io.cn"
1056 1056 source: hosted
1057   - version: "4.2.5"
  1057 + version: "4.2.4"
1058 1058 permission_handler:
1059 1059 dependency: "direct main"
1060 1060 description:
1061 1061 name: permission_handler
1062 1062 sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
1063   - url: "https://pub.dev"
  1063 + url: "https://pub.flutter-io.cn"
1064 1064 source: hosted
1065 1065 version: "10.4.5"
1066 1066 permission_handler_android:
... ... @@ -1068,7 +1068,7 @@ packages:
1068 1068 description:
1069 1069 name: permission_handler_android
1070 1070 sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
1071   - url: "https://pub.dev"
  1071 + url: "https://pub.flutter-io.cn"
1072 1072 source: hosted
1073 1073 version: "10.3.6"
1074 1074 permission_handler_apple:
... ... @@ -1076,7 +1076,7 @@ packages:
1076 1076 description:
1077 1077 name: permission_handler_apple
1078 1078 sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
1079   - url: "https://pub.dev"
  1079 + url: "https://pub.flutter-io.cn"
1080 1080 source: hosted
1081 1081 version: "9.1.4"
1082 1082 permission_handler_platform_interface:
... ... @@ -1084,7 +1084,7 @@ packages:
1084 1084 description:
1085 1085 name: permission_handler_platform_interface
1086 1086 sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
1087   - url: "https://pub.dev"
  1087 + url: "https://pub.flutter-io.cn"
1088 1088 source: hosted
1089 1089 version: "3.12.0"
1090 1090 permission_handler_windows:
... ... @@ -1092,7 +1092,7 @@ packages:
1092 1092 description:
1093 1093 name: permission_handler_windows
1094 1094 sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
1095   - url: "https://pub.dev"
  1095 + url: "https://pub.flutter-io.cn"
1096 1096 source: hosted
1097 1097 version: "0.1.3"
1098 1098 petitparser:
... ... @@ -1100,7 +1100,7 @@ packages:
1100 1100 description:
1101 1101 name: petitparser
1102 1102 sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
1103   - url: "https://pub.dev"
  1103 + url: "https://pub.flutter-io.cn"
1104 1104 source: hosted
1105 1105 version: "6.0.2"
1106 1106 platform:
... ... @@ -1108,7 +1108,7 @@ packages:
1108 1108 description:
1109 1109 name: platform
1110 1110 sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
1111   - url: "https://pub.dev"
  1111 + url: "https://pub.flutter-io.cn"
1112 1112 source: hosted
1113 1113 version: "3.1.6"
1114 1114 plugin_platform_interface:
... ... @@ -1116,63 +1116,55 @@ packages:
1116 1116 description:
1117 1117 name: plugin_platform_interface
1118 1118 sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
1119   - url: "https://pub.dev"
  1119 + url: "https://pub.flutter-io.cn"
1120 1120 source: hosted
1121 1121 version: "2.1.8"
1122 1122 pool:
1123 1123 dependency: transitive
1124 1124 description:
1125 1125 name: pool
1126   - sha256: "4177f68c237ea2128d1bee66ac17b2ce05ba3dbaafcbdd54c5d40a39d0b6b11c"
1127   - url: "https://pub.dev"
1128   - source: hosted
1129   - version: "1.5.3"
1130   - posix:
1131   - dependency: transitive
1132   - description:
1133   - name: posix
1134   - sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
1135   - url: "https://pub.dev"
  1126 + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
  1127 + url: "https://pub.flutter-io.cn"
1136 1128 source: hosted
1137   - version: "6.5.2"
  1129 + version: "1.5.1"
1138 1130 pretty_dio_logger:
1139 1131 dependency: "direct main"
1140 1132 description:
1141 1133 name: pretty_dio_logger
1142 1134 sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407"
1143   - url: "https://pub.dev"
  1135 + url: "https://pub.flutter-io.cn"
1144 1136 source: hosted
1145 1137 version: "1.4.0"
1146 1138 provider:
1147 1139 dependency: transitive
1148 1140 description:
1149 1141 name: provider
1150   - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
1151   - url: "https://pub.dev"
  1142 + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
  1143 + url: "https://pub.flutter-io.cn"
1152 1144 source: hosted
1153   - version: "6.1.5+1"
  1145 + version: "6.1.2"
1154 1146 pub_semver:
1155 1147 dependency: transitive
1156 1148 description:
1157 1149 name: pub_semver
1158   - sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24"
1159   - url: "https://pub.dev"
  1150 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
  1151 + url: "https://pub.flutter-io.cn"
1160 1152 source: hosted
1161   - version: "2.2.1"
  1153 + version: "2.1.4"
1162 1154 pubspec_parse:
1163 1155 dependency: transitive
1164 1156 description:
1165 1157 name: pubspec_parse
1166   - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
1167   - url: "https://pub.dev"
  1158 + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
  1159 + url: "https://pub.flutter-io.cn"
1168 1160 source: hosted
1169   - version: "1.5.0"
  1161 + version: "1.4.0"
1170 1162 pull_to_refresh:
1171 1163 dependency: "direct main"
1172 1164 description:
1173 1165 name: pull_to_refresh
1174 1166 sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12
1175   - url: "https://pub.dev"
  1167 + url: "https://pub.flutter-io.cn"
1176 1168 source: hosted
1177 1169 version: "2.0.0"
1178 1170 rational:
... ... @@ -1180,55 +1172,63 @@ packages:
1180 1172 description:
1181 1173 name: rational
1182 1174 sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
1183   - url: "https://pub.dev"
  1175 + url: "https://pub.flutter-io.cn"
1184 1176 source: hosted
1185 1177 version: "2.2.3"
  1178 + recase:
  1179 + dependency: transitive
  1180 + description:
  1181 + name: recase
  1182 + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
  1183 + url: "https://pub.flutter-io.cn"
  1184 + source: hosted
  1185 + version: "4.1.0"
1186 1186 responsive_framework:
1187 1187 dependency: "direct main"
1188 1188 description:
1189 1189 name: responsive_framework
1190 1190 sha256: a8e1c13d4ba980c60cbf6fa1e9907cd60662bf2585184d7c96ca46c43de91552
1191   - url: "https://pub.dev"
  1191 + url: "https://pub.flutter-io.cn"
1192 1192 source: hosted
1193 1193 version: "1.5.1"
1194 1194 rxdart:
1195 1195 dependency: transitive
1196 1196 description:
1197 1197 name: rxdart
1198   - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
1199   - url: "https://pub.dev"
  1198 + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
  1199 + url: "https://pub.flutter-io.cn"
1200 1200 source: hosted
1201   - version: "0.28.0"
  1201 + version: "0.27.7"
1202 1202 shared_preferences:
1203 1203 dependency: "direct main"
1204 1204 description:
1205 1205 name: shared_preferences
1206   - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
1207   - url: "https://pub.dev"
  1206 + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
  1207 + url: "https://pub.flutter-io.cn"
1208 1208 source: hosted
1209   - version: "2.5.3"
  1209 + version: "2.2.3"
1210 1210 shared_preferences_android:
1211 1211 dependency: transitive
1212 1212 description:
1213 1213 name: shared_preferences_android
1214   - sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
1215   - url: "https://pub.dev"
  1214 + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
  1215 + url: "https://pub.flutter-io.cn"
1216 1216 source: hosted
1217   - version: "2.4.11"
  1217 + version: "2.2.2"
1218 1218 shared_preferences_foundation:
1219 1219 dependency: transitive
1220 1220 description:
1221 1221 name: shared_preferences_foundation
1222   - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
1223   - url: "https://pub.dev"
  1222 + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d"
  1223 + url: "https://pub.flutter-io.cn"
1224 1224 source: hosted
1225   - version: "2.5.4"
  1225 + version: "2.5.3"
1226 1226 shared_preferences_linux:
1227 1227 dependency: transitive
1228 1228 description:
1229 1229 name: shared_preferences_linux
1230 1230 sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
1231   - url: "https://pub.dev"
  1231 + url: "https://pub.flutter-io.cn"
1232 1232 source: hosted
1233 1233 version: "2.4.1"
1234 1234 shared_preferences_platform_interface:
... ... @@ -1236,41 +1236,41 @@ packages:
1236 1236 description:
1237 1237 name: shared_preferences_platform_interface
1238 1238 sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
1239   - url: "https://pub.dev"
  1239 + url: "https://pub.flutter-io.cn"
1240 1240 source: hosted
1241 1241 version: "2.4.1"
1242 1242 shared_preferences_web:
1243 1243 dependency: transitive
1244 1244 description:
1245 1245 name: shared_preferences_web
1246   - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
1247   - url: "https://pub.dev"
  1246 + sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2"
  1247 + url: "https://pub.flutter-io.cn"
1248 1248 source: hosted
1249   - version: "2.4.3"
  1249 + version: "2.4.1"
1250 1250 shared_preferences_windows:
1251 1251 dependency: transitive
1252 1252 description:
1253 1253 name: shared_preferences_windows
1254 1254 sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
1255   - url: "https://pub.dev"
  1255 + url: "https://pub.flutter-io.cn"
1256 1256 source: hosted
1257 1257 version: "2.4.1"
1258 1258 shelf:
1259 1259 dependency: transitive
1260 1260 description:
1261 1261 name: shelf
1262   - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
1263   - url: "https://pub.dev"
  1262 + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
  1263 + url: "https://pub.flutter-io.cn"
1264 1264 source: hosted
1265   - version: "1.4.2"
  1265 + version: "1.4.1"
1266 1266 shelf_web_socket:
1267 1267 dependency: transitive
1268 1268 description:
1269 1269 name: shelf_web_socket
1270   - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
1271   - url: "https://pub.dev"
  1270 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
  1271 + url: "https://pub.flutter-io.cn"
1272 1272 source: hosted
1273   - version: "3.0.0"
  1273 + version: "1.0.4"
1274 1274 sky_engine:
1275 1275 dependency: transitive
1276 1276 description: flutter
... ... @@ -1280,72 +1280,56 @@ packages:
1280 1280 dependency: transitive
1281 1281 description:
1282 1282 name: source_gen
1283   - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
1284   - url: "https://pub.dev"
  1283 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
  1284 + url: "https://pub.flutter-io.cn"
1285 1285 source: hosted
1286   - version: "2.0.0"
  1286 + version: "1.5.0"
1287 1287 source_helper:
1288 1288 dependency: transitive
1289 1289 description:
1290 1290 name: source_helper
1291   - sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca
1292   - url: "https://pub.dev"
  1291 + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
  1292 + url: "https://pub.flutter-io.cn"
1293 1293 source: hosted
1294   - version: "1.3.7"
  1294 + version: "1.3.4"
1295 1295 source_span:
1296 1296 dependency: transitive
1297 1297 description:
1298 1298 name: source_span
1299 1299 sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
1300   - url: "https://pub.dev"
  1300 + url: "https://pub.flutter-io.cn"
1301 1301 source: hosted
1302 1302 version: "1.10.0"
  1303 + sprintf:
  1304 + dependency: transitive
  1305 + description:
  1306 + name: sprintf
  1307 + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
  1308 + url: "https://pub.flutter-io.cn"
  1309 + source: hosted
  1310 + version: "7.0.0"
1303 1311 sqflite:
1304 1312 dependency: "direct main"
1305 1313 description:
1306 1314 name: sqflite
1307   - sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
1308   - url: "https://pub.dev"
1309   - source: hosted
1310   - version: "2.4.1"
1311   - sqflite_android:
1312   - dependency: transitive
1313   - description:
1314   - name: sqflite_android
1315   - sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
1316   - url: "https://pub.dev"
  1315 + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
  1316 + url: "https://pub.flutter-io.cn"
1317 1317 source: hosted
1318   - version: "2.4.0"
  1318 + version: "2.3.3+1"
1319 1319 sqflite_common:
1320 1320 dependency: transitive
1321 1321 description:
1322 1322 name: sqflite_common
1323   - sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
1324   - url: "https://pub.dev"
  1323 + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
  1324 + url: "https://pub.flutter-io.cn"
1325 1325 source: hosted
1326   - version: "2.5.4+6"
1327   - sqflite_darwin:
1328   - dependency: transitive
1329   - description:
1330   - name: sqflite_darwin
1331   - sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
1332   - url: "https://pub.dev"
1333   - source: hosted
1334   - version: "2.4.1+1"
1335   - sqflite_platform_interface:
1336   - dependency: transitive
1337   - description:
1338   - name: sqflite_platform_interface
1339   - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
1340   - url: "https://pub.dev"
1341   - source: hosted
1342   - version: "2.4.0"
  1326 + version: "2.5.4"
1343 1327 stack_trace:
1344 1328 dependency: transitive
1345 1329 description:
1346 1330 name: stack_trace
1347 1331 sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
1348   - url: "https://pub.dev"
  1332 + url: "https://pub.flutter-io.cn"
1349 1333 source: hosted
1350 1334 version: "1.12.0"
1351 1335 stream_channel:
... ... @@ -1353,39 +1337,39 @@ packages:
1353 1337 description:
1354 1338 name: stream_channel
1355 1339 sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
1356   - url: "https://pub.dev"
  1340 + url: "https://pub.flutter-io.cn"
1357 1341 source: hosted
1358 1342 version: "2.1.2"
1359 1343 stream_transform:
1360 1344 dependency: transitive
1361 1345 description:
1362 1346 name: stream_transform
1363   - sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a
1364   - url: "https://pub.dev"
  1347 + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
  1348 + url: "https://pub.flutter-io.cn"
1365 1349 source: hosted
1366   - version: "2.1.2"
  1350 + version: "2.1.1"
1367 1351 string_scanner:
1368 1352 dependency: transitive
1369 1353 description:
1370 1354 name: string_scanner
1371 1355 sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
1372   - url: "https://pub.dev"
  1356 + url: "https://pub.flutter-io.cn"
1373 1357 source: hosted
1374 1358 version: "1.3.0"
1375 1359 synchronized:
1376 1360 dependency: transitive
1377 1361 description:
1378 1362 name: synchronized
1379   - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
1380   - url: "https://pub.dev"
  1363 + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
  1364 + url: "https://pub.flutter-io.cn"
1381 1365 source: hosted
1382   - version: "3.3.0+3"
  1366 + version: "3.1.0+1"
1383 1367 term_glyph:
1384 1368 dependency: transitive
1385 1369 description:
1386 1370 name: term_glyph
1387 1371 sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
1388   - url: "https://pub.dev"
  1372 + url: "https://pub.flutter-io.cn"
1389 1373 source: hosted
1390 1374 version: "1.2.1"
1391 1375 test_api:
... ... @@ -1393,79 +1377,79 @@ packages:
1393 1377 description:
1394 1378 name: test_api
1395 1379 sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
1396   - url: "https://pub.dev"
  1380 + url: "https://pub.flutter-io.cn"
1397 1381 source: hosted
1398 1382 version: "0.7.3"
1399 1383 timing:
1400 1384 dependency: transitive
1401 1385 description:
1402 1386 name: timing
1403   - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
1404   - url: "https://pub.dev"
  1387 + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
  1388 + url: "https://pub.flutter-io.cn"
1405 1389 source: hosted
1406   - version: "1.0.2"
  1390 + version: "1.0.1"
1407 1391 tobias:
1408 1392 dependency: "direct main"
1409 1393 description:
1410 1394 name: tobias
1411 1395 sha256: "32836a9dae348d1c3f5f4247ed0a6a409fd5b9ddf490b06966a0fad2fd02cb7a"
1412   - url: "https://pub.dev"
  1396 + url: "https://pub.flutter-io.cn"
1413 1397 source: hosted
1414 1398 version: "3.3.2"
1415 1399 typed_data:
1416 1400 dependency: transitive
1417 1401 description:
1418 1402 name: typed_data
1419   - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
1420   - url: "https://pub.dev"
  1403 + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
  1404 + url: "https://pub.flutter-io.cn"
1421 1405 source: hosted
1422   - version: "1.4.0"
  1406 + version: "1.3.2"
1423 1407 umeng_apm_sdk:
1424 1408 dependency: "direct main"
1425 1409 description:
1426 1410 name: umeng_apm_sdk
1427   - sha256: "37f61a30ad84c06d89bedbcf00ca4f09347468a2186975c4c121aee1db3a2083"
1428   - url: "https://pub.dev"
  1411 + sha256: a1d1afa3fbcc71079fec2acdf9189a24c9d5350c30f3048b17b729c169ff0e0f
  1412 + url: "https://pub.flutter-io.cn"
1429 1413 source: hosted
1430   - version: "2.3.7"
  1414 + version: "2.3.5"
1431 1415 umeng_common_sdk:
1432 1416 dependency: "direct main"
1433 1417 description:
1434 1418 name: umeng_common_sdk
1435 1419 sha256: "203ae013220ffd00c677a164636239a2c9722ba533b6f25e00e7a4929a91aecd"
1436   - url: "https://pub.dev"
  1420 + url: "https://pub.flutter-io.cn"
1437 1421 source: hosted
1438 1422 version: "1.3.1"
1439 1423 url_launcher:
1440 1424 dependency: "direct main"
1441 1425 description:
1442 1426 name: url_launcher
1443   - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
1444   - url: "https://pub.dev"
  1427 + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
  1428 + url: "https://pub.flutter-io.cn"
1445 1429 source: hosted
1446   - version: "6.3.2"
  1430 + version: "6.3.1"
1447 1431 url_launcher_android:
1448 1432 dependency: transitive
1449 1433 description:
1450 1434 name: url_launcher_android
1451   - sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"
1452   - url: "https://pub.dev"
  1435 + sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9"
  1436 + url: "https://pub.flutter-io.cn"
1453 1437 source: hosted
1454   - version: "6.3.17"
  1438 + version: "6.3.2"
1455 1439 url_launcher_ios:
1456 1440 dependency: transitive
1457 1441 description:
1458 1442 name: url_launcher_ios
1459   - sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
1460   - url: "https://pub.dev"
  1443 + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
  1444 + url: "https://pub.flutter-io.cn"
1461 1445 source: hosted
1462   - version: "6.3.3"
  1446 + version: "6.3.2"
1463 1447 url_launcher_linux:
1464 1448 dependency: transitive
1465 1449 description:
1466 1450 name: url_launcher_linux
1467 1451 sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
1468   - url: "https://pub.dev"
  1452 + url: "https://pub.flutter-io.cn"
1469 1453 source: hosted
1470 1454 version: "3.2.1"
1471 1455 url_launcher_macos:
... ... @@ -1473,7 +1457,7 @@ packages:
1473 1457 description:
1474 1458 name: url_launcher_macos
1475 1459 sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
1476   - url: "https://pub.dev"
  1460 + url: "https://pub.flutter-io.cn"
1477 1461 source: hosted
1478 1462 version: "3.2.2"
1479 1463 url_launcher_platform_interface:
... ... @@ -1481,63 +1465,62 @@ packages:
1481 1465 description:
1482 1466 name: url_launcher_platform_interface
1483 1467 sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
1484   - url: "https://pub.dev"
  1468 + url: "https://pub.flutter-io.cn"
1485 1469 source: hosted
1486 1470 version: "2.3.2"
1487 1471 url_launcher_web:
1488 1472 dependency: transitive
1489 1473 description:
1490 1474 name: url_launcher_web
1491   - sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
1492   - url: "https://pub.dev"
  1475 + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
  1476 + url: "https://pub.flutter-io.cn"
1493 1477 source: hosted
1494   - version: "2.4.1"
  1478 + version: "2.3.3"
1495 1479 url_launcher_windows:
1496 1480 dependency: transitive
1497 1481 description:
1498 1482 name: url_launcher_windows
1499   - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
1500   - url: "https://pub.dev"
  1483 + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
  1484 + url: "https://pub.flutter-io.cn"
1501 1485 source: hosted
1502   - version: "3.1.4"
  1486 + version: "3.1.3"
1503 1487 uuid:
1504 1488 dependency: transitive
1505 1489 description:
1506 1490 name: uuid
1507   - sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
1508   - url: "https://pub.dev"
  1491 + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
  1492 + url: "https://pub.flutter-io.cn"
1509 1493 source: hosted
1510   - version: "4.6.0"
  1494 + version: "4.5.1"
1511 1495 vector_math:
1512 1496 dependency: transitive
1513 1497 description:
1514 1498 name: vector_math
1515 1499 sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
1516   - url: "https://pub.dev"
  1500 + url: "https://pub.flutter-io.cn"
1517 1501 source: hosted
1518 1502 version: "2.1.4"
1519 1503 video_player:
1520 1504 dependency: "direct main"
1521 1505 description:
1522 1506 name: video_player
1523   - sha256: "4a8c3492d734f7c39c2588a3206707a05ee80cef52e8c7f3b2078d430c84bc17"
1524   - url: "https://pub.dev"
  1507 + sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a"
  1508 + url: "https://pub.flutter-io.cn"
1525 1509 source: hosted
1526   - version: "2.9.2"
  1510 + version: "2.10.0"
1527 1511 video_player_android:
1528   - dependency: transitive
  1512 + dependency: "direct overridden"
1529 1513 description:
1530   - name: video_player_android
1531   - sha256: "28dcc4122079f40f93a0965b3679aff1a5f4251cf79611bd8011f937eb6b69de"
1532   - url: "https://pub.dev"
1533   - source: hosted
  1514 + path: "packages/video_player_android"
  1515 + relative: true
  1516 + source: path
1534 1517 version: "2.8.4"
1535 1518 video_player_avfoundation:
1536 1519 dependency: transitive
1537 1520 description:
1538 1521 name: video_player_avfoundation
1539 1522 sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd
1540   - url: "https://pub.dev"
  1523 + url: "https://pub.flutter-io.cn"
1541 1524 source: hosted
1542 1525 version: "2.8.4"
1543 1526 video_player_platform_interface:
... ... @@ -1545,23 +1528,23 @@ packages:
1545 1528 description:
1546 1529 name: video_player_platform_interface
1547 1530 sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a
1548   - url: "https://pub.dev"
  1531 + url: "https://pub.flutter-io.cn"
1549 1532 source: hosted
1550 1533 version: "6.4.0"
1551 1534 video_player_web:
1552 1535 dependency: transitive
1553 1536 description:
1554 1537 name: video_player_web
1555   - sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
1556   - url: "https://pub.dev"
  1538 + sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774"
  1539 + url: "https://pub.flutter-io.cn"
1557 1540 source: hosted
1558   - version: "2.4.0"
  1541 + version: "2.3.2"
1559 1542 vm_service:
1560 1543 dependency: transitive
1561 1544 description:
1562 1545 name: vm_service
1563 1546 sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
1564   - url: "https://pub.dev"
  1547 + url: "https://pub.flutter-io.cn"
1565 1548 source: hosted
1566 1549 version: "14.3.0"
1567 1550 wakelock_plus:
... ... @@ -1569,7 +1552,7 @@ packages:
1569 1552 description:
1570 1553 name: wakelock_plus
1571 1554 sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d
1572   - url: "https://pub.dev"
  1555 + url: "https://pub.flutter-io.cn"
1573 1556 source: hosted
1574 1557 version: "1.1.4"
1575 1558 wakelock_plus_platform_interface:
... ... @@ -1577,63 +1560,55 @@ packages:
1577 1560 description:
1578 1561 name: wakelock_plus_platform_interface
1579 1562 sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
1580   - url: "https://pub.dev"
  1563 + url: "https://pub.flutter-io.cn"
1581 1564 source: hosted
1582 1565 version: "1.3.0"
1583 1566 watcher:
1584 1567 dependency: transitive
1585 1568 description:
1586 1569 name: watcher
1587   - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
1588   - url: "https://pub.dev"
  1570 + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
  1571 + url: "https://pub.flutter-io.cn"
1589 1572 source: hosted
1590   - version: "1.2.1"
  1573 + version: "1.1.1"
1591 1574 web:
1592 1575 dependency: transitive
1593 1576 description:
1594 1577 name: web
1595   - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
1596   - url: "https://pub.dev"
1597   - source: hosted
1598   - version: "1.1.1"
1599   - web_socket:
1600   - dependency: transitive
1601   - description:
1602   - name: web_socket
1603   - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
1604   - url: "https://pub.dev"
  1578 + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
  1579 + url: "https://pub.flutter-io.cn"
1605 1580 source: hosted
1606   - version: "1.0.1"
  1581 + version: "0.5.1"
1607 1582 web_socket_channel:
1608 1583 dependency: transitive
1609 1584 description:
1610 1585 name: web_socket_channel
1611   - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
1612   - url: "https://pub.dev"
  1586 + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
  1587 + url: "https://pub.flutter-io.cn"
1613 1588 source: hosted
1614   - version: "3.0.3"
  1589 + version: "2.4.0"
1615 1590 win32:
1616 1591 dependency: transitive
1617 1592 description:
1618 1593 name: win32
1619 1594 sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
1620   - url: "https://pub.dev"
  1595 + url: "https://pub.flutter-io.cn"
1621 1596 source: hosted
1622 1597 version: "5.10.1"
1623 1598 win32_registry:
1624 1599 dependency: transitive
1625 1600 description:
1626 1601 name: win32_registry
1627   - sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
1628   - url: "https://pub.dev"
  1602 + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb"
  1603 + url: "https://pub.flutter-io.cn"
1629 1604 source: hosted
1630   - version: "1.1.5"
  1605 + version: "1.1.3"
1631 1606 xdg_directories:
1632 1607 dependency: transitive
1633 1608 description:
1634 1609 name: xdg_directories
1635 1610 sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
1636   - url: "https://pub.dev"
  1611 + url: "https://pub.flutter-io.cn"
1637 1612 source: hosted
1638 1613 version: "1.1.0"
1639 1614 xml:
... ... @@ -1641,17 +1616,17 @@ packages:
1641 1616 description:
1642 1617 name: xml
1643 1618 sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
1644   - url: "https://pub.dev"
  1619 + url: "https://pub.flutter-io.cn"
1645 1620 source: hosted
1646 1621 version: "6.5.0"
1647 1622 yaml:
1648 1623 dependency: transitive
1649 1624 description:
1650 1625 name: yaml
1651   - sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea
1652   - url: "https://pub.dev"
  1626 + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
  1627 + url: "https://pub.flutter-io.cn"
1653 1628 source: hosted
1654   - version: "3.1.4"
  1629 + version: "3.1.2"
1655 1630 sdks:
1656 1631 dart: ">=3.6.0 <4.0.0"
1657 1632 flutter: ">=3.27.0"
... ...
pubspec.yaml
... ... @@ -16,7 +16,7 @@ publish_to: &#39;none&#39; # Remove this line if you wish to publish to pub.dev
16 16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 17 # In Windows, build-name is used as the major, minor, and patch parts
18 18 # of the product and file versions while build-number is used as the build suffix.
19   -version: 1.0.14+14
  19 +version: 1.0.17+17
20 20  
21 21 environment:
22 22 sdk: '>=3.6.0 <4.0.0'
... ... @@ -95,7 +95,7 @@ dependencies:
95 95 # 富文本插件 https://pub.dev/packages/extended_text
96 96 extended_text: ^11.0.1
97 97 # 视频播放 https://pub.dev/packages/video_player
98   - video_player: 2.9.2
  98 + video_player: 2.10.0
99 99 # 视频播放器控制层 https://pub.dev/packages/chewie
100 100 chewie: 1.7.5
101 101 # 驰声语音评测 SDK(官方离线包裁剪为当前支持架构)
... ... @@ -135,6 +135,9 @@ dependencies:
135 135 dependency_overrides:
136 136 # lottie 3.1.2 depends on http ^1.0.0 umeng_apm_sdk >=2.0.1 depends on http ^0.13.1
137 137 http: ^0.13.3
  138 + # 开启 Media3 解码器初始化失败后的备用解码器回退。
  139 + video_player_android:
  140 + path: packages/video_player_android
138 141  
139 142 dev_dependencies:
140 143 build_runner: ^2.4.4
... ...