cocos打包web - ios設備息屏及前后臺切換音頻播放問題

  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&…