サイネージ画面でチャンネル内のコンテンツを自動切替する際、動画・YouTube・音声がミュート状態としていました。 一部のユーザーから「いちいち音声をONにする操作が面倒」、「自然に音声が再生されると嬉しい」という要望がありました。
Chromeは2018年頃から、ユーザー体験向上とバッテリー消費抑制のため、音声付きメディアの自動再生を制限しています。 以下の条件のいずれかを満たさない限り、音声付きの自動再生はブロックされます:
--autoplay-policy=no-user-gesture-required フラグで起動従来は安全策として、常にミュート状態で開始していました:
// 常にミュートで初期化(安全だが不便) const [isMuted, setIsMuted] = useState(true)
自動切り替え時にコンポーネントが再マウントされると、毎回ミュート状態にリセットされてしまうという問題がありました。
サイネージ表示端末のChromium起動スクリプトに、自動再生を許可するフラグを設定:
#!/bin/bash # ネットワークが上がるまで待機 while ! ping -c 1 8.8.8.8 &> /dev/null; do sleep 3 done # Chromium 起動(autoplay-policyフラグ付き) /usr/bin/chromium-browser \ --user-data-dir=/home/pi/chromium-profile \ --noerrdialogs \ --disable-session-crashed-bubble \ --disable-infobars \ --autoplay-policy=no-user-gesture-required \ --window-position=0,0 \ --start-fullscreen \ --kiosk \ --hide-scrollbars \ https://example.com/signage/{departmentId}
Chromiumの起動フラグをJavaScriptから直接検出することは不可能です。 よって、音声付き自動再生が許可されているかどうかを実際に試して判定するようにしました。
ブラウザによってサポートするAPIが異なるため、2段階のフォールバック方式で検出を行います。
| 検出方法 | 対応ブラウザ | 特徴 |
|---|---|---|
navigator.getAutoplayPolicy() |
Chrome 66+, Edge 79+ | 高速・確実。モダンブラウザで優先使用 |
| テスト音声の実再生 | 全ブラウザ | フォールバック。実際に試行して判定 |
/audio/audio_test.mp3 を使用します。音量は0.01(1%)に設定されており、ユーザーには聞こえません。再生成功後は即座に停止・クリーンアップされます。
ブラウザが音声付きメディアの自動再生を許可しているかを検出します。 2つの検出方法を使用し、フォールバックにより幅広いブラウザに対応しています。
/** * 音声付き自動再生が許可されているかを検出する * * 検出方法: * 1. navigator.getAutoplayPolicy() APIをチェック(最新ブラウザ) * 2. フォールバック: テスト音声を実際に再生して確認 */ export async function canAutoplayWithSound(): Promise<boolean> { // クライアントサイドでのみ実行 if (typeof window === 'undefined') { return false } // キャッシュがあれば返す(重複検出防止) if (cachedResult !== null) { return cachedResult } // 1. navigator.getAutoplayPolicy() をチェック(Chrome 114+) const nav = navigator as NavigatorWithAutoplayPolicy if (typeof nav.getAutoplayPolicy === 'function') { const policy = nav.getAutoplayPolicy('mediaelement') return policy === 'allowed' } // 2. フォールバック: テスト音声を実際に再生 return testAudioPlayback() } /** * テスト音声を再生して自動再生の可否を判定 */ async function testAudioPlayback(): Promise<boolean> { return new Promise((resolve) => { const audio = new Audio('/audio/audio_test.mp3') audio.volume = 0.01 // ほぼ無音 // タイムアウト設定(3秒) const timeoutId = setTimeout(() => { resolve(false) }, 3000) audio.play() .then(() => { clearTimeout(timeoutId) audio.pause() resolve(true) // 再生成功 = 自動再生許可 }) .catch(() => { clearTimeout(timeoutId) resolve(false) // NotAllowedError = ブロック }) }) }
検出結果とユーザーのミュート状態をグローバルに管理します。 セッション中は検出結果をキャッシュし、全コンポーネントで共有します。
import { create } from 'zustand' import { canAutoplayWithSound } from '@/utils/autoplay' interface AudioAutoplayState { /** 自動再生可能かどうか(null = 未検出) */ canAutoplay: boolean | null /** ユーザーが手動でミュートを解除したか */ userUnmuted: boolean /** 検出処理中かどうか */ isDetecting: boolean } export const useAudioAutoplayStore = create<AudioAutoplayStore>((set, get) => ({ canAutoplay: null, userUnmuted: false, isDetecting: false, // 自動再生可否を検出 detectAutoplay: async () => { const { canAutoplay, isDetecting } = get() if (canAutoplay !== null || isDetecting) return set({ isDetecting: true }) const result = await canAutoplayWithSound() set({ canAutoplay: result, isDetecting: false }) }, // 現在ミュートすべきかどうか shouldBeMuted: () => { const { canAutoplay, userUnmuted } = get() // ユーザーが手動解除した場合は以降ミュートしない if (userUnmuted) return false // 未検出の場合はミュート(安全策) if (canAutoplay === null) return true // 自動再生許可されていればミュート不要 return !canAutoplay }, }))
コンポーネントで使いやすいインターフェースを提供します。
useAudioMuteState は初期ミュート値の管理とユーザー操作のハンドリングを担当します。
/** * ミュート状態を管理するフック * プレビューコンポーネントで使用 */ export function useAudioMuteState() { const { shouldBeMuted, onUserUnmute, canAutoplay } = useAudioAutoplay() // 初期値はストアから取得(検出結果に基づく) const [isMuted, setIsMuted] = useState(() => shouldBeMuted()) // 検出完了後にミュート状態を更新 useEffect(() => { if (canAutoplay !== null) { setIsMuted(shouldBeMuted()) } }, [canAutoplay, shouldBeMuted]) // ミュート解除時にストアも更新 const handleUnmute = useCallback(() => { setIsMuted(false) onUserUnmute() }, [onUserUnmute]) return { isMuted, setIsMuted, handleUnmute, canAutoplay } }
サイネージ画面の表示時に1回だけ自動再生検出を実行します。
useAudioAutoplay(true) の引数で自動検出を有効化しています。
export function SignageDisplay({ departmentId, initialData, ... }) { // 音声自動再生検出(画面表示時に1回のみ実行) useAudioAutoplay(true) // ... 他のロジック }
検出結果に基づいてミュート状態を初期化し、ミュート時は解除ボタンを表示します。
function VideoPreviewComponent({ content, ... }) { // ミュート状態管理(自動再生検出結果に基づく) const { isMuted, handleUnmute } = useAudioMuteState() return ( <div> <video autoPlay loop muted={isMuted} playsInline ... /> {/* ミュート解除ボタン */} {isMuted && ( <Button onClick={handleUnmute}> タップして音声ON </Button> )} </div> ) }
YouTubeは常にmute:1で開始し、onReadyで検出結果に基づいてミュート解除します。
const handleYouTubeReady = useCallback((event) => { playerRef.current = event.target // 検出結果に基づいてミュート状態を設定 if (canAutoplay === true) { event.target.unMute() event.target.setVolume(100) console.log('YouTube: Autoplay allowed, unmuting') } else { event.target.mute() console.log('YouTube: Autoplay not allowed, keeping muted') } }, [canAutoplay]) return ( <YouTube videoId={videoId} opts={{ playerVars: { autoplay: 1, mute: 1, // 常にミュートで開始 }, }} onReady={handleYouTubeReady} /> )
| コンポーネント | 対象コンテンツ | 実装方法 |
|---|---|---|
VideoPreview |
動画ファイル(MP4, WebM等) | useAudioMuteState で初期化 |
AudioPreview |
音声ファイル(MP3等) | useAudioMuteState で初期化 |
URLPreview |
YouTube動画 | onReady で条件分岐 |
| 環境 | 検出結果 | 初期ミュート | 動作 |
|---|---|---|---|
| ラズパイ + Chromium(フラグ付き) | true |
OFF | 音声ON + 自動切替 |
| 通常ブラウザ | false |
ON | ミュートで開始、ユーザー操作で解除 |
userUnmuted: true がストアに保存され、
以降のコンテンツ切り替えでもミュート状態にならなくなります。