Qt + FFmpeg 音频工具:媒体流详情实现

发布时间:2026/7/10 23:23:52
Qt + FFmpeg 音频工具:媒体流详情实现 前言音视频文件在容器层面由一个或多个流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,mj2result.insert(formatLongName,formatContext-iformat-long_name);// QuickTime / MOVresult.insert(bitRate,formatContext-bit_rate);result.insert(metadata,dictionaryToMap(formatContext-metadata));通用增强字段每个流在基础的codec和bitRate之外还解析了以下增强字段Profile 和 Level直接从AVCodecParameters读取数值streamInfo.insert(profile,stream-codecpar-profile0?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_baseconstqint64 streamDurationMsstream-duration!AV_NOPTS_VALUE?stream-duration*av_q2d(stream-time_base)*1000.0:-1;起始时间某些容器流存在非零起始时间如 TS 文件constqint64 startTimeMsstream-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-dispositionAV_DISPOSITION_DEFAULT)dispFlagsdefault;if(stream-dispositionAV_DISPOSITION_DUB)dispFlagsdub;if(stream-dispositionAV_DISPOSITION_LYRICS)dispFlagslyrics;// ... 更多标志位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(streamDurationMs0params-sample_rate0)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_frames0)streamInfo.insert(nbFrames,stream-nb_frames);streamInfo.insert(colorSpace,QString::number(params-color_space));streamInfo.insert(colorRange,params-color_rangeAVCOL_RANGE_JPEG?JPEG/Full:(params-color_rangeAVCOL_RANGE_MPEG?MPEG/Limited:-));// Profile 名称需要 codec descriptorconstchar*profileNameavcodec_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 │ ├── 音频: typeaudio, channels, channelLayout, sampleRate, sampleFormat, │ │ bitsPerSample, frameCount │ └── 视频: typevideo, width, height, pixelFormat, frameRate, nbFrames, │ colorSpace, colorRange, profileName ├── audioStreams: [ audioStream0, ... ] └── videoStreams: [ videoStream0, ... ]主处理函数MediaInfoParser::parse位于src/media/mediainfoparser.cpp无状态类只负责读取文件描述信息boolMediaInfoParser::parse(constQStringfilePath,QVariantMap*info,QString*errorText)const处理步骤文件校验QFileInfo检查文件存在性和类型。打开容器avformat_open_input创建AVFormatContext。探测流信息avformat_find_stream_info填充流参数。提取文件级信息文件名、大小、创建/修改时间、时长、格式。遍历流列表按类型提取音频/视频/其它流的参数和增强字段。组装输出streams、audioStreams、videoStreams三个列表 顶层常用字段。关闭容器avformat_close_input释放资源。注意该类不解码音频数据只读取描述信息因此执行速度很快。导出封装MediaAnalyzer::parseMediaInfoQML 调用入口调用MediaInfoParser::parse并更新mediaInfo属性boolMediaAnalyzer::parseMediaInfo(constQUrlfileUrl){constQString filePathtoLocalFile(fileUrl);setBusy(true);setCurrentFile(filePath);constboolokm_infoParser.parse(filePath,info,errorText);if(ok){// 保留已有的 PCM 子字段避免解码页状态丢失constQVariantMap pcmInfom_mediaInfo.value(pcm).toMap();if(!pcmInfo.isEmpty())info.insert(pcm,pcmInfo);setMediaInfo(info);setStatus(媒体属性解析完成);}setBusy(false);returnok;}默认字段保护文件可能没有音频流如纯视频文件顶层常用字段补默认值避免 QML 侧出现undefinedif(!result.contains(codec))result.insert(codec,-);if(!result.contains(channels))result.insert(channels,-);if(!result.contains(sampleRate))result.insert(sampleRate,-);// ... sampleFormat, bitsPerSample 同理UI 展示StreamDetailPageStreamDetailPage读取mediaAnalyzer.mediaInfo.streams数组为每个流渲染一个详情卡片容器概览区展示总流数、音频流数、视频流数和封装格式。逐流卡片卡片头部流索引#0、类型标签AUDIO/VIDEO颜色区分、编码全称。详情网格buildStreamRows()根据流类型动态生成字段列表音频流展示采样率/通道/位深等视频流展示分辨率/像素格式/帧率等。元数据区从stream.metadata中读取所有键值对按 key 排序展示。响应式布局详情网格在宽度 ≥600 时双列否则单列GridLayout{columns:width600?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 开销。