一 、前言
最近做了一个浏览器&视频播放的项目,是在73.0.3683.90版本的chrome源码上修改而来,涉及到抓取网页里视频的播放地址、播放视频、视频投屏、视频下载、网页内广告屏蔽等方面,了解到ijkplayer、GSYVideoPlayer、ffmpeg、乐播投屏、cling、NanoHttp、adblock等相关技术,现在就准备花点时间把一些技术相关的内容整理一下,分享给大家。
为什么先写的是投屏相关的技术呢?刚开始投屏用的乐播的sdk,乐播的效果肯定是很好的,支持的协议更多,更稳定,但是乐播有一个限制,个人开发者不能获取到APPID和SDK资源,最开始是帮别人做的项目,他们提供了相关的资源,所以就没有去研究过投屏的其他方案。但是后来又有了个新项目,新项目也有一个需求是投屏,但是他们没法提供相关的APPID和SDK,所以我就只能找新的方案,它就是cling。
android相关的投屏方案封装不止cling一个,只是恰巧看到了,并且有人说cling算是封装的比较好的了,所以就直接选择了cling开始做。截止目前,我做的这个项目基本上能正常的投屏图片、音频、视频等资源了,至于控制功能暂时还未尝试,但是相关的方法是有的,只是没有尝试调用。因为需求不同,所以目前我只研究了发送端的功能,至于接收端,我给的参考链接的最后两个链接里是有代码可以参考的。
本来说到投屏技术,一般都会讲到DLNA、AirPlay、UPNP协议等相关基础,但是这方面的介绍文献实在是多如牛毛,我就不在这里浪费时间去复制粘贴别人的劳动成果了,我给出几个当时我找资料时参考的几篇文章,供大家参考:
本着大家都是着重于“取而用之”的实际需求,这里先附上本次项目的源码
二 、实现的过程
我这个人呢,有个特别不好的习惯,不是十分喜欢直接抄袭别人的东西,又喜欢重复造轮子,但是呢,能力又有限,所以写出来的东西会和参考的东西有所区别,但是不一定比别人的好,请大家不要见怪。但这次重复造轮子的原因,主要是因为那个demo里的代码我没办法直接用,以及要解决cling2.2.0版本在9.0系统上出现无法解析描述文件的问题。
整个工程的目录结构如下图所示
[外链图片转存失败(img-znXFPZXt-1563761574281)(https://raw.githubusercontent.com/ykbjson/ykbjson.github.io/master/blogimage/simpledlna/simpledlna_code_structure.png)]
2.1源码浅析前的说明
webserver这个module就是基于NanoHttp实现的本地http服务器的代码。
simplepermission整个module是一个权限请求的库,因为整个工程基于androidx,没花时间去找适配androidx的权限库,就自己改吧改吧了一下原来用的一个权限库来用,因为要实现投屏,必须要一些权限,参见screening module的manifest文件。
sereening module是整个项目的核心,有三个地方要先提出来说清楚,一个是log包下的AndroidLoggingHandler,这个类是为了解决cling包里的logger不输出日志的问题,具体的请看
另一个是xml包下的几个类,主要是重写了cling里解析设备交互报文的SAX解析器,cling原来的代码,在生成解析器的时候抛了异常,导致设备交互的报文无法被解析,后续流程就中断了,以至于无法发现可以投屏的设备。说到这里,不得不说,大神们写的代码,设计的真的非常强大,扩展性考虑的很好,我本以为只能clone cling的源码下来自己改,没想到这个解析器可以自定义,为作者手动点赞!
最后一个地方呢,就是DLNABrowserService,里面只是重载了AndroidUpnpServiceImpl的一个方法,返回DLNAUDA10ServiceDescriptorBinderSAXImpl,以便于替换cling自带的无法在android9.0上面正常工作的UDA10ServiceDescriptorBinderSAXImpl。所以,在使用这个库的时候,在app module的manifest里声明的就不是AndroidUpnpServiceImpl而是DLNABrowserService,这一点要注意。
至于bean包下的两个类,DeviceInfo是对支持投屏的设备——Device 的一个封装;MediaInfo是为了方便传递要投屏的多媒体信息做的封装。
2.2部分源码浅析
接下来我们从listener包开始讲解整个项目的源码,里面有四个回调接口,其实我感觉有些是多余的,但是呢,因为一些操作是异步的,感觉有一个回调接口能更好的控制使用这个库的逻辑,避免出现一些错误。
###初始化DLNAManager回调接口——DLNAStateCallback
public interface DLNAStateCallback {
void onConnected();
void onDisconnected();
}
这个其实应该叫DLNAManagerInitCallback,初始化DLNAManager的时候传递的,可以为null,只要你能保证你后续代码时在DLNAManager初始化之后调用的。
###注册设备列表和状态回调接口——DLNARegistryListener
public abstract class DLNARegistryListener implements RegistryListener {
private final DeviceType DMR_DEVICE_TYPE = new UDADeviceType("MediaRenderer");
public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) {
}
public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex) {
}
/**
* Calls the {@link #onDeviceChanged(List)} method.
*
* @param registry The Cling registry of all devices and services know to the local UPnP stack.
* @param device A validated and hydrated device metadata graph, with complete service metadata.
*/
public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
onDeviceChanged(build(registry.getDevices()));
onDeviceAdded(registry, device);
}
public void remoteDeviceUpdated(Registry registry, RemoteDevice device) {
}
/**
* Calls the {@link #onDeviceChanged(List)} method.
*
* @param registry The Cling registry of all devices and services know to the local UPnP stack.
* @param device A validated and hydrated device metadata graph, with complete service metadata.
*/
public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
onDeviceChanged(build(registry.getDevices()));
onDeviceRemoved(registry, device);
}
/**
* Calls the {@link #onDeviceChanged(List)} method.
*
* @param registry The Cling registry of all devices and services know to the local UPnP stack.
* @param device The local device added to the {@link org.fourthline.cling.registry.Registry}.
*/
public void localDeviceAdded(Registry registry, LocalDevice device) {
onDeviceChanged(build(registry.getDevices()));
onDeviceAdded(registry, device);
}
/**
* Calls the {@link #onDeviceChanged(List)} method.
*
* @param registry The Cling registry of all devices and services know to the local UPnP stack.
* @param device The local device removed from the {@link org.fourthline.cling.registry.Registry}.
*/
public void localDeviceRemoved(Registry registry, LocalDevice device) {
onDeviceChanged(build(registry.getDevices()));
onDeviceRemoved(registry, device);
}
public void beforeShutdown(Registry registry) {
}
public void afterShutdown() {
}
public void onDeviceChanged(Collection<Device> deviceInfoList) {
onDeviceChanged(build(deviceInfoList));
}
public abstract void onDeviceChanged(List<DeviceInfo> deviceInfoList);
public void onDeviceAdded(Registry registry, Device device) {
}
public void onDeviceRemoved(Registry registry, Device device) {
}
private List<DeviceInfo> build(Collection<Device> deviceList) {
final List<DeviceInfo> deviceInfoList = new ArrayList<>();
for (Device device : deviceList) {
//过滤不支持投屏渲染的设备
if (null == device.findDevices(DMR_DEVICE_TYPE)) {
continue;
}
final DeviceInfo deviceInfo = new DeviceInfo(device, getDeviceName(device));
deviceInfoList.add(deviceInfo);
}
return deviceInfoList;
}
private String getDeviceName(Device device) {
String name = "";
if (device.getDetails() != null && device.getDetails().getFriendlyName() != null) {
name = device.getDetails().getFriendlyName();
} else {
name = device.getDisplayString();
}
return name;
}
}
这个类只是对RegistryListener的封装,因为我当时想着这个类主要是回调当前发现的设备的列表信息,所以就简单封装了一下,每次设备数量改变的时候就把新的设备数量通过一个回调方法传递出去,忽略一些不关注的方法。
###连接设备回调接口——DLNADeviceConnectListener
public interface DLNADeviceConnectListener {
int TYPE_DLNA = 1;
int TYPE_IM = 2;
int TYPE_NEW_LELINK = 3;
int CONNECT_INFO_CONNECT_SUCCESS = 100000;
int CONNECT_INFO_CONNECT_FAILURE = 100001;
int CONNECT_INFO_DISCONNECT = 212000;
int CONNECT_INFO_DISCONNECT_SUCCESS = 212001;
int CONNECT_ERROR_FAILED = 212010;
int CONNECT_ERROR_IO = 212011;
int CONNECT_ERROR_IM_WAITTING = 212012;
int CONNECT_ERROR_IM_REJECT = 212013;
int CONNECT_ERROR_IM_TIMEOUT = 212014;
int CONNECT_ERROR_IM_BLACKLIST = 212015;
void onConnect(DeviceInfo deviceInfo, int errorCode);
void onDisconnect(DeviceInfo deviceInfo,int type,int errorCode);
}
这个类是给DLNAPlayer连接设备时用的。说到这个所谓的连接设备,其实感觉也不需要这个步骤,cling本身可能已经做好了设备之间的连接,回调回来的设备列表里的设备都是连接过了的,直接可以通信。但是我发现乐播的sdk里就有一个连接设备的方法,必须先调用连接设备的这个方法,在回调里才能继续后续操作,所以我这里也设计了一个连接设备的步骤,我怕万一是cling有专门连接设备的接口,只是我还没发现而已,后面发现了就来改写这个连接设备的方法。
###控制设备回调接口——DLNAControlCallback
public interface DLNAControlCallback {
int ERROR_CODE_NO_ERROR = 0;
int ERROR_CODE_RE_PLAY = 1;
int ERROR_CODE_RE_PAUSE = 2;
int ERROR_CODE_RE_STOP = 3;
int ERROR_CODE_DLNA_ERROR = 4;
int ERROR_CODE_SERVICE_ERROR = 5;
int ERROR_CODE_NOT_READY = 6;
void onSuccess(@Nullable ActionInvocation invocation);
void onReceived(@Nullable ActionInvocation invocation,@Nullable Object ... extra);
void onFailure(@Nullable ActionInvocation invocation,
@IntRange(from = ERROR_CODE_NO_ERROR, to = ERROR_CODE_NOT_READY) int errorCode,
@Nullable String errorMsg);
}
顾名思义,这个类就是发送端在控制接收端做出一系列动作时的回调接口,包括播放、暂停、结束、静音开闭、音量调整、播放进度获取等等。播放、暂停、结束、静音开闭、音量调整等方法只会回调onSuccess和onFailure方法;获取播放进度这种需要获取结果的方法会在onReceived方法里返回结果。
看