SDK experimental da Sala de jogos do YouTube para Flutter

Este guia explica como usar o SDK Playables do YouTube com um app da Web do Flutter.

Configuração do Flutter

O app da Web Flutter padrão precisa de algumas mudanças para funcionar como um app jogável. As seguintes mudanças são obrigatórias a partir da versão 3.24.5 do Flutter.

Por padrão, o Flutter é configurado para carregar recursos do diretório raiz, enquanto o Playables exige que os recursos sejam carregados em relação ao ponto de entrada. Isso vai aparecer como um erro 404 ou possivelmente como um erro que aparece no console do JavaScript que diz: Refused to execute script from... because its MIME type ('text/html') is not executable. Uma maneira de reconfigurar isso é remover a seguinte tag do arquivo index.html:

<base href="$FLUTTER_BASE_HREF">

Por padrão, o Flutter carrega algumas bibliotecas dinamicamente em vez de incorporá-las ao app. O CanvasKit é um exemplo disso. Isso vai aparecer como um erro no console JavaScript, que diz: Refused to connect to '...' because it violates the following Content Security Policy directive: "connect-src 'self' blob: data:". O Flutter também pode gerar registros adicionais quando o carregamento falha, como Failed to download any of the following CanvasKit URLs. Para corrigir isso, crie seu app da Web com a seguinte flag adicional:

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

Por padrão, os apps da Web do Flutter carregam fontes dinamicamente em vez de incorporá-las ao app. O app padrão do Flutter usa a fonte Roboto, que aparece como um erro no console do JavaScript: Refused to connect to '...' because it violates the following Content Security Policy directive: "connect-src 'self' blob: data". Para corrigir isso, siga estas etapas:

  • Crie uma pasta chamada "fonts" na raiz do seu app da Web do Flutter.
  • Faça o download da família de fontes Roboto em fonts.google.com.
  • Extraia as fontes e copie Roboto-Regular.ttf para a pasta de fontes.
  • Adicione a linha a seguir ao pubspec.yaml do seu app da Web do Flutter:
  fonts:
    - family: Roboto
      fonts:
       - asset: fonts/Roboto-Regular.ttf

Mais informações sobre a configuração de fontes podem ser encontradas na documentação do Flutter.

Integração do SDK

O SDK Playables do YouTube pode ser usado em um jogo da Web do Flutter usando um wrapper de interoperabilidade com JS semelhante a este:

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;

Uso

  1. Siga as instruções para configurar e inicializar o SDK da Web.
    • Modifique o web/index.html do app da Web do Flutter para incluir as tags de script necessárias.
  2. Adicione uma cópia de ytgame.dart no src do seu app da Web do Flutter.
  3. Verifique se o pacote js foi adicionado ao Flutter, por exemplo, executando o comando flutter pub add js.
  4. Adicione import 'src/location/of/ytgame.dart'; onde for necessário.
  5. Use o objeto ytgame para acessar o SDK.

Exemplos

Confira abaixo alguns exemplos da API Dart em ação.

firstFrameReady e gameReady

Para que o jogo seja iniciado corretamente, você precisa chamar firstFrameReady quando o primeiro frame estiver pronto para ser exibido e gameReady quando o jogo estiver pronto para interação. Uma maneira de fazer isso no app Flutter padrão é adicionar as chamadas ao arquivo main.dart:

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

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

Esse é apenas um exemplo para colocar o jogo em funcionamento. Você vai precisar colocar o local dessas chamadas corretamente para fornecer a implementação adequada.

sendScore

Este exemplo mostra uma implementação de sendScore(int points) em 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

Este é um exemplo de como um jogo pode detectar eventos Pause provenientes de jogos do YouTube para pausar o mecanismo quando necessário:

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

Consulte a referência completa da API YT Playables.