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();
  }
}
相关推荐
半个落月1 小时前
在浏览器里运行 DeepSeek-R1:推理、流式输出与停止生成(二)
前端·人工智能·react.js
绿岛之北1 小时前
Electron 安全第四章: Preload 与 IPC
前端·electron
半个落月1 小时前
在浏览器里运行 DeepSeek-R1:从 WebGPU 检测到模型加载(一)
前端·人工智能·react.js
八角丶1 小时前
Node.js 事件循环详解(实验驱动)
前端·node.js
deli0070071 小时前
汉诺塔益智小游戏:浏览器里说句话,码道 WebUI 一键生成+部署上线
前端·ai编程
用户2181697049301 小时前
Flutter (二十四) 音频
前端
愚公搬代码1 小时前
【愚公系列】《Web应用安全》012-Behinder工具的使用
前端·安全
aixingpan1 小时前
aixingpan.cn API开发文档:api_docs_errors接口指南
前端·php
李剑一2 小时前
前端转AI要了解的技术,其他人不用看。前端架构基础之:让Js的计算运行在GPU上
前端·aigc·ai编程