$30 off During Our Annual Pro Sale. View Details »

Size does not matter, 2.83 inches is enough

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
Tweet

More Decks by David González

Other Decks in Technology

Transcript

  1. image
    Size does not matter
    2.83 inches is enough

    View Slide

  2. David González
    dggonzalez
    +davidgonzalezmalmstein
    malmstein
    Android Software Craftsman
    Google Developer Expert Android

    View Slide

  3. image
    Let’s play a little game

    View Slide

  4. image
    https://www.youtube.com/watch?v=o0Ji2PYPyFU&feature=youtu.be

    View Slide

  5. View Slide

  6. Discovery

    View Slide

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

    View Slide

  8. Route selector

    View Slide

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

    View Slide

  10. Cast button: ActionBar

    xmlns:app="http://schemas.android.com/apk/res-auto" >
    android:title="@string/media_route_menu_title"
    app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
    app:showAsAction="always" />

    View Slide

  11. 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);

    }

    View Slide

  12. Cast button: MediaRouteButton
    * To use the media route button, the activity must be a subclass of
    * {@link FragmentActivity} from the android.support.v4
    * support library. Refer to support library documentation for details.
    *
    *
    * @see MediaRouteActionProvider
    * @see #setRouteSelector
    */
    public class MediaRouteButton extends View {

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  17. Connection
    private final class ConnectionResultCallback implements
    ResultCallback {
    @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);
    }
    }
    }

    View Slide

  18. image
    Messages between Sender and Receiver
    http://www.wondersandmarvels.com/wp-content/uploads/2014/10/3-pigeons-Pigeoncameras.jpg

    View Slide

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

    View Slide

  20. 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 {
    ....
    @Override
    public void onResult(Status result) {
    if (!result.isSuccess()) {
    Log.d(TAG, "Failed to send message. statusCode: " +
    result.getStatusCode() + " message: " + mMessage); }
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  23. image

    View Slide

  24. image
    http://fc02.deviantart.net/fs18/i/2012/235/6/1/remote_control_cat_by_rengokuy-dx54f7.jpg

    View Slide

  25. View Slide

  26. image
    https://lh3.googleusercontent.com/-xzAEWO6TYiw/VEq5lQXaKzI/AAAAAAAADVY/Ga2AQ9v_EMo/w1184-h888-no/2014%2B-%2B1

    View Slide

  27. Default Receiver
    UI not customizable
    Hosted by Google
    CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID

    View Slide

  28. 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;
    }

    View Slide

  29. Custom receiver

    View Slide

  30. Custom receiver


    Example minimum receiver




    <br/>window.mediaElement = document.getElementById('media');<br/>window.mediaManager = new cast.receiver.MediaManager(window.mediaElement);<br/>window.castReceiverManager = cast.receiver.CastReceiverManager.getInstance();<br/>window.castReceiverManager.start();<br/>


    View Slide

  31. Tip: Host in Google Drive

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  37. Cast Activity
    getCastManager().startCastControllerActivity(
    this, mSelectedMedia, position, true);

    View Slide

  38. Tip: Make Cast button always available

    View Slide

  39. Further reading
    https://developers.google.com/cast/docs/design_checklist
    http://stackoverflow.com/questions/tagged/google-cast
    https://github.com/googlecast/CastCompanionLibrary-android
    https://github.com/malmstein/cast_tictactoe

    View Slide

  40. David González
    dggonzalez
    +davidgonzalezmalmstein
    malmstein
    Android Software Craftsman
    Google Developer Expert Android
    Trolling time,
    questions?

    View Slide