Share
Embed
Size does not matter, 2.83 inches is enough
Talk from Droidcon UK 2014 explaining how to create a small game with Chromecast and best practices when remote video streaming
David González
October 31, 2014
Transcript
40 slides
-
Media Router mMediaRouter = MediaRouter.getInstance(getApplicationContext()); @Override protected void onStart() {
super.onStart(); mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); } @Override protected void onStop() { disconnectApiClient(); mMediaRouter.removeCallback(mMediaRouterCallback); super.onStop(); } -
Route selector mMediaRouteSelector = new MediaRouteSelector.Builder() .addControlCategory(CastMediaControlIntent.categoryForCast(APP_ID)) .build(); // These
are the framework-supported intents .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO) .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) -
Cast button: ActionBar <?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/media_route_menu_item" android:title="@string/media_route_menu_title" app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider" app:showAsAction="always" /> </menu> -
Add the selector to the ActionProvider @Override public boolean onCreateOptionsMenu(Menu
menu) { super.onCreateOptionsMenu(menu); … // Attach the MediaRouteSelector to the menu item MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item); MediaRouteActionProvider mediaRouteActionProvider= (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mediaRouteMenuItem); mediaRouteActionProvider.setRouteSelector(mSelector); … } -
Cast button: MediaRouteButton * To use the media route button,
the activity must be a subclass of * {@link FragmentActivity} from the <code>android.support.v4</code> * support library. Refer to support library documentation for details. * </p> * * @see MediaRouteActionProvider * @see #setRouteSelector */ public class MediaRouteButton extends View { -
private final class MediaRouterCallback extends MediaRouter.Callback { public void onRouteAdded(MediaRouter
router, MediaRouter.RouteInfo info public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo info) public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo info) public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) The callback -
The callback, yet another one private class MediaRouterCallback extends MediaRouter.Callback
{ @Override public void onRouteSelected(MediaRouter router, RouteInfo route) { CastDevice device = CastDevice.getFromBundle(route.getExtras()); connectApiClient(); } @Override public void onRouteUnselected(MediaRouter router, RouteInfo route) { disconnectApiClient(); leaveGame(); resetUI(); } -
Connection private void connectApiClient() { Cast.CastOptions apiOptions = Cast.CastOptions.builder(mSelectedDevice, mCastListener)
.build(); mApiClient = new GoogleApiClient.Builder(this) .addApi(Cast.API, apiOptions) .addConnectionCallbacks(mConnectionCallbacks) .addOnConnectionFailedListener(mConnectionFailedListener) .build(); mApiClient.connect(); } -
Connection private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks { @Override public void
onConnectionSuspended(int cause) { Log.d(TAG, "ConnectionCallbacks.onConnectionSuspended"); } @Override public void onConnected(Bundle connectionHint) { Log.d(TAG, "ConnectionCallbacks.onConnected"); Cast.CastApi.launchApplication(mApiClient, APP_ID).setResultCallback( new ConnectionResultCallback()); } } -
Connection private final class ConnectionResultCallback implements ResultCallback<ApplicationConnectionResult> { @Override public
void onResult(ApplicationConnectionResult result) { Status status = result.getStatus(); ApplicationMetadata appMetaData = result.getApplicationMetadata(); if (status.isSuccess()) { mJoinGameButton.setEnabled(true); Cast.CastApi.setMessageReceivedCallbacks(mApiClient, mGameChannel.getNamespace(), mGameChannel); } else { mJoinGameButton.setEnabled(false); } } } -
Tic-Tac-Toe: Movement information public final void move(GoogleApiClient apiClient, int row,
int column) { Log.d(TAG, "move: row:" + row + " column:" + column); try { JSONObject payload = new JSONObject(); payload.put(KEY_COMMAND, KEY_MOVE); payload.put(KEY_ROW, row); payload.put(KEY_COLUMN, column); sendMessage(apiClient, payload.toString()); } catch (JSONException e) { Log.e(TAG, "Cannot create object to send a move", e); } } -
Tic-Tac-Toe: Sending information private final void sendMessage(GoogleApiClient apiClient, String message){
Log.d(TAG, "Sending message: (ns=" + GAME_NAMESPACE + ") " + message); Cast.CastApi.sendMessage(apiClient, GAME_NAMESPACE, message) .setResultCallback( new SendMessageResultCallback(message)); } private final class SendMessageResultCallback implements ResultCallback<Status> { .... @Override public void onResult(Status result) { if (!result.isSuccess()) { Log.d(TAG, "Failed to send message. statusCode: " + result.getStatusCode() + " message: " + mMessage); } } } -
Tic-Tac-Toe: Receiver load function onLoad() { var canvas = document.getElementById("board");
var context = canvas.getContext("2d"); var mBoard = new board(context); var favIcon = document.getElementById("favIcon"); mBoard.clear(); mBoard.drawGrid(); window.gameEngine = new cast.TicTacToe(mBoard); } -
Tic-Tac-Toe: Receiver message onMessage: function(event) { ... if (message.command ==
'join') { this.onJoin(senderId, message); } else if (message.command == 'leave') { this.onLeave(senderId); } else if (message.command == 'move') { this.onMove(senderId, message); } else if (message.command == 'board_layout_request') { this.onBoardLayoutRequest(senderId); } else { console.log('Invalid message command: ' + message.command); } }, -
Styled Media Receiver .background { background: center no-repeat url(background.png); }
.logo { background-image: url(logo.png); } .progressBar { background-color: rgb(238, 255, 65); } .splash { background-image: url(splash.png); } .watermark { background-image: url(watermark.png); background-size: 57px 57px; } -
Custom receiver <html> <head> <title>Example minimum receiver</title> <script src="//www.gstatic.com/cast/sdk/libs/receiver/2.0.0/cast_receiver.js"></script> </head>
<body> <video id='media'/> <script> window.mediaElement = document.getElementById('media'); window.mediaManager = new cast.receiver.MediaManager(window.mediaElement); window.castReceiverManager = cast.receiver.CastReceiverManager.getInstance(); window.castReceiverManager.start(); </script> </body> </html> -
Cast Companion Library https://github.com/googlecast/CastCompanionLibrary-android mCastMgr = VideoCastManager.initialize(context, BuildConfig.CAST_APP_ID, null, null);
if (BuildConfig.DEBUG) { mCastMgr.enableFeatures(VideoCastManager.FEATURE_NOTIFICATION | VideoCastManager.FEATURE_LOCKSCREEN | VideoCastManager.FEATURE_DEBUGGING); } else { mCastMgr.enableFeatures(VideoCastManager.FEATURE_NOTIFICATION | VideoCastManager.FEATURE_LOCKSCREEN); } -
Callbacks, remember? @Override public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean
wasLaunched) { if (mPlaybackState == PlaybackState.PLAYING) { notifyPauseToListeners(); try { notifyRemoteLoadToListeners(); } catch (Exception e) { notifyExceptionToListeners(e); } } updatePlaybackLocation(PlaybackLocation.REMOTE); } -
Callbacks, remember? @Override public void onApplicationDisconnected(int errorCode) { updatePlaybackLocation(PlaybackLocation.LOCAL); }
@Override public void onDisconnected() { mPlaybackState = PlaybackState.PAUSED; mLocation = PlaybackLocation.LOCAL; } @Override public void onRemoteMediaPlayerMetadataUpdated() { mRemoteMediaInformation = castManager.getRemoteMediaInformation(); } -
Receiver: MediaInfo public void createMediaInfo(String title, String description, String author,
String streamUrl, String mediaType, int streamType) { MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); metadata.putString(MediaMetadata.KEY_SUBTITLE, description); metadata.putString(MediaMetadata.KEY_TITLE, title); metadata.putString(MediaMetadata.KEY_STUDIO, author); metadata.addImage(new WebImage(Uri.parse(imageUrl))); mSelectedMedia = new MediaInfo.Builder(streamUrl) .setStreamType(streamType) .setContentType(mediaType) .setMetadata(metadata) .build(); } MediaInfo.STREAM_TYPE_LIVE MediaInfo.STREAM_TYPE_BUFFERED "application/x-mpegURL" "video/mp4" -
Tip: Enable CORS XMLHttpRequest cannot load http://artestras.vo.llnwd. net/v2/am/tvguide/HLS/055209-000-A_HQ_1_VOA_01519674_MP4- 2200_AMM-HLS/055209-000-A_HQ_1_VOA_01519674_MP4-2200_AMM-HLS. m3u8.
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://www.arte.tv' is therefore not allowed access. -
David González dggonzalez +davidgonzalezmalmstein malmstein Android Software Craftsman Google Developer
Expert Android Trolling time, questions?