鸿蒙app开发·后台长任务与本地媒体服务

①后台长时任务

通过backgroundTaskManager的startBackgroundRunning接口,通过ability上下文,任务类型(音视频、语音等)、定义动作属性(拉起的ability)开启后台任务,并用backgroundTaskManager.stopBackgroundRunning结束后台任务。

note:需配合本地媒体会话使用。

封装:

//后台任务,backgroundTask.ets

import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { common, wantAgent, WantAgent } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { avSession as AVSessionManager } from '@kit.AVSessionKit';
import { AVPlayerVideo } from './AVPlayerVideo';

// 后台长时任务管理类
export class BackgroundTask {
  //  上下文
  private context: Context;
  // 是否正在运行
  private isRunning: boolean = false;

  // 构造函数
  constructor(context: Context) {
    this.context = context;
  }

  // 后台任务开始
  async startBackgroundTask() {
    if (this.isRunning) {
      console.log('后台任务已启动,无需重复启动');
      return;
    }

    const cmm = this.context as common.UIAbilityContext
    //通知参数,用于指定点击长时任务通知后跳转的界面
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      // 添加需要被拉起应用的bundleName和abilityName、模块名
      wants: [{
        bundleName: cmm.abilityInfo.bundleName,
        abilityName: cmm.abilityInfo.name,
        moduleName: cmm.abilityInfo.moduleName,
      }],
      // 指定点击通知栏消息后的动作是拉起ability
      actionType: wantAgent.OperationType.START_ABILITY, //abilityWantAgent.OperationType
      // 私有属性
      requestCode: 0,
      // 动作的具体属性
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG],
    };

    //长时任务的类型,当前为音视频播放。
    const taskType: backgroundTaskManager.BackgroundMode = backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK;
    //通过wantAgent模块下getWantAgent方法获取WantAgent对象
    const wantAgentOBJ: WantAgent = await wantAgent.getWantAgent(wantAgentInfo);
    // 开始后台任务
    backgroundTaskManager.startBackgroundRunning(this.context, taskType, wantAgentOBJ).then((result) => {
      //成功的逻辑
      this.isRunning = true;
    }).catch((error: BusinessError) => {
      //失败的逻辑
      console.error(`Failed to Operation startBackgroundRunning. code is ${error.code} message is ${error.message}`);
      this.isRunning = false;
    });
  }

  // 后台任务结束
  async stopBackgroundTask() {
    await backgroundTaskManager.stopBackgroundRunning(this.context)
    this.isRunning = false;
  }
}

②本地媒体会话

通过avSession这个会话管理模块定义好会话名字、类型、上下文后,调用createAVSession方法创建会话,并使用activate激活会话。添加对应的监听回调后,状态栏的对应的功能图标会亮起,可在状态监听中通过avplayer来控制播放,实现控制效果。

封装(需在播放完成后暂时暂停会话)

//本地媒体会话管理器
//本地媒体会话管理器
// 使用案例
// const sessionManager = new SessionManager(player);
// await sessionManager.play();   // 自动激活并播放
// await sessionManager.pause();  // 暂停
// await sessionManager.stop();   // 停止播放
// await sessionManager.endSession(); // 销毁
export class SessionManager {
  private context: Context; // 上下文对象,通常为 UIAbility 上下文
  private session?: AVSessionManager.AVSession; // 媒体会话对象
  private player?: AVPlayerVideo; // 媒体播放器实例
  private playState: AVSessionManager.PlaybackState = AVSessionManager.PlaybackState.PLAYBACK_STATE_INITIAL; // 当前播放状态
  private isRunning: boolean = false; // 会话是否已激活
  private currentPostion: number = 0; // 当前播放进度(单位:毫秒)
  private total: number = 100000000; // 媒体总时长(单位:毫秒)
  // 播放下一首/上一首的回调
  public onPlayNext?: () => void;
  public onPlayPrevious?: () => void;
  //元数据
  private metaData: AVSessionManager.AVMetadata|undefined;

  constructor(player: AVPlayerVideo, context?: Context) {
    this.player = player;
    this.context = context ?? getContext(this);
    this.init(); // 初始化媒体会话
  }

  /**
   * 初始化方法:创建媒体会话,设置元数据、状态、监听器等
   */
  private async init() {
    await this.createSession(); // 创建媒体会话
    // await this.setMetadata(); // 设置媒体信息
    await this.setPlaybackState(); // 初始化播放状态
    this.setListenerForMesFromController(); // 设置控制器消息监听
    this.setLaunchAbility(); // 设置拉起能力
  }

  /**
   * 创建媒体会话
   */
  private async createSession() {
    const sessionType: AVSessionManager.AVSessionType = 'video';
    const sessionName = 'SESSION_NAME';
    this.session = await AVSessionManager.createAVSession(this.context, sessionName, sessionType);
    console.info(`Session created: ID = ${this.session.sessionId}`);
  }

  /**
   * 设置元数据(由外部传入)(媒体信息)
   */
  public async setMetadata(metadata: AVSessionManager.AVMetadata) {
    this.metaData = metadata;
    await this.session?.setAVMetadata(metadata);
  }

  /**
   * 设置当前播放状态到媒体会话(包含播放进度)
   */
  private async setPlaybackState() {
    const playbackState: AVSessionManager.AVPlaybackState = {
      state: this.playState,
      isFavorite: false,
      speed: 1,
      duration: this.total, // 单位:秒
      activeItemId: 0,
      position: {
        elapsedTime: this.currentPostion, // 当前播放时间(秒)
        updateTime: Date.now() // 更新时间戳(毫秒)
      }
    };
    await this.session?.setAVPlaybackState(playbackState);
  }

  /**
   * 当前进度由页面传递来,更新当前播放进度
   */
  public updateCurrentPosition(time: number) {
    this.currentPostion = time;
    this.setPlaybackState();
  }

  /**
   * 总进度由页面传递
   */
  public updateTotalTime(time: number) {
    this.total = time;
    console.log('this is total:',this.total)
    this.setPlaybackState();
    //  更新元数据
    this.metaData!.duration  = time;
    this.setMetadata(this.metaData!)
    // 重置当前播放进度
    this.currentPostion = 0;
  }

  /**
   * 激活媒体会话(如果尚未激活)
   */
  async activateSession() {
    if (!this.isRunning && this.session) {
      try {
        await this.session.activate();
        this.isRunning = true;
        console.info('Session activated.');
      } catch (err) {
        console.error('Failed to activate session:', err);
      }
    }
  }

  /**
   * 播放媒体
   */
  async play() {
    await this.activateSession();
    this.player?.play();
    this.playState = AVSessionManager.PlaybackState.PLAYBACK_STATE_PLAY;
    await this.setPlaybackState(); // 同步播放状态
  }

  /**
   * 暂停播放
   */
  async pause() {
    this.player?.pause();
    this.playState = AVSessionManager.PlaybackState.PLAYBACK_STATE_PAUSE;
    await this.setPlaybackState();
  }

  /**
   * 停止播放
   */
  async stop() {
    this.player?.stop();
    this.playState = AVSessionManager.PlaybackState.PLAYBACK_STATE_STOP;
    await this.setPlaybackState();
  }

  /**
   * 跳转到指定播放时间
   */
  async seek(time: number) {
    this.player?.avPlayerSeek(time);
    this.playState = AVSessionManager.PlaybackState.PLAYBACK_STATE_PLAY;
    await this.setPlaybackState();
  }

  /**
   * 控制器消息监听(来自系统或外设的控制行为)
   */
  private setListenerForMesFromController() {
    if (!this.session) {
      return;
    }

    this.session.on('play', async () => {
      console.info(`on play`);
      await this.play();
      console.info('this is op:',JSON.stringify(this.currentPostion))
    });

    this.session.on('pause', async () => {
      console.info(`on pause`);
      await this.pause();
    });

    this.session.on('stop', async () => {
      console.info(`on stop`);
      await this.stop();
    });

    this.session.on('seek', (time: number) => {
      console.info(`[SessionManager] 接收到 seek 指令,时间点: ${time}`);
      this.player?.avPlayerSeek(time); // 媒体跳转
    });

    // ✅ 新增:下一首
    this.session.on('playNext', () => {
      console.info('[SessionManager] 接收到 playNext 指令');
      // 通知 UI 调用播放下一首
      // 方式一:通过回调(推荐)
      this.onPlayNext?.();
    });

    // ✅ 新增:上一首
    this.session.on('playPrevious', () => {
      console.info('[SessionManager] 接收到 playPrevious 指令');
      this.onPlayPrevious?.();
    });

    //   ✅ 新增:快进3秒
    this.session.on('fastForward', () => {
      let newTime = this.currentPostion + 3000;
      this.player!.avPlayerSeek(newTime);
      this.updateCurrentPosition(newTime);
    });

    // ✅ 新增:快退3秒
    this.session.on('rewind', () => {
      let newTime = this.currentPostion - 3000;
      if (newTime < 0) newTime = 0;
      this.player!.avPlayerSeek(newTime);
      this.updateCurrentPosition(newTime);
    });

    // ✅ 新增:设置播放速度
    this.session.on('setSpeed', (speed) => {
      console.info(`on setSpeed , the speed is ${speed}`);
      // do some tasks ···
    });
    // ✅ 新增:设置循环模式
    this.session.on('setLoopMode', (mode) => {
      console.info(`on setLoopMode , the loop mode is ${mode}`);
    });
    // ✅ 新增:切换收藏状态
    this.session.on('toggleFavorite', (assetId) => {
      console.info(`on toggleFavorite , the target asset Id is ${assetId}`);
    });

  }

  /**
   * 解绑控制器监听器(用于清理)
   */
  async unsetListenerForMesFromController() {
    this.session?.off('play');
    this.session?.off('pause');
    this.session?.off('stop');
    this.session?.off('playNext');
    this.session?.off('playPrevious');
    this.session?.off('fastForward');
    this.session?.off('rewind');
    this.session?.off('seek');
    this.session?.off('setSpeed');
    this.session?.off('setLoopMode');
    this.session?.off('toggleFavorite');
  }

  /**
   * 设置会话拉起能力(锁屏或外设点击后拉起 App)
   */
  private setLaunchAbility() {
    if (!this.context) {
      return;
    }

    const ctx = this.context as common.UIAbilityContext;

    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [{
        bundleName: ctx.abilityInfo.bundleName,
        abilityName: ctx.abilityInfo.name,
        moduleName: ctx.abilityInfo.moduleName,
      }],
      operationType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };

    wantAgent.getWantAgent(wantAgentInfo).then(agent => {
      this.session?.setLaunchAbility(agent);
    }).catch(() => {
      AlertDialog.show({ message: '会话拉起窗口失败', alignment: DialogAlignment.Center });
    });
  }

  /**
   * 销毁媒体会话
   */
  async endSession() {
    await this.session?.destroy();
    this.isRunning = false;
  }

  /**
   * 禁用媒体会话
   */
  async disableSession() {
    await this.session?.deactivate();
    this.isRunning = false;
  }
}

视频播放与通知栏联动实现

// videoPlayerPage.ets

import { AVPlayerVideo } from './AVPlayerVideo';
import { BackgroundTask, SessionManager } from './backgroundTask';
import { avSession as AVSessionManager } from '@kit.AVSessionKit';

@Entry
@Component
struct VideoPlayerPage {
  private player: AVPlayerVideo = new AVPlayerVideo();
  private xComponentController: XComponentController = new XComponentController();
  //当前的播放进度
  @State currentTime: number = 0;
  //总播放时长
  @State totalTime: number = 0;
  //后台任务类
  backgroundTask = new BackgroundTask(getContext(this));
  //会话类,需要传入上下文与播放的控制类
  sessionManager = new SessionManager(this.player, getContext(this));
  // 作品信息列表
  @State videoList: AVSessionManager.AVMetadata[] = [
    {
      assetId: "song001",
      title: "晴天",
      artist: "周杰伦",
      subtitle: "叶惠美",
      mediaImage: "https://i0.hdslb.com/bfs/archive/d2b0b68b1e0c45853db463a1e1c4f04002a102fc.jpg",
      description: "经典情歌",
      lyric: "[00:00.00]晴天 - 周杰伦\n[00:15.00]故事的小黄花 从出生那年就飘着..."
    },
    {
      assetId: "song002",
      title: "七里香",
      artist: "周杰伦",
      subtitle: "七里香",
      mediaImage: "https://i0.hdslb.com/bfs/archive/d2b0b68b1e0c45853db463a1e1c4f04002a102fc.jpg",
      description: "经典情歌",
      lyric: "[00:00.00]七里香 - 周杰伦\n[00:15.00]故事的小黄花 从出生那年就飘着..."
    }
  ];
  // 相关作品地址
  @State url:string[] = [
    'https://consumer.huawei.com/content/dam/huawei-cbg-site/cn/mkt/pdp/phones/pura-x/video/kv-popup.mp4',
    'https://v-cdn.zjol.com.cn/276985.mp4'
  ];

  // 当前播放索引
  @State currentIndex: number = 0;

  // 加载当前播放视频(封装)
  private async loadCurrentVideo(surfaceId: string) {
    // 停止播放,并更新会话状态
    this.player.stop();
    // 加载当前播放视频
    await this.player.init(surfaceId, this.url[this.currentIndex], true);
    // 获取视频时长,并更新会话状态
    this.player.getDuration((time) => {
      this.totalTime = time;
      this.sessionManager.updateTotalTime(time);
      //更新元数据信息
      if (this.videoList[this.currentIndex].duration === undefined) this.videoList[this.currentIndex].duration = this.totalTime;
    });
    // 获取当前播放进度,并更新会话状态
    this.player.getCurrentPosition((time) => {
      this.currentTime = time;
      this.sessionManager.updateCurrentPosition(time);
    });
    console.log('元数据信息 :',JSON.stringify(this.videoList))
    // 设置媒体元数据
    await this.sessionManager.setMetadata(this.videoList[this.currentIndex]);

    // 设置播放完成回调
    this.player.setOnPlaybackCompleted(async () => {
      //  播放完成逻辑区,播放下一首

      this.playNext(this.xComponentController.getXComponentSurfaceId());

    });
  }

  //  播放下一首
  private async playNext(surfaceId: string) {
    this.currentIndex = (this.currentIndex + 1) % this.videoList.length;
    await this.loadCurrentVideo(surfaceId);
    await this.sessionManager.pause()
  }

  //  播放上一首
  private async playPrevious(surfaceId: string) {
    this.currentIndex = (this.currentIndex - 1 + this.videoList.length) % this.videoList.length;
    await this.loadCurrentVideo(surfaceId);
    await this.sessionManager.pause()
  }

  build() {
    Column({ space: 20 }) {

      XComponent({
        id: 'video_surface',
        type: 'surface',
        controller: this.xComponentController
      })
        .width('100%')
        .height(300)
        .onLoad(async () => {
          // 获取 XComponent 的 Surface ID
          const surfaceId = this.xComponentController.getXComponentSurfaceId();
          //  加载当前播放视频
          this.loadCurrentVideo(surfaceId)

          // 注册回调给系统媒体控制栏用
          this.sessionManager.onPlayNext = async () => {
            await this.playNext(surfaceId);
          };
          this.sessionManager.onPlayPrevious = async () => {
            await this.playPrevious(surfaceId);
          };
        });

      Slider({
        value: this.currentTime / 1000,
        min: 0,
        max: this.totalTime / 1000,
        step: 1,
        style: SliderStyle.OutSet  // 设置样式为 OutSet(有凸起效果)
      })
        .blockColor('#4A90E2')// 滑块颜色
        .trackColor('#E0E0E0')// 轨道背景色
        .selectedColor('#4A90E2')// 已填充进度颜色
        .onChange((value: number) => {
          // 跳转到指定时间
          this.player.avPlayerSeek(value * 1000)
          //  更新会话状态
          this.sessionManager.seek(value * 1000)
        })
        .width('90%')

      Row({ space: 10 }) {
        Button('播放')
          .onClick(() => {
            this.player.play();
            // 启动后台任务
            this.backgroundTask.startBackgroundTask()
            // 启动本地媒体会话
            this.sessionManager.play()
          });
        Button('暂停')
          .onClick(() => {
            // 暂停播放,并更新会话状态
            this.player.pause();
            this.sessionManager.pause()
          });
        Button('快进5秒')
          .onClick(async () => {
            this.player.avPlayerSeek(this.currentTime + 5000); // 跳转到5秒(也可以传 currentTime + 5)
          });
      }

      Button('停止')
        .backgroundColor('#ff4444')
        .onClick(() => {
          this.player.stop();
          this.player.closeFile();
        });
      Button('下一个')
        .onClick(() => {
          this.playNext(this.xComponentController.getXComponentSurfaceId());
          //
        });
      Button('上一个')
        .onClick(() => {
          this.playPrevious(this.xComponentController.getXComponentSurfaceId());
          // 更新会话状态

        });

    }
    .padding(20);
  }
}

播放class如下

/// 封装 AVPlayer 播放器,用于视频播放功能
// 需配合组件 XComponent 使用

import { media } from '@kit.MediaKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class AVPlayerVideo {
  private surfaceID: string = '';
  private isSeek: boolean = true;
  private fileSize: number = -1;
  private fd: number = 0;
  private avPlayer: media.AVPlayer | undefined = undefined;
  private file: fs.File | undefined;

  public PathUrl: string = '';
  // 播放完成回调
  private playbackCompletedCallback: (() => void) | null = null;

  // 设置播放完成回调
  setOnPlaybackCompleted(callback: () => void) {
    this.playbackCompletedCallback = callback;
  }

  // 初始化播放器,支持本地路径和网络 URL
  async init(surfaceID: string, url: string, isSeek?: boolean) {
    try {
      if (this.avPlayer) {
        this.avPlayer.release();
        this.avPlayer = undefined;
      }

      this.avPlayer = await media.createAVPlayer();
      this.surfaceID = surfaceID;
      this.isSeek = isSeek ?? true;

      if (url.startsWith('http')) {
        this.PathUrl = url;
      } else {
        this.file = await fs.open(url, fs.OpenMode.READ_ONLY);
        this.PathUrl = 'fd://' + this.file.fd;
      }

      this.avPlayer.url = this.PathUrl;
      this.setAVPlayerCallback(this.avPlayer);

    } catch (e) {
      console.error('初始化播放器失败: ' + JSON.stringify(e));
    }
  }

  // 设置 AVPlayer 的各种回调事件
  private setAVPlayerCallback(avPlayer: media.AVPlayer) {
    avPlayer.on('startRenderFrame', () => {
      console.info('开始渲染视频帧');
    });

    avPlayer.on('seekDone', (seekDoneTime: number) => {
      console.info(`定位完成,时间点: ${seekDoneTime}`);
    });

    avPlayer.on('error', async (err: BusinessError) => {
      console.error(`AVPlayer 错误 [${err.code}]: ${err.message}`);
      await avPlayer.reset();
      await avPlayer.release();
    });

    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
      console.info(`状态变更: ${state}, 原因: ${reason}`);
      switch (state) {
        case 'initialized':
          avPlayer.surfaceId = this.surfaceID;
          await avPlayer.prepare();
          break;
        case 'prepared':
          break;
        case 'completed':
          // 播放完成回调
          this.playbackCompletedCallback!();
          break;
        case 'stopped':
          await avPlayer.reset();
          await avPlayer.release();
          break;
      }
    });
  }

  // 手动播放
  public play() {
    this.avPlayer?.play();
  }

  // 手动暂停
  public pause() {
    this.avPlayer?.pause();
  }

  // 手动停止并释放资源
  public stop() {
    this.avPlayer?.stop();
    this.avPlayer?.release();
  }

  // 获取当前播放进度(回调)
  public getCurrentPosition(callback: (time: number) => void) {
    this.avPlayer?.on('timeUpdate', callback);
  }

  // 获取总时长(一次性回调)
  public getDuration(callback: (time: number) => void) {
    this.avPlayer?.on('durationUpdate', callback);
  }

  // 跳转播放位置(单位:秒)
  async avPlayerSeek(time: number): Promise<void> {
    if (!this.avPlayer) throw new Error('avPlayer 未初始化');
    if (!this.isSeek) throw new Error('当前资源不支持 seek');
    this.avPlayer.seek(time, media.SeekMode.SEEK_CLOSEST);
  }

  // 设置播放速度
  public setSpeed(speed: number) {
    this.avPlayer?.setSpeed(speed);
  }

  // 设置播放比特率,仅对网络流有效(HLS/DASH)
  public setBitrate(bitrate: number) {
    this.avPlayer?.setBitrate(bitrate);
  }

  // 设置音量(0.0 - 1.0)
  public setVolume(volume: number) {
    this.avPlayer?.setVolume(volume);
  }

  // 关闭本地文件句柄
  public closeFile() {
    if (this.file) {
      fs.close(this.file);
    }
  }

  // 示例:预下载设置(未实现)
  async preDownloadDemo() {
    try {
      const source = media.createMediaSourceWithUrl("http://xxx", { "User-Agent": "User-Agent-Value" });
      const strategy: media.PlaybackStrategy = {
        preferredWidth: 1,
        preferredHeight: 2,
        preferredBufferDuration: 3,
        preferredHdr: false
      };
      this.avPlayer?.setMediaSource(source, strategy);
    } catch (e) {
      console.error('预下载设置失败: ' + JSON.stringify(e));
    }
  }
}

后台长时任务(单独简单版)


/**
 * @Author:
 * @Date: 2025.11.01
 * @Description: backgroundTask.ets后台任务
 */
import { bundleManager, wantAgent } from '@kit.AbilityKit'
import { backgroundTaskManager } from '@kit.BackgroundTasksKit'
import common from '@ohos.app.ability.common';

class BackgroundRunningManager {
  // 申请长时任务
  async startBackgroundRunning(ctx: Context | undefined) {
    const context = ctx ?? getContext(this) as common.UIAbilityContext;
    // 获取 bundle 应用信息
    const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
    // 通过wantAgent模块下getWantAgent方法获取WantAgent对象
    const wantAgentObj = await wantAgent.getWantAgent({
      // 添加需要被拉起应用的bundleName和abilityName
      wants: [{ bundleName: bundleInfo.name, abilityName: "EntryAbility" }],
      // 使用者自定义的一个私有值
      requestCode: 0,
    })
    // 重点2: 创建后台任务
    await backgroundTaskManager.startBackgroundRunning(context,
      backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj)
  }

  // 停止后台任务
  async stopBackgroundRunning() {
    backgroundTaskManager.stopBackgroundRunning(getContext())
  }
}

export const backgroundRunningManagerClass = new BackgroundRunningManager()

note:1、需要申请权限:"requestPermissions": [
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
}
],
2、声明后台模式类型。
在module.json5配置文件中abilities下的backgroundModes字段里,为需要使用长时任务的UIAbility声明相应的长时任务类型,配置文件中填写长时任务类型的配置项。

"module": {
    "abilities": [
        {
           "backgroundModes": [
           // 长时任务类型的配置项
           "audioRecording",
           "bluetoothInteraction",
           "audioPlayback"
           ]
        }
    ],
    // ...
}

posted @ 2025-05-04 20:06  带头大哥d小弟  阅读(5)  评论(0)    收藏  举报