è·¯äžã§ã¯ãã¹ããŒããã©ã³ãããªãŒãã£ãªããã¯ãããããã£ã¹ããããèããŸãã 家ã«åž°ã£ãããAndroid TVãŸãã¯Google Homeã§åŒãç¶ãèãããã§ãã ãã ãããã¹ãŠã®ã¢ããªã±ãŒã·ã§ã³ãChromecastããµããŒãããŠããããã§ã¯ãããŸããã ãããŠãããã¯äŸ¿å©ã§ãããã
éå»3幎éã®Googleã®çµ±èšã«ãããš ãAndroid TVã®ããã€ã¹æ°ã¯4åã«å¢å ãã補é ããŒãããŒã®æ°ã¯ãã§ã«ãã¹ããŒãããã¬ããã¹ããŒã«ãŒãã»ãããããããã¯ã¹ã®100ãè¶
ããŠããŸãã ãããã¯ãã¹ãŠChromecastããµããŒãããŠããŸãã ãããããŸã å€ãã®ã¢ããªã±ãŒã·ã§ã³ãåžå Žã«ãããæããã«ãããšã®çµ±åãæ¬ ããŠããŸãã
ãã®èšäºã§ã¯ãã¡ãã£ã¢ã³ã³ãã³ããåçããããã«ChromecastãAndroidã¢ããªã±ãŒã·ã§ã³ã«çµ±åããçµéšãå
±æããããšæããŸãã
ä»çµã¿
ãChromecastããšããèšèãèããã®ãåããŠã®å Žåã¯ãç°¡åã«èª¬æããŸãã 䜿çšã«é¢ããŠã¯ã次ã®ããã«ãªããŸãã
- ãŠãŒã¶ãŒã¯ãã¢ããªã±ãŒã·ã§ã³ãŸãã¯Webãµã€ããéããŠé³æ¥œãèŽãããããããªãèŠããããŸãã
- Chromecastããã€ã¹ãããŒã«ã«ãããã¯ãŒã¯ã«è¡šç€ºãããŸãã
- 察å¿ãããã¿ã³ããã¬ãŒã€ãŒã®ã€ã³ã¿ãŒãã§ãŒã¹ã«è¡šç€ºãããŸãã
- ãŠãŒã¶ãŒã¯ãããã¯ãªãã¯ããŠããªã¹ãããç®çã®ããã€ã¹ãéžæããŸãã Nexus PlayerãAndroid TVããŸãã¯ã¹ããŒãã¹ããŒã«ãŒã䜿çšã§ããŸãã
- ãã®ããã€ã¹ã§ããã«åçãç¶è¡ãããŸãã
æè¡çã«ã¯ã次ã®ãããªããšãèµ·ãããŸãã
- GoogleãµãŒãã¹ã¯ããããŒããã£ã¹ããä»ããŠããŒã«ã«ãããã¯ãŒã¯äžã®Chromecastããã€ã¹ã®ååšãç£èŠããŸãã
- MediaRouterãã¢ããªã±ãŒã·ã§ã³ã«æ¥ç¶ãããŠããå Žåãããã«é¢ããã€ãã³ããåãåããŸãã
- ãŠãŒã¶ãŒããã£ã¹ãããã€ã¹ãéžæããŠæ¥ç¶ãããšãæ°ããã¡ãã£ã¢ã»ãã·ã§ã³ïŒCastSessionïŒãéããŸãã
- äœæãããã»ãã·ã§ã³ã§æ¢ã«ãåçã®ããã«ã³ã³ãã³ãã転éããŸãã
ãšãŠãç°¡åã«èãããŸãã
çµ±å
Googleã«ã¯Chromecastãæäœããããã®ç¬èªã®SDKããããŸãããããã¥ã¡ã³ãã§ååã«ã«ããŒãããŠãããããã®ã³ãŒãã¯é£èªåãããŠããŸãã ãããã£ãŠãå€ãã®ããšãå
¥åããŠç¢ºèªããå¿
èŠããããŸããã ãã¹ãŠãé çªã«ååŸããŸãããã
åæå
ãŸããCast Application FrameworkãšMediaRouterãæ¥ç¶ããå¿
èŠããããŸãã
implementation "com.google.android.gms:play-services-cast-framework:16.1.0" implementation "androidx.mediarouter:mediarouter:1.0.0"
次ã«ãCast Frameworkã¯ã¢ããªã±ãŒã·ã§ã³èå¥åïŒè©³çŽ°ã¯åŸã»ã©èª¬æããŸãïŒããã³ãµããŒããããŠããã¡ãã£ã¢ã³ã³ãã³ãã®çš®é¡ãååŸããå¿
èŠããããŸãã ã€ãŸããã¢ããªã±ãŒã·ã§ã³ããããªã®ã¿ãåçããå ŽåãGoogle Homeåãžã®ãã£ã¹ãã¯äžå¯èœã«ãªããããã€ã¹ã®ãªã¹ãã«ã¯å«ãŸããªããªããŸãã ãããè¡ãã«ã¯ãOptionsProviderã®å®è£
ãäœæããŸãã
class CastOptionsProvider: OptionsProvider { override fun getCastOptions(context: Context): CastOptions { return CastOptions.Builder() .setReceiverApplicationId(BuildConfig.CHROMECAST_APP_ID) .build() } override fun getAdditionalSessionProviders(context: Context): MutableList<SessionProvider>? { return null } }
ãããã§ã¹ãã§å®£èšããŸãã
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:value="your.app.package.CastOptionsProvider" />
ã¢ããªã±ãŒã·ã§ã³ãç»é²ãã
Chromecastãã¢ããªã±ãŒã·ã§ã³ã§äœ¿çšããã«ã¯ã Google Cast SDK Developers Consoleã«ç»é²ããå¿
èŠããããŸãã ããã«ã¯ãChromecastéçºè
ã¢ã«ãŠã³ããå¿
èŠã§ãïŒGoogle Playéçºè
ã¢ã«ãŠã³ããšæ··åããªãã§ãã ããïŒã ç»é²æã«ã5ãã«ã®1åéãã®æéãæ¯æãå¿
èŠããããŸãã ChromeCastã¢ããªã±ãŒã·ã§ã³ãå
¬éããåŸãå°ãåŸ
ã€å¿
èŠããããŸãã
ã³ã³ãœãŒã«ã§ã¯ãç»é¢ã®ããããã€ã¹ã®ãã£ã¹ããã¬ãŒã€ãŒã®å€èŠ³ãå€æŽããã¢ããªã±ãŒã·ã§ã³å
ã®ãã£ã¹ãåæã確èªã§ããŸãã
MediaRouteFrameworkã¯ããŠãŒã¶ãŒã®è¿ãã«ãããã¹ãŠã®ãªã¢ãŒãåçããã€ã¹ãèŠã€ããããšãã§ããã¡ã«ããºã ã§ãã ããã«ã¯ãChromecastã ãã§ãªãããµãŒãããŒãã£ã®ãããã³ã«ã䜿çšãããªã¢ãŒããã£ã¹ãã¬ã€ãã¹ããŒã«ãŒã䜿çšã§ããŸãã ããããç§ãã¡ãèå³ãæã£ãŠããã®ã¯Chromecastã§ãã
MediaRouteFrameworkã«ã¯ãã¡ãã£ã¢ã¹ã¯ãŒã¿ãŒã®ç¶æ
ãåæ ãããã¥ãŒããããŸãã æ¥ç¶ããã«ã¯2ã€ã®æ¹æ³ããããŸãã
1ïŒã¡ãã¥ãŒããïŒ
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> ... <item android:id="@+id/menu_media_route" android:title="@string/cast" app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider" app:showAsAction="always"/> ... </menu>
2ïŒã¬ã€ã¢ãŠãçµç±ïŒ
<androidx.mediarouter.app.MediaRouteButton android:id="@+id/mediaRouteButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:mediaRouteTypes="user"/>
ã³ãŒãããããã¿ã³ãCastButtonFactoryã«ç»é²ããã ãã§ãã 次ã«ãã¡ãã£ã¢ã¹ã¯ãŒã¿ãŒã®çŸåšã®ç¶æ
ãã¹ããŒãããŸãã
CastButtonFactory.setUpMediaRouteButton(applicationContext, view.mediaRouteButton)
ããã§ã¢ããªã±ãŒã·ã§ã³ãç»é²ãããMediaRouterãæ§æãããã®ã§ãChromeCastããã€ã¹ã«æ¥ç¶ããŠãããã®ã»ãã·ã§ã³ãéãããšãã§ããŸãã
ChromeCastã¯3ã€ã®äž»èŠãªã³ã³ãã³ãã¿ã€ãããµããŒãããŠããŸãã
ã¡ãã£ã¢ã³ã³ãã³ãããã£ã¹ãããã€ã¹ãªã©ã®ãã¬ãŒã€ãŒã®èšå®ã«ãã£ãŠããã¬ãŒã€ãŒã®ã€ã³ã¿ãŒãã§ãŒã¹ã¯ç°ãªãå ŽåããããŸãã
ãã£ã¹ãã»ãã·ã§ã³
ãã®ããããŠãŒã¶ãŒãç®çã®ããã€ã¹ãéžæãããšãCastFrameworkã¯æ°ããã»ãã·ã§ã³ãéããŸããã ããã§ã®ã¿ã¹ã¯ã¯ãããã«å¿çããåçã®ããã«ããã€ã¹æ
å ±ãæž¡ãããšã§ãã
ã»ãã·ã§ã³ã®çŸåšã®ç¶æ
ã確èªãããã®ç¶æ
ãæŽæ°ããããã«ãµã€ã³ã¢ããããã«ã¯ã SessionManagerãªããžã§ã¯ãã䜿çšããŸãã
private val mediaSessionListener = object : SessionManagerListener<CastSession> { override fun onSessionStarted(session: CastSession, sessionId: String) { currentSession = session
ãŸããçŸåšéããŠããã»ãã·ã§ã³ããããã©ããã確èªã§ããŸãã
val currentSession: CastSession? = sessionManager.currentCastSession
ãã£ã¹ããéå§ã§ããäž»ãªæ¡ä»¶ã¯2ã€ãããŸãã
- ã»ãã·ã§ã³ã¯ãã§ã«éããŠããŸãã
- ãã£ã¹ãçšã®ã³ã³ãã³ãããããŸãã
ãããã®2ã€ã®ã€ãã³ãã®ããããã§ãã¹ããŒã¿ã¹ã確èªãããã¹ãŠãæ£åžžã§ããå Žåããã£ã¹ããéå§ã§ããŸãã
ãã£ã¹ãã£ã³ã°
ãã£ã¹ããããã®ãšãã£ã¹ãããå Žæãã§ããã®ã§ã次ã«æãéèŠãªããšã«ç§»ããŸãã ç¹ã«ãCastSessionã«ã¯ãã¡ãã£ã¢ã³ã³ãã³ãã®åçç¶æ
ãæ
åœããRemoteMediaClientãªããžã§ã¯ãããããŸãã 圌ãšäžç·ã«åããŸãã
äœæè
ãã¢ã«ãã ãªã©ã«é¢ããæ
å ±ãä¿åãããMediaMetadataãäœæããŸããããããã¯ãããŒã«ã«åçãéå§ãããšãã«MediaSessionã«è»¢éãããã®ãšéåžžã«äŒŒãŠããŸãã
val mediaMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK ).apply { putString(MediaMetadata.KEY_TITLE, âIn Câ) putString(MediaMetadata.KEY_ARTIST, âTerry Rileyâ) mediaContent?.metadata?.posterUrl?.let { poster -> addImage(WebImage(Uri.parse(âhttps:
MediaMetadataã«ã¯å€ãã®ãã©ã¡ãŒã¿ãŒããããããã¥ã¡ã³ãã§ç¢ºèªããããšããå§ãããŸãã ããããããã§ã¯ãªããåã«WebImageå
ã®ãªã³ã¯ã«ãã£ãŠç»åãè¿œå ã§ããããšã«é©ããŸããã
MediaInfoãªããžã§ã¯ãã¯ãã³ã³ãã³ãã¡ã¿ããŒã¿ã«é¢ããæ
å ±ãä¿æã ãã¡ãã£ã¢ã³ã³ãã³ãã®çºä¿¡å
ãçš®é¡ãåçæ¹æ³ã«ã€ããŠèª¬æããŸãã
val mediaInfo = MediaInfo.Builder(âhttps:
contentTypeã¯ã MIMEä»æ§ã«ããã³ã³ãã³ãã®ã¿ã€ãã§ããããšãæãåºããŠãã ããã
MediaInfoã§ã¯ãåºåæ¿å
¥ç©ã転éããããšãã§ããŸãã
- setAdBreakClips-ã³ã³ãã³ããã¿ã€ãã«ãã¿ã€ãã³ã°ãåºåãã¹ããããããæéãžã®ãªã³ã¯ãå«ãAdBreakClipInfoã³ããŒã·ã£ã«ã®ãªã¹ããåãå
¥ããŸãã
- setAdBreaks-åºåæ¿å
¥ã®ã¬ã€ã¢ãŠããšã¿ã€ãã³ã°ã«é¢ããæ
å ±ã
MediaLoadOptionsã§ã¯ãã¡ãã£ã¢ã¹ããªãŒã ã®åŠçæ¹æ³ïŒé床ãéå§äœçœ®ïŒã«ã€ããŠèª¬æããŸãã ããã¥ã¡ã³ãã§ã¯ãsetCredentialsãä»ããŠèªèšŒããããŒãæž¡ãããšãã§ããŸãããChromecastããã®èŠæ±ã«ã¯èŠæ±ãããèªèšŒãã£ãŒã«ããå«ãŸããŠããªããšèšèŒãããŠããŸãã
val mediaLoadOptions = MediaLoadOptions.Builder() .setPlayPosition(position!!) .setAutoplay(true) .setPlaybackRate(playbackSpeed) .setCredentials(context.getString(R.string.bearer_token, authGateway.authState.accessToken!!)) .setCredentialsType(context.getString(R.string.authorization_header_key)) .build()
ãã¹ãŠã®æºåãæŽã£ããããã¹ãŠã®ããŒã¿ãRemoteMediaClientã«æž¡ãããšãã§ããChromecastã¯åçãéå§ããŸãã ããŒã«ã«åçãäžæåæ¢ããããšãéèŠã§ãã
val remoteMediaClient = currentSession!!.remoteMediaClient remoteMediaClient.load(mediaInfo, mediaLoadOptions)
ã€ãã³ãåŠç
ãããªãåçããå§ãããããŠäœãïŒ ãŠãŒã¶ãŒããã¬ããäžæåæ¢ãããšã©ããªããŸããïŒ Chromecastã®åŽããã€ãã³ãã«ã€ããŠåŠã¶ããã«ãRemoteMediaClientã«ã¯ã³ãŒã«ããã¯ããããŸãã
private val castStatusCallback = object : RemoteMediaClient.Callback() { override fun onStatusUpdated() {
çŸåšã®é²è¡ç¶æ³ãç¥ãããšãç°¡åã§ãã
val periodMills = 1000L remoteMediaClient.addProgressListener( RemoteMediaClient.ProgressListener { progressMills, durationMills ->
æ¢åã®ãã¬ãŒã€ãŒãšã®çµ±åçµéš
ç§ãåãçµãã§ããã¢ããªã±ãŒã·ã§ã³ã«ã¯ããã§ã«æ¢è£œã®ã¡ãã£ã¢ãã¬ãŒã€ãŒããããŸããã ç®æšã¯ãChromecastãµããŒããçµ±åããããšã§ããã ã¡ãã£ã¢ãã¬ãŒã€ãŒã¯ã¹ããŒããã·ã³ã«åºã¥ããŠãããæåã«èããã®ã¯æ°ããç¶æ
ãCastingStateããè¿œå ããããšã§ããã ãã ããåãã¬ãŒã€ãŒã®ç¶æ
ãåçç¶æ
ãåæ ããŠããããããã®ã¢ã€ãã¢ã¯ããã«æåŠãããŸãããExoPlayerãŸãã¯ChromeCastã®å®è£
ãšããŠæ©èœãããã©ããã¯é¢ä¿ãããŸããã
ãã®åŸããã¬ãŒã€ãŒã®ãã©ã€ããµã€ã¯ã«ãã®åªå
é äœä»ããšåŠçãè¡ãç¹å®ã®ããªã²ãŒãã·ã¹ãã ãäœæãããšããã¢ã€ãã¢ãçãŸããŸããã ãã¹ãŠã®ããªã²ãŒãã¯ããã¬ãŒã€ãŒã¹ããŒã¿ã¹ã€ãã³ããåä¿¡ã§ããŸãïŒãã¬ã€ãäžæåæ¢ãªã©ã -ãã ãã䞻任代ç人ã®ã¿ãã¡ãã£ã¢ã³ã³ãã³ããåçããŸãã
ãã®ãã¬ãŒã€ãŒã€ã³ã¿ãŒãã§ã€ã¹ã®ãããªãã®ããããŸãã
interface Player { val isPlaying: Boolean val isReleased: Boolean val duration: Long var positionInMillis: Long var speed: Float var volume: Float var loop: Boolean fun addListener(listener: PlayerCallback) fun removeListener(listener: PlayerCallback): Boolean fun getListeners(): MutableSet<PlayerCallback> fun prepare(mediaContent: MediaContent) fun play() fun pause() fun release() interface PlayerCallback { fun onPlaying(currentPosition: Long) fun onPaused(currentPosition: Long) fun onPreparing() fun onPrepared() fun onLoadingChanged(isLoading: Boolean) fun onDurationChanged(duration: Long) fun onSetSpeed(speed: Float) fun onSeekTo(fromTimeInMillis: Long, toTimeInMillis: Long) fun onWaitingForNetwork() fun onError(error: String?) fun onReleased() fun onPlayerProgress(currentPosition: Long) } }
å
éšã«ã¯éåžžã«å€ãã®ç¶æ
ãæã€ç¶æ
ãã·ã³ããããŸãïŒ
- 空-åæååã®åæç¶æ
ã
- æºåäž-ãã¬ãŒã€ãŒã¯ã¡ãã£ã¢ã³ã³ãã³ãã®åçãéå§ããŸãã
- æºåå®äº-ã¡ãã£ã¢ãã¢ããããŒããããåçããæºåãã§ããŸããã
- éã¶
- äžæåæ¢
- ãããã¯ãŒã¯ãåŸ
ã£ãŠããŸã
- ãšã©ãŒ
以åã¯ãåæåäžã®åç¶æ
ãExoPlayerã§ã³ãã³ããçºè¡ããŠããŸããã ããã§ãPlayingããªã²ãŒãã®ãªã¹ãã«ã³ãã³ããçºè¡ããããLeadãããªã²ãŒãããããåŠçã§ããããã«ãªããŸãã ããªã²ãŒãã¯ãã¬ãŒã€ãŒã®ãã¹ãŠã®æ©èœãå®è£
ããããããã¬ãŒã€ãŒã®ã€ã³ã¿ãŒãã§ãŒã¹ããç¶æ¿ããå¿
èŠã«å¿ããŠåå¥ã«äœ¿çšããããšãã§ããŸãã 次ã«ãæœè±¡ããªã²ãŒãã¯æ¬¡ã®ããã«ãªããŸãã
abstract class PlayingDelegate( protected val playerCallback: Player.PlayerCallback, var isLeading: Boolean = false ) : Player { fun setIsLeading(isLeading: Boolean, positionMills: Long, isPlaying: Boolean) { this.isLeading = isLeading if (isLeading) { onLeading(positionMills, isPlaying) } else { onDormant() } } final override fun addListener(listener: Player.PlayerCallback) {
ããšãã°ãã€ã³ã¿ãŒãã§ã€ã¹ãç°¡çŽ åããŸããã å®éã«ã¯ãããå°ãã€ãã³ãããããŸãã
è€è£œãœãŒã¹ãšåæ°ã®ããªã²ãŒããååšããå¯èœæ§ããããŸãã Chromecastããªã²ãŒãã¯æ¬¡ã®ããã«ãªããŸãã
ChromeCastDelegate.kt class ChromeCastDelegate( private val context: Context, private val castCallback: ChromeCastListener, playerCallback: Player.PlayerCallback ) : PlayingDelegate(playerCallback) { companion object { private const val CONTENT_TYPE_VIDEO = "videos/mp4" private const val CONTENT_TYPE_AUDIO = "audio/mp3" private const val PROGRESS_DELAY_MILLS = 500L } interface ChromeCastListener { fun onCastStarted() fun onCastStopped() } private var sessionManager: SessionManager? = null private var currentSession: CastSession? = null private var mediaContent: MediaContent? = null private var currentPosition: Long = 0 private val mediaSessionListener = object : SessionManagerListener<CastSession> { override fun onSessionStarted(session: CastSession, sessionId: String) { currentSession = session castCallback.onCastStarted() } override fun onSessionEnding(session: CastSession) { currentPosition = session.remoteMediaClient?.approximateStreamPosition ?: currentPosition stopCasting() } override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) { currentSession = session castCallback.onCastStarted() } override fun onSessionStartFailed(session: CastSession, p1: Int) { stopCasting() } override fun onSessionEnded(session: CastSession, p1: Int) {
åçã«é¢ããã³ãã³ããäžããåã«ãäž»èŠãªããªã²ãŒãã決å®ããå¿
èŠããããŸãã ãããè¡ãã«ã¯ããã¬ãŒã€ãŒã«åªå
床ã®é ã«è¿œå ãããããããreadyForLeadingïŒïŒã¡ãœããã§æºåç¶æ
ãæäŸã§ããŸãã å®å
šãªãµã³ãã«ã³ãŒãã¯GitHubã§èŠãããšãã§ããŸãã
ChromeCastã®åŸã«ç掻ã¯ãããŸãã
Chromecastã®ãµããŒããã¢ããªã±ãŒã·ã§ã³ã«çµ±åããåŸãããããã©ã³ã ãã§ãªããGoogle Homeã䜿ã£ãŠå®¶ã«åž°ã£ãŠãªãŒãã£ãªããã¯ã楜ããããšããã楜ãããªããŸããã ã¢ãŒããã¯ãã£ã«é¢ããŠã¯ãç°ãªãã¢ããªã±ãŒã·ã§ã³ã§ã®ãã¬ãŒã€ãŒã®å®è£
ã¯ç°ãªãå Žåãããããããã®ã¢ãããŒãã¯ã©ãã§ãé©åã§ã¯ãããŸããã ããããç§ãã¡ã®ã¢ãŒããã¯ãã£ã«ã€ããŠã¯ããããåºãŠããŸããã ãã®èšäºãã圹ã«ç«ãŠã°å¹žãã§ããè¿ãå°æ¥ãããžã¿ã«ç°å¢ãšçµ±åã§ããã¢ããªã±ãŒã·ã§ã³ãããã«å¢ããããšãæåŸ
ããŠããŸãã