2021SC@SDUSC
上文对openmeetings-screenshare模块中src/main和src/site目录下的文件做了结构梳理,site文件下主要对site.xml进行了分析,main文件下对assembly、jnlp、resourse以及java文件夹进行了分析,主要是对java目录下的org.apache.openmeetings.screenshare下的gui和util进行了分析,对job目录开了个头,接下来,将job目录的分析,以及该目录下的主要其它类进行分析。
目录
job包
job文件夹以及包下面的类:
其中CursorJob、EncodeJob、RemoteJob、SendJob均实现了org.quartz.Job接口的java类(作业类),job接口包含的唯一一个方法:
public interface Job {
void execute(JobExecutionContext var1) throws JobExecutionException;
}
在我的Job接口实现类里面,添加一些逻辑到execute()方法。配置好Job实现类并设定好调度时间表,实现定时调度。
CursorJob
首先打开CursorJob类:代码如下
@DisallowConcurrentExecution
public class CursorJob implements Job {
public static final String CAPTURE_KEY = "capture";
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap data = context.getJobDetail().getJobDataMap();
CaptureScreen capture = (CaptureScreen)data.get(CAPTURE_KEY);
if (!capture.getSendFrameGuard()) {
capture.sendCursorStatus();
}
}
}
CursorJob类的execute方法接收一个JobExecutionContext对象,先来谈一下JobExecutionContext类和JobDataMap类。
JobExecutionContext简介:execute()方法只允许抛出JobExecutionException异常。
(1)当Scheduler调用一个Job,就会将JobExecutionContext传递给job的execute方法。
quartz无法调用job的有参构造函数,所以创建job的实例的时候是运用反射机制,通过newInstance创建实例,并且通过JobDetail描述的name与group属性然后给Job设置一些属性。
(2)Job能通过JobExecutionContext对象访问到Quartz运行时候的环境以及Job本身的明细数据。
(简介来自浅谈JobExecutionContext与JobDataMap - QiaoZhi - 博客园 (cnblogs.com))
JobDataMap是什么?
在进行任务调度时JobDataMap存储在JobExecutionContext中,JobDataMap可以用来装在任何可以序列化的数据对象,当job实例对象被执行时这些参数对象会传递给它。JobDataMap实现了JDK的Map接口,并且添加了一些方法来存取数据。
// 具体任务 JobDetail
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity("job1", "group1")
.usingJobData("jobSays", "Hello World!")
.usingJobData("myFloatValue", 3.1415f)
.build();
获取JobDataMap的方式:
- 从JobExecutionContext实例中获取,如上面代码:即使用该方法获取
//获取JobDetail通过JobDataMap传递的参数信息
JobDataMap data = context.getJobDetail().getJobDataMap();
//获取Trigger通过JobDataMap传递的信息
JobDataMap triggerDataMap = context.getTrigger().getJobDataMap();
- 从实例中获取JobDetail与Trigger合并的JobDataMap的方法:
//获取JobDetail通过JobDataMap传递的参数信息(获取两者合并后的JobDataMap)
JobDataMap jobDataMap = context.getMergedJobDataMap();
- Job实现类添加setter方法对应JobDataMap的键值(Quartz框架默认的JobFactory实现类在初始化job实例对象时会自动调用这些setter方法)
接下来回到Cursor类,该类里面只实现了execute方法,定义一个JobDataMap对象和CaptureScreen对象,然后该对象capture调用sendCursorStatus()方法,实时获取并发送鼠标的光标状态。
(CaptureScreen类里面的sendCursorStatus()方法)
(Core类里面的sendCursorStatus()方法)
EncodeJob
接下来看EncodeJob类:代码如下
@DisallowConcurrentExecution
public class EncodeJob implements Job {
private static final Logger log = getLogger(EncodeJob.class);
public static final String CAPTURE_KEY = "capture";
private Robot robot;
private ScreenDimensions dim;
private Rectangle screen = null;
private int[][] image = null;
public EncodeJob() {
try {
robot = new Robot();
} catch (AWTException e) {
log.error("encode: Unexpected Error while creating robot", e);
}
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap data = context.getJobDetail().getJobDataMap();
CaptureScreen capture = (CaptureScreen)data.get(CAPTURE_KEY);
if (screen == null) {
dim = capture.getDim();
screen = new Rectangle(dim.getSpinnerX(), dim.getSpinnerY()
, dim.getSpinnerWidth(), dim.getSpinnerHeight());
}
long start = 0;
if (log.isTraceEnabled()) {
start = System.currentTimeMillis();
}
image = ScreenV1Encoder.getImage(dim, screen, robot);
if (log.isTraceEnabled()) {
log.trace(String.format("encode: Image was captured in %s ms, size %sk", System.currentTimeMillis() - start, 4 * image.length * image[0].length / 1024));
start = System.currentTimeMillis();
}
try {
VideoData vData = capture.getEncoder().encode(image);
if (log.isTraceEnabled()) {
long now = System.currentTimeMillis();
log.trace(String.format("encode: Image was encoded in %s ms, timestamp is %s", now - start, now - capture.getStartTime()));
}
capture.getFrames().offer(vData);
capture.getEncoder().createUnalteredFrame();
} catch (Exception e) {
log.error("Error while encoding: ", e);
}
}
}
该类在构造方法里面实现的Robot类对象的初始化,否则则打印日志"encode: Unexpected Error while creating robot"。
Java Robot类用于测试自动化、自运行演示程序和其他需要控制鼠标和键盘的应用程序生成本机系统输入事件。Robot的主要目的是便于Java平台实现自动测试。
execute方法与CursorJob类中一样,初始化一个JobDataMap类对象和CaptureScreen类对象,并初始化Rectangle对象screen,如果screen==null则通过capture.getDim()获取ScreenDimensions对象dim,再通过dim的四个数据对象来初始化screen。而execute方法里主要是将image = ScreenV1Encoder.getImage(dim, screen, robot)获取得到的图片再通过capture.getEncoder().encode(image)进行编码,并打印获取图片所花的时间日志和编码所花时间的日志。
RemoteJob
接下来再看RemoteJob类:代码如下
@DisallowConcurrentExecution
public class RemoteJob implements Job {
private static final Logger log = getLogger(RemoteJob.class);
public static final String CORE_KEY = "core";
private Robot robot = null;
private ScreenDimensions dim = null;
public RemoteJob() {
try {
robot = new Robot();
robot.setAutoDelay(5);
} catch (AWTException e) {
log.error("Unexpected error while creating Robot", e);
}
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap data = context.getJobDetail().getJobDataMap();
Core core = (Core)data.get(CORE_KEY);
if (dim == null) {
dim = core.getDim();
}
try {
Map<String, Object> obj;
while ((obj = core.getRemoteEvents().poll(1, TimeUnit.MILLISECONDS)) != null) {
String action = String.valueOf(obj.get("action"));
log.trace("Action polled:: {}, count: {}", action, core.getRemoteEvents().size());
switch (action) {
case "mouseUp":
{
Point p = getCoordinates(obj);
robot.mouseMove(p.x, p.y);
robot.mouseRelease(InputEvent.BUTTON1_MASK);
}
break;
case "mouseDown":
{
Point p = getCoordinates(obj);
robot.mouseMove(p.x, p.y);
robot.mousePress(InputEvent.BUTTON1_MASK);
}
break;
case "mousePos":
{
Point p = getCoordinates(obj);
robot.mouseMove(p.x, p.y);
}
break;
case "keyDown":
new OmKeyEvent(obj).press(this);
break;
case "paste":
paste(String.valueOf(obj.get("paste")));
break;
case "copy":
{
String paste = getHighlightedText();
Map<Integer, String> map = new HashMap<>();
map.put(0, "copiedText");
map.put(1, paste);
String uid = String.valueOf(obj.get("uid"));
core.getInstance().invoke("sendMessageToClient", new Object[]{uid, map}, core);
}
break;
}
}
} catch (Exception err) {
log.error("[sendRemoteCursorEvent]", err);
}
}
public void press(List<Integer> codes) {
log.debug("sequence:: codes {}", codes);
press(codes.stream().mapToInt(Integer::intValue).toArray());
}
public void press(int... codes) {
for (int i = 0; i < codes.length; ++i) {
robot.keyPress(codes[i]);
}
for (int i = codes.length - 1; i > -1; --i) {
robot.keyRelease(codes[i]);
}
}
private String getHighlightedText() {
try {
if (SystemUtils.IS_OS_MAC) {
// Macintosh simulate Copy
press(157, 67);
} else {
// pressing CTRL+C == copy
press(KeyEvent.VK_CONTROL, KeyEvent.VK_C);
}
return getClipboardText();
} catch (Exception e) {
log.error("Unexpected exception while getting highlighted text", e);
}
return "";
}
public String getClipboardText() {
try {
// get the contents on the clipboard in a transferable object
Transferable data = getDefaultToolkit().getSystemClipboard().getContents(null);
// check if clipboard is empty
if (data == null) {
// Clipboard is empty!!!
} else if (data.isDataFlavorSupported(stringFlavor)) {
// see if DataFlavor of DataFlavor.stringFlavor is supported return text content
return (String) data.getTransferData(stringFlavor);
}
} catch (Exception e) {
log.error("Unexpected exception while getting clipboard text", e);
}
return "";
}
private void paste(String charValue) {
Clipboard clippy = getDefaultToolkit().getSystemClipboard();
try {
Transferable transferableText = new StringSelection(charValue);
clippy.setContents(transferableText, null);
if (SystemUtils.IS_OS_MAC) {
// Macintosh simulate Insert
press(157, 86);
} else {
// pressing CTRL+V == insert-mode
press(KeyEvent.VK_CONTROL, KeyEvent.VK_V);
}
} catch (Exception e) {
log.error("Unexpected exception while pressSpecialSign", e);
}
}
private Point getCoordinates(Map<String, Object> obj) {
float scaleFactorX = ((float)dim.getSpinnerWidth()) / dim.getResizeX();
float scaleFactorY = ((float)dim.getSpinnerHeight()) / dim.getResizeY();
int x = Math.round(scaleFactorX * getFloat(obj, "x") + dim.getSpinnerX());
int y = Math.round(scaleFactorY * getFloat(obj, "y") + dim.getSpinnerY());
return new Point(x, y);
}
}
该类和EncodeJob类一样在构造方法里面实现的Robot类对象的初始化,并设置了自动延迟5,否则则打印日志"encode: Unexpected Error while creating robot"。
在RemoteJob的execute方法里,主要是实现一个Map<String,Object> obj = core.getRemoteEvents().poll(1, TimeUnit.MILLISECONDS),String action = String.valueOf(obj.get("action"));再用switch/case语句来实现不同的action完成不同的操作,如
case "mouseUp":
{
Point p = getCoordinates(obj);
robot.mouseMove(p.x, p.y);
robot.mouseRelease(InputEvent.BUTTON1_MASK);
}
break;
后面的两个press方法是用robot实现对按下键和松开键的模拟,调用了robot.keyPress()和robot.ketRelease()方法。
然后getHighlightedText()方法来模拟复制这个操作,调用press方法。
getClipboradText()方法用来在Transferable对象中获取剪贴板上的内容
paste()方法用来模拟粘贴这个操作,调用press方法。
getCoordinates方法()获取坐标x,y
SendJob
然后是SendJob类:代码如下
public class SendJob implements Job {
private static final Logger log = getLogger(SendJob.class);
public static final String CAPTURE_KEY = "capture";
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap data = context.getJobDetail().getJobDataMap();
CaptureScreen capture = (CaptureScreen)data.get(CAPTURE_KEY);
capture.setSendFrameGuard(true);
if (log.isTraceEnabled()) {
long real = System.currentTimeMillis() - capture.getStartTime();
log.trace(String.format("send: Enter method, timestamp: %s, real: %s, diff: %s", capture.getTimestamp(), real, real - capture.getTimestamp().get()));
}
VideoData f = capture.getFrames().poll();
if (log.isTraceEnabled()) {
log.trace(String.format("send: Getting %s image", f == null ? "DUMMY" : "CAPTURED"));
}
f = f == null ? capture.getEncoder().getUnalteredFrame() : f;
if (f != null) {
capture.pushVideo(f, capture.getTimestamp().get());
if (log.isTraceEnabled()) {
long real = System.currentTimeMillis() - capture.getStartTime();
log.trace(String.format("send: Sending video %sk, timestamp: %s, real: %s, diff: %s", f.getData().capacity() / 1024, capture.getTimestamp(), real, real - capture.getTimestamp().get()));
}
capture.getTimestamp().addAndGet(capture.getTimestampDelta());
if (log.isTraceEnabled()) {
log.trace(String.format("send: new timestamp: %s", capture.getTimestamp()));
}
} else if (log.isTraceEnabled()) {
log.trace("send: nothing to send");
}
capture.setSendFrameGuard(false);
}
}
execute方法与CursorJob类中一样,初始化一个JobDataMap类对象和CaptureScreen类对象,通过capture对象进行video的发送与记录时间,主要语句为下面两句。
VideoData f = capture.getFrames().poll();
capture.pushVideo(f, capture.getTimestamp().get());
OmKeyEvent
然后是job包下最后一个类OmKeyEvent:代码如下
public class OmKeyEvent {
private static final Logger log = getLogger(OmKeyEvent.class);
private static final Map<Integer, Integer> KEY_MAP = new HashMap<>();
private static final Map<Character, Integer> CHAR_MAP = new HashMap<>();
private static final List<Character> _UMLAUTS = Arrays.asList('ß', 'ö', 'Ö', 'ä', 'Ä', 'ü', 'Ü');
private static final Set<Character> UMLAUTS = Collections.unmodifiableSet(_UMLAUTS.stream().collect(Collectors.toSet()));
private static final Set<Character> UNPRINTABLE = Collections.unmodifiableSet(Stream.concat(_UMLAUTS.stream(), Stream.of('§')).collect(Collectors.toSet()));
static {
KEY_MAP.put(13, KeyEvent.VK_ENTER);
KEY_MAP.put(16, 0);
KEY_MAP.put(20, KeyEvent.VK_CAPS_LOCK);
KEY_MAP.put(43, KeyEvent.VK_ADD); //normal + -> numpad + ????
KEY_MAP.put(46, KeyEvent.VK_DELETE);
KEY_MAP.put(110, KeyEvent.VK_DECIMAL);
KEY_MAP.put(186, KeyEvent.VK_SEMICOLON);
KEY_MAP.put(187, KeyEvent.VK_EQUALS);
KEY_MAP.put(188, KeyEvent.VK_COMMA);
KEY_MAP.put(189, KeyEvent.VK_MINUS);
KEY_MAP.put(190, KeyEvent.VK_PERIOD);
KEY_MAP.put(191, KeyEvent.VK_SLASH);
KEY_MAP.put(219, KeyEvent.VK_OPEN_BRACKET);
KEY_MAP.put(220, KeyEvent.VK_BACK_SLASH);
KEY_MAP.put(221, KeyEvent.VK_CLOSE_BRACKET);
KEY_MAP.put(222, KeyEvent.VK_QUOTE);
CHAR_MAP.put(Character.valueOf('#'), KeyEvent.VK_NUMBER_SIGN);
CHAR_MAP.put(Character.valueOf('<'), KeyEvent.VK_LESS);
CHAR_MAP.put(Character.valueOf('.'), KeyEvent.VK_PERIOD);
CHAR_MAP.put(Character.valueOf(','), KeyEvent.VK_COMMA);
CHAR_MAP.put(Character.valueOf('-'), KeyEvent.VK_MINUS);
CHAR_MAP.put(Character.valueOf('='), KeyEvent.VK_EQUALS);
CHAR_MAP.put(Character.valueOf('['), KeyEvent.VK_OPEN_BRACKET);
CHAR_MAP.put(Character.valueOf(']'), KeyEvent.VK_CLOSE_BRACKET);
CHAR_MAP.put(Character.valueOf(';'), KeyEvent.VK_SEMICOLON);
CHAR_MAP.put(Character.valueOf('\''), KeyEvent.VK_QUOTE);
CHAR_MAP.put(Character.valueOf('\\'), KeyEvent.VK_BACK_SLASH);
CHAR_MAP.put(Character.valueOf('`'), KeyEvent.VK_BACK_QUOTE);
CHAR_MAP.put(Character.valueOf('/'), KeyEvent.VK_SLASH);
}
private boolean alt = false;
private boolean ctrl = false;
private boolean shift = false;
private int inKey = 0;
private int key = 0;
private char ch = 0;
public OmKeyEvent(Map<String, Object> obj) {
alt = TRUE.equals(obj.get("alt"));
ctrl = TRUE.equals(obj.get("ctrl"));
shift = TRUE.equals(obj.get("shift")) || isUpperCase(ch);
ch = (char)getInt(obj, "char");
key = inKey = getInt(obj, "key");
Integer _key = null;
if (CharUtils.isAsciiPrintable(ch)) {
boolean alpha = Character.isAlphabetic(ch);
if (alpha) { // can't be combined due to different types
key = getKeyStroke(toUpperCase(ch), 0).getKeyCode();
} else {
key = getKeyStroke(Character.valueOf(ch), 0).getKeyCode();
}
if (key == 0) {
_key = CHAR_MAP.get(ch);
if (_key == null) {
// fallback
key = inKey;
}
}
if (!alpha && _key == null) {
_key = KEY_MAP.get(key);
}
} else {
_key = KEY_MAP.get(key);
}
this.key = _key == null ? key : _key;
log.debug("sequence:: shift {}, ch {}, orig {} -> key {}({}), map {}", shift, ch == 0 ? ' ' : ch, inKey, key, Integer.toHexString(key), _key);
}
private static int getVowel(char ch) {
int vowel = ch;
switch(toUpperCase(ch)) {
case 'Ö':
vowel = KeyEvent.VK_O;
break;
case 'Ä':
vowel = KeyEvent.VK_A;
break;
case 'Ü':
vowel = KeyEvent.VK_U;
break;
}
return vowel;
}
public void press(RemoteJob r) {
List<Integer> list = new ArrayList<>();
if (UNPRINTABLE.contains(ch)) {
if (SystemUtils.IS_OS_LINUX) {
r.press(KeyEvent.VK_CONTROL, KeyEvent.VK_SHIFT, KeyEvent.VK_U);
String hex = Integer.toHexString(ch);
log.debug("sequence:: hex {}", hex);
for (int i = 0; i < hex.length(); ++i) {
r.press(KeyStroke.getKeyStroke(toUpperCase(hex.charAt(i)), 0).getKeyCode());
}
r.press(KeyEvent.VK_ENTER);
} else if (SystemUtils.IS_OS_MAC) {
if (ch == 'ß') {
r.press(KeyEvent.VK_ALT, KeyEvent.VK_S);
} else {
if (UMLAUTS.contains(ch)) {
r.press(KeyEvent.VK_ALT, KeyEvent.VK_U);
if (shift) {
list.add(KeyEvent.VK_SHIFT);
}
list.add(getVowel(ch));
r.press(list);
}
}
} else if (SystemUtils.IS_OS_WINDOWS && UMLAUTS.contains(ch)) {
list.add(KeyEvent.VK_ALT);
list.add(KeyEvent.VK_ADD);
String code = String.format("%04d", (int)ch);
for (int i = 0; i < code.length(); ++i) {
list.add(KeyEvent.VK_NUMPAD0 + code.charAt(i));
}
r.press(list);
}
} else {
if (shift) {
list.add(KeyEvent.VK_SHIFT);
}
if (alt) {
list.add(KeyEvent.VK_ALT);
}
if (ctrl) {
list.add(KeyEvent.VK_CONTROL);
}
if (key != 0) {
list.add(key);
}
r.press(list);
}
}
}
建立了KET_MAP和CHAR_MAP值和字符与对应键盘的一一映射表,如ENTER键、DELETE键、BRACKET键等,以及字符”#”、”[”、”]”等,设置alt、ctrl、shift的boolean值,对字符'ß', 'ö', 'Ö', 'ä', 'Ä', 'ü', 'Ü'也建立了对应的键盘事件,使用时可以方便查询。
总结
总结:以上便是java包下org.apache.openmeetings.screenshare下job文件里所有的类与功能。四个不同的job类均继承了Job类重写了execute()方法,添加了相应需要的逻辑,完成不同的功能。
再看该文件下的其他Java类:可以看出主要类是Capture和Core类,之后将对这些类进行代码分析。