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,7 +29,7 @@ apply from: "../archive_apk.gradle"
29 android { 29 android {
30 namespace "com.kouyuxingqiu.wow_english" 30 namespace "com.kouyuxingqiu.wow_english"
31 // compileSdkVersion flutter.compileSdkVersion 31 // compileSdkVersion flutter.compileSdkVersion
32 - compileSdkVersion 34 32 + compileSdkVersion 35
33 // 暂时没有ndk需求 33 // 暂时没有ndk需求
34 // ndkVersion flutter.ndkVersion 34 // ndkVersion flutter.ndkVersion
35 35
android/build.gradle
1 apply from: "/Users/stay/StudioProjects/ishowMavenRepositories.gradle" 1 apply from: "/Users/stay/StudioProjects/ishowMavenRepositories.gradle"
2 2
3 buildscript { 3 buildscript {
4 - ext.kotlin_version = '1.8.21' 4 + ext.kotlin_version = '1.8.22'
5 repositories { 5 repositories {
6 maven { url 'https://maven.aliyun.com/repository/google' } 6 maven { url 'https://maven.aliyun.com/repository/google' }
7 maven { url "https://maven.aliyun.com/repository/public" } 7 maven { url "https://maven.aliyun.com/repository/public" }
@@ -12,8 +12,7 @@ buildscript { @@ -12,8 +12,7 @@ buildscript {
12 } 12 }
13 13
14 dependencies { 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 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 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,5 +2,4 @@ distributionBase=GRADLE_USER_HOME
2 distributionPath=wrapper/dists 2 distributionPath=wrapper/dists
3 zipStoreBase=GRADLE_USER_HOME 3 zipStoreBase=GRADLE_USER_HOME
4 zipStorePath=wrapper/dists 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 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
2 import 'package:flutter_bloc/flutter_bloc.dart'; 4 import 'package:flutter_bloc/flutter_bloc.dart';
3 import 'package:flutter_screenutil/flutter_screenutil.dart'; 5 import 'package:flutter_screenutil/flutter_screenutil.dart';
4 import 'package:video_player/video_player.dart'; 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 import 'package:wow_english/models/read_content_entity.dart'; 9 import 'package:wow_english/models/read_content_entity.dart';
6 10
7 import '../bloc/repeat_after_content_bloc.dart'; 11 import '../bloc/repeat_after_content_bloc.dart';
@@ -23,32 +27,75 @@ class _RepeatVideoWidgetState extends State&lt;RepeatVideoWidget&gt; { @@ -23,32 +27,75 @@ class _RepeatVideoWidgetState extends State&lt;RepeatVideoWidget&gt; {
23 String _totalTime = '00:00'; 27 String _totalTime = '00:00';
24 double _playDegree = 0.5; 28 double _playDegree = 0.5;
25 int _currentPlayIndex = 0; 29 int _currentPlayIndex = 0;
  30 + Object? _loadError;
  31 + Timer? _bufferingTimer;
  32 + VoidCallback? _controllerListener;
  33 + bool _bufferingWarningShown = false;
  34 + int _loadGeneration = 0;
26 35
27 String formatDuration(Duration duration) { 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 return "$minutes:$seconds"; 41 return "$minutes:$seconds";
31 } 42 }
32 43
33 void _addListener() { 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,127 +103,207 @@ class _RepeatVideoWidgetState extends State&lt;RepeatVideoWidget&gt; {
56 void initState() { 103 void initState() {
57 super.initState(); 104 super.initState();
58 var videoUrl = ''; 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 @override 112 @override
66 Widget build(BuildContext context) { 113 Widget build(BuildContext context) {
67 return Center( 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 @override 302 @override
178 void dispose() { 303 void dispose() {
179 - _destroyVideo(); 304 + _loadGeneration++;
  305 + _bufferingTimer?.cancel();
  306 + unawaited(_destroyController());
180 super.dispose(); 307 super.dispose();
181 } 308 }
182 } 309 }
183 \ No newline at end of file 310 \ No newline at end of file
lib/pages/shopping/bloc.dart
@@ -15,13 +15,17 @@ import &#39;../../utils/toast_util.dart&#39;; @@ -15,13 +15,17 @@ import &#39;../../utils/toast_util.dart&#39;;
15 import 'event.dart'; 15 import 'event.dart';
16 import 'state.dart'; 16 import 'state.dart';
17 17
  18 +const bool isWechatPayOptionVisible = false;
  19 +
18 class ShoppingBloc extends Bloc<ShoppingEvent, ShoppingState> { 20 class ShoppingBloc extends Bloc<ShoppingEvent, ShoppingState> {
19 21
20 ProductEntity? _productData; 22 ProductEntity? _productData;
21 23
22 ProductEntity? get productData => _productData; 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 PaymentChannel get curPaymentChannel => _curPaymentChannel; 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,17 +149,26 @@ Widget _paymentWidget() =&gt; BlocBuilder&lt;ShoppingBloc, ShoppingState&gt;(
149 fontFamily: 'PingFangSC-Regular')), 149 fontFamily: 'PingFangSC-Regular')),
150 ), 150 ),
151 const SizedBox(height: 18.0), 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 buildRadioOption( 172 buildRadioOption(
164 value: PaymentChannel.aliPay.payChannelType, 173 value: PaymentChannel.aliPay.payChannelType,
165 icon: AssetImage('zhifubao'.assetPng), 174 icon: AssetImage('zhifubao'.assetPng),
lib/pages/user/setting/setting_page.dart
  1 +import 'package:flutter/foundation.dart';
1 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
2 import 'package:flutter_easyloading/flutter_easyloading.dart'; 3 import 'package:flutter_easyloading/flutter_easyloading.dart';
3 import 'package:flutter_screenutil/flutter_screenutil.dart'; 4 import 'package:flutter_screenutil/flutter_screenutil.dart';
4 import 'package:package_info_plus/package_info_plus.dart'; 5 import 'package:package_info_plus/package_info_plus.dart';
  6 +import 'package:wow_english/common/utils/video_rendering_preference.dart';
5 import 'package:wow_english/common/widgets/we_app_bar.dart'; 7 import 'package:wow_english/common/widgets/we_app_bar.dart';
6 import 'package:wow_english/utils/toast_util.dart'; 8 import 'package:wow_english/utils/toast_util.dart';
7 9
@@ -19,9 +21,11 @@ class SettingPage extends StatefulWidget { @@ -19,9 +21,11 @@ class SettingPage extends StatefulWidget {
19 class SettingPageState extends State<SettingPage> { 21 class SettingPageState extends State<SettingPage> {
20 String? _version; 22 String? _version;
21 String? _buildNum; 23 String? _buildNum;
  24 + bool _usePlatformView = false;
22 @override 25 @override
23 void initState() { 26 void initState() {
24 super.initState(); 27 super.initState();
  28 + _usePlatformView = VideoRenderingPreference.usesPlatformView;
25 _retrieveVersionInfo(); 29 _retrieveVersionInfo();
26 } 30 }
27 31
@@ -62,6 +66,10 @@ class SettingPageState extends State&lt;SettingPage&gt; { @@ -62,6 +66,10 @@ class SettingPageState extends State&lt;SettingPage&gt; {
62 _buildItemWidget('帮助与反馈', onPress: () { 66 _buildItemWidget('帮助与反馈', onPress: () {
63 pushNamed(AppRouteName.reBack); 67 pushNamed(AppRouteName.reBack);
64 }), 68 }),
  69 + if (defaultTargetPlatform == TargetPlatform.android) ...[
  70 + 12.verticalSpace,
  71 + _buildVideoRenderingSetting(),
  72 + ],
65 12.verticalSpace, 73 12.verticalSpace,
66 _buildItemWidget('Version: $_version Build:$_buildNum', 74 _buildItemWidget('Version: $_version Build:$_buildNum',
67 onPress: () {}), 75 onPress: () {}),
@@ -73,6 +81,49 @@ class SettingPageState extends State&lt;SettingPage&gt; { @@ -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 Widget _buildItemWidget(String text, {VoidCallback? onPress}) { 127 Widget _buildItemWidget(String text, {VoidCallback? onPress}) {
77 return OutlinedButton( 128 return OutlinedButton(
78 onPressed: () => onPress?.call(), 129 onPressed: () => onPress?.call(),
lib/pages/video/lookvideo/widgets/video_widget.dart
  1 +import 'dart:async';
  2 +
1 import 'package:common_utils/common_utils.dart'; 3 import 'package:common_utils/common_utils.dart';
2 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
3 import 'package:flutter_bloc/flutter_bloc.dart'; 5 import 'package:flutter_bloc/flutter_bloc.dart';
4 import 'package:flutter_screenutil/flutter_screenutil.dart'; 6 import 'package:flutter_screenutil/flutter_screenutil.dart';
5 import 'package:video_player/video_player.dart'; 7 import 'package:video_player/video_player.dart';
6 import 'package:wow_english/common/extension/string_extension.dart'; 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 import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart'; 11 import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart';
8 import 'package:wow_english/route/route.dart'; 12 import 'package:wow_english/route/route.dart';
9 13
@@ -36,6 +40,11 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -36,6 +40,11 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
36 String _totalTime = '00:00'; 40 String _totalTime = '00:00';
37 double _playDegree = 0.0; 41 double _playDegree = 0.0;
38 bool _hiddenTipView = false; 42 bool _hiddenTipView = false;
  43 + Object? _loadError;
  44 + Timer? _bufferingTimer;
  45 + int _loadGeneration = 0;
  46 + VoidCallback? _diagnosticListener;
  47 + bool _bufferingWarningShown = false;
39 TimerUtil? timerUtil; 48 TimerUtil? timerUtil;
40 49
41 String formatDuration(Duration duration) { 50 String formatDuration(Duration duration) {
@@ -142,19 +151,132 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -142,19 +151,132 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
142 @override 151 @override
143 void initState() { 152 void initState() {
144 super.initState(); 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 @override 282 @override
@@ -162,12 +284,18 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -162,12 +284,18 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
162 return BlocListener<LookVideoBloc, BaseSectionState>( 284 return BlocListener<LookVideoBloc, BaseSectionState>(
163 listener: (context, state) async { 285 listener: (context, state) async {
164 if (state is SectionAgainState) { 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 child: GestureDetector( 294 child: GestureDetector(
170 onTap: () { 295 onTap: () {
  296 + if (_loadError != null || _controller?.value.isInitialized != true) {
  297 + return;
  298 + }
171 setState(() { 299 setState(() {
172 _hiddenTipView = !_hiddenTipView; 300 _hiddenTipView = !_hiddenTipView;
173 if (!_hiddenTipView) { 301 if (!_hiddenTipView) {
@@ -180,7 +308,7 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -180,7 +308,7 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
180 }); 308 });
181 }, 309 },
182 onDoubleTap: () { 310 onDoubleTap: () {
183 - if (_controller!.value.isInitialized) { 311 + if (_controller?.value.isInitialized == true) {
184 if (_controller!.value.isPlaying) { 312 if (_controller!.value.isPlaying) {
185 _controller!.pause(); 313 _controller!.pause();
186 } else { 314 } else {
@@ -190,65 +318,78 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; { @@ -190,65 +318,78 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
190 } 318 }
191 }, 319 },
192 child: Center( 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 children: [ 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,8 +397,18 @@ class _VideoWidgetState extends State&lt;VideoWidget&gt; {
256 397
257 @override 398 @override
258 void dispose() { 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 if (timerUtil != null) { 412 if (timerUtil != null) {
262 timerUtil!.cancel(); 413 timerUtil!.cancel();
263 timerUtil = null; 414 timerUtil = null;
lib/pages/video/zone/video_zone_page.dart
@@ -2,12 +2,12 @@ import &#39;dart:async&#39;; @@ -2,12 +2,12 @@ import &#39;dart:async&#39;;
2 2
3 import 'package:cached_network_image/cached_network_image.dart'; 3 import 'package:cached_network_image/cached_network_image.dart';
4 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
5 -import 'package:flutter/services.dart';  
6 import 'package:flutter_screenutil/flutter_screenutil.dart'; 5 import 'package:flutter_screenutil/flutter_screenutil.dart';
7 import 'package:wow_english/common/dialogs/show_dialog.dart'; 6 import 'package:wow_english/common/dialogs/show_dialog.dart';
8 import 'package:wow_english/common/request/dao/video_dao.dart'; 7 import 'package:wow_english/common/request/dao/video_dao.dart';
9 import 'package:wow_english/common/widgets/we_app_bar.dart'; 8 import 'package:wow_english/common/widgets/we_app_bar.dart';
10 import 'package:wow_english/route/route.dart'; 9 import 'package:wow_english/route/route.dart';
  10 +import 'package:wow_english/utils/toast_util.dart';
11 11
12 import 'video_zone_item.dart'; 12 import 'video_zone_item.dart';
13 13
@@ -24,104 +24,93 @@ class _VideoZonePageState extends State&lt;VideoZonePage&gt; { @@ -24,104 +24,93 @@ class _VideoZonePageState extends State&lt;VideoZonePage&gt; {
24 @override 24 @override
25 void initState() { 25 void initState() {
26 super.initState(); 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 void _reload() { 44 void _reload() {
42 setState(() { 45 setState(() {
43 - _itemsFuture = VideoDao.list(); 46 + _itemsFuture = _loadItems();
44 }); 47 });
45 } 48 }
46 49
47 @override 50 @override
48 Widget build(BuildContext context) { 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,13 +133,7 @@ class _VideoZonePageState extends State&lt;VideoZonePage&gt; {
144 .toList(), 133 .toList(),
145 'initialVideoId': item.id, 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,8 +186,7 @@ class _VideoCard extends StatelessWidget {
203 child: Column( 186 child: Column(
204 crossAxisAlignment: CrossAxisAlignment.start, 187 crossAxisAlignment: CrossAxisAlignment.start,
205 children: [ 188 children: [
206 - AspectRatio(  
207 - aspectRatio: 16 / 9, 189 + Expanded(
208 child: ClipRRect( 190 child: ClipRRect(
209 borderRadius: BorderRadius.circular(12.r), 191 borderRadius: BorderRadius.circular(12.r),
210 child: CachedNetworkImage( 192 child: CachedNetworkImage(
lib/pages/video/zone/video_zone_player_page.dart
@@ -6,7 +6,10 @@ import &#39;package:flutter/material.dart&#39;; @@ -6,7 +6,10 @@ import &#39;package:flutter/material.dart&#39;;
6 import 'package:flutter/services.dart'; 6 import 'package:flutter/services.dart';
7 import 'package:video_player/video_player.dart'; 7 import 'package:video_player/video_player.dart';
8 import 'package:wow_english/common/request/dao/video_dao.dart'; 8 import 'package:wow_english/common/request/dao/video_dao.dart';
  9 +import 'package:wow_english/common/utils/video_playback_diagnostics.dart';
9 import 'package:wow_english/common/utils/video_progress_cache.dart'; 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 import 'video_zone_item.dart'; 14 import 'video_zone_item.dart';
12 15
@@ -26,13 +29,20 @@ class VideoZonePlayerPage extends StatefulWidget { @@ -26,13 +29,20 @@ class VideoZonePlayerPage extends StatefulWidget {
26 29
27 class _VideoZonePlayerPageState extends State<VideoZonePlayerPage> 30 class _VideoZonePlayerPageState extends State<VideoZonePlayerPage>
28 with WidgetsBindingObserver { 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 VideoPlayerController? _controller; 37 VideoPlayerController? _controller;
30 ChewieController? _chewieController; 38 ChewieController? _chewieController;
31 Timer? _cacheTimer; 39 Timer? _cacheTimer;
  40 + Timer? _bufferingTimer;
32 late int _currentIndex; 41 late int _currentIndex;
33 double _playbackSpeed = 1; 42 double _playbackSpeed = 1;
34 bool _switchingVideo = false; 43 bool _switchingVideo = false;
35 bool _handlingCompletion = false; 44 bool _handlingCompletion = false;
  45 + bool _bufferingWarningShown = false;
36 bool? _lastIsPlaying; 46 bool? _lastIsPlaying;
37 int? _completedReportedVideoId; 47 int? _completedReportedVideoId;
38 Object? _loadError; 48 Object? _loadError;
@@ -56,6 +66,8 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt; @@ -56,6 +66,8 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
56 Future<void> _loadCurrentVideo() async { 66 Future<void> _loadCurrentVideo() async {
57 final useCupertinoControls = defaultTargetPlatform == TargetPlatform.iOS; 67 final useCupertinoControls = defaultTargetPlatform == TargetPlatform.iOS;
58 _cacheTimer?.cancel(); 68 _cacheTimer?.cancel();
  69 + _bufferingTimer?.cancel();
  70 + _bufferingWarningShown = false;
59 final oldController = _controller; 71 final oldController = _controller;
60 final oldChewieController = _chewieController; 72 final oldChewieController = _chewieController;
61 if (oldController?.value.isInitialized == true) { 73 if (oldController?.value.isInitialized == true) {
@@ -63,25 +75,32 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt; @@ -63,25 +75,32 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
63 } 75 }
64 oldController?.removeListener(_onVideoChanged); 76 oldController?.removeListener(_onVideoChanged);
65 _lastIsPlaying = null; 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 _controller = controller; 82 _controller = controller;
69 _chewieController = null; 83 _chewieController = null;
70 _loadError = null; 84 _loadError = null;
71 if (mounted) { 85 if (mounted) {
72 setState(() {}); 86 setState(() {});
73 } 87 }
74 - oldChewieController?.dispose();  
75 - await oldController?.dispose();  
76 - 88 + var stage = '释放旧播放器';
77 try { 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 if (!mounted || controller != _controller) { 94 if (!mounted || controller != _controller) {
80 await controller.dispose(); 95 await controller.dispose();
81 return; 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 final savedProgress = await _resumeProgressFor(_currentItem.id); 104 final savedProgress = await _resumeProgressFor(_currentItem.id);
86 if (!mounted || controller != _controller) { 105 if (!mounted || controller != _controller) {
87 await controller.dispose(); 106 await controller.dispose();
@@ -89,10 +108,25 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt; @@ -89,10 +108,25 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
89 } 108 }
90 if (savedProgress > 0 && 109 if (savedProgress > 0 &&
91 savedProgress < controller.value.duration.inSeconds) { 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 _lastIsPlaying = controller.value.isPlaying; 127 _lastIsPlaying = controller.value.isPlaying;
95 controller.addListener(_onVideoChanged); 128 controller.addListener(_onVideoChanged);
  129 + _updateBufferingWatch(controller);
96 _chewieController = ChewieController( 130 _chewieController = ChewieController(
97 videoPlayerController: controller, 131 videoPlayerController: controller,
98 autoPlay: true, 132 autoPlay: true,
@@ -139,15 +173,42 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt; @@ -139,15 +173,42 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
139 ); 173 );
140 setState(() {}); 174 setState(() {});
141 } catch (error) { 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 void _onVideoChanged() { 199 void _onVideoChanged() {
149 final controller = _controller; 200 final controller = _controller;
150 if (!mounted || controller == null) return; 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 final isPlaying = controller.value.isPlaying; 212 final isPlaying = controller.value.isPlaying;
152 if (_lastIsPlaying != null && _lastIsPlaying != isPlaying) { 213 if (_lastIsPlaying != null && _lastIsPlaying != isPlaying) {
153 unawaited(_cacheCurrentProgress()); 214 unawaited(_cacheCurrentProgress());
@@ -162,6 +223,30 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt; @@ -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 Future<void> _playNext({bool currentCompleted = false}) async { 250 Future<void> _playNext({bool currentCompleted = false}) async {
166 if (_switchingVideo || _currentIndex >= widget.items.length - 1) return; 251 if (_switchingVideo || _currentIndex >= widget.items.length - 1) return;
167 _switchingVideo = true; 252 _switchingVideo = true;
@@ -187,6 +272,7 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt; @@ -187,6 +272,7 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
187 SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky), 272 SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky),
188 ); 273 );
189 _cacheTimer?.cancel(); 274 _cacheTimer?.cancel();
  275 + _bufferingTimer?.cancel();
190 _controller?.removeListener(_onVideoChanged); 276 _controller?.removeListener(_onVideoChanged);
191 unawaited(_reportCurrentProgress()); 277 unawaited(_reportCurrentProgress());
192 _chewieController?.dispose(); 278 _chewieController?.dispose();
@@ -237,14 +323,52 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt; @@ -237,14 +323,52 @@ class _VideoZonePlayerPageState extends State&lt;VideoZonePlayerPage&gt;
237 323
238 Future<int> _resumeProgressFor(int videoId) async { 324 Future<int> _resumeProgressFor(int videoId) async {
239 try { 325 try {
240 - final cached = await VideoProgressCache.get(videoId); 326 + final cached =
  327 + await VideoProgressCache.get(videoId).timeout(_progressReadTimeout);
241 if (cached != null) return cached.progressSeconds; 328 if (cached != null) return cached.progressSeconds;
  329 + } on TimeoutException catch (error) {
  330 + debugPrint('读取本地视频进度超时(videoId=$videoId): $error');
  331 + _showVideoToast('本地进度读取超时,已从头播放');
242 } catch (error) { 332 } catch (error) {
243 debugPrint('读取本地视频播放进度失败: $error'); 333 debugPrint('读取本地视频播放进度失败: $error');
  334 + _showVideoToast('本地进度读取失败,已从头播放');
244 } 335 }
245 return 0; 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 Future<void> _cacheCurrentProgress() async { 372 Future<void> _cacheCurrentProgress() async {
249 final controller = _controller; 373 final controller = _controller;
250 if (controller == null || !controller.value.isInitialized) return; 374 if (controller == null || !controller.value.isInitialized) return;
packages/chivox_aiengine/android/build.gradle
@@ -22,6 +22,7 @@ rootProject.allprojects { @@ -22,6 +22,7 @@ rootProject.allprojects {
22 apply plugin: 'com.android.library' 22 apply plugin: 'com.android.library'
23 23
24 android { 24 android {
  25 + namespace 'com.example.chivox_aiengine'
25 compileSdkVersion 31 26 compileSdkVersion 31
26 27
27 compileOptions { 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,56 +5,56 @@ packages:
5 dependency: transitive 5 dependency: transitive
6 description: 6 description:
7 name: _fe_analyzer_shared 7 name: _fe_analyzer_shared
8 - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f  
9 - url: "https://pub.dev" 8 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
  9 + url: "https://pub.flutter-io.cn"
10 source: hosted 10 source: hosted
11 - version: "85.0.0" 11 + version: "67.0.0"
12 analyzer: 12 analyzer:
13 dependency: transitive 13 dependency: transitive
14 description: 14 description:
15 name: analyzer 15 name: analyzer
16 - sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"  
17 - url: "https://pub.dev" 16 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
  17 + url: "https://pub.flutter-io.cn"
18 source: hosted 18 source: hosted
19 - version: "7.7.1" 19 + version: "6.4.1"
20 archive: 20 archive:
21 dependency: transitive 21 dependency: transitive
22 description: 22 description:
23 name: archive 23 name: archive
24 - sha256: ace891da0862b0e4cabbb064ee3fd87b2728b898949fdb366d83fe98342c9f19  
25 - url: "https://pub.dev" 24 + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
  25 + url: "https://pub.flutter-io.cn"
26 source: hosted 26 source: hosted
27 - version: "4.2.0" 27 + version: "3.6.1"
28 args: 28 args:
29 dependency: transitive 29 dependency: transitive
30 description: 30 description:
31 name: args 31 name: args
32 - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04  
33 - url: "https://pub.dev" 32 + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
  33 + url: "https://pub.flutter-io.cn"
34 source: hosted 34 source: hosted
35 - version: "2.7.0" 35 + version: "2.6.0"
36 async: 36 async:
37 dependency: transitive 37 dependency: transitive
38 description: 38 description:
39 name: async 39 name: async
40 sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 40 sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
41 - url: "https://pub.dev" 41 + url: "https://pub.flutter-io.cn"
42 source: hosted 42 source: hosted
43 version: "2.11.0" 43 version: "2.11.0"
44 audio_session: 44 audio_session:
45 dependency: "direct main" 45 dependency: "direct main"
46 description: 46 description:
47 name: audio_session 47 name: audio_session
48 - sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac"  
49 - url: "https://pub.dev" 48 + sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261"
  49 + url: "https://pub.flutter-io.cn"
50 source: hosted 50 source: hosted
51 - version: "0.1.25" 51 + version: "0.1.21"
52 audioplayers: 52 audioplayers:
53 dependency: "direct main" 53 dependency: "direct main"
54 description: 54 description:
55 name: audioplayers 55 name: audioplayers
56 sha256: c346ba5a39dc208f1bab55fc239855f573d69b0e832402114bf0b793622adc4d 56 sha256: c346ba5a39dc208f1bab55fc239855f573d69b0e832402114bf0b793622adc4d
57 - url: "https://pub.dev" 57 + url: "https://pub.flutter-io.cn"
58 source: hosted 58 source: hosted
59 version: "6.1.0" 59 version: "6.1.0"
60 audioplayers_android: 60 audioplayers_android:
@@ -62,55 +62,55 @@ packages: @@ -62,55 +62,55 @@ packages:
62 description: 62 description:
63 name: audioplayers_android 63 name: audioplayers_android
64 sha256: "5a09cf83ac1a1a8e4392eafa5f66ddc21fc435a605d37542dd9c2f838d516e26" 64 sha256: "5a09cf83ac1a1a8e4392eafa5f66ddc21fc435a605d37542dd9c2f838d516e26"
65 - url: "https://pub.dev" 65 + url: "https://pub.flutter-io.cn"
66 source: hosted 66 source: hosted
67 version: "5.0.2" 67 version: "5.0.2"
68 audioplayers_darwin: 68 audioplayers_darwin:
69 dependency: transitive 69 dependency: transitive
70 description: 70 description:
71 name: audioplayers_darwin 71 name: audioplayers_darwin
72 - sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91  
73 - url: "https://pub.dev" 72 + sha256: "34e1fb3a88ba02566615c6ca7173aa93b39cf61a38a05a729b60ba14c4899169"
  73 + url: "https://pub.flutter-io.cn"
74 source: hosted 74 source: hosted
75 - version: "6.4.0" 75 + version: "6.1.0"
76 audioplayers_linux: 76 audioplayers_linux:
77 dependency: transitive 77 dependency: transitive
78 description: 78 description:
79 name: audioplayers_linux 79 name: audioplayers_linux
80 - sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001  
81 - url: "https://pub.dev" 80 + sha256: ce8383c1ff0db1ba93b5b0b8ef5b260ec2d0ce2598ad37dc8b946b773dd2c5fa
  81 + url: "https://pub.flutter-io.cn"
82 source: hosted 82 source: hosted
83 - version: "4.2.1" 83 + version: "4.1.0"
84 audioplayers_platform_interface: 84 audioplayers_platform_interface:
85 dependency: transitive 85 dependency: transitive
86 description: 86 description:
87 name: audioplayers_platform_interface 87 name: audioplayers_platform_interface
88 - sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"  
89 - url: "https://pub.dev" 88 + sha256: "6834dd48dfb7bc6c2404998ebdd161f79cd3774a7e6779e1348d54a3bfdcfaa5"
  89 + url: "https://pub.flutter-io.cn"
90 source: hosted 90 source: hosted
91 - version: "7.1.1" 91 + version: "7.0.0"
92 audioplayers_web: 92 audioplayers_web:
93 dependency: transitive 93 dependency: transitive
94 description: 94 description:
95 name: audioplayers_web 95 name: audioplayers_web
96 - sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9  
97 - url: "https://pub.dev" 96 + sha256: "3609bdf0e05e66a3d9750ee40b1e37f2a622c4edb796cc600b53a90a30a2ace4"
  97 + url: "https://pub.flutter-io.cn"
98 source: hosted 98 source: hosted
99 - version: "5.2.0" 99 + version: "5.0.1"
100 audioplayers_windows: 100 audioplayers_windows:
101 dependency: transitive 101 dependency: transitive
102 description: 102 description:
103 name: audioplayers_windows 103 name: audioplayers_windows
104 - sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734  
105 - url: "https://pub.dev" 104 + sha256: "52b57c706a20fc85daa911be67381b3a5c9d03f8e27cb9fdb226de5bddf293b1"
  105 + url: "https://pub.flutter-io.cn"
106 source: hosted 106 source: hosted
107 - version: "4.3.0" 107 + version: "4.1.0"
108 bloc: 108 bloc:
109 dependency: transitive 109 dependency: transitive
110 description: 110 description:
111 name: bloc 111 name: bloc
112 sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" 112 sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
113 - url: "https://pub.dev" 113 + url: "https://pub.flutter-io.cn"
114 source: hosted 114 source: hosted
115 version: "8.1.4" 115 version: "8.1.4"
116 boolean_selector: 116 boolean_selector:
@@ -118,103 +118,103 @@ packages: @@ -118,103 +118,103 @@ packages:
118 description: 118 description:
119 name: boolean_selector 119 name: boolean_selector
120 sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 120 sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
121 - url: "https://pub.dev" 121 + url: "https://pub.flutter-io.cn"
122 source: hosted 122 source: hosted
123 version: "2.1.1" 123 version: "2.1.1"
124 build: 124 build:
125 dependency: transitive 125 dependency: transitive
126 description: 126 description:
127 name: build 127 name: build
128 - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0  
129 - url: "https://pub.dev" 128 + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
  129 + url: "https://pub.flutter-io.cn"
130 source: hosted 130 source: hosted
131 - version: "2.4.2" 131 + version: "2.4.1"
132 build_config: 132 build_config:
133 dependency: transitive 133 dependency: transitive
134 description: 134 description:
135 name: build_config 135 name: build_config
136 - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"  
137 - url: "https://pub.dev" 136 + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
  137 + url: "https://pub.flutter-io.cn"
138 source: hosted 138 source: hosted
139 - version: "1.1.2" 139 + version: "1.1.1"
140 build_daemon: 140 build_daemon:
141 dependency: transitive 141 dependency: transitive
142 description: 142 description:
143 name: build_daemon 143 name: build_daemon
144 - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"  
145 - url: "https://pub.dev" 144 + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
  145 + url: "https://pub.flutter-io.cn"
146 source: hosted 146 source: hosted
147 - version: "4.0.4" 147 + version: "4.0.1"
148 build_resolvers: 148 build_resolvers:
149 dependency: transitive 149 dependency: transitive
150 description: 150 description:
151 name: build_resolvers 151 name: build_resolvers
152 - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0  
153 - url: "https://pub.dev" 152 + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
  153 + url: "https://pub.flutter-io.cn"
154 source: hosted 154 source: hosted
155 - version: "2.4.4" 155 + version: "2.4.2"
156 build_runner: 156 build_runner:
157 dependency: "direct dev" 157 dependency: "direct dev"
158 description: 158 description:
159 name: build_runner 159 name: build_runner
160 - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"  
161 - url: "https://pub.dev" 160 + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
  161 + url: "https://pub.flutter-io.cn"
162 source: hosted 162 source: hosted
163 - version: "2.4.15" 163 + version: "2.4.9"
164 build_runner_core: 164 build_runner_core:
165 dependency: transitive 165 dependency: transitive
166 description: 166 description:
167 name: build_runner_core 167 name: build_runner_core
168 - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"  
169 - url: "https://pub.dev" 168 + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799"
  169 + url: "https://pub.flutter-io.cn"
170 source: hosted 170 source: hosted
171 - version: "8.0.0" 171 + version: "7.3.0"
172 built_collection: 172 built_collection:
173 dependency: transitive 173 dependency: transitive
174 description: 174 description:
175 name: built_collection 175 name: built_collection
176 sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" 176 sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
177 - url: "https://pub.dev" 177 + url: "https://pub.flutter-io.cn"
178 source: hosted 178 source: hosted
179 version: "5.1.1" 179 version: "5.1.1"
180 built_value: 180 built_value:
181 dependency: transitive 181 dependency: transitive
182 description: 182 description:
183 name: built_value 183 name: built_value
184 - sha256: f87ea98192116f7093cb214551ce1929caae0681fdba282b3d8b4462adee7bb7  
185 - url: "https://pub.dev" 184 + sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61"
  185 + url: "https://pub.flutter-io.cn"
186 source: hosted 186 source: hosted
187 - version: "8.13.0" 187 + version: "8.9.4"
188 cached_network_image: 188 cached_network_image:
189 dependency: "direct main" 189 dependency: "direct main"
190 description: 190 description:
191 name: cached_network_image 191 name: cached_network_image
192 - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"  
193 - url: "https://pub.dev" 192 + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
  193 + url: "https://pub.flutter-io.cn"
194 source: hosted 194 source: hosted
195 - version: "3.4.1" 195 + version: "3.3.1"
196 cached_network_image_platform_interface: 196 cached_network_image_platform_interface:
197 dependency: transitive 197 dependency: transitive
198 description: 198 description:
199 name: cached_network_image_platform_interface 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 source: hosted 202 source: hosted
203 - version: "4.1.1" 203 + version: "4.0.0"
204 cached_network_image_web: 204 cached_network_image_web:
205 dependency: transitive 205 dependency: transitive
206 description: 206 description:
207 name: cached_network_image_web 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 source: hosted 210 source: hosted
211 - version: "1.3.1" 211 + version: "1.2.0"
212 card_swiper: 212 card_swiper:
213 dependency: "direct main" 213 dependency: "direct main"
214 description: 214 description:
215 name: card_swiper 215 name: card_swiper
216 sha256: "0c94c538f47be1dab52d018d4900a7046b4cb0700dc7f95b8628da89d1212b35" 216 sha256: "0c94c538f47be1dab52d018d4900a7046b4cb0700dc7f95b8628da89d1212b35"
217 - url: "https://pub.dev" 217 + url: "https://pub.flutter-io.cn"
218 source: hosted 218 source: hosted
219 version: "2.0.4" 219 version: "2.0.4"
220 characters: 220 characters:
@@ -222,7 +222,7 @@ packages: @@ -222,7 +222,7 @@ packages:
222 description: 222 description:
223 name: characters 223 name: characters
224 sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 224 sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
225 - url: "https://pub.dev" 225 + url: "https://pub.flutter-io.cn"
226 source: hosted 226 source: hosted
227 version: "1.3.0" 227 version: "1.3.0"
228 checked_yaml: 228 checked_yaml:
@@ -230,7 +230,7 @@ packages: @@ -230,7 +230,7 @@ packages:
230 description: 230 description:
231 name: checked_yaml 231 name: checked_yaml
232 sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff 232 sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
233 - url: "https://pub.dev" 233 + url: "https://pub.flutter-io.cn"
234 source: hosted 234 source: hosted
235 version: "2.0.3" 235 version: "2.0.3"
236 chewie: 236 chewie:
@@ -238,7 +238,7 @@ packages: @@ -238,7 +238,7 @@ packages:
238 description: 238 description:
239 name: chewie 239 name: chewie
240 sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144" 240 sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144"
241 - url: "https://pub.dev" 241 + url: "https://pub.flutter-io.cn"
242 source: hosted 242 source: hosted
243 version: "1.7.5" 243 version: "1.7.5"
244 chivox_aiengine: 244 chivox_aiengine:
@@ -253,23 +253,23 @@ packages: @@ -253,23 +253,23 @@ packages:
253 description: 253 description:
254 name: clock 254 name: clock
255 sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 255 sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
256 - url: "https://pub.dev" 256 + url: "https://pub.flutter-io.cn"
257 source: hosted 257 source: hosted
258 version: "1.1.1" 258 version: "1.1.1"
259 code_builder: 259 code_builder:
260 dependency: transitive 260 dependency: transitive
261 description: 261 description:
262 name: code_builder 262 name: code_builder
263 - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"  
264 - url: "https://pub.dev" 263 + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
  264 + url: "https://pub.flutter-io.cn"
265 source: hosted 265 source: hosted
266 - version: "4.10.1" 266 + version: "4.10.0"
267 collection: 267 collection:
268 dependency: transitive 268 dependency: transitive
269 description: 269 description:
270 name: collection 270 name: collection
271 sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf 271 sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
272 - url: "https://pub.dev" 272 + url: "https://pub.flutter-io.cn"
273 source: hosted 273 source: hosted
274 version: "1.19.0" 274 version: "1.19.0"
275 common_utils: 275 common_utils:
@@ -277,7 +277,7 @@ packages: @@ -277,7 +277,7 @@ packages:
277 description: 277 description:
278 name: common_utils 278 name: common_utils
279 sha256: c26884339b13ff99b0739e56f4b02090c84054ed9dd3a045435cd24e7b99c2c1 279 sha256: c26884339b13ff99b0739e56f4b02090c84054ed9dd3a045435cd24e7b99c2c1
280 - url: "https://pub.dev" 280 + url: "https://pub.flutter-io.cn"
281 source: hosted 281 source: hosted
282 version: "2.1.0" 282 version: "2.1.0"
283 connectivity_plus: 283 connectivity_plus:
@@ -285,7 +285,7 @@ packages: @@ -285,7 +285,7 @@ packages:
285 description: 285 description:
286 name: connectivity_plus 286 name: connectivity_plus
287 sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b" 287 sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b"
288 - url: "https://pub.dev" 288 + url: "https://pub.flutter-io.cn"
289 source: hosted 289 source: hosted
290 version: "4.0.2" 290 version: "4.0.2"
291 connectivity_plus_platform_interface: 291 connectivity_plus_platform_interface:
@@ -293,39 +293,39 @@ packages: @@ -293,39 +293,39 @@ packages:
293 description: 293 description:
294 name: connectivity_plus_platform_interface 294 name: connectivity_plus_platform_interface
295 sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a 295 sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
296 - url: "https://pub.dev" 296 + url: "https://pub.flutter-io.cn"
297 source: hosted 297 source: hosted
298 version: "1.2.4" 298 version: "1.2.4"
299 convert: 299 convert:
300 dependency: transitive 300 dependency: transitive
301 description: 301 description:
302 name: convert 302 name: convert
303 - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68  
304 - url: "https://pub.dev" 303 + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
  304 + url: "https://pub.flutter-io.cn"
305 source: hosted 305 source: hosted
306 - version: "3.1.2" 306 + version: "3.1.1"
307 cross_file: 307 cross_file:
308 dependency: transitive 308 dependency: transitive
309 description: 309 description:
310 name: cross_file 310 name: cross_file
311 sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" 311 sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
312 - url: "https://pub.dev" 312 + url: "https://pub.flutter-io.cn"
313 source: hosted 313 source: hosted
314 version: "0.3.4+2" 314 version: "0.3.4+2"
315 crypto: 315 crypto:
316 dependency: transitive 316 dependency: transitive
317 description: 317 description:
318 name: crypto 318 name: crypto
319 - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf  
320 - url: "https://pub.dev" 319 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
  320 + url: "https://pub.flutter-io.cn"
321 source: hosted 321 source: hosted
322 - version: "3.0.7" 322 + version: "3.0.3"
323 csslib: 323 csslib:
324 dependency: transitive 324 dependency: transitive
325 description: 325 description:
326 name: csslib 326 name: csslib
327 sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" 327 sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
328 - url: "https://pub.dev" 328 + url: "https://pub.flutter-io.cn"
329 source: hosted 329 source: hosted
330 version: "1.0.2" 330 version: "1.0.2"
331 cupertino_icons: 331 cupertino_icons:
@@ -333,31 +333,31 @@ packages: @@ -333,31 +333,31 @@ packages:
333 description: 333 description:
334 name: cupertino_icons 334 name: cupertino_icons
335 sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 335 sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
336 - url: "https://pub.dev" 336 + url: "https://pub.flutter-io.cn"
337 source: hosted 337 source: hosted
338 version: "1.0.8" 338 version: "1.0.8"
339 dart_style: 339 dart_style:
340 dependency: transitive 340 dependency: transitive
341 description: 341 description:
342 name: dart_style 342 name: dart_style
343 - sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"  
344 - url: "https://pub.dev" 343 + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
  344 + url: "https://pub.flutter-io.cn"
345 source: hosted 345 source: hosted
346 - version: "3.0.1" 346 + version: "2.3.6"
347 dbus: 347 dbus:
348 dependency: transitive 348 dependency: transitive
349 description: 349 description:
350 name: dbus 350 name: dbus
351 - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270  
352 - url: "https://pub.dev" 351 + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
  352 + url: "https://pub.flutter-io.cn"
353 source: hosted 353 source: hosted
354 - version: "0.7.12" 354 + version: "0.7.11"
355 decimal: 355 decimal:
356 dependency: "direct main" 356 dependency: "direct main"
357 description: 357 description:
358 name: decimal 358 name: decimal
359 sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" 359 sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21"
360 - url: "https://pub.dev" 360 + url: "https://pub.flutter-io.cn"
361 source: hosted 361 source: hosted
362 version: "2.3.3" 362 version: "2.3.3"
363 device_info_plus: 363 device_info_plus:
@@ -365,7 +365,7 @@ packages: @@ -365,7 +365,7 @@ packages:
365 description: 365 description:
366 name: device_info_plus 366 name: device_info_plus
367 sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" 367 sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110"
368 - url: "https://pub.dev" 368 + url: "https://pub.flutter-io.cn"
369 source: hosted 369 source: hosted
370 version: "9.1.2" 370 version: "9.1.2"
371 device_info_plus_platform_interface: 371 device_info_plus_platform_interface:
@@ -373,39 +373,39 @@ packages: @@ -373,39 +373,39 @@ packages:
373 description: 373 description:
374 name: device_info_plus_platform_interface 374 name: device_info_plus_platform_interface
375 sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" 375 sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2"
376 - url: "https://pub.dev" 376 + url: "https://pub.flutter-io.cn"
377 source: hosted 377 source: hosted
378 version: "7.0.2" 378 version: "7.0.2"
379 dio: 379 dio:
380 dependency: "direct main" 380 dependency: "direct main"
381 description: 381 description:
382 name: dio 382 name: dio
383 - sha256: "852ec3b48cc431ac04fff978413c541502b67ffc3e26921e74e3d994694192c1"  
384 - url: "https://pub.dev" 383 + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
  384 + url: "https://pub.flutter-io.cn"
385 source: hosted 385 source: hosted
386 - version: "5.11.1" 386 + version: "5.8.0+1"
387 dio_web_adapter: 387 dio_web_adapter:
388 dependency: transitive 388 dependency: transitive
389 description: 389 description:
390 name: dio_web_adapter 390 name: dio_web_adapter
391 - sha256: "3a1b2cd7be71086f38504956e3ebcd2837288d231ff454bafa78021244102bfc"  
392 - url: "https://pub.dev" 391 + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
  392 + url: "https://pub.flutter-io.cn"
393 source: hosted 393 source: hosted
394 - version: "2.2.2" 394 + version: "2.1.1"
395 equatable: 395 equatable:
396 dependency: "direct main" 396 dependency: "direct main"
397 description: 397 description:
398 name: equatable 398 name: equatable
399 - sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2"  
400 - url: "https://pub.dev" 399 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
  400 + url: "https://pub.flutter-io.cn"
401 source: hosted 401 source: hosted
402 - version: "2.1.0" 402 + version: "2.0.7"
403 extended_text: 403 extended_text:
404 dependency: "direct main" 404 dependency: "direct main"
405 description: 405 description:
406 name: extended_text 406 name: extended_text
407 sha256: "32119a9447888fe97f506df6c87e23744ea2dbdf25a75fd713030cc7bc055c3a" 407 sha256: "32119a9447888fe97f506df6c87e23744ea2dbdf25a75fd713030cc7bc055c3a"
408 - url: "https://pub.dev" 408 + url: "https://pub.flutter-io.cn"
409 source: hosted 409 source: hosted
410 version: "11.2.0" 410 version: "11.2.0"
411 extended_text_library: 411 extended_text_library:
@@ -413,7 +413,7 @@ packages: @@ -413,7 +413,7 @@ packages:
413 description: 413 description:
414 name: extended_text_library 414 name: extended_text_library
415 sha256: f6ae456324ff6ba3f9fe560a5c12f51ec02c5f463ef5a02bf165b678f049d547 415 sha256: f6ae456324ff6ba3f9fe560a5c12f51ec02c5f463ef5a02bf165b678f049d547
416 - url: "https://pub.dev" 416 + url: "https://pub.flutter-io.cn"
417 source: hosted 417 source: hosted
418 version: "11.1.0" 418 version: "11.1.0"
419 fake_async: 419 fake_async:
@@ -421,7 +421,7 @@ packages: @@ -421,7 +421,7 @@ packages:
421 description: 421 description:
422 name: fake_async 422 name: fake_async
423 sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 423 sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
424 - url: "https://pub.dev" 424 + url: "https://pub.flutter-io.cn"
425 source: hosted 425 source: hosted
426 version: "1.3.1" 426 version: "1.3.1"
427 ffi: 427 ffi:
@@ -429,55 +429,55 @@ packages: @@ -429,55 +429,55 @@ packages:
429 description: 429 description:
430 name: ffi 430 name: ffi
431 sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" 431 sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
432 - url: "https://pub.dev" 432 + url: "https://pub.flutter-io.cn"
433 source: hosted 433 source: hosted
434 version: "2.1.3" 434 version: "2.1.3"
435 file: 435 file:
436 dependency: transitive 436 dependency: transitive
437 description: 437 description:
438 name: file 438 name: file
439 - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4  
440 - url: "https://pub.dev" 439 + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
  440 + url: "https://pub.flutter-io.cn"
441 source: hosted 441 source: hosted
442 - version: "7.0.1" 442 + version: "6.1.4"
443 file_selector_linux: 443 file_selector_linux:
444 dependency: transitive 444 dependency: transitive
445 description: 445 description:
446 name: file_selector_linux 446 name: file_selector_linux
447 sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" 447 sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
448 - url: "https://pub.dev" 448 + url: "https://pub.flutter-io.cn"
449 source: hosted 449 source: hosted
450 version: "0.9.3+2" 450 version: "0.9.3+2"
451 file_selector_macos: 451 file_selector_macos:
452 dependency: transitive 452 dependency: transitive
453 description: 453 description:
454 name: file_selector_macos 454 name: file_selector_macos
455 - sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711"  
456 - url: "https://pub.dev" 455 + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
  456 + url: "https://pub.flutter-io.cn"
457 source: hosted 457 source: hosted
458 - version: "0.9.4+3" 458 + version: "0.9.4+2"
459 file_selector_platform_interface: 459 file_selector_platform_interface:
460 dependency: transitive 460 dependency: transitive
461 description: 461 description:
462 name: file_selector_platform_interface 462 name: file_selector_platform_interface
463 sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b 463 sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
464 - url: "https://pub.dev" 464 + url: "https://pub.flutter-io.cn"
465 source: hosted 465 source: hosted
466 version: "2.6.2" 466 version: "2.6.2"
467 file_selector_windows: 467 file_selector_windows:
468 dependency: transitive 468 dependency: transitive
469 description: 469 description:
470 name: file_selector_windows 470 name: file_selector_windows
471 - sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"  
472 - url: "https://pub.dev" 471 + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
  472 + url: "https://pub.flutter-io.cn"
473 source: hosted 473 source: hosted
474 - version: "0.9.3+4" 474 + version: "0.9.3+3"
475 fixnum: 475 fixnum:
476 dependency: transitive 476 dependency: transitive
477 description: 477 description:
478 name: fixnum 478 name: fixnum
479 sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be 479 sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
480 - url: "https://pub.dev" 480 + url: "https://pub.flutter-io.cn"
481 source: hosted 481 source: hosted
482 version: "1.1.1" 482 version: "1.1.1"
483 flutter: 483 flutter:
@@ -489,32 +489,32 @@ packages: @@ -489,32 +489,32 @@ packages:
489 dependency: "direct main" 489 dependency: "direct main"
490 description: 490 description:
491 name: flutter_app_update 491 name: flutter_app_update
492 - sha256: "336a87c9b8f5c2f040ac314939f61a567e812a3ec3c066d8c99a524d95171d81"  
493 - url: "https://pub.dev" 492 + sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65"
  493 + url: "https://pub.flutter-io.cn"
494 source: hosted 494 source: hosted
495 - version: "3.2.4" 495 + version: "3.2.2"
496 flutter_bloc: 496 flutter_bloc:
497 dependency: "direct main" 497 dependency: "direct main"
498 description: 498 description:
499 name: flutter_bloc 499 name: flutter_bloc
500 sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a 500 sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
501 - url: "https://pub.dev" 501 + url: "https://pub.flutter-io.cn"
502 source: hosted 502 source: hosted
503 version: "8.1.6" 503 version: "8.1.6"
504 flutter_cache_manager: 504 flutter_cache_manager:
505 dependency: transitive 505 dependency: transitive
506 description: 506 description:
507 name: flutter_cache_manager 507 name: flutter_cache_manager
508 - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"  
509 - url: "https://pub.dev" 508 + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
  509 + url: "https://pub.flutter-io.cn"
510 source: hosted 510 source: hosted
511 - version: "3.4.1" 511 + version: "3.3.1"
512 flutter_easyloading: 512 flutter_easyloading:
513 dependency: "direct main" 513 dependency: "direct main"
514 description: 514 description:
515 name: flutter_easyloading 515 name: flutter_easyloading
516 sha256: ba21a3c883544e582f9cc455a4a0907556714e1e9cf0eababfcb600da191d17c 516 sha256: ba21a3c883544e582f9cc455a4a0907556714e1e9cf0eababfcb600da191d17c
517 - url: "https://pub.dev" 517 + url: "https://pub.flutter-io.cn"
518 source: hosted 518 source: hosted
519 version: "3.0.5" 519 version: "3.0.5"
520 flutter_inappwebview: 520 flutter_inappwebview:
@@ -522,63 +522,63 @@ packages: @@ -522,63 +522,63 @@ packages:
522 description: 522 description:
523 name: flutter_inappwebview 523 name: flutter_inappwebview
524 sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" 524 sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959"
525 - url: "https://pub.dev" 525 + url: "https://pub.flutter-io.cn"
526 source: hosted 526 source: hosted
527 version: "6.0.0" 527 version: "6.0.0"
528 flutter_inappwebview_android: 528 flutter_inappwebview_android:
529 dependency: transitive 529 dependency: transitive
530 description: 530 description:
531 name: flutter_inappwebview_android 531 name: flutter_inappwebview_android
532 - sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"  
533 - url: "https://pub.dev" 532 + sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421
  533 + url: "https://pub.flutter-io.cn"
534 source: hosted 534 source: hosted
535 - version: "1.1.3" 535 + version: "1.0.13"
536 flutter_inappwebview_internal_annotations: 536 flutter_inappwebview_internal_annotations:
537 dependency: transitive 537 dependency: transitive
538 description: 538 description:
539 name: flutter_inappwebview_internal_annotations 539 name: flutter_inappwebview_internal_annotations
540 sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" 540 sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd"
541 - url: "https://pub.dev" 541 + url: "https://pub.flutter-io.cn"
542 source: hosted 542 source: hosted
543 version: "1.2.0" 543 version: "1.2.0"
544 flutter_inappwebview_ios: 544 flutter_inappwebview_ios:
545 dependency: transitive 545 dependency: transitive
546 description: 546 description:
547 name: flutter_inappwebview_ios 547 name: flutter_inappwebview_ios
548 - sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"  
549 - url: "https://pub.dev" 548 + sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f
  549 + url: "https://pub.flutter-io.cn"
550 source: hosted 550 source: hosted
551 - version: "1.1.2" 551 + version: "1.0.13"
552 flutter_inappwebview_macos: 552 flutter_inappwebview_macos:
553 dependency: transitive 553 dependency: transitive
554 description: 554 description:
555 name: flutter_inappwebview_macos 555 name: flutter_inappwebview_macos
556 - sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1  
557 - url: "https://pub.dev" 556 + sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636
  557 + url: "https://pub.flutter-io.cn"
558 source: hosted 558 source: hosted
559 - version: "1.1.2" 559 + version: "1.0.11"
560 flutter_inappwebview_platform_interface: 560 flutter_inappwebview_platform_interface:
561 dependency: transitive 561 dependency: transitive
562 description: 562 description:
563 name: flutter_inappwebview_platform_interface 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 source: hosted 566 source: hosted
567 - version: "1.3.0+1" 567 + version: "1.0.10"
568 flutter_inappwebview_web: 568 flutter_inappwebview_web:
569 dependency: transitive 569 dependency: transitive
570 description: 570 description:
571 name: flutter_inappwebview_web 571 name: flutter_inappwebview_web
572 - sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"  
573 - url: "https://pub.dev" 572 + sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07
  573 + url: "https://pub.flutter-io.cn"
574 source: hosted 574 source: hosted
575 - version: "1.1.2" 575 + version: "1.0.8"
576 flutter_lints: 576 flutter_lints:
577 dependency: "direct dev" 577 dependency: "direct dev"
578 description: 578 description:
579 name: flutter_lints 579 name: flutter_lints
580 sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 580 sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
581 - url: "https://pub.dev" 581 + url: "https://pub.flutter-io.cn"
582 source: hosted 582 source: hosted
583 version: "2.0.3" 583 version: "2.0.3"
584 flutter_oss_aliyun: 584 flutter_oss_aliyun:
@@ -586,57 +586,57 @@ packages: @@ -586,57 +586,57 @@ packages:
586 description: 586 description:
587 name: flutter_oss_aliyun 587 name: flutter_oss_aliyun
588 sha256: "8280c1e8dfb792dec6449d1e6052e1d3492b3b3a1dfee456cc41d3f31b4cbc26" 588 sha256: "8280c1e8dfb792dec6449d1e6052e1d3492b3b3a1dfee456cc41d3f31b4cbc26"
589 - url: "https://pub.dev" 589 + url: "https://pub.flutter-io.cn"
590 source: hosted 590 source: hosted
591 version: "6.4.2" 591 version: "6.4.2"
592 flutter_plugin_android_lifecycle: 592 flutter_plugin_android_lifecycle:
593 dependency: transitive 593 dependency: transitive
594 description: 594 description:
595 name: flutter_plugin_android_lifecycle 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 source: hosted 598 source: hosted
599 - version: "2.0.29" 599 + version: "2.0.19"
600 flutter_screenutil: 600 flutter_screenutil:
601 dependency: "direct main" 601 dependency: "direct main"
602 description: 602 description:
603 name: flutter_screenutil 603 name: flutter_screenutil
604 sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de" 604 sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de"
605 - url: "https://pub.dev" 605 + url: "https://pub.flutter-io.cn"
606 source: hosted 606 source: hosted
607 version: "5.9.3" 607 version: "5.9.3"
608 flutter_sound: 608 flutter_sound:
609 dependency: "direct main" 609 dependency: "direct main"
610 description: 610 description:
611 name: flutter_sound 611 name: flutter_sound
612 - sha256: ef89477f6e8ce2fa395158ebc4a8b11982e3ada440b4021c06fd97a4e771554b  
613 - url: "https://pub.dev" 612 + sha256: "1f40b26b92907a433afe877c927cd48f5a2e4d0f7188e5d39eb5756008aa51ab"
  613 + url: "https://pub.flutter-io.cn"
614 source: hosted 614 source: hosted
615 - version: "9.28.0" 615 + version: "9.6.0"
616 flutter_sound_platform_interface: 616 flutter_sound_platform_interface:
617 dependency: transitive 617 dependency: transitive
618 description: 618 description:
619 name: flutter_sound_platform_interface 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 source: hosted 622 source: hosted
623 - version: "9.28.0" 623 + version: "9.6.0"
624 flutter_sound_web: 624 flutter_sound_web:
625 dependency: transitive 625 dependency: transitive
626 description: 626 description:
627 name: flutter_sound_web 627 name: flutter_sound_web
628 - sha256: "4e10c94a8574bd93bb8668af59bf76f5312a890bccd3778d73168a7133217dc5"  
629 - url: "https://pub.dev" 628 + sha256: "5013a15e4e69a4bdc8badd729130eef43c3305e30ba8e6933f863436ce969932"
  629 + url: "https://pub.flutter-io.cn"
630 source: hosted 630 source: hosted
631 - version: "9.28.0" 631 + version: "9.6.0"
632 flutter_spinkit: 632 flutter_spinkit:
633 dependency: transitive 633 dependency: transitive
634 description: 634 description:
635 name: flutter_spinkit 635 name: flutter_spinkit
636 - sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f"  
637 - url: "https://pub.dev" 636 + sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472
  637 + url: "https://pub.flutter-io.cn"
638 source: hosted 638 source: hosted
639 - version: "5.2.2" 639 + version: "5.2.1"
640 flutter_test: 640 flutter_test:
641 dependency: "direct dev" 641 dependency: "direct dev"
642 description: flutter 642 description: flutter
@@ -651,16 +651,16 @@ packages: @@ -651,16 +651,16 @@ packages:
651 dependency: "direct main" 651 dependency: "direct main"
652 description: 652 description:
653 name: fluttertoast 653 name: fluttertoast
654 - sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8"  
655 - url: "https://pub.dev" 654 + sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc"
  655 + url: "https://pub.flutter-io.cn"
656 source: hosted 656 source: hosted
657 - version: "8.2.14" 657 + version: "8.2.8"
658 fluwx: 658 fluwx:
659 dependency: "direct main" 659 dependency: "direct main"
660 description: 660 description:
661 name: fluwx 661 name: fluwx
662 sha256: d25c26a12a1fa280833105869ea255a5877695e03dc1190f0d2c027a7c989ef3 662 sha256: d25c26a12a1fa280833105869ea255a5877695e03dc1190f0d2c027a7c989ef3
663 - url: "https://pub.dev" 663 + url: "https://pub.flutter-io.cn"
664 source: hosted 664 source: hosted
665 version: "4.6.3" 665 version: "4.6.3"
666 frontend_server_client: 666 frontend_server_client:
@@ -668,47 +668,47 @@ packages: @@ -668,47 +668,47 @@ packages:
668 description: 668 description:
669 name: frontend_server_client 669 name: frontend_server_client
670 sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 670 sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
671 - url: "https://pub.dev" 671 + url: "https://pub.flutter-io.cn"
672 source: hosted 672 source: hosted
673 version: "4.0.0" 673 version: "4.0.0"
674 glob: 674 glob:
675 dependency: transitive 675 dependency: transitive
676 description: 676 description:
677 name: glob 677 name: glob
678 - sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb"  
679 - url: "https://pub.dev" 678 + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
  679 + url: "https://pub.flutter-io.cn"
680 source: hosted 680 source: hosted
681 - version: "2.2.0" 681 + version: "2.1.3"
682 google_fonts: 682 google_fonts:
683 dependency: "direct main" 683 dependency: "direct main"
684 description: 684 description:
685 name: google_fonts 685 name: google_fonts
686 sha256: "2776c66b3e97c6cdd58d1bd3281548b074b64f1fd5c8f82391f7456e38849567" 686 sha256: "2776c66b3e97c6cdd58d1bd3281548b074b64f1fd5c8f82391f7456e38849567"
687 - url: "https://pub.dev" 687 + url: "https://pub.flutter-io.cn"
688 source: hosted 688 source: hosted
689 version: "4.0.5" 689 version: "4.0.5"
690 graphs: 690 graphs:
691 dependency: transitive 691 dependency: transitive
692 description: 692 description:
693 name: graphs 693 name: graphs
694 - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"  
695 - url: "https://pub.dev" 694 + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
  695 + url: "https://pub.flutter-io.cn"
696 source: hosted 696 source: hosted
697 - version: "2.3.2" 697 + version: "2.3.1"
698 html: 698 html:
699 dependency: transitive 699 dependency: transitive
700 description: 700 description:
701 name: html 701 name: html
702 - sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699"  
703 - url: "https://pub.dev" 702 + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
  703 + url: "https://pub.flutter-io.cn"
704 source: hosted 704 source: hosted
705 - version: "0.15.7" 705 + version: "0.15.5"
706 http: 706 http:
707 dependency: "direct overridden" 707 dependency: "direct overridden"
708 description: 708 description:
709 name: http 709 name: http
710 sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" 710 sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
711 - url: "https://pub.dev" 711 + url: "https://pub.flutter-io.cn"
712 source: hosted 712 source: hosted
713 version: "0.13.6" 713 version: "0.13.6"
714 http_multi_server: 714 http_multi_server:
@@ -716,111 +716,111 @@ packages: @@ -716,111 +716,111 @@ packages:
716 description: 716 description:
717 name: http_multi_server 717 name: http_multi_server
718 sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 718 sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
719 - url: "https://pub.dev" 719 + url: "https://pub.flutter-io.cn"
720 source: hosted 720 source: hosted
721 version: "3.2.2" 721 version: "3.2.2"
722 http_parser: 722 http_parser:
723 dependency: transitive 723 dependency: transitive
724 description: 724 description:
725 name: http_parser 725 name: http_parser
726 - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"  
727 - url: "https://pub.dev" 726 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
  727 + url: "https://pub.flutter-io.cn"
728 source: hosted 728 source: hosted
729 - version: "4.1.2" 729 + version: "4.0.2"
730 image: 730 image:
731 dependency: "direct main" 731 dependency: "direct main"
732 description: 732 description:
733 name: image 733 name: image
734 - sha256: "1976370a4df3091bb0f72409c187ad1f9132a818bc6b95ca59c0bae1c75c688e"  
735 - url: "https://pub.dev" 734 + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
  735 + url: "https://pub.flutter-io.cn"
736 source: hosted 736 source: hosted
737 - version: "4.9.2" 737 + version: "4.3.0"
738 image_picker: 738 image_picker:
739 dependency: "direct main" 739 dependency: "direct main"
740 description: 740 description:
741 name: image_picker 741 name: image_picker
742 sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c 742 sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c
743 - url: "https://pub.dev" 743 + url: "https://pub.flutter-io.cn"
744 source: hosted 744 source: hosted
745 version: "0.8.9" 745 version: "0.8.9"
746 image_picker_android: 746 image_picker_android:
747 dependency: transitive 747 dependency: transitive
748 description: 748 description:
749 name: image_picker_android 749 name: image_picker_android
750 - sha256: e83b2b05141469c5e19d77e1dfa11096b6b1567d09065b2265d7c6904560050c  
751 - url: "https://pub.dev" 750 + sha256: "0f57fee1e8bfadf8cc41818bbcd7f72e53bb768a54d9496355d5e8a5681a19f1"
  751 + url: "https://pub.flutter-io.cn"
752 source: hosted 752 source: hosted
753 - version: "0.8.13" 753 + version: "0.8.12+1"
754 image_picker_for_web: 754 image_picker_for_web:
755 dependency: transitive 755 dependency: transitive
756 description: 756 description:
757 name: image_picker_for_web 757 name: image_picker_for_web
758 sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" 758 sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0"
759 - url: "https://pub.dev" 759 + url: "https://pub.flutter-io.cn"
760 source: hosted 760 source: hosted
761 version: "2.2.0" 761 version: "2.2.0"
762 image_picker_ios: 762 image_picker_ios:
763 dependency: transitive 763 dependency: transitive
764 description: 764 description:
765 name: image_picker_ios 765 name: image_picker_ios
766 - sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e  
767 - url: "https://pub.dev" 766 + sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
  767 + url: "https://pub.flutter-io.cn"
768 source: hosted 768 source: hosted
769 - version: "0.8.13" 769 + version: "0.8.12+1"
770 image_picker_linux: 770 image_picker_linux:
771 dependency: transitive 771 dependency: transitive
772 description: 772 description:
773 name: image_picker_linux 773 name: image_picker_linux
774 - sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"  
775 - url: "https://pub.dev" 774 + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
  775 + url: "https://pub.flutter-io.cn"
776 source: hosted 776 source: hosted
777 - version: "0.2.2" 777 + version: "0.2.1+1"
778 image_picker_macos: 778 image_picker_macos:
779 dependency: transitive 779 dependency: transitive
780 description: 780 description:
781 name: image_picker_macos 781 name: image_picker_macos
782 - sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04  
783 - url: "https://pub.dev" 782 + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
  783 + url: "https://pub.flutter-io.cn"
784 source: hosted 784 source: hosted
785 - version: "0.2.2" 785 + version: "0.2.1+1"
786 image_picker_platform_interface: 786 image_picker_platform_interface:
787 dependency: transitive 787 dependency: transitive
788 description: 788 description:
789 name: image_picker_platform_interface 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 source: hosted 792 source: hosted
793 - version: "2.11.0" 793 + version: "2.10.0"
794 image_picker_windows: 794 image_picker_windows:
795 dependency: transitive 795 dependency: transitive
796 description: 796 description:
797 name: image_picker_windows 797 name: image_picker_windows
798 - sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae  
799 - url: "https://pub.dev" 798 + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
  799 + url: "https://pub.flutter-io.cn"
800 source: hosted 800 source: hosted
801 - version: "0.2.2" 801 + version: "0.2.1+1"
802 intl: 802 intl:
803 dependency: "direct main" 803 dependency: "direct main"
804 description: 804 description:
805 name: intl 805 name: intl
806 sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf 806 sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
807 - url: "https://pub.dev" 807 + url: "https://pub.flutter-io.cn"
808 source: hosted 808 source: hosted
809 version: "0.19.0" 809 version: "0.19.0"
810 io: 810 io:
811 dependency: transitive 811 dependency: transitive
812 description: 812 description:
813 name: io 813 name: io
814 - sha256: "2635216ca6a737e60de577ffa1a48a0bec76ca8a62917cfc1bb88c14c570646f"  
815 - url: "https://pub.dev" 814 + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
  815 + url: "https://pub.flutter-io.cn"
816 source: hosted 816 source: hosted
817 - version: "1.1.0" 817 + version: "1.0.4"
818 js: 818 js:
819 dependency: transitive 819 dependency: transitive
820 description: 820 description:
821 name: js 821 name: js
822 sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 822 sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
823 - url: "https://pub.dev" 823 + url: "https://pub.flutter-io.cn"
824 source: hosted 824 source: hosted
825 version: "0.6.7" 825 version: "0.6.7"
826 json_annotation: 826 json_annotation:
@@ -828,23 +828,23 @@ packages: @@ -828,23 +828,23 @@ packages:
828 description: 828 description:
829 name: json_annotation 829 name: json_annotation
830 sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" 830 sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
831 - url: "https://pub.dev" 831 + url: "https://pub.flutter-io.cn"
832 source: hosted 832 source: hosted
833 version: "4.9.0" 833 version: "4.9.0"
834 json_serializable: 834 json_serializable:
835 dependency: "direct dev" 835 dependency: "direct dev"
836 description: 836 description:
837 name: json_serializable 837 name: json_serializable
838 - sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c  
839 - url: "https://pub.dev" 838 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
  839 + url: "https://pub.flutter-io.cn"
840 source: hosted 840 source: hosted
841 - version: "6.9.5" 841 + version: "6.8.0"
842 leak_tracker: 842 leak_tracker:
843 dependency: transitive 843 dependency: transitive
844 description: 844 description:
845 name: leak_tracker 845 name: leak_tracker
846 sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" 846 sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
847 - url: "https://pub.dev" 847 + url: "https://pub.flutter-io.cn"
848 source: hosted 848 source: hosted
849 version: "10.0.7" 849 version: "10.0.7"
850 leak_tracker_flutter_testing: 850 leak_tracker_flutter_testing:
@@ -852,7 +852,7 @@ packages: @@ -852,7 +852,7 @@ packages:
852 description: 852 description:
853 name: leak_tracker_flutter_testing 853 name: leak_tracker_flutter_testing
854 sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" 854 sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
855 - url: "https://pub.dev" 855 + url: "https://pub.flutter-io.cn"
856 source: hosted 856 source: hosted
857 version: "3.0.8" 857 version: "3.0.8"
858 leak_tracker_testing: 858 leak_tracker_testing:
@@ -860,7 +860,7 @@ packages: @@ -860,7 +860,7 @@ packages:
860 description: 860 description:
861 name: leak_tracker_testing 861 name: leak_tracker_testing
862 sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 862 sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
863 - url: "https://pub.dev" 863 + url: "https://pub.flutter-io.cn"
864 source: hosted 864 source: hosted
865 version: "3.0.1" 865 version: "3.0.1"
866 limiting_direction_csx: 866 limiting_direction_csx:
@@ -868,7 +868,7 @@ packages: @@ -868,7 +868,7 @@ packages:
868 description: 868 description:
869 name: limiting_direction_csx 869 name: limiting_direction_csx
870 sha256: "283a846809a076ad641924ba830e3e39ff5385b9fb9bab7ecd322200948a89bd" 870 sha256: "283a846809a076ad641924ba830e3e39ff5385b9fb9bab7ecd322200948a89bd"
871 - url: "https://pub.dev" 871 + url: "https://pub.flutter-io.cn"
872 source: hosted 872 source: hosted
873 version: "0.2.0+5" 873 version: "0.2.0+5"
874 lints: 874 lints:
@@ -876,39 +876,39 @@ packages: @@ -876,39 +876,39 @@ packages:
876 description: 876 description:
877 name: lints 877 name: lints
878 sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" 878 sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
879 - url: "https://pub.dev" 879 + url: "https://pub.flutter-io.cn"
880 source: hosted 880 source: hosted
881 version: "2.1.1" 881 version: "2.1.1"
882 logger: 882 logger:
883 dependency: transitive 883 dependency: transitive
884 description: 884 description:
885 name: logger 885 name: logger
886 - sha256: "2a0dc097e7b01d942475bdd552356db2d0f768b05540bd4b2b53f1840f2239a7"  
887 - url: "https://pub.dev" 886 + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1
  887 + url: "https://pub.flutter-io.cn"
888 source: hosted 888 source: hosted
889 - version: "2.8.0" 889 + version: "2.5.0"
890 logging: 890 logging:
891 dependency: transitive 891 dependency: transitive
892 description: 892 description:
893 name: logging 893 name: logging
894 - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61  
895 - url: "https://pub.dev" 894 + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
  895 + url: "https://pub.flutter-io.cn"
896 source: hosted 896 source: hosted
897 - version: "1.3.0" 897 + version: "1.2.0"
898 lottie: 898 lottie:
899 dependency: "direct main" 899 dependency: "direct main"
900 description: 900 description:
901 name: lottie 901 name: lottie
902 - sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950  
903 - url: "https://pub.dev" 902 + sha256: "7afc60865a2429d994144f7d66ced2ae4305fe35d82890b8766e3359872d872c"
  903 + url: "https://pub.flutter-io.cn"
904 source: hosted 904 source: hosted
905 - version: "3.3.1" 905 + version: "3.1.3"
906 matcher: 906 matcher:
907 dependency: transitive 907 dependency: transitive
908 description: 908 description:
909 name: matcher 909 name: matcher
910 sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 910 sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
911 - url: "https://pub.dev" 911 + url: "https://pub.flutter-io.cn"
912 source: hosted 912 source: hosted
913 version: "0.12.16+1" 913 version: "0.12.16+1"
914 material_color_utilities: 914 material_color_utilities:
@@ -916,7 +916,7 @@ packages: @@ -916,7 +916,7 @@ packages:
916 description: 916 description:
917 name: material_color_utilities 917 name: material_color_utilities
918 sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 918 sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
919 - url: "https://pub.dev" 919 + url: "https://pub.flutter-io.cn"
920 source: hosted 920 source: hosted
921 version: "0.11.1" 921 version: "0.11.1"
922 meta: 922 meta:
@@ -924,7 +924,7 @@ packages: @@ -924,7 +924,7 @@ packages:
924 description: 924 description:
925 name: meta 925 name: meta
926 sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 926 sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
927 - url: "https://pub.dev" 927 + url: "https://pub.flutter-io.cn"
928 source: hosted 928 source: hosted
929 version: "1.15.0" 929 version: "1.15.0"
930 mime: 930 mime:
@@ -932,7 +932,7 @@ packages: @@ -932,7 +932,7 @@ packages:
932 description: 932 description:
933 name: mime 933 name: mime
934 sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" 934 sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
935 - url: "https://pub.dev" 935 + url: "https://pub.flutter-io.cn"
936 source: hosted 936 source: hosted
937 version: "1.0.6" 937 version: "1.0.6"
938 nested: 938 nested:
@@ -940,7 +940,7 @@ packages: @@ -940,7 +940,7 @@ packages:
940 description: 940 description:
941 name: nested 941 name: nested
942 sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 942 sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
943 - url: "https://pub.dev" 943 + url: "https://pub.flutter-io.cn"
944 source: hosted 944 source: hosted
945 version: "1.0.0" 945 version: "1.0.0"
946 nested_scroll_views: 946 nested_scroll_views:
@@ -948,7 +948,7 @@ packages: @@ -948,7 +948,7 @@ packages:
948 description: 948 description:
949 name: nested_scroll_views 949 name: nested_scroll_views
950 sha256: "37d0f35682441a4cdf5f50bd5767a53b7192780a085c9dc728fc273df8a3443a" 950 sha256: "37d0f35682441a4cdf5f50bd5767a53b7192780a085c9dc728fc273df8a3443a"
951 - url: "https://pub.dev" 951 + url: "https://pub.flutter-io.cn"
952 source: hosted 952 source: hosted
953 version: "0.0.12" 953 version: "0.0.12"
954 nm: 954 nm:
@@ -956,7 +956,7 @@ packages: @@ -956,7 +956,7 @@ packages:
956 description: 956 description:
957 name: nm 957 name: nm
958 sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" 958 sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
959 - url: "https://pub.dev" 959 + url: "https://pub.flutter-io.cn"
960 source: hosted 960 source: hosted
961 version: "0.5.0" 961 version: "0.5.0"
962 octo_image: 962 octo_image:
@@ -964,23 +964,23 @@ packages: @@ -964,23 +964,23 @@ packages:
964 description: 964 description:
965 name: octo_image 965 name: octo_image
966 sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" 966 sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
967 - url: "https://pub.dev" 967 + url: "https://pub.flutter-io.cn"
968 source: hosted 968 source: hosted
969 version: "2.1.0" 969 version: "2.1.0"
970 package_config: 970 package_config:
971 dependency: transitive 971 dependency: transitive
972 description: 972 description:
973 name: package_config 973 name: package_config
974 - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc  
975 - url: "https://pub.dev" 974 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
  975 + url: "https://pub.flutter-io.cn"
976 source: hosted 976 source: hosted
977 - version: "2.2.0" 977 + version: "2.1.0"
978 package_info_plus: 978 package_info_plus:
979 dependency: "direct main" 979 dependency: "direct main"
980 description: 980 description:
981 name: package_info_plus 981 name: package_info_plus
982 sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" 982 sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
983 - url: "https://pub.dev" 983 + url: "https://pub.flutter-io.cn"
984 source: hosted 984 source: hosted
985 version: "4.2.0" 985 version: "4.2.0"
986 package_info_plus_platform_interface: 986 package_info_plus_platform_interface:
@@ -988,7 +988,7 @@ packages: @@ -988,7 +988,7 @@ packages:
988 description: 988 description:
989 name: package_info_plus_platform_interface 989 name: package_info_plus_platform_interface
990 sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" 990 sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
991 - url: "https://pub.dev" 991 + url: "https://pub.flutter-io.cn"
992 source: hosted 992 source: hosted
993 version: "2.0.1" 993 version: "2.0.1"
994 path: 994 path:
@@ -996,31 +996,31 @@ packages: @@ -996,31 +996,31 @@ packages:
996 description: 996 description:
997 name: path 997 name: path
998 sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 998 sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
999 - url: "https://pub.dev" 999 + url: "https://pub.flutter-io.cn"
1000 source: hosted 1000 source: hosted
1001 version: "1.9.0" 1001 version: "1.9.0"
1002 path_provider: 1002 path_provider:
1003 dependency: "direct main" 1003 dependency: "direct main"
1004 description: 1004 description:
1005 name: path_provider 1005 name: path_provider
1006 - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"  
1007 - url: "https://pub.dev" 1006 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
  1007 + url: "https://pub.flutter-io.cn"
1008 source: hosted 1008 source: hosted
1009 - version: "2.1.5" 1009 + version: "2.1.4"
1010 path_provider_android: 1010 path_provider_android:
1011 dependency: transitive 1011 dependency: transitive
1012 description: 1012 description:
1013 name: path_provider_android 1013 name: path_provider_android
1014 - sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9  
1015 - url: "https://pub.dev" 1014 + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d
  1015 + url: "https://pub.flutter-io.cn"
1016 source: hosted 1016 source: hosted
1017 - version: "2.2.17" 1017 + version: "2.2.4"
1018 path_provider_foundation: 1018 path_provider_foundation:
1019 dependency: transitive 1019 dependency: transitive
1020 description: 1020 description:
1021 name: path_provider_foundation 1021 name: path_provider_foundation
1022 sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" 1022 sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
1023 - url: "https://pub.dev" 1023 + url: "https://pub.flutter-io.cn"
1024 source: hosted 1024 source: hosted
1025 version: "2.4.1" 1025 version: "2.4.1"
1026 path_provider_linux: 1026 path_provider_linux:
@@ -1028,7 +1028,7 @@ packages: @@ -1028,7 +1028,7 @@ packages:
1028 description: 1028 description:
1029 name: path_provider_linux 1029 name: path_provider_linux
1030 sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 1030 sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
1031 - url: "https://pub.dev" 1031 + url: "https://pub.flutter-io.cn"
1032 source: hosted 1032 source: hosted
1033 version: "2.2.1" 1033 version: "2.2.1"
1034 path_provider_platform_interface: 1034 path_provider_platform_interface:
@@ -1036,7 +1036,7 @@ packages: @@ -1036,7 +1036,7 @@ packages:
1036 description: 1036 description:
1037 name: path_provider_platform_interface 1037 name: path_provider_platform_interface
1038 sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 1038 sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
1039 - url: "https://pub.dev" 1039 + url: "https://pub.flutter-io.cn"
1040 source: hosted 1040 source: hosted
1041 version: "2.1.2" 1041 version: "2.1.2"
1042 path_provider_windows: 1042 path_provider_windows:
@@ -1044,23 +1044,23 @@ packages: @@ -1044,23 +1044,23 @@ packages:
1044 description: 1044 description:
1045 name: path_provider_windows 1045 name: path_provider_windows
1046 sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 1046 sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
1047 - url: "https://pub.dev" 1047 + url: "https://pub.flutter-io.cn"
1048 source: hosted 1048 source: hosted
1049 version: "2.3.0" 1049 version: "2.3.0"
1050 percent_indicator: 1050 percent_indicator:
1051 dependency: "direct main" 1051 dependency: "direct main"
1052 description: 1052 description:
1053 name: percent_indicator 1053 name: percent_indicator
1054 - sha256: "157d29133bbc6ecb11f923d36e7960a96a3f28837549a20b65e5135729f0f9fd"  
1055 - url: "https://pub.dev" 1054 + sha256: "0d77d5c6fa9b7f60202cedf748b568ba9ba38d3f30405d6ceae4da76f5185462"
  1055 + url: "https://pub.flutter-io.cn"
1056 source: hosted 1056 source: hosted
1057 - version: "4.2.5" 1057 + version: "4.2.4"
1058 permission_handler: 1058 permission_handler:
1059 dependency: "direct main" 1059 dependency: "direct main"
1060 description: 1060 description:
1061 name: permission_handler 1061 name: permission_handler
1062 sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 1062 sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
1063 - url: "https://pub.dev" 1063 + url: "https://pub.flutter-io.cn"
1064 source: hosted 1064 source: hosted
1065 version: "10.4.5" 1065 version: "10.4.5"
1066 permission_handler_android: 1066 permission_handler_android:
@@ -1068,7 +1068,7 @@ packages: @@ -1068,7 +1068,7 @@ packages:
1068 description: 1068 description:
1069 name: permission_handler_android 1069 name: permission_handler_android
1070 sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" 1070 sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
1071 - url: "https://pub.dev" 1071 + url: "https://pub.flutter-io.cn"
1072 source: hosted 1072 source: hosted
1073 version: "10.3.6" 1073 version: "10.3.6"
1074 permission_handler_apple: 1074 permission_handler_apple:
@@ -1076,7 +1076,7 @@ packages: @@ -1076,7 +1076,7 @@ packages:
1076 description: 1076 description:
1077 name: permission_handler_apple 1077 name: permission_handler_apple
1078 sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" 1078 sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
1079 - url: "https://pub.dev" 1079 + url: "https://pub.flutter-io.cn"
1080 source: hosted 1080 source: hosted
1081 version: "9.1.4" 1081 version: "9.1.4"
1082 permission_handler_platform_interface: 1082 permission_handler_platform_interface:
@@ -1084,7 +1084,7 @@ packages: @@ -1084,7 +1084,7 @@ packages:
1084 description: 1084 description:
1085 name: permission_handler_platform_interface 1085 name: permission_handler_platform_interface
1086 sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" 1086 sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
1087 - url: "https://pub.dev" 1087 + url: "https://pub.flutter-io.cn"
1088 source: hosted 1088 source: hosted
1089 version: "3.12.0" 1089 version: "3.12.0"
1090 permission_handler_windows: 1090 permission_handler_windows:
@@ -1092,7 +1092,7 @@ packages: @@ -1092,7 +1092,7 @@ packages:
1092 description: 1092 description:
1093 name: permission_handler_windows 1093 name: permission_handler_windows
1094 sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 1094 sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
1095 - url: "https://pub.dev" 1095 + url: "https://pub.flutter-io.cn"
1096 source: hosted 1096 source: hosted
1097 version: "0.1.3" 1097 version: "0.1.3"
1098 petitparser: 1098 petitparser:
@@ -1100,7 +1100,7 @@ packages: @@ -1100,7 +1100,7 @@ packages:
1100 description: 1100 description:
1101 name: petitparser 1101 name: petitparser
1102 sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 1102 sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
1103 - url: "https://pub.dev" 1103 + url: "https://pub.flutter-io.cn"
1104 source: hosted 1104 source: hosted
1105 version: "6.0.2" 1105 version: "6.0.2"
1106 platform: 1106 platform:
@@ -1108,7 +1108,7 @@ packages: @@ -1108,7 +1108,7 @@ packages:
1108 description: 1108 description:
1109 name: platform 1109 name: platform
1110 sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 1110 sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
1111 - url: "https://pub.dev" 1111 + url: "https://pub.flutter-io.cn"
1112 source: hosted 1112 source: hosted
1113 version: "3.1.6" 1113 version: "3.1.6"
1114 plugin_platform_interface: 1114 plugin_platform_interface:
@@ -1116,63 +1116,55 @@ packages: @@ -1116,63 +1116,55 @@ packages:
1116 description: 1116 description:
1117 name: plugin_platform_interface 1117 name: plugin_platform_interface
1118 sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 1118 sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
1119 - url: "https://pub.dev" 1119 + url: "https://pub.flutter-io.cn"
1120 source: hosted 1120 source: hosted
1121 version: "2.1.8" 1121 version: "2.1.8"
1122 pool: 1122 pool:
1123 dependency: transitive 1123 dependency: transitive
1124 description: 1124 description:
1125 name: pool 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 source: hosted 1128 source: hosted
1137 - version: "6.5.2" 1129 + version: "1.5.1"
1138 pretty_dio_logger: 1130 pretty_dio_logger:
1139 dependency: "direct main" 1131 dependency: "direct main"
1140 description: 1132 description:
1141 name: pretty_dio_logger 1133 name: pretty_dio_logger
1142 sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" 1134 sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407"
1143 - url: "https://pub.dev" 1135 + url: "https://pub.flutter-io.cn"
1144 source: hosted 1136 source: hosted
1145 version: "1.4.0" 1137 version: "1.4.0"
1146 provider: 1138 provider:
1147 dependency: transitive 1139 dependency: transitive
1148 description: 1140 description:
1149 name: provider 1141 name: provider
1150 - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"  
1151 - url: "https://pub.dev" 1142 + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
  1143 + url: "https://pub.flutter-io.cn"
1152 source: hosted 1144 source: hosted
1153 - version: "6.1.5+1" 1145 + version: "6.1.2"
1154 pub_semver: 1146 pub_semver:
1155 dependency: transitive 1147 dependency: transitive
1156 description: 1148 description:
1157 name: pub_semver 1149 name: pub_semver
1158 - sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24"  
1159 - url: "https://pub.dev" 1150 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
  1151 + url: "https://pub.flutter-io.cn"
1160 source: hosted 1152 source: hosted
1161 - version: "2.2.1" 1153 + version: "2.1.4"
1162 pubspec_parse: 1154 pubspec_parse:
1163 dependency: transitive 1155 dependency: transitive
1164 description: 1156 description:
1165 name: pubspec_parse 1157 name: pubspec_parse
1166 - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"  
1167 - url: "https://pub.dev" 1158 + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
  1159 + url: "https://pub.flutter-io.cn"
1168 source: hosted 1160 source: hosted
1169 - version: "1.5.0" 1161 + version: "1.4.0"
1170 pull_to_refresh: 1162 pull_to_refresh:
1171 dependency: "direct main" 1163 dependency: "direct main"
1172 description: 1164 description:
1173 name: pull_to_refresh 1165 name: pull_to_refresh
1174 sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12 1166 sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12
1175 - url: "https://pub.dev" 1167 + url: "https://pub.flutter-io.cn"
1176 source: hosted 1168 source: hosted
1177 version: "2.0.0" 1169 version: "2.0.0"
1178 rational: 1170 rational:
@@ -1180,55 +1172,63 @@ packages: @@ -1180,55 +1172,63 @@ packages:
1180 description: 1172 description:
1181 name: rational 1173 name: rational
1182 sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 1174 sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
1183 - url: "https://pub.dev" 1175 + url: "https://pub.flutter-io.cn"
1184 source: hosted 1176 source: hosted
1185 version: "2.2.3" 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 responsive_framework: 1186 responsive_framework:
1187 dependency: "direct main" 1187 dependency: "direct main"
1188 description: 1188 description:
1189 name: responsive_framework 1189 name: responsive_framework
1190 sha256: a8e1c13d4ba980c60cbf6fa1e9907cd60662bf2585184d7c96ca46c43de91552 1190 sha256: a8e1c13d4ba980c60cbf6fa1e9907cd60662bf2585184d7c96ca46c43de91552
1191 - url: "https://pub.dev" 1191 + url: "https://pub.flutter-io.cn"
1192 source: hosted 1192 source: hosted
1193 version: "1.5.1" 1193 version: "1.5.1"
1194 rxdart: 1194 rxdart:
1195 dependency: transitive 1195 dependency: transitive
1196 description: 1196 description:
1197 name: rxdart 1197 name: rxdart
1198 - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"  
1199 - url: "https://pub.dev" 1198 + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
  1199 + url: "https://pub.flutter-io.cn"
1200 source: hosted 1200 source: hosted
1201 - version: "0.28.0" 1201 + version: "0.27.7"
1202 shared_preferences: 1202 shared_preferences:
1203 dependency: "direct main" 1203 dependency: "direct main"
1204 description: 1204 description:
1205 name: shared_preferences 1205 name: shared_preferences
1206 - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"  
1207 - url: "https://pub.dev" 1206 + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
  1207 + url: "https://pub.flutter-io.cn"
1208 source: hosted 1208 source: hosted
1209 - version: "2.5.3" 1209 + version: "2.2.3"
1210 shared_preferences_android: 1210 shared_preferences_android:
1211 dependency: transitive 1211 dependency: transitive
1212 description: 1212 description:
1213 name: shared_preferences_android 1213 name: shared_preferences_android
1214 - sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"  
1215 - url: "https://pub.dev" 1214 + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
  1215 + url: "https://pub.flutter-io.cn"
1216 source: hosted 1216 source: hosted
1217 - version: "2.4.11" 1217 + version: "2.2.2"
1218 shared_preferences_foundation: 1218 shared_preferences_foundation:
1219 dependency: transitive 1219 dependency: transitive
1220 description: 1220 description:
1221 name: shared_preferences_foundation 1221 name: shared_preferences_foundation
1222 - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"  
1223 - url: "https://pub.dev" 1222 + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d"
  1223 + url: "https://pub.flutter-io.cn"
1224 source: hosted 1224 source: hosted
1225 - version: "2.5.4" 1225 + version: "2.5.3"
1226 shared_preferences_linux: 1226 shared_preferences_linux:
1227 dependency: transitive 1227 dependency: transitive
1228 description: 1228 description:
1229 name: shared_preferences_linux 1229 name: shared_preferences_linux
1230 sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" 1230 sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
1231 - url: "https://pub.dev" 1231 + url: "https://pub.flutter-io.cn"
1232 source: hosted 1232 source: hosted
1233 version: "2.4.1" 1233 version: "2.4.1"
1234 shared_preferences_platform_interface: 1234 shared_preferences_platform_interface:
@@ -1236,41 +1236,41 @@ packages: @@ -1236,41 +1236,41 @@ packages:
1236 description: 1236 description:
1237 name: shared_preferences_platform_interface 1237 name: shared_preferences_platform_interface
1238 sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" 1238 sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
1239 - url: "https://pub.dev" 1239 + url: "https://pub.flutter-io.cn"
1240 source: hosted 1240 source: hosted
1241 version: "2.4.1" 1241 version: "2.4.1"
1242 shared_preferences_web: 1242 shared_preferences_web:
1243 dependency: transitive 1243 dependency: transitive
1244 description: 1244 description:
1245 name: shared_preferences_web 1245 name: shared_preferences_web
1246 - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019  
1247 - url: "https://pub.dev" 1246 + sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2"
  1247 + url: "https://pub.flutter-io.cn"
1248 source: hosted 1248 source: hosted
1249 - version: "2.4.3" 1249 + version: "2.4.1"
1250 shared_preferences_windows: 1250 shared_preferences_windows:
1251 dependency: transitive 1251 dependency: transitive
1252 description: 1252 description:
1253 name: shared_preferences_windows 1253 name: shared_preferences_windows
1254 sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" 1254 sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
1255 - url: "https://pub.dev" 1255 + url: "https://pub.flutter-io.cn"
1256 source: hosted 1256 source: hosted
1257 version: "2.4.1" 1257 version: "2.4.1"
1258 shelf: 1258 shelf:
1259 dependency: transitive 1259 dependency: transitive
1260 description: 1260 description:
1261 name: shelf 1261 name: shelf
1262 - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12  
1263 - url: "https://pub.dev" 1262 + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
  1263 + url: "https://pub.flutter-io.cn"
1264 source: hosted 1264 source: hosted
1265 - version: "1.4.2" 1265 + version: "1.4.1"
1266 shelf_web_socket: 1266 shelf_web_socket:
1267 dependency: transitive 1267 dependency: transitive
1268 description: 1268 description:
1269 name: shelf_web_socket 1269 name: shelf_web_socket
1270 - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"  
1271 - url: "https://pub.dev" 1270 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
  1271 + url: "https://pub.flutter-io.cn"
1272 source: hosted 1272 source: hosted
1273 - version: "3.0.0" 1273 + version: "1.0.4"
1274 sky_engine: 1274 sky_engine:
1275 dependency: transitive 1275 dependency: transitive
1276 description: flutter 1276 description: flutter
@@ -1280,72 +1280,56 @@ packages: @@ -1280,72 +1280,56 @@ packages:
1280 dependency: transitive 1280 dependency: transitive
1281 description: 1281 description:
1282 name: source_gen 1282 name: source_gen
1283 - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"  
1284 - url: "https://pub.dev" 1283 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
  1284 + url: "https://pub.flutter-io.cn"
1285 source: hosted 1285 source: hosted
1286 - version: "2.0.0" 1286 + version: "1.5.0"
1287 source_helper: 1287 source_helper:
1288 dependency: transitive 1288 dependency: transitive
1289 description: 1289 description:
1290 name: source_helper 1290 name: source_helper
1291 - sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca  
1292 - url: "https://pub.dev" 1291 + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
  1292 + url: "https://pub.flutter-io.cn"
1293 source: hosted 1293 source: hosted
1294 - version: "1.3.7" 1294 + version: "1.3.4"
1295 source_span: 1295 source_span:
1296 dependency: transitive 1296 dependency: transitive
1297 description: 1297 description:
1298 name: source_span 1298 name: source_span
1299 sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 1299 sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
1300 - url: "https://pub.dev" 1300 + url: "https://pub.flutter-io.cn"
1301 source: hosted 1301 source: hosted
1302 version: "1.10.0" 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 sqflite: 1311 sqflite:
1304 dependency: "direct main" 1312 dependency: "direct main"
1305 description: 1313 description:
1306 name: sqflite 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 source: hosted 1317 source: hosted
1318 - version: "2.4.0" 1318 + version: "2.3.3+1"
1319 sqflite_common: 1319 sqflite_common:
1320 dependency: transitive 1320 dependency: transitive
1321 description: 1321 description:
1322 name: sqflite_common 1322 name: sqflite_common
1323 - sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"  
1324 - url: "https://pub.dev" 1323 + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
  1324 + url: "https://pub.flutter-io.cn"
1325 source: hosted 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 stack_trace: 1327 stack_trace:
1344 dependency: transitive 1328 dependency: transitive
1345 description: 1329 description:
1346 name: stack_trace 1330 name: stack_trace
1347 sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" 1331 sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
1348 - url: "https://pub.dev" 1332 + url: "https://pub.flutter-io.cn"
1349 source: hosted 1333 source: hosted
1350 version: "1.12.0" 1334 version: "1.12.0"
1351 stream_channel: 1335 stream_channel:
@@ -1353,39 +1337,39 @@ packages: @@ -1353,39 +1337,39 @@ packages:
1353 description: 1337 description:
1354 name: stream_channel 1338 name: stream_channel
1355 sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 1339 sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
1356 - url: "https://pub.dev" 1340 + url: "https://pub.flutter-io.cn"
1357 source: hosted 1341 source: hosted
1358 version: "2.1.2" 1342 version: "2.1.2"
1359 stream_transform: 1343 stream_transform:
1360 dependency: transitive 1344 dependency: transitive
1361 description: 1345 description:
1362 name: stream_transform 1346 name: stream_transform
1363 - sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a  
1364 - url: "https://pub.dev" 1347 + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
  1348 + url: "https://pub.flutter-io.cn"
1365 source: hosted 1349 source: hosted
1366 - version: "2.1.2" 1350 + version: "2.1.1"
1367 string_scanner: 1351 string_scanner:
1368 dependency: transitive 1352 dependency: transitive
1369 description: 1353 description:
1370 name: string_scanner 1354 name: string_scanner
1371 sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" 1355 sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
1372 - url: "https://pub.dev" 1356 + url: "https://pub.flutter-io.cn"
1373 source: hosted 1357 source: hosted
1374 version: "1.3.0" 1358 version: "1.3.0"
1375 synchronized: 1359 synchronized:
1376 dependency: transitive 1360 dependency: transitive
1377 description: 1361 description:
1378 name: synchronized 1362 name: synchronized
1379 - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"  
1380 - url: "https://pub.dev" 1363 + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
  1364 + url: "https://pub.flutter-io.cn"
1381 source: hosted 1365 source: hosted
1382 - version: "3.3.0+3" 1366 + version: "3.1.0+1"
1383 term_glyph: 1367 term_glyph:
1384 dependency: transitive 1368 dependency: transitive
1385 description: 1369 description:
1386 name: term_glyph 1370 name: term_glyph
1387 sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 1371 sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
1388 - url: "https://pub.dev" 1372 + url: "https://pub.flutter-io.cn"
1389 source: hosted 1373 source: hosted
1390 version: "1.2.1" 1374 version: "1.2.1"
1391 test_api: 1375 test_api:
@@ -1393,79 +1377,79 @@ packages: @@ -1393,79 +1377,79 @@ packages:
1393 description: 1377 description:
1394 name: test_api 1378 name: test_api
1395 sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" 1379 sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
1396 - url: "https://pub.dev" 1380 + url: "https://pub.flutter-io.cn"
1397 source: hosted 1381 source: hosted
1398 version: "0.7.3" 1382 version: "0.7.3"
1399 timing: 1383 timing:
1400 dependency: transitive 1384 dependency: transitive
1401 description: 1385 description:
1402 name: timing 1386 name: timing
1403 - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"  
1404 - url: "https://pub.dev" 1387 + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
  1388 + url: "https://pub.flutter-io.cn"
1405 source: hosted 1389 source: hosted
1406 - version: "1.0.2" 1390 + version: "1.0.1"
1407 tobias: 1391 tobias:
1408 dependency: "direct main" 1392 dependency: "direct main"
1409 description: 1393 description:
1410 name: tobias 1394 name: tobias
1411 sha256: "32836a9dae348d1c3f5f4247ed0a6a409fd5b9ddf490b06966a0fad2fd02cb7a" 1395 sha256: "32836a9dae348d1c3f5f4247ed0a6a409fd5b9ddf490b06966a0fad2fd02cb7a"
1412 - url: "https://pub.dev" 1396 + url: "https://pub.flutter-io.cn"
1413 source: hosted 1397 source: hosted
1414 version: "3.3.2" 1398 version: "3.3.2"
1415 typed_data: 1399 typed_data:
1416 dependency: transitive 1400 dependency: transitive
1417 description: 1401 description:
1418 name: typed_data 1402 name: typed_data
1419 - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006  
1420 - url: "https://pub.dev" 1403 + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
  1404 + url: "https://pub.flutter-io.cn"
1421 source: hosted 1405 source: hosted
1422 - version: "1.4.0" 1406 + version: "1.3.2"
1423 umeng_apm_sdk: 1407 umeng_apm_sdk:
1424 dependency: "direct main" 1408 dependency: "direct main"
1425 description: 1409 description:
1426 name: umeng_apm_sdk 1410 name: umeng_apm_sdk
1427 - sha256: "37f61a30ad84c06d89bedbcf00ca4f09347468a2186975c4c121aee1db3a2083"  
1428 - url: "https://pub.dev" 1411 + sha256: a1d1afa3fbcc71079fec2acdf9189a24c9d5350c30f3048b17b729c169ff0e0f
  1412 + url: "https://pub.flutter-io.cn"
1429 source: hosted 1413 source: hosted
1430 - version: "2.3.7" 1414 + version: "2.3.5"
1431 umeng_common_sdk: 1415 umeng_common_sdk:
1432 dependency: "direct main" 1416 dependency: "direct main"
1433 description: 1417 description:
1434 name: umeng_common_sdk 1418 name: umeng_common_sdk
1435 sha256: "203ae013220ffd00c677a164636239a2c9722ba533b6f25e00e7a4929a91aecd" 1419 sha256: "203ae013220ffd00c677a164636239a2c9722ba533b6f25e00e7a4929a91aecd"
1436 - url: "https://pub.dev" 1420 + url: "https://pub.flutter-io.cn"
1437 source: hosted 1421 source: hosted
1438 version: "1.3.1" 1422 version: "1.3.1"
1439 url_launcher: 1423 url_launcher:
1440 dependency: "direct main" 1424 dependency: "direct main"
1441 description: 1425 description:
1442 name: url_launcher 1426 name: url_launcher
1443 - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8  
1444 - url: "https://pub.dev" 1427 + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
  1428 + url: "https://pub.flutter-io.cn"
1445 source: hosted 1429 source: hosted
1446 - version: "6.3.2" 1430 + version: "6.3.1"
1447 url_launcher_android: 1431 url_launcher_android:
1448 dependency: transitive 1432 dependency: transitive
1449 description: 1433 description:
1450 name: url_launcher_android 1434 name: url_launcher_android
1451 - sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"  
1452 - url: "https://pub.dev" 1435 + sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9"
  1436 + url: "https://pub.flutter-io.cn"
1453 source: hosted 1437 source: hosted
1454 - version: "6.3.17" 1438 + version: "6.3.2"
1455 url_launcher_ios: 1439 url_launcher_ios:
1456 dependency: transitive 1440 dependency: transitive
1457 description: 1441 description:
1458 name: url_launcher_ios 1442 name: url_launcher_ios
1459 - sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"  
1460 - url: "https://pub.dev" 1443 + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
  1444 + url: "https://pub.flutter-io.cn"
1461 source: hosted 1445 source: hosted
1462 - version: "6.3.3" 1446 + version: "6.3.2"
1463 url_launcher_linux: 1447 url_launcher_linux:
1464 dependency: transitive 1448 dependency: transitive
1465 description: 1449 description:
1466 name: url_launcher_linux 1450 name: url_launcher_linux
1467 sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" 1451 sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
1468 - url: "https://pub.dev" 1452 + url: "https://pub.flutter-io.cn"
1469 source: hosted 1453 source: hosted
1470 version: "3.2.1" 1454 version: "3.2.1"
1471 url_launcher_macos: 1455 url_launcher_macos:
@@ -1473,7 +1457,7 @@ packages: @@ -1473,7 +1457,7 @@ packages:
1473 description: 1457 description:
1474 name: url_launcher_macos 1458 name: url_launcher_macos
1475 sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" 1459 sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
1476 - url: "https://pub.dev" 1460 + url: "https://pub.flutter-io.cn"
1477 source: hosted 1461 source: hosted
1478 version: "3.2.2" 1462 version: "3.2.2"
1479 url_launcher_platform_interface: 1463 url_launcher_platform_interface:
@@ -1481,63 +1465,62 @@ packages: @@ -1481,63 +1465,62 @@ packages:
1481 description: 1465 description:
1482 name: url_launcher_platform_interface 1466 name: url_launcher_platform_interface
1483 sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" 1467 sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
1484 - url: "https://pub.dev" 1468 + url: "https://pub.flutter-io.cn"
1485 source: hosted 1469 source: hosted
1486 version: "2.3.2" 1470 version: "2.3.2"
1487 url_launcher_web: 1471 url_launcher_web:
1488 dependency: transitive 1472 dependency: transitive
1489 description: 1473 description:
1490 name: url_launcher_web 1474 name: url_launcher_web
1491 - sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"  
1492 - url: "https://pub.dev" 1475 + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
  1476 + url: "https://pub.flutter-io.cn"
1493 source: hosted 1477 source: hosted
1494 - version: "2.4.1" 1478 + version: "2.3.3"
1495 url_launcher_windows: 1479 url_launcher_windows:
1496 dependency: transitive 1480 dependency: transitive
1497 description: 1481 description:
1498 name: url_launcher_windows 1482 name: url_launcher_windows
1499 - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"  
1500 - url: "https://pub.dev" 1483 + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
  1484 + url: "https://pub.flutter-io.cn"
1501 source: hosted 1485 source: hosted
1502 - version: "3.1.4" 1486 + version: "3.1.3"
1503 uuid: 1487 uuid:
1504 dependency: transitive 1488 dependency: transitive
1505 description: 1489 description:
1506 name: uuid 1490 name: uuid
1507 - sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"  
1508 - url: "https://pub.dev" 1491 + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
  1492 + url: "https://pub.flutter-io.cn"
1509 source: hosted 1493 source: hosted
1510 - version: "4.6.0" 1494 + version: "4.5.1"
1511 vector_math: 1495 vector_math:
1512 dependency: transitive 1496 dependency: transitive
1513 description: 1497 description:
1514 name: vector_math 1498 name: vector_math
1515 sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 1499 sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
1516 - url: "https://pub.dev" 1500 + url: "https://pub.flutter-io.cn"
1517 source: hosted 1501 source: hosted
1518 version: "2.1.4" 1502 version: "2.1.4"
1519 video_player: 1503 video_player:
1520 dependency: "direct main" 1504 dependency: "direct main"
1521 description: 1505 description:
1522 name: video_player 1506 name: video_player
1523 - sha256: "4a8c3492d734f7c39c2588a3206707a05ee80cef52e8c7f3b2078d430c84bc17"  
1524 - url: "https://pub.dev" 1507 + sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a"
  1508 + url: "https://pub.flutter-io.cn"
1525 source: hosted 1509 source: hosted
1526 - version: "2.9.2" 1510 + version: "2.10.0"
1527 video_player_android: 1511 video_player_android:
1528 - dependency: transitive 1512 + dependency: "direct overridden"
1529 description: 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 version: "2.8.4" 1517 version: "2.8.4"
1535 video_player_avfoundation: 1518 video_player_avfoundation:
1536 dependency: transitive 1519 dependency: transitive
1537 description: 1520 description:
1538 name: video_player_avfoundation 1521 name: video_player_avfoundation
1539 sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd 1522 sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd
1540 - url: "https://pub.dev" 1523 + url: "https://pub.flutter-io.cn"
1541 source: hosted 1524 source: hosted
1542 version: "2.8.4" 1525 version: "2.8.4"
1543 video_player_platform_interface: 1526 video_player_platform_interface:
@@ -1545,23 +1528,23 @@ packages: @@ -1545,23 +1528,23 @@ packages:
1545 description: 1528 description:
1546 name: video_player_platform_interface 1529 name: video_player_platform_interface
1547 sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a 1530 sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a
1548 - url: "https://pub.dev" 1531 + url: "https://pub.flutter-io.cn"
1549 source: hosted 1532 source: hosted
1550 version: "6.4.0" 1533 version: "6.4.0"
1551 video_player_web: 1534 video_player_web:
1552 dependency: transitive 1535 dependency: transitive
1553 description: 1536 description:
1554 name: video_player_web 1537 name: video_player_web
1555 - sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"  
1556 - url: "https://pub.dev" 1538 + sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774"
  1539 + url: "https://pub.flutter-io.cn"
1557 source: hosted 1540 source: hosted
1558 - version: "2.4.0" 1541 + version: "2.3.2"
1559 vm_service: 1542 vm_service:
1560 dependency: transitive 1543 dependency: transitive
1561 description: 1544 description:
1562 name: vm_service 1545 name: vm_service
1563 sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b 1546 sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
1564 - url: "https://pub.dev" 1547 + url: "https://pub.flutter-io.cn"
1565 source: hosted 1548 source: hosted
1566 version: "14.3.0" 1549 version: "14.3.0"
1567 wakelock_plus: 1550 wakelock_plus:
@@ -1569,7 +1552,7 @@ packages: @@ -1569,7 +1552,7 @@ packages:
1569 description: 1552 description:
1570 name: wakelock_plus 1553 name: wakelock_plus
1571 sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d 1554 sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d
1572 - url: "https://pub.dev" 1555 + url: "https://pub.flutter-io.cn"
1573 source: hosted 1556 source: hosted
1574 version: "1.1.4" 1557 version: "1.1.4"
1575 wakelock_plus_platform_interface: 1558 wakelock_plus_platform_interface:
@@ -1577,63 +1560,55 @@ packages: @@ -1577,63 +1560,55 @@ packages:
1577 description: 1560 description:
1578 name: wakelock_plus_platform_interface 1561 name: wakelock_plus_platform_interface
1579 sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" 1562 sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
1580 - url: "https://pub.dev" 1563 + url: "https://pub.flutter-io.cn"
1581 source: hosted 1564 source: hosted
1582 version: "1.3.0" 1565 version: "1.3.0"
1583 watcher: 1566 watcher:
1584 dependency: transitive 1567 dependency: transitive
1585 description: 1568 description:
1586 name: watcher 1569 name: watcher
1587 - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"  
1588 - url: "https://pub.dev" 1570 + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
  1571 + url: "https://pub.flutter-io.cn"
1589 source: hosted 1572 source: hosted
1590 - version: "1.2.1" 1573 + version: "1.1.1"
1591 web: 1574 web:
1592 dependency: transitive 1575 dependency: transitive
1593 description: 1576 description:
1594 name: web 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 source: hosted 1580 source: hosted
1606 - version: "1.0.1" 1581 + version: "0.5.1"
1607 web_socket_channel: 1582 web_socket_channel:
1608 dependency: transitive 1583 dependency: transitive
1609 description: 1584 description:
1610 name: web_socket_channel 1585 name: web_socket_channel
1611 - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8  
1612 - url: "https://pub.dev" 1586 + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
  1587 + url: "https://pub.flutter-io.cn"
1613 source: hosted 1588 source: hosted
1614 - version: "3.0.3" 1589 + version: "2.4.0"
1615 win32: 1590 win32:
1616 dependency: transitive 1591 dependency: transitive
1617 description: 1592 description:
1618 name: win32 1593 name: win32
1619 sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e 1594 sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
1620 - url: "https://pub.dev" 1595 + url: "https://pub.flutter-io.cn"
1621 source: hosted 1596 source: hosted
1622 version: "5.10.1" 1597 version: "5.10.1"
1623 win32_registry: 1598 win32_registry:
1624 dependency: transitive 1599 dependency: transitive
1625 description: 1600 description:
1626 name: win32_registry 1601 name: win32_registry
1627 - sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"  
1628 - url: "https://pub.dev" 1602 + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb"
  1603 + url: "https://pub.flutter-io.cn"
1629 source: hosted 1604 source: hosted
1630 - version: "1.1.5" 1605 + version: "1.1.3"
1631 xdg_directories: 1606 xdg_directories:
1632 dependency: transitive 1607 dependency: transitive
1633 description: 1608 description:
1634 name: xdg_directories 1609 name: xdg_directories
1635 sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" 1610 sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
1636 - url: "https://pub.dev" 1611 + url: "https://pub.flutter-io.cn"
1637 source: hosted 1612 source: hosted
1638 version: "1.1.0" 1613 version: "1.1.0"
1639 xml: 1614 xml:
@@ -1641,17 +1616,17 @@ packages: @@ -1641,17 +1616,17 @@ packages:
1641 description: 1616 description:
1642 name: xml 1617 name: xml
1643 sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 1618 sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
1644 - url: "https://pub.dev" 1619 + url: "https://pub.flutter-io.cn"
1645 source: hosted 1620 source: hosted
1646 version: "6.5.0" 1621 version: "6.5.0"
1647 yaml: 1622 yaml:
1648 dependency: transitive 1623 dependency: transitive
1649 description: 1624 description:
1650 name: yaml 1625 name: yaml
1651 - sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea  
1652 - url: "https://pub.dev" 1626 + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
  1627 + url: "https://pub.flutter-io.cn"
1653 source: hosted 1628 source: hosted
1654 - version: "3.1.4" 1629 + version: "3.1.2"
1655 sdks: 1630 sdks:
1656 dart: ">=3.6.0 <4.0.0" 1631 dart: ">=3.6.0 <4.0.0"
1657 flutter: ">=3.27.0" 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,7 +16,7 @@ publish_to: &#39;none&#39; # Remove this line if you wish to publish to pub.dev
16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 # In Windows, build-name is used as the major, minor, and patch parts 17 # In Windows, build-name is used as the major, minor, and patch parts
18 # of the product and file versions while build-number is used as the build suffix. 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 environment: 21 environment:
22 sdk: '>=3.6.0 <4.0.0' 22 sdk: '>=3.6.0 <4.0.0'
@@ -95,7 +95,7 @@ dependencies: @@ -95,7 +95,7 @@ dependencies:
95 # 富文本插件 https://pub.dev/packages/extended_text 95 # 富文本插件 https://pub.dev/packages/extended_text
96 extended_text: ^11.0.1 96 extended_text: ^11.0.1
97 # 视频播放 https://pub.dev/packages/video_player 97 # 视频播放 https://pub.dev/packages/video_player
98 - video_player: 2.9.2 98 + video_player: 2.10.0
99 # 视频播放器控制层 https://pub.dev/packages/chewie 99 # 视频播放器控制层 https://pub.dev/packages/chewie
100 chewie: 1.7.5 100 chewie: 1.7.5
101 # 驰声语音评测 SDK(官方离线包裁剪为当前支持架构) 101 # 驰声语音评测 SDK(官方离线包裁剪为当前支持架构)
@@ -135,6 +135,9 @@ dependencies: @@ -135,6 +135,9 @@ dependencies:
135 dependency_overrides: 135 dependency_overrides:
136 # lottie 3.1.2 depends on http ^1.0.0 umeng_apm_sdk >=2.0.1 depends on http ^0.13.1 136 # lottie 3.1.2 depends on http ^1.0.0 umeng_apm_sdk >=2.0.1 depends on http ^0.13.1
137 http: ^0.13.3 137 http: ^0.13.3
  138 + # 开启 Media3 解码器初始化失败后的备用解码器回退。
  139 + video_player_android:
  140 + path: packages/video_player_android
138 141
139 dev_dependencies: 142 dev_dependencies:
140 build_runner: ^2.4.4 143 build_runner: ^2.4.4