基础入门 Flutter for OpenHarmony:video_thumbnail 视频缩略图详解

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
🎯 欢迎来到 Flutter for OpenHarmony 社区!本文将深入讲解 Flutter 中 video_thumbnail 视频缩略图插件的使用方法,带你全面掌握从视频中提取缩略图、设置缩略图参数等功能。


一、video_thumbnail 组件概述

在 Flutter for OpenHarmony 应用开发中,video_thumbnail 是一个非常实用的插件,用于从视频文件中提取缩略图。它支持本地视频和网络视频,可以自定义缩略图的尺寸、格式、质量和提取时间点。

📋 video_thumbnail 组件特点

特点 说明
跨平台支持 支持 Android、iOS、OpenHarmony
多种来源 支持本地视频文件和网络视频 URL
多种格式 支持 JPEG、PNG、WebP 输出格式
尺寸控制 支持设置最大宽高限制
时间点选择 支持指定提取缩略图的时间点
质量控制 支持设置输出图片质量
两种输出方式 支持返回文件路径或内存数据
HTTP 头支持 支持自定义 HTTP 请求头

💡 使用场景:视频列表预览、视频编辑应用、媒体库管理、视频分享预览等需要显示视频缩略图的场景。


二、OpenHarmony 平台适配说明

2.1 兼容性信息

本项目适配 Flutter 3.7.12-ohos-1.0.6,SDK 5.0.0(12),DevEco Studio 5.0.13.200,ROM 5.1.0.120 SP3。

2.2 支持的功能

在 OpenHarmony 平台上,video_thumbnail 支持以下功能:

功能 说明 OpenHarmony 支持
thumbnailData() 生成缩略图数据(内存) ✅ yes
thumbnailFile() 生成缩略图文件 ✅ yes
JPEG 格式 ImageFormat.JPEG ✅ yes
PNG 格式 ImageFormat.PNG ✅ yes
WebP 格式 ImageFormat.WEBP ✅ yes
自定义尺寸 maxHeight / maxWidth ✅ yes
自定义质量 quality (0-100) ✅ yes
时间点选择 timeMs ✅ yes
HTTP 头 headers ✅ yes
本地视频 本地文件路径 ✅ yes
网络视频 URL 地址 ✅ yes (API>=20)

⚠️ 注意:网络视频需要 SDK API 版本 >= 20 才支持。如果 API 版本低于 20,会抛出错误 "Below API20 and an online url, thumbnail retrieval is not supported."


三、项目配置与安装

3.1 添加依赖配置

首先,需要在你的 Flutter 项目的 pubspec.yaml 文件中添加 video_thumbnail_ohos 依赖。

打开项目根目录下的 pubspec.yaml 文件,找到 dependencies 部分,添加以下配置:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter

  # 添加 video_thumbnail 依赖(OpenHarmony 适配版本)
  video_thumbnail_ohos:
    git:
      url: https://atomgit.com/openharmony-sig/fluttertpc_video_thumbnail.git
      path: ohos

配置说明:

  • 使用 git 方式引用开源鸿蒙适配的 fluttertpc_video_thumbnail 仓库
  • url:指定 AtomGit托管的仓库地址
  • path:指定 ohos 包的具体路径
  • 本插件直接使用 video_thumbnail_ohos 包,无需额外的 dev_dependency

⚠️ 重要 :对于 OpenHarmony 平台,直接使用 video_thumbnail_ohos 包即可,这是鸿蒙平台的原生实现。

3.2 下载依赖

配置完成后,需要在项目根目录执行以下命令下载依赖:

bash 复制代码
flutter pub get

执行成功后,你会看到类似以下的输出:

复制代码
Running "flutter pub get" in my_cross_platform_app...
Resolving dependencies...
Got dependencies!

3.3 权限配置

在 OpenHarmony 平台上,使用 video_thumbnail 需要配置读取媒体文件的权限。

ohos/entry/src/main/module.json5:

json 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_IMAGEVIDEO",
        "reason": "$string:read_media_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

ohos/entry/src/main/resources/base/element/string.json:

添加权限申请原因:

json 复制代码
{
  "string": [
    {
      "name": "read_media_reason",
      "value": "读取视频文件生成缩略图"
    }
  ]
}

3.4 权限等级说明

在 OpenHarmony 系统中,权限分为三个等级:

等级 说明
normal 普通权限,普通应用可直接申请
system_basic 系统基础权限,需要系统签名或特殊配置
system_core 系统核心权限,仅系统应用可使用

重要提示ohos.permission.READ_IMAGEVIDEO 属于 system_basic 级别权限,默认的应用权限等级是 normal。如果直接安装包含此权限的应用,会报错 9568289

3.5 如何使用 system_basic 权限

要使用 READ_IMAGEVIDEO 权限,需要修改应用的权限等级。请参考华为官方文档:安装 HAP 时提示 code 9568289

步骤 1:修改 Debug 签名模板

找到 SDK 目录下的签名模板文件:

复制代码
{SDK路径}/openharmony/toolchains/lib/UnsgnedDebugProfileTemplate.json

例如:d:\huawei\DevEco Studio\sdk\default\openharmony\toolchains\lib\UnsgnedDebugProfileTemplate.json

打开文件,修改以下内容:

1. 确保 APL 等级为 system_basic:

json 复制代码
"bundle-info": {
    ...
    "apl": "system_basic",
    ...
}

2. 在 acls 中添加允许的权限:

json 复制代码
"acls": {
    "allowed-acls": [
        "ohos.permission.READ_IMAGEVIDEO"
    ]
}

3. 在 permissions 中添加受限权限:

json 复制代码
"permissions": {
    "restricted-permissions": [
        "ohos.permission.READ_IMAGEVIDEO"
    ]
}

完整的修改后的文件示例:

json 复制代码
{
    "version-name": "2.0.0",
    "version-code": 2,
    "uuid": "fe686e1b-3770-4824-a938-961b140a7c98",
    "validity": {
        "not-before": 1610519532,
        "not-after": 1705127532
    },
    "type": "debug",
    "bundle-info": {
        "developer-id": "OpenHarmony",
        "development-certificate": "...",
        "bundle-name": "com.OpenHarmony.app.test",
        "apl": "system_basic",
        "app-feature": "hos_normal_app"
    },
    "acls": {
        "allowed-acls": [
            "ohos.permission.READ_IMAGEVIDEO"
        ]
    },
    "permissions": {
        "restricted-permissions": [
            "ohos.permission.READ_IMAGEVIDEO"
        ]
    },
    "debug-info": {
        "device-ids": [...],
        "device-id-type": "udid"
    },
    "issuer": "pki_internal"
}
步骤 2:修改 Release 签名模板(可选)

如果需要发布应用,同样修改 UnsgnedReleasedProfileTemplate.json 文件。

步骤 3:在 DevEco Studio 中重新签名
  1. 打开 DevEco Studio
  2. 点击 File > Project Structure > Project > Signing Configs
  3. 取消勾选 "Automatically generate signature"
  4. 重新勾选 "Automatically generate signature"
  5. 等待自动签名完成
  6. 点击 OK
步骤 4:重新运行应用

签名完成后,重新运行应用即可正常安装。

⚠️ 注意:修改签名模板后必须重新签名才能生效。如果仍然报错,请尝试清理项目后重新构建。


四、video_thumbnail 基础用法

4.1 导入库

在使用 video_thumbnail 之前,需要先导入库:

dart 复制代码
import 'dart:typed_data';
import 'package:video_thumbnail_ohos/video_thumbnail_ohos.dart';

4.2 图片格式枚举

video_thumbnail 支持三种输出格式:

dart 复制代码
enum ImageFormat { JPEG, PNG, WEBP }
格式 说明 特点
JPEG 有损压缩格式 文件小,适合照片类图像
PNG 无损压缩格式 支持透明,质量高
WebP 现代压缩格式 压缩率高,质量好

4.3 生成缩略图数据 - thumbnailData()

thumbnailData() 方法直接返回图片的二进制数据,适合直接在内存中使用:

dart 复制代码
Future<Uint8List?> _generateThumbnailData(String videoPath) async {
  final bytes = await VideoThumbnailOhos.thumbnailData(
    video: videoPath,
    imageFormat: ImageFormat.JPEG,
    maxHeight: 256,
    maxWidth: 256,
    timeMs: 0,
    quality: 50,
  );
  return bytes;
}

参数说明:

参数 类型 必填 说明
video String 视频文件路径或 URL
headers Map<String, String>? HTTP 请求头(网络视频用)
imageFormat ImageFormat 输出图片格式,默认 PNG
maxHeight int 最大高度,建议设置具体值
maxWidth int 最大宽度,建议设置具体值
timeMs int 提取时间点(毫秒),默认 0
quality int 图片质量(0-100),PNG 忽略此参数

⚠️ 注意maxHeightmaxWidth 建议设置具体值(如 256),设为 0 可能导致 FetchFrameByTime failed 错误。

4.4 生成缩略图文件 - thumbnailFile()

thumbnailFile() 方法将缩略图保存为文件,返回文件路径:

dart 复制代码
Future<String?> _generateThumbnailFile(String videoPath, String savePath) async {
  final thumbnailPath = await VideoThumbnailOhos.thumbnailFile(
    video: videoPath,
    thumbnailPath: savePath,
    imageFormat: ImageFormat.JPEG,
    maxHeight: 256,
    maxWidth: 256,
    timeMs: 0,
    quality: 50,
  );
  return thumbnailPath;
}

参数说明:

参数 类型 必填 说明
video String 视频文件路径或 URL
headers Map<String, String>? HTTP 请求头(网络视频用)
thumbnailPath String? 缩略图保存路径,null 则保存到视频同目录
imageFormat ImageFormat 输出图片格式,默认 PNG
maxHeight int 最大高度,建议设置具体值
maxWidth int 最大宽度,建议设置具体值
timeMs int 提取时间点(毫秒),默认 0
quality int 图片质量(0-100),PNG 忽略此参数

4.5 显示缩略图

生成缩略图数据后,可以直接使用 Image.memory() 显示:

dart 复制代码
Widget _buildThumbnail(Uint8List? thumbnailData) {
  if (thumbnailData == null) {
    return const Icon(Icons.video_library, size: 100);
  }
  return Image.memory(
    thumbnailData,
    fit: BoxFit.cover,
  );
}

五、完整示例:视频缩略图生成器

下面是一个完整的示例,展示如何实现视频缩略图生成功能:

dart 复制代码
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:video_thumbnail_ohos/video_thumbnail_ohos.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Video Thumbnail Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String? _videoPath;
  Uint8List? _thumbnailData;
  String? _thumbnailPath;
  bool _isLoading = false;
  String? _error;

  ImageFormat _format = ImageFormat.JPEG;
  int _quality = 50;
  int _maxHeight = 256;
  int _maxWidth = 256;
  int _timeMs = 0;

  Future<void> _pickVideo() async {
    final picker = ImagePicker();
    final video = await picker.pickVideo(source: ImageSource.gallery);

    if (video != null) {
      setState(() {
        _videoPath = video.path;
        _thumbnailData = null;
        _thumbnailPath = null;
        _error = null;
      });
    }
  }

  Future<void> _generateThumbnailData() async {
    if (_videoPath == null) return;

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final bytes = await VideoThumbnailOhos.thumbnailData(
        video: _videoPath!,
        imageFormat: _format,
        maxHeight: _maxHeight,
        maxWidth: _maxWidth,
        timeMs: _timeMs,
        quality: _quality,
      );

      setState(() {
        _thumbnailData = bytes;
        _thumbnailPath = null;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  Future<void> _generateThumbnailFile() async {
    if (_videoPath == null) return;

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final tempDir = await getTemporaryDirectory();
      final path = await VideoThumbnailOhos.thumbnailFile(
        video: _videoPath!,
        thumbnailPath: tempDir.path,
        imageFormat: _format,
        maxHeight: _maxHeight,
        maxWidth: _maxWidth,
        timeMs: _timeMs,
        quality: _quality,
      );

      setState(() {
        _thumbnailPath = path;
        _thumbnailData = null;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Video Thumbnail Demo'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 视频选择区域
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text('视频文件', style: TextStyle(fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                    if (_videoPath != null)
                      Text(_videoPath!, style: const TextStyle(fontSize: 12)),
                    const SizedBox(height: 8),
                    ElevatedButton.icon(
                      icon: const Icon(Icons.video_library),
                      label: const Text('选择视频'),
                      onPressed: _pickVideo,
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // 参数设置区域
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text('缩略图参数', style: TextStyle(fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                  
                    // 格式选择
                    Row(
                      children: [
                        const Text('格式: '),
                        DropdownButton<ImageFormat>(
                          value: _format,
                          items: const [
                            DropdownMenuItem(value: ImageFormat.JPEG, child: Text('JPEG')),
                            DropdownMenuItem(value: ImageFormat.PNG, child: Text('PNG')),
                            DropdownMenuItem(value: ImageFormat.WEBP, child: Text('WebP')),
                          ],
                          onChanged: (v) => setState(() => _format = v!),
                        ),
                      ],
                    ),
                  
                    // 质量滑块
                    Row(
                      children: [
                        const Text('质量: '),
                        Expanded(
                          child: Slider(
                            value: _quality.toDouble(),
                            min: 0,
                            max: 100,
                            divisions: 100,
                            label: '$_quality',
                            onChanged: (v) => setState(() => _quality = v.toInt()),
                          ),
                        ),
                        Text('$_quality'),
                      ],
                    ),
                  
                    // 时间点滑块
                    Row(
                      children: [
                        const Text('时间: '),
                        Expanded(
                          child: Slider(
                            value: _timeMs.toDouble(),
                            min: 0,
                            max: 10000,
                            divisions: 100,
                            label: '${_timeMs}ms',
                            onChanged: (v) => setState(() => _timeMs = v.toInt()),
                          ),
                        ),
                        Text('$_timeMs ms'),
                      ],
                    ),
                  
                    // 尺寸设置
                    Row(
                      children: [
                        const Text('尺寸: '),
                        Expanded(
                          child: Slider(
                            value: _maxWidth.toDouble(),
                            min: 0,
                            max: 500,
                            divisions: 50,
                            label: '$_maxWidth',
                            onChanged: (v) => setState(() {
                              _maxWidth = v.toInt();
                              _maxHeight = v.toInt();
                            }),
                          ),
                        ),
                        Text('$_maxWidth x $_maxHeight'),
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // 操作按钮
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton.icon(
                  icon: const Icon(Icons.memory),
                  label: const Text('生成数据'),
                  onPressed: _videoPath != null && !_isLoading ? _generateThumbnailData : null,
                ),
                ElevatedButton.icon(
                  icon: const Icon(Icons.save),
                  label: const Text('生成文件'),
                  onPressed: _videoPath != null && !_isLoading ? _generateThumbnailFile : null,
                ),
              ],
            ),
            const SizedBox(height: 16),

            // 结果显示区域
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text('缩略图预览', style: TextStyle(fontWeight: FontWeight.bold)),
                    const SizedBox(height: 8),
                    if (_isLoading)
                      const Center(child: CircularProgressIndicator())
                    else if (_error != null)
                      Text('错误: $_error', style: const TextStyle(color: Colors.red))
                    else if (_thumbnailData != null)
                      Column(
                        children: [
                          Image.memory(_thumbnailData!, fit: BoxFit.cover),
                          const SizedBox(height: 8),
                          Text('数据大小: ${_thumbnailData!.length} bytes'),
                        ],
                      )
                    else if (_thumbnailPath != null)
                      Column(
                        children: [
                          Image.file(File(_thumbnailPath!), fit: BoxFit.cover),
                          const SizedBox(height: 8),
                          Text('文件路径: $_thumbnailPath'),
                        ],
                      )
                    else
                      const Center(
                        child: Icon(Icons.image, size: 100, color: Colors.grey),
                      ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

六、高级用法

6.1 带进度指示的缩略图生成

使用 FutureBuilder 实现带进度指示的缩略图生成:

dart 复制代码
class ThumbnailGenerator extends StatelessWidget {
  final String videoPath;
  final int width;
  final int height;
  final int timeMs;

  const ThumbnailGenerator({
    Key? key,
    required this.videoPath,
    this.width = 200,
    this.height = 200,
    this.timeMs = 0,
  }) : super(key: key);

  Future<Uint8List?> _generateThumbnail() async {
    return await VideoThumbnailOhos.thumbnailData(
      video: videoPath,
      imageFormat: ImageFormat.JPEG,
      maxHeight: height,
      maxWidth: width,
      timeMs: timeMs,
      quality: 75,
    );
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Uint8List?>(
      future: _generateThumbnail(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          if (snapshot.hasData && snapshot.data != null) {
            return Image.memory(snapshot.data!, fit: BoxFit.cover);
          } else if (snapshot.hasError) {
            return const Icon(Icons.error, color: Colors.red);
          }
          return const Icon(Icons.video_library);
        }
        return const CircularProgressIndicator();
      },
    );
  }
}

6.2 批量生成缩略图

为视频列表批量生成缩略图:

dart 复制代码
Future<Map<String, Uint8List>> _generateBatchThumbnails(List<String> videoPaths) async {
  final Map<String, Uint8List> results = {};
  
  for (final path in videoPaths) {
    try {
      final bytes = await VideoThumbnailOhos.thumbnailData(
        video: path,
        imageFormat: ImageFormat.JPEG,
        maxHeight: 120,
        maxWidth: 160,
        quality: 60,
      );
      if (bytes != null) {
        results[path] = bytes;
      }
    } catch (e) {
      debugPrint('Failed to generate thumbnail for $path: $e');
    }
  }
  
  return results;
}

6.3 网络视频缩略图

⚠️ 注意:OpenHarmony 平台目前对网络视频生成缩略图存在已知问题,建议先将视频下载到本地再生成缩略图。

dart 复制代码
Future<Uint8List?> _generateNetworkThumbnail(String url) async {
  // 建议先下载视频到本地
  // 然后使用本地路径生成缩略图
  
  final bytes = await VideoThumbnailOhos.thumbnailData(
    video: url,
    headers: {
      'User-Agent': 'Mozilla/5.0',
      'Authorization': 'Bearer token',
    },
    imageFormat: ImageFormat.JPEG,
    maxHeight: 256,
    maxWidth: 256,
    quality: 50,
  );
  return bytes;
}

七、常见问题与解决方案

7.1 缩略图生成失败

问题:生成缩略图时返回 null 或抛出异常。

解决方案

  1. 检查视频文件路径是否正确
  2. 确保已配置 READ_IMAGEVIDEO 权限
  3. 检查视频格式是否支持
  4. 对于网络视频,建议先下载到本地

7.2 缩略图质量不佳

问题:生成的缩略图模糊或质量差。

解决方案

  • 提高输出尺寸(maxHeight/maxWidth)
  • 提高质量参数(quality)
  • 使用 PNG 格式(无损压缩)
dart 复制代码
final bytes = await VideoThumbnailOhos.thumbnailData(
  video: videoPath,
  imageFormat: ImageFormat.PNG,
  maxHeight: 400,
  maxWidth: 400,
  quality: 100,
);

7.3 内存占用过高

问题:批量生成缩略图时内存占用过高。

解决方案

  • 限制缩略图尺寸
  • 及时释放不需要的缩略图数据
  • 使用文件方式而非内存方式
dart 复制代码
// 使用文件方式减少内存占用
final path = await VideoThumbnailOhos.thumbnailFile(
  video: videoPath,
  thumbnailPath: tempDir,
  maxHeight: 120,
  maxWidth: 160,
  quality: 60,
);

八、最佳实践

8.1 缩略图尺寸建议

使用场景 建议尺寸 建议格式 建议质量
列表预览 120 x 160 JPEG 60
详情页预览 200 x 300 JPEG 75
全屏预览 400 x 600 JPEG/PNG 85
高清预览 原始尺寸 PNG 100

8.2 性能优化建议

  • 对于列表显示,使用较小的缩略图尺寸
  • 批量生成时考虑使用队列避免并发过高
  • 缓存已生成的缩略图避免重复生成
  • 使用文件方式存储缩略图便于后续使用

8.3 错误处理建议

dart 复制代码
Future<Uint8List?> safeGenerateThumbnail(String videoPath) async {
  try {
    if (!File(videoPath).existsSync()) {
      debugPrint('Video file not found: $videoPath');
      return null;
    }
  
    final bytes = await VideoThumbnailOhos.thumbnailData(
      video: videoPath,
      imageFormat: ImageFormat.JPEG,
      maxHeight: 256,
      maxWidth: 256,
      quality: 50,
    );
  
    return bytes;
  } on PlatformException catch (e) {
    debugPrint('Platform error: ${e.message}');
    return null;
  } catch (e) {
    debugPrint('Unknown error: $e');
    return null;
  }
}

九、总结

本文详细介绍了 Flutter for OpenHarmony 中 video_thumbnail 插件的使用方法,包括:

  • ✅ 插件的基本概念和特点
  • ✅ OpenHarmony 平台的适配说明
  • ✅ 依赖配置和权限设置
  • ✅ 基础用法和 API 详解
  • ✅ 完整的示例代码
  • ✅ 高级用法技巧
  • ✅ 常见问题与解决方案
  • ✅ 最佳实践建议

通过本文的学习,你应该能够在 Flutter for OpenHarmony 项目中熟练使用 video_thumbnail 插件实现视频缩略图生成功能。


📌 参考资源

相关推荐
aqi0013 小时前
FFmpeg开发笔记(一百零一)跨平台的开源音视频移动框架MobileFFmpeg
android·ffmpeg·音视频·直播·流媒体
小歆88414 小时前
音频分析仪推荐
音视频
菊风 Juphoon14 小时前
如何让车载通话从“能用”变“好用”?请看菊风智能车载音视频解决方案
音视频
互联网科技看点15 小时前
以标准立标杆,以技术赢口碑——园世赋能中国运动音频高质量发展
音视频
沉浸式学习ing15 小时前
播客和视频怎么变成知识库里的笔记?音视频转结构化笔记完整方案
人工智能·笔记·gpt·学习·ai·音视频·notion
沃普天科技15 小时前
USB显示器多屏异显多屏拼接IF8032 IT690 VL171 8801 RTD2556
arm开发·驱动开发·算法·计算机外设·音视频·硬件工程·pcb工艺
byte轻骑兵15 小时前
【LE Audio】CAP精讲[6]: 控制中枢操盘指南,Commander协同全流程拆解
人工智能·音视频·le audio·低功耗音频
KKei163815 小时前
Flutter for OpenHarmony 编程技能树APP技术文章
flutter·华为·harmonyos