文档管理中心

子页面播放音频返回首页后,仍会短暂听到音频声音

问题现象

应用中子页面播放音频,点击返回首页后,仍然可以短暂地听到音频声音,声音没有立刻结束。

背景知识

  • AVPlayer:支持将Audio/Video媒体资源(比如mp4/mp3/mkv/mpeg-ts等)转码为可供渲染的图像和可听见的音频模拟信号,并通过输出设备进行播放。
  • onPageShow:router路由页面(即@Entry装饰的自定义组件)每次显示时触发一次,包括路由跳转、应用进入前台等场景。

问题定位

检查应用中AVPlayer实例调用stop接口停止音频播放的调用时机,是否在首页的onPageShow函数中停止的音频播放,问题代码如下:

收起
自动换行
深色代码主题
复制
  1. onPageShow(): void {
  2. // 应用首页停止子页面的音频播放
  3. this.avStop();
  4. }
  5. avStop(): void {
  6. if (this.avPlayer) {
  7. try {
  8. this.avPlayer.stop();
  9. console.info(`avPlayer Stop success`);
  10. } catch (e) {
  11. console.error(`avPlayer Stop failed`);
  12. }
  13. }
  14. }

分析结论

应用在首页的onPageShow函数中,调用的AVPlayer实例的stop接口停止的音频播放;而子页面从销毁到执行首页onPageShow函数中stop接口停止音频播放,有一定代码运行时间,导致返回首页后短暂的音频播放问题。

修改建议

应用在子页面的aboutToDisappear函数中,调用AVPlayer实例的stop及release接口,停止音频播放并销毁AVPlayer实例,避免返回首页后仍然有音频播放的问题,示例如下:

收起
自动换行
深色代码主题
复制
  1. aboutToDisappear() {
  2. if (this.avPlayer == null) {
  3. console.info(`${this.tag}: avPlayer has not init aboutToDisappear`);
  4. return;
  5. }
  6. try {
  7. this.avPlayer.stop();
  8. console.info(`${this.tag}: avStop==`);
  9. } catch (e) {
  10. console.error(`${this.tag}: avStop== ${JSON.stringify(e)}`);
  11. }
  12. this.avPlayer.release((err) => {
  13. if (err == null) {
  14. console.info(`${this.tag}: videoRelease release success`);
  15. } else {
  16. console.error(`${this.tag}: videoRelease release failed, error message is = ${JSON.stringify(err.message)}`);
  17. }
  18. });
  19. if (this.subFile) {
  20. try {
  21. fileIo.closeSync(this.subFile);
  22. } catch (err) {
  23. console.error(`failed to close subtitle file, ${JSON.stringify(err)}`);
  24. }
  25. }
  26. }

完整示例如下:

  1. 应用首页,点击Next会跳转到音频播放子页面。
    收起
    自动换行
    深色代码主题
    复制
    1. @Entry
    2. @Component
    3. struct Index {
    4. pathStack: NavPathStack = new NavPathStack();
    5. build() {
    6. Navigation(this.pathStack) {
    7. Row() {
    8. Column() {
    9. Text('首页')
    10. .fontSize(50)
    11. .fontWeight(FontWeight.Bold);
    12. // 添加按钮,以响应用户onClick事件
    13. Button() {
    14. Text('子页面音频')
    15. .fontSize(20)
    16. .fontWeight(FontWeight.Bold);
    17. }
    18. .type(ButtonType.Capsule)
    19. .margin({
    20. top: 50
    21. })
    22. .backgroundColor('#0D9FFB')
    23. .width('40%')
    24. .height('5%')
    25. .onClick(() => {
    26. this.pathStack.pushPathByName('PageOne', null);
    27. });
    28. }
    29. .width('100%');
    30. }
    31. .height('100%');
    32. }
    33. .title('Navigation')
    34. .mode(NavigationMode.Stack);
    35. }
    36. }
  2. 音频播放子页面,自动播放音频,点击Back会返回首页,并停止音乐播放。
    收起
    自动换行
    深色代码主题
    复制
    1. import display from '@ohos.display';
    2. import { common } from '@kit.AbilityKit';
    3. import media from '@ohos.multimedia.media';
    4. import { fileIo } from '@kit.CoreFileKit';
    5. const PROPORTION = 0.99; // 占屏幕比例
    6. const SURFACE_W = 0.9; // 表面宽比例
    7. const SURFACE_H = 1.78; // 表面高比例
    8. @Builder
    9. export function PageOneBuilder() {
    10. PageOne();
    11. }
    12. @Component
    13. struct PageOne {
    14. pathStack: NavPathStack = new NavPathStack();
    15. tag: string = 'AVPlayManager';
    16. private xComponentController: XComponentController = new XComponentController();
    17. private avPlayer: media.AVPlayer | null = null;
    18. private subFile: fileIo.File | null = null;
    19. private surfaceId: string = '';
    20. private intervalID: number = -1;
    21. private context: common.UIAbilityContext | undefined = undefined;
    22. private fileName: string = '3463094780.mp3'; // 资源需替换成用户自己的资源,否则无法成功运行
    23. private isSwiping: boolean = false; // 用户滑动过程中
    24. private xComponentFlag: boolean = false;
    25. @State surfaceW: number | null = null;
    26. @State surfaceH: number | null = null;
    27. private percent: number = 0;
    28. private windowWidth: number = 300;
    29. private windowHeight: number = 200;
    30. async msleepAsync(ms: number): Promise<boolean> {
    31. return new Promise((resolve) => {
    32. setTimeout(() => {
    33. resolve(true);
    34. }, ms);
    35. });
    36. }
    37. async avSetupVideoAndSubtitle() {
    38. // 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址。
    39. if (this.context === undefined) {
    40. return;
    41. }
    42. // this.fileName需根据应用实际情况配置对应的资源,否则会导致程序无法正常运行
    43. let fileDescriptorVideo = await this.context.resourceManager.getRawFd(this.fileName);
    44. let avFileDescriptor: media.AVFileDescriptor =
    45. { fd: fileDescriptorVideo.fd, offset: fileDescriptorVideo.offset, length: fileDescriptorVideo.length };
    46. if (this.avPlayer) {
    47. console.info(`${this.tag}: init avPlayer release2createNew`);
    48. this.avPlayer.release();
    49. await this.msleepAsync(1500);
    50. }
    51. // 创建avPlayer实例对象
    52. this.avPlayer = await media.createAVPlayer();
    53. // 创建状态机变化回调函数
    54. await this.setAVPlayerCallback((avPlayer: media.AVPlayer) => {
    55. this.percent = avPlayer.width / avPlayer.height;
    56. this.setVideoWH();
    57. });
    58. // 为fdSrc赋值触发initialized状态机上报
    59. this.avPlayer.fdSrc = avFileDescriptor;
    60. }
    61. avPlay(): void {
    62. if (this.avPlayer) {
    63. try {
    64. this.avPlayer.play();
    65. } catch (e) {
    66. console.error(`${this.tag}: avPlay = ${JSON.stringify(e)}`);
    67. }
    68. }
    69. }
    70. // 注册avplayer回调函数
    71. async setAVPlayerCallback(callback: (avPlayer: media.AVPlayer) => void): Promise<void> {
    72. // seek操作结果回调函数
    73. if (this.avPlayer == null) {
    74. console.error(`${this.tag}: avPlayer has not init!`);
    75. return;
    76. }
    77. this.avPlayer.on('seekDone', (seekDoneTime) => {
    78. console.info(`${this.tag}: setAVPlayerCallback AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
    79. });
    80. this.avPlayer.on('speedDone', (speed) => {
    81. console.info(`${this.tag}: setAVPlayerCallback AVPlayer speedDone, speed is ${speed}`);
    82. });
    83. // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
    84. this.avPlayer.on('error', (err) => {
    85. console.error(`${this.tag}: setAVPlayerCallback Invoke avPlayer failed ${JSON.stringify(err)}`);
    86. if (this.avPlayer == null) {
    87. console.error(`${this.tag}: avPlayer has not init on error`);
    88. return;
    89. }
    90. this.avPlayer.reset();
    91. });
    92. // 状态机变化回调函数
    93. this.avPlayer.on('stateChange', async (state) => {
    94. if (this.avPlayer == null) {
    95. console.info(`${this.tag}: avPlayer has not init on state change`);
    96. return;
    97. }
    98. switch (state) {
    99. case 'idle': // 成功调用reset接口后触发该状态机上报
    100. console.info(`${this.tag}: setAVPlayerCallback AVPlayer state idle called.`);
    101. break;
    102. case 'initialized': // avplayer设置播放源后触发该状态上报
    103. console.info(`${this.tag}: setAVPlayerCallback AVPlayer state initialized called.`);
    104. if (this.surfaceId) {
    105. this.avPlayer.surfaceId = this.surfaceId; // 设置显示画面,当播放的资源为纯音频时无需设置
    106. console.info(`${this.tag}: setAVPlayerCallback this.avPlayer.surfaceId = ${this.avPlayer.surfaceId}`);
    107. this.avPlayer.prepare();
    108. }
    109. break;
    110. case 'prepared': // prepare调用成功后上报该状态机
    111. console.info(`${this.tag}: setAVPlayerCallback AVPlayer state prepared called.`);
    112. this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
    113. console.info(`${this.tag}: bufferingUpdate called, infoType value: ${infoType}, value:${value}}`);
    114. });
    115. this.avPlayer.play(); // 调用播放接口开始播放
    116. callback(this.avPlayer);
    117. break;
    118. case 'playing': // play成功调用后触发该状态机上报
    119. console.info(`${this.tag}: setAVPlayerCallback AVPlayer state playing called.`);
    120. if (this.intervalID !== -1) {
    121. clearInterval(this.intervalID);
    122. }
    123. break;
    124. case 'completed': // 播放结束后触发该状态机上报
    125. console.info(`${this.tag}: setAVPlayerCallback AVPlayer state completed called.`);
    126. if (this.intervalID !== -1) {
    127. clearInterval(this.intervalID);
    128. }
    129. this.avPlayer.off('bufferingUpdate');
    130. break;
    131. case 'released':
    132. console.info(`${this.tag}: setAVPlayerCallback released called.`);
    133. break;
    134. case 'stopped':
    135. console.info(`${this.tag}: setAVPlayerCallback AVPlayer state stopped called.`);
    136. break;
    137. case 'error':
    138. console.error(`${this.tag}: setAVPlayerCallback AVPlayer state error called.`);
    139. break;
    140. case 'paused':
    141. console.info(`${this.tag}: setAVPlayerCallback AVPlayer state paused called.`);
    142. break;
    143. default:
    144. console.info(`${this.tag}: setAVPlayerCallback AVPlayer state unknown called.`);
    145. break;
    146. }
    147. });
    148. }
    149. async aboutToAppear() {
    150. this.windowWidth = display.getDefaultDisplaySync().width;
    151. this.windowHeight = display.getDefaultDisplaySync().height;
    152. this.surfaceW = this.windowWidth * SURFACE_W;
    153. this.surfaceH = this.surfaceW / SURFACE_H;
    154. this.context = this.getUIContext().getHostContext() as common.UIAbilityContext;
    155. // 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址。
    156. if (this.context === undefined) {
    157. return;
    158. }
    159. // this.fileName需根据应用实际情况配置对应的资源,否则会导致程序无法正常运行
    160. let fileDescriptorVideo = await this.context.resourceManager.getRawFd(this.fileName);
    161. let avFileDescriptor: media.AVFileDescriptor =
    162. { fd: fileDescriptorVideo.fd, offset: fileDescriptorVideo.offset, length: fileDescriptorVideo.length };
    163. if (this.avPlayer) {
    164. console.info(`${this.tag}: init avPlayer release2createNew`);
    165. this.avPlayer.release();
    166. await this.msleepAsync(1500);
    167. }
    168. // 创建avPlayer实例对象
    169. this.avPlayer = await media.createAVPlayer();
    170. // 创建状态机变化回调函数
    171. await this.setAVPlayerCallback((avPlayer: media.AVPlayer) => {
    172. this.percent = avPlayer.width / avPlayer.height;
    173. this.setVideoWH();
    174. });
    175. // 为fdSrc赋值触发initialized状态机上报
    176. this.avPlayer.fdSrc = avFileDescriptor;
    177. }
    178. aboutToDisappear() {
    179. if (this.avPlayer == null) {
    180. console.info(`${this.tag}: avPlayer has not init aboutToDisappear`);
    181. return;
    182. }
    183. try {
    184. this.avPlayer.stop();
    185. console.info(`${this.tag}: avStop==`);
    186. } catch (e) {
    187. console.error(`${this.tag}: avStop== ${JSON.stringify(e)}`);
    188. }
    189. this.avPlayer.release((err) => {
    190. if (err == null) {
    191. console.info(`${this.tag}: videoRelease release success`);
    192. } else {
    193. console.error(`${this.tag}: videoRelease release failed, error message is = ${JSON.stringify(err.message)}`);
    194. }
    195. });
    196. if (this.subFile) {
    197. try {
    198. fileIo.closeSync(this.subFile);
    199. } catch (err) {
    200. console.error(`failed to close subtitle file, ${JSON.stringify(err)}`);
    201. }
    202. }
    203. }
    204. setVideoWH(): void {
    205. if (this.percent >= 1) { // 横向视频
    206. this.surfaceW = Math.round(this.windowWidth * PROPORTION);
    207. this.surfaceH = Math.round(this.surfaceW / this.percent);
    208. } else { // 纵向视频
    209. this.surfaceH = Math.round(this.windowHeight * PROPORTION);
    210. this.surfaceW = Math.round(this.surfaceH * this.percent);
    211. }
    212. }
    213. @Builder
    214. CoverXComponent() {
    215. XComponent({
    216. // 装载视频容器
    217. id: 'xComponent',
    218. type: XComponentType.SURFACE,
    219. controller: this.xComponentController
    220. })
    221. .id('VideoView')
    222. .visibility(this.xComponentFlag ? Visibility.Visible : Visibility.Hidden)
    223. .onLoad(() => {
    224. this.surfaceId = this.xComponentController.getXComponentSurfaceId();
    225. })
    226. .height(`${this.surfaceH}px`)
    227. .width(`${this.surfaceW}px`);
    228. }
    229. build() {
    230. NavDestination() {
    231. Column() {
    232. Row() {
    233. Button() {
    234. Text('返回首页')
    235. .fontSize(30)
    236. .fontWeight(FontWeight.Bold);
    237. }
    238. .type(ButtonType.Capsule)
    239. .margin({
    240. top: 20
    241. })
    242. .backgroundColor('#0D9FFB')
    243. .width('40%')
    244. .height('5%')
    245. .onClick(() => {
    246. this.pathStack.clear();
    247. });
    248. };
    249. Stack() {
    250. Column() {
    251. this.CoverXComponent();
    252. }
    253. .align(Alignment.TopStart)
    254. .margin({ top: 80 })
    255. .id('VideoView')
    256. .justifyContent(FlexAlign.Center);
    257. Text()
    258. .height(`${this.surfaceH}px`)
    259. .width(`${this.surfaceW}px`)
    260. .margin({ top: 80 })
    261. .backgroundColor(Color.Black)
    262. .visibility(this.isSwiping ? Visibility.Visible : Visibility.Hidden);
    263. }
    264. .backgroundColor(Color.Black)
    265. .height('90%')
    266. .width('100%');
    267. }.backgroundColor(Color.Black)
    268. .height('100%')
    269. .width('100%');
    270. }.title('PageOne')
    271. .onReady((context: NavDestinationContext) => {
    272. this.pathStack = context.pathStack;
    273. });
    274. }
    275. }
在 FAQ 中进行搜索
请输入您想要搜索的关键词