package com.atomjack.vcfp.services;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.media.MediaRouter;
import com.atomjack.shared.NewLogger;
import com.atomjack.shared.PlayerState;
import com.atomjack.shared.Preferences;
import com.atomjack.shared.model.Timeline;
import com.atomjack.vcfp.BuildConfig;
import com.atomjack.vcfp.PlexHeaders;
import com.atomjack.vcfp.QueryString;
import com.atomjack.vcfp.R;
import com.atomjack.vcfp.VCFPCastConsumer;
import com.atomjack.vcfp.VoiceControlForPlexApplication;
import com.atomjack.vcfp.exceptions.UnauthorizedException;
import com.atomjack.vcfp.interfaces.ActiveConnectionHandler;
import com.atomjack.vcfp.interfaces.PlexSubscriptionListener;
import com.atomjack.vcfp.model.Capabilities;
import com.atomjack.vcfp.model.Connection;
import com.atomjack.vcfp.model.MediaContainer;
import com.atomjack.vcfp.model.PlexClient;
import com.atomjack.vcfp.model.PlexMedia;
import com.atomjack.vcfp.model.PlexResponse;
import com.atomjack.vcfp.model.PlexServer;
import com.atomjack.vcfp.model.PlexTrack;
import com.atomjack.vcfp.model.PlexVideo;
import com.atomjack.vcfp.model.Stream;
import com.atomjack.vcfp.net.PlexHttpClient;
import com.atomjack.vcfp.net.PlexHttpMediaContainerHandler;
import com.atomjack.vcfp.net.PlexHttpResponseHandler;
import com.atomjack.vcfp.receivers.RemoteControlReceiver;
import com.google.android.gms.cast.ApplicationMetadata;
import com.google.android.libraries.cast.companionlibrary.cast.CastConfiguration;
import com.google.android.libraries.cast.companionlibrary.cast.VideoCastManager;
import com.google.gson.reflect.TypeToken;
import org.json.JSONObject;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.lang.reflect.Type;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import retrofit.Call;
import retrofit.Callback;
import retrofit.Response;
import retrofit.Retrofit;
public class SubscriptionService extends Service {
public static final String CLIENT = "com.atomjack.vcfp.client";
public static final String MEDIA = "com.atomjack.vcfp.media";
public static final String PLAYLIST = "com.atomjack.vcfp.playlist";
// Common variables
private NewLogger logger;
private boolean subscribed = false;
private boolean subscribing = false;
public PlexClient client;
private PlexMedia nowPlayingMedia;
private ArrayList<? extends PlexMedia> nowPlayingPlaylist;
private PlexSubscriptionListener plexSubscriptionListener;
private final IBinder subBind = new SubscriptionBinder();
private PlayerState currentState = PlayerState.STOPPED;
int position = 0;
private MediaSessionCompat mediaSession;
ComponentName remoteControlReceiver;
// Plex client specific variables
private static final int SUBSCRIBE_INTERVAL = 30000; // Send subscribe message every 30 seconds to keep us alive
private ServerSocket serverSocket;
Thread serverThread = null;
Handler updateConversationHandler;
private int subscriptionPort = 59409;
private static Serializer serial = new Persister();
private int commandId = 1;
private int failedHeartbeats = 0;
private final int failedHeartbeatMax = 5;
public Date timeLastHeardFromClient;
private Calendar lastHeartbeatResponded;
private Handler mHandler = new Handler();
private Timeline currentTimeline;
// Chromecast specific variables
private static VideoCastManager castManager = null;
private VCFPCastConsumer castConsumer;
private String mSessionId;
private String plexSessionId;
private double volume = 1.0;
private String transientToken;
public static final class PARAMS {
public static final String MEDIA_TYPE = "media_type";
public static final String MEDIA_TYPE_VIDEO = "media_type_video";
public static final String MEDIA_TYPE_AUDIO = "media_type_audio";
public static final String TITLE = "title";
public static final String PLOT = "plot";
public static final String RUNTIME = "runtime";
public static final String KEY = "key";
public static final String THUMB = "thumb";
public static final String OFFSET = "offset";
public static final String RESUME = "resume";
public static final String VOLUME = "volume";
public static final String SRC = "src";
public static final String SUBTITLE_SRC = "subtitle_src";
public static final String AUDIO_STREAMS = "audio_streams";
public static final String SUBTITLE_STREAMS = "subtitle_streams";
public static final String ACTIVE_SUBTITLE = "active_subtitle";
public static final String STREAM_TYPE = "stream_type";
public static final String STREAM_ID = "stream_id";
public static final String SESSION_ID = "session_id";
public static final String ART = "art";
public static final String ARTIST = "artist";
public static final String ALBUM = "album";
public static final String TRACK = "track";
public static final String CLIENT = "client";
public static final String PLAYLIST = "playlist";
public static final String ACTION = "action";
public static final String ACTION_LOAD = "load";
public static final String ACTION_PLAY = "play";
public static final String ACTION_PAUSE = "pause";
public static final String ACTION_STOP = "stop";
public static final String ACTION_SEEK = "seek";
public static final String ACTION_GET_PLAYBACK_STATE = "getPlaybackState";
public static final String ACTION_NEXT = "next";
public static final String ACTION_PREV = "prev";
public static final String ACTION_SET_STREAM = "setStream";
public static final String ACTION_CYCLE_STREAMS = "cycleStreams";
public static final String ACTION_SET_VOLUME = "setVolume";
public static final String PLEX_USERNAME = "plexUsername";
public static final String ACCESS_TOKEN = "accessToken";
public static final String RECEIVE_SERVERS = "receiveServers";
public static final String SERVERS = "servers";
public static final String ACTIVE_CONNECTIONS = "active_connections";
public static final String PLAYBACK_LIMITED = "playback_limited"; // Whether or not playback should stop after 1 minute
};
public static final class RECEIVER_EVENTS {
public static final String PLAYLIST_ADVANCE = "playlistAdvance";
public static final String PLAYER_STATUS_CHANGED = "playerStatusChanged";
public static final String GET_PLAYBACK_STATE = "getPlaybackState";
public static final String TIME_UPDATE = "timeUpdate";
public static final String DEVICE_CAPABILITIES = "deviceCapabilities";
public static final String SHUTDOWN = "shutdown";
}
/*
PlexSubscriptionListener interface:
void onSubscribed(PlexClient client, boolean showFeedback);
void onUnsubscribed();
void onTimeUpdate(PlayerState state, int seconds);
void onMediaChanged(PlexMedia media, PlayerState state);
void onStateChanged(PlexMedia media, PlayerState state);
void onPlayStarted(PlexMedia media, ArrayList<? extends PlexMedia> playlist, PlayerState state);
void onSubscribeError(String message);
*/
public SubscriptionService() {
logger = new NewLogger(this);
plexSessionId = VoiceControlForPlexApplication.generateRandomString();
setCastConsumer();
}
@Override
public void onCreate() {
super.onCreate();
remoteControlReceiver = new ComponentName(getPackageName(), RemoteControlReceiver.class.getName());
mediaSession = new MediaSessionCompat(this, "VCFPRemoteControlReceiver", remoteControlReceiver, null);
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
PlaybackStateCompat state = new PlaybackStateCompat.Builder()
.setActions(PlaybackStateCompat.ACTION_FAST_FORWARD |
PlaybackStateCompat.ACTION_PAUSE |
PlaybackStateCompat.ACTION_PLAY |
PlaybackStateCompat.ACTION_PLAY_PAUSE |
PlaybackStateCompat.ACTION_SKIP_TO_NEXT |
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS |
PlaybackStateCompat.ACTION_STOP)
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1, SystemClock.elapsedRealtime())
.build();
mediaSession.setPlaybackState(state);
Intent intent = new Intent(this, RemoteControlReceiver.class);
PendingIntent pintent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
mediaSession.setMediaButtonReceiver(pintent);
mediaSession.setActive(false);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return subBind;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
logger.d("onStartCommand: %s", intent != null ? intent.getAction() : "no intent");
if(intent == null) {
// Service was restarted after being destroyed by the system
} else {
if(intent.getParcelableExtra(CLIENT) != null) {
client = intent.getParcelableExtra(CLIENT);
}
if(intent.getParcelableExtra(MEDIA) != null) {
nowPlayingMedia = intent.getParcelableExtra(MEDIA);
}
String action = intent.getAction();
if(action != null) {
if (intent.getAction().equals(com.atomjack.shared.Intent.ACTION_PLAY)) {
play();
} else if (intent.getAction().equals(com.atomjack.shared.Intent.ACTION_PAUSE)) {
pause();
} else if (intent.getAction().equals(com.atomjack.shared.Intent.ACTION_STOP)) {
stop();
} else if (intent.getAction().equals(com.atomjack.shared.Intent.ACTION_REWIND)) {
rewind();
} else if (intent.getAction().equals(com.atomjack.shared.Intent.ACTION_PREVIOUS)) {
previous();
} else if (intent.getAction().equals(com.atomjack.shared.Intent.ACTION_FORWARD)) {
forward();
} else if (intent.getAction().equals(com.atomjack.shared.Intent.ACTION_NEXT)) {
next();
} else if (intent.getAction().equals(com.atomjack.shared.Intent.ACTION_DISCONNECT)) {
unsubscribe();
}
}
}
return Service.START_STICKY;
}
public class SubscriptionBinder extends Binder {
public SubscriptionService getService() {
return SubscriptionService.this;
}
}
public void setListener(PlexSubscriptionListener listener) {
plexSubscriptionListener = listener;
}
public boolean isSubscribed() {
return subscribed;
}
public boolean isSubscribing() { return subscribing; }
/*
public void subscribe(PlexClient client) {
if(client.isCastClient) {
subscribe(client, null, true);
} else if(client.isLocalClient) {
} else {
startSubscription(client, true);
}
}
*/
public PlexClient getClient() {
return client;
}
public PlayerState getCurrentState() {
return currentState;
}
public PlexMedia getNowPlayingMedia() {
return nowPlayingMedia;
}
public int getPosition() {
return position;
}
// Playback methods
public void play() {
if(client.isCastClient) {
sendMessage(PARAMS.ACTION_PLAY);
} else {
client.play();
}
}
public void pause() {
if(client.isCastClient) {
sendMessage(PARAMS.ACTION_PAUSE);
} else {
client.pause();
}
}
public void stop() {
if (client.isCastClient) {
sendMessage(PARAMS.ACTION_STOP);
} else {
client.stop();
}
}
public void rewind() {
if (client.isCastClient) {
// null
} else {
client.seekTo((position*1000) - 15000);
}
}
public void forward() {
if (client.isCastClient) {
// null
} else {
client.seekTo((position*1000) + 30000);
}
}
public void previous() {
if (client.isCastClient) {
sendMessage(PARAMS.ACTION_PREV);
} else {
client.previous(nowPlayingMedia.isMusic() ? "music" : "video", null);
}
}
public void next() {
if (client.isCastClient) {
sendMessage(PARAMS.ACTION_NEXT);
} else {
client.next(nowPlayingMedia.isMusic() ? "music" : "video", null);
}
}
public void seekTo(int seconds) {
if (client.isCastClient) {
nowPlayingMedia.server.findServerConnection(new ActiveConnectionHandler() {
@Override
public void onSuccess(Connection connection) {
JSONObject obj = new JSONObject();
try {
obj.put(PARAMS.ACTION, PARAMS.ACTION_SEEK);
obj.put(PARAMS.OFFSET, seconds);
if (nowPlayingMedia instanceof PlexVideo)
obj.put(PARAMS.SRC, getTranscodeUrl(nowPlayingMedia, connection, seconds));
obj.put(PARAMS.RESUME, VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.RESUME, false));
sendMessage(obj);
plexSubscriptionListener.onTimeUpdate(currentState, seconds);
position = seconds;
} catch (Exception ex) {
}
}
@Override
public void onFailure(int statusCode) {
// TODO: Handle failure
}
});
} else {
client.seekTo((seconds*1000) + 30000);
}
}
public void setStream(Stream stream) {
logger.d("setStream: %s", stream.getTitle());
if(client.isCastClient) {
setActiveStream(stream);
} else if(!client.isLocalClient) {
PlexHttpClient.PlexHttpService service = PlexHttpClient.getService(String.format("http://%s:%s", client.address, client.port));
HashMap<String, String> qs = new HashMap<>();
if (stream.streamType == Stream.AUDIO) {
qs.put("audioStreamID", stream.id);
} else if (stream.streamType == Stream.SUBTITLE) {
qs.put("subtitleStreamID", stream.id);
}
qs.put("type", nowPlayingMedia.isMusic() ? "music" : "video");
Call<PlexResponse> call = service.setStreams(qs, "0", VoiceControlForPlexApplication.getInstance().getUUID());
call.enqueue(new Callback<PlexResponse>() {
@Override
public void onResponse(Response<PlexResponse> response, Retrofit retrofit) {
if(response.body() != null)
logger.d("setStream response: %s", response.body().status);
}
@Override
public void onFailure(Throwable t) {
}
});
}
}
public void cycleStreams(int streamType) {
Stream newStream = nowPlayingMedia.getNextStream(streamType);
setStream(newStream);
nowPlayingMedia.setActiveStream(newStream);
}
public void subtitlesOn() {
setStream(nowPlayingMedia.getStreams(Stream.SUBTITLE).get(1));
nowPlayingMedia.setActiveStream(nowPlayingMedia.getStreams(Stream.SUBTITLE).get(1));
}
public void subtitlesOff() {
setStream(nowPlayingMedia.getStreams(Stream.SUBTITLE).get(0));
nowPlayingMedia.setActiveStream(nowPlayingMedia.getStreams(Stream.SUBTITLE).get(0));
}
// end Playback methods
public void unsubscribe() {
unsubscribe(null);
}
public void unsubscribe(final Runnable onFinish) {
unsubscribe(true, onFinish);
}
public void unsubscribe(final boolean notify, final Runnable onFinish) {
if(client == null) {
onUnsubscribed();
return;
}
if(client.isLocalClient) {
subscribed = false;
onUnsubscribed();
} else if(client.isCastClient) {
try {
logger.d("is connected: %s", castManager.isConnected());
if(castManager.isConnected()) {
castManager.disconnect();
}
} catch (Exception ex) {
ex.printStackTrace();
}
subscribed = false;
client = null;
if(plexSubscriptionListener != null)
plexSubscriptionListener.onUnsubscribed();
} else {
PlexHttpClient.unsubscribe(client, commandId, VoiceControlForPlexApplication.getInstance().prefs.getUUID(), VoiceControlForPlexApplication.getInstance().getString(R.string.app_name), new PlexHttpResponseHandler() {
@Override
public void onSuccess(PlexResponse response) {
logger.d("Unsubscribed");
subscribed = false;
VoiceControlForPlexApplication.getInstance().prefs.remove(Preferences.SUBSCRIBED_CLIENT);
commandId++;
client = null;
mHandler.removeCallbacks(subscriptionHeartbeat);
try {
serverSocket.close();
serverSocket = null;
} catch (Exception ex) {
logger.d("Exception attempting to close socket.");
ex.printStackTrace();
}
if (notify)
onUnsubscribed();
if (onFinish != null)
onFinish.run();
}
@Override
public void onFailure(Throwable error) {
logger.d("failure unsubscribing");
subscribed = false;
mHandler.removeCallbacks(subscriptionHeartbeat);
try {
serverSocket.close();
serverSocket = null;
} catch (Exception ex) {
logger.d("Exception attempting to close socket due to failed unsubscribe.");
ex.printStackTrace();
}
onUnsubscribed();
}
});
}
}
private void registerMediaSessionCallback() {
mediaSession.setCallback(new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
play();
}
@Override
public void onPause() {
pause();
}
@Override
public void onSkipToNext() {
next();
}
@Override
public void onSkipToPrevious() {
previous();
}
@Override
public void onStop() {
stop();
}
});
}
@Override
public void onDestroy() {
super.onDestroy();
mediaSession.release();
}
// Plex client specific methods
public void checkLastHeartbeat() {
if(subscribed) {
Calendar cal = Calendar.getInstance();
SimpleDateFormat format = new SimpleDateFormat("EEEE, MMMM d, yyyy 'at' h:mm:ss a");
cal.add(Calendar.SECOND, -60);
if (lastHeartbeatResponded != null && lastHeartbeatResponded.before(cal)) {
logger.d("It's been more than 60 seconds since last heartbeat responded. now: %s, last heartbeat responded: %s", format.format(Calendar.getInstance().getTime()), format.format(lastHeartbeatResponded.getTime()));
if (client != null)
subscribe(client, true);
}
}
}
public void subscribe(final PlexClient client, boolean showFeedback) {
subscribing = true;
if(client.isLocalClient) {
subscribed = true;
subscribing = false;
this.client = client;
onSubscribed(showFeedback);
} else if(client.isCastClient) {
subscribeToChromecast(client, null, showFeedback);
} else {
logger.d("PlexSubscription subscribe!: %s, handler is null: %s", client, updateConversationHandler == null);
if (updateConversationHandler == null)
startSubscription(client, showFeedback);
else
subscribe(client, false, showFeedback);
}
}
public void subscribe(PlexClient client, final boolean isHeartbeat, final boolean showFeedback) {
if(client == null)
return;
this.client = client;
PlexHttpClient.subscribe(client, subscriptionPort, commandId, VoiceControlForPlexApplication.getInstance().getUUID(), VoiceControlForPlexApplication.getInstance().getString(R.string.app_name), new PlexHttpResponseHandler() {
@Override
public void onSuccess(PlexResponse response) {
subscribing = false;
failedHeartbeats = 0;
if(!isHeartbeat)
logger.d("PlexSubscription: Subscribed: %s, Code: %d", response != null ? response.status : "", response.code);
else
logger.d("PlexSubscription: Heartbeat: %s, Code: %d", response != null ? response.status : "", response.code);
if(response.code != 200) {
this.onFailure(new Throwable(response.status));
// Close the server socket so it's no longer listening on the subscriptionPort
try {
serverSocket.close();
serverSocket = null;
} catch (Exception e) {}
} else {
timeLastHeardFromClient = new Date();
commandId++;
subscribed = true;
if (!isHeartbeat) {
// Start the heartbeat subscription (so the plex client knows we're still here)
mHandler.removeCallbacks(subscriptionHeartbeat);
mHandler.postDelayed(subscriptionHeartbeat, SUBSCRIBE_INTERVAL);
onSubscribed(showFeedback);
} else {
lastHeartbeatResponded = Calendar.getInstance();
}
}
}
@Override
public void onFailure(final Throwable error) {
error.printStackTrace();
subscribing = false;
if(isHeartbeat) {
failedHeartbeats++;
logger.d("%d failed heartbeats", failedHeartbeats);
if(failedHeartbeats >= failedHeartbeatMax) {
logger.d("Unsubscribing due to failed heartbeats");
// Since several heartbeats in a row failed, set ourselves as unsubscribed and notify any listeners that we're no longer subscribed. Don't
// bother trying to actually unsubscribe since we probably can't rely on the client to respond at this point
//
subscribed = false;
onUnsubscribed();
mHandler.removeCallbacks(subscriptionHeartbeat);
mHandler.post(() -> {
VoiceControlForPlexApplication.getInstance().cancelNotification();
if(plexSubscriptionListener != null) {
plexSubscriptionListener.onSubscribeError(String.format(VoiceControlForPlexApplication.getInstance().getString(R.string.client_lost_connection), client.name));
}
});
}
} else {
mHandler.post(() -> {
if(plexSubscriptionListener != null) {
plexSubscriptionListener.onSubscribeError(error.getMessage());
}
VoiceControlForPlexApplication.getInstance().cancelNotification();
});
}
}
});
}
private void onSubscribed(final boolean showFeedback) {
mHandler.post(() -> {
logger.d("PlexSubscription onSubscribed, client: %s", client);
if(plexSubscriptionListener != null)
plexSubscriptionListener.onSubscribed(client, showFeedback);
});
}
private void onUnsubscribed() {
VoiceControlForPlexApplication.getInstance().cancelNotification();
nowPlayingMedia = null;
mHandler.post(() -> {
if (plexSubscriptionListener != null)
plexSubscriptionListener.onUnsubscribed();
});
}
private Runnable subscriptionHeartbeat = new Runnable() {
@Override
public void run() {
if(subscribed) {
if(failedHeartbeats == 0) {
subscribe(client, true, false);
mHandler.postDelayed(subscriptionHeartbeat, SUBSCRIBE_INTERVAL);
}
} else {
logger.d("stopping subscription heartbeat because we are not subscribed anymore");
}
}
};
public synchronized void startSubscription(final PlexClient client, final boolean showFeedback) {
logger.d("startSubscription: %s", updateConversationHandler);
if(updateConversationHandler == null) {
updateConversationHandler = new Handler();
}
ServerThread thread = new ServerThread();
thread.onReady(() -> {
logger.d("subscribing");
subscribe(client, showFeedback);
});
serverThread = new Thread(thread);
serverThread.start();
}
class ServerThread implements Runnable {
Runnable onReady;
private void onReady(Runnable runme) {
onReady = runme;
}
private Runnable onSocketReady = new Runnable() {
@Override
public void run() {
boolean closed = serverSocket.isClosed();
if(!closed) {
onReady.run();
} else {
new Handler().postDelayed(onSocketReady, 1000);
}
}
};
public void run() {
logger.d("starting serverthread");
Socket socket = null;
try {
if(serverSocket != null) {
serverSocket.close();
}
serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress(subscriptionPort));
// subscriptionPort = serverSocket.getLocalPort();
} catch (IOException e) {
e.printStackTrace();
}
logger.d("running");
onSocketReady.run();
while (!Thread.currentThread().isInterrupted()) {
// while(true) {
try {
if (serverSocket == null)
return;
socket = serverSocket.accept();
Map<String, String> headers = new HashMap<String, String>();
String line;
Pattern p = Pattern.compile("^([^:]+): (.+)$");
Matcher matcher;
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while ((line = reader.readLine()) != null) {
matcher = p.matcher(line);
if (matcher.find()) {
headers.put(matcher.group(1), matcher.group(2));
}
if (line.equals("")) {
break; // and don't get the next line!
}
}
int contentLength = Integer.parseInt(headers.get("Content-Length"));
StringBuilder requestContent = new StringBuilder();
for (int i = 0; i < contentLength; i++) {
requestContent.append((char) reader.read());
}
/*
<Timeline address="x.x.x.x" audioStreamID="158"
containerKey="/library/metadata/14"
controllable="playPause,stop,shuffle,repeat,volume,stepBack,stepForward,seekTo,subtitleStream,audioStream"
duration="9266976" guid="com.plexapp.agents.imdb://tt0090605?lang=en"
key="/library/metadata/14" location="fullScreenVideo"
machineIdentifier="xxxxxx" mute="0" playQueueItemID="14"
port="32400" protocol="http" ratingKey="14" repeat="0" seekRange="0-9266976" shuffle="0"
state="playing" subtitleStreamID="-1" time="4087" type="video" volume="1" />
*/
String xml = requestContent.toString();
// logger.d("xml: %s", xml);
MediaContainer mediaContainer = new MediaContainer();
try {
mediaContainer = serial.read(MediaContainer.class, xml);
} catch (Resources.NotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
onMessage(mediaContainer);
// Send a response
String response = "Failure: 200 OK";
PrintStream output = new PrintStream(socket.getOutputStream());
output.flush();
output.println("HTTP/1.1 200 OK");
output.println("Content-Type: text/plain; charset=UTF-8");
output.println("Access-Control-Allow-Origin: *");
output.println("Access-Control-Max-Age: 1209600");
output.println("");
output.println(response);
output.close();
reader.close();
} catch (SocketException se) {
if(se.getMessage().equals("Socket closed")) {
updateConversationHandler = null;
}
// se.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(socket != null) {
socket.close();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
private void onMessage(final MediaContainer mediaContainer) {
mHandler.post(() -> {
List<Timeline> timelines = mediaContainer.timelines;
if(timelines != null) {
boolean foundTimeline = false;
for (final Timeline timeline : timelines) {
if (timeline.key != null) {
foundTimeline = true;
currentTimeline = timeline;
if(timeline.state == null)
timeline.state = "stopped";
PlexServer server = null;
for(PlexServer s : VoiceControlForPlexApplication.servers.values()) {
if(s.machineIdentifier.equals(timeline.machineIdentifier) || timeline.machineIdentifier.equals(String.format("transient-%s", s.machineIdentifier))) {
server = s;
break;
}
}
if(server == null) {
logger.d("Couldn't find server %s", timeline.machineIdentifier);
unsubscribe(() -> {
if(plexSubscriptionListener != null)
plexSubscriptionListener.onSubscribeError(null);
});
return;
}
final String serverName = server.name;
PlayerState oldState = currentState;
currentState = PlayerState.getState(timeline);
position = timeline.time/1000;
// If we don't currently have now playing media, or the media has changed, update nowPlayingMedia and call the appropriate listener method (onMediaChanged, or onPlayStarted)
if((nowPlayingMedia == null || (nowPlayingMedia != null && !nowPlayingMedia.key.equals(timeline.key))) && currentState != PlayerState.STOPPED) {
PlexHttpClient.get(server, timeline.containerKey, new PlexHttpMediaContainerHandler() {
@Override
public void onSuccess(MediaContainer playlistMediaContainer) {
PlexMedia media = null;
if(playlistMediaContainer.tracks.size() > 0) {
nowPlayingPlaylist = playlistMediaContainer.tracks;
for(PlexTrack t : playlistMediaContainer.tracks) {
if(t.key.equals(timeline.key)) {
media = t;
break;
}
}
} else if(playlistMediaContainer.videos.size() > 0) {
nowPlayingPlaylist = playlistMediaContainer.videos;
for(PlexVideo m : playlistMediaContainer.videos) {
if(m.key.equals(timeline.key)) {
media = m;
if(m.isClip())
m.setClipDuration();
break;
}
}
}
if(media != null) {
if(plexSubscriptionListener != null) {
if (nowPlayingMedia != null) // if we're already playing media, this new media we found is different, so notify the listener
plexSubscriptionListener.onMediaChanged(media, PlayerState.getState(timeline));
else if (currentState != PlayerState.STOPPED) { // edge case where we receive a new timeline with a state of stopped after this one, but before this one has finished processing
plexSubscriptionListener.onPlayStarted(media, nowPlayingPlaylist, PlayerState.getState(timeline));
}
}
} else {
// TODO: Handle not finding any media?
}
nowPlayingMedia = media;
if(currentState != PlayerState.STOPPED) {
VoiceControlForPlexApplication.getInstance().setNotification(client, currentState, nowPlayingMedia, nowPlayingPlaylist, mediaSession);
registerMediaSessionCallback();
}
}
@Override
public void onFailure(Throwable error) {
error.printStackTrace();
unsubscribe(() -> {
if(plexSubscriptionListener != null)
plexSubscriptionListener.onSubscribeError(error instanceof UnauthorizedException ? String.format(getString(R.string.server_unauthorized), serverName) : null);
});
}
});
} else {
if(oldState != currentState) {
// State has changed
if(plexSubscriptionListener != null)
plexSubscriptionListener.onStateChanged(nowPlayingMedia, currentState);
if(currentState == PlayerState.STOPPED) {
nowPlayingMedia = null;
VoiceControlForPlexApplication.getInstance().cancelNotification();
} else {
VoiceControlForPlexApplication.getInstance().setNotification(client, currentState, nowPlayingMedia, nowPlayingPlaylist, mediaSession);
registerMediaSessionCallback();
}
} else {
// State has not changed, so alert listener of the current timecode
if(plexSubscriptionListener != null && currentState != PlayerState.STOPPED)
plexSubscriptionListener.onTimeUpdate(currentState, timeline.time/1000); // timecode in Timeline is in ms
}
}
}
}
if(!foundTimeline) {
// No timeline was found
if(nowPlayingMedia != null) {
// There was media playing, but now none can be found on the timeline. Amazon Fire TV Plex Client (and maybe others?) goes from a timeline with
// state=playing to 3 timelines with no key, state, etc (music, video, photos). So, alert listener that playback has stopped.
nowPlayingMedia = null;
currentState = PlayerState.STOPPED;
VoiceControlForPlexApplication.getInstance().cancelNotification();
if(plexSubscriptionListener != null)
plexSubscriptionListener.onStateChanged(nowPlayingMedia, currentState);
}
}
}
});
}
// Chromecast client specific methods
public void subscribeToChromecast(PlexClient _client, Runnable onFinished, boolean showFeedback) {
if(castManager == null) {
logger.d("creating castManager");
castManager = getCastManager(this);
castManager.addVideoCastConsumer(castConsumer);
castManager.incrementUiCounter();
}
if(castManager.isConnected()) {
castManager.disconnect();
}
logger.d("selecting device: %s", _client.castDevice);
castManager.onDeviceSelected(_client.castDevice, null);
logger.d("device selected");
castConsumer.setOnConnected(() -> {
client = _client;
// currentState = castManager.getPlaybackStatus();
logger.d("castConsumer connected to %s", client.name);
sendMessage(PARAMS.ACTION_GET_PLAYBACK_STATE);
//
JSONObject obj = new JSONObject();
try {
obj.put(PARAMS.ACTION, PARAMS.RECEIVE_SERVERS);
if(!VoiceControlForPlexApplication.getInstance().hasChromecast())
obj.put(PARAMS.PLAYBACK_LIMITED, true);
Type serverType = new TypeToken<ConcurrentHashMap<String, PlexServer>>(){}.getType();
PlexServer server = VoiceControlForPlexApplication.gsonRead.fromJson(VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.SERVER, ""), PlexServer.class);
if(server.name.equals(getString(R.string.scan_all)))
obj.put(PARAMS.SERVERS, VoiceControlForPlexApplication.gsonWrite.toJson(VoiceControlForPlexApplication.getInstance().servers, serverType));
else {
ConcurrentHashMap<String, PlexServer> map = new ConcurrentHashMap<>();
map.put(server.name, server);
obj.put(PARAMS.SERVERS, VoiceControlForPlexApplication.gsonWrite.toJson(map, serverType));
}
// Send all the active connections
Type conType = new TypeToken<HashMap<String, Connection>>() {}.getType();
obj.put(PARAMS.ACTIVE_CONNECTIONS, VoiceControlForPlexApplication.gsonWrite.toJson(VoiceControlForPlexApplication.getInstance().getActiveConnectionList(), conType));
sendMessage(obj);
} catch (Exception ex) {}
subscribing = false;
subscribed = true;
if(plexSubscriptionListener != null)
plexSubscriptionListener.onSubscribed(_client, showFeedback);
else
logger.d("listener is null");
if(onFinished != null)
onFinished.run();
});
}
private static VideoCastManager getCastManager(Context context) {
if (null == castManager) {
CastConfiguration options = new CastConfiguration.Builder(BuildConfig.CHROMECAST_APP_ID)
.addNamespace("urn:x-cast:com.atomjack.vcfp")
.build();
VideoCastManager.initialize(context, options);
}
// castManager.setContext(context);
castManager = VideoCastManager.getInstance();
castManager.setStopOnDisconnect(false);
return castManager;
}
private void sendMessage(String action) {
JSONObject message = new JSONObject();
try {
message.put(PARAMS.ACTION, action);
} catch (Exception ex) {}
sendMessage(message);
}
private void sendMessage(JSONObject obj) {
try {
castManager.sendDataMessage(obj.toString());
} catch (Exception ex) {
ex.printStackTrace();
}
}
private void setCastConsumer() {
castConsumer = new VCFPCastConsumer() {
private Runnable onConnectedRunnable;
@Override
public void onApplicationDisconnected(int errorCode) {
logger.d("onApplicationDisconnected: %d", errorCode);
// super.onApplicationDisconnected(errorCode);
}
@Override
public void onDataMessageReceived(String message) {
// logger.d("DATA MESSAGE RECEIVED: %s", message);
try {
JSONObject obj = new JSONObject(message);
if(obj.has("event") && obj.has("status")) {
if(obj.getString("event").equals(RECEIVER_EVENTS.PLAYER_STATUS_CHANGED)) {
logger.d("playerStatusChanged: %s", obj.getString("status"));
PlayerState oldState = currentState;
currentState = PlayerState.getState(obj.getString("status"));
logger.d("current state: %s", currentState);
if(plexSubscriptionListener != null && oldState != currentState)
plexSubscriptionListener.onStateChanged(nowPlayingMedia, currentState);
if(currentState != PlayerState.STOPPED) {
VoiceControlForPlexApplication.getInstance().setNotification(client, currentState, nowPlayingMedia, nowPlayingPlaylist, mediaSession);
}
}
} else if(obj.has("event") && obj.getString("event").equals(RECEIVER_EVENTS.TIME_UPDATE) && obj.has("currentTime")) {
position = obj.getInt("currentTime");
if(obj.has("currentState"))
currentState = PlayerState.getState(obj.getString("currentState"));
if(plexSubscriptionListener != null)
plexSubscriptionListener.onTimeUpdate(currentState, position);
} else if(obj.has("event") && obj.getString("event").equals(RECEIVER_EVENTS.PLAYLIST_ADVANCE) && obj.has("media") && obj.has("type")) {
logger.d("playlistAdvance");
if(obj.getString("type").equals(PARAMS.MEDIA_TYPE_VIDEO))
nowPlayingMedia = VoiceControlForPlexApplication.gsonRead.fromJson(obj.getString("media"), PlexVideo.class);
else
nowPlayingMedia = VoiceControlForPlexApplication.gsonRead.fromJson(obj.getString("media"), PlexTrack.class);
if(plexSubscriptionListener != null)
plexSubscriptionListener.onMediaChanged(nowPlayingMedia, PlayerState.PLAYING);
} else if(obj.has("event") && obj.getString("event").equals(RECEIVER_EVENTS.GET_PLAYBACK_STATE) && obj.has("state")) {
PlayerState oldState = currentState;
currentState = PlayerState.getState(obj.getString("state"));
if(obj.has("media") && obj.has("type") && obj.has("client")) {
if(obj.getString("type").equals(PARAMS.MEDIA_TYPE_VIDEO)) {
nowPlayingMedia = VoiceControlForPlexApplication.gsonRead.fromJson(obj.getString("media"), PlexVideo.class);
if(obj.has("playlist")) {
Type type = new TypeToken<ArrayList<PlexVideo>>() {}.getType();
nowPlayingPlaylist = VoiceControlForPlexApplication.gsonRead.fromJson(obj.getString("playlist"), type);
}
} else {
if(obj.has("playlist")) {
Type type = new TypeToken<ArrayList<PlexTrack>>() {}.getType();
nowPlayingPlaylist = VoiceControlForPlexApplication.gsonRead.fromJson(obj.getString("playlist"), type);
}
nowPlayingMedia = VoiceControlForPlexApplication.gsonRead.fromJson(obj.getString("media"), PlexTrack.class);
}
client = VoiceControlForPlexApplication.gsonRead.fromJson(obj.getString("client"), PlexClient.class);
}
if(plexSubscriptionListener != null && oldState != currentState) {
if(oldState == PlayerState.STOPPED)
plexSubscriptionListener.onPlayStarted(nowPlayingMedia, nowPlayingPlaylist, currentState);
else
plexSubscriptionListener.onStateChanged(nowPlayingMedia, PlayerState.getState(obj.getString("state")));
}
if(currentState != PlayerState.STOPPED) {
VoiceControlForPlexApplication.getInstance().setNotification(client, currentState, nowPlayingMedia, nowPlayingPlaylist, mediaSession);
} else
VoiceControlForPlexApplication.getInstance().cancelNotification();
} else if(obj.has("event") && obj.getString("event").equals(RECEIVER_EVENTS.DEVICE_CAPABILITIES) && obj.has("capabilities")) {
Capabilities capabilities = VoiceControlForPlexApplication.gsonRead.fromJson(obj.getString("capabilities"), Capabilities.class);
client.isAudioOnly = !capabilities.displaySupported;
// TODO: Implement this
// if(listener != null)
// listener.onGetDeviceCapabilities(capabilities);
} else if(obj.has("event") && obj.getString("event").equals(RECEIVER_EVENTS.SHUTDOWN)) {
if(plexSubscriptionListener != null)
plexSubscriptionListener.onUnsubscribed();
subscribed = false;
client = null;
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
@Override
public void onRemoteMediaPlayerMetadataUpdated() {
super.onRemoteMediaPlayerMetadataUpdated();
}
@Override
public void setOnConnected(Runnable runnable) {
onConnectedRunnable = runnable;
}
@Override
public void onFailed(int resourceId, int statusCode) {
logger.d("castConsumer failed: %d", statusCode);
}
@Override
public void onConnectionSuspended(int cause) {
logger.d("onConnectionSuspended() was called with cause: " + cause);
// com.google.sample.cast.refplayer.utils.Utils.
// showToast(VideoBrowserActivity.this, R.string.connection_temp_lost);
}
@Override
public void onApplicationConnected(ApplicationMetadata appMetadata,
String sessionId, boolean wasLaunched) {
super.onApplicationConnected(appMetadata, sessionId, wasLaunched);
logger.d("onApplicationConnected()");
logger.d("metadata: %s", appMetadata);
logger.d("sessionid: %s", sessionId);
logger.d("was launched: %s", wasLaunched);
mSessionId = sessionId;
if(onConnectedRunnable != null)
onConnectedRunnable.run();
}
@Override
public void onConnectivityRecovered() {
}
@Override
public void onApplicationStatusChanged(String appStatus) {
logger.d("onApplicationStatusChanged: %s", appStatus);
}
@Override
public void onApplicationConnectionFailed(int errorCode) {
logger.d("onApplicationConnectionFailed: %d", errorCode);
// TODO: handle error properly
if(plexSubscriptionListener != null)
plexSubscriptionListener.onSubscribeError("");
}
@Override
public void onVolumeChanged(double value, boolean isMute) {
super.onVolumeChanged(value, isMute);
logger.d("Volume is now %s", Double.toString(value));
volume = value;
}
@Override
public void onCastDeviceDetected(final MediaRouter.RouteInfo info) {
logger.d("onCastDeviceDetected: %s", info);
}
};
}
public double getVolume() {
return volume;
}
public void setVolume(double v) {
JSONObject obj = new JSONObject();
try {
obj.put(PARAMS.ACTION, PARAMS.ACTION_SET_VOLUME);
obj.put(PARAMS.VOLUME, v);
sendMessage(obj);
volume = v;
} catch (Exception ex) {
ex.printStackTrace();
}
}
public String getTranscodeUrl(PlexMedia media, Connection connection, int offset) {
return getTranscodeUrl(media, connection, offset, false);
}
public String getTranscodeUrl(PlexMedia media, Connection connection, int offset, boolean subtitles) {
logger.d("getTranscodeUrl, offset: %d", offset);
String url = connection.uri;
url += String.format("/%s/:/transcode/universal/%s?", (media instanceof PlexVideo ? "video" : "music"), (subtitles ? "subtitles" : "start"));
QueryString qs = new QueryString("path", String.format("http://127.0.0.1:32400%s", media.key));
qs.add("mediaIndex", "0");
qs.add("partIndex", "0");
qs.add("subtitles", "auto");
qs.add("copyts", "1");
qs.add("subtitleSize", "100");
qs.add("Accept-Language", "en");
qs.add("X-Plex-Client-Profile-Extra", "");
qs.add("X-Plex-Chunked", "1");
qs.add("X-Plex-Username", VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.PLEX_USERNAME, ""));
qs.add("protocol", "http");
qs.add("offset", Integer.toString(offset));
qs.add("fastSeek", "1");
// String[] videoQuality = VoiceControlForPlexApplication.chromecastVideoQualityOptions.get(VoiceControlForPlexApplication.getInstance().prefs.getString(connection.local ? Preferences.CHROMECAST_VIDEO_QUALITY_LOCAL : Preferences.CHROMECAST_VIDEO_QUALITY_REMOTE));
// qs.add("directPlay", videoQuality.length == 3 && videoQuality[2] == "1" ? "1" : "0");
qs.add("directPlay", "0");
qs.add("directStream", "1");
qs.add("videoQuality", "60");
qs.add("maxVideoBitrate", VoiceControlForPlexApplication.chromecastVideoQualityOptions.get(VoiceControlForPlexApplication.getInstance().prefs.getString(connection.local ? Preferences.CHROMECAST_VIDEO_QUALITY_LOCAL : Preferences.CHROMECAST_VIDEO_QUALITY_REMOTE))[0]);
qs.add("videoResolution", VoiceControlForPlexApplication.chromecastVideoQualityOptions.get(VoiceControlForPlexApplication.getInstance().prefs.getString(connection.local ? Preferences.CHROMECAST_VIDEO_QUALITY_LOCAL : Preferences.CHROMECAST_VIDEO_QUALITY_REMOTE))[1]);
qs.add("audioBoost", "100");
qs.add("session", plexSessionId);
qs.add(PlexHeaders.XPlexClientIdentifier, VoiceControlForPlexApplication.getInstance().prefs.getUUID());
qs.add(PlexHeaders.XPlexProduct, String.format("%s Chromecast", VoiceControlForPlexApplication.getInstance().getString(R.string.app_name)));
qs.add(PlexHeaders.XPlexDevice, client.castDevice.getModelName());
qs.add(PlexHeaders.XPlexDeviceName, client.castDevice.getModelName());
qs.add(PlexHeaders.XPlexPlatform, client.castDevice.getModelName());
if(transientToken != null)
qs.add(PlexHeaders.XPlexToken, transientToken);
qs.add(PlexHeaders.XPlexPlatformVersion, "1.0");
try {
qs.add(PlexHeaders.XPlexVersion, getPackageManager().getPackageInfo(getPackageName(), 0).versionName);
} catch (Exception ex) {
ex.printStackTrace();
}
if(VoiceControlForPlexApplication.getInstance().prefs.getString(Preferences.PLEX_USERNAME) != null)
qs.add(PlexHeaders.XPlexUsername, VoiceControlForPlexApplication.getInstance().prefs.getString(Preferences.PLEX_USERNAME));
return url + qs.toString();
}
public void setActiveStream(final Stream stream) {
nowPlayingMedia.server.findServerConnection(new ActiveConnectionHandler() {
@Override
public void onSuccess(Connection connection) {
JSONObject obj = new JSONObject();
try {
obj.put(PARAMS.ACTION, PARAMS.ACTION_SET_STREAM);
obj.put(PARAMS.STREAM_TYPE, stream.streamType);
obj.put(PARAMS.STREAM_ID, stream.id);
plexSessionId = VoiceControlForPlexApplication.generateRandomString();
obj.put(PARAMS.SESSION_ID, plexSessionId);
obj.put(PARAMS.SRC, getTranscodeUrl(nowPlayingMedia, connection, Integer.parseInt(nowPlayingMedia.viewOffset) / 1000));
obj.put(PARAMS.SUBTITLE_SRC, getTranscodeUrl(nowPlayingMedia, connection, Integer.parseInt(nowPlayingMedia.viewOffset) / 1000, true));
sendMessage(obj);
} catch (Exception ex) {
}
}
@Override
public void onFailure(int statusCode) {
}
});
}
// This will send a message to the cast device to load the passed in media
public void loadMedia(PlexMedia media, ArrayList<? extends PlexMedia> album, final int offset) {
logger.d("Loading media: %s", album);
nowPlayingMedia = media;
nowPlayingPlaylist = album;
nowPlayingMedia.server.findServerConnection(new ActiveConnectionHandler() {
@Override
public void onSuccess(Connection connection) {
sendMessage(buildMedia(connection, offset));
}
@Override
public void onFailure(int statusCode) {
}
});
}
public JSONObject buildMedia(Connection connection, int offset) {
JSONObject data = new JSONObject();
try {
data.put(PARAMS.ACTION, PARAMS.ACTION_LOAD);
if(VoiceControlForPlexApplication.getInstance().prefs.getString(Preferences.PLEX_USERNAME) != null) {
data.put(PARAMS.PLEX_USERNAME, VoiceControlForPlexApplication.getInstance().prefs.getString(Preferences.PLEX_USERNAME));
}
data.put(PARAMS.MEDIA_TYPE, nowPlayingMedia instanceof PlexVideo ? PARAMS.MEDIA_TYPE_VIDEO : PARAMS.MEDIA_TYPE_AUDIO);
data.put(PARAMS.RESUME, VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.RESUME, false));
data.put(PARAMS.CLIENT, VoiceControlForPlexApplication.gsonWrite.toJson(client));
data.put(PARAMS.SESSION_ID, plexSessionId);
logger.d("setting src to %s", getTranscodeUrl(nowPlayingMedia, connection, offset));
data.put(PARAMS.SRC, getTranscodeUrl(nowPlayingMedia, connection, offset));
if(nowPlayingMedia instanceof PlexVideo) {
data.put(PARAMS.SUBTITLE_SRC, getTranscodeUrl(nowPlayingMedia, connection, offset, true));
data.put(PARAMS.AUDIO_STREAMS, VoiceControlForPlexApplication.gsonWrite.toJson(nowPlayingMedia.getStreams(Stream.AUDIO)));
data.put(PARAMS.SUBTITLE_STREAMS, VoiceControlForPlexApplication.gsonWrite.toJson(nowPlayingMedia.getStreams(Stream.SUBTITLE)));
if(nowPlayingMedia.getActiveStream(Stream.SUBTITLE) != null)
data.put(PARAMS.ACTIVE_SUBTITLE, nowPlayingMedia.getActiveStream(Stream.SUBTITLE).id);
}
data.put(PARAMS.ACCESS_TOKEN, nowPlayingMedia.server.accessToken);
data.put(PARAMS.PLAYLIST, getPlaylistJson());
} catch (Exception ex) {
ex.printStackTrace();
}
return data;
}
private String getPlaylistJson() {
return VoiceControlForPlexApplication.gsonWrite.toJson(nowPlayingPlaylist);
}
// End Chromecast client specific methods
// Local client specific methods
public void setMedia(PlexMedia m) {
if(client.isLocalClient)
nowPlayingMedia = m;
}
// End local client specific methods
}