Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

スマートTV内蔵ブラウザをUser-Agent名で端末判定すると、人間が開いた盤面も壊れる

公開 読了時間 約6分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

スマートTVの内蔵ブラウザをUser-Agentの文字列(Google TV・BRAVIA・Tizen等)で実機端末と判定すると、人間が同じ内蔵ブラウザで盤面を開いた場合まで端末扱いになり、縮小表示の判定が外れて全画面表示のまま崩れる。

結論

サイネージの公開ページ /signage/{classToken} にある端末判定 isEmbeddedSignageDevice() は、確実な Android System WebView シグナル(; wv))に加えて、UA名の文字列(Google TV Android TV CrKey SMART-TV BRAVIA Web0S Tizen)でも「実機端末」と判定していた。スマートTVの内蔵ブラウザで人間が同じ盤面URLを開くと、この名前判定に引っかかって端末扱いになり、縮小表示の fit-stage が外れて全画面表示になった。ヘッダーと天気の表示が画面外に出たり、人物の並びが潰れたりする崩れが起きた(2026-06-23 実機確認)。

症状

このページは2種類の相手に同じURLを開かれる。一つは実機のサイネージ端末(組み込みの Android System WebView)、もう一つはPC・タブレットの人間で、盤面を確認するために開く。

人間向けには fit-stage という縮小表示を当てている。盤面を 1920×1080 の固定ステージに収め、「50インチ実機モニタ相当」の見た目で確認できるようにするための処理で、実機端末側は自分の画面そのものを描いているので、縮小表示を当てずに全画面のまま描く必要がある。この出し分けは、UAが実機端末かどうかを1つの関数で判定して決めていた。

50インチのスマートTVを、人間がその内蔵ブラウザ(Chromeベースではない、TV自体に載っているブラウザ)で開いて盤面を確認したところ、縮小表示が当たらず全画面のまま描かれ、ヘッダーと天気の表示が画面の外に出てしまったり、人物の並びが潰れて崩れるという不具合が起きた。

原因

修正前の判定はこうなっていた。

export function isEmbeddedSignageDevice(ua: string): boolean {
  if (!ua) {
    return false;
  }
  // Android System WebView 判定(`; wv)`)。空白揺れを許容。
  if (/;\s*wv\)/i.test(ua)) {
    return true;
  }
  // TV 内蔵ブラウザ系(端末側)。人間の PC/タブレット UA には現れない語に限定する。
  return /\b(Google ?TV|Android ?TV|CrKey|SMART-TV|SmartTV|BRAVIA|Web0S|Tizen)\b/i.test(ua);
}

コメントにあるとおり、この2段目の正規表現は「人間のPC・タブレットのUAには現れない語」という前提で、Google TV BRAVIA Tizen といったTVブランド・プラットフォーム名を拾っていた。ChromeやSafariのUAにこれらの語が出ないのは確かにそのとおりだが、この前提には抜けがあった。人間がスマートTV本体の内蔵ブラウザでページを開いた場合、そのブラウザ自身のUAにも同じTV名が含まれる。 テストで使われている実際のUA文字列を見るとわかりやすい。

Mozilla/5.0 (Linux; Android 12; Chromecast) AppleWebKit/537.36 (KHTML, like Gecko) CrKey/1.56.500000 Safari/537.36
Mozilla/5.0 (Linux; Android 10; BRAVIA 4K GB) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0 Safari/537.36

どちらにも ; wv) は含まれていない(=実機サイネージ端末の Android System WebView ではない)が、CrKey や BRAVIA という語は含まれている。実機端末を広く拾うつもりだった2段目の判定は、「人間がスマートTVの内蔵ブラウザで開いた場合」も区別なく拾ってしまい、端末扱いになって fit-stage が外れていた。

直す

端末判定を、確実なシグナルである ; wv) だけに絞った。

export function isEmbeddedSignageDevice(ua: string): boolean {
  if (!ua) {
    return false;
  }
  // 端末=Android System WebView のみ(`; wv)`・空白揺れ許容)。スマート TV/Google TV の内蔵ブラウザは UA 名で
  // 拾わない(人間優先=fit を当てて崩れを防ぐ)。詳細は関数 doc 参照。
  return /;\s*wv\)/i.test(ua);
}

実機のサイネージ端末は Android System WebView 経由で盤面を表示しており、UAには必ず ; wv) が含まれる。この1点だけに絞ることで、確実に実機端末だけを拾いながら、人間がどの内蔵ブラウザ・どのTVブランドで開いても縮小表示の対象として扱われるようになった。迷ったときは「fitを当てる側=人間優先」に倒す、という方針になっている。

UA名の判定を削って狭くした分、万一 ; wv) を持たない実機端末が出てきた場合は端末ではなく人間側の扱いになり、縮小表示が当たってしまう。これに対しては、その端末のsignage_urlに ?fit=off を付ける安全弁を用意している。これを付けると fitParam が "off" の時点で判定を確定させるため、UA名での判定には一切依存せず全画面表示に戻せる。

再発防止

この修正と同じコミットで、テストのフィクスチャとアサーションを直した。

it("スマート TV / Google TV の内蔵ブラウザは端末扱いしない(人間が開く=fit で崩れを防ぐ・2026-06-23 実機確認)", () => {
  // 旧実装は UA 名(Google TV/BRAVIA/CrKey…)で端末扱いしていたが、50inch スマートモニタの内蔵ブラウザで
  // 開いた盤面まで fit-stage が外れて崩れた。確実な WebView(`; wv)`)以外は人間優先=fit を当てる側に倒す。
  expect(isEmbeddedSignageDevice(UA.googleTvCast)).toBe(false);
  expect(isEmbeddedSignageDevice(UA.braviaTv)).toBe(false);
});

修正前のテストはこの2つのUAに対して true を期待していた。つまり「スマートTVの内蔵ブラウザは端末扱いする」という、今回崩れの原因になった挙動そのものがテストで固定されていた。期待値を反転させたことで、今後この2段目の判定を復活させるような変更を入れればテストが落ちる形になっている。

未指定: 実ブラウザ・スマート TV 内蔵ブラウザは適用 / 実機 WebView 端末は非適用 のテストでも、shouldApplyFitStage(undefined, UA.braviaTv) が true(=fitを当てる)であることを明示的に確認するアサーションを追加した。UAの判定基準を変えるだけでなく、「スマートTVの内蔵ブラウザは人間側として縮小表示の対象になる」という挙動自体をテストに固定したことで、同じ抜け方を機械的に検知できるようにしている。

よくある質問

Q1なぜUA名での判定が『人間には出ない語』のはずだったのですか

旧実装のコメントは『人間のPC・タブレットUAには現れない語に限定する』という前提でGoogle TV・Android TV・BRAVIA・Tizen等のTV名を拾っていました。ChromeやSafariのUAにこれらの語が出ないのは事実ですが、実機のスマートTVが持つ内蔵ブラウザ自体のUAにはTV名が含まれるため、人間がその内蔵ブラウザで開いた場合にも同じ語がヒットしてしまう抜けがありました。

Q2縮小表示(fit-stage)が外れると具体的に何が崩れるのですか

縮小表示は盤面を1920×1080の固定ステージに収めて『50インチ実機モニタ相当』の見た目にするためのものです。端末判定に引っかかって縮小表示が外れると盤面はそのまま全画面で描かれ、ヘッダーや天気の表示が画面外に出たり、人物の並びが潰れたりする崩れが起きました(2026-06-23に実機で確認)。

Q3直したあとはどの文字列だけで端末判定していますか

Android System WebViewのUAに必ず含まれる `; wv)` という文字列(空白の揺れは許容)だけです。tv-ble-bridgeの実機アプリはこのWebViewで盤面を表示するため、UA名での判定を削除してもこの1点だけで実機を拾えます。

Q4UA判定そのものが外れた実機が出た場合はどうするのですか

signage_urlに`?fit=off`を付ける安全弁を用意しています。UAで判定しなくても、この1クエリを個別の端末URLに付ければ強制的に全画面表示へ戻せます。

確認した環境

  • Next.js ^16.0.0 / React ^19.0.0(Server Component)
  • 2026-06-23に実機(50インチスマートモニタ)で発生を確認・同日修正

この記事の根拠

  • TypeScriptファイル 25〜35行目コミット 5ced859
  • TypeScriptファイル 29〜36行目コミット 39fdb3d
  • TypeScriptファイル 9〜45行目コミット 39fdb3d
  • TypeScriptファイル 56〜61行目コミット 39fdb3d

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。