前段时间有做MTP协议扩展的相关的内容,在这里总结一下。
(注意协议方面有很多细节一篇简短的文章是不可能面面俱到,这里只是学习总结,本人接触协议的时间也不是很长,难免有纰漏,有错误之处请不吝指教)。
Media Transfer Protocol即媒体传输协议,主要是用来管理移动设备上的图片、视频、音频等媒体信息,典型的有Android设备,相机设备。
MTP协议是应用层协议,底层协议可以走USB或TCP/IP协议,只要能够无差错传输即可。
MTP协议框架上面定义了很多多媒体相关的命令,例如获取设备信息,获取对象信息,本文着重介绍MTP协议框架,然后举例特定的命令帮助理解。
MTP协议有两个角色,类似于客户端与服务端,在MTP协议里面有特定的称号,发起请求的叫Initiator,对请求进行响应的叫Responder。Initiator通常对应于PC/MAC宿主机,Responder对应于被管理的设备,例如Android手机。任何操作都需要Inititaor发起,然后Reponder进行响应。
Initiator对Reponder的大多数请求都是需要打开一个Session会话,类似于HTTP里面的Session,用于保存上下文相关的环境信息,例如,在MTP传输媒体文件过程中,Inititor传输媒体文件到Reponder是需要发送两次请求才能完成的,第一次请求发送SendObjectInfo的消息,告诉Reponder即将要发送的媒体信息,包括大小,格式,媒体文件名称等;第二次请求发送SendObject传输实际的文件,在这两次请求中第二次请求需要使用第一次请求保存的相关信息,所以就需要保持在一个Session会话里面。MTP按理论上说是可以支持多Session会话的。
还有另一个关键的概念,就是Transaction,对应于Initiator发起请求,然后数据传输,Responder响应一次完整的过程,有点类似于数据库里面的事务,比如Initiator发起一次请求,在Reponder没有响应之前,是不能进行另一次请求。所以在USB单Session实现中,Initiator是不能同时发送多次请求的。
前面介绍到Transaction对应于Initiator发起请求,然后数据传输,Responder响应。所以对于的请求,数据,响应传输分为三个阶段:
其中Data Phase是可选的,并且是单向的。
单向的定义就规定了Data Phase的数据流要么是Initiator到Reponder(以下简称I->R),要么是Reponder到Initiator(以下简称R->I)。
MTP协议也规定Data Phase是可选的,就是意味着Initiator发送完成请求后,Reponder就直接响应,不需要传输数据,因为有的MTP消息不需要传输数据,Request与Repond本身就可以传递少量的参数。
下图就是传输的三种情况:
数据流的字段是有Request的字段OperationCode决定的,根据不同的功能决定Data阶段的数据流向。比如Initiator读取媒体信息GetObjectInfo的数据流向就是R->I;Initiator发送媒体文件信息SenObjectInfo的数据流向就是I->R; 读取设备上媒体文件的个数GetObjectNum由于Reponse中携带的参数已经能够满足表示数量,所以就不需要Data Phase。
下面来说一下Request与Reponse的Dataset,用来表示能够携带哪些参数。Request与Response的Dataset是一样的。需要特别注意的是,不同的底层协议对于Dataset的存放方式是不同的,MTP SEPC只给出的是USB的实现方式。
最关键的是操作码OperationCode定义Request请求要进行什么样的操作,MTP Responder 该处理什么样的任务,然后根据功能决定Data的数据流向。
对于USB来说是单Session的实现方式,在其实现的数据集是不包含sessionID这个字段的,但是在发送大多数Request之前,也还是需要发送OpenSession这个Request请求。
TransactionID由Initiator指定,在一次完整的Request到Response都要指定相同的值,不需要每一次都相同。
Request与Response可以携带0到5个参数,根据OperationCode的功能来决定。
还有一种比较特殊的消息就是Reponder可以直接发送Event给Initiator,用来通知Initiator,Reponder出现了一些状况或者发生了一些变化,可以与Transaction关联,也可以不可Transaction关联,根据Event的事件来定。比如设备上新增了一条媒体文件的信息,就需要通过Event事件来通知Initiator来更新。
Event是不能传递二进制数据的,只能携带0到3个参数,其Dataset为:
常用功能的Request与Reponse的OperationCode,Event的EventCode,在MTP Spec规格文档里面有定义,不同的Code对应什么样含义以及携带什么样的参数,还给出了要厂商可以扩展的Code范围。
之前看Android源码的时候就有点懵,spec上面的定义的Dataset与源码里面的对不上,后来看到sepc文档最后的这个表格才知道不同的实现方式数据的存放是不一样的,下面这个就是USB定义的MTP数据包,Request,Data,Response都要携带定义的头部信息,Initator与Responder都要读取USB数据包来解析MTP数据包。
Request与Response的Payload就是携带的那0到5个参数,Data的Payload就是二进制数据,可能是媒体文件,也有可能是自定义的数据格式。
StorageID:对应一个设备上的存储分区,表现形式就是无符号32位的整数uint32, 高16位表示存储设备,低16位表示对应存储设备的分区。例如Android设备,有内部存储与外置SD卡,SD卡可能有多个分区,就对应不同的StorageID。
ObjectHandle:实际就是一个int32的对象id,对应设备上的一个个媒体文件对象,可能是文件夹或者是媒体文件,MTP读取文件或者发送文件都需要这个id,其通常包含一个父Object,类似于文件系统的目录树,根路径的值比较特殊,0xFFFFFFFF
ObjectFormat:媒体类型
….其余的不难理解,需要时查询文档
Android源码的处理过程:
处理MTP请求的应用就是提供媒体数据库的MediaProvider,对应的包名为com.android.providers.media,对于源码树的位置packages/providers/MediaProvider。
其本身是一个系统应用,在AndroidManifest里面监听USB状态变化的广播:
<receiver android:name=".MtpReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
<intent-filter>
<action android:name="android.hardware.usb.action.USB_STATE" />
</intent-filter>
</receiver>
当连上USB数据线,就会将MtpService.java启动,然后加载动态库,开启一个线程,jni调用MTPServer.cpp的run方法,不断地从mtp驱动读消息,处理,响应。而MTP的Object对应的就是文件数据里面的一个个文件。
MtpServer.cpp的run方法:
void MtpServer::run() {
int fd = mFD;//打开的mtp驱动文件描述符
while (1) {
int ret = mRequest.read(fd);//读取Request请求,放到mRequest封装的类里面
//...
MtpOperationCode operation = mRequest.getOperationCode();
MtpTransactionID transaction = mRequest.getTransactionID();
// FIXME need to generalize this
bool dataIn = (operation == MTP_OPERATION_SEND_OBJECT_INFO
|| operation == MTP_OPERATION_SET_OBJECT_REFERENCES
|| operation == MTP_OPERATION_SET_OBJECT_PROP_VALUE
|| operation == MTP_OPERATION_SET_DEVICE_PROP_VALUE);//根据OperationCode来处理Data Phase的数据流向
if (dataIn) {
int ret = mData.read(fd);
//...
} else {
mData.reset();
}
if (handleRequest()) {//handleRequest根据不同的OperationCode功能处理不同的任务
if (!dataIn && mData.hasData()) {
mData.setOperationCode(operation);
mData.setTransactionID(transaction);
ALOGV("sending data:");
ret = mData.write(fd);
}
mResponse.setTransactionID(transaction);
ret = mResponse.write(fd);//响应
const int savedErrno = errno;
} else {
ALOGV("skipping response\n");
}
}
//....
}
对照MTP SEPC文档的附录接口看代码比较简单,就是读取解析结构化数据或者封装需要发送的结构化数据,但是有一点需要特别注意,
处理发送文件与读取文件的操作不是在用户空间完成的,而是通过IOCTL,调用驱动接口在内核空间完成,这样会比较高效,不用再用户空间拷贝一份处理给内核空间,而是有内核空间直接发送。