Experimental YouTube Playables Flutter Web SDK

This guide explains how to use the YouTube Playables SDK with a Flutter Web app.

Flutter Configuration

The default Flutter web app requires some changes to work as a Playable. The following changes are required as of Flutter version 3.24.5.

By default, Flutter is configured to load resources from the root directory, whereas Playables requires that resources are loaded relative to the entry point. This will show up either as a 404 error, or possibly as a error that shows up in the javascript console that reads: Refused to execute script from... because its MIME type ('text/html') is not executable. One way to reconfigure this is to remove the following tag from the index.html file:

<base href="$FLUTTER_BASE_HREF">

By default, Flutter loads some libraries dynamically instead of embedding them in the app. CanvasKit is an example of this. This will show up as an error in the javascript console that reads: Refused to connect to '...' because it violates the following Content Security Policy directive: "connect-src 'self' blob: data:". Flutter may also output additional logs when loading fails, such as Failed to download any of the following CanvasKit URLs. To fix this, you can build your web app with the following additional flag:

$ flutter build web --no-web-resources-cdn

By default, Flutter web apps load fonts dynamically instead of embedding them in the app. The default flutter app uses the Roboto font, and this will show up as an error in the javascript console that reads: Refused to connect to '...' because it violates the following Content Security Policy directive: "connect-src 'self' blob: data". To fix this you can take the following steps:

  • Create a folder named "fonts" in the root of your flutter web app
  • Download the Roboto font family from fonts.google.com.
  • Extract the fonts, and copy the Roboto-Regular.ttf into the font folder
  • Add the following line to the pubspec.yaml of your flutter web app:
  fonts:
    - family: Roboto
      fonts:
       - asset: fonts/Roboto-Regular.ttf

More information about font configuration can be found in the Flutter documentation.

SDK Integration

The YouTube Playables SDK can be used from a Flutter Web game through a JS-interop wrapper similar to this one:

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;

Usage

  1. Follow the instructions to setup and initialize the web SDK.
    • Modify the web/index.html of your Flutter Web app to include the required script tags.
  2. Add a copy of ytgame.dart in the src of your Flutter web app.
  3. Make sure that the js package is added to Flutter, such as by running the command flutter pub add js.
  4. Add import 'src/location/of/ytgame.dart'; where needed.
  5. Use the ytgame object to access the SDK.

Examples

Find below a couple of examples of the Dart API in action.

firstFrameReady and gameReady

In order for your game to correctly start, you will need to call firstFrameReadywhen the first frame is ready to be displayed, and gameReady when the game is ready to be interacted with. One way to do this in the default Flutter app is to add the calls to the main.dart file:

...
import 'src/ytgame.dart';
...

void main() {
  ytgame.game.firstFrameReady();
  ytgame.game.gameReady();
  runApp(const MyApp());
}

Note that this is just an example to get your game running - you will need to properly place the location of these calls in order to provide the proper implementation.

sendScore

This example shows an implementation of sendScore(int points) in Dart:

...
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

This is an example of how a game can listen to Pause events coming from YT Playables, to pause its engine when needed:

...
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();
}

See the full YT Playables API reference.