当前位置: 首页 > news >正文

cocos打包web - ios设备息屏及前后台切换音频播放问题

  1. 切换前台时,延迟暂停与恢复能解决大部分(95%)ios平台前后台切换后音频无法恢复的问题;
if (cc.sys.isBrowser && cc.sys.os === cc.sys.OS_IOS && cc.sys.isMobile) {cc.game.on(cc.game.EVENT_GAME_INITED, () => {cc.game.on(cc.game.EVENT_SHOW, () => {setTimeout(() => {audioContext.suspend();}, 50);setTimeout(() => {audioContext.resume();}, 100);});});
}
  1. 如果还是无法恢复,重新播放音频时,先暂停一次所有音频,然后在恢复所有音频(重写CCAudio.js);
/* 音频重写【CCAudio.js部分重写】* @Description: 主要用于解决IOS音频异常(切后台后无声音),需在Creator编辑器内设置为插件* @Author: vcom_ls 2670813470@qq.com* @Date: 2025-02-28 10:48:08* @LastEditors: vcom_ls 2670813470@qq.com* @LastEditTime: 2025-03-04 17:55:14* @FilePath: \MyVcom\assets\CC\CCAudioManager\AudioOverriding.js* @Copyright (c) 2025 by vcom_ls, All Rights Reserved.*/let touchBinded = false;
let touchPlayList = [//{ instance: Audio, offset: 0, audio: audio }
];
cc._Audio.prototype._createElement = function () {let elem = this._src._nativeAsset;if (elem instanceof HTMLAudioElement) {// Reuse dom audio elementif (!this._element) {this._element = document.createElement('audio');}this._element.src = elem.src;} else {this._element = new WebAudioElement(elem, this);}
};
cc._Audio.play = function () {let self = this;this._src &&this._src._ensureLoaded(function () {// marked as playing so it will playOnLoadself._state = 1;// TODO: move to audio event listenersself._bindEnded();let playPromise = self._element.play();// dom audio throws an error if pause audio immediately after playingif (window.Promise && playPromise instanceof Promise) {playPromise.catch(function (err) {// do nothing});}self._touchToPlay();});
};
cc._Audio._touchToPlay = function () {// # same start// if (this._src && this._src.loadMode === LoadMode.DOM_AUDIO && this._element.paused) {if (this._src && this._src.loadMode === 0 && this._element.paused) {touchPlayList.push({ instance: this, offset: 0, audio: this._element });}// # same endif (touchBinded) return;touchBinded = true;let touchEventName = 'ontouchend' in window ? 'touchend' : 'mousedown';// Listen to the touchstart body event and play the audio when necessary.cc.game.canvas.addEventListener(touchEventName, function () {let item;while ((item = touchPlayList.pop())) {item.audio.play(item.offset);}});
};
cc._Audio.stop = function () {let self = this;this._src &&this._src._ensureLoaded(function () {self._element.pause();self._element.currentTime = 0;// remove touchPlayListfor (let i = 0; i < touchPlayList.length; i++) {if (touchPlayList[i].instance === self) {touchPlayList.splice(i, 1);break;}}self._unbindEnded();self.emit('stop');self._state = 3;});
};let TIME_CONSTANT;
if (cc.sys.browserType === cc.sys.BROWSER_TYPE_EDGE || cc.sys.browserType === cc.sys.BROWSER_TYPE_BAIDU || cc.sys.browserType === cc.sys.BROWSER_TYPE_UC) {TIME_CONSTANT = 0.01;
} else {TIME_CONSTANT = 0;
}
// Encapsulated WebAudio interface
let WebAudioElement = function (buffer, audio) {this._audio = audio;this._context = cc.sys.__audioSupport.context;this._buffer = buffer;this._gainObj = this._context['createGain']();this.volume = 1;this._gainObj['connect'](this._context['destination']);this._loop = false;// The time stamp on the audio time axis when the recording begins to play.this._startTime = -1;// Record the currently playing 'Source'this._currentSource = null;// Record the time has been playedthis.playedLength = 0;this._currentTimer = null;this._endCallback = function () {if (this.onended) {this.onended(this);}}.bind(this);
};let isHide = false; // 是否切换后台
(function (proto) {proto.play = function (offset) {// # add startif (isHide && cc.sys.isBrowser && cc.sys.os === cc.sys.OS_IOS && cc.sys.isMobile) {isHide = false;cc.sys.__audioSupport.context.suspend();}// # add end// If repeat play, you need to stop before an audioif (this._currentSource && !this.paused) {this._currentSource.onended = null;this._currentSource.stop(0);this.playedLength = 0;}let audio = this._context['createBufferSource']();audio.buffer = this._buffer;audio['connect'](this._gainObj);audio.loop = this._loop;this._startTime = this._context.currentTime;offset = offset || this.playedLength;if (offset) {this._startTime -= offset;}let duration = this._buffer.duration;let startTime = offset;let endTime;if (this._loop) {if (audio.start) audio.start(0, startTime);else if (audio['notoGrainOn']) audio['noteGrainOn'](0, startTime);else audio['noteOn'](0, startTime);} else {endTime = duration - offset;if (audio.start) audio.start(0, startTime, endTime);else if (audio['noteGrainOn']) audio['noteGrainOn'](0, startTime, endTime);else audio['noteOn'](0, startTime, endTime);}this._currentSource = audio;audio.onended = this._endCallback;// If the current audio context time stamp is 0 and audio context state is suspended// There may be a need to touch events before you can actually start playing audioif ((!audio.context.state || audio.context.state === 'suspended') && this._context.currentTime === 0) {let self = this;clearTimeout(this._currentTimer);this._currentTimer = setTimeout(function () {if (self._context.currentTime === 0) {touchPlayList.push({instance: self._audio,offset: offset,audio: self,});}}, 10);}if (cc.sys.os === cc.sys.OS_IOS && cc.sys.isBrowser && cc.sys.isMobile) {// Audio context is suspended when you unplug the earphones,// and is interrupted when the app enters background.// Both make the audioBufferSource unplayable.// # diff start// if ((audio.context.state === 'suspended' && this._context.currentTime !== 0) || audio.context.state === 'interrupted') {// reference: https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/resumeaudio.context.resume();// }// # diff end}};proto.pause = function () {clearTimeout(this._currentTimer);if (this.paused) return;// Record the time the current has been playedthis.playedLength = this._context.currentTime - this._startTime;// If more than the duration of the audio, Need to take the remainderthis.playedLength %= this._buffer.duration;let audio = this._currentSource;if (audio) {if (audio.onended) {audio.onended._binded = false;audio.onended = null;}audio.stop(0);}this._currentSource = null;this._startTime = -1;};Object.defineProperty(proto, 'paused', {get: function () {// If the current audio is a loop, paused is falseif (this._currentSource && this._currentSource.loop) return false;// startTime default is -1if (this._startTime === -1) return true;// Current time -  Start playing time > Audio durationreturn this._context.currentTime - this._startTime > this._buffer.duration;},enumerable: true,configurable: true,});Object.defineProperty(proto, 'loop', {get: function () {return this._loop;},set: function (bool) {if (this._currentSource) this._currentSource.loop = bool;return (this._loop = bool);},enumerable: true,configurable: true,});Object.defineProperty(proto, 'volume', {get: function () {return this._volume;},set: function (num) {this._volume = num;// https://www.chromestatus.com/features/5287995770929152if (this._gainObj.gain.setTargetAtTime) {try {this._gainObj.gain.setTargetAtTime(num, this._context.currentTime, TIME_CONSTANT);} catch (e) {// Some other unknown browsers may crash if TIME_CONSTANT is 0this._gainObj.gain.setTargetAtTime(num, this._context.currentTime, 0.01);}} else {this._gainObj.gain.value = num;}if (cc.sys.os === cc.sys.OS_IOS && !this.paused && this._currentSource) {// IOS must be stop webAudiothis._currentSource.onended = null;this.pause();this.play();}},enumerable: true,configurable: true,});Object.defineProperty(proto, 'currentTime', {get: function () {if (this.paused) {return this.playedLength;}// Record the time the current has been playedthis.playedLength = this._context.currentTime - this._startTime;// If more than the duration of the audio, Need to take the remainderthis.playedLength %= this._buffer.duration;return this.playedLength;},set: function (num) {if (!this.paused) {this.pause();this.playedLength = num;this.play();} else {this.playedLength = num;}return num;},enumerable: true,configurable: true,});Object.defineProperty(proto, 'duration', {get: function () {return this._buffer.duration;},enumerable: true,configurable: true,});
})(WebAudioElement.prototype);// # add start
if (cc.sys.isBrowser && cc.sys.os === cc.sys.OS_IOS && cc.sys.isMobile) {cc.game.on(cc.game.EVENT_GAME_INITED, () => {cc.game.on(cc.game.EVENT_HIDE, () => {// 'suspended':音频处于暂停状态、// 'running':音频正在运行、// 'closed':音频上下文已关闭、// 'interrupted':音频被中断。let audioContext = cc.sys.__audioSupport.context;let state = audioContext.state;console.log('hide', state, new Date().getTime());//// 无效废弃// if (state === 'running') {// 	audioContext.suspend();// }// 切换后台时重置音频状态isHide = true;});cc.game.on(cc.game.EVENT_SHOW, () => {// 'suspended':音频处于暂停状态、// 'running':音频正在运行、// 'closed':音频上下文已关闭、// 'interrupted':音频被中断。let audioContext = cc.sys.__audioSupport.context;let state = audioContext.state;console.log('show', state, new Date().getTime());//// 无效废弃// if (state === 'interrupted' || state === 'suspended') {// 	audioContext// 		.resume()// 		.then(() => {// 			console.log('尝试恢复音频上下文');// 		})// 		.catch((error) => {// 			console.error('恢复音频上下文失败:', error);// 		});// }setTimeout(() => {audioContext.suspend();}, 50);setTimeout(() => {audioContext.resume();}, 100);});});
}
// # add end

简单总结:发现问题后,最开始是准备严格按照音频上下文状态来处理逻辑,测试后发现无效(感兴趣的同学可以去试试)。同时增加了输出的切换前后台输出,发现并非像安卓一样切换后台时输出“hide”,恢复前台时输出“show”,而是有时候一次输出两个“hide”,而且通过输出的时间发现,“hide”和“show”几乎是同时输出的,而且时间明显不是切换后台的时间;因此猜测在ios上 会不会是在恢复前台后才先后调用 “EVENT_HIDE” 与 “EVENT_SHOW” 呢?(仅猜测结果无法保证)不过对此我想到手动来处理音频的暂停与恢复,因此有了第一个方法;

第二种方法是做了一个保证(考虑到万一因为安全机制【非触摸无法播放音频】导致恢复音频失败),在切换后台后,首次播放音频时调用一次 “suspend”方法,再 调用一次 “resume”方法,来恢复音频;

亲测只延迟来处理都能解决大部分 ios 没音的问题(使用第二种方法记得设置插件)。

http://www.dtcms.com/a/306600.html

相关文章:

  • 美国人工智能行动计划:洞察 AI 发展新路径​​
  • 怎样在 Vue 中定义全局方法?
  • 快速删除Word和WPS文字中的空白行
  • LLM—— 基于 MCP 协议(SSE 模式)的工具调用实践
  • PYTHON从入门到实践-17通过网络API获取数据与可视化
  • Deforum Stable Diffusion,轻松实现AI视频生成自由!
  • Python爬虫04_Requests豆瓣电影爬取
  • 《消息队列学习指南:从 MQ 基础到 SpringAMQP 实践》
  • AI一周事件(2025年7月22日-7月29日)
  • wps批量让浮动在表格的图片跟随单元格移动和调整大小
  • 树莓派 香橙派SD卡TF卡U盘系统备份与还原
  • 【Kubernetes 指南】基础入门——Kubernetes 201(二)
  • 【昇腾】基于Atlas 200I DK A2开发者套件修改usb0的默认IP重启后被恢复的问题处理_20250730
  • 第十二天:C++ 标准库函数分类总结
  • LeetCode 56 - 合并区间
  • SpringBoot收尾+myBatis plus
  • 算法精讲:二分查找(二)—— 变形技巧
  • JavaScript中的数据类型以及存储上的差别
  • Keil STM32工程各文件作用
  • ArcGIS以及ArcGIS Pro如何去除在线地图制作者名单
  • 黑马点评-超卖问题
  • 实现了加载 正向 碰撞 雅可比 仿真
  • 4.Origin2021如何绘制多组误差棒图?
  • Vulnhub靶场:thales
  • c++之基础B(第一课)
  • 力扣面试150(44/150)
  • Linux 系统进程管理与计划任务详解
  • Kafka 单机多 Broker 实例集群搭建 | 详情
  • 育儿补贴之外,父母更需要的 “认知补贴”|创客匠人
  • 测试用例颗粒度全解析