BiliRoamingX の動画表示における画面回転制御について
モバイルデバイス上の動画視聴体験において、ディスプレイの向きを適切に切り替える機能は、ユーザー満足度に直結する重要な要素です。BiliRoamingX は B 站的な Android クライアントを強化したモジュールであり、その展開において画面の向きを動的に管理する際に直面するいくつかの課題が存在します。本稿では、このプロジェクト内で採用されている回転ロジックの詳細を検証し、発生しうる不具合や改善策を検討します。
Android プラットフォーム上の方向管理基盤
Android OS では、ActivityInfo.SCREEN_ORIENTATION_* に定義された定数群を利用して、アクティビティの表示方向を定義しています。
<code>// 主な方向制御定数 ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED // 既定値を利用 ActivityInfo.SCREEN_ORIENTATION_PORTRAIT // 縦横比を固定(縦) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE // 横画面へ固定 ActivityInfo.SCREEN_ORIENTATION_BEHIND // アクティビティスタック内の前画面に合わせる ActivityInfo.SCREEN_ORIENTATION_SENSOR // センサー入力に基づく自動回転</code>
BiliRoamingX では、この基礎的な機能を拡張し、主に PlayURLPlayViewUGC クラス内のフック処理を通じて制御を実現しています。
システム内での方向制御ロジックの解説
以下のコード例は、元々の実装の意図を保持しつつ、構造的に再設計したバージョンを示します。
<code>class VideoOrientationManager : MossHook<PlayViewReq, PlayViewReply>() {
override fun hookAfter(
req: PlayViewReq,
reply: PlayViewReply?,
error: MossException?
): PlayViewReply? {
if (reply != null) {
// 高解像度版判定かつ回転ロック無効か確認
if (SystemUtil.isHighDefinitionVersion() && PrefHelper.allowAutoRotate()) {
val streamData = reply.videoInfo.streamListList
.find { it.hasDashVideo() }?.dashVideo
if (streamData != null) {
val displayWidth = streamData.width
val displayHeight = streamData.height
// メインスレッドで UI 操作を実行
MainThreadExecutor.execute {
val currentTopActivity = AppContext.topActiveWindow()
if (currentTopActivity is VideoDetailsActivity) {
val targetRotator = if (displayHeight > displayWidth) {
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
} else {
ActivityInfo.SCREEN_ORIENTATION_BEHIND
}
currentTopActivity.requestedOrientation = targetRotator
}
}
}
}
}
return super.hookAfter(req, reply, error)
}
}
</code>
主要な分析点
1. アスペクト比による判断基準
動画の物理的な幅と高さの比較に基づき、自動的に最適な向きを推測する処理が組まれています。
2. 発動条件の厳格さ
機能が有効になるには、以下の条件を両方満たす必要があります。
| チェック項目 | 内容説明 | 初期値 |
|---|---|---|
SystemUtil.isHighDefinitionVersion() |
アプリケーションが高画質(HD)エディションであるか | インストールパッケージ名からの判定 |
PrefHelper.allowAutoRotate() |
設定メニューで「回転ロック解除」がオンの状態か | false(デフォルト) |
3. スレッド安全性
UI スレッド以外から直接画面設定を変更しようとするとクラッシュの原因となるため、必ずメインスレッド経由で処理を行います。
<code>UiThreadRunner.run {
val act = AppManager.getForegroundActivity()
if (act is VideoDetailsActivity) {
act.requestedOrientation = mode
}
}</code>
典型的なトラブルシューティング
事象:回転が反映されない
想定される原因:
- 標準版 APK で動作している可能性
- ユーザ設定で自動回転が OFF
- 動画ストリームのメタデータ読み取り失敗
修正アプローチ:
<code>val pkgName = context.packageName
if (pkgName == "tv.danmaku.bilibilihd") {
if (PrefHelper.autoRotateEnabled) {
// ロジック実行
}
}</code>
事象:切り替え時のちらつき
頻繁なリフレッシュによる再描画が視覚的なノイズを生む場合、アスペクト比に閾値を加えることで安定させます。
<code>val ratio = h.toFloat() / w.toFloat()
val mode = when {
ratio > 1.3 -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
ratio < 0.75 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}</code>
事象:複数アクティビティ間の同期ミス
ナビゲーションによって遷移した際、前のページの設定が残ってしまう問題を防ぐため、グローバルな状態管理クラスを導入します。
<code>object GlobalOrientationController {
private var activeMode = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
fun apply(target: Int) {
activeMode = target
AppManager.findAllActivities().forEach { it.requestedOrientation = target }
}
}</code>
開発推奨事項
優先順位の明確化
ユーザーの手動設定は、システムの自動判定よりも優先させるべきです。
<code>when (UserSettings.rotationPolicy) {
"automatic" -> autoDetect(videoMetadata)
"portrait" -> SCREEN_ORIENTATION_PORTRAIT
"landscape" -> SCREEN_ORIENTATION_LANDSCAPE
else -> SCREEN_ORIENTATION_UNSPECIFIED
}</code>
エラーハンドリング
予期せぬ例外によりアプリが強制終了しないよう、ロールバック機制を設けます。
<code>try {
setupRotation(safeConfig)
} catch (ex: SecurityException) {
Log.w("Rotation", "Failed", ex)
fallbackToDefaultLayout()
}</code>
パフォーマンスへの配慮
不要な方向変更は避けるため、現在の設定と比較して差分がある場合にのみ更新処理を行います。
<code>var cachedRotation = -1
fun update(current: Int) {
if (current != cachedRotation) {
cachedRotation = current
executeChange(current)
}
}</code>
方式の比較評価
| 手法 | メリット | デメリット | 推奨シナリオ |
|---|---|---|---|
| 動画比率検知 | コンテンツに適応 | 誤認識リスク | 一般的なビデオ視聴 |
| 手動指定 | 正確なコントロール | 操作コスト | マニア向け設定 |
| 加速度センサー | 自然な挙動 | 消費電力増大 | 日常利用 |
| ハイブリッド | バランス型 | 実装負荷増 | 総合推奨 |
将来の技術的展望
行動学習による最適化
ユーザーの利用履歴を分析し、個人の好みに基づいて推薦を行うアルゴリズムの導入が期待されます。
<code>val preference = analyzeHistoryPattern() val finalSetting = merge(preference, aspectRatio)</code>
シチュエーション別対応
- ショート動画:強制的な縦向き維持
- 長時間コンテンツ:横画面推奨
- ライブ配信:センサ追従モード
- ピクチャーインピクチャー:親画面の維持
バージョン互換性の担保
OS レベルやアプリ改変による API 変動に対応するため、機能サポートチェックを実装する必要があります。
<code>fun verifyCompatibility(): Boolean {
return SDK_INT >= O AND AppVersionCheck.compare("7.0.0")
}</code>