功能标志和 Canary 模式

Media3 在其库中引入了新的优化、行为改进和架构增强功能。虽然这些变更在发布前经过了全面测试和验证,但 Media3 包含一个以 Canary 模式为中心的集中式功能标志基础架构 (androidx.media3.common.Flags),以便应用控制采用新行为的方式和时间。

本页面介绍了 Canary 模式和功能标志的工作原理,以及如何在应用中使用它们来降低跨版本升级风险、运行受控发布和配置紧急回滚。

概览

Canary 模式将新引入且经过验证的库改进功能归入一个统一的全局切换开关下:

  • 默认启用:在标准 Media3 版本中,Canary 模式默认处于启用状态 (true)。这样可确保您的应用自动受益于安全的性能增强、优化和新功能。
  • 选择退出和回滚功能:应用可以在运行时全局停用 Canary 模式,也可以单独替换特定功能标志。
  • 零布线复杂性:标志通过 common 模块中的中央 Flags 类进行管理,无需在不同组件中使用定制的 setter 或配置对象。

为什么要在应用中使用标志?

虽然 Canary 模式默认处于启用状态,但将 Flags 与应用的配置或实验框架集成可带来以下好处:

1. 降低版本升级风险

在生产环境中升级库的主要或次要版本可能会在特定 OEM 设备或自定义媒体流水线上引入意外交互。更新 Media3 时,您可以使用 Canary 模式和远程配置,在不同版本之间实现更安全的升级流程:

  1. 发布停用 Canary 模式的更新:发布停用 Canary 模式的新 Media3 版本,可大致保留上一版本(启用 Canary)的特征集,让您在采用新行为之前,先在生产环境中验证基准库的稳定性和常规问题修复。
  2. 逐步启用 Canary 模式:使用远程配置在用户群组中分阶段启用 Canary 模式(例如,5% → 25% → 100%),或运行 A/B 实验来衡量指标影响。
  3. 根据需要回滚:如果特定设备上出现回归问题,可以远程停用 Canary 模式或特定标志 ID,而无需紧急发布应用。
  4. 采用为默认设置:验证后,默认情况下保持 Canary 模式处于启用状态,直到下次版本升级,届时之前验证的 Canary 功能将成为永久功能。

Kotlin

// Enable or disable Canary mode globally (defaults to true).
Flags.setCanaryModeEnabled(false)

Java

// Enable or disable Canary mode globally (defaults to true).
Flags.setCanaryModeEnabled(false);

2. 受控的分阶段发布和 A/B 测试

您可以将 Flags 连接到远程配置或实验系统,而不是一次性向 100% 的用户公开新的库行为。

您可以分阶段启用 Canary 模式(例如,5% 的用户 → 25% → 100%),也可以运行 A/B 实验来衡量新优化对核心播放指标(例如启动延迟时间、重新缓冲率和播放失败率)的影响。

3. 远程紧急停止开关和快速回滚

如果特定设备型号或操作系统版本在生产环境中出现回归问题,您可以使用服务器端配置远程停用 Canary 模式(或负责的特定标志 ID)。这会立即将受影响用户的库还原到其稳定的旧版回退行为,而无需紧急应用热修复或二进制文件发布。

配置 Canary 模式和标志

Flags API 提供了控制全局 Canary 模式和单个标志替换的方法。

全球 Canary 模式

如需启用或停用 Canary 模式涵盖的所有增强功能,请执行以下操作:

Kotlin

// Enable or disable Canary mode globally (defaults to true).
Flags.setCanaryModeEnabled(false)

Java

// Enable or disable Canary mode globally (defaults to true).
Flags.setCanaryModeEnabled(false);

替换个别标志

您还可以单独启用或停用特定标志,而不受全局 Canary 模式设置的影响:

Kotlin

// Explicitly enable an experimental flag.
Flags.enableFlag(Flags.FLAG_PER_STREAM_MEDIA_PROGRESSION)

// Explicitly disable a flag.
Flags.disableFlag(Flags.FLAG_DYNAMIC_SCHEDULING)

Java

// Explicitly enable an experimental flag.
Flags.enableFlag(Flags.FLAG_PER_STREAM_MEDIA_PROGRESSION);

// Explicitly disable a flag.
Flags.disableFlag(Flags.FLAG_DYNAMIC_SCHEDULING);

显式替换始终优先于全局 Canary 模式默认设置。

正在检查标记状态

如需检查代码中是否启用了 Canary 模式或特定标志,请执行以下操作:

Kotlin

// Query whether Canary mode is active globally.
val isCanaryActive = Flags.isCanaryModeEnabled()

// Query whether a specific flag is active.
val isFeatureActive = Flags.isEnabled(Flags.FLAG_PER_STREAM_MEDIA_PROGRESSION)

Java

// Query whether Canary mode is active globally.
boolean isCanaryActive = Flags.isCanaryModeEnabled();

// Query whether a specific flag is active.
boolean isFeatureActive = Flags.isEnabled(Flags.FLAG_PER_STREAM_MEDIA_PROGRESSION);

标志解析顺序

当 Media3 在运行时检查某项功能是否已启用时,它会按以下顺序解析状态:

  1. 显式运行时替换:如果针对标志 ID 调用了 enableFlag 或 disableFlag,则会返回该替换值。
  2. 静态库状态:如果标志被静态停用(例如,尚未准备好进入 Canary 模式的早期开发功能)或静态启用(从 Canary 模式毕业的功能,保留了临时停用后备选项),则会返回该静态值。
  3. Canary 模式状态:否则,该标志会遵循全局 Canary 模式设置(如果 Canary 模式为 true,则启用;如果为 false,则停用)。

初始化要求

在实例化任何 Media3 组件(例如 ExoPlayer、Transformer 或 MediaSession)之前配置标志:

  • 及早设置:在应用启动期间(例如,在 Application.onCreate 内)或在初始化应用的媒体层之前配置 Flags。
  • 线程安全:Media3 组件在实例化期间读取标志,并将这些值传播到内部播放和处理线程。确保在创建 Media3 组件之前进行标志配置。不支持在播放器或转换器正在运行时修改标志。

与远程配置集成

建议的架构是在远程配置平台中公开三个参数:

  • media3_canary_mode_enabled(布尔值):Canary 模式的全局切换开关(默认为 true)。
  • media3_force_enable_flags(整数列表):要为目标试用强制启用的特定标志 ID。
  • media3_force_disable_flags(整数列表):如果发现问题,则强制停用的特定标志 ID。

在启动期间创建播放器实例之前,请应用以下值:

Kotlin

fun initializeMedia3Flags(
  canaryModeEnabled: Boolean,
  forceEnabledFlags: List<Int>,
  forceDisabledFlags: List<Int>,
) {
  // 1. Set global Canary mode state.
  Flags.setCanaryModeEnabled(canaryModeEnabled)

  // 2. Apply explicit force-enable overrides.
  for (flagId in forceEnabledFlags) {
    Flags.enableFlag(flagId)
  }

  // 3. Apply explicit force-disable overrides (remote kill switch).
  for (flagId in forceDisabledFlags) {
    Flags.disableFlag(flagId)
  }
}

Java

public static void initializeMedia3Flags(
    boolean canaryModeEnabled,
    List<Integer> forceEnabledFlags,
    List<Integer> forceDisabledFlags) {
  // 1. Set global Canary mode state.
  Flags.setCanaryModeEnabled(canaryModeEnabled);

  // 2. Apply explicit force-enable overrides.
  for (int flagId : forceEnabledFlags) {
    Flags.enableFlag(flagId);
  }

  // 3. Apply explicit force-disable overrides (remote kill switch).
  for (int flagId : forceDisabledFlags) {
    Flags.disableFlag(flagId);
  }
}

使用标志进行测试

为受功能标志影响的代码路径编写单元测试或 Robolectric 测试时,请使用 androidx.media3.test.utils.Media3FlagsRule(在 media3-test-utils 模块中提供),以确保测试不会相互污染静态状态。

使用 Media3FlagsRule 自动清理

添加 Media3FlagsRule 可确保在每次测试后自动重置所有标志替换和 Canary 模式修改:

Kotlin

class MyPlayerTest {

  @get:Rule val flagsRule = Media3FlagsRule(this)

  @Test
  fun playback_withFeatureFlagEnabled() {
    Flags.enableFlag(Flags.FLAG_PER_STREAM_MEDIA_PROGRESSION)
    // Run test with the flag enabled...
  }
}

Java

public static class MyPlayerTest {

  @Rule public final Media3FlagsRule flagsRule = new Media3FlagsRule(this);

  @Test
  public void playback_withFeatureFlagEnabled() {
    Flags.enableFlag(Flags.FLAG_PER_STREAM_MEDIA_PROGRESSION);
    // Run test with the flag enabled...
  }
}

参数化测试

您可以将 Media3FlagsRule 与 @BindFlag 和 @BindCanaryMode 注解结合使用,以在各种标志状态下运行参数化测试:

Kotlin

class MyParameterizedPlayerTest {

  @BindCanaryMode var canaryMode: Boolean = false

  @BindFlag(Flags.FLAG_PER_STREAM_MEDIA_PROGRESSION) var perStreamProgression: Boolean = false

  @get:Rule val flagsRule = Media3FlagsRule(this)

  @Test
  fun playback() {
    // Flags are automatically set for each test parameter combination.
  }
}

Java

public static class MyParameterizedPlayerTest {

  @BindCanaryMode public boolean canaryMode;

  @BindFlag(Flags.FLAG_PER_STREAM_MEDIA_PROGRESSION)
  public boolean perStreamProgression;

  @Rule public final Media3FlagsRule flagsRule = new Media3FlagsRule(this);

  @Test
  public void playback() {
    // Flags are automatically set for each test parameter combination.
  }
}

诊断和日志记录

Media3 提供了诊断工具,用于在播放初始化期间和运行时验证活跃标志和 Canary 模式状态。

初始化日志

ExoPlayer 实例初始化后,它会将活跃 Canary 状态和任何显式替换项以及库版本输出到 logcat:

ExoPlayerImpl: Init 59a18ab [AndroidXMedia3/1.10.1] [flame, Pixel 4, Google, 33] [Canary:ON]

如果替换处于有效状态,则系统只会列出与默认状态不同的设置:

ExoPlayerImpl: Init 59a18ab [AndroidXMedia3/1.10.1] [flame, Pixel 4, Google, 33] [Canary:ON, Overrides: enabled=[1], disabled=[3]]

程序化检查

您可以通过编程方式检索格式化的诊断字符串或查询替换列表(例如,在应用的崩溃报告或分析中包含标志状态):

Kotlin

// Get formatted string representing active Canary state and overrides.
val flagLogString = Flags.getLogString()

// Query explicit override IDs if needed.
val enabledFlags = Flags.getEnabledOverrides()
val disabledFlags = Flags.getDisabledOverrides()

Java

// Get formatted string representing active Canary state and overrides.
String flagLogString = Flags.getLogString();

// Query explicit override IDs if needed.
int[] enabledFlags = Flags.getEnabledOverrides();
int[] disabledFlags = Flags.getDisabledOverrides();

标记生命周期

Media3 中的标志旨在临时使用:

  1. 开发:在 Flags.java 中,在标志 ID 后面添加了新的优化或功能。
  2. Canary 模式:经过验证并可供一般使用后,该功能会在各个版本的 Canary 模式下处于启用状态。
  3. 升级和清理:在浸泡期证明生产环境中的稳定性后,该功能会在库中升级为永久启用,并且标志常量、回退代码路径和条件检查会被移除。

如果您遇到功能标志问题,请在 AndroidX Media3 GitHub 问题跟踪器中提交报告,以便团队在永久升级之前解决该问题。