デジタルサイネージにおける音声付き自動再生の実現

1背景と課題

課題

サイネージ画面でチャンネル内のコンテンツを自動切替する際、動画・YouTube・音声がミュート状態としていました。 一部のユーザーから「いちいち音声をONにする操作が面倒」、「自然に音声が再生されると嬉しい」という要望がありました。

⚠️
ミュートにしていた理由: Chromium Autoplay Policy
Chromium(Chrome)のAutoplay Policyにより、ユーザーの操作なしに音声付きメディアの自動再生が制限されています。音声付き再生は要件にはなかったため、ミュート状態で開始し、フロートの「音声ON」ボタンを押すことで音声を再生するようにしていました。

Chromium Autoplay Policy とは

Chromeは2018年頃から、ユーザー体験向上とバッテリー消費抑制のため、音声付きメディアの自動再生を制限しています。 以下の条件のいずれかを満たさない限り、音声付きの自動再生はブロックされます:

  1. ユーザーインタラクション(クリック、タップ)があった
  2. Media Engagement Index(MEI)が閾値を超えている
  3. PWAとしてホーム画面に追加された
  4. --autoplay-policy=no-user-gesture-required フラグで起動
  5. エンタープライズポリシー設定

従来の実装の問題

従来は安全策として、常にミュート状態で開始していました:

video-preview.tsx (従来) TypeScript
// 常にミュートで初期化(安全だが不便)
const [isMuted, setIsMuted] = useState(true)

自動切り替え時にコンポーネントが再マウントされると、毎回ミュート状態にリセットされてしまうという問題がありました。

2解決アプローチ

2段階のアプローチ

💡
キーポイント: 表示端末(ラズパイ)側の設定と、コードベース側の検出ロジックを組み合わせることで実現しています。

1. ラズパイ側(端末設定)

サイネージ表示端末のChromium起動スクリプトに、自動再生を許可するフラグを設定:

/home/pi/Documents/scripts/start_chromium.sh Bash
#!/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}

2. コードベース側(ランタイム検出)

Chromiumの起動フラグをJavaScriptから直接検出することは不可能です。 よって、音声付き自動再生が許可されているかどうかを実際に試して判定するようにしました。

全体フロー
画面表示
自動再生テスト
結果を保存
プレビューに反映

自動再生テストの詳細フロー

ブラウザによってサポートするAPIが異なるため、2段階のフォールバック方式で検出を行います。

検出ロジック(フォールバック方式)
canAutoplayWithSound()
  │
  ├── Step 1: navigator.getAutoplayPolicy('mediaelement')
  │         │
  │         ├── 'allowed' → return true (音声ON可能)
  │         ├── 'allowed-muted' → return false (ミュートのみ)
  │         └── 未サポート / エラー → Step 2へ
  │
  └── Step 2: テスト音声の実再生
             │
             ├── new Audio('/audio/audio_test.mp3')
             ├── volume = 0.01 (ほぼ無音)
             ├── audio.play()
             │    │
             │    ├── 成功 (Promise resolved) → return true
             │    └── NotAllowedError → return false
             └── タイムアウト (3秒) → return false
検出方法 対応ブラウザ 特徴
navigator.getAutoplayPolicy() Chrome 66+, Edge 79+ 高速・確実。モダンブラウザで優先使用
テスト音声の実再生 全ブラウザ フォールバック。実際に試行して判定
💡
テスト音声について
検出用に /audio/audio_test.mp3 を使用します。音量は0.01(1%)に設定されており、ユーザーには聞こえません。再生成功後は即座に停止・クリーンアップされます。

3アーキテクチャ

レイヤー構成

Utility
autoplay-detection.ts
Store (Zustand)
audio-autoplay-store.ts
Hook
use-audio-autoplay.ts

ファイル構成

apps/src/
├── utils/
│   └── autoplay/
│       └── autoplay-detection.ts 検出ロジック
├── stores/
│   └── audio-autoplay-store.ts 状態管理
├── hooks/
│   └── use-audio-autoplay.ts カスタムフック
└── features/pages/
    ├── signage/
    │   └── signage-display.tsx // 検出トリガー
    └── contents/components/previews/
        ├── video-preview.tsx // 動画
        ├── audio-preview.tsx // 音声
        └── url-preview.tsx // YouTube

4実装詳細

4.1 自動再生検出ユーティリティ

ブラウザが音声付きメディアの自動再生を許可しているかを検出します。 2つの検出方法を使用し、フォールバックにより幅広いブラウザに対応しています。

utils/autoplay/autoplay-detection.ts TypeScript
/**
 * 音声付き自動再生が許可されているかを検出する
 *
 * 検出方法:
 * 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 = ブロック
      })
  })
}

4.2 Zustand ストア

検出結果とユーザーのミュート状態をグローバルに管理します。 セッション中は検出結果をキャッシュし、全コンポーネントで共有します。

stores/audio-autoplay-store.ts TypeScript
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
  },
}))

4.3 カスタムフック

コンポーネントで使いやすいインターフェースを提供します。 useAudioMuteState は初期ミュート値の管理とユーザー操作のハンドリングを担当します。

hooks/use-audio-autoplay.ts TypeScript
/**
 * ミュート状態を管理するフック
 * プレビューコンポーネントで使用
 */
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 }
}

5コンポーネント連携

5.1 サイネージ画面(検出トリガー)

サイネージ画面の表示時に1回だけ自動再生検出を実行します。 useAudioAutoplay(true) の引数で自動検出を有効化しています。

signage-display.tsx TypeScript
export function SignageDisplay({ departmentId, initialData, ... }) {
  // 音声自動再生検出(画面表示時に1回のみ実行)
  useAudioAutoplay(true)

  // ... 他のロジック
}

5.2 動画プレビュー

検出結果に基づいてミュート状態を初期化し、ミュート時は解除ボタンを表示します。

video-preview.tsx TypeScript
function VideoPreviewComponent({ content, ... }) {
  // ミュート状態管理(自動再生検出結果に基づく)
  const { isMuted, handleUnmute } = useAudioMuteState()

  return (
    <div>
      <video
        autoPlay
        loop
        muted={isMuted}
        playsInline
        ...
      />

      {/* ミュート解除ボタン */}
      {isMuted && (
        <Button onClick={handleUnmute}>
          タップして音声ON
        </Button>
      )}
    </div>
  )
}

5.3 YouTube(URL Preview)

YouTubeは常にmute:1で開始し、onReadyで検出結果に基づいてミュート解除します。

url-preview.tsx TypeScript
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 で条件分岐

6動作フロー

環境別の動作

環境 検出結果 初期ミュート 動作
ラズパイ + Chromium(フラグ付き) true OFF 音声ON + 自動切替
通常ブラウザ false ON ミュートで開始、ユーザー操作で解除

シーケンス図

初期化フロー
SignageDisplayuseAudioAutoplay(true)
    │
    └── detectAutoplay()
          │
          └── canAutoplayWithSound()
                 │
                 ├── Success → canAutoplay = true
                 └── Failed → canAutoplay = false
          │
VideoPreview ←── shouldBeMuted()
    │
    └── muted = !canAutoplay
ユーザー操作の記憶
ユーザーが一度ミュートを解除すると、userUnmuted: true がストアに保存され、 以降のコンテンツ切り替えでもミュート状態にならなくなります。

7参考リンク

公式ドキュメント

Raspberry Pi 関連

使用技術