Upgrade to Pro — share decks privately, control downloads, hide ads and more …

動画プレイヤーはなぜカクつくのか 〜Flutterのレンダリングとネイティブの境界を解剖する〜

動画プレイヤーはなぜカクつくのか 〜Flutterのレンダリングとネイティブの境界を解剖する〜

FlutterKaigi2026 Session

Avatar for Naruo Yoshiki

Naruo Yoshiki

October 29, 2026

Other Decks in Programming

Transcript

  1. 株式会社サイバーエージェント / FANBASE事業部 Flutterのレンダリングと ネイティブの境界を解剖する 成尾 嘉貴 NARUO YOSHIKI 職種:

    バックエンド&アプリエンジニア 技術: Flutter / Swift / Kotlin / Go / GCP Day 1 10.29 THU 14:10–14:40 経歴: サイバーエージェント2024年度新卒入社 X: @naruogram Yoshiki Naruo
  2. OS:描画結果を合成して表示する Scene 描画命令 描画結果 UI Raster GPU OS 構成・配置・描画命令 GPUへ描画を指示

    描画を実行 合成して表示 アプリなどの描画結果を合成し、 ディスプレイへの表示につなぐ 描画の完了と、実際の表示は別の時点 25
  3. Flutter UIが画面に届くまでの流れ Dart Flutter Engine Scene ハードウェア 描画命令 プラットフォーム 描画結果

    UI Raster GPU OS 構成・配置・描画命令 GPUへ描画を指示 描画を実行 合成して表示 準備する 指示する 描く 表示する 26
  4. Flutter UIが画面に届くまで Dart Flutter Engine Scene ハードウェア 描画命令 プラットフォーム 描画結果

    UI Raster GPU OS 構成・配置・描画命令 GPUへ描画を指示 描画を実行 合成して表示 準備する 指示する 描く 表示する これでFlutter UIの描画経路を整理できました 27
  5. Flutter UIが画面に届くまで Dart Flutter Engine Scene ハードウェア 描画命令 プラットフォーム 描画結果

    UI Raster GPU OS 準備する 指示する 描く 表示する ここで、シェーダーの準備に伴う遅れを 構成・配置・描画命令 GPUへ描画を指示 描画を実行 合成して表示 減らす仕組み、Impellerを見てみます 28
  6. Impeller:シェーダーの事前準備 RasterはImpellerを使って、GPUへの描画を進める UI Raster GPU OS Build / Layout /

    Paint Impeller 描画を実行 合成・表示 シェーダー:GPUで使う描画プログラム Engineのビルド時にシェーダーを準備済み 実行時のコンパイルに伴う遅れを減らす 29
  7. Flutter UIが画面に届くまで Dart Flutter Engine Scene ハードウェア 描画命令 プラットフォーム 描画結果

    UI Raster GPU OS 準備する 指示する 描く 表示する ここまでで、描画の流れが分かりました 構成・配置・描画命令 GPUへ描画を指示 描画を実行 合成して表示 次は、表示に間に合わない場合を見てみます これでFlutter UIの描画経路を整理できました 30
  8. 画面の更新タイミング(固定60 Hzの例) 0 ms 16.67 ms 33.33 ms 60 Hz=画面を1秒に60回更新

    固定60 Hzなら、約16.67 msごとに更新 実際のHzは、端末・設定・動作状況で変わる 31
  9. 次の表示までに、描画結果を用意する 0 ms 16.67 ms 描画 Frame Bを描画 表示 Frame

    A 33.33 ms Frame B 次の表示機会にBへ更新 固定60 Hzの概念図 32
  10. 表示に間に合わないと、前の画面が残る 0 ms 描画 表示 16.67 ms 33.33 ms Frame

    Bを描画 Frame A Frame Aを継続 Bが間に合わず、Aが残る 固定60 Hzの概念図 33
  11. 表示に間に合わないと、前の画面が残る 0 ms 描画 16.67 ms 33.33 ms Frame Bを描画

    表示が間に合わず、カクつきが発生 表示 Frame A Frame Aを継続 Bが間に合わず、Aが残る 固定60 Hzの概念図 34
  12. 表示に間に合わないと、前の画面が残る 0 ms 描画 16.67 ms 33.33 ms Frame Bを描画

    同じ映像フレームが続いても 処理落ちとは限りません 映像のfpsと画面のHzを比べてみます 表示 Frame A Frame Aを継続 Bが間に合わず、Aが残る 固定60 Hzの概念図 35
  13. 30 fpsの映像は、60 Hzの画面で同じコマを2回ずつ表示する 例:30 fpsの映像を、固定60 Hzで表示 A・B・Cは映像の各コマ(図は100 ms分) 映像 30

    fps A B C fps(フレーム/秒) 映像のコマ数を表す 1秒に30コマ 画面 60 Hz A A B B C C Hz(ヘルツ) 画面の更新回数を表す 1秒に60回更新 同じコマを2回ずつ表示して正常。処理落ちではない ※画面のHzは、端末や設定・動作状況で変わる 36
  14. 30 fpsの映像は、60 Hzの画面で同じコマを2回ずつ表示する 例:30 fpsの映像を、固定60 Hzで表示 A・B・Cは映像の各コマ(図は100 ms分) 映像 30

    fps A B C この例のコマの繰り返しは正常です fps(フレーム/秒) 映像のコマ数を表す 次は、処理の遅れを調べるために 1秒に30コマ UIとRasterの動きを追ってみましょう 画面 60 Hz A A B B C C Hz(ヘルツ) 画面の更新回数を表す 1秒に60回更新 同じコマを2回ずつ表示して正常。処理落ちではない ※画面のHzは、端末や設定・動作状況で変わる 37
  15. Texture:映像をFlutterの描画に取り込む Flutter UIの描画情報と映像フレームを、Rasterが描画に使います Flutter Engine 描画内容 Flutter UI 表示する内容を指定 Scene

    映像の位置・サイズ Raster 映像バッファ ネイティブ側 Impellerで描画を準備 映像フレーム Texture GPU・OSへ続く 映像フレームの更新自体には、通常Dartの実行は不要 52
  16. 映像とUIは、別々の経路で更新される Textureの場合:映像フレームはRasterへ、再生位置などの値はFlutter UIの更新へ Scene 描画命令 描画結果 UI Raster GPU OS

    Build / Layout / Paint Impeller 描画を実行 合成・表示 状態の値 映像フレーム 再生位置など 映像 ネイティブ Texture プレイヤー 外部映像を取り込む 映像フレームごとの Buildは不要 54
  17. TextureとPlatformViewは、何を取り込むかが違う 比較すること Texture PlatformView 取り込むもの 動画の各コマ (映像フレーム) iOS / Android側の

    映像表示部品 表示方法 Flutterが映像を描画する 映像表示部品を Flutterの画面に配置する ボタンやコメントは、どちらでもFlutterで作れる 58
  18. Android:Texture Layer Hybrid Composition もう1つは、Viewの描画結果を画像としてFlutterに渡す方式 Flutter UI Raster Impeller Flutterの

    描画結果 この後、OSの合成・表示へ 映像表示部品 Viewの描画結果 Texture SurfaceViewを含むと、配置・重なり順に制約 64
  19. Android:合成方式で、性能の傾向が変わる 比較すること Hybrid Composition Texture Layer Hybrid Composition 表示の仕方 Androidの画面部品として

    Flutter UIと重ねる 再生画面を画像に描画し、 Flutter UIと合成する 性能の傾向 通常のView階層で表示 FlutterのFPSは低下しうる Flutterの描画性能を 保ちやすい 負荷・弱点 RasterとPlatformが統合 ネイティブ処理が描画を待たせる SurfaceViewの位置・重なり 高速スクロール時に制約 判断の軸 SurfaceViewを含む映像部品 通常のView階層で表示 通常のView描画が対象 SurfaceViewには不向き SurfaceView:映像を別のSurface(描画先)に出力するView 通常のViewの描画を取り込む方法では、配置・重なり順が崩れる場合がある 66
  20. iOS Texture:映像フレームをFlutterが取得して描画 CVPixelBuffer=映像フレームの画素データを保持するiOSのオブジェクト Flutter Engine 描画内容 Flutter UI 表示する内容を指定 Scene

    映像の位置・サイズ Raster CVPixelBuffer iOS側 Impellerで描画を準備 映像フレーム Texture GPU・OSへ続く 確認:フレーム取得・更新通知・復帰後の映像更新 69
  21. 3つの対象ごとに、症状と計測項目を整理する 切り分ける対象 症状の例 最初に計測・記録する項目 Flutterの描画 UIのアニメーションや スクロールがカクつく UI / Rasterの処理時間

    操作への応答 再生・シークの操作に 反応するまでが遅い 要求の往復時間 ネイティブ処理時間 映像の出力 UIは動くが 映像だけが進まない プレイヤーの状態 映像の更新・出力先 75
  22. UI / Rasterの処理時間を、1更新周期と比較する UIとRasterの合計ではなく、それぞれの処理時間で判断する 1,000 ÷ リフレッシュレート(Hz)= 更新周期(ms) 60 Hz:約16.7

    ms 120 Hz:約8.3 ms 例:60 HzでUIが20 msなら、約16.7 msの更新周期を超える ※UI・Rasterが更新周期を超えていなくても、表示が遅れる場合がある 79
  23. シークが遅いとき、どこで時間がかかっているか シーク=再生位置の変更。MethodChannelでDartからネイティブへ依頼する Dart ネイティブ側の処理 呼び出し #42 同じ呼び出しを #42 で識別 Dart・ネイティブ両側に記録

    映像への反映は別に確認 結果受信 #42 処理開始 #42 シークを実行 結果返却 #42 Dart:呼び出し→結果受信 ネイティブ:処理開始→結果返却 84
  24. Main threadが処理中だと、次の処理は待たされる UIとPlatformは標準で統合:iOS / Android(Flutter 3.29以降) 同じスレッドでは、複数の処理を同時に実行できない Main thread UI

    Dartの処理 Platform OS・ネイティブ連携 長い処理の間は、UI更新や操作の処理が待機 Raster Androidの従来の Hybrid Compositionでは Rasterも統合 86
  25. iOS:Instrumentsで処理と待機を調べる Instruments:Xcodeに付属する性能解析ツール Main threadの処理時間と待機時間を確認 Time Profiler CPU時間の大きい関数と その呼び出し元を確認 Thread State

    Trace CPUを使っていない時間を確認 ロックや他の処理の完了を 待っていないかを調べる Main thread:OSのUI処理などを実行するスレッド 88
  26. Android:System Traceで処理と待機を調べる System Trace:アプリとOSの動作を時系列に記録したもの Main threadの処理時間と待機時間を確認 記録する Android StudioのProfilerで記録 症状が出た時間帯を選ぶ

    状態を確認する Running:CPUで実行中 Runnable:実行可能だがCPU待ち Sleepingなど:イベントやロック待ち Perfetto UI:記録したシステムトレースを表示・解析できるツール 89
  27. 計測結果に合わせて、試す対策を選ぶ 調べた対象 計測・記録した状況 試す対策 位置更新で Buildの対象が広がる 位置ラベルだけで 再生位置の通知を受ける Paint・合成に 時間がかかる

    再描画の範囲と 描画効果を見直す 操作への応答 通知の受信処理などが Main threadを占有 通知頻度と 処理の実行先を見直す 映像の出力 シーク・復帰後に 映像が更新されない 更新通知と 出力先を確認する Flutterの描画 97
  28. 単色の背景なら、Opacityを色のalphaに置き換える alpha:色の不透明度(0で透明、1で不透明) 変更前 変更後 Dart Opacity( opacity: 0.5, child: const

    ColoredBox( color: Colors.black, child: SizedBox.expand(), ), ) Dart ColoredBox( color: Colors.black .withValues(alpha: 0.5), child: const SizedBox.expand(), ) 同じ見た目になるのは単色背景の例。子全体を半透明にする場合は置き換えない。 105
  29. Android:チャネルのハンドラを別スレッドで実行する Kotlin:独自プラグインのonAttachedToEngine内(抜粋) Kotlin val messenger = binding.binaryMessenger val queue =

    messenger.makeBackgroundTaskQueue() channel = MethodChannel( messenger, "app/video/metadata", StandardMethodCodec.INSTANCE, queue ) channel.setMethodCallHandler(this) iOSにもFlutterBinaryMessenger.makeBackgroundTaskQueue()がある 対象は分離できる計算・I/O。View操作やPlayer APIは指定スレッドで実行する 115
  30. AndroidのPlatformViewは、合成方式も選ぶ 対応条件を満たす端末 Android API 34以降 Impeller Vulkan Hybrid Composition++ を検討

    対応条件を満たさない端末 古いOS・Vulkan非対応など 設定済みの既存方式へ フォールバック Flutter 3.44以降で利用可能(実験的・オプトイン) AndroidManifest.xmlで明示的に有効化 123
  31. 【おまけ】プレイヤー構成診断 video_playerのAPIで 必要な機能を扱える? Yes No video_player 拡張・独自実装など ネイティブViewの 表示機能が必要? ネイティブViewの

    表示機能が必要? Yes No Yes No PlatformView まずTexture PlatformView まずTexture Hybrid Composition++は実験的・オプトイン(Flutter 3.44以降) Android API 34以降・Impeller Vulkanが必要。設定で有効化 124
  32. 参考資料 SurfaceProducer API api.flutter.dev/javadoc/io/flutter/view/TextureRegistry.SurfaceProducer.html FlutterTextureRegistry textureFrameAvailable api.flutter.dev/ios-embedder/protocol_flutter_texture_registry-p.html FrameTiming API api.flutter.dev/flutter/dart-ui/FrameTiming-class.html

    Platform channels and threading docs.flutter.dev/platform-integration/platform-channels Flutter architecture / thread merging docs.flutter.dev/resources/architectural-overview Android Platform Views(Hybrid Composition++ / Texture Layer) docs.flutter.dev/platform-integration/android/platform-views 126