PPAPI插件开发指南
前⾔
插件⼀直是浏览器的重要组成部分,丰富浏览器的运⾏能⼒,实现⼀些HTML+JS实现不了本地应⽤(⽐如⾳视频、⽂件操作等)。早期⼴为熟知的是IE下的插件ActiveX,这是⼀项熟悉可能暴露年龄的技术,它基于COM规范,在IE占浏览器市场主流份额的时代,ActiveX可谓出尽了风头,但它并不是浏览器业内的插件标准。由⽹景发布的NPAPI长久以来是除了IE之外的其他浏览器共同⽀持的业内标准。ActiveX和NPAPI长久共存,如果某项业务要发布浏览器插件,⼀般都需要实现这两种插件规范。有个开源的插件开发框架FireBreath 抽象了这两种插件的接⼝细节,让开发者专注于业务逻辑(个⼈⽐较嫌弃FireBreath 开发出来的插件体积太⼤)。早期的银⾏开发⽹页安全控件都是使⽤ActiveX开发,所以他们都要求在IE浏览器或者其他双核浏览器的兼容模式下登录。现在很多银⾏已经⽀持NPAPI 版本的安全控件,但NPAPI却做为不安全因素被主流浏览器(Chrome、FireFox)抛弃,好在国内的双核浏览器都保留有NPAPI功能的移植,让市⾯上⼤量的NPAPI插件得以正常运⾏。现在,随着IE逐渐退出历史舞台,ActiveX插件的运⾏范围越来越窄,NPAPI也成为过时的插件标准,虽然HTML5标准也在逐渐完善,很多之前只靠Web技术做不到的功能都能得到很好的⽀持,但是并不是所有的功能都能通过HTML5实现,本地插件依然是我们这个时代绕不开的功能应⽤,最典型的⽐如Flash。于是Chrome提出了名叫PPAPI的全新插件机制,运⾏在Chrome浏览器的沙箱环境。
插件类型Chrome基于Chromium核的浏览器IE
ActiveX No No Yes
NPAPI No Yes(暂时兼容)No
PPAPI Yes Yes No
从对⽐可以看出,PPAPI的运⾏场景完全覆盖NPAPI,以后开发插件完全可以只开发PPAPI+ActiveX两个版本。但是⽬前市⾯上,除了内置的PPAPI版本的Flash组件以及Pdf组件,没遇到其他的PPAPI版本的商业组件,可见再造⼀个平台是多么地不容易。
PPAPI组件介绍
在Chrome中,PPAPI组件分为两种存在形式:
可信组件:可信的PPAPI组件可以通过平台动态库的形式(Windows下为dll⽂件,Linux下是so⽂件)由浏览器直接加载,⽐如内置的Flash组件、Pdf组件,或者通过指定命令⾏参数--register-pepper-plugins来加载,⽐如:chrome --register-pepper-
plugins="D:\\ppapi\\ppapi_example_gles2.dll;application/x-ppapi-example-gles2" D:\\ppapi\\gles2.html,这种⽅案常⽤于调试。可信的PPAPI组件以平台动态库的形式存在,所以⼀般Chrome沙箱内允许的API(⽐如CreateThread)都可以调⽤。
不可信组件:不可信的PPAPI组件需要使⽤Native Client机制发布(以Native Client的形式发布到Chrome Web Store或者以Portable Native Client的形式发布了⽹址的Web服务器上),⽽Native Client需要维系跨平台可移植性,完全与系统API告别了,只能使⽤PPAPI提供的开发库和C/C++运⾏库,不过⽬前来看,基本的需求也是可以满⾜的。
下⾯贴⼀段PPAPI与JS交互的典型场景:
<object id="plugin" type="application/x-ppapi-post-message-example" width="1" height="1"/>
function HandleMessage(message_event) {
if (message_event.data) {
alert("The string was a palindrome.");
} else {
alert("The string was not a palindrome.");
}
}
function AddListener() {
var plugin = ElementById("plugin");
plugin.addEventListener("message", HandleMessage, false);
}
document.addEventListener("DOMContentLoaded", AddListener, false);
function SendString() {
var plugin = ElementById("plugin");
var inputBox = ElementById("inputBox");
/
/ Send the string to the plugin using postMessage. This results in a call
// to Instance::HandleMessage in C++ (or PPP_Messaging::HandleMessage in C).
plugin.postMessage(inputBox.value);
}
JS通过挂接插件的message事件来接收C/C++发送过来的通知,同时可以通过调⽤插件的postMessage⽅法给C/C++发送消息。
PPAPI的开发接⼝
⽆论是可信的还是不可信的PPAPI组件,⼀般都会提供三个导出函数:
int32_t PPP_InitializeModule(PP_Module module, PPB_GetInterface get_browser_interface);
初始化PPAPI模块,传⼊浏览器⽣成的模块句柄PP_Module,同时通过get_browser_interface参数给插件模块提供浏览器宿主常见的功能接⼝,⽐如PPB_Core、PPB_Messaging 、PPB_URLLoader、PPB_FileIO等接⼝。
void PPP_ShutdownModule(void);
关闭模块
const void* PPP_GetInterface(const char* interface_name);
提供给浏览器宿主的功能接⼝,⽐如PPP_Instance、PPP_InputEvent、PPP_Messaging等接⼝。
除了PPP_ShutdownModule是可选的,其他两个导出函数必须实现。PPB_GetInterface和PPP_GetInterface是插件(PPAPI)和宿主(浏览器的Plugin进程)功能交互的唯⼀切⼊⼝。
命名规范上,宿主Browser提供给插件(PPAPI)的接⼝以PPB命名开头,可以从导出函数PPP_InitializeModule 传⼊的PPB_GetInterface参数中根据接⼝ID查询对应的功能接⼝,下⾯列举⼀些常⽤的功能接⼝:
功能说明接⼝ID接⼝定义重要成员⽅法说明
核⼼基础接⼝PPB_CORE_INTERFACE PPB_Core AddRefResource、ReleaseResource:增加/减少资源的引⽤计数GetTime、GetTimeTicks:获取当前的时间CallOnMainThread、IsMainThread:主线程相关的处理
消息接⼝PPB_MESSAGING_INTERFACE PPB_Messaging PostMessage:抛送消息(Plugin->JS,JS通过addEventListener挂接message事件)
RegisterMessageHandler、UnregisterMessageHandler:⾃定义消息处理对象,⽤于拦截PPP_Messaging接⼝的HandleMessage处理(应⽤场景未明)
HTTP请求接⼝PPB_URLLOADER_INTERFACE PPB_URLLoader
常⽤的HTTP请求操作:Open、GetResponseInfo、
HTTP请求接⼝PPB_URLLOADER_INTERFACE PPB_URLLoader
ReadResponseBody、Close等等
⽂件IO操作PPB_FILEIO_INTERFACE PPB_FileIO File的基本操作:Create、Open、Read、Write、Close等
⽹络操作PPB_UDPSOCKET_INTERFACE、
PPB_TCPSOCKET_INTERFACE、
PPB_HOSTRESOLVER_INTERFACE等
PPB_UDPSocket、
PPB_TCPSocket、
PPB_HostResolver等
UDP套接字、TCP套接字、域名解析服务等常⽤的操作接⼝。
⾳视频操作PPB_AUDIO_INTERFACE、
PPB_VIDEODECODER_INTERFACE、
PPB_VIDEOENCODER_INTERFACE等
PPB_Audio、
PPB_VideoDecoder、
PPB_VideoEncoder等
封装⾳视频的编解码等操作。
此外还有Image、2D、3D等接⼝,基本上满⾜开发本地应⽤的所有功能。
插件(PPAPI)提供给宿主Browser的接⼝以PPP命名开头
功能说明接⼝ID接⼝定义重要成员⽅法说明
插件实例对象PPP_INSTANCE_INTERFACE PPP_Instance DidCreate、DidDestroy:插件创建销毁通知DidChangeView、DidChangeFocus:插件尺⼨、焦点改变通知HandleDocumentLoad:Document加载完成通知
这些调⽤最终都映射到Instance实例的相关成员⽅法上。
输⼊接⼝PPP_INPUT_EVENT_INTERFACE PPP_InputEvent HandleInputEvent:处理输⼊消息,最终转接到Instance的HandleInputEvent接⼝
消息接⼝PPP_MESSAGING_INTERFACE PPP_Messaging HandleMessage:处理消息(JS->Plugin, JS调⽤插件的postMessage⽅法),最终映射到Instance实例的HandleMessage⽅法。
除了这三个基础的接⼝,Module对象(后⾯再细讲)的AddPluginInterface提供了扩展插件接⼝的功能,⽐如:
封装PDF插件功能的PPP_Pdf接⼝;
封装视频采集功能的PPP_VideoCapture_Dev接⼝;
封装视频硬件解码的PPP_VideoDecoder_Dev接⼝;
封装3D图形相关的通知事件的PPP_Graphics3D接⼝;
此外还有PPP_MouseLock、PPP_Find_Private、PPP_Instance_Private等私有接⼝。
插件实例:每类插件都⽀持创建多个插件实例(⽐如浏览器打开多个带有Flash元素的页⾯),每个插件实例都对应⼀个PP_Instance句柄。
资源:由PP_Resource表⽰,这⾥的资源只是⼀种概念,因为前⾯提到的PPB、PPP接⼝都是模块级别,调⽤的时候需要传⼊PP_Instance句柄表⽰这次调⽤派往哪个插件实例,同时,某个功能的调⽤需要记录调⽤上下⽂,⽐如PPB_URLLoader,开启某个URL请求,读取某个已打开的URL请求多少字节、关闭某个已打开的URL请求,这个过程中“某个”就是⽤PP_Resource来表⽰,它是⽤来衔接插件端
和宿主容器功能调⽤上下⽂的句柄。每个PP_Resource都⾪属于⼀个PP_Instance。PP_Resource资源对象采⽤引⽤计数的⽅式维系⽣命周期,开发者可以使⽤PPB_Core接⼝的AddRefResource和ReleaseResource来对PP_Resource的引⽤计数进⾏增减。
PP_Instance、PP_Resource不仅是PPAPI插件与PPAPI Plugin宿主之间的交互桥梁,同时也是PPAPI Plugin宿主进程与浏览器Browser进程和Render进程之间的衔接,对于某个功能Resource,⽐如URLLoader,从插件模块的⾓度来看,插件模块封装接⼝调⽤的Proxy端,Plugin宿主进程是真正实现功能的Stub端,但Plugin进程也不是真正执⾏请求的地⽅,因为Plugin进程运⾏在沙箱⾥⾯,权限受限,⽂件操作、硬件访问等之类的功能都做不到。从Plugin进程的⾓度看,Plugin进程也只是个Proxy,Stub是Browser进程(有些功能Resource,Stub是Render进程,⽐如⾳视频编解码等)。他们之前都是通过PP_Instance和PP_Resource来做衔接标识。
介绍完基础概念,下⾯介绍开发框架。原⽣的开发接⼝都是C形式的,可以使⽤这些C接⼝直接开发你的应⽤:实现三个前⾯提到的导出函数,尤其是必须实现PPP_InitializeModule和
PPP_GetInterface,保存宿主传过来的PPB_GetInterface功能接⼝,提供给宿主⾃⼰的PPP_GetInterface调⽤,代码⽰例如下:
PP_Module g_module_id;
PPB_GetInterface g_get_browser_interface = NULL;
PP_EXPORT int32_t PPP_InitializeModule(PP_Module module_id,
PPB_GetInterface get_browser_interface) {
// Save the global module information for later.
g_module_id = module_id;
g_get_browser_interface = get_browser_interface;
return PP_OK;
}
PP_EXPORT void PPP_ShutdownModule() {
}
PP_EXPORT const void* PPP_GetInterface(const char* interface_name) {
/
/ You will normally implement a getter for at least PPP_INSTANCE_INTERFACE
// here.
return NULL;
}
C语⾔的相关接⼝请参见前⾯的章节。
除了C接⼝之外,PPAPI框架还提供了这些C接⼝的C++封装版本,使⽤C++版本可以忽略⼀些接⼝细节,⽐如PP_Resource。下⾯详细说下PPAPI的C++开发库。
⾸先,使⽤C++版本,开发者就⽆需关⼼三个导出函数的具体实现,框架在⽂件⾥对三个导出函数做了实现,创建Module对象标识整个插件模块:
开发者只需要提供⾃⼰的Module实现—MyModule,定制⾃⼰的插件实例MyInstance即可,⽰例代码:
class MyInstance : public pp::Instance {
public:
explicit MyInstance(PP_Instance instance) : pp::Instance(instance) {}
virtual ~MyInstance() {}
virtual bool Init(uint32_t argc, const char* argn[], const char* argv[]) {
return true;
}
};
// This object is the global object representing this plugin library as long
// as it is loaded.
class MyModule : public pp::Module {
public:
MyModule() : pp::Module() {}
virtual ~MyModule() {}
// Override CreateInstance to create your customized Instance object.
virtual pp::Instance* CreateInstance(PP_Instance instance) {
return new MyInstance(instance);
}
};
namespace pp {
// Factory function for your specialization of the Module object.
Module* CreateModule() {
return new MyModule();
}
} // namespace pp
在Module的实现中,除了保存PPP_InitializeModule导出函数传送过来的PPB_GetInterface接⼝并通过GetBrowserInterface接⼝暴露出去,同时也对PPP_GetInterface导出函数做了实现,对
PPP_INPUT_EVENT_INTERFACE、PPP_INSTANCE_INTERFACE、PPP_MESSAGING_INTERFACE三个PPP功能接⼝做了封装,把Plugin宿主的调⽤全部转接到Instance实现的对应成员⽅法上。除了⽀持这三个核⼼的PPP接⼝,Module还提供了AddPluginInterface⽅法让其它的功能组件定制其它的PPP接⼝实现,⽐如之前接⼝说明中提到的PPP_VideoCapture_Dev、PPP_Graphics3D等。Instance主要成员⽅法说明
成员⽅法功能说明
Init初始化,在PPP_Instance接⼝的DidCreate创建Instance实
例后调⽤
DidChangeView同样来⾃PPP_Instance接⼝的通知,在插件显⽰区域创建或
改变时触发,通知参数被封装成View类,View类通过
PPB_View接⼝可以获取插件的区域、缩放⽐例等信息
HandleInputEvent来⾃PPP_InputEvent接⼝的通知,处理⽤户输⼊事件(⿏
标、键盘等),具体请查看InputEvent类的封装。
HandleMessage来⾃PPP_Messaging接⼝,接收前端JS发送过来的消息。
PostMessage调⽤PPB_Messaging接⼝的PostMessage接⼝,功能与
HandleMessage相对,给前端JS发送通知。
BindGraphics调⽤PPB_Instance的BindGraphics接⼝,指定渲染接⼝,
有Graphics2D、Graphics3D、Compositor三种渲染⽅式。
前⾯说过,每个功能都对应⼀个PP_Resource句柄,PPAPI的C++库使⽤Resource类封装了PP_Resource句柄(主要封装PPB_Core接⼝对引⽤计数的调⽤),具体的功能类都派⽣⾃Resource。
上图列举了⼀些具有典型意义的功能类封装:
View:获取插件的界⾯显⽰区域信息,封装PPB_View接⼝。类似的UI封装还有Fullscreen
FileIO:⽂件IO操作(⽂件读取/写⼊),封装PPB_FileIO接⼝调⽤。FileIO类并不是我们平时理解的直接操作磁盘上某个⽬录读写操作,它使⽤HTML5 的本地⽂件系统FileSystem API,读写的⽂件路径都是在这个系统下的相对⽬录,File System API可以理解为Chromium沙箱内的⽂件系统,页⾯之间是隔离的。类似的还有DirectoryEntry(⽬录操作)。
VideoEncoder:视频编码操作,封装PPB_VideoEncoder接⼝调⽤,类似的还有VideoDecoder(视频解码)、VideoFrame(视频帧数据封装)、AudioEncoder(⾳频编码)、Audio(⾳频播放控制)、AudioBuffer(⾳频帧数据封装)、AudioInput_Dev(⾳频采集)、VideoCapture_Dev(视频采集)等等,PPAPI的⾳视频接⼝较多,有效地弥补了HTML5这⽅⾯的不⾜。从Chromium源码端分析,⽬前⾳频编码只⽀持OPUS,视频编解码⽀持H264、VP8/VP9。
html5开发示例编码⽅⾯:H264只⽀持硬件编码,VP8/VP9同时⽀持硬件和软件编码
解码⽅⾯:如果指定使⽤硬件加速,H264和VP8/VP9优先使⽤硬件解码(GPU),失败后再尝试软件解码(使⽤libvpx和FFmpeg)。
TCPSocket、URLLoader:⽹络相关的操作,同样的封装还有NetAddress、UDPSocket、HostResolver等。
Graphics3D:3D渲染逻辑,封装PPB_Graphics3D接⼝,类似的还有Graphics2D、Compositor⽅式。
PP_CompletionCallback概念:封装了编程中的任务操作,PPAPI中耗时的操作都采⽤异步的编程⽅式,⽐如⾳视频编码、⽂件读取、URL请求等操作,这些操作不会⽴马返回,需要传⼊⼀个回调接⼝来接受完成通知。这个回调接⼝的C语⾔定义格式如下:
struct PP_CompletionCallback {
/**
* This value is a callback function that will be called, or NULL if this is
* a blocking completion callback.
*/
PP_CompletionCallback_Func func;
/**
* This value is a pointer to user data passed to a callback function.
*/
void* user_data;
/**
* Flags used to control how non-NULL callbacks are scheduled by
* asynchronous methods.
*/
int32_t flags;
};
PP_CompletionCallback_Func的原型:
typedef void (*PP_CompletionCallback_Func)(void* user_data, int32_t result);
⽐如PPB_VideoEncoder接⼝的Encode⽅法,其原型如下
int32_t (*Encode)(PP_Resource video_encoder,
PP_Resource video_frame,
PP_Bool force_keyframe,
struct PP_CompletionCallback callback);
当这⼀帧编码完成时,会回调传⼊的callback接⼝,user_data参数原封不动传回去,result指定编码的结果,成功值是PP_OK。user_data参数原封不动地⼀来⼀回,⼀个很重要的考虑就是兼容C++的成员⽅法做为回调函数的处理逻辑。关于如何将C++的成员函数转成PP_CompletionCallback_Func风格的C函数,请参考CompletionCallback和CompletionCallbackFactory的代码逻
辑,CompletionCallbackFactory还实现了⼀套简陋的参数Bind机制,此外还有⼀个CompletionCallbackWithOutput模板类,⽤于⽀持带输出参数的CompletionCallback 版本,原理上也仅仅是在CompletionCallbackWithOutput内部创建⼀个成员变量作为PPB接⼝的参数。从个⼈⾓度看,这些看似复杂的玩意玩不转就不玩,不影响使⽤,再说CompletionCallbackWithOutput实现的也不够通⽤,只⽀持⼀个输出参数。
PPB_MessageLoop:对应的C++封装类是MessageLoop,是线程环境执⾏的重要辅助类。将MessageLoop对象附加到当前线程,线程间投递任务⾮常⽅便。
此外还有⼀套模拟脚本变量声明的组件:Var、VarArray、VarDictionary等,主要是为了⽅便postMessage、HandleMessage与JS交互的接⼝数据,抽象具体的类型信息,避免过载,还有Point、Rect、Size等常⽤的UI基础封装,这些都没有细讲的必要。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论