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();
  }
}
相关推荐
yivifu39 分钟前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
默_笙1 小时前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript
zhangzeyuaaa2 小时前
深入理解 Ruby 运算符:本质、分类、坑点与重载实战
开发语言·前端·ruby
一木 之林2 小时前
DeepSeek Agent 开发(一)
开发语言·前端·javascript
树下水月3 小时前
Typora破解
linux·服务器·前端
用户5508492902563 小时前
CSS布局实战:从Flex到Grid的完整避坑指南
前端
用户5508492902563 小时前
前端接口请求层怎么封装?一个可落地的请求方案
前端
MetWeave3 小时前
跟着一条 SPECI 走流水线:从电报到界面
前端
Helix2504 小时前
Chrome 开发者工具进阶:长截图、网络调试与性能分析
前端·chrome·chrome devtools·使用技巧·性能分析·开发者工具·长截图
liangshanbo12154 小时前
面试题:Webpack 的 publicPath 有什么作用?
前端·webpack·node.js