GStreamer——教程——基础教程4:Time management

基础教程4:Time management(时间管理)

目标

本教程展示了如何使用GStreamer时间相关工具。特别是:

  • 如何查询管道以获取流位置或持续时间等信息。
  • 如何寻找(跳转)到流内的不同位置(时间)。

介绍

GstQuery是一种机制,允许向元素或 pad 询问 一条信息。在此示例中,我们询问 pipeline 是否正在寻找允许(某些来源,如直播,不允许查找)。如果它是允许的,那么,一旦movie 播放了十秒钟,我们使用查找跳到不同的位置。

在前面的教程中,一旦我们设置并运行了pipeline , 我们的主要功能只是等待接收 ERROR 或 EOS 通过 bus 。在这里,我们修改这个函数以定期唤醒并查询pipeline 中的流位置,以便我们可以打印它 屏幕。这类似于媒体播放器会做的事情,更新定期的用户界面。

最后,只要听歌时长发生变化,就会进行查询和更新。

寻求的例子

将此代码复制到名为basic-tutorial-4.c文本文件中(或找到它 在您的GStreamer安装中)。

basic-tutorial-4.c

#include <gst/gst.h>

/* Structure to contain all our information, so we can pass it around */
typedef struct _CustomData {
  GstElement *playbin;  /* Our one and only element */
  gboolean playing;      /* Are we in the PLAYING state? */
  gboolean terminate;    /* Should we terminate execution? */
  gboolean seek_enabled; /* Is seeking enabled for this media? */
  gboolean seek_done;    /* Have we performed the seek already? */
  gint64 duration;       /* How long does this media last, in nanoseconds */
} CustomData;

/* Forward definition of the message processing function */
static void handle_message (CustomData *data, GstMessage *msg);

int main(int argc, char *argv[]) {
  CustomData data;
  GstBus *bus;
  GstMessage *msg;
  GstStateChangeReturn ret;

  data.playing = FALSE;
  data.terminate = FALSE;
  data.seek_enabled = FALSE;
  data.seek_done = FALSE;
  data.duration = GST_CLOCK_TIME_NONE;

  /* Initialize GStreamer */
  gst_init (&argc, &argv);

  /* Create the elements */
  data.playbin = gst_element_factory_make ("playbin", "playbin");

  if (!data.playbin) {
    g_printerr ("Not all elements could be created.\n");
    return -1;
  }

  /* Set the URI to play */
  g_object_set (data.playbin, "uri", "https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm", NULL);

  /* Start playing */
  ret = gst_element_set_state (data.playbin, GST_STATE_PLAYING);
  if (ret == GST_STATE_CHANGE_FAILURE) {
    g_printerr ("Unable to set the pipeline to the playing state.\n");
    gst_object_unref (data.playbin);
    return -1;
  }

  /* Listen to the bus */
  bus = gst_element_get_bus (data.playbin);
  do {
    msg = gst_bus_timed_pop_filtered (bus, 100 * GST_MSECOND,
        GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION);

    /* Parse message */
    if (msg != NULL) {
      handle_message (&data, msg);
    } else {
      /* We got no message, this means the timeout expired */
      if (data.playing) {
        gint64 current = -1;

        /* Query the current position of the stream */
        if (!gst_element_query_position (data.playbin, GST_FORMAT_TIME, &current)) {
          g_printerr ("Could not query current position.\n");
        }

        /* If we didn't know it yet, query the stream duration */
        if (!GST_CLOCK_TIME_IS_VALID (data.duration)) {
          if (!gst_element_query_duration (data.playbin, GST_FORMAT_TIME, &data.duration)) {
            g_printerr ("Could not query current duration.\n");
          }
        }

        /* Print current position and total duration */
        g_print ("Position %" GST_TIME_FORMAT " / %" GST_TIME_FORMAT "\r",
            GST_TIME_ARGS (current), GST_TIME_ARGS (data.duration));

        /* If seeking is enabled, we have not done it yet, and the time is right, seek */
        if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND) {
          g_print ("\nReached 10s, performing seek...\n");
          gst_element_seek_simple (data.playbin, GST_FORMAT_TIME,
              GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND);
          data.seek_done = TRUE;
        }
      }
    }
  } while (!data.terminate);

  /* Free resources */
  gst_object_unref (bus);
  gst_element_set_state (data.playbin, GST_STATE_NULL);
  gst_object_unref (data.playbin);
  return 0;
}

static void handle_message (CustomData *data, GstMessage *msg) {
  GError *err;
  gchar *debug_info;

  switch (GST_MESSAGE_TYPE (msg)) {
    case GST_MESSAGE_ERROR:
      gst_message_parse_error (msg, &err, &debug_info);
      g_printerr ("Error received from element %s: %s\n", GST_OBJECT_NAME (msg->src), err->message);
      g_printerr ("Debugging information: %s\n", debug_info ? debug_info : "none");
      g_clear_error (&err);
      g_free (debug_info);
      data->terminate = TRUE;
      break;
    case GST_MESSAGE_EOS:
      g_print ("\nEnd-Of-Stream reached.\n");
      data->terminate = TRUE;
      break;
    case GST_MESSAGE_DURATION:
      /* The duration has changed, mark the current one as invalid */
      data->duration = GST_CLOCK_TIME_NONE;
      break;
    case GST_MESSAGE_STATE_CHANGED: {
      GstState old_state, new_state, pending_state;
      gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
      if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data->playbin)) {
        g_print ("Pipeline state changed from %s to %s:\n",
            gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));

        /* Remember whether we are in the PLAYING state or not */
        data->playing = (new_state == GST_STATE_PLAYING);

        if (data->playing) {
          /* We just moved to PLAYING. Check if seeking is possible */
          GstQuery *query;
          gint64 start, end;
          query = gst_query_new_seeking (GST_FORMAT_TIME);
          if (gst_element_query (data->playbin, query)) {
            gst_query_parse_seeking (query, NULL, &data->seek_enabled, &start, &end);
            if (data->seek_enabled) {
              g_print ("Seeking is ENABLED from %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT "\n",
                  GST_TIME_ARGS (start), GST_TIME_ARGS (end));
            } else {
              g_print ("Seeking is DISABLED for this stream.\n");
            }
          }
          else {
            g_printerr ("Seeking query failed.");
          }
          gst_query_unref (query);
        }
      }
    } break;
    default:
      /* We should not reach here */
      g_printerr ("Unexpected message received.\n");
      break;
  }
  gst_message_unref (msg);
}

需要帮忙吗?

如果您需要帮助来编译此代码,请参阅为您的平台构建教程部分:Linux、Mac OS X或Windows,或在Linux上使用此特定命令:

gcc basic-tutorial-4.c -o basic-tutorial-4 `pkg-config --cflags --libs gstreamer-1.0`

如果您需要帮助来运行此代码,请参阅为您的平台运行教程部分:Linux、Mac OS X或Windows。

本教程打开一个窗口并显示一个带有音频的电影。媒体是从Internet获取的,因此窗口可能需要几秒钟才能出现,具体取决于您的连接速度。进入电影10秒后,它会跳到一个新位置

所需库:gstreamer-1.0

工作流

/* Structure to contain all our information, so we can pass it around */
typedef struct _CustomData {
  GstElement *playbin;  /* Our one and only element */
  gboolean playing;      /* Are we in the PLAYING state? */
  gboolean terminate;    /* Should we terminate execution? */
  gboolean seek_enabled; /* Is seeking enabled for this media? */
  gboolean seek_done;    /* Have we performed the seek already? */
  gint64 duration;       /* How long does this media last, in nanoseconds */
} CustomData;

/* Forward definition of the message processing function */
static void handle_message (CustomData *data, GstMessage *msg);

我们首先定义一个结构来包含我们所有的信息,所以我们可以将其传递给其他函数。特别是,在这个例子中,我们 将消息处理代码移动到自己的函数中 handle_message因为它长得有点太大了。

然后我们构建一个由单个元素组成的管道 playbin,我们已经在基本教程1:Hello world!了解。playbin本身就是一个管道,在这种情况下,它是唯一的 元素,所以我们直接使用playbin元素。我们将跳过细节:剪辑的URI通过playbin提供给 URI属性和管道设置为播放状态。

msg = gst_bus_timed_pop_filtered (bus, 100 * GST_MSECOND,
    GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION);

以前我们没有提供超时 gst_bus_timed_pop_filtered(),这意味着它直到消息已收到。现在我们使用100毫秒的超时,所以,如果十分之一秒内没有收到消息,函数将返回 NULL。我们将使用这个逻辑来更新我们的"UI"。

请注意,所需的超时必须指定为GstClockTime,因此, 以纳秒为单位。表示不同时间单位的数字应该是 乘以GST_SECONDGST_MSECOND之类的宏 您的代码更具可读性。

如果我们收到消息,我们在handle_message函数中处理它 (下一小节),否则:

用户接口刷新

/* We got no message, this means the timeout expired */
if (data.playing) {

如果pipeline 处于PLAYING状态,则是刷新屏幕的时候了。 如果我们不在PLAYING状态,我们什么都不想做,因为大多数查询都会失败。

我们到达这里大约每秒10次,足够刷新了为我们的UI评分。我们将在屏幕上打印当前媒体位置,我们可以通过查询管道来学习。这涉及一个下一个小节将显示的几个步骤,但是,由于位置 和持续时间是常见的查询,GstElement提供更容易, 现成的替代品:

/* Query the current position of the stream */
if (!gst_element_query_position (data.pipeline, GST_FORMAT_TIME, &current)) {
  g_printerr ("Could not query current position.\n");
}

gst_element_query_position() 并直接向我们提供结果。

/* If we didn't know it yet, query the stream duration */
if (!GST_CLOCK_TIME_IS_VALID (data.duration)) {
  if (!gst_element_query_duration (data.pipeline, GST_FORMAT_TIME, &data.duration)) {
     g_printerr ("Could not query current duration.\n");
  }
}

现在是了解溪流长度的好时机 另一个GstElement辅助函数:gst_element_query_duration()

/* Print current position and total duration */
g_print ("Position %" GST_TIME_FORMAT " / %" GST_TIME_FORMAT "\r",
    GST_TIME_ARGS (current), GST_TIME_ARGS (data.duration));

注意GST_TIME_FORMATGST_TIME_ARGS宏的用法提供GStreamer时间的用户友好表示。

/* If seeking is enabled, we have not done it yet, and the time is right, seek */
if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND) {
  g_print ("\nReached 10s, performing seek...\n");
  gst_element_seek_simple (data.pipeline, GST_FORMAT_TIME,
      GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND);
  data.seek_done = TRUE;
}

现在我们执行寻找,“simply”通过在pipeline上调用gst_element_seek_simple()。很多寻找的复杂性隐藏在这种方法中,这是一个很好的东西!

让我们回顾一下参数:

GST_FORMAT_TIME 表示我们正在指定目的地时间单位。其他查找格式使用不同的单位。

然后是 GstSeekFlags,让我们回顾一下最常见的:

GST_SEEK_FLAG_FLUSH:这将丢弃当前在pipeline 中的所有数据在寻找之前。pipeline 重新填充时可能会暂停一点新的数据开始出现,但大大增加了应用程序的“响应能力”。如果未提供此标志, “stale”数据可能会显示一段时间,直到新位置出现在pipeline 的末端。

GST_SEEK_FLAG_KEY_UNIT:对于大多数编码视频流,寻求任意位置是不可能的,但只能用于称为关键帧的某些帧。当这使用标志,查找实际上会移动到最近的关键帧和立即开始生成数据。如果不使用此标志,则pipeline 将在内部移动到最近的关键帧(它没有其他替代)但数据在达到请求之前不会显示 位置。最后一个选择更准确,但可能需要更长时间。

GST_SEEK_FLAG_ACCURATE:某些媒体剪辑不提供足够的索引信息,这意味着定位到任意位置很耗时。在这些情况下,GStreamer通常会估计要定位的位置,通常效果还不错。如果这种精度对你的案例来说不够好(你看到的定位没有到达你要求确切的时间),那么就提供这个标志。请注意,计算定位位置可能需要更长的时间(在某些文件上可能会非常长)。

最后,我们提供要定位的位置。由于我们要求使用GST_FORMAT_TIME,因此该值必须以纳秒为单位,因此为了简单起见,我们将时间表示为秒,然后再乘以GST_SECOND。

消息Pump

handle_message函数处理通过管道总线接收到的所有消息。错误和结束的处理与以前的教程相同,所以我们跳到有趣的部分:

case GST_MESSAGE_DURATION:
  /* The duration has changed, mark the current one as invalid */
  data->duration = GST_CLOCK_TIME_NONE;
  break;

每当流的持续时间发生变化时,此消息都会发布到总线上。在这里,我们简单地将当前持续时间标记为无效,以便稍后重新查询。

case GST_MESSAGE_STATE_CHANGED: {
  GstState old_state, new_state, pending_state;
  gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
  if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data->pipeline)) {
    g_print ("Pipeline state changed from %s to %s:\n",
        gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));

    /* Remember whether we are in the PLAYING state or not */
    data->playing = (new_state == GST_STATE_PLAYING);

查找和时间查询通常只在 PAUSEDPLAYING状态,因为所有元素都有机会接收信息并配置自己。在这里,我们使用playing 变量来跟踪管道是否处于PLAYING状态。 此外,如果我们刚刚进入PLAYING状态,我们将执行第一个查询。 我们询问管道是否允许在此流中查找:

if (data->playing) {
  /* We just moved to PLAYING. Check if seeking is possible */
  GstQuery *query;
  gint64 start, end;
  query = gst_query_new_seeking (GST_FORMAT_TIME);
  if (gst_element_query (data->pipeline, query)) {
    gst_query_parse_seeking (query, NULL, &data->seek_enabled, &start, &end);
    if (data->seek_enabled) {
      g_print ("Seeking is ENABLED from %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT "\n",
          GST_TIME_ARGS (start), GST_TIME_ARGS (end));
    } else {
      g_print ("Seeking is DISABLED for this stream.\n");
    }
  }
  else {
    g_printerr ("Seeking query failed.");
  }
  gst_query_unref (query);
}

gst_query_new_seeking()以GST_FORMAT_TIME格式创建一个新的“seeking”类型查询对象。这表明我们有兴趣通过指定我们想要移动的新时间来进行定位。我们也可以选择询问GST_FORMAT_BYTES,然后定位到源文件内的特定字节位置,但这通常较少使用。

然后,这个查询对象通过gst_element_query()传递给管道。结果存储在同一个查询中,可以用gst_query_parse_seeking()轻松检索。它提取一个布尔值,表示是否允许定位,以及可能进行定位的范围。

当你完成查询对象的使用时,别忘了取消引用它。

就这样!有了这些知识,就可以构建一个媒体播放器,它可以根据当前流位置定期更新滑块,并通过移动滑块来实现定位!

结论

本教程显示:

  • 如何使用GstQuery查询管道信息

  • 如何使用gst_element_query_position()和gst_element_query_duration()获取常见信息,如位置和持续时间

  • 如何使用gst_element_seek_simple()定位到流中的任意位置

  • 所有这些操作可以在哪些状态下执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/716253.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

图知识蒸馏综述:算法分类与应用分析

源自&#xff1a;软件学报 作者&#xff1a;陈哲涵 黎学臻 注&#xff1a;若出现无法显示完全的情况&#xff0c;可 V 搜索“人工智能技术与咨询”查看完整文章 摘 要 图数据, 如引文网络, 社交网络和交通网络, 广泛地存在现实生活中. 图神经网络凭借强大的表现力受到广泛…

PyQt5 生成py文件不能运行;pushButton点击事件;QTextEdit 获取输入框内容

目录 cant open file c.pyuic: c.pyuic $FileName$ -o $FileNameWithoutExtension$.p PyQt5 生成py文件不能运行 pushButton点击事件 QTextEdit 获取输入框内容 整体运行代码: Creating a Qt Widget Based Application | Qt Creator Manual cant open file c.pyuic: c.…

爬虫初学篇——看完这些还怕自己入门不了?

初次学习爬虫&#xff0c;知识笔记小分享 学scrapy框架可看&#xff1a;孤寒者博主的【Python爬虫必备—&#xff1e;Scrapy框架快速入门篇——上】 目录&#x1f31f; 一、&#x1f349;基础知识二、&#x1f349;http协议&#xff1a;三、&#x1f349;解析网页(1) xpath的用…

Vim基础操作:常用命令、安装插件、在VS Code中使用Vim及解决Vim编辑键盘错乱

Vim模式 普通模式&#xff08;Normal Mode&#xff09;&#xff1a; 这是 Vim 的默认模式&#xff0c;用于执行文本编辑命令&#xff0c;如复制、粘贴、删除等。在此模式下&#xff0c;你可以使用各种 Vim 命令来操作文本。插入模式&#xff08;Insert Mode&#xff09;&#…

Qt实现单例模式:Q_GLOBAL_STATIC和Q_GLOBAL_STATIC_WITH_ARGS

目录 1.引言 2.了解Q_GLOBAL_STATIC 3.了解Q_GLOBAL_STATIC_WITH_ARGS 4.实现原理 4.1.对象的创建 4.2.QGlobalStatic 4.3.宏定义实现 4.4.注意事项 5.总结 1.引言 设计模式之单例模式-CSDN博客 所谓的全局静态对象&#xff0c;大多是在单例类中所见&#xff0c;在之前…

来自工业界的知识库 RAG 服务(四),FinGLM 竞赛冠军项目详解

背景介绍 在 前一篇文章 中介绍过智谱组织的一个金融大模型 RAG 比赛 FinGLM 以及 ChatGLM反卷总局 团队的项目&#xff0c;这篇文章继续介绍下获得冠军的馒头科技的技术方案。 建议不了解比赛背景信息的可以先查看 来自工业界的知识库 RAG 服务(三)&#xff0c;FinGLM 竞赛获…

STM学习记录(六)————串口的发送接收

文章目录 前言一、串口结构体及库函数二、实现串口发送&#xff08;库函数&#xff09;1.程序设计2.代码 三.串口接收1.串口接收&#xff08;普通&#xff09;2.串口中断接收3. 串口发送字符串函数4.串口实现printf&#xff08;重定向&#xff09;5. 串口实现scanf&#xff08;…

五大维度大比拼:ChatGPT比较文心一言,你的AI助手选择指南

文章目录 一、评估AI助手的五个关键维度二、ChatGPT和文心一言的比较 评估AI助手的五个关键维度&#xff0c;以及ChatGPT和文心一言的比较如下&#xff1a; 一、评估AI助手的五个关键维度 界面友好性 &#xff1a; 评估标准&#xff1a;用户界面是否直观易用&#xff0c;是否…

详解 HBase 的架构和基本原理

一、基本架构 StoreFile&#xff1a;保存实际数据的物理文件&#xff0c;StoreFile 以 HFile 的格式 (KV) 存储在 HDFS 上。每个 Store 会有一个或多个 StoreFile&#xff08;HFile&#xff09;&#xff0c;数据在每个 StoreFile 中都是有序的MemStore&#xff1a;写缓存&#…

Samba 服务器的搭建以及windows server 2008客户端的使用实验报告

一、 实验目的 通过 Samba 服务器的搭建&#xff0c;基本了解搭建服务器的基本步骤&#xff0c;理解 Samba 服务器的实现文件共享的功能&#xff0c;如何配置 Samba服务器配置文件等。 二、 实验环境 准备一台安装 centOS7系统的 Linux 虚拟机作为 Samba 服务器 server,准备…

手机ip地址怎么换成成都的

随着互联网的快速发展&#xff0c;我们越来越依赖于网络进行各种操作。而在某些情况下&#xff0c;为了更好地享受网络服务或保护个人隐私&#xff0c;我们可能需要改变手机的IP地址。本文将详细介绍如何将手机IP地址换成成都的&#xff0c;同时提醒大家在操作过程中需要注意的…

如何学习创建和使用 Java 归档(JAR)文件

1. 简介 JAR&#xff08;Java ARchive&#xff09;文件是一种用于打包多个Java类、资源文件和元数据的压缩文件格式。它在Java开发和发布过程中扮演着重要角色。通过使用JAR文件&#xff0c;开发者可以将应用程序的所有组件打包在一个文件中&#xff0c;方便分发和部署。 2. …

二次元资源汇总

获取更多资源&#xff0c;请关注公众号&#xff1a;阿宇的编程之旅&#xff0c;回复‘书签’获取 动漫网站 动漫世界 网站名称&#xff1a;动漫世界网址&#xff1a;nav.acgsq.com介绍&#xff1a;中国最大最权威的正版动漫网站&#xff0c;提供漫画、动画、资讯、论坛等全方…

一些激活函数

一些激活函数 摘要激活函数分类sigmoidTanhSoftsignSoftmaxReLUSoftplusNoisy ReLULeaky ReLUPReluELUSELUSwishGELUGLUGEGLUMishMaxout 摘要 本篇博客对一些激活函数进行总结&#xff0c;以便加深理解和记忆 激活函数分类 饱和激活函数&#xff1a;sigmoid、tanh… 非饱和激…

短链接生成器排名前三!长链接转化成短链接工具有哪些?

在现今的网络营销环境中&#xff0c;短链接的应用越来越广泛。它不仅能简化长链接&#xff0c;提高分享效果&#xff0c;还能提升企业品牌形象和用户体验。于是&#xff0c;市场上涌现出众多短链接生成工具。本文将为您揭秘短链接生成器排名前三的产品&#xff0c;帮您找到最适…

ABB工业喷涂机器人保养,轻松搞定!

小伙伴都知道机器人在长时间的使用下&#xff0c;难免遇到一些机械手故障。一旦发生了机器人故障&#xff0c;会影响整个生产线的作业&#xff0c;那么怎么才能做到防止机器人的故障率发生呢&#xff1f;定期的保养与维护显得尤为重要&#xff0c;一个好的维修保养服务商也很重…

yml配置文件快速上手

yml配置文件快速上手 springboot中&#xff0c;有三种文件可以作为配置文件 xml文件(不推荐&#xff0c;臃肿)application.propertis文件&#xff08;层次不够分明&#xff09;yml文件&#xff08;推荐&#xff0c;层次分明&#xff0c;语法简洁&#xff09; yml文件的基本语…

【递归、搜索与回溯】记忆化搜索

一、经验总结 以斐波那契数为例引入今天的主角&#xff1a;记忆化搜索和动态规划 题目链接 509. 斐波那契数 - 力扣&#xff08;LeetCode&#xff09; 题目描述 算法原理 编写代码 //解法二&#xff1a;递归->记忆化搜索 class Solution {int mem[31]; //备忘录 public…

揭秘未来:用线性回归模型预测一切的秘密武器!

线性回归模型 1. 引言2. 理论基础2.1 线性回归模型的定义与原理原理与关键假设模型参数估计 2.2 模型评估指标2.2.1 残差分析2.2.2 拟合优度指标2.2.3 统计检验 3. 应用场景3.1. 金融领域中的应用3.2. 医疗健康领域中的应用3.3. 其他领域的应用 4. 实例分析4.1、数据集选择4.2、…

目标检测算法YOLOv10简介

YOLOv10由Ao Wang等人于2024年提出&#xff0c;论文名为&#xff1a;《YOLOv10: Real-Time End-to-End Object Detection》&#xff0c;论文见&#xff1a;https://arxiv.org/pdf/2405.14458 &#xff1b;源码见: https://github.com/THU-MIG/yolov10 以下内容主要来自论文&a…