Flutter (二十五)视频播放

视频播放

安装插件flutter pub add video_player

Android配置

android/app/src/main/AndroidManifest.xml<application> 标签下添加网络权限:

ini 复制代码
<uses-permission android:name="android.permission.INTERNET"/>

iOS配置

ios/Runner/Info.plist 中添加以下配置以允许网络加载:

xml 复制代码
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>

(注意:video_player 在 iOS 模拟器上无法运行,必须使用真机进行测试。)

代码

less 复制代码
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';

void main(List<String> args) {
  runApp(MaterialApp(home: Scaffold(body: HomePage())));
}

class HomePage extends StatefulWidget {
  HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  VideoPlayerController? _controller;

  void _onControllerChanged() {
    setState(() {});
  }

  String _fmt(Duration d) {
    final mm = (d.inMinutes % 60).toString().padLeft(2, '0');
    final ss = (d.inSeconds % 60).toString().padLeft(2, '0');
    return '$mm:$ss';
  }

  Widget _buildVideoView() {
    return Container(
      color: Colors.blueGrey,
      child: _controller?.value.isInitialized == true
          ? VideoPlayer(_controller!)
          : CircularProgressIndicator(),
    );
  }

  Widget _buildVideoControlView() {
    return Positioned(
      bottom: 0,
      left: 0,
      right: 0,
      height: 40,
      child: Container(color: Colors.white54),
    );
  }

  Widget _buildVideoPlayView() {
    return AspectRatio(
      aspectRatio: 16 / 9,
      child: Container(
        color: Colors.black,
        alignment: Alignment.center,
        child: Stack(children: [_buildVideoView(), _buildVideoControlView()]),
      ),
    );
  }

  final _speeds = const [0.5, 1.0, 1.25, 1.5, 2.0];

  Widget _buildSpeedButtons() {
    return Wrap(
      spacing: 6,
      children: _speeds.map((s) {
        final selected = _controller?.value.playbackSpeed == s;
        return GestureDetector(
          onTap: () async {
            if (_controller != null) {
              await _controller!.setPlaybackSpeed(s);
            }
          },
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
            decoration: BoxDecoration(
              color: selected ? Colors.blue : Colors.white12,
              borderRadius: BorderRadius.circular(4),
              border: Border.all(
                color: selected ? Colors.blueAccent : Colors.white24,
              ),
            ),
            child: Text(
              '${s.toStringAsFixed(s.truncateToDouble() == s ? 1 : s % 1 == 0.25 || s % 1 == 0.5 ? 2 : 1)}x',
              style: const TextStyle(color: Colors.white, fontSize: 12),
            ),
          ),
        );
      }).toList(),
    );
  }

  Widget _buildVideoInfoView() {
    final v = _controller?.value;
    final info = <String>[];
    if (v == null) {
      info.add('尚未加载视频');
    } else {
      info.add('状态: ${v.isInitialized ? "已初始化" : "初始化中"}');
      info.add('播放: ${v.isPlaying ? "播放中" : "已暂停"}');
      info.add('缓冲: ${v.isBuffering ? "缓冲中" : "完成"}');
      info.add('进度: ${_fmt(v.position)} / ${_fmt(v.duration)}');
      info.add('进度%: ${v.duration.inMilliseconds > 0 ? ((v.position.inMilliseconds / v.duration.inMilliseconds) * 100).toStringAsFixed(1) : "0.0"}%');
      info.add('分辨率: ${v.size.width.toInt()} x ${v.size.height.toInt()}');
      info.add('倍速: ${v.playbackSpeed.toStringAsFixed(1)}x');
      info.add('音量: ${(v.volume * 100).toInt()}%');
      info.add('错误: ${v.errorDescription ?? "无"}');
    }

    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(12),
      color: Colors.grey.shade900,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          ...info.map((s) => Text(s,
              style: const TextStyle(color: Colors.white, fontSize: 13, height: 1.5))),
          const SizedBox(height: 10),
          const Text('倍速选择:', style: TextStyle(color: Colors.white, fontSize: 13)),
          const SizedBox(height: 6),
          _buildSpeedButtons(),
        ],
      ),
    );
  }

  Widget _buildVideoSourceView() {
    return Container(
      color: Colors.greenAccent,
      height: 40,
      child: Flex(
        direction: Axis.horizontal,
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          GestureDetector(
            onTap: () async {
              final old = _controller;
              _controller = null;
              if (old != null) {
                old.removeListener(_onControllerChanged);
                await old.dispose();
              }
              _controller =
                  VideoPlayerController.asset('film/film.mp4')
                    ..addListener(_onControllerChanged);
              _controller?.initialize().then((_) {
                setState(() {});
                _controller?.play();
              });
            },
            child: Text("播放本地视频", style: TextStyle(color: Colors.white)),
          ),
          GestureDetector(
            onTap: () async {
              final old = _controller;
              _controller = null;
              if (old != null) {
                old.removeListener(_onControllerChanged);
                await old.dispose();
              }
              _controller = VideoPlayerController.networkUrl(
                Uri.parse(
                  'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
                ),
              )..addListener(_onControllerChanged);
              _controller?.initialize().then((_) {
                setState(() {});
                _controller?.play();
              });
            },
            child: Text("播放网络视频", style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(children: [
        _buildVideoPlayView(),
        _buildVideoInfoView(),
        _buildVideoSourceView(),
      ]),
    );
  }

  @override
  void dispose() {
    _controller?.removeListener(_onControllerChanged);
    _controller?.dispose();
    super.dispose();
  }
}
相关推荐
CharlesYu011 天前
前端性能优化的第一性原理,是不断缩短“用户发起意图 → 获得可用结果”之间的时间
前端
平头哥技术团队1 天前
Day 21 _ 页内锚点_给每段起个 id,目录写 href=_#id_,点一下页面就滚到那一段
前端·html·html5
子兮曰1 天前
Bun v1.4.1 深度解析:从 Zig 到 Rust,一场 11 天、64 个 AI 代理的语言迁徙
前端·后端·bun
人民广场吃泡面1 天前
什么是AI Agent?它又能给前端带来哪些效率提升?
前端·人工智能
中科三方1 天前
两家域名注册商资质被ICANN终止:企业域名资产安全再受关注
前端·网络·安全·域名
bug总结1 天前
uniapp vue3全局方法注册使用
前端·javascript·uni-app
华无丽言1 天前
如何在宜搭中实现获取子表中的字段值赋值到父表中?
前端·javascript·低代码
IT_陈寒1 天前
Vue的computed属性竟然坑了我一把
前端·人工智能·后端
威斯软科的老司机1 天前
通俗讲解 CNN 图像识别、向量 Embedding、Softmax 概率计算这三块的简化原理
前端·人工智能·ui·数字孪生
泯泷1 天前
手搓JSVM第 12 篇:完整最小 JSVM 实现与源码设计复盘
前端·javascript·前端框架