はじめに
UnityモバイルゲームでLevelPlay(ironSource)のバナー広告を表示すると、BGM/SEが突然無音になる問題に遭った。原因と対策をまとめる。
LevelPlay固有の問題ではなく、AdMob・AppLovin MAX等、WebViewベースのバナー広告を使う広告SDKなら同様に起こり得る。
環境
- Unity 6 (6000.3.1f1)
- LevelPlay SDK
- iOS / Android
症状
- バナー広告の初回表示・自動リフレッシュ のタイミングでBGM/SEが無音になる
-
AudioSource.isPlayingは true のまま(Unity公式 Won't Fix: UUM-60061) - ユーザーが音量設定を触ると復帰する(=音声データ自体は生きている)
原因
バナー広告はWKWebView(iOS)で描画される。WKWebViewは別プロセスで動作し、広告のロード/リフレッシュ時にAVAudioSessionを奪取する。
UnityのFMODベースのオーディオパイプラインはセッションを失うと音声出力が停止するが、isPlayingは内部状態を見ているだけなのでtrueを返し続ける。
さらにバナー広告はSDK内部で自動リフレッシュするため、OnAdLoadedコールバックだけでは全ての中断を検知できない。
試したが不十分だったアプローチ
| アプローチ | 結果 |
|---|---|
OnAdLoadedコールバックで復帰 |
SDK内部の広告取得を検知できず、放置すると無音のまま |
AudioSettings.Reset() 単体 |
セッション共存設定なしでは復帰失敗し、リカバリーループに陥る |
Stop() → Play() のみ(Reset不使用) |
FMODパイプラインが停止しているため無効 |
解決策
3つの要素の組み合わせで解決した。
-
timeSamplesポーリングで中断を検出 —isPlayingは信用できないため、AudioSource.timeSamplesが進んでいるかを毎フレーム監視する -
iOS ネイティブプラグインで
MixWithOthersを設定 —AVAudioSessionCategoryPlayback + MixWithOthersでWebViewとのセッション共存を確立する -
AudioSettings.Reset()でオーディオパイプラインを再構築 — セッション共存を確立した上でReset()を呼び、確実に復帰させる
実装
iOS ネイティブプラグイン(AudioSessionMixer.mm)
#import <AVFoundation/AVFoundation.h>
extern "C" void AudioSession_SetMixWithOthers_()
{
AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error = nil;
[session setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionMixWithOthers
error:&error];
if (error) { return; }
[session setActive:YES error:&error];
}
C# 側(SoundManager 抜粋)
// --- フィールド ---
private int lastTimeSamples;
private int stallFrameCount;
private const int StallFrameThreshold = 3;
private float lastRecoveryTime;
private const float RecoveryCooldown = 2f;
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void AudioSession_SetMixWithOthers_();
#endif
// --- Update() から毎フレーム呼び出し ---
private void DetectAndRecoverAudioStall()
{
if (bgmSource == null || bgmSource.clip == null) return;
// クールダウン中はスキップ(復帰ループ防止)
if (Time.realtimeSinceStartup - lastRecoveryTime < RecoveryCooldown)
{
stallFrameCount = 0;
lastTimeSamples = bgmSource.timeSamples;
return;
}
var current = bgmSource.timeSamples;
// isPlaying=true なのに timeSamples が進んでいない → セッション奪取
if (bgmSource.isPlaying && current == lastTimeSamples && current > 0)
{
stallFrameCount++;
if (stallFrameCount >= StallFrameThreshold)
{
RecoverAudio();
}
}
else
{
stallFrameCount = 0;
}
lastTimeSamples = current;
}
private void RecoverAudio()
{
lastRecoveryTime = Time.realtimeSinceStartup;
stallFrameCount = 0;
#if UNITY_IOS && !UNITY_EDITOR
AudioSession_SetMixWithOthers_(); // セッション共存を確立
#endif
var clip = bgmSource.clip;
var time = bgmSource.timeSamples;
var pitch = bgmSource.pitch;
AudioSettings.Reset(AudioSettings.GetConfiguration());
#if UNITY_IOS && !UNITY_EDITOR
AudioSession_SetMixWithOthers_(); // Reset()がカテゴリを上書きするため再設定
#endif
bgmSource.clip = clip;
bgmSource.timeSamples = time;
bgmSource.pitch = pitch;
bgmSource.Play();
}
ポイント
| 要素 | 理由 |
|---|---|
timeSamples 監視 |
isPlaying が信用できないため、実際にサンプルが進んでいるかで判定 |
| 3フレーム閾値 | 1-2フレームの誤検知を防止 |
| 2秒クールダウン |
Reset() → 再検出 → Reset() の無限ループを防止 |
MixWithOthers |
WebViewとセッションを共存させ、Reset()の成功率を上げる |
Reset()後の再MixWithOthers
|
Reset()が内部でカテゴリを再設定する可能性があるため |
残る制約
-
約100msの無音(検出3フレーム + Reset処理)は
AudioSettings.Reset()を使う限り不可避。Unityエンジンレベルの制約。 - 起動時から
Playback + MixWithOthersを設定すれば中断自体を防げるが、マナーモードで音が鳴る副作用があるため不採用。