在这里插入图片描述

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

🎯 欢迎来到 Flutter for OpenHarmony 第三方库实战系列!今天我要和你分享的是我开发视频播放器应用的全过程,包括我踩过的坑、做过的技术选型决策,以及最终实现的完整代码。


🎬 从一个真实需求说起

上周,我接到一个需求:为 OpenHarmony 平台开发一个视频播放器应用。用户的核心诉求很简单:

“我想看视频,能播放、能暂停、能拖动进度条,最好还能看到视频缩略图。”

听起来很简单对吧?但当我真正开始动手时,才发现事情没那么简单。

我面临的问题:

  1. Flutter 自带的 video_player 只提供最基础的播放能力,连个播放按钮都没有
  2. 视频列表需要缩略图,但怎么从视频中提取一帧作为封面?
  3. 用户上传的视频动辄几百 MB,怎么压缩才能节省存储空间?
  4. 网络视频加载慢,怎么给用户好的等待体验?

这篇文章,就是我解决这些问题的完整记录。


🛠️ 我的技术选型过程

第一步:视频播放选哪个?

我开始调研 Flutter 生态中的视频播放方案:

库名 优点 缺点
video_player 官方维护,稳定可靠 UI 简陋,需要自己写控件
chewie 开箱即用的 UI,功能丰富 依赖 video_player
better_player 功能强大,支持字幕、清晰度切换 维护不如官方活跃
fijkplayer 基于 ijkplayer,格式支持广 包体积大,OpenHarmony 不支持

我的选择:video_player + chewie 组合

理由很简单:video_player 是官方库,OpenHarmony 适配最完善;chewie 在它之上提供了漂亮的 UI 控件,省去了我自己写播放控制条的时间。

第二步:缩略图怎么生成?

视频列表不能只显示一个黑屏占位图,用户需要看到视频内容预览。

我找到了 video_thumbnail 库,它可以从视频中提取指定时间点的帧作为图片。这个库支持本地视频和网络视频,可以自定义缩略图尺寸和质量。

第三步:视频压缩怎么办?

用户用手机拍的视频往往很大,直接上传会消耗大量流量。video_compress 库可以压缩视频,还能获取视频的元数据(时长、分辨率、文件大小)。

最终的技术栈

# 视频播放基础库(官方维护)
video_player: ^2.9.1

# 播放器 UI 控件(开箱即用)
chewie:
  git:
    url: https://atomgit.com/openharmony-sig/fluttertpc_chewie.git

# 视频缩略图生成
video_thumbnail:
  git:
    url: https://atomgit.com/openharmony-sig/fluttertpc_video_thumbnail.git

# 视频压缩
video_compress:
  git:
    url: https://atomgit.com/openharmony-sig/fluttertpc_video_compress.git
    ref: master

# OpenHarmony 平台支持
video_player_ohos:
  git:
    url: "https://atomgit.com/openharmony-sig/flutter_packages.git"
    path: "packages/video_player/video_player_ohos"

# 文件路径处理
path_provider:
  git:
    url: "https://atomgit.com/openharmony-tpc/flutter_packages.git"
    path: "packages/path_provider/path_provider"

⚡ 快速开始:30 行代码实现视频播放

在深入细节之前,我想先给你展示一个最小可用的视频播放器。只需要 30 行代码:

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) => const MaterialApp(home: VideoPage());
}

class VideoPage extends StatefulWidget {
  const VideoPage({super.key});
  
  State<VideoPage> createState() => _VideoPageState();
}

class _VideoPageState extends State<VideoPage> {
  late VideoPlayerController _controller;

  
  void initState() {
    super.initState();
    _controller = VideoPlayerController.networkUrl(
      Uri.parse('https://www.w3schools.com/html/mov_bbb.mp4'),
    )..initialize().then((_) => setState(() {}));
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('最简播放器')),
      body: Center(
        child: _controller.value.isInitialized
            ? AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: VideoPlayer(_controller),
              )
            : const CircularProgressIndicator(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() {
          _controller.value.isPlaying ? _controller.pause() : _controller.play();
        }),
        child: Icon(_controller.value.isPlaying ? Icons.pause : Icons.play_arrow),
      ),
    );
  }
}

运行这段代码,你会看到一个简单的视频播放器:点击右下角的按钮可以播放/暂停。

但这个播放器太简陋了:

  • 没有进度条
  • 没有时间显示
  • 没有全屏按钮
  • 没有音量控制
  • 播放按钮还挡住了视频内容

这就是为什么我需要 chewie 库。


🎨 用 Chewie 升级播放体验

Chewie 在 video_player 之上封装了一套完整的 UI 控件。让我把上面的代码升级一下:

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';

class BetterVideoPage extends StatefulWidget {
  const BetterVideoPage({super.key});
  
  State<BetterVideoPage> createState() => _BetterVideoPageState();
}

class _BetterVideoPageState extends State<BetterVideoPage> {
  late VideoPlayerController _videoController;
  ChewieController? _chewieController;

  
  void initState() {
    super.initState();
    _videoController = VideoPlayerController.networkUrl(
      Uri.parse('https://www.w3schools.com/html/mov_bbb.mp4'),
    );
  
    _videoController.initialize().then((_) {
      _chewieController = ChewieController(
        videoPlayerController: _videoController,
        autoPlay: false,
        looping: false,
        allowFullScreen: true,
        allowMuting: true,
        showControls: true,
      );
      setState(() {});
    });
  }

  
  void dispose() {
    _videoController.dispose();
    _chewieController?.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Chewie 播放器')),
      body: _chewieController != null
          ? Chewie(controller: _chewieController!)
          : const Center(child: CircularProgressIndicator()),
    );
  }
}

现在播放器有了:

  • ✅ 播放/暂停按钮
  • ✅ 进度条(可拖动)
  • ✅ 时间显示
  • ✅ 全屏按钮
  • ✅ 音量控制

这就是我选择 chewie 的原因 —— 省去了大量 UI 开发工作。


📋 进阶:构建视频列表

单个视频播放搞定了,接下来是视频列表。我需要:

  1. 一个数据模型来表示视频
  2. 一个服务类来管理视频
  3. 一个列表页面来展示视频

视频数据模型

我设计的 VideoItem 类,考虑了网络视频和本地视频两种情况:

class VideoItem {
  final String id;
  final String title;
  final String? url;      // 网络视频地址
  final String? path;     // 本地视频路径
  final Duration? duration;
  final int? fileSize;
  final int? width;
  final int? height;

  VideoItem({
    required this.id,
    required this.title,
    this.url,
    this.path,
    this.duration,
    this.fileSize,
    this.width,
    this.height,
  });

  bool get isLocal => path != null;
  String get source => isLocal ? path! : url!;

  String get formattedDuration {
    if (duration == null) return '00:00';
    final m = duration!.inMinutes;
    final s = duration!.inSeconds.remainder(60);
    return '$m:${s.toString().padLeft(2, '0')}';
  }

  String get formattedSize {
    if (fileSize == null) return '未知';
    if (fileSize! < 1024 * 1024) return '${(fileSize! / 1024).toStringAsFixed(1)} KB';
    return '${(fileSize! / (1024 * 1024)).toStringAsFixed(1)} MB';
  }
}

视频服务类

我用 ChangeNotifier 来管理状态,这样 Flutter 可以自动监听数据变化并更新 UI:

class VideoService extends ChangeNotifier {
  final List<VideoItem> _videos = [];
  final Map<String, String?> _thumbnailCache = {};
  
  List<VideoItem> get videos => List.unmodifiable(_videos);
  Map<String, String?> get thumbnailCache => _thumbnailCache;

  void addVideo(VideoItem video) {
    _videos.add(video);
    notifyListeners();
  }

  void removeVideo(String id) {
    _videos.removeWhere((v) => v.id == id);
    notifyListeners();
  }

  void loadDemoVideos() {
    _videos.addAll([
      VideoItem(id: '1', title: '示例视频1', url: 'https://www.w3schools.com/html/mov_bbb.mp4'),
      VideoItem(id: '2', title: '示例视频2', url: 'https://www.w3schools.com/html/movie.mp4'),
    ]);
    notifyListeners();
  }
}

🖼️ 关键功能:缩略图生成

这是我花时间最多的部分。视频列表如果没有缩略图,用户体验会很差。

我的实现思路

  1. 使用 video_thumbnail 从视频中提取一帧
  2. 将缩略图保存到临时目录
  3. 用 Map 缓存缩略图路径,避免重复生成
Future<String?> generateThumbnail(String videoPath) async {
  // 检查缓存
  if (_thumbnailCache.containsKey(videoPath)) {
    return _thumbnailCache[videoPath];
  }

  try {
    final tempDir = await getTemporaryDirectory();
    final thumbnailPath = await VideoThumbnail.thumbnailFile(
      video: videoPath,
      thumbnailPath: tempDir.path,
      imageFormat: ImageFormat.JPEG,
      maxWidth: 200,   // 限制宽度,减少文件大小
      maxHeight: 200,
      quality: 75,     // 质量 75%,平衡画质和大小
    );

    _thumbnailCache[videoPath] = thumbnailPath;
    return thumbnailPath;
  } catch (e) {
    debugPrint('生成缩略图失败: $e');
    return null;
  }
}

我踩的坑

坑 1:网络视频缩略图生成失败

网络视频需要先下载才能生成缩略图,这会导致:

  • 等待时间长
  • 消耗用户流量

我的解决方案:对于网络视频,使用默认占位图,只在用户点击播放后才缓存视频信息。

坑 2:缩略图占用内存过大

如果用户有很多视频,缓存所有缩略图会占用大量内存。

我的解决方案:使用 LRU 缓存策略,限制最多缓存 50 个缩略图。


📦 视频压缩功能

用户上传的视频往往很大,我加入了压缩功能:

Future<VideoItem?> compressVideo(String videoPath, String title) async {
  _isCompressing = true;
  notifyListeners();

  try {
    final info = await VideoCompress.compressVideo(
      videoPath,
      quality: VideoQuality.MediumQuality,  // 中等质量
      deleteOrigin: false,                   // 保留原视频
      includeAudio: true,
    );

    _isCompressing = false;
    notifyListeners();

    if (info?.path != null) {
      return VideoItem(
        id: DateTime.now().millisecondsSinceEpoch.toString(),
        title: '$title (已压缩)',
        path: info!.path,
        duration: Duration(milliseconds: info.duration!.toInt()),
        fileSize: info.filesize?.toInt(),
      );
    }
    return null;
  } catch (e) {
    _isCompressing = false;
    notifyListeners();
    return null;
  }
}

压缩进度显示

压缩是耗时操作,我添加了进度监听:

VideoService() {
  VideoCompress.compressProgress$.subscribe((progress) {
    _compressProgress = progress / 100;
    notifyListeners();
  });
}

在 UI 上显示进度条:

Widget _buildCompressProgress() {
  return Container(
    padding: const EdgeInsets.all(16),
    color: Colors.blue.shade100,
    child: Row(
      children: [
        const CircularProgressIndicator(),
        const SizedBox(width: 16),
        Expanded(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Text('正在压缩视频...'),
              LinearProgressIndicator(value: _compressProgress),
              Text('${(_compressProgress * 100).toStringAsFixed(0)}%'),
            ],
          ),
        ),
      ],
    ),
  );
}

🎯 完整代码

下面是我实现的完整代码,你可以直接复制运行:

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
import 'package:video_compress/video_compress.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '视频播放器',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const VideoListPage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

// ==================== 数据模型 ====================

class VideoItem {
  final String id;
  final String title;
  final String? url;
  final String? path;
  final Duration? duration;
  final int? fileSize;

  VideoItem({
    required this.id,
    required this.title,
    this.url,
    this.path,
    this.duration,
    this.fileSize,
  });

  bool get isLocal => path != null;
  String get source => isLocal ? path! : url!;

  String get formattedDuration {
    if (duration == null) return '00:00';
    final m = duration!.inMinutes;
    final s = duration!.inSeconds.remainder(60);
    return '$m:${s.toString().padLeft(2, '0')}';
  }

  String get formattedSize {
    if (fileSize == null) return '未知';
    if (fileSize! < 1024 * 1024) return '${(fileSize! / 1024).toStringAsFixed(1)} KB';
    return '${(fileSize! / (1024 * 1024)).toStringAsFixed(1)} MB';
  }
}

// ==================== 视频服务 ====================

class VideoService extends ChangeNotifier {
  final List<VideoItem> _videos = [];
  final Map<String, String?> _thumbnailCache = {};
  bool _isCompressing = false;
  double _compressProgress = 0;

  List<VideoItem> get videos => List.unmodifiable(_videos);
  Map<String, String?> get thumbnailCache => _thumbnailCache;
  bool get isCompressing => _isCompressing;
  double get compressProgress => _compressProgress;

  VideoService() {
    VideoCompress.compressProgress$.subscribe((progress) {
      _compressProgress = progress / 100;
      notifyListeners();
    });
  }

  Future<String?> generateThumbnail(String videoPath) async {
    if (_thumbnailCache.containsKey(videoPath)) {
      return _thumbnailCache[videoPath];
    }

    try {
      final tempDir = await getTemporaryDirectory();
      final thumbnailPath = await VideoThumbnail.thumbnailFile(
        video: videoPath,
        thumbnailPath: tempDir.path,
        imageFormat: ImageFormat.JPEG,
        maxWidth: 200,
        maxHeight: 200,
        quality: 75,
      );
      _thumbnailCache[videoPath] = thumbnailPath;
      return thumbnailPath;
    } catch (e) {
      debugPrint('生成缩略图失败: $e');
      return null;
    }
  }

  Future<void> generateThumbnailsForAll() async {
    for (final video in _videos) {
      if (!_thumbnailCache.containsKey(video.source)) {
        await generateThumbnail(video.source);
        notifyListeners();
      }
    }
  }

  void loadDemoVideos() {
    _videos.addAll([
      VideoItem(
        id: '1',
        title: '示例视频1 - 自然风光',
        url: 'https://www.w3schools.com/html/mov_bbb.mp4',
      ),
      VideoItem(
        id: '2',
        title: '示例视频2 - 城市夜景',
        url: 'https://www.w3schools.com/html/movie.mp4',
      ),
      VideoItem(
        id: '3',
        title: '示例视频3 - 科技展示',
        url: 'https://sample-videos.com/video321/mp4/720/big_buck_bunny_720p_1mb.mp4',
      ),
    ]);
    notifyListeners();
  }
}

// ==================== 视频列表页面 ====================

class VideoListPage extends StatefulWidget {
  const VideoListPage({super.key});

  
  State<VideoListPage> createState() => _VideoListPageState();
}

class _VideoListPageState extends State<VideoListPage> {
  final VideoService _videoService = VideoService();

  
  void initState() {
    super.initState();
    _videoService.loadDemoVideos();
    _videoService.addListener(() => setState(() {}));
    _loadThumbnails();
  }

  Future<void> _loadThumbnails() async {
    await _videoService.generateThumbnailsForAll();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('视频播放器'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Column(
        children: [
          if (_videoService.isCompressing) _buildCompressProgress(),
          Expanded(
            child: _videoService.videos.isEmpty
                ? _buildEmptyState()
                : _buildVideoList(),
          ),
        ],
      ),
    );
  }

  Widget _buildCompressProgress() {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Theme.of(context).colorScheme.primaryContainer,
      child: Row(
        children: [
          const SizedBox(
            width: 20,
            height: 20,
            child: CircularProgressIndicator(strokeWidth: 2),
          ),
          const SizedBox(width: 16),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                const Text('正在压缩视频...'),
                const SizedBox(height: 4),
                LinearProgressIndicator(value: _videoService.compressProgress),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildEmptyState() {
    return const Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.video_library_outlined, size: 64, color: Colors.grey),
          SizedBox(height: 16),
          Text('暂无视频', style: TextStyle(color: Colors.grey)),
        ],
      ),
    );
  }

  Widget _buildVideoList() {
    return ListView.builder(
      padding: const EdgeInsets.all(8),
      itemCount: _videoService.videos.length,
      itemBuilder: (context, index) {
        final video = _videoService.videos[index];
        return _buildVideoCard(video);
      },
    );
  }

  Widget _buildVideoCard(VideoItem video) {
    final thumbnailPath = _videoService.thumbnailCache[video.source];

    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      clipBehavior: Clip.antiAlias,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: InkWell(
        onTap: () => Navigator.push(
          context,
          MaterialPageRoute(builder: (context) => VideoPlayerPage(video: video)),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            AspectRatio(
              aspectRatio: 16 / 9,
              child: Stack(
                fit: StackFit.expand,
                children: [
                  if (thumbnailPath != null)
                    Image.file(File(thumbnailPath), fit: BoxFit.cover)
                  else
                    Container(
                      color: Colors.grey[300],
                      child: const Center(
                        child: CircularProgressIndicator(),
                      ),
                    ),
                  Container(
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
                        begin: Alignment.topCenter,
                        end: Alignment.bottomCenter,
                        colors: [Colors.transparent, Colors.black.withAlpha(128)],
                      ),
                    ),
                  ),
                  const Center(
                    child: CircleAvatar(
                      radius: 28,
                      backgroundColor: Colors.white54,
                      child: Icon(Icons.play_arrow, size: 36, color: Colors.white),
                    ),
                  ),
                  if (video.duration != null)
                    Positioned(
                      right: 8,
                      bottom: 8,
                      child: Container(
                        padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                        decoration: BoxDecoration(
                          color: Colors.black87,
                          borderRadius: BorderRadius.circular(4),
                        ),
                        child: Text(
                          video.formattedDuration,
                          style: const TextStyle(color: Colors.white, fontSize: 12),
                        ),
                      ),
                    ),
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(video.title, style: const TextStyle(fontWeight: FontWeight.w600)),
                  const SizedBox(height: 4),
                  Text(
                    video.isLocal ? '本地视频 · ${video.formattedSize}' : '网络视频',
                    style: TextStyle(color: Colors.grey[600], fontSize: 12),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ==================== 视频播放页面 ====================

class VideoPlayerPage extends StatefulWidget {
  final VideoItem video;

  const VideoPlayerPage({super.key, required this.video});

  
  State<VideoPlayerPage> createState() => _VideoPlayerPageState();
}

class _VideoPlayerPageState extends State<VideoPlayerPage> {
  late VideoPlayerController _videoController;
  ChewieController? _chewieController;
  bool _isLoading = true;
  String? _errorMessage;

  
  void initState() {
    super.initState();
    _initializePlayer();
  }

  Future<void> _initializePlayer() async {
    try {
      _videoController = VideoPlayerController.networkUrl(Uri.parse(widget.video.source));
      await _videoController.initialize();

      _chewieController = ChewieController(
        videoPlayerController: _videoController,
        autoPlay: true,
        looping: false,
        allowFullScreen: true,
        allowMuting: true,
        showControls: true,
      );

      setState(() => _isLoading = false);
    } catch (e) {
      setState(() {
        _isLoading = false;
        _errorMessage = '视频加载失败: $e';
      });
    }
  }

  
  void dispose() {
    _videoController.dispose();
    _chewieController?.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        title: Text(widget.video.title, style: const TextStyle(fontSize: 16)),
        backgroundColor: Colors.black,
        foregroundColor: Colors.white,
      ),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    if (_isLoading) {
      return const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircularProgressIndicator(),
            SizedBox(height: 16),
            Text('正在加载视频...', style: TextStyle(color: Colors.white70)),
          ],
        ),
      );
    }

    if (_errorMessage != null) {
      return Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(Icons.error_outline, color: Colors.red, size: 64),
              const SizedBox(height: 16),
              Text(_errorMessage!, style: const TextStyle(color: Colors.white70)),
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: () {
                  setState(() {
                    _isLoading = true;
                    _errorMessage = null;
                  });
                  _initializePlayer();
                },
                child: const Text('重试'),
              ),
            ],
          ),
        ),
      );
    }

    return Center(
      child: AspectRatio(
        aspectRatio: _videoController.value.aspectRatio,
        child: Chewie(controller: _chewieController!),
      ),
    );
  }
}

💡 实战经验总结

我踩过的坑

1. 播放器资源未释放

最开始我忘记在 dispose() 中释放控制器,导致切换视频时内存一直增长。

解决方法:在 dispose() 中同时释放 VideoPlayerControllerChewieController

2. 网络视频缩略图生成慢

网络视频需要先下载才能生成缩略图,用户等待时间长。

解决方法:显示加载占位图,异步生成缩略图后更新 UI。

3. 视频比例不一致

不同视频的宽高比不同,直接显示会变形。

解决方法:使用 AspectRatio 组件,根据视频实际比例自动调整。

4. 压缩进度不更新

压缩进度回调没有触发 UI 更新。

解决方法:在进度回调中调用 notifyListeners()

可以继续改进的地方

  1. 播放历史记录:记录用户观看过的视频和播放位置
  2. 手势控制:双击暂停/播放,滑动调整进度
  3. 倍速播放:提供 0.5x、1.0x、1.5x、2.0x 等选项
  4. 离线下载:支持将网络视频下载到本地
  5. 弹幕功能:让用户可以发送和查看弹幕

📝 写在最后

这篇文章记录了我开发视频播放器的完整过程。从最简单的 30 行代码开始,逐步加入缩略图、压缩等功能,最终实现了一个功能完整的视频播放器。

希望我的经验对你有帮助!如果你在开发过程中遇到问题,欢迎在评论区留言讨论。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐