找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1012

积分

0

好友

141

主题
发表于 前天 09:42 | 查看: 4| 回复: 0

在之前的实现中,我们已经能够获取TS文件中的不同音视频轨道信息,并指定索引进行播放。但这仍不完整,例如,在VLC播放器中可以看到直接切换“节目”的选项,而非切换轨道索引。每个节目都包含其专属的音视频轨道,且可能有多个。例如,节目1可能包含一个视频轨道和两个音频轨道(如中英文音轨)。之前未考虑此结构,导致播放某些文件时出现音画不同步的问题,即画面是节目1的,而声音却是节目2的。

通过查阅资料发现,FFmpeg本身提供了接口用于获取节目信息,包括中文节目名称等。回顾之前获取流信息的代码,nb_streams表示流的总数,同时还有nb_programsnb_chapters。这里的nbnumber的缩写,nb_programs即表示节目数量。我们可以根据此数量逐个取出对应的AVProgram对象,以获取详细信息。

需要特别注意的是,AVProgram对象包含一个stream_index数组,其中存储了当前节目所包含的流在全局AVFormatContext对象流数组中的索引。我们需要遍历此数组,判断每个索引对应的流是音频、视频还是其他类型的数据轨道,并过滤掉不需要的类型。

当获得了每个节目对应的音视频轨道索引后,切换节目就变得简单了。AVFormatContext对象通过av_read_frame读取数据时,会读出所有流的数据包。用户只需根据需要选择解码哪些数据包即可。因此,切换节目时,只需动态设置当前需要解码的流索引,并在处理每个数据包(AVPacket)时,判断其stream_index是否属于当前选中节目的音视频轨道索引集合。实践表明,此方法效果极佳,能够实现节目的瞬间实时切换。

TS流节目切换效果图
多节目信息展示效果图

核心功能特点

该播放器基于Qt跨平台应用框架与FFmpeg构建,具备以下与TS流处理相关的关键特性:

  1. 多解码内核支持:除FFmpeg内核外,还支持VLC、mpv、mdk等多种内核,方便在不同场景下选择最优方案。
  2. 动态节目/轨道切换:可完整读取TS文件的节目信息,支持在播放前或播放过程中动态切换节目及音视频轨道。
  3. 智能流识别与管理:自动识别视频旋转角度、分辨率变化,支持共享解码线程以节省网络流量。
  4. 高性能与低延迟:解码与渲染分离,采用高效的FFmpeg多媒体处理框架进行纯解码,结合QOpenGLWidget进行GPU加速渲染,实现极低延迟播放。
  5. 强大的兼容性:支持H.264/H.265编码,兼容多种音频格式,并可在Windows、Linux、macOS、Android及多种国产嵌入式Linux系统上运行。

关键代码实现

以下代码展示了如何从AVFormatContext中提取节目信息,以及如何根据音视频轨道列表智能组合节目信息。

// 节目信息结构体
struct ProgramInfo {
    int id;
    QString name;
    QString provider;
    QList<int> audioTracks;
    QList<int> videoTracks;

    // 重载打印输出格式
    friend QDebug operator<<(QDebug debug, const ProgramInfo &programInfo) {
        QStringList list;
        list << QString("节目编号: %1").arg(programInfo.id);
        list << QString("节目名称: %1").arg(programInfo.name);
        list << QString("节目发布: %1").arg(programInfo.provider);
        QStringList audios;
        foreach (int track, programInfo.audioTracks) {
            audios << QString::number(track);
        }
        QStringList videos;
        foreach (int track, programInfo.videoTracks) {
            videos << QString::number(track);
        }
        list << QString("音频索引: %1").arg(audios.join("|"));
        list << QString("视频索引: %1").arg(videos.join("|"));
        debug.noquote() << list.join(" ");
        return debug;
    }
};

// 获取节目信息
QList<ProgramInfo> FFmpegThreadHelper::readProgramInfo(AVFormatContext *formatCtx, QList<int> &audioTracks, QList<int> &videoTracks) {
    audioTracks.clear();
    videoTracks.clear();
    QList<ProgramInfo> programs;

    // 获取全局音视频索引集合
    for (int i = 0; i < formatCtx->nb_streams; ++i) {
        AVMediaType type = FFmpegHelper::getMediaType(formatCtx->streams[i]);
        if (type == AVMEDIA_TYPE_AUDIO) {
            audioTracks << i;
        } else if (type == AVMEDIA_TYPE_VIDEO) {
            videoTracks << i;
        }
    }

    // 特殊情况处理:当文件仅声明有1个节目,但实际发现多个视频流时,尝试智能组合
    if (formatCtx->nb_programs == 1 && videoTracks.count() > 1) {
        TrackHelper::getProgram(programs, audioTracks, videoTracks);
        return programs;
    }

    // 标准流程:遍历 nb_programs 获取节目信息
    for (int i = 0; i < formatCtx->nb_programs; ++i) {
        ProgramInfo program;
        AVProgram *avprogram = formatCtx->programs[i];
        program.id = avprogram->id;

        // 读取元数据,如节目名称和服务提供商
        AVDictionaryEntry *tag = NULL;
        while ((tag = av_dict_get(avprogram->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) {
            QString key = tag->key;
            QString value = tag->value;
            if (key == "service_name") {
                program.name = value;
            } else if (key == "service_provider") {
                program.provider = value;
            }
        }

        // 获取该节目下的所有流索引,并分类为音视频轨道
        int number = avprogram->nb_stream_indexes;
        for (int j = 0; j < number; ++j) {
            int index = avprogram->stream_index[j];
            AVMediaType type = FFmpegHelper::getMediaType(formatCtx->streams[index]);
            if (type == AVMEDIA_TYPE_AUDIO) {
                program.audioTracks << index;
            } else if (type == AVMEDIA_TYPE_VIDEO) {
                program.videoTracks << index;
            }
        }
        programs << program;
    }
    return programs;
}

// 工具函数:智能组合节目(用于处理无规范节目信息的文件)
void TrackHelper::getProgram(QList<ProgramInfo> &programs, const QList<int> &audioTracks, const QList<int> &videoTracks) {
    int audioCount = audioTracks.count();
    int videoCount = videoTracks.count();
    int number = audioCount / videoCount;
    if (audioCount % videoCount != 0) {
        return; // 非整数倍关系,不进行自动组合
    }
    int index = 0;
    programs.clear();
    for (int i = 0; i < videoCount; ++i) {
        ProgramInfo program;
        program.id = i;
        program.name = QString("节目%1").arg(i + 1);
        program.provider = QString("发布%1").arg(i + 1);
        program.videoTracks << videoTracks.at(i);
        for (int j = 0; j < number; ++j) {
            program.audioTracks << audioTracks.at(index++);
        }
        programs << program;
    }
}

通过上述方法,我们能够精确解析TS容器中的多节目结构,并实现流畅、准确的节目切换功能,为开发专业的流媒体播放器提供了关键技术支撑。




上一篇:Paper2Slides:基于RAG的论文转PPT工具,一键生成学术汇报幻灯片
下一篇:Java包装类型与基本类型详解:遵循阿里巴巴开发手册的业务场景与性能考量
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 10:37 , Processed in 0.134653 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表