前言
音视频文件在容器层面由一个或多个流(stream)组成,每个流拥有独立的编码格式、参数配置和元数据。了解这些流的详细信息对于后续处理决策至关重要——比如判断音频是否需要重采样、视频帧率是否匹配、色彩空间是否正确等。AudioTools 的媒体流详情功能通过 FFmpeg 的媒体探测能力,完整解析容器中每个流的编码参数、时长、disposition 标记和元数据,以卡片形式清晰展示。
功能流程
用户导入文件 → 主页卡片点击 → parseMediaInfo() ↓ MediaInfoParser::parse() ↓ avformat_open_input + avformat_find_stream_info ↓ 遍历 formatContext->nb_streams ├── 通用字段:codec、profile、level、time_base、duration、disposition ├── 音频流字段:sample_rate、channels、sample_format、bits_per_sample、frame_count └── 视频流字段:width、height、pixel_format、frame_rate、nb_frames、color_space ↓ setMediaInfo(info) → mediaInfo 属性更新 ↓ StreamDetailPage 读取 mediaInfo.streams 数组 ↓ 逐流渲染详情卡片 + 元数据列表关键特点:
- 按需解析:只在用户点击基础属性/元数据功能入口时调用
parseMediaInfo(),避免导入文件时产生不必要的耗时操作。 - 一次性解析:基础属性、元数据、流详情共用同一次
MediaInfoParser::parse()调用结果。 - PCM 保留:如果当前已解码过 PCM,重新解析媒体信息时会保留
mediaInfo["pcm"]子字段,避免解码页状态丢失。
核心算法
容器级信息
MediaInfoParser::parse()通过avformat_open_input打开容器,avformat_find_stream_info探测流信息后,首先提取容器级字段:
result.insert("fileName",fileInfo.fileName());result.insert("fileSizeText",formatBytes(fileInfo.size()));result.insert("durationMs",durationMs);// AV_TIME_BASE → msresult.insert("durationText",formatDuration(durationMs));result.insert("formatName",formatContext->iformat->name);// "mov,mp4,m4a,3gp,3g2,mj2"result.insert("formatLongName",formatContext->iformat->long_name);// "QuickTime / MOV"result.insert("bitRate",formatContext->bit_rate);result.insert("metadata",dictionaryToMap(formatContext->metadata));通用增强字段
每个流在基础的codec和bitRate之外,还解析了以下增强字段:
Profile 和 Level:直接从AVCodecParameters读取数值:
streamInfo.insert("profile",stream->codecpar->profile>0?QString::number(stream->codecpar->profile):"-");streamInfo.insert("level",stream->codecpar->level!=FF_LEVEL_UNKNOWN?QString::number(stream->codecpar->level):"-");Time Base:流的时间基准,反映时间戳精度:
streamInfo.insert("timeBase",QString("%1/%2").arg(stream->time_base.num).arg(stream->time_base.den));流时长:从流级 duration 换算为毫秒(基于 time_base):
constqint64 streamDurationMs=stream->duration!=AV_NOPTS_VALUE?stream->duration*av_q2d(stream->time_base)*1000.0:-1;起始时间:某些容器流存在非零起始时间(如 TS 文件):
constqint64 startTimeMs=stream->start_time!=AV_NOPTS_VALUE?stream->start_time*av_q2d(stream->time_base)*1000.0:0;Codec Tag:四字符编码标签(hex 格式),用于精确识别编码变体:
streamInfo.insert("codecTag",QString("0x%1").arg(params->codec_tag,4,16,QLatin1Char('0')));Disposition 标记:流的特殊属性标志位,包括default、dub、lyrics、comment、hearing_impaired、visual_impaired等:
QStringList dispFlags;if(stream->disposition&AV_DISPOSITION_DEFAULT)dispFlags<<"default";if(stream->disposition&AV_DISPOSITION_DUB)dispFlags<<"dub";if(stream->disposition&AV_DISPOSITION_LYRICS)dispFlags<<"lyrics";// ... 更多标志位streamInfo.insert("disposition",dispFlags.join(", "));音频流字段
音频流在通用字段基础上补充以下参数:
streamInfo.insert("type","audio");streamInfo.insert("channels",params->ch_layout.nb_channels);streamInfo.insert("channelLayout",channelLayoutName(params->ch_layout));streamInfo.insert("sampleRate",params->sample_rate);streamInfo.insert("sampleFormat",sampleFormatName(params->format));streamInfo.insert("bitsPerSample",bitsPerSample(params));帧数估算:基于流时长和采样率推算总采样帧数:
if(streamDurationMs>0&¶ms->sample_rate>0)streamInfo.insert("frameCount",streamDurationMs*params->sample_rate/1000.0);第一个音频流的关键参数会被"提升"到容器顶层,供主页卡片和 PCM 解码快速读取:
if(!result.contains("codec"))result.insert("codec",streamInfo.value("codec"));if(!result.contains("channels"))result.insert("channels",streamInfo.value("channels"));if(!result.contains("sampleRate"))result.insert("sampleRate",streamInfo.value("sampleRate"));// ... sampleFormat, bitsPerSample 同理视频流字段
视频流补充分辨率、像素格式、帧率和色彩信息:
streamInfo.insert("type","video");streamInfo.insert("width",params->width);streamInfo.insert("height",params->height);streamInfo.insert("pixelFormat",av_get_pix_fmt_name(params->format));streamInfo.insert("frameRate",av_q2d(stream->avg_frame_rate));// 增强字段if(stream->nb_frames>0)streamInfo.insert("nbFrames",stream->nb_frames);streamInfo.insert("colorSpace",QString::number(params->color_space));streamInfo.insert("colorRange",params->color_range==AVCOL_RANGE_JPEG?"JPEG/Full":(params->color_range==AVCOL_RANGE_MPEG?"MPEG/Limited":"-"));// Profile 名称(需要 codec descriptor)constchar*profileName=avcodec_profile_name(params->codec_id,stream->codecpar->profile);streamInfo.insert("profileName",profileName?profileName:"-");数据结构
parse()返回的QVariantMap结构:
mediaInfo ├── fileName, fileSizeText, durationText ├── formatName, formatLongName, bitRate ├── metadata: { title, artist, album, ... } ├── streamCount, audioStreamCount, videoStreamCount ├── codec, channels, sampleRate, ... (第一个音频流的提升字段) ├── streams: [ stream0, stream1, ... ] │ ├── 通用: index, codec, codecLongName, bitRate, profile, level, │ │ timeBase, durationText, startTimeMs, codecTag, disposition, metadata │ ├── 音频: type=audio, channels, channelLayout, sampleRate, sampleFormat, │ │ bitsPerSample, frameCount │ └── 视频: type=video, width, height, pixelFormat, frameRate, nbFrames, │ colorSpace, colorRange, profileName ├── audioStreams: [ audioStream0, ... ] └── videoStreams: [ videoStream0, ... ]主处理函数
MediaInfoParser::parse
位于src/media/mediainfoparser.cpp,无状态类,只负责读取文件描述信息:
boolMediaInfoParser::parse(constQString&filePath,QVariantMap*info,QString*errorText)const处理步骤:
- 文件校验:
QFileInfo检查文件存在性和类型。 - 打开容器:
avformat_open_input创建AVFormatContext。 - 探测流信息:
avformat_find_stream_info填充流参数。 - 提取文件级信息:文件名、大小、创建/修改时间、时长、格式。
- 遍历流列表:按类型提取音频/视频/其它流的参数和增强字段。
- 组装输出:
streams、audioStreams、videoStreams三个列表 + 顶层常用字段。 - 关闭容器:
avformat_close_input释放资源。
注意:该类不解码音频数据,只读取描述信息,因此执行速度很快。
导出封装
MediaAnalyzer::parseMediaInfo
QML 调用入口,调用MediaInfoParser::parse并更新mediaInfo属性:
boolMediaAnalyzer::parseMediaInfo(constQUrl&fileUrl){constQString filePath=toLocalFile(fileUrl);setBusy(true);setCurrentFile(filePath);constboolok=m_infoParser.parse(filePath,&info,&errorText);if(ok){// 保留已有的 PCM 子字段,避免解码页状态丢失constQVariantMap pcmInfo=m_mediaInfo.value("pcm").toMap();if(!pcmInfo.isEmpty())info.insert("pcm",pcmInfo);setMediaInfo(info);setStatus("媒体属性解析完成");}setBusy(false);returnok;}默认字段保护
文件可能没有音频流(如纯视频文件),顶层常用字段补默认值避免 QML 侧出现undefined:
if(!result.contains("codec"))result.insert("codec","-");if(!result.contains("channels"))result.insert("channels","-");if(!result.contains("sampleRate"))result.insert("sampleRate","-");// ... sampleFormat, bitsPerSample 同理UI 展示
StreamDetailPage
StreamDetailPage读取mediaAnalyzer.mediaInfo.streams数组,为每个流渲染一个详情卡片:
容器概览区:展示总流数、音频流数、视频流数和封装格式。
逐流卡片:
- 卡片头部:流索引(
#0)、类型标签(AUDIO/VIDEO,颜色区分)、编码全称。 - 详情网格:
buildStreamRows()根据流类型动态生成字段列表,音频流展示采样率/通道/位深等,视频流展示分辨率/像素格式/帧率等。 - 元数据区:从
stream.metadata中读取所有键值对,按 key 排序展示。
响应式布局:详情网格在宽度 ≥600 时双列,否则单列:
GridLayout{columns:width>=600?2:1columnSpacing:24rowSpacing:4// ...}空状态处理:如果streams为空或不存在,显示"请先在基础属性页解析媒体信息"的提示。
状态清理
媒体流详情数据存储在mediaInfo属性中,清理时机:
| 时机 | 行为 |
|---|---|
openFile()导入新文件 | setMediaInfo(QVariantMap())完全清空 |
parseMediaInfo()重新解析 | 整体替换为新的解析结果 |
clear()清空全部 | setMediaInfo(QVariantMap()) |
与 PCM 相关的功能(重采样、降噪等)不会影响mediaInfo,因为媒体信息是容器级别的描述,和当前 PCM 数据无关。
小结
媒体流详情功能是媒体信息解析的可视化展示层:
- 算法层:
MediaInfoParser::parse通过 FFmpegavformatAPI 读取容器和流描述,不执行音频解码,执行速度快。增强字段覆盖了 profile、level、time_base、disposition、色彩空间和帧数等高级属性。 - 状态层:
MediaAnalyzer将解析结果存入mediaInfo属性,重新解析时保留已有的 PCM 子字段,基础属性和流详情页面共用同一次解析结果。 - UI 层:
StreamDetailPage以流卡片形式展示每个流的详情,支持音频/视频差异化字段和元数据折叠展示,响应式布局适配不同窗口宽度。 - 按需解析:只在用户主动触发时解析,避免导入文件时产生不必要的 I/O 开销。