このガイドでは、Flutter ウェブアプリで YouTube Playables SDK を使用する方法について説明します。
Flutter の構成
デフォルトの Flutter ウェブアプリを Playable として機能させるには、いくつかの変更が必要です。Flutter バージョン 3.24.5 以降では、次の変更が必要です。
デフォルトでは、Flutter はルート ディレクトリからリソースを読み込むように構成されていますが、Playables では、リソースをエントリ ポイントを基準に読み込む必要があります。これは 404 エラーとして表示されるか、JavaScript コンソールに「Refused to execute script
from... because its MIME type ('text/html') is not executable.
」というエラーとして表示されます。これを再構成する方法の一つは、index.html ファイルから次のタグを削除することです。
<base href="$FLUTTER_BASE_HREF">
デフォルトでは、Flutter は一部のライブラリのアプリへの埋め込みではなく、ライブラリを動的に読み込みます。CanvasKit がその一例です。これは、JavaScript コンソールに「Refused to connect to '...' because it
violates the following Content Security Policy directive: "connect-src 'self'
blob: data:".
」というエラーとして表示されます。また、読み込みに失敗すると、Failed to download any of the following CanvasKit URLs
などの追加ログが出力されることもあります。この問題を解決するには、次のフラグを追加してウェブアプリをビルドします。
$ flutter build web --no-web-resources-cdn
デフォルトでは、Flutter ウェブアプリはフォントをアプリに埋め込むのではなく、動的に読み込みます。デフォルトの Flutter アプリは Roboto フォントを使用しますが、これは JavaScript コンソールに Refused to connect to '...'
because it violates the following Content Security Policy directive:
"connect-src 'self' blob: data".
というエラーとして表示されます。これを修正するには、次の手順を実施します。
- Flutter ウェブアプリのルートに「fonts」という名前のフォルダを作成します。
- fonts.google.com から Roboto フォント ファミリーをダウンロードします。
- フォントを解凍し、Roboto-Regular.ttf をフォント フォルダにコピーします。
- Flutter ウェブアプリの pubspec.yaml に次の行を追加します。
fonts:
- family: Roboto
fonts:
- asset: fonts/Roboto-Regular.ttf
フォント構成の詳細については、Flutter のドキュメントをご覧ください。
SDK 統合
YouTube Playables SDK は、次のような JS 相互運用ラッパーを介して Flutter ウェブゲームから使用できます。
ytgame.dart
// Copyright 2023 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@JS()
library ytgame_api;
import 'package:js/js.dart';
import 'package:js/js_util.dart' as js_util;
enum PlayablesErrorType {
unknown('UNKNOWN'),
apiUnavailable('API_UNAVAILABLE'),
invalidParams('INVALID_PARAMS'),
sizeLimitExceeded('SIZE_LIMIT_EXCEEDED');
const PlayablesErrorType(String type) : _type = type;
final String _type;
@override
String toString() => _type;
static PlayablesErrorType fromString(String errorType) {
return values.firstWhere(
(element) => element._type == errorType,
);
}
}
@JS()
@staticInterop
class PlayablesError {}
extension PlayablesErrorExtension on PlayablesError {
@JS('errorType')
external String get _errorType;
PlayablesErrorType get errorType {
return PlayablesErrorType.fromString(_errorType);
}
}
@JS()
@anonymous
@staticInterop
class PlayablesScore {
external factory PlayablesScore({
required num value,
});
}
extension PlayablesScoreExtension on PlayablesScore {
external num get value;
}
// engagement
@JS()
@staticInterop
class PlayablesEngagement {}
extension PlayablesEngagementExtension on PlayablesEngagement {
@JS('sendScore')
external Object _sendScore(PlayablesScore score);
Future<void> sendScore(PlayablesScore score) {
return js_util.promiseToFuture(_sendScore(score));
}
}
// game
@JS()
@staticInterop
class PlayablesGame {}
extension PlayablesGameExtension on PlayablesGame {
external void firstFrameReady();
external void gameReady();
@JS('loadData')
external Object _loadData();
Future<String?> loadData() {
return js_util.promiseToFuture<String?>(_loadData());
}
@JS('saveData')
external Object _saveData(String? data);
Future<void> saveData(String? data) {
return js_util.promiseToFuture<void>(_saveData(data));
}
}
// health
@JS()
@staticInterop
class PlayablesHealth {}
extension PlayablesHealthExtension on PlayablesHealth {
external void logError();
external void logWarning();
}
// system
typedef PlayablesUnsetCallback = void Function();
typedef PlayablesOnAudioEnabledChange = void Function(bool isAudioEnabled);
typedef PlayablesOnPause = void Function();
typedef PlayablesOnResume = void Function();
@JS()
@staticInterop
class PlayablesSystem {}
extension PlayablesSystemExtension on PlayablesSystem {
external bool isAudioEnabled();
@JS('onAudioEnabledChange')
external PlayablesUnsetCallback _onAudioEnabledChange(
PlayablesOnAudioEnabledChange callback);
PlayablesUnsetCallback onAudioEnabledChange(
PlayablesOnAudioEnabledChange callback) {
return _onAudioEnabledChange(allowInterop(callback));
}
@JS('onPause')
external PlayablesUnsetCallback _onPause(PlayablesOnPause callback);
PlayablesUnsetCallback onPause(PlayablesOnPause callback) {
return _onPause(allowInterop(callback));
}
@JS('onResume')
external PlayablesUnsetCallback _onResume(PlayablesOnResume callback);
PlayablesUnsetCallback onResume(PlayablesOnResume callback) {
return _onResume(allowInterop(callback));
}
}
@JS()
@staticInterop
class Playables {}
extension PlayablesExtension on Playables {
@JS('SDK_VERSION')
external String get sdkVersion;
external PlayablesEngagement get engagement;
external PlayablesGame get game;
external PlayablesHealth get health;
external PlayablesSystem get system;
}
@JS('ytgame')
external Playables get ytgame;
用途
- 手順に沿って ウェブ SDK を設定、初期化します。
- Flutter ウェブアプリの
web/index.html
を変更して、必要なスクリプト タグを追加します。
- Flutter ウェブアプリの
- Flutter ウェブアプリの
src
にytgame.dart
のコピーを追加します。 flutter pub add js
コマンドを実行するなどして、js パッケージが Flutter に追加されていることを確認します。- 必要に応じて
import 'src/location/of/ytgame.dart';
を追加します。 ytgame
オブジェクトを使用して SDK にアクセスします。
例
Dart API の使用例をいくつか示します。
firstFrameReady
、gameReady
ゲームを正しく開始するには、最初のフレームを表示する準備ができたら firstFrameReady
を呼び出し、ゲームを操作する準備ができたら gameReady
を呼び出す必要があります。デフォルトの Flutter アプリでこれを行う方法の一つは、main.dart ファイルに呼び出しを追加することです。
...
import 'src/ytgame.dart';
...
void main() {
ytgame.game.firstFrameReady();
ytgame.game.gameReady();
runApp(const MyApp());
}
これはゲームを実行するための例にすぎません。適切な実装を行うには、これらの呼び出しのロケーションを適切に配置する必要があります。
sendScore
次の例は、Dart での sendScore(int points)
の実装を示しています。
...
import 'src/ytgame.dart';
...
/// Sends [points] as score to YT Playables
Future<void> sendScore(int points) async {
// Create a score object...
final PlayablesScore score = PlayablesScore(
value: points,
);
// Submit the score to ytgame
await ytgame.engagement.sendScore(score);
}
onPause
以下は、ゲームが YT Playables から送信される Pause
イベントをリッスンし、必要に応じてエンジンを一時停止する方法の例です。
...
import 'src/ytgame.dart';
...
/// The instance of your game.
late Game myGame;
/// Callback to stop listening for Pause events (cleanup).
PlayablesUnsetCallback? _unsetOnPause;
...
/// Sets a [callback] to respond to Pause events.
void registerOnPauseListener(PlayablesOnPause callback) {
_unsetOnPause = ytgame.system.onPause(callback);
}
/// Stops listening to Pause events, and clears [_unsetOnPause].
void unsetOnPauseListener() {
if (_unsetOnPause != null) {
_unsetOnPause!();
_unsetOnPause = null;
}
}
...
/// Initialization for your game
void initGame() {
...
myGame = ...
registerOnPauseListener(_onPause);
}
void _onPause() {
// This is called when a Pause event is received from YT Playables.
myGame.pauseEngine();
}
/// Teardown for your game
void disposeGame() {
...
unsetOnPauseListener();
}
YT Playables API リファレンスの全文をご覧ください。