You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter实现YouTube视频下载:视频损坏、大小异常问题求助

问题排查与修复方案

核心问题分析

你遇到的文件损坏、体积过小问题,本质是直接请求YouTube网页链接下载到的是HTML页面,而非视频文件。YouTube的分享链接指向的是视频播放网页,不是原始的视频资源URL,所以http.get拿到的只是几KB的网页代码,自然无法正常播放。

另外代码还有几个次要问题:

  • 未处理用户拒绝存储权限的情况,会导致后续文件写入失败
  • 进度条变量_progress从未更新,下载过程中不会显示进度
  • 固定文件名video.mp4,多次下载会覆盖旧文件

修复步骤与代码实现

1. 引入专业的YouTube解析库

使用youtube_explode_dart库来解析YouTube视频的真实媒体流地址,这是处理YouTube下载的标准方案。在pubspec.yaml中添加依赖:

dependencies:
  youtube_explode_dart: ^2.0.0
  permission_handler: ^10.0.0
  path_provider: ^2.0.0
  http: ^0.13.0
  intl: ^0.18.0

2. 修改核心下载逻辑

以下是修复后的完整代码,包含权限处理、进度更新、唯一文件名生成等优化:

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:permission_handler/permission_handler.dart';
import 'package:path_provider/path_provider.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:intl/intl.dart';

class DownloadPage extends StatefulWidget {
  @override
  _DownloadPageState createState() => _DownloadPageState();
}

class _DownloadPageState extends State<DownloadPage> {
  final TextEditingController _linkController = TextEditingController();
  bool _isLoading = false;
  double _progress = 0;

  Future<void> _downloadVideo(String link) async {
    setState(() {
      _isLoading = true;
      _progress = 0;
    });

    // 处理存储权限
    final status = await Permission.storage.status;
    if (status.isDenied) {
      final result = await Permission.storage.request();
      if (!result.isGranted) {
        setState(() => _isLoading = false);
        _showErrorDialog("存储权限被拒绝,无法下载视频");
        return;
      }
    }

    try {
      // 解析YouTube视频
      final yt = YoutubeExplode();
      final video = await yt.videos.get(link);
      final manifest = await yt.videos.streamsClient.getManifest(video.id);
      
      // 选择最高质量的MP4视频流
      final streamInfo = manifest.muxed.withHighestBitrate();
      final streamUrl = streamInfo.url;

      // 获取存储目录并生成唯一文件名
      final directory = await getExternalStorageDirectory();
      final timestamp = DateFormat('yyyyMMddHHmmss').format(DateTime.now());
      final fileName = '${video.title.replaceAll(RegExp(r'[^\w\s]'), '')}_$timestamp.mp4';
      final filePath = '${directory!.path}/$fileName';

      // 带进度的下载
      final request = await http.get(Uri.parse(streamUrl.toString()),
          onReceiveProgress: (received, total) {
        if (total != -1) {
          setState(() {
            _progress = (received / total) * 100;
          });
        }
      });

      // 写入文件
      final file = File(filePath);
      await file.writeAsBytes(request.bodyBytes);

      // 关闭YoutubeExplode客户端
      await yt.close();

      setState(() => _isLoading = false);
      _showSuccessDialog(filePath);
    } catch (e) {
      setState(() => _isLoading = false);
      _showErrorDialog("下载失败:${e.toString()}");
    }
  }

  void _showSuccessDialog(String filePath) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('下载完成'),
        content: Text('视频已保存至:\n$filePath'),
        actions: [
          ElevatedButton(
            child: const Text('确定'),
            onPressed: () => Navigator.of(context).pop(),
          ),
        ],
      ),
    );
  }

  void _showErrorDialog(String message) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('下载失败'),
        content: Text(message),
        actions: [
          ElevatedButton(
            child: const Text('确定'),
            onPressed: () => Navigator.of(context).pop(),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('下载YouTube视频')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _linkController,
              decoration: const InputDecoration(
                hintText: '粘贴YouTube视频链接',
                border: OutlineInputBorder(),
              ),
              maxLines: 1,
            ),
            const SizedBox(height: 16.0),
            _isLoading
                ? Column(
                    children: [
                      LinearProgressIndicator(value: _progress / 100),
                      const SizedBox(height: 16.0),
                      Text('下载进度:${_progress.toStringAsFixed(1)}%'),
                    ],
                  )
                : ElevatedButton(
                    onPressed: () => _downloadVideo(_linkController.text.trim()),
                    child: const Text('开始下载'),
                  ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _linkController.dispose();
    super.dispose();
  }
}

关键优化说明

  • 视频流解析:通过youtube_explode_dart获取视频的真实媒体流URL,确保下载的是真正的视频文件
  • 权限处理:新增权限请求后的结果判断,避免无权限时的崩溃
  • 进度更新:利用http.get的onReceiveProgress回调实时更新下载进度
  • 唯一文件名:基于视频标题和时间戳生成文件名,避免覆盖旧文件
  • 错误处理:添加try-catch捕获解析和下载过程中的异常,给出友好提示

内容的提问来源于stack exchange,提问作者M Al

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 05:15:35