如何在Flutter中通过Dart FFI结合TagLib读取多格式音频元数据?
Flutter跨平台音乐播放器音频元数据解决方案(基于TagLib+FFI)
一、编译TagLib为共享库
先把TagLib编译成各平台的动态库(Windows:dll、Linux:so、macOS:dylib),用命令行替代CMake GUI更高效:
- Windows:
- 下载TagLib源码解压,新建
build目录并进入 - 执行CMake配置:
cmake -G "Visual Studio 17 2022" -A x64 -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=../install .. - 编译安装:
cmake --build . --config Release --target install
完成后install目录会生成动态库和头文件
- 下载TagLib源码解压,新建
- Linux/macOS:
- 源码解压后,执行:
mkdir build && cd build - 配置CMake:
cmake -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=../install .. - 编译安装:
make && make install
- 源码解压后,执行:
二、编写C++包装函数(适配FFI)
Dart FFI无法直接操作C++对象,需写C风格的包装函数,封装TagLib的核心操作:
#include <taglib/fileref.h> #include <taglib/tag.h> #include <taglib/tpropertymap.h> #include <cstring> extern "C" { // 封装FileRef对象为void*,避免暴露C++类型 void* file_ref_open(const char* path) { try { return new TagLib::FileRef(path); } catch (...) { return nullptr; } } void file_ref_close(void* ref) { delete static_cast<TagLib::FileRef*>(ref); } // 获取标题 const char* get_title(void* ref) { auto fileRef = static_cast<TagLib::FileRef*>(ref); if (!fileRef || !fileRef->file()) return nullptr; static char buffer[256]; strncpy(buffer, fileRef->tag()->title().toCString(), sizeof(buffer)-1); buffer[sizeof(buffer)-1] = '\0'; return buffer; } // 获取流派 const char* get_genre(void* ref) { auto fileRef = static_cast<TagLib::FileRef*>(ref); if (!fileRef || !fileRef->file()) return nullptr; static char buffer[256]; strncpy(buffer, fileRef->tag()->genre().toCString(), sizeof(buffer)-1); buffer[sizeof(buffer)-1] = '\0'; return buffer; } // 获取自定义元数据(情绪、语言、评分等) const char* get_property(void* ref, const char* key) { auto fileRef = static_cast<TagLib::FileRef*>(ref); if (!fileRef || !fileRef->file()) return nullptr; TagLib::PropertyMap props = fileRef->file()->properties(); TagLib::StringList values = props[key]; if (values.isEmpty()) return nullptr; static char buffer[256]; strncpy(buffer, values.front().toCString(), sizeof(buffer)-1); buffer[sizeof(buffer)-1] = '\0'; return buffer; } }
编译包装函数为共享库:
- Windows:
cl /LD wrapper.cpp /I ../install/include /link ../install/lib/tag.lib - Linux:
g++ -shared -fPIC wrapper.cpp -o libtagwrapper.so -I ../install/include -L ../install/lib -ltag - macOS:
clang++ -shared -fPIC wrapper.cpp -o libtagwrapper.dylib -I ../install/include -L ../install/lib -ltag
三、Dart FFI绑定代码示例
在Flutter项目中编写FFI调用逻辑:
import 'dart:ffi'; import 'dart:io'; import 'package:ffi/ffi.dart'; // 定义C函数签名 typedef FileRefOpenC = Pointer<Void> Function(Pointer<Utf8> path); typedef FileRefOpen = Pointer<Void> Function(Pointer<Utf8>); typedef FileRefCloseC = Void Function(Pointer<Void> ref); typedef FileRefClose = void Function(Pointer<Void>); typedef GetStringC = Pointer<Utf8> Function(Pointer<Void> ref); typedef GetString = Pointer<Utf8> Function(Pointer<Void>); typedef GetPropertyC = Pointer<Utf8> Function(Pointer<Void> ref, Pointer<Utf8> key); typedef GetProperty = Pointer<Utf8> Function(Pointer<Void>, Pointer<Utf8>); class TagLibWrapper { late final DynamicLibrary _lib; late final FileRefOpen _fileRefOpen; late final FileRefClose _fileRefClose; late final GetString _getTitle; late final GetString _getGenre; late final GetProperty _getProperty; TagLibWrapper() { // 加载对应平台的动态库 if (Platform.isWindows) { _lib = DynamicLibrary.open('tagwrapper.dll'); } else if (Platform.isLinux) { _lib = DynamicLibrary.open('libtagwrapper.so'); } else if (Platform.isMacOS) { _lib = DynamicLibrary.open('libtagwrapper.dylib'); } else { throw UnsupportedError('当前平台不支持'); } // 绑定C函数 _fileRefOpen = _lib.lookupFunction<FileRefOpenC, FileRefOpen>('file_ref_open'); _fileRefClose = _lib.lookupFunction<FileRefCloseC, FileRefClose>('file_ref_close'); _getTitle = _lib.lookupFunction<GetStringC, GetString>('get_title'); _getGenre = _lib.lookupFunction<GetStringC, GetString>('get_genre'); _getProperty = _lib.lookupFunction<GetPropertyC, GetProperty>('get_property'); } // 封装元数据读取方法 Map<String, String?> readMetadata(String filePath) { final pathUtf8 = filePath.toNativeUtf8(); final ref = _fileRefOpen(pathUtf8); calloc.free(pathUtf8); if (ref == nullptr) return {}; final title = _getTitle(ref).toDartString(); final genre = _getGenre(ref).toDartString(); final mood = _getProperty(ref, 'MOOD'.toNativeUtf8()).toDartString(); final language = _getProperty(ref, 'LANGUAGE'.toNativeUtf8()).toDartString(); final rating = _getProperty(ref, 'RATING'.toNativeUtf8()).toDartString(); _fileRefClose(ref); return { 'title': title, 'genre': genre, 'mood': mood, 'language': language, 'rating': rating, }; } } // 扩展方法简化指针转字符串 extension Utf8Pointer on Pointer<Utf8> { String? toDartString() { if (this == nullptr) return null; return cast<Utf8>().toDartString(); } }
使用示例:
void main() { final wrapper = TagLibWrapper(); final metadata = wrapper.readMetadata('/path/to/your/audio/file.mp3'); print(metadata); }
四、替代方案
如果编译TagLib过于繁琐,可尝试:
- 查找现成Flutter插件:比如
audio_metadata,但需确认是否支持所需格式和元数据字段 - 原生通道(Method Channel):Android用
MediaMetadataRetriever、iOS用AVFoundation分别实现元数据读取,通过通道传给Dart,但部分自定义字段(如情绪、评分)可能无法覆盖,不如TagLib全面
内容的提问来源于stack exchange,提问作者NattyPluz
相关产品推荐
相关产品推荐

