Blame view

lib/pages/video/lookvideo/widgets/video_widget.dart 14.2 KB
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
  }