普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月26日首页

Flutter组件封装:视频播放组件全局封装

作者 SoaringHeart
2025年12月26日 20:52

一、需求来源

最近要开发一个在线视频播放的功能,每次实时获取播放,有些卡顿和初始化慢的问题,就随手优化一下。

二、使用示例

1、数据源

class VideoDetailsProvider extends ChangeNotifier {

    /// 当前播放中的
    final _videoModelController = StreamController<VideoDetailModel?>.broadcast();

    /// 当前播放中 Stream<VideoDetailModel>
    Stream<VideoDetailModel?> get videoModelStream => _videoModelController.stream;


    /// 点击(model不为空时播放,为空时关闭)
    sinkModelForVideoPlayer({required VideoDetailModel? model}) {
      _videoModelController.add(model);
    }

}

2、显示播放器组件

StreamBuilder<VideoDetailModel?>(
  stream: provider.videoModelStream,
  builder: (context, snapshot) {
    if (snapshot.data == null) {
      return const SizedBox();
    }
    final model = snapshot.data;
    if (model?.isVideo != true) {
      return const SizedBox();
    }

    return SafeArea(
      top: true,
      bottom: false,
      left: false,
      right: false,
      child: Container(
        decoration: BoxDecoration(
          color: Colors.black,
        ),
        child: AppVideoPlayer(
          key: Key(model?.url ?? ""),
          url: model?.url ?? "",
          onFullScreen: (value) async {
            if (!value) {
              await Future.delayed(const Duration(milliseconds: 300));//等待旋转完成
              SystemChromeExt.changeDeviceOrientation(isPortrait: true);
            }
          },
          onClose: () {
            DLog.d("close");
            provider.sinkModelForVideoPlayer(model: null);
          },
        ),
      ),
    );
  },
)

三、源码

1、AppVideoPlayer.dart

//
//  AppVideoPlayer.dart
//  flutter_templet_project
//
//  Created by shang on 2025/12/12 18:11.
//  Copyright © 2025/12/12 shang. All rights reserved.
//

import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/AppVideoPlayer/AppVideoPlayerService.dart';
import 'package:flutter_templet_project/extension/extension_local.dart';
import 'package:video_player/video_player.dart';

/// 播放器
class AppVideoPlayer extends StatefulWidget {
  const AppVideoPlayer({
    super.key,
    this.controller,
    required this.url,
    this.autoPlay = true,
    this.looping = false,
    this.aspectRatio = 16 / 9,
    this.isPortrait = true,
    this.fullScreenVN,
    this.onFullScreen,
    this.onClose,
  });

  final AppVideoPlayerController? controller;

  final String url;
  final bool autoPlay;
  final bool looping;
  final double aspectRatio;

  /// 设备方向
  final bool isPortrait;

  final ValueNotifier<bool>? fullScreenVN;

  final void Function(bool isFullScreen)? onFullScreen;

  final VoidCallback? onClose;

  @override
  State<AppVideoPlayer> createState() => _AppVideoPlayerState();
}

class _AppVideoPlayerState extends State<AppVideoPlayer> with WidgetsBindingObserver, AutomaticKeepAliveClientMixin {
  VideoPlayerController? _videoController;
  ChewieController? _chewieController;

  Duration position = Duration.zero;

  @override
  void dispose() {
    widget.controller?._detach(this);
    WidgetsBinding.instance.removeObserver(this);
    _onClose();
    // 不销毁 VideoPlayerController,让全局复用
    // _chewieController?.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    widget.controller?._attach(this);
    WidgetsBinding.instance.addObserver(this);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      initPlayer();
    });
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.inactive:
      case AppLifecycleState.hidden:
      case AppLifecycleState.paused:
        {
          _chewieController?.pause();
        }
        break;
      case AppLifecycleState.detached:
        break;
      case AppLifecycleState.resumed:
        {
          _chewieController?.play();
        }
        break;
    }
  }

  Future<void> initPlayer() async {
    // DLog.d(widget.url.split("/").last);
    assert(widget.url.startsWith("http"), "url 错误");

    _videoController = await AppVideoPlayerService.instance.getController(widget.url);

    _chewieController?.dispose();
    _chewieController = ChewieController(
      videoPlayerController: _videoController!,
      autoPlay: widget.autoPlay,
      looping: widget.looping,
      aspectRatio: widget.aspectRatio,
      autoInitialize: true,
      allowFullScreen: true,
      allowMuting: false,
      showControlsOnInitialize: false,
      // customControls: const AppVideoControls(),
    );

    _chewieController!.addListener(() {
      final isFullScreen = _chewieController!.isFullScreen;
      widget.fullScreenVN?.value = isFullScreen;
      widget.onFullScreen?.call(isFullScreen);
      if (isFullScreen) {
        DLog.d("进入全屏");
      } else {
        DLog.d("退出全屏");
      }
    });
    if (mounted) {
      setState(() {});
    }
  }

  Future<void> _onClose() async {
    if (_videoController?.value.isPlaying == true) {
      _videoController?.pause();
    }
  }

  @override
  void didUpdateWidget(covariant AppVideoPlayer oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.url != widget.url) {
      initPlayer();
    }
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    /// 🔥屏幕横竖切换时 rebuild Chewie,但不 rebuild video
    DLog.d([MediaQuery.of(context).orientation]);
    // setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    if (widget.url.startsWith("http") != true) {
      return const SizedBox();
    }

    if (_chewieController == null) {
      return const Center(child: CircularProgressIndicator());
    }
    // return Chewie(controller: _chewieController!);
    return Stack(
      children: [
        Positioned.fill(
          child: Chewie(
            controller: _chewieController!,
          ),
        ),
        Positioned(
          right: 20,
          top: 10,
          child: Container(
            // decoration: BoxDecoration(
            //   color: Colors.red,
            //   border: Border.all(color: Colors.blue),
            // ),
            child: buildCloseBtn(onTap: widget.onClose),
          ),
        ),
      ],
    );
  }

  Widget buildCloseBtn({VoidCallback? onTap}) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: () {
        DLog.d("buildCloseBtn");
        _onClose();
        if (onTap != null) {
          onTap();
          return;
        }
        Navigator.pop(context);
      },
      child: Container(
        // padding: EdgeInsets.all(6),
        // decoration: BoxDecoration(
        //   color: Colors.black54,
        //   shape: BoxShape.circle,
        //   border: Border.all(color: Colors.blue),
        // ),
        child: const Icon(Icons.close, color: Colors.white, size: 24),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

class AppVideoPlayerController {
  _AppVideoPlayerState? _anchor;

  void _attach(_AppVideoPlayerState anchor) {
    _anchor = anchor;
  }

  void _detach(_AppVideoPlayerState anchor) {
    if (_anchor == anchor) {
      _anchor = null;
    }
  }

  VideoPlayerController? get videoController {
    assert(_anchor != null);
    return _anchor!._videoController;
  }

  ChewieController? get chewieController {
    assert(_anchor != null);
    return _anchor!._chewieController;
  }
}

2、AppVideoPlayerService.dart

//
//  AppVideoPlayerService.dart
//  flutter_templet_project
//
//  Created by shang on 2025/12/12 18:10.
//  Copyright © 2025/12/12 shang. All rights reserved.
//

import 'package:flutter_templet_project/extension/extension_local.dart';
import 'package:quiver/collection.dart';
import 'package:video_player/video_player.dart';

/// 视频播放控制器全局管理
class AppVideoPlayerService {
  AppVideoPlayerService._();
  static final AppVideoPlayerService _instance = AppVideoPlayerService._();
  factory AppVideoPlayerService() => _instance;
  static AppVideoPlayerService get instance => _instance;

  /// 播放器字典
  LruMap<String, VideoPlayerController> get controllerMap => _controllerMap;
  // final _controllerMap = <String, VideoPlayerController>{};
  final _controllerMap = LruMap<String, VideoPlayerController>(maximumSize: 10);

  /// 最新使用播放器
  VideoPlayerController? current;

  /// 有缓存控制器
  bool hasCtrl({required String url}) => _controllerMap[url] != null;

  /// 是网络视频
  static bool isVideo(String? url) {
    if (url?.isNotEmpty != true) {
      return false;
    }

    final videoUri = Uri.tryParse(url!);
    if (videoUri == null) {
      return false;
    }

    final videoExt = ['.mp4', '.mov', '.avi', '.wmv', '.flv', '.mkv', '.webm'];
    final ext = url.toLowerCase();
    final result = videoExt.any((e) => ext.endsWith(e));
    return result;
  }

  /// 获取 VideoPlayerController
  Future<VideoPlayerController?> getController(String url, {bool isLog = false}) async {
    assert(url.startsWith("http"), "必须是视频链接,请检查链接是否合法");
    final vc = _controllerMap[url];
    if (vc != null) {
      current = vc;
      if (isLog) {
        DLog.d(["缓存: ${vc.hashCode}"]);
      }
      return vc;
    }

    final videoUri = Uri.tryParse(url);
    if (videoUri == null) {
      return null;
    }

    final ctrl = VideoPlayerController.networkUrl(videoUri);
    await ctrl.initialize();
    _controllerMap[url] = ctrl;
    current = ctrl;
    if (isLog) {
      DLog.d(["新建: ${_controllerMap[url].hashCode}"]);
    }
    return ctrl;
  }

  /// 播放
  Future<void> play({required String url, bool onlyOne = true}) async {
    await getController(url);
    for (final e in _controllerMap.entries) {
      if (e.key == url) {
        if (!e.value.value.isPlaying) {
          e.value.play();
        } else {
          e.value.pause();
        }
      } else {
        if (onlyOne) {
          e.value.pause();
        }
      }
    }
  }

  /// 暂停所有视频
  void pauseAll() {
    for (final e in _controllerMap.entries) {
      e.value.pause();
    }
  }

  void dispose(String url) {
    _controllerMap[url]?.dispose();
    _controllerMap.remove(url);
  }

  void disposeAll() {
    for (final c in _controllerMap.values) {
      c.dispose();
    }
    _controllerMap.clear();
  }
}

最后、总结

1、播放视频组件和视频列表是分开的,通过监听 VideoPlayerController 保持状态(播放,暂停)一致性,类似网络视频小窗口播放。

2、通过 AppVideoPlayerService 实现全局 VideoPlayerController 缓存,提高初始化加载速度。

3、通过 quiver 的 LruMap 实现最大缓存数控制,防止内存爆炸。

github

❌
❌