mirror of https://github.com/YTVanced/VancedMicroG
345 lines
13 KiB
Java
345 lines
13 KiB
Java
/*
|
|
* Copyright (C) 2013-2017 microG Project Team
|
|
*
|
|
* 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 org.microg.gms.cast;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Context;
|
|
import android.content.IntentFilter;
|
|
import android.net.nsd.NsdManager;
|
|
import android.net.nsd.NsdServiceInfo;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.util.Log;
|
|
|
|
import androidx.mediarouter.media.MediaControlIntent;
|
|
import androidx.mediarouter.media.MediaRouteDescriptor;
|
|
import androidx.mediarouter.media.MediaRouteDiscoveryRequest;
|
|
import androidx.mediarouter.media.MediaRouteProvider;
|
|
import androidx.mediarouter.media.MediaRouteProviderDescriptor;
|
|
import androidx.mediarouter.media.MediaRouter;
|
|
|
|
import com.google.android.gms.cast.CastDevice;
|
|
import com.google.android.gms.cast.CastMediaControlIntent;
|
|
|
|
import java.net.InetAddress;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
public class CastMediaRouteProvider extends MediaRouteProvider {
|
|
private static final String TAG = CastMediaRouteProvider.class.getSimpleName();
|
|
|
|
private Map<String, CastDevice> castDevices = new HashMap<>();
|
|
private Map<String, String> serviceCastIds = new HashMap<>();
|
|
|
|
private NsdManager mNsdManager;
|
|
private NsdManager.DiscoveryListener mDiscoveryListener;
|
|
|
|
private List<String> customCategories = new ArrayList<>();
|
|
|
|
private enum State {
|
|
NOT_DISCOVERING,
|
|
DISCOVERY_REQUESTED,
|
|
DISCOVERING,
|
|
DISCOVERY_STOP_REQUESTED,
|
|
}
|
|
private State state = State.NOT_DISCOVERING;
|
|
|
|
private static final ArrayList<IntentFilter> BASE_CONTROL_FILTERS = new ArrayList<>();
|
|
static {
|
|
IntentFilter filter;
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(CastMediaControlIntent.CATEGORY_CAST);
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
|
filter.addAction(MediaControlIntent.ACTION_PLAY);
|
|
filter.addDataScheme("http");
|
|
filter.addDataScheme("https");
|
|
String[] types = {
|
|
"image/jpeg",
|
|
"image/pjpeg",
|
|
"image/jpg",
|
|
"image/webp",
|
|
"image/png",
|
|
"image/gif",
|
|
"image/bmp",
|
|
"image/vnd.microsoft.icon",
|
|
"image/x-icon",
|
|
"image/x-xbitmap",
|
|
"audio/wav",
|
|
"audio/x-wav",
|
|
"audio/mp3",
|
|
"audio/x-mp3",
|
|
"audio/x-m4a",
|
|
"audio/mpeg",
|
|
"audio/webm",
|
|
"audio/ogg",
|
|
"audio/x-matroska",
|
|
"video/mp4",
|
|
"video/x-m4v",
|
|
"video/mp2t",
|
|
"video/webm",
|
|
"video/ogg",
|
|
"video/x-matroska",
|
|
"application/x-mpegurl",
|
|
"application/vnd.apple.mpegurl",
|
|
"application/dash+xml",
|
|
"application/vnd.ms-sstr+xml",
|
|
};
|
|
for (String type : types) {
|
|
try {
|
|
filter.addDataType(type);
|
|
} catch (IntentFilter.MalformedMimeTypeException ex) {
|
|
Log.e(TAG, "Error adding filter type " + type);
|
|
}
|
|
}
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
|
filter.addAction(MediaControlIntent.ACTION_PAUSE);
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
|
filter.addAction(MediaControlIntent.ACTION_RESUME);
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
|
filter.addAction(MediaControlIntent.ACTION_STOP);
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
|
filter.addAction(MediaControlIntent.ACTION_SEEK);
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
|
filter.addAction(MediaControlIntent.ACTION_GET_STATUS);
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
|
filter.addAction(MediaControlIntent.ACTION_START_SESSION);
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
|
filter.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
|
filter.addAction(MediaControlIntent.ACTION_END_SESSION);
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(CastMediaControlIntent.CATEGORY_CAST_REMOTE_PLAYBACK);
|
|
filter.addAction(CastMediaControlIntent.ACTION_SYNC_STATUS);
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
|
|
filter = new IntentFilter();
|
|
filter.addCategory(CastMediaControlIntent.CATEGORY_CAST_REMOTE_PLAYBACK);
|
|
filter.addAction(CastMediaControlIntent.ACTION_SYNC_STATUS);
|
|
BASE_CONTROL_FILTERS.add(filter);
|
|
}
|
|
|
|
@SuppressLint("NewApi")
|
|
public CastMediaRouteProvider(Context context) {
|
|
super(context);
|
|
|
|
if (android.os.Build.VERSION.SDK_INT < 16) {
|
|
Log.i(TAG, "Cast discovery disabled. Android SDK version 16 or higher required.");
|
|
return;
|
|
}
|
|
|
|
mNsdManager = (NsdManager)context.getSystemService(Context.NSD_SERVICE);
|
|
|
|
mDiscoveryListener = new NsdManager.DiscoveryListener() {
|
|
|
|
@Override
|
|
public void onDiscoveryStarted(String regType) {
|
|
CastMediaRouteProvider.this.state = State.DISCOVERING;
|
|
}
|
|
|
|
@Override
|
|
public void onServiceFound(NsdServiceInfo service) {
|
|
mNsdManager.resolveService(service, new NsdManager.ResolveListener() {
|
|
@Override
|
|
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
|
|
if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) {
|
|
return;
|
|
}
|
|
Log.e(TAG, "DiscoveryListener Resolve failed. Error code " + errorCode);
|
|
}
|
|
|
|
@Override
|
|
public void onServiceResolved(NsdServiceInfo serviceInfo) {
|
|
String name = serviceInfo.getServiceName();
|
|
InetAddress host = serviceInfo.getHost();
|
|
int port = serviceInfo.getPort();
|
|
Map<String, byte[]> attributes = serviceInfo.getAttributes();
|
|
if (attributes == null) {
|
|
Log.e(TAG, "Error getting service attributes from DNS-SD response");
|
|
return;
|
|
}
|
|
try {
|
|
String id = new String(attributes.get("id"), StandardCharsets.UTF_8);
|
|
String deviceVersion = new String(attributes.get("ve"), StandardCharsets.UTF_8);
|
|
String friendlyName = new String(attributes.get("fn"), StandardCharsets.UTF_8);
|
|
String modelName = new String(attributes.get("md"), StandardCharsets.UTF_8);
|
|
String iconPath = new String(attributes.get("ic"), StandardCharsets.UTF_8);
|
|
int status = Integer.parseInt(new String(attributes.get("st"), StandardCharsets.UTF_8));
|
|
|
|
onChromeCastDiscovered(id, name, host, port, deviceVersion, friendlyName, modelName, iconPath, status);
|
|
} catch (NullPointerException ex) {
|
|
Log.e(TAG, "Error getting cast details from DNS-SD response", ex);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void onServiceLost(NsdServiceInfo serviceInfo) {
|
|
String name = serviceInfo.getServiceName();
|
|
onChromeCastLost(name);
|
|
}
|
|
|
|
@Override
|
|
public void onDiscoveryStopped(String serviceType) {
|
|
CastMediaRouteProvider.this.state = State.NOT_DISCOVERING;
|
|
}
|
|
|
|
@Override
|
|
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
|
|
CastMediaRouteProvider.this.state = State.NOT_DISCOVERING;
|
|
}
|
|
|
|
@Override
|
|
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
|
|
CastMediaRouteProvider.this.state = State.DISCOVERING;
|
|
}
|
|
};
|
|
}
|
|
|
|
private void onChromeCastDiscovered(
|
|
String id, String name, InetAddress host, int port, String
|
|
deviceVersion, String friendlyName, String modelName, String
|
|
iconPath, int status) {
|
|
if (!this.castDevices.containsKey(id)) {
|
|
// TODO: Capabilities
|
|
int capabilities = CastDevice.CAPABILITY_VIDEO_OUT | CastDevice.CAPABILITY_AUDIO_OUT;
|
|
|
|
CastDevice castDevice = new CastDevice(id, name, host, port, deviceVersion, friendlyName, modelName, iconPath, status, capabilities);
|
|
this.castDevices.put(id, castDevice);
|
|
this.serviceCastIds.put(name, id);
|
|
}
|
|
|
|
publishRoutesInMainThread();
|
|
}
|
|
|
|
private void onChromeCastLost(String name) {
|
|
String id = this.serviceCastIds.remove(name);
|
|
if (id != null) {
|
|
this.castDevices.remove(id);
|
|
}
|
|
|
|
publishRoutesInMainThread();
|
|
}
|
|
|
|
@SuppressLint("NewApi")
|
|
@Override
|
|
public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
|
|
if (android.os.Build.VERSION.SDK_INT < 16) {
|
|
return;
|
|
}
|
|
|
|
if (request != null && request.isValid() && request.isActiveScan()) {
|
|
if (request.getSelector() != null) {
|
|
for (String category : request.getSelector().getControlCategories()) {
|
|
if (CastMediaControlIntent.isCategoryForCast(category)) {
|
|
this.customCategories.add(category);
|
|
}
|
|
}
|
|
}
|
|
if (this.state == State.NOT_DISCOVERING) {
|
|
mNsdManager.discoverServices("_googlecast._tcp.", NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
|
|
this.state = State.DISCOVERY_REQUESTED;
|
|
}
|
|
} else {
|
|
if (this.state == State.DISCOVERING) {
|
|
mNsdManager.stopServiceDiscovery(mDiscoveryListener);
|
|
this.state = State.DISCOVERY_STOP_REQUESTED;
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public RouteController onCreateRouteController(String routeId) {
|
|
CastDevice castDevice = this.castDevices.get(routeId);
|
|
if (castDevice == null) {
|
|
return null;
|
|
}
|
|
return new CastMediaRouteController(this, routeId, castDevice.getAddress());
|
|
}
|
|
|
|
private void publishRoutesInMainThread() {
|
|
Handler mainHandler = new Handler(this.getContext().getMainLooper());
|
|
mainHandler.post(this::publishRoutes);
|
|
}
|
|
|
|
private void publishRoutes() {
|
|
MediaRouteProviderDescriptor.Builder builder = new MediaRouteProviderDescriptor.Builder();
|
|
for (CastDevice castDevice : this.castDevices.values()) {
|
|
ArrayList<IntentFilter> controlFilters = new ArrayList<>(BASE_CONTROL_FILTERS);
|
|
// Include any app-specific control filters that have been requested.
|
|
// TODO: Do we need to check with the device?
|
|
for (String category : this.customCategories) {
|
|
IntentFilter filter = new IntentFilter();
|
|
filter.addCategory(category);
|
|
controlFilters.add(filter);
|
|
}
|
|
|
|
Bundle extras = new Bundle();
|
|
castDevice.putInBundle(extras);
|
|
MediaRouteDescriptor route = new MediaRouteDescriptor.Builder(
|
|
castDevice.getDeviceId(),
|
|
castDevice.getFriendlyName())
|
|
.setDescription(castDevice.getModelName())
|
|
.addControlFilters(controlFilters)
|
|
.setDeviceType(MediaRouter.RouteInfo.DEVICE_TYPE_TV)
|
|
.setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
|
|
.setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED)
|
|
.setVolumeMax(20)
|
|
.setVolume(0)
|
|
.setEnabled(true)
|
|
.setExtras(extras)
|
|
.setConnectionState(MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED)
|
|
.build();
|
|
builder.addRoute(route);
|
|
}
|
|
this.setDescriptor(builder.build());
|
|
}
|
|
}
|