1. 切换前台时,延迟暂停与恢复能解决大部分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 web 没音的问题(使用第二种方法记得设置插件)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/91408.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/91408.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/91408.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

期货Level2五档委托簿0.25秒高频分钟与日级历史行情数据解析

在金融数据分析领域&#xff0c;本地CSV格式的期货数据为研究人员和交易者提供了丰富的原始信息。本文将介绍如何有效利用不同类型的期货数据&#xff0c;包括分钟数据、高频Tick、五档Level2等&#xff0c;并阐述数据处理与分析方法。一、数据概述期货分钟数据通常包含时间戳、…

原生html+js+jq+less 实现时间区间下拉弹窗选择器

html弹窗<div class"popupForm" id"popupForm10"><div class"pop-box"><i class"iconfont icon-quxiao cancel" onclick"toggleForm(10)"></i><div class"title">选择时间</div…

基于逻辑回归、随机森林、梯度提升树、XGBoost的广告点击预测模型的研究实现

文章目录有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主一、项目背景与目标二、数据概览与预处理2.1 数据导入与初步分析2.2 缺失值与重复值处理2.3 目标变量分布三、探索性数据分析&#xff08;EDA&#xff09;3.1 数值变量分布3.2 类别变量分布3…

Docker学习相关视频笔记(三)

参考视频地址&#xff1a;40分钟的Docker实战攻略&#xff0c;一期视频精通Docker。感谢作者的辛苦付出。 本文是Docker学习相关视频笔记&#xff08;一&#xff09;与Docker学习相关视频笔记&#xff08;二&#xff09;的后续 4、Docker命令 4.8 Docker 网络 4.8.1 桥接模式…

RK3568笔记九十五:基于FFmpeg和Qt实现简易视频播放器

若该文为原创文章,转载请注明原文出处。 一、开发环境 1、硬件:正点原子ATK-DLRK3568 2、QT: 5.14.2 3、系统: buildroot 二、实现功能 使用ffmpeg音视频库软解码实现视频播放器 支持打开多种本地视频文件(如mp4,mov,avi等) 视频播放支持实时开始,暂停,继续播放 采…

【LLM】Kimi-K2模型架构(MuonClip 优化器等)

note Kimi K2 的预训练阶段使用 MuonClip 优化器实现万亿参数模型的稳定高效训练&#xff0c;在人类高质量数据成为瓶颈的背景下&#xff0c;有效提高 Token 利用效率。MuonClip Optimizer优化器&#xff0c;解决随着scaling up时的不稳定性。Kimi-K2 与 DeepSeek-R1 架构对比…

Vue基础(25)_组件与Vue的内置关系(原型链)

了解组件与Vue的内置关系前&#xff0c;我们需要回顾js原型链基础知识&#xff1a;1、构造函数构造函数是一种特殊的方法&#xff0c;用于创建和初始化一个新的对象。它们是使用 new 关键字和函数调用来创建对象的。构造函数实际上只是一个普通的函数&#xff0c;通常以大写字母…

kafka中生产者的数据分发策略

在 Kafka 中&#xff0c;生产者的数据分发策略决定了消息如何分配到主题的不同分区。在 Python 中&#xff0c;我们通常使用 kafka-python 库来操作 Kafka&#xff0c;下面详细讲解其数据分发策略及实现代码。一、Kafka 生产者数据分发核心概念分区&#xff08;Partition&#…

【动态规划算法】斐波那契数列模型

一. (1137.)第N个泰波那契数(力扣)1.1动态规划的算法流程 对于初学者来讲学术上的概念晦涩难懂,将用通俗易懂的方式带来感性的理解. 1.状态表示dp表(一维或二维数组)里面的值所表示的含义 从哪获取? 1.题目要求,如本题 2.题目没有明确说明的情况下做题经验的累积 3.分析问题的…

Odoo 18 PWA 全面掌握:从架构、实现到高级定制

本文旨在对 Odoo 18 中的渐进式网络应用&#xff08;Progressive Web App, PWA&#xff09;技术进行一次全面而深入的剖析。本文的目标读者为 Odoo 技术顾问、高级开发人员及解决方案架构师&#xff0c;旨在提供一份权威的技术参考&#xff0c;以指导 PWA 相关的实施项目与战略…

Binary Classifier Optimization for Large Language Model Alignment

2025.acl-long.93.pdfhttps://aclanthology.org/2025.acl-long.93.pdf 1. 概述 在生产环境中部署大型语言模型(LLMs)时,对齐LLMs一直是一个关键因素,因为预训练的LLMs容易产生不良输出。Ouyang等人(2022)引入了基于人类反馈的强化学习(RLHF),该方法涉及基于单个提示的…

在CentOS上以源码编译的方式安装PostgreSQL

下载目录&#xff1a;PostgreSQL: File Browser&#xff0c;我使用的PostgreSQLv17.5。Linux系统&#xff1a;CentOS Linux release 7.9.2009 (Core) 安装依赖包和工具链&#xff08;必须且重要&#xff01;&#xff09; yum groupinstall "Development Tools" -y yu…

Baumer工业相机堡盟工业相机如何通过YoloV8深度学习模型实现沙滩小人检测识别(C#代码UI界面版)

Baumer工业相机堡盟工业相机如何通过YoloV8深度学习模型实现沙滩小人检测识别&#xff08;C#代码UI界面版&#xff09;工业相机使用YoloV8模型实现沙滩小人检测识别工业相机通过YoloV8模型实现沙滩小人检测识别的技术背景在相机SDK中获取图像转换图像的代码分析工业相机图像转换…

Ubuntu服务器安装与运维手册——操作纯享版

本手册汇总了从硬件预配置、Ubuntu 安装、网络与服务配置&#xff0c;到 Windows/macOS 访问共享、MySQL 初始化的完整流程&#xff0c;便于今后运维参考。 目录 环境与硬件概览BIOS/UEFI 设置制作与启动安装介质Ubuntu 24.04 LTS 安装流程静态 IP 配置&#xff08;netplan&am…

【Nginx】Nginx进阶指南:解锁代理与负载均衡的多样玩法

在Web服务的世界里&#xff0c;Nginx就像是一位多面手&#xff0c;它不仅能作为高性能的Web服务器&#xff0c;还能轻松胜任代理服务器、负载均衡器等多种角色。今天&#xff0c;我们就来深入探索Nginx的几个常见应用场景&#xff0c;通过实际案例和关键配置解析&#xff0c;带…

原创-锐能微82xx系列电能计量芯片软件驱动开发与精度校准流程完全指南

引言 电能计量芯片的软件驱动开发是整个计量系统的核心&#xff0c;它直接决定了计量精度、系统稳定性和功能完整性。锐能微82xx系列电能计量芯片凭借其强大的数字信号处理能力和丰富的功能特性&#xff0c;为开发者提供了灵活的软件开发平台。本文将详细介绍82xx系列芯片的软…

如何使用 Apache Ignite 作为 Spring 框架的缓存(Spring Cache)后端

这份文档是关于 如何使用 Apache Ignite 作为 Spring 框架的缓存&#xff08;Spring Cache&#xff09;后端&#xff0c;实现方法级别的缓存功能。 这和前面我们讲的 Spring Data Ignite 是两个不同的概念。我们先明确区别&#xff0c;再深入理解。&#x1f501; 一、核心区别…

Android 超大图片、长图分割加载

在Android开发中&#xff0c;处理大图片的加载是一个常见且重要的问题&#xff0c;尤其是在需要显示高分辨率图片时。大图片如果不正确处理&#xff0c;可能会导致内存溢出或应用性能下降。下面是一些常用的策略和技术来优化大图片的加载&#xff1a;1. 使用图片压缩库a. Glide…

Linux:理解操作系统

文章目录数据流动操作系统数据流动 软件运行&#xff0c;必须先加载到内存&#xff0c;本质要把磁盘上的文件 加载到内存。 我们写的算法是处理存储器里面的数据&#xff0c;数据就是文件&#xff0c;我们自己写的可执行文件。 图中QQ就是软件&#xff0c;加载内存后进行下一步…

【每日一错】PostgreSQL的WAL默认段大小

文章目录题目扩展学习WAL工作原理流程图题目 扩展学习 WAL&#xff08;Write Ahead Log&#xff09;预写日志&#xff1a; WAL是PostgreSQL先写日志、后写数据的机制&#xff0c;用来防止数据丢失、提升数据恢复能力。 流程&#xff1a; 事务先写日志文件&#xff08;WAL&…