React Native应用中读取音频歌手专辑封面等元数据

34 阅读8分钟

前言

我们在使用音乐播放软件时会发现很多歌曲有封面图片,专辑,歌名,歌手,音质等级,比如HQ、SQ、Hi...等信息展示,网络时代嘛,像专辑,封面歌名等信息很容易想到可能是播放器应用通过后端数据匹配得到的,但是如果是一首本地歌曲(音乐文件就在本机),它们是怎么知道音频的音质等级的的?而且我发现:我本地新增一个歌曲文件(文件名是字符串),在音乐播放器没有网络时,也能显示出封面,专辑歌手,音质,而且有些歌曲有这些信息,有些没有!那这应该就说明音乐应用在不依赖后端的情况下读取到文件中的信息,并呈现出来了!事实也确实如此,媒体文件(音频、视频、图片等)资源身上可能携带了元数据!比如常见的手机相机拍照可能带了经纬度信息,在发送图片时会提示移除定位信息,就是关于元数据的处理,关于元数据,在我的[react native中实现水印相机](react native中实现水印相机功能参考市面app应用的水印相机功能)中有提到,可以通过依赖库读取图片元数据,写入元数据。今天带大家简单编写功能代码,实现对音频文件的元数据读取!今天希望这篇文章能为大家揭开音频meta数据的神秘面纱!

微信图片_20260209082838_230_23.jpg 微信图片_20260209082839_231_23.jpg

核心依赖版本

依赖版本匹配性在rn应用中尤为重要,版本不匹配的异常问题很头疼,写出版本信息,供大家参考,今天的主角依赖库是:kroog-ffmpeg-kit-react-native!之前已有文章写 如何使用该依赖库提取视频文件中的音频部分为歌曲,它非常强大,在node端也有该依赖库,我也测试过在node端从视频中提取音频为独立文件。

    "kroog-ffmpeg-kit-react-native": "^6.0.9",
    "react": "19.1.0",
    "react-dom": "19.1.0",
    "react-native": "0.81.5",
    "expo": "~54.0.33"

元数据类型

元数据其实有很多,具体看需求想要获取什么,那么在音乐应用中我们很关心:

  • duration 歌曲时长number类型,通常是秒为单位
  • bit_rate 比特率,它是衡量音质的重要标准,比如HQ、SQ等,一般是依据它来划分的,值是数字
  • format_name 文件的格式,扩展名,当你使用音乐播放器依赖时吗,应该注意音频格式是否支持,不支持的歌曲无法播放
  • title/TITLE 歌曲名称
  • album/ALBUM 专辑信息,歌曲属于哪个专辑
  • artist/ARTIST 艺术家,歌曲演唱者,歌手
  • 歌曲封面图片,它不是字段读取的,是通过其他API读取的

哪些类型音频文件中包含我们需要的字段的元信息呢?通常.mp3、.flac格式的音频经过了特殊处理,写入了元数据, 其实我们也可以手动通过kroog-ffmpeg-kit-react-native依赖库为指定音频文件写入元数据。

读取封面图片

读取封面图片需要使用 kroog-ffmpeg-kit-react-native 依赖库的FFmpegKit类,执行命令 -i "${audioUri}" -an -vcodec copy -y "${coverPath}",下面是代码演示:

/**
 * 使用FFmpeg提取MP3音频文件的封面图片
 * @param audioUri 音频文件的URI路径
 * @param coverName 封面图片的名称(不带扩展名)
 * @returns 封面图片的完整URI路径,如果提取失败返回null
 */
export const extractMp3Cover = async (audioUri: string, coverName: string): Promise<string | null> => {
    if (!audioUri || !coverName.trim()) {
        return null;
    }

    try {
        // 创建cover目录
        const coverDir = new Directory(Paths.document, 'cover');
        if (!coverDir.exists) {
            coverDir.create({ intermediates: true });
        }

        // 构建输出路径
        const coverPath = `${Paths.document.uri}cover/${coverName}.jpg`;
        const coverFile = new File(coverPath);

        // 检查封面是否已存在
        if (coverFile.exists) {
            return coverPath;
        }

        // 使用FFmpeg提取封面图片
        // -an: 不处理音频
        // -vcodec copy: 直接复制视频流(封面)
        // -y: 覆盖已存在的文件
        const command = `-i "${audioUri}" -an -vcodec copy -y "${coverPath}"`;
        const session = await FFmpegKit.execute(command);
        const returnCode = await session.getReturnCode();

        if (returnCode.isValueSuccess()) {
            // 检查文件是否真的创建成功
            if (coverFile.exists) {
                return coverPath;
            } else {
                // 如果文件没有创建,说明没有嵌入封面
                return null;
            }
        } else {
            const output = await session.getOutput();
            console.warn('提取封面失败:', output);
            return null;
        }
    } catch (error) {
        console.error('提取MP3封面出错:', error);
        return null;
    }
};

核心就是提供音频文件路径,和文件将要存储的路径,调用FFmpegKit.execute执行命令,后是检查是否执行成功,是否存在封面文件,执行过程中,控制台会打印非常多的日志,是依赖库的行为,通过以上函数我们就可以得到封面图片,后续可以根据需要判断是否成功生成文件,如果没有生成使用默认图片。

获取其他字段信息

与上面不同的是,我们要使用kroog-ffmpeg-kit-react-native依赖库的另一个类:FFprobeKit执行 -v quiet -print_format json -show_streams -show_format "${filePath}"命令:

/**
 * 音频元信息接口
 */
export interface AudioMetadata {
    title?: string;
    artist?: string;
    album?: string;
    duration?: number;
    bitrate?: number;
    sampleRate?: number;
    channels?: number;
    codec?: string;
    format?: string;
    coverUrl?: string; // 封面URI
    quality: AudioQuality;
}

/**
 * 使用FFmpeg读取音频文件的元信息
 * @param audioUri 音频文件的URI路径
 * @param coverName 封面名字
 * @param extractCover 是否提取封面(默认为true)
 * @returns {Promise<<AudioMetadata | null>} 音频元信息,读取失败返回null
 */
export const getAudioMetadata = async (audioUri: string, coverName: string, extractCover: boolean = true): Promise<AudioMetadata | null> => {
    if (!audioUri) {
        return null;
    }

    try {
        // 转换文件路径:file:/// -> file://
        let filePath = audioUri;
        if (filePath.startsWith('file:///')) {
            filePath = filePath.replace('file:///', 'file:/');
        }

        // 使用FFprobe获取音频元信息
        // -v quiet: 减少输出信息
        // -print_format json: 以JSON格式输出
        // -show_streams: 显示流信息
        // -show_format: 显示格式信息
        const command = `-v quiet -print_format json -show_streams -show_format "${filePath}"`;
        const session = await FFprobeKit.execute(command);
        const returnCode = await session.getReturnCode();

        if (!returnCode.isValueSuccess()) {
            const output = await session.getOutput();
            console.warn('获取音频元信息失败:', output);
            return null;
        }

        const output = await session.getOutput();

        // 检查输出是否为空
        if (!output || output.trim() === '') {
            console.warn('FFprobe输出为空');
            return null;
        }

        const data = JSON.parse(output);

        // 查找音频流
        const audioStream = data.streams?.find((stream: any) => stream.codec_type === 'audio');
        const format = data.format;

        if (!audioStream && !format) {
            console.warn('未找到音频流或格式信息');
            return null;
        }

        const { tags, duration = 0, bit_rate = 0, format_name = '' } = format ?? {};
        const { title, artist, album, TITLE, ALBUM, ARTIST } = tags ?? {};

        // 提取封面(如果需要)
        let coverUrl: string | null = null;
        if (extractCover) {
            // 使用文件名作为封面名称(去除扩展名)
            coverUrl = await extractMp3Cover(audioUri, coverName);
        }
        const bitrate = bit_rate ? parseInt(format.bit_rate) / 1000 : undefined;
        const quality = getAudioQuality(bitrate);
        // 解析元数据
        const metadata: AudioMetadata = {
            title: TITLE ?? title ?? audioStream?.tags?.title,
            artist: ARTIST ?? artist ?? audioStream?.tags?.artist,
            album: ALBUM ?? album ?? audioStream?.tags?.album,
            duration: duration ? parseFloat(format.duration) : undefined,
            bitrate, // 转换为kbps
            sampleRate: audioStream?.sample_rate ? parseInt(audioStream.sample_rate) : undefined,
            channels: audioStream?.channels,
            codec: audioStream?.codec_name,
            format: format_name,
            coverUrl: coverUrl ?? undefined,
            quality
        };

        return metadata;
    } catch (error) {
        console.error('读取音频元信息出错:', error);
        return null;
    }
};

title/TITLE、album/ALBUM、artist/ARTIST、duration、bit_rate这些是通用字段,通常是这些,不排除不是这些,因为上面也提到了使用kroog-ffmpeg-kit-react-native自定义写入元数据内容,而且这些数据,尤其是title,artist,album的文字格式可能不是utf-8,读取后可能是乱码,这个我没有特殊处理,目前没有好的方案解决,因为我将读取的歌曲数据最终存入sqlite数据库表中,通过修改功能可以调整展示的内容,也不修改元数据,这种情况的歌曲我发现过,已经改了没有截图保存。

音质等级

核心是依据上面函数得到比特率bitrate,通常是使用kbps,因此做一下简单的数值处理:const bitrate = bit_rate ? parseInt(format.bit_rate) / 1000 : undefined;关于音质等级如何根据该数值划分,我简单查了一下,一般3201000以上是SQ,192320是HQ,1000以上有什么Hi-Res什么的类型,感兴趣可以去查一下,比如电话音质,MP3,CD,无损(通常是FLAC/WAV格式)。其实比特率虽然是关键数据,但是其实还有其他的更多标准衡量音质,这些有专业的或者发烧友给科普一下吗?

音质等级判断

这就很简单了,上面说到,咱们就依据bitrate比特率值判定音质等级。在应用开发时我将SQ的判定值设置为1000,然后有一首歌曲得到的质量等级是HQ,但是我发现同一首歌在手机自带音乐播放器上显示的等级是SQ,这么说就是1000的值给高了?因此我将SQ的判定值为>=900,然后我的应用中该歌曲也显示为了SQ!(技术人就是牛逼啊,音质不够,判断来凑,我自己都感觉666)。无所谓了,上面也简单说了,具体音质判定中bitrate并不是唯一标准,咱就图一乐,但是可以确定的是.flac格式的音频文件体积通常是.mp3的几倍,想必音质等级就是高吧。下面的判断函数就非常简单了:

type AudioQuality = 'SQ' | 'HQ' | 'MQ' | 'PQ';

/**
 * 获取音频质量等级
 * @param bitrate 比特率 (kbps)
 * @returns 质量等级标识
 */
export const getAudioQuality = (bitrate?: number): AudioQuality => {
    if (!bitrate) {
        return 'PQ';
    }
    if (bitrate >= 900) {
        return 'SQ'; // 超高品质(无损)
    } else if (bitrate >= 320) {
        return 'HQ'; // 高音质
    } else if (bitrate >= 192) {
        return 'MQ'; // 中等音质
    } else {
        return 'PQ'; // 标准音质
    }
};

PQ是我自封的啊,primary quality,起初我定义为BQ basic quality,MQ好像是有,就算是medium quality, 其余是通用的HQ、SQ,再高就没有划分,而且我发现我测试了很多歌曲,没有显示MQ的,我也不太清楚是不是192~320区间的歌曲比较少还是怎么回事,MQ的倒是挺多,现在应用中调整为PQ等级的不显示音质标识。具体的应用代码就非常简单,TS定义也有,如果想要详细了解,或者也想自己搓一个音乐播放器应用可以尝试一下 整个项目已经成型,我已经打包成apk使用了,bug肯定有,iOS因为没有苹果设备没有做精细适配调试,后续会更新音乐播放核心依赖库选择讲解,音乐播放器通知栏控件,锁屏控件相关的技术分享,图片我尺寸调整的比较小,具体可以看文档中的图片

最后附上项目地址,欢迎star交流建议

[项目地址](expo rn: expo创建的react native的音乐播放器应用,专注视频转歌和本地歌曲播放)