/* * FireTVService * Connect SDK * * Copyright (c) 2015 LG Electronics. * Created by Oleksii Frolov on 08 Jul 2015 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.connectsdk.service; import com.amazon.whisperplay.fling.media.controller.RemoteMediaPlayer; import com.amazon.whisperplay.fling.media.service.CustomMediaPlayer; import com.amazon.whisperplay.fling.media.service.MediaPlayerInfo; import com.amazon.whisperplay.fling.media.service.MediaPlayerStatus; import com.connectsdk.core.ImageInfo; import com.connectsdk.core.MediaInfo; import com.connectsdk.core.Util; import com.connectsdk.discovery.DiscoveryFilter; import com.connectsdk.service.capability.CapabilityMethods; import com.connectsdk.service.capability.MediaControl; import com.connectsdk.service.capability.MediaPlayer; import com.connectsdk.service.capability.listeners.ResponseListener; import com.connectsdk.service.command.FireTVServiceError; import com.connectsdk.service.command.ServiceCommandError; import com.connectsdk.service.command.ServiceSubscription; import com.connectsdk.service.config.ServiceConfig; import com.connectsdk.service.config.ServiceDescription; import com.connectsdk.service.sessions.LaunchSession; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; /** * FireTVService provides capabilities for FireTV devices. FireTVService acts as a layer on top of * Fling SDK, and requires the Fling SDK library to function. FireTVService provides the following * functionality: * - Media playback * - Media control * * Using Connect SDK for discovery/control of FireTV devices will result in your app complying with * the Fling SDK terms of service. */ public class FireTVService extends DeviceService implements MediaPlayer, MediaControl { public static final String ID = "FireTV"; private static final String META_TITLE = "title"; private static final String META_DESCRIPTION = "description"; private static final String META_MIME_TYPE = "type"; private static final String META_ICON_IMAGE = "poster"; private static final String META_NOREPLAY = "noreplay"; private static final String META_TRACKS = "tracks"; private static final String META_SRC = "src"; private static final String META_KIND = "kind"; private static final String META_SRCLANG = "srclang"; private static final String META_LABEL = "label"; private final RemoteMediaPlayer remoteMediaPlayer; private PlayStateSubscription playStateSubscription; public FireTVService(ServiceDescription serviceDescription, ServiceConfig serviceConfig) { super(serviceDescription, serviceConfig); if (serviceDescription.getDevice() instanceof RemoteMediaPlayer) { this.remoteMediaPlayer = (RemoteMediaPlayer) serviceDescription.getDevice(); } else { this.remoteMediaPlayer = null; } } /** * Get filter instance for this service which contains a name of service and id. It is used in * discovery process */ public static DiscoveryFilter discoveryFilter() { return new DiscoveryFilter(ID, "FireTV"); } /** * Prepare a service for usage */ @Override public void connect() { super.connect(); if (remoteMediaPlayer != null) { connected = true; reportConnected(connected); } } /** * Check if service is ready */ @Override public boolean isConnected() { return connected; } /** * Check if service implements connect/disconnect methods */ @Override public boolean isConnectable() { return true; } /** * Disconnect a service and close all subscriptions */ @Override public void disconnect() { super.disconnect(); if (playStateSubscription != null) { playStateSubscription.unsubscribe(); } connected = false; } @Override protected void updateCapabilities() { List<String> capabilities = new ArrayList<String>(); capabilities.add(MediaPlayer.MediaInfo_Get); capabilities.add(MediaPlayer.Display_Image); capabilities.add(MediaPlayer.Play_Audio); capabilities.add(MediaPlayer.Play_Video); capabilities.add(MediaPlayer.Close); capabilities.add(MediaPlayer.MetaData_MimeType); capabilities.add(MediaPlayer.MetaData_Thumbnail); capabilities.add(MediaPlayer.MetaData_Title); capabilities.add(MediaPlayer.Subtitles_Vtt); capabilities.add(MediaControl.Play); capabilities.add(MediaControl.Pause); capabilities.add(MediaControl.Stop); capabilities.add(MediaControl.Seek); capabilities.add(MediaControl.Duration); capabilities.add(MediaControl.Position); capabilities.add(MediaControl.PlayState); capabilities.add(MediaControl.PlayState_Subscribe); setCapabilities(capabilities); } /** * Get a priority level for a particular capability */ @Override public CapabilityPriorityLevel getPriorityLevel(Class<? extends CapabilityMethods> clazz) { if (clazz.equals(MediaPlayer.class)) { return getMediaPlayerCapabilityLevel(); } else if (clazz.equals(MediaControl.class)) { return getMediaControlCapabilityLevel(); } return CapabilityPriorityLevel.NOT_SUPPORTED; } /** * Get MediaPlayer implementation */ @Override public MediaPlayer getMediaPlayer() { return this; } /** * Get MediaPlayer priority level */ @Override public CapabilityPriorityLevel getMediaPlayerCapabilityLevel() { return CapabilityPriorityLevel.HIGH; } /** * Get MediaInfo available only during playback otherwise returns an error * @param listener */ @Override public void getMediaInfo(final MediaInfoListener listener) { final String error = "Error getting media info"; RemoteMediaPlayer.AsyncFuture<MediaPlayerInfo> asyncFuture = null; try { asyncFuture = remoteMediaPlayer.getMediaInfo(); handleAsyncFutureWithConversion(listener, asyncFuture, new ConvertResult<MediaInfo, MediaPlayerInfo>() { @Override public MediaInfo convert(MediaPlayerInfo data) throws JSONException { JSONObject metaJson = null; metaJson = new JSONObject(data.getMetadata()); List<ImageInfo> images = null; if (metaJson.has(META_ICON_IMAGE)) { images = new ArrayList<ImageInfo>(); images.add(new ImageInfo(metaJson.getString(META_ICON_IMAGE))); } MediaInfo mediaInfo = new MediaInfo(data.getSource(), metaJson.getString(META_MIME_TYPE), metaJson.getString(META_TITLE), metaJson.getString(META_DESCRIPTION), images); return mediaInfo; } }, error); } catch (Exception e) { Util.postError(listener, new FireTVServiceError(error)); return; } } /** * Not supported */ @Override public ServiceSubscription<MediaInfoListener> subscribeMediaInfo(MediaInfoListener listener) { Util.postError(listener, ServiceCommandError.notSupported()); return null; } /** * Display an image with metadata * @param url media source * @param mimeType * @param title * @param description * @param iconSrc * @param listener */ @Override public void displayImage(String url, String mimeType, String title, String description, String iconSrc, final LaunchListener listener) { setMediaSource(url, null, mimeType, title, description, iconSrc, listener); } /** * Play audio/video * @param url media source * @param mimeType * @param title * @param description * @param iconSrc * @param shouldLoop skipped in current implementation * @param listener */ @Override public void playMedia(String url, String mimeType, String title, String description, String iconSrc, boolean shouldLoop, LaunchListener listener) { setMediaSource(url, null, mimeType, title, description, iconSrc, listener); } /** * Stop and close media player on FireTV. In current implementation it's similar to stop method * @param launchSession * @param listener */ @Override public void closeMedia(LaunchSession launchSession, final ResponseListener<Object> listener) { stop(listener); } /** * Display an image with metadata * @param mediaInfo * @param listener */ @Override public void displayImage(MediaInfo mediaInfo, LaunchListener listener) { setMediaSourceFromMediaInfo(mediaInfo, listener); } /** * Play audio/video * @param mediaInfo * @param shouldLoop skipped in current implementation * @param listener */ @Override public void playMedia(MediaInfo mediaInfo, boolean shouldLoop, LaunchListener listener) { setMediaSourceFromMediaInfo(mediaInfo, listener); } /** * Get MediaControl capability. It should be used only during media playback. */ @Override public MediaControl getMediaControl() { return this; } /** * Get MediaControl priority level */ @Override public CapabilityPriorityLevel getMediaControlCapabilityLevel() { return CapabilityPriorityLevel.HIGH; } /** * Play current media. */ @Override public void play(ResponseListener<Object> listener) { final String error = "Error playing"; RemoteMediaPlayer.AsyncFuture<Void> asyncFuture = null; try { asyncFuture = remoteMediaPlayer.play(); handleVoidAsyncFuture(listener, asyncFuture, error); } catch (Exception e) { Util.postError(listener, new FireTVServiceError(error, e)); } } /** * Pause current media. */ @Override public void pause(ResponseListener<Object> listener) { final String error = "Error pausing"; RemoteMediaPlayer.AsyncFuture<Void> asyncFuture = null; try { asyncFuture = remoteMediaPlayer.pause(); handleVoidAsyncFuture(listener, asyncFuture, error); } catch (Exception e) { Util.postError(listener, new FireTVServiceError(error, e)); } } /** * Stop current media and close FireTV application. */ @Override public void stop(ResponseListener<Object> listener) { final String error = "Error stopping"; RemoteMediaPlayer.AsyncFuture<Void> asyncFuture = null; try { asyncFuture = remoteMediaPlayer.stop(); handleVoidAsyncFuture(listener, asyncFuture, error); } catch (Exception e) { Util.postError(listener, new FireTVServiceError(error, e)); } } /** * Not supported */ @Override public void rewind(ResponseListener<Object> listener) { Util.postError(listener, ServiceCommandError.notSupported()); } /** * Not supported */ @Override public void fastForward(ResponseListener<Object> listener) { Util.postError(listener, ServiceCommandError.notSupported()); } /** * Not supported */ @Override public void previous(ResponseListener<Object> listener) { Util.postError(listener, ServiceCommandError.notSupported()); } /** * Not supported */ @Override public void next(ResponseListener<Object> listener) { Util.postError(listener, ServiceCommandError.notSupported()); } /** * Seek current media. * @param position time in milliseconds * @param listener */ @Override public void seek(long position, ResponseListener<Object> listener) { final String error = "Error seeking"; RemoteMediaPlayer.AsyncFuture<Void> asyncFuture = null; try { asyncFuture = remoteMediaPlayer.seek(CustomMediaPlayer.PlayerSeekMode.Absolute, position); handleVoidAsyncFuture(listener, asyncFuture, error); } catch (Exception e) { Util.postError(listener, new FireTVServiceError(error, e)); } } /** * Get current media duration. */ @Override public void getDuration(final DurationListener listener) { final String error = "Error getting duration"; RemoteMediaPlayer.AsyncFuture<Long> asyncFuture; try { asyncFuture = remoteMediaPlayer.getDuration(); handleAsyncFuture(listener, asyncFuture, error); } catch (Exception e) { Util.postError(listener, new FireTVServiceError(error, e)); return; } } /** * Get playback position */ @Override public void getPosition(final PositionListener listener) { final String error = "Error getting position"; RemoteMediaPlayer.AsyncFuture<Long> asyncFuture; try { asyncFuture = remoteMediaPlayer.getPosition(); handleAsyncFuture(listener, asyncFuture, error); } catch (Exception e) { Util.postError(listener, new FireTVServiceError(error, e)); return; } } /** * Get playback state */ @Override public void getPlayState(final PlayStateListener listener) { final String error = "Error getting play state"; RemoteMediaPlayer.AsyncFuture<MediaPlayerStatus> asyncFuture; try { asyncFuture = remoteMediaPlayer.getStatus(); handleAsyncFutureWithConversion(listener, asyncFuture, new ConvertResult<PlayStateStatus, MediaPlayerStatus>() { @Override public PlayStateStatus convert(MediaPlayerStatus data) { return createPlayStateStatusFromFireTVStatus(data); } }, error); } catch (Exception e) { Util.postError(listener, new FireTVServiceError(error, e)); return; } } /** * Subscribe to playback state. Only single instance of subscription is available. Each new * call returns the same subscription object. */ @Override public ServiceSubscription<PlayStateListener> subscribePlayState( final PlayStateListener listener) { if (playStateSubscription == null) { playStateSubscription = new PlayStateSubscription(listener); remoteMediaPlayer.addStatusListener(playStateSubscription); } else if (!playStateSubscription.getListeners().contains(listener)) { playStateSubscription.addListener(listener); } getPlayState(listener); return playStateSubscription; } PlayStateStatus createPlayStateStatusFromFireTVStatus(MediaPlayerStatus status) { PlayStateStatus playState = PlayStateStatus.Unknown; switch (status.getState()) { case PreparingMedia: playState = PlayStateStatus.Buffering; break; case Playing: playState = PlayStateStatus.Playing; break; case Paused: playState = PlayStateStatus.Paused; break; case Finished: playState = PlayStateStatus.Finished; break; case NoSource: playState = PlayStateStatus.Idle; } return playState; } /** * Make a json metadata for Fire TV controller from strings * * * @param title * @param description * @param mimeType * @param iconImage * @return */ private String getMetadata(String title, String description, String mimeType, String iconImage, String subsUrl) throws JSONException { JSONObject json = new JSONObject(); if (title != null && !title.isEmpty()) { json.put(META_TITLE, title); } if (description != null && !description.isEmpty()) { json.put(META_DESCRIPTION, description); } json.put(META_MIME_TYPE, mimeType); if (iconImage != null && !iconImage.isEmpty()) { json.put(META_ICON_IMAGE, iconImage); } json.put(META_NOREPLAY, true); if(subsUrl != null && !subsUrl.isEmpty()) { JSONArray tracksArray = new JSONArray(); JSONObject trackObj = new JSONObject(); trackObj.put(META_KIND, "subtitles"); trackObj.put(META_LABEL, "Subtitle"); trackObj.put(META_SRC, subsUrl); trackObj.put(META_SRCLANG, "en"); tracksArray.put(trackObj); json.put(META_TRACKS, tracksArray); } return json.toString(); } private MediaLaunchObject createMediaLaunchObject() { LaunchSession launchSession = new LaunchSession(); launchSession.setService(this); launchSession.setSessionType(LaunchSession.LaunchSessionType.Media); launchSession.setAppId(remoteMediaPlayer.getUniqueIdentifier()); launchSession.setAppName(remoteMediaPlayer.getName()); MediaLaunchObject mediaLaunchObject = new MediaLaunchObject(launchSession, this); return mediaLaunchObject; } private void setMediaSourceFromMediaInfo(MediaInfo mediaInfo, LaunchListener listener) { String iconSrc = ""; if (mediaInfo.getImages() != null && !mediaInfo.getImages().isEmpty()) { ImageInfo imageInfo = mediaInfo.getImages().get(0); if (imageInfo != null) { iconSrc = imageInfo.getUrl(); } } setMediaSource(mediaInfo.getUrl(), mediaInfo.getSubsUrl(), mediaInfo.getMimeType(), mediaInfo.getTitle(), mediaInfo.getDescription(), iconSrc, listener); } private void setMediaSource(String url, String subsUrl, String mimeType, String title, String description, String iconSrc, final LaunchListener listener) { final String error = "Error setting media source"; RemoteMediaPlayer.AsyncFuture<Void> asyncFuture = null; try { final String metadata = getMetadata(title, description, mimeType, iconSrc, subsUrl); asyncFuture = remoteMediaPlayer.setMediaSource(url, metadata, true, false); } catch (Exception e) { Util.postError(listener, new FireTVServiceError(error, e)); return; } handleAsyncFutureWithConversion(listener, asyncFuture, new ConvertResult<MediaLaunchObject, Void>() { @Override public MediaLaunchObject convert(Void data) { return createMediaLaunchObject(); } }, error); } private void handleVoidAsyncFuture(final ResponseListener<Object> listener, final RemoteMediaPlayer.AsyncFuture<Void> asyncFuture, final String errorMessage) { handleAsyncFutureWithConversion(listener, asyncFuture, new ConvertResult<Object, Void>() { @Override public Object convert(Void data) { return data; } }, errorMessage); } private <T> void handleAsyncFuture(final ResponseListener<T> listener, final RemoteMediaPlayer.AsyncFuture<T> asyncFuture, final String errorMessage) { handleAsyncFutureWithConversion(listener, asyncFuture, new ConvertResult<T, T>() { @Override public T convert(T data) { return data; } }, errorMessage); } private <Response, Result> void handleAsyncFutureWithConversion( final ResponseListener<Response> listener, final RemoteMediaPlayer.AsyncFuture<Result> asyncFuture, final ConvertResult<Response, Result> conversion, final String errorMessage) { if (asyncFuture != null) { asyncFuture.getAsync(new RemoteMediaPlayer.FutureListener<Result>() { @Override public void futureIsNow(Future<Result> future) { try { Result result = future.get(); Util.postSuccess(listener, conversion.convert(result)); } catch (ExecutionException e) { Util.postError(listener, new FireTVServiceError(errorMessage, e.getCause())); } catch (Exception e) { Util.postError(listener, new FireTVServiceError(errorMessage, e)); } } }); } else { Util.postError(listener, new FireTVServiceError(errorMessage)); } } private interface ConvertResult<Response, Result> { Response convert(Result data) throws Exception; } private abstract static class Subscription<Status, Listener extends ResponseListener<Status>> implements ServiceSubscription<Listener> { List<Listener> listeners = new ArrayList<Listener>(); Status prevStatus; public Subscription(Listener listener) { this.listeners.add(listener); } synchronized void notifyListeners(final Status status) { if (!status.equals(prevStatus)) { Util.runOnUI(new Runnable() { @Override public void run() { for (Listener listener : listeners) { listener.onSuccess(status); } } }); prevStatus = status; } } @Override public Listener addListener(Listener listener) { listeners.add(listener); return listener; } @Override public void removeListener(Listener listener) { listeners.remove(listener); } @Override public List<Listener> getListeners() { return listeners; } } /** * Internal play state subscription implementation */ class PlayStateSubscription extends Subscription<PlayStateStatus, PlayStateListener> implements CustomMediaPlayer.StatusListener { public PlayStateSubscription(PlayStateListener listener) { super(listener); } @Override public void onStatusChange(MediaPlayerStatus mediaPlayerStatus, long position) { final PlayStateStatus status = createPlayStateStatusFromFireTVStatus(mediaPlayerStatus); notifyListeners(status); } @Override public void unsubscribe() { remoteMediaPlayer.removeStatusListener(this); playStateSubscription = null; } } }