/** * 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.red5.logging.Red5LoggerFactory; import org.red5.server.adapter.MultiThreadedApplicationAdapter; import org.red5.server.api.IConnection; import org.red5.server.api.Red5; import org.red5.server.api.scope.IScope; import org.red5.server.api.stream.IBroadcastStream; import org.red5.server.api.stream.IServerStream; import org.red5.server.api.stream.IStreamListener; import org.red5.server.stream.ClientBroadcastStream; import org.slf4j.Logger; import com.google.gson.Gson; public class VideoApplication extends MultiThreadedApplicationAdapter { private static Logger log = Red5LoggerFactory.getLogger(VideoApplication.class, "video-broadcast"); private IScope appScope; private IServerStream serverStream; private boolean recordVideoStream = false; private EventRecordingService recordingService; private final Map<String, IStreamListener> streamListeners = new HashMap<String, IStreamListener>(); private int packetTimeout = 10000; @Override public boolean appStart(IScope app) { super.appStart(app); log.info("BBB Video-broadcast appStart"); System.out.println("BBB Video-broadcast appStart"); appScope = app; return true; } @Override public boolean appConnect(IConnection conn, Object[] params) { log.info("BBB Video-broadcast appConnect"); return super.appConnect(conn, params); } @Override public boolean roomConnect(IConnection conn, Object[] params) { log.info("BBB Video-broadcast roomConnect"); return super.roomConnect(conn, params); } private String getConnectionType(String connType) { if ("persistent".equals(connType.toLowerCase())) { return "RTMP"; } else if("polling".equals(connType.toLowerCase())) { return "RTMPT"; } else { return connType.toUpperCase(); } } private String getUserId() { String userid = (String) Red5.getConnectionLocal().getAttribute("USERID"); if ((userid == null) || ("".equals(userid))) userid = "unknown-userid"; return userid; } private String getMeetingId() { String meetingId = (String) Red5.getConnectionLocal().getAttribute("MEETING_ID"); if ((meetingId == null) || ("".equals(meetingId))) meetingId = "unknown-meetingid"; return meetingId; } @Override public void appDisconnect(IConnection conn) { log.info("BBB Video-broadcast appDisconnect"); if (appScope == conn.getScope() && serverStream != null) { serverStream.close(); } String connType = getConnectionType(Red5.getConnectionLocal().getType()); String connId = Red5.getConnectionLocal().getSessionId(); 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("event", "user_leaving_bbb_video_broadcast"); logData.put("description", "User leaving BBB video-broadcast."); Gson gson = new Gson(); String logStr = gson.toJson(logData); log.info("User leaving bbb-video-broadcast: data={}", logStr); super.appDisconnect(conn); } @Override public void roomDisconnect(IConnection conn) { log.info("BBB video-broadcast roomDisconnect"); String connType = getConnectionType(Red5.getConnectionLocal().getType()); String connId = Red5.getConnectionLocal().getSessionId(); 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("event", "user_leaving_bbb_video_broadcast"); logData.put("description", "User leaving BBB video-broadcast."); Gson gson = new Gson(); String logStr = gson.toJson(logData); log.info("User leaving bbb-video-broadcast: data={}", logStr); super.roomDisconnect(conn); } @Override public void streamPublishStart(IBroadcastStream stream) { super.streamPublishStart(stream); IConnection conn = Red5.getConnectionLocal(); log.info("streamPublishStart " + stream.getPublishedName() + " " + System.currentTimeMillis() + " " + conn.getScope().getName()); } @Override public void streamBroadcastStart(IBroadcastStream stream) { IConnection conn = Red5.getConnectionLocal(); super.streamBroadcastStart(stream); log.info("streamBroadcastStart " + stream.getPublishedName() + " " + System.currentTimeMillis() + " " + conn.getScope().getName()); // TODO Anton publish a message to a redis channel so that all [Flash client] users are // notified that there is an rtmp stream being broadcasted. if (recordVideoStream) { recordStream(stream); } } private Long genTimestamp() { return TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); } @Override public void streamBroadcastClose(IBroadcastStream stream) { super.streamBroadcastClose(stream); IConnection conn = Red5.getConnectionLocal(); String scopeName; if (conn != null) { scopeName = conn.getScope().getName(); } else { log.info("Connection local was null, using scope name from the stream: {}", stream); scopeName = stream.getScope().getName(); } log.info("Stream broadcast closed for stream=[{}] meeting=[{}]", stream.getPublishedName(), scopeName); // TODO Anton publish a message to a redis channel so that all [Flash client] users are // notified that there is no longer rtmp stream being broadcasted. String userId = getUserId(); String meetingId = conn.getScope().getName(); String streamId = stream.getPublishedName(); if (recordVideoStream) { long publishDuration = (System.currentTimeMillis() - stream.getCreationTime()) / 1000; log.info("Stop recording event for stream=[{}] meeting=[{}]", stream.getPublishedName(), scopeName); Map<String, String> event = new HashMap<String, String>(); event.put("module", "WEBRTC-DESKSHARE"); event.put("timestamp", genTimestamp().toString()); event.put("meetingId", scopeName); event.put("stream", stream.getPublishedName()); event.put("duration", new Long(publishDuration).toString()); event.put("eventName", "StopWebRTCDesktopShareEvent"); recordingService.record(scopeName, event); } } /** * A hook to record a stream. A file is written in webapps/video-broadcast/streams/ * @param stream */ private void recordStream(IBroadcastStream stream) { IConnection conn = Red5.getConnectionLocal(); long now = System.currentTimeMillis(); String recordingStreamName = stream.getPublishedName(); // + "-" + now; /** Comment out for now...forgot why I added this - ralam */ try { log.info("Recording stream " + recordingStreamName ); ClientBroadcastStream cstream = (ClientBroadcastStream) this.getBroadcastStream(conn.getScope(), stream.getPublishedName()); cstream.saveAs(recordingStreamName, false); } catch(Exception e) { log.error("ERROR while recording stream " + e.getMessage()); e.printStackTrace(); } } public void setRecordVideoStream(boolean recordVideoStream) { this.recordVideoStream = recordVideoStream; } public void setPacketTimeout(int timeout) { this.packetTimeout = timeout; } public void setEventRecordingService(EventRecordingService s) { recordingService = s; } /** * Start transmission notification from Flash Player 11.1+. This command asks the server to transmit more data because the buffer is running low. * * http://help.adobe.com/en_US/flashmediaserver/devguide/WSd391de4d9c7bd609-569139412a3743e78e-8000.html * * @param bool boolean * @param num number */ public void startTransmit(Boolean bool, int num) { } /** * Stop transmission notification from Flash Player 11.1+. This command asks the server to suspend transmission until the client sends a * startTransmit event because there is enough data in the buffer. */ public void stopTransmit() { } /** * Stop transmission notification from Flash Player 11.1+. This command asks the server to suspend transmission until the client sends a * startTransmit event because there is enough data in the buffer. * * @param bool boolean * @param num number */ public void stopTransmit(Boolean bool, int num) { } /** * Notification method that is sent by FME just before publishing starts. * * @param streamName Name of stream that is about to be published. */ @Override public void FCPublish(String streamName) { IConnection conn = Red5.getConnectionLocal(); log.info("FCPublish " + streamName + " " + System.currentTimeMillis() + " " + conn.getScope().getName()); } /** * Notification method that is sent by FME when publishing of a stream ends. */ @Override public void FCUnpublish() { } /** * Notification method that is sent by FME when publishing of a stream ends. * * @param streamName Name of stream that is about to be un-published. */ @Override public void FCUnpublish(String streamName) { IConnection conn = Red5.getConnectionLocal(); log.info("FCUnpublish " + streamName + " " + System.currentTimeMillis() + " " + conn.getScope().getName()); } /** * Notification method that is sent by some clients just before playback starts. * * @param streamName Name of stream that is about to be played. */ @Override public void FCSubscribe(String streamName) { IConnection conn = Red5.getConnectionLocal(); log.info("FCSubscribe " + streamName + " " + System.currentTimeMillis() + " " + conn.getScope().getName()); } }