/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2015 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.app.videobroadcast;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.mina.core.buffer.IoBuffer;
import org.red5.server.api.scheduling.IScheduledJob;
import org.red5.server.api.scheduling.ISchedulingService;
import org.red5.server.api.scope.IScope;
import org.red5.server.api.stream.IBroadcastStream;
import org.red5.server.api.stream.IStreamListener;
import org.red5.server.api.stream.IStreamPacket;
import org.red5.server.net.rtmp.event.VideoData;
import org.red5.server.scheduling.QuartzSchedulingService;
import org.slf4j.Logger;
import org.red5.logging.Red5LoggerFactory;
import com.google.gson.Gson;
/**
* Class to listen for the first video packet of the webcam.
* We need to listen for the first packet and send a startWebcamEvent.
* The reason is that when starting the webcam, sometimes Flash Player
* needs to prompt the user for permission to access the webcam. However,
* while waiting for the user to click OK to the prompt, Red5 has already
* called the startBroadcast method which we take as the start of the recording.
* When the user finally clicks OK, the packets then start to flow through.
* This introduces a delay of when we assume the start of the recording and
* the webcam actually publishes video packets. When we do the ingest and
* processing of the video and multiplex the audio, the video and audio will
* be un-synched by at least this amount of delay.
* @author Richard Alam
*
*/
public class VideoStreamListener implements IStreamListener {
private static final Logger log = Red5LoggerFactory.getLogger(VideoStreamListener.class, "video-broadcast");
private EventRecordingService recordingService;
private volatile boolean firstPacketReceived = false;
// Maximum time between video packets
private int videoTimeout = 10000;
private long firstPacketTime = 0L;
private long packetCount = 0L;
// Last time video was received, not video timestamp
private long lastVideoTime;
private String userId;
// Stream being observed
private IBroadcastStream stream;
// if this stream is recorded or not
private boolean record;
// Scheduler
private QuartzSchedulingService scheduler;
// Event queue worker job name
private String timeoutJobName;
private volatile boolean publishing = false;
private volatile boolean streamPaused = false;
private IScope scope;
public VideoStreamListener(IScope scope, IBroadcastStream stream, Boolean record, String userId, int packetTimeout) {
this.scope = scope;
this.stream = stream;
this.record = record;
this.videoTimeout = packetTimeout;
this.userId = userId;
// get the scheduler
scheduler = (QuartzSchedulingService) scope.getParent().getContext().getBean(QuartzSchedulingService.BEAN_NAME);
}
private Long genTimestamp() {
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
}
@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) {
Map<String, String> event = new HashMap<String, String>();
event.put("module", "WEBCAM");
event.put("timestamp", genTimestamp().toString());
event.put("meetingId", scope.getName());
event.put("stream", stream.getPublishedName());
event.put("eventName", "StartWebRTCDeskShareEvent");
recordingService.record(scope.getName(), event);
}
}
if (streamPaused) {
streamPaused = false;
long now = System.currentTimeMillis();
long numSeconds = (now - lastVideoTime)/1000;
Map<String, Object> logData = new HashMap<String, Object>();
logData.put("meetingId", scope.getName());
logData.put("userId", userId);
logData.put("stream", stream.getPublishedName());
logData.put("packetCount", packetCount);
logData.put("publishing", publishing);
logData.put("pausedFor (sec)", numSeconds);
Gson gson = new Gson();
String logStr = gson.toJson(logData);
log.warn("Video stream restarted. data={}", logStr );
}
}
}
public void setEventRecordingService(EventRecordingService s) {
recordingService = s;
}
public void streamStopped() {
this.publishing = false;
}
private class TimeoutJob implements IScheduledJob {
private boolean streamStopped = false;
public void execute(ISchedulingService service) {
Map<String, Object> logData = new HashMap<String, Object>();
logData.put("meetingId", scope.getName());
logData.put("userId", userId);
logData.put("stream", stream.getPublishedName());
logData.put("packetCount", packetCount);
logData.put("publishing", publishing);
Gson gson = new Gson();
long now = System.currentTimeMillis();
if ((now - lastVideoTime) > videoTimeout && !streamPaused) {
streamPaused = true;
long numSeconds = (now - lastVideoTime)/1000;
logData.put("lastPacketTime (sec)", numSeconds);
String logStr = gson.toJson(logData);
log.warn("Video packet timeout. data={}", logStr );
/*
if (!streamStopped) {
streamStopped = true;
// remove the scheduled job
scheduler.removeScheduledJob(timeoutJobName);
// stop / clean up
if (publishing) {
stream.stop();
}
}
*/
}
String logStr = gson.toJson(logData);
if (!publishing) {
log.warn("Removing scheduled job. data={}", logStr );
// remove the scheduled job
scheduler.removeScheduledJob(timeoutJobName);
}
}
}
}