什么是HarmonyOS开发中的音频会话

举报
Jack20 发表于 2026/06/20 20:53:26 2026/06/20
【摘要】 HarmonyOS开发中的音频会话:AVSession、媒体会话创建与销毁、会话元数据与锁屏媒体控制核心要点:音频会话(AVSession)是鸿蒙系统中媒体应用与系统 UI 之间的桥梁。本文从 AVSession 的核心概念入手,深入讲解媒体会话的创建与销毁生命周期、会话元数据的配置、控制命令的接收与响应、锁屏/通知栏媒体控制的实现,以及多应用会话协调的最佳实践。 一、背景与动机你有没有注...

HarmonyOS开发中的音频会话:AVSession、媒体会话创建与销毁、会话元数据与锁屏媒体控制

核心要点:音频会话(AVSession)是鸿蒙系统中媒体应用与系统 UI 之间的桥梁。本文从 AVSession 的核心概念入手,深入讲解媒体会话的创建与销毁生命周期、会话元数据的配置、控制命令的接收与响应、锁屏/通知栏媒体控制的实现,以及多应用会话协调的最佳实践。


一、背景与动机

你有没有注意过这样的体验:在手机上用音乐 App 听歌时,锁屏界面会显示当前歌曲的封面、歌名、歌手,还有播放/暂停/上一首/下一首的按钮?甚至蓝牙耳机上的按键也能控制播放?

这些功能不是"白来的"——它们都依赖于**音频会话(AVSession)**机制。

AVSession 的核心价值是:让媒体应用和系统 UI 解耦。你的 App 不需要自己画锁屏控件、不需要自己处理蓝牙按键,只需要创建一个 AVSession 并提供元数据,系统就会自动在锁屏、通知栏、控制中心等位置展示媒体控制界面。

但如果你不创建 AVSession 呢?那你的 App 就是一个"隐形"的媒体播放器——系统不知道你在播放什么,用户无法通过锁屏或蓝牙控制,甚至在切换应用后找不到回去的路。

说白了,不做 AVSession 的媒体 App,就像没有门牌号的房子——没人找得到你


二、核心原理

2.1 AVSession 架构总览

flowchart TD
    subgraph 应用层
        APP[媒体应用]
        SESSION[AVSession<br/>媒体会话]
    end

    subgraph 系统服务层
        SM[AVSessionManager<br/>会话管理器]
        SC[AVSessionController<br/>会话控制器]
    end

    subgraph 系统 UILS[锁屏界面]
        NB[通知栏]
        CC[控制中心]
        BT[蓝牙/耳机]
    end

    APP -->|1. 创建会话| SESSION
    SESSION -->|2. 注册到系统| SM
    SM -->|3. 分发控制| SC
    SC -->|4. 展示媒体信息| LS
    SC --> NB
    SC --> CC
    SC --> BT

    BT -->|5. 用户操作| SC
    SC -->|6. 转发命令| SESSION
    SESSION -->|7. 回调到应用| APP

    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

    class SESSION primary
    class SM,SC info
    class LS,NB,CC,BT purple
    class APP warning

2.2 核心概念

概念 说明
AVSession 媒体会话,应用侧创建,用于向系统声明"我在播放媒体"
AVSessionManager 会话管理器,系统服务,管理所有会话的生命周期
AVSessionController 会话控制器,系统 UI 侧使用,用于获取会话信息和发送控制命令
会话元数据 媒体信息:标题、作者、封面、时长等
控制命令 播放/暂停/上下曲/快进/快退/Seek 等
会话激活 激活的会话才能被系统 UI 展示和接收控制命令

2.3 会话生命周期

stateDiagram-v2
    [*] --> Created: createAVSession()
    Created --> Activated: session.activate()
    Activated --> Deactivated: session.deactivate()
    Deactivated --> Activated: session.activate()
    Activated --> Destroyed: session.destroy()
    Deactivated --> Destroyed: session.destroy()
    Created --> Destroyed: session.destroy()
    Destroyed --> [*]

    note right of Activated: 可被系统UI发现\n可接收控制命令
    note right of Deactivated: 不可被发现\n不接收命令

关键规则

  • 只有激活的会话才会出现在锁屏/通知栏
  • 应用切到后台时,应该保持会话激活(而不是销毁),这样用户还能通过锁屏控制
  • 应用退出时,必须销毁会话,否则会话会变成"僵尸"
  • 同一应用可以创建多个会话(如音乐播放 + 视频播放),但同一时间只应激活一个

2.4 会话元数据结构

// AVSessionMetadata 核心字段
interface AVSessionMetadata {
  assetId: string;        // 媒体资源 ID(唯一标识)
  title: string;          // 标题(歌名/视频名)
  artist: string;         // 艺术家/作者
  author: string;         // 作者(与 artist 可不同)
  album: string;          // 专辑名
  writer: string;         // 作词/编剧
  composer: string;       // 作曲
  duration: number;       // 时长(毫秒)
  mediaImage: image.PixelMap | string;  // 封面图
  subtitle: string;       // 副标题
  description: string;    // 描述
  lyric: string;          // 歌词
  previousAssetId: string; // 上一首资源 ID
  nextAssetId: string;     // 下一首资源 ID
}

三、代码实战

3.1 基础:创建 AVSession 并设置元数据

// 文件名:BasicAVSession.ets
// 功能:创建媒体会话,设置元数据,激活会话

import { avSession } from '@kit.AvSessionKit';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct BasicAVSessionPage {
  @State sessionActive: boolean = false;
  @State currentTitle: string = '未播放';
  @State currentArtist: string = '-';
  @State isPlaying: boolean = false;

  private session: avSession.AVSession | null = null;
  private sessionManager: avSession.AVSessionManager | null = null;

  // 模拟播放列表
  private playlist = [
    { id: 'song_1', title: '晚风', artist: '周杰伦', album: '七里香', duration: 270000 },
    { id: 'song_2', title: '晴天', artist: '周杰伦', album: '叶惠美', duration: 269000 },
    { id: 'song_3', title: '稻香', artist: '周杰伦', album: '魔杰座', duration: 223000 },
  ];
  private currentIndex: number = 0;

  aboutToAppear() {
    this.createSession();
  }

  // 创建媒体会话
  async createSession() {
    try {
      // 1. 获取会话管理器
      this.sessionManager = avSession.getSessionManager(getContext(this) as common.UIAbilityContext);

      // 2. 创建会话——指定会话类型和标签
      this.session = await this.sessionManager.createAVSession(
        'music_session_001',  // 会话 ID(全局唯一)
        '我的音乐播放器',       // 会话标签(用于调试)
        avSession.AVSessionType.AUDIO  // 会话类型:音频
      );
      console.info('[AVSession] 会话创建成功');

      // 3. 设置会话元数据
      await this.updateMetadata();

      // 4. 注册控制命令回调
      this.registerControlCommands();

      // 5. 激活会话
      await this.session.activate();
      this.sessionActive = true;
      console.info('[AVSession] 会话已激活');

    } catch (error) {
      console.error(`[AVSession] 创建失败: ${JSON.stringify(error)}`);
    }
  }

  // 更新会话元数据
  async updateMetadata() {
    if (!this.session) return;

    const song = this.playlist[this.currentIndex];

    // 构建元数据
    const metadata: avSession.AVMetadata = {
      assetId: song.id,
      title: song.title,
      artist: song.artist,
      album: song.album,
      duration: song.duration,
      // 封面图——使用网络图片 URL
      mediaImage: 'https://example.com/album_cover.jpg',
      // 上下首资源 ID
      previousAssetId: this.currentIndex > 0 ? this.playlist[this.currentIndex - 1].id : '',
      nextAssetId: this.currentIndex < this.playlist.length - 1 ? this.playlist[this.currentIndex + 1].id : '',
    };

    // 设置元数据到会话
    await this.session.setAVMetadata(metadata);

    // 更新 UI
    this.currentTitle = song.title;
    this.currentArtist = song.artist;

    console.info(`[AVSession] 元数据已更新: ${song.title} - ${song.artist}`);
  }

  // 注册控制命令回调
  private registerControlCommands() {
    if (!this.session) return;

    // ★ 核心:监听系统 UI 发来的控制命令
    // 这些命令来自锁屏、通知栏、控制中心、蓝牙耳机等

    // 播放命令
    this.session.on('play', () => {
      console.info('[AVSession] 收到播放命令');
      this.isPlaying = true;
      // 实际开发中,这里调用播放器的 play() 方法
    });

    // 暂停命令
    this.session.on('pause', () => {
      console.info('[AVSession] 收到暂停命令');
      this.isPlaying = false;
    });

    // 停止命令
    this.session.on('stop', () => {
      console.info('[AVSession] 收到停止命令');
      this.isPlaying = false;
    });

    // 下一首命令
    this.session.on('playNext', () => {
      console.info('[AVSession] 收到下一首命令');
      this.playNext();
    });

    // 上一首命令
    this.session.on('playPrevious', () => {
      console.info('[AVSession] 收到上一首命令');
      this.playPrevious();
    });

    // 快进命令
    this.session.on('fastForward', () => {
      console.info('[AVSession] 收到快进命令');
    });

    // 快退命令
    this.session.on('rewind', () => {
      console.info('[AVSession] 收到快退命令');
    });

    // Seek 命令
    this.session.on('seek', (time: number) => {
      console.info(`[AVSession] 收到 Seek 命令: ${time}ms`);
    });

    // 设置播放速度
    this.session.on('setSpeed', (speed: number) => {
      console.info(`[AVSession] 收到设置速度命令: ${speed}x`);
    });

    // 设置循环模式
    this.session.on('setLoopMode', (mode: avSession.LoopMode) => {
      console.info(`[AVSession] 收到设置循环模式命令: ${mode}`);
    });

    // 收藏/取消收藏
    this.session.on('toggleFavorite', (assetId: string) => {
      console.info(`[AVSession] 收到收藏切换命令: ${assetId}`);
    });
  }

  // 播放下一首
  private async playNext() {
    if (this.currentIndex < this.playlist.length - 1) {
      this.currentIndex++;
      await this.updateMetadata();
    }
  }

  // 播放上一首
  private async playPrevious() {
    if (this.currentIndex > 0) {
      this.currentIndex--;
      await this.updateMetadata();
    }
  }

  // 模拟播放/暂停
  private async togglePlay() {
    this.isPlaying = !this.isPlaying;

    // ★ 重要:同步更新会话的播放状态
    // 这样锁屏/通知栏的按钮状态才会正确更新
    if (this.session) {
      await this.session.setAVPlaybackState({
        state: this.isPlaying ? avSession.PlaybackState.PLAYBACK_STATE_PLAY :
                               avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
        speed: 1.0,
        position: {
          elapsedTime: 0,      // 已播放时间(ms)
          updateTime: Date.now(), // 更新时间戳
        },
        bufferedTime: 0,       // 缓冲时间(ms)
        loopMode: avSession.LoopMode.LOOP_MODE_SEQUENCE,
        isFavorite: false,
      });
    }
  }

  build() {
    Column() {
      Text('AVSession 媒体会话')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#E0E0E0')
        .margin({ bottom: 20 })

      // 会话状态指示
      Row() {
        Circle({ width: 12, height: 12 })
          .fill(this.sessionActive ? '#81C784' : '#EF5350')
        Text(this.sessionActive ? '会话已激活' : '会话未激活')
          .fontSize(14)
          .fontColor(this.sessionActive ? '#81C784' : '#EF5350')
          .margin({ left: 8 })
      }
      .margin({ bottom: 20 })

      // 当前播放信息
      Column() {
        // 封面占位
        Row() {
          Column() {
            Text('🎵')
              .fontSize(48)
          }
          .width(120)
          .height(120)
          .borderRadius(12)
          .backgroundColor('#1a1a2e')
          .justifyContent(FlexAlign.Center)

          Column() {
            Text(this.currentTitle)
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E0E0E0')
            Text(this.currentArtist)
              .fontSize(14)
              .fontColor('#888888')
              .margin({ top: 8 })
            Text(`曲目 ${this.currentIndex + 1} / ${this.playlist.length}`)
              .fontSize(12)
              .fontColor('#6C63FF')
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 15 })
        }
      }
      .width('100%')
      .padding(20)
      .borderRadius(12)
      .backgroundColor('#16213e')
      .margin({ bottom: 20 })

      // 播放控制
      Row() {
        Button('上一首')
          .onClick(() => this.playPrevious())
          .width(80)
          .height(44)
          .backgroundColor('#16213e')
          .fontColor('#E0E0E0')
          .fontSize(13)

        Button(this.isPlaying ? '暂停' : '播放')
          .onClick(() => this.togglePlay())
          .width(80)
          .height(44)
          .backgroundColor('#6C63FF')
          .fontSize(13)

        Button('下一首')
          .onClick(() => this.playNext())
          .width(80)
          .height(44)
          .backgroundColor('#16213e')
          .fontColor('#E0E0E0')
          .fontSize(13)
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .margin({ bottom: 20 })

      // 提示信息
      Text('锁屏/通知栏/控制中心将显示媒体控制')
        .fontSize(12)
        .fontColor('#888888')
        .margin({ bottom: 8 })
      Text('蓝牙耳机按键也可控制播放')
        .fontSize(12)
        .fontColor('#888888')
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#0d0d1a')
  }

  aboutToDisappear() {
    // ★ 重要:销毁会话,避免"僵尸会话"
    this.destroySession();
  }

  // 销毁会话
  async destroySession() {
    try {
      if (this.session) {
        await this.session.deactivate();
        await this.session.destroy();
        this.session = null;
        this.sessionActive = false;
        console.info('[AVSession] 会话已销毁');
      }
    } catch (error) {
      console.error(`[AVSession] 销毁失败: ${JSON.stringify(error)}`);
    }
  }
}

3.2 进阶:播放状态同步与锁屏媒体控制

锁屏媒体控制的关键在于播放状态的实时同步。应用侧的播放状态变化必须及时通知到 AVSession,否则锁屏界面的按钮状态会与实际不一致。

// 文件名:LockScreenControl.ets
// 功能:完整的锁屏媒体控制实现,包含播放状态同步

import { avSession } from '@kit.AvSessionKit';
import { common } from '@kit.AbilityKit';

// 播放器状态
interface PlayerState {
  isPlaying: boolean;
  currentTime: number;    // 当前播放位置(ms)
  duration: number;       // 总时长(ms)
  speed: number;          // 播放速度
  loopMode: avSession.LoopMode;
  isFavorite: boolean;
  bufferedTime: number;   // 缓冲进度(ms)
}

@Entry
@Component
struct LockScreenControlPage {
  @State playerState: PlayerState = {
    isPlaying: false,
    currentTime: 0,
    duration: 270000,  // 4:30
    speed: 1.0,
    loopMode: avSession.LoopMode.LOOP_MODE_SEQUENCE,
    isFavorite: false,
    bufferedTime: 270000,
  };
  @State sessionInfo: string = '未创建';

  private session: avSession.AVSession | null = null;
  private playTimer: number = -1;

  aboutToAppear() {
    this.initSessionWithFullControl();
  }

  // 初始化会话——注册完整的控制命令
  async initSessionWithFullControl() {
    try {
      const context = getContext(this) as common.UIAbilityContext;
      const sessionManager = avSession.getSessionManager(context);

      this.session = await sessionManager.createAVSession(
        'full_control_session',
        '完整控制播放器',
        avSession.AVSessionType.AUDIO
      );

      // 设置初始元数据
      await this.session.setAVMetadata({
        assetId: 'track_001',
        title: '夜曲',
        artist: '周杰伦',
        album: '十一月的萧邦',
        duration: this.playerState.duration,
        mediaImage: 'https://example.com/cover.jpg',
      });

      // ★ 核心:设置支持的控制命令
      // 只有注册了的命令,锁屏界面才会显示对应的按钮
      await this.session.setAVPlaybackState({
        state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
        speed: 1.0,
        position: { elapsedTime: 0, updateTime: Date.now() },
        bufferedTime: this.playerState.bufferedTime,
        loopMode: this.playerState.loopMode,
        isFavorite: false,
      });

      // 注册所有控制命令
      this.registerAllCommands();

      // 激活会话
      await this.session.activate();
      this.sessionInfo = '会话已激活 ✓';

    } catch (error) {
      console.error(`[Session] 初始化失败: ${JSON.stringify(error)}`);
      this.sessionInfo = `初始化失败: ${error.message}`;
    }
  }

  // 注册所有控制命令
  private registerAllCommands() {
    if (!this.session) return;

    // 播放
    this.session.on('play', () => {
      console.info('[Control] play');
      this.playerState.isPlaying = true;
      this.syncPlaybackState();
      this.startProgressTimer();
    });

    // 暂停
    this.session.on('pause', () => {
      console.info('[Control] pause');
      this.playerState.isPlaying = false;
      this.syncPlaybackState();
      this.stopProgressTimer();
    });

    // 停止
    this.session.on('stop', () => {
      console.info('[Control] stop');
      this.playerState.isPlaying = false;
      this.playerState.currentTime = 0;
      this.syncPlaybackState();
      this.stopProgressTimer();
    });

    // 下一首
    this.session.on('playNext', () => {
      console.info('[Control] playNext');
      this.playerState.currentTime = 0;
      this.syncPlaybackState();
    });

    // 上一首
    this.session.on('playPrevious', () => {
      console.info('[Control] playPrevious');
      this.playerState.currentTime = 0;
      this.syncPlaybackState();
    });

    // 快进
    this.session.on('fastForward', () => {
      console.info('[Control] fastForward');
      this.playerState.currentTime = Math.min(
        this.playerState.duration,
        this.playerState.currentTime + 10000
      );
      this.syncPlaybackState();
    });

    // 快退
    this.session.on('rewind', () => {
      console.info('[Control] rewind');
      this.playerState.currentTime = Math.max(
        0,
        this.playerState.currentTime - 10000
      );
      this.syncPlaybackState();
    });

    // Seek 到指定位置
    this.session.on('seek', (time: number) => {
      console.info(`[Control] seek to ${time}ms`);
      this.playerState.currentTime = Math.max(0, Math.min(time, this.playerState.duration));
      this.syncPlaybackState();
    });

    // 设置速度
    this.session.on('setSpeed', (speed: number) => {
      console.info(`[Control] setSpeed ${speed}x`);
      this.playerState.speed = speed;
      this.syncPlaybackState();
    });

    // 设置循环模式
    this.session.on('setLoopMode', (mode: avSession.LoopMode) => {
      console.info(`[Control] setLoopMode ${mode}`);
      this.playerState.loopMode = mode;
      this.syncPlaybackState();
    });

    // 收藏切换
    this.session.on('toggleFavorite', (assetId: string) => {
      console.info(`[Control] toggleFavorite ${assetId}`);
      this.playerState.isFavorite = !this.playerState.isFavorite;
      this.syncPlaybackState();
    });
  }

  // ★★★ 核心:同步播放状态到 AVSession ★★★
  // 每次播放状态变化时都必须调用此方法
  private async syncPlaybackState() {
    if (!this.session) return;

    try {
      await this.session.setAVPlaybackState({
        // 播放状态
        state: this.playerState.isPlaying
          ? avSession.PlaybackState.PLAYBACK_STATE_PLAY
          : avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
        // 播放速度
        speed: this.playerState.speed,
        // 当前播放位置
        position: {
          elapsedTime: this.playerState.currentTime,
          updateTime: Date.now(),
        },
        // 缓冲进度
        bufferedTime: this.playerState.bufferedTime,
        // 循环模式
        loopMode: this.playerState.loopMode,
        // 是否收藏
        isFavorite: this.playerState.isFavorite,
      });
    } catch (error) {
      console.error(`[Session] 状态同步失败: ${JSON.stringify(error)}`);
    }
  }

  // 启动进度计时器
  private startProgressTimer() {
    this.stopProgressTimer();
    this.playTimer = setInterval(() => {
      if (this.playerState.isPlaying) {
        this.playerState.currentTime += 1000 * this.playerState.speed;
        if (this.playerState.currentTime >= this.playerState.duration) {
          this.playerState.currentTime = this.playerState.duration;
          this.playerState.isPlaying = false;
          this.stopProgressTimer();
        }
        // 定期同步状态(不需要每秒都同步,每 5 秒一次即可)
        if (Math.floor(this.playerState.currentTime / 1000) % 5 === 0) {
          this.syncPlaybackState();
        }
      }
    }, 1000);
  }

  // 停止进度计时器
  private stopProgressTimer() {
    if (this.playTimer !== -1) {
      clearInterval(this.playTimer);
      this.playTimer = -1;
    }
  }

  build() {
    Column() {
      Text('锁屏媒体控制')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#E0E0E0')
        .margin({ bottom: 8 })

      Text(this.sessionInfo)
        .fontSize(13)
        .fontColor('#6C63FF')
        .margin({ bottom: 20 })

      // 播放进度条
      Column() {
        Row() {
          Text(this.formatTime(this.playerState.currentTime))
            .fontSize(12)
            .fontColor('#888888')
            .width(50)
          Progress({
            value: this.playerState.currentTime,
            total: this.playerState.duration,
            type: ProgressType.Linear
          })
            .layoutWeight(1)
            .color('#6C63FF')
          Text(this.formatTime(this.playerState.duration))
            .fontSize(12)
            .fontColor('#888888')
            .width(50)
            .textAlign(TextAlign.End)
        }
      }
      .width('100%')
      .padding(15)
      .borderRadius(12)
      .backgroundColor('#16213e')
      .margin({ bottom: 15 })

      // 播放状态信息
      Column() {
        Grid() {
          GridItem() {
            this.StateItem('播放状态', this.playerState.isPlaying ? '播放中' : '已暂停',
              this.playerState.isPlaying ? '#81C784' : '#FFB74D')
          }
          GridItem() {
            this.StateItem('播放速度', `${this.playerState.speed}x`, '#4FC3F7')
          }
          GridItem() {
            this.StateItem('循环模式', this.getLoopModeText(this.playerState.loopMode), '#CE93D8')
          }
          GridItem() {
            this.StateItem('收藏', this.playerState.isFavorite ? '❤️' : '🤍', '#EF5350')
          }
        }
        .columnsTemplate('1fr 1fr')
        .rowsTemplate('1fr 1fr')
        .width('100%')
        .height(140)
      }
      .width('100%')
      .padding(15)
      .borderRadius(12)
      .backgroundColor('#16213e')
      .margin({ bottom: 15 })

      // 控制按钮
      Row() {
        Button('⏮').onClick(() => {
          this.playerState.currentTime = 0;
          this.syncPlaybackState();
        }).width(50).height(50).backgroundColor('#16213e').fontSize(20)

        Button(this.playerState.isPlaying ? '⏸' : '▶️')
          .onClick(() => {
            this.playerState.isPlaying = !this.playerState.isPlaying;
            this.syncPlaybackState();
            if (this.playerState.isPlaying) {
              this.startProgressTimer();
            } else {
              this.stopProgressTimer();
            }
          })
          .width(60).height(60).backgroundColor('#6C63FF').fontSize(24)

        Button('⏭').onClick(() => {
          this.playerState.currentTime = 0;
          this.syncPlaybackState();
        }).width(50).height(50).backgroundColor('#16213e').fontSize(20)
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .margin({ bottom: 20 })

      // 速度控制
      Row() {
        ForEach([0.5, 0.75, 1.0, 1.25, 1.5, 2.0], (speed: number) => {
          Button(`${speed}x`)
            .onClick(() => {
              this.playerState.speed = speed;
              this.syncPlaybackState();
            })
            .fontSize(11)
            .height(32)
            .backgroundColor(this.playerState.speed === speed ? '#6C63FF' : '#16213e')
            .fontColor(this.playerState.speed === speed ? '#FFFFFF' : '#888888')
            .margin({ right: 4 })
        })
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#0d0d1a')
  }

  @Builder
  StateItem(label: string, value: string, color: string) {
    Column() {
      Text(value).fontSize(16).fontWeight(FontWeight.Bold).fontColor(color)
      Text(label).fontSize(11).fontColor('#888888').margin({ top: 4 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .borderRadius(8)
    .backgroundColor('#1a1a2e')
  }

  private formatTime(ms: number): string {
    const seconds = Math.floor(ms / 1000);
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins}:${secs.toString().padStart(2, '0')}`;
  }

  private getLoopModeText(mode: avSession.LoopMode): string {
    switch (mode) {
      case avSession.LoopMode.LOOP_MODE_SINGLE: return '单曲循环';
      case avSession.LoopMode.LOOP_MODE_SEQUENCE: return '列表循环';
      case avSession.LoopMode.LOOP_MODE_SHUFFLE: return '随机播放';
      default: return '不循环';
    }
  }

  aboutToDisappear() {
    this.stopProgressTimer();
    this.session?.deactivate();
    this.session?.destroy();
  }
}

3.3 高级:多应用会话协调与系统级控制

在多个媒体应用同时运行时,系统需要协调各个会话的优先级。下面的示例展示了如何监听系统会话变化并实现跨应用控制。

// 文件名:SessionCoordinator.ets
// 功能:多应用会话协调器——监听系统所有会话变化

import { avSession } from '@kit.AvSessionKit';
import { common } from '@kit.AbilityKit';

// 会话信息摘要
interface SessionSummary {
  sessionId: string;
  tag: string;
  type: avSession.AVSessionType;
  isActive: boolean;
  title: string;
  artist: string;
  isTopSession: boolean;
}

@Entry
@Component
struct SessionCoordinatorPage {
  @State sessions: SessionSummary[] = [];
  @State topSessionId: string = '';
  @State isMonitoring: boolean = false;

  private sessionManager: avSession.AVSessionManager | null = null;

  aboutToAppear() {
    this.initManager();
  }

  // 初始化会话管理器
  async initManager() {
    try {
      const context = getContext(this) as common.UIAbilityContext;
      this.sessionManager = avSession.getSessionManager(context);
      console.info('[Coordinator] 会话管理器初始化完成');
    } catch (error) {
      console.error(`[Coordinator] 初始化失败: ${JSON.stringify(error)}`);
    }
  }

  // 开始监听系统会话变化
  async startMonitoring() {
    if (!this.sessionManager) return;

    this.isMonitoring = true;

    try {
      // 监听会话创建
      this.sessionManager.on('sessionCreate', (session: avSession.AVSessionDescriptor) => {
        console.info(`[Coordinator] 新会话创建: ${session.sessionTag}`);
        this.refreshSessionList();
      });

      // 监听会话销毁
      this.sessionManager.on('sessionDestroy', (session: avSession.AVSessionDescriptor) => {
        console.info(`[Coordinator] 会话销毁: ${session.sessionTag}`);
        this.refreshSessionList();
      });

      // 监听顶级会话变化
      this.sessionManager.on('topSessionChange', (session: avSession.AVSessionDescriptor) => {
        console.info(`[Coordinator] 顶级会话变化: ${session.sessionTag}`);
        this.topSessionId = session.sessionId;
        this.refreshSessionList();
      });

      // 初始刷新
      await this.refreshSessionList();

    } catch (error) {
      console.error(`[Coordinator] 监听启动失败: ${JSON.stringify(error)}`);
    }
  }

  // 停止监听
  stopMonitoring() {
    this.isMonitoring = false;
    // 移除监听
    this.sessionManager?.off('sessionCreate');
    this.sessionManager?.off('sessionDestroy');
    this.sessionManager?.off('topSessionChange');
  }

  // 刷新会话列表
  private async refreshSessionList() {
    if (!this.sessionManager) return;

    try {
      // 获取所有已激活的会话
      const allSessions = await this.sessionManager.getActivatedSessionDescriptors();

      this.sessions = allSessions.map(desc => ({
        sessionId: desc.sessionId,
        tag: desc.sessionTag ?? '未知',
        type: desc.sessionType,
        isActive: true,
        title: '媒体内容',
        artist: '未知',
        isTopSession: desc.sessionId === this.topSessionId,
      }));

      console.info(`[Coordinator] 当前活跃会话: ${this.sessions.length}`);

    } catch (error) {
      console.error(`[Coordinator] 刷新失败: ${JSON.stringify(error)}`);
    }
  }

  // 向指定会话发送控制命令
  async sendControlCommand(sessionId: string, command: string) {
    if (!this.sessionManager) return;

    try {
      // 创建会话控制器
      const controller = await this.sessionManager.createController(sessionId);

      // 根据命令类型发送控制
      switch (command) {
        case 'play':
          await controller.sendControlCommand({ command: 'play' });
          break;
        case 'pause':
          await controller.sendControlCommand({ command: 'pause' });
          break;
        case 'playNext':
          await controller.sendControlCommand({ command: 'playNext' });
          break;
        case 'playPrevious':
          await controller.sendControlCommand({ command: 'playPrevious' });
          break;
        case 'stop':
          await controller.sendControlCommand({ command: 'stop' });
          break;
      }

      console.info(`[Coordinator] 已发送命令: ${command}${sessionId}`);

      // 释放控制器
      controller.destroy();

    } catch (error) {
      console.error(`[Coordinator] 发送命令失败: ${JSON.stringify(error)}`);
    }
  }

  build() {
    Scroll() {
      Column() {
        Text('多应用会话协调器')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E0E0E0')
          .margin({ bottom: 20 })

        // 监控状态
        Row() {
          Circle({ width: 12, height: 12 })
            .fill(this.isMonitoring ? '#81C784' : '#EF5350')
          Text(this.isMonitoring ? '监控中' : '未启动')
            .fontSize(14)
            .fontColor(this.isMonitoring ? '#81C784' : '#EF5350')
            .margin({ left: 8 })
          Text(`活跃会话: ${this.sessions.length}`)
            .fontSize(14)
            .fontColor('#4FC3F7')
            .margin({ left: 20 })
        }
        .margin({ bottom: 20 })

        // 会话列表
        if (this.sessions.length === 0) {
          Column() {
            Text('暂无活跃会话')
              .fontSize(16)
              .fontColor('#888888')
            Text('请打开其他媒体应用后刷新')
              .fontSize(12)
              .fontColor('#666666')
              .margin({ top: 8 })
          }
          .width('100%')
          .height(150)
          .justifyContent(FlexAlign.Center)
          .borderRadius(12)
          .backgroundColor('#16213e')
        } else {
          ForEach(this.sessions, (session: SessionSummary) => {
            Column() {
              Row() {
                // 顶级会话标记
                if (session.isTopSession) {
                  Text('👑')
                    .fontSize(16)
                }
                Column() {
                  Text(session.tag)
                    .fontSize(15)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#E0E0E0')
                  Text(`类型: ${session.type === avSession.AVSessionType.AUDIO ? '音频' : '视频'}`)
                    .fontSize(12)
                    .fontColor('#888888')
                    .margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)

                // 控制按钮
                Row() {
                  Button('⏮')
                    .onClick(() => this.sendControlCommand(session.sessionId, 'playPrevious'))
                    .width(36).height(36).backgroundColor('#1a1a2e').fontSize(14)
                  Button('⏸')
                    .onClick(() => this.sendControlCommand(session.sessionId, 'pause'))
                    .width(36).height(36).backgroundColor('#1a1a2e').fontSize(14)
                  Button('▶️')
                    .onClick(() => this.sendControlCommand(session.sessionId, 'play'))
                    .width(36).height(36).backgroundColor('#1a1a2e').fontSize(14)
                  Button('⏭')
                    .onClick(() => this.sendControlCommand(session.sessionId, 'playNext'))
                    .width(36).height(36).backgroundColor('#1a1a2e').fontSize(14)
                }
              }
            }
            .width('100%')
            .padding(15)
            .borderRadius(12)
            .backgroundColor(session.isTopSession ? '#1a2744' : '#16213e')
            .border({
              width: session.isTopSession ? 1 : 0,
              color: '#6C63FF',
            })
            .margin({ bottom: 8 })
          })
        }

        // 控制按钮
        Row() {
          Button(this.isMonitoring ? '停止监控' : '开始监控')
            .onClick(() => {
              if (this.isMonitoring) {
                this.stopMonitoring();
              } else {
                this.startMonitoring();
              }
            })
            .width(150)
            .height(50)
            .backgroundColor(this.isMonitoring ? '#EF5350' : '#6C63FF')
            .borderRadius(12)

          Button('刷新列表')
            .onClick(() => this.refreshSessionList())
            .width(100)
            .height(50)
            .backgroundColor('#16213e')
            .fontColor('#4FC3F7')
            .borderRadius(12)
            .margin({ left: 10 })
        }
        .margin({ top: 20 })
      }
      .width('100%')
      .padding(20)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0d0d1a')
  }

  aboutToDisappear() {
    this.stopMonitoring();
  }
}

四、踩坑与注意事项

4.1 常见陷阱

陷阱 现象 解决方案
忘记激活会话 锁屏/通知栏不显示媒体控制 创建后必须调用 session.activate()
不同步播放状态 锁屏按钮状态与应用不一致 每次状态变化都调用 setAVPlaybackState()
不销毁会话 应用退出后锁屏仍显示"幽灵"控制 aboutToDisappear 中调用 destroy()
元数据封面过大 封面图加载慢或 OOM 使用适当分辨率的图片,建议不超过 512×512
控制命令未注册 锁屏上某些按钮不显示 确保注册了对应的命令回调
多会话冲突 同一应用多个会话互相覆盖 同一时间只激活一个会话

4.2 会话元数据最佳实践

// 元数据设置的最佳实践
async function setBestPracticeMetadata(session: avSession.AVSession) {
  await session.setAVMetadata({
    // ★ assetId 必须全局唯一,建议使用 "应用包名_媒体ID" 格式
    assetId: 'com.example.music_track_001',

    // 标题和艺术家——这是锁屏上最显眼的信息,务必准确
    title: '歌曲名称',
    artist: '歌手名',

    // 专辑——有助于用户识别
    album: '专辑名',

    // 时长——单位是毫秒,不是秒!
    duration: 270000,  // 4分30秒 = 270000ms

    // 封面图——推荐使用 PixelMap 而非 URL
    // URL 需要网络加载,PixelMap 是本地数据,加载更快
    // mediaImage: pixelMap,

    // 上下首 ID——帮助系统 UI 显示"上一首/下一首"按钮状态
    previousAssetId: 'com.example.music_track_000',
    nextAssetId: 'com.example.music_track_002',
  });
}

4.3 会话与音频焦点的关系

AVSession 和音频焦点是两个独立但相关的机制

  • AVSession 解决的是"系统 UI 如何展示和控制媒体"
  • 音频焦点 解决的是"多个音频流谁优先播放"

最佳实践是两者配合使用

// 会话激活 + 音频焦点请求
async function startPlaybackWithFocus(
  session: avSession.AVSession,
  renderer: audio.AudioRenderer
) {
  // 1. 激活会话(让系统 UI 知道你在播放)
  await session.activate();

  // 2. 请求音频焦点(让系统音频策略知道你要播放)
  // AudioRenderer 创建时自动请求焦点

  // 3. 开始播放
  await renderer.start();

  // 4. 同步播放状态
  await session.setAVPlaybackState({
    state: avSession.PlaybackState.PLAYBACK_STATE_PLAY,
    speed: 1.0,
    position: { elapsedTime: 0, updateTime: Date.now() },
    bufferedTime: 0,
    loopMode: avSession.LoopMode.LOOP_MODE_SEQUENCE,
    isFavorite: false,
  });
}

五、HarmonyOS 6 适配

5.1 版本差异

特性 HarmonyOS 5 (API 12) HarmonyOS 6 (API 14)
锁屏控制 基础播放控制 增强:支持歌词滚动显示、进度条拖动
会话类型 AUDIO / VIDEO 新增:VOICE_CALL 通话类型
控制命令 基础命令 新增:setRating 评分命令、customCommand 自定义命令
多设备 单设备 新增:AVCastSession 投播会话,支持跨设备控制
会话分组 新增:AVSessionGroup 会话分组,多会话统一管理

5.2 迁移指南

// HarmonyOS 5 写法——基础控制命令
this.session.on('play', () => { /* 播放 */ });
this.session.on('pause', () => { /* 暂停 */ });

// HarmonyOS 6 写法——新增自定义命令和评分
// this.session.on('customCommand', (command: string, args: Record<string, Object>) => {
//   console.info(`[Session] 自定义命令: ${command}, 参数: ${JSON.stringify(args)}`);
//   switch (command) {
//     case 'add_to_playlist':
//       // 添加到播放列表
//       break;
//     case 'share':
//       // 分享当前曲目
//       break;
//   }
// });
//
// this.session.on('setRating', (rating: number) => {
//   console.info(`[Session] 评分: ${rating}`);
//   // 保存用户评分
// });
//
// // 锁屏歌词显示
// await this.session.setAVMetadata({
//   ...metadata,
//   lyric: '[00:00.00]晚风 - 周杰伦\n[00:05.00]晚风轻轻吹过...',  // LRC 格式歌词
// });

5.3 注意事项

  • HarmonyOS 6 的锁屏歌词需要 LRC 格式,不支持逐字歌词
  • customCommand 的命令字符串建议使用"应用包名.命令名"格式避免冲突
  • AVCastSession 投播会话需要设备在同一局域网内

六、总结

mindmap
  root((音频会话 AVSession))
    核心概念
      AVSession 媒体会话
      AVSessionManager 会话管理
      AVSessionController 会话控制
      会话元数据
    生命周期
      创建 createAVSession
      激活 activate
      停用 deactivate
      销毁 destroy
    控制命令
      播放/暂停/停止
      上下曲切换
      快进/快退/Seek
      速度/循环模式
      收藏切换
    锁屏媒体控制
      元数据同步
      播放状态同步
      封面图设置
      进度条更新
    多应用协调
      会话列表监听
      顶级会话管理
      跨应用控制
      焦点与会话配合
    HarmonyOS 6
      锁屏歌词
      自定义命令
      投播会话
      会话分组

    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

关键知识点回顾

  1. AVSession 是媒体应用与系统 UI 的桥梁——不创建会话,你的 App 就是"隐形的"
  2. 会话必须激活才能被系统发现和控制——创建后别忘了 activate()
  3. 播放状态必须实时同步——每次状态变化都调用 setAVPlaybackState()
  4. 应用退出时必须销毁会话——避免"僵尸会话"困扰用户
  5. AVSession 和音频焦点是两个独立机制——两者配合使用才是最佳实践

音频会话让你的媒体 App 从"自说自话"变成"融入系统生态",这是专业媒体应用的标配。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。