2021SC@SDUSC
目录
ScreenShare下的类
网络会议openmeetings文件下的openmeetings-screenshare即将分析完毕,该文件下的其它类,上文已经分析完IScreenRncoder接口、IScreenShare接口、BaseScreenEncoder抽象类、AudioTone类以及ScreenV1Encoder类。本篇将对本文件的最后的内容Core、CaptureScreen、RTMPClientPublish以及四个RTMPScreenShare进行分析(四个文件代码几乎一致),则openmeetings-screenshare分析完毕。
Core
Core实现了IpendingServiceCallback和InetStreamEventHandler接口并实现了其中的抽象方法,IPendingServiceCallback接口中的ResultReceived ()方法专门用来处理调用的结果,InetStreamEventHandler接口中onStreamEvent()方法专门处理流事件。
Core类里面定义了一个不变的Java Logger日志记录器对象log,提供了日志记录的API ,可以往控制台/文件中写日志了。
然后定义了几个字符串常量值分别为“ScreenShare”、“RemoteJob”、“RemoteTrigger”、“NetConnection.Connect.Rejected”、“NetConnection.Connect.Failed”来分别对应不同的类和运行情况。
然后定义了一个枚举变量Protocol协议,其中的协议有rtmp、rtmpt、rtmpe、rtmps
再接着定义了IScreenShare接口类对象instance,这里会利用多态来实现并调用不同的方法。
定义了两个URI变量url和fallback分别表示当前url和回退到上一个页面的url。
定义布尔变量fallbackUsed=false表示是否能回退。
定义了字符串变量host、app以及int变量port表示主机号、应用、端口号。
定义了CaptureScreen类对象_capture、RTMPClientPublish类对象publishClient、以及ScreenSharerFrame类对象。
定义了默认quality为1,默认fps为10,是否显示fps为true,布尔变量allowRecording和allowPublishing为ture,表示是否网络允许记录和网络准许发布,布尔变量startSharing、startRecording、startPublishing、connected、readyToRecord、audioNotify均为false,来表示屏幕共享应用的对应状态,remoteEnabled为true(启用远程)、nativeSsl(本机ssl,ssl是一种安全协议,它提供使用TCP/IP的通信应用程序间的隐私与完整性)为false。
定义了SchedulerFactory类对象schdlrFactory和Scheduler对象schdlr,调度器Scheduler是Quartz框架的心脏,用来管理触发器和Job,并保证Job能被触发执行。程序员与框架内部之间的调用都是通过org.quartz.Scheduler接口来完成的。然而我们一般不使用构造方法去创建调度器,而是通过调度器工厂来创建。调度器工厂接口org.quartz.SchedulerFactory提供了两种不同类型的工厂实现,分别是org.quartz.impl.DirectSchedulerFactoryh和org.quartz.impl.StdSchedulerFactory。这里的SchedulerFactory类便是用来初始化创建Scheduler的。
定义了一个LinkedBlockingQueue类队列remoteEvent来表示远程事件。定义了ScreenDimensions类对象dim来表示屏幕的各种维度。
接下来看看构造方法:public Core(String[] args)
初始化dim,为屏幕共享的屏幕设置了不同的维度。
System.setProperty()方法设置键对值的系统属性,其中第一个参数是系统属性的名称,第二个参数是设置系统属性的值,这里设置"org.terracotta.quartz.skipUpdateCheck"为true,这里是设置quartz中的定时检查版本更新为true。然后打印日志,String args的参数。
然后为我的类属性设置初始值,url = URI(args[0]),fallbacl = URI(args[1]), sid = args[2],labelTexts = args[3];defaultQuality = Integer.parseInt(args[4]),defaultFps = Integer.parseInt(args[5]);showFps = bool(args[6]);remoteEnabled = bool(args[7]);allowRecording = bool(args[8]);allowPublishing = bool(args[9]);nativeSsl = bool(args[10])。
然后初始化调度器schdlr,设置JobDetail和Trigger类对象,当一个trigger被触发时,与之关联的JobDetail实例会被加载,JobDetail引用的job类通过配置在Scheduler上的JobFactory进行初始化。这里的JobDetail类remoteJob用JobBuilder类.build()方法来初始化,设置新的job,Trigger类对象cursorTrigger用TriggerBuilder类.build()方法来初始化,设置触发,然后加入调度器中等待调度。
下面的getCapture()方法即是判断我的类变量_capture是否为空,如果为空则new一个,返回该变量。
setInstance()方法,根据传进来的参数URI来获取uri中的协议,根据uri获取host、port和app的值,对应的方法直接调用即可getHost()、getPort()、getPath().substring(1)。然后根据不同的协议调用不同的类构造方法初始化instance。rtmp对应的是RTMPScreenShare,rtmpt对应的是RTMPTScreenShare,rtmps对应的是RTMPSScreenShare,否则抛出RuntimeException错误。然后log设置此处debug值,里面写的是对应的host、port、app以及sid的值用于调试。
接下来的createWindow()方法,则是初始化frame属性值,new一个ScreenSharerFrame对象,前文提到了这个界面是用javaswing写的,设置为可见,调用setVisible(true),然后设置允许记录标签和准许发布标签是否启用。
接下来的sendCursorStatus()方法,获取鼠标的实际位置x和y,判断是否连接,如果没有,则利用Red5类调用setConnectionLocal方法建立连接,然后调用invoke方法à查找并调用传入的参数中对应的方法,设置新的触发器位置,这个触发器指鼠标,位置即为鼠标的位置。如果设置失败,则frame设置状态err,log打印日志err。
接下来的setConnectionAsSharingClient()方法,首先打印debug日志,方便调试,首先判断是否连接,若没有则利用Red5类调用setConnectionLocal方法建立连接,然后定义一个Mao类对象,首先获取dim的X和Y大小,然后将属性一一建立Map映射放入map中,然后继续检查是否连接,然后调用invoke方法à查找并调用传入的参数中对应的方法,设置用户端的连接,第二个参数为map组,否则设置frame的状态为error,log打印日志err。
接下来的sharingStart()方法,即开始任务调度(屏幕共享),schdlr.start(),将startSharing设置为true。recordingStart()方法,将startRecording设置为true。publishingStart()方法,将startPublishing设置为true。上面三个方法均调用该类里面的captureScreenStart()方法,即开始屏幕捕捉。后文再继续分析该方法。
接下来的connect()方法则是instance调用connect()方法建立连接。handleException()方法则是处理异常,回调上一个网页,然后调用该Core类里面connect()即前面那个方法重新建立连接。然后captureScreenStart()方法,如果没有连接则重新建立连接,然后调用上文setConnectionAsSharingClient方法,调用invoke方法设置用户端的连接,开始对屏幕进行捕捉。invoke方法的使用需要导入Red5 的package包。
接下来的sharingStop()、recordingStop()、publishingStop()方法均是将对应的属性设置为false,然后调用captureScreenStop()方法。然后后面的captureScreenStop()方法则是即停止屏幕捕捉。调用invoke方法,根据传入的参数调用screenSharerAction方法。invoke方法的使用需要导入Red5 的package包。然后stopSharing()方法则是schdlr.standby(),设置Scheduler为standby模式会让调度器暂停寻找Job去执行。然后设置startSharing属性为false。后来的stopRecording()、stopPublishing()则是将状态设为false,断开连接。
后面的onCommand()方法,是对待处理的命令(传参)进行处理。接下来的stopStream()方法,会停止任何活动并断开连接,使用未使用的参数来执行调用。resultReceived()方法,根据传入的参数来得到调用的方法名字,建立连接,再根据该名字调用不同的方法来完成该操作,追踪并log记录,返回不同的结果。
其他方法即是一些get方法,即获取并返回某些属性值或对象。
public class Core implements IPendingServiceCallback, INetStreamEventHandler {
private static final Logger log = getLogger(Core.class);
static final String QUARTZ_GROUP_NAME = "ScreenShare";
static final String QUARTZ_REMOTE_JOB_NAME = "RemoteJob";
static final String QUARTZ_REMOTE_TRIGGER_NAME = "RemoteTrigger";
private static final String CONNECT_REJECTED = "NetConnection.Connect.Rejected";
private static final String CONNECT_FAILED = "NetConnection.Connect.Failed";
enum Protocol {
rtmp, rtmpt, rtmpe, rtmps
}
private IScreenShare instance = null;
private URI url;
private URI fallback;
private boolean fallbackUsed = false;
private String host;
private String app;
private int port;
private String sid;
private CaptureScreen _capture = null;
private RTMPClientPublish publishClient = null;
private ScreenSharerFrame frame;
private int defaultQuality = 1;
private int defaultFps = 10;
private boolean showFps = true;
private boolean allowRecording = true;
private boolean allowPublishing = true;
private boolean startSharing = false;
private boolean startRecording = false;
private boolean startPublishing = false;
private boolean connected = false;
private boolean readyToRecord = false;
private boolean audioNotify = false;
private boolean remoteEnabled = true;
private boolean nativeSsl = false;
private SchedulerFactory schdlrFactory;
private Scheduler schdlr;
private LinkedBlockingQueue<Map<String, Object>> remoteEvents = new LinkedBlockingQueue<>();
private final ScreenDimensions dim;
public Core(String[] args) {
dim = new ScreenDimensions();
try {
System.setProperty("org.terracotta.quartz.skipUpdateCheck", "true");
for (String arg : args) {
log.debug("arg: " + arg);
}
String[] textArray = null;
if (args.length > 8) {
url = new URI(args[0]);
fallback = new URI(args[1]);
sid = args[2];
String labelTexts = args[3];
defaultQuality = Integer.parseInt(args[4]);
defaultFps = Integer.parseInt(args[5]);
showFps = bool(args[6]);
remoteEnabled = bool(args[7]);
allowRecording = bool(args[8]);
allowPublishing = bool(args[9]);
nativeSsl = bool(args[10]);
if (labelTexts.length() > 0) {
textArray = labelTexts.split(";");
log.debug("labelTexts :: " + labelTexts);
log.debug("textArray Length " + textArray.length);
for (int i = 0; i < textArray.length; i++) {
log.debug(i + " :: " + textArray[i]);
}
}
} else {
System.exit(0);
}
schdlrFactory = new StdSchedulerFactory(getQurtzProps("CoreScreenShare"));
schdlr = schdlrFactory.getScheduler();
JobDetail remoteJob = JobBuilder.newJob(RemoteJob.class).withIdentity(QUARTZ_REMOTE_JOB_NAME, QUARTZ_GROUP_NAME).build();
Trigger cursorTrigger = TriggerBuilder.newTrigger()
.withIdentity(QUARTZ_REMOTE_TRIGGER_NAME, QUARTZ_GROUP_NAME)
.withSchedule(simpleSchedule().withIntervalInMilliseconds(50).repeatForever())
.build();
remoteJob.getJobDataMap().put(RemoteJob.CORE_KEY, this);
schdlr.scheduleJob(remoteJob, cursorTrigger);
createWindow(textArray);
} catch (Exception err) {
log.error("", err);
}
}
private CaptureScreen getCapture() {
if (_capture == null) {
_capture = new CaptureScreen(this, instance, host, app, port);
}
return _capture;
}
private void setInstance(URI uri) {
Protocol protocol = Protocol.valueOf(uri.getScheme());
host = uri.getHost();
port = uri.getPort();
app = uri.getPath().substring(1);
switch (protocol) {
case rtmp:
instance = new RTMPScreenShare(this);
break;
case rtmpt:
instance = new RTMPTScreenShare(this);
break;
case rtmps:
if (nativeSsl) {
RTMPSScreenShare client = new RTMPSScreenShare(this);
instance = client;
} else {
instance = new RTMPTSScreenShare(this);
}
break;
case rtmpe:
default:
throw new RuntimeException("Unsupported protocol");
}
instance.setServiceProvider(this);
log.debug(String.format("host: %s, port: %s, app: %s, publish: %s", host, port, app, sid));
}
// ------------------------------------------------------------------------
//
// Main
//
// ------------------------------------------------------------------------
public static void main(String[] args) {
new Core(args);
}
// ------------------------------------------------------------------------
//
// GUI
//
// ------------------------------------------------------------------------
public void createWindow(String[] textArray) {
try {
frame = new ScreenSharerFrame(this, textArray);
frame.setVisible(true);
frame.setRecordingTabEnabled(allowRecording);
frame.setPublishingTabEnabled(allowPublishing);
log.debug("initialized");
} catch (Exception err) {
log.error("createWindow Exception: ", err);
}
}
public void sendCursorStatus() {
try {
Point mouseP = MouseInfo.getPointerInfo().getLocation();
float scaleFactor = (1.0f * dim.getResizeX()) / dim.getSpinnerWidth();
// Real size: Real mouse position = Resize : X
int x = (int)((mouseP.getX() - dim.getSpinnerX()) * scaleFactor);
int y = (int)((mouseP.getY() - dim.getSpinnerY()) * scaleFactor);
if (instance.getConnection() != null) {
if (Red5.getConnectionLocal() == null) {
Red5.setConnectionLocal(instance.getConnection());
}
instance.invoke("setNewCursorPosition", new Object[] { x, y }, this);
}
} catch (NullPointerException npe) {
//noop
} catch (Exception err) {
frame.setStatus("Exception: " + err);
log.error("[sendCursorStatus]", err);
}
}
/**
* @param id The streamid sent by server
*/
public void setId(String id) {
//no-op
}
public void setConnectionAsSharingClient() {
log.debug("########## setConnectionAsSharingClient");
try {
if (Red5.getConnectionLocal() == null) {
Red5.setConnectionLocal(instance.getConnection());
}
Map<String, Object> map = new HashMap<>();
int scaledWidth = dim.getResizeX();
int scaledHeight = dim.getResizeY();
map.put("screenWidth", scaledWidth);
map.put("screenHeight", scaledHeight);
map.put("startRecording", startRecording);
map.put("startStreaming", startSharing);
map.put("startPublishing", startPublishing);
map.put("publishingHost", frame.getPublishHost());
map.put("publishingApp", frame.getPublishApp());
map.put("publishingId", frame.getPublishId());
if (Red5.getConnectionLocal() == null) {
Red5.setConnectionLocal(instance.getConnection());
}
instance.invoke("setConnectionAsSharingClient", new Object[] { map }, this);
} catch (Exception err) {
frame.setStatus("Error: " + err.getLocalizedMessage());
log.error("[setConnectionAsSharingClient]", err);
}
}
public void sharingStart() {
try {
schdlr.start();
} catch (SchedulerException e) {
log.error("[schdlr.start]", e);
}
startSharing = true;
captureScreenStart();
}
public void recordingStart() {
startRecording= true;
captureScreenStart();
}
public void publishingStart() {
startPublishing = true;
captureScreenStart();
}
private void connect(String sid) {
setInstance(fallbackUsed ? fallback : url);
Map<String, Object> map = instance.makeDefaultConnectionParams(host, port, app);
map.put("screenClient", true);
Map<String, Object> params = new HashMap<>();
params.put("sid", sid);
instance.connect(host, port, map, this, new Object[]{params});
}
void handleException(Throwable e) {
frame.setStatus("Exception: " + e);
if (e instanceof ConnectException) {
fallbackUsed = true;
connect(sid);
}
}
private void captureScreenStart() {
try {
log.debug("captureScreenStart");
if (!connected) {
connect(sid);
} else {
setConnectionAsSharingClient();
}
} catch (Exception err) {
log.error("captureScreenStart Exception: ", err);
frame.setStatus("Exception: " + err);
}
}
public void sharingStop() {
startSharing = false;
captureScreenStop("stopStreaming");
}
public void recordingStop() {
startRecording = false;
captureScreenStop("stopRecording");
}
public void publishingStop() {
startPublishing = false;
captureScreenStop("stopPublishing");
}
private void captureScreenStop(String action) {
try {
log.debug("INVOKE screenSharerAction" );
Map<String, Object> map = new HashMap<>();
map.put(action, true);
if (Red5.getConnectionLocal() == null) {
Red5.setConnectionLocal(instance.getConnection());
}
instance.invoke("screenSharerAction", new Object[] { map }, this);
} catch (Exception err) {
log.error("captureScreenStop Exception: ", err);
frame.setStatus("Exception: " + err);
}
}
public void stopSharing() {
try {
schdlr.standby();
} catch (SchedulerException e) {
log.error("[schdlr.standby]", e);
}
frame.setSharingStatus(false, !startPublishing && !startRecording && !startSharing);
startSharing = false;
}
public void stopRecording() {
frame.setRecordingStatus(false, !startPublishing && !startRecording && !startSharing);
startRecording = false;
}
public void stopPublishing() {
frame.setPublishingStatus(false, !startPublishing && !startRecording && !startSharing);
startPublishing = false;
if (publishClient != null) {
publishClient.disconnect();
publishClient = null;
}
}
public synchronized boolean isReadyToRecord() {
return readyToRecord;
}
private synchronized void setReadyToRecord(boolean readyToRecord) {
this.readyToRecord = readyToRecord;
}
/**
* @param command - command to be processed
*/
protected void onCommand(ICommand command) {
if (!(command instanceof Notify)) {
return;
}
Notify invoke = (Notify)command;
if (invoke.getType() == IEvent.Type.STREAM_DATA) {
return;
}
String method = invoke.getCall().getServiceMethodName();
if ("screenSharerAction".equals(method)) {
Object[] args = invoke.getCall().getArguments();
if (args != null && args.length > 0) {
@SuppressWarnings("unchecked")
Map<String, Object> params = (Map<String, Object>)args[0];
if (bool(params.get("stopPublishing"))) {
stopPublishing();
}
if (params.containsKey("error")) {
frame.setStatus("" + params.get("error"));
}
}
}
}
/**
* Will stop any activity and disconnect
*
* @param obj - dummy unused param to perform the call
*/
public void stopStream(Object obj) {
try {
log.debug("ScreenShare stopStream");
stopSharing();
stopRecording();
stopPublishing();
connected = false;
if (instance != null) {
instance.disconnect();
}
setReadyToRecord(false);
getCapture().setStartPublish(false);
getCapture().release();
_capture = null;
} catch (Exception e) {
log.error("ScreenShare stopStream exception " + e);
}
}
@Override
public void onStreamEvent(Notify notify) {
log.debug( "onStreamEvent " + notify );
@SuppressWarnings("rawtypes")
ObjectMap map = (ObjectMap) notify.getCall().getArguments()[0];
String code = (String) map.get("code");
if (StatusCodes.NS_PUBLISH_START.equals(code)) {
log.debug( "onStreamEvent Publish start" );
getCapture().setStartPublish(true);
setReadyToRecord(true);
}
}
private static boolean bool(Object b) {
return TRUE.equals(Boolean.valueOf("" + b));
}
public void sendRemoteCursorEvent(Map<String, Object> obj) {
if (!remoteEnabled) {
return;
}
log.trace("#### sendRemoteCursorEvent ");
log.trace("Result Map Type "+ obj);
if (obj != null) {
remoteEvents.offer(obj);
log.trace("Action offered:: {}, count: {}", obj, remoteEvents.size());
}
}
@Override
public void resultReceived(IPendingServiceCall call) {
try {
log.trace("service call result: " + call);
if (call == null) {
return;
}
String method = call.getServiceMethodName();
Object o = call.getResult();
if (log.isTraceEnabled()) {
log.trace("Result Map Type " + (o == null ? null : o.getClass().getName()));
log.trace("" + o);
}
@SuppressWarnings("unchecked")
Map<String, Object> returnMap = (o != null && o instanceof Map) ? (Map<String, Object>) o : new HashMap<>();
log.trace("call ### get Method Name " + method);
if ("connect".equals(method)) {
Object code = returnMap.get("code");
if (CONNECT_FAILED.equals(code) && !fallbackUsed) {
fallbackUsed = true;
connect(sid);
frame.setStatus("Re-connecting using fallback");
return;
}
if (CONNECT_FAILED.equals(code) || CONNECT_REJECTED.equals(code)) {
frame.setStatus(String.format("Error: %s %s", code, returnMap.get("description")));
return;
}
connected = true;
setConnectionAsSharingClient();
} else if ("setConnectionAsSharingClient".equals(method)) {
if (!bool(returnMap.get("alreadyPublished"))) {
log.trace("Stream not yet started - do it ");
instance.createStream(this);
} else {
log.trace("The Stream was already started ");
}
if (o != null) {
Object modus = returnMap.get("modus");
if ("startStreaming".equals(modus)) {
frame.setSharingStatus(true, false);
} else if ("startRecording".equals(modus)) {
frame.setRecordingStatus(true, false);
} else if ("startPublishing".equals(modus)) {
frame.setPublishingStatus(true, false);
publishClient = new RTMPClientPublish(
this
, frame.getPublishHost()
, frame.getPublishApp()
, frame.getPublishId());
publishClient.connect();
}
} else {
String err = "Could not aquire modus for event setConnectionAsSharingClient";
frame.setStatus(String.format("Error: %s", err));
return;
}
} else if ("createStream".equals(method)) {
if (startRecording || startSharing) {
CaptureScreen capture = getCapture();
if (o != null && o instanceof Number) {
if (capture.getStreamId() != null) {
instance.unpublish(capture.getStreamId());
}
capture.setStreamId((Number)o);
}
final String broadcastId = UUID.randomUUID().toString();
log.debug("createPublishStream result stream id: {}; name: {}", capture.getStreamId(), broadcastId);
instance.publish(capture.getStreamId(), broadcastId, "live", this);
log.debug("setup capture thread spinnerWidth = {}; spinnerHeight = {};", dim.getSpinnerWidth(), dim.getSpinnerHeight());
if (!capture.isStarted()) {
capture.setSendCursor(startSharing);
capture.start();
}
}
} else if ("screenSharerAction".equals(method)) {
Object result = returnMap.get("result");
if ("stopAll".equals(result)) {
log.trace("Stopping to stream, there is neither a Desktop Sharing nor Recording anymore");
stopStream(null);
} else if ("stopSharingOnly".equals(result)) {
stopSharing();
} else if ("stopRecordingOnly".equals(result)) {
stopRecording();
} else if ("stopPublishingOnly".equals(result)) {
stopPublishing();
}
} else if ("setNewCursorPosition".equals(method)) {
// Do not do anything
} else {
log.debug("Unknown method " + method);
}
} catch (Exception err) {
log.error("[resultReceived]", err);
}
}
public boolean isAudioNotify() {
return audioNotify;
}
public void setAudioNotify(boolean audioNotify) {
this.audioNotify = audioNotify;
}
public boolean isRemoteEnabled() {
return remoteEnabled;
}
public void setRemoteEnabled(boolean remoteEnabled) {
this.remoteEnabled = remoteEnabled;
}
public void setDeadlockGuard(RTMPConnection conn) {
ThreadPoolTaskScheduler deadlockGuard = new ThreadPoolTaskScheduler();
deadlockGuard.setPoolSize(16);
deadlockGuard.setDaemon(false);
deadlockGuard.setWaitForTasksToCompleteOnShutdown(true);
deadlockGuard.setThreadNamePrefix("DeadlockGuardScheduler-");
deadlockGuard.afterPropertiesSet();
conn.setDeadlockGuardScheduler(deadlockGuard);
}
public IScreenShare getInstance() {
return instance;
}
public LinkedBlockingQueue<Map<String, Object>> getRemoteEvents() {
return remoteEvents;
}
public ScreenDimensions getDim() {
return dim;
}
public int getDefaultQuality() {
return defaultQuality;
}
public int getDefaultFps() {
return defaultFps;
}
public boolean isShowFps() {
return showFps;
}
}
CaptureScreen
该CaptureScreen类继承了Thread线程类并重写了run方法。
首先看该类的构造方法,CaptureScreen(),初始化类属性core,连接客户client,主机host,应用app以及端口号port,cursorJob作业和cursorTrigger触发器。然后getScheduler()方法是通过抽象工厂构造方法的类SchedulerFactory来初始化任务调度器_scheduler并返回。release()方法是任务调度器调用shutdown方法来关闭任务调度,等待所有正在执行的任务执行完毕后关闭该任务调度器。
run()方法则是对Job任务调度的初始化以及调度开始,将EncodeJob类和SendJob类建立map映射加入任务调度序列,然后分别添加触发器,调用Scheduler类的start方法,开始任务调度。当该线程开始则自动调用run方法。
public class CaptureScreen extends Thread {
private static final Logger log = getLogger(CaptureScreen.class);
private static final String QUARTZ_CURSOR_TRIGGER_NAME = "CursorTrigger";
private static final String QUARTZ_CURSOR_JOB_NAME = "CursorJob";
private final Core core;
private int timestampDelta;
private volatile AtomicInteger timestamp = new AtomicInteger(0);
private volatile AtomicBoolean sendFrameGuard = new AtomicBoolean(false);
private long startTime = 0;
private volatile boolean active = true;
private IScreenEncoder se;
private IScreenShare client;
private Queue<VideoData> frames = new ArrayBlockingQueue<>(2);
private String host = null;
private String app = null;
private int port = -1;
private Number streamId;
private boolean startPublish = false;
private Scheduler _scheduler;
private final JobDetail cursorJob;
private final Trigger cursorTrigger;
public CaptureScreen(Core coreScreenShare, IScreenShare client, String host, String app, int port) {
core = coreScreenShare;
this.client = client;
this.host = host;
this.app = app;
this.port = port;
cursorJob = JobBuilder.newJob(CursorJob.class).withIdentity(QUARTZ_CURSOR_JOB_NAME, QUARTZ_GROUP_NAME).build();
cursorTrigger = TriggerBuilder.newTrigger()
.withIdentity(QUARTZ_CURSOR_TRIGGER_NAME, QUARTZ_GROUP_NAME)
.withSchedule(simpleSchedule().withIntervalInMilliseconds(1000 / Math.min(5, core.getDim().getFps())).repeatForever())
.build();
cursorJob.getJobDataMap().put(CursorJob.CAPTURE_KEY, this);
}
private Scheduler getScheduler() {
if (_scheduler == null) {
try {
SchedulerFactory schdlrFactory = new StdSchedulerFactory(getQurtzProps("CaptureScreen"));
_scheduler = schdlrFactory.getScheduler();
} catch (SchedulerException e) {
log.error("Unexpected error while creating scheduler", e);
}
}
return _scheduler;
}
public void release() {
try {
if (_scheduler != null) {
_scheduler.shutdown(true);
_scheduler = null;
}
} catch (Exception e) {
log.error("Unexpected error while shutting down scheduler", e);
}
active = false;
timestamp = new AtomicInteger(0);
startTime = 0;
}
@Override
public void run() {
try {
while (active && !core.isReadyToRecord()) {
Thread.sleep(60);
}
timestampDelta = 1000 / core.getDim().getFps();
se = new ScreenV1Encoder(core.getDim()); //send keyframe every 3 seconds
startTime = System.currentTimeMillis();
JobDetail encodeJob = JobBuilder.newJob(EncodeJob.class).withIdentity("EncodeJob", QUARTZ_GROUP_NAME).build();
encodeJob.getJobDataMap().put(EncodeJob.CAPTURE_KEY, this);
Trigger encodeTrigger = TriggerBuilder.newTrigger()
.withIdentity("EncodeTrigger", QUARTZ_GROUP_NAME)
.withSchedule(simpleSchedule().withIntervalInMilliseconds(timestampDelta).repeatForever())
.build();
JobDetail sendJob = JobBuilder.newJob(SendJob.class).withIdentity("SendJob", QUARTZ_GROUP_NAME).build();
Trigger sendTrigger = TriggerBuilder.newTrigger()
.withIdentity("SendTrigger", QUARTZ_GROUP_NAME)
.withSchedule(simpleSchedule().withIntervalInMilliseconds(timestampDelta).repeatForever())
.build();
sendJob.getJobDataMap().put(SendJob.CAPTURE_KEY, this);
Scheduler s = getScheduler();
s.scheduleJob(encodeJob, encodeTrigger);
s.scheduleJob(sendJob, sendTrigger);
s.start();
} catch (Exception e) {
log.error("Error while running: ", e);
}
}
public void pushVideo(VideoData data, int ts) {
if (startPublish) {
if (Red5.getConnectionLocal() == null) {
Red5.setConnectionLocal(client.getConnection());
}
RTMPMessage rtmpMsg = RTMPMessage.build(data, ts);
client.publishStreamData(streamId, rtmpMsg);
}
}
public String getHost() {
return host;
}
public String getApp() {
return app;
}
public int getPort() {
return port;
}
public Number getStreamId() {
return streamId;
}
public void setStreamId(Number streamId) {
this.streamId = streamId;
}
public void setStartPublish(boolean startPublish) {
this.startPublish = startPublish;
}
public IScreenEncoder getEncoder() {
return se;
}
public Queue<VideoData> getFrames() {
return frames;
}
public void setSendFrameGuard(boolean b) {
sendFrameGuard.set(b);
}
public boolean getSendFrameGuard() {
return sendFrameGuard.get();
}
public AtomicInteger getTimestamp() {
return timestamp;
}
public long getStartTime() {
return startTime;
}
public int getTimestampDelta() {
return timestampDelta;
}
public void sendCursorStatus() {
core.sendCursorStatus();
}
public boolean isStarted() throws SchedulerException {
return active && _scheduler != null && _scheduler.isStarted() && !_scheduler.isShutdown();
}
public void setSendCursor(boolean sendCursor) {
try {
Scheduler s = getScheduler();
if (sendCursor) {
s.scheduleJob(cursorJob, cursorTrigger);
} else {
s.deleteJob(JobKey.jobKey(QUARTZ_CURSOR_JOB_NAME, QUARTZ_GROUP_NAME));
}
} catch (SchedulerException e) {
log.error("Unexpected Error schedule/unschedule cursor job", e);
}
}
public ScreenDimensions getDim() {
return core.getDim();
}
}
RTMPClientPublish
RTMPClientPublish类继承了RTMPClient类,实现了IPendingServiceCallback, INetStreamEventHandler, IScreenShare三个接口,构造方法即是对类属性id,core等的初始化,hanleException()方法则是log记录打印error错误,connectionOpened()方法是建立连接,创建流,connectionClosed()方法相反,关闭连接,释放关闭任务调度器进行的任务调度(进行完目前的任务)。resultReceived()方法是IPendingServiceCallback接口里的抽象方法的实现,获取参数里面的方法名,如果为createStream则创建流,设置流id和模式,然后调用start方法开始执行该线程。
class RTMPClientPublish extends RTMPClient implements IPendingServiceCallback, INetStreamEventHandler, IScreenShare {
private static final Logger logger = LoggerFactory.getLogger(RTMPClientPublish.class);
private final CaptureScreen publishScreen;
private String id;
private Core core;
RTMPClientPublish(Core core, String host, String app, String id) {
this.id = id;
this.core = core;
publishScreen = new CaptureScreen(core, this, host, app, 1935);
}
public void setCore(Core core) {
this.core = core;
}
public void connect() {
super.connect(publishScreen.getHost(), 1935, publishScreen.getApp(), this);
}
@Override
public void handleException(Throwable throwable) {
logger.error("ERROR", throwable);
}
@Override
public void connectionOpened(RTMPConnection conn) {
super.connectionOpened(conn);
createStream(this);
}
@Override
public void connectionClosed(RTMPConnection conn) {
super.connectionClosed(conn);
connectionClosed();
}
private void connectionClosed() {
publishScreen.setStartPublish(false);
publishScreen.release();
core.publishingStop();
}
@Override
public void resultReceived(IPendingServiceCall call) {
String method = call == null ? null : call.getServiceMethodName();
logger.trace("call ### get Method Name " + method);
if ("createStream".equals(method)) {
if (call.getResult() != null) {
publishScreen.setStreamId((Integer)call.getResult());
publish(publishScreen.getStreamId(), id, "live", this);
publishScreen.setStartPublish(true);
publishScreen.start();
} else {
connectionClosed();
}
}
}
@Override
public void onStreamEvent(Notify notify) {
//no-op
}
}
RTMPScreenShare
下面的四个类代码几乎一致,RTMPTSScreenShare类继承了RTMPTSClient类,实现了IScreenShare接口。初始化类变量Core类对象core,connectionOpened方法则是建立连接,调用Core类的setDeadlockGuard方法,建立锁机制。connectionClosed方法关闭连接,关闭流。onCommand()方法处理待进行的命令。handleException方法调用core. handleException处理异常。
public class RTMPTSScreenShare extends RTMPTSClient implements IScreenShare {
private static final Logger log = LoggerFactory.getLogger(RTMPTSScreenShare.class);
private final Core core;
public RTMPTSScreenShare(Core core) {
this.core = core;
};
@Override
public void connectionOpened(RTMPConnection conn) {
log.debug("connection opened");
super.connectionOpened(conn);
core.setDeadlockGuard(conn);
}
@Override
public void connectionClosed(RTMPConnection conn) {
log.debug("connection closed");
super.connectionClosed(conn);
if (core.isAudioNotify()) {
AudioTone.play();
}
core.stopStream(null);
}
@Override
protected void onCommand(RTMPConnection conn, Channel channel, Header source, ICommand command) {
super.onCommand(conn, channel, source, command);
core.onCommand(command);
}
@Override
public void handleException(Throwable throwable) {
Throwable cause = throwable.getCause();
log.error("{}", new Object[] { cause });
core.handleException(cause);
}
}
总结
openmeetings-screenshare文件下的所有文件已经分析完毕,该文件主要是对会议进行中屏幕共享这一功能的处理,可以让用户在会议过程中对自己屏幕进行分享,让会议更加的方便。可以查看往期内容。之后将对openmeetings-util文件进行分析。