diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/Meeting.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/Meeting.java index 9a932c938b3d6ba9fcb029610b6d147bb0fad2a9..45e568d758468f393997ff6f685d44a16984b18b 100755 --- a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/Meeting.java +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/Meeting.java @@ -2,8 +2,12 @@ package org.bigbluebutton.app.screenshare; import java.util.HashMap; import java.util.Map; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; public class Meeting { + private static Logger log = Red5LoggerFactory.getLogger(Meeting.class, "screenshare"); + public final String id; private Map<String, VideoStream> videoStreams = new HashMap<String, VideoStream>(); @@ -13,17 +17,21 @@ public class Meeting { } public synchronized void addStream(VideoStream stream) { - videoStreams.put(stream.getStreamId(), stream); + log.debug("Adding VideoStream {} to meeting {}", stream.getStreamId(), id); + videoStreams.put(stream.getStreamId(), stream); } public synchronized void removeStream(String streamId) { - VideoStream vs = videoStreams.remove(streamId); + log.debug("Removing VideoStream {} to meeting {}", streamId, id); + VideoStream vs = videoStreams.remove(streamId); } public synchronized void streamBroadcastClose(String streamId) { - VideoStream vs = videoStreams.remove(streamId); + log.debug("streamBroadcastClose VideoStream {} to meeting {}", streamId, id); + VideoStream vs = videoStreams.remove(streamId); if (vs != null) { - vs.streamBroadcastClose(); + log.debug("Closing VideoStream {} to meeting {}", streamId, id); + vs.streamBroadcastClose(); } } diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/MeetingManager.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/MeetingManager.java index 76861013ed9c7767457fc484986129263e08ce49..2698ad89d8f65d4d4ea3f22795519a761703a6ab 100755 --- a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/MeetingManager.java +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/MeetingManager.java @@ -2,8 +2,11 @@ package org.bigbluebutton.app.screenshare; import java.util.HashMap; import java.util.Map; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; public class MeetingManager { + private static Logger log = Red5LoggerFactory.getLogger(MeetingManager.class, "screenshare"); private Map<String, Meeting> meetings = new HashMap<String, Meeting>(); @@ -16,6 +19,8 @@ public class MeetingManager { } public void addStream(String meetingId, VideoStream vs) { + log.debug("Adding VideoStream {} to meeting {}", vs.getStreamId(), meetingId); + Meeting m = meetings.get(meetingId); if (m != null) { m.addStream(vs); @@ -27,16 +32,22 @@ public class MeetingManager { } public void removeStream(String meetingId, String streamId) { + log.debug("Removing VideoStream {} to meeting {}", streamId, meetingId); Meeting m = meetings.get(meetingId); if (m != null) { + log.debug("Removed VideoStream {} to meeting {}", streamId, meetingId); m.removeStream(streamId); } } public void streamBroadcastClose(String meetingId, String streamId) { - Meeting m = meetings.get(meetingId); + log.debug("streamBroadcastClose VideoStream {} to meeting {}", streamId, meetingId); + + Meeting m = meetings.get(meetingId); if (m != null) { - m.streamBroadcastClose(streamId); + log.debug("streamBroadcastClose 2 VideoStream {} to meeting {}", streamId, meetingId); + + m.streamBroadcastClose(streamId); if (!m.hasVideoStreams()) { remove(m.id); } diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/VideoStream.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/VideoStream.java index 0ff5b30d93b538347494354f922b100d841a16cf..3a3239b2cf3920fe318d9309d981b13b1e9946a9 100755 --- a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/VideoStream.java +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/VideoStream.java @@ -7,6 +7,9 @@ import org.red5.server.api.scope.IScope; import org.red5.server.api.stream.IBroadcastStream; import org.red5.server.stream.ClientBroadcastStream; import org.slf4j.Logger; +import java.util.HashMap; +import java.util.Map; +import com.google.gson.Gson; public class VideoStream { private static Logger log = Red5LoggerFactory.getLogger(VideoStream.class, "screenshare"); @@ -26,7 +29,7 @@ public class VideoStream { } public String getStreamId() { - return streamId; + return stream.getPublishedName(); } public synchronized void startRecording() { @@ -36,19 +39,38 @@ public class VideoStream { log.info("Recording stream " + recordingStreamName); videoStreamListener.setStreamId(recordingStreamName); cstream.saveAs(recordingStreamName, false); + + Map<String, Object> logData2 = new HashMap<String, Object>(); + logData2.put("broadcastStream", stream.getPublishedName()); + logData2.put("recordStreamId", recordingStreamName); + logData2.put("recording", cstream.isRecording()); + logData2.put("event", "start_recording_stream"); + logData2.put("description", "Start recording stream."); + + Gson gson2 = new Gson(); + String logStr2 = gson2.toJson(logData2); + log.info(logStr2); } catch (Exception e) { log.error("ERROR while recording stream " + e.getMessage()); e.printStackTrace(); } } - public synchronized void stopRecording() { - if (cstream.isRecording()) { - cstream.stopRecording(); - videoStreamListener.stopRecording(); - videoStreamListener.reset(); - } - } + public synchronized void stopRecording() { + log.debug("STOP RECORDING STREAM {} recording {}", stream.getPublishedName(), cstream.isRecording()); + Map<String, Object> logData2 = new HashMap<String, Object>(); + logData2.put("broadcastStream", stream.getPublishedName()); + logData2.put("recordStreamId", recordingStreamName); + logData2.put("event", "stop_recording_stream"); + logData2.put("description", "Stop recording stream."); + Gson gson2 = new Gson(); + String logStr2 = gson2.toJson(logData2); + log.info(logStr2); + cstream.stopRecording(); + videoStreamListener.stopRecording(); + videoStreamListener.reset(); + + } public synchronized void stopStartRecording() { stopRecording(); diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/VideoStreamListener.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/VideoStreamListener.java index 49d461c4241d1ac2d81e47c2bd978c3ebe2f4ea2..41468e7ed845331d4706df0ab30a711a6d70f35a 100755 --- a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/VideoStreamListener.java +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/VideoStreamListener.java @@ -34,6 +34,7 @@ import org.slf4j.Logger; import org.red5.logging.Red5LoggerFactory; import com.google.gson.Gson; +import java.text.SimpleDateFormat; /** * Class to listen for the first video packet of the webcam. @@ -51,7 +52,7 @@ import com.google.gson.Gson; * */ public class VideoStreamListener implements IStreamListener { - private static final Logger log = Red5LoggerFactory.getLogger(VideoStreamListener.class, "video"); + private static final Logger log = Red5LoggerFactory.getLogger(VideoStreamListener.class, "screenshare"); private EventRecordingService recordingService; private volatile boolean firstPacketReceived = false; @@ -60,6 +61,7 @@ public class VideoStreamListener implements IStreamListener { private int videoTimeout = 10000; private long firstPacketTime = 0L; private long packetCount = 0L; + private int keyFrameCount = 0; // Last time video was received, not video timestamp private long lastVideoTime; @@ -81,12 +83,16 @@ public class VideoStreamListener implements IStreamListener { private volatile boolean publishing = false; private volatile boolean streamPaused = false; + private volatile boolean streamStarted = false; private String meetingId; private long recordingStartTime; private String filename; + private final String DATE = "date"; + private final String TIMESTAMP_UTC = "timestampUTC"; + public VideoStreamListener(String meetingId, String streamId, Boolean record, String recordingDir, int packetTimeout, QuartzSchedulingService scheduler, @@ -98,6 +104,9 @@ public class VideoStreamListener implements IStreamListener { this.recordingDir = recordingDir; this.scheduler = scheduler; this.recordingService = recordingService; + + // start the worker to monitor if we are still receiving video packets + timeoutJobName = scheduler.addScheduledJob(videoTimeout, new TimeoutJob()); } private Long genTimestamp() { @@ -106,81 +115,112 @@ public class VideoStreamListener implements IStreamListener { public void reset() { firstPacketReceived = false; + keyFrameCount = 0; } public void setStreamId(String streamId) { this.streamId = streamId; } - @Override - public void packetReceived(IBroadcastStream stream, IStreamPacket packet) { - IoBuffer buf = packet.getData(); - if (buf != null) - buf.rewind(); - - if (buf == null || buf.remaining() == 0) { - return; - } - - if (packet instanceof VideoData) { - // keep track of last time video was received - lastVideoTime = System.currentTimeMillis(); - packetCount++; - - if (!firstPacketReceived) { - firstPacketReceived = true; - publishing = true; - firstPacketTime = lastVideoTime; - - // start the worker to monitor if we are still receiving video packets - timeoutJobName = scheduler.addScheduledJob(videoTimeout, new TimeoutJob()); - - if (record) { - recordingStartTime = System.currentTimeMillis(); - filename = recordingDir; - if (!filename.endsWith("/")) { - filename.concat("/"); - } - - filename = filename.concat(meetingId).concat("/").concat(streamId).concat(".flv"); - recordingStartTime = System.currentTimeMillis(); - Map<String, String> event = new HashMap<String, String>(); - event.put("module", "Deskshare"); - event.put("timestamp", genTimestamp().toString()); - event.put("meetingId", meetingId); - event.put("file", filename); - event.put("stream", streamId); - event.put("eventName", "DeskshareStartedEvent"); - - recordingService.record(meetingId, event); - } - } - - - if (streamPaused) { - streamPaused = false; - long now = System.currentTimeMillis(); - long numSeconds = (now - lastVideoTime) / 1000; + @Override + public void packetReceived(IBroadcastStream stream, IStreamPacket packet) { + IoBuffer buf = packet.getData(); + if (buf != null) + buf.rewind(); + + if (buf == null || buf.remaining() == 0) { + return; + } + + if (packet instanceof VideoData) { + // keep track of last time video was received + lastVideoTime = System.currentTimeMillis(); + packetCount++; + + VideoData vidPkt = (VideoData) packet; + + if (!firstPacketReceived && vidPkt.getFrameType() == VideoData.FrameType.KEYFRAME) { + log.info("******* Receiving first screenshare KEYFRAME packet"); + firstPacketReceived = true; + publishing = true; + firstPacketTime = lastVideoTime; + streamStarted = true; + + if (record) { + recordingStartTime = System.currentTimeMillis(); + filename = recordingDir; + if (!filename.endsWith("/")) { + filename.concat("/"); + } + + filename = filename.concat(meetingId).concat("/").concat(streamId).concat(".flv"); + recordingStartTime = System.currentTimeMillis(); + Map<String, String> event = new HashMap<String, String>(); + event.put("module", "Deskshare"); + event.put("timestamp", genTimestamp().toString()); + event.put("meetingId", meetingId); + event.put("file", filename); + event.put("stream", streamId); + event.put(TIMESTAMP_UTC, Long.toString(recordingStartTime)); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX"); + event.put(DATE, sdf.format(recordingStartTime)); + event.put("eventName", "DeskshareStartedEvent"); + + recordingService.record(meetingId, event); + + + Gson gson = new Gson(); + String logStr = gson.toJson(event); + log.info("StartScreenShareEvent data={} timeoutJobName={}", logStr, timeoutJobName); + } + } + + + if (streamPaused) { + streamPaused = false; + long now = System.currentTimeMillis(); + long numSeconds = (now - lastVideoTime) / 1000; + + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("meetingId", meetingId); + logData.put("stream", streamId); + logData.put("packetCount", packetCount); + logData.put("publishing", publishing); + logData.put("pausedFor (sec)", numSeconds); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + + log.warn("Screenshare stream restarted. data={}", logStr); + } + + if (vidPkt.getFrameType() == VideoData.FrameType.KEYFRAME && keyFrameCount < 3) { + // Log first 3 keyframe packets to allow us to see interval between key frames. Helps + // to debug if there are any synch issues with recording playback. (ralam feb 12, 2018) + keyFrameCount++; + long now = System.currentTimeMillis(); + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("meetingId", meetingId); + logData.put("stream", stream.getPublishedName()); + logData.put("packetCount", packetCount); + logData.put("keyFrameCount", keyFrameCount); + logData.put("publishing", publishing); + logData.put(TIMESTAMP_UTC, Long.toString(now)); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX"); + logData.put(DATE, sdf.format(now)); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + log.warn("Video stream keyframe. data={}", logStr); + } + } + } - Map<String, Object> logData = new HashMap<String, Object>(); - logData.put("meetingId", meetingId); - logData.put("stream", streamId); - logData.put("packetCount", packetCount); - logData.put("publishing", publishing); - logData.put("pausedFor (sec)", numSeconds); - - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - - log.warn("Screenshare stream restarted. data={}", logStr); - } - - } - } public void stopRecording() { if (record) { - long publishDuration = (System.currentTimeMillis() - recordingStartTime) / 1000; + long now = System.currentTimeMillis(); + long publishDuration = (now - recordingStartTime) / 1000; Map<String, String> event = new HashMap<String, String>(); event.put("module", "Deskshare"); @@ -189,13 +229,34 @@ public class VideoStreamListener implements IStreamListener { event.put("stream", streamId); event.put("file", filename); event.put("duration", new Long(publishDuration).toString()); - event.put("eventName", "DeskshareStoppedEvent"); + event.put(TIMESTAMP_UTC, Long.toString(now)); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX"); + event.put(DATE, sdf.format(now)); + event.put("eventName", "DeskshareStoppedEvent"); recordingService.record(meetingId, event); + + Gson gson = new Gson(); + String logStr = gson.toJson(event); + log.info("StopScreenShareEvent data={}", logStr); } } - public void streamStopped() { - this.publishing = false; + public void streamStopped() { + this.publishing = false; + if (!streamStarted) { + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("meetingId", meetingId); + logData.put("stream", streamId); + logData.put("packetCount", packetCount); + logData.put("publishing", publishing); + logData.put("timeoutJobName", timeoutJobName); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + log.warn("Removing scheduled job.as stream hasn't started. data={}", logStr); + // remove the scheduled job + scheduler.removeScheduledJob(timeoutJobName); + } } private class TimeoutJob implements IScheduledJob { @@ -207,11 +268,12 @@ public class VideoStreamListener implements IStreamListener { logData.put("stream", streamId); logData.put("packetCount", packetCount); logData.put("publishing", publishing); + logData.put("timeoutJobName", timeoutJobName); Gson gson = new Gson(); long now = System.currentTimeMillis(); - if ((now - lastVideoTime) > videoTimeout && !streamPaused) { + if ((now - lastVideoTime) > videoTimeout && !streamPaused && streamStarted) { streamPaused = true; long numSeconds = (now - lastVideoTime) / 1000; @@ -224,7 +286,7 @@ public class VideoStreamListener implements IStreamListener { } String logStr = gson.toJson(logData); - if (!publishing) { + if (!publishing && streamStarted) { log.warn("Removing scheduled job. data={}", logStr); // remove the scheduled job scheduler.removeScheduledJob(timeoutJobName); diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppAdapter.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppAdapter.java index b55fb42fc524eb3709a417edf6b74ec83b1c747e..67df6a499085eb8fd0527a18bf64d2c679fb528a 100755 --- a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppAdapter.java +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppAdapter.java @@ -151,8 +151,24 @@ public class Red5AppAdapter extends MultiThreadedApplicationAdapter { String connId = conn.getSessionId(); String scopeName = stream.getScope().getName(); + String connType = getConnectionType(Red5.getConnectionLocal().getType()); String streamId = stream.getPublishedName(); + + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("meetingId", getMeetingId()); + logData.put("userId", getUserId()); + logData.put("connType", connType); + logData.put("connId", connId); + logData.put("stream", stream.getPublishedName()); + logData.put("context", scopeName); + logData.put("event", "stream_broadcast_start"); + logData.put("description", "Stream broadcast start."); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + log.info(logStr); + Matcher matcher = STREAM_ID_PATTERN.matcher(stream.getPublishedName()); if (matcher.matches()) { String meetingId = matcher.group(1).trim(); @@ -162,25 +178,33 @@ public class Red5AppAdapter extends MultiThreadedApplicationAdapter { app.authorizeBroadcastStream(meetingId, streamId, connId, scopeName); boolean recordVideoStream = app.recordStream(meetingId, streamId); - VideoStreamListener listener = new VideoStreamListener(meetingId, streamId, - recordVideoStream, recordingDirectory, packetTimeout, scheduler, recordingService); - ClientBroadcastStream cstream = (ClientBroadcastStream) this.getBroadcastStream(conn.getScope(), stream.getPublishedName()); - stream.addStreamListener(listener); - VideoStream vstream = new VideoStream(stream, listener, cstream); - vstream.startRecording(); - - meetingManager.addStream(meetingId, vstream); + if (recordVideoStream) { + Map<String, Object> logData2 = new HashMap<String, Object>(); + logData2.put("meetingId", meetingId); + logData2.put("connType", connType); + logData2.put("connId", connId); + logData.put("streamId", streamId); + logData.put("url", url); + logData.put("recorded", recordVideoStream); + logData2.put("context", scopeName); + logData2.put("event", "stream_broadcast_record_start"); + logData2.put("description", "Stream broadcast record start."); + + Gson gson2 = new Gson(); + String logStr2 = gson2.toJson(logData2); + log.info(logStr2); + + VideoStreamListener listener = new VideoStreamListener(meetingId, streamId, + recordVideoStream, recordingDirectory, packetTimeout, scheduler, recordingService); + ClientBroadcastStream cstream = (ClientBroadcastStream) this.getBroadcastStream(conn.getScope(), stream.getPublishedName()); + stream.addStreamListener(listener); + VideoStream vstream = new VideoStream(stream, listener, cstream); + vstream.startRecording(); + + meetingManager.addStream(meetingId, vstream); + + } - Map<String, Object> logData = new HashMap<String, Object>(); - logData.put("meetingId", meetingId); - logData.put("streamId", streamId); - logData.put("url", url); - logData.put("recorded", recordVideoStream); - - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - - log.info("ScreenShare broadcast started: data={}", logStr); } else { log.error("Invalid streamid format [{}]", streamId); conn.close(); @@ -195,8 +219,11 @@ public class Red5AppAdapter extends MultiThreadedApplicationAdapter { public void streamBroadcastClose(IBroadcastStream stream) { super.streamBroadcastClose(stream); - log.info("streamBroadcastStop " + stream.getPublishedName() + "]"); - String streamId = stream.getPublishedName(); + String connType = getConnectionType(Red5.getConnectionLocal().getType()); + String connId = Red5.getConnectionLocal().getSessionId(); + String scopeName = stream.getScope().getName(); + + String streamId = stream.getPublishedName(); Matcher matcher = STREAM_ID_PATTERN.matcher(stream.getPublishedName()); if (matcher.matches()) { String meetingId = matcher.group(1).trim(); @@ -206,15 +233,18 @@ public class Red5AppAdapter extends MultiThreadedApplicationAdapter { meetingManager.streamBroadcastClose(meetingId, streamId); - Map<String, Object> logData = new HashMap<String, Object>(); - logData.put("meetingId", meetingId); - logData.put("streamId", streamId); - logData.put("recorded", recordVideoStream); - - Gson gson = new Gson(); - String logStr = gson.toJson(logData); + Map<String, Object> logData2 = new HashMap<String, Object>(); + logData2.put("meetingId", meetingId); + logData2.put("connType", connType); + logData2.put("connId", connId); + logData2.put("stream", stream.getPublishedName()); + logData2.put("context", scopeName); + logData2.put("event", "stream_broadcast_close"); + logData2.put("description", "Stream broadcast close."); - log.info("ScreenShare broadcast stopped: data={}", logStr); + Gson gson2 = new Gson(); + String logStr2 = gson2.toJson(logData2); + log.info(logStr2); } else { log.error("Invalid streamid format [{}]", streamId); }