su.litvak.chromecast.api.v2.ChromeCast Maven / Gradle / Ivy
Show all versions of api-v2 Show documentation
/*
* Copyright 2014 Vitaly Litvak ([email protected])
*
* 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 su.litvak.chromecast.api.v2;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
/**
* ChromeCast device - main object used for interaction with ChromeCast dongle.
*/
public class ChromeCast {
public static final String SERVICE_TYPE = "_googlecast._tcp.local.";
private final EventListenerHolder eventListenerHolder = new EventListenerHolder();
private String name;
private final String address;
private final int port;
private String appsURL;
private String application;
private Channel channel;
private boolean autoReconnect = true;
private String title;
private String appTitle;
private String model;
ChromeCast(JmDNS mDNS, String name) {
this.name = name;
ServiceInfo serviceInfo = mDNS.getServiceInfo(SERVICE_TYPE, name);
this.address = serviceInfo.getInet4Addresses()[0].getHostAddress();
this.port = serviceInfo.getPort();
this.appsURL = serviceInfo.getURLs().length == 0 ? null : serviceInfo.getURLs()[0];
this.application = serviceInfo.getApplication();
this.title = serviceInfo.getPropertyString("fn");
this.appTitle = serviceInfo.getPropertyString("rs");
this.model = serviceInfo.getPropertyString("md");
}
public ChromeCast(String address) {
this(address, 8009);
}
public ChromeCast(String address, int port) {
this.address = address;
this.port = port;
}
/**
* @return The technical name of the device. Usually something like Chromecast-e28835678bc02247abcdef112341278f.
*/
public final String getName() {
return name;
}
public final void setName(String name) {
this.name = name;
}
/**
* @return The IP address of the device.
*/
public final String getAddress() {
return address;
}
/**
* @return The TCP port number that the device is listening to.
*/
public final int getPort() {
return port;
}
public final String getAppsURL() {
return appsURL;
}
public final void setAppsURL(String appsURL) {
this.appsURL = appsURL;
}
/**
* @return The mDNS service name. Usually "googlecast".
*
* @see #getRunningApp()
*/
public final String getApplication() {
return application;
}
public final void setApplication(String application) {
this.application = application;
}
/**
* @return The name of the device as entered by the person who installed it.
* Usually something like "Living Room Chromecast".
*/
public final String getTitle() {
return title;
}
/**
* @return The title of the app that is currently running, or empty string in case of the backdrop.
* Usually something like "YouTube" or "Spotify", but could also be, say, the URL of a web page being mirrored.
*/
public final String getAppTitle() {
return appTitle;
}
/**
* @return The model of the device. Usually "Chromecast" or, if Chromecast is built into your TV,
* the model of your TV.
*/
public final String getModel() {
return model;
}
/**
* Returns the {@link #channel}. May open it if autoReconnect
is set to "true" (default value)
* and it's not yet or no longer open.
* @return an open channel.
*/
private synchronized Channel channel() throws IOException {
if (autoReconnect) {
try {
connect();
} catch (GeneralSecurityException e) {
throw new IOException(e);
}
}
return channel;
}
public final synchronized void connect() throws IOException, GeneralSecurityException {
if (channel == null || channel.isClosed()) {
channel = new Channel(this.address, this.port, this.eventListenerHolder);
channel.open();
}
}
public final synchronized void disconnect() throws IOException {
if (channel == null) {
return;
}
channel.close();
channel = null;
}
public final boolean isConnected() {
return channel != null && !channel.isClosed();
}
/**
* Changes behaviour for opening/closing of connection with ChromeCast device. If set to "true" (default value)
* then connection will be re-established on every request in case it is not present yet, or has been lost.
* "false" value means manual control over connection with ChromeCast device, i.e. calling connect()
* or disconnect()
methods when needed.
*
* @param autoReconnect true means controlling connection with ChromeCast device automatically, false - manually
* @see #connect()
* @see #disconnect()
*/
public void setAutoReconnect(boolean autoReconnect) {
this.autoReconnect = autoReconnect;
}
/**
* @return current value of autoReconnect
setting, which controls opening/closing of connection
* with ChromeCast device
*
* @see #setAutoReconnect(boolean)
*/
public boolean isAutoReconnect() {
return autoReconnect;
}
/**
* Set up how much time to wait until request is processed (in milliseconds).
* @param requestTimeout value in milliseconds until request times out waiting for response
*/
public void setRequestTimeout(long requestTimeout) {
channel.setRequestTimeout(requestTimeout);
}
/**
* @return current chromecast status - volume, running applications, etc.
* @throws IOException
*/
public final Status getStatus() throws IOException {
return channel().getStatus();
}
/**
* @return descriptor of currently running application
* @throws IOException
*/
public final Application getRunningApp() throws IOException {
Status status = getStatus();
return status.getRunningApp();
}
/**
* @param appId application identifier
* @return true if application is available to this chromecast device, false otherwise
* @throws IOException
*/
public final boolean isAppAvailable(String appId) throws IOException {
return channel().isAppAvailable(appId);
}
/**
* @param appId application identifier
* @return true if application with specified identifier is running now
* @throws IOException
*/
public final boolean isAppRunning(String appId) throws IOException {
Status status = getStatus();
return status.getRunningApp() != null && appId.equals(status.getRunningApp().id);
}
/**
* @param appId application identifier
* @return application descriptor if app successfully launched, null otherwise
* @throws IOException
*/
public final Application launchApp(String appId) throws IOException {
Status status = channel().launch(appId);
return status == null ? null : status.getRunningApp();
}
/**
* Stops currently running application
*
* If no application is running at the moment then exception is thrown.
*
* @throws IOException
*/
public final void stopApp() throws IOException {
Application runningApp = getRunningApp();
if (runningApp == null) {
throw new ChromeCastException("No application is running in ChromeCast");
}
channel().stop(runningApp.sessionId);
}
/**
* @param level volume level from 0 to 1 to set
*/
public final void setVolume(float level) throws IOException {
channel().setVolume(new Volume(level, false, Volume.DEFAULT_INCREMENT,
Volume.DEFAULT_INCREMENT.doubleValue(), Volume.DEFAULT_CONTROL_TYPE));
}
/**
* ChromeCast does not allow you to jump levels too quickly to avoid blowing speakers.
* Setting by increment allows us to easily get the level we want
*
* @param level volume level from 0 to 1 to set
* @throws IOException
* @see sender
*/
public final void setVolumeByIncrement(float level) throws IOException {
Volume volume = this.getStatus().volume;
float total = volume.level;
if (volume.increment <= 0f) {
throw new ChromeCastException("Volume.increment is <= 0");
}
// With floating points we always have minor decimal variations, using the Math.min/max
// works around this issue
// Increase volume
if (level > total) {
while (total < level) {
total = Math.min(total + volume.increment, level);
setVolume(total);
}
// Decrease Volume
} else if (level < total) {
while (total > level) {
total = Math.max(total - volume.increment, level);
setVolume(total);
}
}
}
/**
* @param muted is to mute or not
*/
public final void setMuted(boolean muted) throws IOException {
channel().setVolume(new Volume(null, muted, Volume.DEFAULT_INCREMENT,
Volume.DEFAULT_INCREMENT.doubleValue(), Volume.DEFAULT_CONTROL_TYPE));
}
/**
* If no application is running at the moment then exception is thrown.
*
* @return current media status, state, time, playback rate, etc.
* @throws IOException
*/
public final MediaStatus getMediaStatus() throws IOException {
Application runningApp = getRunningApp();
if (runningApp == null) {
throw new ChromeCastException("No application is running in ChromeCast");
}
return channel().getMediaStatus(runningApp.transportId);
}
/**
* Resume paused media playback
*
* If no application is running at the moment then exception is thrown.
*
* @throws IOException
*/
public final void play() throws IOException {
Status status = getStatus();
Application runningApp = status.getRunningApp();
if (runningApp == null) {
throw new ChromeCastException("No application is running in ChromeCast");
}
MediaStatus mediaStatus = channel().getMediaStatus(runningApp.transportId);
if (mediaStatus == null) {
throw new ChromeCastException("ChromeCast has invalid state to resume media playback");
}
channel().play(runningApp.transportId, runningApp.sessionId, mediaStatus.mediaSessionId);
}
/**
* Pause current playback
*
* If no application is running at the moment then exception is thrown.
*
* @throws IOException
*/
public final void pause() throws IOException {
Status status = getStatus();
Application runningApp = status.getRunningApp();
if (runningApp == null) {
throw new ChromeCastException("No application is running in ChromeCast");
}
MediaStatus mediaStatus = channel().getMediaStatus(runningApp.transportId);
if (mediaStatus == null) {
throw new ChromeCastException("ChromeCast has invalid state to pause media playback");
}
channel().pause(runningApp.transportId, runningApp.sessionId, mediaStatus.mediaSessionId);
}
/**
* Moves current playback time point to specified value
*
* If no application is running at the moment then exception is thrown.
*
* @param time time point between zero and media duration
* @throws IOException
*/
public final void seek(double time) throws IOException {
Status status = getStatus();
Application runningApp = status.getRunningApp();
if (runningApp == null) {
throw new ChromeCastException("No application is running in ChromeCast");
}
MediaStatus mediaStatus = channel().getMediaStatus(runningApp.transportId);
if (mediaStatus == null) {
throw new ChromeCastException("ChromeCast has invalid state to seek media playback");
}
channel().seek(runningApp.transportId, runningApp.sessionId, mediaStatus.mediaSessionId, time);
}
private String getContentType(String url) {
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) new URL(url).openConnection();
connection.connect();
return connection.getContentType();
} catch (IOException e) {
} finally {
if (connection != null) {
connection.disconnect();
}
}
return null;
}
/**
* Loads and starts playing media in specified URL
*
* If no application is running at the moment then exception is thrown.
*
* @param url media url
* @return The new media status that resulted from loading the media.
* @throws IOException
*/
public final MediaStatus load(String url) throws IOException {
return load(url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.')), null, url, null);
}
/**
* Loads and starts playing specified media
*
* If no application is running at the moment then exception is thrown.
*
* @param mediaTitle name to be displayed
* @param thumb url of video thumbnail to be displayed, relative to media url
* @param url media url
* @param contentType MIME content type
* @return The new media status that resulted from loading the media.
* @throws IOException
*/
public final MediaStatus load(String mediaTitle, String thumb, String url, String contentType) throws IOException {
Status status = getStatus();
Application runningApp = status.getRunningApp();
if (runningApp == null) {
throw new ChromeCastException("No application is running in ChromeCast");
}
Map metadata = new HashMap(2);
metadata.put("title", mediaTitle);
metadata.put("thumb", thumb);
return channel().load(runningApp.transportId, runningApp.sessionId, new Media(url,
contentType == null ? getContentType(url) : contentType, null, null, null,
metadata, null, null), true, 0d, null);
}
/**
* Loads and starts playing specified media
*
* If no application is running at the moment then exception is thrown.
*
* @param media The media to load and play.
* See
* https://developers.google.com/cast/docs/reference/messages#Load for more details.
* @return The new media status that resulted from loading the media.
* @throws IOException
*/
public final MediaStatus load(final Media media) throws IOException {
Status status = getStatus();
Application runningApp = status.getRunningApp();
if (runningApp == null) {
throw new ChromeCastException("No application is running in ChromeCast");
}
Media mediaToPlay;
if (media.contentType == null) {
mediaToPlay = new Media(media.url, getContentType(media.url), media.duration, media.streamType,
media.customData, media.metadata, media.textTrackStyle, media.tracks);
} else {
mediaToPlay = media;
}
return channel().load(runningApp.transportId, runningApp.sessionId, mediaToPlay, true, 0d, null);
}
/**
* Sends some generic request to the currently running application.
*
* If no application is running at the moment then exception is thrown.
*
* @param namespace request namespace
* @param request request object
* @param responseClass class of the response for proper deserialization
* @param type of response
* @return deserialized response
* @throws IOException
*/
public final T send(String namespace, Request request, Class responseClass)
throws IOException {
Status status = getStatus();
Application runningApp = status.getRunningApp();
if (runningApp == null) {
throw new ChromeCastException("No application is running in ChromeCast");
}
return channel().sendGenericRequest(runningApp.transportId, namespace, request, responseClass);
}
/**
* Sends some generic request to the currently running application.
* No response is expected as a result of this call.
*
* If no application is running at the moment then exception is thrown.
*
* @param namespace request namespace
* @param request request object
* @throws IOException
*/
public final void send(String namespace, Request request) throws IOException {
send(namespace, request, null);
}
public final void registerListener(ChromeCastSpontaneousEventListener listener) {
this.eventListenerHolder.registerListener(listener);
}
public final void unregisterListener(ChromeCastSpontaneousEventListener listener) {
this.eventListenerHolder.unregisterListener(listener);
}
public final void registerConnectionListener(ChromeCastConnectionEventListener listener) {
this.eventListenerHolder.registerConnectionListener(listener);
}
public final void unregisterConnectionListener(ChromeCastConnectionEventListener listener) {
this.eventListenerHolder.unregisterConnectionListener(listener);
}
@Override
public final String toString() {
return String.format("ChromeCast{name: %s, title: %s, model: %s, address: %s, port: %d}",
this.name, this.title, this.model, this.address, this.port);
}
}