android投屏技术的基本原理就是根据DLNA以及UPNP来实现,另外还有些黑科技技术便是根据端口号或者通过广播来用adb下载本身相关的apk来间接实现投屏,当然此处不提及黑科技。原理什么的百度一堆。这里主要讲实现方式和具体实现的代码。
那么,开始开发这玩意的时候肯定要先看看有没有现成的轮子,git上是有轮子的,链接如下:
https://github.com/4thline/cling 最基础的包,main类方法中有基础的调用方式
https://github.com/offbye/DroidDLNA 在cling上改动,代码表现为当前设备的媒资资源DLNA投屏,没涉及到相关的网络媒资投屏
DroidDLNA 中是有bug的,具体表现在api版本为25以后,是不能投屏的,原因出自AndroidNetworkAddressFactory类中的反射问题,api版本25以后,结果与之前完全不一致,改法已经在cling中完善,代码改动如下:
@Override
protected boolean isUsableAddress(NetworkInterface networkInterface, InetAddress address) {
boolean result = super.isUsableAddress(networkInterface, address);
if (result) {
// TODO: Workaround Android DNS reverse lookup issue, still a problem on ICS+?
// http://4thline.org/projects/mailinglists.html#nabble-td3011461
String hostName = address.getHostAddress();
Log.e("gjh test Log", "sdk version=" + Build.VERSION.SDK_INT + "->hostName=" + hostName);
try {
Field field0 = null;
Object target = null;
try {
field0 = InetAddress.class.getDeclaredField("holder");
field0.setAccessible(true);
target = field0.get(address);
field0 = target.getClass().getDeclaredField("hostName");
} catch( NoSuchFieldException e ) {
// Let's try the non-OpenJDK variant
field0 = InetAddress.class.getDeclaredField("hostName");
target = address;
}
if (field0 != null && hostName != null) {
field0.setAccessible(true);
field0.set(target, hostName);
} else {
return false;
}
} catch (Exception ex) {
log.log(Level.SEVERE,
"Failed injecting hostName to work around Android InetAddress DNS bug: " + address,
ex
);
return false;
}
}
return result;
}
DLNA轮子中投屏的方法为广播,代码表现为:
private void jumpToControl(ContentItem localContentItem) {
Intent localIntent = new Intent("com.transport.info");
localIntent.putExtra("name", localContentItem.toString());
localIntent.putExtra("playURI", localContentItem.getItem()
.getFirstResource().getValue());
localIntent.putExtra("currentContentFormatMimeType",
currentContentFormatMimeType);
try {
localIntent.putExtra("metaData",
new GenerateXml().generate(localContentItem));
} catch (Exception e) {
e.printStackTrace();
}
IndexActivity.mTabHost.setCurrentTabByTag(getString(R.string.control));
IndexActivity.setSelect();
this.sendBroadcast(localIntent);
}
当然如果需求为投射本地已经存在的资源的话只要稍微改动下界面和bug即可。
下面说说如何投射网络在线视频。
从上面的DLNA播放本地资源已经大致可以猜到只要修改其中的name、playURI和metaData即可。
但实际上我们只要改动metaData,因为最终解析是根据MetaData来拿到相关的媒资数据,其他大致上没什么用处
具体实现的代码如下:
private AndroidUpnpService upnpService;//DLNA投屏服务
private DeviceListRegistryListener deviceListRegistryListener;//搜索设备的回调
mContext.bindService(
new Intent(mContext, AndroidUpnpServiceImpl.class),
serviceConnection, Context.BIND_AUTO_CREATE);
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
if (mContext != null) {
upnpService = (AndroidUpnpService) service;//本地设备服务,用于执行投屏命令
upnpService.getRegistry().addListener(deviceListRegistryListener);//搜索设备的回调
ThreadManager.execute(new Runnable() {
@Override
public void run() {
if (mContext != null && upnpService != null) {
upnpService.getControlPoint().search();//搜索相关设备
}
}
});
}
}
public void onServiceDisconnected(ComponentName className) {
upnpService = null;
}
};
public class DeviceListRegistryListener extends DefaultRegistryListener {
@Override
public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
if (device.getType().getNamespace().equals("schemas-upnp-org")
&& device.getType().getType().equals("MediaRenderer")) {
final DeviceItem dmrDisplay = new DeviceItem(device, device
.getDetails().getFriendlyName(),
device.getDisplayString(), "(REMOTE) "
+ device.getType().getDisplayString());
dmrRemoved(dmrDisplay);
}
}
@Override
public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
if (device.getType().getNamespace().equals("schemas-upnp-org")
&& device.getType().getType().equals("MediaRenderer")) {
final DeviceItem dmrDisplay = new DeviceItem(device, device
.getDetails().getFriendlyName(),
device.getDisplayString(), "(REMOTE) "
+ device.getType().getDisplayString());
dmrAdded(dmrDisplay);
}
}
public void dmrAdded(final DeviceItem di) {
if (mTvDataList == null) {
mTvDataList = new ArrayList<>();
}
mTvDataList.add(di);
}
public void dmrRemoved(final DeviceItem di) {
if (mTvDataList != null && mTvDataList.contains(di)) {
mTvDataList.remove(di);
}
}
}
private void playToTv(DeviceItem deviceItem) {
String url = "网络视频链接";
Service avtService = deviceItem.getDevice()
.findService(new UDAServiceType("AVTransport"));
String metaData = TvUtil.pushMediaToRender(url, "video-item", "DLNA测试视频", "50:00", "ggg");
upnpService.getControlPoint().execute(new SetAVTransportURI(avtService, url, metaData) {
@Override
public void success(ActionInvocation invocation) {
super.success(invocation);
LogUtil.e("playToTv", "-------success");
}
@Override
public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
LogUtil.e("playToTv", "-------failure defaultMsg=" + defaultMsg);
}
});
}
当然上面的网络视频链接最好以最终的播放地址,不然会出现无法播放的情况.
具体代码 https://download.csdn.net/download/gan303/10392625