DIAL Server在Android上的移植

无线显示标准

HDMI使我们连接TV或者扩展屏,但是是有线连接。
无线连接包括以下几种技术:

  • AirPlay是苹果的无线连接技术。可以通过WiFi将iPhone 、iPad、iPod touch 等iOS 设备上的包括图片、音频、视频通过无线的方式传输到支持AirPlay 设备,还可以实现镜像及扩展。
  • DLNA,Digital Living Network Alliance,是索尼、英特尔、微软等发起的一套 PC、移动设备、消费电器之间互联互通的协议。DLNA 并没有Apple TV的AirPlay 的镜像功能,也没有Apple TV 所支持的双屏体游戏体验。目前DLNA更多只是能将手机的照片和视频投送到大屏幕中。DLNA是基于UPnP的,严格上讲不算是wireless display,从一个设备的本地存储中拿内容到另外一个设备上去display。
  • Miracast是由Wi-Fi联盟制定,以Wi-Fi直连为基础的无线显示标准,支持投送和镜像功能和AirPlay差不多。安卓4.2和win8支持。但目前各家设备对miracast支持的都不太好,延迟大,兼容性不好。作为一个标准,miracast似乎没有对厂家有统一的”miracast”mark授权。大部分厂家各干各,比如LG叫smartshare,三星叫allshare cast,索尼叫screen mirroring,松下叫display mirroring。
  • WiDi是intel的wireless display技术,基于wifi direct。从WiDi 3.5开始兼容miracast。
  • DIAL是美国网络视频巨头YouTube及Netflix联合开发一种新协议,以对抗苹果的AirPlay。
  • chromecast是一个插在HDMI口上的接收器。基于DIAL协议,运行简化的chrome操作系统,通过终端设备为chromecast设备建立链接,由chromecast设备自行去network上down内容来显示。这是一个搜索,操作与下载显示相互分离的过程,分别在两个设备上进行。chromecast也支持将终端设备内容stream推送到显示设备的功能。

UPnP

参考:UPnP官网,UPnP基本原理

DIAL协议

参考:DIAL官方文档 DIAL-2ndScreenProtocol-2.0.1.pdf

DIAL Server

源码说明

mongoose

开源且小巧的 web服务器类,实现了对socket的封装。其采用了一个自适应的线程池的模型。参考

quick_ssdp(对应DIAL Service Discovery)

  • 在handle_mcast()函数中进行socket server bind并监听。
    监听DIAL Service Discovery中的M-SEARCH多播(组播)请求,判断请求中是否有“urn:dial-multiscreen-org:service:dial:1”,有则返回M-SEARCH响应,Log中打印“Sending SSDP reply to 设备ip:端口”。其中服务器端口号,程序中默认是567890。
  • 在request_handler()函数中进行设备描述响应。
    判断为/dd.xml的get请求,返回设备描述信息。

dial_server(对应DIAL REST Service)

  • 在handle_app_status()函数中处理查询应用响应。//GET请求
    查找app应用是否存在,并返回状态信息(running or stopped)。
  • 在handle_app_start ()函数中处理启动应用响应。//POST请求
    首先查找app,若存在执行app启动的回调函数,app启动后更新状态信息。回调函数在main.c中实现,这也是源码中需修改的函数。
  • 在handle_app_stop()函数中处理停止应用响应。//DELETE请求
    同理start app流程。
  • 在request_handler()函数中处理客户端的查询、启动、停止应用请求。
    判断接受到的请求(GET、POST、DELETE),选择响应的处理函数(handle_app_status、handle_app_start、handle_app_stop)。

源码修改说明

1
2
static char *spAppYouTubeMatch = "chrome.*google-chrome-dial";
static char *spAppYouTubeExecutable = "/opt/google/chrome/google-chrome";

spAppYouTubeMatch为YouTube应用进程名,用于在isAPPRunning()中检测进程名为com.android.chrome的应用是否运行
spAppYouTubeExecutable为YouTube应用启动执行路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
static int isAppRunning( char *pzName, char *pzCommandPattern ) {
//通过/proc 文件系统,在运行时访问内核内部数据结构、改变内核设置的机制
DIR* proc_fd = opendir("/proc");
if( proc_fd != NULL ) {
struct dirent* procEntry;//为了获取某文件夹目录内容,所使用的结构体
while((procEntry=readdir(proc_fd)) != NULL) {
//* 表示匹配0个或多个前面这个字符,^ 匹配一行的开头,$匹配一行的结束
if( doesMatch( "^[0-9][0-9]*$", procEntry->d_name ) ) {
//proc中包含许多以数字命名的子目录,这些数字表示系统当前正在运行进程的进程号,里面包含对应进程相关的多个信息文件。
char exePath[64] = {0,};//指向启动当前进程的可执行文件(完整路径)的符号链接
char link[256] = {0,};
char cmdlinePath[64] = {0,};//启动当前进程的完整命令
char buffer[1024] = {0,};
int len;
sprintf( exePath, "/proc/%s/exe", procEntry->d_name);
sprintf( cmdlinePath, "/proc/%s/cmdline", procEntry->d_name);
printf("exePath:%s\n", exePath);
printf("cmdlinePatb:%s\n",cmdlinePath);
if( (len = readlink( exePath, link, sizeof(link)-1)) != -1 ) {//exePath内容拷贝到link中
char executable[256] = {0,};
strcat( executable, pzName );//将两个char类型连接结果放在executable
strcat( executable, "$" );
// TODO: Make this search for EOL to prevent false positivies
printf("executable:%s\nlink:%s\n",executable,link);
if( !doesMatch( executable, link ) ) {
continue;
}
// else //fall through, we found it
}
else continue;
if (pzCommandPattern != NULL) {
FILE *cmdline = fopen(cmdlinePath, "r");
if (!cmdline) {
continue;
}
if (fgets(buffer, 1024, cmdline) == NULL) {
fclose(cmdline);
continue;
}
fclose(cmdline);
if (!doesMatch( pzCommandPattern, buffer )) {
continue;
}
}
closedir(proc_fd);
return atoi(procEntry->d_name);//进程号字符串转换为整形
}
}
closedir(proc_fd);
} else {
printf("/proc failed to open\n");
}
return 0;
}

此函数用来判断当前是否有YouTube应用运行。
Linux 内核提供了一种通过 /proc 文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。/proc包括一些以数字命名的目录,它们是进程目录。即系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的 PID号为目录名,它们是读取进程信息的接口。

以下是/proc目录中进程N的信息

目录 描述
/proc/N pid N的进程信息
/proc/N/cmdline 进程启动命令
/proc/N/cwd 链接到进程当前工作目录
/proc/N/environ 进程环境变量列表
/proc/N/exe 链接到进程的执行命令文件
/proc/N/fd 包含进程相关的所有的文件描述符
/proc/N/maps 与进程相关的内存映射信息
/proc/N/mem 指代进程持有的内存,不可读
/proc/N/root 链接到进程的根目录
/proc/N/stat 进程的状态
/proc/N/statm 进程使用的内存的状态
/proc/N/status 进程状态信息,比stat/statm更具可读性
/proc/self 链接到当前正在运行的进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static DIALStatus youtube_start(DIALServer *ds, const char *appname,
const char *payload, const char *additionalDataUrl,
DIAL_run_t *run_id, void *callback_data) {
printf("\n\n ** LAUNCH YouTube ** with payload %s\n\n", payload);
char url[512] = {0,}, data[512] = {0,};
if (strlen(payload) && strlen(additionalDataUrl)){
sprintf( url, "https://www.youtube.com/tv?%s&%s", payload, additionalDataUrl);
}else if (strlen(payload)){
sprintf( url, "https://www.youtube.com/tv?%s", payload);
}else{
sprintf( url, "https://www.youtube.com/tv");
}
sprintf( data, "--user-data-dir=%s/.config/google-chrome-dial", getenv("HOME") );
const char * const youtube_args[] = { spAppYouTubeExecutable,
spYouTubePS3UserAgent,
data, "--app", url, NULL
};
runApplication( youtube_args, run_id );
return kDIALStatusRunning;
}

此函数更改为Android平台启动应用的方式,即

1
am start -n com.android.chrome/com.google.android.apps.chrome.Main -d https://www.youtube.com/tv?pairingCode=eac4ae42-8b54-4441-9be3-d8a9abb5c481&v=cKG5HDyTW8o&t=0&additionalDataUrl=http%3A%2F%2Flocalhost%3A56789%2Fapps%2FYouTube%2Fdial_data%3F

待解决问题

DIAL Server目前只能在chrome中打开推送视频并控制,因为没有找到Native App支持的API,没有实现启动Native App(如YouTube、Netflix)。
YouTube APP现有api为https://developers.google.com/youtube/android/player/ ,只处理三种格式的视频URL请求

Netflix在2014年关闭了公有api https://gigaom.com/2014/11/14/netflix-is-shutting-down-its-public-api-today/ ,示例中代码为启动linux客户端传递命令行参数的形式。