狠狠撸

狠狠撸Share a Scribd company logo
Cast SDK for Flutter
山田 幸司
koji.yamada@gree.net
グリー株式会社
開発本部 / インフラストラクチャ部 / ディベロップメント
オペレーションズグループ / サービスディベロップメント
チーム 所属
業務内容
VRアプリフロントエンド担当
最近のお仕事はFlutter / Unity / Unreal Engine 4など
アジェンダ
● Chromecastについて
○ Chromecastとは?
○ Cast SDKについて
● Cast SDK for Flutterを作る
○ Flutterでプラグインを作る準備やインストール方法など
○ 構成から実装のおおまかな流れ
○ 実装方法などについて
Chromecast
● Googleが開発?販売する小型のデバイス
● HDMI端子に接続/Wi-Fiを介してスマートフォンやタブレットなどで再生して
いる動画、写真、ウェブサイトなどをディスプレイに表示出来るデバイス
● ミラーリングとはやや違ってスマートフォンをコントローラとして扱い動画
の再生や停止などもできる
● 値段は5,000円程度(4K対応のUltraは10,000円程度)
● アプリで利用するにはCast SDKを組み込む必要がある
※↑アプリが対応しているかつ周辺にChromecast
がある場合はこのアイコンが表示される
Cast SDK
● Googleが提供するChromecast用のSDK
○ https://developers.google.com/cast/
● Android/iOS/Chrome(ブラウザ)に対応
○ Android → Java/Kotlin
○ iOS → Objective-C/Swift
○ Chrome → Javascript
● 公式のFlutter用Cast SDKはまだ存在しない
● Flutter版もいつか対応するかも
○ GitHubのissuesはあがっている
○ https://github.com/flutter/flutter/issues/18212
Cast SDK for Flutter
● 今日の話
○ Flutter用のChromecast Pluginを作る話
○ ChromecastのAndroid/iOS両方に対応するアプリをさく
っと作れる
● 実装するもの
○ 送信側(Sender)と受信側(Receiver)を作る必要があ
りますが、今回はSenderアプリのみについて
○ 動画コントロールパネルやキャストボタンなどのUI部分
はFlutterで実装
○ 動画をChromecastにキャスト/再生と停止ができる
準備
● Plugin Packageプロジェクトの作成
○ $ flutter create --template=plugin -i swift -a kotlin [プロジェクト名]
例) chrome_cast_plugin
Android/build.gradle
…
dependencies {
implementation 'com.google.android.gms:play-services-cast-framework:16.1.2'
...
iOS/Podfile
…
target 'Runner' do
pod 'google-cast-sdk', '~> 4.3'
...
Sync Now
pod install
構成
app
main.dart
lib
chrome_cast_
plugin.dart
Flutter/Dart
Android/Kotlin
iOS/Swift
CastOptionsProvider.kt
ChromeCastPlugin.kt
SwiftChromeCast
Plugin.swift
Cast SDK for Android
gms:play-services-cast
Cast SDK for iOS
google-cast-sdk
● main.dart
○ キャストボタンなどのUI表示
● chrome_cast_plugin.dart
○ Kotlin/Swiftとのつなぎ
プラグイン側
Pluginで実装すること
1. Cast Contextの初期化
2. Cast Dialogの表示
3. Cast Buttonの表示
4. リスナーの登録
5. 動画をキャストする
6. キャストした動画を制御する
Cast Contextの初期化
● Chromecast の機能を使うために必要
● Android
○ OptionsProviderインタフェースを実装したクラスの作成
○ レシーバーIdのCastOptionsのインスタンスを作る
○ CastContext.getSharedInstance(Activity)で初期化
● iOS
○ レシーバーIdのGCKCastOptionのインスタンスを作る
○ GCKCastContext.setSharedInstanceWith(GCKCastOption)で初期化
● 初期化タイミング
○ pluginのregisterWith / register
class ChromeCastPlugin(private val pActivity: Activity, private val pChannel:
MethodChannel):
…
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
CastContext.getSharedInstance(pActivity)
…
Android/Kotlin
static const MethodChannel _channel =
const MethodChannel('chrome_cast_plugin');
chrome_cast_plugin.dart
public class SwiftChromeCastPlugin: NSObject, FlutterPlugin {
…
public static func register(with registrar: FlutterPluginRegistrar) {
let tCriteria = GCKDiscoveryCriteria(applicationID: [レシーバーのId])
let tOption = GCKCastOptions(discoveryCriteria: tCriteria)
GCKCastContext.setSharedInstanceWith(tOption)
…
iOS/Swift
class CastOptionsProvider : OptionsProvider {
…
override fun getCastOptions(context: Context): CastOptions? {
val tAppId = context.resources.getIdentifier("chromecast_app_id", "string", context.packageName)
…
return CastOptions.Builder()
.setReceiverApplicationId(context.getString(tAppId))
.build()
}
…
}
Android/Kotlin
Cast Contextの初期化(Android CastOptionsProvider.kt)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="chromecast_app_id">[レシーバーId]</string>
</resources>
app/res/values/string.xml
Cast Dialogの表示
● キャストしてないとき
○ キャスト可能なデバイス一覧を表示
● キャストしてるとき
○ メディア情報、音量などを表示
● Android(少々手間がかかる)
○ 現在キャスト中かどうかを判定して出し分けが必要
○ 現在のキャストセッションがあれば
MediaRouteControllerDialog
○ 無い場合はMediaRouteChooserDialogを呼ぶ
● iOS
○ presentCastDialogを呼ぶだけ
private fun showDialog(pCall: MethodCall, pResult: Result) {
val tCastSession = CastContext.getSharedInstance()?.sessionManager?.currentCastSession
if (tCastSession != null) {
val tControllerDialog = MediaRouteControllerDialog(pActivity,
R.style.CastControllerDialogTheme)
tControllerDialog.show()
} else {
val tChooserDialog = MediaRouteChooserDialog(pActivity, R.style.CastMediaRouterTheme)
tChooserDialog.routeSelector = CastContext.getSharedInstance()?.mergedSelector!!
tChooserDialog.show()
}
}
Android/Kotlin
Future<void> showCastDialog() async {
await _channel.invokeMethod('ShowCastDialog');
}
chrome_cast_plugin.dart
private func showDialog(_ pCall: FlutterMethodCall, _ pResult: @escaping FlutterResult) {
let tContext:GCKCastContext = GCKCastContext.sharedInstance()
tContext.presentCastDialog()
}
iOS/Swift
動画をキャストする
● 動画をキャストするために必要な情報をFlutterからPlugin側に渡す
● 手順
○ [1] キャストするための必要なメタデータ(Metadata)を作成するためのパ
ラメータ
■ タイトル、キャストダイアログに出す画像など
○ [2] 作成したメタデータからメディア情報(MediaInfo)を作成するためのパ
ラメータ
■ 動画のコンテンツタイプ(mp4など)、レシーバーに渡す独自のJsonデータなど
○ [3] 必要に応じて読み込み時のオプションを作成するためのパラメータ
■ 再生位置、自動再生するかどうかなど
○ [4] 動画をデバイスにキャスト
Future<bool> startCast(Media media) async {
bool _result =
await _channel.invokeMethod('StartCast', {
'Uri': media.url,
'Title': media.title,
'Subtitle': media.subTitle,
'Studio': media.studio,
'ContentType': media.contentType,
'Images': {
'Dialog': {
'Url': media.dialogThumbnail.url,
'Width': media.dialogThumbnail.width,
'Height': media.dialogThumbnail.height
},
'Notification': {
'Url': media.notificationThumbnail.url,
}
},
chrome_cast_plugin.dart
'IsAuto': media.isAuto,
'PlayPosition': media.position,
'IsLive': media.isLive,
'CustomData': media.customData
});
return _result;
}
...
class Media {
Media({
this.url,
this.title,
this.subTitle,
this.studio,
this.contentType:,
this.customData: const {},
this.isAuto,
this.position,
this.isLive,
this.dialogThumbnail,
this.notificationThumbnail,
}) :
...
private fun startCast(pCall: MethodCall, pResult: Result) {
val tArguments = pCall.arguments as? Map<String, Any>
…
// [1] メタデータの設定
val tMovieMetadata =
MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE)
tMovieMetadata.putString(MediaMetadata.KEY_TITLE, tTitle)
tMovieMetadata.addImage(WebImage(Uri.parse(tDialogUrl), tWidth,
tHeight))
tMovieMetadata.addImage(WebImage(Uri.parse(tNotificationUrl)))
…
// [2] メディア情報の作成
val tMediaInfo = MediaInfo.Builder(tUri)
.setStreamType(tStreamType)
.setContentType(tContentType)
.setMetadata(tMovieMetadata)
.setCustomData(tJsonObject)
.build() …
Android/Kotlin
// [3] 読み込み時のオプション
val tMediaLoadOptions: MediaLoadOptions =
MediaLoadOptions.Builder()
.setAutoplay(tIsAuto)
.setPlayPosition(tPlayPosition.toLong())
.build()
…
// [4] Chromecastにキャスト
val tIsSuccess = (tCastSession?.remoteMediaClient?.load(tMediaInfo,
tMediaLoadOptions) != null)
pResult.success(tIsSuccess)
}
private func startCast(_ pCall: FlutterMethodCall, _ pResult: @escaping
FlutterResult) {
let tArguments = pCall.arguments as? Dictionary<String, Any>
…
// [1] メタデータの設定
let tMetadata:GCKMediaMetadata = GCKMediaMetadata(metadataType:
GCKMediaMetadataType.movie)
tMetadata.setString(tTitle, forKey: kGCKMetadataKeyTitle)
tMetadata.addImage(GCKImage(url: URL(/string: (tDialogUrl)), width:
tWidth, height: tHeight))
…
// [2] メディア情報の作成
let tBuilder = GCKMediaInformationBuilder.init(contentURL: tParseUrl)
tBuilder.contentID = tUri
tBuilder.streamType = tStreamType
tBuilder.contentType = tContentType
tBuilder.metadata = tMetadata
tBuilder.customData = tJsonData …
// [3] 読み込み時のオプション
let tMediaLoadOption = GCKMediaLoadOptions();
tMediaLoadOption.autoplay = tIsAuto
tMediaLoadOption.playPosition = tPlayPosition
…
// [4] Chromecastにキャスト
var tIsSuccess = true;
if (tCastSession?.remoteMediaClient?
.loadMedia(tMediaInfo, with: tMediaLoadOption)) != nil {
tIsSuccess = true;
} else {
tIsSuccess = false
}
pResult(tIsSuccess)
}
iOS/Swift
実演(动画)
● 仕様
○ Android / iOSで動作するFlutterプラグイン及びアプリ
■ Flutter : 1.2.1
■ Android : Kotlin 1.3.11 / gms-play-service : 16.1.2
■ iOS : Swift 4.2.1 / google-cast-sdk : 4.3.5
● 開発エディタ
○ VSCode
○ Android Studio
○ Xcode
● Google Cast Document
○ https://developers.google.com/cast/docs/developers
● Google Cast GitHub(Android/iOS)
○ https://github.com/googlecast/CastVideos-ios
○ https://github.com/googlecast/CastVideos-android
ここから先のスライドは補足
補足 iOS/Xcodeで開発する場合の注意
● Xcode10以上、iOS 12以降をターゲットにしている場合
● Access Wifi InformationをONにする
Cast Buttonの表示
● キャスト可能なデバイスがあるときはアイコンを表示、接続中は接続中のア
イコン、キャスト可能なデバイスがない場合は非表示に切り替える
● Cast StatusをAndroid/iOSのプラグイン側から取得
● Cast Statusの状態をもとにFlutter側でアイコンの表示?非表示を切り替える
キャストしてない キャスト中
private fun getCastState(pCall: MethodCall, pResult: Result) {
val tCastContext = CastContext.getSharedInstance()
val Status = tCastContext.castState
pResult.success(tStatus)
}
Android/Kotlin
enum CastStatus {
NO_DEVICES_AVAILABLE,
NOT_CONNECTED,
CONNECTING,
CONNECTED,
}
…
Future<CastStatus> getCastStatus() async {
CastStatus _castStatus;
int _result = await _channel.invokeMethod('GetCastStatus');
_castStatus = _getCastStateByValue(_result);
return _castStatus;
}
…
CastStatus _getCastStateByValue(int result) {
switch (result) {
case 1:
_castStatus = CastStatus.NO_DEVICES_AVAILABLE; …
break;
case 2:
_castStatus = CastStatus.NOT_CONNECTED; …
break;
…
chrome_cast_plugin.dart
private func getCastStatus(_ pCall: FlutterMethodCall, _ pResult: @escaping
FlutterResult) {
let tCastContext = GCKCastContext.sharedInstance()
let tStatus = tCastContext.castState.rawValue
pResult(tStatus)
}
iOS/Swift
リスナーの登録
● SessionManager Listener
○ アプリ全体のキャストセッションを管理
○ SessionManagerに登録する
■ アプリがセッションを開始したとき
■ アプリがセッションとの接続を停止したとき など…
● RemoteMediaClient Listener
○ アプリと現在キャストしているデバイス(Chromecast)のメディアのセッションを管理
○ CastSessionに登録する
■ 動画などをキャストをしたとき
■ キャストした動画に変更があったとき など…
● Flutter側の実装は必要なし
○ ※リスナーの中身の実装は今回は省略
class ChromeCastPlugin(private val pActivity: Activity, private val pChannel: MethodChannel):
…
CastContext.getSharedInstance()?.sessionManager?
.addSessionManagerListener(*fugafura, CastSession::class.java)
...
CastContext.getSharedInstance()?.sessionManager?.currentCastSession?
.remoteMediaClient?.registerCallback(*hogehoge)
….
Android/Kotlin
public class SwiftChromeCastPlugin: NSObject, FlutterPlugin, GCKSessionManagerListener, GCKRemoteMediaClientListener {
…
GCKCastContext.sharedInstance().sessionManager.add(*hogehoge)
….
GCKCastContext.sharedInstance().sessionManager.currentSession?.remoteMediaClient?.add(*fugafuga)
….
iOS/Swift
キャストした動画を制御する
● キャストしている動画をFlutter側から制御(一時停
止やシークなど)する
● Flutter側
○ 動画コントロールパネル用のUIを作る
● Plugin側
○ 現在のキャストセッションからリモートメディアクライアン
ト(remoteMediaClient)を取得して再生(play)や一時停止
(pause)、シーク(seek)を呼ぶ
シーク
停止
再生
ボリューム変更
Android/Kotlin
private fun setMediaStreamPosition(pCall: MethodCall, pResult: Result) {
…
tCastSession.remoteMediaClient.seek(tSeekTime.toLong())
…
}
iOS/Swift
private func setMediaStreamPosition(_ pCall: FlutterMethodCall, _ pResult:
@escaping FlutterResult) {
…
let tOptions = GCKMediaSeekOptions()
// remotMediaClient.seekに渡す引数のために、ミリ秒から秒へ変換
tOptions.interval = tTime! / 1000
tCastSession?.remoteMediaClient?.seek(with: tOptions)
}
chrome_cast_plugin.dart
Future<bool> setMediaStreamPosition(var milliseconds) async {
bool _result =
await _channel.invokeMethod('SetMediaStreamPosition',
milliseconds);
return _result;
}
例)シーク
応用 キャストボタンの表示バグをなくす
● Cast Statusの状態がNOT_CONNECTEDの状態からアプリがバックグラウンドに
いくと、キャストデバイスはない状態(NO_DEVICES_AVALLABLE)になる
● アプリがフォアグラウンドに戻った際に、キャストボタンが表示?非表示をし
て点滅する
○ Google Playムービーなどの一部アプリでも起きている
● アプリがフォアグラウンドに戻ってきた際、数ミリ秒まってからCast Stateの
状態を取得する
chrome_cast_plugin.dart
class ChromeCast extends WidgetsBindingObserver {
…
WidgetsBinding.instance.addObserver(this);
….
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.paused:
_isDelay = true;
break;
default:
break;
}
}
…
abstract WidgetsBindingObserver
アプリのライフサイクルを監視する
didChangeAppLifecycleState関数を使うために継承
didChangeAppLifecycleState
AppLifecycleState state
● resumed
● inactive
● paused
● suspending
のいずれかの状態を返す
pausedのときに遅延を起こす
WidgetsBindingObserverに登録
chrome_cast_plugin.dart
...
typedef OnChangeCastState = Function(CastStatus);
…
class ChromeCast extends WidgetsBindingObserver {
static const MethodChannel _channel =
const MethodChannel('chrome_cast_plugin');
OnChangeCastState onChangeCastState;
bool _isDelay = false;
bool _isWaiting = false;
…
ChromeCast._internal() {
…
if (call.method == "OnChangeCastState") {
CastStatus _castStatus = _getCastStateByValue(call.arguments);
if (_isDelay) {
if (_isWaiting == false) {
_delayGetCastStatus();
}
} else {
onChangeCastState(_castStatus);
}
}
});
...
}
…
chrome_cast_plugin.dart
OnChangeCastState
キャストの状態が変化したときに呼ばれるようにデリ
ゲートを用意
_delayGetCastStatus
数ミリ秒待機してからキャストの状態を取得する(自作)
…
_delayGetCastStatus() async {
_isWaiting = true;
await Future.delayed(Duration(milliseconds: _waitSeconds));
getCastStatus().then((onStatus) {
if (onStatus != CastStatus.NO_DEVICES_AVAILABLE) {
onChangeCastState(onStatus);
_isDelay = false;
}
_isWaiting = false;
});
}
…
chrome_cast_plugin.dart
数秒待機してCastStatusを取得する

More Related Content

What's hot (20)

PDF
UXデザインの上流工程の考え方とプロセス  ~リサーチからアイデア発想そしてUIデザインへ
Masaya Ando
?
PPTX
Azure Spatial Anchors V2概要 ~空間情報の共有~
Takahiro Miyaura
?
PDF
ISUCONの勝ち方 YAPC::Asia Tokyo 2015
Masahiro Nagano
?
PDF
UXデザインとコンセプト評価 ~俺様企画はだめなのよ
Masaya Ando
?
PDF
ユーザーインタビューからその後どうするの? 得られた情報を「UXデザイン」に落とし込む方法 | UXデザイン基礎セミナー 第3回
Yoshiki Hayama
?
PDF
ソーシャルゲームのためのデータベース设计
Yoshinori Matsunobu
?
PDF
ドメイン駆动设计サンプルコードの彻底解説
増田 亨
?
PDF
実践に向けたドメイン駆动设计のエッセンス
増田 亨
?
PPTX
さるでも分かりたい9诲辞蹿で作るクォータニオン姿势
ytanno
?
PDF
モデリングもしないでアジャイルとは何事だ
Iwao Harada
?
PDF
驰补丑辞辞!ニュースにおける叠贵贵パフォーマンスチューニング事例
驰补丑辞辞!デベロッパーネットワーク
?
PDF
鲍齿デザイン概论
Masaya Ando
?
PDF
130821 owasp zed attack proxyをぶん回せ
Minoru Sakai
?
PDF
「ユーザーを理解するって言うほどカンタンじゃないよね」 UXデザイン?UXリサーチをもう一度ちゃんと理解しよう!
Yoshiki Hayama
?
PPTX
GitLab から GitLab に移行したときの思い出
富士通クラウドテクノロジーズ株式会社
?
PDF
CEH(Certified Ethical Hacker:認定ホワイトハッカー)のご紹介
グローバルセキュリティエキスパート株式会社(骋厂齿)
?
PDF
[part 2]ナレッジグラフ推論チャレンジ?Tech Live!
KnowledgeGraph
?
PPTX
社会心理学者のための时系列分析入门冲小森
Masashi Komori
?
PDF
最近の鲍滨テ?サ?インフ?ロセス
Shingo Katsushima
?
PDF
SQLアンチパターン - 開発者を待ち受ける25の落とし穴 (拡大版)
Takuto Wada
?
UXデザインの上流工程の考え方とプロセス  ~リサーチからアイデア発想そしてUIデザインへ
Masaya Ando
?
Azure Spatial Anchors V2概要 ~空間情報の共有~
Takahiro Miyaura
?
ISUCONの勝ち方 YAPC::Asia Tokyo 2015
Masahiro Nagano
?
UXデザインとコンセプト評価 ~俺様企画はだめなのよ
Masaya Ando
?
ユーザーインタビューからその後どうするの? 得られた情報を「UXデザイン」に落とし込む方法 | UXデザイン基礎セミナー 第3回
Yoshiki Hayama
?
ソーシャルゲームのためのデータベース设计
Yoshinori Matsunobu
?
ドメイン駆动设计サンプルコードの彻底解説
増田 亨
?
実践に向けたドメイン駆动设计のエッセンス
増田 亨
?
さるでも分かりたい9诲辞蹿で作るクォータニオン姿势
ytanno
?
モデリングもしないでアジャイルとは何事だ
Iwao Harada
?
驰补丑辞辞!ニュースにおける叠贵贵パフォーマンスチューニング事例
驰补丑辞辞!デベロッパーネットワーク
?
鲍齿デザイン概论
Masaya Ando
?
130821 owasp zed attack proxyをぶん回せ
Minoru Sakai
?
「ユーザーを理解するって言うほどカンタンじゃないよね」 UXデザイン?UXリサーチをもう一度ちゃんと理解しよう!
Yoshiki Hayama
?
GitLab から GitLab に移行したときの思い出
富士通クラウドテクノロジーズ株式会社
?
CEH(Certified Ethical Hacker:認定ホワイトハッカー)のご紹介
グローバルセキュリティエキスパート株式会社(骋厂齿)
?
[part 2]ナレッジグラフ推論チャレンジ?Tech Live!
KnowledgeGraph
?
社会心理学者のための时系列分析入门冲小森
Masashi Komori
?
最近の鲍滨テ?サ?インフ?ロセス
Shingo Katsushima
?
SQLアンチパターン - 開発者を待ち受ける25の落とし穴 (拡大版)
Takuto Wada
?

Recently uploaded (6)

PDF
フィシ?カル础滨时代のセキュリティ:ロホ?ティクスと础滨セキュリティの融合のあり方
Osaka University
?
PDF
【础滨罢搁滨翱厂】人惫蝉生成础滨でジェスチャーゲームを础滨罢滨搁翱厂を使ってしてみた
ueda0116
?
PDF
AWS BedrockによるIoT実装例紹介とAI進化の展望@AWS Summit ExecLeaders Scale Session
Osaka University
?
PPTX
[Liberaware] Engineer Summer Internship.pptx
koyamakohei
?
PDF
音学シンポジウム2025 招待讲演 远隔会话音声认识のための音声强调フロントエント?:概要と我々の取り组み
Tsubasa Ochiai
?
PDF
React Native vs React Lynx (React Native Meetup #22)
Taiju Muto
?
フィシ?カル础滨时代のセキュリティ:ロホ?ティクスと础滨セキュリティの融合のあり方
Osaka University
?
【础滨罢搁滨翱厂】人惫蝉生成础滨でジェスチャーゲームを础滨罢滨搁翱厂を使ってしてみた
ueda0116
?
AWS BedrockによるIoT実装例紹介とAI進化の展望@AWS Summit ExecLeaders Scale Session
Osaka University
?
[Liberaware] Engineer Summer Internship.pptx
koyamakohei
?
音学シンポジウム2025 招待讲演 远隔会话音声认识のための音声强调フロントエント?:概要と我々の取り组み
Tsubasa Ochiai
?
React Native vs React Lynx (React Native Meetup #22)
Taiju Muto
?
Ad

Cast SDK for Flutter

  • 1. Cast SDK for Flutter
  • 2. 山田 幸司 koji.yamada@gree.net グリー株式会社 開発本部 / インフラストラクチャ部 / ディベロップメント オペレーションズグループ / サービスディベロップメント チーム 所属 業務内容 VRアプリフロントエンド担当 最近のお仕事はFlutter / Unity / Unreal Engine 4など
  • 3. アジェンダ ● Chromecastについて ○ Chromecastとは? ○ Cast SDKについて ● Cast SDK for Flutterを作る ○ Flutterでプラグインを作る準備やインストール方法など ○ 構成から実装のおおまかな流れ ○ 実装方法などについて
  • 4. Chromecast ● Googleが開発?販売する小型のデバイス ● HDMI端子に接続/Wi-Fiを介してスマートフォンやタブレットなどで再生して いる動画、写真、ウェブサイトなどをディスプレイに表示出来るデバイス ● ミラーリングとはやや違ってスマートフォンをコントローラとして扱い動画 の再生や停止などもできる ● 値段は5,000円程度(4K対応のUltraは10,000円程度) ● アプリで利用するにはCast SDKを組み込む必要がある ※↑アプリが対応しているかつ周辺にChromecast がある場合はこのアイコンが表示される
  • 5. Cast SDK ● Googleが提供するChromecast用のSDK ○ https://developers.google.com/cast/ ● Android/iOS/Chrome(ブラウザ)に対応 ○ Android → Java/Kotlin ○ iOS → Objective-C/Swift ○ Chrome → Javascript ● 公式のFlutter用Cast SDKはまだ存在しない ● Flutter版もいつか対応するかも ○ GitHubのissuesはあがっている ○ https://github.com/flutter/flutter/issues/18212
  • 6. Cast SDK for Flutter ● 今日の話 ○ Flutter用のChromecast Pluginを作る話 ○ ChromecastのAndroid/iOS両方に対応するアプリをさく っと作れる ● 実装するもの ○ 送信側(Sender)と受信側(Receiver)を作る必要があ りますが、今回はSenderアプリのみについて ○ 動画コントロールパネルやキャストボタンなどのUI部分 はFlutterで実装 ○ 動画をChromecastにキャスト/再生と停止ができる
  • 7. 準備 ● Plugin Packageプロジェクトの作成 ○ $ flutter create --template=plugin -i swift -a kotlin [プロジェクト名] 例) chrome_cast_plugin Android/build.gradle … dependencies { implementation 'com.google.android.gms:play-services-cast-framework:16.1.2' ... iOS/Podfile … target 'Runner' do pod 'google-cast-sdk', '~> 4.3' ... Sync Now pod install
  • 8. 構成 app main.dart lib chrome_cast_ plugin.dart Flutter/Dart Android/Kotlin iOS/Swift CastOptionsProvider.kt ChromeCastPlugin.kt SwiftChromeCast Plugin.swift Cast SDK for Android gms:play-services-cast Cast SDK for iOS google-cast-sdk ● main.dart ○ キャストボタンなどのUI表示 ● chrome_cast_plugin.dart ○ Kotlin/Swiftとのつなぎ プラグイン側
  • 9. Pluginで実装すること 1. Cast Contextの初期化 2. Cast Dialogの表示 3. Cast Buttonの表示 4. リスナーの登録 5. 動画をキャストする 6. キャストした動画を制御する
  • 10. Cast Contextの初期化 ● Chromecast の機能を使うために必要 ● Android ○ OptionsProviderインタフェースを実装したクラスの作成 ○ レシーバーIdのCastOptionsのインスタンスを作る ○ CastContext.getSharedInstance(Activity)で初期化 ● iOS ○ レシーバーIdのGCKCastOptionのインスタンスを作る ○ GCKCastContext.setSharedInstanceWith(GCKCastOption)で初期化 ● 初期化タイミング ○ pluginのregisterWith / register
  • 11. class ChromeCastPlugin(private val pActivity: Activity, private val pChannel: MethodChannel): … companion object { @JvmStatic fun registerWith(registrar: Registrar) { CastContext.getSharedInstance(pActivity) … Android/Kotlin static const MethodChannel _channel = const MethodChannel('chrome_cast_plugin'); chrome_cast_plugin.dart public class SwiftChromeCastPlugin: NSObject, FlutterPlugin { … public static func register(with registrar: FlutterPluginRegistrar) { let tCriteria = GCKDiscoveryCriteria(applicationID: [レシーバーのId]) let tOption = GCKCastOptions(discoveryCriteria: tCriteria) GCKCastContext.setSharedInstanceWith(tOption) … iOS/Swift
  • 12. class CastOptionsProvider : OptionsProvider { … override fun getCastOptions(context: Context): CastOptions? { val tAppId = context.resources.getIdentifier("chromecast_app_id", "string", context.packageName) … return CastOptions.Builder() .setReceiverApplicationId(context.getString(tAppId)) .build() } … } Android/Kotlin Cast Contextの初期化(Android CastOptionsProvider.kt) <?xml version="1.0" encoding="utf-8"?> <resources> <string name="chromecast_app_id">[レシーバーId]</string> </resources> app/res/values/string.xml
  • 13. Cast Dialogの表示 ● キャストしてないとき ○ キャスト可能なデバイス一覧を表示 ● キャストしてるとき ○ メディア情報、音量などを表示 ● Android(少々手間がかかる) ○ 現在キャスト中かどうかを判定して出し分けが必要 ○ 現在のキャストセッションがあれば MediaRouteControllerDialog ○ 無い場合はMediaRouteChooserDialogを呼ぶ ● iOS ○ presentCastDialogを呼ぶだけ
  • 14. private fun showDialog(pCall: MethodCall, pResult: Result) { val tCastSession = CastContext.getSharedInstance()?.sessionManager?.currentCastSession if (tCastSession != null) { val tControllerDialog = MediaRouteControllerDialog(pActivity, R.style.CastControllerDialogTheme) tControllerDialog.show() } else { val tChooserDialog = MediaRouteChooserDialog(pActivity, R.style.CastMediaRouterTheme) tChooserDialog.routeSelector = CastContext.getSharedInstance()?.mergedSelector!! tChooserDialog.show() } } Android/Kotlin Future<void> showCastDialog() async { await _channel.invokeMethod('ShowCastDialog'); } chrome_cast_plugin.dart private func showDialog(_ pCall: FlutterMethodCall, _ pResult: @escaping FlutterResult) { let tContext:GCKCastContext = GCKCastContext.sharedInstance() tContext.presentCastDialog() } iOS/Swift
  • 15. 動画をキャストする ● 動画をキャストするために必要な情報をFlutterからPlugin側に渡す ● 手順 ○ [1] キャストするための必要なメタデータ(Metadata)を作成するためのパ ラメータ ■ タイトル、キャストダイアログに出す画像など ○ [2] 作成したメタデータからメディア情報(MediaInfo)を作成するためのパ ラメータ ■ 動画のコンテンツタイプ(mp4など)、レシーバーに渡す独自のJsonデータなど ○ [3] 必要に応じて読み込み時のオプションを作成するためのパラメータ ■ 再生位置、自動再生するかどうかなど ○ [4] 動画をデバイスにキャスト
  • 16. Future<bool> startCast(Media media) async { bool _result = await _channel.invokeMethod('StartCast', { 'Uri': media.url, 'Title': media.title, 'Subtitle': media.subTitle, 'Studio': media.studio, 'ContentType': media.contentType, 'Images': { 'Dialog': { 'Url': media.dialogThumbnail.url, 'Width': media.dialogThumbnail.width, 'Height': media.dialogThumbnail.height }, 'Notification': { 'Url': media.notificationThumbnail.url, } }, chrome_cast_plugin.dart 'IsAuto': media.isAuto, 'PlayPosition': media.position, 'IsLive': media.isLive, 'CustomData': media.customData }); return _result; } ... class Media { Media({ this.url, this.title, this.subTitle, this.studio, this.contentType:, this.customData: const {}, this.isAuto, this.position, this.isLive, this.dialogThumbnail, this.notificationThumbnail, }) : ...
  • 17. private fun startCast(pCall: MethodCall, pResult: Result) { val tArguments = pCall.arguments as? Map<String, Any> … // [1] メタデータの設定 val tMovieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE) tMovieMetadata.putString(MediaMetadata.KEY_TITLE, tTitle) tMovieMetadata.addImage(WebImage(Uri.parse(tDialogUrl), tWidth, tHeight)) tMovieMetadata.addImage(WebImage(Uri.parse(tNotificationUrl))) … // [2] メディア情報の作成 val tMediaInfo = MediaInfo.Builder(tUri) .setStreamType(tStreamType) .setContentType(tContentType) .setMetadata(tMovieMetadata) .setCustomData(tJsonObject) .build() … Android/Kotlin // [3] 読み込み時のオプション val tMediaLoadOptions: MediaLoadOptions = MediaLoadOptions.Builder() .setAutoplay(tIsAuto) .setPlayPosition(tPlayPosition.toLong()) .build() … // [4] Chromecastにキャスト val tIsSuccess = (tCastSession?.remoteMediaClient?.load(tMediaInfo, tMediaLoadOptions) != null) pResult.success(tIsSuccess) }
  • 18. private func startCast(_ pCall: FlutterMethodCall, _ pResult: @escaping FlutterResult) { let tArguments = pCall.arguments as? Dictionary<String, Any> … // [1] メタデータの設定 let tMetadata:GCKMediaMetadata = GCKMediaMetadata(metadataType: GCKMediaMetadataType.movie) tMetadata.setString(tTitle, forKey: kGCKMetadataKeyTitle) tMetadata.addImage(GCKImage(url: URL(/string: (tDialogUrl)), width: tWidth, height: tHeight)) … // [2] メディア情報の作成 let tBuilder = GCKMediaInformationBuilder.init(contentURL: tParseUrl) tBuilder.contentID = tUri tBuilder.streamType = tStreamType tBuilder.contentType = tContentType tBuilder.metadata = tMetadata tBuilder.customData = tJsonData … // [3] 読み込み時のオプション let tMediaLoadOption = GCKMediaLoadOptions(); tMediaLoadOption.autoplay = tIsAuto tMediaLoadOption.playPosition = tPlayPosition … // [4] Chromecastにキャスト var tIsSuccess = true; if (tCastSession?.remoteMediaClient? .loadMedia(tMediaInfo, with: tMediaLoadOption)) != nil { tIsSuccess = true; } else { tIsSuccess = false } pResult(tIsSuccess) } iOS/Swift
  • 20. ● 仕様 ○ Android / iOSで動作するFlutterプラグイン及びアプリ ■ Flutter : 1.2.1 ■ Android : Kotlin 1.3.11 / gms-play-service : 16.1.2 ■ iOS : Swift 4.2.1 / google-cast-sdk : 4.3.5 ● 開発エディタ ○ VSCode ○ Android Studio ○ Xcode ● Google Cast Document ○ https://developers.google.com/cast/docs/developers ● Google Cast GitHub(Android/iOS) ○ https://github.com/googlecast/CastVideos-ios ○ https://github.com/googlecast/CastVideos-android ここから先のスライドは補足
  • 21. 補足 iOS/Xcodeで開発する場合の注意 ● Xcode10以上、iOS 12以降をターゲットにしている場合 ● Access Wifi InformationをONにする
  • 22. Cast Buttonの表示 ● キャスト可能なデバイスがあるときはアイコンを表示、接続中は接続中のア イコン、キャスト可能なデバイスがない場合は非表示に切り替える ● Cast StatusをAndroid/iOSのプラグイン側から取得 ● Cast Statusの状態をもとにFlutter側でアイコンの表示?非表示を切り替える キャストしてない キャスト中
  • 23. private fun getCastState(pCall: MethodCall, pResult: Result) { val tCastContext = CastContext.getSharedInstance() val Status = tCastContext.castState pResult.success(tStatus) } Android/Kotlin enum CastStatus { NO_DEVICES_AVAILABLE, NOT_CONNECTED, CONNECTING, CONNECTED, } … Future<CastStatus> getCastStatus() async { CastStatus _castStatus; int _result = await _channel.invokeMethod('GetCastStatus'); _castStatus = _getCastStateByValue(_result); return _castStatus; } … CastStatus _getCastStateByValue(int result) { switch (result) { case 1: _castStatus = CastStatus.NO_DEVICES_AVAILABLE; … break; case 2: _castStatus = CastStatus.NOT_CONNECTED; … break; … chrome_cast_plugin.dart private func getCastStatus(_ pCall: FlutterMethodCall, _ pResult: @escaping FlutterResult) { let tCastContext = GCKCastContext.sharedInstance() let tStatus = tCastContext.castState.rawValue pResult(tStatus) } iOS/Swift
  • 24. リスナーの登録 ● SessionManager Listener ○ アプリ全体のキャストセッションを管理 ○ SessionManagerに登録する ■ アプリがセッションを開始したとき ■ アプリがセッションとの接続を停止したとき など… ● RemoteMediaClient Listener ○ アプリと現在キャストしているデバイス(Chromecast)のメディアのセッションを管理 ○ CastSessionに登録する ■ 動画などをキャストをしたとき ■ キャストした動画に変更があったとき など… ● Flutter側の実装は必要なし ○ ※リスナーの中身の実装は今回は省略
  • 25. class ChromeCastPlugin(private val pActivity: Activity, private val pChannel: MethodChannel): … CastContext.getSharedInstance()?.sessionManager? .addSessionManagerListener(*fugafura, CastSession::class.java) ... CastContext.getSharedInstance()?.sessionManager?.currentCastSession? .remoteMediaClient?.registerCallback(*hogehoge) …. Android/Kotlin public class SwiftChromeCastPlugin: NSObject, FlutterPlugin, GCKSessionManagerListener, GCKRemoteMediaClientListener { … GCKCastContext.sharedInstance().sessionManager.add(*hogehoge) …. GCKCastContext.sharedInstance().sessionManager.currentSession?.remoteMediaClient?.add(*fugafuga) …. iOS/Swift
  • 26. キャストした動画を制御する ● キャストしている動画をFlutter側から制御(一時停 止やシークなど)する ● Flutter側 ○ 動画コントロールパネル用のUIを作る ● Plugin側 ○ 現在のキャストセッションからリモートメディアクライアン ト(remoteMediaClient)を取得して再生(play)や一時停止 (pause)、シーク(seek)を呼ぶ シーク 停止 再生 ボリューム変更
  • 27. Android/Kotlin private fun setMediaStreamPosition(pCall: MethodCall, pResult: Result) { … tCastSession.remoteMediaClient.seek(tSeekTime.toLong()) … } iOS/Swift private func setMediaStreamPosition(_ pCall: FlutterMethodCall, _ pResult: @escaping FlutterResult) { … let tOptions = GCKMediaSeekOptions() // remotMediaClient.seekに渡す引数のために、ミリ秒から秒へ変換 tOptions.interval = tTime! / 1000 tCastSession?.remoteMediaClient?.seek(with: tOptions) } chrome_cast_plugin.dart Future<bool> setMediaStreamPosition(var milliseconds) async { bool _result = await _channel.invokeMethod('SetMediaStreamPosition', milliseconds); return _result; } 例)シーク
  • 28. 応用 キャストボタンの表示バグをなくす ● Cast Statusの状態がNOT_CONNECTEDの状態からアプリがバックグラウンドに いくと、キャストデバイスはない状態(NO_DEVICES_AVALLABLE)になる ● アプリがフォアグラウンドに戻った際に、キャストボタンが表示?非表示をし て点滅する ○ Google Playムービーなどの一部アプリでも起きている ● アプリがフォアグラウンドに戻ってきた際、数ミリ秒まってからCast Stateの 状態を取得する
  • 29. chrome_cast_plugin.dart class ChromeCast extends WidgetsBindingObserver { … WidgetsBinding.instance.addObserver(this); …. @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); switch (state) { case AppLifecycleState.paused: _isDelay = true; break; default: break; } } … abstract WidgetsBindingObserver アプリのライフサイクルを監視する didChangeAppLifecycleState関数を使うために継承 didChangeAppLifecycleState AppLifecycleState state ● resumed ● inactive ● paused ● suspending のいずれかの状態を返す pausedのときに遅延を起こす WidgetsBindingObserverに登録
  • 30. chrome_cast_plugin.dart ... typedef OnChangeCastState = Function(CastStatus); … class ChromeCast extends WidgetsBindingObserver { static const MethodChannel _channel = const MethodChannel('chrome_cast_plugin'); OnChangeCastState onChangeCastState; bool _isDelay = false; bool _isWaiting = false; … ChromeCast._internal() { … if (call.method == "OnChangeCastState") { CastStatus _castStatus = _getCastStateByValue(call.arguments); if (_isDelay) { if (_isWaiting == false) { _delayGetCastStatus(); } } else { onChangeCastState(_castStatus); } } }); ... } … chrome_cast_plugin.dart OnChangeCastState キャストの状態が変化したときに呼ばれるようにデリ ゲートを用意 _delayGetCastStatus 数ミリ秒待機してからキャストの状態を取得する(自作)
  • 31. … _delayGetCastStatus() async { _isWaiting = true; await Future.delayed(Duration(milliseconds: _waitSeconds)); getCastStatus().then((onStatus) { if (onStatus != CastStatus.NO_DEVICES_AVAILABLE) { onChangeCastState(onStatus); _isDelay = false; } _isWaiting = false; }); } … chrome_cast_plugin.dart 数秒待機してCastStatusを取得する