dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
1
2
|
import 'dart:async';
|
119ba920
liangchengyou
feat:视频播放器
|
3
|
import 'package:common_utils/common_utils.dart';
|
119ba920
liangchengyou
feat:视频播放器
|
4
|
import 'package:flutter/material.dart';
|
46675a89
吴启风
feat:过渡页-视频环节
|
5
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
cb38bc90
liangchengyou
feat:视频跟读逻辑处理
|
6
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
119ba920
liangchengyou
feat:视频播放器
|
7
|
import 'package:video_player/video_player.dart';
|
91fe517a
liangchengyou
feat:看视频功能开发
|
8
|
import 'package:wow_english/common/extension/string_extension.dart';
|
dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
9
10
|
import 'package:wow_english/common/utils/video_playback_diagnostics.dart';
import 'package:wow_english/common/utils/video_rendering_preference.dart';
|
46675a89
吴启风
feat:过渡页-视频环节
|
11
|
import 'package:wow_english/pages/video/lookvideo/bloc/look_video_bloc.dart';
|
934e2b47
liangchengyou
feat:权限调整+课程进度接口对接
|
12
|
import 'package:wow_english/route/route.dart';
|
4b358e22
liangchengyou
feat:调整文件结构
|
13
|
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
14
15
|
import '../../../section/subsection/base_section/event.dart';
import '../../../section/subsection/base_section/state.dart';
|
4b358e22
liangchengyou
feat:调整文件结构
|
16
|
import 'video_opera_widget.dart';
|
119ba920
liangchengyou
feat:视频播放器
|
17
18
|
class VideoWidget extends StatefulWidget {
|
1e22b7d1
吴启风
feat:儿歌/视频环节接口请求时机优化
|
19
20
21
22
23
24
|
const VideoWidget(
{super.key,
this.videoUrl = '',
this.typeTitle,
this.courseLessonId = '',
this.isTopic = false});
|
119ba920
liangchengyou
feat:视频播放器
|
25
26
|
final String videoUrl;
|
842b7132
liangchengyou
feat:磨耳朵/练习页面调整
|
27
|
final String? typeTitle;
|
934e2b47
liangchengyou
feat:权限调整+课程进度接口对接
|
28
|
final String courseLessonId;
|
46675a89
吴启风
feat:过渡页-视频环节
|
29
|
final bool isTopic;
|
119ba920
liangchengyou
feat:视频播放器
|
30
31
32
33
34
35
36
37
38
39
40
41
42
|
@override
State<StatefulWidget> createState() {
return _VideoWidgetState();
}
}
class _VideoWidgetState extends State<VideoWidget> {
VideoPlayerController? _controller;
String _currentTime = '00:00';
String _totalTime = '00:00';
double _playDegree = 0.0;
bool _hiddenTipView = false;
|
dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
43
44
45
46
47
|
Object? _loadError;
Timer? _bufferingTimer;
int _loadGeneration = 0;
VoidCallback? _diagnosticListener;
bool _bufferingWarningShown = false;
|
119ba920
liangchengyou
feat:视频播放器
|
48
49
50
51
|
TimerUtil? timerUtil;
String formatDuration(Duration duration) {
String hours = duration.inHours.toString().padLeft(2, '0');
|
1e22b7d1
吴启风
feat:儿歌/视频环节接口请求时机优化
|
52
53
54
55
|
String minutes =
duration.inMinutes.remainder(60).toString().padLeft(2, '0');
String seconds =
duration.inSeconds.remainder(60).toString().padLeft(2, '0');
|
119ba920
liangchengyou
feat:视频播放器
|
56
57
58
59
60
|
return "$hours:$minutes:$seconds";
}
void _addListener() {
_controller!.addListener(() {
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
61
|
if (_controller!.value.isInitialized) {
|
119ba920
liangchengyou
feat:视频播放器
|
62
63
|
if (_controller!.value.isPlaying) {
setState(() {
|
66a7e3e7
吴启风
feat:退出课堂和结束课堂优化
|
64
|
double currentSecond = getCurrentPositionSeconds().toDouble();
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
65
66
67
|
int totalSecond =
_controller!.value.duration.inMinutes.remainder(60) * 60 +
_controller!.value.duration.inSeconds.remainder(60);
|
119ba920
liangchengyou
feat:视频播放器
|
68
|
_currentTime = formatDuration(_controller!.value.position);
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
69
|
_playDegree = currentSecond / totalSecond;
|
3840b7fe
liangchengyou
feat:更新设置页面
|
70
71
72
|
if (_playDegree > 1.0) {
_playDegree = 1.0;
}
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
73
|
if (_playDegree < 0) {
|
3840b7fe
liangchengyou
feat:更新设置页面
|
74
75
|
_playDegree = 0.0;
}
|
119ba920
liangchengyou
feat:视频播放器
|
76
|
});
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
77
78
79
80
81
82
83
|
} else if (widget.isTopic &&
//受限于video_player库没有唯一的播放完成状态标识,发现isBuffering=false的时候暂时是安全唯一的
_controller?.value.isBuffering == false &&
_controller!.value.isCompleted &&
_controller!.value.position.inSeconds ==
_controller!.value.duration.inSeconds) {
final lookVideoBloc = context.read<LookVideoBloc>();
|
1e22b7d1
吴启风
feat:儿歌/视频环节接口请求时机优化
|
84
85
86
87
88
89
90
91
92
|
lookVideoBloc.sectionComplete(
() {
popPage(data: {
'courseLessonId': widget.courseLessonId,
'currentTime': getCurrentPositionSeconds(),
'isCompleted': true,
'nextSection': widget.isTopic
});
} as VoidCallback, againSectionTap: (() {
|
c272c662
吴启风
fix: 崩溃修复
|
93
94
95
|
if (mounted && !lookVideoBloc.isClosed) {
lookVideoBloc.add(SectionAgainEvent());
}
|
1e22b7d1
吴启风
feat:儿歌/视频环节接口请求时机优化
|
96
|
}), context: context);
|
119ba920
liangchengyou
feat:视频播放器
|
97
98
99
100
101
102
|
}
}
});
}
//开始倒计时
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
103
104
105
|
void startTimer() {
if (timerUtil == null) {
timerUtil = TimerUtil(mInterval: 1000, mTotalTime: 1000 * 10);
|
119ba920
liangchengyou
feat:视频播放器
|
106
107
|
timerUtil!.setOnTimerTickCallback((int tick) {
double currentTick = tick / 1000;
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
108
109
|
if (currentTick.toInt() == 0) {
//倒计时结束
|
119ba920
liangchengyou
feat:视频播放器
|
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
setState(() {
_hiddenTipView = true;
});
timerUtil!.cancel();
timerUtil = null;
}
});
timerUtil!.startCountDown();
}
}
//取消倒计时
void cancelTimer() {
timerUtil!.cancel();
timerUtil = null;
}
|
91fe517a
liangchengyou
feat:看视频功能开发
|
127
128
|
void actionType(OperationType type) async {
if (type == OperationType.back) {
|
934e2b47
liangchengyou
feat:权限调整+课程进度接口对接
|
129
130
131
132
133
134
135
|
if (widget.courseLessonId.isEmpty) {
popPage();
} else {
if (_controller == null) {
popPage();
return;
}
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
136
137
|
popPage(data: {
'courseLessonId': widget.courseLessonId,
|
66a7e3e7
吴启风
feat:退出课堂和结束课堂优化
|
138
|
'currentTime': getCurrentPositionSeconds(),
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
139
|
});
|
934e2b47
liangchengyou
feat:权限调整+课程进度接口对接
|
140
|
}
|
91fe517a
liangchengyou
feat:看视频功能开发
|
141
142
143
144
145
146
|
} else if (type == OperationType.playState) {
if (_controller!.value.isPlaying) {
_controller!.pause();
} else {
_controller!.play();
}
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
147
|
setState(() {});
|
91fe517a
liangchengyou
feat:看视频功能开发
|
148
149
150
|
}
}
|
119ba920
liangchengyou
feat:视频播放器
|
151
152
153
|
@override
void initState() {
super.initState();
|
dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
|
unawaited(_initializeVideo());
}
Future<void> _initializeVideo() async {
final generation = ++_loadGeneration;
_bufferingTimer?.cancel();
_bufferingWarningShown = false;
final oldController = _controller;
if (oldController != null) {
if (_diagnosticListener != null) {
oldController.removeListener(_diagnosticListener!);
}
}
_diagnosticListener = null;
_controller = null;
_loadError = null;
if (oldController != null) {
await VideoPlaybackDiagnostics.disposeController(
oldController,
context: '课程视频切换',
);
}
if (!mounted || generation != _loadGeneration) return;
final url = widget.videoUrl.trim();
if (url.isEmpty) {
_loadError = StateError('课程视频地址为空');
VideoPlaybackDiagnostics.show('课程视频地址为空');
if (mounted) setState(() {});
return;
}
final controller = VideoPlayerController.networkUrl(
Uri.parse(url),
viewType: VideoRenderingPreference.viewType,
);
_controller = controller;
if (mounted) setState(() {});
var stage = '视频初始化';
try {
await controller
.initialize()
.timeout(VideoPlaybackDiagnostics.initializeTimeout);
if (!mounted ||
generation != _loadGeneration ||
controller != _controller) {
await controller.dispose();
return;
}
stage = '播放器配置';
await controller
.setLooping(!widget.isTopic)
.timeout(VideoPlaybackDiagnostics.commandTimeout);
await controller
.setVolume(1)
.timeout(VideoPlaybackDiagnostics.commandTimeout);
_addListener();
_diagnosticListener = _onDiagnosticChanged;
controller.addListener(_diagnosticListener!);
_updateBufferingWatch(controller);
await controller.play().timeout(VideoPlaybackDiagnostics.commandTimeout);
if (!mounted || generation != _loadGeneration) return;
startTimer();
setState(() {
_currentTime = formatDuration(controller.value.position);
_totalTime = formatDuration(controller.value.duration);
|
119ba920
liangchengyou
feat:视频播放器
|
218
|
});
|
dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
|
} catch (error) {
final failedCurrentController = controller == _controller;
if (failedCurrentController) {
_bufferingTimer?.cancel();
_bufferingTimer = null;
controller.removeListener(_diagnosticListener ?? _onDiagnosticChanged);
}
await VideoPlaybackDiagnostics.disposeController(
controller,
context: '课程视频初始化失败($stage)',
);
if (!mounted ||
generation != _loadGeneration ||
!failedCurrentController) {
return;
}
_controller = null;
final message = VideoPlaybackDiagnostics.messageFor(error, stage: stage);
debugPrint('课程视频加载失败(stage=$stage): $error');
VideoPlaybackDiagnostics.show(message);
setState(() => _loadError = error);
}
}
void _onDiagnosticChanged() {
final controller = _controller;
if (!mounted || controller == null) return;
_updateBufferingWatch(controller);
if (controller.value.hasError && _loadError == null) {
final error = controller.value.errorDescription ?? '未知播放器错误';
debugPrint('课程视频播放失败: $error');
VideoPlaybackDiagnostics.show(
VideoPlaybackDiagnostics.messageFor(error, stage: '视频播放/解码'),
);
setState(() => _loadError = error);
}
}
void _updateBufferingWatch(VideoPlayerController controller) {
if (!controller.value.isBuffering) {
_bufferingTimer?.cancel();
_bufferingTimer = null;
_bufferingWarningShown = false;
return;
}
if (_bufferingTimer != null || _bufferingWarningShown) return;
_bufferingTimer = Timer(VideoPlaybackDiagnostics.bufferingWarningDelay, () {
_bufferingTimer = null;
if (!mounted ||
controller != _controller ||
!controller.value.isBuffering) {
return;
}
_bufferingWarningShown = true;
final error = controller.value.errorDescription;
final message = error == null
? '视频持续缓冲,网络或设备解码可能异常'
: VideoPlaybackDiagnostics.messageFor(error, stage: '视频播放/解码');
debugPrint('课程视频持续缓冲: $message ${error ?? ''}');
VideoPlaybackDiagnostics.show(message);
});
|
119ba920
liangchengyou
feat:视频播放器
|
280
281
282
283
|
}
@override
Widget build(BuildContext context) {
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
284
285
286
|
return BlocListener<LookVideoBloc, BaseSectionState>(
listener: (context, state) async {
if (state is SectionAgainState) {
|
dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
287
288
289
290
291
|
final controller = _controller;
if (controller?.value.isInitialized == true) {
await controller!.seekTo(Duration.zero);
await controller.play();
}
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
292
|
}
|
119ba920
liangchengyou
feat:视频播放器
|
293
|
},
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
294
295
|
child: GestureDetector(
onTap: () {
|
dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
296
297
298
|
if (_loadError != null || _controller?.value.isInitialized != true) {
return;
}
|
91fe517a
liangchengyou
feat:看视频功能开发
|
299
|
setState(() {
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
300
301
302
303
304
305
306
307
|
_hiddenTipView = !_hiddenTipView;
if (!_hiddenTipView) {
startTimer();
} else {
if (timerUtil!.isActive()) {
cancelTimer();
}
}
|
91fe517a
liangchengyou
feat:看视频功能开发
|
308
|
});
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
309
310
|
},
onDoubleTap: () {
|
dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
311
|
if (_controller?.value.isInitialized == true) {
|
aa0d2360
吴启风
feat:过渡页-视频环节(再来一次)
|
312
313
314
315
316
317
318
319
320
|
if (_controller!.value.isPlaying) {
_controller!.pause();
} else {
_controller!.play();
}
setState(() {});
}
},
child: Center(
|
dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
321
322
323
|
child: _loadError != null
? Column(
mainAxisSize: MainAxisSize.min,
|
1e22b7d1
吴启风
feat:儿歌/视频环节接口请求时机优化
|
324
|
children: [
|
dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
325
326
327
328
329
|
const Text('视频加载失败,请重试'),
const SizedBox(height: 8),
ElevatedButton(
onPressed: _initializeVideo,
child: const Text('重试'),
|
1e22b7d1
吴启风
feat:儿歌/视频环节接口请求时机优化
|
330
|
),
|
1e22b7d1
吴启风
feat:儿歌/视频环节接口请求时机优化
|
331
332
|
],
)
|
dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
|
: _controller?.value.isInitialized == true
? Stack(
alignment: Alignment.center,
children: [
SizedBox(
height: double.infinity,
width: double.infinity,
child: AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: VideoPlayer(_controller!),
),
),
Offstage(
offstage: _hiddenTipView,
child: VideoOperaWidget(
title: widget.typeTitle ?? 'song',
degree: _playDegree,
totalTime: _totalTime,
currentTime: _currentTime,
isPlay: _controller!.value.isPlaying,
actionEvent: (OperationType type) {
actionType(type);
},
sliderChangeEvent: (double degree) {
int totalSecond = _controller!
.value.duration.inMinutes
.remainder(60) *
60 +
_controller!.value.duration.inSeconds
.remainder(60);
int positionSecond =
(totalSecond * degree).toInt();
_controller!
.seekTo(Duration(seconds: positionSecond));
},
),
),
Offstage(
offstage: _controller!.value.isPlaying,
child: IconButton(
onPressed: () {
_controller!.play();
},
icon: Image.asset(
'video_stop'.assetPng,
width: 70.w,
height: 70.h,
),
),
)
],
)
: Container(
color: Colors.white,
child: const CircularProgressIndicator(),
// Text(
// '视频加载中....',
// style: TextStyle(fontSize: 20.sp, color: Colors.black),
// ),
),
|
119ba920
liangchengyou
feat:视频播放器
|
393
394
395
396
397
398
399
|
),
),
);
}
@override
void dispose() {
|
dbca8811
吴启风
fix: 修复视频兼容性并增加渲染...
|
400
401
402
403
404
405
406
407
408
409
410
411
|
_loadGeneration++;
_bufferingTimer?.cancel();
if (_diagnosticListener != null) {
_controller?.removeListener(_diagnosticListener!);
}
final controller = _controller;
if (controller != null) {
unawaited(VideoPlaybackDiagnostics.disposeController(
controller,
context: '课程视频页面退出',
));
}
|
119ba920
liangchengyou
feat:视频播放器
|
412
413
414
415
416
417
|
if (timerUtil != null) {
timerUtil!.cancel();
timerUtil = null;
}
super.dispose();
}
|
66a7e3e7
吴启风
feat:退出课堂和结束课堂优化
|
418
419
420
421
422
423
|
///获取当前进度秒数
int getCurrentPositionSeconds() {
return (_controller!.value.position.inMinutes.remainder(60) * 60 +
_controller!.value.position.inSeconds.remainder(60));
}
|
119ba920
liangchengyou
feat:视频播放器
|
424
|
}
|