iOS實(shí)現(xiàn)自定義鈴聲(從VoIP到NSE)

一、背景

2018年,受通信監(jiān)管要求,蘋果國(guó)區(qū)AppStore不允許app支持CallKit。
2019年,隨著iOS 13正式開放, Xcode11隨之發(fā)布,蘋果已不再允許將PushKit應(yīng)用在非VoIP語(yǔ)音通話的場(chǎng)景上,開發(fā)者必須在接入CallKit的情況下才能使用PushKit。也就是說,通過 Xcode 11 編譯的 iOS 13 系統(tǒng) App,如果使用 PushKit 則必須依賴蘋果 CallKit 才能正常接收VoIP推送。
結(jié)論:使用VoIP功能則無法在中國(guó)區(qū)App Store上架。

二、技術(shù)方案

VoIP

以微聊音視頻鈴聲播放為例,在之前的方案中,微聊音視頻消息會(huì)使用VoIP Push Notification,客戶端在被喚醒之后將獲得30s的后臺(tái)運(yùn)行時(shí)間,在這30s內(nèi),微聊被喚醒調(diào)起音視頻頁(yè)面,并播放定制鈴聲音頻。

Notification Service Extension

新的方案是主要是利用了蘋果在iOS10中推出的Notification Service Extension(以下簡(jiǎn)稱NSE),當(dāng)apns的payload上帶上"mutable-content"的值為1時(shí),就會(huì)進(jìn)入NSE的代碼中。與Voip方案最大的不同之處是,NSE不能喚醒主應(yīng)用,也不能訪問主應(yīng)用的文件空間,只能在Extension進(jìn)程中處理相應(yīng)的邏輯。在NSE中,開發(fā)者可以更改通知的內(nèi)容,利用離線合成或者從后臺(tái)下載的方式,生成需要播報(bào)的內(nèi)容,通過自定義通知鈴聲的方式,達(dá)到語(yǔ)音播報(bào)提醒的目的。NSE方案也是蘋果在WWDC2019的Session707上推薦的解決方式。


消息推送流程
UNNotificationSound

在NSE中,可以通過給UNNotificationContent中的Sound屬性賦值來達(dá)到在通知彈出時(shí)播放一段自定義音頻的目的。

// The sound file to be played for the notification. The sound must be in the Library/Sounds folder of the app's data container or the Library/Sounds folder of an app group data container. If the file is not found in a container, the system will look in the app's bundle.
文檔中明確描述了音頻文件的存儲(chǔ)路徑,以及讀取的優(yōu)先級(jí):

1.主應(yīng)用中的Library/Sounds文件夾中
2.AppGroups共享目錄中的Library/Sounds文件夾中
3.main bundle中

自定義鈴聲支持的聲音格式包括,aiff、wav以及caf格式,鈴聲的長(zhǎng)度必須小于30s,否則系統(tǒng)會(huì)播放默認(rèn)的鈴聲。
鎖屏情況下,鈴聲可以持續(xù)30s(與NES強(qiáng)制結(jié)束時(shí)間一致)在亮屏情況下鈴聲只能持續(xù)5s,是系統(tǒng)行為限制。
而且由于是通知鈴聲,聲音是默認(rèn)跟靜音開關(guān)的。

AppGroups

由于我們是在NSE中自定義鈴聲,所以1和3這兩個(gè)文件路徑我們是無法訪問的。只能將合成好或者下載到語(yǔ)音音頻文件存儲(chǔ)到AppGroups下的Library/Sounds文件夾中,需要在Capablities中打開這個(gè)AppGroups的能力,即可通過NSFileManager的containerURLForSecurityApplicationGroupIdentifier:方法訪問AppGroups的根目錄。

示例代碼:
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *containerUrl = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.com.wchat.share"];
    
    NSString *newPath = [containerUrl.path stringByAppendingPathComponent:@"Library/Sounds"];
    // 清理本地緩存,刪除sounds文件夾(可選)
    [fileManager removeItemAtPath:newPath error:nil];
    
    if (![fileManager fileExistsAtPath:newPath]) {
        [fileManager createDirectoryAtPath:newPath withIntermediateDirectories:NO attributes:nil error:nil];
    }
    NSURL *localURL = [NSURL fileURLWithPath:[newPath stringByAppendingPathComponent:[NSString stringWithFormat:@"/%@.%@", identifier, fileType]]];
    [fileManager moveItemAtURL:temporaryFileLocation toURL:localURL error:&error];

    //設(shè)置播發(fā)音頻 文件名sname.sfileType
    self.bestAttemptContent.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"%@.%@", sname, sfileType]];

三、開發(fā)過程中遇到的問題

消息播放隊(duì)列

NSE方案有個(gè)問題是:當(dāng)客戶端短時(shí)間內(nèi)收到多條播報(bào)通知時(shí),后面的通知會(huì)頂?shù)羟懊娴耐ㄖ?,?dǎo)致前面的通知播報(bào)不完整。所以需要增加一個(gè)消息隊(duì)列,將所有需要播報(bào)的通知都添加到隊(duì)列中,當(dāng)前面的消息播放完畢后,再播放后面的消息。

多線程問題

要注意的是,NSE的代碼邏輯并不是在主App工程中執(zhí)行的。一方面避免了開發(fā)者在NSE由于代碼設(shè)計(jì)失誤導(dǎo)致前臺(tái)的其他應(yīng)用界面卡住的問題,另一方面是主工程此時(shí)已被掛起或者已被kill掉,NSE本質(zhì)是push部分而不是調(diào)起主App。
所以我們?cè)谔幚砩厦嫣岬降南⒉シ抨?duì)列,以及涉及到文件讀寫的邏輯上,需要給相應(yīng)的代碼邏輯加鎖,否則會(huì)出現(xiàn)多線程問題。

四、NSE擴(kuò)展

iOS Notification Service Extension 共享空間數(shù)據(jù)互通測(cè)試

五、相關(guān)資料

Advances in App Background Execution - 2019
UNNotificationServiceExtension

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容