com.connectsdk.service.FireTVService.java Source code

Java tutorial

Introduction

Here is the source code for com.connectsdk.service.FireTVService.java

Source

/*
 * 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;
        }

    }

}