com.scooter1556.sms.android.playback.CastPlayback.java Source code

Java tutorial

Introduction

Here is the source code for com.scooter1556.sms.android.playback.CastPlayback.java

Source

/*
 * Author: Scott Ware <scoot.software@gmail.com>
 * Copyright (c) 2015 Scott Ware
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.scooter1556.sms.android.playback;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import android.util.Log;

import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.gson.Gson;
import com.loopj.android.http.JsonHttpResponseHandler;
import com.loopj.android.http.TextHttpResponseHandler;
import com.scooter1556.sms.android.utils.MediaUtils;
import com.scooter1556.sms.android.domain.MediaElement;
import com.scooter1556.sms.android.domain.TranscodeProfile;
import com.scooter1556.sms.android.service.RESTService;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.List;
import java.util.UUID;

/**
 * An implementation of Playback that talks to Cast devices.
 */
public class CastPlayback implements Playback {

    private static final String TAG = "CastPlayback";

    private static final String CLIENT_ID = "chromecast";

    static final String FORMAT = "hls";
    static final String SUPPORTED_FILES = "mkv,webm,aac,m4a,mp3,oga,ogg,mp4";
    static final String SUPPORTED_CODECS = "h264,vp8,aac,mp3";
    static final String MCH_CODECS = "ac3";

    static final int MAX_SAMPLE_RATE = 48000;

    private static final String SERVER_URL = "serverUrl";
    private static final String MEDIA_ID = "mediaId";
    private static final String QUALITY = "quality";

    private final Context appContext;
    private final RemoteMediaClient remoteMediaClient;
    private final RemoteMediaClient.Listener remoteMediaClientListener;

    private int playbackState;

    private Callback callback;
    private volatile long currentPosition;
    private volatile String currentMediaId;
    private volatile UUID currentJobId;

    private boolean finished = false;

    public CastPlayback(Context context) {
        appContext = context.getApplicationContext();

        CastSession castSession = CastContext.getSharedInstance(appContext).getSessionManager()
                .getCurrentCastSession();
        remoteMediaClient = castSession.getRemoteMediaClient();
        remoteMediaClientListener = new CastMediaClientListener();
    }

    @Override
    public void start() {
        Log.d(TAG, "start()");

        remoteMediaClient.addListener(remoteMediaClientListener);
    }

    @Override
    public void stop(boolean notifyListeners) {
        Log.d(TAG, "stop()");

        playbackState = PlaybackStateCompat.STATE_STOPPED;

        if (notifyListeners && callback != null) {
            callback.onPlaybackStatusChanged(playbackState);
        }

        currentJobId = null;
    }

    @Override
    public void destroy() {
    }

    @Override
    public void setState(int state) {
        Log.d(TAG, "setState(" + state + ")");

        this.playbackState = state;
    }

    @Override
    public long getCurrentStreamPosition() {
        long pos;

        if (!isConnected()) {
            pos = currentPosition;
        } else {
            pos = remoteMediaClient.getApproximateStreamPosition();
        }

        Log.d(TAG, "getCurrentStreamPosition() < " + pos);

        return pos;
    }

    @Override
    public void setCurrentStreamPosition(long pos) {
        Log.d(TAG, "setCurrentStreamPosition(" + pos + ")");

        this.currentPosition = pos;
    }

    @Override
    public void updateLastKnownStreamPosition() {
        Log.d(TAG, "updateLastKnownStreamPosition()");

        currentPosition = getCurrentStreamPosition();
    }

    @Override
    public void play(MediaSessionCompat.QueueItem item) {
        Log.d(TAG, "play(" + item.getDescription().getMediaId() + ")");

        boolean mediaHasChanged = !TextUtils.equals(item.getDescription().getMediaId(), currentMediaId);

        if (mediaHasChanged) {
            currentMediaId = item.getDescription().getMediaId();
            currentPosition = 0;
        }

        if (playbackState == PlaybackStateCompat.STATE_PAUSED && !mediaHasChanged
                && remoteMediaClient.hasMediaSession()) {
            remoteMediaClient.play();
        } else {
            loadMedia(item, true);
            playbackState = PlaybackStateCompat.STATE_BUFFERING;
        }

        if (callback != null) {
            callback.onPlaybackStatusChanged(playbackState);
        }
    }

    @Override
    public void pause() {
        Log.d(TAG, "pause()");

        if (remoteMediaClient.hasMediaSession()) {
            remoteMediaClient.pause();
            currentPosition = (int) remoteMediaClient.getApproximateStreamPosition();
        }
    }

    @Override
    public void seekTo(long position) {
        Log.d(TAG, "seekTo(" + position + ")");

        if (currentMediaId == null) {
            if (callback != null) {
                callback.onError("Cannot seek if media ID is unknown.");
            }

            return;
        }

        if (remoteMediaClient.hasMediaSession()) {
            remoteMediaClient.seek(position);
            currentPosition = position;
        }
    }

    @Override
    public void setCurrentMediaId(String mediaId) {
        Log.d(TAG, "setCurrentMediaId(" + mediaId + ")");

        this.currentMediaId = mediaId;
    }

    @Override
    public String getCurrentMediaId() {
        Log.d(TAG, "getCurrentMediaId()");

        return currentMediaId;
    }

    @Override
    public void setSessionId(UUID sessionId) {

    }

    @Override
    public UUID getSessionId() {
        return null;
    }

    @Override
    public void setCurrentJobId(UUID jobId) {
        Log.d(TAG, "setCurrentJobId(" + jobId + ")");

        this.currentJobId = jobId;
    }

    @Override
    public UUID getCurrentJobId() {
        Log.d(TAG, "getCurrentJobId()");

        return currentJobId;
    }

    @Override
    public SimpleExoPlayer getMediaPlayer() {
        return null;
    }

    @Override
    public void setCallback(Callback callback) {
        Log.d(TAG, "setCallback()");

        this.callback = callback;
    }

    @Override
    public boolean isConnected() {
        CastSession castSession = CastContext.getSharedInstance(appContext).getSessionManager()
                .getCurrentCastSession();
        boolean connected = (castSession != null && castSession.isConnected());

        Log.d(TAG, "isConnected() < " + connected);

        return connected;
    }

    @Override
    public boolean isPlaying() {
        boolean playing = isConnected() && remoteMediaClient.isPlaying();

        Log.d(TAG, "isPlaying() < " + playing);

        return playing;
    }

    @Override
    public int getState() {
        Log.d(TAG, "getState() < " + playbackState);

        return playbackState;
    }

    private void loadMedia(final MediaSessionCompat.QueueItem item, final boolean autoPlay) {
        Log.d(TAG, "loadMedia(" + item.getDescription().getMediaId() + ", " + autoPlay + ")");

        // Get Media Element ID from Media ID
        List<String> mediaID = MediaUtils.parseMediaId(currentMediaId);

        if (mediaID.size() <= 1) {
            error("Error initialising stream", null);
            return;
        }

        initialiseStream(item, autoPlay);
    }

    private void initialiseStream(final MediaSessionCompat.QueueItem item, final boolean autoPlay) {
        Log.d(TAG, "initialiseStream(" + item.getDescription().getMediaId() + ", " + autoPlay + ")");

        // Get Media Element ID from Media ID
        List<String> mediaID = MediaUtils.parseMediaId(currentMediaId);

        if (mediaID.size() <= 1) {
            error("Error initialising stream", null);
            return;
        }

        UUID id = UUID.fromString(mediaID.get(1));

        // Get settings
        final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(appContext);

        // Get quality
        int quality = 0;

        if (MediaUtils.getMediaTypeFromID(currentMediaId) == MediaElement.MediaElementType.AUDIO) {
            quality = Integer.parseInt(settings.getString("pref_audio_quality", "0"));
        } else if (MediaUtils.getMediaTypeFromID(currentMediaId) == MediaElement.MediaElementType.VIDEO) {
            quality = Integer.parseInt(settings.getString("pref_video_quality", "0"));
        }

        // Set custom data
        JSONObject customData = new JSONObject();

        try {
            customData.put(MEDIA_ID, currentMediaId);
            customData.put(QUALITY, quality);
            customData.put(SERVER_URL, RESTService.getInstance().getAddress());
        } catch (JSONException e) {
            error("Error setting custom parameters for stream", e);
        }

        MediaInfo media = new MediaInfo.Builder(id.toString()).setContentType("media")
                .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
                .setMetadata(MediaUtils.getMediaMetadataFromMediaDescription(item.getDescription()))
                .setCustomData(customData).build();

        remoteMediaClient.load(media, autoPlay, currentPosition, customData);
    }

    private void setMetadataFromRemote() {
        Log.d(TAG, "setMetadataFromRemote()");

        // Get the custom data from the remote media information and update the local
        // metadata if it's different from the one we are currently using.
        // This can happen when the app was either reconnected, or if the
        // app joins an existing session while the cast device is playing a queue.
        try {
            MediaInfo mediaInfo = remoteMediaClient.getMediaInfo();

            if (mediaInfo == null) {
                return;
            }

            JSONObject customData = mediaInfo.getCustomData();

            if (customData != null && customData.has(MEDIA_ID)) {
                String remoteMediaId = customData.getString(MEDIA_ID);

                if (!TextUtils.equals(currentMediaId, remoteMediaId)) {
                    currentMediaId = remoteMediaId;

                    if (callback != null) {
                        callback.setCurrentMediaID(remoteMediaId);
                    }

                    updateLastKnownStreamPosition();
                }
            }
        } catch (JSONException e) {
            Log.e(TAG, "Exception processing metadata from remote client", e);
        }

    }

    private void updatePlaybackState() {
        String log = "updatePlaybackState() > ";

        int status = remoteMediaClient.getPlayerState();
        int idleReason = remoteMediaClient.getIdleReason();

        // Convert the remote playback states to media playback states
        switch (status) {
        case MediaStatus.PLAYER_STATE_IDLE:
            log += "IDLE";

            if (idleReason == MediaStatus.IDLE_REASON_FINISHED && !finished) {
                log += ":FINISHED";

                finished = true;
                currentJobId = null;

                if (callback != null) {
                    callback.onCompletion();
                }
            }

            break;

        case MediaStatus.PLAYER_STATE_BUFFERING:
            log += "BUFFERING";

            finished = false;

            playbackState = PlaybackStateCompat.STATE_BUFFERING;

            if (callback != null) {
                callback.onPlaybackStatusChanged(playbackState);
            }

            break;

        case MediaStatus.PLAYER_STATE_PLAYING:
            log += "PLAYING";

            finished = false;

            playbackState = PlaybackStateCompat.STATE_PLAYING;
            setMetadataFromRemote();

            if (callback != null) {
                callback.onPlaybackStatusChanged(playbackState);
            }

            break;

        case MediaStatus.PLAYER_STATE_PAUSED:
            log += "PAUSED";

            playbackState = PlaybackStateCompat.STATE_PAUSED;
            setMetadataFromRemote();

            if (callback != null) {
                callback.onPlaybackStatusChanged(playbackState);
            }
            break;

        default:
            log += "UNKNOWN:" + status;
            break;
        }

        Log.d(TAG, log);
    }

    private void error(String message, Exception e) {
        Log.e(TAG, "error(" + message + ")", e);

        currentJobId = null;

        if (callback != null) {
            callback.onError(message);
        }
    }

    private class CastMediaClientListener implements RemoteMediaClient.Listener {
        @Override
        public void onMetadataUpdated() {
            Log.d(TAG, "RemoteMediaClient.onMetadataUpdated");
        }

        @Override
        public void onStatusUpdated() {
            Log.d(TAG, "RemoteMediaClient.onStatusUpdated");
            updatePlaybackState();
        }

        @Override
        public void onSendingRemoteMediaRequest() {
            Log.d(TAG, "RemoteMediaClient.onSendingRemoteMediaRequest");
        }

        @Override
        public void onAdBreakStatusUpdated() {
            Log.d(TAG, "RemoteMediaClient.onAdBreakStatusUpdated");
        }

        @Override
        public void onQueueStatusUpdated() {
            Log.d(TAG, "RemoteMediaClient.onQueueStatusUpdated");
        }

        @Override
        public void onPreloadStatusUpdated() {
            Log.d(TAG, "RemoteMediaClient.onPreloadStatusUpdated");
        }
    }
}